Phase 5 batch kind: ceiling migrates to registry (always-on)

Structurally identical to slab. Stage A migration: registers the kind,
wraps the legacy renderer + system, applies the panel slider-drag fix
recipe.

Files added (packages/nodes/src/ceiling/):
 - schema.ts: re-exports CeilingNode from core.
 - parametrics.ts: height slider only. Polygon + holes via floor-plan
   editors.
 - definition.ts: capabilities (no `movable`, `surfaces.top` mapped to
   `height`), relations (hosts: ['item'] for ceiling-mounted lights /
   fans, cascadeDelete: 'descendants'), toolHints, parametrics.
 - renderer.tsx: wrap-export of legacy CeilingRenderer. The legacy
   renderer uses TSL shader code for grid-line patterns (~100 lines);
   not worth duplicating at Stage A. Per-stage migration plan in
   plans/editor-node-registry.md moves the renderer body into this
   folder at Stage B/F.
 - system.tsx: re-exports legacy CeilingSystem.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for CeilingRenderer
   + CeilingSystem.
 - packages/nodes/src/index.ts: appends ceilingDefinition.
 - packages/editor/src/components/ui/panels/ceiling-panel.tsx: panel
   slider-drag fix recipe applied (nodeRef pattern, useScene.getState()
   inside handler, drop subscribed updateNode dep) so the height
   slider doesn't trigger the same cascade fence + wall + slab fixed.

Phase 5 progress: shelf , spawn , wall , fence , slab , ceiling .
Six kinds on the registry. Door / window / item / stair / roof / zone
follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 14:32:01 -04:00
co-authored by Claude Opus 4.7
parent 4891f681f3
commit 2dd50fa5be
9 changed files with 151 additions and 4 deletions
+80
View File
@@ -0,0 +1,80 @@
import type { NodeDefinition } from '@pascal-app/core'
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`.
*
* 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.
*
* Relations: `hosts: ['item']` for ceiling-mounted items (lights, fans).
* `cascadeDelete: 'descendants'` removes hosted items on ceiling delete.
*/
export const ceilingDefinition: NodeDefinition<typeof CeilingNode> = {
kind: 'ceiling',
schemaVersion: 1,
schema: CeilingNode,
category: 'structure',
defaults: () => ({
object: 'node',
parentId: null,
visible: true,
metadata: {},
children: [],
polygon: [],
holes: [],
holeMetadata: [],
height: 2.5,
autoFromWalls: false,
}),
capabilities: {
selectable: { hitVolume: 'bbox' },
surfaces: {
top: { height: (n) => (n as CeilingNode).height },
},
duplicable: true,
deletable: true,
},
relations: {
hosts: ['item'],
cascadeDelete: 'descendants',
},
parametrics: ceilingParametrics,
renderer: {
kind: 'parametric',
module: () => import('./renderer'),
},
system: {
module: () => import('./system'),
priority: 4,
},
toolHints: [
{ key: 'Left click', label: 'Trace ceiling outline' },
{ key: 'Enter', label: 'Finish ceiling' },
{ key: 'Esc', label: 'Cancel' },
],
presentation: {
label: 'Ceiling',
description: 'A polygon-bounded ceiling surface that hosts ceiling-mounted items.',
icon: { kind: 'iconify', name: 'lucide:square-dashed' },
paletteSection: 'structure',
paletteOrder: 40,
},
mcp: {
description: 'A polygon-bounded ceiling with optional cutout holes.',
},
}
+2
View File
@@ -0,0 +1,2 @@
export { ceilingDefinition } from './definition'
export { CeilingNode } from './schema'
+16
View File
@@ -0,0 +1,16 @@
import type { ParametricDescriptor } from '@pascal-app/core'
import type { CeilingNode } from './schema'
/**
* Inspector descriptor for ceiling. Polygon + holes are edited via the
* floor-plan boundary / hole editors — not number inputs. Inspector
* exposes only the per-instance scalar (height).
*/
export const ceilingParametrics: ParametricDescriptor<CeilingNode> = {
groups: [
{
label: 'Dimensions',
fields: [{ key: 'height', kind: 'number', unit: 'm', min: 1.5, max: 6, step: 0.05 }],
},
],
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
import { CeilingRenderer } from '@pascal-app/viewer'
/**
* Wrap-export of the legacy `CeilingRenderer`.
*
* Ceiling's renderer uses TSL shader code for the grid-line pattern
* (~100 lines incl. material setup) — too much to duplicate at Stage A.
* The legacy file stays in viewer; the registry imports it through the
* public export. Phase 5 Stage B/F (per-kind migration stages, see
* plans/editor-node-registry.md) moves the renderer body into this
* folder and deletes the legacy file.
*/
export default CeilingRenderer
+1
View File
@@ -0,0 +1 @@
export { CeilingNode } from '@pascal-app/core'
+19
View File
@@ -0,0 +1,19 @@
'use client'
import { CeilingSystem } from '@pascal-app/viewer'
/**
* Registry-driven ceiling system bundle. Re-exports the legacy
* `CeilingSystem` so it mounts via `RegisteredSystems` when ceiling is
* registry-driven. `<LegacySystem kind="ceiling">` in viewer/components/
* viewer/index.tsx short-circuits whenever `nodeRegistry.has('ceiling')`
* is true — same shape wall / fence / slab use.
*
* Future Phase 5+: extract polygon triangulation + hole CSG into a pure
* `buildCeilingGeometry(node)` and migrate to `def.geometry`.
*/
const CeilingSystems = () => {
return <CeilingSystem />
}
export default CeilingSystems
+3
View File
@@ -1,4 +1,5 @@
import type { AnyNodeDefinition, Plugin } from '@pascal-app/core'
import { ceilingDefinition } from './ceiling'
import { fenceDefinition } from './fence'
import { shelfDefinition } from './shelf'
import { slabDefinition } from './slab'
@@ -32,9 +33,11 @@ export const builtinPlugin: Plugin = {
wallDefinition as unknown as AnyNodeDefinition,
fenceDefinition as unknown as AnyNodeDefinition,
slabDefinition as unknown as AnyNodeDefinition,
ceilingDefinition as unknown as AnyNodeDefinition,
],
}
export { ceilingDefinition } from './ceiling'
export { fenceDefinition } from './fence'
export { shelfDefinition } from './shelf'
export { slabDefinition } from './slab'