OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet commits / 7ddca2c

Fix graph rendering: adaptive edges, spacing, zoom reset, font metrics
- Grid layout: fixed minimum cell size (90x70px) instead of proportional width that caused nodes to bunch into 60px strips - Adaptive edge rendering: filter low-MI edges and reduce alpha when dense (>50 edges), prevents "white soup" overlay - Reset zoom/pan on algorithm switch so new layout is always visible - Use font metrics (get_string_size) for label centering instead of character-count estimation - Cluster labels at top of bounding box instead of centroid (no overlap) - Brighten unclustered node color (0.75 vs 0.6) for dark bg contrast - Increase spread distance to 80px, scale cap to 3.0 - Fix SyncDialog lambda → explicit method (prevents signal errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7ddca2c37fa03e56ecaf302769c93cb0ab035b0f
by plankatron <fleeky@gmail.com> · 2026-02-23T03:56:55.000Z

Changed files (2) +58 -33

scripts/graph_canvas.gd +53 -32
……@@ -38,7 +38,7 @@ const CLUSTER_COLORS: Array[Color] = [
3838 Color("#e67e22"), Color("#5865f2"), Color("#0dbd8b"), Color("#e74c3c"),
3939 Color("#f1c40f"), Color("#9b59b6"), Color("#1abc9c"), Color("#3498db")
4040]
41const COLOR_UNCLUSTERED := Color(0.6, 0.65, 0.7)
41const COLOR_UNCLUSTERED := Color(0.75, 0.8, 0.85)
4242const COLOR_EDGE := Color(1.0, 1.0, 1.0)
4343const COLOR_BG := Color(0.08, 0.08, 0.1)
4444
……@@ -58,6 +58,11 @@ func set_graph_data(nodes: Array, edges: Array, clusters: Array, cluster_labels:
5858 _title_map.clear()
5959 _cluster_labels = cluster_labels
6060
61 # Reset zoom/pan so new layout is fully visible
62 _zoom = 1.0
63 _pan = Vector2.ZERO
64 _hovered_node = ""
65
6166 # Build cluster membership: room_id -> cluster_id
6267 for ci: int in clusters.size():
6368 var cluster: Variant = clusters[ci]
……@@ -127,23 +132,23 @@ func _grid_layout() -> void:
127132 if groups.is_empty():
128133 return
129134
130 # Layout groups in a horizontal strip, each group gets proportional width
131 var total_nodes: int = _nodes.size()
135 # Layout groups with fixed minimum cell size so nodes never bunch up.
136 # Each group gets enough space for its nodes at min_cell_w apart.
132137 var padding: float = 40.0
133 var group_gap: float = 30.0
134 var avail_w: float = rect.x - padding * 2 - group_gap * float(groups.size() - 1)
138 var group_gap: float = 40.0
139 var min_cell_w: float = 90.0 # minimum horizontal spacing between nodes
140 var min_cell_h: float = 70.0 # minimum vertical spacing between nodes
135141 var avail_h: float = rect.y - padding * 2
136142
137143 var x_offset: float = padding
138144 for g: Dictionary in groups:
139145 var ids: Array = g["ids"]
140146 var n: int = ids.size()
141 # Width proportional to number of nodes (min width for small groups)
142 var group_w: float = maxf(avail_w * (float(n) / float(total_nodes)), 60.0)
143147 var cols: int = ceili(sqrt(float(n)))
144148 var rows: int = ceili(float(n) / float(cols))
145 var cell_w: float = group_w / float(cols + 1)
146 var cell_h: float = avail_h / float(rows + 1)
149 var cell_w: float = maxf(min_cell_w, avail_h / float(rows + 1))
150 var cell_h: float = maxf(min_cell_h, avail_h / float(rows + 1))
151 var group_w: float = cell_w * float(cols + 1)
147152
148153 for i: int in n:
149154 var nid: String = ids[i]
……@@ -244,7 +249,7 @@ func _layout_step() -> void:
244249func _spread_overlapping() -> void:
245250 ## Push apart nodes that are too close, so labels don't overlap.
246251 ## Minimum distance = enough for a label (~80px at zoom 1.0).
247 var min_dist: float = 50.0
252 var min_dist: float = 80.0
248253 var node_ids: Array = _positions.keys()
249254 # Run a few passes to resolve cascading overlaps
250255 for _pass: int in 5:
……@@ -294,8 +299,8 @@ func _rescale_to_fit() -> void:
294299
295300 var available: Vector2 = rect - Vector2(padding * 2, padding * 2)
296301 var scale_factor: float = minf(available.x / extent.x, available.y / extent.y)
297 # Don't scale up if layout is already small
298 scale_factor = minf(scale_factor, 2.0)
302 # Don't scale up excessively
303 scale_factor = minf(scale_factor, 3.0)
299304
300305 var center_of_nodes: Vector2 = (min_pos + max_pos) / 2.0
301306 var viewport_center: Vector2 = rect / 2.0
……@@ -315,17 +320,30 @@ func _draw() -> void:
315320 return
316321
317322 # Draw edges — daemon uses {from, to, mi}
323 # Adaptive rendering: reduce alpha and filter low-MI edges when dense
324 var edge_count: int = _edges.size()
325 var mi_floor: float = 0.0 # skip edges below this MI
326 var alpha_scale: float = 1.0
327 if edge_count > 100:
328 mi_floor = 0.3
329 alpha_scale = 0.5
330 elif edge_count > 50:
331 mi_floor = 0.1
332 alpha_scale = 0.7
333
318334 for edge: Variant in _edges:
319335 if not edge is Dictionary:
320336 continue
321337 var src: String = str(edge.get("from", ""))
322338 var tgt: String = str(edge.get("to", ""))
323339 var mi: float = float(edge.get("mi", 1.0))
340 if mi < mi_floor:
341 continue
324342 if not _positions.has(src) or not _positions.has(tgt):
325343 continue
326344 var p1: Vector2 = _world_to_screen(_positions[src])
327345 var p2: Vector2 = _world_to_screen(_positions[tgt])
328 var alpha: float = clampf(mi / 3.0, 0.15, 0.7)
346 var alpha: float = clampf(mi / 3.0 * alpha_scale, 0.05, 0.6)
329347 var width: float = clampf(mi, 0.5, 3.0) * _zoom
330348 draw_line(p1, p2, Color(COLOR_EDGE, alpha), width)
331349
……@@ -350,46 +368,49 @@ func _draw() -> void:
350368
351369 draw_circle(pos, radius, color)
352370
353 # Label below node
371 # Label below node — centered using font metrics
354372 var title: String = _title_map.get(nid, nid.left(8))
355373 if title.length() > MAX_LABEL:
356374 title = title.left(MAX_LABEL - 1) + "..."
357 var label_pos: Vector2 = pos + Vector2(-title.length() * 2.5 * _zoom, (radius + 12.0 * _zoom))
358375 var font_size: int = int(LABEL_SIZE * _zoom)
359376 if font_size >= 6:
360 draw_string(_font, label_pos, title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(0.8, 0.8, 0.8))
377 var text_w: float = _font.get_string_size(title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
378 var label_pos: Vector2 = pos + Vector2(-text_w * 0.5, radius + 14.0 * _zoom)
379 draw_string(_font, label_pos, title, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color(0.85, 0.85, 0.85))
361380
362 # Draw cluster labels at centroids
381 # Draw cluster labels above cluster bounding box (not centroid — avoids node collision)
363382 if not _cluster_labels.is_empty():
364 # Collect node positions per cluster
365 var cluster_positions: Dictionary = {} # cluster_index -> Array[Vector2]
383 # Collect node positions per cluster and find top-center of bounding box
384 var cluster_bounds: Dictionary = {} # cluster_index -> {min_x, max_x, min_y}
366385 for nid: String in _cluster_map.keys():
367386 var ci: int = _cluster_map[nid]
368387 if not _positions.has(nid):
369388 continue
370 if not cluster_positions.has(ci):
371 cluster_positions[ci] = []
372 cluster_positions[ci].append(_positions[nid])
389 var p: Vector2 = _positions[nid]
390 if not cluster_bounds.has(ci):
391 cluster_bounds[ci] = {"min_x": p.x, "max_x": p.x, "min_y": p.y}
392 else:
393 var b: Dictionary = cluster_bounds[ci]
394 b["min_x"] = minf(b["min_x"], p.x)
395 b["max_x"] = maxf(b["max_x"], p.x)
396 b["min_y"] = minf(b["min_y"], p.y)
373397
374 for ci: int in cluster_positions.keys():
398 for ci: int in cluster_bounds.keys():
375399 if not _cluster_labels.has(ci):
376400 continue
377401 var meta: Dictionary = _cluster_labels[ci]
378402 var label_text: String = str(meta.get("label", ""))
379403 if label_text == "":
380404 continue
381 # Compute centroid
382 var pts: Array = cluster_positions[ci]
383 var centroid := Vector2.ZERO
384 for p: Vector2 in pts:
385 centroid += p
386 centroid /= float(pts.size())
387 var screen_pos: Vector2 = _world_to_screen(centroid)
405 var b: Dictionary = cluster_bounds[ci]
406 # Position label at top-center of cluster, above all nodes
407 var top_center := Vector2((b["min_x"] + b["max_x"]) / 2.0, b["min_y"])
408 var screen_pos: Vector2 = _world_to_screen(top_center)
388409 var color: Color = CLUSTER_COLORS[ci % CLUSTER_COLORS.size()]
389410 var font_size: int = int(14.0 * _zoom)
390411 if font_size >= 8:
391 # Draw label above the centroid
392 var lbl_pos: Vector2 = screen_pos - Vector2(label_text.length() * 3.5 * _zoom, 20.0 * _zoom)
412 var text_w: float = _font.get_string_size(label_text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
413 var lbl_pos: Vector2 = screen_pos - Vector2(text_w * 0.5, 24.0 * _zoom)
393414 draw_string(_font, lbl_pos, label_text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, color.lightened(0.4))
394415
395416 # Tooltip for hovered node
scripts/graph_panel.gd +5 -1
……@@ -50,7 +50,7 @@ func _ready() -> void:
5050 sync_btn.pressed.connect(_on_sync_pressed)
5151 copy_btn.pressed.connect(_on_copy_key)
5252 join_btn.pressed.connect(_on_join_meta)
53 sync_dialog.close_requested.connect(func(): sync_dialog.hide())
53 sync_dialog.close_requested.connect(_on_sync_dialog_close)
5454
5555 # Populate algo selector
5656 algo_btn.add_item("Default", 0)
……@@ -187,6 +187,10 @@ func _on_room_clicked(room_id: String) -> void:
187187 KeetService.select_room(room_id)
188188
189189
190func _on_sync_dialog_close() -> void:
191 sync_dialog.hide()
192
193
190194# --- MetadataBase Sync ---
191195
192196func _on_sync_pressed() -> void: