Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import sys
- import random
- import os
- import tempfile
- import json
- import math
- import time
- # PyQt imports
- from PyQt5.QtCore import QUrl, QObject, pyqtSlot, QTimer
- from PyQt5.QtWidgets import (
- QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
- QLineEdit, QPushButton, QSlider, QLabel
- )
- from PyQt5.QtWebEngineWidgets import QWebEngineView
- from PyQt5.QtWebChannel import QWebChannel
- # Google Generative AI
- import google.generativeai as genai
- ###############################################################################
- # Bridge for JS-Python Communication
- ###############################################################################
- class ThreeJSBridge(QObject):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.main_window = parent
- @pyqtSlot(str)
- def nodeClicked(self, node_name):
- if self.main_window:
- self.main_window.expand_node(node_name)
- @pyqtSlot(str, float, float, float)
- def updateNodePosition(self, name, x, y, z):
- if name in self.main_window.nodes:
- self.main_window.nodes[name]["x"] = x
- self.main_window.nodes[name]["y"] = y
- self.main_window.nodes[name]["z"] = z
- print(f"Updated position of {name}: ({x}, {y}, {z})")
- @pyqtSlot(str, str)
- def customNodeSelected(self, parent_name, option):
- # Options: "E" (Practical Examples), "A" (Applications), "C" (Challenges),
- # "V" (Start Evolution), "H" (Hide nodes), "S" (Show nodes)
- if self.main_window:
- if option in ["E", "A", "C"]:
- self.main_window.add_custom_child(parent_name, option)
- elif option == "V":
- self.main_window.start_evolution(parent_name)
- elif option == "H":
- self.main_window.hide_children(parent_name)
- elif option == "S":
- self.main_window.show_children(parent_name)
- @pyqtSlot(str)
- def flyToNode(self, node_name):
- if self.main_window:
- self.main_window.fly_to_node(node_name)
- ###############################################################################
- # Main Application Class
- ###############################################################################
- class KnowledgeGraphExplorer(QMainWindow):
- def __init__(self, api_key):
- super().__init__()
- self.setWindowTitle("3D Knowledge Graph Explorer")
- genai.configure(api_key=api_key)
- self.model = genai.GenerativeModel('gemini-2.0-flash')
- # Each node: { x, y, z, expanded, broad, future, (parent), (color), (hidden),
- # (evolving), (growthStart), (growthDuration) }
- self.nodes = {}
- self.edges = [] # list of (node1, node2)
- self.scene_loaded = False
- self.temp_dir = tempfile.mkdtemp()
- self.html_path = os.path.join(self.temp_dir, 'graph.html')
- # Evolution process variables:
- self.evolution_timer = None
- self.evolving_root = None # the node (by name) where evolution started
- self.evolution_interval = 5000 # in ms (default 5 sec)
- self.evolution_grow_speed = 100 # slider value, default 100 -> 5000ms growth duration
- self.init_ui()
- self.setup_web_channel()
- def init_ui(self):
- main_widget = QWidget()
- self.setCentralWidget(main_widget)
- layout = QVBoxLayout(main_widget)
- # Top control panel: input, sliders, and Stop Evolution button
- top_layout = QHBoxLayout()
- self.word_edit = QLineEdit()
- self.word_edit.setPlaceholderText("Enter concept to explore")
- self.word_edit.setStyleSheet("""
- QLineEdit { color: #fff; background: #2a2a2a; border: 1px solid #3a3a3a;
- border-radius: 4px; padding: 8px; font-size: 14px; }
- """)
- self.word_edit.returnPressed.connect(self.on_explore)
- top_layout.addWidget(self.word_edit)
- self.explore_button = QPushButton("Explore")
- self.explore_button.setStyleSheet("""
- QPushButton { color: #fff; background: #3a3a3a; border: none; border-radius: 4px;
- padding: 8px 16px; font-size: 14px; }
- QPushButton:hover { background: #4a4a4a; }
- """)
- self.explore_button.clicked.connect(self.on_explore)
- top_layout.addWidget(self.explore_button)
- # Node size slider
- self.size_slider = QSlider()
- self.size_slider.setOrientation(1)
- self.size_slider.setMinimum(1)
- self.size_slider.setMaximum(200)
- self.size_slider.setValue(100)
- self.size_slider.valueChanged.connect(self.on_size_change)
- size_label = QLabel("Node Size")
- size_label.setStyleSheet("color: #fff;")
- top_layout.addWidget(size_label)
- top_layout.addWidget(self.size_slider)
- # Orbit speed slider (min 0)
- self.orbit_slider = QSlider()
- self.orbit_slider.setOrientation(1)
- self.orbit_slider.setMinimum(0)
- self.orbit_slider.setMaximum(100)
- self.orbit_slider.setValue(10)
- self.orbit_slider.valueChanged.connect(self.on_orbit_change)
- orbit_label = QLabel("Orbit Speed")
- orbit_label.setStyleSheet("color: #fff;")
- top_layout.addWidget(orbit_label)
- top_layout.addWidget(self.orbit_slider)
- # Evolution Grow Speed slider
- self.evolution_grow_slider = QSlider()
- self.evolution_grow_slider.setOrientation(1)
- self.evolution_grow_slider.setMinimum(1)
- self.evolution_grow_slider.setMaximum(200)
- self.evolution_grow_slider.setValue(100)
- self.evolution_grow_slider.valueChanged.connect(self.on_evolution_grow_change)
- grow_label = QLabel("Evolution Grow Speed")
- grow_label.setStyleSheet("color: #fff;")
- top_layout.addWidget(grow_label)
- top_layout.addWidget(self.evolution_grow_slider)
- # Evolution Timer slider (in seconds)
- self.evolution_timer_slider = QSlider()
- self.evolution_timer_slider.setOrientation(1)
- self.evolution_timer_slider.setMinimum(1)
- self.evolution_timer_slider.setMaximum(30)
- self.evolution_timer_slider.setValue(5)
- self.evolution_timer_slider.valueChanged.connect(self.on_evolution_timer_change)
- timer_label = QLabel("Evolution Timer (sec)")
- timer_label.setStyleSheet("color: #fff;")
- top_layout.addWidget(timer_label)
- top_layout.addWidget(self.evolution_timer_slider)
- # Stop Evolution button
- self.stop_evolution_button = QPushButton("Stop Evolution")
- self.stop_evolution_button.setStyleSheet("""
- QPushButton { color: #fff; background: #AA0000; border: none; border-radius: 4px;
- padding: 8px 16px; font-size: 14px; }
- QPushButton:hover { background: #CC0000; }
- """)
- self.stop_evolution_button.clicked.connect(self.stop_evolution)
- top_layout.addWidget(self.stop_evolution_button)
- layout.addLayout(top_layout)
- # 3D visualization canvas
- self.web_view = QWebEngineView()
- self.web_view.setStyleSheet("background: #000;")
- layout.addWidget(self.web_view)
- self.resize(1280, 800)
- self.setStyleSheet("background: #1a1a1a; color: #fff;")
- def on_evolution_grow_change(self, value):
- # Store slider value; growthDuration = 5000*(100/value) ms.
- self.evolution_grow_speed = value
- js = f"updateEvolutionGrowSpeed({value});"
- self.web_view.page().runJavaScript(js)
- def on_evolution_timer_change(self, value):
- # Update evolution timer interval in ms.
- self.evolution_interval = value * 1000
- js = f"updateEvolutionTimer({value});"
- self.web_view.page().runJavaScript(js)
- if self.evolution_timer and self.evolving_root:
- # Restart timer with new interval.
- self.evolution_timer.stop()
- self.evolution_timer.start(self.evolution_interval)
- print(f"Evolution timer updated to {value} sec.")
- def setup_web_channel(self):
- self.channel = QWebChannel()
- self.bridge = ThreeJSBridge(self)
- self.channel.registerObject("bridge", self.bridge)
- self.web_view.page().setWebChannel(self.channel)
- def on_size_change(self, value):
- scale = value / 100.0
- js = f"updateNodeScale({scale});"
- self.web_view.page().runJavaScript(js)
- def on_orbit_change(self, value):
- speed = value / 1000.0
- js = f"updateOrbitSpeed({speed});"
- self.web_view.page().runJavaScript(js)
- def query_llm(self, word):
- existing_words = list(self.nodes.keys())
- prompt = (
- f"Generate concepts related to '{word}' as 4–6 comma-separated items.\n"
- "Requirements:\n"
- "1) Exactly one concept prefixed with 'B:' (a broader topic).\n"
- "2) Exactly one concept prefixed with 'F:' (a future evolution topic).\n"
- "3) The remaining concepts are regular.\n"
- "Do NOT include any of these existing concepts: "
- f"{', '.join(existing_words) if existing_words else '(none)'}\n"
- )
- try:
- response = self.model.generate_content(prompt)
- return [w.strip() for w in response.text.split(',') if w.strip()]
- except Exception as e:
- print(f"LLM Error: {e}")
- return []
- def query_custom_llm(self, parent, option):
- existing_words = [w.lower() for w in self.nodes.keys()]
- if option == "E":
- req = "a practical example"
- elif option == "A":
- req = "an application"
- elif option == "C":
- req = "a challenge"
- else:
- req = "a related concept"
- prompt = (
- f"Generate one single one-word concept that is {req} for the concept '{parent}'.\n"
- f"Ensure the new concept is directly related to '{parent}' and does not duplicate any concept already present.\n"
- "Return only one word, with no extra text.\n"
- "Do NOT include any existing concept: "
- f"{', '.join(existing_words) if existing_words else '(none)'}."
- )
- try:
- response = self.model.generate_content(prompt)
- word = response.text.strip().split()[0]
- if word.lower() in existing_words:
- return None
- return word
- except Exception as e:
- print(f"Custom LLM Error: {e}")
- return None
- def add_node(self, word, pos, broad=False, future=False, parent=None, color=None, evolving=False, growthDuration=None):
- if word not in self.nodes:
- self.nodes[word] = {
- "x": pos[0],
- "y": pos[1],
- "z": pos[2],
- "expanded": False,
- "broad": broad,
- "future": future
- }
- if parent:
- self.nodes[word]["parent"] = parent
- if color:
- self.nodes[word]["color"] = color
- if evolving:
- self.nodes[word]["evolving"] = True
- self.nodes[word]["growthStart"] = int(time.time()*1000)
- self.nodes[word]["growthDuration"] = growthDuration if growthDuration else 5000
- # No duplicates are added.
- def add_edge(self, w1, w2):
- if (w1, w2) not in self.edges and (w2, w1) not in self.edges:
- self.edges.append((w1, w2))
- def expand_node(self, word):
- if word not in self.nodes or self.nodes[word]["expanded"]:
- return
- self.nodes[word]["expanded"] = True
- new_concepts = self.query_llm(word)
- px, py, pz = self.nodes[word]["x"], self.nodes[word]["y"], self.nodes[word]["z"]
- radius = 3
- for c in new_concepts:
- if c in self.nodes:
- continue
- is_broad = False
- is_future = False
- concept_str = c
- if c.lower().startswith("b:"):
- is_broad = True
- concept_str = c[2:].strip()
- elif c.lower().startswith("f:"):
- is_future = True
- concept_str = c[2:].strip()
- theta = random.uniform(0, 2 * math.pi)
- phi = random.uniform(0, math.pi)
- nx = px + radius * math.sin(phi) * math.cos(theta)
- ny = py + radius * math.sin(phi) * math.sin(theta)
- nz = pz + radius * math.cos(phi)
- self.add_node(concept_str, (nx, ny, nz), broad=is_broad, future=is_future, parent=word)
- self.add_edge(word, concept_str)
- self.update_visualization(initial=False)
- def add_custom_child(self, parent_name, option):
- if parent_name not in self.nodes:
- return
- new_word = self.query_custom_llm(parent_name, option)
- if not new_word:
- print("Duplicate or invalid custom concept.")
- return
- parent = self.nodes[parent_name]
- radius = 3
- theta = random.uniform(0, 2 * math.pi)
- phi = random.uniform(0, math.pi)
- nx = parent["x"] + radius * math.sin(phi) * math.cos(theta)
- ny = parent["y"] + radius * math.sin(phi) * math.sin(theta)
- nz = parent["z"] + radius * math.cos(phi)
- if option == "E":
- color = "#00FF00" # green for practical examples
- elif option == "A":
- color = "#808080" # gray for applications
- elif option == "C":
- color = "#FF0000" # red for challenges
- else:
- color = "#FFFFFF"
- self.add_node(new_word, (nx, ny, nz), color=color, parent=parent_name)
- self.add_edge(parent_name, new_word)
- self.update_visualization(initial=False)
- def hide_children(self, parent_name):
- for word, data in self.nodes.items():
- if data.get("parent") == parent_name:
- data["hidden"] = True
- self.update_visualization(initial=False)
- def show_children(self, parent_name):
- for word, data in self.nodes.items():
- if data.get("parent") == parent_name:
- data["hidden"] = False
- self.update_visualization(initial=False)
- def start_evolution(self, parent_name):
- if parent_name not in self.nodes:
- return
- self.stop_evolution() # Stop any existing evolution
- self.evolving_root = parent_name
- self.evolution_timer = QTimer(self)
- self.evolution_timer.timeout.connect(lambda: self.evolve_random_descendant(self.evolving_root))
- self.evolution_timer.start(self.evolution_interval)
- print(f"Evolution started on branch: {parent_name}")
- def stop_evolution(self):
- if self.evolution_timer:
- self.evolution_timer.stop()
- self.evolution_timer = None
- print("Evolution stopped.")
- self.evolving_root = None
- def get_descendants(self, root):
- descendants = []
- for word, data in self.nodes.items():
- current = data.get("parent")
- while current:
- if current == root:
- descendants.append(word)
- break
- current = self.nodes.get(current, {}).get("parent")
- descendants.append(root)
- return descendants
- def evolve_random_descendant(self, root):
- descendants = self.get_descendants(root)
- if not descendants:
- return
- random_node = random.choice(descendants)
- if random_node in self.nodes and not self.nodes[random_node].get("hidden", False):
- existing_words = [w.lower() for w in self.nodes.keys()]
- prompt = (
- f"Generate one single one-word concept that is evolutionarily related to '{random_node}'.\n"
- "Return only one word, with no extra text.\n"
- "Do NOT include any concept already present: "
- f"{', '.join(existing_words) if existing_words else '(none)'}."
- )
- try:
- response = self.model.generate_content(prompt)
- new_word = response.text.strip().split()[0]
- if new_word.lower() in existing_words:
- print("Evolution: duplicate concept; not added.")
- return
- except Exception as e:
- print(f"Evolution LLM Error: {e}")
- return
- parent = self.nodes[random_node]
- radius = 3
- theta = random.uniform(0, 2 * math.pi)
- phi = random.uniform(0, math.pi)
- nx = parent["x"] + radius * math.sin(phi) * math.cos(theta)
- ny = parent["y"] + radius * math.sin(phi) * math.sin(theta)
- nz = parent["z"] + radius * math.cos(phi)
- growthDuration = int(5000 * (100 / self.evolution_grow_speed))
- self.add_node(new_word, (nx, ny, nz), parent=random_node, color="#2196F3",
- evolving=True, growthDuration=growthDuration)
- self.add_edge(random_node, new_word)
- self.update_visualization(initial=False)
- print(f"Evolution added node: {new_word} under {random_node}")
- self.fly_to_node(new_word)
- def fly_to_node(self, word):
- js = f"flyCameraToNode('{word}');"
- self.web_view.page().runJavaScript(js)
- def on_explore(self):
- concept = self.word_edit.text().strip()
- if concept:
- self.nodes.clear()
- self.edges.clear()
- self.add_node(concept, (0, 0, 0))
- self.expand_node(concept)
- self.update_visualization(initial=True)
- def generate_scene_data(self):
- scene_nodes = []
- for word, data in self.nodes.items():
- node = {
- "name": word,
- "x": data["x"],
- "y": data["y"],
- "z": data["z"],
- "color": data.get("color", (
- "#FFFF00" if data.get("broad", False) else (
- "#800080" if data.get("future", False) else (
- "#4CAF50" if data["expanded"] and not data.get("hidden", False) else "#2196F3"
- )
- )
- ))
- }
- if "parent" in data:
- node["parent"] = data["parent"]
- node["hidden"] = data.get("hidden", False)
- if data.get("evolving", False):
- node["evolving"] = True
- node["growthStart"] = data.get("growthStart", 0)
- node["growthDuration"] = data.get("growthDuration", 5000)
- scene_nodes.append(node)
- return {
- "nodes": scene_nodes,
- "edges": [
- {
- "startName": w1,
- "endName": w2,
- "x1": self.nodes[w1]["x"],
- "y1": self.nodes[w1]["y"],
- "z1": self.nodes[w1]["z"],
- "x2": self.nodes[w2]["x"],
- "y2": self.nodes[w2]["y"],
- "z2": self.nodes[w2]["z"],
- "color": f"hsl({(i * 30) % 360}, 80%, 60%)"
- }
- for i, (w1, w2) in enumerate(self.edges)
- ]
- }
- def update_visualization(self, initial=False):
- scene_data = self.generate_scene_data()
- if initial or not self.scene_loaded:
- html_content = self.generate_threejs_html(scene_data)
- with open(self.html_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
- self.web_view.setUrl(QUrl.fromLocalFile(self.html_path))
- self.scene_loaded = True
- else:
- js_command = "updateSceneData(" + json.dumps(scene_data) + ")"
- self.web_view.page().runJavaScript(js_command)
- def generate_threejs_html(self, scene_data):
- # The animate loop now also checks for evolving nodes.
- return f"""
- <!DOCTYPE html>
- <html>
- <head>
- <style>
- body {{ margin: 0; overflow: hidden; background: #000; }}
- canvas {{ width: 100%; height: 100%; }}
- /* Context Menu Styling */
- #contextMenu {{
- position: absolute;
- display: none;
- background: #333;
- color: #fff;
- padding: 5px;
- border: 1px solid #888;
- z-index: 1000;
- font-family: sans-serif;
- }}
- #contextMenu .menuItem {{
- padding: 5px 10px;
- cursor: pointer;
- }}
- #contextMenu .menuItem:hover {{
- background: #555;
- }}
- </style>
- </head>
- <body>
- <!-- Custom Context Menu -->
- <div id="contextMenu">
- <div class="menuItem" data-option="E">Practical Examples</div>
- <div class="menuItem" data-option="A">Applications</div>
- <div class="menuItem" data-option="C">Challenges</div>
- <div class="menuItem" data-option="V">Start Evolution</div>
- <div class="menuItem" data-option="H">Hide nodes</div>
- <div class="menuItem" data-option="S">Show nodes</div>
- </div>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/DragControls.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/geometries/TextGeometry.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/FontLoader.js"></script>
- <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
- <script>
- let scene, camera, renderer, controls, font;
- let nodes = [];
- let draggableNodes = [];
- let edges = [];
- let dragControls;
- let isDragging = false;
- window.nodeScale = 1.0;
- window.orbitSpeed = 0.01;
- window.evolutionGrowSlider = 100; // Default evolution grow speed slider value
- // Evolution timer in seconds; slider value (default 5 sec)
- window.evolutionIntervalSec = 5;
- const sceneData = {json.dumps(scene_data)};
- // Helper: rotate vector v around Y axis by angle
- function rotateY(v, angle) {{
- let cos = Math.cos(angle);
- let sin = Math.sin(angle);
- return new THREE.Vector3(v.x * cos - v.z * sin, v.y, v.x * sin + v.z * cos);
- }}
- function findNodeByName(name) {{
- return nodes.find(m => m.userData.name === name);
- }}
- new THREE.FontLoader().load(
- 'https://cdn.jsdelivr.net/npm/[email protected]/examples/fonts/helvetiker_regular.typeface.json',
- function(loadedFont) {{
- font = loadedFont;
- initScene();
- animate();
- }}
- );
- function createNode(n) {{
- const geometry = new THREE.SphereGeometry(0.6, 32, 32);
- const material = new THREE.MeshPhongMaterial({{
- color: n.color,
- emissive: n.color,
- emissiveIntensity: 0.3,
- transparent: true,
- opacity: 0.9
- }});
- const mesh = new THREE.Mesh(geometry, material);
- mesh.position.set(n.x, n.y, n.z);
- mesh.scale.set(window.nodeScale, window.nodeScale, window.nodeScale);
- const textGeo = new THREE.TextGeometry(n.name, {{
- font: font,
- size: 0.3,
- height: 0.05,
- curveSegments: 2
- }});
- textGeo.computeBoundingBox();
- const tw = textGeo.boundingBox.max.x - textGeo.boundingBox.min.x;
- const th = textGeo.boundingBox.max.y - textGeo.boundingBox.min.y;
- const textMat = new THREE.MeshBasicMaterial({{ color: '#ffffff' }});
- const textMesh = new THREE.Mesh(textGeo, textMat);
- textMesh.position.set(-tw/2, -th/2, 0.7);
- mesh.add(textMesh);
- mesh.userData.name = n.name;
- mesh.userData.textMesh = textMesh;
- mesh.userData.hidden = n.hidden || false;
- mesh.visible = !mesh.userData.hidden;
- if(n.parent) {{
- mesh.userData.parent = n.parent;
- let parentMesh = findNodeByName(n.parent);
- if(parentMesh) {{
- let parentPos = new THREE.Vector3();
- parentMesh.getWorldPosition(parentPos);
- mesh.userData.localOffset = mesh.position.clone().sub(parentPos);
- }} else {{
- mesh.userData.localOffset = mesh.position.clone();
- }}
- }}
- if(n.evolving) {{
- mesh.userData.evolving = true;
- mesh.userData.growthStart = n.growthStart;
- mesh.userData.growthDuration = n.growthDuration;
- mesh.scale.set(0.1 * window.nodeScale, 0.1 * window.nodeScale, 0.1 * window.nodeScale);
- }}
- return mesh;
- }}
- function createEdge(e) {{
- const startNode = findNodeByName(e.startName);
- const endNode = findNodeByName(e.endName);
- if (!startNode || !endNode) return null;
- let startPos = new THREE.Vector3(), endPos = new THREE.Vector3();
- startNode.getWorldPosition(startPos);
- endNode.getWorldPosition(endPos);
- let dir = new THREE.Vector3().subVectors(endPos, startPos).normalize();
- let r = 0.6 * window.nodeScale;
- let newStart = startPos.clone().add(dir.clone().multiplyScalar(r));
- let newEnd = endPos.clone().add(dir.clone().multiplyScalar(-r));
- let mid = new THREE.Vector3().addVectors(newStart, newEnd).multiplyScalar(0.5);
- let offset = new THREE.Vector3((Math.random()-0.5)*2, (Math.random()-0.5)*2, (Math.random()-0.5)*2);
- let ctrl = mid.clone().add(offset);
- let curve = new THREE.CatmullRomCurve3([newStart, ctrl, newEnd]);
- let points = curve.getPoints(50);
- let geom = new THREE.BufferGeometry().setFromPoints(points);
- let mat = new THREE.LineDashedMaterial({{
- color: e.color,
- dashSize: 0.25,
- gapSize: 0.15,
- linewidth: 1.5,
- transparent: true,
- opacity: 0.7
- }});
- let line = new THREE.Line(geom, mat);
- line.computeLineDistances();
- line.userData = {{
- startName: e.startName,
- endName: e.endName,
- offset: offset
- }};
- return line;
- }}
- function initScene() {{
- scene = new THREE.Scene();
- camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1e10);
- camera.position.z = 60;
- renderer = new THREE.WebGLRenderer({{ antialias: true, alpha: true }});
- renderer.setSize(window.innerWidth, window.innerHeight);
- document.body.appendChild(renderer.domElement);
- scene.add(new THREE.AmbientLight(0x404040, 2));
- let dirLight = new THREE.DirectionalLight(0xffffff, 1);
- dirLight.position.set(10, 10, 10);
- scene.add(dirLight);
- scene.fog = new THREE.Fog(0x000000, 1000, 1e10);
- sceneData.nodes.forEach(n => {{
- const mesh = createNode(n);
- scene.add(mesh);
- draggableNodes.push(mesh);
- nodes.push(mesh);
- }});
- sceneData.edges.forEach(e => {{
- const line = createEdge(e);
- if(line) {{
- scene.add(line);
- edges.push(line);
- }}
- }});
- controls = new THREE.OrbitControls(camera, renderer.domElement);
- controls.enableDamping = true;
- controls.dampingFactor = 0.05;
- controls.minDistance = 0;
- controls.maxDistance = Infinity;
- dragControls = new THREE.DragControls(draggableNodes, camera, renderer.domElement);
- dragControls.addEventListener('dragstart', event => {{
- controls.enabled = false;
- event.object.userData.dragging = true;
- isDragging = true;
- }});
- dragControls.addEventListener('dragend', event => {{
- controls.enabled = true;
- event.object.userData.dragging = false;
- if(event.object.userData.parent) {{
- let parentMesh = findNodeByName(event.object.userData.parent);
- if(parentMesh) {{
- let parentPos = new THREE.Vector3();
- parentMesh.getWorldPosition(parentPos);
- event.object.userData.localOffset = event.object.position.clone().sub(parentPos);
- }}
- }}
- let obj = event.object;
- let worldPos = new THREE.Vector3();
- obj.getWorldPosition(worldPos);
- new QWebChannel(qt.webChannelTransport, channel => {{
- channel.objects.bridge.updateNodePosition(obj.userData.name, worldPos.x, worldPos.y, worldPos.z);
- }});
- setTimeout(() => {{ isDragging = false; }}, 100);
- }});
- // Middle mouse: fly-to node and zoom in (offset 5).
- renderer.domElement.addEventListener('auxclick', event => {{
- if(event.button === 1) {{
- const mouse = new THREE.Vector2();
- mouse.x = (event.clientX/window.innerWidth)*2 - 1;
- mouse.y = -(event.clientY/window.innerHeight)*2 + 1;
- const raycaster = new THREE.Raycaster();
- raycaster.setFromCamera(mouse, camera);
- const intersects = raycaster.intersectObjects(nodes, true);
- if(intersects.length > 0) {{
- let obj = intersects[0].object;
- while(obj.parent && !obj.userData.name) {{
- obj = obj.parent;
- }}
- if(obj.userData.name) {{
- new QWebChannel(qt.webChannelTransport, channel => {{
- channel.objects.bridge.flyToNode(obj.userData.name);
- }});
- }}
- }}
- }}
- }});
- // Right-click: show context menu.
- renderer.domElement.addEventListener('contextmenu', event => {{
- event.preventDefault();
- const mouse = new THREE.Vector2();
- mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
- mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
- const raycaster = new THREE.Raycaster();
- raycaster.setFromCamera(mouse, camera);
- const intersects = raycaster.intersectObjects(nodes, true);
- if(intersects.length > 0) {{
- let target = intersects[0].object;
- while(!target.userData.name && target.parent) {{
- target = target.parent;
- }}
- if(target.userData.name) {{
- let menu = document.getElementById("contextMenu");
- menu.style.left = event.clientX + "px";
- menu.style.top = event.clientY + "px";
- menu.style.display = "block";
- menu.setAttribute("data-parent", target.userData.name);
- }}
- }}
- }});
- window.addEventListener('click', () => {{
- document.getElementById("contextMenu").style.display = "none";
- }});
- document.querySelectorAll("#contextMenu .menuItem").forEach(item => {{
- item.addEventListener('click', event => {{
- const option = event.target.getAttribute("data-option");
- const menu = document.getElementById("contextMenu");
- const parentName = menu.getAttribute("data-parent");
- new QWebChannel(qt.webChannelTransport, channel => {{
- channel.objects.bridge.customNodeSelected(parentName, option);
- }});
- menu.style.display = "none";
- }});
- }});
- // Left-click: regular expansion.
- const raycaster = new THREE.Raycaster();
- const mouse = new THREE.Vector2();
- window.addEventListener('click', event => {{
- if(isDragging) return;
- mouse.x = (event.clientX/window.innerWidth)*2 - 1;
- mouse.y = -(event.clientY/window.innerHeight)*2 + 1;
- raycaster.setFromCamera(mouse, camera);
- const intersects = raycaster.intersectObjects(nodes, true);
- if(intersects.length > 0) {{
- let obj = intersects[0].object;
- while(obj.parent && !obj.userData.name) {{
- obj = obj.parent;
- }}
- if(obj.userData.name) {{
- new QWebChannel(qt.webChannelTransport, channel => {{
- channel.objects.bridge.nodeClicked(obj.userData.name);
- }});
- }}
- }}
- }});
- }}
- function updateSceneData(newData) {{
- newData.nodes.forEach(n => {{
- let existing = nodes.find(m => m.userData.name === n.name);
- if(!existing) {{
- let mesh = createNode(n);
- scene.add(mesh);
- draggableNodes.push(mesh);
- nodes.push(mesh);
- }} else {{
- existing.userData.hidden = n.hidden;
- existing.visible = !n.hidden;
- if(n.evolving) {{
- existing.userData.evolving = true;
- existing.userData.growthStart = n.growthStart;
- existing.userData.growthDuration = n.growthDuration;
- }} else {{
- existing.userData.evolving = false;
- }}
- }}
- }});
- newData.edges.forEach(e => {{
- let exists = edges.some(line =>
- line.userData.startName === e.startName &&
- line.userData.endName === e.endName
- );
- if(!exists) {{
- let line = createEdge(e);
- if(line) {{
- scene.add(line);
- edges.push(line);
- }}
- }}
- }});
- dragControls.objects = draggableNodes;
- }}
- // Fly-to animation with closer zoom (offset 5).
- function flyCameraToNode(nodeName) {{
- let targetMesh = nodes.find(m => m.userData.name === nodeName);
- if(!targetMesh) return;
- let targetPos = new THREE.Vector3();
- targetMesh.getWorldPosition(targetPos);
- let startPos = camera.position.clone();
- let duration = 500;
- let startTime = performance.now();
- function animateFly() {{
- let now = performance.now();
- let t = Math.min((now - startTime) / duration, 1);
- let ease = t * (2 - t);
- camera.position.lerpVectors(startPos, targetPos.clone().add(new THREE.Vector3(0, 0, 5)), ease);
- controls.target.lerp(targetPos, ease);
- controls.update();
- if(t < 1) {{
- requestAnimationFrame(animateFly);
- }}
- }}
- animateFly();
- }}
- function updateNodeScale(scale) {{
- window.nodeScale = scale;
- nodes.forEach(mesh => {{
- mesh.scale.set(scale, scale, scale);
- }});
- }}
- function updateOrbitSpeed(speed) {{
- window.orbitSpeed = speed;
- }}
- // Evolution grow speed update (can be used in growth interpolation)
- function updateEvolutionGrowSpeed(value) {{
- window.evolutionGrowSlider = value;
- }}
- // Evolution timer update (for display, if needed)
- function updateEvolutionTimer(value) {{
- window.evolutionIntervalSec = value;
- }}
- function animate() {{
- requestAnimationFrame(animate);
- scene.updateMatrixWorld(true);
- let currentTime = Date.now();
- nodes.forEach(mesh => {{
- if(mesh.userData.textMesh) {{
- mesh.userData.textMesh.lookAt(camera.position);
- }}
- if(mesh.userData.parent && !mesh.userData.dragging) {{
- let parentMesh = nodes.find(m => m.userData.name === mesh.userData.parent);
- if(parentMesh) {{
- let parentPos = new THREE.Vector3();
- parentMesh.getWorldPosition(parentPos);
- if(!mesh.userData.localOffset) {{
- mesh.userData.localOffset = mesh.position.clone().sub(parentPos);
- }}
- let newOffset = rotateY(mesh.userData.localOffset, window.orbitSpeed);
- mesh.userData.localOffset = newOffset;
- let newPos = parentPos.clone().add(newOffset);
- mesh.position.copy(newPos);
- }}
- }}
- // Animate evolving nodes (growth)
- if(mesh.userData.evolving) {{
- let elapsed = currentTime - mesh.userData.growthStart;
- let progress = Math.min(elapsed / mesh.userData.growthDuration, 1);
- let newScale = 0.1 * window.nodeScale + progress * (window.nodeScale - 0.1 * window.nodeScale);
- mesh.scale.set(newScale, newScale, newScale);
- if(progress >= 1) {{
- mesh.userData.evolving = false;
- }}
- }}
- }});
- edges.forEach(line => {{
- const startNode = nodes.find(m => m.userData.name === line.userData.startName);
- const endNode = nodes.find(m => m.userData.name === line.userData.endName);
- if(startNode && endNode) {{
- let startPos = new THREE.Vector3(), endPos = new THREE.Vector3();
- startNode.getWorldPosition(startPos);
- endNode.getWorldPosition(endPos);
- let dir = new THREE.Vector3().subVectors(endPos, startPos).normalize();
- let r = 0.6 * window.nodeScale;
- let newStart = startPos.clone().add(dir.clone().multiplyScalar(r));
- let newEnd = endPos.clone().add(dir.clone().multiplyScalar(-r));
- let mid = new THREE.Vector3().addVectors(newStart, newEnd).multiplyScalar(0.5);
- let ctrl = mid.clone().add(line.userData.offset);
- let curve = new THREE.CatmullRomCurve3([newStart, ctrl, newEnd]);
- let points = curve.getPoints(50);
- line.geometry.setFromPoints(points);
- line.computeLineDistances();
- if(!startNode.visible || !endNode.visible) {{
- line.visible = false;
- }} else {{
- line.visible = true;
- }}
- }}
- line.material.dashOffset -= 0.1;
- line.material.opacity = 0.6 + Math.sin(Date.now()/500)*0.3;
- }});
- controls.update();
- renderer.render(scene, camera);
- }}
- window.addEventListener('resize', () => {{
- camera.aspect = window.innerWidth/window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(window.innerWidth, window.innerHeight);
- }});
- </script>
- </body>
- </html>
- """
- if __name__ == '__main__':
- app = QApplication(sys.argv)
- window = KnowledgeGraphExplorer("ENTER_YOUR_API_HERE")
- window.show()
- sys.exit(app.exec_())
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement