diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.ts index 1170b46..f472c75 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -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, ); diff --git a/src/app/features/galaxy-system/system-framing.spec.ts b/src/app/features/galaxy-system/system-framing.spec.ts index bcda348..b920144 100644 --- a/src/app/features/galaxy-system/system-framing.spec.ts +++ b/src/app/features/galaxy-system/system-framing.spec.ts @@ -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); diff --git a/src/app/features/galaxy-system/system-framing.ts b/src/app/features/galaxy-system/system-framing.ts index 9395f26..4a4f3f2 100644 --- a/src/app/features/galaxy-system/system-framing.ts +++ b/src/app/features/galaxy-system/system-framing.ts @@ -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) { diff --git a/src/app/features/galaxy-system/system-orbits-renderer.spec.ts b/src/app/features/galaxy-system/system-orbits-renderer.spec.ts index 6d669ef..2247e36 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.spec.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.spec.ts @@ -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. diff --git a/src/app/features/galaxy-system/system-orbits-renderer.ts b/src/app/features/galaxy-system/system-orbits-renderer.ts index 90ae8d4..b721fca 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.ts @@ -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(); - 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,