The clock reaches AD 1, but TT - UT was held at today's 69.184 s. At AD 1000 it was 1 574 s and at AD 1 about 10 570 (Espenak and Meeus, NASA's Five Millennium Canon; Horizons' TDB - UT gives 1 658 and 10 466 on JD 2086455 and 1721600). So every spin but Earth's was (ΔT - 69 s) times its rate out, Jupiter 15.2 degrees at AD 1000 and 106 at AD 1, Mars 6 and 43, and every orbit that much behind: the Moon about 0.2 and 1.4 degrees. ttMinusUtSeconds gives TT - UT for a date on the clock: the Espenak-Meeus polynomials before 1972, 32.184 s plus UTC's leap seconds from 1972 to the last one, at the start of 2017, and 69.184 s held after it, as Horizons holds it. Its pieces join within 0.1 s. tdbFromUtc, which positions and spins already share, now adds it. Within 0.2 s of Horizons in 1950, 105 s at AD 1 and 86 s at AD 1000, where the historical record itself is that uncertain. Earth is the exception: its turning is what UT counts, so the clock's date already says how far it has turned, and ΔT would turn it again, 44 degrees at AD 1. Its W, fitted to today, keeps today's 69.184 s (bodyOrientation's followsUt, set for Earth in the system view and on its page). In the running app at 1000-01-01 00:00 UT, Jupiter's drawn prime meridian sits 0.000 degrees from its IAU W at TT and 15.164 from where the held offset put it; Earth's sits on its W at UT + 69.184 s, 6.288 degrees short of what TT would have turned it to. The renderer spec now hands its frozen Horizons vectors over as the UT dates that name them through the same TT - UT, and checks Jupiter's and Earth's prime meridians at AD 1000. Controls: the leap-second rule used before 1972 fails "follows the historical record before 1972"; TT - UT held at 69 s fails "turns Jupiter at AD 1000 by its W"; Earth turned at TDB, or the renderer or the page not keeping it on UT, fails the Earth tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
91 lines
6.3 KiB
TypeScript
91 lines
6.3 KiB
TypeScript
import * as THREE from 'three/webgpu';
|
||
import { describe, expect, it } from 'vitest';
|
||
|
||
import { tdbFromUtc } from '../astro/constants';
|
||
import { eclipticToEquatorial } from '../astro/coordinates';
|
||
import { meanElementsAt, positionAtEpoch } from '../astro/kepler';
|
||
import { BodyRecord } from '../models/body.model';
|
||
import { bodyOrientation, bodyPageView } from './body-orientation';
|
||
|
||
// Earth (the Earth-Moon barycentre's mean elements) and the Moon as bodies.json carries them.
|
||
const EARTH: BodyRecord = {
|
||
id: 'earth', systemStarId: 0, name: 'Earth', kind: 'planet', radiusKm: 6371, orbitSource: 'test',
|
||
orbit: {semiMajorAxisAu: 1.00000018, eccentricity: 0.01673163, inclinationDeg: -0.00054346, longitudeOfAscendingNodeDeg: -5.11260389, argumentOfPeriapsisDeg: 108.04266274, meanAnomalyAtEpochDeg: -2.4631431299999917, epochJd: 2451545},
|
||
rates: {meanMotionDegPerDay: 0.9856091187759068, longitudeOfAscendingNodeDegPerDay: -0.000006604751813826146, argumentOfPeriapsisDegPerDay: 0.000015309819575633124, semiMajorAxisAuPerDay: -8.213552361396303e-13, eccentricityPerDay: -1.002327173169062e-9, inclinationDegPerDay: -3.6609938398357287e-7},
|
||
rotationalElements: {poleRaDeg: [0, -0.641, 0], poleDecDeg: [90, -0.557, 0], primeMeridianDeg: [190.147, 360.9856235, 0]}
|
||
};
|
||
const MOON: BodyRecord = {
|
||
id: 'moon', systemStarId: 0, name: 'Moon', kind: 'moon', radiusKm: 1737.4, orbitSource: 'test', parentBodyId: 'earth',
|
||
orbit: {semiMajorAxisAu: 0.0025695552897999907, eccentricity: 0.0554, inclinationDeg: 5.16, longitudeOfAscendingNodeDeg: 125.08, argumentOfPeriapsisDeg: 318.15, meanAnomalyAtEpochDeg: 135.27, epochJd: 2451545},
|
||
rates: {meanMotionDegPerDay: 13.176358, longitudeOfAscendingNodeDegPerDay: -0.052990660396105185, argumentOfPeriapsisDegPerDay: 0.164353223839846},
|
||
rotationalElements: {poleRaDeg: [269.9949, 0.0031, 0], poleDecDeg: [66.5392, 0.013, 0], primeMeridianDeg: [38.3213, 13.17635815, -1.4e-12], terms: [{angleDeg: [125.045, -1935.5364525], ra: -3.8787, dec: 1.5419, pm: 3.561}, {angleDeg: [250.089, -3871.072905], ra: -0.1204, dec: 0.0239, pm: 0.1208}, {angleDeg: [260.008, 475263.3328725], ra: 0.07, dec: -0.0278, pm: -0.0642}, {angleDeg: [176.625, 487269.629985], ra: -0.0172, dec: 0.0068, pm: 0.0158}, {angleDeg: [357.529, 35999.0509575], ra: 0, dec: 0, pm: 0.0252}]}
|
||
};
|
||
const BODIES = [EARTH, MOON];
|
||
const JUNE_1_2025_NOON_UTC = 2460828.0;
|
||
/** The page's light, at (4, 3, 5): 38.7 degrees round from the camera's side. */
|
||
const SUN_AZIMUTH = Math.atan2(4, 5);
|
||
|
||
/** The point of the page's sphere, as east longitude and latitude on its map, that faces the Sun. */
|
||
function subSolarPoint(body: BodyRecord, jdUtc: number): { eastDeg: number; latDeg: number } {
|
||
const sphere = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 32));
|
||
const sun = new THREE.Vector3();
|
||
expect(bodyPageView(body, BODIES, jdUtc, SUN_AZIMUTH, sphere.quaternion, sun)).toBe(true);
|
||
sphere.updateMatrixWorld();
|
||
const hit = new THREE.Raycaster(sun.clone().multiplyScalar(4), sun.clone().negate()).intersectObject(sphere)[0];
|
||
return { eastDeg: (hit.uv!.x - 0.5) * 360, latDeg: (hit.uv!.y - 0.5) * 180 };
|
||
}
|
||
|
||
describe('bodyPageView', () => {
|
||
it('lights the same face of Earth on its page: within 4 degrees of Greenwich at noon UTC, and where Horizons has it', () => {
|
||
expect(Math.abs(subSolarPoint(EARTH, JUNE_1_2025_NOON_UTC).eastDeg)).toBeLessThan(4);
|
||
// Horizons' sub-solar point from the Sun, 1.5795 E and 22.2604 N, is Earth as it was 8.43
|
||
// minutes before, when the light arriving then left the Sun; its latitude is geodetic, on the
|
||
// flattened Earth, where the sphere's is geocentric: 0.14 degrees apart at this latitude.
|
||
const horizons = subSolarPoint(EARTH, JUNE_1_2025_NOON_UTC - 8.43351424 / 1440);
|
||
const geodetic = (Math.atan(Math.tan((horizons.latDeg * Math.PI) / 180) / (1 - 1 / 298.257) ** 2) * 180) / Math.PI;
|
||
expect(Math.abs(horizons.eastDeg - 1.579501)).toBeLessThan(0.1);
|
||
expect(Math.abs(geodetic - 22.260426)).toBeLessThan(0.05);
|
||
});
|
||
|
||
it('takes a moon’s Sun from where it and its planet are: the Moon’s sub-solar point is Horizons’', () => {
|
||
const moon = subSolarPoint(MOON, JUNE_1_2025_NOON_UTC);
|
||
// 116.2859 E and 1.5030 N, seen from Earth's centre.
|
||
expect(Math.abs(moon.eastDeg - 116.285934)).toBeLessThan(0.1);
|
||
expect(Math.abs(moon.latDeg - 1.503004)).toBeLessThan(0.05);
|
||
});
|
||
|
||
it('takes the Sun where it stands at the same TDB instant the body is turned for, and Earth turned as the system view turns it', () => {
|
||
// Earth's own sphere, turned as the system view turns it (by UT, see `bodyOrientation`), and
|
||
// the Sun seen from Earth's mean place at the clock's date taken to TDB: the page must light that
|
||
// same point of its map, today and at AD 1000, when TT was 1 574 s past UT.
|
||
for (const jdUt of [JUNE_1_2025_NOON_UTC, 2086307.5]) {
|
||
const planet = new THREE.Quaternion();
|
||
const sun = new THREE.Vector3();
|
||
bodyPageView(EARTH, BODIES, jdUt, SUN_AZIMUTH, planet, sun);
|
||
const place = eclipticToEquatorial(positionAtEpoch(meanElementsAt(EARTH.orbit, EARTH.rates, tdbFromUtc(jdUt))));
|
||
const expected = new THREE.Vector3(-place.x, -place.y, -place.z).normalize().applyQuaternion(bodyOrientation(EARTH.rotationalElements!, jdUt, undefined, true).invert());
|
||
expect(sun.clone().applyQuaternion(planet.clone().invert()).angleTo(expected)).toBeLessThan(1e-9);
|
||
}
|
||
});
|
||
|
||
it('keeps the pole up and the Sun where the page’s light stands, turning the body under it', () => {
|
||
const planet = new THREE.Quaternion();
|
||
const sun = new THREE.Vector3();
|
||
for (const hours of [0, 6, 12]) {
|
||
bodyPageView(EARTH, BODIES, JUNE_1_2025_NOON_UTC + hours / 24, SUN_AZIMUTH, planet, sun);
|
||
expect(new THREE.Vector3(0, 1, 0).applyQuaternion(planet).angleTo(new THREE.Vector3(0, 1, 0))).toBeLessThan(1e-9);
|
||
expect(Math.atan2(sun.x, sun.z)).toBeCloseTo(SUN_AZIMUTH, 9);
|
||
}
|
||
});
|
||
|
||
it('leaves a body with no elements to the page, as it was', () => {
|
||
const planet = new THREE.Quaternion(0.1, 0.2, 0.3, 0.9).normalize();
|
||
const before = planet.clone();
|
||
const sun = new THREE.Vector3(4, 3, 5);
|
||
|
||
expect(bodyPageView({ ...EARTH, rotationalElements: undefined }, BODIES, JUNE_1_2025_NOON_UTC, SUN_AZIMUTH, planet, sun)).toBe(false);
|
||
expect(planet.equals(before)).toBe(true);
|
||
expect(sun.toArray()).toEqual([4, 3, 5]);
|
||
});
|
||
});
|