feat(kernel): exact face and edge history model

Bodies can now carry the exact history of the operation that made them:
for each result face its origins among the operation's operands (kept,
modified, split, merged, or generated from a profile segment, cap, edge or
offset) and for each edge the faces it separates and the operand edge or
face pair it came from. History is recorded by the operation, never
sampled, and checked against the body when attached.

- history.rs: the vocabulary (moved from provenance.rs), History::check,
  identity, substitute (composes an operation with the one that made an
  operand, including bodies made from a sketch) and face-source propagation.
- One edge order for a body (body::edge_order), shared by tessellation and
  history, so history edge indices are the mesh's edge indices.
- Transforms report an identity history; placed_topology documents the
  face/edge order guarantee it relies on.
- provenance.rs keeps the geometric trace for operations that do not yet
  report history.

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 65a99608d9
commit c87efa1f35
9 files changed
+530 -99

No files matched your search

+12 -62
View File
@@ -1,4 +1,5 @@
//! Face and edge provenance: where each face (and edge) of an operation's result came from.
//! Face and edge provenance found by geometry: where each face (and edge) of an operation's
//! result came from, for operations that do not yet report exact [`crate::history`].
//!
//! [`trace`] works for any operation: a result face that lies on an input face (on its surface,
//! inside its trimmed region) comes from that face; one that lies on none is generated by the
@@ -13,58 +14,7 @@ use solvecraft_geom::{Mesh, Plane, Region2, Seg2, Vec2, Vec3};
use crate::Result;
use crate::body::Body;
/// Where a result face came from.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FaceOrigin {
/// A face of input `operand` (its index in the operand's face order).
Face { operand: usize, face: usize },
/// Swept from segment `segment` of the profile's loops (in order: outer, then holes).
Profile { segment: usize },
/// The start or end cap of a sweep (the profile itself, or where it ends up).
Cap { end: bool },
/// Made in place of edge `edge` of input `operand` (a fillet or chamfer).
Edge { operand: usize, edge: usize },
/// An offset of face `face` of input `operand` (a shell's inner wall).
Offset { operand: usize, face: usize },
}
/// How a result face relates to its origins.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Change {
/// The same face as its origin (same region).
Kept,
/// Its origin's surface, trimmed differently.
Modified,
/// One of several result faces made from one input face.
Split,
/// Made from several input faces (coplanar or cosurface faces joined).
Merged,
/// New to the operation (no input face under it).
Generated,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FaceProv {
pub origins: Vec<FaceOrigin>,
pub change: Change,
}
/// Where a result edge came from: the result faces it separates and, when it lies on an edge
/// of an input body, that edge.
#[derive(Clone, Debug, PartialEq)]
pub struct EdgeProv {
pub faces: Vec<usize>,
pub origin: Option<(usize, usize)>,
}
/// Provenance of an operation's result: one entry per face (in the result's face order) and
/// per edge (in its mesh's edge order).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Provenance {
pub faces: Vec<FaceProv>,
pub edges: Vec<EdgeProv>,
}
use crate::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History};
/// Up to this many sample points per result face.
const SAMPLES: usize = 24;
@@ -153,7 +103,7 @@ fn face_samples(f: &FaceTris) -> Vec<(Vec3, Vec3)> {
}
/// Provenance of `result` from the bodies it was made of (in operand order), by geometry.
pub fn trace(result: &Body, inputs: &[&Body]) -> Result<Provenance> {
pub fn trace(result: &Body, inputs: &[&Body]) -> Result<History> {
let size = inputs.iter().map(|b| b.size()).fold(result.size(), f64::max).max(1e-9);
let mesh_tol = size * 2e-4;
let tol = mesh_tol * 2.0;
@@ -221,9 +171,9 @@ pub fn trace(result: &Body, inputs: &[&Body]) -> Result<Provenance> {
}
}
}
edges.push(EdgeProv { faces: fs, origin });
edges.push(EdgeProv { faces: fs, origins: origin.map(|(operand, edge)| EdgeOrigin::Edge { operand, edge }).into_iter().collect() });
}
Ok(Provenance { faces, edges })
Ok(History { operands: inputs.len(), faces, edges })
}
/// The profile segments of the loops, in [`FaceOrigin::Profile`] order (outer, then holes).
@@ -320,7 +270,7 @@ pub fn revolve_origins(tool: &Body, plane: &Plane, regions: &[Region2], axis_ori
/// Fill in generated faces of a fillet or chamfer (`result` from `input` with `edges` blended,
/// edge indices in `input`'s mesh order): each blend face comes from the edge it lies along.
pub fn blend_origins(result: &Body, input: &Body, edges: &[usize], prov: &mut Provenance) -> Result<()> {
pub fn blend_origins(result: &Body, input: &Body, edges: &[usize], prov: &mut History) -> Result<()> {
let size = input.size().max(1e-9);
let rm = result.tessellate(size * 2e-4)?;
let im = input.tessellate(size * 2e-4)?;
@@ -346,7 +296,7 @@ pub fn blend_origins(result: &Body, input: &Body, edges: &[usize], prov: &mut Pr
/// Fill in generated faces of a shell or offset (`result` from `input`, walls `thickness`
/// inside): an inner face whose points, moved back out by the thickness, land on an input face
/// comes from that face ([`FaceOrigin::Offset`]).
pub fn offset_origins(result: &Body, input: &Body, thickness: f64, prov: &mut Provenance) -> Result<()> {
pub fn offset_origins(result: &Body, input: &Body, thickness: f64, prov: &mut History) -> Result<()> {
let size = input.size().max(1e-9);
let tol = size * 4e-4;
let rm = result.tessellate(size * 2e-4)?;
@@ -431,7 +381,7 @@ pub fn sweep_origins(tool: &Body, plane: &Plane, regions: &[Region2]) -> Result<
/// Replace origins on operand `operand` (a tool made by the operation) with that tool's own
/// origins (`tool[k]` for its face k): a result face from the tool's side face k comes from the
/// profile segment that swept it.
pub fn compose(prov: &mut Provenance, operand: usize, tool: &[FaceOrigin]) {
pub fn compose(prov: &mut History, operand: usize, tool: &[FaceOrigin]) {
for fp in &mut prov.faces {
for o in &mut fp.origins {
if let FaceOrigin::Face { operand: op, face } = *o
@@ -501,7 +451,7 @@ mod tests {
let m = cut.tessellate(1e-3).unwrap();
let p = trace(&cut, &[&a, &pin]).unwrap();
let corner = (0..m.edges.len()).find(|i| m.edges[*i].iter().all(|q| q.x.abs() < 1e-9 && q.y.abs() < 1e-9)).unwrap();
assert!(matches!(p.edges[corner].origin, Some((0, _))));
assert!(matches!(p.edges[corner].origins[..], [super::EdgeOrigin::Edge { operand: 0, .. }]));
assert_eq!(p.edges[corner].faces.len(), 2);
}
@@ -509,7 +459,7 @@ mod tests {
fn provenance_of_sweeps_blends_and_shells() {
use solvecraft_geom::{Loop2, Plane, Region2, Vec2};
use super::{Provenance, blend_origins, compose, extrude_origins, offset_origins, revolve_origins};
use super::{History, blend_origins, compose, extrude_origins, offset_origins, revolve_origins};
let plane = Plane::named("XY").unwrap();
// A 40 × 30 rectangle (segments 0: bottom, 1: right, 2: top, 3: left) with a hole.
let rect = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(40.0, 0.0), Vec2::new(40.0, 30.0), Vec2::new(0.0, 30.0)]);
@@ -543,7 +493,7 @@ mod tests {
let bm = b.tessellate(1e-3).unwrap();
let e = (0..bm.edges.len()).find(|i| bm.edges[*i].iter().all(|q| (q.y - 30.0).abs() < 1e-9 && (q.z - 20.0).abs() < 1e-9)).unwrap();
let f = crate::fillet(&b, &[Vec3::new(20.0, 30.0, 20.0)], 5.0).unwrap();
let mut p: Provenance = trace(&f, &[&b]).unwrap();
let mut p: History = trace(&f, &[&b]).unwrap();
blend_origins(&f, &b, &[e], &mut p).unwrap();
let blend = face_at(&f, Vec3::new(20.0, 25.0 + 5.0 * std::f64::consts::FRAC_1_SQRT_2, 15.0 + 5.0 * std::f64::consts::FRAC_1_SQRT_2));
assert_eq!(p.faces[blend].origins, vec![FaceOrigin::Edge { operand: 0, edge: e }]);