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
+13 -6
View File
@@ -15,7 +15,7 @@ import {
useAlignmentGuides,
useScene,
} from '@pascal-app/core'
import { triggerSFX } from '@pascal-app/editor'
import { getFloorStackPreviewPosition, triggerSFX } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useMemo, useRef } from 'react'
import { type Group, Vector3 } from 'three'
@@ -70,16 +70,16 @@ function getLevelLocalPosition(
const local = (event as GridEvent).localPosition
if (local) {
const [sx, sz] = snapPointToGrid([local[0], local[2]], GRID_STEP)
return [sx, local[1] ?? 0, sz]
return [sx, 0, sz]
}
const [sx, sz] = snapPointToGrid([event.position[0], event.position[2]], GRID_STEP)
return [sx, event.position[1], sz]
return [sx, 0, sz]
}
worldVector.set(event.position[0], event.position[1], event.position[2])
levelObject.updateWorldMatrix(true, false)
levelObject.worldToLocal(worldVector)
const [sx, sz] = snapPointToGrid([worldVector.x, worldVector.z], GRID_STEP)
return [sx, worldVector.y, sz]
return [sx, 0, sz]
}
const ShelfTool = () => {
@@ -150,8 +150,15 @@ const ShelfTool = () => {
useAlignmentGuides.getState().clear()
}
cursorRef.current?.position.set(ax, event.localPosition[1], az)
lastCursorRef.current = [ax, event.localPosition[1], az]
const position: [number, number, number] = [ax, 0, az]
const visualPosition = getFloorStackPreviewPosition({
node: previewNode,
position,
rotation: previewNode.rotation,
levelId: activeLevelId,
})
cursorRef.current?.position.set(...visualPosition)
lastCursorRef.current = position
const prev = previousSnapRef.current
if (!prev || prev[0] !== ax || prev[1] !== az) {