import { type AnyNode, type AnyNodeId, getScaledDimensions, type ItemNode, type SlabNode, type WallNode, } from '../../schema' import useScene from '../../store/use-scene' import { itemOverlapsPolygon, spatialGridManager, wallOverlapsPolygon, } from './spatial-grid-manager' export function resolveLevelId(node: AnyNode, nodes: Record): string { // If the node itself is a level if (node.type === 'level') return node.id // Walk up parent chain to find level // This assumes you track parentId or can derive it let current: AnyNode | undefined = node while (current) { if (current.type === 'level') return current.id // Find parent (you might need to add parentId to your schema or derive it) if (current.parentId) { current = nodes[current.parentId] } else { current = undefined } } return 'default' // fallback for orphaned items } // Call this once at app initialization. Returns an unsubscribe function that // detaches the scene-store listener (useful when the editor is unmounted so // the spatial grid singleton does not hold stale references to old scenes). export function initSpatialGridSync(): () => void { const store = useScene // 1. Initial sync - process all existing nodes const state = store.getState() for (const node of Object.values(state.nodes)) { const levelId = resolveLevelId(node, state.nodes) spatialGridManager.handleNodeCreated(node, levelId) } // 2. Then subscribe to future changes const markDirty = (id: AnyNodeId) => store.getState().markDirty(id) // Subscribe to all changes const unsubscribe = store.subscribe((state, prevState) => { // Detect added nodes for (const [id, node] of Object.entries(state.nodes)) { if (!prevState.nodes[id as AnyNode['id']]) { const levelId = resolveLevelId(node, state.nodes) spatialGridManager.handleNodeCreated(node, levelId) // When a slab is added, mark overlapping items/walls dirty if (node.type === 'slab') { markNodesOverlappingSlab(node as SlabNode, state.nodes, markDirty) } } } // Detect removed nodes for (const [id, node] of Object.entries(prevState.nodes)) { if (!state.nodes[id as AnyNode['id']]) { const levelId = resolveLevelId(node, prevState.nodes) spatialGridManager.handleNodeDeleted(id, node.type, levelId) // When a slab is removed, mark items/walls that were on it dirty (using current state) if (node.type === 'slab') { markNodesOverlappingSlab(node as SlabNode, state.nodes, markDirty) } } } // Detect updated nodes (items with position/rotation/parentId/side changes, slabs with polygon/elevation changes) for (const [id, node] of Object.entries(state.nodes)) { const prev = prevState.nodes[id as AnyNode['id']] if (!prev) continue if (node.type === 'item' && prev.type === 'item') { if ( !( arraysEqual(node.position, prev.position) && arraysEqual(node.rotation, prev.rotation) && arraysEqual(node.scale, prev.scale) ) || node.parentId !== prev.parentId || node.side !== prev.side ) { const levelId = resolveLevelId(node, state.nodes) spatialGridManager.handleNodeUpdated(node, levelId) // Scale changes affect footprint size — mark dirty so slab elevation recalculates if (!arraysEqual(node.scale, prev.scale)) { markDirty(node.id) } } } else if (node.type === 'slab' && prev.type === 'slab') { if ( node.polygon !== prev.polygon || node.elevation !== prev.elevation || node.holes !== prev.holes ) { const levelId = resolveLevelId(node, state.nodes) spatialGridManager.handleNodeUpdated(node, levelId) // Mark nodes overlapping old polygon and new polygon as dirty markNodesOverlappingSlab(prev as SlabNode, state.nodes, markDirty) markNodesOverlappingSlab(node as SlabNode, state.nodes, markDirty) } } } }) return unsubscribe } function arraysEqual(a: number[], b: number[]): boolean { return a.length === b.length && a.every((v, i) => v === b[i]) } /** * Mark all floor items and walls that overlap a slab polygon as dirty. */ function markNodesOverlappingSlab( slab: SlabNode, nodes: Record, markDirty: (id: AnyNodeId) => void, ) { if (slab.polygon.length < 3) return const slabLevelId = resolveLevelId(slab, nodes) for (const node of Object.values(nodes)) { if (node.type === 'item') { const item = node as ItemNode // Only floor items are affected by slabs if (item.asset.attachTo) continue if (resolveLevelId(node, nodes) !== slabLevelId) continue if ( itemOverlapsPolygon( item.position, getScaledDimensions(item), item.rotation, slab.polygon, 0.01, ) ) { markDirty(node.id) } } else if (node.type === 'wall') { const wall = node as WallNode if (resolveLevelId(node, nodes) !== slabLevelId) continue if (wallOverlapsPolygon(wall.start, wall.end, slab.polygon)) { markDirty(node.id) } } } }