brep: history types move to solidcraft_geom::brep::history

FaceOrigin, Change, FaceProv, Side, EdgeOrigin, EdgeProv and History move
unchanged (fields, serde derives) to solidcraft-geom, so a backend can
report history without depending on solidcraft-kernel. The pure methods
(composition, renumbering, count checks, mark_shared_split) move with
them and return brep::history::Failed, which converts to
KernelError::Failed with the same "history: …" message.

What needs a kernel body (History::identity, check, face_sources) stays
in the kernel as the BodyHistory trait; solidcraft_kernel::history
re-exports the types, so existing paths keep working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
jyviko
2026-10-20 22:53:52 -04:00
parent 4afbe3b909
commit 5a9bd4398d
17 changed files with 420 additions and 362 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ use std::sync::{Arc, OnceLock};
use solidcraft_geom::Region2;
use solidcraft_kernel::Body;
use solidcraft_kernel::history::{Change, FaceOrigin, FaceProv, History};
use solidcraft_kernel::history::{BodyHistory, Change, FaceOrigin, FaceProv, History};
use crate::SolvedSketch;
+1
View File
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
mod curve2;
pub mod envelope;
pub mod history;
pub use curve2::Curve2;
pub const VERSION: u32 = 3;
+380
View File
@@ -0,0 +1,380 @@
//! 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 canonical face and edge indices (docs/native-brep.md, "Canonical order"), the
//! positions in a body's face order and edge order. Operand numbers are positions in the
//! operation's own operand list, which each operation documents. An operation returns its
//! history beside the B-rep; a backend that cannot report it returns none and never guesses.
//! These are the runtime types; the saved form is [`super::envelope`], kept separate so that a
//! change here cannot redefine saved fields. `solidcraft_kernel::history` re-exports them and
//! adds what needs a kernel body.
use std::collections::{BTreeSet, HashMap};
/// Why a history could not be composed or checked.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("history: {0}")]
pub struct Failed(pub &'static str);
type Result<T> = std::result::Result<T, Failed>;
fn failed(detail: &'static str) -> Failed {
Failed(detail)
}
/// Where a result face came from.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
pub struct FaceProv {
pub origins: Vec<FaceOrigin>,
pub change: Change,
}
/// One side of a meeting: the origins of the face on that side of the edge. One origin, or
/// several where that face was made from several (a merged face) and the edge lies along it
/// as a whole. Never empty; sorted, without repeats, so equal sides compare equal.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
pub struct Side(Vec<FaceOrigin>);
impl Side {
/// The side made of `origins` (in any order, repeats allowed); `None` when there are none.
pub fn new(origins: impl IntoIterator<Item = FaceOrigin>) -> Option<Side> {
let side = Side::canonical(origins.into_iter().collect());
(!side.0.is_empty()).then_some(side)
}
/// Its origins, sorted.
pub fn origins(&self) -> &[FaceOrigin] {
&self.0
}
/// The side's one origin, when it has exactly one.
pub fn single(&self) -> Option<FaceOrigin> {
match self.0.as_slice() {
[o] => Some(*o),
_ => None,
}
}
/// The same side with each origin mapped by `f` (failing where `f` does).
pub fn try_map<E>(&self, f: impl Fn(FaceOrigin) -> std::result::Result<FaceOrigin, E>) -> std::result::Result<Side, E> {
Ok(Side::canonical(self.0.iter().map(|o| f(*o)).collect::<std::result::Result<_, E>>()?))
}
/// The same side with each origin's operand renumbered by `f`.
pub fn map_operand(&self, f: impl Fn(usize) -> usize) -> Side {
Side::canonical(self.0.iter().map(|o| o.map_operand(&f)).collect())
}
/// `origins` sorted, without repeats.
fn canonical(mut origins: Vec<FaceOrigin>) -> Side {
origins.sort_unstable();
origins.dedup();
Side(origins)
}
}
impl From<FaceOrigin> for Side {
fn from(origin: FaceOrigin) -> Side {
Side(vec![origin])
}
}
/// Where a result edge came from.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
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): the
/// origins of the face on each side, `a <= b` (made with [`EdgeOrigin::meet`]).
Meet { a: Side, b: Side },
}
/// 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, serde::Serialize, serde::Deserialize)]
pub struct EdgeProv {
pub faces: Vec<usize>,
pub origins: Vec<EdgeOrigin>,
}
/// History of an operation's result: one entry per face and per edge, in the result's orders.
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct History {
/// How many operands the operation had.
pub operands: usize,
pub faces: Vec<FaceProv>,
pub edges: Vec<EdgeProv>,
}
impl FaceOrigin {
/// The same origin with its operand (if it has one) renumbered by `f`.
pub 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 {
/// Where the faces with origins `a` and `b` (sides, or single origins) meet, in canonical
/// order (the lesser side first).
pub fn meet(a: impl Into<Side>, b: impl Into<Side>) -> EdgeOrigin {
let (a, b) = (a.into(), b.into());
if a <= b { EdgeOrigin::Meet { a, b } } else { EdgeOrigin::Meet { a: b, b: a } }
}
/// The same origin with its operands renumbered by `f`.
pub fn map_operand(&self, f: impl Fn(usize) -> usize) -> EdgeOrigin {
match self {
EdgeOrigin::Edge { operand, edge } => EdgeOrigin::Edge { operand: f(*operand), edge: *edge },
EdgeOrigin::Meet { a, b } => EdgeOrigin::meet(a.map_operand(&f), b.map_operand(&f)),
}
}
}
impl History {
/// Check that this history describes a result with `result` (faces, edges) made from operands
/// with `counts`: one entry per face and edge, and every origin within its operand's faces
/// and edges.
pub fn check_counts(&self, result: (usize, usize), counts: &[(usize, usize)]) -> Result<()> {
if self.operands != counts.len() {
return Err(failed("operand count does not match"));
}
if self.faces.len() != result.0 || self.edges.len() != result.1 {
return Err(failed("entries do not match the result's faces and edges"));
}
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 } => a.origins().iter().chain(b.origins()).all(face_ok),
});
if !ok {
return Err(failed("an edge origin lies outside its operand"));
}
if e.origins.iter().any(|o| matches!(o, EdgeOrigin::Meet { a, b } if a > b)) {
return Err(failed("a meeting's sides are out of order"));
}
}
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; a meeting's side becomes the origins of the faces
/// it names (several where that operation merged faces). Fails, without changing `self`,
/// where a lineage cannot be expressed (an offset of a face that operation generated or
/// merged, a blend of an edge it generated, a meeting with a face it made from nothing).
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<FaceOrigin>, Option<Change>)> {
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 kept = f.change == Change::Kept;
for o in &f.origins {
let (os, change) = map_face(*o)?;
origins.extend(os);
kept &= change.is_none_or(|c| c == Change::Kept);
}
let origins: Vec<FaceOrigin> = 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 kept {
Change::Kept
} else {
Change::Modified
};
faces.push(FaceProv { origins, change });
}
// Split where an origin lies under several composed faces (pieces the inner operation
// split and this one joined back, or dropped all but one of, are not).
mark_shared_split(&mut faces);
// One side of a meeting: the origins of what its faces were made from.
let side = |s: &Side| -> Result<Side> {
let mut origins = Vec::new();
for o in s.origins() {
origins.extend(map_face(*o)?.0);
}
Side::new(origins).ok_or_else(|| failed("an edge meets a face the inner operation made from nothing"))
};
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(side(a)?, 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(())
}
/// Renumber the operands: operand `i` becomes `map[i]` of `operands` (several may become
/// one, which joins their origins), or is dropped (`None`), which fails, without changing
/// `self`, where an origin lies on it.
pub fn renumber(&mut self, map: &[Option<usize>], operands: usize) -> Result<()> {
if map.len() != self.operands || map.iter().flatten().any(|op| *op >= operands) {
return Err(failed("operand map does not match"));
}
let to = |op: usize| map.get(op).copied().flatten().ok_or_else(|| failed("an origin lies on a dropped operand"));
let face = |o: FaceOrigin| -> Result<FaceOrigin> {
Ok(match o {
FaceOrigin::Face { operand, face } => FaceOrigin::Face { operand: to(operand)?, face },
FaceOrigin::Edge { operand, edge } => FaceOrigin::Edge { operand: to(operand)?, edge },
FaceOrigin::Offset { operand, face } => FaceOrigin::Offset { operand: to(operand)?, face },
other => other,
})
};
let mut faces = Vec::with_capacity(self.faces.len());
for f in &self.faces {
let origins: BTreeSet<FaceOrigin> = f.origins.iter().map(|o| face(*o)).collect::<Result<_>>()?;
// Origins that became one are one face: no longer a merge.
let change = if f.change == Change::Merged && origins.len() == 1 { Change::Modified } else { f.change };
faces.push(FaceProv { origins: origins.into_iter().collect(), change });
}
let mut edges = Vec::with_capacity(self.edges.len());
for e in &self.edges {
let origins: BTreeSet<EdgeOrigin> = e
.origins
.iter()
.map(|o| match o {
EdgeOrigin::Edge { operand, edge } => Ok(EdgeOrigin::Edge { operand: to(*operand)?, edge: *edge }),
EdgeOrigin::Meet { a, b } => Ok(EdgeOrigin::meet(a.try_map(face)?, b.try_map(face)?)),
})
.collect::<Result<_>>()?;
edges.push(EdgeProv { faces: e.faces.clone(), origins: origins.into_iter().collect() });
}
self.operands = operands;
self.faces = faces;
self.edges = edges;
Ok(())
}
/// [`History::renumber`], consuming.
pub fn renumbered(mut self, map: &[Option<usize>], operands: usize) -> Result<History> {
self.renumber(map, operands)?;
Ok(self)
}
/// [`History::substitute`], consuming.
pub fn substituted(mut self, k: usize, inner: &History) -> Result<History> {
self.substitute(k, inner)?;
Ok(self)
}
/// This history, whose operand `i` was made by the operation `parts[i]` describes (all of
/// them over the same operands), as a history over those operands.
pub fn over(mut self, parts: &[&History]) -> Result<History> {
let n = parts.first().map_or(0, |p| p.operands);
if parts.len() != self.operands || parts.iter().any(|p| p.operands != n) {
return Err(failed("operand histories do not match"));
}
// Each part's operands come in at its place: then part i's operand j is i * n + j.
for (k, part) in parts.iter().enumerate().rev() {
self.substitute(k, part)?;
}
let map: Vec<Option<usize>> = (0..self.operands).map(|op| op.checked_rem(n)).collect();
self.renumber(&map, n)?;
Ok(self)
}
}
/// Faces of one origin that is under other faces too, and that still say Kept or Modified,
/// are Split (also where histories are put side by side: pieces of one body can share a
/// source).
pub fn mark_shared_split(faces: &mut [FaceProv]) {
let mut uses: HashMap<FaceOrigin, usize> = HashMap::new();
for o in faces.iter().flat_map(|f| &f.origins) {
*uses.entry(*o).or_default() += 1;
}
for f in faces.iter_mut() {
if matches!(f.change, Change::Kept | Change::Modified)
&& let [o] = f.origins.as_slice()
&& uses.get(o).copied().unwrap_or(0) > 1
{
f.change = Change::Split;
}
}
}
+1
View File
@@ -13,6 +13,7 @@ use solidcraft_geom::{Aabb3, Vec3};
use truck_modeling::{self as mt, builder};
use crate::body::{Body, Solid, from_p3, p3, v3};
use crate::history::BodyHistory;
use crate::history::{FaceOrigin, History};
use crate::record::{self, Rebuild};
use crate::{KernelError, Result, guard};
+1
View File
@@ -3,6 +3,7 @@ use solidcraft_geom::{Aabb3, Mesh, Vec3};
use truck_meshalgo::prelude::*;
use truck_modeling as mt;
use crate::history::BodyHistory;
use crate::{KernelError, Result, guard};
#[path = "planar_regularize.rs"]
@@ -7,6 +7,7 @@ use std::collections::BTreeSet;
use solidcraft_geom::{Mesh, Vec3};
use crate::body::Body;
use crate::history::BodyHistory;
use crate::history::{Change, EdgeOrigin, FaceOrigin, History, Side};
use crate::{BoolOp, boolean, box_solid};
+2 -1
View File
@@ -12,11 +12,12 @@ use truck_modeling as mt;
use crate::Result;
use crate::body::{Body, Solid, edge_order};
use crate::history::BodyHistory;
use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History};
use crate::record;
/// A composition step's history, or none (logged) where the lineage can't be expressed.
pub(crate) fn composed(h: Result<History>) -> Option<History> {
pub(crate) fn composed<E: std::fmt::Display>(h: std::result::Result<History, E>) -> Option<History> {
h.map_err(|e| log::debug!("no history: {e}")).ok()
}
+24 -342
View File
@@ -15,161 +15,36 @@
//! history here gives its faces attachments with no attachment code of its own. An operation
//! that can't should leave its result without one (`Body::without_history`), never guess.
use std::collections::BTreeSet;
pub use solidcraft_geom::brep::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History, Side};
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
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, serde::Serialize, serde::Deserialize)]
pub struct FaceProv {
pub origins: Vec<FaceOrigin>,
pub change: Change,
}
/// One side of a meeting: the origins of the face on that side of the edge. One origin, or
/// several where that face was made from several (a merged face) and the edge lies along it
/// as a whole. Never empty; sorted, without repeats, so equal sides compare equal.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
pub struct Side(Vec<FaceOrigin>);
impl Side {
/// The side made of `origins` (in any order, repeats allowed); `None` when there are none.
pub fn new(origins: impl IntoIterator<Item = FaceOrigin>) -> Option<Side> {
let side = Side::canonical(origins.into_iter().collect());
(!side.0.is_empty()).then_some(side)
}
/// Its origins, sorted.
pub fn origins(&self) -> &[FaceOrigin] {
&self.0
}
/// The side's one origin, when it has exactly one.
pub fn single(&self) -> Option<FaceOrigin> {
match self.0.as_slice() {
[o] => Some(*o),
_ => None,
}
}
/// The same side with each origin mapped by `f` (failing where `f` does).
fn try_map(&self, f: impl Fn(FaceOrigin) -> Result<FaceOrigin>) -> Result<Side> {
Ok(Side::canonical(self.0.iter().map(|o| f(*o)).collect::<Result<_>>()?))
}
/// The same side with each origin's operand renumbered by `f`.
fn map_operand(&self, f: impl Fn(usize) -> usize) -> Side {
Side::canonical(self.0.iter().map(|o| o.map_operand(&f)).collect())
}
/// `origins` sorted, without repeats.
fn canonical(mut origins: Vec<FaceOrigin>) -> Side {
origins.sort_unstable();
origins.dedup();
Side(origins)
impl From<solidcraft_geom::brep::history::Failed> for KernelError {
fn from(e: solidcraft_geom::brep::history::Failed) -> KernelError {
KernelError::Failed(e.to_string())
}
}
impl From<FaceOrigin> for Side {
fn from(origin: FaceOrigin) -> Side {
Side(vec![origin])
}
}
/// Where a result edge came from.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
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): the
/// origins of the face on each side, `a <= b` (made with [`EdgeOrigin::meet`]).
Meet { a: Side, b: Side },
}
/// 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, serde::Serialize, serde::Deserialize)]
pub struct EdgeProv {
pub faces: Vec<usize>,
pub origins: Vec<EdgeOrigin>,
}
/// History of an operation's result: one entry per face and per edge, in the result's orders.
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct History {
/// How many operands the operation had.
pub operands: usize,
pub faces: Vec<FaceProv>,
pub edges: Vec<EdgeProv>,
}
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 {
/// Where the faces with origins `a` and `b` (sides, or single origins) meet, in canonical
/// order (the lesser side first).
pub fn meet(a: impl Into<Side>, b: impl Into<Side>) -> EdgeOrigin {
let (a, b) = (a.into(), b.into());
if a <= b { EdgeOrigin::Meet { a, b } } else { EdgeOrigin::Meet { a: b, b: a } }
}
/// 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: *edge },
EdgeOrigin::Meet { a, b } => EdgeOrigin::meet(a.map_operand(&f), b.map_operand(&f)),
}
}
}
impl History {
/// What a [`History`] needs a kernel body for: the history types belong to
/// `solidcraft_geom::brep::history`, so a backend can report them without this crate.
pub trait BodyHistory {
/// 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 {
fn identity(body: &Body) -> History;
/// 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.
fn check(&self, result: &Body, operands: &[&Body]) -> Result<()>;
/// 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.
fn face_sources(&self, operands: &[&Body]) -> Vec<Option<FaceSource>>;
}
impl BodyHistory for 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).
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)
@@ -182,208 +57,15 @@ impl History {
/// 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<()> {
fn check(&self, result: &Body, operands: &[&Body]) -> Result<()> {
let counts: Vec<(usize, usize)> = operands.iter().map(|b| (b.face_count(), b.edge_count())).collect();
self.check_counts((result.face_count(), result.edge_count()), &counts)
}
/// [`History::check`] against the result's and each operand's face and edge counts.
pub(crate) fn check_counts(&self, result: (usize, usize), counts: &[(usize, usize)]) -> Result<()> {
if self.operands != counts.len() {
return Err(failed("operand count does not match"));
}
if self.faces.len() != result.0 || self.edges.len() != result.1 {
return Err(failed("entries do not match the result's faces and edges"));
}
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 } => a.origins().iter().chain(b.origins()).all(face_ok),
});
if !ok {
return Err(failed("an edge origin lies outside its operand"));
}
if e.origins.iter().any(|o| matches!(o, EdgeOrigin::Meet { a, b } if a > b)) {
return Err(failed("a meeting's sides are out of order"));
}
}
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; a meeting's side becomes the origins of the faces
/// it names (several where that operation merged faces). Fails, without changing `self`,
/// where a lineage cannot be expressed (an offset of a face that operation generated or
/// merged, a blend of an edge it generated, a meeting with a face it made from nothing).
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<FaceOrigin>, Option<Change>)> {
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 kept = f.change == Change::Kept;
for o in &f.origins {
let (os, change) = map_face(*o)?;
origins.extend(os);
kept &= change.is_none_or(|c| c == Change::Kept);
}
let origins: Vec<FaceOrigin> = 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 kept {
Change::Kept
} else {
Change::Modified
};
faces.push(FaceProv { origins, change });
}
// Split where an origin lies under several composed faces (pieces the inner operation
// split and this one joined back, or dropped all but one of, are not).
crate::record::mark_shared_split(&mut faces);
// One side of a meeting: the origins of what its faces were made from.
let side = |s: &Side| -> Result<Side> {
let mut origins = Vec::new();
for o in s.origins() {
origins.extend(map_face(*o)?.0);
}
Side::new(origins).ok_or_else(|| failed("an edge meets a face the inner operation made from nothing"))
};
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(side(a)?, 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(())
}
/// Renumber the operands: operand `i` becomes `map[i]` of `operands` (several may become
/// one, which joins their origins), or is dropped (`None`), which fails, without changing
/// `self`, where an origin lies on it.
pub(crate) fn renumber(&mut self, map: &[Option<usize>], operands: usize) -> Result<()> {
if map.len() != self.operands || map.iter().flatten().any(|op| *op >= operands) {
return Err(failed("operand map does not match"));
}
let to = |op: usize| map.get(op).copied().flatten().ok_or_else(|| failed("an origin lies on a dropped operand"));
let face = |o: FaceOrigin| -> Result<FaceOrigin> {
Ok(match o {
FaceOrigin::Face { operand, face } => FaceOrigin::Face { operand: to(operand)?, face },
FaceOrigin::Edge { operand, edge } => FaceOrigin::Edge { operand: to(operand)?, edge },
FaceOrigin::Offset { operand, face } => FaceOrigin::Offset { operand: to(operand)?, face },
other => other,
})
};
let mut faces = Vec::with_capacity(self.faces.len());
for f in &self.faces {
let origins: BTreeSet<FaceOrigin> = f.origins.iter().map(|o| face(*o)).collect::<Result<_>>()?;
// Origins that became one are one face: no longer a merge.
let change = if f.change == Change::Merged && origins.len() == 1 { Change::Modified } else { f.change };
faces.push(FaceProv { origins: origins.into_iter().collect(), change });
}
let mut edges = Vec::with_capacity(self.edges.len());
for e in &self.edges {
let origins: BTreeSet<EdgeOrigin> = e
.origins
.iter()
.map(|o| match o {
EdgeOrigin::Edge { operand, edge } => Ok(EdgeOrigin::Edge { operand: to(*operand)?, edge: *edge }),
EdgeOrigin::Meet { a, b } => Ok(EdgeOrigin::meet(a.try_map(face)?, b.try_map(face)?)),
})
.collect::<Result<_>>()?;
edges.push(EdgeProv { faces: e.faces.clone(), origins: origins.into_iter().collect() });
}
self.operands = operands;
self.faces = faces;
self.edges = edges;
Ok(())
}
/// [`History::renumber`], consuming.
pub(crate) fn renumbered(mut self, map: &[Option<usize>], operands: usize) -> Result<History> {
self.renumber(map, operands)?;
Ok(self)
}
/// [`History::substitute`], consuming.
pub(crate) fn substituted(mut self, k: usize, inner: &History) -> Result<History> {
self.substitute(k, inner)?;
Ok(self)
}
/// This history, whose operand `i` was made by the operation `parts[i]` describes (all of
/// them over the same operands), as a history over those operands.
pub(crate) fn over(mut self, parts: &[&History]) -> Result<History> {
let n = parts.first().map_or(0, |p| p.operands);
if parts.len() != self.operands || parts.iter().any(|p| p.operands != n) {
return Err(failed("operand histories do not match"));
}
// Each part's operands come in at its place: then part i's operand j is i * n + j.
for (k, part) in parts.iter().enumerate().rev() {
self.substitute(k, part)?;
}
let map: Vec<Option<usize>> = (0..self.operands).map(|op| op.checked_rem(n)).collect();
self.renumber(&map, n)?;
Ok(self)
Ok(self.check_counts((result.face_count(), result.edge_count()), &counts)?)
}
/// 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<Option<FaceSource>> {
fn face_sources(&self, operands: &[&Body]) -> Vec<Option<FaceSource>> {
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,
+1
View File
@@ -1,6 +1,7 @@
use solidcraft_geom::Vec3;
use super::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History, Side};
use crate::history::BodyHistory;
use crate::{Body, box_solid, transform};
fn face(operand: usize, face: usize) -> FaceOrigin {
@@ -6,6 +6,7 @@ use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
use crate::body::Body;
use crate::boolean_record::tests::{self, Reach, changes, to_edge, to_face};
use crate::history::BodyHistory;
use crate::history::{Change, EdgeOrigin, FaceOrigin, History};
use crate::{BoolOp, boolean, box_solid, cylinder, extrude};
+1 -1
View File
@@ -244,7 +244,7 @@ pub(super) fn encode(body: &Body) -> Result<w::Metadata> {
.paint
.as_ref()
.map(|p| {
Ok(w::Paint {
Ok::<_, crate::KernelError>(w::Paint {
opacity: p.opacity,
faces: p
.faces
+1
View File
@@ -9,6 +9,7 @@ use std::collections::HashMap;
use crate::body::{Body, Solid, from_p3};
use crate::boolean_diagnostics::{BooleanFallback, BooleanStrategy, capture_boolean_diagnostics, merge, record, reject};
use crate::boolean_record::{self as recorded, Traced};
use crate::history::BodyHistory;
use crate::{KernelError, Result, guard, record};
#[cfg(test)]
+1 -17
View File
@@ -68,23 +68,7 @@ pub(crate) fn region(origins: BTreeSet<FaceOrigin>, whole: bool) -> FaceProv {
FaceProv { origins: origins.into_iter().collect(), change }
}
/// Faces of one origin that is under other faces too, and that still say Kept or Modified,
/// are Split (also where histories are put side by side: pieces of one body can share a
/// source).
pub(crate) fn mark_shared_split(faces: &mut [FaceProv]) {
let mut uses: HashMap<FaceOrigin, usize> = HashMap::new();
for o in faces.iter().flat_map(|f| &f.origins) {
*uses.entry(*o).or_default() += 1;
}
for f in faces.iter_mut() {
if matches!(f.change, Change::Kept | Change::Modified)
&& let [o] = f.origins.as_slice()
&& uses.get(o).copied().unwrap_or(0) > 1
{
f.change = Change::Split;
}
}
}
pub(crate) use solidcraft_geom::brep::history::mark_shared_split;
/// The index of each of `solid`'s edges by identity, in its edge order.
pub(crate) fn edge_index(solid: &crate::body::Solid) -> HashMap<mt::EdgeID, usize> {
+1
View File
@@ -7,6 +7,7 @@ use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use crate::body::Body;
use crate::build::PathSeg;
use crate::history::BodyHistory;
use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv, History, Side};
const P: fn(usize) -> FaceOrigin = |segment| FaceOrigin::Profile { segment };
+1
View File
@@ -13,6 +13,7 @@ use solidcraft_geom::{Plane, Vec3};
use crate::body::{Body, from_p3};
use crate::boolean_record as recorded;
use crate::history::BodyHistory;
use crate::history::History;
use crate::splitface::{SplitTool, split_any};
use crate::{BoolOp, FaceSurface, Result, face_surface};
+1
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::history::BodyHistory;
use std::f64::consts::{FRAC_PI_2, PI};
fn dome() -> Body {
+1
View File
@@ -15,6 +15,7 @@ use truck_modeling::{ParametricCurve, SearchNearestParameter};
use crate::body::{Body, from_p3, p3};
use crate::boolean_record as recorded;
use crate::history::BodyHistory;
use crate::history::History;
use crate::splitface::{SplitTool, split_any};
use crate::{BoolOp, FaceSurface, Result, face_surface};