@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
881 lines (837 loc) • 28.2 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); }
/**
* 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;