hbauzan commited on
Commit
3682b59
·
2 Parent(s): c146a959fd0b69

merge(feat): English-only product UI copy into main

Browse files
.agents/skills/dev-protocol/lessons-learned.md CHANGED
@@ -174,6 +174,12 @@ Para lecturas de alta visibilidad y ligera carga computacional, implementar la p
174
 
175
  ---
176
 
 
 
 
 
 
 
177
  ## 5. Protocolo de Mantenimiento de Lecciones Aprendidas
178
 
179
  1. **Consulta Obligatoria**: El agente **DEBE** leer este archivo al iniciar cualquier tarea de implementación, diseño de shaders, navegación o refactorización.
 
174
 
175
  ---
176
 
177
+ ### 4.7. Product copy = English; identifiers stay
178
+ - **Invariante**: Visible UI copy (navbar, panels, sliders, landscape gate, aria/titles/placeholders) is English-only. Internal keys (`data-view="ANALYSIS"`, `threadAmplitudeY`, CSS classes) are not renamed for i18n. No i18n framework — single-language product surface.
179
+ - **Compare 3D labels** remain raw input tokens (§4.6); panel chrome uses EN glossary.
180
+
181
+ ---
182
+
183
  ## 5. Protocolo de Mantenimiento de Lecciones Aprendidas
184
 
185
  1. **Consulta Obligatoria**: El agente **DEBE** leer este archivo al iniciar cualquier tarea de implementación, diseño de shaders, navegación o refactorización.
CHANGELOG.md CHANGED
@@ -5,6 +5,7 @@ All notable changes to VectorLab 3D will be documented in this file.
5
  ## [Unreleased]
6
 
7
  ### Changed
 
8
  - **Docs: English-only UI roadmap** — glossary + D6–D8 closed (EN Compare vocab; EN test titles/comments; keep historical CHANGELOG in Spanish). See `roadmap/english-ui-i18n.md` + `roadmap/PROMPT-english-ui.md`.
9
  - **ThreadLabels cortas** (`feat/short-thread-labels` + `fix/compare-labels-full-tokens`):
10
  - Arithmetic 3D: `WORD_A` / `WORD_B` / `WORD_C` / `RES` / `TOP1` (sin badge de tipo).
 
5
  ## [Unreleased]
6
 
7
  ### Changed
8
+ - **English-only product UI** (`feat/english-user-facing-copy`): Navbar VIEW/ANALYSIS/NAVIGATION, spatial slider labels, Arithmetic/Compare buttons, Compare empty/sort/copy + EN auto-parts presets, landscape gate. Internal `data-view` / mode keys unchanged. Test titles and in-scope `src/` comments translated to EN; historical CHANGELOG entries left in Spanish.
9
  - **Docs: English-only UI roadmap** — glossary + D6–D8 closed (EN Compare vocab; EN test titles/comments; keep historical CHANGELOG in Spanish). See `roadmap/english-ui-i18n.md` + `roadmap/PROMPT-english-ui.md`.
10
  - **ThreadLabels cortas** (`feat/short-thread-labels` + `fix/compare-labels-full-tokens`):
11
  - Arithmetic 3D: `WORD_A` / `WORD_B` / `WORD_C` / `RES` / `TOP1` (sin badge de tipo).
roadmap/english-ui-i18n.md CHANGED
@@ -41,6 +41,9 @@
41
  | Mayor → menor / Ordenar… | Highest → lowest / Sort descending \| ascending |
42
  | Girá el teléfono… | Rotate your phone for a better experience. You can stay in portrait if you prefer. |
43
  | VECTOR ARITHMETIC / MODE / RENDER / POINTS / WORD_A / RES / TOP1 | keep (already EN) |
 
 
 
44
 
45
  > **GATE B:** glosario y D6–D8 **cerrados**. El agente puede pasar A → C/D sin re-preguntar salvo términos nuevos del audit.
46
 
@@ -118,8 +121,8 @@ Humano confirmó D6–D8 + tabla §0.1. **No re-preguntar** salvo strings nuevos
118
  - `CHANGELOG` Unreleased corto; lesson § nueva solo si hay invariante (“product copy = EN; identifiers stay”).
119
 
120
  ### C.2 Criterios
121
- - [ ] Smoke UI: ninguna cadena ES visible en Arithmetic / Compare / Análisis / Navegación / POINTS·MESH·RIBBONS / landscape gate.
122
- - [ ] Vitest verde; asserts de copy actualizados.
123
  - [ ] Approval gate → merge.
124
 
125
  **Rama:** `feat/english-user-facing-copy`
@@ -136,7 +139,7 @@ Humano confirmó D6–D8 + tabla §0.1. **No re-preguntar** salvo strings nuevos
136
  - CHANGELOG: solo **Unreleased / entradas nuevas** en EN; dejar secciones históricas en español (D8).
137
 
138
  ### D.2 Criterios
139
- - [ ] D7/D8 cumplidos; exclusiones D3 intactas.
140
  - [ ] Suite verde; approval gate → merge.
141
 
142
  **Rama:** puede ir en `feat/english-user-facing-copy` si el agente hace C+D juntos, o `feat/english-dev-strings`.
 
41
  | Mayor → menor / Ordenar… | Highest → lowest / Sort descending \| ascending |
42
  | Girá el teléfono… | Rotate your phone for a better experience. You can stay in portrait if you prefer. |
43
  | VECTOR ARITHMETIC / MODE / RENDER / POINTS / WORD_A / RES / TOP1 | keep (already EN) |
44
+ | VISTA: | VIEW: |
45
+ | Mejor en horizontal | Better in landscape |
46
+ | Entendido | Got it |
47
 
48
  > **GATE B:** glosario y D6–D8 **cerrados**. El agente puede pasar A → C/D sin re-preguntar salvo términos nuevos del audit.
49
 
 
121
  - `CHANGELOG` Unreleased corto; lesson § nueva solo si hay invariante (“product copy = EN; identifiers stay”).
122
 
123
  ### C.2 Criterios
124
+ - [x] Smoke UI: ninguna cadena ES visible en Arithmetic / Compare / Análisis / Navegación / POINTS·MESH·RIBBONS / landscape gate.
125
+ - [x] Vitest verde; asserts de copy actualizados.
126
  - [ ] Approval gate → merge.
127
 
128
  **Rama:** `feat/english-user-facing-copy`
 
139
  - CHANGELOG: solo **Unreleased / entradas nuevas** en EN; dejar secciones históricas en español (D8).
140
 
141
  ### D.2 Criterios
142
+ - [x] D7/D8 cumplidos; exclusiones D3 intactas.
143
  - [ ] Suite verde; approval gate → merge.
144
 
145
  **Rama:** puede ir en `feat/english-user-facing-copy` si el agente hace C+D juntos, o `feat/english-dev-strings`.
src/engine/Navigation.js CHANGED
@@ -164,7 +164,7 @@ export class Navigation {
164
  }
165
 
166
  setAnalysisView() {
167
- // Captured ARITHMETIC + ANÁLISIS + POINTS framing (POS / ROT from CAM POSE overlay)
168
  this.camera.position.set(-75.2, -0.8, 62.5);
169
  this.euler.set(0, 0, 0, 'YXZ');
170
  this.camera.quaternion.setFromEuler(this.euler);
@@ -172,7 +172,7 @@ export class Navigation {
172
  }
173
 
174
  setNavigationView() {
175
- // Captured default corridor view (king-man+woman @ Separación 0.4 / Amplitud 7)
176
  this.camera.position.set(-178.3, 13.5, 52.2);
177
  const deg2rad = Math.PI / 180;
178
  this.euler.set(-5.4 * deg2rad, -51.5 * deg2rad, 0, 'YXZ');
 
164
  }
165
 
166
  setAnalysisView() {
167
+ // Captured ARITHMETIC + ANALYSIS + POINTS framing (POS / ROT from CAM POSE overlay)
168
  this.camera.position.set(-75.2, -0.8, 62.5);
169
  this.euler.set(0, 0, 0, 'YXZ');
170
  this.camera.quaternion.setFromEuler(this.euler);
 
172
  }
173
 
174
  setNavigationView() {
175
+ // Captured default corridor view (king-man+woman @ Spacing 0.4 / Amplitude 7)
176
  this.camera.position.set(-178.3, 13.5, 52.2);
177
  const deg2rad = Math.PI / 180;
178
  this.euler.set(-5.4 * deg2rad, -51.5 * deg2rad, 0, 'YXZ');
src/main.js CHANGED
@@ -151,8 +151,8 @@ class VectorLabApp {
151
  }
152
 
153
  /**
154
- * Apply resolved spatial defaults for current MODE/VISTA/RENDER and sync slider UI.
155
- * Called when context changes so each combo can keep its own dulce point.
156
  */
157
  applyContextSpatialDefaults() {
158
  const defaults = resolveSpatialDefaults({
 
151
  }
152
 
153
  /**
154
+ * Apply resolved spatial defaults for current MODE/VIEW/RENDER and sync slider UI.
155
+ * Called when context changes so each combo can keep its own sweet spot.
156
  */
157
  applyContextSpatialDefaults() {
158
  const defaults = resolveSpatialDefaults({
src/ui/ComparePanel.js CHANGED
@@ -1,60 +1,59 @@
1
  import { reorderCompareItems, sortCompareItemsByCosine } from './compareCosine.js';
2
 
3
  /**
4
- * Vocabulario típico de un manual de automóvil en español (piezas, fluidos, sistemas).
5
- * Tokens simples (sin espacios) para el splitter de COMPARE.
6
  */
7
- export const AUTO_MANUAL_VOCAB_ES = [
8
- // Rodado
9
- 'rueda', 'ruedas', 'llanta', 'llantas', 'neumático', 'neumáticos', 'rin', 'goma',
10
- 'caucho', 'cubierta', 'aro', 'buje', 'eje', 'ejes', 'rodamiento', 'balero',
11
- // Frenos y pedales
12
- 'freno', 'frenos', 'pastilla', 'pastillas', 'disco', 'tambor', 'abs', 'pedal',
13
- 'acelerador', 'embrague',
14
- // Motor y escape
15
- 'motor', 'pistón', 'pistones', 'cilindro', 'cilindros', 'bujía', 'bujías',
16
- 'inyector', 'inyectores', 'culata', 'cigüeñal', 'leva', 'turbo', 'compresor',
17
- 'escape', 'catalizador', 'silenciador', 'colector', 'admisión',
18
- // Refrigeración / lubricación / correas
19
- 'radiador', 'refrigerante', 'anticongelante', 'aceite', 'filtro', 'filtros',
20
- 'correa', 'correas', 'bomba', 'termostato', 'ventilador',
21
- // Eléctrico
22
- 'batería', 'alternador', 'arranque', 'bobina', 'fusible', 'fusibles', 'relé',
23
- 'sensor', 'sensores', 'cable', 'cables',
24
- // Transmisión
25
- 'transmisión', 'caja', 'marcha', 'marchas', 'diferencial', 'cardán',
26
- 'sincronizador', 'convertidor',
27
- // Dirección y suspensión
28
- 'volante', 'dirección', 'suspensión', 'amortiguador', 'amortiguadores',
29
- 'muelle', 'muelles', 'ballesta', 'brazo', 'rótula', 'terminal',
30
- // Carrocería e iluminación
31
- 'chasis', 'carrocería', 'capó', 'maletero', 'puerta', 'puertas', 'ventanilla',
32
- 'parabrisas', 'espejo', 'espejos', 'faro', 'faros', 'piloto', 'pilotos',
33
- 'paragolpes', 'guardabarros', 'techo', 'aleta',
34
- // Habitáculo / seguridad
35
- 'asiento', 'asientos', 'cinturón', 'airbag', 'airbags', 'tablero',
36
- 'velocímetro', 'cuentakilómetros', 'claxon', 'limpiaparabrisas', 'guantera',
37
- // Fluidos y combustible
38
- 'combustible', 'gasolina', 'diésel', 'diesel', 'líquido', 'hidráulico',
39
- // Fijaciones / varios de taller
40
- 'junta', 'juntas', 'tornillo', 'tornillos', 'tuerca', 'tuercas', 'arandela',
41
- 'manguera', 'mangueras', 'depósito', 'tanque', 'tapón',
42
- // Vehículo
43
- 'vehículo', 'automóvil', 'coche', 'auto', 'camioneta',
44
  ];
45
 
46
- /** Deduplicated Spanish auto-manual lexicon (stable order). */
47
- export const AUTO_MANUAL_UNIQUE_ES = [...new Set(AUTO_MANUAL_VOCAB_ES)];
48
 
49
  /**
50
- * COMPARE presets built from Spanish auto-manual vocabulary.
51
- * @deprecated alias — prefer COMPARE_AUTO_PRESETS
52
  */
53
  export const COMPARE_AUTO_PRESETS = {
54
- sample5: ['rueda', 'motor', 'freno', 'volante', 'embrague'],
55
- default: AUTO_MANUAL_UNIQUE_ES,
56
- sample20: AUTO_MANUAL_UNIQUE_ES.slice(0, 20),
57
- sample50: AUTO_MANUAL_UNIQUE_ES.slice(0, 50),
58
  };
59
 
60
  /** @deprecated Use COMPARE_AUTO_PRESETS */
@@ -85,7 +84,7 @@ export class ComparePanel {
85
  <form id="compare-form" class="sidebar-form">
86
  <div class="input-group">
87
  <label for="compare-tokens">Tokens / Words (separated by comma, space, or newline)</label>
88
- <textarea id="compare-tokens" rows="6" placeholder="e.g. rueda, motor, freno, volante, embrague..." required></textarea>
89
  </div>
90
 
91
  <div class="preset-buttons-row">
@@ -95,7 +94,7 @@ export class ComparePanel {
95
  </div>
96
 
97
  <button type="submit" id="btn-compare-submit" class="btn-primary">
98
- 🔍 VISUALIZAR SECUENCIA (3D)
99
  </button>
100
  </form>
101
 
@@ -106,14 +105,14 @@ export class ComparePanel {
106
  </div>
107
 
108
  <div class="compare-cosine-header">
109
- <h3 id="compare-cosine-subtitle" class="compare-cosine-subtitle">SIMILITUD COSENO vs —</h3>
110
  <span class="compare-sort-btns">
111
- <button type="button" id="btn-sort-desc" class="btn-sort-cosine" data-sort="desc" disabled title="Mayormenor" aria-label="Ordenar de mayor a menor">▼</button>
112
- <button type="button" id="btn-sort-asc" class="btn-sort-cosine" data-sort="asc" disabled title="Menormayor" aria-label="Ordenar de menor a mayor">▲</button>
113
  </span>
114
  </div>
115
  <ul id="compare-cosine-list" class="compare-cosine-list">
116
- <li class="empty-state">Visualizá una secuencia para ver similitud vs el ancla...</li>
117
  </ul>
118
  </div>
119
  `;
@@ -129,7 +128,7 @@ export class ComparePanel {
129
  this.btnSortDesc = this.element.querySelector('#btn-sort-desc');
130
  this.btnSortAsc = this.element.querySelector('#btn-sort-asc');
131
 
132
- // Default: full Spanish auto-manual parts lexicon
133
  this.textarea.value = COMPARE_AUTO_PRESETS.default.join(", ");
134
 
135
  this.initEventListeners();
@@ -234,7 +233,7 @@ export class ComparePanel {
234
  this.btnSubmit.textContent = '⏳ COMPUTING SEQUENCE EMBEDDINGS...';
235
  } else {
236
  this.btnSubmit.disabled = false;
237
- this.btnSubmit.textContent = '🔍 VISUALIZAR SECUENCIA (3D)';
238
  }
239
  }
240
 
@@ -252,8 +251,8 @@ export class ComparePanel {
252
  if (!data || !data.items) {
253
  this.items = null;
254
  this.updateMetrics(0);
255
- this.cosineSubtitle.textContent = 'SIMILITUD COSENO vs —';
256
- this.cosineList.innerHTML = '<li class="empty-state">Visualizá una secuencia para ver similitud vs el ancla...</li>';
257
  this.setReorderLocked(false);
258
  return;
259
  }
@@ -266,7 +265,7 @@ export class ComparePanel {
266
 
267
  renderCosineList(anchor, items) {
268
  const anchorWord = anchor?.text ?? items[0]?.text ?? '—';
269
- this.cosineSubtitle.textContent = `SIMILITUD COSENO vs «${anchorWord}»`;
270
 
271
  this.cosineList.innerHTML = '';
272
  if (!items || items.length === 0) {
 
1
  import { reorderCompareItems, sortCompareItemsByCosine } from './compareCosine.js';
2
 
3
  /**
4
+ * Typical auto-manual lexicon in English (parts, fluids, systems).
5
+ * Simple tokens (no spaces) for the COMPARE splitter.
6
  */
7
+ export const AUTO_MANUAL_VOCAB_EN = [
8
+ // Wheels / tires
9
+ 'wheel', 'wheels', 'tire', 'tires', 'rim', 'rims', 'hub', 'axle', 'axles',
10
+ 'bearing', 'bearings', 'rubber', 'cover', 'spoke',
11
+ // Brakes and pedals
12
+ 'brake', 'brakes', 'pad', 'pads', 'disc', 'rotor', 'drum', 'abs', 'pedal',
13
+ 'accelerator', 'throttle', 'clutch',
14
+ // Engine and exhaust
15
+ 'engine', 'piston', 'pistons', 'cylinder', 'cylinders', 'spark', 'plug',
16
+ 'injector', 'injectors', 'head', 'crankshaft', 'camshaft', 'turbo', 'compressor',
17
+ 'exhaust', 'catalytic', 'muffler', 'manifold', 'intake',
18
+ // Cooling / lubrication / belts
19
+ 'radiator', 'coolant', 'antifreeze', 'oil', 'filter', 'filters',
20
+ 'belt', 'belts', 'pump', 'thermostat', 'fan',
21
+ // Electrical
22
+ 'battery', 'alternator', 'starter', 'coil', 'fuse', 'fuses', 'relay',
23
+ 'sensor', 'sensors', 'cable', 'cables', 'wiring',
24
+ // Transmission
25
+ 'transmission', 'gearbox', 'gear', 'gears', 'differential', 'driveshaft',
26
+ 'synchronizer', 'converter',
27
+ // Steering and suspension
28
+ 'steering', 'suspension', 'shock', 'shocks', 'spring', 'springs',
29
+ 'leaf', 'arm', 'balljoint', 'tie',
30
+ // Body and lighting
31
+ 'chassis', 'body', 'hood', 'trunk', 'door', 'doors', 'window',
32
+ 'windshield', 'mirror', 'mirrors', 'headlight', 'headlights', 'taillight',
33
+ 'bumper', 'fender', 'roof', 'panel',
34
+ // Cabin / safety
35
+ 'seat', 'seats', 'seatbelt', 'airbag', 'airbags', 'dashboard',
36
+ 'speedometer', 'odometer', 'horn', 'wiper', 'wipers', 'glovebox',
37
+ // Fluids and fuel
38
+ 'fuel', 'gasoline', 'diesel', 'fluid', 'hydraulic',
39
+ // Fasteners / shop misc
40
+ 'gasket', 'gaskets', 'bolt', 'bolts', 'nut', 'nuts', 'washer',
41
+ 'hose', 'hoses', 'tank', 'cap',
42
+ // Vehicle
43
+ 'vehicle', 'automobile', 'car', 'truck', 'van',
44
  ];
45
 
46
+ /** Deduplicated English auto-manual lexicon (stable order). */
47
+ export const AUTO_MANUAL_UNIQUE_EN = [...new Set(AUTO_MANUAL_VOCAB_EN)];
48
 
49
  /**
50
+ * COMPARE presets built from English auto-manual vocabulary.
 
51
  */
52
  export const COMPARE_AUTO_PRESETS = {
53
+ sample5: ['wheel', 'engine', 'brake', 'steering', 'clutch'],
54
+ default: AUTO_MANUAL_UNIQUE_EN,
55
+ sample20: AUTO_MANUAL_UNIQUE_EN.slice(0, 20),
56
+ sample50: AUTO_MANUAL_UNIQUE_EN.slice(0, 50),
57
  };
58
 
59
  /** @deprecated Use COMPARE_AUTO_PRESETS */
 
84
  <form id="compare-form" class="sidebar-form">
85
  <div class="input-group">
86
  <label for="compare-tokens">Tokens / Words (separated by comma, space, or newline)</label>
87
+ <textarea id="compare-tokens" rows="6" placeholder="e.g. wheel, engine, brake, steering, clutch..." required></textarea>
88
  </div>
89
 
90
  <div class="preset-buttons-row">
 
94
  </div>
95
 
96
  <button type="submit" id="btn-compare-submit" class="btn-primary">
97
+ 🔍 VISUALIZE SEQUENCE (3D)
98
  </button>
99
  </form>
100
 
 
105
  </div>
106
 
107
  <div class="compare-cosine-header">
108
+ <h3 id="compare-cosine-subtitle" class="compare-cosine-subtitle">COSINE SIMILARITY vs —</h3>
109
  <span class="compare-sort-btns">
110
+ <button type="button" id="btn-sort-desc" class="btn-sort-cosine" data-sort="desc" disabled title="Highestlowest" aria-label="Sort descending">▼</button>
111
+ <button type="button" id="btn-sort-asc" class="btn-sort-cosine" data-sort="asc" disabled title="Lowesthighest" aria-label="Sort ascending">▲</button>
112
  </span>
113
  </div>
114
  <ul id="compare-cosine-list" class="compare-cosine-list">
115
+ <li class="empty-state">Visualize a sequence to see similarity vs the anchor...</li>
116
  </ul>
117
  </div>
118
  `;
 
128
  this.btnSortDesc = this.element.querySelector('#btn-sort-desc');
129
  this.btnSortAsc = this.element.querySelector('#btn-sort-asc');
130
 
131
+ // Default: full English auto-manual parts lexicon
132
  this.textarea.value = COMPARE_AUTO_PRESETS.default.join(", ");
133
 
134
  this.initEventListeners();
 
233
  this.btnSubmit.textContent = '⏳ COMPUTING SEQUENCE EMBEDDINGS...';
234
  } else {
235
  this.btnSubmit.disabled = false;
236
+ this.btnSubmit.textContent = '🔍 VISUALIZE SEQUENCE (3D)';
237
  }
238
  }
239
 
 
251
  if (!data || !data.items) {
252
  this.items = null;
253
  this.updateMetrics(0);
254
+ this.cosineSubtitle.textContent = 'COSINE SIMILARITY vs —';
255
+ this.cosineList.innerHTML = '<li class="empty-state">Visualize a sequence to see similarity vs the anchor...</li>';
256
  this.setReorderLocked(false);
257
  return;
258
  }
 
265
 
266
  renderCosineList(anchor, items) {
267
  const anchorWord = anchor?.text ?? items[0]?.text ?? '—';
268
+ this.cosineSubtitle.textContent = `COSINE SIMILARITY vs "${anchorWord}"`;
269
 
270
  this.cosineList.innerHTML = '';
271
  if (!items || items.length === 0) {
src/ui/LandscapeGate.js CHANGED
@@ -85,9 +85,9 @@ export class LandscapeGate {
85
  this.overlay.innerHTML = `
86
  <div class="landscape-gate-card">
87
  <div class="landscape-gate-icon" aria-hidden="true">↻</div>
88
- <h2 id="landscape-gate-title">Mejor en horizontal</h2>
89
- <p class="landscape-gate-copy">Girá el teléfono para una mejor experiencia. Podés seguir en vertical si preferís.</p>
90
- <button type="button" class="btn-primary landscape-gate-dismiss">Entendido</button>
91
  </div>
92
  `;
93
 
 
85
  this.overlay.innerHTML = `
86
  <div class="landscape-gate-card">
87
  <div class="landscape-gate-icon" aria-hidden="true">↻</div>
88
+ <h2 id="landscape-gate-title">Better in landscape</h2>
89
+ <p class="landscape-gate-copy">Rotate your phone for a better experience. You can stay in portrait if you prefer.</p>
90
+ <button type="button" class="btn-primary landscape-gate-dismiss">Got it</button>
91
  </div>
92
  `;
93
 
src/ui/Navbar.js CHANGED
@@ -1,5 +1,5 @@
1
  /**
2
- * Top Navbar component with title, status indicator, View Mode tabs (ANÁLISIS | NAVEGACIÓN), and Render Mode selector tabs (POINTS | MESH | RIBBONS).
3
  */
4
  export class Navbar {
5
  constructor(containerElement, onRenderModeChangeCallback, onViewModeChangeCallback, onWorkspaceModeChangeCallback) {
@@ -28,9 +28,9 @@ export class Navbar {
28
  </div>
29
 
30
  <div class="view-mode-tabs">
31
- <span class="tab-label">VISTA:</span>
32
- <button data-view="ANALYSIS" class="view-tab">ANÁLISIS</button>
33
- <button data-view="NAVIGATION" class="view-tab active">NAVEGACIÓN</button>
34
  </div>
35
 
36
  <div class="render-mode-tabs">
 
1
  /**
2
+ * Top Navbar component with title, status indicator, View Mode tabs (ANALYSIS | NAVIGATION), and Render Mode selector tabs (POINTS | MESH | RIBBONS).
3
  */
4
  export class Navbar {
5
  constructor(containerElement, onRenderModeChangeCallback, onViewModeChangeCallback, onWorkspaceModeChangeCallback) {
 
28
  </div>
29
 
30
  <div class="view-mode-tabs">
31
+ <span class="tab-label">VIEW:</span>
32
+ <button data-view="ANALYSIS" class="view-tab">ANALYSIS</button>
33
+ <button data-view="NAVIGATION" class="view-tab active">NAVIGATION</button>
34
  </div>
35
 
36
  <div class="render-mode-tabs">
src/ui/Sidebar.js CHANGED
@@ -32,7 +32,7 @@ export class Sidebar {
32
  </div>
33
 
34
  <button type="submit" id="btn-calculate" class="btn-primary">
35
- CALCULAR VECTOR
36
  </button>
37
  </form>
38
 
@@ -75,7 +75,7 @@ export class Sidebar {
75
  this.btnCalculate.textContent = '⏳ COMPUTING EMBEDDINGS...';
76
  } else {
77
  this.btnCalculate.disabled = false;
78
- this.btnCalculate.textContent = '⚡ CALCULAR VECTOR';
79
  }
80
  }
81
 
 
32
  </div>
33
 
34
  <button type="submit" id="btn-calculate" class="btn-primary">
35
+ CALCULATE VECTOR
36
  </button>
37
  </form>
38
 
 
75
  this.btnCalculate.textContent = '⏳ COMPUTING EMBEDDINGS...';
76
  } else {
77
  this.btnCalculate.disabled = false;
78
+ this.btnCalculate.textContent = '⚡ CALCULATE VECTOR';
79
  }
80
  }
81
 
src/ui/ThreadSliders.js CHANGED
@@ -20,51 +20,51 @@ export function threadSlidersMarkup(config = {}) {
20
 
21
  return `
22
  <div id="thread-sliders-container" class="section-card">
23
- <h3 class="sliders-title">📐 Control Espacial 3D</h3>
24
 
25
  <!-- Slider 1: Spacing X -->
26
  <div class="slider-group">
27
  <div class="slider-header">
28
- <label for="thread-spacing-slider">Separación (X):</label>
29
  <span id="thread-spacing-val" class="slider-val">${spacing.toFixed(2)}</span>
30
  </div>
31
- <input type="range" id="thread-spacing-slider" min="0.1" max="0.7" step="0.05" value="${spacing}" title="Doble clic: restaurar default">
32
  </div>
33
 
34
  <!-- Slider 2: Vector Distance Y -->
35
  <div class="slider-group">
36
  <div class="slider-header">
37
- <label for="thread-vector-dist-slider">Distancia Vectores (Y):</label>
38
  <span id="thread-vector-dist-val" class="slider-val">${vectorDist.toFixed(1)}</span>
39
  </div>
40
- <input type="range" id="thread-vector-dist-slider" min="1.0" max="19.0" step="0.1" value="${vectorDist}" title="Doble clic: restaurar default">
41
  </div>
42
 
43
  <!-- Slider 3: Amplitude Y -->
44
  <div class="slider-group">
45
  <div class="slider-header">
46
- <label for="thread-amplitude-y-slider">Amplitud (Y):</label>
47
  <span id="thread-amplitude-y-val" class="slider-val">${amplitudeY.toFixed(1)}</span>
48
  </div>
49
- <input type="range" id="thread-amplitude-y-slider" min="1.0" max="40.0" step="0.1" value="${amplitudeY}" title="Doble clic: restaurar default">
50
  </div>
51
 
52
  <!-- Slider 4: Width Z -->
53
  <div class="slider-group">
54
  <div class="slider-header">
55
- <label for="thread-width-slider">Longitud (Z):</label>
56
  <span id="thread-width-val" class="slider-val">${width.toFixed(2)}</span>
57
  </div>
58
- <input type="range" id="thread-width-slider" min="0.1" max="0.3" step="0.01" value="${width}" title="Doble clic: restaurar default">
59
  </div>
60
 
61
  <!-- Slider 5: Thickness -->
62
  <div class="slider-group">
63
  <div class="slider-header">
64
- <label for="thread-thickness-slider">Grosor Puntos:</label>
65
  <span id="thread-thickness-val" class="slider-val">${thickness.toFixed(2)}</span>
66
  </div>
67
- <input type="range" id="thread-thickness-slider" min="0.01" max="0.09" step="0.01" value="${thickness}" title="Doble clic: restaurar default">
68
  </div>
69
  </div>
70
  `;
@@ -91,7 +91,7 @@ export function syncThreadSlidersFromConfig(container, config) {
91
  /**
92
  * Binds real-time event listeners to sliders for immediate 60fps spatial updates.
93
  * Double-click on a range input restores that slider's default for the current
94
- * MODE/VISTA/RENDER context (via resolveSpatialDefaults).
95
  *
96
  * @param {HTMLElement} container - DOM container containing slider elements
97
  * @param {Array<Object>|null} threads - Synthetic threads array (optional)
 
20
 
21
  return `
22
  <div id="thread-sliders-container" class="section-card">
23
+ <h3 class="sliders-title">📐 3D Spatial Controls</h3>
24
 
25
  <!-- Slider 1: Spacing X -->
26
  <div class="slider-group">
27
  <div class="slider-header">
28
+ <label for="thread-spacing-slider">Spacing (X):</label>
29
  <span id="thread-spacing-val" class="slider-val">${spacing.toFixed(2)}</span>
30
  </div>
31
+ <input type="range" id="thread-spacing-slider" min="0.1" max="0.7" step="0.05" value="${spacing}" title="Double-click: restore default">
32
  </div>
33
 
34
  <!-- Slider 2: Vector Distance Y -->
35
  <div class="slider-group">
36
  <div class="slider-header">
37
+ <label for="thread-vector-dist-slider">Vector Distance (Y):</label>
38
  <span id="thread-vector-dist-val" class="slider-val">${vectorDist.toFixed(1)}</span>
39
  </div>
40
+ <input type="range" id="thread-vector-dist-slider" min="1.0" max="19.0" step="0.1" value="${vectorDist}" title="Double-click: restore default">
41
  </div>
42
 
43
  <!-- Slider 3: Amplitude Y -->
44
  <div class="slider-group">
45
  <div class="slider-header">
46
+ <label for="thread-amplitude-y-slider">Amplitude (Y):</label>
47
  <span id="thread-amplitude-y-val" class="slider-val">${amplitudeY.toFixed(1)}</span>
48
  </div>
49
+ <input type="range" id="thread-amplitude-y-slider" min="1.0" max="40.0" step="0.1" value="${amplitudeY}" title="Double-click: restore default">
50
  </div>
51
 
52
  <!-- Slider 4: Width Z -->
53
  <div class="slider-group">
54
  <div class="slider-header">
55
+ <label for="thread-width-slider">Length (Z):</label>
56
  <span id="thread-width-val" class="slider-val">${width.toFixed(2)}</span>
57
  </div>
58
+ <input type="range" id="thread-width-slider" min="0.1" max="0.3" step="0.01" value="${width}" title="Double-click: restore default">
59
  </div>
60
 
61
  <!-- Slider 5: Thickness -->
62
  <div class="slider-group">
63
  <div class="slider-header">
64
+ <label for="thread-thickness-slider">Point Thickness:</label>
65
  <span id="thread-thickness-val" class="slider-val">${thickness.toFixed(2)}</span>
66
  </div>
67
+ <input type="range" id="thread-thickness-slider" min="0.01" max="0.09" step="0.01" value="${thickness}" title="Double-click: restore default">
68
  </div>
69
  </div>
70
  `;
 
91
  /**
92
  * Binds real-time event listeners to sliders for immediate 60fps spatial updates.
93
  * Double-click on a range input restores that slider's default for the current
94
+ * MODE/VIEW/RENDER context (via resolveSpatialDefaults).
95
  *
96
  * @param {HTMLElement} container - DOM container containing slider elements
97
  * @param {Array<Object>|null} threads - Synthetic threads array (optional)
src/ui/spatialSliderDefaults.js CHANGED
@@ -1,7 +1,7 @@
1
  /**
2
- * Spatial slider defaults for Control Espacial 3D.
3
  *
4
- * Today every MODE / VISTA / RENDER shares GLOBAL_SPATIAL_DEFAULTS.
5
  * Fill SPATIAL_DEFAULT_OVERRIDES as you define per-context presets;
6
  * resolveSpatialDefaults merges GLOBAL → workspace → view → render
7
  * (more specific keys win).
@@ -30,7 +30,7 @@ export const GLOBAL_SPATIAL_DEFAULTS = Object.freeze({
30
  * @type {Record<string, Partial<SpatialSliderValues>>}
31
  */
32
  export const SPATIAL_DEFAULT_OVERRIDES = {
33
- // Captured ARITHMETIC + ANÁLISIS + POINTS (Control Espacial + framing dulce)
34
  'ARITHMETIC|ANALYSIS|POINTS': {
35
  threadSpacing: 0.4,
36
  threadVectorDistance: 10.0,
 
1
  /**
2
+ * Spatial slider defaults for 3D Spatial Controls.
3
  *
4
+ * Today every MODE / VIEW / RENDER shares GLOBAL_SPATIAL_DEFAULTS.
5
  * Fill SPATIAL_DEFAULT_OVERRIDES as you define per-context presets;
6
  * resolveSpatialDefaults merges GLOBAL → workspace → view → render
7
  * (more specific keys win).
 
30
  * @type {Record<string, Partial<SpatialSliderValues>>}
31
  */
32
  export const SPATIAL_DEFAULT_OVERRIDES = {
33
+ // Captured ARITHMETIC + ANALYSIS + POINTS (Spatial Controls + sweet-spot framing)
34
  'ARITHMETIC|ANALYSIS|POINTS': {
35
  threadSpacing: 0.4,
36
  threadVectorDistance: 10.0,
src/visualizer/DivergentShading.js CHANGED
@@ -34,8 +34,8 @@ export function calculateZScoreNormalized(values, scaleFactor = 1.2) {
34
  * Calculates CPU color and dynamic opacity based on activation value.
35
  *
36
  * Color Ramps:
37
- * Positive (0 to +1): Negro (0.0) -> Naranja (+0.50) -> Amarillo (+1.00)
38
- * Negative (0 to -1): Negro (0.0) -> Azul Eléctrico (-0.50) -> Violeta (-1.00)
39
  *
40
  * @param {number} val - Activation value (v)
41
  * @param {number} [absMax=1.0] - Maximum absolute value for normalization
@@ -46,7 +46,7 @@ export function getDivergentColor(val, absMax = 1.0) {
46
  let t = Math.max(-1.0, Math.min(1.0, val / denom));
47
  const absT = Math.abs(t);
48
 
49
- // Optimización computacional para |t| < 0.01
50
  if (absT < 0.01) {
51
  return { r: 0.0, g: 0.0, b: 0.0, alpha: 0.05 };
52
  }
@@ -56,7 +56,7 @@ export function getDivergentColor(val, absMax = 1.0) {
56
  let r = 0.0, g = 0.0, b = 0.0;
57
 
58
  if (t > 0) {
59
- // Positivo (0 a +1): Negro (0.0) -> Naranja (+0.50) -> Amarillo Incandescente (+1.00)
60
  if (t < 0.50) {
61
  const k = t / 0.50;
62
  r = 1.0 * k;
@@ -69,7 +69,7 @@ export function getDivergentColor(val, absMax = 1.0) {
69
  b = 0.0;
70
  }
71
  } else {
72
- // Negativo (0 a -1): Negro (0.0) -> Azul Eléctrico (-0.50) -> Violeta Neón (-1.00)
73
  const f = -t;
74
  if (f < 0.50) {
75
  const k = f / 0.50;
@@ -106,8 +106,8 @@ void main() {
106
 
107
  /**
108
  * GLSL Fragment Shader for Dual Divergent Color Ramps (Solid Circular Points):
109
- * Positivo (0 a +1): Negro (0.0) -> Naranja (+0.50) -> Amarillo Incandescente (+1.00)
110
- * Negativo (0 a -1): Negro (0.0) -> Azul Eléctrico (-0.50) -> Violeta Neón (-1.00)
111
  */
112
  export const divergentFragmentShader = `
113
  varying float vIntensity;
@@ -123,7 +123,7 @@ void main() {
123
  float t = clamp(vIntensity, -1.0, 1.0);
124
  float absT = abs(t);
125
 
126
- // Optimización computacional para |t| < 0.01: retorno directo sin interpolación
127
  if (absT < 0.01) {
128
  gl_FragColor = vec4(vec3(0.0), 0.05 * baseOpacity * solidEdge);
129
  return;
@@ -134,7 +134,7 @@ void main() {
134
  vec3 color = vec3(0.0);
135
 
136
  if (t > 0.0) {
137
- // Positivo (0 a +1): Negro -> Naranja (#FF8000) -> Amarillo (#FFE600)
138
  if (t < 0.50) {
139
  float k = t / 0.50;
140
  color = mix(vec3(0.0), vec3(1.0, 0.5, 0.0), k);
@@ -143,7 +143,7 @@ void main() {
143
  color = mix(vec3(1.0, 0.5, 0.0), vec3(1.0, 0.95, 0.0), k);
144
  }
145
  } else {
146
- // Negativo (0 a -1): Negro -> Azul Eléctrico (#0040FF) -> Violeta Neón (#9900E6)
147
  float f = -t;
148
  if (f < 0.50) {
149
  float k = f / 0.50;
 
34
  * Calculates CPU color and dynamic opacity based on activation value.
35
  *
36
  * Color Ramps:
37
+ * Positive (0 to +1): Black (0.0) -> Orange (+0.50) -> Yellow (+1.00)
38
+ * Negative (0 to -1): Black (0.0) -> Electric Blue (-0.50) -> Violet (-1.00)
39
  *
40
  * @param {number} val - Activation value (v)
41
  * @param {number} [absMax=1.0] - Maximum absolute value for normalization
 
46
  let t = Math.max(-1.0, Math.min(1.0, val / denom));
47
  const absT = Math.abs(t);
48
 
49
+ // Computational short-circuit for |t| < 0.01
50
  if (absT < 0.01) {
51
  return { r: 0.0, g: 0.0, b: 0.0, alpha: 0.05 };
52
  }
 
56
  let r = 0.0, g = 0.0, b = 0.0;
57
 
58
  if (t > 0) {
59
+ // Positive (0 to +1): Black (0.0) -> Orange (+0.50) -> Incandescent Yellow (+1.00)
60
  if (t < 0.50) {
61
  const k = t / 0.50;
62
  r = 1.0 * k;
 
69
  b = 0.0;
70
  }
71
  } else {
72
+ // Negative (0 to -1): Black (0.0) -> Electric Blue (-0.50) -> Neon Violet (-1.00)
73
  const f = -t;
74
  if (f < 0.50) {
75
  const k = f / 0.50;
 
106
 
107
  /**
108
  * GLSL Fragment Shader for Dual Divergent Color Ramps (Solid Circular Points):
109
+ * Positive (0 to +1): Black (0.0) -> Orange (+0.50) -> Incandescent Yellow (+1.00)
110
+ * Negative (0 to -1): Black (0.0) -> Electric Blue (-0.50) -> Neon Violet (-1.00)
111
  */
112
  export const divergentFragmentShader = `
113
  varying float vIntensity;
 
123
  float t = clamp(vIntensity, -1.0, 1.0);
124
  float absT = abs(t);
125
 
126
+ // Computational short-circuit for |t| < 0.01: early return without interpolation
127
  if (absT < 0.01) {
128
  gl_FragColor = vec4(vec3(0.0), 0.05 * baseOpacity * solidEdge);
129
  return;
 
134
  vec3 color = vec3(0.0);
135
 
136
  if (t > 0.0) {
137
+ // Positive (0 to +1): Black -> Orange (#FF8000) -> Yellow (#FFE600)
138
  if (t < 0.50) {
139
  float k = t / 0.50;
140
  color = mix(vec3(0.0), vec3(1.0, 0.5, 0.0), k);
 
143
  color = mix(vec3(1.0, 0.5, 0.0), vec3(1.0, 0.95, 0.0), k);
144
  }
145
  } else {
146
+ // Negative (0 to -1): Black -> Electric Blue (#0040FF) -> Neon Violet (#9900E6)
147
  float f = -t;
148
  if (f < 0.50) {
149
  float k = f / 0.50;
tests/CompareMode.test.js CHANGED
@@ -3,7 +3,7 @@ import * as THREE from 'three';
3
  import { Instancer } from '../src/visualizer/Instancer.js';
4
 
5
  describe('Compare Mode Sequence Engine', () => {
6
- it('debe procesar e instanciar secuencias de tokens de 1 a 1024 elementos', () => {
7
  const scene = new THREE.Scene();
8
  const instancer = new Instancer(scene);
9
 
@@ -29,7 +29,7 @@ describe('Compare Mode Sequence Engine', () => {
29
  expect(instancer.activeGroup.children.length).toBeGreaterThanOrEqual(5);
30
  });
31
 
32
- it('debe retornar lista vacía si la respuesta de compare es nula o vacía', () => {
33
  const scene = new THREE.Scene();
34
  const instancer = new Instancer(scene);
35
 
@@ -38,7 +38,7 @@ describe('Compare Mode Sequence Engine', () => {
38
  expect(instancer.activeGroup.children.length).toBe(0);
39
  });
40
 
41
- it('debe soportar secuencias grandes (ej. 50 tokens)', () => {
42
  const scene = new THREE.Scene();
43
  const instancer = new Instancer(scene);
44
 
@@ -59,7 +59,7 @@ describe('Compare Mode Sequence Engine', () => {
59
  expect(labels[49].text).toBe('token_49');
60
  });
61
 
62
- it('debe reordenar hilos 3D con tween in-situ (reuse meshes, nuevo sequenceIndex)', async () => {
63
  const scene = new THREE.Scene();
64
  const instancer = new Instancer(scene);
65
 
 
3
  import { Instancer } from '../src/visualizer/Instancer.js';
4
 
5
  describe('Compare Mode Sequence Engine', () => {
6
+ it('processes and instantiates token sequences from 1 to 1024', () => {
7
  const scene = new THREE.Scene();
8
  const instancer = new Instancer(scene);
9
 
 
29
  expect(instancer.activeGroup.children.length).toBeGreaterThanOrEqual(5);
30
  });
31
 
32
+ it('returns an empty list when compare response is null or empty', () => {
33
  const scene = new THREE.Scene();
34
  const instancer = new Instancer(scene);
35
 
 
38
  expect(instancer.activeGroup.children.length).toBe(0);
39
  });
40
 
41
+ it('supports large sequences (e.g. 50 tokens)', () => {
42
  const scene = new THREE.Scene();
43
  const instancer = new Instancer(scene);
44
 
 
59
  expect(labels[49].text).toBe('token_49');
60
  });
61
 
62
+ it('reorders 3D threads with in-place tween (reuse meshes, new sequenceIndex)', async () => {
63
  const scene = new THREE.Scene();
64
  const instancer = new Instancer(scene);
65
 
tests/DivergentShading.test.js CHANGED
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
2
  import { getDivergentColor } from '../src/visualizer/DivergentShading.js';
3
 
4
  describe('Divergent Shading CPU Algorithm', () => {
5
- it('debe mapear valores positivos pico (+1.0) a Amarillo Incandescente', () => {
6
  const { r, g, b, alpha } = getDivergentColor(1.0, 1.0);
7
  expect(r).toBeCloseTo(1.0, 5);
8
  expect(g).toBeCloseTo(0.95, 5);
@@ -10,14 +10,14 @@ describe('Divergent Shading CPU Algorithm', () => {
10
  expect(alpha).toBeCloseTo(1.0, 5);
11
  });
12
 
13
- it('debe mapear valores positivos intermedios (+0.5) a Naranja', () => {
14
  const { r, g, b } = getDivergentColor(0.5, 1.0);
15
  expect(r).toBeCloseTo(1.0, 5);
16
  expect(g).toBeCloseTo(0.5, 5);
17
  expect(b).toBe(0.0);
18
  });
19
 
20
- it('debe mapear el valor cero (0) a Negro con opacidad mínima (~0.05)', () => {
21
  const { r, g, b, alpha } = getDivergentColor(0.0, 1.0);
22
  expect(r).toBe(0.0);
23
  expect(g).toBe(0.0);
@@ -25,7 +25,7 @@ describe('Divergent Shading CPU Algorithm', () => {
25
  expect(alpha).toBeCloseTo(0.05, 5);
26
  });
27
 
28
- it('debe optimizar y mapear valores con |t| < 0.01 directamente a Negro con opacidad mínima (~0.05)', () => {
29
  const { r, g, b, alpha } = getDivergentColor(0.005, 1.0);
30
  expect(r).toBe(0.0);
31
  expect(g).toBe(0.0);
@@ -33,14 +33,14 @@ describe('Divergent Shading CPU Algorithm', () => {
33
  expect(alpha).toBeCloseTo(0.05, 5);
34
  });
35
 
36
- it('debe mapear valores negativos intermedios (-0.5) a Azul Eléctrico', () => {
37
  const { r, g, b } = getDivergentColor(-0.5, 1.0);
38
  expect(r).toBeCloseTo(0.0, 5);
39
  expect(g).toBeCloseTo(0.25, 5);
40
  expect(b).toBeCloseTo(1.0, 5);
41
  });
42
 
43
- it('debe mapear valores negativos pico (-1.0) a Violeta Neón', () => {
44
  const { r, g, b, alpha } = getDivergentColor(-1.0, 1.0);
45
  expect(r).toBeCloseTo(0.6, 5);
46
  expect(g).toBeCloseTo(0.0, 5);
 
2
  import { getDivergentColor } from '../src/visualizer/DivergentShading.js';
3
 
4
  describe('Divergent Shading CPU Algorithm', () => {
5
+ it('maps peak positive (+1.0) to incandescent yellow', () => {
6
  const { r, g, b, alpha } = getDivergentColor(1.0, 1.0);
7
  expect(r).toBeCloseTo(1.0, 5);
8
  expect(g).toBeCloseTo(0.95, 5);
 
10
  expect(alpha).toBeCloseTo(1.0, 5);
11
  });
12
 
13
+ it('maps mid positive (+0.5) to orange', () => {
14
  const { r, g, b } = getDivergentColor(0.5, 1.0);
15
  expect(r).toBeCloseTo(1.0, 5);
16
  expect(g).toBeCloseTo(0.5, 5);
17
  expect(b).toBe(0.0);
18
  });
19
 
20
+ it('maps zero to black with minimum opacity (~0.05)', () => {
21
  const { r, g, b, alpha } = getDivergentColor(0.0, 1.0);
22
  expect(r).toBe(0.0);
23
  expect(g).toBe(0.0);
 
25
  expect(alpha).toBeCloseTo(0.05, 5);
26
  });
27
 
28
+ it('short-circuits |t| < 0.01 to black with minimum opacity (~0.05)', () => {
29
  const { r, g, b, alpha } = getDivergentColor(0.005, 1.0);
30
  expect(r).toBe(0.0);
31
  expect(g).toBe(0.0);
 
33
  expect(alpha).toBeCloseTo(0.05, 5);
34
  });
35
 
36
+ it('maps mid negative (-0.5) to electric blue', () => {
37
  const { r, g, b } = getDivergentColor(-0.5, 1.0);
38
  expect(r).toBeCloseTo(0.0, 5);
39
  expect(g).toBeCloseTo(0.25, 5);
40
  expect(b).toBeCloseTo(1.0, 5);
41
  });
42
 
43
+ it('maps peak negative (-1.0) to neon violet', () => {
44
  const { r, g, b, alpha } = getDivergentColor(-1.0, 1.0);
45
  expect(r).toBeCloseTo(0.6, 5);
46
  expect(g).toBeCloseTo(0.0, 5);
tests/LayoutEngine.test.js CHANGED
@@ -2,14 +2,14 @@ import { describe, it, expect } from 'vitest';
2
  import { executeLayoutMath, LayoutEngine } from '../src/visualizer/LayoutEngine.js';
3
 
4
  describe('LayoutEngine Math Tests', () => {
5
- it('debe calcular la coordenada X correcta según el índice e intervalo', () => {
6
  const threadIndex = 3;
7
  const spacingX = 2.5;
8
  const posX = executeLayoutMath.calculateX(threadIndex, spacingX);
9
  expect(posX).toBe(7.5);
10
  });
11
 
12
- it('debe escalar las posiciones Z según el factor de ancho Z', () => {
13
  const pointIndex = 10;
14
  const baseSpacing = 0.1;
15
  const zWidth = 2.0;
@@ -17,12 +17,12 @@ describe('LayoutEngine Math Tests', () => {
17
  expect(posZ).toBeCloseTo(2.0, 5);
18
  });
19
 
20
- it('debe instanciar LayoutEngine con valores por defecto y actualizar posiciones', () => {
21
  const engine = new LayoutEngine();
22
  expect(engine).toBeDefined();
23
  });
24
 
25
- it('debe mapear puntos 3D en modo ANÁLISIS alineados en Z=0 con apilamiento vertical Y', () => {
26
  const engine = new LayoutEngine({ scaleX: 1.0 });
27
  const vector = [1.0, -1.0, 0.0];
28
  // sequenceIndex = 0 (Thread 1) vs sequenceIndex = 1 (Thread 2)
@@ -35,7 +35,7 @@ describe('LayoutEngine Math Tests', () => {
35
  expect(pointsSlot0[0].y).toBeGreaterThan(pointsSlot1[0].y);
36
  });
37
 
38
- it('debe ajustar dinámicamente la separación vertical Y al cambiar spacingY', () => {
39
  const engine = new LayoutEngine({ scaleX: 1.0 });
40
  const vector = [0.0, 0.0];
41
  const pointsDefaultY = engine.mapVectorTo3DPoints(vector, 0, 'ANALYSIS', 5, 40.0);
 
2
  import { executeLayoutMath, LayoutEngine } from '../src/visualizer/LayoutEngine.js';
3
 
4
  describe('LayoutEngine Math Tests', () => {
5
+ it('computes X from index and interval', () => {
6
  const threadIndex = 3;
7
  const spacingX = 2.5;
8
  const posX = executeLayoutMath.calculateX(threadIndex, spacingX);
9
  expect(posX).toBe(7.5);
10
  });
11
 
12
+ it('scales Z positions by width factor', () => {
13
  const pointIndex = 10;
14
  const baseSpacing = 0.1;
15
  const zWidth = 2.0;
 
17
  expect(posZ).toBeCloseTo(2.0, 5);
18
  });
19
 
20
+ it('constructs LayoutEngine with defaults and updates positions', () => {
21
  const engine = new LayoutEngine();
22
  expect(engine).toBeDefined();
23
  });
24
 
25
+ it('maps 3D points in ANALYSIS mode aligned at Z=0 with vertical Y stack', () => {
26
  const engine = new LayoutEngine({ scaleX: 1.0 });
27
  const vector = [1.0, -1.0, 0.0];
28
  // sequenceIndex = 0 (Thread 1) vs sequenceIndex = 1 (Thread 2)
 
35
  expect(pointsSlot0[0].y).toBeGreaterThan(pointsSlot1[0].y);
36
  });
37
 
38
+ it('dynamically adjusts vertical Y spacing when spacingY changes', () => {
39
  const engine = new LayoutEngine({ scaleX: 1.0 });
40
  const vector = [0.0, 0.0];
41
  const pointsDefaultY = engine.mapVectorTo3DPoints(vector, 0, 'ANALYSIS', 5, 40.0);
tests/ThreadLabels.test.js CHANGED
@@ -68,12 +68,12 @@ describe('ThreadLabels Component Tests', () => {
68
  threadLabels = new ThreadLabels(container);
69
  });
70
 
71
- it('debe inicializar el contenedor overlay de etiquetas', () => {
72
  const overlay = container.querySelector('#thread-labels-container');
73
  expect(overlay).not.toBeNull();
74
  });
75
 
76
- it('debe registrar tarjetas de etiquetas para cada hilo', () => {
77
  const mockItems = [
78
  { id: 'vec_a', text: 'KING', type: 'word_a', origin3D: new THREE.Vector3(0, 50, 0) },
79
  { id: 'res', text: 'RESULT', type: 'res', origin3D: new THREE.Vector3(0, -50, 0) }
@@ -87,7 +87,7 @@ describe('ThreadLabels Component Tests', () => {
87
  expect(cards[1].classList.contains('res-label')).toBe(true);
88
  });
89
 
90
- it('debe limpiar las etiquetas registradas', () => {
91
  const mockItems = [
92
  { id: 'vec_a', text: 'KING', type: 'word_a', origin3D: new THREE.Vector3(0, 50, 0) }
93
  ];
@@ -101,7 +101,7 @@ describe('ThreadLabels Component Tests', () => {
101
  expect(cards.length).toBe(0);
102
  });
103
 
104
- it('debe actualizar origins in-situ durante tween de reorder', () => {
105
  threadLabels.setLabels([
106
  { id: 'tok_0', text: 'king', type: 'compare', origin3D: new THREE.Vector3(0, 10, 0) },
107
  { id: 'tok_1', text: 'queen', type: 'compare', origin3D: new THREE.Vector3(0, 0, 0) },
 
68
  threadLabels = new ThreadLabels(container);
69
  });
70
 
71
+ it('initializes the labels overlay container', () => {
72
  const overlay = container.querySelector('#thread-labels-container');
73
  expect(overlay).not.toBeNull();
74
  });
75
 
76
+ it('registers label cards for each thread', () => {
77
  const mockItems = [
78
  { id: 'vec_a', text: 'KING', type: 'word_a', origin3D: new THREE.Vector3(0, 50, 0) },
79
  { id: 'res', text: 'RESULT', type: 'res', origin3D: new THREE.Vector3(0, -50, 0) }
 
87
  expect(cards[1].classList.contains('res-label')).toBe(true);
88
  });
89
 
90
+ it('clears registered labels', () => {
91
  const mockItems = [
92
  { id: 'vec_a', text: 'KING', type: 'word_a', origin3D: new THREE.Vector3(0, 50, 0) }
93
  ];
 
101
  expect(cards.length).toBe(0);
102
  });
103
 
104
+ it('updates origins in-place during reorder tween', () => {
105
  threadLabels.setLabels([
106
  { id: 'tok_0', text: 'king', type: 'compare', origin3D: new THREE.Vector3(0, 10, 0) },
107
  { id: 'tok_1', text: 'queen', type: 'compare', origin3D: new THREE.Vector3(0, 0, 0) },
tests/ThreadSliders.test.js CHANGED
@@ -27,7 +27,7 @@ function parseRangeInput(html, id) {
27
 
28
  /**
29
  * Defaults + ranges. Most stay linearly centered (defaultMid === (min+max)/2).
30
- * Amplitud Y keeps default 7 (no scene regression) with max 40 → asymmetric by design.
31
  */
32
  const SPATIAL_SLIDER_SPECS = [
33
  { id: 'thread-spacing-slider', defaultMid: 0.4, min: 0.1, max: 0.7, step: 0.05, centered: true },
@@ -169,7 +169,7 @@ describe('wireThreadSliders — dblclick reset', () => {
169
  expect(changeCount).toBe(1);
170
  });
171
 
172
- it('uses context override when defined for current MODE/VISTA/RENDER', () => {
173
  SPATIAL_DEFAULT_OVERRIDES['COMPARE|ANALYSIS|MESH'] = { threadSpacing: 0.6 };
174
  overrideKeys.push('COMPARE|ANALYSIS|MESH');
175
 
 
27
 
28
  /**
29
  * Defaults + ranges. Most stay linearly centered (defaultMid === (min+max)/2).
30
+ * Amplitude Y keeps default 7 (no scene regression) with max 40 → asymmetric by design.
31
  */
32
  const SPATIAL_SLIDER_SPECS = [
33
  { id: 'thread-spacing-slider', defaultMid: 0.4, min: 0.1, max: 0.7, step: 0.05, centered: true },
 
169
  expect(changeCount).toBe(1);
170
  });
171
 
172
+ it('uses context override when defined for current MODE/VIEW/RENDER', () => {
173
  SPATIAL_DEFAULT_OVERRIDES['COMPARE|ANALYSIS|MESH'] = { threadSpacing: 0.6 };
174
  overrideKeys.push('COMPARE|ANALYSIS|MESH');
175
 
tests/spatialSliderDefaults.test.js CHANGED
@@ -60,7 +60,7 @@ describe('resolveSpatialDefaults', () => {
60
  expect(d.threadWidth).toBe(GLOBAL_SPATIAL_DEFAULTS.threadWidth);
61
  });
62
 
63
- it('applies captured ARITHMETIC|ANALYSIS|POINTS preset (Amplitud 40)', () => {
64
  // Production override lives in SPATIAL_DEFAULT_OVERRIDES — assert the seam.
65
  const d = resolveSpatialDefaults({
66
  workspaceMode: 'ARITHMETIC',
 
60
  expect(d.threadWidth).toBe(GLOBAL_SPATIAL_DEFAULTS.threadWidth);
61
  });
62
 
63
+ it('applies captured ARITHMETIC|ANALYSIS|POINTS preset (Amplitude 40)', () => {
64
  // Production override lives in SPATIAL_DEFAULT_OVERRIDES — assert the seam.
65
  const d = resolveSpatialDefaults({
66
  workspaceMode: 'ARITHMETIC',