1use super::*;
2
3const DATUM_PLANE_COLOR: &str = "#6b8fd0";
5const DATUM_PLANE_SELECTED_COLOR: &str = "#ffc400";
7const DATUM_PLANE_HOVERED_COLOR: &str = "#fbff00";
11
12impl EngineState {
13 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 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 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 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 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 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 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 pub fn datum_visible(&self, name: &str) -> bool {
129 !self.hidden_datums.contains(name)
130 }
131
132 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 pub fn construction_datums(&self) -> Vec<(String, bool)> {
148 self.construction_datum_names()
149 .into_iter()
150 .map(|name| {
151 let visible = !self.hidden_datums.contains(&name);
152 (name, visible)
153 })
154 .collect()
155 }
156
157 pub fn datum_entities_json(&self) -> String {
161 let list: Vec<serde_json::Value> = self
162 .construction_datums()
163 .into_iter()
164 .map(|(name, visible)| serde_json::json!({ "name": name, "visible": visible }))
165 .collect();
166 serde_json::Value::Array(list).to_string()
167 }
168
169 pub fn select_datum(&mut self, name: &str) -> bool {
175 if name.is_empty() || !self.construction_frames.iter().any(|(n, _)| n == name) {
176 return false;
177 }
178 if Self::datum_feature_of(name, &self.feature_type_map()).is_none() {
179 return false;
180 }
181 self.emphasis.selected_solids.clear();
182 self.emphasis.selected_faces.clear();
183 self.emphasis.selected_edges.clear();
184 self.emphasis.selected_vertices.clear();
185 self.emphasis.selected_datums.clear();
186 self.emphasis.selected_datums.insert(name.to_string());
187 self.emphasis.generation = self.emphasis.generation.wrapping_add(1);
188 self.refresh_construction_datums();
189 self.dirty = true;
190 true
191 }
192}
193
194
195#[cfg(test)]
199mod construction_datum_tests {
200 use super::*;
201
202 fn cube_and_datum_history() -> String {
205 serde_json::json!({
206 "features": [
207 {
208 "type": "P.CU",
209 "inputParams": {
210 "id": "Box",
211 "sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
212 "transform": {
213 "position": [0.0, 0.0, 0.0],
214 "rotationEuler": [0.0, 0.0, 0.0],
215 "scale": [1.0, 1.0, 1.0]
216 },
217 "boolean": { "targets": [], "operation": "NONE" }
218 },
219 "persistentData": {}
220 },
221 {
222 "type": "D",
223 "inputParams": { "id": "Datum" },
224 "persistentData": {}
225 }
226 ]
227 })
228 .to_string()
229 }
230
231 fn plane_history() -> String {
233 serde_json::json!({
234 "features": [{
235 "type": "P",
236 "inputParams": { "id": "Pl", "orientation": "XZ", "offset_distance": 3.0 },
237 "persistentData": {}
238 }]
239 })
240 .to_string()
241 }
242
243 fn datum_and_sketch_history() -> String {
247 serde_json::json!({
248 "features": [
249 {
250 "type": "D",
251 "inputParams": { "id": "Datum" },
252 "persistentData": {}
253 },
254 {
255 "type": "S",
256 "inputParams": { "id": "Sk" },
257 "persistentData": {
258 "basis": { "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "z": [0, 0, 1] },
259 "sketch": {
260 "points": [
261 { "id": 0, "x": 0.0, "y": 0.0 },
262 { "id": 1, "x": 10.0, "y": 0.0 },
263 { "id": 2, "x": 10.0, "y": 6.0 },
264 { "id": 3, "x": 0.0, "y": 6.0 }
265 ],
266 "geometries": [
267 { "id": 10, "type": "line", "points": [0, 1] },
268 { "id": 11, "type": "line", "points": [1, 2] },
269 { "id": 12, "type": "line", "points": [2, 3] },
270 { "id": 13, "type": "line", "points": [3, 0] }
271 ],
272 "constraints": []
273 }
274 }
275 }
276 ]
277 })
278 .to_string()
279 }
280
281 fn fed_datum_names(engine: &EngineState) -> Vec<String> {
284 let mut names: Vec<String> = engine
285 .widgets
286 .datum_plane_names()
287 .iter()
288 .map(|(n, _)| n.to_string())
289 .collect();
290 names.sort();
291 names
292 }
293
294 #[test]
295 fn datum_feature_feeds_three_named_planes_and_lists_them() {
296 let mut engine = EngineState::new();
297 engine.set_history_json(&cube_and_datum_history()).unwrap();
298
299 assert_eq!(
301 fed_datum_names(&engine),
302 vec!["Datum:XY".to_string(), "Datum:XZ".to_string(), "Datum:YZ".to_string()]
303 );
304
305 let listed = engine.construction_datums();
308 assert_eq!(listed.len(), 3);
309 assert!(listed.iter().all(|(_, visible)| *visible));
310 let json: serde_json::Value =
311 serde_json::from_str(&engine.datum_entities_json()).unwrap();
312 assert_eq!(json.as_array().unwrap().len(), 3);
313 assert!(json.as_array().unwrap().iter().any(|d| d["name"] == "Datum:XY"));
314
315 let solids: serde_json::Value =
317 serde_json::from_str(&engine.scene_entities_json()).unwrap();
318 assert_eq!(solids.as_array().unwrap().len(), 1);
319 assert_eq!(solids[0]["name"], "Box");
320 }
321
322 #[test]
323 fn plane_feature_feeds_and_lists_its_single_frame() {
324 let mut engine = EngineState::new();
325 engine.set_history_json(&plane_history()).unwrap();
326 assert_eq!(fed_datum_names(&engine), vec!["Pl".to_string()]);
327 assert_eq!(engine.construction_datums(), vec![("Pl".to_string(), true)]);
328 }
329
330 #[test]
331 fn creating_feature_resolves_datum_and_plane_frames() {
332 let mut engine = EngineState::new();
337 engine.set_history_json(&cube_and_datum_history()).unwrap();
338 assert_eq!(
339 engine.creating_feature("Datum:YZ"),
340 Some(("Datum".to_string(), "D".to_string())),
341 "a datum plane resolves to its D feature"
342 );
343
344 let mut engine = EngineState::new();
345 engine.set_history_json(&plane_history()).unwrap();
346 assert_eq!(
347 engine.creating_feature("Pl"),
348 Some(("Pl".to_string(), "P".to_string())),
349 "a construction plane resolves to its P feature"
350 );
351 }
352
353 #[test]
354 fn sketch_frame_is_not_fed_as_a_datum() {
355 let mut engine = EngineState::new();
356 engine.set_history_json(&datum_and_sketch_history()).unwrap();
357 assert_eq!(
360 fed_datum_names(&engine),
361 vec!["Datum:XY".to_string(), "Datum:XZ".to_string(), "Datum:YZ".to_string()]
362 );
363 assert!(!fed_datum_names(&engine).contains(&"Sk".to_string()));
364 assert!(engine.construction_datums().iter().all(|(n, _)| n != "Sk"));
365 }
366
367 #[test]
368 fn rollback_before_datum_clears_its_planes() {
369 let mut engine = EngineState::new();
370 engine.set_history_json(&cube_and_datum_history()).unwrap();
371 assert_eq!(fed_datum_names(&engine).len(), 3);
372
373 engine.roll_to(0);
376 assert!(fed_datum_names(&engine).is_empty(), "rolled-back datum cleared");
377 assert!(engine.construction_datums().is_empty());
378
379 engine.roll_to(1);
381 assert_eq!(fed_datum_names(&engine).len(), 3, "rolled-forward datum re-shown");
382 }
383
384 #[test]
385 fn set_datum_visible_hides_and_restores_a_plane() {
386 let mut engine = EngineState::new();
387 engine.set_history_json(&cube_and_datum_history()).unwrap();
388
389 engine.set_datum_visible("Datum:XZ", false);
391 assert!(!engine.datum_visible("Datum:XZ"));
392 assert!(!fed_datum_names(&engine).contains(&"Datum:XZ".to_string()));
393 assert_eq!(fed_datum_names(&engine).len(), 2);
394 assert!(engine
395 .construction_datums()
396 .contains(&("Datum:XZ".to_string(), false)));
397
398 engine.set_datum_visible("Datum:XZ", true);
400 assert!(engine.datum_visible("Datum:XZ"));
401 assert_eq!(fed_datum_names(&engine).len(), 3);
402 }
403
404 #[test]
405 fn select_datum_sets_selection_and_emphasizes_it() {
406 let mut engine = EngineState::new();
407 engine.set_history_json(&cube_and_datum_history()).unwrap();
408
409 assert!(engine.select_datum("Datum:XY"));
410 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
411 assert_eq!(sel["datums"].as_array().unwrap().len(), 1);
412 assert_eq!(sel["datums"][0], "Datum:XY");
413
414 let hot: Vec<(&str, bool)> = engine.widgets.datum_plane_names();
416 assert!(
417 hot.iter().any(|(n, hot)| *n == "Datum:XY" && *hot),
418 "selected datum is emphasized: {hot:?}"
419 );
420
421 assert!(!engine.select_datum("NotADatum"));
423
424 assert!(engine.clear_selection());
426 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
427 assert!(sel["datums"].as_array().unwrap().is_empty());
428 }
429
430 #[test]
431 fn select_by_name_routes_datum_kind() {
432 let mut engine = EngineState::new();
433 engine.set_history_json(&cube_and_datum_history()).unwrap();
434 assert!(engine.select_by_name("datum", "Datum:YZ"));
435 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
436 assert_eq!(sel["datums"][0], "Datum:YZ");
437
438 assert!(engine.select_by_name("solid", "Box"));
440 let sel: serde_json::Value = serde_json::from_str(&engine.selection_json()).unwrap();
441 assert!(sel["datums"].as_array().unwrap().is_empty());
442 assert_eq!(sel["solids"][0], "Box");
443 }
444
445 #[test]
446 fn object_info_json_on_a_datum_returns_a_graceful_record() {
447 let mut engine = EngineState::new();
448 engine.set_history_json(&cube_and_datum_history()).unwrap();
449
450 let info: serde_json::Value =
452 serde_json::from_str(&engine.object_info_json("Datum:XY")).unwrap();
453 assert_eq!(info["ok"], true, "datum info must not error: {info}");
454 assert_eq!(info["kind"], "datum");
455 assert_eq!(info["creatingFeature"]["id"], "Datum");
456 assert_eq!(info["creatingFeature"]["type"], "D");
457
458 let unknown: serde_json::Value =
460 serde_json::from_str(&engine.object_info_json("Nope")).unwrap();
461 assert_eq!(unknown["ok"], false);
462 }
463
464 #[test]
465 fn object_info_json_on_a_plane_reports_kind_plane() {
466 let mut engine = EngineState::new();
467 engine.set_history_json(&plane_history()).unwrap();
468 let info: serde_json::Value =
469 serde_json::from_str(&engine.object_info_json("Pl")).unwrap();
470 assert_eq!(info["ok"], true, "plane info must not error: {info}");
471 assert_eq!(info["kind"], "plane");
472 assert_eq!(info["creatingFeature"]["type"], "P");
473 }
474
475 #[test]
481 fn ref_select_falls_back_to_datum_pick_on_a_geometry_miss() {
482 let mut engine = EngineState::new();
483 engine.set_history_json(&datum_and_sketch_history()).unwrap();
484 engine.resize(800.0, 600.0);
487 engine.camera.eye = [0.0, 0.0, 40.0];
488 engine.camera.target = [0.0, 0.0, 0.0];
489 engine.camera.up = [0.0, 1.0, 0.0];
490 engine.camera.projection = crate::view::Projection::Orthographic { half_height: 20.0 };
491
492 engine.begin_ref_select(
493 "Sk",
494 vec!["sketchPlane".to_string()],
495 "Sketch plane".to_string(),
496 vec!["PLANE".to_string(), "FACE".to_string()],
497 false,
498 Vec::new(),
499 );
500 engine.ref_select_click(400.0, 300.0);
503 assert_eq!(
504 engine.ref_select_names(),
505 vec!["Datum:XY".to_string()],
506 "geometry-miss click picks the construction datum"
507 );
508 }
509}
510