Naposledy aktivní 1785852451

Interactive Knowledge Graph Debugger

Revize fca92ad58a0ef26e76109f1822895d65d479864d

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
Scenetree.md Raw
 ┖╴GraphNode
    ┠╴ColorRect
    ┃  ┖╴Label
network_graph3.json Raw
1{
2 "@context": "https://schema.org",
3 "@graph": [
4 {
5 "@id": "#alice_chen",
6 "name": "Alice Chen\nCEO",
7 "jobTitle": "Chief Executive Officer",
8 "knows": [
9 {"@id": "#bob_smith"}, {"@id": "#carol_white"}, {"@id": "#dave_brown"}
10 ]
11 },
12 {
13 "@id": "#bob_smith",
14 "name": "Bob Smith\nCTO",
15 "jobTitle": "Chief Technology Officer",
16 "knows": [
17 {"@id": "#alice_chen"}, {"@id": "#eve_johnson"}, {"@id": "#frank_miller"}, {"@id": "#george_davis"}
18 ]
19 },
20 {
21 "@id": "#carol_white",
22 "name": "Carol White\nCFO",
23 "jobTitle": "Chief Financial Officer",
24 "knows": [
25 {"@id": "#alice_chen"}, {"@id": "#dave_brown"}, {"@id": "#henry_wilson"}
26 ]
27 },
28 {
29 "@id": "#dave_brown",
30 "name": "Dave Brown\nCOO",
31 "jobTitle": "Chief Operating Officer",
32 "knows": [
33 {"@id": "#alice_chen"}, {"@id": "#henry_wilson"}, {"@id": "#frank_miller"}
34 ]
35 },
36 {
37 "@id": "#eve_johnson",
38 "name": "Eve Johnson\nVP Engineering",
39 "jobTitle": "Vice President of Engineering",
40 "knows": [
41 {"@id": "#bob_smith"}, {"@id": "#jack_anderson"}, {"@id": "#kelly_thomas"}, {"@id": "#liam_martin"}
42 ]
43 },
44 {
45 "@id": "#frank_miller",
46 "name": "Frank Miller\nVP Sales",
47 "jobTitle": "Vice President of Sales",
48 "knows": [
49 {"@id": "#dave_brown"}, {"@id": "#nora_lee"}, {"@id": "#oscar_clark"}, {"@id": "#mia_garcia"}
50 ]
51 },
52 {
53 "@id": "#george_davis",
54 "name": "George Davis\nVP Product",
55 "jobTitle": "Vice President of Product",
56 "knows": [
57 {"@id": "#bob_smith"}, {"@id": "#mia_garcia"}, {"@id": "#liam_martin"}, {"@id": "#paul_lewis"}
58 ]
59 },
60 {
61 "@id": "#henry_wilson",
62 "name": "Henry Wilson\nHead of HR",
63 "jobTitle": "Human Resources Director",
64 "knows": [
65 {"@id": "#carol_white"}, {"@id": "#quinn_adams"}, {"@id": "#rachel_king"}
66 ]
67 },
68 {
69 "@id": "#jack_anderson",
70 "name": "Jack Anderson\nLead Dev",
71 "jobTitle": "Senior Software Engineer",
72 "knows": [
73 {"@id": "#eve_johnson"}, {"@id": "#kelly_thomas"}, {"@id": "#paul_lewis"}, {"@id": "#liam_martin"}
74 ]
75 },
76 {
77 "@id": "#kelly_thomas",
78 "name": "Kelly Thomas\nQA Lead",
79 "jobTitle": "Quality Assurance Manager",
80 "knows": [
81 {"@id": "#eve_johnson"}, {"@id": "#jack_anderson"}, {"@id": "#quinn_adams"}
82 ]
83 },
84 {
85 "@id": "#liam_martin",
86 "name": "Liam Martin\nProduct Manager",
87 "jobTitle": "Senior Product Manager",
88 "knows": [
89 {"@id": "#eve_johnson"}, {"@id": "#george_davis"}, {"@id": "#jack_anderson"}, {"@id": "#mia_garcia"}
90 ]
91 },
92 {
93 "@id": "#mia_garcia",
94 "name": "Mia Garcia\nUX Designer",
95 "jobTitle": "Lead UX/UI Designer",
96 "knows": [
97 {"@id": "#frank_miller"}, {"@id": "#george_davis"}, {"@id": "#liam_martin"}
98 ]
99 },
100 {
101 "@id": "#nora_lee",
102 "name": "Nora Lee\nSales Rep",
103 "jobTitle": "Enterprise Sales Account Executive",
104 "knows": [
105 {"@id": "#frank_miller"}, {"@id": "#oscar_clark"}
106 ]
107 },
108 {
109 "@id": "#oscar_clark",
110 "name": "Oscar Clark\nSales Rep",
111 "jobTitle": "Account Manager",
112 "knows": [
113 {"@id": "#frank_miller"}, {"@id": "#nora_lee"}
114 ]
115 },
116 {
117 "@id": "#paul_lewis",
118 "name": "Paul Lewis\nData Scientist",
119 "jobTitle": "Staff Data Scientist",
120 "knows": [
121 {"@id": "#george_davis"}, {"@id": "#jack_anderson"}, {"@id": "#eve_johnson"}
122 ]
123 },
124 {
125 "@id": "#quinn_adams",
126 "name": "Quinn Adams\nIntern",
127 "jobTitle": "Engineering Intern",
128 "knows": [
129 {"@id": "#henry_wilson"}, {"@id": "#kelly_thomas"}
130 ]
131 },
132 {
133 "@id": "#rachel_king",
134 "name": "Rachel King\nIntern",
135 "jobTitle": "Marketing Intern",
136 "knows": [
137 {"@id": "#henry_wilson"}, {"@id": "#frank_miller"}
138 ]
139 },
140 {
141 "@id": "#steve_ross",
142 "name": "Steve Ross\nBackend Dev",
143 "jobTitle": "Software Engineer II",
144 "knows": [
145 {"@id": "#jack_anderson"}, {"@id": "#paul_lewis"}
146 ]
147 },
148 {
149 "@id": "#tina_turner",
150 "name": "Tina Turner\nFrontend Dev",
151 "jobTitle": "Software Engineer II",
152 "knows": [
153 {"@id": "#jack_anderson"}, {"@id": "#mia_garcia"}
154 ]
155 },
156 {
157 "@id": "#ursula_march",
158 "name": "Ursula March\nAccountant",
159 "jobTitle": "Senior Accountant",
160 "knows": [
161 {"@id": "#carol_white"}
162 ]
163 },
164 {
165 "@id": "#victor_stone",
166 "name": "Victor Stone\nRecruiter",
167 "jobTitle": "Technical Recruiter",
168 "knows": [
169 {"@id": "#henry_wilson"}, {"@id": "#eve_johnson"}
170 ]
171 },
172 {
173 "@id": "#wendy_wilson",
174 "name": "Wendy Wilson\nOffice Mgr",
175 "jobTitle": "Office Manager",
176 "knows": [
177 {"@id": "#henry_wilson"}, {"@id": "#dave_brown"}
178 ]
179 },
180 {
181 "@id": "#xavier_mercer",
182 "name": "Xavier Mercer\nSecurity",
183 "jobTitle": "IT Security Specialist",
184 "knows": [
185 {"@id": "#bob_smith"}, {"@id": "#eve_johnson"}
186 ]
187 },
188 {
189 "@id": "#yara_helmy",
190 "name": "Yara Helmy\nCompliance",
191 "jobTitle": "Legal & Compliance Officer",
192 "knows": [
193 {"@id": "#carol_white"}, {"@id": "#alice_chen"}
194 ]
195 },
196 {
197 "@id": "#zack_berry",
198 "name": "Zack Berry\nSupport Lead",
199 "jobTitle": "Customer Success Manager",
200 "knows": [
201 {"@id": "#frank_miller"}, {"@id": "#eve_johnson"}, {"@id": "#liam_martin"}
202 ]
203 },
204 {
205 "@id": "#amy_pond",
206 "name": "Amy Pond\nSupport",
207 "jobTitle": "Tier 2 Support Specialist",
208 "knows": [
209 {"@id": "#zack_berry"}, {"@id": "#jack_anderson"}
210 ]
211 },
212 {
213 "@id": "#brian_cox",
214 "name": "Brian Cox\nDevOps",
215 "jobTitle": "Infrastructure Engineer",
216 "knows": [
217 {"@id": "#eve_johnson"}, {"@id": "#xavier_mercer"}
218 ]
219 },
220 {
221 "@id": "#chloe_law",
222 "name": "Chloe Law\nLegal",
223 "jobTitle": "General Counsel",
224 "knows": [
225 {"@id": "#yara_helmy"}, {"@id": "#alice_chen"}, {"@id": "#carol_white"}
226 ]
227 },
228 {
229 "@id": "#derek_ryusaki",
230 "name": "Derek Ryusaki\nArchitect",
231 "jobTitle": "Solutions Architect",
232 "knows": [
233 {"@id": "#bob_smith"}, {"@id": "#jack_anderson"}, {"@id": "#liam_martin"}
234 ]
235 },
236 {
237 "@id": "#elaine_benese",
238 "name": "Elaine Benese\nAdmin",
239 "jobTitle": "Executive Assistant",
240 "knows": [
241 {"@id": "#alice_chen"}, {"@id": "#wendy_wilson"}
242 ]
243 }
244 ]
245}
246