┖╴GraphNode
┠╴ColorRect
┃ ┖╴Label
MainGraphLoader.gd
· 10 KiB · GDScript3
Raw
extends Node2D
const GRAPH_FILE_PATH = "res://network_graph3.json"
var graph_node_scene: PackedScene = preload("res://GraphNode.tscn")
#@onready var status_label: Label = $ColorRect/Label
@onready var status_label: RichTextLabel = $ColorRect/Label
var _stable_frames: int = 0
# ==========================================
# PHYSICS TUNING & MOBILE OPTIMIZATIONS
# ==========================================
const REPULSION_STRENGTH: float = 45000.0
const SPRING_STRENGTH: float = 2.5
const REST_LENGTH: float = 330.0
const DAMPING: float = 0.95
const MAX_SPEED: float = 500.0 # Mobile safety cap
#const PHYSICS_SLEEP_THRESHOLD: float = 1.10 # Stops sim when stable (saves mobile CPU/battery)
const PHYSICS_SLEEP_THRESHOLD: float = 250.0 # Avg force per node allowed before sleeping
const STABLE_FRAME_REQUIREMENT: int = 60 # Must stay stable for ~1.5s (at 60fps) to sleep
# State & Caching
var spawned_nodes: Dictionary = {}
var node_ids: Array[String] = []
var relationships: Array = []
var lines: Array[Line2D] = []
var node_colors: Dictionary = {}
var viewport_size: Vector2i = Vector2i.ZERO
var physics_active: bool = true
# FPS & Metrics Tracking
var _last_metrics_update: float = 0.0
const METRICS_UPDATE_INTERVAL: float = 0.2 # Update UI 5x/sec to prevent GC spikes
var _total_node_count: int = 0
var _total_edge_count: int = 0
# ✅ NEW: Store benchmark results for the dashboard
var _benchmark_read_ms: float = 0.0
var _benchmark_parse_ms: float = 0.0
# ==========================================
# INITIALIZATION
# ==========================================
func _ready() -> void:
viewport_size = get_viewport_rect().size
get_window().size_changed.connect(_on_window_resized)
# FIX: Enable BBCode parsing so [color=...] tags render as actual colors
status_label.bbcode_enabled = true
var json_data: Dictionary = load_json_ld(GRAPH_FILE_PATH)
if json_data.is_empty():
push_warning("JSON-LD failed to load or is empty.")
return
var graph_array = json_data.get("@graph", [])
var nodes_map: Dictionary = {}
for item in graph_array:
if item is Dictionary and item.has("@id"):
nodes_map[item["@id"]] = item
build_graph_network(nodes_map)
_total_node_count = spawned_nodes.size()
_total_edge_count = relationships.size()
benchmark_json_ld()
print_rich("[color=cyan]Graph Loaded: ", str(_total_node_count), " nodes, ", str(_total_edge_count), " edges.[/color]")
# ==========================================
# CORE FUNCTIONS
# ==========================================
func _process(delta: float) -> void:
if physics_active:
_run_physics_step(min(delta, 0.03))
_update_lines()
_update_metrics(delta, Time.get_ticks_msec())
# ==========================================
# INPUT HANDLING (Mouse + Touch)
# ==========================================
func _unhandled_input(event: InputEvent) -> void:
var screen_pos = Vector2.ZERO
var is_click_like = false
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
screen_pos = event.position
is_click_like = true
elif event is InputEventScreenTouch:
screen_pos = event.position
is_click_like = true
if not is_click_like:
return
if event.is_pressed():
_attempt_grab_node(screen_pos, true)
else:
_attempt_release_drag()
func _on_window_resized() -> void:
viewport_size = get_viewport_rect().size
# ==========================================
# PHYSICS & UPDATE LOOPS
# ==========================================
func _run_physics_step(dt: float) -> void:
var total_force_magnitude: float = 0.0
var node_count = node_ids.size()
for i in range(node_count):
var id_a = node_ids[i]
var node_a = spawned_nodes[id_a]
if node_a.is_dragging: continue
var pos_a = node_a.position
var vel_a = node_a.velocity
for j in range(i + 1, node_count):
var id_b = node_ids[j]
var node_b = spawned_nodes[id_b]
if node_b.is_dragging: continue
var dir = pos_a - node_b.position
var dist_sq = dir.dot(dir)
if dist_sq < 1.0 or dist_sq > REPULSION_STRENGTH * 2.0: continue
var dist = sqrt(dist_sq)
var force = (REPULSION_STRENGTH / dist_sq) * dt
var f_vec = dir.normalized() * force
vel_a += f_vec
node_b.velocity -= f_vec if not node_b.is_dragging else Vector2.ZERO
total_force_magnitude += absf(force)
for rel in relationships:
var node_a = rel[0]
var node_b = rel[1]
var dir = node_b.position - node_a.position
var dist = dir.length()
if dist < 0.1: continue
var displacement = dist - REST_LENGTH
var force_vec = dir.normalized() * (SPRING_STRENGTH * displacement) * dt
if not node_a.is_dragging: node_a.velocity += force_vec
if not node_b.is_dragging: node_b.velocity -= force_vec
total_force_magnitude += absf(SPRING_STRENGTH * displacement)
for id in node_ids:
var node = spawned_nodes[id]
if node.is_dragging: continue
node.position += node.velocity * dt
node.velocity *= DAMPING
if node.velocity.length() > MAX_SPEED:
node.velocity = node.velocity.normalized() * MAX_SPEED
var gp = node.global_position
gp.x = clampf(gp.x, 50.0, float(viewport_size.x - 200))
gp.y = clampf(gp.y, 50.0, float(viewport_size.y - 100))
node.global_position = gp
# ==========================================
# STABILIZATION & AUTO-SLEEP LOGIC
# ==========================================
var avg_force_per_node = total_force_magnitude / max(node_count, 1.0)
if avg_force_per_node < PHYSICS_SLEEP_THRESHOLD:
_stable_frames += 1
else:
_stable_frames = 0
if _stable_frames >= STABLE_FRAME_REQUIREMENT:
physics_active = false
func _update_lines() -> void:
for i in range(relationships.size()):
var line = lines[i]
var rel = relationships[i]
var offset = Vector2(75.0, 25.0)
line.points = [
Vector2(rel[0].position + offset),
Vector2(rel[1].position + offset)
]
# ✅ UPDATED: Injects benchmark data into the persistent dashboard
func _update_metrics(_delta: float, current_time_ms: int) -> void:
if current_time_ms - _last_metrics_update < METRICS_UPDATE_INTERVAL * 1000.0:
return
var fps = Engine.get_frames_per_second()
var frame_time = 1000.0 / fps if fps > 0 else 0.0
var phys_color = "#7ee787" if physics_active else "#e74c3c"
var phys_status = "ACTIVE" if physics_active else "SLEEPING"
status_label.text = ("[color=#7ee787]▮ MOBILE DASHBOARD[/color]\n" +
"FPS: [color=cyan]%d[/color] | Frame: [color=orange]%.2f ms[/color]\n" +
"Nodes: %d | Edges: %d\n" +
"Physics: [color=%s]%s[/color]\n" +
"[color=#ffdd57]📊 BENCHMARK:[/color] Read: %.1f ms | Parse: %.1f ms") \
% [fps, frame_time, _total_node_count, _total_edge_count,
phys_color, phys_status, _benchmark_read_ms, _benchmark_parse_ms]
_last_metrics_update = current_time_ms
# ==========================================
# DRAG SYSTEM
# ==========================================
func _attempt_grab_node(screen_pos: Vector2, press: bool) -> void:
if not press: return
var best_dist: float = 50.0
var grabbed_id: String = ""
for id in node_ids:
var node = spawned_nodes[id]
var dist = (node.position - screen_pos).length()
if dist < best_dist:
best_dist = dist
grabbed_id = id
if not grabbed_id.is_empty():
spawned_nodes[grabbed_id].is_dragging = true
#func _attempt_release_drag() -> void:
#for node in spawned_nodes.values():
#if node.is_dragging:
#node.is_dragging = false
#physics_active = true
func _attempt_release_drag() -> void:
for node in spawned_nodes.values():
if node.is_dragging:
node.is_dragging = false
physics_active = true
_stable_frames = 0 # ✅ Reset stabilization counter on interaction
# ==========================================
# BUILDERS & UTILITIES
# ==========================================
func build_graph_network(nodes_map: Dictionary) -> void:
var keys = nodes_map.keys()
var total_nodes = keys.size()
var center = get_viewport_rect().size * 0.5
for i in range(total_nodes):
var node_id = keys[i]
var node_data = nodes_map[node_id]
var new_visual_node = graph_node_scene.instantiate()
if not new_visual_node.has_method("setup"):
new_visual_node.set_script(preload("res://GraphNode.gd"))
add_child(new_visual_node)
var angle = (TAU / total_nodes) * i
var radius = 400.0
new_visual_node.position = center + Vector2.from_angle(angle) * radius
new_visual_node.position += Vector2(randf_range(-50, 50), randf_range(-50, 50))
new_visual_node.setup(node_id, node_data.get("name", "Unknown"))
spawned_nodes[node_id] = new_visual_node
node_ids.append(node_id)
node_colors[node_id] = _get_unique_color(node_id)
for node_id in keys:
var node_data = nodes_map[node_id]
if not node_data.has("knows"): continue
var raw_connections = node_data["knows"]
var connection_list: Array = raw_connections if raw_connections is Array else [raw_connections]
for conn in connection_list:
if conn is Dictionary and conn.has("@id") and spawned_nodes.has(conn["@id"]):
relationships.append([spawned_nodes[node_id], spawned_nodes[conn["@id"]]])
var line = Line2D.new()
line.width = 5.0
line.z_index = -1
var grad = Gradient.new()
grad.set_color(0, node_colors[node_id])
grad.set_color(1, node_colors[conn["@id"]])
line.gradient = grad
add_child(line)
lines.append(line)
func _get_unique_color(id_string: String) -> Color:
var h = hash(id_string)
return Color8(
max(abs(h) % 256, 128),
max((abs(h) >> 8) % 256, 128),
max((abs(h) >> 16) % 256, 128)
)
func load_json_ld(path: String) -> Dictionary:
if not FileAccess.file_exists(path):
push_error("JSON-LD file not found at: " + path)
return {}
var file = FileAccess.open(path, FileAccess.READ)
var json_string = file.get_as_text()
file.close()
var parsed_data = JSON.parse_string(json_string)
return parsed_data if parsed_data is Dictionary else {}
# ✅ UPDATED: Saves timing data to class variables instead of just printing
func benchmark_json_ld() -> void:
if not FileAccess.file_exists(GRAPH_FILE_PATH):
return
var start_time = Time.get_ticks_msec()
var file = FileAccess.open(GRAPH_FILE_PATH, FileAccess.READ)
var json_string = file.get_as_text()
file.close()
_benchmark_read_ms = float(Time.get_ticks_msec() - start_time)
start_time = Time.get_ticks_msec()
var _parsed = JSON.parse_string(json_string)
_benchmark_parse_ms = float(Time.get_ticks_msec() - start_time)
#print_rich("[color=yellow]📊 BENCHMARK: Read: %.1f ms | Parse: %.1f ms[/color]" % [_benchmark_read_ms, _benchmark_parse_ms])
print_rich("[color=yellow]📊 BENCHMARK: Read: {read} ms | Parse: {parse} [/color]".format({
"read": "%0.1f" % _benchmark_read_ms,
"parse": "%0.1f" % _benchmark_parse_ms
}))
| 1 | extends Node2D |
| 2 | |
| 3 | const GRAPH_FILE_PATH = "res://network_graph3.json" |
| 4 | var graph_node_scene: PackedScene = preload("res://GraphNode.tscn") |
| 5 | #@onready var status_label: Label = $ColorRect/Label |
| 6 | @onready var status_label: RichTextLabel = $ColorRect/Label |
| 7 | var _stable_frames: int = 0 |
| 8 | |
| 9 | # ========================================== |
| 10 | # PHYSICS TUNING & MOBILE OPTIMIZATIONS |
| 11 | # ========================================== |
| 12 | const REPULSION_STRENGTH: float = 45000.0 |
| 13 | const SPRING_STRENGTH: float = 2.5 |
| 14 | const REST_LENGTH: float = 330.0 |
| 15 | const DAMPING: float = 0.95 |
| 16 | const MAX_SPEED: float = 500.0 # Mobile safety cap |
| 17 | #const PHYSICS_SLEEP_THRESHOLD: float = 1.10 # Stops sim when stable (saves mobile CPU/battery) |
| 18 | const PHYSICS_SLEEP_THRESHOLD: float = 250.0 # Avg force per node allowed before sleeping |
| 19 | const STABLE_FRAME_REQUIREMENT: int = 60 # Must stay stable for ~1.5s (at 60fps) to sleep |
| 20 | |
| 21 | # State & Caching |
| 22 | var spawned_nodes: Dictionary = {} |
| 23 | var node_ids: Array[String] = [] |
| 24 | var relationships: Array = [] |
| 25 | var lines: Array[Line2D] = [] |
| 26 | var node_colors: Dictionary = {} |
| 27 | |
| 28 | var viewport_size: Vector2i = Vector2i.ZERO |
| 29 | var physics_active: bool = true |
| 30 | |
| 31 | # FPS & Metrics Tracking |
| 32 | var _last_metrics_update: float = 0.0 |
| 33 | const METRICS_UPDATE_INTERVAL: float = 0.2 # Update UI 5x/sec to prevent GC spikes |
| 34 | var _total_node_count: int = 0 |
| 35 | var _total_edge_count: int = 0 |
| 36 | |
| 37 | # ✅ NEW: Store benchmark results for the dashboard |
| 38 | var _benchmark_read_ms: float = 0.0 |
| 39 | var _benchmark_parse_ms: float = 0.0 |
| 40 | |
| 41 | # ========================================== |
| 42 | # INITIALIZATION |
| 43 | # ========================================== |
| 44 | func _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 | # ========================================== |
| 74 | func _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 | # ========================================== |
| 84 | func _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 | |
| 103 | func _on_window_resized() -> void: |
| 104 | viewport_size = get_viewport_rect().size |
| 105 | |
| 106 | # ========================================== |
| 107 | # PHYSICS & UPDATE LOOPS |
| 108 | # ========================================== |
| 109 | func _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 | |
| 185 | func _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 |
| 197 | func _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 | # ========================================== |
| 220 | func _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 |
| 241 | func _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 | # ========================================== |
| 251 | func 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 | |
| 299 | func _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 | |
| 307 | func 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 |
| 319 | func 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 |