| … | … | @@ -88,6 +88,7 @@ func set_graph_data(nodes: Array, edges: Array, clusters: Array, cluster_labels: |
| 88 | 88 | _init_cluster_positions(clusters) |
| 89 | 89 | for i: int in LAYOUT_ITERATIONS: |
| 90 | 90 | _layout_step() |
| 91 | _spread_overlapping() |
| 91 | 92 | _rescale_to_fit() |
| 92 | 93 | queue_redraw() |
| 93 | 94 | |
| … | … | @@ -240,6 +241,34 @@ func _layout_step() -> void: |
| 240 | 241 | _positions[nid] += velocities[nid] |
| 241 | 242 | |
| 242 | 243 | |
| 244 | func _spread_overlapping() -> void: |
| 245 | ## Push apart nodes that are too close, so labels don't overlap. |
| 246 | ## Minimum distance = enough for a label (~80px at zoom 1.0). |
| 247 | var min_dist: float = 50.0 |
| 248 | var node_ids: Array = _positions.keys() |
| 249 | # Run a few passes to resolve cascading overlaps |
| 250 | for _pass: int in 5: |
| 251 | var moved: bool = false |
| 252 | for i: int in node_ids.size(): |
| 253 | for j: int in range(i + 1, node_ids.size()): |
| 254 | var a: String = node_ids[i] |
| 255 | var b: String = node_ids[j] |
| 256 | var delta: Vector2 = _positions[a] - _positions[b] |
| 257 | var dist: float = delta.length() |
| 258 | if dist < min_dist and dist > 0.01: |
| 259 | var push: Vector2 = delta.normalized() * (min_dist - dist) * 0.5 |
| 260 | _positions[a] += push |
| 261 | _positions[b] -= push |
| 262 | moved = true |
| 263 | elif dist < 0.01: |
| 264 | # Exactly overlapping — nudge apart |
| 265 | _positions[a] += Vector2(min_dist * 0.5, 0) |
| 266 | _positions[b] -= Vector2(min_dist * 0.5, 0) |
| 267 | moved = true |
| 268 | if not moved: |
| 269 | break |
| 270 | |
| 271 | |
| 243 | 272 | func _rescale_to_fit() -> void: |
| 244 | 273 | ## After layout, rescale all positions to fit within the viewport with padding. |
| 245 | 274 | if _positions.is_empty(): |