import { type AnyNode, type AnyNodeType, type EventSuffix, emitter, type NodeEvent, } from '@pascal-app/core' import type { ThreeEvent } from '@react-three/fiber' import useViewer from '../store/use-viewer' // Derive `{ node, event }` per kind directly from the `AnyNode` // discriminated union — no hand-maintained kind→type map. Adding a new // kind to `AnyNode` automatically makes it valid here; removing one // removes its overload. `Extract` picks the node // shape, `NodeEvent` adapts the bus payload to that shape. type NodeByKind = Extract export function useNodeEvents(node: NodeByKind, type: K) { const emit = (suffix: EventSuffix, e: ThreeEvent) => { const eventKey = `${type}:${suffix}` as `${K}:${EventSuffix}` const localPoint = e.object.worldToLocal(e.point.clone()) const payload: NodeEvent> = { 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, } // `emitter.emit` is typed over a fixed union of `${kind}:${suffix}` // keys; the `as never` cast lets us emit a kind-specific payload // through that generic surface without enumerating every kind. emitter.emit(eventKey, payload as never) } // Camera drags (orbit / pan / dolly) suppress ALL node pointer events. // // `inputDragging` (host-driven drags: handle arrows, press-drag moves) // additionally suppresses the SELECTION events — without it the click // synthesized on pointer-release would reroute selection to whatever mesh // sits under the cursor at release. It must NOT suppress the SPATIAL events // (`enter` / `move` / `leave`): a surface-following move tool — a door / // window sliding along a wall — runs WITH `inputDragging` set and depends on // those events to track the cursor. Consumers that should ignore drag-time // spatial events gate on `inputDragging` themselves (the editor's hover and // paint paths, box-select), so emitting them during a drag only reaches the // active move tool that wants them. const spatialSuppressed = () => useViewer.getState().cameraDragging const selectionSuppressed = () => { const s = useViewer.getState() return s.cameraDragging || s.inputDragging } return { onPointerDown: (e: ThreeEvent) => { if (selectionSuppressed()) return if (e.button !== 0) return emit('pointerdown', e) }, onPointerUp: (e: ThreeEvent) => { if (selectionSuppressed()) 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) => { // Disable default R3F click since we synthesize it on pointerup // This prevents double-clicks from firing twice. }, onPointerEnter: (e: ThreeEvent) => { if (spatialSuppressed()) return emit('enter', e) }, onPointerLeave: (e: ThreeEvent) => { if (spatialSuppressed()) return emit('leave', e) }, onPointerMove: (e: ThreeEvent) => { if (spatialSuppressed()) return emit('move', e) }, onDoubleClick: (e: ThreeEvent) => { if (selectionSuppressed()) return emit('double-click', e) }, onContextMenu: (e: ThreeEvent) => { if (selectionSuppressed()) return emit('context-menu', e) }, } }