Fix auto ceiling height sync
This commit is contained in:
@@ -61,6 +61,7 @@ export {
|
|||||||
} from './lib/door-operation'
|
} from './lib/door-operation'
|
||||||
export { getRenderableSlabPolygon } from './lib/slab-polygon'
|
export { getRenderableSlabPolygon } from './lib/slab-polygon'
|
||||||
export {
|
export {
|
||||||
|
type AutoCeilingPlanningContext,
|
||||||
type AutoCeilingSyncPlan,
|
type AutoCeilingSyncPlan,
|
||||||
type AutoSlabSyncPlan,
|
type AutoSlabSyncPlan,
|
||||||
detectSpacesForLevel,
|
detectSpacesForLevel,
|
||||||
@@ -69,6 +70,7 @@ export {
|
|||||||
pauseSpaceDetection,
|
pauseSpaceDetection,
|
||||||
planAutoCeilingsForLevel,
|
planAutoCeilingsForLevel,
|
||||||
planAutoSlabsForLevel,
|
planAutoSlabsForLevel,
|
||||||
|
projectAutoSlabsForPlan,
|
||||||
resumeSpaceDetection,
|
resumeSpaceDetection,
|
||||||
type Space,
|
type Space,
|
||||||
wallTouchesOthers,
|
wallTouchesOthers,
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
import { CeilingNode, SlabNode, WallNode } from '../schema'
|
||||||
|
import { planAutoCeilingsForLevel } from './space-detection'
|
||||||
|
|
||||||
|
const square: Array<[number, number]> = [
|
||||||
|
[0, 0],
|
||||||
|
[4, 0],
|
||||||
|
[4, 3],
|
||||||
|
[0, 3],
|
||||||
|
]
|
||||||
|
|
||||||
|
function roomPolygon() {
|
||||||
|
return square.map(([x, y]) => ({ x, y }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function squareWalls(height = 2.5) {
|
||||||
|
return [
|
||||||
|
WallNode.parse({ start: [0, 0], end: [4, 0], height }),
|
||||||
|
WallNode.parse({ start: [4, 0], end: [4, 3], height }),
|
||||||
|
WallNode.parse({ start: [4, 3], end: [0, 3], height }),
|
||||||
|
WallNode.parse({ start: [0, 3], end: [0, 0], height }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function slab(elevation: number) {
|
||||||
|
return SlabNode.parse({
|
||||||
|
polygon: square,
|
||||||
|
elevation,
|
||||||
|
autoFromWalls: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('planAutoCeilingsForLevel', () => {
|
||||||
|
test('creates auto ceilings at the top of the room walls', () => {
|
||||||
|
const created = planAutoCeilingsForLevel([roomPolygon()], [], {
|
||||||
|
walls: squareWalls(),
|
||||||
|
slabs: [slab(0.05)],
|
||||||
|
}).create[0]
|
||||||
|
|
||||||
|
expect(created?.height).toBeCloseTo(2.55)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('updates existing auto ceiling height when the slab elevation changes', () => {
|
||||||
|
const ceiling = CeilingNode.parse({
|
||||||
|
polygon: square,
|
||||||
|
height: 2.55,
|
||||||
|
autoFromWalls: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
|
||||||
|
walls: squareWalls(),
|
||||||
|
slabs: [slab(0.4)],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(plan.update).toHaveLength(1)
|
||||||
|
expect(plan.update[0]?.id).toBe(ceiling.id)
|
||||||
|
expect(plan.update[0]?.data.polygon).toBeUndefined()
|
||||||
|
expect(plan.update[0]?.data.height).toBeCloseTo(2.9)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('updates existing auto ceiling height when wall height changes', () => {
|
||||||
|
const ceiling = CeilingNode.parse({
|
||||||
|
polygon: square,
|
||||||
|
height: 2.55,
|
||||||
|
autoFromWalls: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const plan = planAutoCeilingsForLevel([roomPolygon()], [ceiling], {
|
||||||
|
walls: squareWalls(3),
|
||||||
|
slabs: [slab(0.05)],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(plan.update).toHaveLength(1)
|
||||||
|
expect(plan.update[0]?.data.height).toBeCloseTo(3.05)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not replace a manual ceiling with an auto ceiling', () => {
|
||||||
|
const manualCeiling = CeilingNode.parse({
|
||||||
|
polygon: square,
|
||||||
|
height: 2.5,
|
||||||
|
autoFromWalls: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const plan = planAutoCeilingsForLevel([roomPolygon()], [manualCeiling], {
|
||||||
|
walls: squareWalls(),
|
||||||
|
slabs: [slab(0.4)],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(plan.create).toHaveLength(0)
|
||||||
|
expect(plan.update).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -41,6 +41,10 @@ type DetectedRoom = {
|
|||||||
bbox: ReturnType<typeof bboxOf>
|
bbox: ReturnType<typeof bboxOf>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DetectedCeilingRoom = DetectedRoom & {
|
||||||
|
ceilingHeight: number
|
||||||
|
}
|
||||||
|
|
||||||
export type AutoSlabSyncPlan = {
|
export type AutoSlabSyncPlan = {
|
||||||
create: SlabNodeType[]
|
create: SlabNodeType[]
|
||||||
update: Array<{ id: SlabNodeType['id']; data: Partial<SlabNodeType> }>
|
update: Array<{ id: SlabNodeType['id']; data: Partial<SlabNodeType> }>
|
||||||
@@ -55,9 +59,16 @@ export type AutoCeilingSyncPlan = {
|
|||||||
|
|
||||||
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05
|
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05
|
||||||
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5
|
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5
|
||||||
|
const CEILING_HEIGHT_EPSILON = 1e-6
|
||||||
const ROOM_CURVE_TOLERANCE = 0.04
|
const ROOM_CURVE_TOLERANCE = 0.04
|
||||||
const MAX_CURVE_SUBDIVISION_DEPTH = 6
|
const MAX_CURVE_SUBDIVISION_DEPTH = 6
|
||||||
const AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE = 0.08
|
const AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE = 0.08
|
||||||
|
const WALL_ROOM_BOUNDARY_TOLERANCE = 0.08
|
||||||
|
|
||||||
|
export type AutoCeilingPlanningContext = {
|
||||||
|
walls?: WallNode[]
|
||||||
|
slabs?: SlabNodeType[]
|
||||||
|
}
|
||||||
|
|
||||||
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] }
|
||||||
@@ -186,6 +197,100 @@ function bboxOverlapArea(a: ReturnType<typeof bboxOf>, b: ReturnType<typeof bbox
|
|||||||
return ix * iy
|
return ix * iy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pointDistanceToPolygonBoundary(point: Point2D, polygon: Point2D[]) {
|
||||||
|
let minDistance = Number.POSITIVE_INFINITY
|
||||||
|
for (let index = 0; index < polygon.length; index += 1) {
|
||||||
|
const start = polygon[index]
|
||||||
|
const end = polygon[(index + 1) % polygon.length]
|
||||||
|
if (!(start && end)) continue
|
||||||
|
minDistance = Math.min(
|
||||||
|
minDistance,
|
||||||
|
distanceToSegment(pointToTuple(point), pointToTuple(start), pointToTuple(end)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return minDistance
|
||||||
|
}
|
||||||
|
|
||||||
|
function wallBoundsRoom(wall: WallNode, roomPolygon: Point2D[]) {
|
||||||
|
const sampled = sampleWallPointsForRoomDetection(wall)
|
||||||
|
if (sampled.length === 0) return false
|
||||||
|
|
||||||
|
const candidates =
|
||||||
|
sampled.length === 2
|
||||||
|
? [
|
||||||
|
sampled[0]!,
|
||||||
|
{
|
||||||
|
x: (sampled[0]!.x + sampled[1]!.x) / 2,
|
||||||
|
y: (sampled[0]!.y + sampled[1]!.y) / 2,
|
||||||
|
},
|
||||||
|
sampled[1]!,
|
||||||
|
]
|
||||||
|
: sampled
|
||||||
|
|
||||||
|
const matchingPoints = candidates.filter(
|
||||||
|
(point) => pointDistanceToPolygonBoundary(point, roomPolygon) <= WALL_ROOM_BOUNDARY_TOLERANCE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return matchingPoints.length >= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointIsOnSlab(point: Point2D, slab: SlabNodeType) {
|
||||||
|
if (slab.polygon.length < 3) return false
|
||||||
|
const slabPolygon = slab.polygon.map(pointFromTuple)
|
||||||
|
if (!pointInPolygon(point, slabPolygon)) return false
|
||||||
|
|
||||||
|
for (const hole of slab.holes ?? []) {
|
||||||
|
if (hole.length >= 3 && pointInPolygon(point, hole.map(pointFromTuple))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function slabSupportsRoom(roomPolygon: Point2D[], slab: SlabNodeType) {
|
||||||
|
if (slab.polygon.length < 3) return false
|
||||||
|
if (polygonSignature(slab.polygon.map(pointFromTuple)) === polygonSignature(roomPolygon)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return pointIsOnSlab(polygonCentroid(roomPolygon), slab)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRoomSlabElevation(roomPolygon: Point2D[], slabs: SlabNodeType[] = []) {
|
||||||
|
let maxElevation = 0
|
||||||
|
|
||||||
|
for (const slab of slabs) {
|
||||||
|
if (!slabSupportsRoom(roomPolygon, slab)) continue
|
||||||
|
maxElevation = Math.max(maxElevation, slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxElevation
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRoomWallHeight(roomPolygon: Point2D[], walls: WallNode[] = []) {
|
||||||
|
let maxHeight = 0
|
||||||
|
|
||||||
|
for (const wall of walls) {
|
||||||
|
if (!wallBoundsRoom(wall, roomPolygon)) continue
|
||||||
|
const height = wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT
|
||||||
|
if (Number.isFinite(height)) {
|
||||||
|
maxHeight = Math.max(maxHeight, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxHeight > 0 ? maxHeight : DEFAULT_AUTO_CEILING_HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAutoCeilingHeight(
|
||||||
|
roomPolygon: Point2D[],
|
||||||
|
context: AutoCeilingPlanningContext = {},
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
resolveRoomSlabElevation(roomPolygon, context.slabs) +
|
||||||
|
resolveRoomWallHeight(roomPolygon, context.walls)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function getWallDirection(wall: Pick<WallNode, 'start' | 'end'>) {
|
function getWallDirection(wall: Pick<WallNode, 'start' | 'end'>) {
|
||||||
const dx = wall.end[0] - wall.start[0]
|
const dx = wall.end[0] - wall.start[0]
|
||||||
const dy = wall.end[1] - wall.start[1]
|
const dy = wall.end[1] - wall.start[1]
|
||||||
@@ -481,6 +586,7 @@ function wallGeometrySignature(wall: WallNode) {
|
|||||||
wall.end[0].toFixed(4),
|
wall.end[0].toFixed(4),
|
||||||
wall.end[1].toFixed(4),
|
wall.end[1].toFixed(4),
|
||||||
(wall.thickness ?? 0.2).toFixed(4),
|
(wall.thickness ?? 0.2).toFixed(4),
|
||||||
|
(wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT).toFixed(4),
|
||||||
getClampedWallCurveOffset(wall).toFixed(4),
|
getClampedWallCurveOffset(wall).toFixed(4),
|
||||||
].join('|')
|
].join('|')
|
||||||
}
|
}
|
||||||
@@ -489,6 +595,48 @@ function levelWallSnapshot(walls: WallNode[]) {
|
|||||||
return walls.map(wallGeometrySignature).sort().join('||')
|
return walls.map(wallGeometrySignature).sort().join('||')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function slabGeometrySignature(slab: SlabNodeType) {
|
||||||
|
const polygon = slab.polygon
|
||||||
|
.map((point) => `${point[0].toFixed(4)},${point[1].toFixed(4)}`)
|
||||||
|
.join(';')
|
||||||
|
const holes = (slab.holes ?? [])
|
||||||
|
.map((hole) => hole.map((point) => `${point[0].toFixed(4)},${point[1].toFixed(4)}`).join(';'))
|
||||||
|
.join('/')
|
||||||
|
|
||||||
|
return [slab.id, (slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION).toFixed(4), polygon, holes].join(
|
||||||
|
'|',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function levelSlabSnapshot(slabs: SlabNodeType[]) {
|
||||||
|
return slabs.map(slabGeometrySignature).sort().join('||')
|
||||||
|
}
|
||||||
|
|
||||||
|
function levelStructureSnapshots(nodes: Record<string, any>) {
|
||||||
|
const byLevel = new Map<string, { walls: WallNode[]; slabs: SlabNodeType[] }>()
|
||||||
|
const getEntry = (levelId: string) => {
|
||||||
|
const entry = byLevel.get(levelId) ?? { walls: [], slabs: [] }
|
||||||
|
byLevel.set(levelId, entry)
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of Object.values(nodes)) {
|
||||||
|
if (!(node && typeof node === 'object' && 'parentId' in node && node.parentId)) continue
|
||||||
|
if ((node as any).type === 'wall') {
|
||||||
|
getEntry((node as any).parentId).walls.push(node as WallNode)
|
||||||
|
} else if ((node as any).type === 'slab') {
|
||||||
|
getEntry((node as any).parentId).slabs.push(SlabNode.parse(node))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshots = new Map<string, string>()
|
||||||
|
for (const [levelId, entry] of byLevel.entries()) {
|
||||||
|
snapshots.set(levelId, `${levelWallSnapshot(entry.walls)}##${levelSlabSnapshot(entry.slabs)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots
|
||||||
|
}
|
||||||
|
|
||||||
function buildSpace(levelId: string, polygon: Point2D[]): Space {
|
function buildSpace(levelId: string, polygon: Point2D[]): Space {
|
||||||
const signature = polygonSignature(polygon)
|
const signature = polygonSignature(polygon)
|
||||||
return {
|
return {
|
||||||
@@ -654,18 +802,44 @@ function syncAutoSlabsForLevel(
|
|||||||
if (plan.create.length > 0) {
|
if (plan.create.length > 0) {
|
||||||
sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })))
|
sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectAutoSlabsForPlan(
|
||||||
|
existingSlabs: SlabNodeType[],
|
||||||
|
plan: AutoSlabSyncPlan,
|
||||||
|
): SlabNodeType[] {
|
||||||
|
const slabsById = new Map(existingSlabs.map((slab) => [slab.id, slab]))
|
||||||
|
|
||||||
|
for (const id of plan.delete) {
|
||||||
|
slabsById.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const update of plan.update) {
|
||||||
|
const slab = slabsById.get(update.id)
|
||||||
|
if (!slab) continue
|
||||||
|
slabsById.set(update.id, SlabNode.parse({ ...slab, ...update.data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const slab of plan.create) {
|
||||||
|
slabsById.set(slab.id, slab)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...slabsById.values()]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function planAutoCeilingsForLevel(
|
export function planAutoCeilingsForLevel(
|
||||||
roomPolygons: Point2D[][],
|
roomPolygons: Point2D[][],
|
||||||
existingCeilings: CeilingNodeType[],
|
existingCeilings: CeilingNodeType[],
|
||||||
|
context: AutoCeilingPlanningContext = {},
|
||||||
): AutoCeilingSyncPlan {
|
): AutoCeilingSyncPlan {
|
||||||
const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls)
|
const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls)
|
||||||
const manualSignatures = new Set(
|
const manualSignatures = new Set(
|
||||||
manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))),
|
manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))),
|
||||||
)
|
)
|
||||||
|
|
||||||
const detected: DetectedRoom[] = roomPolygons
|
const detected: DetectedCeilingRoom[] = roomPolygons
|
||||||
.map((poly) => ({
|
.map((poly) => ({
|
||||||
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(
|
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(
|
||||||
pointFromTuple,
|
pointFromTuple,
|
||||||
@@ -681,6 +855,7 @@ export function planAutoCeilingsForLevel(
|
|||||||
centroid: polygonCentroid(room.poly),
|
centroid: polygonCentroid(room.poly),
|
||||||
area: Math.abs(polygonArea(room.poly)),
|
area: Math.abs(polygonArea(room.poly)),
|
||||||
bbox: bboxOf(room.poly),
|
bbox: bboxOf(room.poly),
|
||||||
|
ceilingHeight: resolveAutoCeilingHeight(room.poly, context),
|
||||||
}))
|
}))
|
||||||
.filter(({ sig }) => !manualSignatures.has(sig))
|
.filter(({ sig }) => !manualSignatures.has(sig))
|
||||||
|
|
||||||
@@ -698,7 +873,7 @@ export function planAutoCeilingsForLevel(
|
|||||||
|
|
||||||
const matchedCeilingIds = new Set<string>()
|
const matchedCeilingIds = new Set<string>()
|
||||||
const matchedDetectedIdx = new Set<number>()
|
const matchedDetectedIdx = new Set<number>()
|
||||||
const updatesById = new Map<string, [number, number][]>()
|
const updatesById = new Map<string, { polygon: [number, number][]; height: number }>()
|
||||||
|
|
||||||
const autoBySignature = new Map<string, (typeof existingAutoMeta)[number]>()
|
const autoBySignature = new Map<string, (typeof existingAutoMeta)[number]>()
|
||||||
for (const entry of existingAutoMeta) {
|
for (const entry of existingAutoMeta) {
|
||||||
@@ -711,7 +886,10 @@ export function planAutoCeilingsForLevel(
|
|||||||
|
|
||||||
matchedDetectedIdx.add(index)
|
matchedDetectedIdx.add(index)
|
||||||
matchedCeilingIds.add(existing.ceiling.id)
|
matchedCeilingIds.add(existing.ceiling.id)
|
||||||
updatesById.set(existing.ceiling.id, room.poly.map(pointToTuple))
|
updatesById.set(existing.ceiling.id, {
|
||||||
|
polygon: room.poly.map(pointToTuple),
|
||||||
|
height: room.ceilingHeight,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const remainingDetected = detected
|
const remainingDetected = detected
|
||||||
@@ -746,7 +924,10 @@ export function planAutoCeilingsForLevel(
|
|||||||
|
|
||||||
matchedDetectedIdx.add(index)
|
matchedDetectedIdx.add(index)
|
||||||
matchedCeilingIds.add(bestMatch.entry.ceiling.id)
|
matchedCeilingIds.add(bestMatch.entry.ceiling.id)
|
||||||
updatesById.set(bestMatch.entry.ceiling.id, room.poly.map(pointToTuple))
|
updatesById.set(bestMatch.entry.ceiling.id, {
|
||||||
|
polygon: room.poly.map(pointToTuple),
|
||||||
|
height: room.ceilingHeight,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ceilingsToDelete = existingAuto
|
const ceilingsToDelete = existingAuto
|
||||||
@@ -756,12 +937,21 @@ export function planAutoCeilingsForLevel(
|
|||||||
const ceilingsToUpdate = existingAuto
|
const ceilingsToUpdate = existingAuto
|
||||||
.filter((ceiling) => updatesById.has(ceiling.id))
|
.filter((ceiling) => updatesById.has(ceiling.id))
|
||||||
.flatMap((ceiling) => {
|
.flatMap((ceiling) => {
|
||||||
const polygon = updatesById.get(ceiling.id)
|
const update = updatesById.get(ceiling.id)
|
||||||
if (!polygon) return []
|
if (!update) return []
|
||||||
|
|
||||||
return sameTuplePolygon(ceiling.polygon, polygon)
|
const data: Partial<CeilingNodeType> = {}
|
||||||
? []
|
if (!sameTuplePolygon(ceiling.polygon, update.polygon)) {
|
||||||
: [{ id: ceiling.id, data: { polygon } }]
|
data.polygon = update.polygon
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Math.abs((ceiling.height ?? DEFAULT_AUTO_CEILING_HEIGHT) - update.height) >
|
||||||
|
CEILING_HEIGHT_EPSILON
|
||||||
|
) {
|
||||||
|
data.height = update.height
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(data).length === 0 ? [] : [{ id: ceiling.id, data }]
|
||||||
})
|
})
|
||||||
|
|
||||||
const plannedCeilingsForNaming: Array<{ name?: string }> = [...existingCeilings]
|
const plannedCeilingsForNaming: Array<{ name?: string }> = [...existingCeilings]
|
||||||
@@ -780,7 +970,7 @@ export function planAutoCeilingsForLevel(
|
|||||||
name,
|
name,
|
||||||
polygon: room.poly.map(pointToTuple),
|
polygon: room.poly.map(pointToTuple),
|
||||||
holes: [],
|
holes: [],
|
||||||
height: DEFAULT_AUTO_CEILING_HEIGHT,
|
height: room.ceilingHeight,
|
||||||
autoFromWalls: true,
|
autoFromWalls: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -798,8 +988,9 @@ function syncAutoCeilingsForLevel(
|
|||||||
roomPolygons: Point2D[][],
|
roomPolygons: Point2D[][],
|
||||||
existingCeilings: CeilingNodeType[],
|
existingCeilings: CeilingNodeType[],
|
||||||
sceneStore: any,
|
sceneStore: any,
|
||||||
|
context: AutoCeilingPlanningContext = {},
|
||||||
) {
|
) {
|
||||||
const plan = planAutoCeilingsForLevel(roomPolygons, existingCeilings)
|
const plan = planAutoCeilingsForLevel(roomPolygons, existingCeilings, context)
|
||||||
|
|
||||||
if (plan.delete.length > 0) {
|
if (plan.delete.length > 0) {
|
||||||
sceneStore.getState().deleteNodes(plan.delete)
|
sceneStore.getState().deleteNodes(plan.delete)
|
||||||
@@ -882,17 +1073,15 @@ function runSpaceDetection(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
syncAutoSlabsForLevel(
|
const parsedSlabs = slabs.map((slab: any) => SlabNode.parse(slab))
|
||||||
levelId,
|
const slabPlan = syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore)
|
||||||
roomPolygons,
|
const projectedSlabs = projectAutoSlabsForPlan(parsedSlabs, slabPlan)
|
||||||
slabs.map((slab: any) => SlabNode.parse(slab)),
|
|
||||||
sceneStore,
|
|
||||||
)
|
|
||||||
syncAutoCeilingsForLevel(
|
syncAutoCeilingsForLevel(
|
||||||
levelId,
|
levelId,
|
||||||
roomPolygons,
|
roomPolygons,
|
||||||
ceilings.map((ceiling: any) => CeilingNode.parse(ceiling)),
|
ceilings.map((ceiling: any) => CeilingNode.parse(ceiling)),
|
||||||
sceneStore,
|
sceneStore,
|
||||||
|
{ walls, slabs: projectedSlabs },
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const space of spaces) {
|
for (const space of spaces) {
|
||||||
@@ -935,21 +1124,7 @@ export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () =>
|
|||||||
if (getSceneHistoryPauseDepth() > 0) return
|
if (getSceneHistoryPauseDepth() > 0) return
|
||||||
|
|
||||||
const nodes = state.nodes
|
const nodes = state.nodes
|
||||||
const wallsByLevel = new Map<string, WallNode[]>()
|
const currentSnapshots = levelStructureSnapshots(nodes)
|
||||||
|
|
||||||
for (const node of Object.values(nodes)) {
|
|
||||||
if (node && (node as any).type === 'wall' && (node as any).parentId) {
|
|
||||||
const levelId = (node as any).parentId as string
|
|
||||||
const levelWalls = wallsByLevel.get(levelId) ?? []
|
|
||||||
levelWalls.push(node as WallNode)
|
|
||||||
wallsByLevel.set(levelId, levelWalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentSnapshots = new Map<string, string>()
|
|
||||||
for (const [levelId, walls] of wallsByLevel.entries()) {
|
|
||||||
currentSnapshots.set(levelId, levelWallSnapshot(walls))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paused: roll the snapshot forward so we don't backfill (and re-duplicate)
|
// Paused: roll the snapshot forward so we don't backfill (and re-duplicate)
|
||||||
// every paused change once detection resumes. Whatever the AI built while
|
// every paused change once detection resumes. Whatever the AI built while
|
||||||
@@ -984,7 +1159,8 @@ export function initSpaceDetectionSync(sceneStore: any, editorStore: any): () =>
|
|||||||
} finally {
|
} finally {
|
||||||
resumeSceneHistory(sceneStore)
|
resumeSceneHistory(sceneStore)
|
||||||
previousSnapshots.clear()
|
previousSnapshots.clear()
|
||||||
for (const [levelId, snapshot] of currentSnapshots.entries()) {
|
const postRunSnapshots = levelStructureSnapshots(sceneStore.getState().nodes)
|
||||||
|
for (const [levelId, snapshot] of postRunSnapshots.entries()) {
|
||||||
previousSnapshots.set(levelId, snapshot)
|
previousSnapshots.set(levelId, snapshot)
|
||||||
}
|
}
|
||||||
isProcessing = false
|
isProcessing = false
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
planAutoCeilingsForLevel,
|
planAutoCeilingsForLevel,
|
||||||
planAutoSlabsForLevel,
|
planAutoSlabsForLevel,
|
||||||
planWallMoveJunctions,
|
planWallMoveJunctions,
|
||||||
|
projectAutoSlabsForPlan,
|
||||||
resumeSceneHistory,
|
resumeSceneHistory,
|
||||||
type SlabNode,
|
type SlabNode,
|
||||||
useLiveNodeOverrides,
|
useLiveNodeOverrides,
|
||||||
@@ -266,10 +267,15 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
// existing room → existing slab" logic sees stable IDs across
|
// existing room → existing slab" logic sees stable IDs across
|
||||||
// ticks. Without this anchor, IDs would drift as overrides
|
// ticks. Without this anchor, IDs would drift as overrides
|
||||||
// re-flowed through the planner.
|
// re-flowed through the planner.
|
||||||
const slabPlan = planAutoSlabsForLevel(roomPolygons, getLevelSlabs(levelId, sceneState.nodes))
|
const existingSlabs = getLevelSlabs(levelId, sceneState.nodes)
|
||||||
|
const slabPlan = planAutoSlabsForLevel(roomPolygons, existingSlabs)
|
||||||
const ceilingPlan = planAutoCeilingsForLevel(
|
const ceilingPlan = planAutoCeilingsForLevel(
|
||||||
roomPolygons,
|
roomPolygons,
|
||||||
getLevelCeilings(levelId, sceneState.nodes),
|
getLevelCeilings(levelId, sceneState.nodes),
|
||||||
|
{
|
||||||
|
walls: levelWalls,
|
||||||
|
slabs: projectAutoSlabsForPlan(existingSlabs, slabPlan),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
latestSurfacePlans = { slabs: slabPlan, ceilings: ceilingPlan }
|
latestSurfacePlans = { slabs: slabPlan, ceilings: ceilingPlan }
|
||||||
@@ -281,8 +287,8 @@ export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
|
|||||||
touchedSlabIds.add(update.id as AnyNodeId)
|
touchedSlabIds.add(update.id as AnyNodeId)
|
||||||
}
|
}
|
||||||
for (const update of ceilingPlan.update) {
|
for (const update of ceilingPlan.update) {
|
||||||
if (update.data.polygon === undefined) continue
|
if (update.data.polygon === undefined && update.data.height === undefined) continue
|
||||||
overrideEntries.push([update.id, { polygon: update.data.polygon }])
|
overrideEntries.push([update.id, update.data as Record<string, unknown>])
|
||||||
touchedCeilingIds.add(update.id as AnyNodeId)
|
touchedCeilingIds.add(update.id as AnyNodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user