File size: 5,827 Bytes
b30b7c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Vista «Arquitectura» — grafo 3D de dependencias del proyecto, FIEL a CodeFlow
// (braedonsaunders/codeflow, MIT): usa la MISMA librería `3d-force-graph`, con
// nodos coloreados por capa (carpeta de primer nivel), tamaño por número de
// conexiones y partículas de flujo viajando por los enlaces (la firma de
// CodeFlow). Lee los ficheros reales, extrae los imports y resuelve las aristas.
// Clic en un nodo → abre el fichero en Monaco.
import * as code from './tools/code.js';

const CODE_EXT = /\.(js|mjs|cjs|jsx|ts|tsx|py|go|rs|java|c|h|cpp|hpp|vue|svelte)$/i;
const FG_URL = 'https://cdn.jsdelivr.net/npm/3d-force-graph@1/dist/3d-force-graph.min.js';
// paleta por capa (carpeta de primer nivel), estilo CodeFlow
const LAYER_COLORS = ['#7c5cff', '#49e8ff', '#ff4d8d', '#3fb970', '#ffd479', '#ff9f45', '#a86cff', '#42b883', '#e34c26', '#00add8'];

// Extrae destinos de import de un fichero (JS/TS/Py, tolerante).
function importsOf(path, src) {
  const t = [];
  const re = /(?:import\s+[^'"]*from\s+|import\s+|require\(\s*|export\s+[^'"]*from\s+)['"]([^'"]+)['"]/g;
  let m; while ((m = re.exec(src))) t.push(m[1]);
  const py = /^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm;
  while ((m = py.exec(src))) t.push((m[1] || m[2]).replace(/\./g, '/'));
  return t;
}

// Resuelve un import relativo a un fichero real del proyecto.
function resolve(from, spec, files) {
  if (!/^[./]/.test(spec)) {
    const hit = files.find(f => f.endsWith('/' + spec) || f.includes('/' + spec + '.') || f.includes('/' + spec + '/'));
    return hit || null;
  }
  const base = from.split('/').slice(0, -1);
  for (const part of spec.split('/')) { if (part === '..') base.pop(); else if (part !== '.') base.push(part); }
  const cand = base.join('/');
  return files.find(f => f === cand) ||
    files.find(f => f.replace(CODE_EXT, '') === cand) ||
    files.find(f => f === cand + '/index.js' || f === cand + '/__init__.py') || null;
}

// Monaco carga un loader AMD (define.amd); el UMD de 3d-force-graph se
// registraría por AMD y NO crearía window.ForceGraph3D. Anulamos `define`
// mientras carga el bundle para forzar la rama global (igual que con xterm).
const loadFG = () => window.ForceGraph3D ? Promise.resolve() : new Promise((res, rej) => {
  const savedDefine = window.define;
  window.define = undefined;
  const restore = () => { window.define = savedDefine; };
  const s = document.createElement('script');
  s.src = FG_URL;
  s.onload = () => { restore(); window.ForceGraph3D ? res() : rej(new Error('3d-force-graph cargó pero no expuso ForceGraph3D')); };
  s.onerror = () => { restore(); rej(new Error('no cargó 3d-force-graph')); };
  document.head.appendChild(s);
});

let graph = null;

export async function renderArchitecture(container, onOpenFile) {
  disposeArch();
  container.innerHTML = '<div class="view-loading">Analizando la arquitectura del proyecto…</div>';
  const files = (await code.fileList()).filter(f => CODE_EXT.test(f)).slice(0, 400);
  if (!files.length) { container.innerHTML = '<div class="view-loading">Sin ficheros de código para analizar.</div>'; return; }

  const topOf = p => p.includes('/') ? p.split('/')[0] : '(raíz)';
  const layers = [...new Set(files.map(topOf))];
  const layerColor = new Map(layers.map((l, i) => [l, LAYER_COLORS[i % LAYER_COLORS.length]]));
  const deg = new Map(files.map(f => [f, 0]));
  const links = [];
  const seen = new Set();
  for (const p of files) {
    let src = ''; try { src = await code.read({ path: p }); } catch { continue; }
    for (const spec of importsOf(p, src)) {
      const target = resolve(p, spec, files);
      if (target && target !== p) {
        const key = p + '' + target;
        if (seen.has(key)) continue; seen.add(key);
        links.push({ source: p, target });
        deg.set(p, deg.get(p) + 1); deg.set(target, deg.get(target) + 1);
      }
    }
  }
  const nodes = files.map(p => ({ id: p, name: p.split('/').pop(), layer: topOf(p), val: 1 + deg.get(p) * 2, color: layerColor.get(topOf(p)) }));

  container.innerHTML =
    '<div id="arch-graph"></div>' +
    '<div class="view-head">Arquitectura · ' + nodes.length + ' ficheros, ' + links.length + ' dependencias' +
    '<span class="view-hint">arrastra para orbitar · las partículas fluyen del importado al importador · clic en un nodo → abrir</span></div>';
  const el = container.querySelector('#arch-graph');

  await loadFG();
  graph = window.ForceGraph3D({ controlType: 'orbit' })(el)
    .backgroundColor('#0b0d12')
    .width(el.clientWidth || 800).height(el.clientHeight || 600)
    .graphData({ nodes, links })
    .nodeVal('val')
    .nodeColor('color')
    .nodeOpacity(0.92)
    .nodeLabel(n => `${n.id} · ${deg.get(n.id)} conexiones`)
    .nodeResolution(12)
    .linkColor(() => 'rgba(140,150,180,0.28)')
    .linkWidth(0.6)
    .linkDirectionalParticles(2)
    .linkDirectionalParticleSpeed(0.006)
    .linkDirectionalParticleWidth(1.8)
    .linkDirectionalParticleColor(() => '#49e8ff')
    .onNodeClick(n => onOpenFile(n.id))
    .onBackgroundClick(() => {})
    .cooldownTicks(120)
    .onEngineStop(() => graph && graph.zoomToFit(500, 40));
  // separación por capas: espaciar un poco las cargas de nodos
  graph.d3Force('charge').strength(-120);
  graph.nodeThreeObjectExtend?.(false);
  const onResize = () => graph && graph.width(el.clientWidth).height(el.clientHeight);
  window.addEventListener('resize', onResize);
  graph.__onResize = onResize;
  // para pruebas: nº de nodos accesible en el DOM
  el.dataset.nodes = String(nodes.length);
  el.dataset.links = String(links.length);
}

export function disposeArch() {
  if (!graph) return;
  try { if (graph.__onResize) window.removeEventListener('resize', graph.__onResize); graph._destructor?.(); } catch { /* mejor esfuerzo */ }
  graph = null;
}