9e76ec9328
CI / Lint, typecheck, test, build (pull_request) Failing after 9s
- apps/live-map/src/hooks/useLiveState.ts: WebSocket subscription with exponential-backoff reconnect, polling fallback, status tracking - apps/live-map/src/scene/Scene.tsx: refactored Three.js scene with per-frame orbit propagation, vessel marker color-coding by owner, orbit-line visibility tied to focus filters, smooth camera follow on selected vessel/body - apps/live-map/src/scene/layout.ts: bodyPositionAt / vesselPositionAt helpers (heliocentric frame, walk up the parent chain), logScale helpers for the system view - apps/live-map/src/scene/color.ts: per-body and per-owner color maps - apps/live-map/src/panels/TimeControls.tsx: play/pause/reverse/reset buttons, ×1/×10/×100/×1k/×10k/×100k speeds, UT scrub slider, live-edge indicator (LIVE / Nh behind / Nh ahead) - apps/live-map/src/panels/VesselList.tsx: vessel sidebar with click- to-track; color-coded by owner (KASA=blue, SPES=orange) - apps/live-map/src/panels/FocusPanel.tsx: planet/moon/vessel orbit visibility toggles - apps/live-map/src/panels/StatusPill.tsx: WS status (LIVE/POLLING/ OFFLINE/STALE), body + vessel + message counts - tests/scene.test.ts: 10 tests for layout helpers (periodicity, vessel-centered positioning, logScale round-trips) End-to-end verified: mock publisher → API → live-map WebSocket → scene re-renders with the new vessel positions and orbits.
167 lines
4.5 KiB
TypeScript
167 lines
4.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
bodyPositionAt,
|
|
vesselPositionAt,
|
|
findBodyMu,
|
|
logScale,
|
|
inverseLogScale,
|
|
} from '../src/scene/layout.js';
|
|
import type { CelestialBody, Vessel } from '@kerbal-rt/shared-types';
|
|
|
|
const kerbol: CelestialBody = {
|
|
id: 'kerbol',
|
|
name: 'Kerbol',
|
|
kind: 'star',
|
|
parentId: null,
|
|
radius: 261_600_000,
|
|
sphereOfInfluence: 1e30,
|
|
gravitationalParameter: 1.172e18,
|
|
rotationPeriod: 432_000,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 0,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
};
|
|
|
|
const kerbin: CelestialBody = {
|
|
id: 'kerbin',
|
|
name: 'Kerbin',
|
|
kind: 'planet',
|
|
parentId: 'kerbol',
|
|
radius: 600_000,
|
|
sphereOfInfluence: 84_159_286,
|
|
gravitationalParameter: 3.5316e12,
|
|
rotationPeriod: 21_600,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 13_599_840_256,
|
|
eccentricity: 0, // circular for predictable test math
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
};
|
|
|
|
const mun: CelestialBody = {
|
|
id: 'mun',
|
|
name: 'Mun',
|
|
kind: 'moon',
|
|
parentId: 'kerbin',
|
|
radius: 200_000,
|
|
sphereOfInfluence: 2_429_559,
|
|
gravitationalParameter: 6.514e10,
|
|
rotationPeriod: 138_984,
|
|
axialTilt: 0,
|
|
orbit: {
|
|
semiMajorAxis: 12_000_000,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
};
|
|
|
|
const bodies: CelestialBody[] = [kerbol, kerbin, mun];
|
|
|
|
describe('bodyPositionAt', () => {
|
|
it('returns origin for the star', () => {
|
|
const p = bodyPositionAt(bodies, 'kerbol', 100);
|
|
expect(p.x).toBe(0);
|
|
expect(p.y).toBe(0);
|
|
expect(p.z).toBe(0);
|
|
});
|
|
|
|
it('returns a non-zero position for a planet at t > 0', () => {
|
|
const p = bodyPositionAt(bodies, 'kerbin', 1_000_000);
|
|
const dist = Math.hypot(p.x, p.y, p.z);
|
|
// SMA is 13.6 billion meters, so position is on that order
|
|
expect(dist).toBeGreaterThan(1e10);
|
|
expect(dist).toBeLessThan(2e10);
|
|
});
|
|
|
|
it('returns origin for a body not in the catalog', () => {
|
|
const p = bodyPositionAt(bodies, 'unknown', 1);
|
|
expect(p).toEqual({ x: 0, y: 0, z: 0 });
|
|
});
|
|
|
|
it('is periodic (returns near the same position after one full orbit)', () => {
|
|
// Kerbin orbits Kerbol, so the period is computed with kerbol's μ
|
|
const p1 = bodyPositionAt(bodies, 'kerbin', 0);
|
|
const period =
|
|
(2 * Math.PI) / Math.sqrt(kerbol.gravitationalParameter / Math.pow(13_599_840_256, 3));
|
|
const p2 = bodyPositionAt(bodies, 'kerbin', period);
|
|
expect(p1.x).toBeCloseTo(p2.x, -2);
|
|
expect(p1.y).toBeCloseTo(p2.y, -2);
|
|
});
|
|
});
|
|
|
|
describe('vesselPositionAt', () => {
|
|
const vessel: Vessel = {
|
|
id: 'v1',
|
|
name: 'Test',
|
|
type: 'Probe',
|
|
owner: 'KASA',
|
|
situation: 'ORBITING',
|
|
status: 'ACTIVE',
|
|
orbit: {
|
|
semiMajorAxis: 7_000_000,
|
|
eccentricity: 0,
|
|
inclination: 0,
|
|
longitudeOfAscendingNode: 0,
|
|
argumentOfPeriapsis: 0,
|
|
meanAnomalyAtEpoch: 0,
|
|
epoch: 0,
|
|
},
|
|
referenceBodyId: 'kerbin',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
retiredAt: null,
|
|
};
|
|
|
|
it('returns the kerbin-centered position when reference body is kerbin', () => {
|
|
// At t=0, vessel is at (7e6, 0, 0) in kerbin frame
|
|
const p = vesselPositionAt(bodies, vessel, 0);
|
|
// kerbin is at (sma, 0, 0) = (13.6e9, 0, 0)
|
|
// so vessel should be at roughly (13.6e9 + 7e6, 0, 0)
|
|
expect(p.x).toBeCloseTo(13_599_840_256 + 7_000_000, -2);
|
|
expect(p.y).toBeCloseTo(0, 2);
|
|
});
|
|
});
|
|
|
|
describe('findBodyMu', () => {
|
|
it("returns the body's own gravitational parameter", () => {
|
|
expect(findBodyMu(bodies, 'kerbin')).toBe(kerbin.gravitationalParameter);
|
|
});
|
|
it('returns 0 for null id', () => {
|
|
expect(findBodyMu(bodies, null)).toBe(0);
|
|
});
|
|
it('returns 0 for unknown id', () => {
|
|
expect(findBodyMu(bodies, 'nope')).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('logScale', () => {
|
|
it('is the inverse of inverseLogScale', () => {
|
|
for (const t of [0, 4, 8, 12]) {
|
|
const d = logScale(t);
|
|
expect(inverseLogScale(d)).toBeCloseTo(t, 6);
|
|
}
|
|
});
|
|
it('produces reasonable distances for camera placement', () => {
|
|
// t=4 → ~5.5e9 m (good for showing Kerbin at 13.6e9)
|
|
expect(logScale(4)).toBeGreaterThan(1e9);
|
|
expect(logScale(4)).toBeLessThan(1e10);
|
|
// t=10 → ~22e12 m (good for showing all planets)
|
|
expect(logScale(10)).toBeGreaterThan(1e12);
|
|
});
|
|
});
|