brep_render/engine_state/
construction_datums.rs1use super::*;
2
3const DATUM_PLANE_COLOR: &str = "#6b8fd0";
5const DATUM_PLANE_SELECTED_COLOR: &str = "#ffc400";
7
8impl EngineState {
9 fn feature_type_map(&self) -> HashMap<String, String> {
14 let rollback = self.history.rollback();
15 let mut map = HashMap::new();
16 for index in 0..=rollback {
17 if let (Some(id), Some(ty)) =
18 (self.history.feature_id(index), self.history.feature_type(index))
19 {
20 map.insert(id, ty);
21 }
22 }
23 map
24 }
25
26 fn datum_feature_of(
32 name: &str,
33 type_map: &HashMap<String, String>,
34 ) -> Option<(String, String)> {
35 let base = [":XY", ":XZ", ":YZ"]
36 .iter()
37 .find_map(|suffix| name.strip_suffix(suffix))
38 .unwrap_or(name);
39 let ty = type_map.get(base)?;
40 if ty == "D" || ty == "P" {
41 Some((base.to_string(), ty.clone()))
42 } else {
43 None
44 }
45 }
46
47 fn construction_datum_names(&self) -> Vec<String> {
52 let type_map = self.feature_type_map();
53 self.construction_frames
54 .iter()
55 .filter(|(name, _)| Self::datum_feature_of(name, &type_map).is_some())
56 .map(|(name, _)| name.clone())
57 .collect()
58 }
59
60 pub fn datum_feature_for_name(&self, name: &str) -> Option<(String, String)> {
64 if !self.construction_frames.iter().any(|(n, _)| n == name) {
65 return None;
66 }
67 Self::datum_feature_of(name, &self.feature_type_map())
68 }
69
70 pub fn refresh_construction_datums(&mut self) {
77 let type_map = self.feature_type_map();
78 let mut planes: Vec<serde_json::Value> = Vec::new();
79 let mut fed: Vec<String> = Vec::new();
80 for (name, frame) in &self.construction_frames {
81 if Self::datum_feature_of(name, &type_map).is_none() {
82 continue;
83 }
84 if self.hidden_datums.contains(name) {
85 continue;
86 }
87 let selected = self.emphasis.selected_datums.contains(name);
88 let color = if selected {
89 DATUM_PLANE_SELECTED_COLOR
90 } else {
91 DATUM_PLANE_COLOR
92 };
93 planes.push(serde_json::json!({
94 "name": name,
95 "origin": [frame.origin.x, frame.origin.y, frame.origin.z],
96 "x": [frame.x_axis.x, frame.x_axis.y, frame.x_axis.z],
97 "y": [frame.y_axis.x, frame.y_axis.y, frame.y_axis.z],
98 "color": color,
99 "selected": selected,
100 }));
101 fed.push(name.clone());
102 }
103 let payload = serde_json::json!({ "planes": planes }).to_string();
106 let _ = self.set_datums_json(&payload);
107 self.shown_datum_names = fed;
108 self.dirty = true;
109 }
110
111 pub fn datum_visible(&self, name: &str) -> bool {
114 !self.hidden_datums.contains(name)
115 }
116
117 pub fn set_datum_visible(&mut self, name: &str, visible: bool) {
121 if visible {
122 self.hidden_datums.remove(name);
123 } else {
124 self.hidden_datums.insert(name.to_string());
125 }
126 self.refresh_construction_datums();
127 }
128
129 pub fn construction_datums(&self) -> Vec<(String, bool)> {
133 self.construction_datum_names()
134 .into_iter()
135 .map(|name| {
136 let visible = !self.hidden_datums.contains(&name);
137 (name, visible)
138 })
139 .collect()
140 }
141
142 pub fn datum_entities_json(&self) -> String {
146 let list: Vec<serde_json::Value> = self
147 .construction_datums()
148 .into_iter()
149 .map(|(name, visible)| serde_json::json!({ "name": name, "visible": visible }))
150 .collect();
151 serde_json::Value::Array(list).to_string()
152 }
153
154 pub fn select_datum(&mut self, name: &str) -> bool {
160 if name.is_empty() || !self.construction_frames.iter().any(|(n, _)| n == name) {
161 return false;
162 }
163 if Self::datum_feature_of(name, &self.feature_type_map()).is_none() {
164 return false;
165 }
166 self.emphasis.selected_solids.clear();
167 self.emphasis.selected_faces.clear();
168 self.emphasis.selected_edges.clear();
169 self.emphasis.selected_vertices.clear();
170 self.emphasis.selected_datums.clear();
171 self.emphasis.selected_datums.insert(name.to_string());
172 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
173 self.refresh_construction_datums();
174 self.dirty = true;
175 true
176 }
177}
178
179
180#[cfg(test)]
184mod construction_datum_tests {
185 use super::*;
186
187 fn cube_and_datum_history() -> String {
190 serde_json::json!({
191 "features": [
192 {
193 "type": "P.CU",
194 "inputParams": {
195 "id": "Box",
196 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
197 "transform": {
198 "position": [0.0, 0.0, 0.0],
199 "rotationEuler": [0.0, 0.0, 0.0],
200 "scale": [1.0, 1.0, 1.0]
201 },
202 "boolean": { "targets": [], "operation": "NONE" }
203 },
204 "persistentData": {}
205 },
206 {
207 "type": "D",
208 "inputParams": { "id": "Datum" },
209 "persistentData": {}
210 }
211 ]
212 })
213 .to_string()
214 }
215
216 fn plane_history() -> String {
218 serde_json::json!({
219 "features": [{
220 "type": "P",
221 "inputParams": { "id": "Pl", "orientation": "XZ", "offset_distance": 3.0 },
222 "persistentData": {}
223 }]
224 })
225 .to_string()
226 }
227
228 fn datum_and_sketch_history() -> String {
232 serde_json::json!({
233 "features": [
234 {
235 "type": "D",
236 "inputParams": { "id": "Datum" },
237 "persistentData": {}
238 },
239 {
240 "type": "S",
241 "inputParams": { "id": "Sk" },
242 "persistentData": {
243 "basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
244 "sketch": {
245 "points": [
246 { "id": 0, "x": 0.0, "y": 0.0 },
247 { "id": 1, "x": 10.0, "y": 0.0 },
248 { "id": 2, "x": 10.0, "y": 6.0 },
249 { "id": 3, "x": 0.0, "y": 6.0 }
250 ],
251 "geometries": [
252 { "id": 10, "type": "line", "points": [0, 1] },
253 { "id": 11, "type": "line", "points": [1, 2] },
254 { "id": 12, "type": "line", "points": [2, 3] },
255 { "id": 13, "type": "line", "points": [3, 0] }
256 ],
257 "constraints": []
258 }
259 }
260 }
261 ]
262 })
263 .to_string()
264 }
265
266 fn fed_datum_names(engine: &EngineState) -> Vec<String> {
269 let mut names: Vec<String> = engine
270 .widgets
271 .datum_plane_names()
272 .iter()
273 .map(|(n, _)| n.to_string())
274 .collect();
275 names.sort();
276 names
277 }
278
279 #[test]
280 fn datum_feature_feeds_three_named_planes_and_lists_them() {
281 let mut engine = EngineState::new();
282 engine.set_history_json(&cube_and_datum_history()).unwrap();
283
284 assert_eq!(
286 fed_datum_names(&engine),
287 vec!["Datum:XY".to_string(), "Datum:XZ".to_string(), "Datum:YZ".to_string()]
288 );
289
290 let listed = engine.construction_datums();
293 assert_eq!(listed.len(), 3);
294 assert!(listed.iter().all(|(_, visible)| *visible));
295 let json: serde_json::Value =
296 serde_json::from_str(&engine.datum_entities_json()).unwrap();
297 assert_eq!(json.as_array().unwrap().len(), 3);
298 assert!(json.as_array().unwrap().iter().any(|d| d["name"] == "Datum:XY"));
299
300 let solids: serde_json::Value =
302 serde_json::from_str(&engine.scene_entities_json()).unwrap();
303 assert_eq!(solids.as_array().unwrap().len(), 1);
304 assert_eq!(solids[0]["name"], "Box");
305 }
306
307 #[test]
308 fn plane_feature_feeds_and_lists_its_single_frame() {
309 let mut engine = EngineState::new();
310 engine.set_history_json(&plane_history()).unwrap();
311 assert_eq!(fed_datum_names(&engine), vec!["Pl".to_string()]);
312 assert_eq!(engine.construction_datums(), vec![("Pl".to_string(), true)]);
313 }
314
315 #[test]
316 fn sketch_frame_is_not_fed_as_a_datum() {
317 let mut engine = EngineState::new();
318 engine.set_history_json(&datum_and_sketch_history()).unwrap();
319 assert_eq!(
322 fed_datum_names(&engine),
323 vec!["Datum:XY".to_string(), "Datum:XZ".to_string(), "Datum:YZ".to_string()]
324 );
325 assert!(!fed_datum_names(&engine).contains(&"Sk".to_string()));
326 assert!(engine.construction_datums().iter().all(|(n, _)| n != "Sk"));
327 }
328
329 #[test]
330 fn rollback_before_datum_clears_its_planes() {
331 let mut engine = EngineState::new();
332 engine.set_history_json(&cube_and_datum_history()).unwrap();
333 assert_eq!(fed_datum_names(&engine).len(), 3);
334
335 engine.roll_to(0);
338 assert!(fed_datum_names(&engine).is_empty(), "rolled-back datum cleared");
339 assert!(engine.construction_datums().is_empty());
340
341 engine.roll_to(1);
343 assert_eq!(fed_datum_names(&engine).len(), 3, "rolled-forward datum re-shown");
344 }
345
346 #[test]
347 fn set_datum_visible_hides_and_restores_a_plane() {
348 let mut engine = EngineState::new();
349 engine.set_history_json(&cube_and_datum_history()).unwrap();
350
351 engine.set_datum_visible("Datum:XZ", false);
353 assert!(!engine.datum_visible("Datum:XZ"));
354 assert!(!fed_datum_names(&engine).contains(&"Datum:XZ".to_string()));
355 assert_eq!(fed_datum_names(&engine).len(), 2);
356 assert!(engine
357 .construction_datums()
358 .contains(&("Datum:XZ".to_string(), false)));
359
360 engine.set_datum_visible("Datum:XZ", true);
362 assert!(engine.datum_visible("Datum:XZ"));
363 assert_eq!(fed_datum_names(&engine).len(), 3);
364 }
365
366 #[test]
367 fn select_datum_sets_selection_and_emphasizes_it() {
368 let mut engine = EngineState::new();
369 engine.set_history_json(&cube_and_datum_history()).unwrap();
370
371 assert!(engine.select_datum("Datum:XY"));
372 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
373 assert_eq!(sel["datums"].as_array().unwrap().len(), 1);
374 assert_eq!(sel["datums"][0], "Datum:XY");
375
376 let hot: Vec<(&str, bool)> = engine.widgets.datum_plane_names();
378 assert!(
379 hot.iter().any(|(n, hot)| *n == "Datum:XY" && *hot),
380 "selected datum is emphasized: {hot:?}"
381 );
382
383 assert!(!engine.select_datum("NotADatum"));
385
386 assert!(engine.clear_selection());
388 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
389 assert!(sel["datums"].as_array().unwrap().is_empty());
390 }
391
392 #[test]
393 fn select_by_name_routes_datum_kind() {
394 let mut engine = EngineState::new();
395 engine.set_history_json(&cube_and_datum_history()).unwrap();
396 assert!(engine.select_by_name("datum", "Datum:YZ"));
397 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
398 assert_eq!(sel["datums"][0], "Datum:YZ");
399
400 assert!(engine.select_by_name("solid", "Box"));
402 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
403 assert!(sel["datums"].as_array().unwrap().is_empty());
404 assert_eq!(sel["solids"][0], "Box");
405 }
406
407 #[test]
408 fn object_info_json_on_a_datum_returns_a_graceful_record() {
409 let mut engine = EngineState::new();
410 engine.set_history_json(&cube_and_datum_history()).unwrap();
411
412 let info: serde_json::Value =
414 serde_json::from_str(&engine.object_info_json("Datum:XY")).unwrap();
415 assert_eq!(info["ok"], true, "datum info must not error: {info}");
416 assert_eq!(info["kind"], "datum");
417 assert_eq!(info["creatingFeature"]["id"], "Datum");
418 assert_eq!(info["creatingFeature"]["type"], "D");
419
420 let unknown: serde_json::Value =
422 serde_json::from_str(&engine.object_info_json("Nope")).unwrap();
423 assert_eq!(unknown["ok"], false);
424 }
425
426 #[test]
427 fn object_info_json_on_a_plane_reports_kind_plane() {
428 let mut engine = EngineState::new();
429 engine.set_history_json(&plane_history()).unwrap();
430 let info: serde_json::Value =
431 serde_json::from_str(&engine.object_info_json("Pl")).unwrap();
432 assert_eq!(info["ok"], true, "plane info must not error: {info}");
433 assert_eq!(info["kind"], "plane");
434 assert_eq!(info["creatingFeature"]["type"], "P");
435 }
436}
437