From 4f0aa7f7b64850a833809f65bc4c042c899a1a75 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Wed, 15 Jul 2026 20:10:50 -0400 Subject: [PATCH] fix(core): prevent unbounded slab corner miters (#500) * fix(core): bound slab corner miters * style(core): format slab miter fix --- packages/core/src/lib/slab-polygon.test.ts | 29 ++++++++++++++++++++++ packages/core/src/lib/slab-polygon.ts | 26 ++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/slab-polygon.test.ts b/packages/core/src/lib/slab-polygon.test.ts index d86006c6..45490c1e 100644 --- a/packages/core/src/lib/slab-polygon.test.ts +++ b/packages/core/src/lib/slab-polygon.test.ts @@ -753,6 +753,35 @@ describe('getRenderableSlabPolygon', () => { ]) expect(poly).toHaveLength(6) }) + + test('nearly collinear corners do not create unbounded wall-offset miters', () => { + const stored = [ + [3.1318685080276216, -4.3412497001265], + [11.261660174864861, -4.341249992437693], + [11.26165987489183, -1.3166851233565569], + [3.131868208054532, -1.3166857310453555], + [1.7318390128066048, -1.3166857522417288], + [1.7318388127796835, -4.341249921322843], + ] as Array<[number, number]> + const poly = getRenderableSlabPolygon(slabOf(stored, false), { + walls: [ + wallOf([1.7318387751077626, -4.323741654294752], [11.300000049514956, -4.341249931281993]), + ], + siblingSlabs: [], + }) + + const storedExtent = Math.max(...stored.flat().map(Math.abs)) + const renderedExtent = Math.max(...poly.flat().map(Math.abs)) + const longestEdge = Math.max( + ...poly.map((point, index) => { + const next = poly[(index + 1) % poly.length]! + return Math.hypot(point[0] - next[0], point[1] - next[1]) + }), + ) + + expect(renderedExtent - storedExtent).toBeLessThan(0.2) + expect(longestEdge).toBeLessThan(8.4) + }) }) describe('snapSlabEdgeToWallBand', () => { diff --git a/packages/core/src/lib/slab-polygon.ts b/packages/core/src/lib/slab-polygon.ts index 23a196e2..5b121adb 100644 --- a/packages/core/src/lib/slab-polygon.ts +++ b/packages/core/src/lib/slab-polygon.ts @@ -74,6 +74,8 @@ const WALL_LATERAL_TIE_EPSILON = 0.02 const CURVED_WALL_SAMPLE_SEGMENTS = 32 const SLAB_SEAM_ELEVATION_EPSILON = 1e-4 const DEFAULT_SLAB_ELEVATION = 0.05 +/** Prevent near-parallel offset lines from producing unbounded corner spikes. */ +const MAX_CORNER_MITER_RATIO = 10 export type SlabPolygonContext = { /** Walls on the slab's level. */ @@ -748,7 +750,9 @@ function offsetPolygonPerEdge( const last = spans[spans.length - 1]! const first = subSpans[j]![0]! const [ax, az] = pointAt(i, last.start, last.offset) - const [bx, bz] = pointAt(j, first.start, first.offset) + const endI = pointAt(i, last.end, last.offset) + const startJ = pointAt(j, first.start, first.offset) + const [bx, bz] = startJ const frameI = frames[i]! const frameJ = frames[j]! const denom = frameI.dx * frameJ.dz - frameI.dz * frameJ.dx @@ -756,11 +760,25 @@ function offsetPolygonPerEdge( // Parallel edges have no unique intersection. Emit both offset // endpoints — collinear edges with different offsets need the step // between them. - push(pointAt(i, last.end, last.offset)) - push([bx, bz]) + push(endI) + push(startJ) } else { const t = ((bx - ax) * frameJ.dz - (bz - az) * frameJ.dx) / denom - push([ax + t * frameI.dx, az + t * frameI.dz]) + const intersection: [number, number] = [ax + t * frameI.dx, az + t * frameI.dz] + const miterReach = Math.max( + Math.hypot(intersection[0] - endI[0], intersection[1] - endI[1]), + Math.hypot(intersection[0] - startJ[0], intersection[1] - startJ[1]), + ) + const offsetScale = Math.max(Math.abs(last.offset), Math.abs(first.offset), 1e-9) + if ( + !(Number.isFinite(intersection[0]) && Number.isFinite(intersection[1])) || + miterReach > offsetScale * MAX_CORNER_MITER_RATIO + ) { + push(endI) + push(startJ) + } else { + push(intersection) + } } }