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:
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user