@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
455 lines (435 loc) • 17.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactflow = require("reactflow");
var _PathfindingEngine = _interopRequireDefault(require("../../services/PathfindingEngine"));
var _GridSystem = _interopRequireDefault(require("../../services/GridSystem"));
var _CollisionDetector = _interopRequireDefault(require("../../services/CollisionDetector"));
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); }
/**
* Create a simple orthogonal path between two points
*/
const createSimpleOrthogonalPath = (start, end) => {
const path = [start];
// Create L-shaped path (horizontal first, then vertical)
const dx = Math.abs(end.x - start.x);
const dy = Math.abs(end.y - start.y);
if (dx > 10 && dy > 10) {
const midX = start.x + (end.x - start.x) * 0.7; // 70% of the way
path.push({
x: midX,
y: start.y
});
path.push({
x: midX,
y: end.y
});
} else if (dx > 10) {
// Only horizontal movement needed
const midX = start.x + (end.x - start.x) * 0.5;
path.push({
x: midX,
y: start.y
});
} else if (dy > 10) {
// Only vertical movement needed
const midY = start.y + (end.y - start.y) * 0.5;
path.push({
x: start.x,
y: midY
});
}
path.push(end);
return path;
};
/**
* IntelligentOrthogonalEdge - Automatic orthogonal routing like draw.io
* Provides intelligent pathfinding with collision avoidance and grid alignment
*/
const IntelligentOrthogonalEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerStart,
markerEnd,
data,
selected
}) => {
const {
getNodes,
getEdges,
setEdges
} = (0, _reactflow.useReactFlow)();
const [pathfindingEngine] = (0, _react.useState)(() => new _PathfindingEngine.default({
gridSize: 20,
jettySize: 20
}));
const [gridSystem] = (0, _react.useState)(() => new _GridSystem.default({
gridSize: 20
}));
const [collisionDetector] = (0, _react.useState)(() => new _CollisionDetector.default({
gridSize: 20,
nodeMargin: 20
}));
const [isRoutingActive, setIsRoutingActive] = (0, _react.useState)(true);
// Get current nodes and edges for pathfinding
const nodes = getNodes();
const edges = getEdges();
// Find source and target nodes
const currentEdge = (0, _react.useMemo)(() => {
return edges.find(e => e.id === id);
}, [edges, id]);
const sourceNode = (0, _react.useMemo)(() => {
const sourceId = (currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.source) || (data === null || data === void 0 ? void 0 : data.source);
const foundNode = nodes.find(n => n.id === sourceId);
if (!foundNode && process.env.NODE_ENV === 'development') {
console.warn('Source node not found:', {
sourceId,
availableNodes: nodes.map(n => n.id)
});
}
return foundNode;
}, [nodes, currentEdge, data === null || data === void 0 ? void 0 : data.source]);
const targetNode = (0, _react.useMemo)(() => {
const targetId = (currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.target) || (data === null || data === void 0 ? void 0 : data.target);
const foundNode = nodes.find(n => n.id === targetId);
if (!foundNode && process.env.NODE_ENV === 'development') {
console.warn('Target node not found:', {
targetId,
availableNodes: nodes.map(n => n.id)
});
}
return foundNode;
}, [nodes, currentEdge, data === null || data === void 0 ? void 0 : data.target]);
// Calculate optimal path using intelligent routing
const optimalPath = (0, _react.useMemo)(() => {
if (!sourceNode || !targetNode || !isRoutingActive) {
// Fallback to simple path if routing is disabled or nodes not found
if (process.env.NODE_ENV === 'development') {
console.warn('IntelligentOrthogonalEdge: Missing nodes or routing disabled', {
edgeId: id,
sourceNodeFound: !!sourceNode,
targetNodeFound: !!targetNode,
sourceId: (currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.source) || (data === null || data === void 0 ? void 0 : data.source),
targetId: (currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.target) || (data === null || data === void 0 ? void 0 : data.target),
isRoutingActive,
availableNodes: nodes.map(n => n.id)
});
}
return [{
x: sourceX,
y: sourceY
}, {
x: targetX,
y: targetY
}];
}
try {
// Get actual connection points from nodes with intelligent defaults
const sourceHandle = (currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.sourceHandle) || 'right-source';
const targetHandle = (currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.targetHandle) || 'left-target';
// Ensure we have valid source and target nodes
if (!sourceNode || !targetNode) {
console.warn('IntelligentOrthogonalEdge: Missing source or target node', {
sourceId: currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.source,
targetId: currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.target,
sourceNode: !!sourceNode,
targetNode: !!targetNode
});
return [{
x: sourceX,
y: sourceY
}, {
x: targetX,
y: targetY
}];
}
const sourcePoint = pathfindingEngine.getConnectionPoint(sourceNode, sourceHandle);
const targetPoint = pathfindingEngine.getConnectionPoint(targetNode, targetHandle);
// Connection points calculated successfully
// Update collision detection with current state
collisionDetector.updateCollisionMap(nodes.filter(n => n.id !== sourceNode.id && n.id !== targetNode.id), edges.filter(e => e.id !== id));
// Try to find optimal orthogonal path
let path;
try {
path = pathfindingEngine.findPath(sourcePoint, targetPoint, [], nodes.filter(n => n.id !== sourceNode.id && n.id !== targetNode.id));
// If pathfinding returns a valid path, use it
if (path && path.length >= 2) {
const orthogonalPath = gridSystem.enforceOrthogonalPath(path);
if (orthogonalPath && orthogonalPath.length >= 2) {
return orthogonalPath;
}
}
} catch (pathError) {
if (process.env.NODE_ENV === 'development') {
console.warn('Pathfinding failed, using simple routing', pathError);
}
}
// Fallback to simple orthogonal routing
return createSimpleOrthogonalPath(sourcePoint, targetPoint);
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('IntelligentOrthogonalEdge: Pathfinding failed, using fallback', error);
}
// Create simple orthogonal fallback using actual node positions if available
let start, end;
if (sourceNode && targetNode) {
// Use node centers as fallback
start = {
x: sourceNode.position.x + (sourceNode.width || 150) / 2,
y: sourceNode.position.y + (sourceNode.height || 100) / 2
};
end = {
x: targetNode.position.x + (targetNode.width || 150) / 2,
y: targetNode.position.y + (targetNode.height || 100) / 2
};
} else {
// Final fallback to provided coordinates
start = {
x: sourceX,
y: sourceY
};
end = {
x: targetX,
y: targetY
};
}
const snappedStart = gridSystem.snapToGrid(start);
const snappedEnd = gridSystem.snapToGrid(end);
// Create a simple L-shaped path as final fallback
const fallbackPath = [snappedStart];
const dx = Math.abs(snappedEnd.x - snappedStart.x);
const dy = Math.abs(snappedEnd.y - snappedStart.y);
if (dx > 20 || dy > 20) {
if (dx > dy) {
// Horizontal first
const midX = snappedStart.x + (snappedEnd.x - snappedStart.x) * 0.7;
fallbackPath.push({
x: midX,
y: snappedStart.y
});
fallbackPath.push({
x: midX,
y: snappedEnd.y
});
} else {
// Vertical first
const midY = snappedStart.y + (snappedEnd.y - snappedStart.y) * 0.7;
fallbackPath.push({
x: snappedStart.x,
y: midY
});
fallbackPath.push({
x: snappedEnd.x,
y: midY
});
}
}
fallbackPath.push(snappedEnd);
return fallbackPath;
}
}, [sourceNode, targetNode, sourcePosition, targetPosition, sourceX, sourceY, targetX, targetY, nodes, edges, id, isRoutingActive, pathfindingEngine, gridSystem, collisionDetector, currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.source, currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.sourceHandle, currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.target, currentEdge === null || currentEdge === void 0 ? void 0 : currentEdge.targetHandle, data === null || data === void 0 ? void 0 : data.source, data === null || data === void 0 ? void 0 : data.target]);
// Auto-update edge data with calculated waypoints
(0, _react.useEffect)(() => {
if (optimalPath.length > 2 && data && !data.manuallyEdited) {
const waypoints = optimalPath.slice(1, -1); // Remove start and end points
// Only update if waypoints have changed significantly
const existingWaypoints = data.waypoints || [];
const hasSignificantChange = waypoints.length !== existingWaypoints.length || waypoints.some((wp, i) => {
const existing = existingWaypoints[i];
return !existing || Math.abs(wp.x - existing.x) > 5 || Math.abs(wp.y - existing.y) > 5;
});
if (hasSignificantChange) {
setEdges(edges => edges.map(edge => {
if (edge.id === id) {
return {
...edge,
data: {
...edge.data,
waypoints,
autoRouted: true,
lastUpdated: Date.now()
}
};
}
return edge;
}));
}
}
}, [optimalPath, data, id, setEdges]);
// Generate SVG path from optimal route - CRITICAL FIX
const pathString = (0, _react.useMemo)(() => {
if (optimalPath.length < 2) {
// If no path is available, create a simple direct line
const directPath = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
if (process.env.NODE_ENV === 'development') {
console.log(`Edge ${id}: Using direct path:`, directPath);
}
return directPath;
}
let path = `M ${optimalPath[0].x},${optimalPath[0].y}`;
for (let i = 1; i < optimalPath.length; i++) {
path += ` L ${optimalPath[i].x},${optimalPath[i].y}`;
}
if (process.env.NODE_ENV === 'development') {
console.log(`Edge ${id}: Generated path:`, path, 'from optimalPath:', optimalPath);
}
return path;
}, [optimalPath, sourceX, sourceY, targetX, targetY, id]);
// Final fallback - ensure we always have a valid path
const finalPathString = (0, _react.useMemo)(() => {
if (pathString && pathString.length > 0) {
if (process.env.NODE_ENV === 'development') {
console.log(`Edge ${id}: Final path string:`, pathString);
}
return pathString;
}
// Ultimate fallback - direct line
const fallbackPath = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
if (process.env.NODE_ENV === 'development') {
console.log(`Edge ${id}: Using fallback path:`, fallbackPath);
}
return fallbackPath;
}, [pathString, sourceX, sourceY, targetX, targetY, id]);
// Calculate label position (center of path)
const labelPosition = (0, _react.useMemo)(() => {
if (optimalPath.length < 2) {
return {
x: (sourceX + targetX) / 2,
y: (sourceY + targetY) / 2
};
}
const midIndex = Math.floor(optimalPath.length / 2);
const midPoint = optimalPath[midIndex];
return {
x: midPoint.x,
y: midPoint.y
};
}, [optimalPath, sourceX, sourceY, targetX, targetY]);
// Handle manual routing toggle
const toggleManualRouting = (0, _react.useCallback)(() => {
setIsRoutingActive(!isRoutingActive);
setEdges(edges => edges.map(edge => {
if (edge.id === id) {
return {
...edge,
data: {
...edge.data,
manuallyEdited: isRoutingActive,
// If turning off routing, mark as manually edited
autoRouted: !isRoutingActive
}
};
}
return edge;
}));
}, [id, isRoutingActive, setEdges]);
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, process.env.NODE_ENV === 'development' && console.log(`Edge ${id}: Rendering BaseEdge with path:`, finalPathString), /*#__PURE__*/_react.default.createElement(_reactflow.BaseEdge, {
id: id,
path: finalPathString,
style: {
...style,
strokeWidth: selected ? 3 : 2,
stroke: selected ? '#2563eb' : style.stroke || '#6b7280',
strokeDasharray: data !== null && data !== void 0 && data.autoRouted ? 'none' : '5,5'
},
markerStart: markerStart,
markerEnd: markerEnd
}), process.env.NODE_ENV === 'development' && /*#__PURE__*/_react.default.createElement(_reactflow.EdgeLabelRenderer, null, /*#__PURE__*/_react.default.createElement("div", {
style: {
position: 'absolute',
left: (sourceX + targetX) / 2,
top: (sourceY + targetY) / 2 - 30,
fontSize: '10px',
padding: '2px 4px',
backgroundColor: 'rgba(255, 0, 0, 0.8)',
color: 'white',
borderRadius: '2px',
pointerEvents: 'none',
whiteSpace: 'nowrap',
zIndex: 1000
}
}, "Edge: ", id, " | Path: ", finalPathString.length, " chars")), selected && optimalPath.length > 2 && /*#__PURE__*/_react.default.createElement("g", {
className: "intelligent-waypoints"
}, optimalPath.slice(1, -1).map((point, index) => /*#__PURE__*/_react.default.createElement("circle", {
key: `waypoint-${index}`,
cx: point.x,
cy: point.y,
r: 3,
fill: "#10b981",
stroke: "#ffffff",
strokeWidth: 1,
className: "waypoint-indicator"
}))), selected && gridSystem.showGrid && /*#__PURE__*/_react.default.createElement("g", {
className: "grid-indicators"
}, optimalPath.map((point, index) => /*#__PURE__*/_react.default.createElement("rect", {
key: `grid-${index}`,
x: point.x - 1,
y: point.y - 1,
width: 2,
height: 2,
fill: "#ef4444",
className: "grid-alignment-indicator"
}))), /*#__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: '#374151',
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 intelligent-edge-label"
}, (data === null || data === void 0 ? void 0 : data.label) || '', selected && /*#__PURE__*/_react.default.createElement("button", {
onClick: toggleManualRouting,
style: {
marginLeft: '8px',
padding: '2px 4px',
fontSize: '10px',
border: '1px solid #d1d5db',
borderRadius: '2px',
background: isRoutingActive ? '#10b981' : '#6b7280',
color: 'white',
cursor: 'pointer'
},
title: isRoutingActive ? 'Switch to manual routing' : 'Switch to automatic routing'
}, isRoutingActive ? 'AUTO' : 'MANUAL'))), selected && /*#__PURE__*/_react.default.createElement(_reactflow.EdgeLabelRenderer, null, /*#__PURE__*/_react.default.createElement("div", {
style: {
position: 'absolute',
left: labelPosition.x + 100,
top: labelPosition.y - 20,
fontSize: '10px',
padding: '2px 4px',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
borderRadius: '2px',
pointerEvents: 'none',
whiteSpace: 'nowrap'
},
className: "path-quality-indicator"
}, "Segments: ", optimalPath.length - 1, " | Grid: ", gridSystem.validateOrthogonalPath(optimalPath) ? '✓' : '✗', " | Length: ", Math.round(optimalPath.reduce((sum, point, i) => {
if (i === 0) return sum;
const prev = optimalPath[i - 1];
return sum + Math.abs(point.x - prev.x) + Math.abs(point.y - prev.y);
}, 0)), "px")));
};
var _default = exports.default = IntelligentOrthogonalEdge;