Spaces:
Running
Running
Commit Β·
a1cf96b
1
Parent(s): 87b51e4
feat(ui): 3D Visualizer enhancements with node labels and seamless agent lerp animation
Browse files- static/viz3d.html +54 -13
static/viz3d.html
CHANGED
|
@@ -83,12 +83,12 @@
|
|
| 83 |
#tl-header h3 { font-size: 10px; color: #7dd3fc; letter-spacing: 0.1em; }
|
| 84 |
#step-label { font-size: 11px; color: #f0abfc; font-weight: 700; }
|
| 85 |
#slider {
|
| 86 |
-
width: 100%; -webkit-appearance: none; height: 4px;
|
| 87 |
background: linear-gradient(to right, #7dd3fc 0%, #7dd3fc var(--pct,0%), #1e293b var(--pct,0%));
|
| 88 |
border-radius: 4px; outline: none; cursor: pointer;
|
| 89 |
}
|
| 90 |
#slider::-webkit-slider-thumb {
|
| 91 |
-
-webkit-appearance: none; width: 15px; height: 15px;
|
| 92 |
border-radius: 50%; background: #7dd3fc; cursor: pointer;
|
| 93 |
box-shadow: 0 0 8px rgba(125,211,252,0.8);
|
| 94 |
}
|
|
@@ -133,11 +133,24 @@
|
|
| 133 |
text-align: center; color: #475569; font-size: 13px;
|
| 134 |
display: none;
|
| 135 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
</style>
|
| 137 |
</head>
|
| 138 |
<body>
|
| 139 |
|
| 140 |
<canvas id="three-canvas"></canvas>
|
|
|
|
| 141 |
|
| 142 |
<div id="loader"><div class="spin"></div><p>Loading 3D...</p></div>
|
| 143 |
<div id="no-data">
|
|
@@ -284,9 +297,10 @@ function updateCamera() {
|
|
| 284 |
// ββ Scene state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 285 |
const COLS = { src:0xf97316, test:0x3b82f6, spec:0xa855f7, visited:0x22c55e, bug:0xef4444, agent:0xfbbf24, path:0xfacc15, edge:0x334155 };
|
| 286 |
|
| 287 |
-
let nodeMap = {}; // filename β { mesh, basePos }
|
| 288 |
let pathLines = [], edgeLines = [];
|
| 289 |
let agentMesh = null;
|
|
|
|
| 290 |
let vizData = null;
|
| 291 |
let curStep = 0, maxStep = 0;
|
| 292 |
let playing = false, playTimer = null;
|
|
@@ -294,7 +308,10 @@ let frame = 0;
|
|
| 294 |
|
| 295 |
// ββ Build scene βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 296 |
function clearScene() {
|
| 297 |
-
Object.values(nodeMap).forEach(o =>
|
|
|
|
|
|
|
|
|
|
| 298 |
pathLines.forEach(l => scene.remove(l));
|
| 299 |
edgeLines.forEach(l => scene.remove(l));
|
| 300 |
if (agentMesh) scene.remove(agentMesh);
|
|
@@ -339,7 +356,13 @@ function buildScene(data) {
|
|
| 339 |
ring.rotation.x = Math.PI / 2;
|
| 340 |
mesh.add(ring);
|
| 341 |
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
});
|
| 344 |
|
| 345 |
// Dependency edges
|
|
@@ -443,17 +466,17 @@ function applyStep(idx) {
|
|
| 443 |
scene.add(line); pathLines.push(line);
|
| 444 |
}
|
| 445 |
|
| 446 |
-
// Move agent
|
| 447 |
if (idx > 0 && idx <= steps.length) {
|
| 448 |
const cur = steps[idx - 1];
|
| 449 |
if (cur && cur.path && nodeMap[cur.path]) {
|
| 450 |
const tp = nodeMap[cur.path].basePos;
|
| 451 |
-
|
| 452 |
} else {
|
| 453 |
-
|
| 454 |
}
|
| 455 |
} else {
|
| 456 |
-
|
| 457 |
}
|
| 458 |
|
| 459 |
updateLog(steps, idx - 1);
|
|
@@ -554,15 +577,33 @@ function animate() {
|
|
| 554 |
requestAnimationFrame(animate);
|
| 555 |
frame++;
|
| 556 |
updateCamera();
|
| 557 |
-
// Pulsing agent
|
| 558 |
if (agentMesh) {
|
|
|
|
| 559 |
const p = 1 + Math.sin(frame * 0.09) * 0.18;
|
| 560 |
agentMesh.scale.setScalar(p);
|
| 561 |
agentMesh.rotation.y += 0.04;
|
| 562 |
}
|
| 563 |
-
// Subtle node float
|
| 564 |
-
Object.values(nodeMap).forEach(({ mesh, basePos }, i) => {
|
| 565 |
-
mesh.position.y = basePos.y + Math.sin(frame * 0.018 + i * 1.1) * 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
});
|
| 567 |
renderer.render(scene, camera);
|
| 568 |
}
|
|
|
|
| 83 |
#tl-header h3 { font-size: 10px; color: #7dd3fc; letter-spacing: 0.1em; }
|
| 84 |
#step-label { font-size: 11px; color: #f0abfc; font-weight: 700; }
|
| 85 |
#slider {
|
| 86 |
+
width: 100%; -webkit-appearance: none; appearance: none; height: 4px;
|
| 87 |
background: linear-gradient(to right, #7dd3fc 0%, #7dd3fc var(--pct,0%), #1e293b var(--pct,0%));
|
| 88 |
border-radius: 4px; outline: none; cursor: pointer;
|
| 89 |
}
|
| 90 |
#slider::-webkit-slider-thumb {
|
| 91 |
+
-webkit-appearance: none; appearance: none; width: 15px; height: 15px;
|
| 92 |
border-radius: 50%; background: #7dd3fc; cursor: pointer;
|
| 93 |
box-shadow: 0 0 8px rgba(125,211,252,0.8);
|
| 94 |
}
|
|
|
|
| 133 |
text-align: center; color: #475569; font-size: 13px;
|
| 134 |
display: none;
|
| 135 |
}
|
| 136 |
+
/* Node Labels Overlay */
|
| 137 |
+
#labels-container { position: fixed; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index: 15; }
|
| 138 |
+
.node-label {
|
| 139 |
+
position: absolute; color: rgba(224,230,240,0.9);
|
| 140 |
+
font-size: 10px; font-weight: 600; padding: 3px 7px;
|
| 141 |
+
background: rgba(10,14,26,0.7); border: 1px solid rgba(125,211,252,0.25);
|
| 142 |
+
border-radius: 4px; transform: translate(-50%, -200%);
|
| 143 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
| 144 |
+
pointer-events: auto; white-space: nowrap;
|
| 145 |
+
opacity: 0; transition: opacity 0.2s, box-shadow 0.2s;
|
| 146 |
+
}
|
| 147 |
+
.node-label:hover { box-shadow: 0 0 10px rgba(125,211,252,0.6); z-index: 100; }
|
| 148 |
</style>
|
| 149 |
</head>
|
| 150 |
<body>
|
| 151 |
|
| 152 |
<canvas id="three-canvas"></canvas>
|
| 153 |
+
<div id="labels-container"></div>
|
| 154 |
|
| 155 |
<div id="loader"><div class="spin"></div><p>Loading 3D...</p></div>
|
| 156 |
<div id="no-data">
|
|
|
|
| 297 |
// ββ Scene state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 298 |
const COLS = { src:0xf97316, test:0x3b82f6, spec:0xa855f7, visited:0x22c55e, bug:0xef4444, agent:0xfbbf24, path:0xfacc15, edge:0x334155 };
|
| 299 |
|
| 300 |
+
let nodeMap = {}; // filename β { mesh, basePos, labelEl }
|
| 301 |
let pathLines = [], edgeLines = [];
|
| 302 |
let agentMesh = null;
|
| 303 |
+
let targetAgentPos = new THREE.Vector3(0, 3.5, 0); // For smooth lerp interpolation
|
| 304 |
let vizData = null;
|
| 305 |
let curStep = 0, maxStep = 0;
|
| 306 |
let playing = false, playTimer = null;
|
|
|
|
| 308 |
|
| 309 |
// ββ Build scene βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 310 |
function clearScene() {
|
| 311 |
+
Object.values(nodeMap).forEach(o => {
|
| 312 |
+
scene.remove(o.mesh);
|
| 313 |
+
if (o.labelEl && o.labelEl.parentNode) o.labelEl.parentNode.removeChild(o.labelEl);
|
| 314 |
+
});
|
| 315 |
pathLines.forEach(l => scene.remove(l));
|
| 316 |
edgeLines.forEach(l => scene.remove(l));
|
| 317 |
if (agentMesh) scene.remove(agentMesh);
|
|
|
|
| 356 |
ring.rotation.x = Math.PI / 2;
|
| 357 |
mesh.add(ring);
|
| 358 |
|
| 359 |
+
// HTML Label Overlay
|
| 360 |
+
const labelEl = document.createElement('div');
|
| 361 |
+
labelEl.className = 'node-label';
|
| 362 |
+
labelEl.textContent = f.name;
|
| 363 |
+
document.getElementById('labels-container').appendChild(labelEl);
|
| 364 |
+
|
| 365 |
+
nodeMap[f.name] = { mesh, basePos: pos.clone(), labelEl };
|
| 366 |
});
|
| 367 |
|
| 368 |
// Dependency edges
|
|
|
|
| 466 |
scene.add(line); pathLines.push(line);
|
| 467 |
}
|
| 468 |
|
| 469 |
+
// Move agent target (actual animation uses lerp in animate loop)
|
| 470 |
if (idx > 0 && idx <= steps.length) {
|
| 471 |
const cur = steps[idx - 1];
|
| 472 |
if (cur && cur.path && nodeMap[cur.path]) {
|
| 473 |
const tp = nodeMap[cur.path].basePos;
|
| 474 |
+
targetAgentPos.set(tp.x, tp.y + 1.6, tp.z);
|
| 475 |
} else {
|
| 476 |
+
targetAgentPos.set(0, 2.5, 0);
|
| 477 |
}
|
| 478 |
} else {
|
| 479 |
+
targetAgentPos.set(0, 3.5, 0);
|
| 480 |
}
|
| 481 |
|
| 482 |
updateLog(steps, idx - 1);
|
|
|
|
| 577 |
requestAnimationFrame(animate);
|
| 578 |
frame++;
|
| 579 |
updateCamera();
|
| 580 |
+
// Pulsing & moving agent (seamless flow)
|
| 581 |
if (agentMesh) {
|
| 582 |
+
agentMesh.position.lerp(targetAgentPos, 0.08); // Smooth transition
|
| 583 |
const p = 1 + Math.sin(frame * 0.09) * 0.18;
|
| 584 |
agentMesh.scale.setScalar(p);
|
| 585 |
agentMesh.rotation.y += 0.04;
|
| 586 |
}
|
| 587 |
+
// Subtle node float & Update HTML overaly positions
|
| 588 |
+
Object.values(nodeMap).forEach(({ mesh, basePos, labelEl }, i) => {
|
| 589 |
+
mesh.position.y = basePos.y + Math.sin(frame * 0.018 + i * 1.1) * 0.12;
|
| 590 |
+
|
| 591 |
+
// Project 3D vector to 2D screen space
|
| 592 |
+
if (labelEl) {
|
| 593 |
+
const pos = mesh.position.clone();
|
| 594 |
+
pos.project(camera);
|
| 595 |
+
const x = (pos.x * 0.5 + 0.5) * window.innerWidth;
|
| 596 |
+
const y = (pos.y * -0.5 + 0.5) * window.innerHeight;
|
| 597 |
+
|
| 598 |
+
// Hide if behind camera or very close to edges
|
| 599 |
+
if (pos.z > 0.99 || Math.abs(pos.x) > 1.2 || Math.abs(pos.y) > 1.2) {
|
| 600 |
+
labelEl.style.opacity = '0';
|
| 601 |
+
} else {
|
| 602 |
+
labelEl.style.left = `${x}px`;
|
| 603 |
+
labelEl.style.top = `${y}px`;
|
| 604 |
+
labelEl.style.opacity = '1';
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
});
|
| 608 |
renderer.render(scene, camera);
|
| 609 |
}
|