import { type AnyNodeId, type BuildingNode, type CeilingNode, nodeRegistry, type SlabNode, useScene, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { type ComponentType, lazy, Suspense } from 'react' import useEditor, { type Phase, type Tool } from '../../store/use-editor' import { CeilingBoundaryEditor } from './ceiling/ceiling-boundary-editor' import { CeilingHoleEditor } from './ceiling/ceiling-hole-editor' import { CeilingTool } from './ceiling/ceiling-tool' import { ColumnTool } from './column/column-tool' import { DoorTool } from './door/door-tool' import { ElevatorTool } from './elevator/elevator-tool' import { CurveFenceTool } from './fence/curve-fence-tool' import { FenceTool } from './fence/fence-tool' import { MoveFenceEndpointTool } from './fence/move-fence-endpoint-tool' import { ItemTool } from './item/item-tool' import { MoveTool } from './item/move-tool' import { RoofTool } from './roof/roof-tool' import { SiteBoundaryEditor } from './site/site-boundary-editor' import { SlabBoundaryEditor } from './slab/slab-boundary-editor' import { SlabHoleEditor } from './slab/slab-hole-editor' import { SlabTool } from './slab/slab-tool' import { SpawnTool } from './spawn/spawn-tool' import { StairTool } from './stair/stair-tool' import { CurveWallTool } from './wall/curve-wall-tool' import { MoveWallEndpointTool } from './wall/move-wall-endpoint-tool' import { WallTool } from './wall/wall-tool' import { WindowTool } from './window/window-tool' import { ZoneBoundaryEditor } from './zone/zone-boundary-editor' import { ZoneTool } from './zone/zone-tool' // Cache lazy tool components keyed by their loader so React.lazy isn't // re-invoked across renders. const lazyToolCache = new WeakMap<() => Promise, ComponentType>() function getRegistryTool(tool: Tool | null): ComponentType | null { if (!tool) return null const def = nodeRegistry.get(tool) if (!def?.tool) return null const cached = lazyToolCache.get(def.tool) if (cached) return cached const Comp = lazy(def.tool as () => Promise<{ default: ComponentType }>) lazyToolCache.set(def.tool, Comp) return Comp } const tools: Record>> = { site: { 'property-line': SiteBoundaryEditor, }, structure: { wall: WallTool, fence: FenceTool, slab: SlabTool, ceiling: CeilingTool, roof: RoofTool, stair: StairTool, door: DoorTool, item: ItemTool, zone: ZoneTool, window: WindowTool, }, furnish: { item: ItemTool, }, } export const ToolManager: React.FC = () => { const phase = useEditor((state) => state.phase) const mode = useEditor((state) => state.mode) const tool = useEditor((state) => state.tool) const movingNode = useEditor((state) => state.movingNode) const movingWallEndpoint = useEditor((state) => state.movingWallEndpoint) const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint) const curvingWall = useEditor((state) => state.curvingWall) const curvingFence = useEditor((state) => state.curvingFence) const editingHole = useEditor((state) => state.editingHole) const selectedZoneId = useViewer((state) => state.selection.zoneId) const selectedIds = useViewer((state) => state.selection.selectedIds) const buildingId = useViewer((state) => state.selection.buildingId) const activeLevelId = useViewer((state) => state.selection.levelId) const setSelection = useViewer((state) => state.setSelection) const nodes = useScene((state) => state.nodes) // Building transform for the local group — all building-relative tools live inside this group // so their cursor positions and committed data are naturally in building-local space. const building = buildingId ? (nodes[buildingId as AnyNodeId] as BuildingNode | undefined) : undefined const buildingPosition = building?.position ?? [0, 0, 0] const buildingRotation = building?.rotation ?? [0, 0, 0] // Check if a slab is selected const selectedSlabId = selectedIds.find((id) => nodes[id as AnyNodeId]?.type === 'slab') as | SlabNode['id'] | undefined // Check if a ceiling is selected const selectedCeilingId = selectedIds.find((id) => nodes[id as AnyNodeId]?.type === 'ceiling') as | CeilingNode['id'] | undefined // Show site boundary editor when in site phase (toggle controls entry/exit) const showSiteBoundaryEditor = phase === 'site' // Show slab boundary editor when in structure/select mode with a slab selected (but not editing a hole) const showSlabBoundaryEditor = phase === 'structure' && mode === 'select' && selectedSlabId !== undefined && (!editingHole || editingHole.nodeId !== selectedSlabId) // Show slab hole editor when editing a hole on the selected slab const showSlabHoleEditor = selectedSlabId !== undefined && editingHole !== null && editingHole.nodeId === selectedSlabId // Show ceiling boundary editor when in structure/select mode with a ceiling selected (but not editing a hole) const showCeilingBoundaryEditor = phase === 'structure' && mode === 'select' && selectedCeilingId !== undefined && (!editingHole || editingHole.nodeId !== selectedCeilingId) // Show ceiling hole editor when editing a hole on the selected ceiling const showCeilingHoleEditor = selectedCeilingId !== undefined && editingHole !== null && editingHole.nodeId === selectedCeilingId // Show zone boundary editor when in structure/select mode with a zone selected // Hide when editing a slab or ceiling to avoid overlapping handles const showZoneBoundaryEditor = phase === 'structure' && mode === 'select' && selectedZoneId !== null && !showSlabBoundaryEditor && !showCeilingBoundaryEditor // Show build tools when in build mode const showBuildTool = mode === 'build' && tool !== null // Registry-first: if the active tool's kind has a NodeDefinition with a // tool contribution, the registry-driven tool takes over. Otherwise fall // through to the legacy `tools` map below. Today the registry is empty so // RegistryToolComponent is always null — zero behavior change. const RegistryToolComponent = showBuildTool ? getRegistryTool(tool) : null const useRegistryTool = RegistryToolComponent != null const BuildToolComponent = showBuildTool && !useRegistryTool ? tools[phase]?.[tool] : null const handlePlacedNodeSelected = (nodeId: AnyNodeId) => { setSelection({ selectedIds: [nodeId] }) } const handlePlacedElevatorSelected = ( nodeId: AnyNodeId, elevatorBuildingId: BuildingNode['id'], ) => { setSelection({ buildingId: elevatorBuildingId, selectedIds: [nodeId] }) } return ( <> {/* World-space tools: site boundary and building movement operate in world coordinates */} {showSiteBoundaryEditor && } {movingNode?.type === 'building' && ( )} {/* Building-local group: all other tools are relative to the selected building. Cursor visuals set positions in building-local space; this group applies the building's world transform so they render at the correct world position. */} {showZoneBoundaryEditor && selectedZoneId && } {showSlabBoundaryEditor && selectedSlabId && } {showSlabHoleEditor && selectedSlabId && editingHole && ( )} {showCeilingBoundaryEditor && selectedCeilingId && ( )} {showCeilingHoleEditor && selectedCeilingId && editingHole && ( )} {movingWallEndpoint && } {movingFenceEndpoint && } {curvingWall && } {curvingFence && } {movingNode && movingNode.type !== 'building' && ( )} {/* Registry-first: when the active tool's kind has a registered NodeDefinition with a tool contribution, mount it here. Today the registry is empty so this branch never fires. */} {!movingNode && useRegistryTool && RegistryToolComponent && ( )} {!movingNode && !useRegistryTool && showBuildTool && tool === 'spawn' && ( )} {!movingNode && !useRegistryTool && showBuildTool && tool === 'column' && ( )} {!movingNode && !useRegistryTool && showBuildTool && tool === 'elevator' && ( )} {!movingNode && BuildToolComponent && tool !== 'spawn' && tool !== 'column' && tool !== 'elevator' ? ( ) : null} ) }