fix: cuts no longer split walls they don't touch (#670)

A cut built slab by slab (the exact prism boolean) sewed the result from horizontal
slabs and only joins merged the coplanar pieces back. So a floor vent cut through the
Raspberry Pi case split the long wall into strips at the ports' tops and bottoms
(87 faces instead of 79 at 32 mm), and at 24 mm the end opening split the end wall.

- Prism booleans merge coplanar neighbours for every operation (faces only: edges,
  curves and surfaces stay exact), keeping each face's construction source where all
  its pieces share it, Split Face lines and regularization protection. Painted or
  protected operands keep their authored pieces as before.
- The prism history now tells apart a wall's several edges with its caps (a wall
  with openings meets each opening's floor), so cuts beside existing openings keep
  exact history and faces keep their names.
- Tests: the Pi case's faces pinned after every feature at 24, at 24 changed to 32,
  and built at 32 (each outer side one face); random openings and bosses on a
  shelled case never split another side; every corpus design rebuilds a parameter
  edit like a fresh build and reverts exactly.
- Action matrix baseline: demo/pi-case re-blessed (fewer, merged faces; no
  regressions), demo/pi-case-0.8 recorded, and 88 new passes on
  regressions/lid_ribs_flush_with_window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 10:14:52 -04:00
1 parent 9ba72e118b
commit 1e4387f3ea
8 files changed
+1277 -376

No files matched your search

+42 -6
View File
@@ -235,9 +235,10 @@ impl<'a> Context<'a> {
/// The origins of the edge between the wall over a segment tagged `t` and a cap lying on the
/// operand faces `cap`: the operand's edge between the wall and its own cap there (the very
/// edge the segment was read off, or the one it runs along where its slab ends, when that is
/// one), else where the wall meets the cap (all of the faces it lies on).
fn cap_edge(&self, t: Tag, cap: &BTreeSet<FaceOrigin>) -> Option<Vec<EdgeOrigin>> {
/// edge the segment was read off, or the one it runs along where its slab ends: the one
/// running through `at`, a point of the edge, where the wall meets its caps along several),
/// else where the wall meets the cap (all of the faces it lies on).
fn cap_edge(&self, t: Tag, cap: &BTreeSet<FaceOrigin>, at: Vec3) -> Option<Vec<EdgeOrigin>> {
let (x, w) = (t.operand, t.wall?);
let own: Vec<usize> =
cap.iter().filter_map(|o| if let Face { operand, face } = *o { (operand == x).then_some(face) } else { None }).collect();
@@ -250,6 +251,40 @@ impl<'a> Context<'a> {
match along.as_slice() {
[e] => Some(vec![EdgeOrigin::Edge { operand: x, edge: *e }]),
[] => Some(vec![EdgeOrigin::meet(Self::wall(t)?, Side::new(cap.iter().copied())?)]),
// One wall with openings meets its caps along several edges (a side wall with ports
// cut through it meets the ports' floors at each port): the one this edge lies on,
// else (a new opening beside them) where the wall meets the cap.
many => match self.edge_through(x, many, at)? {
Some(edge) => Some(vec![EdgeOrigin::Edge { operand: x, edge }]),
None => Some(vec![EdgeOrigin::meet(Self::wall(t)?, Side::new(cap.iter().copied())?)]),
},
}
}
/// Of the operand's edges `edges`, the one the point `at` lies on (`Some(None)` when none
/// does; `None` when several do).
fn edge_through(&self, operand: usize, edges: &[usize], at: Vec3) -> Option<Option<usize>> {
if edges.len() > 4096 {
return None;
}
let body = self.operands.get(operand)?;
let order = crate::body::edge_order(body.solid.face_iter()).0;
let mut on = edges.iter().filter(|e| {
order.get(**e).is_some_and(|edge| {
use mt::{BoundedCurve, ParametricCurve};
let c = edge.curve();
let (t0, t1) = c.range_tuple();
// The edge as a fine polyline: a line exactly, an arc within its chords' sag.
let n = if matches!(c, mt::Curve::Line(_)) { 1 } else { 64 };
let pts: Vec<Vec3> = (0..=n).map(|k| from_p3(c.subs(t0 + (t1 - t0) * k as f64 / n as f64))).collect();
let length: f64 = pts.windows(2).map(|w| w[0].dist(w[1])).sum();
let near = self.tol * 10.0 + if n == 1 { 0.0 } else { length * 2e-3 };
pts.windows(2).any(|w| at.dist_to_segment(w[0], w[1]) <= near)
})
});
match (on.next(), on.next()) {
(Some(e), None) => Some(Some(*e)),
(None, _) => Some(None),
_ => None,
}
}
@@ -412,7 +447,7 @@ impl<'a> Context<'a> {
let o = match (kinds.get(*f)?, kinds.get(*g)?) {
(Kind::Wall(segment), Kind::Cap(end)) | (Kind::Cap(end), Kind::Wall(segment)) => {
let layer = if *end == 0 { 0 } else { layers.len() - 1 };
self.cap_edge(tag(layer, *segment)?, tops.get(*end)?)?
self.cap_edge(tag(layer, *segment)?, tops.get(*end)?, crate::heal::edge_points(e).get(2).copied()?)?
}
(Kind::Wall(..), Kind::Wall(..)) => {
let (s, next, split) = *[e.absolute_front().id(), e.absolute_back().id()].iter().find_map(|v| junctions.get(v))?;
@@ -468,9 +503,10 @@ impl<'a> Context<'a> {
let (front, back) = (from_p3(e.absolute_front().point()), from_p3(e.absolute_back().point()));
let (h1, h2) = (self.plane.height(front), self.plane.height(back));
let (lo, hi) = (h1.min(h2), h1.max(h2));
let middle = || crate::heal::edge_points(e).get(2).copied();
let o = match (x, y) {
(Spec::Wall { .. }, Spec::Cap { .. }) => self.cap_edge(x.end(lo, self.tol)?.tag, cap_of.get(j)?.as_ref()?)?,
(Spec::Cap { .. }, Spec::Wall { .. }) => self.cap_edge(y.end(lo, self.tol)?.tag, cap_of.get(i)?.as_ref()?)?,
(Spec::Wall { .. }, Spec::Cap { .. }) => self.cap_edge(x.end(lo, self.tol)?.tag, cap_of.get(j)?.as_ref()?, middle()?)?,
(Spec::Cap { .. }, Spec::Wall { .. }) => self.cap_edge(y.end(lo, self.tol)?.tag, cap_of.get(i)?.as_ref()?, middle()?)?,
(Spec::Wall { .. }, Spec::Wall { .. }) if hi - lo <= self.tol => {
// One over the other at a level: the lower first.
let (below, above) = match (x.top(lo, self.tol), y.bottom(lo, self.tol)) {