最終更新 1785852451

Interactive Knowledge Graph Debugger

修正履歴 21ba7165072119de8c28dfae2b84889f585b20dd

Godot-Scenetree.md Raw
 ┖╴GraphNode
    ┠╴ColorRect
    ┃  ┖╴Label
MainGraphLoader.gd Raw
1extends Node2D
2
3const GRAPH_FILE_PATH = "res://network_graph3.json"
4var graph_node_scene: PackedScene = preload("res://GraphNode.tscn")
5#@onready var status_label: Label = $ColorRect/Label
6@onready var status_label: RichTextLabel = $ColorRect/Label
7var _stable_frames: int = 0
8
9# ==========================================
10# PHYSICS TUNING & MOBILE OPTIMIZATIONS
11# ==========================================
12const REPULSION_STRENGTH: float = 45000.0
13const SPRING_STRENGTH: float = 2.5
14const REST_LENGTH: float = 330.0
15const DAMPING: float = 0.95
16const MAX_SPEED: float = 500.0 # Mobile safety cap
17#const PHYSICS_SLEEP_THRESHOLD: float = 1.10 # Stops sim when stable (saves mobile CPU/battery)
18const PHYSICS_SLEEP_THRESHOLD: float = 250.0 # Avg force per node allowed before sleeping
19const STABLE_FRAME_REQUIREMENT: int = 60 # Must stay stable for ~1.5s (at 60fps) to sleep
20
21# State & Caching
22var spawned_nodes: Dictionary = {}
23var node_ids: Array[String] = []
24var relationships: Array = []
25var lines: Array[Line2D] = []
26var node_colors: Dictionary = {}
27
28var viewport_size: Vector2i = Vector2i.ZERO
29var physics_active: bool = true
30
31# FPS & Metrics Tracking
32var _last_metrics_update: float = 0.0
33const METRICS_UPDATE_INTERVAL: float = 0.2 # Update UI 5x/sec to prevent GC spikes
34var _total_node_count: int = 0
35var _total_edge_count: int = 0
36
37# ✅ NEW: Store benchmark results for the dashboard
38var _benchmark_read_ms: float = 0.0
39var _benchmark_parse_ms: float = 0.0
40
41# ==========================================
42# INITIALIZATION
43# ==========================================
44func _ready() -> void:
45 viewport_size = get_viewport_rect().size
46 get_window().size_changed.connect(_on_window_resized)
47
48 # FIX: Enable BBCode parsing so [color=...] tags render as actual colors
49 status_label.bbcode_enabled = true
50
51 var json_data: Dictionary = load_json_ld(GRAPH_FILE_PATH)
52 if json_data.is_empty():
53 push_warning("JSON-LD failed to load or is empty.")
54 return
55
56 var graph_array = json_data.get("@graph", [])
57 var nodes_map: Dictionary = {}
58
59 for item in graph_array:
60 if item is Dictionary and item.has("@id"):
61 nodes_map[item["@id"]] = item
62
63 build_graph_network(nodes_map)
64
65 _total_node_count = spawned_nodes.size()
66 _total_edge_count = relationships.size()
67
68 benchmark_json_ld()
69 print_rich("[color=cyan]Graph Loaded: ", str(_total_node_count), " nodes, ", str(_total_edge_count), " edges.[/color]")
70
71# ==========================================
72# CORE FUNCTIONS
73# ==========================================
74func _process(delta: float) -> void:
75 if physics_active:
76 _run_physics_step(min(delta, 0.03))
77
78 _update_lines()
79 _update_metrics(delta, Time.get_ticks_msec())
80
81# ==========================================
82# INPUT HANDLING (Mouse + Touch)
83# ==========================================
84func _unhandled_input(event: InputEvent) -> void:
85 var screen_pos = Vector2.ZERO
86 var is_click_like = false
87
88 if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
89 screen_pos = event.position
90 is_click_like = true
91 elif event is InputEventScreenTouch:
92 screen_pos = event.position
93 is_click_like = true
94
95 if not is_click_like:
96 return
97
98 if event.is_pressed():
99 _attempt_grab_node(screen_pos, true)
100 else:
101 _attempt_release_drag()
102
103func _on_window_resized() -> void:
104 viewport_size = get_viewport_rect().size
105
106# ==========================================
107# PHYSICS & UPDATE LOOPS
108# ==========================================
109func _run_physics_step(dt: float) -> void:
110 var total_force_magnitude: float = 0.0
111 var node_count = node_ids.size()
112
113 for i in range(node_count):
114 var id_a = node_ids[i]
115 var node_a = spawned_nodes[id_a]
116 if node_a.is_dragging: continue
117
118 var pos_a = node_a.position
119 var vel_a = node_a.velocity
120
121 for j in range(i + 1, node_count):
122 var id_b = node_ids[j]
123 var node_b = spawned_nodes[id_b]
124 if node_b.is_dragging: continue
125
126 var dir = pos_a - node_b.position
127 var dist_sq = dir.dot(dir)
128 if dist_sq < 1.0 or dist_sq > REPULSION_STRENGTH * 2.0: continue
129
130 var dist = sqrt(dist_sq)
131 var force = (REPULSION_STRENGTH / dist_sq) * dt
132 var f_vec = dir.normalized() * force
133
134 vel_a += f_vec
135 node_b.velocity -= f_vec if not node_b.is_dragging else Vector2.ZERO
136
137 total_force_magnitude += absf(force)
138
139 for rel in relationships:
140 var node_a = rel[0]
141 var node_b = rel[1]
142
143 var dir = node_b.position - node_a.position
144 var dist = dir.length()
145 if dist < 0.1: continue
146
147 var displacement = dist - REST_LENGTH
148 var force_vec = dir.normalized() * (SPRING_STRENGTH * displacement) * dt
149
150 if not node_a.is_dragging: node_a.velocity += force_vec
151 if not node_b.is_dragging: node_b.velocity -= force_vec
152
153 total_force_magnitude += absf(SPRING_STRENGTH * displacement)
154
155 for id in node_ids:
156 var node = spawned_nodes[id]
157 if node.is_dragging: continue
158
159 node.position += node.velocity * dt
160 node.velocity *= DAMPING
161
162 if node.velocity.length() > MAX_SPEED:
163 node.velocity = node.velocity.normalized() * MAX_SPEED
164
165 var gp = node.global_position
166 gp.x = clampf(gp.x, 50.0, float(viewport_size.x - 200))
167 gp.y = clampf(gp.y, 50.0, float(viewport_size.y - 100))
168 node.global_position = gp
169
170 # ==========================================
171 # STABILIZATION & AUTO-SLEEP LOGIC
172 # ==========================================
173 var avg_force_per_node = total_force_magnitude / max(node_count, 1.0)
174
175 if avg_force_per_node < PHYSICS_SLEEP_THRESHOLD:
176 _stable_frames += 1
177 else:
178 _stable_frames = 0
179 if _stable_frames >= STABLE_FRAME_REQUIREMENT:
180 physics_active = false
181
182
183
184
185func _update_lines() -> void:
186 for i in range(relationships.size()):
187 var line = lines[i]
188 var rel = relationships[i]
189 var offset = Vector2(75.0, 25.0)
190
191 line.points = [
192 Vector2(rel[0].position + offset),
193 Vector2(rel[1].position + offset)
194 ]
195
196# ✅ UPDATED: Injects benchmark data into the persistent dashboard
197func _update_metrics(_delta: float, current_time_ms: int) -> void:
198 if current_time_ms - _last_metrics_update < METRICS_UPDATE_INTERVAL * 1000.0:
199 return
200
201 var fps = Engine.get_frames_per_second()
202 var frame_time = 1000.0 / fps if fps > 0 else 0.0
203 var phys_color = "#7ee787" if physics_active else "#e74c3c"
204 var phys_status = "ACTIVE" if physics_active else "SLEEPING"
205
206 status_label.text = ("[color=#7ee787]▮ MOBILE DASHBOARD[/color]\n" +
207 "FPS: [color=cyan]%d[/color] | Frame: [color=orange]%.2f ms[/color]\n" +
208 "Nodes: %d | Edges: %d\n" +
209 "Physics: [color=%s]%s[/color]\n" +
210 "[color=#ffdd57]📊 BENCHMARK:[/color] Read: %.1f ms | Parse: %.1f ms") \
211 % [fps, frame_time, _total_node_count, _total_edge_count,
212 phys_color, phys_status, _benchmark_read_ms, _benchmark_parse_ms]
213
214
215 _last_metrics_update = current_time_ms
216
217# ==========================================
218# DRAG SYSTEM
219# ==========================================
220func _attempt_grab_node(screen_pos: Vector2, press: bool) -> void:
221 if not press: return
222
223 var best_dist: float = 50.0
224 var grabbed_id: String = ""
225
226 for id in node_ids:
227 var node = spawned_nodes[id]
228 var dist = (node.position - screen_pos).length()
229 if dist < best_dist:
230 best_dist = dist
231 grabbed_id = id
232
233 if not grabbed_id.is_empty():
234 spawned_nodes[grabbed_id].is_dragging = true
235
236#func _attempt_release_drag() -> void:
237 #for node in spawned_nodes.values():
238 #if node.is_dragging:
239 #node.is_dragging = false
240 #physics_active = true
241func _attempt_release_drag() -> void:
242 for node in spawned_nodes.values():
243 if node.is_dragging:
244 node.is_dragging = false
245 physics_active = true
246 _stable_frames = 0 # ✅ Reset stabilization counter on interaction
247
248# ==========================================
249# BUILDERS & UTILITIES
250# ==========================================
251func build_graph_network(nodes_map: Dictionary) -> void:
252 var keys = nodes_map.keys()
253 var total_nodes = keys.size()
254 var center = get_viewport_rect().size * 0.5
255
256 for i in range(total_nodes):
257 var node_id = keys[i]
258 var node_data = nodes_map[node_id]
259 var new_visual_node = graph_node_scene.instantiate()
260
261 if not new_visual_node.has_method("setup"):
262 new_visual_node.set_script(preload("res://GraphNode.gd"))
263
264 add_child(new_visual_node)
265
266 var angle = (TAU / total_nodes) * i
267 var radius = 400.0
268 new_visual_node.position = center + Vector2.from_angle(angle) * radius
269 new_visual_node.position += Vector2(randf_range(-50, 50), randf_range(-50, 50))
270 new_visual_node.setup(node_id, node_data.get("name", "Unknown"))
271
272 spawned_nodes[node_id] = new_visual_node
273 node_ids.append(node_id)
274 node_colors[node_id] = _get_unique_color(node_id)
275
276 for node_id in keys:
277 var node_data = nodes_map[node_id]
278 if not node_data.has("knows"): continue
279
280 var raw_connections = node_data["knows"]
281 var connection_list: Array = raw_connections if raw_connections is Array else [raw_connections]
282
283 for conn in connection_list:
284 if conn is Dictionary and conn.has("@id") and spawned_nodes.has(conn["@id"]):
285 relationships.append([spawned_nodes[node_id], spawned_nodes[conn["@id"]]])
286
287 var line = Line2D.new()
288 line.width = 5.0
289 line.z_index = -1
290
291 var grad = Gradient.new()
292 grad.set_color(0, node_colors[node_id])
293 grad.set_color(1, node_colors[conn["@id"]])
294 line.gradient = grad
295
296 add_child(line)
297 lines.append(line)
298
299func _get_unique_color(id_string: String) -> Color:
300 var h = hash(id_string)
301 return Color8(
302 max(abs(h) % 256, 128),
303 max((abs(h) >> 8) % 256, 128),
304 max((abs(h) >> 16) % 256, 128)
305 )
306
307func load_json_ld(path: String) -> Dictionary:
308 if not FileAccess.file_exists(path):
309 push_error("JSON-LD file not found at: " + path)
310 return {}
311
312 var file = FileAccess.open(path, FileAccess.READ)
313 var json_string = file.get_as_text()
314 file.close()
315 var parsed_data = JSON.parse_string(json_string)
316 return parsed_data if parsed_data is Dictionary else {}
317
318# ✅ UPDATED: Saves timing data to class variables instead of just printing
319func benchmark_json_ld() -> void:
320 if not FileAccess.file_exists(GRAPH_FILE_PATH):
321 return
322
323 var start_time = Time.get_ticks_msec()
324 var file = FileAccess.open(GRAPH_FILE_PATH, FileAccess.READ)
325 var json_string = file.get_as_text()
326 file.close()
327 _benchmark_read_ms = float(Time.get_ticks_msec() - start_time)
328
329 start_time = Time.get_ticks_msec()
330 var _parsed = JSON.parse_string(json_string)
331 _benchmark_parse_ms = float(Time.get_ticks_msec() - start_time)
332
333 #print_rich("[color=yellow]📊 BENCHMARK: Read: %.1f ms | Parse: %.1f ms[/color]" % [_benchmark_read_ms, _benchmark_parse_ms])
334 print_rich("[color=yellow]📊 BENCHMARK: Read: {read} ms | Parse: {parse} [/color]".format({
335 "read": "%0.1f" % _benchmark_read_ms,
336 "parse": "%0.1f" % _benchmark_parse_ms
337 }))
338