139 lines
5.6 KiB
TypeScript
139 lines
5.6 KiB
TypeScript
import { useCallback } from 'react'
|
|
import { Node, Edge } from '@xyflow/react'
|
|
|
|
interface UseDiagramCommandsParams {
|
|
nodes: Node[]
|
|
edges: Edge[]
|
|
pushHistory: (nodes: Node[], edges: Edge[]) => void
|
|
setNodes: React.Dispatch<React.SetStateAction<Node[]>>
|
|
}
|
|
|
|
export function useDiagramCommands({
|
|
nodes,
|
|
edges,
|
|
pushHistory,
|
|
setNodes,
|
|
}: UseDiagramCommandsParams) {
|
|
const selectedNodes = nodes.filter(n => n.selected)
|
|
|
|
// ── Alignment ──────────────────────────────────────────────────────────
|
|
const alignLeft = useCallback(() => {
|
|
if (selectedNodes.length < 2) return
|
|
pushHistory(nodes, edges)
|
|
const minX = Math.min(...selectedNodes.map(n => n.position.x))
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected ? { ...n, position: { ...n.position, x: minX } } : n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
const alignRight = useCallback(() => {
|
|
if (selectedNodes.length < 2) return
|
|
pushHistory(nodes, edges)
|
|
const maxX = Math.max(...selectedNodes.map(n => n.position.x + (n.measured?.width ?? 100)))
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected ? { ...n, position: { ...n.position, x: maxX - (n.measured?.width ?? 100) } } : n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
const alignCenterH = useCallback(() => {
|
|
if (selectedNodes.length < 2) return
|
|
pushHistory(nodes, edges)
|
|
const minX = Math.min(...selectedNodes.map(n => n.position.x))
|
|
const maxX = Math.max(...selectedNodes.map(n => n.position.x + (n.measured?.width ?? 100)))
|
|
const centerX = (minX + maxX) / 2
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected ? { ...n, position: { ...n.position, x: centerX - (n.measured?.width ?? 100) / 2 } } : n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
const alignTop = useCallback(() => {
|
|
if (selectedNodes.length < 2) return
|
|
pushHistory(nodes, edges)
|
|
const minY = Math.min(...selectedNodes.map(n => n.position.y))
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected ? { ...n, position: { ...n.position, y: minY } } : n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
const alignBottom = useCallback(() => {
|
|
if (selectedNodes.length < 2) return
|
|
pushHistory(nodes, edges)
|
|
const maxY = Math.max(...selectedNodes.map(n => n.position.y + (n.measured?.height ?? 100)))
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected ? { ...n, position: { ...n.position, y: maxY - (n.measured?.height ?? 100) } } : n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
const alignCenterV = useCallback(() => {
|
|
if (selectedNodes.length < 2) return
|
|
pushHistory(nodes, edges)
|
|
const minY = Math.min(...selectedNodes.map(n => n.position.y))
|
|
const maxY = Math.max(...selectedNodes.map(n => n.position.y + (n.measured?.height ?? 100)))
|
|
const centerY = (minY + maxY) / 2
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected ? { ...n, position: { ...n.position, y: centerY - (n.measured?.height ?? 100) / 2 } } : n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
// ── Distribution ───────────────────────────────────────────────────────
|
|
const distributeHorizontally = useCallback(() => {
|
|
if (selectedNodes.length < 3) return
|
|
pushHistory(nodes, edges)
|
|
const sorted = [...selectedNodes].sort((a, b) => a.position.x - b.position.x)
|
|
const minX = sorted[0].position.x
|
|
const maxX = sorted[sorted.length - 1].position.x + (sorted[sorted.length - 1].measured?.width ?? 100)
|
|
const totalWidth = sorted.reduce((sum, n) => sum + (n.measured?.width ?? 100), 0)
|
|
const gap = (maxX - minX - totalWidth) / (sorted.length - 1)
|
|
let cursor = minX
|
|
const positions: Record<string, number> = {}
|
|
for (const n of sorted) {
|
|
positions[n.id] = cursor
|
|
cursor += (n.measured?.width ?? 100) + gap
|
|
}
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected && positions[n.id] !== undefined
|
|
? { ...n, position: { ...n.position, x: positions[n.id] } }
|
|
: n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
const distributeVertically = useCallback(() => {
|
|
if (selectedNodes.length < 3) return
|
|
pushHistory(nodes, edges)
|
|
const sorted = [...selectedNodes].sort((a, b) => a.position.y - b.position.y)
|
|
const minY = sorted[0].position.y
|
|
const maxY = sorted[sorted.length - 1].position.y + (sorted[sorted.length - 1].measured?.height ?? 100)
|
|
const totalHeight = sorted.reduce((sum, n) => sum + (n.measured?.height ?? 100), 0)
|
|
const gap = (maxY - minY - totalHeight) / (sorted.length - 1)
|
|
let cursor = minY
|
|
const positions: Record<string, number> = {}
|
|
for (const n of sorted) {
|
|
positions[n.id] = cursor
|
|
cursor += (n.measured?.height ?? 100) + gap
|
|
}
|
|
setNodes(prev => prev.map(n =>
|
|
n.selected && positions[n.id] !== undefined
|
|
? { ...n, position: { ...n.position, y: positions[n.id] } }
|
|
: n
|
|
))
|
|
}, [nodes, edges, selectedNodes, pushHistory, setNodes])
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
const canAlign = selectedNodes.length >= 2
|
|
const canDistribute = selectedNodes.length >= 3
|
|
|
|
return {
|
|
alignLeft,
|
|
alignRight,
|
|
alignCenterH,
|
|
alignTop,
|
|
alignBottom,
|
|
alignCenterV,
|
|
distributeHorizontally,
|
|
distributeVertically,
|
|
canAlign,
|
|
canDistribute,
|
|
selectedNodes,
|
|
}
|
|
}
|