@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
297 lines (283 loc) • 13.8 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); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
const AdjustableEdge = _ref => {
let {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerStart,
markerEnd,
data,
selected
} = _ref;
const {
project,
setEdges
} = (0, _reactflow.useReactFlow)();
// Get all edges and nodes to check for intersections
const edges = (0, _reactflow.useEdges)();
const nodes = (0, _reactflow.useNodes)();
const control = (data === null || data === void 0 ? void 0 : data.control) || {
x: (sourceX + targetX) / 2,
y: (sourceY + targetY) / 2
};
const intersection = (data === null || data === void 0 ? void 0 : data.intersection) || 'none';
// Helper function to get edge coordinates
const getEdgeCoordinates = (0, _react.useCallback)(edge => {
var _sourceNode$measured, _sourceNode$style, _sourceNode$measured2, _sourceNode$style2, _targetNode$measured, _targetNode$style, _targetNode$measured2, _targetNode$style2, _edge$data;
if (!nodes || nodes.length === 0) return null;
const sourceNode = nodes.find(n => n.id === edge.source);
const targetNode = nodes.find(n => n.id === edge.target);
if (!sourceNode || !targetNode) return null;
const sourcePos = sourceNode.positionAbsolute || sourceNode.position;
const targetPos = targetNode.positionAbsolute || targetNode.position;
if (!sourcePos || !targetPos) return null;
// Calculate center points of nodes
const sourceWidth = ((_sourceNode$measured = sourceNode.measured) === null || _sourceNode$measured === void 0 ? void 0 : _sourceNode$measured.width) || ((_sourceNode$style = sourceNode.style) === null || _sourceNode$style === void 0 ? void 0 : _sourceNode$style.width) || 150;
const sourceHeight = ((_sourceNode$measured2 = sourceNode.measured) === null || _sourceNode$measured2 === void 0 ? void 0 : _sourceNode$measured2.height) || ((_sourceNode$style2 = sourceNode.style) === null || _sourceNode$style2 === void 0 ? void 0 : _sourceNode$style2.height) || 80;
const targetWidth = ((_targetNode$measured = targetNode.measured) === null || _targetNode$measured === void 0 ? void 0 : _targetNode$measured.width) || ((_targetNode$style = targetNode.style) === null || _targetNode$style === void 0 ? void 0 : _targetNode$style.width) || 150;
const targetHeight = ((_targetNode$measured2 = targetNode.measured) === null || _targetNode$measured2 === void 0 ? void 0 : _targetNode$measured2.height) || ((_targetNode$style2 = targetNode.style) === null || _targetNode$style2 === void 0 ? void 0 : _targetNode$style2.height) || 80;
return {
sourceX: sourcePos.x + sourceWidth / 2,
sourceY: sourcePos.y + sourceHeight / 2,
targetX: targetPos.x + targetWidth / 2,
targetY: targetPos.y + targetHeight / 2,
control: ((_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.control) || {
x: (sourcePos.x + sourceWidth / 2 + targetPos.x + targetWidth / 2) / 2,
y: (sourcePos.y + sourceHeight / 2 + targetPos.y + targetHeight / 2) / 2
}
};
}, [nodes]);
// Line-line intersection function
const lineIntersection = (0, _react.useCallback)((line1, line2) => {
const [x1, y1, x2, y2] = line1;
const [x3, y3, x4, y4] = line2;
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (Math.abs(denom) < 1e-10) return null; // Lines are parallel
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
return {
x: x1 + t * (x2 - x1),
y: y1 + t * (y2 - y1),
t: t // Parameter along the first line
};
}
return null;
}, []);
// Get intersections with other edges
const intersections = (0, _react.useMemo)(() => {
if (intersection === 'none' || !edges || edges.length === 0) return [];
const currentSegments = [[sourceX, sourceY, control.x, control.y], [control.x, control.y, targetX, targetY]];
const result = [];
const seen = new Set();
edges.forEach(edge => {
if (edge.id === id) return;
const coords = getEdgeCoordinates(edge);
if (!coords) return;
const otherSegments = [[coords.sourceX, coords.sourceY, coords.control.x, coords.control.y], [coords.control.x, coords.control.y, coords.targetX, coords.targetY]];
currentSegments.forEach((currentSeg, segIndex) => {
otherSegments.forEach(otherSeg => {
const intersectionPoint = lineIntersection(currentSeg, otherSeg);
if (intersectionPoint) {
const key = "".concat(segIndex, "-").concat(intersectionPoint.x.toFixed(1), "-").concat(intersectionPoint.y.toFixed(1));
if (!seen.has(key)) {
seen.add(key);
result.push(_objectSpread(_objectSpread({}, intersectionPoint), {}, {
segmentIndex: segIndex
}));
}
}
});
});
});
return result;
}, [edges, id, sourceX, sourceY, targetX, targetY, control, intersection, getEdgeCoordinates, lineIntersection]);
// Generate path with intersections
const generatePathWithIntersections = (0, _react.useCallback)(() => {
if (intersection === 'none' || intersections.length === 0) {
return "M ".concat(sourceX, ",").concat(sourceY, " Q ").concat(control.x, ",").concat(control.y, " ").concat(targetX, ",").concat(targetY);
}
const segments = [{
start: {
x: sourceX,
y: sourceY
},
end: {
x: control.x,
y: control.y
}
}, {
start: {
x: control.x,
y: control.y
},
end: {
x: targetX,
y: targetY
}
}];
let pathParts = [];
segments.forEach((segment, segIndex) => {
const segmentIntersections = intersections.filter(int => int.segmentIndex === segIndex).sort((a, b) => a.t - b.t);
if (segmentIntersections.length === 0) {
// No intersections, draw normal line
if (segIndex === 0) {
pathParts.push("M ".concat(segment.start.x, ",").concat(segment.start.y, " L ").concat(segment.end.x, ",").concat(segment.end.y));
} else {
pathParts.push("L ".concat(segment.end.x, ",").concat(segment.end.y));
}
return;
}
// Draw segment with intersections
let lastPoint = segment.start;
if (segIndex === 0) {
pathParts.push("M ".concat(lastPoint.x, ",").concat(lastPoint.y));
}
segmentIntersections.forEach(int => {
const jumpSize = 12; // Size of the jump
// Calculate direction vector
const dx = segment.end.x - segment.start.x;
const dy = segment.end.y - segment.start.y;
const length = Math.sqrt(dx * dx + dy * dy);
const unitX = dx / length;
const unitY = dy / length;
// Calculate perpendicular vector
const perpX = -unitY;
const perpY = unitX;
// Points before and after the intersection
const beforeInt = {
x: int.x - unitX * jumpSize / 2,
y: int.y - unitY * jumpSize / 2
};
const afterInt = {
x: int.x + unitX * jumpSize / 2,
y: int.y + unitY * jumpSize / 2
};
// Draw line to before intersection
pathParts.push("L ".concat(beforeInt.x, ",").concat(beforeInt.y));
if (intersection === 'arc') {
// Arc jump - create a semicircle
const controlPoint = {
x: int.x + perpX * jumpSize / 2,
y: int.y + perpY * jumpSize / 2
};
pathParts.push("Q ".concat(controlPoint.x, ",").concat(controlPoint.y, " ").concat(afterInt.x, ",").concat(afterInt.y));
} else if (intersection === 'sharp') {
// Sharp jump - create an angular bridge
const peak1 = {
x: beforeInt.x + perpX * jumpSize / 2,
y: beforeInt.y + perpY * jumpSize / 2
};
const peak2 = {
x: afterInt.x + perpX * jumpSize / 2,
y: afterInt.y + perpY * jumpSize / 2
};
pathParts.push("L ".concat(peak1.x, ",").concat(peak1.y, " L ").concat(peak2.x, ",").concat(peak2.y, " L ").concat(afterInt.x, ",").concat(afterInt.y));
}
lastPoint = afterInt;
});
// Draw remaining part of segment
pathParts.push("L ".concat(segment.end.x, ",").concat(segment.end.y));
});
return pathParts.join(' ');
}, [sourceX, sourceY, targetX, targetY, control, intersection, intersections]);
const path = generatePathWithIntersections();
const labelPosition = {
x: 0.25 * sourceX + 0.5 * control.x + 0.25 * targetX,
y: 0.25 * sourceY + 0.5 * control.y + 0.25 * targetY
};
const updateControl = (0, _react.useCallback)(event => {
const flowBounds = document.querySelector('.react-flow');
if (!flowBounds) return;
const position = project({
x: event.clientX - flowBounds.getBoundingClientRect().left,
y: event.clientY - flowBounds.getBoundingClientRect().top
});
setEdges(eds => eds.map(e => e.id === id ? _objectSpread(_objectSpread({}, e), {}, {
data: _objectSpread(_objectSpread({}, e.data), {}, {
control: position
})
}) : e));
}, [id, project, setEdges]);
const onMouseDown = (0, _react.useCallback)(event => {
event.stopPropagation();
event.preventDefault();
const onMouseMove = e => updateControl(e);
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}, [updateControl]);
const label = (data === null || data === void 0 ? void 0 : data.label) || '';
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, label && /*#__PURE__*/_react.default.createElement("div", {
style: {
position: 'absolute',
pointerEvents: 'none',
transform: "translate(-50%, -50%) translate(".concat(labelPosition.x, "px, ").concat(labelPosition.y, "px)"),
fontSize: 12,
padding: '2px 4px',
background: 'white',
border: '1px solid #999',
borderRadius: 4,
color: '#333',
whiteSpace: 'nowrap'
}
}, label), /*#__PURE__*/_react.default.createElement("circle", {
className: "cursor-move fill-white stroke-gray-600 hover:stroke-indigo-500",
cx: control.x,
cy: control.y,
r: 6,
strokeWidth: 2,
onMouseDown: onMouseDown,
style: {
pointerEvents: 'all'
}
}), selected && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("circle", {
cx: sourceX,
cy: sourceY,
r: 5,
className: "fill-white stroke-indigo-600",
strokeWidth: 2
}), /*#__PURE__*/_react.default.createElement("circle", {
cx: targetX,
cy: targetY,
r: 5,
className: "fill-white stroke-indigo-600",
strokeWidth: 2
})), intersection !== 'none' && intersections.map((int, idx) => /*#__PURE__*/_react.default.createElement("circle", {
key: idx,
cx: int.x,
cy: int.y,
r: 3,
className: "fill-red-500 opacity-50",
style: {
pointerEvents: 'none'
}
}))));
};
var _default = exports.default = AdjustableEdge;