Turn the Sun about its IAU pole, once in 25.38 days, as every planet already is

Every body with IAU elements was turned by its pole and W, but the Sun, which is the system's star
marker and no BodyRecord, was built with an identity rotation and never touched: its pole pointed
at RA 90, Dec 0, 115.03 degrees from the WGCCRE 2015 solar pole (RA 286.13, Dec 63.87), and it
stood still where its W turns 14.1844 degrees a day.

SUN_ROTATIONAL_ELEMENTS carries NAIF body 10 from pck00011.tpc, and the ETL fails if they are not
the kernel's. The scene turns the star marker by bodyOrientation each tick when the star is the
Sun, as the renderer turns the planets. In the running app the Sun's drawn pole lies on the IAU's
(0.00 degrees) and its map turns 14.1844 degrees between 2026-01-01 and 01-02. The map's longitudes
are Solar System Scope's, not Carrington's, so the phase of W is not the Sun's own; the pole and
the rate are.

Controls: leaving the marker unturned fails "turns the Sun about its IAU pole, once in 25.38 days";
the app's elements off the kernel's (W rate 14.18) fails the ETL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:21:15 +02:00
co-authored by Claude Opus 5.5
parent 8e3a494fe9
commit 97b8dd9dc8
4 changed files with 46 additions and 2 deletions
@@ -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'));
@@ -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;
@@ -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;
}