Advertisement
AntonOd

AI 3d knowledge graph

Feb 19th, 2025 (edited)
300
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 40.72 KB | None | 0 0
  1. import sys
  2. import random
  3. import os
  4. import tempfile
  5. import json
  6. import math
  7. import time
  8.  
  9. # PyQt imports
  10. from PyQt5.QtCore import QUrl, QObject, pyqtSlot, QTimer
  11. from PyQt5.QtWidgets import (
  12. QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  13. QLineEdit, QPushButton, QSlider, QLabel
  14. )
  15. from PyQt5.QtWebEngineWidgets import QWebEngineView
  16. from PyQt5.QtWebChannel import QWebChannel
  17.  
  18. # Google Generative AI
  19. import google.generativeai as genai
  20.  
  21. ###############################################################################
  22. # Bridge for JS-Python Communication
  23. ###############################################################################
  24. class ThreeJSBridge(QObject):
  25. def __init__(self, parent=None):
  26. super().__init__(parent)
  27. self.main_window = parent
  28.  
  29. @pyqtSlot(str)
  30. def nodeClicked(self, node_name):
  31. if self.main_window:
  32. self.main_window.expand_node(node_name)
  33.  
  34. @pyqtSlot(str, float, float, float)
  35. def updateNodePosition(self, name, x, y, z):
  36. if name in self.main_window.nodes:
  37. self.main_window.nodes[name]["x"] = x
  38. self.main_window.nodes[name]["y"] = y
  39. self.main_window.nodes[name]["z"] = z
  40. print(f"Updated position of {name}: ({x}, {y}, {z})")
  41.  
  42. @pyqtSlot(str, str)
  43. def customNodeSelected(self, parent_name, option):
  44. # Options: "E" (Practical Examples), "A" (Applications), "C" (Challenges),
  45. # "V" (Start Evolution), "H" (Hide nodes), "S" (Show nodes)
  46. if self.main_window:
  47. if option in ["E", "A", "C"]:
  48. self.main_window.add_custom_child(parent_name, option)
  49. elif option == "V":
  50. self.main_window.start_evolution(parent_name)
  51. elif option == "H":
  52. self.main_window.hide_children(parent_name)
  53. elif option == "S":
  54. self.main_window.show_children(parent_name)
  55.  
  56. @pyqtSlot(str)
  57. def flyToNode(self, node_name):
  58. if self.main_window:
  59. self.main_window.fly_to_node(node_name)
  60.  
  61. ###############################################################################
  62. # Main Application Class
  63. ###############################################################################
  64. class KnowledgeGraphExplorer(QMainWindow):
  65. def __init__(self, api_key):
  66. super().__init__()
  67. self.setWindowTitle("3D Knowledge Graph Explorer")
  68. genai.configure(api_key=api_key)
  69. self.model = genai.GenerativeModel('gemini-2.0-flash')
  70.  
  71. # Each node: { x, y, z, expanded, broad, future, (parent), (color), (hidden),
  72. # (evolving), (growthStart), (growthDuration) }
  73. self.nodes = {}
  74. self.edges = [] # list of (node1, node2)
  75. self.scene_loaded = False
  76. self.temp_dir = tempfile.mkdtemp()
  77. self.html_path = os.path.join(self.temp_dir, 'graph.html')
  78.  
  79. # Evolution process variables:
  80. self.evolution_timer = None
  81. self.evolving_root = None # the node (by name) where evolution started
  82. self.evolution_interval = 5000 # in ms (default 5 sec)
  83. self.evolution_grow_speed = 100 # slider value, default 100 -> 5000ms growth duration
  84.  
  85. self.init_ui()
  86. self.setup_web_channel()
  87.  
  88. def init_ui(self):
  89. main_widget = QWidget()
  90. self.setCentralWidget(main_widget)
  91. layout = QVBoxLayout(main_widget)
  92.  
  93. # Top control panel: input, sliders, and Stop Evolution button
  94. top_layout = QHBoxLayout()
  95. self.word_edit = QLineEdit()
  96. self.word_edit.setPlaceholderText("Enter concept to explore")
  97. self.word_edit.setStyleSheet("""
  98. QLineEdit { color: #fff; background: #2a2a2a; border: 1px solid #3a3a3a;
  99. border-radius: 4px; padding: 8px; font-size: 14px; }
  100. """)
  101. self.word_edit.returnPressed.connect(self.on_explore)
  102. top_layout.addWidget(self.word_edit)
  103.  
  104. self.explore_button = QPushButton("Explore")
  105. self.explore_button.setStyleSheet("""
  106. QPushButton { color: #fff; background: #3a3a3a; border: none; border-radius: 4px;
  107. padding: 8px 16px; font-size: 14px; }
  108. QPushButton:hover { background: #4a4a4a; }
  109. """)
  110. self.explore_button.clicked.connect(self.on_explore)
  111. top_layout.addWidget(self.explore_button)
  112.  
  113. # Node size slider
  114. self.size_slider = QSlider()
  115. self.size_slider.setOrientation(1)
  116. self.size_slider.setMinimum(1)
  117. self.size_slider.setMaximum(200)
  118. self.size_slider.setValue(100)
  119. self.size_slider.valueChanged.connect(self.on_size_change)
  120. size_label = QLabel("Node Size")
  121. size_label.setStyleSheet("color: #fff;")
  122. top_layout.addWidget(size_label)
  123. top_layout.addWidget(self.size_slider)
  124.  
  125. # Orbit speed slider (min 0)
  126. self.orbit_slider = QSlider()
  127. self.orbit_slider.setOrientation(1)
  128. self.orbit_slider.setMinimum(0)
  129. self.orbit_slider.setMaximum(100)
  130. self.orbit_slider.setValue(10)
  131. self.orbit_slider.valueChanged.connect(self.on_orbit_change)
  132. orbit_label = QLabel("Orbit Speed")
  133. orbit_label.setStyleSheet("color: #fff;")
  134. top_layout.addWidget(orbit_label)
  135. top_layout.addWidget(self.orbit_slider)
  136.  
  137. # Evolution Grow Speed slider
  138. self.evolution_grow_slider = QSlider()
  139. self.evolution_grow_slider.setOrientation(1)
  140. self.evolution_grow_slider.setMinimum(1)
  141. self.evolution_grow_slider.setMaximum(200)
  142. self.evolution_grow_slider.setValue(100)
  143. self.evolution_grow_slider.valueChanged.connect(self.on_evolution_grow_change)
  144. grow_label = QLabel("Evolution Grow Speed")
  145. grow_label.setStyleSheet("color: #fff;")
  146. top_layout.addWidget(grow_label)
  147. top_layout.addWidget(self.evolution_grow_slider)
  148.  
  149. # Evolution Timer slider (in seconds)
  150. self.evolution_timer_slider = QSlider()
  151. self.evolution_timer_slider.setOrientation(1)
  152. self.evolution_timer_slider.setMinimum(1)
  153. self.evolution_timer_slider.setMaximum(30)
  154. self.evolution_timer_slider.setValue(5)
  155. self.evolution_timer_slider.valueChanged.connect(self.on_evolution_timer_change)
  156. timer_label = QLabel("Evolution Timer (sec)")
  157. timer_label.setStyleSheet("color: #fff;")
  158. top_layout.addWidget(timer_label)
  159. top_layout.addWidget(self.evolution_timer_slider)
  160.  
  161. # Stop Evolution button
  162. self.stop_evolution_button = QPushButton("Stop Evolution")
  163. self.stop_evolution_button.setStyleSheet("""
  164. QPushButton { color: #fff; background: #AA0000; border: none; border-radius: 4px;
  165. padding: 8px 16px; font-size: 14px; }
  166. QPushButton:hover { background: #CC0000; }
  167. """)
  168. self.stop_evolution_button.clicked.connect(self.stop_evolution)
  169. top_layout.addWidget(self.stop_evolution_button)
  170.  
  171. layout.addLayout(top_layout)
  172.  
  173. # 3D visualization canvas
  174. self.web_view = QWebEngineView()
  175. self.web_view.setStyleSheet("background: #000;")
  176. layout.addWidget(self.web_view)
  177.  
  178. self.resize(1280, 800)
  179. self.setStyleSheet("background: #1a1a1a; color: #fff;")
  180.  
  181. def on_evolution_grow_change(self, value):
  182. # Store slider value; growthDuration = 5000*(100/value) ms.
  183. self.evolution_grow_speed = value
  184. js = f"updateEvolutionGrowSpeed({value});"
  185. self.web_view.page().runJavaScript(js)
  186.  
  187. def on_evolution_timer_change(self, value):
  188. # Update evolution timer interval in ms.
  189. self.evolution_interval = value * 1000
  190. js = f"updateEvolutionTimer({value});"
  191. self.web_view.page().runJavaScript(js)
  192. if self.evolution_timer and self.evolving_root:
  193. # Restart timer with new interval.
  194. self.evolution_timer.stop()
  195. self.evolution_timer.start(self.evolution_interval)
  196. print(f"Evolution timer updated to {value} sec.")
  197.  
  198. def setup_web_channel(self):
  199. self.channel = QWebChannel()
  200. self.bridge = ThreeJSBridge(self)
  201. self.channel.registerObject("bridge", self.bridge)
  202. self.web_view.page().setWebChannel(self.channel)
  203.  
  204. def on_size_change(self, value):
  205. scale = value / 100.0
  206. js = f"updateNodeScale({scale});"
  207. self.web_view.page().runJavaScript(js)
  208.  
  209. def on_orbit_change(self, value):
  210. speed = value / 1000.0
  211. js = f"updateOrbitSpeed({speed});"
  212. self.web_view.page().runJavaScript(js)
  213.  
  214. def query_llm(self, word):
  215. existing_words = list(self.nodes.keys())
  216. prompt = (
  217. f"Generate concepts related to '{word}' as 4–6 comma-separated items.\n"
  218. "Requirements:\n"
  219. "1) Exactly one concept prefixed with 'B:' (a broader topic).\n"
  220. "2) Exactly one concept prefixed with 'F:' (a future evolution topic).\n"
  221. "3) The remaining concepts are regular.\n"
  222. "Do NOT include any of these existing concepts: "
  223. f"{', '.join(existing_words) if existing_words else '(none)'}\n"
  224. )
  225. try:
  226. response = self.model.generate_content(prompt)
  227. return [w.strip() for w in response.text.split(',') if w.strip()]
  228. except Exception as e:
  229. print(f"LLM Error: {e}")
  230. return []
  231.  
  232. def query_custom_llm(self, parent, option):
  233. existing_words = [w.lower() for w in self.nodes.keys()]
  234. if option == "E":
  235. req = "a practical example"
  236. elif option == "A":
  237. req = "an application"
  238. elif option == "C":
  239. req = "a challenge"
  240. else:
  241. req = "a related concept"
  242. prompt = (
  243. f"Generate one single one-word concept that is {req} for the concept '{parent}'.\n"
  244. f"Ensure the new concept is directly related to '{parent}' and does not duplicate any concept already present.\n"
  245. "Return only one word, with no extra text.\n"
  246. "Do NOT include any existing concept: "
  247. f"{', '.join(existing_words) if existing_words else '(none)'}."
  248. )
  249. try:
  250. response = self.model.generate_content(prompt)
  251. word = response.text.strip().split()[0]
  252. if word.lower() in existing_words:
  253. return None
  254. return word
  255. except Exception as e:
  256. print(f"Custom LLM Error: {e}")
  257. return None
  258.  
  259. def add_node(self, word, pos, broad=False, future=False, parent=None, color=None, evolving=False, growthDuration=None):
  260. if word not in self.nodes:
  261. self.nodes[word] = {
  262. "x": pos[0],
  263. "y": pos[1],
  264. "z": pos[2],
  265. "expanded": False,
  266. "broad": broad,
  267. "future": future
  268. }
  269. if parent:
  270. self.nodes[word]["parent"] = parent
  271. if color:
  272. self.nodes[word]["color"] = color
  273. if evolving:
  274. self.nodes[word]["evolving"] = True
  275. self.nodes[word]["growthStart"] = int(time.time()*1000)
  276. self.nodes[word]["growthDuration"] = growthDuration if growthDuration else 5000
  277. # No duplicates are added.
  278.  
  279. def add_edge(self, w1, w2):
  280. if (w1, w2) not in self.edges and (w2, w1) not in self.edges:
  281. self.edges.append((w1, w2))
  282.  
  283. def expand_node(self, word):
  284. if word not in self.nodes or self.nodes[word]["expanded"]:
  285. return
  286.  
  287. self.nodes[word]["expanded"] = True
  288. new_concepts = self.query_llm(word)
  289. px, py, pz = self.nodes[word]["x"], self.nodes[word]["y"], self.nodes[word]["z"]
  290. radius = 3
  291. for c in new_concepts:
  292. if c in self.nodes:
  293. continue
  294. is_broad = False
  295. is_future = False
  296. concept_str = c
  297. if c.lower().startswith("b:"):
  298. is_broad = True
  299. concept_str = c[2:].strip()
  300. elif c.lower().startswith("f:"):
  301. is_future = True
  302. concept_str = c[2:].strip()
  303. theta = random.uniform(0, 2 * math.pi)
  304. phi = random.uniform(0, math.pi)
  305. nx = px + radius * math.sin(phi) * math.cos(theta)
  306. ny = py + radius * math.sin(phi) * math.sin(theta)
  307. nz = pz + radius * math.cos(phi)
  308. self.add_node(concept_str, (nx, ny, nz), broad=is_broad, future=is_future, parent=word)
  309. self.add_edge(word, concept_str)
  310. self.update_visualization(initial=False)
  311.  
  312. def add_custom_child(self, parent_name, option):
  313. if parent_name not in self.nodes:
  314. return
  315. new_word = self.query_custom_llm(parent_name, option)
  316. if not new_word:
  317. print("Duplicate or invalid custom concept.")
  318. return
  319. parent = self.nodes[parent_name]
  320. radius = 3
  321. theta = random.uniform(0, 2 * math.pi)
  322. phi = random.uniform(0, math.pi)
  323. nx = parent["x"] + radius * math.sin(phi) * math.cos(theta)
  324. ny = parent["y"] + radius * math.sin(phi) * math.sin(theta)
  325. nz = parent["z"] + radius * math.cos(phi)
  326. if option == "E":
  327. color = "#00FF00" # green for practical examples
  328. elif option == "A":
  329. color = "#808080" # gray for applications
  330. elif option == "C":
  331. color = "#FF0000" # red for challenges
  332. else:
  333. color = "#FFFFFF"
  334. self.add_node(new_word, (nx, ny, nz), color=color, parent=parent_name)
  335. self.add_edge(parent_name, new_word)
  336. self.update_visualization(initial=False)
  337.  
  338. def hide_children(self, parent_name):
  339. for word, data in self.nodes.items():
  340. if data.get("parent") == parent_name:
  341. data["hidden"] = True
  342. self.update_visualization(initial=False)
  343.  
  344. def show_children(self, parent_name):
  345. for word, data in self.nodes.items():
  346. if data.get("parent") == parent_name:
  347. data["hidden"] = False
  348. self.update_visualization(initial=False)
  349.  
  350. def start_evolution(self, parent_name):
  351. if parent_name not in self.nodes:
  352. return
  353. self.stop_evolution() # Stop any existing evolution
  354. self.evolving_root = parent_name
  355. self.evolution_timer = QTimer(self)
  356. self.evolution_timer.timeout.connect(lambda: self.evolve_random_descendant(self.evolving_root))
  357. self.evolution_timer.start(self.evolution_interval)
  358. print(f"Evolution started on branch: {parent_name}")
  359.  
  360. def stop_evolution(self):
  361. if self.evolution_timer:
  362. self.evolution_timer.stop()
  363. self.evolution_timer = None
  364. print("Evolution stopped.")
  365. self.evolving_root = None
  366.  
  367. def get_descendants(self, root):
  368. descendants = []
  369. for word, data in self.nodes.items():
  370. current = data.get("parent")
  371. while current:
  372. if current == root:
  373. descendants.append(word)
  374. break
  375. current = self.nodes.get(current, {}).get("parent")
  376. descendants.append(root)
  377. return descendants
  378.  
  379. def evolve_random_descendant(self, root):
  380. descendants = self.get_descendants(root)
  381. if not descendants:
  382. return
  383. random_node = random.choice(descendants)
  384. if random_node in self.nodes and not self.nodes[random_node].get("hidden", False):
  385. existing_words = [w.lower() for w in self.nodes.keys()]
  386. prompt = (
  387. f"Generate one single one-word concept that is evolutionarily related to '{random_node}'.\n"
  388. "Return only one word, with no extra text.\n"
  389. "Do NOT include any concept already present: "
  390. f"{', '.join(existing_words) if existing_words else '(none)'}."
  391. )
  392. try:
  393. response = self.model.generate_content(prompt)
  394. new_word = response.text.strip().split()[0]
  395. if new_word.lower() in existing_words:
  396. print("Evolution: duplicate concept; not added.")
  397. return
  398. except Exception as e:
  399. print(f"Evolution LLM Error: {e}")
  400. return
  401. parent = self.nodes[random_node]
  402. radius = 3
  403. theta = random.uniform(0, 2 * math.pi)
  404. phi = random.uniform(0, math.pi)
  405. nx = parent["x"] + radius * math.sin(phi) * math.cos(theta)
  406. ny = parent["y"] + radius * math.sin(phi) * math.sin(theta)
  407. nz = parent["z"] + radius * math.cos(phi)
  408. growthDuration = int(5000 * (100 / self.evolution_grow_speed))
  409. self.add_node(new_word, (nx, ny, nz), parent=random_node, color="#2196F3",
  410. evolving=True, growthDuration=growthDuration)
  411. self.add_edge(random_node, new_word)
  412. self.update_visualization(initial=False)
  413. print(f"Evolution added node: {new_word} under {random_node}")
  414. self.fly_to_node(new_word)
  415.  
  416. def fly_to_node(self, word):
  417. js = f"flyCameraToNode('{word}');"
  418. self.web_view.page().runJavaScript(js)
  419.  
  420. def on_explore(self):
  421. concept = self.word_edit.text().strip()
  422. if concept:
  423. self.nodes.clear()
  424. self.edges.clear()
  425. self.add_node(concept, (0, 0, 0))
  426. self.expand_node(concept)
  427. self.update_visualization(initial=True)
  428.  
  429. def generate_scene_data(self):
  430. scene_nodes = []
  431. for word, data in self.nodes.items():
  432. node = {
  433. "name": word,
  434. "x": data["x"],
  435. "y": data["y"],
  436. "z": data["z"],
  437. "color": data.get("color", (
  438. "#FFFF00" if data.get("broad", False) else (
  439. "#800080" if data.get("future", False) else (
  440. "#4CAF50" if data["expanded"] and not data.get("hidden", False) else "#2196F3"
  441. )
  442. )
  443. ))
  444. }
  445. if "parent" in data:
  446. node["parent"] = data["parent"]
  447. node["hidden"] = data.get("hidden", False)
  448. if data.get("evolving", False):
  449. node["evolving"] = True
  450. node["growthStart"] = data.get("growthStart", 0)
  451. node["growthDuration"] = data.get("growthDuration", 5000)
  452. scene_nodes.append(node)
  453. return {
  454. "nodes": scene_nodes,
  455. "edges": [
  456. {
  457. "startName": w1,
  458. "endName": w2,
  459. "x1": self.nodes[w1]["x"],
  460. "y1": self.nodes[w1]["y"],
  461. "z1": self.nodes[w1]["z"],
  462. "x2": self.nodes[w2]["x"],
  463. "y2": self.nodes[w2]["y"],
  464. "z2": self.nodes[w2]["z"],
  465. "color": f"hsl({(i * 30) % 360}, 80%, 60%)"
  466. }
  467. for i, (w1, w2) in enumerate(self.edges)
  468. ]
  469. }
  470.  
  471. def update_visualization(self, initial=False):
  472. scene_data = self.generate_scene_data()
  473. if initial or not self.scene_loaded:
  474. html_content = self.generate_threejs_html(scene_data)
  475. with open(self.html_path, 'w', encoding='utf-8') as f:
  476. f.write(html_content)
  477. self.web_view.setUrl(QUrl.fromLocalFile(self.html_path))
  478. self.scene_loaded = True
  479. else:
  480. js_command = "updateSceneData(" + json.dumps(scene_data) + ")"
  481. self.web_view.page().runJavaScript(js_command)
  482.  
  483. def generate_threejs_html(self, scene_data):
  484. # The animate loop now also checks for evolving nodes.
  485. return f"""
  486. <!DOCTYPE html>
  487. <html>
  488. <head>
  489. <style>
  490. body {{ margin: 0; overflow: hidden; background: #000; }}
  491. canvas {{ width: 100%; height: 100%; }}
  492. /* Context Menu Styling */
  493. #contextMenu {{
  494. position: absolute;
  495. display: none;
  496. background: #333;
  497. color: #fff;
  498. padding: 5px;
  499. border: 1px solid #888;
  500. z-index: 1000;
  501. font-family: sans-serif;
  502. }}
  503. #contextMenu .menuItem {{
  504. padding: 5px 10px;
  505. cursor: pointer;
  506. }}
  507. #contextMenu .menuItem:hover {{
  508. background: #555;
  509. }}
  510. </style>
  511. </head>
  512. <body>
  513. <!-- Custom Context Menu -->
  514. <div id="contextMenu">
  515. <div class="menuItem" data-option="E">Practical Examples</div>
  516. <div class="menuItem" data-option="A">Applications</div>
  517. <div class="menuItem" data-option="C">Challenges</div>
  518. <div class="menuItem" data-option="V">Start Evolution</div>
  519. <div class="menuItem" data-option="H">Hide nodes</div>
  520. <div class="menuItem" data-option="S">Show nodes</div>
  521. </div>
  522. <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  523. <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
  524. <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/DragControls.js"></script>
  525. <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/geometries/TextGeometry.js"></script>
  526. <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/FontLoader.js"></script>
  527. <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
  528. <script>
  529. let scene, camera, renderer, controls, font;
  530. let nodes = [];
  531. let draggableNodes = [];
  532. let edges = [];
  533. let dragControls;
  534. let isDragging = false;
  535. window.nodeScale = 1.0;
  536. window.orbitSpeed = 0.01;
  537. window.evolutionGrowSlider = 100; // Default evolution grow speed slider value
  538. // Evolution timer in seconds; slider value (default 5 sec)
  539. window.evolutionIntervalSec = 5;
  540. const sceneData = {json.dumps(scene_data)};
  541.  
  542. // Helper: rotate vector v around Y axis by angle
  543. function rotateY(v, angle) {{
  544. let cos = Math.cos(angle);
  545. let sin = Math.sin(angle);
  546. return new THREE.Vector3(v.x * cos - v.z * sin, v.y, v.x * sin + v.z * cos);
  547. }}
  548.  
  549. function findNodeByName(name) {{
  550. return nodes.find(m => m.userData.name === name);
  551. }}
  552.  
  553. new THREE.FontLoader().load(
  554. 'https://cdn.jsdelivr.net/npm/[email protected]/examples/fonts/helvetiker_regular.typeface.json',
  555. function(loadedFont) {{
  556. font = loadedFont;
  557. initScene();
  558. animate();
  559. }}
  560. );
  561.  
  562. function createNode(n) {{
  563. const geometry = new THREE.SphereGeometry(0.6, 32, 32);
  564. const material = new THREE.MeshPhongMaterial({{
  565. color: n.color,
  566. emissive: n.color,
  567. emissiveIntensity: 0.3,
  568. transparent: true,
  569. opacity: 0.9
  570. }});
  571. const mesh = new THREE.Mesh(geometry, material);
  572. mesh.position.set(n.x, n.y, n.z);
  573. mesh.scale.set(window.nodeScale, window.nodeScale, window.nodeScale);
  574. const textGeo = new THREE.TextGeometry(n.name, {{
  575. font: font,
  576. size: 0.3,
  577. height: 0.05,
  578. curveSegments: 2
  579. }});
  580. textGeo.computeBoundingBox();
  581. const tw = textGeo.boundingBox.max.x - textGeo.boundingBox.min.x;
  582. const th = textGeo.boundingBox.max.y - textGeo.boundingBox.min.y;
  583. const textMat = new THREE.MeshBasicMaterial({{ color: '#ffffff' }});
  584. const textMesh = new THREE.Mesh(textGeo, textMat);
  585. textMesh.position.set(-tw/2, -th/2, 0.7);
  586. mesh.add(textMesh);
  587. mesh.userData.name = n.name;
  588. mesh.userData.textMesh = textMesh;
  589. mesh.userData.hidden = n.hidden || false;
  590. mesh.visible = !mesh.userData.hidden;
  591. if(n.parent) {{
  592. mesh.userData.parent = n.parent;
  593. let parentMesh = findNodeByName(n.parent);
  594. if(parentMesh) {{
  595. let parentPos = new THREE.Vector3();
  596. parentMesh.getWorldPosition(parentPos);
  597. mesh.userData.localOffset = mesh.position.clone().sub(parentPos);
  598. }} else {{
  599. mesh.userData.localOffset = mesh.position.clone();
  600. }}
  601. }}
  602. if(n.evolving) {{
  603. mesh.userData.evolving = true;
  604. mesh.userData.growthStart = n.growthStart;
  605. mesh.userData.growthDuration = n.growthDuration;
  606. mesh.scale.set(0.1 * window.nodeScale, 0.1 * window.nodeScale, 0.1 * window.nodeScale);
  607. }}
  608. return mesh;
  609. }}
  610.  
  611. function createEdge(e) {{
  612. const startNode = findNodeByName(e.startName);
  613. const endNode = findNodeByName(e.endName);
  614. if (!startNode || !endNode) return null;
  615. let startPos = new THREE.Vector3(), endPos = new THREE.Vector3();
  616. startNode.getWorldPosition(startPos);
  617. endNode.getWorldPosition(endPos);
  618. let dir = new THREE.Vector3().subVectors(endPos, startPos).normalize();
  619. let r = 0.6 * window.nodeScale;
  620. let newStart = startPos.clone().add(dir.clone().multiplyScalar(r));
  621. let newEnd = endPos.clone().add(dir.clone().multiplyScalar(-r));
  622. let mid = new THREE.Vector3().addVectors(newStart, newEnd).multiplyScalar(0.5);
  623. let offset = new THREE.Vector3((Math.random()-0.5)*2, (Math.random()-0.5)*2, (Math.random()-0.5)*2);
  624. let ctrl = mid.clone().add(offset);
  625. let curve = new THREE.CatmullRomCurve3([newStart, ctrl, newEnd]);
  626. let points = curve.getPoints(50);
  627. let geom = new THREE.BufferGeometry().setFromPoints(points);
  628. let mat = new THREE.LineDashedMaterial({{
  629. color: e.color,
  630. dashSize: 0.25,
  631. gapSize: 0.15,
  632. linewidth: 1.5,
  633. transparent: true,
  634. opacity: 0.7
  635. }});
  636. let line = new THREE.Line(geom, mat);
  637. line.computeLineDistances();
  638. line.userData = {{
  639. startName: e.startName,
  640. endName: e.endName,
  641. offset: offset
  642. }};
  643. return line;
  644. }}
  645.  
  646. function initScene() {{
  647. scene = new THREE.Scene();
  648. camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1e10);
  649. camera.position.z = 60;
  650. renderer = new THREE.WebGLRenderer({{ antialias: true, alpha: true }});
  651. renderer.setSize(window.innerWidth, window.innerHeight);
  652. document.body.appendChild(renderer.domElement);
  653. scene.add(new THREE.AmbientLight(0x404040, 2));
  654. let dirLight = new THREE.DirectionalLight(0xffffff, 1);
  655. dirLight.position.set(10, 10, 10);
  656. scene.add(dirLight);
  657. scene.fog = new THREE.Fog(0x000000, 1000, 1e10);
  658.  
  659. sceneData.nodes.forEach(n => {{
  660. const mesh = createNode(n);
  661. scene.add(mesh);
  662. draggableNodes.push(mesh);
  663. nodes.push(mesh);
  664. }});
  665.  
  666. sceneData.edges.forEach(e => {{
  667. const line = createEdge(e);
  668. if(line) {{
  669. scene.add(line);
  670. edges.push(line);
  671. }}
  672. }});
  673.  
  674. controls = new THREE.OrbitControls(camera, renderer.domElement);
  675. controls.enableDamping = true;
  676. controls.dampingFactor = 0.05;
  677. controls.minDistance = 0;
  678. controls.maxDistance = Infinity;
  679.  
  680. dragControls = new THREE.DragControls(draggableNodes, camera, renderer.domElement);
  681. dragControls.addEventListener('dragstart', event => {{
  682. controls.enabled = false;
  683. event.object.userData.dragging = true;
  684. isDragging = true;
  685. }});
  686. dragControls.addEventListener('dragend', event => {{
  687. controls.enabled = true;
  688. event.object.userData.dragging = false;
  689. if(event.object.userData.parent) {{
  690. let parentMesh = findNodeByName(event.object.userData.parent);
  691. if(parentMesh) {{
  692. let parentPos = new THREE.Vector3();
  693. parentMesh.getWorldPosition(parentPos);
  694. event.object.userData.localOffset = event.object.position.clone().sub(parentPos);
  695. }}
  696. }}
  697. let obj = event.object;
  698. let worldPos = new THREE.Vector3();
  699. obj.getWorldPosition(worldPos);
  700. new QWebChannel(qt.webChannelTransport, channel => {{
  701. channel.objects.bridge.updateNodePosition(obj.userData.name, worldPos.x, worldPos.y, worldPos.z);
  702. }});
  703. setTimeout(() => {{ isDragging = false; }}, 100);
  704. }});
  705.  
  706. // Middle mouse: fly-to node and zoom in (offset 5).
  707. renderer.domElement.addEventListener('auxclick', event => {{
  708. if(event.button === 1) {{
  709. const mouse = new THREE.Vector2();
  710. mouse.x = (event.clientX/window.innerWidth)*2 - 1;
  711. mouse.y = -(event.clientY/window.innerHeight)*2 + 1;
  712. const raycaster = new THREE.Raycaster();
  713. raycaster.setFromCamera(mouse, camera);
  714. const intersects = raycaster.intersectObjects(nodes, true);
  715. if(intersects.length > 0) {{
  716. let obj = intersects[0].object;
  717. while(obj.parent && !obj.userData.name) {{
  718. obj = obj.parent;
  719. }}
  720. if(obj.userData.name) {{
  721. new QWebChannel(qt.webChannelTransport, channel => {{
  722. channel.objects.bridge.flyToNode(obj.userData.name);
  723. }});
  724. }}
  725. }}
  726. }}
  727. }});
  728.  
  729. // Right-click: show context menu.
  730. renderer.domElement.addEventListener('contextmenu', event => {{
  731. event.preventDefault();
  732. const mouse = new THREE.Vector2();
  733. mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  734. mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  735. const raycaster = new THREE.Raycaster();
  736. raycaster.setFromCamera(mouse, camera);
  737. const intersects = raycaster.intersectObjects(nodes, true);
  738. if(intersects.length > 0) {{
  739. let target = intersects[0].object;
  740. while(!target.userData.name && target.parent) {{
  741. target = target.parent;
  742. }}
  743. if(target.userData.name) {{
  744. let menu = document.getElementById("contextMenu");
  745. menu.style.left = event.clientX + "px";
  746. menu.style.top = event.clientY + "px";
  747. menu.style.display = "block";
  748. menu.setAttribute("data-parent", target.userData.name);
  749. }}
  750. }}
  751. }});
  752.  
  753. window.addEventListener('click', () => {{
  754. document.getElementById("contextMenu").style.display = "none";
  755. }});
  756.  
  757. document.querySelectorAll("#contextMenu .menuItem").forEach(item => {{
  758. item.addEventListener('click', event => {{
  759. const option = event.target.getAttribute("data-option");
  760. const menu = document.getElementById("contextMenu");
  761. const parentName = menu.getAttribute("data-parent");
  762. new QWebChannel(qt.webChannelTransport, channel => {{
  763. channel.objects.bridge.customNodeSelected(parentName, option);
  764. }});
  765. menu.style.display = "none";
  766. }});
  767. }});
  768.  
  769. // Left-click: regular expansion.
  770. const raycaster = new THREE.Raycaster();
  771. const mouse = new THREE.Vector2();
  772. window.addEventListener('click', event => {{
  773. if(isDragging) return;
  774. mouse.x = (event.clientX/window.innerWidth)*2 - 1;
  775. mouse.y = -(event.clientY/window.innerHeight)*2 + 1;
  776. raycaster.setFromCamera(mouse, camera);
  777. const intersects = raycaster.intersectObjects(nodes, true);
  778. if(intersects.length > 0) {{
  779. let obj = intersects[0].object;
  780. while(obj.parent && !obj.userData.name) {{
  781. obj = obj.parent;
  782. }}
  783. if(obj.userData.name) {{
  784. new QWebChannel(qt.webChannelTransport, channel => {{
  785. channel.objects.bridge.nodeClicked(obj.userData.name);
  786. }});
  787. }}
  788. }}
  789. }});
  790. }}
  791.  
  792. function updateSceneData(newData) {{
  793. newData.nodes.forEach(n => {{
  794. let existing = nodes.find(m => m.userData.name === n.name);
  795. if(!existing) {{
  796. let mesh = createNode(n);
  797. scene.add(mesh);
  798. draggableNodes.push(mesh);
  799. nodes.push(mesh);
  800. }} else {{
  801. existing.userData.hidden = n.hidden;
  802. existing.visible = !n.hidden;
  803. if(n.evolving) {{
  804. existing.userData.evolving = true;
  805. existing.userData.growthStart = n.growthStart;
  806. existing.userData.growthDuration = n.growthDuration;
  807. }} else {{
  808. existing.userData.evolving = false;
  809. }}
  810. }}
  811. }});
  812. newData.edges.forEach(e => {{
  813. let exists = edges.some(line =>
  814. line.userData.startName === e.startName &&
  815. line.userData.endName === e.endName
  816. );
  817. if(!exists) {{
  818. let line = createEdge(e);
  819. if(line) {{
  820. scene.add(line);
  821. edges.push(line);
  822. }}
  823. }}
  824. }});
  825. dragControls.objects = draggableNodes;
  826. }}
  827.  
  828. // Fly-to animation with closer zoom (offset 5).
  829. function flyCameraToNode(nodeName) {{
  830. let targetMesh = nodes.find(m => m.userData.name === nodeName);
  831. if(!targetMesh) return;
  832. let targetPos = new THREE.Vector3();
  833. targetMesh.getWorldPosition(targetPos);
  834. let startPos = camera.position.clone();
  835. let duration = 500;
  836. let startTime = performance.now();
  837. function animateFly() {{
  838. let now = performance.now();
  839. let t = Math.min((now - startTime) / duration, 1);
  840. let ease = t * (2 - t);
  841. camera.position.lerpVectors(startPos, targetPos.clone().add(new THREE.Vector3(0, 0, 5)), ease);
  842. controls.target.lerp(targetPos, ease);
  843. controls.update();
  844. if(t < 1) {{
  845. requestAnimationFrame(animateFly);
  846. }}
  847. }}
  848. animateFly();
  849. }}
  850.  
  851. function updateNodeScale(scale) {{
  852. window.nodeScale = scale;
  853. nodes.forEach(mesh => {{
  854. mesh.scale.set(scale, scale, scale);
  855. }});
  856. }}
  857.  
  858. function updateOrbitSpeed(speed) {{
  859. window.orbitSpeed = speed;
  860. }}
  861.  
  862. // Evolution grow speed update (can be used in growth interpolation)
  863. function updateEvolutionGrowSpeed(value) {{
  864. window.evolutionGrowSlider = value;
  865. }}
  866.  
  867. // Evolution timer update (for display, if needed)
  868. function updateEvolutionTimer(value) {{
  869. window.evolutionIntervalSec = value;
  870. }}
  871.  
  872. function animate() {{
  873. requestAnimationFrame(animate);
  874. scene.updateMatrixWorld(true);
  875. let currentTime = Date.now();
  876. nodes.forEach(mesh => {{
  877. if(mesh.userData.textMesh) {{
  878. mesh.userData.textMesh.lookAt(camera.position);
  879. }}
  880. if(mesh.userData.parent && !mesh.userData.dragging) {{
  881. let parentMesh = nodes.find(m => m.userData.name === mesh.userData.parent);
  882. if(parentMesh) {{
  883. let parentPos = new THREE.Vector3();
  884. parentMesh.getWorldPosition(parentPos);
  885. if(!mesh.userData.localOffset) {{
  886. mesh.userData.localOffset = mesh.position.clone().sub(parentPos);
  887. }}
  888. let newOffset = rotateY(mesh.userData.localOffset, window.orbitSpeed);
  889. mesh.userData.localOffset = newOffset;
  890. let newPos = parentPos.clone().add(newOffset);
  891. mesh.position.copy(newPos);
  892. }}
  893. }}
  894. // Animate evolving nodes (growth)
  895. if(mesh.userData.evolving) {{
  896. let elapsed = currentTime - mesh.userData.growthStart;
  897. let progress = Math.min(elapsed / mesh.userData.growthDuration, 1);
  898. let newScale = 0.1 * window.nodeScale + progress * (window.nodeScale - 0.1 * window.nodeScale);
  899. mesh.scale.set(newScale, newScale, newScale);
  900. if(progress >= 1) {{
  901. mesh.userData.evolving = false;
  902. }}
  903. }}
  904. }});
  905. edges.forEach(line => {{
  906. const startNode = nodes.find(m => m.userData.name === line.userData.startName);
  907. const endNode = nodes.find(m => m.userData.name === line.userData.endName);
  908. if(startNode && endNode) {{
  909. let startPos = new THREE.Vector3(), endPos = new THREE.Vector3();
  910. startNode.getWorldPosition(startPos);
  911. endNode.getWorldPosition(endPos);
  912. let dir = new THREE.Vector3().subVectors(endPos, startPos).normalize();
  913. let r = 0.6 * window.nodeScale;
  914. let newStart = startPos.clone().add(dir.clone().multiplyScalar(r));
  915. let newEnd = endPos.clone().add(dir.clone().multiplyScalar(-r));
  916. let mid = new THREE.Vector3().addVectors(newStart, newEnd).multiplyScalar(0.5);
  917. let ctrl = mid.clone().add(line.userData.offset);
  918. let curve = new THREE.CatmullRomCurve3([newStart, ctrl, newEnd]);
  919. let points = curve.getPoints(50);
  920. line.geometry.setFromPoints(points);
  921. line.computeLineDistances();
  922. if(!startNode.visible || !endNode.visible) {{
  923. line.visible = false;
  924. }} else {{
  925. line.visible = true;
  926. }}
  927. }}
  928. line.material.dashOffset -= 0.1;
  929. line.material.opacity = 0.6 + Math.sin(Date.now()/500)*0.3;
  930. }});
  931. controls.update();
  932. renderer.render(scene, camera);
  933. }}
  934.  
  935. window.addEventListener('resize', () => {{
  936. camera.aspect = window.innerWidth/window.innerHeight;
  937. camera.updateProjectionMatrix();
  938. renderer.setSize(window.innerWidth, window.innerHeight);
  939. }});
  940. </script>
  941. </body>
  942. </html>
  943. """
  944.  
  945. if __name__ == '__main__':
  946. app = QApplication(sys.argv)
  947. window = KnowledgeGraphExplorer("ENTER_YOUR_API_HERE")
  948. window.show()
  949. sys.exit(app.exec_())
  950.  
Tags: ai
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement