feat(editor): live floor-stacking, unified handle system, slab-hole editing + interaction polish (#375)

- Live slab-stacking Y previews for all floor-placed kinds (item/shelf/spawn/column/stair) during placement + both move pathways, via a shared core resolver; canonical positions unchanged.
- Unified 3D handle system (one drag pipeline + one visual primitive) with forgiving invisible hit-areas on every handle, kept on EDITOR_LAYER so they don't poison the MRT scene pass.
- Hover + click-to-edit slab holes in 3D (manual hole -> hole editor; stair/elevator hole -> select owner); generic cross-arrow polygon-move grip; normalized handle interaction colors.
- NaN-safe node mutations + non-finite shadow-light bounds guard.
- Built on #373 (level-scoped alignment / registry slab tool); #373 owns X/Z alignment, this owns Y floor-stacking.
This commit is contained in:
Aymeric Rabot
2026-06-05 16:24:48 -04:00
committed by GitHub
parent d1b40aa98d
commit 0b338cf647
51 changed files with 3942 additions and 1418 deletions
+3 -1
View File
@@ -346,8 +346,10 @@ export type TranslateHandle<N = any> = {
* `[alongX, alongOther]` — `alongOther` is Z for the 'horizontal' plane and
* Y for 'node-normal'. Used to align the node's edges to the grid (rotation-
* aware: swap the pair at 90°). Omit / return null for free movement.
* `sceneApi` is supplied for composite nodes whose footprint depends on
* children, such as straight stairs.
*/
snapExtents?: (node: N) => readonly [number, number] | null
snapExtents?: (node: N, sceneApi: SceneApi) => readonly [number, number] | null
portal?: HandlePortal
}
+5
View File
@@ -57,6 +57,11 @@ export type {
CuttableConfig,
DragAction,
EditorCtx,
FloorPlacedConfig,
FloorPlacedFootprint,
FloorPlacedFootprintContext,
FloorPlacedFootprintResolver,
FloorPlacedFootprintsResolver,
FloorplanAffordance,
FloorplanAffordanceModifiers,
FloorplanAffordancePoint,
+26 -6
View File
@@ -1235,20 +1235,40 @@ export type SelectableConfig = {
override?: (ctx: CapabilityCtx) => SelectableConfig | null
}
export type FloorPlacedFootprint = {
dimensions: [number, number, number]
rotation: [number, number, number]
position?: [number, number, number]
}
export type FloorPlacedFootprintContext = {
nodes: Readonly<Record<AnyNodeId, AnyNode>>
}
export type FloorPlacedFootprintResolver = (
node: AnyNode,
ctx?: FloorPlacedFootprintContext,
) => FloorPlacedFootprint
export type FloorPlacedFootprintsResolver = (
node: AnyNode,
ctx?: FloorPlacedFootprintContext,
) => readonly FloorPlacedFootprint[]
/**
* 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;
* registered mesh on every dirty mark. `footprint` returns the default
* world-space footprint the spatial-grid manager uses to find overlapping
* slabs; `footprints` lets composite kinds expose multiple footprint
* segments, with the canonical resolver taking the max slab elevation;
* `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]
}
footprint?: FloorPlacedFootprintResolver
footprints?: FloorPlacedFootprintsResolver
applies?: (node: AnyNode) => boolean
}