feat: Add auto-arrange button and functionality in ObjectFlowDesigner
This commit is contained in:
353
script.js
353
script.js
@@ -57,6 +57,11 @@ class ObjectFlowDesigner {
|
||||
.getElementById("deleteBtn")
|
||||
.addEventListener("click", () => this.setMode("delete"));
|
||||
|
||||
// Auto-arrange button - one simple button to make everything tidy
|
||||
document
|
||||
.getElementById("autoArrangeBtn")
|
||||
.addEventListener("click", () => this.autoArrangeObjects());
|
||||
|
||||
// Control buttons
|
||||
document
|
||||
.getElementById("clearBtn")
|
||||
@@ -1892,6 +1897,354 @@ class ObjectFlowDesigner {
|
||||
this.calculateVoltageDrops();
|
||||
this.render();
|
||||
}
|
||||
|
||||
// Simple Auto-Arrange Tool - One Button to Make Everything Tidy
|
||||
getSelectedObjects() {
|
||||
// For now, we'll work with all objects if none specifically selected
|
||||
// Later this could be enhanced to work with multi-selection
|
||||
return this.objects.filter((obj) => obj === this.selectedObject);
|
||||
}
|
||||
|
||||
getAllObjects() {
|
||||
return this.objects;
|
||||
}
|
||||
|
||||
alignObjects(direction) {
|
||||
const objectsToAlign = this.getSelectedObjects();
|
||||
|
||||
// If no specific selection, work with all objects
|
||||
if (objectsToAlign.length === 0) {
|
||||
if (this.objects.length < 2) {
|
||||
this.showError("Need at least 2 objects to align");
|
||||
return;
|
||||
}
|
||||
// Work with all objects
|
||||
this.alignAllObjects(direction);
|
||||
} else if (objectsToAlign.length < 2) {
|
||||
this.showError("Select at least 2 objects to align");
|
||||
return;
|
||||
} else {
|
||||
this.alignSelectedObjects(objectsToAlign, direction);
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
alignAllObjects(direction) {
|
||||
const objects = this.objects;
|
||||
if (objects.length < 2) return;
|
||||
|
||||
// Calculate bounds
|
||||
const bounds = this.calculateBounds(objects);
|
||||
|
||||
objects.forEach((obj) => {
|
||||
switch (direction) {
|
||||
case "left":
|
||||
obj.x = bounds.minX;
|
||||
break;
|
||||
case "center":
|
||||
obj.x = bounds.centerX - obj.width / 2;
|
||||
break;
|
||||
case "right":
|
||||
obj.x = bounds.maxX - obj.width;
|
||||
break;
|
||||
case "top":
|
||||
obj.y = bounds.minY;
|
||||
break;
|
||||
case "middle":
|
||||
obj.y = bounds.centerY - obj.height / 2;
|
||||
break;
|
||||
case "bottom":
|
||||
obj.y = bounds.maxY - obj.height;
|
||||
break;
|
||||
}
|
||||
|
||||
// Snap to grid
|
||||
obj.x = Math.round(obj.x / this.gridSize) * this.gridSize;
|
||||
obj.y = Math.round(obj.y / this.gridSize) * this.gridSize;
|
||||
});
|
||||
}
|
||||
|
||||
alignSelectedObjects(objects, direction) {
|
||||
if (objects.length < 2) return;
|
||||
|
||||
// Use first object as reference
|
||||
const reference = objects[0];
|
||||
|
||||
objects.slice(1).forEach((obj) => {
|
||||
switch (direction) {
|
||||
case "left":
|
||||
obj.x = reference.x;
|
||||
break;
|
||||
case "center":
|
||||
obj.x = reference.x + reference.width / 2 - obj.width / 2;
|
||||
break;
|
||||
case "right":
|
||||
obj.x = reference.x + reference.width - obj.width;
|
||||
break;
|
||||
case "top":
|
||||
obj.y = reference.y;
|
||||
break;
|
||||
case "middle":
|
||||
obj.y = reference.y + reference.height / 2 - obj.height / 2;
|
||||
break;
|
||||
case "bottom":
|
||||
obj.y = reference.y + reference.height - obj.height;
|
||||
break;
|
||||
}
|
||||
|
||||
// Snap to grid
|
||||
obj.x = Math.round(obj.x / this.gridSize) * this.gridSize;
|
||||
obj.y = Math.round(obj.y / this.gridSize) * this.gridSize;
|
||||
});
|
||||
}
|
||||
|
||||
distributeObjects(direction) {
|
||||
const objects = this.objects;
|
||||
if (objects.length < 3) {
|
||||
this.showError("Need at least 3 objects to distribute");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort objects by position
|
||||
const sortedObjects = [...objects].sort((a, b) => {
|
||||
if (direction === "horizontal") {
|
||||
return a.x - b.x;
|
||||
} else {
|
||||
return a.y - b.y;
|
||||
}
|
||||
});
|
||||
|
||||
const first = sortedObjects[0];
|
||||
const last = sortedObjects[sortedObjects.length - 1];
|
||||
|
||||
if (direction === "horizontal") {
|
||||
const totalSpace = last.x + last.width - first.x;
|
||||
const objectsWidth = sortedObjects.reduce(
|
||||
(sum, obj) => sum + obj.width,
|
||||
0
|
||||
);
|
||||
const availableSpace = totalSpace - objectsWidth;
|
||||
const spacing = availableSpace / (sortedObjects.length - 1);
|
||||
|
||||
let currentX = first.x + first.width;
|
||||
for (let i = 1; i < sortedObjects.length - 1; i++) {
|
||||
sortedObjects[i].x = currentX + spacing;
|
||||
currentX = sortedObjects[i].x + sortedObjects[i].width;
|
||||
|
||||
// Snap to grid
|
||||
sortedObjects[i].x =
|
||||
Math.round(sortedObjects[i].x / this.gridSize) * this.gridSize;
|
||||
}
|
||||
} else {
|
||||
const totalSpace = last.y + last.height - first.y;
|
||||
const objectsHeight = sortedObjects.reduce(
|
||||
(sum, obj) => sum + obj.height,
|
||||
0
|
||||
);
|
||||
const availableSpace = totalSpace - objectsHeight;
|
||||
const spacing = availableSpace / (sortedObjects.length - 1);
|
||||
|
||||
let currentY = first.y + first.height;
|
||||
for (let i = 1; i < sortedObjects.length - 1; i++) {
|
||||
sortedObjects[i].y = currentY + spacing;
|
||||
currentY = sortedObjects[i].y + sortedObjects[i].height;
|
||||
|
||||
// Snap to grid
|
||||
sortedObjects[i].y =
|
||||
Math.round(sortedObjects[i].y / this.gridSize) * this.gridSize;
|
||||
}
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
autoArrangeObjects() {
|
||||
if (this.objects.length === 0) return;
|
||||
|
||||
// Find transformer (should be at the top)
|
||||
const transformer = this.objects.find((obj) => obj.type === "triangle");
|
||||
|
||||
if (!transformer) {
|
||||
this.showError("No transformer found for auto-arrangement");
|
||||
return;
|
||||
}
|
||||
|
||||
// Position transformer at left center (for LEFT TO RIGHT flow)
|
||||
transformer.x = Math.round(80 / this.gridSize) * this.gridSize;
|
||||
transformer.y =
|
||||
Math.round(
|
||||
(this.canvas.height / 2 - transformer.height / 2) / this.gridSize
|
||||
) * this.gridSize;
|
||||
|
||||
// Get all nodes (non-transformer objects)
|
||||
const nodes = this.objects.filter((obj) => obj.type !== "triangle");
|
||||
|
||||
if (nodes.length === 0) {
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrange nodes in a tree-like structure based on connections
|
||||
this.arrangeNodesInTree(transformer, nodes);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
arrangeNodesInTree(transformer, nodes) {
|
||||
const arranged = new Set();
|
||||
const levels = [];
|
||||
|
||||
// Level 0: directly connected to transformer
|
||||
let currentLevel = [];
|
||||
transformer.connections.outputs.forEach((conn) => {
|
||||
if (nodes.includes(conn.to)) {
|
||||
currentLevel.push(conn.to);
|
||||
arranged.add(conn.to);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentLevel.length > 0) {
|
||||
levels.push(currentLevel);
|
||||
}
|
||||
|
||||
// Build subsequent levels
|
||||
while (currentLevel.length > 0) {
|
||||
const nextLevel = [];
|
||||
currentLevel.forEach((node) => {
|
||||
node.connections.outputs.forEach((conn) => {
|
||||
if (nodes.includes(conn.to) && !arranged.has(conn.to)) {
|
||||
nextLevel.push(conn.to);
|
||||
arranged.add(conn.to);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (nextLevel.length > 0) {
|
||||
levels.push(nextLevel);
|
||||
currentLevel = nextLevel;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining unconnected nodes to the last level
|
||||
const unconnected = nodes.filter((node) => !arranged.has(node));
|
||||
if (unconnected.length > 0) {
|
||||
if (levels.length === 0) {
|
||||
levels.push(unconnected);
|
||||
} else {
|
||||
levels[levels.length - 1].push(...unconnected);
|
||||
}
|
||||
}
|
||||
|
||||
// Position nodes level by level - LEFT TO RIGHT layout
|
||||
const levelSpacing = 200; // Horizontal spacing between columns
|
||||
const minNodeSpacing = 100; // Minimum vertical spacing between nodes
|
||||
|
||||
levels.forEach((level, levelIndex) => {
|
||||
// Calculate X position - all nodes in same level have same X (horizontally aligned)
|
||||
const x =
|
||||
transformer.x + transformer.width + 50 + levelIndex * levelSpacing;
|
||||
|
||||
// Calculate vertical positioning for this level
|
||||
const nodeCount = level.length;
|
||||
|
||||
if (nodeCount === 1) {
|
||||
// Single node - center it vertically with transformer
|
||||
const y = transformer.y + transformer.height / 2 - level[0].height / 2;
|
||||
level[0].y = Math.round(y / this.gridSize) * this.gridSize;
|
||||
level[0].x = Math.round(x / this.gridSize) * this.gridSize;
|
||||
} else {
|
||||
// Multiple nodes - distribute vertically across canvas height
|
||||
const canvasHeight = this.canvas.height;
|
||||
const margin = 60;
|
||||
const availableHeight = canvasHeight - 2 * margin;
|
||||
|
||||
// Calculate spacing to use full height effectively
|
||||
const nodeSpacing = Math.max(
|
||||
minNodeSpacing,
|
||||
availableHeight / (nodeCount + 1)
|
||||
);
|
||||
const totalUsedHeight = (nodeCount - 1) * nodeSpacing;
|
||||
const startY = margin + (availableHeight - totalUsedHeight) / 2;
|
||||
|
||||
// Position each node in this level
|
||||
level.forEach((node, nodeIndex) => {
|
||||
// All nodes in same level have same X coordinate (horizontally aligned)
|
||||
node.x = Math.round(x / this.gridSize) * this.gridSize;
|
||||
|
||||
// Distribute nodes vertically across available height
|
||||
const y = startY + nodeIndex * nodeSpacing;
|
||||
node.y = Math.round(y / this.gridSize) * this.gridSize;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Optimize the tree layout to prevent overlaps and improve readability
|
||||
this.optimizeTreeLayout(transformer, levels);
|
||||
}
|
||||
|
||||
// Optimize the tree layout to prevent overlaps and improve readability
|
||||
optimizeTreeLayout(transformer, levels) {
|
||||
levels.forEach((level, levelIndex) => {
|
||||
// Ensure nodes don't overlap horizontally with previous level
|
||||
if (levelIndex > 0) {
|
||||
const prevLevel = levels[levelIndex - 1];
|
||||
const prevMaxX = Math.max(
|
||||
...prevLevel.map((node) => node.x + node.width)
|
||||
);
|
||||
const currentMinX = Math.min(...level.map((node) => node.x));
|
||||
|
||||
if (currentMinX - prevMaxX < 40) {
|
||||
const adjustment = 40 - (currentMinX - prevMaxX);
|
||||
level.forEach((node) => {
|
||||
node.x =
|
||||
Math.round((node.x + adjustment) / this.gridSize) * this.gridSize;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure no nodes go off the top or bottom edge
|
||||
level.forEach((node) => {
|
||||
const minY = 20;
|
||||
const maxY = this.canvas.height - node.height - 20;
|
||||
|
||||
if (node.y < minY) {
|
||||
node.y = Math.round(minY / this.gridSize) * this.gridSize;
|
||||
} else if (node.y > maxY) {
|
||||
node.y = Math.round(maxY / this.gridSize) * this.gridSize;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
calculateBounds(objects) {
|
||||
if (objects.length === 0) return null;
|
||||
|
||||
let minX = objects[0].x;
|
||||
let maxX = objects[0].x + objects[0].width;
|
||||
let minY = objects[0].y;
|
||||
let maxY = objects[0].y + objects[0].height;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
minX = Math.min(minX, obj.x);
|
||||
maxX = Math.max(maxX, obj.x + obj.width);
|
||||
minY = Math.min(minY, obj.y);
|
||||
maxY = Math.max(maxY, obj.y + obj.height);
|
||||
});
|
||||
|
||||
return {
|
||||
minX,
|
||||
maxX,
|
||||
minY,
|
||||
maxY,
|
||||
centerX: (minX + maxX) / 2,
|
||||
centerY: (minY + maxY) / 2,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the application
|
||||
|
||||
Reference in New Issue
Block a user