Skip to main content

dotloom_engine/
scene_build.rs

1//! Scene output: incremental [`SceneDelta`]s for renderers.
2//!
3//! Colors equal to `0` mean "theme default" (foreground for strokes and text);
4//! renderers substitute their theme color, so documents stay theme-independent.
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use dotloom_document::{Color, Entity, EntityId};
9use dotloom_scene::{Primitive, SceneDelta, SceneItem, Stroke, flags};
10
11use crate::{
12    Engine,
13    eval::{Drawable, Evaluated},
14    registry::PrimStyle,
15};
16
17/// Theme-default color marker.
18pub const THEME_DEFAULT: u32 = 0;
19
20/// Pending scene changes.
21#[derive(Debug, Clone, Default)]
22pub struct SceneState {
23    /// Entities to (re)emit.
24    pub dirty: BTreeSet<EntityId>,
25    /// Entities to remove.
26    pub removed: BTreeSet<EntityId>,
27    /// Emit everything.
28    pub reset: bool,
29    /// Draw order changed.
30    pub order_changed: bool,
31    /// Entities currently shown with preview content.
32    pub preview: BTreeSet<EntityId>,
33}
34
35fn rgba(c: Option<Color>) -> Option<u32> {
36    c.map(|c| c.0)
37}
38
39/// Convert evaluated drawables of an entity into scene primitives.
40pub(crate) fn primitives(e: &Entity, layer_color: Option<Color>, ev: &Evaluated) -> Vec<Primitive> {
41    let entity_stroke = rgba(e.style.stroke).or(rgba(layer_color));
42    let width = e.style.stroke_width.unwrap_or(1.0) as f32;
43    let dash: Vec<f32> = e.style.dash.as_ref().map(|d| d.iter().map(|x| *x as f32).collect()).unwrap_or_default();
44    let stroke_for = |st: &PrimStyle| -> Option<Stroke> {
45        if st.no_stroke {
46            return None;
47        }
48        Some(Stroke {
49            color: rgba(st.stroke).or(entity_stroke).unwrap_or(THEME_DEFAULT),
50            width: st.width.map_or(width, |w| w as f32),
51            dash: st.dash.as_ref().map_or_else(|| dash.clone(), |d| d.iter().map(|x| *x as f32).collect()),
52        })
53    };
54    ev.drawables
55        .iter()
56        .map(|d| match d {
57            Drawable::Shape(s, st) => {
58                let fill = if s.is_region() { rgba(st.fill).or(rgba(e.style.fill)) } else { None };
59                Primitive::Shape { shape: s.clone(), stroke: stroke_for(st), fill }
60            }
61            Drawable::Text(t, st) => {
62                Primitive::Text { text: t.clone(), color: rgba(st.stroke).or(entity_stroke).unwrap_or(THEME_DEFAULT) }
63            }
64            Drawable::Arrow { tip, direction, size } => Primitive::Arrow {
65                tip: *tip,
66                direction: *direction,
67                size: *size,
68                color: entity_stroke.unwrap_or(THEME_DEFAULT),
69            },
70        })
71        .collect()
72}
73
74impl Engine {
75    fn layer_index(&self) -> BTreeMap<dotloom_document::LayerId, (u32, bool, bool, Option<Color>)> {
76        self.doc
77            .layers()
78            .iter()
79            .enumerate()
80            .map(|(i, l)| (l.id, (u32::try_from(i).unwrap_or(u32::MAX), l.visible, l.locked, l.color)))
81            .collect()
82    }
83
84    pub(crate) fn scene_item(
85        &mut self,
86        id: EntityId,
87        preview: Option<&Evaluated>,
88        preview_entity: Option<&Entity>,
89    ) -> Option<SceneItem> {
90        let layers = self.layer_index();
91        let e = preview_entity.cloned().or_else(|| self.doc.entity(id).cloned())?;
92        let (layer, visible, layer_locked, layer_color) = layers.get(&e.layer).copied()?;
93        if e.hidden || !visible {
94            return None;
95        }
96        let ev = match preview {
97            Some(p) => p.clone(),
98            None => {
99                let ctx = crate::eval::Ctx { view: &self.doc, registry: &self.registry };
100                self.cache.get(ctx, id).clone()
101            }
102        };
103        let mut f = 0u8;
104        if self.selection.contains(&id) {
105            f |= flags::SELECTED;
106        }
107        if preview.is_some() {
108            f |= flags::PREVIEW;
109        }
110        if e.locked || layer_locked {
111            f |= flags::LOCKED;
112        }
113        if ev.read_only.is_some() {
114            f |= flags::READONLY;
115        }
116        if ev.error.is_some() {
117            f |= flags::PROBLEM;
118        }
119        Some(SceneItem { id: id.0, layer, bbox: ev.bbox, flags: f, prims: primitives(&e, layer_color, &ev) })
120    }
121
122    /// Changes since the last call (or the whole scene after load/reset).
123    pub fn take_scene_delta(&mut self) -> SceneDelta {
124        let reset = core::mem::take(&mut self.scene.reset);
125        let mut delta = SceneDelta { revision: self.revision, reset, ..SceneDelta::default() };
126        let ids: Vec<EntityId> = if reset {
127            self.scene.dirty.clear();
128            self.scene.removed.clear();
129            self.scene.preview.clear();
130            self.doc.order().to_vec()
131        } else {
132            let mut d: Vec<EntityId> = core::mem::take(&mut self.scene.dirty).into_iter().collect();
133            d.extend(core::mem::take(&mut self.scene.preview));
134            d.sort_unstable();
135            d.dedup();
136            d
137        };
138        for id in core::mem::take(&mut self.scene.removed) {
139            if self.doc.entity(id).is_none() {
140                delta.removals.push(id.0);
141            }
142        }
143        for id in ids {
144            match self.scene_item(id, None, None) {
145                Some(item) => delta.upserts.push(item),
146                None => delta.removals.push(id.0),
147            }
148        }
149        let order_changed = core::mem::take(&mut self.scene.order_changed);
150        if reset || order_changed {
151            delta.order = Some(self.doc.order().iter().map(|id| id.0).collect());
152        }
153        delta.removals.sort_unstable();
154        delta.removals.dedup();
155        delta
156    }
157
158    /// The complete scene.
159    pub fn full_scene(&mut self) -> SceneDelta {
160        self.scene.reset = true;
161        self.take_scene_delta()
162    }
163}