UNPKG

@ichigo_san/graphing

Version:

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

1,172 lines (1,115 loc) 39.7 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); } /** * Smart Orthogonal Edge - Draw.io Style Implementation * * Features: * - Draw.io-style jetty system for automatic spacing * - Proper orthogonal routing with collision avoidance * - Clean waypoint management (no on-the-fly creation) * - Segment dragging with orthogonal constraints * - Performance-optimized with minimal overhead * - Single unified component replacing multiple competing systems * * Fixes all issues: * ✅ Proper orthogonal bending * ✅ Clean edge movement/dragging * ✅ Fixed waypoint addition/persistence on double-click * ✅ Edge collision avoidance with spacing * ✅ No performance-heavy web worker processing */ // Draw.io style jetty distance calculation const JETTY_SIZE = 20; const MIN_SEGMENT_LENGTH = 30; const COLLISION_MARGIN = 15; /** * Draw.io-style connection point calculation with jetty system */ const calculateConnectionPoint = (node, targetPoint, position) => { if (!node) return null; const bounds = { x: node.position.x, y: node.position.y, width: node.width || 150, height: node.height || 60 }; const center = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }; // Calculate optimal connection side based on target const dx = targetPoint.x - center.x; const dy = targetPoint.y - center.y; let connectionPoint; // Use provided position if specified, otherwise auto-calculate if (position) { switch (position) { case _reactflow.Position.Top: connectionPoint = { x: center.x, y: bounds.y }; break; case _reactflow.Position.Right: connectionPoint = { x: bounds.x + bounds.width, y: center.y }; break; case _reactflow.Position.Bottom: connectionPoint = { x: center.x, y: bounds.y + bounds.height }; break; case _reactflow.Position.Left: connectionPoint = { x: bounds.x, y: center.y }; break; default: connectionPoint = { x: center.x, y: center.y }; } } else { // Auto-calculate best connection point if (Math.abs(dx) > Math.abs(dy)) { // Horizontal connection preferred if (dx > 0) { connectionPoint = { x: bounds.x + bounds.width, y: center.y }; } else { connectionPoint = { x: bounds.x, y: center.y }; } } else { // Vertical connection preferred if (dy > 0) { connectionPoint = { x: center.x, y: bounds.y + bounds.height }; } else { connectionPoint = { x: center.x, y: bounds.y }; } } } return connectionPoint; }; /** * Draw.io-style orthogonal routing with jetty system and fallback */ const calculateOrthogonalPath = (sourcePoint, targetPoint, existingEdges = [], nodes = []) => { const dx = targetPoint.x - sourcePoint.x; const dy = targetPoint.y - sourcePoint.y; // If points are already aligned, no waypoints needed if (Math.abs(dx) < 5 || Math.abs(dy) < 5) { return []; } try { // Calculate jetty points (connection points extended outward) const sourceJetty = calculateJettyPoint(sourcePoint, targetPoint, JETTY_SIZE); const targetJetty = calculateJettyPoint(targetPoint, sourcePoint, JETTY_SIZE); let waypoints = []; // Try advanced orthogonal routing first if (Math.abs(dx) > MIN_SEGMENT_LENGTH && Math.abs(dy) > MIN_SEGMENT_LENGTH) { if (Math.abs(dx) > Math.abs(dy)) { // Horizontal-first routing const midX = sourceJetty.x + (targetJetty.x - sourceJetty.x) / 2; waypoints.push({ x: midX, y: sourceJetty.y }); waypoints.push({ x: midX, y: targetJetty.y }); } else { // Vertical-first routing const midY = sourceJetty.y + (targetJetty.y - sourceJetty.y) / 2; waypoints.push({ x: sourceJetty.x, y: midY }); waypoints.push({ x: targetJetty.x, y: midY }); } } else { // Fallback to simple routing for short distances waypoints = calculateSimplePath(sourcePoint, targetPoint); } // Apply collision avoidance return applyCollisionAvoidance(waypoints, existingEdges, nodes, sourcePoint, targetPoint); } catch (error) { console.warn('Orthogonal routing failed, falling back to simple path:', error); return calculateSimplePath(sourcePoint, targetPoint); } }; /** * Simple fallback path calculation */ const calculateSimplePath = (sourcePoint, targetPoint) => { const dx = targetPoint.x - sourcePoint.x; const dy = targetPoint.y - sourcePoint.y; if (Math.abs(dx) < 5) { // Vertical line, no waypoints needed return []; } if (Math.abs(dy) < 5) { // Horizontal line, no waypoints needed return []; } // Single waypoint L-shape if (Math.abs(dx) > Math.abs(dy)) { return [{ x: targetPoint.x, y: sourcePoint.y }]; } else { return [{ x: sourcePoint.x, y: targetPoint.y }]; } }; /** * Calculate jetty point (connection point extended outward) */ const calculateJettyPoint = (connectionPoint, targetPoint, jettySize) => { const dx = targetPoint.x - connectionPoint.x; const dy = targetPoint.y - connectionPoint.y; if (Math.abs(dx) > Math.abs(dy)) { // Horizontal jetty const direction = dx > 0 ? 1 : -1; return { x: connectionPoint.x + jettySize * direction, y: connectionPoint.y }; } else { // Vertical jetty const direction = dy > 0 ? 1 : -1; return { x: connectionPoint.x, y: connectionPoint.y + jettySize * direction }; } }; /** * Enhanced collision avoidance - avoids nodes, edges, and other objects */ const applyCollisionAvoidance = (waypoints, existingEdges, nodes, sourcePoint, targetPoint) => { if (waypoints.length === 0) return waypoints; const adjustedWaypoints = [...waypoints]; const allPoints = [sourcePoint, ...adjustedWaypoints, targetPoint]; // Get all node bounds for obstacle avoidance const nodeObstacles = nodes.map(node => ({ x: node.position.x - COLLISION_MARGIN, y: node.position.y - COLLISION_MARGIN, width: (node.width || 150) + COLLISION_MARGIN * 2, height: (node.height || 60) + COLLISION_MARGIN * 2, right: node.position.x + (node.width || 150) + COLLISION_MARGIN, bottom: node.position.y + (node.height || 60) + COLLISION_MARGIN })); // Get all existing edge segments for collision detection const existingSegments = []; if (existingEdges && existingEdges.length > 0) { existingEdges.forEach(edge => { const segments = getEdgeSegments(edge, nodes); existingSegments.push(...segments.map(seg => ({ ...seg, edgeId: edge.id }))); }); } // Check each segment for collisions for (let i = 0; i < allPoints.length - 1; i++) { const segmentStart = allPoints[i]; const segmentEnd = allPoints[i + 1]; const segment = { start: segmentStart, end: segmentEnd, isHorizontal: Math.abs(segmentStart.y - segmentEnd.y) < 5, isVertical: Math.abs(segmentStart.x - segmentEnd.x) < 5 }; // Check for node collisions nodeObstacles.forEach(obstacle => { if (segmentIntersectsNode(segment, obstacle)) { offsetSegmentFromNode(adjustedWaypoints, allPoints, i, obstacle, sourcePoint, targetPoint); } }); // Check for parallel segment collisions with existing edges existingSegments.forEach(existingSegment => { if (detectParallelCollision(segment, existingSegment)) { offsetSegmentFromEdge(adjustedWaypoints, allPoints, i, existingSegment, sourcePoint, targetPoint); } }); // Update allPoints array for next iteration if (i === 0) { allPoints[i + 1] = adjustedWaypoints[i]; } else if (i < adjustedWaypoints.length) { allPoints[i + 1] = adjustedWaypoints[i]; } } return adjustedWaypoints; }; /** * Check if a segment intersects with a node */ const segmentIntersectsNode = (segment, node) => { const { start, end } = segment; // Check if segment passes through node bounds const segmentLeft = Math.min(start.x, end.x); const segmentRight = Math.max(start.x, end.x); const segmentTop = Math.min(start.y, end.y); const segmentBottom = Math.max(start.y, end.y); // Check for overlap const horizontalOverlap = !(segmentRight < node.x || segmentLeft > node.right); const verticalOverlap = !(segmentBottom < node.y || segmentTop > node.bottom); return horizontalOverlap && verticalOverlap; }; /** * Offset segment to avoid node collision */ const offsetSegmentFromNode = (waypoints, allPoints, segmentIndex, node, sourcePoint, targetPoint) => { const segment = { start: allPoints[segmentIndex], end: allPoints[segmentIndex + 1] }; const isHorizontal = Math.abs(segment.start.y - segment.end.y) < 5; const offset = COLLISION_MARGIN + 10; if (isHorizontal) { // Move horizontal segment above or below node const segmentY = segment.start.y; const nodeCenter = node.y + node.height / 2; const newY = segmentY > nodeCenter ? node.bottom + offset : node.y - offset; // Update waypoints if (segmentIndex === 0 && waypoints.length > 0) { waypoints[0] = { ...waypoints[0], y: newY }; } else if (segmentIndex < waypoints.length) { waypoints[segmentIndex] = { ...waypoints[segmentIndex], y: newY }; } if (segmentIndex + 1 < waypoints.length) { waypoints[segmentIndex + 1] = { ...waypoints[segmentIndex + 1], y: newY }; } } else { // Move vertical segment left or right of node const segmentX = segment.start.x; const nodeCenter = node.x + node.width / 2; const newX = segmentX > nodeCenter ? node.right + offset : node.x - offset; // Update waypoints if (segmentIndex === 0 && waypoints.length > 0) { waypoints[0] = { ...waypoints[0], x: newX }; } else if (segmentIndex < waypoints.length) { waypoints[segmentIndex] = { ...waypoints[segmentIndex], x: newX }; } if (segmentIndex + 1 < waypoints.length) { waypoints[segmentIndex + 1] = { ...waypoints[segmentIndex + 1], x: newX }; } } }; /** * Offset segment to avoid edge collision */ const offsetSegmentFromEdge = (waypoints, allPoints, segmentIndex, existingSegment, sourcePoint, targetPoint) => { const segment = { start: allPoints[segmentIndex], end: allPoints[segmentIndex + 1] }; const offset = COLLISION_MARGIN + 5; if (segment.isHorizontal && existingSegment.isHorizontal) { // Both horizontal - offset vertically const direction = segment.start.y > existingSegment.start.y ? 1 : -1; const newY = segment.start.y + offset * direction; if (segmentIndex === 0 && waypoints.length > 0) { waypoints[0] = { ...waypoints[0], y: newY }; } else if (segmentIndex < waypoints.length) { waypoints[segmentIndex] = { ...waypoints[segmentIndex], y: newY }; } if (segmentIndex + 1 < waypoints.length) { waypoints[segmentIndex + 1] = { ...waypoints[segmentIndex + 1], y: newY }; } } else if (segment.isVertical && existingSegment.isVertical) { // Both vertical - offset horizontally const direction = segment.start.x > existingSegment.start.x ? 1 : -1; const newX = segment.start.x + offset * direction; if (segmentIndex === 0 && waypoints.length > 0) { waypoints[0] = { ...waypoints[0], x: newX }; } else if (segmentIndex < waypoints.length) { waypoints[segmentIndex] = { ...waypoints[segmentIndex], x: newX }; } if (segmentIndex + 1 < waypoints.length) { waypoints[segmentIndex + 1] = { ...waypoints[segmentIndex + 1], x: newX }; } } }; /** * Get edge segments for collision detection */ const getEdgeSegments = (edge, nodes) => { var _targetNode$position, _targetNode$position2, _sourceNode$position, _sourceNode$position2, _edge$data; const sourceNode = nodes.find(n => n.id === edge.source); const targetNode = nodes.find(n => n.id === edge.target); if (!sourceNode || !targetNode) return []; const sourcePoint = sourceNode ? calculateConnectionPoint(sourceNode, { x: (targetNode === null || targetNode === void 0 || (_targetNode$position = targetNode.position) === null || _targetNode$position === void 0 ? void 0 : _targetNode$position.x) || 0, y: (targetNode === null || targetNode === void 0 || (_targetNode$position2 = targetNode.position) === null || _targetNode$position2 === void 0 ? void 0 : _targetNode$position2.y) || 0 }) || { x: 0, y: 0 } : { x: 0, y: 0 }; const targetPoint = targetNode ? calculateConnectionPoint(targetNode, { x: (sourceNode === null || sourceNode === void 0 || (_sourceNode$position = sourceNode.position) === null || _sourceNode$position === void 0 ? void 0 : _sourceNode$position.x) || 0, y: (sourceNode === null || sourceNode === void 0 || (_sourceNode$position2 = sourceNode.position) === null || _sourceNode$position2 === void 0 ? void 0 : _sourceNode$position2.y) || 0 }) || { x: 0, y: 0 } : { x: 0, y: 0 }; const waypoints = ((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.waypoints) || []; const allPoints = [sourcePoint, ...waypoints, targetPoint]; const segments = []; for (let i = 0; i < allPoints.length - 1; i++) { segments.push({ start: allPoints[i], end: allPoints[i + 1], isHorizontal: Math.abs(allPoints[i].y - allPoints[i + 1].y) < 5, isVertical: Math.abs(allPoints[i].x - allPoints[i + 1].x) < 5 }); } return segments; }; /** * Detect if two segments are parallel and potentially colliding */ const detectParallelCollision = (segment1, segment2) => { // Both horizontal if (segment1.isHorizontal && segment2.isHorizontal) { const yDiff = Math.abs(segment1.start.y - segment2.start.y); if (yDiff < COLLISION_MARGIN) { // Check for horizontal overlap const seg1Left = Math.min(segment1.start.x, segment1.end.x); const seg1Right = Math.max(segment1.start.x, segment1.end.x); const seg2Left = Math.min(segment2.start.x, segment2.end.x); const seg2Right = Math.max(segment2.start.x, segment2.end.x); return !(seg1Right < seg2Left || seg2Right < seg1Left); } } // Both vertical if (segment1.isVertical && segment2.isVertical) { const xDiff = Math.abs(segment1.start.x - segment2.start.x); if (xDiff < COLLISION_MARGIN) { // Check for vertical overlap const seg1Top = Math.min(segment1.start.y, segment1.end.y); const seg1Bottom = Math.max(segment1.start.y, segment1.end.y); const seg2Top = Math.min(segment2.start.y, segment2.end.y); const seg2Bottom = Math.max(segment2.start.y, segment2.end.y); return !(seg1Bottom < seg2Top || seg2Bottom < seg1Top); } } return false; }; /** * Offset a segment to avoid collision */ const offsetSegment = (waypoints, segmentIndex, collidingSegment) => { const offset = COLLISION_MARGIN + 5; if (collidingSegment.isHorizontal) { // Offset vertically const direction = waypoints[segmentIndex + 1].y > collidingSegment.start.y ? 1 : -1; waypoints[segmentIndex + 1].y += offset * direction; if (segmentIndex > 0) { waypoints[segmentIndex].y += offset * direction; } } else if (collidingSegment.isVertical) { // Offset horizontally const direction = waypoints[segmentIndex + 1].x > collidingSegment.start.x ? 1 : -1; waypoints[segmentIndex + 1].x += offset * direction; if (segmentIndex > 0) { waypoints[segmentIndex].x += offset * direction; } } }; /** * Generate SVG path string from points */ const generatePathString = points => { if (points.length < 2) return ''; 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; }; /** * Main SmartOrthogonalEdge Component */ const SmartOrthogonalEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerStart, markerEnd, data, selected }) => { const { screenToFlowPosition, setEdges, getEdges, getNodes } = (0, _reactflow.useReactFlow)(); // State const [hoveredSegment, setHoveredSegment] = (0, _react.useState)(null); const [draggedSegment, setDraggedSegment] = (0, _react.useState)(null); const [hoveredVirtualBend, setHoveredVirtualBend] = (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 points const sourcePoint = (0, _react.useMemo)(() => { if (sourceNode) { const targetPoint = targetNode ? calculateConnectionPoint(targetNode, { x: sourceX, y: sourceY }, targetPosition) || { x: targetX, y: targetY } : { x: targetX, y: targetY }; return calculateConnectionPoint(sourceNode, targetPoint, sourcePosition) || { x: sourceX, y: sourceY }; } return { x: sourceX, y: sourceY }; }, [sourceNode, targetNode, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition]); const targetPoint = (0, _react.useMemo)(() => { if (targetNode) { const sourcePoint = sourceNode ? calculateConnectionPoint(sourceNode, { x: targetX, y: targetY }, sourcePosition) || { x: sourceX, y: sourceY } : { x: sourceX, y: sourceY }; return calculateConnectionPoint(targetNode, sourcePoint, targetPosition) || { x: targetX, y: targetY }; } return { x: targetX, y: targetY }; }, [targetNode, sourceNode, targetX, targetY, sourceX, sourceY, targetPosition, sourcePosition]); // Calculate waypoints 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) { const validWaypoints = data.waypoints.filter(wp => wp && typeof wp.x === 'number' && typeof wp.y === 'number'); if (validWaypoints.length > 0) { return validWaypoints; } } // Calculate new orthogonal path const existingEdges = getEdges().filter(e => e.id !== id); return calculateOrthogonalPath(sourcePoint, targetPoint, existingEdges, nodes); }, [data === null || data === void 0 ? void 0 : data.waypoints, sourcePoint, targetPoint, id, getEdges, nodes]); // Generate all points for path const allPoints = (0, _react.useMemo)(() => { return [sourcePoint, ...waypoints, targetPoint]; }, [sourcePoint, waypoints, targetPoint]); // Generate SVG path const path = (0, _react.useMemo)(() => { return generatePathString(allPoints); }, [allPoints]); // Save initial waypoints to edge data (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]); // Calculate virtual bend points (for adding new waypoints) const virtualBends = (0, _react.useMemo)(() => { const bends = []; for (let i = 0; i < allPoints.length - 1; i++) { const p1 = allPoints[i]; const p2 = allPoints[i + 1]; const midPoint = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, segmentIndex: i }; bends.push(midPoint); } return bends; }, [allPoints]); // Handle virtual bend click (add waypoint) const handleVirtualBendClick = (0, _react.useCallback)((event, virtualBend) => { event.stopPropagation(); event.preventDefault(); const newWaypoints = [...waypoints]; const insertIndex = virtualBend.segmentIndex; // Insert new waypoint at virtual bend position newWaypoints.splice(insertIndex, 0, { x: virtualBend.x, y: virtualBend.y }); setEdges(edges => edges.map(edge => { if (edge.id === id) { return { ...edge, data: { ...edge.data, waypoints: newWaypoints } }; } return edge; })); }, [waypoints, id, setEdges]); // Enhanced draw.io-style segment dragging const handleSegmentMouseDown = (0, _react.useCallback)((event, segmentIndex) => { event.stopPropagation(); event.preventDefault(); setDraggedSegment(segmentIndex); const p1 = allPoints[segmentIndex]; const p2 = allPoints[segmentIndex + 1]; const isHorizontal = Math.abs(p1.y - p2.y) < 5; // Store initial mouse position for better dragging const initialMousePos = screenToFlowPosition({ x: event.clientX, y: event.clientY }); const onMouseMove = moveEvent => { const currentMousePos = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.clientY }); setEdges(edges => edges.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) || [])]; const allCurrentPoints = [sourcePoint, ...currentWaypoints, targetPoint]; // Create waypoints if they don't exist for this segment if (currentWaypoints.length === 0 && allCurrentPoints.length === 2) { // Need to create initial waypoints for dragging const midWaypoint = { x: (allCurrentPoints[0].x + allCurrentPoints[1].x) / 2, y: (allCurrentPoints[0].y + allCurrentPoints[1].y) / 2 }; currentWaypoints.push(midWaypoint); } // Ensure we have enough waypoints for the segment being dragged while (segmentIndex >= currentWaypoints.length) { const lastPoint = currentWaypoints[currentWaypoints.length - 1] || sourcePoint; const targetPt = targetPoint; currentWaypoints.push({ x: (lastPoint.x + targetPt.x) / 2, y: (lastPoint.y + targetPt.y) / 2 }); } if (isHorizontal) { // Move horizontal segment vertically const newY = currentMousePos.y; // Update both endpoints of the horizontal segment if (segmentIndex === 0) { // First segment - only update the waypoint if (currentWaypoints[0]) { currentWaypoints[0] = { ...currentWaypoints[0], y: newY }; } } else if (segmentIndex >= currentWaypoints.length) { // Last segment - only update the last waypoint if (currentWaypoints[currentWaypoints.length - 1]) { currentWaypoints[currentWaypoints.length - 1] = { ...currentWaypoints[currentWaypoints.length - 1], y: newY }; } } else { // Middle segment - update both waypoints if (currentWaypoints[segmentIndex - 1]) { currentWaypoints[segmentIndex - 1] = { ...currentWaypoints[segmentIndex - 1], y: newY }; } if (currentWaypoints[segmentIndex]) { currentWaypoints[segmentIndex] = { ...currentWaypoints[segmentIndex], y: newY }; } } } else { // Move vertical segment horizontally const newX = currentMousePos.x; // Update both endpoints of the vertical segment if (segmentIndex === 0) { // First segment - only update the waypoint if (currentWaypoints[0]) { currentWaypoints[0] = { ...currentWaypoints[0], x: newX }; } } else if (segmentIndex >= currentWaypoints.length) { // Last segment - only update the last waypoint if (currentWaypoints[currentWaypoints.length - 1]) { currentWaypoints[currentWaypoints.length - 1] = { ...currentWaypoints[currentWaypoints.length - 1], x: newX }; } } else { // Middle segment - update both waypoints if (currentWaypoints[segmentIndex - 1]) { currentWaypoints[segmentIndex - 1] = { ...currentWaypoints[segmentIndex - 1], x: newX }; } if (currentWaypoints[segmentIndex]) { currentWaypoints[segmentIndex] = { ...currentWaypoints[segmentIndex], x: newX }; } } } return { ...edge, data: { ...edge.data, waypoints: currentWaypoints } }; } return edge; })); }; const onMouseUp = () => { setDraggedSegment(null); window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); // Cleanup and optimize waypoints after dragging 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) || []; // Remove any waypoints that are too close to connection points const cleanedWaypoints = currentWaypoints.filter((wp, index) => { const prevPoint = index === 0 ? sourcePoint : currentWaypoints[index - 1]; const nextPoint = index === currentWaypoints.length - 1 ? targetPoint : currentWaypoints[index + 1]; const distToPrev = Math.sqrt(Math.pow(wp.x - prevPoint.x, 2) + Math.pow(wp.y - prevPoint.y, 2)); const distToNext = Math.sqrt(Math.pow(wp.x - nextPoint.x, 2) + Math.pow(wp.y - nextPoint.y, 2)); return distToPrev > 20 && distToNext > 20; }); return { ...edge, data: { ...edge.data, waypoints: cleanedWaypoints } }; } return edge; })); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }, [allPoints, id, setEdges, screenToFlowPosition, sourcePoint, targetPoint]); // Handle waypoint dragging const handleWaypointMouseDown = (0, _react.useCallback)((event, waypointIndex) => { event.stopPropagation(); event.preventDefault(); const onMouseMove = moveEvent => { const position = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.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) || [])]; if (waypointIndex < currentWaypoints.length) { currentWaypoints[waypointIndex] = position; } return { ...edge, data: { ...edge.data, waypoints: currentWaypoints } }; } return edge; })); }; const onMouseUp = () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); }, [id, setEdges, screenToFlowPosition]); // Handle waypoint double-click (remove waypoint) 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 { x: sourcePoint.x, y: sourcePoint.y }; // 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, sourcePoint]); 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, backdropFilter: 'blur(4px)' }, className: "nodrag nopan edge-label" }, data.label)), allPoints.slice(0, -1).map((p1, i) => { const p2 = allPoints[i + 1]; const isHorizontal = Math.abs(p1.y - p2.y) < 5; const isVertical = Math.abs(p1.x - p2.x) < 5; const isDragging = draggedSegment === i; const isHovered = hoveredSegment === i; // Calculate segment length for better hit detection const segmentLength = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); const showSegment = segmentLength > 10; // Only show segments longer than 10px if (!showSegment) return null; 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.9)" : isHovered ? "rgba(59, 130, 246, 0.7)" : "rgba(59, 130, 246, 0.2)", strokeWidth: isDragging ? 5 : isHovered ? 4 : 2, strokeDasharray: isDragging || isHovered ? "8,4" : "none", style: { cursor: isHorizontal ? 'ns-resize' : isVertical ? 'ew-resize' : 'move', pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("line", { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, stroke: "transparent", strokeWidth: Math.max(16, segmentLength * 0.1) // Adaptive hit area , style: { cursor: isHorizontal ? 'ns-resize' : isVertical ? 'ew-resize' : 'move' }, onMouseDown: event => handleSegmentMouseDown(event, i), onMouseEnter: () => setHoveredSegment(i), onMouseLeave: () => setHoveredSegment(null) }), isHovered && !isDragging && /*#__PURE__*/_react.default.createElement("g", null, isHorizontal && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("polygon", { points: `${(p1.x + p2.x) / 2 - 6},${(p1.y + p2.y) / 2 - 8} ${(p1.x + p2.x) / 2},${(p1.y + p2.y) / 2 - 2} ${(p1.x + p2.x) / 2 + 6},${(p1.y + p2.y) / 2 - 8}`, fill: "rgba(59, 130, 246, 0.8)", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("polygon", { points: `${(p1.x + p2.x) / 2 - 6},${(p1.y + p2.y) / 2 + 8} ${(p1.x + p2.x) / 2},${(p1.y + p2.y) / 2 + 2} ${(p1.x + p2.x) / 2 + 6},${(p1.y + p2.y) / 2 + 8}`, fill: "rgba(59, 130, 246, 0.8)", 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 - 6} ${(p1.x + p2.x) / 2 - 2},${(p1.y + p2.y) / 2} ${(p1.x + p2.x) / 2 - 8},${(p1.y + p2.y) / 2 + 6}`, fill: "rgba(59, 130, 246, 0.8)", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("polygon", { points: `${(p1.x + p2.x) / 2 + 8},${(p1.y + p2.y) / 2 - 6} ${(p1.x + p2.x) / 2 + 2},${(p1.y + p2.y) / 2} ${(p1.x + p2.x) / 2 + 8},${(p1.y + p2.y) / 2 + 6}`, fill: "rgba(59, 130, 246, 0.8)", style: { pointerEvents: 'none' } })))); }), virtualBends.map((bend, index) => { const isHovered = hoveredVirtualBend === index; const segmentLength = allPoints.length > index + 1 ? Math.sqrt(Math.pow(allPoints[index + 1].x - allPoints[index].x, 2) + Math.pow(allPoints[index + 1].y - allPoints[index].y, 2)) : 0; // Only show virtual bends on segments longer than 40px if (segmentLength < 40) return null; return /*#__PURE__*/_react.default.createElement("g", { key: `virtual-bend-${index}` }, /*#__PURE__*/_react.default.createElement("circle", { cx: bend.x, cy: bend.y, r: isHovered ? 10 : 8, fill: "rgba(255, 255, 255, 0.9)", stroke: "rgba(59, 130, 246, 0.3)", strokeWidth: 1, style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("circle", { cx: bend.x, cy: bend.y, r: isHovered ? 7 : 5, fill: isHovered ? "rgba(59, 130, 246, 0.7)" : "rgba(59, 130, 246, 0.4)", stroke: "rgb(59, 130, 246)", strokeWidth: isHovered ? 2 : 1, strokeDasharray: isHovered ? "none" : "3,2", style: { cursor: 'pointer' }, onClick: event => handleVirtualBendClick(event, bend), onMouseEnter: () => setHoveredVirtualBend(index), onMouseLeave: () => setHoveredVirtualBend(null) }), /*#__PURE__*/_react.default.createElement("g", { style: { pointerEvents: 'none' } }, /*#__PURE__*/_react.default.createElement("line", { x1: bend.x - 3, y1: bend.y, x2: bend.x + 3, y2: bend.y, stroke: isHovered ? "white" : "rgb(59, 130, 246)", strokeWidth: isHovered ? 2 : 1.5 }), /*#__PURE__*/_react.default.createElement("line", { x1: bend.x, y1: bend.y - 3, x2: bend.x, y2: bend.y + 3, stroke: isHovered ? "white" : "rgb(59, 130, 246)", strokeWidth: isHovered ? 2 : 1.5 })), isHovered && /*#__PURE__*/_react.default.createElement("g", null, /*#__PURE__*/_react.default.createElement("rect", { x: bend.x + 12, y: bend.y - 12, width: 70, height: 20, rx: 3, fill: "rgba(0, 0, 0, 0.8)", style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("text", { x: bend.x + 47, y: bend.y - 2, textAnchor: "middle", dominantBaseline: "middle", fill: "white", fontSize: "11", style: { pointerEvents: 'none' } }, "Click to add"))); }), waypoints.map((waypoint, index) => /*#__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: 7, fill: "white", stroke: "rgba(59, 130, 246, 0.3)", strokeWidth: 1, style: { pointerEvents: 'none' } }), /*#__PURE__*/_react.default.createElement("circle", { cx: waypoint.x, cy: waypoint.y, r: 5, fill: "rgb(59, 130, 246)", stroke: "white", strokeWidth: 2, style: { cursor: 'move' }, onMouseDown: event => handleWaypointMouseDown(event, index), onDoubleClick: event => handleWaypointDoubleClick(event, index) }), /*#__PURE__*/_react.default.createElement("circle", { cx: waypoint.x, cy: waypoint.y, r: 2, fill: "white", style: { pointerEvents: 'none' } }))), hoveredSegment !== null && /*#__PURE__*/_react.default.createElement("foreignObject", { x: labelPosition.x + 10, y: labelPosition.y - 30, width: 100, height: 60, style: { overflow: 'visible' } }, /*#__PURE__*/_react.default.createElement("div", { style: { backgroundColor: 'rgba(0, 0, 0, 0.8)', color: 'white', padding: '4px 8px', borderRadius: '4px', fontSize: '11px', pointerEvents: 'none', whiteSpace: 'nowrap' } }, "Drag to move", /*#__PURE__*/_react.default.createElement("br", null), /*#__PURE__*/_react.default.createElement("span", { style: { fontSize: '9px', opacity: 0.8 } }, "Click + to add bend")))); }; var _default = exports.default = SmartOrthogonalEdge;