diff --git a/AGENTS.md b/AGENTS.md
index 7ad76e27..090d8fee 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -50,7 +50,8 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
(`cargo xtask assets` enforces it). Screenshots of Autodesk software are never committed.
Breaking this rule is the most serious mistake you can make in this repo.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
- (`CommandSpec`: id = Fusion's command id where Fusion has the command, else a dotted id; label;
+ (`CommandSpec`: id = our own dotted, lower-case id `.[.]` such as
+ `solid.extrude` or `sketch.rectangle.two_point`, never another product's command id; label;
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + tests. The toolbar,
dialogs, sketch tools, palette, scripts, CLI and control channel all reach the same commands.
- **Programmatic calls never open dialogs.** `engine.execute` runs the JSON form; only toolbar
@@ -74,8 +75,8 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
- `cargo run --release -p solvecraft -- --sample --control PORT` (sample design + control channel).
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
- - `{"id":1,"method":"engine.execute","params":{"command":"Extrude","params":{"distance":20}}}`
- - `{"id":2,"method":"ui.start","params":{"command":"SketchCreate"}}` then `ui.click {x, y}`
+ - `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
+ - `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
- **For UI work, look at the result** (screenshot, read the PNG). Without a window use
`ui.render` or `solvecraft-cli snapshot`.
diff --git a/CLAUDE.md b/CLAUDE.md
index 7ad76e27..090d8fee 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -50,7 +50,8 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
(`cargo xtask assets` enforces it). Screenshots of Autodesk software are never committed.
Breaking this rule is the most serious mistake you can make in this repo.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
- (`CommandSpec`: id = Fusion's command id where Fusion has the command, else a dotted id; label;
+ (`CommandSpec`: id = our own dotted, lower-case id `.[.]` such as
+ `solid.extrude` or `sketch.rectangle.two_point`, never another product's command id; label;
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + tests. The toolbar,
dialogs, sketch tools, palette, scripts, CLI and control channel all reach the same commands.
- **Programmatic calls never open dialogs.** `engine.execute` runs the JSON form; only toolbar
@@ -74,8 +75,8 @@ People trust SolveCraft with their designs; a crash loses their work. **This out
- `cargo run --release -p solvecraft -- --sample --control PORT` (sample design + control channel).
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
- - `{"id":1,"method":"engine.execute","params":{"command":"Extrude","params":{"distance":20}}}`
- - `{"id":2,"method":"ui.start","params":{"command":"SketchCreate"}}` then `ui.click {x, y}`
+ - `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
+ - `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
- **For UI work, look at the result** (screenshot, read the PNG). Without a window use
`ui.render` or `solvecraft-cli snapshot`.
diff --git a/Cargo.toml b/Cargo.toml
index 57ac3cf5..c7989515 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,7 @@ edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.90"
authors = ["SolveCraft contributors"]
-repository = "https://github.com/storytold/solvecraft"
+repository = "https://github.com/bherbruck/solvecraft"
[workspace.dependencies]
# internal
diff --git a/README.md b/README.md
index 79d9d1a1..baacc57d 100644
--- a/README.md
+++ b/README.md
@@ -69,10 +69,10 @@ A script is a JSON list of commands, the same ones the UI runs:
```json
{"commands": [
- {"command": "SketchCreate", "params": {"plane": "XY"}},
- {"command": "ShapeRectangleTwoPoint", "params": {"p0": [0, 0], "p1": [40, 30]}},
- {"command": "Extrude", "params": {"distance": 20}},
- {"command": "FusionFilletEdgesCommand", "params": {"edges": [[0, 0, 10]], "radius": 3}}
+ {"command": "sketch.create", "params": {"plane": "XY"}},
+ {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}},
+ {"command": "solid.extrude", "params": {"distance": 20}},
+ {"command": "solid.fillet", "params": {"edges": [[0, 0, 10]], "radius": 3}}
]}
```
diff --git a/ROADMAP.md b/ROADMAP.md
index 247fa703..7c81d298 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -52,7 +52,7 @@ headline counts in-scope tabs only and lists the deferred ones separately.
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done (oracle 67-76). PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, no Fusion oracle (licence), tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table, DXF export), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Bend (Fold) has no engine command yet | in progress | 36 |
| M11 | 2D drawings | deferred | — |
| M12 | Release: installers, signing, docs, performance | planned | 16 |
-| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (FusionStartDesignConfigModeCmd, FusionShowDesignConfigPanelCmd, config.column/row/activate). Left: configuration rules, per-row materials and appearances | in progress | 4 |
+| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Left: configuration rules, per-row materials and appearances | in progress | 4 |
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solvecraft/2"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solvecraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
## Known limitations
diff --git a/apps/solvecraft-cli/src/main.rs b/apps/solvecraft-cli/src/main.rs
index cf7cc1c2..98750378 100644
--- a/apps/solvecraft-cli/src/main.rs
+++ b/apps/solvecraft-cli/src/main.rs
@@ -150,13 +150,13 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
_ => {}
}
for out in flags(args, "--out") {
- s.execute("ExportCommand", &json!({"path": out})).map_err(|e| e.to_string())?;
+ s.execute("file.export", &json!({"path": out})).map_err(|e| e.to_string())?;
}
if let Some(p) = flag(args, "--save") {
- s.execute("SaveDocumentAsCommand", &json!({"path": p})).map_err(|e| e.to_string())?;
+ s.execute("file.save_as", &json!({"path": p})).map_err(|e| e.to_string())?;
}
if !args.iter().any(|a| a == "--quiet") {
- let m = s.execute("MeasureCommand", &json!({})).map_err(|e| e.to_string())?;
+ let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
println!("{}", pretty(&m));
}
Ok(())
@@ -165,7 +165,7 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
fn cmd_eval(args: &[String]) -> Result<(), String> {
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(|| USAGE.to_string())?;
let mut s = load(path)?;
- let m = s.execute("MeasureCommand", &json!({})).map_err(|e| e.to_string())?;
+ let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
let out = json!({
"body_count": m["body_count"],
@@ -239,7 +239,7 @@ fn cmd_save_stress(args: &[String]) -> Result<(), String> {
s.doc_mut().params.extend(filler);
for r in 0..rounds {
s.doc_mut().set_param("save_round", &r.to_string(), None, None).map_err(|e| e.to_string())?;
- s.execute("SaveDocumentCommand", &json!({ "path": path })).map_err(|e| e.to_string())?;
+ s.execute("file.save", &json!({ "path": path })).map_err(|e| e.to_string())?;
}
Ok(())
}
@@ -266,10 +266,10 @@ fn cmd_bench_edit(args: &[String]) -> Result<(), String> {
Ok(())
};
let expr = expr.replace("{old}", &old);
- time(&mut s, "edit", "ChangeParameterCommand", json!({"name": param, "expression": expr}))?;
- time(&mut s, "set back", "ChangeParameterCommand", json!({"name": param, "expression": old}))?;
- time(&mut s, "undo", "UndoCommand", json!({}))?;
- time(&mut s, "redo", "RedoCommand", json!({}))?;
+ time(&mut s, "edit", "parameters.change", json!({"name": param, "expression": expr}))?;
+ time(&mut s, "set back", "parameters.change", json!({"name": param, "expression": old}))?;
+ time(&mut s, "undo", "edit.undo", json!({}))?;
+ time(&mut s, "redo", "edit.redo", json!({}))?;
println!("{}", pretty(&json!(out)));
Ok(())
}
diff --git a/apps/solvecraft-cli/src/oracle.rs b/apps/solvecraft-cli/src/oracle.rs
index 7e31a09c..e26c85d6 100644
--- a/apps/solvecraft-cli/src/oracle.rs
+++ b/apps/solvecraft-cli/src/oracle.rs
@@ -31,7 +31,7 @@ fn check_case(dir: &str) -> Value {
if let Err(e) = s.run_script(&script) {
return json!({"case": name, "pass": false, "error": e.to_string()});
}
- let got = match s.execute("MeasureCommand", &json!({})) {
+ let got = match s.execute("inspect.measure", &json!({})) {
Ok(m) => m,
Err(e) => return json!({"case": name, "pass": false, "error": e.to_string()}),
};
@@ -183,7 +183,7 @@ fn check_step_case(dir: &str) -> Value {
Ok(v) => v,
Err(e) => return json!({"case": name, "pass": false, "error": e.to_string()}),
};
- let got = match s.execute("MeasureCommand", &json!({})) {
+ let got = match s.execute("inspect.measure", &json!({})) {
Ok(m) => m,
Err(e) => return json!({"case": name, "pass": false, "error": e.to_string()}),
};
diff --git a/apps/solvecraft-cli/src/recipe.rs b/apps/solvecraft-cli/src/recipe.rs
index c63f39c5..7a51d47f 100644
--- a/apps/solvecraft-cli/src/recipe.rs
+++ b/apps/solvecraft-cli/src/recipe.rs
@@ -44,23 +44,23 @@ fn sketch_cmds(f: &Value, out: &mut Vec) -> Result<(), String> {
if let Some(n) = f.get("name") {
p["name"] = n.clone();
}
- out.push(json!({"command": "SketchCreate", "params": p}));
+ out.push(json!({"command": "sketch.create", "params": p}));
for e in f.get("entities").and_then(Value::as_array).into_iter().flatten() {
let id = e.get("id").cloned().unwrap_or(Value::Null);
let construction = e.get("construction").cloned().unwrap_or(json!(false));
match e.get("type").and_then(Value::as_str) {
- Some("line") => out.push(json!({"command": "DrawPolyline", "params": {"points": [parg(e.get("start")), parg(e.get("end"))], "ids": [id], "construction": construction}})),
- Some("circle") => out.push(json!({"command": "CircleCenterRadius", "params": {"center": parg(e.get("center")), "radius": e.get("radius"), "id": id, "construction": construction}})),
- Some("arc_three_point") => out.push(json!({"command": "ArcThreePoint", "params": {"start": parg(e.get("start")), "end": parg(e.get("end")), "through": e.get("mid").or(e.get("through")), "id": id}})),
- Some("arc_center_start_sweep") => out.push(json!({"command": "ArcCenterTwoPoint", "params": {"center": parg(e.get("center")), "start": parg(e.get("start")), "sweep": e.get("sweep_deg").or(e.get("sweep")), "id": id}})),
+ Some("line") => out.push(json!({"command": "sketch.line", "params": {"points": [parg(e.get("start")), parg(e.get("end"))], "ids": [id], "construction": construction}})),
+ Some("circle") => out.push(json!({"command": "sketch.circle.center", "params": {"center": parg(e.get("center")), "radius": e.get("radius"), "id": id, "construction": construction}})),
+ Some("arc_three_point") => out.push(json!({"command": "sketch.arc.three_point", "params": {"start": parg(e.get("start")), "end": parg(e.get("end")), "through": e.get("mid").or(e.get("through")), "id": id}})),
+ Some("arc_center_start_sweep") => out.push(json!({"command": "sketch.arc.center_point", "params": {"center": parg(e.get("center")), "start": parg(e.get("start")), "sweep": e.get("sweep_deg").or(e.get("sweep")), "id": id}})),
Some("arc") => {
if e.get("center").is_some() {
- out.push(json!({"command": "ArcCenterTwoPoint", "params": {"center": parg(e.get("center")), "start": parg(e.get("start")), "end": parg(e.get("end")), "id": id}}))
+ out.push(json!({"command": "sketch.arc.center_point", "params": {"center": parg(e.get("center")), "start": parg(e.get("start")), "end": parg(e.get("end")), "id": id}}))
} else {
- out.push(json!({"command": "ArcThreePoint", "params": {"start": parg(e.get("start")), "end": parg(e.get("end")), "through": parg(e.get("through").or(e.get("mid"))), "id": id}}))
+ out.push(json!({"command": "sketch.arc.three_point", "params": {"start": parg(e.get("start")), "end": parg(e.get("end")), "through": parg(e.get("through").or(e.get("mid"))), "id": id}}))
}
}
- Some("point") => out.push(json!({"command": "DrawPoint", "params": {"point": parg(e.get("at").map(|a| json!({"at": a})).as_ref().or(Some(e))), "id": id}})),
+ Some("point") => out.push(json!({"command": "sketch.point", "params": {"point": parg(e.get("at").map(|a| json!({"at": a})).as_ref().or(Some(e))), "id": id}})),
// A 3D helix fit: the sweep along it becomes a coil (see `helix_path`).
Some("fitted_spline") if e.get("fit_points").and_then(Value::as_array).is_some_and(|a| a.iter().any(|p| p.get(2).is_some())) => {}
Some(other) => return Err(format!("recipe: sketch entity type `{other}` is not supported yet")),
@@ -77,9 +77,9 @@ fn sketch_cmds(f: &Value, out: &mut Vec) -> Result<(), String> {
p[k] = v.clone();
}
}
- out.push(json!({"command": "SketchDimension", "params": p}));
+ out.push(json!({"command": "sketch.dimension", "params": p}));
}
- out.push(json!({"command": "SketchStop", "params": {}}));
+ out.push(json!({"command": "sketch.finish", "params": {}}));
Ok(())
}
@@ -138,33 +138,33 @@ fn constraint_cmd(c: &Value) -> Result {
let at = |i: usize| r.get(i).cloned().unwrap_or(Value::Null);
let is_point = |v: &Value| v.as_str().is_some_and(|s| s.contains('.') || s == "origin" || s.starts_with('p'));
let (cmd, p) = match ty.as_str() {
- "horizontal" | "vertical" if r.len() >= 2 => ("ConstraintHorizontalVertical", json!({"points": [at(0), at(1)], "mode": ty})),
- "horizontal" | "vertical" => ("ConstraintHorizontalVertical", json!({"line": at(0), "mode": ty})),
- "horizontal_points" => ("ConstraintHorizontalVertical", json!({"points": [at(0), at(1)], "mode": "horizontal"})),
- "vertical_points" => ("ConstraintHorizontalVertical", json!({"points": [at(0), at(1)], "mode": "vertical"})),
+ "horizontal" | "vertical" if r.len() >= 2 => ("sketch.constraint.horizontal_vertical", json!({"points": [at(0), at(1)], "mode": ty})),
+ "horizontal" | "vertical" => ("sketch.constraint.horizontal_vertical", json!({"line": at(0), "mode": ty})),
+ "horizontal_points" => ("sketch.constraint.horizontal_vertical", json!({"points": [at(0), at(1)], "mode": "horizontal"})),
+ "vertical_points" => ("sketch.constraint.horizontal_vertical", json!({"points": [at(0), at(1)], "mode": "vertical"})),
"coincident" | "point_on_curve" => {
// The point goes first.
if is_point(&at(0)) {
- ("ConstraintCoincident", json!({"a": at(0), "b": at(1)}))
+ ("sketch.constraint.coincident", json!({"a": at(0), "b": at(1)}))
} else {
- ("ConstraintCoincident", json!({"a": at(1), "b": at(0)}))
+ ("sketch.constraint.coincident", json!({"a": at(1), "b": at(0)}))
}
}
"midpoint" => {
if is_point(&at(0)) {
- ("ConstraintMidPoint", json!({"point": at(0), "line": at(1)}))
+ ("sketch.constraint.midpoint", json!({"point": at(0), "line": at(1)}))
} else {
- ("ConstraintMidPoint", json!({"point": at(1), "line": at(0)}))
+ ("sketch.constraint.midpoint", json!({"point": at(1), "line": at(0)}))
}
}
- "symmetric" | "symmetry" => ("ConstraintSymmetry", json!({"a": at(0), "b": at(1), "line": at(2)})),
- "fix" => ("ConstraintFix", json!({"entity": at(0), "fixed": true})),
- "tangent" => ("ConstraintTangent", json!({"a": at(0), "b": at(1)})),
- "equal" => ("ConstraintEqual", json!({"a": at(0), "b": at(1)})),
- "parallel" => ("ConstraintParallel", json!({"a": at(0), "b": at(1)})),
- "perpendicular" => ("ConstraintPerpendicular", json!({"a": at(0), "b": at(1)})),
- "concentric" => ("ConstraintConcentric", json!({"a": at(0), "b": at(1)})),
- "collinear" => ("ConstraintCollinear", json!({"a": at(0), "b": at(1)})),
+ "symmetric" | "symmetry" => ("sketch.constraint.symmetry", json!({"a": at(0), "b": at(1), "line": at(2)})),
+ "fix" => ("sketch.constraint.fix", json!({"entity": at(0), "fixed": true})),
+ "tangent" => ("sketch.constraint.tangent", json!({"a": at(0), "b": at(1)})),
+ "equal" => ("sketch.constraint.equal", json!({"a": at(0), "b": at(1)})),
+ "parallel" => ("sketch.constraint.parallel", json!({"a": at(0), "b": at(1)})),
+ "perpendicular" => ("sketch.constraint.perpendicular", json!({"a": at(0), "b": at(1)})),
+ "concentric" => ("sketch.constraint.concentric", json!({"a": at(0), "b": at(1)})),
+ "collinear" => ("sketch.constraint.collinear", json!({"a": at(0), "b": at(1)})),
other => return Err(format!("recipe: constraint `{other}` is not supported yet")),
};
Ok(json!({"command": cmd, "params": p}))
@@ -294,7 +294,7 @@ fn circle_fit(pts: &[[f64; 3]]) -> Option<(f64, f64)> {
pub fn to_script(recipe: &Value) -> Result {
let mut out: Vec = Vec::new();
for p in recipe.get("parameters").and_then(Value::as_array).into_iter().flatten() {
- out.push(json!({"command": "ChangeParameterCommand", "params": {"name": p.get("name"), "expression": p.get("expression").or(p.get("value")), "unit": p.get("unit").cloned().unwrap_or(json!("mm"))}}));
+ out.push(json!({"command": "parameters.change", "params": {"name": p.get("name"), "expression": p.get("expression").or(p.get("value")), "unit": p.get("unit").cloned().unwrap_or(json!("mm"))}}));
}
let features = recipe.get("features").and_then(Value::as_array).ok_or("recipe: missing `features`")?;
for f in features {
@@ -305,9 +305,9 @@ pub fn to_script(recipe: &Value) -> Result {
Some("extrude") => {
let mut p = json!({"sketch": profile_sketch(f), "profiles": profiles_of(f), "operation": op(f), "name": name, "body_names": bodies});
extent(f, &mut p);
- out.push(json!({"command": "Extrude", "params": p}));
+ out.push(json!({"command": "solid.extrude", "params": p}));
}
- Some("combine") => out.push(json!({"command": "FusionCombineCommand", "params": {
+ Some("combine") => out.push(json!({"command": "solid.combine", "params": {
"target": f.get("target_body").or(f.get("target")), "tools": f.get("tool_bodies").or(f.get("tools")),
"operation": op(f), "keep_tools": f.get("keep_tools").cloned().unwrap_or(json!(false)), "name": name}})),
Some("revolve") => {
@@ -316,7 +316,7 @@ pub fn to_script(recipe: &Value) -> Result {
Some(a) => a.get("line").or(a.get("ref")).cloned().unwrap_or_else(|| a.clone()),
None => Value::Null,
};
- out.push(json!({"command": "Revolve", "params": {
+ out.push(json!({"command": "solid.revolve", "params": {
"sketch": profile_sketch(f), "profiles": profiles_of(f), "axis": axis,
"angle": f.get("angle").cloned().unwrap_or(json!("360 deg")), "operation": op(f), "name": name, "body_names": bodies}}));
}
@@ -329,11 +329,11 @@ pub fn to_script(recipe: &Value) -> Result {
p["count2"] = d2.get("count").cloned().unwrap_or(Value::Null);
p["spacing2"] = d2.get("spacing").cloned().unwrap_or(Value::Null);
}
- out.push(json!({"command": "PatternRectangular", "params": p}));
+ out.push(json!({"command": "solid.pattern.rectangular", "params": p}));
}
Some("circular_pattern") => {
let axis = f.get("axis").map(|a| json!({"origin": a.get("origin").cloned().unwrap_or(json!([0, 0, 0])), "dir": a.get("dir")})).unwrap_or(Value::Null);
- out.push(json!({"command": "PatternCircular", "params": {"features": feature_refs(f.get("features")), "axis": axis, "count": f.get("count"), "angle": f.get("total_angle").cloned().unwrap_or(json!(360)), "name": name}}));
+ out.push(json!({"command": "solid.pattern.circular", "params": {"features": feature_refs(f.get("features")), "axis": axis, "count": f.get("count"), "angle": f.get("total_angle").cloned().unwrap_or(json!(360)), "name": name}}));
}
Some("sm_flange") => match f.get("type").and_then(Value::as_str) {
Some("edge") => {
@@ -344,11 +344,11 @@ pub fn to_script(recipe: &Value) -> Result {
if f.get("flip").and_then(Value::as_bool) == Some(true) {
p["flip"] = json!(true);
}
- out.push(json!({"command": "FusionSheetMetalFlangeCommand", "params": p}));
+ out.push(json!({"command": "sheet.flange", "params": p}));
}
- Some("contour") => out.push(json!({"command": "FusionSheetMetalFlangeCommand", "params": {
+ Some("contour") => out.push(json!({"command": "sheet.flange", "params": {
"type": "contour", "sketch": f.get("profile_sketch"), "curves": f.get("profile_curves"), "distance": f.get("distance"), "name": name}})),
- _ => out.push(json!({"command": "FusionSheetMetalFlangeCommand", "params": {
+ _ => out.push(json!({"command": "sheet.flange", "params": {
"type": "base", "sketch": profile_sketch(f), "profiles": profiles_of(f), "name": name, "body_names": bodies}})),
},
Some("sheet_metal_rule_edit") => {
@@ -368,7 +368,7 @@ pub fn to_script(recipe: &Value) -> Result {
p["thickness"] = json!(format!("{v} {u}"));
}
}
- out.push(json!({"command": "FusionSheetMetalRulesCommand", "params": p}));
+ out.push(json!({"command": "sheet.manage_rules", "params": p}));
}
Some("sm_hem") => {
let e = f.get("edge").and_then(|e| e.get("point")).cloned().unwrap_or(Value::Null);
@@ -376,9 +376,9 @@ pub fn to_script(recipe: &Value) -> Result {
if f.get("flipped").and_then(Value::as_bool) == Some(true) {
p["flip"] = json!(true);
}
- out.push(json!({"command": "FusionSheetMetalHemFlangeCommand", "params": p}));
+ out.push(json!({"command": "sheet.hem", "params": p}));
}
- Some("sm_unfold") => out.push(json!({"command": "FusionSheetmetalUnfoldCommand", "params": {"name": name}})),
+ Some("sm_unfold") => out.push(json!({"command": "sheet.unfold", "params": {"name": name}})),
Some("sm_refold") => out.push(json!({"command": "sheet.refold", "params": {"name": name}})),
Some("path_pattern") => {
let path = f.get("path").cloned().unwrap_or(Value::Null);
@@ -392,7 +392,7 @@ pub fn to_script(recipe: &Value) -> Result {
} else {
p["features"] = feature_refs(f.get("features"));
}
- out.push(json!({"command": "PatternOnPath", "params": p}));
+ out.push(json!({"command": "solid.pattern.path", "params": p}));
}
Some("loft") => {
let sections: Vec = f
@@ -405,24 +405,24 @@ pub fn to_script(recipe: &Value) -> Result {
None => json!({"sketch": sec.get("sketch"), "profiles": [sec]}),
})
.collect();
- out.push(json!({"command": "SolidLoft", "params": {"sections": sections, "operation": op(f), "name": name, "body_names": bodies}}));
+ out.push(json!({"command": "solid.loft", "params": {"sections": sections, "operation": op(f), "name": name, "body_names": bodies}}));
}
Some("sweep") if helix_path(features, f).is_some() => {
// A swept circle along a fitted 3D helix: a coil (the true helix).
let Some((base, diameter, pitch, turns, start, cw, size)) = helix_path(features, f) else { continue };
- out.push(json!({"command": "PrimitiveCoil", "params": {
+ out.push(json!({"command": "solid.coil", "params": {
"base": base, "diameter": diameter, "pitch": pitch, "revolutions": turns, "section_size": size,
"start_angle": format!("{start} rad"), "clockwise": cw, "operation": op(f), "name": name, "body_names": bodies}}));
}
Some("sweep") => {
let path = f.get("path").cloned().unwrap_or(Value::Null);
- out.push(json!({"command": "Sweep", "params": {
+ out.push(json!({"command": "solid.sweep", "params": {
"sketch": profile_sketch(f), "profiles": profiles_of(f), "path_sketch": path.get("sketch"), "path": path.get("curves"),
"operation": op(f), "name": name, "body_names": bodies}}));
}
Some("shell") => {
let faces: Vec = f.get("faces_removed").and_then(Value::as_array).into_iter().flatten().filter_map(|x| x.get("point").cloned()).collect();
- out.push(json!({"command": "FusionShellBodyCommand", "params": {"faces": faces, "thickness": f.get("inside_thickness"), "name": name}}));
+ out.push(json!({"command": "solid.shell", "params": {"faces": faces, "thickness": f.get("inside_thickness"), "name": name}}));
}
Some("draft") => {
let faces: Vec = f.get("faces").and_then(Value::as_array).into_iter().flatten().filter_map(|x| x.get("point").cloned()).collect();
@@ -433,7 +433,7 @@ pub fn to_script(recipe: &Value) -> Result {
if f.get("flipped").and_then(Value::as_bool).unwrap_or(false) {
angle = -angle;
}
- out.push(json!({"command": "FusionDraftCommand", "params": {"faces": faces, "angle": angle, "neutral": neutral, "pull": n, "name": name}}));
+ out.push(json!({"command": "solid.draft", "params": {"faces": faces, "angle": angle, "neutral": neutral, "pull": n, "name": name}}));
}
Some("thread") => {
let face = f.get("face").and_then(|x| x.get("point")).cloned().unwrap_or(Value::Null);
@@ -443,7 +443,7 @@ pub fn to_script(recipe: &Value) -> Result {
{
p["length"] = l.clone();
}
- out.push(json!({"command": "FusionThreadCommand", "params": p}));
+ out.push(json!({"command": "solid.thread", "params": p}));
}
Some("hole") => {
let n = f.get("face").and_then(|fc| fc.get("plane_normal_outward").or(fc.get("normal_at_point_on_face"))).cloned();
@@ -495,20 +495,20 @@ pub fn to_script(recipe: &Value) -> Result {
p[k] = v.clone();
}
}
- out.push(json!({"command": "FusionHoleCommand", "params": p}));
+ out.push(json!({"command": "solid.hole", "params": p}));
}
Some("construction_plane") => match f.get("method").and_then(Value::as_str) {
- Some("offset") => out.push(json!({"command": "ConstructionPlaneOffsetFromPlaneCommand", "params": {"base": f.get("base"), "offset": f.get("offset"), "name": name}})),
+ Some("offset") => out.push(json!({"command": "construct.plane.offset", "params": {"base": f.get("base"), "offset": f.get("offset"), "name": name}})),
_ => {
// Use the resulting plane as recorded.
let (o, n) = (f.get("result_origin").cloned().unwrap_or(json!([0, 0, 0])), f.get("result_normal").cloned().unwrap_or(Value::Null));
if n.is_null() {
return Err(format!("recipe: construction plane method {:?} without result_normal", f.get("method")));
}
- out.push(json!({"command": "ConstructionPlaneOffsetFromPlaneCommand", "params": {"base": {"origin": o, "normal": n}, "offset": 0, "name": name}}));
+ out.push(json!({"command": "construct.plane.offset", "params": {"base": {"origin": o, "normal": n}, "offset": 0, "name": name}}));
}
},
- Some("split_body") => out.push(json!({"command": "FusionSplitBodyCommand", "params": {"body": f.get("body"), "plane": f.get("tool"), "name": name}})),
+ Some("split_body") => out.push(json!({"command": "solid.split_body", "params": {"body": f.get("body"), "plane": f.get("tool"), "name": name}})),
Some("mirror") => {
let pl = f.get("plane").cloned().unwrap_or(Value::Null);
let plane = match pl.get("ref").and_then(Value::as_str) {
@@ -519,13 +519,13 @@ pub fn to_script(recipe: &Value) -> Result {
if f.get("mirror_of").and_then(Value::as_str) == Some("bodies") {
p = json!({"bodies": f.get("bodies"), "combine": f.get("combine").cloned().unwrap_or(json!(false)), "plane": plane, "name": name});
}
- out.push(json!({"command": "MirrorCommand", "params": p}));
+ out.push(json!({"command": "solid.mirror", "params": p}));
}
Some("fillet") => {
- out.push(json!({"command": "FusionFilletEdgesCommand", "params": {"edges": edge_points(f), "radius": f.get("radius"), "name": name}}))
+ out.push(json!({"command": "solid.fillet", "params": {"edges": edge_points(f), "radius": f.get("radius"), "name": name}}))
}
Some("chamfer") => {
- out.push(json!({"command": "FusionChamferCommand", "params": {"edges": edge_points(f), "distance": f.get("distance"), "name": name}}))
+ out.push(json!({"command": "solid.chamfer", "params": {"edges": edge_points(f), "distance": f.get("distance"), "name": name}}))
}
Some(other) => return Err(format!("recipe: feature op `{other}` is not supported yet")),
None => return Err("recipe: feature without `op`".into()),
diff --git a/apps/solvecraft-cli/tests/corpus/scripts/components-arm.json b/apps/solvecraft-cli/tests/corpus/scripts/components-arm.json
index d3235d00..3b7cb356 100644
--- a/apps/solvecraft-cli/tests/corpus/scripts/components-arm.json
+++ b/apps/solvecraft-cli/tests/corpus/scripts/components-arm.json
@@ -1,15 +1,15 @@
{"commands": [
- {"command": "PrimitiveBox", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Base"}},
- {"command": "FusionCreateNewComponentCommand", "params": {"name": "Arm"}},
- {"command": "PrimitiveBox", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}},
- {"command": "FusionFilletEdgesCommand", "params": {"edges": [[25, 4, 4]], "radius": 1}},
+ {"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Base"}},
+ {"command": "component.create", "params": {"name": "Arm"}},
+ {"command": "solid.box", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}},
+ {"command": "solid.fillet", "params": {"edges": [[25, 4, 4]], "radius": 1}},
{"command": "component.activate", "params": {"component": 0}},
{"command": "occurrence.move", "params": {"occurrence": 3, "translate": [0, 50, 0]}},
{"command": "occurrence.copy", "params": {"component": "Arm", "translate": [0, 20, 0]}},
{"command": "component.paste_new", "params": {"component": "Arm", "translate": [0, -30, 0]}},
{"command": "occurrence.ground", "params": {"occurrence": 3}},
- {"command": "SketchCreate", "params": {"plane": "XY", "name": "S1"}},
- {"command": "CircleCenterRadius", "params": {"center": [5, 5], "radius": 2}},
- {"command": "SketchStop", "params": {}},
- {"command": "Extrude", "params": {"distance": 10, "operation": "cut"}}
+ {"command": "sketch.create", "params": {"plane": "XY", "name": "S1"}},
+ {"command": "sketch.circle.center", "params": {"center": [5, 5], "radius": 2}},
+ {"command": "sketch.finish", "params": {}},
+ {"command": "solid.extrude", "params": {"distance": 10, "operation": "cut"}}
]}
diff --git a/apps/solvecraft-cli/tests/corpus/scripts/joints-hinge.json b/apps/solvecraft-cli/tests/corpus/scripts/joints-hinge.json
index 37467809..32c25a7f 100644
--- a/apps/solvecraft-cli/tests/corpus/scripts/joints-hinge.json
+++ b/apps/solvecraft-cli/tests/corpus/scripts/joints-hinge.json
@@ -1,14 +1,14 @@
{"commands": [
- {"command": "ChangeParameterCommand", "params": {"name": "open_angle", "expression": "90 deg"}},
- {"command": "FusionCreateNewComponentCommand", "params": {"name": "LeafA"}},
- {"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}},
- {"command": "PrimitiveCylinder", "params": {"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}},
+ {"command": "parameters.change", "params": {"name": "open_angle", "expression": "90 deg"}},
+ {"command": "component.create", "params": {"name": "LeafA"}},
+ {"command": "solid.box", "params": {"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}},
+ {"command": "solid.cylinder", "params": {"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}},
{"command": "component.activate", "params": {"component": "root"}},
- {"command": "FusionCreateNewComponentCommand", "params": {"name": "LeafB"}},
- {"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}},
- {"command": "PrimitiveCylinder", "params": {"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}},
+ {"command": "component.create", "params": {"name": "LeafB"}},
+ {"command": "solid.box", "params": {"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}},
+ {"command": "solid.cylinder", "params": {"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}},
{"command": "component.activate", "params": {"component": "root"}},
{"command": "occurrence.ground", "params": {"occurrence": 2, "grounded": true}},
- {"command": "JointAssembleCmdNew", "params": {"type": "revolute", "a": {"occurrence": "LeafA", "circle": [1, 37, 4]}, "b": {"occurrence": "LeafB", "circle": [1, 37, 4]}, "name": "Hinge"}},
- {"command": "FusionMoveJointsCommand", "params": {"joint": "Hinge", "value": "open_angle"}}
+ {"command": "joint.create", "params": {"type": "revolute", "a": {"occurrence": "LeafA", "circle": [1, 37, 4]}, "b": {"occurrence": "LeafB", "circle": [1, 37, 4]}, "name": "Hinge"}},
+ {"command": "joint.drive", "params": {"joint": "Hinge", "value": "open_angle"}}
]}
diff --git a/apps/solvecraft-cli/tests/corpus/scripts/m0-box-fillet-hole.json b/apps/solvecraft-cli/tests/corpus/scripts/m0-box-fillet-hole.json
index 281834c1..e2bdec2e 100644
--- a/apps/solvecraft-cli/tests/corpus/scripts/m0-box-fillet-hole.json
+++ b/apps/solvecraft-cli/tests/corpus/scripts/m0-box-fillet-hole.json
@@ -1,12 +1,12 @@
{"commands": [
- {"command": "ChangeParameterCommand", "params": {"name": "width", "expression": "40 mm"}},
- {"command": "SketchCreate", "params": {"plane": "XY"}},
- {"command": "ShapeRectangleTwoPoint", "params": {"p0": [0, 0], "p1": [40, 30]}},
- {"command": "SketchStop", "params": {}},
- {"command": "Extrude", "params": {"distance": "width / 2"}},
- {"command": "FusionFilletEdgesCommand", "params": {"edges": [[0, 0, 10]], "radius": 3}},
- {"command": "SketchCreate", "params": {"plane": "XY"}},
- {"command": "CircleCenterRadius", "params": {"center": [20, 15], "radius": 5}},
- {"command": "SketchStop", "params": {}},
- {"command": "Extrude", "params": {"distance": 20, "operation": "cut"}}
+ {"command": "parameters.change", "params": {"name": "width", "expression": "40 mm"}},
+ {"command": "sketch.create", "params": {"plane": "XY"}},
+ {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}},
+ {"command": "sketch.finish", "params": {}},
+ {"command": "solid.extrude", "params": {"distance": "width / 2"}},
+ {"command": "solid.fillet", "params": {"edges": [[0, 0, 10]], "radius": 3}},
+ {"command": "sketch.create", "params": {"plane": "XY"}},
+ {"command": "sketch.circle.center", "params": {"center": [20, 15], "radius": 5}},
+ {"command": "sketch.finish", "params": {}},
+ {"command": "solid.extrude", "params": {"distance": 20, "operation": "cut"}}
]}
diff --git a/apps/solvecraft-cli/tests/corpus/scripts/m0-revolve-primitives.json b/apps/solvecraft-cli/tests/corpus/scripts/m0-revolve-primitives.json
index 56fd5c90..fc4cca22 100644
--- a/apps/solvecraft-cli/tests/corpus/scripts/m0-revolve-primitives.json
+++ b/apps/solvecraft-cli/tests/corpus/scripts/m0-revolve-primitives.json
@@ -1,11 +1,11 @@
{"commands": [
- {"command": "SketchCreate", "params": {"plane": "XZ"}},
- {"command": "DrawPolyline", "params": {"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true}},
- {"command": "SketchStop", "params": {}},
- {"command": "Revolve", "params": {"axis": "y"}},
- {"command": "PrimitiveBox", "params": {"length": 10, "width": 10, "height": 10, "corner": [30, 0, 0]}},
- {"command": "PrimitiveCylinder", "params": {"diameter": 4, "height": 20, "base": [35, 5, -5]}},
- {"command": "FusionCombineCommand", "params": {"target": "Body2", "tools": ["Body3"], "operation": "cut"}},
- {"command": "FusionChamferCommand", "params": {"edges": [[30, 5, 10]], "distance": 1}},
+ {"command": "sketch.create", "params": {"plane": "XZ"}},
+ {"command": "sketch.line", "params": {"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true}},
+ {"command": "sketch.finish", "params": {}},
+ {"command": "solid.revolve", "params": {"axis": "y"}},
+ {"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "corner": [30, 0, 0]}},
+ {"command": "solid.cylinder", "params": {"diameter": 4, "height": 20, "base": [35, 5, -5]}},
+ {"command": "solid.combine", "params": {"target": "Body2", "tools": ["Body3"], "operation": "cut"}},
+ {"command": "solid.chamfer", "params": {"edges": [[30, 5, 10]], "distance": 1}},
{"command": "timeline.edit", "params": {"feature": "Revolve1", "set": {"angle": "180 deg"}}}
]}
diff --git a/apps/solvecraft-cli/tests/corpus/scripts/modern-path-pattern.json b/apps/solvecraft-cli/tests/corpus/scripts/modern-path-pattern.json
index fbbfdb82..dedc8381 100644
--- a/apps/solvecraft-cli/tests/corpus/scripts/modern-path-pattern.json
+++ b/apps/solvecraft-cli/tests/corpus/scripts/modern-path-pattern.json
@@ -1,14 +1,14 @@
{
"commands": [
{
- "command": "SketchCreate",
+ "command": "sketch.create",
"params": {
"name": "Plate_sketch",
"plane": "XY"
}
},
{
- "command": "DrawPolyline",
+ "command": "sketch.line",
"params": {
"construction": false,
"ids": [
@@ -27,7 +27,7 @@
}
},
{
- "command": "DrawPolyline",
+ "command": "sketch.line",
"params": {
"construction": false,
"ids": [
@@ -43,7 +43,7 @@
}
},
{
- "command": "DrawPolyline",
+ "command": "sketch.line",
"params": {
"construction": false,
"ids": [
@@ -59,7 +59,7 @@
}
},
{
- "command": "DrawPolyline",
+ "command": "sketch.line",
"params": {
"construction": false,
"ids": [
@@ -72,11 +72,11 @@
}
},
{
- "command": "SketchStop",
+ "command": "sketch.finish",
"params": {}
},
{
- "command": "Extrude",
+ "command": "solid.extrude",
"params": {
"body_names": [
"Plate"
@@ -110,14 +110,14 @@
}
},
{
- "command": "SketchCreate",
+ "command": "sketch.create",
"params": {
"name": "PathSketch",
"plane": "XY"
}
},
{
- "command": "ArcThreePoint",
+ "command": "sketch.arc.three_point",
"params": {
"end": [
20,
@@ -135,7 +135,7 @@
}
},
{
- "command": "CircleCenterRadius",
+ "command": "sketch.circle.center",
"params": {
"center": "a1.start",
"construction": false,
@@ -144,11 +144,11 @@
}
},
{
- "command": "SketchStop",
+ "command": "sketch.finish",
"params": {}
},
{
- "command": "Extrude",
+ "command": "solid.extrude",
"params": {
"body_names": [
"Plate"
@@ -179,7 +179,7 @@
}
},
{
- "command": "PatternOnPath",
+ "command": "solid.pattern.path",
"params": {
"count": 7,
"features": [
diff --git a/apps/solvecraft-cli/tests/corpus/scripts/modern-plastic.json b/apps/solvecraft-cli/tests/corpus/scripts/modern-plastic.json
index 08edf91d..d4113ede 100644
--- a/apps/solvecraft-cli/tests/corpus/scripts/modern-plastic.json
+++ b/apps/solvecraft-cli/tests/corpus/scripts/modern-plastic.json
@@ -1,10 +1,10 @@
{"commands": [
- {"command": "ChangeParameterCommand", "params": {"name": "wall", "expression": "2 mm"}},
- {"command": "PrimitiveBox", "params": {"length": 80, "width": 50, "height": 30}},
- {"command": "FusionShellBodyCommand", "params": {"faces": [[40, 25, 30]], "thickness": "wall"}},
- {"command": "FusionLipCommand", "params": {"face": [1, 25, 30], "width": 1, "height": 2}},
- {"command": "FusionBossCommand", "params": {"position": [20, 20, 2], "diameter": 6, "height": 10, "hole_diameter": 2.5, "draft": "1 deg"}},
- {"command": "FusionRestCommand", "params": {"position": [60, 30, 2], "width": 8, "length": 12, "height": 3, "thickness": 1}},
- {"command": "FusionSnapFitCommand", "params": {"position": [40, 12, 2], "direction": [0, 0, 1], "hook": [0, 1, 0], "length": 12, "thickness": 1.5, "width": 6, "catch_depth": 1, "catch_length": 2}},
- {"command": "FusionManagePlasticRuleCommand", "params": {"name": "ABS (1.5mm)", "active": true}}
+ {"command": "parameters.change", "params": {"name": "wall", "expression": "2 mm"}},
+ {"command": "solid.box", "params": {"length": 80, "width": 50, "height": 30}},
+ {"command": "solid.shell", "params": {"faces": [[40, 25, 30]], "thickness": "wall"}},
+ {"command": "plastic.lip", "params": {"face": [1, 25, 30], "width": 1, "height": 2}},
+ {"command": "plastic.boss", "params": {"position": [20, 20, 2], "diameter": 6, "height": 10, "hole_diameter": 2.5, "draft": "1 deg"}},
+ {"command": "plastic.rest", "params": {"position": [60, 30, 2], "width": 8, "length": 12, "height": 3, "thickness": 1}},
+ {"command": "plastic.snap_fit", "params": {"position": [40, 12, 2], "direction": [0, 0, 1], "hook": [0, 1, 0], "length": 12, "thickness": 1.5, "width": 6, "catch_depth": 1, "catch_length": 2}},
+ {"command": "plastic.manage_rules", "params": {"name": "ABS (1.5mm)", "active": true}}
]}
diff --git a/apps/solvecraft-cli/tests/corpus/scripts/modern-sheet-metal.json b/apps/solvecraft-cli/tests/corpus/scripts/modern-sheet-metal.json
index 1a1af3d1..58582dea 100644
--- a/apps/solvecraft-cli/tests/corpus/scripts/modern-sheet-metal.json
+++ b/apps/solvecraft-cli/tests/corpus/scripts/modern-sheet-metal.json
@@ -1,7 +1,7 @@
{
"commands": [
{
- "command": "SketchCreate",
+ "command": "sketch.create",
"params": {
"name": "Contour",
"plane": {
@@ -24,7 +24,7 @@
}
},
{
- "command": "DrawPolyline",
+ "command": "sketch.line",
"params": {
"construction": false,
"ids": [
@@ -43,7 +43,7 @@
}
},
{
- "command": "DrawPolyline",
+ "command": "sketch.line",
"params": {
"construction": false,
"ids": [
@@ -59,7 +59,7 @@
}
},
{
- "command": "DrawPolyline",
+ "command": "sketch.line",
"params": {
"construction": false,
"ids": [
@@ -75,11 +75,11 @@
}
},
{
- "command": "SketchStop",
+ "command": "sketch.finish",
"params": {}
},
{
- "command": "FusionSheetMetalFlangeCommand",
+ "command": "sheet.flange",
"params": {
"curves": [
"l1",
diff --git a/apps/solvecraft-cli/tests/design_corpus.rs b/apps/solvecraft-cli/tests/design_corpus.rs
index 2a904dc7..65a87f96 100644
--- a/apps/solvecraft-cli/tests/design_corpus.rs
+++ b/apps/solvecraft-cli/tests/design_corpus.rs
@@ -57,7 +57,7 @@ fn old_designs_open_and_evaluate_as_saved() {
}
}
assert_eq!(s.doc.format, solvecraft_engine::doc::format::format_string(solvecraft_engine::doc::format::FORMAT), "{name}: upgraded");
- let got = s.execute("MeasureCommand", &json!({})).unwrap();
+ let got = s.execute("inspect.measure", &json!({})).unwrap();
if got["body_count"] != want["body_count"] && !known(&name, "body_count") {
problems.push(format!("{name}: {} bodies, saved with {}", got["body_count"], want["body_count"]));
}
@@ -145,7 +145,7 @@ fn at_mut<'a>(v: &'a mut Value, path: &[Value]) -> Option<&'a mut Value> {
fn try_bytes(bytes: &[u8]) {
if let Ok(doc) = solvecraft_engine::io::read_design(bytes) {
let mut s = Session::new(doc);
- let _ = s.execute("MeasureCommand", &json!({}));
+ let _ = s.execute("inspect.measure", &json!({}));
let _ = s.execute("document.inspect", &json!({}));
}
}
diff --git a/apps/solvecraft/src/main.rs b/apps/solvecraft/src/main.rs
index b3f806df..b78b746e 100644
--- a/apps/solvecraft/src/main.rs
+++ b/apps/solvecraft/src/main.rs
@@ -6,7 +6,7 @@
//! `.3mf`/`.stl` file as a new design holding mesh bodies.
//!
//! `--control ` (or `SOLVECRAFT_CONTROL_PORT`) starts a localhost JSON-lines control
-//! server: `{"id":1,"method":"engine.execute","params":{"command":"PrimitiveBox","params":{…}}}`.
+//! server: `{"id":1,"method":"engine.execute","params":{"command":"solid.box","params":{…}}}`.
//! See `solvecraft_ui_egui::control` and docs/control-protocol.md.
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
diff --git a/crates/engine/src/cmd/browser.rs b/crates/engine/src/cmd/browser.rs
index fda9c657..30642cb6 100644
--- a/crates/engine/src/cmd/browser.rs
+++ b/crates/engine/src/cmd/browser.rs
@@ -385,10 +385,10 @@ mod tests {
#[test]
fn renamed_body_keeps_its_users() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]}));
let [a, b] = [names(&s)[0].clone(), names(&s)[1].clone()];
- run(&mut s, "FusionMoveCommand", json!({"bodies": [b.clone()], "translate": [0, 0, 5]}));
+ run(&mut s, "solid.move", json!({"bodies": [b.clone()], "translate": [0, 0, 5]}));
run(&mut s, "body.rename", json!({"body": b, "name": "Lid"}));
assert_eq!(names(&s), vec![a.clone(), "Lid".to_string()]);
assert!(s.model.results.iter().all(|r| r.error.is_none()), "the move still finds its body");
@@ -396,18 +396,18 @@ mod tests {
assert!(s.execute("body.rename", &json!({"body": "Lid", "name": a})).is_err());
assert!(s.execute("body.rename", &json!({"body": "Nope", "name": "X"})).is_err());
assert!(s.execute("body.rename", &json!({"body": "Lid", "name": " "})).is_err());
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert_eq!(names(&s)[1], b);
}
#[test]
fn redefined_sketch_moves_its_extrude() {
let mut s = Session::default();
- let sk = run(&mut s, "SketchCreate", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
- run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [10, 20]}));
- run(&mut s, "SketchStop", json!({}));
- run(&mut s, "Extrude", json!({"distance": 5}));
- let zmax = |s: &mut Session| run(s, "MeasureCommand", json!({}))["bodies"][0]["bbox"]["max"][2].as_f64().unwrap();
+ let sk = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
+ run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 20]}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "solid.extrude", json!({"distance": 5}));
+ let zmax = |s: &mut Session| run(s, "inspect.measure", json!({}))["bodies"][0]["bbox"]["max"][2].as_f64().unwrap();
assert!((zmax(&mut s) - 5.0).abs() < 1e-6);
run(&mut s, "sketch.redefine", json!({"sketch": sk, "plane": "XZ"}));
let z = zmax(&mut s);
@@ -419,8 +419,8 @@ mod tests {
#[test]
fn design_units_change_without_moving_geometry() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 20, "width": 10, "height": 5}));
- let vol = |s: &mut Session| run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
+ run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 5}));
+ let vol = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
let v0 = vol(&mut s);
run(&mut s, "document.units", json!({"units": "in"}));
assert_eq!(s.doc.units, "in");
@@ -430,14 +430,14 @@ mod tests {
let mut s = Session::default();
run(&mut s, "parameters.add", json!({"name": "w", "expression": "5", "unit": ""}));
run(&mut s, "parameters.add", json!({"name": "n", "expression": "3", "unit": ""}));
- run(&mut s, "PrimitiveBox", json!({"length": "w", "width": 10, "height": 5}));
+ run(&mut s, "solid.box", json!({"length": "w", "width": 10, "height": 5}));
let b = s.model.state().bodies[0].name.clone();
- run(&mut s, "PatternRectangular", json!({"bodies": [b], "dir1": [1, 0, 0], "count1": "n", "spacing1": 20}));
+ run(&mut s, "solid.pattern.rectangular", json!({"bodies": [b], "dir1": [1, 0, 0], "count1": "n", "spacing1": 20}));
let v0 = vol(&mut s);
let r = run(&mut s, "document.units", json!({"units": "cm"}));
assert!((vol(&mut s) - v0).abs() < 1e-6, "{r}");
assert!(s.doc.param("n").is_some_and(|p| p.expr == "3"), "the count stays bare");
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.units, "mm");
}
@@ -457,15 +457,15 @@ mod tests {
for p in [json!({}), json!({"camera": {"target": [0, 0, 0], "yaw": 0, "pitch": 0, "distance": -1}}), json!({"camera": "x"})] {
assert!(s.execute("view.save", &p).is_err(), "{p}");
}
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.named_views.len(), 2);
}
#[test]
fn groups_hold_items_and_follow_renames() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]}));
let [a, b] = [names(&s)[0].clone(), names(&s)[1].clone()];
let g = run(&mut s, "browser.group", json!({"folder": "bodies", "items": [a.clone()]}))["group"].as_u64().unwrap();
assert_eq!(s.doc.browser_groups[0].name, "Group1");
@@ -495,7 +495,7 @@ mod tests {
] {
assert!(s.execute(c, &p).is_err(), "{c} {p}");
}
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.browser_groups.len(), 1, "ungroup undoes");
}
}
diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs
index b0a8375e..026bf806 100644
--- a/crates/engine/src/cmd/clipboard.rs
+++ b/crates/engine/src/cmd/clipboard.rs
@@ -122,32 +122,32 @@ mod tests {
}
fn volume(s: &mut Session) -> f64 {
- run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
+ run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
}
#[test]
fn copy_paste_sketch_and_extrude() {
let mut s = Session::default();
- run(&mut s, "ChangeParameterCommand", json!({"name": "w", "expression": "10 mm"}));
- run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "S"}));
- run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [10, 10]}));
- run(&mut s, "ConstraintCoincident", json!({"a": "p1", "b": "origin"}));
- run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": "w"}));
- run(&mut s, "SketchDimension", json!({"entities": ["l2"], "value": 10}));
- run(&mut s, "SketchStop", json!({}));
- run(&mut s, "Extrude", json!({"sketch": "S", "distance": 5}));
+ run(&mut s, "parameters.change", json!({"name": "w", "expression": "10 mm"}));
+ run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
+ run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
+ run(&mut s, "sketch.constraint.coincident", json!({"a": "p1", "b": "origin"}));
+ run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": "w"}));
+ run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "value": 10}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "solid.extrude", json!({"sketch": "S", "distance": 5}));
assert!((volume(&mut s) - 500.0).abs() < 1e-6);
run(&mut s, "timeline.copy", json!({"features": ["S", "Extrude1"]}));
let r = run(&mut s, "timeline.paste", json!({"translate": [30, 0, 0]}));
assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}");
- assert_eq!(run(&mut s, "MeasureCommand", json!({}))["body_count"], 2);
+ assert_eq!(run(&mut s, "inspect.measure", json!({}))["body_count"], 2);
assert!((volume(&mut s) - 1000.0).abs() < 1e-6);
// The copy's dimensions are its own, still driven by the user parameter.
- run(&mut s, "ChangeParameterCommand", json!({"name": "w", "expression": "20 mm"}));
+ run(&mut s, "parameters.change", json!({"name": "w", "expression": "20 mm"}));
assert!((volume(&mut s) - 2000.0).abs() < 1e-6);
let pasted_extrude = r["pasted"][1]["name"].as_str().unwrap_or_default().to_string();
let d = s.doc.find_feature(&pasted_extrude).and_then(|f| f.param_names.first().cloned()).unwrap_or_default();
- run(&mut s, "ChangeParameterCommand", json!({"name": d, "expression": "10"}));
+ run(&mut s, "parameters.change", json!({"name": d, "expression": "10"}));
assert!((volume(&mut s) - 3000.0).abs() < 1e-6, "only the copy got thicker");
assert!(s.execute("timeline.paste", &json!({"translate": "x"})).is_err());
let mut t = Session::default();
diff --git a/crates/engine/src/cmd/component.rs b/crates/engine/src/cmd/component.rs
index 68d59ef5..62b8cc3a 100644
--- a/crates/engine/src/cmd/component.rs
+++ b/crates/engine/src/cmd/component.rs
@@ -12,11 +12,11 @@ use crate::params::{bad, bool_, num, str_, string_list, vec3};
use crate::{Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("FusionCreateNewComponentCommand", "New Component", new_component)
+ CommandSpec::new("component.create", "New Component", new_component)
.at("SOLID", "ASSEMBLE")
.icon("component")
.params("name?, parent?: component id or name (default: the active one); activate?: bool (default true)"),
- CommandSpec::new("FusionCreateComponentsFromBodiesCommand", "Create Components from Bodies", from_bodies)
+ CommandSpec::new("component.from_bodies", "Create Components from Bodies", from_bodies)
.icon("component")
.params("bodies: [names] → one new component per body, named after it"),
CommandSpec::new("component.activate", "Activate Component", activate).noundo().params("component: id or name (0 or \"root\" for the design)"),
@@ -31,8 +31,8 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("occurrence.move", "Move Occurrence", move_occurrence)
.params("occurrence; translate?: [x,y,z]; axis?: [x,y,z], angle?: radians or \"30 deg\", origin?: [x,y,z]; capture?: bool (default true; false keeps it pending until Capture Position)"),
CommandSpec::new("occurrence.ground", "Ground", ground).params("occurrence; grounded?: bool (default toggles)"),
- CommandSpec::new("SnapshotCmd", "Capture Position", capture).at("SOLID", "POSITION").icon("capture").params("keeps pending occurrence moves"),
- CommandSpec::new("AsBuiltPositionsCmd", "Revert Position", revert).at("SOLID", "POSITION").icon("revert").noundo().params("drops pending occurrence moves"),
+ CommandSpec::new("component.capture_position", "Capture Position", capture).at("SOLID", "POSITION").icon("capture").params("keeps pending occurrence moves"),
+ CommandSpec::new("component.revert_position", "Revert Position", revert).at("SOLID", "POSITION").icon("revert").noundo().params("drops pending occurrence moves"),
];
fn key(v: Option<&Value>) -> Option {
@@ -58,7 +58,7 @@ fn translation(t: Vec3) -> Mat {
}
fn new_component(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionCreateNewComponentCommand";
+ let cmd = "component.create";
let parent = if p.get("parent").is_some() { component(s, p, "parent", cmd)? } else { s.active_component };
let id = s.doc_mut().add_component(str_(p, "name"), parent)?;
if p.get("activate").and_then(Value::as_bool).unwrap_or(true) {
@@ -70,7 +70,7 @@ fn new_component(s: &mut Session, p: &Value) -> Result {
}
fn from_bodies(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionCreateComponentsFromBodiesCommand";
+ let cmd = "component.from_bodies";
let bodies = string_list(p, "bodies");
if bodies.is_empty() || bodies.len() > 1000 {
return Err(bad(cmd, "`bodies` must list 1…1000 body names"));
diff --git a/crates/engine/src/cmd/config.rs b/crates/engine/src/cmd/config.rs
index a29ff524..74f2c8dc 100644
--- a/crates/engine/src/cmd/config.rs
+++ b/crates/engine/src/cmd/config.rs
@@ -9,10 +9,10 @@ use crate::params::{bad, bool_, str_, string_list};
use crate::{Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("FusionStartDesignConfigModeCmd", "Configure", configure).at("SOLID", "CONFIGURE").icon("params").params(
+ CommandSpec::new("config.configure", "Configure", configure).at("SOLID", "CONFIGURE").icon("params").params(
"params?: [parameter names]; features?: [feature names] (columns to add; with no rows yet, a \"Default\" row takes the current values)",
),
- CommandSpec::new("FusionShowDesignConfigPanelCmd", "Display Configuration Table", show)
+ CommandSpec::new("config.table", "Display Configuration Table", show)
.at("SOLID", "CONFIGURE")
.icon("params")
.noundo()
@@ -54,7 +54,7 @@ fn add_column(s: &mut Session, c: Column, cmd: &str) -> Result<()> {
}
fn configure(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionStartDesignConfigModeCmd";
+ let cmd = "config.configure";
for n in string_list(p, "params") {
let c = column_of(s, &json!({ "param": n }), cmd)?;
add_column(s, c, cmd)?;
@@ -196,17 +196,17 @@ mod tests {
}
fn volume(s: &mut Session) -> f64 {
- run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
+ run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
}
#[test]
fn configurations_switch_sizes_and_suppressions() {
let mut s = Session::default();
- run(&mut s, "ChangeParameterCommand", json!({"name": "len", "expression": "40 mm"}));
- run(&mut s, "PrimitiveBox", json!({"length": "len", "width": 20, "height": 10}));
- run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[0, 0, 5]], "radius": 3}));
+ run(&mut s, "parameters.change", json!({"name": "len", "expression": "40 mm"}));
+ run(&mut s, "solid.box", json!({"length": "len", "width": 20, "height": 10}));
+ run(&mut s, "solid.fillet", json!({"edges": [[0, 0, 5]], "radius": 3}));
let fillet = s.doc.features.last().map(|f| f.name.clone()).unwrap_or_default();
- let t = run(&mut s, "FusionStartDesignConfigModeCmd", json!({"params": ["len"], "features": [fillet]}));
+ let t = run(&mut s, "config.configure", json!({"params": ["len"], "features": [fillet]}));
assert_eq!(t["rows"][0]["name"], "Default");
run(&mut s, "config.row", json!({"name": "Long", "values": {"len": "80 mm"}}));
run(&mut s, "config.row", json!({"name": "Sharp", "from": "Long", "values": {fillet.clone(): true}}));
@@ -216,17 +216,17 @@ mod tests {
assert!((volume(&mut s) - (16000.0 - (9.0 - std::f64::consts::PI * 9.0 / 4.0) * 10.0)).abs() < 0.5, "{}", volume(&mut s));
run(&mut s, "config.activate", json!({"row": "Sharp"}));
assert!((volume(&mut s) - 16000.0).abs() < 1e-6);
- let t = run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}));
+ let t = run(&mut s, "config.table", json!({}));
assert_eq!((t["active"].as_str(), t["matches"].as_bool()), (Some("Sharp"), Some(true)), "{t}");
// Undo goes back to the previous configuration's values.
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert!(volume(&mut s) > 15000.0 && volume(&mut s) < 16000.0);
// Renaming the parameter keeps the column; editing it by hand breaks the match.
run(&mut s, "parameters.rename", json!({"name": "len", "new_name": "length"}));
- let t = run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}));
+ let t = run(&mut s, "config.table", json!({}));
assert_eq!(t["columns"][0], "param:length", "{t}");
- run(&mut s, "ChangeParameterCommand", json!({"name": "length", "expression": "50 mm"}));
- assert_eq!(run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}))["matches"], false);
+ run(&mut s, "parameters.change", json!({"name": "length", "expression": "50 mm"}));
+ assert_eq!(run(&mut s, "config.table", json!({}))["matches"], false);
// Bad input.
assert!(s.execute("config.row", &json!({"name": "X", "values": {"nope": 1}})).is_err());
assert!(s.execute("config.activate", &json!({"row": "Missing"})).is_err());
@@ -234,6 +234,6 @@ mod tests {
assert!(s.execute("config.activate", &json!({"row": "Broken"})).is_err());
run(&mut s, "config.row", json!({"name": "Broken", "delete": true}));
run(&mut s, "config.column", json!({"feature": fillet, "delete": true}));
- assert_eq!(run(&mut s, "FusionShowDesignConfigPanelCmd", json!({}))["columns"].as_array().map(Vec::len), Some(1));
+ assert_eq!(run(&mut s, "config.table", json!({}))["columns"].as_array().map(Vec::len), Some(1));
}
}
diff --git a/crates/engine/src/cmd/delete.rs b/crates/engine/src/cmd/delete.rs
index c42959f5..8b77caf9 100644
--- a/crates/engine/src/cmd/delete.rs
+++ b/crates/engine/src/cmd/delete.rs
@@ -95,7 +95,7 @@ fn apply(s: &mut Session, p: &Value) -> Result {
}
if !features.is_empty() {
let ids: Vec = features.iter().map(u64::to_string).collect();
- sub(s, "FusionDeleteCommand", json!({ "features": ids }))?;
+ sub(s, "timeline.delete", json!({ "features": ids }))?;
done = true;
}
// Bodies still there (not made by a feature deleted above) get a Remove feature.
@@ -103,7 +103,7 @@ fn apply(s: &mut Session, p: &Value) -> Result {
bodies.retain(|b| st.body(b).is_some());
bodies.dedup();
if !bodies.is_empty() {
- sub(s, "SoftDeleteCommand", json!({ "bodies": bodies }))?;
+ sub(s, "solid.remove", json!({ "bodies": bodies }))?;
done = true;
}
// An occurrence goes alone while its component has others; the last one takes the component.
@@ -174,12 +174,12 @@ mod tests {
/// A sketch with a rectangle extruded into a body, a second box, and an offset plane.
fn part() -> (Session, u64) {
let mut s = Session::default();
- let sk = run(&mut s, "SketchCreate", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
- run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [10, 20]}));
- run(&mut s, "SketchStop", json!({}));
- run(&mut s, "Extrude", json!({"distance": 5}));
- run(&mut s, "PrimitiveBox", json!({"length": 5, "width": 5, "height": 5, "corner": [50, 0, 0]}));
- run(&mut s, "ConstructionPlaneOffsetFromPlaneCommand", json!({"base": "XY", "offset": 10}));
+ let sk = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
+ run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 20]}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "solid.extrude", json!({"distance": 5}));
+ run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [50, 0, 0]}));
+ run(&mut s, "construct.plane.offset", json!({"base": "XY", "offset": 10}));
(s, sk)
}
@@ -192,7 +192,7 @@ mod tests {
assert!(dry["deleted"].as_array().unwrap().len() >= 2, "the extrude goes with its sketch: {dry}");
run(&mut s, "selection.delete", json!({"items": [{"type": "feature", "id": sk}]}));
assert_eq!(bodies(&s), 1);
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc.features.len(), n);
assert_eq!(bodies(&s), 2);
}
@@ -212,7 +212,7 @@ mod tests {
assert!(s.doc.find_feature("Plane1").is_none());
assert_eq!(r["skipped"][0], "the origin planes can't be deleted");
assert_eq!(s.undo.len(), depth + 1);
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert_eq!((bodies(&s), s.doc.features.len()), (2, n));
}
@@ -237,7 +237,7 @@ mod tests {
fn occurrences_components_and_sketch_entities() {
let (mut s, _) = part();
let b = s.model.state().bodies[1].name.clone();
- run(&mut s, "FusionCreateComponentsFromBodiesCommand", json!({"bodies": [b]}));
+ run(&mut s, "component.from_bodies", json!({"bodies": [b]}));
let c = s.doc.components[0].id;
let o1 = s.doc.occurrence_of(c).unwrap().id;
run(&mut s, "occurrence.copy", json!({"component": c, "translate": [0, 30, 0]}));
@@ -249,8 +249,8 @@ mod tests {
run(&mut s, "selection.delete", json!({"occurrences": [o2]}));
assert!(s.doc.components.is_empty());
// Sketch entities while sketching.
- run(&mut s, "SketchCreate", json!({"plane": "XY"}));
- run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [10, 0]]}));
+ run(&mut s, "sketch.create", json!({"plane": "XY"}));
+ run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]]}));
let sk = s.active_sketch.unwrap();
let id = s.model.state().sketch(sk).unwrap().sketch.curves[0].id.clone();
run(&mut s, "selection.delete", json!({"items": [{"type": "sketch_curve", "id": id}]}));
diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs
index 849e8742..64bd563e 100644
--- a/crates/engine/src/cmd/edit.rs
+++ b/crates/engine/src/cmd/edit.rs
@@ -8,17 +8,17 @@ use crate::params::{bad, bool_, num, str_, string_list};
use crate::{EngineError, Result, Sel, Session, Snapshot};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("UndoCommand", "Undo", undo).icon("undo").key("Ctrl+Z").noundo(),
- CommandSpec::new("RedoCommand", "Redo", redo).icon("redo").key("Ctrl+Y").noundo(),
- CommandSpec::new("FusionComputeAllCommand", "Compute All", compute_all).at("SOLID", "MODIFY").icon("compute").noundo(),
- CommandSpec::new("FusionDeleteCommand", "Delete", delete)
+ CommandSpec::new("edit.undo", "Undo", undo).icon("undo").key("Ctrl+Z").noundo(),
+ CommandSpec::new("edit.redo", "Redo", redo).icon("redo").key("Ctrl+Y").noundo(),
+ CommandSpec::new("timeline.compute_all", "Compute All", compute_all).at("SOLID", "MODIFY").icon("compute").noundo(),
+ CommandSpec::new("timeline.delete", "Delete", delete)
.at("SOLID", "MODIFY")
.icon("delete")
.key("Delete")
.params("features: [id or name] (a sketch takes its dependent features with it)"),
- CommandSpec::new("FusionRenameTimelineEntryCommand", "Rename", rename).params("feature: id|name, name"),
+ CommandSpec::new("timeline.rename", "Rename", rename).params("feature: id|name, name"),
CommandSpec::new("timeline.rollback", "Move Timeline Marker", rollback).params("position: number of features to keep active (omit = end)"),
- CommandSpec::new("timeline.rollTo", "Roll History Marker", rollback)
+ CommandSpec::new("timeline.roll_to", "Roll History Marker", rollback)
.params("position: number of features to keep active (omit = end) | feature: id|name (the marker goes right after it)"),
CommandSpec::new("timeline.reorder", "Reorder Feature", reorder)
.params("feature: id|name, position: its new index; refused when a feature that worked would fail"),
@@ -75,7 +75,7 @@ fn feature_id(s: &Session, v: Option<&Value>, cmd: &str) -> Result {
}
fn delete(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionDeleteCommand";
+ let cmd = "timeline.delete";
let mut keys = string_list(p, "features");
if let Some(Value::Array(a)) = p.get("features") {
keys.extend(a.iter().filter_map(Value::as_u64).map(|n| n.to_string()));
@@ -108,7 +108,7 @@ fn delete(s: &mut Session, p: &Value) -> Result {
}
fn rename(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionRenameTimelineEntryCommand";
+ let cmd = "timeline.rename";
let id = feature_id(s, p.get("feature"), cmd)?;
let name =
str_(p, "name").map(str::trim).filter(|n| !n.is_empty() && n.len() <= 128).ok_or_else(|| bad(cmd, "`name` must be 1…128 characters"))?;
@@ -125,7 +125,7 @@ fn rollback(s: &mut Session, p: &Value) -> Result {
let n = s.doc.features.len();
let pos = match p.get("feature") {
Some(f) => {
- let id = feature_id(s, Some(f), "timeline.rollTo")?;
+ let id = feature_id(s, Some(f), "timeline.roll_to")?;
s.doc.feature_index(id).map(|i| i + 1)
}
None => num(p, "position").map(|x| x.max(0.0) as usize),
@@ -278,7 +278,7 @@ fn redefine(s: &mut Session, p: &Value) -> Result {
let id = feature_id(s, p.get("feature"), cmd)?;
let cid = str_(p, "command").ok_or_else(|| bad(cmd, "`command` must name the feature's command"))?;
let spec = super::find_command(cid).ok_or_else(|| bad(cmd, format!("no command `{cid}`")))?;
- if cid.starts_with("timeline.") || cid.starts_with("select.") || cid.starts_with("Sketch") {
+ if spec.id.starts_with("timeline.") || spec.id.starts_with("select.") || spec.id.starts_with("sketch.") {
return Err(bad(cmd, format!("`{cid}` does not make a feature")));
}
let params = p.get("params").cloned().unwrap_or_else(|| json!({}));
diff --git a/crates/engine/src/cmd/face.rs b/crates/engine/src/cmd/face.rs
index 4742d3e7..8a13da8f 100644
--- a/crates/engine/src/cmd/face.rs
+++ b/crates/engine/src/cmd/face.rs
@@ -118,7 +118,7 @@ pub(super) fn sketch_of_face(s: &mut Session, point: Vec3, cmd: &str) -> Result<
let spec = crate::find_command(id).ok_or_else(|| bad(cmd, id))?;
(spec.run)(s, &p)
};
- let r = run(s, "SketchCreate", json!({"plane": {"face": [inside.x, inside.y, inside.z]}, "project_edges": true}))?;
+ let r = run(s, "sketch.create", json!({"plane": {"face": [inside.x, inside.y, inside.z]}, "project_edges": true}))?;
let sid = r.get("sketch").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "could not start a sketch on the face"))?;
s.refresh();
let plane = s.model.state().sketch(sid).map(|ss| ss.plane).ok_or_else(|| bad(cmd, "could not start a sketch on the face"))?;
@@ -142,10 +142,10 @@ pub(super) fn sketch_of_face(s: &mut Session, point: Vec3, cmd: &str) -> Result<
None
};
match circle {
- Some((o, r)) if closed => run(s, "CircleCenterRadius", json!({"center": xy(o), "radius": r}))?,
+ Some((o, r)) if closed => run(s, "sketch.circle.center", json!({"center": xy(o), "radius": r}))?,
Some(_) => {
let mid = pts.get(pts.len() / 2).copied().unwrap_or(first);
- run(s, "ArcThreePoint", json!({"start": xy(first), "end": xy(last), "through": xy(mid)}))?
+ run(s, "sketch.arc.three_point", json!({"start": xy(first), "end": xy(last), "through": xy(mid)}))?
}
None => {
let mut list: Vec = Vec::new();
@@ -159,7 +159,7 @@ pub(super) fn sketch_of_face(s: &mut Session, point: Vec3, cmd: &str) -> Result<
if closed_poly {
list.pop();
}
- run(s, "DrawPolyline", json!({"points": list, "closed": closed_poly}))?
+ run(s, "sketch.line", json!({"points": list, "closed": closed_poly}))?
}
};
}
diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs
index 084fe62c..eabb0eb1 100644
--- a/crates/engine/src/cmd/features.rs
+++ b/crates/engine/src/cmd/features.rs
@@ -9,95 +9,95 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec2, vec3};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("Extrude", "Extrude", extrude)
+ CommandSpec::new("solid.extrude", "Extrude", extrude)
.at("SOLID", "CREATE")
.icon("extrude")
.key("E")
.params("distance: expr (or through_all: true); taper?: angle expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [[curve ids]] | [{point:[x,y]}]; face?: [x,y,z] (extrude a planar body face instead of a sketch profile); direction?: positive|negative|symmetric; distance2?; start_offset?; operation?: new|join|cut|intersect|auto (cut into a body, join out of one, else new); targets?: [body]; name?; body_name?"),
- CommandSpec::new("Revolve", "Revolve", revolve)
+ CommandSpec::new("solid.revolve", "Revolve", revolve)
.at("SOLID", "CREATE")
.icon("revolve")
.params("axis: sketch line id | x | y (sketch axes) | X|Y|Z (world); angle?: expr (default 360 deg); sketch?, profiles? | face: [x,y,z] (a planar body face as the profile), operation?, targets?, name?, body_name?"),
- CommandSpec::new("Sweep", "Sweep", sweep)
+ CommandSpec::new("solid.sweep", "Sweep", sweep)
.at("SOLID", "CREATE")
.icon("sweep")
.params("sketch: profile sketch; profiles?; path_sketch: sketch; path: [curve ids in order]; operation?"),
- CommandSpec::new("SolidLoft", "Loft", loft).at("SOLID", "CREATE").icon("loft").params("sections: [{sketch, profiles?} | {sketch, point: id} (an apex)] (2 or more, in order); operation?"),
- CommandSpec::new("PrimitiveBox", "Box", prim_box).at("SOLID", "CREATE").icon("box").params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z]; operation?"),
- CommandSpec::new("PrimitiveCylinder", "Cylinder", prim_cylinder).at("SOLID", "CREATE").icon("cylinder").params("radius | diameter, height: expr; base?: [x,y,z]; axis?: [x,y,z]; operation?"),
- CommandSpec::new("PrimitiveSphere", "Sphere", prim_sphere).at("SOLID", "CREATE").icon("sphere").params("radius | diameter: expr; center?: [x,y,z]; operation?"),
- CommandSpec::new("PrimitiveTorus", "Torus", prim_torus).at("SOLID", "CREATE").icon("torus").params("major, minor: expr (radii); center?; operation?"),
- CommandSpec::new("FusionFilletEdgesCommand", "Fillet", fillet)
+ CommandSpec::new("solid.loft", "Loft", loft).at("SOLID", "CREATE").icon("loft").params("sections: [{sketch, profiles?} | {sketch, point: id} (an apex)] (2 or more, in order); operation?"),
+ CommandSpec::new("solid.box", "Box", prim_box).at("SOLID", "CREATE").icon("box").params("length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z]; operation?"),
+ CommandSpec::new("solid.cylinder", "Cylinder", prim_cylinder).at("SOLID", "CREATE").icon("cylinder").params("radius | diameter, height: expr; base?: [x,y,z]; axis?: [x,y,z]; operation?"),
+ CommandSpec::new("solid.sphere", "Sphere", prim_sphere).at("SOLID", "CREATE").icon("sphere").params("radius | diameter: expr; center?: [x,y,z]; operation?"),
+ CommandSpec::new("solid.torus", "Torus", prim_torus).at("SOLID", "CREATE").icon("torus").params("major, minor: expr (radii); center?; operation?"),
+ CommandSpec::new("solid.fillet", "Fillet", fillet)
.at("SOLID", "MODIFY")
.icon("fillet")
.key("F")
.params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other); radius2?; start?: [x,y,z]"),
- CommandSpec::new("FusionChamferCommand", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}]; distance: expr; distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?"),
- CommandSpec::new("FusionCombineCommand", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool"),
- CommandSpec::new("PatternRectangular", "Rectangular Pattern", pattern_rect)
+ CommandSpec::new("solid.chamfer", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}]; distance: expr; distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?"),
+ CommandSpec::new("solid.combine", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool"),
+ CommandSpec::new("solid.pattern.rectangular", "Rectangular Pattern", pattern_rect)
.at("SOLID", "CREATE")
.icon("pattern_rect")
.params("features: [names] | bodies: [names] | components: [names]; dir1: [x,y,z]; count1; spacing1; dir2?, count2?, spacing2?"),
- CommandSpec::new("PatternCircular", "Circular Pattern", pattern_circ)
+ CommandSpec::new("solid.pattern.circular", "Circular Pattern", pattern_circ)
.at("SOLID", "CREATE")
.icon("pattern_circ")
.params("features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg)"),
- CommandSpec::new("PatternOnPath", "Pattern on Path", pattern_path)
+ CommandSpec::new("solid.pattern.path", "Pattern on Path", pattern_path)
.at("SOLID", "CREATE")
.icon("pattern_rect")
.params("features: [names] | bodies: [names]; path_sketch: sketch; path: [curve ids in order]; count; spacing (between instances) | distance (first to last); orientation?: identical|path; flip?: bool"),
- CommandSpec::new("MirrorCommand", "Mirror", mirror).at("SOLID", "CREATE").icon("mirror").params("features: [names] | bodies: [names] (combine?: join with the original); plane: XY|XZ|YZ | {origin, x_dir, y_dir}"),
- CommandSpec::new("FusionHoleCommand", "Hole", hole)
+ CommandSpec::new("solid.mirror", "Mirror", mirror).at("SOLID", "CREATE").icon("mirror").params("features: [names] | bodies: [names] (combine?: join with the original); plane: XY|XZ|YZ | {origin, x_dir, y_dir}"),
+ CommandSpec::new("solid.hole", "Hole", hole)
.at("SOLID", "CREATE")
.icon("hole")
.key("H")
.params("position: [x,y,z] on a face | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch); direction?: [x,y,z] (default: into the face); diameter | thread (a tapped hole: the tap drill, major − pitch) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\" (cosmetic)"),
- CommandSpec::new("PrimitivePipe", "Pipe", pipe)
+ CommandSpec::new("solid.pipe", "Pipe", pipe)
.at("SOLID", "CREATE")
.icon("pipe")
.params("path_sketch: sketch; path: [curve ids in order]; diameter: expr; wall?: expr (hollow); operation?, targets?, name?, body_name?"),
- CommandSpec::new("StockModelCommand", "Bounding Solid", bounding_solid)
+ CommandSpec::new("solid.bounding_solid", "Bounding Solid", bounding_solid)
.at("SOLID", "CREATE")
.icon("box")
.params("bodies?: [names] (default: all); margin?: expr; name?, body_name?"),
- CommandSpec::new("ModifyScale", "Scale", scale)
+ CommandSpec::new("solid.scale", "Scale", scale)
.at("SOLID", "MODIFY")
.icon("scale")
.params("bodies: [names]; factor: expr | factors: [x, y, z] exprs; origin?: [x,y,z]"),
- CommandSpec::new("FusionOffsetFacesCommand", "Offset Face", offset_face)
+ CommandSpec::new("solid.offset_face", "Offset Face", offset_face)
.at("SOLID", "MODIFY")
.icon("offset_face")
.params("faces: [[x,y,z] points on planar faces]; distance: expr (positive: outward); body?"),
- CommandSpec::new("FusionThreadCommand", "Thread", thread)
+ CommandSpec::new("solid.thread", "Thread", thread)
.at("SOLID", "CREATE")
.icon("thread")
.params("face: [x,y,z] on a cylindrical face; designation?: ISO metric (\"M8\", \"M8x1\"; default: the size that fits); length?: expr (default: the whole face). Cosmetic: the model is unchanged, model.threads lists it"),
- CommandSpec::new("FusionShellBodyCommand", "Shell", shell)
+ CommandSpec::new("solid.shell", "Shell", shell)
.at("SOLID", "MODIFY")
.icon("shell")
.params("faces: [[x,y,z] points on the faces to remove]; thickness: expr (inside); body?"),
- CommandSpec::new("FusionDraftCommand", "Draft", draft)
+ CommandSpec::new("solid.draft", "Draft", draft)
.at("SOLID", "MODIFY")
.icon("draft")
.params("faces: [[x,y,z]]; angle: expr; neutral: XY|XZ|YZ|plane name|{origin, normal}; pull?: [x,y,z] (default: neutral plane normal); body?"),
- CommandSpec::new("ConstructionPlaneOffsetFromPlaneCommand", "Offset Plane", plane_offset)
+ CommandSpec::new("construct.plane.offset", "Offset Plane", plane_offset)
.at("SOLID", "CONSTRUCT")
.icon("plane")
.params("base: XY|XZ|YZ|plane name; offset: expr; name?"),
- CommandSpec::new("ConstructionPlaneAtAngleCommand", "Plane at Angle", plane_angle)
+ CommandSpec::new("construct.plane.at_angle", "Plane at Angle", plane_angle)
.at("SOLID", "CONSTRUCT")
.icon("plane")
.params("base: XY|XZ|YZ|plane name; axis: X|Y|Z|{origin, dir}; angle: expr; name?"),
- CommandSpec::new("FusionSplitBodyCommand", "Split Body", split_body)
+ CommandSpec::new("solid.split_body", "Split Body", split_body)
.at("SOLID", "MODIFY")
.icon("split")
.params("body: name; plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended)"),
- CommandSpec::new("FusionPressPullCommand", "Press Pull", press_pull)
+ CommandSpec::new("solid.press_pull", "Press Pull", press_pull)
.at("SOLID", "MODIFY")
.icon("presspull")
.key("Q")
.params("face: [x,y,z] and distance: expr (positive adds material, negative cuts), or edges: [[x,y,z]…] and distance (a fillet)"),
- CommandSpec::new("FusionMoveCommand", "Move/Copy", move_bodies).at("SOLID", "MODIFY").icon("move").key("M").params("bodies: [names]; translate?: [x,y,z] (exprs or numbers); axis?: [x,y,z]; angle?: expr"),
+ CommandSpec::new("solid.move", "Move/Copy", move_bodies).at("SOLID", "MODIFY").icon("move").key("M").params("bodies: [names]; translate?: [x,y,z] (exprs or numbers); axis?: [x,y,z]; angle?: expr"),
];
/// Where an extrude starts and which way is positive: a point inside the (first) profile or
@@ -316,7 +316,7 @@ pub(super) fn add_feature(s: &mut Session, p: &Value, kind: FeatureKind) -> Resu
}
fn extrude(s: &mut Session, p: &Value) -> Result {
- let cmd = "Extrude";
+ let cmd = "solid.extrude";
// A planar body face extrudes like a profile: its boundary is copied into a sketch on it.
let (sketch, face_profile) = match p.get("face").and_then(vec3) {
Some(fp) => {
@@ -363,7 +363,7 @@ fn extrude(s: &mut Session, p: &Value) -> Result {
}
fn revolve(s: &mut Session, p: &Value) -> Result {
- let cmd = "Revolve";
+ let cmd = "solid.revolve";
// A planar body face revolves like a profile (its edges projected into a sketch on it).
let (sketch, face_profile) = match p.get("face").and_then(vec3) {
Some(fp) => {
@@ -404,7 +404,7 @@ fn radius_expr(p: &Value, cmd: &str) -> Result {
}
fn prim_box(s: &mut Session, p: &Value) -> Result {
- let cmd = "PrimitiveBox";
+ let cmd = "solid.box";
let (l, w, h) = (req_expr(cmd, p, "length")?, req_expr(cmd, p, "width")?, req_expr(cmd, p, "height")?);
for (e, n) in [(&l, "length"), (&w, "width"), (&h, "height")] {
check_expr(s, e, Kind::Length, cmd, n)?;
@@ -421,7 +421,7 @@ fn prim_box(s: &mut Session, p: &Value) -> Result {
}
fn prim_cylinder(s: &mut Session, p: &Value) -> Result {
- let cmd = "PrimitiveCylinder";
+ let cmd = "solid.cylinder";
let radius = radius_expr(p, cmd)?;
let height = req_expr(cmd, p, "height")?;
check_expr(s, &radius, Kind::Length, cmd, "radius")?;
@@ -432,7 +432,7 @@ fn prim_cylinder(s: &mut Session, p: &Value) -> Result {
}
fn prim_sphere(s: &mut Session, p: &Value) -> Result {
- let cmd = "PrimitiveSphere";
+ let cmd = "solid.sphere";
let radius = radius_expr(p, cmd)?;
check_expr(s, &radius, Kind::Length, cmd, "radius")?;
let center = p.get("center").and_then(vec3).unwrap_or(Vec3::ZERO);
@@ -440,7 +440,7 @@ fn prim_sphere(s: &mut Session, p: &Value) -> Result {
}
fn prim_torus(s: &mut Session, p: &Value) -> Result {
- let cmd = "PrimitiveTorus";
+ let cmd = "solid.torus";
let (major, minor) = (req_expr(cmd, p, "major")?, req_expr(cmd, p, "minor")?);
check_expr(s, &major, Kind::Length, cmd, "major")?;
check_expr(s, &minor, Kind::Length, cmd, "minor")?;
@@ -474,7 +474,7 @@ fn edge_points(s: &Session, p: &Value, cmd: &str) -> Result> {
/// Press Pull: a face moves along its normal (an extrude that joins outward or cuts inward); an
/// edge is rounded (a fillet).
fn press_pull(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionPressPullCommand";
+ let cmd = "solid.press_pull";
let distance = req_expr(cmd, p, "distance")?;
if p.get("edges").is_some() {
return fillet(s, &json!({"edges": p.get("edges"), "radius": distance}));
@@ -503,7 +503,7 @@ fn press_pull(s: &mut Session, p: &Value) -> Result {
return Ok(json!({"feature": f.id, "edited": f.name, key: new, "recomputed": out.get("recomputed")}));
}
// Another curved face moves along its normal.
- let r = super::find_command("FusionOffsetFacesCommand").ok_or_else(|| bad(cmd, "FusionOffsetFacesCommand"))?;
+ let r = super::find_command("solid.offset_face").ok_or_else(|| bad(cmd, "solid.offset_face"))?;
return (r.run)(s, &json!({"faces": [face], "distance": distance}));
}
let (dist, dir, op) = if d > 0.0 { (distance, "positive", "join") } else { (format!("-({distance})"), "negative", "cut") };
@@ -511,7 +511,7 @@ fn press_pull(s: &mut Session, p: &Value) -> Result {
}
fn fillet(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionFilletEdgesCommand";
+ let cmd = "solid.fillet";
let edges = edge_points(s, p, cmd)?;
let radius = req_expr(cmd, p, "radius")?;
check_expr(s, &radius, Kind::Length, cmd, "radius")?;
@@ -531,7 +531,7 @@ fn fillet(s: &mut Session, p: &Value) -> Result {
}
fn chamfer(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionChamferCommand";
+ let cmd = "solid.chamfer";
let edges = edge_points(s, p, cmd)?;
let distance = req_expr(cmd, p, "distance")?;
check_expr(s, &distance, Kind::Length, cmd, "distance")?;
@@ -550,7 +550,7 @@ fn chamfer(s: &mut Session, p: &Value) -> Result {
}
fn combine(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionCombineCommand";
+ let cmd = "solid.combine";
let target = str_(p, "target").ok_or_else(|| bad(cmd, "`target` must be a body name"))?.to_string();
let tools = string_list(p, "tools");
if tools.is_empty() {
@@ -564,7 +564,7 @@ fn combine(s: &mut Session, p: &Value) -> Result {
}
fn move_bodies(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionMoveCommand";
+ let cmd = "solid.move";
let bodies = string_list(p, "bodies");
if bodies.is_empty() {
return Err(bad(cmd, "`bodies` must list bodies"));
@@ -658,7 +658,7 @@ fn pattern_sources(s: &Session, p: &Value, cmd: &str) -> Result<(Vec, Ve
}
fn pattern_rect(s: &mut Session, p: &Value) -> Result {
- let cmd = "PatternRectangular";
+ let cmd = "solid.pattern.rectangular";
let (features, bodies) = pattern_sources(s, p, cmd)?;
let dir1 = p.get("dir1").and_then(vec3).ok_or_else(|| bad(cmd, "`dir1` must be [x, y, z]"))?;
let count1 = req_expr(cmd, p, "count1")?;
@@ -678,7 +678,7 @@ fn pattern_rect(s: &mut Session, p: &Value) -> Result {
}
fn pattern_circ(s: &mut Session, p: &Value) -> Result {
- let cmd = "PatternCircular";
+ let cmd = "solid.pattern.circular";
let (features, bodies) = pattern_sources(s, p, cmd)?;
let (origin, axis) = axis_line(p, cmd)?;
let count = req_expr(cmd, p, "count")?;
@@ -690,7 +690,7 @@ fn pattern_circ(s: &mut Session, p: &Value) -> Result {
}
fn pattern_path(s: &mut Session, p: &Value) -> Result {
- let cmd = "PatternOnPath";
+ let cmd = "solid.pattern.path";
let (features, bodies) = pattern_sources(s, p, cmd)?;
let path_sketch = sketch_id(s, p.get("path_sketch").or_else(|| p.get("sketch")), cmd, "path_sketch")?;
let path = string_list(p, "path");
@@ -716,7 +716,7 @@ fn pattern_path(s: &mut Session, p: &Value) -> Result {
}
fn mirror(s: &mut Session, p: &Value) -> Result {
- let cmd = "MirrorCommand";
+ let cmd = "solid.mirror";
let bodies = string_list(p, "bodies");
let features = if bodies.is_empty() { source_features(s, p, cmd)? } else { Vec::new() };
let plane = match p.get("plane") {
@@ -760,7 +760,7 @@ pub fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result Result {
- let cmd = "ConstructionPlaneOffsetFromPlaneCommand";
+ let cmd = "construct.plane.offset";
let base = plane_param(s, p.get("base"), cmd)?;
let d = req_expr(cmd, p, "offset")?;
check_expr(s, &d, Kind::Length, cmd, "offset")?;
@@ -768,7 +768,7 @@ fn plane_offset(s: &mut Session, p: &Value) -> Result {
}
fn plane_angle(s: &mut Session, p: &Value) -> Result {
- let cmd = "ConstructionPlaneAtAngleCommand";
+ let cmd = "construct.plane.at_angle";
let base = plane_param(s, p.get("base"), cmd)?;
let (axis_origin, axis_dir) = axis_line(p, cmd)?;
let angle = req_expr(cmd, p, "angle")?;
@@ -781,7 +781,7 @@ fn plane_angle(s: &mut Session, p: &Value) -> Result {
}
fn split_body(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionSplitBodyCommand";
+ let cmd = "solid.split_body";
let body = str_(p, "body").ok_or_else(|| bad(cmd, "`body` is required"))?.to_string();
if s.model.state().body(&body).is_none() {
return Err(bad(cmd, format!("no body `{body}`")));
@@ -846,7 +846,7 @@ fn clearance_diameter(size: &str, fit: &str) -> Option {
}
fn hole(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionHoleCommand";
+ let cmd = "solid.hole";
// At sketch points: the holes go perpendicular to the sketch and follow its points.
let points = match (p.get("sketch"), p.get("points")) {
(Some(_), Some(_)) => {
@@ -951,7 +951,7 @@ fn face_points(p: &Value, cmd: &str) -> Result> {
}
fn shell(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionShellBodyCommand";
+ let cmd = "solid.shell";
let faces = face_points(p, cmd)?;
let thickness = req_expr(cmd, p, "thickness")?;
check_expr(s, &thickness, Kind::Length, cmd, "thickness")?;
@@ -959,7 +959,7 @@ fn shell(s: &mut Session, p: &Value) -> Result {
}
fn draft(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionDraftCommand";
+ let cmd = "solid.draft";
let faces = face_points(p, cmd)?;
let angle = req_expr(cmd, p, "angle")?;
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
@@ -983,7 +983,7 @@ pub(super) fn sketch_id(s: &Session, v: Option<&Value>, cmd: &str, what: &str) -
}
fn sweep(s: &mut Session, p: &Value) -> Result {
- let cmd = "Sweep";
+ let cmd = "solid.sweep";
let sketch = feature_sketch(s, p, cmd)?;
let path_sketch = sketch_id(s, p.get("path_sketch"), cmd, "path_sketch")?;
let path = string_list(p, "path");
@@ -1005,7 +1005,7 @@ fn sweep(s: &mut Session, p: &Value) -> Result {
}
fn loft(s: &mut Session, p: &Value) -> Result {
- let cmd = "SolidLoft";
+ let cmd = "solid.loft";
let list = p.get("sections").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`sections` must be a list"))?;
if list.len() < 2 || list.len() > 50 {
return Err(bad(cmd, "a loft needs 2…50 sections"));
@@ -1028,7 +1028,7 @@ fn loft(s: &mut Session, p: &Value) -> Result {
}
fn thread(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionThreadCommand";
+ let cmd = "solid.thread";
let face = p
.get("face")
.and_then(|v| vec3(v).or_else(|| v.get("point").and_then(vec3)))
@@ -1050,7 +1050,7 @@ fn thread(s: &mut Session, p: &Value) -> Result {
}
fn pipe(s: &mut Session, p: &Value) -> Result {
- let cmd = "PrimitivePipe";
+ let cmd = "solid.pipe";
let path_sketch = sketch_id(s, p.get("path_sketch"), cmd, "path_sketch")?;
let path = string_list(p, "path");
if path.is_empty() {
@@ -1066,14 +1066,14 @@ fn pipe(s: &mut Session, p: &Value) -> Result {
}
fn bounding_solid(s: &mut Session, p: &Value) -> Result {
- let cmd = "StockModelCommand";
+ let cmd = "solid.bounding_solid";
let margin = expr(p, "margin").unwrap_or_else(|| "0".into());
check_expr(s, &margin, Kind::Length, cmd, "margin")?;
add_feature(s, p, FeatureKind::BoundingSolid { bodies: string_list(p, "bodies"), margin })
}
fn scale(s: &mut Session, p: &Value) -> Result {
- let cmd = "ModifyScale";
+ let cmd = "solid.scale";
let bodies = string_list(p, "bodies");
if bodies.is_empty() {
return Err(bad(cmd, "`bodies` must list bodies"));
@@ -1100,7 +1100,7 @@ fn scale(s: &mut Session, p: &Value) -> Result {
}
fn offset_face(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionOffsetFacesCommand";
+ let cmd = "solid.offset_face";
let faces = face_points(p, cmd)?;
let distance = req_expr(cmd, p, "distance")?;
check_expr(s, &distance, Kind::Length, cmd, "distance")?;
diff --git a/crates/engine/src/cmd/features_more.rs b/crates/engine/src/cmd/features_more.rs
index c6d68d09..bcc090e7 100644
--- a/crates/engine/src/cmd/features_more.rs
+++ b/crates/engine/src/cmd/features_more.rs
@@ -11,35 +11,35 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
use crate::{Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("PrimitiveCoil", "Coil", coil).at("SOLID", "CREATE").icon("coil").params(
+ CommandSpec::new("solid.coil", "Coil", coil).at("SOLID", "CREATE").icon("coil").params(
"diameter; two of revolutions (or turns), height, pitch; section_size; section?: circular|square; section_position?: inside|center|outside; \
base?: [x,y,z]; axis?: X|Y|Z|[x,y,z] (default Z); start_angle?; clockwise?: bool; operation?, targets?, name?, body_name?",
),
- CommandSpec::new("FusionRibCommand", "Rib", rib)
+ CommandSpec::new("solid.rib", "Rib", rib)
.at("SOLID", "CREATE")
.icon("rib")
.params("sketch, curves: [open curve ids, in order]; thickness (across the sketch plane); depth? (default: to the next face); flip?: bool (fill to the other side)"),
- CommandSpec::new("FusionWebCommand", "Web", web)
+ CommandSpec::new("solid.web", "Web", web)
.at("SOLID", "CREATE")
.icon("rib")
.params("sketch, curves: [open curve ids] (one wall each); thickness; depth? (default: to the next face); flip?: bool"),
- CommandSpec::new("EmbossCmd", "Emboss", emboss)
+ CommandSpec::new("solid.emboss", "Emboss", emboss)
.at("SOLID", "CREATE")
.icon("emboss")
.params("sketch (on a planar face), profiles?; depth; mode?: emboss|deboss (default emboss); targets?"),
- CommandSpec::new("FusionReplaceFaceCommand", "Replace Face", replace_face)
+ CommandSpec::new("solid.replace_face", "Replace Face", replace_face)
.at("SOLID", "MODIFY")
.icon("offset_face")
.params("faces: [[x,y,z] points on planar faces]; target: plane (XY|XZ|YZ|construction plane|{origin, normal}) | target_face: [x,y,z] on a parallel planar face; body?"),
- CommandSpec::new("AlignCmd", "Align", align)
+ CommandSpec::new("solid.align", "Align", align)
.at("SOLID", "MODIFY")
.icon("move")
.params("bodies: [names]; from: [x,y,z] | from_face: [x,y,z]; to: [x,y,z] | to_face: [x,y,z] (faces also turn to meet); flip?: bool"),
- CommandSpec::new("SoftDeleteCommand", "Remove", remove).at("SOLID", "MODIFY").icon("delete").params("bodies: [names] (removed from here on in the timeline)"),
+ CommandSpec::new("solid.remove", "Remove", remove).at("SOLID", "MODIFY").icon("delete").params("bodies: [names] (removed from here on in the timeline)"),
];
fn coil(s: &mut Session, p: &Value) -> Result {
- let cmd = "PrimitiveCoil";
+ let cmd = "solid.coil";
let base = p.get("base").and_then(vec3).unwrap_or(Vec3::ZERO);
let axis = match p.get("axis") {
None => Vec3::Z,
@@ -131,15 +131,15 @@ fn rib_like(s: &mut Session, p: &Value, cmd: &str, web: bool) -> Result {
}
fn rib(s: &mut Session, p: &Value) -> Result {
- rib_like(s, p, "FusionRibCommand", false)
+ rib_like(s, p, "solid.rib", false)
}
fn web(s: &mut Session, p: &Value) -> Result {
- rib_like(s, p, "FusionWebCommand", true)
+ rib_like(s, p, "solid.web", true)
}
fn emboss(s: &mut Session, p: &Value) -> Result {
- let cmd = "EmbossCmd";
+ let cmd = "solid.emboss";
let sketch = feature_sketch(s, p, cmd)?;
let profiles = profiles(p, cmd)?;
let depth = req_expr(cmd, p, "depth")?;
@@ -166,7 +166,7 @@ fn face_points(p: &Value, k: &str, cmd: &str) -> Result> {
}
fn replace_face(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionReplaceFaceCommand";
+ let cmd = "solid.replace_face";
let faces = face_points(p, "faces", cmd)?;
for f in &faces {
if super::face::face_normal(s, *f).is_none() {
@@ -186,7 +186,7 @@ fn replace_face(s: &mut Session, p: &Value) -> Result {
}
fn align(s: &mut Session, p: &Value) -> Result {
- let cmd = "AlignCmd";
+ let cmd = "solid.align";
let bodies = string_list(p, "bodies");
if bodies.is_empty() {
return Err(bad(cmd, "`bodies` must list the bodies to move"));
@@ -215,7 +215,7 @@ fn align(s: &mut Session, p: &Value) -> Result {
}
fn remove(s: &mut Session, p: &Value) -> Result {
- let cmd = "SoftDeleteCommand";
+ let cmd = "solid.remove";
let bodies = string_list(p, "bodies");
if bodies.is_empty() {
return Err(bad(cmd, "`bodies` must list bodies"));
diff --git a/crates/engine/src/cmd/features_more_tests.rs b/crates/engine/src/cmd/features_more_tests.rs
index 040ae48f..078464e1 100644
--- a/crates/engine/src/cmd/features_more_tests.rs
+++ b/crates/engine/src/cmd/features_more_tests.rs
@@ -10,7 +10,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
}
fn measure(s: &mut Session) -> Value {
- run(s, "MeasureCommand", json!({}))
+ run(s, "inspect.measure", json!({}))
}
fn volume(s: &mut Session) -> f64 {
@@ -24,76 +24,76 @@ fn rel(a: f64, b: f64) -> f64 {
/// An L bracket: a 40 x 20 x 5 base and a 5 x 20 x 40 wall at x = 0..5.
fn bracket() -> Session {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 20, "height": 5, "corner": [0, 0, 0], "body_name": "L"}));
- run(&mut s, "PrimitiveBox", json!({"length": 5, "width": 20, "height": 40, "corner": [0, 0, 0], "operation": "join"}));
+ run(&mut s, "solid.box", json!({"length": 40, "width": 20, "height": 5, "corner": [0, 0, 0], "body_name": "L"}));
+ run(&mut s, "solid.box", json!({"length": 5, "width": 20, "height": 40, "corner": [0, 0, 0], "operation": "join"}));
assert!(rel(volume(&mut s), 4000.0 + 3500.0) < 1e-9);
// Mid-plane of the bracket (y = 10): sketch x = X, sketch y = Z.
- run(&mut s, "SketchCreate", json!({"plane": {"origin": [0, 10, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, 1]}, "name": "RibSketch"}));
- run(&mut s, "DrawPolyline", json!({"points": [[5, 25], [25, 5]], "ids": ["l1"]}));
- run(&mut s, "SketchStop", json!({}));
+ run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 10, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, 1]}, "name": "RibSketch"}));
+ run(&mut s, "sketch.line", json!({"points": [[5, 25], [25, 5]], "ids": ["l1"]}));
+ run(&mut s, "sketch.finish", json!({}));
s
}
#[test]
fn rib_to_the_next_face_fills_the_corner() {
let mut s = bracket();
- run(&mut s, "FusionRibCommand", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4}));
+ run(&mut s, "solid.rib", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4}));
// The triangle (5,5)-(5,25)-(25,5) of area 200, 4 thick.
assert!(rel(volume(&mut s), 7500.0 + 800.0) < 1e-6, "{}", volume(&mut s));
assert_eq!(measure(&mut s)["body_count"], 1);
// The thickness is a parameter.
let t = s.doc.features.last().and_then(|f| f.param_names.first().cloned()).unwrap_or_default();
- run(&mut s, "ChangeParameterCommand", json!({"name": t, "expression": "2"}));
+ run(&mut s, "parameters.change", json!({"name": t, "expression": "2"}));
assert!(rel(volume(&mut s), 7500.0 + 400.0) < 1e-6);
}
#[test]
fn rib_with_a_depth_and_web() {
let mut s = bracket();
- run(&mut s, "FusionRibCommand", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4, "depth": 2}));
+ run(&mut s, "solid.rib", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4, "depth": 2}));
let v = volume(&mut s);
assert!(v > 7500.0 + 100.0 && v < 7500.0 + 800.0, "{v}");
let mut s = bracket();
- run(&mut s, "FusionWebCommand", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4}));
+ run(&mut s, "solid.web", json!({"sketch": "RibSketch", "curves": ["l1"], "thickness": 4}));
assert!(rel(volume(&mut s), 8300.0) < 1e-6);
// Curves that face no body.
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10}));
- run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Far"}));
- run(&mut s, "DrawPolyline", json!({"points": [[50, 50], [60, 60]], "ids": ["l1"]}));
- run(&mut s, "SketchStop", json!({}));
- let e = s.execute("FusionRibCommand", &json!({"sketch": "Far", "curves": ["l1"], "thickness": 1}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
+ run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Far"}));
+ run(&mut s, "sketch.line", json!({"points": [[50, 50], [60, 60]], "ids": ["l1"]}));
+ run(&mut s, "sketch.finish", json!({}));
+ let e = s.execute("solid.rib", &json!({"sketch": "Far", "curves": ["l1"], "thickness": 1}));
assert!(e.is_err_and(|e| e.to_string().contains("do not face a body")));
}
#[test]
fn emboss_and_deboss_on_a_face() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 10}));
- run(&mut s, "SketchCreate", json!({"plane": {"face": [20, 15, 10]}, "name": "Logo"}));
- run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [5, 5], "p1": [15, 15]}));
- run(&mut s, "SketchStop", json!({}));
- run(&mut s, "EmbossCmd", json!({"sketch": "Logo", "depth": 2}));
+ run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 10}));
+ run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 10]}, "name": "Logo"}));
+ run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [15, 15]}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "solid.emboss", json!({"sketch": "Logo", "depth": 2}));
assert!(rel(volume(&mut s), 12000.0 + 200.0) < 1e-6, "{}", volume(&mut s));
- run(&mut s, "UndoCommand", json!({}));
- run(&mut s, "EmbossCmd", json!({"sketch": "Logo", "depth": 3, "mode": "deboss"}));
+ run(&mut s, "edit.undo", json!({}));
+ run(&mut s, "solid.emboss", json!({"sketch": "Logo", "depth": 3, "mode": "deboss"}));
assert!(rel(volume(&mut s), 12000.0 - 300.0) < 1e-6);
- assert!(s.execute("EmbossCmd", &json!({"sketch": "Logo", "depth": 1, "mode": "sideways"})).is_err());
+ assert!(s.execute("solid.emboss", &json!({"sketch": "Logo", "depth": 1, "mode": "sideways"})).is_err());
}
#[test]
fn replace_face_align_remove() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "A"}));
- run(&mut s, "FusionReplaceFaceCommand", json!({"faces": [[5, 5, 10]], "target": {"origin": [0, 0, 15], "normal": [0, 0, 1]}}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "A"}));
+ run(&mut s, "solid.replace_face", json!({"faces": [[5, 5, 10]], "target": {"origin": [0, 0, 15], "normal": [0, 0, 1]}}));
assert!(rel(volume(&mut s), 1500.0) < 1e-6);
- let e = s.execute("FusionReplaceFaceCommand", &json!({"faces": [[5, 5, 15]], "target": {"origin": [0, 0, 15], "normal": [1, 0, 1]}}));
+ let e = s.execute("solid.replace_face", &json!({"faces": [[5, 5, 15]], "target": {"origin": [0, 0, 15], "normal": [1, 0, 1]}}));
assert!(e.is_err() || s.model.results.last().is_some_and(|r| r.error.as_deref().unwrap_or("").contains("parallel")));
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "A"}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [50, 0, 0], "body_name": "B"}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "A"}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [50, 0, 0], "body_name": "B"}));
// B's top face onto A's top face: B turns over and sits on A.
- run(&mut s, "AlignCmd", json!({"bodies": ["B"], "from_face": [55, 5, 10], "to_face": [5, 5, 10]}));
+ run(&mut s, "solid.align", json!({"bodies": ["B"], "from_face": [55, 5, 10], "to_face": [5, 5, 10]}));
let m = measure(&mut s);
let b = m["bodies"].as_array().unwrap().iter().find(|b| b["name"] == "B").unwrap().clone();
let (lo, hi) = (b["bbox"]["min"].clone(), b["bbox"]["max"].clone());
@@ -101,11 +101,11 @@ fn replace_face_align_remove() {
assert!((got.as_f64().unwrap_or(f64::NAN) - want).abs() < 1e-6, "{b}");
}
// Points only: a plain move.
- run(&mut s, "AlignCmd", json!({"bodies": ["B"], "from": [0, 0, 10], "to": [0, 0, 30]}));
+ run(&mut s, "solid.align", json!({"bodies": ["B"], "from": [0, 0, 10], "to": [0, 0, 30]}));
let m = measure(&mut s);
let b = m["bodies"].as_array().unwrap().iter().find(|b| b["name"] == "B").unwrap().clone();
assert!((b["bbox"]["min"][2].as_f64().unwrap_or(0.0) - 30.0).abs() < 1e-6, "{b}");
- run(&mut s, "SoftDeleteCommand", json!({"bodies": ["B"]}));
+ run(&mut s, "solid.remove", json!({"bodies": ["B"]}));
assert_eq!(measure(&mut s)["body_count"], 1);
}
@@ -113,27 +113,27 @@ fn replace_face_align_remove() {
fn coil_and_loft_to_a_point() {
use std::f64::consts::{PI, TAU};
let mut s = Session::default();
- run(&mut s, "PrimitiveCoil", json!({"diameter": 40, "revolutions": 2, "pitch": 8, "section_size": 4}));
+ run(&mut s, "solid.coil", json!({"diameter": 40, "revolutions": 2, "pitch": 8, "section_size": 4}));
assert!(rel(volume(&mut s), PI * 4.0 * TAU * 20.0 * 2.0) < 1e-3, "{}", volume(&mut s));
// Height and revolutions give the pitch; a square section outside the diameter.
let mut s = Session::default();
run(
&mut s,
- "PrimitiveCoil",
+ "solid.coil",
json!({"diameter": 40, "revolutions": 1.5, "height": 15, "section": "square", "section_size": 2, "section_position": "outside", "axis": "X"}),
);
assert!(rel(volume(&mut s), 4.0 * TAU * 21.0 * 1.5) < 1e-3, "{}", volume(&mut s));
- assert!(s.execute("PrimitiveCoil", &json!({"diameter": 40, "pitch": 1, "revolutions": 3, "section_size": 4})).is_err(), "runs into itself");
- assert!(s.execute("PrimitiveCoil", &json!({"diameter": 40, "section_size": 4, "pitch": 8})).is_err(), "two of three");
+ assert!(s.execute("solid.coil", &json!({"diameter": 40, "pitch": 1, "revolutions": 3, "section_size": 4})).is_err(), "runs into itself");
+ assert!(s.execute("solid.coil", &json!({"diameter": 40, "section_size": 4, "pitch": 8})).is_err(), "two of three");
// Pyramid: a square lofted to a point.
let mut s = Session::default();
- run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Base"}));
- run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [-10, -10], "p1": [10, 10]}));
- run(&mut s, "SketchStop", json!({}));
- run(&mut s, "SketchCreate", json!({"plane": {"origin": [0, 0, 30], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Top"}));
- run(&mut s, "DrawPoint", json!({"point": [0, 0], "id": "apex"}));
- run(&mut s, "SketchStop", json!({}));
- run(&mut s, "SolidLoft", json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "apex"}]}));
+ run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
+ run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-10, -10], "p1": [10, 10]}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, 30], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}, "name": "Top"}));
+ run(&mut s, "sketch.point", json!({"point": [0, 0], "id": "apex"}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "solid.loft", json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "apex"}]}));
assert!(rel(volume(&mut s), 400.0 * 30.0 / 3.0) < 1e-6);
- assert!(s.execute("SolidLoft", &json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "nope"}]})).is_err());
+ assert!(s.execute("solid.loft", &json!({"sections": [{"sketch": "Base"}, {"sketch": "Top", "point": "nope"}]})).is_err());
}
diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs
index db754d77..21bf8fbb 100644
--- a/crates/engine/src/cmd/file.rs
+++ b/crates/engine/src/cmd/file.rs
@@ -9,22 +9,22 @@ use crate::params::{bad, bool_, str_, string_list};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("NewDocumentCommand", "New Design", new_doc).icon("new").key("Ctrl+N").noundo().params("name?"),
+ CommandSpec::new("file.new", "New Design", new_doc).icon("new").key("Ctrl+N").noundo().params("name?"),
CommandSpec::new("doc.open", "Open", open)
.icon("open")
.key("Ctrl+O")
.noundo()
.params("path: .solvecraft design, or a .step/.stp file (opens as a new design)"),
- CommandSpec::new("FusionImportCommandFromToolbar", "Insert STEP", insert_step)
+ CommandSpec::new("file.insert_step", "Insert STEP", insert_step)
.at("SOLID", "INSERT")
.icon("import")
.params("path: .step/.stp file (its bodies join the design as an Import base feature); name?"),
- CommandSpec::new("ParaMeshInsertAlignCommand", "Insert Mesh", insert_mesh)
+ CommandSpec::new("file.insert_mesh", "Insert Mesh", insert_mesh)
.at("SOLID", "INSERT")
.icon("import")
.params("path: .3mf or .stl file (its meshes join the design as mesh bodies); name?"),
- CommandSpec::new("SaveDocumentCommand", "Save", save).icon("save").key("Ctrl+S").noundo().params("path? (default: current file)"),
- CommandSpec::new("SaveDocumentAsCommand", "Save As", save_as).icon("save").noundo().params("path"),
+ CommandSpec::new("file.save", "Save", save).icon("save").key("Ctrl+S").noundo().params("path? (default: current file)"),
+ CommandSpec::new("file.save_as", "Save As", save_as).icon("save").noundo().params("path"),
CommandSpec::new("doc.recovery_list", "Recoverable Designs", recovery_list)
.noundo()
.params("dir? (default: the recovery folder) → designs autosaved by apps that crashed or closed with unsaved changes"),
@@ -32,11 +32,11 @@ pub static COMMANDS: &[CommandSpec] = &[
.noundo()
.params("id (from doc.recovery_list); dir?; discard?: bool (delete the entry once open) — opens it with its file path, unsaved"),
CommandSpec::new("doc.recovery_discard", "Discard Recovered Design", recovery_discard).noundo().params("id, or all: true; dir?"),
- CommandSpec::new("ExportCommand", "Export", export)
+ CommandSpec::new("file.export", "Export", export)
.icon("export")
.noundo()
.params("path; format?: stl|stla|obj|step|3mf (default from extension); bodies?: [names]"),
- CommandSpec::new("FusionSaveAsSTLCommand", "Save As Mesh", save_stl).icon("export").noundo().params("path; bodies?: [names]; ascii?: bool"),
+ CommandSpec::new("file.save_mesh", "Save As Mesh", save_stl).icon("export").noundo().params("path; bodies?: [names]; ascii?: bool"),
];
fn new_doc(s: &mut Session, p: &Value) -> Result {
@@ -103,7 +103,7 @@ fn add_mesh(s: &mut Session, path: &str, name: Option<&str>) -> Result {
}
fn insert_mesh(s: &mut Session, p: &Value) -> Result {
- let cmd = "ParaMeshInsertAlignCommand";
+ let cmd = "file.insert_mesh";
let path = path_arg(p, cmd)?;
if !solvecraft_io::is_mesh_path(path) {
return Err(bad(cmd, "only 3MF and STL files (.3mf, .stl) can be inserted as meshes"));
@@ -112,7 +112,7 @@ fn insert_mesh(s: &mut Session, p: &Value) -> Result {
}
fn insert_step(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionImportCommandFromToolbar";
+ let cmd = "file.insert_step";
let path = path_arg(p, cmd)?;
if !solvecraft_io::is_step_path(path) {
return Err(bad(cmd, "only STEP files (.step, .stp) can be inserted"));
@@ -156,13 +156,13 @@ fn write(s: &mut Session, path: &str) -> Result {
fn save(s: &mut Session, p: &Value) -> Result {
let path = match str_(p, "path") {
Some(x) if !x.trim().is_empty() => x.to_string(),
- _ => s.path.clone().ok_or_else(|| bad("SaveDocumentCommand", "the design has no file yet: give a `path`"))?,
+ _ => s.path.clone().ok_or_else(|| bad("file.save", "the design has no file yet: give a `path`"))?,
};
write(s, &path)
}
fn save_as(s: &mut Session, p: &Value) -> Result {
- let path = path_arg(p, "SaveDocumentAsCommand")?.to_string();
+ let path = path_arg(p, "file.save_as")?.to_string();
write(s, &path)
}
@@ -180,13 +180,13 @@ fn export_to(s: &Session, path: &str, format: Format, bodies: &[String]) -> Resu
}
fn export(s: &mut Session, p: &Value) -> Result {
- let path = path_arg(p, "ExportCommand")?;
+ let path = path_arg(p, "file.export")?;
let format = Format::from_name(str_(p, "format").unwrap_or(path))?;
export_to(s, path, format, &string_list(p, "bodies"))
}
fn save_stl(s: &mut Session, p: &Value) -> Result {
- let path = path_arg(p, "FusionSaveAsSTLCommand")?;
+ let path = path_arg(p, "file.save_mesh")?;
let f = if bool_(p, "ascii").unwrap_or(false) { Format::StlAscii } else { Format::StlBinary };
export_to(s, path, f, &string_list(p, "bodies"))
}
diff --git a/crates/engine/src/cmd/inspect.rs b/crates/engine/src/cmd/inspect.rs
index 3d79f218..7a9d558b 100644
--- a/crates/engine/src/cmd/inspect.rs
+++ b/crates/engine/src/cmd/inspect.rs
@@ -8,7 +8,7 @@ use crate::params::{bad, bool_, str_, string_list};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("MeasureCommand", "Measure", measure)
+ CommandSpec::new("inspect.measure", "Measure", measure)
.at("SOLID", "INSPECT")
.icon("measure")
.key("I")
@@ -19,11 +19,11 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("model.edges", "List Edges", model_edges).noundo().params("body: name"),
CommandSpec::new("model.faces", "List Faces", model_faces).noundo().params("body: name"),
CommandSpec::new("model.threads", "List Threads", model_threads).noundo(),
- CommandSpec::new("PhysicalMaterialCommand", "Physical Material", physical_material)
+ CommandSpec::new("material.assign", "Physical Material", physical_material)
.at("SOLID", "MODIFY")
.icon("material")
.params("bodies: [names]; material: name (Steel, Aluminum, ABS Plastic, …; `material.list`)"),
- CommandSpec::new("AppearanceCommand", "Appearance", appearance).at("SOLID", "MODIFY").icon("appearance").key("A").params(
+ CommandSpec::new("appearance.assign", "Appearance", appearance).at("SOLID", "MODIFY").icon("appearance").key("A").params(
"bodies?: [names]; faces?: [[x,y,z] on faces] (body?: which body, default the nearest); components?: [names or ids, or occurrence names]; \
appearance?: library name (appearance.library) | color: \"#rrggbb\" or [r, g, b] (0–255), opacity? (0–1, default 1); \
neither (or color: null): clear, so the face follows its body, the body its component, the component its material",
@@ -74,7 +74,7 @@ fn measure(s: &mut Session, p: &Value) -> Result {
}
}
if !want.is_empty() && bodies.len() != want.len() {
- return Err(bad("MeasureCommand", "unknown body name"));
+ return Err(bad("inspect.measure", "unknown body name"));
}
// Sheet metal keeps the edges where bends meet flat faces (as Fusion does): count its
// B-rep faces as they are, without merging coplanar neighbours.
@@ -265,7 +265,7 @@ fn model_threads(s: &mut Session, _p: &Value) -> Result {
}
fn physical_material(s: &mut Session, p: &Value) -> Result {
- let cmd = "PhysicalMaterialCommand";
+ let cmd = "material.assign";
let bodies = string_list(p, "bodies");
let material = str_(p, "material").ok_or_else(|| bad(cmd, "`material` must be a material name"))?;
let canonical = solvecraft_doc::MATERIALS
@@ -304,7 +304,7 @@ fn color_param(v: &Value) -> Option<[u8; 3]> {
fn appearance(s: &mut Session, p: &Value) -> Result {
use solvecraft_doc::appearance::{FaceLook, Look, library};
- let cmd = "AppearanceCommand";
+ let cmd = "appearance.assign";
let look: Option = match (str_(p, "appearance"), p.get("color")) {
// A colour with a name: that colour, called that (a name not in the library is fine).
(Some(n), Some(v)) if !v.is_null() => {
diff --git a/crates/engine/src/cmd/joints.rs b/crates/engine/src/cmd/joints.rs
index 722f2319..e51d28e8 100644
--- a/crates/engine/src/cmd/joints.rs
+++ b/crates/engine/src/cmd/joints.rs
@@ -13,21 +13,21 @@ use crate::params::{bad, bool_, num, str_, string_list, vec3};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("JointOrigin", "Joint Origin", joint_origin).at("SOLID", "CREATE").icon("joint_origin").params(
+ CommandSpec::new("joint.origin", "Joint Origin", joint_origin).at("SOLID", "CREATE").icon("joint_origin").params(
"name?, occurrence?: id|name (default: from the pick), face: [x,y,z] (centre of a planar face) | circle: [x,y,z] (centre of a round end) | point: [x,y,z], z?: [x,y,z]",
),
- CommandSpec::new("JointAssembleCmdNew", "Joint", joint).at("SOLID", "ASSEMBLE").icon("joint").key("J").params(
+ CommandSpec::new("joint.create", "Joint", joint).at("SOLID", "ASSEMBLE").icon("joint").key("J").params(
"type: rigid|revolute|slider|cylindrical|pin_slot|planar|ball; a, b: joint origins ({occurrence, face|circle|point: [x,y,z] in world} | origin name); \
values?: [angles in deg or expressions, distances]; limits?: [[min, max] | null…]; flip?: bool; offset?; angle?; name?",
),
- CommandSpec::new("JointAsBuiltCmd", "As-Built Joint", as_built).at("SOLID", "ASSEMBLE").icon("as_built").params(
+ CommandSpec::new("joint.as_built", "As-Built Joint", as_built).at("SOLID", "ASSEMBLE").icon("as_built").params(
"type, a: occurrence, b: occurrence (stay where they are); at?: joint origin (default: b's origin) — the joint's frame",
),
- CommandSpec::new("RigidGroupCmd", "Rigid Group", rigid_group).at("SOLID", "ASSEMBLE").icon("rigid_group").params("occurrences: [ids or names] (move together, as they are)"),
- CommandSpec::new("FusionMoveJointsCommand", "Drive Joints", drive).at("SOLID", "ASSEMBLE").icon("drive").params(
+ CommandSpec::new("joint.rigid_group", "Rigid Group", rigid_group).at("SOLID", "ASSEMBLE").icon("rigid_group").params("occurrences: [ids or names] (move together, as they are)"),
+ CommandSpec::new("joint.drive", "Drive Joints", drive).at("SOLID", "ASSEMBLE").icon("drive").params(
"joint: id|name; value | values: [angles in deg or expressions, distances] (clamped to the limits)",
),
- CommandSpec::new("FusionMotionRelationshipCommand", "Motion Link", motion_link)
+ CommandSpec::new("joint.motion_link", "Motion Link", motion_link)
.at("SOLID", "ASSEMBLE")
.icon("motion_link")
.params("a, b: joints; ratio (b = ratio · a + offset); offset?; ia?, ib?: value indices (default 0)"),
@@ -36,7 +36,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("joint.delete", "Delete Joint", delete).params("joint: id|name (or origin: name, link: index)"),
CommandSpec::new("joint.list", "List Joints", list).noundo().params("→ joints (type, occurrences, values, limits), origins, links, degrees of freedom, conflicts"),
CommandSpec::new("joint.solve", "Solve Joints", solve_cmd).params("re-place the occurrences from the joints → conflicts, errors, dof"),
- CommandSpec::new("InterferenceCheckCommand", "Interference", interference).at("SOLID", "INSPECT").icon("interference").noundo().params(
+ CommandSpec::new("inspect.interference", "Interference", interference).at("SOLID", "INSPECT").icon("interference").noundo().params(
"bodies?: [names] (default: all, in world placement) → pairs that overlap with the overlap volume, area, bounding box and centre",
),
];
@@ -221,7 +221,7 @@ fn add_joint(s: &mut Session, j: Joint) -> Result {
return Err(EngineError::Other("too many joints".into()));
}
if j.a.occurrence == j.b.occurrence {
- return Err(bad("JointAssembleCmdNew", "a joint needs two different occurrences"));
+ return Err(bad("joint.create", "a joint needs two different occurrences"));
}
let id = j.id;
let name = j.name.clone();
@@ -234,7 +234,7 @@ fn add_joint(s: &mut Session, j: Joint) -> Result {
}
fn joint_origin(s: &mut Session, p: &Value) -> Result {
- let cmd = "JointOrigin";
+ let cmd = "joint.origin";
let origin = origin_param(s, Some(p), cmd)?;
let name = match str_(p, "name").map(str::trim).filter(|n| !n.is_empty()) {
Some(n) => n.to_string(),
@@ -254,7 +254,7 @@ fn kind_param(p: &Value, cmd: &str) -> Result {
}
fn joint(s: &mut Session, p: &Value) -> Result {
- let cmd = "JointAssembleCmdNew";
+ let cmd = "joint.create";
let kind = kind_param(p, cmd)?;
let a = origin_param(s, p.get("a"), cmd)?;
let b = origin_param(s, p.get("b"), cmd)?;
@@ -285,7 +285,7 @@ fn joint(s: &mut Session, p: &Value) -> Result {
}
fn as_built(s: &mut Session, p: &Value) -> Result {
- let cmd = "JointAsBuiltCmd";
+ let cmd = "joint.as_built";
let kind = kind_param(p, cmd)?;
let (oa, ob) = (occurrence_id(s, p.get("a"), cmd)?, occurrence_id(s, p.get("b"), cmd)?);
// The joint's frame in the world: the given origin, or B's component origin.
@@ -318,7 +318,7 @@ fn as_built(s: &mut Session, p: &Value) -> Result {
}
fn rigid_group(s: &mut Session, p: &Value) -> Result {
- let cmd = "RigidGroupCmd";
+ let cmd = "joint.rigid_group";
let list = p.get("occurrences").and_then(Value::as_array).cloned().unwrap_or_default();
if list.len() < 2 || list.len() > 1000 {
return Err(bad(cmd, "`occurrences` must list 2…1000 occurrences"));
@@ -334,7 +334,7 @@ fn rigid_group(s: &mut Session, p: &Value) -> Result {
}
fn drive(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionMoveJointsCommand";
+ let cmd = "joint.drive";
let id = joint_id(s, p.get("joint"), cmd)?;
let kind = s.doc.assembly.joints.iter().find(|j| j.id == id).map(|j| j.kind).ok_or_else(|| bad(cmd, "joint"))?;
if kind == JointKind::Rigid {
@@ -364,7 +364,7 @@ fn drive(s: &mut Session, p: &Value) -> Result {
}
fn motion_link(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionMotionRelationshipCommand";
+ let cmd = "joint.motion_link";
let (a, b) = (joint_id(s, p.get("a"), cmd)?, joint_id(s, p.get("b"), cmd)?);
if a == b {
return Err(bad(cmd, "link two different joints"));
@@ -523,7 +523,7 @@ fn solve_cmd(s: &mut Session, _p: &Value) -> Result {
}
fn interference(s: &mut Session, p: &Value) -> Result {
- let cmd = "InterferenceCheckCommand";
+ let cmd = "inspect.interference";
let want = string_list(p, "bodies");
let st = s.world_state();
let bodies: Vec<&solvecraft_doc::ModelBody> =
diff --git a/crates/engine/src/cmd/joints_tests.rs b/crates/engine/src/cmd/joints_tests.rs
index 7b091ed1..0e46e79b 100644
--- a/crates/engine/src/cmd/joints_tests.rs
+++ b/crates/engine/src/cmd/joints_tests.rs
@@ -29,9 +29,9 @@ fn near(a: Vec3, b: Vec3) -> bool {
/// at y = 33, z = 4.
fn leaf(s: &mut Session, name: &str) {
run(s, "component.activate", json!({"component": "root"}));
- run(s, "FusionCreateNewComponentCommand", json!({"name": name}));
- run(s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}));
- run(s, "PrimitiveCylinder", json!({"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}));
+ run(s, "component.create", json!({"name": name}));
+ run(s, "solid.box", json!({"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}));
+ run(s, "solid.cylinder", json!({"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}));
}
#[test]
@@ -43,7 +43,7 @@ fn revolute_hinge_turns_about_the_knuckle() {
run(&mut s, "occurrence.ground", json!({"occurrence": o, "grounded": true}));
let r = run(
&mut s,
- "JointAssembleCmdNew",
+ "joint.create",
json!({
"type": "revolute", "a": {"occurrence": "LeafA", "circle": [1, 37, 4]}, "b": {"occurrence": "LeafB", "circle": [1, 37, 4]}, "flip": true, "name": "Hinge"}),
);
@@ -54,7 +54,7 @@ fn revolute_hinge_turns_about_the_knuckle() {
let dof = l["dof"].as_array().unwrap().iter().find(|d| d["occurrence"] == occ(&s, "LeafB")).unwrap()["dof"].clone();
assert_eq!(dof, 1, "{l}");
// Drive 90°: B turns about the knuckle axis (through (0, 33, 4), along the frame's z = −X).
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Hinge", "value": 90}));
+ run(&mut s, "joint.drive", json!({"joint": "Hinge", "value": 90}));
let want = solvecraft_doc::rigid(Vec3::ZERO, Vec3::new(0.0, 33.0, 4.0), Vec3::new(-1.0, 0.0, 0.0), FRAC_PI_2);
let p = Vec3::new(20.0, 0.0, 0.0);
assert!(near(place(&s, "LeafB", p), solvecraft_doc::apply_point(&want, p)), "{:?}", place(&s, "LeafB", p));
@@ -63,9 +63,9 @@ fn revolute_hinge_turns_about_the_knuckle() {
assert!(near(place(&s, "LeafA", p), p));
assert!(near(place(&s, "LeafB", Vec3::new(10.0, 33.0, 4.0)), Vec3::new(10.0, 33.0, 4.0)));
// Expressions drive too, and undo puts it back.
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Hinge", "value": "pi rad"}));
+ run(&mut s, "joint.drive", json!({"joint": "Hinge", "value": "pi rad"}));
assert!(near(place(&s, "LeafB", p), Vec3::new(20.0, 66.0, 8.0)));
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert!(near(place(&s, "LeafB", p), Vec3::new(20.0, 29.0, 37.0)));
}
@@ -74,8 +74,8 @@ fn two_cubes() -> Session {
let mut s = Session::default();
for (name, x) in [("A", 0), ("B", 20)] {
run(&mut s, "component.activate", json!({"component": "root"}));
- run(&mut s, "FusionCreateNewComponentCommand", json!({"name": name}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0]}));
+ run(&mut s, "component.create", json!({"name": name}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0]}));
}
let o = occ(&s, "A");
run(&mut s, "occurrence.ground", json!({"occurrence": o, "grounded": true}));
@@ -88,19 +88,19 @@ fn slider_limits_clamp() {
// B's bottom face onto A's top face (faces meet), sliding along their common normal.
let r = run(
&mut s,
- "JointAssembleCmdNew",
+ "joint.create",
json!({"type": "slider", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "limits": [[0, 10]], "name": "Rail"}),
);
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{r}");
// B now sits on A.
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 10.0)), "{:?}", place(&s, "B", Vec3::new(25.0, 5.0, 0.0)));
- let r = run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 25}));
+ let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 25}));
assert_eq!(r["values"], json!([10.0]), "{r}");
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 20.0)));
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 10.0)));
// Changing the limits clamps the current value.
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 8}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 8}));
run(&mut s, "joint.limits", json!({"joint": "Rail", "min": 0, "max": 4}));
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 14.0)));
// Joints follow edits: A gets taller, B rides on its new top.
@@ -111,7 +111,7 @@ fn slider_limits_clamp() {
.find(|f| matches!(f.kind, solvecraft_doc::FeatureKind::Box { .. }))
.and_then(|f| f.param_names.get(2).cloned())
.unwrap_or_default();
- run(&mut s, "ChangeParameterCommand", json!({"name": h, "expression": "15"}));
+ run(&mut s, "parameters.change", json!({"name": h, "expression": "15"}));
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 19.0)), "{:?}", place(&s, "B", Vec3::new(25.0, 5.0, 0.0)));
}
@@ -119,12 +119,12 @@ fn slider_limits_clamp() {
fn rigid_cycles_solve_and_conflicts_report() {
let mut s = two_cubes();
run(&mut s, "component.activate", json!({"component": "root"}));
- run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "C"}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [40, 0, 0]}));
+ run(&mut s, "component.create", json!({"name": "C"}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [40, 0, 0]}));
run(&mut s, "component.activate", json!({"component": "root"}));
// A rigid group and a closing as-built joint: a consistent loop.
- run(&mut s, "RigidGroupCmd", json!({"occurrences": ["A", "B", "C"]}));
- let r = run(&mut s, "JointAsBuiltCmd", json!({"type": "rigid", "a": "B", "b": "C"}));
+ run(&mut s, "joint.rigid_group", json!({"occurrences": ["A", "B", "C"]}));
+ let r = run(&mut s, "joint.as_built", json!({"type": "rigid", "a": "B", "b": "C"}));
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0), "{r}");
assert!(near(place(&s, "C", Vec3::new(40.0, 0.0, 0.0)), Vec3::new(40.0, 0.0, 0.0)));
let l = run(&mut s, "joint.list", json!({}));
@@ -132,7 +132,7 @@ fn rigid_cycles_solve_and_conflicts_report() {
// A joint that wants C elsewhere conflicts and says so.
let r = run(
&mut s,
- "JointAssembleCmdNew",
+ "joint.create",
json!({"type": "rigid", "a": {"occurrence": "A", "point": [0, 0, 0]}, "b": {"occurrence": "C", "point": [40, 0, 0]}, "name": "Wrong"}),
);
let c = r["conflicts"].as_array().cloned().unwrap_or_default();
@@ -143,37 +143,37 @@ fn rigid_cycles_solve_and_conflicts_report() {
assert_eq!(r["conflicts"].as_array().map(Vec::len), Some(0));
assert!(
s.execute(
- "JointAssembleCmdNew",
+ "joint.create",
&json!({"type": "rigid", "a": {"occurrence": "A", "point": [0, 0, 0]}, "b": {"occurrence": "A", "point": [1, 0, 0]}})
)
.is_err()
);
- assert!(s.execute("JointAssembleCmdNew", &json!({"type": "hinge"})).is_err());
+ assert!(s.execute("joint.create", &json!({"type": "hinge"})).is_err());
}
#[test]
fn motion_links_and_interference() {
let mut s = two_cubes();
- run(&mut s, "JointAssembleCmdNew", json!({"type": "cylindrical", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "name": "Screw"}));
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Screw", "values": [0, 0]}));
+ run(&mut s, "joint.create", json!({"type": "cylindrical", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "name": "Screw"}));
+ run(&mut s, "joint.drive", json!({"joint": "Screw", "values": [0, 0]}));
// No overlap while B sits on A; then push B 4 mm into A.
- let i = run(&mut s, "InterferenceCheckCommand", json!({}));
+ let i = run(&mut s, "inspect.interference", json!({}));
assert_eq!(i["count"], 0, "{i}");
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Screw", "values": [0, -4]}));
- let i = run(&mut s, "InterferenceCheckCommand", json!({}));
+ run(&mut s, "joint.drive", json!({"joint": "Screw", "values": [0, -4]}));
+ let i = run(&mut s, "inspect.interference", json!({}));
assert_eq!(i["count"], 1, "{i}");
assert!((i["interferences"][0]["volume_mm3"].as_f64().unwrap_or(0.0) - 400.0).abs() < 1e-3, "{i}");
// A motion link: a second joint's value follows.
run(&mut s, "component.activate", json!({"component": "root"}));
- run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "D"}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [60, 0, 0]}));
+ run(&mut s, "component.create", json!({"name": "D"}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [60, 0, 0]}));
run(
&mut s,
- "JointAssembleCmdNew",
+ "joint.create",
json!({"type": "slider", "a": {"occurrence": "A", "face": [5, 5, 10]}, "b": {"occurrence": "D", "face": [65, 5, 0]}, "name": "Follower"}),
);
- run(&mut s, "FusionMotionRelationshipCommand", json!({"a": "Screw", "ia": 1, "b": "Follower", "ratio": 2}));
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Screw", "values": [0, 3]}));
+ run(&mut s, "joint.motion_link", json!({"a": "Screw", "ia": 1, "b": "Follower", "ratio": 2}));
+ run(&mut s, "joint.drive", json!({"joint": "Screw", "values": [0, 3]}));
assert!(near(place(&s, "D", Vec3::new(65.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 16.0)), "{:?}", place(&s, "D", Vec3::new(65.0, 5.0, 0.0)));
// Saved and reopened, joints come back.
let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
@@ -188,7 +188,7 @@ fn motion_links_and_interference() {
#[test]
fn faces_mate_by_default() {
let mut s = two_cubes();
- run(&mut s, "JointAssembleCmdNew", json!({"type": "rigid", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "name": "Stack"}));
+ run(&mut s, "joint.create", json!({"type": "rigid", "a": {"face": [5, 5, 10]}, "b": {"face": [25, 5, 0]}, "name": "Stack"}));
// B's bottom centre (25, 5, 0) at A's top centre, B above it.
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 0.0)), Vec3::new(5.0, 5.0, 10.0)));
assert!(near(place(&s, "B", Vec3::new(25.0, 5.0, 10.0)), Vec3::new(5.0, 5.0, 20.0)), "{:?}", place(&s, "B", Vec3::new(25.0, 5.0, 10.0)));
diff --git a/crates/engine/src/cmd/kernel_heal_tests.rs b/crates/engine/src/cmd/kernel_heal_tests.rs
index 125a67ad..0884599c 100644
--- a/crates/engine/src/cmd/kernel_heal_tests.rs
+++ b/crates/engine/src/cmd/kernel_heal_tests.rs
@@ -12,7 +12,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
}
fn volume(s: &mut Session) -> f64 {
- run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap()
+ run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap()
}
/// A box with a drilled hole (cone tip) shells: the cavity is the box shrunk by the wall and the
@@ -21,15 +21,15 @@ fn volume(s: &mut Session) -> f64 {
fn shell_a_box_with_a_drilled_hole() {
use std::f64::consts::PI;
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20}));
- run(&mut s, "FusionHoleCommand", json!({"position": [20, 15, 20], "diameter": 8, "depth": 10, "type": "drilled", "tip_angle": 118}));
+ run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
+ run(&mut s, "solid.hole", json!({"position": [20, 15, 20], "diameter": 8, "depth": 10, "type": "drilled", "tip_angle": 118}));
let v_body = volume(&mut s);
let (r, t) = (4.0, 2.0);
let a = (59.0f64).to_radians();
// Cylinder depth from the measured body: 24000 − π r² h − π r² (r / tan α) / 3.
let h = (24000.0 - v_body - PI * r * r * (r / a.tan()) / 3.0) / (PI * r * r);
assert!(h > 9.0 && h < 11.0, "{h}");
- run(&mut s, "FusionShellBodyCommand", json!({"faces": [[5, 5, 20]], "thickness": t}));
+ run(&mut s, "solid.shell", json!({"faces": [[5, 5, 20]], "thickness": t}));
let v_shell = volume(&mut s);
let apex = 20.0 - h - r / a.tan() - t / a.sin();
let big_r = r + t;
@@ -45,8 +45,8 @@ fn shell_a_box_with_a_drilled_hole() {
fn offset_cone_and_sphere_faces() {
use std::f64::consts::PI;
let mut s = Session::default();
- run(&mut s, "PrimitiveSphere", json!({"center": [0, 0, 0], "radius": 10}));
- let r = run(&mut s, "FusionOffsetFacesCommand", json!({"faces": [[10, 0, 0]], "distance": 2}));
+ run(&mut s, "solid.sphere", json!({"center": [0, 0, 0], "radius": 10}));
+ let r = run(&mut s, "solid.offset_face", json!({"faces": [[10, 0, 0]], "distance": 2}));
let v = volume(&mut s);
assert!((v - 4.0 / 3.0 * PI * 1728.0).abs() < 1e-3 * v, "{v} {r}");
}
@@ -56,10 +56,10 @@ fn offset_cone_and_sphere_faces() {
fn offset_torus_face() {
use std::f64::consts::PI;
let mut s = Session::default();
- run(&mut s, "PrimitiveTorus", json!({"major": 20, "minor": 5}));
+ run(&mut s, "solid.torus", json!({"major": 20, "minor": 5}));
let v0 = volume(&mut s);
assert!((v0 - 2.0 * PI * PI * 20.0 * 25.0).abs() < 1e-3 * v0, "{v0}");
- run(&mut s, "FusionOffsetFacesCommand", json!({"faces": [[25, 0, 0]], "distance": 1}));
+ run(&mut s, "solid.offset_face", json!({"faces": [[25, 0, 0]], "distance": 1}));
let v = volume(&mut s);
assert!((v - 2.0 * PI * PI * 20.0 * 36.0).abs() < 1e-3 * v, "{v}");
}
diff --git a/crates/engine/src/cmd/measure_sel.rs b/crates/engine/src/cmd/measure_sel.rs
index c4c9e5a4..95dc2dc9 100644
--- a/crates/engine/src/cmd/measure_sel.rs
+++ b/crates/engine/src/cmd/measure_sel.rs
@@ -1,4 +1,4 @@
-//! Measuring picked geometry (`MeasureCommand` with `items`): one item gives its length, area,
+//! Measuring picked geometry (`inspect.measure` with `items`): one item gives its length, area,
//! volume or position; two give the minimum distance between them (with the two closest
//! points) and, where both have a direction (planar faces, straight edges), the angle.
@@ -146,9 +146,9 @@ fn closest(a: &Geo, b: &Geo) -> Option<(Vec3, Vec3)> {
}
}
-/// `MeasureCommand` with `items` (one or two selections).
+/// `inspect.measure` with `items` (one or two selections).
pub(crate) fn measure_items(s: &Session, items: &Value) -> Result {
- let cmd = "MeasureCommand";
+ let cmd = "inspect.measure";
let list = items.as_array().ok_or_else(|| bad(cmd, "`items` must be a list of selections"))?;
if list.is_empty() || list.len() > 2 {
return Err(bad(cmd, "measure one or two items"));
diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs
index 13089238..1dcc59df 100644
--- a/crates/engine/src/cmd/mod.rs
+++ b/crates/engine/src/cmd/mod.rs
@@ -1,6 +1,6 @@
-//! The command registry. Ids follow Fusion's command ids where Fusion has the command; the
-//! toolbar placement (`tab`, `panel`) follows Fusion's Design workspace so the registry doubles
-//! as the parity metric (`cargo xtask parity`).
+//! The command registry. Ids are SolveCraft's own dotted ids (`solid.extrude`,
+//! `sketch.rectangle.two_point`); the toolbar placement (`tab`, `panel`) follows the familiar
+//! Design workspace layout. Ids used before the rename still resolve (`legacy_ids`).
mod browser;
mod clipboard;
@@ -165,5 +165,7 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
let all = command_specs();
+ // Ids from before the rename still find their command.
+ let id = crate::legacy_ids::current_id(id);
all.iter().find(|c| c.id == id).or_else(|| all.iter().find(|c| c.id.eq_ignore_ascii_case(id))).copied()
}
diff --git a/crates/engine/src/cmd/motion.rs b/crates/engine/src/cmd/motion.rs
index c9ea4300..8e566718 100644
--- a/crates/engine/src/cmd/motion.rs
+++ b/crates/engine/src/cmd/motion.rs
@@ -13,22 +13,22 @@ use crate::params::{bad, bool_, str_, vec3};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("EnableContactSetsCmd", "Enable Contact Sets", enable_sets).at("SOLID", "ASSEMBLE").icon("joint").params(
+ CommandSpec::new("contact.enable_sets", "Enable Contact Sets", enable_sets).at("SOLID", "ASSEMBLE").icon("joint").params(
"enabled?: bool (default true) — driving joints and motion studies stop where bodies in one contact set would pass through each other",
),
- CommandSpec::new("EnableAllContactCmd", "Enable All Contact", enable_all)
+ CommandSpec::new("contact.enable_all", "Enable All Contact", enable_all)
.at("SOLID", "ASSEMBLE")
.icon("joint")
.params("every occurrence takes part in contact (one set of all)"),
- CommandSpec::new("DisableAllContactCmd", "Disable Contact", disable_all).at("SOLID", "ASSEMBLE").icon("joint").params("contact off"),
- CommandSpec::new("ContactSetCmd", "New Contact Set", new_set)
+ CommandSpec::new("contact.disable_all", "Disable Contact", disable_all).at("SOLID", "ASSEMBLE").icon("joint").params("contact off"),
+ CommandSpec::new("contact.create", "New Contact Set", new_set)
.at("SOLID", "ASSEMBLE")
.icon("joint")
.params("occurrences: [ids or names] (two or more); name?"),
CommandSpec::new("contact.edit", "Edit Contact Set", edit_set).params("set: id|name; occurrences?; name?; suppressed?: bool; delete?: bool"),
CommandSpec::new("contact.list", "List Contact Sets", list_sets).noundo().params("→ sets, enabled, all"),
CommandSpec::new("contact.check", "Check Contact", check).noundo().params("→ pairs in contact sets that pass through each other now"),
- CommandSpec::new("FusionMotionStudyCommand", "Motion Study", study).at("SOLID", "ASSEMBLE").icon("joint").params(
+ CommandSpec::new("motion.study", "Motion Study", study).at("SOLID", "ASSEMBLE").icon("joint").params(
"name?; study? (edit an existing one); steps? (timeline length, default 100); keys: [{joint, index?, points: [[step, value], …]}] \
(values: degrees for angles, mm for distances, or expressions)",
),
@@ -285,7 +285,7 @@ fn drive_with_contact(s: &mut Session, want: &[(u64, Vec)]) -> Result) -> Result {
drive_with_contact(s, &[(id, q)])
}
@@ -311,7 +311,7 @@ fn disable_all(s: &mut Session, _p: &Value) -> Result {
}
fn new_set(s: &mut Session, p: &Value) -> Result {
- let cmd = "ContactSetCmd";
+ let cmd = "contact.create";
let occ = occurrences(s, p, cmd)?;
if occ.len() < 2 {
return Err(bad(cmd, "a contact set needs two or more occurrences"));
@@ -387,7 +387,7 @@ fn study_index(s: &Session, p: &Value, cmd: &str) -> Result {
}
fn study(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionMotionStudyCommand";
+ let cmd = "motion.study";
let existing = if p.get("study").is_some() { Some(study_index(s, p, cmd)?) } else { None };
let steps = match p.get("steps") {
Some(v) => {
diff --git a/crates/engine/src/cmd/motion_tests.rs b/crates/engine/src/cmd/motion_tests.rs
index 030ae964..dbf1f062 100644
--- a/crates/engine/src/cmd/motion_tests.rs
+++ b/crates/engine/src/cmd/motion_tests.rs
@@ -26,14 +26,14 @@ fn rail() -> Session {
let mut s = Session::default();
for (name, x) in [("A", 0), ("B", 20)] {
run(&mut s, "component.activate", json!({"component": "root"}));
- run(&mut s, "FusionCreateNewComponentCommand", json!({"name": name}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0]}));
+ run(&mut s, "component.create", json!({"name": name}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0]}));
}
run(&mut s, "component.activate", json!({"component": "root"}));
let a = occ(&s, "A");
run(&mut s, "occurrence.ground", json!({"occurrence": a, "grounded": true}));
- run(&mut s, "JointAssembleCmdNew", json!({"type": "slider", "a": {"face": [10, 5, 5]}, "b": {"face": [20, 5, 5]}, "name": "Rail"}));
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10}));
+ run(&mut s, "joint.create", json!({"type": "slider", "a": {"face": [10, 5, 5]}, "b": {"face": [20, 5, 5]}, "name": "Rail"}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
assert!((b_x(&s) - 20.0).abs() < 1e-6, "{}", b_x(&s));
s
}
@@ -42,45 +42,44 @@ fn rail() -> Session {
fn contact_sets_stop_a_driven_joint_at_touching() {
let mut s = rail();
// No contact: B passes into A.
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
assert!((b_x(&s) - 5.0).abs() < 1e-6);
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10}));
- run(&mut s, "ContactSetCmd", json!({"occurrences": ["A", "B"]}));
- let r = run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
+ run(&mut s, "contact.create", json!({"occurrences": ["A", "B"]}));
+ let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
assert!(r["stopped_by_contact"].is_object(), "{r}");
// Stopped where the faces touch (to the search's resolution, 15 mm / 2^14).
assert!((b_x(&s) - 10.0).abs() < 2e-3, "{}", b_x(&s));
assert!(b_x(&s) >= 10.0 - 2e-3, "a touch, not a pass");
assert_eq!(run(&mut s, "contact.check", json!({}))["collisions"].as_array().map(Vec::len), Some(0));
// Away is free; contact off lets it through again.
- let r = run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 3}));
+ let r = run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 3}));
assert!(r["stopped_by_contact"].is_null() && (b_x(&s) - 13.0).abs() < 1e-6, "{r}");
- run(&mut s, "DisableAllContactCmd", json!({}));
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
+ run(&mut s, "contact.disable_all", json!({}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
assert!((b_x(&s) - 5.0).abs() < 1e-6);
// Suppressed sets don't count; Enable All Contact puts every occurrence in one.
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10}));
- run(&mut s, "EnableContactSetsCmd", json!({}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
+ run(&mut s, "contact.enable_sets", json!({}));
run(&mut s, "contact.edit", json!({"set": "Contact Set1", "suppressed": true}));
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
assert!((b_x(&s) - 5.0).abs() < 1e-6);
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": 10}));
- run(&mut s, "EnableAllContactCmd", json!({}));
- run(&mut s, "FusionMoveJointsCommand", json!({"joint": "Rail", "value": -5}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": 10}));
+ run(&mut s, "contact.enable_all", json!({}));
+ run(&mut s, "joint.drive", json!({"joint": "Rail", "value": -5}));
assert!((b_x(&s) - 10.0).abs() < 2e-3);
- assert!(s.execute("ContactSetCmd", &json!({"occurrences": ["A"]})).is_err());
+ assert!(s.execute("contact.create", &json!({"occurrences": ["A"]})).is_err());
}
#[test]
fn motion_study_plays_and_exports() {
let mut s = rail();
- let r =
- run(&mut s, "FusionMotionStudyCommand", json!({"name": "Push", "steps": 100, "keys": [{"joint": "Rail", "points": [[0, 10], [100, -5]]}]}));
+ let r = run(&mut s, "motion.study", json!({"name": "Push", "steps": 100, "keys": [{"joint": "Rail", "points": [[0, 10], [100, -5]]}]}));
assert_eq!(r["keys"], 1);
run(&mut s, "motion.play", json!({"study": "Push", "step": 50}));
assert!((b_x(&s) - 12.5).abs() < 1e-6, "{}", b_x(&s));
// With contact, the end of the study stops at touching.
- run(&mut s, "ContactSetCmd", json!({"occurrences": ["A", "B"]}));
+ run(&mut s, "contact.create", json!({"occurrences": ["A", "B"]}));
let r = run(&mut s, "motion.play", json!({"study": "Push", "step": 100}));
assert!(r["stopped_by_contact"].is_object() && (b_x(&s) - 10.0).abs() < 2e-3, "{r}");
// Export: positions per sample; the design keeps its joint value.
@@ -104,7 +103,7 @@ fn motion_study_plays_and_exports() {
let text = std::fs::read_to_string(&csv).unwrap();
assert!(text.starts_with("step,occurrence,x,y,z") && text.lines().count() == 1 + 3 * 2, "{text}");
let _ = std::fs::remove_dir_all(&dir);
- assert!(s.execute("FusionMotionStudyCommand", &json!({"keys": [{"joint": "Rail", "index": 3, "points": [[0, 1]]}]})).is_err());
+ assert!(s.execute("motion.study", &json!({"keys": [{"joint": "Rail", "index": 3, "points": [[0, 1]]}]})).is_err());
run(&mut s, "motion.delete", json!({"study": "Push"}));
assert_eq!(run(&mut s, "motion.list", json!({}))["studies"].as_array().map(Vec::len), Some(0));
}
@@ -117,7 +116,7 @@ fn exploded_views_show_without_changing_the_design() {
assert_eq!(r["moves"].as_array().map(Vec::len), Some(2), "{r}");
run(&mut s, "explode.show", json!({"view": "Apart"}));
// Centres at x 5 and 25 → middle 15: each moves 10 mm further out.
- let bb = |s: &mut Session, b: &str| run(s, "MeasureCommand", json!({"bodies": [b]}))["bodies"][0]["bbox"]["min"][0].as_f64().unwrap_or(f64::NAN);
+ let bb = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({"bodies": [b]}))["bodies"][0]["bbox"]["min"][0].as_f64().unwrap_or(f64::NAN);
let names: Vec = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
assert!((bb(&mut s, &names[0]) + 10.0).abs() < 1e-6, "{names:?}");
assert!((bb(&mut s, &names[1]) - 30.0).abs() < 1e-6);
@@ -127,7 +126,7 @@ fn exploded_views_show_without_changing_the_design() {
run(&mut s, "explode.create", json!({"name": "Hand", "moves": [{"occurrence": "B", "translate": [0, 0, 50]}]}));
run(&mut s, "explode.show", json!({"view": "Hand"}));
assert!(
- (run(&mut s, "MeasureCommand", json!({"bodies": [names[1]]}))["bodies"][0]["bbox"]["min"][2].as_f64().unwrap_or(0.0) - 50.0).abs() < 1e-6
+ (run(&mut s, "inspect.measure", json!({"bodies": [names[1]]}))["bodies"][0]["bbox"]["min"][2].as_f64().unwrap_or(0.0) - 50.0).abs() < 1e-6
);
assert_eq!(run(&mut s, "explode.list", json!({}))["views"].as_array().map(Vec::len), Some(2));
}
diff --git a/crates/engine/src/cmd/parameters.rs b/crates/engine/src/cmd/parameters.rs
index 282a4463..82b8759d 100644
--- a/crates/engine/src/cmd/parameters.rs
+++ b/crates/engine/src/cmd/parameters.rs
@@ -10,7 +10,7 @@ use crate::params::{bad, bool_, str_};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("ChangeParameterCommand", "Change Parameters", change_param).at("SOLID", "MODIFY").icon("params").params(
+ CommandSpec::new("parameters.change", "Change Parameters", change_param).at("SOLID", "MODIFY").icon("params").params(
"name, expression (number or text), unit?: mm|cm|m|in|ft|deg|rad|\"\" (new parameters default to their value's unit), comment?, \
favorite?: bool, new_name?: rename (references follow); or delete: name",
),
@@ -65,7 +65,7 @@ fn known(s: &Session, name: &str) -> bool {
}
fn change_param(s: &mut Session, p: &Value) -> Result {
- let cmd = "ChangeParameterCommand";
+ let cmd = "parameters.change";
if let Some(n) = str_(p, "delete") {
s.doc_mut().remove_param(n.trim())?;
return Ok(json!({"deleted": n.trim()}));
diff --git a/crates/engine/src/cmd/parameters_tests.rs b/crates/engine/src/cmd/parameters_tests.rs
index f5dd4cc4..18983408 100644
--- a/crates/engine/src/cmd/parameters_tests.rs
+++ b/crates/engine/src/cmd/parameters_tests.rs
@@ -17,7 +17,7 @@ fn err(s: &mut Session, id: &str, p: Value) -> String {
}
fn volume(s: &mut Session) -> f64 {
- run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
+ run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
}
fn rel(a: f64, b: f64) -> f64 {
@@ -36,13 +36,13 @@ fn expr_of(s: &mut Session, name: &str) -> String {
/// A sketched block 40 x (len / 2) x 10 whose width is an expression of a user parameter.
fn block(s: &mut Session) {
run(s, "parameters.add", json!({"name": "len", "expression": "60 mm", "comment": "overall length", "favorite": true}));
- run(s, "SketchCreate", json!({"plane": "XY", "name": "Base"}));
- run(s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]}));
- run(s, "ConstraintCoincident", json!({"a": "p1", "b": "origin"}));
- run(s, "SketchDimension", json!({"entities": ["l1"], "value": 40}));
- run(s, "SketchDimension", json!({"entities": ["l2"], "value": "len/2"}));
- run(s, "SketchStop", json!({}));
- run(s, "Extrude", json!({"distance": 10, "body_name": "Block"}));
+ run(s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
+ run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]}));
+ run(s, "sketch.constraint.coincident", json!({"a": "p1", "b": "origin"}));
+ run(s, "sketch.dimension", json!({"entities": ["l1"], "value": 40}));
+ run(s, "sketch.dimension", json!({"entities": ["l2"], "value": "len/2"}));
+ run(s, "sketch.finish", json!({}));
+ run(s, "solid.extrude", json!({"distance": 10, "body_name": "Block"}));
}
#[test]
@@ -50,20 +50,20 @@ fn width_follows_an_expression() {
let mut s = Session::default();
block(&mut s);
assert!(rel(volume(&mut s), 40.0 * 30.0 * 10.0) < 1e-9);
- let r = run(&mut s, "ChangeParameterCommand", json!({"name": "len", "expression": "100 mm"}));
+ let r = run(&mut s, "parameters.change", json!({"name": "len", "expression": "100 mm"}));
assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}");
assert!(rel(volume(&mut s), 40.0 * 50.0 * 10.0) < 1e-9);
// Units convert: 4 in = 101.6 mm.
- run(&mut s, "ChangeParameterCommand", json!({"name": "len", "expression": "4 in"}));
+ run(&mut s, "parameters.change", json!({"name": "len", "expression": "4 in"}));
assert!(rel(volume(&mut s), 40.0 * 50.8 * 10.0) < 1e-9);
// A parameter in inches reads bare numbers as inches.
- run(&mut s, "ChangeParameterCommand", json!({"name": "len", "expression": "5", "unit": "in"}));
+ run(&mut s, "parameters.change", json!({"name": "len", "expression": "5", "unit": "in"}));
assert!(rel(volume(&mut s), 40.0 * 63.5 * 10.0) < 1e-9);
let l = run(&mut s, "parameters.list", json!({}));
assert_eq!(row(&l, "len")["value"], 5.0, "{l}");
assert_eq!(row(&l, "len")["unit"], "in");
// A failing parameter fails the features using it, with its error.
- let e = err(&mut s, "ChangeParameterCommand", json!({"name": "len", "expression": "5 deg"}));
+ let e = err(&mut s, "parameters.change", json!({"name": "len", "expression": "5 deg"}));
assert!(e.contains("expected a length, got an angle"), "{e}");
}
@@ -75,14 +75,14 @@ fn feature_inputs_are_named_and_editable() {
let dist = l["parameters"].as_array().unwrap().iter().find(|r| r["input"] == "Distance").unwrap().clone();
assert_eq!(dist["feature"], "Extrude1", "{l}");
let d = dist["name"].as_str().unwrap().to_string();
- run(&mut s, "ChangeParameterCommand", json!({"name": d, "expression": "len / 3"}));
+ run(&mut s, "parameters.change", json!({"name": d, "expression": "len / 3"}));
assert!(rel(volume(&mut s), 40.0 * 30.0 * 20.0) < 1e-9);
// Editing the feature keeps its parameter name.
run(&mut s, "timeline.edit", json!({"feature": "Extrude1", "set": {"extent": {"distance": "12 mm"}}}));
assert!(rel(volume(&mut s), 40.0 * 30.0 * 12.0) < 1e-9);
assert_eq!(expr_of(&mut s, &d), "12 mm");
// Another feature can use it.
- run(&mut s, "PrimitiveBox", json!({"length": d, "width": 5, "height": 5, "corner": [100, 0, 0]}));
+ run(&mut s, "solid.box", json!({"length": d, "width": 5, "height": 5, "corner": [100, 0, 0]}));
assert!(rel(volume(&mut s), 40.0 * 30.0 * 12.0 + 12.0 * 25.0) < 1e-9);
let u = run(&mut s, "parameters.users", json!({"name": d}));
assert_eq!(u["users"].as_array().map(Vec::len), Some(1), "{u}");
@@ -101,7 +101,7 @@ fn unit_errors_and_cycles_are_refused() {
assert!(e.contains("expected an angle, got a length"), "{e}");
assert!(err(&mut s, "parameters.add", json!({"name": "x", "expression": "1", "unit": "furlong"})).contains("unknown unit"));
run(&mut s, "parameters.add", json!({"name": "b", "expression": "a * 2"}));
- let e = err(&mut s, "ChangeParameterCommand", json!({"name": "a", "expression": "b + 1"}));
+ let e = err(&mut s, "parameters.change", json!({"name": "a", "expression": "b + 1"}));
assert!(e.contains("circular reference: a → b → a"), "{e}");
assert!(err(&mut s, "parameters.add", json!({"name": "selfref", "expression": "selfref"})).contains("circular"));
assert!(err(&mut s, "parameters.add", json!({"name": "a", "expression": "1"})).contains("already"));
@@ -132,7 +132,7 @@ fn rename_updates_every_reference() {
let l = run(&mut s, "parameters.list", json!({"favorites": true}));
assert_eq!(l["count"], 1, "the favourite follows the rename");
assert_eq!(row(&l, "length")["comment"], "overall length");
- run(&mut s, "ChangeParameterCommand", json!({"name": "length", "expression": "80"}));
+ run(&mut s, "parameters.change", json!({"name": "length", "expression": "80"}));
assert!(rel(volume(&mut s), 40.0 * 40.0 * 10.0) < 1e-9);
// Model and feature parameters can be renamed too: dimensions and features follow.
let dim = s.doc.features.first().and_then(|f| match &f.kind {
@@ -140,17 +140,17 @@ fn rename_updates_every_reference() {
_ => None,
});
let dim = dim.unwrap_or_default();
- run(&mut s, "ChangeParameterCommand", json!({"name": dim, "new_name": "base_w", "expression": "50"}));
+ run(&mut s, "parameters.change", json!({"name": dim, "new_name": "base_w", "expression": "50"}));
assert!(rel(volume(&mut s), 50.0 * 40.0 * 10.0) < 1e-9);
let ext = s.doc.features.get(1).and_then(|f| f.param_names.first().cloned()).unwrap_or_default();
run(&mut s, "parameters.rename", json!({"name": ext, "new_name": "thick"}));
- run(&mut s, "ChangeParameterCommand", json!({"name": "thick", "expression": "base_w / 10"}));
+ run(&mut s, "parameters.change", json!({"name": "thick", "expression": "base_w / 10"}));
assert!(rel(volume(&mut s), 50.0 * 40.0 * 5.0) < 1e-9);
assert!(err(&mut s, "parameters.rename", json!({"name": "twice", "new_name": "length"})).contains("already"));
assert!(err(&mut s, "parameters.rename", json!({"name": "nope", "new_name": "x"})).contains("unknown"));
// Undo puts the old name back.
- run(&mut s, "UndoCommand", json!({}));
- run(&mut s, "UndoCommand", json!({}));
+ run(&mut s, "edit.undo", json!({}));
+ run(&mut s, "edit.undo", json!({}));
assert!(s.doc.features.get(1).is_some_and(|f| f.param_names.first() == Some(&ext)));
}
@@ -165,7 +165,7 @@ fn deleting_a_used_parameter_lists_its_users() {
assert_eq!(u["users"].as_array().map(Vec::len), Some(2), "{u}");
assert_eq!(u["drives"][0]["name"], "Base", "{u}");
let dim = u["users"].as_array().unwrap().iter().find(|x| x["name"] != "spare").unwrap()["name"].as_str().unwrap().to_string();
- let e = err(&mut s, "ChangeParameterCommand", json!({"delete": dim}));
+ let e = err(&mut s, "parameters.change", json!({"delete": dim}));
assert!(e.contains("Base dimension"), "{e}");
run(&mut s, "parameters.delete", json!({"name": "spare"}));
assert!(s.doc.param("spare").is_none());
diff --git a/crates/engine/src/cmd/parts.rs b/crates/engine/src/cmd/parts.rs
index 0192b564..f8964ab6 100644
--- a/crates/engine/src/cmd/parts.rs
+++ b/crates/engine/src/cmd/parts.rs
@@ -18,7 +18,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("parts.library", "Standard Parts", library)
.noundo()
.params("family? → families (name, standard, sizes, lengths) and, with family, each size's dimensions (mm)"),
- CommandSpec::new("FusionFastenersCommand", "Insert Fastener", insert).at("SOLID", "INSERT").icon("fastener").params(
+ CommandSpec::new("parts.fastener", "Insert Fastener", insert).at("SOLID", "INSERT").icon("fastener").params(
"the Insert Part dialog's command (same as parts.insert): family, size, length?, at?|point?+direction?, name?",
),
CommandSpec::new("parts.insert", "Insert Part", insert).icon("fastener").params(
@@ -215,31 +215,31 @@ fn run(s: &mut Session, id: &str, p: Value) -> Result {
/// A hex prism (across flats `af`) from z0 to z1, joined or cut.
fn hex(s: &mut Session, af: f64, z0: f64, z1: f64, op: &str) -> Result<()> {
- run(s, "SketchCreate", json!({"plane": "XY", "offset": z0}))?;
- run(s, "ShapePolygonCircumscribed", json!({"center": [0, 0], "radius": af / 2.0, "sides": 6}))?;
- run(s, "SketchStop", json!({}))?;
+ run(s, "sketch.create", json!({"plane": "XY", "offset": z0}))?;
+ run(s, "sketch.polygon.circumscribed", json!({"center": [0, 0], "radius": af / 2.0, "sides": 6}))?;
+ run(s, "sketch.finish", json!({}))?;
let (dist, dir) = if z1 >= z0 { (z1 - z0, "positive") } else { (z0 - z1, "negative") };
- run(s, "Extrude", json!({"distance": dist, "direction": dir, "operation": op}))?;
+ run(s, "solid.extrude", json!({"distance": dist, "direction": dir, "operation": op}))?;
Ok(())
}
fn cyl(s: &mut Session, d: f64, z0: f64, z1: f64, op: &str) -> Result<()> {
- run(s, "PrimitiveCylinder", json!({"diameter": d, "height": z1 - z0, "base": [0, 0, z0], "axis": [0, 0, 1], "operation": op}))?;
+ run(s, "solid.cylinder", json!({"diameter": d, "height": z1 - z0, "base": [0, 0, z0], "axis": [0, 0, 1], "operation": op}))?;
Ok(())
}
/// A solid of revolution about z from a closed (r, z) outline, as one body or joined.
fn revolve(s: &mut Session, pts: &[(f64, f64)], op: &str) -> Result<()> {
- run(s, "SketchCreate", json!({"plane": "XZ"}))?;
+ run(s, "sketch.create", json!({"plane": "XZ"}))?;
let points: Vec<[f64; 2]> = pts.iter().map(|(r, z)| [*r, *z]).collect();
- run(s, "DrawPolyline", json!({"points": points, "closed": true}))?;
- run(s, "SketchStop", json!({}))?;
- run(s, "Revolve", json!({"axis": "y", "operation": op}))?;
+ run(s, "sketch.line", json!({"points": points, "closed": true}))?;
+ run(s, "sketch.finish", json!({}))?;
+ run(s, "solid.revolve", json!({"axis": "y", "operation": op}))?;
Ok(())
}
fn thread(s: &mut Session, d: f64, pitch: f64, at: Vec3) {
- let _ = run(s, "FusionThreadCommand", json!({"face": at, "designation": format!("M{}x{}", d, pitch)}));
+ let _ = run(s, "solid.thread", json!({"face": at, "designation": format!("M{}x{}", d, pitch)}));
let _ = d;
}
@@ -352,7 +352,7 @@ fn insert(s: &mut Session, p: &Value) -> Result {
};
let name = str_(p, "name").map(str::to_string).unwrap_or_else(|| part_name(&family, &size, &sh));
let active = s.active_component;
- let c = run(s, "FusionCreateNewComponentCommand", json!({"name": name}))?;
+ let c = run(s, "component.create", json!({"name": name}))?;
let (comp, occ) = (c["component"].as_u64().unwrap_or(0), c["occurrence"].as_u64().unwrap_or(0));
build(s, &sh)?;
s.doc_mut().parts.insert(comp, solvecraft_doc::StandardPart { family: family.clone(), size: size.clone(), length: shape_len(&sh) });
@@ -363,7 +363,7 @@ fn insert(s: &mut Session, p: &Value) -> Result {
// into the hole (the joint mates the part's −z with the face's outward z).
let j = run(
s,
- "JointAssembleCmdNew",
+ "joint.create",
json!({"type": "rigid", "a": {"occurrence": h, "circle": a}, "b": {"occurrence": occ, "point": [0, 0, 0], "z": [0, 0, -1]}, "name": format!("{name} seat")}),
)?;
out["joint"] = j["joint"].clone();
@@ -500,8 +500,8 @@ mod tests {
#[test]
fn a_screw_seats_in_a_hole_and_resizes() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 10, "body_name": "Plate"}));
- run(&mut s, "FusionHoleCommand", json!({"position": [20, 15, 10], "diameter": 6.4}));
+ run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 10, "body_name": "Plate"}));
+ run(&mut s, "solid.hole", json!({"position": [20, 15, 10], "diameter": 6.4}));
// Pick the hole's rim at the top face.
let r = run(&mut s, "parts.insert", json!({"family": "socket_head_cap_screw", "size": "M6", "length": 20, "at": [23.2, 15, 10]}));
assert!(r["joint"].is_number(), "{r}");
diff --git a/crates/engine/src/cmd/patterns_tests.rs b/crates/engine/src/cmd/patterns_tests.rs
index 91d05573..09eb681e 100644
--- a/crates/engine/src/cmd/patterns_tests.rs
+++ b/crates/engine/src/cmd/patterns_tests.rs
@@ -12,7 +12,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value {
}
fn volume(s: &mut Session) -> f64 {
- run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
+ run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
}
fn rel(a: f64, b: f64) -> f64 {
@@ -22,12 +22,12 @@ fn rel(a: f64, b: f64) -> f64 {
/// A 100 x 50 x 5 plate with a hole at the start of a straight path (10,10) → (90,10).
fn plate_with_hole() -> Session {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 100, "width": 50, "height": 5, "corner": [0, 0, 0]}));
- run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Path"}));
- run(&mut s, "DrawPolyline", json!({"points": [[10, 10], [90, 10]], "ids": ["l1"]}));
- run(&mut s, "CircleCenterRadius", json!({"center": [10, 10], "radius": 2, "id": "c1"}));
- run(&mut s, "SketchStop", json!({}));
- run(&mut s, "Extrude", json!({"sketch": "Path", "through_all": true, "operation": "cut", "name": "Hole1"}));
+ run(&mut s, "solid.box", json!({"length": 100, "width": 50, "height": 5, "corner": [0, 0, 0]}));
+ run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Path"}));
+ run(&mut s, "sketch.line", json!({"points": [[10, 10], [90, 10]], "ids": ["l1"]}));
+ run(&mut s, "sketch.circle.center", json!({"center": [10, 10], "radius": 2, "id": "c1"}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "solid.extrude", json!({"sketch": "Path", "through_all": true, "operation": "cut", "name": "Hole1"}));
s
}
@@ -38,25 +38,25 @@ const HOLE: f64 = PI * 4.0 * 5.0;
fn pattern_on_path_spacing_extent_and_flip() {
let mut s = plate_with_hole();
assert!(rel(volume(&mut s), PLATE - HOLE) < 1e-4);
- run(&mut s, "PatternOnPath", json!({"features": ["Hole1"], "path_sketch": "Path", "path": ["l1"], "count": 5, "spacing": 20}));
+ run(&mut s, "solid.pattern.path", json!({"features": ["Hole1"], "path_sketch": "Path", "path": ["l1"], "count": 5, "spacing": 20}));
assert!(rel(volume(&mut s), PLATE - 5.0 * HOLE) < 1e-4);
// The count is a parameter like any other input.
let n = s.doc.features.last().and_then(|f| f.param_names.first().cloned()).unwrap_or_default();
- run(&mut s, "ChangeParameterCommand", json!({"name": n, "expression": "3"}));
+ run(&mut s, "parameters.change", json!({"name": n, "expression": "3"}));
assert!(rel(volume(&mut s), PLATE - 3.0 * HOLE) < 1e-4);
// Instances past the end of the path are an error.
- run(&mut s, "ChangeParameterCommand", json!({"name": n, "expression": "6"}));
+ run(&mut s, "parameters.change", json!({"name": n, "expression": "6"}));
let err = s.model.results.last().and_then(|r| r.error.clone()).unwrap_or_default();
assert!(err.contains("past the end of the path"), "{err}");
// Distance = first to last instance.
let mut s = plate_with_hole();
- run(&mut s, "PatternOnPath", json!({"features": ["Hole1"], "path_sketch": "Path", "path": ["l1"], "count": 3, "distance": 80}));
+ run(&mut s, "solid.pattern.path", json!({"features": ["Hole1"], "path_sketch": "Path", "path": ["l1"], "count": 3, "distance": 80}));
assert!(rel(volume(&mut s), PLATE - 3.0 * HOLE) < 1e-4);
// Flipped, the path runs from (90,10) back toward (10,10).
let mut s = plate_with_hole();
- run(&mut s, "PatternOnPath", json!({"features": ["Hole1"], "path_sketch": "Path", "path": ["l1"], "count": 2, "spacing": 30, "flip": true}));
+ run(&mut s, "solid.pattern.path", json!({"features": ["Hole1"], "path_sketch": "Path", "path": ["l1"], "count": 2, "spacing": 30, "flip": true}));
// The copy moves by path(30) − path(0) = (60,10) − (90,10) = −30: off the plate side → only 1 hole.
assert!(rel(volume(&mut s), PLATE - HOLE) < 1e-4);
}
@@ -64,47 +64,47 @@ fn pattern_on_path_spacing_extent_and_flip() {
#[test]
fn patterns_of_bodies() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Cube"}));
- run(&mut s, "PatternCircular", json!({"bodies": ["Cube"], "axis": "Z", "count": 4}));
- let m = run(&mut s, "MeasureCommand", json!({}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Cube"}));
+ run(&mut s, "solid.pattern.circular", json!({"bodies": ["Cube"], "axis": "Z", "count": 4}));
+ let m = run(&mut s, "inspect.measure", json!({}));
assert_eq!(m["body_count"], 4, "{m}");
assert!(rel(volume(&mut s), 4000.0) < 1e-4);
- run(&mut s, "PatternRectangular", json!({"bodies": ["Cube"], "dir1": [0, 0, 1], "count1": 3, "spacing1": 20}));
+ run(&mut s, "solid.pattern.rectangular", json!({"bodies": ["Cube"], "dir1": [0, 0, 1], "count1": 3, "spacing1": 20}));
assert!(rel(volume(&mut s), 6000.0) < 1e-4);
- assert!(s.execute("PatternRectangular", &json!({"bodies": ["Nope"], "dir1": [1, 0, 0], "count1": 2, "spacing1": 5})).is_err());
+ assert!(s.execute("solid.pattern.rectangular", &json!({"bodies": ["Nope"], "dir1": [1, 0, 0], "count1": 2, "spacing1": 5})).is_err());
}
#[test]
fn patterns_of_components_place_occurrences() {
let mut s = Session::default();
- run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Peg"}));
- run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]}));
+ run(&mut s, "component.create", json!({"name": "Peg"}));
+ run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]}));
run(&mut s, "component.activate", json!({"component": "root"}));
- let r = run(&mut s, "PatternRectangular", json!({"components": ["Peg"], "dir1": [0, 0, 1], "count1": 3, "spacing1": 20}));
+ let r = run(&mut s, "solid.pattern.rectangular", json!({"components": ["Peg"], "dir1": [0, 0, 1], "count1": 3, "spacing1": 20}));
assert_eq!(r["occurrences"].as_array().map(Vec::len), Some(2), "{r}");
- let m = run(&mut s, "MeasureCommand", json!({}));
+ let m = run(&mut s, "inspect.measure", json!({}));
assert_eq!(m["body_count"], 3, "{m}");
let zs: Vec = m["bodies"].as_array().unwrap().iter().filter_map(|b| b["bbox"]["min"][2].as_f64()).collect();
for z in [0.0, 20.0, 40.0] {
assert!(zs.iter().any(|x| (x - z).abs() < 1e-6), "{zs:?}");
}
- assert!(s.execute("PatternCircular", &json!({"components": ["Nope"], "axis": "Z", "count": 2})).is_err());
+ assert!(s.execute("solid.pattern.circular", &json!({"components": ["Nope"], "axis": "Z", "count": 2})).is_err());
}
#[test]
fn path_pattern_orientation_follows_the_path() {
let width_of_copy = |orient: &str| {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 2, "width": 4, "height": 2, "corner": [19, -2, 0], "body_name": "Tab"}));
- run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Arc"}));
- run(&mut s, "ArcCenterTwoPoint", json!({"center": [0, 0], "start": [20, 0], "sweep": 90, "id": "a1"}));
- run(&mut s, "SketchStop", json!({}));
+ run(&mut s, "solid.box", json!({"length": 2, "width": 4, "height": 2, "corner": [19, -2, 0], "body_name": "Tab"}));
+ run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Arc"}));
+ run(&mut s, "sketch.arc.center_point", json!({"center": [0, 0], "start": [20, 0], "sweep": 90, "id": "a1"}));
+ run(&mut s, "sketch.finish", json!({}));
run(
&mut s,
- "PatternOnPath",
+ "solid.pattern.path",
json!({"bodies": ["Tab"], "path_sketch": "Arc", "path": ["a1"], "count": 2, "distance": "pi * 10", "orientation": orient}),
);
- let m = run(&mut s, "MeasureCommand", json!({}));
+ let m = run(&mut s, "inspect.measure", json!({}));
let b = m["bodies"].as_array().unwrap().iter().find(|b| b["name"] != "Tab").unwrap().clone();
let (lo, hi) = (b["bbox"]["min"].clone(), b["bbox"]["max"].clone());
// The copy sits at the arc's end (0, 20).
@@ -121,22 +121,24 @@ fn path_pattern_orientation_follows_the_path() {
fn patterns_of_a_blind_hole_drill_every_copy() {
let one = {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 50, "width": 25, "height": 15}));
+ run(&mut s, "solid.box", json!({"length": 50, "width": 25, "height": 15}));
let v0 = volume(&mut s);
- run(&mut s, "FusionHoleCommand", json!({"position": [10, 12, 15], "diameter": "5 mm", "depth": "10 mm"}));
+ run(&mut s, "solid.hole", json!({"position": [10, 12, 15], "diameter": "5 mm", "depth": "10 mm"}));
v0 - volume(&mut s)
};
let patterned = |p: (&str, Value)| {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 50, "width": 25, "height": 15}));
+ run(&mut s, "solid.box", json!({"length": 50, "width": 25, "height": 15}));
let v0 = volume(&mut s);
- run(&mut s, "FusionHoleCommand", json!({"position": [10, 12, 15], "diameter": "5 mm", "depth": "10 mm"}));
+ run(&mut s, "solid.hole", json!({"position": [10, 12, 15], "diameter": "5 mm", "depth": "10 mm"}));
run(&mut s, p.0, p.1);
v0 - volume(&mut s)
};
- let r = patterned(("PatternRectangular", json!({"features": ["Hole1"], "dir1": [1, 0, 0], "count1": 3, "spacing1": "15 mm"})));
+ let r = patterned(("solid.pattern.rectangular", json!({"features": ["Hole1"], "dir1": [1, 0, 0], "count1": 3, "spacing1": "15 mm"})));
assert!((r - 3.0 * one).abs() < 1e-3 * one, "{r} vs 3 × {one}");
- let c =
- patterned(("PatternCircular", json!({"features": ["Hole1"], "axis": {"origin": [25, 12, 0], "dir": [0, 0, 1]}, "count": 2, "angle": 360})));
+ let c = patterned((
+ "solid.pattern.circular",
+ json!({"features": ["Hole1"], "axis": {"origin": [25, 12, 0], "dir": [0, 0, 1]}, "count": 2, "angle": 360}),
+ ));
assert!((c - 2.0 * one).abs() < 1e-3 * one, "{c} vs 2 × {one}");
}
diff --git a/crates/engine/src/cmd/plastic.rs b/crates/engine/src/cmd/plastic.rs
index c01c7bba..a62a09a1 100644
--- a/crates/engine/src/cmd/plastic.rs
+++ b/crates/engine/src/cmd/plastic.rs
@@ -12,26 +12,26 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("FusionBossCommand", "Boss", boss).at("PLASTIC", "CREATE").icon("boss").params(
+ CommandSpec::new("plastic.boss", "Boss", boss).at("PLASTIC", "CREATE").icon("boss").params(
"position: [x,y,z] on a face; direction? (default: out of the face); diameter, height; hole_diameter?, hole_depth? (default: to the face); \
draft? (angle), fillet? (root radius); ribs? (count), rib_thickness?, rib_length? (out from the wall), rib_offset? (below the top); body?",
),
- CommandSpec::new("FusionLipCommand", "Lip", lip).at("PLASTIC", "CREATE").icon("lip").params(
+ CommandSpec::new("plastic.lip", "Lip", lip).at("PLASTIC", "CREATE").icon("lip").params(
"face: [x,y,z] on the rim face of a shelled body; width, height; type?: lip|groove; gap? (groove clearance); side?: inside|outside (which rim edge); body?",
),
- CommandSpec::new("FusionSnapFitCommand", "Snap Fit", snap_fit).at("PLASTIC", "CREATE").icon("snap").params(
+ CommandSpec::new("plastic.snap_fit", "Snap Fit", snap_fit).at("PLASTIC", "CREATE").icon("snap").params(
"position: [x,y,z] on a face; direction? (default: out of the face); hook: [x,y,z] (the catch's direction); length, thickness, width, catch_depth, catch_length; body?",
),
- CommandSpec::new("FusionRestCommand", "Rest", rest).at("PLASTIC", "CREATE").icon("rest").params(
+ CommandSpec::new("plastic.rest", "Rest", rest).at("PLASTIC", "CREATE").icon("rest").params(
"position: [x,y,z] on a face (the rest's centre); direction? (default: out of the face); width (round: the diameter); length? (a rectangle, along `along`); along?: [x,y,z]; \
height; draft? (angle; default: the body's plastic rule); thickness? (wall of a hollow rest); body?",
),
- CommandSpec::new("FusionManagePlasticRuleCommand", "Manage Plastic Rules", manage_rules).at("PLASTIC", "SETUP").icon("plastic_rule").params(
+ CommandSpec::new("plastic.manage_rules", "Manage Plastic Rules", manage_rules).at("PLASTIC", "SETUP").icon("plastic_rule").params(
"name (library or new rule; a new one starts from `from`, default ABS (1.5mm)); from?; material?; thickness?, nominal_radius?, clearance?, knife_edge?, \
reveal_height?, thickness_variation?, draft?, max_thickness?, min_thickness?, min_draft? (expressions; `Thickness` is the rule's); active?: bool (rule for bodies without one); \
delete?: bool (a design rule; a library rule goes back to the library values)",
),
- CommandSpec::new("FusionAssignPlasticRuleCommand", "Assign Plastic Rule", assign_rule).at("PLASTIC", "SETUP").icon("plastic_assign").params(
+ CommandSpec::new("plastic.assign_rule", "Assign Plastic Rule", assign_rule).at("PLASTIC", "SETUP").icon("plastic_assign").params(
"bodies: [names]; rule: name (empty: no rule) — plastic features on these bodies take its draft and clearance as defaults",
),
CommandSpec::new("plastic.rules", "List Plastic Rules", list_rules).noundo().params("→ rules with their expressions and values, the active rule and the rules assigned to bodies"),
@@ -53,7 +53,7 @@ fn rule_json(s: &Session, r: &PlasticRule) -> Value {
}
fn manage_rules(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionManagePlasticRuleCommand";
+ let cmd = "plastic.manage_rules";
let name = str_(p, "name").map(str::trim).filter(|n| !n.is_empty() && n.len() <= 128).ok_or_else(|| bad(cmd, "`name` is required"))?.to_string();
if bool_(p, "delete").unwrap_or(false) {
let doc = s.doc_mut();
@@ -116,7 +116,7 @@ fn manage_rules(s: &mut Session, p: &Value) -> Result {
}
fn assign_rule(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionAssignPlasticRuleCommand";
+ let cmd = "plastic.assign_rule";
let bodies = string_list(p, "bodies");
if bodies.is_empty() || bodies.len() > 10_000 {
return Err(bad(cmd, "`bodies` must list bodies"));
@@ -147,7 +147,7 @@ fn list_rules(s: &mut Session, _p: &Value) -> Result {
}
fn rest(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionRestCommand";
+ let cmd = "plastic.rest";
let (position, direction) = placed(s, p, cmd)?;
let along = match p.get("along") {
Some(v) => Some(
@@ -199,7 +199,7 @@ fn placed(s: &Session, p: &Value, cmd: &str) -> Result<(Vec3, Vec3)> {
}
fn boss(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionBossCommand";
+ let cmd = "plastic.boss";
let (position, direction) = placed(s, p, cmd)?;
let kind = FeatureKind::Boss {
position,
@@ -220,7 +220,7 @@ fn boss(s: &mut Session, p: &Value) -> Result {
}
fn lip(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionLipCommand";
+ let cmd = "plastic.lip";
let face = p.get("face").and_then(vec3).ok_or_else(|| bad(cmd, "`face` must be a point [x, y, z] on the rim face"))?;
let groove = match str_(p, "type").map(str::to_ascii_lowercase).as_deref() {
None | Some("lip") => false,
@@ -245,7 +245,7 @@ fn lip(s: &mut Session, p: &Value) -> Result {
}
fn snap_fit(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionSnapFitCommand";
+ let cmd = "plastic.snap_fit";
let (position, direction) = placed(s, p, cmd)?;
let hook = p.get("hook").and_then(vec3).and_then(|h| h.normalized()).ok_or_else(|| bad(cmd, "`hook` must be the catch's direction [x, y, z]"))?;
if hook.cross(direction).len() < 1e-6 {
@@ -281,7 +281,7 @@ mod tests {
}
fn volume(s: &mut Session) -> f64 {
- run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
+ run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
}
fn rel(a: f64, b: f64) -> f64 {
@@ -291,17 +291,17 @@ mod tests {
#[test]
fn boss_with_hole_fillet_and_ribs() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 60, "width": 60, "height": 3, "body_name": "Plate"}));
- run(&mut s, "FusionBossCommand", json!({"position": [30, 30, 3], "diameter": 10, "height": 12, "hole_diameter": 4}));
+ run(&mut s, "solid.box", json!({"length": 60, "width": 60, "height": 3, "body_name": "Plate"}));
+ run(&mut s, "plastic.boss", json!({"position": [30, 30, 3], "diameter": 10, "height": 12, "hole_diameter": 4}));
let want = 10800.0 + PI * 25.0 * 12.0 - PI * 4.0 * 12.0;
assert!(rel(volume(&mut s), want) < 1e-3, "{} {want}", volume(&mut s));
- assert_eq!(run(&mut s, "MeasureCommand", json!({}))["body_count"], 1);
+ assert_eq!(run(&mut s, "inspect.measure", json!({}))["body_count"], 1);
// Ribs add t·l·h each.
let mut t = Session::default();
- run(&mut t, "PrimitiveBox", json!({"length": 60, "width": 60, "height": 3}));
+ run(&mut t, "solid.box", json!({"length": 60, "width": 60, "height": 3}));
run(
&mut t,
- "FusionBossCommand",
+ "plastic.boss",
json!({"position": [30, 30, 3], "diameter": 10, "height": 12, "ribs": 4, "rib_thickness": 1.5, "rib_length": 6, "rib_offset": 2}),
);
let ribs = 4.0 * 1.5 * 6.0 * 10.0;
@@ -309,52 +309,52 @@ mod tests {
let v = volume(&mut t) - 10800.0 - PI * 25.0 * 12.0;
assert!(v > ribs && v < ribs * 1.05, "{v} {ribs}");
let mut u = Session::default();
- run(&mut u, "PrimitiveBox", json!({"length": 60, "width": 60, "height": 3}));
- run(&mut u, "FusionBossCommand", json!({"position": [30, 30, 3], "diameter": 10, "height": 12, "fillet": 2, "draft": 1}));
+ run(&mut u, "solid.box", json!({"length": 60, "width": 60, "height": 3}));
+ run(&mut u, "plastic.boss", json!({"position": [30, 30, 3], "diameter": 10, "height": 12, "fillet": 2, "draft": 1}));
assert!(volume(&mut u) > 10800.0 + PI * 25.0 * 10.0);
- assert!(u.execute("FusionBossCommand", &json!({"position": [30, 30, 3], "diameter": 10, "height": 12, "hole_diameter": 20})).is_err());
+ assert!(u.execute("plastic.boss", &json!({"position": [30, 30, 3], "diameter": 10, "height": 12, "hole_diameter": 20})).is_err());
}
#[test]
fn lip_and_groove_on_a_shelled_box() {
let shelled = || {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 80, "width": 50, "height": 30}));
- run(&mut s, "FusionShellBodyCommand", json!({"faces": [[40, 25, 30]], "thickness": 2}));
+ run(&mut s, "solid.box", json!({"length": 80, "width": 50, "height": 30}));
+ run(&mut s, "solid.shell", json!({"faces": [[40, 25, 30]], "thickness": 2}));
s
};
let mut s = shelled();
let v0 = volume(&mut s);
- run(&mut s, "FusionLipCommand", json!({"face": [1, 25, 30], "width": 1, "height": 2}));
+ run(&mut s, "plastic.lip", json!({"face": [1, 25, 30], "width": 1, "height": 2}));
// Inside band 1 wide along the inner edge (76 x 46): (76+2)(46+2) − 76·46 = 248, 2 high.
assert!(rel(volume(&mut s) - v0, 248.0 * 2.0) < 1e-6, "{}", volume(&mut s) - v0);
let mut g = shelled();
- run(&mut g, "FusionLipCommand", json!({"face": [1, 25, 30], "width": 1, "height": 2, "type": "groove", "side": "outside"}));
+ run(&mut g, "plastic.lip", json!({"face": [1, 25, 30], "width": 1, "height": 2, "type": "groove", "side": "outside"}));
// Outside band along the outer edge (80 x 50): 80·50 − 78·48 = 256, 2 deep.
assert!(rel(v0 - volume(&mut g), 256.0 * 2.0) < 1e-6, "{}", v0 - volume(&mut g));
- assert!(g.execute("FusionLipCommand", &json!({"face": [40, 25, 0], "width": 1, "height": 2})).is_err(), "a face without an inner edge");
+ assert!(g.execute("plastic.lip", &json!({"face": [40, 25, 0], "width": 1, "height": 2})).is_err(), "a face without an inner edge");
}
#[test]
fn snap_fit_arm_and_catch() {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 40, "height": 4}));
+ run(&mut s, "solid.box", json!({"length": 40, "width": 40, "height": 4}));
let v0 = volume(&mut s);
run(
&mut s,
- "FusionSnapFitCommand",
+ "plastic.snap_fit",
json!({"position": [20, 20, 4], "hook": [1, 0, 0], "length": 15, "thickness": 1.5, "width": 6, "catch_depth": 1, "catch_length": 3}),
);
let want = 1.5 * 15.0 * 6.0 + 0.5 * 1.0 * 3.0 * 6.0;
assert!(rel(volume(&mut s) - v0, want) < 1e-6, "{}", volume(&mut s) - v0);
- let m = run(&mut s, "MeasureCommand", json!({}));
+ let m = run(&mut s, "inspect.measure", json!({}));
assert!((m["bodies"][0]["bbox"]["max"][0].as_f64().unwrap_or(0.0) - 40.0).abs() < 1e-9);
- assert!(s.execute("FusionSnapFitCommand", &json!({"position": [5, 5, 4], "direction": [0, 0, 1], "hook": [0, 0, 1], "length": 15, "thickness": 1.5, "width": 6, "catch_depth": 1, "catch_length": 3})).is_err());
+ assert!(s.execute("plastic.snap_fit", &json!({"position": [5, 5, 4], "direction": [0, 0, 1], "hook": [0, 0, 1], "length": 15, "thickness": 1.5, "width": 6, "catch_depth": 1, "catch_length": 3})).is_err());
}
fn plate() -> (Session, f64) {
let mut s = Session::default();
- run(&mut s, "PrimitiveBox", json!({"length": 60, "width": 60, "height": 3}));
+ run(&mut s, "solid.box", json!({"length": 60, "width": 60, "height": 3}));
let v0 = volume(&mut s);
(s, v0)
}
@@ -363,27 +363,27 @@ mod tests {
fn rest_pads() {
// Rectangle 20 x 10, 4 high: 800.
let (mut s, v0) = plate();
- run(&mut s, "FusionRestCommand", json!({"position": [30, 30, 3], "width": 10, "length": 20, "along": [1, 0, 0], "height": 4}));
+ run(&mut s, "plastic.rest", json!({"position": [30, 30, 3], "width": 10, "length": 20, "along": [1, 0, 0], "height": 4}));
assert!(rel(volume(&mut s) - v0, 800.0) < 1e-6, "{}", volume(&mut s) - v0);
- let m = run(&mut s, "MeasureCommand", json!({}));
+ let m = run(&mut s, "inspect.measure", json!({}));
assert!((m["bodies"][0]["bbox"]["max"][2].as_f64().unwrap_or(0.0) - 7.0).abs() < 1e-9);
// Round, diameter 10: π·25·4.
let (mut r, v0) = plate();
- run(&mut r, "FusionRestCommand", json!({"position": [30, 30, 3], "width": 10, "height": 4}));
+ run(&mut r, "plastic.rest", json!({"position": [30, 30, 3], "width": 10, "height": 4}));
assert!(rel(volume(&mut r) - v0, PI * 25.0 * 4.0) < 1e-3, "{}", volume(&mut r) - v0);
// Hollow, wall 1: (20·10 − 18·8)·4 = 224.
let (mut h, v0) = plate();
- run(&mut h, "FusionRestCommand", json!({"position": [30, 30, 3], "width": 10, "length": 20, "height": 4, "thickness": 1}));
+ run(&mut h, "plastic.rest", json!({"position": [30, 30, 3], "width": 10, "length": 20, "height": 4, "thickness": 1}));
assert!(rel(volume(&mut h) - v0, 224.0) < 1e-6, "{}", volume(&mut h) - v0);
// Drafted 2°: the section shrinks by 2·s·tan 2° each way at height s.
let (mut d, v0) = plate();
- run(&mut d, "FusionRestCommand", json!({"position": [30, 30, 3], "width": 10, "length": 20, "height": 4, "draft": "2 deg"}));
+ run(&mut d, "plastic.rest", json!({"position": [30, 30, 3], "width": 10, "length": 20, "height": 4, "draft": "2 deg"}));
let a = 2f64.to_radians().tan();
let want = 800.0 - a * 30.0 * 16.0 + 4.0 / 3.0 * a * a * 64.0;
assert!(rel(volume(&mut d) - v0, want) < 1e-6, "{} {want}", volume(&mut d) - v0);
// A wall thicker than half the width, or a draft that closes the top, is refused.
- assert!(d.execute("FusionRestCommand", &json!({"position": [10, 10, 3], "width": 4, "height": 4, "thickness": 2})).is_err());
- assert!(d.execute("FusionRestCommand", &json!({"position": [10, 10, 3], "width": 4, "height": 40, "draft": 10})).is_err());
+ assert!(d.execute("plastic.rest", &json!({"position": [10, 10, 3], "width": 4, "height": 4, "thickness": 2})).is_err());
+ assert!(d.execute("plastic.rest", &json!({"position": [10, 10, 3], "width": 4, "height": 40, "draft": 10})).is_err());
}
#[test]
@@ -395,31 +395,31 @@ mod tests {
assert_eq!(list["rules"][0]["values"]["nominal_radius"], 0.75);
assert_eq!(list["rules"][3]["values"]["thickness"].as_f64().map(|t| (t - 2.54).abs() < 1e-9), Some(true));
// A new rule from ABS: Thickness-based values follow its thickness.
- let r = run(&mut s, "FusionManagePlasticRuleCommand", json!({"name": "Thick ABS", "thickness": 3, "draft": "1.5 deg"}));
+ let r = run(&mut s, "plastic.manage_rules", json!({"name": "Thick ABS", "thickness": 3, "draft": "1.5 deg"}));
assert_eq!(r["rule"]["values"]["nominal_radius"], 1.5);
- assert!(s.execute("FusionManagePlasticRuleCommand", &json!({"name": "Bad", "draft": "3 mm"})).is_err());
+ assert!(s.execute("plastic.manage_rules", &json!({"name": "Bad", "draft": "3 mm"})).is_err());
// Assigned to the plate: a boss without a draft takes the rule's 1.5°.
- let body = run(&mut s, "MeasureCommand", json!({}))["bodies"][0]["name"].as_str().unwrap_or_default().to_string();
- run(&mut s, "FusionAssignPlasticRuleCommand", json!({"bodies": [body], "rule": "Thick ABS"}));
- run(&mut s, "FusionBossCommand", json!({"position": [30, 30, 3], "diameter": 10, "height": 12}));
+ let body = run(&mut s, "inspect.measure", json!({}))["bodies"][0]["name"].as_str().unwrap_or_default().to_string();
+ run(&mut s, "plastic.assign_rule", json!({"bodies": [body], "rule": "Thick ABS"}));
+ run(&mut s, "plastic.boss", json!({"position": [30, 30, 3], "diameter": 10, "height": 12}));
let rt = 5.0 - 12.0 * 1.5f64.to_radians().tan();
let want = PI * 12.0 / 3.0 * (25.0 + 5.0 * rt + rt * rt);
assert!(rel(volume(&mut s) - v0, want) < 1e-3, "{} {want}", volume(&mut s) - v0);
// Editing the rule rebuilds the boss.
- run(&mut s, "FusionManagePlasticRuleCommand", json!({"name": "Thick ABS", "draft": "0 deg"}));
+ run(&mut s, "plastic.manage_rules", json!({"name": "Thick ABS", "draft": "0 deg"}));
assert!(rel(volume(&mut s) - v0, PI * 25.0 * 12.0) < 1e-3, "{}", volume(&mut s) - v0);
- assert!(s.execute("FusionAssignPlasticRuleCommand", &json!({"bodies": [body], "rule": "Nope"})).is_err());
- assert!(s.execute("FusionAssignPlasticRuleCommand", &json!({"bodies": ["Nope"], "rule": "Thick ABS"})).is_err());
+ assert!(s.execute("plastic.assign_rule", &json!({"bodies": [body], "rule": "Nope"})).is_err());
+ assert!(s.execute("plastic.assign_rule", &json!({"bodies": ["Nope"], "rule": "Thick ABS"})).is_err());
}
#[test]
fn groove_takes_the_rule_clearance() {
let mut g = Session::default();
- run(&mut g, "PrimitiveBox", json!({"length": 80, "width": 50, "height": 30}));
- run(&mut g, "FusionShellBodyCommand", json!({"faces": [[40, 25, 30]], "thickness": 2}));
+ run(&mut g, "solid.box", json!({"length": 80, "width": 50, "height": 30}));
+ run(&mut g, "solid.shell", json!({"faces": [[40, 25, 30]], "thickness": 2}));
let v0 = volume(&mut g);
- run(&mut g, "FusionManagePlasticRuleCommand", json!({"name": "ABS (1.5mm)", "active": true}));
- run(&mut g, "FusionLipCommand", json!({"face": [1, 25, 30], "width": 1, "height": 2, "type": "groove", "side": "outside"}));
+ run(&mut g, "plastic.manage_rules", json!({"name": "ABS (1.5mm)", "active": true}));
+ run(&mut g, "plastic.lip", json!({"face": [1, 25, 30], "width": 1, "height": 2, "type": "groove", "side": "outside"}));
// Width and depth grow by the 0.1 clearance: (80·50 − 77.8·47.8)·2.1.
let want = (4000.0 - 77.8 * 47.8) * 2.1;
assert!(rel(v0 - volume(&mut g), want) < 1e-6, "{} {want}", v0 - volume(&mut g));
diff --git a/crates/engine/src/cmd/section.rs b/crates/engine/src/cmd/section.rs
index 2623531c..cad866b5 100644
--- a/crates/engine/src/cmd/section.rs
+++ b/crates/engine/src/cmd/section.rs
@@ -8,14 +8,14 @@ use super::CommandSpec;
use crate::params::{bad, bool_, expr, vec3};
use crate::{Result, Session};
-pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new("FusionHalfSectionViewCommand", "Section Analysis", section)
+pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new("inspect.section", "Section Analysis", section)
.at("SOLID", "INSPECT")
.icon("section")
.noundo()
.params("plane: XY|XZ|YZ | {origin, normal}; offset?: expr along the normal; flip?: bool; or clear: true")];
fn section(s: &mut Session, p: &Value) -> Result {
- let cmd = "FusionHalfSectionViewCommand";
+ let cmd = "inspect.section";
if bool_(p, "clear").unwrap_or(false) {
s.section = None;
s.revision += 1;
diff --git a/crates/engine/src/cmd/sheet.rs b/crates/engine/src/cmd/sheet.rs
index 9f2807c5..bbaf5333 100644
--- a/crates/engine/src/cmd/sheet.rs
+++ b/crates/engine/src/cmd/sheet.rs
@@ -13,31 +13,31 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
use crate::{EngineError, Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
- CommandSpec::new("FusionSheetMetalFlangeCommand", "Flange", flange).at("SHEET METAL", "CREATE").icon("flange").params(
+ CommandSpec::new("sheet.flange", "Flange", flange).at("SHEET METAL", "CREATE").icon("flange").params(
"base: sketch + profiles? (rule?, flip?) | contour: sketch + curves (open chain of lines) + distance (rule?, flip?, reverse?) | \
edge: edges: [[x,y,z] on top/bottom edges] + height (to the outer face), angle? (default 90 deg), radius? (default: rule), \
position?: inside|outside|middle, flip?, body?; type?: base|edge|contour (default from the inputs)",
),
- CommandSpec::new("FusionSheetMetalHemFlangeCommand", "Hem", hem)
+ CommandSpec::new("sheet.hem", "Hem", hem)
.at("SHEET METAL", "CREATE")
.icon("hem")
.params("edges: [[x,y,z] on sheet edges]; length (to the outer face); gap? (inner radius, default: rule); flip?; body?"),
- CommandSpec::new("SheetMetalFoldCmd", "Fold", fold).at("SHEET METAL", "CREATE").icon("fold").params(
+ CommandSpec::new("sheet.fold", "Fold", fold).at("SHEET METAL", "CREATE").icon("fold").params(
"line: sketch + curve (a sketch line on the sheet's base face; followed when the sketch changes) or points: [[x,y,z], [x,y,z]]; \
angle? (default 90 deg); radius? (default: rule); position?: centerline|start|end|mould (where the line sits in the bend, default centerline); \
fixed?: [x,y,z] on the side that stays (default: the larger side); flip? (fold the other way); body?",
),
- CommandSpec::new("FusionSheetmetalUnfoldCommand", "Unfold", unfold).at("SHEET METAL", "MODIFY").icon("unfold").params("body? (default: the last sheet body) — all bends"),
+ CommandSpec::new("sheet.unfold", "Unfold", unfold).at("SHEET METAL", "MODIFY").icon("unfold").params("body? (default: the last sheet body) — all bends"),
CommandSpec::new("sheet.refold", "Refold", refold).at("SHEET METAL", "MODIFY").icon("refold").params("body? (default: the last unfolded sheet)"),
- CommandSpec::new("ConvertToSheetMetalCmd", "Convert to Sheet Metal", convert)
+ CommandSpec::new("sheet.convert", "Convert to Sheet Metal", convert)
.at("SHEET METAL", "CREATE")
.icon("convert_sheet")
.params("body; face: [x,y,z] on its large flat face (a plate of even thickness); rule?"),
- CommandSpec::new("FusionSheetMetalRulesCommand", "Sheet Metal Rules", rules).at("SHEET METAL", "MODIFY").icon("sheet_rules").params(
+ CommandSpec::new("sheet.manage_rules", "Sheet Metal Rules", rules).at("SHEET METAL", "MODIFY").icon("sheet_rules").params(
"name (new or existing); thickness?, k_factor?, bend_radius?, relief_width?, relief_depth?, corner_relief?, hem_gap?, gap? (expressions; `Thickness` is the rule's); active?: bool; delete?: bool",
),
CommandSpec::new("sheet.rules", "List Sheet Metal Rules", list_rules).noundo().params("→ rules with their expressions and values, and the active one"),
- CommandSpec::new("FusionSheetMetalFlatPatternCmd", "Create Flat Pattern", flat_pattern)
+ CommandSpec::new("sheet.flat_pattern", "Create Flat Pattern", flat_pattern)
.at("SHEET METAL", "CREATE")
.icon("flat")
.noundo()
@@ -60,13 +60,13 @@ fn points(p: &Value, k: &str, cmd: &str) -> Result> {
fn rule_param(s: &Session, p: &Value, cmd: &str) -> Result