fix(core): prevent unbounded slab corner miters (#500)

* fix(core): bound slab corner miters

* style(core): format slab miter fix
This commit is contained in:
Wassim SAMAD
2026-07-15 20:10:50 -04:00
committed by GitHub
parent 78223d61cf
commit 4f0aa7f7b6
2 changed files with 51 additions and 4 deletions
@@ -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', () => {
+22 -4
View File
@@ -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)
}
}
}