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:
1 parent
65a99608d9
commit
c87efa1f35
9 files changed
+530
-99
No files matched your search
@@ -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 }]);
|
||||
|
||||
Reference in new issue
Block a user