147 lines
No EOL
5.2 KiB
GDScript
147 lines
No EOL
5.2 KiB
GDScript
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 |