Files
resolutionflow/frontend/src/components/network/hooks/useDiagramCommands.ts
2026-04-13 20:08:37 +00:00

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,
}
}