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:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user