103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
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
|
|
const EXIT_DEBOUNCE_MS = 50 // ignore rapid exit→re-enter within this window
|
|
|
|
export const ZoneSystem = () => {
|
|
const lastHighlightedZoneRef = useRef<string | null>(null)
|
|
const lastChangeTimeRef = useRef(0)
|
|
const isTransitioningRef = useRef(false)
|
|
// Debounce exit-to-null: track the raw pending value and when it last changed
|
|
const pendingZoneRef = useRef<string | null>(null)
|
|
const pendingZoneSinceRef = useRef(0)
|
|
|
|
useFrame(({clock}, delta) => {
|
|
const hoveredId = useViewer.getState().hoveredId
|
|
let rawZone: string | null = null
|
|
|
|
if (hoveredId) {
|
|
const hoveredNode = useScene.getState().nodes[hoveredId]
|
|
if (hoveredNode?.type === 'zone') {
|
|
rawZone = hoveredId
|
|
}
|
|
}
|
|
|
|
// Update pending zone when the raw value changes
|
|
if (rawZone !== pendingZoneRef.current) {
|
|
pendingZoneRef.current = rawZone
|
|
pendingZoneSinceRef.current = clock.elapsedTime * 1000
|
|
}
|
|
|
|
// Apply non-null immediately; debounce null to filter out brief exits
|
|
const age = clock.elapsedTime * 1000 - pendingZoneSinceRef.current
|
|
const highlightedZone = rawZone !== null
|
|
? rawZone
|
|
: age >= EXIT_DEBOUNCE_MS ? null : lastHighlightedZoneRef.current
|
|
|
|
// Detect stable zone change
|
|
if (highlightedZone !== lastHighlightedZoneRef.current) {
|
|
// Fade out previous zone label-pin
|
|
if (lastHighlightedZoneRef.current) {
|
|
const prevLabel = document.getElementById(`${lastHighlightedZoneRef.current}-label`)
|
|
const pin = prevLabel?.querySelector('.label-pin') as HTMLElement | null
|
|
if (pin) pin.style.opacity = '0'
|
|
console.log('setting opacity to 0 for', lastHighlightedZoneRef.current)
|
|
}
|
|
// Fade in new zone label-pin
|
|
if (highlightedZone) {
|
|
const label = document.getElementById(`${highlightedZone}-label`)
|
|
const pin = label?.querySelector('.label-pin') as HTMLElement | null
|
|
if (pin) pin.style.opacity = '1'
|
|
console.log('setting opacity to 1 for', highlightedZone)
|
|
}
|
|
|
|
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
|
|
}
|