| … | … | @@ -269,28 +269,57 @@ func _layout_step() -> void: |
| 269 | 269 | |
| 270 | 270 | |
| 271 | 271 | func _spread_overlapping() -> void: |
| 272 | | ## Push apart nodes that are too close, so labels don't overlap. |
| 273 | | ## Minimum distance = enough for a label (~80px at zoom 1.0). |
| 274 | | var min_dist: float = 80.0 |
| 272 | ## Push apart nodes whose labels would overlap. |
| 273 | ## Uses actual text width per node to compute bounding rects. |
| 275 | 274 | var node_ids: Array = _positions.keys() |
| 276 | | # Run a few passes to resolve cascading overlaps |
| 277 | | for _pass: int in 5: |
| 275 | if node_ids.size() < 2: |
| 276 | return |
| 277 | |
| 278 | # Pre-compute label half-widths and vertical extent for each node |
| 279 | var half_w: Dictionary = {} # nid -> float (half of label width + node radius) |
| 280 | var half_h: float = NODE_RADIUS + 14.0 + float(LABEL_SIZE) # node + gap + text height |
| 281 | for nid: String in node_ids: |
| 282 | var title: String = _title_map.get(nid, nid.left(8)) |
| 283 | if title.length() > MAX_LABEL: |
| 284 | title = title.left(MAX_LABEL - 1) + "..." |
| 285 | var text_w: float = 0.0 |
| 286 | if _font != null: |
| 287 | text_w = _font.get_string_size(title, HORIZONTAL_ALIGNMENT_LEFT, -1, LABEL_SIZE).x |
| 288 | half_w[nid] = maxf(text_w * 0.5, NODE_RADIUS) + 8.0 # 8px padding |
| 289 | |
| 290 | # Run passes to resolve overlaps using axis-aligned bounding box overlap |
| 291 | for _pass: int in 10: |
| 278 | 292 | var moved: bool = false |
| 279 | 293 | for i: int in node_ids.size(): |
| 280 | 294 | for j: int in range(i + 1, node_ids.size()): |
| 281 | 295 | var a: String = node_ids[i] |
| 282 | 296 | var b: String = node_ids[j] |
| 283 | | var delta: Vector2 = _positions[a] - _positions[b] |
| 284 | | var dist: float = delta.length() |
| 285 | | if dist < min_dist and dist > 0.01: |
| 286 | | var push: Vector2 = delta.normalized() * (min_dist - dist) * 0.5 |
| 287 | | _positions[a] += push |
| 288 | | _positions[b] -= push |
| 289 | | moved = true |
| 290 | | elif dist < 0.01: |
| 291 | | # Exactly overlapping — nudge apart |
| 292 | | _positions[a] += Vector2(min_dist * 0.5, 0) |
| 293 | | _positions[b] -= Vector2(min_dist * 0.5, 0) |
| 297 | var pa: Vector2 = _positions[a] |
| 298 | var pb: Vector2 = _positions[b] |
| 299 | # Required separation on each axis to avoid overlap |
| 300 | var sep_x: float = half_w[a] + half_w[b] |
| 301 | var sep_y: float = half_h |
| 302 | var dx: float = absf(pa.x - pb.x) |
| 303 | var dy: float = absf(pa.y - pb.y) |
| 304 | # Check if bounding boxes overlap |
| 305 | if dx < sep_x and dy < sep_y: |
| 306 | var delta: Vector2 = pa - pb |
| 307 | if delta.length() < 0.01: |
| 308 | # Exactly overlapping — nudge horizontally |
| 309 | _positions[a] += Vector2(sep_x * 0.5, 0) |
| 310 | _positions[b] -= Vector2(sep_x * 0.5, 0) |
| 311 | else: |
| 312 | # Push apart along the axis with less overlap |
| 313 | var overlap_x: float = sep_x - dx |
| 314 | var overlap_y: float = sep_y - dy |
| 315 | if overlap_x < overlap_y: |
| 316 | var sign_x: float = 1.0 if pa.x >= pb.x else -1.0 |
| 317 | _positions[a].x += overlap_x * 0.5 * sign_x |
| 318 | _positions[b].x -= overlap_x * 0.5 * sign_x |
| 319 | else: |
| 320 | var sign_y: float = 1.0 if pa.y >= pb.y else -1.0 |
| 321 | _positions[a].y += overlap_y * 0.5 * sign_y |
| 322 | _positions[b].y -= overlap_y * 0.5 * sign_y |
| 294 | 323 | moved = true |
| 295 | 324 | if not moved: |
| 296 | 325 | break |