diff --git a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx index 96974b36..a8d89bcd 100644 --- a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx +++ b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx @@ -2,7 +2,7 @@ import { useRegistry, type ZoneNode } from '@pascal-app/core' import { Html } from '@react-three/drei' import { useMemo, useRef } from 'react' import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three' -import { color, float, uv } from 'three/tsl' +import { color, float, uniform, uv } from 'three/tsl' import { MeshBasicNodeMaterial } from 'three/webgpu' import { useNodeEvents } from '../../../hooks/use-node-events' @@ -19,16 +19,20 @@ const createWallGradientMaterial = (zoneColor: string) => { // Use UV y coordinate for vertical gradient (0 at bottom, 1 at top) const gradientT = uv().y + const opacity = uniform(0); // Fade opacity from 0.6 at bottom to 0 at top - const opacity = float(0.6).mul(float(1).sub(gradientT)) + const finalOpacity = float(0.6).mul(float(1).sub(gradientT)).mul(opacity); return new MeshBasicNodeMaterial({ transparent: true, colorNode: baseColor, - opacityNode: opacity, + opacityNode: finalOpacity, side: DoubleSide, depthWrite: false, depthTest: false, + userData: { + uOpacity: opacity, + } }) } @@ -37,13 +41,15 @@ const createWallGradientMaterial = (zoneColor: string) => { */ const createFloorMaterial = (zoneColor: string) => { const baseColor = color(new Color(zoneColor)) - + const opacity = uniform(0) return new MeshBasicNodeMaterial({ transparent: true, colorNode: baseColor, - opacityNode: float(0.15), + opacityNode: float(0.25).mul(opacity), side: DoubleSide, depthWrite: false, + depthTest: false, + userData: { uOpacity: opacity} }) } @@ -187,12 +193,12 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => { {node.name} {/* Floor fill */} - + {/* Wall borders with gradient */} - + ) } diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx index df13ebb8..eaeb7535 100644 --- a/packages/viewer/src/components/viewer/index.tsx +++ b/packages/viewer/src/components/viewer/index.tsx @@ -8,6 +8,7 @@ import { GuideSystem } from '../../systems/guide/guide-system' import { LevelSystem } from '../../systems/level/level-system' import { ScanSystem } from '../../systems/scan/scan-system' import { WallCutout } from '../../systems/wall/wall-cutout' +import { ZoneSystem } from '../../systems/zone/zone-system' import { SceneRenderer } from '../renderers/scene-renderer' import { Lights } from './lights' import PostProcessing from './post-processing' @@ -63,6 +64,7 @@ const Viewer: React.FC = ({ children, selectionManager = 'default' + {selectionManager === 'default' && } diff --git a/packages/viewer/src/systems/zone/zone-system.tsx b/packages/viewer/src/systems/zone/zone-system.tsx new file mode 100644 index 00000000..4062fc5b --- /dev/null +++ b/packages/viewer/src/systems/zone/zone-system.tsx @@ -0,0 +1,70 @@ +import { sceneRegistry, useScene } from '@pascal-app/core' +import { useFrame } from '@react-three/fiber' +import { useRef } from 'react' +import { type Group, MathUtils, type Mesh } from 'three' +import type { MeshBasicNodeMaterial } from 'three/webgpu' +import useViewer from '../../store/use-viewer' + +const TRANSITION_DURATION = 400 // ms + +export const ZoneSystem = () => { + const lastHighlightedZoneRef = useRef(null) + const lastChangeTimeRef = useRef(0) + const isTransitioningRef = useRef(false) + + useFrame(({clock}, delta) => { + const hoveredId = useViewer.getState().hoveredId + let highlightedZone: string | null = null + + if (hoveredId) { + const hoveredNode = useScene.getState().nodes[hoveredId] + if (hoveredNode?.type === 'zone') { + highlightedZone = hoveredId + } + } + + // Detect zone change + if (highlightedZone !== lastHighlightedZoneRef.current) { + lastHighlightedZoneRef.current = highlightedZone + lastChangeTimeRef.current = clock.elapsedTime * 1000 + isTransitioningRef.current = true + } + + // Skip frame if not transitioning + if (!isTransitioningRef.current) return + + const elapsed = clock.elapsedTime * 1000 - lastChangeTimeRef.current + + // Stop transitioning after duration + if (elapsed >= TRANSITION_DURATION) { + isTransitioningRef.current = false + } + + // Lerp speed: complete transition in ~400ms + const lerpSpeed = 10 * delta + + sceneRegistry.byType.zone.forEach((zoneId) => { + const zone = sceneRegistry.nodes.get(zoneId) + if (!zone) return + + const isHighlighted = zoneId === highlightedZone + const targetOpacity = isHighlighted ? 1 : 0 + + const walls = (zone as Group).getObjectByName('walls') as Mesh | undefined + if (walls) { + const material = walls.material as MeshBasicNodeMaterial + const currentOpacity = material.userData.uOpacity.value + material.userData.uOpacity.value = MathUtils.lerp(currentOpacity, targetOpacity, lerpSpeed) + } + + const floor = (zone as Group).getObjectByName('floor') as Mesh | undefined + if (floor) { + const material = floor.material as MeshBasicNodeMaterial + const currentOpacity = material.userData.uOpacity.value + material.userData.uOpacity.value = MathUtils.lerp(currentOpacity, targetOpacity, lerpSpeed) + } + }) + }) + + return null +}