Persistent face names: keep exact history when a prism boolean meets stacked pieces of one cylinder (#1209) (#2205)

* claim: #1209

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YdMi9BDdjWZ8twfeBHAXz

* Prism boolean: a curved edge at one level crosses the walls there only (#1209)

The prism boolean sews its result slab by slab and keeps, for each section segment, the operand
wall it was read beside; its history is built from those tags. `Stations::new` took every curved
edge between two walls as crossing them at every level, so where two pieces of one cylinder are
stacked (an arm's round end under a boss of that radius) the circle between them cleared the
wall tags of every slab. `prism_lineage::sewn` then gave up, the join or cut came out with no
history, and every face of the body was named from geometry (`exact: false`).

A curve whose control points lie at one level (a B-spline, or a NURBS curve with positive
weights, which lies in their convex hull) is at that level: it crosses the walls there only.
Any other curve still spans every level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YdMi9BDdjWZ8twfeBHAXz

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-18 05:33:30 -04:00
1 parent 2e4266cb0a
commit 292747e50b
3 files changed
+136 -3

No files matched your search

@@ -501,3 +501,26 @@ fn bodies_apart_joined_keep_their_faces() {
}
assert!(faces_of(h).iter().all(|(change, origins)| *change == Change::Kept && origins.len() == 1), "{}", show(h));
}
/// A plate with a round end and a boss of that radius standing on it (#1209): the boss's wall
/// and the plate's round end are two pieces of one cylinder, one over the other, meeting along a
/// circle at the plate's top. That circle crosses the walls at its level only, so a second boss
/// joined to the plate and a cut through the first, both prism booleans sewn slab by slab, keep
/// every face of the plate and its boss, with their history.
#[test]
fn stacked_pieces_of_one_cylinder_keep_their_walls() {
let plate =
boolean(&boxed(0.0, -6.0, 0.0, 40.0, 6.0, 5.0), &crate::cylinder(Vec3::ZERO, Vec3::Z, 10.0, 5.0).unwrap(), BoolOp::Union).unwrap().unwrap();
let arm = boolean(&plate, &crate::cylinder(Vec3::ZERO, Vec3::Z, 10.0, 8.0).unwrap(), BoolOp::Union).unwrap().unwrap();
verified(&arm, &[&plate, &crate::cylinder(Vec3::ZERO, Vec3::Z, 10.0, 8.0).unwrap()], 0.01);
// Standing over the plate's far end, as the boss at the arm's other end does.
let second = crate::cylinder(Vec3::new(40.0, 0.0, 0.0), Vec3::Z, 4.0, 7.0).unwrap();
let bore = boxed(-3.0, -3.0, -1.0, 3.0, 3.0, 10.0);
for (tool, op) in [(&second, BoolOp::Union), (&bore, BoolOp::Cut)] {
let r = boolean(&arm, tool, op).unwrap().unwrap();
let h = verified(&r, &[&arm, tool], 0.01);
for f in 0..arm.face_count() {
assert!(!from(h, face(0, f)).is_empty(), "{op:?}: face {f} of the plate and boss is lost\n{}", show(h));
}
}
}