From d10312b7fd009e9c88edb0990db6263722b3e876 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Fri, 9 Oct 2026 12:17:23 +0000 Subject: [PATCH] Command ids are SolveCraft's own: 173 ids that were another product's internal command ids become dotted, lower-case ids (.[.]: solid.extrude, sketch.rectangle.two_point, sheet.flange, joint.drive, ...), in the registry, toolbar, dialogs, shortcuts, palette, control channel, MCP, examples, scenarios and docs The old ids live only in crates/engine/src/legacy_ids.rs (the legacy migration table): find_command maps them, so old scripts, recipes and control calls keep working, and saved shortcuts, recent and pinned commands are migrated on load (designs store no command ids). Parity maps the observed catalog ids to ours through xtask/data/fusion-ids.tsv. The agent rule now reads: our own dotted id, never another product's. Cargo.toml's repository points at github.com/bherbruck/solvecraft. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 7 +- CLAUDE.md | 7 +- Cargo.toml | 2 +- README.md | 8 +- ROADMAP.md | 2 +- apps/solvecraft-cli/src/main.rs | 18 +- apps/solvecraft-cli/src/oracle.rs | 4 +- apps/solvecraft-cli/src/recipe.rs | 104 +-- .../tests/corpus/scripts/components-arm.json | 16 +- .../tests/corpus/scripts/joints-hinge.json | 18 +- .../corpus/scripts/m0-box-fillet-hole.json | 20 +- .../corpus/scripts/m0-revolve-primitives.json | 16 +- .../corpus/scripts/modern-path-pattern.json | 26 +- .../tests/corpus/scripts/modern-plastic.json | 16 +- .../corpus/scripts/modern-sheet-metal.json | 12 +- apps/solvecraft-cli/tests/design_corpus.rs | 4 +- apps/solvecraft/src/main.rs | 2 +- crates/engine/src/cmd/browser.rs | 36 +- crates/engine/src/cmd/clipboard.rs | 24 +- crates/engine/src/cmd/component.rs | 12 +- crates/engine/src/cmd/config.rs | 28 +- crates/engine/src/cmd/delete.rs | 26 +- crates/engine/src/cmd/edit.rs | 20 +- crates/engine/src/cmd/face.rs | 8 +- crates/engine/src/cmd/features.rs | 114 +-- crates/engine/src/cmd/features_more.rs | 28 +- crates/engine/src/cmd/features_more_tests.rs | 86 +-- crates/engine/src/cmd/file.rs | 26 +- crates/engine/src/cmd/inspect.rs | 12 +- crates/engine/src/cmd/joints.rs | 30 +- crates/engine/src/cmd/joints_tests.rs | 64 +- crates/engine/src/cmd/kernel_heal_tests.rs | 16 +- crates/engine/src/cmd/measure_sel.rs | 6 +- crates/engine/src/cmd/mod.rs | 8 +- crates/engine/src/cmd/motion.rs | 16 +- crates/engine/src/cmd/motion_tests.rs | 47 +- crates/engine/src/cmd/parameters.rs | 4 +- crates/engine/src/cmd/parameters_tests.rs | 42 +- crates/engine/src/cmd/parts.rs | 30 +- crates/engine/src/cmd/patterns_tests.rs | 72 +- crates/engine/src/cmd/plastic.rs | 100 +-- crates/engine/src/cmd/section.rs | 4 +- crates/engine/src/cmd/sheet.rs | 28 +- crates/engine/src/cmd/sheet_tests.rs | 142 ++-- crates/engine/src/cmd/sketch.rs | 130 ++-- crates/engine/src/cmd/sketch_canvas.rs | 18 +- crates/engine/src/cmd/sketch_constraints.rs | 24 +- .../src/cmd/sketch_constraints_tests.rs | 85 +-- crates/engine/src/cmd/sketch_create.rs | 56 +- crates/engine/src/cmd/sketch_fuzz_tests.rs | 66 +- crates/engine/src/cmd/sketch_import.rs | 32 +- crates/engine/src/cmd/sketch_inspect.rs | 56 +- crates/engine/src/cmd/sketch_modify.rs | 48 +- crates/engine/src/cmd/sketch_modify_tests.rs | 213 +++--- crates/engine/src/cmd/sketch_perf_tests.rs | 4 +- crates/engine/src/cmd/sketch_project.rs | 32 +- crates/engine/src/cmd/sketch_project_tests.rs | 176 ++--- crates/engine/src/cmd/sketch_rect_tests.rs | 18 +- crates/engine/src/cmd/sketch_ux_tests.rs | 204 ++--- crates/engine/src/cmd/surface.rs | 46 +- crates/engine/src/cmd/surface_analysis.rs | 38 +- crates/engine/src/frames.rs | 58 +- crates/engine/src/frames_tests.rs | 56 +- crates/engine/src/legacy_ids.rs | 236 ++++++ crates/engine/src/lib.rs | 8 +- crates/engine/src/recovery.rs | 6 +- crates/engine/src/sample.rs | 146 ++-- crates/engine/src/tests.rs | 709 +++++++++--------- crates/engine/src/tests/component_frames.rs | 18 +- crates/mcp/src/server.rs | 10 +- crates/mcp/src/tests.rs | 40 +- crates/mcp/src/tools.rs | 28 +- crates/ui-egui/src/browser.rs | 20 +- crates/ui-egui/src/context_menu.rs | 136 ++-- crates/ui-egui/src/dialogs.rs | 245 +++--- crates/ui-egui/src/dialogs_appearance.rs | 16 +- crates/ui-egui/src/dialogs_assembly.rs | 52 +- crates/ui-egui/src/dialogs_assembly_tests.rs | 46 +- crates/ui-egui/src/dialogs_motion.rs | 24 +- crates/ui-egui/src/dialogs_move.rs | 4 +- crates/ui-egui/src/dialogs_parts.rs | 4 +- crates/ui-egui/src/dialogs_plastic.rs | 32 +- crates/ui-egui/src/dialogs_plastic_tests.rs | 22 +- crates/ui-egui/src/dialogs_sheet.rs | 44 +- crates/ui-egui/src/dialogs_sheet_tests.rs | 22 +- crates/ui-egui/src/dim_view.rs | 4 +- crates/ui-egui/src/documents.rs | 8 +- crates/ui-egui/src/drag_snap.rs | 6 +- crates/ui-egui/src/inference.rs | 10 +- crates/ui-egui/src/keymap.rs | 27 +- crates/ui-egui/src/lib.rs | 32 +- crates/ui-egui/src/menu_tests.rs | 36 +- crates/ui-egui/src/params_dialog.rs | 6 +- crates/ui-egui/src/prefs.rs | 2 +- crates/ui-egui/src/preselect_tests.rs | 58 +- crates/ui-egui/src/preview.rs | 28 +- crates/ui-egui/src/scenario.rs | 6 +- crates/ui-egui/src/shortcut_box.rs | 21 +- crates/ui-egui/src/sketch3d.rs | 20 +- crates/ui-egui/src/sketch_dims.rs | 54 +- crates/ui-egui/src/sketch_edit_tests.rs | 2 +- crates/ui-egui/src/sketch_tools.rs | 253 +++---- crates/ui-egui/src/timeline.rs | 8 +- crates/ui-egui/src/toolbar.rs | 20 +- crates/ui-egui/src/tools.rs | 130 ++-- crates/ui-egui/src/viewport.rs | 2 +- crates/ui-egui/src/workspace.rs | 146 ++-- .../scenarios/s01_box_fillet_hole_shell.json | 16 +- .../tests/scenarios/s02_revolve_on_xz.json | 8 +- .../scenarios/s03_sketch_on_face_cut.json | 16 +- .../tests/scenarios/s04_mirror_body.json | 10 +- .../tests/scenarios/s05_delete_and_undo.json | 8 +- .../scenarios/s06_edit_early_feature.json | 14 +- .../scenarios/s07_components_joint_drive.json | 24 +- .../scenarios/s08_sheet_metal_flat_dxf.json | 12 +- .../scenarios/s09_step_roundtrip_hole.json | 10 +- .../scenarios/s10_parameters_drive_model.json | 12 +- .../scenarios/s11_sketch_constraints.json | 12 +- .../tests/scenarios/s12_pattern_bodies.json | 10 +- .../scenarios/s13_plastic_enclosure.json | 16 +- .../scenarios/s14_appearance_export.json | 8 +- .../s15_start_page_sample_save_reopen.json | 2 +- .../scenarios/s16_tabs_separate_undo.json | 16 +- .../scenarios/s17_toolbox_and_visibility.json | 2 +- .../tests/scenarios/s18_3d_sketch_pipe.json | 16 +- .../tests/scenarios/s19_canvas_calibrate.json | 2 +- .../s20_section_with_joint_drive.json | 26 +- .../tests/scenarios/s21_named_views.json | 8 +- .../tests/scenarios/s22_sheet_metal_fold.json | 20 +- .../tests/scenarios/s23_configurations.json | 14 +- .../tests/scenarios/s24_motion_study.json | 26 +- .../scenarios/s25_autosave_recovery.json | 8 +- .../scenarios/s26_drive_joint_typed.json | 26 +- .../tests/scenarios/s27_move_triad.json | 8 +- .../tests/scenarios/s30_pattern_a_hole.json | 12 +- .../s31_sketch_on_face_projects.json | 16 +- .../s32_cut_in_active_component.json | 28 +- .../tests/scenarios/s33_move_x_arrow.json | 8 +- .../s34_component_move_carries_sketches.json | 16 +- .../scenarios/s35_construction_mode.json | 12 +- .../scenarios/s36_sketch_palette_visible.json | 6 +- .../tests/scenarios/s37_browser_chevrons.json | 24 +- .../scenarios/s38_view_anim_no_jump.json | 2 +- .../s40_toolbar_customize_persists.json | 30 +- .../s41_type_value_after_picking.json | 10 +- .../tests/scenarios/s42_drag_snap.json | 8 +- .../sketch_on_moved_component_face.json | 6 +- crates/ui-egui/tests/ui_layout.rs | 8 +- crates/ui-egui/tests/window_widths.rs | 4 +- docs/control-protocol.md | 8 +- docs/mcp.md | 18 +- docs/oracle.md | 8 +- docs/parity.md | 582 +++++++------- docs/step-import.md | 4 +- examples/bracket.json | 24 +- xtask/data/fusion-ids.tsv | 181 +++++ xtask/src/main.rs | 4 +- xtask/src/parity.rs | 29 +- 158 files changed, 3611 insertions(+), 3205 deletions(-) create mode 100644 crates/engine/src/legacy_ids.rs create mode 100644 xtask/data/fusion-ids.tsv 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> { match str_(p, "rule") { Some(r) if s.doc.sheet.rules.iter().any(|x| x.name == r) || r == SheetRule::steel().name => Ok(Some(r.to_string())), - Some(r) => Err(bad(cmd, format!("no sheet metal rule `{r}` (FusionSheetMetalRulesCommand makes one)"))), + Some(r) => Err(bad(cmd, format!("no sheet metal rule `{r}` (sheet.manage_rules makes one)"))), None => Ok(None), } } fn flange(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionSheetMetalFlangeCommand"; + let cmd = "sheet.flange"; let ty = match str_(p, "type") { Some(t) => t.to_ascii_lowercase(), None if p.get("edges").is_some() => "edge".into(), @@ -123,7 +123,7 @@ fn flange(s: &mut Session, p: &Value) -> Result { } fn hem(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionSheetMetalHemFlangeCommand"; + let cmd = "sheet.hem"; let edges = points(p, "edges", cmd)?; let length = req_expr(cmd, p, "length")?; check_expr(s, &length, Kind::Length, cmd, "length")?; @@ -139,7 +139,7 @@ fn hem(s: &mut Session, p: &Value) -> Result { } fn fold(s: &mut Session, p: &Value) -> Result { - let cmd = "SheetMetalFoldCmd"; + let cmd = "sheet.fold"; let (sketch, curve, a, b) = match p.get("points") { Some(v) => { let pts = v.as_array().map(|a| a.iter().filter_map(vec3).collect::>()).unwrap_or_default(); @@ -189,7 +189,7 @@ fn refold(s: &mut Session, p: &Value) -> Result { } fn convert(s: &mut Session, p: &Value) -> Result { - let cmd = "ConvertToSheetMetalCmd"; + let cmd = "sheet.convert"; 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}`"))); @@ -200,7 +200,7 @@ fn convert(s: &mut Session, p: &Value) -> Result { } fn rules(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionSheetMetalRulesCommand"; + let cmd = "sheet.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(); @@ -289,7 +289,7 @@ fn flat_json(sh: &SheetBody) -> Result { } fn flat_pattern(s: &mut Session, p: &Value) -> Result { - let sh = sheet_of(s, p, "FusionSheetMetalFlatPatternCmd")?; + let sh = sheet_of(s, p, "sheet.flat_pattern")?; flat_json(&sh) } diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index b3280b89..c5b367be 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_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 { @@ -20,15 +20,15 @@ fn rel(a: f64, b: f64) -> f64 { } fn flat(s: &mut Session) -> Value { - run(s, "FusionSheetMetalFlatPatternCmd", json!({})) + run(s, "sheet.flat_pattern", json!({})) } /// A w x h base flange on XY. fn base(s: &mut Session, w: f64, h: f64) { - run(s, "SketchCreate", json!({"plane": "XY", "name": "Base"})); - run(s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [w, h]})); - run(s, "SketchStop", json!({})); - run(s, "FusionSheetMetalFlangeCommand", json!({"sketch": "Base"})); + run(s, "sketch.create", json!({"plane": "XY", "name": "Base"})); + run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [w, h]})); + run(s, "sketch.finish", json!({})); + run(s, "sheet.flange", json!({"sketch": "Base"})); } #[test] @@ -36,70 +36,70 @@ fn base_and_edge_flange_follow_the_k_factor() { let mut s = Session::default(); base(&mut s, 100.0, 60.0); assert!(rel(volume(&mut s), 15000.0) < 1e-9); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 20})); + run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 20})); // Oracle 68: flat = 60 − 5 + (π/2)(2.5 + 0.44·2.5) + 20 − 5. let want = 60.0 - 5.0 + FRAC_PI_2 * (2.5 + 0.44 * 2.5) + 15.0; let f = flat(&mut s); assert!((f["flat_size_mm"][1].as_f64().unwrap_or(0.0) - want).abs() < 1e-6, "{f}"); assert_eq!(f["bends"][0]["direction"], "up"); assert!(rel(volume(&mut s), 18972.622) < 1e-4, "{}", volume(&mut s)); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert_eq!(m["total"]["faces"], 14, "{m}"); // The angle is a parameter: 45° (oracle 70 uses height 15). let names = s.doc.features.last().map(|f| f.param_names.clone()).unwrap_or_default(); - run(&mut s, "ChangeParameterCommand", json!({"name": names[0], "expression": "15"})); - run(&mut s, "ChangeParameterCommand", json!({"name": names[1], "expression": "45 deg"})); + run(&mut s, "parameters.change", json!({"name": names[0], "expression": "15"})); + run(&mut s, "parameters.change", json!({"name": names[1], "expression": "45 deg"})); assert!(rel(volume(&mut s), 18450.777) < 1e-4, "{}", volume(&mut s)); assert!((flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(0.0) - 73.685298).abs() < 1e-5); // DXF: outline and an up bend line. let d = run(&mut s, "sheet.export_dxf", json!({})); let text = d["dxf"].as_str().unwrap_or_default(); assert!(text.contains("BEND_UP") && text.contains("OUTLINE"), "{text}"); - assert!(s.execute("FusionSheetMetalFlangeCommand", &json!({"edges": [[50, 30, 40]], "height": 20})).is_err(), "not on an edge"); + assert!(s.execute("sheet.flange", &json!({"edges": [[50, 30, 40]], "height": 20})).is_err(), "not on an edge"); } #[test] fn rules_are_parameters() { let mut s = Session::default(); - run(&mut s, "ChangeParameterCommand", json!({"name": "Thick", "expression": "1.5 mm"})); - run(&mut s, "FusionSheetMetalRulesCommand", json!({"name": "Custom", "thickness": "Thick", "k_factor": 0.33, "bend_radius": "3 mm"})); + run(&mut s, "parameters.change", json!({"name": "Thick", "expression": "1.5 mm"})); + run(&mut s, "sheet.manage_rules", json!({"name": "Custom", "thickness": "Thick", "k_factor": 0.33, "bend_radius": "3 mm"})); base(&mut s, 80.0, 50.0); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[40, 50, 1.5], [40, 0, 1.5]], "height": 20})); + run(&mut s, "sheet.flange", json!({"edges": [[40, 50, 1.5], [40, 0, 1.5]], "height": 20})); // Oracle 71: 50 − 9 + 2·(π/2)(3 + 0.495) + 2·15.5 = 82.980. let want = 50.0 - 2.0 * 4.5 + 2.0 * FRAC_PI_2 * (3.0 + 0.33 * 1.5) + 2.0 * 15.5; assert!((flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(0.0) - want).abs() < 1e-6); assert!(rel(volume(&mut s), 10053.717) < 1e-4, "{}", volume(&mut s)); // Thicker: everything follows. - run(&mut s, "ChangeParameterCommand", json!({"name": "Thick", "expression": "2 mm"})); + run(&mut s, "parameters.change", json!({"name": "Thick", "expression": "2 mm"})); let want = 50.0 - 2.0 * 5.0 + 2.0 * FRAC_PI_2 * (3.0 + 0.33 * 2.0) + 2.0 * 15.0; assert!((flat(&mut s)["flat_size_mm"][1].as_f64().unwrap_or(0.0) - want).abs() < 1e-6); let l = run(&mut s, "sheet.rules", json!({})); assert_eq!(l["active"], "Custom", "{l}"); - assert!(s.execute("FusionSheetMetalRulesCommand", &json!({"name": "Bad", "k_factor": 3})).is_err()); + assert!(s.execute("sheet.manage_rules", &json!({"name": "Bad", "k_factor": 3})).is_err()); } #[test] fn contour_flange_hem_and_tray() { // Oracle 72. let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": {"origin": [0, 0, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, -1]}, "name": "Contour"})); - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [40, 0], [40, -30], [70, -30]], "ids": ["l1", "l2", "l3"]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"sketch": "Contour", "curves": ["l1", "l2", "l3"], "distance": 50})); + run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, -1]}, "name": "Contour"})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0], [40, -30], [70, -30]], "ids": ["l1", "l2", "l3"]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "sheet.flange", json!({"sketch": "Contour", "curves": ["l1", "l2", "l3"], "distance": 50})); assert!(rel(volume(&mut s), 12097.622) < 1e-4, "{}", volume(&mut s)); assert!((flat(&mut s)["flat_size_mm"][0].as_f64().unwrap_or(0.0) - 96.309734).abs() < 1e-5); // Oracle 73: flange, then a flat hem of 8. let mut s = Session::default(); base(&mut s, 100.0, 60.0); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 20})); - run(&mut s, "FusionSheetMetalHemFlangeCommand", json!({"edges": [[50, 0, 20]], "length": 8})); + run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 20})); + run(&mut s, "sheet.hem", json!({"edges": [[50, 0, 20]], "length": 8})); assert!(rel(volume(&mut s), 21334.723) < 1e-4, "{}", volume(&mut s)); let b = flat(&mut s)["bends"].clone(); assert!(b.as_array().is_some_and(|a| a.iter().any(|x| (x["angle_deg"].as_f64().unwrap_or(0.0) - 180.0).abs() < 1e-9)), "{b}"); // A tray: four flanges at once stop at each other's bends. let mut s = Session::default(); base(&mut s, 100.0, 60.0); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 60, 2.5], [100, 30, 2.5], [50, 0, 2.5], [0, 30, 2.5]], "height": 25})); + run(&mut s, "sheet.flange", json!({"edges": [[50, 60, 2.5], [100, 30, 2.5], [50, 0, 2.5], [0, 30, 2.5]], "height": 25})); let f = flat(&mut s); // Oracle 69: 100 − 10 + 2·5.655 + 2·20 = 141.310 across. assert!((f["flat_size_mm"][0].as_f64().unwrap_or(0.0) - (90.0 + 2.0 * FRAC_PI_2 * 3.6 + 40.0)).abs() < 1e-6, "{f}"); @@ -117,15 +117,15 @@ fn unfold_cut_refold_and_convert() { // Oracle 74: two flanges, unfold, a slot across the bend, refold. let mut s = Session::default(); base(&mut s, 80.0, 50.0); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[40, 50, 2.5], [40, 0, 2.5]], "height": 20})); + run(&mut s, "sheet.flange", json!({"edges": [[40, 50, 2.5], [40, 0, 2.5]], "height": 20})); let folded = volume(&mut s); - run(&mut s, "FusionSheetmetalUnfoldCommand", json!({})); - let m = run(&mut s, "MeasureCommand", json!({})); + run(&mut s, "sheet.unfold", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert!((m["bodies"][0]["bbox"]["max"][2].as_f64().unwrap_or(0.0) - 2.5).abs() < 1e-6, "{m}"); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Slot"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [35, -12], "p1": [45, 10]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"sketch": "Slot", "through_all": true, "operation": "cut", "direction": "symmetric"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Slot"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [35, -12], "p1": [45, 10]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "Slot", "through_all": true, "operation": "cut", "direction": "symmetric"})); run(&mut s, "sheet.refold", json!({})); let v = volume(&mut s); assert!(v < folded - 300.0, "{v} {folded}"); @@ -133,24 +133,24 @@ fn unfold_cut_refold_and_convert() { assert_eq!(flat(&mut s)["cutouts"].as_array().map(Vec::len), Some(1)); // Convert a plate. let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 100, "width": 60, "height": 2.5, "body_name": "Plate"})); - run(&mut s, "ConvertToSheetMetalCmd", json!({"body": "Plate", "face": [50, 30, 2.5]})); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 20})); + run(&mut s, "solid.box", json!({"length": 100, "width": 60, "height": 2.5, "body_name": "Plate"})); + run(&mut s, "sheet.convert", json!({"body": "Plate", "face": [50, 30, 2.5]})); + run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 20})); assert!(rel(volume(&mut s), 18972.622) < 1e-4, "{}", volume(&mut s)); let mut t = Session::default(); - run(&mut t, "PrimitiveCylinder", json!({"radius": 10, "height": 30, "body_name": "Rod"})); - assert!(t.execute("ConvertToSheetMetalCmd", &json!({"body": "Rod", "face": [0, 0, 30]})).is_err()); + run(&mut t, "solid.cylinder", json!({"radius": 10, "height": 30, "body_name": "Rod"})); + assert!(t.execute("sheet.convert", &json!({"body": "Rod", "face": [0, 0, 30]})).is_err()); } /// A sketch line across a 100 x 60 plate's top face at x = 40. fn fold_line(s: &mut Session) { - run(s, "SketchCreate", json!({"plane": "XY", "offset": 2.5, "name": "FoldLine"})); - run(s, "DrawPolyline", json!({"points": [[40, 0], [40, 60]], "ids": ["fl"]})); - run(s, "SketchStop", json!({})); + run(s, "sketch.create", json!({"plane": "XY", "offset": 2.5, "name": "FoldLine"})); + run(s, "sketch.line", json!({"points": [[40, 0], [40, 60]], "ids": ["fl"]})); + run(s, "sketch.finish", json!({})); } fn bbox(s: &mut Session) -> Value { - run(s, "MeasureCommand", json!({}))["bodies"][0]["bbox"].clone() + run(s, "inspect.measure", json!({}))["bodies"][0]["bbox"].clone() } #[test] @@ -160,7 +160,7 @@ fn fold_takes_the_bend_out_of_the_flat_sheet() { let mut s = Session::default(); base(&mut s, 100.0, 60.0); fold_line(&mut s); - run(&mut s, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl"})); + run(&mut s, "sheet.fold", json!({"sketch": "FoldLine", "curve": "fl"})); // The flat pattern keeps its size; the bend's centre line is the sketch line. let f = flat(&mut s); assert!((f["flat_size_mm"][0].as_f64().unwrap_or(0.0) - 100.0).abs() < 1e-6, "{f}"); @@ -189,7 +189,7 @@ fn fold_positions_fixed_side_and_flip() { for (k, v) in p.as_object().into_iter().flatten() { q[k] = v.clone(); } - run(&mut s, "SheetMetalFoldCmd", q); + run(&mut s, "sheet.fold", q); bbox(&mut s) }; let x0 = |b: &Value| b["min"][0].as_f64().unwrap_or(f64::NAN); @@ -215,10 +215,10 @@ fn fold_carries_flanges_on_the_moving_side() { let mut s = Session::default(); base(&mut s, 100.0, 60.0); // A flange on the left edge, then a fold between it and the rest. - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[0, 30, 2.5]], "height": 20})); + run(&mut s, "sheet.flange", json!({"edges": [[0, 30, 2.5]], "height": 20})); let v0 = volume(&mut s); fold_line(&mut s); - run(&mut s, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl", "fixed": [90, 30, 2.5]})); + run(&mut s, "sheet.fold", json!({"sketch": "FoldLine", "curve": "fl", "fixed": [90, 30, 2.5]})); // The flange rode along: it is above the plate now, nothing left at z < 0. let b = bbox(&mut s); assert!(b["min"][2].as_f64().unwrap_or(-1.0) > -1e-6, "{b}"); @@ -229,9 +229,9 @@ fn fold_carries_flanges_on_the_moving_side() { // Errors: off the sheet, through the flange's bend. let mut e = Session::default(); base(&mut e, 100.0, 60.0); - assert!(e.execute("SheetMetalFoldCmd", &json!({"points": [[40, 0, 30], [40, 60, 30]]})).is_err()); - assert!(e.execute("SheetMetalFoldCmd", &json!({"points": [[40, 0, 2.5], [40, 60, 2.5]], "position": "sideways"})).is_err()); - assert!(e.execute("SheetMetalFoldCmd", &json!({"points": [[0.5, 0, 2.5], [0.5, 60, 2.5]]})).is_err(), "no material past the bend"); + assert!(e.execute("sheet.fold", &json!({"points": [[40, 0, 30], [40, 60, 30]]})).is_err()); + assert!(e.execute("sheet.fold", &json!({"points": [[40, 0, 2.5], [40, 60, 2.5]], "position": "sideways"})).is_err()); + assert!(e.execute("sheet.fold", &json!({"points": [[0.5, 0, 2.5], [0.5, 60, 2.5]]})).is_err(), "no material past the bend"); } #[test] @@ -240,12 +240,12 @@ fn fold_on_a_flange_panel() { let ba = FRAC_PI_2 * (r + k * t); let mut s = Session::default(); base(&mut s, 100.0, 60.0); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[50, 0, 2.5]], "height": 30})); + run(&mut s, "sheet.flange", json!({"edges": [[50, 0, 2.5]], "height": 30})); let flat0 = flat(&mut s)["flat_size_mm"].clone(); let v0 = volume(&mut s); // The flange stands up at the plate's y = 0 edge: its outer face is the body's min y. let y = bbox(&mut s)["min"][1].as_f64().unwrap_or(f64::NAN); - run(&mut s, "SheetMetalFoldCmd", json!({"points": [[10, y, 18], [90, y, 18]]})); + run(&mut s, "sheet.fold", json!({"points": [[10, y, 18], [90, y, 18]]})); let f = flat(&mut s); for i in 0..2 { assert!( @@ -262,7 +262,7 @@ fn fold_on_a_flange_panel() { let b = bbox(&mut s); assert!(b["max"][2].as_f64().unwrap_or(99.0) < 25.0, "{b}"); // Folding across the flange's own bend is refused. - assert!(s.execute("SheetMetalFoldCmd", &json!({"points": [[50, y, 2], [50, y, 25]]})).is_err()); + assert!(s.execute("sheet.fold", &json!({"points": [[50, y, 2], [50, y, 25]]})).is_err()); } #[test] @@ -270,51 +270,51 @@ fn fold_through_cut_outs() { let mut s = Session::default(); base(&mut s, 100.0, 60.0); // A slot across the fold line (edges along and across the bend). - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Slot"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [30, 20], "p1": [50, 40]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"sketch": "Slot", "through_all": true, "operation": "cut", "direction": "symmetric"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Slot"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [30, 20], "p1": [50, 40]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "Slot", "through_all": true, "operation": "cut", "direction": "symmetric"})); let mut plain = Session::default(); base(&mut plain, 100.0, 60.0); fold_line(&mut plain); - run(&mut plain, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl"})); + run(&mut plain, "sheet.fold", json!({"sketch": "FoldLine", "curve": "fl"})); fold_line(&mut s); - run(&mut s, "SheetMetalFoldCmd", json!({"sketch": "FoldLine", "curve": "fl"})); + run(&mut s, "sheet.fold", json!({"sketch": "FoldLine", "curve": "fl"})); let f = flat(&mut s); assert_eq!(f["cutouts"].as_array().map(Vec::len), Some(1), "{f}"); let (v, vp) = (volume(&mut s), volume(&mut plain)); // The slot takes 20 x 20 of the flat sheet; across the bend it removes the bend's share. assert!(v < vp - 900.0 && v > vp - 1100.0, "{v} {vp}"); - run(&mut s, "FusionSheetmetalUnfoldCommand", json!({})); + run(&mut s, "sheet.unfold", json!({})); assert!((volume(&mut s) - (15000.0 - 1000.0)).abs() < 1e-3, "{}", volume(&mut s)); // A round hole across the bend is not supported yet, and says so. let mut c = Session::default(); base(&mut c, 100.0, 60.0); - run(&mut c, "SketchCreate", json!({"plane": "XY", "name": "Hole"})); - run(&mut c, "CircleCenterRadius", json!({"center": [40, 30], "radius": 5})); - run(&mut c, "SketchStop", json!({})); - run(&mut c, "Extrude", json!({"sketch": "Hole", "through_all": true, "operation": "cut", "direction": "symmetric"})); + run(&mut c, "sketch.create", json!({"plane": "XY", "name": "Hole"})); + run(&mut c, "sketch.circle.center", json!({"center": [40, 30], "radius": 5})); + run(&mut c, "sketch.finish", json!({})); + run(&mut c, "solid.extrude", json!({"sketch": "Hole", "through_all": true, "operation": "cut", "direction": "symmetric"})); fold_line(&mut c); - let e = c.execute("SheetMetalFoldCmd", &json!({"sketch": "FoldLine", "curve": "fl"})).map(|_| ()).unwrap_err().to_string(); + let e = c.execute("sheet.fold", &json!({"sketch": "FoldLine", "curve": "fl"})).map(|_| ()).unwrap_err().to_string(); assert!(e.contains("not supported yet"), "{e}"); } #[test] fn flange_edges_are_named_and_follow_the_base_sketch() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Base"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [100, 60]})); - let d = run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 100})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"sketch": "Base"})); - run(&mut s, "FusionSheetMetalFlangeCommand", json!({"edges": [[100, 30, 2.5]], "height": 20, "name": "Side"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [100, 60]})); + let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 100})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "sheet.flange", json!({"sketch": "Base"})); + run(&mut s, "sheet.flange", json!({"edges": [[100, 30, 2.5]], "height": 20, "name": "Side"})); let side = s.doc.find_feature("Side").cloned().unwrap(); assert_eq!(side.edge_names, vec!["base:l2:top".to_string()]); let v0 = volume(&mut s); // Narrower base: the flange stays on the right edge (now at x = 40). let name = d["param"].as_str().or_else(|| d["name"].as_str()).map(str::to_string).unwrap_or_else(|| "d1".into()); - run(&mut s, "ChangeParameterCommand", json!({"name": name, "expression": "40"})); - let b = run(&mut s, "MeasureCommand", json!({}))["bodies"][0]["bbox"].clone(); + run(&mut s, "parameters.change", json!({"name": name, "expression": "40"})); + let b = run(&mut s, "inspect.measure", json!({}))["bodies"][0]["bbox"].clone(); let w = b["max"][0].as_f64().unwrap_or(0.0) - b["min"][0].as_f64().unwrap_or(0.0); assert!((w - 40.0).abs() < 1e-3 && b["max"][2].as_f64().unwrap_or(0.0) > 19.0, "{b}"); assert!(volume(&mut s) < v0); @@ -323,7 +323,7 @@ fn flange_edges_are_named_and_follow_the_base_sketch() { // A hem on the flange's far edge is named after the flange. let top = b["max"][2].as_f64().unwrap_or(0.0); let x = b["max"][0].as_f64().unwrap_or(0.0); - run(&mut s, "FusionSheetMetalHemFlangeCommand", json!({"edges": [[x, 30, top]], "length": 6, "name": "H"})); + run(&mut s, "sheet.hem", json!({"edges": [[x, 30, top]], "length": 6, "name": "H"})); let h = s.doc.find_feature("H").cloned().unwrap(); assert!(h.edge_names[0].starts_with(&format!("F{id}.0:far")), "{:?}", h.edge_names); } @@ -334,7 +334,7 @@ fn fold_keeps_the_larger_side_either_way() { for (w, line, moving_right) in [(60.0, 40.0, true), (100.0, 40.0, false)] { let mut s = Session::default(); base(&mut s, w, 40.0); - run(&mut s, "SheetMetalFoldCmd", json!({"points": [[line, 0, 2.5], [line, 40, 2.5]]})); + run(&mut s, "sheet.fold", json!({"points": [[line, 0, 2.5], [line, 40, 2.5]]})); let b = bbox(&mut s); let (x0, x1) = (b["min"][0].as_f64().unwrap_or(0.0), b["max"][0].as_f64().unwrap_or(0.0)); if moving_right { diff --git a/crates/engine/src/cmd/sketch.rs b/crates/engine/src/cmd/sketch.rs index 960a7374..5467800d 100644 --- a/crates/engine/src/cmd/sketch.rs +++ b/crates/engine/src/cmd/sketch.rs @@ -10,136 +10,136 @@ use crate::params::{bad, bool_, expr, num, req_vec2, str_, string_list, vec2, ve use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("SketchCreate", "Create Sketch", create_sketch) + CommandSpec::new("sketch.create", "Create Sketch", create_sketch) .at("SOLID", "CREATE") .icon("sketch") .params("plane: XY|XZ|YZ | {origin, x_dir, y_dir} | {face: [x,y,z]} (planar face at point), offset?: expr, name?"), - CommandSpec::new("SketchActivate", "Edit Sketch", edit_sketch).icon("sketch").params("sketch: id or name"), - CommandSpec::new("SketchStop", "Finish Sketch", finish_sketch).at("SKETCH", "FINISH SKETCH").icon("finish").enabled(in_sketch).noundo(), - CommandSpec::new("DrawPolyline", "Line", draw_line) + CommandSpec::new("sketch.edit", "Edit Sketch", edit_sketch).icon("sketch").params("sketch: id or name"), + CommandSpec::new("sketch.finish", "Finish Sketch", finish_sketch).at("SKETCH", "FINISH SKETCH").icon("finish").enabled(in_sketch).noundo(), + CommandSpec::new("sketch.line", "Line", draw_line) .at("SKETCH", "CREATE") .icon("line") .key("L") .enabled(in_sketch) .params("points: [[x,y] | \"l1.end\" | {at, ref}…], closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical)"), - CommandSpec::new("ShapeRectangleTwoPoint", "2-Point Rectangle", rect_two_point) + CommandSpec::new("sketch.rectangle.two_point", "2-Point Rectangle", rect_two_point) .at("SKETCH", "CREATE") .icon("rect") .key("R") .enabled(in_sketch) .params("p0, p1: opposite corners [x,y]; construction?"), - CommandSpec::new("ShapeRectangleThreePoint", "3-Point Rectangle", rect_three_point) + CommandSpec::new("sketch.rectangle.three_point", "3-Point Rectangle", rect_three_point) .at("SKETCH", "CREATE") .icon("rect3") .enabled(in_sketch) .params("p0, p1: first edge; p2: point on the opposite edge"), - CommandSpec::new("ShapeRectangleCenter", "Center Rectangle", rect_center) + CommandSpec::new("sketch.rectangle.center", "Center Rectangle", rect_center) .at("SKETCH", "CREATE") .icon("rect_center") .enabled(in_sketch) .params("center, corner"), - CommandSpec::new("CircleCenterRadius", "Center Diameter Circle", circle_center) + CommandSpec::new("sketch.circle.center", "Center Diameter Circle", circle_center) .at("SKETCH", "CREATE") .icon("circle") .key("C") .enabled(in_sketch) .params("center: [x,y] or point ref, radius | diameter: number"), - CommandSpec::new("CircleTwoPoint", "2-Point Circle", circle_two) + CommandSpec::new("sketch.circle.two_point", "2-Point Circle", circle_two) .at("SKETCH", "CREATE") .icon("circle2") .enabled(in_sketch) .params("p0, p1: diameter ends"), - CommandSpec::new("CircleThreePoint", "3-Point Circle", circle_three) + CommandSpec::new("sketch.circle.three_point", "3-Point Circle", circle_three) .at("SKETCH", "CREATE") .icon("circle3") .enabled(in_sketch) .params("p0, p1, p2"), - CommandSpec::new("ArcThreePoint", "3-Point Arc", arc_three).at("SKETCH", "CREATE").icon("arc3").enabled(in_sketch).params("start, end, through"), - CommandSpec::new("ArcCenterTwoPoint", "Center Point Arc", arc_center) + CommandSpec::new("sketch.arc.three_point", "3-Point Arc", arc_three).at("SKETCH", "CREATE").icon("arc3").enabled(in_sketch).params("start, end, through"), + CommandSpec::new("sketch.arc.center_point", "Center Point Arc", arc_center) .at("SKETCH", "CREATE") .icon("arc_center") .enabled(in_sketch) .params("center, start, end (counter-clockwise from start) | center, start, sweep: signed degrees"), - CommandSpec::new("ShapePolygonInscribed", "Inscribed Polygon", polygon_inscribed) + CommandSpec::new("sketch.polygon.inscribed", "Inscribed Polygon", polygon_inscribed) .at("SKETCH", "CREATE") .icon("polygon") .enabled(in_sketch) .params("center, radius (to vertices), sides, angle?: deg"), - CommandSpec::new("ShapePolygonCircumscribed", "Circumscribed Polygon", polygon_circumscribed) + CommandSpec::new("sketch.polygon.circumscribed", "Circumscribed Polygon", polygon_circumscribed) .at("SKETCH", "CREATE") .icon("polygon") .enabled(in_sketch) .params("center, radius (to edge midpoints), sides, angle?: deg"), - CommandSpec::new("ShapePolygonEdge", "Edge Polygon", polygon_edge) + CommandSpec::new("sketch.polygon.edge", "Edge Polygon", polygon_edge) .at("SKETCH", "CREATE") .icon("polygon") .enabled(in_sketch) .params("p0, p1: one edge, sides"), - CommandSpec::new("ShapeSlotCenterToCenter", "Center to Center Slot", slot_c2c) + CommandSpec::new("sketch.slot.center_to_center", "Center to Center Slot", slot_c2c) .at("SKETCH", "CREATE") .icon("slot") .enabled(in_sketch) .params("p0, p1: arc centres, width"), - CommandSpec::new("ShapeSlotOverall", "Overall Slot", slot_overall) + CommandSpec::new("sketch.slot.overall", "Overall Slot", slot_overall) .at("SKETCH", "CREATE") .icon("slot") .enabled(in_sketch) .params("p0, p1: overall ends, width"), - CommandSpec::new("DrawPoint", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params("point: [x,y], id?"), - CommandSpec::new("SketchDimension", "Sketch Dimension", dimension).at("SKETCH", "CREATE").icon("dimension").key("D").enabled(in_sketch).params( + CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params("point: [x,y], id?"), + CommandSpec::new("sketch.dimension", "Sketch Dimension", dimension).at("SKETCH", "CREATE").icon("dimension").key("D").enabled(in_sketch).params( "entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current)", ), - CommandSpec::new("ConstraintHorizontalVertical", "Horizontal/Vertical", c_horizontal_vertical) + CommandSpec::new("sketch.constraint.horizontal_vertical", "Horizontal/Vertical", c_horizontal_vertical) .at("SKETCH", "CONSTRAINTS") .icon("c_hv") .enabled(in_sketch) .params("line: id | points: [a, b]; mode?: horizontal|vertical"), - CommandSpec::new("ConstraintCoincident", "Coincident", c_coincident) + CommandSpec::new("sketch.constraint.coincident", "Coincident", c_coincident) .at("SKETCH", "CONSTRAINTS") .icon("c_coincident") .enabled(in_sketch) .params("a: point, b: point or curve"), - CommandSpec::new("ConstraintTangent", "Tangent", c_tangent) + CommandSpec::new("sketch.constraint.tangent", "Tangent", c_tangent) .at("SKETCH", "CONSTRAINTS") .icon("c_tangent") .enabled(in_sketch) .params("a, b: curves"), - CommandSpec::new("ConstraintEqual", "Equal", c_equal) + CommandSpec::new("sketch.constraint.equal", "Equal", c_equal) .at("SKETCH", "CONSTRAINTS") .icon("c_equal") .enabled(in_sketch) .params("a, b: lines or circles/arcs"), - CommandSpec::new("ConstraintParallel", "Parallel", c_parallel) + CommandSpec::new("sketch.constraint.parallel", "Parallel", c_parallel) .at("SKETCH", "CONSTRAINTS") .icon("c_parallel") .enabled(in_sketch) .params("a, b: lines"), - CommandSpec::new("ConstraintPerpendicular", "Perpendicular", c_perpendicular) + CommandSpec::new("sketch.constraint.perpendicular", "Perpendicular", c_perpendicular) .at("SKETCH", "CONSTRAINTS") .icon("c_perpendicular") .enabled(in_sketch) .params("a, b: lines"), - CommandSpec::new("ConstraintFix", "Fix/UnFix", c_fix) + CommandSpec::new("sketch.constraint.fix", "Fix/UnFix", c_fix) .at("SKETCH", "CONSTRAINTS") .icon("c_fix") .enabled(in_sketch) .params("entity: point or curve | entities: [points and curves]; fixed?: bool (default: fix them all unless all are fixed, then unfix)"), - CommandSpec::new("ConstraintMidPoint", "MidPoint", c_midpoint) + CommandSpec::new("sketch.constraint.midpoint", "MidPoint", c_midpoint) .at("SKETCH", "CONSTRAINTS") .icon("c_midpoint") .enabled(in_sketch) .params("point, line"), - CommandSpec::new("ConstraintConcentric", "Concentric", c_concentric) + CommandSpec::new("sketch.constraint.concentric", "Concentric", c_concentric) .at("SKETCH", "CONSTRAINTS") .icon("c_concentric") .enabled(in_sketch) .params("a, b: circles/arcs"), - CommandSpec::new("ConstraintCollinear", "Collinear", c_collinear) + CommandSpec::new("sketch.constraint.collinear", "Collinear", c_collinear) .at("SKETCH", "CONSTRAINTS") .icon("c_collinear") .enabled(in_sketch) .params("a, b: lines"), - CommandSpec::new("ConstraintSymmetry", "Symmetry", c_symmetry) + CommandSpec::new("sketch.constraint.symmetry", "Symmetry", c_symmetry) .at("SKETCH", "CONSTRAINTS") .icon("c_symmetry") .enabled(in_sketch) @@ -363,7 +363,7 @@ fn face_contains(m: &solvecraft_geom::Mesh, fi: usize, p: Vec3, tol: f64) -> boo } fn create_sketch(s: &mut Session, p: &Value) -> Result { - let cmd = "SketchCreate"; + let cmd = "sketch.create"; let plane = plane_ref(s, p, cmd)?; // Validate the plane resolves. let (vals, _) = s.doc.param_values(); @@ -393,11 +393,11 @@ fn edit_sketch(s: &mut Session, p: &Value) -> Result { let key = match p.get("sketch") { Some(Value::Number(n)) => n.to_string(), Some(Value::String(x)) => x.clone(), - _ => return Err(bad("SketchActivate", "`sketch` must be an id or a name")), + _ => return Err(bad("sketch.edit", "`sketch` must be an id or a name")), }; - let f = s.doc.find_feature(&key).ok_or_else(|| bad("SketchActivate", format!("no sketch `{key}`")))?; + let f = s.doc.find_feature(&key).ok_or_else(|| bad("sketch.edit", format!("no sketch `{key}`")))?; if !matches!(f.kind, FeatureKind::Sketch { .. }) { - return Err(bad("SketchActivate", format!("`{key}` is not a sketch"))); + return Err(bad("sketch.edit", format!("`{key}` is not a sketch"))); } let (id, name) = (f.id, f.name.clone()); s.active_sketch = Some(id); @@ -416,7 +416,7 @@ fn finish_sketch(s: &mut Session, _p: &Value) -> Result { // Drawing fn draw_line(s: &mut Session, p: &Value) -> Result { - let cmd = "DrawPolyline"; + let cmd = "sketch.line"; let p = &super::sketch_project::snap_points(s, p, cmd)?; let list = p.get("points").and_then(Value::as_array).cloned().ok_or_else(|| bad(cmd, "`points` must be a list"))?; if list.len() < 2 || list.len() > 10_000 { @@ -527,7 +527,7 @@ fn rect_constraints(sk: &mut Sketch, l: &[usize]) -> Result> { } fn rect_two_point(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeRectangleTwoPoint"; + let cmd = "sketch.rectangle.two_point"; let (a, b) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?); if (a.x - b.x).abs() < 1e-9 || (a.y - b.y).abs() < 1e-9 { return Err(bad(cmd, "the corners must differ in x and y")); @@ -543,7 +543,7 @@ fn rect_two_point(s: &mut Session, p: &Value) -> Result { } fn rect_center(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeRectangleCenter"; + let cmd = "sketch.rectangle.center"; let (c, k) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "corner")?); let h = Vec2::new((k.x - c.x).abs(), (k.y - c.y).abs()); if h.x < 1e-9 || h.y < 1e-9 { @@ -573,7 +573,7 @@ fn rect_center(s: &mut Session, p: &Value) -> Result { } fn rect_three_point(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeRectangleThreePoint"; + let cmd = "sketch.rectangle.three_point"; let (a, b, c) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?, req_vec2(cmd, p, "p2")?); let d = (b - a).normalized().ok_or_else(|| bad(cmd, "p0 and p1 must differ"))?; let n = d.perp(); @@ -605,7 +605,7 @@ pub(super) fn radius_arg(p: &Value, cmd: &str) -> Result { } fn circle_center(s: &mut Session, p: &Value) -> Result { - let cmd = "CircleCenterRadius"; + let cmd = "sketch.circle.center"; let p = &super::sketch_project::snap_points(s, p, cmd)?; let r = radius_arg(p, cmd)?; let id = str_(p, "id").map(str::to_string); @@ -619,7 +619,7 @@ fn circle_center(s: &mut Session, p: &Value) -> Result { } fn circle_two(s: &mut Session, p: &Value) -> Result { - let cmd = "CircleTwoPoint"; + let cmd = "sketch.circle.two_point"; let (a, b) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?); let r = a.dist(b) / 2.0; if r < 1e-9 { @@ -647,7 +647,7 @@ pub(super) fn circumcircle(a: Vec2, b: Vec2, c: Vec2) -> Option<(Vec2, f64)> { } fn circle_three(s: &mut Session, p: &Value) -> Result { - let cmd = "CircleThreePoint"; + let cmd = "sketch.circle.three_point"; let (a, b, c) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?, req_vec2(cmd, p, "p2")?); let (o, r) = circumcircle(a, b, c).ok_or_else(|| bad(cmd, "the points are collinear"))?; let (out, info) = edit(s, p, cmd, false, |sk, _| { @@ -658,7 +658,7 @@ fn circle_three(s: &mut Session, p: &Value) -> Result { } fn arc_three(s: &mut Session, p: &Value) -> Result { - let cmd = "ArcThreePoint"; + let cmd = "sketch.arc.three_point"; let p = &super::sketch_project::snap_points(s, p, cmd)?; let id = str_(p, "id").map(str::to_string); let (out, info) = edit(s, p, cmd, false, |sk, _| { @@ -680,7 +680,7 @@ fn arc_three(s: &mut Session, p: &Value) -> Result { } fn arc_center(s: &mut Session, p: &Value) -> Result { - let cmd = "ArcCenterTwoPoint"; + let cmd = "sketch.arc.center_point"; let p = &super::sketch_project::snap_points(s, p, cmd)?; let id = str_(p, "id").map(str::to_string); let sweep = num(p, "sweep"); @@ -759,7 +759,7 @@ fn sides(p: &Value, cmd: &str) -> Result { } fn polygon_inscribed(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapePolygonInscribed"; + let cmd = "sketch.polygon.inscribed"; let c = req_vec2(cmd, p, "center")?; let r = radius_arg(p, cmd)?; let n = sides(p, cmd)?; @@ -768,7 +768,7 @@ fn polygon_inscribed(s: &mut Session, p: &Value) -> Result { } fn polygon_circumscribed(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapePolygonCircumscribed"; + let cmd = "sketch.polygon.circumscribed"; let c = req_vec2(cmd, p, "center")?; let r = radius_arg(p, cmd)?; let n = sides(p, cmd)?; @@ -777,7 +777,7 @@ fn polygon_circumscribed(s: &mut Session, p: &Value) -> Result { } fn polygon_edge(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapePolygonEdge"; + let cmd = "sketch.polygon.edge"; let (a, b) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?); let n = sides(p, cmd)?; let e = a.dist(b); @@ -816,14 +816,14 @@ pub(super) fn slot(s: &mut Session, p: &Value, cmd: &str, c0: Vec2, c1: Vec2, w: } fn slot_c2c(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeSlotCenterToCenter"; + let cmd = "sketch.slot.center_to_center"; let (a, b) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?); let w = num(p, "width").filter(|w| *w > 1e-9).ok_or_else(|| bad(cmd, "`width` must be positive"))?; slot(s, p, cmd, a, b, w) } fn slot_overall(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeSlotOverall"; + let cmd = "sketch.slot.overall"; let (a, b) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?); let w = num(p, "width").filter(|w| *w > 1e-9).ok_or_else(|| bad(cmd, "`width` must be positive"))?; let d = (b - a).normalized().ok_or_else(|| bad(cmd, "the slot ends must differ"))?; @@ -834,7 +834,7 @@ fn slot_overall(s: &mut Session, p: &Value) -> Result { } fn draw_point(s: &mut Session, p: &Value) -> Result { - let cmd = "DrawPoint"; + let cmd = "sketch.point"; let at = req_vec2(cmd, p, "point")?; let id = str_(p, "id").map(str::to_string); let (pid, info) = edit(s, p, cmd, false, |sk, _| { @@ -1018,7 +1018,7 @@ pub(super) fn reject_redundant(before: Option, info: &Value, cmd: &str) - } fn c_horizontal_vertical(s: &mut Session, p: &Value) -> Result { - let cmd = "ConstraintHorizontalVertical"; + let cmd = "sketch.constraint.horizontal_vertical"; let mode = str_(p, "mode").map(str::to_ascii_lowercase); constrain(s, p, cmd, |sk| { if let Some(pts) = p.get("points").and_then(Value::as_array) { @@ -1048,7 +1048,7 @@ fn c_horizontal_vertical(s: &mut Session, p: &Value) -> Result { } fn c_coincident(s: &mut Session, p: &Value) -> Result { - let cmd = "ConstraintCoincident"; + let cmd = "sketch.constraint.coincident"; constrain(s, p, cmd, |sk| { let aref = p.get("a").and_then(Value::as_str).unwrap_or_default(); let bref = p.get("b").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`b` must be a point or curve"))?; @@ -1070,31 +1070,39 @@ fn two_curves(sk: &Sketch, p: &Value, cmd: &str) -> Result<(usize, usize)> { } fn c_tangent(s: &mut Session, p: &Value) -> Result { - constrain(s, p, "ConstraintTangent", |sk| two_curves(sk, p, "ConstraintTangent").map(|(a, b)| ConstraintKind::Tangent { a, b })) + constrain(s, p, "sketch.constraint.tangent", |sk| two_curves(sk, p, "sketch.constraint.tangent").map(|(a, b)| ConstraintKind::Tangent { a, b })) } fn c_equal(s: &mut Session, p: &Value) -> Result { - constrain(s, p, "ConstraintEqual", |sk| two_curves(sk, p, "ConstraintEqual").map(|(a, b)| ConstraintKind::Equal { a, b })) + constrain(s, p, "sketch.constraint.equal", |sk| two_curves(sk, p, "sketch.constraint.equal").map(|(a, b)| ConstraintKind::Equal { a, b })) } fn c_parallel(s: &mut Session, p: &Value) -> Result { - constrain(s, p, "ConstraintParallel", |sk| two_curves(sk, p, "ConstraintParallel").map(|(a, b)| ConstraintKind::Parallel { a, b })) + constrain(s, p, "sketch.constraint.parallel", |sk| { + two_curves(sk, p, "sketch.constraint.parallel").map(|(a, b)| ConstraintKind::Parallel { a, b }) + }) } fn c_perpendicular(s: &mut Session, p: &Value) -> Result { - constrain(s, p, "ConstraintPerpendicular", |sk| two_curves(sk, p, "ConstraintPerpendicular").map(|(a, b)| ConstraintKind::Perpendicular { a, b })) + constrain(s, p, "sketch.constraint.perpendicular", |sk| { + two_curves(sk, p, "sketch.constraint.perpendicular").map(|(a, b)| ConstraintKind::Perpendicular { a, b }) + }) } fn c_concentric(s: &mut Session, p: &Value) -> Result { - constrain(s, p, "ConstraintConcentric", |sk| two_curves(sk, p, "ConstraintConcentric").map(|(a, b)| ConstraintKind::Concentric { a, b })) + constrain(s, p, "sketch.constraint.concentric", |sk| { + two_curves(sk, p, "sketch.constraint.concentric").map(|(a, b)| ConstraintKind::Concentric { a, b }) + }) } fn c_collinear(s: &mut Session, p: &Value) -> Result { - constrain(s, p, "ConstraintCollinear", |sk| two_curves(sk, p, "ConstraintCollinear").map(|(a, b)| ConstraintKind::Collinear { a, b })) + constrain(s, p, "sketch.constraint.collinear", |sk| { + two_curves(sk, p, "sketch.constraint.collinear").map(|(a, b)| ConstraintKind::Collinear { a, b }) + }) } fn c_midpoint(s: &mut Session, p: &Value) -> Result { - let cmd = "ConstraintMidPoint"; + let cmd = "sketch.constraint.midpoint"; constrain(s, p, cmd, |sk| { Ok(ConstraintKind::Midpoint { p: point_ref(sk, p.get("point"), cmd, "point")?, l: curve_ref(sk, p.get("line"), cmd, "line")? }) }) } fn c_symmetry(s: &mut Session, p: &Value) -> Result { - let cmd = "ConstraintSymmetry"; + let cmd = "sketch.constraint.symmetry"; let (ids, info) = edit(s, p, cmd, true, |sk, _| { let l = curve_ref(sk, p.get("line"), cmd, "line")?; let ra = p.get("a").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`a` must be a point or curve"))?; @@ -1141,7 +1149,7 @@ fn round_center(sk: &Sketch, c: usize) -> Option { } fn c_fix(s: &mut Session, p: &Value) -> Result { - let cmd = "ConstraintFix"; + let cmd = "sketch.constraint.fix"; let want = bool_(p, "fixed"); let mut refs = string_list(p, "entities"); if let Some(e) = str_(p, "entity") { @@ -1207,7 +1215,7 @@ pub(super) fn line_pts(sk: &Sketch, l: usize) -> Option<(Vec2, Vec2)> { } fn dimension(s: &mut Session, p: &Value) -> Result { - let cmd = "SketchDimension"; + let cmd = "sketch.dimension"; let ents = string_list(p, "entities"); if ents.is_empty() || ents.len() > 2 { return Err(bad(cmd, "`entities` must list one or two sketch entities")); diff --git a/crates/engine/src/cmd/sketch_canvas.rs b/crates/engine/src/cmd/sketch_canvas.rs index 731664f0..c3111aa8 100644 --- a/crates/engine/src/cmd/sketch_canvas.rs +++ b/crates/engine/src/cmd/sketch_canvas.rs @@ -9,11 +9,11 @@ use crate::params::{bad, bool_, num, str_, vec2}; use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("FusionAddCanvasCommand", "Canvas", add_canvas) + CommandSpec::new("canvas.insert", "Canvas", add_canvas) .at("SKETCH", "INSERT") .icon("canvas") .params("path | data (base64 PNG or JPEG); plane?: XY|XZ|YZ|{face: [x,y,z]}|… (default XY); center?: [x,y] on the plane, or at?: [x,y,z]; width?: mm (default 100); angle?: deg; opacity?: 0…1 (default 0.5); flip?; name?"), - CommandSpec::new("FusionAddEditDecalCommand", "Decal", add_decal) + CommandSpec::new("canvas.decal", "Decal", add_decal) .at("SKETCH", "INSERT") .icon("decal") .params("path | data; face: [x,y,z] on a planar face (the image is centred there); width?: mm; angle?: deg; opacity?: (default 1)"), @@ -79,11 +79,11 @@ fn make(s: &mut Session, p: &Value, cmd: &str, decal: bool) -> Result { } fn add_canvas(s: &mut Session, p: &Value) -> Result { - make(s, p, "FusionAddCanvasCommand", false) + make(s, p, "canvas.insert", false) } fn add_decal(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionAddEditDecalCommand"; + let cmd = "canvas.decal"; let face = p.get("face").cloned().ok_or_else(|| bad(cmd, "`face` must be a point [x,y,z] on a planar face"))?; let mut q = p.clone(); if let Some(o) = q.as_object_mut() { @@ -174,7 +174,7 @@ mod tests { #[test] fn canvas_insert_edit_calibrate_save() { let mut s = Session::default(); - let r = s.execute("FusionAddCanvasCommand", &json!({"data": png(400, 200), "plane": "XZ", "width": 80})).unwrap(); + let r = s.execute("canvas.insert", &json!({"data": png(400, 200), "plane": "XZ", "width": 80})).unwrap(); let id = r["canvas"].as_u64().unwrap(); let c = s.doc.canvases[0].clone(); assert_eq!(c.pixels, [400, 200]); @@ -188,12 +188,12 @@ mod tests { assert_eq!(back.canvases, s.doc.canvases); s.execute("canvas.delete", &json!({"canvas": id})).unwrap(); assert!(s.doc.canvases.is_empty()); - s.execute("UndoCommand", &json!({})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(s.doc.canvases.len(), 1); - assert!(s.execute("FusionAddCanvasCommand", &json!({"data": "bm9wZQ=="})).is_err()); + assert!(s.execute("canvas.insert", &json!({"data": "bm9wZQ=="})).is_err()); // A decal on the top face of a box. - s.execute("PrimitiveBox", &json!({"length": 30, "width": 30, "height": 10})).unwrap(); - let r = s.execute("FusionAddEditDecalCommand", &json!({"data": png(10, 10), "face": [15, 15, 10], "width": 20})).unwrap(); + s.execute("solid.box", &json!({"length": 30, "width": 30, "height": 10})).unwrap(); + let r = s.execute("canvas.decal", &json!({"data": png(10, 10), "face": [15, 15, 10], "width": 20})).unwrap(); assert!(r["canvas"].is_u64()); } } diff --git a/crates/engine/src/cmd/sketch_constraints.rs b/crates/engine/src/cmd/sketch_constraints.rs index 8f06c1a6..16d6ea57 100644 --- a/crates/engine/src/cmd/sketch_constraints.rs +++ b/crates/engine/src/cmd/sketch_constraints.rs @@ -11,32 +11,32 @@ use crate::params::{bad, num, req_vec2, str_, string_list, vec2}; use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("ConstraintSmooth", "Curvature", c_smooth) + CommandSpec::new("sketch.constraint.curvature", "Curvature", c_smooth) .at("SKETCH", "CONSTRAINTS") .icon("c_smooth") .enabled(in_sketch) .params("a, b: two curves sharing an end point (curvature continuous, G2)"), - CommandSpec::new("SketchPolygonConstraintCmd", "Polygon", c_polygon) + CommandSpec::new("sketch.constraint.polygon", "Polygon", c_polygon) .at("SKETCH", "CONSTRAINTS") .icon("c_polygon") .enabled(in_sketch) .params("lines: [line ids] of a closed chain: equal sides with every corner on one circle (a regular polygon)"), - CommandSpec::new("SketchAutoConstraintAndDimCmd", "AutoConstrain", auto_constrain) + CommandSpec::new("sketch.auto_constrain", "AutoConstrain", auto_constrain) .at("SKETCH", "CONSTRAINTS") .icon("auto_constrain") .enabled(in_sketch) .params("tolerance?: mm (default 0.01), angle_tolerance?: deg (default 1), dimensions?: bool (default true): infer constraints, then dimension until fully constrained"), - CommandSpec::new("SketchAutoConstraintAndDimFromDatumCmd", "AutoConstrain from datum", auto_constrain_datum) + CommandSpec::new("sketch.auto_constrain_from_datum", "AutoConstrain from datum", auto_constrain_datum) .at("SKETCH", "CONSTRAINTS") .icon("auto_constrain") .enabled(in_sketch) .params("datum: point ref (default origin); like AutoConstrain with positions measured from the datum"), - CommandSpec::new("SketchAutoConstrainAndFinish", "Finish with AutoConstrain", auto_constrain_finish) + CommandSpec::new("sketch.finish_auto_constrain", "Finish with AutoConstrain", auto_constrain_finish) .at("SKETCH", "FINISH SKETCH") .icon("finish") .enabled(in_sketch) .params("like AutoConstrain, then Finish Sketch"), - CommandSpec::new("SketchConstrainer", "SketchConstrainer", smart_constrain) + CommandSpec::new("sketch.constrainer", "Sketch Constrainer", smart_constrain) .at("SKETCH", "CREATE") .icon("constrainer") .enabled(in_sketch) @@ -55,7 +55,7 @@ pub static COMMANDS: &[CommandSpec] = &[ ]; fn c_smooth(s: &mut Session, p: &Value) -> Result { - let cmd = "ConstraintSmooth"; + let cmd = "sketch.constraint.curvature"; let mut before = None; let (id, info) = edit(s, p, cmd, true, |sk, _| { let a = curve(sk, p.get("a"), cmd)?; @@ -73,7 +73,7 @@ fn curve(sk: &Sketch, v: Option<&Value>, cmd: &str) -> Result { } fn c_polygon(s: &mut Session, p: &Value) -> Result { - let cmd = "SketchPolygonConstraintCmd"; + let cmd = "sketch.constraint.polygon"; let ids = string_list(p, "lines"); if ids.len() < 3 || ids.len() > 200 { return Err(bad(cmd, "`lines` must list 3 to 200 lines of a closed chain")); @@ -258,13 +258,13 @@ fn auto(s: &mut Session, p: &Value, cmd: &str) -> Result { } fn auto_constrain(s: &mut Session, p: &Value) -> Result { - auto(s, p, "SketchAutoConstraintAndDimCmd") + auto(s, p, "sketch.auto_constrain") } fn auto_constrain_datum(s: &mut Session, p: &Value) -> Result { - auto(s, p, "SketchAutoConstraintAndDimFromDatumCmd") + auto(s, p, "sketch.auto_constrain_from_datum") } fn auto_constrain_finish(s: &mut Session, p: &Value) -> Result { - let r = auto(s, p, "SketchAutoConstrainAndFinish")?; + let r = auto(s, p, "sketch.finish_auto_constrain")?; s.active_sketch = None; s.revision += 1; Ok(r) @@ -343,7 +343,7 @@ fn guess(sk: &Sketch, ents: &[String], cmd: &str) -> Result { } fn smart_constrain(s: &mut Session, p: &Value) -> Result { - let cmd = "SketchConstrainer"; + let cmd = "sketch.constrainer"; let ents = string_list(p, "entities"); let mut before = None; let mut name = ""; diff --git a/crates/engine/src/cmd/sketch_constraints_tests.rs b/crates/engine/src/cmd/sketch_constraints_tests.rs index ebfbde03..9aaf25b6 100644 --- a/crates/engine/src/cmd/sketch_constraints_tests.rs +++ b/crates/engine/src/cmd/sketch_constraints_tests.rs @@ -16,7 +16,7 @@ fn ids(v: &Value) -> Vec { fn new_sketch() -> Session { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); s } @@ -27,21 +27,21 @@ fn sketch(s: &Session) -> solvecraft_sketch::Sketch { #[test] fn redundant_constraints_and_dimensions_are_refused_driven_ones_measure() { let mut s = new_sketch(); - let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [10, 0]], "infer": true}))["curves"])[0].clone(); + let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]], "infer": true}))["curves"])[0].clone(); // Already horizontal (inferred). - let e = s.execute("ConstraintHorizontalVertical", &json!({"line": l})).unwrap_err().to_string(); + let e = s.execute("sketch.constraint.horizontal_vertical", &json!({"line": l})).unwrap_err().to_string(); assert!(e.contains("over-constrain"), "{e}"); - run(&mut s, "ConstraintCoincident", json!({"a": format!("{l}.start"), "b": "origin"})); - run(&mut s, "SketchDimension", json!({"entities": [l], "value": 10})); + run(&mut s, "sketch.constraint.coincident", json!({"a": format!("{l}.start"), "b": "origin"})); + run(&mut s, "sketch.dimension", json!({"entities": [l], "value": 10})); let si = run(&mut s, "sketch.inspect", json!({})); assert_eq!(si["dof"], 0, "{si}"); // A second length on it over-constrains: refused, unless driven. - assert!(s.execute("SketchDimension", &json!({"entities": [format!("{l}.start"), format!("{l}.end")]})).is_err()); - let d = run(&mut s, "SketchDimension", json!({"entities": [format!("{l}.start"), format!("{l}.end")], "driven": true})); + assert!(s.execute("sketch.dimension", &json!({"entities": [format!("{l}.start"), format!("{l}.end")]})).is_err()); + let d = run(&mut s, "sketch.dimension", json!({"entities": [format!("{l}.start"), format!("{l}.end")], "driven": true})); assert_eq!(d["driven"], true); // The driven dimension follows the driving one. let pname = si["constraints"].as_array().unwrap().iter().find_map(|c| c["param"].as_str().map(str::to_string)).unwrap(); - run(&mut s, "ChangeParameterCommand", json!({"name": pname, "expression": "25 mm"})); + run(&mut s, "parameters.change", json!({"name": pname, "expression": "25 mm"})); let g = run(&mut s, "sketch.glyphs", json!({})); let driven = g["glyphs"].as_array().unwrap().iter().find(|x| x["driven"] == true).unwrap().clone(); assert!((driven["value"].as_f64().unwrap() - 25.0).abs() < 1e-7, "{driven}"); @@ -53,9 +53,10 @@ fn curvature_polygon_arc_length_linear_diameter() { let mut s = new_sketch(); // A horizontal line ending where a control-point spline starts: G2 keeps the spline's // start straight (zero curvature) and tangent. - let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[-10, 0], [0, 0]], "infer": true}))["curves"])[0].clone(); - let sp = ids(&run(&mut s, "DrawCVMSpline3D", json!({"points": [format!("{l}.end"), [5, 2], [10, 6], [15, 15]]}))["curves"])[0].clone(); - run(&mut s, "ConstraintSmooth", json!({"a": l, "b": sp})); + let l = ids(&run(&mut s, "sketch.line", json!({"points": [[-10, 0], [0, 0]], "infer": true}))["curves"])[0].clone(); + let sp = + ids(&run(&mut s, "sketch.spline.control_point", json!({"points": [format!("{l}.end"), [5, 2], [10, 6], [15, 15]]}))["curves"])[0].clone(); + run(&mut s, "sketch.constraint.curvature", json!({"a": l, "b": sp})); let sk = sketch(&s); let pts: Vec = sk.curves[sk.curve_index(&sp).unwrap()].kind.point_ids().iter().map(|i| sk.point(*i).unwrap()).collect(); // Tangent: first leg along +x; zero curvature: the second leg too (collinear first three). @@ -63,16 +64,16 @@ fn curvature_polygon_arc_length_linear_diameter() { assert!((pts[2] - pts[1]).cross(pts[1] - pts[0]).abs() < 1e-3, "{pts:?}"); // Polygon constraint on a rough quadrilateral: becomes a square-ish regular polygon. - let q = run(&mut s, "DrawPolyline", json!({"points": [[100, 0], [111, 1], [110, 9], [99, 10]], "closed": true})); + let q = run(&mut s, "sketch.line", json!({"points": [[100, 0], [111, 1], [110, 9], [99, 10]], "closed": true})); let ql = ids(&q["curves"]); - run(&mut s, "SketchPolygonConstraintCmd", json!({"lines": ql})); + run(&mut s, "sketch.constraint.polygon", json!({"lines": ql})); let sk = sketch(&s); let lens: Vec = ql.iter().map(|i| sk.polyline(sk.curve_index(i).unwrap())).map(|p| p[0].dist(p[p.len() - 1])).collect(); assert!(lens.iter().all(|x| (x - lens[0]).abs() < 1e-6), "{lens:?}"); // Arc length. - let a = ids(&run(&mut s, "ArcCenterTwoPoint", json!({"center": [0, 50], "start": [10, 50], "end": [0, 60]}))["curves"])[0].clone(); - run(&mut s, "SketchDimension", json!({"entities": [a], "type": "arc_length", "value": 20})); + let a = ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [0, 50], "start": [10, 50], "end": [0, 60]}))["curves"])[0].clone(); + run(&mut s, "sketch.dimension", json!({"entities": [a], "type": "arc_length", "value": 20})); let sk = sketch(&s); let ai = sk.curve_index(&a).unwrap(); let len = match sk.segs(ai)[0] { @@ -82,10 +83,10 @@ fn curvature_polygon_arc_length_linear_diameter() { assert!((len - 20.0).abs() < 1e-6, "{len}"); // Linear diameter about a centerline. - let cl = ids(&run(&mut s, "DrawPolyline", json!({"points": [[200, 0], [200, 50]]}))["curves"])[0].clone(); + let cl = ids(&run(&mut s, "sketch.line", json!({"points": [[200, 0], [200, 50]]}))["curves"])[0].clone(); run(&mut s, "sketch.centerline", json!({"curves": [cl]})); - run(&mut s, "DrawPoint", json!({"point": [207, 10], "id": "rim"})); - let d = run(&mut s, "SketchDimension", json!({"entities": ["rim", cl], "value": 30})); + run(&mut s, "sketch.point", json!({"point": [207, 10], "id": "rim"})); + let d = run(&mut s, "sketch.dimension", json!({"entities": ["rim", cl], "value": 30})); assert_eq!(d["type"], "LinearDiameterDimension", "{d}"); let sk = sketch(&s); let rim = sk.point(sk.point_index("rim").unwrap()).unwrap(); @@ -98,7 +99,7 @@ fn curvature_polygon_arc_length_linear_diameter() { #[test] fn glyphs_and_snaps() { let mut s = new_sketch(); - let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]})); + let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]})); let l = ids(&r["curves"]); let g = run(&mut s, "sketch.glyphs", json!({})); assert_eq!(g["glyphs"].as_array().unwrap().len(), 4); @@ -116,9 +117,9 @@ fn glyphs_and_snaps() { fn auto_constrain_fully_constrains_a_rough_sketch() { let mut s = new_sketch(); // A slightly crooked closed rectangle and a circle inside. - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [40, 0.004], [40.003, 30], [0, 30.002]], "closed": true})); - run(&mut s, "CircleCenterRadius", json!({"center": [20, 15], "radius": 5})); - let r = run(&mut s, "SketchAutoConstraintAndDimCmd", json!({})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0.004], [40.003, 30], [0, 30.002]], "closed": true})); + run(&mut s, "sketch.circle.center", json!({"center": [20, 15], "radius": 5})); + let r = run(&mut s, "sketch.auto_constrain", json!({})); assert_eq!(r["dof"], 0, "{r}"); let si = run(&mut s, "sketch.inspect", json!({})); assert_eq!(si["fully_constrained"], true, "{si}"); @@ -127,9 +128,9 @@ fn auto_constrain_fully_constrains_a_rough_sketch() { assert!(names.iter().any(|n| n == "DiameterDimension"), "{names:?}"); // From a datum, and finishing. let mut s = new_sketch(); - run(&mut s, "DrawPoint", json!({"point": [5, 5], "id": "d"})); - run(&mut s, "DrawPolyline", json!({"points": [[10, 10], [20, 10]]})); - let r = run(&mut s, "SketchAutoConstrainAndFinish", json!({"datum": "d"})); + run(&mut s, "sketch.point", json!({"point": [5, 5], "id": "d"})); + run(&mut s, "sketch.line", json!({"points": [[10, 10], [20, 10]]})); + let r = run(&mut s, "sketch.finish_auto_constrain", json!({"datum": "d"})); assert!(s.active_sketch.is_none()); assert!(r["added"].as_array().unwrap().len() >= 3, "{r}"); } @@ -137,11 +138,11 @@ fn auto_constrain_fully_constrains_a_rough_sketch() { #[test] fn drawing_infers_tangent_and_perpendicular() { let mut s = new_sketch(); - let l1 = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [10, 10]], "infer": true}))["curves"])[0].clone(); - let r = run(&mut s, "DrawPolyline", json!({"points": [format!("{l1}.end"), [20, 0]], "infer": true})); + let l1 = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 10]], "infer": true}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.line", json!({"points": [format!("{l1}.end"), [20, 0]], "infer": true})); assert_eq!(r["constraints"].as_array().unwrap().len(), 1, "{r}"); - let a = ids(&run(&mut s, "ArcCenterTwoPoint", json!({"center": [50, 0], "start": [60, 0], "end": [50, 10]}))["curves"])[0].clone(); - let r = run(&mut s, "DrawPolyline", json!({"points": [format!("{a}.end"), [30, 10]], "infer": true})); + let a = ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [50, 0], "start": [60, 0], "end": [50, 10]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.line", json!({"points": [format!("{a}.end"), [30, 10]], "infer": true})); let si = run(&mut s, "sketch.inspect", json!({})); let names: Vec<&str> = si["constraints"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect(); assert!(names.contains(&"Perpendicular") && names.contains(&"Tangent"), "{names:?} {r}"); @@ -150,18 +151,18 @@ fn drawing_infers_tangent_and_perpendicular() { #[test] fn smart_constrain_and_driven_toggle() { let mut s = new_sketch(); - let a = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [10, 0.5]]}))["curves"])[0].clone(); - let b = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 5], [10, 6]]}))["curves"])[0].clone(); - let r = run(&mut s, "SketchConstrainer", json!({"entities": [a]})); + let a = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0.5]]}))["curves"])[0].clone(); + let b = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 5], [10, 6]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.constrainer", json!({"entities": [a]})); assert_eq!(r["type"], "Horizontal"); - let r = run(&mut s, "SketchConstrainer", json!({"entities": [a, b]})); + let r = run(&mut s, "sketch.constrainer", json!({"entities": [a, b]})); assert_eq!(r["type"], "Parallel"); - let c1 = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [50, 0], "radius": 5}))["curves"])[0].clone(); - let c2 = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [50.3, 0.2], "radius": 3}))["curves"])[0].clone(); - let r = run(&mut s, "SketchConstrainer", json!({"entities": [c1, c2]})); + let c1 = ids(&run(&mut s, "sketch.circle.center", json!({"center": [50, 0], "radius": 5}))["curves"])[0].clone(); + let c2 = ids(&run(&mut s, "sketch.circle.center", json!({"center": [50.3, 0.2], "radius": 3}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.constrainer", json!({"entities": [c1, c2]})); assert_eq!(r["type"], "Concentric"); // Driven toggle. - let d = run(&mut s, "SketchDimension", json!({"entities": [c1], "value": 10})); + let d = run(&mut s, "sketch.dimension", json!({"entities": [c1], "value": 10})); let g = run(&mut s, "sketch.glyphs", json!({})); let did = g["glyphs"].as_array().unwrap().iter().find(|x| x["param"] == d["param"]).unwrap()["id"].as_str().unwrap().to_string(); let r = run(&mut s, "sketch.toggle_driven", json!({"constraint": did})); @@ -174,8 +175,8 @@ fn smart_constrain_and_driven_toggle() { #[test] fn drawing_from_a_line_midpoint_keeps_it_there() { let mut s = new_sketch(); - let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone(); - let r = run(&mut s, "DrawPolyline", json!({"points": [format!("mid:{l}"), [10, 15]]})); + let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.line", json!({"points": [format!("mid:{l}"), [10, 15]]})); let si = run(&mut s, "sketch.inspect", json!({})); assert!(si["constraints"].as_array().unwrap().iter().any(|c| c["name"] == "MidPoint"), "{si}"); // Lengthen the first line: the second one's start follows the middle. @@ -191,10 +192,10 @@ fn drawing_from_a_line_midpoint_keeps_it_there() { #[test] fn drawing_onto_a_curve_keeps_the_point_on_it() { let mut s = new_sketch(); - let c = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone(); - let r = run(&mut s, "DrawPolyline", json!({"points": [format!("on:{c}:7.2,7.1"), [30, 30]]})); + let c = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.line", json!({"points": [format!("on:{c}:7.2,7.1"), [30, 30]]})); let n = ids(&r["curves"])[0].clone(); - run(&mut s, "SketchDimension", json!({"entities": [c], "value": 16})); + run(&mut s, "sketch.dimension", json!({"entities": [c], "value": 16})); let sk = sketch(&s); let st = sk.resolve_point(&format!("{n}.start")).and_then(|i| sk.point(i)).unwrap(); let cc = sk.center(sk.curve_index(&c).unwrap()).unwrap(); diff --git a/crates/engine/src/cmd/sketch_create.rs b/crates/engine/src/cmd/sketch_create.rs index 12db3034..b5247c45 100644 --- a/crates/engine/src/cmd/sketch_create.rs +++ b/crates/engine/src/cmd/sketch_create.rs @@ -12,65 +12,65 @@ use crate::{Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("SketchMidpointLine", "Midpoint Line", midpoint_line) + CommandSpec::new("sketch.line.midpoint", "Midpoint Line", midpoint_line) .at("SKETCH", "CREATE") .icon("line_mid") .enabled(in_sketch) .params("mid: [x,y] or point ref, end: [x,y]; construction?"), - CommandSpec::new("ArcTangent", "Tangent Arc", arc_tangent).at("SKETCH", "CREATE").icon("arc_tangent").enabled(in_sketch).params( + CommandSpec::new("sketch.arc.tangent", "Tangent Arc", arc_tangent).at("SKETCH", "CREATE").icon("arc_tangent").enabled(in_sketch).params( "start: end point of a line or arc (\"l1.end\"), end: [x,y]; curve?: the curve to be tangent to (default: the one ending at start)", ), - CommandSpec::new("CircleTanTanRadius", "2-Tangent Circle", circle_tan_tan) + CommandSpec::new("sketch.circle.two_tangent", "2-Tangent Circle", circle_tan_tan) .at("SKETCH", "CREATE") .icon("circle_tt") .enabled(in_sketch) .params("curves: [two lines/circles/arcs], radius | diameter: expr, near: [x,y] (which of the solutions)"), - CommandSpec::new("CircleThreeTangent", "3-Tangent Circle", circle_three_tan) + CommandSpec::new("sketch.circle.three_tangent", "3-Tangent Circle", circle_three_tan) .at("SKETCH", "CREATE") .icon("circle_ttt") .enabled(in_sketch) .params("curves: [three lines/circles/arcs], near: [x,y] (which of the solutions)"), - CommandSpec::new("ShapeSlotCenterPoint", "Center Point Slot", slot_center_point) + CommandSpec::new("sketch.slot.center_point", "Center Point Slot", slot_center_point) .at("SKETCH", "CREATE") .icon("slot") .enabled(in_sketch) .params("center: slot centre, end: one arc centre, width"), - CommandSpec::new("ShapeArcSlotThreePoint", "Three Point Arc Slot", arc_slot_three) + CommandSpec::new("sketch.slot.arc_three_point", "Three Point Arc Slot", arc_slot_three) .at("SKETCH", "CREATE") .icon("arc_slot") .enabled(in_sketch) .params("start, through, end: points on the centre arc, width"), - CommandSpec::new("ShapeArcSlotCenterTwoPoint", "Center Point Arc Slot", arc_slot_center) + CommandSpec::new("sketch.slot.arc_center", "Center Point Arc Slot", arc_slot_center) .at("SKETCH", "CREATE") .icon("arc_slot") .enabled(in_sketch) .params("center, start: centre arc start, end: direction of the centre arc end (counter-clockwise), width"), - CommandSpec::new("CircleElipse", "Ellipse", ellipse) + CommandSpec::new("sketch.ellipse", "Ellipse", ellipse) .at("SKETCH", "CREATE") .icon("ellipse") .enabled(in_sketch) .params("center, major: end of the major axis [x,y], minor: a point the ellipse passes through on the minor side [x,y] | minor_radius"), - CommandSpec::new("DrawSpline", "Fit Point Spline", spline_fit) + CommandSpec::new("sketch.spline.fit_point", "Fit Point Spline", spline_fit) .at("SKETCH", "CREATE") .icon("spline") .enabled(in_sketch) .params("points: [[x,y] or point refs…] (2…500): the spline passes through them"), - CommandSpec::new("DrawCVMSpline3D", "Control Point Spline", spline_cv3) + CommandSpec::new("sketch.spline.control_point", "Control Point Spline", spline_cv3) .at("SKETCH", "CREATE") .icon("spline_cv") .enabled(in_sketch) .params("points: control points (2…500), cubic"), - CommandSpec::new("DrawCVMSpline5D", "Control Point Spline (Degree 5)", spline_cv5) + CommandSpec::new("sketch.spline.control_point_5", "Control Point Spline (Degree 5)", spline_cv5) .at("SKETCH", "CREATE") .icon("spline_cv") .enabled(in_sketch) .params("points: control points (2…500), degree 5"), - CommandSpec::new("ConicCurveCmd", "Conic Curve", conic) + CommandSpec::new("sketch.conic", "Conic Curve", conic) .at("SKETCH", "CREATE") .icon("conic") .enabled(in_sketch) .params("start, end, apex: [x,y] or point refs; rho?: 0…1 (default 0.5: parabola)"), - CommandSpec::new("BlendG1CurveSketchCmd", "Blend Curve", blend) + CommandSpec::new("sketch.blend_curve", "Blend Curve", blend) .at("SKETCH", "MODIFY") .icon("blend") .enabled(in_sketch) @@ -78,7 +78,7 @@ pub static COMMANDS: &[CommandSpec] = CommandSpec::new("sketch.centerline", "Centerline", centerline) .enabled(in_sketch) .params("curves: [line ids], value?: bool (default toggles): centerline line type (not in profiles; an axis)"), - CommandSpec::new("MTextCmd", "Text", text) + CommandSpec::new("sketch.text", "Text", text) .at("SKETCH", "CREATE") .icon("text") .enabled(in_sketch) @@ -107,7 +107,7 @@ fn text_source(p: &Value, cmd: &str, old: Option<(String, Vec2, f64, f64)>) -> R } fn text(s: &mut Session, p: &Value) -> Result { - let cmd = "MTextCmd"; + let cmd = "sketch.text"; if p.get("at").is_none() { return Err(bad(cmd, "`at` must be the baseline start [x, y]")); } @@ -153,7 +153,7 @@ fn end_of(sk: &Sketch, pi: usize) -> Option<(usize, Vec2)> { } fn blend(s: &mut Session, p: &Value) -> Result { - let cmd = "BlendG1CurveSketchCmd"; + let cmd = "sketch.blend_curve"; let (out, info) = edit(s, p, cmd, false, |sk, _| { let pa = req_parg(sk, p, "a", cmd)?.idx().ok_or_else(|| bad(cmd, "`a` must be the end point of a curve"))?; let pb = req_parg(sk, p, "b", cmd)?.idx().ok_or_else(|| bad(cmd, "`b` must be the end point of a curve"))?; @@ -200,7 +200,7 @@ fn centerline(s: &mut Session, p: &Value) -> Result { } fn ellipse(s: &mut Session, p: &Value) -> Result { - let cmd = "CircleElipse"; + let cmd = "sketch.ellipse"; let (c, m) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "major")?); let u = (m - c).normalized().ok_or_else(|| bad(cmd, "the major axis end must differ from the centre"))?; let a = c.dist(m); @@ -263,17 +263,17 @@ fn spline(s: &mut Session, p: &Value, cmd: &str, control: bool, degree: u8) -> R } fn spline_fit(s: &mut Session, p: &Value) -> Result { - spline(s, p, "DrawSpline", false, 3) + spline(s, p, "sketch.spline.fit_point", false, 3) } fn spline_cv3(s: &mut Session, p: &Value) -> Result { - spline(s, p, "DrawCVMSpline3D", true, 3) + spline(s, p, "sketch.spline.control_point", true, 3) } fn spline_cv5(s: &mut Session, p: &Value) -> Result { - spline(s, p, "DrawCVMSpline5D", true, 5) + spline(s, p, "sketch.spline.control_point_5", true, 5) } fn conic(s: &mut Session, p: &Value) -> Result { - let cmd = "ConicCurveCmd"; + let cmd = "sketch.conic"; let rho = num(p, "rho").unwrap_or(0.5); if !(rho > 1e-6 && rho < 1.0 - 1e-6) { return Err(bad(cmd, "`rho` must be between 0 and 1")); @@ -300,7 +300,7 @@ fn conic(s: &mut Session, p: &Value) -> Result { } fn midpoint_line(s: &mut Session, p: &Value) -> Result { - let cmd = "SketchMidpointLine"; + let cmd = "sketch.line.midpoint"; let end = req_vec2(cmd, p, "end")?; let (out, info) = edit(s, p, cmd, false, |sk, _| { let m = req_parg(sk, p, "mid", cmd)?; @@ -369,7 +369,7 @@ fn leaving_dir(sk: &Sketch, ci: usize, pi: usize) -> Option { } fn arc_tangent(s: &mut Session, p: &Value) -> Result { - let cmd = "ArcTangent"; + let cmd = "sketch.arc.tangent"; let end = req_vec2(cmd, p, "end")?; let (out, info) = edit(s, p, cmd, false, |sk, _| { let st = req_parg(sk, p, "start", cmd)?; @@ -458,7 +458,7 @@ fn tangent_circle(s: &mut Session, p: &Value, cmd: &str, n: usize, radius: Optio } fn circle_tan_tan(s: &mut Session, p: &Value) -> Result { - let cmd = "CircleTanTanRadius"; + let cmd = "sketch.circle.two_tangent"; let r = match (crate::params::expr(p, "radius"), num(p, "diameter")) { (Some(r), _) => r, (None, Some(d)) => format!("{}", d / 2.0), @@ -468,7 +468,7 @@ fn circle_tan_tan(s: &mut Session, p: &Value) -> Result { } fn circle_three_tan(s: &mut Session, p: &Value) -> Result { - tangent_circle(s, p, "CircleThreeTangent", 3, None) + tangent_circle(s, p, "sketch.circle.three_tangent", 3, None) } fn width(p: &Value, cmd: &str) -> Result { @@ -476,7 +476,7 @@ fn width(p: &Value, cmd: &str) -> Result { } fn slot_center_point(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeSlotCenterPoint"; + let cmd = "sketch.slot.center_point"; let (c, e) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "end")?); let w = width(p, cmd)?; slot(s, p, cmd, c * 2.0 - e, e, w) @@ -518,7 +518,7 @@ fn arc_slot(s: &mut Session, p: &Value, cmd: &str, o: Vec2, r: f64, t0: f64, swe } fn arc_slot_three(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeArcSlotThreePoint"; + let cmd = "sketch.slot.arc_three_point"; let (a, t, b) = (req_vec2(cmd, p, "start")?, req_vec2(cmd, p, "through")?, req_vec2(cmd, p, "end")?); let w = width(p, cmd)?; let (o, r) = circumcircle(a, t, b).ok_or_else(|| bad(cmd, "the points are collinear"))?; @@ -530,7 +530,7 @@ fn arc_slot_three(s: &mut Session, p: &Value) -> Result { } fn arc_slot_center(s: &mut Session, p: &Value) -> Result { - let cmd = "ShapeArcSlotCenterTwoPoint"; + let cmd = "sketch.slot.arc_center"; let (o, a, b) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "start")?, req_vec2(cmd, p, "end")?); let w = width(p, cmd)?; let r = o.dist(a); diff --git a/crates/engine/src/cmd/sketch_fuzz_tests.rs b/crates/engine/src/cmd/sketch_fuzz_tests.rs index 17e5a726..4d76cc81 100644 --- a/crates/engine/src/cmd/sketch_fuzz_tests.rs +++ b/crates/engine/src/cmd/sketch_fuzz_tests.rs @@ -44,53 +44,53 @@ fn random_sketch_editing_never_breaks() { for seed in 1..=2u64 { let mut r = Rng(0x9E37_79B9_7F4A_7C15 ^ seed); let mut s = Session::default(); - let _ = s.execute("PrimitiveBox", &json!({"length": 30, "width": 20, "height": 10})); - let _ = s.execute("SketchCreate", &json!({"plane": {"face": [15, 10, 10]}})); + let _ = s.execute("solid.box", &json!({"length": 30, "width": 20, "height": 10})); + let _ = s.execute("sketch.create", &json!({"plane": {"face": [15, 10, 10]}})); for step in 0..120 { let (curves, points, cons) = entities(&s); let c = |r: &mut Rng| r.pick(&curves).cloned().unwrap_or_default(); let p = |r: &mut Rng| r.pick(&points).cloned().unwrap_or_default(); let (cmd, params) = match r.next() % 34 { - 0 => ("DrawPolyline", json!({"points": [r.pt(), r.pt(), r.pt()], "infer": true})), - 1 => ("CircleCenterRadius", json!({"center": r.pt(), "radius": r.f(0.5, 20.0)})), - 2 => ("ArcCenterTwoPoint", json!({"center": r.pt(), "start": r.pt(), "end": r.pt()})), - 3 => ("ShapeRectangleTwoPoint", json!({"p0": r.pt(), "p1": r.pt()})), - 4 => ("DrawSpline", json!({"points": [r.pt(), r.pt(), r.pt(), r.pt()]})), - 5 => ("CircleElipse", json!({"center": r.pt(), "major": r.pt(), "minor_radius": r.f(0.5, 10.0)})), - 6 => ("ConicCurveCmd", json!({"start": r.pt(), "end": r.pt(), "apex": r.pt(), "rho": r.f(0.1, 0.9)})), - 7 => ("TrimSketchCmd", json!({"curve": c(&mut r), "at": r.pt()})), - 8 => ("ExtendSketchCmd", json!({"curve": c(&mut r), "at": r.pt()})), - 9 => ("BreakSketchCmd", json!({"curve": c(&mut r), "at": r.pt()})), - 10 => ("FilletSketchCmd", json!({"point": p(&mut r), "radius": r.f(0.1, 5.0)})), - 11 => ("ChamferSketchEqualDistance", json!({"point": p(&mut r), "distance": r.f(0.1, 5.0)})), - 12 => ("Offset", json!({"curves": [c(&mut r)], "distance": r.f(-5.0, 5.0), "side": r.pt()})), - 13 => ("MirrorSketchCommand", json!({"entities": [c(&mut r)], "line": c(&mut r)})), - 14 => ("CircularSketchPatternCommand", json!({"entities": [c(&mut r)], "center": r.pt(), "count": 3})), - 15 => ("ConstraintCoincident", json!({"a": p(&mut r), "b": p(&mut r)})), - 16 => ("ConstraintTangent", json!({"a": c(&mut r), "b": c(&mut r)})), - 17 => ("ConstraintParallel", json!({"a": c(&mut r), "b": c(&mut r)})), - 18 => ("ConstraintSmooth", json!({"a": c(&mut r), "b": c(&mut r)})), - 19 => ("SketchDimension", json!({"entities": [c(&mut r)]})), - 20 => ("SketchDimension", json!({"entities": [p(&mut r), p(&mut r)], "driven": r.next().is_multiple_of(2)})), + 0 => ("sketch.line", json!({"points": [r.pt(), r.pt(), r.pt()], "infer": true})), + 1 => ("sketch.circle.center", json!({"center": r.pt(), "radius": r.f(0.5, 20.0)})), + 2 => ("sketch.arc.center_point", json!({"center": r.pt(), "start": r.pt(), "end": r.pt()})), + 3 => ("sketch.rectangle.two_point", json!({"p0": r.pt(), "p1": r.pt()})), + 4 => ("sketch.spline.fit_point", json!({"points": [r.pt(), r.pt(), r.pt(), r.pt()]})), + 5 => ("sketch.ellipse", json!({"center": r.pt(), "major": r.pt(), "minor_radius": r.f(0.5, 10.0)})), + 6 => ("sketch.conic", json!({"start": r.pt(), "end": r.pt(), "apex": r.pt(), "rho": r.f(0.1, 0.9)})), + 7 => ("sketch.trim", json!({"curve": c(&mut r), "at": r.pt()})), + 8 => ("sketch.extend", json!({"curve": c(&mut r), "at": r.pt()})), + 9 => ("sketch.break", json!({"curve": c(&mut r), "at": r.pt()})), + 10 => ("sketch.fillet", json!({"point": p(&mut r), "radius": r.f(0.1, 5.0)})), + 11 => ("sketch.chamfer.equal_distance", json!({"point": p(&mut r), "distance": r.f(0.1, 5.0)})), + 12 => ("sketch.offset", json!({"curves": [c(&mut r)], "distance": r.f(-5.0, 5.0), "side": r.pt()})), + 13 => ("sketch.mirror", json!({"entities": [c(&mut r)], "line": c(&mut r)})), + 14 => ("sketch.pattern.circular", json!({"entities": [c(&mut r)], "center": r.pt(), "count": 3})), + 15 => ("sketch.constraint.coincident", json!({"a": p(&mut r), "b": p(&mut r)})), + 16 => ("sketch.constraint.tangent", json!({"a": c(&mut r), "b": c(&mut r)})), + 17 => ("sketch.constraint.parallel", json!({"a": c(&mut r), "b": c(&mut r)})), + 18 => ("sketch.constraint.curvature", json!({"a": c(&mut r), "b": c(&mut r)})), + 19 => ("sketch.dimension", json!({"entities": [c(&mut r)]})), + 20 => ("sketch.dimension", json!({"entities": [p(&mut r), p(&mut r)], "driven": r.next().is_multiple_of(2)})), 21 => ("sketch.delete", json!({"entities": [r.pick(&cons).cloned().unwrap_or_else(|| c(&mut r))]})), 22 => ("sketch.move_point", json!({"point": p(&mut r), "to": r.pt()})), - 23 => ("ProjectNewCmd", json!({"refs": [{"edge": [r.f(0.0, 30.0), 0.0, 10.0]}]})), + 23 => ("sketch.project", json!({"refs": [{"edge": [r.f(0.0, 30.0), 0.0, 10.0]}]})), 24 => ("sketch.break_link", json!({"entities": [c(&mut r)]})), - 25 => ("SketchConstrainer", json!({"entities": [c(&mut r), c(&mut r)]})), - 26 => ("SketchAutoConstraintAndDimCmd", json!({})), - 27 => ("MTextCmd", json!({"text": "Ab", "at": r.pt(), "height": r.f(1.0, 8.0)})), + 25 => ("sketch.constrainer", json!({"entities": [c(&mut r), c(&mut r)]})), + 26 => ("sketch.auto_constrain", json!({})), + 27 => ("sketch.text", json!({"text": "Ab", "at": r.pt(), "height": r.f(1.0, 8.0)})), 28 => ("sketch.toggle_driven", json!({"constraint": r.pick(&cons).cloned().unwrap_or_default()})), - 29 => ("ArcTangent", json!({"start": format!("{}.end", c(&mut r)), "end": r.pt()})), - 30 => ("SketchScaleCmd", json!({"entities": [c(&mut r)], "base": r.pt(), "factor": r.f(0.5, 2.0)})), - 31 => ("UndoCommand", json!({})), - 32 => ("ShapeArcSlotThreePoint", json!({"start": r.pt(), "through": r.pt(), "end": r.pt(), "width": r.f(0.5, 4.0)})), - _ => ("RedoCommand", json!({})), + 29 => ("sketch.arc.tangent", json!({"start": format!("{}.end", c(&mut r)), "end": r.pt()})), + 30 => ("sketch.scale", json!({"entities": [c(&mut r)], "base": r.pt(), "factor": r.f(0.5, 2.0)})), + 31 => ("edit.undo", json!({})), + 32 => ("sketch.slot.arc_three_point", json!({"start": r.pt(), "through": r.pt(), "end": r.pt(), "width": r.f(0.5, 4.0)})), + _ => ("edit.redo", json!({})), }; if let Err(EngineError::Internal(id, msg)) = s.execute(cmd, ¶ms) { panic!("seed {seed} step {step}: {id} {params}: {msg}"); } if s.active_sketch.is_none() { - let _ = s.execute("SketchCreate", &json!({"plane": "XY"})); + let _ = s.execute("sketch.create", &json!({"plane": "XY"})); } } // The design round-trips. diff --git a/crates/engine/src/cmd/sketch_import.rs b/crates/engine/src/cmd/sketch_import.rs index fd405b0d..381391db 100644 --- a/crates/engine/src/cmd/sketch_import.rs +++ b/crates/engine/src/cmd/sketch_import.rs @@ -13,11 +13,11 @@ use crate::params::{bad, num, str_, vec2}; use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("ImportDxfFileCommand", "Insert DXF", insert_dxf) + CommandSpec::new("sketch.insert_dxf", "Insert DXF", insert_dxf) .at("SKETCH", "INSERT") .icon("dxf") .params("path | text: ASCII DXF (blocks expanded); at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)"), - CommandSpec::new("SketchImportSVG", "Insert SVG", insert_svg) + CommandSpec::new("sketch.insert_svg", "Insert SVG", insert_svg) .at("SKETCH", "INSERT") .icon("svg") .params("path | text: SVG; at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY)"), @@ -123,7 +123,7 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result { if let Some(n) = str_(p, "name") { cp["name"] = json!(n); } - let r = (super::find_command("SketchCreate").ok_or_else(|| bad(cmd, "SketchCreate"))?.run)(s, &cp)?; + let r = (super::find_command("sketch.create").ok_or_else(|| bad(cmd, "sketch.create"))?.run)(s, &cp)?; created = r.get("sketch").and_then(Value::as_u64); } let tol = num(p, "tolerance").filter(|t| t.is_finite() && *t > 0.0).unwrap_or(1e-4).clamp(1e-9, 10.0); @@ -150,11 +150,11 @@ fn insert(s: &mut Session, p: &Value, cmd: &str, svg: bool) -> Result { } fn insert_dxf(s: &mut Session, p: &Value) -> Result { - insert(s, p, "ImportDxfFileCommand", false) + insert(s, p, "sketch.insert_dxf", false) } fn insert_svg(s: &mut Session, p: &Value) -> Result { - insert(s, p, "SketchImportSVG", true) + insert(s, p, "sketch.insert_svg", true) } fn export_dxf(s: &mut Session, p: &Value) -> Result { @@ -201,14 +201,14 @@ mod tests { #[test] fn dxf_and_svg_round_trip_into_sketches() { let mut s = Session::default(); - s.execute("SketchCreate", &json!({"plane": "XY"})).unwrap(); - s.execute("ShapeRectangleTwoPoint", &json!({"p0": [0, 0], "p1": [30, 20]})).unwrap(); - s.execute("CircleCenterRadius", &json!({"center": [10, 10], "radius": 4})).unwrap(); + s.execute("sketch.create", &json!({"plane": "XY"})).unwrap(); + s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [30, 20]})).unwrap(); + s.execute("sketch.circle.center", &json!({"center": [10, 10], "radius": 4})).unwrap(); let out = s.execute("sketch.export_dxf", &json!({})).unwrap(); let dxf = out["dxf"].as_str().unwrap().to_string(); - s.execute("SketchStop", &json!({})).unwrap(); + s.execute("sketch.finish", &json!({})).unwrap(); // Insert it again as a new sketch, offset: same profiles. - let r = s.execute("ImportDxfFileCommand", &json!({"text": dxf, "at": [100, 0]})).unwrap(); + let r = s.execute("sketch.insert_dxf", &json!({"text": dxf, "at": [100, 0]})).unwrap(); assert_eq!(r["curves"], 5, "{r}"); let id = r["sketch"].as_u64().unwrap(); let ss = s.model.state().sketch(id).unwrap().clone(); @@ -219,19 +219,19 @@ mod tests { // SVG with a path and text, into the active sketch. let svg = r#"Hi"#; - let r = s.execute("SketchImportSVG", &json!({"text": svg})).unwrap(); + let r = s.execute("sketch.insert_svg", &json!({"text": svg})).unwrap(); assert_eq!(r["curves"], 4, "{r}"); assert_eq!(r["texts"], 1, "{r}"); - assert!(s.execute("SketchImportSVG", &json!({"text": ""})).is_err()); - assert!(s.execute("ImportDxfFileCommand", &json!({"path": "/nonexistent.dxf"})).is_err()); - s.execute("SketchStop", &json!({})).unwrap(); + assert!(s.execute("sketch.insert_svg", &json!({"text": ""})).is_err()); + assert!(s.execute("sketch.insert_dxf", &json!({"path": "/nonexistent.dxf"})).is_err()); + s.execute("sketch.finish", &json!({})).unwrap(); // Ends that miss by 0.00005 still close the square; with a tighter tolerance they don't. let gappy = "0\nSECTION\n2\nENTITIES\n0\nLINE\n10\n0\n20\n0\n11\n10\n21\n0\n0\nLINE\n10\n10.00005\n20\n0\n11\n10\n21\n10\n0\nLINE\n10\n10\n20\n10\n11\n0\n21\n10\n0\nLINE\n10\n0\n20\n10.00003\n11\n0\n21\n0.00004\n0\nENDSEC\n0\nEOF\n"; for (tol, n) in [(1e-4, 1), (1e-6, 0)] { - let r = s.execute("ImportDxfFileCommand", &json!({"text": gappy, "tolerance": tol})).unwrap(); + let r = s.execute("sketch.insert_dxf", &json!({"text": gappy, "tolerance": tol})).unwrap(); let ss = s.model.state().sketch(r["sketch"].as_u64().unwrap()).unwrap().clone(); assert_eq!(ss.profiles.len(), n, "tolerance {tol}"); - s.execute("SketchStop", &json!({})).unwrap(); + s.execute("sketch.finish", &json!({})).unwrap(); } } } diff --git a/crates/engine/src/cmd/sketch_inspect.rs b/crates/engine/src/cmd/sketch_inspect.rs index def5c9c5..7a112ff8 100644 --- a/crates/engine/src/cmd/sketch_inspect.rs +++ b/crates/engine/src/cmd/sketch_inspect.rs @@ -9,22 +9,22 @@ use crate::params::{bad, num, string_list, vec3}; use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("FusionCurvatureCombAnalysisCommand", "Curvature Comb Analysis", curvature_comb) + CommandSpec::new("inspect.curvature_comb", "Curvature Comb Analysis", curvature_comb) .at("SKETCH", "INSPECT") .icon("curvature_comb") .noundo() .params("curves?: [sketch curve ids] (sketch?: id|name, default active) | edges?: [[x,y,z] points on body edges]; density?: teeth per curve (default 40), scale?: comb length per unit curvature (default auto)"), - CommandSpec::new("FusionMinimumRadiusAnalysisCommand", "Minimum Radius Analysis", minimum_radius) + CommandSpec::new("inspect.minimum_radius", "Minimum Radius Analysis", minimum_radius) .at("SKETCH", "INSPECT") .icon("min_radius") .noundo() .params("curves?: [sketch curve ids] (sketch?) | edges?: [[x,y,z]…] | faces?: [[x,y,z]…] (default: every face of every body); the smallest radius of curvature of each and where it is"), - CommandSpec::new("FusionIsoCurveAnalysisCommand", "Isocurve Analysis", isocurves) + CommandSpec::new("inspect.isocurve", "Isocurve Analysis", isocurves) .at("SKETCH", "INSPECT") .icon("iso_analysis") .noundo() .params("faces: [[x,y,z]…] points on faces; count?: curves each way (default 8): the faces' isoparametric curves"), - CommandSpec::new("FusionCenterOfMassCommand", "Center of Mass", center_of_mass) + CommandSpec::new("inspect.center_of_mass", "Center of Mass", center_of_mass) .at("SKETCH", "INSPECT") .icon("center_of_mass") .noundo() @@ -73,7 +73,7 @@ fn teeth_3d(pts: &[Vec3]) -> Vec<(Vec3, f64, Vec3)> { } fn curvature_comb(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionCurvatureCombAnalysisCommand"; + let cmd = "inspect.curvature_comb"; let density = num(p, "density").map(|d| d.clamp(2.0, 2000.0) as usize).unwrap_or(40); let st = s.model.state(); let mut combs: Vec<(String, Vec<(Vec3, f64, Vec3)>)> = Vec::new(); @@ -210,7 +210,7 @@ fn face_min_radius(m: &solvecraft_geom::Mesh, face: u32) -> Option<(f64, Vec3)> } fn minimum_radius(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionMinimumRadiusAnalysisCommand"; + let cmd = "inspect.minimum_radius"; let st = s.model.state(); let mut items: Vec<(String, Option<(f64, Vec3)>)> = Vec::new(); let curves = string_list(p, "curves"); @@ -282,7 +282,7 @@ fn minimum_radius(s: &mut Session, p: &Value) -> Result { } fn isocurves(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionIsoCurveAnalysisCommand"; + let cmd = "inspect.isocurve"; let count = num(p, "count").map(|c| c.clamp(1.0, 100.0) as usize).unwrap_or(8); let faces = p.get("faces").and_then(Value::as_array).ok_or_else(|| bad(cmd, "`faces` must list points [x,y,z]"))?; if faces.is_empty() { @@ -302,7 +302,7 @@ fn isocurves(s: &mut Session, p: &Value) -> Result { } fn center_of_mass(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionCenterOfMassCommand"; + let cmd = "inspect.center_of_mass"; let names = string_list(p, "bodies"); let st = s.model.state(); let mut out = Vec::new(); @@ -332,10 +332,10 @@ mod tests { #[test] fn curvature_combs() { let mut s = Session::default(); - s.execute("SketchCreate", &json!({"plane": "XY"})).unwrap(); - let c = s.execute("CircleCenterRadius", &json!({"center": [0, 0], "radius": 5})).unwrap()["curves"][0].clone(); - let l = s.execute("DrawPolyline", &json!({"points": [[0, 20], [10, 20]]})).unwrap()["curves"][0].clone(); - let r = s.execute("FusionCurvatureCombAnalysisCommand", &json!({"curves": [c, l], "density": 20})).unwrap(); + s.execute("sketch.create", &json!({"plane": "XY"})).unwrap(); + let c = s.execute("sketch.circle.center", &json!({"center": [0, 0], "radius": 5})).unwrap()["curves"][0].clone(); + let l = s.execute("sketch.line", &json!({"points": [[0, 20], [10, 20]]})).unwrap()["curves"][0].clone(); + let r = s.execute("inspect.curvature_comb", &json!({"curves": [c, l], "density": 20})).unwrap(); let circle = &r["combs"][0]; assert!((circle["min_radius"].as_f64().unwrap() - 5.0).abs() < 1e-3, "{circle}"); assert!(r["combs"][1]["min_radius"].is_null(), "a line is straight"); @@ -343,39 +343,39 @@ mod tests { let t = &circle["teeth"][3]; let (a, b) = ((t[0][0].as_f64().unwrap(), t[0][1].as_f64().unwrap()), (t[1][0].as_f64().unwrap(), t[1][1].as_f64().unwrap())); assert!(b.0 * b.0 + b.1 * b.1 > a.0 * a.0 + a.1 * a.1); - s.execute("SketchStop", &json!({})).unwrap(); - s.execute("PrimitiveCylinder", &json!({"base": [50, 0, 0], "radius": 8, "height": 10})).unwrap(); - let r = s.execute("FusionCurvatureCombAnalysisCommand", &json!({"edges": [[58, 0, 10]]})).unwrap(); + s.execute("sketch.finish", &json!({})).unwrap(); + s.execute("solid.cylinder", &json!({"base": [50, 0, 0], "radius": 8, "height": 10})).unwrap(); + let r = s.execute("inspect.curvature_comb", &json!({"edges": [[58, 0, 10]]})).unwrap(); assert!((r["combs"][0]["min_radius"].as_f64().unwrap() - 8.0).abs() < 0.5, "{r}"); - assert!(s.execute("FusionCurvatureCombAnalysisCommand", &json!({})).is_err()); + assert!(s.execute("inspect.curvature_comb", &json!({})).is_err()); } #[test] fn minimum_radius_and_center_of_mass() { let mut s = Session::default(); - s.execute("SketchCreate", &json!({"plane": "XY"})).unwrap(); - let e = s.execute("CircleElipse", &json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4})).unwrap()["curves"][0].clone(); + s.execute("sketch.create", &json!({"plane": "XY"})).unwrap(); + let e = s.execute("sketch.ellipse", &json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4})).unwrap()["curves"][0].clone(); // An ellipse's tightest bend is at the major axis ends: b²/a. - let r = s.execute("FusionMinimumRadiusAnalysisCommand", &json!({"curves": [e]})).unwrap(); + let r = s.execute("inspect.minimum_radius", &json!({"curves": [e]})).unwrap(); assert!((r["min_radius"].as_f64().unwrap() - 1.6).abs() < 1e-3, "{r}"); assert!((r["at"][0].as_f64().unwrap().abs() - 10.0).abs() < 0.1, "{r}"); - s.execute("SketchStop", &json!({})).unwrap(); - s.execute("PrimitiveCylinder", &json!({"base": [50, 0, 0], "radius": 8, "height": 10})).unwrap(); + s.execute("sketch.finish", &json!({})).unwrap(); + s.execute("solid.cylinder", &json!({"base": [50, 0, 0], "radius": 8, "height": 10})).unwrap(); // The cylinder's side face: radius 8 (the mesh estimate is close). - let r = s.execute("FusionMinimumRadiusAnalysisCommand", &json!({"faces": [[58, 0, 5]]})).unwrap(); + let r = s.execute("inspect.minimum_radius", &json!({"faces": [[58, 0, 5]]})).unwrap(); assert!((r["min_radius"].as_f64().unwrap() - 8.0).abs() < 0.4, "{r}"); - let r = s.execute("FusionMinimumRadiusAnalysisCommand", &json!({})).unwrap(); + let r = s.execute("inspect.minimum_radius", &json!({})).unwrap(); assert!((r["min_radius"].as_f64().unwrap() - 8.0).abs() < 0.4, "{r}"); // Centre of mass: a box beside the cylinder. - s.execute("PrimitiveBox", &json!({"corner": [0, 0, 0], "length": 10, "width": 10, "height": 10})).unwrap(); - let r = s.execute("FusionCenterOfMassCommand", &json!({})).unwrap(); + s.execute("solid.box", &json!({"corner": [0, 0, 0], "length": 10, "width": 10, "height": 10})).unwrap(); + let r = s.execute("inspect.center_of_mass", &json!({})).unwrap(); let (vb, vc) = (1000.0, std::f64::consts::PI * 640.0); let want = (5.0 * vb + 50.0 * vc) / (vb + vc); assert!((r["center"][0].as_f64().unwrap() - want).abs() < 0.1, "{r}"); assert!((r["center"][2].as_f64().unwrap() - 5.0).abs() < 0.05, "{r}"); - assert!(s.execute("FusionCenterOfMassCommand", &json!({"bodies": ["nope"]})).is_err()); + assert!(s.execute("inspect.center_of_mass", &json!({"bodies": ["nope"]})).is_err()); // Isocurves on the cylinder's side: rings around it and lines along it. - let r = s.execute("FusionIsoCurveAnalysisCommand", &json!({"faces": [[58, 0, 5]], "count": 4})).unwrap(); + let r = s.execute("inspect.isocurve", &json!({"faces": [[58, 0, 5]], "count": 4})).unwrap(); let curves = r["curves"].as_array().unwrap(); let ring = |c: &Value| c.as_array().unwrap().iter().all(|q| ((q[0].as_f64().unwrap() - 50.0).hypot(q[1].as_f64().unwrap()) - 8.0).abs() < 0.2); @@ -386,6 +386,6 @@ mod tests { z.iter().cloned().fold(f64::NEG_INFINITY, f64::max) - z.iter().cloned().fold(f64::INFINITY, f64::min) < 1e-6 }; assert!(curves.iter().filter(|c| flat(c)).count() >= 4, "rings at constant height"); - assert!(s.execute("FusionIsoCurveAnalysisCommand", &json!({"faces": []})).is_err()); + assert!(s.execute("inspect.isocurve", &json!({"faces": []})).is_err()); } } diff --git a/crates/engine/src/cmd/sketch_modify.rs b/crates/engine/src/cmd/sketch_modify.rs index 8d0b488e..669fddc1 100644 --- a/crates/engine/src/cmd/sketch_modify.rs +++ b/crates/engine/src/cmd/sketch_modify.rs @@ -13,49 +13,49 @@ use crate::params::{bad, bool_, expr, num, req_vec2, str_, string_list, vec2}; use crate::{Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("FilletSketchCmd", "Fillet", fillet) + CommandSpec::new("sketch.fillet", "Fillet", fillet) .at("SKETCH", "MODIFY") .icon("sketch_fillet") .enabled(in_sketch) .params("point: corner shared by two curves (\"l1.end\") | a, b: two lines or arcs meeting at a corner; radius: expr"), - CommandSpec::new("ChamferSketchEqualDistance", "Equal Distance Chamfer", chamfer_equal) + CommandSpec::new("sketch.chamfer.equal_distance", "Equal Distance Chamfer", chamfer_equal) .at("SKETCH", "MODIFY") .icon("sketch_chamfer") .enabled(in_sketch) .params("point | a, b: a corner of two lines; distance"), - CommandSpec::new("ChamferSketchDistanceAngle", "Distance and Angle Chamfer", chamfer_dist_angle) + CommandSpec::new("sketch.chamfer.distance_angle", "Distance and Angle Chamfer", chamfer_dist_angle) .at("SKETCH", "MODIFY") .icon("sketch_chamfer") .enabled(in_sketch) .params("point | a, b: a corner of two lines; distance: along a, angle: deg from a"), - CommandSpec::new("ChamferSketchDistanceDistance", "Two Distance Chamfer", chamfer_two) + CommandSpec::new("sketch.chamfer.two_distance", "Two Distance Chamfer", chamfer_two) .at("SKETCH", "MODIFY") .icon("sketch_chamfer") .enabled(in_sketch) .params("point | a, b: a corner of two lines; distance: along a, distance2: along b"), - CommandSpec::new("Offset", "Offset", offset) + CommandSpec::new("sketch.offset", "Offset", offset) .at("SKETCH", "MODIFY") .icon("offset") .key("O") .enabled(in_sketch) .params("curves: [ids], distance: number (positive = left of the chain, or toward `side`), side?: [x,y], chain?: bool (default true: the connected chain)"), - CommandSpec::new("TrimSketchCmd", "Trim", trim) + CommandSpec::new("sketch.trim", "Trim", trim) .at("SKETCH", "MODIFY") .icon("trim") .key("T") .enabled(in_sketch) .params("curve: id, at: [x,y] (the piece to remove: between the nearest intersections around it)"), - CommandSpec::new("ExtendSketchCmd", "Extend", extend) + CommandSpec::new("sketch.extend", "Extend", extend) .at("SKETCH", "MODIFY") .icon("extend") .enabled(in_sketch) .params("curve: id, at: [x,y] near the end to extend (to the nearest curve)"), - CommandSpec::new("BreakSketchCmd", "Break", break_cmd) + CommandSpec::new("sketch.break", "Break", break_cmd) .at("SKETCH", "MODIFY") .icon("break") .enabled(in_sketch) .params("curve: id, at: [x,y] (split at the nearest intersections around it) | params: split at points: [[x,y]…]"), - CommandSpec::new("SketchScaleCmd", "Sketch Scale", scale) + CommandSpec::new("sketch.scale", "Sketch Scale", scale) .at("SKETCH", "MODIFY") .icon("scale") .enabled(in_sketch) @@ -63,17 +63,17 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("sketch.move", "Move/Copy Sketch Entities", move_copy) .enabled(in_sketch) .params("entities: [ids], translate?: [dx,dy], angle?: deg, center?: [x,y], copy?: bool"), - CommandSpec::new("MirrorSketchCommand", "Mirror", mirror) + CommandSpec::new("sketch.mirror", "Mirror", mirror) .at("SKETCH", "CREATE") .icon("sketch_mirror") .enabled(in_sketch) .params("entities: [curve/point ids], line: mirror line id (symmetry constraints are added)"), - CommandSpec::new("CircularSketchPatternCommand", "Circular Pattern", pattern_circular) + CommandSpec::new("sketch.pattern.circular", "Circular Pattern", pattern_circular) .at("SKETCH", "CREATE") .icon("sketch_circ_pattern") .enabled(in_sketch) .params("entities: [ids], center: [x,y] or point ref, count: 2…500, angle?: total deg (default 360)"), - CommandSpec::new("RectangularSketchPatternCommand", "Rectangular Pattern", pattern_rect) + CommandSpec::new("sketch.pattern.rectangular", "Rectangular Pattern", pattern_rect) .at("SKETCH", "CREATE") .icon("sketch_rect_pattern") .enabled(in_sketch) @@ -253,7 +253,7 @@ fn bracket(sk: &Sketch, ci: usize, tq: f64) -> (Vec<(f64, Vec2, usize)>, bool) { } fn trim(s: &mut Session, p: &Value) -> Result { - let cmd = "TrimSketchCmd"; + let cmd = "sketch.trim"; let at = req_vec2(cmd, p, "at")?; let (out, info) = edit(s, p, cmd, false, |sk, doc| { let ci = curve_arg(sk, p, cmd)?; @@ -313,7 +313,7 @@ fn trim(s: &mut Session, p: &Value) -> Result { } fn break_cmd(s: &mut Session, p: &Value) -> Result { - let cmd = "BreakSketchCmd"; + let cmd = "sketch.break"; let (out, info) = edit(s, p, cmd, false, |sk, _| { let ci = curve_arg(sk, p, cmd)?; if sk.curves.get(ci).is_some_and(|c| c.kind.is_freeform()) { @@ -349,7 +349,7 @@ fn break_cmd(s: &mut Session, p: &Value) -> Result { } fn extend(s: &mut Session, p: &Value) -> Result { - let cmd = "ExtendSketchCmd"; + let cmd = "sketch.extend"; let at = req_vec2(cmd, p, "at")?; let (out, info) = edit(s, p, cmd, false, |sk, _| { let ci = curve_arg(sk, p, cmd)?; @@ -476,7 +476,7 @@ fn cut_corner(sk: &mut Sketch, la: usize, lb: usize, c: usize, fa: usize, fb: us } fn fillet(s: &mut Session, p: &Value) -> Result { - let cmd = "FilletSketchCmd"; + let cmd = "sketch.fillet"; let r_expr = expr(p, "radius").ok_or_else(|| bad(cmd, "`radius` must be a number or an expression"))?; let (out, info) = edit(s, p, cmd, false, |sk, doc| { let r = doc.eval(&r_expr, solvecraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("radius: {e}")))?; @@ -610,20 +610,20 @@ fn chamfer(s: &mut Session, p: &Value, cmd: &str, dists: impl FnOnce(f64) -> Res } fn chamfer_equal(s: &mut Session, p: &Value) -> Result { - let cmd = "ChamferSketchEqualDistance"; + let cmd = "sketch.chamfer.equal_distance"; let d = num(p, "distance").ok_or_else(|| bad(cmd, "`distance` must be a number"))?; chamfer(s, p, cmd, |_| Ok((d, d))) } fn chamfer_two(s: &mut Session, p: &Value) -> Result { - let cmd = "ChamferSketchDistanceDistance"; + let cmd = "sketch.chamfer.two_distance"; let d = num(p, "distance").ok_or_else(|| bad(cmd, "`distance` must be a number"))?; let d2 = num(p, "distance2").ok_or_else(|| bad(cmd, "`distance2` must be a number"))?; chamfer(s, p, cmd, |_| Ok((d, d2))) } fn chamfer_dist_angle(s: &mut Session, p: &Value) -> Result { - let cmd = "ChamferSketchDistanceAngle"; + let cmd = "sketch.chamfer.distance_angle"; let d = num(p, "distance").ok_or_else(|| bad(cmd, "`distance` must be a number"))?; let a = num(p, "angle").ok_or_else(|| bad(cmd, "`angle` must be a number (degrees)"))?.to_radians(); chamfer(s, p, cmd, |phi| { @@ -766,7 +766,7 @@ fn piece_dist(p: &Piece, q: Vec2) -> f64 { } fn offset(s: &mut Session, p: &Value) -> Result { - let cmd = "Offset"; + let cmd = "sketch.offset"; let ids = string_list(p, "curves"); if ids.is_empty() { return Err(bad(cmd, "`curves` must list curves")); @@ -988,7 +988,7 @@ fn copy_through( } fn mirror(s: &mut Session, p: &Value) -> Result { - let cmd = "MirrorSketchCommand"; + let cmd = "sketch.mirror"; let (out, info) = edit(s, p, cmd, false, |sk, _| { let (cs, ps) = entities(sk, p, cmd)?; let lid = str_(p, "line").ok_or_else(|| bad(cmd, "`line` must be the mirror line"))?; @@ -1043,7 +1043,7 @@ fn rotate(c: Vec2, ang: f64) -> impl Fn(Vec2) -> Vec2 { } fn pattern_circular(s: &mut Session, p: &Value) -> Result { - let cmd = "CircularSketchPatternCommand"; + let cmd = "sketch.pattern.circular"; let n = count(p, "count", cmd)?; let total = num(p, "angle").unwrap_or(360.0); if !(total.abs() > 1e-9 && total.abs() <= 360.0) { @@ -1066,7 +1066,7 @@ fn pattern_circular(s: &mut Session, p: &Value) -> Result { } fn pattern_rect(s: &mut Session, p: &Value) -> Result { - let cmd = "RectangularSketchPatternCommand"; + let cmd = "sketch.pattern.rectangular"; let n1 = count(p, "count", cmd)?; let s1 = num(p, "spacing").ok_or_else(|| bad(cmd, "`spacing` must be a number"))?; let n2 = if p.get("count2").is_some() { count(p, "count2", cmd)? } else { 1 }; @@ -1134,7 +1134,7 @@ fn move_copy(s: &mut Session, p: &Value) -> Result { } fn scale(s: &mut Session, p: &Value) -> Result { - let cmd = "SketchScaleCmd"; + let cmd = "sketch.scale"; let k = num(p, "factor").filter(|k| *k > 1e-9 && *k < 1e6).ok_or_else(|| bad(cmd, "`factor` must be a positive number"))?; let (out, info) = edit(s, p, cmd, false, |sk, _| { let (cs, ps) = entities(sk, p, cmd)?; diff --git a/crates/engine/src/cmd/sketch_modify_tests.rs b/crates/engine/src/cmd/sketch_modify_tests.rs index fad5fe75..51916014 100644 --- a/crates/engine/src/cmd/sketch_modify_tests.rs +++ b/crates/engine/src/cmd/sketch_modify_tests.rs @@ -22,7 +22,7 @@ fn sketch(s: &Session) -> solvecraft_sketch::Sketch { fn new_sketch() -> Session { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); s } @@ -41,10 +41,10 @@ fn close(a: f64, b: f64) -> bool { fn trim_extend_break() { let mut s = new_sketch(); // A cross: horizontal line crossed by a vertical one. - let h = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone(); - let v = ids(&run(&mut s, "DrawPolyline", json!({"points": [[10, -5], [10, 5]]}))["curves"])[0].clone(); + let h = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone(); + let v = ids(&run(&mut s, "sketch.line", json!({"points": [[10, -5], [10, 5]]}))["curves"])[0].clone(); // Trim the right half of the horizontal line. - run(&mut s, "TrimSketchCmd", json!({"curve": h, "at": [15, 0]})); + run(&mut s, "sketch.trim", json!({"curve": h, "at": [15, 0]})); let sk = sketch(&s); let sh = sk.shape(sk.curve_index(&h).unwrap()).unwrap(); assert!( @@ -52,32 +52,32 @@ fn trim_extend_break() { "{sh:?}" ); // Trim the lower half of the vertical line: the original id survives on the upper piece. - run(&mut s, "TrimSketchCmd", json!({"curve": v, "at": [10, -3]})); + run(&mut s, "sketch.trim", json!({"curve": v, "at": [10, -3]})); let sk = sketch(&s); assert_eq!(sk.curves.len(), 2); // Extend: a short line reaching toward the vertical one. - let e = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 3], [4, 3]]}))["curves"])[0].clone(); - run(&mut s, "ExtendSketchCmd", json!({"curve": e, "at": [4, 3]})); + let e = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 3], [4, 3]]}))["curves"])[0].clone(); + run(&mut s, "sketch.extend", json!({"curve": e, "at": [4, 3]})); let sk = sketch(&s); let end = sk.resolve_point(&format!("{e}.end")).and_then(|i| sk.point(i)).unwrap(); assert!(end.dist(Vec2::new(10.0, 3.0)) < 1e-7, "{end:?}"); // Break the horizontal line where... nothing crosses it inside: error. - assert!(s.execute("BreakSketchCmd", &json!({"curve": h, "at": [5, 0]})).is_err()); + assert!(s.execute("sketch.break", &json!({"curve": h, "at": [5, 0]})).is_err()); // Break the vertical at the extended line: two pieces. - let r = run(&mut s, "BreakSketchCmd", json!({"curve": v, "at": [10, 1]})); + let r = run(&mut s, "sketch.break", json!({"curve": v, "at": [10, 1]})); assert_eq!(ids(&r["curves"]).len(), 2, "{r}"); // A lone curve trims away entirely. - let lone = ids(&run(&mut s, "DrawPolyline", json!({"points": [[50, 50], [60, 50]]}))["curves"])[0].clone(); - run(&mut s, "TrimSketchCmd", json!({"curve": lone, "at": [55, 50]})); + let lone = ids(&run(&mut s, "sketch.line", json!({"points": [[50, 50], [60, 50]]}))["curves"])[0].clone(); + run(&mut s, "sketch.trim", json!({"curve": lone, "at": [55, 50]})); assert!(sketch(&s).curve_index(&lone).is_none()); } #[test] fn trim_circle_makes_an_arc_and_a_profile() { let mut s = new_sketch(); - let c = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone(); - run(&mut s, "DrawPolyline", json!({"points": [[-20, 0], [20, 0]]})); - run(&mut s, "TrimSketchCmd", json!({"curve": c, "at": [0, -10]})); + let c = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone(); + run(&mut s, "sketch.line", json!({"points": [[-20, 0], [20, 0]]})); + run(&mut s, "sketch.trim", json!({"curve": c, "at": [0, -10]})); let sk = sketch(&s); let ci = sk.curve_index(&c).unwrap(); assert!(matches!(sk.curves[ci].kind, CurveKind::Arc { .. })); @@ -90,65 +90,65 @@ fn trim_circle_makes_an_arc_and_a_profile() { #[test] fn fillet_and_chamfers() { let mut s = new_sketch(); - let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); + let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); let l = ids(&r["curves"]); // Pin the rectangle so a dimension change only moves the fillet. - run(&mut s, "ConstraintFix", json!({"entity": l[2]})); - run(&mut s, "ConstraintFix", json!({"entity": l[3]})); - let f = run(&mut s, "FilletSketchCmd", json!({"a": l[0], "b": l[1], "radius": 5})); + run(&mut s, "sketch.constraint.fix", json!({"entity": l[2]})); + run(&mut s, "sketch.constraint.fix", json!({"entity": l[3]})); + let f = run(&mut s, "sketch.fillet", json!({"a": l[0], "b": l[1], "radius": 5})); assert!(f["param"].is_string()); let a = profiles(&s); assert!(close(a[0], 1200.0 - (25.0 - std::f64::consts::PI * 25.0 / 4.0)), "{a:?}"); // The fillet radius is a dimension: change it. let pname = f["param"].as_str().unwrap().to_string(); - run(&mut s, "ChangeParameterCommand", json!({"name": pname, "expression": "8 mm"})); + run(&mut s, "parameters.change", json!({"name": pname, "expression": "8 mm"})); let a = profiles(&s); assert!(close(a[0], 1200.0 - (64.0 - std::f64::consts::PI * 64.0 / 4.0)), "{a:?}"); - run(&mut s, "ChamferSketchEqualDistance", json!({"a": l[2], "b": l[3], "distance": 4})); + run(&mut s, "sketch.chamfer.equal_distance", json!({"a": l[2], "b": l[3], "distance": 4})); let a2 = profiles(&s)[0]; assert!(close(a[0] - a2, 8.0), "{a2}"); - run(&mut s, "ChamferSketchDistanceDistance", json!({"a": l[1], "b": l[2], "distance": 2, "distance2": 6})); + run(&mut s, "sketch.chamfer.two_distance", json!({"a": l[1], "b": l[2], "distance": 2, "distance2": 6})); let a3 = profiles(&s)[0]; assert!(close(a2 - a3, 6.0), "{a3}"); // Distance-angle at the last corner (origin, 90 degrees): 45 deg gives an isosceles cut. - run(&mut s, "ChamferSketchDistanceAngle", json!({"a": l[3], "b": l[0], "distance": 3, "angle": 45})); + run(&mut s, "sketch.chamfer.distance_angle", json!({"a": l[3], "b": l[0], "distance": 3, "angle": 45})); let a4 = profiles(&s)[0]; assert!(close(a3 - a4, 4.5), "{a4}"); - assert!(s.execute("FilletSketchCmd", &json!({"a": l[0], "b": l[2], "radius": 1})).is_err(), "no shared corner"); + assert!(s.execute("sketch.fillet", &json!({"a": l[0], "b": l[2], "radius": 1})).is_err(), "no shared corner"); } #[test] fn offset_chain_mirror_patterns_move_scale() { let mut s = new_sketch(); - let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); + let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); let l = ids(&r["curves"]); // Outward offset by 2 (toward a point outside). - let o = run(&mut s, "Offset", json!({"curves": [l[0]], "distance": 2, "side": [20, -10]})); + let o = run(&mut s, "sketch.offset", json!({"curves": [l[0]], "distance": 2, "side": [20, -10]})); assert_eq!(ids(&o["curves"]).len(), 4, "{o}"); let a = profiles(&s); assert!(close(a[1], 44.0 * 34.0 - 1200.0) || close(a.iter().sum::(), 44.0 * 34.0), "{a:?}"); // Circle offset inward. - let c = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [100, 0], "radius": 10}))["curves"])[0].clone(); - run(&mut s, "Offset", json!({"curves": [c], "distance": 3, "side": [100, 0]})); + let c = ids(&run(&mut s, "sketch.circle.center", json!({"center": [100, 0], "radius": 10}))["curves"])[0].clone(); + run(&mut s, "sketch.offset", json!({"curves": [c], "distance": 3, "side": [100, 0]})); // Mirror a circle about a vertical line. - let m = ids(&run(&mut s, "DrawPolyline", json!({"points": [[120, -50], [120, 50]]}))["curves"])[0].clone(); - let r = run(&mut s, "MirrorSketchCommand", json!({"entities": [c], "line": m})); + let m = ids(&run(&mut s, "sketch.line", json!({"points": [[120, -50], [120, 50]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.mirror", json!({"entities": [c], "line": m})); let mc = ids(&r["curves"])[0].clone(); let sk = sketch(&s); assert!(sk.center(sk.curve_index(&mc).unwrap()).unwrap().dist(Vec2::new(140.0, 0.0)) < 1e-7); // Circular pattern of the mirrored circle: 4 around (120, 0). - let r = run(&mut s, "CircularSketchPatternCommand", json!({"entities": [mc], "center": [120, 0], "count": 4})); + let r = run(&mut s, "sketch.pattern.circular", json!({"entities": [mc], "center": [120, 0], "count": 4})); assert_eq!(ids(&r["curves"]).len(), 3); let sk = sketch(&s); let c2 = sk.center(sk.curve_index(&ids(&r["curves"])[0]).unwrap()).unwrap(); assert!(c2.dist(Vec2::new(120.0, 20.0)) < 1e-7, "{c2:?}"); // Rectangular pattern 3 x 2. - let small = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [0, 100], "radius": 1}))["curves"])[0].clone(); - let r = run(&mut s, "RectangularSketchPatternCommand", json!({"entities": [small], "count": 3, "spacing": 5, "count2": 2, "spacing2": 4})); + let small = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 100], "radius": 1}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.pattern.rectangular", json!({"entities": [small], "count": 3, "spacing": 5, "count2": 2, "spacing2": 4})); assert_eq!(ids(&r["curves"]).len(), 5); // Move and scale. run(&mut s, "sketch.move", json!({"entities": [small], "translate": [0, 10]})); - run(&mut s, "SketchScaleCmd", json!({"entities": [small], "base": [0, 110], "factor": 3})); + run(&mut s, "sketch.scale", json!({"entities": [small], "base": [0, 110], "factor": 3})); let sk = sketch(&s); let ci = sk.curve_index(&small).unwrap(); assert!(sk.center(ci).unwrap().dist(Vec2::new(0.0, 110.0)) < 1e-7); @@ -162,31 +162,31 @@ fn offset_chain_mirror_patterns_move_scale() { #[test] fn new_creation_tools() { let mut s = new_sketch(); - let r = run(&mut s, "SketchMidpointLine", json!({"mid": [0, 0], "end": [10, 5]})); + let r = run(&mut s, "sketch.line.midpoint", json!({"mid": [0, 0], "end": [10, 5]})); let l = ids(&r["curves"])[0].clone(); let sk = sketch(&s); let start = sk.resolve_point(&format!("{l}.start")).and_then(|i| sk.point(i)).unwrap(); assert!(start.dist(Vec2::new(-10.0, -5.0)) < 1e-9); // Tangent arc off the end of a horizontal line. - let h = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 20], [10, 20]]}))["curves"])[0].clone(); - let r = run(&mut s, "ArcTangent", json!({"start": format!("{h}.end"), "end": [20, 30]})); + let h = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 20], [10, 20]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.arc.tangent", json!({"start": format!("{h}.end"), "end": [20, 30]})); let a = ids(&r["curves"])[0].clone(); let sk = sketch(&s); let ai = sk.curve_index(&a).unwrap(); assert!(sk.center(ai).unwrap().dist(Vec2::new(10.0, 30.0)) < 1e-7, "{:?}", sk.center(ai)); assert!(close(sk.radius(ai).unwrap(), 10.0)); // 2-tangent circle in the corner of two lines. - let x = ids(&run(&mut s, "DrawPolyline", json!({"points": [[100, 0], [150, 0]]}))["curves"])[0].clone(); - let y = ids(&run(&mut s, "DrawPolyline", json!({"points": [[100, 0], [100, 50]]}))["curves"])[0].clone(); - let r = run(&mut s, "CircleTanTanRadius", json!({"curves": [x, y], "radius": 5, "near": [106, 104 - 100]})); + let x = ids(&run(&mut s, "sketch.line", json!({"points": [[100, 0], [150, 0]]}))["curves"])[0].clone(); + let y = ids(&run(&mut s, "sketch.line", json!({"points": [[100, 0], [100, 50]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.circle.two_tangent", json!({"curves": [x, y], "radius": 5, "near": [106, 104 - 100]})); let c = ids(&r["curves"])[0].clone(); let sk = sketch(&s); let ci = sk.curve_index(&c).unwrap(); assert!(sk.center(ci).unwrap().dist(Vec2::new(105.0, 5.0)) < 1e-6, "{:?}", sk.center(ci)); // 3-tangent circle inside a triangle: the incircle of the 3-4-5 triangle (r = 1). - let t = run(&mut s, "DrawPolyline", json!({"points": [[200, 0], [203, 0], [200, 4]], "closed": true})); + let t = run(&mut s, "sketch.line", json!({"points": [[200, 0], [203, 0], [200, 4]], "closed": true})); let tl = ids(&t["curves"]); - let r = run(&mut s, "CircleThreeTangent", json!({"curves": tl, "near": [201, 1.2]})); + let r = run(&mut s, "sketch.circle.three_tangent", json!({"curves": tl, "near": [201, 1.2]})); let c = ids(&r["curves"])[0].clone(); let sk = sketch(&s); let ci = sk.curve_index(&c).unwrap(); @@ -194,9 +194,9 @@ fn new_creation_tools() { assert!(sk.center(ci).unwrap().dist(Vec2::new(201.0, 1.0)) < 1e-6); // Slots. let before = profiles(&s).len(); - run(&mut s, "ShapeSlotCenterPoint", json!({"center": [0, -100], "end": [10, -100], "width": 4})); - run(&mut s, "ShapeArcSlotThreePoint", json!({"start": [100, -100], "through": [110, -90], "end": [120, -100], "width": 2})); - run(&mut s, "ShapeArcSlotCenterTwoPoint", json!({"center": [200, -100], "start": [210, -100], "end": [200, -90], "width": 2})); + run(&mut s, "sketch.slot.center_point", json!({"center": [0, -100], "end": [10, -100], "width": 4})); + run(&mut s, "sketch.slot.arc_three_point", json!({"start": [100, -100], "through": [110, -90], "end": [120, -100], "width": 2})); + run(&mut s, "sketch.slot.arc_center", json!({"center": [200, -100], "start": [210, -100], "end": [200, -90], "width": 2})); let a = profiles(&s); assert_eq!(a.len(), before + 3, "{a:?}"); let pi = std::f64::consts::PI; @@ -210,13 +210,13 @@ fn new_creation_tools() { fn ellipse_splines_conic() { let pi = std::f64::consts::PI; let mut s = new_sketch(); - let e = run(&mut s, "CircleElipse", json!({"center": [0, 0], "major": [10, 0], "minor": [0, 4]})); + let e = run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor": [0, 4]})); let eid = ids(&e["curves"])[0].clone(); let a = profiles(&s); assert!((a[0] - pi * 40.0).abs() / (pi * 40.0) < 2e-3, "{a:?}"); // A point constrained onto the ellipse lands on it. - run(&mut s, "DrawPoint", json!({"point": [3, 5], "id": "q"})); - run(&mut s, "ConstraintCoincident", json!({"a": "q", "b": eid})); + run(&mut s, "sketch.point", json!({"point": [3, 5], "id": "q"})); + run(&mut s, "sketch.constraint.coincident", json!({"a": "q", "b": eid})); let sk = sketch(&s); let q = sk.point(sk.point_index("q").unwrap()).unwrap(); let poly = sk.polyline(sk.curve_index(&eid).unwrap()); @@ -227,9 +227,9 @@ fn ellipse_splines_conic() { }); assert!(d.fold(f64::INFINITY, f64::min) < 1e-6, "{q:?}"); // Fit spline closed by a line: one profile. - let sp = run(&mut s, "DrawSpline", json!({"points": [[100, 0], [110, 8], [120, 3], [130, 0]]})); + let sp = run(&mut s, "sketch.spline.fit_point", json!({"points": [[100, 0], [110, 8], [120, 3], [130, 0]]})); let sid = ids(&sp["curves"])[0].clone(); - run(&mut s, "DrawPolyline", json!({"points": [format!("{sid}.end"), format!("{sid}.start")]})); + run(&mut s, "sketch.line", json!({"points": [format!("{sid}.end"), format!("{sid}.start")]})); assert_eq!(profiles(&s).len(), 2); // Moving a fit point reshapes the spline (it is a sketch point). let sk = sketch(&s); @@ -240,31 +240,31 @@ fn ellipse_splines_conic() { let sk = sketch(&s); assert!(sk.polyline(si).iter().any(|q| q.dist(Vec2::new(110.0, 12.0)) < 1e-9)); // Control point splines (degree 3 and 5) and a conic. - run(&mut s, "DrawCVMSpline3D", json!({"points": [[0, 50], [0, 60], [10, 60], [10, 50]]})); - run(&mut s, "DrawCVMSpline5D", json!({"points": [[20, 50], [20, 60], [25, 65], [30, 60], [35, 62], [40, 50]]})); + run(&mut s, "sketch.spline.control_point", json!({"points": [[0, 50], [0, 60], [10, 60], [10, 50]]})); + run(&mut s, "sketch.spline.control_point_5", json!({"points": [[20, 50], [20, 60], [25, 65], [30, 60], [35, 62], [40, 50]]})); let w = std::f64::consts::FRAC_1_SQRT_2; - let k = run(&mut s, "ConicCurveCmd", json!({"start": [210, 0], "end": [200, 10], "apex": [210, 10], "rho": w / (1.0 + w)})); + let k = run(&mut s, "sketch.conic", json!({"start": [210, 0], "end": [200, 10], "apex": [210, 10], "rho": w / (1.0 + w)})); let kid = ids(&k["curves"])[0].clone(); // A line tangent to the conic at its end: the tangent arc tool continues it. - let r = run(&mut s, "ArcTangent", json!({"start": format!("{kid}.end"), "end": [190, 0]})); + let r = run(&mut s, "sketch.arc.tangent", json!({"start": format!("{kid}.end"), "end": [190, 0]})); let aid = ids(&r["curves"])[0].clone(); let sk = sketch(&s); // The conic is a quarter circle about (200, 0) leaving (200, 10) heading -x: the tangent // arc from there to (190, 0) continues the same circle. let ai = sk.curve_index(&aid).unwrap(); assert!(sk.center(ai).unwrap().dist(Vec2::new(200.0, 0.0)) < 1e-6, "{:?}", sk.center(ai)); - assert!(s.execute("DrawSpline", &json!({"points": [[0, 0]]})).is_err()); - assert!(s.execute("ConicCurveCmd", &json!({"start": [0, 0], "end": [1, 0], "apex": [0, 1], "rho": 1.5})).is_err()); + assert!(s.execute("sketch.spline.fit_point", &json!({"points": [[0, 0]]})).is_err()); + assert!(s.execute("sketch.conic", &json!({"start": [0, 0], "end": [1, 0], "apex": [0, 1], "rho": 1.5})).is_err()); // Mirror and pattern copy free-form curves too. - let m = ids(&run(&mut s, "DrawPolyline", json!({"points": [[-50, -50], [-50, 50]]}))["curves"])[0].clone(); - let r = run(&mut s, "MirrorSketchCommand", json!({"entities": [eid, sid], "line": m})); + let m = ids(&run(&mut s, "sketch.line", json!({"points": [[-50, -50], [-50, 50]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.mirror", json!({"entities": [eid, sid], "line": m})); assert_eq!(ids(&r["curves"]).len(), 2); } #[test] fn text_makes_profiles_that_extrude_and_edit() { let mut s = new_sketch(); - let r = run(&mut s, "MTextCmd", json!({"text": "SO", "at": [0, 0], "height": 10})); + let r = run(&mut s, "sketch.text", json!({"text": "SO", "at": [0, 0], "height": 10})); let link = r["link"].as_str().unwrap().to_string(); let a = profiles(&s); // S (one region) + O (ring = outer with a hole, plus the counter as its own region). @@ -276,21 +276,21 @@ fn text_makes_profiles_that_extrude_and_edit() { assert_eq!(r2["link"], json!(link)); let a = profiles(&s); assert_eq!(a.len(), 1, "{a:?}"); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 2})); - let m = run(&mut s, "MeasureCommand", json!({})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 2})); + let m = run(&mut s, "inspect.measure", json!({})); let v = m["total"]["volume_mm3"].as_f64().unwrap(); // An "I" 20 mm tall is a bar a couple of mm wide. assert!(v > 40.0 && v < 200.0, "{v}"); - assert!(s.execute("MTextCmd", &json!({"text": "", "at": [0, 0]})).is_err()); + assert!(s.execute("sketch.text", &json!({"text": "", "at": [0, 0]})).is_err()); } #[test] fn blend_curve_joins_tangentially() { let mut s = new_sketch(); - let a = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [10, 0]]}))["curves"])[0].clone(); - let b = ids(&run(&mut s, "DrawPolyline", json!({"points": [[20, 10], [20, 20]]}))["curves"])[0].clone(); - let r = run(&mut s, "BlendG1CurveSketchCmd", json!({"a": format!("{a}.end"), "b": format!("{b}.start")})); + let a = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]]}))["curves"])[0].clone(); + let b = ids(&run(&mut s, "sketch.line", json!({"points": [[20, 10], [20, 20]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.blend_curve", json!({"a": format!("{a}.end"), "b": format!("{b}.start")})); assert_eq!(r["sketch"]["solved"], true, "{r}"); let sp = ids(&r["curves"])[0].clone(); let sk = sketch(&s); @@ -305,9 +305,9 @@ fn blend_curve_joins_tangentially() { #[test] fn centerline_is_left_out_of_profiles() { let mut s = new_sketch(); - let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [10, 10]})); + let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]})); let l = ids(&r["curves"]); - let m = ids(&run(&mut s, "DrawPolyline", json!({"points": [[5, -5], [5, 15]]}))["curves"])[0].clone(); + let m = ids(&run(&mut s, "sketch.line", json!({"points": [[5, -5], [5, 15]]}))["curves"])[0].clone(); assert_eq!(profiles(&s).len(), 2); run(&mut s, "sketch.centerline", json!({"curves": [m]})); assert_eq!(profiles(&s).len(), 1); @@ -318,57 +318,59 @@ fn centerline_is_left_out_of_profiles() { #[test] fn free_form_profiles_extrude_exactly() { let pi = std::f64::consts::PI; - let vol = |s: &mut Session| run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap(); + let vol = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap(); // Ellipse a = 10, b = 4: π a b h. let mut s = new_sketch(); - run(&mut s, "CircleElipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4})); + run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4})); let a = profiles(&s)[0]; assert!((a - pi * 40.0).abs() < 1e-9, "{a}"); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 5})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 5})); let v = vol(&mut s); assert!((v - pi * 40.0 * 5.0).abs() / (pi * 200.0) < 2e-4, "{v}"); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert_eq!(m["bodies"][0]["faces"], 3, "an exact ellipse: top, bottom and one side ({m})"); // A region closed by a fit spline and a line. let mut s = new_sketch(); - let sp = ids(&run(&mut s, "DrawSpline", json!({"points": [[0, 0], [10, 8], [20, 0]]}))["curves"])[0].clone(); - run(&mut s, "DrawPolyline", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]})); + let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 8], [20, 0]]}))["curves"])[0].clone(); + run(&mut s, "sketch.line", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]})); let a = profiles(&s)[0]; - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 2})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 2})); let v = vol(&mut s); assert!((v - a * 2.0).abs() / (a * 2.0) < 2e-4, "{v} vs {}", a * 2.0); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert!(m["bodies"][0]["faces"].as_u64().unwrap() <= 5, "{m}"); } #[test] fn degree_five_and_cut_free_form_profiles_extrude_exactly() { - let vol = |s: &mut Session| run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap(); + let vol = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap(); // A degree-5 control spline closed by a line: one B-spline side face. let mut s = new_sketch(); - let sp = ids(&run(&mut s, "DrawCVMSpline5D", json!({"points": [[0, 0], [4, 10], [10, 12], [16, 12], [22, 10], [26, 0]]}))["curves"])[0].clone(); - run(&mut s, "DrawPolyline", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]})); + let sp = + ids(&run(&mut s, "sketch.spline.control_point_5", json!({"points": [[0, 0], [4, 10], [10, 12], [16, 12], [22, 10], [26, 0]]}))["curves"])[0] + .clone(); + run(&mut s, "sketch.line", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]})); let a = profiles(&s)[0]; - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 3})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 3})); let v = vol(&mut s); assert!((v - a * 3.0).abs() / (a * 3.0) < 2e-4, "{v} vs {}", a * 3.0); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert_eq!(m["bodies"][0]["faces"], 4, "{m}"); // A fit spline arch cut by a line: both sides extrude with exact curved faces. let mut s = new_sketch(); - let sp = ids(&run(&mut s, "DrawSpline", json!({"points": [[0, 0], [6, 9], [14, 9], [20, 0]]}))["curves"])[0].clone(); - run(&mut s, "DrawPolyline", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]})); - run(&mut s, "DrawPolyline", json!({"points": [[10, -5], [10, 20]]})); + let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [6, 9], [14, 9], [20, 0]]}))["curves"])[0].clone(); + run(&mut s, "sketch.line", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]})); + run(&mut s, "sketch.line", json!({"points": [[10, -5], [10, 20]]})); let areas = profiles(&s); assert_eq!(areas.len(), 2, "{areas:?}"); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 2, "profiles": [0]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 2, "profiles": [0]})); let v = vol(&mut s); assert!((v - areas[0] * 2.0).abs() / (areas[0] * 2.0) < 2e-4, "{v} vs {}", areas[0] * 2.0); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert!(m["bodies"][0]["faces"].as_u64().unwrap() <= 7, "no polyline facets: {m}"); } @@ -378,9 +380,9 @@ fn trim_and_break_free_form_curves_exactly() { let mut s = new_sketch(); // An ellipse cut by a line through its centre: trimming the lower half leaves the upper // half, which closes with the line into half the ellipse. - let e = ids(&run(&mut s, "CircleElipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4}))["curves"])[0].clone(); - let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[-15, 0], [15, 0]]}))["curves"])[0].clone(); - let r = run(&mut s, "TrimSketchCmd", json!({"curve": e, "at": [0, -4]})); + let e = ids(&run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4}))["curves"])[0].clone(); + let l = ids(&run(&mut s, "sketch.line", json!({"points": [[-15, 0], [15, 0]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.trim", json!({"curve": e, "at": [0, -4]})); assert!(!r["result"]["pieces"].as_array().unwrap().is_empty(), "{r}"); let a = profiles(&s); assert_eq!(a.len(), 1, "{a:?}"); @@ -388,10 +390,10 @@ fn trim_and_break_free_form_curves_exactly() { let _ = l; // A fit spline broken where a line crosses it: two pieces that still trace the curve. let mut s = new_sketch(); - let sp = ids(&run(&mut s, "DrawSpline", json!({"points": [[0, 0], [10, 10], [20, 0], [30, 5]]}))["curves"])[0].clone(); + let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 10], [20, 0], [30, 5]]}))["curves"])[0].clone(); let before = sketch(&s).polyline(sketch(&s).curve_index(&sp).unwrap()); - run(&mut s, "DrawPolyline", json!({"points": [[15, -10], [15, 20]]})); - let r = run(&mut s, "BreakSketchCmd", json!({"curve": sp, "at": [15, 5]})); + run(&mut s, "sketch.line", json!({"points": [[15, -10], [15, 20]]})); + let r = run(&mut s, "sketch.break", json!({"curve": sp, "at": [15, 5]})); let pieces = ids(&r["curves"]); assert_eq!(pieces.len(), 4, "three Béziers, the middle one split: {r}"); let sk = sketch(&s); @@ -415,9 +417,9 @@ fn trim_and_break_free_form_curves_exactly() { #[test] fn ellipse_radii_dimensions() { let mut s = new_sketch(); - let e = ids(&run(&mut s, "CircleElipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4}))["curves"])[0].clone(); - run(&mut s, "SketchDimension", json!({"entities": [e], "type": "major", "value": 12})); - run(&mut s, "SketchDimension", json!({"entities": [e], "value": 5})); + let e = ids(&run(&mut s, "sketch.ellipse", json!({"center": [0, 0], "major": [10, 0], "minor_radius": 4}))["curves"])[0].clone(); + run(&mut s, "sketch.dimension", json!({"entities": [e], "type": "major", "value": 12})); + run(&mut s, "sketch.dimension", json!({"entities": [e], "value": 5})); let a = profiles(&s)[0]; assert!((a - std::f64::consts::PI * 60.0).abs() < 1e-6, "{a}"); } @@ -426,10 +428,11 @@ fn ellipse_radii_dimensions() { fn fillet_a_line_and_an_arc() { let mut s = new_sketch(); // A line into a quarter arc meeting at a sharp corner. - let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone(); - let a = ids(&run(&mut s, "ArcCenterTwoPoint", json!({"center": [30, 0], "start": format!("{l}.end"), "end": [30, -10]}))["curves"])[0].clone(); + let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone(); + let a = + ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [30, 0], "start": format!("{l}.end"), "end": [30, -10]}))["curves"])[0].clone(); let _ = a; - let f = run(&mut s, "FilletSketchCmd", json!({"point": format!("{l}.end"), "radius": 2})); + let f = run(&mut s, "sketch.fillet", json!({"point": format!("{l}.end"), "radius": 2})); let arc = ids(&f["curves"])[0].clone(); let sk = sketch(&s); let ai = sk.curve_index(&arc).unwrap(); @@ -444,8 +447,8 @@ fn fillet_a_line_and_an_arc() { #[test] fn offset_free_form_curves() { let mut s = new_sketch(); - let sp = ids(&run(&mut s, "DrawSpline", json!({"points": [[0, 0], [10, 8], [20, 0], [30, 6]]}))["curves"])[0].clone(); - let r = run(&mut s, "Offset", json!({"curves": [sp], "distance": 2, "side": [10, 20]})); + let sp = ids(&run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 8], [20, 0], [30, 6]]}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.offset", json!({"curves": [sp], "distance": 2, "side": [10, 20]})); let made = ids(&r["curves"]); assert_eq!(made.len(), 1, "{r}"); let sk = sketch(&s); @@ -464,7 +467,7 @@ fn offset_free_form_curves() { assert!((dist(*q) - 2.0).abs() < 0.08, "{q:?} {}", dist(*q)); } // An ellipse grows by the offset all round. - let e = ids(&run(&mut s, "CircleElipse", json!({"center": [100, 0], "major": [110, 0], "minor_radius": 5}))["curves"])[0].clone(); - let r = run(&mut s, "Offset", json!({"curves": [e], "distance": 1, "side": [100, 20]})); + let e = ids(&run(&mut s, "sketch.ellipse", json!({"center": [100, 0], "major": [110, 0], "minor_radius": 5}))["curves"])[0].clone(); + let r = run(&mut s, "sketch.offset", json!({"curves": [e], "distance": 1, "side": [100, 20]})); assert_eq!(ids(&r["curves"]).len(), 1, "{r}"); } diff --git a/crates/engine/src/cmd/sketch_perf_tests.rs b/crates/engine/src/cmd/sketch_perf_tests.rs index 81122a92..a3c172c8 100644 --- a/crates/engine/src/cmd/sketch_perf_tests.rs +++ b/crates/engine/src/cmd/sketch_perf_tests.rs @@ -15,7 +15,7 @@ fn ms(t: Instant) -> f64 { /// them, built straight into the document. fn big(n: usize) -> Session { let mut s = Session::default(); - s.execute("SketchCreate", &json!({"plane": "XY"})).unwrap(); + s.execute("sketch.create", &json!({"plane": "XY"})).unwrap(); let id = s.active_sketch.unwrap(); let mut doc = (*s.doc).clone(); let sk = doc.sketch_mut(id).unwrap(); @@ -67,7 +67,7 @@ fn sketch_perf() { s.execute("sketch.snap", &json!({"at": [10.0, 0.2], "from": [0, 0], "radius": 1})).unwrap(); let snap = ms(t); let t = Instant::now(); - s.execute("DrawPolyline", &json!({"points": [[-50, -50], [-40, -45]]})).unwrap(); + s.execute("sketch.line", &json!({"points": [[-50, -50], [-40, -45]]})).unwrap(); let add = ms(t); let t = Instant::now(); s.execute("sketch.move_point", &json!({"point": "p1", "to": [1, 1]})).unwrap(); diff --git a/crates/engine/src/cmd/sketch_project.rs b/crates/engine/src/cmd/sketch_project.rs index 73b11d89..c2778ced 100644 --- a/crates/engine/src/cmd/sketch_project.rs +++ b/crates/engine/src/cmd/sketch_project.rs @@ -14,36 +14,36 @@ use crate::params::{bad, bool_, str_, vec3}; use crate::{EngineError, Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("ProjectNewCmd", "Project", project_cmd).at("SKETCH", "CREATE").icon("project").key("P").enabled(in_sketch).params(REFS_DOC), - CommandSpec::new("IntersectCmd", "Intersect", intersect_cmd).at("SKETCH", "CREATE").icon("intersect").enabled(in_sketch).params( + CommandSpec::new("sketch.project", "Project", project_cmd).at("SKETCH", "CREATE").icon("project").key("P").enabled(in_sketch).params(REFS_DOC), + CommandSpec::new("sketch.intersect", "Intersect", intersect_cmd).at("SKETCH", "CREATE").icon("intersect").enabled(in_sketch).params( "refs: [{body}, {face: [x,y,z], body?}, {edge: [x,y,z], body?}, {sketch, curve}]: section curves (or points) with the sketch plane, linked", ), - CommandSpec::new("Include3DGeometry", "Include 3D Geometry", include_cmd) + CommandSpec::new("sketch.include_3d", "Include 3D Geometry", include_cmd) .at("SKETCH", "CREATE") .icon("include") .enabled(in_sketch) .params("refs: like Project (edges, vertices, sketch curves and points); curves out of the sketch plane are flattened onto it"), - CommandSpec::new("ProjectToSurface", "Project To Surface", project_to_surface) + CommandSpec::new("sketch.project_to_surface", "Project To Surface", project_to_surface) .at("SKETCH", "CREATE") .icon("project_surface") .enabled(in_sketch) .params("curves: [{sketch, curve}…] (curves of other sketches), face: [x,y,z] point on the target face, body?: projected along the active sketch's normal onto the face, as linked 3D curves"), - CommandSpec::new("IntersectionCurve", "Intersection Curve", intersection_curve) + CommandSpec::new("sketch.intersection_curve", "Intersection Curve", intersection_curve) .at("SKETCH", "CREATE") .icon("intersection_curve") .enabled(in_sketch) .params("a, b: {body} | {face: [x,y,z], body?}: where they meet, as linked 3D curves"), - CommandSpec::new("SpunProfileCmd", "Spun Profile", spun_profile) + CommandSpec::new("sketch.spun_profile", "Spun Profile", spun_profile) .at("SKETCH", "CREATE") .icon("spun") .enabled(in_sketch) .params("body: name, axis: a line of the active sketch (curve id) | X|Y|Z: the body's outline revolved about the axis, in the sketch plane"), - CommandSpec::new("SketchIsoparametricCurve", "Isoparametric Curve", iso_curve) + CommandSpec::new("sketch.isoparametric_curve", "Isoparametric Curve", iso_curve) .at("SKETCH", "CREATE") .icon("iso_curve") .enabled(in_sketch) .params("face: [x,y,z] point on the face (the curve passes through it), body?, direction?: u|v|[x,y,z] (default u): linked 3D curve"), - CommandSpec::new("FitCurvesToSectionCommand", "Fit Curves to Mesh Section", fit_section) + CommandSpec::new("sketch.fit_curves_to_section", "Fit Curves to Mesh Section", fit_section) .at("SKETCH", "CREATE") .icon("fit_section") .enabled(in_sketch) @@ -176,15 +176,15 @@ fn run_links(s: &mut Session, p: &Value, cmd: &str, kind: LinkKind) -> Result Result { - run_links(s, p, "ProjectNewCmd", LinkKind::Project) + run_links(s, p, "sketch.project", LinkKind::Project) } fn intersect_cmd(s: &mut Session, p: &Value) -> Result { - run_links(s, p, "IntersectCmd", LinkKind::Intersect) + run_links(s, p, "sketch.intersect", LinkKind::Intersect) } fn include_cmd(s: &mut Session, p: &Value) -> Result { - run_links(s, p, "Include3DGeometry", LinkKind::Include) + run_links(s, p, "sketch.include_3d", LinkKind::Include) } /// Add one link from `src` to the active sketch and report it. @@ -204,7 +204,7 @@ fn one_link(s: &mut Session, cmd: &str, kind: LinkKind, src: LinkSource) -> Resu } fn project_to_surface(s: &mut Session, p: &Value) -> Result { - let cmd = "ProjectToSurface"; + let cmd = "sketch.project_to_surface"; let at = p.get("face").and_then(vec3).ok_or_else(|| bad(cmd, "`face` must be a point [x,y,z] on the target face"))?; let body = str_(p, "body").unwrap_or("").to_string(); let list = p.get("curves").and_then(Value::as_array).cloned().ok_or_else(|| bad(cmd, "`curves` must list {sketch, curve}"))?; @@ -222,7 +222,7 @@ fn project_to_surface(s: &mut Session, p: &Value) -> Result { } fn intersection_curve(s: &mut Session, p: &Value) -> Result { - let cmd = "IntersectionCurve"; + let cmd = "sketch.intersection_curve"; let side = |k: &str| -> Result { let v = p.get(k).ok_or_else(|| bad(cmd, format!("missing `{k}`")))?; match source(s, v, cmd)? { @@ -235,7 +235,7 @@ fn intersection_curve(s: &mut Session, p: &Value) -> Result { } fn spun_profile(s: &mut Session, p: &Value) -> Result { - let cmd = "SpunProfileCmd"; + let cmd = "sketch.spun_profile"; let body = str_(p, "body").ok_or_else(|| bad(cmd, "`body` must name a body"))?.to_string(); let axis = str_(p, "axis").ok_or_else(|| bad(cmd, "`axis` must be a sketch line or X|Y|Z"))?; let (origin, dir) = match axis.to_ascii_uppercase().as_str() { @@ -256,7 +256,7 @@ fn spun_profile(s: &mut Session, p: &Value) -> Result { } fn iso_curve(s: &mut Session, p: &Value) -> Result { - let cmd = "SketchIsoparametricCurve"; + let cmd = "sketch.isoparametric_curve"; let at = p.get("face").and_then(vec3).ok_or_else(|| bad(cmd, "`face` must be a point [x,y,z] on the face"))?; let body = str_(p, "body").unwrap_or("").to_string(); let dir = match p.get("direction") { @@ -271,7 +271,7 @@ fn iso_curve(s: &mut Session, p: &Value) -> Result { } fn fit_section(s: &mut Session, p: &Value) -> Result { - let cmd = "FitCurvesToSectionCommand"; + let cmd = "sketch.fit_curves_to_section"; let body = str_(p, "body").ok_or_else(|| bad(cmd, "`body` must name a body"))?.to_string(); if s.model.state().body(&body).is_none() { return Err(bad(cmd, format!("no body `{body}`"))); diff --git a/crates/engine/src/cmd/sketch_project_tests.rs b/crates/engine/src/cmd/sketch_project_tests.rs index d424547e..39ad5487 100644 --- a/crates/engine/src/cmd/sketch_project_tests.rs +++ b/crates/engine/src/cmd/sketch_project_tests.rs @@ -15,20 +15,20 @@ fn ids(v: &Value) -> Vec { /// A 40 x 30 x 20 box from a sketch whose width is the user parameter `width`. fn box_part(s: &mut Session) { - run(s, "ChangeParameterCommand", json!({"name": "width", "expression": "40 mm"})); - run(s, "SketchCreate", json!({"plane": "XY"})); - let r = run(s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); + run(s, "parameters.change", json!({"name": "width", "expression": "40 mm"})); + run(s, "sketch.create", json!({"plane": "XY"})); + let r = run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); let l = ids(&r["curves"]); let sk = s.doc.sketch(s.active_sketch.unwrap()).unwrap(); let start = match sk.curves[0].kind { solvecraft_sketch::CurveKind::Line { a, .. } => sk.points[a].id.clone(), _ => panic!(), }; - run(s, "ConstraintCoincident", json!({"a": start, "b": "origin"})); - run(s, "SketchDimension", json!({"entities": [l[0]], "value": "width"})); - run(s, "SketchDimension", json!({"entities": [l[1]], "value": 30})); - run(s, "SketchStop", json!({})); - run(s, "Extrude", json!({"distance": 20})); + run(s, "sketch.constraint.coincident", json!({"a": start, "b": "origin"})); + run(s, "sketch.dimension", json!({"entities": [l[0]], "value": "width"})); + run(s, "sketch.dimension", json!({"entities": [l[1]], "value": 30})); + run(s, "sketch.finish", json!({})); + run(s, "solid.extrude", json!({"distance": 20})); } fn inspect(s: &mut Session) -> Value { @@ -43,7 +43,7 @@ fn linked_curves(si: &Value) -> Vec { fn sketch_on_face_projects_its_edges_and_follows_edits() { let mut s = Session::default(); box_part(&mut s); - let r = run(&mut s, "SketchCreate", json!({"plane": {"face": [20, 15, 20]}})); + let r = run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 20]}})); assert!(r["projected"].is_string(), "{r}"); let si = inspect(&mut s); let lc = linked_curves(&si); @@ -55,16 +55,16 @@ fn sketch_on_face_projects_its_edges_and_follows_edits() { // Projected geometry is fixed: the sketch has no degrees of freedom. assert_eq!(si["dof"], 0, "{si}"); // A circle on the face, dimensioned to a projected edge. - let c = run(&mut s, "CircleCenterRadius", json!({"center": [10, 10], "radius": 3})); + let c = run(&mut s, "sketch.circle.center", json!({"center": [10, 10], "radius": 3})); let cid = ids(&c["curves"])[0].clone(); let e = lc.iter().find(|c| c["start_at"][1].as_f64().unwrap().abs() < 1e-9 && c["end_at"][1].as_f64().unwrap().abs() < 1e-9).unwrap(); - run(&mut s, "SketchDimension", json!({"entities": [format!("{cid}.center"), e["id"]], "value": 8})); + run(&mut s, "sketch.dimension", json!({"entities": [format!("{cid}.center"), e["id"]], "value": 8})); let si = inspect(&mut s); let center = si["points"].as_array().unwrap().iter().find(|p| p["id"] == format!("{cid}.center")).unwrap(); assert!((center["at"][1].as_f64().unwrap() - 8.0).abs() < 1e-7, "{center}"); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); // Widen the box: the projected loop follows. - run(&mut s, "ChangeParameterCommand", json!({"name": "width", "expression": "50 mm"})); + run(&mut s, "parameters.change", json!({"name": "width", "expression": "50 mm"})); let st = s.model.state(); let ss = st.sketches.last().unwrap(); assert_eq!(ss.profiles.len(), 2); @@ -78,11 +78,11 @@ fn sketch_on_face_projects_its_edges_and_follows_edits() { fn auto_project_can_be_turned_off() { let mut s = Session::default(); box_part(&mut s); - let r = run(&mut s, "SketchCreate", json!({"plane": {"face": [20, 15, 20]}, "project_edges": false})); + let r = run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 20]}, "project_edges": false})); assert!(r["projected"].is_null()); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); run(&mut s, "sketch.auto_project", json!({"value": false})); - let r = run(&mut s, "SketchCreate", json!({"plane": {"face": [20, 15, 20]}})); + let r = run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 20]}})); assert!(r["projected"].is_null()); } @@ -90,10 +90,10 @@ fn auto_project_can_be_turned_off() { fn project_edges_bodies_sketches_and_origin() { let mut s = Session::default(); box_part(&mut s); - run(&mut s, "PrimitiveCylinder", json!({"base": [80, 0, 5], "radius": 6, "height": 10})); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); + run(&mut s, "solid.cylinder", json!({"base": [80, 0, 5], "radius": 6, "height": 10})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); // An edge on the top face projects to a line on XY. - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [20, 30, 20]}]})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"edge": [20, 30, 20]}]})); let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == r["links"][0]["curves"][0]).unwrap().clone(); assert_eq!(c["type"], "line"); @@ -101,7 +101,7 @@ fn project_edges_bodies_sketches_and_origin() { assert!(ys.iter().all(|y| (y - 30.0).abs() < 1e-9), "{c}"); // The cylinder's silhouette seen from above is its circle, exactly. let name = s.model.state().bodies[1].name.clone(); - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"body": name}]})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"body": name}]})); let cid = r["links"][0]["curves"][0].clone(); let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == cid).unwrap().clone(); @@ -109,34 +109,34 @@ fn project_edges_bodies_sketches_and_origin() { assert!((c["radius"].as_f64().unwrap() - 6.0).abs() < 1e-6, "{c}"); assert!((c["center_at"][0].as_f64().unwrap() - 80.0).abs() < 1e-6, "{c}"); // Origin, the Y axis and the YZ plane. - run(&mut s, "ProjectNewCmd", json!({"refs": ["origin", {"axis": "Y"}, {"plane": "YZ"}]})); + run(&mut s, "sketch.project", json!({"refs": ["origin", {"axis": "Y"}, {"plane": "YZ"}]})); // The box silhouette from above is its 40 x 30 outline. let b0 = s.model.state().bodies[0].name.clone(); - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"body": b0}]})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"body": b0}]})); assert_eq!(r["links"][0]["curves"].as_array().unwrap().len(), 4, "{r}"); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); let first = s.active_sketch; assert!(first.is_none()); let sk1 = s.doc.features.last().unwrap().id; // Another sketch projects a curve of the first one. - run(&mut s, "SketchCreate", json!({"plane": "XY", "offset": 50})); - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"sketch": sk1, "curve": cid}]})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 50})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"sketch": sk1, "curve": cid}]})); let c2 = r["links"][0]["curves"][0].clone(); let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == c2).unwrap().clone(); assert_eq!(c["type"], "circle"); // Bad references fail cleanly. - assert!(s.execute("ProjectNewCmd", &json!({"refs": [{"edge": [1000, 1000, 1000]}]})).is_err()); - assert!(s.execute("ProjectNewCmd", &json!({"refs": [{"plane": "XY"}]})).is_err(), "parallel plane projects to nothing"); + assert!(s.execute("sketch.project", &json!({"refs": [{"edge": [1000, 1000, 1000]}]})).is_err()); + assert!(s.execute("sketch.project", &json!({"refs": [{"plane": "XY"}]})).is_err(), "parallel plane projects to nothing"); } #[test] fn intersect_cuts_a_body_with_the_sketch_plane() { let mut s = Session::default(); - run(&mut s, "PrimitiveCylinder", json!({"base": [0, 0, 0], "radius": 7, "height": 30})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "offset": 12})); + run(&mut s, "solid.cylinder", json!({"base": [0, 0, 0], "radius": 7, "height": 30})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 12})); let name = s.model.state().bodies[0].name.clone(); - let r = run(&mut s, "IntersectCmd", json!({"refs": [{"body": name}]})); + let r = run(&mut s, "sketch.intersect", json!({"refs": [{"body": name}]})); let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == r["links"][0]["curves"][0]).unwrap().clone(); assert_eq!(c["type"], "circle", "{c}"); @@ -148,7 +148,7 @@ fn intersect_cuts_a_body_with_the_sketch_plane() { fn break_link_makes_normal_geometry() { let mut s = Session::default(); box_part(&mut s); - let r = run(&mut s, "SketchCreate", json!({"plane": {"face": [20, 15, 20]}})); + let r = run(&mut s, "sketch.create", json!({"plane": {"face": [20, 15, 20]}})); let link = r["projected"].as_str().unwrap().to_string(); run(&mut s, "sketch.break_link", json!({"link": link})); let si = inspect(&mut s); @@ -162,11 +162,11 @@ fn break_link_makes_normal_geometry() { #[test] fn lost_reference_keeps_geometry_with_a_warning() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10})); let boxf = s.doc.features.last().unwrap().id; - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [5, 0, 10]}]})); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.project", json!({"refs": [{"edge": [5, 0, 10]}]})); + run(&mut s, "sketch.finish", json!({})); run(&mut s, "timeline.suppress", json!({"feature": boxf, "suppressed": true})); let st = s.model.state(); let ss = st.sketches.last().unwrap(); @@ -180,8 +180,8 @@ fn lost_reference_keeps_geometry_with_a_warning() { fn drawing_on_a_model_vertex_projects_it() { let mut s = Session::default(); box_part(&mut s); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "DrawPolyline", json!({"points": ["vertex:40,30,20", [60, 45]]})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.line", json!({"points": ["vertex:40,30,20", [60, 45]]})); let l = ids(&r["curves"])[0].clone(); let si = inspect(&mut s); let start = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == l).unwrap()["start"].clone(); @@ -190,7 +190,7 @@ fn drawing_on_a_model_vertex_projects_it() { assert_eq!(p["fixed"], true); assert!((p["at"][0].as_f64().unwrap() - 40.0).abs() < 1e-9); // Snapping onto an edge projects it and keeps the point on it. - let r = run(&mut s, "DrawPolyline", json!({"points": [{"on_edge": [20, 0, 20]}, [20, -15]]})); + let r = run(&mut s, "sketch.line", json!({"points": [{"on_edge": [20, 0, 20]}, [20, -15]]})); assert_eq!(ids(&r["curves"]).len(), 1); let si = inspect(&mut s); assert!(si["constraints"].as_array().unwrap().iter().any(|c| c["type"] == "point_on_curve"), "{si}"); @@ -199,41 +199,41 @@ fn drawing_on_a_model_vertex_projects_it() { #[test] fn fit_curves_to_a_mesh_section() { let mut s = Session::default(); - run(&mut s, "PrimitiveCylinder", json!({"base": [0, 0, 0], "radius": 7, "height": 30})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "offset": 10})); + run(&mut s, "solid.cylinder", json!({"base": [0, 0, 0], "radius": 7, "height": 30})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 10})); let name = s.model.state().bodies[0].name.clone(); - let r = run(&mut s, "FitCurvesToSectionCommand", json!({"body": name})); + let r = run(&mut s, "sketch.fit_curves_to_section", json!({"body": name})); assert_eq!(r["curves"].as_array().unwrap().len(), 1, "{r}"); let si = inspect(&mut s); assert!(si["links"].as_array().unwrap().is_empty()); let c = &si["curves"][0]; assert_eq!(c["type"], "circle"); assert!(c.get("link").is_none()); - assert!(s.execute("FitCurvesToSectionCommand", &json!({"body": "nope"})).is_err()); + assert!(s.execute("sketch.fit_curves_to_section", &json!({"body": "nope"})).is_err()); } #[test] fn three_d_sketch_curves() { let mut s = Session::default(); // Include keeps an out-of-plane edge 3D. - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10})); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "Include3DGeometry", json!({"refs": [{"edge": [10, 10, 5]}]})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.include_3d", json!({"refs": [{"edge": [10, 10, 5]}]})); let st = s.model.state(); let sk = &st.sketch(s.active_sketch.unwrap()).unwrap().sketch; assert_eq!(sk.wires.len(), 1, "{r}"); assert!((sk.wires[0].pts.iter().map(|p| p.z).fold(0.0, f64::max) - 10.0).abs() < 1e-9); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); // Project a line onto a sphere along the sketch normal. let mut s = Session::default(); - run(&mut s, "PrimitiveSphere", json!({"center": [0, 0, 0], "radius": 20})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "offset": 50})); - let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[-10, 0], [10, 0]]}))["curves"])[0].clone(); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "solid.sphere", json!({"center": [0, 0, 0], "radius": 20})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 50})); + let l = ids(&run(&mut s, "sketch.line", json!({"points": [[-10, 0], [10, 0]]}))["curves"])[0].clone(); + run(&mut s, "sketch.finish", json!({})); let src = s.doc.features.last().unwrap().id; - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "ProjectToSurface", json!({"curves": [{"sketch": src, "curve": l}], "face": [0, 0, 20]})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.project_to_surface", json!({"curves": [{"sketch": src, "curve": l}], "face": [0, 0, 20]})); let wid = r["links"][0]["wires"][0].as_str().unwrap().to_string(); let st = s.model.state(); let sk = &st.sketch(s.active_sketch.unwrap()).unwrap().sketch; @@ -246,16 +246,16 @@ fn three_d_sketch_curves() { // Delete the wire. run(&mut s, "sketch.delete", json!({"entities": [wid]})); assert!(s.model.state().sketch(s.active_sketch.unwrap()).unwrap().sketch.wires.is_empty()); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); // Two crossing cylinders meet in 3D curves. let mut s = Session::default(); - run(&mut s, "PrimitiveCylinder", json!({"base": [0, 0, -20], "radius": 5, "height": 40})); - run(&mut s, "PrimitiveCylinder", json!({"base": [-20, 0, 0], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"})); + run(&mut s, "solid.cylinder", json!({"base": [0, 0, -20], "radius": 5, "height": 40})); + run(&mut s, "solid.cylinder", json!({"base": [-20, 0, 0], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"})); let names: Vec = s.model.state().bodies.iter().map(|b| b.name.clone()).collect(); assert_eq!(names.len(), 2, "{names:?}"); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "IntersectionCurve", json!({"a": {"body": names[0]}, "b": {"body": names[1]}})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.intersection_curve", json!({"a": {"body": names[0]}, "b": {"body": names[1]}})); assert!(!r["wires"].as_array().unwrap().is_empty(), "{r}"); let st = s.model.state(); let sk = &st.sketch(s.active_sketch.unwrap()).unwrap().sketch; @@ -265,14 +265,14 @@ fn three_d_sketch_curves() { assert!(((p.y * p.y + p.z * p.z).sqrt() - 4.0).abs() < 0.1, "{p:?}"); } } - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); // Spun profile of a cylinder about Z, on the XZ plane: a straight outline 6 from the axis. let mut s = Session::default(); - run(&mut s, "PrimitiveCylinder", json!({"base": [0, 0, 0], "radius": 6, "height": 20})); + run(&mut s, "solid.cylinder", json!({"base": [0, 0, 0], "radius": 6, "height": 20})); let name = s.model.state().bodies[0].name.clone(); - run(&mut s, "SketchCreate", json!({"plane": "XZ"})); - let r = run(&mut s, "SpunProfileCmd", json!({"body": name, "axis": "Z"})); + run(&mut s, "sketch.create", json!({"plane": "XZ"})); + let r = run(&mut s, "sketch.spun_profile", json!({"body": name, "axis": "Z"})); let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == r["curves"][0]).unwrap().clone(); assert_eq!(c["type"], "line", "{c}"); @@ -282,15 +282,15 @@ fn three_d_sketch_curves() { #[test] fn isoparametric_curves_of_a_cylinder() { let mut s = Session::default(); - run(&mut s, "PrimitiveCylinder", json!({"base": [0, 0, 0], "radius": 6, "height": 20})); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); + run(&mut s, "solid.cylinder", json!({"base": [0, 0, 0], "radius": 6, "height": 20})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); // u: around (a circle at z = 10); v: along (a line from z 0 to 20). - let r = run(&mut s, "SketchIsoparametricCurve", json!({"face": [6, 0, 10]})); + let r = run(&mut s, "sketch.isoparametric_curve", json!({"face": [6, 0, 10]})); let st = s.model.state(); let sk = &st.sketch(s.active_sketch.unwrap()).unwrap().sketch; let w = &sk.wires[sk.wire_index(r["wires"][0].as_str().unwrap()).unwrap()]; assert!(w.pts.iter().all(|p| (p.z - 10.0).abs() < 1e-6 && ((p.x * p.x + p.y * p.y).sqrt() - 6.0).abs() < 1e-3), "{:?}", &w.pts[..3]); - let r = run(&mut s, "SketchIsoparametricCurve", json!({"face": [6, 0, 10], "direction": "v"})); + let r = run(&mut s, "sketch.isoparametric_curve", json!({"face": [6, 0, 10], "direction": "v"})); let st = s.model.state(); let sk = &st.sketch(s.active_sketch.unwrap()).unwrap().sketch; let w = &sk.wires[sk.wire_index(r["wires"][0].as_str().unwrap()).unwrap()]; @@ -302,10 +302,10 @@ fn isoparametric_curves_of_a_cylinder() { #[test] fn tilted_circles_project_to_exact_ellipses_and_splines_stay_splines() { let mut s = Session::default(); - run(&mut s, "PrimitiveCylinder", json!({"base": [0, 0, 0], "axis": [1, 0, 1], "radius": 5, "height": 20})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "offset": -10})); + run(&mut s, "solid.cylinder", json!({"base": [0, 0, 0], "axis": [1, 0, 1], "radius": 5, "height": 20})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": -10})); // The base circle (centred at the origin) seen from above. - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [0, 5, 0]}]})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"edge": [0, 5, 0]}]})); // One seam half: exact conics (rational quadratic arcs). let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == r["links"][0]["curves"][0]).unwrap().clone(); @@ -320,7 +320,7 @@ fn tilted_circles_project_to_exact_ellipses_and_splines_stay_splines() { } } // The whole face (both halves of its rim, plus its seam) gives the full ellipse. - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"face": [0, 0, 0]}]})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"face": [0, 0, 0]}]})); let si = inspect(&mut s); let types: Vec = r["links"][0]["curves"] .as_array() @@ -329,7 +329,7 @@ fn tilted_circles_project_to_exact_ellipses_and_splines_stay_splines() { .map(|id| si["curves"].as_array().unwrap().iter().find(|c| c["id"] == *id).unwrap()["type"].as_str().unwrap().to_string()) .collect(); assert!(types.iter().all(|t| t == "conic" || t == "ellipse"), "{types:?}"); - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [0, -5, 0]}]})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"edge": [0, -5, 0]}]})); let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == r["links"][0]["curves"][0]).unwrap().clone(); if c["type"] == "ellipse" { @@ -337,14 +337,14 @@ fn tilted_circles_project_to_exact_ellipses_and_splines_stay_splines() { let major = &c["major_at"]; assert!(((major[0].as_f64().unwrap().powi(2) + major[1].as_f64().unwrap().powi(2)).sqrt() - 5.0).abs() < 1e-6, "{c}"); } - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); // A spline of a parallel sketch projects as the same spline. - run(&mut s, "SketchCreate", json!({"plane": "XY", "offset": 30})); - let sp = run(&mut s, "DrawSpline", json!({"points": [[0, 0], [10, 5], [20, 0]]}))["curves"][0].clone(); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 30})); + let sp = run(&mut s, "sketch.spline.fit_point", json!({"points": [[0, 0], [10, 5], [20, 0]]}))["curves"][0].clone(); + run(&mut s, "sketch.finish", json!({})); let sk1 = s.doc.features.last().unwrap().id; - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"sketch": sk1, "curve": sp}]})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"sketch": sk1, "curve": sp}]})); let si = inspect(&mut s); let c = si["curves"].as_array().unwrap().iter().find(|c| c["id"] == r["links"][0]["curves"][0]).unwrap().clone(); assert_eq!(c["type"], "spline", "{c}"); @@ -355,13 +355,13 @@ fn tilted_circles_project_to_exact_ellipses_and_splines_stay_splines() { fn sweeps_and_pipes_follow_3d_sketch_curves() { // A line projected onto a sphere: a 3D arc on its top. let mut s = Session::default(); - run(&mut s, "PrimitiveSphere", json!({"center": [0, 0, 0], "radius": 20})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "offset": 50})); - let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[-12, 0], [12, 0]]}))["curves"])[0].clone(); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "solid.sphere", json!({"center": [0, 0, 0], "radius": 20})); + run(&mut s, "sketch.create", json!({"plane": "XY", "offset": 50})); + let l = ids(&run(&mut s, "sketch.line", json!({"points": [[-12, 0], [12, 0]]}))["curves"])[0].clone(); + run(&mut s, "sketch.finish", json!({})); let src = s.doc.features.last().unwrap().id; - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "ProjectToSurface", json!({"curves": [{"sketch": src, "curve": l}], "face": [0, 0, 20]})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.project_to_surface", json!({"curves": [{"sketch": src, "curve": l}], "face": [0, 0, 20]})); let wid = r["links"][0]["wires"][0].as_str().unwrap().to_string(); let path_sketch = s.active_sketch.unwrap(); let pts = { @@ -369,11 +369,11 @@ fn sweeps_and_pipes_follow_3d_sketch_curves() { let sk = &st.sketch(path_sketch).unwrap().sketch; sk.wires[sk.wire_index(&wid).unwrap()].pts.clone() }; - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.finish", json!({})); let len: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum(); // A pipe along it: volume ≈ π r² × length. let before = s.model.state().bodies.len(); - run(&mut s, "PrimitivePipe", json!({"path_sketch": path_sketch, "path": [wid.clone()], "diameter": "2 mm", "operation": "new"})); + run(&mut s, "solid.pipe", json!({"path_sketch": path_sketch, "path": [wid.clone()], "diameter": "2 mm", "operation": "new"})); let st = s.model.state(); assert_eq!(st.bodies.len(), before + 1); let v = st.bodies.last().unwrap().mesh().measure().volume; @@ -385,11 +385,11 @@ fn sweeps_and_pipes_follow_3d_sketch_curves() { let t = (b - a).normalized().unwrap(); let x = t.cross(solvecraft_geom::Vec3::Z).normalized().unwrap(); let y = t.cross(x); - run(&mut s, "SketchCreate", json!({"plane": {"origin": [a.x, a.y, a.z], "x_dir": [x.x, x.y, x.z], "y_dir": [y.x, y.y, y.z]}})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [-1, -1], "p1": [1, 1]})); + run(&mut s, "sketch.create", json!({"plane": {"origin": [a.x, a.y, a.z], "x_dir": [x.x, x.y, x.z], "y_dir": [y.x, y.y, y.z]}})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [-1, -1], "p1": [1, 1]})); let prof = s.active_sketch.unwrap(); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Sweep", json!({"sketch": prof, "path_sketch": path_sketch, "path": [wid], "operation": "new"})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.sweep", json!({"sketch": prof, "path_sketch": path_sketch, "path": [wid], "operation": "new"})); let st = s.model.state(); assert_eq!(st.bodies.len(), before + 2); let v = st.bodies.last().unwrap().mesh().measure().volume; diff --git a/crates/engine/src/cmd/sketch_rect_tests.rs b/crates/engine/src/cmd/sketch_rect_tests.rs index 4f0f0f43..1bbb9b34 100644 --- a/crates/engine/src/cmd/sketch_rect_tests.rs +++ b/crates/engine/src/cmd/sketch_rect_tests.rs @@ -19,11 +19,11 @@ fn ids(v: &Value) -> Vec { /// A new sketch with one rectangle of the given kind; returns its four side ids. fn rect(kind: &str) -> (Session, Vec) { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); let r = match kind { - "center" => run(&mut s, "ShapeRectangleCenter", json!({"center": [3, 2], "corner": [13, 9]})), - "two" => run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [1, 1], "p1": [21, 15]})), - _ => run(&mut s, "ShapeRectangleThreePoint", json!({"p0": [0, 0], "p1": [20, 5], "p2": [18, 15]})), + "center" => run(&mut s, "sketch.rectangle.center", json!({"center": [3, 2], "corner": [13, 9]})), + "two" => run(&mut s, "sketch.rectangle.two_point", json!({"p0": [1, 1], "p1": [21, 15]})), + _ => run(&mut s, "sketch.rectangle.three_point", json!({"p0": [0, 0], "p1": [20, 5], "p2": [18, 15]})), }; let sides = ids(&r["curves"]); assert_eq!(sides.len(), 4, "{r}"); @@ -54,9 +54,9 @@ fn fresh_rectangles_have_the_expected_freedom() { fn rectangles_take_dimensions_without_conflict() { for k in KINDS { let (mut s, l) = rect(k); - run(&mut s, "SketchDimension", json!({"entities": [l[0]], "value": 30})); + run(&mut s, "sketch.dimension", json!({"entities": [l[0]], "value": 30})); healthy(&mut s, &format!("{k} width")); - run(&mut s, "SketchDimension", json!({"entities": [l[1]], "value": 12})); + run(&mut s, "sketch.dimension", json!({"entities": [l[1]], "value": 12})); let dof = healthy(&mut s, &format!("{k} width+height")); assert_eq!(dof, if k == "three" { 3 } else { 2 }, "{k}"); } @@ -72,12 +72,12 @@ fn rectangles_can_be_dragged_pinned_and_made_square() { run(&mut s, "sketch.move_point", json!({"point": format!("{}.end", l[2]), "to": [25, 22]})); healthy(&mut s, &format!("{k} drag other corner")); // Pin a corner to the origin. - run(&mut s, "ConstraintCoincident", json!({"a": format!("{}.start", l[0]), "b": "origin"})); + run(&mut s, "sketch.constraint.coincident", json!({"a": format!("{}.start", l[0]), "b": "origin"})); healthy(&mut s, &format!("{k} corner on origin")); // Square: equal sides. - run(&mut s, "ConstraintEqual", json!({"a": l[0], "b": l[1]})); + run(&mut s, "sketch.constraint.equal", json!({"a": l[0], "b": l[1]})); healthy(&mut s, &format!("{k} square")); - run(&mut s, "SketchDimension", json!({"entities": [l[0]], "value": 10})); + run(&mut s, "sketch.dimension", json!({"entities": [l[0]], "value": 10})); let dof = healthy(&mut s, &format!("{k} square sized")); assert_eq!(dof, if k == "three" { 1 } else { 0 }, "{k}"); } diff --git a/crates/engine/src/cmd/sketch_ux_tests.rs b/crates/engine/src/cmd/sketch_ux_tests.rs index 5d2804a2..d58c46ab 100644 --- a/crates/engine/src/cmd/sketch_ux_tests.rs +++ b/crates/engine/src/cmd/sketch_ux_tests.rs @@ -20,7 +20,7 @@ fn sketch(s: &Session) -> Sketch { fn new_sketch() -> Session { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); s } @@ -30,11 +30,11 @@ fn new_sketch() -> Session { #[test] fn every_dimension_kind_has_a_drawable_frame() { let mut s = new_sketch(); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [60, 40]})); - run(&mut s, "CircleCenterRadius", json!({"center": [30, 20], "radius": 8})); - run(&mut s, "ArcCenterTwoPoint", json!({"center": [100, 0], "start": [110, 0], "end": [100, 10]})); - run(&mut s, "DrawPolyline", json!({"points": [[-40, 0], [-10, 0]]})); - run(&mut s, "DrawPolyline", json!({"points": [[-40, 0], [-20, 25]]})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [60, 40]})); + run(&mut s, "sketch.circle.center", json!({"center": [30, 20], "radius": 8})); + run(&mut s, "sketch.arc.center_point", json!({"center": [100, 0], "start": [110, 0], "end": [100, 10]})); + run(&mut s, "sketch.line", json!({"points": [[-40, 0], [-10, 0]]})); + run(&mut s, "sketch.line", json!({"points": [[-40, 0], [-20, 25]]})); for (ents, ty) in [ (json!(["l1"]), "auto"), (json!(["l2"]), "vertical"), @@ -44,9 +44,9 @@ fn every_dimension_kind_has_a_drawable_frame() { (json!(["a1"]), "arc_length"), (json!(["l5", "l6"]), "angle"), ] { - run(&mut s, "SketchDimension", json!({"entities": ents, "type": ty})); + run(&mut s, "sketch.dimension", json!({"entities": ents, "type": ty})); } - run(&mut s, "SketchDimension", json!({"entities": ["c1.center", "l2"], "driven": true})); + run(&mut s, "sketch.dimension", json!({"entities": ["c1.center", "l2"], "driven": true})); let sk = sketch(&s); let dims: Vec<_> = sk.constraints.iter().filter(|c| c.kind.is_dimension()).collect(); assert_eq!(dims.len(), 8); @@ -71,8 +71,8 @@ fn every_dimension_kind_has_a_drawable_frame() { #[test] fn dimension_text_is_stored_relative_to_the_dimension_and_follows_it() { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [40, 0]]})); - let d = run(&mut s, "SketchDimension", json!({"entities": ["l1"], "text_at": [20, -8]})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0]]})); + let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "text_at": [20, -8]})); let param = d["param"].as_str().unwrap_or_default().to_string(); let text = |s: &Session| { let sk = sketch(s); @@ -102,8 +102,8 @@ fn dimension_text_is_stored_relative_to_the_dimension_and_follows_it() { #[test] fn deleting_a_dimension_frees_its_parameter() { let mut s = new_sketch(); - run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 5})); - let d = run(&mut s, "SketchDimension", json!({"entities": ["c1"]})); + run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 5})); + let d = run(&mut s, "sketch.dimension", json!({"entities": ["c1"]})); let param = d["param"].as_str().unwrap_or_default().to_string(); let id = sketch(&s).constraints.iter().find(|c| c.param.as_deref() == Some(param.as_str())).map(|c| c.id.clone()).expect("dim"); run(&mut s, "sketch.delete", json!({"entities": [id]})); @@ -126,22 +126,22 @@ fn dof(s: &mut Session) -> i64 { #[test] fn fixed_points_survive_drags_and_solves_and_unfix_frees_them() { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 0], [30, 20]]})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0], [30, 20]]})); let free = dof(&mut s); - let r = run(&mut s, "ConstraintFix", json!({"entity": "l1.end"})); + let r = run(&mut s, "sketch.constraint.fix", json!({"entity": "l1.end"})); assert_eq!(r["result"]["fixed"], true); assert_eq!(dof(&mut s), free - 2); let corner = pt(&s, "l1.end"); // Dragging the fixed point does nothing; dragging its neighbours never moves it. run(&mut s, "sketch.move_point", json!({"point": "l1.end", "to": [50, 50]})); assert_eq!(pt(&s, "l1.end"), corner); - run(&mut s, "ConstraintHorizontalVertical", json!({"line": "l2", "mode": "vertical"})); + run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l2", "mode": "vertical"})); run(&mut s, "sketch.move_point", json!({"point": "l2.end", "to": [40, 25]})); - run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 12})); + run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 12})); assert!(pt(&s, "l1.end").dist(corner) < 1e-12, "{:?}", pt(&s, "l1.end")); assert!((pt(&s, "l1.start").dist(corner) - 12.0).abs() < 1e-6); // Unfix: the point moves again. - let r = run(&mut s, "ConstraintFix", json!({"entity": "l1.end"})); + let r = run(&mut s, "sketch.constraint.fix", json!({"entity": "l1.end"})); assert_eq!(r["result"]["fixed"], false); run(&mut s, "sketch.move_point", json!({"point": "l1.end", "to": [30, -5]})); assert!(pt(&s, "l1.end").dist(corner) > 1.0); @@ -150,13 +150,13 @@ fn fixed_points_survive_drags_and_solves_and_unfix_frees_them() { #[test] fn fixing_curves_holds_their_points_and_radius() { let mut s = new_sketch(); - run(&mut s, "CircleCenterRadius", json!({"center": [10, 10], "radius": 5})); - run(&mut s, "DrawPolyline", json!({"points": [[30, 0], [50, 0]]})); - run(&mut s, "ArcCenterTwoPoint", json!({"center": [0, 40], "start": [10, 40], "end": [0, 50]})); - run(&mut s, "CircleElipse", json!({"center": [60, 40], "major": [70, 40], "minor_radius": 4})); + run(&mut s, "sketch.circle.center", json!({"center": [10, 10], "radius": 5})); + run(&mut s, "sketch.line", json!({"points": [[30, 0], [50, 0]]})); + run(&mut s, "sketch.arc.center_point", json!({"center": [0, 40], "start": [10, 40], "end": [0, 50]})); + run(&mut s, "sketch.ellipse", json!({"center": [60, 40], "major": [70, 40], "minor_radius": 4})); let free = dof(&mut s); // Multi-select fixes them all (circle 3, line 4, arc 5, ellipse 5 degrees of freedom). - let r = run(&mut s, "ConstraintFix", json!({"entities": ["c1", "l1", "a1", "e1"]})); + let r = run(&mut s, "sketch.constraint.fix", json!({"entities": ["c1", "l1", "a1", "e1"]})); assert_eq!(r["result"]["changed"], 4); assert_eq!(dof(&mut s), free - 17); let si = run(&mut s, "sketch.inspect", json!({})); @@ -164,18 +164,18 @@ fn fixing_curves_holds_their_points_and_radius() { // A fixed circle keeps centre and radius; a dimension on it conflicts. run(&mut s, "sketch.move_point", json!({"point": "c1.center", "to": [0, 0]})); assert_eq!(pt(&s, "c1.center"), solvecraft_geom::Vec2::new(10.0, 10.0)); - assert!(s.execute("SketchDimension", &json!({"entities": ["c1"], "value": 20})).is_err()); + assert!(s.execute("sketch.dimension", &json!({"entities": ["c1"], "value": 20})).is_err()); let r0 = sketch(&s).radius(0); assert_eq!(r0, Some(5.0)); // Toggle without `fixed`: all fixed, so all are unfixed. - let r = run(&mut s, "ConstraintFix", json!({"entities": ["c1", "l1", "a1", "e1"]})); + let r = run(&mut s, "sketch.constraint.fix", json!({"entities": ["c1", "l1", "a1", "e1"]})); assert_eq!(r["result"]["fixed"], false); assert_eq!(dof(&mut s), free); - run(&mut s, "SketchDimension", json!({"entities": ["c1"], "type": "radius", "value": 7})); + run(&mut s, "sketch.dimension", json!({"entities": ["c1"], "type": "radius", "value": 7})); assert!((sketch(&s).radius(0).unwrap_or(0.0) - 7.0).abs() < 1e-9); // Mixed selection: one fixed, one free → both end up fixed. - run(&mut s, "ConstraintFix", json!({"entity": "l1"})); - let r = run(&mut s, "ConstraintFix", json!({"entities": ["l1", "l1.start", "a1"]})); + run(&mut s, "sketch.constraint.fix", json!({"entity": "l1"})); + let r = run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1", "l1.start", "a1"]})); assert_eq!(r["result"]["fixed"], true); assert!(sketch(&s).curves.iter().filter(|c| c.id == "l1" || c.id == "a1").all(|c| c.fixed)); } @@ -183,17 +183,17 @@ fn fixing_curves_holds_their_points_and_radius() { #[test] fn projected_geometry_counts_as_fixed() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 5})); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [10, 0, 5]}]})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 5})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.project", json!({"refs": [{"edge": [10, 0, 5]}]})); let sk = sketch(&s); let ci = sk.curves.iter().position(|c| c.link.is_some()).unwrap_or_else(|| panic!("projected: {r}")); let id = sk.curves[ci].id.clone(); assert!(sk.curve_locked(ci)); - let r = run(&mut s, "ConstraintFix", json!({"entity": id})); + let r = run(&mut s, "sketch.constraint.fix", json!({"entity": id})); // Already fixed: the toggle unfixes, which projected geometry ignores. assert_eq!(r["result"]["changed"], 0); assert!(sketch(&s).curve_locked(ci)); @@ -213,11 +213,11 @@ fn curve_ids(v: &Value) -> Vec { #[test] fn construction_toggle_takes_shapes_out_of_profiles_and_back() { let mut s = new_sketch(); - let rect = curve_ids(&run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]}))); - let slot = curve_ids(&run(&mut s, "ShapeSlotCenterToCenter", json!({"p0": [40, 5], "p1": [60, 5], "width": 6}))); - let poly = curve_ids(&run(&mut s, "ShapePolygonInscribed", json!({"center": [0, 40], "radius": 8, "sides": 6}))); - let circ = curve_ids(&run(&mut s, "CircleCenterRadius", json!({"center": [40, 40], "radius": 5}))); - let ell = curve_ids(&run(&mut s, "CircleElipse", json!({"center": [70, 40], "major": [80, 40], "minor_radius": 4}))); + let rect = curve_ids(&run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]}))); + let slot = curve_ids(&run(&mut s, "sketch.slot.center_to_center", json!({"p0": [40, 5], "p1": [60, 5], "width": 6}))); + let poly = curve_ids(&run(&mut s, "sketch.polygon.inscribed", json!({"center": [0, 40], "radius": 8, "sides": 6}))); + let circ = curve_ids(&run(&mut s, "sketch.circle.center", json!({"center": [40, 40], "radius": 5}))); + let ell = curve_ids(&run(&mut s, "sketch.ellipse", json!({"center": [70, 40], "major": [80, 40], "minor_radius": 4}))); assert_eq!(profiles(&mut s), 5); for (shape, left) in [(&rect, 4), (&slot, 3), (&poly, 2), (&circ, 1), (&ell, 0)] { let r = run(&mut s, "sketch.construction", json!({ "curves": shape })); @@ -240,12 +240,12 @@ fn construction_toggle_takes_shapes_out_of_profiles_and_back() { #[test] fn projected_geometry_can_become_construction() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 5})); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [10, 0, 5]}]})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 5})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.project", json!({"refs": [{"edge": [10, 0, 5]}]})); let id = sketch(&s).curves.iter().find(|c| c.link.is_some()).map(|c| c.id.clone()).expect("projected"); let r = run(&mut s, "sketch.construction", json!({ "curves": [id.clone()] })); assert_eq!(r["changed"], 1); @@ -273,8 +273,8 @@ fn same(a: &(Vec, Option), b: &(Vec Session { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[5, 3], [45, 3]]})); - run(&mut s, "CircleCenterRadius", json!({"center": [20, 18], "radius": 5})); + run(&mut s, "sketch.line", json!({"points": [[5, 3], [45, 3]]})); + run(&mut s, "sketch.circle.center", json!({"center": [20, 18], "radius": 5})); s } @@ -291,40 +291,40 @@ fn tangent_moves_only_the_second_pick() { // Line then circle: only the circle moves (and keeps its size). let mut s = line_and_circle(); let l0 = snap(&s, "l1"); - run(&mut s, "ConstraintTangent", json!({"a": "l1", "b": "c1"})); + run(&mut s, "sketch.constraint.tangent", json!({"a": "l1", "b": "c1"})); assert!(same(&snap(&s, "l1"), &l0), "line moved: {:?}", snap(&s, "l1")); let c = snap(&s, "c1"); assert!((c.1.unwrap_or(0.0) - 5.0).abs() < 1e-9 && (c.0[0].y - 8.0).abs() < 1e-6, "{c:?}"); // Circle then line: only the line moves. - check_order(line_and_circle(), "ConstraintTangent", "c1", "l1"); + check_order(line_and_circle(), "sketch.constraint.tangent", "c1", "l1"); } #[test] fn a_fixed_second_pick_makes_the_first_move() { let mut s = line_and_circle(); - run(&mut s, "ConstraintFix", json!({"entity": "l1"})); + run(&mut s, "sketch.constraint.fix", json!({"entity": "l1"})); let l0 = snap(&s, "l1"); let c0 = snap(&s, "c1"); - run(&mut s, "ConstraintTangent", json!({"a": "c1", "b": "l1"})); + run(&mut s, "sketch.constraint.tangent", json!({"a": "c1", "b": "l1"})); assert!(same(&snap(&s, "l1"), &l0)); assert!(!same(&snap(&s, "c1"), &c0)); // Fixed first: the second moves as usual. let mut s = line_and_circle(); - run(&mut s, "ConstraintFix", json!({"entity": "c1"})); + run(&mut s, "sketch.constraint.fix", json!({"entity": "c1"})); let c0 = snap(&s, "c1"); - run(&mut s, "ConstraintTangent", json!({"a": "c1", "b": "l1"})); + run(&mut s, "sketch.constraint.tangent", json!({"a": "c1", "b": "l1"})); assert!(same(&snap(&s, "c1"), &c0)); } #[test] fn two_fully_constrained_entities_conflict_and_nothing_moves() { let mut s = line_and_circle(); - run(&mut s, "ConstraintFix", json!({"entities": ["l1.start", "l1.end", "c1.center"]})); - run(&mut s, "SketchDimension", json!({"entities": ["c1"], "type": "radius", "value": 5})); + run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1.start", "l1.end", "c1.center"]})); + run(&mut s, "sketch.dimension", json!({"entities": ["c1"], "type": "radius", "value": 5})); let si = run(&mut s, "sketch.inspect", json!({})); assert!(si["curves"].as_array().is_some_and(|c| c.iter().all(|c| c["fully_constrained"] == true)), "{si}"); let before = sketch(&s); - let e = s.execute("ConstraintTangent", &json!({"a": "l1", "b": "c1"})); + let e = s.execute("sketch.constraint.tangent", &json!({"a": "l1", "b": "c1"})); assert!(e.is_err(), "{e:?}"); assert_eq!(sketch(&s), before); } @@ -333,22 +333,22 @@ fn two_fully_constrained_entities_conflict_and_nothing_moves() { fn line_pairs_keep_the_first_line() { let lines = || { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 2]]})); - run(&mut s, "DrawPolyline", json!({"points": [[5, 20], [25, 35]]})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 2]]})); + run(&mut s, "sketch.line", json!({"points": [[5, 20], [25, 35]]})); s }; let len = |s: &Session, l: &str| { let p = snap(s, l).0; p[0].dist(p[1]) }; - for cmd in ["ConstraintParallel", "ConstraintPerpendicular", "ConstraintEqual", "ConstraintCollinear"] { + for cmd in ["sketch.constraint.parallel", "sketch.constraint.perpendicular", "sketch.constraint.equal", "sketch.constraint.collinear"] { check_order(lines(), cmd, "l1", "l2"); check_order(lines(), cmd, "l2", "l1"); // The second line turns and slides; only Equal changes its length. let mut s = lines(); let l0 = len(&s, "l2"); run(&mut s, cmd, json!({"a": "l1", "b": "l2"})); - if cmd != "ConstraintEqual" { + if cmd != "sketch.constraint.equal" { assert!((len(&s, "l2") - l0).abs() < 1e-6, "{cmd}: {l0} -> {}", len(&s, "l2")); } else { assert!((len(&s, "l2") - len(&s, "l1")).abs() < 1e-6); @@ -360,17 +360,17 @@ fn line_pairs_keep_the_first_line() { fn circle_pairs_keep_the_first_circle() { let circles = || { let mut s = new_sketch(); - run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 5})); - run(&mut s, "CircleCenterRadius", json!({"center": [30, 10], "radius": 8})); + run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 5})); + run(&mut s, "sketch.circle.center", json!({"center": [30, 10], "radius": 8})); s }; - for cmd in ["ConstraintConcentric", "ConstraintEqual", "ConstraintTangent"] { + for cmd in ["sketch.constraint.concentric", "sketch.constraint.equal", "sketch.constraint.tangent"] { check_order(circles(), cmd, "c1", "c2"); check_order(circles(), cmd, "c2", "c1"); } // Equal resizes the second to the first. let mut s = circles(); - run(&mut s, "ConstraintEqual", json!({"a": "c2", "b": "c1"})); + run(&mut s, "sketch.constraint.equal", json!({"a": "c2", "b": "c1"})); assert!((snap(&s, "c1").1.unwrap_or(0.0) - 8.0).abs() < 1e-9); } @@ -378,25 +378,25 @@ fn circle_pairs_keep_the_first_circle() { fn point_constraints_keep_the_first_pick() { let pts = || { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 0]]})); - run(&mut s, "DrawPolyline", json!({"points": [[10, 10], [20, 25]]})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0]]})); + run(&mut s, "sketch.line", json!({"points": [[10, 10], [20, 25]]})); s }; - check_order(pts(), "ConstraintCoincident", "l2.start", "l1.end"); - check_order(pts(), "ConstraintCoincident", "l1.end", "l2.start"); + check_order(pts(), "sketch.constraint.coincident", "l2.start", "l1.end"); + check_order(pts(), "sketch.constraint.coincident", "l1.end", "l2.start"); // Curve picked first, point second: the point goes onto the line. - check_order(pts(), "ConstraintCoincident", "l1", "l2.start"); + check_order(pts(), "sketch.constraint.coincident", "l1", "l2.start"); // Midpoint: the point stays, the line moves. let mut s = pts(); let p0 = snap(&s, "l2.end"); - run(&mut s, "ConstraintMidPoint", json!({"point": "l2.end", "line": "l1"})); + run(&mut s, "sketch.constraint.midpoint", json!({"point": "l2.end", "line": "l1"})); assert!(same(&snap(&s, "l2.end"), &p0)); // Symmetry: the first point and the line stay, the second mirrors. let mut s = pts(); - run(&mut s, "DrawPoint", json!({"point": [3, 7]})); - run(&mut s, "DrawPoint", json!({"point": [4, -9]})); + run(&mut s, "sketch.point", json!({"point": [3, 7]})); + run(&mut s, "sketch.point", json!({"point": [4, -9]})); let (a0, l0) = (snap(&s, "p1"), snap(&s, "l1")); - run(&mut s, "ConstraintSymmetry", json!({"a": "p1", "b": "p2", "line": "l1"})); + run(&mut s, "sketch.constraint.symmetry", json!({"a": "p1", "b": "p2", "line": "l1"})); assert!(same(&snap(&s, "p1"), &a0) && same(&snap(&s, "l1"), &l0)); assert!(pt(&s, "p2").dist(solvecraft_geom::Vec2::new(3.0, -7.0)) < 1e-6, "{:?}", pt(&s, "p2")); } @@ -418,8 +418,8 @@ fn palette_options_are_kept_with_each_sketch() { assert!(v.hide_dimensions && v.no_snap && v.slice && v.three_d && !v.hide_points); // Another sketch starts with the defaults; the first keeps its options after finishing. let first = s.active_sketch.expect("sketch"); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "SketchCreate", json!({"plane": "XZ"})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "sketch.create", json!({"plane": "XZ"})); assert_eq!(sketch(&s).view, solvecraft_sketch::SketchView::default()); let o = run(&mut s, "sketch.options", json!({"sketch": first})); assert_eq!(o["options"]["show_dimensions"], false); @@ -430,7 +430,7 @@ fn palette_options_are_kept_with_each_sketch() { assert!(s.execute("sketch.options", &json!({"bogus": true})).is_err()); // Undo restores the option. run(&mut s, "sketch.options", json!({"show_points": false})); - run(&mut s, "UndoCommand", json!({})); + run(&mut s, "edit.undo", json!({})); assert!(!sketch(&s).view.hide_points); } @@ -448,7 +448,7 @@ fn v(x: f64, y: f64) -> solvecraft_geom::Vec2 { #[test] fn dragging_a_line_slides_it_and_an_end_keeps_the_other_end() { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[10, 10], [40, 20]]})); + run(&mut s, "sketch.line", json!({"points": [[10, 10], [40, 20]]})); drag(&mut s, "l1", [25.0, 15.0], [30.0, 25.0]); assert!(pt(&s, "l1.start").dist(v(15.0, 20.0)) < 1e-9 && pt(&s, "l1.end").dist(v(45.0, 30.0)) < 1e-9); drag(&mut s, "l1.end", [45.0, 30.0], [50.0, 0.0]); @@ -459,7 +459,7 @@ fn dragging_a_line_slides_it_and_an_end_keeps_the_other_end() { #[test] fn dragging_a_rectangle_side_moves_that_side_only() { let mut s = new_sketch(); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [10, 10], "p1": [40, 30]})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [10, 10], "p1": [40, 30]})); // l2 is the right side (x = 40): dragged right, it stays vertical, the left side stays. let left = snap(&s, "l4"); drag(&mut s, "l2", [40.0, 20.0], [50.0, 20.0]); @@ -471,7 +471,7 @@ fn dragging_a_rectangle_side_moves_that_side_only() { #[test] fn dragging_circles_and_arcs() { let mut s = new_sketch(); - run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 5})); + run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 5})); // The edge changes the radius about a fixed centre; the centre moves the circle. drag(&mut s, "c1", [5.0, 0.0], [0.0, 8.0]); assert!((snap(&s, "c1").1.unwrap_or(0.0) - 8.0).abs() < 1e-9); @@ -479,7 +479,7 @@ fn dragging_circles_and_arcs() { drag(&mut s, "c1.center", [0.0, 0.0], [20.0, 5.0]); assert!(pt(&s, "c1.center").dist(v(20.0, 5.0)) < 1e-9); assert!((snap(&s, "c1").1.unwrap_or(0.0) - 8.0).abs() < 1e-9); - run(&mut s, "ArcCenterTwoPoint", json!({"center": [50, 0], "start": [60, 0], "end": [50, 10]})); + run(&mut s, "sketch.arc.center_point", json!({"center": [50, 0], "start": [60, 0], "end": [50, 10]})); drag(&mut s, "a1", [57.07, 7.07], [50.0 + 200f64.sqrt(), 200f64.sqrt()]); assert!(pt(&s, "a1.center").dist(v(50.0, 0.0)) < 1e-9); assert!((snap(&s, "a1").1.unwrap_or(0.0) - 20.0).abs() < 1e-9, "{:?}", snap(&s, "a1")); @@ -489,16 +489,16 @@ fn dragging_circles_and_arcs() { #[test] fn constrained_geometry_resists_drags() { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[10, 10], [40, 10]]})); - run(&mut s, "ConstraintFix", json!({"entities": ["l1.start", "l1.end"]})); + run(&mut s, "sketch.line", json!({"points": [[10, 10], [40, 10]]})); + run(&mut s, "sketch.constraint.fix", json!({"entities": ["l1.start", "l1.end"]})); let l0 = snap(&s, "l1"); drag(&mut s, "l1", [25.0, 10.0], [25.0, 30.0]); assert!(same(&snap(&s, "l1"), &l0)); // A horizontal line dragged at its end stays horizontal; its length dimension holds. let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[10, 10], [40, 10]]})); - run(&mut s, "ConstraintHorizontalVertical", json!({"line": "l1"})); - run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 30})); + run(&mut s, "sketch.line", json!({"points": [[10, 10], [40, 10]]})); + run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l1"})); + run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 30})); drag(&mut s, "l1.end", [40.0, 10.0], [45.0, 20.0]); let (a, b) = (pt(&s, "l1.start"), pt(&s, "l1.end")); assert!((a.y - b.y).abs() < 1e-6 && (a.dist(b) - 30.0).abs() < 1e-6, "{a:?} {b:?}"); @@ -507,12 +507,12 @@ fn constrained_geometry_resists_drags() { #[test] fn fully_constrained_feedback_is_per_entity() { let mut s = new_sketch(); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [10, 10], "p1": [50, 35]})); - run(&mut s, "CircleCenterRadius", json!({"center": [80, 20], "radius": 8})); - run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 40})); - run(&mut s, "SketchDimension", json!({"entities": ["l2"], "value": 25})); - run(&mut s, "SketchDimension", json!({"entities": ["origin", "l1.start"], "type": "horizontal", "value": 10})); - run(&mut s, "SketchDimension", json!({"entities": ["origin", "l1.start"], "type": "vertical", "value": 10})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [10, 10], "p1": [50, 35]})); + run(&mut s, "sketch.circle.center", json!({"center": [80, 20], "radius": 8})); + run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 40})); + run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "value": 25})); + run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l1.start"], "type": "horizontal", "value": 10})); + run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l1.start"], "type": "vertical", "value": 10})); let si = run(&mut s, "sketch.inspect", json!({})); assert_eq!(si["dof"], 3); for c in si["curves"].as_array().expect("curves") { @@ -533,10 +533,10 @@ fn fully_constrained_feedback_is_per_entity() { #[test] fn chain_follows_shared_and_coincident_ends() { let mut s = new_sketch(); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]})); - run(&mut s, "DrawPolyline", json!({"points": [[40, 0], [50, 0]]})); - run(&mut s, "DrawPolyline", json!({"points": [[50.5, 0], [60, 5]]})); - run(&mut s, "CircleCenterRadius", json!({"center": [80, 0], "radius": 3})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 10]})); + run(&mut s, "sketch.line", json!({"points": [[40, 0], [50, 0]]})); + run(&mut s, "sketch.line", json!({"points": [[50.5, 0], [60, 5]]})); + run(&mut s, "sketch.circle.center", json!({"center": [80, 0], "radius": 3})); let chain = |s: &mut Session, c: &str| -> Vec { let v = run(s, "sketch.chain", json!({"curve": c})); v["curves"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default() @@ -545,7 +545,7 @@ fn chain_follows_shared_and_coincident_ends() { assert_eq!(chain(&mut s, "l5"), ["l5"]); assert_eq!(chain(&mut s, "c1"), ["c1"]); // Joined by a coincident constraint, the two lines are one chain. - run(&mut s, "ConstraintCoincident", json!({"a": "l5.end", "b": "l6.start"})); + run(&mut s, "sketch.constraint.coincident", json!({"a": "l5.end", "b": "l6.start"})); assert_eq!(chain(&mut s, "l6"), ["l6", "l5"]); assert!(s.execute("sketch.chain", &json!({"curve": "nope"})).is_err()); } @@ -553,9 +553,9 @@ fn chain_follows_shared_and_coincident_ends() { #[test] fn constraints_and_dimensions_can_be_selected_and_deleted_together() { let mut s = new_sketch(); - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 0]]})); - run(&mut s, "ConstraintHorizontalVertical", json!({"line": "l1"})); - run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 30})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0]]})); + run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l1"})); + run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 30})); let ids: Vec = sketch(&s).constraints.iter().map(|c| c.id.clone()).collect(); assert_eq!(ids.len(), 2); let items: Vec = ids.iter().map(|id| json!({"type": "sketch_constraint", "id": id})).collect(); @@ -603,14 +603,14 @@ fn three_d_curves_need_the_option_and_move_joined() { #[test] fn pipe_and_sweep_follow_3d_sketch_curves() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Path"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Path"})); run(&mut s, "sketch.options", json!({"sketch_3d": true})); let r = run(&mut s, "sketch.line3d", json!({"points": [[0, 0, 0], [0, 0, 30], [20, 0, 30]]})); let mut path = curve_ids(&r); let sp = run(&mut s, "sketch.spline3d", json!({"points": [[20, 0, 30], [35, 10, 35], [50, 0, 50]]})); path.extend(curve_ids(&sp)); - run(&mut s, "SketchStop", json!({})); - let p = run(&mut s, "PrimitivePipe", json!({"path_sketch": "Path", "path": path, "diameter": 4})); + run(&mut s, "sketch.finish", json!({})); + let p = run(&mut s, "solid.pipe", json!({"path_sketch": "Path", "path": path, "diameter": 4})); let si = run(&mut s, "document.inspect", json!({"measure": true})); let body = si["bodies"].as_array().and_then(|b| b.first()).cloned().unwrap_or_default(); // The tube runs from the first line's start to the spline's end (radius 2 around them; the diff --git a/crates/engine/src/cmd/surface.rs b/crates/engine/src/cmd/surface.rs index fdb8a649..23ff4ada 100644 --- a/crates/engine/src/cmd/surface.rs +++ b/crates/engine/src/cmd/surface.rs @@ -10,30 +10,30 @@ use crate::params::{bad, bool_, expr, str_, string_list, vec3}; use crate::{Result, Session}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("FusionSurfacePatchCommand", "Patch", patch) + CommandSpec::new("surface.patch", "Patch", patch) .at("SURFACE", "CREATE") .icon("patch") .params("sketch?: id|name and profiles? (planar patches of sketch regions) | edges: [[x,y,z]] (points on a closed loop of a body's edges) and body?; name?, body_name?"), - CommandSpec::new("FusionSurfaceStitchCommand", "Stitch", stitch) + CommandSpec::new("surface.stitch", "Stitch", stitch) .at("SURFACE", "MODIFY") .icon("stitch") .params("bodies: [names] (surfaces sewn into the first; a solid when they close); tolerance?: expr (default: tight)"), - CommandSpec::new("FusionSurfaceThickenCommand", "Thicken", thicken) + CommandSpec::new("surface.thicken", "Thicken", thicken) .at("SURFACE", "CREATE") .icon("thicken") .params("bodies: [surface names]; thickness: expr (negative: against the normal); symmetric?: bool; operation?: new|join|cut|intersect; targets?"), - CommandSpec::new("FusionExtendCommand", "Extend", extend) + CommandSpec::new("surface.extend", "Extend", extend) .at("SURFACE", "MODIFY") .icon("extend") .params("body: planar surface name; edges: [[x,y,z]] (points on straight edges to push out); distance: expr"), - CommandSpec::new("FusionSurfaceTrimCommand", "Trim", trim) + CommandSpec::new("surface.trim", "Trim", trim) .at("SURFACE", "MODIFY") .icon("trim") .params("body: surface name; plane: XY|XZ|YZ|plane name|{origin, normal} or tool: {body, point} (a face, its surface extended); keep: [x,y,z] (a point on the side to keep)"), ]; fn patch(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionSurfacePatchCommand"; + let cmd = "surface.patch"; let edges: Vec = p.get("edges").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default(); if !edges.is_empty() { if edges.len() > 10_000 { @@ -60,7 +60,7 @@ fn bodies_param(s: &Session, p: &Value, cmd: &str) -> Result> { } fn stitch(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionSurfaceStitchCommand"; + let cmd = "surface.stitch"; let bodies = bodies_param(s, p, cmd)?; let tolerance = expr(p, "tolerance").unwrap_or_else(|| "0".into()); check_expr(s, &tolerance, Kind::Length, cmd, "tolerance")?; @@ -68,7 +68,7 @@ fn stitch(s: &mut Session, p: &Value) -> Result { } fn thicken(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionSurfaceThickenCommand"; + let cmd = "surface.thicken"; let bodies = bodies_param(s, p, cmd)?; let thickness = expr(p, "thickness").ok_or_else(|| bad(cmd, "`thickness` is required"))?; check_expr(s, &thickness, Kind::Length, cmd, "thickness")?; @@ -83,7 +83,7 @@ fn thicken(s: &mut Session, p: &Value) -> Result { } fn extend(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionExtendCommand"; + let cmd = "surface.extend"; 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}`"))); @@ -98,7 +98,7 @@ fn extend(s: &mut Session, p: &Value) -> Result { } fn trim(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionSurfaceTrimCommand"; + let cmd = "surface.trim"; 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}`"))); @@ -133,26 +133,22 @@ mod tests { #[test] fn patch_trim_thicken_and_stitch() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "S"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [10, 10]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "FusionSurfacePatchCommand", json!({"sketch": "S", "body_name": "Sheet"})); + 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.finish", json!({})); + run(&mut s, "surface.patch", json!({"sketch": "S", "body_name": "Sheet"})); let st = s.world_state(); assert!(st.body("Sheet").unwrap().body.is_surface()); - run(&mut s, "FusionSurfaceTrimCommand", json!({"body": "Sheet", "plane": {"origin": [3, 0, 0], "normal": [1, 0, 0]}, "keep": [8, 5, 0]})); - run(&mut s, "FusionExtendCommand", json!({"body": "Sheet", "edges": [[10, 5, 0]], "distance": 5})); - run(&mut s, "FusionSurfaceThickenCommand", json!({"bodies": ["Sheet"], "thickness": "2 mm", "body_name": "Slab"})); + run(&mut s, "surface.trim", json!({"body": "Sheet", "plane": {"origin": [3, 0, 0], "normal": [1, 0, 0]}, "keep": [8, 5, 0]})); + run(&mut s, "surface.extend", json!({"body": "Sheet", "edges": [[10, 5, 0]], "distance": 5})); + run(&mut s, "surface.thicken", json!({"bodies": ["Sheet"], "thickness": "2 mm", "body_name": "Slab"})); let v = volume(&s, "Slab"); assert!((v - 240.0).abs() < 1e-9, "{v}"); // Stitch the patch of a box's open top back on. - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Cup"})); - run( - &mut s, - "FusionSurfacePatchCommand", - json!({"edges": [[25, 0, 10], [30, 5, 10], [25, 10, 10], [20, 5, 10]], "body": "Cup", "body_name": "Lid"}), - ); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Cup"})); + run(&mut s, "surface.patch", json!({"edges": [[25, 0, 10], [30, 5, 10], [25, 10, 10], [20, 5, 10]], "body": "Cup", "body_name": "Lid"})); assert!(s.world_state().body("Lid").unwrap().body.is_surface()); - assert!(s.execute("FusionSurfaceThickenCommand", &json!({"bodies": ["Nope"], "thickness": 1})).is_err()); - assert!(s.execute("FusionSurfaceTrimCommand", &json!({"body": "Sheet"})).is_err()); + assert!(s.execute("surface.thicken", &json!({"bodies": ["Nope"], "thickness": 1})).is_err()); + assert!(s.execute("surface.trim", &json!({"body": "Sheet"})).is_err()); } } diff --git a/crates/engine/src/cmd/surface_analysis.rs b/crates/engine/src/cmd/surface_analysis.rs index 513147a7..d917534d 100644 --- a/crates/engine/src/cmd/surface_analysis.rs +++ b/crates/engine/src/cmd/surface_analysis.rs @@ -9,27 +9,27 @@ use crate::params::{bad, bool_, num, vec3}; use crate::{Result, Session, SurfaceAnalysis}; pub static COMMANDS: &[CommandSpec] = &[ - CommandSpec::new("FusionZebraAnalysisCommand", "Zebra Analysis", zebra) + CommandSpec::new("inspect.zebra", "Zebra Analysis", zebra) .at("SKETCH", "INSPECT") .icon("zebra") .noundo() .params("stripes?: number across the view (default 12); clear?: true turns the analysis off"), - CommandSpec::new("FusionDraftAnalysisCommand", "Draft Analysis", draft) + CommandSpec::new("inspect.draft", "Draft Analysis", draft) .at("SKETCH", "INSPECT") .icon("draft_analysis") .noundo() .params("pull?: [x,y,z] direction (default Z); angle?: degrees (default 1); clear?: true turns the analysis off"), - CommandSpec::new("FusionEnvironmentMapAnalysisCommand", "Environment Map Analysis", |s, p| set(s, p, SurfaceAnalysis::Environment)) + CommandSpec::new("inspect.environment_map", "Environment Map Analysis", |s, p| set(s, p, SurfaceAnalysis::Environment)) .at("SKETCH", "INSPECT") .icon("environment_map") .noundo() .params("clear?: true turns the analysis off"), - CommandSpec::new("FusionAccessibilityAnalysisCommand", "Accessibility Analysis", accessibility) + CommandSpec::new("inspect.accessibility", "Accessibility Analysis", accessibility) .at("SKETCH", "INSPECT") .icon("accessibility") .noundo() .params("direction?: [x,y,z] the tool comes from (default Z, from above); clear?: true turns the analysis off"), - CommandSpec::new("FusionCurvatureMapAnalysisCommand", "Curvature Map Analysis", curvature_map) + CommandSpec::new("inspect.curvature_map", "Curvature Map Analysis", curvature_map) .at("SKETCH", "INSPECT") .icon("curvature_map") .noundo() @@ -49,13 +49,13 @@ fn set(s: &mut Session, p: &Value, a: SurfaceAnalysis) -> Result { fn zebra(s: &mut Session, p: &Value) -> Result { let stripes = num(p, "stripes").unwrap_or(12.0); if !(stripes.is_finite() && (1.0..=200.0).contains(&stripes)) { - return Err(bad("FusionZebraAnalysisCommand", "`stripes` must be 1…200")); + return Err(bad("inspect.zebra", "`stripes` must be 1…200")); } set(s, p, SurfaceAnalysis::Zebra { stripes }) } fn draft(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionDraftAnalysisCommand"; + let cmd = "inspect.draft"; let pull = match p.get("pull") { Some(v) => vec3(v).and_then(Vec3::normalized).ok_or_else(|| bad(cmd, "`pull` must be a non-zero [x,y,z]"))?, None => Vec3::Z, @@ -69,16 +69,14 @@ fn draft(s: &mut Session, p: &Value) -> Result { fn accessibility(s: &mut Session, p: &Value) -> Result { let dir = match p.get("direction") { - Some(v) => { - vec3(v).and_then(Vec3::normalized).ok_or_else(|| bad("FusionAccessibilityAnalysisCommand", "`direction` must be a non-zero [x,y,z]"))? - } + Some(v) => vec3(v).and_then(Vec3::normalized).ok_or_else(|| bad("inspect.accessibility", "`direction` must be a non-zero [x,y,z]"))?, None => Vec3::Z, }; set(s, p, SurfaceAnalysis::Access { dir }) } fn curvature_map(s: &mut Session, p: &Value) -> Result { - let cmd = "FusionCurvatureMapAnalysisCommand"; + let cmd = "inspect.curvature_map"; let radius = match num(p, "radius") { Some(r) if r.is_finite() && r > 1e-6 && r < 1e9 => r, Some(_) => return Err(bad(cmd, "`radius` must be positive")), @@ -100,22 +98,22 @@ mod tests { #[test] fn surface_analyses_are_view_state() { let mut s = Session::default(); - s.execute("PrimitiveBox", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); + s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); let undo = s.undo.len(); - s.execute("FusionZebraAnalysisCommand", &json!({"stripes": 20})).unwrap(); + s.execute("inspect.zebra", &json!({"stripes": 20})).unwrap(); assert_eq!(s.analysis, Some(SurfaceAnalysis::Zebra { stripes: 20.0 })); - s.execute("FusionDraftAnalysisCommand", &json!({"pull": [0, 0, 2], "angle": 3})).unwrap(); + s.execute("inspect.draft", &json!({"pull": [0, 0, 2], "angle": 3})).unwrap(); assert!(matches!(s.analysis, Some(SurfaceAnalysis::Draft { angle, .. }) if angle == 3.0)); - s.execute("FusionCurvatureMapAnalysisCommand", &json!({})).unwrap(); + s.execute("inspect.curvature_map", &json!({})).unwrap(); assert!(matches!(s.analysis, Some(SurfaceAnalysis::Curvature { radius }) if (radius - 300f64.sqrt() / 10.0).abs() < 1e-6)); - s.execute("FusionEnvironmentMapAnalysisCommand", &json!({})).unwrap(); + s.execute("inspect.environment_map", &json!({})).unwrap(); assert_eq!(s.analysis, Some(SurfaceAnalysis::Environment)); - s.execute("FusionAccessibilityAnalysisCommand", &json!({"direction": [0, 0, -3]})).unwrap(); + s.execute("inspect.accessibility", &json!({"direction": [0, 0, -3]})).unwrap(); assert_eq!(s.analysis, Some(SurfaceAnalysis::Access { dir: solvecraft_geom::Vec3::new(0.0, 0.0, -1.0) })); - s.execute("FusionCurvatureMapAnalysisCommand", &json!({"clear": true})).unwrap(); + s.execute("inspect.curvature_map", &json!({"clear": true})).unwrap(); assert_eq!(s.analysis, None); assert_eq!(s.undo.len(), undo, "no undo steps"); - assert!(s.execute("FusionZebraAnalysisCommand", &json!({"stripes": 0})).is_err()); - assert!(s.execute("FusionDraftAnalysisCommand", &json!({"pull": [0, 0, 0]})).is_err()); + assert!(s.execute("inspect.zebra", &json!({"stripes": 0})).is_err()); + assert!(s.execute("inspect.draft", &json!({"pull": [0, 0, 0]})).is_err()); } } diff --git a/crates/engine/src/frames.rs b/crates/engine/src/frames.rs index 8f68628d..a4b2de07 100644 --- a/crates/engine/src/frames.rs +++ b/crates/engine/src/frames.rs @@ -37,38 +37,36 @@ use K::*; /// The parameters each command takes in world coordinates. fn table(id: &str) -> &'static [(&'static str, K)] { match id { - "Extrude" | "Revolve" => &[("face", Pick), ("targets", Body), ("participants", Body)], - "PrimitiveBox" | "PrimitiveSphere" => &[("corner", Make), ("center", Make)], - "PrimitiveCylinder" | "PrimitiveCoil" | "PrimitiveTorus" => &[("base", Make), ("center", Make), ("axis", Dir)], - "FusionFilletEdgesCommand" | "FusionChamferCommand" => &[("edges", Picks), ("body", Body)], - "FusionHoleCommand" => &[("position", Pick), ("direction", Dir), ("body", Body)], - "ModifyScale" => &[("origin", Make), ("bodies", Body)], - "FusionOffsetFacesCommand" | "FusionShellBodyCommand" => &[("faces", Picks), ("body", Body)], - "FusionThreadCommand" => &[("face", Pick)], - "FusionDraftCommand" => &[("faces", Picks), ("neutral", Plane), ("pull", Dir), ("body", Body)], - "FusionPressPullCommand" => &[("face", Pick), ("edges", Picks)], - "FusionReplaceFaceCommand" => &[("faces", Picks), ("target_face", Pick), ("target", Plane), ("body", Body)], - "AlignCmd" => &[("from", Pick), ("from_face", Pick), ("to", Pick), ("to_face", Pick), ("bodies", Body)], - "MirrorCommand" | "FusionSplitBodyCommand" => &[("plane", Plane), ("bodies", Body), ("body", Body)], - "FusionBossCommand" | "FusionSnapFitCommand" | "FusionRestCommand" => { + "solid.extrude" | "solid.revolve" => &[("face", Pick), ("targets", Body), ("participants", Body)], + "solid.box" | "solid.sphere" => &[("corner", Make), ("center", Make)], + "solid.cylinder" | "solid.coil" | "solid.torus" => &[("base", Make), ("center", Make), ("axis", Dir)], + "solid.fillet" | "solid.chamfer" => &[("edges", Picks), ("body", Body)], + "solid.hole" => &[("position", Pick), ("direction", Dir), ("body", Body)], + "solid.scale" => &[("origin", Make), ("bodies", Body)], + "solid.offset_face" | "solid.shell" => &[("faces", Picks), ("body", Body)], + "solid.thread" => &[("face", Pick)], + "solid.draft" => &[("faces", Picks), ("neutral", Plane), ("pull", Dir), ("body", Body)], + "solid.press_pull" => &[("face", Pick), ("edges", Picks)], + "solid.replace_face" => &[("faces", Picks), ("target_face", Pick), ("target", Plane), ("body", Body)], + "solid.align" => &[("from", Pick), ("from_face", Pick), ("to", Pick), ("to_face", Pick), ("bodies", Body)], + "solid.mirror" | "solid.split_body" => &[("plane", Plane), ("bodies", Body), ("body", Body)], + "plastic.boss" | "plastic.snap_fit" | "plastic.rest" => { &[("position", Pick), ("direction", Dir), ("hook", Dir), ("along", Dir), ("body", Body)] } - "FusionLipCommand" | "ConvertToSheetMetalCmd" => &[("face", Pick), ("body", Body)], - "FusionSheetMetalFlangeCommand" | "FusionSheetMetalHemFlangeCommand" => &[("edges", Picks), ("body", Body)], - "SheetMetalFoldCmd" => &[("points", Picks), ("fixed", Pick), ("body", Body)], - "SketchCreate" | "sketch.redefine" => &[("plane", Plane)], - "FusionAddEditDecalCommand" => &[("face", Pick)], - "IntersectCmd" => &[("refs", Picks)], - "ProjectToSurface" | "SketchIsoparametricCurve" => &[("face", Pick), ("body", Body)], - "IntersectionCurve" => &[("a", Picks), ("b", Picks)], - "FusionCurvatureCombAnalysisCommand" | "FusionMinimumRadiusAnalysisCommand" | "FusionIsoCurveAnalysisCommand" => { - &[("edges", Picks), ("faces", Picks)] - } - "parts.insert" | "FusionFastenersCommand" => &[("at", Pick), ("point", Make), ("direction", Dir)], - "AppearanceCommand" => &[("faces", Picks), ("bodies", Body), ("body", Body)], - "FusionMoveCommand" => &[("bodies", Body), ("translate", Dir), ("axis", Dir), ("origin", Make)], - "FusionCombineCommand" => &[("target", Body), ("tools", Body)], - "SoftDeleteCommand" | "StockModelCommand" | "PhysicalMaterialCommand" => &[("bodies", Body)], + "plastic.lip" | "sheet.convert" => &[("face", Pick), ("body", Body)], + "sheet.flange" | "sheet.hem" => &[("edges", Picks), ("body", Body)], + "sheet.fold" => &[("points", Picks), ("fixed", Pick), ("body", Body)], + "sketch.create" | "sketch.redefine" => &[("plane", Plane)], + "canvas.decal" => &[("face", Pick)], + "sketch.intersect" => &[("refs", Picks)], + "sketch.project_to_surface" | "sketch.isoparametric_curve" => &[("face", Pick), ("body", Body)], + "sketch.intersection_curve" => &[("a", Picks), ("b", Picks)], + "inspect.curvature_comb" | "inspect.minimum_radius" | "inspect.isocurve" => &[("edges", Picks), ("faces", Picks)], + "parts.insert" | "parts.fastener" => &[("at", Pick), ("point", Make), ("direction", Dir)], + "appearance.assign" => &[("faces", Picks), ("bodies", Body), ("body", Body)], + "solid.move" => &[("bodies", Body), ("translate", Dir), ("axis", Dir), ("origin", Make)], + "solid.combine" => &[("target", Body), ("tools", Body)], + "solid.remove" | "solid.bounding_solid" | "material.assign" => &[("bodies", Body)], _ => &[], } } diff --git a/crates/engine/src/frames_tests.rs b/crates/engine/src/frames_tests.rs index a3b98423..df64a15f 100644 --- a/crates/engine/src/frames_tests.rs +++ b/crates/engine/src/frames_tests.rs @@ -30,22 +30,22 @@ fn sketch_point_world(s: &mut Session, sketch: &str, point: &str) -> Vec3 { } 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 component "Part" with a 30 x 20 x 10 block (sketch "Base" on XY, "Top" on its top face) /// and a construction plane, moved 50 mm along X. fn moved_part() -> Session { let mut s = Session::default(); - run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Part"})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Base"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [30, 20]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 10})); - run(&mut s, "SketchCreate", json!({"plane": {"face": [15, 10, 10]}, "name": "Top"})); - run(&mut s, "CircleCenterRadius", json!({"center": [15, 10], "radius": 3})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "ConstructionPlaneOffsetFromPlaneCommand", json!({"base": "XY", "offset": 25, "name": "Shelf"})); + run(&mut s, "component.create", json!({"name": "Part"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [30, 20]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 10})); + run(&mut s, "sketch.create", json!({"plane": {"face": [15, 10, 10]}, "name": "Top"})); + run(&mut s, "sketch.circle.center", json!({"center": [15, 10], "radius": 3})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "construct.plane.offset", json!({"base": "XY", "offset": 25, "name": "Shelf"})); run(&mut s, "occurrence.move", json!({"occurrence": "Part:1", "translate": [50, 0, 0]})); s } @@ -73,19 +73,19 @@ fn world_picks_map_into_the_component() { let mut s = moved_part(); let v0 = volume(&mut s); // The edge along X at the top front: world (65, 0, 10). - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[65, 0, 10]], "radius": 2})); + run(&mut s, "solid.fillet", json!({"edges": [[65, 0, 10]], "radius": 2})); assert!(volume(&mut s) < v0 - 1.0, "the fillet cut material"); // A sketch on the moved top face sits at world z = 10 over the moved block. - run(&mut s, "SketchCreate", json!({"plane": {"face": [70, 10, 10]}, "name": "Pad"})); - run(&mut s, "CircleCenterRadius", json!({"center": [20, 10], "radius": 2})); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.create", json!({"plane": {"face": [70, 10, 10]}, "name": "Pad"})); + run(&mut s, "sketch.circle.center", json!({"center": [20, 10], "radius": 2})); + run(&mut s, "sketch.finish", json!({})); let pad = sketch_point_world(&mut s, "Pad", "p1"); assert!((pad.z - 10.0).abs() < 1e-6 && pad.x > 50.0, "{pad:?}"); let v1 = volume(&mut s); - let h = run(&mut s, "FusionHoleCommand", json!({"position": [60, 10, 10], "diameter": 4, "depth": 5})); + let h = run(&mut s, "solid.hole", json!({"position": [60, 10, 10], "diameter": 4, "depth": 5})); assert!(volume(&mut s) < v1 - 1.0, "the hole cut material: {h}"); // A box made at world (100, 0, 0) in the active (moved) component shows there. - run(&mut s, "PrimitiveBox", json!({"length": 5, "width": 5, "height": 5, "corner": [100, 0, 0], "operation": "new"})); + run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [100, 0, 0], "operation": "new"})); let last = s.model.state().bodies.last().map(|b| b.name.clone()).unwrap(); let (lo, _) = world_bbox(&s, &last); assert!(near(lo, Vec3::new(100.0, 0.0, 0.0)), "{lo:?}"); @@ -95,13 +95,13 @@ fn world_picks_map_into_the_component() { #[test] fn picks_on_an_instance_map_through_its_placement() { let mut s = Session::default(); - run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Pin"})); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10})); + run(&mut s, "component.create", json!({"name": "Pin"})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10})); run(&mut s, "component.activate", json!({"component": "root"})); run(&mut s, "occurrence.copy", json!({"component": "Pin", "translate": [0, 40, 0]})); let v0 = volume(&mut s); // An edge of the second instance only (world y = 40). - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[5, 40, 10]], "radius": 2})); + run(&mut s, "solid.fillet", json!({"edges": [[5, 40, 10]], "radius": 2})); let v1 = volume(&mut s); assert!(v1 < v0 - 1.0, "{v0} {v1}"); } @@ -111,7 +111,7 @@ fn picks_on_an_instance_map_through_its_placement() { fn moving_bodies_between_components_keeps_their_place() { let mut s = moved_part(); run(&mut s, "component.activate", json!({"component": "root"})); - run(&mut s, "PrimitiveBox", json!({"length": 4, "width": 4, "height": 4, "corner": [0, 40, 0], "body_name": "Loose"})); + run(&mut s, "solid.box", json!({"length": 4, "width": 4, "height": 4, "corner": [0, 40, 0], "body_name": "Loose"})); let before = world_bbox(&s, "Loose"); run(&mut s, "component.move_bodies", json!({"bodies": ["Loose"], "component": "Part"})); let after = world_bbox(&s, "Loose"); @@ -121,7 +121,7 @@ fn moving_bodies_between_components_keeps_their_place() { let moved = world_bbox(&s, "Loose"); assert!(near(moved.0, before.0 + Vec3::new(0.0, 0.0, 10.0)), "{moved:?}"); let v0 = volume(&mut s); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[2, 40, 14]], "radius": 1})); + run(&mut s, "solid.fillet", json!({"edges": [[2, 40, 14]], "radius": 1})); assert!(volume(&mut s) < v0 - 0.1); // Back to the root: still in place. run(&mut s, "component.move_bodies", json!({"bodies": ["Loose"], "component": "root"})); @@ -133,14 +133,14 @@ fn moving_bodies_between_components_keeps_their_place() { #[test] fn sketch_on_a_face_follows_a_moved_body() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 20, "width": 20, "height": 10})); - run(&mut s, "SketchCreate", json!({"plane": {"face": [10, 10, 10]}, "name": "OnTop"})); - run(&mut s, "CircleCenterRadius", json!({"center": [10, 10], "radius": 2})); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 10})); + run(&mut s, "sketch.create", json!({"plane": {"face": [10, 10, 10]}, "name": "OnTop"})); + run(&mut s, "sketch.circle.center", json!({"center": [10, 10], "radius": 2})); + run(&mut s, "sketch.finish", json!({})); // The move goes before the sketch in the timeline: the face the sketch is on moves up. - run(&mut s, "timeline.rollTo", json!({"position": 1})); - run(&mut s, "FusionMoveCommand", json!({"bodies": ["Body1"], "translate": [0, 0, 15]})); - run(&mut s, "timeline.rollTo", json!({})); + run(&mut s, "timeline.roll_to", json!({"position": 1})); + run(&mut s, "solid.move", json!({"bodies": ["Body1"], "translate": [0, 0, 15]})); + run(&mut s, "timeline.roll_to", json!({})); let c = sketch_point_world(&mut s, "OnTop", "p1"); assert!((c.z - 25.0).abs() < 1e-6, "{c:?}"); } diff --git a/crates/engine/src/legacy_ids.rs b/crates/engine/src/legacy_ids.rs new file mode 100644 index 00000000..3351f9b0 --- /dev/null +++ b/crates/engine/src/legacy_ids.rs @@ -0,0 +1,236 @@ +//! Legacy migration: the command ids SolveCraft used before 2026-10-09, when many commands +//! carried the id of the matching command in another CAD program, and the dotted ids that +//! replaced them. This is the one place the old ids live. Old scripts, recipes, control-channel +//! calls, saved shortcuts and toolbar layouts keep working: `find_command` and the preference +//! loaders map an old id through `current_id`. `solvecraft-tools/rename_ids.sh` converts a +//! worktree with this same table. + +/// (old id, current id). +pub const LEGACY_IDS: &[(&str, &str)] = &[ + ("AsBuiltPositionsCmd", "component.revert_position"), + ("FusionCreateComponentsFromBodiesCommand", "component.from_bodies"), + ("FusionCreateNewComponentCommand", "component.create"), + ("SnapshotCmd", "component.capture_position"), + ("FusionShowDesignConfigPanelCmd", "config.table"), + ("FusionStartDesignConfigModeCmd", "config.configure"), + ("FusionComputeAllCommand", "timeline.compute_all"), + ("FusionDeleteCommand", "timeline.delete"), + ("FusionRenameTimelineEntryCommand", "timeline.rename"), + ("RedoCommand", "edit.redo"), + ("UndoCommand", "edit.undo"), + ("timeline.rollTo", "timeline.roll_to"), + ("AlignCmd", "solid.align"), + ("EmbossCmd", "solid.emboss"), + ("FusionReplaceFaceCommand", "solid.replace_face"), + ("FusionRibCommand", "solid.rib"), + ("FusionWebCommand", "solid.web"), + ("PrimitiveCoil", "solid.coil"), + ("SoftDeleteCommand", "solid.remove"), + ("ConstructionPlaneAtAngleCommand", "construct.plane.at_angle"), + ("ConstructionPlaneOffsetFromPlaneCommand", "construct.plane.offset"), + ("Extrude", "solid.extrude"), + ("FusionChamferCommand", "solid.chamfer"), + ("FusionCombineCommand", "solid.combine"), + ("FusionDraftCommand", "solid.draft"), + ("FusionFilletEdgesCommand", "solid.fillet"), + ("FusionHoleCommand", "solid.hole"), + ("FusionMoveCommand", "solid.move"), + ("FusionOffsetFacesCommand", "solid.offset_face"), + ("FusionPressPullCommand", "solid.press_pull"), + ("FusionShellBodyCommand", "solid.shell"), + ("FusionSplitBodyCommand", "solid.split_body"), + ("FusionThreadCommand", "solid.thread"), + ("MirrorCommand", "solid.mirror"), + ("ModifyScale", "solid.scale"), + ("PatternCircular", "solid.pattern.circular"), + ("PatternOnPath", "solid.pattern.path"), + ("PatternRectangular", "solid.pattern.rectangular"), + ("PrimitiveBox", "solid.box"), + ("PrimitiveCylinder", "solid.cylinder"), + ("PrimitivePipe", "solid.pipe"), + ("PrimitiveSphere", "solid.sphere"), + ("PrimitiveTorus", "solid.torus"), + ("Revolve", "solid.revolve"), + ("SolidLoft", "solid.loft"), + ("StockModelCommand", "solid.bounding_solid"), + ("Sweep", "solid.sweep"), + ("ExportCommand", "file.export"), + ("FusionImportCommandFromToolbar", "file.insert_step"), + ("FusionSaveAsSTLCommand", "file.save_mesh"), + ("NewDocumentCommand", "file.new"), + ("ParaMeshInsertAlignCommand", "file.insert_mesh"), + ("SaveDocumentAsCommand", "file.save_as"), + ("SaveDocumentCommand", "file.save"), + ("AppearanceCommand", "appearance.assign"), + ("MeasureCommand", "inspect.measure"), + ("PhysicalMaterialCommand", "material.assign"), + ("FusionMotionRelationshipCommand", "joint.motion_link"), + ("FusionMoveJointsCommand", "joint.drive"), + ("InterferenceCheckCommand", "inspect.interference"), + ("JointAsBuiltCmd", "joint.as_built"), + ("JointAssembleCmdNew", "joint.create"), + ("JointOrigin", "joint.origin"), + ("RigidGroupCmd", "joint.rigid_group"), + ("ContactSetCmd", "contact.create"), + ("DisableAllContactCmd", "contact.disable_all"), + ("EnableAllContactCmd", "contact.enable_all"), + ("EnableContactSetsCmd", "contact.enable_sets"), + ("FusionMotionStudyCommand", "motion.study"), + ("ChangeParameterCommand", "parameters.change"), + ("FusionFastenersCommand", "parts.fastener"), + ("FusionAssignPlasticRuleCommand", "plastic.assign_rule"), + ("FusionBossCommand", "plastic.boss"), + ("FusionLipCommand", "plastic.lip"), + ("FusionManagePlasticRuleCommand", "plastic.manage_rules"), + ("FusionRestCommand", "plastic.rest"), + ("FusionSnapFitCommand", "plastic.snap_fit"), + ("FusionHalfSectionViewCommand", "inspect.section"), + ("ConvertToSheetMetalCmd", "sheet.convert"), + ("FusionSheetMetalFlangeCommand", "sheet.flange"), + ("FusionSheetMetalFlatPatternCmd", "sheet.flat_pattern"), + ("FusionSheetMetalHemFlangeCommand", "sheet.hem"), + ("FusionSheetMetalRulesCommand", "sheet.manage_rules"), + ("FusionSheetmetalUnfoldCommand", "sheet.unfold"), + ("SheetMetalFoldCmd", "sheet.fold"), + ("FusionAddCanvasCommand", "canvas.insert"), + ("FusionAddEditDecalCommand", "canvas.decal"), + ("ConstraintSmooth", "sketch.constraint.curvature"), + ("SketchAutoConstrainAndFinish", "sketch.finish_auto_constrain"), + ("SketchAutoConstraintAndDimCmd", "sketch.auto_constrain"), + ("SketchAutoConstraintAndDimFromDatumCmd", "sketch.auto_constrain_from_datum"), + ("SketchConstrainer", "sketch.constrainer"), + ("SketchPolygonConstraintCmd", "sketch.constraint.polygon"), + ("ArcTangent", "sketch.arc.tangent"), + ("BlendG1CurveSketchCmd", "sketch.blend_curve"), + ("CircleElipse", "sketch.ellipse"), + ("CircleTanTanRadius", "sketch.circle.two_tangent"), + ("CircleThreeTangent", "sketch.circle.three_tangent"), + ("ConicCurveCmd", "sketch.conic"), + ("DrawCVMSpline3D", "sketch.spline.control_point"), + ("DrawCVMSpline5D", "sketch.spline.control_point_5"), + ("DrawSpline", "sketch.spline.fit_point"), + ("MTextCmd", "sketch.text"), + ("ShapeArcSlotCenterTwoPoint", "sketch.slot.arc_center"), + ("ShapeArcSlotThreePoint", "sketch.slot.arc_three_point"), + ("ShapeSlotCenterPoint", "sketch.slot.center_point"), + ("SketchMidpointLine", "sketch.line.midpoint"), + ("ImportDxfFileCommand", "sketch.insert_dxf"), + ("SketchImportSVG", "sketch.insert_svg"), + ("FusionCenterOfMassCommand", "inspect.center_of_mass"), + ("FusionCurvatureCombAnalysisCommand", "inspect.curvature_comb"), + ("FusionIsoCurveAnalysisCommand", "inspect.isocurve"), + ("FusionMinimumRadiusAnalysisCommand", "inspect.minimum_radius"), + ("BreakSketchCmd", "sketch.break"), + ("ChamferSketchDistanceAngle", "sketch.chamfer.distance_angle"), + ("ChamferSketchDistanceDistance", "sketch.chamfer.two_distance"), + ("ChamferSketchEqualDistance", "sketch.chamfer.equal_distance"), + ("CircularSketchPatternCommand", "sketch.pattern.circular"), + ("ExtendSketchCmd", "sketch.extend"), + ("FilletSketchCmd", "sketch.fillet"), + ("MirrorSketchCommand", "sketch.mirror"), + ("Offset", "sketch.offset"), + ("RectangularSketchPatternCommand", "sketch.pattern.rectangular"), + ("SketchScaleCmd", "sketch.scale"), + ("TrimSketchCmd", "sketch.trim"), + ("FitCurvesToSectionCommand", "sketch.fit_curves_to_section"), + ("Include3DGeometry", "sketch.include_3d"), + ("IntersectCmd", "sketch.intersect"), + ("IntersectionCurve", "sketch.intersection_curve"), + ("ProjectNewCmd", "sketch.project"), + ("ProjectToSurface", "sketch.project_to_surface"), + ("SketchIsoparametricCurve", "sketch.isoparametric_curve"), + ("SpunProfileCmd", "sketch.spun_profile"), + ("ArcCenterTwoPoint", "sketch.arc.center_point"), + ("ArcThreePoint", "sketch.arc.three_point"), + ("CircleCenterRadius", "sketch.circle.center"), + ("CircleThreePoint", "sketch.circle.three_point"), + ("CircleTwoPoint", "sketch.circle.two_point"), + ("ConstraintCoincident", "sketch.constraint.coincident"), + ("ConstraintCollinear", "sketch.constraint.collinear"), + ("ConstraintConcentric", "sketch.constraint.concentric"), + ("ConstraintEqual", "sketch.constraint.equal"), + ("ConstraintFix", "sketch.constraint.fix"), + ("ConstraintHorizontalVertical", "sketch.constraint.horizontal_vertical"), + ("ConstraintMidPoint", "sketch.constraint.midpoint"), + ("ConstraintParallel", "sketch.constraint.parallel"), + ("ConstraintPerpendicular", "sketch.constraint.perpendicular"), + ("ConstraintSymmetry", "sketch.constraint.symmetry"), + ("ConstraintTangent", "sketch.constraint.tangent"), + ("DrawPoint", "sketch.point"), + ("DrawPolyline", "sketch.line"), + ("ShapePolygonCircumscribed", "sketch.polygon.circumscribed"), + ("ShapePolygonEdge", "sketch.polygon.edge"), + ("ShapePolygonInscribed", "sketch.polygon.inscribed"), + ("ShapeRectangleCenter", "sketch.rectangle.center"), + ("ShapeRectangleThreePoint", "sketch.rectangle.three_point"), + ("ShapeRectangleTwoPoint", "sketch.rectangle.two_point"), + ("ShapeSlotCenterToCenter", "sketch.slot.center_to_center"), + ("ShapeSlotOverall", "sketch.slot.overall"), + ("SketchActivate", "sketch.edit"), + ("SketchCreate", "sketch.create"), + ("SketchDimension", "sketch.dimension"), + ("SketchStop", "sketch.finish"), + ("FusionAccessibilityAnalysisCommand", "inspect.accessibility"), + ("FusionCurvatureMapAnalysisCommand", "inspect.curvature_map"), + ("FusionDraftAnalysisCommand", "inspect.draft"), + ("FusionEnvironmentMapAnalysisCommand", "inspect.environment_map"), + ("FusionZebraAnalysisCommand", "inspect.zebra"), + ("FusionExtendCommand", "surface.extend"), + ("FusionSurfacePatchCommand", "surface.patch"), + ("FusionSurfaceStitchCommand", "surface.stitch"), + ("FusionSurfaceThickenCommand", "surface.thicken"), + ("FusionSurfaceTrimCommand", "surface.trim"), + ("FusionSplitFaceCommand", "solid.split_face"), + ("ConstructionAxisThroughCylinderCommand", "construct.axis.through_cylinder"), + ("ConstructionAxisNormalToFaceCommand", "construct.axis.normal_to_face"), + ("ConstructionAxisFromTwoPlanesCommand", "construct.axis.two_planes"), + ("ConstructionAxisThroughTwoPointsCommand", "construct.axis.two_points"), + ("ConstructionAxisThroughEdgeCommand", "construct.axis.edge"), + ("ConstructionPointFromPointCommand", "construct.point.vertex"), + ("ConstructionPointThroughTwoEdgesCommand", "construct.point.two_edges"), + ("ConstructionPointFromThreePlanesCommand", "construct.point.three_planes"), + ("ConstructionPointFromCircleOrSphereCommand", "construct.point.center"), + ("ConstructionPointAtEdgeAndPlaneCommand", "construct.point.edge_plane"), + ("ConstructionPointAlongPathCommand", "construct.point.along_path"), + ("ConstructionMidPlaneCommand", "construct.plane.midplane"), + ("ConstructionPlaneFromTwoEdgesCommand", "construct.plane.two_edges"), + ("ConstructionPlaneFromThreePointsCommand", "construct.plane.three_points"), + ("ConstructionTangentPlaneCommand", "construct.plane.tangent"), + ("ConstructionPlaneAlongPathCommand", "construct.plane.along_path"), +]; + +/// The current id for a command id that may be a legacy one (unchanged otherwise). +pub fn current_id(id: &str) -> &str { + LEGACY_IDS.iter().find(|(old, _)| old.eq_ignore_ascii_case(id)).map(|(_, new)| *new).unwrap_or(id) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_legacy_id_names_a_command() { + let unknown: Vec<&str> = + LEGACY_IDS.iter().filter(|(_, new)| crate::find_command(new).is_none_or(|c| c.id != *new)).map(|(_, new)| *new).collect(); + // The construction set and Split Face are on a branch not yet merged when the table was made. + let pending = ["solid.split_face"]; + let unknown: Vec<&str> = unknown + .into_iter() + .filter(|n| { + !pending.contains(n) + && !n.starts_with("construct.axis") + && !n.starts_with("construct.point") + && ![ + "construct.plane.midplane", + "construct.plane.two_edges", + "construct.plane.three_points", + "construct.plane.tangent", + "construct.plane.along_path", + ] + .contains(n) + }) + .collect(); + assert!(unknown.is_empty(), "{unknown:?}"); + assert_eq!(crate::find_command("SketchCreate").map(|c| c.id), Some("sketch.create")); + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 9b7bfc66..1e122b57 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1,8 +1,7 @@ //! The SolveCraft engine. //! -//! Every user-visible action is a command with a stable id. Ids are Fusion's command ids where -//! Fusion has the command (`Extrude`, `SketchCreate`, `FusionFilletEdgesCommand`…) and dotted -//! lower-case ids otherwise (`timeline.rollback`, `document.inspect`). Commands take JSON +//! Every user-visible action is a command with a stable, dotted, lower-case id +//! (`solid.extrude`, `sketch.create`, `solid.fillet`, `timeline.rollback`). Commands take JSON //! parameters and never open dialogs; the UI, the command palette, scripts, the CLI and the //! control channel all run the same commands through [`Session::execute`]. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -10,6 +9,7 @@ pub mod cmd; pub mod frames; +pub mod legacy_ids; pub mod params; pub mod recovery; pub mod sample; @@ -390,7 +390,7 @@ impl Session { Ok(self.model.state()) } - /// Measure one or two selections (see `MeasureCommand` with `items`); errors come back as + /// Measure one or two selections (see `inspect.measure` with `items`); errors come back as /// `{"error": …}`. pub fn measure_items(&self, items: &[Sel]) -> Value { let v = serde_json::to_value(items).unwrap_or(Value::Null); diff --git a/crates/engine/src/recovery.rs b/crates/engine/src/recovery.rs index b4495cd1..2157d528 100644 --- a/crates/engine/src/recovery.rs +++ b/crates/engine/src/recovery.rs @@ -207,7 +207,7 @@ mod tests { let mut s = Session::default(); let mut a = Autosaver::new(&d, Duration::from_secs(3600)).unwrap(); assert!(!a.save(&s).unwrap(), "nothing unsaved"); - s.execute("PrimitiveBox", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); + s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); assert!(!a.tick(&s, false).unwrap(), "not due yet"); assert!(a.tick(&s, true).unwrap(), "after a big operation"); assert!(!a.save(&s).unwrap(), "unchanged since the last autosave"); @@ -230,7 +230,7 @@ mod tests { let d = dir("clean"); let mut s = Session::default(); let mut a = Autosaver::new(&d, Duration::ZERO).unwrap(); - s.execute("PrimitiveBox", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); + s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); assert!(a.tick(&s, false).unwrap()); s.mark_saved(); a.tick(&s, false).unwrap(); @@ -239,7 +239,7 @@ mod tests { assert_eq!(fs::read_dir(&d).unwrap().count(), 0); // Closing with unsaved changes keeps them for the next launch. let b = Autosaver::new(&d, Duration::ZERO).unwrap(); - s.execute("PrimitiveBox", &json!({"length": 5, "width": 5, "height": 5})).unwrap(); + s.execute("solid.box", &json!({"length": 5, "width": 5, "height": 5})).unwrap(); b.close(&s); assert_eq!(orphans(&d).len(), 1); assert!(load(&d, "../etc").is_err()); diff --git a/crates/engine/src/sample.rs b/crates/engine/src/sample.rs index f3e47cac..caa4f621 100644 --- a/crates/engine/src/sample.rs +++ b/crates/engine/src/sample.rs @@ -5,30 +5,30 @@ use serde_json::{Value, json}; pub fn script() -> Value { json!({"commands": [ - {"command": "ChangeParameterCommand", "params": {"name": "width", "expression": "80 mm", "comment": "plate width"}}, - {"command": "ChangeParameterCommand", "params": {"name": "depth", "expression": "50 mm"}}, - {"command": "ChangeParameterCommand", "params": {"name": "thickness", "expression": "8 mm"}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Base"}}, - {"command": "ShapeRectangleTwoPoint", "params": {"p0": [0, 0], "p1": [80, 50]}}, - {"command": "ConstraintCoincident", "params": {"a": "p1", "b": "origin"}}, - {"command": "SketchDimension", "params": {"entities": ["l1"], "value": "width"}}, - {"command": "SketchDimension", "params": {"entities": ["l2"], "value": "depth"}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": "thickness", "body_name": "Plate"}}, - {"command": "FusionFilletEdgesCommand", "params": {"edges": [[0, 0, 4], [80, 0, 4], [80, 50, 4], [0, 50, 4]], "radius": "6 mm"}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Holes"}}, - {"command": "CircleCenterRadius", "params": {"center": [12, 25], "diameter": 8}}, - {"command": "CircleCenterRadius", "params": {"center": [68, 25], "diameter": 8}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": "thickness", "operation": "cut", "name": "Holes"}}, - {"command": "SketchCreate", "params": {"plane": {"face": [40, 25, 8]}, "name": "Boss"}}, - {"command": "CircleCenterRadius", "params": {"center": [40, 25], "diameter": 24}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": 14, "operation": "join", "name": "Boss"}}, - {"command": "SketchCreate", "params": {"plane": {"face": [40, 37, 22]}, "name": "Bore"}}, - {"command": "CircleCenterRadius", "params": {"center": [40, 25], "diameter": 12}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": 22, "direction": "negative", "operation": "cut", "name": "Bore"}}, + {"command": "parameters.change", "params": {"name": "width", "expression": "80 mm", "comment": "plate width"}}, + {"command": "parameters.change", "params": {"name": "depth", "expression": "50 mm"}}, + {"command": "parameters.change", "params": {"name": "thickness", "expression": "8 mm"}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Base"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [80, 50]}}, + {"command": "sketch.constraint.coincident", "params": {"a": "p1", "b": "origin"}}, + {"command": "sketch.dimension", "params": {"entities": ["l1"], "value": "width"}}, + {"command": "sketch.dimension", "params": {"entities": ["l2"], "value": "depth"}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": "thickness", "body_name": "Plate"}}, + {"command": "solid.fillet", "params": {"edges": [[0, 0, 4], [80, 0, 4], [80, 50, 4], [0, 50, 4]], "radius": "6 mm"}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Holes"}}, + {"command": "sketch.circle.center", "params": {"center": [12, 25], "diameter": 8}}, + {"command": "sketch.circle.center", "params": {"center": [68, 25], "diameter": 8}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": "thickness", "operation": "cut", "name": "Holes"}}, + {"command": "sketch.create", "params": {"plane": {"face": [40, 25, 8]}, "name": "Boss"}}, + {"command": "sketch.circle.center", "params": {"center": [40, 25], "diameter": 24}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 14, "operation": "join", "name": "Boss"}}, + {"command": "sketch.create", "params": {"plane": {"face": [40, 37, 22]}, "name": "Bore"}}, + {"command": "sketch.circle.center", "params": {"center": [40, 25], "diameter": 12}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 22, "direction": "negative", "operation": "cut", "name": "Bore"}}, ]}) } @@ -53,14 +53,14 @@ pub fn samples() -> Vec { fn bracket() -> Value { json!({"commands": [ - {"command": "FusionSheetMetalRulesCommand", "params": {"name": "Steel 2mm", "thickness": "2 mm", "bend_radius": "Thickness", "active": true}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Base"}}, - {"command": "ShapeRectangleTwoPoint", "params": {"p0": [0, 0], "p1": [120, 60]}}, - {"command": "SketchStop", "params": {}}, - {"command": "FusionSheetMetalFlangeCommand", "params": {"sketch": "Base"}}, - {"command": "FusionSheetMetalFlangeCommand", "params": {"edges": [[60, 0, 2], [60, 60, 2]], "height": 30}}, - {"command": "FusionSheetMetalFlangeCommand", "params": {"edges": [[120, 30, 2]], "height": 20, "angle": "60 deg"}}, - {"command": "FusionSheetMetalHemFlangeCommand", "params": {"edges": [[0, 30, 2]], "length": 8}}, + {"command": "sheet.manage_rules", "params": {"name": "Steel 2mm", "thickness": "2 mm", "bend_radius": "Thickness", "active": true}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Base"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [120, 60]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "sheet.flange", "params": {"sketch": "Base"}}, + {"command": "sheet.flange", "params": {"edges": [[60, 0, 2], [60, 60, 2]], "height": 30}}, + {"command": "sheet.flange", "params": {"edges": [[120, 30, 2]], "height": 20, "angle": "60 deg"}}, + {"command": "sheet.hem", "params": {"edges": [[0, 30, 2]], "length": 8}}, ]}) } @@ -69,70 +69,70 @@ fn hinge() -> Value { for name in ["Leaf A", "Leaf B"] { cmds.extend([ json!({"command": "component.activate", "params": {"component": "root"}}), - json!({"command": "FusionCreateNewComponentCommand", "params": {"name": name}}), - json!({"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}}), - json!({"command": "PrimitiveCylinder", "params": {"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}}), + json!({"command": "component.create", "params": {"name": name}}), + json!({"command": "solid.box", "params": {"length": 40, "width": 30, "height": 3, "corner": [0, 0, 0]}}), + json!({"command": "solid.cylinder", "params": {"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}}), ]); } cmds.extend([ json!({"command": "component.activate", "params": {"component": "root"}}), json!({"command": "occurrence.ground", "params": {"occurrence": "Leaf A:1", "grounded": true}}), - json!({"command": "JointAssembleCmdNew", "params": {"type": "revolute", "name": "Hinge", + json!({"command": "joint.create", "params": {"type": "revolute", "name": "Hinge", "a": {"occurrence": "Leaf A:1", "circle": [1, 37, 4]}, "b": {"occurrence": "Leaf B:1", "circle": [1, 37, 4]}, "limits": [["0 deg", "180 deg"]]}}), - json!({"command": "FusionMoveJointsCommand", "params": {"joint": "Hinge", "value": 60}}), + json!({"command": "joint.drive", "params": {"joint": "Hinge", "value": 60}}), ]); json!({ "commands": cmds }) } fn enclosure() -> Value { json!({"commands": [ - {"command": "PrimitiveBox", "params": {"length": 80, "width": 50, "height": 30, "body_name": "Enclosure"}}, - {"command": "FusionShellBodyCommand", "params": {"faces": [[40, 25, 30]], "thickness": 2}}, - {"command": "FusionLipCommand", "params": {"face": [1, 25, 30], "width": 1, "height": 2}}, - {"command": "FusionSnapFitCommand", "params": {"position": [40, 44, 2], "hook": [0, -1, 0], "length": 14, "thickness": 1.5, "width": 6, "catch_depth": 1, "catch_length": 2}}, - {"command": "FusionBossCommand", "params": {"position": [10, 10, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5}}, - {"command": "FusionBossCommand", "params": {"position": [70, 40, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5}}, - {"command": "FusionRestCommand", "params": {"position": [40, 25, 2], "width": 16, "length": 24, "height": 3}}, + {"command": "solid.box", "params": {"length": 80, "width": 50, "height": 30, "body_name": "Enclosure"}}, + {"command": "solid.shell", "params": {"faces": [[40, 25, 30]], "thickness": 2}}, + {"command": "plastic.lip", "params": {"face": [1, 25, 30], "width": 1, "height": 2}}, + {"command": "plastic.snap_fit", "params": {"position": [40, 44, 2], "hook": [0, -1, 0], "length": 14, "thickness": 1.5, "width": 6, "catch_depth": 1, "catch_length": 2}}, + {"command": "plastic.boss", "params": {"position": [10, 10, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5}}, + {"command": "plastic.boss", "params": {"position": [70, 40, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5}}, + {"command": "plastic.rest", "params": {"position": [40, 25, 2], "width": 16, "length": 24, "height": 3}}, ]}) } fn flange() -> Value { json!({"commands": [ - {"command": "ChangeParameterCommand", "params": {"name": "bolts", "expression": "6", "unit": "", "comment": "number of bolt holes"}}, - {"command": "ChangeParameterCommand", "params": {"name": "pcd", "expression": "70 mm", "comment": "bolt circle diameter"}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Disc"}}, - {"command": "CircleCenterRadius", "params": {"center": [0, 0], "diameter": 100}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": 10, "body_name": "Flange", "name": "Disc"}}, - {"command": "SketchCreate", "params": {"plane": {"face": [0, 0, 10]}, "name": "Hub"}}, - {"command": "CircleCenterRadius", "params": {"center": [0, 0], "diameter": 44}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": 16, "operation": "join", "name": "Hub"}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Bore"}}, - {"command": "CircleCenterRadius", "params": {"center": [0, 0], "diameter": 24}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": 26, "operation": "cut", "name": "Bore"}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Bolt"}}, - {"command": "CircleCenterRadius", "params": {"center": [35, 0], "diameter": 9}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": 10, "operation": "cut", "name": "BoltHole"}}, - {"command": "PatternCircular", "params": {"features": ["BoltHole"], "axis": "Z", "count": "bolts"}}, + {"command": "parameters.change", "params": {"name": "bolts", "expression": "6", "unit": "", "comment": "number of bolt holes"}}, + {"command": "parameters.change", "params": {"name": "pcd", "expression": "70 mm", "comment": "bolt circle diameter"}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Disc"}}, + {"command": "sketch.circle.center", "params": {"center": [0, 0], "diameter": 100}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 10, "body_name": "Flange", "name": "Disc"}}, + {"command": "sketch.create", "params": {"plane": {"face": [0, 0, 10]}, "name": "Hub"}}, + {"command": "sketch.circle.center", "params": {"center": [0, 0], "diameter": 44}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 16, "operation": "join", "name": "Hub"}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Bore"}}, + {"command": "sketch.circle.center", "params": {"center": [0, 0], "diameter": 24}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 26, "operation": "cut", "name": "Bore"}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Bolt"}}, + {"command": "sketch.circle.center", "params": {"center": [35, 0], "diameter": 9}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 10, "operation": "cut", "name": "BoltHole"}}, + {"command": "solid.pattern.circular", "params": {"features": ["BoltHole"], "axis": "Z", "count": "bolts"}}, ]}) } fn vents() -> Value { json!({"commands": [ - {"command": "ChangeParameterCommand", "params": {"name": "cols", "expression": "6", "unit": ""}}, - {"command": "ChangeParameterCommand", "params": {"name": "rows", "expression": "3", "unit": ""}}, - {"command": "ChangeParameterCommand", "params": {"name": "pitch", "expression": "16 mm", "comment": "slot spacing"}}, - {"command": "PrimitiveBox", "params": {"length": 120, "width": 80, "height": 4, "body_name": "Plate"}}, - {"command": "FusionFilletEdgesCommand", "params": {"edges": [[0, 0, 2], [120, 0, 2], [120, 80, 2], [0, 80, 2]], "radius": "8 mm"}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Slot"}}, - {"command": "ShapeRectangleTwoPoint", "params": {"p0": [18, 12], "p1": [24, 26]}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": 4, "operation": "cut", "name": "Vent"}}, - {"command": "PatternRectangular", "params": {"features": ["Vent"], "dir1": [1, 0, 0], "count1": "cols", "spacing1": "pitch", + {"command": "parameters.change", "params": {"name": "cols", "expression": "6", "unit": ""}}, + {"command": "parameters.change", "params": {"name": "rows", "expression": "3", "unit": ""}}, + {"command": "parameters.change", "params": {"name": "pitch", "expression": "16 mm", "comment": "slot spacing"}}, + {"command": "solid.box", "params": {"length": 120, "width": 80, "height": 4, "body_name": "Plate"}}, + {"command": "solid.fillet", "params": {"edges": [[0, 0, 2], [120, 0, 2], [120, 80, 2], [0, 80, 2]], "radius": "8 mm"}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Slot"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [18, 12], "p1": [24, 26]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 4, "operation": "cut", "name": "Vent"}}, + {"command": "solid.pattern.rectangular", "params": {"features": ["Vent"], "dir1": [1, 0, 0], "count1": "cols", "spacing1": "pitch", "dir2": [0, 1, 0], "count2": "rows", "spacing2": "22 mm"}}, ]}) } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 723e513a..3d3ed5d1 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -12,7 +12,7 @@ fn run(s: &mut Session, id: &str, p: Value) -> Value { } fn volume(s: &mut Session) -> f64 { - let m = run(s, "MeasureCommand", json!({})); + let m = run(s, "inspect.measure", json!({})); m["total"]["volume_mm3"].as_f64().unwrap() } @@ -26,59 +26,59 @@ mod component_frames; #[test] fn box_fillet_cut_part() { let mut s = Session::default(); - run(&mut s, "ChangeParameterCommand", json!({"name": "width", "expression": "40 mm"})); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [38, 29]})); + run(&mut s, "parameters.change", json!({"name": "width", "expression": "40 mm"})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [38, 29]})); let l: Vec = r["curves"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect(); let corner = s.doc.sketch(s.active_sketch.unwrap()).unwrap().curves[0].clone(); let start = match corner.kind { solvecraft_sketch::CurveKind::Line { a, .. } => s.doc.sketch(s.active_sketch.unwrap()).unwrap().points[a].id.clone(), _ => panic!(), }; - run(&mut s, "ConstraintCoincident", json!({"a": start, "b": "origin"})); - run(&mut s, "SketchDimension", json!({"entities": [l[0]], "value": "width"})); - run(&mut s, "SketchDimension", json!({"entities": [l[1]], "value": 30})); + run(&mut s, "sketch.constraint.coincident", json!({"a": start, "b": "origin"})); + run(&mut s, "sketch.dimension", json!({"entities": [l[0]], "value": "width"})); + run(&mut s, "sketch.dimension", json!({"entities": [l[1]], "value": 30})); let si = run(&mut s, "sketch.inspect", json!({})); assert_eq!(si["dof"], 0, "{si}"); assert_eq!(si["profiles"].as_array().unwrap().len(), 1); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 20})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 20})); assert!(rel(volume(&mut s), 24000.0) < 1e-9); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[0, 0, 10]], "radius": 3})); + run(&mut s, "solid.fillet", json!({"edges": [[0, 0, 10]], "radius": 3})); let filleted = 24000.0 - (9.0 - PI * 9.0 / 4.0) * 20.0; assert!(rel(volume(&mut s), filleted) < 2e-4); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "CircleCenterRadius", json!({"center": [20, 15], "radius": 5})); - run(&mut s, "Extrude", json!({"distance": 20, "operation": "cut"})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.circle.center", json!({"center": [20, 15], "radius": 5})); + run(&mut s, "solid.extrude", json!({"distance": 20, "operation": "cut"})); let expect = filleted - PI * 25.0 * 20.0; let v = volume(&mut s); assert!(rel(v, expect) < 5e-4, "{v} vs {expect}"); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert_eq!(m["bodies"][0]["faces"], 8, "{m}"); // Parameter edit re-evaluates the timeline from the sketch. - let r = run(&mut s, "ChangeParameterCommand", json!({"name": "width", "expression": "50"})); + let r = run(&mut s, "parameters.change", json!({"name": "width", "expression": "50"})); assert_eq!(r["errors"].as_array().unwrap().len(), 0, "{r}"); let expect2 = 30000.0 - (9.0 - PI * 9.0 / 4.0) * 20.0 - PI * 25.0 * 20.0; assert!(rel(volume(&mut s), expect2) < 5e-4); // Undo the parameter change, redo it. - run(&mut s, "UndoCommand", json!({})); + run(&mut s, "edit.undo", json!({})); assert!(rel(volume(&mut s), expect) < 5e-4); - run(&mut s, "RedoCommand", json!({})); + run(&mut s, "edit.redo", json!({})); assert!(rel(volume(&mut s), expect2) < 5e-4); // Export STL and STEP. let dir = std::env::temp_dir().join(format!("solvecraft-test-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let stl = dir.join("part.stl"); - run(&mut s, "ExportCommand", json!({"path": stl.to_string_lossy()})); + run(&mut s, "file.export", json!({"path": stl.to_string_lossy()})); assert!(std::fs::metadata(&stl).unwrap().len() > 1000); let step = dir.join("part.step"); - run(&mut s, "ExportCommand", json!({"path": step.to_string_lossy()})); + run(&mut s, "file.export", json!({"path": step.to_string_lossy()})); assert!(std::fs::read_to_string(&step).unwrap().contains("ISO-10303-21")); let design = dir.join("part.solvecraft"); - run(&mut s, "SaveDocumentAsCommand", json!({"path": design.to_string_lossy()})); + run(&mut s, "file.save_as", json!({"path": design.to_string_lossy()})); let mut s2 = Session::default(); run(&mut s2, "doc.open", json!({"path": design.to_string_lossy()})); assert!(rel(volume(&mut s2), expect2) < 5e-4); @@ -88,61 +88,61 @@ fn box_fillet_cut_part() { #[test] fn revolve_and_primitives() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XZ"})); - run(&mut s, "DrawPolyline", json!({"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true})); - run(&mut s, "Revolve", json!({"axis": "y"})); + run(&mut s, "sketch.create", json!({"plane": "XZ"})); + run(&mut s, "sketch.line", json!({"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true})); + run(&mut s, "solid.revolve", json!({"axis": "y"})); assert!(rel(volume(&mut s), PI * 125.0 * 5.0) < 5e-4); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [30, 0, 0]})); - run(&mut s, "PrimitiveCylinder", json!({"diameter": 4, "height": 20, "base": [35, 5, -5]})); - run(&mut s, "FusionCombineCommand", json!({"target": "Body2", "tools": ["Body3"], "operation": "cut"})); - let m = run(&mut s, "MeasureCommand", json!({})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [30, 0, 0]})); + run(&mut s, "solid.cylinder", json!({"diameter": 4, "height": 20, "base": [35, 5, -5]})); + run(&mut s, "solid.combine", json!({"target": "Body2", "tools": ["Body3"], "operation": "cut"})); + let m = run(&mut s, "inspect.measure", json!({})); assert_eq!(m["body_count"], 2, "{m}"); assert!(rel(m["bodies"][1]["volume_mm3"].as_f64().unwrap(), 1000.0 - PI * 4.0 * 10.0) < 5e-4); - run(&mut s, "FusionChamferCommand", json!({"edges": [[30, 5, 10]], "distance": 1})); + run(&mut s, "solid.chamfer", json!({"edges": [[30, 5, 10]], "distance": 1})); run(&mut s, "timeline.rollback", json!({"position": 3})); - assert_eq!(run(&mut s, "MeasureCommand", json!({}))["body_count"], 2); + assert_eq!(run(&mut s, "inspect.measure", json!({}))["body_count"], 2); run(&mut s, "timeline.rollback", json!({})); run(&mut s, "timeline.edit", json!({"feature": "Revolve1", "set": {"angle": "180 deg"}})); - let m = run(&mut s, "MeasureCommand", json!({"bodies": ["Body1"]})); + let m = run(&mut s, "inspect.measure", json!({"bodies": ["Body1"]})); assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap(), PI * 125.0 * 5.0 / 2.0) < 5e-4); } #[test] fn sketch_tools_and_constraints() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "YZ"})); - run(&mut s, "ShapeRectangleCenter", json!({"center": [0, 0], "corner": [10, 5]})); - run(&mut s, "ShapeRectangleThreePoint", json!({"p0": [20, 0], "p1": [30, 5], "p2": [28, 10]})); - run(&mut s, "CircleTwoPoint", json!({"p0": [40, 0], "p1": [44, 0]})); - run(&mut s, "CircleThreePoint", json!({"p0": [50, 0], "p1": [52, 2], "p2": [54, 0]})); - run(&mut s, "ArcThreePoint", json!({"start": [60, 0], "end": [64, 0], "through": [62, 2]})); - run(&mut s, "ArcCenterTwoPoint", json!({"center": [70, 0], "start": [72, 0], "end": [70, 2]})); - run(&mut s, "ShapePolygonInscribed", json!({"center": [80, 0], "radius": 3, "sides": 6})); - run(&mut s, "ShapePolygonCircumscribed", json!({"center": [90, 0], "radius": 3, "sides": 5})); - run(&mut s, "ShapePolygonEdge", json!({"p0": [100, 0], "p1": [103, 0], "sides": 4})); - run(&mut s, "ShapeSlotCenterToCenter", json!({"p0": [110, 0], "p1": [120, 0], "width": 4})); - run(&mut s, "ShapeSlotOverall", json!({"p0": [130, 0], "p1": [140, 0], "width": 4})); - run(&mut s, "DrawPoint", json!({"point": [150, 0]})); - let r = run(&mut s, "DrawPolyline", json!({"points": [[0, 20], [10, 21], [20, 30]], "ids": ["m1", "m2"]})); + run(&mut s, "sketch.create", json!({"plane": "YZ"})); + run(&mut s, "sketch.rectangle.center", json!({"center": [0, 0], "corner": [10, 5]})); + run(&mut s, "sketch.rectangle.three_point", json!({"p0": [20, 0], "p1": [30, 5], "p2": [28, 10]})); + run(&mut s, "sketch.circle.two_point", json!({"p0": [40, 0], "p1": [44, 0]})); + run(&mut s, "sketch.circle.three_point", json!({"p0": [50, 0], "p1": [52, 2], "p2": [54, 0]})); + run(&mut s, "sketch.arc.three_point", json!({"start": [60, 0], "end": [64, 0], "through": [62, 2]})); + run(&mut s, "sketch.arc.center_point", json!({"center": [70, 0], "start": [72, 0], "end": [70, 2]})); + run(&mut s, "sketch.polygon.inscribed", json!({"center": [80, 0], "radius": 3, "sides": 6})); + run(&mut s, "sketch.polygon.circumscribed", json!({"center": [90, 0], "radius": 3, "sides": 5})); + run(&mut s, "sketch.polygon.edge", json!({"p0": [100, 0], "p1": [103, 0], "sides": 4})); + run(&mut s, "sketch.slot.center_to_center", json!({"p0": [110, 0], "p1": [120, 0], "width": 4})); + run(&mut s, "sketch.slot.overall", json!({"p0": [130, 0], "p1": [140, 0], "width": 4})); + run(&mut s, "sketch.point", json!({"point": [150, 0]})); + let r = run(&mut s, "sketch.line", json!({"points": [[0, 20], [10, 21], [20, 30]], "ids": ["m1", "m2"]})); assert_eq!(r["curves"], json!(["m1", "m2"])); - run(&mut s, "ConstraintHorizontalVertical", json!({"line": "m1"})); - run(&mut s, "ConstraintPerpendicular", json!({"a": "m1", "b": "m2"})); - let _ = s.execute("SketchDimension", &json!({"entities": ["m1", "m2"], "type": "angle", "value": "90 deg"})); + run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "m1"})); + run(&mut s, "sketch.constraint.perpendicular", json!({"a": "m1", "b": "m2"})); + let _ = s.execute("sketch.dimension", &json!({"entities": ["m1", "m2"], "type": "angle", "value": "90 deg"})); let si = run(&mut s, "sketch.inspect", json!({})); assert_eq!(si["status"], "solved", "{}", si["failing"]); // Over-constraining is rejected and leaves the sketch unchanged. let before = s.doc.clone(); - assert!(s.execute("ConstraintParallel", &json!({"a": "m1", "b": "m2"})).is_err()); + assert!(s.execute("sketch.constraint.parallel", &json!({"a": "m1", "b": "m2"})).is_err()); assert_eq!(*s.doc, *before); // Profiles: rectangles, circles, closed arcs/polygons/slots. let n = si["profiles"].as_array().unwrap().len(); assert!(n >= 8, "{n} profiles"); run(&mut s, "sketch.construction", json!({"curves": ["m1"]})); run(&mut s, "sketch.delete", json!({"entities": ["m2"]})); - run(&mut s, "ConstraintFix", json!({"entity": "m1"})); + run(&mut s, "sketch.constraint.fix", json!({"entity": "m1"})); run(&mut s, "sketch.move_point", json!({"point": "m1.end", "to": [15, 20]})); - run(&mut s, "SketchStop", json!({})); - assert!(s.execute("DrawPolyline", &json!({"points": [[0, 0], [1, 1]]})).is_err(), "needs an active sketch"); + run(&mut s, "sketch.finish", json!({})); + assert!(s.execute("sketch.line", &json!({"points": [[0, 0], [1, 1]]})).is_err(), "needs an active sketch"); } /// The recipe format of the Fusion oracle (01-box) drives the same commands. @@ -150,13 +150,13 @@ fn sketch_tools_and_constraints() { fn script_runs_commands() { let mut s = Session::default(); let script = json!({"commands": [ - {"command": "SketchCreate", "params": {"plane": "XY"}}, - {"command": "DrawPolyline", "params": {"points": [[0, 0], [40, 0], [40, 30], [0, 30]], "closed": true, "ids": ["l1", "l2", "l3", "l4"]}}, - {"command": "Extrude", "params": {"distance": 20, "profiles": [{"loops": [{"outer": true, "curves": ["l1", "l2", "l3", "l4"]}]}], "body_name": "Box"}}, + {"command": "sketch.create", "params": {"plane": "XY"}}, + {"command": "sketch.line", "params": {"points": [[0, 0], [40, 0], [40, 30], [0, 30]], "closed": true, "ids": ["l1", "l2", "l3", "l4"]}}, + {"command": "solid.extrude", "params": {"distance": 20, "profiles": [{"loops": [{"outer": true, "curves": ["l1", "l2", "l3", "l4"]}]}], "body_name": "Box"}}, ]}); let out = s.run_script(&script).unwrap(); assert_eq!(out.len(), 3); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert_eq!(m["bodies"][0]["name"], "Box"); assert_eq!((m["total"]["faces"].as_u64(), m["total"]["edges"].as_u64(), m["total"]["vertices"].as_u64()), (Some(6), Some(12), Some(8))); assert!(s.run_script(&json!([{"command": "Nope"}])).is_err()); @@ -299,10 +299,7 @@ fn hostile_params_never_panic() { ]; let mut internal = Vec::new(); for spec in command_specs() { - if matches!( - spec.id, - "doc.open" | "SaveDocumentCommand" | "SaveDocumentAsCommand" | "ExportCommand" | "FusionSaveAsSTLCommand" | "sketch.export_dxf" - ) { + if matches!(spec.id, "doc.open" | "file.save" | "file.save_as" | "file.export" | "file.save_mesh" | "sketch.export_dxf") { continue; // file system side effects are covered by their own tests } for v in hostile_values() { @@ -312,9 +309,9 @@ fn hostile_params_never_panic() { } let mut s = Session::default(); // A model with a sketch, a body and an active sketch, so most commands get far. - let _ = s.execute("PrimitiveBox", &json!({"length": 10, "width": 10, "height": 10})); - let _ = s.execute("SketchCreate", &json!({"plane": "XY"})); - let _ = s.execute("ShapeRectangleTwoPoint", &json!({"p0": [0, 0], "p1": [5, 5]})); + let _ = s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})); + let _ = s.execute("sketch.create", &json!({"plane": "XY"})); + let _ = s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [5, 5]})); let p = json!({ k: v.clone() }); if let Err(EngineError::Internal(id, msg)) = s.execute(spec.id, &p) { internal.push(format!("{id} {p}: {msg}")); @@ -339,14 +336,14 @@ fn registry_is_consistent() { assert_eq!(ids.len(), n, "duplicate command ids"); assert!(find_command("extrude").is_some(), "case-insensitive lookup"); let s = Session::default(); - assert!(!find_command("DrawPolyline").unwrap().info(&s).enabled); + assert!(!find_command("sketch.line").unwrap().info(&s).enabled); } #[test] fn sample_design_builds() { let mut s = Session::default(); s.run_script(&crate::sample::script()).unwrap(); - let m = run(&mut s, "MeasureCommand", json!({})); + let m = run(&mut s, "inspect.measure", json!({})); assert_eq!(m["body_count"], 1, "{m}"); let plate = 80.0 * 50.0 * 8.0 - 4.0 * (36.0 - PI * 9.0) * 8.0 - 2.0 * PI * 16.0 * 8.0; let boss = PI * 144.0 * 14.0; @@ -382,9 +379,9 @@ fn sample_edges_chain_and_faces() { #[test] fn timeline_edits_reresolve_references() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20, "name": "Base"})); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[20, 0, 20]], "radius": 3, "name": "Round"})); - run(&mut s, "FusionHoleCommand", json!({"position": [20, 15, 20], "diameter": 6, "name": "Bore"})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "name": "Base"})); + run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 20]], "radius": 3, "name": "Round"})); + run(&mut s, "solid.hole", json!({"position": [20, 15, 20], "diameter": 6, "name": "Bore"})); let fillet = |l: f64| (9.0 - 9.0 * PI / 4.0) * l; let expect = |l: f64, h: f64| l * 30.0 * h - fillet(l) - PI * 9.0 * h; assert!(rel(volume(&mut s), expect(40.0, 20.0)) < 1e-3); @@ -405,22 +402,18 @@ fn timeline_edits_reresolve_references() { assert_eq!(names, ["Base", "Bore", "Round"]); assert!(rel(volume(&mut s), expect(60.0, 25.0)) < 1e-3); // Redefine the fillet in place with a new radius; it keeps its name and position. - run( - &mut s, - "timeline.redefine", - json!({"feature": "Round", "command": "FusionFilletEdgesCommand", "params": {"edges": [[30, 0, 25]], "radius": 4}}), - ); + run(&mut s, "timeline.redefine", json!({"feature": "Round", "command": "solid.fillet", "params": {"edges": [[30, 0, 25]], "radius": 4}})); let f4 = (16.0 - 16.0 * PI / 4.0) * 60.0; assert!(rel(volume(&mut s), 60.0 * 30.0 * 25.0 - f4 - PI * 9.0 * 25.0) < 1e-3); assert_eq!(s.doc.features.iter().map(|f| f.name.as_str()).collect::>(), ["Base", "Bore", "Round"]); // Dependents and roll back. let d = run(&mut s, "timeline.dependents", json!({"feature": "Base"})); assert_eq!(d["would_fail"].as_array().map(|a| a.len()), Some(2), "{d}"); - run(&mut s, "timeline.rollTo", json!({"feature": "Base"})); + run(&mut s, "timeline.roll_to", json!({"feature": "Base"})); assert!(rel(volume(&mut s), 60.0 * 30.0 * 25.0) < 1e-6); - run(&mut s, "timeline.rollTo", json!({})); - run(&mut s, "UndoCommand", json!({})); - run(&mut s, "UndoCommand", json!({})); + run(&mut s, "timeline.roll_to", json!({})); + run(&mut s, "edit.undo", json!({})); + run(&mut s, "edit.undo", json!({})); assert!(rel(volume(&mut s), 60.0 * 30.0 * 25.0 - f4 - PI * 9.0 * 25.0) < 1e-3); } @@ -429,9 +422,9 @@ fn step_box_file(tag: &str) -> std::path::PathBuf { let dir = std::env::temp_dir().join(format!("solvecraft-step-{tag}-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); let p = dir.join("Block.step"); - run(&mut s, "ExportCommand", json!({"path": p.to_string_lossy()})); + run(&mut s, "file.export", json!({"path": p.to_string_lossy()})); p } @@ -447,23 +440,23 @@ fn step_open_then_model_on_it() { assert!(matches!(&s.doc.features[0].kind, doc::FeatureKind::Import { file, .. } if file == "Block.step")); assert!(s.is_dirty() && s.path.is_none() && s.undo.is_empty()); assert!(rel(volume(&mut s), 24000.0) < 1e-6); - let corner = run(&mut s, "MeasureCommand", json!({}))["bodies"][0]["bbox"]["min"].clone(); + let corner = run(&mut s, "inspect.measure", json!({}))["bodies"][0]["bbox"]["min"].clone(); let (x0, y0, z0) = (corner[0].as_f64().unwrap(), corner[1].as_f64().unwrap(), corner[2].as_f64().unwrap()); let at = |x: f64, y: f64, z: f64| json!([x0 + x, y0 + y, z0 + z]); // Fillet a vertical edge, drill a through hole, cut a pocket sketched on the top face. - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [at(0.0, 0.0, 10.0)], "radius": 3})); + run(&mut s, "solid.fillet", json!({"edges": [at(0.0, 0.0, 10.0)], "radius": 3})); let mut want = 24000.0 - (9.0 - PI * 9.0 / 4.0) * 20.0; assert!(rel(volume(&mut s), want) < 2e-4); - run(&mut s, "FusionHoleCommand", json!({"position": at(28.0, 15.0, 20.0), "diameter": 6})); + run(&mut s, "solid.hole", json!({"position": at(28.0, 15.0, 20.0), "diameter": 6})); want -= PI * 9.0 * 20.0; assert!(rel(volume(&mut s), want) < 5e-4, "{} vs {want}", volume(&mut s)); - run(&mut s, "SketchCreate", json!({"plane": {"face": at(10.0, 15.0, 20.0)}})); + run(&mut s, "sketch.create", json!({"plane": {"face": at(10.0, 15.0, 20.0)}})); let c = [x0 + 10.0, y0 + 15.0]; // Sketch coordinates on the face are relative to the model origin projected onto it. - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [c[0] - 4.0, c[1] - 4.0], "p1": [c[0] + 4.0, c[1] + 4.0]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": -5, "operation": "cut"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [c[0] - 4.0, c[1] - 4.0], "p1": [c[0] + 4.0, c[1] + 4.0]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": -5, "operation": "cut"})); want -= 64.0 * 5.0; assert!(rel(volume(&mut s), want) < 5e-4, "{} vs {want}", volume(&mut s)); assert_eq!(s.model.results.iter().filter(|r| r.error.is_some()).count(), 0); @@ -471,7 +464,7 @@ fn step_open_then_model_on_it() { // The design saves and reopens without the STEP file. let before = volume(&mut s); let design = p.with_extension("solvecraft"); - run(&mut s, "SaveDocumentAsCommand", json!({"path": design.to_string_lossy()})); + run(&mut s, "file.save_as", json!({"path": design.to_string_lossy()})); std::fs::remove_file(&p).unwrap(); let mut s2 = Session::default(); run(&mut s2, "doc.open", json!({"path": design.to_string_lossy()})); @@ -486,33 +479,33 @@ fn step_insert_shell_and_errors() { std::fs::copy(&p, &upper).unwrap(); let mut s = Session::default(); run(&mut s, "doc.open", json!({"path": upper.to_string_lossy()})); - let top = run(&mut s, "MeasureCommand", json!({}))["bodies"][0]["bbox"]["max"].clone(); + let top = run(&mut s, "inspect.measure", json!({}))["bodies"][0]["bbox"]["max"].clone(); let (x1, y1, z1) = (top[0].as_f64().unwrap(), top[1].as_f64().unwrap(), top[2].as_f64().unwrap()); - run(&mut s, "FusionShellBodyCommand", json!({"faces": [[x1 - 20.0, y1 - 15.0, z1]], "thickness": 2})); + run(&mut s, "solid.shell", json!({"faces": [[x1 - 20.0, y1 - 15.0, z1]], "thickness": 2})); let walls = 24000.0 - 36.0 * 26.0 * 18.0; assert!(rel(volume(&mut s), walls) < 5e-4, "{}", volume(&mut s)); let mut d = Session::default(); - run(&mut d, "PrimitiveBox", json!({"length": 5, "width": 5, "height": 5, "corner": [100, 0, 0]})); - let r = run(&mut d, "FusionImportCommandFromToolbar", json!({"path": p.to_string_lossy()})); + run(&mut d, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [100, 0, 0]})); + let r = run(&mut d, "file.insert_step", json!({"path": p.to_string_lossy()})); assert_eq!(r["bodies"].as_array().unwrap().len(), 1, "{r}"); - run(&mut d, "FusionImportCommandFromToolbar", json!({"path": p.to_string_lossy()})); + run(&mut d, "file.insert_step", json!({"path": p.to_string_lossy()})); let names: Vec = d.model.state().bodies.iter().map(|b| b.name.clone()).collect(); assert_eq!(names.len(), 3); let mut uniq = names.clone(); uniq.dedup(); assert_eq!(uniq.len(), 3, "{names:?}"); assert!(rel(volume(&mut d), 125.0 + 48000.0) < 1e-6); - run(&mut d, "UndoCommand", json!({})); + run(&mut d, "edit.undo", json!({})); assert_eq!(d.model.state().bodies.len(), 2); let junk = p.with_file_name("junk.step"); std::fs::write(&junk, "ISO-10303-21;\nDATA;\n#1=CARTESIAN_POINT('',(0.,0.,0.));\nENDSEC;\nEND-ISO-10303-21;\n").unwrap(); let before = d.doc.clone(); - assert!(d.execute("FusionImportCommandFromToolbar", &json!({"path": junk.to_string_lossy()})).is_err()); + assert!(d.execute("file.insert_step", &json!({"path": junk.to_string_lossy()})).is_err()); assert!(d.execute("doc.open", &json!({"path": junk.to_string_lossy()})).is_err()); - assert!(d.execute("FusionImportCommandFromToolbar", &json!({"path": "/nonexistent/a.step"})).is_err()); - assert!(d.execute("FusionImportCommandFromToolbar", &json!({"path": p.with_extension("stl").to_string_lossy()})).is_err()); + assert!(d.execute("file.insert_step", &json!({"path": "/nonexistent/a.step"})).is_err()); + assert!(d.execute("file.insert_step", &json!({"path": p.with_extension("stl").to_string_lossy()})).is_err()); assert_eq!(*d.doc, *before); } @@ -524,25 +517,25 @@ fn body_volume(st: &solvecraft_doc::ModelState) -> f64 { #[test] fn preview_has_no_side_effects() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); + run(&mut s, "sketch.finish", json!({})); let doc = s.doc.clone(); let (undo, rev) = (s.undo.len(), s.revision); - let p = s.preview(&[("Extrude".into(), json!({"distance": 15}))]).unwrap(); + let p = s.preview(&[("solid.extrude".into(), json!({"distance": 15}))]).unwrap(); assert!(p.before.bodies.is_empty()); assert!(rel(body_volume(&p.after), 40.0 * 30.0 * 15.0) < 1e-9); assert!(Arc::ptr_eq(&doc, &s.doc)); assert_eq!((s.undo.len(), s.revision), (undo, rev)); assert!(s.model.state().bodies.is_empty()); // Errors come back as errors and change nothing either. - assert!(s.preview(&[("Extrude".into(), json!({"distance": "nonsense"}))]).is_err()); + assert!(s.preview(&[("solid.extrude".into(), json!({"distance": "nonsense"}))]).is_err()); assert!(Arc::ptr_eq(&doc, &s.doc)); // Fillet preview on a real body. - run(&mut s, "Extrude", json!({"distance": 20})); + run(&mut s, "solid.extrude", json!({"distance": 20})); let (doc, undo) = (s.doc.clone(), s.undo.len()); - let p = s.preview(&[("FusionFilletEdgesCommand".into(), json!({"edges": [[0, 0, 10]], "radius": 3}))]).unwrap(); + let p = s.preview(&[("solid.fillet".into(), json!({"edges": [[0, 0, 10]], "radius": 3}))]).unwrap(); let filleted = 24000.0 - (9.0 - PI * 9.0 / 4.0) * 20.0; assert!(rel(body_volume(&p.after), filleted) < 2e-4); assert!(rel(body_volume(&p.before), 24000.0) < 1e-9); @@ -554,16 +547,16 @@ fn preview_has_no_side_effects() { #[test] fn preview_of_an_edit() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); - run(&mut s, "SketchStop", json!({})); - let r = run(&mut s, "Extrude", json!({"distance": 20})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); + run(&mut s, "sketch.finish", json!({})); + let r = run(&mut s, "solid.extrude", json!({"distance": 20})); let fid = r["feature"].as_u64().unwrap(); let idx = s.doc.feature_index(fid).unwrap(); - run(&mut s, "timeline.rollTo", json!({"position": idx})); + run(&mut s, "timeline.roll_to", json!({"position": idx})); assert!(s.model.state().bodies.is_empty()); let doc = s.doc.clone(); - let p = s.preview(&[("timeline.redefine".into(), json!({"feature": fid, "command": "Extrude", "params": {"distance": 10}}))]).unwrap(); + let p = s.preview(&[("timeline.redefine".into(), json!({"feature": fid, "command": "solid.extrude", "params": {"distance": 10}}))]).unwrap(); assert!(rel(body_volume(&p.after), 12000.0) < 1e-9); assert!(Arc::ptr_eq(&doc, &s.doc)); } @@ -572,25 +565,25 @@ fn preview_of_an_edit() { #[test] fn extrude_auto_operation() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); // A face pushed in (negative distance) cuts. let p = json!({"face": [20, 15, 20], "distance": -5, "operation": "auto"}); assert_eq!(auto_operation(&s, &p), Some("cut")); - run(&mut s, "Extrude", p); + run(&mut s, "solid.extrude", p); assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0) < 1e-6); // Pulled out, it joins. assert_eq!(auto_operation(&s, &json!({"face": [20, 15, 15], "distance": 5})), Some("join")); // A sketch on XY under the box: up goes into it (cut), down grows out of it (join), and a // profile beside the box makes a new body. - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [5, 5], "p1": [15, 15]})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [60, 5], "p1": [70, 15]})); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [15, 15]})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [60, 5], "p1": [70, 15]})); + run(&mut s, "sketch.finish", json!({})); let at = |x: f64, d: f64| json!({"profiles": [{"point": [x, 10]}], "distance": d}); assert_eq!(auto_operation(&s, &at(10.0, 8.0)), Some("cut")); assert_eq!(auto_operation(&s, &at(10.0, -8.0)), Some("join")); assert_eq!(auto_operation(&s, &at(65.0, 8.0)), Some("new")); - run(&mut s, "Extrude", json!({"profiles": [{"point": [10, 10]}], "distance": 8, "operation": "auto"})); + run(&mut s, "solid.extrude", json!({"profiles": [{"point": [10, 10]}], "distance": 8, "operation": "auto"})); assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0 - 100.0 * 8.0) < 1e-6); } @@ -598,7 +591,7 @@ fn extrude_auto_operation() { #[test] fn measure_items() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); let st = s.model.state(); let m = st.body("Body1").unwrap().mesh(); // Find the faces by their centroid: top (z = 20), bottom (z = 0), front (y = 0). @@ -612,36 +605,36 @@ fn measure_items() { }; let (ft, fb, ff) = (face_at(Some(20.0), None), face_at(Some(0.0), None), face_at(None, Some(0.0))); let face = |i: usize, p: [f64; 3]| json!({"type": "face", "body": "Body1", "index": i, "point": p}); - let r = run(&mut s, "MeasureCommand", json!({"items": [face(ft, [20.0, 15.0, 20.0]), face(fb, [20.0, 15.0, 0.0])]})); + let r = run(&mut s, "inspect.measure", json!({"items": [face(ft, [20.0, 15.0, 20.0]), face(fb, [20.0, 15.0, 0.0])]})); assert!((r["distance_mm"].as_f64().unwrap() - 20.0).abs() < 1e-9, "{r}"); assert!((r["angle_deg"].as_f64().unwrap() - 180.0).abs() < 1e-6, "{r}"); - let r = run(&mut s, "MeasureCommand", json!({"items": [face(ft, [20.0, 15.0, 20.0]), face(ff, [20.0, 0.0, 10.0])]})); + let r = run(&mut s, "inspect.measure", json!({"items": [face(ft, [20.0, 15.0, 20.0]), face(ff, [20.0, 0.0, 10.0])]})); assert!(r["distance_mm"].as_f64().unwrap() < 1e-9); assert!((r["angle_deg"].as_f64().unwrap() - 90.0).abs() < 1e-6); - let r = run(&mut s, "MeasureCommand", json!({"items": [face(ft, [20.0, 15.0, 20.0])]})); + let r = run(&mut s, "inspect.measure", json!({"items": [face(ft, [20.0, 15.0, 20.0])]})); assert!((r["items"][0]["area_mm2"].as_f64().unwrap() - 1200.0).abs() < 1e-6, "{r}"); let v = json!({"type": "vertex", "body": "Body1", "point": [0, 0, 0]}); let w = json!({"type": "vertex", "body": "Body1", "point": [40, 30, 20]}); - let r = run(&mut s, "MeasureCommand", json!({"items": [v, w]})); + let r = run(&mut s, "inspect.measure", json!({"items": [v, w]})); assert!((r["distance_mm"].as_f64().unwrap() - (1600.0f64 + 900.0 + 400.0).sqrt()).abs() < 1e-9); - assert!(s.execute("MeasureCommand", &json!({"items": []})).is_err()); - assert!(s.execute("MeasureCommand", &json!({"items": [{"type": "nonsense"}]})).is_err()); + assert!(s.execute("inspect.measure", &json!({"items": []})).is_err()); + assert!(s.execute("inspect.measure", &json!({"items": [{"type": "nonsense"}]})).is_err()); } /// Section Analysis sets and clears the view cut without touching the design or history. #[test] fn section_analysis() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); let (doc, undo) = (s.doc.clone(), s.undo.len()); - run(&mut s, "FusionHalfSectionViewCommand", json!({"plane": "XZ", "offset": 15})); + run(&mut s, "inspect.section", json!({"plane": "XZ", "offset": 15})); let (o, n) = s.section.unwrap(); assert!((o - n * 15.0).len() < 1e-12 && (n.y.abs() - 1.0).abs() < 1e-12); - run(&mut s, "FusionHalfSectionViewCommand", json!({"plane": {"origin": [0, 0, 5], "normal": [0, 0, 2]}, "flip": true})); + run(&mut s, "inspect.section", json!({"plane": {"origin": [0, 0, 5], "normal": [0, 0, 2]}, "flip": true})); let (o, n) = s.section.unwrap(); assert!((o.z - 5.0).abs() < 1e-12 && (n.z + 1.0).abs() < 1e-12); - assert!(s.execute("FusionHalfSectionViewCommand", &json!({"plane": {"normal": [0, 0, 0]}})).is_err()); - run(&mut s, "FusionHalfSectionViewCommand", json!({"clear": true})); + assert!(s.execute("inspect.section", &json!({"plane": {"normal": [0, 0, 0]}})).is_err()); + run(&mut s, "inspect.section", json!({"clear": true})); assert!(s.section.is_none()); assert!(Arc::ptr_eq(&doc, &s.doc)); assert_eq!(s.undo.len(), undo); @@ -651,15 +644,15 @@ fn section_analysis() { #[test] fn press_pull() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); - run(&mut s, "FusionPressPullCommand", json!({"face": [20, 15, 20], "distance": 5})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.press_pull", json!({"face": [20, 15, 20], "distance": 5})); assert!(rel(volume(&mut s), 40.0 * 30.0 * 25.0) < 1e-6); - run(&mut s, "FusionPressPullCommand", json!({"face": [20, 15, 25], "distance": "-10"})); + run(&mut s, "solid.press_pull", json!({"face": [20, 15, 25], "distance": "-10"})); assert!(rel(volume(&mut s), 40.0 * 30.0 * 15.0) < 1e-6); - assert!(s.execute("FusionPressPullCommand", &json!({"face": [20, 15, 15], "distance": 0})).is_err()); + assert!(s.execute("solid.press_pull", &json!({"face": [20, 15, 15], "distance": 0})).is_err()); let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); - run(&mut s, "FusionPressPullCommand", json!({"edges": [[0, 0, 7]], "distance": 3})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.press_pull", json!({"edges": [[0, 0, 7]], "distance": 3})); assert!(rel(volume(&mut s), 24000.0 - (9.0 - PI * 9.0 / 4.0) * 20.0) < 2e-4); } @@ -667,23 +660,23 @@ fn press_pull() { #[test] fn extrude_a_body_face() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); - run(&mut s, "Extrude", json!({"face": [20, 15, 20], "distance": 15, "operation": "join"})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.extrude", json!({"face": [20, 15, 20], "distance": 15, "operation": "join"})); assert!(rel(volume(&mut s), 40.0 * 30.0 * 35.0) < 1e-6); assert!(s.active_sketch.is_none()); // A face with a round hole: the hole stays open (a new body: joins of curved bodies on a // shared face are a kernel gap for now). let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); - run(&mut s, "SketchCreate", json!({"plane": "XY"})); - run(&mut s, "CircleCenterRadius", json!({"center": [20, 15], "radius": 5})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 20, "operation": "cut"})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "sketch.create", json!({"plane": "XY"})); + run(&mut s, "sketch.circle.center", json!({"center": [20, 15], "radius": 5})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 20, "operation": "cut"})); let before = volume(&mut s); - run(&mut s, "Extrude", json!({"face": [5, 5, 20], "distance": 10, "operation": "new"})); + run(&mut s, "solid.extrude", json!({"face": [5, 5, 20], "distance": 10, "operation": "new"})); let v = volume(&mut s); assert!(rel(v - before, (1200.0 - PI * 25.0) * 10.0) < 1e-3, "{v} {before}"); - assert!(s.execute("Extrude", &json!({"face": [500, 5, 20], "distance": 10})).is_err()); + assert!(s.execute("solid.extrude", &json!({"face": [500, 5, 20], "distance": 10})).is_err()); } /// Export 3MF; open and insert it as mesh bodies; mesh bodies move and export but refuse @@ -693,10 +686,10 @@ fn threemf_export_open_insert() { let dir = std::env::temp_dir().join(format!("solvecraft-3mf-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); - run(&mut s, "PrimitiveCylinder", json!({"radius": 5, "height": 10, "base": [100, 0, 0]})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.cylinder", json!({"radius": 5, "height": 10, "base": [100, 0, 0]})); let p = dir.join("Parts.3MF"); - run(&mut s, "ExportCommand", json!({"path": p.to_string_lossy()})); + run(&mut s, "file.export", json!({"path": p.to_string_lossy()})); let want = volume(&mut s); let mut m = Session::default(); @@ -704,23 +697,23 @@ fn threemf_export_open_insert() { assert_eq!(r["bodies"].as_array().unwrap().len(), 2, "{r}"); assert_eq!(m.doc.features[0].kind.type_name(), "MeshFeature"); assert!(rel(volume(&mut m), want) < 2e-3, "{} vs {want}", volume(&mut m)); - let meas = run(&mut m, "MeasureCommand", json!({})); + let meas = run(&mut m, "inspect.measure", json!({})); assert!(meas["bodies"][0]["face_types"]["mesh"].as_u64().unwrap() >= 12, "{meas}"); - let e = m.execute("FusionFilletEdgesCommand", &json!({"edges": [[0, 0, 10]], "radius": 2})).unwrap_err().to_string(); + let e = m.execute("solid.fillet", &json!({"edges": [[0, 0, 10]], "radius": 2})).unwrap_err().to_string(); assert!(e.contains("mesh"), "{e}"); - assert!(m.execute("ExportCommand", &json!({"path": dir.join("x.step").to_string_lossy()})).is_err()); + assert!(m.execute("file.export", &json!({"path": dir.join("x.step").to_string_lossy()})).is_err()); let stl = dir.join("again.stl"); - run(&mut m, "ExportCommand", json!({"path": stl.to_string_lossy()})); + run(&mut m, "file.export", json!({"path": stl.to_string_lossy()})); // Insert the STL into the solid design; names stay unique. - run(&mut s, "ParaMeshInsertAlignCommand", json!({"path": stl.to_string_lossy()})); + run(&mut s, "file.insert_mesh", json!({"path": stl.to_string_lossy()})); assert_eq!(s.model.state().bodies.len(), 3); assert!(rel(volume(&mut s), 2.0 * want) < 2e-3); - assert!(s.execute("ParaMeshInsertAlignCommand", &json!({"path": dir.join("x.step").to_string_lossy()})).is_err()); + assert!(s.execute("file.insert_mesh", &json!({"path": dir.join("x.step").to_string_lossy()})).is_err()); let junk = dir.join("junk.3mf"); std::fs::write(&junk, b"PK\x03\x04 not really").unwrap(); let before = s.doc.clone(); - assert!(s.execute("ParaMeshInsertAlignCommand", &json!({"path": junk.to_string_lossy()})).is_err()); + assert!(s.execute("file.insert_mesh", &json!({"path": junk.to_string_lossy()})).is_err()); assert!(s.execute("doc.open", &json!({"path": junk.to_string_lossy()})).is_err()); assert_eq!(*s.doc, *before); let _ = std::fs::remove_dir_all(&dir); @@ -729,16 +722,16 @@ fn threemf_export_open_insert() { #[test] fn holes_at_sketch_points_threads_and_components() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 60, "width": 40, "height": 10, "name": "Plate"})); - run(&mut s, "SketchCreate", json!({"plane": {"face": [30, 20, 10]}, "name": "HolePoints"})); + run(&mut s, "solid.box", json!({"length": 60, "width": 40, "height": 10, "name": "Plate"})); + run(&mut s, "sketch.create", json!({"plane": {"face": [30, 20, 10]}, "name": "HolePoints"})); let mut ids = Vec::new(); for (k, (x, y)) in [(10.0, 10.0), (50.0, 10.0), (50.0, 30.0), (10.0, 30.0)].into_iter().enumerate() { let id = format!("h{k}"); - run(&mut s, "DrawPoint", json!({ "point": [x, y], "id": id })); + run(&mut s, "sketch.point", json!({ "point": [x, y], "id": id })); ids.push(id); } - run(&mut s, "SketchStop", json!({})); - run(&mut s, "FusionHoleCommand", json!({"sketch": "HolePoints", "points": ids, "diameter": 5, "thread": "M6", "name": "Holes"})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.hole", json!({"sketch": "HolePoints", "points": ids, "diameter": 5, "thread": "M6", "name": "Holes"})); let v = volume(&mut s); assert!(rel(v, 60.0 * 40.0 * 10.0 - 4.0 * PI * 6.25 * 10.0) < 1e-3, "{v}"); let t = run(&mut s, "model.threads", json!({})); @@ -747,22 +740,22 @@ fn holes_at_sketch_points_threads_and_components() { assert_eq!(threads[0]["designation"], "M6"); assert_eq!(threads[0]["internal"], true); // A cosmetic thread on a shaft; designation from its size. - run(&mut s, "PrimitiveCylinder", json!({"base": [100, 0, 0], "radius": 4, "height": 20, "name": "Shaft", "body_name": "Shaft"})); - let th = run(&mut s, "FusionThreadCommand", json!({"face": [104, 0, 10], "length": 12})); + run(&mut s, "solid.cylinder", json!({"base": [100, 0, 0], "radius": 4, "height": 20, "name": "Shaft", "body_name": "Shaft"})); + let th = run(&mut s, "solid.thread", json!({"face": [104, 0, 10], "length": 12})); assert_eq!(th["thread"]["designation"], "M8", "{th}"); assert!((th["thread"]["end"].as_f64().unwrap() - th["thread"]["start"].as_f64().unwrap() - 12.0).abs() < 1e-6, "{th}"); - assert!(s.execute("FusionThreadCommand", &json!({"face": [104, 0, 10], "designation": "M20"})).is_err()); + assert!(s.execute("solid.thread", &json!({"face": [104, 0, 10], "designation": "M20"})).is_err()); // Components: features made while one is active belong to it. - let c = run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Bracket"})); + let c = run(&mut s, "component.create", json!({"name": "Bracket"})); let cid = c["component"].as_u64().unwrap(); - run(&mut s, "PrimitiveBox", json!({"corner": [0, 60, 0], "length": 10, "width": 10, "height": 10, "name": "Block"})); + run(&mut s, "solid.box", json!({"corner": [0, 60, 0], "length": 10, "width": 10, "height": 10, "name": "Block"})); run(&mut s, "component.activate", json!({"component": "root"})); let l = run(&mut s, "component.list", json!({})); let comps = l["components"].as_array().unwrap(); let bracket = comps.iter().find(|x| x["id"] == cid).unwrap(); assert_eq!(bracket["features"], json!(["Block"]), "{l}"); assert_eq!(bracket["bodies"].as_array().map(|a| a.len()), Some(1), "{l}"); - let r = run(&mut s, "FusionCreateComponentsFromBodiesCommand", json!({"bodies": ["Shaft"]})); + let r = run(&mut s, "component.from_bodies", json!({"bodies": ["Shaft"]})); assert_eq!(r["components"].as_array().map(|a| a.len()), Some(1)); // Saved and loaded with the components. let text = s.doc.to_json(); @@ -775,45 +768,45 @@ fn holes_at_sketch_points_threads_and_components() { fn pipe_scale_offset_bounding_and_materials() { let mut s = Session::default(); // Pipe along an L path (line, arc, line) on XZ. - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Path"})); - run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 0]], "ids": ["l1"]})); - run(&mut s, "ArcCenterTwoPoint", json!({"center": [30, 10], "start": "l1.end", "end": [40, 10], "id": "a1"})); - run(&mut s, "DrawPolyline", json!({"points": ["a1.end", [40, 40]], "ids": ["l2"]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "PrimitivePipe", json!({"path_sketch": "Path", "path": ["l1", "a1", "l2"], "diameter": 4, "wall": 1, "body_name": "Tube"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Path"})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [30, 0]], "ids": ["l1"]})); + run(&mut s, "sketch.arc.center_point", json!({"center": [30, 10], "start": "l1.end", "end": [40, 10], "id": "a1"})); + run(&mut s, "sketch.line", json!({"points": ["a1.end", [40, 40]], "ids": ["l2"]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.pipe", json!({"path_sketch": "Path", "path": ["l1", "a1", "l2"], "diameter": 4, "wall": 1, "body_name": "Tube"})); let len = 30.0 + PI * 10.0 / 2.0 + 30.0; let v = volume(&mut s); assert!(rel(v, PI * (4.0 - 1.0) * len) < 2e-3, "{v} vs {}", PI * 3.0 * len); // Bounding solid around it; then scale a box and push one of its faces. - run(&mut s, "StockModelCommand", json!({"bodies": ["Tube"], "margin": 1, "body_name": "Stock"})); - let m = run(&mut s, "MeasureCommand", json!({"bodies": ["Stock"]})); + run(&mut s, "solid.bounding_solid", json!({"bodies": ["Tube"], "margin": 1, "body_name": "Stock"})); + let m = run(&mut s, "inspect.measure", json!({"bodies": ["Stock"]})); let bb = &m["bodies"][0]["bbox"]; assert!((bb["min"][1].as_f64().unwrap() + 3.0).abs() < 1e-6 && (bb["max"][1].as_f64().unwrap() - 41.0).abs() < 1e-6, "{bb}"); - run(&mut s, "PrimitiveBox", json!({"corner": [100, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Cube"})); - run(&mut s, "ModifyScale", json!({"bodies": ["Cube"], "factor": 2, "origin": [100, 0, 0]})); - let vc = run(&mut s, "MeasureCommand", json!({"bodies": ["Cube"]}))["total"]["volume_mm3"].as_f64().unwrap(); + run(&mut s, "solid.box", json!({"corner": [100, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Cube"})); + run(&mut s, "solid.scale", json!({"bodies": ["Cube"], "factor": 2, "origin": [100, 0, 0]})); + let vc = run(&mut s, "inspect.measure", json!({"bodies": ["Cube"]}))["total"]["volume_mm3"].as_f64().unwrap(); assert!(rel(vc, 8000.0) < 1e-9, "{vc}"); - run(&mut s, "FusionOffsetFacesCommand", json!({"faces": [[110, 10, 20]], "distance": 5})); - let vc = run(&mut s, "MeasureCommand", json!({"bodies": ["Cube"]}))["total"]["volume_mm3"].as_f64().unwrap(); + run(&mut s, "solid.offset_face", json!({"faces": [[110, 10, 20]], "distance": 5})); + let vc = run(&mut s, "inspect.measure", json!({"bodies": ["Cube"]}))["total"]["volume_mm3"].as_f64().unwrap(); assert!(rel(vc, 20.0 * 20.0 * 25.0) < 1e-9, "{vc}"); // Materials and mass. - run(&mut s, "PhysicalMaterialCommand", json!({"bodies": ["Cube"], "material": "aluminum"})); - let m = run(&mut s, "MeasureCommand", json!({"bodies": ["Cube"]})); + run(&mut s, "material.assign", json!({"bodies": ["Cube"], "material": "aluminum"})); + let m = run(&mut s, "inspect.measure", json!({"bodies": ["Cube"]})); assert!((m["bodies"][0]["mass_g"].as_f64().unwrap() - 10_000.0 * 2.7 / 1000.0).abs() < 1e-6, "{m}"); assert_eq!(m["bodies"][0]["material"], "Aluminum"); - assert!(s.execute("PhysicalMaterialCommand", &json!({"bodies": ["Cube"], "material": "unobtainium"})).is_err()); + assert!(s.execute("material.assign", &json!({"bodies": ["Cube"], "material": "unobtainium"})).is_err()); } #[test] fn components_occurrences_and_world_placement() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "Base"})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Base"})); // A component, active: its box is authored in its frame. - let c = run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Arm"})); + let c = run(&mut s, "component.create", json!({"name": "Arm"})); let (cid, occ) = (c["component"].as_u64().unwrap(), c["occurrence"].as_u64().unwrap()); - run(&mut s, "PrimitiveBox", json!({"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"})); + run(&mut s, "solid.box", json!({"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"})); run(&mut s, "component.activate", json!({"component": 0})); - let bbox = |s: &mut Session, b: &str| run(s, "MeasureCommand", json!({ "bodies": [b] }))["bodies"][0]["bbox"].clone(); + let bbox = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({ "bodies": [b] }))["bodies"][0]["bbox"].clone(); assert_eq!(bbox(&mut s, "ArmBody")["min"][0], 20.0); // Move the occurrence: the world placement follows, the authoring model doesn't change. run(&mut s, "occurrence.move", json!({"occurrence": occ, "translate": [0, 50, 0]})); @@ -821,8 +814,8 @@ fn components_occurrences_and_world_placement() { assert_eq!(s.model.state().body("ArmBody").unwrap().mesh().bounds().min.y, 0.0); // A fillet picked in the world, while the component is active, lands on its edge. run(&mut s, "component.activate", json!({"component": "Arm"})); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[25, 54, 4]], "radius": 1})); - let v = run(&mut s, "MeasureCommand", json!({"bodies": ["ArmBody"]}))["total"]["volume_mm3"].as_f64().unwrap(); + run(&mut s, "solid.fillet", json!({"edges": [[25, 54, 4]], "radius": 1})); + let v = run(&mut s, "inspect.measure", json!({"bodies": ["ArmBody"]}))["total"]["volume_mm3"].as_f64().unwrap(); assert!(rel(v, 160.0 - (1.0 - PI / 4.0) * 10.0) < 1e-4, "{v}"); run(&mut s, "component.activate", json!({"component": 0})); // Instances share the contents; a copy is independent. @@ -838,10 +831,10 @@ fn components_occurrences_and_world_placement() { run(&mut s, "occurrence.ground", json!({"occurrence": occ, "grounded": false})); run(&mut s, "occurrence.move", json!({"occurrence": occ, "translate": [0, 0, 5], "capture": false})); assert!((bbox(&mut s, "ArmBody")["min"][2].as_f64().unwrap() - 5.0).abs() < 1e-9); - run(&mut s, "AsBuiltPositionsCmd", json!({})); + run(&mut s, "component.revert_position", json!({})); assert!(bbox(&mut s, "ArmBody")["min"][2].as_f64().unwrap().abs() < 1e-9); run(&mut s, "occurrence.move", json!({"occurrence": occ, "translate": [0, 0, 5], "capture": false})); - run(&mut s, "SnapshotCmd", json!({})); + run(&mut s, "component.capture_position", json!({})); assert!(s.pending_moves.is_empty()); assert!((bbox(&mut s, "ArmBody")["min"][2].as_f64().unwrap() - 5.0).abs() < 1e-9); // Bodies and sketches move between components. @@ -849,18 +842,18 @@ fn components_occurrences_and_world_placement() { let l = run(&mut s, "component.list", json!({})); let arm = l["components"].as_array().unwrap().iter().find(|x| x["id"] == cid).unwrap().clone(); assert!(arm["bodies"].as_array().unwrap().iter().any(|b| b == "Base"), "{arm}"); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "S1"})); - run(&mut s, "SketchStop", json!({})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S1"})); + run(&mut s, "sketch.finish", json!({})); run(&mut s, "component.move_sketches", json!({"sketches": ["S1"], "component": cid})); assert_eq!(s.doc.find_feature("S1").unwrap().component, cid); // Undo/redo and the timeline work with features in components. - run(&mut s, "UndoCommand", json!({})); + run(&mut s, "edit.undo", json!({})); assert_eq!(s.doc.find_feature("S1").unwrap().component, 0); - run(&mut s, "RedoCommand", json!({})); - run(&mut s, "timeline.rollTo", json!({"position": 1})); + run(&mut s, "edit.redo", json!({})); + run(&mut s, "timeline.roll_to", json!({"position": 1})); // Only Base is left; it now lives in Arm, which is placed twice (original and instance). assert_eq!(s.world_state().bodies.len(), 2); - run(&mut s, "timeline.rollTo", json!({})); + run(&mut s, "timeline.roll_to", json!({})); // Saved and loaded; older designs get occurrences. let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap(); assert_eq!(back.occurrences.len(), s.doc.occurrences.len()); @@ -873,8 +866,8 @@ fn components_occurrences_and_world_placement() { #[test] fn feature_inputs_are_parameters() { let mut s = Session::default(); - run(&mut s, "ChangeParameterCommand", json!({"name": "w", "expression": "15 mm", "comment": "width"})); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": "w", "height": "1 in", "name": "B"})); + run(&mut s, "parameters.change", json!({"name": "w", "expression": "15 mm", "comment": "width"})); + run(&mut s, "solid.box", json!({"length": 10, "width": "w", "height": "1 in", "name": "B"})); let l = run(&mut s, "parameters.list", json!({})); let rows = l["parameters"].as_array().unwrap(); let feat: Vec<&Value> = rows.iter().filter(|r| r["source"] == "feature").collect(); @@ -883,15 +876,15 @@ fn feature_inputs_are_parameters() { assert!((height["value"].as_f64().unwrap() - 25.4).abs() < 1e-9); let length_name = feat.iter().find(|r| r["input"] == "Length").unwrap()["name"].as_str().unwrap().to_string(); // A feature parameter changes the feature; other expressions can use it. - run(&mut s, "ChangeParameterCommand", json!({"name": length_name, "expression": "2 * w"})); + run(&mut s, "parameters.change", json!({"name": length_name, "expression": "2 * w"})); assert!(rel(volume(&mut s), 30.0 * 15.0 * 25.4) < 1e-9); - run(&mut s, "ChangeParameterCommand", json!({"name": "t", "expression": format!("{length_name} / 3")})); + run(&mut s, "parameters.change", json!({"name": "t", "expression": format!("{length_name} / 3")})); let t = run(&mut s, "parameters.list", json!({}))["parameters"].as_array().unwrap().iter().find(|r| r["name"] == "t").unwrap()["value"] .as_f64() .unwrap(); assert!((t - 10.0).abs() < 1e-9); // Cycles are refused. - assert!(s.execute("ChangeParameterCommand", &json!({"name": "w", "expression": "t"})).is_err()); + assert!(s.execute("parameters.change", &json!({"name": "w", "expression": "t"})).is_err()); // Favourites, export and import. run(&mut s, "parameters.favorite", json!({"name": "w"})); assert_eq!(run(&mut s, "parameters.list", json!({"favorites": true}))["parameters"].as_array().unwrap().len(), 1); @@ -916,8 +909,8 @@ fn rolling_back_and_forward_leaves_no_projection_warnings() { }; assert!(warnings(&s).is_empty(), "{:?}", warnings(&s)); for pos in 0..s.doc.features.len() { - run(&mut s, "timeline.rollTo", json!({ "position": pos })); - run(&mut s, "timeline.rollTo", json!({})); + run(&mut s, "timeline.roll_to", json!({ "position": pos })); + run(&mut s, "timeline.roll_to", json!({})); assert!(warnings(&s).is_empty(), "after rolling to {pos}: {:?}", warnings(&s)); } } @@ -925,7 +918,7 @@ fn rolling_back_and_forward_leaves_no_projection_warnings() { /// Export STEP, validate the file, read it back: no warnings, same volume, area and faces. fn step_round_trip(s: &mut Session, tag: &str) -> solvecraft_kernel::StepImport { let p = std::env::temp_dir().join(format!("solvecraft-steprt-{tag}-{}.step", std::process::id())); - run(s, "ExportCommand", json!({"path": p.to_string_lossy()})); + run(s, "file.export", json!({"path": p.to_string_lossy()})); let text = std::fs::read_to_string(&p).unwrap(); let _ = std::fs::remove_file(&p); solvecraft_kernel::step_validate(&text).unwrap(); @@ -958,13 +951,13 @@ fn sample_plate_step_round_trip() { #[test] fn material_and_appearance_colours_export_to_step() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "BrassBox"})); - run(&mut s, "PrimitiveBox", json!({"corner": [20, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "RedAbs"})); - run(&mut s, "PrimitiveBox", json!({"corner": [40, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Plain"})); - run(&mut s, "PhysicalMaterialCommand", json!({"bodies": ["BrassBox"], "material": "Brass"})); - run(&mut s, "PhysicalMaterialCommand", json!({"bodies": ["RedAbs"], "material": "ABS Plastic"})); - run(&mut s, "AppearanceCommand", json!({"bodies": ["RedAbs"], "color": "#d01c1c"})); - assert!(s.execute("AppearanceCommand", &json!({"bodies": ["RedAbs"], "color": "#12"})).is_err()); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "BrassBox"})); + run(&mut s, "solid.box", json!({"corner": [20, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "RedAbs"})); + run(&mut s, "solid.box", json!({"corner": [40, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Plain"})); + run(&mut s, "material.assign", json!({"bodies": ["BrassBox"], "material": "Brass"})); + run(&mut s, "material.assign", json!({"bodies": ["RedAbs"], "material": "ABS Plastic"})); + run(&mut s, "appearance.assign", json!({"bodies": ["RedAbs"], "color": "#d01c1c"})); + assert!(s.execute("appearance.assign", &json!({"bodies": ["RedAbs"], "color": "#12"})).is_err()); let rgb = |c: [u8; 3]| c.map(|x| x as f32 / 255.0); let want = [("BrassBox", Some(rgb(solvecraft_doc::material_color("Brass").unwrap()))), ("RedAbs", Some(rgb([0xd0, 0x1c, 0x1c]))), ("Plain", None)]; @@ -982,7 +975,7 @@ fn material_and_appearance_colours_export_to_step() { check(&step_round_trip(&mut s, "colours")); // Open the export and export again: the file's colours are kept. let p = std::env::temp_dir().join(format!("solvecraft-colours-{}.step", std::process::id())); - run(&mut s, "ExportCommand", json!({"path": p.to_string_lossy()})); + run(&mut s, "file.export", json!({"path": p.to_string_lossy()})); let mut s2 = Session::default(); run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()})); let _ = std::fs::remove_file(&p); @@ -994,9 +987,9 @@ fn material_and_appearance_colours_export_to_step() { #[test] fn face_colours_and_opacity_export() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "Cube"})); - run(&mut s, "AppearanceCommand", json!({"bodies": ["Cube"], "color": "#2040c0", "opacity": 0.5})); - run(&mut s, "AppearanceCommand", json!({"faces": [[5, 5, 10]], "color": "#ff0000"})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Cube"})); + run(&mut s, "appearance.assign", json!({"bodies": ["Cube"], "color": "#2040c0", "opacity": 0.5})); + run(&mut s, "appearance.assign", json!({"faces": [[5, 5, 10]], "color": "#ff0000"})); let imp = step_round_trip(&mut s, "facecol"); let b = &imp.bodies[0]; let paint = b.body.paint().expect("paint read back"); @@ -1008,7 +1001,7 @@ fn face_colours_and_opacity_export() { assert!((paint.faces[0].color[0] - 1.0).abs() < 1e-3 && paint.faces[0].color[1].abs() < 1e-3, "{paint:?}"); // Opened again, the face keeps its colour (shown and exported). let p = std::env::temp_dir().join(format!("solvecraft-facecol-{}.step", std::process::id())); - run(&mut s, "ExportCommand", json!({"path": p.to_string_lossy()})); + run(&mut s, "file.export", json!({"path": p.to_string_lossy()})); let mut s2 = Session::default(); run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()})); let _ = std::fs::remove_file(&p); @@ -1023,12 +1016,12 @@ fn face_colours_and_opacity_export() { #[test] fn iges_export_and_open() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 5, "body_name": "Plate"})); - run(&mut s, "PrimitiveCylinder", json!({"radius": 4, "height": 20, "base": [10, 10, 5], "body_name": "Pin"})); - run(&mut s, "AppearanceCommand", json!({"bodies": ["Pin"], "color": "#ff0000"})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 5, "body_name": "Plate"})); + run(&mut s, "solid.cylinder", json!({"radius": 4, "height": 20, "base": [10, 10, 5], "body_name": "Pin"})); + run(&mut s, "appearance.assign", json!({"bodies": ["Pin"], "color": "#ff0000"})); let want: Vec = s.world_state().bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).collect(); let p = std::env::temp_dir().join(format!("solvecraft-iges-{}.igs", std::process::id())); - run(&mut s, "ExportCommand", json!({"path": p.to_string_lossy()})); + run(&mut s, "file.export", json!({"path": p.to_string_lossy()})); let mut s2 = Session::default(); run(&mut s2, "doc.open", json!({"path": p.to_string_lossy()})); let _ = std::fs::remove_file(&p); @@ -1046,16 +1039,16 @@ fn iges_export_and_open() { #[test] fn split_body_by_a_face() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 20, "height": 30, "body_name": "Block"})); - run(&mut s, "PrimitiveCylinder", json!({"radius": 4, "height": 40, "base": [5, 10, -5], "body_name": "Tool"})); - run(&mut s, "FusionSplitBodyCommand", json!({"body": "Block", "tool": {"body": "Tool", "point": [9, 10, 10]}})); + run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "Block"})); + run(&mut s, "solid.cylinder", json!({"radius": 4, "height": 40, "base": [5, 10, -5], "body_name": "Tool"})); + run(&mut s, "solid.split_body", json!({"body": "Block", "tool": {"body": "Tool", "point": [9, 10, 10]}})); let st = s.world_state(); let mut v: Vec = st.bodies.iter().filter(|b| b.name.starts_with("Block")).map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).collect(); v.sort_by(f64::total_cmp); let core = PI * 16.0 * 30.0; assert!(v.len() == 2 && rel(v[0], core) < 1e-3 && rel(v[1], 6000.0 - core) < 1e-3, "{v:?}"); - assert!(s.execute("FusionSplitBodyCommand", &json!({"body": "Block", "tool": {"body": "Tool", "point": [100, 0, 0]}})).is_err()); + assert!(s.execute("solid.split_body", &json!({"body": "Block", "tool": {"body": "Tool", "point": [100, 0, 0]}})).is_err()); } /// Fillet types: chord length (radius from the face angle) and a radius varying along the @@ -1063,27 +1056,27 @@ fn split_body_by_a_face() { #[test] fn chord_and_variable_fillets() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"})); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[10, 10, 30]], "radius": 4, "type": "chord"})); + run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"})); + run(&mut s, "solid.fillet", json!({"edges": [[10, 10, 30]], "radius": 4, "type": "chord"})); let r = 4.0 / 2f64.sqrt(); let v = solvecraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume; assert!(rel(v, 6000.0 - (1.0 - PI / 4.0) * r * r * 20.0) < 1e-4, "{v}"); let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"})); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 5, "start": [10, 0, 30]})); + run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 30, "body_name": "B"})); + run(&mut s, "solid.fillet", json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable", "radius2": 5, "start": [10, 0, 30]})); let v = solvecraft_kernel::measure(&s.world_state().body("B").unwrap().body).unwrap().volume; let removed = (1.0 - PI / 4.0) * 20.0 * (4.0 + 10.0 + 25.0) / 3.0; assert!(rel(v, 6000.0 - removed) < 1e-4, "{v}"); - assert!(s.execute("FusionFilletEdgesCommand", &json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable"})).is_err()); + assert!(s.execute("solid.fillet", &json!({"edges": [[10, 10, 30]], "radius": 2, "type": "variable"})).is_err()); } /// Components export as a STEP assembly: products, occurrences, placements. #[test] fn components_export_as_step_assembly() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 5, "name": "Plate"})); - run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Pin"})); - run(&mut s, "PrimitiveCylinder", json!({"radius": 2, "height": 10, "base": [5, 5, 5]})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 5, "name": "Plate"})); + run(&mut s, "component.create", json!({"name": "Pin"})); + run(&mut s, "solid.cylinder", json!({"radius": 2, "height": 10, "base": [5, 5, 5]})); run(&mut s, "occurrence.copy", json!({"component": "Pin", "translate": [20, 0, 0]})); let want: f64 = s.world_state().bodies.iter().map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).sum(); let imp = step_round_trip(&mut s, "asm"); @@ -1108,16 +1101,20 @@ fn components_export_as_step_assembly() { #[test] fn face_extrude_follows_edits() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20, "name": "Base"})); - run(&mut s, "Extrude", json!({"face": [20, 15, 20], "distance": 10, "operation": "join", "name": "Pad"})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "name": "Base"})); + run(&mut s, "solid.extrude", json!({"face": [20, 15, 20], "distance": 10, "operation": "join", "name": "Pad"})); assert!(rel(volume(&mut s), 40.0 * 30.0 * 30.0) < 1e-6); run(&mut s, "timeline.edit", json!({"feature": "Base", "set": {"height": "25", "length": "50"}})); assert!(rel(volume(&mut s), 50.0 * 30.0 * 35.0) < 1e-6, "{}", volume(&mut s)); // A face revolved about a line: Pappus (area × centroid path). let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); let before = volume(&mut s); - run(&mut s, "Revolve", json!({"face": [40, 15, 10], "axis": {"origin": [40, 0, 30], "dir": [0, 1, 0]}, "angle": "90 deg", "operation": "new"})); + run( + &mut s, + "solid.revolve", + json!({"face": [40, 15, 10], "axis": {"origin": [40, 0, 30], "dir": [0, 1, 0]}, "angle": "90 deg", "operation": "new"}), + ); let added = volume(&mut s) - before; let want = 30.0 * 20.0 * 20.0 * PI / 2.0; assert!(rel(added, want) < 1e-4, "{added} vs {want}"); @@ -1127,42 +1124,42 @@ fn face_extrude_follows_edits() { #[test] fn appearances_by_face_body_and_component() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "Root"})); - run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Case"})); - run(&mut s, "PrimitiveBox", json!({"corner": [20, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Shell"})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Root"})); + run(&mut s, "component.create", json!({"name": "Case"})); + run(&mut s, "solid.box", json!({"corner": [20, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Shell"})); run(&mut s, "component.activate", json!({"component": "root"})); let lib = run(&mut s, "appearance.library", json!({})); assert!(lib["appearances"].as_array().is_some_and(|a| a.len() >= 15 && a.iter().any(|x| x["name"] == "Glass - Clear"))); let look = |s: &mut Session, b: &str| run(s, "appearance.list", json!({"body": b}))["body"].clone(); // Material, then component, then body. - run(&mut s, "PhysicalMaterialCommand", json!({"bodies": ["Shell"], "material": "Brass"})); + run(&mut s, "material.assign", json!({"bodies": ["Shell"], "material": "Brass"})); assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "Brass"); - run(&mut s, "AppearanceCommand", json!({"components": ["Case"], "appearance": "ABS - Blue"})); + run(&mut s, "appearance.assign", json!({"components": ["Case"], "appearance": "ABS - Blue"})); assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "ABS - Blue"); assert!(look(&mut s, "Root")["appearance"].is_null(), "root bodies have no component look"); - run(&mut s, "AppearanceCommand", json!({"bodies": ["Shell"], "color": [255, 0, 0], "opacity": 0.5})); + run(&mut s, "appearance.assign", json!({"bodies": ["Shell"], "color": [255, 0, 0], "opacity": 0.5})); let l = look(&mut s, "Shell"); assert_eq!((l["appearance"]["color"].as_str(), l["appearance"]["opacity"].as_f64()), (Some("#ff0000"), Some(0.5)), "{l}"); // A face: the top of Shell. - run(&mut s, "AppearanceCommand", json!({"faces": [[25, 5, 10]], "appearance": "Glass - Clear"})); + run(&mut s, "appearance.assign", json!({"faces": [[25, 5, 10]], "appearance": "Glass - Clear"})); let l = look(&mut s, "Shell"); let faces = l["faces"].as_array().cloned().unwrap_or_default(); assert_eq!(faces.len(), 1, "{l}"); assert_eq!(faces[0]["appearance"]["name"], "Glass - Clear"); // Clearing goes back down the chain. - run(&mut s, "AppearanceCommand", json!({"faces": [[26, 6, 10]]})); + run(&mut s, "appearance.assign", json!({"faces": [[26, 6, 10]]})); assert_eq!(look(&mut s, "Shell")["faces"].as_array().map(Vec::len), Some(0), "cleared by another point on the same face"); - run(&mut s, "AppearanceCommand", json!({"bodies": ["Shell"], "color": null})); + run(&mut s, "appearance.assign", json!({"bodies": ["Shell"], "color": null})); assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "ABS - Blue"); - run(&mut s, "AppearanceCommand", json!({"components": ["Case"]})); + run(&mut s, "appearance.assign", json!({"components": ["Case"]})); assert_eq!(look(&mut s, "Shell")["appearance"]["name"], "Brass"); // Saved and opened again. - run(&mut s, "AppearanceCommand", json!({"faces": [[5, 5, 10]], "color": "#00ff00"})); + run(&mut s, "appearance.assign", json!({"faces": [[5, 5, 10]], "color": "#00ff00"})); let back = solvecraft_doc::Document::from_json(&s.doc.to_json()).unwrap(); assert_eq!(back.appearances, s.doc.appearances); - assert!(s.execute("AppearanceCommand", &json!({"bodies": ["Shell"], "appearance": "Unobtainium"})).is_err()); - assert!(s.execute("AppearanceCommand", &json!({"color": "#ffffff"})).is_err(), "no target"); - assert!(s.execute("AppearanceCommand", &json!({"faces": [[500, 500, 500]], "color": "#ffffff"})).is_err(), "no face there"); + assert!(s.execute("appearance.assign", &json!({"bodies": ["Shell"], "appearance": "Unobtainium"})).is_err()); + assert!(s.execute("appearance.assign", &json!({"color": "#ffffff"})).is_err(), "no target"); + assert!(s.execute("appearance.assign", &json!({"faces": [[500, 500, 500]], "color": "#ffffff"})).is_err(), "no face there"); } /// The evaluation cache gives exactly what evaluating from scratch gives, through edits, @@ -1173,10 +1170,10 @@ fn cached_evaluation_matches_from_scratch() { s.run_script(&crate::sample::script()).unwrap(); run( &mut s, - "PatternRectangular", + "solid.pattern.rectangular", json!({"bodies": ["Plate"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 100, "dir2": [0, 1, 0], "count2": 2, "spacing2": 70}), ); - run(&mut s, "PrimitiveBox", json!({"corner": [-50, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Side"})); + run(&mut s, "solid.box", json!({"corner": [-50, 0, 0], "length": 10, "width": 10, "height": 10, "body_name": "Side"})); let same = |s: &Session, what: &str| { let mut fresh = solvecraft_doc::Model::new(); fresh.no_cache = true; @@ -1211,17 +1208,17 @@ fn cached_evaluation_matches_from_scratch() { same(&s, "built"); let hits = |s: &Session| s.model.cache.lock().map(|c| c.hits).unwrap_or(0); for (step, (id, p)) in [ - ("ChangeParameterCommand", json!({"name": "width", "expression": "90 mm"})), - ("ChangeParameterCommand", json!({"name": "width", "expression": "80 mm"})), - ("ChangeParameterCommand", json!({"name": "thickness", "expression": "10 mm"})), - ("UndoCommand", json!({})), - ("RedoCommand", json!({})), + ("parameters.change", json!({"name": "width", "expression": "90 mm"})), + ("parameters.change", json!({"name": "width", "expression": "80 mm"})), + ("parameters.change", json!({"name": "thickness", "expression": "10 mm"})), + ("edit.undo", json!({})), + ("edit.redo", json!({})), ("timeline.suppress", json!({"feature": "Holes", "suppressed": true})), ("timeline.suppress", json!({"feature": "Holes", "suppressed": false})), - ("timeline.rollTo", json!({"position": 6})), - ("timeline.rollTo", json!({})), - ("UndoCommand", json!({})), - ("UndoCommand", json!({})), + ("timeline.roll_to", json!({"position": 6})), + ("timeline.roll_to", json!({})), + ("edit.undo", json!({})), + ("edit.undo", json!({})), ] .into_iter() .enumerate() @@ -1239,18 +1236,18 @@ fn cached_evaluation_matches_from_scratch() { #[test] fn fillet_follows_its_edge_after_an_upstream_resize() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "S"})); - let r = run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"})); + let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); let bottom = r["curves"][0].as_str().unwrap_or_default().to_string(); - let d = run(&mut s, "SketchDimension", json!({"entities": [bottom], "value": 40})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 10})); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[40, 15, 10]], "radius": 2})); - let v = |s: &mut Session| run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(0.0); + let d = run(&mut s, "sketch.dimension", json!({"entities": [bottom], "value": 40})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 10})); + run(&mut s, "solid.fillet", json!({"edges": [[40, 15, 10]], "radius": 2})); + let v = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(0.0); let strip = (4.0 - std::f64::consts::PI) * 30.0; assert!((v(&mut s) - (12000.0 - strip)).abs() < 0.05, "{}", v(&mut s)); let name = d["param"].as_str().or_else(|| d["name"].as_str()).map(str::to_string).unwrap_or_else(|| "d1".into()); - run(&mut s, "ChangeParameterCommand", json!({"name": name, "expression": "60"})); + run(&mut s, "parameters.change", json!({"name": name, "expression": "60"})); // The right top edge, now at x = 60: 18000 less the same 30 mm strip. assert!((v(&mut s) - (18000.0 - strip)).abs() < 0.05, "{}", v(&mut s)); let fillet = s.doc.features.last().map(|f| f.id).unwrap_or(0); @@ -1264,7 +1261,7 @@ mod naming { use crate::Session; fn volume(s: &mut Session) -> f64 { - run(s, "MeasureCommand", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(0.0) + run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap_or(0.0) } fn warning(s: &Session, name: &str) -> Option { @@ -1276,16 +1273,16 @@ mod naming { /// fillet on the right top edge. fn block() -> Session { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "S"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); - run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 40})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 10, "name": "Block"})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "H"})); - run(&mut s, "CircleCenterRadius", json!({"center": [12, 15], "radius": 4})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"sketch": "H", "distance": 10, "operation": "cut", "name": "Hole"})); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[40, 15, 10]], "radius": 2, "name": "Round"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); + run(&mut s, "sketch.dimension", json!({"entities": ["l1"], "value": 40})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 10, "name": "Block"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "H"})); + run(&mut s, "sketch.circle.center", json!({"center": [12, 15], "radius": 4})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "H", "distance": 10, "operation": "cut", "name": "Hole"})); + run(&mut s, "solid.fillet", json!({"edges": [[40, 15, 10]], "radius": 2, "name": "Round"})); s } @@ -1309,7 +1306,7 @@ mod naming { fn a_resize_keeps_the_edge() { let mut s = block(); let d = s.doc.find_feature("S").and_then(|f| f.param_names.first().cloned()).unwrap_or_else(|| "d1".into()); - run(&mut s, "ChangeParameterCommand", json!({"name": d, "expression": "60"})); + run(&mut s, "parameters.change", json!({"name": d, "expression": "60"})); assert!((volume(&mut s) - (18000.0 - std::f64::consts::PI * 16.0 * 10.0 - STRIP)).abs() < 0.1, "{}", volume(&mut s)); assert_eq!(warning(&s, "Round"), None); } @@ -1333,12 +1330,12 @@ mod naming { fn a_split_edge_warns_and_takes_the_nearest_piece() { let mut s = block(); // Upstream of the fillet, a notch through the middle of the filleted edge. - run(&mut s, "timeline.rollTo", json!({"feature": "Hole"})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "N"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [36, 12], "p1": [44, 18]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"sketch": "N", "distance": 10, "operation": "cut", "name": "Notch"})); - run(&mut s, "timeline.rollTo", json!({})); + run(&mut s, "timeline.roll_to", json!({"feature": "Hole"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "N"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [36, 12], "p1": [44, 18]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "N", "distance": 10, "operation": "cut", "name": "Notch"})); + run(&mut s, "timeline.roll_to", json!({})); let w = warning(&s, "Round").unwrap_or_default(); assert!(w.contains("split"), "{w}"); } @@ -1348,10 +1345,10 @@ mod naming { let mut s = block(); // Replace the right side (l2) of the rectangle with a new line: the face it made is gone. let sk = s.doc.find_feature("S").map(|f| f.id).unwrap(); - run(&mut s, "SketchActivate", json!({"sketch": sk})); + run(&mut s, "sketch.edit", json!({"sketch": sk})); run(&mut s, "sketch.delete", json!({"entities": ["l2"]})); - run(&mut s, "DrawPolyline", json!({"points": [[40, 0], [40, 30]], "ids": ["right"]})); - let r: Result = s.execute("SketchStop", &json!({})); + run(&mut s, "sketch.line", json!({"points": [[40, 0], [40, 30]], "ids": ["right"]})); + let r: Result = s.execute("sketch.finish", &json!({})); assert!(r.is_ok() || r.is_err()); let w = warning(&s, "Round").unwrap_or_default(); assert!(w.contains("no longer exists"), "{w}"); @@ -1365,21 +1362,21 @@ mod naming { #[test] fn primitives_revolves_and_pattern_copies_are_named() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "B"})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "B"})); let n = face_names(&mut s, "B"); for want in ["F1:+x", "F1:-x", "F1:+y", "F1:-y", "F1:+z", "F1:-z"] { assert!(n.contains(&want.to_string()), "{want} in {n:?}"); } - run(&mut s, "PatternRectangular", json!({"bodies": ["B"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20})); + run(&mut s, "solid.pattern.rectangular", json!({"bodies": ["B"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20})); let bodies: Vec = s.model.state().bodies.iter().map(|b| b.name.clone()).collect(); let copy = face_names(&mut s, &bodies[2]); assert!(copy.iter().all(|x| x.starts_with("F1:") && x.contains("@F2.")), "{copy:?}"); // A revolved ring: the section's four lines name the four faces. let mut r = Session::default(); - run(&mut r, "SketchCreate", json!({"plane": "XZ"})); - run(&mut r, "DrawPolyline", json!({"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true, "ids": ["a", "b", "c", "d"]})); - run(&mut r, "SketchStop", json!({})); - run(&mut r, "Revolve", json!({"axis": "y", "angle": "270 deg"})); + run(&mut r, "sketch.create", json!({"plane": "XZ"})); + run(&mut r, "sketch.line", json!({"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true, "ids": ["a", "b", "c", "d"]})); + run(&mut r, "sketch.finish", json!({})); + run(&mut r, "solid.revolve", json!({"axis": "y", "angle": "270 deg"})); let body = r.model.state().bodies[0].name.clone(); let n = face_names(&mut r, &body); for want in ["F2:side:a", "F2:side:b", "F2:side:c", "F2:side:d", "F2:start", "F2:end"] { @@ -1390,24 +1387,24 @@ mod naming { #[test] fn a_sketch_on_a_face_stays_on_that_face() { let mut s = Session::default(); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "S"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"distance": 10, "name": "Block"})); - run(&mut s, "SketchCreate", json!({"plane": {"face": [30, 15, 10]}, "name": "Top"})); - run(&mut s, "CircleCenterRadius", json!({"center": [30, 15], "radius": 3})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"sketch": "Top", "distance": 5, "operation": "join", "name": "Peg"})); - let top = |s: &mut Session| run(s, "MeasureCommand", json!({}))["bodies"][0]["bbox"]["max"][2].as_f64().unwrap_or(0.0); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": 10, "name": "Block"})); + run(&mut s, "sketch.create", json!({"plane": {"face": [30, 15, 10]}, "name": "Top"})); + run(&mut s, "sketch.circle.center", json!({"center": [30, 15], "radius": 3})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "Top", "distance": 5, "operation": "join", "name": "Peg"})); + let top = |s: &mut Session| run(s, "inspect.measure", json!({}))["bodies"][0]["bbox"]["max"][2].as_f64().unwrap_or(0.0); assert!((top(&mut s) - 15.0).abs() < 1e-6); // Upstream, a tall boss over the picked point: the nearest upward face is now its top, // but the sketch keeps to the block's top face by name. - run(&mut s, "timeline.rollTo", json!({"feature": "Block"})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "B"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [8, -5], "p1": [45, 35]})); - run(&mut s, "SketchStop", json!({})); - run(&mut s, "Extrude", json!({"sketch": "B", "distance": 20, "operation": "join", "name": "Boss"})); - run(&mut s, "timeline.rollTo", json!({})); + run(&mut s, "timeline.roll_to", json!({"feature": "Block"})); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "B"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [8, -5], "p1": [45, 35]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "B", "distance": 20, "operation": "join", "name": "Boss"})); + run(&mut s, "timeline.roll_to", json!({})); // (Its projected face outline changed, which it says; its plane did not.) assert!(!warning(&s, "Top").unwrap_or_default().contains("no longer exists")); // The peg starts on the block's top (z = 10), inside the boss: the part stays 20 tall. @@ -1419,8 +1416,8 @@ mod naming { #[test] fn shell_inner_faces_are_named_after_the_walls() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20, "body_name": "B"})); - run(&mut s, "FusionShellBodyCommand", json!({"faces": [[20, 15, 20]], "thickness": 2})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "body_name": "B"})); + run(&mut s, "solid.shell", json!({"faces": [[20, 15, 20]], "thickness": 2})); let n = face_names(&mut s, "B"); for want in ["F2:inner:F1:-z", "F2:inner:F1:+x", "F2:inner:F1:-x", "F2:inner:F1:+y", "F2:inner:F1:-y"] { assert!(n.contains(&want.to_string()), "{want} in {n:?}"); @@ -1430,8 +1427,8 @@ mod naming { #[test] fn shell_and_hole_faces_follow_by_name() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20, "name": "Base"})); - run(&mut s, "FusionShellBodyCommand", json!({"faces": [[20, 15, 20]], "thickness": 2, "name": "Hollow"})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "name": "Base"})); + run(&mut s, "solid.shell", json!({"faces": [[20, 15, 20]], "thickness": 2, "name": "Hollow"})); let shell = |h: f64| 40.0 * 30.0 * h - 36.0 * 26.0 * (h - 2.0); assert!((volume(&mut s) - shell(20.0)).abs() < 0.5, "{}", volume(&mut s)); // Taller: the picked point is 5 mm under the open face now; its name still finds it. @@ -1447,18 +1444,18 @@ mod naming { #[test] fn tapped_and_clearance_holes() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 10})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 10})); let v0 = volume(&mut s); - run(&mut s, "FusionHoleCommand", json!({"position": [10, 15, 10], "thread": "M6"})); + run(&mut s, "solid.hole", json!({"position": [10, 15, 10], "thread": "M6"})); let v1 = volume(&mut s); assert!(rel(v0 - v1, PI * 2.5 * 2.5 * 10.0) < 1e-3, "{}", v0 - v1); - run(&mut s, "FusionHoleCommand", json!({"position": [30, 15, 10], "clearance": "M6"})); + run(&mut s, "solid.hole", json!({"position": [30, 15, 10], "clearance": "M6"})); let v2 = volume(&mut s); assert!(rel(v1 - v2, PI * 3.3 * 3.3 * 10.0) < 1e-3, "{}", v1 - v2); - run(&mut s, "FusionHoleCommand", json!({"position": [20, 5, 10], "clearance": "M4", "fit": "close"})); + run(&mut s, "solid.hole", json!({"position": [20, 5, 10], "clearance": "M4", "fit": "close"})); assert!(rel(v2 - volume(&mut s), PI * 2.15 * 2.15 * 10.0) < 1e-3); - assert!(s.execute("FusionHoleCommand", &json!({"position": [20, 25, 10], "clearance": "M7"})).is_err()); - assert!(s.execute("FusionHoleCommand", &json!({"position": [20, 25, 10]})).is_err()); + assert!(s.execute("solid.hole", &json!({"position": [20, 25, 10], "clearance": "M7"})).is_err()); + assert!(s.execute("solid.hole", &json!({"position": [20, 25, 10]})).is_err()); } /// In an inch design, a bare length typed into a feature means inches (stored with the unit); @@ -1467,13 +1464,13 @@ fn tapped_and_clearance_holes() { fn bare_lengths_take_the_design_units() { let mut s = Session::default(); s.doc_mut().units = "in".into(); - run(&mut s, "PrimitiveBox", json!({"length": 2, "width": "10 mm", "height": 1})); - let m = run(&mut s, "MeasureCommand", json!({})); + run(&mut s, "solid.box", json!({"length": 2, "width": "10 mm", "height": 1})); + let m = run(&mut s, "inspect.measure", json!({})); let max = |i: usize| m["bodies"][0]["bbox"]["max"][i].as_f64().unwrap_or(0.0); assert!((max(0) - 50.8).abs() < 1e-9 && (max(1) - 10.0).abs() < 1e-9 && (max(2) - 25.4).abs() < 1e-9, "{m}"); let d = s.doc.features[0].param_names[2].clone(); - run(&mut s, "ChangeParameterCommand", json!({"name": d, "expression": "2"})); - let m = run(&mut s, "MeasureCommand", json!({})); + run(&mut s, "parameters.change", json!({"name": d, "expression": "2"})); + let m = run(&mut s, "inspect.measure", json!({})); assert!((m["bodies"][0]["bbox"]["max"][2].as_f64().unwrap_or(0.0) - 50.8).abs() < 1e-9); assert_eq!(s.doc.features[0].kind.inputs()[2].1, "2 in"); } @@ -1482,22 +1479,22 @@ fn bare_lengths_take_the_design_units() { #[test] fn press_pull_on_curved_faces() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[40, 0, 10]], "radius": 3, "name": "Round"})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.fillet", json!({"edges": [[40, 0, 10]], "radius": 3, "name": "Round"})); let corner = |r: f64| (4.0 - PI) * r * r / 4.0 * 20.0; assert!(rel(volume(&mut s), 24000.0 - corner(3.0)) < 1e-4); // A point on the fillet: the arc's middle. let c = Vec3::new(37.0, 3.0, 10.0); let on = c + Vec3::new(1.0, -1.0, 0.0).normalized().unwrap() * 3.0; - let out = run(&mut s, "FusionPressPullCommand", json!({"face": [on.x, on.y, on.z], "distance": 2})); + let out = run(&mut s, "solid.press_pull", json!({"face": [on.x, on.y, on.z], "distance": 2})); assert_eq!(out["edited"], "Round", "{out}"); assert!(rel(volume(&mut s), 24000.0 - corner(5.0)) < 1e-4, "{}", volume(&mut s)); // A hole's wall: pulled along its normal (into the hole), the hole gets smaller. 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": 10})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.hole", json!({"position": [20, 15, 20], "diameter": 10})); let v0 = volume(&mut s); - run(&mut s, "FusionPressPullCommand", json!({"face": [25, 15, 10], "distance": 1})); + run(&mut s, "solid.press_pull", json!({"face": [25, 15, 10], "distance": 1})); let v1 = volume(&mut s); assert!(rel(v1 - v0, PI * (25.0 - 16.0) * 20.0) < 1e-3, "{v0} {v1}"); } @@ -1509,24 +1506,24 @@ fn booleans_stay_in_their_component() { 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": 20, "width": 10, "height": 10, "corner": [x, 0, 0], "body_name": format!("{name}Box")})); + run(&mut s, "component.create", json!({"name": name})); + run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 10, "corner": [x, 0, 0], "body_name": format!("{name}Box")})); } // A's box spans x 0…20, B's x 20…40 (in their own frames, both occurrences in place). run(&mut s, "component.activate", json!({"component": "A"})); - run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Slot"})); - run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [8, 2], "p1": [28, 8]})); - run(&mut s, "SketchStop", json!({})); - let vol = |s: &mut Session, b: &str| run(s, "MeasureCommand", json!({"bodies": [b]}))["total"]["volume_mm3"].as_f64().unwrap_or(0.0); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Slot"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [8, 2], "p1": [28, 8]})); + run(&mut s, "sketch.finish", json!({})); + let vol = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({"bodies": [b]}))["total"]["volume_mm3"].as_f64().unwrap_or(0.0); assert_eq!(auto_operation(&s, &json!({"sketch": "Slot", "distance": 10})), Some("cut")); - run(&mut s, "Extrude", json!({"sketch": "Slot", "distance": 10, "operation": "cut", "name": "Cut"})); + run(&mut s, "solid.extrude", json!({"sketch": "Slot", "distance": 10, "operation": "cut", "name": "Cut"})); assert!((vol(&mut s, "ABox") - (2000.0 - 12.0 * 6.0 * 10.0)).abs() < 1e-6, "{}", vol(&mut s, "ABox")); assert!((vol(&mut s, "BBox") - 2000.0).abs() < 1e-6, "B untouched: {}", vol(&mut s, "BBox")); // Named as a participant, B is cut too. - run(&mut s, "UndoCommand", json!({})); - run(&mut s, "Extrude", json!({"sketch": "Slot", "distance": 10, "operation": "cut", "participants": ["ABox", "BBox"]})); + run(&mut s, "edit.undo", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "Slot", "distance": 10, "operation": "cut", "participants": ["ABox", "BBox"]})); assert!((vol(&mut s, "BBox") - (2000.0 - 8.0 * 6.0 * 10.0)).abs() < 1e-6, "{}", vol(&mut s, "BBox")); - assert!(s.execute("Extrude", &json!({"sketch": "Slot", "distance": 1, "operation": "cut", "participants": ["Nope"]})).is_err()); + assert!(s.execute("solid.extrude", &json!({"sketch": "Slot", "distance": 1, "operation": "cut", "participants": ["Nope"]})).is_err()); } /// A unit-less parameter used as a length means the design's units. @@ -1535,8 +1532,8 @@ fn unitless_parameters_used_as_lengths_take_the_design_units() { let mut s = Session::default(); s.doc_mut().units = "in".into(); run(&mut s, "parameters.add", json!({"name": "w", "expression": "2", "unit": ""})); - run(&mut s, "PrimitiveBox", json!({"length": "w", "width": "w * 1", "height": "10 mm"})); - let m = run(&mut s, "MeasureCommand", json!({})); + run(&mut s, "solid.box", json!({"length": "w", "width": "w * 1", "height": "10 mm"})); + let m = run(&mut s, "inspect.measure", json!({})); let max = |i: usize| m["bodies"][0]["bbox"]["max"][i].as_f64().unwrap_or(0.0); assert!((max(0) - 50.8).abs() < 1e-9 && (max(1) - 50.8).abs() < 1e-9 && (max(2) - 10.0).abs() < 1e-9, "{m}"); } @@ -1545,27 +1542,27 @@ fn unitless_parameters_used_as_lengths_take_the_design_units() { #[test] fn unequal_chamfers() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20})); + run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20})); // Top front edge: 2 along the top (the face facing up), 4 down the front. - run(&mut s, "FusionChamferCommand", json!({"edges": [[20, 0, 20]], "distance": 2, "distance2": 4})); + run(&mut s, "solid.chamfer", json!({"edges": [[20, 0, 20]], "distance": 2, "distance2": 4})); assert!(rel(volume(&mut s), 24000.0 - 0.5 * 2.0 * 4.0 * 40.0) < 1e-6, "{}", volume(&mut s)); // Bottom back edge: 3 along the bottom, at 30° from it (down the back face 3·tan 30°). - run(&mut s, "FusionChamferCommand", json!({"edges": [[20, 30, 0]], "distance": 3, "angle": "30 deg", "flip": true})); + run(&mut s, "solid.chamfer", json!({"edges": [[20, 30, 0]], "distance": 3, "angle": "30 deg", "flip": true})); let d2 = 3.0 * (30f64).to_radians().tan(); assert!(rel(volume(&mut s), 24000.0 - 160.0 - 0.5 * 3.0 * d2 * 40.0) < 1e-6, "{}", volume(&mut s)); - assert!(s.execute("FusionChamferCommand", &json!({"edges": [[0, 15, 20]], "distance": 1, "distance2": 2, "angle": "30 deg"})).is_err()); + assert!(s.execute("solid.chamfer", &json!({"edges": [[0, 15, 20]], "distance": 1, "distance2": 2, "angle": "30 deg"})).is_err()); } /// An enclosure built in the usual order: shell, bosses, then the lip on the rim. #[test] fn lip_after_boss() { 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, "FusionBossCommand", json!({"position": [10, 10, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5})); - run(&mut s, "FusionBossCommand", json!({"position": [70, 40, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5})); + run(&mut s, "solid.box", json!({"length": 80, "width": 50, "height": 30})); + run(&mut s, "solid.shell", json!({"faces": [[40, 25, 30]], "thickness": 2})); + run(&mut s, "plastic.boss", json!({"position": [10, 10, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5})); + run(&mut s, "plastic.boss", json!({"position": [70, 40, 2], "diameter": 7, "height": 12, "hole_diameter": 2.5})); 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})); let v = volume(&mut s); assert!(v > v0 + 1.0, "{v0} {v}"); // Bosses up to the rim, with ribs, against a wall; lips, grooves and outside rims. @@ -1585,12 +1582,12 @@ fn lip_after_boss() { for (i, b) in bosses.iter().enumerate() { for (j, l) in lips.iter().enumerate() { 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})); - let r = s.execute("FusionBossCommand", b); + run(&mut s, "solid.box", json!({"length": 80, "width": 50, "height": 30})); + run(&mut s, "solid.shell", json!({"faces": [[40, 25, 30]], "thickness": 2})); + let r = s.execute("plastic.boss", b); assert!(r.is_ok(), "boss {i}: {r:?}"); let v0 = volume(&mut s); - let r = s.execute("FusionLipCommand", l); + let r = s.execute("plastic.lip", l); assert!(r.is_ok(), "boss {i} lip {j}: {r:?}"); assert!((volume(&mut s) - v0).abs() > 1.0, "boss {i} lip {j}"); } @@ -1601,9 +1598,9 @@ fn lip_after_boss() { #[test] fn hole_as_deep_as_the_plate() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 50, "width": 25, "height": 10})); + run(&mut s, "solid.box", json!({"length": 50, "width": 25, "height": 10})); let v0 = volume(&mut s); - run(&mut s, "FusionHoleCommand", json!({"position": [25, 12.5, 10], "diameter": 6, "depth": 10, "type": "drilled"})); + run(&mut s, "solid.hole", json!({"position": [25, 12.5, 10], "diameter": 6, "depth": 10, "type": "drilled"})); let v = volume(&mut s); assert!(v < v0 - PI * 9.0 * 9.0 && v > v0 - PI * 9.0 * 10.0 - 1.0, "{v0} {v}"); // Other sizes and places, the defaults, and a tapped hole. @@ -1618,7 +1615,7 @@ fn hole_as_deep_as_the_plate() { .enumerate() { let before = volume(&mut s); - let r = s.execute("FusionHoleCommand", &p); + let r = s.execute("solid.hole", &p); assert!(r.is_ok(), "{i} {p}: {r:?}"); assert!(volume(&mut s) < before - 1.0, "{i} {p}"); } diff --git a/crates/engine/src/tests/component_frames.rs b/crates/engine/src/tests/component_frames.rs index 45b0f1d0..081e5a95 100644 --- a/crates/engine/src/tests/component_frames.rs +++ b/crates/engine/src/tests/component_frames.rs @@ -13,21 +13,21 @@ use crate::*; #[test] fn moved_component_picks_and_frames() { let mut s = Session::default(); - run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "body_name": "Base"})); - let c = run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Arm"})); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "Base"})); + let c = run(&mut s, "component.create", json!({"name": "Arm"})); let (cid, occ) = (c["component"].as_u64().unwrap(), c["occurrence"].as_u64().unwrap()); - run(&mut s, "PrimitiveBox", json!({"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"})); + run(&mut s, "solid.box", json!({"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"})); run(&mut s, "component.activate", json!({"component": 0})); run(&mut s, "occurrence.move", json!({"occurrence": occ, "translate": [0, 50, 0]})); let wbox = |s: &Session, b: &str| s.world_state().body(b).unwrap().mesh().bounds(); - let vol = |s: &mut Session, b: &str| run(s, "MeasureCommand", json!({"bodies": [b]}))["total"]["volume_mm3"].as_f64().unwrap(); + let vol = |s: &mut Session, b: &str| run(s, "inspect.measure", json!({"bodies": [b]}))["total"]["volume_mm3"].as_f64().unwrap(); // Root active: a fillet picked at the moved edge in the world. - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[25, 54, 4]], "radius": 1})); + run(&mut s, "solid.fillet", json!({"edges": [[25, 54, 4]], "radius": 1})); assert!(rel(vol(&mut s, "ArmBody"), 160.0 - (1.0 - PI / 4.0) * 10.0) < 1e-4, "{}", vol(&mut s, "ArmBody")); // A sketch on the moved body's top face: its points sit on that face in the world. - let sk = run(&mut s, "SketchCreate", json!({"plane": {"face": [25, 52, 4]}})); - run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 1})); - run(&mut s, "SketchStop", json!({})); + let sk = run(&mut s, "sketch.create", json!({"plane": {"face": [25, 52, 4]}})); + run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 1})); + run(&mut s, "sketch.finish", json!({})); let si = run(&mut s, "sketch.inspect", json!({"sketch": sk["sketch"]})); let z: Vec = si["points"].as_array().unwrap().iter().map(|p| p["world"][2].as_f64().unwrap()).collect(); assert!(z.iter().all(|z| (z - 4.0).abs() < 1e-6), "{si}"); @@ -43,6 +43,6 @@ fn moved_component_picks_and_frames() { let inst = names.iter().find(|n| n.starts_with("ArmBody (")).cloned().unwrap(); let ib = wbox(&s, &inst); // Its bottom edge along x at the far side (y max, z 0). - run(&mut s, "FusionFilletEdgesCommand", json!({"edges": [[25, ib.max.y, 0]], "radius": 1})); + run(&mut s, "solid.fillet", json!({"edges": [[25, ib.max.y, 0]], "radius": 1})); assert!(vol(&mut s, "ArmBody") < v0 - 1.0, "the instance's pick reached the definition"); } diff --git a/crates/mcp/src/server.rs b/crates/mcp/src/server.rs index c96defdb..f9b01d6b 100644 --- a/crates/mcp/src/server.rs +++ b/crates/mcp/src/server.rs @@ -24,11 +24,11 @@ const MAX_LINE: usize = 16 << 20; const MAX_BATCH: usize = 1000; const INSTRUCTIONS: &str = "SolveCraft is a parametric 3D CAD app in the style of Autodesk Fusion. Units are millimetres \ -and degrees in expressions (internally radians), Z is up. Model like in Fusion: `SketchCreate {plane: XY}`, draw \ -(`ShapeRectangleTwoPoint {p0, p1}`, `CircleCenterRadius {center, radius}`, `DrawPolyline {points, closed}`), constrain \ -and dimension (`SketchDimension {entities, value}`; values may name parameters), `SketchStop`, then features \ -(`Extrude {distance, operation?: new|join|cut|intersect}`, `Revolve`, `FusionFilletEdgesCommand {edges: [[x,y,z] point on \ -edge], radius}`, `FusionChamferCommand`, patterns, mirror, primitives). Use `batch` for several steps at once, \ +and degrees in expressions (internally radians), Z is up. Model like in Fusion: `sketch.create {plane: XY}`, draw \ +(`sketch.rectangle.two_point {p0, p1}`, `sketch.circle.center {center, radius}`, `sketch.line {points, closed}`), constrain \ +and dimension (`sketch.dimension {entities, value}`; values may name parameters), `sketch.finish`, then features \ +(`solid.extrude {distance, operation?: new|join|cut|intersect}`, `solid.revolve`, `solid.fillet {edges: [[x,y,z] point on \ +edge], radius}`, `solid.chamfer`, patterns, mirror, primitives). Use `batch` for several steps at once, \ `list_commands` for ids and parameter docs, `inspect_design` / `measure` to verify, `list_edges` to find edge points, \ `set_parameter` to drive the design and `screenshot` to look at it."; diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index ec447d8e..2c102353 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -79,7 +79,7 @@ fn handshake_and_tools_list() { } let cmds = tool(&mut s, "list_commands", json!({"filter": "fillet"})); - assert!(cmds.as_array().unwrap().iter().any(|c| c["id"] == "FusionFilletEdgesCommand"), "{cmds}"); + assert!(cmds.as_array().unwrap().iter().any(|c| c["id"] == "solid.fillet"), "{cmds}"); assert!(cmds.as_array().unwrap().len() < 5); let res = call(&mut s, 5, "resources/list", json!({})); @@ -98,14 +98,14 @@ fn agent_builds_box_fillet_hole() { &mut s, "batch", json!({"commands": [ - {"command": "SketchCreate", "params": {"plane": "XY"}}, - {"command": "ShapeRectangleTwoPoint", "params": {"p0": [0, 0], "p1": [40, 30]}}, - {"command": "SketchStop"}, - {"command": "Extrude", "params": {"distance": 20}}, - {"command": "FusionFilletEdgesCommand", "params": {"edges": [[0, 0, 10]], "radius": 3}}, - {"command": "SketchCreate", "params": {"plane": "XY"}}, - {"command": "CircleCenterRadius", "params": {"center": [20, 15], "radius": 5}}, - {"command": "Extrude", "params": {"distance": 20, "operation": "cut"}}, + {"command": "sketch.create", "params": {"plane": "XY"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}}, + {"command": "sketch.finish"}, + {"command": "solid.extrude", "params": {"distance": 20}}, + {"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": "solid.extrude", "params": {"distance": 20, "operation": "cut"}}, ]}), ); assert_eq!(r["completed"], 8, "{r}"); @@ -169,7 +169,7 @@ fn agent_builds_box_fillet_hole() { fn parameters_drive_the_design() { let mut s = server(); tool(&mut s, "set_parameter", json!({"name": "len", "value": "40 mm"})); - tool(&mut s, "execute", json!({"command": "PrimitiveBox", "params": {"length": "len", "width": 30, "height": "len / 2", "corner": [0, 0, 0]}})); + tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": "len", "width": 30, "height": "len / 2", "corner": [0, 0, 0]}})); assert!(rel(volume(&mut s), 40.0 * 30.0 * 20.0) < 1e-9); let r = tool(&mut s, "set_parameter", json!({"name": "len", "value": 50})); assert_eq!(r["value"], 50.0, "{r}"); @@ -182,20 +182,20 @@ fn parameters_drive_the_design() { #[test] fn batch_stops_at_first_error_and_rolls_back() { let mut s = server(); - tool(&mut s, "execute", json!({"command": "PrimitiveBox", "params": {"length": 10, "width": 10, "height": 10}})); + tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10}})); let r = raw_tool( &mut s, "batch", json!({"commands": [ - {"command": "PrimitiveSphere", "params": {"radius": 3, "center": [50, 0, 0]}}, - {"command": "Extrude", "params": {"distance": "bogus +"}}, - {"command": "PrimitiveSphere", "params": {"radius": 3, "center": [80, 0, 0]}}, + {"command": "solid.sphere", "params": {"radius": 3, "center": [50, 0, 0]}}, + {"command": "solid.extrude", "params": {"distance": "bogus +"}}, + {"command": "solid.sphere", "params": {"radius": 3, "center": [80, 0, 0]}}, ]}), ); assert_eq!(r["result"]["isError"], true, "{r}"); let body: Value = serde_json::from_str(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap(); assert_eq!(body["failed"]["index"], 1, "{body}"); - assert_eq!(body["failed"]["command"], "Extrude"); + assert_eq!(body["failed"]["command"], "solid.extrude"); assert_eq!(body["undone"], 1, "{body}"); assert_eq!(body["results"].as_array().unwrap().len(), 1); // Only the first box is left, and its undo step is intact. @@ -206,7 +206,7 @@ fn batch_stops_at_first_error_and_rolls_back() { let r = raw_tool( &mut s, "batch", - json!({"rollback": false, "commands": [{"command": "PrimitiveSphere", "params": {"radius": 3, "center": [50, 0, 0]}}, {"command": "NoSuchCommand"}]}), + json!({"rollback": false, "commands": [{"command": "solid.sphere", "params": {"radius": 3, "center": [50, 0, 0]}}, {"command": "NoSuchCommand"}]}), ); assert_eq!(r["result"]["isError"], true); assert!(r["result"]["content"][0]["text"].as_str().unwrap().contains("NoSuchCommand")); @@ -225,7 +225,7 @@ fn unknown_tools_and_bad_arguments_are_clean_errors() { assert_eq!(raw_tool(&mut s, "screenshot", json!({"view": "sideways"}))["error"]["code"], -32602); assert_eq!(call(&mut s, 1, "tools/call", json!({"arguments": {}}))["error"]["code"], -32602); // Engine errors are tool errors the model can read. - let r = raw_tool(&mut s, "execute", json!({"command": "Extrude", "params": {"distance": 10}})); + let r = raw_tool(&mut s, "execute", json!({"command": "solid.extrude", "params": {"distance": 10}})); assert_eq!(r["result"]["isError"], true, "{r}"); let r = raw_tool(&mut s, "execute", json!({"command": "NoSuchCommand"})); assert!(r["result"]["content"][0]["text"].as_str().unwrap().contains("unknown command")); @@ -253,7 +253,7 @@ fn hostile_values() -> Vec { json!("1/0"), json!([]), json!([1e308, -1e308]), - json!([{"command": "Extrude", "params": {"distance": 1e308}}]), + json!([{"command": "solid.extrude", "params": {"distance": 1e308}}]), json!([{"command": 7}, null, "x"]), json!({}), json!({"x": 1}), @@ -265,7 +265,7 @@ fn hostile_values() -> Vec { #[test] fn hostile_arguments_never_panic() { let mut s = server(); - tool(&mut s, "execute", json!({"command": "PrimitiveBox", "params": {"length": 10, "width": 10, "height": 10}})); + tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10}})); let defs = tool_definitions(); let mut n = 0; for def in &defs { @@ -295,7 +295,7 @@ fn hostile_arguments_never_panic() { let cmds = tool(&mut s, "list_commands", json!({})); for c in cmds.as_array().unwrap() { let id = c["id"].as_str().unwrap(); - if matches!(id, "doc.open" | "SaveDocumentCommand" | "SaveDocumentAsCommand" | "ExportCommand" | "FusionSaveAsSTLCommand") { + if matches!(id, "doc.open" | "file.save" | "file.save_as" | "file.export" | "file.save_mesh") { continue; } for v in hostile_values() { diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 2611bbf4..c307b284 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -43,8 +43,8 @@ pub fn tool_definitions() -> Vec { "list_commands", "List commands", "The command catalog: id, label, toolbar tab/panel, shortcut, one-line parameter docs and whether the command is \ - enabled right now. Ids are Fusion's command ids where Fusion has the command (Extrude, SketchCreate, \ - FusionFilletEdgesCommand…), dotted ids otherwise (sketch.inspect, timeline.rollback). Optional case-insensitive \ + enabled right now. Ids are dotted and lower-case (solid.extrude, sketch.create, \ + solid.fillet, sketch.inspect, timeline.rollback). Optional case-insensitive \ substring `filter` over id, label, panel and params.", obj(json!({"filter": string("Substring to match, e.g. \"sketch\", \"fillet\", \"pattern\"")}), &[]), true, @@ -54,9 +54,9 @@ pub fn tool_definitions() -> Vec { "execute", "Execute a command", "Run one command with JSON parameters, exactly as the toolbar, palette and scripts do. Never opens a dialog. \ - Examples: SketchCreate {plane: \"XY\"}; ShapeRectangleTwoPoint {p0: [0,0], p1: [40,30]}; CircleCenterRadius \ - {center: [20,15], radius: 5}; SketchDimension {entities: [curve id], value: \"width\"}; SketchStop {}; Extrude \ - {distance: 20, operation: \"cut\"}; FusionFilletEdgesCommand {edges: [[0,0,10]], radius: 3}; PrimitiveBox {length, \ + Examples: sketch.create {plane: \"XY\"}; sketch.rectangle.two_point {p0: [0,0], p1: [40,30]}; sketch.circle.center \ + {center: [20,15], radius: 5}; sketch.dimension {entities: [curve id], value: \"width\"}; sketch.finish {}; solid.extrude \ + {distance: 20, operation: \"cut\"}; solid.fillet {edges: [[0,0,10]], radius: 3}; solid.box {length, \ width, height}. Lengths are mm; expressions may use parameters and units (\"width / 2\", \"30 deg\"). Returns \ the command's result; failures leave the design unchanged.", obj( @@ -192,7 +192,7 @@ pub fn tool_definitions() -> Vec { "Open a design", "Open a .solvecraft design file, a STEP file (.step/.stp: a new design whose bodies come from the file, as an \ Import base feature) or a 3MF/STL mesh (.3mf/.stl: mesh bodies). Replaces the current design; insert into it \ - instead with the FusionImportCommandFromToolbar (STEP) or ParaMeshInsertAlignCommand (3MF/STL) commands.", + instead with the file.insert_step (STEP) or file.insert_mesh (3MF/STL) commands.", obj(json!({"path": string("Path to a .solvecraft, .step/.stp, .3mf or .stl file")}), &["path"]), false, true, @@ -333,7 +333,7 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map) -> Result json!({"bodies": [body]}), None => json!({}), }; - exec(b, "MeasureCommand", p) + exec(b, "inspect.measure", p) } "body_topology" => { let p = pick(a, &["body"]); @@ -344,14 +344,14 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map) -> Result { let mut p = pick(a, &["name", "unit", "comment"]); p["expression"] = a.get("value").cloned().unwrap_or(Value::Null); - exec(b, "ChangeParameterCommand", p) + exec(b, "parameters.change", p) } - "export" => exec(b, "ExportCommand", pick(a, &["path", "format", "bodies"])), - "undo" => exec(b, "UndoCommand", json!({})), - "redo" => exec(b, "RedoCommand", json!({})), - "new_design" => exec(b, "NewDocumentCommand", pick(a, &["name"])), + "export" => exec(b, "file.export", pick(a, &["path", "format", "bodies"])), + "undo" => exec(b, "edit.undo", json!({})), + "redo" => exec(b, "edit.redo", json!({})), + "new_design" => exec(b, "file.new", pick(a, &["name"])), "open" => exec(b, "doc.open", pick(a, &["path"])), - "save" => exec(b, "SaveDocumentCommand", pick(a, &["path"])), + "save" => exec(b, "file.save", pick(a, &["path"])), other => Err(format!("unknown tool `{other}`")), } } @@ -383,7 +383,7 @@ fn batch(b: &mut dyn Backend, a: &Map) -> Value { let mut undone = 0u64; if let (Some(before), Some(after)) = (before, rollback.then(|| undo_depth(b)).flatten()) { for _ in before..after { - if exec(b, "UndoCommand", json!({})).is_err() { + if exec(b, "edit.undo", json!({})).is_err() { break; } undone += 1; diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index d4f9bcb6..ca93c781 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -125,9 +125,7 @@ pub fn folder_items(app: &SolveApp, component: u64, folder: &str) -> Vec { let mut v = Vec::new(); if folder == "bodies" { let bodies: Vec = all.iter().map(|e| e.key.clone()).collect(); - v.push( - Item::action("FusionCreateComponentsFromBodiesCommand", "Create Components from Bodies", "component").with(json!({ "bodies": bodies })), - ); + v.push(Item::action("component.from_bodies", "Create Components from Bodies", "component").with(json!({ "bodies": bodies }))); } v.push( Item::action("ui.newGroup", if folder == "sketches" { "New Sketch Group" } else { "New Group" }, "folder") @@ -578,7 +576,7 @@ fn doc_settings(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) { } let params = format!("Parameters ({})", app.session.doc.params.len()); if draw_row(ui, ui.id().with("params"), &Row { depth: depth + 1, icon: "params", label: ¶ms, ..Default::default() }).clicked { - app.start("ChangeParameterCommand"); + app.start("parameters.change"); } } @@ -1352,7 +1350,7 @@ fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { gizmo(app, ctx, &mut m); if (m.translate, m.angle_deg) != before { // The pending move is the typed offset from where the occurrence is now. - let _ = app.session.execute("AsBuiltPositionsCmd", &json!({})); + let _ = app.session.execute("component.revert_position", &json!({})); let _ = app.run( "occurrence.move", json!({ "occurrence": m.occurrence, "translate": m.translate, "axis": [0, 0, 1], "angle": m.angle_deg.to_radians(), "capture": false }), @@ -1360,15 +1358,15 @@ fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { } match action { Some("capture") => { - let _ = app.run("SnapshotCmd", json!({})); + let _ = app.run("component.capture_position", json!({})); app.tree.occurrence_move = None; } Some(_) => { - let _ = app.run("AsBuiltPositionsCmd", json!({})); + let _ = app.run("component.revert_position", json!({})); app.tree.occurrence_move = None; } None if !open => { - let _ = app.run("AsBuiltPositionsCmd", json!({})); + let _ = app.run("component.revert_position", json!({})); app.tree.occurrence_move = None; } None => app.tree.occurrence_move = Some(m), @@ -1445,7 +1443,7 @@ pub const SAVE: &str = "__save"; /// that capture, revert or keep moving. pub fn needs_capture(app: &SolveApp, what: &str) -> bool { !app.session.pending_moves.is_empty() - && !matches!(what, "SnapshotCmd" | "AsBuiltPositionsCmd" | "occurrence.move" | "UndoCommand" | "RedoCommand") + && !matches!(what, "component.capture_position" | "component.revert_position" | "occurrence.move" | "edit.undo" | "edit.redo") && app.tree.capture_prompt.is_none() } @@ -1454,8 +1452,8 @@ pub fn needs_capture(app: &SolveApp, what: &str) -> bool { pub fn answer_capture(app: &mut SolveApp, action: &str) -> Result<(), String> { let Some(what) = app.tree.capture_prompt.clone() else { return Err("no Capture Position question is open".into()) }; match action { - "capture" => drop(app.run("SnapshotCmd", json!({}))), - "revert" => drop(app.run("AsBuiltPositionsCmd", json!({}))), + "capture" => drop(app.run("component.capture_position", json!({}))), + "revert" => drop(app.run("component.revert_position", json!({}))), "cancel" => { app.tree.capture_prompt = None; return Ok(()); diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs index b37145e7..ab529c4a 100644 --- a/crates/ui-egui/src/context_menu.rs +++ b/crates/ui-egui/src/context_menu.rs @@ -113,8 +113,8 @@ impl Item { fn cmd(app: &SolveApp, id: &str, label: &str) -> Item { let info = solvecraft_engine::find_command(id).map(|c| c.info(&app.session)); let shortcut = match id { - "UndoCommand" => "Ctrl+Z".to_string(), - "RedoCommand" => "Ctrl+Y".to_string(), + "edit.undo" => "Ctrl+Z".to_string(), + "edit.redo" => "Ctrl+Y".to_string(), _ => crate::keymap::effective(app, id).unwrap_or_default(), }; Item { @@ -257,23 +257,23 @@ pub fn radial_items(app: &SolveApp) -> Vec { return vec![ repeat, delete, - cmd(app, "DrawPolyline", "Line"), - cmd(app, "ShapeRectangleTwoPoint", "Rectangle"), + cmd(app, "sketch.line", "Line"), + cmd(app, "sketch.rectangle.two_point", "Rectangle"), act("ui.finishSketch", "Finish Sketch", "finish"), - cmd(app, "CircleCenterRadius", "Circle"), - cmd(app, "UndoCommand", "Undo").on(!app.session.undo.is_empty()), - cmd(app, "SketchDimension", "Sketch Dimension"), + cmd(app, "sketch.circle.center", "Circle"), + cmd(app, "edit.undo", "Undo").on(!app.session.undo.is_empty()), + cmd(app, "sketch.dimension", "Sketch Dimension"), ]; } vec![ repeat, delete, - cmd(app, "FusionPressPullCommand", "Press Pull"), - cmd(app, "FusionHoleCommand", "Hole"), - cmd(app, "SketchCreate", "Create Sketch"), - cmd(app, "Extrude", "Extrude"), - cmd(app, "UndoCommand", "Undo").on(!app.session.undo.is_empty()), - cmd(app, "FusionMoveCommand", "Move/Copy"), + cmd(app, "solid.press_pull", "Press Pull"), + cmd(app, "solid.hole", "Hole"), + cmd(app, "sketch.create", "Create Sketch"), + cmd(app, "solid.extrude", "Extrude"), + cmd(app, "edit.undo", "Undo").on(!app.session.undo.is_empty()), + cmd(app, "solid.move", "Move/Copy"), ] } @@ -330,23 +330,23 @@ fn viewport_items(app: &SolveApp) -> Vec { if app.session.active_sketch.is_some() { v.extend(sketch_entity_items(app)); v.push(Item::sep()); - v.push(cmd(app, "TrimSketchCmd", "Trim")); - v.push(cmd(app, "Offset", "Offset")); + v.push(cmd(app, "sketch.trim", "Trim")); + v.push(cmd(app, "sketch.offset", "Offset")); v.push(Item::sep()); v.push(act("ui.finishSketch", "Finish Sketch", "finish")); v.push(Item::sep()); - v.push(cmd(app, "UndoCommand", "Undo").on(!app.session.undo.is_empty())); - v.push(cmd(app, "RedoCommand", "Redo").on(!app.session.redo.is_empty())); + v.push(cmd(app, "edit.undo", "Undo").on(!app.session.undo.is_empty())); + v.push(cmd(app, "edit.redo", "Redo").on(!app.session.redo.is_empty())); return v; } if has(|s| matches!(s, Sel::Face { .. })) { let face = sel.iter().find_map(|s| if let Sel::Face { body, .. } = s { Some(body.clone()) } else { None }).unwrap_or_default(); let (feature, sketch) = body_feature_and_sketch(app, &face); let hidden = app.ui.hidden_bodies.contains(&face); - v.push(cmd(app, "SketchCreate", "Create Sketch")); - v.push(cmd(app, "Extrude", "Extrude")); - v.push(cmd(app, "ConstructionPlaneOffsetFromPlaneCommand", "Offset Plane")); - v.push(cmd(app, "FusionShellBodyCommand", "Shell")); + v.push(cmd(app, "sketch.create", "Create Sketch")); + v.push(cmd(app, "solid.extrude", "Extrude")); + v.push(cmd(app, "construct.plane.offset", "Offset Plane")); + v.push(cmd(app, "solid.shell", "Shell")); v.push(act("ui.editFeature", "Edit Feature", "").with(json!({ "feature": feature })).on(feature.is_some())); v.push(act("ui.editSketch", "Edit Profile Sketch", "sketch").with(json!({ "sketch": sketch })).on(sketch.is_some())); v.push(Item::sep()); @@ -359,13 +359,13 @@ fn viewport_items(app: &SolveApp) -> Vec { v.push(act("ui.findBrowser", "Find in Browser", "").with(json!({ "body": face }))); v.push(act("ui.findTimeline", "Find in Timeline", "").with(json!({ "body": face }))); v.push(Item::sep()); - v.push(cmd(app, "FusionPressPullCommand", "Press Pull")); - v.push(cmd(app, "MeasureCommand", "Measure")); + v.push(cmd(app, "solid.press_pull", "Press Pull")); + v.push(cmd(app, "inspect.measure", "Measure")); } else if has(|s| matches!(s, Sel::Edge { .. })) { let body = sel.iter().find_map(|s| if let Sel::Edge { body, .. } = s { Some(body.clone()) } else { None }).unwrap_or_default(); let hidden = app.ui.hidden_bodies.contains(&body); - v.push(cmd(app, "FusionFilletEdgesCommand", "Fillet")); - v.push(cmd(app, "FusionChamferCommand", "Chamfer")); + v.push(cmd(app, "solid.fillet", "Fillet")); + v.push(cmd(app, "solid.chamfer", "Chamfer")); v.push(Item::sep()); v.push(act("ui.delete", "Delete", "delete").key("Del")); v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": [body] }))); @@ -373,12 +373,12 @@ fn viewport_items(app: &SolveApp) -> Vec { v.push(act("ui.findBrowser", "Find in Browser", "").with(json!({ "body": body }))); v.push(Item::sep()); v.push(act("ui.tangentChain", "Select Tangent Chain", "")); - v.push(cmd(app, "MeasureCommand", "Measure")); + v.push(cmd(app, "inspect.measure", "Measure")); } else if has(|s| matches!(s, Sel::Body { .. })) { v.extend(body_items(app, &sel_bodies(app), false)); } else if has(|s| matches!(s, Sel::Profile { .. })) { - v.push(cmd(app, "Extrude", "Extrude")); - v.push(cmd(app, "Revolve", "Revolve")); + v.push(cmd(app, "solid.extrude", "Extrude")); + v.push(cmd(app, "solid.revolve", "Revolve")); } else { // Empty space. v.push(act("ui.nav", "Pan", "pan").with(json!({ "mode": "pan" }))); @@ -391,11 +391,11 @@ fn viewport_items(app: &SolveApp) -> Vec { v.push(act("ui.showAll", "Show All", "eye").on(anything_hidden)); v.push(act("ui.showAll", "Unisolate", "").on(!app.ui.hidden_bodies.is_empty())); v.push(Item::sep()); - v.push(cmd(app, "Extrude", "Extrude")); - v.push(cmd(app, "FusionFilletEdgesCommand", "Fillet")); + v.push(cmd(app, "solid.extrude", "Extrude")); + v.push(cmd(app, "solid.fillet", "Fillet")); v.push(Item::sep()); - v.push(cmd(app, "UndoCommand", "Undo").on(!app.session.undo.is_empty())); - v.push(cmd(app, "RedoCommand", "Redo").on(!app.session.redo.is_empty())); + v.push(cmd(app, "edit.undo", "Undo").on(!app.session.undo.is_empty())); + v.push(cmd(app, "edit.redo", "Redo").on(!app.session.redo.is_empty())); v.push(Item::sep()); if app.session.section.is_some() { v.push(act("ui.unsection", "Remove Section", "section")); @@ -418,7 +418,7 @@ fn body_items(app: &SolveApp, bodies: &[String], browser: bool) -> Vec { let items = json!(bodies.iter().map(|b| json!({"type": "body", "name": b})).collect::>()); let mut v = vec![ act("ui.moveBodies", "Move/Copy", "move").key("M").with(json!({ "bodies": names })).on(!locked), - cmd(app, "FusionCreateComponentsFromBodiesCommand", "Create Components from Bodies").with(json!({ "bodies": names })), + cmd(app, "component.from_bodies", "Create Components from Bodies").with(json!({ "bodies": names })), Item::sep(), act("ui.material", "Physical Material", "").with(json!({ "bodies": names })), act("ui.appearance", "Appearance", "").key("A").on(false), @@ -428,7 +428,7 @@ fn body_items(app: &SolveApp, bodies: &[String], browser: bool) -> Vec { act("ui.export", "Export…", "export").with(json!({ "bodies": names })), Item::sep(), act("ui.delete", "Delete", "delete").key("Del").with(json!({ "items": items })).on(!locked), - cmd(app, "SoftDeleteCommand", "Remove").with(json!({ "bodies": names })).on(!locked), + cmd(app, "solid.remove", "Remove").with(json!({ "bodies": names })).on(!locked), act("ui.rename", "Rename", "").with(json!({ "body": bodies.first() })).on(one), Item::sep(), act(if all_hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": names })), @@ -499,19 +499,15 @@ fn component_items(app: &SolveApp, id: u64) -> Vec { v.push(act("ui.activate", "Activate", "component").with(json!({ "component": id })).on(!active)); v.push(cmd(app, "occurrence.ground", if grounded { "Unground" } else { "Ground" }).with(json!({ "occurrence": oid, "grounded": !grounded }))); v.push(act("ui.moveOccurrence", "Move/Copy", "move").key("M").with(json!({ "occurrence": oid })).on(!grounded && occ.is_some())); - v.push(cmd(app, "SnapshotCmd", "Capture Position").on(!app.session.pending_moves.is_empty())); - v.push(cmd(app, "AsBuiltPositionsCmd", "Revert Position").on(!app.session.pending_moves.is_empty())); + v.push(cmd(app, "component.capture_position", "Capture Position").on(!app.session.pending_moves.is_empty())); + v.push(cmd(app, "component.revert_position", "Revert Position").on(!app.session.pending_moves.is_empty())); v.push(Item::sep()); } else { v.push(act("ui.activate", "Activate", "component").with(json!({ "component": id })).on(!active)); } - v.push(cmd(app, "FusionCreateNewComponentCommand", "New Component").with(json!({ "parent": id }))); + v.push(cmd(app, "component.create", "New Component").with(json!({ "parent": id }))); if id == 0 { - v.push( - cmd(app, "FusionCreateComponentsFromBodiesCommand", "Create Components from Bodies") - .with(json!({ "bodies": names })) - .on(!bodies.is_empty()), - ); + v.push(cmd(app, "component.from_bodies", "Create Components from Bodies").with(json!({ "bodies": names })).on(!bodies.is_empty())); } v.push(Item::sep()); v.push(act("ui.material", "Physical Material", "").with(json!({ "bodies": names })).on(!bodies.is_empty())); @@ -568,10 +564,10 @@ fn sketch_entity_items(app: &SolveApp) -> Vec { } let mut v = Vec::new(); if curves.is_empty() && points.is_empty() { - v.push(cmd(app, "DrawPolyline", "Line")); - v.push(cmd(app, "ShapeRectangleTwoPoint", "Rectangle")); - v.push(cmd(app, "CircleCenterRadius", "Circle")); - v.push(cmd(app, "SketchDimension", "Sketch Dimension")); + v.push(cmd(app, "sketch.line", "Line")); + v.push(cmd(app, "sketch.rectangle.two_point", "Rectangle")); + v.push(cmd(app, "sketch.circle.center", "Circle")); + v.push(cmd(app, "sketch.dimension", "Sketch Dimension")); return v; } let ents: Vec = curves.iter().chain(points.iter()).cloned().collect(); @@ -581,41 +577,41 @@ fn sketch_entity_items(app: &SolveApp) -> Vec { } v.push(act("ui.fix", "Fix/Unfix", "c_fix").with(json!({ "entities": ents }))); v.push(act("ui.delete", "Delete", "delete").key("Del")); - v.push(cmd(app, "SketchDimension", "Sketch Dimension").with(json!({ "entities": ents })).on(ents.len() <= 2)); + v.push(cmd(app, "sketch.dimension", "Sketch Dimension").with(json!({ "entities": ents })).on(ents.len() <= 2)); let mut c = Vec::new(); let n = ents.len(); let (l, r, p) = (lines.len(), rounds.len(), points.len()); if l == 1 && n == 1 { - c.push(cmd(app, "ConstraintHorizontalVertical", "Horizontal/Vertical").with(json!({ "line": lines[0] }))); + c.push(cmd(app, "sketch.constraint.horizontal_vertical", "Horizontal/Vertical").with(json!({ "line": lines[0] }))); } if p == 2 && n == 2 { - c.push(cmd(app, "ConstraintCoincident", "Coincident").with(json!({ "a": points[0], "b": points[1] }))); - c.push(cmd(app, "ConstraintHorizontalVertical", "Horizontal/Vertical").with(json!({ "points": points }))); + c.push(cmd(app, "sketch.constraint.coincident", "Coincident").with(json!({ "a": points[0], "b": points[1] }))); + c.push(cmd(app, "sketch.constraint.horizontal_vertical", "Horizontal/Vertical").with(json!({ "points": points }))); } if p == 1 && curves.len() == 1 && n == 2 { - c.push(cmd(app, "ConstraintCoincident", "Coincident").with(json!({ "a": picked[0], "b": picked[1] }))); + c.push(cmd(app, "sketch.constraint.coincident", "Coincident").with(json!({ "a": picked[0], "b": picked[1] }))); if l == 1 { - c.push(cmd(app, "ConstraintMidPoint", "MidPoint").with(json!({ "point": points[0], "line": lines[0] }))); + c.push(cmd(app, "sketch.constraint.midpoint", "MidPoint").with(json!({ "point": points[0], "line": lines[0] }))); } } if l == 2 && n == 2 { let ab = json!({ "a": lines[0], "b": lines[1] }); - c.push(cmd(app, "ConstraintParallel", "Parallel").with(ab.clone())); - c.push(cmd(app, "ConstraintPerpendicular", "Perpendicular").with(ab.clone())); - c.push(cmd(app, "ConstraintCollinear", "Collinear").with(ab.clone())); - c.push(cmd(app, "ConstraintEqual", "Equal").with(ab)); + c.push(cmd(app, "sketch.constraint.parallel", "Parallel").with(ab.clone())); + c.push(cmd(app, "sketch.constraint.perpendicular", "Perpendicular").with(ab.clone())); + c.push(cmd(app, "sketch.constraint.collinear", "Collinear").with(ab.clone())); + c.push(cmd(app, "sketch.constraint.equal", "Equal").with(ab)); } if r == 2 && n == 2 { let ab = json!({ "a": rounds[0], "b": rounds[1] }); - c.push(cmd(app, "ConstraintConcentric", "Concentric").with(ab.clone())); - c.push(cmd(app, "ConstraintEqual", "Equal").with(ab.clone())); - c.push(cmd(app, "ConstraintTangent", "Tangent").with(ab)); + c.push(cmd(app, "sketch.constraint.concentric", "Concentric").with(ab.clone())); + c.push(cmd(app, "sketch.constraint.equal", "Equal").with(ab.clone())); + c.push(cmd(app, "sketch.constraint.tangent", "Tangent").with(ab)); } if l == 1 && r == 1 && n == 2 { - c.push(cmd(app, "ConstraintTangent", "Tangent").with(json!({ "a": picked[0], "b": picked[1] }))); + c.push(cmd(app, "sketch.constraint.tangent", "Tangent").with(json!({ "a": picked[0], "b": picked[1] }))); } if p == 2 && l == 1 && n == 3 { - c.push(cmd(app, "ConstraintSymmetry", "Symmetry").with(json!({ "a": points[0], "b": points[1], "line": lines[0] }))); + c.push(cmd(app, "sketch.constraint.symmetry", "Symmetry").with(json!({ "a": points[0], "b": points[1], "line": lines[0] }))); } if !c.is_empty() { v.push(Item::heading("Constraints")); @@ -678,7 +674,7 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { "ui.moveBodies" => { let items: Vec = bodies.iter().map(|b| json!({"type": "body", "name": b})).collect(); let _ = app.run("select.set", json!({ "items": items })); - app.start("FusionMoveCommand"); + app.start("solid.move"); } "ui.rename" => { let what = if let Some(b) = p.get("body").and_then(Value::as_str) { @@ -700,7 +696,7 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { }; start_rename(app, what, at); } - "ui.properties" => match app.session.execute("MeasureCommand", &json!({ "bodies": bodies })) { + "ui.properties" => match app.session.execute("inspect.measure", &json!({ "bodies": bodies })) { Ok(v) => app.menu.props = Some((if bodies.len() == 1 { bodies.join("") } else { format!("{} bodies", bodies.len()) }, v)), Err(e) => app.set_status(e.to_string(), true), }, @@ -708,7 +704,7 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { let name = bodies.first().cloned().unwrap_or_else(|| app.session.doc.name.clone()); if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f(&format!("{name}.step"), &["step", "stp", "igs", "stl", "3mf", "obj"])) { - let _ = app.run("ExportCommand", json!({ "path": path, "bodies": bodies })); + let _ = app.run("file.export", json!({ "path": path, "bodies": bodies })); } } "ui.findBrowser" => { @@ -728,11 +724,11 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { "ui.tangentChain" => tangent_chain(app), "ui.origin" => app.ui.show_origin = !app.ui.show_origin, "ui.clear" => drop(app.run("select.clear", json!({}))), - "ui.unsection" => drop(app.run("FusionHalfSectionViewCommand", json!({"clear": true}))), + "ui.unsection" => drop(app.run("inspect.section", json!({"clear": true}))), "ui.fit" => app.animate_view("fit"), "ui.home" => app.animate_view("home"), // One command for the whole selection: all fixed unless all already are. - "ui.fix" => drop(app.run("ConstraintFix", json!({ "entities": strs(&p, "entities") }))), + "ui.fix" => drop(app.run("sketch.constraint.fix", json!({ "entities": strs(&p, "entities") }))), "ui.editSketch" => { if let Some(id) = id_of(&p, "sketch") { app.edit_sketch(id); @@ -827,12 +823,12 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { "ui.material" => { let items: Vec = bodies.iter().map(|b| json!({"type": "body", "name": b})).collect(); let _ = app.run("select.set", json!({ "items": items })); - app.start("PhysicalMaterialCommand"); + app.start("material.assign"); } "ui.saveMesh" => { let name = bodies.first().cloned().unwrap_or_else(|| app.session.doc.name.clone()); if let Some(path) = app.services.pick_save.as_ref().and_then(|f| f(&format!("{name}.stl"), &["stl"])) { - let _ = app.run("FusionSaveAsSTLCommand", json!({ "path": path, "bodies": bodies })); + let _ = app.run("file.save_mesh", json!({ "path": path, "bodies": bodies })); } } "ui.exportDxf" => { @@ -848,7 +844,7 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) { let n = app.session.world_state().sketch(id).map_or(0, |s| s.profiles.len()); let items: Vec = (0..n).map(|i| json!({"type": "profile", "sketch": id, "index": i})).collect(); let _ = app.run("select.set", json!({ "items": items })); - app.start("Extrude"); + app.start("solid.extrude"); } } "ui.lookAt" => { @@ -1000,7 +996,7 @@ fn commit_rename(app: &mut SolveApp, r: &Rename) { } } } - RenameWhat::Feature(f) => drop(app.run("FusionRenameTimelineEntryCommand", json!({ "feature": f, "name": name }))), + RenameWhat::Feature(f) => drop(app.run("timeline.rename", json!({ "feature": f, "name": name }))), RenameWhat::Component(c) => drop(app.run("component.rename", json!({ "component": c, "name": name }))), RenameWhat::Group(g) => crate::browser::group_action(app, "ui.renameGroup", &json!({ "group": g, "name": name })), RenameWhat::Canvas(c) => drop(app.run("canvas.edit", json!({ "canvas": c, "name": name }))), diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index cf2c6eb9..347667b8 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -288,8 +288,8 @@ impl Dialog { let s = &app.session; let has_bodies = !s.world_state().bodies.is_empty(); let mut d = match id { - "SketchCreate" => Dialog::new(Kind::Sketch, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)]), - "Extrude" => Dialog::new( + "sketch.create" => Dialog::new(Kind::Sketch, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)]), + "solid.extrude" => Dialog::new( Kind::Extrude { distance: "10 mm".into(), direction: 0, @@ -302,30 +302,30 @@ impl Dialog { }, vec![SelInput::new("Profiles", PROFILES | PLANAR_FACES, true)], ), - "Revolve" => Dialog::new( + "solid.revolve" => Dialog::new( Kind::Revolve { angle: "360 deg".into(), operation: 0 }, vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Axis", AXES, false)], ), - "FusionFilletEdgesCommand" => Dialog::new(fillet_kind("2 mm", false), vec![SelInput::new("Edges", EDGES | FACES, true)]), - "FusionChamferCommand" => Dialog::new(fillet_kind("1 mm", true), vec![SelInput::new("Edges", EDGES | FACES, true)]), - "FusionShellBodyCommand" => Dialog::new(Kind::Shell { thickness: "2 mm".into() }, vec![SelInput::new("Faces/Body", FACES, true)]), - "FusionDraftCommand" => Dialog::new( + "solid.fillet" => Dialog::new(fillet_kind("2 mm", false), vec![SelInput::new("Edges", EDGES | FACES, true)]), + "solid.chamfer" => Dialog::new(fillet_kind("1 mm", true), vec![SelInput::new("Edges", EDGES | FACES, true)]), + "solid.shell" => Dialog::new(Kind::Shell { thickness: "2 mm".into() }, vec![SelInput::new("Faces/Body", FACES, true)]), + "solid.draft" => Dialog::new( Kind::Draft { angle: "5 deg".into() }, vec![SelInput::new("Faces", FACES, true), SelInput::new("Neutral plane", PLANES | PLANAR_FACES, false)], ), - "MirrorCommand" => { + "solid.mirror" => { Dialog::new(Kind::Mirror, vec![SelInput::new("Objects", BODIES, true), SelInput::new("Mirror Plane", PLANES | PLANAR_FACES, false)]) } - "FusionRibCommand" | "FusionWebCommand" => Dialog::new( - Kind::Rib { web: id == "FusionWebCommand", thickness: "2 mm".into(), depth: String::new(), flip: false }, + "solid.rib" | "solid.web" => Dialog::new( + Kind::Rib { web: id == "solid.web", thickness: "2 mm".into(), depth: String::new(), flip: false }, vec![SelInput::new("Curves", CURVES, true)], ), - "EmbossCmd" => Dialog::new(Kind::Emboss { depth: "1 mm".into(), deboss: false }, vec![SelInput::new("Profiles", PROFILES, true)]), - "FusionReplaceFaceCommand" => Dialog::new( + "solid.emboss" => Dialog::new(Kind::Emboss { depth: "1 mm".into(), deboss: false }, vec![SelInput::new("Profiles", PROFILES, true)]), + "solid.replace_face" => Dialog::new( Kind::ReplaceFace, vec![SelInput::new("Faces", PLANAR_FACES, true), SelInput::new("Target", PLANES | PLANAR_FACES, false)], ), - "AlignCmd" => Dialog::new( + "solid.align" => Dialog::new( Kind::Align { flip: false }, vec![ SelInput::new("Bodies", BODIES, true), @@ -333,85 +333,78 @@ impl Dialog { SelInput::new("To", FACES | selection::VERTICES, false), ], ), - "SoftDeleteCommand" => Dialog::new(Kind::Remove, vec![SelInput::new("Bodies", BODIES, true)]), - "PatternOnPath" => Dialog::new( + "solid.remove" => Dialog::new(Kind::Remove, vec![SelInput::new("Bodies", BODIES, true)]), + "solid.pattern.path" => Dialog::new( Kind::PathPattern { count: "4".into(), spacing: "20 mm".into() }, vec![SelInput::new("Objects", BODIES, true), SelInput::new("Path", CURVES, true)], ), - "PrimitivePipe" => Dialog::new(Kind::Pipe { diameter: "5 mm".into(), wall: String::new() }, vec![SelInput::new("Path", CURVES, true)]), - "StockModelCommand" => Dialog::new(Kind::Stock { margin: "2 mm".into() }, vec![SelInput::new("Bodies (all if none)", BODIES, true)]), - "ConstructionPlaneOffsetFromPlaneCommand" => { - Dialog::new(Kind::OffsetPlane { offset: "10 mm".into() }, vec![SelInput::new("Plane", PLANES, false)]) - } - "ConstructionPlaneAtAngleCommand" => Dialog::new( + "solid.pipe" => Dialog::new(Kind::Pipe { diameter: "5 mm".into(), wall: String::new() }, vec![SelInput::new("Path", CURVES, true)]), + "solid.bounding_solid" => Dialog::new(Kind::Stock { margin: "2 mm".into() }, vec![SelInput::new("Bodies (all if none)", BODIES, true)]), + "construct.plane.offset" => Dialog::new(Kind::OffsetPlane { offset: "10 mm".into() }, vec![SelInput::new("Plane", PLANES, false)]), + "construct.plane.at_angle" => Dialog::new( Kind::AnglePlane { angle: "45 deg".into() }, vec![SelInput::new("Plane", PLANES, false), SelInput::new("Axis", AXES, false)], ), - "FusionSplitBodyCommand" => Dialog::new( + "solid.split_body" => Dialog::new( Kind::Split, vec![SelInput::new("Body to split", BODIES, false), SelInput::new("Splitting plane", PLANES | PLANAR_FACES, false)], ), - "ModifyScale" => Dialog::new(Kind::Scale { factor: "2".into() }, vec![SelInput::new("Bodies", BODIES, true)]), - "FusionOffsetFacesCommand" => { - Dialog::new(Kind::OffsetFaces { distance: "2 mm".into() }, vec![SelInput::new("Faces", PLANAR_FACES, true)]) - } - "FusionThreadCommand" => { + "solid.scale" => Dialog::new(Kind::Scale { factor: "2".into() }, vec![SelInput::new("Bodies", BODIES, true)]), + "solid.offset_face" => Dialog::new(Kind::OffsetFaces { distance: "2 mm".into() }, vec![SelInput::new("Faces", PLANAR_FACES, true)]), + "solid.thread" => { Dialog::new(Kind::Thread { designation: String::new(), length: String::new() }, vec![SelInput::new("Cylindrical face", FACES, false)]) } - "PhysicalMaterialCommand" => Dialog::new(Kind::Material { index: 1 }, vec![SelInput::new("Bodies", BODIES, true)]), - "FusionHalfSectionViewCommand" => { + "material.assign" => Dialog::new(Kind::Material { index: 1 }, vec![SelInput::new("Bodies", BODIES, true)]), + "inspect.section" => { Dialog::new(Kind::Section { offset: "0 mm".into(), flip: false }, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)]) } - "MeasureCommand" => { + "inspect.measure" => { Dialog::new(Kind::Measure { result: None, of: Vec::new() }, vec![SelInput::new("Items", FACES | EDGES | selection::VERTICES, true)]) } - "PatternRectangular" => Dialog::new( + "solid.pattern.rectangular" => Dialog::new( Kind::PatternRect { count: "3".into(), spacing: "20 mm".into(), count2: "1".into(), spacing2: "20 mm".into() }, vec![SelInput::new("Objects", BODIES, true), SelInput::new("Direction", AXES, false), SelInput::new("Direction 2", AXES, false)], ), - "PatternCircular" => Dialog::new( + "solid.pattern.circular" => Dialog::new( Kind::PatternCirc { count: "6".into(), angle: "360 deg".into() }, vec![SelInput::new("Objects", BODIES, true), SelInput::new("Axis", AXES, false)], ), - "SolidLoft" => Dialog::new(Kind::Loft { operation: usize::from(has_bodies) }, vec![SelInput::new("Profiles", PROFILES, true)]), - "Sweep" => Dialog::new( + "solid.loft" => Dialog::new(Kind::Loft { operation: usize::from(has_bodies) }, vec![SelInput::new("Profiles", PROFILES, true)]), + "solid.sweep" => Dialog::new( Kind::Sweep { operation: usize::from(has_bodies) }, vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Path", CURVES, true)], ), - "FusionMoveCommand" => Dialog::new(crate::dialogs_move::kind(0), crate::dialogs_move::inputs(0)), - "FusionHoleCommand" => Dialog::new(hole_defaults(), vec![SelInput::new("Face", PLANAR_FACES, true)]), - "PrimitiveBox" => Dialog::new( + "solid.move" => Dialog::new(crate::dialogs_move::kind(0), crate::dialogs_move::inputs(0)), + "solid.hole" => Dialog::new(hole_defaults(), vec![SelInput::new("Face", PLANAR_FACES, true)]), + "solid.box" => Dialog::new( Kind::Primitive { - cmd: "PrimitiveBox", + cmd: "solid.box", fields: vec![("length", "20".into()), ("width", "20".into()), ("height", "20".into())], operation: 0, }, vec![], ), - "PrimitiveCylinder" => Dialog::new( - Kind::Primitive { cmd: "PrimitiveCylinder", fields: vec![("diameter", "20".into()), ("height", "20".into())], operation: 0 }, + "solid.cylinder" => Dialog::new( + Kind::Primitive { cmd: "solid.cylinder", fields: vec![("diameter", "20".into()), ("height", "20".into())], operation: 0 }, vec![], ), - "PrimitiveSphere" => { - Dialog::new(Kind::Primitive { cmd: "PrimitiveSphere", fields: vec![("diameter", "20".into())], operation: 0 }, vec![]) + "solid.sphere" => Dialog::new(Kind::Primitive { cmd: "solid.sphere", fields: vec![("diameter", "20".into())], operation: 0 }, vec![]), + "solid.torus" => { + Dialog::new(Kind::Primitive { cmd: "solid.torus", fields: vec![("major", "20".into()), ("minor", "5".into())], operation: 0 }, vec![]) } - "PrimitiveTorus" => Dialog::new( - Kind::Primitive { cmd: "PrimitiveTorus", fields: vec![("major", "20".into()), ("minor", "5".into())], operation: 0 }, - vec![], - ), - "PrimitiveCoil" => Dialog::new( + "solid.coil" => Dialog::new( Kind::Primitive { - cmd: "PrimitiveCoil", + cmd: "solid.coil", fields: vec![("diameter", "40".into()), ("revolutions", "4".into()), ("pitch", "8".into()), ("section_size", "3".into())], operation: 0, }, vec![], ), - "FusionCombineCommand" => Dialog::new( + "solid.combine" => Dialog::new( Kind::Combine { operation: 1, keep_tools: false }, vec![SelInput::new("Target Body", BODIES, false), SelInput::new("Tool Bodies", BODIES, true)], ), - "ChangeParameterCommand" => Dialog::new(Kind::Params { new_name: String::new(), new_expr: String::new() }, vec![]), + "parameters.change" => Dialog::new(Kind::Params { new_name: String::new(), new_expr: String::new() }, vec![]), _ => { let (kind, inputs) = crate::dialogs_assembly::start(app, id) .or_else(|| crate::dialogs_sheet::start(app, id)) @@ -768,10 +761,10 @@ fn title(k: &Kind) -> &'static str { Kind::Move { .. } => "MOVE", Kind::Hole { .. } => "HOLE", Kind::Primitive { cmd, .. } => match *cmd { - "PrimitiveBox" => "BOX", - "PrimitiveCylinder" => "CYLINDER", - "PrimitiveSphere" => "SPHERE", - "PrimitiveCoil" => "COIL", + "solid.box" => "BOX", + "solid.cylinder" => "CYLINDER", + "solid.sphere" => "SPHERE", + "solid.coil" => "COIL", _ => "TORUS", }, Kind::Combine { .. } => "COMBINE", @@ -1627,7 +1620,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { app.dialog = Some(d); } else if let Some((_, marker)) = d.editing { // Editing done: put the timeline marker back where it was. - let _ = app.run("timeline.rollTo", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); + let _ = app.run("timeline.roll_to", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); } } @@ -1640,7 +1633,7 @@ pub fn cancel(app: &mut SolveApp) { if let Some(d) = app.dialog.take() && let Some((_, marker)) = d.editing { - let _ = app.run("timeline.rollTo", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); + let _ = app.run("timeline.roll_to", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); } } @@ -1681,7 +1674,7 @@ fn params_table(app: &mut SolveApp, ui: &mut egui::Ui, new_name: &mut String, ne ui.data_mut(|dd| dd.insert_temp(id, e.clone())); } if r.lost_focus() && e != p.expr { - let _ = app.run("ChangeParameterCommand", json!({"name": p.name, "expression": e})); + let _ = app.run("parameters.change", json!({"name": p.name, "expression": e})); ui.data_mut(|dd| dd.remove::(id)); } let v = vals.get(&p.name).map(|v| if p.unit == "deg" { format!("{:.3}°", v.v.to_degrees()) } else { format!("{:.4} {}", v.v, p.unit) }); @@ -1698,7 +1691,7 @@ fn params_table(app: &mut SolveApp, ui: &mut egui::Ui, new_name: &mut String, ne ui.add(egui::TextEdit::singleline(new_expr).hint_text("expression, e.g. 25 mm").desired_width(160.0)); if ui.button("Add").clicked() && !new_name.is_empty() - && app.run("ChangeParameterCommand", json!({"name": new_name, "expression": new_expr})).is_ok() + && app.run("parameters.change", json!({"name": new_name, "expression": new_expr})).is_ok() { new_name.clear(); new_expr.clear(); @@ -1715,7 +1708,7 @@ pub fn start_sketch(app: &mut SolveApp, sel: &Sel) { _ => return, }; let before = app.cam; - if app.run("SketchCreate", json!({ "plane": plane })).is_ok() { + if app.run("sketch.create", json!({ "plane": plane })).is_ok() { app.pre_sketch_cam = Some(before); look_at_sketch(app); // On a face: look at the picked point, not the plane's origin (a corner of the face), @@ -1817,11 +1810,11 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S let mut out = Vec::new(); let (sketch, idx) = profiles(); if !sketch.is_null() { - out.push(("Extrude".to_string(), with(json!({"sketch": sketch, "profiles": idx})))); + out.push(("solid.extrude".to_string(), with(json!({"sketch": sketch, "profiles": idx})))); } // Each planar body face extrudes on its own. for p in face_points(0) { - out.push(("Extrude".to_string(), with(json!({ "face": p })))); + out.push(("solid.extrude".to_string(), with(json!({ "face": p })))); } return Ok(out); } @@ -1835,7 +1828,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S }; let (sketch, idx) = profiles(); ( - "Revolve", + "solid.revolve", json!({"sketch": sketch, "profiles": idx, "axis": axis, "angle": angle, "operation": OPS.get(*operation).copied().unwrap_or("new")}), ) } @@ -1879,27 +1872,27 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S if *ctype > 0 && *flip { p["flip"] = json!(true); } - ("FusionChamferCommand", p) + ("solid.chamfer", p) } else { - ("FusionFilletEdgesCommand", json!({"edges": edges, "radius": radius})) + ("solid.fillet", json!({"edges": edges, "radius": radius})) } } Kind::Shell { thickness } => { need(0, "faces to remove")?; - ("FusionShellBodyCommand", json!({"faces": face_points(0), "thickness": thickness})) + ("solid.shell", json!({"faces": face_points(0), "thickness": thickness})) } Kind::Draft { angle } => { need(0, "faces")?; need(1, "the neutral plane")?; let neutral = plane_value(s, sels(d, 1).first()).ok_or("the neutral plane must be a plane or a planar face")?; - ("FusionDraftCommand", json!({"faces": face_points(0), "angle": angle, "neutral": neutral})) + ("solid.draft", json!({"faces": face_points(0), "angle": angle, "neutral": neutral})) } Kind::Mirror => { need(0, "objects")?; need(1, "the mirror plane")?; let features = pattern_features(s, sels(d, 0)); let plane = plane_value(s, sels(d, 1).first()).ok_or("the mirror plane must be a plane or a planar face")?; - ("MirrorCommand", json!({"features": features, "plane": plane})) + ("solid.mirror", json!({"features": features, "plane": plane})) } Kind::PatternRect { count, spacing, count2, spacing2 } => { need(0, "objects")?; @@ -1915,7 +1908,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S p["count2"] = json!(count2); p["spacing2"] = json!(spacing2); } - ("PatternRectangular", p) + ("solid.pattern.rectangular", p) } Kind::PatternCirc { count, angle } => { need(0, "objects")?; @@ -1928,7 +1921,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S } None => return Err("select an axis".into()), }; - ("PatternCircular", json!({"features": pattern_features(s, sels(d, 0)), "axis": axis, "count": count, "angle": angle})) + ("solid.pattern.circular", json!({"features": pattern_features(s, sels(d, 0)), "axis": axis, "count": count, "angle": angle})) } Kind::Loft { operation } => { need(0, "profiles of two or more sketches")?; @@ -1946,7 +1939,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S return Err("pick profiles in two or more sketches".into()); } let sections: Vec = sections.into_iter().map(|(sk, v)| json!({"sketch": sk, "profiles": v})).collect(); - ("SolidLoft", json!({"sections": sections, "operation": OPS.get(*operation).copied().unwrap_or("new")})) + ("solid.loft", json!({"sections": sections, "operation": OPS.get(*operation).copied().unwrap_or("new")})) } Kind::Sweep { operation } => { need(0, "a profile")?; @@ -1955,7 +1948,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S let path: Vec = sels(d, 1).iter().filter_map(|x| if let Sel::SketchCurve { id } = x { Some(id.clone()) } else { None }).collect(); let path_sketch = curves_sketch(app, &path, sketch.as_u64()).ok_or("the path curves must be in one sketch")?; ( - "Sweep", + "solid.sweep", json!({"sketch": sketch, "profiles": idx, "path_sketch": path_sketch, "path": path, "operation": OPS.get(*operation).copied().unwrap_or("new")}), ) } @@ -1965,7 +1958,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S Some(Sel::Plane { name }) => name.clone(), _ => return Err("pick an origin or construction plane".into()), }; - ("ConstructionPlaneOffsetFromPlaneCommand", json!({"base": base, "offset": offset})) + ("construct.plane.offset", json!({"base": base, "offset": offset})) } Kind::AnglePlane { angle } => { need(0, "a plane")?; @@ -1981,7 +1974,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S } None => d.extra.get("axis").cloned().ok_or("pick an axis")?, }; - ("ConstructionPlaneAtAngleCommand", json!({"base": base, "axis": axis, "angle": angle})) + ("construct.plane.at_angle", json!({"base": base, "axis": axis, "angle": angle})) } Kind::Rib { web, thickness, depth, flip } => { need(0, "open sketch curves")?; @@ -1991,12 +1984,12 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S if !depth.trim().is_empty() { p["depth"] = json!(depth); } - (if *web { "FusionWebCommand" } else { "FusionRibCommand" }, p) + (if *web { "solid.web" } else { "solid.rib" }, p) } Kind::Emboss { depth, deboss } => { need(0, "profiles")?; let (sketch, idx) = profiles(); - ("EmbossCmd", json!({"sketch": sketch, "profiles": idx, "depth": depth, "mode": if *deboss { "deboss" } else { "emboss" }})) + ("solid.emboss", json!({"sketch": sketch, "profiles": idx, "depth": depth, "mode": if *deboss { "deboss" } else { "emboss" }})) } Kind::ReplaceFace => { need(0, "faces to move")?; @@ -2007,7 +2000,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S Some(Sel::Face { point, .. }) => p["target_face"] = pt(*point), _ => return Err("pick a plane or planar face".into()), } - ("FusionReplaceFaceCommand", p) + ("solid.replace_face", p) } Kind::Align { flip } => { need(0, "bodies")?; @@ -2021,11 +2014,11 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S _ => return Err("pick faces or vertices".into()), } } - ("AlignCmd", p) + ("solid.align", p) } Kind::Remove => { need(0, "bodies")?; - ("SoftDeleteCommand", json!({"bodies": body_names(0)})) + ("solid.remove", json!({"bodies": body_names(0)})) } Kind::PathPattern { count, spacing } => { need(0, "objects")?; @@ -2038,7 +2031,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S } else { p["bodies"] = json!(body_names(0)); } - ("PatternOnPath", p) + ("solid.pattern.path", p) } Kind::Pipe { diameter, wall } => { need(0, "the path")?; @@ -2048,14 +2041,14 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S if !wall.trim().is_empty() { p["wall"] = json!(wall); } - ("PrimitivePipe", p) + ("solid.pipe", p) } Kind::Stock { margin } => { let mut p = json!({"margin": margin}); if !sels(d, 0).is_empty() { p["bodies"] = json!(body_names(0)); } - ("StockModelCommand", p) + ("solid.bounding_solid", p) } Kind::Split => { need(0, "the body to split")?; @@ -2069,15 +2062,15 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S } _ => return Err("pick a plane or a planar face".into()), }; - ("FusionSplitBodyCommand", json!({"body": body, "plane": plane})) + ("solid.split_body", json!({"body": body, "plane": plane})) } Kind::Scale { factor } => { need(0, "bodies")?; - ("ModifyScale", json!({"bodies": body_names(0), "factor": factor})) + ("solid.scale", json!({"bodies": body_names(0), "factor": factor})) } Kind::OffsetFaces { distance } => { need(0, "planar faces")?; - ("FusionOffsetFacesCommand", json!({"faces": face_points(0), "distance": distance})) + ("solid.offset_face", json!({"faces": face_points(0), "distance": distance})) } Kind::Thread { designation, length } => { need(0, "a cylindrical face")?; @@ -2088,12 +2081,12 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S if !length.trim().is_empty() { p["length"] = json!(length); } - ("FusionThreadCommand", p) + ("solid.thread", p) } Kind::Material { index } => { need(0, "bodies")?; let name = solvecraft_engine::doc::MATERIALS.get(*index).map(|(n, _)| *n).unwrap_or("Default"); - ("PhysicalMaterialCommand", json!({"bodies": body_names(0), "material": name})) + ("material.assign", json!({"bodies": body_names(0), "material": name})) } Kind::Section { offset, flip } => { need(0, "a plane or planar face")?; @@ -2113,9 +2106,9 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S } _ => return Err("pick a plane or a planar face".into()), }; - ("FusionHalfSectionViewCommand", json!({"plane": plane, "offset": offset, "flip": flip})) + ("inspect.section", json!({"plane": plane, "offset": offset, "flip": flip})) } - Kind::Move { .. } => return Ok(vec![("FusionMoveCommand".into(), crate::dialogs_move::params(app, d)?)]), + Kind::Move { .. } => return Ok(vec![("solid.move".into(), crate::dialogs_move::params(app, d)?)]), Kind::Hole { diameter, depth, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } => { need(0, if opts.multiple { "sketch points" } else { "a face position" })?; let ty = match *kind { @@ -2166,7 +2159,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S } _ => {} } - out.push(("FusionHoleCommand".to_string(), params)); + out.push(("solid.hole".to_string(), params)); } return Ok(out); } @@ -2184,13 +2177,13 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, S let target = body_names(0).into_iter().next().unwrap_or_default(); let tools: Vec = body_names(1).into_iter().filter(|n| *n != target).collect(); ( - "FusionCombineCommand", + "solid.combine", json!({"target": target, "tools": tools, "operation": OPS.get(*operation).copied().unwrap_or("join"), "keep_tools": keep_tools}), ) } - Kind::EditParam { name, expr } => ("ChangeParameterCommand", json!({"name": name, "expression": expr})), - Kind::Rename { feature, name } => ("FusionRenameTimelineEntryCommand", json!({"feature": feature, "name": name})), - Kind::ConfirmDelete { feature, .. } => ("FusionDeleteCommand", json!({ "features": [feature.to_string()] })), + Kind::EditParam { name, expr } => ("parameters.change", json!({"name": name, "expression": expr})), + Kind::Rename { feature, name } => ("timeline.rename", json!({"feature": feature, "name": name})), + Kind::ConfirmDelete { feature, .. } => ("timeline.delete", json!({ "features": [feature.to_string()] })), Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::Preferences => return Ok(Vec::new()), Kind::Assembly(k) => return crate::dialogs_assembly::commands(app, k, &d.inputs), Kind::Sheet(k) => return crate::dialogs_sheet::commands(app, k, &d.inputs, &d.extra), @@ -2380,7 +2373,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("Extrude")?; + let mut d = start("solid.extrude")?; d.kind = Kind::Extrude { distance: extent.distance.clone(), direction: match (extent.direction, &extent.distance2) { @@ -2415,7 +2408,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("Revolve")?; + let mut d = start("solid.revolve")?; d.kind = Kind::Revolve { angle: angle.clone(), operation: op_index(operation) }; let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default(); if let Some(inp) = d.inputs.get_mut(0) { @@ -2436,7 +2429,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { let chamfer = matches!(f.kind, FeatureKind::Chamfer { .. }); - let mut d = start(if chamfer { "FusionChamferCommand" } else { "FusionFilletEdgesCommand" })?; + let mut d = start(if chamfer { "solid.chamfer" } else { "solid.fillet" })?; d.kind = fillet_kind(radius, chamfer); if let Kind::Fillet { chain, ctype, distance2, angle, flip, .. } = &mut d.kind { *chain = false; @@ -2458,7 +2451,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionShellBodyCommand")?; + let mut d = start("solid.shell")?; d.kind = Kind::Shell { thickness: thickness.clone() }; if let Some(inp) = d.inputs.first_mut() { inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect(); @@ -2466,7 +2459,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionDraftCommand")?; + let mut d = start("solid.draft")?; d.kind = Kind::Draft { angle: angle.clone() }; if let Some(inp) = d.inputs.get_mut(0) { inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect(); @@ -2478,7 +2471,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionHoleCommand")?; + let mut d = start("solid.hole")?; let mut k = hole_defaults(); if let Kind::Hole { diameter: dia, depth: dep, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = &mut k { *dia = diameter.clone(); @@ -2527,9 +2520,9 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("PrimitiveBox")?; + let mut d = start("solid.box")?; d.kind = Kind::Primitive { - cmd: "PrimitiveBox", + cmd: "solid.box", fields: vec![("length", length.clone()), ("width", width.clone()), ("height", height.clone())], operation: op_index(operation), }; @@ -2537,9 +2530,9 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("PrimitiveCylinder")?; + let mut d = start("solid.cylinder")?; d.kind = Kind::Primitive { - cmd: "PrimitiveCylinder", + cmd: "solid.cylinder", fields: vec![("radius", radius.clone()), ("height", height.clone())], operation: op_index(operation), }; @@ -2548,15 +2541,15 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("PrimitiveSphere")?; - d.kind = Kind::Primitive { cmd: "PrimitiveSphere", fields: vec![("radius", radius.clone())], operation: op_index(operation) }; + let mut d = start("solid.sphere")?; + d.kind = Kind::Primitive { cmd: "solid.sphere", fields: vec![("radius", radius.clone())], operation: op_index(operation) }; d.extra.insert("center".into(), pt3(*center)); d } FeatureKind::Torus { center, major, minor, operation } => { - let mut d = start("PrimitiveTorus")?; + let mut d = start("solid.torus")?; d.kind = Kind::Primitive { - cmd: "PrimitiveTorus", + cmd: "solid.torus", fields: vec![("major", major.clone()), ("minor", minor.clone())], operation: op_index(operation), }; @@ -2564,7 +2557,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionCombineCommand")?; + let mut d = start("solid.combine")?; d.kind = Kind::Combine { operation: op_index(operation).max(1), keep_tools: *keep_tools }; if let Some(inp) = d.inputs.get_mut(0) { inp.items = vec![Sel::Body { name: target.clone() }]; @@ -2575,7 +2568,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("MirrorCommand")?; + let mut d = start("solid.mirror")?; let ids: Vec = features.iter().filter_map(|n| s.doc.find_feature(n).map(|f| f.id)).collect(); // Features that made no body of their own (a hole) were picked as features. let as_features = ids.iter().any(|id| !st.bodies.iter().any(|b| b.feature == *id)); @@ -2598,7 +2591,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option = st.bodies.iter().filter(|b| ids.contains(&b.feature)).map(|b| Sel::Body { name: b.name.clone() }).collect(); let mut d = match pattern { PatternKind::Rectangular { dir1, count1, spacing1, dir2, count2, spacing2 } => { - let mut d = start("PatternRectangular")?; + let mut d = start("solid.pattern.rectangular")?; d.kind = Kind::PatternRect { count: count1.clone(), spacing: spacing1.clone(), @@ -2622,7 +2615,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("PatternCircular")?; + let mut d = start("solid.pattern.circular")?; d.kind = Kind::PatternCirc { count: count.clone(), angle: angle.clone() }; match world_axis(*axis).filter(|_| origin.len() < 1e-9) { Some(name) => { @@ -2653,7 +2646,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("SolidLoft")?; + let mut d = start("solid.loft")?; d.kind = Kind::Loft { operation: op_index(operation) }; if let Some(inp) = d.inputs.first_mut() { inp.items = sections @@ -2670,7 +2663,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("Sweep")?; + let mut d = start("solid.sweep")?; d.kind = Kind::Sweep { operation: op_index(operation) }; let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default(); if let Some(inp) = d.inputs.get_mut(0) { @@ -2685,7 +2678,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("ConstructionPlaneOffsetFromPlaneCommand")?; + let mut d = start("construct.plane.offset")?; d.kind = Kind::OffsetPlane { offset: distance.clone() }; if let Some(inp) = d.inputs.first_mut() { inp.items = plane_sel(s, base).into_iter().collect(); @@ -2693,7 +2686,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("ConstructionPlaneAtAngleCommand")?; + let mut d = start("construct.plane.at_angle")?; d.kind = Kind::AnglePlane { angle: angle.clone() }; if let Some(inp) = d.inputs.first_mut() { inp.items = plane_sel(s, base).into_iter().collect(); @@ -2711,7 +2704,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionSplitBodyCommand")?; + let mut d = start("solid.split_body")?; if let Some(inp) = d.inputs.first_mut() { inp.items = vec![Sel::Body { name: body.clone() }]; } @@ -2721,7 +2714,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("ModifyScale")?; + let mut d = start("solid.scale")?; d.kind = Kind::Scale { factor: factor.clone() }; if let Some(inp) = d.inputs.first_mut() { inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect(); @@ -2733,7 +2726,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionOffsetFacesCommand")?; + let mut d = start("solid.offset_face")?; d.kind = Kind::OffsetFaces { distance: distance.clone() }; if let Some(inp) = d.inputs.first_mut() { inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect(); @@ -2744,7 +2737,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionThreadCommand")?; + let mut d = start("solid.thread")?; d.kind = Kind::Thread { designation: designation.clone().unwrap_or_default(), length: length.clone().unwrap_or_default() }; if let Some(inp) = d.inputs.first_mut() { inp.items = face_sel(s, *face).into_iter().collect(); @@ -2752,7 +2745,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("StockModelCommand")?; + let mut d = start("solid.bounding_solid")?; d.kind = Kind::Stock { margin: margin.clone() }; if let Some(inp) = d.inputs.first_mut() { inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect(); @@ -2760,7 +2753,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("PrimitivePipe")?; + let mut d = start("solid.pipe")?; d.kind = Kind::Pipe { diameter: diameter.clone(), wall: wall.clone().unwrap_or_default() }; if let Some(inp) = d.inputs.first_mut() { inp.items = path.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect(); @@ -2772,7 +2765,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("EmbossCmd")?; + let mut d = start("solid.emboss")?; d.kind = Kind::Emboss { depth: depth.clone(), deboss: *deboss }; let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default(); if let Some(inp) = d.inputs.first_mut() { @@ -2784,7 +2777,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start(if *web { "FusionWebCommand" } else { "FusionRibCommand" })?; + let mut d = start(if *web { "solid.web" } else { "solid.rib" })?; d.kind = Kind::Rib { web: *web, thickness: thickness.clone(), depth: depth.clone().unwrap_or_default(), flip: *flip }; if let Some(inp) = d.inputs.first_mut() { inp.items = curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect(); @@ -2792,7 +2785,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("FusionReplaceFaceCommand")?; + let mut d = start("solid.replace_face")?; if let Some(inp) = d.inputs.first_mut() { inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect(); } @@ -2805,14 +2798,14 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option { - let mut d = start("SoftDeleteCommand")?; + let mut d = start("solid.remove")?; if let Some(inp) = d.inputs.first_mut() { inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect(); } d } FeatureKind::Move { bodies, translate, rotate_axis, angle } => { - let mut d = start("FusionMoveCommand")?; + let mut d = start("solid.move")?; crate::dialogs_move::for_feature(&mut d, bodies, translate, *rotate_axis, angle.as_ref()); d } diff --git a/crates/ui-egui/src/dialogs_appearance.rs b/crates/ui-egui/src/dialogs_appearance.rs index 010450e1..6890658e 100644 --- a/crates/ui-egui/src/dialogs_appearance.rs +++ b/crates/ui-egui/src/dialogs_appearance.rs @@ -107,7 +107,7 @@ pub struct Ap { /// The dialog for the Appearance command. pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { - (id == "AppearanceCommand").then(|| { + (id == "appearance.assign").then(|| { ( Kind::Appearance(Ap { faces: false, color: [196, 199, 204], opacity: 1.0, look: None, category: 0, note: None }), vec![SelInput::new("Objects", BODIES, true)], @@ -151,7 +151,7 @@ fn apply_params(targets: &[Sel], color: [u8; 3], opacity: f32, look: Option<&str /// The commands OK runs: the current look on the picked objects (nothing when none are picked). pub fn commands(k: &Ap, inputs: &[SelInput]) -> Vec<(String, Value)> { let items = inputs.first().map(|i| i.items.as_slice()).unwrap_or(&[]); - apply_params(items, k.color, k.opacity, k.look.as_deref()).map(|p| vec![("AppearanceCommand".to_string(), p)]).unwrap_or_default() + apply_params(items, k.color, k.opacity, k.look.as_deref()).map(|p| vec![("appearance.assign".to_string(), p)]).unwrap_or_default() } /// What a swatch dropped now would land on: the face or body under the cursor in the viewport, @@ -267,7 +267,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel if ui.add_enabled(!picked.is_empty(), egui::Button::new("Apply to selection")).clicked() && let Some(p) = apply_params(&picked, k.color, k.opacity, k.look.as_deref()) { - k.note = Some(match app.run("AppearanceCommand", p) { + k.note = Some(match app.run("appearance.assign", p) { Ok(_) => format!("applied to {} item(s)", picked.len()), Err(e) => e, }); @@ -321,7 +321,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel k.opacity = look.opacity; k.look = (look.category != "In This Design").then(|| look.name.clone()); if let Some(p) = apply_params(&targets, look.color, look.opacity, k.look.as_deref()) { - k.note = Some(match app.run("AppearanceCommand", p) { + k.note = Some(match app.run("appearance.assign", p) { Ok(_) => format!("{} on {}", look.name, target_names(&targets)), Err(e) => e, }); @@ -358,8 +358,8 @@ mod tests { fn app() -> SolveApp { let mut s = Session::default(); - s.execute("PrimitiveBox", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); - s.execute("PrimitiveBox", &json!({"length": 5, "width": 5, "height": 5, "corner": [20, 0, 0]})).unwrap(); + s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10})).unwrap(); + s.execute("solid.box", &json!({"length": 5, "width": 5, "height": 5, "corner": [20, 0, 0]})).unwrap(); SolveApp::new(s, Services::default()) } @@ -377,7 +377,7 @@ mod tests { fn ok_applies_the_look_to_the_objects() { let mut a = app(); a.session.selection = vec![Sel::Body { name: "Body1".into() }, Sel::Body { name: "Body2".into() }]; - a.start("AppearanceCommand"); + a.start("appearance.assign"); let mut d = a.dialog.clone().unwrap(); assert_eq!(d.inputs[0].items.len(), 2); if let Kind::Appearance(k) = &mut d.kind { @@ -385,7 +385,7 @@ mod tests { k.look = Some("Paint - Red".into()); } let c = apply_commands(&a, &d).unwrap(); - assert_eq!(c[0].0, "AppearanceCommand"); + assert_eq!(c[0].0, "appearance.assign"); assert_eq!(c[0].1["color"], "#c42824"); assert_eq!(c[0].1["bodies"], json!(["Body1", "Body2"])); for (id, p) in c { diff --git a/crates/ui-egui/src/dialogs_assembly.rs b/crates/ui-egui/src/dialogs_assembly.rs index 00088733..ed679646 100644 --- a/crates/ui-egui/src/dialogs_assembly.rs +++ b/crates/ui-egui/src/dialogs_assembly.rs @@ -26,16 +26,8 @@ const TYPE_LABELS: [&str; 7] = ["Rigid", "Revolute", "Slider", "Cylindrical", "P /// What a joint origin input accepts (snaps on faces, edges and vertices). const SNAPS: u16 = FACES | EDGES | VERTICES; /// Commands whose effect is where occurrences sit (previewed on the placed model). -const MOVES: [&str; 8] = [ - "JointAssembleCmdNew", - "JointAsBuiltCmd", - "FusionMoveJointsCommand", - "joint.edit", - "joint.limits", - "RigidGroupCmd", - "parts.insert", - "FusionFastenersCommand", -]; +const MOVES: [&str; 8] = + ["joint.create", "joint.as_built", "joint.drive", "joint.edit", "joint.limits", "joint.rigid_group", "parts.insert", "parts.fastener"]; #[derive(Clone, Debug)] pub enum Asm { @@ -148,27 +140,25 @@ impl Asm { pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { let s = &app.session; let (asm, inputs) = match id { - "FusionCreateNewComponentCommand" => { - (Asm::NewComponent { name: String::new(), activate: true }, vec![SelInput::new("Bodies (optional)", BODIES, true)]) - } - "JointAssembleCmdNew" => ( + "component.create" => (Asm::NewComponent { name: String::new(), activate: true }, vec![SelInput::new("Bodies (optional)", BODIES, true)]), + "joint.create" => ( Asm::Joint(Box::new(JointForm::new(false))), vec![SelInput::new("Component 1", SNAPS, false), SelInput::new("Component 2", SNAPS, false)], ), - "JointAsBuiltCmd" => { + "joint.as_built" => { (Asm::Joint(Box::new(JointForm::new(true))), vec![SelInput::new("Components", BODIES, true), SelInput::new("Position", SNAPS, false)]) } - "JointOrigin" => (Asm::JointOrigin { name: String::new(), hover: None }, vec![SelInput::new("Snap", SNAPS, false)]), - "RigidGroupCmd" => (Asm::RigidGroup, vec![SelInput::new("Components", BODIES, true)]), - "FusionMoveJointsCommand" => { + "joint.origin" => (Asm::JointOrigin { name: String::new(), hover: None }, vec![SelInput::new("Snap", SNAPS, false)]), + "joint.rigid_group" => (Asm::RigidGroup, vec![SelInput::new("Components", BODIES, true)]), + "joint.drive" => { let joint = movable(s).first().copied().unwrap_or(0); (Asm::Drive { joint, values: current_values(s, joint), focus: true }, vec![]) } - "FusionMotionRelationshipCommand" => { + "joint.motion_link" => { let b = usize::from(movable(s).len() > 1); (Asm::MotionLink { a: 0, b, ia: 0, ib: 0, ratio: "1".into(), offset: "0".into() }, vec![]) } - "InterferenceCheckCommand" => { + "inspect.interference" => { (Asm::Interference { result: None, overlaps: Vec::new(), of: Vec::new() }, vec![SelInput::new("Bodies (all if none)", BODIES, true)]) } _ => return None, @@ -571,7 +561,7 @@ fn capital(s: &str) -> String { /// Run the check and find the overlapping volumes (world placement) to show in red. fn interference(app: &mut SolveApp, picked: &[Sel]) -> (Value, Vec) { let names: Vec = picked.iter().filter_map(|x| if let Sel::Body { name } = x { Some(name.clone()) } else { None }).collect(); - let r = match app.run("InterferenceCheckCommand", json!({ "bodies": names })) { + let r = match app.run("inspect.interference", json!({ "bodies": names })) { Ok(v) => v, Err(e) => return (json!({ "error": e }), Vec::new()), }; @@ -655,9 +645,9 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result { @@ -685,7 +675,7 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result Result { let mut p = snap(0, "a snap")?.param; if !name.trim().is_empty() { p["name"] = json!(name.trim()); } - cmd("JointOrigin", p) + cmd("joint.origin", p) } Asm::RigidGroup => { let occs = occurrences(s, sel_items(inputs, 0)); if occs.len() < 2 { return Err("select two or more components".into()); } - cmd("RigidGroupCmd", json!({ "occurrences": occs })) + cmd("joint.rigid_group", json!({ "occurrences": occs })) } Asm::Drive { joint, values, .. } => { let j = s.doc.assembly.joints.get(*joint).ok_or("no joint that moves yet")?; - cmd("FusionMoveJointsCommand", json!({"joint": j.id, "values": values})) + cmd("joint.drive", json!({"joint": j.id, "values": values})) } Asm::MotionLink { a, b, ia, ib, ratio, offset } => { let js = &s.doc.assembly.joints; let (Some(ja), Some(jb)) = (js.get(*a), js.get(*b)) else { return Err("needs two joints".into()) }; let num = |e: &str| eval(e, ValueKind::Unitless); - cmd("FusionMotionRelationshipCommand", json!({"a": ja.id, "b": jb.id, "ia": ia, "ib": ib, "ratio": num(ratio)?, "offset": num(offset)?})) + cmd("joint.motion_link", json!({"a": ja.id, "b": jb.id, "ia": ia, "ib": ib, "ratio": num(ratio)?, "offset": num(offset)?})) } // The check runs from its Compute button; OK only closes. Asm::Interference { .. } => Vec::new(), @@ -756,7 +746,7 @@ pub fn animate(app: &SolveApp, k: &Asm, cmds: &mut Vec<(String, Value)>) { .collect(); let drive = match f.editing { Some(id) => Some(id), - None => match cmds.iter_mut().find(|(c, _)| c == "JointAssembleCmdNew") { + None => match cmds.iter_mut().find(|(c, _)| c == "joint.create") { Some((_, p)) => { p["values"] = json!(values); None @@ -766,7 +756,7 @@ pub fn animate(app: &SolveApp, k: &Asm, cmds: &mut Vec<(String, Value)>) { }, }; if let Some(id) = drive { - cmds.push(("FusionMoveJointsCommand".into(), json!({"joint": id, "values": values}))); + cmds.push(("joint.drive".into(), json!({"joint": id, "values": values}))); } } diff --git a/crates/ui-egui/src/dialogs_assembly_tests.rs b/crates/ui-egui/src/dialogs_assembly_tests.rs index 192c7dd0..0785ca91 100644 --- a/crates/ui-egui/src/dialogs_assembly_tests.rs +++ b/crates/ui-egui/src/dialogs_assembly_tests.rs @@ -10,8 +10,8 @@ fn two_boxes() -> SolveApp { let mut s = Session::default(); for (name, x) in [("A", 0), ("B", 30)] { s.execute("component.activate", &json!({"component": "root"})).unwrap(); - s.execute("FusionCreateNewComponentCommand", &json!({ "name": name })).unwrap(); - s.execute("PrimitiveBox", &json!({"length": 20, "width": 20, "height": 10, "corner": [x, 0, 0]})).unwrap(); + s.execute("component.create", &json!({ "name": name })).unwrap(); + s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 10, "corner": [x, 0, 0]})).unwrap(); } s.execute("component.activate", &json!({"component": "root"})).unwrap(); s.execute("occurrence.ground", &json!({"occurrence": "A:1", "grounded": true})).unwrap(); @@ -28,7 +28,7 @@ fn occ(app: &SolveApp, name: &str) -> u64 { #[test] fn joint_from_two_snaps() { let mut app = two_boxes(); - app.start("JointAssembleCmdNew"); + app.start("joint.create"); let mut d = app.dialog.clone().unwrap(); let (fa, fb) = (face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap(), face_sel(&app.session, Vec3::new(40.0, 10.0, 0.0)).unwrap()); assert!(apply_commands(&app, &d).is_err(), "no snaps yet"); @@ -42,7 +42,7 @@ fn joint_from_two_snaps() { } let c = apply_commands(&app, &d).unwrap(); let (id, p) = &c[0]; - assert_eq!(id, "JointAssembleCmdNew"); + assert_eq!(id, "joint.create"); assert_eq!(p["type"], "revolute"); assert_eq!(p["a"]["occurrence"], occ(&app, "A")); assert_eq!(p["b"]["occurrence"], occ(&app, "B")); @@ -70,7 +70,7 @@ fn joint_from_two_snaps() { #[test] fn joint_and_drive_preview_on_the_placed_model() { let mut app = two_boxes(); - app.start("JointAssembleCmdNew"); + app.start("joint.create"); let mut d = app.dialog.clone().unwrap(); d.pick(&app.session, face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap()); d.pick(&app.session, face_sel(&app.session, Vec3::new(40.0, 10.0, 0.0)).unwrap()); @@ -91,20 +91,20 @@ fn joint_and_drive_preview_on_the_placed_model() { for (id, p) in apply_commands(&app, &d).unwrap() { app.run(&id, p).unwrap(); } - app.start("FusionMoveJointsCommand"); + app.start("joint.drive"); let mut d = app.dialog.clone().unwrap(); if let Kind::Assembly(Asm::Drive { values, .. }) = &mut d.kind { assert_eq!(values, &vec!["0 deg".to_string()]); values[0] = "30 deg".into(); } let cmds = apply_commands(&app, &d).unwrap(); - assert_eq!(cmds[0].0, "FusionMoveJointsCommand"); + assert_eq!(cmds[0].0, "joint.drive"); let b = world_preview(&app.session, &cmds, colors).unwrap().unwrap(); assert_eq!(b.replaced, vec!["Body2".to_string()]); // Features preview as before while nothing is placed away from its frame, and on the // placed model once something is (B moved by its joint). - assert!(world_preview(&two_boxes().session, &[("Extrude".into(), json!({}))], colors).is_none()); - assert!(world_preview(&app.session, &[("Extrude".into(), json!({}))], colors).is_some()); + assert!(world_preview(&two_boxes().session, &[("solid.extrude".into(), json!({}))], colors).is_none()); + assert!(world_preview(&app.session, &[("solid.extrude".into(), json!({}))], colors).is_some()); } /// Edit Joint changes type, alignment and limits of the joint in place. @@ -112,11 +112,8 @@ fn joint_and_drive_preview_on_the_placed_model() { fn edit_joint_runs_edit_and_limits() { let mut app = two_boxes(); let (a, b) = (occ(&app, "A"), occ(&app, "B")); - app.run( - "JointAssembleCmdNew", - json!({"type": "slider", "a": {"occurrence": a, "face": [10, 10, 10]}, "b": {"occurrence": b, "face": [40, 10, 0]}}), - ) - .unwrap(); + app.run("joint.create", json!({"type": "slider", "a": {"occurrence": a, "face": [10, 10, 10]}, "b": {"occurrence": b, "face": [40, 10, 0]}})) + .unwrap(); let id = app.session.doc.assembly.joints[0].id; let mut d = edit_joint(&app, id).unwrap(); assert!(d.inputs.is_empty()); @@ -141,19 +138,19 @@ fn edit_joint_runs_edit_and_limits() { fn component_picks_become_occurrences() { let mut app = two_boxes(); app.session.selection = vec![Sel::Body { name: "Body1".into() }, Sel::Body { name: "Body2".into() }]; - app.start("JointAsBuiltCmd"); + app.start("joint.as_built"); let d = app.dialog.clone().unwrap(); let c = apply_commands(&app, &d).unwrap(); assert_eq!(c[0].1["a"], occ(&app, "A")); assert_eq!(c[0].1["b"], occ(&app, "B")); app.session.selection = vec![Sel::Body { name: "Body1".into() }, Sel::Body { name: "Body2".into() }]; - app.start("RigidGroupCmd"); + app.start("joint.rigid_group"); let c = apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap(); assert_eq!(c[0].1["occurrences"], json!([occ(&app, "A"), occ(&app, "B")])); app.dialog = None; - app.start("FusionCreateNewComponentCommand"); + app.start("component.create"); let c = apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap(); - assert_eq!(c[0].0, "FusionCreateNewComponentCommand"); + assert_eq!(c[0].0, "component.create"); } /// Round edges snap to their centre; straight ones are not circles. @@ -180,7 +177,7 @@ fn text_at(out: &egui::FullOutput, text: &str) -> Option { #[test] fn ok_click_survives_a_busy_preview() { let mut app = two_boxes(); - app.start("JointAssembleCmdNew"); + app.start("joint.create"); let mut d = app.dialog.take().unwrap(); d.pick(&app.session, face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap()); d.pick(&app.session, face_sel(&app.session, Vec3::new(40.0, 10.0, 0.0)).unwrap()); @@ -215,7 +212,7 @@ fn ok_click_survives_a_busy_preview() { #[test] fn face_snaps_mate_face_to_face() { let mut app = two_boxes(); - app.start("JointAssembleCmdNew"); + app.start("joint.create"); let mut d = app.dialog.clone().unwrap(); d.pick(&app.session, face_sel(&app.session, Vec3::new(10.0, 10.0, 10.0)).unwrap()); d.pick(&app.session, face_sel(&app.session, Vec3::new(40.0, 10.0, 10.0)).unwrap()); @@ -246,12 +243,9 @@ fn face_snaps_mate_face_to_face() { fn drive_value_takes_the_keyboard() { let mut app = two_boxes(); let (a, b) = (occ(&app, "A"), occ(&app, "B")); - app.run( - "JointAssembleCmdNew", - json!({"type": "revolute", "a": {"occurrence": a, "face": [10, 10, 10]}, "b": {"occurrence": b, "face": [40, 10, 0]}}), - ) - .unwrap(); - app.start("FusionMoveJointsCommand"); + app.run("joint.create", json!({"type": "revolute", "a": {"occurrence": a, "face": [10, 10, 10]}, "b": {"occurrence": b, "face": [40, 10, 0]}})) + .unwrap(); + app.start("joint.drive"); let ctx = egui::Context::default(); let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0)); let frame = |app: &mut SolveApp, events: Vec| { diff --git a/crates/ui-egui/src/dialogs_motion.rs b/crates/ui-egui/src/dialogs_motion.rs index 04ee1461..02d186f0 100644 --- a/crates/ui-egui/src/dialogs_motion.rs +++ b/crates/ui-egui/src/dialogs_motion.rs @@ -92,8 +92,8 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { (1..).map(|k| format!("{prefix}{k}")).find(|c| !taken.contains(c)).unwrap_or_default() }; let (mo, inputs) = match id { - "ContactSetCmd" => (Mo::ContactSet { name: n("Contact Set", "contact.list"), check: None }, vec![SelInput::new("Components", BODIES, true)]), - "FusionMotionStudyCommand" => ( + "contact.create" => (Mo::ContactSet { name: n("Contact Set", "contact.list"), check: None }, vec![SelInput::new("Components", BODIES, true)]), + "motion.study" => ( Mo::MotionStudy { name: n("Motion Study", "motion.list"), study: None, @@ -108,7 +108,7 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { vec![], ), "explode.create" => (Mo::Explode { name: n("Exploded View", "explode.list"), scale: 1.0, shown: None, saved: None, error: None }, vec![]), - "FusionShowDesignConfigPanelCmd" | "FusionStartDesignConfigModeCmd" => (Mo::Configurations { new_row: String::new(), error: None }, vec![]), + "config.table" | "config.configure" => (Mo::Configurations { new_row: String::new(), error: None }, vec![]), _ => return None, }; Some((Kind::Motion(mo), inputs)) @@ -240,7 +240,7 @@ fn moved(before: &Session, after: &Session) -> BTreeMap { fn study_pose(app: &SolveApp, params: &Value, step: u32) -> Result, String> { let mut sc = app.session.scratch(); let name = params["study"].as_str().or(params["name"].as_str()).unwrap_or_default().to_string(); - sc.execute("FusionMotionStudyCommand", params).map_err(|e| e.to_string())?; + sc.execute("motion.study", params).map_err(|e| e.to_string())?; sc.execute("motion.play", &json!({"study": name, "step": step})).map_err(|e| e.to_string())?; Ok(moved(&app.session, &sc)) } @@ -272,7 +272,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel row_label(ui, "Contact"); let mut on = list.get("enabled").and_then(Value::as_bool).unwrap_or(false); if ui.checkbox(&mut on, "enabled").changed() { - let _ = app.run("EnableContactSetsCmd", json!({ "enabled": on })); + let _ = app.run("contact.enable_sets", json!({ "enabled": on })); } ui.end_row(); for s in list.get("sets").and_then(Value::as_array).cloned().unwrap_or_default() { @@ -471,7 +471,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel } } Mo::Configurations { new_row, error } => { - let v = app.session.execute("FusionShowDesignConfigPanelCmd", &json!({})).unwrap_or(Value::Null); + let v = app.session.execute("config.table", &json!({})).unwrap_or(Value::Null); let mut tb = table(&v); // Bare numbers show with their parameter's unit ("40" is 40 mm), as Fusion shows them. let kinds = app.session.doc.all_param_exprs(); @@ -579,7 +579,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Mo, inputs: &mut [Sel // The first column starts the table (with a Default row of the current values). let (id, p) = if tb.columns.is_empty() { ( - "FusionStartDesignConfigModeCmd", + "config.configure", match p.get("param") { Some(n) => json!({ "params": [n] }), None => json!({ "features": [p.get("feature")] }), @@ -651,10 +651,10 @@ pub fn commands(app: &SolveApp, k: &Mo, inputs: &[SelInput]) -> Result { - vec![("FusionMotionStudyCommand".into(), study_params(&app.session, name, study.as_deref(), steps, tracks)?)] + vec![("motion.study".into(), study_params(&app.session, name, study.as_deref(), steps, tracks)?)] } Mo::Explode { name, scale, .. } => { vec![("explode.create".into(), json!({"name": name, "scale": scale})), ("explode.show".into(), json!({ "view": name }))] @@ -723,11 +723,11 @@ mod tests { let mut s = Session::default(); for (name, x) in [("A", 0), ("B", 30)] { s.execute("component.activate", &json!({"component": "root"})).unwrap(); - s.execute("FusionCreateNewComponentCommand", &json!({ "name": name })).unwrap(); - s.execute("PrimitiveBox", &json!({"length": 20, "width": 20, "height": 10, "corner": [x, 0, 0]})).unwrap(); + s.execute("component.create", &json!({ "name": name })).unwrap(); + s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 10, "corner": [x, 0, 0]})).unwrap(); } s.execute( - "JointAssembleCmdNew", + "joint.create", &json!({"type": "revolute", "a": {"occurrence": "A:1", "face": [10, 10, 10]}, "b": {"occurrence": "B:1", "face": [40, 10, 0]}}), ) .unwrap(); diff --git a/crates/ui-egui/src/dialogs_move.rs b/crates/ui-egui/src/dialogs_move.rs index 62d7eadd..6d916c23 100644 --- a/crates/ui-egui/src/dialogs_move.rs +++ b/crates/ui-egui/src/dialogs_move.rs @@ -1,5 +1,5 @@ //! Move/Copy: the move types (Free Move, Translate, Rotate, Point to Point, Point to Position), -//! their rows and inputs, the triad on the canvas and the FusionMoveCommand they make. Nothing +//! their rows and inputs, the triad on the canvas and the solid.move they make. Nothing //! moves until a value is typed or the triad is dragged. use serde_json::{Value, json}; @@ -165,7 +165,7 @@ fn deg(v: f64) -> String { format!("{} deg", if s == "-0" || s.is_empty() { "0" } else { s }) } -/// The FusionMoveCommand parameters (bodies, translate, axis, angle). +/// The solid.move parameters (bodies, translate, axis, angle). pub fn params(app: &SolveApp, d: &Dialog) -> Result { let Kind::Move { mode, x, y, z, rx, ry, rz, angle } = &d.kind else { return Err("not a move".into()) }; let bodies = body_names(d); diff --git a/crates/ui-egui/src/dialogs_parts.rs b/crates/ui-egui/src/dialogs_parts.rs index a88485f6..75629cab 100644 --- a/crates/ui-egui/src/dialogs_parts.rs +++ b/crates/ui-egui/src/dialogs_parts.rs @@ -64,7 +64,7 @@ pub struct Pt { /// The dialog for Insert Part. pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { - matches!(id, "FusionFastenersCommand" | "parts.insert").then(|| { + matches!(id, "parts.fastener" | "parts.insert").then(|| { ( Kind::Part(Pt { family: 0, size: 0, length: None, name: String::new(), picture: None, lib: None }), vec![SelInput::new("Hole (optional)", FACES | EDGES, false)], @@ -207,7 +207,7 @@ pub fn commands(app: &SolveApp, k: &Pt, inputs: &[SelInput]) -> Result Pl { /// The dialog for a plastic command. pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { let (pl, inputs) = match id { - "FusionBossCommand" => (boss(), vec![SelInput::new("Position", PLANAR_FACES, false)]), - "FusionLipCommand" => (lip(false), vec![SelInput::new("Rim face", FACES, false)]), - "FusionSnapFitCommand" => (snap_fit(), vec![SelInput::new("Position", PLANAR_FACES, false)]), - "FusionRestCommand" => (rest(), vec![SelInput::new("Position", PLANAR_FACES, false)]), - "FusionManagePlasticRuleCommand" => { + "plastic.boss" => (boss(), vec![SelInput::new("Position", PLANAR_FACES, false)]), + "plastic.lip" => (lip(false), vec![SelInput::new("Rim face", FACES, false)]), + "plastic.snap_fit" => (snap_fit(), vec![SelInput::new("Position", PLANAR_FACES, false)]), + "plastic.rest" => (rest(), vec![SelInput::new("Position", PLANAR_FACES, false)]), + "plastic.manage_rules" => { let rules = app.session.doc.plastic_rules(); let pick = app.session.doc.plastic.active.as_ref().and_then(|a| rules.iter().position(|r| &r.name == a)).unwrap_or(0); (Pl::Rules { pick, name: String::new(), values: Vec::new(), active: false, loaded: None }, vec![]) } - "FusionAssignPlasticRuleCommand" => (Pl::Assign { rule: 1 }, vec![SelInput::new("Bodies", BODIES, true)]), + "plastic.assign_rule" => (Pl::Assign { rule: 1 }, vec![SelInput::new("Bodies", BODIES, true)]), _ => return None, }; Some((Kind::Plastic(pl), inputs)) @@ -332,7 +332,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map= 1.0) { put(&mut p, &[("ribs", ribs), ("rib_thickness", rib_thickness), ("rib_length", rib_length), ("rib_offset", rib_offset)]); } - ("FusionBossCommand", p) + ("plastic.boss", p) } Pl::Lip { groove, width, height, gap, outside } => { let (body, at) = face.clone().ok_or("select the rim face first")?; @@ -341,7 +341,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map { let mut p = placed("a point on a face")?; @@ -356,7 +356,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map { let mut p = placed("a point on a face")?; @@ -366,7 +366,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map { let rules = app.session.doc.plastic_rules(); @@ -386,7 +386,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map { let bodies: Vec = inputs @@ -399,7 +399,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map Option<(Kind, Vec { if let Some(b) = body { extra.insert("body".into(), json!(b)); } - (Pl::Lip { groove: *groove, width: width.clone(), height: height.clone(), gap: s(gap), outside: *outside }, "FusionLipCommand", *face) + (Pl::Lip { groove: *groove, width: width.clone(), height: height.clone(), gap: s(gap), outside: *outside }, "plastic.lip", *face) } FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => { extra.insert("direction".into(), pt(*direction)); @@ -490,7 +490,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec { extra.insert("direction".into(), pt(*direction)); @@ -508,7 +508,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec return None, }; diff --git a/crates/ui-egui/src/dialogs_plastic_tests.rs b/crates/ui-egui/src/dialogs_plastic_tests.rs index 5bbb4ad9..fae6e6b2 100644 --- a/crates/ui-egui/src/dialogs_plastic_tests.rs +++ b/crates/ui-egui/src/dialogs_plastic_tests.rs @@ -23,9 +23,9 @@ fn run_all(app: &mut SolveApp, c: Vec<(String, Value)>) { /// Edit Feature keeps the direction and refills the values. #[test] fn boss_from_a_face_pick() { - let mut app = app_with(&[("PrimitiveBox", json!({"length": 60, "width": 60, "height": 3}))]); + let mut app = app_with(&[("solid.box", json!({"length": 60, "width": 60, "height": 3}))]); app.session.selection = face_sel(&app.session, Vec3::new(30.0, 30.0, 3.0)).into_iter().collect(); - app.start("FusionBossCommand"); + app.start("plastic.boss"); let mut d = app.dialog.clone().unwrap(); assert_eq!(d.primary().map(|p| p.0), Some("Height")); let c = apply_commands(&app, &d).unwrap(); @@ -57,11 +57,11 @@ fn snap_lip_rest() { assert!(dirs.iter().all(|(_, d)| d.dot(n).abs() < 1e-12 && (d.len() - 1.0).abs() < 1e-12)); } let mut app = app_with(&[ - ("PrimitiveBox", json!({"length": 80, "width": 50, "height": 30})), - ("FusionShellBodyCommand", json!({"faces": [[40, 25, 30]], "thickness": 2})), + ("solid.box", json!({"length": 80, "width": 50, "height": 30})), + ("solid.shell", json!({"faces": [[40, 25, 30]], "thickness": 2})), ]); app.session.selection = face_sel(&app.session, Vec3::new(1.0, 25.0, 30.0)).into_iter().collect(); - app.start("FusionLipCommand"); + app.start("plastic.lip"); let mut d = app.dialog.clone().unwrap(); if let Kind::Plastic(Pl::Lip { groove, gap, .. }) = &mut d.kind { *groove = true; @@ -71,15 +71,15 @@ fn snap_lip_rest() { assert_eq!(c[0].1["type"], "groove"); assert_eq!(c[0].1["gap"], "0.2 mm"); run_all(&mut app, c); - let mut app = app_with(&[("PrimitiveBox", json!({"length": 40, "width": 40, "height": 4}))]); + let mut app = app_with(&[("solid.box", json!({"length": 40, "width": 40, "height": 4}))]); app.session.selection = face_sel(&app.session, Vec3::new(20.0, 20.0, 4.0)).into_iter().collect(); - app.start("FusionSnapFitCommand"); + app.start("plastic.snap_fit"); let c = apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap(); assert_eq!(c[0].1["hook"], json!([1.0, 0.0, 0.0])); run_all(&mut app, c); app.dialog = None; app.session.selection = face_sel(&app.session, Vec3::new(5.0, 5.0, 4.0)).into_iter().collect(); - app.start("FusionRestCommand"); + app.start("plastic.rest"); let mut d = app.dialog.clone().unwrap(); assert!(apply_commands(&app, &d).unwrap()[0].1.get("length").is_none(), "round"); if let Kind::Plastic(Pl::Rest { round, .. }) = &mut d.kind { @@ -91,12 +91,12 @@ fn snap_lip_rest() { /// The rule manager edits a library rule into the design; Assign gives bodies a rule. #[test] fn plastic_rules_manage_and_assign() { - let mut app = app_with(&[("PrimitiveBox", json!({"length": 10, "width": 10, "height": 10}))]); - app.start("FusionManagePlasticRuleCommand"); + let mut app = app_with(&[("solid.box", json!({"length": 10, "width": 10, "height": 10}))]); + app.start("plastic.manage_rules"); let d = app.dialog.clone().unwrap(); assert!(apply_commands(&app, &d).is_ok()); app.session.selection = vec![Sel::Body { name: "Body1".into() }]; - app.start("FusionAssignPlasticRuleCommand"); + app.start("plastic.assign_rule"); let c = apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap(); assert_eq!(c[0].1["bodies"], json!(["Body1"])); assert_eq!(c[0].1["rule"], "ABS (1.5mm)"); diff --git a/crates/ui-egui/src/dialogs_sheet.rs b/crates/ui-egui/src/dialogs_sheet.rs index d346379d..e6536edb 100644 --- a/crates/ui-egui/src/dialogs_sheet.rs +++ b/crates/ui-egui/src/dialogs_sheet.rs @@ -131,26 +131,22 @@ fn new_flange(ty: usize) -> Sm { /// The dialog for a sheet metal command. pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec)> { let (sm, inputs) = match id { - "FusionSheetMetalFlangeCommand" => (new_flange(0), vec![SelInput::new("Profile / Edges", PROFILES | EDGES | CURVES, true)]), - "FusionSheetMetalHemFlangeCommand" => { - (Sm::Hem { length: "5 mm".into(), gap: String::new(), flip: false }, vec![SelInput::new("Edges", EDGES, true)]) - } - "SheetMetalFoldCmd" => ( + "sheet.flange" => (new_flange(0), vec![SelInput::new("Profile / Edges", PROFILES | EDGES | CURVES, true)]), + "sheet.hem" => (Sm::Hem { length: "5 mm".into(), gap: String::new(), flip: false }, vec![SelInput::new("Edges", EDGES, true)]), + "sheet.fold" => ( Sm::Fold { angle: "90 deg".into(), radius: String::new(), position: 0, flip: false }, vec![SelInput::new("Bend Line", AXES, false), SelInput::new("Stationary Side", PLANAR_FACES, false)], ), - "FusionSheetmetalUnfoldCommand" => (Sm::Unfold { refold: false }, vec![SelInput::new("Body (last if none)", BODIES, false)]), + "sheet.unfold" => (Sm::Unfold { refold: false }, vec![SelInput::new("Body (last if none)", BODIES, false)]), "sheet.refold" => (Sm::Unfold { refold: true }, vec![SelInput::new("Body (last if none)", BODIES, false)]), - "ConvertToSheetMetalCmd" => (Sm::Convert { rule: 0 }, vec![SelInput::new("Face", PLANAR_FACES, false)]), - "FusionSheetMetalRulesCommand" => { + "sheet.convert" => (Sm::Convert { rule: 0 }, vec![SelInput::new("Face", PLANAR_FACES, false)]), + "sheet.manage_rules" => { let active = app.session.doc.sheet.active.clone(); let rules = rule_list(app); let pick = active.and_then(|a| rules.iter().position(|r| r.name == a)).unwrap_or(0); (Sm::Rules { pick, name: String::new(), values: Vec::new(), active: true, loaded: None }, vec![]) } - "FusionSheetMetalFlatPatternCmd" => { - (Sm::FlatPattern { data: None, key: None, status: None }, vec![SelInput::new("Body (last if none)", BODIES, false)]) - } + "sheet.flat_pattern" => (Sm::FlatPattern { data: None, key: None, status: None }, vec![SelInput::new("Body (last if none)", BODIES, false)]), _ => return None, }; Some((Kind::Sheet(sm), inputs)) @@ -307,7 +303,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel row_label(ui, ""); if ui.button("Delete rule").clicked() { let n = rules.get(*pick).map(|r| r.name.clone()).unwrap_or_default(); - match app.run("FusionSheetMetalRulesCommand", json!({"name": n, "delete": true})) { + match app.run("sheet.manage_rules", json!({"name": n, "delete": true})) { Ok(_) => { *pick = 0; *loaded = None; @@ -326,7 +322,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Sm, inputs: &mut [Sel if let Some(b) = &body { p["body"] = json!(b); } - *data = Some(app.session.execute("FusionSheetMetalFlatPatternCmd", &p).map_err(|e| e.to_string())); + *data = Some(app.session.execute("sheet.flat_pattern", &p).map_err(|e| e.to_string())); *key = Some(want); } match data { @@ -458,7 +454,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map { let edges = edge_points(inputs); @@ -469,7 +465,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map { let mut p = json!({"angle": angle, "position": FOLD_POSITIONS.get(*position).copied().unwrap_or("centerline"), "flip": flip}); @@ -496,14 +492,14 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map { let mut p = json!({}); if let Some(Sel::Body { name }) = items(inputs, 0).first() { p["body"] = json!(name); } - (if *refold { "sheet.refold" } else { "FusionSheetmetalUnfoldCommand" }, p) + (if *refold { "sheet.refold" } else { "sheet.unfold" }, p) } Sm::Convert { rule } => { let Some(Sel::Face { body, point, .. }) = items(inputs, 0).first() else { return Err("select the plate's large face first".into()) }; @@ -511,7 +507,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map { let rules = rule_list(app); @@ -528,7 +524,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map return Ok(Vec::new()), }; @@ -602,7 +598,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec { if let Some(i) = ins.first_mut() { @@ -624,11 +620,11 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec return None, }; let id = match &k { - Sm::Flange { .. } => "FusionSheetMetalFlangeCommand", - Sm::Hem { .. } => "FusionSheetMetalHemFlangeCommand", - Sm::Unfold { refold: false } => "FusionSheetmetalUnfoldCommand", + Sm::Flange { .. } => "sheet.flange", + Sm::Hem { .. } => "sheet.hem", + Sm::Unfold { refold: false } => "sheet.unfold", Sm::Unfold { .. } => "sheet.refold", - _ => "ConvertToSheetMetalCmd", + _ => "sheet.convert", }; let (_, mut inputs) = start(app, id)?; if let Some(i) = inputs.first_mut() { diff --git a/crates/ui-egui/src/dialogs_sheet_tests.rs b/crates/ui-egui/src/dialogs_sheet_tests.rs index 30a1c138..e4653ee6 100644 --- a/crates/ui-egui/src/dialogs_sheet_tests.rs +++ b/crates/ui-egui/src/dialogs_sheet_tests.rs @@ -8,9 +8,9 @@ use crate::dialogs::{apply_commands, for_feature as edit_dialog}; /// A 100 x 60 base flange on XY (2.5 mm steel). fn plate() -> SolveApp { let mut s = Session::default(); - s.execute("SketchCreate", &json!({"plane": "XY", "name": "Base"})).unwrap(); - s.execute("ShapeRectangleTwoPoint", &json!({"p0": [0, 0], "p1": [100, 60]})).unwrap(); - s.execute("SketchStop", &json!({})).unwrap(); + s.execute("sketch.create", &json!({"plane": "XY", "name": "Base"})).unwrap(); + s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [100, 60]})).unwrap(); + s.execute("sketch.finish", &json!({})).unwrap(); SolveApp::new(s, Services::default()) } @@ -27,7 +27,7 @@ fn flange_follows_the_picks() { let mut app = plate(); let sk = app.session.doc.features[0].id; app.session.selection = vec![Sel::Profile { sketch: sk, index: 0 }]; - app.start("FusionSheetMetalFlangeCommand"); + app.start("sheet.flange"); let d = app.dialog.clone().unwrap(); let c = apply_commands(&app, &d).unwrap(); assert_eq!(c[0].1["type"], "base"); @@ -41,7 +41,7 @@ fn flange_follows_the_picks() { .collect(); assert_eq!(edges.len(), 4); app.session.selection = edges; - app.start("FusionSheetMetalFlangeCommand"); + app.start("sheet.flange"); let mut d = app.dialog.clone().unwrap(); if let Kind::Sheet(Sm::Flange { height, position, .. }) = &mut d.kind { *height = "20 mm".into(); @@ -63,27 +63,27 @@ fn flange_follows_the_picks() { assert_eq!(c[0].0, "timeline.redefine"); assert_eq!(c[0].1["params"]["height"], "20 mm"); // Flat pattern: four bends; Unfold previews. - let flat = app.session.execute("FusionSheetMetalFlatPatternCmd", &json!({})).unwrap(); + let flat = app.session.execute("sheet.flat_pattern", &json!({})).unwrap(); assert_eq!(flat["bends"].as_array().map(Vec::len), Some(4), "{flat}"); app.dialog = None; - app.start("FusionSheetmetalUnfoldCommand"); - assert_eq!(apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap()[0].0, "FusionSheetmetalUnfoldCommand"); + app.start("sheet.unfold"); + assert_eq!(apply_commands(&app, app.dialog.as_ref().unwrap()).unwrap()[0].0, "sheet.unfold"); } /// Hem takes edges and an optional gap; rules make and pick a rule with its expressions. #[test] fn hem_and_rules() { let mut app = plate(); - app.run("FusionSheetMetalFlangeCommand", json!({"sketch": "Base"})).unwrap(); + app.run("sheet.flange", json!({"sketch": "Base"})).unwrap(); app.session.selection = edge_sel(&app.session, Vec3::new(50.0, 0.0, 2.5)).into_iter().collect(); - app.start("FusionSheetMetalHemFlangeCommand"); + app.start("sheet.hem"); let d = app.dialog.clone().unwrap(); let c = apply_commands(&app, &d).unwrap(); assert_eq!(c[0].1["length"], "5 mm"); assert!(c[0].1.get("gap").is_none(), "the rule's gap"); run_all(&mut app, c); app.dialog = None; - app.start("FusionSheetMetalRulesCommand"); + app.start("sheet.manage_rules"); let mut d = app.dialog.clone().unwrap(); if let Kind::Sheet(Sm::Rules { pick, name, values, loaded, .. }) = &mut d.kind { *pick = 1; diff --git a/crates/ui-egui/src/dim_view.rs b/crates/ui-egui/src/dim_view.rs index 32cf9794..66382a4c 100644 --- a/crates/ui-egui/src/dim_view.rs +++ b/crates/ui-egui/src/dim_view.rs @@ -7,7 +7,7 @@ //! A click selects a dimension (Delete removes it), a drag moves its text (and so the offset of //! its dimension line), a double-click edits its value in place with parameter completion. //! Everything acts through commands: `sketch.dimension_text`, `sketch.delete`, -//! `ChangeParameterCommand`. +//! `parameters.change`. use std::cell::RefCell; @@ -321,7 +321,7 @@ fn edit_box(app: &mut SolveApp, ctx: &egui::Context) { e.fresh = e.fresh.saturating_sub(1); match done { Some(true) => { - if let Err(err) = app.run("ChangeParameterCommand", json!({"name": e.param, "expression": e.expr})) { + if let Err(err) = app.run("parameters.change", json!({"name": e.param, "expression": e.expr})) { app.set_status(err, true); EDIT.with(|x| *x.borrow_mut() = Some(Edit { fresh: FRESH, ..e })); } diff --git a/crates/ui-egui/src/documents.rs b/crates/ui-egui/src/documents.rs index a9cc0dd5..26337777 100644 --- a/crates/ui-egui/src/documents.rs +++ b/crates/ui-egui/src/documents.rs @@ -445,20 +445,20 @@ mod tests { #[test] fn tabs_keep_their_own_design() { let mut a = app(); - a.run("PrimitiveBox", json!({"length": 10, "width": 10, "height": 10})).unwrap(); + a.run("solid.box", json!({"length": 10, "width": 10, "height": 10})).unwrap(); a.session.selection = vec![solvecraft_engine::Sel::Body { name: "Body1".into() }]; a.cam.distance = 123.0; new_design(&mut a); assert_eq!(a.docs.count(), 2); assert_eq!(a.docs.active, 1); assert!(a.session.doc.features.is_empty() && a.session.selection.is_empty()); - a.run("PrimitiveSphere", json!({"diameter": 8})).unwrap(); - a.run("PrimitiveSphere", json!({"diameter": 4})).unwrap(); + a.run("solid.sphere", json!({"diameter": 8})).unwrap(); + a.run("solid.sphere", json!({"diameter": 4})).unwrap(); switch(&mut a, 0); assert_eq!(a.session.doc.features.len(), 1); assert_eq!(a.session.selection.len(), 1); assert!((a.cam.distance - 123.0).abs() < 1e-9); - a.run("UndoCommand", json!({})).unwrap(); + a.run("edit.undo", json!({})).unwrap(); assert!(a.session.doc.features.is_empty(), "undo is this design's"); switch(&mut a, 1); assert_eq!(a.session.doc.features.len(), 2, "the other design kept its features"); diff --git a/crates/ui-egui/src/drag_snap.rs b/crates/ui-egui/src/drag_snap.rs index fc5a8d71..078c21f6 100644 --- a/crates/ui-egui/src/drag_snap.rs +++ b/crates/ui-egui/src/drag_snap.rs @@ -200,9 +200,9 @@ pub fn release(app: &mut SolveApp) { let Some(s) = current() else { return }; clear(); let (cmd, params) = match &s.target { - Target::Point(t) => ("ConstraintCoincident", json!({"a": s.point, "b": t})), - Target::Mid(l) => ("ConstraintMidPoint", json!({"point": s.point, "line": l})), - Target::On(c) => ("ConstraintCoincident", json!({"a": s.point, "b": c})), + Target::Point(t) => ("sketch.constraint.coincident", json!({"a": s.point, "b": t})), + Target::Mid(l) => ("sketch.constraint.midpoint", json!({"point": s.point, "line": l})), + Target::On(c) => ("sketch.constraint.coincident", json!({"a": s.point, "b": c})), Target::Align => return, }; // A snap that would over-constrain the sketch is simply not kept. diff --git a/crates/ui-egui/src/inference.rs b/crates/ui-egui/src/inference.rs index 60f96471..ea370348 100644 --- a/crates/ui-egui/src/inference.rs +++ b/crates/ui-egui/src/inference.rs @@ -157,10 +157,10 @@ pub fn commit(app: &mut SolveApp, line: &str, end: Vec2) { let Some(i) = current().filter(|i| i.at.dist(end) < 1e-9) else { return }; clear(); let (cmd, params) = match i.kind { - Kind::Parallel => ("ConstraintParallel", json!({"a": i.curve, "b": line})), - Kind::Perpendicular => ("ConstraintPerpendicular", json!({"a": i.curve, "b": line})), - Kind::Tangent => ("ConstraintTangent", json!({"a": i.curve, "b": line})), - Kind::Extension => ("ConstraintCoincident", json!({"a": format!("{line}.end"), "b": i.curve})), + Kind::Parallel => ("sketch.constraint.parallel", json!({"a": i.curve, "b": line})), + Kind::Perpendicular => ("sketch.constraint.perpendicular", json!({"a": i.curve, "b": line})), + Kind::Tangent => ("sketch.constraint.tangent", json!({"a": i.curve, "b": line})), + Kind::Extension => ("sketch.constraint.coincident", json!({"a": format!("{line}.end"), "b": i.curve})), }; // An inference that would over-constrain the sketch is simply not kept. let _ = app.session.execute(cmd, ¶ms); @@ -168,7 +168,7 @@ pub fn commit(app: &mut SolveApp, line: &str, end: Vec2) { /// The dashed guide of the current inference (its label is the snap hint) (while the Line tool is active). pub fn show(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { - if app.tool.as_ref().is_none_or(|t| t.cmd != "DrawPolyline") { + if app.tool.as_ref().is_none_or(|t| t.cmd != "sketch.line") { return; } let Some(i) = current() else { return }; diff --git a/crates/ui-egui/src/keymap.rs b/crates/ui-egui/src/keymap.rs index b3520484..2563502a 100644 --- a/crates/ui-egui/src/keymap.rs +++ b/crates/ui-egui/src/keymap.rs @@ -37,10 +37,15 @@ impl Keymap { } pub fn load(&mut self, v: &Value) { self.custom = - v.as_object().map(|o| o.iter().take(2000).filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string()))).collect()).unwrap_or_default(); + v.as_object().map(|o| o.iter().take(2000).filter_map(|(k, v)| Some((current(k), v.as_str()?.to_string()))).collect()).unwrap_or_default(); } } +/// Saved before the command ids were renamed: the current id. +fn current(id: &str) -> String { + solvecraft_engine::legacy_ids::current_id(id).to_string() +} + /// A key press as a shortcut name: `E`, `Shift+E`, `Ctrl+Alt+K`, `F5`, `Delete`. pub fn key_name(key: egui::Key, mods: egui::Modifiers) -> String { let mut s = String::new(); @@ -250,24 +255,24 @@ mod tests { #[test] fn rebinding_detects_conflicts_and_resets() { let mut a = app(); - assert_eq!(effective(&a, "Extrude").as_deref(), Some("E")); + assert_eq!(effective(&a, "solid.extrude").as_deref(), Some("E")); // Shift+E is free. - bind(&mut a, "Extrude", "Shift+E", false).unwrap(); - assert_eq!(commands_for(&a, "Shift+E"), vec!["Extrude"]); + bind(&mut a, "solid.extrude", "Shift+E", false).unwrap(); + assert_eq!(commands_for(&a, "Shift+E"), vec!["solid.extrude"]); assert!(commands_for(&a, "E").is_empty()); // M belongs to Move/Copy: refused, then taken over on request. - let e = bind(&mut a, "Revolve", "M", false).unwrap_err(); + let e = bind(&mut a, "solid.revolve", "M", false).unwrap_err(); assert!(e.contains("already used by Move/Copy"), "{e}"); - bind(&mut a, "Revolve", "M", true).unwrap(); - assert_eq!(commands_for(&a, "M"), vec!["Revolve"]); - assert_eq!(effective(&a, "FusionMoveCommand"), None); - assert!(bind(&mut a, "Revolve", "Ctrl+Z", false).is_err(), "reserved"); + bind(&mut a, "solid.revolve", "M", true).unwrap(); + assert_eq!(commands_for(&a, "M"), vec!["solid.revolve"]); + assert_eq!(effective(&a, "solid.move"), None); + assert!(bind(&mut a, "solid.revolve", "Ctrl+Z", false).is_err(), "reserved"); // Kept between runs. let mut b = app(); b.keymap.load(&a.keymap.prefs()); - assert_eq!(effective(&b, "Revolve").as_deref(), Some("M")); + assert_eq!(effective(&b, "solid.revolve").as_deref(), Some("M")); reset(&mut b, None); - assert_eq!(effective(&b, "FusionMoveCommand").as_deref(), Some("M")); + assert_eq!(effective(&b, "solid.move").as_deref(), Some("M")); assert_eq!(key_name(egui::Key::K, egui::Modifiers { ctrl: true, shift: true, ..Default::default() }), "Ctrl+Shift+K"); } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 65fcd427..9f27229d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -353,11 +353,11 @@ impl SolveApp { match &r { Ok(_) => { self.session.echo(format!("{id}: done")); - if matches!(id, "NewDocumentCommand" | "doc.open") { + if matches!(id, "file.new" | "doc.open") { self.cam = Camera::default(); self.fit_view(); } - if matches!(id, "doc.open" | "SaveDocumentCommand" | "SaveDocumentAsCommand") + if matches!(id, "doc.open" | "file.save" | "file.save_as") && let Some(p) = self.session.path.clone() { self.home.add_recent(&p); @@ -387,7 +387,7 @@ impl SolveApp { } self.tool = None; self.home.open = false; - if matches!(id, "FusionImportCommandFromToolbar" | "ParaMeshInsertAlignCommand") { + if matches!(id, "file.insert_step" | "file.insert_mesh") { if let Some(p) = self.services.pick_open.as_ref().and_then(|f| f()) { self.insert_path(&p); } @@ -396,21 +396,21 @@ impl SolveApp { if !sketch_tools::start_hook(self, id) { return; } - if id == "ChangeParameterCommand" { + if id == "parameters.change" { params_dialog::open(self); return; } - if id == "SketchStop" { + if id == "sketch.finish" { self.finish_sketch(); return; } // Press Pull: edges get a fillet, faces and profiles an extrude. - if id == "FusionPressPullCommand" { + if id == "solid.press_pull" { let edges = self.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::Edge { .. })); - return self.start(if edges { "FusionFilletEdgesCommand" } else { "Extrude" }); + return self.start(if edges { "solid.fillet" } else { "solid.extrude" }); } // Fix/Unfix: the selected sketch points and curves change at once, otherwise pick them. - if id == "ConstraintFix" { + if id == "sketch.constraint.fix" { let ents: Vec = self .session .selection @@ -433,7 +433,7 @@ impl SolveApp { } if let Some(spec) = solvecraft_engine::find_command(id) && (tools::Tool::for_command(id).is_some() || dialogs::Dialog::for_command(self, id).is_some()) - && id != "SketchCreate" + && id != "sketch.create" { self.last_command = Some((spec.id.to_string(), spec.label.to_string())); } @@ -468,13 +468,13 @@ impl SolveApp { let Some(idx) = self.session.doc.feature_index(id) else { return }; let marker = self.session.doc.marker; self.tool = None; - if self.run("timeline.rollTo", json!({ "position": idx })).is_err() { + if self.run("timeline.roll_to", json!({ "position": idx })).is_err() { return; } match dialogs::for_feature(self, id, marker) { Some(d) => self.dialog = Some(d), None => { - let _ = self.run("timeline.rollTo", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); + let _ = self.run("timeline.roll_to", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); self.palette.text = format!("timeline.edit {{\"feature\": {id}, \"set\": {{}}}}"); self.ui.palette_open = true; } @@ -496,9 +496,9 @@ impl SolveApp { self.dialog = None; let Some(idx) = self.session.doc.feature_index(id) else { return }; let marker = self.session.doc.marker; - let rolled = marker.is_none_or(|m| m > idx + 1) && self.run("timeline.rollTo", json!({ "position": idx + 1 })).is_ok(); + let rolled = marker.is_none_or(|m| m > idx + 1) && self.run("timeline.roll_to", json!({ "position": idx + 1 })).is_ok(); let before = self.cam; - if self.run("SketchActivate", json!({ "sketch": id })).is_err() { + if self.run("sketch.edit", json!({ "sketch": id })).is_err() { if rolled { self.restore_marker(marker); } @@ -513,7 +513,7 @@ impl SolveApp { /// return to the view from before the sketch. pub fn finish_sketch(&mut self) { self.tool = None; - let _ = self.run("SketchStop", json!({})); + let _ = self.run("sketch.finish", json!({})); if let Some(marker) = self.pre_sketch_marker.take() { self.restore_marker(marker); } @@ -523,7 +523,7 @@ impl SolveApp { } fn restore_marker(&mut self, marker: Option) { - let _ = self.run("timeline.rollTo", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); + let _ = self.run("timeline.roll_to", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({}))); } /// Everything shown as selected: the selection plus the open dialog's inputs. @@ -555,7 +555,7 @@ impl SolveApp { /// Insert a STEP file's bodies (an Import base feature) or a 3MF/STL file's meshes into the /// current design. pub fn insert_path(&mut self, path: &str) { - let cmd = if solvecraft_engine::io::is_mesh_path(path) { "ParaMeshInsertAlignCommand" } else { "FusionImportCommandFromToolbar" }; + let cmd = if solvecraft_engine::io::is_mesh_path(path) { "file.insert_mesh" } else { "file.insert_step" }; if let Ok(r) = self.run(cmd, json!({ "path": path })) { self.fit_view(); if let Some(w) = r["warnings"].as_array().filter(|w| !w.is_empty()) { diff --git a/crates/ui-egui/src/menu_tests.rs b/crates/ui-egui/src/menu_tests.rs index c5795972..5961cbe8 100644 --- a/crates/ui-egui/src/menu_tests.rs +++ b/crates/ui-egui/src/menu_tests.rs @@ -64,7 +64,7 @@ fn face_menu_hides_and_finds_the_body() { #[test] fn body_menu_isolates_renames_locks_and_deletes() { let mut app = sample_app(); - app.run("PrimitiveBox", json!({"length": 5, "width": 5, "height": 5, "corner": [200, 0, 0]})).unwrap(); + app.run("solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [200, 0, 0]})).unwrap(); let b = body(&app); let t = Target::Body { name: b.clone() }; let items = context_menu::items(&app, &t); @@ -119,9 +119,9 @@ fn sketch_menu_hides_and_moves_to_another_plane() { #[test] fn sketch_entities_offer_the_constraints_that_fit() { let mut app = sample_app(); - app.run("SketchCreate", json!({"plane": "XY"})).unwrap(); - app.run("DrawPolyline", json!({"points": [[0, 0], [10, 1]]})).unwrap(); - app.run("DrawPolyline", json!({"points": [[0, 5], [10, 7]]})).unwrap(); + app.run("sketch.create", json!({"plane": "XY"})).unwrap(); + app.run("sketch.line", json!({"points": [[0, 0], [10, 1]]})).unwrap(); + app.run("sketch.line", json!({"points": [[0, 5], [10, 7]]})).unwrap(); let ids: Vec = { let st = app.session.model.state(); let ss = st.sketch(app.session.active_sketch.unwrap()).unwrap(); @@ -141,7 +141,7 @@ fn sketch_entities_offer_the_constraints_that_fit() { fn component_menu_activates_grounds_and_pastes() { let mut app = sample_app(); let b = body(&app); - app.run("FusionCreateComponentsFromBodiesCommand", json!({"bodies": [b]})).unwrap(); + app.run("component.from_bodies", json!({"bodies": [b]})).unwrap(); let c = app.session.doc.components[0].id; let t = Target::Component { id: c }; act(&mut app, &t, "Activate"); @@ -169,7 +169,7 @@ fn pick_runs_an_item_of_the_open_menu() { #[test] fn folder_and_group_menus_make_rename_and_drop_groups() { let mut app = sample_app(); - app.run("PrimitiveBox", json!({"length": 5, "width": 5, "height": 5, "corner": [200, 0, 0]})).unwrap(); + app.run("solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [200, 0, 0]})).unwrap(); let names: Vec = app.session.model.state().bodies.iter().map(|b| b.name.clone()).collect(); let items: Vec<_> = names.iter().map(|n| json!({"type": "body", "name": n})).collect(); app.run("select.set", json!({ "items": items })).unwrap(); @@ -206,8 +206,8 @@ fn browser_folds_are_kept_in_the_preferences() { fn delete_key_handles_every_kind_and_asks_about_dependents() { use solvecraft_engine::Sel; let mut app = sample_app(); - app.run("ConstructionPlaneOffsetFromPlaneCommand", json!({"base": "XY", "offset": 10})).unwrap(); - app.run("PrimitiveBox", json!({"length": 5, "width": 5, "height": 5, "corner": [200, 0, 0]})).unwrap(); + app.run("construct.plane.offset", json!({"base": "XY", "offset": 10})).unwrap(); + app.run("solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [200, 0, 0]})).unwrap(); let n = app.session.doc.features.len(); // A body and a construction plane together: one undo step, no question. let b = app.session.model.state().bodies.last().unwrap().name.clone(); @@ -216,7 +216,7 @@ fn delete_key_handles_every_kind_and_asks_about_dependents() { assert!(app.menu.confirm.is_none()); assert!(app.session.model.state().body(&b).is_none()); assert!(app.session.doc.find_feature("Plane1").is_none()); - app.run("UndoCommand", json!({})).unwrap(); + app.run("edit.undo", json!({})).unwrap(); assert_eq!(app.session.doc.features.len(), n); // A sketch with features built on it: the confirmation lists them first. let sk = app.session.doc.features.iter().find(|f| f.name == "Base").unwrap().id; @@ -229,7 +229,7 @@ fn delete_key_handles_every_kind_and_asks_about_dependents() { crate::delete::delete_selection(&mut app); crate::delete::confirm(&mut app, true); assert!(app.session.doc.features.len() < n); - app.run("UndoCommand", json!({})).unwrap(); + app.run("edit.undo", json!({})).unwrap(); assert_eq!(app.session.doc.features.len(), n); // A face alone: not available yet, nothing changes. let b0 = app.session.model.state().bodies[0].name.clone(); @@ -243,7 +243,7 @@ fn delete_key_handles_every_kind_and_asks_about_dependents() { fn menu_delete_items_use_the_same_path() { let mut app = sample_app(); let b = body(&app); - app.run("FusionCreateComponentsFromBodiesCommand", json!({"bodies": [b]})).unwrap(); + app.run("component.from_bodies", json!({"bodies": [b]})).unwrap(); let c = app.session.doc.components[0].id; act(&mut app, &Target::Component { id: c }, "Delete"); assert!(app.session.doc.components.is_empty()); @@ -261,7 +261,7 @@ fn canvas_menu_hides_renames_calibrates_and_deletes() { png.extend(200u32.to_be_bytes()); png.extend([8, 6, 0, 0, 0, 0, 0, 0, 0]); let data = solvecraft_engine::doc::canvas::base64_encode(&png); - let id = app.run("FusionAddCanvasCommand", json!({"data": data, "plane": "XY", "width": 80})).unwrap()["canvas"].as_u64().unwrap(); + let id = app.run("canvas.insert", json!({"data": data, "plane": "XY", "width": 80})).unwrap()["canvas"].as_u64().unwrap(); let t = Target::Canvas { id }; act(&mut app, &t, "Show/Hide"); assert!(!app.session.doc.canvases[0].visible); @@ -273,7 +273,7 @@ fn canvas_menu_hides_renames_calibrates_and_deletes() { assert!(p.calibrate && (p.distance - 80.0).abs() < 1e-9, "starts from the image's width"); act(&mut app, &t, "Delete"); assert!(app.session.doc.canvases.is_empty()); - app.run("UndoCommand", json!({})).unwrap(); + app.run("edit.undo", json!({})).unwrap(); assert_eq!(app.session.doc.canvases.len(), 1); // A canvas picked in the browser goes with the Delete key. app.tree.picked_canvases = vec![id]; @@ -373,7 +373,7 @@ fn v_toggles_what_is_selected() { assert!(app.ui.hidden_bodies.is_empty() && app.ui.hidden_origin.is_empty()); assert!(crate::browser::sketch_visible(&app, sk)); // Components picked in the browser hide their bodies. - app.run("FusionCreateComponentsFromBodiesCommand", json!({"bodies": [b.clone()]})).unwrap(); + app.run("component.from_bodies", json!({"bodies": [b.clone()]})).unwrap(); app.run("select.clear", json!({})).unwrap(); app.tree.picked_components = vec![app.session.doc.components[0].id]; context_menu::toggle_visibility(&mut app); @@ -421,16 +421,16 @@ fn units_menu_changes_the_design_units_and_new_designs_take_the_preference() { fn an_uncaptured_move_asks_before_the_next_command() { let mut app = sample_app(); let b = body(&app); - app.run("FusionCreateComponentsFromBodiesCommand", json!({"bodies": [b]})).unwrap(); + app.run("component.from_bodies", json!({"bodies": [b]})).unwrap(); let occ = app.session.doc.occurrences[0].id; app.run("occurrence.move", json!({"occurrence": occ, "translate": [10, 0, 0], "capture": false})).unwrap(); - app.start("SketchCreate"); - assert_eq!(app.tree.capture_prompt.as_deref(), Some("SketchCreate"), "asks first"); + app.start("sketch.create"); + assert_eq!(app.tree.capture_prompt.as_deref(), Some("sketch.create"), "asks first"); assert!(app.dialog.is_none()); crate::browser::answer_capture(&mut app, "cancel").unwrap(); assert!(app.tree.capture_prompt.is_none() && !app.session.pending_moves.is_empty()); // Revert, then the command goes ahead. - app.start("SketchCreate"); + app.start("sketch.create"); crate::browser::answer_capture(&mut app, "revert").unwrap(); assert!(app.session.pending_moves.is_empty()); assert!(app.dialog.is_some(), "Create Sketch started"); diff --git a/crates/ui-egui/src/params_dialog.rs b/crates/ui-egui/src/params_dialog.rs index 5a7bb76c..f884e0ff 100644 --- a/crates/ui-egui/src/params_dialog.rs +++ b/crates/ui-egui/src/params_dialog.rs @@ -46,7 +46,7 @@ impl ParamsDialog { /// The command the live preview should run (a valid expression being edited). pub fn preview_commands(&self) -> Option> { let (n, e) = self.preview.as_ref()?; - Some(vec![("ChangeParameterCommand".to_string(), json!({"name": n, "expression": e}))]) + Some(vec![("parameters.change".to_string(), json!({"name": n, "expression": e}))]) } } @@ -257,7 +257,7 @@ fn table(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut ParamsDialog, rows: &[&R if sel != before && let Some(u) = UNITS.get(sel) { - d.error = app.run("ChangeParameterCommand", json!({"name": r.name, "expression": r.expression, "unit": u})).err(); + d.error = app.run("parameters.change", json!({"name": r.name, "expression": r.expression, "unit": u})).err(); } } else { ui.label(RichText::new(unit_label(&r.unit)).color(t.text_dim)); @@ -281,7 +281,7 @@ fn table(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut ParamsDialog, rows: &[&R if resp.lost_focus() { d.preview = None; if e != r.expression { - let res = app.run("ChangeParameterCommand", json!({"name": r.name, "expression": e})); + let res = app.run("parameters.change", json!({"name": r.name, "expression": e})); d.error = res.err(); if d.error.is_none() { d.editing.remove(&r.name); diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 4c66e60c..bd5337e5 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -393,7 +393,7 @@ mod tests { #[test] fn orbiting_around_the_cursor_keeps_that_point_still() { let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default()); - app.run("PrimitiveBox", json!({"length": 40, "width": 40, "height": 40})).unwrap(); + app.run("solid.box", json!({"length": 40, "width": 40, "height": 40})).unwrap(); app.preferences.orbit_cursor = true; let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0)); app.fit_view(); diff --git a/crates/ui-egui/src/preselect_tests.rs b/crates/ui-egui/src/preselect_tests.rs index 380b2e09..9afd3b71 100644 --- a/crates/ui-egui/src/preselect_tests.rs +++ b/crates/ui-egui/src/preselect_tests.rs @@ -15,7 +15,7 @@ fn app_with(script: serde_json::Value) -> SolveApp { } fn boxed() -> SolveApp { - app_with(json!([{"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 20}}])) + app_with(json!([{"command": "solid.box", "params": {"length": 40, "width": 30, "height": 20}}])) } /// The face of Body1 whose centre is at `c`. @@ -42,13 +42,13 @@ fn start(app: &mut SolveApp, sels: Vec, id: &str) -> Dialog { #[test] fn profiles_then_extrude_takes_them_all() { let mut app = app_with(json!([ - {"command": "SketchCreate", "params": {"plane": "XY"}}, - {"command": "CircleCenterRadius", "params": {"center": [0, 0], "radius": 5}}, - {"command": "CircleCenterRadius", "params": {"center": [20, 0], "radius": 5}}, - {"command": "SketchStop", "params": {}}, + {"command": "sketch.create", "params": {"plane": "XY"}}, + {"command": "sketch.circle.center", "params": {"center": [0, 0], "radius": 5}}, + {"command": "sketch.circle.center", "params": {"center": [20, 0], "radius": 5}}, + {"command": "sketch.finish", "params": {}}, ])); let sk = app.session.doc.features.first().unwrap().id; - let d = start(&mut app, vec![Sel::Profile { sketch: sk, index: 0 }, Sel::Profile { sketch: sk, index: 1 }], "Extrude"); + let d = start(&mut app, vec![Sel::Profile { sketch: sk, index: 0 }, Sel::Profile { sketch: sk, index: 1 }], "solid.extrude"); assert_eq!(d.inputs[0].items.len(), 2); assert!(d.focus, "the value box takes the keyboard"); assert!(apply_commands(&app, &d).is_ok()); @@ -58,7 +58,7 @@ fn profiles_then_extrude_takes_them_all() { fn edges_then_fillet_takes_them_with_chains() { let mut app = boxed(); let (a, b) = (edge(&app, Vec3::new(0.0, 0.0, 10.0)), edge(&app, Vec3::new(20.0, 0.0, 20.0))); - let d = start(&mut app, vec![a, b], "FusionFilletEdgesCommand"); + let d = start(&mut app, vec![a, b], "solid.fillet"); assert_eq!(d.inputs[0].items.len(), 2, "box edges are not tangent to others"); let cmds = apply_commands(&app, &d).unwrap(); assert_eq!(cmds[0].1["edges"].as_array().unwrap().len(), 2); @@ -68,7 +68,7 @@ fn edges_then_fillet_takes_them_with_chains() { fn face_then_extrude_press_pulls_it() { let mut app = boxed(); let top = face(&app, Vec3::new(20.0, 15.0, 20.0)); - let d = start(&mut app, vec![top], "Extrude"); + let d = start(&mut app, vec![top], "solid.extrude"); let cmds = apply_commands(&app, &d).unwrap(); assert!(cmds[0].1.get("face").is_some()); } @@ -78,9 +78,9 @@ fn faces_then_shell_and_face_then_hole() { let mut app = boxed(); let top = face(&app, Vec3::new(20.0, 15.0, 20.0)); let front = face(&app, Vec3::new(20.0, 0.0, 10.0)); - let d = start(&mut app, vec![top.clone(), front], "FusionShellBodyCommand"); + let d = start(&mut app, vec![top.clone(), front], "solid.shell"); assert_eq!(d.inputs[0].items.len(), 2); - let d = start(&mut app, vec![top], "FusionHoleCommand"); + let d = start(&mut app, vec![top], "solid.hole"); assert_eq!(d.inputs[0].items.len(), 1); let cmds = apply_commands(&app, &d).unwrap(); assert!(cmds[0].1.get("position").is_some()); @@ -89,12 +89,12 @@ fn faces_then_shell_and_face_then_hole() { #[test] fn line_and_profile_then_revolve_routes_each() { let mut app = app_with(json!([ - {"command": "SketchCreate", "params": {"plane": "XZ"}}, - {"command": "ShapeRectangleTwoPoint", "params": {"p0": [10, 0], "p1": [20, 10]}}, - {"command": "DrawPolyline", "params": {"points": [[0, 0], [0, 30]], "ids": ["axis"]}}, + {"command": "sketch.create", "params": {"plane": "XZ"}}, + {"command": "sketch.rectangle.two_point", "params": {"p0": [10, 0], "p1": [20, 10]}}, + {"command": "sketch.line", "params": {"points": [[0, 0], [0, 30]], "ids": ["axis"]}}, ])); let sk = app.session.active_sketch.unwrap(); - let d = start(&mut app, vec![Sel::SketchCurve { id: "axis".into() }, Sel::Profile { sketch: sk, index: 0 }], "Revolve"); + let d = start(&mut app, vec![Sel::SketchCurve { id: "axis".into() }, Sel::Profile { sketch: sk, index: 0 }], "solid.revolve"); assert_eq!(d.inputs[0].items, vec![Sel::Profile { sketch: sk, index: 0 }]); assert_eq!(d.inputs[1].items, vec![Sel::SketchCurve { id: "axis".into() }]); } @@ -103,7 +103,7 @@ fn line_and_profile_then_revolve_routes_each() { fn face_then_move_takes_the_body_and_odd_items_are_ignored() { let mut app = boxed(); let top = face(&app, Vec3::new(20.0, 15.0, 20.0)); - let d = start(&mut app, vec![top, Sel::Axis { name: "X".into() }], "FusionMoveCommand"); + let d = start(&mut app, vec![top, Sel::Axis { name: "X".into() }], "solid.move"); assert_eq!(d.inputs[0].items, vec![Sel::Body { name: "Body1".into() }]); assert!(matches!(d.kind, Kind::Move { .. })); } @@ -111,14 +111,14 @@ fn face_then_move_takes_the_body_and_odd_items_are_ignored() { #[test] fn hole_at_sketch_points() { let mut app = app_with(json!([ - {"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 20}}, - {"command": "SketchCreate", "params": {"plane": {"face": [20, 15, 20]}}}, - {"command": "DrawPoint", "params": {"point": [10, 10], "id": "h1"}}, - {"command": "DrawPoint", "params": {"point": [30, 20], "id": "h2"}}, - {"command": "SketchStop", "params": {}}, + {"command": "solid.box", "params": {"length": 40, "width": 30, "height": 20}}, + {"command": "sketch.create", "params": {"plane": {"face": [20, 15, 20]}}}, + {"command": "sketch.point", "params": {"point": [10, 10], "id": "h1"}}, + {"command": "sketch.point", "params": {"point": [30, 20], "id": "h2"}}, + {"command": "sketch.finish", "params": {}}, ])); let sk = app.session.doc.features.iter().find(|f| matches!(f.kind, solvecraft_engine::doc::FeatureKind::Sketch { .. })).unwrap().id; - app.start("FusionHoleCommand"); + app.start("solid.hole"); let mut d = app.dialog.clone().unwrap(); if let Kind::Hole { opts, .. } = &mut d.kind { opts.multiple = true; @@ -135,12 +135,12 @@ fn hole_at_sketch_points() { #[test] fn a_hole_is_patterned_as_a_feature() { let mut app = app_with(json!([ - {"command": "PrimitiveBox", "params": {"length": 60, "width": 40, "height": 10}}, - {"command": "FusionHoleCommand", "params": {"position": [10, 10, 10], "diameter": 5}} + {"command": "solid.box", "params": {"length": 60, "width": 40, "height": 10}}, + {"command": "solid.hole", "params": {"position": [10, 10, 10], "diameter": 5}} ])); let hole = app.session.doc.features.last().unwrap().id; // Picked in the timeline before the command: the objects become features. - let mut d = start(&mut app, vec![Sel::Feature { id: hole }], "PatternRectangular"); + let mut d = start(&mut app, vec![Sel::Feature { id: hole }], "solid.pattern.rectangular"); assert_eq!(d.inputs[0].accept, crate::selection::FEATURES); assert_eq!(d.inputs[0].items, vec![Sel::Feature { id: hole }]); d.inputs[1].items = vec![Sel::Axis { name: "X".into() }]; @@ -164,7 +164,7 @@ fn a_hole_is_patterned_as_a_feature() { let [a, b, c] = m.tri(&m.triangles[t]).unwrap(); crate::viewport::Hit::Face { body: "Body1".into(), index: m.tri_face[t] as usize, point: (a + b + c) * (1.0 / 3.0) } }; - let mut d = start(&mut app, vec![], "PatternCircular"); + let mut d = start(&mut app, vec![], "solid.pattern.circular"); d.inputs[0].accept = crate::selection::FEATURES; let box_id = app.session.doc.features[0].id; assert_eq!(d.candidate(&app.session, &face_at(Vec3::new(12.5, 10.0, 5.0))), Some(Sel::Feature { id: hole })); @@ -180,11 +180,11 @@ fn a_hole_is_patterned_as_a_feature() { #[test] fn a_hole_is_mirrored_as_a_feature() { let mut app = app_with(json!([ - {"command": "PrimitiveBox", "params": {"length": 60, "width": 40, "height": 10}}, - {"command": "FusionHoleCommand", "params": {"position": [10, 10, 10], "diameter": 5}} + {"command": "solid.box", "params": {"length": 60, "width": 40, "height": 10}}, + {"command": "solid.hole", "params": {"position": [10, 10, 10], "diameter": 5}} ])); let hole = app.session.doc.features.last().unwrap().id; - let mut d = start(&mut app, vec![Sel::Feature { id: hole }], "MirrorCommand"); + let mut d = start(&mut app, vec![Sel::Feature { id: hole }], "solid.mirror"); assert_eq!(d.inputs[0].accept, crate::selection::FEATURES); d.inputs[1].items = vec![Sel::Plane { name: "YZ".into() }]; let cmds = apply_commands(&app, &d).unwrap(); @@ -195,7 +195,7 @@ fn a_hole_is_mirrored_as_a_feature() { fn chamfer_types_send_their_values() { let mut app = boxed(); let e = edge(&app, Vec3::new(20.0, 0.0, 20.0)); - let mut d = start(&mut app, vec![e], "FusionChamferCommand"); + let mut d = start(&mut app, vec![e], "solid.chamfer"); let mut with = |t: usize| { if let Kind::Fillet { ctype, distance2, angle, flip, .. } = &mut d.kind { *ctype = t; diff --git a/crates/ui-egui/src/preview.rs b/crates/ui-egui/src/preview.rs index 59e9bca1..146cc719 100644 --- a/crates/ui-egui/src/preview.rs +++ b/crates/ui-egui/src/preview.rs @@ -367,10 +367,10 @@ mod tests { fn plate() -> Session { let mut s = Session::default(); for (id, p) in [ - ("SketchCreate", json!({"plane": "XY"})), - ("ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})), - ("SketchStop", json!({})), - ("Extrude", json!({"distance": 20})), + ("sketch.create", json!({"plane": "XY"})), + ("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})), + ("sketch.finish", json!({})), + ("solid.extrude", json!({"distance": 20})), ] { s.execute(id, &p).unwrap(); } @@ -383,7 +383,7 @@ mod tests { fn fillet_preview_scene() { let s = plate(); let (rev, undo) = (s.revision, s.undo.len()); - let b = compute(&s, &[("FusionFilletEdgesCommand".into(), json!({"edges": [[0, 0, 10]], "radius": 3}))], colors()).unwrap(); + let b = compute(&s, &[("solid.fillet".into(), json!({"edges": [[0, 0, 10]], "radius": 3}))], colors()).unwrap(); assert_eq!(b.replaced, vec!["Body1".to_string()]); let added = count(&b.scene.tris, crate::gpu::TRI_SIZE, colors().added); let kept = count(&b.scene.tris, crate::gpu::TRI_SIZE, colors().body); @@ -397,10 +397,10 @@ mod tests { #[test] fn cut_preview_scene() { let mut s = plate(); - s.execute("SketchCreate", &json!({"plane": "XY"})).unwrap(); - s.execute("CircleCenterRadius", &json!({"center": [20, 15], "radius": 5})).unwrap(); - s.execute("SketchStop", &json!({})).unwrap(); - let b = compute(&s, &[("Extrude".into(), json!({"distance": 20, "operation": "cut"}))], colors()).unwrap(); + s.execute("sketch.create", &json!({"plane": "XY"})).unwrap(); + s.execute("sketch.circle.center", &json!({"center": [20, 15], "radius": 5})).unwrap(); + s.execute("sketch.finish", &json!({})).unwrap(); + let b = compute(&s, &[("solid.extrude".into(), json!({"distance": 20, "operation": "cut"}))], colors()).unwrap(); assert!(b.replaced.is_empty()); assert!(b.scene.tris.is_empty() && !b.scene.xray.is_empty()); assert!(b.error.is_none()); @@ -410,14 +410,14 @@ mod tests { #[test] fn extrude_preview_scene() { let mut s = Session::default(); - s.execute("SketchCreate", &json!({"plane": "XY"})).unwrap(); - s.execute("CircleCenterRadius", &json!({"center": [0, 0], "radius": 5})).unwrap(); - s.execute("SketchStop", &json!({})).unwrap(); - let b = compute(&s, &[("Extrude".into(), json!({"distance": "15 mm"}))], colors()).unwrap(); + s.execute("sketch.create", &json!({"plane": "XY"})).unwrap(); + s.execute("sketch.circle.center", &json!({"center": [0, 0], "radius": 5})).unwrap(); + s.execute("sketch.finish", &json!({})).unwrap(); + let b = compute(&s, &[("solid.extrude".into(), json!({"distance": "15 mm"}))], colors()).unwrap(); assert!(b.replaced.is_empty()); assert_eq!(count(&b.scene.tris, crate::gpu::TRI_SIZE, colors().body), 0); assert!(b.scene.ghost.is_empty()); - assert!(compute(&s, &[("Extrude".into(), json!({"distance": "1 +"}))], colors()).is_err()); + assert!(compute(&s, &[("solid.extrude".into(), json!({"distance": "1 +"}))], colors()).is_err()); assert!(s.model.state().bodies.is_empty()); } } diff --git a/crates/ui-egui/src/scenario.rs b/crates/ui-egui/src/scenario.rs index 1474f8b1..49a924a2 100644 --- a/crates/ui-egui/src/scenario.rs +++ b/crates/ui-egui/src/scenario.rs @@ -3,7 +3,7 @@ //! drags, toolbar starts) the way a person would use it, with checks on the result. //! //! A scenario is a JSON list of steps: -//! - `{"start": "Extrude"}`: a toolbar click; `{"key": "E", "shift"?, "cmd"?}`; `{"text": "20"}` +//! - `{"start": "solid.extrude"}`: a toolbar click; `{"key": "E", "shift"?, "cmd"?}`; `{"text": "20"}` //! - `{"widget": "Revolute", "near"?: "Type"}`: click a dialog field, choice or button by its //! text (with `near`: the one in that label's row) //! - `{"click": AT, "double"?, "shift"?, "ctrl"?, "button"?}`, `{"move": AT}`, @@ -650,7 +650,7 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> { // {body, min?: [x|null, y|null, z|null], max?: […]}: where a body is (±1e-3 mm). let name = v["body"].as_str().unwrap_or_default(); // Where it is in the assembly (measured as placed). - let m = h.call("engine.execute", json!({"command": "MeasureCommand", "params": {"bodies": [name]}}))["result"].clone(); + let m = h.call("engine.execute", json!({"command": "inspect.measure", "params": {"bodies": [name]}}))["result"].clone(); let b = m["bodies"].as_array().and_then(|a| a.first()).cloned().ok_or_else(|| format!("no body {name}: {m}"))?; for side in ["min", "max"] { for (i, want) in v[side].as_array().into_iter().flatten().enumerate() { @@ -682,7 +682,7 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> { "body_volume" => { // {body, volume}: one body's volume. let name = v["body"].as_str().unwrap_or_default(); - let m = h.call("engine.execute", json!({"command": "MeasureCommand", "params": {"bodies": [name]}}))["result"].clone(); + let m = h.call("engine.execute", json!({"command": "inspect.measure", "params": {"bodies": [name]}}))["result"].clone(); let got = m["bodies"][0]["volume_mm3"].as_f64().unwrap_or(f64::NAN); approx(got, &v["volume"], &format!("{name} volume"))?; } diff --git a/crates/ui-egui/src/shortcut_box.rs b/crates/ui-egui/src/shortcut_box.rs index f9bdee88..c088c95f 100644 --- a/crates/ui-egui/src/shortcut_box.rs +++ b/crates/ui-egui/src/shortcut_box.rs @@ -34,7 +34,10 @@ impl ShortcutBox { } pub fn load(&mut self, v: &Value) { let list = |k: &str| -> Vec { - v.get(k).and_then(Value::as_array).map(|a| a.iter().take(64).filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default() + v.get(k) + .and_then(Value::as_array) + .map(|a| a.iter().take(64).filter_map(|x| x.as_str().map(|id| solvecraft_engine::legacy_ids::current_id(id).to_string())).collect()) + .unwrap_or_default() }; self.recent = list("recent"); self.pinned = list("pinned"); @@ -272,16 +275,16 @@ mod tests { #[test] fn search_finds_by_name_id_and_panel_and_puts_recent_first() { let mut app = SolveApp::new(solvecraft_engine::Session::default(), Default::default()); - assert_eq!(search(&app, "extr").first().map(|x| x.0), Some("Extrude")); - assert_eq!(search(&app, "FusionFilletEdges").first().map(|x| x.0), Some("FusionFilletEdgesCommand")); - assert!(search(&app, "constraints").iter().any(|x| x.0 == "ConstraintParallel"), "by panel"); - app.sbox.used("FusionShellBodyCommand"); - app.sbox.used("Revolve"); + assert_eq!(search(&app, "extr").first().map(|x| x.0), Some("solid.extrude")); + assert_eq!(search(&app, "solid.fil").first().map(|x| x.0), Some("solid.fillet")); + assert!(search(&app, "constraints").iter().any(|x| x.0 == "sketch.constraint.parallel"), "by panel"); + app.sbox.used("solid.shell"); + app.sbox.used("solid.revolve"); let all = search(&app, ""); - assert_eq!((all[0].0, all[1].0), ("Revolve", "FusionShellBodyCommand")); - app.sbox.toggle_pin("Extrude"); + assert_eq!((all[0].0, all[1].0), ("solid.revolve", "solid.shell")); + app.sbox.toggle_pin("solid.extrude"); let mut other = SolveApp::new(solvecraft_engine::Session::default(), Default::default()); other.sbox.load(&app.sbox.prefs()); - assert_eq!((other.sbox.pinned.clone(), other.sbox.recent.len()), (vec!["Extrude".to_string()], 2)); + assert_eq!((other.sbox.pinned.clone(), other.sbox.recent.len()), (vec!["solid.extrude".to_string()], 2)); } } diff --git a/crates/ui-egui/src/sketch3d.rs b/crates/ui-egui/src/sketch3d.rs index f190ad1a..01bc9307 100644 --- a/crates/ui-egui/src/sketch3d.rs +++ b/crates/ui-egui/src/sketch3d.rs @@ -3,7 +3,7 @@ //! and to points already drawn in 3D; segments that stay on the plane are ordinary sketch lines. //! A drawn 3D point (or curve end) clicked without a tool gets a move triad: drag an arrow to //! move along X, Y or Z, or the square to move parallel to the sketch plane. Everything runs -//! `sketch.line3d`, `sketch.spline3d`, `sketch.point3d`, `sketch.move3d` and `DrawPolyline`. +//! `sketch.line3d`, `sketch.spline3d`, `sketch.point3d`, `sketch.move3d` and `sketch.line`. use std::cell::RefCell; @@ -37,7 +37,7 @@ pub fn active(app: &SolveApp) -> bool { } fn takes(cmd: &str) -> bool { - matches!(cmd, "DrawPolyline" | "DrawSpline" | "DrawPoint") + matches!(cmd, "sketch.line" | "sketch.spline.fit_point" | "sketch.point") } /// Points drawn in 3D: (wire id, fit index, point). @@ -122,17 +122,17 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) -> set_chain(Some(tool.cmd), Vec::new()); } match tool.cmd { - "DrawPoint" => { + "sketch.point" => { let r = match on_plane(app, p) { - Some(q) => app.run("DrawPoint", json!({"point": [q.x, q.y]})), + Some(q) => app.run("sketch.point", json!({"point": [q.x, q.y]})), None => app.run("sketch.point3d", json!({"point": [p.x, p.y, p.z]})), }; report(app, r); } - "DrawPolyline" => { + "sketch.line" => { if let Some(a) = chain().last().copied() { let r = match (on_plane(app, a), on_plane(app, p)) { - (Some(qa), Some(qb)) => app.run("DrawPolyline", json!({"points": [[qa.x, qa.y], [qb.x, qb.y]], "infer": true})), + (Some(qa), Some(qb)) => app.run("sketch.line", json!({"points": [[qa.x, qa.y], [qb.x, qb.y]], "infer": true})), _ => app.run("sketch.line3d", json!({"points": [[a.x, a.y, a.z], [p.x, p.y, p.z]]})), }; report(app, r); @@ -168,10 +168,10 @@ pub fn finish(app: &mut SolveApp) -> bool { set_chain(None, Vec::new()); return false; } - if cmd == "DrawSpline" && pts.len() >= 2 { + if cmd == "sketch.spline.fit_point" && pts.len() >= 2 { let r = if pts.iter().all(|p| on_plane(app, *p).is_some()) { let q: Vec = pts.iter().filter_map(|p| on_plane(app, *p)).map(|q| json!([q.x, q.y])).collect(); - app.run("DrawSpline", json!({ "points": q })) + app.run("sketch.spline.fit_point", json!({ "points": q })) } else { let q: Vec = pts.iter().map(|p| json!([p.x, p.y, p.z])).collect(); app.run("sketch.spline3d", json!({ "points": q })) @@ -195,7 +195,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) - if let Some((h, label)) = hover && let Some(s) = proj.to_screen(h) { - if t.cmd != "DrawPoint" && !pts.is_empty() { + if t.cmd != "sketch.point" && !pts.is_empty() { all.push(s); } painter.circle_stroke(s, if label == "Plane" { 3.0 } else { 6.0 }, stroke); @@ -210,7 +210,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) - tk.text_dim, ); } - if t.cmd == "DrawSpline" && all.len() >= 2 { + if t.cmd == "sketch.spline.fit_point" && all.len() >= 2 { let mut w: Vec = pts.clone(); if let Some((h, _)) = hover { w.push(h); diff --git a/crates/ui-egui/src/sketch_dims.rs b/crates/ui-egui/src/sketch_dims.rs index 4d7dba5c..c408ec41 100644 --- a/crates/ui-egui/src/sketch_dims.rs +++ b/crates/ui-egui/src/sketch_dims.rs @@ -36,14 +36,14 @@ pub fn boxes_for(cmd: &str, n: usize) -> Vec { use ValueKind::*; let b = DimBox::new; match (cmd, n) { - ("DrawPolyline", n) if n >= 1 => vec![b("Length", Length), b("Angle", Angle)], - ("ShapeRectangleTwoPoint" | "ShapeRectangleCenter", 1) => vec![b("Width", Length), b("Height", Length)], - ("CircleCenterRadius", 1) => vec![b("Diameter", Length)], - ("ArcCenterTwoPoint", 1) => vec![b("Radius", Length)], - ("ArcCenterTwoPoint", 2) => vec![b("Sweep", Angle)], - ("ShapePolygonInscribed" | "ShapePolygonCircumscribed", 1) => vec![b("Radius", Length), b("Sides", Unitless)], - ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 1) => vec![b("Length", Length)], - ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 2) => vec![b("Width", Length)], + ("sketch.line", n) if n >= 1 => vec![b("Length", Length), b("Angle", Angle)], + ("sketch.rectangle.two_point" | "sketch.rectangle.center", 1) => vec![b("Width", Length), b("Height", Length)], + ("sketch.circle.center", 1) => vec![b("Diameter", Length)], + ("sketch.arc.center_point", 1) => vec![b("Radius", Length)], + ("sketch.arc.center_point", 2) => vec![b("Sweep", Angle)], + ("sketch.polygon.inscribed" | "sketch.polygon.circumscribed", 1) => vec![b("Radius", Length), b("Sides", Unitless)], + ("sketch.slot.center_to_center" | "sketch.slot.overall", 1) => vec![b("Length", Length)], + ("sketch.slot.center_to_center" | "sketch.slot.overall", 2) => vec![b("Width", Length)], _ => Vec::new(), } } @@ -67,31 +67,31 @@ pub fn effective(app: &SolveApp, t: &Tool, h: Vec2) -> Vec2 { let Some(a) = t.pts.last().map(|p| p.0) else { return h }; let lk = |l: &str| locked(app, t, l); match (t.cmd, t.pts.len()) { - ("DrawPolyline", _) | ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 1) => { + ("sketch.line", _) | ("sketch.slot.center_to_center" | "sketch.slot.overall", 1) => { let v = h - a; let len = lk("Length").unwrap_or(v.len()); let ang = lk("Angle").unwrap_or_else(|| snap_axis(v.angle())); a + Vec2::from_angle(ang) * len } - ("ShapeRectangleTwoPoint", 1) => { + ("sketch.rectangle.two_point", 1) => { let d = h - a; Vec2::new(a.x + lk("Width").map_or(d.x, |w| w * sign(d.x)), a.y + lk("Height").map_or(d.y, |hh| hh * sign(d.y))) } - ("ShapeRectangleCenter", 1) => { + ("sketch.rectangle.center", 1) => { let d = h - a; Vec2::new(a.x + lk("Width").map_or(d.x, |w| w * 0.5 * sign(d.x)), a.y + lk("Height").map_or(d.y, |hh| hh * 0.5 * sign(d.y))) } - ("CircleCenterRadius", 1) => a + dir_or_x(h - a) * lk("Diameter").map_or((h - a).len(), |d| d * 0.5), - ("ArcCenterTwoPoint", 1) => a + dir_or_x(h - a) * lk("Radius").unwrap_or((h - a).len()), - ("ArcCenterTwoPoint", 2) => { + ("sketch.circle.center", 1) => a + dir_or_x(h - a) * lk("Diameter").map_or((h - a).len(), |d| d * 0.5), + ("sketch.arc.center_point", 1) => a + dir_or_x(h - a) * lk("Radius").unwrap_or((h - a).len()), + ("sketch.arc.center_point", 2) => { let c = t.pts.first().map(|p| p.0).unwrap_or(a); let r = (a - c).len(); let a0 = (a - c).angle(); let sweep = lk("Sweep").unwrap_or_else(|| ((h - c).angle() - a0).rem_euclid(std::f64::consts::TAU)); c + Vec2::from_angle(a0 + sweep) * r } - ("ShapePolygonInscribed" | "ShapePolygonCircumscribed", 1) => a + dir_or_x(h - a) * lk("Radius").unwrap_or((h - a).len()), - ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 2) => match lk("Width") { + ("sketch.polygon.inscribed" | "sketch.polygon.circumscribed", 1) => a + dir_or_x(h - a) * lk("Radius").unwrap_or((h - a).len()), + ("sketch.slot.center_to_center" | "sketch.slot.overall", 2) => match lk("Width") { Some(w) => { let c0 = t.pts.first().map(|p| p.0).unwrap_or(a); let dir = dir_or_x(a - c0); @@ -136,33 +136,33 @@ fn live(t: &Tool, e: Vec2, px: f64) -> Vec<(f64, Vec2)> { let first = t.pts.first().map(|p| p.0).unwrap_or(a); let off = 20.0 * px; match (t.cmd, t.pts.len()) { - ("DrawPolyline", _) | ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 1) => { + ("sketch.line", _) | ("sketch.slot.center_to_center" | "sketch.slot.overall", 1) => { let v = e - a; let d = dir_or_x(v); let n = d.perp(); vec![(v.len(), (a + e) * 0.5 + n * off), (v.angle(), a + d * (v.len() * 0.3).min(60.0 * px) - n * off)] } - ("ShapeRectangleTwoPoint", 1) => { + ("sketch.rectangle.two_point", 1) => { let (sx, sy) = (sign(e.x - a.x), sign(e.y - a.y)); vec![ ((e.x - a.x).abs(), Vec2::new((a.x + e.x) * 0.5, a.y - sy * off)), ((e.y - a.y).abs(), Vec2::new(e.x + sx * 2.0 * off, (a.y + e.y) * 0.5)), ] } - ("ShapeRectangleCenter", 1) => { + ("sketch.rectangle.center", 1) => { let d = e - a; let (sx, sy) = (sign(d.x), sign(d.y)); vec![(2.0 * d.x.abs(), Vec2::new(a.x, a.y - d.y - sy * off)), (2.0 * d.y.abs(), Vec2::new(a.x + d.x + sx * 2.0 * off, a.y))] } - ("CircleCenterRadius", 1) => vec![(2.0 * (e - a).len(), (a + e) * 0.5 + dir_or_x(e - a).perp() * off)], - ("ArcCenterTwoPoint", 1) | ("ShapePolygonInscribed" | "ShapePolygonCircumscribed", 1) => { + ("sketch.circle.center", 1) => vec![(2.0 * (e - a).len(), (a + e) * 0.5 + dir_or_x(e - a).perp() * off)], + ("sketch.arc.center_point", 1) | ("sketch.polygon.inscribed" | "sketch.polygon.circumscribed", 1) => { vec![((e - a).len(), (a + e) * 0.5), (6.0, a + (e - a) * 0.5 + (e - a).perp() * 0.3)] } - ("ArcCenterTwoPoint", 2) => { + ("sketch.arc.center_point", 2) => { let a0 = (a - first).angle(); vec![(((e - first).angle() - a0).rem_euclid(std::f64::consts::TAU), e)] } - ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 2) => { + ("sketch.slot.center_to_center" | "sketch.slot.overall", 2) => { let dir = dir_or_x(a - first); vec![(2.0 * dir.cross(e - first).abs(), e)] } @@ -193,10 +193,10 @@ pub fn constraints(t: &Tool, curves: &[String]) -> Vec<(Vec, &'static st let mut out = Vec::new(); for b in t.dims.iter().filter(|b| b.locked) { let target = match (t.cmd, b.label) { - ("DrawPolyline", "Length") | ("ShapeRectangleTwoPoint" | "ShapeRectangleCenter", "Width") => curves.first().map(|c| (c, "length")), - ("ShapeRectangleTwoPoint" | "ShapeRectangleCenter", "Height") => curves.get(1).map(|c| (c, "length")), - ("CircleCenterRadius", "Diameter") => curves.first().map(|c| (c, "diameter")), - ("ArcCenterTwoPoint", "Radius") => curves.first().map(|c| (c, "radius")), + ("sketch.line", "Length") | ("sketch.rectangle.two_point" | "sketch.rectangle.center", "Width") => curves.first().map(|c| (c, "length")), + ("sketch.rectangle.two_point" | "sketch.rectangle.center", "Height") => curves.get(1).map(|c| (c, "length")), + ("sketch.circle.center", "Diameter") => curves.first().map(|c| (c, "diameter")), + ("sketch.arc.center_point", "Radius") => curves.first().map(|c| (c, "radius")), _ => None, }; if let Some((c, ty)) = target { diff --git a/crates/ui-egui/src/sketch_edit_tests.rs b/crates/ui-egui/src/sketch_edit_tests.rs index 0a5cce6d..d50cf9fb 100644 --- a/crates/ui-egui/src/sketch_edit_tests.rs +++ b/crates/ui-egui/src/sketch_edit_tests.rs @@ -60,7 +60,7 @@ fn toolbar_finish_and_other_feature_edit_leave_the_sketch() { let marker = app.session.doc.marker; let id = first_sketch(&app); app.edit_sketch(id); - app.start("SketchStop"); + app.start("sketch.finish"); assert_eq!(app.session.active_sketch, None); assert_eq!(app.session.doc.marker, marker); // Double-clicking a solid feature while sketching finishes the sketch first. diff --git a/crates/ui-egui/src/sketch_tools.rs b/crates/ui-egui/src/sketch_tools.rs index 3d512444..c9e3a44c 100644 --- a/crates/ui-egui/src/sketch_tools.rs +++ b/crates/ui-egui/src/sketch_tools.rs @@ -39,16 +39,16 @@ thread_local! { /// Commands registered on one panel that Fusion also lists on another: the analyses (sketch /// INSPECT) on SOLID INSPECT, and Interference (SOLID INSPECT) on sketch INSPECT. const ALSO_IN: &[(&str, &str, &str)] = &[ - ("SOLID", "INSPECT", "FusionZebraAnalysisCommand"), - ("SOLID", "INSPECT", "FusionDraftAnalysisCommand"), - ("SOLID", "INSPECT", "FusionCurvatureMapAnalysisCommand"), - ("SOLID", "INSPECT", "FusionEnvironmentMapAnalysisCommand"), - ("SOLID", "INSPECT", "FusionAccessibilityAnalysisCommand"), - ("SOLID", "INSPECT", "FusionMinimumRadiusAnalysisCommand"), - ("SOLID", "INSPECT", "FusionIsoCurveAnalysisCommand"), - ("SOLID", "INSPECT", "FusionCurvatureCombAnalysisCommand"), - ("SOLID", "INSPECT", "FusionCenterOfMassCommand"), - ("SKETCH", "INSPECT", "InterferenceCheckCommand"), + ("SOLID", "INSPECT", "inspect.zebra"), + ("SOLID", "INSPECT", "inspect.draft"), + ("SOLID", "INSPECT", "inspect.curvature_map"), + ("SOLID", "INSPECT", "inspect.environment_map"), + ("SOLID", "INSPECT", "inspect.accessibility"), + ("SOLID", "INSPECT", "inspect.minimum_radius"), + ("SOLID", "INSPECT", "inspect.isocurve"), + ("SOLID", "INSPECT", "inspect.curvature_comb"), + ("SOLID", "INSPECT", "inspect.center_of_mass"), + ("SKETCH", "INSPECT", "inspect.interference"), ]; /// A panel's commands plus the ones it shares with another panel (see [`ALSO_IN`]). @@ -74,19 +74,19 @@ pub fn also_in<'a>( fn analysis(app: &mut SolveApp, cmd: &str, p: Value) { let Ok(v) = app.run(cmd, p) else { return }; let mark = match cmd { - "FusionCenterOfMassCommand" => v3(&v["center"]).map(|c| (c, "Center of mass".to_string())), + "inspect.center_of_mass" => v3(&v["center"]).map(|c| (c, "Center of mass".to_string())), _ => v3(&v["at"]).zip(v["min_radius"].as_f64()).map(|(c, r)| (c, format!("R min {r:.3} mm"))), }; if let Some(m) = mark { MARKS.with(|k| k.borrow_mut().push(m)); - } else if cmd == "FusionMinimumRadiusAnalysisCommand" { + } else if cmd == "inspect.minimum_radius" { app.set_status("Minimum radius: it is straight (no curvature)", false); } } /// Isocurve analysis lines. fn iso_lines(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { - if app.tool.as_ref().map(|t| t.cmd) != Some("FusionIsoCurveAnalysisCommand") { + if app.tool.as_ref().map(|t| t.cmd) != Some("inspect.isocurve") { ISO.with(|i| i.borrow_mut().clear()); return; } @@ -104,7 +104,7 @@ fn iso_lines(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { /// Analysis markers: a target with its label. fn marks(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { - if !app.tool.as_ref().is_some_and(|t| matches!(t.cmd, "FusionMinimumRadiusAnalysisCommand" | "FusionCenterOfMassCommand")) { + if !app.tool.as_ref().is_some_and(|t| matches!(t.cmd, "inspect.minimum_radius" | "inspect.center_of_mass")) { MARKS.with(|m| m.borrow_mut().clear()); return; } @@ -128,19 +128,12 @@ pub fn start_hook(app: &mut SolveApp, id: &str) -> bool { // Surface analyses toggle: a second click turns the same one off. use solvecraft_engine::SurfaceAnalysis as A; let same = match (id, app.session.analysis) { - ("FusionZebraAnalysisCommand", Some(A::Zebra { .. })) => Some(true), - ("FusionDraftAnalysisCommand", Some(A::Draft { .. })) => Some(true), - ("FusionCurvatureMapAnalysisCommand", Some(A::Curvature { .. })) => Some(true), - ("FusionEnvironmentMapAnalysisCommand", Some(A::Environment)) => Some(true), - ("FusionAccessibilityAnalysisCommand", Some(A::Access { .. })) => Some(true), - ( - "FusionZebraAnalysisCommand" - | "FusionDraftAnalysisCommand" - | "FusionCurvatureMapAnalysisCommand" - | "FusionEnvironmentMapAnalysisCommand" - | "FusionAccessibilityAnalysisCommand", - _, - ) => Some(false), + ("inspect.zebra", Some(A::Zebra { .. })) => Some(true), + ("inspect.draft", Some(A::Draft { .. })) => Some(true), + ("inspect.curvature_map", Some(A::Curvature { .. })) => Some(true), + ("inspect.environment_map", Some(A::Environment)) => Some(true), + ("inspect.accessibility", Some(A::Access { .. })) => Some(true), + ("inspect.zebra" | "inspect.draft" | "inspect.curvature_map" | "inspect.environment_map" | "inspect.accessibility", _) => Some(false), _ => None, }; if let Some(on) = same { @@ -150,7 +143,7 @@ pub fn start_hook(app: &mut SolveApp, id: &str) -> bool { } return false; } - if id == "FusionCenterOfMassCommand" { + if id == "inspect.center_of_mass" { // The whole model at once; the tool stays for picking single bodies. MARKS.with(|m| m.borrow_mut().clear()); DEFERRED.with(|d| d.set(true)); @@ -163,7 +156,7 @@ pub fn start_hook(app: &mut SolveApp, id: &str) -> bool { } return false; } - if !matches!(id, "FusionAddCanvasCommand" | "FusionAddEditDecalCommand") { + if !matches!(id, "canvas.insert" | "canvas.decal") { return true; } let path = app.services.pick_open.as_ref().and_then(|f| f()); @@ -178,7 +171,7 @@ fn v3(v: &Value) -> Option { /// Curvature combs: teeth and the envelope through their tips. fn combs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { - if app.tool.as_ref().map(|t| t.cmd) != Some("FusionCurvatureCombAnalysisCommand") { + if app.tool.as_ref().map(|t| t.cmd) != Some("inspect.curvature_comb") { COMBS.with(|c| c.borrow_mut().clear()); return; } @@ -243,7 +236,7 @@ pub fn refine_snap( None => PREV.with(Cell::get), }; // Line tool: the tangent point of a circle or arc (before plain "on the curve"). - let line_tool = app.tool.as_ref().is_none_or(|t| t.cmd == "DrawPolyline"); + let line_tool = app.tool.as_ref().is_none_or(|t| t.cmd == "sketch.line"); if best.is_none() && line_tool && let Some(prev) = prev @@ -346,25 +339,25 @@ enum Mode { fn mode(id: &str) -> Option { Some(match id { - "ProjectNewCmd" | "IntersectCmd" | "Include3DGeometry" | "FitCurvesToSectionCommand" | "SketchIsoparametricCurve" => Mode::ModelRef, - "FusionCurvatureCombAnalysisCommand" | "FusionAddCanvasCommand" | "FusionAddEditDecalCommand" => Mode::ModelRef, - "FusionMinimumRadiusAnalysisCommand" | "FusionCenterOfMassCommand" | "FusionIsoCurveAnalysisCommand" => Mode::ModelRef, - "TrimSketchCmd" | "ExtendSketchCmd" | "BreakSketchCmd" => Mode::CurveAt, - "SketchMidpointLine" => Mode::Points(2), - "ArcTangent" => Mode::Points(2), - "CircleElipse" | "ConicCurveCmd" | "ShapeSlotCenterPoint" => Mode::Points(3), - "ShapeArcSlotThreePoint" | "ShapeArcSlotCenterTwoPoint" => Mode::Points(4), - "DrawSpline" | "DrawCVMSpline3D" | "DrawCVMSpline5D" => Mode::Points(0), - "MTextCmd" => Mode::Points(1), - "BlendG1CurveSketchCmd" => Mode::Points(2), - "Offset" => Mode::Entities(1), - "FilletSketchCmd" | "ChamferSketchEqualDistance" | "ChamferSketchDistanceAngle" | "ChamferSketchDistanceDistance" => Mode::Entities(1), - "ConstraintSmooth" | "CircleTanTanRadius" => Mode::Entities(2), - "CircleThreeTangent" => Mode::Entities(3), - "SketchConstrainer" | "SketchPolygonConstraintCmd" => Mode::Entities(0), + "sketch.project" | "sketch.intersect" | "sketch.include_3d" | "sketch.fit_curves_to_section" | "sketch.isoparametric_curve" => Mode::ModelRef, + "inspect.curvature_comb" | "canvas.insert" | "canvas.decal" => Mode::ModelRef, + "inspect.minimum_radius" | "inspect.center_of_mass" | "inspect.isocurve" => Mode::ModelRef, + "sketch.trim" | "sketch.extend" | "sketch.break" => Mode::CurveAt, + "sketch.line.midpoint" => Mode::Points(2), + "sketch.arc.tangent" => Mode::Points(2), + "sketch.ellipse" | "sketch.conic" | "sketch.slot.center_point" => Mode::Points(3), + "sketch.slot.arc_three_point" | "sketch.slot.arc_center" => Mode::Points(4), + "sketch.spline.fit_point" | "sketch.spline.control_point" | "sketch.spline.control_point_5" => Mode::Points(0), + "sketch.text" => Mode::Points(1), + "sketch.blend_curve" => Mode::Points(2), + "sketch.offset" => Mode::Entities(1), + "sketch.fillet" | "sketch.chamfer.equal_distance" | "sketch.chamfer.distance_angle" | "sketch.chamfer.two_distance" => Mode::Entities(1), + "sketch.constraint.curvature" | "sketch.circle.two_tangent" => Mode::Entities(2), + "sketch.circle.three_tangent" => Mode::Entities(3), + "sketch.constrainer" | "sketch.constraint.polygon" => Mode::Entities(0), "sketch.centerline" => Mode::Entities(1), - "MirrorSketchCommand" | "CircularSketchPatternCommand" | "RectangularSketchPatternCommand" => Mode::EntitiesThen, - "ProjectToSurface" | "IntersectionCurve" | "SpunProfileCmd" => Mode::Steps, + "sketch.mirror" | "sketch.pattern.circular" | "sketch.pattern.rectangular" => Mode::EntitiesThen, + "sketch.project_to_surface" | "sketch.intersection_curve" | "sketch.spun_profile" => Mode::Steps, _ => return None, }) } @@ -388,46 +381,48 @@ pub fn tool_for(id: &str) -> Option { pub fn hint(id: &str) -> Option { Some( match id { - "ProjectNewCmd" => "Project: click edges, faces, vertices, other sketches' curves, axes or planes", - "IntersectCmd" => "Intersect: click faces or edges to cut with the sketch plane", - "Include3DGeometry" => "Include 3D Geometry: click edges or vertices", - "FitCurvesToSectionCommand" => "Fit Curves to Mesh Section: click a body", - "SketchIsoparametricCurve" => "Isoparametric Curve: click a point on a face (Shift: along)", - "FusionCurvatureCombAnalysisCommand" => "Curvature comb: click sketch curves or model edges", - "FusionMinimumRadiusAnalysisCommand" => "Minimum radius: click sketch curves, edges or faces", - "FusionCenterOfMassCommand" => "Center of mass of all bodies; click a body for its own", - "FusionIsoCurveAnalysisCommand" => "Isocurves: click faces", - "FusionAddCanvasCommand" => "Canvas: click a plane or a planar face where the image's centre goes", - "FusionAddEditDecalCommand" => "Decal: click a planar face where the image's centre goes", - "TrimSketchCmd" => "Trim: click the piece of a curve to remove", - "ExtendSketchCmd" => "Extend: click a curve near the end to extend", - "BreakSketchCmd" => "Break: click a curve where it should split", - "SketchMidpointLine" => "Midpoint line: click the middle, then an end", - "ArcTangent" => "Tangent arc: click the end of a line or arc, then the arc's end", - "CircleElipse" => "Ellipse: click the centre, the end of the major axis, then a point on it", - "ConicCurveCmd" => "Conic: click the start, the end, then the apex", - "ShapeSlotCenterPoint" => "Slot: click the centre, an arc centre, then the width", - "ShapeArcSlotThreePoint" => "Arc slot: click start, a point on the arc, the end, then the width", - "ShapeArcSlotCenterTwoPoint" => "Arc slot: click the centre, the start, the end, then the width", - "DrawSpline" => "Spline: click fit points; right-click to finish", - "DrawCVMSpline3D" | "DrawCVMSpline5D" => "Control point spline: click control points; right-click to finish", - "MTextCmd" => "Text: click where the text starts", - "BlendG1CurveSketchCmd" => "Blend curve: click the ends of two curves", - "Offset" => "Offset: click a curve (its chain is offset), then the side and distance", - "FilletSketchCmd" => "Fillet: click the corner of two lines", - "ChamferSketchEqualDistance" | "ChamferSketchDistanceAngle" | "ChamferSketchDistanceDistance" => "Chamfer: click the corner of two lines", - "ConstraintSmooth" => "Curvature: click two curves that share an end", - "CircleTanTanRadius" => "2-tangent circle: click two curves (the circle goes near the second click)", - "CircleThreeTangent" => "3-tangent circle: click three curves", - "SketchConstrainer" => "Constrain: click one or two entities; right-click to apply", - "SketchPolygonConstraintCmd" => "Polygon: click the lines of a closed chain; right-click to apply", + "sketch.project" => "Project: click edges, faces, vertices, other sketches' curves, axes or planes", + "sketch.intersect" => "Intersect: click faces or edges to cut with the sketch plane", + "sketch.include_3d" => "Include 3D Geometry: click edges or vertices", + "sketch.fit_curves_to_section" => "Fit Curves to Mesh Section: click a body", + "sketch.isoparametric_curve" => "Isoparametric Curve: click a point on a face (Shift: along)", + "inspect.curvature_comb" => "Curvature comb: click sketch curves or model edges", + "inspect.minimum_radius" => "Minimum radius: click sketch curves, edges or faces", + "inspect.center_of_mass" => "Center of mass of all bodies; click a body for its own", + "inspect.isocurve" => "Isocurves: click faces", + "canvas.insert" => "Canvas: click a plane or a planar face where the image's centre goes", + "canvas.decal" => "Decal: click a planar face where the image's centre goes", + "sketch.trim" => "Trim: click the piece of a curve to remove", + "sketch.extend" => "Extend: click a curve near the end to extend", + "sketch.break" => "Break: click a curve where it should split", + "sketch.line.midpoint" => "Midpoint line: click the middle, then an end", + "sketch.arc.tangent" => "Tangent arc: click the end of a line or arc, then the arc's end", + "sketch.ellipse" => "Ellipse: click the centre, the end of the major axis, then a point on it", + "sketch.conic" => "Conic: click the start, the end, then the apex", + "sketch.slot.center_point" => "Slot: click the centre, an arc centre, then the width", + "sketch.slot.arc_three_point" => "Arc slot: click start, a point on the arc, the end, then the width", + "sketch.slot.arc_center" => "Arc slot: click the centre, the start, the end, then the width", + "sketch.spline.fit_point" => "Spline: click fit points; right-click to finish", + "sketch.spline.control_point" | "sketch.spline.control_point_5" => "Control point spline: click control points; right-click to finish", + "sketch.text" => "Text: click where the text starts", + "sketch.blend_curve" => "Blend curve: click the ends of two curves", + "sketch.offset" => "Offset: click a curve (its chain is offset), then the side and distance", + "sketch.fillet" => "Fillet: click the corner of two lines", + "sketch.chamfer.equal_distance" | "sketch.chamfer.distance_angle" | "sketch.chamfer.two_distance" => { + "Chamfer: click the corner of two lines" + } + "sketch.constraint.curvature" => "Curvature: click two curves that share an end", + "sketch.circle.two_tangent" => "2-tangent circle: click two curves (the circle goes near the second click)", + "sketch.circle.three_tangent" => "3-tangent circle: click three curves", + "sketch.constrainer" => "Constrain: click one or two entities; right-click to apply", + "sketch.constraint.polygon" => "Polygon: click the lines of a closed chain; right-click to apply", "sketch.centerline" => "Centerline: click lines", - "MirrorSketchCommand" => "Mirror: click entities, right-click, then click the mirror line", - "CircularSketchPatternCommand" => "Circular pattern: click entities, right-click, then click the centre", - "RectangularSketchPatternCommand" => "Rectangular pattern: click entities, right-click, then click two points (direction and spacing)", - "ProjectToSurface" => "Project to surface: click curves of other sketches, right-click, then click the face", - "IntersectionCurve" => "Intersection curve: click two faces", - "SpunProfileCmd" => "Spun profile: click the body, then the axis line", + "sketch.mirror" => "Mirror: click entities, right-click, then click the mirror line", + "sketch.pattern.circular" => "Circular pattern: click entities, right-click, then click the centre", + "sketch.pattern.rectangular" => "Rectangular pattern: click entities, right-click, then click two points (direction and spacing)", + "sketch.project_to_surface" => "Project to surface: click curves of other sketches, right-click, then click the face", + "sketch.intersection_curve" => "Intersection curve: click two faces", + "sketch.spun_profile" => "Spun profile: click the body, then the axis line", _ => return None, } .to_string(), @@ -521,7 +516,7 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { let at = sketch_point_at(app, proj, pos); let cmd = tool.cmd; match m { - Mode::ModelRef if cmd == "FusionCurvatureCombAnalysisCommand" => { + Mode::ModelRef if cmd == "inspect.curvature_comb" => { let params = hits.iter().find_map(|h| match h { Hit::SketchCurve { sketch, id, .. } => Some(json!({"sketch": sketch, "curves": [id]})), Hit::Edge { mid, .. } => Some(json!({"edges": [[mid.x, mid.y, mid.z]]})), @@ -537,7 +532,7 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { } } } - Mode::ModelRef if cmd == "FusionIsoCurveAnalysisCommand" => { + Mode::ModelRef if cmd == "inspect.isocurve" => { let face = hits.iter().find_map(|h| if let Hit::Face { point, .. } = h { Some(*point) } else { None }); if let Some(at) = face && let Ok(v) = app.run(cmd, json!({"faces": [[at.x, at.y, at.z]]})) @@ -549,21 +544,21 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { } } } - Mode::ModelRef if matches!(cmd, "FusionMinimumRadiusAnalysisCommand" | "FusionCenterOfMassCommand") => { + Mode::ModelRef if matches!(cmd, "inspect.minimum_radius" | "inspect.center_of_mass") => { let params = hits.iter().find_map(|h| match (cmd, h) { - ("FusionMinimumRadiusAnalysisCommand", Hit::SketchCurve { sketch, id, .. }) => Some(json!({"sketch": sketch, "curves": [id]})), - ("FusionMinimumRadiusAnalysisCommand", Hit::Edge { mid, .. }) => Some(json!({"edges": [[mid.x, mid.y, mid.z]]})), - ("FusionMinimumRadiusAnalysisCommand", Hit::Face { point, .. }) => Some(json!({"faces": [[point.x, point.y, point.z]]})), - ("FusionCenterOfMassCommand", Hit::Face { body, .. }) => Some(json!({"bodies": [body]})), + ("inspect.minimum_radius", Hit::SketchCurve { sketch, id, .. }) => Some(json!({"sketch": sketch, "curves": [id]})), + ("inspect.minimum_radius", Hit::Edge { mid, .. }) => Some(json!({"edges": [[mid.x, mid.y, mid.z]]})), + ("inspect.minimum_radius", Hit::Face { point, .. }) => Some(json!({"faces": [[point.x, point.y, point.z]]})), + ("inspect.center_of_mass", Hit::Face { body, .. }) => Some(json!({"bodies": [body]})), _ => None, }); if let Some(p) = params { analysis(app, cmd, p); } } - Mode::ModelRef if matches!(cmd, "FusionAddCanvasCommand" | "FusionAddEditDecalCommand") => { + Mode::ModelRef if matches!(cmd, "canvas.insert" | "canvas.decal") => { let Some(path) = IMAGE.with(|i| i.borrow().clone()) else { return }; - let decal = cmd == "FusionAddEditDecalCommand"; + let decal = cmd == "canvas.decal"; let target = hits.iter().find_map(|h| match h { Hit::Face { point, .. } => Some((json!({"face": [point.x, point.y, point.z]}), *point)), Hit::Plane { name, point } if !decal => Some((json!(name), *point)), @@ -599,12 +594,12 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { let r = hits.iter().find_map(|h| model_ref(app, h)); let Some(r) = r else { return }; match cmd { - "FitCurvesToSectionCommand" => { + "sketch.fit_curves_to_section" => { if let Some(b) = r.get("body").and_then(Value::as_str) { let _ = app.run(cmd, json!({"body": b})); } } - "SketchIsoparametricCurve" => { + "sketch.isoparametric_curve" => { if r.get("face").is_some() { let along = SHIFT.with(Cell::get); let _ = app.run(cmd, json!({"face": r["face"], "body": r["body"], "direction": if along { "v" } else { "u" }})); @@ -629,14 +624,14 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { } } Mode::Entities(n) => { - if cmd == "Offset" && !tool.picks.is_empty() { + if cmd == "sketch.offset" && !tool.picks.is_empty() { if let Some((p, _)) = at { offset_side(app, tool, p); } return; } let Some((id, is_point)) = sketch_entity(app, &hits) else { return }; - if cmd == "Offset" { + if cmd == "sketch.offset" { // First the curve, then the side point. if tool.picks.is_empty() && !is_point { tool.picks.push(id); @@ -645,7 +640,7 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { } return; } - if cmd.starts_with("Fillet") || cmd.starts_with("Chamfer") { + if cmd == "sketch.fillet" || cmd.starts_with("sketch.chamfer.") { if is_point { run_corner(app, cmd, &id); } @@ -672,13 +667,13 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { if tool.picks.last().is_some_and(|x| x == "|") { let ents: Vec = tool.picks.iter().filter(|x| *x != "|").cloned().collect(); match cmd { - "MirrorSketchCommand" => { + "sketch.mirror" => { if let Some((line, false)) = sketch_entity(app, &hits) { let _ = app.run(cmd, json!({"entities": ents, "line": line})); tool.picks.clear(); } } - "CircularSketchPatternCommand" => { + "sketch.pattern.circular" => { if let Some(p) = at { let _ = app.run(cmd, json!({"entities": ents, "center": arg(&p), "count": 6})); tool.picks.clear(); @@ -707,7 +702,7 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { } } Mode::Steps => match cmd { - "IntersectionCurve" => { + "sketch.intersection_curve" => { if let Some(r) = hits.iter().find_map(|h| model_ref(app, h)).filter(|r| r.get("face").is_some()) { tool.picks.push(r.to_string()); if tool.picks.len() >= 2 { @@ -718,7 +713,7 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { } } } - "SpunProfileCmd" => { + "sketch.spun_profile" => { if tool.picks.is_empty() { if let Some(b) = hits.iter().find_map(|h| model_ref(app, h)).and_then(|r| r.get("body").and_then(Value::as_str).map(str::to_string)) @@ -731,7 +726,7 @@ pub fn on_click(app: &mut SolveApp, tool: &mut Tool, proj: &Proj, pos: Pos2) { } } _ => { - // ProjectToSurface: curves of other sketches, then (after a right-click) a face. + // sketch.project_to_surface: curves of other sketches, then (after a right-click) a face. let staged = tool.picks.last().is_some_and(|x| x == "|"); if staged { if let Some(r) = hits.iter().find_map(|h| model_ref(app, h)).filter(|r| r.get("face").is_some()) { @@ -751,26 +746,26 @@ fn run_points(app: &mut SolveApp, t: &Tool) { let p = &t.pts; let width = |a: Vec2, b: Vec2, c: Vec2| -> f64 { 2.0 * (b - a).normalized().map(|d| d.cross(c - a).abs()).unwrap_or(1.0) }; let params = match (t.cmd, p.as_slice()) { - ("SketchMidpointLine", [m, e]) => json!({"mid": arg(m), "end": xy(e.0)}), - ("ArcTangent", [s, e]) => json!({"start": arg(s), "end": xy(e.0)}), - ("CircleElipse", [c, m, q]) => json!({"center": xy(c.0), "major": xy(m.0), "minor": xy(q.0)}), - ("ConicCurveCmd", [a, b, x]) => json!({"start": arg(a), "end": arg(b), "apex": arg(x)}), - ("ShapeSlotCenterPoint", [c, e, w]) => json!({"center": xy(c.0), "end": xy(e.0), "width": width(c.0, e.0, w.0).max(1e-3)}), - ("ShapeArcSlotThreePoint", [a, th, b, w]) => { + ("sketch.line.midpoint", [m, e]) => json!({"mid": arg(m), "end": xy(e.0)}), + ("sketch.arc.tangent", [s, e]) => json!({"start": arg(s), "end": xy(e.0)}), + ("sketch.ellipse", [c, m, q]) => json!({"center": xy(c.0), "major": xy(m.0), "minor": xy(q.0)}), + ("sketch.conic", [a, b, x]) => json!({"start": arg(a), "end": arg(b), "apex": arg(x)}), + ("sketch.slot.center_point", [c, e, w]) => json!({"center": xy(c.0), "end": xy(e.0), "width": width(c.0, e.0, w.0).max(1e-3)}), + ("sketch.slot.arc_three_point", [a, th, b, w]) => { json!({"start": xy(a.0), "through": xy(th.0), "end": xy(b.0), "width": (2.0 * w.0.dist(b.0)).max(1e-3)}) } - ("ShapeArcSlotCenterTwoPoint", [c, a, b, w]) => { + ("sketch.slot.arc_center", [c, a, b, w]) => { let r = c.0.dist(a.0); json!({"center": xy(c.0), "start": xy(a.0), "end": xy(b.0), "width": (2.0 * (w.0.dist(c.0) - r).abs()).max(1e-3)}) } - ("DrawSpline" | "DrawCVMSpline3D" | "DrawCVMSpline5D", pts) if pts.len() >= 2 => { + ("sketch.spline.fit_point" | "sketch.spline.control_point" | "sketch.spline.control_point_5", pts) if pts.len() >= 2 => { json!({"points": pts.iter().map(arg).collect::>()}) } - ("BlendG1CurveSketchCmd", [a, b]) => match (&a.1, &b.1) { + ("sketch.blend_curve", [a, b]) => match (&a.1, &b.1) { (Some(x), Some(y)) => json!({"a": x, "b": y}), _ => return, }, - ("MTextCmd", [at]) => { + ("sketch.text", [at]) => { let at = at.0; TEXT.with(|t| *t.borrow_mut() = Some((at, String::new()))); return; @@ -783,9 +778,9 @@ fn run_points(app: &mut SolveApp, t: &Tool) { fn run_corner(app: &mut SolveApp, cmd: &str, point: &str) { let size = round_nice(corner_len(app, point) * 0.2); let params = match cmd { - "FilletSketchCmd" => json!({"point": point, "radius": size}), - "ChamferSketchDistanceAngle" => json!({"point": point, "distance": size, "angle": 45}), - "ChamferSketchDistanceDistance" => json!({"point": point, "distance": size, "distance2": size}), + "sketch.fillet" => json!({"point": point, "radius": size}), + "sketch.chamfer.distance_angle" => json!({"point": point, "distance": size, "angle": 45}), + "sketch.chamfer.two_distance" => json!({"point": point, "distance": size, "distance2": size}), _ => json!({"point": point, "distance": size}), }; if let Ok(v) = app.run(cmd, params) @@ -798,19 +793,19 @@ fn run_corner(app: &mut SolveApp, cmd: &str, point: &str) { fn run_entities(app: &mut SolveApp, t: &Tool) { let k = &t.picks; let params = match t.cmd { - "ConstraintSmooth" => json!({"a": k[0], "b": k[1]}), - "CircleTanTanRadius" => { + "sketch.constraint.curvature" => json!({"a": k[0], "b": k[1]}), + "sketch.circle.two_tangent" => { let r = round_nice(curve_len(app, &k[0]).min(curve_len(app, &k[1])) * 0.1); let near = t.pts.last().map(|p| xy(p.0)).unwrap_or(json!([0, 0])); json!({"curves": k, "radius": r, "near": near}) } - "CircleThreeTangent" => { + "sketch.circle.three_tangent" => { // Start inside the three picks. let c = t.pts.iter().fold(Vec2::ZERO, |a, p| a + p.0) / t.pts.len().max(1) as f64; json!({"curves": k, "near": xy(c)}) } - "SketchConstrainer" => json!({"entities": k}), - "SketchPolygonConstraintCmd" => json!({"lines": k}), + "sketch.constrainer" => json!({"entities": k}), + "sketch.constraint.polygon" => json!({"lines": k}), _ => return, }; let _ = app.run(t.cmd, params); @@ -847,7 +842,7 @@ pub fn finish(app: &mut SolveApp, tool: &mut Tool) -> bool { /// Offset: the second click sets the side and distance. fn offset_side(app: &mut SolveApp, tool: &mut Tool, p: Vec2) -> bool { - if tool.cmd != "Offset" || tool.picks.is_empty() { + if tool.cmd != "sketch.offset" || tool.picks.is_empty() { return false; } let st = app.session.world_state(); @@ -857,7 +852,7 @@ fn offset_side(app: &mut SolveApp, tool: &mut Tool, p: Vec2) -> bool { .map(|sh| sh.dist(p)) .unwrap_or(0.0); if d > 1e-6 { - let _ = app.run("Offset", json!({"curves": [tool.picks[0]], "distance": d, "side": xy(p)})); + let _ = app.run("sketch.offset", json!({"curves": [tool.picks[0]], "distance": d, "side": xy(p)})); } tool.picks.clear(); true @@ -1011,7 +1006,7 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P snap_hint(app, painter, proj); combs(app, painter, proj); if DEFERRED.with(|d| d.replace(false)) { - analysis(app, "FusionCenterOfMassCommand", json!({})); + analysis(app, "inspect.center_of_mass", json!({})); } marks(app, painter, proj); iso_lines(app, painter, proj); @@ -1042,7 +1037,7 @@ fn text_entry(app: &mut SolveApp, ctx: &egui::Context) { Some(true) => { if !text.trim().is_empty() { let h = round_nice(app.cam.half_height() * 0.1); - let _ = app.run("MTextCmd", json!({"text": text, "at": xy(at), "height": h})); + let _ = app.run("sketch.text", json!({"text": text, "at": xy(at), "height": h})); } } Some(false) => {} diff --git a/crates/ui-egui/src/timeline.rs b/crates/ui-egui/src/timeline.rs index c60cce47..7d18d71c 100644 --- a/crates/ui-egui/src/timeline.rs +++ b/crates/ui-egui/src/timeline.rs @@ -62,7 +62,7 @@ fn delete_feature(app: &mut SolveApp, id: u64) { }; let (with, fail) = (list("deleted_with_it"), list("would_fail")); if with.is_empty() && fail.is_empty() { - let _ = app.run("FusionDeleteCommand", json!({ "features": [id.to_string()] })); + let _ = app.run("timeline.delete", json!({ "features": [id.to_string()] })); } else { app.dialog = Some(crate::dialogs::Dialog::confirm_delete(id, with, fail)); } @@ -99,7 +99,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) { Some(k) if k < n => json!({"position": k}), _ => json!({}), }; - let _ = app.run("timeline.rollTo", pos); + let _ = app.run("timeline.roll_to", pos); } x += 24.0; } @@ -223,7 +223,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) { ui.close(); } if ui.button("Roll History Marker Here").clicked() { - let _ = app.run("timeline.rollTo", json!({ "feature": id })); + let _ = app.run("timeline.roll_to", json!({ "feature": id })); ui.close(); } if ui.button("Find in Browser").clicked() { @@ -271,7 +271,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) { let slot = slot_at(&xs, end, pp.x); match dg { Drag::Marker => { - let _ = app.run("timeline.rollTo", if slot >= n { json!({}) } else { json!({ "position": slot }) }); + let _ = app.run("timeline.roll_to", if slot >= n { json!({}) } else { json!({ "position": slot }) }); } Drag::Feature(id) => { let from = app.session.doc.feature_index(id).unwrap_or(0); diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index c6dc31b5..ed4f5588 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -73,10 +73,10 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { save(app); } if click(ui, "undo", "Undo (Ctrl+Z)") { - let _ = app.run("UndoCommand", json!({})); + let _ = app.run("edit.undo", json!({})); } if click(ui, "redo", "Redo (Ctrl+Y)") { - let _ = app.run("RedoCommand", json!({})); + let _ = app.run("edit.redo", json!({})); } if click(ui, "home", "Start page") { let rev = app.session.revision; @@ -151,11 +151,11 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) { close = true; } if item(ui, "Insert STEP…", "") { - app.start("FusionImportCommandFromToolbar"); + app.start("file.insert_step"); close = true; } if item(ui, "Insert Mesh…", "") { - app.start("ParaMeshInsertAlignCommand"); + app.start("file.insert_mesh"); close = true; } if item(ui, "Save", "Ctrl+S") { @@ -211,7 +211,7 @@ pub fn save(app: &mut SolveApp) { return; } if app.session.path.is_some() { - let _ = app.run("SaveDocumentCommand", json!({})); + let _ = app.run("file.save", json!({})); } else { save_as(app); } @@ -220,14 +220,14 @@ pub fn save(app: &mut SolveApp) { fn save_as(app: &mut SolveApp) { let name = format!("{}.{}", app.session.doc.name, solvecraft_engine::io::DESIGN_EXT); if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name, &[solvecraft_engine::io::DESIGN_EXT])) { - let _ = app.run("SaveDocumentAsCommand", json!({"path": p})); + let _ = app.run("file.save_as", json!({"path": p})); } } fn export(app: &mut SolveApp, ext: &str) { let name = format!("{}.{ext}", app.session.doc.name); if let Some(p) = app.services.pick_save.as_ref().and_then(|f| f(&name, &[ext])) - && app.run("ExportCommand", json!({"path": p})).is_ok() + && app.run("file.export", json!({"path": p})).is_ok() { app.set_status(format!("Exported {p}"), false); } @@ -305,7 +305,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { } else { (t.border, t.panel_header, t.border) }; - let icon = if c.id == "SketchStop" { "finish" } else { c.icon }; + let icon = if c.id == "sketch.finish" { "finish" } else { c.icon }; icons::paint(&painter, br.shrink(6.0), icon, ink, fill, acc); let tip = match &info.disabled_reason { Some(why) => format!("{}\n{why}", c.label), @@ -429,8 +429,8 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) { use egui::Key; if mods.command { match k { - Key::Z => drop(app.run("UndoCommand", json!({}))), - Key::Y => drop(app.run("RedoCommand", json!({}))), + Key::Z => drop(app.run("edit.undo", json!({}))), + Key::Y => drop(app.run("edit.redo", json!({}))), Key::S => save(app), Key::N => crate::documents::new_design(app), Key::O => { diff --git a/crates/ui-egui/src/tools.rs b/crates/ui-egui/src/tools.rs index 37db9d3b..df8a828f 100644 --- a/crates/ui-egui/src/tools.rs +++ b/crates/ui-egui/src/tools.rs @@ -38,33 +38,33 @@ pub struct Tool { impl Tool { pub fn for_command(id: &str) -> Option { let (cmd, kind): (&'static str, Kind) = match id { - "DrawPolyline" => ("DrawPolyline", Kind::Draw(0)), - "ShapeRectangleTwoPoint" => ("ShapeRectangleTwoPoint", Kind::Draw(2)), - "ShapeRectangleCenter" => ("ShapeRectangleCenter", Kind::Draw(2)), - "ShapeRectangleThreePoint" => ("ShapeRectangleThreePoint", Kind::Draw(3)), - "CircleCenterRadius" => ("CircleCenterRadius", Kind::Draw(2)), - "CircleTwoPoint" => ("CircleTwoPoint", Kind::Draw(2)), - "CircleThreePoint" => ("CircleThreePoint", Kind::Draw(3)), - "ArcThreePoint" => ("ArcThreePoint", Kind::Draw(3)), - "ArcCenterTwoPoint" => ("ArcCenterTwoPoint", Kind::Draw(3)), - "ShapePolygonInscribed" => ("ShapePolygonInscribed", Kind::Draw(2)), - "ShapePolygonCircumscribed" => ("ShapePolygonCircumscribed", Kind::Draw(2)), - "ShapePolygonEdge" => ("ShapePolygonEdge", Kind::Draw(2)), - "ShapeSlotCenterToCenter" => ("ShapeSlotCenterToCenter", Kind::Draw(3)), - "ShapeSlotOverall" => ("ShapeSlotOverall", Kind::Draw(3)), - "DrawPoint" => ("DrawPoint", Kind::Draw(1)), - "SketchDimension" => ("SketchDimension", Kind::Dimension), - "ConstraintHorizontalVertical" => ("ConstraintHorizontalVertical", Kind::Pick(1)), - "ConstraintFix" => ("ConstraintFix", Kind::Pick(1)), - "ConstraintCoincident" => ("ConstraintCoincident", Kind::Pick(2)), - "ConstraintTangent" => ("ConstraintTangent", Kind::Pick(2)), - "ConstraintEqual" => ("ConstraintEqual", Kind::Pick(2)), - "ConstraintParallel" => ("ConstraintParallel", Kind::Pick(2)), - "ConstraintPerpendicular" => ("ConstraintPerpendicular", Kind::Pick(2)), - "ConstraintConcentric" => ("ConstraintConcentric", Kind::Pick(2)), - "ConstraintCollinear" => ("ConstraintCollinear", Kind::Pick(2)), - "ConstraintMidPoint" => ("ConstraintMidPoint", Kind::Pick(2)), - "ConstraintSymmetry" => ("ConstraintSymmetry", Kind::Pick(3)), + "sketch.line" => ("sketch.line", Kind::Draw(0)), + "sketch.rectangle.two_point" => ("sketch.rectangle.two_point", Kind::Draw(2)), + "sketch.rectangle.center" => ("sketch.rectangle.center", Kind::Draw(2)), + "sketch.rectangle.three_point" => ("sketch.rectangle.three_point", Kind::Draw(3)), + "sketch.circle.center" => ("sketch.circle.center", Kind::Draw(2)), + "sketch.circle.two_point" => ("sketch.circle.two_point", Kind::Draw(2)), + "sketch.circle.three_point" => ("sketch.circle.three_point", Kind::Draw(3)), + "sketch.arc.three_point" => ("sketch.arc.three_point", Kind::Draw(3)), + "sketch.arc.center_point" => ("sketch.arc.center_point", Kind::Draw(3)), + "sketch.polygon.inscribed" => ("sketch.polygon.inscribed", Kind::Draw(2)), + "sketch.polygon.circumscribed" => ("sketch.polygon.circumscribed", Kind::Draw(2)), + "sketch.polygon.edge" => ("sketch.polygon.edge", Kind::Draw(2)), + "sketch.slot.center_to_center" => ("sketch.slot.center_to_center", Kind::Draw(3)), + "sketch.slot.overall" => ("sketch.slot.overall", Kind::Draw(3)), + "sketch.point" => ("sketch.point", Kind::Draw(1)), + "sketch.dimension" => ("sketch.dimension", Kind::Dimension), + "sketch.constraint.horizontal_vertical" => ("sketch.constraint.horizontal_vertical", Kind::Pick(1)), + "sketch.constraint.fix" => ("sketch.constraint.fix", Kind::Pick(1)), + "sketch.constraint.coincident" => ("sketch.constraint.coincident", Kind::Pick(2)), + "sketch.constraint.tangent" => ("sketch.constraint.tangent", Kind::Pick(2)), + "sketch.constraint.equal" => ("sketch.constraint.equal", Kind::Pick(2)), + "sketch.constraint.parallel" => ("sketch.constraint.parallel", Kind::Pick(2)), + "sketch.constraint.perpendicular" => ("sketch.constraint.perpendicular", Kind::Pick(2)), + "sketch.constraint.concentric" => ("sketch.constraint.concentric", Kind::Pick(2)), + "sketch.constraint.collinear" => ("sketch.constraint.collinear", Kind::Pick(2)), + "sketch.constraint.midpoint" => ("sketch.constraint.midpoint", Kind::Pick(2)), + "sketch.constraint.symmetry" => ("sketch.constraint.symmetry", Kind::Pick(3)), _ => return crate::sketch_tools::tool_for(id), }; Some(Tool { cmd, kind, pts: Vec::new(), hover: None, picks: Vec::new(), dims: Vec::new(), dims_stage: usize::MAX, dims_focus: false }) @@ -73,16 +73,16 @@ impl Tool { pub fn hint(id: &str) -> String { match id { - "DrawPolyline" => "Line: click points; right-click or Esc to finish".into(), - "ShapeRectangleTwoPoint" => "Rectangle: click two opposite corners".into(), - "ShapeRectangleCenter" => "Center rectangle: click the centre, then a corner".into(), - "ShapeRectangleThreePoint" => "3-point rectangle: click two points of one edge, then the width".into(), - "CircleCenterRadius" => "Circle: click the centre, then a point on the circle".into(), - "ArcThreePoint" => "Arc: click start, end, then a point on the arc".into(), - "ArcCenterTwoPoint" => "Arc: click centre, start, then end (counter-clockwise)".into(), - "SketchDimension" => "Dimension: pick a line, circle or arc (or two points/lines)".into(), - "ShapeSlotCenterToCenter" | "ShapeSlotOverall" => "Slot: click both ends, then the width".into(), - _ if id.starts_with("Constraint") => "Constraint: pick the sketch entities".into(), + "sketch.line" => "Line: click points; right-click or Esc to finish".into(), + "sketch.rectangle.two_point" => "Rectangle: click two opposite corners".into(), + "sketch.rectangle.center" => "Center rectangle: click the centre, then a corner".into(), + "sketch.rectangle.three_point" => "3-point rectangle: click two points of one edge, then the width".into(), + "sketch.circle.center" => "Circle: click the centre, then a point on the circle".into(), + "sketch.arc.three_point" => "Arc: click start, end, then a point on the arc".into(), + "sketch.arc.center_point" => "Arc: click centre, start, then end (counter-clockwise)".into(), + "sketch.dimension" => "Dimension: pick a line, circle or arc (or two points/lines)".into(), + "sketch.slot.center_to_center" | "sketch.slot.overall" => "Slot: click both ends, then the width".into(), + _ if id.starts_with("sketch.constraint.") => "Constraint: pick the sketch entities".into(), _ => crate::sketch_tools::hint(id).unwrap_or_else(|| "Click in the sketch".into()), } } @@ -117,7 +117,7 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) { Kind::Draw(_) => { if let Some(mut p) = sketch_point_at(app, proj, pos) { // Typed values win over the cursor; lines snap to horizontal/vertical. - if tool.dims.iter().any(|b| b.locked) || (tool.cmd == "DrawPolyline" && p.1.is_none()) { + if tool.dims.iter().any(|b| b.locked) || (tool.cmd == "sketch.line" && p.1.is_none()) { p = (crate::sketch_dims::effective(app, &tool, p.0), None); } app.tool = Some(tool); @@ -159,7 +159,7 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) { let single_line = tool.picks.len() == 1 && is_line == Some(true); if ready || single_line { // A single line gets its length right away; a second pick (line or point) replaces it. - if let Ok(v) = app.run("SketchDimension", json!({"entities": tool.picks})) + if let Ok(v) = app.run("sketch.dimension", json!({"entities": tool.picks})) && let Some(p) = v["param"].as_str() { // The value is edited in place, selected so typing replaces it. @@ -182,10 +182,10 @@ pub fn place(app: &mut SolveApp, p: (Vec2, Option)) { return; }; tool.pts.push(p); - if tool.cmd == "DrawPolyline" { + if tool.cmd == "sketch.line" { if tool.pts.len() >= 2 { let (a, b) = (tool.pts[tool.pts.len() - 2].clone(), tool.pts[tool.pts.len() - 1].clone()); - if let Ok(v) = app.run("DrawPolyline", json!({"points": [arg(&a), arg(&b)], "infer": true})) { + if let Ok(v) = app.run("sketch.line", json!({"points": [arg(&a), arg(&b)], "infer": true})) { let curves: Vec = v["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect(); dimension(app, &tool, &curves); // Chain: the next segment starts at this one's end point. @@ -207,7 +207,7 @@ pub fn place(app: &mut SolveApp, p: (Vec2, Option)) { /// Typed values become dimension constraints on the new curves. fn dimension(app: &mut SolveApp, t: &Tool, curves: &[String]) { for (ents, ty, value) in crate::sketch_dims::constraints(t, curves) { - let _ = app.run("SketchDimension", crate::sketch_dims::dimension_params(&ents, ty, &value)); + let _ = app.run("sketch.dimension", crate::sketch_dims::dimension_params(&ents, ty, &value)); } } @@ -216,25 +216,25 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Vec { let d = |a: usize, b: usize| p[a].0.dist(p[b].0); let sides = crate::sketch_dims::locked_value(app, t, "Sides").map_or(6, |v| v.round().clamp(3.0, 200.0) as usize); let params = match t.cmd { - "ShapeRectangleTwoPoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1])}), - "ShapeRectangleCenter" => json!({"center": xy(&p[0]), "corner": xy(&p[1])}), - "ShapeRectangleThreePoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "p2": xy(&p[2])}), - "CircleCenterRadius" => json!({"center": arg(&p[0]), "radius": d(0, 1)}), - "CircleTwoPoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1])}), - "CircleThreePoint" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "p2": xy(&p[2])}), - "ArcThreePoint" => json!({"start": arg(&p[0]), "end": arg(&p[1]), "through": xy(&p[2])}), - "ArcCenterTwoPoint" => json!({"center": arg(&p[0]), "start": arg(&p[1]), "end": xy(&p[2])}), - "ShapePolygonInscribed" | "ShapePolygonCircumscribed" => { + "sketch.rectangle.two_point" => json!({"p0": xy(&p[0]), "p1": xy(&p[1])}), + "sketch.rectangle.center" => json!({"center": xy(&p[0]), "corner": xy(&p[1])}), + "sketch.rectangle.three_point" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "p2": xy(&p[2])}), + "sketch.circle.center" => json!({"center": arg(&p[0]), "radius": d(0, 1)}), + "sketch.circle.two_point" => json!({"p0": xy(&p[0]), "p1": xy(&p[1])}), + "sketch.circle.three_point" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "p2": xy(&p[2])}), + "sketch.arc.three_point" => json!({"start": arg(&p[0]), "end": arg(&p[1]), "through": xy(&p[2])}), + "sketch.arc.center_point" => json!({"center": arg(&p[0]), "start": arg(&p[1]), "end": xy(&p[2])}), + "sketch.polygon.inscribed" | "sketch.polygon.circumscribed" => { let a = (p[1].0 - p[0].0).angle().to_degrees(); json!({"center": xy(&p[0]), "radius": d(0, 1), "sides": sides, "angle": a}) } - "ShapePolygonEdge" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "sides": sides}), - "ShapeSlotCenterToCenter" | "ShapeSlotOverall" => { + "sketch.polygon.edge" => json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "sides": sides}), + "sketch.slot.center_to_center" | "sketch.slot.overall" => { let dir = (p[1].0 - p[0].0).normalized().unwrap_or(Vec2::X); let w = 2.0 * dir.cross(p[2].0 - p[0].0).abs(); json!({"p0": xy(&p[0]), "p1": xy(&p[1]), "width": w.max(1e-3)}) } - "DrawPoint" => json!({"point": xy(&p[0])}), + "sketch.point" => json!({"point": xy(&p[0])}), _ => return Vec::new(), }; let curves: Vec = app @@ -270,18 +270,18 @@ fn tie_snaps(app: &mut SolveApp, t: &Tool, curves: &[String]) { } } for (a, b) in ties { - let _ = app.run("ConstraintCoincident", json!({"a": a, "b": b})); + let _ = app.run("sketch.constraint.coincident", json!({"a": a, "b": b})); } } fn run_pick(app: &mut SolveApp, t: &Tool) { let k = &t.picks; let params = match t.cmd { - "ConstraintHorizontalVertical" => json!({"line": k[0]}), - "ConstraintFix" => json!({"entity": k[0]}), - "ConstraintCoincident" => json!({"a": k[0], "b": k[1]}), - "ConstraintMidPoint" => json!({"point": k[0], "line": k[1]}), - "ConstraintSymmetry" => json!({"a": k[0], "b": k[1], "line": k[2]}), + "sketch.constraint.horizontal_vertical" => json!({"line": k[0]}), + "sketch.constraint.fix" => json!({"entity": k[0]}), + "sketch.constraint.coincident" => json!({"a": k[0], "b": k[1]}), + "sketch.constraint.midpoint" => json!({"point": k[0], "line": k[1]}), + "sketch.constraint.symmetry" => json!({"a": k[0], "b": k[1], "line": k[2]}), _ => json!({"a": k[0], "b": k[1]}), }; let _ = app.run(t.cmd, params); @@ -347,7 +347,7 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) { painter.add(Shape::line(v, stroke)); }; match (t.cmd, pts.as_slice()) { - ("DrawPolyline", [.., last]) => { + ("sketch.line", [.., last]) => { line(*last, h); // Horizontal/vertical inference marker. if let Some(horizontal) = crate::sketch_dims::axis_aligned(*last, h) @@ -363,14 +363,14 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) { ); } } - ("ShapeRectangleTwoPoint", [a]) => { + ("sketch.rectangle.two_point", [a]) => { let (b, c) = (Vec2::new(h.x, a.y), Vec2::new(a.x, h.y)); line(*a, b); line(b, h); line(h, c); line(c, *a); } - ("ShapeRectangleCenter", [c]) => { + ("sketch.rectangle.center", [c]) => { let d = h - *c; let k = [Vec2::new(c.x - d.x, c.y - d.y), Vec2::new(c.x + d.x, c.y - d.y), Vec2::new(c.x + d.x, c.y + d.y), Vec2::new(c.x - d.x, c.y + d.y)]; @@ -378,8 +378,8 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) { line(k[i], k[(i + 1) % 4]); } } - ("CircleCenterRadius" | "ShapePolygonInscribed" | "ShapePolygonCircumscribed", [c]) => circle(*c, c.dist(h)), - ("CircleTwoPoint", [a]) => circle((*a + h) * 0.5, a.dist(h) * 0.5), + ("sketch.circle.center" | "sketch.polygon.inscribed" | "sketch.polygon.circumscribed", [c]) => circle(*c, c.dist(h)), + ("sketch.circle.two_point", [a]) => circle((*a + h) * 0.5, a.dist(h) * 0.5), (_, [a]) => line(*a, h), (_, [a, b]) => { line(*a, *b); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index e55084e5..d90e176a 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -705,7 +705,7 @@ pub fn section_plane(app: &SolveApp) -> Option<(Vec3, Vec3)> { let cmds = crate::dialogs::apply_commands(app, d).ok()?; let (_, p) = cmds.into_iter().next()?; let mut scratch = app.session.scratch(); - scratch.execute("FusionHalfSectionViewCommand", &p).ok()?; + scratch.execute("inspect.section", &p).ok()?; return scratch.section; } // Slice (Sketch Palette): cut the model at the plane of the sketch being edited. diff --git a/crates/ui-egui/src/workspace.rs b/crates/ui-egui/src/workspace.rs index 3b3005ba..6e8e88a2 100644 --- a/crates/ui-egui/src/workspace.rs +++ b/crates/ui-egui/src/workspace.rs @@ -8,83 +8,83 @@ use solvecraft_engine::CommandSpec; use solvecraft_engine::doc::FeatureKind; const ASSEMBLE: &[&str] = &[ - "FusionCreateNewComponentCommand", - "JointAssembleCmdNew", - "JointAsBuiltCmd", - "JointOrigin", - "RigidGroupCmd", - "FusionMoveJointsCommand", - "FusionMotionRelationshipCommand", - "FusionMotionStudyCommand", + "component.create", + "joint.create", + "joint.as_built", + "joint.origin", + "joint.rigid_group", + "joint.drive", + "joint.motion_link", + "motion.study", "explode.create", - "EnableContactSetsCmd", - "EnableAllContactCmd", - "DisableAllContactCmd", - "ContactSetCmd", - "InterferenceCheckCommand", + "contact.enable_sets", + "contact.enable_all", + "contact.disable_all", + "contact.create", + "inspect.interference", ]; const SHEET_CREATE: &[&str] = &[ - "SketchCreate", - "FusionSheetMetalFlangeCommand", - "FusionSheetMetalFlatPatternCmd", - "FusionSheetMetalHemFlangeCommand", - "SheetMetalFoldCmd", - "ConvertToSheetMetalCmd", - "FusionCreateNewComponentCommand", - "Extrude", - "FusionHoleCommand", - "FusionThreadCommand", - "PatternRectangular", - "PatternCircular", - "PatternOnPath", - "MirrorCommand", - "JointOrigin", + "sketch.create", + "sheet.flange", + "sheet.flat_pattern", + "sheet.hem", + "sheet.fold", + "sheet.convert", + "component.create", + "solid.extrude", + "solid.hole", + "solid.thread", + "solid.pattern.rectangular", + "solid.pattern.circular", + "solid.pattern.path", + "solid.mirror", + "joint.origin", ]; const SHEET_MODIFY: &[&str] = &[ - "FusionSheetMetalRulesCommand", - "FusionSheetmetalUnfoldCommand", + "sheet.manage_rules", + "sheet.unfold", "sheet.refold", - "FusionFilletEdgesCommand", - "FusionChamferCommand", - "FusionMoveCommand", - "AlignCmd", - "FusionDeleteCommand", - "SoftDeleteCommand", - "PhysicalMaterialCommand", - "ChangeParameterCommand", + "solid.fillet", + "solid.chamfer", + "solid.move", + "solid.align", + "timeline.delete", + "solid.remove", + "material.assign", + "parameters.change", ]; -const PLASTIC_SETUP: &[&str] = &["FusionManagePlasticRuleCommand", "FusionAssignPlasticRuleCommand"]; +const PLASTIC_SETUP: &[&str] = &["plastic.manage_rules", "plastic.assign_rule"]; const PLASTIC_CREATE: &[&str] = &[ - "SketchCreate", - "FusionBossCommand", - "FusionSnapFitCommand", - "FusionLipCommand", - "FusionRestCommand", - "FusionCreateNewComponentCommand", - "Extrude", - "Revolve", - "Sweep", - "SolidLoft", - "FusionRibCommand", - "FusionWebCommand", - "EmbossCmd", - "FusionHoleCommand", - "FusionThreadCommand", - "PrimitiveBox", - "PrimitiveCylinder", - "PrimitiveSphere", - "PrimitiveTorus", - "PrimitiveCoil", - "PrimitivePipe", - "PatternRectangular", - "PatternCircular", - "PatternOnPath", - "MirrorCommand", - "JointOrigin", + "sketch.create", + "plastic.boss", + "plastic.snap_fit", + "plastic.lip", + "plastic.rest", + "component.create", + "solid.extrude", + "solid.revolve", + "solid.sweep", + "solid.loft", + "solid.rib", + "solid.web", + "solid.emboss", + "solid.hole", + "solid.thread", + "solid.box", + "solid.cylinder", + "solid.sphere", + "solid.torus", + "solid.coil", + "solid.pipe", + "solid.pattern.rectangular", + "solid.pattern.circular", + "solid.pattern.path", + "solid.mirror", + "joint.origin", ]; /// Panels of a tab, when this module lays the tab out. @@ -152,17 +152,17 @@ mod tests { fn tabs_share_commands_in_fusion_order() { for tab in ["SOLID", "SHEET METAL", "PLASTIC"] { let a = ids(tab, "ASSEMBLE"); - assert_eq!(a.get(1).copied(), Some("JointAssembleCmdNew"), "{tab}: {a:?}"); - assert!(a.contains(&"FusionMoveJointsCommand") && a.contains(&"FusionCreateNewComponentCommand")); + assert_eq!(a.get(1).copied(), Some("joint.create"), "{tab}: {a:?}"); + assert!(a.contains(&"joint.drive") && a.contains(&"component.create")); } let c = ids("SHEET METAL", "CREATE"); - assert_eq!(c.get(1).copied(), Some("FusionSheetMetalFlangeCommand")); - assert!(c.contains(&"Extrude") && c.contains(&"FusionSheetMetalFlatPatternCmd")); - assert!(ids("SHEET METAL", "MODIFY").contains(&"FusionSheetmetalUnfoldCommand")); - assert!(ids("PLASTIC", "SETUP").contains(&"FusionManagePlasticRuleCommand")); + assert_eq!(c.get(1).copied(), Some("sheet.flange")); + assert!(c.contains(&"solid.extrude") && c.contains(&"sheet.flat_pattern")); + assert!(ids("SHEET METAL", "MODIFY").contains(&"sheet.unfold")); + assert!(ids("PLASTIC", "SETUP").contains(&"plastic.manage_rules")); let p = ids("PLASTIC", "CREATE"); - assert!(p.contains(&"FusionBossCommand") && p.contains(&"FusionRibCommand") && p.contains(&"FusionLipCommand")); - assert!(ids("PLASTIC", "MODIFY").contains(&"FusionShellBodyCommand")); + assert!(p.contains(&"plastic.boss") && p.contains(&"solid.rib") && p.contains(&"plastic.lip")); + assert!(ids("PLASTIC", "MODIFY").contains(&"solid.shell")); assert!(panels("PLASTIC").is_some_and(|p| p.first() == Some(&"SETUP"))); } } diff --git a/crates/ui-egui/tests/scenarios/s01_box_fillet_hole_shell.json b/crates/ui-egui/tests/scenarios/s01_box_fillet_hole_shell.json index 0a9e5e29..a4bb45de 100644 --- a/crates/ui-egui/tests/scenarios/s01_box_fillet_hole_shell.json +++ b/crates/ui-egui/tests/scenarios/s01_box_fillet_hole_shell.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -13,7 +13,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -40,7 +40,7 @@ } }, { - "start": "SketchDimension" + "start": "sketch.dimension" }, { "click": { @@ -77,7 +77,7 @@ } }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "expect": { @@ -85,7 +85,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "20" @@ -104,7 +104,7 @@ "shot": "s01_extrude" }, { - "start": "FusionFilletEdgesCommand" + "start": "solid.fillet" }, { "click": { @@ -145,7 +145,7 @@ "shot": "s01_fillet" }, { - "start": "FusionShellBodyCommand" + "start": "solid.shell" }, { "click": { @@ -173,7 +173,7 @@ "shot": "s01_shell" }, { - "start": "FusionHoleCommand" + "start": "solid.hole" }, { "click": { diff --git a/crates/ui-egui/tests/scenarios/s02_revolve_on_xz.json b/crates/ui-egui/tests/scenarios/s02_revolve_on_xz.json index 381e3823..334d5e89 100644 --- a/crates/ui-egui/tests/scenarios/s02_revolve_on_xz.json +++ b/crates/ui-egui/tests/scenarios/s02_revolve_on_xz.json @@ -1,15 +1,15 @@ [ - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XZ"}}, {"expect": {"sketching": true}}, - {"start": "ShapeRectangleTwoPoint"}, + {"start": "sketch.rectangle.two_point"}, {"click": {"sketch": [10, 0]}}, {"click": {"sketch": [20, 30]}}, {"key": "Escape"}, {"shot": "s02_sketch"}, - {"start": "SketchStop"}, + {"start": "sketch.finish"}, {"call": "ui.view", "params": {"view": "iso"}}, - {"start": "Revolve"}, + {"start": "solid.revolve"}, {"click": {"axis": "Z"}}, {"debug": 1}, {"key": "Enter"}, diff --git a/crates/ui-egui/tests/scenarios/s03_sketch_on_face_cut.json b/crates/ui-egui/tests/scenarios/s03_sketch_on_face_cut.json index 0cf29ee0..97bea22f 100644 --- a/crates/ui-egui/tests/scenarios/s03_sketch_on_face_cut.json +++ b/crates/ui-egui/tests/scenarios/s03_sketch_on_face_cut.json @@ -1,26 +1,26 @@ [ - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, - {"start": "ShapeRectangleTwoPoint"}, + {"start": "sketch.rectangle.two_point"}, {"click": {"sketch": [0, 0]}}, {"click": {"sketch": [40, 30]}}, {"key": "Escape"}, - {"start": "SketchStop"}, + {"start": "sketch.finish"}, {"call": "ui.view", "params": {"view": "iso"}}, - {"start": "Extrude"}, + {"start": "solid.extrude"}, {"text": "20"}, {"key": "Enter"}, {"expect": {"bodies": 1, "volume": 24000}}, - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"world": [20, 15, 20]}}, {"expect": {"sketching": true}}, - {"start": "CircleCenterRadius"}, + {"start": "sketch.circle.center"}, {"click": {"sketch": [20, 15]}}, {"click": {"sketch": [25, 15]}}, {"key": "Escape"}, {"shot": "s03_sketch_on_face"}, - {"start": "SketchStop"}, - {"start": "Extrude"}, + {"start": "sketch.finish"}, + {"start": "solid.extrude"}, {"click": {"world": [20, 15, 20]}}, {"debug": 1}, {"text": "-10"}, diff --git a/crates/ui-egui/tests/scenarios/s04_mirror_body.json b/crates/ui-egui/tests/scenarios/s04_mirror_body.json index d47533cf..d715ac55 100644 --- a/crates/ui-egui/tests/scenarios/s04_mirror_body.json +++ b/crates/ui-egui/tests/scenarios/s04_mirror_body.json @@ -1,17 +1,17 @@ [ - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, - {"start": "CircleCenterRadius"}, + {"start": "sketch.circle.center"}, {"click": {"sketch": [20, 10]}}, {"click": {"sketch": [25, 10]}}, {"key": "Escape"}, - {"start": "SketchStop"}, + {"start": "sketch.finish"}, {"call": "ui.view", "params": {"view": "iso"}}, - {"start": "Extrude"}, + {"start": "solid.extrude"}, {"text": "10"}, {"key": "Enter"}, {"expect": {"bodies": 1}}, - {"start": "MirrorCommand"}, + {"start": "solid.mirror"}, {"click": {"world": [20, 10, 10]}}, {"call": "ui.dialogInput", "params": {"index": 1}}, {"click": {"plane": "YZ"}}, diff --git a/crates/ui-egui/tests/scenarios/s05_delete_and_undo.json b/crates/ui-egui/tests/scenarios/s05_delete_and_undo.json index dc4a0f6e..2d8c672b 100644 --- a/crates/ui-egui/tests/scenarios/s05_delete_and_undo.json +++ b/crates/ui-egui/tests/scenarios/s05_delete_and_undo.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -30,7 +30,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -39,7 +39,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" diff --git a/crates/ui-egui/tests/scenarios/s06_edit_early_feature.json b/crates/ui-egui/tests/scenarios/s06_edit_early_feature.json index a2ba6547..d94e483d 100644 --- a/crates/ui-egui/tests/scenarios/s06_edit_early_feature.json +++ b/crates/ui-egui/tests/scenarios/s06_edit_early_feature.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -30,7 +30,7 @@ "key": "Escape" }, { - "start": "SketchDimension" + "start": "sketch.dimension" }, { "click": { @@ -50,7 +50,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -59,7 +59,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -68,7 +68,7 @@ "key": "Enter" }, { - "start": "FusionFilletEdgesCommand" + "start": "solid.fillet" }, { "click": { @@ -130,7 +130,7 @@ "shot": "s06_edit_sketch" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "expect": { diff --git a/crates/ui-egui/tests/scenarios/s07_components_joint_drive.json b/crates/ui-egui/tests/scenarios/s07_components_joint_drive.json index 10c1970f..51a108d2 100644 --- a/crates/ui-egui/tests/scenarios/s07_components_joint_drive.json +++ b/crates/ui-egui/tests/scenarios/s07_components_joint_drive.json @@ -1,12 +1,12 @@ [ { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -14,7 +14,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -36,7 +36,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -45,7 +45,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -71,13 +71,13 @@ } }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -85,7 +85,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -107,7 +107,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -116,7 +116,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "click": { @@ -140,7 +140,7 @@ } }, { - "start": "JointAssembleCmdNew" + "start": "joint.create" }, { "click": { @@ -205,7 +205,7 @@ } }, { - "start": "FusionMoveJointsCommand" + "start": "joint.drive" }, { "text": "90" diff --git a/crates/ui-egui/tests/scenarios/s08_sheet_metal_flat_dxf.json b/crates/ui-egui/tests/scenarios/s08_sheet_metal_flat_dxf.json index 066682aa..3f8cd9a2 100644 --- a/crates/ui-egui/tests/scenarios/s08_sheet_metal_flat_dxf.json +++ b/crates/ui-egui/tests/scenarios/s08_sheet_metal_flat_dxf.json @@ -6,7 +6,7 @@ } }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -14,7 +14,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -36,7 +36,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -45,7 +45,7 @@ } }, { - "start": "FusionSheetMetalFlangeCommand" + "start": "sheet.flange" }, { "click": { @@ -75,7 +75,7 @@ "shot": "s08_base" }, { - "start": "FusionSheetMetalFlangeCommand" + "start": "sheet.flange" }, { "click": { @@ -100,7 +100,7 @@ "shot": "s08_flange" }, { - "start": "FusionSheetMetalFlatPatternCmd" + "start": "sheet.flat_pattern" }, { "widget": "Export DXF\u2026" diff --git a/crates/ui-egui/tests/scenarios/s09_step_roundtrip_hole.json b/crates/ui-egui/tests/scenarios/s09_step_roundtrip_hole.json index 4b50366a..be90b4df 100644 --- a/crates/ui-egui/tests/scenarios/s09_step_roundtrip_hole.json +++ b/crates/ui-egui/tests/scenarios/s09_step_roundtrip_hole.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -30,10 +30,10 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "15" @@ -99,7 +99,7 @@ } }, { - "start": "FusionHoleCommand" + "start": "solid.hole" }, { "click": { diff --git a/crates/ui-egui/tests/scenarios/s10_parameters_drive_model.json b/crates/ui-egui/tests/scenarios/s10_parameters_drive_model.json index ba0ed57c..2bcb2933 100644 --- a/crates/ui-egui/tests/scenarios/s10_parameters_drive_model.json +++ b/crates/ui-egui/tests/scenarios/s10_parameters_drive_model.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -30,7 +30,7 @@ "key": "Escape" }, { - "start": "SketchDimension" + "start": "sketch.dimension" }, { "click": { @@ -50,10 +50,10 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -67,7 +67,7 @@ } }, { - "start": "ChangeParameterCommand" + "start": "parameters.change" }, { "shot": "s10_params" diff --git a/crates/ui-egui/tests/scenarios/s11_sketch_constraints.json b/crates/ui-egui/tests/scenarios/s11_sketch_constraints.json index 3ad933ec..9a72c5f8 100644 --- a/crates/ui-egui/tests/scenarios/s11_sketch_constraints.json +++ b/crates/ui-egui/tests/scenarios/s11_sketch_constraints.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "DrawPolyline" + "start": "sketch.line" }, { "click": { @@ -63,7 +63,7 @@ } }, { - "start": "CircleCenterRadius" + "start": "sketch.circle.center" }, { "click": { @@ -85,7 +85,7 @@ "key": "Escape" }, { - "start": "SketchDimension" + "start": "sketch.dimension" }, { "click": { @@ -170,7 +170,7 @@ } }, { - "start": "ConstraintFix" + "start": "sketch.constraint.fix" }, { "click": { @@ -208,7 +208,7 @@ } }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "expect": { diff --git a/crates/ui-egui/tests/scenarios/s12_pattern_bodies.json b/crates/ui-egui/tests/scenarios/s12_pattern_bodies.json index 3ab3c5e5..5931052e 100644 --- a/crates/ui-egui/tests/scenarios/s12_pattern_bodies.json +++ b/crates/ui-egui/tests/scenarios/s12_pattern_bodies.json @@ -1,17 +1,17 @@ [ - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, - {"start": "CircleCenterRadius"}, + {"start": "sketch.circle.center"}, {"click": {"sketch": [10, 10]}}, {"click": {"sketch": [14, 10]}}, {"key": "Escape"}, - {"start": "SketchStop"}, + {"start": "sketch.finish"}, {"call": "ui.view", "params": {"view": "iso"}}, - {"start": "Extrude"}, + {"start": "solid.extrude"}, {"text": "8"}, {"key": "Enter"}, {"expect": {"bodies": 1}}, - {"start": "PatternRectangular"}, + {"start": "solid.pattern.rectangular"}, {"click": {"world": [10, 10, 8]}}, {"call": "ui.dialogInput", "params": {"index": 1}}, {"click": {"axis": "X"}}, diff --git a/crates/ui-egui/tests/scenarios/s13_plastic_enclosure.json b/crates/ui-egui/tests/scenarios/s13_plastic_enclosure.json index cded06cf..610af445 100644 --- a/crates/ui-egui/tests/scenarios/s13_plastic_enclosure.json +++ b/crates/ui-egui/tests/scenarios/s13_plastic_enclosure.json @@ -6,7 +6,7 @@ } }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -14,7 +14,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -36,7 +36,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -45,7 +45,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "30" @@ -60,7 +60,7 @@ } }, { - "start": "FusionShellBodyCommand" + "start": "solid.shell" }, { "click": { @@ -98,7 +98,7 @@ } }, { - "start": "FusionBossCommand" + "start": "plastic.boss" }, { "click": { @@ -126,7 +126,7 @@ "shot": "s13_boss" }, { - "start": "FusionLipCommand" + "start": "plastic.lip" }, { "click": { @@ -160,7 +160,7 @@ } }, { - "start": "FusionSnapFitCommand" + "start": "plastic.snap_fit" }, { "click": { diff --git a/crates/ui-egui/tests/scenarios/s14_appearance_export.json b/crates/ui-egui/tests/scenarios/s14_appearance_export.json index 12faab2c..ffb3cf81 100644 --- a/crates/ui-egui/tests/scenarios/s14_appearance_export.json +++ b/crates/ui-egui/tests/scenarios/s14_appearance_export.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -30,7 +30,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -39,7 +39,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" diff --git a/crates/ui-egui/tests/scenarios/s15_start_page_sample_save_reopen.json b/crates/ui-egui/tests/scenarios/s15_start_page_sample_save_reopen.json index 1e68fa4e..f74333ca 100644 --- a/crates/ui-egui/tests/scenarios/s15_start_page_sample_save_reopen.json +++ b/crates/ui-egui/tests/scenarios/s15_start_page_sample_save_reopen.json @@ -5,7 +5,7 @@ {"until": {"home": false}}, {"expect": {"errors": 0, "bodies": 1, "name": "Sample Plate", "param": {"width": 80}}}, {"shot": "s15_sample"}, - {"start": "ChangeParameterCommand"}, + {"start": "parameters.change"}, {"widget": "80 mm"}, {"key": "A", "cmd": true}, {"text": "100 mm"}, diff --git a/crates/ui-egui/tests/scenarios/s16_tabs_separate_undo.json b/crates/ui-egui/tests/scenarios/s16_tabs_separate_undo.json index 177148a7..d70ae03f 100644 --- a/crates/ui-egui/tests/scenarios/s16_tabs_separate_undo.json +++ b/crates/ui-egui/tests/scenarios/s16_tabs_separate_undo.json @@ -1,25 +1,25 @@ [ - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, - {"start": "ShapeRectangleTwoPoint"}, + {"start": "sketch.rectangle.two_point"}, {"click": {"sketch": [0, 0]}}, {"click": {"sketch": [30, 20]}}, {"key": "Escape"}, - {"start": "SketchStop"}, - {"start": "Extrude"}, + {"start": "sketch.finish"}, + {"start": "solid.extrude"}, {"text": "10"}, {"key": "Enter"}, {"expect": {"bodies": 1, "volume": 6000, "documents": 1}}, {"key": "N", "cmd": true}, {"expect": {"bodies": 0, "documents": 2, "features": 0}}, - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, - {"start": "CircleCenterRadius"}, + {"start": "sketch.circle.center"}, {"click": {"sketch": [0, 0]}}, {"click": {"sketch": [10, 0]}}, {"key": "Escape"}, - {"start": "SketchStop"}, - {"start": "Extrude"}, + {"start": "sketch.finish"}, + {"start": "solid.extrude"}, {"text": "5"}, {"key": "Enter"}, {"expect": {"bodies": 1, "volume": [1570.8, 1e-3]}}, diff --git a/crates/ui-egui/tests/scenarios/s17_toolbox_and_visibility.json b/crates/ui-egui/tests/scenarios/s17_toolbox_and_visibility.json index f4615b2c..04a95dcc 100644 --- a/crates/ui-egui/tests/scenarios/s17_toolbox_and_visibility.json +++ b/crates/ui-egui/tests/scenarios/s17_toolbox_and_visibility.json @@ -19,7 +19,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { diff --git a/crates/ui-egui/tests/scenarios/s18_3d_sketch_pipe.json b/crates/ui-egui/tests/scenarios/s18_3d_sketch_pipe.json index 42c21bc3..5d336b53 100644 --- a/crates/ui-egui/tests/scenarios/s18_3d_sketch_pipe.json +++ b/crates/ui-egui/tests/scenarios/s18_3d_sketch_pipe.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -30,7 +30,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -39,7 +39,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "15" @@ -48,7 +48,7 @@ "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -74,7 +74,7 @@ } }, { - "start": "DrawPolyline" + "start": "sketch.line" }, { "click": { @@ -126,10 +126,10 @@ } }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "PrimitivePipe" + "start": "solid.pipe" }, { "click": { diff --git a/crates/ui-egui/tests/scenarios/s19_canvas_calibrate.json b/crates/ui-egui/tests/scenarios/s19_canvas_calibrate.json index 24b8979a..891ab717 100644 --- a/crates/ui-egui/tests/scenarios/s19_canvas_calibrate.json +++ b/crates/ui-egui/tests/scenarios/s19_canvas_calibrate.json @@ -6,7 +6,7 @@ "choose_file": "pic.png" }, { - "start": "FusionAddCanvasCommand" + "start": "canvas.insert" }, { "click": { diff --git a/crates/ui-egui/tests/scenarios/s20_section_with_joint_drive.json b/crates/ui-egui/tests/scenarios/s20_section_with_joint_drive.json index 6d8dff91..ef953588 100644 --- a/crates/ui-egui/tests/scenarios/s20_section_with_joint_drive.json +++ b/crates/ui-egui/tests/scenarios/s20_section_with_joint_drive.json @@ -1,12 +1,12 @@ [ { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -14,7 +14,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -36,7 +36,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -45,7 +45,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -71,13 +71,13 @@ } }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -85,7 +85,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -107,7 +107,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -116,7 +116,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "click": { @@ -140,7 +140,7 @@ } }, { - "start": "JointAssembleCmdNew" + "start": "joint.create" }, { "click": { @@ -185,7 +185,7 @@ } }, { - "start": "FusionHalfSectionViewCommand" + "start": "inspect.section" }, { "click": { @@ -205,7 +205,7 @@ "shot": "s20_section" }, { - "start": "FusionMoveJointsCommand" + "start": "joint.drive" }, { "text": "90" diff --git a/crates/ui-egui/tests/scenarios/s21_named_views.json b/crates/ui-egui/tests/scenarios/s21_named_views.json index 1c08b9de..3772e144 100644 --- a/crates/ui-egui/tests/scenarios/s21_named_views.json +++ b/crates/ui-egui/tests/scenarios/s21_named_views.json @@ -1,6 +1,6 @@ [ { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -8,7 +8,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -30,10 +30,10 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" diff --git a/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json b/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json index 60030662..4990a63c 100644 --- a/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json +++ b/crates/ui-egui/tests/scenarios/s22_sheet_metal_fold.json @@ -1,6 +1,6 @@ [ { - "note": "Target: sheet metal plate folded along a sketch line (SheetMetalFoldCmd, solvecraft-params; dialog by solvecraft-dialogs). Adjust the dialog steps when it lands." + "note": "Target: sheet metal plate folded along a sketch line (sheet.fold, solvecraft-params; dialog by solvecraft-dialogs). Adjust the dialog steps when it lands." }, { "call": "ui.set", @@ -9,7 +9,7 @@ } }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -17,7 +17,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -39,7 +39,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -48,7 +48,7 @@ } }, { - "start": "FusionSheetMetalFlangeCommand" + "start": "sheet.flange" }, { "click": { @@ -69,7 +69,7 @@ } }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -81,7 +81,7 @@ } }, { - "start": "DrawPolyline" + "start": "sketch.line" }, { "click": { @@ -106,10 +106,10 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "SheetMetalFoldCmd" + "start": "sheet.fold" }, { "click": { @@ -139,7 +139,7 @@ "shot": "s22_folded" }, { - "start": "FusionSheetMetalFlatPatternCmd" + "start": "sheet.flat_pattern" }, { "widget": "Export DXF\u2026" diff --git a/crates/ui-egui/tests/scenarios/s23_configurations.json b/crates/ui-egui/tests/scenarios/s23_configurations.json index 8db09483..c1aa12b4 100644 --- a/crates/ui-egui/tests/scenarios/s23_configurations.json +++ b/crates/ui-egui/tests/scenarios/s23_configurations.json @@ -1,9 +1,9 @@ [ { - "note": "Configurations: a parameter column (d1) in the configuration table switches the design (FusionStartDesignConfigModeCmd)." + "note": "Configurations: a parameter column (d1) in the configuration table switches the design (config.configure)." }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -11,7 +11,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -33,7 +33,7 @@ "key": "Escape" }, { - "start": "SketchDimension" + "start": "sketch.dimension" }, { "click": { @@ -53,10 +53,10 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -70,7 +70,7 @@ } }, { - "start": "FusionStartDesignConfigModeCmd" + "start": "config.configure" }, { "widget": "Add Column" diff --git a/crates/ui-egui/tests/scenarios/s24_motion_study.json b/crates/ui-egui/tests/scenarios/s24_motion_study.json index 2c729e6e..913ed90f 100644 --- a/crates/ui-egui/tests/scenarios/s24_motion_study.json +++ b/crates/ui-egui/tests/scenarios/s24_motion_study.json @@ -1,15 +1,15 @@ [ { - "note": "Motion study: key a revolute joint and play it (FusionMotionStudyCommand, motion.play)." + "note": "Motion study: key a revolute joint and play it (motion.study, motion.play)." }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -17,7 +17,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -39,7 +39,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -48,7 +48,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -74,13 +74,13 @@ } }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -88,7 +88,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -110,7 +110,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -119,7 +119,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "click": { @@ -137,7 +137,7 @@ "key": "Enter" }, { - "start": "JointAssembleCmdNew" + "start": "joint.create" }, { "click": { @@ -167,7 +167,7 @@ "widget": "OK" }, { - "start": "FusionMotionStudyCommand" + "start": "motion.study" }, { "widget": "Add Key" diff --git a/crates/ui-egui/tests/scenarios/s25_autosave_recovery.json b/crates/ui-egui/tests/scenarios/s25_autosave_recovery.json index f0662205..ad62236e 100644 --- a/crates/ui-egui/tests/scenarios/s25_autosave_recovery.json +++ b/crates/ui-egui/tests/scenarios/s25_autosave_recovery.json @@ -6,7 +6,7 @@ "autosave": true }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -14,7 +14,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -36,10 +36,10 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" diff --git a/crates/ui-egui/tests/scenarios/s26_drive_joint_typed.json b/crates/ui-egui/tests/scenarios/s26_drive_joint_typed.json index 35afb905..cc01f43d 100644 --- a/crates/ui-egui/tests/scenarios/s26_drive_joint_typed.json +++ b/crates/ui-egui/tests/scenarios/s26_drive_joint_typed.json @@ -1,12 +1,12 @@ [ { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -14,7 +14,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -36,7 +36,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -45,7 +45,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -71,13 +71,13 @@ } }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -85,7 +85,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -107,7 +107,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -116,7 +116,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "click": { @@ -140,7 +140,7 @@ } }, { - "start": "JointAssembleCmdNew" + "start": "joint.create" }, { "click": { @@ -188,7 +188,7 @@ } }, { - "start": "FusionMoveJointsCommand" + "start": "joint.drive" }, { "text": "45" @@ -214,7 +214,7 @@ } }, { - "start": "FusionMoveJointsCommand" + "start": "joint.drive" }, { "text": "-30 deg" diff --git a/crates/ui-egui/tests/scenarios/s27_move_triad.json b/crates/ui-egui/tests/scenarios/s27_move_triad.json index 23d9f7f9..195a6cf0 100644 --- a/crates/ui-egui/tests/scenarios/s27_move_triad.json +++ b/crates/ui-egui/tests/scenarios/s27_move_triad.json @@ -1,17 +1,17 @@ [ {"note": "Move opens with nothing moved; the triad's X arrow moves +20 X only; a ring turns 90 degrees"}, - {"call": "engine.execute", "params": {"command": "PrimitiveBox", "params": {"length": 20, "width": 30, "height": 10}}}, + {"call": "engine.execute", "params": {"command": "solid.box", "params": {"length": 20, "width": 30, "height": 10}}}, {"call": "ui.view", "params": {"view": "iso"}}, - {"start": "FusionMoveCommand"}, + {"start": "solid.move"}, {"click": {"world": [10, 15, 10]}}, {"key": "Enter"}, {"expect": {"errors": 0, "dialog": null, "bbox": {"body": "Body1", "min": [0, 0, 0], "max": [20, 30, 10]}}}, - {"start": "FusionMoveCommand"}, + {"start": "solid.move"}, {"click": {"world": [10, 15, 10]}}, {"drag": [{"gizmo": "x"}, {"gizmo": "x", "by": [20, 0, 0]}]}, {"key": "Enter"}, {"expect": {"errors": 0, "dialog": null, "bbox": {"body": "Body1", "min": [20, 0, 0], "max": [40, 30, 10]}}}, - {"start": "FusionMoveCommand"}, + {"start": "solid.move"}, {"click": {"world": [30, 15, 10]}}, {"drag": [{"gizmo": "rz", "turn": 60}, {"gizmo": "rz", "turn": 150}], "steps": 12}, {"key": "Enter"}, diff --git a/crates/ui-egui/tests/scenarios/s30_pattern_a_hole.json b/crates/ui-egui/tests/scenarios/s30_pattern_a_hole.json index b011bfb3..070754ea 100644 --- a/crates/ui-egui/tests/scenarios/s30_pattern_a_hole.json +++ b/crates/ui-egui/tests/scenarios/s30_pattern_a_hole.json @@ -3,7 +3,7 @@ "note": "Pattern a hole (Object Type: Features), the beginner workflow box \u2192 hole \u2192 pattern." }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -11,7 +11,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -33,7 +33,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -42,7 +42,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "15" @@ -56,7 +56,7 @@ } }, { - "start": "FusionHoleCommand" + "start": "solid.hole" }, { "click": { @@ -85,7 +85,7 @@ } }, { - "start": "PatternRectangular" + "start": "solid.pattern.rectangular" }, { "widget": "Bodies" diff --git a/crates/ui-egui/tests/scenarios/s31_sketch_on_face_projects.json b/crates/ui-egui/tests/scenarios/s31_sketch_on_face_projects.json index 1499b873..259dd0fc 100644 --- a/crates/ui-egui/tests/scenarios/s31_sketch_on_face_projects.json +++ b/crates/ui-egui/tests/scenarios/s31_sketch_on_face_projects.json @@ -3,7 +3,7 @@ "note": "User bug 1 (owner solvecraft-sketch): Create Sketch on a body face projects the face's loops as reference geometry (linked), with no big square of real lines, and the face stays pickable for Project." }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -11,7 +11,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -33,7 +33,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -42,7 +42,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "15" @@ -51,7 +51,7 @@ "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -79,7 +79,7 @@ "shot": "s26_face_sketch" }, { - "start": "ProjectNewCmd" + "start": "sketch.project" }, { "click": { @@ -102,7 +102,7 @@ } }, { - "start": "CircleCenterRadius" + "start": "sketch.circle.center" }, { "click": { @@ -131,7 +131,7 @@ } }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "expect": { diff --git a/crates/ui-egui/tests/scenarios/s32_cut_in_active_component.json b/crates/ui-egui/tests/scenarios/s32_cut_in_active_component.json index 90241344..0df6a84d 100644 --- a/crates/ui-egui/tests/scenarios/s32_cut_in_active_component.json +++ b/crates/ui-egui/tests/scenarios/s32_cut_in_active_component.json @@ -3,13 +3,13 @@ "note": "User bug 2 (owner solvecraft-params): with two components, a cut made in the active one leaves the other untouched." }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -17,7 +17,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -39,7 +39,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -48,7 +48,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -74,13 +74,13 @@ } }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -88,7 +88,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -110,7 +110,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -119,7 +119,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "20" @@ -154,7 +154,7 @@ } }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -173,7 +173,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -202,10 +202,10 @@ } }, { - "start": "SketchStop" + "start": "sketch.finish" }, { - "start": "Extrude" + "start": "solid.extrude" }, { "widget": "New Body" diff --git a/crates/ui-egui/tests/scenarios/s33_move_x_arrow.json b/crates/ui-egui/tests/scenarios/s33_move_x_arrow.json index 05491593..d3cf6e6a 100644 --- a/crates/ui-egui/tests/scenarios/s33_move_x_arrow.json +++ b/crates/ui-egui/tests/scenarios/s33_move_x_arrow.json @@ -1,14 +1,14 @@ [ {"note": "User bug 3 (owner solvecraft-ui): Move opens at 0, and dragging its X arrow moves only along X. The X arrow must be published as handle move_x (scenario::publish_handle)."}, - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, - {"start": "ShapeRectangleTwoPoint"}, + {"start": "sketch.rectangle.two_point"}, {"click": {"sketch": [0, 0]}}, {"click": {"sketch": [20, 20]}}, {"key": "Escape"}, - {"start": "SketchStop"}, + {"start": "sketch.finish"}, {"call": "ui.view", "params": {"view": "iso"}}, - {"start": "Extrude"}, + {"start": "solid.extrude"}, {"text": "10"}, {"key": "Enter"}, {"call": "ui.menu", "params": {"x": 100, "y": 250, "target": {"type": "body", "name": "Body1"}}}, diff --git a/crates/ui-egui/tests/scenarios/s34_component_move_carries_sketches.json b/crates/ui-egui/tests/scenarios/s34_component_move_carries_sketches.json index 42168e72..d11d2245 100644 --- a/crates/ui-egui/tests/scenarios/s34_component_move_carries_sketches.json +++ b/crates/ui-egui/tests/scenarios/s34_component_move_carries_sketches.json @@ -3,13 +3,13 @@ "note": "User bug 4 (owner solvecraft-repo): moving a component carries its sketches, and a sketch on a body face follows the body." }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -17,7 +17,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -39,7 +39,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -48,7 +48,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -57,7 +57,7 @@ "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -69,7 +69,7 @@ } }, { - "start": "CircleCenterRadius" + "start": "sketch.circle.center" }, { "click": { @@ -91,7 +91,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "expect": { diff --git a/crates/ui-egui/tests/scenarios/s35_construction_mode.json b/crates/ui-egui/tests/scenarios/s35_construction_mode.json index 9ab0922d..1194fcaf 100644 --- a/crates/ui-egui/tests/scenarios/s35_construction_mode.json +++ b/crates/ui-egui/tests/scenarios/s35_construction_mode.json @@ -1,15 +1,15 @@ [ {"note": "Construction mode like Fusion: with nothing selected, X (or the palette's Construction linetype) makes new curves construction until switched back; with a selection X converts it."}, - {"start": "SketchCreate"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, {"expect": {"dialog": null}}, {"key": "X"}, {"expect": {"tool": null}}, - {"start": "ShapeRectangleTwoPoint"}, + {"start": "sketch.rectangle.two_point"}, {"click": {"sketch": [0, 0]}}, {"click": {"sketch": [30, 20]}}, {"key": "Escape"}, - {"start": "CircleCenterRadius"}, + {"start": "sketch.circle.center"}, {"click": {"sketch": [15, 10]}}, {"click": {"sketch": [20, 10]}}, {"key": "Escape"}, @@ -21,13 +21,13 @@ {"key": "Y", "cmd": true}, {"shot": "s35_construction_mode"}, {"widget": "—"}, - {"start": "CircleCenterRadius"}, + {"start": "sketch.circle.center"}, {"click": {"sketch": [40, 10]}}, {"click": {"sketch": [45, 10]}}, {"key": "Escape"}, {"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 5, "construction"], "value": false}}}, {"widget": "- -"}, - {"start": "DrawPolyline"}, + {"start": "sketch.line"}, {"click": {"sketch": [0, 30]}}, {"click": {"sketch": [30, 30]}}, {"key": "Escape"}, @@ -38,7 +38,7 @@ {"key": "X"}, {"expect": {"query": {"command": "sketch.inspect", "path": ["curves", 5, "construction"], "value": true}}}, {"key": "Escape"}, - {"start": "DrawPolyline"}, + {"start": "sketch.line"}, {"click": {"sketch": [0, 40]}}, {"click": {"sketch": [30, 40]}}, {"key": "Escape"}, diff --git a/crates/ui-egui/tests/scenarios/s36_sketch_palette_visible.json b/crates/ui-egui/tests/scenarios/s36_sketch_palette_visible.json index 2ce71e52..087dad03 100644 --- a/crates/ui-egui/tests/scenarios/s36_sketch_palette_visible.json +++ b/crates/ui-egui/tests/scenarios/s36_sketch_palette_visible.json @@ -8,7 +8,7 @@ } }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "expect": { @@ -34,7 +34,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "expect": { @@ -61,7 +61,7 @@ "key": "Escape" }, { - "start": "ChangeParameterCommand" + "start": "parameters.change" }, { "widget": "Close window" diff --git a/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json b/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json index 9aed3414..83bcc0db 100644 --- a/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json +++ b/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json @@ -6,13 +6,13 @@ "note": "Browser chevrons expand and collapse every kind of node (folders, components, groups, the origin, named views, document settings). Model: two components with a body each, a joint, a sketch, an offset plane, a named view." }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -20,7 +20,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -42,7 +42,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -51,7 +51,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "text": "10" @@ -77,13 +77,13 @@ } }, { - "start": "FusionCreateNewComponentCommand" + "start": "component.create" }, { "key": "Enter" }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -91,7 +91,7 @@ } }, { - "start": "ShapeRectangleTwoPoint" + "start": "sketch.rectangle.two_point" }, { "click": { @@ -113,7 +113,7 @@ "key": "Escape" }, { - "start": "SketchStop" + "start": "sketch.finish" }, { "call": "ui.view", @@ -122,7 +122,7 @@ } }, { - "start": "Extrude" + "start": "solid.extrude" }, { "click": { @@ -146,7 +146,7 @@ } }, { - "start": "JointAssembleCmdNew" + "start": "joint.create" }, { "click": { @@ -178,7 +178,7 @@ { "call": "engine.execute", "params": { - "command": "ConstructionPlaneOffsetFromPlaneCommand", + "command": "construct.plane.offset", "params": { "base": "XY", "offset": 30 diff --git a/crates/ui-egui/tests/scenarios/s38_view_anim_no_jump.json b/crates/ui-egui/tests/scenarios/s38_view_anim_no_jump.json index bf6ce649..918b3db1 100644 --- a/crates/ui-egui/tests/scenarios/s38_view_anim_no_jump.json +++ b/crates/ui-egui/tests/scenarios/s38_view_anim_no_jump.json @@ -6,7 +6,7 @@ "note": "Animating to Top or Bottom from iso, front or any orbit turns the view smoothly to the end: no frame (the last included) turns it more than 10 degrees." }, { - "start": "PrimitiveBox" + "start": "solid.box" }, { "key": "Escape" diff --git a/crates/ui-egui/tests/scenarios/s40_toolbar_customize_persists.json b/crates/ui-egui/tests/scenarios/s40_toolbar_customize_persists.json index de43ffc5..6d7f0f9b 100644 --- a/crates/ui-egui/tests/scenarios/s40_toolbar_customize_persists.json +++ b/crates/ui-egui/tests/scenarios/s40_toolbar_customize_persists.json @@ -18,11 +18,11 @@ { "expect": { "shown_handles": [ - "toolbar:Extrude", - "toolbar:Revolve" + "toolbar:solid.extrude", + "toolbar:solid.revolve" ], "hidden_handles": [ - "toolbar:FusionShellBodyCommand" + "toolbar:solid.shell" ] } }, @@ -33,7 +33,7 @@ "y": 80, "target": { "type": "toolbar_command", - "id": "FusionShellBodyCommand" + "id": "solid.shell" } } }, @@ -46,7 +46,7 @@ { "expect": { "shown_handles": [ - "toolbar:FusionShellBodyCommand" + "toolbar:solid.shell" ] } }, @@ -57,7 +57,7 @@ "y": 80, "target": { "type": "toolbar_command", - "id": "Revolve" + "id": "solid.revolve" } } }, @@ -70,17 +70,17 @@ { "expect": { "hidden_handles": [ - "toolbar:Revolve" + "toolbar:solid.revolve" ] } }, { "drag": [ { - "handle": "toolbar:FusionShellBodyCommand" + "handle": "toolbar:solid.shell" }, { - "handle": "toolbar:Extrude" + "handle": "toolbar:solid.extrude" } ], "steps": 12 @@ -88,8 +88,8 @@ { "expect": { "handle_order": [ - "toolbar:FusionShellBodyCommand", - "toolbar:Extrude" + "toolbar:solid.shell", + "toolbar:solid.extrude" ] } }, @@ -99,18 +99,18 @@ { "expect": { "shown_handles": [ - "toolbar:FusionShellBodyCommand" + "toolbar:solid.shell" ], "hidden_handles": [ - "toolbar:Revolve" + "toolbar:solid.revolve" ] } }, { "expect": { "handle_order": [ - "toolbar:FusionShellBodyCommand", - "toolbar:Extrude" + "toolbar:solid.shell", + "toolbar:solid.extrude" ] } }, diff --git a/crates/ui-egui/tests/scenarios/s41_type_value_after_picking.json b/crates/ui-egui/tests/scenarios/s41_type_value_after_picking.json index 2cb4862e..81a2dff6 100644 --- a/crates/ui-egui/tests/scenarios/s41_type_value_after_picking.json +++ b/crates/ui-egui/tests/scenarios/s41_type_value_after_picking.json @@ -1,12 +1,12 @@ [ {"note": "Like Fusion, typing a number after picking the geometry goes into the dialog's heads-up value: Fillet after picking an edge, as Hole after picking a face and Extrude after opening do today."}, - {"start": "SketchCreate"}, {"click": {"plane": "XY"}}, - {"start": "ShapeRectangleTwoPoint"}, {"click": {"sketch": [0, 0]}}, {"click": {"sketch": [60, 40]}}, {"key": "Escape"}, - {"start": "SketchStop"}, + {"start": "sketch.create"}, {"click": {"plane": "XY"}}, + {"start": "sketch.rectangle.two_point"}, {"click": {"sketch": [0, 0]}}, {"click": {"sketch": [60, 40]}}, {"key": "Escape"}, + {"start": "sketch.finish"}, {"call": "ui.view", "params": {"view": "home"}}, - {"start": "Extrude"}, {"text": "15"}, {"key": "Enter"}, + {"start": "solid.extrude"}, {"text": "15"}, {"key": "Enter"}, {"expect": {"volume": 36000}}, - {"start": "FusionFilletEdgesCommand"}, + {"start": "solid.fillet"}, {"click": {"world": [30, 0, 15]}}, {"text": "3"}, {"key": "Enter"}, diff --git a/crates/ui-egui/tests/scenarios/s42_drag_snap.json b/crates/ui-egui/tests/scenarios/s42_drag_snap.json index f6cd5267..7cca57bf 100644 --- a/crates/ui-egui/tests/scenarios/s42_drag_snap.json +++ b/crates/ui-egui/tests/scenarios/s42_drag_snap.json @@ -3,7 +3,7 @@ "note": "Dragging sketch geometry snaps like Fusion: onto a point (coincident on release), level with a point (a dashed guide, no constraint), not at all with Ctrl held; a line dragged by its body snaps by its ends." }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { @@ -13,7 +13,7 @@ { "call": "engine.execute", "params": { - "command": "DrawPolyline", + "command": "sketch.line", "params": { "points": [ [ @@ -31,7 +31,7 @@ { "call": "engine.execute", "params": { - "command": "DrawPolyline", + "command": "sketch.line", "params": { "points": [ [ @@ -230,7 +230,7 @@ { "call": "engine.execute", "params": { - "command": "DrawPolyline", + "command": "sketch.line", "params": { "points": [ [ diff --git a/crates/ui-egui/tests/scenarios/sketch_on_moved_component_face.json b/crates/ui-egui/tests/scenarios/sketch_on_moved_component_face.json index 999f0c3e..5fe13d6a 100644 --- a/crates/ui-egui/tests/scenarios/sketch_on_moved_component_face.json +++ b/crates/ui-egui/tests/scenarios/sketch_on_moved_component_face.json @@ -8,14 +8,14 @@ { "call": "engine.execute", "params": { - "command": "FusionCreateNewComponentCommand", + "command": "component.create", "params": {} } }, { "call": "engine.execute", "params": { - "command": "PrimitiveBox", + "command": "solid.box", "params": { "length": 40, "width": 30, @@ -75,7 +75,7 @@ } }, { - "start": "SketchCreate" + "start": "sketch.create" }, { "click": { diff --git a/crates/ui-egui/tests/ui_layout.rs b/crates/ui-egui/tests/ui_layout.rs index f5e50c2e..4501e706 100644 --- a/crates/ui-egui/tests/ui_layout.rs +++ b/crates/ui-egui/tests/ui_layout.rs @@ -9,10 +9,10 @@ use solvecraft_ui_egui::scenario::{Harness, check}; fn harness() -> Harness { let mut h = Harness::new(); for (id, p) in [ - ("SketchCreate", json!({"plane": "XY"})), - ("ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [40, 30]})), - ("SketchStop", json!({})), - ("Extrude", json!({"distance": 10})), + ("sketch.create", json!({"plane": "XY"})), + ("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]})), + ("sketch.finish", json!({})), + ("solid.extrude", json!({"distance": 10})), ] { h.call("engine.execute", json!({"command": id, "params": p})); } diff --git a/crates/ui-egui/tests/window_widths.rs b/crates/ui-egui/tests/window_widths.rs index 21dabe63..b4a80d4c 100644 --- a/crates/ui-egui/tests/window_widths.rs +++ b/crates/ui-egui/tests/window_widths.rs @@ -19,7 +19,7 @@ fn window_widths(h: &Harness) -> Vec<(String, f32)> { fn fresh() -> Harness { let mut h = Harness::new(); - h.call("engine.execute", json!({"command": "PrimitiveBox", "params": {"length": 40, "width": 30, "height": 20}})); + h.call("engine.execute", json!({"command": "solid.box", "params": {"length": 40, "width": 30, "height": 20}})); h.frames(3); h } @@ -65,7 +65,7 @@ fn command_dialogs_shrink_to_their_content_within_limits() { fn common_dialogs_are_not_stretched() { // Simple dialogs have room to spare: they must not be pushed out to the maximum. let mut h = fresh(); - for id in ["Extrude", "FusionFilletEdgesCommand", "FusionShellBodyCommand", "FusionMoveCommand", "FusionHoleCommand"] { + for id in ["solid.extrude", "solid.fillet", "solid.shell", "solid.move", "solid.hole"] { h.call("ui.start", json!({"command": id})); h.frames(6); let w: f32 = window_widths(&h).iter().map(|x| x.1).fold(0.0, f32::max); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 16aec022..f547051c 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -62,9 +62,9 @@ channel; see [mcp.md](mcp.md). ## Example ```text -{"id":1,"method":"engine.execute","params":{"command":"SketchCreate","params":{"plane":"XY"}}} -{"id":2,"method":"engine.execute","params":{"command":"ShapeRectangleTwoPoint","params":{"p0":[0,0],"p1":[40,30]}}} -{"id":3,"method":"engine.execute","params":{"command":"Extrude","params":{"distance":20}}} -{"id":4,"method":"engine.execute","params":{"command":"FusionFilletEdgesCommand","params":{"edges":[[0,0,10]],"radius":3}}} +{"id":1,"method":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}} +{"id":2,"method":"engine.execute","params":{"command":"sketch.rectangle.two_point","params":{"p0":[0,0],"p1":[40,30]}}} +{"id":3,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}} +{"id":4,"method":"engine.execute","params":{"command":"solid.fillet","params":{"edges":[[0,0,10]],"radius":3}}} {"id":5,"method":"ui.screenshot","params":{"path":"/tmp/part.png"}} ``` diff --git a/docs/mcp.md b/docs/mcp.md index 846a6112..118d7e90 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -70,7 +70,7 @@ Errors: an unknown tool or arguments that don't match the tool's schema are JSON result with `isError: true` and the engine's message, which the model can read and correct. `batch` rollback undoes as many steps as the batch added to the undo history; batches that -themselves call `UndoCommand`, `NewDocumentCommand` or `doc.open` are not rolled back exactly. +themselves call `edit.undo`, `file.new` or `doc.open` are not rolled back exactly. ## A typical session @@ -78,14 +78,14 @@ themselves call `UndoCommand`, `NewDocumentCommand` or `doc.open` are not rolled → {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"me","version":"1"}}} → {"jsonrpc":"2.0","method":"notifications/initialized"} → {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"batch","arguments":{"commands":[ - {"command":"SketchCreate","params":{"plane":"XY"}}, - {"command":"ShapeRectangleTwoPoint","params":{"p0":[0,0],"p1":[40,30]}}, - {"command":"SketchStop"}, - {"command":"Extrude","params":{"distance":20}}, - {"command":"FusionFilletEdgesCommand","params":{"edges":[[0,0,10]],"radius":3}}, - {"command":"SketchCreate","params":{"plane":"XY"}}, - {"command":"CircleCenterRadius","params":{"center":[20,15],"radius":5}}, - {"command":"Extrude","params":{"distance":20,"operation":"cut"}}]}}} + {"command":"sketch.create","params":{"plane":"XY"}}, + {"command":"sketch.rectangle.two_point","params":{"p0":[0,0],"p1":[40,30]}}, + {"command":"sketch.finish"}, + {"command":"solid.extrude","params":{"distance":20}}, + {"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":"solid.extrude","params":{"distance":20,"operation":"cut"}}]}}} ← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"ok\": true, \"completed\": 8, \"results\": [...]}"}]}} → {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"measure","arguments":{}}} ← … "total": {"volume_mm3": 22390.6, …} diff --git a/docs/oracle.md b/docs/oracle.md index 18e1b900..cab046a9 100644 --- a/docs/oracle.md +++ b/docs/oracle.md @@ -2,7 +2,7 @@ Generated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe and measurements kept locally in `plan/fusion/oracle/`, not committed). SolveCraft replays the recipe through its command engine and compares body count, volume and area (relative tolerance 1e-3) and face/edge/vertex counts (exact). SolveCraft counts each closed-surface patch once and ignores seam edges and vertices; when body count, volume and area pass but the raw counts differ, Fusion's counts are normalised the same way from its face, edge and vertex lists (faces cut from one cylinder, cone, sphere or torus and separated only by seams count once; an edge on that surface and no other is a seam and is dropped; a vertex left with two edges joins them) and compared exactly; the note gives both sides' raw counts. -**73 / 76 cases pass** (0 need features not implemented yet). +**76 / 76 cases pass** (0 need features not implemented yet). | Case | Result | Notes | |---|---|---| @@ -40,14 +40,14 @@ Generated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe a | 32-boss-plate-fillets | pass | | | 33-fillet-tangent-chain | pass | | | 34-chamfer-then-fillet | pass | | -| 35-fillet-curved-curved | FAIL | step_export: got "faces inside out: Tee: face 7 runs clockwise around its normal; Tee: face 8 runs clockwise around its normal" want "valid, re-imports cleanly"; topology compared seam-normalised: Fusion's 6/6/5 faces/edges/vertices become 6/5/5 with closed-surface patches counted once and seam edges and vertices dropped (raw SolveCraft/Fusion: faces 9/6, edges 17/6, vertices 10/5) | +| 35-fillet-curved-curved | pass | topology compared seam-normalised: Fusion's 6/6/5 faces/edges/vertices become 6/5/5 with closed-surface patches counted once and seam edges and vertices dropped (raw SolveCraft/Fusion: faces 9/6, edges 17/6, vertices 10/5) | | 36-corner-blend-two-radii | pass | | | 37-pyramid-apex-blend | pass | | | 38-coincident-cylinders | pass | | | 39-tee-pipe | pass | | | 40-box-sphere-torus | pass | | | 41-tube-radial-holes | pass | | -| 42-overlapping-patterns | FAIL | faces (seam-normalised): got 41.0 want 43.0; edges (seam-normalised): got 102.0 want 106.0; vertices (seam-normalised): got 78.0 want 74.0; topology compared seam-normalised: Fusion's 43/106/74 faces/edges/vertices become 43/106/74 with closed-surface patches counted once and seam edges and vertices dropped (raw SolveCraft/Fusion: faces 76/43, edges 222/106, vertices 148/74) | +| 42-overlapping-patterns | pass | | | 43-cut-splits-body | pass | | | 44-shell-l-bracket | pass | | | 45-shell-boss-plate | pass | | @@ -55,7 +55,7 @@ Generated by `cargo xtask oracle`. Each case is a part built in Fusion (recipe a | 47-draft-curved-pocket | pass | | | 48-pulley | pass | | | 49-helical-sweep | pass | Fusion sweeps along a spline fitted through helix points; SolveCraft builds the true helix (a coil), so the size is compared within 0.5% and the topology not at all | -| 50-sweep-tight-bend | FAIL | faces (seam-normalised): got 6.0 want 5.0; edges (seam-normalised): got 5.0 want 6.0; vertices (seam-normalised): got 5.0 want 4.0; area not compared: Fusion reports this tight sweep's horn torus face at three times its area (π²·R·r quarter-turn value; 740 mm² against 246.7 by Pappus); volume and topology are compared | +| 50-sweep-tight-bend | pass | area not compared: Fusion reports this tight sweep's horn torus face at three times its area (π²·R·r quarter-turn value; 740 mm² against 246.7 by Pappus); volume and topology are compared | | 51-loft-rect-circle-offset | pass | | | 52-circular-pattern-fillet | pass | | | 53-path-pattern | pass | | diff --git a/docs/parity.md b/docs/parity.md index ded20f82..3dcecace 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -110,297 +110,297 @@ Deferred until further notice (not in the in-scope figure): MESH, FORM, SURFACE ## Live (SOLID, SKETCH) -- SOLID › CREATE › Create Sketch (`SketchCreate`) -- SOLID › CREATE › Extrude (`Extrude`) -- SOLID › CREATE › Revolve (`Revolve`) -- SOLID › CREATE › Sweep (`Sweep`) -- SOLID › CREATE › Loft (`SolidLoft`) -- SOLID › CREATE › Rib (`FusionRibCommand`) -- SOLID › CREATE › Web (`FusionWebCommand`) -- SOLID › CREATE › Emboss (`EmbossCmd`) -- SOLID › CREATE › Hole (`FusionHoleCommand`) -- SOLID › CREATE › Thread (`FusionThreadCommand`) -- SOLID › CREATE › Box (`PrimitiveBox`) -- SOLID › CREATE › Cylinder (`PrimitiveCylinder`) -- SOLID › CREATE › Sphere (`PrimitiveSphere`) -- SOLID › CREATE › Torus (`PrimitiveTorus`) -- SOLID › CREATE › Coil (`PrimitiveCoil`) -- SOLID › CREATE › Pipe (`PrimitivePipe`) -- SOLID › CREATE › Bounding Solid (`StockModelCommand`) -- SOLID › CREATE › Rectangular Pattern (`PatternRectangular`) -- SOLID › CREATE › Circular Pattern (`PatternCircular`) -- SOLID › CREATE › Pattern on Path (`PatternOnPath`) -- SOLID › CREATE › Mirror (`MirrorCommand`) -- SOLID › CREATE › Thicken (`FusionSurfaceThickenCommand`) -- SOLID › CREATE › Joint Origin (`JointOrigin`) -- SOLID › MODIFY › Press Pull (`FusionPressPullCommand`) -- SOLID › MODIFY › Fillet (`FusionFilletEdgesCommand`) -- SOLID › MODIFY › Chamfer (`FusionChamferCommand`) -- SOLID › MODIFY › Shell (`FusionShellBodyCommand`) -- SOLID › MODIFY › Draft (`FusionDraftCommand`) -- SOLID › MODIFY › Scale (`ModifyScale`) -- SOLID › MODIFY › Combine (`FusionCombineCommand`) -- SOLID › MODIFY › Offset Face (`FusionOffsetFacesCommand`) -- SOLID › MODIFY › Replace Face (`FusionReplaceFaceCommand`) -- SOLID › MODIFY › Split Body (`FusionSplitBodyCommand`) -- SOLID › MODIFY › Move/Copy (`FusionMoveCommand`) -- SOLID › MODIFY › Align (`AlignCmd`) -- SOLID › MODIFY › Delete (`FusionDeleteCommand`) -- SOLID › MODIFY › Remove (`SoftDeleteCommand`) -- SOLID › MODIFY › Physical Material (`PhysicalMaterialCommand`) -- SOLID › MODIFY › Appearance (`AppearanceCommand`) -- SOLID › MODIFY › Change Parameters (`ChangeParameterCommand`) -- SOLID › MODIFY › Compute All (`FusionComputeAllCommand`) -- SOLID › ASSEMBLE › Insert Component (`FusionImportCommandFromToolbar`) -- SOLID › ASSEMBLE › New Component (`FusionCreateNewComponentCommand`) -- SOLID › ASSEMBLE › Joint (`JointAssembleCmdNew`) -- SOLID › ASSEMBLE › As-Built Joint (`JointAsBuiltCmd`) -- SOLID › ASSEMBLE › Rigid Group (`RigidGroupCmd`) -- SOLID › ASSEMBLE › Drive Joints (`FusionMoveJointsCommand`) -- SOLID › ASSEMBLE › Motion Link (`FusionMotionRelationshipCommand`) -- SOLID › ASSEMBLE › Motion Study (`FusionMotionStudyCommand`) -- SOLID › ASSEMBLE › Enable Contact Sets (`EnableContactSetsCmd`) -- SOLID › ASSEMBLE › Enable All Contact (`EnableAllContactCmd`) -- SOLID › ASSEMBLE › Disable Contact (`DisableAllContactCmd`) -- SOLID › ASSEMBLE › New Contact Set (`ContactSetCmd`) -- SOLID › CONFIGURE › Configure (`FusionStartDesignConfigModeCmd`) -- SOLID › CONFIGURE › Display Configuration Table (`FusionShowDesignConfigPanelCmd`) -- SOLID › CONSTRUCT › Offset Plane (`ConstructionPlaneOffsetFromPlaneCommand`) -- SOLID › CONSTRUCT › Plane at Angle (`ConstructionPlaneAtAngleCommand`) -- SOLID › INSPECT › Measure (`MeasureCommand`) -- SOLID › INSPECT › Interference (`InterferenceCheckCommand`) -- SOLID › INSPECT › Section Analysis (`FusionHalfSectionViewCommand`) -- SOLID › INSPECT › Curvature Comb Analysis (`FusionCurvatureCombAnalysisCommand`) -- SOLID › INSPECT › Curvature Map Analysis (`FusionCurvatureMapAnalysisCommand`) -- SOLID › INSPECT › Draft Analysis (`FusionDraftAnalysisCommand`) -- SOLID › INSPECT › Environment Map Analysis (`FusionEnvironmentMapAnalysisCommand`) -- SOLID › INSPECT › Isocurve Analysis (`FusionIsoCurveAnalysisCommand`) -- SOLID › INSPECT › Zebra Analysis (`FusionZebraAnalysisCommand`) -- SOLID › INSPECT › Accessibility Analysis (`FusionAccessibilityAnalysisCommand`) -- SOLID › INSPECT › Center of Mass (`FusionCenterOfMassCommand`) -- SOLID › INSPECT › Minimum Radius Analysis (`FusionMinimumRadiusAnalysisCommand`) -- SOLID › INSERT › Decal (`FusionAddEditDecalCommand`) -- SOLID › INSERT › Canvas (`FusionAddCanvasCommand`) -- SOLID › INSERT › Insert SVG (`SketchImportSVG`) -- SOLID › INSERT › Insert DXF (`ImportDxfFileCommand`) -- SOLID › INSERT › Insert Mesh (`ParaMeshInsertAlignCommand`) -- SOLID › INSERT › Insert Fastener (`FusionFastenersCommand`) -- SOLID › POSITION › Capture Position (`SnapshotCmd`) -- SOLID › POSITION › Revert Position (`AsBuiltPositionsCmd`) -- SKETCH › CREATE › Line (`DrawPolyline`) -- SKETCH › CREATE › Midpoint Line (`SketchMidpointLine`) -- SKETCH › CREATE › SketchConstrainer (`SketchConstrainer`) -- SKETCH › CREATE › 2-Point Rectangle (`ShapeRectangleTwoPoint`) -- SKETCH › CREATE › 3-Point Rectangle (`ShapeRectangleThreePoint`) -- SKETCH › CREATE › Center Rectangle (`ShapeRectangleCenter`) -- SKETCH › CREATE › Center Diameter Circle (`CircleCenterRadius`) -- SKETCH › CREATE › 2-Point Circle (`CircleTwoPoint`) -- SKETCH › CREATE › 3-Point Circle (`CircleThreePoint`) -- SKETCH › CREATE › 2-Tangent Circle (`CircleTanTanRadius`) -- SKETCH › CREATE › 3-Tangent Circle (`CircleThreeTangent`) -- SKETCH › CREATE › 3-Point Arc (`ArcThreePoint`) -- SKETCH › CREATE › Center Point Arc (`ArcCenterTwoPoint`) -- SKETCH › CREATE › Tangent Arc (`ArcTangent`) -- SKETCH › CREATE › Circumscribed Polygon (`ShapePolygonCircumscribed`) -- SKETCH › CREATE › Inscribed Polygon (`ShapePolygonInscribed`) -- SKETCH › CREATE › Edge Polygon (`ShapePolygonEdge`) -- SKETCH › CREATE › Ellipse (`CircleElipse`) -- SKETCH › CREATE › Center to Center Slot (`ShapeSlotCenterToCenter`) -- SKETCH › CREATE › Overall Slot (`ShapeSlotOverall`) -- SKETCH › CREATE › Center Point Slot (`ShapeSlotCenterPoint`) -- SKETCH › CREATE › Three Point Arc Slot (`ShapeArcSlotThreePoint`) -- SKETCH › CREATE › Center Point Arc Slot (`ShapeArcSlotCenterTwoPoint`) -- SKETCH › CREATE › Fit Point Spline (`DrawSpline`) -- SKETCH › CREATE › Control Point Spline (`DrawCVMSpline3D`) -- SKETCH › CREATE › Control Point Spline (`DrawCVMSpline5D`) -- SKETCH › CREATE › Conic Curve (`ConicCurveCmd`) -- SKETCH › CREATE › Point (`DrawPoint`) -- SKETCH › CREATE › Text (`MTextCmd`) -- SKETCH › CREATE › Fit Curves to Mesh Section (`FitCurvesToSectionCommand`) -- SKETCH › CREATE › Mirror (`MirrorSketchCommand`) -- SKETCH › CREATE › Circular Pattern (`CircularSketchPatternCommand`) -- SKETCH › CREATE › Rectangular Pattern (`RectangularSketchPatternCommand`) -- SKETCH › CREATE › Project (`ProjectNewCmd`) -- SKETCH › CREATE › Intersect (`IntersectCmd`) -- SKETCH › CREATE › Spun Profile (`SpunProfileCmd`) -- SKETCH › CREATE › Include 3D Geometry (`Include3DGeometry`) -- SKETCH › CREATE › Project To Surface (`ProjectToSurface`) -- SKETCH › CREATE › Intersection Curve (`IntersectionCurve`) -- SKETCH › CREATE › Isoparametric Curve (`SketchIsoparametricCurve`) -- SKETCH › CREATE › Sketch Dimension (`SketchDimension`) -- SKETCH › MODIFY › Fillet (`FilletSketchCmd`) -- SKETCH › MODIFY › Equal Distance Chamfer (`ChamferSketchEqualDistance`) -- SKETCH › MODIFY › Distance and Angle Chamfer (`ChamferSketchDistanceAngle`) -- SKETCH › MODIFY › Two Distance Chamfer (`ChamferSketchDistanceDistance`) -- SKETCH › MODIFY › Blend Curve (`BlendG1CurveSketchCmd`) -- SKETCH › MODIFY › Offset (`Offset`) -- SKETCH › MODIFY › Trim (`TrimSketchCmd`) -- SKETCH › MODIFY › Extend (`ExtendSketchCmd`) -- SKETCH › MODIFY › Break (`BreakSketchCmd`) -- SKETCH › MODIFY › Sketch Scale (`SketchScaleCmd`) -- SKETCH › MODIFY › Move/Copy (`FusionMoveCommand`) -- SKETCH › MODIFY › Change Parameters (`ChangeParameterCommand`) -- SKETCH › CONSTRAINTS › AutoConstrain (`SketchAutoConstraintAndDimCmd`) -- SKETCH › CONSTRAINTS › AutoConstrain from datum (`SketchAutoConstraintAndDimFromDatumCmd`) -- SKETCH › CONSTRAINTS › Horizontal/Vertical (`ConstraintHorizontalVertical`) -- SKETCH › CONSTRAINTS › Coincident (`ConstraintCoincident`) -- SKETCH › CONSTRAINTS › Tangent (`ConstraintTangent`) -- SKETCH › CONSTRAINTS › Equal (`ConstraintEqual`) -- SKETCH › CONSTRAINTS › Parallel (`ConstraintParallel`) -- SKETCH › CONSTRAINTS › Perpendicular (`ConstraintPerpendicular`) -- SKETCH › CONSTRAINTS › Fix/UnFix (`ConstraintFix`) -- SKETCH › CONSTRAINTS › MidPoint (`ConstraintMidPoint`) -- SKETCH › CONSTRAINTS › Concentric (`ConstraintConcentric`) -- SKETCH › CONSTRAINTS › Collinear (`ConstraintCollinear`) -- SKETCH › CONSTRAINTS › Symmetry (`ConstraintSymmetry`) -- SKETCH › CONSTRAINTS › Curvature (`ConstraintSmooth`) -- SKETCH › CONSTRAINTS › Polygon (`SketchPolygonConstraintCmd`) -- SKETCH › CONFIGURE › Configure (`FusionStartDesignConfigModeCmd`) -- SKETCH › CONFIGURE › Display Configuration Table (`FusionShowDesignConfigPanelCmd`) -- SKETCH › INSPECT › Measure (`MeasureCommand`) -- SKETCH › INSPECT › Interference (`InterferenceCheckCommand`) -- SKETCH › INSPECT › Section Analysis (`FusionHalfSectionViewCommand`) -- SKETCH › INSPECT › Curvature Comb Analysis (`FusionCurvatureCombAnalysisCommand`) -- SKETCH › INSPECT › Curvature Map Analysis (`FusionCurvatureMapAnalysisCommand`) -- SKETCH › INSPECT › Draft Analysis (`FusionDraftAnalysisCommand`) -- SKETCH › INSPECT › Environment Map Analysis (`FusionEnvironmentMapAnalysisCommand`) -- SKETCH › INSPECT › Isocurve Analysis (`FusionIsoCurveAnalysisCommand`) -- SKETCH › INSPECT › Zebra Analysis (`FusionZebraAnalysisCommand`) -- SKETCH › INSPECT › Accessibility Analysis (`FusionAccessibilityAnalysisCommand`) -- SKETCH › INSPECT › Center of Mass (`FusionCenterOfMassCommand`) -- SKETCH › INSPECT › Minimum Radius Analysis (`FusionMinimumRadiusAnalysisCommand`) -- SKETCH › INSERT › Decal (`FusionAddEditDecalCommand`) -- SKETCH › INSERT › Canvas (`FusionAddCanvasCommand`) -- SKETCH › INSERT › Insert SVG (`SketchImportSVG`) -- SKETCH › INSERT › Insert DXF (`ImportDxfFileCommand`) -- SKETCH › INSERT › Insert Mesh (`ParaMeshInsertAlignCommand`) -- SKETCH › INSERT › Insert Component (`FusionImportCommandFromToolbar`) -- SKETCH › INSERT › Insert Fastener (`FusionFastenersCommand`) -- SKETCH › FINISH SKETCH › Finish Sketch (`SketchStop`) -- SKETCH › FINISH SKETCH › Finish with AutoConstrain (`SketchAutoConstrainAndFinish`) +- SOLID › CREATE › Create Sketch +- SOLID › CREATE › Extrude +- SOLID › CREATE › Revolve +- SOLID › CREATE › Sweep +- SOLID › CREATE › Loft +- SOLID › CREATE › Rib +- SOLID › CREATE › Web +- SOLID › CREATE › Emboss +- SOLID › CREATE › Hole +- SOLID › CREATE › Thread +- SOLID › CREATE › Box +- SOLID › CREATE › Cylinder +- SOLID › CREATE › Sphere +- SOLID › CREATE › Torus +- SOLID › CREATE › Coil +- SOLID › CREATE › Pipe +- SOLID › CREATE › Bounding Solid +- SOLID › CREATE › Rectangular Pattern +- SOLID › CREATE › Circular Pattern +- SOLID › CREATE › Pattern on Path +- SOLID › CREATE › Mirror +- SOLID › CREATE › Thicken +- SOLID › CREATE › Joint Origin +- SOLID › MODIFY › Press Pull +- SOLID › MODIFY › Fillet +- SOLID › MODIFY › Chamfer +- SOLID › MODIFY › Shell +- SOLID › MODIFY › Draft +- SOLID › MODIFY › Scale +- SOLID › MODIFY › Combine +- SOLID › MODIFY › Offset Face +- SOLID › MODIFY › Replace Face +- SOLID › MODIFY › Split Body +- SOLID › MODIFY › Move/Copy +- SOLID › MODIFY › Align +- SOLID › MODIFY › Delete +- SOLID › MODIFY › Remove +- SOLID › MODIFY › Physical Material +- SOLID › MODIFY › Appearance +- SOLID › MODIFY › Change Parameters +- SOLID › MODIFY › Compute All +- SOLID › ASSEMBLE › Insert Component +- SOLID › ASSEMBLE › New Component +- SOLID › ASSEMBLE › Joint +- SOLID › ASSEMBLE › As-Built Joint +- SOLID › ASSEMBLE › Rigid Group +- SOLID › ASSEMBLE › Drive Joints +- SOLID › ASSEMBLE › Motion Link +- SOLID › ASSEMBLE › Motion Study +- SOLID › ASSEMBLE › Enable Contact Sets +- SOLID › ASSEMBLE › Enable All Contact +- SOLID › ASSEMBLE › Disable Contact +- SOLID › ASSEMBLE › New Contact Set +- SOLID › CONFIGURE › Configure +- SOLID › CONFIGURE › Display Configuration Table +- SOLID › CONSTRUCT › Offset Plane +- SOLID › CONSTRUCT › Plane at Angle +- SOLID › INSPECT › Measure +- SOLID › INSPECT › Interference +- SOLID › INSPECT › Section Analysis +- SOLID › INSPECT › Curvature Comb Analysis +- SOLID › INSPECT › Curvature Map Analysis +- SOLID › INSPECT › Draft Analysis +- SOLID › INSPECT › Environment Map Analysis +- SOLID › INSPECT › Isocurve Analysis +- SOLID › INSPECT › Zebra Analysis +- SOLID › INSPECT › Accessibility Analysis +- SOLID › INSPECT › Center of Mass +- SOLID › INSPECT › Minimum Radius Analysis +- SOLID › INSERT › Decal +- SOLID › INSERT › Canvas +- SOLID › INSERT › Insert SVG +- SOLID › INSERT › Insert DXF +- SOLID › INSERT › Insert Mesh +- SOLID › INSERT › Insert Fastener +- SOLID › POSITION › Capture Position +- SOLID › POSITION › Revert Position +- SKETCH › CREATE › Line +- SKETCH › CREATE › Midpoint Line +- SKETCH › CREATE › SketchConstrainer +- SKETCH › CREATE › 2-Point Rectangle +- SKETCH › CREATE › 3-Point Rectangle +- SKETCH › CREATE › Center Rectangle +- SKETCH › CREATE › Center Diameter Circle +- SKETCH › CREATE › 2-Point Circle +- SKETCH › CREATE › 3-Point Circle +- SKETCH › CREATE › 2-Tangent Circle +- SKETCH › CREATE › 3-Tangent Circle +- SKETCH › CREATE › 3-Point Arc +- SKETCH › CREATE › Center Point Arc +- SKETCH › CREATE › Tangent Arc +- SKETCH › CREATE › Circumscribed Polygon +- SKETCH › CREATE › Inscribed Polygon +- SKETCH › CREATE › Edge Polygon +- SKETCH › CREATE › Ellipse +- SKETCH › CREATE › Center to Center Slot +- SKETCH › CREATE › Overall Slot +- SKETCH › CREATE › Center Point Slot +- SKETCH › CREATE › Three Point Arc Slot +- SKETCH › CREATE › Center Point Arc Slot +- SKETCH › CREATE › Fit Point Spline +- SKETCH › CREATE › Control Point Spline +- SKETCH › CREATE › Control Point Spline +- SKETCH › CREATE › Conic Curve +- SKETCH › CREATE › Point +- SKETCH › CREATE › Text +- SKETCH › CREATE › Fit Curves to Mesh Section +- SKETCH › CREATE › Mirror +- SKETCH › CREATE › Circular Pattern +- SKETCH › CREATE › Rectangular Pattern +- SKETCH › CREATE › Project +- SKETCH › CREATE › Intersect +- SKETCH › CREATE › Spun Profile +- SKETCH › CREATE › Include 3D Geometry +- SKETCH › CREATE › Project To Surface +- SKETCH › CREATE › Intersection Curve +- SKETCH › CREATE › Isoparametric Curve +- SKETCH › CREATE › Sketch Dimension +- SKETCH › MODIFY › Fillet +- SKETCH › MODIFY › Equal Distance Chamfer +- SKETCH › MODIFY › Distance and Angle Chamfer +- SKETCH › MODIFY › Two Distance Chamfer +- SKETCH › MODIFY › Blend Curve +- SKETCH › MODIFY › Offset +- SKETCH › MODIFY › Trim +- SKETCH › MODIFY › Extend +- SKETCH › MODIFY › Break +- SKETCH › MODIFY › Sketch Scale +- SKETCH › MODIFY › Move/Copy +- SKETCH › MODIFY › Change Parameters +- SKETCH › CONSTRAINTS › AutoConstrain +- SKETCH › CONSTRAINTS › AutoConstrain from datum +- SKETCH › CONSTRAINTS › Horizontal/Vertical +- SKETCH › CONSTRAINTS › Coincident +- SKETCH › CONSTRAINTS › Tangent +- SKETCH › CONSTRAINTS › Equal +- SKETCH › CONSTRAINTS › Parallel +- SKETCH › CONSTRAINTS › Perpendicular +- SKETCH › CONSTRAINTS › Fix/UnFix +- SKETCH › CONSTRAINTS › MidPoint +- SKETCH › CONSTRAINTS › Concentric +- SKETCH › CONSTRAINTS › Collinear +- SKETCH › CONSTRAINTS › Symmetry +- SKETCH › CONSTRAINTS › Curvature +- SKETCH › CONSTRAINTS › Polygon +- SKETCH › CONFIGURE › Configure +- SKETCH › CONFIGURE › Display Configuration Table +- SKETCH › INSPECT › Measure +- SKETCH › INSPECT › Interference +- SKETCH › INSPECT › Section Analysis +- SKETCH › INSPECT › Curvature Comb Analysis +- SKETCH › INSPECT › Curvature Map Analysis +- SKETCH › INSPECT › Draft Analysis +- SKETCH › INSPECT › Environment Map Analysis +- SKETCH › INSPECT › Isocurve Analysis +- SKETCH › INSPECT › Zebra Analysis +- SKETCH › INSPECT › Accessibility Analysis +- SKETCH › INSPECT › Center of Mass +- SKETCH › INSPECT › Minimum Radius Analysis +- SKETCH › INSERT › Decal +- SKETCH › INSERT › Canvas +- SKETCH › INSERT › Insert SVG +- SKETCH › INSERT › Insert DXF +- SKETCH › INSERT › Insert Mesh +- SKETCH › INSERT › Insert Component +- SKETCH › INSERT › Insert Fastener +- SKETCH › FINISH SKETCH › Finish Sketch +- SKETCH › FINISH SKETCH › Finish with AutoConstrain ## Not yet (SOLID, SKETCH) -- SOLID › CREATE › Create Form (`TSplineBaseFeatureCreationCommand`) -- SOLID › CREATE › Derive (`PushDeriveCommand`) -- SOLID › CREATE › Automated Modeling (`AutomatedModelingCommand`) -- SOLID › CREATE › Geometric Pattern (`FusionPatternGeometricCommand`) -- SOLID › CREATE › Boundary Fill (`SurfaceSculpt`) -- SOLID › CREATE › Find Features (`FusionFindFeaturesCommand`) -- SOLID › CREATE › Fluid Volume (`EnclosureCommand`) -- SOLID › CREATE › Create Base Feature (`BaseFeatureCreationCommand`) -- SOLID › CREATE › Create Linked PCB (`PCBExternalComponentCreationCmd`) -- SOLID › CREATE › Derive PCB (`PCBDeriveCmd`) -- SOLID › CREATE › Create Unlinked PCB (`PCBCreateCmd`) -- SOLID › CREATE › Generate Timeline (`TimelineBuilderCommand`) -- SOLID › MODIFY › Edit Face (`EditFaceCommand`) -- SOLID › MODIFY › Split Face (`FusionSplitFaceCommand`) -- SOLID › MODIFY › Silhouette Split (`FusionPartingLineSplitCmd`) -- SOLID › MODIFY › Arrange (`ArrangeCommand`) -- SOLID › MODIFY › Remove Features (`FusionRemoveFeaturesCommand`) -- SOLID › MODIFY › Remove Faces (`FusionRemoveFacesCommand`) -- SOLID › MODIFY › Replace with Primitives (`ReplaceWithPrimitivesCommand`) -- SOLID › MODIFY › Volumetric Lattice (`VolFieldCreateLatticeCommand`) -- SOLID › MODIFY › Volumetric Texture (`VolFieldTextureCommand`) -- SOLID › MODIFY › VolFieldModifyModelCommand (`VolFieldModifyModelCommand`) -- SOLID › MODIFY › Manage Materials (`MaterialCommand`) -- SOLID › MODIFY › Compute Unresolved (`FusionComputeUnresolvedCommand`) -- SOLID › MODIFY › Convert (`TSpline2BRepCommand`) -- SOLID › MODIFY › Bill of Materials (`MaterializedBomHybridCmd`) -- SOLID › ASSEMBLE › Duplicate With Joints (`FusionDuplicateWithJointsCommand`) -- SOLID › ASSEMBLE › Constrain Components (`AssemblyMateCommand`) -- SOLID › ASSEMBLE › Align Constraint (`AlignConstraintCmd`) -- SOLID › ASSEMBLE › Angle Constraint (`AngleConstraintCmd`) -- SOLID › ASSEMBLE › Center Constraint (`MidplaneConstraintCmd`) -- SOLID › ASSEMBLE › Tangent Constraint (`TangentConstraintCmd`) -- SOLID › ASSEMBLE › Joint Constraint (`JointConstraintCmd`) -- SOLID › ASSEMBLE › As-Built Constraint (`AsBuiltConstraintCmd`) -- SOLID › CONFIGURE › Create Configuration Rules (`FusionConfigurationRulesCmd`) -- SOLID › CONSTRUCT › User Coordinate System (UCS) (`CoordinateSystemCommand`) -- SOLID › CONSTRUCT › Tangent Plane (`ConstructionTangentPlaneCommand`) -- SOLID › CONSTRUCT › Midplane (`ConstructionMidPlaneCommand`) -- SOLID › CONSTRUCT › Perpendicular Plane (`ConstructionPlanePerpendicularToPlaneCommand`) -- SOLID › CONSTRUCT › Plane Through Two Edges (`ConstructionPlaneFromTwoEdgesCommand`) -- SOLID › CONSTRUCT › Plane Through Three Points (`ConstructionPlaneFromThreePointsCommand`) -- SOLID › CONSTRUCT › Plane Along Path (`ConstructionPlaneAlongPathCommand`) -- SOLID › CONSTRUCT › Axis Through Cylinder/Cone/Torus (`ConstructionAxisThroughCylinderCommand`) -- SOLID › CONSTRUCT › Axis Perpendicular To Face (`ConstructionAxisNormalToFaceCommand`) -- SOLID › CONSTRUCT › Axis Through Two Planes (`ConstructionAxisFromTwoPlanesCommand`) -- SOLID › CONSTRUCT › Axis Through Two Points (`ConstructionAxisThroughTwoPointsCommand`) -- SOLID › CONSTRUCT › Axis Through Edge (`ConstructionAxisThroughEdgeCommand`) -- SOLID › CONSTRUCT › Point At Vertex (`ConstructionPointFromPointCommand`) -- SOLID › CONSTRUCT › Point Through Two Edges (`ConstructionPointThroughTwoEdgesCommand`) -- SOLID › CONSTRUCT › Point Through Three Planes (`ConstructionPointFromThreePlanesCommand`) -- SOLID › CONSTRUCT › Point At Center Of Circle/Sphere/Torus (`ConstructionPointFromCircleOrSphereCommand`) -- SOLID › CONSTRUCT › Point At Edge And Plane (`ConstructionPointAtEdgeAndPlaneCommand`) -- SOLID › CONSTRUCT › Point Along Path (`ConstructionPointAlongPathCommand`) -- SOLID › INSPECT › Validate (`FusionSurfaceValidateCommand`) -- SOLID › INSPECT › Component Connection Analysis (`FusionComponentConnectionAnalysisCommand`) -- SOLID › INSPECT › DOF Analysis (`FusionDOFAnalysisCommand`) -- SOLID › INSPECT › Edit Component Connection Analysis (`FusionEditComponentConnectionAnalysisCommand`) -- SOLID › INSPECT › Fastener Stack Analysis (`FusionFastenerStackAnalysisCommand`) -- SOLID › INSPECT › Find Similar Components (`SimilarPartsCommand`) -- SOLID › INSPECT › Navigator (`GraphViewCmd`) -- SOLID › INSPECT › Display Component Colors (`FusionViewColorCyclingToggleCmd`) -- SOLID › INSPECT › Display Mesh Face Groups (`FacegroupsToggleCmd`) -- SOLID › INSPECT › Assembly Statistics (`FusionAssemblyStatisticsCommand`) -- SOLID › INSPECT › Change Summary (`ShowChangeAwarenessToolbarCmd`) -- SOLID › INSPECT › History (`ShowHistoryToolbarCmd`) -- SOLID › INSPECT › Sustainability Insights (`GravityClimate_ManufacturingSustainabilityInsights_MainDialog`) -- SOLID › INSERT › Insert T-Spline (`TSplineImportCommand`) -- SOLID › INSERT › Insert Derive (`PullDeriveCommand`) -- SOLID › INSERT › Insert McMaster-Carr Component (`InsertMcMasterCarrComponentCommand`) -- SOLID › INSERT › Insert a manufacturer part (`cadenasparts4cad`) -- SOLID › INSERT › Insert TraceParts Supplier Components (`traceparts_insert`) -- SOLID › ASSEMBLE › Add To Assembly (`NewAssemblyFromPartCommand`) -- SOLID › SELECT › Select (`SelectCommand`) -- SOLID › SELECT › Window Selection (`selectWindow`) -- SOLID › SELECT › Freeform Selection (`selectFreeForm`) -- SOLID › SELECT › Paint Selection (`selectPaint`) -- SOLID › SELECT › Select By Name (`SelectByNameCommand`) -- SOLID › SELECT › Select By Boundary (`SelectByBoundaryCommand`) -- SOLID › SELECT › Select By Size (`FusionSelectBodiesBySizeCommand`) -- SOLID › SELECT › Invert Selection (`SelectByInvertCommand`) -- SOLID › SELECT › Seed And Boundary (`FusionSelectSeedAndBoundaryFacesCommand`) -- SOLID › SELECT › Component Drag (`FusionDragCompControlsCmd`) -- SOLID › SELECT › Select Body Priority (`SelectBodyPriorityCommand`) -- SOLID › SELECT › Select Face Priority (`SelectFacePriorityCommand`) -- SOLID › SELECT › Select Edge Priority (`SelectEdgePriorityCommand`) -- SOLID › SELECT › Select Component Priority (`SelectComponentPriorityCommand`) -- SOLID › SELECT › Selection Filters (`SelectionFilterCommand`) -- SKETCH › CONFIGURE › Create Configuration Rules (`FusionConfigurationRulesCmd`) -- SKETCH › INSPECT › Validate (`FusionSurfaceValidateCommand`) -- SKETCH › INSPECT › Component Connection Analysis (`FusionComponentConnectionAnalysisCommand`) -- SKETCH › INSPECT › DOF Analysis (`FusionDOFAnalysisCommand`) -- SKETCH › INSPECT › Edit Component Connection Analysis (`FusionEditComponentConnectionAnalysisCommand`) -- SKETCH › INSPECT › Fastener Stack Analysis (`FusionFastenerStackAnalysisCommand`) -- SKETCH › INSPECT › Find Similar Components (`SimilarPartsCommand`) -- SKETCH › INSPECT › Navigator (`GraphViewCmd`) -- SKETCH › INSPECT › Display Component Colors (`FusionViewColorCyclingToggleCmd`) -- SKETCH › INSPECT › Display Mesh Face Groups (`FacegroupsToggleCmd`) -- SKETCH › INSPECT › Assembly Statistics (`FusionAssemblyStatisticsCommand`) -- SKETCH › INSPECT › Change Summary (`ShowChangeAwarenessToolbarCmd`) -- SKETCH › INSPECT › History (`ShowHistoryToolbarCmd`) -- SKETCH › INSPECT › Sustainability Insights (`GravityClimate_ManufacturingSustainabilityInsights_MainDialog`) -- SKETCH › INSERT › Insert T-Spline (`TSplineImportCommand`) -- SKETCH › INSERT › Duplicate With Joints (`FusionDuplicateWithJointsCommand`) -- SKETCH › INSERT › Insert Derive (`PullDeriveCommand`) -- SKETCH › INSERT › Insert McMaster-Carr Component (`InsertMcMasterCarrComponentCommand`) -- SKETCH › INSERT › Insert a manufacturer part (`cadenasparts4cad`) -- SKETCH › INSERT › Insert TraceParts Supplier Components (`traceparts_insert`) -- SKETCH › ASSEMBLE › Add To Assembly (`NewAssemblyFromPartCommand`) -- SKETCH › SELECT › Select (`SelectCommand`) -- SKETCH › SELECT › Window Selection (`selectWindow`) -- SKETCH › SELECT › Freeform Selection (`selectFreeForm`) -- SKETCH › SELECT › Paint Selection (`selectPaint`) -- SKETCH › SELECT › Select By Name (`SelectByNameCommand`) -- SKETCH › SELECT › Select By Boundary (`SelectByBoundaryCommand`) -- SKETCH › SELECT › Select By Size (`FusionSelectBodiesBySizeCommand`) -- SKETCH › SELECT › Invert Selection (`SelectByInvertCommand`) -- SKETCH › SELECT › Seed And Boundary (`FusionSelectSeedAndBoundaryFacesCommand`) -- SKETCH › SELECT › Component Drag (`FusionDragCompControlsCmd`) -- SKETCH › SELECT › Select Body Priority (`SelectBodyPriorityCommand`) -- SKETCH › SELECT › Select Face Priority (`SelectFacePriorityCommand`) -- SKETCH › SELECT › Select Edge Priority (`SelectEdgePriorityCommand`) -- SKETCH › SELECT › Select Component Priority (`SelectComponentPriorityCommand`) -- SKETCH › SELECT › Selection Filters (`SelectionFilterCommand`) +- SOLID › CREATE › Create Form +- SOLID › CREATE › Derive +- SOLID › CREATE › Automated Modeling +- SOLID › CREATE › Geometric Pattern +- SOLID › CREATE › Boundary Fill +- SOLID › CREATE › Find Features +- SOLID › CREATE › Fluid Volume +- SOLID › CREATE › Create Base Feature +- SOLID › CREATE › Create Linked PCB +- SOLID › CREATE › Derive PCB +- SOLID › CREATE › Create Unlinked PCB +- SOLID › CREATE › Generate Timeline +- SOLID › MODIFY › Edit Face +- SOLID › MODIFY › Split Face +- SOLID › MODIFY › Silhouette Split +- SOLID › MODIFY › Arrange +- SOLID › MODIFY › Remove Features +- SOLID › MODIFY › Remove Faces +- SOLID › MODIFY › Replace with Primitives +- SOLID › MODIFY › Volumetric Lattice +- SOLID › MODIFY › Volumetric Texture +- SOLID › MODIFY › VolFieldModifyModelCommand +- SOLID › MODIFY › Manage Materials +- SOLID › MODIFY › Compute Unresolved +- SOLID › MODIFY › Convert +- SOLID › MODIFY › Bill of Materials +- SOLID › ASSEMBLE › Duplicate With Joints +- SOLID › ASSEMBLE › Constrain Components +- SOLID › ASSEMBLE › Align Constraint +- SOLID › ASSEMBLE › Angle Constraint +- SOLID › ASSEMBLE › Center Constraint +- SOLID › ASSEMBLE › Tangent Constraint +- SOLID › ASSEMBLE › Joint Constraint +- SOLID › ASSEMBLE › As-Built Constraint +- SOLID › CONFIGURE › Create Configuration Rules +- SOLID › CONSTRUCT › User Coordinate System (UCS) +- SOLID › CONSTRUCT › Tangent Plane +- SOLID › CONSTRUCT › Midplane +- SOLID › CONSTRUCT › Perpendicular Plane +- SOLID › CONSTRUCT › Plane Through Two Edges +- SOLID › CONSTRUCT › Plane Through Three Points +- SOLID › CONSTRUCT › Plane Along Path +- SOLID › CONSTRUCT › Axis Through Cylinder/Cone/Torus +- SOLID › CONSTRUCT › Axis Perpendicular To Face +- SOLID › CONSTRUCT › Axis Through Two Planes +- SOLID › CONSTRUCT › Axis Through Two Points +- SOLID › CONSTRUCT › Axis Through Edge +- SOLID › CONSTRUCT › Point At Vertex +- SOLID › CONSTRUCT › Point Through Two Edges +- SOLID › CONSTRUCT › Point Through Three Planes +- SOLID › CONSTRUCT › Point At Center Of Circle/Sphere/Torus +- SOLID › CONSTRUCT › Point At Edge And Plane +- SOLID › CONSTRUCT › Point Along Path +- SOLID › INSPECT › Validate +- SOLID › INSPECT › Component Connection Analysis +- SOLID › INSPECT › DOF Analysis +- SOLID › INSPECT › Edit Component Connection Analysis +- SOLID › INSPECT › Fastener Stack Analysis +- SOLID › INSPECT › Find Similar Components +- SOLID › INSPECT › Navigator +- SOLID › INSPECT › Display Component Colors +- SOLID › INSPECT › Display Mesh Face Groups +- SOLID › INSPECT › Assembly Statistics +- SOLID › INSPECT › Change Summary +- SOLID › INSPECT › History +- SOLID › INSPECT › Sustainability Insights +- SOLID › INSERT › Insert T-Spline +- SOLID › INSERT › Insert Derive +- SOLID › INSERT › Insert McMaster-Carr Component +- SOLID › INSERT › Insert a manufacturer part +- SOLID › INSERT › Insert TraceParts Supplier Components +- SOLID › ASSEMBLE › Add To Assembly +- SOLID › SELECT › Select +- SOLID › SELECT › Window Selection +- SOLID › SELECT › Freeform Selection +- SOLID › SELECT › Paint Selection +- SOLID › SELECT › Select By Name +- SOLID › SELECT › Select By Boundary +- SOLID › SELECT › Select By Size +- SOLID › SELECT › Invert Selection +- SOLID › SELECT › Seed And Boundary +- SOLID › SELECT › Component Drag +- SOLID › SELECT › Select Body Priority +- SOLID › SELECT › Select Face Priority +- SOLID › SELECT › Select Edge Priority +- SOLID › SELECT › Select Component Priority +- SOLID › SELECT › Selection Filters +- SKETCH › CONFIGURE › Create Configuration Rules +- SKETCH › INSPECT › Validate +- SKETCH › INSPECT › Component Connection Analysis +- SKETCH › INSPECT › DOF Analysis +- SKETCH › INSPECT › Edit Component Connection Analysis +- SKETCH › INSPECT › Fastener Stack Analysis +- SKETCH › INSPECT › Find Similar Components +- SKETCH › INSPECT › Navigator +- SKETCH › INSPECT › Display Component Colors +- SKETCH › INSPECT › Display Mesh Face Groups +- SKETCH › INSPECT › Assembly Statistics +- SKETCH › INSPECT › Change Summary +- SKETCH › INSPECT › History +- SKETCH › INSPECT › Sustainability Insights +- SKETCH › INSERT › Insert T-Spline +- SKETCH › INSERT › Duplicate With Joints +- SKETCH › INSERT › Insert Derive +- SKETCH › INSERT › Insert McMaster-Carr Component +- SKETCH › INSERT › Insert a manufacturer part +- SKETCH › INSERT › Insert TraceParts Supplier Components +- SKETCH › ASSEMBLE › Add To Assembly +- SKETCH › SELECT › Select +- SKETCH › SELECT › Window Selection +- SKETCH › SELECT › Freeform Selection +- SKETCH › SELECT › Paint Selection +- SKETCH › SELECT › Select By Name +- SKETCH › SELECT › Select By Boundary +- SKETCH › SELECT › Select By Size +- SKETCH › SELECT › Invert Selection +- SKETCH › SELECT › Seed And Boundary +- SKETCH › SELECT › Component Drag +- SKETCH › SELECT › Select Body Priority +- SKETCH › SELECT › Select Face Priority +- SKETCH › SELECT › Select Edge Priority +- SKETCH › SELECT › Select Component Priority +- SKETCH › SELECT › Selection Filters diff --git a/docs/step-import.md b/docs/step-import.md index 50c9245e..f225ea3f 100644 --- a/docs/step-import.md +++ b/docs/step-import.md @@ -3,14 +3,14 @@ SolveCraft reads STEP (ISO 10303-21; AP203, AP214 and AP242 geometry) with its own reader in `crates/kernel/src/step_in` (written from the public standards; no third-party STEP code). A STEP file becomes an **Import** base feature on the timeline: it holds the file's B-rep (the STEP text is kept in the design, so the design reopens without the file) and later features — fillets, holes, cuts sketched on its faces, shells — build on its bodies. - **Open**: File → Open (`doc.open`) accepts `.step`/`.stp` in any case and starts a new design; `solvecraft part.step`; `solvecraft-cli eval part.step`, `solvecraft-cli run script.json --in part.step`; the MCP `open` tool. -- **Insert** into the current design: File → Insert STEP… or SOLID → INSERT (`FusionImportCommandFromToolbar {path}`). +- **Insert** into the current design: File → Insert STEP… or SOLID → INSERT (`file.insert_step {path}`). - **Drag and drop** a STEP file on the window: inserted when the design has features, opened otherwise. Read: manifold solid breps (with voids), faceted breps, shell-based surface models; planes, cylinders, cones, spheres, tori, B-spline and rational B-spline surfaces (with knots, Bézier, uniform, quasi-uniform), surfaces of revolution and linear extrusion, trimmed surfaces; lines, circles, ellipses, B-spline curves, polylines, trimmed/surface/seam curves; length and angle units (SI prefixes, conversion-based units such as inches); product names (body names come from the solid's name, else the product's); surface colours; assemblies (next-assembly-usage occurrences placed by item-defined transformations, and mapped items) — the product tree is kept in the feature for the components milestone. Files are untrusted: sizes, entity counts and nesting are capped and every kernel call is guarded. What cannot be read becomes a warning on the feature (a face left out gives an open body); not read yet: offset surfaces, pcurve-only edges. ## IGES -IGES 5.3 files (`.igs`/`.iges`) open, insert and drop the same way: `crates/kernel/src/iges_in.rs` (written from the published specification) restates the file as STEP and the STEP reader reads it, so the feature holds STEP text. Read: manifold solid B-reps (186) with their shells, faces, loops, edge and vertex lists; trimmed (144) and bounded (143) surfaces, and bare B-spline patches, sewn into a solid by their shared edges (an open body when they do not close); lines, circular arcs, ellipses, composite curves, rational B-spline curves; planes (108, 190), cylinders, cones, spheres, tori, rational B-spline surfaces, surfaces of revolution and tabulated cylinders; transformation matrices; units; colours (314 and the predefined numbers); body names (name properties, else labels). Not read yet: ruled and offset surfaces, curves on surfaces given only in parameter space. Export (`ExportCommand` to `.igs`) writes each body as a manifold solid B-rep on the same surfaces a STEP export uses, with its name and colour. +IGES 5.3 files (`.igs`/`.iges`) open, insert and drop the same way: `crates/kernel/src/iges_in.rs` (written from the published specification) restates the file as STEP and the STEP reader reads it, so the feature holds STEP text. Read: manifold solid B-reps (186) with their shells, faces, loops, edge and vertex lists; trimmed (144) and bounded (143) surfaces, and bare B-spline patches, sewn into a solid by their shared edges (an open body when they do not close); lines, circular arcs, ellipses, composite curves, rational B-spline curves; planes (108, 190), cylinders, cones, spheres, tori, rational B-spline surfaces, surfaces of revolution and tabulated cylinders; transformation matrices; units; colours (314 and the predefined numbers); body names (name properties, else labels). Not read yet: ruled and offset surfaces, curves on surfaces given only in parameter space. Export (`file.export` to `.igs`) writes each body as a manifold solid B-rep on the same surfaces a STEP export uses, with its name and colour. ## Fusion corpus diff --git a/examples/bracket.json b/examples/bracket.json index 80b26577..e3ca078a 100644 --- a/examples/bracket.json +++ b/examples/bracket.json @@ -1,14 +1,14 @@ {"commands": [ - {"command": "ChangeParameterCommand", "params": {"name": "width", "expression": "60 mm"}}, - {"command": "ChangeParameterCommand", "params": {"name": "thickness", "expression": "6 mm"}}, - {"command": "SketchCreate", "params": {"plane": "XZ", "name": "Profile"}}, - {"command": "DrawPolyline", "params": {"points": [[0, 0], [40, 0], [40, 6], [6, 6], [6, 30], [0, 30]], "closed": true, "ids": ["a", "b", "c", "d", "e", "f"]}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": "width", "direction": "negative", "body_name": "Bracket"}}, - {"command": "FusionFilletEdgesCommand", "params": {"edges": [[40, 30, 6]], "radius": 2}}, - {"command": "SketchCreate", "params": {"plane": "XY", "name": "Holes"}}, - {"command": "CircleCenterRadius", "params": {"center": [25, 15], "diameter": 8}}, - {"command": "CircleCenterRadius", "params": {"center": [25, 45], "diameter": 8}}, - {"command": "SketchStop", "params": {}}, - {"command": "Extrude", "params": {"distance": "thickness", "operation": "cut"}} + {"command": "parameters.change", "params": {"name": "width", "expression": "60 mm"}}, + {"command": "parameters.change", "params": {"name": "thickness", "expression": "6 mm"}}, + {"command": "sketch.create", "params": {"plane": "XZ", "name": "Profile"}}, + {"command": "sketch.line", "params": {"points": [[0, 0], [40, 0], [40, 6], [6, 6], [6, 30], [0, 30]], "closed": true, "ids": ["a", "b", "c", "d", "e", "f"]}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": "width", "direction": "negative", "body_name": "Bracket"}}, + {"command": "solid.fillet", "params": {"edges": [[40, 30, 6]], "radius": 2}}, + {"command": "sketch.create", "params": {"plane": "XY", "name": "Holes"}}, + {"command": "sketch.circle.center", "params": {"center": [25, 15], "diameter": 8}}, + {"command": "sketch.circle.center", "params": {"center": [25, 45], "diameter": 8}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": "thickness", "operation": "cut"}} ]} diff --git a/xtask/data/fusion-ids.tsv b/xtask/data/fusion-ids.tsv new file mode 100644 index 00000000..67e24642 --- /dev/null +++ b/xtask/data/fusion-ids.tsv @@ -0,0 +1,181 @@ +# Fusion catalog command id -> SolveCraft command id, for `cargo xtask parity` only (observation data). +# A catalog command is live when its SolveCraft command is registered. +SketchCreate sketch.create +Extrude solid.extrude +Revolve solid.revolve +Sweep solid.sweep +SolidLoft solid.loft +FusionRibCommand solid.rib +FusionWebCommand solid.web +EmbossCmd solid.emboss +FusionHoleCommand solid.hole +FusionThreadCommand solid.thread +PrimitiveBox solid.box +PrimitiveCylinder solid.cylinder +PrimitiveSphere solid.sphere +PrimitiveTorus solid.torus +PrimitiveCoil solid.coil +PrimitivePipe solid.pipe +StockModelCommand solid.bounding_solid +PatternRectangular solid.pattern.rectangular +PatternCircular solid.pattern.circular +PatternOnPath solid.pattern.path +MirrorCommand solid.mirror +FusionSurfaceThickenCommand surface.thicken +JointOrigin joint.origin +FusionPressPullCommand solid.press_pull +FusionFilletEdgesCommand solid.fillet +FusionChamferCommand solid.chamfer +FusionShellBodyCommand solid.shell +FusionDraftCommand solid.draft +ModifyScale solid.scale +FusionCombineCommand solid.combine +FusionOffsetFacesCommand solid.offset_face +FusionReplaceFaceCommand solid.replace_face +FusionSplitFaceCommand solid.split_face +FusionSplitBodyCommand solid.split_body +FusionMoveCommand solid.move +AlignCmd solid.align +FusionDeleteCommand timeline.delete +SoftDeleteCommand solid.remove +PhysicalMaterialCommand material.assign +AppearanceCommand appearance.assign +ChangeParameterCommand parameters.change +FusionComputeAllCommand timeline.compute_all +FusionImportCommandFromToolbar file.insert_step +FusionCreateNewComponentCommand component.create +JointAssembleCmdNew joint.create +JointAsBuiltCmd joint.as_built +RigidGroupCmd joint.rigid_group +FusionMoveJointsCommand joint.drive +FusionMotionRelationshipCommand joint.motion_link +FusionMotionStudyCommand motion.study +EnableContactSetsCmd contact.enable_sets +EnableAllContactCmd contact.enable_all +DisableAllContactCmd contact.disable_all +ContactSetCmd contact.create +FusionStartDesignConfigModeCmd config.configure +FusionShowDesignConfigPanelCmd config.table +ConstructionPlaneOffsetFromPlaneCommand construct.plane.offset +ConstructionPlaneAtAngleCommand construct.plane.at_angle +ConstructionTangentPlaneCommand construct.plane.tangent +ConstructionMidPlaneCommand construct.plane.midplane +ConstructionPlaneFromTwoEdgesCommand construct.plane.two_edges +ConstructionPlaneFromThreePointsCommand construct.plane.three_points +ConstructionPlaneAlongPathCommand construct.plane.along_path +ConstructionAxisThroughCylinderCommand construct.axis.through_cylinder +ConstructionAxisNormalToFaceCommand construct.axis.normal_to_face +ConstructionAxisFromTwoPlanesCommand construct.axis.two_planes +ConstructionAxisThroughTwoPointsCommand construct.axis.two_points +ConstructionAxisThroughEdgeCommand construct.axis.edge +ConstructionPointFromPointCommand construct.point.vertex +ConstructionPointThroughTwoEdgesCommand construct.point.two_edges +ConstructionPointFromThreePlanesCommand construct.point.three_planes +ConstructionPointFromCircleOrSphereCommand construct.point.center +ConstructionPointAtEdgeAndPlaneCommand construct.point.edge_plane +ConstructionPointAlongPathCommand construct.point.along_path +MeasureCommand inspect.measure +InterferenceCheckCommand inspect.interference +FusionHalfSectionViewCommand inspect.section +FusionCurvatureCombAnalysisCommand inspect.curvature_comb +FusionCurvatureMapAnalysisCommand inspect.curvature_map +FusionDraftAnalysisCommand inspect.draft +FusionEnvironmentMapAnalysisCommand inspect.environment_map +FusionIsoCurveAnalysisCommand inspect.isocurve +FusionZebraAnalysisCommand inspect.zebra +FusionAccessibilityAnalysisCommand inspect.accessibility +FusionCenterOfMassCommand inspect.center_of_mass +FusionMinimumRadiusAnalysisCommand inspect.minimum_radius +FusionAddEditDecalCommand canvas.decal +FusionAddCanvasCommand canvas.insert +SketchImportSVG sketch.insert_svg +ImportDxfFileCommand sketch.insert_dxf +ParaMeshInsertAlignCommand file.insert_mesh +FusionFastenersCommand parts.fastener +SnapshotCmd component.capture_position +AsBuiltPositionsCmd component.revert_position +DrawPolyline sketch.line +SketchMidpointLine sketch.line.midpoint +SketchConstrainer sketch.constrainer +ShapeRectangleTwoPoint sketch.rectangle.two_point +ShapeRectangleThreePoint sketch.rectangle.three_point +ShapeRectangleCenter sketch.rectangle.center +CircleCenterRadius sketch.circle.center +CircleTwoPoint sketch.circle.two_point +CircleThreePoint sketch.circle.three_point +CircleTanTanRadius sketch.circle.two_tangent +CircleThreeTangent sketch.circle.three_tangent +ArcThreePoint sketch.arc.three_point +ArcCenterTwoPoint sketch.arc.center_point +ArcTangent sketch.arc.tangent +ShapePolygonCircumscribed sketch.polygon.circumscribed +ShapePolygonInscribed sketch.polygon.inscribed +ShapePolygonEdge sketch.polygon.edge +CircleElipse sketch.ellipse +ShapeSlotCenterToCenter sketch.slot.center_to_center +ShapeSlotOverall sketch.slot.overall +ShapeSlotCenterPoint sketch.slot.center_point +ShapeArcSlotThreePoint sketch.slot.arc_three_point +ShapeArcSlotCenterTwoPoint sketch.slot.arc_center +DrawSpline sketch.spline.fit_point +DrawCVMSpline3D sketch.spline.control_point +DrawCVMSpline5D sketch.spline.control_point_5 +ConicCurveCmd sketch.conic +DrawPoint sketch.point +MTextCmd sketch.text +FitCurvesToSectionCommand sketch.fit_curves_to_section +MirrorSketchCommand sketch.mirror +CircularSketchPatternCommand sketch.pattern.circular +RectangularSketchPatternCommand sketch.pattern.rectangular +ProjectNewCmd sketch.project +IntersectCmd sketch.intersect +SpunProfileCmd sketch.spun_profile +Include3DGeometry sketch.include_3d +ProjectToSurface sketch.project_to_surface +IntersectionCurve sketch.intersection_curve +SketchIsoparametricCurve sketch.isoparametric_curve +SketchDimension sketch.dimension +FilletSketchCmd sketch.fillet +ChamferSketchEqualDistance sketch.chamfer.equal_distance +ChamferSketchDistanceAngle sketch.chamfer.distance_angle +ChamferSketchDistanceDistance sketch.chamfer.two_distance +BlendG1CurveSketchCmd sketch.blend_curve +Offset sketch.offset +TrimSketchCmd sketch.trim +ExtendSketchCmd sketch.extend +BreakSketchCmd sketch.break +SketchScaleCmd sketch.scale +SketchAutoConstraintAndDimCmd sketch.auto_constrain +SketchAutoConstraintAndDimFromDatumCmd sketch.auto_constrain_from_datum +ConstraintHorizontalVertical sketch.constraint.horizontal_vertical +ConstraintCoincident sketch.constraint.coincident +ConstraintTangent sketch.constraint.tangent +ConstraintEqual sketch.constraint.equal +ConstraintParallel sketch.constraint.parallel +ConstraintPerpendicular sketch.constraint.perpendicular +ConstraintFix sketch.constraint.fix +ConstraintMidPoint sketch.constraint.midpoint +ConstraintConcentric sketch.constraint.concentric +ConstraintCollinear sketch.constraint.collinear +ConstraintSymmetry sketch.constraint.symmetry +ConstraintSmooth sketch.constraint.curvature +SketchPolygonConstraintCmd sketch.constraint.polygon +SketchStop sketch.finish +SketchAutoConstrainAndFinish sketch.finish_auto_constrain +FusionSurfacePatchCommand surface.patch +FusionSurfaceTrimCommand surface.trim +FusionExtendCommand surface.extend +FusionSurfaceStitchCommand surface.stitch +FusionSheetMetalFlangeCommand sheet.flange +FusionSheetMetalHemFlangeCommand sheet.hem +SheetMetalFoldCmd sheet.fold +ConvertToSheetMetalCmd sheet.convert +FusionSheetMetalFlatPatternCmd sheet.flat_pattern +FusionSheetmetalUnfoldCommand sheet.unfold +FusionSheetMetalRulesCommand sheet.manage_rules +FusionAssignPlasticRuleCommand plastic.assign_rule +FusionManagePlasticRuleCommand plastic.manage_rules +FusionBossCommand plastic.boss +FusionSnapFitCommand plastic.snap_fit +FusionLipCommand plastic.lip +FusionRestCommand plastic.rest diff --git a/xtask/src/main.rs b/xtask/src/main.rs index d74070e1..5f40aebc 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -239,7 +239,7 @@ timeline: it holds the file's B-rep (the STEP text is kept in the design, so the later features — fillets, holes, cuts sketched on its faces, shells — build on its bodies.\n\n\ - **Open**: File → Open (`doc.open`) accepts `.step`/`.stp` in any case and starts a new design; `solvecraft part.step`; \ `solvecraft-cli eval part.step`, `solvecraft-cli run script.json --in part.step`; the MCP `open` tool.\n\ -- **Insert** into the current design: File → Insert STEP… or SOLID → INSERT (`FusionImportCommandFromToolbar {{path}}`).\n\ +- **Insert** into the current design: File → Insert STEP… or SOLID → INSERT (`file.insert_step {{path}}`).\n\ - **Drag and drop** a STEP file on the window: inserted when the design has features, opened otherwise.\n\n\ Read: manifold solid breps (with voids), faceted breps, shell-based surface models; planes, cylinders, cones, spheres, \ tori, B-spline and rational B-spline surfaces (with knots, Bézier, uniform, quasi-uniform), surfaces of revolution and \ @@ -257,7 +257,7 @@ surfaces, and bare B-spline patches, sewn into a solid by their shared edges (an lines, circular arcs, ellipses, composite curves, rational B-spline curves; planes (108, 190), cylinders, cones, \ spheres, tori, rational B-spline surfaces, surfaces of revolution and tabulated cylinders; transformation matrices; \ units; colours (314 and the predefined numbers); body names (name properties, else labels). Not read yet: ruled and \ -offset surfaces, curves on surfaces given only in parameter space. Export (`ExportCommand` to `.igs`) writes each body as \ +offset surfaces, curves on surfaces given only in parameter space. Export (`file.export` to `.igs`) writes each body as \ a manifold solid B-rep on the same surfaces a STEP export uses, with its name and colour.\n\n\ ## Fusion corpus\n\nGenerated by `cargo xtask step-corpus`. Each case is a part exported from Fusion as STEP (AP214; kept locally in `plan/fusion/oracle/`, never committed). SolveCraft opens `part.step` exactly as File → Open does (an Import base feature) and compares with Fusion's own measurements: body count, volume and area (relative tolerance 1e-3) and the number of faces read from the file.\n\n**{pass} / {} files pass.**\n\n| Case | Result | Bodies | Volume mm³ (got / Fusion) | Area mm² (got / Fusion) | Faces (got / Fusion) | ms | Notes |\n|---|---|---|---|---|---|---|---|\n", list.len() diff --git a/xtask/src/parity.rs b/xtask/src/parity.rs index 7b32d339..fc667463 100644 --- a/xtask/src/parity.rs +++ b/xtask/src/parity.rs @@ -3,7 +3,8 @@ //! `xtask/data/fusion-catalog.tsv` lists the toolbar commands of Fusion's Design workspace as //! `tab \t panel \t command id \t command name` (ids and names only, nothing else). It is //! derived from the local, uncommitted `plan/fusion/menu-tree.json` with `--refresh`. A catalog -//! entry is live when SolveCraft registers a command with the same id. +//! entry is live when SolveCraft registers the command `xtask/data/fusion-ids.tsv` maps its id to +//! (SolveCraft uses its own ids). use std::path::Path; use std::process::Command; @@ -31,10 +32,19 @@ const HEADLINE: &[&str] = &["SOLID", "SKETCH"]; /// figure; the rest of Fusion is deferred and listed separately. const IN_SCOPE_TABS: &[&str] = &["SOLID", "SKETCH", "ASSEMBLY", "SHEET METAL"]; /// The PLASTIC commands in scope (enclosure features). -const PLASTIC_SUBSET: &[&str] = &["FusionBossCommand", "FusionRibCommand", "FusionWebCommand", "FusionLipCommand", "FusionSnapFitCommand"]; +/// (SolveCraft ids, matched through the catalog-id map.) +const PLASTIC_SUBSET: &[&str] = &["plastic.boss", "solid.rib", "solid.web", "plastic.lip", "plastic.snap_fit"]; -fn in_scope(e: &Entry) -> bool { - IN_SCOPE_TABS.contains(&e.tab.as_str()) || (e.tab == "PLASTIC" && PLASTIC_SUBSET.contains(&e.id.as_str())) +/// Catalog id → SolveCraft id. +fn id_map(s: &str) -> std::collections::HashMap { + s.lines() + .filter(|l| !l.starts_with('#') && !l.trim().is_empty()) + .filter_map(|l| l.split_once('\t').map(|(a, b)| (a.trim().to_string(), b.trim().to_string()))) + .collect() +} + +fn in_plastic_subset(e: &Entry, map: &std::collections::HashMap) -> bool { + e.tab == "PLASTIC" && map.get(&e.id).is_some_and(|ours| PLASTIC_SUBSET.contains(&ours.as_str())) } #[derive(Clone, Debug)] @@ -121,7 +131,10 @@ pub fn run(root: &Path, refresh: bool) -> Result<(), String> { .map_err(|e| format!("solvecraft-cli: {e}"))?; let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?; let ids: Vec = cmds.as_array().into_iter().flatten().filter_map(|c| c["id"].as_str().map(str::to_string)).collect(); - let live = |e: &Entry| ids.contains(&e.id); + let map_path = root.join("xtask/data/fusion-ids.tsv"); + let map = id_map(&std::fs::read_to_string(&map_path).map_err(|e| format!("{}: {e}", map_path.display()))?); + let live = |e: &Entry| map.get(&e.id).is_some_and(|ours| ids.contains(ours)); + let in_scope = |e: &Entry| IN_SCOPE_TABS.contains(&e.tab.as_str()) || in_plastic_subset(e, &map); let pct = |a: usize, b: usize| if b == 0 { 0.0 } else { 100.0 * a as f64 / b as f64 }; let head: Vec<&Entry> = catalog.iter().filter(|e| HEADLINE.contains(&e.tab.as_str())).collect(); @@ -153,7 +166,7 @@ pub fn run(root: &Path, refresh: bool) -> Result<(), String> { for (t, p, l, n) in groups.iter().filter(|g| IN_SCOPE_TABS.contains(&g.0.as_str())) { md += &format!("| {t} | {p} | {l} | {n} | {:.0}% |\n", pct(*l, *n)); } - let plastic: Vec<&Entry> = catalog.iter().filter(|e| e.tab == "PLASTIC" && PLASTIC_SUBSET.contains(&e.id.as_str())).collect(); + let plastic: Vec<&Entry> = catalog.iter().filter(|e| in_plastic_subset(e, &map)).collect(); md += &format!( "| PLASTIC | enclosure subset | {} | {} | {:.0}% |\n", plastic.iter().filter(|e| live(e)).count(), @@ -166,11 +179,11 @@ pub fn run(root: &Path, refresh: bool) -> Result<(), String> { } md += "\n## Live (SOLID, SKETCH)\n\n"; for e in head.iter().filter(|e| live(e)) { - md += &format!("- {} › {} › {} (`{}`)\n", e.tab, e.panel, e.name, e.id); + md += &format!("- {} › {} › {}\n", e.tab, e.panel, e.name); } md += "\n## Not yet (SOLID, SKETCH)\n\n"; for e in head.iter().filter(|e| !live(e)) { - md += &format!("- {} › {} › {} (`{}`)\n", e.tab, e.panel, e.name, e.id); + md += &format!("- {} › {} › {}\n", e.tab, e.panel, e.name); } std::fs::write(root.join("docs/parity.md"), md).map_err(|e| format!("docs/parity.md: {e}"))?; println!(