UNPKG

@ichigo_san/graphing

Version:

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

443 lines (420 loc) 17.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactflow = require("reactflow"); var _OrthogonalRouter = _interopRequireDefault(require("../../services/OrthogonalRouter")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } 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); } const getConnectionPoint = (x, y, width, height, position) => { switch (position) { case _reactflow.Position.Top: return { x: x + width / 2, y }; case _reactflow.Position.Right: return { x: x + width, y: y + height / 2 }; case _reactflow.Position.Bottom: return { x: x + width / 2, y: y + height }; case _reactflow.Position.Left: return { x, y: y + height / 2 }; default: return { x: x + width / 2, y: y + height / 2 }; } }; const OrthogonalEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerStart, markerEnd, data, selected }) => { const { screenToFlowPosition, setEdges, getEdges, getNodes } = (0, _reactflow.useReactFlow)(); const [hoveredSegmentInfo, setHoveredSegmentInfo] = (0, _react.useState)(null); const [draggedSegmentIndex, setDraggedSegmentIndex] = (0, _react.useState)(null); const [router] = (0, _react.useState)(() => new _OrthogonalRouter.default()); // Get node dimensions and positions for source/target const nodes = getNodes(); const sourceNode = nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.source) || n.id === (data === null || data === void 0 ? void 0 : data.sourceNode) || n.id === (data === null || data === void 0 ? void 0 : data.sourceId)); const targetNode = nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.target) || n.id === (data === null || data === void 0 ? void 0 : data.targetNode) || n.id === (data === null || data === void 0 ? void 0 : data.targetId)); const sourcePoint = (0, _react.useMemo)(() => { return sourceNode && sourcePosition ? getConnectionPoint(sourceNode.position.x, sourceNode.position.y, sourceNode.width, sourceNode.height, sourcePosition) : { x: sourceX, y: sourceY }; }, [sourceNode, sourcePosition, sourceX, sourceY]); const targetPoint = (0, _react.useMemo)(() => { return targetNode && targetPosition ? getConnectionPoint(targetNode.position.x, targetNode.position.y, targetNode.width, targetNode.height, targetPosition) : { x: targetX, y: targetY }; }, [targetNode, targetPosition, targetX, targetY]); // Auto-calculate route when source or target changes const autoCalculatedRoute = (0, _react.useMemo)(() => { if (!sourceNode || !targetNode) return { waypoints: [] }; // Get all nodes as obstacles (excluding source and target) const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); // Calculate optimal route return router.calculateOptimalRoute(sourceNode, targetNode, obstacles); }, [sourceNode, targetNode, nodes, router]); // Use existing waypoints or auto-calculated route const waypoints = (0, _react.useMemo)(() => { if (data !== null && data !== void 0 && data.waypoints && data.waypoints.length > 0) { const validWaypoints = data.waypoints.filter(wp => wp && typeof wp.x === 'number' && typeof wp.y === 'number' && !isNaN(wp.x) && !isNaN(wp.y) && isFinite(wp.x) && isFinite(wp.y)); return validWaypoints; } // Use auto-calculated route return autoCalculatedRoute.waypoints || []; }, [data === null || data === void 0 ? void 0 : data.waypoints, autoCalculatedRoute.waypoints]); // This function generates the SVG path string for the edge. const path = (0, _react.useMemo)(() => { const points = [sourcePoint, ...waypoints, targetPoint]; let pathString = `M ${points[0].x},${points[0].y}`; for (let i = 1; i < points.length; i++) { pathString += ` L ${points[i].x},${points[i].y}`; } return pathString; }, [sourcePoint, targetPoint, waypoints]); // Enhanced segment dragging with collision avoidance const handleSegmentMouseDown = (0, _react.useCallback)((event, segmentIndex) => { var _clickedEdge$data; event.stopPropagation(); event.preventDefault(); console.log('Segment mouse down triggered:', segmentIndex); setDraggedSegmentIndex(segmentIndex); const initialEdges = getEdges(); const clickedEdge = initialEdges.find(e => e.id === id); if (!clickedEdge) return; // Get current waypoints let currentWaypoints = ((_clickedEdge$data = clickedEdge.data) === null || _clickedEdge$data === void 0 ? void 0 : _clickedEdge$data.waypoints) || []; // If no waypoints exist, use auto-calculated route if (currentWaypoints.length === 0) { currentWaypoints = autoCalculatedRoute.waypoints || []; } const points = [sourcePoint, ...currentWaypoints, targetPoint]; const p1 = points[segmentIndex]; const p2 = points[segmentIndex + 1]; if (!p1 || !p2) return; // Determine if this is a horizontal or vertical segment const isHorizontal = Math.abs(p1.y - p2.y) < 10; const isVertical = Math.abs(p1.x - p2.x) < 10; console.log('Dragging segment:', segmentIndex, 'isHorizontal:', isHorizontal, 'isVertical:', isVertical); const onMouseMove = moveEvent => { const position = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.clientY }); console.log('Mouse position:', position); // Get obstacles for collision avoidance const obstacles = nodes.filter(node => node.id !== (sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.id) && node.id !== (targetNode === null || targetNode === void 0 ? void 0 : targetNode.id)).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); setEdges(eds => eds.map(e => { if (e.id === id) { var _e$data; let currentWaypoints = ((_e$data = e.data) === null || _e$data === void 0 ? void 0 : _e$data.waypoints) || []; // If waypoints are empty, use auto-calculated route if (currentWaypoints.length === 0) { currentWaypoints = autoCalculatedRoute.waypoints || []; } // Adjust path for obstacles during dragging const adjustedWaypoints = router.adjustPathForObstacles(currentWaypoints, obstacles, segmentIndex - 1, // Adjust for source point offset position); console.log('Adjusted waypoints:', adjustedWaypoints); return { ...e, data: { ...e.data, waypoints: adjustedWaypoints } }; } return e; })); }; const onMouseUp = () => { console.log('Mouse up - stopping drag'); setDraggedSegmentIndex(null); window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; // Add event listeners to window window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }, [id, sourcePoint, targetPoint, getEdges, setEdges, screenToFlowPosition, nodes, sourceNode, targetNode, router, autoCalculatedRoute]); // Enhanced waypoint dragging with collision avoidance const onWaypointMouseDown = (0, _react.useCallback)((event, index) => { event.stopPropagation(); event.preventDefault(); console.log('Waypoint mouse down triggered:', index); const onMouseMove = e => { const position = screenToFlowPosition({ x: e.clientX, y: e.clientY }); // Get obstacles for collision avoidance const obstacles = nodes.filter(node => node.id !== (sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.id) && node.id !== (targetNode === null || targetNode === void 0 ? void 0 : targetNode.id)).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); setEdges(eds => eds.map(edge => { if (edge.id === id) { var _edge$data; let currentWaypoints = ((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.waypoints) || []; // If waypoints are empty, use auto-calculated route if (currentWaypoints.length === 0) { currentWaypoints = autoCalculatedRoute.waypoints || []; } if (!currentWaypoints || currentWaypoints.length === 0 || index >= currentWaypoints.length) { return edge; } // Adjust path for obstacles during waypoint dragging const adjustedWaypoints = router.adjustPathForObstacles(currentWaypoints, obstacles, index, position); return { ...edge, data: { ...edge.data, waypoints: adjustedWaypoints } }; } return edge; })); }; const onMouseUp = () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }, [id, setEdges, screenToFlowPosition, nodes, sourceNode, targetNode, router, autoCalculatedRoute]); // Waypoint double-click handler - removes waypoint const onWaypointDoubleClick = (0, _react.useCallback)((event, index) => { event.stopPropagation(); event.preventDefault(); console.log('Waypoint double-click triggered:', index); setEdges(eds => eds.map(edge => { if (edge.id === id) { var _edge$data2; const currentWaypoints = ((_edge$data2 = edge.data) === null || _edge$data2 === void 0 ? void 0 : _edge$data2.waypoints) || []; if (!currentWaypoints || currentWaypoints.length === 0 || index >= currentWaypoints.length) { return edge; } const newWaypoints = [...currentWaypoints]; newWaypoints.splice(index, 1); return { ...edge, data: { ...edge.data, waypoints: newWaypoints } }; } return edge; })); }, [id, setEdges]); // Calculate label position const labelPosition = (0, _react.useMemo)(() => { const points = [sourcePoint, ...waypoints, targetPoint]; if (points.length < 2) return { x: (sourcePoint.x + targetPoint.x) / 2, y: (sourcePoint.y + targetPoint.y) / 2 }; const midIndex = Math.floor(points.length / 2); const p1 = points[midIndex - 1]; const p2 = points[midIndex]; return { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; }, [sourcePoint, targetPoint, waypoints]); // Auto-reroute when nodes move (0, _react.useEffect)(() => { if (sourceNode && targetNode && !(data !== null && data !== void 0 && data.waypoints)) { // Auto-reroute only if no manual waypoints exist const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({ x: node.position.x, y: node.position.y, width: node.width || 100, height: node.height || 100 })); const newRoute = router.calculateOptimalRoute(sourceNode, targetNode, obstacles); if (newRoute.waypoints.length > 0) { setEdges(eds => eds.map(edge => { var _edge$data3; if (edge.id === id && (!((_edge$data3 = edge.data) !== null && _edge$data3 !== void 0 && _edge$data3.waypoints) || edge.data.waypoints.length === 0)) { return { ...edge, data: { ...edge.data, waypoints: newRoute.waypoints } }; } return edge; })); } } }, [sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.position, targetNode === null || targetNode === void 0 ? void 0 : targetNode.position, nodes, router, id, setEdges, data === null || data === void 0 ? void 0 : data.waypoints]); return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_reactflow.BaseEdge, { id: id, path: path, style: style, markerStart: markerStart, markerEnd: markerEnd }), /*#__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 === null || data === void 0 ? void 0 : data.label) || '')), (() => { const points = [sourcePoint, ...waypoints, targetPoint]; return points.slice(0, -1).map((p1, i) => { const p2 = points[i + 1]; if (!p1 || !p2) return null; const isHorizontal = Math.abs(p1.y - p2.y) < 5; const isDragging = draggedSegmentIndex === i; const isHovered = (hoveredSegmentInfo === null || hoveredSegmentInfo === void 0 ? void 0 : hoveredSegmentInfo.segmentIndex) === i; return /*#__PURE__*/_react.default.createElement("g", { key: `segment-${i}` }, /*#__PURE__*/_react.default.createElement("line", { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: isDragging ? "rgba(59, 130, 246, 0.8)" : isHovered ? "rgba(59, 130, 246, 0.6)" : "rgba(59, 130, 246, 0.3)", strokeWidth: isDragging ? 4 : isHovered ? 3 : 2, strokeDasharray: isDragging || isHovered ? "5,5" : "none", style: { cursor: isHorizontal ? 'ns-resize' : 'ew-resize' }, onMouseDown: event => handleSegmentMouseDown(event, i), onMouseEnter: () => setHoveredSegmentInfo({ segmentIndex: i, distance: 0 }), onMouseLeave: () => setHoveredSegmentInfo(null) }), /*#__PURE__*/_react.default.createElement("line", { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: "transparent", strokeWidth: 12, style: { cursor: isHorizontal ? 'ns-resize' : 'ew-resize' }, onMouseDown: event => handleSegmentMouseDown(event, i), onMouseEnter: () => setHoveredSegmentInfo({ segmentIndex: i, distance: 0 }), onMouseLeave: () => setHoveredSegmentInfo(null) })); }); })(), hoveredSegmentInfo && !draggedSegmentIndex && (() => { const points = [sourcePoint, ...waypoints, targetPoint]; const p1 = points[hoveredSegmentInfo.segmentIndex]; const p2 = points[hoveredSegmentInfo.segmentIndex + 1]; if (!p1 || !p2) return null; const midX = (p1.x + p2.x) / 2; const midY = (p1.y + p2.y) / 2; return /*#__PURE__*/_react.default.createElement("foreignObject", { x: midX + 10, y: midY - 20, width: 80, height: 40, style: { overflow: 'visible' } }, /*#__PURE__*/_react.default.createElement("div", { style: { backgroundColor: 'rgba(0, 0, 0, 0.8)', color: 'white', padding: '4px 8px', borderRadius: '4px', fontSize: '12px', fontFamily: 'monospace', pointerEvents: 'none', whiteSpace: 'nowrap', zIndex: 1000 } }, Math.round(midX), ", ", Math.round(midY))); })(), waypoints.map((wp, i) => /*#__PURE__*/_react.default.createElement("g", { key: i, className: "react-flow__custom-edge-waypoint" }, /*#__PURE__*/_react.default.createElement("circle", { cx: wp.x, cy: wp.y, r: 5, fill: "rgb(59, 130, 246)", stroke: "white", strokeWidth: 2, className: "cursor-move", onMouseDown: event => onWaypointMouseDown(event, i), onDoubleClick: event => onWaypointDoubleClick(event, i) }))), (!(data !== null && data !== void 0 && data.waypoints) || data.waypoints.length === 0) && /*#__PURE__*/_react.default.createElement("g", null, /*#__PURE__*/_react.default.createElement("circle", { cx: labelPosition.x, cy: labelPosition.y - 20, r: 3, fill: "rgba(34, 197, 94, 0.8)", stroke: "rgba(34, 197, 94, 1)", strokeWidth: 1 }))); }; var _default = exports.default = OrthogonalEdge;