use crate::engine_state::EngineState;
use crate::runner::{MeasureKind, MeasureQuery};
use serde_json::Value;
use std::collections::BTreeMap;
pub const DEFAULT_DENSITY: f64 = 1.0;
#[derive(Debug, Clone, Default)]
pub struct MetadataStore {
entries: BTreeMap<String, BTreeMap<String, String>>,
}
impl MetadataStore {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn set_attribute(&mut self, name: &str, key: &str, value: &str) {
if name.is_empty() || key.is_empty() {
return;
}
self.entries
.entry(name.to_string())
.or_default()
.insert(key.to_string(), value.to_string());
}
pub fn remove_attribute(&mut self, name: &str, key: &str) -> bool {
let Some(record) = self.entries.get_mut(name) else {
return false;
};
let existed = record.remove(key).is_some();
if record.is_empty() {
self.entries.remove(name);
}
existed
}
pub fn attribute(&self, name: &str, key: &str) -> Option<&str> {
self.entries.get(name)?.get(key).map(String::as_str)
}
pub fn record(&self, name: &str) -> BTreeMap<String, String> {
self.entries.get(name).cloned().unwrap_or_default()
}
pub fn all(&self) -> &BTreeMap<String, BTreeMap<String, String>> {
&self.entries
}
pub fn density(&self, name: &str) -> f64 {
self.attribute(name, "density")
.and_then(|value| value.trim().parse::<f64>().ok())
.filter(|density| density.is_finite() && *density > 0.0)
.unwrap_or(DEFAULT_DENSITY)
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn record_json(&self, name: &str) -> String {
Value::Object(
self.record(name)
.into_iter()
.map(|(key, value)| (key, Value::String(value)))
.collect(),
)
.to_string()
}
pub fn to_json(&self) -> Value {
Value::Object(
self.entries
.iter()
.map(|(name, record)| {
let object = record
.iter()
.map(|(key, value)| (key.clone(), Value::String(value.clone())))
.collect();
(name.clone(), Value::Object(object))
})
.collect(),
)
}
pub fn to_json_string(&self) -> String {
self.to_json().to_string()
}
pub fn load_json(&mut self, value: Option<&Value>) {
self.entries.clear();
let Some(Value::Object(objects)) = value else {
return;
};
for (name, record) in objects {
let Value::Object(attributes) = record else {
continue;
};
let map: BTreeMap<String, String> = attributes
.iter()
.map(|(key, value)| (key.clone(), value_to_string(value)))
.collect();
if !map.is_empty() {
self.entries.insert(name.clone(), map);
}
}
}
}
fn value_to_string(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
Value::Null => String::new(),
other => other.to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ObjectKind {
Solid,
Face,
Edge,
}
impl EngineState {
pub fn object_metadata_json(&self, name: &str) -> String {
self.metadata.record_json(name)
}
pub fn set_metadata_attribute(&mut self, name: &str, key: &str, value: &str) {
self.metadata.set_attribute(name, key, value);
self.info_cache.remove(name);
}
pub fn remove_metadata_attribute(&mut self, name: &str, key: &str) -> bool {
let existed = self.metadata.remove_attribute(name, key);
self.info_cache.remove(name);
existed
}
pub fn metadata_json(&self) -> String {
self.metadata.to_json_string()
}
}
impl EngineState {
fn resolve_object(&self, name: &str) -> Option<(ObjectKind, String)> {
if name.is_empty() {
return None;
}
if self.scene.solid(name).is_some() {
return Some((ObjectKind::Solid, name.to_string()));
}
for solid in self.scene.solids() {
if solid.faces.iter().any(|face| face.name == name) {
return Some((ObjectKind::Face, solid.name.clone()));
}
}
for solid in self.scene.solids() {
if solid.edges.iter().any(|edge| edge.name == name) {
return Some((ObjectKind::Edge, solid.name.clone()));
}
}
None
}
pub fn creating_feature(&self, name: &str) -> Option<(String, String)> {
let (_, owner) = self.resolve_object(name)?;
let id = self.provenance.get(&owner)?.clone();
Some((id.clone(), self.feature_type_of(&id)))
}
fn feature_type_of(&self, id: &str) -> String {
self.history
.index_of(id)
.and_then(|index| self.history.feature_type(index))
.unwrap_or_default()
}
fn creating_feature_value(&self, owner: &str) -> Value {
match self.provenance.get(owner) {
Some(id) => serde_json::json!({ "id": id, "type": self.feature_type_of(id) }),
None => Value::Null,
}
}
pub fn object_info_json(&mut self, name: &str) -> String {
let Some((kind, owner)) = self.resolve_object(name) else {
if let Some((feature_id, feature_type)) = self.datum_feature_for_name(name) {
let kind_label = if feature_type == "P" { "plane" } else { "datum" };
return serde_json::json!({
"ok": true,
"name": name,
"kind": kind_label,
"creatingFeature": { "id": feature_id, "type": feature_type },
})
.to_string();
}
return serde_json::json!({
"ok": false, "name": name, "message": "unknown object",
})
.to_string();
};
if let Some(solid) = self.scene.solid(&owner) {
if solid.is_sketch {
return self.sketch_info_json(name, kind, solid);
}
}
if let Some(cached) = self.info_cache.get(name) {
return cached.clone();
}
if !self.pending_query.values().any(|(pending, _)| pending == name) {
self.next_query_id += 1;
let id = self.next_query_id;
let measure_kind = match kind {
ObjectKind::Solid => MeasureKind::Solid,
ObjectKind::Face => MeasureKind::Face,
ObjectKind::Edge => MeasureKind::Edge,
};
let density = self.metadata.density(name);
self.pending_query.insert(id, (name.to_string(), owner.clone()));
self.runner.submit_query(MeasureQuery {
id,
kind: measure_kind,
owner,
entity: name.to_string(),
density,
});
}
self.pump_queries();
match self.info_cache.get(name) {
Some(resolved) => resolved.clone(),
None => serde_json::json!({ "ok": false, "name": name, "pending": true }).to_string(),
}
}
pub fn pump_queries(&mut self) {
while let Some(reply) = self.runner.poll_query() {
let Some((name, owner)) = self.pending_query.remove(&reply.id) else {
continue;
};
let fragment: Value = serde_json::from_str(&reply.result).unwrap_or(Value::Null);
let merged = self.merge_info(&name, &owner, &fragment);
self.info_cache.insert(name, merged);
}
}
fn merge_info(&self, name: &str, owner: &str, fragment: &Value) -> String {
let object = fragment.as_object();
let ok = object
.and_then(|map| map.get("ok"))
.and_then(Value::as_bool)
.unwrap_or(false);
let mut merged = serde_json::Map::new();
merged.insert("ok".to_string(), Value::Bool(ok));
merged.insert("name".to_string(), Value::String(name.to_string()));
if let Some(map) = object {
for (key, value) in map {
if key == "ok" {
continue;
}
merged.insert(key.clone(), value.clone());
}
}
if ok {
merged.insert(
"creatingFeature".to_string(),
self.creating_feature_value(owner),
);
}
Value::Object(merged).to_string()
}
fn sketch_info_json(
&self,
name: &str,
kind: ObjectKind,
solid: &crate::scene::SolidDisplay,
) -> String {
let creating = serde_json::json!({
"id": solid.name,
"type": self.feature_type_of(&solid.name),
});
let edge_total: f64 = solid.edges.iter().map(|e| polyline_length(&e.polyline)).sum();
match kind {
ObjectKind::Solid => serde_json::json!({
"ok": true,
"name": name,
"kind": "sketch",
"area": sheet_mesh_area(solid),
"edgeLengthTotal": edge_total,
"creatingFeature": creating,
})
.to_string(),
ObjectKind::Face => serde_json::json!({
"ok": true,
"name": name,
"kind": "face",
"solid": solid.name,
"area": sheet_mesh_area(solid),
"edgeLengthTotal": edge_total,
"creatingFeature": creating,
})
.to_string(),
ObjectKind::Edge => {
let length = solid
.edges
.iter()
.find(|e| e.name == name)
.map(|e| polyline_length(&e.polyline))
.unwrap_or(0.0);
serde_json::json!({
"ok": true,
"name": name,
"kind": "edge",
"solid": solid.name,
"length": length,
"creatingFeature": creating,
})
.to_string()
}
}
}
}
fn sheet_mesh_area(solid: &crate::scene::SolidDisplay) -> f64 {
let p = &solid.mesh.positions;
solid
.mesh
.indices
.chunks_exact(3)
.map(|t| {
let a = p[t[0] as usize];
let b = p[t[1] as usize];
let c = p[t[2] as usize];
let ab = [
(b[0] - a[0]) as f64,
(b[1] - a[1]) as f64,
(b[2] - a[2]) as f64,
];
let ac = [
(c[0] - a[0]) as f64,
(c[1] - a[1]) as f64,
(c[2] - a[2]) as f64,
];
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
(cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt() * 0.5
})
.sum()
}
fn polyline_length(polyline: &[[f32; 3]]) -> f64 {
polyline
.windows(2)
.map(|w| {
let d = [
(w[1][0] - w[0][0]) as f64,
(w[1][1] - w[0][1]) as f64,
(w[1][2] - w[0][2]) as f64,
];
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
})
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn cube_history(name: &str, side: f64) -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": name,
"sizeX": side, "sizeY": side, "sizeZ": side,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
})
.to_string()
}
fn seed_history() -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [
{
"type": "P.CU",
"inputParams": {
"id": "Box",
"sizeX": 20.0, "sizeY": 20.0, "sizeZ": 20.0,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
},
{
"type": "P.CY",
"inputParams": {
"id": "Pin",
"radius": 6.0, "height": 30.0,
"transform": {
"position": [10.0, -5.0, 10.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
},
{
"type": "B",
"inputParams": {
"id": "Cut",
"targetSolid": "Box",
"boolean": { "operation": "SUBTRACT", "targets": ["Pin"] }
},
"persistentData": {}
}
]
})
.to_string()
}
fn first_named_face(engine: &EngineState) -> String {
engine
.scene
.solid("Box")
.unwrap()
.faces
.iter()
.find(|face| !face.name.is_empty())
.expect("a named face")
.name
.clone()
}
fn first_named_edge(engine: &EngineState) -> String {
engine
.scene
.solid("Box")
.unwrap()
.edges
.iter()
.find(|edge| !edge.name.is_empty())
.expect("a named edge")
.name
.clone()
}
#[test]
fn metadata_set_get_remove_and_density() {
let mut store = MetadataStore::new();
assert!(store.is_empty());
assert_eq!(store.density("Box"), DEFAULT_DENSITY);
store.set_attribute("Box", "material", "steel");
store.set_attribute("Box", "density", "7.85");
assert_eq!(store.attribute("Box", "material"), Some("steel"));
assert_eq!(store.density("Box"), 7.85);
let record: Value = serde_json::from_str(&store.record_json("Box")).unwrap();
assert_eq!(record["material"], "steel");
assert_eq!(record["density"], "7.85");
store.set_attribute("", "k", "v");
store.set_attribute("Box", "", "v");
assert_eq!(store.all().len(), 1);
assert!(store.remove_attribute("Box", "material"));
assert_eq!(store.attribute("Box", "material"), None);
assert!(!store.is_empty());
assert!(!store.remove_attribute("Box", "material"));
assert!(store.remove_attribute("Box", "density"));
assert!(store.is_empty());
}
#[test]
fn metadata_round_trips_through_save_and_load() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
engine.set_metadata_attribute("Box", "material", "aluminium");
engine.set_metadata_attribute("Box", "density", "2.7");
let all: Value = serde_json::from_str(&engine.metadata_json()).unwrap();
assert_eq!(all["Box"]["material"], "aluminium");
let saved = engine.history_request_json();
let document: Value = serde_json::from_str(&saved).unwrap();
assert_eq!(document["metadata"]["Box"]["density"], "2.7");
let mut reopened = EngineState::new();
reopened.set_history_json(&saved).unwrap();
assert_eq!(reopened.object_metadata_json("Box"), engine.object_metadata_json("Box"));
assert_eq!(reopened.metadata.density("Box"), 2.7);
reopened.set_history_json(&cube_history("Box", 10.0)).unwrap();
assert!(reopened.metadata.is_empty());
assert_eq!(reopened.object_metadata_json("Box"), "{}");
}
#[test]
fn unannotated_model_persists_without_metadata_field() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
let document: Value = serde_json::from_str(&engine.history_request_json()).unwrap();
assert!(document.get("metadata").is_none());
}
#[test]
fn edge_length_of_cube_edge_equals_side() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
let edge = first_named_edge(&engine);
let info: Value = serde_json::from_str(&engine.object_info_json(&edge)).unwrap();
assert_eq!(info["ok"], true);
assert_eq!(info["kind"], "edge");
assert_eq!(info["solid"], "Box");
assert!(
(info["length"].as_f64().unwrap() - 10.0).abs() < 1e-6,
"edge length {} != side 10",
info["length"]
);
}
#[test]
fn face_area_and_boundary_of_cube_face() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
let face = first_named_face(&engine);
let info: Value = serde_json::from_str(&engine.object_info_json(&face)).unwrap();
assert_eq!(info["ok"], true);
assert_eq!(info["kind"], "face");
assert!((info["area"].as_f64().unwrap() - 100.0).abs() < 1e-6);
assert!((info["edgeLengthTotal"].as_f64().unwrap() - 40.0).abs() < 1e-6);
}
#[test]
fn solid_measurements_and_weight_with_density() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
let info: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
assert_eq!(info["ok"], true);
assert_eq!(info["kind"], "solid");
assert!((info["volume"].as_f64().unwrap() - 1000.0).abs() < 1e-6);
assert!((info["surfaceArea"].as_f64().unwrap() - 600.0).abs() < 1e-6);
assert!((info["edgeLengthTotal"].as_f64().unwrap() - 120.0).abs() < 1e-6);
assert!((info["density"].as_f64().unwrap() - 1.0).abs() < 1e-12);
assert!((info["weight"].as_f64().unwrap() - 1000.0).abs() < 1e-6);
engine.set_metadata_attribute("Box", "density", "2");
let heavy: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
assert!((heavy["density"].as_f64().unwrap() - 2.0).abs() < 1e-12);
assert!((heavy["weight"].as_f64().unwrap() - 2000.0).abs() < 1e-6);
}
#[test]
fn creating_feature_of_seed_box_and_cut() {
let mut engine = EngineState::new();
engine.set_history_json(&seed_history()).unwrap();
let info: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
assert_eq!(info["creatingFeature"]["id"], "Cut");
assert_eq!(info["creatingFeature"]["type"], "B");
assert_eq!(
engine.creating_feature("Box"),
Some(("Cut".to_string(), "B".to_string()))
);
engine.roll_to(0);
let seed: Value = serde_json::from_str(&engine.object_info_json("Box")).unwrap();
assert_eq!(seed["creatingFeature"]["id"], "Box");
assert_eq!(seed["creatingFeature"]["type"], "P.CU");
}
#[test]
fn unknown_object_reports_not_ok() {
let mut engine = EngineState::new();
engine.set_history_json(&cube_history("Box", 10.0)).unwrap();
let info: Value = serde_json::from_str(&engine.object_info_json("Nope")).unwrap();
assert_eq!(info["ok"], false);
assert!(engine.creating_feature("Nope").is_none());
}
}