UNPKG

@ichigo_san/graphing

Version:

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

881 lines (837 loc) 28.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _reactflow = require("reactflow"); 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); } /** * Draw.io-Style Orthogonal Edge - Exact Implementation * * Based on actual draw.io algorithms: * ✅ Route patterns instead of dynamic calculation * ✅ Jetty system with proper spacing * ✅ Virtual handles for segment dragging * ✅ Minimal waypoints (2-waypoint preference) * ✅ Clean collision avoidance * ✅ No waypoint proliferation */ // Draw.io constants const ORTH_BUFFER = 10; // Base spacing unit like draw.io const JETTY_SIZE = 20; // Standard jetty size const VIRTUAL_HANDLE_OFFSET = 1000; const MIN_SEGMENT_LENGTH = 30; // Draw.io direction constants const DIRECTIONS = { NORTH: 1, EAST: 2, SOUTH: 3, WEST: 4 }; // Draw.io route patterns - exact implementation const ROUTE_PATTERNS = { // [source][target] = waypoint pattern 11: 'direct', // North-North: direct route 12: 'L-shape', // North-East: L-shape 13: 'S-shape', // North-South: S-shape 14: 'L-shape', // North-West: L-shape 21: 'L-shape', // East-North 22: 'direct', // East-East 23: 'L-shape', // East-South 24: 'S-shape', // East-West 31: 'S-shape', // South-North 32: 'L-shape', // South-East 33: 'direct', // South-South 34: 'L-shape', // South-West 41: 'L-shape', // West-North 42: 'S-shape', // West-East 43: 'L-shape', // West-South 44: 'direct' // West-West }; /** * Calculate connection point and direction like draw.io */ const getConnectionInfo = (node, targetPoint) => { if (!node) return { point: { x: 0, y: 0 }, direction: DIRECTIONS.EAST }; const bounds = { x: node.position.x, y: node.position.y, width: node.width || 150, height: node.height || 60, centerX: node.position.x + (node.width || 150) / 2, centerY: node.position.y + (node.height || 60) / 2 }; // Calculate which side is closest to target const dx = targetPoint.x - bounds.centerX; const dy = targetPoint.y - bounds.centerY; let point, direction; if (Math.abs(dx) > Math.abs(dy)) { // Horizontal connection if (dx > 0) { point = { x: bounds.x + bounds.width, y: bounds.centerY }; direction = DIRECTIONS.EAST; } else { point = { x: bounds.x, y: bounds.centerY }; direction = DIRECTIONS.WEST; } } else { // Vertical connection if (dy > 0) { point = { x: bounds.centerX, y: bounds.y + bounds.height }; direction = DIRECTIONS.SOUTH; } else { point = { x: bounds.centerX, y: bounds.y }; direction = DIRECTIONS.NORTH; } } return { point, direction }; }; /** * Calculate jetty point (connection extended outward) */ const getJettyPoint = (connectionPoint, direction, jettySize = JETTY_SIZE) => { switch (direction) { case DIRECTIONS.NORTH: return { x: connectionPoint.x, y: connectionPoint.y - jettySize }; case DIRECTIONS.EAST: return { x: connectionPoint.x + jettySize, y: connectionPoint.y }; case DIRECTIONS.SOUTH: return { x: connectionPoint.x, y: connectionPoint.y + jettySize }; case DIRECTIONS.WEST: return { x: connectionPoint.x - jettySize, y: connectionPoint.y }; default: return connectionPoint; } }; /** * Draw.io routing algorithm - creates minimal waypoints */ const calculateDrawIoRoute = (sourceInfo, targetInfo) => { const sourceJetty = getJettyPoint(sourceInfo.point, sourceInfo.direction); const targetJetty = getJettyPoint(targetInfo.point, targetInfo.direction); // Get route pattern const patternKey = sourceInfo.direction * 10 + targetInfo.direction; const pattern = ROUTE_PATTERNS[patternKey] || 'L-shape'; const dx = targetJetty.x - sourceJetty.x; const dy = targetJetty.y - sourceJetty.y; // Check if we can use direct routing (draw.io optimization) const distance = Math.sqrt(dx * dx + dy * dy); const combinedJetty = JETTY_SIZE * 2; if (distance < combinedJetty) { // Too close - use simple direct routing return []; } switch (pattern) { case 'direct': // Same direction - use simple 2-waypoint routing if (sourceInfo.direction === DIRECTIONS.NORTH || sourceInfo.direction === DIRECTIONS.SOUTH) { const midY = sourceJetty.y + dy / 2; return [{ x: sourceJetty.x, y: midY }, { x: targetJetty.x, y: midY }]; } else { const midX = sourceJetty.x + dx / 2; return [{ x: midX, y: sourceJetty.y }, { x: midX, y: targetJetty.y }]; } case 'L-shape': // L-shape routing - exactly 2 waypoints if (Math.abs(dx) > Math.abs(dy)) { return [{ x: targetJetty.x, y: sourceJetty.y }]; } else { return [{ x: sourceJetty.x, y: targetJetty.y }]; } case 'S-shape': // S-shape for opposite directions - exactly 2 waypoints if (sourceInfo.direction === DIRECTIONS.NORTH || sourceInfo.direction === DIRECTIONS.SOUTH) { const midY = sourceJetty.y + dy / 2; return [{ x: sourceJetty.x, y: midY }, { x: targetJetty.x, y: midY }]; } else { const midX = sourceJetty.x + dx / 2; return [{ x: midX, y: sourceJetty.y }, { x: midX, y: targetJetty.y }]; } default: return []; } }; /** * Apply very simple draw.io collision avoidance - minimal offset only */ const applySimpleCollisionAvoidance = (waypoints, existingEdges) => { // Draw.io actually does MINIMAL collision avoidance // Most of the time, it just lets edges overlap slightly if (!existingEdges || existingEdges.length === 0 || waypoints.length === 0) { return waypoints; } // Only apply very small offset if absolutely necessary const minSpacing = ORTH_BUFFER; // 10px minimum spacing return waypoints.map((wp, wpIndex) => { let needsOffset = false; let offsetCount = 0; // Check only direct overlaps (not all nearby waypoints) existingEdges.forEach(edge => { var _edge$data; if ((_edge$data = edge.data) !== null && _edge$data !== void 0 && _edge$data.waypoints) { edge.data.waypoints.forEach((existingWp, existingIndex) => { // Only check waypoints at similar positions in the path if (Math.abs(wpIndex - existingIndex) <= 1) { const distance = Math.sqrt(Math.pow(wp.x - existingWp.x, 2) + Math.pow(wp.y - existingWp.y, 2)); if (distance < minSpacing) { needsOffset = true; offsetCount++; } } }); } }); if (needsOffset && offsetCount <= 2) { // Only offset if max 2 conflicts // Very small offset to avoid exact overlap const smallOffset = 8 * offsetCount; // Incremental offset return { x: wp.x + smallOffset, y: wp.y + smallOffset }; } return wp; // No offset needed }); }; /** * Generate virtual handles for segment dragging (draw.io style) */ const generateVirtualHandles = allPoints => { const handles = []; for (let i = 0; i < allPoints.length - 1; i++) { const p1 = allPoints[i]; const p2 = allPoints[i + 1]; const segmentLength = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); // Only create virtual handles for segments longer than minimum if (segmentLength > MIN_SEGMENT_LENGTH) { handles.push({ id: VIRTUAL_HANDLE_OFFSET + i, x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, segmentIndex: i, isHorizontal: Math.abs(p1.y - p2.y) < 5, isVertical: Math.abs(p1.x - p2.x) < 5 }); } } return handles; }; /** * Main Draw.io Orthogonal Edge Component */ const DrawIoOrthogonalEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerStart, markerEnd, data, selected }) => { const { screenToFlowPosition, setEdges, getEdges, getNodes } = (0, _reactflow.useReactFlow)(); // State const [hoveredHandle, setHoveredHandle] = (0, _react.useState)(null); const [draggedHandle, setDraggedHandle] = (0, _react.useState)(null); // Get nodes const nodes = getNodes(); const sourceNode = nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.source)); const targetNode = nodes.find(n => n.id === (data === null || data === void 0 ? void 0 : data.target)); // Calculate connection info like draw.io const sourceInfo = (0, _react.useMemo)(() => { if (sourceNode) { const targetPoint = targetNode ? { x: targetNode.position.x + (targetNode.width || 150) / 2, y: targetNode.position.y + (targetNode.height || 60) / 2 } : { x: targetX, y: targetY }; return getConnectionInfo(sourceNode, targetPoint); } return { point: { x: sourceX, y: sourceY }, direction: DIRECTIONS.EAST }; }, [sourceNode, targetNode, sourceX, sourceY, targetX, targetY]); const targetInfo = (0, _react.useMemo)(() => { if (targetNode) { const sourcePoint = sourceNode ? { x: sourceNode.position.x + (sourceNode.width || 150) / 2, y: sourceNode.position.y + (sourceNode.height || 60) / 2 } : { x: sourceX, y: sourceY }; return getConnectionInfo(targetNode, sourcePoint); } return { point: { x: targetX, y: targetY }, direction: DIRECTIONS.WEST }; }, [targetNode, sourceNode, targetX, targetY, sourceX, sourceY]); // Calculate waypoints using draw.io algorithm const waypoints = (0, _react.useMemo)(() => { // Use existing waypoints if they exist and are valid if (data !== null && data !== void 0 && data.waypoints && Array.isArray(data.waypoints) && data.waypoints.length > 0) { // Filter out invalid waypoints const validWaypoints = data.waypoints.filter(wp => wp && typeof wp.x === 'number' && typeof wp.y === 'number'); if (validWaypoints.length > 0) { return validWaypoints; } } // Generate new waypoints using draw.io algorithm only when needed const calculatedWaypoints = calculateDrawIoRoute(sourceInfo, targetInfo); // Only apply collision avoidance if we have waypoints if (calculatedWaypoints.length > 0) { const existingEdges = getEdges().filter(e => e.id !== id); return applySimpleCollisionAvoidance(calculatedWaypoints, existingEdges); } return calculatedWaypoints; }, [data === null || data === void 0 ? void 0 : data.waypoints, sourceInfo, targetInfo, id, getEdges]); // Generate all points const allPoints = (0, _react.useMemo)(() => { return [sourceInfo.point, ...waypoints, targetInfo.point]; }, [sourceInfo.point, waypoints, targetInfo.point]); // Generate path const path = (0, _react.useMemo)(() => { let pathString = `M ${allPoints[0].x},${allPoints[0].y}`; for (let i = 1; i < allPoints.length; i++) { pathString += ` L ${allPoints[i].x},${allPoints[i].y}`; } return pathString; }, [allPoints]); // Save calculated waypoints to edge data when they don't exist (0, _react.useEffect)(() => { if (!(data !== null && data !== void 0 && data.waypoints) && waypoints.length > 0) { setEdges(edges => edges.map(edge => { if (edge.id === id) { return { ...edge, data: { ...edge.data, waypoints: waypoints } }; } return edge; })); } }, [id, data === null || data === void 0 ? void 0 : data.waypoints, waypoints, setEdges]); // Generate virtual handles const virtualHandles = (0, _react.useMemo)(() => { return generateVirtualHandles(allPoints); }, [allPoints]); // Handle segment dragging (proper draw.io style) const handleSegmentDrag = (0, _react.useCallback)((event, segmentIndex) => { event.stopPropagation(); event.preventDefault(); setDraggedHandle(segmentIndex); const p1 = allPoints[segmentIndex]; const p2 = allPoints[segmentIndex + 1]; const isHorizontal = Math.abs(p1.y - p2.y) < 5; const onMouseMove = moveEvent => { const position = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.clientY }); setEdges(edges => edges.map(edge => { if (edge.id === id) { var _edge$data2; let currentWaypoints = [...(((_edge$data2 = edge.data) === null || _edge$data2 === void 0 ? void 0 : _edge$data2.waypoints) || [])]; // Create initial waypoints if none exist if (currentWaypoints.length === 0) { const sourceP = allPoints[0]; const targetP = allPoints[allPoints.length - 1]; const dx = targetP.x - sourceP.x; const dy = targetP.y - sourceP.y; if (Math.abs(dx) > Math.abs(dy)) { currentWaypoints = [{ x: sourceP.x + dx / 2, y: sourceP.y }, { x: sourceP.x + dx / 2, y: targetP.y }]; } else { currentWaypoints = [{ x: sourceP.x, y: sourceP.y + dy / 2 }, { x: targetP.x, y: sourceP.y + dy / 2 }]; } } // Move segment if (isHorizontal) { // Move horizontal segment vertically const newY = position.y; // Update all waypoints that should move with this segment if (segmentIndex === 0) { // First segment if (currentWaypoints[0]) { currentWaypoints[0] = { ...currentWaypoints[0], y: newY }; } } else { // Middle or last segment const waypointIndex = segmentIndex - 1; if (currentWaypoints[waypointIndex]) { currentWaypoints[waypointIndex] = { ...currentWaypoints[waypointIndex], y: newY }; } if (currentWaypoints[waypointIndex + 1]) { currentWaypoints[waypointIndex + 1] = { ...currentWaypoints[waypointIndex + 1], y: newY }; } } } else { // Move vertical segment horizontally const newX = position.x; if (segmentIndex === 0) { // First segment if (currentWaypoints[0]) { currentWaypoints[0] = { ...currentWaypoints[0], x: newX }; } } else { // Middle or last segment const waypointIndex = segmentIndex - 1; if (currentWaypoints[waypointIndex]) { currentWaypoints[waypointIndex] = { ...currentWaypoints[waypointIndex], x: newX }; } if (currentWaypoints[waypointIndex + 1]) { currentWaypoints[waypointIndex + 1] = { ...currentWaypoints[waypointIndex + 1], x: newX }; } } } return { ...edge, data: { ...edge.data, waypoints: currentWaypoints } }; } return edge; })); }; const onMouseUp = () => { setDraggedHandle(null); window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }, [id, setEdges, screenToFlowPosition, allPoints]); // Enhanced waypoint dragging with better responsiveness const handleWaypointDrag = (0, _react.useCallback)((event, waypointIndex) => { event.stopPropagation(); event.preventDefault(); // Store initial position for better tracking const startPos = screenToFlowPosition({ x: event.clientX, y: event.clientY }); const onMouseMove = moveEvent => { const position = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.clientY }); // Immediate update for responsiveness setEdges(edges => edges.map(edge => { if (edge.id === id) { var _edge$data3; const currentWaypoints = [...(((_edge$data3 = edge.data) === null || _edge$data3 === void 0 ? void 0 : _edge$data3.waypoints) || [])]; if (waypointIndex >= 0 && waypointIndex < currentWaypoints.length) { currentWaypoints[waypointIndex] = position; return { ...edge, data: { ...edge.data, waypoints: currentWaypoints } }; } } return edge; })); }; const onMouseUp = () => { window.removeEventListener('mousemove', onMouseMove, true); window.removeEventListener('mouseup', onMouseUp, true); }; // Use capture phase for better mouse tracking window.addEventListener('mousemove', onMouseMove, true); window.addEventListener('mouseup', onMouseUp, true); }, [id, setEdges, screenToFlowPosition]); // Handle adding waypoints by clicking on virtual bends const handleVirtualBendClick = (0, _react.useCallback)((event, virtualBend) => { event.stopPropagation(); event.preventDefault(); const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }); setEdges(edges => edges.map(edge => { if (edge.id === id) { var _edge$data4; const currentWaypoints = [...(((_edge$data4 = edge.data) === null || _edge$data4 === void 0 ? void 0 : _edge$data4.waypoints) || [])]; // Insert new waypoint at the virtual bend position const insertIndex = virtualBend.segmentIndex; currentWaypoints.splice(insertIndex, 0, position); return { ...edge, data: { ...edge.data, waypoints: currentWaypoints } }; } return edge; })); }, [id, setEdges, screenToFlowPosition]); // Handle removing waypoints on double-click const handleWaypointDoubleClick = (0, _react.useCallback)((event, waypointIndex) => { event.stopPropagation(); event.preventDefault(); setEdges(edges => edges.map(edge => { if (edge.id === id) { var _edge$data5; const currentWaypoints = [...(((_edge$data5 = edge.data) === null || _edge$data5 === void 0 ? void 0 : _edge$data5.waypoints) || [])]; currentWaypoints.splice(waypointIndex, 1); return { ...edge, data: { ...edge.data, waypoints: currentWaypoints } }; } return edge; })); }, [id, setEdges]); // Calculate label position const labelPosition = (0, _react.useMemo)(() => { if (allPoints.length < 2) return sourceInfo.point; const midIndex = Math.floor(allPoints.length / 2); return allPoints[midIndex] || sourceInfo.point; }, [allPoints, sourceInfo.point]); 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 }), (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 }, className: "nodrag nopan edge-label" }, data.label)), allPoints.slice(0, -1).map((p1, segmentIndex) => { const p2 = allPoints[segmentIndex + 1]; const isHorizontal = Math.abs(p1.y - p2.y) < 5; const isVertical = Math.abs(p1.x - p2.x) < 5; const isDragged = draggedHandle === segmentIndex; const isHovered = hoveredHandle === segmentIndex; // Calculate segment length to show only meaningful segments const segmentLength = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); if (segmentLength < 20) return null; // Skip very short segments return /*#__PURE__*/_react.default.createElement("g", { key: `segment-${segmentIndex}` }, /*#__PURE__*/_react.default.createElement("line", { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: isDragged ? "rgba(59, 130, 246, 0.9)" : isHovered ? "rgba(59, 130, 246, 0.6)" : "transparent", strokeWidth: isDragged ? 6 : isHovered ? 4 : 2, strokeDasharray: isDragged || isHovered ? "8,4" : "none", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("line", { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: "transparent", strokeWidth: 20 // Wide hit area , style: { cursor: isHorizontal ? 'ns-resize' : isVertical ? 'ew-resize' : 'move' }, onMouseDown: event => handleSegmentDrag(event, segmentIndex), onMouseEnter: () => setHoveredHandle(segmentIndex), onMouseLeave: () => setHoveredHandle(null) }), isHovered && !isDragged && /*#__PURE__*/_react.default.createElement("g", null, /*#__PURE__*/_react.default.createElement("circle", { cx: (p1.x + p2.x) / 2, cy: (p1.y + p2.y) / 2, r: 6, fill: "rgba(59, 130, 246, 0.8)", stroke: "white", strokeWidth: 2, style: { pointerEvents: 'none' } }), isHorizontal && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("polygon", { points: `${(p1.x + p2.x) / 2 - 4},${(p1.y + p2.y) / 2 - 8} ${(p1.x + p2.x) / 2},${(p1.y + p2.y) / 2 - 4} ${(p1.x + p2.x) / 2 + 4},${(p1.y + p2.y) / 2 - 8}`, fill: "white", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("polygon", { points: `${(p1.x + p2.x) / 2 - 4},${(p1.y + p2.y) / 2 + 8} ${(p1.x + p2.x) / 2},${(p1.y + p2.y) / 2 + 4} ${(p1.x + p2.x) / 2 + 4},${(p1.y + p2.y) / 2 + 8}`, fill: "white", style: { pointerEvents: 'none' } })), isVertical && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("polygon", { points: `${(p1.x + p2.x) / 2 - 8},${(p1.y + p2.y) / 2 - 4} ${(p1.x + p2.x) / 2 - 4},${(p1.y + p2.y) / 2} ${(p1.x + p2.x) / 2 - 8},${(p1.y + p2.y) / 2 + 4}`, fill: "white", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("polygon", { points: `${(p1.x + p2.x) / 2 + 8},${(p1.y + p2.y) / 2 - 4} ${(p1.x + p2.x) / 2 + 4},${(p1.y + p2.y) / 2} ${(p1.x + p2.x) / 2 + 8},${(p1.y + p2.y) / 2 + 4}`, fill: "white", style: { pointerEvents: 'none' } })))); }), virtualHandles.map((handle, index) => { const isHovered = hoveredHandle === `virtual-${index}`; return /*#__PURE__*/_react.default.createElement("g", { key: `virtual-${index}` }, /*#__PURE__*/_react.default.createElement("circle", { cx: handle.x, cy: handle.y, r: isHovered ? 6 : 4, fill: isHovered ? "rgba(34, 197, 94, 0.8)" : "rgba(34, 197, 94, 0.4)", stroke: "white", strokeWidth: isHovered ? 2 : 1, strokeDasharray: "3,2", style: { cursor: 'pointer' }, onClick: event => handleVirtualBendClick(event, handle), onMouseEnter: () => setHoveredHandle(`virtual-${index}`), onMouseLeave: () => setHoveredHandle(null) }), /*#__PURE__*/_react.default.createElement("g", { style: { pointerEvents: 'none' } }, /*#__PURE__*/_react.default.createElement("line", { x1: handle.x - 2, y1: handle.y, x2: handle.x + 2, y2: handle.y, stroke: "white", strokeWidth: 1 }), /*#__PURE__*/_react.default.createElement("line", { x1: handle.x, y1: handle.y - 2, x2: handle.x, y2: handle.y + 2, stroke: "white", strokeWidth: 1 }))); }), waypoints.map((waypoint, index) => { const isHovered = hoveredHandle === `waypoint-${index}`; return /*#__PURE__*/_react.default.createElement("g", { key: `waypoint-${index}` }, /*#__PURE__*/_react.default.createElement("circle", { cx: waypoint.x + 1, cy: waypoint.y + 1, r: 6, fill: "rgba(0, 0, 0, 0.2)", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("circle", { cx: waypoint.x, cy: waypoint.y, r: 12, fill: "transparent", style: { cursor: 'move' }, onMouseDown: event => handleWaypointDrag(event, index), onDoubleClick: event => handleWaypointDoubleClick(event, index), onMouseEnter: () => setHoveredHandle(`waypoint-${index}`), onMouseLeave: () => setHoveredHandle(null) }), /*#__PURE__*/_react.default.createElement("circle", { cx: waypoint.x, cy: waypoint.y, r: isHovered ? 7 : 6, fill: "white", stroke: "rgba(0, 0, 0, 0.1)", strokeWidth: 1, style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("circle", { cx: waypoint.x, cy: waypoint.y, r: isHovered ? 6 : 5, fill: isHovered ? "rgb(34, 197, 94)" : "rgb(59, 130, 246)", stroke: "white", strokeWidth: 2, style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("circle", { cx: waypoint.x, cy: waypoint.y, r: 2, fill: "white", style: { pointerEvents: 'none' } }), isHovered && /*#__PURE__*/_react.default.createElement("g", null, /*#__PURE__*/_react.default.createElement("rect", { x: waypoint.x + 10, y: waypoint.y - 15, width: 80, height: 20, rx: 3, fill: "rgba(0, 0, 0, 0.8)", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("text", { x: waypoint.x + 50, y: waypoint.y - 5, textAnchor: "middle", dominantBaseline: "middle", fill: "white", fontSize: "11", style: { pointerEvents: 'none' } }, "Drag \u2022 Double-click to remove"))); })); }; var _default = exports.default = DrawIoOrthogonalEdge;