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 { Html } from '@react-three/drei'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three'
|
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 { MeshBasicNodeMaterial } from 'three/webgpu'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
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)
|
// Use UV y coordinate for vertical gradient (0 at bottom, 1 at top)
|
||||||
const gradientT = uv().y
|
const gradientT = uv().y
|
||||||
|
|
||||||
|
const opacity = uniform(0);
|
||||||
// Fade opacity from 0.6 at bottom to 0 at top
|
// 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({
|
return new MeshBasicNodeMaterial({
|
||||||
transparent: true,
|
transparent: true,
|
||||||
colorNode: baseColor,
|
colorNode: baseColor,
|
||||||
opacityNode: opacity,
|
opacityNode: finalOpacity,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
|
userData: {
|
||||||
|
uOpacity: opacity,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,13 +41,15 @@ const createWallGradientMaterial = (zoneColor: string) => {
|
|||||||
*/
|
*/
|
||||||
const createFloorMaterial = (zoneColor: string) => {
|
const createFloorMaterial = (zoneColor: string) => {
|
||||||
const baseColor = color(new Color(zoneColor))
|
const baseColor = color(new Color(zoneColor))
|
||||||
|
const opacity = uniform(0)
|
||||||
return new MeshBasicNodeMaterial({
|
return new MeshBasicNodeMaterial({
|
||||||
transparent: true,
|
transparent: true,
|
||||||
colorNode: baseColor,
|
colorNode: baseColor,
|
||||||
opacityNode: float(0.15),
|
opacityNode: float(0.25).mul(opacity),
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
|
depthTest: false,
|
||||||
|
userData: { uOpacity: opacity}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,12 +193,12 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
|||||||
{node.name}</div>
|
{node.name}</div>
|
||||||
</Html>
|
</Html>
|
||||||
{/* Floor fill */}
|
{/* 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]} />
|
<shapeGeometry args={[floorShape]} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Wall borders with gradient */}
|
{/* Wall borders with gradient */}
|
||||||
<mesh geometry={wallGeometry} material={wallMaterial} />
|
<mesh geometry={wallGeometry} material={wallMaterial} name="walls" />
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { GuideSystem } from '../../systems/guide/guide-system'
|
|||||||
import { LevelSystem } from '../../systems/level/level-system'
|
import { LevelSystem } from '../../systems/level/level-system'
|
||||||
import { ScanSystem } from '../../systems/scan/scan-system'
|
import { ScanSystem } from '../../systems/scan/scan-system'
|
||||||
import { WallCutout } from '../../systems/wall/wall-cutout'
|
import { WallCutout } from '../../systems/wall/wall-cutout'
|
||||||
|
import { ZoneSystem } from '../../systems/zone/zone-system'
|
||||||
import { SceneRenderer } from '../renderers/scene-renderer'
|
import { SceneRenderer } from '../renderers/scene-renderer'
|
||||||
import { Lights } from './lights'
|
import { Lights } from './lights'
|
||||||
import PostProcessing from './post-processing'
|
import PostProcessing from './post-processing'
|
||||||
@@ -63,6 +64,7 @@ const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default'
|
|||||||
<RoofSystem />
|
<RoofSystem />
|
||||||
<SlabSystem />
|
<SlabSystem />
|
||||||
<WallSystem />
|
<WallSystem />
|
||||||
|
<ZoneSystem />
|
||||||
<PostProcessing />
|
<PostProcessing />
|
||||||
|
|
||||||
{selectionManager === 'default' && <SelectionManager />}
|
{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