extends RefCounted class_name NodeLayoutHelper # Configuration var map_width: int = 6 # Available horizontal positions (0-5) var level_height: float = 1.0 # Vertical spacing between levels var node_spread: float = 0.7 # How much to spread nodes horizontally (0-1) var path_smoothness: float = 0.3 # How much to align nodes with their connections (0-1) # Internal var _rng = RandomNumberGenerator.new() func _init(seed_value: int = 0): if seed_value != 0: _rng.seed = seed_value else: _rng.randomize() # Organize node positions for better visual layout func organize_layout(nodes: Array, connections: Array) -> Array: # Clone the nodes so we don't modify the original array var updated_nodes = [] for node in nodes: updated_nodes.append(node.duplicate()) # Group nodes by level var nodes_by_level = {} for node in updated_nodes: var level = node.level if not nodes_by_level.has(level): nodes_by_level[level] = [] nodes_by_level[level].append(node) # Process each level for level in nodes_by_level: var level_nodes = nodes_by_level[level] # Skip levels with fixed positions if level == 0 or level == nodes_by_level.size() - 1: continue # Calculate optimal positions based on connections for node in level_nodes: _adjust_node_position(node, updated_nodes, connections, nodes_by_level) # Resolve overlaps after initial placement for level in nodes_by_level: var level_nodes = nodes_by_level[level] if level_nodes.size() > 1: _resolve_horizontal_overlaps(level_nodes) return updated_nodes # Adjust node position based on its connections func _adjust_node_position(node, all_nodes, connections, nodes_by_level): # Find all connections to/from this node var connected_from = [] # Nodes in the previous level connecting to this node var connected_to = [] # Nodes in the next level this node connects to for conn in connections: if conn.to == node.id: var from_node = _find_node_by_id(all_nodes, conn.from) if from_node and from_node.level == node.level - 1: connected_from.append(from_node) if conn.from == node.id: var to_node = _find_node_by_id(all_nodes, conn.to) if to_node and to_node.level == node.level + 1: connected_to.append(to_node) # Calculate optimal x position based on connected nodes var optimal_x = node.position.x # Start with current position if connected_from.size() > 0 or connected_to.size() > 0: var avg_x = 0.0 var total_connected = 0 # Consider positions of connected nodes for from_node in connected_from: avg_x += from_node.position.x total_connected += 1 for to_node in connected_to: avg_x += to_node.position.x total_connected += 1 if total_connected > 0: avg_x /= total_connected # Blend between current position and optimal position optimal_x = lerp(node.position.x, avg_x, path_smoothness) # Add some random variation to prevent everything aligning too perfectly optimal_x += (_rng.randf() - 0.5) * node_spread # Keep within bounds optimal_x = clamp(optimal_x, 0, map_width) # Update node position node.position.x = optimal_x # Y position is determined by level node.position.y = node.level * level_height # Resolve horizontal overlaps between nodes on the same level func _resolve_horizontal_overlaps(level_nodes): # Sort nodes by x position level_nodes.sort_custom(func(a, b): return a.position.x < b.position.x) # Minimum distance between nodes var min_distance = 1.0 # Check for overlaps and adjust for i in range(1, level_nodes.size()): var prev_node = level_nodes[i-1] var curr_node = level_nodes[i] if curr_node.position.x - prev_node.position.x < min_distance: # If too close, move them apart var midpoint = (prev_node.position.x + curr_node.position.x) / 2 var half_distance = min_distance / 2 # Try to maintain relative positions prev_node.position.x = midpoint - half_distance curr_node.position.x = midpoint + half_distance # Ensure we stay within bounds if prev_node.position.x < 0: var shift = -prev_node.position.x prev_node.position.x += shift curr_node.position.x += shift if curr_node.position.x > map_width: var shift = curr_node.position.x - map_width prev_node.position.x -= shift curr_node.position.x -= shift # Final boundary check for previous node prev_node.position.x = max(0, prev_node.position.x) # Find a node by its ID func _find_node_by_id(nodes, id): for node in nodes: if node.id == id: return node return null