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>
This commit is contained in:
Wassim SAMAD
2026-05-19 19:44:21 -04:00
co-authored by Claude Opus 4.7
parent 78e3ed13d8
commit 9a481f2511
10 changed files with 170 additions and 43 deletions
@@ -1,11 +1,5 @@
import {
type AnyNode,
type AnyNodeId,
getScaledDimensions,
type ItemNode,
type SlabNode,
type WallNode,
} from '../../schema'
import { nodeRegistry } from '../../registry'
import type { AnyNode, AnyNodeId, SlabNode, WallNode } from '../../schema'
import useScene from '../../store/use-scene'
import {
itemOverlapsPolygon,
@@ -135,31 +129,35 @@ function markNodesOverlappingSlab(
const slabLevelId = resolveLevelId(slab, nodes)
for (const node of Object.values(nodes)) {
if (node.type === 'item') {
const item = node as ItemNode
// Only floor items are affected by slabs
if (item.asset.attachTo) continue
if (resolveLevelId(node, nodes) !== slabLevelId) continue
if (
itemOverlapsPolygon(
item.position,
getScaledDimensions(item),
item.rotation,
slab.polygon,
0.01,
)
) {
markDirty(node.id)
}
} else if (node.type === 'wall') {
if (node.type === 'wall') {
const wall = node as WallNode
if (resolveLevelId(node, nodes) !== slabLevelId) continue
if (wallOverlapsPolygon(wall.start, wall.end, slab.polygon)) {
markDirty(node.id)
}
} else if (node.type === 'stair') {
continue
}
if (node.type === 'stair') {
if (resolveLevelId(node, nodes) !== slabLevelId) continue
markDirty(node.id)
continue
}
// Generic floor-placed sweep: any registry kind that opts in via
// `capabilities.floorPlaced` (item / shelf / column / spawn / …)
// re-elevates through `<FloorElevationSystem>` when a slab below
// changes. We dirty-mark when the kind's footprint overlaps the
// changed slab so the system picks it up next frame.
const def = nodeRegistry.get(node.type)
const floorPlaced = def?.capabilities?.floorPlaced
if (!floorPlaced) continue
if (floorPlaced.applies && !floorPlaced.applies(node)) continue
if (resolveLevelId(node, nodes) !== slabLevelId) continue
const position = (node as { position?: [number, number, number] }).position
if (!position) continue
const { dimensions, rotation } = floorPlaced.footprint(node)
if (itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) {
markDirty(node.id)
}
}
}
+18
View File
@@ -735,6 +735,7 @@ export type Capabilities = {
groupable?: boolean
selectable?: SelectableConfig
interactive?: boolean
floorPlaced?: FloorPlacedConfig
}
export type CapabilityCtx = { node: AnyNode }
@@ -795,6 +796,23 @@ export type SelectableConfig = {
override?: (ctx: CapabilityCtx) => SelectableConfig | null
}
/**
* Floor-placed kinds rest directly on a level and need their Y lifted by
* any slab the footprint overlaps. The generic `<FloorElevationSystem>`
* computes `slabElevation + node.position[1]` and writes it onto the
* registered mesh on every dirty mark. `footprint` returns the world-space
* footprint the spatial-grid manager uses to find overlapping slabs;
* `applies` is an optional predicate to skip nodes that share a kind but
* are mounted off-floor (items attached to a wall / ceiling).
*/
export type FloorPlacedConfig = {
footprint: (node: AnyNode) => {
dimensions: [number, number, number]
rotation: [number, number, number]
}
applies?: (node: AnyNode) => boolean
}
// ─── Relations ───────────────────────────────────────────────────────
export type Relations = {