diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx
index b61644af..a7591d90 100644
--- a/packages/editor/src/components/editor/index.tsx
+++ b/packages/editor/src/components/editor/index.tsx
@@ -7,7 +7,7 @@ import {
spatialGridManager,
useScene,
} from '@pascal-app/core'
-import { InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
+import { type HoverStyles, InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
import {
memo,
type ReactNode,
@@ -75,6 +75,17 @@ const PAINT_CURSOR_BADGE_COLOR = '#f59e0b'
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
const PAINT_CURSOR_BADGE_OFFSET_X = 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
@@ -885,7 +896,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
/>
) : null}
-
+
+
diff --git a/packages/editor/src/components/editor/selection-manager.tsx b/packages/editor/src/components/editor/selection-manager.tsx
index a53c870d..bf1fd870 100755
--- a/packages/editor/src/components/editor/selection-manager.tsx
+++ b/packages/editor/src/components/editor/selection-manager.tsx
@@ -25,20 +25,18 @@ import {
type WallSurfaceSide,
} 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 {
applyMaterialPresetToMaterials,
createMaterial,
createMaterialFromPresetRef,
-} from '../../../../viewer/src/lib/materials'
-import { getRoofMaterialArray } from '../../../../viewer/src/systems/roof/roof-materials'
-import {
+ getRoofMaterialArray,
getStairBodyMaterials,
getStairRailingMaterial,
-} from '../../../../viewer/src/systems/stair/stair-materials'
-import { getVisibleWallMaterials } from '../../../../viewer/src/systems/wall/wall-materials'
+ getVisibleWallMaterials,
+ useViewer,
+} from '@pascal-app/viewer'
+import { useCallback, useEffect, useRef } from 'react'
+import { type BufferGeometry, Color, type Material, type Mesh, type Object3D } from 'three'
import {
type ActivePaintMaterial,
buildRoofSurfaceMaterialPatch,
diff --git a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx
index 398abf54..91e7487c 100644
--- a/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx
+++ b/packages/viewer/src/components/renderers/ceiling/ceiling-renderer.tsx
@@ -8,7 +8,6 @@ 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
@@ -55,16 +54,10 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
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(materialPreview?.materialPreset ?? node.materialPreset)
- const props =
- preset?.mapProperties ?? resolveMaterial(materialPreview?.material ?? node.material)
+ const preset = getMaterialPresetByRef(node.materialPreset)
+ const props = preset?.mapProperties ?? resolveMaterial(node.material)
const color = props.color || '#999999'
return getCeilingMaterials(color)
}, [
@@ -73,11 +66,6 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
node.material?.preset,
node.material?.properties,
node.material?.texture,
- materialPreview?.materialPreset,
- materialPreview?.material,
- materialPreview?.material?.preset,
- materialPreview?.material?.properties,
- materialPreview?.material?.texture,
])
return (
diff --git a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx
index c5448ada..4e563572 100644
--- a/packages/viewer/src/components/renderers/fence/fence-renderer.tsx
+++ b/packages/viewer/src/components/renderers/fence/fence-renderer.tsx
@@ -7,22 +7,14 @@ import {
createMaterialFromPresetRef,
DEFAULT_STAIR_MATERIAL,
} from '../../../lib/materials'
-import useViewer from '../../../store/use-viewer'
export const FenceRenderer = ({ node }: { node: FenceNode }) => {
const ref = useRef(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(
- materialPreview?.materialPreset ?? node.materialPreset,
- )
+ const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial
- const mat = materialPreview?.material ?? node.material
+ const mat = node.material
if (!mat) return DEFAULT_STAIR_MATERIAL
return createMaterial(mat)
}, [
@@ -31,11 +23,6 @@ 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)
diff --git a/packages/viewer/src/components/renderers/roof-segment/roof-segment-renderer.tsx b/packages/viewer/src/components/renderers/roof-segment/roof-segment-renderer.tsx
index daebf9fb..8bd052fa 100644
--- a/packages/viewer/src/components/renderers/roof-segment/roof-segment-renderer.tsx
+++ b/packages/viewer/src/components/renderers/roof-segment/roof-segment-renderer.tsx
@@ -23,11 +23,6 @@ export const RoofSegmentRenderer = ({ node }: { node: RoofSegmentNode }) => {
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))
@@ -38,40 +33,13 @@ 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 previewParentNode ? getRoofMaterialArray(previewParentNode) : null
- }, [node, previewParentNode])
+ return parentNode ? getRoofMaterialArray(parentNode) : null
+ }, [node, parentNode])
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
diff --git a/packages/viewer/src/components/renderers/roof/roof-renderer.tsx b/packages/viewer/src/components/renderers/roof/roof-renderer.tsx
index 4a4eb866..1dee2a98 100644
--- a/packages/viewer/src/components/renderers/roof/roof-renderer.tsx
+++ b/packages/viewer/src/components/renderers/roof/roof-renderer.tsx
@@ -14,11 +14,6 @@ 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))
@@ -29,33 +24,7 @@ export const RoofRenderer = ({ node }: { node: RoofNode }) => {
return geometry
}, [])
- 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 customMaterial = useMemo(() => getRoofMaterialArray(node), [node])
const material = debugColors ? roofDebugMaterials : customMaterial || roofMaterials
diff --git a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx
index 0e97f23d..e2b935f6 100644
--- a/packages/viewer/src/components/renderers/slab/slab-renderer.tsx
+++ b/packages/viewer/src/components/renderers/slab/slab-renderer.tsx
@@ -8,7 +8,6 @@ import {
createMaterial,
DEFAULT_SLAB_MATERIAL,
} from '../../../lib/materials'
-import useViewer from '../../../store/use-viewer'
const slabMaterialCache = new Map()
@@ -52,15 +51,10 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
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 resolvedMaterial = node.material
+ const resolvedMaterialPreset = node.materialPreset
const cacheKey = JSON.stringify({
material: resolvedMaterial ?? null,
materialPreset: resolvedMaterialPreset ?? null,
@@ -76,11 +70,6 @@ export const SlabRenderer = ({ node }: { node: SlabNode }) => {
node.material?.properties,
node.material?.texture,
node.materialPreset,
- materialPreview?.material,
- materialPreview?.material?.preset,
- materialPreview?.material?.properties,
- materialPreview?.material?.texture,
- materialPreview?.materialPreset,
])
return (
diff --git a/packages/viewer/src/components/renderers/stair-segment/stair-segment-renderer.tsx b/packages/viewer/src/components/renderers/stair-segment/stair-segment-renderer.tsx
index 1c27a364..73c834ea 100644
--- a/packages/viewer/src/components/renderers/stair-segment/stair-segment-renderer.tsx
+++ b/packages/viewer/src/components/renderers/stair-segment/stair-segment-renderer.tsx
@@ -8,7 +8,6 @@ import {
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 }) => {
@@ -25,42 +24,10 @@ export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => {
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 material = useMemo(
+ () => getStraightStairSegmentBodyMaterials(node, parentNode),
+ [node, parentNode],
)
- 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 geometry = new THREE.BufferGeometry()
diff --git a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx
index 4bc8821a..d05ed49c 100644
--- a/packages/viewer/src/components/renderers/stair/stair-renderer.tsx
+++ b/packages/viewer/src/components/renderers/stair/stair-renderer.tsx
@@ -13,7 +13,6 @@ import {
createMaterialFromPresetRef,
DEFAULT_STAIR_MATERIAL,
} from '../../../lib/materials'
-import useViewer from '../../../store/use-viewer'
import {
getStairBodyMaterials,
getStairRailingMaterial,
@@ -62,55 +61,24 @@ 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(previewNode.materialPreset)
+ const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
if (presetMaterial) return presetMaterial
- const mat = previewNode.material
+ const mat = node.material
if (!mat) return DEFAULT_STAIR_MATERIAL
return createMaterial(mat)
}, [
- previewNode.materialPreset,
- previewNode.material,
- previewNode.material?.preset,
- previewNode.material?.properties,
- previewNode.material?.texture,
+ node.materialPreset,
+ node.material,
+ node.material?.preset,
+ node.material?.properties,
+ 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 geometry = new THREE.BufferGeometry()
diff --git a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
index a61fbb82..9ff38006 100644
--- a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
+++ b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx
@@ -2,7 +2,6 @@ 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'
@@ -16,30 +15,7 @@ export const WallRenderer = ({ node }: { node: WallNode }) => {
}, [node.id])
const handlers = useNodeEvents(node, 'wall')
- 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)
+ const material = getVisibleWallMaterials(node)
return (
diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx
index 42182d9a..6ee40f54 100644
--- a/packages/viewer/src/components/viewer/index.tsx
+++ b/packages/viewer/src/components/viewer/index.tsx
@@ -26,7 +26,7 @@ import { SceneRenderer } from '../renderers/scene-renderer'
import FrameLimiter from './frame-limiter'
import { Lights } from './lights'
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 { ViewerCamera } from './viewer-camera'
@@ -101,12 +101,14 @@ function GPUDeviceWatcher() {
interface ViewerProps {
children?: React.ReactNode
+ hoverStyles?: HoverStyles
selectionManager?: 'default' | 'custom'
perf?: boolean
}
const Viewer: React.FC = ({
children,
+ hoverStyles = DEFAULT_HOVER_STYLES,
selectionManager = 'default',
perf = false,
}) => {
@@ -165,7 +167,7 @@ const Viewer: React.FC = ({
-
+
{/* */}
diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx
index 63217975..253fb927 100644
--- a/packages/viewer/src/components/viewer/post-processing.tsx
+++ b/packages/viewer/src/components/viewer/post-processing.tsx
@@ -47,6 +47,46 @@ const RETRY_DELAY_MS = 500
const DARK_BG = '#1f2433'
const LIGHT_BG = '#ffffff'
+export type HoverStyle = {
+ visibleColor: number
+ hiddenColor: number
+ strength: number
+ pulse: boolean
+}
+
+export type HoverStyles = {
+ default: HoverStyle
+} & Record
+
+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[]) {
let nextIndex = 0
@@ -62,7 +102,11 @@ function sanitizeOutlineObjects(objects: Object3D[]) {
objects.length = nextIndex
}
-const PostProcessingPasses = () => {
+const PostProcessingPasses = ({
+ hoverStyles = DEFAULT_HOVER_STYLES,
+}: {
+ hoverStyles?: HoverStyles
+}) => {
const { gl: renderer, scene, camera } = useThree()
const renderPipelineRef = useRef(null)
const hasPipelineErrorRef = useRef(false)
@@ -83,6 +127,10 @@ const PostProcessingPasses = () => {
return l
}, [])
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
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
useEffect(() => {
// Intentionally touch these so React/biome treat project switches and retry bumps
@@ -248,36 +311,9 @@ const PostProcessingPasses = () => {
.mul(selectedStrength)
// 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 osc =
- hoverHighlightMode === 'delete' || hoverHighlightMode === 'paint-disabled'
- ? float(1)
- : oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5) // [ 0.5, 1.0 ]
+ const oscillating = oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5)
+ const osc = mix(oscillating, float(1), hoverPulseMix)
const hoverOutline = outlineNode.secondaryVisibleEdge
.mul(hoverVisibleColor)
.add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor))
@@ -316,7 +352,18 @@ const PostProcessingPasses = () => {
}
renderPipelineRef.current = null
}
- }, [renderer, scene, camera, hoverHighlightMode, zoneLayers, projectId, pipelineVersion])
+ }, [
+ camera,
+ hoverHiddenColor,
+ hoverPulseMix,
+ hoverStrength,
+ hoverVisibleColor,
+ pipelineVersion,
+ projectId,
+ renderer,
+ scene,
+ zoneLayers,
+ ])
useFrame((_, delta) => {
// Animate background colour toward the current theme target (same lerp as AnimatedBackground)
diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts
index 799e1312..e867a444 100644
--- a/packages/viewer/src/index.ts
+++ b/packages/viewer/src/index.ts
@@ -1,12 +1,18 @@
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 { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url'
export { SCENE_LAYER, ZONE_LAYER } from './lib/layers'
export {
+ applyMaterialPresetToMaterials,
clearMaterialCache,
createDefaultMaterial,
createMaterial,
+ createMaterialFromPresetRef,
DEFAULT_CEILING_MATERIAL,
DEFAULT_DOOR_MATERIAL,
DEFAULT_ROOF_MATERIAL,
@@ -19,3 +25,6 @@ export { mergedOutline } from './lib/merged-outline-node'
export { default as useViewer } from './store/use-viewer'
export { InteractiveSystem } from './systems/interactive/interactive-system'
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'
diff --git a/packages/viewer/src/store/use-viewer.d.ts b/packages/viewer/src/store/use-viewer.d.ts
index 64586624..1342032a 100644
--- a/packages/viewer/src/store/use-viewer.d.ts
+++ b/packages/viewer/src/store/use-viewer.d.ts
@@ -3,10 +3,6 @@ import type {
BaseNode,
BuildingNode,
LevelNode,
- MaterialSchema,
- RoofSurfaceMaterialRole,
- StairSurfaceMaterialRole,
- WallSurfaceSide,
ZoneNode,
} from '@pascal-app/core'
import type { Object3D } from 'three'
@@ -20,40 +16,12 @@ 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
- materialPreview: MaterialPreview
- setMaterialPreview: (preview: MaterialPreview) => void
- clearMaterialPreview: () => void
- hoverHighlightMode: 'default' | 'delete' | 'paint-ready' | 'paint-disabled'
- setHoverHighlightMode: (mode: 'default' | 'delete' | 'paint-ready' | 'paint-disabled') => void
+ hoverHighlightMode: string
+ setHoverHighlightMode: (mode: string) => void
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
cameraMode: 'perspective' | 'orthographic'
diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts
index c431ab71..00649a63 100644
--- a/packages/viewer/src/store/use-viewer.ts
+++ b/packages/viewer/src/store/use-viewer.ts
@@ -1,17 +1,6 @@
'use client'
-import type {
- AnyNode,
- AnyNodeId,
- BaseNode,
- BuildingNode,
- LevelNode,
- MaterialSchema,
- RoofSurfaceMaterialRole,
- StairSurfaceMaterialRole,
- WallSurfaceSide,
- ZoneNode,
-} from '@pascal-app/core'
+import type { AnyNode, BaseNode, BuildingNode, LevelNode, ZoneNode } from '@pascal-app/core'
import type { Object3D } from 'three'
import { create } from 'zustand'
@@ -29,59 +18,12 @@ 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
+ hoverHighlightMode: string
+ setHoverHighlightMode: (mode: string) => void
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
@@ -142,15 +84,6 @@ const useViewer = create()(
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 })),
@@ -255,7 +188,6 @@ const useViewer = create()(
selectedIds: [],
},
previewSelectedIds: [],
- materialPreview: null,
}),
outliner: { selectedObjects: [], hoveredObjects: [] },