UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

297 lines (283 loc) 13.8 kB
"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;