Monorepo for Aesthetic.Computer aesthetic.computer
at main 1018 lines 34 kB view raw
1// 3D Source Code Visualization 2// Renders JavaScript/TypeScript files as interactive 3D trees with click navigation 3 4(function() { 5 'use strict'; 6 7 // Wait for ProcessTreeViz if it's not ready yet 8 if (!window.ProcessTreeViz) { 9 console.log('⏳ Waiting for ProcessTreeViz...'); 10 const checkInterval = setInterval(() => { 11 if (window.ProcessTreeViz) { 12 clearInterval(checkInterval); 13 initASTVisualization(); 14 } 15 }, 100); 16 return; 17 } else { 18 initASTVisualization(); 19 } 20 21 function initASTVisualization() { 22 console.log('🚀 Initializing Source Tree Visualization'); 23 24 const { scene, camera, renderer, colorSchemes } = window.ProcessTreeViz; 25 // Update theme reference dynamically as it might change 26 let scheme = window.ProcessTreeViz.getScheme(); 27 28 // ... rest of initialization ... 29 // Tab system state 30 let currentTab = 'processes'; // 'processes' or 'sources' 31 let sourcesVisible = false; 32 33 // Source visualization state 34 const sourceFiles = new Map(); // fileName -> { rootMesh, nodes: Map<id, mesh>, connections: [] } 35 const sourceConnections = new Map(); 36 let astFiles = []; 37 let focusedSourceNode = null; 38 let hoveredNode = null; 39 40 // Raycaster for click detection 41 const raycaster = new THREE.Raycaster(); 42 const mouse = new THREE.Vector2(); 43 44 // Icon mapping for node types 45 const nodeIcons = { 46 47 Program: '📄', 48 FunctionDeclaration: '🔧', 49 FunctionExpression: '🔧', 50 ArrowFunctionExpression: '➡️', 51 AsyncFunctionDeclaration: '⚡', 52 AsyncArrowFunctionExpression: '⚡', 53 ClassDeclaration: '🏛️', 54 ClassExpression: '🏛️', 55 MethodDefinition: '🔩', 56 VariableDeclaration: '📦', 57 VariableDeclarator: '📦', 58 ImportDeclaration: '📥', 59 ImportSpecifier: '📥', 60 ExportNamedDeclaration: '📤', 61 ExportDefaultDeclaration: '📤', 62 CallExpression: '📞', 63 NewExpression: '🆕', 64 MemberExpression: '🔗', 65 Identifier: '🏷️', 66 Literal: '✨', 67 StringLiteral: '💬', 68 NumericLiteral: '🔢', 69 ObjectExpression: '{}', 70 ObjectPattern: '{}', 71 ArrayExpression: '[]', 72 ArrayPattern: '[]', 73 IfStatement: '❓', 74 ConditionalExpression: '❓', 75 ForStatement: '🔄', 76 ForOfStatement: '🔄', 77 ForInStatement: '🔄', 78 WhileStatement: '🔁', 79 DoWhileStatement: '🔁', 80 SwitchStatement: '🔀', 81 TryStatement: '🛡️', 82 CatchClause: '🎣', 83 ThrowStatement: '💥', 84 ReturnStatement: '↩️', 85 AwaitExpression: '⏳', 86 YieldExpression: '🌾', 87 SpreadElement: '...', 88 TemplateLiteral: '📝', 89 BlockStatement: '📦', 90 Property: '🔑', 91 AssignmentExpression: '=', 92 BinaryExpression: '➕', 93 LogicalExpression: '🧮', 94 UnaryExpression: '!', 95 UpdateExpression: '++', 96 SequenceExpression: ',', 97 ExpressionStatement: '💭', 98 }; 99 100 // Color mapping for node types (more vibrant) 101 const nodeColors = { 102 Program: 0x88ccff, 103 FunctionDeclaration: 0xff6b9f, 104 FunctionExpression: 0xff6b9f, 105 ArrowFunctionExpression: 0xff8faf, 106 AsyncFunctionDeclaration: 0xffaf6b, 107 ClassDeclaration: 0xb06bff, 108 ClassExpression: 0xb06bff, 109 MethodDefinition: 0xd080ff, 110 VariableDeclaration: 0x6bff9f, 111 VariableDeclarator: 0x50d080, 112 ImportDeclaration: 0xffeb6b, 113 ImportSpecifier: 0xffd040, 114 ExportNamedDeclaration: 0xffc040, 115 ExportDefaultDeclaration: 0xffa030, 116 CallExpression: 0x6bb4ff, 117 NewExpression: 0x80c0ff, 118 MemberExpression: 0x5090d0, 119 Identifier: 0x9999aa, 120 Literal: 0x77aa77, 121 StringLiteral: 0x88cc88, 122 NumericLiteral: 0x88aacc, 123 ObjectExpression: 0x6bffff, 124 ArrayExpression: 0x50d0d0, 125 IfStatement: 0xff9f6b, 126 ConditionalExpression: 0xffaf80, 127 ForStatement: 0xe08050, 128 ForOfStatement: 0xe09060, 129 WhileStatement: 0xd07040, 130 SwitchStatement: 0xc06030, 131 TryStatement: 0x60c0a0, 132 CatchClause: 0x50b090, 133 ReturnStatement: 0x80ff80, 134 AwaitExpression: 0xffd080, 135 BlockStatement: 0x555566, 136 ExpressionStatement: 0x444455, 137 Property: 0x8899aa, 138 }; 139 140 // Get display name for a node (actual code identifier, not generic type) 141 function getNodeDisplayName(node) { 142 // Priority: actual identifier names 143 if (node.name && node.name !== node.type) return node.name; 144 145 // For different node types, try to extract meaningful names 146 switch (node.type) { 147 case 'FunctionDeclaration': 148 case 'FunctionExpression': 149 case 'ArrowFunctionExpression': 150 return node.name || 'λ'; 151 case 'ClassDeclaration': 152 case 'ClassExpression': 153 return node.name || 'Class'; 154 case 'MethodDefinition': 155 return node.name || 'method'; 156 case 'VariableDeclaration': 157 return node.kind || 'var'; // const, let, var 158 case 'VariableDeclarator': 159 return node.name || 'binding'; 160 case 'ImportDeclaration': 161 return node.source || 'import'; 162 case 'ExportNamedDeclaration': 163 case 'ExportDefaultDeclaration': 164 return node.name || 'export'; 165 case 'CallExpression': 166 return node.callee || 'call()'; 167 case 'MemberExpression': 168 return node.property || 'member'; 169 case 'Identifier': 170 return node.name || 'id'; 171 case 'Literal': 172 const val = String(node.value || ''); 173 return val.length > 12 ? val.slice(0, 10) + '…' : val; 174 case 'Property': 175 return node.name || 'prop'; 176 case 'Program': 177 return '📄 ' + (node.fileName || 'source'); 178 default: 179 return node.name || node.type.replace(/Declaration|Expression|Statement/g, ''); 180 } 181 } 182 183 function getNodeIcon(type) { 184 return nodeIcons[type] || '●'; 185 } 186 187 function getNodeColor(type) { 188 return nodeColors[type] || 0x666688; 189 } 190 191 function getNodeSize(node) { 192 const span = (node.end || 0) - (node.start || 0); 193 const baseSize = Math.max(4, Math.min(14, 3 + Math.log(span + 1) * 0.9)); 194 195 // Boost root and important nodes 196 const important = ['Program', 'FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', 'ExportDefaultDeclaration']; 197 if (important.includes(node.type)) return baseSize * 1.4; 198 return baseSize; 199 } 200 201 function createSourceNodeMesh(node, fileInfo) { 202 const size = getNodeSize(node); 203 const color = getNodeColor(node.type); 204 205 // Support legacy string arg or object 206 const fileName = (typeof fileInfo === 'string') ? fileInfo : fileInfo.fileName; 207 const filePath = (typeof fileInfo === 'object') ? fileInfo.filePath : undefined; 208 const fileId = (typeof fileInfo === 'object') ? (fileInfo.id || fileName) : fileName; 209 210 // Use icosahedron for functions/classes, sphere for others 211 const isImportant = ['FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', 'Program'].includes(node.type); 212 const geo = isImportant 213 ? new THREE.IcosahedronGeometry(size, 1) 214 : new THREE.SphereGeometry(size, 12, 12); 215 216 const mat = new THREE.MeshBasicMaterial({ 217 color: color, 218 transparent: true, 219 opacity: 0.85, 220 }); 221 222 const mesh = new THREE.Mesh(geo, mat); 223 mesh.userData = { 224 ...node, 225 fileName, 226 filePath, // Store full path for navigation 227 fileId, // Store unique ID for management 228 size, 229 baseColor: color, 230 displayName: getNodeDisplayName(node), 231 icon: getNodeIcon(node.type), 232 targetPos: new THREE.Vector3(), 233 pulsePhase: Math.random() * Math.PI * 2, 234 isSourceNode: true, 235 }; 236 237 return mesh; 238 } 239 240 function createSourceConnection(thickness = 1.2) { 241 const geo = new THREE.CylinderGeometry(thickness, thickness, 1, 8); 242 const mat = new THREE.MeshBasicMaterial({ 243 color: scheme.three.connectionLine, 244 transparent: true, 245 opacity: 0.6, 246 }); 247 return new THREE.Mesh(geo, mat); 248 } 249 250 function updateConnectionMesh(conn, childPos, parentPos) { 251 const mesh = conn.line; 252 const mid = new THREE.Vector3().addVectors(childPos, parentPos).multiplyScalar(0.5); 253 mesh.position.copy(mid); 254 const dir = new THREE.Vector3().subVectors(parentPos, childPos); 255 const length = dir.length(); 256 mesh.scale.set(1, length, 1); 257 mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.normalize()); 258 } 259 260 // Improved tree layout with more spacing 261 function layoutSourceTree(root, fileIndex = 0, totalFiles = 1) { 262 if (!root) return; 263 264 const levelHeight = 45; // More vertical spacing 265 const baseSpacing = 300; 266 const fileSpread = totalFiles > 1 ? 360 / totalFiles : 0; 267 const fileAngle = (fileIndex / totalFiles) * Math.PI * 2 - Math.PI / 2; 268 const baseX = Math.cos(fileAngle) * baseSpacing; 269 const baseZ = Math.sin(fileAngle) * baseSpacing; 270 271 function countDescendants(node) { 272 if (!node.children || node.children.length === 0) return 1; 273 return node.children.reduce((sum, child) => sum + countDescendants(child), 0); 274 } 275 276 function positionNode(node, depth, angle, radius, parentX, parentZ) { 277 const childCount = node.children?.length || 0; 278 279 const x = parentX + Math.cos(angle) * radius; 280 const z = parentZ + Math.sin(angle) * radius; 281 const y = -depth * levelHeight + 50; // Start above center 282 283 node.targetX = x; 284 node.targetY = y; 285 node.targetZ = z; 286 287 if (childCount > 0) { 288 // Calculate arc spread based on descendants for better spacing 289 const totalDescendants = node.children.reduce((sum, child) => sum + countDescendants(child), 0); 290 const arcSpread = Math.min(Math.PI * 1.5, Math.PI * 0.15 * totalDescendants); 291 const startAngle = angle - arcSpread / 2; 292 293 let currentAngle = startAngle; 294 node.children.forEach((child, i) => { 295 const childDescendants = countDescendants(child); 296 const childArcPortion = (childDescendants / totalDescendants) * arcSpread; 297 const childAngle = currentAngle + childArcPortion / 2; 298 currentAngle += childArcPortion; 299 300 const childRadius = 30 + Math.sqrt(childDescendants) * 8; 301 positionNode(child, depth + 1, childAngle, childRadius, x, z); 302 }); 303 } 304 } 305 306 positionNode(root, 0, fileAngle, 0, baseX, baseZ); 307 } 308 309 // Helper to get consistent ID 310 const getFileId = (f) => f.id || f.fileName; 311 312 // Get icon/color for non-AST buffer files (markdown, lisp) 313 function getBufferIcon(fileName) { 314 if (fileName.endsWith('.md')) return '📝'; 315 if (fileName.endsWith('.lisp')) return '🔮'; 316 return '📄'; 317 } 318 319 function getBufferColor(fileName) { 320 if (fileName.endsWith('.md')) return 0x88ccff; // Markdown - light blue 321 if (fileName.endsWith('.lisp')) return 0xff79c6; // Lisp - pink 322 return 0x666688; 323 } 324 325 // Create a simple buffer node mesh for non-AST files 326 function createBufferNodeMesh(file) { 327 const size = 12; 328 const color = getBufferColor(file.fileName); 329 const icon = getBufferIcon(file.fileName); 330 331 const geo = new THREE.IcosahedronGeometry(size, 1); 332 const mat = new THREE.MeshBasicMaterial({ 333 color: color, 334 transparent: true, 335 opacity: 0.85, 336 }); 337 338 const mesh = new THREE.Mesh(geo, mat); 339 mesh.userData = { 340 type: 'Buffer', 341 fileName: file.fileName, 342 filePath: file.filePath, 343 fileId: getFileId(file), 344 size, 345 baseColor: color, 346 displayName: file.fileName, 347 icon: icon, 348 targetPos: new THREE.Vector3(), 349 pulsePhase: Math.random() * Math.PI * 2, 350 isSourceNode: true, 351 isBuffer: true, 352 }; 353 354 return mesh; 355 } 356 357 function updateSourceVisualization(files) { 358 astFiles = files; 359 360 if (!sourcesVisible) return; 361 362 const currentFileIds = new Set(files.map(f => getFileId(f))); 363 364 // Remove old visualizations 365 sourceFiles.forEach((fileData, fileId) => { 366 if (!currentFileIds.has(fileId)) { 367 fileData.nodes.forEach(mesh => { 368 scene.remove(mesh); 369 mesh.geometry?.dispose(); 370 mesh.material?.dispose(); 371 }); 372 sourceFiles.delete(fileId); 373 } 374 }); 375 376 // Remove old connections 377 sourceConnections.forEach((conn, key) => { 378 // Compatibility: check fileId if available, else fallback to fileName check? 379 // Actually we should store fileId in conn 380 const idToCheck = conn.fileId || conn.fileName; 381 if (!currentFileIds.has(idToCheck)) { 382 scene.remove(conn.line); 383 conn.line.geometry?.dispose(); 384 conn.line.material?.dispose(); 385 sourceConnections.delete(key); 386 } 387 }); 388 389 // Create/update visualizations 390 const astFiles = files.filter(f => f.ast); 391 const bufferFiles = files.filter(f => !f.ast); // Markdown, Lisp, etc. 392 const totalAstFiles = astFiles.length; 393 const totalBufferFiles = bufferFiles.length; 394 let fileIndex = 0; 395 396 // Layout AST files (existing behavior) 397 astFiles.forEach((file) => { 398 const fileId = getFileId(file); 399 400 // Add fileName to root node 401 file.ast.fileName = file.fileName; 402 403 layoutSourceTree(file.ast, fileIndex, totalAstFiles); 404 fileIndex++; 405 406 let fileData = sourceFiles.get(fileId); 407 if (!fileData) { 408 fileData = { nodes: new Map() }; 409 sourceFiles.set(fileId, fileData); 410 } 411 412 const currentNodeIds = new Set(); 413 414 function processNode(node, parentId) { 415 if (!node) return; 416 417 currentNodeIds.add(node.id); 418 419 let mesh = fileData.nodes.get(node.id); 420 if (!mesh) { 421 mesh = createSourceNodeMesh(node, file); // Pass full file object 422 mesh.position.set(node.targetX || 0, node.targetY || 0, node.targetZ || 0); 423 scene.add(mesh); 424 fileData.nodes.set(node.id, mesh); 425 } 426 427 // Update mesh data 428 mesh.userData.targetPos.set(node.targetX || 0, node.targetY || 0, node.targetZ || 0); 429 mesh.userData.displayName = getNodeDisplayName(node); 430 mesh.userData.loc = node.loc; 431 mesh.visible = sourcesVisible; 432 433 // Create connection to parent 434 if (parentId) { 435 const connKey = `${fileId}:${node.id}->${parentId}`; 436 if (!sourceConnections.has(connKey)) { 437 const thickness = node.depth < 3 ? 2 : 1.2; 438 const line = createSourceConnection(thickness); 439 line.visible = sourcesVisible; 440 scene.add(line); 441 sourceConnections.set(connKey, { 442 line, 443 childId: node.id, 444 parentId, 445 fileId: fileId, // Store ID 446 fileName: file.fileName, 447 depth: node.depth 448 }); 449 } else { 450 sourceConnections.get(connKey).line.visible = sourcesVisible; 451 } 452 } 453 454 // Process children 455 if (node.children) { 456 node.children.forEach(child => processNode(child, node.id)); 457 } 458 } 459 460 processNode(file.ast, null); 461 462 // Remove deleted nodes 463 fileData.nodes.forEach((mesh, nodeId) => { 464 if (!currentNodeIds.has(nodeId)) { 465 scene.remove(mesh); 466 mesh.geometry?.dispose(); 467 mesh.material?.dispose(); 468 fileData.nodes.delete(nodeId); 469 } 470 }); 471 }); 472 473 // Layout buffer files (markdown, lisp) - simple nodes in an arc 474 bufferFiles.forEach((file, bufferIndex) => { 475 const fileId = getFileId(file); 476 477 let fileData = sourceFiles.get(fileId); 478 if (!fileData) { 479 fileData = { nodes: new Map(), isBuffer: true }; 480 sourceFiles.set(fileId, fileData); 481 } 482 483 const bufferId = `buffer-${fileId}`; 484 let mesh = fileData.nodes.get(bufferId); 485 486 // Position buffer files in an arc below AST files 487 const bufferRadius = 80; 488 const angleSpread = Math.PI * 0.8; 489 const startAngle = Math.PI + (Math.PI - angleSpread) / 2; 490 const angle = totalBufferFiles === 1 491 ? Math.PI * 1.5 492 : startAngle + (angleSpread / (totalBufferFiles - 1)) * bufferIndex; 493 494 const targetX = Math.cos(angle) * bufferRadius; 495 const targetY = -60; // Below the main AST view 496 const targetZ = Math.sin(angle) * bufferRadius; 497 498 if (!mesh) { 499 mesh = createBufferNodeMesh(file); 500 mesh.position.set(targetX, targetY, targetZ); 501 scene.add(mesh); 502 fileData.nodes.set(bufferId, mesh); 503 } 504 505 mesh.userData.targetPos.set(targetX, targetY, targetZ); 506 mesh.visible = sourcesVisible; 507 }); 508 509 // Clean orphaned connections 510 sourceConnections.forEach((conn, key) => { 511 // Use fileId to lookup 512 const idToCheck = conn.fileId || conn.fileName; 513 const fileData = sourceFiles.get(idToCheck); 514 515 if (!fileData || !fileData.nodes.has(conn.childId)) { 516 scene.remove(conn.line); 517 conn.line.geometry?.dispose(); 518 conn.line.material?.dispose(); 519 sourceConnections.delete(key); 520 } 521 }); 522 } 523 524 // Tab switching 525 function setTab(tab) { 526 if (tab !== 'processes' && tab !== 'sources') return; 527 currentTab = tab; 528 529 // Toggle visibility 530 sourcesVisible = (tab === 'sources'); 531 532 // Hide/show process meshes 533 window.ProcessTreeViz.meshes?.forEach(mesh => { 534 mesh.visible = !sourcesVisible; 535 }); 536 window.ProcessTreeViz.connections?.forEach(conn => { 537 if (conn.line) conn.line.visible = !sourcesVisible; 538 }); 539 window.ProcessTreeViz.graveyard?.forEach(grave => { 540 if (grave.mesh) grave.mesh.visible = !sourcesVisible; 541 }); 542 543 // Hide/show source meshes 544 sourceFiles.forEach(fileData => { 545 fileData.nodes.forEach(mesh => { 546 mesh.visible = sourcesVisible; 547 }); 548 }); 549 sourceConnections.forEach(conn => { 550 conn.line.visible = sourcesVisible; 551 }); 552 553 // Link Update Labels 554 const processLabels = document.getElementById('labels'); 555 const sourceLabels = document.getElementById('source-labels'); 556 const hudCenter = document.querySelector('.hud.center'); 557 558 if (processLabels) processLabels.style.display = sourcesVisible ? 'none' : 'block'; 559 if (sourceLabels) sourceLabels.style.display = sourcesVisible ? 'block' : 'none'; 560 if (hudCenter) hudCenter.style.display = sourcesVisible ? 'none' : 'flex'; 561 562 // Reset camera for sources view 563 if (sourcesVisible) { 564 // Re-run visualization with current files 565 updateSourceVisualization(astFiles); 566 } 567 568 updateTabUI(); 569 } 570 571 // Tour Mode for Sources 572 let tourMode = false; 573 let tourList = []; 574 let tourIndex = 0; 575 let tourAutoPlay = false; 576 let tourInterval = null; 577 578 function buildTourList() { 579 const list = []; 580 sourceFiles.forEach(fileData => { 581 // Add file root 582 // list.push(fileData.nodes.get(fileData.rootId)); 583 584 // Add interesting nodes 585 fileData.nodes.forEach(mesh => { 586 const d = mesh.userData; 587 const important = ['Program', 'FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', 'ExportDefaultDeclaration']; 588 if (important.includes(d.type)) { 589 list.push(mesh); 590 } 591 }); 592 }); 593 594 // Sort by position roughly to make a logical path? 595 // Or just keep them in file/traversal order which map iteration should mostly preserve 596 return list; 597 } 598 599 function startSourceTour() { 600 tourMode = true; 601 tourList = buildTourList(); 602 tourIndex = 0; 603 604 if (tourList.length > 0) { 605 focusOnNode(tourList[0]); 606 } 607 608 // Notify ProcessTree to update UI button state if needed 609 if (window.ProcessTreeViz) { 610 const btn = document.getElementById('tour-btn'); 611 if (btn) btn.textContent = '⏹ Stop Tour'; 612 } 613 } 614 615 function stopSourceTour() { 616 tourMode = false; 617 tourAutoPlay = false; 618 if (tourInterval) { 619 clearInterval(tourInterval); 620 tourInterval = null; 621 } 622 focusedSourceNode = null; 623 624 if (window.ProcessTreeViz) { 625 const btn = document.getElementById('tour-btn'); 626 if (btn) btn.textContent = '🎬 Tour'; 627 window.ProcessTreeViz.controls.autoRotate = false; 628 } 629 } 630 631 function tourNext() { 632 if (!tourMode || tourList.length === 0) return; 633 tourIndex = (tourIndex + 1) % tourList.length; 634 focusOnNode(tourList[tourIndex]); 635 } 636 637 function tourPrev() { 638 if (!tourMode || tourList.length === 0) return; 639 tourIndex = (tourIndex - 1 + tourList.length) % tourList.length; 640 focusOnNode(tourList[tourIndex]); 641 } 642 643 function focusOnNode(mesh) { 644 if (!mesh) return; 645 focusedSourceNode = mesh.userData.id; 646 647 const controls = window.ProcessTreeViz.controls; 648 const camera = window.ProcessTreeViz.camera; 649 650 if (controls) { 651 controls.target.copy(mesh.position); 652 controls.autoRotate = true; 653 controls.autoRotateSpeed = 2.0; // Slow rotation 654 655 // Zoom in appropriately 656 const dist = 100 + (mesh.userData.size || 10) * 5; 657 const currentDist = camera.position.distanceTo(controls.target); 658 659 // Smoothly move camera distance in animate loop? 660 // For now, let's just set a target for the animate loop to handle if we add that support 661 // Or just jump 662 /* 663 const direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize(); 664 camera.position.copy(controls.target).add(direction.multiplyScalar(dist)); 665 */ 666 } 667 updateSourceLabels(); 668 } 669 670 function toggleTourAutoPlay() { 671 tourAutoPlay = !tourAutoPlay; 672 if (tourAutoPlay) { 673 tourInterval = setInterval(tourNext, 3000); // 3 seconds per node 674 } else { 675 if (tourInterval) { 676 clearInterval(tourInterval); 677 tourInterval = null; 678 } 679 } 680 } 681 682 function updateTabUI() { 683 // Insert tabs into #header-center if it exists, otherwise create floating tabs 684 let headerCenter = document.getElementById('header-center'); 685 let tabBar = document.getElementById('view-tabs'); 686 687 if (headerCenter) { 688 // Use existing header structure 689 if (!tabBar) { 690 tabBar = document.createElement('div'); 691 tabBar.id = 'view-tabs'; 692 tabBar.style.cssText = 'display: flex; gap: 4px; margin-left: 16px;'; 693 headerCenter.appendChild(tabBar); 694 } 695 } else { 696 // Fallback: create in header-right area 697 const headerRight = document.getElementById('header-right'); 698 if (headerRight && !tabBar) { 699 tabBar = document.createElement('div'); 700 tabBar.id = 'view-tabs'; 701 tabBar.style.cssText = 'display: flex; gap: 4px;'; 702 headerRight.insertBefore(tabBar, headerRight.firstChild); 703 } 704 } 705 706 if (!tabBar) return; // No place to put tabs 707 708 const processActive = currentTab === 'processes'; 709 const sourceActive = currentTab === 'sources'; 710 const fileCount = astFiles.filter(f => f.ast).length; 711 712 tabBar.innerHTML = ` 713 <button id="tab-processes" class="hdr-btn" style=" 714 background: ${processActive ? (scheme?.accent || '#ff69b4') : 'rgba(255,255,255,0.08)'}; 715 color: ${processActive ? '#000' : '#888'}; 716 ">Proc</button> 717 <button id="tab-sources" class="hdr-btn" style=" 718 background: ${sourceActive ? (scheme?.accent || '#ff69b4') : 'rgba(255,255,255,0.08)'}; 719 color: ${sourceActive ? '#000' : '#888'}; 720 ">Src${fileCount > 0 ? ' ' + fileCount : ''}</button> 721 `; 722 723 document.getElementById('tab-processes').onclick = () => setTab('processes'); 724 document.getElementById('tab-sources').onclick = () => setTab('sources'); 725 } 726 727 // Source labels with rich info 728 function updateSourceLabels() { 729 if (!sourcesVisible) return; 730 731 let container = document.getElementById('source-labels'); 732 if (!container) { 733 container = document.createElement('div'); 734 container.id = 'source-labels'; 735 container.className = 'label-container'; 736 document.body.appendChild(container); 737 } 738 container.innerHTML = ''; 739 container.style.display = sourcesVisible ? 'block' : 'none'; 740 741 const width = window.innerWidth; 742 const height = window.innerHeight; 743 const camera = window.ProcessTreeViz.camera; 744 745 sourceFiles.forEach((fileData, fileName) => { 746 fileData.nodes.forEach((mesh) => { 747 if (!mesh.visible) return; 748 749 const pos = new THREE.Vector3(); 750 mesh.getWorldPosition(pos); 751 const labelPos = pos.clone(); 752 labelPos.y += (mesh.userData.size || 6) + 4; 753 labelPos.project(camera); 754 755 const x = (labelPos.x * 0.5 + 0.5) * width; 756 const y = (-labelPos.y * 0.5 + 0.5) * height; 757 758 if (labelPos.z < 1 && x > -50 && x < width + 50 && y > -50 && y < height + 50) { 759 const d = mesh.userData; 760 const distToCamera = camera.position.distanceTo(pos); 761 762 // Show more labels when zoomed in 763 // Relaxed thresholds for visibility 764 const important = ['Program', 'FunctionDeclaration', 'ClassDeclaration', 'MethodDefinition', 765 'VariableDeclaration', 'ImportDeclaration', 'ExportDefaultDeclaration', 'ExportNamedDeclaration', 766 'Buffer']; // Buffer files (markdown, lisp) are always visible 767 const isImportant = important.includes(d.type) || d.isBuffer; 768 769 if (!isImportant && distToCamera > 800) return; 770 if (distToCamera > 1200) return; 771 772 const proximityScale = Math.max(0.5, Math.min(2.5, 300 / distToCamera)); 773 const opacity = Math.max(0.6, Math.min(1, 400 / distToCamera)); 774 const color = '#' + (d.baseColor || 0x666666).toString(16).padStart(6, '0'); 775 const isFocused = focusedSourceNode === d.id; 776 const isHovered = hoveredNode === d.id; 777 778 const label = document.createElement('div'); 779 label.className = 'proc-label source-label' + (isFocused ? ' focused' : '') + (isHovered ? ' hovered' : '') + (d.isBuffer ? ' buffer' : ''); 780 label.style.cssText = ` 781 left: ${x}px; top: ${y}px; 782 opacity: ${isFocused || isHovered ? 1 : opacity}; 783 transform: translate(-50%, -100%) scale(${isFocused || isHovered ? proximityScale * 1.3 : proximityScale}); 784 cursor: pointer; 785 pointer-events: auto; 786 ${isFocused ? 'z-index: 100;' : ''} 787 text-align: center; 788 `; 789 790 label.innerHTML = ` 791 <div style=" 792 font-size: ${d.isBuffer ? '10px' : '8px'}; font-weight: bold; color: ${color}; 793 text-shadow: 0 1px 2px rgba(0,0,0,0.8); 794 white-space: nowrap; 795 background: rgba(0,0,0,0.4); 796 padding: 2px 4px; 797 border-radius: 4px; 798 display: inline-flex; overflow: visible; align-items: center; gap: 4px; 799 "> 800 <span style="font-size: ${d.isBuffer ? '14px' : '10px'};">${d.icon}</span>${d.displayName} 801 </div> 802 ${isHovered || isFocused ? `<div style="font-size: 7px; color: #888; margin-top: 1px;">${d.type}</div>` : ''} 803 `; 804 805 // Click to focus/navigate 806 807 // Click to focus/navigate 808 label.onclick = (e) => { 809 e.stopPropagation(); 810 handleNodeClick(mesh); 811 }; 812 813 container.appendChild(label); 814 } 815 }); 816 }); 817 } 818 819 // Click handling for source nodes 820 function handleNodeClick(mesh) { 821 const d = mesh.userData; 822 823 if (focusedSourceNode === d.id) { 824 // Double-click to navigate to source 825 // Prefer filePath if available, fallback to fileName (legacy/simple) 826 const targetFile = d.filePath || d.fileName; 827 828 if (d.loc && targetFile) { 829 // Send message to VS Code to open file at line 830 const vscode = window.vscodeApi || (typeof acquireVsCodeApi !== 'undefined' ? acquireVsCodeApi() : null); 831 if (vscode) { 832 vscode.postMessage({ 833 command: 'navigateToSource', 834 filePath: d.filePath, // Explicitly send both 835 fileName: d.fileName, 836 line: d.loc.start.line, 837 column: d.loc.start.column 838 }); 839 } 840 console.log(`📍 Navigate to ${targetFile}:${d.loc.start.line}`); 841 } 842 focusedSourceNode = null; 843 } else { 844 // Single click to focus 845 focusedSourceNode = d.id; 846 847 // Move camera to focus on node 848 const controls = window.ProcessTreeViz.controls; 849 if (controls) { 850 controls.target.copy(mesh.position); 851 } 852 } 853 854 updateSourceLabels(); 855 } 856 857 // Click detection on 3D scene 858 function onCanvasClick(event) { 859 if (!sourcesVisible) return; 860 861 const canvas = renderer.domElement; 862 const rect = canvas.getBoundingClientRect(); 863 mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; 864 mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; 865 866 raycaster.setFromCamera(mouse, camera); 867 868 // Get all source meshes 869 const meshArray = []; 870 sourceFiles.forEach(fileData => { 871 fileData.nodes.forEach(mesh => { 872 if (mesh.visible) meshArray.push(mesh); 873 }); 874 }); 875 876 const intersects = raycaster.intersectObjects(meshArray); 877 878 if (intersects.length > 0) { 879 handleNodeClick(intersects[0].object); 880 } else { 881 focusedSourceNode = null; 882 updateSourceLabels(); 883 } 884 } 885 886 // Hover detection 887 function onCanvasMove(event) { 888 if (!sourcesVisible) return; 889 890 const canvas = renderer.domElement; 891 const rect = canvas.getBoundingClientRect(); 892 mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; 893 mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; 894 895 raycaster.setFromCamera(mouse, camera); 896 897 const meshArray = []; 898 sourceFiles.forEach(fileData => { 899 fileData.nodes.forEach(mesh => { 900 if (mesh.visible) meshArray.push(mesh); 901 }); 902 }); 903 904 const intersects = raycaster.intersectObjects(meshArray); 905 const newHovered = intersects.length > 0 ? intersects[0].object.userData.id : null; 906 907 if (newHovered !== hoveredNode) { 908 hoveredNode = newHovered; 909 canvas.style.cursor = hoveredNode ? 'pointer' : 'default'; 910 } 911 } 912 913 // Animation hook 914 let time = 0; 915 function animateAST() { 916 if (!sourcesVisible) return; 917 918 time += 0.016; 919 920 // Animate source nodes 921 sourceFiles.forEach((fileData) => { 922 fileData.nodes.forEach((mesh) => { 923 if (!mesh.visible) return; 924 const d = mesh.userData; 925 if (!d.targetPos) return; 926 927 // Smooth movement 928 mesh.position.x += (d.targetPos.x - mesh.position.x) * 0.06; 929 mesh.position.y += (d.targetPos.y - mesh.position.y) * 0.06; 930 mesh.position.z += (d.targetPos.z - mesh.position.z) * 0.06; 931 932 // Pulse effect 933 const isFocused = focusedSourceNode === d.id; 934 const isHovered = hoveredNode === d.id; 935 const pulseAmp = isFocused ? 0.25 : (isHovered ? 0.15 : 0.08); 936 const pulse = 1 + Math.sin(time * (isFocused ? 2 : 0.8) + d.pulsePhase) * pulseAmp; 937 const sizeMult = isFocused ? 1.5 : (isHovered ? 1.2 : 1); 938 mesh.scale.setScalar((d.size / 6) * pulse * sizeMult); 939 940 // Opacity 941 mesh.material.opacity = isFocused ? 1 : (isHovered ? 0.95 : 0.85); 942 }); 943 }); 944 945 // Update connections 946 sourceConnections.forEach(conn => { 947 if (!conn.line.visible) return; 948 949 const fileData = sourceFiles.get(conn.fileName); 950 if (!fileData) return; 951 952 const childMesh = fileData.nodes.get(conn.childId); 953 const parentMesh = fileData.nodes.get(conn.parentId); 954 if (childMesh && parentMesh) { 955 updateConnectionMesh(conn, childMesh.position, parentMesh.position); 956 957 // Highlight connections to focused node 958 const isFocusPath = focusedSourceNode && 959 (conn.childId === focusedSourceNode || conn.parentId === focusedSourceNode); 960 conn.line.material.opacity = isFocusPath ? 0.9 : 0.5; 961 conn.line.material.color.setHex(isFocusPath ? scheme.three.connectionActive : scheme.three.connectionLine); 962 } 963 }); 964 965 updateSourceLabels(); 966 } 967 968 // Initialize 969 renderer.domElement.addEventListener('click', onCanvasClick); 970 renderer.domElement.addEventListener('mousemove', onCanvasMove); 971 972 // Create initial tab UI 973 updateTabUI(); 974 975 // Keyboard shortcut: Tab to switch views 976 document.addEventListener('keydown', (e) => { 977 if (e.key === 'Tab' && !e.ctrlKey && !e.altKey && !e.shiftKey) { 978 e.preventDefault(); 979 setTab(currentTab === 'processes' ? 'sources' : 'processes'); 980 } 981 // Escape to unfocus 982 if (e.key === 'Escape' && sourcesVisible) { 983 focusedSourceNode = null; 984 updateSourceLabels(); 985 } 986 }); 987 988 // Expose API 989 window.ASTTreeViz = { 990 updateASTVisualization: updateSourceVisualization, 991 animateAST, 992 setTab, 993 getTab: () => currentTab, 994 sourceFiles, 995 sourceConnections, 996 focusNode: (id) => { focusedSourceNode = id; }, 997 // Tour API 998 startTour: startSourceTour, 999 stopTour: stopSourceTour, 1000 tourNext, 1001 tourPrev, 1002 toggleTourAutoPlay, 1003 isTourMode: () => tourMode, 1004 }; 1005 1006 console.log('📜 Source Tree Visualization loaded - Press Tab to switch views'); 1007 1008 // Request initial data after a short delay to ensure VSCode API is ready 1009 setTimeout(() => { 1010 const vscode = window.vscodeApi || (typeof acquireVsCodeApi !== 'undefined' ? acquireVsCodeApi() : null); 1011 if (vscode) { 1012 console.log('📡 Requesting initial AST data...'); 1013 vscode.postMessage({ command: 'requestAST' }); 1014 } 1015 }, 200); 1016 1017 } // End initASTVisualization 1018})();