From 1cc65cbc912cd4633aa3ba29856bee8cf078dad3 Mon Sep 17 00:00:00 2001
From: Que <57122349+queball1999@users.noreply.github.com>
Date: Wed, 14 Oct 2026 15:49:45 -0700
Subject: [PATCH] fix: a lip swept flush around a rounded rim joins the body it
sits on (#1164)
fix: a lip swept flush around a rounded rim joins the body it sits on.
---
crates/engine/src/boolean_regression_tests.rs | 17 +
crates/kernel/src/build.rs | 10 +
crates/kernel/src/coplanar.rs | 2 +-
crates/kernel/src/heal.rs | 2 +-
crates/kernel/src/lib.rs | 3 +
crates/kernel/src/ops.rs | 5 +
crates/kernel/src/outline.rs | 563 ++++++++++++++++++
crates/kernel/src/outline_tests.rs | 56 ++
crates/kernel/src/splitface.rs | 13 +-
.../lip_swept_flush_round_corner.json | 33 +
10 files changed, 699 insertions(+), 5 deletions(-)
create mode 100644 crates/kernel/src/outline.rs
create mode 100644 crates/kernel/src/outline_tests.rs
create mode 100644 examples/regressions/lip_swept_flush_round_corner.json
diff --git a/crates/engine/src/boolean_regression_tests.rs b/crates/engine/src/boolean_regression_tests.rs
index 02014d11..d1d5b954 100644
--- a/crates/engine/src/boolean_regression_tests.rs
+++ b/crates/engine/src/boolean_regression_tests.rs
@@ -250,3 +250,20 @@ fn press_pull_on_the_enclosure_lid_is_prompt() {
}
assert!(total(&s) > before, "{before} -> {}", total(&s));
}
+
+/// A lip swept round a block's rim, flush with its sides and round its corners, joins it as one
+/// solid of both volumes: the block (50 × 40 with R4.5 corners, 10 high) and the lip (1 × 1 along
+/// 41 + 31 + 41 mm of straight rim and two quarter turns between radii 3.5 and 4.5).
+#[test]
+fn lip_swept_flush_round_a_corner_joins() {
+ let s = replay(include_str!("../../../examples/regressions/lip_swept_flush_round_corner.json"));
+ let bodies = &s.model.state().bodies;
+ assert_eq!(bodies.len(), 1);
+ let b = &bodies[0];
+ assert!(b.body.validity().is_empty(), "{:?}", b.body.validity());
+ assert_eq!(b.body.shell_count(), 1);
+ let block = (50.0 * 40.0 - (4.0 - std::f64::consts::PI) * 4.5 * 4.5) * 10.0;
+ let lip = 41.0 + 31.0 + 41.0 + 2.0 * std::f64::consts::PI * (4.5 * 4.5 - 3.5 * 3.5) / 4.0;
+ let volume = b.body.tessellate(1e-5).unwrap().measure().volume;
+ assert!((volume - block - lip).abs() < 0.02, "{volume} expected {}", block + lip);
+}
diff --git a/crates/kernel/src/build.rs b/crates/kernel/src/build.rs
index a1c02076..649e192c 100644
--- a/crates/kernel/src/build.rs
+++ b/crates/kernel/src/build.rs
@@ -1836,6 +1836,16 @@ pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result
}
ends.push(cur);
}
+ // A straight profile edge square to an arc's axis turns in its own plane: that face is
+ // given a plane, so that it is found lying on a planar face of another body.
+ let mut bounds = solidcraft_geom::Aabb3::EMPTY;
+ shell.vertex_iter().for_each(|v| bounds.add(from_p3(v.point())));
+ let size = bounds.size().len();
+ if size.is_finite()
+ && let Some(flat) = crate::heal::planar_surfaces(&shell, (size * 1e-7).max(1e-9))
+ {
+ shell = flat;
+ }
// truck sweeps the faces: each profile edge's run along the whole path, path segment
// after segment.
let sides: Vec = match record::columns(shell.face_iter(), &seeds(&profile), None, record::Column::ToEnd, &[]) {
diff --git a/crates/kernel/src/coplanar.rs b/crates/kernel/src/coplanar.rs
index 364bc955..b834963d 100644
--- a/crates/kernel/src/coplanar.rs
+++ b/crates/kernel/src/coplanar.rs
@@ -529,7 +529,7 @@ fn backed(a: &Body, b: &Body, m: Vec3, d: f64, depth: f64) -> bool {
}
/// The faces of `body` lying in the plane (n, d), facing along n.
-fn faces_in(body: &Body, n: Vec3, d: f64, tol: f64) -> Vec {
+pub(crate) fn faces_in(body: &Body, n: Vec3, d: f64, tol: f64) -> Vec {
planes(body).into_iter().filter(|(_, p)| p.n.dot(n) > 1.0 - 1e-9 && (p.d - d).abs() < tol).map(|(i, _)| i).collect()
}
diff --git a/crates/kernel/src/heal.rs b/crates/kernel/src/heal.rs
index ee1b4259..5e51cd35 100644
--- a/crates/kernel/src/heal.rs
+++ b/crates/kernel/src/heal.rs
@@ -417,7 +417,7 @@ fn exact_edges(shell: &mt::Shell, tol: f64, trace: &mut Trace) -> Option Option {
+pub(crate) fn planar_surfaces(shell: &mt::Shell, tol: f64) -> Option {
use mt::{BoundedCurve, ParametricCurve, ParametricSurface, ParametricSurface3D, SearchNearestParameter};
let mut changed = false;
let mut faces = Vec::new();
diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs
index e600d2b5..2a21c3fe 100644
--- a/crates/kernel/src/lib.rs
+++ b/crates/kernel/src/lib.rs
@@ -65,6 +65,9 @@ mod native;
mod numbered;
mod offset;
mod ops;
+mod outline;
+#[cfg(test)]
+mod outline_tests;
mod par;
pub use par::{for_each_on_threads, map_on_threads};
#[cfg(test)]
diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs
index 4ea3dfc4..f0b1c14f 100644
--- a/crates/kernel/src/ops.rs
+++ b/crates/kernel/src/ops.rs
@@ -662,6 +662,11 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result