@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
659 lines (620 loc) • 25.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.layoutAlgorithms = exports.autoLayoutNodes = exports.applyLayoutAlgorithm = void 0;
/**
* Enhanced Auto Layout System - Based on draw.io's spacing algorithms
* Prevents node overlap and maintains proper spacing using collision detection
*/
/**
* Check if two rectangles overlap with optional margin
*/
const rectanglesOverlap = (rect1, rect2, margin = 0) => {
return !(rect1.x + rect1.width + margin <= rect2.x || rect2.x + rect2.width + margin <= rect1.x || rect1.y + rect1.height + margin <= rect2.y || rect2.y + rect2.height + margin <= rect1.y);
};
/**
* Calculate dynamic spacing based on node dimensions (draw.io approach)
*/
const calculateDynamicSpacing = (nodeWidth, nodeHeight, baseSpacing = 20) => {
// Use larger dimension + percentage for dynamic spacing
const maxDimension = Math.max(nodeWidth, nodeHeight);
return Math.max(baseSpacing, maxDimension * 0.3);
};
/**
* Smart layout algorithm selection based on graph structure
*/
const selectLayoutAlgorithm = nodes => {
const containers = nodes.filter(n => n.type === 'container');
const components = nodes.filter(n => n.type === 'component');
const totalNodes = nodes.length;
// Hierarchical for container-heavy structures
if (containers.length > components.length) {
return 'hierarchical';
}
// Grid for many small components
if (totalNodes > 15 && components.length > containers.length * 3) {
return 'grid';
}
// Force-directed for mixed structures
if (totalNodes > 5 && totalNodes <= 15) {
return 'force';
}
// Smart circular for smaller sets
return 'smart-circular';
};
/**
* Hierarchical Layout (draw.io style) - Enhanced
*/
const hierarchicalLayout = (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const containers = nodes.filter(n => n.type === 'container' && !n.parentNode);
const components = nodes.filter(n => n.type === 'component' && !n.parentNode);
// Layout parameters based on draw.io
const intraCellSpacing = 80; // Increased horizontal spacing
const interRankCellSpacing = 140; // Increased vertical spacing
const margin = 60;
let currentY = margin;
// For containers, use intelligent multi-row layout
if (containers.length > 0) {
// Calculate optimal layout grid for containers
const containerWidths = containers.map(c => {
var _c$style;
return ((_c$style = c.style) === null || _c$style === void 0 ? void 0 : _c$style.width) || 300;
});
const containerHeights = containers.map(c => {
var _c$style2;
return ((_c$style2 = c.style) === null || _c$style2 === void 0 ? void 0 : _c$style2.height) || 200;
});
const avgWidth = containerWidths.reduce((a, b) => a + b, 0) / containerWidths.length;
const maxWidth = Math.max(...containerWidths);
// Determine containers per row based on viewport and container sizes
const minSpacingNeeded = maxWidth + intraCellSpacing;
const maxContainersPerRow = Math.max(2, Math.floor((viewportWidth - 2 * margin) / minSpacingNeeded));
const containersPerRow = Math.min(maxContainersPerRow, Math.ceil(Math.sqrt(containers.length)));
let currentX = margin;
let rowMaxHeight = 0;
containers.forEach((container, idx) => {
var _container$style, _container$style2;
// Start new row if needed
if (idx > 0 && idx % containersPerRow === 0) {
currentY += rowMaxHeight + interRankCellSpacing;
currentX = margin;
rowMaxHeight = 0;
}
const containerWidth = ((_container$style = container.style) === null || _container$style === void 0 ? void 0 : _container$style.width) || 300;
const containerHeight = ((_container$style2 = container.style) === null || _container$style2 === void 0 ? void 0 : _container$style2.height) || 200;
const spacing = calculateDynamicSpacing(containerWidth, containerHeight, intraCellSpacing);
container.position = {
x: currentX,
y: currentY
};
currentX += containerWidth + spacing;
rowMaxHeight = Math.max(rowMaxHeight, containerHeight);
});
// Move Y position for next level
currentY += rowMaxHeight + interRankCellSpacing;
}
// For components, use grid layout with proper spacing
if (components.length > 0) {
const componentWidths = components.map(c => {
var _c$style3;
return ((_c$style3 = c.style) === null || _c$style3 === void 0 ? void 0 : _c$style3.width) || 200;
});
const componentHeights = components.map(c => {
var _c$style4;
return ((_c$style4 = c.style) === null || _c$style4 === void 0 ? void 0 : _c$style4.height) || 100;
});
const avgCompWidth = componentWidths.reduce((a, b) => a + b, 0) / componentWidths.length;
const maxCompWidth = Math.max(...componentWidths);
const minCompSpacing = maxCompWidth + intraCellSpacing;
const maxComponentsPerRow = Math.max(3, Math.floor((viewportWidth - 2 * margin) / minCompSpacing));
const componentsPerRow = Math.min(maxComponentsPerRow, Math.ceil(Math.sqrt(components.length)));
let currentX = margin;
let rowMaxHeight = 0;
components.forEach((component, idx) => {
var _component$style, _component$style2;
// Start new row if needed
if (idx > 0 && idx % componentsPerRow === 0) {
currentY += rowMaxHeight + 100; // Smaller spacing for components
currentX = margin;
rowMaxHeight = 0;
}
const componentWidth = ((_component$style = component.style) === null || _component$style === void 0 ? void 0 : _component$style.width) || 200;
const componentHeight = ((_component$style2 = component.style) === null || _component$style2 === void 0 ? void 0 : _component$style2.height) || 100;
const spacing = calculateDynamicSpacing(componentWidth, componentHeight, intraCellSpacing);
component.position = {
x: currentX,
y: currentY
};
currentX += componentWidth + spacing;
rowMaxHeight = Math.max(rowMaxHeight, componentHeight);
});
}
return nodes;
};
/**
* Smart Grid Layout with collision detection
*/
const gridLayout = (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const margin = 50;
const baseSpacing = 40;
// Sort nodes by size (larger first)
const sortedNodes = [...nodes].sort((a, b) => {
var _a$style, _a$style2, _b$style, _b$style2;
const aSize = (((_a$style = a.style) === null || _a$style === void 0 ? void 0 : _a$style.width) || 150) * (((_a$style2 = a.style) === null || _a$style2 === void 0 ? void 0 : _a$style2.height) || 100);
const bSize = (((_b$style = b.style) === null || _b$style === void 0 ? void 0 : _b$style.width) || 150) * (((_b$style2 = b.style) === null || _b$style2 === void 0 ? void 0 : _b$style2.height) || 100);
return bSize - aSize;
});
// Calculate optimal grid dimensions
const avgWidth = sortedNodes.reduce((sum, n) => {
var _n$style;
return sum + (((_n$style = n.style) === null || _n$style === void 0 ? void 0 : _n$style.width) || 150);
}, 0) / sortedNodes.length;
const avgHeight = sortedNodes.reduce((sum, n) => {
var _n$style2;
return sum + (((_n$style2 = n.style) === null || _n$style2 === void 0 ? void 0 : _n$style2.height) || 100);
}, 0) / sortedNodes.length;
const cols = Math.max(2, Math.floor((viewportWidth - 2 * margin) / (avgWidth + baseSpacing)));
const rows = Math.ceil(sortedNodes.length / cols);
sortedNodes.forEach((node, idx) => {
var _node$style, _node$style2;
const col = idx % cols;
const row = Math.floor(idx / cols);
const nodeWidth = ((_node$style = node.style) === null || _node$style === void 0 ? void 0 : _node$style.width) || 150;
const nodeHeight = ((_node$style2 = node.style) === null || _node$style2 === void 0 ? void 0 : _node$style2.height) || 100;
const spacing = calculateDynamicSpacing(nodeWidth, nodeHeight, baseSpacing);
node.position = {
x: margin + col * (avgWidth + spacing),
y: margin + row * (avgHeight + spacing)
};
});
return sortedNodes;
};
/**
* Force-directed layout with collision avoidance (WebCola inspired)
*/
const forceDirectedLayout = (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const center = {
x: viewportWidth / 2,
y: viewportHeight / 2
};
const iterations = 50;
const cellSpacing = 30; // Base collision margin
// Initialize positions randomly but avoid edges
nodes.forEach(node => {
if (!node.position) {
node.position = {
x: 100 + Math.random() * (viewportWidth - 200),
y: 100 + Math.random() * (viewportHeight - 200)
};
}
});
// Force-directed simulation with collision detection
for (let iter = 0; iter < iterations; iter++) {
const forces = nodes.map(() => ({
x: 0,
y: 0
}));
// Repulsion forces (prevent overlap)
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
var _node1$style, _node1$style2, _node2$style, _node2$style2;
const node1 = nodes[i];
const node2 = nodes[j];
const dx = node2.position.x - node1.position.x;
const dy = node2.position.y - node1.position.y;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
// Calculate minimum safe distance
const w1 = ((_node1$style = node1.style) === null || _node1$style === void 0 ? void 0 : _node1$style.width) || 150;
const h1 = ((_node1$style2 = node1.style) === null || _node1$style2 === void 0 ? void 0 : _node1$style2.height) || 100;
const w2 = ((_node2$style = node2.style) === null || _node2$style === void 0 ? void 0 : _node2$style.width) || 150;
const h2 = ((_node2$style2 = node2.style) === null || _node2$style2 === void 0 ? void 0 : _node2$style2.height) || 100;
const minDistance = Math.max(w1, h1, w2, h2) / 2 + cellSpacing;
if (distance < minDistance * 2) {
const force = (minDistance * 2 - distance) / distance * 0.1;
const fx = dx / distance * force;
const fy = dy / distance * force;
forces[i].x -= fx;
forces[i].y -= fy;
forces[j].x += fx;
forces[j].y += fy;
}
}
// Attraction to center (weak)
const centerAttraction = 0.01;
forces[i].x += (center.x - nodes[i].position.x) * centerAttraction;
forces[i].y += (center.y - nodes[i].position.y) * centerAttraction;
}
// Apply forces with damping
const damping = 0.8;
nodes.forEach((node, i) => {
var _node$style3, _node$style4;
node.position.x += forces[i].x * damping;
node.position.y += forces[i].y * damping;
// Keep within viewport bounds
const nodeWidth = ((_node$style3 = node.style) === null || _node$style3 === void 0 ? void 0 : _node$style3.width) || 150;
const nodeHeight = ((_node$style4 = node.style) === null || _node$style4 === void 0 ? void 0 : _node$style4.height) || 100;
node.position.x = Math.max(50, Math.min(viewportWidth - nodeWidth - 50, node.position.x));
node.position.y = Math.max(50, Math.min(viewportHeight - nodeHeight - 50, node.position.y));
});
}
return nodes;
};
/**
* Smart Circular Layout (enhanced)
*/
const smartCircularLayout = (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const center = {
x: viewportWidth / 2,
y: viewportHeight / 2
};
if (nodes.length === 1) {
var _nodes$0$style, _nodes$0$style2;
nodes[0].position = {
x: center.x - (((_nodes$0$style = nodes[0].style) === null || _nodes$0$style === void 0 ? void 0 : _nodes$0$style.width) || 150) / 2,
y: center.y - (((_nodes$0$style2 = nodes[0].style) === null || _nodes$0$style2 === void 0 ? void 0 : _nodes$0$style2.height) || 100) / 2
};
return nodes;
}
// Calculate optimal radius based on node sizes and spacing
const avgNodeSize = nodes.reduce((sum, n) => {
var _n$style3, _n$style4;
const width = ((_n$style3 = n.style) === null || _n$style3 === void 0 ? void 0 : _n$style3.width) || 150;
const height = ((_n$style4 = n.style) === null || _n$style4 === void 0 ? void 0 : _n$style4.height) || 100;
return sum + Math.max(width, height);
}, 0) / nodes.length;
const totalPerimeter = nodes.reduce((sum, n) => {
var _n$style5, _n$style6;
const width = ((_n$style5 = n.style) === null || _n$style5 === void 0 ? void 0 : _n$style5.width) || 150;
const height = ((_n$style6 = n.style) === null || _n$style6 === void 0 ? void 0 : _n$style6.height) || 100;
const nodeSpacing = calculateDynamicSpacing(width, height, 40);
return sum + Math.max(width, height) + nodeSpacing;
}, 0);
const radius = Math.max(200, totalPerimeter / (2 * Math.PI));
const angleStep = 2 * Math.PI / nodes.length;
nodes.forEach((node, idx) => {
var _node$style5, _node$style6;
const angle = idx * angleStep;
const nodeWidth = ((_node$style5 = node.style) === null || _node$style5 === void 0 ? void 0 : _node$style5.width) || 150;
const nodeHeight = ((_node$style6 = node.style) === null || _node$style6 === void 0 ? void 0 : _node$style6.height) || 100;
node.position = {
x: center.x + radius * Math.cos(angle) - nodeWidth / 2,
y: center.y + radius * Math.sin(angle) - nodeHeight / 2
};
});
return nodes;
};
/**
* Post-layout collision detection and adjustment
*/
const resolveCollisions = (nodes, maxIterations = 10) => {
const cellSpacing = 20;
for (let iter = 0; iter < maxIterations; iter++) {
let hasCollisions = false;
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
var _node1$style3, _node1$style4, _node2$style3, _node2$style4;
const node1 = nodes[i];
const node2 = nodes[j];
const rect1 = {
x: node1.position.x,
y: node1.position.y,
width: ((_node1$style3 = node1.style) === null || _node1$style3 === void 0 ? void 0 : _node1$style3.width) || 150,
height: ((_node1$style4 = node1.style) === null || _node1$style4 === void 0 ? void 0 : _node1$style4.height) || 100
};
const rect2 = {
x: node2.position.x,
y: node2.position.y,
width: ((_node2$style3 = node2.style) === null || _node2$style3 === void 0 ? void 0 : _node2$style3.width) || 150,
height: ((_node2$style4 = node2.style) === null || _node2$style4 === void 0 ? void 0 : _node2$style4.height) || 100
};
if (rectanglesOverlap(rect1, rect2, cellSpacing)) {
hasCollisions = true;
// Calculate separation vector
const centerX1 = rect1.x + rect1.width / 2;
const centerY1 = rect1.y + rect1.height / 2;
const centerX2 = rect2.x + rect2.width / 2;
const centerY2 = rect2.y + rect2.height / 2;
const dx = centerX2 - centerX1;
const dy = centerY2 - centerY1;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
const minDistance = (Math.max(rect1.width, rect1.height) + Math.max(rect2.width, rect2.height)) / 2 + cellSpacing;
const separation = (minDistance - distance) / 2;
const moveX = dx / distance * separation;
const moveY = dy / distance * separation;
node1.position.x -= moveX;
node1.position.y -= moveY;
node2.position.x += moveX;
node2.position.y += moveY;
}
}
}
if (!hasCollisions) break;
}
return nodes;
};
/**
* Organization Chart Layouts (from draw.io)
*/
const orgChartLayouts = {
linear: (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const margin = 60;
const spacing = 100;
let currentY = margin;
nodes.forEach(node => {
var _node$style7, _node$style8;
const nodeWidth = ((_node$style7 = node.style) === null || _node$style7 === void 0 ? void 0 : _node$style7.width) || 200;
const nodeHeight = ((_node$style8 = node.style) === null || _node$style8 === void 0 ? void 0 : _node$style8.height) || 100;
node.position = {
x: (viewportWidth - nodeWidth) / 2,
// Center horizontally
y: currentY
};
currentY += nodeHeight + spacing;
});
return nodes;
},
hanger2: (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const margin = 60;
const spacing = 80;
const cols = 2;
nodes.forEach((node, idx) => {
var _node$style9, _node$style0;
const col = idx % cols;
const row = Math.floor(idx / cols);
const nodeWidth = ((_node$style9 = node.style) === null || _node$style9 === void 0 ? void 0 : _node$style9.width) || 200;
const nodeHeight = ((_node$style0 = node.style) === null || _node$style0 === void 0 ? void 0 : _node$style0.height) || 100;
node.position = {
x: margin + col * (nodeWidth + spacing),
y: margin + row * (nodeHeight + spacing)
};
});
return nodes;
},
hanger4: (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const margin = 60;
const spacing = 80;
const cols = 4;
nodes.forEach((node, idx) => {
var _node$style1, _node$style10;
const col = idx % cols;
const row = Math.floor(idx / cols);
const nodeWidth = ((_node$style1 = node.style) === null || _node$style1 === void 0 ? void 0 : _node$style1.width) || 200;
const nodeHeight = ((_node$style10 = node.style) === null || _node$style10 === void 0 ? void 0 : _node$style10.height) || 100;
node.position = {
x: margin + col * (nodeWidth + spacing),
y: margin + row * (nodeHeight + spacing)
};
});
return nodes;
},
fishbone1: (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const centerX = viewportWidth / 2;
const centerY = viewportHeight / 2;
const spacing = 120;
nodes.forEach((node, idx) => {
var _node$style11, _node$style12;
const nodeWidth = ((_node$style11 = node.style) === null || _node$style11 === void 0 ? void 0 : _node$style11.width) || 200;
const nodeHeight = ((_node$style12 = node.style) === null || _node$style12 === void 0 ? void 0 : _node$style12.height) || 100;
// Alternate sides (left/right of center)
const side = idx % 2 === 0 ? -1 : 1;
const level = Math.floor(idx / 2);
node.position = {
x: centerX + side * (nodeWidth + spacing) - nodeWidth / 2,
y: centerY + level * (nodeHeight + spacing) - nodeHeight / 2
};
});
return nodes;
},
fishbone2: (nodes, viewportWidth = 1400, viewportHeight = 800) => {
const centerX = viewportWidth / 2;
const centerY = viewportHeight / 2;
const spacing = 100;
nodes.forEach((node, idx) => {
var _node$style13, _node$style14;
const nodeWidth = ((_node$style13 = node.style) === null || _node$style13 === void 0 ? void 0 : _node$style13.width) || 200;
const nodeHeight = ((_node$style14 = node.style) === null || _node$style14 === void 0 ? void 0 : _node$style14.height) || 100;
// Four quadrant layout
const quadrant = idx % 4;
const level = Math.floor(idx / 4);
let x, y;
switch (quadrant) {
case 0:
// Top-left
x = centerX - nodeWidth - spacing * (level + 1);
y = centerY - nodeHeight - spacing * (level + 1);
break;
case 1:
// Top-right
x = centerX + spacing * (level + 1);
y = centerY - nodeHeight - spacing * (level + 1);
break;
case 2:
// Bottom-right
x = centerX + spacing * (level + 1);
y = centerY + spacing * (level + 1);
break;
case 3:
// Bottom-left
x = centerX - nodeWidth - spacing * (level + 1);
y = centerY + spacing * (level + 1);
break;
}
node.position = {
x,
y
};
});
return nodes;
}
};
/**
* All available layout algorithms (draw.io complete set)
*/
const layoutAlgorithms = exports.layoutAlgorithms = {
// Basic layouts
'hierarchical': {
name: 'Hierarchical',
category: 'Basic',
func: hierarchicalLayout
},
'grid': {
name: 'Grid',
category: 'Basic',
func: gridLayout
},
'circular': {
name: 'Circular',
category: 'Basic',
func: smartCircularLayout
},
'force-directed': {
name: 'Force Directed',
category: 'Basic',
func: forceDirectedLayout
},
// Organization charts
'org-linear': {
name: 'Linear',
category: 'Organization',
func: orgChartLayouts.linear
},
'org-hanger2': {
name: 'Hanger (2 columns)',
category: 'Organization',
func: orgChartLayouts.hanger2
},
'org-hanger4': {
name: 'Hanger (4 columns)',
category: 'Organization',
func: orgChartLayouts.hanger4
},
'org-fishbone1': {
name: 'Fishbone 1',
category: 'Organization',
func: orgChartLayouts.fishbone1
},
'org-fishbone2': {
name: 'Fishbone 2',
category: 'Organization',
func: orgChartLayouts.fishbone2
},
// Tree layouts
'tree-vertical': {
name: 'Vertical Tree',
category: 'Tree',
func: hierarchicalLayout
},
'tree-horizontal': {
name: 'Horizontal Tree',
category: 'Tree',
func: (nodes, w, h) => {
// Transpose hierarchical layout
const result = hierarchicalLayout(nodes, h, w);
return result.map(node => ({
...node,
position: {
x: node.position.y,
y: node.position.x
}
}));
}
},
// Special layouts
'radial': {
name: 'Radial',
category: 'Special',
func: smartCircularLayout
},
'compact': {
name: 'Compact Tree',
category: 'Special',
func: gridLayout
},
'organic': {
name: 'Organic',
category: 'Special',
func: forceDirectedLayout
}
};
/**
* Apply layout with specific algorithm
*/
const applyLayoutAlgorithm = (nodes, algorithm = 'hierarchical', viewportWidth = 1400, viewportHeight = 800) => {
const updated = [...nodes];
// Apply text-based sizing to all nodes
const estimateTextSize = (text, minW = 80, minH = 50, maxW = 300, maxH = 180) => {
if (!text) return {
width: minW,
height: minH
};
const words = text.split(/\s+/);
const avgCharWidth = 8;
const lineHeight = 20;
const padding = 24;
const totalChars = text.length;
const charsPerLine = Math.floor((maxW - padding) / avgCharWidth);
const lines = Math.max(1, Math.ceil(totalChars / charsPerLine));
const width = Math.min(maxW, Math.max(minW, totalChars * avgCharWidth / lines + padding));
const height = Math.min(maxH, Math.max(minW, lines * lineHeight + padding));
return {
width,
height
};
};
updated.forEach(node => {
var _node$data, _node$data2;
const text = `${((_node$data = node.data) === null || _node$data === void 0 ? void 0 : _node$data.label) || ''} ${((_node$data2 = node.data) === null || _node$data2 === void 0 ? void 0 : _node$data2.description) || ''}`.trim();
const textSize = estimateTextSize(text, node.type === 'container' ? 200 : 120, node.type === 'container' ? 120 : 80, node.type === 'container' ? 350 : 300, node.type === 'container' ? 200 : 180);
if (!node.style) node.style = {};
node.style.width = textSize.width;
node.style.height = textSize.height;
});
// Handle container layouts (nested nodes)
const containers = updated.filter(n => n.type === 'container');
containers.forEach(container => {
const children = updated.filter(n => n.parentNode === container.id);
if (children.length > 0) {
const gridNodes = gridLayout(children, container.style.width - 40, container.style.height - 60);
if (gridNodes.length > 0) {
const bounds = gridNodes.reduce((acc, node) => {
var _node$style15, _node$style16;
return {
minX: Math.min(acc.minX, node.position.x),
minY: Math.min(acc.minY, node.position.y),
maxX: Math.max(acc.maxX, node.position.x + (((_node$style15 = node.style) === null || _node$style15 === void 0 ? void 0 : _node$style15.width) || 150)),
maxY: Math.max(acc.maxY, node.position.y + (((_node$style16 = node.style) === null || _node$style16 === void 0 ? void 0 : _node$style16.height) || 100))
};
}, {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity
});
container.style.width = Math.max(container.style.width, bounds.maxX - bounds.minX + 80);
container.style.height = Math.max(container.style.height, bounds.maxY - bounds.minY + 100);
}
}
});
// Apply selected layout algorithm
const topLevelNodes = updated.filter(n => !n.parentNode);
const layoutConfig = layoutAlgorithms[algorithm];
let layoutResult;
if (layoutConfig && layoutConfig.func) {
layoutResult = layoutConfig.func(topLevelNodes, viewportWidth, viewportHeight);
} else {
console.warn(`Unknown layout algorithm: ${algorithm}, falling back to hierarchical`);
layoutResult = hierarchicalLayout(topLevelNodes, viewportWidth, viewportHeight);
}
// Final collision resolution pass
const finalNodes = resolveCollisions(layoutResult);
console.log(`Applied ${algorithm} layout algorithm with collision detection`);
return updated;
};
// Legacy function for backward compatibility
exports.applyLayoutAlgorithm = applyLayoutAlgorithm;
const autoLayoutNodes = (nodes, algorithm = null) => {
const selectedAlgorithm = algorithm || selectLayoutAlgorithm(nodes.filter(n => !n.parentNode));
return applyLayoutAlgorithm(nodes, selectedAlgorithm);
};
exports.autoLayoutNodes = autoLayoutNodes;