diff --git a/crates/engine/src/cmd/geometry.rs b/crates/engine/src/cmd/geometry.rs index d186de4..9f0d53a 100644 --- a/crates/engine/src/cmd/geometry.rs +++ b/crates/engine/src/cmd/geometry.rs @@ -16,7 +16,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session}; pub const OPEN: CommandSpec = CommandSpec { id: "geometry.open", label: "Open Design", - params: r#"{"path": "part.solidcraft", "model"?: "keep"} — rebuilds the SolidCraft design and makes its bodies the geometry. `model`: "keep" (default) keeps the mesh and every definition, so opening the same design again after an edit picks up the change and sets follow their persistent face names; "new" starts a new model with the design (no mesh, sets, materials, sections, BCs, loads or steps; the unit system stays; the model is untitled until saved, and undo restores the previous file). The reply's `related` is false when the model (before the open) refers to bodies (mesh tags, set geometry) and the design has none of them, and `stale` lists what no longer resolves: `mesh_bodies` (bodies the mesh is tagged with that the design lacks) `sets` (sets naming a body or face it lacks) and `seeds` (edge seeds on edges it lacks). One undo step"#, + params: r#"{"path": "part.solidcraft", "model"?: "keep"} — rebuilds the SolidCraft design and makes its bodies the geometry. `model`: "keep" (default) keeps the mesh and every definition, so opening the same design again after an edit picks up the change and sets follow their persistent face names; "new" starts a new model with the design (no mesh, sets, materials, sections, BCs, loads or steps; the unit system stays; the model is untitled until saved, and undo restores the previous file). The reply's `related` is false when the model (before the open) refers to bodies (mesh tags, set geometry) and the design has none of them, and `stale` lists what no longer resolves: `mesh_bodies` (bodies the mesh is tagged with that the design lacks) `sets` (sets naming a body or face it lacks) and `seeds` (edge seeds on edges it lacks). `errors` lists the features that failed to rebuild with the `bodies` each may change, and `warnings` says so in one line each. One undo step"#, form: Some(super::forms::OPEN_DESIGN), kind: CommandKind::Model, requires: &[], @@ -86,7 +86,9 @@ fn open(s: &mut Session, p: &Value) -> Result { let out = json!({ "name": design.name, "bodies": design.bodies.iter().map(|b| b.name.as_str()).collect::>(), - "errors": design.errors.iter().map(|e| json!({"feature": e.feature, "message": e.message})).collect::>(), + "errors": design.errors.iter().map(|e| json!({"feature": e.feature, "message": e.message, "bodies": e.bodies})).collect::>(), + // Shown with the open: a body that differs from the design is never silent. + "warnings": design.errors.iter().map(|e| e.warning()).collect::>(), "model": if new_model { "new" } else { "kept" }, "related": fit.related, "stale": {"mesh_bodies": fit.mesh_bodies, "sets": fit.sets, "seeds": fit.seeds}, diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index b1f31a8..3ebcf90 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -734,6 +734,7 @@ mod tests { let opened = s.execute("geometry.open", &json!({"path": path})).unwrap(); assert_eq!(opened["bodies"], json!(["Body1"])); assert_eq!(opened["errors"], json!([])); + assert_eq!(opened["warnings"], json!([])); assert_eq!(s.journal.len(), 1); let bodies = s.execute("geometry.bodies", &json!({})).unwrap(); diff --git a/crates/geometry/Cargo.toml b/crates/geometry/Cargo.toml index 897cd06..30ba374 100644 --- a/crates/geometry/Cargo.toml +++ b/crates/geometry/Cargo.toml @@ -18,9 +18,9 @@ thiserror = { workspace = true } zip = { workspace = true } quick-xml = { workspace = true } solidcraft-engine = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } +serde_json = { workspace = true } [features] # Build designs from SolidCraft command scripts (`script_design`): the test corpus and tooling. # Off by default; the library itself only opens designs. -scripts = ["dep:solidcraft-engine", "dep:serde_json"] +scripts = ["dep:solidcraft-engine"] diff --git a/crates/geometry/src/design.rs b/crates/geometry/src/design.rs index 277c4b4..ccb30f2 100644 --- a/crates/geometry/src/design.rs +++ b/crates/geometry/src/design.rs @@ -34,6 +34,42 @@ pub struct Design { pub struct FeatureError { pub feature: String, pub message: String, + /// The bodies that may differ from the design as SolidCraft builds it: those the feature + /// names, or every body when it names none of them. + pub bodies: Vec, +} + +impl FeatureError { + /// One line for the person opening the design, naming the bodies it may change. + pub fn warning(&self) -> String { + format!("{} failed to build, so {} may differ from the design: {}", self.feature, quoted(&self.bodies), self.message) + } +} + +fn quoted(bodies: &[String]) -> String { + match bodies { + [] => "the design".into(), + [b] => format!("body \"{b}\""), + _ => format!("bodies {}", bodies.iter().map(|b| format!("\"{b}\"")).collect::>().join(", ")), + } +} + +/// The bodies of `bodies` that `feature` names anywhere in its saved form (targets, tools, +/// selections); all of them when it names none. +fn bodies_of(feature: Option<&solidcraft_doc::Feature>, bodies: &[Shape]) -> Vec { + fn strings<'a>(v: &'a serde_json::Value, out: &mut Vec<&'a str>) { + match v { + serde_json::Value::String(s) => out.push(s), + serde_json::Value::Array(a) => a.iter().for_each(|v| strings(v, out)), + serde_json::Value::Object(o) => o.values().for_each(|v| strings(v, out)), + _ => {} + } + } + let value = feature.and_then(|f| serde_json::to_value(f).ok()).unwrap_or_default(); + let mut named = Vec::new(); + strings(&value, &mut named); + let hit: Vec = bodies.iter().filter(|b| named.contains(&b.name.as_str())).map(|b| b.name.clone()).collect(); + if hit.is_empty() { bodies.iter().map(|b| b.name.clone()).collect() } else { hit } } impl Design { @@ -55,12 +91,6 @@ pub(crate) fn rebuild(doc: &Document) -> Result { m.evaluate(doc); Ok(m) })?; - let errors = model - .results - .iter() - .filter(|r| !r.skipped) - .filter_map(|r| r.error.as_ref().map(|e| FeatureError { feature: r.name.clone(), message: e.clone() })) - .collect(); // Placed in the world: a component's bodies through every occurrence of it (an instance is a // body of its own, named after its occurrence path), as SolidCraft displays and exports them. let state = guard("place components", || Ok(solidcraft_doc::world_state(doc, &model.state())))?; @@ -72,6 +102,18 @@ pub(crate) fn rebuild(doc: &Document) -> Result { })?; bodies.push(Shape { name: b.name.clone(), body: b.body.clone(), faces }); } + // A feature that failed leaves a different solid than SolidCraft's: each one names the bodies + // it would have changed, so opening never hands back a silently different solid. + let errors = model + .results + .iter() + .filter(|r| !r.skipped) + .filter_map(|r| { + let message = r.error.clone()?; + let bodies = bodies_of(doc.features.iter().find(|f| f.id == r.id), &bodies); + Some(FeatureError { feature: r.name.clone(), message, bodies }) + }) + .collect(); Ok(Design { name: doc.name.clone(), source: std::sync::Arc::new(bodies.clone()), bodies, errors, cuts: Vec::new() }) } diff --git a/crates/geometry/src/partition.rs b/crates/geometry/src/partition.rs index e3e1bcf..76810c5 100644 --- a/crates/geometry/src/partition.rs +++ b/crates/geometry/src/partition.rs @@ -104,7 +104,7 @@ impl Design { let feature = format!("partition {}", cut.name); for b in &cut.bodies { if !bodies.iter().any(|x| &x.name == b) { - errors.push(FeatureError { feature: feature.clone(), message: format!("no body `{b}` to cut") }); + errors.push(FeatureError { feature: feature.clone(), message: format!("no body `{b}` to cut"), bodies: vec![b.clone()] }); } } let mut report = CutReport { name: cut.name.clone(), split: Vec::new(), faces: 0 }; @@ -123,7 +123,11 @@ impl Design { } None => { if listed { - errors.push(FeatureError { feature: feature.clone(), message: format!("the plane does not divide `{}`", body.name) }); + errors.push(FeatureError { + feature: feature.clone(), + message: format!("the plane does not divide `{}`", body.name), + bodies: vec![body.name.clone()], + }); } next.push(body); }