feat(kernel): an edge's meeting sides are sets of origins (merged faces)

EdgeOrigin::Meet now names each side as a Side: the sorted, de-duplicated
origins of the face there. substitute maps a side to the union of the inner
faces' origins, so a meeting with a face an inner operation merged keeps its
lineage; meeting() derives sides from whole faces. Fillets after box cuts
that leave coplanar pieces now report exact history. Prism lineage meets a
cap of several origins and resolves wall junctions away from shared
vertices by the operand's one edge between the walls. bore_union's recut
reports history only for a one-face bore (pieces of a two-arc bore's recut
wall were given both arcs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 08d7037176
commit 43ddde4a5d
11 files changed
+347 -76

No files matched your search

+59 -2
View File
@@ -193,7 +193,7 @@ fn chamfer_cutters_mitre_at_a_corner() {
}
// The two chamfers meet along the mitre.
let (a, c) = (edge(edge_at(&b, pts[0])), edge(edge_at(&b, pts[1])));
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::Meet { a, b: c }] || e.origins == [EdgeOrigin::Meet { a: c, b: a }]));
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::meet(a, c)]));
}
#[test]
@@ -218,7 +218,7 @@ fn delete_a_fillet() {
// The faces either side of the round meet again where they met nowhere in its input.
let (top, back_face) = (face_at(&r, Vec3::new(20.0, 10.0, 20.0)), face_at(&r, Vec3::new(20.0, 30.0, 5.0)));
let (x, y) = (face(top), face(back_face));
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::Meet { a: x.min(y), b: x.max(y) }]));
assert!(h.edges.iter().any(|e| e.origins == [EdgeOrigin::meet(x, y)]));
}
#[test]
@@ -376,3 +376,60 @@ fn mirrored_copy_keeps_every_face_and_edge() {
}
}
}
/// The block with box cuts of each common shape: a notch through the back, a pocket, a step
/// along one end, a slot across the top, a hole through, a corner bitten out.
fn box_cuts() -> Vec<(&'static str, Body, Body)> {
let b = cube();
[
("notch", Vec3::new(15.0, 25.0, -5.0), Vec3::new(25.0, 35.0, 25.0)),
("pocket", Vec3::new(10.0, 10.0, 10.0), Vec3::new(30.0, 20.0, 25.0)),
("step", Vec3::new(30.0, -5.0, 10.0), Vec3::new(45.0, 35.0, 25.0)),
("slot", Vec3::new(15.0, -5.0, 10.0), Vec3::new(25.0, 35.0, 25.0)),
("hole", Vec3::new(15.0, 10.0, -5.0), Vec3::new(25.0, 20.0, 25.0)),
("bite", Vec3::new(30.0, 20.0, 10.0), Vec3::new(45.0, 35.0, 25.0)),
]
.into_iter()
.map(|(name, lo, hi)| {
let tool = box_solid(lo, hi).unwrap();
let cut = boolean(&b, &tool, BoolOp::Cut).unwrap().unwrap();
(name, cut, tool)
})
.collect()
}
/// Meetings with a side of several origins (a face the modifier's heal merged).
fn merged_meetings(h: &History) -> usize {
h.edges.iter().flat_map(|e| &e.origins).filter(|o| matches!(o, EdgeOrigin::Meet { a, b } if a.single().is_none() || b.single().is_none())).count()
}
/// A fillet right after a box cut. Where the cut leaves coplanar pieces (the step, slot and
/// bite leave the front face in pieces), the fillet's heal merges them, and the edge where the
/// round runs into that face meets all of it. Composed with the cut, over the block and the tool.
#[test]
fn fillet_after_box_cuts() {
let b = cube();
let at = Vec3::new(5.0, 0.0, 20.0);
for (name, cut, tool) in box_cuts() {
let r = crate::fillet(&cut, &[at], 2.0).unwrap();
let h = verified(&r, &cut, reach(2.0 * 1.5));
assert_eq!(blends(h), [vec![edge(edge_at(&cut, at))]], "{name}");
let merged = matches!(name, "step" | "slot" | "bite");
assert_eq!(merged_meetings(h) > 0, merged, "{name}: {h:?}");
let mut through = h.clone();
through.substitute(0, cut.history().unwrap()).unwrap();
through.check(&r, &[&b, &tool]).unwrap();
}
}
/// A chamfer right after a box cut whose cutter's cut goes the prism way: the cutter's walls meet
/// the cut body's walls where a segment was split off.
#[test]
fn chamfer_after_box_cuts() {
let at = Vec3::new(5.0, 0.0, 20.0);
for (name, cut, _) in box_cuts().into_iter().filter(|(name, ..)| matches!(*name, "notch" | "hole")) {
let r = crate::chamfer(&cut, &[at], 2.0).unwrap();
let h = verified(&r, &cut, reach(2.0 * 1.5));
assert_eq!(with(h, &[edge(edge_at(&cut, at))]).len(), 1, "{name}: {h:?}");
}
}