Imported looks outlive later features: a body's imported colour, opacity and face colours (kernel paint, dropped by operations) are carried to the next version of the body, its faces by persistent name (split faces keep the colour, new faces take the body's); assigned face appearances already follow their faces by name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
2ea9955621
commit
9ee8ba5dd7
12 files changed
+120
-45
No files matched your search
@@ -74,7 +74,8 @@ fn contact_sets_stop_a_driven_joint_at_touching() {
|
||||
#[test]
|
||||
fn motion_study_plays_and_exports() {
|
||||
let mut s = rail();
|
||||
let r = run(&mut s, "motion.study", json!({"name": "Push", "steps": 100, "keys": [{"joint": "Rail", "points": [[0, 10], [100, -5]]}]}));
|
||||
let r =
|
||||
run(&mut s, "motion.study", json!({"name": "Push", "steps": 100, "keys": [{"joint": "Rail", "points": [[0, 10], [100, -5]]}]}));
|
||||
assert_eq!(r["keys"], 1);
|
||||
run(&mut s, "motion.play", json!({"study": "Push", "step": 50}));
|
||||
assert!((b_x(&s) - 12.5).abs() < 1e-6, "{}", b_x(&s));
|
||||
|
||||
@@ -136,9 +136,7 @@ fn patterns_of_a_blind_hole_drill_every_copy() {
|
||||
};
|
||||
let r = patterned(("solid.pattern.rectangular", json!({"features": ["Hole1"], "dir1": [1, 0, 0], "count1": 3, "spacing1": "15 mm"})));
|
||||
assert!((r - 3.0 * one).abs() < 1e-3 * one, "{r} vs 3 × {one}");
|
||||
let c = patterned((
|
||||
"solid.pattern.circular",
|
||||
json!({"features": ["Hole1"], "axis": {"origin": [25, 12, 0], "dir": [0, 0, 1]}, "count": 2, "angle": 360}),
|
||||
));
|
||||
let c =
|
||||
patterned(("solid.pattern.circular", json!({"features": ["Hole1"], "axis": {"origin": [25, 12, 0], "dir": [0, 0, 1]}, "count": 2, "angle": 360})));
|
||||
assert!((c - 2.0 * one).abs() < 1e-3 * one, "{c} vs 2 × {one}");
|
||||
}
|
||||
@@ -1076,24 +1076,16 @@ fn c_equal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
constrain(s, p, "sketch.constraint.equal", |sk| two_curves(sk, p, "sketch.constraint.equal").map(|(a, b)| ConstraintKind::Equal { a, b }))
|
||||
}
|
||||
fn c_parallel(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
constrain(s, p, "sketch.constraint.parallel", |sk| {
|
||||
two_curves(sk, p, "sketch.constraint.parallel").map(|(a, b)| ConstraintKind::Parallel { a, b })
|
||||
})
|
||||
constrain(s, p, "sketch.constraint.parallel", |sk| two_curves(sk, p, "sketch.constraint.parallel").map(|(a, b)| ConstraintKind::Parallel { a, b }))
|
||||
}
|
||||
fn c_perpendicular(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
constrain(s, p, "sketch.constraint.perpendicular", |sk| {
|
||||
two_curves(sk, p, "sketch.constraint.perpendicular").map(|(a, b)| ConstraintKind::Perpendicular { a, b })
|
||||
})
|
||||
constrain(s, p, "sketch.constraint.perpendicular", |sk| two_curves(sk, p, "sketch.constraint.perpendicular").map(|(a, b)| ConstraintKind::Perpendicular { a, b }))
|
||||
}
|
||||
fn c_concentric(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
constrain(s, p, "sketch.constraint.concentric", |sk| {
|
||||
two_curves(sk, p, "sketch.constraint.concentric").map(|(a, b)| ConstraintKind::Concentric { a, b })
|
||||
})
|
||||
constrain(s, p, "sketch.constraint.concentric", |sk| two_curves(sk, p, "sketch.constraint.concentric").map(|(a, b)| ConstraintKind::Concentric { a, b }))
|
||||
}
|
||||
fn c_collinear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
constrain(s, p, "sketch.constraint.collinear", |sk| {
|
||||
two_curves(sk, p, "sketch.constraint.collinear").map(|(a, b)| ConstraintKind::Collinear { a, b })
|
||||
})
|
||||
constrain(s, p, "sketch.constraint.collinear", |sk| two_curves(sk, p, "sketch.constraint.collinear").map(|(a, b)| ConstraintKind::Collinear { a, b }))
|
||||
}
|
||||
fn c_midpoint(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.constraint.midpoint";
|
||||
|
||||
@@ -54,8 +54,7 @@ fn curvature_polygon_arc_length_linear_diameter() {
|
||||
// A horizontal line ending where a control-point spline starts: G2 keeps the spline's
|
||||
// start straight (zero curvature) and tangent.
|
||||
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[-10, 0], [0, 0]], "infer": true}))["curves"])[0].clone();
|
||||
let sp =
|
||||
ids(&run(&mut s, "sketch.spline.control_point", json!({"points": [format!("{l}.end"), [5, 2], [10, 6], [15, 15]]}))["curves"])[0].clone();
|
||||
let sp = ids(&run(&mut s, "sketch.spline.control_point", json!({"points": [format!("{l}.end"), [5, 2], [10, 6], [15, 15]]}))["curves"])[0].clone();
|
||||
run(&mut s, "sketch.constraint.curvature", json!({"a": l, "b": sp}));
|
||||
let sk = sketch(&s);
|
||||
let pts: Vec<Vec2> = sk.curves[sk.curve_index(&sp).unwrap()].kind.point_ids().iter().map(|i| sk.point(*i).unwrap()).collect();
|
||||
|
||||
@@ -348,9 +348,7 @@ fn degree_five_and_cut_free_form_profiles_extrude_exactly() {
|
||||
let vol = |s: &mut Session| run(s, "inspect.measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap();
|
||||
// A degree-5 control spline closed by a line: one B-spline side face.
|
||||
let mut s = new_sketch();
|
||||
let sp =
|
||||
ids(&run(&mut s, "sketch.spline.control_point_5", json!({"points": [[0, 0], [4, 10], [10, 12], [16, 12], [22, 10], [26, 0]]}))["curves"])[0]
|
||||
.clone();
|
||||
let sp = ids(&run(&mut s, "sketch.spline.control_point_5", json!({"points": [[0, 0], [4, 10], [10, 12], [16, 12], [22, 10], [26, 0]]}))["curves"])[0].clone();
|
||||
run(&mut s, "sketch.line", json!({"points": [format!("{sp}.end"), format!("{sp}.start")]}));
|
||||
let a = profiles(&s)[0];
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
@@ -429,8 +427,7 @@ fn fillet_a_line_and_an_arc() {
|
||||
let mut s = new_sketch();
|
||||
// A line into a quarter arc meeting at a sharp corner.
|
||||
let l = ids(&run(&mut s, "sketch.line", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone();
|
||||
let a =
|
||||
ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [30, 0], "start": format!("{l}.end"), "end": [30, -10]}))["curves"])[0].clone();
|
||||
let a = ids(&run(&mut s, "sketch.arc.center_point", json!({"center": [30, 0], "start": format!("{l}.end"), "end": [30, -10]}))["curves"])[0].clone();
|
||||
let _ = a;
|
||||
let f = run(&mut s, "sketch.fillet", json!({"point": format!("{l}.end"), "radius": 2}));
|
||||
let arc = ids(&f["curves"])[0].clone();
|
||||
|
||||
@@ -146,7 +146,11 @@ mod tests {
|
||||
assert!((v - 240.0).abs() < 1e-9, "{v}");
|
||||
// Stitch the patch of a box's open top back on.
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0], "body_name": "Cup"}));
|
||||
run(&mut s, "surface.patch", json!({"edges": [[25, 0, 10], [30, 5, 10], [25, 10, 10], [20, 5, 10]], "body": "Cup", "body_name": "Lid"}));
|
||||
run(
|
||||
&mut s,
|
||||
"surface.patch",
|
||||
json!({"edges": [[25, 0, 10], [30, 5, 10], [25, 10, 10], [20, 5, 10]], "body": "Cup", "body_name": "Lid"}),
|
||||
);
|
||||
assert!(s.world_state().body("Lid").unwrap().body.is_surface());
|
||||
assert!(s.execute("surface.thicken", &json!({"bodies": ["Nope"], "thickness": 1})).is_err());
|
||||
assert!(s.execute("surface.trim", &json!({"body": "Sheet"})).is_err());
|
||||
|
||||
@@ -69,7 +69,9 @@ fn draft(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn accessibility(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = match p.get("direction") {
|
||||
Some(v) => vec3(v).and_then(Vec3::normalized).ok_or_else(|| bad("inspect.accessibility", "`direction` must be a non-zero [x,y,z]"))?,
|
||||
Some(v) => {
|
||||
vec3(v).and_then(Vec3::normalized).ok_or_else(|| bad("inspect.accessibility", "`direction` must be a non-zero [x,y,z]"))?
|
||||
}
|
||||
None => Vec3::Z,
|
||||
};
|
||||
set(s, p, SurfaceAnalysis::Access { dir })
|
||||
|
||||
@@ -61,7 +61,9 @@ fn table(id: &str) -> &'static [(&'static str, K)] {
|
||||
"sketch.intersect" => &[("refs", Picks)],
|
||||
"sketch.project_to_surface" | "sketch.isoparametric_curve" => &[("face", Pick), ("body", Body)],
|
||||
"sketch.intersection_curve" => &[("a", Picks), ("b", Picks)],
|
||||
"inspect.curvature_comb" | "inspect.minimum_radius" | "inspect.isocurve" => &[("edges", Picks), ("faces", Picks)],
|
||||
"inspect.curvature_comb" | "inspect.minimum_radius" | "inspect.isocurve" => {
|
||||
&[("edges", Picks), ("faces", Picks)]
|
||||
}
|
||||
"parts.insert" | "parts.fastener" => &[("at", Pick), ("point", Make), ("direction", Dir)],
|
||||
"appearance.assign" => &[("faces", Picks), ("bodies", Body), ("body", Body)],
|
||||
"solid.move" => &[("bodies", Body), ("translate", Dir), ("axis", Dir), ("origin", Make)],
|
||||
|
||||
@@ -299,7 +299,10 @@ fn hostile_params_never_panic() {
|
||||
];
|
||||
let mut internal = Vec::new();
|
||||
for spec in command_specs() {
|
||||
if matches!(spec.id, "doc.open" | "file.save" | "file.save_as" | "file.export" | "file.save_mesh" | "sketch.export_dxf") {
|
||||
if matches!(
|
||||
spec.id,
|
||||
"doc.open" | "file.save" | "file.save_as" | "file.export" | "file.save_mesh" | "sketch.export_dxf"
|
||||
) {
|
||||
continue; // file system side effects are covered by their own tests
|
||||
}
|
||||
for v in hostile_values() {
|
||||
@@ -402,7 +405,11 @@ fn timeline_edits_reresolve_references() {
|
||||
assert_eq!(names, ["Base", "Bore", "Round"]);
|
||||
assert!(rel(volume(&mut s), expect(60.0, 25.0)) < 1e-3);
|
||||
// Redefine the fillet in place with a new radius; it keeps its name and position.
|
||||
run(&mut s, "timeline.redefine", json!({"feature": "Round", "command": "solid.fillet", "params": {"edges": [[30, 0, 25]], "radius": 4}}));
|
||||
run(
|
||||
&mut s,
|
||||
"timeline.redefine",
|
||||
json!({"feature": "Round", "command": "solid.fillet", "params": {"edges": [[30, 0, 25]], "radius": 4}}),
|
||||
);
|
||||
let f4 = (16.0 - 16.0 * PI / 4.0) * 60.0;
|
||||
assert!(rel(volume(&mut s), 60.0 * 30.0 * 25.0 - f4 - PI * 9.0 * 25.0) < 1e-3);
|
||||
assert_eq!(s.doc.features.iter().map(|f| f.name.as_str()).collect::<Vec<_>>(), ["Base", "Bore", "Round"]);
|
||||
@@ -1110,11 +1117,7 @@ fn face_extrude_follows_edits() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
|
||||
let before = volume(&mut s);
|
||||
run(
|
||||
&mut s,
|
||||
"solid.revolve",
|
||||
json!({"face": [40, 15, 10], "axis": {"origin": [40, 0, 30], "dir": [0, 1, 0]}, "angle": "90 deg", "operation": "new"}),
|
||||
);
|
||||
run(&mut s, "solid.revolve", json!({"face": [40, 15, 10], "axis": {"origin": [40, 0, 30], "dir": [0, 1, 0]}, "angle": "90 deg", "operation": "new"}));
|
||||
let added = volume(&mut s) - before;
|
||||
let want = 30.0 * 20.0 * 20.0 * PI / 2.0;
|
||||
assert!(rel(added, want) < 1e-4, "{added} vs {want}");
|
||||
@@ -1619,4 +1622,36 @@ fn hole_as_deep_as_the_plate() {
|
||||
assert!(r.is_ok(), "{i} {p}: {r:?}");
|
||||
assert!(volume(&mut s) < before - 1.0, "{i} {p}");
|
||||
}
|
||||
/// Appearances outlive later features: a coloured face keeps its colour after a fillet on a
|
||||
/// neighbouring edge; an imported STEP's body and face colours survive a hole.
|
||||
#[test]
|
||||
fn appearances_survive_later_features() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "body_name": "B"}));
|
||||
run(&mut s, "appearance.assign", json!({"faces": [[20, 15, 20]], "color": "#ff0000"}));
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[40, 15, 0]], "radius": 3}));
|
||||
let red = |s: &mut Session, body: &str| {
|
||||
let l = run(s, "appearance.list", json!({ "body": body }));
|
||||
l["body"]["faces"].as_array().into_iter().flatten().any(|f| f["appearance"]["color"] == "#ff0000")
|
||||
};
|
||||
assert!(red(&mut s, "B"), "the top face is still red after the fillet");
|
||||
// Exported with a body colour and the red face, then opened: the colours come in as the
|
||||
// body's own; a hole afterwards keeps them.
|
||||
run(&mut s, "appearance.assign", json!({"bodies": ["B"], "color": "#2050c0"}));
|
||||
let dir = std::env::temp_dir().join(format!("solvecraft-paint-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let step = dir.join("painted.step");
|
||||
run(&mut s, "file.export", json!({"path": step.to_string_lossy()}));
|
||||
let mut t = Session::default();
|
||||
run(&mut t, "doc.open", json!({"path": step.to_string_lossy()}));
|
||||
let body = t.model.state().bodies[0].name.clone();
|
||||
let before = t.doc.body_color(&t.model.state().bodies[0]);
|
||||
assert!(before.is_some());
|
||||
run(&mut t, "solid.hole", json!({"position": [10, 10, 20], "diameter": 4}));
|
||||
let st = t.model.state();
|
||||
let b = st.body(&body).unwrap();
|
||||
assert!(b.body.paint().is_none(), "the hole made a new kernel body");
|
||||
assert_eq!(t.doc.body_color(b), before, "the imported body colour carries on");
|
||||
assert!(red(&mut t, &body), "the imported red face carries on");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in new issue
Block a user