diff --git a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx index 1c1d05ff..e1d2717c 100644 --- a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx +++ b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx @@ -40,13 +40,14 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => { const handlers = useNodeEvents(node, 'ceiling') const materials = useMemo(() => { - if (node.material) { - const props = node.material.properties + const mat = node.material + if (mat) { + const props = mat.properties const color = props?.color || '#999999' return createCeilingMaterials(color) } return { topMaterial: createCeilingMaterials().topMaterial, bottomMaterial: DEFAULT_CEILING_MATERIAL } - }, [node.material]) + }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]) return ( diff --git a/packages/viewer/src/components/renderers/door/door-renderer.tsx b/packages/viewer/src/components/renderers/door/door-renderer.tsx index d268903c..9d87eba1 100644 --- a/packages/viewer/src/components/renderers/door/door-renderer.tsx +++ b/packages/viewer/src/components/renderers/door/door-renderer.tsx @@ -12,8 +12,10 @@ export const DoorRenderer = ({ node }: { node: DoorNode }) => { const isTransient = !!(node.metadata as Record | null)?.isTransient const material = useMemo(() => { - return node.material ? createMaterial(node.material) : DEFAULT_DOOR_MATERIAL - }, [node.material]) + const mat = node.material + if (!mat) return DEFAULT_DOOR_MATERIAL + return createMaterial(mat) + }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]) return ( { const handlers = useNodeEvents(node, 'roof-segment') const debugColors = useViewer((s) => s.debugColors) + const customMaterial = useMemo(() => { + const mat = node.material + if (!mat) return null + return createMaterial(mat) + }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]) + + const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials + return ( { ) -} +} \ No newline at end of file diff --git a/packages/viewer/src/components/renderers/roof/roof-renderer.tsx b/packages/viewer/src/components/renderers/roof/roof-renderer.tsx index d6694fe7..e5c88b51 100644 --- a/packages/viewer/src/components/renderers/roof/roof-renderer.tsx +++ b/packages/viewer/src/components/renderers/roof/roof-renderer.tsx @@ -16,8 +16,10 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => { const debugColors = useViewer((s) => s.debugColors) const customMaterial = useMemo(() => { - return node.material ? createMaterial(node.material) : null - }, [node.material]) + const mat = node.material + if (!mat) return null + return createMaterial(mat) + }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]) const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials diff --git a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx index 37f267d2..f33b6ec0 100644 --- a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx +++ b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx @@ -12,8 +12,10 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => { const handlers = useNodeEvents(node, 'slab') const material = useMemo(() => { - return node.material ? createMaterial(node.material) : DEFAULT_SLAB_MATERIAL - }, [node.material]) + const mat = node.material + if (!mat) return DEFAULT_SLAB_MATERIAL + return createMaterial(mat) + }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]) return ( diff --git a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx index a01ca1e0..1f54e9ec 100644 --- a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx +++ b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx @@ -17,8 +17,10 @@ export const WallRenderer = ({ node }: { node: WallNode }) => { const handlers = useNodeEvents(node, 'wall') const material = useMemo(() => { - return node.material ? createMaterial(node.material) : DEFAULT_WALL_MATERIAL - }, [node.material]) + const mat = node.material + if (!mat) return DEFAULT_WALL_MATERIAL + return createMaterial(mat) + }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]) return ( diff --git a/packages/viewer/src/components/renderers/window/window-renderer.tsx b/packages/viewer/src/components/renderers/window/window-renderer.tsx index c6c61d24..1f5c2c8e 100644 --- a/packages/viewer/src/components/renderers/window/window-renderer.tsx +++ b/packages/viewer/src/components/renderers/window/window-renderer.tsx @@ -12,8 +12,10 @@ export const WindowRenderer = ({ node }: { node: WindowNode }) => { const isTransient = !!(node.metadata as Record | null)?.isTransient const material = useMemo(() => { - return node.material ? createMaterial(node.material) : DEFAULT_WINDOW_MATERIAL - }, [node.material]) + const mat = node.material + if (!mat) return DEFAULT_WINDOW_MATERIAL + return createMaterial(mat) + }, [node.material, node.material?.preset, node.material?.properties, node.material?.texture]) return ( { - // Create a repeating grid pattern based on world position - const scale = float(0.1) // Dot grid spacing (10cm) - const dotSize = float(0.3) // Size of dots relative to grid + const scale = float(0.1) + const dotSize = float(0.3) - // Use XY coordinates for pattern on wall face const uv = vec2(positionLocal.x, positionLocal.y).div(scale) const gridUV = fract(uv) - // Distance from center of grid cell (creates circular dots) const dist = length(gridUV.sub(0.5)) - // Create dots: 1 where we want dots, 0 elsewhere const dots = step(dist, dotSize.mul(0.5)) - // Vertical fade: fade out as Y increases (from bottom to top) - const fadeHeight = float(2.5) // Fade over 2.5 meters + const fadeHeight = float(2.5) const yFade = float(1).sub(smoothstep(float(0), fadeHeight, positionLocal.y)) return dots.mul(yFade) }) -const invsibleWallMaterial = new MeshStandardNodeMaterial({ - transparent: true, - opacityNode: mix(float(0.0), float(0.24), dotPattern()), - color: 'white', - depthWrite: false, - emissive: 'white', -}) -const wallMaterial = new MeshStandardNodeMaterial({ - color: 'white', - roughness: 1, - metalness: 0, -}) +interface WallMaterials { + visible: MeshStandardNodeMaterial + invisible: MeshStandardNodeMaterial + materialHash: string +} + +const wallMaterialCache = new Map() + +function getMaterialHash(wallNode: WallNode): string { + if (!wallNode.material) return 'none' + const mat = wallNode.material + if (mat.preset && mat.preset !== 'custom') { + return `preset-${mat.preset}` + } + if (mat.properties) { + return `props-${mat.properties.color}-${mat.properties.roughness}-${mat.properties.metalness}` + } + return 'default' +} + +const presetColors = { + white: '#ffffff', + brick: '#8b4513', + concrete: '#808080', + wood: '#deb887', + glass: '#87ceeb', + metal: '#c0c0c0', + plaster: '#f5f5dc', + tile: '#dcdcdc', + marble: '#f5f5f5', +} as const + +function getPresetColor(preset: string): string { + return presetColors[preset as keyof typeof presetColors] ?? '#ffffff' +} + +function getMaterialsForWall(wallNode: WallNode): WallMaterials { + const cacheKey = wallNode.id + const materialHash = getMaterialHash(wallNode) + + const existing = wallMaterialCache.get(cacheKey) + if (existing && existing.materialHash === materialHash) { + return existing + } + + if (existing) { + existing.visible.dispose() + existing.invisible.dispose() + } + + let userColor = '#ffffff' + if (wallNode.material?.properties?.color) { + userColor = wallNode.material.properties.color + } else if (wallNode.material?.preset && wallNode.material.preset !== 'custom') { + userColor = getPresetColor(wallNode.material.preset) + } + + const visibleMat = new MeshStandardNodeMaterial({ + color: userColor, + roughness: 1, + metalness: 0, + }) + + const invisibleMat = new MeshStandardNodeMaterial({ + transparent: true, + opacityNode: mix(float(0.0), float(0.24), dotPattern()), + color: userColor, + depthWrite: false, + emissive: userColor, + }) + + const result: WallMaterials = { visible: visibleMat, invisible: invisibleMat, materialHash } + wallMaterialCache.set(cacheKey, result) + return result +} + +function getWallHideState(wallNode: WallNode, wallMesh: Mesh, wallMode: string, cameraDir: Vector3): boolean { + let hideWall = wallNode.frontSide === 'interior' && wallNode.backSide === 'interior' + + if (wallMode === 'up') { + hideWall = false + } else if (wallMode === 'down') { + hideWall = true + } else { + wallMesh.getWorldDirection(v) + if (v.dot(cameraDir) < 0) { + if (wallNode.frontSide === 'exterior' && wallNode.backSide !== 'exterior') { + hideWall = true + } + } else if (wallNode.backSide === 'exterior' && wallNode.frontSide !== 'exterior') { + hideWall = true + } + } + + return hideWall +} export const WallCutout = () => { const lastCameraPosition = useRef(new Vector3()) @@ -52,6 +130,7 @@ export const WallCutout = () => { const lastUpdateTime = useRef(0) const lastWallMode = useRef(useViewer.getState().wallMode) const lastNumberOfWalls = useRef(0) + const lastWallMaterials = useRef>(new Map()) useFrame(({ camera, clock }) => { const wallMode = useViewer.getState().wallMode @@ -60,54 +139,67 @@ export const WallCutout = () => { camera.getWorldDirection(tmpVec) tmpVec.add(currentCameraPosition) - // Throttle: only update if camera moved significantly AND enough time passed const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current) const directionChanged = tmpVec.distanceTo(lastCameraTarget.current) const timeSinceUpdate = currentTime - lastUpdateTime.current - // Update if moved > 0.5m OR direction changed > 0.3 AND at least 100ms passed - if ( + const shouldUpdate = ((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) || lastWallMode.current !== wallMode || sceneRegistry.byType.wall.size !== lastNumberOfWalls.current - ) { - // Camera has moved, update cutout logic here - // Update last known positions and time + const walls = sceneRegistry.byType.wall + const currentWallIds = new Set() + + walls.forEach((wallId) => { + const wallMesh = sceneRegistry.nodes.get(wallId) + if (!wallMesh) return + const wallNode = useScene.getState().nodes[wallId as WallNode['id']] + if (!wallNode || wallNode.type !== 'wall') return + + currentWallIds.add(wallId) + + const hideWall = getWallHideState(wallNode, wallMesh as Mesh, wallMode, u) + + if (shouldUpdate) { + const materials = getMaterialsForWall(wallNode) + ;(wallMesh as Mesh).material = hideWall ? materials.invisible : materials.visible + } else { + const currentMaterial = (wallMesh as Mesh).material + const materials = wallMaterialCache.get(wallId) + if (!materials || currentMaterial !== (hideWall ? materials.invisible : materials.visible)) { + const newMaterials = getMaterialsForWall(wallNode) + ;(wallMesh as Mesh).material = hideWall ? newMaterials.invisible : newMaterials.visible + } + } + }) + + if (shouldUpdate) { lastCameraPosition.current.copy(currentCameraPosition) lastCameraTarget.current.copy(tmpVec) lastUpdateTime.current = currentTime camera.getWorldDirection(u) - const walls = sceneRegistry.byType.wall - walls.forEach((wallId) => { - const wallMesh = sceneRegistry.nodes.get(wallId) - if (!wallMesh) return - const wallNode = useScene.getState().nodes[wallId as WallNode['id']] - if (!wallNode || wallNode.type !== 'wall') return - let hideWall = wallNode.frontSide === 'interior' && wallNode.backSide === 'interior' + if (lastWallMode.current !== wallMode) { + wallMaterialCache.clear() + } - if (wallMode === 'up') { - hideWall = false - } else if (wallMode === 'down') { - hideWall = true - } else { - wallMesh.getWorldDirection(v) - if (v.dot(u) < 0) { - // Front side - if (wallNode.frontSide === 'exterior' && wallNode.backSide !== 'exterior') { - hideWall = true - } - } else if (wallNode.backSide === 'exterior' && wallNode.frontSide !== 'exterior') { - // Back side - hideWall = true - } + for (const [wallId, mats] of lastWallMaterials.current) { + if (!currentWallIds.has(wallId)) { + mats.visible.dispose() + mats.invisible.dispose() + wallMaterialCache.delete(wallId) } - ;(wallMesh as Mesh).material = hideWall ? invsibleWallMaterial : wallMaterial - }) + } + + lastWallMaterials.current.clear() + for (const [wallId, mats] of wallMaterialCache) { + lastWallMaterials.current.set(wallId, mats) + } + lastWallMode.current = wallMode lastNumberOfWalls.current = sceneRegistry.byType.wall.size } }) return null -} +} \ No newline at end of file