fix(kernel): revolving adjacent profiles on the axis makes no zero-area faces (#490)

Profiles revolved together merge into one region that keeps a vertex where
each shared edge met the axis. With the axis edge in several pieces the loop
skipped the builders for a profile with an edge on the axis, and the generic
sweep turned each piece into a degenerate face that booleans failed on.
Runs of collinear axis lines are merged into one first.

Fixes #487

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
MqriusandClaude Opus 5.5 authored and GitHub committed 2026-10-12 22:38:48 -04:00
1 parent 219816d282
commit 91de39d3fe
3 files changed
+64 -2

No files matched your search

+26
View File
@@ -125,6 +125,32 @@ fn revolve_washer_and_sphere() {
assert!(rel(ms.volume, 4.0 / 3.0 * PI * 1000.0) < 1e-3, "{}", ms.volume);
}
#[test]
fn revolve_adjacent_profiles_on_the_axis() {
// Two rectangles on the axis, revolved together: their merged outline keeps a vertex
// where the shared edge met the axis. The swept axis pieces used to be zero-area faces,
// and cutting the body's flat end out of another failed.
let r = Region2 {
outer: Loop2::polygon(&[
Vec2::new(0.0, 0.0),
Vec2::new(30.0, 0.0),
Vec2::new(60.0, 0.0),
Vec2::new(60.0, 20.0),
Vec2::new(30.0, 20.0),
Vec2::new(0.0, 20.0),
]),
holes: vec![],
};
let b = revolve(&Plane::XZ, &[r], Vec2::ZERO, Vec2::X, 2.0 * PI).unwrap().pop().unwrap();
assert!(rel(measure(&b).unwrap().volume, PI * 400.0 * 60.0) < 5e-4);
let faces = b.faces(1e-3).unwrap();
assert!(faces.iter().all(|f| f.area > 1e-6), "{:?}", faces.iter().map(|f| f.area).collect::<Vec<_>>());
let disc = cylinder(Vec3::new(-1.0, 0.0, 0.0), Vec3::X, 40.0, 3.0).unwrap();
let cut = boolean(&disc, &b, BoolOp::Cut).unwrap().unwrap();
let want = PI * 1600.0 * 3.0 - PI * 400.0 * 2.0;
assert!(rel(measure(&cut).unwrap().volume, want) < 1e-3, "{} vs {want}", measure(&cut).unwrap().volume);
}
#[test]
fn chamfer_and_step() {
let b = box_solid(Vec3::ZERO, Vec3::new(20.0, 20.0, 20.0)).unwrap();