hbauzan Cursor commited on
Commit
16b67eb
·
1 Parent(s): 24ba1bb

feat(galaxy): larger UMAP world and slower Galaxy flight profile

Browse files

Scale 96 + spacing×240 and Galaxy-only move/look profile (32 / 0.0014);
leaving Galaxy restores ANALYSIS/NAV defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>

src/engine/Navigation.js CHANGED
@@ -1,5 +1,6 @@
1
  import * as THREE from 'three';
2
  import { resolveCameraPose, VIEW_CAMERA_FALLBACKS } from './cameraViewDefaults.js';
 
3
 
4
  /**
5
  * Inertial Flight & Camera Navigation Controller (WASDQE + Mouse Drag Look + Shift Turbo).
@@ -21,8 +22,9 @@ export class Navigation {
21
  };
22
 
23
  this.velocity = new THREE.Vector3();
24
- this.moveSpeed = 75.0; // Bounded speed units/sec for responsive control
25
- this.turboMultiplier = 2.0;
 
26
  this.damping = 0.85;
27
  this._cameraTweenRaf = null;
28
 
@@ -63,9 +65,12 @@ export class Navigation {
63
  * @param {number} deltaY
64
  */
65
  applyLookDelta(deltaX, deltaY) {
 
 
 
66
  this.euler.setFromQuaternion(this.camera.quaternion);
67
- this.euler.y -= deltaX * 0.003;
68
- this.euler.x -= deltaY * 0.003;
69
  this.euler.x = Math.max(-Math.PI / 2 + 0.05, Math.min(Math.PI / 2 - 0.05, this.euler.x));
70
  this.camera.quaternion.setFromEuler(this.euler);
71
  }
 
1
  import * as THREE from 'three';
2
  import { resolveCameraPose, VIEW_CAMERA_FALLBACKS } from './cameraViewDefaults.js';
3
+ import { DEFAULT_FLIGHT_PROFILE } from './galaxyFlightProfile.js';
4
 
5
  /**
6
  * Inertial Flight & Camera Navigation Controller (WASDQE + Mouse Drag Look + Shift Turbo).
 
22
  };
23
 
24
  this.velocity = new THREE.Vector3();
25
+ this.moveSpeed = DEFAULT_FLIGHT_PROFILE.moveSpeed;
26
+ this.turboMultiplier = DEFAULT_FLIGHT_PROFILE.turboMultiplier;
27
+ this.lookSensitivity = DEFAULT_FLIGHT_PROFILE.lookSensitivity;
28
  this.damping = 0.85;
29
  this._cameraTweenRaf = null;
30
 
 
65
  * @param {number} deltaY
66
  */
67
  applyLookDelta(deltaX, deltaY) {
68
+ const sens = Number.isFinite(this.lookSensitivity)
69
+ ? this.lookSensitivity
70
+ : DEFAULT_FLIGHT_PROFILE.lookSensitivity;
71
  this.euler.setFromQuaternion(this.camera.quaternion);
72
+ this.euler.y -= deltaX * sens;
73
+ this.euler.x -= deltaY * sens;
74
  this.euler.x = Math.max(-Math.PI / 2 + 0.05, Math.min(Math.PI / 2 - 0.05, this.euler.x));
75
  this.camera.quaternion.setFromEuler(this.euler);
76
  }
src/engine/galaxyFlightProfile.js ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Flight feel profiles for Navigation (WASD/QE + mouse look).
3
+ * Galaxy VIEW uses a slower profile; ANALYSIS/NAVIGATION keep defaults.
4
+ */
5
+
6
+ /** @typedef {{ moveSpeed: number, turboMultiplier: number, lookSensitivity: number }} FlightProfile */
7
+
8
+ /** Current ANALYSIS / NAVIGATION feel (pre–Galaxy-feel baseline). */
9
+ export const DEFAULT_FLIGHT_PROFILE = Object.freeze({
10
+ moveSpeed: 75.0,
11
+ turboMultiplier: 2.0,
12
+ lookSensitivity: 0.003,
13
+ });
14
+
15
+ /**
16
+ * Galaxy-only: slower cruise + softer look so a larger UMAP world feels elegant.
17
+ * Calibrated with GALAXY_DEFAULT_SCALE ≈ 96 (RMS≈1 → ~several seconds across IT-core).
18
+ */
19
+ export const GALAXY_FLIGHT_PROFILE = Object.freeze({
20
+ moveSpeed: 32.0,
21
+ turboMultiplier: 1.75,
22
+ lookSensitivity: 0.0014,
23
+ });
24
+
25
+ /**
26
+ * @param {{ moveSpeed?: number, turboMultiplier?: number, lookSensitivity?: number }|null|undefined} nav
27
+ * @param {FlightProfile|null|undefined} profile
28
+ * @returns {typeof nav}
29
+ */
30
+ export function applyFlightProfile(nav, profile) {
31
+ if (!nav || !profile) return nav;
32
+ nav.moveSpeed = profile.moveSpeed;
33
+ nav.turboMultiplier = profile.turboMultiplier;
34
+ nav.lookSensitivity = profile.lookSensitivity;
35
+ return nav;
36
+ }
37
+
38
+ /** @param {object|null|undefined} nav */
39
+ export function applyGalaxyFlightProfile(nav) {
40
+ return applyFlightProfile(nav, GALAXY_FLIGHT_PROFILE);
41
+ }
42
+
43
+ /** @param {object|null|undefined} nav */
44
+ export function restoreDefaultFlightProfile(nav) {
45
+ return applyFlightProfile(nav, DEFAULT_FLIGHT_PROFILE);
46
+ }
src/main.js CHANGED
@@ -65,6 +65,10 @@ import {
65
  } from './ui/galaxyChrome.js';
66
  import { runGalaxyPipeline, compareTextsFingerprint } from './ui/galaxyPipeline.js';
67
  import { galaxyCameraTarget } from './visualizer/galaxyLayout.js';
 
 
 
 
68
  import {
69
  loadArithmeticSettings,
70
  saveArithmeticSettings,
@@ -390,6 +394,7 @@ class VHectorLabApp {
390
  this._preGalaxyTriad = entered.restore;
391
  this.viewMode = entered.viewMode;
392
  this.galaxyMethod = entered.method;
 
393
  this.navbar.setModeRenderLocked(true);
394
  this.navbar.setGalaxyMethod(entered.method);
395
  this.navbar.setViewMode(entered.viewMode);
@@ -417,6 +422,7 @@ class VHectorLabApp {
417
  this._galaxyFramePending = false;
418
  this.comparePanel?.clearGalaxyProgress?.();
419
  this.viewMode = left.viewMode;
 
420
  this.navbar.setModeRenderLocked(false);
421
  this.navbar.setViewMode(left.viewMode);
422
 
@@ -636,7 +642,8 @@ class VHectorLabApp {
636
  // Prefer fit-box so sparse cores stay readable; look-at is IT centroid when present.
637
  const box = target.box.clone();
638
  // Inflate slightly so focus isn't clipped
639
- box.expandByScalar(8);
 
640
  void this.navigation.animateToFitBox(box, {
641
  viewMode: 'NAVIGATION',
642
  durationMs: 550,
 
65
  } from './ui/galaxyChrome.js';
66
  import { runGalaxyPipeline, compareTextsFingerprint } from './ui/galaxyPipeline.js';
67
  import { galaxyCameraTarget } from './visualizer/galaxyLayout.js';
68
+ import {
69
+ applyGalaxyFlightProfile,
70
+ restoreDefaultFlightProfile,
71
+ } from './engine/galaxyFlightProfile.js';
72
  import {
73
  loadArithmeticSettings,
74
  saveArithmeticSettings,
 
394
  this._preGalaxyTriad = entered.restore;
395
  this.viewMode = entered.viewMode;
396
  this.galaxyMethod = entered.method;
397
+ applyGalaxyFlightProfile(this.navigation);
398
  this.navbar.setModeRenderLocked(true);
399
  this.navbar.setGalaxyMethod(entered.method);
400
  this.navbar.setViewMode(entered.viewMode);
 
422
  this._galaxyFramePending = false;
423
  this.comparePanel?.clearGalaxyProgress?.();
424
  this.viewMode = left.viewMode;
425
+ restoreDefaultFlightProfile(this.navigation);
426
  this.navbar.setModeRenderLocked(false);
427
  this.navbar.setViewMode(left.viewMode);
428
 
 
642
  // Prefer fit-box so sparse cores stay readable; look-at is IT centroid when present.
643
  const box = target.box.clone();
644
  // Inflate slightly so focus isn't clipped
645
+ // Inflate padding scales with cloud size so fit framing stays comfortable
646
+ box.expandByScalar(24);
647
  void this.navigation.animateToFitBox(box, {
648
  viewMode: 'NAVIGATION',
649
  durationMs: 550,
src/visualizer/Instancer.js CHANGED
@@ -13,7 +13,7 @@ import {
13
  computeDimRelationMetrics,
14
  hasGroupsForDimContrast,
15
  } from './groupDimContrast.js';
16
- import { GALAXY_DEFAULT_SCALE, layoutGalaxyPoints } from './galaxyLayout.js';
17
 
18
  /**
19
  * Instancer Manager for rendering vector points, ribbons, and highlights in the Three.js scene.
@@ -431,10 +431,7 @@ export class Instancer {
431
  const thicknessFactor = (spatialConfig && spatialConfig.threadThickness !== undefined)
432
  ? spatialConfig.threadThickness
433
  : 0.3;
434
- // Prefer threadSpacing as global galaxy spread when present.
435
- const scale = (spatialConfig && spatialConfig.threadSpacing !== undefined)
436
- ? Math.max(8, Number(spatialConfig.threadSpacing) * 40)
437
- : GALAXY_DEFAULT_SCALE;
438
 
439
  const { pointsData, labels } = layoutGalaxyPoints(
440
  compareResponse.items,
 
13
  computeDimRelationMetrics,
14
  hasGroupsForDimContrast,
15
  } from './groupDimContrast.js';
16
+ import { layoutGalaxyPoints, resolveGalaxyWorldScale } from './galaxyLayout.js';
17
 
18
  /**
19
  * Instancer Manager for rendering vector points, ribbons, and highlights in the Three.js scene.
 
431
  const thicknessFactor = (spatialConfig && spatialConfig.threadThickness !== undefined)
432
  ? spatialConfig.threadThickness
433
  : 0.3;
434
+ const scale = resolveGalaxyWorldScale(spatialConfig);
 
 
 
435
 
436
  const { pointsData, labels } = layoutGalaxyPoints(
437
  compareResponse.items,
src/visualizer/galaxyLayout.js CHANGED
@@ -6,8 +6,34 @@
6
  import * as THREE from 'three';
7
  import { IT_CORE_GROUP_ID } from '../ui/itCoreCorpus.js';
8
 
9
- /** Default world scale for server-normalized (RMS≈1) UMAP coords. */
10
- export const GALAXY_DEFAULT_SCALE = 48;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  /**
13
  * @param {Array<{ id?: string, text?: string, groupId?: string, groupLabel?: string, cosine_vs_first?: number }>} items
 
6
  import * as THREE from 'three';
7
  import { IT_CORE_GROUP_ID } from '../ui/itCoreCorpus.js';
8
 
9
+ /**
10
+ * Default world scale for server-normalized (RMS≈1) UMAP coords.
11
+ * Raised (was 48) so Galaxy feels spacious with the slower flight profile.
12
+ */
13
+ export const GALAXY_DEFAULT_SCALE = 96;
14
+
15
+ /**
16
+ * Maps Spatial `threadSpacing` → galaxy world scale.
17
+ * At global default spacing 0.4 → 96 (matches GALAXY_DEFAULT_SCALE).
18
+ * Was 40 (→ scale 16 at spacing 0.4 — cramped).
19
+ */
20
+ export const GALAXY_SPACING_TO_SCALE = 240;
21
+
22
+ /** Floor so tiny spacing sliders never collapse the cloud. */
23
+ export const GALAXY_MIN_SCALE = 32;
24
+
25
+ /**
26
+ * Resolve Galaxy world scale from Spatial sliders (or default).
27
+ * @param {{ threadSpacing?: number }|null|undefined} spatialConfig
28
+ * @returns {number}
29
+ */
30
+ export function resolveGalaxyWorldScale(spatialConfig) {
31
+ if (spatialConfig && spatialConfig.threadSpacing !== undefined) {
32
+ const raw = Number(spatialConfig.threadSpacing) * GALAXY_SPACING_TO_SCALE;
33
+ return Math.max(GALAXY_MIN_SCALE, Number.isFinite(raw) ? raw : GALAXY_DEFAULT_SCALE);
34
+ }
35
+ return GALAXY_DEFAULT_SCALE;
36
+ }
37
 
38
  /**
39
  * @param {Array<{ id?: string, text?: string, groupId?: string, groupLabel?: string, cosine_vs_first?: number }>} items
tests/Navigation.test.js CHANGED
@@ -1,6 +1,12 @@
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 {
@@ -66,3 +72,23 @@ describe('Navigation default poses', () => {
66
  expect(nav.camera.position.z).toBeCloseTo(52.2, 5);
67
  });
68
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { describe, it, expect } from 'vitest';
2
  import * as THREE from 'three';
3
  import { Navigation } from '../src/engine/Navigation.js';
4
+ import {
5
+ DEFAULT_FLIGHT_PROFILE,
6
+ GALAXY_FLIGHT_PROFILE,
7
+ applyGalaxyFlightProfile,
8
+ restoreDefaultFlightProfile,
9
+ } from '../src/engine/galaxyFlightProfile.js';
10
 
11
  function stubCamera() {
12
  return {
 
72
  expect(nav.camera.position.z).toBeCloseTo(52.2, 5);
73
  });
74
  });
75
+
76
+ describe('Navigation flight profile', () => {
77
+ it('starts on default profile and applyLookDelta uses lookSensitivity', () => {
78
+ const camera = stubCamera();
79
+ const nav = new Navigation(camera, { addEventListener() {} });
80
+ expect(nav.moveSpeed).toBe(DEFAULT_FLIGHT_PROFILE.moveSpeed);
81
+ expect(nav.lookSensitivity).toBe(DEFAULT_FLIGHT_PROFILE.lookSensitivity);
82
+
83
+ applyGalaxyFlightProfile(nav);
84
+ expect(nav.moveSpeed).toBe(GALAXY_FLIGHT_PROFILE.moveSpeed);
85
+
86
+ nav.euler.set(0, 0, 0, 'YXZ');
87
+ nav.camera.quaternion.setFromEuler(nav.euler);
88
+ nav.applyLookDelta(100, 0);
89
+ expect(nav.euler.y).toBeCloseTo(-100 * GALAXY_FLIGHT_PROFILE.lookSensitivity, 6);
90
+
91
+ restoreDefaultFlightProfile(nav);
92
+ expect(nav.moveSpeed).toBe(DEFAULT_FLIGHT_PROFILE.moveSpeed);
93
+ });
94
+ });
tests/galaxyFlightProfile.test.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ DEFAULT_FLIGHT_PROFILE,
4
+ GALAXY_FLIGHT_PROFILE,
5
+ applyFlightProfile,
6
+ applyGalaxyFlightProfile,
7
+ restoreDefaultFlightProfile,
8
+ } from '../src/engine/galaxyFlightProfile.js';
9
+
10
+ function stubNav(overrides = {}) {
11
+ return {
12
+ moveSpeed: DEFAULT_FLIGHT_PROFILE.moveSpeed,
13
+ turboMultiplier: DEFAULT_FLIGHT_PROFILE.turboMultiplier,
14
+ lookSensitivity: DEFAULT_FLIGHT_PROFILE.lookSensitivity,
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ describe('galaxyFlightProfile', () => {
20
+ it('Galaxy profile is slower translation and look than default', () => {
21
+ expect(GALAXY_FLIGHT_PROFILE.moveSpeed).toBeLessThan(DEFAULT_FLIGHT_PROFILE.moveSpeed);
22
+ expect(GALAXY_FLIGHT_PROFILE.lookSensitivity).toBeLessThan(
23
+ DEFAULT_FLIGHT_PROFILE.lookSensitivity,
24
+ );
25
+ expect(GALAXY_FLIGHT_PROFILE.turboMultiplier).toBeGreaterThan(1);
26
+ });
27
+
28
+ it('applyGalaxyFlightProfile sets slower speeds on nav', () => {
29
+ const nav = stubNav();
30
+ applyGalaxyFlightProfile(nav);
31
+ expect(nav.moveSpeed).toBe(GALAXY_FLIGHT_PROFILE.moveSpeed);
32
+ expect(nav.turboMultiplier).toBe(GALAXY_FLIGHT_PROFILE.turboMultiplier);
33
+ expect(nav.lookSensitivity).toBe(GALAXY_FLIGHT_PROFILE.lookSensitivity);
34
+ });
35
+
36
+ it('restoreDefaultFlightProfile restores ANALYSIS/NAV feel', () => {
37
+ const nav = stubNav();
38
+ applyGalaxyFlightProfile(nav);
39
+ restoreDefaultFlightProfile(nav);
40
+ expect(nav.moveSpeed).toBe(DEFAULT_FLIGHT_PROFILE.moveSpeed);
41
+ expect(nav.turboMultiplier).toBe(DEFAULT_FLIGHT_PROFILE.turboMultiplier);
42
+ expect(nav.lookSensitivity).toBe(DEFAULT_FLIGHT_PROFILE.lookSensitivity);
43
+ });
44
+
45
+ it('applyFlightProfile is a no-op for missing nav/profile', () => {
46
+ expect(applyFlightProfile(null, GALAXY_FLIGHT_PROFILE)).toBeNull();
47
+ const nav = stubNav({ moveSpeed: 99 });
48
+ expect(applyFlightProfile(nav, null)).toBe(nav);
49
+ expect(nav.moveSpeed).toBe(99);
50
+ });
51
+ });
tests/galaxyInstancer.test.js CHANGED
@@ -2,6 +2,10 @@ import { describe, expect, it } from 'vitest';
2
  import * as THREE from 'three';
3
  import { Instancer } from '../src/visualizer/Instancer.js';
4
  import { IT_CORE_GROUP_ID } from '../src/ui/itCoreCorpus.js';
 
 
 
 
5
 
6
  describe('Instancer.renderGalaxyData', () => {
7
  it('mounts a single POINTS mesh and no ribbons', () => {
@@ -44,6 +48,7 @@ describe('Instancer.renderGalaxyData', () => {
44
  expect(labels).toHaveLength(3);
45
  expect(labels[0].text).toBe('server');
46
  expect(labels[0].groupId).toBe(IT_CORE_GROUP_ID);
 
47
 
48
  expect(instancer.activeGroup.children).toHaveLength(1);
49
  expect(instancer.activeGroup.children[0].type).toBe('Points');
@@ -51,6 +56,23 @@ describe('Instancer.renderGalaxyData', () => {
51
  expect(instancer.compareRuntime).toBeNull();
52
  });
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  it('returns empty when positions missing', () => {
55
  const scene = new THREE.Scene();
56
  const instancer = new Instancer(scene);
 
2
  import * as THREE from 'three';
3
  import { Instancer } from '../src/visualizer/Instancer.js';
4
  import { IT_CORE_GROUP_ID } from '../src/ui/itCoreCorpus.js';
5
+ import {
6
+ GALAXY_DEFAULT_SCALE,
7
+ GALAXY_SPACING_TO_SCALE,
8
+ } from '../src/visualizer/galaxyLayout.js';
9
 
10
  describe('Instancer.renderGalaxyData', () => {
11
  it('mounts a single POINTS mesh and no ribbons', () => {
 
48
  expect(labels).toHaveLength(3);
49
  expect(labels[0].text).toBe('server');
50
  expect(labels[0].groupId).toBe(IT_CORE_GROUP_ID);
51
+ expect(labels[0].origin3D.x).toBeCloseTo(0.1 * GALAXY_DEFAULT_SCALE);
52
 
53
  expect(instancer.activeGroup.children).toHaveLength(1);
54
  expect(instancer.activeGroup.children[0].type).toBe('Points');
 
56
  expect(instancer.compareRuntime).toBeNull();
57
  });
58
 
59
+ it('scales from threadSpacing via resolveGalaxyWorldScale', () => {
60
+ const scene = new THREE.Scene();
61
+ const instancer = new Instancer(scene);
62
+ const compareResponse = {
63
+ items: [
64
+ { id: 'a', text: 'a', embedding: [0], cosine_vs_first: 1 },
65
+ ],
66
+ };
67
+ const positions = [[1, 0, 0]];
68
+ const spacing = 0.5;
69
+ const labels = instancer.renderGalaxyData(compareResponse, positions, {
70
+ threadSpacing: spacing,
71
+ threadThickness: 0.05,
72
+ });
73
+ expect(labels[0].origin3D.x).toBeCloseTo(spacing * GALAXY_SPACING_TO_SCALE);
74
+ });
75
+
76
  it('returns empty when positions missing', () => {
77
  const scene = new THREE.Scene();
78
  const instancer = new Instancer(scene);
tests/galaxyLayout.test.js CHANGED
@@ -5,7 +5,11 @@ import {
5
  boundingBoxFromLabels,
6
  centroidForGroup,
7
  galaxyCameraTarget,
 
 
 
8
  layoutGalaxyPoints,
 
9
  } from '../src/visualizer/galaxyLayout.js';
10
 
11
  describe('galaxyLayout', () => {
@@ -27,6 +31,23 @@ describe('galaxyLayout', () => {
27
  expect(pointsData[0].meta.token).toBe('server');
28
  });
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  it('centroidForGroup averages member origins', () => {
31
  const labels = [
32
  { groupId: IT_CORE_GROUP_ID, origin3D: new THREE.Vector3(0, 0, 0) },
 
5
  boundingBoxFromLabels,
6
  centroidForGroup,
7
  galaxyCameraTarget,
8
+ GALAXY_DEFAULT_SCALE,
9
+ GALAXY_MIN_SCALE,
10
+ GALAXY_SPACING_TO_SCALE,
11
  layoutGalaxyPoints,
12
+ resolveGalaxyWorldScale,
13
  } from '../src/visualizer/galaxyLayout.js';
14
 
15
  describe('galaxyLayout', () => {
 
31
  expect(pointsData[0].meta.token).toBe('server');
32
  });
33
 
34
+ it('default layout scale matches GALAXY_DEFAULT_SCALE', () => {
35
+ const { scale } = layoutGalaxyPoints(
36
+ [{ id: 'a', text: 'x', cosine_vs_first: 0 }],
37
+ [[1, 0, 0]],
38
+ );
39
+ expect(scale).toBe(GALAXY_DEFAULT_SCALE);
40
+ expect(GALAXY_DEFAULT_SCALE).toBeGreaterThan(48);
41
+ });
42
+
43
+ it('resolveGalaxyWorldScale maps spacing and floors tiny values', () => {
44
+ expect(resolveGalaxyWorldScale(null)).toBe(GALAXY_DEFAULT_SCALE);
45
+ expect(resolveGalaxyWorldScale({ threadSpacing: 0.4 })).toBeCloseTo(
46
+ 0.4 * GALAXY_SPACING_TO_SCALE,
47
+ );
48
+ expect(resolveGalaxyWorldScale({ threadSpacing: 0.01 })).toBe(GALAXY_MIN_SCALE);
49
+ });
50
+
51
  it('centroidForGroup averages member origins', () => {
52
  const labels = [
53
  { groupId: IT_CORE_GROUP_ID, origin3D: new THREE.Vector3(0, 0, 0) },