Phase 5 batch: door + window migrate to registry (always-on)

Both kinds share traits — hosted on walls, cuttable, animated open/
close state via a geometry system + animation system. Stage A
migration: register + wrap-export the legacy renderer + bundle both
per-kind systems. Pure geometry + floor-plan ports are later
milestones.

Files added (packages/nodes/src/door/, packages/nodes/src/window/):
 - schema.ts: re-export from core.
 - parametrics.ts: minimal — dimensions only. Door has 29 sliders +
   segmented controls + presets in its legacy panel; window has 15+
   sliders. Auto-inspector can't cover them at Stage A — legacy
   panel keeps rendering via panel-manager.tsx case fall-through.
   Stage E may extend parametrics or use parametrics.customPanel
   escape hatch.
 - definition.ts: capabilities (no `movable` — wall-bound drag is
   bespoke; capability-driven dispatch keeps legacy MoveDoorTool /
   MoveWindowTool), parametrics, renderer, system. defaults() uses
   `DoorNode.parse({...stub})` to leverage zod's schema-level
   `.default()` annotations — door has 40+ fields, window has 20+;
   listing them inline duplicates the schema.
 - renderer.tsx: wrap-export of legacy DoorRenderer / WindowRenderer
   (thin 33-36 lines each).
 - system.tsx: bundles each kind's TWO systems — DoorSystem +
   DoorAnimationSystem, WindowSystem + WindowAnimationSystem. Both
   per-kind systems mount via RegisteredSystems when the kind is
   registry-driven; `<LegacySystem kind="door|window">` wrappers
   around each individual system short-circuit.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for DoorRenderer,
   DoorSystem, DoorAnimationSystem, WindowRenderer, WindowSystem,
   WindowAnimationSystem.
 - packages/nodes/src/index.ts: appends doorDefinition + windowDefinition.
 - packages/editor/src/components/ui/panels/door-panel.tsx + window-
   panel.tsx: panel slider-drag fix recipe applied. Drop the
   subscribed `updateNode` action, drop the `node` dep from
   handleUpdate / previewDoorUpdate / commitDoorPreview useCallbacks.
   Use useScene.getState() inside. Door panel has 29 SliderControls,
   window 15+ — both at high risk of the Maximum update depth
   cascade without the fix.

Phase 5 progress: shelf , spawn , wall , fence , slab , ceiling ,
door , window . Eight kinds on the registry. Item / stair / roof /
zone / containers remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-15 15:10:17 -04:00
co-authored by Claude Opus 4.7
parent 2dd50fa5be
commit 9eced06f32
16 changed files with 306 additions and 12 deletions
+76
View File
@@ -0,0 +1,76 @@
import type { NodeDefinition } from '@pascal-app/core'
import { doorParametrics } from './parametrics'
import { DoorNode } from './schema'
/**
* Door — Phase 5 batch kind. Hosted on walls, cuts holes in them,
* animated open/close state.
*
* Capabilities:
* - **No `movable`**: door's move is bespoke wall-bound drag (slide
* along the wall, snap to wall start/end). Capability-driven dispatch
* keeps legacy `MoveDoorTool`.
* - `selectable`, `duplicable`, `deletable` standard.
*
* Relations:
* - `parentId` references a wall — re-anchors on wall move (handled by
* `DoorSystem`'s cascade to parent wall).
* - `cascadeDelete: 'children'` — door has no children in v1.
*
* Renderer + system: wrap-export legacy `DoorRenderer` + bundle
* `DoorSystem` + `DoorAnimationSystem`.
*
* Tool field absent: door placement / move tools wired through editor
* state, not registry dispatch. Legacy DoorTool / MoveDoorTool continue.
*/
export const doorDefinition: NodeDefinition<typeof DoorNode> = {
kind: 'door',
schemaVersion: 1,
schema: DoorNode,
category: 'structure',
// Leverage the schema's zod `.default()` annotations to compute the
// full default shape — door has 40+ fields, listing them inline would
// duplicate the schema. Parse a minimal stub, drop id/type, return rest.
defaults: () => {
const stub = DoorNode.parse({ id: 'door_default' as never, type: 'door' })
const { id: _id, type: _type, ...rest } = stub
return rest
},
capabilities: {
selectable: { hitVolume: 'bbox' },
duplicable: true,
deletable: true,
},
parametrics: doorParametrics,
renderer: {
kind: 'parametric',
module: () => import('./renderer'),
},
system: {
module: () => import('./system'),
// Priority 3 mirrors the legacy DoorSystem (after animation at 2,
// before wall mitering at 4).
priority: 3,
},
toolHints: [
{ key: 'Left click', label: 'Place door on wall' },
{ key: 'Esc', label: 'Cancel' },
],
presentation: {
label: 'Door',
description: 'A door cut into a wall. Animated open/close state.',
icon: { kind: 'iconify', name: 'lucide:door-open' },
paletteSection: 'structure',
paletteOrder: 50,
},
mcp: {
description: 'A door mounted on a wall, with type / dimensions / hardware options.',
},
}
+2
View File
@@ -0,0 +1,2 @@
export { doorDefinition } from './definition'
export { DoorNode } from './schema'
+32
View File
@@ -0,0 +1,32 @@
import type { ParametricDescriptor } from '@pascal-app/core'
import type { DoorNode } from './schema'
/**
* Minimal inspector descriptor for door. The legacy `<DoorPanel>` has
* 29 SliderControls covering segments, hardware, hinges, panic bar,
* opening shape, etc. — too elaborate for the auto-inspector at Stage A.
* Legacy panel keeps rendering via the hardcoded `case 'door':` in
* panel-manager.tsx. This descriptor only exposes the simple dimension
* fields so the registry knows door has parametric data. Phase 5 Stage E
* (drop legacy panel) will extend this — likely via
* `parametrics.customPanel?` since door has too much non-numeric UI
* (segmented controls, presets) to fit the generic auto-UI.
*/
export const doorParametrics: ParametricDescriptor<DoorNode> = {
groups: [
{
label: 'Dimensions',
fields: [
{ key: 'width', kind: 'number', unit: 'm', min: 0.5, max: 6, step: 0.05 },
{ key: 'height', kind: 'number', unit: 'm', min: 1.0, max: 4, step: 0.05 },
],
},
{
label: 'Frame',
fields: [
{ key: 'frameThickness', kind: 'number', unit: 'm', min: 0.01, max: 0.2, step: 0.005 },
{ key: 'frameDepth', kind: 'number', unit: 'm', min: 0.01, max: 0.3, step: 0.005 },
],
},
],
}
+11
View File
@@ -0,0 +1,11 @@
'use client'
import { DoorRenderer } from '@pascal-app/viewer'
/**
* Wrap-export of the legacy `DoorRenderer`. The renderer is 33 lines
* (thin placeholder + register + dirty-on-mount) — could be duplicated
* but at Stage A re-export is sufficient. Phase 5 Stage F will inline
* it here and delete the viewer-side file.
*/
export default DoorRenderer
+1
View File
@@ -0,0 +1 @@
export { DoorNode } from '@pascal-app/core'
+34
View File
@@ -0,0 +1,34 @@
'use client'
import { DoorAnimationSystem, DoorSystem } from '@pascal-app/viewer'
/**
* Registry-driven door system bundle. Door has TWO per-frame systems:
*
* - **`DoorSystem`** — rebuilds frame / leaf / glass / hardware
* geometry from `dirtyNodes`. Cascades dirty to the parent wall so
* the wall cutout reflects the new door footprint.
* - **`DoorAnimationSystem`** — advances `operationState` (open/close
* angle for hinged, slide offset for sliding/pocket, fold angle for
* folding) at frame priority 2, then marks the door dirty so the
* geometry system rebuilds at priority 3.
*
* Both are wrapped in `<LegacySystem kind="door">` at the legacy mount
* point; with door registered, those wrappers short-circuit and this
* bundle takes over.
*
* Future Phase 5 Stage B: extract the geometry into a pure
* `buildDoorGeometry(node, ctx)` and migrate to `def.geometry`. The
* animation system stays as `def.system` (it's a real per-frame
* concern, not a geometry build).
*/
const DoorSystems = () => {
return (
<>
<DoorAnimationSystem />
<DoorSystem />
</>
)
}
export default DoorSystems