forked from 1b-agent/fieldcraft-upstream
geometry: a feature that fails to rebuild names the bodies it changes, and opening warns
The design file stores no measure to compare with, so the rebuild's own failures are the guard: each failed feature lists the bodies it names (all of them when it names none), and geometry.open replies with a warning per failure, which the window shows with the open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
ecd78b9ee6
commit
358ed2f54f
5 files changed
+61
-12
No files matched your search
@@ -16,7 +16,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
|||||||
pub const OPEN: CommandSpec = CommandSpec {
|
pub const OPEN: CommandSpec = CommandSpec {
|
||||||
id: "geometry.open",
|
id: "geometry.open",
|
||||||
label: "Open Design",
|
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),
|
form: Some(super::forms::OPEN_DESIGN),
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
requires: &[],
|
requires: &[],
|
||||||
@@ -86,7 +86,9 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let out = json!({
|
let out = json!({
|
||||||
"name": design.name,
|
"name": design.name,
|
||||||
"bodies": design.bodies.iter().map(|b| b.name.as_str()).collect::<Vec<_>>(),
|
"bodies": design.bodies.iter().map(|b| b.name.as_str()).collect::<Vec<_>>(),
|
||||||
"errors": design.errors.iter().map(|e| json!({"feature": e.feature, "message": e.message})).collect::<Vec<_>>(),
|
"errors": design.errors.iter().map(|e| json!({"feature": e.feature, "message": e.message, "bodies": e.bodies})).collect::<Vec<_>>(),
|
||||||
|
// Shown with the open: a body that differs from the design is never silent.
|
||||||
|
"warnings": design.errors.iter().map(|e| e.warning()).collect::<Vec<_>>(),
|
||||||
"model": if new_model { "new" } else { "kept" },
|
"model": if new_model { "new" } else { "kept" },
|
||||||
"related": fit.related,
|
"related": fit.related,
|
||||||
"stale": {"mesh_bodies": fit.mesh_bodies, "sets": fit.sets, "seeds": fit.seeds},
|
"stale": {"mesh_bodies": fit.mesh_bodies, "sets": fit.sets, "seeds": fit.seeds},
|
||||||
|
|||||||
@@ -734,6 +734,7 @@ mod tests {
|
|||||||
let opened = s.execute("geometry.open", &json!({"path": path})).unwrap();
|
let opened = s.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||||
assert_eq!(opened["bodies"], json!(["Body1"]));
|
assert_eq!(opened["bodies"], json!(["Body1"]));
|
||||||
assert_eq!(opened["errors"], json!([]));
|
assert_eq!(opened["errors"], json!([]));
|
||||||
|
assert_eq!(opened["warnings"], json!([]));
|
||||||
assert_eq!(s.journal.len(), 1);
|
assert_eq!(s.journal.len(), 1);
|
||||||
|
|
||||||
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
|
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ thiserror = { workspace = true }
|
|||||||
zip = { workspace = true }
|
zip = { workspace = true }
|
||||||
quick-xml = { workspace = true }
|
quick-xml = { workspace = true }
|
||||||
solidcraft-engine = { workspace = true, optional = true }
|
solidcraft-engine = { workspace = true, optional = true }
|
||||||
serde_json = { workspace = true, optional = true }
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Build designs from SolidCraft command scripts (`script_design`): the test corpus and tooling.
|
# Build designs from SolidCraft command scripts (`script_design`): the test corpus and tooling.
|
||||||
# Off by default; the library itself only opens designs.
|
# Off by default; the library itself only opens designs.
|
||||||
scripts = ["dep:solidcraft-engine", "dep:serde_json"]
|
scripts = ["dep:solidcraft-engine"]
|
||||||
@@ -34,6 +34,42 @@ pub struct Design {
|
|||||||
pub struct FeatureError {
|
pub struct FeatureError {
|
||||||
pub feature: String,
|
pub feature: String,
|
||||||
pub message: 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<Vec<_>>().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<String> {
|
||||||
|
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<String> = 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 {
|
impl Design {
|
||||||
@@ -55,12 +91,6 @@ pub(crate) fn rebuild(doc: &Document) -> Result<Design> {
|
|||||||
m.evaluate(doc);
|
m.evaluate(doc);
|
||||||
Ok(m)
|
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
|
// 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.
|
// 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())))?;
|
let state = guard("place components", || Ok(solidcraft_doc::world_state(doc, &model.state())))?;
|
||||||
@@ -72,6 +102,18 @@ pub(crate) fn rebuild(doc: &Document) -> Result<Design> {
|
|||||||
})?;
|
})?;
|
||||||
bodies.push(Shape { name: b.name.clone(), body: b.body.clone(), faces });
|
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() })
|
Ok(Design { name: doc.name.clone(), source: std::sync::Arc::new(bodies.clone()), bodies, errors, cuts: Vec::new() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ impl Design {
|
|||||||
let feature = format!("partition {}", cut.name);
|
let feature = format!("partition {}", cut.name);
|
||||||
for b in &cut.bodies {
|
for b in &cut.bodies {
|
||||||
if !bodies.iter().any(|x| &x.name == b) {
|
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 };
|
let mut report = CutReport { name: cut.name.clone(), split: Vec::new(), faces: 0 };
|
||||||
@@ -123,7 +123,11 @@ impl Design {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if listed {
|
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);
|
next.push(body);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user