1use crate::engine_state::EngineState;
27use crate::runner::{MeasureKind, MeasureQuery};
28use serde_json::Value;
29use std::collections::BTreeMap;
30
31pub const DEFAULT_DENSITY: f64 = 1.0;
34
35#[derive(Debug, Clone, Default)]
40pub struct MetadataStore {
41 entries: BTreeMap<String, BTreeMap<String, String>>,
42}
43
44impl MetadataStore {
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn is_empty(&self) -> bool {
51 self.entries.is_empty()
52 }
53
54 pub fn set_attribute(&mut self, name: &str, key: &str, value: &str) {
57 if name.is_empty() || key.is_empty() {
58 return;
59 }
60 self.entries
61 .entry(name.to_string())
62 .or_default()
63 .insert(key.to_string(), value.to_string());
64 }
65
66 pub fn remove_attribute(&mut self, name: &str, key: &str) -> bool {
69 let Some(record) = self.entries.get_mut(name) else {
70 return false;
71 };
72 let existed = record.remove(key).is_some();
73 if record.is_empty() {
74 self.entries.remove(name);
75 }
76 existed
77 }
78
79 pub fn attribute(&self, name: &str, key: &str) -> Option<&str> {
81 self.entries.get(name)?.get(key).map(String::as_str)
82 }
83
84 pub fn record(&self, name: &str) -> BTreeMap<String, String> {
86 self.entries.get(name).cloned().unwrap_or_default()
87 }
88
89 pub fn all(&self) -> &BTreeMap<String, BTreeMap<String, String>> {
91 &self.entries
92 }
93
94 pub fn density(&self, name: &str) -> f64 {
97 self.attribute(name, "density")
98 .and_then(|value| value.trim().parse::<f64>().ok())
99 .filter(|density| density.is_finite() && *density > 0.0)
100 .unwrap_or(DEFAULT_DENSITY)
101 }
102
103 pub fn clear(&mut self) {
105 self.entries.clear();
106 }
107
108 pub fn record_json(&self, name: &str) -> String {
111 Value::Object(
112 self.record(name)
113 .into_iter()
114 .map(|(key, value)| (key, Value::String(value)))
115 .collect(),
116 )
117 .to_string()
118 }
119
120 pub fn to_json(&self) -> Value {
123 Value::Object(
124 self.entries
125 .iter()
126 .map(|(name, record)| {
127 let object = record
128 .iter()
129 .map(|(key, value)| (key.clone(), Value::String(value.clone())))
130 .collect();
131 (name.clone(), Value::Object(object))
132 })
133 .collect(),
134 )
135 }
136
137 pub fn to_json_string(&self) -> String {
139 self.to_json().to_string()
140 }
141
142 pub fn load_json(&mut self, value: Option<&Value>) {
146 self.entries.clear();
147 let Some(Value::Object(objects)) = value else {
148 return;
149 };
150 for (name, record) in objects {
151 let Value::Object(attributes) = record else {
152 continue;
153 };
154 let map: BTreeMap<String, String> = attributes
155 .iter()
156 .map(|(key, value)| (key.clone(), value_to_string(value)))
157 .collect();
158 if !map.is_empty() {
159 self.entries.insert(name.clone(), map);
160 }
161 }
162 }
163}
164
165fn value_to_string(value: &Value) -> String {
168 match value {
169 Value::String(text) => text.clone(),
170 Value::Null => String::new(),
171 other => other.to_string(),
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177enum ObjectKind {
178 Solid,
179 Face,
180 Edge,
181}
182
183impl EngineState {
186 pub fn object_metadata_json(&self, name: &str) -> String {
188 self.metadata.record_json(name)
189 }
190
191 pub fn set_metadata_attribute(&mut self, name: &str, key: &str, value: &str) {
194 self.metadata.set_attribute(name, key, value);
195 self.info_cache.remove(name);
199 }
200
201 pub fn remove_metadata_attribute(&mut self, name: &str, key: &str) -> bool {
203 let existed = self.metadata.remove_attribute(name, key);
204 self.info_cache.remove(name);
206 existed
207 }
208
209 pub fn metadata_json(&self) -> String {
211 self.metadata.to_json_string()
212 }
213}
214
215impl EngineState {
217 fn resolve_object(&self, name: &str) -> Option<(ObjectKind, String)> {
221 if name.is_empty() {
222 return None;
223 }
224 if self.scene.solid(name).is_some() {
225 return Some((ObjectKind::Solid, name.to_string()));
226 }
227 for solid in self.scene.solids() {
228 if solid.faces.iter().any(|face| face.name == name) {
229 return Some((ObjectKind::Face, solid.name.clone()));
230 }
231 }
232 for solid in self.scene.solids() {
233 if solid.edges.iter().any(|edge| edge.name == name) {
234 return Some((ObjectKind::Edge, solid.name.clone()));
235 }
236 }
237 None
238 }
239
240 pub fn creating_feature(&self, name: &str) -> Option<(String, String)> {
246 let (_, owner) = self.resolve_object(name)?;
247 let id = self.provenance.get(&owner)?.clone();
248 Some((id.clone(), self.feature_type_of(&id)))
249 }
250
251 fn feature_type_of(&self, id: &str) -> String {
253 self.history
254 .index_of(id)
255 .and_then(|index| self.history.feature_type(index))
256 .unwrap_or_default()
257 }
258
259 fn creating_feature_value(&self, owner: &str) -> Value {
262 match self.provenance.get(owner) {
263 Some(id) => serde_json::json!({ "id": id, "type": self.feature_type_of(id) }),
264 None => Value::Null,
265 }
266 }
267
268 pub fn object_info_json(&mut self, name: &str) -> String {
296 let Some((kind, owner)) = self.resolve_object(name) else {
297 if let Some((feature_id, feature_type)) = self.datum_feature_for_name(name) {
303 let kind_label = if feature_type == "P" { "plane" } else { "datum" };
304 return serde_json::json!({
305 "ok": true,
306 "name": name,
307 "kind": kind_label,
308 "creatingFeature": { "id": feature_id, "type": feature_type },
309 })
310 .to_string();
311 }
312 return serde_json::json!({
313 "ok": false, "name": name, "message": "unknown object",
314 })
315 .to_string();
316 };
317 if let Some(solid) = self.scene.solid(&owner) {
322 if solid.is_sketch {
323 return self.sketch_info_json(name, kind, solid);
324 }
325 }
326
327 if let Some(cached) = self.info_cache.get(name) {
331 return cached.clone();
332 }
333 if !self.pending_query.values().any(|(pending, _)| pending == name) {
334 self.next_query_id += 1;
335 let id = self.next_query_id;
336 let measure_kind = match kind {
337 ObjectKind::Solid => MeasureKind::Solid,
338 ObjectKind::Face => MeasureKind::Face,
339 ObjectKind::Edge => MeasureKind::Edge,
340 };
341 let density = self.metadata.density(name);
342 self.pending_query.insert(id, (name.to_string(), owner.clone()));
343 self.runner.submit_query(MeasureQuery {
344 id,
345 kind: measure_kind,
346 owner,
347 entity: name.to_string(),
348 density,
349 });
350 }
351 self.pump_queries();
352 match self.info_cache.get(name) {
353 Some(resolved) => resolved.clone(),
354 None => serde_json::json!({ "ok": false, "name": name, "pending": true }).to_string(),
355 }
356 }
357
358 pub fn pump_queries(&mut self) {
365 while let Some(reply) = self.runner.poll_query() {
366 let Some((name, owner)) = self.pending_query.remove(&reply.id) else {
367 continue;
370 };
371 let fragment: Value = serde_json::from_str(&reply.result).unwrap_or(Value::Null);
372 let merged = self.merge_info(&name, &owner, &fragment);
373 self.info_cache.insert(name, merged);
374 }
375 }
376
377 fn merge_info(&self, name: &str, owner: &str, fragment: &Value) -> String {
384 let object = fragment.as_object();
385 let ok = object
386 .and_then(|map| map.get("ok"))
387 .and_then(Value::as_bool)
388 .unwrap_or(false);
389 let mut merged = serde_json::Map::new();
390 merged.insert("ok".to_string(), Value::Bool(ok));
391 merged.insert("name".to_string(), Value::String(name.to_string()));
392 if let Some(map) = object {
393 for (key, value) in map {
394 if key == "ok" {
395 continue;
396 }
397 merged.insert(key.clone(), value.clone());
398 }
399 }
400 if ok {
401 merged.insert(
402 "creatingFeature".to_string(),
403 self.creating_feature_value(owner),
404 );
405 }
406 Value::Object(merged).to_string()
407 }
408
409 fn sketch_info_json(
415 &self,
416 name: &str,
417 kind: ObjectKind,
418 solid: &crate::scene::SolidDisplay,
419 ) -> String {
420 let creating = serde_json::json!({
421 "id": solid.name,
422 "type": self.feature_type_of(&solid.name),
423 });
424 let edge_total: f64 = solid.edges.iter().map(|e| polyline_length(&e.polyline)).sum();
425 match kind {
426 ObjectKind::Solid => serde_json::json!({
427 "ok": true,
428 "name": name,
429 "kind": "sketch",
430 "area": sheet_mesh_area(solid),
431 "edgeLengthTotal": edge_total,
432 "creatingFeature": creating,
433 })
434 .to_string(),
435 ObjectKind::Face => serde_json::json!({
436 "ok": true,
437 "name": name,
438 "kind": "face",
439 "solid": solid.name,
440 "area": sheet_mesh_area(solid),
441 "edgeLengthTotal": edge_total,
442 "creatingFeature": creating,
443 })
444 .to_string(),
445 ObjectKind::Edge => {
446 let length = solid
447 .edges
448 .iter()
449 .find(|e| e.name == name)
450 .map(|e| polyline_length(&e.polyline))
451 .unwrap_or(0.0);
452 serde_json::json!({
453 "ok": true,
454 "name": name,
455 "kind": "edge",
456 "solid": solid.name,
457 "length": length,
458 "creatingFeature": creating,
459 })
460 .to_string()
461 }
462 }
463 }
464}
465
466fn sheet_mesh_area(solid: &crate::scene::SolidDisplay) -> f64 {
469 let p = &solid.mesh.positions;
470 solid
471 .mesh
472 .indices
473 .chunks_exact(3)
474 .map(|t| {
475 let a = p[t[0] as usize];
476 let b = p[t[1] as usize];
477 let c = p[t[2] as usize];
478 let ab = [
479 (b[0] - a[0]) as f64,
480 (b[1] - a[1]) as f64,
481 (b[2] - a[2]) as f64,
482 ];
483 let ac = [
484 (c[0] - a[0]) as f64,
485 (c[1] - a[1]) as f64,
486 (c[2] - a[2]) as f64,
487 ];
488 let cross = [
489 ab[1] * ac[2] - ab[2] * ac[1],
490 ab[2] * ac[0] - ab[0] * ac[2],
491 ab[0] * ac[1] - ab[1] * ac[0],
492 ];
493 (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt() * 0.5
494 })
495 .sum()
496}
497
498fn polyline_length(polyline: &[[f32; 3]]) -> f64 {
500 polyline
501 .windows(2)
502 .map(|w| {
503 let d = [
504 (w[1][0] - w[0][0]) as f64,
505 (w[1][1] - w[0][1]) as f64,
506 (w[1][2] - w[0][2]) as f64,
507 ];
508 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
509 })
510 .sum()
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 fn cube_history(name: &str, side: f64) -> String {
518 serde_json::json!({
519 "expressions": "",
520 "configurator": {},
521 "features": [{
522 "type": "P.CU",
523 "inputParams": {
524 "id": name,
525 "sizeX": side, "sizeY": side, "sizeZ": side,
526 "transform": {
527 "position": [0.0, 0.0, 0.0],
528 "rotationEuler": [0.0, 0.0, 0.0],
529 "scale": [1.0, 1.0, 1.0]
530 },
531 "boolean": { "targets": [], "operation": "NONE" }
532 },
533 "persistentData": {}
534 }]
535 })
536 .to_string()
537 }
538
539 fn seed_history() -> String {
543 serde_json::json!({
544 "expressions": "",
545 "configurator": {},
546 "features": [
547 {
548 "type": "P.CU",
549 "inputParams": {
550 "id": "Box",
551 "sizeX": 20.0, "sizeY": 20.0, "sizeZ": 20.0,
552 "transform": {
553 "position": [0.0, 0.0, 0.0],
554 "rotationEuler": [0.0, 0.0, 0.0],
555 "scale": [1.0, 1.0, 1.0]
556 },
557 "boolean": { "targets": [], "operation": "NONE" }
558 },
559 "persistentData": {}
560 },
561 {
562 "type": "P.CY",
563 "inputParams": {
564 "id": "Pin",
565 "radius": 6.0, "height": 30.0,
566 "transform": {
567 "position": [10.0, -5.0, 10.0],
568 "rotationEuler": [0.0, 0.0, 0.0],
569 "scale": [1.0, 1.0, 1.0]
570 },
571 "boolean": { "targets": [], "operation": "NONE" }
572 },
573 "persistentData": {}
574 },
575 {
576 "type": "B",
577 "inputParams": {
578 "id": "Cut",
579 "targetSolid": "Box",
580 "boolean": { "operation": "SUBTRACT", "targets": ["Pin"] }
581 },
582 "persistentData": {}
583 }
584 ]
585 })
586 .to_string()
587 }
588
589 fn first_named_face(engine: &EngineState) -> String {
592 engine
593 .scene
594 .solid("Box")
595 .unwrap()
596 .faces
597 .iter()
598 .find(|face| !face.name.is_empty())
599 .expect("a named face")
600 .name
601 .clone()
602 }
603
604 fn first_named_edge(engine: &EngineState) -> String {
605 engine
606 .scene
607 .solid("Box")
608 .unwrap()
609 .edges
610 .iter()
611 .find(|edge| !edge.name.is_empty())
612 .expect("a named edge")
613 .name
614 .clone()
615 }
616
617 #[test]
618 fn metadata_set_get_remove_and_density() {
619 let mut store = MetadataStore::new();
620 assert!(store.is_empty());
621 assert_eq!(store.density("Box"), DEFAULT_DENSITY);
623
624 store.set_attribute("Box", "material", "steel");
625 store.set_attribute("Box", "density", "7.85");
626 assert_eq!(store.attribute("Box", "material"), Some("steel"));
627 assert_eq!(store.density("Box"), 7.85);
628 let record: Value = serde_json::from_str(&store.record_json("Box")).unwrap();
630 assert_eq!(record["material"], "steel");
631 assert_eq!(record["density"], "7.85");
632
633 store.set_attribute("", "k", "v");
635 store.set_attribute("Box", "", "v");
636 assert_eq!(store.all().len(), 1);
637
638 assert!(store.remove_attribute("Box", "material"));
640 assert_eq!(store.attribute("Box", "material"), None);
641 assert!(!store.is_empty());
642 assert!(!store.remove_attribute("Box", "material"));
644 assert!(store.remove_attribute("Box", "density"));
646 assert!(store.is_empty());
647 }
648
649 #[test]
650 fn metadata_round_trips_through_save_and_load() {
651 let mut engine = EngineState::new();
652 engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
653 engine.set_metadata_attribute("Box", "material", "aluminium");
654 engine.set_metadata_attribute("Box", "density", "2.7");
655
656 let all: Value = serde_json::from_str(&engine.metadata_json()).unwrap();
658 assert_eq!(all["Box"]["material"], "aluminium");
659
660 let saved = engine.history_request_json();
662 let document: Value = serde_json::from_str(&saved).unwrap();
663 assert_eq!(document["metadata"]["Box"]["density"], "2.7");
664
665 let mut reopened = EngineState::new();
667 reopened.set_history_json(&saved).unwrap();
668 assert_eq!(reopened.object_metadata_json("Box"), engine.object_metadata_json("Box"));
669 assert_eq!(reopened.metadata.density("Box"), 2.7);
670
671 reopened.set_history_json(&cube_history("Box", 10.0)).unwrap();
673 assert!(reopened.metadata.is_empty());
674 assert_eq!(reopened.object_metadata_json("Box"), "{}");
675 }
676
677 #[test]
678 fn unannotated_model_persists_without_metadata_field() {
679 let mut engine = EngineState::new();
680 engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
681 let document: Value = serde_json::from_str(&engine.history_request_json()).unwrap();
682 assert!(document.get("metadata").is_none());
683 }
684
685 #[test]
686 fn edge_length_of_cube_edge_equals_side() {
687 let mut engine = EngineState::new();
688 engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
689 let edge = first_named_edge(&engine);
690 let info: Value = serde_json::from_str(&engine.object_info_json(&edge)).unwrap();
691 assert_eq!(info["ok"], true);
692 assert_eq!(info["kind"], "edge");
693 assert_eq!(info["solid"], "Box");
694 assert!(
695 (info["length"].as_f64().unwrap() - 10.0).abs() < 1e-6,
696 "edge length {} != side 10",
697 info["length"]
698 );
699 }
700
701 #[test]
702 fn face_area_and_boundary_of_cube_face() {
703 let mut engine = EngineState::new();
704 engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
705 let face = first_named_face(&engine);
706 let info: Value = serde_json::from_str(&engine.object_info_json(&face)).unwrap();
707 assert_eq!(info["ok"], true);
708 assert_eq!(info["kind"], "face");
709 assert!((info["area"].as_f64().unwrap() - 100.0).abs() < 1e-6);
711 assert!((info["edgeLengthTotal"].as_f64().unwrap() - 40.0).abs() < 1e-6);
712 }
713
714 #[test]
715 fn solid_measurements_and_weight_with_density() {
716 let mut engine = EngineState::new();
717 engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
718
719 let info: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
721 assert_eq!(info["ok"], true);
722 assert_eq!(info["kind"], "solid");
723 assert!((info["volume"].as_f64().unwrap() - 1000.0).abs() < 1e-6);
724 assert!((info["surfaceArea"].as_f64().unwrap() - 600.0).abs() < 1e-6);
725 assert!((info["edgeLengthTotal"].as_f64().unwrap() - 120.0).abs() < 1e-6);
726 assert!((info["density"].as_f64().unwrap() - 1.0).abs() < 1e-12);
727 assert!((info["weight"].as_f64().unwrap() - 1000.0).abs() < 1e-6);
728
729 engine.set_metadata_attribute("Box", "density", "2");
731 let heavy: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
732 assert!((heavy["density"].as_f64().unwrap() - 2.0).abs() < 1e-12);
733 assert!((heavy["weight"].as_f64().unwrap() - 2000.0).abs() < 1e-6);
734 }
735
736 #[test]
737 fn creating_feature_of_seed_box_and_cut() {
738 let mut engine = EngineState::new();
739 engine.set_history_json(&seed_history()).unwrap();
740
741 let info: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
743 assert_eq!(info["creatingFeature"]["id"], "Cut");
744 assert_eq!(info["creatingFeature"]["type"], "B");
745 assert_eq!(
747 engine.creating_feature("Box"),
748 Some(("Cut".to_string(), "B".to_string()))
749 );
750
751 engine.roll_to(0);
753 let seed: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
754 assert_eq!(seed["creatingFeature"]["id"], "Box");
755 assert_eq!(seed["creatingFeature"]["type"], "P.CU");
756 }
757
758 #[test]
759 fn unknown_object_reports_not_ok() {
760 let mut engine = EngineState::new();
761 engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
762 let info: Value = serde_json::from_str(&engine.object_info_json("Nope")).unwrap();
763 assert_eq!(info["ok"], false);
764 assert!(engine.creating_feature("Nope").is_none());
765 }
766}