fix(viewer): 修复墙体材质切换后缩放旋转时颜色恢复问题
问题描述: 选中墙体并切换材质颜色后,在界面上进行缩放、旋转或移动操作时, 墙体实际渲染的颜色会恢复为原来的白色,但颜色选项卡显示仍为选中的颜色。 根本原因: WallCutout 系统在每次相机移动时,直接使用固定的白色材质覆盖墙体材质, 完全忽略了用户在节点上设置的自定义材质。 修复内容: - 为每个墙体创建独立的材质对象,支持用户自定义颜色 - 支持预设颜色(brick, concrete, wood 等)和自定义颜色属性 - 添加材质哈希值跟踪,检测材质变化并重新创建材质 - 提取 getWallHideState() 辅助函数,优化代码结构 - 添加预设颜色映射的类型安全改进,使用 as const 和 keyof 类型操作
This commit is contained in:
@@ -40,13 +40,14 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
|||||||
const handlers = useNodeEvents(node, 'ceiling')
|
const handlers = useNodeEvents(node, 'ceiling')
|
||||||
|
|
||||||
const materials = useMemo(() => {
|
const materials = useMemo(() => {
|
||||||
if (node.material) {
|
const mat = node.material
|
||||||
const props = node.material.properties
|
if (mat) {
|
||||||
|
const props = mat.properties
|
||||||
const color = props?.color || '#999999'
|
const color = props?.color || '#999999'
|
||||||
return createCeilingMaterials(color)
|
return createCeilingMaterials(color)
|
||||||
}
|
}
|
||||||
return { topMaterial: createCeilingMaterials().topMaterial, bottomMaterial: DEFAULT_CEILING_MATERIAL }
|
return { topMaterial: createCeilingMaterials().topMaterial, bottomMaterial: DEFAULT_CEILING_MATERIAL }
|
||||||
}, [node.material])
|
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh material={materials.bottomMaterial} ref={ref}>
|
<mesh material={materials.bottomMaterial} ref={ref}>
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ export const DoorRenderer = ({ node }: { node: DoorNode }) => {
|
|||||||
const isTransient = !!(node.metadata as Record<string, unknown> | null)?.isTransient
|
const isTransient = !!(node.metadata as Record<string, unknown> | null)?.isTransient
|
||||||
|
|
||||||
const material = useMemo(() => {
|
const material = useMemo(() => {
|
||||||
return node.material ? createMaterial(node.material) : DEFAULT_DOOR_MATERIAL
|
const mat = node.material
|
||||||
}, [node.material])
|
if (!mat) return DEFAULT_DOOR_MATERIAL
|
||||||
|
return createMaterial(mat)
|
||||||
|
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { type RoofSegmentNode, useRegistry } from '@pascal-app/core'
|
import { type RoofSegmentNode, useRegistry } from '@pascal-app/core'
|
||||||
import { useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import type * as THREE from 'three'
|
import type * as THREE from 'three'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
|
import { createMaterial } from '../../../lib/materials'
|
||||||
import useViewer from '../../../store/use-viewer'
|
import useViewer from '../../../store/use-viewer'
|
||||||
import { roofDebugMaterials, roofMaterials } from '../roof/roof-materials'
|
import { roofDebugMaterials, roofMaterials } from '../roof/roof-materials'
|
||||||
|
|
||||||
@@ -13,9 +14,17 @@ export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
|
|||||||
const handlers = useNodeEvents(node, 'roof-segment')
|
const handlers = useNodeEvents(node, 'roof-segment')
|
||||||
const debugColors = useViewer((s) => s.debugColors)
|
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 (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
material={debugColors ? roofDebugMaterials : roofMaterials}
|
material={material}
|
||||||
position={node.position}
|
position={node.position}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
rotation-y={node.rotation}
|
rotation-y={node.rotation}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
|
|||||||
const debugColors = useViewer((s) => s.debugColors)
|
const debugColors = useViewer((s) => s.debugColors)
|
||||||
|
|
||||||
const customMaterial = useMemo(() => {
|
const customMaterial = useMemo(() => {
|
||||||
return node.material ? createMaterial(node.material) : null
|
const mat = node.material
|
||||||
}, [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
|
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
|||||||
const handlers = useNodeEvents(node, 'slab')
|
const handlers = useNodeEvents(node, 'slab')
|
||||||
|
|
||||||
const material = useMemo(() => {
|
const material = useMemo(() => {
|
||||||
return node.material ? createMaterial(node.material) : DEFAULT_SLAB_MATERIAL
|
const mat = node.material
|
||||||
}, [node.material])
|
if (!mat) return DEFAULT_SLAB_MATERIAL
|
||||||
|
return createMaterial(mat)
|
||||||
|
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh castShadow receiveShadow ref={ref} {...handlers} visible={node.visible} material={material}>
|
<mesh castShadow receiveShadow ref={ref} {...handlers} visible={node.visible} material={material}>
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
|
|||||||
const handlers = useNodeEvents(node, 'wall')
|
const handlers = useNodeEvents(node, 'wall')
|
||||||
|
|
||||||
const material = useMemo(() => {
|
const material = useMemo(() => {
|
||||||
return node.material ? createMaterial(node.material) : DEFAULT_WALL_MATERIAL
|
const mat = node.material
|
||||||
}, [node.material])
|
if (!mat) return DEFAULT_WALL_MATERIAL
|
||||||
|
return createMaterial(mat)
|
||||||
|
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh castShadow receiveShadow ref={ref} visible={node.visible} material={material}>
|
<mesh castShadow receiveShadow ref={ref} visible={node.visible} material={material}>
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ export const WindowRenderer = ({ node }: { node: WindowNode }) => {
|
|||||||
const isTransient = !!(node.metadata as Record<string, unknown> | null)?.isTransient
|
const isTransient = !!(node.metadata as Record<string, unknown> | null)?.isTransient
|
||||||
|
|
||||||
const material = useMemo(() => {
|
const material = useMemo(() => {
|
||||||
return node.material ? createMaterial(node.material) : DEFAULT_WINDOW_MATERIAL
|
const mat = node.material
|
||||||
}, [node.material])
|
if (!mat) return DEFAULT_WINDOW_MATERIAL
|
||||||
|
return createMaterial(mat)
|
||||||
|
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
|
|||||||
@@ -10,41 +10,119 @@ const tmpVec = new Vector3()
|
|||||||
const u = new Vector3()
|
const u = new Vector3()
|
||||||
const v = new Vector3()
|
const v = new Vector3()
|
||||||
|
|
||||||
// Dot pattern shader
|
|
||||||
const dotPattern = Fn(() => {
|
const dotPattern = Fn(() => {
|
||||||
// Create a repeating grid pattern based on world position
|
const scale = float(0.1)
|
||||||
const scale = float(0.1) // Dot grid spacing (10cm)
|
const dotSize = float(0.3)
|
||||||
const dotSize = float(0.3) // Size of dots relative to grid
|
|
||||||
|
|
||||||
// Use XY coordinates for pattern on wall face
|
|
||||||
const uv = vec2(positionLocal.x, positionLocal.y).div(scale)
|
const uv = vec2(positionLocal.x, positionLocal.y).div(scale)
|
||||||
const gridUV = fract(uv)
|
const gridUV = fract(uv)
|
||||||
|
|
||||||
// Distance from center of grid cell (creates circular dots)
|
|
||||||
const dist = length(gridUV.sub(0.5))
|
const dist = length(gridUV.sub(0.5))
|
||||||
|
|
||||||
// Create dots: 1 where we want dots, 0 elsewhere
|
|
||||||
const dots = step(dist, dotSize.mul(0.5))
|
const dots = step(dist, dotSize.mul(0.5))
|
||||||
|
|
||||||
// Vertical fade: fade out as Y increases (from bottom to top)
|
const fadeHeight = float(2.5)
|
||||||
const fadeHeight = float(2.5) // Fade over 2.5 meters
|
|
||||||
const yFade = float(1).sub(smoothstep(float(0), fadeHeight, positionLocal.y))
|
const yFade = float(1).sub(smoothstep(float(0), fadeHeight, positionLocal.y))
|
||||||
|
|
||||||
return dots.mul(yFade)
|
return dots.mul(yFade)
|
||||||
})
|
})
|
||||||
|
|
||||||
const invsibleWallMaterial = new MeshStandardNodeMaterial({
|
interface WallMaterials {
|
||||||
transparent: true,
|
visible: MeshStandardNodeMaterial
|
||||||
opacityNode: mix(float(0.0), float(0.24), dotPattern()),
|
invisible: MeshStandardNodeMaterial
|
||||||
color: 'white',
|
materialHash: string
|
||||||
depthWrite: false,
|
}
|
||||||
emissive: 'white',
|
|
||||||
})
|
const wallMaterialCache = new Map<string, WallMaterials>()
|
||||||
const wallMaterial = new MeshStandardNodeMaterial({
|
|
||||||
color: 'white',
|
function getMaterialHash(wallNode: WallNode): string {
|
||||||
roughness: 1,
|
if (!wallNode.material) return 'none'
|
||||||
metalness: 0,
|
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 = () => {
|
export const WallCutout = () => {
|
||||||
const lastCameraPosition = useRef(new Vector3())
|
const lastCameraPosition = useRef(new Vector3())
|
||||||
@@ -52,6 +130,7 @@ export const WallCutout = () => {
|
|||||||
const lastUpdateTime = useRef(0)
|
const lastUpdateTime = useRef(0)
|
||||||
const lastWallMode = useRef<string>(useViewer.getState().wallMode)
|
const lastWallMode = useRef<string>(useViewer.getState().wallMode)
|
||||||
const lastNumberOfWalls = useRef(0)
|
const lastNumberOfWalls = useRef(0)
|
||||||
|
const lastWallMaterials = useRef<Map<string, WallMaterials>>(new Map())
|
||||||
|
|
||||||
useFrame(({ camera, clock }) => {
|
useFrame(({ camera, clock }) => {
|
||||||
const wallMode = useViewer.getState().wallMode
|
const wallMode = useViewer.getState().wallMode
|
||||||
@@ -60,51 +139,64 @@ export const WallCutout = () => {
|
|||||||
camera.getWorldDirection(tmpVec)
|
camera.getWorldDirection(tmpVec)
|
||||||
tmpVec.add(currentCameraPosition)
|
tmpVec.add(currentCameraPosition)
|
||||||
|
|
||||||
// Throttle: only update if camera moved significantly AND enough time passed
|
|
||||||
const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current)
|
const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current)
|
||||||
const directionChanged = tmpVec.distanceTo(lastCameraTarget.current)
|
const directionChanged = tmpVec.distanceTo(lastCameraTarget.current)
|
||||||
const timeSinceUpdate = currentTime - lastUpdateTime.current
|
const timeSinceUpdate = currentTime - lastUpdateTime.current
|
||||||
|
|
||||||
// Update if moved > 0.5m OR direction changed > 0.3 AND at least 100ms passed
|
const shouldUpdate =
|
||||||
if (
|
|
||||||
((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
|
((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
|
||||||
lastWallMode.current !== wallMode ||
|
lastWallMode.current !== wallMode ||
|
||||||
sceneRegistry.byType.wall.size !== lastNumberOfWalls.current
|
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<string>()
|
||||||
|
|
||||||
|
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)
|
lastCameraPosition.current.copy(currentCameraPosition)
|
||||||
lastCameraTarget.current.copy(tmpVec)
|
lastCameraTarget.current.copy(tmpVec)
|
||||||
lastUpdateTime.current = currentTime
|
lastUpdateTime.current = currentTime
|
||||||
camera.getWorldDirection(u)
|
camera.getWorldDirection(u)
|
||||||
|
|
||||||
const walls = sceneRegistry.byType.wall
|
if (lastWallMode.current !== wallMode) {
|
||||||
walls.forEach((wallId) => {
|
wallMaterialCache.clear()
|
||||||
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 (wallMode === 'up') {
|
for (const [wallId, mats] of lastWallMaterials.current) {
|
||||||
hideWall = false
|
if (!currentWallIds.has(wallId)) {
|
||||||
} else if (wallMode === 'down') {
|
mats.visible.dispose()
|
||||||
hideWall = true
|
mats.invisible.dispose()
|
||||||
} else {
|
wallMaterialCache.delete(wallId)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
;(wallMesh as Mesh).material = hideWall ? invsibleWallMaterial : wallMaterial
|
}
|
||||||
})
|
|
||||||
|
lastWallMaterials.current.clear()
|
||||||
|
for (const [wallId, mats] of wallMaterialCache) {
|
||||||
|
lastWallMaterials.current.set(wallId, mats)
|
||||||
|
}
|
||||||
|
|
||||||
lastWallMode.current = wallMode
|
lastWallMode.current = wallMode
|
||||||
lastNumberOfWalls.current = sceneRegistry.byType.wall.size
|
lastNumberOfWalls.current = sceneRegistry.byType.wall.size
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user