UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

439 lines (409 loc) 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactflow = require("reactflow"); var _routingPipeline = require("../../utils/routingPipeline.js"); var _edgeHandlers = require("../../utils/edgeHandlers.js"); var _edgeTypes = require("../../utils/edgeTypes.js"); var _gridSnapping = require("../../utils/gridSnapping.js"); var _coordinateTransforms = require("../../utils/coordinateTransforms.js"); var _anchorPoint = require("../../utils/anchorPoint.js"); var _jetty = require("../../utils/jetty.js"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /** * Optimized Orthogonal Edge - Draw.io Style Implementation * * Features: * ✅ Uses our new routing infrastructure (Modules A, B, C) * ✅ Handler-based editing system (Module D) * ✅ Performance optimized with caching and debouncing * ✅ Draw.io-style connection points and jetty system * ✅ Virtual handles for segment dragging * ✅ Collision avoidance and route optimization * ✅ Grid snapping and orthogonal constraints * ✅ Live preview during editing */ // Performance optimization constants const DEBOUNCE_DELAY = 100; const CACHE_EXPIRY = 30000; // 30 seconds const MIN_SEGMENT_LENGTH = 30; const OptimizedOrthogonalEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerStart, markerEnd, data, selected }) => { const { screenToFlowPosition, setEdges, getEdges, getNodes, getViewport } = (0, _reactflow.useReactFlow)(); // State management const [hoveredElement, setHoveredElement] = (0, _react.useState)(null); const [isProcessing, setIsProcessing] = (0, _react.useState)(false); const [routeCache, setRouteCache] = (0, _react.useState)(new Map()); const [lastRouteUpdate, setLastRouteUpdate] = (0, _react.useState)(0); // Refs for performance optimization const debounceTimeoutRef = (0, _react.useRef)(null); const handlerRef = (0, _react.useRef)(null); const lastNodesRef = (0, _react.useRef)(null); // Get current nodes and viewport const nodes = getNodes(); const viewport = getViewport(); const transform = { x: viewport.x, y: viewport.y, k: viewport.zoom }; // Grid configuration const gridConfig = (0, _react.useMemo)(() => ({ ..._gridSnapping.DEFAULT_GRID_CONFIG, tolerance: _gridSnapping.DEFAULT_GRID_CONFIG.tolerance * viewport.zoom }), [viewport.zoom]); // Get source and target nodes const sourceNode = (0, _react.useMemo)(() => nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.source)), [nodes, data === null || data === void 0 ? void 0 : data.source]); const targetNode = (0, _react.useMemo)(() => nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.target)), [nodes, data === null || data === void 0 ? void 0 : data.target]); // Determine edge style const edgeStyle = (0, _react.useMemo)(() => (data === null || data === void 0 ? void 0 : data.style) || _edgeTypes.EDGE_STYLES.ORTHOGONAL, [data === null || data === void 0 ? void 0 : data.style]); // Create handler context const handlerContext = (0, _react.useMemo)(() => (0, _edgeHandlers.createHandlerContext)({ id, sourceX, sourceY, targetX, targetY, data, selected }, nodes, transform, gridConfig, () => {}, // updateEdge function setEdges), [id, sourceX, sourceY, targetX, targetY, data, selected, nodes, transform, gridConfig, setEdges]); // Initialize handler (0, _react.useEffect)(() => { handlerRef.current = (0, _edgeHandlers.getEdgeHandler)(edgeStyle, handlerContext); }, [edgeStyle, handlerContext]); // Calculate route using our optimized infrastructure const calculateRoute = (0, _react.useCallback)(async () => { if (!sourceNode || !targetNode) return null; // Check cache first const cacheKey = `${sourceNode.id}-${targetNode.id}-${sourceNode.position.x}-${sourceNode.position.y}-${targetNode.position.x}-${targetNode.position.y}`; const cached = routeCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_EXPIRY) { return cached.route; } setIsProcessing(true); try { // Use our routing pipeline const routingContext = { sourceNode, targetNode, sourcePort: data === null || data === void 0 ? void 0 : data.sourcePort, targetPort: data === null || data === void 0 ? void 0 : data.targetPort, transform, gridConfig, obstacles: nodes.filter(n => n.id !== sourceNode.id && n.id !== targetNode.id), avoidObstacles: true }; const routeResult = (0, _routingPipeline.routeOrthogonalEdge)(routingContext); if (routeResult.success) { // Cache the result setRouteCache(prev => new Map(prev).set(cacheKey, { route: routeResult, timestamp: Date.now() })); return routeResult; } } catch (error) { console.warn('Route calculation failed:', error); } finally { setIsProcessing(false); } return null; }, [sourceNode, targetNode, data === null || data === void 0 ? void 0 : data.sourcePort, data === null || data === void 0 ? void 0 : data.targetPort, transform, gridConfig, nodes, routeCache]); // Debounced route update const updateRoute = (0, _react.useCallback)(() => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); } debounceTimeoutRef.current = setTimeout(async () => { const routeResult = await calculateRoute(); if (routeResult) { setLastRouteUpdate(Date.now()); // Update edge data with new route setEdges(edges => edges.map(edge => { if (edge.id === id) { return { ...edge, data: { ...edge.data, waypoints: routeResult.modelPoints, lastRouteUpdate: Date.now(), routeMetadata: routeResult.metadata } }; } return edge; })); } }, DEBOUNCE_DELAY); }, [calculateRoute, id, setEdges]); // Check if nodes have moved significantly const checkNodeMovement = (0, _react.useCallback)(() => { if (!lastNodesRef.current) { lastNodesRef.current = nodes; return false; } const hasMoved = nodes.some(node => { const lastNode = lastNodesRef.current.find(n => n.id === node.id); if (!lastNode) return true; const dx = Math.abs(node.position.x - lastNode.position.x); const dy = Math.abs(node.position.y - lastNode.position.y); return dx > 5 || dy > 5; }); lastNodesRef.current = nodes; return hasMoved; }, [nodes]); // Update route when nodes move (0, _react.useEffect)(() => { if (checkNodeMovement()) { updateRoute(); } }, [nodes, checkNodeMovement, updateRoute]); // Get current waypoints (from data or calculated) const waypoints = (0, _react.useMemo)(() => { return (data === null || data === void 0 ? void 0 : data.waypoints) || []; }, [data === null || data === void 0 ? void 0 : data.waypoints]); // Calculate connection points with jetty const connectionPoints = (0, _react.useMemo)(() => { if (!sourceNode || !targetNode) { return { sourcePoint: { x: sourceX, y: sourceY }, targetPoint: { x: targetX, y: targetY } }; } // Get anchor points const sourceAnchor = (0, _anchorPoint.anchorPoint)(sourceNode, data === null || data === void 0 ? void 0 : data.sourcePort, { x: targetNode.position.x, y: targetNode.position.y }); const targetAnchor = (0, _anchorPoint.anchorPoint)(targetNode, data === null || data === void 0 ? void 0 : data.targetPort, { x: sourceNode.position.x, y: sourceNode.position.y }); if (!sourceAnchor || !targetAnchor) { return { sourcePoint: { x: sourceX, y: sourceY }, targetPoint: { x: targetX, y: targetY } }; } // Apply jetty system const jettyResult = (0, _jetty.jettyEdgeEndpoints)(sourceAnchor.point, sourceAnchor.side, targetAnchor.point, targetAnchor.side); return { sourcePoint: jettyResult.sourcePoint, targetPoint: jettyResult.targetPoint, sourceSide: sourceAnchor.side, targetSide: targetAnchor.side }; }, [sourceNode, targetNode, sourceX, sourceY, targetX, targetY, data === null || data === void 0 ? void 0 : data.sourcePort, data === null || data === void 0 ? void 0 : data.targetPort]); // Generate all points for the path const allPoints = (0, _react.useMemo)(() => { return [connectionPoints.sourcePoint, ...waypoints, connectionPoints.targetPoint]; }, [connectionPoints.sourcePoint, waypoints, connectionPoints.targetPoint]); // Generate SVG path const path = (0, _react.useMemo)(() => { if (allPoints.length < 2) return ''; let pathString = `M ${allPoints[0].x},${allPoints[0].y}`; for (let i = 1; i < allPoints.length; i++) { const prev = allPoints[i - 1]; const curr = allPoints[i]; // Check if segment is orthogonal const isOrthogonal = Math.abs(prev.x - curr.x) < 5 || Math.abs(prev.y - curr.y) < 5; if (isOrthogonal) { pathString += ` L ${curr.x},${curr.y}`; } else { // Smooth curve for non-orthogonal segments const midX = (prev.x + curr.x) / 2; const midY = (prev.y + curr.y) / 2; pathString += ` Q ${midX},${midY} ${curr.x},${curr.y}`; } } return pathString; }, [allPoints]); // Calculate label position const labelPosition = (0, _react.useMemo)(() => { if (allPoints.length < 2) return { x: sourceX, y: sourceY }; // Find the longest segment for label placement let maxLength = 0; let bestSegment = 0; for (let i = 0; i < allPoints.length - 1; i++) { const p1 = allPoints[i]; const p2 = allPoints[i + 1]; const length = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); if (length > maxLength) { maxLength = length; bestSegment = i; } } const p1 = allPoints[bestSegment]; const p2 = allPoints[bestSegment + 1]; return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; }, [allPoints, sourceX, sourceY]); // Event handlers const handleMouseDown = (0, _react.useCallback)((event, elementType, index) => { if (handlerRef.current) { handlerRef.current.onMouseDown(event, index); } }, []); const handleMouseMove = (0, _react.useCallback)(event => { if (handlerRef.current) { handlerRef.current.onMouseMove(event); } }, []); const handleMouseUp = (0, _react.useCallback)(() => { if (handlerRef.current) { handlerRef.current.onMouseUp(); } }, []); const handleDoubleClick = (0, _react.useCallback)((event, index) => { if (handlerRef.current) { handlerRef.current.onDoubleClick(event, index); } }, []); // Global mouse event listeners (0, _react.useEffect)(() => { const handleGlobalMouseMove = event => { handleMouseMove(event); }; const handleGlobalMouseUp = () => { handleMouseUp(); }; document.addEventListener('mousemove', handleGlobalMouseMove); document.addEventListener('mouseup', handleGlobalMouseUp); return () => { document.removeEventListener('mousemove', handleGlobalMouseMove); document.removeEventListener('mouseup', handleGlobalMouseUp); }; }, [handleMouseMove, handleMouseUp]); // Render interactive handles const renderHandles = (0, _react.useCallback)(() => { if (!handlerRef.current) return null; return handlerRef.current.renderHandles(waypoints, selected, hoveredElement === 'edge'); }, [waypoints, selected, hoveredElement]); // Render virtual segment handles const renderVirtualHandles = (0, _react.useCallback)(() => { if (!selected && hoveredElement !== 'edge') return null; return allPoints.slice(0, -1).map((p1, segmentIndex) => { const p2 = allPoints[segmentIndex + 1]; const segmentLength = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); // Only show handles for meaningful segments if (segmentLength < MIN_SEGMENT_LENGTH) return null; const isHorizontal = Math.abs(p1.y - p2.y) < 5; const isVertical = Math.abs(p1.x - p2.x) < 5; return /*#__PURE__*/_react.default.createElement("g", { key: `virtual-handle-${segmentIndex}` }, /*#__PURE__*/_react.default.createElement("line", { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: "transparent", strokeWidth: 20, style: { cursor: isHorizontal ? 'ns-resize' : isVertical ? 'ew-resize' : 'move' }, onMouseDown: e => handleMouseDown(e, 'segment', segmentIndex), onMouseEnter: () => setHoveredElement('segment'), onMouseLeave: () => setHoveredElement(null) }), /*#__PURE__*/_react.default.createElement("line", { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: hoveredElement === 'segment' ? "rgba(59, 130, 246, 0.6)" : "transparent", strokeWidth: 4, strokeDasharray: "8,4", style: { pointerEvents: 'none' } })); }); }, [allPoints, selected, hoveredElement, handleMouseDown]); return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_reactflow.BaseEdge, { id: id, path: path, style: { ...style, opacity: isProcessing ? 0.7 : 1, transition: 'opacity 0.2s ease' }, markerStart: markerStart, markerEnd: markerEnd, onMouseEnter: () => setHoveredElement('edge'), onMouseLeave: () => setHoveredElement(null) }), isProcessing && /*#__PURE__*/_react.default.createElement("g", null, /*#__PURE__*/_react.default.createElement("circle", { cx: labelPosition.x, cy: labelPosition.y, r: 8, fill: "rgba(59, 130, 246, 0.1)", stroke: "rgb(59, 130, 246)", strokeWidth: 2, opacity: 0.8 }, /*#__PURE__*/_react.default.createElement("animateTransform", { attributeName: "transform", type: "rotate", values: `0 ${labelPosition.x} ${labelPosition.y};360 ${labelPosition.x} ${labelPosition.y}`, dur: "1s", repeatCount: "indefinite" }))), (data === null || data === void 0 ? void 0 : data.label) && /*#__PURE__*/_react.default.createElement(_reactflow.EdgeLabelRenderer, null, /*#__PURE__*/_react.default.createElement("div", { style: { position: 'absolute', left: labelPosition.x, top: labelPosition.y, transform: 'translate(-50%, -50%)', fontSize: 12, fontWeight: 500, color: '#333', backgroundColor: 'rgba(255, 255, 255, 0.95)', padding: '2px 6px', borderRadius: '4px', border: '1px solid rgba(0, 0, 0, 0.1)', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', pointerEvents: 'all', whiteSpace: 'nowrap', zIndex: 10, backdropFilter: 'blur(4px)' }, className: "nodrag nopan edge-label" }, data.label)), renderVirtualHandles(), renderHandles()); }; var _default = exports.default = OptimizedOrthogonalEdge;