fix(viewer): 修复墙体材质切换后缩放旋转时颜色恢复问题
问题描述: 选中墙体并切换材质颜色后,在界面上进行缩放、旋转或移动操作时, 墙体实际渲染的颜色会恢复为原来的白色,但颜色选项卡显示仍为选中的颜色。 根本原因: WallCutout 系统在每次相机移动时,直接使用固定的白色材质覆盖墙体材质, 完全忽略了用户在节点上设置的自定义材质。 修复内容: - 为每个墙体创建独立的材质对象,支持用户自定义颜色 - 支持预设颜色(brick, concrete, wood 等)和自定义颜色属性 - 添加材质哈希值跟踪,检测材质变化并重新创建材质 - 提取 getWallHideState() 辅助函数,优化代码结构 - 添加预设颜色映射的类型安全改进,使用 as const 和 keyof 类型操作
This commit is contained in:
@@ -10,41 +10,119 @@ const tmpVec = new Vector3()
|
||||
const u = new Vector3()
|
||||
const v = new Vector3()
|
||||
|
||||
// Dot pattern shader
|
||||
const dotPattern = Fn(() => {
|
||||
// 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<string, WallMaterials>()
|
||||
|
||||
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<string>(useViewer.getState().wallMode)
|
||||
const lastNumberOfWalls = useRef(0)
|
||||
const lastWallMaterials = useRef<Map<string, WallMaterials>>(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<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)
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user