Fix what the review of this branch found, starting with the pick rule it only claimed
The off-screen rule for clicks was described in 3f0abf8 and in the pull request, but only its
comment was committed: pickAt still let the slop reach past the frame. The mutant that was said
to catch it matched nothing, and an unrelated flaky test failed instead. The frame test is now in
pickAt, before the slop, and its test fails without it (star at NDC 1.01, click at 0.995).
Venus, Uranus and Pluto turned forwards: Horizons states a retrograde spin twice, by a negative
rate and by an obliquity over 90 degrees, and both were applied. The period's sign is now used
only when no obliquity is known. Measured on the live markers, spin axis against orbit normal is
cos(obliquity) for each: Venus -0.999, Uranus -0.135, Pluto -0.494, Earth 0.917.
Moons listed as rates rather than "Synchronous" drifted about 5 degrees an orbit and Titan did not
turn: every moon is now locked at its Kepler period. Pluto's obliquity comes from IAU WGCCRE 2015,
Horizons gives none.
Also:
- the star's light is white at pi, not a warm 2.2 that left the photographs dim;
- procedural textures are 128x64, not 512x256 that froze the main thread ~60 ms a body;
- Io, Pluto, Titan and Deimos lose their "maps", which were disc photographs with black sky;
- an exoplanet with only a mass gets a radius from it (M^0.55, capped at Jupiter), not Earth's;
- the clock knows when it has left the present even once back at real time, so the date and
"Back to now" stay up.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1733,7 +1733,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
// On the label cadence rather than per frame: at a month a second the date changes faster
|
// On the label cadence rather than per frame: at a month a second the date changes faster
|
||||||
// than anyone can read it, and the HUD does not need to re-render sixty times a second to
|
// than anyone can read it, and the HUD does not need to re-render sixty times a second to
|
||||||
// say so.
|
// say so.
|
||||||
this.hudDate.set(this.time.rate() === 1 ? '' : this.time.date().toISOString().slice(0, 10));
|
this.hudDate.set(this.time.atNow() ? '' : this.time.date().toISOString().slice(0, 10));
|
||||||
|
|
||||||
if (this.systemGroup.visible && star) {
|
if (this.systemGroup.visible && star) {
|
||||||
const planetCount =
|
const planetCount =
|
||||||
@@ -1760,7 +1760,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
? [{ label: 'Luminosity', value: formatLuminosity(luminosity), derived: true }]
|
? [{ label: 'Luminosity', value: formatLuminosity(luminosity), derived: true }]
|
||||||
: []),
|
: []),
|
||||||
]);
|
]);
|
||||||
this.hudNote.set('Orbits propagated from published elements to the current date.');
|
this.hudNote.set(this.time.atNow() ? 'Orbits propagated from published elements to the current date.' : 'Orbits propagated from published elements to the date on the clock.');
|
||||||
this.hudRange.set(
|
this.hudRange.set(
|
||||||
formatAu(
|
formatAu(
|
||||||
this.engine.visibleHalfHeight(
|
this.engine.visibleHalfHeight(
|
||||||
@@ -2227,7 +2227,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
? SUN_RADIUS_AU
|
? SUN_RADIUS_AU
|
||||||
: starMarkerRadiusAu(this.systemRenderer.minTopLevelSemiMajorAxisAu);
|
: starMarkerRadiusAu(this.systemRenderer.minTopLevelSemiMajorAxisAu);
|
||||||
this.starMarkerGeometry?.dispose();
|
this.starMarkerGeometry?.dispose();
|
||||||
this.starMarkerGeometry = new THREE.SphereGeometry(starRadiusAu, 24, 16);
|
this.starMarkerGeometry = new THREE.SphereGeometry(starRadiusAu, 64, 32);
|
||||||
|
|
||||||
const starMarkerMaterial = this.starMarkerMaterial.clone();
|
const starMarkerMaterial = this.starMarkerMaterial.clone();
|
||||||
const starColor = colorIndexToRgb(star.colorIndex, star.spectralType);
|
const starColor = colorIndexToRgb(star.colorIndex, star.spectralType);
|
||||||
|
|||||||
@@ -180,11 +180,13 @@ describe('StarFieldRenderer', () => {
|
|||||||
const centre = new THREE.Vector3(0, 0, -10).project(camera);
|
const centre = new THREE.Vector3(0, 0, -10).project(camera);
|
||||||
expect(renderer.pickAt(new THREE.Vector2(centre.x, centre.y), camera, camera.aspect)).toBe(9);
|
expect(renderer.pickAt(new THREE.Vector2(centre.x, centre.y), camera, camera.aspect)).toBe(9);
|
||||||
|
|
||||||
// The same star, now a hair outside the top of the frame.
|
// The same star, just outside the top of the frame: its centre at NDC 1.01, its disc ending at
|
||||||
const above = [star({ id: 9, x: 0, y: 10 * Math.tan((camera.fov * Math.PI) / 360) * 1.02, z: -10, magnitude: -2 })];
|
// 1.0033. A click at 0.995 is within its hit radius (0.0167) — so without the frame test this
|
||||||
|
// picks it — while none of the star is on screen.
|
||||||
|
const above = [star({ id: 9, x: 0, y: 10 * Math.tan((camera.fov * Math.PI) / 360) * 1.01, z: -10, magnitude: -2 })];
|
||||||
const outside = new StarFieldRenderer(above, packPositions(above));
|
const outside = new StarFieldRenderer(above, packPositions(above));
|
||||||
|
|
||||||
expect(outside.pickAt(new THREE.Vector2(0, 0.99), camera, camera.aspect)).toBeUndefined();
|
expect(outside.pickAt(new THREE.Vector2(0, 0.995), camera, camera.aspect)).toBeUndefined();
|
||||||
renderer.dispose();
|
renderer.dispose();
|
||||||
outside.dispose();
|
outside.dispose();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -434,10 +434,16 @@ export class StarFieldRenderer {
|
|||||||
if (projected.z < -1 || projected.z > 1) {
|
if (projected.z < -1 || projected.z > 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A sprite square in view space projects to an ellipse in NDC: the same half-extent in y,
|
// A sprite square in view space projects to an ellipse in NDC: the same half-extent in y,
|
||||||
// divided by the aspect ratio in x. Scaling dx by the aspect makes the comparison circular.
|
// divided by the aspect ratio in x. Scaling dx by the aspect makes the comparison circular.
|
||||||
const ndcRadius = (0.5 * sizes[index]) / tanHalfFov + PICK_NDC_SLOP;
|
const drawnRadius = (0.5 * sizes[index]) / tanHalfFov;
|
||||||
|
// Off screen if no part of the drawn disc is inside the frame. Tested before the slop is
|
||||||
|
// added: the slop is forgiveness for an imprecise click on a star you can see, not a reach
|
||||||
|
// past the edge to one you cannot.
|
||||||
|
if (Math.abs(projected.x) - drawnRadius / aspect > 1 || Math.abs(projected.y) - drawnRadius > 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ndcRadius = drawnRadius + PICK_NDC_SLOP;
|
||||||
const dx = (projected.x - pointerNdc.x) * aspect;
|
const dx = (projected.x - pointerNdc.x) * aspect;
|
||||||
const dy = projected.y - pointerNdc.y;
|
const dy = projected.y - pointerNdc.y;
|
||||||
const score = Math.hypot(dx, dy) / ndcRadius;
|
const score = Math.hypot(dx, dy) / ndcRadius;
|
||||||
|
|||||||
@@ -184,9 +184,9 @@ export function systemGridRingsAu(outermostOrbitAu: number): number[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A body is drawn at its true size. Astronomical Unit in kilometres, and what a body with no
|
* A body is drawn at its true size. Astronomical Unit in kilometres, and what a body with neither
|
||||||
* published radius is drawn as — Earth, which is the middle of the range for the exoplanets that
|
* a radius nor a mass to estimate one from is drawn as: an Earth, for want of anything better —
|
||||||
* reach here without one.
|
* exoplanets with a mass and no radius get an estimate from their mass before they reach here.
|
||||||
*/
|
*/
|
||||||
const KM_PER_AU = 149597870.7;
|
const KM_PER_AU = 149597870.7;
|
||||||
const DEFAULT_BODY_RADIUS_KM = 6371;
|
const DEFAULT_BODY_RADIUS_KM = 6371;
|
||||||
|
|||||||
@@ -409,12 +409,32 @@ describe('rotation', () => {
|
|||||||
expect(Math.abs(turnedDegrees(spinning(), 23.934 / 96))).toBeCloseTo(90, 1);
|
expect(Math.abs(turnedDegrees(spinning(), 23.934 / 96))).toBeCloseTo(90, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('turns a retrograde body the other way', () => {
|
/**
|
||||||
// Venus: its day runs backwards, which the catalogue carries as a negative period.
|
* Which way a body spins in the world: its angular velocity projected on its orbit's normal.
|
||||||
const forward = turnedDegrees(spinning(), 23.934 / 96);
|
* Positive is prograde, turning the same way it goes round; negative is retrograde.
|
||||||
const backward = turnedDegrees(spinning({ rotationPeriodHours: -23.934 }), 23.934 / 96);
|
*/
|
||||||
|
function spinSense(body: BodyRecord): number {
|
||||||
|
const renderer = new SystemOrbitsRenderer([body], [], undefined, 1);
|
||||||
|
renderer.update(DEFAULT_EPOCH_JD);
|
||||||
|
const start = renderer.members[0].marker.quaternion.clone();
|
||||||
|
renderer.update(DEFAULT_EPOCH_JD + 0.01);
|
||||||
|
const turn = renderer.members[0].marker.quaternion.clone().multiply(start.invert());
|
||||||
|
const axis = new THREE.Vector3(turn.x, turn.y, turn.z).multiplyScalar(Math.sign(turn.w));
|
||||||
|
return axis.normalize().dot(new THREE.Vector3(0, 0, 1).applyQuaternion(renderer.referenceFrame));
|
||||||
|
}
|
||||||
|
|
||||||
expect(Math.sign(backward)).toBe(-Math.sign(forward));
|
it('turns Venus backwards, as Horizons gives it: a negative rate and an obliquity past 90', () => {
|
||||||
|
// Both say retrograde, in two conventions. Applied together they cancelled into a forward
|
||||||
|
// turn, which is how Venus and Uranus used to be drawn.
|
||||||
|
const venus = spinning({ id: 'venus', rotationPeriodHours: -5832.54, obliquityDeg: 177.3 });
|
||||||
|
|
||||||
|
expect(spinSense(spinning())).toBeGreaterThan(0.9);
|
||||||
|
expect(spinSense(venus)).toBeLessThan(-0.9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads the sign of the period only where no obliquity says which way the pole points', () => {
|
||||||
|
expect(spinSense(spinning({ rotationPeriodHours: -23.934, obliquityDeg: undefined }))).toBeLessThan(-0.9);
|
||||||
|
expect(spinSense(spinning({ rotationPeriodHours: 23.934, obliquityDeg: undefined }))).toBeGreaterThan(0.9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leaves a body with no published rotation still', () => {
|
it('leaves a body with no published rotation still', () => {
|
||||||
@@ -427,3 +447,20 @@ describe('rotation', () => {
|
|||||||
expect(renderer.members[0].marker.quaternion.angleTo(start)).toBe(0);
|
expect(renderer.members[0].marker.quaternion.angleTo(start)).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('exoplanet size without a measured radius', () => {
|
||||||
|
const radiusOf = (overrides: Partial<ExoplanetRecord>): number => {
|
||||||
|
const renderer = new SystemOrbitsRenderer([], [exoplanet(overrides)], undefined, 1);
|
||||||
|
return ((renderer.members[0].marker as THREE.Mesh).geometry as THREE.SphereGeometry).parameters.radius;
|
||||||
|
};
|
||||||
|
const EARTH_AU = 6371 / 149597870.7;
|
||||||
|
|
||||||
|
it('draws a giant known only by its mass at about Jupiter’s size, not at an Earth', () => {
|
||||||
|
// 14 Her b: 2 829 Earth masses, no radius. It used to come out the size of the Earth.
|
||||||
|
expect(radiusOf({ radiusEarth: undefined, massEarth: 2829 }) / EARTH_AU).toBeCloseTo(11.2, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a measured radius over any estimate', () => {
|
||||||
|
expect(radiusOf({ radiusEarth: 1.88, massEarth: 2829 }) / EARTH_AU).toBeCloseTo(1.88, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -132,11 +132,12 @@ function buildOrbitLine(elements: OrbitalElements, kind: SystemMemberKind, frame
|
|||||||
* marker can now fill the frame, so it takes the real image at the size the detail page uses.
|
* marker can now fill the frame, so it takes the real image at the size the detail page uses.
|
||||||
*/
|
*/
|
||||||
function buildMarker(id: string | undefined, kind: SystemMemberKind, radiusKm: number | undefined, appearance: PlanetAppearance | undefined): THREE.Mesh {
|
function buildMarker(id: string | undefined, kind: SystemMemberKind, radiusKm: number | undefined, appearance: PlanetAppearance | undefined): THREE.Mesh {
|
||||||
// 32 by 24 rather than 16 by 12: at true scale a body is drawn as small as a pixel and as large
|
const geometry = new THREE.SphereGeometry(bodyMarkerRadiusAu(radiusKm), MARKER_WIDTH_SEGMENTS, MARKER_HEIGHT_SEGMENTS);
|
||||||
// as the screen, and the silhouette of the old sphere was visibly faceted at the near end.
|
|
||||||
const geometry = new THREE.SphereGeometry(bodyMarkerRadiusAu(radiusKm), 32, 24);
|
|
||||||
const photograph = id ? bodyTexturePath(id) : undefined;
|
const photograph = id ? bodyTexturePath(id) : undefined;
|
||||||
const map = photograph ? loadCachedTexture(photograph) : appearance ? planetTexture(appearance) : undefined;
|
// 128 by 64 for the derived texture, not the detail page's 512 by 256: that size costs about
|
||||||
|
// 60 ms a body on the main thread, 360 ms on entering a six-planet system, for a disc that is a
|
||||||
|
// few pixels across until the camera is on top of it.
|
||||||
|
const map = photograph ? loadCachedTexture(photograph) : appearance ? planetTexture(appearance, { width: 128, height: 64 }) : undefined;
|
||||||
const material = new THREE.MeshStandardMaterial({
|
const material = new THREE.MeshStandardMaterial({
|
||||||
map,
|
map,
|
||||||
color: map ? 0xffffff : colorForKind(kind),
|
color: map ? 0xffffff : colorForKind(kind),
|
||||||
@@ -150,18 +151,45 @@ function buildMarker(id: string | undefined, kind: SystemMemberKind, radiusKm: n
|
|||||||
* The star's own light, at the centre of the system it lights.
|
* The star's own light, at the centre of the system it lights.
|
||||||
*
|
*
|
||||||
* `decay` is 0, which is not what light does: a point source falls off with the square of the
|
* `decay` is 0, which is not what light does: a point source falls off with the square of the
|
||||||
* distance, and under that law Neptune receives a thousandth of what Mercury does and reads as
|
* distance, and under that law Neptune, at 30.2 AU, receives about a six-thousandth of what
|
||||||
* black. The map is a set of worlds to look at rather than a light meter, so each is lit as a
|
* Mercury does at 0.39 AU and reads as black. The map is a set of worlds to look at rather than a
|
||||||
* photograph of it would be — the same concession the pixel floor makes for size. What the light
|
* light meter, so each is lit as a photograph of it would be — the same concession the pixel
|
||||||
* does carry truthfully is *direction*: every body shows its day side toward the star and its
|
* floor makes for size. What the light does carry truthfully is which side is day: every body
|
||||||
* night side away from it, and the terminator falls where it really falls.
|
* shows its lit face toward the star, and the terminator falls where it really falls.
|
||||||
|
*
|
||||||
|
* White, at π: a Lambertian surface returns intensity / π of its texture where the light falls
|
||||||
|
* square on it, so π gives back the photograph itself at the point facing the star, and less
|
||||||
|
* towards the limb. A warm tint or a smaller figure darkened the photographs below what they are.
|
||||||
*/
|
*/
|
||||||
function starLight(): THREE.PointLight {
|
function starLight(): THREE.PointLight {
|
||||||
const light = new THREE.PointLight(0xfff4e0, 2.2, 0, 0);
|
const light = new THREE.PointLight(0xffffff, Math.PI, 0, 0);
|
||||||
light.position.set(0, 0, 0);
|
light.position.set(0, 0, 0);
|
||||||
return light;
|
return light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sphere segments. On a UV sphere the silhouette seen down the pole is the ring of width segments
|
||||||
|
* and the one seen from the side is the meridian profile, so height at half the width makes the
|
||||||
|
* error the same from every direction: at 64 by 32 a body filling the screen — Jupiter reaches
|
||||||
|
* 641 px of radius in the plan view — strays under a pixel from its true circle.
|
||||||
|
*/
|
||||||
|
const MARKER_WIDTH_SEGMENTS = 64;
|
||||||
|
const MARKER_HEIGHT_SEGMENTS = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A drawn radius, in Earth radii, for an exoplanet that has a mass and no measured radius — 1 076
|
||||||
|
* of the 1 692 drawn, most of them found by radial velocity, and most of those giants: their
|
||||||
|
* median is 315 Earth masses. Drawn at an Earth, as they were, a nine-Jupiter-mass planet came out
|
||||||
|
* smaller than its system's super-Earth.
|
||||||
|
*
|
||||||
|
* A rough power law, capped at Jupiter's radius: giants from a third of a Jupiter mass to ten are
|
||||||
|
* all about Jupiter's size, since past that point added mass compresses rather than inflates. It
|
||||||
|
* sets a size to draw, not a figure to print — the readout still says the radius is unknown.
|
||||||
|
*/
|
||||||
|
function radiusFromMassEarth(massEarth: number | null | undefined): number | undefined {
|
||||||
|
return massEarth && massEarth > 0 ? Math.min(JUPITER_RADIUS_EARTH, massEarth ** 0.55) : undefined;
|
||||||
|
}
|
||||||
|
const JUPITER_RADIUS_EARTH = 11.2;
|
||||||
|
|
||||||
/** Local axis a sphere is built around, and what the spin is applied about. */
|
/** Local axis a sphere is built around, and what the spin is applied about. */
|
||||||
const SPIN_AXIS = new THREE.Vector3(0, 1, 0);
|
const SPIN_AXIS = new THREE.Vector3(0, 1, 0);
|
||||||
@@ -170,23 +198,29 @@ const HOURS_PER_DAY = 24;
|
|||||||
/**
|
/**
|
||||||
* How a body is turned at a given date: its own sidereal rotation, about its own axis.
|
* How a body is turned at a given date: its own sidereal rotation, about its own axis.
|
||||||
*
|
*
|
||||||
* The axis is the orbit normal tilted by the obliquity, about the orbit’s ascending node —
|
* The obliquity fixes how far the pole leans from the orbit normal, and nothing more: which way
|
||||||
* which is where an obliquity is measured from, and the only line in the orbit the elements
|
* it leans needs the pole's right ascension, which the Horizons pages this reads do not carry. The
|
||||||
* name. The phase at the epoch is not published for any of these bodies, so a body is drawn
|
* lean is taken about the orbit's ascending node because that is the one line the elements name,
|
||||||
* with its prime meridian toward the node at J2000 and turned from there: the rate and the
|
* not because the data says so — so the tilt is real and its azimuth is not. Likewise the phase:
|
||||||
* direction are real, where the face pointed at the camera is not.
|
* each body starts at its elements' epoch (2025-01-01 here) in an arbitrary orientation, the
|
||||||
|
* shortest rotation of +Y onto its axis, and turns from there. The rate and the sense are real;
|
||||||
|
* the face towards the camera is not.
|
||||||
*
|
*
|
||||||
* A negative period is retrograde — Venus, and Uranus by the IAU’s convention — and comes
|
* Horizons states a retrograde spin twice over, in two conventions: an obliquity past 90 degrees
|
||||||
* out of the arithmetic without a special case.
|
* (Venus 177.3, Uranus 97.8) and a negative rate. Either one alone turns the body backwards, and
|
||||||
|
* both together cancel into a forward turn — which is how Venus and Uranus were drawn. Where an
|
||||||
|
* obliquity is given it carries the sense, and the period is taken as a magnitude; the sign of the
|
||||||
|
* period is only read for a body with no obliquity at all.
|
||||||
*/
|
*/
|
||||||
function spinFor(elements: OrbitalElements, frame: THREE.Quaternion, rotationPeriodHours: number, obliquityDeg: number, epochJd: number): THREE.Quaternion {
|
function spinFor(elements: OrbitalElements, frame: THREE.Quaternion, rotationPeriodHours: number, obliquityDeg: number | undefined, epochJd: number): THREE.Quaternion {
|
||||||
const node = elements.longitudeOfAscendingNodeDeg * DEG_TO_RAD;
|
const node = elements.longitudeOfAscendingNodeDeg * DEG_TO_RAD;
|
||||||
const inclination = elements.inclinationDeg * DEG_TO_RAD;
|
const inclination = elements.inclinationDeg * DEG_TO_RAD;
|
||||||
const nodeDirection = new THREE.Vector3(Math.cos(node), Math.sin(node), 0);
|
const nodeDirection = new THREE.Vector3(Math.cos(node), Math.sin(node), 0);
|
||||||
const axis = new THREE.Vector3(Math.sin(inclination) * Math.sin(node), -Math.sin(inclination) * Math.cos(node), Math.cos(inclination))
|
const axis = new THREE.Vector3(Math.sin(inclination) * Math.sin(node), -Math.sin(inclination) * Math.cos(node), Math.cos(inclination))
|
||||||
.applyAxisAngle(nodeDirection, obliquityDeg * DEG_TO_RAD)
|
.applyAxisAngle(nodeDirection, (obliquityDeg ?? 0) * DEG_TO_RAD)
|
||||||
.applyQuaternion(frame);
|
.applyQuaternion(frame);
|
||||||
const turns = ((epochJd - elements.epochJd) * HOURS_PER_DAY) / rotationPeriodHours;
|
const period = obliquityDeg === undefined ? rotationPeriodHours : Math.abs(rotationPeriodHours);
|
||||||
|
const turns = ((epochJd - elements.epochJd) * HOURS_PER_DAY) / period;
|
||||||
return new THREE.Quaternion()
|
return new THREE.Quaternion()
|
||||||
.setFromUnitVectors(SPIN_AXIS, axis)
|
.setFromUnitVectors(SPIN_AXIS, axis)
|
||||||
.multiply(new THREE.Quaternion().setFromAxisAngle(SPIN_AXIS, turns * 2 * Math.PI));
|
.multiply(new THREE.Quaternion().setFromAxisAngle(SPIN_AXIS, turns * 2 * Math.PI));
|
||||||
@@ -269,8 +303,6 @@ export class SystemOrbitsRenderer {
|
|||||||
const members: SystemMember[] = [];
|
const members: SystemMember[] = [];
|
||||||
const topLevelBodiesById = new Map<string, BodyRecord>();
|
const topLevelBodiesById = new Map<string, BodyRecord>();
|
||||||
|
|
||||||
// Measured before anything is built, because marker sizes are scaled against the span and
|
|
||||||
// the markers are created as the bodies are added.
|
|
||||||
const topLevelAxes = [
|
const topLevelAxes = [
|
||||||
...bodies.filter((body) => !body.parentBodyId).map((body) => body.orbit.semiMajorAxisAu),
|
...bodies.filter((body) => !body.parentBodyId).map((body) => body.orbit.semiMajorAxisAu),
|
||||||
...exoplanets.filter((exoplanet) => isPropagatableOrbit(exoplanet.orbit)).map((exoplanet) => exoplanet.orbit.semiMajorAxisAu!)
|
...exoplanets.filter((exoplanet) => isPropagatableOrbit(exoplanet.orbit)).map((exoplanet) => exoplanet.orbit.semiMajorAxisAu!)
|
||||||
@@ -319,7 +351,8 @@ export class SystemOrbitsRenderer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const elements = resolveOrbitalElements(exoplanet.orbit);
|
const elements = resolveOrbitalElements(exoplanet.orbit);
|
||||||
const radiusKm = exoplanet.radiusEarth ? exoplanet.radiusEarth * EARTH_RADIUS_KM : undefined;
|
const radiusEarth = exoplanet.radiusEarth ?? radiusFromMassEarth(exoplanet.massEarth);
|
||||||
|
const radiusKm = radiusEarth ? radiusEarth * EARTH_RADIUS_KM : undefined;
|
||||||
// Not `gmForParent(undefined)`: that assumes a solar-mass host for every system, and
|
// Not `gmForParent(undefined)`: that assumes a solar-mass host for every system, and
|
||||||
// most exoplanet hosts are red dwarfs a fraction of the Sun's mass.
|
// most exoplanet hosts are red dwarfs a fraction of the Sun's mass.
|
||||||
const gm = resolveGravitationalParameter({
|
const gm = resolveGravitationalParameter({
|
||||||
@@ -374,7 +407,7 @@ export class SystemOrbitsRenderer {
|
|||||||
body.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(body.frame);
|
body.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(body.frame);
|
||||||
body.marker.position.copy(body.position);
|
body.marker.position.copy(body.position);
|
||||||
if (body.rotationPeriodHours) {
|
if (body.rotationPeriodHours) {
|
||||||
body.marker.quaternion.copy(spinFor(body.elements, body.frame, body.rotationPeriodHours, body.obliquityDeg ?? 0, epochJd));
|
body.marker.quaternion.copy(spinFor(body.elements, body.frame, body.rotationPeriodHours, body.obliquityDeg, epochJd));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +420,7 @@ export class SystemOrbitsRenderer {
|
|||||||
const orbital = propagateOrbit(moon.elements, moon.gmAu3PerDay2, epochJd);
|
const orbital = propagateOrbit(moon.elements, moon.gmAu3PerDay2, epochJd);
|
||||||
moon.marker.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(moon.frame);
|
moon.marker.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(moon.frame);
|
||||||
if (moon.rotationPeriodHours) {
|
if (moon.rotationPeriodHours) {
|
||||||
moon.marker.quaternion.copy(spinFor(moon.elements, moon.frame, moon.rotationPeriodHours, moon.obliquityDeg ?? 0, epochJd));
|
moon.marker.quaternion.copy(spinFor(moon.elements, moon.frame, moon.rotationPeriodHours, moon.obliquityDeg, epochJd));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,8 +502,6 @@ export class SystemOrbitsRenderer {
|
|||||||
): TrackedMoon {
|
): TrackedMoon {
|
||||||
const pivot = new THREE.Group();
|
const pivot = new THREE.Group();
|
||||||
const orbitLine = buildOrbitLine(elements, 'moon', frame);
|
const orbitLine = buildOrbitLine(elements, 'moon', frame);
|
||||||
// A moon's own orbit is the thing it must not swallow: drawn at the system's exaggeration it
|
|
||||||
// is the same size as its planet, and every moon here orbits inside one.
|
|
||||||
const marker = buildMarker(id, 'moon', radiusKm, appearance);
|
const marker = buildMarker(id, 'moon', radiusKm, appearance);
|
||||||
pivot.add(orbitLine, marker);
|
pivot.add(orbitLine, marker);
|
||||||
this.object.add(pivot);
|
this.object.add(pivot);
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ function isWideViewport(): boolean {
|
|||||||
{{ rate.label }}
|
{{ rate.label }}
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
@if (time.rate() !== 1) {
|
@if (!time.atNow()) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="time.reset()"
|
(click)="time.reset()"
|
||||||
@@ -384,7 +384,7 @@ function isWideViewport(): boolean {
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<!-- Only while the clock is running faster than the world: at real time the date is
|
<!-- Only while the map is away from the present: at the present the date is
|
||||||
today's, which the reader's own machine already says. -->
|
today's, which the reader's own machine already says. -->
|
||||||
@if (date()) {
|
@if (date()) {
|
||||||
<p
|
<p
|
||||||
@@ -418,7 +418,7 @@ export class HudDockComponent implements OnInit {
|
|||||||
readonly note = input('');
|
readonly note = input('');
|
||||||
/** Camera range, pre-formatted by the scene, which is the only thing that knows the units. */
|
/** Camera range, pre-formatted by the scene, which is the only thing that knows the units. */
|
||||||
readonly range = input('');
|
readonly range = input('');
|
||||||
/** The date the sky is drawn for; empty while the clock runs at real time. */
|
/** The date the sky is drawn for; empty while the map is drawn for the present. */
|
||||||
readonly date = input('');
|
readonly date = input('');
|
||||||
/** Layer state; `null` means the surface has no layers to toggle and no Display tab. */
|
/** Layer state; `null` means the surface has no layers to toggle and no Display tab. */
|
||||||
readonly display = input<HudDisplay | null>(null);
|
readonly display = input<HudDisplay | null>(null);
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ export interface BodyRecord {
|
|||||||
*/
|
*/
|
||||||
parentBodyId?: string;
|
parentBodyId?: string;
|
||||||
/**
|
/**
|
||||||
* How the body turns on its own axis: the sidereal rotation period in hours, negative where it
|
* How the body turns on its own axis: the sidereal rotation period in hours, negative where
|
||||||
* turns retrograde (Venus, Triton), and the tilt of that axis from its orbital plane.
|
* Horizons gives a negative rate (Venus, Uranus), and the tilt of that axis from its orbital
|
||||||
|
* plane — which past 90 degrees already says the turn is retrograde.
|
||||||
*
|
*
|
||||||
* Absent where Horizons publishes neither — the view then leaves the body still rather than
|
* Absent where Horizons publishes neither — the view then leaves the body still rather than
|
||||||
* spinning it at an invented rate.
|
* spinning it at an invented rate.
|
||||||
|
|||||||
@@ -4,18 +4,21 @@ import * as THREE from 'three/webgpu';
|
|||||||
* Real NASA/ESA/USGS photography baked into `src/assets/textures/bodies/` at build time,
|
* Real NASA/ESA/USGS photography baked into `src/assets/textures/bodies/` at build time,
|
||||||
* keyed by the same ids used in `bodies.json`.
|
* keyed by the same ids used in `bodies.json`.
|
||||||
*
|
*
|
||||||
* This map is the whole of what has actually been photographed. Everything else — every
|
* Only surface *maps* belong here: equirectangular images, twice as wide as tall, that wrap a
|
||||||
* exoplanet, since not one has ever been imaged, and the moons no probe returned a usable map
|
* sphere. Everything else — every exoplanet, since not one has ever been imaged, and every moon
|
||||||
* of — falls through to `procedural-planet-texture.ts`, which derives a surface from the body's
|
* or dwarf planet with no such map in the repository — falls through to
|
||||||
* own measured size, mass, orbit and host star instead.
|
* `procedural-planet-texture.ts`, which derives a surface from the body's own measured size,
|
||||||
|
* mass, orbit and host star instead.
|
||||||
*
|
*
|
||||||
* Provenance (all public domain NASA/JPL or CC BY 4.0 Solar System Scope, via Wikimedia
|
* Io, Pluto, Titan and Deimos used to be listed with the square photographs of them in
|
||||||
* Commons — see each file's Commons page for the original credit line):
|
* `assets/textures/bodies/`: pictures of a lit disc against black sky, not maps. Wrapped round a
|
||||||
* mercury/venus/earth/mars/saturn/uranus/neptune/moon/sun/saturn-ring/skybox — Solar System
|
* sphere they put black sky on a fifth to a third of the surface, in a band up to 57 degrees wide
|
||||||
* Scope texture pack (CC BY 4.0); jupiter — Solar System Scope 8k pack (CC BY 4.0); pluto —
|
* across the equator that the spin then swept past the camera. They are left out until a real map
|
||||||
* NASA/JHUAPL/SwRI New Horizons true-color mosaic; deimos — NASA/JPL/University of Arizona
|
* of each is added.
|
||||||
* MRO HiRISE; io — NASA/JPL Galileo highest-resolution true-color mosaic; titan — NASA/JPL
|
*
|
||||||
* Cassini true-color view.
|
* Provenance (CC BY 4.0 Solar System Scope, via Wikimedia Commons — see each file's Commons page
|
||||||
|
* for the original credit line): mercury/venus/earth/mars/saturn/uranus/neptune/moon/sun/
|
||||||
|
* saturn-ring/skybox — Solar System Scope texture pack; jupiter — Solar System Scope 8k pack.
|
||||||
*/
|
*/
|
||||||
const BODY_TEXTURE_PATHS: Record<string, string> = {
|
const BODY_TEXTURE_PATHS: Record<string, string> = {
|
||||||
mercury: 'assets/textures/bodies/mercury.jpg',
|
mercury: 'assets/textures/bodies/mercury.jpg',
|
||||||
@@ -26,11 +29,7 @@ const BODY_TEXTURE_PATHS: Record<string, string> = {
|
|||||||
saturn: 'assets/textures/bodies/saturn.jpg',
|
saturn: 'assets/textures/bodies/saturn.jpg',
|
||||||
uranus: 'assets/textures/bodies/uranus.jpg',
|
uranus: 'assets/textures/bodies/uranus.jpg',
|
||||||
neptune: 'assets/textures/bodies/neptune.jpg',
|
neptune: 'assets/textures/bodies/neptune.jpg',
|
||||||
pluto: 'assets/textures/bodies/pluto.jpg',
|
moon: 'assets/textures/bodies/moon.jpg'
|
||||||
moon: 'assets/textures/bodies/moon.jpg',
|
|
||||||
deimos: 'assets/textures/bodies/deimos.jpg',
|
|
||||||
io: 'assets/textures/bodies/io.jpg',
|
|
||||||
titan: 'assets/textures/bodies/titan.jpg'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The Sun isn't a `BodyRecord` (it's the system's star marker), so it's looked up separately. */
|
/** The Sun isn't a `BodyRecord` (it's the system's star marker), so it's looked up separately. */
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ describe('TimeStore', () => {
|
|||||||
expect(time.julianDate() - afterTwoDays).toBeLessThan(1 / 24);
|
expect(time.julianDate() - afterTwoDays).toBeLessThan(1 / 24);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('knows the map is away from now even once it is back at real time', () => {
|
||||||
|
expect(time.atNow()).toBe(true);
|
||||||
|
time.setRate(2_629_800);
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
time.setRate(1);
|
||||||
|
|
||||||
|
// Real time, months ahead: the rate says nothing about where the clock stands.
|
||||||
|
expect(time.atNow()).toBe(false);
|
||||||
|
time.reset();
|
||||||
|
expect(time.atNow()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('comes back to now, at real time', () => {
|
it('comes back to now, at real time', () => {
|
||||||
time.setRate(2_629_800);
|
time.setRate(2_629_800);
|
||||||
vi.advanceTimersByTime(5000); // months away
|
vi.advanceTimersByTime(5000); // months away
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ const JULIAN_DATE_AT_EPOCH = 2440587.5;
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TimeStore {
|
export class TimeStore {
|
||||||
readonly rate = signal<number>(TIME_RATES[0].secondsPerSecond);
|
readonly rate = signal<number>(TIME_RATES[0].secondsPerSecond);
|
||||||
|
/**
|
||||||
|
* Whether the map is drawn for the present. Not the same as a rate of one: after an excursion at
|
||||||
|
* a month a second, real time carries on from months ahead, and the map is still away from now.
|
||||||
|
*/
|
||||||
|
readonly atNow = signal(true);
|
||||||
|
|
||||||
private anchorJd = dateToJulianDate();
|
private anchorJd = dateToJulianDate();
|
||||||
private anchorWallMs = Date.now();
|
private anchorWallMs = Date.now();
|
||||||
@@ -62,6 +67,9 @@ export class TimeStore {
|
|||||||
this.anchorJd = this.julianDate();
|
this.anchorJd = this.julianDate();
|
||||||
this.anchorWallMs = Date.now();
|
this.anchorWallMs = Date.now();
|
||||||
this.rate.set(secondsPerSecond);
|
this.rate.set(secondsPerSecond);
|
||||||
|
if (secondsPerSecond !== 1) {
|
||||||
|
this.atNow.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Back to now, at real time — the state the map opens in. */
|
/** Back to now, at real time — the state the map opens in. */
|
||||||
@@ -69,5 +77,6 @@ export class TimeStore {
|
|||||||
this.anchorJd = dateToJulianDate();
|
this.anchorJd = dateToJulianDate();
|
||||||
this.anchorWallMs = Date.now();
|
this.anchorWallMs = Date.now();
|
||||||
this.rate.set(TIME_RATES[0].secondsPerSecond);
|
this.rate.set(TIME_RATES[0].secondsPerSecond);
|
||||||
|
this.atNow.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,8 @@
|
|||||||
"meanAnomalyAtEpochDeg": 51.93655343727463,
|
"meanAnomalyAtEpochDeg": 51.93655343727463,
|
||||||
"epochJd": 2460676.5
|
"epochJd": 2460676.5
|
||||||
},
|
},
|
||||||
"rotationPeriodHours": 153.29335198
|
"rotationPeriodHours": 153.29335198,
|
||||||
|
"obliquityDeg": 119.6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "moon",
|
"id": "moon",
|
||||||
@@ -176,7 +177,7 @@
|
|||||||
"epochJd": 2460676.5
|
"epochJd": 2460676.5
|
||||||
},
|
},
|
||||||
"parentBodyId": "earth",
|
"parentBodyId": "earth",
|
||||||
"rotationPeriodHours": 655.7197475276438,
|
"rotationPeriodHours": 664.8546956215548,
|
||||||
"obliquityDeg": 6.67
|
"obliquityDeg": 6.67
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -302,7 +303,8 @@
|
|||||||
"meanAnomalyAtEpochDeg": 32.18862839469676,
|
"meanAnomalyAtEpochDeg": 32.18862839469676,
|
||||||
"epochJd": 2460676.5
|
"epochJd": 2460676.5
|
||||||
},
|
},
|
||||||
"parentBodyId": "saturn"
|
"parentBodyId": "saturn",
|
||||||
|
"rotationPeriodHours": 382.87527117206236
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "triton",
|
"id": "triton",
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ interface BodySpec {
|
|||||||
horizonsCommand: string;
|
horizonsCommand: string;
|
||||||
center: string;
|
center: string;
|
||||||
parentBodyId?: string;
|
parentBodyId?: string;
|
||||||
|
/**
|
||||||
|
* Obliquity to orbit, in degrees, where the Horizons page states none. Pluto's is from the IAU
|
||||||
|
* WGCCRE 2015 pole (RA 132.99, Dec -6.16), 119.6 degrees: past 90, so it turns retrograde.
|
||||||
|
*/
|
||||||
|
obliquityDeg?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sun-centered planets/dwarf, then their major moons (planetocentric elements).
|
// Sun-centered planets/dwarf, then their major moons (planetocentric elements).
|
||||||
@@ -28,7 +33,7 @@ const BODY_SPECS: BodySpec[] = [
|
|||||||
{ id: 'saturn', name: 'Saturn', kind: 'planet', horizonsCommand: '699', center: '500@10' },
|
{ id: 'saturn', name: 'Saturn', kind: 'planet', horizonsCommand: '699', center: '500@10' },
|
||||||
{ id: 'uranus', name: 'Uranus', kind: 'planet', horizonsCommand: '799', center: '500@10' },
|
{ id: 'uranus', name: 'Uranus', kind: 'planet', horizonsCommand: '799', center: '500@10' },
|
||||||
{ id: 'neptune', name: 'Neptune', kind: 'planet', horizonsCommand: '899', center: '500@10' },
|
{ id: 'neptune', name: 'Neptune', kind: 'planet', horizonsCommand: '899', center: '500@10' },
|
||||||
{ id: 'pluto', name: 'Pluto', kind: 'dwarf', horizonsCommand: '999', center: '500@10' },
|
{ id: 'pluto', name: 'Pluto', kind: 'dwarf', horizonsCommand: '999', center: '500@10', obliquityDeg: 119.6 },
|
||||||
{ id: 'moon', name: 'Moon', kind: 'moon', horizonsCommand: '301', center: '500@399', parentBodyId: 'earth' },
|
{ id: 'moon', name: 'Moon', kind: 'moon', horizonsCommand: '301', center: '500@399', parentBodyId: 'earth' },
|
||||||
{ id: 'phobos', name: 'Phobos', kind: 'moon', horizonsCommand: '401', center: '500@499', parentBodyId: 'mars' },
|
{ id: 'phobos', name: 'Phobos', kind: 'moon', horizonsCommand: '401', center: '500@499', parentBodyId: 'mars' },
|
||||||
{ id: 'deimos', name: 'Deimos', kind: 'moon', horizonsCommand: '402', center: '500@499', parentBodyId: 'mars' },
|
{ id: 'deimos', name: 'Deimos', kind: 'moon', horizonsCommand: '402', center: '500@499', parentBodyId: 'mars' },
|
||||||
@@ -60,9 +65,12 @@ export async function fetchSolarSystem(): Promise<BodyRecord[]> {
|
|||||||
console.warn(` no physical radius found for ${spec.name}; defaulting to 0.`);
|
console.warn(` no physical radius found for ${spec.name}; defaulting to 0.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A tidally locked moon's day is its orbit, which Horizons states as a word rather than a
|
// Every moon listed here is tidally locked, so its day is its orbit — as drawn, from these
|
||||||
// number: Kepler gives the period from the elements just parsed and the parent it goes round.
|
// elements and the parent's mass by Kepler. Not every page says so: the Moon's gives a rate,
|
||||||
const rotationPeriodHours = result.tidallyLocked
|
// the true sidereal month, 1.4% off the orbit these elements trace, so its face drifted five
|
||||||
|
// degrees an orbit; Titan's gives nothing, so it did not turn. Taking the orbit keeps one face
|
||||||
|
// towards the parent, which is what synchronous means.
|
||||||
|
const rotationPeriodHours = result.tidallyLocked || spec.kind === 'moon'
|
||||||
? orbitalPeriodDays(result.orbit.semiMajorAxisAu, gmForParent(spec.parentBodyId)) * HOURS_PER_DAY
|
? orbitalPeriodDays(result.orbit.semiMajorAxisAu, gmForParent(spec.parentBodyId)) * HOURS_PER_DAY
|
||||||
: result.rotationPeriodHours;
|
: result.rotationPeriodHours;
|
||||||
if (rotationPeriodHours === undefined) {
|
if (rotationPeriodHours === undefined) {
|
||||||
@@ -78,7 +86,7 @@ export async function fetchSolarSystem(): Promise<BodyRecord[]> {
|
|||||||
orbit: result.orbit,
|
orbit: result.orbit,
|
||||||
...(spec.parentBodyId ? { parentBodyId: spec.parentBodyId } : {}),
|
...(spec.parentBodyId ? { parentBodyId: spec.parentBodyId } : {}),
|
||||||
...(rotationPeriodHours !== undefined ? { rotationPeriodHours } : {}),
|
...(rotationPeriodHours !== undefined ? { rotationPeriodHours } : {}),
|
||||||
...(result.obliquityDeg !== undefined ? { obliquityDeg: result.obliquityDeg } : {})
|
...((result.obliquityDeg ?? spec.obliquityDeg) !== undefined ? { obliquityDeg: result.obliquityDeg ?? spec.obliquityDeg } : {})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ export interface HorizonsResult {
|
|||||||
radiusKm?: number;
|
radiusKm?: number;
|
||||||
orbit: OrbitalElements;
|
orbit: OrbitalElements;
|
||||||
/**
|
/**
|
||||||
* Sidereal rotation period in hours, negative where the body turns retrograde — Venus, whose
|
* Sidereal rotation period in hours, negative where the page gives a negative rate —
|
||||||
* day runs backwards, and Triton. Absent where the page publishes none.
|
* Venus and Uranus. Absent where the page publishes none. The same pages also give an obliquity
|
||||||
|
* past 90 degrees for those two, which says the same thing again; see the renderer's spinFor.
|
||||||
*/
|
*/
|
||||||
rotationPeriodHours?: number;
|
rotationPeriodHours?: number;
|
||||||
/** Tilt of the rotation axis from the body's own orbital plane, in degrees. */
|
/** Tilt of the rotation axis from the body's own orbital plane, in degrees. */
|
||||||
@@ -43,10 +44,10 @@ const RADIUS_PATTERNS = [
|
|||||||
* How each page states how fast the body turns, in the order they are tried.
|
* How each page states how fast the body turns, in the order they are tried.
|
||||||
*
|
*
|
||||||
* The rate in radians per second is preferred wherever it appears: it is unambiguous and it is
|
* The rate in radians per second is preferred wherever it appears: it is unambiguous and it is
|
||||||
* signed, which is how Venus and Triton are known to turn backwards. A period in hours or days
|
* signed: Venus and Uranus carry a negative one. A period in hours or days
|
||||||
* comes next, then the sexagesimal form the giant planets use, and finally the word every major
|
* comes next, then the sexagesimal form the giant planets use, and finally the word most moons carry
|
||||||
* moon here carries instead of a number — tidally locked, so its day is its year, which the
|
* instead of a number, Synchronous. Not all do — the Moon's page gives a rate, Titan's nothing —
|
||||||
* caller supplies from the orbit it has already parsed.
|
* so the caller treats every moon it lists as locked whatever its page says.
|
||||||
*/
|
*/
|
||||||
const ROTATION_RATE_PATTERN = /Rot(?:ational)?\.?\s*Rate\s*[(,]\s*rad\/s\s*\)?\s*=\s*(-?[\d.]+)/i;
|
const ROTATION_RATE_PATTERN = /Rot(?:ational)?\.?\s*Rate\s*[(,]\s*rad\/s\s*\)?\s*=\s*(-?[\d.]+)/i;
|
||||||
const ROTATION_PERIOD_PATTERNS = [
|
const ROTATION_PERIOD_PATTERNS = [
|
||||||
@@ -63,7 +64,7 @@ const SECONDS_PER_HOUR = 3600;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* True where the page gives no number because the body keeps one face to its parent, so its day
|
* True where the page gives no number because the body keeps one face to its parent, so its day
|
||||||
* is its orbit — every major moon here. The period itself is then Kepler's, which the caller
|
* is its orbit. The period itself is then Kepler's, which the caller
|
||||||
* works out from the elements above and the parent's mass.
|
* works out from the elements above and the parent's mass.
|
||||||
*/
|
*/
|
||||||
export function isTidallyLocked(text: string): boolean {
|
export function isTidallyLocked(text: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user