feat(editor): mode-driven shelf/column/spawn placement + cross-kind floor collision

Migrate the remaining floor-placed kinds onto the unified snapping/modifier
model and generalize floor collision so any solid floor kind blocks any other.

- shelf/column/spawn declare `snapProfile: 'item'` → contextual snapping chip,
  Shift=cycle, Ctrl=grid step during placement; their tools read the active
  mode (grid/lines/off) instead of legacy Shift/Alt bypass; spawn fresh
  placement now respects alignment ("lines") like its move.
- Resize/radial handles claim the handle-drag scope (new RESIZE_HANDLE_DRAG_LABEL)
  so the HUD shows no select-mode shortcuts mid-resize.
- Column move migrated to the generic MoveRegistryNodeTool (declare `movable`,
  drop the bespoke move-tool) — gains mode-driven snapping, alignment, R/T,
  slab lift, grid SFX, and the collision box for free. 2D move still routes
  through `floorplanMoveTarget`.
- Cross-kind floor collision: new declarative `FloorPlacedConfig.collides`
  (item/shelf/column opt in; spawn/MEP/stair stay off). `canPlaceOnFloor` now
  treats every colliding floor kind as an obstacle (was item-only), reading the
  declarative footprint; the generic move tool's red/green placement box gates
  on `collides`. Column footprint uses the visible `columnFootprintHalf` extent
  so the box/slab-lift/collision track the real (round/square) column size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-24 16:22:04 -04:00
co-authored by Claude Opus 4.8
parent 04f1b0d59e
commit 8a57105eec
13 changed files with 170 additions and 409 deletions
+6 -7
View File
@@ -11,6 +11,8 @@ import {
} from '@pascal-app/core'
import {
getFloorStackPreviewPosition,
isGridSnapActive,
isMagneticSnapActive,
triggerSFX,
useAlignmentGuides,
useEditor,
@@ -87,8 +89,8 @@ const ColumnTool = () => {
rawZ: event.localPosition[2],
gridStep: useEditor.getState().gridSnapStep,
candidates: alignmentCandidates,
bypassAlignment: event.nativeEvent?.altKey === true || event.nativeEvent?.shiftKey === true,
bypassGrid: event.nativeEvent?.shiftKey === true,
bypassAlignment: !isMagneticSnapActive(),
bypassGrid: !isGridSnapActive(),
})
useAlignmentGuides.getState().set(guides)
@@ -108,10 +110,7 @@ const ColumnTool = () => {
usePlacementPreview.getState().set({ ...previewNode, position })
const prev = previousSnapRef.current
if (
event.nativeEvent?.shiftKey !== true &&
(!prev || prev[0] !== position[0] || prev[1] !== position[2])
) {
if (!prev || prev[0] !== position[0] || prev[1] !== position[2]) {
triggerSFX('sfx:grid-snap')
previousSnapRef.current = [position[0], position[2]]
}
@@ -124,7 +123,7 @@ const ColumnTool = () => {
activeLevelId,
event,
useEditor.getState().gridSnapStep,
event.nativeEvent?.shiftKey === true,
!isGridSnapActive(),
)
const column = createColumnFromPreset(DEFAULT_COLUMN_PRESET_ID, position)