Spaces:
Sleeping
Sleeping
feat(ui): defaults for ARITHMETIC + ANALYSIS + POINTS from capture
Browse filesPin 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 +3 -2
- CHANGELOG.md +4 -0
- src/engine/Navigation.js +3 -3
- src/main.js +21 -1
- src/ui/ThreadSliders.js +17 -0
- src/ui/spatialSliderDefaults.js +8 -1
- tests/Navigation.test.js +36 -0
- tests/ThreadSliders.test.js +27 -1
- tests/spatialSliderDefaults.test.js +14 -0
.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 |
-
- **
|
| 115 |
-
- **
|
|
|
|
| 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 |
-
//
|
| 168 |
-
this.camera.position.set(
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
});
|