112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import { useFrame } from '@react-three/fiber'
|
|
import { type AnyNodeId, type CeilingNode, sceneRegistry, useScene } from '@pascal-app/core'
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|