UNPKG

@ichigo_san/graphing

Version:

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

416 lines (387 loc) 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LayoutService = void 0; var _autoLayout = require("../components/utils/autoLayout"); class LayoutService { constructor() { this.layoutAlgorithms = new Map(); this.setupDefaultAlgorithms(); } /** * Setup default layout algorithms */ setupDefaultAlgorithms() { // Register the default auto-layout algorithm this.registerAlgorithm('default', this.defaultAutoLayout.bind(this)); // Register hierarchical layout algorithm this.registerAlgorithm('hierarchical', this.hierarchicalLayout.bind(this)); // Register circular layout algorithm this.registerAlgorithm('circular', this.circularLayout.bind(this)); // Register grid layout algorithm this.registerAlgorithm('grid', this.gridLayout.bind(this)); } /** * Register a new layout algorithm * @param {string} name - Algorithm name * @param {Function} algorithm - Layout function */ registerAlgorithm(name, algorithm) { this.layoutAlgorithms.set(name, algorithm); } /** * Get available layout algorithms * @returns {Array} Array of algorithm names */ getAvailableAlgorithms() { return Array.from(this.layoutAlgorithms.keys()); } /** * Apply layout to diagram data * @param {Object} diagramData - Diagram data to layout * @param {Object} options - Layout options * @returns {Promise<Object>} Layouted diagram data */ async layout(diagramData, options = {}) { const algorithm = options.algorithm || 'default'; const layoutAlgorithm = this.layoutAlgorithms.get(algorithm); if (!layoutAlgorithm) { throw new Error(`Layout algorithm '${algorithm}' not found`); } try { const layoutedData = await layoutAlgorithm(diagramData, options); return layoutedData; } catch (error) { throw new Error(`Layout failed: ${error.message}`); } } /** * Default auto-layout algorithm (from existing autoLayout.js) * @param {Object} diagramData - Diagram data * @param {Object} options - Layout options * @returns {Promise<Object>} Layouted diagram data */ async defaultAutoLayout(diagramData, options = {}) { const { containers = [], nodes = [], connections = [] } = diagramData; // Convert to React Flow format for auto-layout const reactFlowData = this.convertToReactFlowFormat(containers, nodes); // Apply auto-layout using existing utility with circular layout by default const layoutedNodes = (0, _autoLayout.autoLayoutNodes)(reactFlowData.nodes, 'circular'); // Convert back to diagram format const layoutedData = this.convertFromReactFlowFormat(layoutedNodes, connections); return layoutedData; } /** * Hierarchical layout algorithm * @param {Object} diagramData - Diagram data * @param {Object} options - Layout options * @returns {Promise<Object>} Layouted diagram data */ async hierarchicalLayout(diagramData, options = {}) { const { containers = [], nodes = [], connections = [] } = diagramData; const { spacing = 100, direction = 'vertical' } = options; const layoutedContainers = [...containers]; const layoutedNodes = [...nodes]; // Group nodes by containers const containerGroups = new Map(); layoutedNodes.forEach(node => { const containerId = node.parentContainer; if (!containerGroups.has(containerId)) { containerGroups.set(containerId, []); } containerGroups.get(containerId).push(node); }); // Layout containers in a hierarchical structure let currentY = 0; layoutedContainers.forEach((container, index) => { var _container$size; container.position = { x: 0, y: currentY }; currentY += (((_container$size = container.size) === null || _container$size === void 0 ? void 0 : _container$size.height) || 300) + spacing; }); // Layout nodes within containers containerGroups.forEach((nodesInContainer, containerId) => { const container = layoutedContainers.find(c => c.id === containerId); if (container) { let nodeY = container.position.y + 50; // Offset from container top nodesInContainer.forEach(node => { var _node$size; node.position = { x: container.position.x + 50, y: nodeY }; nodeY += (((_node$size = node.size) === null || _node$size === void 0 ? void 0 : _node$size.height) || 80) + 20; }); } }); return { containers: layoutedContainers, nodes: layoutedNodes, connections }; } /** * Circular layout algorithm * @param {Object} diagramData - Diagram data * @param {Object} options - Layout options * @returns {Promise<Object>} Layouted diagram data */ async circularLayout(diagramData, options = {}) { const { containers = [], nodes = [], connections = [] } = diagramData; const { radius = 200, centerX = 400, centerY = 300 } = options; const layoutedContainers = [...containers]; const layoutedNodes = [...nodes]; // Layout containers in a circle const totalContainers = layoutedContainers.length; layoutedContainers.forEach((container, index) => { const angle = 2 * Math.PI * index / totalContainers; container.position = { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) }; }); // Layout nodes in a smaller circle within their containers const nodeGroups = new Map(); layoutedNodes.forEach(node => { const containerId = node.parentContainer; if (!nodeGroups.has(containerId)) { nodeGroups.set(containerId, []); } nodeGroups.get(containerId).push(node); }); nodeGroups.forEach((nodesInContainer, containerId) => { const container = layoutedContainers.find(c => c.id === containerId); if (container) { const nodeRadius = 50; const totalNodes = nodesInContainer.length; nodesInContainer.forEach((node, index) => { const angle = 2 * Math.PI * index / totalNodes; node.position = { x: container.position.x + nodeRadius * Math.cos(angle), y: container.position.y + nodeRadius * Math.sin(angle) }; }); } }); return { containers: layoutedContainers, nodes: layoutedNodes, connections }; } /** * Grid layout algorithm * @param {Object} diagramData - Diagram data * @param {Object} options - Layout options * @returns {Promise<Object>} Layouted diagram data */ async gridLayout(diagramData, options = {}) { const { containers = [], nodes = [], connections = [] } = diagramData; const { columns = 3, spacing = 100 } = options; const layoutedContainers = [...containers]; const layoutedNodes = [...nodes]; // Layout containers in a grid layoutedContainers.forEach((container, index) => { var _container$size2, _container$size3; const row = Math.floor(index / columns); const col = index % columns; container.position = { x: col * (((_container$size2 = container.size) === null || _container$size2 === void 0 ? void 0 : _container$size2.width) || 400) + col * spacing, y: row * (((_container$size3 = container.size) === null || _container$size3 === void 0 ? void 0 : _container$size3.height) || 300) + row * spacing }; }); // Layout nodes in a grid within their containers const nodeGroups = new Map(); layoutedNodes.forEach(node => { const containerId = node.parentContainer; if (!nodeGroups.has(containerId)) { nodeGroups.set(containerId, []); } nodeGroups.get(containerId).push(node); }); nodeGroups.forEach((nodesInContainer, containerId) => { const container = layoutedContainers.find(c => c.id === containerId); if (container) { const nodeColumns = Math.ceil(Math.sqrt(nodesInContainer.length)); nodesInContainer.forEach((node, index) => { var _node$size2, _node$size3; const row = Math.floor(index / nodeColumns); const col = index % nodeColumns; node.position = { x: container.position.x + 50 + col * ((((_node$size2 = node.size) === null || _node$size2 === void 0 ? void 0 : _node$size2.width) || 150) + 20), y: container.position.y + 50 + row * ((((_node$size3 = node.size) === null || _node$size3 === void 0 ? void 0 : _node$size3.height) || 80) + 20) }; }); } }); return { containers: layoutedContainers, nodes: layoutedNodes, connections }; } /** * Convert diagram data to React Flow format for layout * @param {Array} containers - Container nodes * @param {Array} nodes - Regular nodes * @returns {Object} React Flow format data */ convertToReactFlowFormat(containers, nodes) { const reactFlowNodes = []; // Convert containers containers.forEach(container => { var _container$size4, _container$size5; reactFlowNodes.push({ id: container.id, type: 'container', position: container.position, data: { label: container.label, color: container.color || '#E3F2FD', bgColor: container.bgColor || '#ffffff', borderColor: container.borderColor || '#ddd', icon: container.icon, description: container.description }, style: { width: ((_container$size4 = container.size) === null || _container$size4 === void 0 ? void 0 : _container$size4.width) || 400, height: ((_container$size5 = container.size) === null || _container$size5 === void 0 ? void 0 : _container$size5.height) || 300, zIndex: container.zIndex || 1 }, zIndex: container.zIndex || 1 }); }); // Convert nodes nodes.forEach(node => { var _node$size4, _node$size5; reactFlowNodes.push({ id: node.id, type: node.type || 'component', position: node.position, parentNode: node.parentContainer, data: { label: node.label, color: node.color || '#E3F2FD', borderColor: node.borderColor || '#ddd', icon: node.icon, description: node.description }, style: { width: ((_node$size4 = node.size) === null || _node$size4 === void 0 ? void 0 : _node$size4.width) || 150, height: ((_node$size5 = node.size) === null || _node$size5 === void 0 ? void 0 : _node$size5.height) || 80, zIndex: node.zIndex || 10 }, zIndex: node.zIndex || 10 }); }); return { nodes: reactFlowNodes }; } /** * Convert React Flow format back to diagram format * @param {Array} reactFlowNodes - React Flow nodes * @param {Array} connections - Connections * @returns {Object} Diagram format data */ convertFromReactFlowFormat(reactFlowNodes, connections) { const containers = reactFlowNodes.filter(node => node.type === 'container').map(container => { var _container$style, _container$style2; return { id: container.id, label: container.data.label, position: container.position, size: { width: ((_container$style = container.style) === null || _container$style === void 0 ? void 0 : _container$style.width) || 400, height: ((_container$style2 = container.style) === null || _container$style2 === void 0 ? void 0 : _container$style2.height) || 300 }, color: container.data.color, bgColor: container.data.bgColor, borderColor: container.data.borderColor, icon: container.data.icon, description: container.data.description, zIndex: container.zIndex || 1 }; }); const nodes = reactFlowNodes.filter(node => node.type !== 'container').map(node => { var _node$style, _node$style2; return { id: node.id, label: node.data.label, type: node.type, position: node.position, parentContainer: node.parentNode, size: { width: ((_node$style = node.style) === null || _node$style === void 0 ? void 0 : _node$style.width) || 150, height: ((_node$style2 = node.style) === null || _node$style2 === void 0 ? void 0 : _node$style2.height) || 80 }, color: node.data.color, borderColor: node.data.borderColor, icon: node.data.icon, description: node.data.description, zIndex: node.zIndex || 10 }; }); return { containers, nodes, connections }; } /** * Calculate optimal spacing between elements * @param {Array} elements - Elements to calculate spacing for * @param {Object} options - Spacing options * @returns {number} Optimal spacing value */ calculateOptimalSpacing(elements, options = {}) { const { minSpacing = 50, maxSpacing = 200, defaultSpacing = 100 } = options; if (elements.length <= 1) { return defaultSpacing; } // Calculate average element size const totalSize = elements.reduce((sum, element) => { var _element$size, _element$size2; const width = ((_element$size = element.size) === null || _element$size === void 0 ? void 0 : _element$size.width) || 150; const height = ((_element$size2 = element.size) === null || _element$size2 === void 0 ? void 0 : _element$size2.height) || 80; return sum + Math.max(width, height); }, 0); const averageSize = totalSize / elements.length; // Return spacing based on average size return Math.max(minSpacing, Math.min(maxSpacing, averageSize * 0.5)); } } exports.LayoutService = LayoutService;