ZZ revised this gist . Go to revision
No changes
ZZ revised this gist . Go to revision
No changes
ZZ revised this gist . Go to revision
No changes
ZZ revised this gist . Go to revision
1 file changed, 0 insertions, 0 deletions
Godot-Scenetree.md renamed to Scenetree.md
File renamed without changes
ZZ revised this gist . Go to revision
1 file changed, 245 insertions
network_graph3.json(file created)
| @@ -0,0 +1,245 @@ | |||
| 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 | + | } | |
ZZ revised this gist . Go to revision
2 files changed, 342 insertions
Godot-Scenetree.md(file created)
| @@ -0,0 +1,5 @@ | |||
| 1 | + | ``` | |
| 2 | + | ┖╴GraphNode | |
| 3 | + | ┠╴ColorRect | |
| 4 | + | ┃ ┖╴Label | |
| 5 | + | ``` | |
MainGraphLoader.gd(file created)
| @@ -0,0 +1,337 @@ | |||
| 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 | + | })) | |