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:
co-authored by
Claude Opus 4.7
parent
78e3ed13d8
commit
9a481f2511
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user