Files
editor/packages/viewer/src/hooks/use-node-events.ts
T
1d28e4208f sync: update core and viewer from monorepo (#143)
Major changes:
- Roof system rewrite with roof-segment support
- Scene store refactor
- Spatial grid improvements
- Item light system
- Post-processing and selection manager updates
- Perf monitor component

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 23:18:30 -04:00

105 lines
3.3 KiB
TypeScript

import {
type BuildingEvent,
type BuildingNode,
type CeilingEvent,
type CeilingNode,
type DoorEvent,
type DoorNode,
type EventSuffix,
emitter,
type ItemEvent,
type ItemNode,
type LevelEvent,
type LevelNode,
type RoofEvent,
type RoofNode,
type RoofSegmentEvent,
type RoofSegmentNode,
type SiteEvent,
type SiteNode,
type SlabEvent,
type SlabNode,
type WallEvent,
type WallNode,
type WindowEvent,
type WindowNode,
type ZoneEvent,
type ZoneNode,
} from '@pascal-app/core'
import type { ThreeEvent } from '@react-three/fiber'
import useViewer from '../store/use-viewer'
type NodeConfig = {
site: { node: SiteNode; event: SiteEvent }
item: { node: ItemNode; event: ItemEvent }
wall: { node: WallNode; event: WallEvent }
building: { node: BuildingNode; event: BuildingEvent }
level: { node: LevelNode; event: LevelEvent }
zone: { node: ZoneNode; event: ZoneEvent }
slab: { node: SlabNode; event: SlabEvent }
ceiling: { node: CeilingNode; event: CeilingEvent }
roof: { node: RoofNode; event: RoofEvent }
'roof-segment': { node: RoofSegmentNode; event: RoofSegmentEvent }
window: { node: WindowNode; event: WindowEvent }
door: { node: DoorNode; event: DoorEvent }
}
type NodeType = keyof NodeConfig
export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T) {
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`
const localPoint = e.object.worldToLocal(e.point.clone())
const payload = {
node,
position: [e.point.x, e.point.y, e.point.z],
localPosition: [localPoint.x, localPoint.y, localPoint.z],
normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined,
stopPropagation: () => e.stopPropagation(),
nativeEvent: e,
} as NodeConfig[T]['event']
emitter.emit(eventKey, payload)
}
return {
onPointerDown: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('pointerdown', e)
},
onPointerUp: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
if (e.button !== 0) return
emit('pointerup', e)
// Synthesize a click event on pointer up to be more forgiving than R3F's default onClick
// which often fails if the mouse moves even 1 pixel.
emit('click', e)
},
onClick: (e: ThreeEvent<PointerEvent>) => {
// Disable default R3F click since we synthesize it on pointerup
// This prevents double-clicks from firing twice.
},
onPointerEnter: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
emit('enter', e)
},
onPointerLeave: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
emit('leave', e)
},
onPointerMove: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
emit('move', e)
},
onDoubleClick: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
emit('double-click', e)
},
onContextMenu: (e: ThreeEvent<PointerEvent>) => {
if (useViewer.getState().cameraDragging) return
emit('context-menu', e)
},
}
}