Simplify auto slab polygons for curved wall rooms

This commit is contained in:
sudhir
2026-04-16 12:39:52 +05:30
parent a7b97caeb0
commit ae68d69f77
3 changed files with 205 additions and 36 deletions
+134
View File
@@ -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
}
+65 -7
View File
@@ -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' import { SlabNode, type SlabNode as SlabNodeType, type WallNode } from '../schema'
type Point2D = { x: number; y: number } type Point2D = { x: number; y: number }
@@ -26,6 +31,9 @@ type DetectedRoom = {
} }
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05 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 { function pointFromTuple(point: [number, number]): Point2D {
return { x: point[0], y: point[1] } 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) { 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() return forward ? points : [...points].reverse()
} }
@@ -426,11 +475,20 @@ function syncAutoSlabsForLevel(
const detected: DetectedRoom[] = roomPolygons const detected: DetectedRoom[] = roomPolygons
.map((poly) => ({ .map((poly) => ({
poly, poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(
sig: polygonSignature(poly), pointFromTuple,
centroid: polygonCentroid(poly), ),
area: Math.abs(polygonArea(poly)), sig: '',
bbox: bboxOf(poly), 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)) .filter(({ sig }) => !manualSignatures.has(sig))
+6 -29
View File
@@ -1,6 +1,7 @@
import { useFrame } from '@react-three/fiber' import { useFrame } from '@react-three/fiber'
import * as THREE from 'three' import * as THREE from 'three'
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry' import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
import { insetPolygonFromCentroid, simplifyClosedPolygon } from '../../lib/polygon-geometry'
import type { AnyNodeId, SlabNode } from '../../schema' import type { AnyNodeId, SlabNode } from '../../schema'
import useScene from '../../store/use-scene' 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 */ /** Half of default wall thickness — used to extend slab geometry under walls */
const SLAB_OUTSET = 0.05 const SLAB_OUTSET = 0.05
const AUTO_SLAB_INSET = 0.02 const AUTO_SLAB_INSET = 0.02
const AUTO_SLAB_SIMPLIFY_TOLERANCE = 0.08
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 getRenderableSlabPolygon(slabNode: SlabNode): Array<[number, number]> { function getRenderableSlabPolygon(slabNode: SlabNode): Array<[number, number]> {
return slabNode.autoFromWalls 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) : outsetPolygon(slabNode.polygon, SLAB_OUTSET)
} }