feat(kernel): bodies joined apart report their history (#136)

union_apart joins bodies whose boxes are clearly apart (many pattern tools,
#261) without any intersection work: their shells side by side. Each face
and edge of the result is its piece's own, unchanged, and the result now
says so (a history over the pieces), so a pattern's cut keeps the exact
names of every copy's faces.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
Iraklis KourtisandClaude Opus 5.5 authored and bherbruck committed 2026-10-12 11:52:53 -04:00
1 parent 08d797235d
commit 2bda050872
2 files changed
+33 -1

No files matched your search

@@ -484,3 +484,20 @@ fn bore_union_of_a_bore_in_two_arcs_has_no_history() {
let r = crate::coplanar::bore_union(&floor, &ring, &|_| true).expect("the bores are recut");
assert!(r.history().is_none());
}
/// Bodies clearly apart, joined as one body of several pieces (`union_apart`): every face and
/// edge is its piece's own, kept.
#[test]
fn bodies_apart_joined_keep_their_faces() {
let pieces = [boxed(0.0, 0.0, 0.0, 2.0, 2.0, 2.0), boxed(5.0, 0.0, 0.0, 7.0, 2.0, 2.0), boxed(10.0, 0.0, 0.0, 12.0, 2.0, 2.0)];
let r = crate::union_apart(&pieces).unwrap().unwrap();
let operands: Vec<&Body> = pieces.iter().collect();
let h = verified(&r, &operands, 0.01);
assert_eq!(h.operands, 3);
for (k, piece) in pieces.iter().enumerate() {
for f in 0..piece.face_count() {
assert_eq!(from(h, face(k, f)).len(), 1, "{}", show(h));
}
}
assert!(faces_of(h).iter().all(|(change, origins)| *change == Change::Kept && origins.len() == 1), "{}", show(h));
}
+16 -1
View File
@@ -672,7 +672,22 @@ pub fn union_apart(bodies: &[Body]) -> Result<Option<Body>> {
}
}
}
join_pieces(bodies.to_vec())
// The pieces stay as they are, side by side: each face and edge of the result is its piece's
// own (the history over `bodies`).
let Some(joined) = join_pieces(bodies.to_vec())? else { return Ok(None) };
let operands: Vec<&Body> = bodies.iter().collect();
let faces = bodies
.iter()
.enumerate()
.flat_map(|(operand, b)| {
(0..b.face_count()).map(move |face| (std::collections::BTreeSet::from([crate::history::FaceOrigin::Face { operand, face }]), true))
})
.collect();
let mut edges = HashMap::new();
for (operand, b) in bodies.iter().enumerate() {
edges.extend(record::edge_origins(&b.solid, operand));
}
Ok(Some(record::traced(joined, &record::counts(&operands), Some(record::regions(faces)), Some(&edges))))
}
/// Bodies (apart from each other) as one body of several pieces.