import type { AnyNodeId, BuildingNode, CeilingNode, ColumnNode, DoorNode, ElevatorNode, FenceNode, ItemNode, RoofNode, RoofSegmentNode, SlabNode, SpawnNode, StairNode, StairSegmentNode, WallNode, WindowNode, } from '@pascal-app/core' import { nodeRegistry } from '@pascal-app/core' import { Vector3 } from 'three' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { MoveBuildingContent } from '../building/move-building-tool' import { MoveCeilingTool } from '../ceiling/move-ceiling-tool' import { MoveColumnTool } from '../column/move-column-tool' import { MoveDoorTool } from '../door/move-door-tool' import { MoveElevatorTool } from '../elevator/move-elevator-tool' import { MoveFenceTool } from '../fence/move-fence-tool' import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool' import { MoveRoofTool } from '../roof/move-roof-tool' import { MoveSlabTool } from '../slab/move-slab-tool' import { MoveSpawnTool } from '../spawn/move-spawn-tool' import { MoveWallTool } from '../wall/move-wall-tool' import { MoveWindowTool } from '../window/move-window-tool' import type { PlacementState } from './placement-types' import { useDraftNode } from './use-draft-node' import { usePlacementCoordinator } from './use-placement-coordinator' function getInitialState(node: { asset: { attachTo?: string } parentId: string | null }): PlacementState { const attachTo = node.asset.attachTo if (attachTo === 'wall' || attachTo === 'wall-side') { return { surface: 'wall', wallId: node.parentId, ceilingId: null, surfaceItemId: null } } if (attachTo === 'ceiling') { return { surface: 'ceiling', wallId: null, ceilingId: node.parentId, surfaceItemId: null } } return { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null } } function MoveItemContent({ movingNode }: { movingNode: ItemNode }) { const draftNode = useDraftNode() const meta = typeof movingNode.metadata === 'object' && movingNode.metadata !== null ? (movingNode.metadata as Record) : {} const isNew = !!meta.isNew const cursor = usePlacementCoordinator({ asset: movingNode.asset, draftNode, // Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft initialState: isNew ? { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null } : getInitialState(movingNode), // Preserve the original item's scale so Y-position calculations use the correct height defaultScale: isNew ? movingNode.scale : undefined, initDraft: (gridPosition) => { if (isNew) { // Duplicate: use the same create() path as ItemTool so ghost rendering works correctly. // Floor items get a draft immediately; wall/ceiling items are created lazily on surface entry. gridPosition.copy(new Vector3(...movingNode.position)) if (!movingNode.asset.attachTo) { draftNode.create(gridPosition, movingNode.asset, movingNode.rotation, movingNode.scale) } } else { draftNode.adopt(movingNode) gridPosition.copy(new Vector3(...movingNode.position)) } }, onCommitted: () => { sfxEmitter.emit('sfx:item-place') useEditor.getState().setMovingNode(null) return false }, onCancel: () => { draftNode.destroy() useEditor.getState().setMovingNode(null) }, }) return <>{cursor} } export const MoveTool: React.FC<{ onNodeMoved?: (nodeId: AnyNodeId) => void onSpawnMoved?: (nodeId: SpawnNode['id']) => void }> = ({ onNodeMoved, onSpawnMoved }) => { const movingNode = useEditor((state) => state.movingNode) if (!movingNode) return null if (movingNode.type === 'building') return if (movingNode.type === 'door') return if (movingNode.type === 'elevator') return if (movingNode.type === 'window') return if (movingNode.type === 'ceiling') return if (movingNode.type === 'column') return if (movingNode.type === 'slab') return if (movingNode.type === 'wall') return if (movingNode.type === 'fence') return if (movingNode.type === 'roof' || movingNode.type === 'roof-segment') return if (movingNode.type === 'spawn') return if (movingNode.type === 'stair' || movingNode.type === 'stair-segment') return // Registry-driven kinds (any NodeDefinition with `capabilities.movable`) // get a generic position+rotation mover. Phase 4 may consolidate this // with the per-kind movers above when they all collapse to the same // shape. Must come BEFORE the MoveItemContent fallback because that // assumes the node is an ItemNode. if (nodeRegistry.has(movingNode.type)) { return } return }