Monorepo for Aesthetic.Computer
aesthetic.computer
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})();