diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index 4c989db2..8eae9eb7 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -35,6 +35,9 @@ pub struct Body { pub(crate) face_sources: Option>>>, /// Sticky transient veto: authored face boundaries must survive later rebuilds. pub(crate) regularization_protected: bool, + /// Exact history of the operation that made this body, relative to that operation's + /// operands; `None` when the operation does not report one. + pub(crate) history: Option>, } #[derive(Clone, Debug, PartialEq)] @@ -334,6 +337,31 @@ pub(crate) fn from_p3(p: mt::Point3) -> Vec3 { Vec3::new(p.x, p.y, p.z) } +/// The distinct edges of `faces` in order of first use, and the faces (by position) each one +/// bounds: the one edge order of a body, used by [`Body::edges`], tessellations and +/// [`crate::history`]. +pub(crate) fn edge_order<'a, P: 'a, C: 'a, S: 'a>( + faces: impl IntoIterator>, +) -> (Vec>, Vec>) { + let mut index = std::collections::HashMap::new(); + let (mut edges, mut bounds): (Vec>, Vec>) = (Vec::new(), Vec::new()); + for (fi, f) in faces.into_iter().enumerate() { + for e in f.edge_iter() { + let i = *index.entry(e.id()).or_insert_with(|| { + edges.push(e.clone()); + bounds.push(Vec::new()); + edges.len() - 1 + }); + if let Some(slot) = bounds.get_mut(i) + && !slot.contains(&fi) + { + slot.push(fi); + } + } + } + (edges, bounds) +} + fn polyline_mid(pts: &[Vec3]) -> (Vec3, f64) { let len: f64 = pts.windows(2).map(|w| w.first().zip(w.get(1)).map(|(a, b)| a.dist(*b)).unwrap_or(0.0)).sum(); let mut acc = 0.0; @@ -363,6 +391,11 @@ impl Body { && self.splits == other.splits && self.face_sources == other.face_sources && self.regularization_protected == other.regularization_protected + && match (&self.history, &other.history) { + (Some(a), Some(b)) => std::sync::Arc::ptr_eq(a, b), + (None, None) => true, + _ => false, + } } pub(crate) fn new(solid: Solid) -> Result { @@ -402,6 +435,7 @@ impl Body { splits: None, face_sources: None, regularization_protected: false, + history: None, }) } @@ -410,6 +444,20 @@ impl Body { self } + /// Exact history of the operation that made this body, relative to that operation's + /// operands (see [`crate::history`]); `None` when the operation does not report one. + pub fn history(&self) -> Option<&crate::history::History> { + self.history.as_deref() + } + + /// Attach the history of the operation that made this body from `operands`, after checking + /// it describes this body. + pub(crate) fn with_history(mut self, history: crate::history::History, operands: &[&Body]) -> Result { + history.check(&self, operands)?; + self.history = Some(std::sync::Arc::new(history)); + Ok(self) + } + pub fn face_sources(&self) -> Option<&[Option]> { self.face_sources.as_deref().map(Vec::as_slice) } @@ -768,27 +816,9 @@ impl Body { } } } - let mut seen = std::collections::HashMap::new(); - for e in meshed.iter().flat_map(|s| s.edge_iter()) { - if seen.contains_key(&e.id()) { - continue; - } - seen.insert(e.id(), out.edges.len()); - let c = e.oriented_curve(); - out.edges.push(c.0.iter().map(|p| Vec3::new(p.x, p.y, p.z)).collect()); - } - // Which faces each edge bounds. - out.edge_faces = vec![Vec::new(); out.edges.len()]; - for (fi, face) in meshed.iter().flat_map(|s| s.face_iter()).enumerate() { - for e in face.edge_iter() { - if let Some(slot) = seen.get(&e.id()).and_then(|i| out.edge_faces.get_mut(*i)) { - let f = u32::try_from(fi).unwrap_or(u32::MAX); - if !slot.contains(&f) { - slot.push(f); - } - } - } - } + let (edges, faces) = edge_order(meshed.iter().flat_map(|s| s.face_iter())); + out.edges = edges.iter().map(|e| e.oriented_curve().0.iter().map(|p| Vec3::new(p.x, p.y, p.z)).collect()).collect(); + out.edge_faces = faces.into_iter().map(|fs| fs.into_iter().map(|f| u32::try_from(f).unwrap_or(u32::MAX)).collect()).collect(); Ok(out) }) } diff --git a/crates/kernel/src/history.rs b/crates/kernel/src/history.rs new file mode 100644 index 00000000..493fd16f --- /dev/null +++ b/crates/kernel/src/history.rs @@ -0,0 +1,267 @@ +//! Exact face and edge history: for each face and edge of an operation's result, what it came +//! from among the operation's operands, recorded by the code that built it (never sampled). +//! +//! Indices are positions in a body's face order (`face_iter`) and edge order (`unique_edges`, +//! the order of [`Body::edges`] and of a tessellation's edges). Operand numbers are positions in +//! the operation's own operand list, which each operation documents. A body made by an +//! operation carries its history (see [`Body::history`]); [`History::substitute`] composes an +//! operation with one that made one of its operands. + +use std::collections::BTreeSet; + +use crate::body::{Body, FaceSource, Solid}; +use crate::{KernelError, Result}; + +/// 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, + pub change: Change, +} + +/// Where a result edge came from. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum EdgeOrigin { + /// (Part of) edge `edge` of input `operand`. + Edge { operand: usize, edge: usize }, + /// Where two faces meet that met nowhere before (a boolean's intersection curve). + Meet { a: FaceOrigin, b: FaceOrigin }, +} + +/// A result edge: the result faces it separates and what it came from (empty when it is new +/// and lies between faces new to the operation). +#[derive(Clone, Debug, PartialEq)] +pub struct EdgeProv { + pub faces: Vec, + pub origins: Vec, +} + +/// History of an operation's result: one entry per face and per edge, in the result's orders. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct History { + /// How many operands the operation had. + pub operands: usize, + pub faces: Vec, + pub edges: Vec, +} + +fn failed(detail: &str) -> KernelError { + KernelError::Failed(format!("history: {detail}")) +} + +impl FaceOrigin { + /// The same origin with its operand (if it has one) renumbered by `f`. + fn map_operand(self, f: impl Fn(usize) -> usize) -> FaceOrigin { + match self { + FaceOrigin::Face { operand, face } => FaceOrigin::Face { operand: f(operand), face }, + FaceOrigin::Edge { operand, edge } => FaceOrigin::Edge { operand: f(operand), edge }, + FaceOrigin::Offset { operand, face } => FaceOrigin::Offset { operand: f(operand), face }, + other => other, + } + } +} + +impl EdgeOrigin { + /// The same origin with its operands renumbered by `f`. + fn map_operand(self, f: impl Fn(usize) -> usize) -> EdgeOrigin { + match self { + EdgeOrigin::Edge { operand, edge } => EdgeOrigin::Edge { operand: f(operand), edge }, + EdgeOrigin::Meet { a, b } => EdgeOrigin::Meet { a: a.map_operand(&f), b: b.map_operand(&f) }, + } + } +} + +impl History { + /// The history of an operation that keeps every face and edge of its one operand in place + /// (a rigid move, a rebuild that keeps the topology's order). + pub fn identity(body: &Body) -> History { + let faces = + (0..body.face_count()).map(|face| FaceProv { origins: vec![FaceOrigin::Face { operand: 0, face }], change: Change::Kept }).collect(); + let edges = edge_faces(&body.solid) + .into_iter() + .enumerate() + .map(|(edge, faces)| EdgeProv { faces, origins: vec![EdgeOrigin::Edge { operand: 0, edge }] }) + .collect(); + History { operands: 1, faces, edges } + } + + /// Check that this history describes `result` made from `operands`: one entry per face and + /// edge, and every origin within its operand's faces and edges. + pub fn check(&self, result: &Body, operands: &[&Body]) -> Result<()> { + if self.operands != operands.len() { + return Err(failed("operand count does not match")); + } + if self.faces.len() != result.face_count() || self.edges.len() != result.edge_count() { + return Err(failed("entries do not match the result's faces and edges")); + } + let counts: Vec<(usize, usize)> = operands.iter().map(|b| (b.face_count(), b.edge_count())).collect(); + let face_ok = |o: &FaceOrigin| match *o { + FaceOrigin::Face { operand, face } | FaceOrigin::Offset { operand, face } => counts.get(operand).is_some_and(|c| face < c.0), + FaceOrigin::Edge { operand, edge } => counts.get(operand).is_some_and(|c| edge < c.1), + FaceOrigin::Profile { .. } | FaceOrigin::Cap { .. } => true, + }; + if !self.faces.iter().all(|f| f.origins.iter().all(face_ok)) { + return Err(failed("a face origin lies outside its operand")); + } + for e in &self.edges { + if e.faces.iter().any(|f| *f >= self.faces.len()) { + return Err(failed("an edge lies on a face outside the result")); + } + let ok = e.origins.iter().all(|o| match o { + EdgeOrigin::Edge { operand, edge } => counts.get(*operand).is_some_and(|c| *edge < c.1), + EdgeOrigin::Meet { a, b } => face_ok(a) && face_ok(b), + }); + if !ok { + return Err(failed("an edge origin lies outside its operand")); + } + } + Ok(()) + } + + /// Compose with the history of the operation that made operand `k`: origins on operand `k` + /// become that operation's origins, its operands (none, for a body made from a sketch) + /// taking `k`'s place in the operand list. Fails, without changing `self`, where a lineage + /// cannot be expressed (an offset of a face that operation generated, a blend of an edge + /// it generated, an edge between merged faces). + pub fn substitute(&mut self, k: usize, inner: &History) -> Result<()> { + if k >= self.operands { + return Err(failed("no such operand")); + } + let n = inner.operands; + // Operands before k stay, k's come in at k, those after k move by n - 1. + let outer = |op: usize| if op < k { op } else { op + n - 1 }; + let lifted = |op: usize| op + k; + let inner_face = |face: usize| inner.faces.get(face).ok_or_else(|| failed("a face origin lies outside the inner result")); + let inner_edge = |edge: usize| inner.edges.get(edge).ok_or_else(|| failed("an edge origin lies outside the inner result")); + // A face origin as origins of the composed operation, with the inner face's change. + let map_face = |o: FaceOrigin| -> Result<(Vec, Option)> { + match o { + FaceOrigin::Face { operand, face } if operand == k => { + let p = inner_face(face)?; + Ok((p.origins.iter().map(|x| x.map_operand(lifted)).collect(), Some(p.change))) + } + FaceOrigin::Offset { operand, face } if operand == k => match inner_face(face)?.origins.as_slice() { + [FaceOrigin::Face { operand, face }] => Ok((vec![FaceOrigin::Offset { operand: lifted(*operand), face: *face }], None)), + _ => Err(failed("an offset of a face the inner operation generated or merged")), + }, + FaceOrigin::Edge { operand, edge } if operand == k => match inner_edge(edge)?.origins.as_slice() { + [EdgeOrigin::Edge { operand, edge }] => Ok((vec![FaceOrigin::Edge { operand: lifted(*operand), edge: *edge }], None)), + _ => Err(failed("a blend of an edge the inner operation generated")), + }, + other => Ok((vec![other.map_operand(outer)], None)), + } + }; + let mut faces = Vec::with_capacity(self.faces.len()); + for f in &self.faces { + let mut origins = BTreeSet::new(); + let (mut split, mut kept) = (f.change == Change::Split, f.change == Change::Kept); + for o in &f.origins { + let (os, change) = map_face(*o)?; + origins.extend(os); + split |= change == Some(Change::Split); + kept &= change.is_none_or(|c| c == Change::Kept); + } + let origins: Vec = origins.into_iter().collect(); + let change = if f.change == Change::Generated || !origins.iter().any(|o| matches!(o, FaceOrigin::Face { .. })) { + Change::Generated + } else if origins.len() > 1 { + Change::Merged + } else if split { + Change::Split + } else if kept { + Change::Kept + } else { + Change::Modified + }; + faces.push(FaceProv { origins, change }); + } + // One side of a meeting: a single face of the composed operation. + let side = |o: FaceOrigin| -> Result { + match map_face(o)?.0.as_slice() { + [x] => Ok(*x), + _ => Err(failed("an edge meets a face the inner operation merged")), + } + }; + let mut edges = Vec::with_capacity(self.edges.len()); + for e in &self.edges { + let mut origins = BTreeSet::new(); + for o in &e.origins { + match *o { + EdgeOrigin::Edge { operand, edge } if operand == k => { + origins.extend(inner_edge(edge)?.origins.iter().map(|x| x.map_operand(lifted))) + } + EdgeOrigin::Edge { .. } => { + origins.insert(o.map_operand(outer)); + } + EdgeOrigin::Meet { a, b } => { + origins.insert(EdgeOrigin::Meet { a: side(a)?, b: side(b)? }); + } + } + } + edges.push(EdgeProv { faces: e.faces.clone(), origins: origins.into_iter().collect() }); + } + self.operands = self.operands + n - 1; + self.faces = faces; + self.edges = edges; + Ok(()) + } + + /// The result's face sources, carried from its operands' through this history: a face + /// kept, modified or split from one face keeps that face's source; a merged face keeps the + /// source its origins share, if they share one; a generated face has none. + pub fn face_sources(&self, operands: &[&Body]) -> Vec> { + let source = |o: &FaceOrigin| match *o { + FaceOrigin::Face { operand, face } => operands.get(operand).and_then(|b| b.face_sources()).and_then(|s| s.get(face)).cloned().flatten(), + _ => None, + }; + self.faces + .iter() + .map(|f| { + if f.change == Change::Generated { + return None; + } + let mut sources = f.origins.iter().map(source); + let first = sources.next().flatten()?; + sources.all(|s| s.as_ref() == Some(&first)).then_some(first) + }) + .collect() + } +} + +/// The faces each edge separates, in the body's edge order. +pub(crate) fn edge_faces(solid: &Solid) -> Vec> { + crate::body::edge_order(solid.face_iter()).1 +} + +#[cfg(test)] +#[path = "history_tests.rs"] +mod tests; diff --git a/crates/kernel/src/history_tests.rs b/crates/kernel/src/history_tests.rs new file mode 100644 index 00000000..03d807c0 --- /dev/null +++ b/crates/kernel/src/history_tests.rs @@ -0,0 +1,174 @@ +use solvecraft_geom::Vec3; + +use super::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History}; +use crate::{Body, box_solid, transform}; + +fn face(operand: usize, face: usize) -> FaceOrigin { + FaceOrigin::Face { operand, face } +} + +fn prov(origins: Vec, change: Change) -> FaceProv { + FaceProv { origins, change } +} + +fn centroids(b: &Body) -> Vec { + b.faces(1e-3).unwrap().into_iter().map(|f| f.centroid).collect() +} + +#[test] +fn a_transform_keeps_every_face_and_edge_in_place() { + let a = box_solid(Vec3::ZERO, Vec3::new(40.0, 30.0, 10.0)).unwrap(); + let t = Vec3::new(5.0, -3.0, 2.0); + let moved = transform(&a, t, Vec3::ZERO, Vec3::Z, 0.7).unwrap(); + let h = moved.history().unwrap(); + h.check(&moved, &[&a]).unwrap(); + assert!(h.faces.iter().enumerate().all(|(i, f)| f.origins == vec![face(0, i)] && f.change == Change::Kept)); + // Face i of the result is face i of the input, moved. + let rot = |p: Vec3| { + let (s, c) = 0.7f64.sin_cos(); + Vec3::new(p.x * c - p.y * s, p.x * s + p.y * c, p.z) + t + }; + for (before, after) in centroids(&a).into_iter().zip(centroids(&moved)) { + assert!(rot(before).dist(after) < 1e-6, "{before:?} → {after:?}"); + } + // Each edge separates the same faces it did. + let (ea, em) = (super::edge_faces(&a.solid), super::edge_faces(&moved.solid)); + assert_eq!(ea, em); + assert!(h.edges.iter().all(|e| e.faces.len() == 2 && e.origins.len() == 1)); + // The history's edges are the tessellation's edges, bounding the same faces. + let mesh = moved.tessellate(1e-3).unwrap(); + let mesh_faces: Vec> = mesh.edge_faces.iter().map(|fs| fs.iter().map(|f| *f as usize).collect()).collect(); + assert_eq!(mesh_faces, h.edges.iter().map(|e| e.faces.clone()).collect::>()); +} + +/// Joining an extrusion: the tool came from a sketch (no operand bodies), so substituting its +/// history removes it from the operand list and its faces become generated profile faces. +#[test] +fn substitute_a_body_made_from_a_sketch() { + let extrude = History { + operands: 0, + faces: vec![prov(vec![FaceOrigin::Cap { end: false }], Change::Generated), prov(vec![FaceOrigin::Profile { segment: 3 }], Change::Generated)], + edges: vec![EdgeProv { faces: vec![0, 1], origins: vec![] }], + }; + let mut join = History { + operands: 2, + faces: vec![prov(vec![face(0, 2)], Change::Modified), prov(vec![face(1, 1)], Change::Kept)], + edges: vec![EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::Meet { a: face(0, 2), b: face(1, 1) }] }], + }; + join.substitute(1, &extrude).unwrap(); + assert_eq!(join.operands, 1); + assert_eq!(join.faces[0], prov(vec![face(0, 2)], Change::Modified)); + assert_eq!(join.faces[1], prov(vec![FaceOrigin::Profile { segment: 3 }], Change::Generated)); + assert_eq!(join.edges[0].origins, vec![EdgeOrigin::Meet { a: face(0, 2), b: FaceOrigin::Profile { segment: 3 } }]); +} + +#[test] +fn bodies_without_reported_history_have_none() { + let a = box_solid(Vec3::ZERO, Vec3::new(4.0, 3.0, 1.0)).unwrap(); + assert!(a.history().is_none()); +} + +#[test] +fn check_refuses_a_history_that_does_not_describe_the_body() { + let a = box_solid(Vec3::ZERO, Vec3::new(4.0, 3.0, 1.0)).unwrap(); + let mut h = History::identity(&a); + h.check(&a, &[&a]).unwrap(); + assert!(h.check(&a, &[]).is_err(), "operand count"); + let mut short = h.clone(); + short.faces.pop(); + assert!(short.check(&a, &[&a]).is_err(), "face count"); + h.faces[0].origins = vec![face(0, 6)]; + assert!(h.check(&a, &[&a]).is_err(), "origin beyond the operand's faces"); + assert!(a.clone().with_history(h, &[&a]).is_err()); +} + +/// A two-step chain: op1 makes B from (A0, A1); op2 makes C from (X, B, Y). Composing puts +/// A0, A1 in B's place: C's operands become (X, A0, A1, Y). +#[test] +fn substitute_composes_kept_split_merged_and_generated_faces() { + let op1 = History { + operands: 2, + faces: vec![ + prov(vec![face(0, 0)], Change::Kept), // B0 = A0.f0 + prov(vec![face(0, 1)], Change::Split), // B1, B2 = pieces of A0.f1 + prov(vec![face(0, 1)], Change::Split), // + prov(vec![face(1, 4)], Change::Modified), // B3 = A1.f4 trimmed + prov(vec![FaceOrigin::Profile { segment: 2 }], Change::Generated), // B4 new + ], + edges: vec![ + EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::Edge { operand: 0, edge: 7 }] }, + EdgeProv { faces: vec![3, 4], origins: vec![EdgeOrigin::Meet { a: face(1, 4), b: FaceOrigin::Profile { segment: 2 } }] }, + ], + }; + let mut op2 = History { + operands: 3, + faces: vec![ + prov(vec![face(1, 0)], Change::Kept), // kept B0 → kept A0.f0 + prov(vec![face(1, 1), face(1, 2)], Change::Merged), // B1 + B2 rejoined → one origin + prov(vec![face(1, 3)], Change::Kept), // kept B3 → modified A1.f4 + prov(vec![face(1, 4)], Change::Modified), // trimmed generated face + prov(vec![face(0, 5), face(2, 1)], Change::Merged), // X and Y faces renumbered + prov(vec![FaceOrigin::Edge { operand: 1, edge: 0 }], Change::Generated), // blend of B's edge 0 + ], + edges: vec![ + EdgeProv { faces: vec![0, 1], origins: vec![EdgeOrigin::Edge { operand: 1, edge: 1 }] }, + EdgeProv { faces: vec![2, 4], origins: vec![EdgeOrigin::Meet { a: face(1, 3), b: face(2, 1) }] }, + EdgeProv { faces: vec![4, 5], origins: vec![EdgeOrigin::Edge { operand: 2, edge: 3 }] }, + ], + }; + op2.substitute(1, &op1).unwrap(); + assert_eq!(op2.operands, 4); + let f = &op2.faces; + assert_eq!(f[0], prov(vec![face(1, 0)], Change::Kept)); + // Two pieces of one face, merged back: one origin, so not a merge across faces. + assert_eq!(f[1], prov(vec![face(1, 1)], Change::Split)); + assert_eq!(f[2], prov(vec![face(2, 4)], Change::Modified)); + assert_eq!(f[3], prov(vec![FaceOrigin::Profile { segment: 2 }], Change::Generated)); + assert_eq!(f[4], prov(vec![face(0, 5), face(3, 1)], Change::Merged)); + assert_eq!(f[5], prov(vec![FaceOrigin::Edge { operand: 1, edge: 7 }], Change::Generated)); + let e = &op2.edges; + assert_eq!(e[0].origins, vec![EdgeOrigin::Meet { a: face(2, 4), b: FaceOrigin::Profile { segment: 2 } }]); + assert_eq!(e[1].origins, vec![EdgeOrigin::Meet { a: face(2, 4), b: face(3, 1) }]); + assert_eq!(e[2].origins, vec![EdgeOrigin::Edge { operand: 3, edge: 3 }]); +} + +#[test] +fn substitute_refuses_lineage_it_cannot_express_and_leaves_history_unchanged() { + let inner = History { + operands: 1, + faces: vec![prov(vec![FaceOrigin::Profile { segment: 0 }], Change::Generated)], + edges: vec![EdgeProv { faces: vec![0], origins: vec![] }], + }; + // An offset of a face the inner operation generated. + let mut offset = History { operands: 1, faces: vec![prov(vec![FaceOrigin::Offset { operand: 0, face: 0 }], Change::Generated)], edges: vec![] }; + let before = offset.clone(); + assert!(offset.substitute(0, &inner).is_err()); + assert_eq!(offset, before); + // A blend of an edge the inner operation generated. + let mut blend = History { operands: 1, faces: vec![prov(vec![FaceOrigin::Edge { operand: 0, edge: 0 }], Change::Generated)], edges: vec![] }; + assert!(blend.substitute(0, &inner).is_err()); + // Out-of-range references and operands. + let mut bad = History { operands: 1, faces: vec![prov(vec![face(0, 9)], Change::Kept)], edges: vec![] }; + assert!(bad.substitute(0, &inner).is_err()); + assert!(bad.substitute(3, &inner).is_err()); +} + +#[test] +fn face_sources_follow_kept_split_and_agreeing_merged_faces() { + let a = box_solid(Vec3::ZERO, Vec3::new(4.0, 3.0, 1.0)).unwrap().with_box_certificate(7).unwrap(); + let src = |i: usize| a.face_sources().unwrap()[i].clone(); + assert!(src(0).is_some() && src(1).is_some() && src(0) != src(1)); + let h = History { + operands: 1, + faces: vec![ + prov(vec![face(0, 0)], Change::Kept), + prov(vec![face(0, 1)], Change::Split), + prov(vec![face(0, 0), face(0, 1)], Change::Merged), + prov(vec![face(0, 2), face(0, 2)], Change::Merged), + prov(vec![FaceOrigin::Cap { end: true }], Change::Generated), + ], + edges: vec![], + }; + let s = h.face_sources(&[&a]); + assert_eq!(s, vec![src(0), src(1), None, src(2), None]); +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 317d414c..bb71c63b 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -29,6 +29,7 @@ mod freeform; pub mod fuzz; mod heal; mod helix; +pub mod history; mod iges_in; mod iges_out; #[cfg(test)] diff --git a/crates/kernel/src/meshbody.rs b/crates/kernel/src/meshbody.rs index 7663ed64..0d96b99b 100644 --- a/crates/kernel/src/meshbody.rs +++ b/crates/kernel/src/meshbody.rs @@ -109,6 +109,7 @@ pub fn mesh_body(positions: &[Vec3], triangles: &[[u32; 3]]) -> Result { paint: None, splits: None, face_sources: None, + history: None, regularization_protected: false, }) } @@ -202,6 +203,7 @@ impl Body { paint: self.paint.clone(), splits: self.splits.clone(), face_sources: None, + history: None, regularization_protected: self.regularization_protected, }) } diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs index e1d37818..fc41c51a 100644 --- a/crates/kernel/src/ops.rs +++ b/crates/kernel/src/ops.rs @@ -647,24 +647,28 @@ pub fn transform_matrix(body: &Body, m: [[f64; 4]; 4]) -> Result { if m.iter().flatten().any(|x| !x.is_finite()) { return Err(KernelError::Invalid("non-finite transform".into())); } - if let Some(b) = body.mesh_transformed(&m) { - return Ok(b); - } - guard("transform", || { - let mat = mt::Matrix4::new( - m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], m[3][2], - m[3][3], - ); - // An affine placement retains face order and appearance metadata. - let sources = body.transformed_sources(m)?; - Body::new(placed_topology(&body.solid, mat)?).map(|placed| { - placed.with_color(body.color()).with_paint(body.paint().cloned()).with_face_sources(sources).inherit_regularization_protection(body) - }) - }) + let placed = match body.mesh_transformed(&m) { + Some(b) => b, + None => guard("transform", || { + let mat = mt::Matrix4::new( + m[0][0], m[0][1], m[0][2], m[0][3], m[1][0], m[1][1], m[1][2], m[1][3], m[2][0], m[2][1], m[2][2], m[2][3], m[3][0], m[3][1], + m[3][2], m[3][3], + ); + // An affine placement retains face order and appearance metadata. + let sources = body.transformed_sources(m)?; + Body::new(placed_topology(&body.solid, mat)?).map(|b| { + b.with_color(body.color()).with_paint(body.paint().cloned()).with_face_sources(sources).inherit_regularization_protection(body) + }) + })?, + }; + let history = crate::history::History::identity(&placed); + placed.with_history(history, &[body]) } -// A STEP periodic face may traverse a seam edge twice. Generic topology builders -// reject that wire while cloning, even though placement preserves its topology. +/// `source` moved by `matrix`, keeping its face and edge order (transforms report an identity +/// [`crate::history::History`] on that basis). Built here rather than by generic topology +/// builders: a STEP periodic face may traverse a seam edge twice, and those reject that wire +/// while cloning even though placement preserves its topology. pub(crate) fn placed_topology(source: &Solid, matrix: mt::Matrix4) -> Result { use mt::{Transform, Transformed}; use std::collections::HashMap; diff --git a/crates/kernel/src/provenance.rs b/crates/kernel/src/provenance.rs index 5a1b696c..b46703fd 100644 --- a/crates/kernel/src/provenance.rs +++ b/crates/kernel/src/provenance.rs @@ -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, - 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, - 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, - pub edges: Vec, -} +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 { +pub fn trace(result: &Body, inputs: &[&Body]) -> Result { 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 { } } } - 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 }]); diff --git a/crates/kernel/src/splitface.rs b/crates/kernel/src/splitface.rs index 10f29cab..e5dc2049 100644 --- a/crates/kernel/src/splitface.rs +++ b/crates/kernel/src/splitface.rs @@ -887,6 +887,7 @@ pub(crate) fn split_any(body: &Body, faces: &[usize], tool: &SplitTool) -> Resul splits: body.splits.clone(), face_sources: None, regularization_protected: true, + history: None, }, imp, )) diff --git a/crates/kernel/src/surfaces.rs b/crates/kernel/src/surfaces.rs index 95dd7880..f2eff4e4 100644 --- a/crates/kernel/src/surfaces.rs +++ b/crates/kernel/src/surfaces.rs @@ -31,6 +31,7 @@ fn surface_body(faces: Vec) -> Body { splits: None, face_sources: None, regularization_protected: false, + history: None, } } @@ -49,6 +50,7 @@ fn body_of(faces: Vec) -> Result { splits: None, face_sources: None, regularization_protected: false, + history: None, }) } }