From 57d738eac4ba3bdcea24f8938a1bc10c1cf8c4d9 Mon Sep 17 00:00:00 2001
From: Giovanni <1319291+gvocale@users.noreply.github.com>
Date: Tue, 13 Oct 2026 20:48:27 -0400
Subject: [PATCH] Join walls swept along splines in 2D (#908)
Walls swept along a spline have flat B-spline tops and bottoms, so booleans between them went to the general intersection, which fails on coincident spline faces. Such faces are now put on true planes, and two prisms of the same height and direction are decided in 2D with their spline outlines fitted by arcs within a micron. The result is kept only when its volume is plausible; otherwise the general boolean takes over. The free-form flag is cleared by a drop guard so an unwinding boolean can't leave it set.
Fixes #844.
---
crates/engine/src/tests.rs | 1 +
crates/engine/src/tests/spiral_join.rs | 44 ++++++
crates/kernel/src/lib.rs | 3 +
crates/kernel/src/ops.rs | 19 +++
crates/kernel/src/planar_faces.rs | 106 +++++++++++++++
crates/kernel/src/planar_faces_tests.rs | 173 ++++++++++++++++++++++++
crates/kernel/src/prism.rs | 28 +++-
crates/kernel/src/prism_curve.rs | 40 ++++++
8 files changed, 413 insertions(+), 1 deletion(-)
create mode 100644 crates/engine/src/tests/spiral_join.rs
create mode 100644 crates/kernel/src/planar_faces.rs
create mode 100644 crates/kernel/src/planar_faces_tests.rs
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
index f3b0fcee..c49a473f 100644
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -259,6 +259,7 @@ mod path_scope;
mod primitive_points;
mod robustness;
mod robustness_goldens;
+mod spiral_join;
mod xz_plane;
/// Plan area a round of radius `r` fills at the concave corner `x` where circles (c1, r1) and
diff --git a/crates/engine/src/tests/spiral_join.rs b/crates/engine/src/tests/spiral_join.rs
new file mode 100644
index 00000000..515a17fb
--- /dev/null
+++ b/crates/engine/src/tests/spiral_join.rs
@@ -0,0 +1,44 @@
+//! Two interleaved spiral walls, joined (the case a of #844: 22 mm pitch, 0.75 turn).
+
+use std::f64::consts::TAU;
+
+use super::*;
+
+/// The join used to be an error: the walls' flat tops and bottoms are B-spline faces, so nothing
+/// saw that they coincide. Now they are prisms, and the join is decided in 2D.
+#[test]
+fn two_interleaved_spiral_walls_join_into_one_body() {
+ let (pitch, turns, points) = (22.0, 0.75, 19usize);
+ let (thickness, height, start) = (12.0, 60.0, 60.0);
+ let (k, end) = (pitch / TAU, turns * TAU);
+ let arm = |sign: f64| -> Vec<[f64; 2]> {
+ (0..points)
+ .map(|i| {
+ let a = end * i as f64 / (points - 1) as f64;
+ let r = start + k * a;
+ [sign * r * a.cos(), sign * r * a.sin()]
+ })
+ .collect()
+ };
+ let mut s = Session::default();
+ for (tag, sign) in [("A", 1.0), ("B", -1.0)] {
+ let x0 = sign * start;
+ run(&mut s, "sketch.create", json!({"plane": "XY", "name": format!("Path{tag}")}));
+ run(&mut s, "sketch.spline.fit_point", json!({"points": arm(sign)}));
+ run(&mut s, "sketch.finish", json!({}));
+ run(&mut s, "sketch.create", json!({"plane": "XZ", "name": format!("Prof{tag}")}));
+ run(&mut s, "sketch.rectangle.two_point", json!({"p0": [x0 - thickness / 2.0, -height], "p1": [x0 + thickness / 2.0, 0]}));
+ run(&mut s, "sketch.finish", json!({}));
+ let sweep = json!({
+ "sketch": format!("Prof{tag}"), "path_sketch": format!("Path{tag}"), "path": ["s1"],
+ "operation": "new", "name": format!("Sweep{tag}"), "body_name": format!("Band{tag}"),
+ });
+ run(&mut s, "solid.sweep", sweep);
+ }
+ run(&mut s, "solid.combine", json!({"target": "BandA", "tools": ["BandB"], "operation": "join", "name": "Joined"}));
+ assert_eq!(s.model.state().bodies.len(), 1);
+ // The two walls' plan areas united, by a plan-view raster (0.6 mm cells, flat ends), times the height.
+ let want = 450_878.0;
+ let got = volume(&mut s);
+ assert!(rel(got, want) < 5e-3, "{got} vs {want}");
+}
diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs
index 83d5bc0e..0df3d1aa 100644
--- a/crates/kernel/src/lib.rs
+++ b/crates/kernel/src/lib.rs
@@ -59,6 +59,9 @@ mod par;
pub use par::{for_each_on_threads, map_on_threads};
#[cfg(test)]
mod pinch_tests;
+mod planar_faces;
+#[cfg(test)]
+mod planar_faces_tests;
mod plane_fit;
mod polybool;
mod polyhedron;
diff --git a/crates/kernel/src/ops.rs b/crates/kernel/src/ops.rs
index ce69aa4e..eb6016c4 100644
--- a/crates/kernel/src/ops.rs
+++ b/crates/kernel/src/ops.rs
@@ -432,7 +432,26 @@ fn prism_result(a: &Body, b: &Body, op: BoolOp, size: f64, counts: &[(usize, usi
})
})
}
+/// Walls swept along splines have flat B-spline tops and bottoms. With those made planes the
+/// operands are prisms, and two of them (same height, one direction) are decided in 2D: fast, and
+/// with no intersection of spline surfaces, which is what fails on them. The outlines are fitted
+/// by arcs within a micron (`prism_boolean_free_form`). `None` when the operands have no such
+/// face, are not such prisms, or the answer is implausible: the general boolean then takes over.
+fn swept_prism(a: &Body, b: &Body, op: BoolOp) -> Option
{
+ let (pa, pb) = (crate::planar_faces::planarized(a), crate::planar_faces::planarized(b));
+ if pa.is_none() && pb.is_none() {
+ return None;
+ }
+ let (ra, rb) = (pa.as_ref().unwrap_or(a), pb.as_ref().unwrap_or(b));
+ let Some(Ok(Some(body))) = crate::prism::prism_boolean_free_form(ra, rb, op) else { return None };
+ let (va, vb) = (volume(a), volume(b));
+ plausible(volume(&body), op, va, vb, 2e-3 * (va + vb) + 1e-9).then_some(body)
+}
+
fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result