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
|
||||
// than anyone can read it, and the HUD does not need to re-render sixty times a second to
|
||||
// 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) {
|
||||
const planetCount =
|
||||
@@ -1760,7 +1760,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
? [{ 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(
|
||||
formatAu(
|
||||
this.engine.visibleHalfHeight(
|
||||
@@ -2227,7 +2227,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
? SUN_RADIUS_AU
|
||||
: starMarkerRadiusAu(this.systemRenderer.minTopLevelSemiMajorAxisAu);
|
||||
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 starColor = colorIndexToRgb(star.colorIndex, star.spectralType);
|
||||
|
||||
@@ -180,11 +180,13 @@ describe('StarFieldRenderer', () => {
|
||||
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);
|
||||
|
||||
// The same star, now a hair outside the top of the frame.
|
||||
const above = [star({ id: 9, x: 0, y: 10 * Math.tan((camera.fov * Math.PI) / 360) * 1.02, z: -10, magnitude: -2 })];
|
||||
// The same star, just outside the top of the frame: its centre at NDC 1.01, its disc ending at
|
||||
// 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));
|
||||
|
||||
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();
|
||||
outside.dispose();
|
||||
});
|
||||
|
||||
@@ -434,10 +434,16 @@ export class StarFieldRenderer {
|
||||
if (projected.z < -1 || projected.z > 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 dy = projected.y - pointerNdc.y;
|
||||
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
|
||||
* published radius is drawn as — Earth, which is the middle of the range for the exoplanets that
|
||||
* reach here without one.
|
||||
* A body is drawn at its true size. Astronomical Unit in kilometres, and what a body with neither
|
||||
* a radius nor a mass to estimate one from is drawn as: an Earth, for want of anything better —
|
||||
* exoplanets with a mass and no radius get an estimate from their mass before they reach here.
|
||||
*/
|
||||
const KM_PER_AU = 149597870.7;
|
||||
const DEFAULT_BODY_RADIUS_KM = 6371;
|
||||
|
||||
@@ -409,12 +409,32 @@ describe('rotation', () => {
|
||||
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.
|
||||
const forward = turnedDegrees(spinning(), 23.934 / 96);
|
||||
const backward = turnedDegrees(spinning({ rotationPeriodHours: -23.934 }), 23.934 / 96);
|
||||
/**
|
||||
* Which way a body spins in the world: its angular velocity projected on its orbit's normal.
|
||||
* Positive is prograde, turning the same way it goes round; negative is retrograde.
|
||||
*/
|
||||
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', () => {
|
||||
@@ -427,3 +447,20 @@ describe('rotation', () => {
|
||||
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.
|
||||
*/
|
||||
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
|
||||
// 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 geometry = new THREE.SphereGeometry(bodyMarkerRadiusAu(radiusKm), MARKER_WIDTH_SEGMENTS, MARKER_HEIGHT_SEGMENTS);
|
||||
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({
|
||||
map,
|
||||
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.
|
||||
*
|
||||
* `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
|
||||
* black. The map is a set of worlds to look at rather than a light meter, so each is lit as a
|
||||
* photograph of it would be — the same concession the pixel floor makes for size. What the light
|
||||
* does carry truthfully is *direction*: every body shows its day side toward the star and its
|
||||
* night side away from it, and the terminator falls where it really falls.
|
||||
* distance, and under that law Neptune, at 30.2 AU, receives about a six-thousandth of what
|
||||
* Mercury does at 0.39 AU and reads as black. The map is a set of worlds to look at rather than a
|
||||
* light meter, so each is lit as a photograph of it would be — the same concession the pixel
|
||||
* floor makes for size. What the light does carry truthfully is which side is day: every body
|
||||
* 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 {
|
||||
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);
|
||||
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. */
|
||||
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.
|
||||
*
|
||||
* The axis is the orbit normal tilted by the obliquity, about the orbit’s ascending node —
|
||||
* which is where an obliquity is measured from, and the only line in the orbit the elements
|
||||
* name. The phase at the epoch is not published for any of these bodies, so a body is drawn
|
||||
* with its prime meridian toward the node at J2000 and turned from there: the rate and the
|
||||
* direction are real, where the face pointed at the camera is not.
|
||||
* The obliquity fixes how far the pole leans from the orbit normal, and nothing more: which way
|
||||
* it leans needs the pole's right ascension, which the Horizons pages this reads do not carry. The
|
||||
* lean is taken about the orbit's ascending node because that is the one line the elements name,
|
||||
* not because the data says so — so the tilt is real and its azimuth is not. Likewise the phase:
|
||||
* 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
|
||||
* out of the arithmetic without a special case.
|
||||
* Horizons states a retrograde spin twice over, in two conventions: an obliquity past 90 degrees
|
||||
* (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 inclination = elements.inclinationDeg * DEG_TO_RAD;
|
||||
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))
|
||||
.applyAxisAngle(nodeDirection, obliquityDeg * DEG_TO_RAD)
|
||||
.applyAxisAngle(nodeDirection, (obliquityDeg ?? 0) * DEG_TO_RAD)
|
||||
.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()
|
||||
.setFromUnitVectors(SPIN_AXIS, axis)
|
||||
.multiply(new THREE.Quaternion().setFromAxisAngle(SPIN_AXIS, turns * 2 * Math.PI));
|
||||
@@ -269,8 +303,6 @@ export class SystemOrbitsRenderer {
|
||||
const members: SystemMember[] = [];
|
||||
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 = [
|
||||
...bodies.filter((body) => !body.parentBodyId).map((body) => body.orbit.semiMajorAxisAu),
|
||||
...exoplanets.filter((exoplanet) => isPropagatableOrbit(exoplanet.orbit)).map((exoplanet) => exoplanet.orbit.semiMajorAxisAu!)
|
||||
@@ -319,7 +351,8 @@ export class SystemOrbitsRenderer {
|
||||
continue;
|
||||
}
|
||||
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
|
||||
// most exoplanet hosts are red dwarfs a fraction of the Sun's mass.
|
||||
const gm = resolveGravitationalParameter({
|
||||
@@ -374,7 +407,7 @@ export class SystemOrbitsRenderer {
|
||||
body.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(body.frame);
|
||||
body.marker.position.copy(body.position);
|
||||
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);
|
||||
moon.marker.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(moon.frame);
|
||||
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 {
|
||||
const pivot = new THREE.Group();
|
||||
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);
|
||||
pivot.add(orbitLine, marker);
|
||||
this.object.add(pivot);
|
||||
|
||||
@@ -323,7 +323,7 @@ function isWideViewport(): boolean {
|
||||
{{ rate.label }}
|
||||
</label>
|
||||
}
|
||||
@if (time.rate() !== 1) {
|
||||
@if (!time.atNow()) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="time.reset()"
|
||||
@@ -384,7 +384,7 @@ function isWideViewport(): boolean {
|
||||
</button>
|
||||
}
|
||||
</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. -->
|
||||
@if (date()) {
|
||||
<p
|
||||
@@ -418,7 +418,7 @@ export class HudDockComponent implements OnInit {
|
||||
readonly note = input('');
|
||||
/** Camera range, pre-formatted by the scene, which is the only thing that knows the units. */
|
||||
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('');
|
||||
/** Layer state; `null` means the surface has no layers to toggle and no Display tab. */
|
||||
readonly display = input<HudDisplay | null>(null);
|
||||
|
||||
@@ -30,8 +30,9 @@ export interface BodyRecord {
|
||||
*/
|
||||
parentBodyId?: string;
|
||||
/**
|
||||
* How the body turns on its own axis: the sidereal rotation period in hours, negative where it
|
||||
* turns retrograde (Venus, Triton), and the tilt of that axis from its orbital plane.
|
||||
* How the body turns on its own axis: the sidereal rotation period in hours, negative where
|
||||
* 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
|
||||
* 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,
|
||||
* keyed by the same ids used in `bodies.json`.
|
||||
*
|
||||
* This map is the whole of what has actually been photographed. Everything else — every
|
||||
* exoplanet, since not one has ever been imaged, and the moons no probe returned a usable map
|
||||
* of — falls through to `procedural-planet-texture.ts`, which derives a surface from the body's
|
||||
* own measured size, mass, orbit and host star instead.
|
||||
* Only surface *maps* belong here: equirectangular images, twice as wide as tall, that wrap a
|
||||
* sphere. Everything else — every exoplanet, since not one has ever been imaged, and every moon
|
||||
* or dwarf planet with no such map in the repository — falls through to
|
||||
* `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
|
||||
* 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 (CC BY 4.0); jupiter — Solar System Scope 8k pack (CC BY 4.0); pluto —
|
||||
* NASA/JHUAPL/SwRI New Horizons true-color mosaic; deimos — NASA/JPL/University of Arizona
|
||||
* MRO HiRISE; io — NASA/JPL Galileo highest-resolution true-color mosaic; titan — NASA/JPL
|
||||
* Cassini true-color view.
|
||||
* Io, Pluto, Titan and Deimos used to be listed with the square photographs of them in
|
||||
* `assets/textures/bodies/`: pictures of a lit disc against black sky, not maps. Wrapped round a
|
||||
* sphere they put black sky on a fifth to a third of the surface, in a band up to 57 degrees wide
|
||||
* across the equator that the spin then swept past the camera. They are left out until a real map
|
||||
* of each is added.
|
||||
*
|
||||
* 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> = {
|
||||
mercury: 'assets/textures/bodies/mercury.jpg',
|
||||
@@ -26,11 +29,7 @@ const BODY_TEXTURE_PATHS: Record<string, string> = {
|
||||
saturn: 'assets/textures/bodies/saturn.jpg',
|
||||
uranus: 'assets/textures/bodies/uranus.jpg',
|
||||
neptune: 'assets/textures/bodies/neptune.jpg',
|
||||
pluto: 'assets/textures/bodies/pluto.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'
|
||||
moon: 'assets/textures/bodies/moon.jpg'
|
||||
};
|
||||
|
||||
/** 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);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
time.setRate(2_629_800);
|
||||
vi.advanceTimersByTime(5000); // months away
|
||||
|
||||
@@ -38,6 +38,11 @@ const JULIAN_DATE_AT_EPOCH = 2440587.5;
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TimeStore {
|
||||
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 anchorWallMs = Date.now();
|
||||
@@ -62,6 +67,9 @@ export class TimeStore {
|
||||
this.anchorJd = this.julianDate();
|
||||
this.anchorWallMs = Date.now();
|
||||
this.rate.set(secondsPerSecond);
|
||||
if (secondsPerSecond !== 1) {
|
||||
this.atNow.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/** Back to now, at real time — the state the map opens in. */
|
||||
@@ -69,5 +77,6 @@ export class TimeStore {
|
||||
this.anchorJd = dateToJulianDate();
|
||||
this.anchorWallMs = Date.now();
|
||||
this.rate.set(TIME_RATES[0].secondsPerSecond);
|
||||
this.atNow.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user