OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet commits / 642d678

Label-aware node spacing: use actual text widths to prevent overlap
Replace fixed 80px spread distance with AABB overlap detection using each node's actual label width from font metrics. Pushes nodes apart along the axis with least overlap (horizontal for side-by-side labels, vertical for stacked ones). Runs up to 10 passes to resolve cascading overlaps. Reverts test label. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
642d678e3cb6b22458f6fb8de2d468f1d7918618
by plankatron <fleeky@gmail.com> · 2026-02-23T12:56:03.000Z

Changed files (2) +46 -17

scripts/graph_canvas.gd +45 -16
……@@ -269,28 +269,57 @@ func _layout_step() -> void:
269269
270270
271271func _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.
275274 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:
278292 var moved: bool = false
279293 for i: int in node_ids.size():
280294 for j: int in range(i + 1, node_ids.size()):
281295 var a: String = node_ids[i]
282296 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
294323 moved = true
295324 if not moved:
296325 break
scripts/graph_panel.gd +1 -1
……@@ -59,7 +59,7 @@ func _ready() -> void:
5959 algo_btn.selected = 0
6060 algo_btn.item_selected.connect(_on_algo_selected)
6161
62 title_label.text = "Graph v2"
62 title_label.text = "Graph"
6363
6464 # Initial fetch
6565 _fetch_all()