118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
import { useRef, useCallback, useEffect } from 'react';
|
|
import { useControlMode, genConnId } from './useControlMode';
|
|
import { bezierPath } from '../ConnectionLine';
|
|
|
|
/**
|
|
* 포트 연결 드래그 로직 (mockup startPortDrag/finishPortDrag 포팅)
|
|
* output 포트 mousedown → 임시 선 → input 포트 mouseup → 연결 생성
|
|
*/
|
|
export function usePortDrag(canvasRef: React.RefObject<HTMLDivElement | null>) {
|
|
const { addRuleConnection, ruleConnections } = useControlMode();
|
|
|
|
const dragRef = useRef<{
|
|
fromNodeId: string;
|
|
fromPort: string;
|
|
line: SVGPathElement;
|
|
x1: number;
|
|
y1: number;
|
|
} | null>(null);
|
|
|
|
const startDrag = useCallback((nodeId: string, port: string, e: React.MouseEvent) => {
|
|
const cv = canvasRef.current;
|
|
if (!cv) return;
|
|
|
|
// SVG 확보
|
|
let svg = cv.querySelector('#rule-svg') as SVGSVGElement | null;
|
|
if (!svg) {
|
|
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
svg.id = 'rule-svg';
|
|
svg.classList.add('ctrl-svg');
|
|
svg.setAttribute('width', '100%');
|
|
svg.setAttribute('height', '100%');
|
|
svg.style.overflow = 'visible';
|
|
cv.appendChild(svg);
|
|
}
|
|
|
|
const cr = cv.getBoundingClientRect();
|
|
const portEl = (e.target as HTMLElement).closest('.ctrl-io-port') as HTMLElement;
|
|
if (!portEl) return;
|
|
const pr = portEl.getBoundingClientRect();
|
|
const x1 = pr.left + pr.width / 2 - cr.left + cv.scrollLeft;
|
|
const y1 = pr.top + pr.height / 2 - cr.top + cv.scrollTop;
|
|
|
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
line.classList.add('rule-temp-line');
|
|
line.setAttribute('d', `M${x1},${y1} L${x1},${y1}`);
|
|
svg.appendChild(line);
|
|
|
|
dragRef.current = { fromNodeId: nodeId, fromPort: port, line, x1, y1 };
|
|
cv.classList.add('port-dragging');
|
|
portEl.classList.add('port-active');
|
|
}, [canvasRef]);
|
|
|
|
const finishDrag = useCallback((toNodeId: string, toPort: string) => {
|
|
const d = dragRef.current;
|
|
if (!d) return;
|
|
|
|
// 같은 노드 연결 방지
|
|
if (d.fromNodeId === toNodeId) {
|
|
cleanup();
|
|
return;
|
|
}
|
|
// 중복 방지
|
|
if (ruleConnections.find((c) =>
|
|
c.from_node_id === d.fromNodeId && c.from_port === d.fromPort && c.to_node_id === toNodeId
|
|
)) {
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
addRuleConnection({
|
|
id: genConnId(),
|
|
from_node_id: d.fromNodeId,
|
|
from_port: d.fromPort,
|
|
to_node_id: toNodeId,
|
|
to_port: toPort,
|
|
});
|
|
|
|
cleanup();
|
|
}, [addRuleConnection, ruleConnections]);
|
|
|
|
const cleanup = useCallback(() => {
|
|
const d = dragRef.current;
|
|
if (!d) return;
|
|
d.line.remove();
|
|
canvasRef.current?.classList.remove('port-dragging');
|
|
document.querySelectorAll('.port-active').forEach((el) => el.classList.remove('port-active'));
|
|
document.querySelectorAll('.port-hover').forEach((el) => el.classList.remove('port-hover'));
|
|
dragRef.current = null;
|
|
}, [canvasRef]);
|
|
|
|
// 마우스 이동/종료 전역 핸들러
|
|
useEffect(() => {
|
|
const onMove = (e: MouseEvent) => {
|
|
const d = dragRef.current;
|
|
if (!d) return;
|
|
const cv = canvasRef.current;
|
|
if (!cv) return;
|
|
const cr = cv.getBoundingClientRect();
|
|
const x2 = e.clientX - cr.left + cv.scrollLeft;
|
|
const y2 = e.clientY - cr.top + cv.scrollTop;
|
|
d.line.setAttribute('d', bezierPath(d.x1, d.y1, x2, y2));
|
|
};
|
|
|
|
const onUp = () => {
|
|
if (dragRef.current) cleanup();
|
|
};
|
|
|
|
document.addEventListener('mousemove', onMove);
|
|
document.addEventListener('mouseup', onUp);
|
|
return () => {
|
|
document.removeEventListener('mousemove', onMove);
|
|
document.removeEventListener('mouseup', onUp);
|
|
};
|
|
}, [canvasRef, cleanup]);
|
|
|
|
return { startDrag, finishDrag };
|
|
}
|