feat(kernel): an edge's meeting sides are sets of origins (merged faces)

EdgeOrigin::Meet now names each side as a Side: the sorted, de-duplicated
origins of the face there. substitute maps a side to the union of the inner
faces' origins, so a meeting with a face an inner operation merged keeps its
lineage; meeting() derives sides from whole faces. Fillets after box cuts
that leave coplanar pieces now report exact history. Prism lineage meets a
cap of several origins and resolves wall junctions away from shared
vertices by the operand's one edge between the walls. bore_union's recut
reports history only for a one-face bore (pieces of a two-arc bore's recut
wall were given both arcs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
Iraklis KourtisandClaude Opus 5.5 authored and bherbruck committed 2026-10-12 11:52:53 -04:00
1 parent 08d7037176
commit 43ddde4a5d
11 files changed
+347 -76

No files matched your search

+88 -22
View File
@@ -48,13 +48,64 @@ pub struct FaceProv {
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)]
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<FaceOrigin> for Side {
fn from(origin: FaceOrigin) -> Side {
Side(vec![origin])
}
}
/// Where a result edge came from.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, 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 },
/// 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
@@ -91,11 +142,18 @@ impl FaceOrigin {
}
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 {
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) },
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)),
}
}
}
@@ -143,20 +201,24 @@ impl History {
}
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),
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. 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).
/// 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"));
@@ -209,26 +271,30 @@ impl History {
};
faces.push(FaceProv { origins, change });
}
// One side of a meeting: a single face of the composed operation.
let side = |o: FaceOrigin| -> Result<FaceOrigin> {
match map_face(o)?.0.as_slice() {
[x] => Ok(*x),
_ => Err(failed("an edge meets a face the inner operation merged")),
// 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)))
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)? });
origins.insert(EdgeOrigin::meet(side(a)?, side(b)?));
}
}
}
@@ -268,9 +334,9 @@ impl History {
let origins: BTreeSet<EdgeOrigin> = e
.origins
.iter()
.map(|o| match *o {
EdgeOrigin::Edge { operand, edge } => Ok(EdgeOrigin::Edge { operand: to(operand)?, edge }),
EdgeOrigin::Meet { a, b } => Ok(EdgeOrigin::Meet { a: face(a)?, b: face(b)? }),
.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() });