Wassim SAMAD 2a8eb3e1de Registry-first dispatch in ToolManager (shim 4/4)
Before mounting any legacy build tool, ToolManager checks whether the
active tool's kind has a registered NodeDefinition with a tool
contribution. If yes, the registry tool wins; the legacy tool map and
special-cased spawn/column/elevator branches are skipped for that kind.

Lazy-loaded via React.lazy (cached by loader) and wrapped in Suspense.

Today the registry is empty, so useRegistryTool is always false and
every code path renders unchanged. The moment a kind registers (Phase
2+), its registry tool takes over without further edits here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:39:31 -04:00

233 lines
9.7 KiB
TypeScript

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<unknown>, 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<Phase, Partial<Record<Tool, React.FC>>> = {
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 && <SiteBoundaryEditor />}
{movingNode?.type === 'building' && (
<MoveTool onNodeMoved={handlePlacedNodeSelected} onSpawnMoved={handlePlacedNodeSelected} />
)}
{/* 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. */}
<group
position={buildingPosition as [number, number, number]}
rotation={buildingRotation as [number, number, number]}
>
{showZoneBoundaryEditor && selectedZoneId && <ZoneBoundaryEditor zoneId={selectedZoneId} />}
{showSlabBoundaryEditor && selectedSlabId && <SlabBoundaryEditor slabId={selectedSlabId} />}
{showSlabHoleEditor && selectedSlabId && editingHole && (
<SlabHoleEditor holeIndex={editingHole.holeIndex} slabId={selectedSlabId} />
)}
{showCeilingBoundaryEditor && selectedCeilingId && (
<CeilingBoundaryEditor ceilingId={selectedCeilingId} />
)}
{showCeilingHoleEditor && selectedCeilingId && editingHole && (
<CeilingHoleEditor ceilingId={selectedCeilingId} holeIndex={editingHole.holeIndex} />
)}
{movingWallEndpoint && <MoveWallEndpointTool target={movingWallEndpoint} />}
{movingFenceEndpoint && <MoveFenceEndpointTool target={movingFenceEndpoint} />}
{curvingWall && <CurveWallTool node={curvingWall} />}
{curvingFence && <CurveFenceTool node={curvingFence} />}
{movingNode && movingNode.type !== 'building' && (
<MoveTool
onNodeMoved={handlePlacedNodeSelected}
onSpawnMoved={handlePlacedNodeSelected}
/>
)}
{/* 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 && (
<Suspense fallback={null}>
<RegistryToolComponent />
</Suspense>
)}
{!movingNode && !useRegistryTool && showBuildTool && tool === 'spawn' && (
<SpawnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
)}
{!movingNode && !useRegistryTool && showBuildTool && tool === 'column' && (
<ColumnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
)}
{!movingNode && !useRegistryTool && showBuildTool && tool === 'elevator' && (
<ElevatorTool
buildingId={buildingId as BuildingNode['id'] | null}
levelId={activeLevelId ?? null}
onPlaced={handlePlacedElevatorSelected}
/>
)}
{!movingNode &&
BuildToolComponent &&
tool !== 'spawn' &&
tool !== 'column' &&
tool !== 'elevator' ? (
<BuildToolComponent />
) : null}
</group>
</>
)
}