feat: Add catalog-based material presets across structural nodes (#231)

* Add catalog-based material presets across structural nodes

* Fix material picker fallback and wall visible highlights

* Preserve wall materials in selection highlights
This commit is contained in:
Sudhir Yadav
2026-04-15 12:16:35 -04:00
committed by GitHub
parent f25806ee8f
commit 57df224948
67 changed files with 1458 additions and 151 deletions
@@ -1,4 +1,4 @@
import { type CeilingNode, resolveMaterial, useRegistry } from '@pascal-app/core'
import { type CeilingNode, getMaterialPresetByRef, resolveMaterial, useRegistry } from '@pascal-app/core'
import { useMemo, useRef } from 'react'
import { float, mix, positionWorld, smoothstep } from 'three/tsl'
import { BackSide, FrontSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
@@ -39,10 +39,11 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
const handlers = useNodeEvents(node, 'ceiling')
const materials = useMemo(() => {
const props = resolveMaterial(node.material)
const preset = getMaterialPresetByRef(node.materialPreset)
const props = preset?.mapProperties ?? resolveMaterial(node.material)
const color = props.color || '#999999'
return createCeilingMaterials(color)
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
}, [node.materialPreset, node.material, node.material?.preset, node.material?.properties, node.material?.texture])
return (
<mesh material={materials.bottomMaterial} ref={ref}>
@@ -2,12 +2,28 @@ import { type FenceNode, useRegistry, useScene } from '@pascal-app/core'
import { useLayoutEffect, useMemo, useRef } from 'react'
import type { Mesh } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
import {
createMaterial,
createMaterialFromPresetRef,
DEFAULT_STAIR_MATERIAL,
} from '../../../lib/materials'
export const FenceRenderer = ({ node }: { node: FenceNode }) => {
const ref = useRef<Mesh>(null!)
const handlers = useNodeEvents(node, 'fence')
const material = useMemo(() => DEFAULT_STAIR_MATERIAL, [])
const material = useMemo(() => {
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial
const mat = node.material
if (!mat) return DEFAULT_STAIR_MATERIAL
return createMaterial(mat)
}, [
node.materialPreset,
node.material,
node.material?.preset,
node.material?.properties,
node.material?.texture,
])
useRegistry(node.id, 'fence', ref)
useLayoutEffect(() => {
@@ -1,24 +1,43 @@
import { type RoofSegmentNode, useRegistry } from '@pascal-app/core'
import { type AnyNodeId, type RoofNode, type RoofSegmentNode, useRegistry, useScene } from '@pascal-app/core'
import { useMemo, useRef } from 'react'
import type * as THREE from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { createMaterial } from '../../../lib/materials'
import { createMaterial, createMaterialFromPresetRef } from '../../../lib/materials'
import useViewer from '../../../store/use-viewer'
import { roofDebugMaterials, roofMaterials } from '../roof/roof-materials'
export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
const ref = useRef<THREE.Mesh>(null!)
const nodes = useScene((state) => state.nodes)
useRegistry(node.id, 'roof-segment', ref)
const handlers = useNodeEvents(node, 'roof-segment')
const debugColors = useViewer((s) => s.debugColors)
const parentNode =
node.parentId ? (nodes[node.parentId as AnyNodeId] as RoofNode | undefined) : undefined
const customMaterial = useMemo(() => {
const mat = node.material
const effectiveMaterialPreset = node.materialPreset ?? parentNode?.materialPreset
const effectiveMaterial = node.material ?? parentNode?.material
const presetMaterial = createMaterialFromPresetRef(effectiveMaterialPreset)
if (presetMaterial) return presetMaterial
const mat = effectiveMaterial
if (!mat) return null
return createMaterial(mat)
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
}, [
node.materialPreset,
node.material,
node.material?.preset,
node.material?.properties,
node.material?.texture,
parentNode?.materialPreset,
parentNode?.material,
parentNode?.material?.preset,
parentNode?.material?.properties,
parentNode?.material?.texture,
])
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
@@ -2,7 +2,7 @@ import { type RoofNode, useRegistry } from '@pascal-app/core'
import { useMemo, useRef } from 'react'
import type * as THREE from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { createMaterial } from '../../../lib/materials'
import { createMaterial, createMaterialFromPresetRef } from '../../../lib/materials'
import useViewer from '../../../store/use-viewer'
import { NodeRenderer } from '../node-renderer'
import { roofDebugMaterials, roofMaterials } from './roof-materials'
@@ -16,10 +16,12 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
const debugColors = useViewer((s) => s.debugColors)
const customMaterial = useMemo(() => {
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial
const mat = node.material
if (!mat) return null
return createMaterial(mat)
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
}, [node.materialPreset, node.material, node.material?.preset, node.material?.properties, node.material?.texture])
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
@@ -2,7 +2,11 @@ import { type SlabNode, useRegistry } from '@pascal-app/core'
import { useMemo, useRef } from 'react'
import type { Mesh } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { createMaterial, DEFAULT_SLAB_MATERIAL } from '../../../lib/materials'
import {
createMaterial,
createMaterialFromPresetRef,
DEFAULT_SLAB_MATERIAL,
} from '../../../lib/materials'
export const SlabRenderer = ({ node }: { node: SlabNode }) => {
const ref = useRef<Mesh>(null!)
@@ -12,10 +16,18 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
const handlers = useNodeEvents(node, 'slab')
const material = useMemo(() => {
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial
const mat = node.material
if (!mat) return DEFAULT_SLAB_MATERIAL
return createMaterial(mat)
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
}, [
node.material,
node.material?.preset,
node.material?.properties,
node.material?.texture,
node.materialPreset,
])
return (
<mesh
@@ -1,11 +1,12 @@
import { type StairSegmentNode, useRegistry, useScene } from '@pascal-app/core'
import { type AnyNodeId, type StairNode, type StairSegmentNode, useRegistry, useScene } from '@pascal-app/core'
import { useLayoutEffect, useMemo, useRef } from 'react'
import type * as THREE from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { createMaterial, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
import { createMaterial, createMaterialFromPresetRef, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => {
const ref = useRef<THREE.Mesh>(null!)
const nodes = useScene((state) => state.nodes)
useRegistry(node.id, 'stair-segment', ref)
@@ -14,12 +15,30 @@ export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => {
}, [node.id])
const handlers = useNodeEvents(node, 'stair-segment')
const parentNode =
node.parentId ? (nodes[node.parentId as AnyNodeId] as StairNode | undefined) : undefined
const material = useMemo(() => {
const mat = node.material
const effectiveMaterialPreset = node.materialPreset ?? parentNode?.materialPreset
const effectiveMaterial = node.material ?? parentNode?.material
const presetMaterial = createMaterialFromPresetRef(effectiveMaterialPreset)
if (presetMaterial) return presetMaterial
const mat = effectiveMaterial
if (!mat) return DEFAULT_STAIR_MATERIAL
return createMaterial(mat)
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
}, [
node.materialPreset,
node.material,
node.material?.preset,
node.material?.properties,
node.material?.texture,
parentNode?.materialPreset,
parentNode?.material,
parentNode?.material?.preset,
parentNode?.material?.properties,
parentNode?.material?.texture,
])
return (
<mesh
@@ -2,7 +2,7 @@ import { type AnyNodeId, type StairNode, type StairSegmentNode, useRegistry, use
import { useLayoutEffect, useMemo, useRef } from 'react'
import * as THREE from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { createMaterial, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
import { createMaterial, createMaterialFromPresetRef, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
import { NodeRenderer } from '../node-renderer'
type SegmentTransform = {
@@ -47,10 +47,12 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
const handlers = useNodeEvents(node, 'stair')
const material = useMemo(() => {
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial
const mat = node.material
if (!mat) return DEFAULT_STAIR_MATERIAL
return createMaterial(mat)
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
}, [node.materialPreset, node.material, node.material?.preset, node.material?.properties, node.material?.texture])
return (
<group
@@ -2,7 +2,11 @@ import { useRegistry, useScene, type WallNode } from '@pascal-app/core'
import { useLayoutEffect, useMemo, useRef } from 'react'
import type { Mesh } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
import { createMaterial, DEFAULT_WALL_MATERIAL } from '../../../lib/materials'
import {
createMaterial,
createMaterialFromPresetRef,
DEFAULT_WALL_MATERIAL,
} from '../../../lib/materials'
import { NodeRenderer } from '../node-renderer'
export const WallRenderer = ({ node }: { node: WallNode }) => {
@@ -17,10 +21,18 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
const handlers = useNodeEvents(node, 'wall')
const material = useMemo(() => {
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial
const mat = node.material
if (!mat) return DEFAULT_WALL_MATERIAL
return createMaterial(mat)
}, [node.material, node.material?.preset, node.material?.properties, node.material?.texture])
}, [
node.material,
node.material?.preset,
node.material?.properties,
node.material?.texture,
node.materialPreset,
])
return (
<mesh castShadow material={material} receiveShadow ref={ref} visible={node.visible}>
@@ -310,6 +310,10 @@ const PostProcessingPasses = () => {
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
bgUniform.current.value.copy(bgCurrent.current)
if (!isInitialized) {
return
}
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
try {
if ((renderer as any).setClearAlpha) {
@@ -281,6 +281,10 @@ export const SelectionManager = () => {
if (!strategy) return
if (strategy.isValid(event.node)) {
event.stopPropagation()
if (event.node.type === 'slab') {
useViewer.setState({ hoveredId: null })
return
}
useViewer.setState({ hoveredId: event.node.id })
}
}
@@ -388,11 +392,14 @@ const OutlinerSync = () => {
const selection = useViewer((s) => s.selection)
const hoveredId = useViewer((s) => s.hoveredId)
const outliner = useViewer((s) => s.outliner)
const nodes = useScene((s) => s.nodes)
useEffect(() => {
// Sync selected objects
outliner.selectedObjects.length = 0
for (const id of selection.selectedIds) {
const node = nodes[id as AnyNodeId]
if (node?.type === 'slab') continue
const obj = sceneRegistry.nodes.get(id)
if (obj) outliner.selectedObjects.push(obj)
}
@@ -400,10 +407,12 @@ const OutlinerSync = () => {
// Sync hovered objects
outliner.hoveredObjects.length = 0
if (hoveredId) {
const hoveredNode = nodes[hoveredId as AnyNodeId]
if (hoveredNode?.type === 'slab') return
const obj = sceneRegistry.nodes.get(hoveredId)
if (obj) outliner.hoveredObjects.push(obj)
}
}, [selection, hoveredId, outliner])
}, [selection, hoveredId, outliner, nodes])
return null
}
+246 -4
View File
@@ -1,4 +1,11 @@
import { type MaterialProperties, type MaterialSchema, resolveMaterial } from '@pascal-app/core'
import {
type MaterialMapProperties,
type MaterialPresetPayload,
type MaterialProperties,
type MaterialSchema,
getMaterialPresetByRef,
resolveMaterial,
} from '@pascal-app/core'
import * as THREE from 'three'
const sideMap: Record<MaterialProperties['side'], THREE.Side> = {
@@ -8,19 +15,247 @@ const sideMap: Record<MaterialProperties['side'], THREE.Side> = {
}
const materialCache = new Map<string, THREE.MeshStandardMaterial>()
const textureCache = new Map<string, THREE.Texture>()
const textureLoadPromises = new Map<string, Promise<THREE.Texture | null>>()
const textureLoader = new THREE.TextureLoader()
const wrapMap = {
Repeat: THREE.RepeatWrapping,
ClampToEdge: THREE.ClampToEdgeWrapping,
MirroredRepeat: THREE.MirroredRepeatWrapping,
} as const
type StandardMaterial = THREE.MeshStandardMaterial | THREE.MeshPhysicalMaterial
type TextureSlot =
| 'map'
| 'normalMap'
| 'roughnessMap'
| 'metalnessMap'
| 'displacementMap'
| 'aoMap'
| 'bumpMap'
| 'alphaMap'
| 'lightMap'
| 'emissiveMap'
const SRGB_TEXTURE_SLOTS: TextureSlot[] = ['map', 'emissiveMap']
function getCacheKey(props: MaterialProperties): string {
return `${props.color}-${props.roughness}-${props.metalness}-${props.opacity}-${props.transparent}-${props.side}`
}
export function createMaterial(material?: MaterialSchema): THREE.MeshStandardMaterial {
const props = resolveMaterial(material)
const cacheKey = getCacheKey(props)
function getTextureKey(material?: MaterialSchema): string {
const texture = material?.texture
if (!texture) return 'none'
const repeat = texture.repeat?.join('x') ?? 'default'
const scale = texture.scale ?? 'default'
return `${texture.url}-${repeat}-${scale}`
}
function getTexture(material?: MaterialSchema): THREE.Texture | undefined {
const textureConfig = material?.texture
if (!textureConfig?.url) return undefined
const cacheKey = getTextureKey(material)
const cached = textureCache.get(cacheKey)
if (cached) return cached
const texture = textureLoader.load(textureConfig.url)
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
const repeatX = textureConfig.repeat?.[0] ?? textureConfig.scale ?? 1
const repeatY = textureConfig.repeat?.[1] ?? textureConfig.scale ?? 1
texture.repeat.set(repeatX, repeatY)
texture.colorSpace = THREE.SRGBColorSpace
textureCache.set(cacheKey, texture)
return texture
}
function isStandardMaterial(material: THREE.Material): material is StandardMaterial {
return (
material instanceof THREE.MeshStandardMaterial ||
material instanceof THREE.MeshPhysicalMaterial
)
}
function applyTextureProperties(
texture: THREE.Texture,
props: MaterialMapProperties,
slot?: TextureSlot,
): THREE.Texture {
texture.wrapS = wrapMap[props.wrapS]
texture.wrapT = wrapMap[props.wrapT]
texture.repeat.set(props.repeatX, props.repeatY)
texture.rotation = props.rotation
texture.flipY = props.flipY
texture.colorSpace = SRGB_TEXTURE_SLOTS.includes(slot ?? 'map')
? THREE.SRGBColorSpace
: THREE.NoColorSpace
texture.needsUpdate = true
return texture
}
function getPresetTextureCacheKey(path: string, props: MaterialMapProperties, slot?: TextureSlot): string {
return `${path}-${props.repeatX}-${props.repeatY}-${props.rotation}-${props.wrapS}-${props.wrapT}-${props.flipY}-${slot ?? 'map'}`
}
function getPresetTexture(path: string, props: MaterialMapProperties, slot?: TextureSlot): THREE.Texture {
const cacheKey = getPresetTextureCacheKey(path, props, slot)
const cached = textureCache.get(cacheKey)
if (cached) return cached
const texture = textureLoader.load(path)
applyTextureProperties(texture, props, slot)
textureCache.set(cacheKey, texture)
return texture
}
async function loadPresetTexture(
path: string,
props: MaterialMapProperties,
slot?: TextureSlot,
): Promise<THREE.Texture | null> {
const cacheKey = getPresetTextureCacheKey(path, props, slot)
const cached = textureCache.get(cacheKey)
if (cached) return cached
const existingPromise = textureLoadPromises.get(cacheKey)
if (existingPromise) return existingPromise
const promise = textureLoader
.loadAsync(path)
.then((texture) => {
applyTextureProperties(texture, props, slot)
textureCache.set(cacheKey, texture)
textureLoadPromises.delete(cacheKey)
return texture
})
.catch((error) => {
console.warn('[viewer] Failed to load material texture', path, error)
textureLoadPromises.delete(cacheKey)
return null
})
textureLoadPromises.set(cacheKey, promise)
return promise
}
function queueTextureAssignment(
material: StandardMaterial,
slot: TextureSlot,
path: string | undefined,
props: MaterialMapProperties,
) {
if (!path) {
material[slot] = null
return
}
const cacheKey = getPresetTextureCacheKey(path, props, slot)
const cached = textureCache.get(cacheKey)
if (cached) {
material[slot] = cached
return
}
material[slot] = null
void loadPresetTexture(path, props, slot).then((texture) => {
if (!texture) return
material[slot] = texture
material.needsUpdate = true
})
}
function applyMaterialMapProperties(material: StandardMaterial, mapProperties: MaterialMapProperties) {
material.color.set(mapProperties.color)
material.roughness = mapProperties.roughness
material.metalness = mapProperties.metalness
material.emissiveIntensity = mapProperties.emissiveIntensity
material.emissive.set(mapProperties.emissiveColor)
material.displacementScale = mapProperties.displacementScale
material.bumpScale = mapProperties.bumpScale
material.aoMapIntensity = mapProperties.aoMapIntensity
material.lightMapIntensity = mapProperties.lightMapIntensity
material.transparent = mapProperties.transparent
material.opacity = mapProperties.opacity
material.side =
mapProperties.side === 0
? THREE.FrontSide
: mapProperties.side === 1
? THREE.BackSide
: THREE.DoubleSide
material.normalScale.set(mapProperties.normalScaleX, mapProperties.normalScaleY)
material.needsUpdate = true
}
function applyMaterialPresetTextures(
material: StandardMaterial,
preset: MaterialPresetPayload,
) {
const { maps, mapProperties } = preset
queueTextureAssignment(material, 'map', maps.albedoMap, mapProperties)
queueTextureAssignment(material, 'normalMap', maps.normalMap, mapProperties)
queueTextureAssignment(material, 'roughnessMap', maps.roughnessMap, mapProperties)
queueTextureAssignment(material, 'metalnessMap', maps.metalnessMap, mapProperties)
queueTextureAssignment(material, 'displacementMap', maps.displacementMap, mapProperties)
queueTextureAssignment(material, 'aoMap', maps.aoMap, mapProperties)
queueTextureAssignment(material, 'bumpMap', maps.bumpMap, mapProperties)
queueTextureAssignment(material, 'alphaMap', maps.alphaMap, mapProperties)
queueTextureAssignment(material, 'lightMap', maps.lightMap, mapProperties)
queueTextureAssignment(material, 'emissiveMap', maps.emissiveMap, mapProperties)
material.needsUpdate = true
}
export function applyMaterialPresetToMaterials(
materialInput: THREE.Material | THREE.Material[],
preset: MaterialPresetPayload | null | undefined,
) {
if (!preset) return
const materials = (Array.isArray(materialInput) ? materialInput : [materialInput]).filter(
isStandardMaterial,
)
if (materials.length === 0) return
for (const material of materials) {
applyMaterialMapProperties(material, preset.mapProperties)
applyMaterialPresetTextures(material, preset)
}
}
export function createMaterialFromPreset(preset: MaterialPresetPayload): THREE.MeshStandardMaterial {
const cacheKey = JSON.stringify(preset)
if (materialCache.has(cacheKey)) {
return materialCache.get(cacheKey)!
}
const material = new THREE.MeshStandardMaterial()
applyMaterialPresetToMaterials(material, preset)
materialCache.set(cacheKey, material)
return material
}
export function createMaterialFromPresetRef(materialPreset?: string): THREE.MeshStandardMaterial | null {
const preset = getMaterialPresetByRef(materialPreset)
if (!preset) return null
return createMaterialFromPreset(preset)
}
export function createMaterial(material?: MaterialSchema): THREE.MeshStandardMaterial {
const props = resolveMaterial(material)
const cacheKey = `${getCacheKey(props)}-${getTextureKey(material)}`
if (materialCache.has(cacheKey)) {
return materialCache.get(cacheKey)!
}
const map = getTexture(material)
const threeMaterial = new THREE.MeshStandardMaterial({
color: props.color,
roughness: props.roughness,
@@ -28,6 +263,7 @@ export function createMaterial(material?: MaterialSchema): THREE.MeshStandardMat
opacity: props.opacity,
transparent: props.transparent,
side: sideMap[props.side],
map,
})
materialCache.set(cacheKey, threeMaterial)
@@ -70,4 +306,10 @@ export function clearMaterialCache(): void {
material.dispose()
}
materialCache.clear()
for (const texture of textureCache.values()) {
texture.dispose()
}
textureCache.clear()
textureLoadPromises.clear()
}
@@ -2,6 +2,7 @@ import {
type AnyNodeId,
baseMaterial,
emitter,
getMaterialPresetByRef,
sceneRegistry,
useScene,
type WallNode,
@@ -13,6 +14,7 @@ import { Color } from 'three'
import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl'
import { type Mesh, MeshStandardNodeMaterial, Vector3 } from 'three/webgpu'
import useViewer from '../../store/use-viewer'
import { createMaterial, createMaterialFromPresetRef } from '../../lib/materials'
const tmpVec = new Vector3()
const u = new Vector3()
@@ -21,12 +23,14 @@ const DEFAULT_WALL_COLOR = '#f2f0ed'
const WALL_HIGHLIGHT_PROFILES = {
delete: {
color: new Color('#dc2626'),
blend: 0.78,
blend: 0.76,
emissiveBlend: 0.92,
emissiveIntensity: 0.46,
},
selection: {
color: new Color('#818cf8'),
blend: 0.32,
emissiveBlend: 0.7,
emissiveIntensity: 0.42,
},
} as const
@@ -51,11 +55,11 @@ const dotPattern = Fn(() => {
})
interface WallMaterials {
visible: MeshStandardNodeMaterial
visible: Material
invisible: MeshStandardNodeMaterial
deleteVisible: MeshStandardNodeMaterial
deleteVisible: Material
deleteInvisible: MeshStandardNodeMaterial
highlightedVisible: MeshStandardNodeMaterial
highlightedVisible: Material
highlightedInvisible: MeshStandardNodeMaterial
materialHash: string
}
@@ -75,6 +79,7 @@ const presetColors = {
} as const
function getMaterialHash(wallNode: WallNode): string {
if (wallNode.materialPreset) return `preset-ref-${wallNode.materialPreset}`
if (!wallNode.material) return 'none'
const mat = wallNode.material
if (mat.preset && mat.preset !== 'custom') {
@@ -90,27 +95,54 @@ function getPresetColor(preset: string): string {
return presetColors[preset as keyof typeof presetColors] ?? '#ffffff'
}
function getHighlightedColor(color: string, kind: WallHighlightKind): Color {
function getHighlightedColor(color: Color, kind: WallHighlightKind): Color {
const profile = WALL_HIGHLIGHT_PROFILES[kind]
return new Color(color).lerp(profile.color, profile.blend)
return color.clone().lerp(profile.color, profile.blend)
}
function createHighlightedWallMaterial(
material: MeshStandardNodeMaterial,
baseColor: string,
material: Material,
kind: WallHighlightKind,
): MeshStandardNodeMaterial {
const highlightedMaterial = material.clone()
const highlightedColor = getHighlightedColor(baseColor, kind)
): Material {
const highlightedMaterial = material.clone() as Material & {
color?: Color
emissive?: Color
emissiveIntensity?: number
needsUpdate?: boolean
}
const profile = WALL_HIGHLIGHT_PROFILES[kind]
highlightedMaterial.color = highlightedColor
highlightedMaterial.emissive = highlightedColor.clone()
highlightedMaterial.emissiveIntensity = profile.emissiveIntensity
if ('color' in highlightedMaterial && highlightedMaterial.color) {
highlightedMaterial.color = getHighlightedColor(highlightedMaterial.color, kind)
}
if ('emissive' in highlightedMaterial && highlightedMaterial.emissive) {
highlightedMaterial.emissive = highlightedMaterial.emissive
.clone()
.lerp(profile.color, profile.emissiveBlend)
}
if ('emissiveIntensity' in highlightedMaterial) {
highlightedMaterial.emissiveIntensity = Math.max(
highlightedMaterial.emissiveIntensity ?? 0,
profile.emissiveIntensity,
)
}
highlightedMaterial.needsUpdate = true
return highlightedMaterial
}
function createBaseVisibleWallMaterial(wallNode: WallNode): Material {
if (wallNode.materialPreset) {
return createMaterialFromPresetRef(wallNode.materialPreset) ?? baseMaterial
}
if (wallNode.material) {
return createMaterial(wallNode.material)
}
return baseMaterial
}
function getMaterialsForWall(wallNode: WallNode): WallMaterials {
const cacheKey = wallNode.id
const materialHash = getMaterialHash(wallNode)
@@ -130,19 +162,16 @@ function getMaterialsForWall(wallNode: WallNode): WallMaterials {
}
let userColor = DEFAULT_WALL_COLOR
if (wallNode.material?.properties?.color) {
const preset = getMaterialPresetByRef(wallNode.materialPreset)
if (preset?.mapProperties?.color) {
userColor = preset.mapProperties.color
} else 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 = wallNode.material
? new MeshStandardNodeMaterial({
color: userColor,
roughness: 1,
metalness: 0,
})
: (baseMaterial.clone() as MeshStandardNodeMaterial)
const visibleMat = createBaseVisibleWallMaterial(wallNode)
const invisibleMat = new MeshStandardNodeMaterial({
transparent: true,
@@ -152,10 +181,13 @@ function getMaterialsForWall(wallNode: WallNode): WallMaterials {
emissive: userColor,
})
const highlightedVisible = createHighlightedWallMaterial(visibleMat, userColor, 'selection')
const highlightedInvisible = createHighlightedWallMaterial(invisibleMat, userColor, 'selection')
const deleteVisible = createHighlightedWallMaterial(visibleMat, userColor, 'delete')
const deleteInvisible = createHighlightedWallMaterial(invisibleMat, userColor, 'delete')
const highlightedVisible = createHighlightedWallMaterial(visibleMat, 'selection')
const highlightedInvisible = createHighlightedWallMaterial(
invisibleMat,
'selection',
) as MeshStandardNodeMaterial
const deleteVisible = createHighlightedWallMaterial(visibleMat, 'delete')
const deleteInvisible = createHighlightedWallMaterial(invisibleMat, 'delete') as MeshStandardNodeMaterial
const result: WallMaterials = {
visible: visibleMat,
@@ -170,6 +202,10 @@ function getMaterialsForWall(wallNode: WallNode): WallMaterials {
return result
}
function getVisibleWallMaterial(wallNode: WallNode): Material {
return createBaseVisibleWallMaterial(wallNode)
}
function getWallHideState(
wallNode: WallNode,
wallMesh: Mesh,
@@ -265,9 +301,7 @@ export const WallCutout = () => {
? materials.deleteVisible
: isSelectionHighlighted
? materials.highlightedVisible
: wallNode.material
? materials.visible
: baseMaterial
: getVisibleWallMaterial(wallNode)
}
})
lastWallMode.current = wallMode
@@ -289,7 +323,7 @@ export const WallCutout = () => {
const current = wallMesh.material as Material
snapshot.set(wallMesh, current)
if (current === mats.highlightedVisible || current === mats.deleteVisible) {
wallMesh.material = mats.visible
wallMesh.material = getVisibleWallMaterial(wallNode)
} else if (current === mats.highlightedInvisible || current === mats.deleteInvisible) {
wallMesh.material = mats.invisible
}