Simplify auto slab polygons for curved wall rooms
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
export function insetPolygonFromCentroid(
|
||||
polygon: Array<[number, number]>,
|
||||
inset: number,
|
||||
): Array<[number, number]> {
|
||||
if (inset <= 0) {
|
||||
return polygon.map(([x, z]) => [x, z] as [number, number])
|
||||
}
|
||||
|
||||
const centroid = polygon.reduce(
|
||||
(acc, [x, z]) => ({ x: acc.x + x, z: acc.z + z }),
|
||||
{ x: 0, z: 0 },
|
||||
)
|
||||
centroid.x /= Math.max(polygon.length, 1)
|
||||
centroid.z /= Math.max(polygon.length, 1)
|
||||
|
||||
return polygon.map(([x, z]) => {
|
||||
const dx = x - centroid.x
|
||||
const dz = z - centroid.z
|
||||
const length = Math.hypot(dx, dz)
|
||||
if (length <= inset + 1e-6) {
|
||||
return [x, z] as [number, number]
|
||||
}
|
||||
|
||||
const scale = (length - inset) / length
|
||||
return [centroid.x + dx * scale, centroid.z + dz * scale] as [number, number]
|
||||
})
|
||||
}
|
||||
|
||||
function pointLineDistance(
|
||||
point: [number, number],
|
||||
start: [number, number],
|
||||
end: [number, number],
|
||||
) {
|
||||
const dx = end[0] - start[0]
|
||||
const dz = end[1] - start[1]
|
||||
const lengthSquared = dx * dx + dz * dz
|
||||
|
||||
if (lengthSquared < 1e-9) {
|
||||
return Math.hypot(point[0] - start[0], point[1] - start[1])
|
||||
}
|
||||
|
||||
const cross = (point[0] - start[0]) * dz - (point[1] - start[1]) * dx
|
||||
return Math.abs(cross) / Math.sqrt(lengthSquared)
|
||||
}
|
||||
|
||||
function dedupePolygonPoints(
|
||||
polygon: Array<[number, number]>,
|
||||
tolerance = 1e-6,
|
||||
): Array<[number, number]> {
|
||||
const deduped: Array<[number, number]> = []
|
||||
|
||||
for (const point of polygon) {
|
||||
const previous = deduped[deduped.length - 1]
|
||||
if (previous && Math.hypot(point[0] - previous[0], point[1] - previous[1]) <= tolerance) {
|
||||
continue
|
||||
}
|
||||
deduped.push(point)
|
||||
}
|
||||
|
||||
if (
|
||||
deduped.length > 2 &&
|
||||
Math.hypot(
|
||||
deduped[0]![0] - deduped[deduped.length - 1]![0],
|
||||
deduped[0]![1] - deduped[deduped.length - 1]![1],
|
||||
) <= tolerance
|
||||
) {
|
||||
deduped.pop()
|
||||
}
|
||||
|
||||
return deduped
|
||||
}
|
||||
|
||||
function simplifyPolyline(points: Array<[number, number]>, tolerance: number): Array<[number, number]> {
|
||||
if (points.length <= 2) {
|
||||
return points.map(([x, z]) => [x, z] as [number, number])
|
||||
}
|
||||
|
||||
let maxDistance = -1
|
||||
let splitIndex = -1
|
||||
|
||||
for (let index = 1; index < points.length - 1; index += 1) {
|
||||
const distance = pointLineDistance(points[index]!, points[0]!, points[points.length - 1]!)
|
||||
if (distance > maxDistance) {
|
||||
maxDistance = distance
|
||||
splitIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDistance <= tolerance || splitIndex === -1) {
|
||||
return [points[0]!, points[points.length - 1]!]
|
||||
}
|
||||
|
||||
const left = simplifyPolyline(points.slice(0, splitIndex + 1), tolerance)
|
||||
const right = simplifyPolyline(points.slice(splitIndex), tolerance)
|
||||
return [...left.slice(0, -1), ...right]
|
||||
}
|
||||
|
||||
export function simplifyClosedPolygon(
|
||||
polygon: Array<[number, number]>,
|
||||
tolerance: number,
|
||||
): Array<[number, number]> {
|
||||
const cleanPolygon = dedupePolygonPoints(polygon)
|
||||
if (cleanPolygon.length <= 3 || tolerance <= 0) {
|
||||
return cleanPolygon
|
||||
}
|
||||
|
||||
let anchorA = 0
|
||||
let anchorB = Math.floor(cleanPolygon.length / 2)
|
||||
let maxDistanceSquared = -1
|
||||
|
||||
for (let i = 0; i < cleanPolygon.length; i += 1) {
|
||||
for (let j = i + 1; j < cleanPolygon.length; j += 1) {
|
||||
const dx = cleanPolygon[j]![0] - cleanPolygon[i]![0]
|
||||
const dz = cleanPolygon[j]![1] - cleanPolygon[i]![1]
|
||||
const distanceSquared = dx * dx + dz * dz
|
||||
if (distanceSquared > maxDistanceSquared) {
|
||||
maxDistanceSquared = distanceSquared
|
||||
anchorA = i
|
||||
anchorB = j
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const forward = cleanPolygon.slice(anchorA, anchorB + 1)
|
||||
const wrapped = [...cleanPolygon.slice(anchorB), ...cleanPolygon.slice(0, anchorA + 1)]
|
||||
const simplifiedForward = simplifyPolyline(forward, tolerance)
|
||||
const simplifiedWrapped = simplifyPolyline(wrapped, tolerance)
|
||||
const simplified = dedupePolygonPoints(
|
||||
[...simplifiedForward.slice(0, -1), ...simplifiedWrapped.slice(0, -1)],
|
||||
tolerance * 0.25,
|
||||
)
|
||||
|
||||
return simplified.length >= 3 ? simplified : cleanPolygon
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { getClampedWallCurveOffset, sampleWallCenterline } from '../systems/wall/wall-curve'
|
||||
import {
|
||||
getClampedWallCurveOffset,
|
||||
getWallCurveFrameAt,
|
||||
isCurvedWall,
|
||||
} from '../systems/wall/wall-curve'
|
||||
import { simplifyClosedPolygon } from './polygon-geometry'
|
||||
import { SlabNode, type SlabNode as SlabNodeType, type WallNode } from '../schema'
|
||||
|
||||
type Point2D = { x: number; y: number }
|
||||
@@ -26,6 +31,9 @@ type DetectedRoom = {
|
||||
}
|
||||
|
||||
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05
|
||||
const ROOM_CURVE_TOLERANCE = 0.04
|
||||
const MAX_CURVE_SUBDIVISION_DEPTH = 6
|
||||
const AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE = 0.08
|
||||
|
||||
function pointFromTuple(point: [number, number]): Point2D {
|
||||
return { x: point[0], y: point[1] }
|
||||
@@ -178,8 +186,49 @@ function getWallDirection(wall: Pick<WallNode, 'start' | 'end'>) {
|
||||
}
|
||||
}
|
||||
|
||||
function pointLineDistance(point: Point2D, start: Point2D, end: Point2D) {
|
||||
const dx = end.x - start.x
|
||||
const dy = end.y - start.y
|
||||
const lengthSquared = dx * dx + dy * dy
|
||||
|
||||
if (lengthSquared < 1e-9) {
|
||||
return Math.hypot(point.x - start.x, point.y - start.y)
|
||||
}
|
||||
|
||||
const cross = (point.x - start.x) * dy - (point.y - start.y) * dx
|
||||
return Math.abs(cross) / Math.sqrt(lengthSquared)
|
||||
}
|
||||
|
||||
function sampleWallPointsForRoomDetection(
|
||||
wall: Pick<WallNode, 'start' | 'end' | 'curveOffset'>,
|
||||
tolerance = ROOM_CURVE_TOLERANCE,
|
||||
) {
|
||||
const start = { x: wall.start[0], y: wall.start[1] }
|
||||
const end = { x: wall.end[0], y: wall.end[1] }
|
||||
|
||||
if (!isCurvedWall(wall)) {
|
||||
return [start, end]
|
||||
}
|
||||
|
||||
const subdivide = (t0: number, p0: Point2D, t1: number, p1: Point2D, depth: number): Point2D[] => {
|
||||
const midT = (t0 + t1) / 2
|
||||
const midPoint = getWallCurveFrameAt(wall, midT).point
|
||||
const deviation = pointLineDistance(midPoint, p0, p1)
|
||||
|
||||
if (depth >= MAX_CURVE_SUBDIVISION_DEPTH || deviation <= tolerance) {
|
||||
return [p0, p1]
|
||||
}
|
||||
|
||||
const left = subdivide(t0, p0, midT, midPoint, depth + 1)
|
||||
const right = subdivide(midT, midPoint, t1, p1, depth + 1)
|
||||
return [...left.slice(0, -1), ...right]
|
||||
}
|
||||
|
||||
return subdivide(0, start, 1, end, 0)
|
||||
}
|
||||
|
||||
function getDirectedWallBoundaryPoints(wall: WallNode, forward: boolean) {
|
||||
const points = sampleWallCenterline(wall).map((point) => ({ x: point.x, y: point.y }))
|
||||
const points = sampleWallPointsForRoomDetection(wall)
|
||||
return forward ? points : [...points].reverse()
|
||||
}
|
||||
|
||||
@@ -426,11 +475,20 @@ function syncAutoSlabsForLevel(
|
||||
|
||||
const detected: DetectedRoom[] = roomPolygons
|
||||
.map((poly) => ({
|
||||
poly,
|
||||
sig: polygonSignature(poly),
|
||||
centroid: polygonCentroid(poly),
|
||||
area: Math.abs(polygonArea(poly)),
|
||||
bbox: bboxOf(poly),
|
||||
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(
|
||||
pointFromTuple,
|
||||
),
|
||||
sig: '',
|
||||
centroid: { x: 0, y: 0 },
|
||||
area: 0,
|
||||
bbox: bboxOf([]),
|
||||
}))
|
||||
.map((room) => ({
|
||||
...room,
|
||||
sig: polygonSignature(room.poly),
|
||||
centroid: polygonCentroid(room.poly),
|
||||
area: Math.abs(polygonArea(room.poly)),
|
||||
bbox: bboxOf(room.poly),
|
||||
}))
|
||||
.filter(({ sig }) => !manualSignatures.has(sig))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
|
||||
import { insetPolygonFromCentroid, simplifyClosedPolygon } from '../../lib/polygon-geometry'
|
||||
import type { AnyNodeId, SlabNode } from '../../schema'
|
||||
import useScene from '../../store/use-scene'
|
||||
|
||||
@@ -60,38 +61,14 @@ function updateSlabGeometry(node: SlabNode, mesh: THREE.Mesh) {
|
||||
/** Half of default wall thickness — used to extend slab geometry under walls */
|
||||
const SLAB_OUTSET = 0.05
|
||||
const AUTO_SLAB_INSET = 0.02
|
||||
|
||||
function insetPolygonFromCentroid(
|
||||
polygon: Array<[number, number]>,
|
||||
inset: number,
|
||||
): Array<[number, number]> {
|
||||
if (inset <= 0) {
|
||||
return polygon.map(([x, z]) => [x, z] as [number, number])
|
||||
}
|
||||
|
||||
const centroid = polygon.reduce(
|
||||
(acc, [x, z]) => ({ x: acc.x + x, z: acc.z + z }),
|
||||
{ x: 0, z: 0 },
|
||||
)
|
||||
centroid.x /= Math.max(polygon.length, 1)
|
||||
centroid.z /= Math.max(polygon.length, 1)
|
||||
|
||||
return polygon.map(([x, z]) => {
|
||||
const dx = x - centroid.x
|
||||
const dz = z - centroid.z
|
||||
const length = Math.hypot(dx, dz)
|
||||
if (length <= inset + 1e-6) {
|
||||
return [x, z] as [number, number]
|
||||
}
|
||||
|
||||
const scale = (length - inset) / length
|
||||
return [centroid.x + dx * scale, centroid.z + dz * scale] as [number, number]
|
||||
})
|
||||
}
|
||||
const AUTO_SLAB_SIMPLIFY_TOLERANCE = 0.08
|
||||
|
||||
function getRenderableSlabPolygon(slabNode: SlabNode): Array<[number, number]> {
|
||||
return slabNode.autoFromWalls
|
||||
? insetPolygonFromCentroid(slabNode.polygon, AUTO_SLAB_INSET)
|
||||
? simplifyClosedPolygon(
|
||||
insetPolygonFromCentroid(slabNode.polygon, AUTO_SLAB_INSET),
|
||||
AUTO_SLAB_SIMPLIFY_TOLERANCE,
|
||||
)
|
||||
: outsetPolygon(slabNode.polygon, SLAB_OUTSET)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user