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>
This commit is contained in:
Wassim SAMAD
2026-05-14 11:39:31 -04:00
co-authored by Claude Opus 4.7
parent 7dd3ffed3f
commit 2a8eb3e1de
@@ -2,10 +2,12 @@ import {
type AnyNodeId, type AnyNodeId,
type BuildingNode, type BuildingNode,
type CeilingNode, type CeilingNode,
nodeRegistry,
type SlabNode, type SlabNode,
useScene, useScene,
} from '@pascal-app/core' } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
import { type ComponentType, lazy, Suspense } from 'react'
import useEditor, { type Phase, type Tool } from '../../store/use-editor' import useEditor, { type Phase, type Tool } from '../../store/use-editor'
import { CeilingBoundaryEditor } from './ceiling/ceiling-boundary-editor' import { CeilingBoundaryEditor } from './ceiling/ceiling-boundary-editor'
import { CeilingHoleEditor } from './ceiling/ceiling-hole-editor' import { CeilingHoleEditor } from './ceiling/ceiling-hole-editor'
@@ -32,6 +34,21 @@ import { WindowTool } from './window/window-tool'
import { ZoneBoundaryEditor } from './zone/zone-boundary-editor' import { ZoneBoundaryEditor } from './zone/zone-boundary-editor'
import { ZoneTool } from './zone/zone-tool' 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>>> = { const tools: Record<Phase, Partial<Record<Tool, React.FC>>> = {
site: { site: {
'property-line': SiteBoundaryEditor, 'property-line': SiteBoundaryEditor,
@@ -127,7 +144,14 @@ export const ToolManager: React.FC = () => {
// Show build tools when in build mode // Show build tools when in build mode
const showBuildTool = mode === 'build' && tool !== null const showBuildTool = mode === 'build' && tool !== null
const BuildToolComponent = showBuildTool ? tools[phase]?.[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) => { const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
setSelection({ selectedIds: [nodeId] }) setSelection({ selectedIds: [nodeId] })
} }
@@ -174,13 +198,21 @@ export const ToolManager: React.FC = () => {
onSpawnMoved={handlePlacedNodeSelected} onSpawnMoved={handlePlacedNodeSelected}
/> />
)} )}
{!movingNode && showBuildTool && tool === 'spawn' && ( {/* 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} /> <SpawnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
)} )}
{!movingNode && showBuildTool && tool === 'column' && ( {!movingNode && !useRegistryTool && showBuildTool && tool === 'column' && (
<ColumnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} /> <ColumnTool currentLevelId={activeLevelId ?? null} onPlaced={handlePlacedNodeSelected} />
)} )}
{!movingNode && showBuildTool && tool === 'elevator' && ( {!movingNode && !useRegistryTool && showBuildTool && tool === 'elevator' && (
<ElevatorTool <ElevatorTool
buildingId={buildingId as BuildingNode['id'] | null} buildingId={buildingId as BuildingNode['id'] | null}
levelId={activeLevelId ?? null} levelId={activeLevelId ?? null}