diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts index 8f9002c..499b92b 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts @@ -845,6 +845,28 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => { expect(note()).toMatch(/^Orbits propagated from published elements to now, \d{4}-\d\d-\d\d \d\d:\d\d UTC\.$/); }); + it('turns the Sun about its IAU pole, once in 25.38 days', async () => { + const time = TestBed.inject(TimeStore); + time.setRate(0); + time.setDate(new Date('2026-01-01T00:00Z')); + navigationStore.selectStar(SUN.id); + await flushAsync(); + await advanceFrames(engine, 2.5); + const sun = (): THREE.Object3D => (fixture.componentInstance as unknown as { starMarker: THREE.Object3D }).starMarker; + const turned = (local: THREE.Vector3): THREE.Vector3 => local.applyQuaternion(sun().getWorldQuaternion(new THREE.Quaternion())); + + // The sphere's +Y, which MAP_TO_BODY carries onto the body's pole, at RA 286.13, Dec 63.87. + const ra = (286.13 * Math.PI) / 180; + const dec = (63.87 * Math.PI) / 180; + const pole = new THREE.Vector3(Math.cos(dec) * Math.cos(ra), Math.cos(dec) * Math.sin(ra), Math.sin(dec)); + expect(turned(new THREE.Vector3(0, 1, 0)).angleTo(pole)).toBeLessThan(1e-6); + + const before = turned(new THREE.Vector3(1, 0, 0)); + time.setDate(new Date('2026-01-02T00:00Z')); + await advanceFrames(engine, 0.1); + expect((turned(new THREE.Vector3(1, 0, 0)).angleTo(before) * 180) / Math.PI).toBeCloseTo(14.1844, 3); + }); + it('names the date the system is drawn for once the clock is set to one', async () => { const note = (): string => (fixture.componentInstance as unknown as { hudNote: () => string }).hudNote(); TestBed.inject(TimeStore).setDate(new Date('2020-12-21T18:00Z')); diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.ts index 54a1b1e..636a7d7 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -21,7 +21,9 @@ import { } from '../../shared/astro/galaxy'; import { DataLoaderService } from '../../core/data/data-loader.service'; import { EngineService, SceneCamera } from '../../core/engine/engine.service'; -import { BodyRecord } from '../../shared/models/body.model'; +import { BodyRecord, RotationalElements } from '../../shared/models/body.model'; +import { SUN_ROTATIONAL_ELEMENTS } from '../../shared/astro/rotational-elements'; +import { bodyOrientation } from '../../shared/rendering/body-orientation'; import { DeepSkyRecord } from '../../shared/models/deepsky.model'; import { ExoplanetRecord } from '../../shared/models/exoplanet.model'; import { applyMilkyWaySkybox } from '../../shared/rendering/skybox'; @@ -548,6 +550,8 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { private currentStarId: number | null = null; private systemRenderer?: SystemOrbitsRenderer; private starMarker?: THREE.Mesh; + /** How the star marker is turned: the Sun's IAU elements for the Sun, nothing for any other star. */ + private starRotation?: RotationalElements; constructor( private readonly engine: EngineService, @@ -820,6 +824,9 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { if (this.systemGroup.visible) { this.systemRenderer?.update(this.time.julianDate()); + if (this.starMarker && this.starRotation) { + bodyOrientation(this.starRotation, this.time.julianDate(), this.starMarker.quaternion); + } this.keepMarkersLegible(camera); } this.updateSelectionMark(camera); @@ -2249,6 +2256,10 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { // stayed put as the camera closed in and ended up filling the screen with the flat gradient // that was meant to dress the star, over the photograph underneath it. this.starMarker = new THREE.Mesh(this.starMarkerGeometry, starMarkerMaterial); + // Its pole 115 degrees from the one the IAU gives, and still, until it was turned like a planet. + // The map's longitudes are Solar System Scope's, not Carrington's, so only the pole and the + // 25.38-day turn are the Sun's own. + this.starRotation = star.id === SOL_STAR_ID ? SUN_ROTATIONAL_ELEMENTS : undefined; this.systemGroup.add(this.starMarker); this.galaxyGroup.visible = false; diff --git a/src/app/shared/astro/rotational-elements.ts b/src/app/shared/astro/rotational-elements.ts index 54da1de..04db074 100644 --- a/src/app/shared/astro/rotational-elements.ts +++ b/src/app/shared/astro/rotational-elements.ts @@ -102,6 +102,13 @@ export function parsePckRotationalElements(text: string, naifId: number): { elem }; } +/** + * The Sun's, which is no `BodyRecord` but the system's star marker: NAIF body 10 in pck00011.tpc, + * the WGCCRE 2015 pole at RA 286.13, Dec 63.87 and W = 84.176 + 14.1844 d, a sidereal day of 25.38 + * days at the Carrington latitude. The ETL checks them against the kernel. + */ +export const SUN_ROTATIONAL_ELEMENTS: RotationalElements = { poleRaDeg: [286.13, 0, 0], poleDecDeg: [63.87, 0, 0], primeMeridianDeg: [84.176, 14.1844, 0] }; + function polynomial(coefficients: readonly number[], x: number): number { return (coefficients[0] ?? 0) + (coefficients[1] ?? 0) * x + (coefficients[2] ?? 0) * x * x; } diff --git a/tools/etl/fetchSolarSystem.ts b/tools/etl/fetchSolarSystem.ts index 6a635ee..095751d 100644 --- a/tools/etl/fetchSolarSystem.ts +++ b/tools/etl/fetchSolarSystem.ts @@ -5,7 +5,7 @@ import { SUN_STAR_ID } from '../../src/app/shared/models/star.model'; import { fetchHorizonsBody } from './lib/horizons'; import { MeanOrbit, parsePlanetMeanElements, parseSatelliteMeanElements, parseSmallBodyElements } from '../../src/app/shared/astro/mean-elements'; import { fetchPlanetMeanElementsText, fetchSatelliteMeanElementsHtml, fetchSmallBodyAnswer } from './lib/mean-elements'; -import { MIN_PERIODIC_TERM_DEG, orbitalTermsOfPrimeMeridian, parsePckRotationalElements } from '../../src/app/shared/astro/rotational-elements'; +import { MIN_PERIODIC_TERM_DEG, orbitalTermsOfPrimeMeridian, parsePckRotationalElements, SUN_ROTATIONAL_ELEMENTS } from '../../src/app/shared/astro/rotational-elements'; import { fetchPckText } from './lib/pck'; import { dataPath, ensureDataDir } from './lib/paths'; @@ -155,6 +155,10 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo const planetElements = await fetchPlanetMeanElementsText(); const satelliteElements = await fetchSatelliteMeanElementsHtml(); const pck = await fetchPckText(); + // The app turns the Sun by elements it carries itself; they must be the kernel's. + if (JSON.stringify(parsePckRotationalElements(pck, 10)?.elements) !== JSON.stringify(SUN_ROTATIONAL_ELEMENTS)) { + throw new Error(`The Sun's rotational elements in the app, ${JSON.stringify(SUN_ROTATIONAL_ELEMENTS)}, are not the kernel's.`); + } const gmById = new Map(); for (const spec of BODY_SPECS) {