Files
editor/packages/viewer/src/hooks/use-node-events.ts
T

122 lines
3.8 KiB
TypeScript

import {
type BuildingEvent,
type BuildingNode,
type CeilingEvent,
type CeilingNode,
type ColumnEvent,
type ColumnNode,
type DoorEvent,
type DoorNode,
type EventSuffix,
emitter,
type FenceEvent,
type FenceNode,
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 SpawnEvent,
type SpawnNode,
type StairEvent,
type StairNode,
type StairSegmentEvent,
type StairSegmentNode,
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 }
fence: { node: FenceNode; event: FenceEvent }
building: { node: BuildingNode; event: BuildingEvent }
level: { node: LevelNode; event: LevelEvent }
zone: { node: ZoneNode; event: ZoneEvent }
slab: { node: SlabNode; event: SlabEvent }
spawn: { node: SpawnNode; event: SpawnEvent }
ceiling: { node: CeilingNode; event: CeilingEvent }
column: { node: ColumnNode; event: ColumnEvent }
roof: { node: RoofNode; event: RoofEvent }
'roof-segment': { node: RoofSegmentNode; event: RoofSegmentEvent }
stair: { node: StairNode; event: StairEvent }
'stair-segment': { node: StairSegmentNode; event: StairSegmentEvent }
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,
faceIndex: e.faceIndex ?? undefined,
object: e.object,
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)
},
}
}