@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
JavaScript
"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;