130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import {
|
|
type CeilingNode,
|
|
getMaterialPresetByRef,
|
|
resolveMaterial,
|
|
useLiveTransforms,
|
|
useRegistry,
|
|
useScene,
|
|
} from '@pascal-app/core'
|
|
import {
|
|
createSurfaceRoleMaterial,
|
|
NodeRenderer,
|
|
resolveSurfaceColor,
|
|
useViewer,
|
|
} from '@pascal-app/viewer'
|
|
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
import { BackSide, type Mesh } from 'three/webgpu'
|
|
import { createPlaceholderGeometry } from '../shared/placeholder-geometry'
|
|
import { ceilingColorFromRef, getCeilingMaterials } from './materials'
|
|
import { CEILING_SLOT_DEFAULT_COLOR } from './slots'
|
|
|
|
function createEmptyGeometry() {
|
|
return createPlaceholderGeometry()
|
|
}
|
|
|
|
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
|
const ref = useRef<Mesh>(null!)
|
|
const placeholderGeometry = useMemo(createEmptyGeometry, [])
|
|
const gridPlaceholderGeometry = useMemo(createEmptyGeometry, [])
|
|
|
|
useRegistry(node.id, 'ceiling', ref)
|
|
// Build the real geometry on mount instead of relying on a child item to
|
|
// mark us dirty (CeilingSystem only rebuilds dirty ceilings). Ceiling-hosted
|
|
// items are async GLB loads, so without this the ceiling holds its
|
|
// placeholder geometry until the first child finishes downloading — and a
|
|
// childless ceiling would never build at all. Mirrors WallRenderer /
|
|
// RoofRenderer.
|
|
useLayoutEffect(() => {
|
|
useScene.getState().markDirty(node.id)
|
|
}, [node.id])
|
|
const textures = useViewer((s) => s.textures)
|
|
const colorPreset = useViewer((s) => s.colorPreset)
|
|
const sceneTheme = useViewer((s) => s.sceneTheme)
|
|
// Subscribe to the scene-material library so editing a `scene:` material the
|
|
// ceiling slot references re-tints it live.
|
|
const sceneMaterials = useScene((s) => s.materials)
|
|
const liveTransform = useLiveTransforms((s) => s.get(node.id))
|
|
const ceilingY = (node.height ?? 2.5) - 0.01 + (liveTransform?.position[1] ?? 0)
|
|
const position: [number, number, number] = [
|
|
liveTransform?.position[0] ?? 0,
|
|
ceilingY,
|
|
liveTransform?.position[2] ?? 0,
|
|
]
|
|
|
|
useEffect(
|
|
() => () => {
|
|
placeholderGeometry.dispose()
|
|
gridPlaceholderGeometry.dispose()
|
|
},
|
|
[gridPlaceholderGeometry, placeholderGeometry],
|
|
)
|
|
|
|
const materials = useMemo(() => {
|
|
// Textures-off mode takes the themed 'ceiling' role colour — the guaranteed
|
|
// escape hatch, independent of any slot override. The bottom (seen from
|
|
// inside the room, looking up) stays opaque so the ceiling reads as a solid
|
|
// surface; the top keeps the transparent grid material so a top-down camera
|
|
// can see through the ceiling whenever the `ceiling-grid` overlay is
|
|
// revealed (placing a ceiling-hosted item, or selecting one of its
|
|
// children). Without that the top mesh would ship an opaque surface-role
|
|
// material and a top-down camera would lose everything under the ceiling.
|
|
if (!textures) {
|
|
const ceilingColor = resolveSurfaceColor('ceiling', colorPreset, sceneTheme)
|
|
return {
|
|
topMaterial: getCeilingMaterials(ceilingColor).topMaterial,
|
|
bottomMaterial: createSurfaceRoleMaterial('ceiling', colorPreset, BackSide, sceneTheme),
|
|
}
|
|
}
|
|
|
|
// Unified slot override — shared scene material or catalog `library:` finish
|
|
// (resolved to its base colour; a ceiling renders flat-tinted, not mapped).
|
|
const slotColor = ceilingColorFromRef(node.slots?.surface, sceneMaterials)
|
|
if (slotColor) return getCeilingMaterials(slotColor)
|
|
|
|
// Legacy inline material / preset (scenes painted before the slot model).
|
|
if (node.materialPreset || node.material) {
|
|
const preset = getMaterialPresetByRef(node.materialPreset)
|
|
const props = preset?.mapProperties ?? resolveMaterial(node.material)
|
|
return getCeilingMaterials(props.color || '#999999')
|
|
}
|
|
|
|
// Declared slot default.
|
|
return getCeilingMaterials(CEILING_SLOT_DEFAULT_COLOR)
|
|
}, [
|
|
textures,
|
|
colorPreset,
|
|
sceneTheme,
|
|
sceneMaterials,
|
|
node.slots,
|
|
node.materialPreset,
|
|
node.material,
|
|
node.material?.preset,
|
|
node.material?.properties,
|
|
node.material?.texture,
|
|
])
|
|
|
|
return (
|
|
<mesh
|
|
geometry={placeholderGeometry}
|
|
material={materials.bottomMaterial}
|
|
position={position}
|
|
ref={ref}
|
|
>
|
|
<mesh
|
|
geometry={gridPlaceholderGeometry}
|
|
material={materials.topMaterial}
|
|
name="ceiling-grid"
|
|
scale={0}
|
|
visible={false}
|
|
/>
|
|
{(node.children ?? []).map((childId) => (
|
|
<NodeRenderer key={childId} nodeId={childId} />
|
|
))}
|
|
</mesh>
|
|
)
|
|
}
|
|
|
|
export default CeilingRenderer
|