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:
@@ -5,6 +5,7 @@ import {
|
||||
type FenceNode,
|
||||
type GridEvent,
|
||||
type LevelNode,
|
||||
nodeRegistry,
|
||||
type RoofNode,
|
||||
type RoofSegmentNode,
|
||||
resolveAlignment,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
import {
|
||||
CursorSphere,
|
||||
clearRoofDuplicateMetadata,
|
||||
getFloorStackPreviewPosition,
|
||||
snapFenceDraftPoint,
|
||||
triggerSFX,
|
||||
useEditor,
|
||||
@@ -113,6 +115,11 @@ export const MoveRoofTool: React.FC<{
|
||||
|
||||
// Track pending rotation — no store updates during drag
|
||||
let pendingRotation: number = movingNode.rotation as number
|
||||
let lastLocalPosition: [number, number, number] = [
|
||||
movingNode.position[0],
|
||||
movingNode.position[1],
|
||||
movingNode.position[2],
|
||||
]
|
||||
|
||||
// For roof-segment moves: the selection was cleared before entering move mode,
|
||||
// so isSelected=false on the parent roof, hiding individual segment meshes and
|
||||
@@ -150,6 +157,20 @@ export const MoveRoofTool: React.FC<{
|
||||
}
|
||||
|
||||
const levelId = resolveLevelId()
|
||||
const isFloorPlaced = nodeRegistry.get(movingNode.type)?.capabilities?.floorPlaced !== undefined
|
||||
const getPreviewPosition = (
|
||||
position: [number, number, number],
|
||||
rotation = pendingRotation,
|
||||
): [number, number, number] => {
|
||||
if (!isFloorPlaced) return position
|
||||
return getFloorStackPreviewPosition({
|
||||
node: movingNode,
|
||||
position,
|
||||
rotation,
|
||||
levelId,
|
||||
nodes: useScene.getState().nodes,
|
||||
})
|
||||
}
|
||||
const levelNode =
|
||||
levelId && useScene.getState().nodes[levelId as AnyNodeId]?.type === 'level'
|
||||
? (useScene.getState().nodes[levelId as AnyNodeId] as LevelNode)
|
||||
@@ -260,20 +281,28 @@ export const MoveRoofTool: React.FC<{
|
||||
}
|
||||
|
||||
previousGridPosRef.current = [gridX, gridZ]
|
||||
setCursorWorldPos([lx, event.localPosition[1], lz])
|
||||
|
||||
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
||||
lastLocalPosition = [localX, movingNode.position[1], localZ]
|
||||
const previewPosition = getPreviewPosition(lastLocalPosition)
|
||||
setCursorWorldPos(isFloorPlaced ? previewPosition : [lx, event.localPosition[1], lz])
|
||||
|
||||
// Directly update the Three.js mesh — no store update during drag
|
||||
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
||||
if (mesh) {
|
||||
mesh.position.x = localX
|
||||
mesh.position.z = localZ
|
||||
if (isFloorPlaced) {
|
||||
mesh.position.set(...previewPosition)
|
||||
} else {
|
||||
mesh.position.x = localX
|
||||
mesh.position.z = localZ
|
||||
}
|
||||
}
|
||||
|
||||
// Publish world-space position so the 2D floorplan can track the drag
|
||||
// Publish canonical position so the 2D floorplan can track the drag.
|
||||
// Floor-placed parents (stairs) stay in their committed local frame;
|
||||
// the lifted Y remains presentation-only in the 3D view.
|
||||
useLiveTransforms.getState().set(movingNode.id, {
|
||||
position: [gridX, y, gridZ],
|
||||
position: isFloorPlaced ? lastLocalPosition : [gridX, y, gridZ],
|
||||
rotation: pendingRotation,
|
||||
})
|
||||
}
|
||||
@@ -359,7 +388,14 @@ export const MoveRoofTool: React.FC<{
|
||||
|
||||
// Directly update the Three.js mesh — no store update during drag
|
||||
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
||||
if (mesh) mesh.rotation.y = pendingRotation
|
||||
if (mesh) {
|
||||
mesh.rotation.y = pendingRotation
|
||||
if (isFloorPlaced) {
|
||||
const previewPosition = getPreviewPosition(lastLocalPosition, pendingRotation)
|
||||
mesh.position.set(...previewPosition)
|
||||
setCursorWorldPos(previewPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Update live transform rotation for 2D floorplan
|
||||
const currentLive = useLiveTransforms.getState().get(movingNode.id)
|
||||
|
||||
Reference in New Issue
Block a user