ASSEMBLE panel (SOLID, SHEET METAL, PLASTIC): New Component, Joint, As-built Joint, Joint Origin, Rigid Group, Drive Joints, Motion Link, Interference. Joint origins are picked by hovering: faces snap to their centre (round faces to the centre of the nearer end), edges to their middle (round edges to their centre), vertices to themselves, shown as glyphs with their z axis. The Joint dialog has the motion type, angle, offsets X/Y/Z, flip, per-value limits and Animate (the preview plays the motion); joints and drives preview on the placed model (world states). Drive Joints moves live with a slider. Interference lists the overlaps and shows them red. Joints appear in the timeline and in a Browser Joints folder (edit, drive, suppress, delete). SHEET METAL tab in Fusion's layout: Flange (base, edge or contour, from the picks; height arrow on the canvas), Hem, Unfold, Refold, Convert to Sheet Metal, Sheet Metal Rules (rule manager), Create Flat Pattern (flat view with up/down bend lines, bend table, DXF export). PLASTIC tab with SETUP: Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Rib, Web and the shared solid commands on the tab. Every feature dialog has live preview, Edit Feature and pre-selection; icons drawn in code. Toolbar layout per tab in workspace.rs; tests for the dialog to command mapping. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
109 lines
4.4 KiB
Rust
109 lines
4.4 KiB
Rust
use serde_json::json;
|
|
use solvecraft_engine::Session;
|
|
|
|
use super::*;
|
|
use crate::Services;
|
|
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();
|
|
SolveApp::new(s, Services::default())
|
|
}
|
|
|
|
fn run_all(app: &mut SolveApp, c: Vec<(String, Value)>) {
|
|
for (id, p) in c {
|
|
app.run(&id, p).unwrap();
|
|
}
|
|
}
|
|
|
|
/// The picks decide the flange: a profile makes the base, edges then edge flanges with the
|
|
/// dialog's height, angle and bend position; Edit Feature brings the edges back.
|
|
#[test]
|
|
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");
|
|
let d = app.dialog.clone().unwrap();
|
|
let c = apply_commands(&app, &d).unwrap();
|
|
assert_eq!(c[0].1["type"], "base");
|
|
assert_eq!(c[0].1["profiles"], json!([0]));
|
|
run_all(&mut app, c);
|
|
assert_eq!(app.session.model.state().sheets.len(), 1);
|
|
// Four edge flanges at once, 20 high, outside.
|
|
let edges: Vec<Sel> = [[50.0, 0.0, 2.5], [50.0, 60.0, 2.5], [0.0, 30.0, 2.5], [100.0, 30.0, 2.5]]
|
|
.iter()
|
|
.filter_map(|p| edge_sel(&app.session, Vec3::new(p[0], p[1], p[2])))
|
|
.collect();
|
|
assert_eq!(edges.len(), 4);
|
|
app.session.selection = edges;
|
|
app.start("FusionSheetMetalFlangeCommand");
|
|
let mut d = app.dialog.clone().unwrap();
|
|
if let Kind::Sheet(Sm::Flange { height, position, .. }) = &mut d.kind {
|
|
*height = "20 mm".into();
|
|
*position = 1;
|
|
}
|
|
assert_eq!(d.primary().map(|p| p.0), Some("Height"), "the arrow drives the height");
|
|
assert!(arrow(&app, &d).is_some_and(|n| n.dist(Vec3::Z) < 1e-9), "edge flanges rise out of the sheet");
|
|
let c = apply_commands(&app, &d).unwrap();
|
|
assert_eq!(c[0].1["type"], "edge");
|
|
assert_eq!(c[0].1["edges"].as_array().map(Vec::len), Some(4));
|
|
assert_eq!(c[0].1["position"], "outside");
|
|
run_all(&mut app, c);
|
|
let f = app.session.doc.features.last().unwrap().clone();
|
|
let e = edit_dialog(&app, f.id, None).unwrap();
|
|
assert_eq!(e.inputs[0].items.len(), 4);
|
|
assert!(matches!(&e.kind, Kind::Sheet(Sm::Flange { ty: 1, position: 1, .. })));
|
|
// Rebuilt in place.
|
|
let c = apply_commands(&app, &e).unwrap();
|
|
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();
|
|
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");
|
|
}
|
|
|
|
/// 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.session.selection = edge_sel(&app.session, Vec3::new(50.0, 0.0, 2.5)).into_iter().collect();
|
|
app.start("FusionSheetMetalHemFlangeCommand");
|
|
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");
|
|
let mut d = app.dialog.clone().unwrap();
|
|
if let Kind::Sheet(Sm::Rules { pick, name, values, loaded, .. }) = &mut d.kind {
|
|
*pick = 1;
|
|
*loaded = Some(1);
|
|
*name = "Alu".into();
|
|
*values = vec![
|
|
"1.5 mm".into(),
|
|
"0.4".into(),
|
|
"Thickness".into(),
|
|
"Thickness".into(),
|
|
"Thickness".into(),
|
|
"Thickness".into(),
|
|
"Thickness".into(),
|
|
"Thickness".into(),
|
|
];
|
|
}
|
|
let c = apply_commands(&app, &d).unwrap();
|
|
assert_eq!(c[0].1["name"], "Alu");
|
|
assert_eq!(c[0].1["thickness"], "1.5 mm");
|
|
run_all(&mut app, c);
|
|
assert!(app.session.doc.sheet.rules.iter().any(|r| r.name == "Alu"));
|
|
}
|