@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
416 lines (387 loc) • 14 kB
JavaScript
"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;