Refactor viewer hover styles and remove preview state

This commit is contained in:
sudhir
2026-04-24 22:49:04 +05:30
parent 7297c76204
commit 325f2e4603
15 changed files with 142 additions and 360 deletions
@@ -7,7 +7,7 @@ import {
spatialGridManager, spatialGridManager,
useScene, useScene,
} from '@pascal-app/core' } from '@pascal-app/core'
import { InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer' import { type HoverStyles, InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
import { import {
memo, memo,
type ReactNode, type ReactNode,
@@ -75,6 +75,17 @@ const PAINT_CURSOR_BADGE_COLOR = '#f59e0b'
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8' const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
const PAINT_CURSOR_BADGE_OFFSET_X = 14 const PAINT_CURSOR_BADGE_OFFSET_X = 14
const PAINT_CURSOR_BADGE_OFFSET_Y = 14 const PAINT_CURSOR_BADGE_OFFSET_Y = 14
const EDITOR_HOVER_STYLES: HoverStyles = {
default: { visibleColor: 0x00_aaff, hiddenColor: 0xf3_ff47, strength: 5, pulse: true },
delete: { visibleColor: 0xef_4444, hiddenColor: 0x99_1b1b, strength: 6, pulse: false },
'paint-ready': { visibleColor: 0xf5_9e0b, hiddenColor: 0xfd_e068, strength: 5, pulse: true },
'paint-disabled': {
visibleColor: 0x94_a3b8,
hiddenColor: 0x47_5569,
strength: 4,
pulse: false,
},
}
/** /**
* Wire up module-level singletons (spatial grid, space detection, SFX) for * Wire up module-level singletons (spatial grid, space detection, SFX) for
@@ -885,7 +896,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
/> />
) : null} ) : null}
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} /> <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
<Viewer selectionManager={isFirstPersonMode ? 'default' : 'custom'}> <Viewer
hoverStyles={EDITOR_HOVER_STYLES}
selectionManager={isFirstPersonMode ? 'default' : 'custom'}
>
<ViewerSceneContent <ViewerSceneContent
isFirstPersonMode={isFirstPersonMode} isFirstPersonMode={isFirstPersonMode}
isLoading={isLoading} isLoading={isLoading}
@@ -1017,7 +1031,7 @@ export default function Editor({
const showLoader = isLoading || isSceneLoading const showLoader = isLoading || isSceneLoading
const previewViewerContent = ( const previewViewerContent = (
<Viewer selectionManager="default"> <Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
<ExportManager /> <ExportManager />
<ViewerZoneSystem /> <ViewerZoneSystem />
<CeilingSystem /> <CeilingSystem />
@@ -25,20 +25,18 @@ import {
type WallSurfaceSide, type WallSurfaceSide,
} from '@pascal-app/core' } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect, useRef } from 'react'
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three'
import { import {
applyMaterialPresetToMaterials, applyMaterialPresetToMaterials,
createMaterial, createMaterial,
createMaterialFromPresetRef, createMaterialFromPresetRef,
} from '../../../../viewer/src/lib/materials' getRoofMaterialArray,
import { getRoofMaterialArray } from '../../../../viewer/src/systems/roof/roof-materials'
import {
getStairBodyMaterials, getStairBodyMaterials,
getStairRailingMaterial, getStairRailingMaterial,
} from '../../../../viewer/src/systems/stair/stair-materials' getVisibleWallMaterials,
import { getVisibleWallMaterials } from '../../../../viewer/src/systems/wall/wall-materials' useViewer,
} from '@pascal-app/viewer'
import { useCallback, useEffect, useRef } from 'react'
import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three'
import { import {
type ActivePaintMaterial, type ActivePaintMaterial,
buildRoofSurfaceMaterialPatch, buildRoofSurfaceMaterialPatch,
@@ -8,7 +8,6 @@ import { useMemo, useRef } from 'react'
import { float, mix, positionWorld, smoothstep } from 'three/tsl' import { float, mix, positionWorld, smoothstep } from 'three/tsl'
import { BackSide, FrontSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu' import { BackSide, FrontSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
import { useNodeEvents } from '../../../hooks/use-node-events' import { useNodeEvents } from '../../../hooks/use-node-events'
import useViewer from '../../../store/use-viewer'
import { NodeRenderer } from '../node-renderer' import { NodeRenderer } from '../node-renderer'
const gridScale = 5 const gridScale = 5
@@ -55,16 +54,10 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
useRegistry(node.id, 'ceiling', ref) useRegistry(node.id, 'ceiling', ref)
const handlers = useNodeEvents(node, 'ceiling') 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 materials = useMemo(() => {
const preset = getMaterialPresetByRef(materialPreview?.materialPreset ?? node.materialPreset) const preset = getMaterialPresetByRef(node.materialPreset)
const props = const props = preset?.mapProperties ?? resolveMaterial(node.material)
preset?.mapProperties ?? resolveMaterial(materialPreview?.material ?? node.material)
const color = props.color || '#999999' const color = props.color || '#999999'
return getCeilingMaterials(color) return getCeilingMaterials(color)
}, [ }, [
@@ -73,11 +66,6 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
node.material?.preset, node.material?.preset,
node.material?.properties, node.material?.properties,
node.material?.texture, node.material?.texture,
materialPreview?.materialPreset,
materialPreview?.material,
materialPreview?.material?.preset,
materialPreview?.material?.properties,
materialPreview?.material?.texture,
]) ])
return ( return (
@@ -7,22 +7,14 @@ import {
createMaterialFromPresetRef, createMaterialFromPresetRef,
DEFAULT_STAIR_MATERIAL, DEFAULT_STAIR_MATERIAL,
} from '../../../lib/materials' } from '../../../lib/materials'
import useViewer from '../../../store/use-viewer'
export const FenceRenderer = ({ node }: { node: FenceNode }) => { export const FenceRenderer = ({ node }: { node: FenceNode }) => {
const ref = useRef<Mesh>(null!) const ref = useRef<Mesh>(null!)
const handlers = useNodeEvents(node, 'fence') 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 material = useMemo(() => {
const presetMaterial = createMaterialFromPresetRef( const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
materialPreview?.materialPreset ?? node.materialPreset,
)
if (presetMaterial) return presetMaterial if (presetMaterial) return presetMaterial
const mat = materialPreview?.material ?? node.material const mat = node.material
if (!mat) return DEFAULT_STAIR_MATERIAL if (!mat) return DEFAULT_STAIR_MATERIAL
return createMaterial(mat) return createMaterial(mat)
}, [ }, [
@@ -31,11 +23,6 @@ export const FenceRenderer = ({ node }: { node: FenceNode }) => {
node.material?.preset, node.material?.preset,
node.material?.properties, node.material?.properties,
node.material?.texture, node.material?.texture,
materialPreview?.materialPreset,
materialPreview?.material,
materialPreview?.material?.preset,
materialPreview?.material?.properties,
materialPreview?.material?.texture,
]) ])
useRegistry(node.id, 'fence', ref) useRegistry(node.id, 'fence', ref)
@@ -23,11 +23,6 @@ export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
const parentNode = node.parentId const parentNode = node.parentId
? (nodes[node.parentId as AnyNodeId] as RoofNode | undefined) ? (nodes[node.parentId as AnyNodeId] as RoofNode | undefined)
: undefined : undefined
const materialPreview = useViewer((state) =>
state.materialPreview?.target === 'roof' && state.materialPreview.nodeId === parentNode?.id
? state.materialPreview
: null,
)
const placeholderGeometry = useMemo(() => { const placeholderGeometry = useMemo(() => {
const geometry = new THREE.BufferGeometry() const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3)) geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3))
@@ -38,40 +33,13 @@ export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
return geometry 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(() => { const customMaterial = useMemo(() => {
if (node.material !== undefined || typeof node.materialPreset === 'string') { if (node.material !== undefined || typeof node.materialPreset === 'string') {
return null return null
} }
return previewParentNode ? getRoofMaterialArray(previewParentNode) : null return parentNode ? getRoofMaterialArray(parentNode) : null
}, [node, previewParentNode]) }, [node, parentNode])
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
@@ -14,11 +14,6 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
const handlers = useNodeEvents(node, 'roof') const handlers = useNodeEvents(node, 'roof')
const debugColors = useViewer((s) => s.debugColors) 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 placeholderGeometry = useMemo(() => {
const geometry = new THREE.BufferGeometry() const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3)) geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3))
@@ -29,33 +24,7 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
return geometry return geometry
}, []) }, [])
const previewNode = const customMaterial = useMemo(() => getRoofMaterialArray(node), [node])
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 const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
@@ -8,7 +8,6 @@ import {
createMaterial, createMaterial,
DEFAULT_SLAB_MATERIAL, DEFAULT_SLAB_MATERIAL,
} from '../../../lib/materials' } from '../../../lib/materials'
import useViewer from '../../../store/use-viewer'
const slabMaterialCache = new Map<string, THREE.MeshStandardMaterial>() const slabMaterialCache = new Map<string, THREE.MeshStandardMaterial>()
@@ -52,15 +51,10 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
useRegistry(node.id, 'slab', ref) useRegistry(node.id, 'slab', ref)
const handlers = useNodeEvents(node, 'slab') 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 material = useMemo(() => {
const resolvedMaterial = materialPreview?.material ?? node.material const resolvedMaterial = node.material
const resolvedMaterialPreset = materialPreview?.materialPreset ?? node.materialPreset const resolvedMaterialPreset = node.materialPreset
const cacheKey = JSON.stringify({ const cacheKey = JSON.stringify({
material: resolvedMaterial ?? null, material: resolvedMaterial ?? null,
materialPreset: resolvedMaterialPreset ?? null, materialPreset: resolvedMaterialPreset ?? null,
@@ -76,11 +70,6 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
node.material?.properties, node.material?.properties,
node.material?.texture, node.material?.texture,
node.materialPreset, node.materialPreset,
materialPreview?.material,
materialPreview?.material?.preset,
materialPreview?.material?.properties,
materialPreview?.material?.texture,
materialPreview?.materialPreset,
]) ])
return ( return (
@@ -8,7 +8,6 @@ import {
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react' import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
import * as THREE from 'three' import * as THREE from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events' import { useNodeEvents } from '../../../hooks/use-node-events'
import useViewer from '../../../store/use-viewer'
import { getStraightStairSegmentBodyMaterials } from '../../../systems/stair/stair-materials' import { getStraightStairSegmentBodyMaterials } from '../../../systems/stair/stair-materials'
export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => { export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => {
@@ -25,42 +24,10 @@ export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => {
const parentNode = node.parentId const parentNode = node.parentId
? (nodes[node.parentId as AnyNodeId] as StairNode | undefined) ? (nodes[node.parentId as AnyNodeId] as StairNode | undefined)
: undefined : undefined
const materialPreview = useViewer((state) => const material = useMemo(
state.materialPreview?.target === 'stair' && state.materialPreview.nodeId === parentNode?.id () => getStraightStairSegmentBodyMaterials(node, parentNode),
? state.materialPreview [node, parentNode],
: 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, previewParentNode)
}, [node, previewParentNode])
const placeholderGeometry = useMemo(() => { const placeholderGeometry = useMemo(() => {
const geometry = new THREE.BufferGeometry() const geometry = new THREE.BufferGeometry()
@@ -13,7 +13,6 @@ import {
createMaterialFromPresetRef, createMaterialFromPresetRef,
DEFAULT_STAIR_MATERIAL, DEFAULT_STAIR_MATERIAL,
} from '../../../lib/materials' } from '../../../lib/materials'
import useViewer from '../../../store/use-viewer'
import { import {
getStairBodyMaterials, getStairBodyMaterials,
getStairRailingMaterial, getStairRailingMaterial,
@@ -62,55 +61,24 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
}, [node.id]) }, [node.id])
const handlers = useNodeEvents(node, 'stair') 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 material = useMemo(() => {
const presetMaterial = createMaterialFromPresetRef(previewNode.materialPreset) const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial if (presetMaterial) return presetMaterial
const mat = previewNode.material const mat = node.material
if (!mat) return DEFAULT_STAIR_MATERIAL if (!mat) return DEFAULT_STAIR_MATERIAL
return createMaterial(mat) return createMaterial(mat)
}, [ }, [
previewNode.materialPreset, node.materialPreset,
previewNode.material, node.material,
previewNode.material?.preset, node.material?.preset,
previewNode.material?.properties, node.material?.properties,
previewNode.material?.texture, node.material?.texture,
]) ])
const straightBodyMaterials = useMemo(() => getStairBodyMaterials(previewNode), [previewNode]) const straightBodyMaterials = useMemo(() => getStairBodyMaterials(node), [node])
const railingMaterial = useMemo(() => getStairRailingMaterial(previewNode), [previewNode]) const railingMaterial = useMemo(() => getStairRailingMaterial(node), [node])
const straightPlaceholderGeometry = useMemo(() => { const straightPlaceholderGeometry = useMemo(() => {
const geometry = new THREE.BufferGeometry() const geometry = new THREE.BufferGeometry()
@@ -2,7 +2,6 @@ import { useRegistry, useScene, type WallNode } from '@pascal-app/core'
import { useLayoutEffect, useRef } from 'react' import { useLayoutEffect, useRef } from 'react'
import type { Mesh } from 'three' import type { Mesh } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events' import { useNodeEvents } from '../../../hooks/use-node-events'
import useViewer from '../../../store/use-viewer'
import { getVisibleWallMaterials } from '../../../systems/wall/wall-materials' import { getVisibleWallMaterials } from '../../../systems/wall/wall-materials'
import { NodeRenderer } from '../node-renderer' import { NodeRenderer } from '../node-renderer'
@@ -16,30 +15,7 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
}, [node.id]) }, [node.id])
const handlers = useNodeEvents(node, 'wall') const handlers = useNodeEvents(node, 'wall')
const materialPreview = useViewer((state) => const material = getVisibleWallMaterials(node)
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 ( return (
<mesh castShadow material={material} receiveShadow ref={ref} visible={node.visible}> <mesh castShadow material={material} receiveShadow ref={ref} visible={node.visible}>
@@ -26,7 +26,7 @@ import { SceneRenderer } from '../renderers/scene-renderer'
import FrameLimiter from './frame-limiter' import FrameLimiter from './frame-limiter'
import { Lights } from './lights' import { Lights } from './lights'
import { PerfMonitor } from './perf-monitor' import { PerfMonitor } from './perf-monitor'
import PostProcessing from './post-processing' import PostProcessing, { DEFAULT_HOVER_STYLES, type HoverStyles } from './post-processing'
import { SelectionManager } from './selection-manager' import { SelectionManager } from './selection-manager'
import { ViewerCamera } from './viewer-camera' import { ViewerCamera } from './viewer-camera'
@@ -101,12 +101,14 @@ function GPUDeviceWatcher() {
interface ViewerProps { interface ViewerProps {
children?: React.ReactNode children?: React.ReactNode
hoverStyles?: HoverStyles
selectionManager?: 'default' | 'custom' selectionManager?: 'default' | 'custom'
perf?: boolean perf?: boolean
} }
const Viewer: React.FC<ViewerProps> = ({ const Viewer: React.FC<ViewerProps> = ({
children, children,
hoverStyles = DEFAULT_HOVER_STYLES,
selectionManager = 'default', selectionManager = 'default',
perf = false, perf = false,
}) => { }) => {
@@ -165,7 +167,7 @@ const Viewer: React.FC<ViewerProps> = ({
<WallSystem /> <WallSystem />
<WindowSystem /> <WindowSystem />
<ZoneSystem /> <ZoneSystem />
<PostProcessing /> <PostProcessing hoverStyles={hoverStyles} />
{/* <DebugRenderer /> */} {/* <DebugRenderer /> */}
<GPUDeviceWatcher /> <GPUDeviceWatcher />
@@ -47,6 +47,46 @@ const RETRY_DELAY_MS = 500
const DARK_BG = '#1f2433' const DARK_BG = '#1f2433'
const LIGHT_BG = '#ffffff' const LIGHT_BG = '#ffffff'
export type HoverStyle = {
visibleColor: number
hiddenColor: number
strength: number
pulse: boolean
}
export type HoverStyles = {
default: HoverStyle
} & Record<string, HoverStyle>
const DEFAULT_HOVER_STYLE: HoverStyle = {
visibleColor: 0x00_aa_ff,
hiddenColor: 0xf3_ff_47,
strength: 5,
pulse: true,
}
export const DEFAULT_HOVER_STYLES: HoverStyles = {
default: DEFAULT_HOVER_STYLE,
delete: {
visibleColor: 0xef_44_44,
hiddenColor: 0x99_1b_1b,
strength: 6,
pulse: false,
},
'paint-ready': {
visibleColor: 0xf5_9e_0b,
hiddenColor: 0xfd_e0_68,
strength: 5,
pulse: true,
},
'paint-disabled': {
visibleColor: 0x94_a3_b8,
hiddenColor: 0x47_55_69,
strength: 4,
pulse: false,
},
}
function sanitizeOutlineObjects(objects: Object3D[]) { function sanitizeOutlineObjects(objects: Object3D[]) {
let nextIndex = 0 let nextIndex = 0
@@ -62,7 +102,11 @@ function sanitizeOutlineObjects(objects: Object3D[]) {
objects.length = nextIndex objects.length = nextIndex
} }
const PostProcessingPasses = () => { const PostProcessingPasses = ({
hoverStyles = DEFAULT_HOVER_STYLES,
}: {
hoverStyles?: HoverStyles
}) => {
const { gl: renderer, scene, camera } = useThree() const { gl: renderer, scene, camera } = useThree()
const renderPipelineRef = useRef<RenderPipeline | null>(null) const renderPipelineRef = useRef<RenderPipeline | null>(null)
const hasPipelineErrorRef = useRef(false) const hasPipelineErrorRef = useRef(false)
@@ -83,6 +127,10 @@ const PostProcessingPasses = () => {
return l return l
}, []) }, [])
const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode) const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode)
const hoverVisibleColor = useMemo(() => uniform(new Color(DEFAULT_HOVER_STYLE.visibleColor)), [])
const hoverHiddenColor = useMemo(() => uniform(new Color(DEFAULT_HOVER_STYLE.hiddenColor)), [])
const hoverStrength = useMemo(() => uniform(DEFAULT_HOVER_STYLE.strength), [])
const hoverPulseMix = useMemo(() => uniform(DEFAULT_HOVER_STYLE.pulse ? 0 : 1), [])
// Subscribe to projectId so the pipeline rebuilds on project switch // Subscribe to projectId so the pipeline rebuilds on project switch
const projectId = useViewer((s) => s.projectId) const projectId = useViewer((s) => s.projectId)
@@ -119,6 +167,21 @@ const PostProcessingPasses = () => {
} }
}, []) }, [])
useEffect(() => {
const style = hoverStyles[hoverHighlightMode] ?? hoverStyles.default
hoverVisibleColor.value.setHex(style.visibleColor)
hoverHiddenColor.value.setHex(style.hiddenColor)
hoverStrength.value = style.strength
hoverPulseMix.value = style.pulse ? 0 : 1
}, [
hoverHiddenColor,
hoverHighlightMode,
hoverPulseMix,
hoverStrength,
hoverStyles,
hoverVisibleColor,
])
// Build / rebuild the post-processing pipeline // Build / rebuild the post-processing pipeline
useEffect(() => { useEffect(() => {
// Intentionally touch these so React/biome treat project switches and retry bumps // Intentionally touch these so React/biome treat project switches and retry bumps
@@ -248,36 +311,9 @@ const PostProcessingPasses = () => {
.mul(selectedStrength) .mul(selectedStrength)
// Hovered: blue visible, yellow hidden, pulsing // Hovered: blue visible, yellow hidden, pulsing
const hoverVisibleColor = uniform(
new Color(
hoverHighlightMode === 'delete'
? 0xef_44_44
: hoverHighlightMode === 'paint-ready'
? 0xf59e_0b
: hoverHighlightMode === 'paint-disabled'
? 0x94_a3_b8
: 0x00_aa_ff,
),
)
const hoverHiddenColor = uniform(
new Color(
hoverHighlightMode === 'delete'
? 0x99_1b_1b
: hoverHighlightMode === 'paint-ready'
? 0xfde0_68
: hoverHighlightMode === 'paint-disabled'
? 0x47_55_69
: 0xf3_ff_47,
),
)
const hoverStrength = uniform(
hoverHighlightMode === 'delete' ? 6 : hoverHighlightMode === 'paint-disabled' ? 4 : 5,
)
const pulsePeriod = uniform(3) const pulsePeriod = uniform(3)
const osc = const oscillating = oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5)
hoverHighlightMode === 'delete' || hoverHighlightMode === 'paint-disabled' const osc = mix(oscillating, float(1), hoverPulseMix)
? float(1)
: oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5) // [ 0.5, 1.0 ]
const hoverOutline = outlineNode.secondaryVisibleEdge const hoverOutline = outlineNode.secondaryVisibleEdge
.mul(hoverVisibleColor) .mul(hoverVisibleColor)
.add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor)) .add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor))
@@ -316,7 +352,18 @@ const PostProcessingPasses = () => {
} }
renderPipelineRef.current = null renderPipelineRef.current = null
} }
}, [renderer, scene, camera, hoverHighlightMode, zoneLayers, projectId, pipelineVersion]) }, [
camera,
hoverHiddenColor,
hoverPulseMix,
hoverStrength,
hoverVisibleColor,
pipelineVersion,
projectId,
renderer,
scene,
zoneLayers,
])
useFrame((_, delta) => { useFrame((_, delta) => {
// Animate background colour toward the current theme target (same lerp as AnimatedBackground) // Animate background colour toward the current theme target (same lerp as AnimatedBackground)
+10 -1
View File
@@ -1,12 +1,18 @@
export { default as Viewer } from './components/viewer' export { default as Viewer } from './components/viewer'
export { SSGI_PARAMS } from './components/viewer/post-processing' export type { HoverStyle, HoverStyles } from './components/viewer/post-processing'
export {
DEFAULT_HOVER_STYLES,
SSGI_PARAMS,
} from './components/viewer/post-processing'
export { WalkthroughControls } from './components/viewer/walkthrough-controls' export { WalkthroughControls } from './components/viewer/walkthrough-controls'
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url' export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url'
export { SCENE_LAYER, ZONE_LAYER } from './lib/layers' export { SCENE_LAYER, ZONE_LAYER } from './lib/layers'
export { export {
applyMaterialPresetToMaterials,
clearMaterialCache, clearMaterialCache,
createDefaultMaterial, createDefaultMaterial,
createMaterial, createMaterial,
createMaterialFromPresetRef,
DEFAULT_CEILING_MATERIAL, DEFAULT_CEILING_MATERIAL,
DEFAULT_DOOR_MATERIAL, DEFAULT_DOOR_MATERIAL,
DEFAULT_ROOF_MATERIAL, DEFAULT_ROOF_MATERIAL,
@@ -19,3 +25,6 @@ export { mergedOutline } from './lib/merged-outline-node'
export { default as useViewer } from './store/use-viewer' export { default as useViewer } from './store/use-viewer'
export { InteractiveSystem } from './systems/interactive/interactive-system' export { InteractiveSystem } from './systems/interactive/interactive-system'
export { snapLevelsToTruePositions } from './systems/level/level-utils' export { snapLevelsToTruePositions } from './systems/level/level-utils'
export { getRoofMaterialArray } from './systems/roof/roof-materials'
export { getStairBodyMaterials, getStairRailingMaterial } from './systems/stair/stair-materials'
export { getVisibleWallMaterials } from './systems/wall/wall-materials'
+2 -34
View File
@@ -3,10 +3,6 @@ import type {
BaseNode, BaseNode,
BuildingNode, BuildingNode,
LevelNode, LevelNode,
MaterialSchema,
RoofSurfaceMaterialRole,
StairSurfaceMaterialRole,
WallSurfaceSide,
ZoneNode, ZoneNode,
} from '@pascal-app/core' } from '@pascal-app/core'
import type { Object3D } from 'three' import type { Object3D } from 'three'
@@ -20,40 +16,12 @@ type Outliner = {
selectedObjects: Object3D[] selectedObjects: Object3D[]
hoveredObjects: 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 = { type ViewerState = {
selection: SelectionPath selection: SelectionPath
previewSelectedIds: BaseNode['id'][] previewSelectedIds: BaseNode['id'][]
setPreviewSelectedIds: (ids: BaseNode['id'][]) => void setPreviewSelectedIds: (ids: BaseNode['id'][]) => void
materialPreview: MaterialPreview hoverHighlightMode: string
setMaterialPreview: (preview: MaterialPreview) => void setHoverHighlightMode: (mode: string) => 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 hoveredId: AnyNode['id'] | ZoneNode['id'] | null
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
cameraMode: 'perspective' | 'orthographic' cameraMode: 'perspective' | 'orthographic'
+3 -71
View File
@@ -1,17 +1,6 @@
'use client' 'use client'
import type { import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core'
AnyNode,
AnyNodeId,
BaseNode,
BuildingNode,
LevelNode,
MaterialSchema,
RoofSurfaceMaterialRole,
StairSurfaceMaterialRole,
WallSurfaceSide,
ZoneNode,
} from '@pascal-app/core'
import type { Object3D } from 'three' import type { Object3D } from 'three'
import { create } from 'zustand' import { create } from 'zustand'
@@ -29,59 +18,12 @@ type Outliner = {
hoveredObjects: Object3D[] 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 = { type ViewerState = {
selection: SelectionPath selection: SelectionPath
previewSelectedIds: BaseNode['id'][] previewSelectedIds: BaseNode['id'][]
setPreviewSelectedIds: (ids: BaseNode['id'][]) => void setPreviewSelectedIds: (ids: BaseNode['id'][]) => void
materialPreview: MaterialPreview hoverHighlightMode: string
setMaterialPreview: (preview: MaterialPreview) => void setHoverHighlightMode: (mode: string) => 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 hoveredId: AnyNode['id'] | ZoneNode['id'] | null
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
@@ -142,15 +84,6 @@ const useViewer = create<ViewerState>()(
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] }, selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
previewSelectedIds: [], previewSelectedIds: [],
setPreviewSelectedIds: (ids) => set({ previewSelectedIds: ids }), 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', hoverHighlightMode: 'default',
setHoverHighlightMode: (mode) => setHoverHighlightMode: (mode) =>
set((state) => (state.hoverHighlightMode === mode ? state : { hoverHighlightMode: mode })), set((state) => (state.hoverHighlightMode === mode ? state : { hoverHighlightMode: mode })),
@@ -255,7 +188,6 @@ const useViewer = create<ViewerState>()(
selectedIds: [], selectedIds: [],
}, },
previewSelectedIds: [], previewSelectedIds: [],
materialPreview: null,
}), }),
outliner: { selectedObjects: [], hoveredObjects: [] }, outliner: { selectedObjects: [], hoveredObjects: [] },