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
+14 -10
View File
@@ -1,20 +1,23 @@
import type { NodeDefinition } from '@pascal-app/core'
import { buildCeilingFloorplan } from './floorplan'
import { ceilingParametrics } from './parametrics'
import { CeilingNode } from './schema'
/**
* Ceiling — Phase 5 batch kind, polygon-based. Structurally identical
* to slab but mounted at `height` rather than `elevation`.
* Ceiling — Phase 5 batch kind, polygon-based. Structurally similar to
* slab but with React-rendered hosted children + TSL shader materials +
* named meshes that other systems poke (`getObjectByName('ceiling-grid')`).
*
* Capabilities:
* - **No `movable`**: ceiling move is bespoke via legacy `MoveCeilingTool`
* + the floor-plan boundary / hole editors. Capability-driven dispatch
* keeps the legacy mover (preserves polygon-aware behavior).
* - **`surfaces.top`**: items host on the ceiling at `height`.
* - `selectable`, `duplicable`, `deletable` standard.
* **Stage B intentionally skipped**: pure `def.geometry` extraction
* would lose the React children rendering (hosted items) and the
* named-mesh structure. Ceiling keeps `def.renderer` as the custom
* escape hatch (per plans/editor-node-registry.md "custom-behavior
* escape hatch"). Renderer wraps the legacy CeilingRenderer; system
* wraps the legacy CeilingSystem.
*
* Relations: `hosts: ['item']` for ceiling-mounted items (lights, fans).
* `cascadeDelete: 'descendants'` removes hosted items on ceiling delete.
* **Stage C completed**: `def.floorplan` builder draws the ceiling
* polygon as a dashed outline in floor plan; legacy `ceilingPolygons`
* short-circuits to [] when ceiling is registered.
*/
export const ceilingDefinition: NodeDefinition<typeof CeilingNode> = {
kind: 'ceiling',
@@ -59,6 +62,7 @@ export const ceilingDefinition: NodeDefinition<typeof CeilingNode> = {
module: () => import('./system'),
priority: 4,
},
floorplan: buildCeilingFloorplan,
toolHints: [
{ key: 'Left click', label: 'Trace ceiling outline' },
+37
View File
@@ -0,0 +1,37 @@
import type { CeilingNode, FloorplanGeometry, FloorplanPoint } from '@pascal-app/core'
/**
* Stage C floor-plan builder for ceiling. Renders the polygon outline
* as a dashed boundary (ceilings are above and would visually obscure
* the slab/walls if drawn solid). Same shape as slab but visually
* distinct.
*/
export function buildCeilingFloorplan(node: CeilingNode): FloorplanGeometry | null {
const polygon = node.polygon
if (!polygon || polygon.length < 3) return null
const outer: FloorplanPoint[] = polygon.map(([x, z]) => [x, z] as FloorplanPoint)
const ring = (points: FloorplanPoint[]) => {
const [first, ...rest] = points
if (!first) return ''
return [`M ${first[0]} ${first[1]}`, ...rest.map(([x, y]) => `L ${x} ${y}`), 'Z'].join(' ')
}
const segments: string[] = [ring(outer)]
const holes = node.holes ?? []
for (const hole of holes) {
if (hole.length < 3) continue
segments.push(ring(hole.map(([x, z]) => [x, z] as FloorplanPoint)))
}
return {
kind: 'path',
d: segments.join(' '),
fill: 'none',
stroke: '#94a3b8',
strokeWidth: 0.03,
strokeDasharray: '0.15 0.1',
opacity: 0.7,
}
}