Put orbits and stars in the same reference frame
The app's two sources disagree about which frame they are in, and nothing reconciled them. HYG star positions are equatorial J2000 — that is what raDecDistanceToXyz produces and what the galaxy view renders directly. Orbital elements come from JPL Horizons, whose default reference plane for element output is the ecliptic, and the ETL never overrides it. The two are tilted 23.4 degrees apart, so the orbits sat that far off the sky they are drawn against. Confirmed rather than assumed, from both ends: the Horizons request in lib/horizons.ts sets no REF_PLANE, and the resulting solar-system inclinations are 0 to 17 degrees with Earth exactly 0.00 — which is only true of the ecliptic, since Earth's orbit defines it. eclipticToEquatorial now rotates orbit positions into the scene frame, so a direction means the same thing in the galaxy view and the system view. The rotation is about the vernal-equinox axis, which both frames share. That exposed a presentation problem the old code had been hiding. The renderer mapped the propagator's z straight onto the scene's vertical, which silently redefined the frame but did make systems render flat. In a properly equatorial scene, orbital planes lie 23.4 degrees off the scene's own axes, so a system would be presented edge-on. Rather than rotate the world back into a comfortable pose — which would only put the orbits at odds with the sky again — the camera now settles relative to the orbital plane: a three-quarter view about 37 degrees off the ecliptic normal. The arrival still begins along the approach direction and swings round as it settles, so the transition stays continuous, and the framing is now the same every time rather than inherited from wherever the camera happened to be. Tests: 247 passing, up from 237. The frame tests are the discriminating kind — Earth's orbit must lie perpendicular to the ecliptic pole rather than to the scene's vertical, and must reach 23.4 degrees of declination a quarter orbit on, where it used to read zero. Verified in a browser against Sol and Gl 357. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
@@ -52,6 +52,11 @@ same place an in-scene click would.
|
|||||||
star field is instanced quads on a `SpriteNodeMaterial` instead, which behaves the same on
|
star field is instanced quads on a `SpriteNodeMaterial` instead, which behaves the same on
|
||||||
both backends. Their size is angular rather than world-space — real stars are unresolvable
|
both backends. Their size is angular rather than world-space — real stars are unresolvable
|
||||||
point sources, so apparent size should follow brightness, not distance.
|
point sources, so apparent size should follow brightness, not distance.
|
||||||
|
- **One reference frame.** The two data sources disagree: HYG gives star positions in
|
||||||
|
equatorial J2000, while JPL Horizons reports orbital elements against the ecliptic, tilted
|
||||||
|
23.4° away. The scene is equatorial throughout and orbits are rotated into it, so a direction
|
||||||
|
means the same thing in both views. Systems are still presented face-on — by placing the
|
||||||
|
camera relative to the orbital plane rather than by rotating the world into a convenient pose.
|
||||||
- **Two coordinate scales.** The galaxy view works in parsecs and the system view in AU —
|
- **Two coordinate scales.** The galaxy view works in parsecs and the system view in AU —
|
||||||
about eight orders of magnitude apart, which wrecks float precision if rendered in one unit
|
about eight orders of magnitude apart, which wrecks float precision if rendered in one unit
|
||||||
space. The camera rig recentres the active star to the origin ("floating origin") and swaps
|
space. The camera rig recentres the active star to the origin ("floating origin") and swaps
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { StarRecord } from '../../shared/models/star.model';
|
|||||||
import { NavigationStore } from '../../shared/state/navigation.store';
|
import { NavigationStore } from '../../shared/state/navigation.store';
|
||||||
import { CameraRigController } from './camera-rig-controller';
|
import { CameraRigController } from './camera-rig-controller';
|
||||||
import { DeepSkyRenderer } from './deep-sky-renderer';
|
import { DeepSkyRenderer } from './deep-sky-renderer';
|
||||||
import { starMarkerRadiusAu, systemFramingDistanceAu } from './system-framing';
|
import { starMarkerRadiusAu, SYSTEM_VIEW_DIRECTION, systemFramingDistanceAu } from './system-framing';
|
||||||
import { colorIndexToRgb, StarFieldRenderer } from './star-field-renderer';
|
import { colorIndexToRgb, StarFieldRenderer } from './star-field-renderer';
|
||||||
import { LabeledPoint, StarLabelOverlay } from './star-label-overlay';
|
import { LabeledPoint, StarLabelOverlay } from './star-label-overlay';
|
||||||
import { SystemOrbitsRenderer } from './system-orbits-renderer';
|
import { SystemOrbitsRenderer } from './system-orbits-renderer';
|
||||||
@@ -431,8 +431,12 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
this.rig!.setImmediate({ position: direction.clone().multiplyScalar(SYSTEM_ENTRY_DISTANCE_AU), target: new THREE.Vector3(0, 0, 0) });
|
this.rig!.setImmediate({ position: direction.clone().multiplyScalar(SYSTEM_ENTRY_DISTANCE_AU), target: new THREE.Vector3(0, 0, 0) });
|
||||||
|
|
||||||
const framingDistance = systemFramingDistanceAu(this.systemRenderer.maxTopLevelSemiMajorAxisAu);
|
const framingDistance = systemFramingDistanceAu(this.systemRenderer.maxTopLevelSemiMajorAxisAu);
|
||||||
|
// Arrives along whichever direction the approach came from, then swings round to look down
|
||||||
|
// on the orbital plane as it settles — so the swap stays continuous but the system is not
|
||||||
|
// presented edge-on. See SYSTEM_VIEW_DIRECTION.
|
||||||
|
const viewDirection = new THREE.Vector3(SYSTEM_VIEW_DIRECTION.x, SYSTEM_VIEW_DIRECTION.y, SYSTEM_VIEW_DIRECTION.z).normalize();
|
||||||
|
|
||||||
this.rig!.flyTo({ position: direction.clone().multiplyScalar(framingDistance), target: new THREE.Vector3(0, 0, 0) }, SETTLE_DURATION_SECONDS, () => {
|
this.rig!.flyTo({ position: viewDirection.multiplyScalar(framingDistance), target: new THREE.Vector3(0, 0, 0) }, SETTLE_DURATION_SECONDS, () => {
|
||||||
this.currentStarId = star.id;
|
this.currentStarId = star.id;
|
||||||
this.navigationStore.setViewLevel('system');
|
this.navigationStore.setViewLevel('system');
|
||||||
onComplete();
|
onComplete();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { CartesianCoordinates, eclipticToEquatorial } from '../../shared/astro/coordinates';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How the system view sizes itself to whatever system it is showing.
|
* How the system view sizes itself to whatever system it is showing.
|
||||||
*
|
*
|
||||||
@@ -42,6 +44,19 @@ function clamp(value: number, min: number, max: number): number {
|
|||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the camera settles when arriving at a system, as a unit direction from the star in the
|
||||||
|
* scene's equatorial frame.
|
||||||
|
*
|
||||||
|
* The scene is equatorial so that orbits and stars share one frame, but orbital planes lie
|
||||||
|
* close to the *ecliptic*, which is tilted 23.4 degrees out of it. Left to the equatorial axes,
|
||||||
|
* a system would be presented edge-on. Rather than rotate the world into a comfortable pose —
|
||||||
|
* which would put the orbits back at odds with the sky — the camera is placed relative to the
|
||||||
|
* plane instead: this is a three-quarter view, about 37 degrees off the ecliptic normal, so a
|
||||||
|
* system reads as a disc while staying where it truly sits.
|
||||||
|
*/
|
||||||
|
export const SYSTEM_VIEW_DIRECTION: CartesianCoordinates = eclipticToEquatorial({ x: 0, y: 0.6, z: 0.8 });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Radius (AU) to draw the system's star at, given its innermost orbit.
|
* Radius (AU) to draw the system's star at, given its innermost orbit.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as THREE from 'three/webgpu';
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { DEFAULT_EPOCH_JD } from '../../shared/astro/constants';
|
import { DEFAULT_EPOCH_JD } from '../../shared/astro/constants';
|
||||||
|
import { eclipticToEquatorial, OBLIQUITY_J2000_DEG } from '../../shared/astro/coordinates';
|
||||||
|
import { BodyRecord } from '../../shared/models/body.model';
|
||||||
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
||||||
import { SystemOrbitsRenderer } from './system-orbits-renderer';
|
import { SystemOrbitsRenderer } from './system-orbits-renderer';
|
||||||
|
|
||||||
@@ -145,4 +147,67 @@ describe('SystemOrbitsRenderer exoplanet propagation', () => {
|
|||||||
}
|
}
|
||||||
renderer.dispose();
|
renderer.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('reference frame', () => {
|
||||||
|
/** Earth: inclination 0 by definition — its orbit *is* the ecliptic plane. */
|
||||||
|
const EARTH: BodyRecord = {
|
||||||
|
id: 'earth',
|
||||||
|
systemStarId: 0,
|
||||||
|
name: 'Earth',
|
||||||
|
kind: 'planet',
|
||||||
|
radiusKm: 6371,
|
||||||
|
orbit: {
|
||||||
|
semiMajorAxisAu: 1,
|
||||||
|
eccentricity: 0.0167,
|
||||||
|
inclinationDeg: 0,
|
||||||
|
longitudeOfAscendingNodeDeg: 0,
|
||||||
|
argumentOfPeriapsisDeg: 0,
|
||||||
|
meanAnomalyAtEpochDeg: 0,
|
||||||
|
epochJd: DEFAULT_EPOCH_JD
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('places an ecliptic orbit in the ecliptic plane of the equatorial scene', () => {
|
||||||
|
// Horizons reports elements against the ecliptic; the scene is equatorial, to match the
|
||||||
|
// star catalogue. So Earth's orbit must come out tilted, lying perpendicular to the
|
||||||
|
// *ecliptic* pole rather than to the scene's own vertical.
|
||||||
|
const renderer = new SystemOrbitsRenderer([EARTH], []);
|
||||||
|
const eclipticPole = eclipticToEquatorial({ x: 0, y: 0, z: 1 });
|
||||||
|
|
||||||
|
for (const offset of [0, 40, 91, 200, 300]) {
|
||||||
|
renderer.update(DEFAULT_EPOCH_JD + offset);
|
||||||
|
const p = renderer.members[0].marker.position;
|
||||||
|
const outOfPlane = p.x * eclipticPole.x + p.y * eclipticPole.y + p.z * eclipticPole.z;
|
||||||
|
expect(Math.abs(outOfPlane)).toBeLessThan(1e-9);
|
||||||
|
}
|
||||||
|
renderer.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tilts that orbit away from the celestial equator by the obliquity', () => {
|
||||||
|
// The discriminating check: before the frames were reconciled, the orbit sat flat in the
|
||||||
|
// scene and this angle was zero.
|
||||||
|
const renderer = new SystemOrbitsRenderer([EARTH], []);
|
||||||
|
renderer.update(DEFAULT_EPOCH_JD + 91); // a quarter orbit on, well away from the equinox
|
||||||
|
|
||||||
|
const p = renderer.members[0].marker.position;
|
||||||
|
const latitudeDeg = (Math.asin(p.z / p.length()) * 180) / Math.PI;
|
||||||
|
|
||||||
|
expect(Math.abs(latitudeDeg)).toBeGreaterThan(1);
|
||||||
|
expect(Math.abs(latitudeDeg)).toBeLessThanOrEqual(OBLIQUITY_J2000_DEG + 1e-6);
|
||||||
|
renderer.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the vernal equinox direction shared between the two frames', () => {
|
||||||
|
// A body at ecliptic longitude 0 sits on the +X axis in both frames, so it must not move.
|
||||||
|
const atEquinox: BodyRecord = { ...EARTH, orbit: { ...EARTH.orbit, eccentricity: 0 } };
|
||||||
|
const renderer = new SystemOrbitsRenderer([atEquinox], []);
|
||||||
|
renderer.update(DEFAULT_EPOCH_JD);
|
||||||
|
|
||||||
|
const p = renderer.members[0].marker.position;
|
||||||
|
expect(p.x).toBeCloseTo(1, 6);
|
||||||
|
expect(p.y).toBeCloseTo(0, 9);
|
||||||
|
expect(p.z).toBeCloseTo(0, 9);
|
||||||
|
renderer.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as THREE from 'three/webgpu';
|
|||||||
|
|
||||||
import { gmForParent } from '../../shared/astro/constants';
|
import { gmForParent } from '../../shared/astro/constants';
|
||||||
import { isPropagatableOrbit, orbitEllipsePoints, propagateOrbit, resolveGravitationalParameter, resolveOrbitalElements } from '../../shared/astro/kepler';
|
import { isPropagatableOrbit, orbitEllipsePoints, propagateOrbit, resolveGravitationalParameter, resolveOrbitalElements } from '../../shared/astro/kepler';
|
||||||
|
import { eclipticToEquatorial } from '../../shared/astro/coordinates';
|
||||||
import { BodyRecord, OrbitalElements } from '../../shared/models/body.model';
|
import { BodyRecord, OrbitalElements } from '../../shared/models/body.model';
|
||||||
import { bodyMarkerRadiusAu } from './system-framing';
|
import { bodyMarkerRadiusAu } from './system-framing';
|
||||||
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
||||||
@@ -46,9 +47,12 @@ function buildOrbitLine(elements: OrbitalElements, kind: SystemMemberKind): THRE
|
|||||||
const points = orbitEllipsePoints(elements);
|
const points = orbitEllipsePoints(elements);
|
||||||
const positions = new Float32Array(points.length * 3);
|
const positions = new Float32Array(points.length * 3);
|
||||||
points.forEach((point, index) => {
|
points.forEach((point, index) => {
|
||||||
positions[index * 3] = point.x;
|
// Elements are ecliptic (Horizons' default reference plane); the scene is equatorial, to
|
||||||
positions[index * 3 + 1] = point.z; // AU "up" (ecliptic normal) maps to scene Y.
|
// match the star catalogue. Without this the orbits sit 23.4 degrees off the sky.
|
||||||
positions[index * 3 + 2] = point.y;
|
const { x, y, z } = eclipticToEquatorial(point);
|
||||||
|
positions[index * 3] = x;
|
||||||
|
positions[index * 3 + 1] = y;
|
||||||
|
positions[index * 3 + 2] = z;
|
||||||
});
|
});
|
||||||
|
|
||||||
const geometry = new THREE.BufferGeometry();
|
const geometry = new THREE.BufferGeometry();
|
||||||
@@ -175,8 +179,8 @@ export class SystemOrbitsRenderer {
|
|||||||
/** Recomputes every marker's position for the given Julian date. Call once per tick. */
|
/** Recomputes every marker's position for the given Julian date. Call once per tick. */
|
||||||
update(epochJd: number): void {
|
update(epochJd: number): void {
|
||||||
for (const body of this.topLevelBodies) {
|
for (const body of this.topLevelBodies) {
|
||||||
const { x, y, z } = propagateOrbit(body.elements, body.gmAu3PerDay2, epochJd);
|
const { x, y, z } = eclipticToEquatorial(propagateOrbit(body.elements, body.gmAu3PerDay2, epochJd));
|
||||||
body.position.set(x, z, y); // AU "up" maps to scene Y, matching buildOrbitLine.
|
body.position.set(x, y, z); // Equatorial, matching buildOrbitLine and the star field.
|
||||||
body.marker.position.copy(body.position);
|
body.marker.position.copy(body.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,8 +190,8 @@ export class SystemOrbitsRenderer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
moon.pivot.position.copy(parent.position);
|
moon.pivot.position.copy(parent.position);
|
||||||
const { x, y, z } = propagateOrbit(moon.elements, moon.gmAu3PerDay2, epochJd);
|
const { x, y, z } = eclipticToEquatorial(propagateOrbit(moon.elements, moon.gmAu3PerDay2, epochJd));
|
||||||
moon.marker.position.set(x, z, y);
|
moon.marker.position.set(x, y, z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { distanceBetween, parallaxMasToParsecs, parseSexagesimal, raDecDistanceToXyz, raDecToUnitVector, raDegDecDistanceToXyz } from './coordinates';
|
import {
|
||||||
|
distanceBetween,
|
||||||
|
eclipticToEquatorial,
|
||||||
|
equatorialToEcliptic,
|
||||||
|
OBLIQUITY_J2000_DEG,
|
||||||
|
parallaxMasToParsecs,
|
||||||
|
parseSexagesimal,
|
||||||
|
raDecDistanceToXyz,
|
||||||
|
raDecToUnitVector,
|
||||||
|
raDegDecDistanceToXyz
|
||||||
|
} from './coordinates';
|
||||||
|
|
||||||
// Reference values taken directly from the HYG v4.1 database (RA/Dec/dist and its own
|
// Reference values taken directly from the HYG v4.1 database (RA/Dec/dist and its own
|
||||||
// precomputed x/y/z, which uses the same equatorial-Cartesian convention we implement).
|
// precomputed x/y/z, which uses the same equatorial-Cartesian convention we implement).
|
||||||
@@ -136,3 +146,61 @@ describe('parseSexagesimal', () => {
|
|||||||
expect(parseSexagesimal('12::56')).toBeNull();
|
expect(parseSexagesimal('12::56')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('eclipticToEquatorial', () => {
|
||||||
|
const RAD = Math.PI / 180;
|
||||||
|
|
||||||
|
it('leaves the vernal equinox untouched, since both frames share that axis', () => {
|
||||||
|
// +X is where the ecliptic crosses the celestial equator, so it is the rotation axis.
|
||||||
|
expect(eclipticToEquatorial({ x: 1, y: 0, z: 0 })).toEqual({ x: 1, y: 0, z: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts the ecliptic pole the obliquity away from the celestial pole', () => {
|
||||||
|
const pole = eclipticToEquatorial({ x: 0, y: 0, z: 1 });
|
||||||
|
const angleFromCelestialPoleDeg = Math.acos(pole.z) / RAD;
|
||||||
|
|
||||||
|
expect(angleFromCelestialPoleDeg).toBeCloseTo(OBLIQUITY_J2000_DEG, 9);
|
||||||
|
expect(pole.x).toBeCloseTo(0, 12);
|
||||||
|
expect(pole.y).toBeCloseTo(-Math.sin(OBLIQUITY_J2000_DEG * RAD), 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('places the summer solstice point at the obliquity in declination', () => {
|
||||||
|
// Ecliptic longitude 90 degrees is the northernmost point of the Sun's yearly path, whose
|
||||||
|
// declination is by definition the obliquity — about 23.4 degrees.
|
||||||
|
const solstice = eclipticToEquatorial({ x: 0, y: 1, z: 0 });
|
||||||
|
const declinationDeg = Math.asin(solstice.z) / RAD;
|
||||||
|
|
||||||
|
expect(declinationDeg).toBeCloseTo(OBLIQUITY_J2000_DEG, 9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves length, being a rotation', () => {
|
||||||
|
const rotated = eclipticToEquatorial({ x: 0.3, y: -0.5, z: 0.81 });
|
||||||
|
expect(Math.hypot(rotated.x, rotated.y, rotated.z)).toBeCloseTo(Math.hypot(0.3, -0.5, 0.81), 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves a point in the ecliptic plane in that plane, tilted out of the equator', () => {
|
||||||
|
const inPlane = eclipticToEquatorial({ x: 0.6, y: 0.8, z: 0 });
|
||||||
|
expect(inPlane.z).toBeCloseTo(0.8 * Math.sin(OBLIQUITY_J2000_DEG * RAD), 12);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('equatorialToEcliptic', () => {
|
||||||
|
it('is the exact inverse of eclipticToEquatorial', () => {
|
||||||
|
for (const point of [
|
||||||
|
{ x: 1, y: 0, z: 0 },
|
||||||
|
{ x: 0, y: 1, z: 0 },
|
||||||
|
{ x: 0, y: 0, z: 1 },
|
||||||
|
{ x: -0.37, y: 0.42, z: 0.83 }
|
||||||
|
]) {
|
||||||
|
const round = equatorialToEcliptic(eclipticToEquatorial(point));
|
||||||
|
expect(round.x).toBeCloseTo(point.x, 12);
|
||||||
|
expect(round.y).toBeCloseTo(point.y, 12);
|
||||||
|
expect(round.z).toBeCloseTo(point.z, 12);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('brings the celestial pole back to the obliquity off the ecliptic pole', () => {
|
||||||
|
const pole = equatorialToEcliptic({ x: 0, y: 0, z: 1 });
|
||||||
|
expect(Math.acos(pole.z) / (Math.PI / 180)).toBeCloseTo(OBLIQUITY_J2000_DEG, 9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -33,6 +33,49 @@ export function raDegDecDistanceToXyz(raDeg: number, decDeg: number, distancePc:
|
|||||||
return raDecDistanceToXyz(raDeg / HOURS_TO_DEG, decDeg, distancePc);
|
return raDecDistanceToXyz(raDeg / HOURS_TO_DEG, decDeg, distancePc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obliquity of the ecliptic at J2000.0, in degrees — the tilt of Earth's orbital plane against
|
||||||
|
* its equator, and so the angle between this app's two source frames.
|
||||||
|
*/
|
||||||
|
export const OBLIQUITY_J2000_DEG = 23.4392911;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates a vector from the **ecliptic** frame into the **equatorial** frame, both J2000.
|
||||||
|
*
|
||||||
|
* The app has to span both because its two sources disagree. Star positions come from HYG as
|
||||||
|
* equatorial coordinates, which `raDecDistanceToXyz` produces and which the galaxy view renders
|
||||||
|
* directly. Orbital elements come from JPL Horizons, whose default reference plane for element
|
||||||
|
* output is the ecliptic — the ETL never overrides it. The two are tilted
|
||||||
|
* {@link OBLIQUITY_J2000_DEG} apart about the shared vernal-equinox axis, so orbits have to be
|
||||||
|
* rotated before they can share a scene with the stars.
|
||||||
|
*/
|
||||||
|
export function eclipticToEquatorial(position: CartesianCoordinates): CartesianCoordinates {
|
||||||
|
const obliquity = OBLIQUITY_J2000_DEG * DEG_TO_RAD;
|
||||||
|
const cos = Math.cos(obliquity);
|
||||||
|
const sin = Math.sin(obliquity);
|
||||||
|
|
||||||
|
// A rotation about +X, which both frames share: it points at the vernal equinox, where the
|
||||||
|
// ecliptic and the celestial equator cross.
|
||||||
|
return {
|
||||||
|
x: position.x,
|
||||||
|
y: position.y * cos - position.z * sin,
|
||||||
|
z: position.y * sin + position.z * cos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inverse of {@link eclipticToEquatorial}. */
|
||||||
|
export function equatorialToEcliptic(position: CartesianCoordinates): CartesianCoordinates {
|
||||||
|
const obliquity = OBLIQUITY_J2000_DEG * DEG_TO_RAD;
|
||||||
|
const cos = Math.cos(obliquity);
|
||||||
|
const sin = Math.sin(obliquity);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: position.x,
|
||||||
|
y: position.y * cos + position.z * sin,
|
||||||
|
z: -position.y * sin + position.z * cos
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direction to a point on the celestial sphere as a unit vector, in the same equatorial frame
|
* Direction to a point on the celestial sphere as a unit vector, in the same equatorial frame
|
||||||
* as {@link raDecDistanceToXyz}. Used for sources whose distance is unknown or irrelevant —
|
* as {@link raDecDistanceToXyz}. Used for sources whose distance is unknown or irrelevant —
|
||||||
|
|||||||
Reference in New Issue
Block a user