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
+78 -8
View File
@@ -6,6 +6,7 @@ import {
type GeometryContext,
getScaledDimensions,
type ItemNode,
useLiveTransforms,
} from '@pascal-app/core'
/**
@@ -25,10 +26,17 @@ import {
*/
type Transform = { x: number; y: number; rotation: number }
// Plan-space rotation convention used by the legacy `rotatePlanVector`
// in `editor/src/lib/floorplan/geometry.ts`. This is a CLOCKWISE rotation
// — the registry-side equivalent of the canonical floor-plan transform
// math. Don't switch to a standard counter-clockwise rotation; the wall
// items math (wallRotation = -atan2(dy, dx)) is calibrated against this
// convention, and the legacy item floor-plan stack reads from these
// offsets across many sites.
function rotateVec(x: number, y: number, angle: number): [number, number] {
const c = Math.cos(angle)
const s = Math.sin(angle)
return [x * c - y * s, x * s + y * c]
return [x * c + y * s, -x * s + y * c]
}
function resolveItemTransform(
@@ -75,6 +83,35 @@ function resolveItemTransform(
rotation: parentT.rotation + localRotation,
}
}
} else if (parentNode?.type === 'shelf') {
// Shelf-hosted item: `item.position` is in shelf-local coords. The
// shelf has its own `position` + `rotation[1]` in its parent (level)
// frame, so the item's plan-space position composes the shelf's
// pose with the item's local offset. Without this branch the
// `else` below would treat shelf-local coords as level-local and
// the item would render at the wrong spot whenever the shelf is
// anywhere other than (0, 0, 0).
//
// We also check `useLiveTransforms` for the shelf — if the shelf is
// mid-move (3D or 2D), its scene-state `position` is still at the
// pre-move spot but the live transform carries the cursor-tracked
// position. Reading the live value here keeps the hosted item
// following the shelf in 2D throughout the drag, mirroring how the
// shelf's own entry follows via the layer's effectiveNode override.
const shelf = parentNode as AnyNode & {
position: [number, number, number]
rotation: [number, number, number]
}
const live = useLiveTransforms.getState().get(shelf.id as AnyNodeId)
const shelfX = live?.position[0] ?? shelf.position[0]
const shelfZ = live?.position[2] ?? shelf.position[2]
const shelfRotationY = live?.rotation ?? shelf.rotation[1] ?? 0
const [offsetX, offsetY] = rotateVec(item.position[0], item.position[2], shelfRotationY)
result = {
x: shelfX + offsetX,
y: shelfZ + offsetY,
rotation: shelfRotationY + localRotation,
}
} else {
// Level / slab / ceiling parent — item.position is level-local.
result = {
@@ -116,12 +153,45 @@ export function buildItemFloorplan(node: ItemNode, ctx: GeometryContext): Floorp
return [cx + rx, cy + ry] as FloorplanPoint
})
return {
kind: 'polygon',
points,
fill: '#fef3c7',
stroke: '#92400e',
strokeWidth: 0.012,
opacity: 0.85,
const isSelected = ctx.viewState?.selected ?? false
const floorPlanUrl = node.asset.floorPlanUrl
const children: FloorplanGeometry[] = [
{
kind: 'polygon',
points,
// When an asset thumbnail is present, the polygon is a transparent
// hit-target — the image carries the visual weight. Without a
// thumbnail the polygon needs a light fill to read at all.
//
// `transparent` (not `none`) so the interior remains hit-testable —
// the registry layer's wrapping `<g>` only fires onPointerDown when
// the child renders a paintable surface. `fill="none"` would make
// clicks pass through to whatever's beneath, breaking selection.
fill: floorPlanUrl ? 'transparent' : '#fef3c7',
stroke: '#92400e',
strokeWidth: 0.012,
opacity: 0.85,
},
]
// Asset thumbnail — top-down PNG capture from the asset modal. Drawn
// inside the footprint with the item's rotation applied. Matches the
// legacy `FloorplanItemImage` overlay.
if (floorPlanUrl) {
children.push({
kind: 'image',
url: floorPlanUrl,
center: [cx, cy],
width,
height: depth,
rotation: transform.rotation,
})
}
// Move handle — orange dot at the item center. Only when selected.
if (isSelected) {
children.push({
kind: 'move-handle',
point: [cx, cy],
})
}
return { kind: 'group', children }
}