feat(kernel): every boolean path reports exact face and edge history

ops::boolean's result carries its history over [a, b] whichever path made
it, recorded by the code that builds the faces:

- truck path (incl. jitter retries): shapeops history mapped through the
  order-preserving copies and the cut's turned tool, then through healing
- heal records which input faces each healed face merges and which input
  edges each joined or exact edge replaces (HealMap); opt-in, so heal_keep
  callers pay nothing
- trivial shortcuts, the void, glue, faces pushed apart (push_faces records
  its own history; the slab cut back on a union maps to the operands'
  faces in that plane), the nudged retry, by_lumps and join composition
- prism booleans: section segments carry tags (operand cap edge, the wall
  beside it, its vertices) through the 2D boolean; walls, caps and edges of
  the extruded or sewn result follow from them
- planar and faceted polygon booleans: polygons carry their operand face;
  faces join the pieces across edges, edges come from the pieces either side

History gains renumber/over (and consuming forms) for composition;
record gains regions/traced/build helpers. truck-shapeops
*_with_history now return the solid with an Option history. Paths that
cannot report (bore narrowing via offset, inexpressible lineage) leave the
result without history. Boolean results, tolerances and fallback order are
unchanged.

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 d69ebbee1b
commit d5cb276965
16 files changed
+2169 -312

No files matched your search

+2 -1
View File
@@ -26,7 +26,8 @@ Changes:
whose intersection made it. The code that creates faces and edges records them as it goes:
`divide_face` (face pieces), `loops_store` (intersection edges, split edges, edges rebuilt on
a moved vertex), across the passes that fold in the operands' extra shells. `and` / `or` are
wrappers that drop the history.
wrappers that drop the history; the `*_with_history` forms return the same solid with the
history as an `Option` (`None` where a face or edge went unrecorded).
## truck-stepio 0.3.0 (Apache-2.0, © RICOS Co. Ltd., https://github.com/ricosjp/truck)
+10 -10
View File
@@ -124,27 +124,27 @@ pub fn or<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
}
/// SolveCraft: AND operation between two solids, with where each face and edge of the result
/// came from (operand 0 is `solid0`, operand 1 is `solid1`). The operands must not share faces
/// or edges.
/// came from (operand 0 is `solid0`, operand 1 is `solid1`): the same result as [`and`], and
/// its history, `None` when a face or edge of it was not recorded. The operands must not share
/// faces or edges.
pub fn and_with_history<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<(Solid<Point3, C, S>, History)> {
let (solid, history) = boolean(solid0, solid1, tol, |[and, _]| and)?;
Some((solid, history?))
) -> Option<(Solid<Point3, C, S>, Option<History>)> {
boolean(solid0, solid1, tol, |[and, _]| and)
}
/// SolveCraft: OR operation between two solids, with where each face and edge of the result
/// came from (operand 0 is `solid0`, operand 1 is `solid1`). The operands must not share faces
/// or edges.
/// came from (operand 0 is `solid0`, operand 1 is `solid1`): the same result as [`or`], and
/// its history, `None` when a face or edge of it was not recorded. The operands must not share
/// faces or edges.
pub fn or_with_history<C: ShapeOpsCurve<S>, S: ShapeOpsSurface>(
solid0: &Solid<Point3, C, S>,
solid1: &Solid<Point3, C, S>,
tol: f64,
) -> Option<(Solid<Point3, C, S>, History)> {
let (solid, history) = boolean(solid0, solid1, tol, |[_, or]| or)?;
Some((solid, history?))
) -> Option<(Solid<Point3, C, S>, Option<History>)> {
boolean(solid0, solid1, tol, |[_, or]| or)
}
/// Combines the operands' first shells, then folds in their other shells one at a time
@@ -85,6 +85,7 @@ fn box_union_box_history() {
let a = cuboid(Point3::origin(), Vector3::new(1.0, 1.0, 1.0));
let b = cuboid(Point3::new(0.5, 0.25, 0.25), Vector3::new(1.0, 0.5, 0.5));
let (or, history) = crate::or_with_history(&a, &b, 0.05).unwrap();
let history = history.unwrap();
let (kept, meets) = check(&or, [&a, &b], &history, 0.05);
// a's five faces away from b and b's far end are kept; a's face at x = 1 gets a hole and
// b's four sides are trimmed where they enter a.
@@ -111,6 +112,7 @@ fn box_minus_cylinder_history() {
let mut cylinder = builder::tsweep(&f, Vector3::unit_z() * 2.0);
cylinder.not();
let (cut, history) = crate::and_with_history(&cube, &cylinder, 0.05).unwrap();
let history = history.unwrap();
let (kept, meets) = check(&cut, [&cube, &cylinder], &history, 0.05);
// The cube's four sides are kept; its top and bottom get holes; only the cylinder's wall
// remains of the tool, trimmed to the cube.
@@ -137,6 +139,7 @@ fn multi_shell_operand_history() {
// Covers the upper half of the hollow cube: its bottom crosses both shells.
let slab = cuboid(Point3::new(-0.5, -0.5, 1.5), Vector3::new(4.0, 4.0, 2.5));
let (and, history) = crate::and_with_history(&hollow, &slab, 0.05).unwrap();
let history = history.unwrap();
let (kept, meets) = check(&and, [&hollow, &slab], &history, 0.05);
// The outer top and the void's top (numbered after the outer shell's six faces) are kept.
assert_eq!(kept, 2);