Files
editor/packages/viewer/src/systems/wall/wall-cutout.tsx
T
Wassim SAMADandClaude Opus 4.8 8633886eef fix(selection): selected objects keep their texture (light glow, no wash)
Selecting a textured wall/slab/item replaced its surface with a flat
purple wash. Root cause: the selection highlight clones the material and
tints it, but `NodeMaterial.clone()` on the WebGPU backend drops the
texture-map node assignments, so the clone rendered flat — and a strong
albedo blend + emissive washed whatever was left.

Fix: re-attach the maps from the source material after cloning (shared by
reference) and drop the albedo tint, keeping only a gentle indigo emissive
so the real material/texture stays readable with a soft "selected" glow.
Applied to both highlight paths:
- generic editor highlight (slabs/items) in selection-manager
- wall path (walls are excluded from the generic one), built lazily +
  cached/self-healing so it survives the wall finish's async texture load

Removes the now-dead eager wall `highlightedVisible`/`selection` profile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:17:41 -04:00

184 lines
6.7 KiB
TypeScript

import { type AnyNodeId, emitter, sceneRegistry, useScene, type WallNode } from '@pascal-app/core'
import { useFrame } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import type { Material } from 'three'
import { type Mesh, Vector3 } from 'three/webgpu'
import useViewer from '../../store/use-viewer'
import { getMaterialsForWall, getSelectionHighlightMaterials } from './wall-materials'
const tmpVec = new Vector3()
const u = new Vector3()
const v = new Vector3()
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())
const lastCameraTarget = useRef(new Vector3())
const lastUpdateTime = useRef(0)
const lastWallMode = useRef<string>(useViewer.getState().wallMode)
const lastShading = useRef(useViewer.getState().shading)
const lastNumberOfWalls = useRef(0)
const lastHighlightKey = useRef('')
const lastTextures = useRef(useViewer.getState().textures)
const lastColorPreset = useRef(useViewer.getState().colorPreset)
const lastSceneTheme = useRef(useViewer.getState().sceneTheme)
useFrame(({ camera, clock }) => {
const wallMode = useViewer.getState().wallMode
const shading = useViewer.getState().shading
const textures = useViewer.getState().textures
const colorPreset = useViewer.getState().colorPreset
const sceneTheme = useViewer.getState().sceneTheme
const selectedIds = useViewer.getState().selection.selectedIds
const previewSelectedIds = useViewer.getState().previewSelectedIds
const hoveredId = useViewer.getState().hoveredId
const hoverHighlightMode = useViewer.getState().hoverHighlightMode
const currentTime = clock.elapsedTime
const currentCameraPosition = camera.position
camera.getWorldDirection(tmpVec)
tmpVec.add(currentCameraPosition)
const highlightedWallIds = new Set(
[...selectedIds, ...previewSelectedIds].filter(
(id) => useScene.getState().nodes[id as AnyNodeId]?.type === 'wall',
),
)
const deleteHoveredWallId =
hoverHighlightMode === 'delete' &&
hoveredId &&
useScene.getState().nodes[hoveredId as AnyNodeId]?.type === 'wall'
? hoveredId
: null
const highlightKey = `${Array.from(highlightedWallIds).sort().join('|')}::${deleteHoveredWallId ?? ''}`
const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current)
const directionChanged = tmpVec.distanceTo(lastCameraTarget.current)
const timeSinceUpdate = currentTime - lastUpdateTime.current
if (
((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
lastWallMode.current !== wallMode ||
lastShading.current !== shading ||
lastTextures.current !== textures ||
lastColorPreset.current !== colorPreset ||
lastSceneTheme.current !== sceneTheme ||
sceneRegistry.byType.wall!.size !== lastNumberOfWalls.current ||
lastHighlightKey.current !== highlightKey
) {
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
const hideWall = getWallHideState(wallNode, wallMesh as Mesh, wallMode, u)
const isDeleteHighlighted = deleteHoveredWallId === wallId
const isSelectionHighlighted = !isDeleteHighlighted && highlightedWallIds.has(wallId)
const materials = getMaterialsForWall(
wallNode,
shading,
textures,
colorPreset,
sceneTheme,
useScene.getState().materials,
)
if (hideWall) {
;(wallMesh as Mesh).material = isDeleteHighlighted
? materials.deleteInvisible
: isSelectionHighlighted
? getSelectionHighlightMaterials(materials.invisible)
: materials.invisible
} else {
;(wallMesh as Mesh).material = isDeleteHighlighted
? materials.deleteVisible
: isSelectionHighlighted
? getSelectionHighlightMaterials(materials.visible)
: materials.visible
}
})
lastWallMode.current = wallMode
lastShading.current = shading
lastTextures.current = textures
lastColorPreset.current = colorPreset
lastSceneTheme.current = sceneTheme
lastNumberOfWalls.current = sceneRegistry.byType.wall!.size
lastHighlightKey.current = highlightKey
}
})
useEffect(() => {
const snapshot = new Map<Mesh, Material | Material[]>()
const restoreForCapture = () => {
sceneRegistry.byType.wall!.forEach((wallId) => {
const wallMesh = sceneRegistry.nodes.get(wallId) as Mesh | undefined
if (!wallMesh) return
const wallNode = useScene.getState().nodes[wallId as AnyNodeId] as WallNode | undefined
if (!wallNode || wallNode.type !== 'wall') return
const mats = getMaterialsForWall(
wallNode,
useViewer.getState().shading,
useViewer.getState().textures,
useViewer.getState().colorPreset,
useViewer.getState().sceneTheme,
useScene.getState().materials,
)
const current = wallMesh.material as Material | Material[]
snapshot.set(wallMesh, current)
if (current === mats.deleteVisible) {
wallMesh.material = mats.visible
} else if (current === mats.deleteInvisible) {
wallMesh.material = mats.invisible
}
})
}
const reapplyAfterCapture = () => {
snapshot.forEach((mat, mesh) => {
mesh.material = mat
})
snapshot.clear()
}
emitter.on('thumbnail:before-capture', restoreForCapture)
emitter.on('thumbnail:after-capture', reapplyAfterCapture)
return () => {
emitter.off('thumbnail:before-capture', restoreForCapture)
emitter.off('thumbnail:after-capture', reapplyAfterCapture)
}
}, [])
return null
}