fix: faces of an extrude drawn over a face's projected edges keep an exact source (#505)

A profile traced over a face sketch's projected boundary left the extrude's caps and sides without an exact planar source, so a sketch or offset plane on the new top face was refused. The drawn curve is now the edge's source where it covers a projection. Clearer fixed-plane message; regression tests in the engine, UI harness and design corpus, plus a corpus-wide flat-face attachment check (known gaps under #491-#495).

Fixes #489

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-12 23:01:35 -04:00
1 parent 4aaa5457b3
commit 45bc22fc30
11 files changed
+1113 -15

No files matched your search

+1 -1
View File
@@ -2321,7 +2321,7 @@ pub fn start_sketch(app: &mut SolveApp, sel: &Sel) -> Result<(), String> {
_ => return Err("Select an origin plane, construction plane or planar body face".into()),
};
let before = app.cam;
app.run("sketch.create", json!({ "plane": plane }))?;
app.run("sketch.create", json!({ "plane": plane })).map_err(|error| crate::friendly_error("sketch.create", &error))?;
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),
@@ -330,3 +330,43 @@ fn nested_anchor_saved_fixed_conversion_preserves_rotated_second_author_plane()
h.app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.app.session.doc.to_json(), before);
}
/// Issue #489: extrude, sketch a rectangle over the whole top face (its corners snap to the
/// face's projected boundary), extrude it up as a Join, then pick the new top face for a sketch.
#[test]
fn picked_top_face_of_a_stacked_join_extrude_takes_a_sketch() {
let mut h = Harness::new();
h.app.ui.show_origin = false;
h.app.run("sketch.create", json!({"plane":"XY","name":"Base"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,40]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("solid.extrude", json!({"distance":10,"name":"First"})).unwrap();
h.call("ui.view", json!({"view":"top"}));
h.call("ui.view", json!({"view":"fit"}));
h.settle();
let pick_face = |h: &mut Harness, z: f64| {
h.step(&json!({"start":"sketch.create"})).unwrap();
h.step(&json!({"click":{"world":[20,20,z]}})).unwrap();
h.settle();
let error = h.app.dialog.as_ref().and_then(|d| d.error.clone());
let sketch = h.app.session.active_sketch.unwrap_or_else(|| panic!("top face at z={z} must take a sketch: {error:?}"));
let FeatureKind::Sketch { plane: PlaneRef::Face { anchor: Some(_), .. }, .. } = &h.app.session.doc.feature(sketch).unwrap().kind else {
panic!("the top face at z={z} must give a certified attachment");
};
sketch
};
pick_face(&mut h, 10.);
h.step(&json!({"start":"sketch.rectangle.two_point"})).unwrap();
h.step(&json!({"click":{"world":[0,0,10]}})).unwrap();
h.step(&json!({"click":{"world":[40,40,10]}})).unwrap();
h.step(&json!({"key":"Escape"})).unwrap();
h.app.finish_sketch();
h.app.run("solid.extrude", json!({"distance":10,"operation":"join","name":"Second"})).unwrap();
assert_eq!(h.app.session.model.state().bodies.len(), 1);
h.settle();
let top = pick_face(&mut h, 20.);
let FaceAttachmentStatus::Healthy { plane, .. } = h.app.session.placed_document().sketch_face_attachment(top, &h.app.session.model) else {
panic!("attachment must be healthy");
};
assert!((plane.origin.z - 20.).abs() < 1e-9);
}