hovered zones system
This commit is contained in:
@@ -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}</div>
|
||||
</Html>
|
||||
{/* Floor fill */}
|
||||
<mesh position={[0, Y_OFFSET, 0]} rotation={[-Math.PI / 2, 0, 0]} material={floorMaterial}>
|
||||
<mesh position={[0, Y_OFFSET, 0]} rotation={[-Math.PI / 2, 0, 0]} material={floorMaterial} name="floor">
|
||||
<shapeGeometry args={[floorShape]} />
|
||||
</mesh>
|
||||
|
||||
{/* Wall borders with gradient */}
|
||||
<mesh geometry={wallGeometry} material={wallMaterial} />
|
||||
<mesh geometry={wallGeometry} material={wallMaterial} name="walls" />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<ViewerProps> = ({ children, selectionManager = 'default'
|
||||
<RoofSystem />
|
||||
<SlabSystem />
|
||||
<WallSystem />
|
||||
<ZoneSystem />
|
||||
<PostProcessing />
|
||||
|
||||
{selectionManager === 'default' && <SelectionManager />}
|
||||
|
||||
@@ -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<string | null>(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
|
||||
}
|
||||
Reference in New Issue
Block a user