Phase 5 Stage E: full kind migration into packages/nodes

Wholesale move of every remaining kind into its own subdirectory under
`packages/nodes/src/`, finishing the registry-driven migration. Each
kind now ships its definition, schema (re-exported from core), and any
of `geometry` / `renderer` / `system` / `floorplan` / `tool` /
`move-tool` / `panel` / `floorplan-move` / `floorplan-affordances` /
`parametrics` / `preview` it needs — no per-kind code remains under
`packages/editor/src/components/tools/` or
`packages/viewer/src/components/renderers/`.

Deleted (replaced by registry-driven equivalents):
- `tools/{ceiling,column,door,fence,item,slab,spawn,wall,window}/...`
  (boundary editors, hole editors, placement tools, move tools,
  endpoint movers, curve tools, helpers, math libs)
- `ui/helpers/{ceiling,slab,wall}-helper.tsx`
- `ui/panels/{column,door,elevator,item,roof,roof-segment,spawn,
  stair,stair-segment,wall,window}-panel.tsx`
- `viewer/src/components/renderers/{building,ceiling,column,door,
  elevator,fence,guide,item,level,roof,roof-segment,scan,site,slab,
  spawn,stair,stair-segment,wall,window,zone}-renderer.tsx`
- `viewer/src/components/viewer/legacy-system.tsx`

Added under `packages/nodes/src/`:
- `building/`, `column/`, `elevator/`, `guide/`, `level/`, `roof/`,
  `roof-segment/`, `scan/`, `shared/`, `site/`, `stair/`,
  `stair-segment/` packages with definition + schema + renderer / system
  / floorplan / panel as appropriate.
- New `floorplan-move.ts` for every kind that supports 2D moves
  (ceiling, door, item, shelf, slab, window) — single registry-driven
  dispatch path via `def.floorplanMoveTarget`.
- New `floorplan-affordances.ts` for kinds with polygon / endpoint
  drags (ceiling, fence, slab, wall) — using the shared
  `polygon-vertex-affordance` factories.
- New per-kind `panel.tsx` for kinds with custom inspector content
  (door, item, shelf, spawn, wall, window).
- New per-kind `tool.tsx` for placement (door, item, shelf, window).
- New per-kind `move-tool.tsx` for kinds with custom 3D move flows
  (door, item, slab, window).

Coordinator + manager updates in `packages/editor/`:
- `tool-manager.tsx` resolves tools from the registry only — no
  hardcoded type→component map.
- `panel-manager.tsx` resolves inspector panels the same way.
- `placement-{coordinator,strategies,types}.ts` extended with
  shelf-surface placement.
- `selection-manager.tsx` adds the registry-selectable fallback.
- `floorplan-panel.tsx`, `floorplan-background-placement.ts`,
  `floorplan-render-context.tsx` updated for the registry layer's new
  contract (props, affordance dispatch, render context).

Viewer updates:
- `viewer/index.tsx` drops legacy renderer mounts.
- `node-renderer.tsx` resolves by registry only.
- `scene-bvh.tsx`, `use-node-events.ts`, `level-system.tsx`,
  `wall-cutout.tsx`, `zone-system.tsx`, `materials.ts` adjusted for
  the registry-only world.

Sidebar tree nodes for ceiling / fence / slab / shelf / tree-node
updated to read from the registered nodes instead of the deleted
legacy renderer trees.

Wiki: new `plugin-authoring.md` page, README index updated.

Tests in `packages/nodes/src/index.test.ts` validate every registered
kind has the required shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-19 15:14:12 -04:00
co-authored by Claude Opus 4.7
parent 11015ea1ed
commit d747d2f0ea
204 changed files with 6888 additions and 7877 deletions
+17 -66
View File
@@ -1,77 +1,25 @@
import {
type BuildingEvent,
type BuildingNode,
type CeilingEvent,
type CeilingNode,
type ColumnEvent,
type ColumnNode,
type DoorEvent,
type DoorNode,
type ElevatorEvent,
type ElevatorNode,
type AnyNode,
type AnyNodeType,
type EventSuffix,
emitter,
type FenceEvent,
type FenceNode,
type ItemEvent,
type ItemNode,
type LevelEvent,
type LevelNode,
type RoofEvent,
type RoofNode,
type RoofSegmentEvent,
type RoofSegmentNode,
type ShelfEvent,
type ShelfNode,
type SiteEvent,
type SiteNode,
type SlabEvent,
type SlabNode,
type SpawnEvent,
type SpawnNode,
type StairEvent,
type StairNode,
type StairSegmentEvent,
type StairSegmentNode,
type WallEvent,
type WallNode,
type WindowEvent,
type WindowNode,
type ZoneEvent,
type ZoneNode,
type NodeEvent,
} from '@pascal-app/core'
import type { ThreeEvent } from '@react-three/fiber'
import useViewer from '../store/use-viewer'
type NodeConfig = {
site: { node: SiteNode; event: SiteEvent }
item: { node: ItemNode; event: ItemEvent }
wall: { node: WallNode; event: WallEvent }
fence: { node: FenceNode; event: FenceEvent }
building: { node: BuildingNode; event: BuildingEvent }
level: { node: LevelNode; event: LevelEvent }
zone: { node: ZoneNode; event: ZoneEvent }
shelf: { node: ShelfNode; event: ShelfEvent }
slab: { node: SlabNode; event: SlabEvent }
spawn: { node: SpawnNode; event: SpawnEvent }
ceiling: { node: CeilingNode; event: CeilingEvent }
column: { node: ColumnNode; event: ColumnEvent }
roof: { node: RoofNode; event: RoofEvent }
'roof-segment': { node: RoofSegmentNode; event: RoofSegmentEvent }
stair: { node: StairNode; event: StairEvent }
'stair-segment': { node: StairSegmentNode; event: StairSegmentEvent }
window: { node: WindowNode; event: WindowEvent }
door: { node: DoorNode; event: DoorEvent }
elevator: { node: ElevatorNode; event: ElevatorEvent }
}
// 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<AnyNode, { type: K }>` picks the node
// shape, `NodeEvent<T>` adapts the bus payload to that shape.
type NodeByKind<K extends AnyNodeType> = Extract<AnyNode, { type: K }>
type NodeType = keyof NodeConfig
export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T) {
export function useNodeEvents<K extends AnyNodeType>(node: NodeByKind<K>, type: K) {
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`
const eventKey = `${type}:${suffix}` as `${K}:${EventSuffix}`
const localPoint = e.object.worldToLocal(e.point.clone())
const payload = {
const payload: NodeEvent<NodeByKind<K>> = {
node,
position: [e.point.x, e.point.y, e.point.z],
localPosition: [localPoint.x, localPoint.y, localPoint.z],
@@ -80,9 +28,12 @@ export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], t
object: e.object,
stopPropagation: () => e.stopPropagation(),
nativeEvent: e,
} as NodeConfig[T]['event']
}
emitter.emit(eventKey, payload)
// `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)
}
return {