92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
import {
|
|
type AnyNode,
|
|
type ArcResizeHandle,
|
|
createSceneApi,
|
|
DEFAULT_ANGLE_STEP,
|
|
type HandleDescriptor,
|
|
hasRegistry3DMoveTool,
|
|
nodeRegistry,
|
|
type SceneApi,
|
|
useScene,
|
|
} from '@pascal-app/core'
|
|
|
|
function resolveHandles(node: AnyNode): HandleDescriptor<AnyNode>[] {
|
|
const handles = nodeRegistry.get(node.type)?.handles
|
|
if (!handles) return []
|
|
return (
|
|
typeof handles === 'function' ? handles(node as never) : handles
|
|
) as HandleDescriptor<AnyNode>[]
|
|
}
|
|
|
|
export function getDirectRotateHandle(node: AnyNode): ArcResizeHandle<AnyNode> | null {
|
|
for (const handle of resolveHandles(node)) {
|
|
if (handle.kind === 'arc-resize' && handle.shape === 'rotate') {
|
|
return handle as ArcResizeHandle<AnyNode>
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function canDirectRotateNode(node: AnyNode): boolean {
|
|
return (
|
|
getDirectRotateHandle(node) !== null ||
|
|
nodeRegistry.get(node.type)?.capabilities?.rotatable !== undefined
|
|
)
|
|
}
|
|
|
|
const BESPOKE_SELECTION_MOVE_KINDS = new Set([
|
|
'duct-segment',
|
|
'duct-fitting',
|
|
'pipe-segment',
|
|
'pipe-fitting',
|
|
'lineset',
|
|
'liquid-line',
|
|
])
|
|
|
|
export function canDirectMoveNode(node: AnyNode): boolean {
|
|
// These MEP kinds own move through bespoke selection rigs (latch cubes,
|
|
// directional arrows, grid-driven previews). Sending body drags/clicks
|
|
// through the generic direct-move handoff conflicts with that path and can
|
|
// leave the editor appearing frozen while their mover waits for the wrong
|
|
// gesture stream.
|
|
if (BESPOKE_SELECTION_MOVE_KINDS.has(node.type)) return false
|
|
// 3D direct move (Ctrl/Meta-drag, the move-cross grip) needs a move tool that
|
|
// mounts in 3D — distinct from `isRegistryMovable`, which also accepts
|
|
// floorplan-only movers (zone) for the 2D plan.
|
|
return hasRegistry3DMoveTool(node.type)
|
|
}
|
|
|
|
export function snapDirectRotationDelta(delta: number, free: boolean): number {
|
|
return free ? delta : Math.round(delta / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
|
|
}
|
|
|
|
export function resolveDirectRotationDragDelta(
|
|
startX: number,
|
|
clientX: number,
|
|
radiansPerPixel: number,
|
|
free: boolean,
|
|
): number {
|
|
return snapDirectRotationDelta((startX - clientX) * radiansPerPixel, free)
|
|
}
|
|
|
|
export function resolveDirectRotationPatch(
|
|
node: AnyNode,
|
|
delta: number,
|
|
sceneApi: SceneApi = createSceneApi(useScene),
|
|
): Partial<AnyNode> | null {
|
|
const rotateHandle = getDirectRotateHandle(node)
|
|
if (rotateHandle) {
|
|
return rotateHandle.apply(node, delta, sceneApi) as Partial<AnyNode>
|
|
}
|
|
|
|
const rotation = (node as { rotation?: unknown }).rotation
|
|
if (typeof rotation === 'number') {
|
|
return { rotation: rotation - delta } as Partial<AnyNode>
|
|
}
|
|
if (Array.isArray(rotation)) {
|
|
const [rx = 0, ry = 0, rz = 0] = rotation as [number?, number?, number?]
|
|
return { rotation: [rx, ry - delta, rz] } as Partial<AnyNode>
|
|
}
|
|
return null
|
|
}
|