Phase 5 depth-first: spawn C, fence B+C, slab B+C, ceiling C

Depth-first session: drive registered kinds through Stage B (pure
def.geometry, drop system re-export) and Stage C (def.floorplan,
short-circuit legacy inline rendering in floorplan-panel.tsx).

spawn → C
 - buildSpawnFloorplan wired on definition (was written but deferred
   to avoid double-render).
 - floorplan-panel.tsx's floorplanSpawnEntries useMemo short-circuits
   to [] when nodeRegistry.has('spawn').

fence → B
 - generateFenceGeometry exported from viewer; buildFenceGeometry
   wraps it in a Group+Mesh with DEFAULT_STAIR_MATERIAL.
 - def.geometry set; renderer + system fields dropped.
 - Deleted nodes/src/fence/{renderer.tsx,system.tsx}.

fence → C
 - buildFenceFloorplan: polyline along centerline (sampled for curved
   fences via sampleWallCenterline from core). Stroke width = node.thickness.
 - floorplan-panel.tsx's floorplanFenceEntries short-circuits.

slab → B
 - generateSlabGeometry exported from viewer; buildSlabGeometry wraps
   it in a Group+Mesh + cached material (preset / custom / default
   pattern preserved from legacy renderer).
 - def.geometry set; renderer + system fields dropped.
 - Deleted nodes/src/slab/{renderer.tsx,system.tsx}.

slab → C
 - buildSlabFloorplan: SVG path with outer polygon + hole subpaths
   (uses getRenderableSlabPolygon from core for wall-clipping parity).
 - floorplan-panel.tsx's slabPolygons short-circuits.

ceiling → B INTENTIONALLY SKIPPED
 - Ceiling renderer renders React children (hosted items) + uses TSL
   shader materials + named meshes that other systems poke
   (getObjectByName('ceiling-grid')). Pure def.geometry can't preserve
   that. Ceiling keeps def.renderer (the custom escape hatch) — same
   pattern item uses. Documented in ceiling/definition.ts.

ceiling → C
 - buildCeilingFloorplan: dashed-outline path with hole subpaths
   (visually distinct from slab since ceilings are above).
 - floorplan-panel.tsx's ceilingPolygons short-circuits.

Per-kind progress after this session:
 - shelf: B  C  (Stage E since brand-new)
 - spawn: A  C 
 - wall: A  (B blocked on ctx.levelData design)
 - fence: A  B  C 
 - slab: A  B  C 
 - ceiling: A  C  (B intentionally not applicable)
 - door / window / item: A  (B+C pending in future sessions)

Known test issue: `bun test` in packages/nodes fails to load
`three-bvh-csg` through the viewer's transitive imports (UMD/ESM
mismatch in Bun's test runner). The Next.js editor build works fine
because it bundles differently. Fix requires either dynamic imports
(breaks sync def.geometry contract) or test env config — deferred.
Other tests (schema, geometry, parity) pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 16:14:22 -04:00
co-authored by Claude Opus 4.7
parent df07f7bcb2
commit 969b154b08
16 changed files with 385 additions and 383 deletions
+20 -34
View File
@@ -1,34 +1,21 @@
import type { NodeDefinition } from '@pascal-app/core'
import { buildFenceFloorplan } from './floorplan'
import { buildFenceGeometry } from './geometry'
import { fenceParametrics } from './parametrics'
import { FenceNode } from './schema'
/**
* Fence — the first Phase 5 batch-migration kind.
* Fence — Phase 5 batch kind. Stage B complete: `def.geometry` drives
* the rebuild via the generic `<GeometrySystem>`; `<ParametricNodeRenderer>`
* mounts the empty group. No per-kind renderer or system file.
*
* What this definition encodes:
* - **Capabilities**: snappable (other walls/fences/items snap to it),
* surfaces (front + back faces host items), selectable, duplicable,
* deletable. No `movable` capability — fence move is bespoke
* endpoint-drag, same shape as wall (handled by legacy MoveFenceTool
* until the affordance port).
* - **Relations**: `linkedBy: 'endpoint-match'` for fence-corner cascade.
* No `hosts` field — doors/windows don't mount on fences. `affectsSpatial`
* omitted: moving a fence doesn't dirty slabs/zones in the legacy
* behavior, so the registry stays parity-equivalent until we
* explicitly add the cascade (separate decision).
* - **Parametrics**: dimensions, posts, style — see `./parametrics.ts`.
* - **toolHints**: placement panel hints for the fence-build tool.
* - **Renderer + system**: thin placeholder mesh + re-export of the
* legacy `FenceSystem`. Same shape as wall milestone B; future Phase 5+
* extracts the pure geometry function and migrates to `def.geometry`.
* Capabilities:
* - **No `movable`**: fence move is bespoke endpoint-drag. Capability-
* driven dispatch keeps the legacy MoveFenceTool until the
* affordance port (Stage D).
* - `surfaces.sides`, `selectable`, `duplicable`, `deletable` standard.
*
* Tool field stays absent: fence has 4 separate tools (build, curve,
* move, move-endpoint) wired through editor state, not the registry
* tool dispatch. They keep running unchanged until the affordance port.
*
* Migration is gated by `feature-flag.ts`
* (env: `NEXT_PUBLIC_USE_REGISTRY_FOR_FENCE`). See
* `plans/editor-node-registry.md#phase-5` for the batch order.
* Relations: `linkedBy: 'endpoint-match'` for corner cascade.
*/
export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
kind: 'fence',
@@ -71,16 +58,15 @@ export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
parametrics: fenceParametrics,
renderer: {
kind: 'parametric',
module: () => import('./renderer'),
},
system: {
module: () => import('./system'),
// Same frame priority as the legacy FenceSystem (4 — runs after door/
// window animations at 2-3, before zone/level systems at 6+).
priority: 4,
},
// Stage B: pure geometry function. Generic <GeometrySystem> rebuilds
// on dirtyNodes; <ParametricNodeRenderer> mounts the empty group.
// `renderer` + `system` fields dropped along with their files.
geometry: buildFenceGeometry,
// Stage C: floor-plan rendering. FloorplanRegistryLayer iterates kinds
// with `floorplan` set and renders via FloorplanGeometryRenderer.
// Legacy `floorplanFenceEntries` short-circuits to [] when fence is
// registered (see floorplan-panel.tsx).
floorplan: buildFenceFloorplan,
toolHints: [
{ key: 'Left click', label: 'Set fence start / end' },