99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
import {
|
|
type AnyNode,
|
|
type AnyNodeId,
|
|
type ColumnNode,
|
|
collectAlignmentAnchors,
|
|
type FloorplanMoveTarget,
|
|
type FloorplanMoveTargetSession,
|
|
movingFootprintAnchors,
|
|
sceneRegistry,
|
|
useLiveTransforms,
|
|
useScene,
|
|
} from '@pascal-app/core'
|
|
import {
|
|
applyFloorplanAlignment,
|
|
getFloorStackPreviewPosition,
|
|
isGridSnapActive,
|
|
isMagneticSnapActive,
|
|
triggerSFX,
|
|
useEditor,
|
|
type WallPlanPoint,
|
|
} from '@pascal-app/editor'
|
|
import { createFloorplanCursorResolver } from '../shared/floorplan-cursor'
|
|
|
|
/**
|
|
* 2D floor-plan move handler for column. Columns need the same footprint-edge
|
|
* alignment as shelf / item, but they must preview through live transforms and
|
|
* commit once on release so the overlay does not churn the scene store on
|
|
* every pointermove.
|
|
*
|
|
* Column stores rotation as a scalar (not a tuple); position is `[x, y, z]`.
|
|
*/
|
|
|
|
export const columnFloorplanMoveTarget: FloorplanMoveTarget<ColumnNode> = ({ node, nodes }) => {
|
|
const columnId = node.id as AnyNodeId
|
|
const originalPosition: [number, number, number] = [...node.position] as [number, number, number]
|
|
const rotationY = node.rotation ?? 0
|
|
const resolveCursor = createFloorplanCursorResolver({
|
|
original: [originalPosition[0], originalPosition[2]],
|
|
metadata: node.metadata,
|
|
})
|
|
let lastPosition: [number, number, number] = originalPosition
|
|
let lastSnapKey: string | null = null
|
|
|
|
// Alignment candidates gathered once — scene is stable during the drag.
|
|
const candidates = collectAlignmentAnchors(nodes, columnId)
|
|
|
|
const session: FloorplanMoveTargetSession = {
|
|
affectedIds: [columnId],
|
|
apply({ planPoint }) {
|
|
const snap = (value: number) => {
|
|
if (!isGridSnapActive()) return value
|
|
const step = useEditor.getState().gridSnapStep
|
|
return Math.round(value / step) * step
|
|
}
|
|
const gridSnapped = resolveCursor(planPoint, { snap }) as WallPlanPoint
|
|
// Figma-style alignment layered on the active snap mode.
|
|
const { point: snapped } = applyFloorplanAlignment(
|
|
gridSnapped,
|
|
movingFootprintAnchors(
|
|
node as unknown as AnyNode,
|
|
gridSnapped[0],
|
|
gridSnapped[1],
|
|
rotationY,
|
|
),
|
|
candidates,
|
|
{ applySnap: isMagneticSnapActive() },
|
|
)
|
|
const next: [number, number, number] = [snapped[0], originalPosition[1], snapped[1]]
|
|
lastPosition = next
|
|
|
|
const snapKey = `${snapped[0]},${snapped[1]}`
|
|
if (snapKey !== lastSnapKey) {
|
|
triggerSFX('sfx:grid-snap')
|
|
lastSnapKey = snapKey
|
|
}
|
|
const visualPosition = getFloorStackPreviewPosition({
|
|
node,
|
|
position: next,
|
|
rotation: rotationY,
|
|
levelId: node.parentId ?? null,
|
|
})
|
|
sceneRegistry.nodes.get(columnId)?.position.set(...visualPosition)
|
|
useLiveTransforms.getState().set(columnId, {
|
|
position: next,
|
|
rotation: rotationY,
|
|
})
|
|
},
|
|
canCommit() {
|
|
const live = useScene.getState().nodes[columnId] as ColumnNode | undefined
|
|
if (live?.type !== 'column') return false
|
|
return !(lastPosition[0] === originalPosition[0] && lastPosition[2] === originalPosition[2])
|
|
},
|
|
commit() {
|
|
useScene.getState().updateNodes([{ id: columnId, data: { position: lastPosition } }])
|
|
},
|
|
}
|
|
return session
|
|
}
|