feat(kernel): creation builders report exact face history

Extrude, revolve (partial and full turn), tapered extrude, loft (incl. to a
point), sweep, path sweep, coil, primitives, lip, emboss and thicken attach
the exact history of the bodies they build: each face's profile segment, cap
or operand face, and each edge's operand edge, recorded by topology identity
inside the building call. Nothing is sampled; a body whose faces do not follow
the expected construction carries no history. Face sources 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 cc4955a8da
commit 8c81bc60d0
11 files changed
+1088 -71

No files matched your search

+24 -1
View File
@@ -1,5 +1,6 @@
//! Exact radial embossing of orthogonal profiles on complete cylindrical walls.
use crate::body::{Body, Solid, from_p3, p3};
use crate::history::Change;
use crate::offset::{Surf, surf_of};
use crate::{KernelError, Result, guard};
use solvecraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
@@ -255,6 +256,9 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
let map2 = |p: Vec2| Vec2::new(s0 + plane.dir_to_world(p).dot(tangent), z0 + plane.dir_to_world(p).dot(axis));
let mut count = 0usize;
let mut mapped = Vec::new();
// The regions mapped onto the wall as given (their loops' turning may differ from the
// sketch's): profile numbering follows them.
let mut unoriented = Vec::new();
for reg in regions {
let mut loops = Vec::new();
for lp in std::iter::once(&reg.outer).chain(&reg.holes) {
@@ -293,6 +297,8 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
let outer = loops.first().ok_or_else(|| fail("profile"))?.ccw();
let holes = loops.iter().skip(1).map(|l| l.ccw().reversed()).collect();
mapped.push(Region2 { outer, holes });
let mut given = loops.into_iter();
unoriented.push(Region2 { outer: given.next().ok_or_else(|| fail("profile"))?, holes: given.collect() });
}
validate_profiles(&mapped, tol)?;
if qualify {
@@ -334,10 +340,19 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
let [p, q, s, t] = c3;
let mat = mt::Matrix4::new(a, b, c, d, e, f, g, h, i, j, l, n, p, q, s, t);
let raised = selected.oriented_surface().transformed(mat);
// History of the faces added (walls from their profile segments, the raised or sunk
// region the profile's end cap), in the order added.
let mut added_history = Vec::new();
let mut origins = crate::record::loop_origins(&unoriented).into_iter();
for reg in &mapped {
let mut cap = Vec::new();
for (li, lp) in std::iter::once(&reg.outer).chain(&reg.holes).enumerate() {
let (base, top, walls) = wrapped_loop(lp, r, rr, &at)?;
let loop_origins = origins.next().ok_or_else(|| fail("profile numbering"))?;
if loop_origins.len() != walls.len() {
return Err(fail("profile numbering"));
}
added_history.extend(loop_origins.iter().map(|o| crate::record::profile(o)));
if li == 0 {
footprints.push((lp, base.inverse()));
} else {
@@ -349,6 +364,7 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
added.extend(walls);
}
added.push(mt::Face::try_new(cap, raised.clone()).map_err(|e| fail(&e.to_string()))?);
added_history.push(crate::record::cap(true));
}
// An island inside a profile hole cuts that hole's original-radius patch,
// rather than adding another hole to the already-cut exterior patch.
@@ -363,6 +379,13 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
base_patches.get_mut(owner).ok_or_else(|| fail("profile ownership"))?.push(wire);
}
let mut all: Vec<_> = faces.iter().enumerate().filter(|(i, _)| *i != fi).map(|(_, f)| f.clone()).collect();
// History (one operand, the body; profile numbering over `regions`), face by face as
// gathered: the other faces kept, the wall's pieces left around the footprints (split
// when the profile's holes leave more than one), then the faces added.
let pieces = if base_patches.len() > 1 { Change::Split } else { Change::Modified };
let mut history: Vec<_> = (0..faces.len()).filter(|i| *i != fi).map(|i| crate::record::from_face(0, i, Change::Kept)).collect();
history.extend(std::iter::repeat_n(crate::record::from_face(0, fi, pieces), base_patches.len()));
history.extend(added_history);
for patch in base_patches {
all.push(mt::Face::try_new(patch, selected.oriented_surface()).map_err(|e| fail(&e.to_string()))?);
}
@@ -372,7 +395,7 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
if !result.validity().is_empty() {
return Err(fail("rebuilt cylinder is not a closed manifold"));
}
Ok(EmbossOutcome::Body(result))
Ok(EmbossOutcome::Body(crate::record::attach(result, &[body], history)?))
})
}