diff --git a/index.html b/index.html index b5ec0f4..eaca65f 100644 --- a/index.html +++ b/index.html @@ -83,6 +83,13 @@ >
+ diff --git a/script.js b/script.js index 024c65e..cb2216a 100644 --- a/script.js +++ b/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 diff --git a/styles.css b/styles.css index eee434e..3e508a9 100644 --- a/styles.css +++ b/styles.css @@ -396,6 +396,248 @@ body { background: white; } +/* Alignment Icons */ +.icon-align-left { + width: 20px; + height: 16px; + position: relative; +} + +.icon-align-left::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background: #4a5568; + border-radius: 1px; + box-shadow: 0 4px 0 #4a5568, 0 8px 0 #4a5568, 0 12px 0 #4a5568; +} + +.icon-btn.active .icon-align-left::before { + background: white; + box-shadow: 0 4px 0 white, 0 8px 0 white, 0 12px 0 white; +} + +.icon-align-center { + width: 20px; + height: 16px; + position: relative; +} + +.icon-align-center::before { + content: ""; + position: absolute; + top: 0; + left: 2px; + width: 16px; + height: 2px; + background: #4a5568; + border-radius: 1px; + box-shadow: 0 4px 0 #4a5568, 0 8px 0 #4a5568, 0 12px 0 #4a5568; +} + +.icon-btn.active .icon-align-center::before { + background: white; + box-shadow: 0 4px 0 white, 0 8px 0 white, 0 12px 0 white; +} + +.icon-align-right { + width: 20px; + height: 16px; + position: relative; +} + +.icon-align-right::before { + content: ""; + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 2px; + background: #4a5568; + border-radius: 1px; + box-shadow: 0 4px 0 #4a5568, 0 8px 0 #4a5568, 0 12px 0 #4a5568; +} + +.icon-btn.active .icon-align-right::before { + background: white; + box-shadow: 0 4px 0 white, 0 8px 0 white, 0 12px 0 white; +} + +.icon-align-top { + width: 16px; + height: 20px; + position: relative; +} + +.icon-align-top::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 2px; + height: 100%; + background: #4a5568; + border-radius: 1px; + box-shadow: 4px 0 0 #4a5568, 8px 0 0 #4a5568, 12px 0 0 #4a5568; +} + +.icon-btn.active .icon-align-top::before { + background: white; + box-shadow: 4px 0 0 white, 8px 0 0 white, 12px 0 0 white; +} + +.icon-align-middle { + width: 16px; + height: 20px; + position: relative; +} + +.icon-align-middle::before { + content: ""; + position: absolute; + top: 2px; + left: 0; + width: 2px; + height: 16px; + background: #4a5568; + border-radius: 1px; + box-shadow: 4px 0 0 #4a5568, 8px 0 0 #4a5568, 12px 0 0 #4a5568; +} + +.icon-btn.active .icon-align-middle::before { + background: white; + box-shadow: 4px 0 0 white, 8px 0 0 white, 12px 0 0 white; +} + +.icon-align-bottom { + width: 16px; + height: 20px; + position: relative; +} + +.icon-align-bottom::before { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 2px; + height: 100%; + background: #4a5568; + border-radius: 1px; + box-shadow: 4px 0 0 #4a5568, 8px 0 0 #4a5568, 12px 0 0 #4a5568; +} + +.icon-btn.active .icon-align-bottom::before { + background: white; + box-shadow: 4px 0 0 white, 8px 0 0 white, 12px 0 0 white; +} + +.icon-distribute-h { + width: 20px; + height: 16px; + position: relative; +} + +.icon-distribute-h::before { + content: ""; + position: absolute; + top: 7px; + left: 0; + width: 4px; + height: 2px; + background: #4a5568; + border-radius: 1px; + box-shadow: 8px 0 0 #4a5568, 16px 0 0 #4a5568; +} + +.icon-distribute-h::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 1px; + height: 12px; + background: #4a5568; + border-radius: 0.5px; + box-shadow: 8px 0 0 #4a5568, 16px 0 0 #4a5568; +} + +.icon-btn.active .icon-distribute-h::before { + background: white; + box-shadow: 8px 0 0 white, 16px 0 0 white; +} + +.icon-btn.active .icon-distribute-h::after { + background: white; + box-shadow: 8px 0 0 white, 16px 0 0 white; +} + +.icon-distribute-v { + width: 16px; + height: 20px; + position: relative; +} + +.icon-distribute-v::before { + content: ""; + position: absolute; + top: 0; + left: 7px; + width: 2px; + height: 4px; + background: #4a5568; + border-radius: 1px; + box-shadow: 0 8px 0 #4a5568, 0 16px 0 #4a5568; +} + +.icon-distribute-v::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 12px; + height: 1px; + background: #4a5568; + border-radius: 0.5px; + box-shadow: 0 8px 0 #4a5568, 0 16px 0 #4a5568; +} + +.icon-btn.active .icon-distribute-v::before { + background: white; + box-shadow: 0 8px 0 white, 0 16px 0 white; +} + +.icon-btn.active .icon-distribute-v::after { + background: white; + box-shadow: 0 8px 0 white, 0 16px 0 white; +} + +.icon-auto-arrange { + width: 20px; + height: 20px; + position: relative; +} + +.icon-auto-arrange::before { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 6px; + height: 6px; + border: 2px solid #4a5568; + border-radius: 1px; + box-shadow: 10px 0 0 #4a5568, 0 10px 0 #4a5568, 10px 10px 0 #4a5568; +} + +.icon-btn.active .icon-auto-arrange::before { + border-color: white; + box-shadow: 10px 0 0 white, 0 10px 0 white, 10px 10px 0 white; +} + .canvas-container { flex: 1; position: relative;