Phase 5 Stage C continued: wall, door, window now in registry floor plan

Three remaining kinds at Stage C this session:

wall → C
 - buildWallFloorplan: uses ctx.siblings to gather other walls in the
   level, runs calculateLevelMiters, computes plan footprint via
   getWallPlanFootprint. Same visual output as legacy.
 - getFloorplanWall thickness exaggeration inlined (~25 lines from
   editor/lib/floorplan/walls.ts) to keep nodes/wall self-contained.
 - floorplan-panel.tsx's wallPolygons short-circuits to [] when wall
   is registered.
 - Performance note: recomputes level miter data per wall (O(N²) for N
   walls in a level). Acceptable for typical scenes; ctx.levelData?.
   miters optimization deferred to Stage B's wall design pass.

door → C
 - buildDoorFloorplan: inlines getOpeningFootprint math from
   floorplan-panel.tsx (40 lines, pure math). Uses ctx.parent as the
   wall to compute direction + perpendicular for the cutout footprint.
 - Returns null when parent isn't a wall (orphaned doors during
   placement).

window → C
 - buildWindowFloorplan: same shape as door, glass-blue tint to
   distinguish visually.

Both share the legacy openingsPolygons gating:
 - floorplan-panel.tsx's openingsPolygons useMemo filters per kind so
   a partial migration still works (e.g., if only door registers, only
   doors get skipped). When both registered, returns [] entirely.

Item C intentionally deferred — needs parent-chain transform helpers
(buildFloorplanItemEntry / getItemFloorplanTransform from editor/lib/
floorplan/items.ts) exposed publicly or moved into core. A focused
session is the right place to design that boundary.

Stage B for door / window / wall still pending — each is a focused
session per kind (large geometry math extractions, wall needs ctx.
levelData design).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 16:43:53 -04:00
co-authored by Claude Opus 4.7
parent 969b154b08
commit 9bcb25d0aa
7 changed files with 274 additions and 124 deletions
+18 -64
View File
@@ -1,34 +1,21 @@
import type { NodeDefinition } from '@pascal-app/core'
import { buildWallFloorplan } from './floorplan'
import { wallParametrics } from './parametrics'
import { WallNode } from './schema'
/**
* Wall — the Phase 3 stress test of the registry-driven node model.
*
* What this definition encodes today:
* - **Capabilities**: cuttable (doors/windows punch holes), snappable
* (other walls, doors, windows snap to wall geometry), surfaces (front
* + back faces host items), selectable, duplicable, deletable.
* - **Relations**: hosts doors/windows/items; affects spatial slabs +
* ceilings + zones when moved; descendants cascade-delete; linked walls
* follow corners via endpoint-match (consumed by the affordances in a
* later milestone — the relations resolver already understands the
* declaration).
* - **Parametrics**: thickness / height / curveOffset for the inspector.
*
* What this definition does *not* yet encode:
* - `geometry` / `renderer` / `system` runtime — the existing
* `wall-renderer.tsx` + `wall-system.tsx` keep serving wall until
* Milestone B ports them into this folder. Until then, this definition
* is metadata-only and *intentionally not registered* in
* `builtinPlugin.nodes` — the Phase 0 shims only flip behavior when a
* kind is registered, so wall stays on its legacy path.
* - `tool` — wall's placement + endpoint drag + curve drag tools port in
* a follow-up milestone, expressed via the `DragAction` primitive so
* the affordances declared in `relations` get real handles.
*
* Migration is gated by `feature-flag.ts` (env: `NEXT_PUBLIC_USE_REGISTRY_FOR_WALL`).
* See `plans/editor-node-registry.md#phase-3` for the milestone breakdown.
* Stage A: registered (capabilities, relations, parametrics, presentation).
* Stage B: deferred — wall geometry depends on level-batch miter data that
* doesn't fit the generic `(node, ctx) => Group` shape without `ctx.
* levelData?.miters`. See plan's "GeometryContext" extension note.
* `renderer` + `system` keep wrap-exporting legacy WallRenderer +
* WallSystem + WallCutout.
* Stage C: `def.floorplan` builder produces the mitered plan footprint
* polygon using `ctx.siblings` to assemble miter context.
* floorplan-panel.tsx's `wallPolygons` short-circuits to [] when
* wall is registered.
*/
export const wallDefinition: NodeDefinition<typeof WallNode> = {
kind: 'wall',
@@ -49,18 +36,12 @@ export const wallDefinition: NodeDefinition<typeof WallNode> = {
}),
capabilities: {
// Wall move is bespoke today (endpoint drag, linked-wall corner cascade,
// ALT-detach). `MoveRegistryNodeTool`'s "translate on X/Z plane" shape
// doesn't apply — wall stays on its own move tool until the affordance
// port. Leaving `movable` omitted keeps that dispatch.
// Wall move is bespoke (endpoint drag, linked-wall corner cascade,
// ALT-detach). Omitting `movable` keeps the legacy MoveWallTool via
// capability-driven dispatch.
selectable: { hitVolume: 'bbox' },
// Front + back faces host items (paintings, shelves, switches).
// `height` callback resolves per-instance so taller walls expose taller
// hosting surface — same shape used by shelf.top.
surfaces: {
// Sides config — wall has two faces; concrete face-selection logic
// stays in the renderer/system for now. Phase 4 will derive snap
// targets from this.
sides: { faces: 'all' },
},
duplicable: true,
@@ -68,50 +49,26 @@ export const wallDefinition: NodeDefinition<typeof WallNode> = {
},
relations: {
// Doors / windows / items mount on walls. The host-resolver consumes
// this to validate `parentId` on creation and to re-anchor children
// when a wall moves (a milestone-B concern; the declaration lives here
// so the wiring exists ahead of time).
hosts: ['door', 'window', 'item'],
// Moving a wall dirties the slabs / ceilings / zones that border it.
// Today this is *not* wired (the existing `wall-system` doesn't cascade
// to slab / zone) — the registry resolver gains this behavior for free
// once wall is registered. This is the "slab reflow on wall move"
// behavior gain called out in Phase 3 acceptance.
affectsSpatial: ['slab', 'ceiling', 'zone'],
// Walls sharing an endpoint move together when a corner is dragged.
// The endpoint affordance (milestone C) uses this declaration via the
// shared cascade resolver — no hand-rolled `getLinkedWallSnapshots`.
linkedBy: 'endpoint-match',
// Deleting a wall deletes its hosted doors/windows/items (today's
// implicit behavior, now declarative).
cascadeDelete: 'descendants',
},
parametrics: wallParametrics,
// Wall's renderer is the thin placeholder-mesh mount point from milestone
// B; the system bundle composes the legacy `WallSystem` + `WallCutout`
// re-exported from viewer (so we don't duplicate ~970 lines of CSG /
// mitering / cutaway logic just to swap the dispatch). The legacy
// mount in `<LegacySystem kind="wall">` short-circuits the moment
// `nodeRegistry.has('wall')` is true.
//
// No `tool` yet — wall placement / endpoint drag / curve drag remain
// bespoke until the affordance port lands. Phase 0 shims keep the legacy
// wall tool running while wall is registered (it's not wired through the
// registry tool dispatch).
renderer: {
kind: 'parametric',
module: () => import('./renderer'),
},
system: {
module: () => import('./system'),
// Priority 4 mirrors the legacy WallSystem's useFrame priority — keeps
// miter cascade running after door/window animation systems (priority 2)
// but before zone/level systems that read wall positions.
// Priority 4 mirrors the legacy WallSystem's useFrame priority.
priority: 4,
},
// Stage C: floor-plan rendering. ctx.siblings provides other walls in
// the level so `calculateLevelMiters` can compute correct corner joins.
floorplan: buildWallFloorplan,
presentation: {
label: 'Wall',
@@ -123,8 +80,5 @@ export const wallDefinition: NodeDefinition<typeof WallNode> = {
mcp: {
description: 'A wall segment defined by start + end points, with optional curve sagitta.',
// Wall has hand-written semantic MCP tooling (`create_wall` builds full
// rooms from polygons; this entry is for the auto-derived single-wall
// primitive). Stays auto-derived until Phase 4 says otherwise.
},
}