import type { ThreeEvent } from '@react-three/fiber' import mitt from 'mitt' import type { Object3D } from 'three' import type { BoxVentNode, BuildingNode, CeilingNode, ChimneyNode, ColumnNode, DoorNode, DormerNode, ElevatorNode, FenceNode, GuideNode, ItemNode, LevelNode, RidgeVentNode, RoofNode, RoofSegmentNode, ScanNode, ShelfNode, SiteNode, SkylightNode, SlabNode, SolarPanelNode, SpawnNode, StairNode, StairSegmentNode, WallNode, WindowNode, ZoneNode, } from '../schema' import type { AnyNode } from '../schema/types' // Base event interfaces export interface GridEvent { /** World-space intersection point on the grid plane. */ position: [number, number, number] /** * Building-local intersection point — relative to the currently selected building. * Equals `position` when no building is selected. * Use this for placing/committing anything that lives inside a building (walls, slabs, items, etc.). */ localPosition: [number, number, number] faceIndex?: number /** * Optional: the hit Three.js object. Present when the grid event was * synthesized from a R3F mesh hit (the legacy grid-plane mesh path); * absent when emitted by the canvas-level raycaster in * `use-grid-events.ts`, where there is no specific mesh to attribute * the intersection to. */ object?: Object3D nativeEvent: ThreeEvent } export interface NodeEvent { node: T position: [number, number, number] localPosition: [number, number, number] normal?: [number, number, number] faceIndex?: number object: Object3D stopPropagation: () => void nativeEvent: ThreeEvent } export type WallEvent = NodeEvent export type FenceEvent = NodeEvent export type ItemEvent = NodeEvent export type SiteEvent = NodeEvent export type BuildingEvent = NodeEvent export type LevelEvent = NodeEvent export type ZoneEvent = NodeEvent export type ShelfEvent = NodeEvent export type SlabEvent = NodeEvent export type SpawnEvent = NodeEvent export type CeilingEvent = NodeEvent export type ColumnEvent = NodeEvent export type RoofEvent = NodeEvent export type RoofSegmentEvent = NodeEvent export type StairEvent = NodeEvent export type StairSegmentEvent = NodeEvent export type WindowEvent = NodeEvent export type DoorEvent = NodeEvent export type ElevatorEvent = NodeEvent export type ScanEvent = NodeEvent export type GuideEvent = NodeEvent export type BoxVentEvent = NodeEvent export type RidgeVentEvent = NodeEvent export type ChimneyEvent = NodeEvent export type SolarPanelEvent = NodeEvent export type SkylightEvent = NodeEvent export type DormerEvent = NodeEvent // Event suffixes - exported for use in hooks export const eventSuffixes = [ 'click', 'move', 'enter', 'leave', 'pointerdown', 'pointerup', 'context-menu', 'double-click', ] as const export type EventSuffix = (typeof eventSuffixes)[number] type NodeEvents = { [K in `${T}:${EventSuffix}`]: E } type GridEvents = { [K in `grid:${EventSuffix}`]: GridEvent } export interface CameraControlEvent { nodeId: AnyNode['id'] } export interface ThumbnailGenerateEvent { projectId: string captureMode?: 'standard' | 'viewport' | 'area' cropRegion?: { x: number; y: number; width: number; height: number } /** * When true, snap levels to their true positions before capturing (for a * consistent auto-thumbnail angle) and defer the capture if the tab is * hidden — the background auto-save path. Omit for user-driven captures * that should fire immediately from the current camera pose. */ snapLevels?: boolean } export interface CameraControlFitSceneEvent { /** * XZ-plane axis-aligned bounds of the scene's geometry, computed from the * scene graph (see `@pascal-app/editor`'s `computeSceneBoundsXZ`). The * viewer's camera-controls listener frames the camera onto this box. * Omitted values fall back to the camera's default pose. */ bounds?: { min: [number, number] max: [number, number] center: [number, number] size: [number, number] } } type CameraControlEvents = { 'camera-controls:view': CameraControlEvent 'camera-controls:focus': CameraControlEvent 'camera-controls:capture': CameraControlEvent 'camera-controls:top-view': undefined 'camera-controls:orbit-cw': undefined 'camera-controls:orbit-ccw': undefined 'camera-controls:fit-scene': CameraControlFitSceneEvent 'camera-controls:generate-thumbnail': ThumbnailGenerateEvent } type ToolEvents = { 'tool:cancel': undefined } type GuideEvents = { 'guide:set-reference-scale': { guideId: GuideNode['id'] } 'guide:cancel-reference-scale': undefined 'guide:deleted': { guideId: GuideNode['id'] } } type DoorAnimationEvents = { 'door:animation-completed': { doorId: DoorNode['id'] field: 'operationState' | 'swingAngle' } } type WindowAnimationEvents = { 'window:animation-completed': { windowId: WindowNode['id'] field: 'operationState' } } type PresetEvents = { 'preset:generate-thumbnail': { presetId: string; nodeId: string } 'preset:thumbnail-updated': { presetId: string; thumbnailUrl: string } } type ThumbnailEvents = { 'thumbnail:before-capture': undefined 'thumbnail:after-capture': undefined } type SnapshotEvents = { 'snapshot:saved': undefined 'camera:go-to-position': { position: [number, number, number]; target: [number, number, number] } } type AIChatEvents = { 'ai-chat:attach-images': { images: { url: string; name: string; kind: 'snapshot' | 'render' }[] } } type EditorEvents = GridEvents & NodeEvents<'wall', WallEvent> & NodeEvents<'fence', FenceEvent> & NodeEvents<'item', ItemEvent> & NodeEvents<'site', SiteEvent> & NodeEvents<'building', BuildingEvent> & NodeEvents<'elevator', ElevatorEvent> & NodeEvents<'level', LevelEvent> & NodeEvents<'zone', ZoneEvent> & NodeEvents<'slab', SlabEvent> & NodeEvents<'shelf', ShelfEvent> & NodeEvents<'spawn', SpawnEvent> & NodeEvents<'ceiling', CeilingEvent> & NodeEvents<'column', ColumnEvent> & NodeEvents<'roof', RoofEvent> & NodeEvents<'roof-segment', RoofSegmentEvent> & NodeEvents<'stair', StairEvent> & NodeEvents<'stair-segment', StairSegmentEvent> & NodeEvents<'window', WindowEvent> & NodeEvents<'door', DoorEvent> & NodeEvents<'scan', ScanEvent> & NodeEvents<'guide', GuideEvent> & NodeEvents<'box-vent', BoxVentEvent> & NodeEvents<'ridge-vent', RidgeVentEvent> & NodeEvents<'chimney', ChimneyEvent> & NodeEvents<'solar-panel', SolarPanelEvent> & NodeEvents<'skylight', SkylightEvent> & NodeEvents<'dormer', DormerEvent> & CameraControlEvents & ToolEvents & GuideEvents & DoorAnimationEvents & WindowAnimationEvents & PresetEvents & ThumbnailEvents & SnapshotEvents & AIChatEvents export const emitter = mitt()