Files
editor/packages/viewer/src/systems/item/item-system.tsx
T
Wassim SAMADandClaude Opus 4.7 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

54 lines
1.7 KiB
TypeScript

import {
type AnyNodeId,
type ItemNode,
sceneRegistry,
useScene,
type WallNode,
} from '@pascal-app/core'
import { useFrame } from '@react-three/fiber'
import type * as THREE from 'three'
// ============================================================================
// ITEM SYSTEM
// ============================================================================
/**
* Per-frame wall-side offset for items mounted to wall faces. The slab-
* elevation lift for floor items lives in the generic
* `<FloorElevationSystem>` and runs at priority 1 — it has already
* landed `mesh.position.y` by the time this system clears the dirty
* mark at priority 2.
*/
export const ItemSystem = () => {
const dirtyNodes = useScene((state) => state.dirtyNodes)
const clearDirty = useScene((state) => state.clearDirty)
useFrame(() => {
if (dirtyNodes.size === 0) return
const nodes = useScene.getState().nodes
dirtyNodes.forEach((id) => {
const node = nodes[id]
if (!node || node.type !== 'item') return
const item = node as ItemNode
const mesh = sceneRegistry.nodes.get(id) as THREE.Object3D
if (!mesh) return
if (item.asset.attachTo === 'wall-side') {
// Wall-attached item: offset Z by half the parent wall's thickness
const parentWall = item.parentId ? nodes[item.parentId as AnyNodeId] : undefined
if (parentWall && parentWall.type === 'wall') {
const wallThickness = (parentWall as WallNode).thickness ?? 0.1
const side = item.side === 'front' ? 1 : -1
mesh.position.z = (wallThickness / 2) * side
}
}
clearDirty(id as AnyNodeId)
})
}, 2)
return null
}