Command ids are SolveCraft's own: 173 ids that were another product's internal command ids become dotted, lower-case ids (<area>.<thing>[.<variant>]: 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 <[email protected]>
This commit is contained in:
1 parent
740360d7c3
commit
d10312b7fd
158 files changed
+3611
-3205
No files matched your search
@@ -125,9 +125,7 @@ pub fn folder_items(app: &SolveApp, component: u64, folder: &str) -> Vec<Item> {
|
||||
let mut v = Vec::new();
|
||||
if folder == "bodies" {
|
||||
let bodies: Vec<String> = 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(());
|
||||
|
||||
@@ -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<Item> {
|
||||
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<Item> {
|
||||
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<Item> {
|
||||
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<Item> {
|
||||
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<Item> {
|
||||
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<Item> {
|
||||
let items = json!(bodies.iter().map(|b| json!({"type": "body", "name": b})).collect::<Vec<_>>());
|
||||
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<Item> {
|
||||
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<Item> {
|
||||
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<Item> {
|
||||
}
|
||||
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<String> = curves.iter().chain(points.iter()).cloned().collect();
|
||||
@@ -581,41 +577,41 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
|
||||
}
|
||||
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<Value> = 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<Value> = 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<Value> = (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 }))),
|
||||
|
||||
+119
-126
@@ -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::<String>(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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, S
|
||||
return Err("pick profiles in two or more sketches".into());
|
||||
}
|
||||
let sections: Vec<Value> = 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<Vec<(String, Value)>, S
|
||||
let path: Vec<String> = 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, 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<Vec<(String, Value)>, S
|
||||
let target = body_names(0).into_iter().next().unwrap_or_default();
|
||||
let tools: Vec<String> = 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<usize>) -> Option<Dia
|
||||
let start = |cmd: &str| Dialog::for_command(app, cmd);
|
||||
let mut d = match &f.kind {
|
||||
FeatureKind::Extrude { sketch, profiles, extent, operation, targets } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Revolve { sketch, profiles, axis, angle, operation, targets } => {
|
||||
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<usize>) -> Option<Dia
|
||||
}
|
||||
FeatureKind::Fillet { edges, radius, .. } | FeatureKind::Chamfer { edges, distance: radius, .. } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Shell { faces, thickness, .. } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Draft { faces, angle, neutral, pull, .. } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Hole { position, direction, diameter, depth, hole, points, thread } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Box { corner, length, width, height, operation } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Sphere { center, radius, operation } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Combine { target, tools, operation, keep_tools } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Mirror { features, plane, .. } => {
|
||||
let mut d = start("MirrorCommand")?;
|
||||
let mut d = start("solid.mirror")?;
|
||||
let ids: Vec<u64> = 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<usize>) -> Option<Dia
|
||||
let bodies: Vec<Sel> = 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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
PatternKind::Circular { origin, axis, count, angle } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Loft { sections, operation, targets } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Sweep { sketch, profiles, path, operation, targets, .. } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::ConstructionPlane { plane: PlaneRef::Offset { base, distance } } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::ConstructionPlane { plane: PlaneRef::AtAngle { base, axis_origin, axis_dir, angle } } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Split { body, plane, .. } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Scale { bodies, origin, factor, factors } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::OffsetFace { faces, distance, body } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Thread { face, designation, length } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::BoundingSolid { bodies, margin } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Pipe { path, diameter, wall, operation, targets, .. } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Emboss { sketch, profiles, depth, deboss, targets } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Rib { curves, thickness, depth, flip, web, .. } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::ReplaceFace { faces, target, body } => {
|
||||
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<usize>) -> Option<Dia
|
||||
d
|
||||
}
|
||||
FeatureKind::Remove { bodies } => {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ pub struct Ap {
|
||||
|
||||
/// The dialog for the Appearance command.
|
||||
pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
(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 {
|
||||
|
||||
@@ -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<SelInput>)> {
|
||||
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<Mesh>) {
|
||||
let names: Vec<String> = 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<Vec<(Str
|
||||
if !name.trim().is_empty() {
|
||||
p["name"] = json!(name.trim());
|
||||
}
|
||||
cmd("FusionCreateNewComponentCommand", p)
|
||||
cmd("component.create", p)
|
||||
} else {
|
||||
cmd("FusionCreateComponentsFromBodiesCommand", json!({ "bodies": bodies }))
|
||||
cmd("component.from_bodies", json!({ "bodies": bodies }))
|
||||
}
|
||||
}
|
||||
Asm::Joint(f) => {
|
||||
@@ -685,7 +675,7 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
|
||||
if let Some(x) = sel_items(inputs, 1).first().and_then(|x| snap_of(s, x)) {
|
||||
p["at"] = x.param;
|
||||
}
|
||||
return Ok(cmd("JointAsBuiltCmd", p));
|
||||
return Ok(cmd("joint.as_built", p));
|
||||
}
|
||||
let (a, b) = (snap(0, "component 1's joint origin")?, snap(1, "component 2's joint origin")?);
|
||||
if a.param.get("occurrence").is_some() && a.param.get("occurrence") == b.param.get("occurrence") {
|
||||
@@ -697,31 +687,31 @@ pub fn commands(app: &SolveApp, k: &Asm, inputs: &[SelInput]) -> Result<Vec<(Str
|
||||
// meet face to face, a pin goes into its hole); the dialog's Flip is the command's
|
||||
// `flip`, which turns B the other way.
|
||||
let p = json!({"type": kind, "a": a_param, "b": b.param, "flip": f.flip, "offset": f.offset[2], "angle": f.angle, "limits": limits});
|
||||
cmd("JointAssembleCmdNew", p)
|
||||
cmd("joint.create", p)
|
||||
}
|
||||
Asm::JointOrigin { name, .. } => {
|
||||
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})));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<egui::Pos2> {
|
||||
#[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<egui::Event>| {
|
||||
|
||||
@@ -92,8 +92,8 @@ pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
(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<SelInput>)> {
|
||||
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<u64, Mat> {
|
||||
fn study_pose(app: &SolveApp, params: &Value, step: u32) -> Result<BTreeMap<u64, Mat>, 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<(Stri
|
||||
// Only managing the sets: nothing to add.
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
vec![("ContactSetCmd".into(), json!({"name": name, "occurrences": occs}))]
|
||||
vec![("contact.create".into(), json!({"name": name, "occurrences": occs}))]
|
||||
}
|
||||
Mo::MotionStudy { name, study, steps, tracks, .. } => {
|
||||
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();
|
||||
|
||||
@@ -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<Value, String> {
|
||||
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);
|
||||
|
||||
@@ -64,7 +64,7 @@ pub struct Pt {
|
||||
|
||||
/// The dialog for Insert Part.
|
||||
pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
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<Vec<(Stri
|
||||
};
|
||||
let p = insert_params(&lib, k, inputs.first().and_then(|i| i.items.first()))?;
|
||||
// The toolbar's command when the engine has it (same parameters).
|
||||
let id = if solvecraft_engine::find_command("FusionFastenersCommand").is_some() { "FusionFastenersCommand" } else { "parts.insert" };
|
||||
let id = if solvecraft_engine::find_command("parts.fastener").is_some() { "parts.fastener" } else { "parts.insert" };
|
||||
Ok(vec![(id.into(), p)])
|
||||
}
|
||||
|
||||
|
||||
@@ -125,16 +125,16 @@ fn lip(groove: bool) -> Pl {
|
||||
/// The dialog for a plastic command.
|
||||
pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
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<String,
|
||||
if app.session.doc.eval(ribs, ValueKind::Unitless).is_ok_and(|n| n >= 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<String,
|
||||
if *groove {
|
||||
put(&mut p, &[("gap", gap)]);
|
||||
}
|
||||
("FusionLipCommand", p)
|
||||
("plastic.lip", p)
|
||||
}
|
||||
Pl::SnapFit { hook, length, thickness, width, catch_depth, catch_length } => {
|
||||
let mut p = placed("a point on a face")?;
|
||||
@@ -356,7 +356,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map<String,
|
||||
{
|
||||
p[key] = json!(v);
|
||||
}
|
||||
("FusionSnapFitCommand", p)
|
||||
("plastic.snap_fit", p)
|
||||
}
|
||||
Pl::Rest { round, width, length, height, draft, thickness } => {
|
||||
let mut p = placed("a point on a face")?;
|
||||
@@ -366,7 +366,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map<String,
|
||||
p["length"] = json!(length);
|
||||
}
|
||||
put(&mut p, &[("draft", draft), ("thickness", thickness)]);
|
||||
("FusionRestCommand", p)
|
||||
("plastic.rest", p)
|
||||
}
|
||||
Pl::Rules { pick, name, values, active, .. } => {
|
||||
let rules = app.session.doc.plastic_rules();
|
||||
@@ -386,7 +386,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map<String,
|
||||
p[*key] = json!(v);
|
||||
}
|
||||
}
|
||||
("FusionManagePlasticRuleCommand", p)
|
||||
("plastic.manage_rules", p)
|
||||
}
|
||||
Pl::Assign { rule } => {
|
||||
let bodies: Vec<String> = inputs
|
||||
@@ -399,7 +399,7 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map<String,
|
||||
return Err("select bodies first".into());
|
||||
}
|
||||
let name = rule.checked_sub(1).and_then(|i| app.session.doc.plastic_rules().get(i).map(|r| r.name.clone())).unwrap_or_default();
|
||||
("FusionAssignPlasticRuleCommand", json!({"bodies": bodies, "rule": name}))
|
||||
("plastic.assign_rule", json!({"bodies": bodies, "rule": name}))
|
||||
}
|
||||
};
|
||||
if let Value::Object(m) = &mut p {
|
||||
@@ -465,13 +465,13 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
}
|
||||
}
|
||||
}
|
||||
(b, "FusionBossCommand", *position)
|
||||
(b, "plastic.boss", *position)
|
||||
}
|
||||
FeatureKind::Lip { face, width, height, groove, gap, outside, body } => {
|
||||
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<SelI
|
||||
catch_depth: catch_depth.clone(),
|
||||
catch_length: catch_length.clone(),
|
||||
};
|
||||
(f, "FusionSnapFitCommand", *position)
|
||||
(f, "plastic.snap_fit", *position)
|
||||
}
|
||||
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
|
||||
extra.insert("direction".into(), pt(*direction));
|
||||
@@ -508,7 +508,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
draft: s(draft),
|
||||
thickness: s(thickness),
|
||||
};
|
||||
(r, "FusionRestCommand", *position)
|
||||
(r, "plastic.rest", *position)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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<SelInput>)> {
|
||||
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<String,
|
||||
if let Some(r) = rule_value(app, *rule).filter(|_| p["type"] != "edge") {
|
||||
p["rule"] = json!(r);
|
||||
}
|
||||
("FusionSheetMetalFlangeCommand", p)
|
||||
("sheet.flange", p)
|
||||
}
|
||||
Sm::Hem { length, gap, flip } => {
|
||||
let edges = edge_points(inputs);
|
||||
@@ -469,7 +465,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String,
|
||||
if !gap.trim().is_empty() {
|
||||
p["gap"] = json!(gap);
|
||||
}
|
||||
("FusionSheetMetalHemFlangeCommand", p)
|
||||
("sheet.hem", p)
|
||||
}
|
||||
Sm::Fold { angle, radius, position, flip } => {
|
||||
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<String,
|
||||
if let Some(Sel::Face { point, .. }) = items(inputs, 1).first() {
|
||||
p["fixed"] = pt(*point);
|
||||
}
|
||||
("SheetMetalFoldCmd", p)
|
||||
("sheet.fold", p)
|
||||
}
|
||||
Sm::Unfold { refold } => {
|
||||
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<String,
|
||||
if let Some(r) = rule_value(app, *rule) {
|
||||
p["rule"] = json!(r);
|
||||
}
|
||||
("ConvertToSheetMetalCmd", p)
|
||||
("sheet.convert", p)
|
||||
}
|
||||
Sm::Rules { pick, name, values, active, .. } => {
|
||||
let rules = rule_list(app);
|
||||
@@ -528,7 +524,7 @@ pub fn commands(app: &SolveApp, k: &Sm, inputs: &[SelInput], extra: &Map<String,
|
||||
p[*k] = json!(v);
|
||||
}
|
||||
}
|
||||
("FusionSheetMetalRulesCommand", p)
|
||||
("sheet.manage_rules", p)
|
||||
}
|
||||
Sm::FlatPattern { .. } => return Ok(Vec::new()),
|
||||
};
|
||||
@@ -602,7 +598,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
position: FOLD_POSITIONS.iter().position(|x| x == position || (*x == "mould" && position == "mold")).unwrap_or(0),
|
||||
flip: *flip,
|
||||
};
|
||||
let (_, mut ins) = start(app, "SheetMetalFoldCmd")?;
|
||||
let (_, mut ins) = start(app, "sheet.fold")?;
|
||||
match curve {
|
||||
Some(c) => {
|
||||
if let Some(i) = ins.first_mut() {
|
||||
@@ -624,11 +620,11 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
|
||||
_ => 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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
+16
-16
@@ -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<String> = 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<usize>) {
|
||||
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()) {
|
||||
|
||||
@@ -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<String> = {
|
||||
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<String> = 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");
|
||||
|
||||
@@ -46,7 +46,7 @@ impl ParamsDialog {
|
||||
/// The command the live preview should run (a valid expression being edited).
|
||||
pub fn preview_commands(&self) -> Option<Vec<(String, Value)>> {
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Sel>, 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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"))?;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ impl ShortcutBox {
|
||||
}
|
||||
pub fn load(&mut self, v: &Value) {
|
||||
let list = |k: &str| -> Vec<String> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -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<Value> = 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<Value> = 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<Vec3> = pts.clone();
|
||||
if let Some((h, _)) = hover {
|
||||
w.push(h);
|
||||
|
||||
@@ -36,14 +36,14 @@ pub fn boxes_for(cmd: &str, n: usize) -> Vec<DimBox> {
|
||||
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<String>, &'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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
+124
-129
@@ -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<Vec3> {
|
||||
|
||||
/// 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<Mode> {
|
||||
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<Tool> {
|
||||
pub fn hint(id: &str) -> Option<String> {
|
||||
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<String> = 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::<Vec<_>>()})
|
||||
}
|
||||
("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) => {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
+65
-65
@@ -38,33 +38,33 @@ pub struct Tool {
|
||||
impl Tool {
|
||||
pub fn for_command(id: &str) -> Option<Tool> {
|
||||
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<String>)) {
|
||||
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<String> = 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<String>)) {
|
||||
/// 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<String> {
|
||||
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<String> = 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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user