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

Item is the first kind to use the `def.renderer` escape hatch (custom
React component with `useGLTF` + drei + interactive widgets) — not
expressible as a pure `def.geometry`. Catalog-backed + multi-host
(free / wall / wall-side / ceiling).

Files added (packages/nodes/src/item/):
 - schema.ts: re-exports ItemNode from core.
 - parametrics.ts: empty groups[]. Item parametrics come from the
   asset's catalog-defined interactive controls (toggles / sliders /
   temperature) — too dynamic for the auto-inspector at Stage A.
   Legacy ItemPanel renders the catalog-driven controls; Phase 5
   Stage E will likely use parametrics.customPanel.
 - definition.ts: capabilities (no `movable` — item move is bespoke
   MoveItemContent that handles attachTo transitions floor↔wall↔
   ceiling mid-drag; capability-driven dispatch keeps legacy mover),
   parametrics, renderer (wrap-export of ItemRenderer), system
   bundling ItemSystem + ItemLightSystem, toolHints matching the
   user's screenshot (Place item / R rotate ccw / T rotate cw /
   Shift free place / Esc cancel). defaults() casts an object
   literal with a stub asset since asset is required by the schema;
   createNode re-parses through ItemNode at runtime.
 - renderer.tsx: wrap-export of legacy ItemRenderer (~280 lines with
   useGLTF + interactive widgets — too much to duplicate at Stage A).
 - system.tsx: bundles ItemSystem + ItemLightSystem.
 - index.ts: barrel.

Files changed:
 - packages/viewer/src/index.ts: new public exports for ItemRenderer,
   ItemSystem, ItemLightSystem.
 - packages/nodes/src/index.ts: appends itemDefinition.
 - packages/editor/src/components/ui/panels/item-panel.tsx: panel
   slider-drag fix recipe applied (nodeRef pattern, drop subscribed
   updateNode dep, drop node from useCallback deps). Item panel has
   scale + position + rotation sliders all subject to the cascade.

Item is the registry's stress test for `def.renderer` escape hatch.
GLB loading via useGLTF + drei works as-is; nothing in the registry
forces a pure-geometry shape on kinds that don't fit.

Phase 5 progress: shelf  spawn  wall  fence  slab  ceiling 
door  window  item . Nine kinds on the registry. 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:23:08 -04:00
co-authored by Claude Opus 4.7
parent 9eced06f32
commit 8d65be17fa
9 changed files with 206 additions and 13 deletions
+110
View File
@@ -0,0 +1,110 @@
import type { ItemNode as ItemNodeType, NodeDefinition } from '@pascal-app/core'
import { itemParametrics } from './parametrics'
import { ItemNode } from './schema'
/**
* Item — Phase 5 batch kind. Catalog-backed, GLB-rendered, multi-host.
*
* Demonstrates the **custom `def.renderer` escape hatch** (see
* plans/editor-node-registry.md): items use `useGLTF` from drei to
* load CDN assets, plus a non-trivial interactive-widget layer inside
* the rendered scene. Not expressible as a pure `def.geometry`. The
* registry mounts the custom React renderer as-is.
*
* Capabilities:
* - **No `movable`**: item's move is bespoke `MoveItemContent` —
* handles attachTo transitions mid-drag (floor ↔ wall ↔ ceiling),
* asset.attachTo lookups, scale-preserving Y math for surface
* placement. The smooth generic mover can't express that. Legacy
* mover keeps running via capability-driven dispatch.
* - `selectable`, `duplicable`, `deletable` standard.
* - Items have a catalog-defined `surface.height` (some items act as
* tables — they expose a surface other items stack on). For Stage A
* we don't surface this via `capabilities.surfaces.top` yet —
* legacy ItemSystem computes the stack y via spatial-grid lookups.
* Phase 5+ may surface it.
*
* `toolHints`: matches the legacy ItemHelper UI (mouse / R / T / Shift /
* Esc) — same panel the user sees during placement. Once item registers,
* `HelperManager` consults `def.toolHints` and renders the
* `RegisteredToolHelper` for placement (the legacy ItemHelper still
* renders for movingNode state — that's a generic "you're moving
* something" panel, not item-specific; Phase 5+ may deprecate it).
*
* Renderer + system: wrap-export of legacy ItemRenderer + bundle of
* ItemSystem + ItemLightSystem.
*
* Tool field absent: catalog UI + item-tool placement flow stays on
* editor state. Phase 5+ may port to `DragAction` once the registry's
* catalog-aware affordances exist.
*/
export const itemDefinition: NodeDefinition<typeof ItemNode> = {
kind: 'item',
schemaVersion: 1,
schema: ItemNode,
category: 'furnish',
// Defaults shape is cast: the schema requires a fully-typed `asset`
// field, but in practice items are always created from the catalog
// (the asset is supplied at placement time). `createNode` re-parses
// through the schema, so any missing zod defaults fill at runtime.
defaults: () =>
({
object: 'node',
parentId: null,
visible: true,
metadata: {},
children: [],
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
asset: {
id: 'placeholder',
category: 'misc',
name: 'Item',
thumbnail: '',
src: 'asset:placeholder',
dimensions: [1, 1, 1],
source: 'library',
},
}) as unknown as Omit<ItemNodeType, 'id' | 'type'>,
capabilities: {
selectable: { hitVolume: 'bbox' },
duplicable: true,
deletable: true,
},
parametrics: itemParametrics,
renderer: {
kind: 'parametric',
module: () => import('./renderer'),
},
system: {
module: () => import('./system'),
// Same priority as the legacy ItemSystem.
priority: 2,
},
toolHints: [
{ key: 'Left click', label: 'Place item' },
{ key: 'R', label: 'Rotate counterclockwise' },
{ key: 'T', label: 'Rotate clockwise' },
{ key: 'Shift', label: 'Free place' },
{ key: 'Esc', label: 'Cancel' },
],
presentation: {
label: 'Item',
description: 'A catalog-backed item (furniture, fixtures, decorations).',
icon: { kind: 'iconify', name: 'lucide:armchair' },
paletteSection: 'furnish',
paletteOrder: 10,
},
mcp: {
description:
'A catalog-backed item with asset reference, transforms, and optional attachTo for wall/ceiling mounting.',
},
}
+2
View File
@@ -0,0 +1,2 @@
export { itemDefinition } from './definition'
export { ItemNode } from './schema'
+18
View File
@@ -0,0 +1,18 @@
import type { ParametricDescriptor } from '@pascal-app/core'
import type { ItemNode } from './schema'
/**
* Minimal inspector descriptor for item. Items have catalog-driven
* properties (asset.id, asset.dimensions, asset.interactive controls,
* etc.) that don't fit the auto-inspector at Stage A — those are edited
* via the legacy `<ItemPanel>` which renders the catalog-defined
* controls dynamically. Auto-inspector covers only the per-instance
* transform (uniform scale).
*
* Phase 5 Stage E (drop legacy panel) probably uses
* `parametrics.customPanel` to render the catalog-driven controls in
* a registry-aware way.
*/
export const itemParametrics: ParametricDescriptor<ItemNode> = {
groups: [],
}
+21
View File
@@ -0,0 +1,21 @@
'use client'
import { ItemRenderer } from '@pascal-app/viewer'
/**
* Wrap-export of the legacy `ItemRenderer`.
*
* Item's renderer is ~280 lines using `useGLTF` from `@react-three/drei`
* to load GLB assets from the CDN. It also handles asset-loaded
* `interactive` widgets (clickable hot-spots, sliders inside the
* scene), surface mounting, attachment offsets — too much code to
* duplicate at Stage A. Phase 5 Stage F (cleanup) moves it into this
* folder if useful, or leaves it in viewer with the public re-export.
*
* Item is also the first kind to demonstrate the "custom def.renderer"
* escape hatch documented in plans/editor-node-registry.md — kinds with
* GLB loaders, drei helpers, `useGLTF`, etc., set `def.renderer` to a
* full React component rather than trying to express geometry as a
* pure builder.
*/
export default ItemRenderer
+1
View File
@@ -0,0 +1 @@
export { ItemNode } from '@pascal-app/core'
+25
View File
@@ -0,0 +1,25 @@
'use client'
import { ItemLightSystem, ItemSystem } from '@pascal-app/viewer'
/**
* Registry-driven item system bundle.
*
* - **`ItemSystem`** — applies attachTo-driven transforms each frame
* (wall-side z-offset, slab elevation, ceiling mounting).
* - **`ItemLightSystem`** — manages light sources attached to items
* (lamps, ceiling lights, etc.).
*
* Both are wrapped in `<LegacySystem kind="item">` legacy mounts; with
* item registered, those short-circuit and this bundle takes over.
*/
const ItemSystems = () => {
return (
<>
<ItemSystem />
<ItemLightSystem />
</>
)
}
export default ItemSystems