Put a reference grid under the system view
A system was a handful of ellipses floating in the dark. You could see that one orbit was bigger than another, but not how big, and not that a planet sat above or below the plane the others share. Adds the same plane-and-tether reading aid the outer scales got: a polar grid in the system's own reference plane, with a drop line from each body onto it. Ring radii snap to a 1-2-5 ladder rather than dividing the system evenly, because the point is to put a number on a distance — 5, 10, 15 AU can be read at a glance and 4.34, 8.68, 13.02 cannot. That holds across the four orders of magnitude real systems span: the solar system gets 5 AU rings, TRAPPIST-1 gets 0.01 AU ones. The outermost ring encloses the outermost orbit rather than falling just inside it. The rings are dashed. Solid ones would sit in the same plane as the orbit ellipses, which are themselves rings, and at a glance a reference circle and a circular orbit are the same picture. Dashes are cut by dropping whole segments rather than by a dashed material: the ring is already built from independent segment pairs, so a material's dash pattern would restart at every one. Drawing the grid exposed a framing bug it made unmissable. The camera settled along one fixed direction derived from the ecliptic, which is face-on only for the one system whose elements are ecliptic. Every exoplanet system — measured against the plane of the sky, perpendicular to the line of sight to its own host star — was being presented nearly edge-on, a smear of overlapping ellipses. The settle direction is now taken relative to whichever plane the system was measured in, so all of them read as discs. The solar system is unmoved, which a test pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
@@ -4,7 +4,8 @@ import { gmForParent } from '../../shared/astro/constants';
|
||||
import { isPropagatableOrbit, orbitEllipsePoints, propagateOrbit, resolveGravitationalParameter, resolveOrbitalElements } from '../../shared/astro/kepler';
|
||||
import { CartesianCoordinates, OBLIQUITY_J2000_DEG } from '../../shared/astro/coordinates';
|
||||
import { BodyRecord, OrbitalElements } from '../../shared/models/body.model';
|
||||
import { bodyMarkerRadiusAu } from './system-framing';
|
||||
import { bodyMarkerRadiusAu, systemGridRingsAu } from './system-framing';
|
||||
import { PolarGridPlane, TetherField } from './grid-plane';
|
||||
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
||||
|
||||
export type SystemMemberKind = 'planet' | 'moon' | 'dwarf' | 'exoplanet';
|
||||
@@ -31,6 +32,11 @@ const ORBIT_LINE_OPACITY_BY_KIND: Record<SystemMemberKind, number> = {
|
||||
const EARTH_RADIUS_KM = 6371;
|
||||
const DEG_TO_RAD = Math.PI / 180;
|
||||
|
||||
/** Spokes on the system's reference grid, and how loudly it is drawn against the orbits. */
|
||||
const SYSTEM_GRID_SPOKES = 12;
|
||||
const SYSTEM_GRID_OPACITY = 0.28;
|
||||
const SYSTEM_TETHER_OPACITY = 0.3;
|
||||
|
||||
/**
|
||||
* Rotation carrying the **ecliptic** frame into the scene's equatorial one — a turn of the
|
||||
* obliquity about the shared vernal-equinox axis. Solar-system elements come from Horizons
|
||||
@@ -145,10 +151,22 @@ export class SystemOrbitsRenderer {
|
||||
readonly maxTopLevelSemiMajorAxisAu: number;
|
||||
/** Smallest semi-major axis (AU) among top-level bodies/exoplanets; 0 if there are none. */
|
||||
readonly minTopLevelSemiMajorAxisAu: number;
|
||||
/**
|
||||
* The plane this system is read against, as a rotation from XY into the scene's equatorial
|
||||
* frame: the ecliptic for the solar system, the plane of the sky for everything else.
|
||||
*/
|
||||
readonly referenceFrame: THREE.Quaternion;
|
||||
|
||||
private readonly topLevelBodies: TrackedTopLevelBody[] = [];
|
||||
private readonly moons: TrackedMoon[] = [];
|
||||
private readonly disposables: Array<{ geometry: THREE.BufferGeometry; material: THREE.Material }> = [];
|
||||
private readonly grid?: PolarGridPlane;
|
||||
private readonly tethers?: TetherField;
|
||||
/**
|
||||
* Aliases of the tracked bodies' own position vectors, which `update` writes in place — so
|
||||
* following them each tick costs no allocation at all.
|
||||
*/
|
||||
private tetherPoints: readonly THREE.Vector3[] = [];
|
||||
|
||||
constructor(
|
||||
bodies: readonly BodyRecord[],
|
||||
@@ -222,6 +240,35 @@ export class SystemOrbitsRenderer {
|
||||
}
|
||||
|
||||
this.members = members;
|
||||
|
||||
// Which plane the system is read against follows from where its elements came from. Only the
|
||||
// Sun has Horizons bodies and no system has both, so this is a choice between the two rather
|
||||
// than a compromise: the ecliptic if there are solar-system bodies, the sky plane otherwise.
|
||||
this.referenceFrame = bodies.some((body) => !body.parentBodyId) ? ECLIPTIC_FRAME.clone() : exoplanetFrame;
|
||||
|
||||
const rings = systemGridRingsAu(this.maxTopLevelSemiMajorAxisAu);
|
||||
if (rings.length > 0) {
|
||||
this.grid = new PolarGridPlane({
|
||||
ringRadii: rings,
|
||||
spokeCount: SYSTEM_GRID_SPOKES,
|
||||
orientation: this.referenceFrame,
|
||||
// Quieter and dashed, unlike the galaxy view's: here the grid shares a plane with the
|
||||
// orbit ellipses, which are themselves rings, and it must not be mistaken for one.
|
||||
opacity: SYSTEM_GRID_OPACITY,
|
||||
dashed: true,
|
||||
emphasisRadii: [rings[rings.length - 1]]
|
||||
});
|
||||
this.grid.setStrength(1);
|
||||
|
||||
this.tethers = new TetherField(this.topLevelBodies.length, {
|
||||
normal: new THREE.Vector3(0, 0, 1).applyQuaternion(this.referenceFrame),
|
||||
opacity: SYSTEM_TETHER_OPACITY
|
||||
});
|
||||
this.tethers.setStrength(1);
|
||||
this.tetherPoints = this.topLevelBodies.map((body) => body.position);
|
||||
|
||||
this.object.add(this.grid.object, this.tethers.object);
|
||||
}
|
||||
}
|
||||
|
||||
/** Recomputes every marker's position for the given Julian date. Call once per tick. */
|
||||
@@ -241,6 +288,10 @@ export class SystemOrbitsRenderer {
|
||||
const orbital = propagateOrbit(moon.elements, moon.gmAu3PerDay2, epochJd);
|
||||
moon.marker.position.set(orbital.x, orbital.y, orbital.z).applyQuaternion(moon.frame);
|
||||
}
|
||||
|
||||
// Moons are left out: their tether would land within a marker's width of their planet's and
|
||||
// say nothing the planet's has not already said.
|
||||
this.tethers?.setTargets(this.tetherPoints);
|
||||
}
|
||||
|
||||
/** Looks up which system member a marker object belongs to (e.g. from a raycast hit). */
|
||||
@@ -254,6 +305,8 @@ export class SystemOrbitsRenderer {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.grid?.dispose();
|
||||
this.tethers?.dispose();
|
||||
for (const { geometry, material } of this.disposables) {
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
|
||||
Reference in New Issue
Block a user