Open .solidcraft designs: rebuild, persistent face names, geometry commands (#4)

* geometry: open .solidcraft designs, rebuild them and keep persistent face names

open_design(bytes) reads a design (container or JSON) with solidcraft-io, rebuilds its
timeline with solidcraft-doc and returns its bodies; each Shape carries SolidCraft's
persistent face names by B-rep face index, so a face keeps its name through a parameter
edit. sample::plate_design builds a parametric test plate in code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: geometry.open, geometry.bodies and geometry.tessellate

The session holds the open design (shared, restored on a failed command, journalled when
it changes). Hostile-params coverage includes non-files, missing files, oversized paths and
extreme tolerances against an open design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: opening SolidCraft designs in the architecture and roadmap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* geometry: open_design_file reads through SolidCraft's bounded file access; simplify

The engine's hand-rolled capped read is replaced by open_design_file, which uses
solidcraft_io::vfs::read (refuses non-regular files before opening, works on the browser's
in-memory files). Drops the duplicate size check, simplifies the sample sketch and the
design-change test in Session::execute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 14:41:05 -04:00
1 parent 620ef6bc64
commit f36b1d9a69
13 files changed
+6290 -19

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
use std::sync::Arc;
use modelcraft_geometry::{Shape, open_design_file};
use serde_json::{Value, json};
use crate::{CommandSpec, EngineError, Result, Session};
pub const OPEN: CommandSpec = CommandSpec {
id: "geometry.open",
label: "Open Design",
params: r#"{"path": "part.solidcraft"} — rebuilds the SolidCraft design and makes its bodies the geometry; opening it again after an edit picks up the change"#,
run: open,
};
pub const BODIES: CommandSpec = CommandSpec {
id: "geometry.bodies",
label: "Bodies",
params: "{} — the open design's bodies: measures and persistent face names (by face index)",
run: bodies,
};
pub const TESSELLATE: CommandSpec = CommandSpec {
id: "geometry.tessellate",
label: "Tessellate Body",
params: r#"{"body": "Body1", "tolerance": 0.05} — triangulates the body's boundary within the chordal tolerance (mm); reports triangles per face"#,
run: tessellate,
};
fn open(s: &mut Session, p: &Value) -> Result<Value> {
let path = p
.get("path")
.and_then(Value::as_str)
.filter(|v| !v.is_empty())
.ok_or_else(|| EngineError::Params("`path` must be the design file's path".into()))?;
let design = open_design_file(path)?;
let out = json!({
"name": design.name,
"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<_>>(),
});
s.design = Some(Arc::new(design));
Ok(out)
}
fn bodies(s: &mut Session, _: &Value) -> Result<Value> {
let Some(design) = &s.design else { return Ok(json!([])) };
let mut out = Vec::with_capacity(design.bodies.len());
for b in &design.bodies {
let m = b.measure()?;
out.push(json!({
"name": b.name,
"volume": m.volume,
"area": m.area,
"centroid": m.centroid,
"bbox": [m.bbox_min, m.bbox_max],
"faces": b.faces().iter().map(|f| json!({"name": f.name, "exact": f.exact})).collect::<Vec<_>>(),
}));
}
Ok(Value::Array(out))
}
fn body<'a>(s: &'a Session, p: &Value) -> Result<&'a Shape> {
let name = p.get("body").and_then(Value::as_str).ok_or_else(|| EngineError::Params("`body` must be a body name".into()))?;
s.design.as_deref().and_then(|d| d.body(name)).ok_or_else(|| EngineError::Params(format!("no body `{name}` (open a design first)")))
}
fn tessellate(s: &mut Session, p: &Value) -> Result<Value> {
let shape = body(s, p)?;
let tolerance = p.get("tolerance").and_then(Value::as_f64).ok_or_else(|| EngineError::Params("`tolerance` must be a number (mm)".into()))?;
let mesh = shape.tessellate(tolerance)?;
let mut per_face = vec![0usize; shape.faces().len()];
for &f in &mesh.tri_face {
if let Some(n) = per_face.get_mut(f as usize) {
*n += 1;
}
}
let faces: Vec<Value> = shape.faces().iter().zip(per_face).map(|(f, n)| json!({"name": f.name, "triangles": n})).collect();
Ok(json!({"body": shape.name, "vertices": mesh.positions.len(), "triangles": mesh.triangles.len(), "faces": faces}))
}
+11 -1
View File
@@ -1,5 +1,6 @@
//! The command registry. Each area has its own module; [`ALL`] lists every command.
mod geometry;
mod mesh;
mod model;
@@ -7,7 +8,16 @@ use serde_json::Value;
use crate::{CommandSpec, EngineError, Result};
pub static ALL: &[CommandSpec] = &[model::INFO, mesh::NODE_ADD, mesh::NODE_REMOVE, mesh::ELEMENT_ADD, mesh::ELEMENT_REMOVE];
pub static ALL: &[CommandSpec] = &[
model::INFO,
geometry::OPEN,
geometry::BODIES,
geometry::TESSELLATE,
mesh::NODE_ADD,
mesh::NODE_REMOVE,
mesh::ELEMENT_ADD,
mesh::ELEMENT_REMOVE,
];
/// A required label parameter (1..=u32::MAX; the model checks its own range).
pub(crate) fn label(p: &Value, key: &str) -> Result<u32> {