mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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
@@ -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,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};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::history::BodyHistory;
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
|
||||
fn dome() -> Body {
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user