Frame the system view from the camera it actually has

The grid overflowed the frame in 368 of the 371 systems the datasets
contain — median fill 1.11, and the outermost ring cut off by the viewport
edge in almost every one.

Two compounding causes. The framing distance was a fixed multiple of the
outermost orbit, tuned by eye against a 55-degree field of view; the
engine's camera is 50. And it framed the outermost *orbit*, while the
widest thing actually drawn is the grid's outer ring, which by
construction always sits beyond it.

Neither is fixable by adjusting the multiple, because a multiple is the
wrong shape of answer: what has to fit is a radius on screen, and how much
radius a given distance buys depends entirely on the lens. So the distance
now comes from the camera's own vertical field of view and aspect —
picking whichever screen axis is the tighter one, so a portrait window
backs off further rather than clipping — applied to the grid's outer ring
with an explicit margin around it.

The ceiling goes up with it. Eighty AU could not frame the solar system
out to Pluto once the real field of view was accounted for; that needs 120
on a landscape display and 140 on a portrait one. Only companions hundreds
of AU out reach the new ceiling, and those still arrive framed on their
inner region.

Measured across every system in the data, at three window shapes: the
overflow count drops from 368 to 2, the fill settles at exactly 0.89 —
the margin, uniformly — and the outer ring still encloses the outermost
orbit everywhere, so neither invariant was traded for the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
Claude
2026-08-05 07:30:02 +00:00
parent ac296f5133
commit 6019987fc4
6 changed files with 152 additions and 18 deletions
@@ -6,8 +6,10 @@ import {
bodyMarkerRadiusAu,
DEFAULT_STAR_MARKER_RADIUS_AU,
starMarkerRadiusAu,
systemFrameRadiusAu,
systemFramingDistanceAu,
systemGridRingsAu,
SystemViewport,
SYSTEM_VIEW_DIRECTION_IN_PLANE,
systemViewDirection
} from './system-framing';
@@ -51,9 +53,9 @@ describe('starMarkerRadiusAu', () => {
});
describe('systemFramingDistanceAu', () => {
it('fits the whole system in view', () => {
for (const { outermost } of [TRAPPIST_1, GL_357, SOLAR]) {
expect(systemFramingDistanceAu(outermost)).toBeGreaterThan(outermost);
it('fits the radius it is given in view, with room around it', () => {
for (const radius of [TRAPPIST_1.outermost, GL_357.outermost, SOLAR.outermost]) {
expect(systemFrameRadiusAu(systemFramingDistanceAu(radius))).toBeGreaterThan(radius);
}
});
@@ -63,13 +65,39 @@ describe('systemFramingDistanceAu', () => {
expect(systemFramingDistanceAu(GL_357.outermost)).toBeLessThan(1);
});
it('scales in proportion to the outermost orbit', () => {
it('scales in proportion to the radius it has to frame', () => {
expect(systemFramingDistanceAu(0.2) / systemFramingDistanceAu(0.1)).toBeCloseTo(2, 9);
});
it('backs off further for a narrower field of view, which a fixed multiple could not', () => {
// The bug this replaced: the multiple was tuned by eye against a 55-degree field and the
// engine's camera is 50, so everything sat that much too close.
const wide = systemFramingDistanceAu(1, { fovDegrees: 70, aspect: 1.78 });
const narrow = systemFramingDistanceAu(1, { fovDegrees: 30, aspect: 1.78 });
expect(narrow).toBeGreaterThan(wide);
});
it('backs off further for a portrait window, where the horizontal axis is the tighter one', () => {
const landscape = systemFramingDistanceAu(1, { fovDegrees: 50, aspect: 1.78 });
const portrait = systemFramingDistanceAu(1, { fovDegrees: 50, aspect: 0.6 });
expect(portrait).toBeCloseTo(landscape / 0.6, 6);
});
it('ignores aspect once the window is landscape, since the vertical binds there', () => {
const square = systemFramingDistanceAu(1, { fovDegrees: 50, aspect: 1 });
expect(systemFramingDistanceAu(1, { fovDegrees: 50, aspect: 2.5 })).toBeCloseTo(square, 9);
});
it('caps the distance so a far-flung companion cannot shrink the star to nothing', () => {
expect(systemFramingDistanceAu(1000)).toBe(systemFramingDistanceAu(5000));
expect(systemFramingDistanceAu(SOLAR.outermost)).toBeLessThanOrEqual(80);
});
it('reaches far enough to frame the solar system out to Pluto', () => {
// The old 80 AU ceiling could not: at the camera's real field of view this needs 120.
const rings = systemGridRingsAu(39.288);
const distance = systemFramingDistanceAu(rings[rings.length - 1]);
expect(distance).toBeLessThan(200);
expect(systemFrameRadiusAu(distance)).toBeGreaterThan(39.288);
});
it('stays outside the orbit controls minimum distance', () => {
@@ -78,8 +106,50 @@ describe('systemFramingDistanceAu', () => {
});
it('uses a sensible default for a star with no known planets', () => {
for (const outermost of [0, -1, Number.NaN]) {
expect(systemFramingDistanceAu(outermost)).toBe(3);
for (const radius of [0, -1, Number.NaN]) {
expect(systemFramingDistanceAu(radius)).toBe(3);
}
});
});
describe('the grid and the framing together', () => {
/** What the scene actually composes: rings from the orbits, then a distance from the rings. */
function fit(outermostOrbitAu: number, viewport?: SystemViewport): { ring: number; frame: number } {
const rings = systemGridRingsAu(outermostOrbitAu);
const ring = rings[rings.length - 1];
return { ring, frame: systemFrameRadiusAu(systemFramingDistanceAu(ring, viewport), viewport) };
}
const VIEWPORTS: SystemViewport[] = [
{ fovDegrees: 50, aspect: 1.78 },
{ fovDegrees: 50, aspect: 1 },
{ fovDegrees: 50, aspect: 0.6 }
];
it('leaves the outermost ring clear of the frame edge at every scale and window shape', () => {
// The whole point of framing against the grid rather than the orbits: before this, 368 of
// the 371 systems in the datasets drew a grid wider than the view that was meant to hold it.
for (const viewport of VIEWPORTS) {
for (const { outermost } of [TRAPPIST_1, GL_357, SOLAR, { outermost: 1 }, { outermost: 12.4 }]) {
const { ring, frame } = fit(outermost, viewport);
expect(ring).toBeLessThan(frame);
expect(ring / frame).toBeLessThan(0.93);
}
}
});
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);
}
});
it('does not overshoot either: the grid still fills most of the frame', () => {
// A margin is not the same as framing a system from orbit. Half the frame empty would be as
// wrong as none of it.
for (const { outermost } of [TRAPPIST_1, GL_357, SOLAR]) {
const { ring, frame } = fit(outermost);
expect(ring / frame).toBeGreaterThan(0.6);
}
});
});