UNPKG

ar-design

Version:

AR Design is a (react | nextjs) ui library.

298 lines (297 loc) 15 kB
import React, { useEffect, useMemo, useRef, useState } from "react"; import "../../../assets/css/components/data-display/diagram/styles.css"; import Grid from "../grid-system"; import Button from "../../form/button"; import Tooltip from "../../feedback/tooltip"; import { ARIcon } from "../../icons"; const { Box } = Grid; const Diagram = ({ nodes, edges }) => { // refs const _arDiagram = useRef(null); const _content = useRef(null); const _arNodes = useRef({}); const _path = useRef(null); // refs -> Start Position const _dragStartMousePosition = useRef({ x: 0, y: 0 }); const _dragStartNodePosition = useRef({ x: 0, y: 0 }); // refs -> Zoom const _zoomIntensity = 0.1; const _maxScale = 4; const _minScale = 0.1; // states const [_nodes, setNodes] = useState(nodes); const [_edges, setEdges] = useState(edges); const [trigger, setTrigger] = useState(false); // states -> Pan const [pan, setPan] = useState({ x: 0, y: 0 }); const [panning, setPanning] = useState(false); const [startPan, setStartPan] = useState({ x: 0, y: 0 }); // states -> Zoom const [scale, setScale] = useState(1); // states -> Drag const [draggedNode, setDraggedNode] = useState(null); // states -> Drawing const [drawingEdge, setDrawingEdge] = useState(null); const [mousePos, setMousePos] = useState(null); // methods const getPortCenter = (id, port) => { const node = _arNodes.current[`${id}_${port}`]; const diagram = _arDiagram.current; if (!node || !diagram) return null; const diagramRect = diagram.getBoundingClientRect(); const nodeRect = node.getBoundingClientRect(); return { x: (nodeRect.left - diagramRect.left + nodeRect.width / 2 - pan.x) / scale, y: (nodeRect.top - diagramRect.top + nodeRect.height / 2 - pan.y) / scale, }; }; const getClosestPort = (position, threshold = 20) => { for (const key in _arNodes.current) { const el = _arNodes.current[key]; if (!el) continue; const [idStr, port] = key.split("_"); const id = parseInt(idStr, 10); const rect = el.getBoundingClientRect(); const diagramRect = _arDiagram.current.getBoundingClientRect(); const portCenter = { x: (rect.left - diagramRect.left + rect.width / 2 - pan.x) / scale, y: (rect.top - diagramRect.top + rect.height / 2 - pan.y) / scale, }; const distance = Math.hypot(position.x - portCenter.x, position.y - portCenter.y); if (distance <= threshold) return { id, port: port }; } return null; }; const renderEdges = useMemo(() => { return _edges.map((edge, index) => { const from = getPortCenter(edge.from.id, edge.from.port); const to = getPortCenter(edge.to.id, edge.to.port); if (!from || !to) return null; const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.hypot(dx, dy); const offset = Math.min(40, distance * 0.25); // maksimum sapma sınırı // S biçimli kontrol noktaları const controlPoint1 = { x: from.x, y: from.y + (dy < 0 ? -offset : offset), }; const controlPoint2 = { x: to.x, y: to.y + (dy < 0 ? offset : -offset), }; const pathData = `M${from.x} ${from.y} C${controlPoint1.x} ${controlPoint1.y}, ${controlPoint2.x} ${controlPoint2.y}, ${to.x} ${to.y}`; return (React.createElement("svg", { key: index, className: "edge" }, React.createElement("path", { ref: _path, d: pathData, fill: "none", stroke: "var(--purple-500)", strokeWidth: 2, strokeDasharray: 10, strokeDashoffset: 10, strokeLinecap: "round" }, React.createElement("animate", { attributeName: "stroke-dashoffset", values: `${20 / scale};0`, dur: "1s", repeatCount: "indefinite" })))); }); }, [_nodes, _edges, trigger]); const onPanStart = (e) => { // Node sürükleniyorsa pan başlatma. if (draggedNode) return; setPanning(true); setStartPan({ x: e.clientX - pan.x, y: e.clientY - pan.y }); }; const onPanMove = (e) => { if (panning) { setPan({ x: e.clientX - startPan.x, y: e.clientY - startPan.y }); } }; const onPanEnd = () => setPanning(false); // methods -> Zoom const handleWheel = (event) => { const direction = event.deltaY > 0 ? -1 : 1; let newScale = scale + direction * _zoomIntensity; newScale = Math.max(_minScale, Math.min(_maxScale, newScale)); // Mouse'un container içindeki konumunu al. const rect = _content.current.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; // İçerik düzleminde mouse'un bulunduğu noktayı bul. const zoomPointX = (mouseX - pan.x) / scale; const zoomPointY = (mouseY - pan.y) / scale; // Yeni pan değerini hesapla ki zoomPoint sabit kalsın. const newPanX = mouseX - zoomPointX * newScale; const newPanY = mouseY - zoomPointY * newScale; setScale(newScale); setPan({ x: newPanX, y: newPanY }); }; const handleZoom = (process) => { let newScale = 0; if (process === "increment") newScale = Math.max(_minScale, Math.min(_maxScale, scale + _zoomIntensity)); if (process === "decrement") newScale = Math.max(_minScale, Math.min(_maxScale, scale - _zoomIntensity)); if (_content.current && _content.current) { const containerRect = _content.current.getBoundingClientRect(); // Ortadaki noktayı bul (container açısından) const centerX = containerRect.width / 2; const centerY = containerRect.height / 2; // İçerik düzleminde bu noktaya karşılık gelen nokta const zoomPointX = (centerX - pan.x) / scale; const zoomPointY = (centerY - pan.y) / scale; // Yeni pan hesapla ki center aynı yerde kalsın const newPanX = centerX - zoomPointX * newScale; const newPanY = centerY - zoomPointY * newScale; setPan({ x: newPanX, y: newPanY }); setScale(newScale); } }; // methods -> Node const onNodeMouseDown = (event, id, node) => { event.stopPropagation(); setDraggedNode(id); _dragStartMousePosition.current = { x: event.clientX, y: event.clientY }; _dragStartNodePosition.current = { x: node.x, y: node.y }; }; const onMouseMove = (event) => { if (drawingEdge) { const rect = _arDiagram.current.getBoundingClientRect(); const x = (event.clientX - rect.left - pan.x) / scale; const y = (event.clientY - rect.top - pan.y) / scale; setMousePos({ x, y }); } if (draggedNode) { const deltaX = (event.clientX - _dragStartMousePosition.current.x) / scale; const deltaY = (event.clientY - _dragStartMousePosition.current.y) / scale; const newX = _dragStartNodePosition.current.x + deltaX; const newY = _dragStartNodePosition.current.y + deltaY; setNodes((prev) => prev.map((node) => (node.id === draggedNode ? { ...node, position: { x: newX, y: newY } } : node))); } }; const onMouseUp = () => { if (drawingEdge && mousePos) { const closest = getClosestPort(mousePos); if (closest) { // Yakın port varsa, oraya bağla const newEdge = { id: crypto.randomUUID(), // _edges[_edges.length - 1]?.id + 1 || 1, from: { id: drawingEdge.id, port: drawingEdge.port }, to: { id: closest.id, port: closest.port }, }; // Aynı edge daha önce eklenmiş mi kontrol et const isDuplicate = _edges.some((edge) => { const samePair = (edge.from.id === newEdge.from.id && edge.to.id === newEdge.to.id) || (edge.from.id === newEdge.to.id && edge.to.id === newEdge.from.id); return samePair; }); if (!isDuplicate) setEdges((prev) => [...prev, newEdge]); } else { // Yakın port yoksa yeni node oluştur const newNodeId = crypto.randomUUID(); // _nodes[_nodes.length - 1]?.id + 1 || 1; const newNode = { id: newNodeId, position: mousePos, data: React.createElement(React.Fragment, null), }; const newPort = mousePos.y < drawingEdge.start.y ? "bottom" : "top"; const newEdge = { id: crypto.randomUUID(), // _edges[_edges.length - 1]?.id + 1 || 1, from: { id: drawingEdge.id, port: drawingEdge.port }, to: { id: newNodeId, port: newPort }, }; setNodes((prev) => [...prev, newNode]); setEdges((prev) => [...prev, newEdge]); } setDrawingEdge(null); setMousePos(null); } setDraggedNode(null); setTrigger((prev) => !prev); }; // useEffects useEffect(() => { setEdges([...edges]); }, []); return (React.createElement("div", { ref: _arDiagram, className: "ar-diagram", onMouseDown: onPanStart, onMouseMove: (event) => { onPanMove(event); onMouseMove(event); }, onMouseUp: () => { onMouseUp(); onPanEnd(); } }, React.createElement("div", { ref: _content, className: "content", style: { backgroundPosition: `${pan.x}px ${pan.y}px` }, onWheel: handleWheel }, React.createElement("div", { className: "nodes-wrapper", style: { transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})`, } }, React.createElement("div", { className: "edges" }, renderEdges, drawingEdge && mousePos && (React.createElement("svg", { className: "edge-temp" }, React.createElement("path", { ref: _path, d: `M${drawingEdge.start.x} ${drawingEdge.start.y} L${mousePos.x} ${mousePos.y}`, fill: "none", stroke: "var(--purple-500)", strokeWidth: 2, strokeDasharray: 10, strokeDashoffset: 10, strokeLinecap: "round" })))), React.createElement("div", { className: "nodes" }, _nodes.map((node, index) => (React.createElement("div", { key: index, className: "node", style: { left: node.position.x, top: node.position.y, }, onMouseDown: (event) => onNodeMouseDown(event, node.id, node.position) }, React.createElement("span", { ref: (el) => { if (!el) return; _arNodes.current[`${node.id}_top`] = el; }, className: "port top", onMouseDown: (event) => { event.stopPropagation(); const port = "top"; const from = getPortCenter(node.id, port); if (from) { setDrawingEdge({ id: node.id, port, start: from, }); } } }), React.createElement("span", { ref: (el) => { if (!el) return; _arNodes.current[`${node.id}_left`] = el; }, className: "port left", onMouseDown: (event) => { event.stopPropagation(); const from = getPortCenter(node.id, "left"); if (from) { setDrawingEdge({ id: node.id, port: "left", start: from }); } } }), React.createElement("span", null, node.data), React.createElement("span", { ref: (el) => { if (!el) return; _arNodes.current[`${node.id}_right`] = el; }, className: "port right", onMouseDown: (event) => { event.stopPropagation(); const from = getPortCenter(node.id, "right"); if (from) { setDrawingEdge({ id: node.id, port: "right", start: from }); } } }), React.createElement("span", { ref: (el) => { if (!el) return; _arNodes.current[`${node.id}_bottom`] = el; }, className: "port bottom", onMouseDown: (event) => { event.stopPropagation(); const from = getPortCenter(node.id, "bottom"); if (from) { setDrawingEdge({ id: node.id, port: "bottom", start: from, }); } } }))))))), React.createElement("div", { className: "zoom-buttons", onMouseDown: (event) => event.stopPropagation() }, React.createElement(Box, null, React.createElement(Tooltip, { text: "Zoom Out" }, React.createElement(Button, { variant: "borderless", color: "light", icon: { element: React.createElement(ARIcon, { icon: "Dash", fill: "currentColor" }) }, onClick: () => handleZoom("decrement") })), React.createElement("div", { className: "zoom-percent" }, Math.round(scale * 100), "%"), React.createElement(Tooltip, { text: "Zoom In" }, React.createElement(Button, { variant: "borderless", color: "light", icon: { element: React.createElement(ARIcon, { icon: "Add", fill: "currentColor" }) }, onClick: () => handleZoom("increment") })))), React.createElement("div", { style: { zIndex: 555 } }, JSON.stringify(drawingEdge)))); }; export default Diagram;