ai-chatbot/packages/nodes/src/item/definition.ts
Wassim SAMAD 9a481f2511 registry: generic FloorElevationSystem driven by a floorPlaced capability
Symptom: a shelf placed on a level with a raised slab underneath
visually clipped through it — `ItemSystem` lifted items onto slabs,
but shelves (and other floor-placed registry kinds) had no equivalent
path.

Fix: lift the slab-elevation logic out of `ItemSystem` into a generic
`<FloorElevationSystem>` keyed off a new `capabilities.floorPlaced`
config. Any kind that opts in declares a `footprint(node)` (dimensions
+ rotation used to query overlapping slabs) and an optional `applies`
predicate (skips items whose `asset.attachTo` is wall / ceiling).

The new system runs at frame priority 1 so its `mesh.position.y`
override lands before `ItemSystem` / `GeometrySystem` (priority 2)
clear the dirty mark. The spatial-grid sync's `markNodesOverlappingSlab`
also dropped its hardcoded `item` branch in favour of an iteration over
every registered kind that declares `floorPlaced` — so any new
floor-placed kind picks up slab-driven re-elevation automatically.

Tagged kinds:
 - `item`  — `footprint = getScaledDimensions`, `applies = !asset.attachTo`
 - `shelf` — `footprint = (w, h, d)`
 - `column` — `footprint = (w, h, d)`
 - `spawn` — `footprint = (0.6, 1.8, 0.6)` (marker)

`ItemSystem` retains only the wall-side z-offset block (`mesh.position.z =
wallThickness / 2`). The elevation block + its `getScaledDimensions` /
`resolveLevelId` / `spatialGridManager` imports moved to the generic
system.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:44:21 -04:00

140 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ItemNode as ItemNodeType, NodeDefinition } from '@pascal-app/core'
import { buildItemFloorplan } from './floorplan'
import { itemFloorplanMoveTarget } from './floorplan-move'
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.
*
* Stages:
* - A: registered.
* - B: N/A — def.renderer escape hatch (GLB / useGLTF).
* - C: `def.floorplan` resolves parent chain via `ctx.resolve`,
* returns a rotated rectangle (width × depth). Mirrors the legacy
* `getItemFloorplanTransform` math. Legacy `floorplanItemEntries`
* short-circuits when item is registered.
*
* `toolHints`: matches the legacy ItemHelper UI (mouse / R / T / Shift /
* Esc) — registry-driven placement panel.
*/
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,
// Floor items get lifted by slabs underneath via the generic
// `<FloorElevationSystem>`. Wall- / ceiling-attached items live in
// their parent's local frame and skip the lift via `applies`.
floorPlaced: {
footprint: (node) => {
const item = node as ItemNodeType
return { dimensions: getScaledDimensions(item), rotation: item.rotation }
},
applies: (node) => !(node as ItemNodeType).asset.attachTo,
},
},
parametrics: itemParametrics,
renderer: {
kind: 'parametric',
module: () => import('./renderer'),
},
system: {
module: () => import('./system'),
// Same priority as the legacy ItemSystem.
priority: 2,
},
// Catalog placement tool — mounted when `useEditor.tool === 'item'`.
// Wraps the same placement coordinator the move-tool uses (surface
// strategies for floor / wall / ceiling / item-surface). Replaces
// the legacy `editor/src/components/tools/item/item-tool.tsx`.
tool: () => import('./tool'),
// Stage D — 3D move-tool (registry-driven). Adopts the moving node
// and runs the placement coordinator with surface strategies for
// floor / wall / ceiling / item-surface, including attachTo
// *transitions* (drop a wall item on a ceiling and have it switch).
// Replaces the legacy `MoveItemContent` in editor's dispatcher; the
// `getRegistryAffordanceTool('item', 'move')` lookup picks this up.
affordanceTools: {
move: () => import('./move-tool'),
},
// Stage C: floor-plan polygon. ctx.resolve walks the parent chain
// (wall / nested item / level) to compute the world-space transform.
floorplan: buildItemFloorplan,
// 2D move-on-floorplan handler. Branches on `asset.attachTo`:
// wall items snap to walls (like door / window), ceiling items
// snap to ceiling polygons, floor items snap to slabs. attachTo
// *transitions* (drop a wall item on a ceiling) remain canonical
// in the 3D path; 2D only re-anchors within the same family.
floorplanMoveTarget: itemFloorplanMoveTarget,
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: 'url', src: '/icons/item.png' },
paletteSection: 'furnish',
paletteOrder: 10,
},
mcp: {
description:
'A catalog-backed item with asset reference, transforms, and optional attachTo for wall/ceiling mounting.',
},
}