Skip to main content

brep_render/engine_state/
construction_datums.rs

1use super::*;
2
3/// The calm base color of an unselected construction datum/plane (a soft blue).
4const DATUM_PLANE_COLOR: &str = "#6b8fd0";
5/// The selection accent for a selected datum/plane (matches `faceSelectedColor`).
6const DATUM_PLANE_SELECTED_COLOR: &str = "#ffc400";
7/// The HOVER accent for a datum/plane under the pointer (matches the faces'
8/// default `hoverColor`) — distinct from the selection accent, so a plane reads
9/// hovered-vs-selected exactly like a face does.
10const DATUM_PLANE_HOVERED_COLOR: &str = "#fbff00";
11
12impl EngineState {
13    /// Map every feature id at the CURRENT rollback (`0..=rollback`) to its TYPE
14    /// token — the lookup that classifies a frame name's producing feature so only
15    /// DATUM (`"D"`) / PLANE (`"P"`) frames display as datum planes (a SKETCH `"S"`
16    /// frame renders as curves, not a datum).
17    fn feature_type_map(&self) -> HashMap<String, String> {
18        let rollback = self.history.rollback();
19        let mut map = HashMap::new();
20        for index in 0..=rollback {
21            if let (Some(id), Some(ty)) =
22                (self.history.feature_id(index), self.history.feature_type(index))
23            {
24                map.insert(id, ty);
25            }
26        }
27        map
28    }
29
30    /// Classify a plane-frame NAME against the feature type map: strip a trailing
31    /// DATUM sub-plane suffix (`:XY`/`:XZ`/`:YZ`) to the producing feature id, look
32    /// up its type, and keep only `"D"`/`"P"` producers. Returns `(producing
33    /// feature id, feature type)` for a datum/plane frame, else `None` (a SKETCH
34    /// `"S"` frame, or a feature past the rollback / not in the history).
35    fn datum_feature_of(
36        name: &str,
37        type_map: &HashMap<String, String>,
38    ) -> Option<(String, String)> {
39        let base = [":XY", ":XZ", ":YZ"]
40            .iter()
41            .find_map(|suffix| name.strip_suffix(suffix))
42            .unwrap_or(name);
43        let ty = type_map.get(base)?;
44        if ty == "D" || ty == "P" {
45            Some((base.to_string(), ty.clone()))
46        } else {
47            None
48        }
49    }
50
51    /// The construction datum/plane frame NAMES the last run resolved, filtered to
52    /// the D/P producing features at the current rollback, in run order. Every
53    /// DATUM contributes three (`{id}:XY|XZ|YZ`), every PLANE one (`{id}`); a
54    /// SKETCH's own plane frame is excluded (it renders as curves).
55    fn construction_datum_names(&self) -> Vec<String> {
56        let type_map = self.feature_type_map();
57        self.construction_frames
58            .iter()
59            .filter(|(name, _)| Self::datum_feature_of(name, &type_map).is_some())
60            .map(|(name, _)| name.clone())
61            .collect()
62    }
63
64    /// The producing `(feature id, feature type)` of a datum/plane frame NAME, but
65    /// ONLY when the name is an actually-resolved D/P frame at the current rollback
66    /// — the provenance the Properties Info tab reports for a selected datum.
67    pub fn datum_feature_for_name(&self, name: &str) -> Option<(String, String)> {
68        if !self.construction_frames.iter().any(|(n, _)| n == name) {
69            return None;
70        }
71        Self::datum_feature_of(name, &self.feature_type_map())
72    }
73
74    /// (Re)build the persistent construction datum/plane overlays. Feeds every D/P
75    /// frame the last run resolved (minus [`hidden_datums`]) to the datum-plane
76    /// widget channel as a screen-constant NAMED plane in the calm datum color — or
77    /// the selection accent when it is in `emphasis.selected_datums` / hovered when
78    /// it is in `emphasis.hovered_datums` (hover wins, the `EmphasisState` order).
79    /// The feed
80    /// REPLACES the widget's datum set wholesale, so a departed/hidden/rolled-back
81    /// plane is auto-dropped; `shown_datum_names` mirrors what was fed. Marks dirty.
82    ///
83    /// The fed set is also exactly what is PICKABLE: `plane_candidates_at` hit-tests
84    /// these cards, so a hidden or rolled-back plane can no more be picked than it
85    /// can be seen.
86    pub fn refresh_construction_datums(&mut self) {
87        let type_map = self.feature_type_map();
88        let mut planes: Vec<serde_json::Value> = Vec::new();
89        let mut fed: Vec<String> = Vec::new();
90        for (name, frame) in &self.construction_frames {
91            if Self::datum_feature_of(name, &type_map).is_none() {
92                continue;
93            }
94            if self.hidden_datums.contains(name) {
95                continue;
96            }
97            let selected = self.emphasis.selected_datums.contains(name);
98            let hovered = self.emphasis.hovered_datums.contains(name);
99            // Hover WINS over selected — the `EmphasisState` order faces follow.
100            let color = if hovered {
101                DATUM_PLANE_HOVERED_COLOR
102            } else if selected {
103                DATUM_PLANE_SELECTED_COLOR
104            } else {
105                DATUM_PLANE_COLOR
106            };
107            planes.push(serde_json::json!({
108                "name": name,
109                "origin": [frame.origin.x, frame.origin.y, frame.origin.z],
110                "x": [frame.x_axis.x, frame.x_axis.y, frame.x_axis.z],
111                "y": [frame.y_axis.x, frame.y_axis.y, frame.y_axis.z],
112                "color": color,
113                "selected": selected,
114                "hovered": hovered,
115            }));
116            fed.push(name.clone());
117        }
118        // `set_datums` replaces its whole datum set, so a full re-feed each call
119        // drops any plane no longer present (rolled back / deleted / hidden).
120        let payload = serde_json::json!({ "planes": planes }).to_string();
121        let _ = self.set_datums_json(&payload);
122        self.shown_datum_names = fed;
123        self.dirty = true;
124    }
125
126    /// Whether the construction datum/plane `name`'s plane is shown (absent from
127    /// [`hidden_datums`] = visible).
128    pub fn datum_visible(&self, name: &str) -> bool {
129        !self.hidden_datums.contains(name)
130    }
131
132    /// Show/hide the construction datum/plane `name`'s plane (the Scene-tree
133    /// checkbox). Toggles [`hidden_datums`] and re-feeds the datum planes so the
134    /// plane appears/disappears immediately.
135    pub fn set_datum_visible(&mut self, name: &str, visible: bool) {
136        if visible {
137            self.hidden_datums.remove(name);
138        } else {
139            self.hidden_datums.insert(name.to_string());
140        }
141        self.refresh_construction_datums();
142    }
143
144    /// The construction datums/planes to list in the Scene tree: every D/P frame at
145    /// the current rollback, each with its live visibility (hidden ones included,
146    /// like [`committed_sketches`](Self::committed_sketches)).
147    pub fn construction_datums(&self) -> Vec<(String, bool)> {
148        crate::visibility::named_visibility(self.construction_datum_names(), &self.hidden_datums)
149    }
150
151    /// The construction datums/planes as JSON (`[{"name","visible"}]`) — the datum
152    /// sibling of [`sketch_entities_json`](Self::sketch_entities_json) the Scene
153    /// panel publishes (`__brepDatums`) for the headed verifier.
154    pub fn datum_entities_json(&self) -> String {
155        crate::visibility::named_visibility_json(self.construction_datums())
156    }
157
158    /// Select a construction datum/plane by frame NAME (replacing the whole
159    /// selection): a Scene-tree row click or a viewport datum pick. Only a name
160    /// that is an actually-resolved D/P frame at the current rollback selects;
161    /// others return false without changing the selection. Re-feeds the datum
162    /// planes so the selected one shows the accent, and bumps the generation.
163    pub fn select_datum(&mut self, name: &str) -> bool {
164        if name.is_empty() || !self.construction_frames.iter().any(|(n, _)| n == name) {
165            return false;
166        }
167        if Self::datum_feature_of(name, &self.feature_type_map()).is_none() {
168            return false;
169        }
170        self.emphasis.selected_solids.clear();
171        self.emphasis.selected_faces.clear();
172        self.emphasis.selected_edges.clear();
173        self.emphasis.selected_vertices.clear();
174        self.emphasis.selected_datums.clear();
175        self.emphasis.selected_datums.insert(name.to_string());
176        self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
177        self.refresh_construction_datums();
178        self.dirty = true;
179        true
180    }
181}
182
183
184// BREP private tests: 28fa92dc5a877890
185