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