import { type AnyNodeId, type CeilingNode, sceneRegistry, useScene } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' import * as THREE from 'three' function ensureUv2Attribute(geometry: THREE.BufferGeometry) { const uv = geometry.getAttribute('uv') if (!uv) return geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2)) } // ============================================================================ // CEILING SYSTEM // ============================================================================ export const CeilingSystem = () => { const dirtyNodes = useScene((state) => state.dirtyNodes) const clearDirty = useScene((state) => state.clearDirty) useFrame(() => { if (dirtyNodes.size === 0) return const nodes = useScene.getState().nodes // Process dirty ceilings dirtyNodes.forEach((id) => { const node = nodes[id] if (!node || node.type !== 'ceiling') return const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh if (mesh) { updateCeilingGeometry(node as CeilingNode, mesh) clearDirty(id as AnyNodeId) } // If mesh not found, keep it dirty for next frame }) }) return null } /** * Updates the geometry for a single ceiling */ function updateCeilingGeometry(node: CeilingNode, mesh: THREE.Mesh) { const newGeo = generateCeilingGeometry(node) mesh.geometry.dispose() mesh.geometry = newGeo const gridMesh = mesh.getObjectByName('ceiling-grid') as THREE.Mesh if (gridMesh) { gridMesh.geometry.dispose() gridMesh.geometry = newGeo.clone() } // Position at the ceiling height mesh.position.y = (node.height ?? 2.5) - 0.01 // Slight offset to avoid z-fighting with upper-level slabs } /** * Generates flat ceiling geometry from polygon (no extrusion) */ export function generateCeilingGeometry(ceilingNode: CeilingNode): THREE.BufferGeometry { const polygon = ceilingNode.polygon if (polygon.length < 3) { return new THREE.BufferGeometry() } // Create shape from polygon // Shape is in X-Y plane, we'll rotate to X-Z plane const shape = new THREE.Shape() const firstPt = polygon[0]! // Negate Y (which becomes Z) to get correct orientation after rotation shape.moveTo(firstPt[0], -firstPt[1]) for (let i = 1; i < polygon.length; i++) { const pt = polygon[i]! shape.lineTo(pt[0], -pt[1]) } shape.closePath() // Add holes to the shape const holes = ceilingNode.holes || [] for (const holePolygon of holes) { if (holePolygon.length < 3) continue const holePath = new THREE.Path() const holeFirstPt = holePolygon[0]! holePath.moveTo(holeFirstPt[0], -holeFirstPt[1]) for (let i = 1; i < holePolygon.length; i++) { const pt = holePolygon[i]! holePath.lineTo(pt[0], -pt[1]) } holePath.closePath() shape.holes.push(holePath) } // Create flat shape geometry (no extrusion) const geometry = new THREE.ShapeGeometry(shape) // Rotate so the shape lies flat in X-Z plane geometry.rotateX(-Math.PI / 2) geometry.computeVertexNormals() ensureUv2Attribute(geometry) return geometry }