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

+28 -2
View File
@@ -10,6 +10,7 @@ use solvecraft_geom::{Plane, Region2, Vec3};
use truck_modeling as mt;
use crate::body::{Body, from_p3, p3};
use crate::history::{Change, EdgeOrigin, FaceOrigin};
use crate::{KernelError, Result, guard};
impl Body {
@@ -430,10 +431,22 @@ pub fn thicken(body: &Body, thickness: f64, symmetric: bool) -> Result<Body> {
Ok(nf)
};
let mut out = Vec::new();
for (f, off) in faces.iter().zip(&offs) {
// History (one operand, the surface): a copy at no distance is the face itself, a copy
// further out its offset, a wall is made along its free edge.
let edge_index = crate::record::edge_index(body);
let mut history = Vec::new();
let copy = |i: usize, d: f64| {
if d == 0.0 {
crate::record::from_face(0, i, Change::Kept)
} else {
crate::record::generated(vec![FaceOrigin::Offset { operand: 0, face: i }])
}
};
for (i, (f, off)) in faces.iter().zip(&offs).enumerate() {
// The lower copy faces back, the upper one forward.
out.push(moved_face(f, off, 0, lo)?.inverse());
out.push(moved_face(f, off, 1, hi)?);
history.extend([copy(i, lo), copy(i, hi)]);
// Walls along free edges, outward across the edge.
for w in f.boundaries() {
for e in w.edge_iter() {
@@ -466,6 +479,8 @@ pub fn thicken(body: &Body, thickness: f64, symmetric: bool) -> Result<Body> {
x
};
out.push(wall);
let edge = edge_index.get(&abs_id).copied().ok_or_else(|| KernelError::Failed("thicken: an edge outside the surface".into()))?;
history.push(crate::record::generated(vec![FaceOrigin::Edge { operand: 0, edge }]));
}
}
}
@@ -473,7 +488,18 @@ pub fn thicken(body: &Body, thickness: f64, symmetric: bool) -> Result<Body> {
if shell.shell_condition() != mt::ShellCondition::Closed {
return Err(KernelError::Failed("the thickened surface does not close".into()));
}
Body::new(mt::Solid::new_unchecked(vec![shell]))
// The edges copied at no distance are the surface's own edges, rebuilt.
let mut copies = HashMap::new();
for (id, (low, high)) in &emoved {
for (copy, d) in [(low, lo), (high, hi)] {
if let Some(&edge) = edge_index.get(id)
&& d == 0.0
{
copies.insert(copy.id(), EdgeOrigin::Edge { operand: 0, edge });
}
}
}
crate::record::attach_edges(Body::new(mt::Solid::new_unchecked(vec![shell]))?, &[body], history, copies)
})
}