@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
443 lines (420 loc) • 17.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactflow = require("reactflow");
var _OrthogonalRouter = _interopRequireDefault(require("../../services/OrthogonalRouter"));
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); }
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
};
}
};
const OrthogonalEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerStart,
markerEnd,
data,
selected
}) => {
const {
screenToFlowPosition,
setEdges,
getEdges,
getNodes
} = (0, _reactflow.useReactFlow)();
const [hoveredSegmentInfo, setHoveredSegmentInfo] = (0, _react.useState)(null);
const [draggedSegmentIndex, setDraggedSegmentIndex] = (0, _react.useState)(null);
const [router] = (0, _react.useState)(() => new _OrthogonalRouter.default());
// Get node dimensions and positions for source/target
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));
const sourcePoint = (0, _react.useMemo)(() => {
return sourceNode && sourcePosition ? getConnectionPoint(sourceNode.position.x, sourceNode.position.y, sourceNode.width, sourceNode.height, sourcePosition) : {
x: sourceX,
y: sourceY
};
}, [sourceNode, sourcePosition, sourceX, sourceY]);
const targetPoint = (0, _react.useMemo)(() => {
return targetNode && targetPosition ? getConnectionPoint(targetNode.position.x, targetNode.position.y, targetNode.width, targetNode.height, targetPosition) : {
x: targetX,
y: targetY
};
}, [targetNode, targetPosition, targetX, targetY]);
// Auto-calculate route when source or target changes
const autoCalculatedRoute = (0, _react.useMemo)(() => {
if (!sourceNode || !targetNode) return {
waypoints: []
};
// Get all nodes as obstacles (excluding source and target)
const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({
x: node.position.x,
y: node.position.y,
width: node.width || 100,
height: node.height || 100
}));
// Calculate optimal route
return router.calculateOptimalRoute(sourceNode, targetNode, obstacles);
}, [sourceNode, targetNode, nodes, router]);
// Use existing waypoints or auto-calculated route
const waypoints = (0, _react.useMemo)(() => {
if (data !== null && data !== void 0 && data.waypoints && data.waypoints.length > 0) {
const validWaypoints = data.waypoints.filter(wp => wp && typeof wp.x === 'number' && typeof wp.y === 'number' && !isNaN(wp.x) && !isNaN(wp.y) && isFinite(wp.x) && isFinite(wp.y));
return validWaypoints;
}
// Use auto-calculated route
return autoCalculatedRoute.waypoints || [];
}, [data === null || data === void 0 ? void 0 : data.waypoints, autoCalculatedRoute.waypoints]);
// This function generates the SVG path string for the edge.
const path = (0, _react.useMemo)(() => {
const points = [sourcePoint, ...waypoints, targetPoint];
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;
}, [sourcePoint, targetPoint, waypoints]);
// Enhanced segment dragging with collision avoidance
const handleSegmentMouseDown = (0, _react.useCallback)((event, segmentIndex) => {
var _clickedEdge$data;
event.stopPropagation();
event.preventDefault();
console.log('Segment mouse down triggered:', segmentIndex);
setDraggedSegmentIndex(segmentIndex);
const initialEdges = getEdges();
const clickedEdge = initialEdges.find(e => e.id === id);
if (!clickedEdge) return;
// Get current waypoints
let currentWaypoints = ((_clickedEdge$data = clickedEdge.data) === null || _clickedEdge$data === void 0 ? void 0 : _clickedEdge$data.waypoints) || [];
// If no waypoints exist, use auto-calculated route
if (currentWaypoints.length === 0) {
currentWaypoints = autoCalculatedRoute.waypoints || [];
}
const points = [sourcePoint, ...currentWaypoints, targetPoint];
const p1 = points[segmentIndex];
const p2 = points[segmentIndex + 1];
if (!p1 || !p2) return;
// Determine if this is a horizontal or vertical segment
const isHorizontal = Math.abs(p1.y - p2.y) < 10;
const isVertical = Math.abs(p1.x - p2.x) < 10;
console.log('Dragging segment:', segmentIndex, 'isHorizontal:', isHorizontal, 'isVertical:', isVertical);
const onMouseMove = moveEvent => {
const position = screenToFlowPosition({
x: moveEvent.clientX,
y: moveEvent.clientY
});
console.log('Mouse position:', position);
// Get obstacles for collision avoidance
const obstacles = nodes.filter(node => node.id !== (sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.id) && node.id !== (targetNode === null || targetNode === void 0 ? void 0 : targetNode.id)).map(node => ({
x: node.position.x,
y: node.position.y,
width: node.width || 100,
height: node.height || 100
}));
setEdges(eds => eds.map(e => {
if (e.id === id) {
var _e$data;
let currentWaypoints = ((_e$data = e.data) === null || _e$data === void 0 ? void 0 : _e$data.waypoints) || [];
// If waypoints are empty, use auto-calculated route
if (currentWaypoints.length === 0) {
currentWaypoints = autoCalculatedRoute.waypoints || [];
}
// Adjust path for obstacles during dragging
const adjustedWaypoints = router.adjustPathForObstacles(currentWaypoints, obstacles, segmentIndex - 1,
// Adjust for source point offset
position);
console.log('Adjusted waypoints:', adjustedWaypoints);
return {
...e,
data: {
...e.data,
waypoints: adjustedWaypoints
}
};
}
return e;
}));
};
const onMouseUp = () => {
console.log('Mouse up - stopping drag');
setDraggedSegmentIndex(null);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
// Add event listeners to window
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}, [id, sourcePoint, targetPoint, getEdges, setEdges, screenToFlowPosition, nodes, sourceNode, targetNode, router, autoCalculatedRoute]);
// Enhanced waypoint dragging with collision avoidance
const onWaypointMouseDown = (0, _react.useCallback)((event, index) => {
event.stopPropagation();
event.preventDefault();
console.log('Waypoint mouse down triggered:', index);
const onMouseMove = e => {
const position = screenToFlowPosition({
x: e.clientX,
y: e.clientY
});
// Get obstacles for collision avoidance
const obstacles = nodes.filter(node => node.id !== (sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.id) && node.id !== (targetNode === null || targetNode === void 0 ? void 0 : targetNode.id)).map(node => ({
x: node.position.x,
y: node.position.y,
width: node.width || 100,
height: node.height || 100
}));
setEdges(eds => eds.map(edge => {
if (edge.id === id) {
var _edge$data;
let currentWaypoints = ((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.waypoints) || [];
// If waypoints are empty, use auto-calculated route
if (currentWaypoints.length === 0) {
currentWaypoints = autoCalculatedRoute.waypoints || [];
}
if (!currentWaypoints || currentWaypoints.length === 0 || index >= currentWaypoints.length) {
return edge;
}
// Adjust path for obstacles during waypoint dragging
const adjustedWaypoints = router.adjustPathForObstacles(currentWaypoints, obstacles, index, position);
return {
...edge,
data: {
...edge.data,
waypoints: adjustedWaypoints
}
};
}
return edge;
}));
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}, [id, setEdges, screenToFlowPosition, nodes, sourceNode, targetNode, router, autoCalculatedRoute]);
// Waypoint double-click handler - removes waypoint
const onWaypointDoubleClick = (0, _react.useCallback)((event, index) => {
event.stopPropagation();
event.preventDefault();
console.log('Waypoint double-click triggered:', index);
setEdges(eds => eds.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) || [];
if (!currentWaypoints || currentWaypoints.length === 0 || index >= currentWaypoints.length) {
return edge;
}
const newWaypoints = [...currentWaypoints];
newWaypoints.splice(index, 1);
return {
...edge,
data: {
...edge.data,
waypoints: newWaypoints
}
};
}
return edge;
}));
}, [id, setEdges]);
// Calculate label position
const labelPosition = (0, _react.useMemo)(() => {
const points = [sourcePoint, ...waypoints, targetPoint];
if (points.length < 2) return {
x: (sourcePoint.x + targetPoint.x) / 2,
y: (sourcePoint.y + targetPoint.y) / 2
};
const midIndex = Math.floor(points.length / 2);
const p1 = points[midIndex - 1];
const p2 = points[midIndex];
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
}, [sourcePoint, targetPoint, waypoints]);
// Auto-reroute when nodes move
(0, _react.useEffect)(() => {
if (sourceNode && targetNode && !(data !== null && data !== void 0 && data.waypoints)) {
// Auto-reroute only if no manual waypoints exist
const obstacles = nodes.filter(node => node.id !== sourceNode.id && node.id !== targetNode.id).map(node => ({
x: node.position.x,
y: node.position.y,
width: node.width || 100,
height: node.height || 100
}));
const newRoute = router.calculateOptimalRoute(sourceNode, targetNode, obstacles);
if (newRoute.waypoints.length > 0) {
setEdges(eds => eds.map(edge => {
var _edge$data3;
if (edge.id === id && (!((_edge$data3 = edge.data) !== null && _edge$data3 !== void 0 && _edge$data3.waypoints) || edge.data.waypoints.length === 0)) {
return {
...edge,
data: {
...edge.data,
waypoints: newRoute.waypoints
}
};
}
return edge;
}));
}
}
}, [sourceNode === null || sourceNode === void 0 ? void 0 : sourceNode.position, targetNode === null || targetNode === void 0 ? void 0 : targetNode.position, nodes, router, id, setEdges, data === null || data === void 0 ? void 0 : data.waypoints]);
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
}), /*#__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) || '')), (() => {
const points = [sourcePoint, ...waypoints, 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)
}));
});
})(), hoveredSegmentInfo && !draggedSegmentIndex && (() => {
const points = [sourcePoint, ...waypoints, targetPoint];
const p1 = points[hoveredSegmentInfo.segmentIndex];
const p2 = points[hoveredSegmentInfo.segmentIndex + 1];
if (!p1 || !p2) return null;
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2;
return /*#__PURE__*/_react.default.createElement("foreignObject", {
x: midX + 10,
y: midY - 20,
width: 80,
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',
zIndex: 1000
}
}, Math.round(midX), ", ", Math.round(midY)));
})(), waypoints.map((wp, i) => /*#__PURE__*/_react.default.createElement("g", {
key: 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)
}))), (!(data !== null && data !== void 0 && data.waypoints) || data.waypoints.length === 0) && /*#__PURE__*/_react.default.createElement("g", null, /*#__PURE__*/_react.default.createElement("circle", {
cx: labelPosition.x,
cy: labelPosition.y - 20,
r: 3,
fill: "rgba(34, 197, 94, 0.8)",
stroke: "rgba(34, 197, 94, 1)",
strokeWidth: 1
})));
};
var _default = exports.default = OrthogonalEdge;