diff --git a/main/opengeometry-three/tests/node/regions.test.mjs b/main/opengeometry-three/tests/node/regions.test.mjs index 9b713251..8f1d7619 100644 --- a/main/opengeometry-three/tests/node/regions.test.mjs +++ b/main/opengeometry-three/tests/node/regions.test.mjs @@ -126,6 +126,32 @@ test('a boundary that cannot be closed is InvalidTopology with the 2.0 text', () ); }); +test('two wall pieces whose mitre faces cross at a 0.003 degree bend unite to one region', () => { + const ring = (points) => points.map(([x, z]) => ({ x, y: 0, z })); + const first = { + outer: ring([ + [-7.915506291175921, -2.642064756497319], [-7.840493708824079, -2.575935243502681], + [-5.360466317231342, -5.389100659702814], [-5.435482107488363, -5.455226533879618], + [-7.915504659116948, -2.642066607788105], + ]), + holes: [], + }; + const second = { + outer: ring([ + [-8.973504659071258, -1.442066607747827], [-8.898495340928742, -1.375933392252173], + [-7.840495340928741, -2.575933392252173], [-7.915504659071259, -2.642066607747827], + [-7.915506291221608, -2.642064756537601], + ]), + holes: [], + }; + const united = OpenGeometry.booleanRegions2D([first], [second], 'union'); + const overlap = area(OpenGeometry.booleanRegions2D([first], [second], 'intersection')); + const apart = Math.abs(signedArea(first.outer)) + Math.abs(signedArea(second.outer)); + assert.equal(united.length, 1); + assert(overlap > 0 && overlap < 1e-7, `overlap ${overlap}`); + assert(Math.abs(area(united) + overlap - apart) < 1e-9); +}); + test('the curved boolean keeps an island in the hole of a frame apart in both operand orders', () => { const frame = curvedRegion(0, 0, 10, 10, [1, 1, 9, 9]); const island = curvedRegion(3, 3, 7, 7, [4, 4, 6, 6]); diff --git a/main/opengeometry/src/geom2d/poly2d.rs b/main/opengeometry/src/geom2d/poly2d.rs index ba2cefaa..065c1037 100644 --- a/main/opengeometry/src/geom2d/poly2d.rs +++ b/main/opengeometry/src/geom2d/poly2d.rs @@ -127,7 +127,7 @@ pub(super) fn segment_cross_t2( let r = Pt2::new(a2.x - a1.x, a2.z - a1.z); let s = Pt2::new(b2.x - b1.x, b2.z - b1.z); let denom = cross2(r, s); - if denom.abs() < eps { + if denom.abs() <= eps * r.x.hypot(r.z).min(s.x.hypot(s.z)) { return None; } let qp = Pt2::new(b1.x - a1.x, b1.z - a1.z); diff --git a/main/opengeometry/src/geom2d/tests/boolean2d.rs b/main/opengeometry/src/geom2d/tests/boolean2d.rs index 06109028..0bb34cd0 100644 --- a/main/opengeometry/src/geom2d/tests/boolean2d.rs +++ b/main/opengeometry/src/geom2d/tests/boolean2d.rs @@ -309,3 +309,28 @@ fn a_hole_that_no_region_contains_is_an_error() { Some(RegionBooleanError::OrphanHole) ); } + +#[test] +fn two_near_collinear_wall_pieces_unite_across_their_mitre() { + let first = vec![ + p(-7.915506291175921, -2.642064756497319), + p(-7.840493708824079, -2.575935243502681), + p(-5.360466317231342, -5.389100659702814), + p(-5.435482107488363, -5.455226533879618), + p(-7.915504659116948, -2.642066607788105), + ]; + let second = vec![ + p(-8.973504659071258, -1.442066607747827), + p(-8.898495340928742, -1.375933392252173), + p(-7.840495340928741, -2.575933392252173), + p(-7.915504659071259, -2.642066607747827), + p(-7.915506291221608, -2.642064756537601), + ]; + let (a, b) = (vec![first], vec![second]); + let [(united, union_area), (_, overlap), (_, remainder)] = areas_of_all_operations(&a, &b); + let (first_area, second_area) = (signed_area2(&a[0]).abs(), signed_area2(&b[0]).abs()); + assert_eq!(united, 1); + assert!(overlap > 0.0 && overlap < 1.0e-7, "overlap {overlap}"); + assert!((union_area + overlap - first_area - second_area).abs() < 1.0e-9); + assert!((remainder + overlap - first_area).abs() < 1.0e-9); +}