@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
715 lines (679 loc) • 26.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactflow = require("reactflow");
var _EdgeWorkerService = _interopRequireDefault(require("../../services/EdgeWorkerService"));
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); }
/**
* Enhanced Orthogonal Edge - Draw.io Style Implementation
* Features:
* - Web Worker-based processing for all operations
* - Intelligent waypoint optimization
* - Segment intersection detection & merging
* - Virtual bend points for dynamic waypoint insertion
* - Smart pathfinding with obstacle avoidance
* - Layout-aware routing
* - Performance monitoring and optimization
* - Draw.io-style connection point calculation
* - Smart debouncing for optimal performance
*/
// Draw.io-style connection point calculation
const calculateOptimalConnectionPoint = (node, targetPoint, sourcePoint) => {
if (!node) return null;
const bounds = {
x: node.position.x,
y: node.position.y,
width: node.width || 150,
height: node.height || 60,
right: node.position.x + (node.width || 150),
bottom: node.position.y + (node.height || 60)
};
const center = {
x: bounds.x + bounds.width / 2,
y: bounds.y + bounds.height / 2
};
// Calculate which side is closest to the target
const dx = targetPoint.x - center.x;
const dy = targetPoint.y - center.y;
// Add margin for better visual appearance (draw.io style)
const margin = 5;
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal connection
if (dx > 0) {
return {
x: bounds.right + margin,
y: center.y
};
} else {
return {
x: bounds.x - margin,
y: center.y
};
}
} else {
// Vertical connection
if (dy > 0) {
return {
x: center.x,
y: bounds.bottom + margin
};
} else {
return {
x: center.x,
y: bounds.y - margin
};
}
}
};
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
};
}
};
// Smart debouncing hook
const useSmartDebouncing = (id, processEdgeWithWorker) => {
const debounceTimers = (0, _react.useRef)(new Map());
const debouncedProcess = (0, _react.useCallback)((operation, delay) => {
const key = `${id}-${operation}`;
if (debounceTimers.current.has(key)) {
clearTimeout(debounceTimers.current.get(key));
}
const timer = setTimeout(() => {
processEdgeWithWorker(true);
debounceTimers.current.delete(key);
}, delay);
debounceTimers.current.set(key, timer);
}, [id, processEdgeWithWorker]);
// Different delays for different operations (optimized for performance)
const processOptimization = (0, _react.useCallback)(() => debouncedProcess('optimize', 150), [debouncedProcess]);
const processVirtualBends = (0, _react.useCallback)(() => debouncedProcess('virtual_bends', 100), [debouncedProcess]);
const processIntersections = (0, _react.useCallback)(() => debouncedProcess('intersections', 200), [debouncedProcess]);
// Cleanup on unmount
(0, _react.useEffect)(() => {
return () => {
debounceTimers.current.forEach(timer => clearTimeout(timer));
debounceTimers.current.clear();
};
}, []);
return {
processOptimization,
processVirtualBends,
processIntersections
};
};
const EnhancedOrthogonalEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerStart,
markerEnd,
data,
selected
}) => {
var _data$waypoints2;
const {
screenToFlowPosition,
setEdges,
getEdges,
getNodes
} = (0, _reactflow.useReactFlow)();
// State management
const [optimizedWaypoints, setOptimizedWaypoints] = (0, _react.useState)([]);
const [virtualBends, setVirtualBends] = (0, _react.useState)([]);
const [intersections, setIntersections] = (0, _react.useState)([]);
const [hoveredSegmentInfo, setHoveredSegmentInfo] = (0, _react.useState)(null);
const [hoveredVirtualBend, setHoveredVirtualBend] = (0, _react.useState)(null);
const [draggedSegmentIndex, setDraggedSegmentIndex] = (0, _react.useState)(null);
const [isProcessing, setIsProcessing] = (0, _react.useState)(false);
const [performanceMetrics, setPerformanceMetrics] = (0, _react.useState)({});
// Refs
const processingTimeoutRef = (0, _react.useRef)(null);
const lastUpdateTimeRef = (0, _react.useRef)(0);
const isVisibleRef = (0, _react.useRef)(true);
// Get node information
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));
// Calculate connection points with draw.io-style logic
const sourcePoint = (0, _react.useMemo)(() => {
if (sourceNode && sourcePosition) {
// Use draw.io-style connection point calculation
const targetPoint = targetNode ? {
x: targetNode.position.x + (targetNode.width || 150) / 2,
y: targetNode.position.y + (targetNode.height || 60) / 2
} : {
x: targetX,
y: targetY
};
return calculateOptimalConnectionPoint(sourceNode, targetPoint, {
x: sourceX,
y: sourceY
}) || getConnectionPoint(sourceNode.position.x, sourceNode.position.y, sourceNode.width || 150, sourceNode.height || 60, sourcePosition);
}
return {
x: sourceX,
y: sourceY
};
}, [sourceNode, sourcePosition, sourceX, sourceY, targetNode, targetX, targetY]);
const targetPoint = (0, _react.useMemo)(() => {
if (targetNode && targetPosition) {
// Use draw.io-style connection point calculation
const sourcePoint = sourceNode ? {
x: sourceNode.position.x + (sourceNode.width || 150) / 2,
y: sourceNode.position.y + (sourceNode.height || 60) / 2
} : {
x: sourceX,
y: sourceY
};
return calculateOptimalConnectionPoint(targetNode, sourcePoint, {
x: targetX,
y: targetY
}) || getConnectionPoint(targetNode.position.x, targetNode.position.y, targetNode.width || 150, targetNode.height || 60, targetPosition);
}
return {
x: targetX,
y: targetY
};
}, [targetNode, targetPosition, targetX, targetY, sourceNode, sourceX, sourceY]);
// Create edge object for Web Worker processing
const edgeForProcessing = (0, _react.useMemo)(() => ({
id,
source: sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.id,
target: targetNode === null || targetNode === void 0 ? void 0 : targetNode.id,
sourceHandle: sourcePosition,
targetHandle: targetPosition,
data: {
...data,
waypoints: (data === null || data === void 0 ? void 0 : data.waypoints) || []
}
}), [id, sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.id, targetNode === null || targetNode === void 0 ? void 0 : targetNode.id, sourcePosition, targetPosition, data]);
// Debounced processing function with smart optimization
const processEdgeWithWorker = (0, _react.useCallback)(async (forceUpdate = false) => {
const now = performance.now();
if (!forceUpdate && now - lastUpdateTimeRef.current < 100) {
// Debounce updates to prevent excessive processing
if (processingTimeoutRef.current) {
clearTimeout(processingTimeoutRef.current);
}
processingTimeoutRef.current = setTimeout(() => processEdgeWithWorker(true), 100);
return;
}
lastUpdateTimeRef.current = now;
setIsProcessing(true);
try {
var _data$waypoints;
// Process multiple operations in parallel for maximum performance
const [optimizedWaypoints, virtualBends, intersections] = await Promise.all([_EdgeWorkerService.default.optimizeWaypoints(edgeForProcessing, nodes), _EdgeWorkerService.default.calculateVirtualBends(edgeForProcessing, nodes), _EdgeWorkerService.default.detectIntersections(edgeForProcessing, nodes)]);
setOptimizedWaypoints(optimizedWaypoints);
setVirtualBends(virtualBends);
setIntersections(intersections);
// Update performance metrics
const metrics = await _EdgeWorkerService.default.getPerformanceMetrics();
setPerformanceMetrics(metrics);
// Update edge data if waypoints were optimized
if (optimizedWaypoints.length !== ((data === null || data === void 0 || (_data$waypoints = data.waypoints) === null || _data$waypoints === void 0 ? void 0 : _data$waypoints.length) || 0)) {
setEdges(edges => edges.map(edge => {
if (edge.id === id) {
return {
...edge,
data: {
...edge.data,
waypoints: optimizedWaypoints
}
};
}
return edge;
}));
}
} catch (error) {
console.error('❌ EnhancedOrthogonalEdge: Processing failed:', error);
} finally {
setIsProcessing(false);
}
}, [edgeForProcessing, nodes, data === null || data === void 0 || (_data$waypoints2 = data.waypoints) === null || _data$waypoints2 === void 0 ? void 0 : _data$waypoints2.length, id, setEdges]);
// Smart debouncing
const {
processOptimization,
processVirtualBends,
processIntersections
} = useSmartDebouncing(id, processEdgeWithWorker);
// Effect to trigger processing when edge or nodes change
(0, _react.useEffect)(() => {
processEdgeWithWorker();
}, [processEdgeWithWorker]);
// Use optimized waypoints or fallback to original
const activeWaypoints = (0, _react.useMemo)(() => {
return optimizedWaypoints.length > 0 ? optimizedWaypoints : (data === null || data === void 0 ? void 0 : data.waypoints) || [];
}, [optimizedWaypoints, data === null || data === void 0 ? void 0 : data.waypoints]);
// Generate SVG path with draw.io-style optimization
const path = (0, _react.useMemo)(() => {
// Use the calculated optimal connection points instead of original coordinates
const points = [sourcePoint, ...activeWaypoints, targetPoint];
let pathString = `M ${points[0].x},${points[0].y}`;
// Draw.io-style path generation with smooth transitions
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const current = points[i];
// Check if we need a smooth curve or straight line
const isOrthogonal = Math.abs(prev.x - current.x) < 5 || Math.abs(prev.y - current.y) < 5;
if (isOrthogonal) {
pathString += ` L ${current.x},${current.y}`;
} else {
// Smooth curve for non-orthogonal segments
const midX = (prev.x + current.x) / 2;
const midY = (prev.y + current.y) / 2;
pathString += ` Q ${midX},${midY} ${current.x},${current.y}`;
}
}
return pathString;
}, [sourcePoint, targetPoint, activeWaypoints]);
// Handle virtual bend click to add waypoint
const handleVirtualBendClick = (0, _react.useCallback)(async (event, virtualBend) => {
event.stopPropagation();
event.preventDefault();
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY
});
// Insert new waypoint at virtual bend position
const currentWaypoints = activeWaypoints || [];
const newWaypoints = [...currentWaypoints];
newWaypoints.splice(virtualBend.segmentIndex + 1, 0, position);
setEdges(edges => edges.map(edge => {
if (edge.id === id) {
return {
...edge,
data: {
...edge.data,
waypoints: newWaypoints
}
};
}
return edge;
}));
// Re-process with new waypoint
await processEdgeWithWorker(true);
}, [activeWaypoints, id, setEdges, screenToFlowPosition, processEdgeWithWorker]);
// Enhanced segment dragging with Web Worker processing
const handleSegmentMouseDown = (0, _react.useCallback)((event, segmentIndex) => {
event.stopPropagation();
event.preventDefault();
setDraggedSegmentIndex(segmentIndex);
const onMouseMove = async moveEvent => {
const position = screenToFlowPosition({
x: moveEvent.clientX,
y: moveEvent.clientY
});
setEdges(edges => edges.map(edge => {
if (edge.id === id) {
var _edge$data;
const currentWaypoints = ((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.waypoints) || [];
const points = [sourcePoint, ...currentWaypoints, targetPoint];
const p1 = points[segmentIndex];
const p2 = points[segmentIndex + 1];
if (!p1 || !p2) return edge;
const isHorizontal = Math.abs(p1.y - p2.y) < 10;
const newWaypoints = [...currentWaypoints];
if (isHorizontal) {
// Move horizontal segment vertically
const newY = position.y;
if (segmentIndex > 0 && newWaypoints[segmentIndex - 1]) {
newWaypoints[segmentIndex - 1] = {
...newWaypoints[segmentIndex - 1],
y: newY
};
}
if (segmentIndex < newWaypoints.length && newWaypoints[segmentIndex]) {
newWaypoints[segmentIndex] = {
...newWaypoints[segmentIndex],
y: newY
};
}
} else {
// Move vertical segment horizontally
const newX = position.x;
if (segmentIndex > 0 && newWaypoints[segmentIndex - 1]) {
newWaypoints[segmentIndex - 1] = {
...newWaypoints[segmentIndex - 1],
x: newX
};
}
if (segmentIndex < newWaypoints.length && newWaypoints[segmentIndex]) {
newWaypoints[segmentIndex] = {
...newWaypoints[segmentIndex],
x: newX
};
}
}
return {
...edge,
data: {
...edge.data,
waypoints: newWaypoints
}
};
}
return edge;
}));
};
const onMouseUp = async () => {
setDraggedSegmentIndex(null);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
// Re-process after drag with optimization
processOptimization();
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}, [id, sourcePoint, targetPoint, setEdges, screenToFlowPosition, processOptimization]);
// Enhanced waypoint dragging
const onWaypointMouseDown = (0, _react.useCallback)((event, index) => {
event.stopPropagation();
event.preventDefault();
const onMouseMove = e => {
const position = screenToFlowPosition({
x: e.clientX,
y: e.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 newWaypoints = [...currentWaypoints];
newWaypoints[index] = position;
return {
...edge,
data: {
...edge.data,
waypoints: newWaypoints
}
};
}
return edge;
}));
};
const onMouseUp = async () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
// Re-process after waypoint drag with optimization
processOptimization();
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}, [id, setEdges, screenToFlowPosition, processOptimization]);
// Waypoint double-click to remove
const onWaypointDoubleClick = (0, _react.useCallback)(async (event, index) => {
event.stopPropagation();
event.preventDefault();
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) || [];
const newWaypoints = [...currentWaypoints];
newWaypoints.splice(index, 1);
return {
...edge,
data: {
...edge.data,
waypoints: newWaypoints
}
};
}
return edge;
}));
// Re-process after waypoint removal with optimization
processOptimization();
}, [id, setEdges, processOptimization]);
// Calculate label position with draw.io-style positioning
const labelPosition = (0, _react.useMemo)(() => {
const points = [sourcePoint, ...activeWaypoints, targetPoint];
if (points.length < 2) return {
x: (sourcePoint.x + targetPoint.x) / 2,
y: (sourcePoint.y + targetPoint.y) / 2
};
// Find the longest segment for better label placement
let maxLength = 0;
let bestSegment = 0;
for (let i = 0; i < points.length - 1; i++) {
const p1 = points[i];
const p2 = points[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 = points[bestSegment];
const p2 = points[bestSegment + 1];
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
}, [sourcePoint, targetPoint, activeWaypoints]);
// Render segments for dragging with enhanced visual feedback
const renderDraggableSegments = () => {
const points = [sourcePoint, ...activeWaypoints, 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)
}));
});
};
// Render virtual bend points with enhanced styling
const renderVirtualBends = () => {
if (!virtualBends || virtualBends.length === 0) return null;
return virtualBends.map((bend, index) => {
const isHovered = hoveredVirtualBend === index;
return /*#__PURE__*/_react.default.createElement("g", {
key: `virtual-bend-${index}`
}, /*#__PURE__*/_react.default.createElement("circle", {
cx: bend.x,
cy: bend.y,
r: isHovered ? 6 : 4,
fill: "rgba(59, 130, 246, 0.4)",
stroke: "rgb(59, 130, 246)",
strokeWidth: isHovered ? 2 : 1,
strokeDasharray: "3,3",
style: {
cursor: 'pointer'
},
onClick: event => handleVirtualBendClick(event, bend),
onMouseEnter: () => setHoveredVirtualBend(index),
onMouseLeave: () => setHoveredVirtualBend(null)
}), isHovered && /*#__PURE__*/_react.default.createElement("text", {
x: bend.x,
y: bend.y,
textAnchor: "middle",
dominantBaseline: "middle",
fill: "rgb(59, 130, 246)",
fontSize: "12",
fontWeight: "bold",
style: {
pointerEvents: 'none'
}
}, "+"));
});
};
// Render intersection points with enhanced styling
const renderIntersections = () => {
if (!intersections || intersections.length === 0) return null;
return intersections.map((intersection, index) => /*#__PURE__*/_react.default.createElement("g", {
key: `intersection-${index}`
}, /*#__PURE__*/_react.default.createElement("circle", {
cx: intersection.point.x,
cy: intersection.point.y,
r: 3,
fill: "rgba(255, 165, 0, 0.8)",
stroke: "rgba(255, 140, 0, 1)",
strokeWidth: 1
})));
};
// Cleanup on unmount
(0, _react.useEffect)(() => {
return () => {
if (processingTimeoutRef.current) {
clearTimeout(processingTimeoutRef.current);
}
};
}, []);
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_reactflow.BaseEdge, {
id: id,
path: path,
style: {
...style,
opacity: isProcessing ? 0.7 : 1,
transition: 'opacity 0.2s ease'
},
markerStart: markerStart,
markerEnd: markerEnd
}), isProcessing && /*#__PURE__*/_react.default.createElement("g", null, /*#__PURE__*/_react.default.createElement("circle", {
cx: labelPosition.x,
cy: labelPosition.y,
r: 8,
fill: "rgba(59, 130, 246, 0.1)",
stroke: "rgb(59, 130, 246)",
strokeWidth: 2,
opacity: 0.8
}, /*#__PURE__*/_react.default.createElement("animateTransform", {
attributeName: "transform",
type: "rotate",
values: `0 ${labelPosition.x} ${labelPosition.y};360 ${labelPosition.x} ${labelPosition.y}`,
dur: "1s",
repeatCount: "indefinite"
}))), /*#__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) || '', selected && performanceMetrics.avgProcessingTime && /*#__PURE__*/_react.default.createElement("div", {
style: {
fontSize: '10px',
color: '#666',
marginTop: '2px'
}
}, Math.round(performanceMetrics.avgProcessingTime), "ms avg"))), renderDraggableSegments(), !isProcessing && renderVirtualBends(), renderIntersections(), activeWaypoints.map((wp, i) => /*#__PURE__*/_react.default.createElement("g", {
key: `waypoint-${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)
}))), hoveredSegmentInfo && !draggedSegmentIndex && /*#__PURE__*/_react.default.createElement("foreignObject", {
x: labelPosition.x + 10,
y: labelPosition.y - 20,
width: 120,
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'
}
}, "Drag to reshape", virtualBends.length > 0 && /*#__PURE__*/_react.default.createElement("div", {
style: {
fontSize: '10px',
opacity: 0.8
}
}, "Click + to add bend"))));
};
var _default = exports.default = EnhancedOrthogonalEdge;