Frame the Sun's system on Eris's aphelion, the furthest it draws, not on the grid ring inside it
The scene framed the grid's outer ring, sized from the largest semi-major axis, and two comments said the ring was "always the wider of the two, by construction". That held until Eris came in: its a = 67.93 AU gives an 80 AU ring, but with e = 0.438 its orbit reaches 97.7 AU, and Eris is 95.5 AU out now. The 12 per cent margin protected the ring, not Eris. The review measured Eris's orbit at 0.982 of the half-width on a 390x844 phone (3.5 px from the edge), 0.987 on 1000x1400, and on a 1000x1000 window Eris's marker at NDC 1.002, off screen on arrival. SystemOrbitsRenderer.gridOuterRadiusAu becomes outermostRadiusAu: the ring, or the largest top-level aphelion a(1 + e) where that runs past it. The scene frames that. Some orbit runs past its ring in 303 of the 1 190 exoplanet systems too (counted on exoplanets.json), and they are framed the same way. The 500 AU ceiling rises to 600: the aphelion needs 508 AU on a 390x844 phone, and 600 holds it with its whole margin down to an aspect of 0.39. The comments are corrected. Measured in the app on :4301 after entering the Sun, Eris's drawn orbit, largest |NDC x| over its 129 vertices (review's figures before): 390x844 camera 507.9 AU 0.804 (0.982) 1000x1400 camera 328.5 AU 0.805 (0.987) 1000x1000 camera 234.7 AU 0.812 (1.004, marker off screen) 950x1000 camera 247.0 AU 0.810 1600x1000 camera 234.7 AU 0.508 (0.627) No orbit vertex of Neptune, Pluto, Eris, Haumea or Makemake is off screen at any of them. The cost: inner bodies arrive smaller, the landscape camera 235 AU out instead of 192. Tests: renderer 'reaches as far as an eccentric orbit goes past the grid: Eris's aphelion, 97.7 AU, not the 80 AU ring' (and the ring where every orbit stays inside it), and framing 'leaves Eris's aphelion its whole margin in every window shape'. Controls, each failing its named test only (1 failed, 839 passed): framing on the ring alone; the ceiling back at 500 AU. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2221,8 +2221,8 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
this.systemGroup.add(this.systemRenderer.object);
|
||||
this.applyDisplay(this.display());
|
||||
|
||||
// Framed against the grid's outer ring rather than the outermost orbit — the ring is always
|
||||
// the wider of the two — and against the camera this scene actually has, so the margin holds
|
||||
// Framed against the outermost thing drawn — the grid's outer ring, or an eccentric orbit's
|
||||
// aphelion where it runs past it — and against the camera this scene actually has, so the margin holds
|
||||
// whatever the window shape. Computed before the star, because how far away the star will be
|
||||
// seen from is what decides how big its halo has to be to stay visible.
|
||||
// Framed against the perspective camera whichever is active: the framing distance is what
|
||||
@@ -2230,7 +2230,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
const framingCamera = this.engine.getPerspectiveCamera();
|
||||
const viewport = { fovDegrees: framingCamera.fov, aspect: framingCamera.aspect };
|
||||
const framingDistance = systemFramingDistanceAu(
|
||||
this.systemRenderer.gridOuterRadiusAu,
|
||||
this.systemRenderer.outermostRadiusAu,
|
||||
viewport,
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ const GL_357 = { innermost: 0.035, outermost: 0.204 };
|
||||
const SOLAR = { innermost: 0.387, outermost: 30.07 };
|
||||
/** The solar system as the map draws it: out to Eris's semi-major axis, 67.93 AU. */
|
||||
const SOLAR_TO_ERIS = { innermost: 0.387, outermost: 67.93 };
|
||||
/** Eris's aphelion, a(1 + e) = 67.934 x 1.4382: past the 80 AU ring its semi-major axis gives. */
|
||||
const ERIS_APHELION_AU = 97.7;
|
||||
|
||||
describe('starMarkerRadiusAu', () => {
|
||||
it('never reaches the innermost orbit', () => {
|
||||
@@ -141,6 +143,14 @@ describe('the grid and the framing together', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves Eris’s aphelion its whole margin in every window shape, a phone held upright included', () => {
|
||||
// The scene frames the aphelion where it runs past the ring. Under the old 500 AU ceiling the
|
||||
// phone would hold it at 0.907 of the half-width instead of 1 / 1.12 = 0.893.
|
||||
for (const viewport of VIEWPORTS) {
|
||||
expect(ERIS_APHELION_AU / systemFrameRadiusAu(systemFramingDistanceAu(ERIS_APHELION_AU, viewport), viewport)).toBeLessThan(0.9);
|
||||
}
|
||||
});
|
||||
|
||||
it('still encloses the outermost orbit, so no planet sits off the edge of the grid', () => {
|
||||
for (const { outermost } of [TRAPPIST_1, GL_357, SOLAR, { outermost: 1 }, { outermost: 12.4 }]) {
|
||||
expect(fit(outermost).ring).toBeGreaterThan(outermost);
|
||||
|
||||
@@ -61,15 +61,17 @@ const MIN_FRAMING_DISTANCE_AU = 0.06;
|
||||
/**
|
||||
* Ceiling on the framing distance, so a distant companion does not push the star to a dot.
|
||||
*
|
||||
* Generous enough to frame the solar system out to Eris in any window a reader holds: its grid's
|
||||
* outer ring, 80 AU, needs 192 AU on a landscape display and 416 on a 390 by 844 phone once the
|
||||
* camera's real field of view is accounted for, and 500 holds it down to an aspect of 0.385. At
|
||||
* 200, which framed Pluto's 40 AU ring, a portrait window arrived with Eris off screen, and a
|
||||
* phone with Makemake too. Only genuinely pathological systems reach it now — the handful with
|
||||
* directly-imaged companions hundreds of AU out — and those still arrive framed on their inner
|
||||
* region, with the orbit controls reaching far enough to pull back to the rest.
|
||||
* Generous enough to frame the solar system out to Eris in any window a reader holds: Eris's
|
||||
* aphelion, 97.7 AU, the furthest it draws, needs 235 AU on a landscape display and 508 on a 390
|
||||
* by 844 phone once the camera's real field of view is accounted for, and 600 holds it down to an
|
||||
* aspect of 0.39. At 500, framed on the 80 AU grid ring inside that aphelion, a phone arrived with
|
||||
* Eris's orbit 3.5 px from the edge; at 200, which framed Pluto's 40 AU ring, a portrait window
|
||||
* arrived with Eris off screen, and a phone with Makemake too. Only genuinely pathological systems
|
||||
* reach it now — the handful with directly-imaged companions hundreds of AU out — and those still
|
||||
* arrive framed on their inner region, with the orbit controls reaching far enough to pull back
|
||||
* to the rest.
|
||||
*/
|
||||
const MAX_FRAMING_DISTANCE_AU = 500;
|
||||
const MAX_FRAMING_DISTANCE_AU = 600;
|
||||
|
||||
/** Framing for a star with no known planets, where there is nothing to fit. */
|
||||
const EMPTY_SYSTEM_FRAMING_DISTANCE_AU = 3;
|
||||
@@ -137,8 +139,9 @@ export function systemFrameRadiusAu(distanceAu: number, viewport: SystemViewport
|
||||
* was tuned by eye against a 55-degree field, and the engine's camera is 50 — which left the
|
||||
* grid overflowing the frame in 368 of the 371 systems the datasets contain.
|
||||
*
|
||||
* Callers pass the outermost thing actually drawn, which is the reference grid's outer ring
|
||||
* rather than the outermost orbit — the ring is always the wider of the two, by construction.
|
||||
* Callers pass the outermost thing actually drawn: the reference grid's outer ring, which runs past
|
||||
* every semi-major axis by construction, or an eccentric orbit's aphelion where that runs past the
|
||||
* ring, as Eris's does.
|
||||
*/
|
||||
export function systemFramingDistanceAu(framedRadiusAu: number, viewport: SystemViewport = DEFAULT_SYSTEM_VIEWPORT): number {
|
||||
if (!Number.isFinite(framedRadiusAu) || framedRadiusAu <= 0) {
|
||||
|
||||
@@ -449,6 +449,28 @@ describe('rotation without IAU elements', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('outermostRadiusAu', () => {
|
||||
function drawn(axis: number, eccentricity: number): BodyRecord {
|
||||
return {
|
||||
id: 'eris', systemStarId: 0, name: 'Eris', kind: 'dwarf', radiusKm: 1163, orbitSource: 'test',
|
||||
orbit: { semiMajorAxisAu: axis, eccentricity, inclinationDeg: 44, longitudeOfAscendingNodeDeg: 36, argumentOfPeriapsisDeg: 151, meanAnomalyAtEpochDeg: 0, epochJd: DEFAULT_EPOCH_JD },
|
||||
rates: keplerRates(axis, GM_SUN_AU3_PER_DAY2)
|
||||
};
|
||||
}
|
||||
|
||||
it('reaches as far as an eccentric orbit goes past the grid: Eris’s aphelion, 97.7 AU, not the 80 AU ring', () => {
|
||||
const renderer = new SystemOrbitsRenderer([drawn(67.934, 0.4382)], []);
|
||||
expect(renderer.outermostRadiusAu).toBeCloseTo(67.934 * 1.4382, 9);
|
||||
renderer.dispose();
|
||||
});
|
||||
|
||||
it('is the grid’s outer ring where every orbit stays inside it', () => {
|
||||
const renderer = new SystemOrbitsRenderer([drawn(30, 0.01)], []);
|
||||
expect(renderer.outermostRadiusAu).toBe(35);
|
||||
renderer.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('photographs', () => {
|
||||
it('puts them on their bodies once loaded, one a frame, so the GPU is not handed every map at once, and in their own colours', () => {
|
||||
// Ids no other test here draws, since the loaded textures are shared through the cache.
|
||||
|
||||
@@ -329,10 +329,12 @@ export class SystemOrbitsRenderer {
|
||||
*/
|
||||
readonly referenceFrame: THREE.Quaternion;
|
||||
/**
|
||||
* Outer radius (AU) of the reference grid, or 0 where there is none. This — not the outermost
|
||||
* orbit — is the widest thing the system draws, so it is what the camera has to frame.
|
||||
* How far (AU) from the star the system draws anything, or 0 where it draws nothing: what the
|
||||
* camera has to frame. The reference grid's outer ring, which runs 15 per cent past the largest
|
||||
* semi-major axis, unless an eccentric orbit reaches further at its aphelion — Eris's, 97.7 AU,
|
||||
* does past the solar system's 80 AU ring, and some orbit does in 303 of the 1 190 exoplanet systems.
|
||||
*/
|
||||
readonly gridOuterRadiusAu: number;
|
||||
readonly outermostRadiusAu: number;
|
||||
|
||||
private readonly topLevelBodies: TrackedTopLevelBody[] = [];
|
||||
private readonly moons: TrackedMoon[] = [];
|
||||
@@ -376,10 +378,11 @@ export class SystemOrbitsRenderer {
|
||||
const members: SystemMember[] = [];
|
||||
const topLevelBodiesById = new Map<string, BodyRecord>();
|
||||
|
||||
const topLevelAxes = [
|
||||
...bodies.filter((body) => !body.parentBodyId).map((body) => body.orbit.semiMajorAxisAu),
|
||||
...exoplanets.filter((exoplanet) => isPropagatableOrbit(exoplanet.orbit)).map((exoplanet) => exoplanet.orbit.semiMajorAxisAu!)
|
||||
].filter((axis) => Number.isFinite(axis) && axis > 0);
|
||||
const topLevelOrbits = [
|
||||
...bodies.filter((body) => !body.parentBodyId).map(({ orbit }) => ({ axis: orbit.semiMajorAxisAu, eccentricity: orbit.eccentricity })),
|
||||
...exoplanets.filter((exoplanet) => isPropagatableOrbit(exoplanet.orbit)).map(({ orbit }) => ({ axis: orbit.semiMajorAxisAu!, eccentricity: orbit.eccentricity ?? 0 }))
|
||||
].filter(({ axis }) => Number.isFinite(axis) && axis > 0);
|
||||
const topLevelAxes = topLevelOrbits.map(({ axis }) => axis);
|
||||
this.maxTopLevelSemiMajorAxisAu = topLevelAxes.length > 0 ? Math.max(...topLevelAxes) : 0;
|
||||
this.minTopLevelSemiMajorAxisAu = topLevelAxes.length > 0 ? Math.min(...topLevelAxes) : 0;
|
||||
|
||||
@@ -453,7 +456,7 @@ export class SystemOrbitsRenderer {
|
||||
this.referenceFrame = bodies.some((body) => !body.parentBodyId) ? ECLIPTIC_FRAME.clone() : exoplanetFrame;
|
||||
|
||||
const rings = systemGridRingsAu(this.maxTopLevelSemiMajorAxisAu);
|
||||
this.gridOuterRadiusAu = rings.length > 0 ? rings[rings.length - 1] : 0;
|
||||
this.outermostRadiusAu = Math.max(rings.length > 0 ? rings[rings.length - 1] : 0, ...topLevelOrbits.map(({ axis, eccentricity }) => axis * (1 + eccentricity)));
|
||||
if (rings.length > 0) {
|
||||
this.grid = new PolarGridPlane({
|
||||
ringRadii: rings,
|
||||
|
||||
Reference in New Issue
Block a user