hbauzan Cursor commited on
Commit
3bb263a
·
1 Parent(s): 14e637b

feat(ui): defaults for ARITHMETIC + ANALYSIS + POINTS from capture

Browse files

Pin spatial preset (Amplitud 40) and analysis camera POS/ROT from CAM
POSE overlay; reapply resolved defaults when MODE/VISTA/RENDER changes.

Co-Authored-By: Cursor <cursoragent@cursor.com>

.agents/skills/dev-protocol/lessons-learned.md CHANGED
@@ -111,8 +111,9 @@ Para lecturas de alta visibilidad y ligera carga computacional, implementar la p
111
 
112
  ### 3.4. Default Navigation Corridor Pose
113
  - **Invariante**: La vista inicial de **Navegación** usa la pose capturada `POS (-178.3, 13.5, 52.2)` + euler YXZ `ROT (-5.4°, -51.5°, 0°)` con sliders espaciales por defecto Separación $X=0.4$, Amplitud $Y=7.0$, Longitud $Z=0.2$.
114
- - **Overlay de captura**: El HUD `CAM POSE` solo se monta si `VITE_SHOW_CAM_POSE=true` (default `false` en `.env.example`). Sirve para releer POS/ROT desde una captura y actualizar `setNavigationView()`.
115
- - **Workflow de captura**: Para fijar una nueva vista default, activar el overlay, navegar a la pose deseada, screenshotear `POS`/`ROT` legibles, y actualizar `Navigation.setNavigationView()` + defaults de sliders espaciales si cambian.
 
116
 
117
  ---
118
 
 
111
 
112
  ### 3.4. Default Navigation Corridor Pose
113
  - **Invariante**: La vista inicial de **Navegación** usa la pose capturada `POS (-178.3, 13.5, 52.2)` + euler YXZ `ROT (-5.4°, -51.5°, 0°)` con sliders espaciales por defecto Separación $X=0.4$, Amplitud $Y=7.0$, Longitud $Z=0.2$.
114
+ - **Análisis (ARITHMETIC + POINTS)**: pose capturada `POS (-75.2, -0.8, 62.5)` + `ROT (0°, 0°, 0°)`; sliders vía override `ARITHMETIC|ANALYSIS|POINTS` (Amplitud $Y=40$, Grosor $0.05$, resto = global). Al cambiar MODE/VISTA/RENDER se reaplica `resolveSpatialDefaults` + sync UI.
115
+ - **Overlay de captura**: El HUD `CAM POSE` solo se monta si `VITE_SHOW_CAM_POSE=true` (default `false` en `.env.example`). Sirve para releer POS/ROT desde una captura y actualizar `setNavigationView()` / `setAnalysisView()`.
116
+ - **Workflow de captura**: Para fijar una nueva vista default, activar el overlay, navegar a la pose deseada, screenshotear `POS`/`ROT` legibles, y actualizar Navigation + `SPATIAL_DEFAULT_OVERRIDES` si cambian.
117
 
118
  ---
119
 
CHANGELOG.md CHANGED
@@ -5,6 +5,10 @@ All notable changes to VectorLab 3D will be documented in this file.
5
  ## [Unreleased]
6
 
7
  ### Changed
 
 
 
 
8
  - **Grosor Puntos mid → 0.05** (`feat/thickness-mid-0.05`):
9
  - Default/mid `0.05` ∈ `[0.01, 0.09]` step `0.01` (simétrico lineal).
10
  - **Amplitud (Y) max → 40** (`feat/amplitude-y-max-40`):
 
5
  ## [Unreleased]
6
 
7
  ### Changed
8
+ - **Defaults ARITHMETIC|ANALYSIS|POINTS** (`feat/arithmetic-analysis-points-defaults`):
9
+ - Sliders: Separación `0.4`, Distancia Y `10`, Amplitud Y `40`, Longitud Z `0.2`, Grosor `0.05` (override en `spatialSliderDefaults.js`).
10
+ - Cámara Análisis: `POS (-75.2, -0.8, 62.5)` / `ROT (0, 0, 0)`.
11
+ - Al cambiar MODE/VISTA/RENDER se reaplica el preset resuelto + sync de sliders.
12
  - **Grosor Puntos mid → 0.05** (`feat/thickness-mid-0.05`):
13
  - Default/mid `0.05` ∈ `[0.01, 0.09]` step `0.01` (simétrico lineal).
14
  - **Amplitud (Y) max → 40** (`feat/amplitude-y-max-40`):
src/engine/Navigation.js CHANGED
@@ -164,10 +164,10 @@ export class Navigation {
164
  }
165
 
166
  setAnalysisView() {
167
- // Position camera straight on facing XY plane, centered on vertically stacked threads
168
- this.camera.position.set(0, 0, 360);
169
- this.camera.lookAt(0, 0, 0);
170
  this.euler.set(0, 0, 0, 'YXZ');
 
171
  this.velocity.set(0, 0, 0);
172
  }
173
 
 
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);
171
  this.velocity.set(0, 0, 0);
172
  }
173
 
src/main.js CHANGED
@@ -11,7 +11,7 @@ import { Sidebar } from './ui/Sidebar.js';
11
  import { HUD } from './ui/HUD.js';
12
  import { CustomModal } from './ui/CustomModal.js';
13
  import { ThreadLabels } from './ui/ThreadLabels.js';
14
- import { threadSlidersMarkup, wireThreadSliders } from './ui/ThreadSliders.js';
15
  import { resolveSpatialDefaults } from './ui/spatialSliderDefaults.js';
16
 
17
  import { ComparePanel, COMPARE_AUTO_PRESETS } from './ui/ComparePanel.js';
@@ -61,6 +61,7 @@ class VectorLabApp {
61
  this.appContainer,
62
  (renderMode) => {
63
  state.setRenderMode(renderMode);
 
64
  this.refreshRender();
65
  },
66
  (viewMode) => {
@@ -70,6 +71,7 @@ class VectorLabApp {
70
  } else {
71
  this.navigation.setNavigationView();
72
  }
 
73
  this.refreshRender();
74
  },
75
  (workspaceMode) => {
@@ -133,6 +135,7 @@ class VectorLabApp {
133
  const sliderWrapper = document.createElement('div');
134
  sliderWrapper.innerHTML = threadSlidersMarkup(this.sliderConfig);
135
  const slidersEl = sliderWrapper.firstElementChild;
 
136
  // Insert sliders above the gizmo inside the right dock body.
137
  this.rightDock.body.insertBefore(slidersEl, this.axisGizmo.container);
138
 
@@ -147,8 +150,25 @@ class VectorLabApp {
147
  });
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  handleWorkspaceModeChange(mode) {
151
  state.setWorkspaceMode(mode);
 
152
  if (mode === 'COMPARE') {
153
  this.sidebar.element.classList.add('hidden');
154
  this.comparePanel.show();
 
11
  import { HUD } from './ui/HUD.js';
12
  import { CustomModal } from './ui/CustomModal.js';
13
  import { ThreadLabels } from './ui/ThreadLabels.js';
14
+ import { threadSlidersMarkup, wireThreadSliders, syncThreadSlidersFromConfig } from './ui/ThreadSliders.js';
15
  import { resolveSpatialDefaults } from './ui/spatialSliderDefaults.js';
16
 
17
  import { ComparePanel, COMPARE_AUTO_PRESETS } from './ui/ComparePanel.js';
 
61
  this.appContainer,
62
  (renderMode) => {
63
  state.setRenderMode(renderMode);
64
+ this.applyContextSpatialDefaults();
65
  this.refreshRender();
66
  },
67
  (viewMode) => {
 
71
  } else {
72
  this.navigation.setNavigationView();
73
  }
74
+ this.applyContextSpatialDefaults();
75
  this.refreshRender();
76
  },
77
  (workspaceMode) => {
 
135
  const sliderWrapper = document.createElement('div');
136
  sliderWrapper.innerHTML = threadSlidersMarkup(this.sliderConfig);
137
  const slidersEl = sliderWrapper.firstElementChild;
138
+ this.slidersEl = slidersEl;
139
  // Insert sliders above the gizmo inside the right dock body.
140
  this.rightDock.body.insertBefore(slidersEl, this.axisGizmo.container);
141
 
 
150
  });
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({
159
+ workspaceMode: state.workspaceMode,
160
+ viewMode: this.viewMode,
161
+ renderMode: state.renderMode,
162
+ });
163
+ Object.assign(this.sliderConfig, defaults);
164
+ if (this.slidersEl) {
165
+ syncThreadSlidersFromConfig(this.slidersEl, this.sliderConfig);
166
+ }
167
+ }
168
+
169
  handleWorkspaceModeChange(mode) {
170
  state.setWorkspaceMode(mode);
171
+ this.applyContextSpatialDefaults();
172
  if (mode === 'COMPARE') {
173
  this.sidebar.element.classList.add('hidden');
174
  this.comparePanel.show();
src/ui/ThreadSliders.js CHANGED
@@ -71,6 +71,23 @@ export function threadSlidersMarkup(config = {}) {
71
  }
72
 
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  /**
75
  * Binds real-time event listeners to sliders for immediate 60fps spatial updates.
76
  * Double-click on a range input restores that slider's default for the current
 
71
  }
72
 
73
 
74
+ /**
75
+ * Writes config values into range inputs + labels (e.g. after context default apply).
76
+ * @param {HTMLElement} container
77
+ * @param {Object} config
78
+ */
79
+ export function syncThreadSlidersFromConfig(container, config) {
80
+ if (!container || !config) return;
81
+ for (const binding of SPATIAL_SLIDER_BINDINGS) {
82
+ const val = config[binding.configKey];
83
+ if (val === undefined || Number.isNaN(val)) continue;
84
+ const input = container.querySelector(`#${binding.inputId}`);
85
+ const labelEl = container.querySelector(`#${binding.labelId}`);
86
+ if (input) input.value = String(val);
87
+ if (labelEl) labelEl.textContent = Number(val).toFixed(binding.decimals);
88
+ }
89
+ }
90
+
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
src/ui/spatialSliderDefaults.js CHANGED
@@ -30,7 +30,14 @@ export const GLOBAL_SPATIAL_DEFAULTS = Object.freeze({
30
  * @type {Record<string, Partial<SpatialSliderValues>>}
31
  */
32
  export const SPATIAL_DEFAULT_OVERRIDES = {
33
- // e.g. 'COMPARE|ANALYSIS': { threadAmplitudeY: 5.0 },
 
 
 
 
 
 
 
34
  };
35
 
36
  /**
 
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,
37
+ threadAmplitudeY: 40.0,
38
+ threadWidth: 0.2,
39
+ threadThickness: 0.05,
40
+ },
41
  };
42
 
43
  /**
tests/Navigation.test.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as THREE from 'three';
3
+ import { Navigation } from '../src/engine/Navigation.js';
4
+
5
+ function stubCamera() {
6
+ return {
7
+ position: new THREE.Vector3(),
8
+ quaternion: new THREE.Quaternion(),
9
+ lookAt() {},
10
+ };
11
+ }
12
+
13
+ describe('Navigation default poses', () => {
14
+ it('setAnalysisView uses captured ARITHMETIC|ANALYSIS|POINTS pose', () => {
15
+ const camera = stubCamera();
16
+ const nav = new Navigation(camera, { addEventListener() {} });
17
+ nav.setAnalysisView();
18
+
19
+ expect(nav.camera.position.x).toBeCloseTo(-75.2, 5);
20
+ expect(nav.camera.position.y).toBeCloseTo(-0.8, 5);
21
+ expect(nav.camera.position.z).toBeCloseTo(62.5, 5);
22
+ expect(nav.euler.x).toBeCloseTo(0, 5);
23
+ expect(nav.euler.y).toBeCloseTo(0, 5);
24
+ expect(nav.euler.z).toBeCloseTo(0, 5);
25
+ });
26
+
27
+ it('setNavigationView keeps corridor pose', () => {
28
+ const camera = stubCamera();
29
+ const nav = new Navigation(camera, { addEventListener() {} });
30
+ nav.setNavigationView();
31
+
32
+ expect(nav.camera.position.x).toBeCloseTo(-178.3, 5);
33
+ expect(nav.camera.position.y).toBeCloseTo(13.5, 5);
34
+ expect(nav.camera.position.z).toBeCloseTo(52.2, 5);
35
+ });
36
+ });
tests/ThreadSliders.test.js CHANGED
@@ -1,5 +1,5 @@
1
  import { describe, it, expect, afterEach } from 'vitest';
2
- import { threadSlidersMarkup, wireThreadSliders } from '../src/ui/ThreadSliders.js';
3
  import {
4
  GLOBAL_SPATIAL_DEFAULTS,
5
  SPATIAL_DEFAULT_OVERRIDES,
@@ -205,3 +205,29 @@ describe('wireThreadSliders — dblclick reset', () => {
205
  expect(spacingLabel.textContent).toBe('0.60');
206
  });
207
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { describe, it, expect, afterEach } from 'vitest';
2
+ import { threadSlidersMarkup, wireThreadSliders, syncThreadSlidersFromConfig } from '../src/ui/ThreadSliders.js';
3
  import {
4
  GLOBAL_SPATIAL_DEFAULTS,
5
  SPATIAL_DEFAULT_OVERRIDES,
 
205
  expect(spacingLabel.textContent).toBe('0.60');
206
  });
207
  });
208
+
209
+ describe('syncThreadSlidersFromConfig', () => {
210
+ it('writes config into inputs and labels', () => {
211
+ const ampInput = createMockEl('thread-amplitude-y-slider');
212
+ const ampLabel = createMockEl('thread-amplitude-y-val');
213
+ const byId = {
214
+ 'thread-amplitude-y-slider': ampInput,
215
+ 'thread-amplitude-y-val': ampLabel,
216
+ 'thread-spacing-slider': createMockEl(),
217
+ 'thread-spacing-val': createMockEl(),
218
+ 'thread-vector-dist-slider': createMockEl(),
219
+ 'thread-vector-dist-val': createMockEl(),
220
+ 'thread-width-slider': createMockEl(),
221
+ 'thread-width-val': createMockEl(),
222
+ 'thread-thickness-slider': createMockEl(),
223
+ 'thread-thickness-val': createMockEl(),
224
+ };
225
+ const container = {
226
+ querySelector: (sel) => byId[sel.replace('#', '')] || null,
227
+ };
228
+
229
+ syncThreadSlidersFromConfig(container, { threadAmplitudeY: 40.0 });
230
+ expect(ampInput.value).toBe('40');
231
+ expect(ampLabel.textContent).toBe('40.0');
232
+ });
233
+ });
tests/spatialSliderDefaults.test.js CHANGED
@@ -59,4 +59,18 @@ describe('resolveSpatialDefaults', () => {
59
  });
60
  expect(d.threadWidth).toBe(GLOBAL_SPATIAL_DEFAULTS.threadWidth);
61
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  });
 
59
  });
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',
67
+ viewMode: 'ANALYSIS',
68
+ renderMode: 'POINTS',
69
+ });
70
+ expect(d.threadSpacing).toBe(0.4);
71
+ expect(d.threadVectorDistance).toBe(10.0);
72
+ expect(d.threadAmplitudeY).toBe(40.0);
73
+ expect(d.threadWidth).toBe(0.2);
74
+ expect(d.threadThickness).toBe(0.05);
75
+ });
76
  });