Add paint hover previews to viewer
This commit is contained in:
@@ -157,14 +157,16 @@ function migrateStairSurfaceMaterials(node: Record<string, any>) {
|
||||
if (node.treadMaterial !== undefined || typeof node.treadMaterialPreset === 'string') {
|
||||
return {
|
||||
material: node.treadMaterial,
|
||||
materialPreset: typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined,
|
||||
materialPreset:
|
||||
typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
if (node.sideMaterial !== undefined || typeof node.sideMaterialPreset === 'string') {
|
||||
return {
|
||||
material: node.sideMaterial,
|
||||
materialPreset: typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined,
|
||||
materialPreset:
|
||||
typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -685,10 +685,12 @@ function PaintCursorLayer({
|
||||
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
||||
const activePaintTarget = useEditor((s) => s.activePaintTarget)
|
||||
const paintDisabledFeedbackTick = useEditor((s) => s.paintDisabledFeedbackTick)
|
||||
const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode)
|
||||
const badgeRef = useRef<HTMLDivElement>(null)
|
||||
const lastPointerPositionRef = useRef<{ x: number; y: number } | null>(null)
|
||||
const [showDisabledFeedback, setShowDisabledFeedback] = useState(false)
|
||||
const active = mode === 'material-paint' && !isVersionPreviewMode
|
||||
const showBlockedState = showDisabledFeedback || hoverHighlightMode === 'paint-disabled'
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
@@ -748,10 +750,26 @@ function PaintCursorLayer({
|
||||
)
|
||||
const label = !hasMaterial
|
||||
? 'Choose material'
|
||||
: showDisabledFeedback
|
||||
: showBlockedState
|
||||
? 'Unsupported'
|
||||
: `Paint ${activePaintTarget}`
|
||||
const icon = showDisabledFeedback ? 'mdi:block-helper' : 'mdi:format-color-fill'
|
||||
const icon = showBlockedState ? 'mdi:block-helper' : 'mdi:format-color-fill'
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) return
|
||||
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
|
||||
if (hoverHighlightMode === 'paint-disabled') {
|
||||
el.style.cursor = 'not-allowed'
|
||||
return () => {
|
||||
el.style.cursor = ''
|
||||
}
|
||||
}
|
||||
|
||||
el.style.cursor = ''
|
||||
}, [active, containerRef, hoverHighlightMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (!paintDisabledFeedbackTick) return
|
||||
@@ -797,7 +815,7 @@ function PaintCursorLayer({
|
||||
style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
|
||||
>
|
||||
<PaintCursorBadge
|
||||
disabled={!hasMaterial || showDisabledFeedback}
|
||||
disabled={!hasMaterial || showBlockedState}
|
||||
icon={icon}
|
||||
label={label}
|
||||
position={{ x: 0, y: 0 }}
|
||||
|
||||
@@ -70,6 +70,14 @@ type ModifierKeys = {
|
||||
ctrl: boolean
|
||||
}
|
||||
|
||||
type SceneMaterialPreview = ReturnType<typeof useViewer.getState>['materialPreview']
|
||||
|
||||
type PaintInteraction = {
|
||||
apply: (() => void) | null
|
||||
preview: SceneMaterialPreview
|
||||
hoveredId: AnyNodeId
|
||||
}
|
||||
|
||||
interface SelectionStrategy {
|
||||
types: SelectableNodeType[]
|
||||
handleSelect: (node: AnyNode, nativeEvent?: any, modifierKeys?: ModifierKeys) => void
|
||||
@@ -503,6 +511,32 @@ export const SelectionManager = () => {
|
||||
if (movingNode || curvingWall) return
|
||||
|
||||
const triggerPaintDisabledFeedback = useEditor.getState().triggerPaintDisabledFeedback
|
||||
let hoverFrame = 0
|
||||
let pendingHoveredId: AnyNodeId | null = null
|
||||
let pendingHoverMode: HoverHighlightMode = 'default'
|
||||
let pendingPreview: SceneMaterialPreview = null
|
||||
|
||||
const flushHoverState = () => {
|
||||
hoverFrame = 0
|
||||
const viewerState = useViewer.getState()
|
||||
|
||||
if (viewerState.hoveredId !== pendingHoveredId) {
|
||||
useViewer.setState({ hoveredId: pendingHoveredId })
|
||||
}
|
||||
|
||||
setHoverHighlightMode(pendingHoverMode)
|
||||
|
||||
if (pendingPreview) {
|
||||
viewerState.setMaterialPreview(pendingPreview)
|
||||
} else {
|
||||
viewerState.clearMaterialPreview()
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleHoverState = () => {
|
||||
if (hoverFrame !== 0) return
|
||||
hoverFrame = window.requestAnimationFrame(flushHoverState)
|
||||
}
|
||||
|
||||
const resolveActivePaintMaterial = () =>
|
||||
useEditor.getState().activePaintMaterial ??
|
||||
@@ -515,11 +549,7 @@ export const SelectionManager = () => {
|
||||
selectedMaterialTarget: useEditor.getState().selectedMaterialTarget,
|
||||
})
|
||||
|
||||
const getPaintInteraction = (
|
||||
event: NodeEvent,
|
||||
): {
|
||||
apply: (() => void) | null
|
||||
} | null => {
|
||||
const getPaintInteraction = (event: NodeEvent): PaintInteraction | null => {
|
||||
const activePaintMaterial = resolveActivePaintMaterial()
|
||||
const node = event.node
|
||||
|
||||
@@ -530,6 +560,7 @@ export const SelectionManager = () => {
|
||||
const compatible =
|
||||
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'wall')
|
||||
return {
|
||||
hoveredId: node.id as AnyNodeId,
|
||||
apply:
|
||||
compatible && hasActivePaintMaterial(activePaintMaterial)
|
||||
? () => {
|
||||
@@ -546,6 +577,16 @@ export const SelectionManager = () => {
|
||||
)
|
||||
}
|
||||
: null,
|
||||
preview:
|
||||
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
||||
? {
|
||||
nodeId: node.id as AnyNodeId,
|
||||
target: 'wall',
|
||||
role,
|
||||
material: activePaintMaterial.material,
|
||||
materialPreset: activePaintMaterial.materialPreset,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +603,7 @@ export const SelectionManager = () => {
|
||||
const compatible =
|
||||
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'roof')
|
||||
return {
|
||||
hoveredId: roofNode.id as AnyNodeId,
|
||||
apply:
|
||||
compatible && hasActivePaintMaterial(activePaintMaterial)
|
||||
? () => {
|
||||
@@ -578,6 +620,16 @@ export const SelectionManager = () => {
|
||||
)
|
||||
}
|
||||
: null,
|
||||
preview:
|
||||
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
||||
? {
|
||||
nodeId: roofNode.id as AnyNodeId,
|
||||
target: 'roof',
|
||||
role,
|
||||
material: activePaintMaterial.material,
|
||||
materialPreset: activePaintMaterial.materialPreset,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +646,7 @@ export const SelectionManager = () => {
|
||||
const compatible =
|
||||
role !== null && isActivePaintMaterialCompatible(activePaintMaterial, 'stair')
|
||||
return {
|
||||
hoveredId: stairNode.id as AnyNodeId,
|
||||
apply:
|
||||
compatible && hasActivePaintMaterial(activePaintMaterial)
|
||||
? () => {
|
||||
@@ -610,6 +663,16 @@ export const SelectionManager = () => {
|
||||
)
|
||||
}
|
||||
: null,
|
||||
preview:
|
||||
compatible && hasActivePaintMaterial(activePaintMaterial) && role
|
||||
? {
|
||||
nodeId: stairNode.id as AnyNodeId,
|
||||
target: 'stair',
|
||||
role,
|
||||
material: activePaintMaterial.material,
|
||||
materialPreset: activePaintMaterial.materialPreset,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,6 +683,7 @@ export const SelectionManager = () => {
|
||||
hasActivePaintMaterial(activePaintMaterial)
|
||||
|
||||
return {
|
||||
hoveredId: node.id as AnyNodeId,
|
||||
apply: compatible
|
||||
? () => {
|
||||
useScene
|
||||
@@ -633,19 +697,62 @@ export const SelectionManager = () => {
|
||||
)
|
||||
}
|
||||
: null,
|
||||
preview: compatible
|
||||
? {
|
||||
nodeId: node.id as AnyNodeId,
|
||||
target,
|
||||
role: 'surface',
|
||||
material: activePaintMaterial.material,
|
||||
materialPreset: activePaintMaterial.materialPreset,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
const disabledNodeTypes = ['item', 'window', 'door', 'zone']
|
||||
if (disabledNodeTypes.includes(node.type)) {
|
||||
return {
|
||||
hoveredId: node.id as AnyNodeId,
|
||||
apply: null,
|
||||
preview: null,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const onEnter = (event: NodeEvent) => {
|
||||
if (boxSelectHandled) return
|
||||
|
||||
const interaction = getPaintInteraction(event)
|
||||
if (!interaction) return
|
||||
|
||||
event.stopPropagation()
|
||||
|
||||
if (!interaction.preview) {
|
||||
pendingHoveredId = interaction.hoveredId
|
||||
pendingHoverMode = 'paint-disabled'
|
||||
pendingPreview = null
|
||||
scheduleHoverState()
|
||||
return
|
||||
}
|
||||
|
||||
pendingHoveredId = interaction.hoveredId
|
||||
pendingHoverMode = 'paint-ready'
|
||||
pendingPreview = interaction.preview
|
||||
scheduleHoverState()
|
||||
}
|
||||
|
||||
const onLeave = (event: NodeEvent) => {
|
||||
const interaction = getPaintInteraction(event)
|
||||
if (!interaction) return
|
||||
|
||||
pendingHoveredId = null
|
||||
pendingHoverMode = 'default'
|
||||
pendingPreview = null
|
||||
scheduleHoverState()
|
||||
}
|
||||
|
||||
const onClick = (event: NodeEvent) => {
|
||||
if (boxSelectHandled) return
|
||||
|
||||
@@ -654,12 +761,19 @@ export const SelectionManager = () => {
|
||||
|
||||
event.stopPropagation()
|
||||
|
||||
if (hoverFrame !== 0) {
|
||||
window.cancelAnimationFrame(hoverFrame)
|
||||
flushHoverState()
|
||||
}
|
||||
|
||||
if (!interaction.apply) {
|
||||
useViewer.getState().clearMaterialPreview()
|
||||
triggerPaintDisabledFeedback()
|
||||
return
|
||||
}
|
||||
|
||||
interaction.apply()
|
||||
useViewer.getState().clearMaterialPreview()
|
||||
}
|
||||
|
||||
const allTypes = [
|
||||
@@ -679,14 +793,24 @@ export const SelectionManager = () => {
|
||||
|
||||
for (const type of allTypes) {
|
||||
emitter.on(`${type}:click` as any, onClick as any)
|
||||
emitter.on(`${type}:enter` as any, onEnter as any)
|
||||
emitter.on(`${type}:leave` as any, onLeave as any)
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const type of allTypes) {
|
||||
emitter.off(`${type}:click` as any, onClick as any)
|
||||
emitter.off(`${type}:enter` as any, onEnter as any)
|
||||
emitter.off(`${type}:leave` as any, onLeave as any)
|
||||
}
|
||||
if (hoverFrame !== 0) {
|
||||
window.cancelAnimationFrame(hoverFrame)
|
||||
}
|
||||
}, [curvingWall, mode, movingNode])
|
||||
useViewer.setState({ hoveredId: null })
|
||||
setHoverHighlightMode('default')
|
||||
useViewer.getState().clearMaterialPreview()
|
||||
}
|
||||
}, [curvingWall, mode, movingNode, setHoverHighlightMode])
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -1241,7 +1365,8 @@ const SelectionMaterialSync = () => {
|
||||
}, [hoverHighlightMode, hoveredId, previewSelectedIds, selectedIds, syncSelectionMaterials])
|
||||
|
||||
useEffect(() => {
|
||||
return useScene.subscribe(() => {
|
||||
return useScene.subscribe((state, prevState) => {
|
||||
if (state.nodes === prevState.nodes) return
|
||||
syncSelectionMaterials()
|
||||
})
|
||||
}, [syncSelectionMaterials])
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { type CeilingNode, getMaterialPresetByRef, 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'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import useViewer from '../../../store/use-viewer'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
const gridScale = 5
|
||||
@@ -32,18 +38,47 @@ function createCeilingMaterials(color = '#999999') {
|
||||
return { topMaterial, bottomMaterial }
|
||||
}
|
||||
|
||||
const ceilingMaterialCache = new Map<string, ReturnType<typeof createCeilingMaterials>>()
|
||||
|
||||
function getCeilingMaterials(color = '#999999') {
|
||||
const cacheKey = color
|
||||
const cached = ceilingMaterialCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const materials = createCeilingMaterials(color)
|
||||
ceilingMaterialCache.set(cacheKey, materials)
|
||||
return materials
|
||||
}
|
||||
|
||||
export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
|
||||
useRegistry(node.id, 'ceiling', ref)
|
||||
const handlers = useNodeEvents(node, 'ceiling')
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'ceiling' && state.materialPreview.nodeId === node.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
|
||||
const materials = useMemo(() => {
|
||||
const preset = getMaterialPresetByRef(node.materialPreset)
|
||||
const props = preset?.mapProperties ?? resolveMaterial(node.material)
|
||||
const preset = getMaterialPresetByRef(materialPreview?.materialPreset ?? node.materialPreset)
|
||||
const props =
|
||||
preset?.mapProperties ?? resolveMaterial(materialPreview?.material ?? node.material)
|
||||
const color = props.color || '#999999'
|
||||
return createCeilingMaterials(color)
|
||||
}, [node.materialPreset, node.material, node.material?.preset, node.material?.properties, node.material?.texture])
|
||||
return getCeilingMaterials(color)
|
||||
}, [
|
||||
node.materialPreset,
|
||||
node.material,
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
materialPreview?.materialPreset,
|
||||
materialPreview?.material,
|
||||
materialPreview?.material?.preset,
|
||||
materialPreview?.material?.properties,
|
||||
materialPreview?.material?.texture,
|
||||
])
|
||||
|
||||
return (
|
||||
<mesh material={materials.bottomMaterial} ref={ref}>
|
||||
|
||||
@@ -7,14 +7,22 @@ import {
|
||||
createMaterialFromPresetRef,
|
||||
DEFAULT_STAIR_MATERIAL,
|
||||
} from '../../../lib/materials'
|
||||
import useViewer from '../../../store/use-viewer'
|
||||
|
||||
export const FenceRenderer = ({ node }: { node: FenceNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
const handlers = useNodeEvents(node, 'fence')
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'fence' && state.materialPreview.nodeId === node.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
const material = useMemo(() => {
|
||||
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
|
||||
const presetMaterial = createMaterialFromPresetRef(
|
||||
materialPreview?.materialPreset ?? node.materialPreset,
|
||||
)
|
||||
if (presetMaterial) return presetMaterial
|
||||
const mat = node.material
|
||||
const mat = materialPreview?.material ?? node.material
|
||||
if (!mat) return DEFAULT_STAIR_MATERIAL
|
||||
return createMaterial(mat)
|
||||
}, [
|
||||
@@ -23,6 +31,11 @@ export const FenceRenderer = ({ node }: { node: FenceNode }) => {
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
materialPreview?.materialPreset,
|
||||
materialPreview?.material,
|
||||
materialPreview?.material?.preset,
|
||||
materialPreview?.material?.properties,
|
||||
materialPreview?.material?.texture,
|
||||
])
|
||||
|
||||
useRegistry(node.id, 'fence', ref)
|
||||
@@ -31,7 +44,14 @@ export const FenceRenderer = ({ node }: { node: FenceNode }) => {
|
||||
}, [node.id])
|
||||
|
||||
return (
|
||||
<mesh castShadow material={material} receiveShadow ref={ref} visible={node.visible} {...handlers}>
|
||||
<mesh
|
||||
castShadow
|
||||
material={material}
|
||||
receiveShadow
|
||||
ref={ref}
|
||||
visible={node.visible}
|
||||
{...handlers}
|
||||
>
|
||||
<boxGeometry args={[0, 0, 0]} />
|
||||
</mesh>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { type AnyNodeId, type RoofNode, type RoofSegmentNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type RoofNode,
|
||||
type RoofSegmentNode,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
@@ -14,8 +20,14 @@ export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
|
||||
|
||||
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 parentNode = node.parentId
|
||||
? (nodes[node.parentId as AnyNodeId] as RoofNode | undefined)
|
||||
: undefined
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'roof' && state.materialPreview.nodeId === parentNode?.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
const placeholderGeometry = useMemo(() => {
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3))
|
||||
@@ -26,30 +38,40 @@ export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
|
||||
return geometry
|
||||
}, [])
|
||||
|
||||
const previewParentNode = !parentNode
|
||||
? undefined
|
||||
: materialPreview?.role === 'top'
|
||||
? {
|
||||
...parentNode,
|
||||
topMaterial: materialPreview.material,
|
||||
topMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'edge'
|
||||
? {
|
||||
...parentNode,
|
||||
edgeMaterial: materialPreview.material,
|
||||
edgeMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'wall'
|
||||
? {
|
||||
...parentNode,
|
||||
wallMaterial: materialPreview.material,
|
||||
wallMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: parentNode
|
||||
const customMaterial = useMemo(() => {
|
||||
if (node.material !== undefined || typeof node.materialPreset === 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return parentNode ? getRoofMaterialArray(parentNode) : null
|
||||
}, [
|
||||
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,
|
||||
parentNode?.topMaterial,
|
||||
parentNode?.topMaterialPreset,
|
||||
parentNode?.edgeMaterial,
|
||||
parentNode?.edgeMaterialPreset,
|
||||
parentNode?.wallMaterial,
|
||||
parentNode?.wallMaterialPreset,
|
||||
])
|
||||
return previewParentNode ? getRoofMaterialArray(previewParentNode) : null
|
||||
}, [node, previewParentNode])
|
||||
|
||||
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
|
||||
|
||||
const handlers = useNodeEvents(node, 'roof')
|
||||
const debugColors = useViewer((s) => s.debugColors)
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'roof' && state.materialPreview.nodeId === node.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
const placeholderGeometry = useMemo(() => {
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3))
|
||||
@@ -24,22 +29,33 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
|
||||
return geometry
|
||||
}, [])
|
||||
|
||||
const customMaterial = useMemo(
|
||||
() => getRoofMaterialArray(node),
|
||||
[
|
||||
node.materialPreset,
|
||||
node.material,
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
node.topMaterial,
|
||||
node.topMaterialPreset,
|
||||
node.edgeMaterial,
|
||||
node.edgeMaterialPreset,
|
||||
node.wallMaterial,
|
||||
node.wallMaterialPreset,
|
||||
],
|
||||
)
|
||||
const previewNode =
|
||||
materialPreview?.role === 'top'
|
||||
? {
|
||||
...node,
|
||||
topMaterial: materialPreview.material,
|
||||
topMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'edge'
|
||||
? {
|
||||
...node,
|
||||
edgeMaterial: materialPreview.material,
|
||||
edgeMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'wall'
|
||||
? {
|
||||
...node,
|
||||
wallMaterial: materialPreview.material,
|
||||
wallMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: node
|
||||
const customMaterial = useMemo(() => getRoofMaterialArray(previewNode), [previewNode])
|
||||
|
||||
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { getMaterialPresetByRef, type SlabNode, useRegistry } from '@pascal-app/core'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import * as THREE from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import {
|
||||
applyMaterialPresetToMaterials,
|
||||
createMaterial,
|
||||
DEFAULT_SLAB_MATERIAL,
|
||||
} from '../../../lib/materials'
|
||||
import useViewer from '../../../store/use-viewer'
|
||||
|
||||
export const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
const slabMaterialCache = new Map<string, THREE.MeshStandardMaterial>()
|
||||
|
||||
useRegistry(node.id, 'slab', ref)
|
||||
function getSlabMaterial(
|
||||
cacheKey: string,
|
||||
params: { material?: SlabNode['material']; materialPreset?: string },
|
||||
) {
|
||||
const cached = slabMaterialCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const handlers = useNodeEvents(node, 'slab')
|
||||
|
||||
const material = useMemo(() => {
|
||||
const preset = getMaterialPresetByRef(node.materialPreset)
|
||||
const preset = getMaterialPresetByRef(params.materialPreset)
|
||||
const slabMaterial = preset
|
||||
? new THREE.MeshStandardMaterial()
|
||||
: node.material
|
||||
? createMaterial(node.material).clone()
|
||||
: params.material
|
||||
? createMaterial(params.material).clone()
|
||||
: DEFAULT_SLAB_MATERIAL.clone()
|
||||
|
||||
if (preset) {
|
||||
@@ -40,21 +42,47 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
||||
slabMaterial.depthWrite = true
|
||||
slabMaterial.needsUpdate = true
|
||||
|
||||
slabMaterialCache.set(cacheKey, slabMaterial)
|
||||
return slabMaterial
|
||||
}
|
||||
|
||||
export const SlabRenderer = ({ node }: { node: SlabNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
|
||||
useRegistry(node.id, 'slab', ref)
|
||||
|
||||
const handlers = useNodeEvents(node, 'slab')
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'slab' && state.materialPreview.nodeId === node.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
|
||||
const material = useMemo(() => {
|
||||
const resolvedMaterial = materialPreview?.material ?? node.material
|
||||
const resolvedMaterialPreset = materialPreview?.materialPreset ?? node.materialPreset
|
||||
const cacheKey = JSON.stringify({
|
||||
material: resolvedMaterial ?? null,
|
||||
materialPreset: resolvedMaterialPreset ?? null,
|
||||
})
|
||||
|
||||
return getSlabMaterial(cacheKey, {
|
||||
material: resolvedMaterial,
|
||||
materialPreset: resolvedMaterialPreset,
|
||||
})
|
||||
}, [
|
||||
node.material,
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
node.materialPreset,
|
||||
materialPreview?.material,
|
||||
materialPreview?.material?.preset,
|
||||
materialPreview?.material?.properties,
|
||||
materialPreview?.material?.texture,
|
||||
materialPreview?.materialPreset,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
material.dispose()
|
||||
}
|
||||
}, [material])
|
||||
|
||||
return (
|
||||
<mesh
|
||||
castShadow
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { type AnyNodeId, type StairNode, type StairSegmentNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type StairNode,
|
||||
type StairSegmentNode,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import useViewer from '../../../store/use-viewer'
|
||||
import { getStraightStairSegmentBodyMaterials } from '../../../systems/stair/stair-materials'
|
||||
|
||||
export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => {
|
||||
@@ -15,29 +22,45 @@ 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 parentNode = node.parentId
|
||||
? (nodes[node.parentId as AnyNodeId] as StairNode | undefined)
|
||||
: undefined
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'stair' && state.materialPreview.nodeId === parentNode?.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
const previewParentNode = !parentNode
|
||||
? undefined
|
||||
: materialPreview?.role === 'railing'
|
||||
? {
|
||||
...parentNode,
|
||||
railingMaterial: materialPreview.material,
|
||||
railingMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'tread'
|
||||
? {
|
||||
...parentNode,
|
||||
treadMaterial: materialPreview.material,
|
||||
treadMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'side'
|
||||
? {
|
||||
...parentNode,
|
||||
sideMaterial: materialPreview.material,
|
||||
sideMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: parentNode
|
||||
|
||||
const material = useMemo(() => {
|
||||
return getStraightStairSegmentBodyMaterials(node, parentNode)
|
||||
}, [
|
||||
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,
|
||||
parentNode?.railingMaterialPreset,
|
||||
parentNode?.railingMaterial,
|
||||
parentNode?.sideMaterialPreset,
|
||||
parentNode?.sideMaterial,
|
||||
parentNode?.treadMaterialPreset,
|
||||
parentNode?.treadMaterial,
|
||||
])
|
||||
return getStraightStairSegmentBodyMaterials(node, previewParentNode)
|
||||
}, [node, previewParentNode])
|
||||
|
||||
const placeholderGeometry = useMemo(() => {
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
|
||||
@@ -8,10 +8,15 @@ import {
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { createMaterial, createMaterialFromPresetRef, DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
|
||||
import {
|
||||
getStairRailingMaterial,
|
||||
createMaterial,
|
||||
createMaterialFromPresetRef,
|
||||
DEFAULT_STAIR_MATERIAL,
|
||||
} from '../../../lib/materials'
|
||||
import useViewer from '../../../store/use-viewer'
|
||||
import {
|
||||
getStairBodyMaterials,
|
||||
getStairRailingMaterial,
|
||||
type StairBodyMaterials,
|
||||
} from '../../../systems/stair/stair-materials'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
@@ -57,48 +62,55 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
|
||||
}, [node.id])
|
||||
|
||||
const handlers = useNodeEvents(node, 'stair')
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'stair' && state.materialPreview.nodeId === node.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
const previewNode =
|
||||
materialPreview?.role === 'railing'
|
||||
? {
|
||||
...node,
|
||||
railingMaterial: materialPreview.material,
|
||||
railingMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'tread'
|
||||
? {
|
||||
...node,
|
||||
treadMaterial: materialPreview.material,
|
||||
treadMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview?.role === 'side'
|
||||
? {
|
||||
...node,
|
||||
sideMaterial: materialPreview.material,
|
||||
sideMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: node
|
||||
|
||||
const material = useMemo(() => {
|
||||
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
|
||||
const presetMaterial = createMaterialFromPresetRef(previewNode.materialPreset)
|
||||
if (presetMaterial) return presetMaterial
|
||||
const mat = node.material
|
||||
const mat = previewNode.material
|
||||
if (!mat) return DEFAULT_STAIR_MATERIAL
|
||||
return createMaterial(mat)
|
||||
}, [
|
||||
node.materialPreset,
|
||||
node.material,
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
previewNode.materialPreset,
|
||||
previewNode.material,
|
||||
previewNode.material?.preset,
|
||||
previewNode.material?.properties,
|
||||
previewNode.material?.texture,
|
||||
])
|
||||
|
||||
const straightBodyMaterials = useMemo(
|
||||
() => getStairBodyMaterials(node),
|
||||
[
|
||||
node.material,
|
||||
node.materialPreset,
|
||||
node.railingMaterial,
|
||||
node.railingMaterialPreset,
|
||||
node.sideMaterial,
|
||||
node.sideMaterialPreset,
|
||||
node.treadMaterial,
|
||||
node.treadMaterialPreset,
|
||||
],
|
||||
)
|
||||
const straightBodyMaterials = useMemo(() => getStairBodyMaterials(previewNode), [previewNode])
|
||||
|
||||
const railingMaterial = useMemo(
|
||||
() => getStairRailingMaterial(node),
|
||||
[
|
||||
node.material,
|
||||
node.materialPreset,
|
||||
node.railingMaterial,
|
||||
node.railingMaterialPreset,
|
||||
node.sideMaterial,
|
||||
node.sideMaterialPreset,
|
||||
node.treadMaterial,
|
||||
node.treadMaterialPreset,
|
||||
],
|
||||
)
|
||||
const railingMaterial = useMemo(() => getStairRailingMaterial(previewNode), [previewNode])
|
||||
|
||||
const straightPlaceholderGeometry = useMemo(() => {
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
@@ -132,7 +144,9 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
|
||||
receiveShadow
|
||||
/>
|
||||
) : null}
|
||||
{!isSegmentBasedStair ? <CurvedStairBody bodyMaterials={straightBodyMaterials} stair={node} /> : null}
|
||||
{!isSegmentBasedStair ? (
|
||||
<CurvedStairBody bodyMaterials={straightBodyMaterials} stair={node} />
|
||||
) : null}
|
||||
<StairRailings material={railingMaterial} stair={node} />
|
||||
{isSegmentBasedStair ? (
|
||||
<group name="segments-wrapper" visible={false}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRegistry, useScene, type WallNode } from '@pascal-app/core'
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import useViewer from '../../../store/use-viewer'
|
||||
import { getVisibleWallMaterials } from '../../../systems/wall/wall-materials'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
@@ -15,7 +16,30 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
|
||||
}, [node.id])
|
||||
|
||||
const handlers = useNodeEvents(node, 'wall')
|
||||
const material = getVisibleWallMaterials(node)
|
||||
const materialPreview = useViewer((state) =>
|
||||
state.materialPreview?.target === 'wall' && state.materialPreview.nodeId === node.id
|
||||
? state.materialPreview
|
||||
: null,
|
||||
)
|
||||
const previewNode =
|
||||
materialPreview && materialPreview.role === 'interior'
|
||||
? {
|
||||
...node,
|
||||
interiorMaterial: materialPreview.material,
|
||||
interiorMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: materialPreview && materialPreview.role === 'exterior'
|
||||
? {
|
||||
...node,
|
||||
exteriorMaterial: materialPreview.material,
|
||||
exteriorMaterialPreset: materialPreview.materialPreset,
|
||||
material: undefined,
|
||||
materialPreset: undefined,
|
||||
}
|
||||
: node
|
||||
const material = getVisibleWallMaterials(previewNode)
|
||||
|
||||
return (
|
||||
<mesh castShadow material={material} receiveShadow ref={ref} visible={node.visible}>
|
||||
|
||||
+41
-3
@@ -1,4 +1,14 @@
|
||||
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core'
|
||||
import type {
|
||||
AnyNode,
|
||||
BaseNode,
|
||||
BuildingNode,
|
||||
LevelNode,
|
||||
MaterialSchema,
|
||||
RoofSurfaceMaterialRole,
|
||||
StairSurfaceMaterialRole,
|
||||
WallSurfaceSide,
|
||||
ZoneNode,
|
||||
} from '@pascal-app/core'
|
||||
import type { Object3D } from 'three'
|
||||
type SelectionPath = {
|
||||
buildingId: BuildingNode['id'] | null
|
||||
@@ -10,12 +20,40 @@ type Outliner = {
|
||||
selectedObjects: Object3D[]
|
||||
hoveredObjects: Object3D[]
|
||||
}
|
||||
type MaterialPreview = {
|
||||
nodeId: string
|
||||
target: 'wall'
|
||||
role: WallSurfaceSide
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
} | {
|
||||
nodeId: string
|
||||
target: 'roof'
|
||||
role: RoofSurfaceMaterialRole
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
} | {
|
||||
nodeId: string
|
||||
target: 'stair'
|
||||
role: StairSurfaceMaterialRole
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
} | {
|
||||
nodeId: string
|
||||
target: 'fence' | 'slab' | 'ceiling'
|
||||
role: 'surface'
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
} | null
|
||||
type ViewerState = {
|
||||
selection: SelectionPath
|
||||
previewSelectedIds: BaseNode['id'][]
|
||||
setPreviewSelectedIds: (ids: BaseNode['id'][]) => void
|
||||
hoverHighlightMode: 'default' | 'delete'
|
||||
setHoverHighlightMode: (mode: 'default' | 'delete') => void
|
||||
materialPreview: MaterialPreview
|
||||
setMaterialPreview: (preview: MaterialPreview) => void
|
||||
clearMaterialPreview: () => void
|
||||
hoverHighlightMode: 'default' | 'delete' | 'paint-ready' | 'paint-disabled'
|
||||
setHoverHighlightMode: (mode: 'default' | 'delete' | 'paint-ready' | 'paint-disabled') => void
|
||||
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
|
||||
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
|
||||
cameraMode: 'perspective' | 'orthographic'
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core'
|
||||
import type {
|
||||
AnyNode,
|
||||
AnyNodeId,
|
||||
BaseNode,
|
||||
BuildingNode,
|
||||
LevelNode,
|
||||
MaterialSchema,
|
||||
RoofSurfaceMaterialRole,
|
||||
StairSurfaceMaterialRole,
|
||||
WallSurfaceSide,
|
||||
ZoneNode,
|
||||
} from '@pascal-app/core'
|
||||
import type { Object3D } from 'three'
|
||||
|
||||
import { create } from 'zustand'
|
||||
@@ -18,10 +29,57 @@ type Outliner = {
|
||||
hoveredObjects: Object3D[]
|
||||
}
|
||||
|
||||
type MaterialPreview =
|
||||
| {
|
||||
nodeId: AnyNodeId
|
||||
target: 'wall'
|
||||
role: WallSurfaceSide
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
}
|
||||
| {
|
||||
nodeId: AnyNodeId
|
||||
target: 'roof'
|
||||
role: RoofSurfaceMaterialRole
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
}
|
||||
| {
|
||||
nodeId: AnyNodeId
|
||||
target: 'stair'
|
||||
role: StairSurfaceMaterialRole
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
}
|
||||
| {
|
||||
nodeId: AnyNodeId
|
||||
target: 'fence' | 'slab' | 'ceiling'
|
||||
role: 'surface'
|
||||
material?: MaterialSchema
|
||||
materialPreset?: string
|
||||
}
|
||||
| null
|
||||
|
||||
function isSameMaterialPreview(left: MaterialPreview, right: MaterialPreview) {
|
||||
if (left === right) return true
|
||||
if (!left || !right) return false
|
||||
|
||||
return (
|
||||
left.nodeId === right.nodeId &&
|
||||
left.target === right.target &&
|
||||
left.role === right.role &&
|
||||
left.material === right.material &&
|
||||
left.materialPreset === right.materialPreset
|
||||
)
|
||||
}
|
||||
|
||||
type ViewerState = {
|
||||
selection: SelectionPath
|
||||
previewSelectedIds: BaseNode['id'][]
|
||||
setPreviewSelectedIds: (ids: BaseNode['id'][]) => void
|
||||
materialPreview: MaterialPreview
|
||||
setMaterialPreview: (preview: MaterialPreview) => void
|
||||
clearMaterialPreview: () => void
|
||||
hoverHighlightMode: 'default' | 'delete' | 'paint-ready' | 'paint-disabled'
|
||||
setHoverHighlightMode: (mode: 'default' | 'delete' | 'paint-ready' | 'paint-disabled') => void
|
||||
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
|
||||
@@ -84,6 +142,15 @@ const useViewer = create<ViewerState>()(
|
||||
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
||||
previewSelectedIds: [],
|
||||
setPreviewSelectedIds: (ids) => set({ previewSelectedIds: ids }),
|
||||
materialPreview: null,
|
||||
setMaterialPreview: (materialPreview) =>
|
||||
set((state) =>
|
||||
isSameMaterialPreview(state.materialPreview, materialPreview)
|
||||
? state
|
||||
: { materialPreview },
|
||||
),
|
||||
clearMaterialPreview: () =>
|
||||
set((state) => (state.materialPreview === null ? state : { materialPreview: null })),
|
||||
hoverHighlightMode: 'default',
|
||||
setHoverHighlightMode: (mode) =>
|
||||
set((state) => (state.hoverHighlightMode === mode ? state : { hoverHighlightMode: mode })),
|
||||
@@ -188,6 +255,7 @@ const useViewer = create<ViewerState>()(
|
||||
selectedIds: [],
|
||||
},
|
||||
previewSelectedIds: [],
|
||||
materialPreview: null,
|
||||
}),
|
||||
|
||||
outliner: { selectedObjects: [], hoveredObjects: [] },
|
||||
|
||||
@@ -8,6 +8,17 @@ import { createMaterial, createMaterialFromPresetRef } from '../../lib/materials
|
||||
|
||||
export type RoofMaterialArray = [THREE.Material, THREE.Material, THREE.Material, THREE.Material]
|
||||
|
||||
const roofMaterialArrayCache = new Map<string, RoofMaterialArray>()
|
||||
|
||||
function getSurfaceMaterialSignature(
|
||||
spec: ReturnType<typeof getEffectiveRoofSurfaceMaterial>,
|
||||
): string {
|
||||
return JSON.stringify({
|
||||
material: spec.material ?? null,
|
||||
materialPreset: spec.materialPreset ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
function createResolvedMaterial(
|
||||
material: RoofNode['material'] | RoofSegmentNode['material'] | undefined,
|
||||
materialPreset: string | undefined,
|
||||
@@ -27,6 +38,14 @@ export function getRoofMaterialArray(node: RoofNode): RoofMaterialArray | null {
|
||||
const top = getEffectiveRoofSurfaceMaterial(node, 'top')
|
||||
const edge = getEffectiveRoofSurfaceMaterial(node, 'edge')
|
||||
const wall = getEffectiveRoofSurfaceMaterial(node, 'wall')
|
||||
const cacheKey = JSON.stringify({
|
||||
top: getSurfaceMaterialSignature(top),
|
||||
edge: getSurfaceMaterialSignature(edge),
|
||||
wall: getSurfaceMaterialSignature(wall),
|
||||
})
|
||||
|
||||
const cached = roofMaterialArrayCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const topMaterial = createResolvedMaterial(top.material, top.materialPreset)
|
||||
const edgeMaterial = createResolvedMaterial(edge.material, edge.materialPreset)
|
||||
@@ -36,11 +55,13 @@ export function getRoofMaterialArray(node: RoofNode): RoofMaterialArray | null {
|
||||
return null
|
||||
}
|
||||
|
||||
return [
|
||||
const materialArray: RoofMaterialArray = [
|
||||
edgeMaterial ?? wallMaterial ?? topMaterial ?? new THREE.MeshStandardMaterial(),
|
||||
wallMaterial ?? edgeMaterial ?? topMaterial ?? new THREE.MeshStandardMaterial(),
|
||||
wallMaterial ?? edgeMaterial ?? topMaterial ?? new THREE.MeshStandardMaterial(),
|
||||
topMaterial ?? wallMaterial ?? edgeMaterial ?? new THREE.MeshStandardMaterial(),
|
||||
]
|
||||
}
|
||||
|
||||
roofMaterialArrayCache.set(cacheKey, materialArray)
|
||||
return materialArray
|
||||
}
|
||||
|
||||
@@ -12,6 +12,18 @@ import {
|
||||
|
||||
export type StairBodyMaterials = [THREE.Material, THREE.Material]
|
||||
|
||||
const stairBodyMaterialCache = new Map<string, StairBodyMaterials>()
|
||||
const stairRailingMaterialCache = new Map<string, THREE.Material>()
|
||||
|
||||
function getSurfaceMaterialSignature(
|
||||
spec: ReturnType<typeof getEffectiveStairSurfaceMaterial>,
|
||||
): string {
|
||||
return JSON.stringify({
|
||||
material: spec.material ?? null,
|
||||
materialPreset: spec.materialPreset ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
function createResolvedMaterial(
|
||||
material: StairNode['material'] | StairSegmentNode['material'] | undefined,
|
||||
materialPreset: string | undefined,
|
||||
@@ -30,16 +42,32 @@ function createResolvedMaterial(
|
||||
export function getStairBodyMaterials(stair: StairNode): StairBodyMaterials {
|
||||
const tread = getEffectiveStairSurfaceMaterial(stair, 'tread')
|
||||
const side = getEffectiveStairSurfaceMaterial(stair, 'side')
|
||||
const cacheKey = JSON.stringify({
|
||||
tread: getSurfaceMaterialSignature(tread),
|
||||
side: getSurfaceMaterialSignature(side),
|
||||
})
|
||||
|
||||
return [
|
||||
const cached = stairBodyMaterialCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const materials: StairBodyMaterials = [
|
||||
createResolvedMaterial(tread.material, tread.materialPreset),
|
||||
createResolvedMaterial(side.material, side.materialPreset),
|
||||
]
|
||||
|
||||
stairBodyMaterialCache.set(cacheKey, materials)
|
||||
return materials
|
||||
}
|
||||
|
||||
export function getStairRailingMaterial(stair: StairNode): THREE.Material {
|
||||
const railing = getEffectiveStairSurfaceMaterial(stair, 'railing')
|
||||
return createResolvedMaterial(railing.material, railing.materialPreset)
|
||||
const cacheKey = getSurfaceMaterialSignature(railing)
|
||||
const cached = stairRailingMaterialCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
const material = createResolvedMaterial(railing.material, railing.materialPreset)
|
||||
stairRailingMaterialCache.set(cacheKey, material)
|
||||
return material
|
||||
}
|
||||
|
||||
export function getStraightStairSegmentBodyMaterials(
|
||||
|
||||
Reference in New Issue
Block a user