A sketch on an extrude's cap keeps its face when the extrude is made thin (#1532)
Thin extrude caps are certified by the curves the wall follows, so a sketch attached to a cap survives switching between solid and thin (#1519). Co-Authored-By: Philipp Bischoff <[email protected]> Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
4 files changed
+149
-2
No files matched your search
@@ -148,6 +148,35 @@ pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solidcraft_k
|
||||
Some(bindings)
|
||||
}
|
||||
|
||||
/// Cap signatures for a thin extrude (#1519). A thin wall's caps are bounded by offset curves
|
||||
/// the sketch doesn't have, so [`extrude_cap_bindings`] can never certify them. A wall built
|
||||
/// from one region is signed by the curves it follows instead: the cap of this extrude whose
|
||||
/// wall runs along them. That is the signature its solid caps had when the wall follows the
|
||||
/// whole profile (a rectangle's four lines), so a sketch attached to a solid cap keeps its face
|
||||
/// when the extrude is made thin, and the other way round. Several regions (walls apart, or one
|
||||
/// meeting itself) are left uncertified: they keep their history names.
|
||||
pub(crate) fn thin_extrude_cap_bindings(curves: &[String], regions: usize, bodies: &[solidcraft_kernel::Body]) -> CapBindings {
|
||||
use solidcraft_kernel::FaceSource;
|
||||
let mut outer = curves.to_vec();
|
||||
outer.sort();
|
||||
outer.dedup();
|
||||
let signed = regions == 1 && bodies.len() == 1 && valid_signature(&outer, &[]);
|
||||
bodies
|
||||
.iter()
|
||||
.map(|body| {
|
||||
let sources: Vec<_> = body.face_sources().into_iter().flatten().collect();
|
||||
let ends =
|
||||
|high: bool| sources.iter().filter(|source| matches!(source, Some(FaceSource::ExtrudeCap { high: h, .. }) if *h == high)).count();
|
||||
// One cap at each end, or none of them is certified.
|
||||
let unique = ends(true) <= 1 && ends(false) <= 1;
|
||||
sources
|
||||
.iter()
|
||||
.map(|source| (signed && unique && matches!(source, Some(FaceSource::ExtrudeCap { .. }))).then(|| (outer.clone(), Vec::new())))
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum FaceCertificate {
|
||||
|
||||
@@ -2938,7 +2938,10 @@ fn feature_tools_with_constructor(
|
||||
// certified as a new body's are (see `Body::with_preserved_union_planar_sources`).
|
||||
if matches!(operation, Operation::NewBody | Operation::Join) && extent.taper.is_none() {
|
||||
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
|
||||
let cap_bindings = crate::config::extrude_cap_bindings(&ss, &bodies, 10_000_000);
|
||||
let cap_bindings = match thin {
|
||||
Some(thin) => Some(crate::config::thin_extrude_cap_bindings(&thin.curves, regions.len(), &bodies)),
|
||||
None => crate::config::extrude_cap_bindings(&ss, &bodies, 10_000_000),
|
||||
};
|
||||
let source_curves = cap_bindings.as_ref().map(|_| crate::config::ExactSketchCurves::new(&ss));
|
||||
tools = tools
|
||||
.into_iter()
|
||||
|
||||
@@ -172,3 +172,113 @@ fn hostile_thin_params_never_panic() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #1519: sketches on a thin extrude's caps. A thin wall's caps are bounded by offset curves the
|
||||
// sketch doesn't have, so they are certified by the curves the wall follows: the signature the
|
||||
// solid caps had when the wall follows the whole profile.
|
||||
|
||||
fn attachment(s: &Session, sketch: &str) -> solidcraft_doc::FaceAttachmentStatus {
|
||||
let id = s.doc.find_feature(sketch).map(|f| f.id).unwrap_or_default();
|
||||
s.placed_document().sketch_face_attachment(id, &s.model)
|
||||
}
|
||||
|
||||
fn attached(s: &Session, sketch: &str) -> (solidcraft_geom::Plane, Option<solidcraft_doc::config::FaceCertificate>) {
|
||||
match attachment(s, sketch) {
|
||||
solidcraft_doc::FaceAttachmentStatus::Healthy { plane, target, .. } => (plane, target.certificate),
|
||||
other => panic!("{sketch}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn same_plane(a: &solidcraft_geom::Plane, b: &solidcraft_geom::Plane) {
|
||||
let near = |p: solidcraft_geom::Vec3, q: solidcraft_geom::Vec3| (p - q).len() < 1e-9;
|
||||
assert!(near(a.origin, b.origin) && near(a.x, b.x) && near(a.y, b.y), "{a:?} != {b:?}");
|
||||
}
|
||||
|
||||
fn sketch_on(s: &mut Session, at: [f64; 3], name: &str) {
|
||||
run(s, "sketch.create", json!({"plane": {"face": at}, "name": name}));
|
||||
run(s, "sketch.finish", json!({}));
|
||||
}
|
||||
|
||||
fn is_extrude_cap(certificate: &Option<solidcraft_doc::config::FaceCertificate>) -> bool {
|
||||
matches!(certificate, Some(solidcraft_doc::config::FaceCertificate::ExtrudeCap { .. }))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketches_on_a_solid_extrudes_caps_keep_their_faces_when_it_is_made_thin_and_solid_again() {
|
||||
for side in ["center", "inside", "outside"] {
|
||||
let mut s = Session::default();
|
||||
let sides = rectangle(&mut s);
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Outline", "distance": 10, "name": "Wall"}));
|
||||
// On the rim of the wall whichever side it lies on (the rectangle starts at (5, 5)).
|
||||
sketch_on(&mut s, [5.5, 5.5, 10.0], "Top");
|
||||
sketch_on(&mut s, [5.5, 5.5, 0.0], "Bottom");
|
||||
let (top, _) = attached(&s, "Top");
|
||||
let (bottom, _) = attached(&s, "Bottom");
|
||||
run(&mut s, "timeline.edit", json!({"feature": "Wall", "set": {"thin": {"curves": sides, "thickness": "1", "side": side}}}));
|
||||
no_errors(&s, side);
|
||||
let (thin_top, certificate) = attached(&s, "Top");
|
||||
assert!(is_extrude_cap(&certificate), "{side}: {certificate:?}");
|
||||
same_plane(&thin_top, &top);
|
||||
same_plane(&attached(&s, "Bottom").0, &bottom);
|
||||
// The sketch follows the thin wall's height, and keeps its face through a save.
|
||||
run(&mut s, "timeline.edit", json!({"feature": "Wall", "set": {"extent": {"distance": "25"}}}));
|
||||
assert!((attached(&s, "Top").0.origin.z - 25.0).abs() < 1e-9, "{side}");
|
||||
let mut reopened = Session { doc: solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap().into(), ..Session::default() };
|
||||
reopened.refresh();
|
||||
no_errors(&reopened, side);
|
||||
assert!((attached(&reopened, "Top").0.origin.z - 25.0).abs() < 1e-9, "{side}");
|
||||
// And solid again.
|
||||
run(&mut s, "timeline.edit", json!({"feature": "Wall", "set": {"thin": null, "extent": {"distance": "10"}}}));
|
||||
no_errors(&s, side);
|
||||
same_plane(&attached(&s, "Top").0, &top);
|
||||
same_plane(&attached(&s, "Bottom").0, &bottom);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sketch_on_a_thin_cap_is_certified_and_keeps_its_face_when_the_extrude_is_made_solid() {
|
||||
let mut s = Session::default();
|
||||
rectangle(&mut s);
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Outline", "thin": true, "thickness": 2, "side": "inside", "distance": 10, "name": "Wall"}));
|
||||
sketch_on(&mut s, [6.0, 6.0, 10.0], "Top");
|
||||
let (top, certificate) = attached(&s, "Top");
|
||||
assert!(is_extrude_cap(&certificate), "{certificate:?}");
|
||||
run(&mut s, "timeline.edit", json!({"feature": "Wall", "set": {"thin": null}}));
|
||||
no_errors(&s, "solid");
|
||||
same_plane(&attached(&s, "Top").0, &top);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_thin_wall_along_part_of_the_profile_does_not_take_over_its_cap() {
|
||||
// A wall along two sides is another wall than the rectangle's: the sketch reports its lost
|
||||
// face instead of moving onto the new one.
|
||||
let mut s = Session::default();
|
||||
let sides = rectangle(&mut s);
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Outline", "distance": 10, "name": "Wall"}));
|
||||
sketch_on(&mut s, [6.0, 6.0, 10.0], "Top");
|
||||
let _ = s.execute("timeline.edit", &json!({"feature": "Wall", "set": {"thin": {"curves": [&sides[0], &sides[1]], "thickness": "2"}}}));
|
||||
assert!(matches!(attachment(&s, "Top"), solidcraft_doc::FaceAttachmentStatus::Lost { .. }), "{:?}", attachment(&s, "Top"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_caps_of_walls_apart_attach_by_their_history_names() {
|
||||
// Two rectangles make two walls: their caps aren't certified (one signature would name
|
||||
// both), so a sketch on one attaches by its exact history name, as before.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Outline"}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [20, 0], "p1": [30, 10]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "Outline", "thin": true, "thickness": 1, "side": "inside", "distance": 5}));
|
||||
sketch_on(&mut s, [0.5, 0.5, 5.0], "Top");
|
||||
let (_, certificate) = attached(&s, "Top");
|
||||
assert!(matches!(certificate, Some(solidcraft_doc::config::FaceCertificate::Lineage)), "{certificate:?}");
|
||||
}
|
||||
|
||||
fn no_errors(s: &Session, context: &str) {
|
||||
assert!(
|
||||
s.model.results.iter().all(|r| r.error.is_none()),
|
||||
"{context}: {:?}",
|
||||
s.model.results.iter().filter_map(|r| r.error.as_ref()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,12 @@ geometry and dimensions.
|
||||
Supported planar sources are exact Box faces, untapered New Body and Join Extrude caps and
|
||||
straight sketch sides, and planar faces of immutable STEP imports. Extrude caps also identify their
|
||||
outer boundary and holes by source curve IDs. Reordering profiles cannot substitute another
|
||||
profile's cap. A profile drawn over a face sketch's projected boundary (a rectangle
|
||||
profile's cap. A thin extrude's caps are bounded by offset curves the sketch doesn't have, so a
|
||||
wall built as one region is identified by the curves it follows instead (as its outer boundary,
|
||||
with no holes). That is the identity the solid caps had when the wall follows the whole profile,
|
||||
so a sketch on a cap keeps its face when the extrude is made thin, or solid again. A wall along
|
||||
other curves, or one of several walls apart, is another face: it is attached by its history
|
||||
name below, and a sketch made on the solid cap reports a lost attachment. A profile drawn over a face sketch's projected boundary (a rectangle
|
||||
traced over the whole face, or a strip whose sides lie on the face's edges) is identified by
|
||||
the drawn curves: where a drawn curve and a projected one cover the same edge, the drawn curve
|
||||
is the source. Two drawn curves, or two projections, over one edge remain ambiguous. Curved surfaces cannot host a planar sketch through this workflow.
|
||||
|
||||
Reference in new issue
Block a user