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)
|
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', () => {
|
describe('snapSlabEdgeToWallBand', () => {
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ const WALL_LATERAL_TIE_EPSILON = 0.02
|
|||||||
const CURVED_WALL_SAMPLE_SEGMENTS = 32
|
const CURVED_WALL_SAMPLE_SEGMENTS = 32
|
||||||
const SLAB_SEAM_ELEVATION_EPSILON = 1e-4
|
const SLAB_SEAM_ELEVATION_EPSILON = 1e-4
|
||||||
const DEFAULT_SLAB_ELEVATION = 0.05
|
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 = {
|
export type SlabPolygonContext = {
|
||||||
/** Walls on the slab's level. */
|
/** Walls on the slab's level. */
|
||||||
@@ -748,7 +750,9 @@ function offsetPolygonPerEdge(
|
|||||||
const last = spans[spans.length - 1]!
|
const last = spans[spans.length - 1]!
|
||||||
const first = subSpans[j]![0]!
|
const first = subSpans[j]![0]!
|
||||||
const [ax, az] = pointAt(i, last.start, last.offset)
|
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 frameI = frames[i]!
|
||||||
const frameJ = frames[j]!
|
const frameJ = frames[j]!
|
||||||
const denom = frameI.dx * frameJ.dz - frameI.dz * frameJ.dx
|
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
|
// Parallel edges have no unique intersection. Emit both offset
|
||||||
// endpoints — collinear edges with different offsets need the step
|
// endpoints — collinear edges with different offsets need the step
|
||||||
// between them.
|
// between them.
|
||||||
push(pointAt(i, last.end, last.offset))
|
push(endI)
|
||||||
push([bx, bz])
|
push(startJ)
|
||||||
} else {
|
} else {
|
||||||
const t = ((bx - ax) * frameJ.dz - (bz - az) * frameJ.dx) / denom
|
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