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); }); });