Leave a system outwards from wherever the camera stands, and test that the scene frames the furthest it draws
Leaving a system flew the camera to a fixed 400 AU. Since c45d916 frames the Sun's system on Eris's
97.7 AU aphelion, a phone held upright arrives 508 AU out, so leaving drew the system 21 per cent
nearer during the 0.9 s exit (review, 390x844: 507.9 -> 400.0 AU). The exit now flies to 400 AU or
half as far again as the camera already stands, whichever is further. Measured live on :4301, the
distance from the Sun through the exit, first and last frame:
390x844 507.9 -> 761.8 AU (was 507.9 -> 400.0)
360x780 508.5 -> 762.7
768x1024 312.9 -> 469.3 (was 312.9 -> 400)
1600x1000 234.7 -> 400.0 (unchanged)
and never nearer in between. The swap puts the camera at GALAXY_APPROACH_DISTANCE_PC whatever the
exit distance, so only the animation changes.
No test checked that the scene hands outermostRadiusAu to the framing: framing on the 67.9 AU
semi-major axis instead, which on a square window puts Eris off screen on arrival, passed all 841
tests. The scene spec's fixture held Earth alone. A describe now adds Eris (a = 67.934, e = 0.4382):
- "frames the furthest the system draws" requires the settled camera to stand at
systemFramingDistanceAu(outermostRadiusAu) from its target, 234.7 AU at aspect 1.
- "leaves the system outwards even from a phone's framing" enters at aspect 390/844, then samples the
camera each frame of the exit until the swap: never nearer, and further at the end.
Controls, each run on the full suite:
- outermostRadiusAu -> maxTopLevelSemiMajorAxisAu in the scene fails both (2 failed, 842 passed);
the second fails too because the arrival is then under 500 AU.
- Math.max -> Math.min in the exit (back to 400 AU) fails the exit test only (1 failed, 843 passed).
The framing spec's fit() said it was "what the scene actually composes"; it frames the ring only, so
its comment now says it is the grid's half and where the aphelion is tested.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@ import { GalaxySystemSceneComponent } from './galaxy-system-scene.component';
|
|||||||
import { galacticNormal } from './grid-plane';
|
import { galacticNormal } from './grid-plane';
|
||||||
import { JumpLinkRenderer } from './jump-link-renderer';
|
import { JumpLinkRenderer } from './jump-link-renderer';
|
||||||
import { StarFieldRenderer } from './star-field-renderer';
|
import { StarFieldRenderer } from './star-field-renderer';
|
||||||
|
import { systemFramingDistanceAu } from './system-framing';
|
||||||
|
import { SystemOrbitsRenderer } from './system-orbits-renderer';
|
||||||
import { LabeledPoint, StarLabelOverlay } from './star-label-overlay';
|
import { LabeledPoint, StarLabelOverlay } from './star-label-overlay';
|
||||||
|
|
||||||
// jsdom does not implement ResizeObserver; the component only uses it to react to real
|
// jsdom does not implement ResizeObserver; the component only uses it to react to real
|
||||||
@@ -877,6 +879,58 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
expect(note()).toMatch(/ to 2020-12-21 18:00 UTC\.$/);
|
expect(note()).toMatch(/ to 2020-12-21 18:00 UTC\.$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with Eris, whose aphelion runs past the grid', () => {
|
||||||
|
type FramedScene = { bodies: BodyRecord[]; controls: { target: THREE.Vector3 }; systemRenderer: SystemOrbitsRenderer; systemGroup: THREE.Group };
|
||||||
|
// Its 67.9 AU axis gives the grid an 80 AU outer ring; at aphelion it is 97.7 AU out.
|
||||||
|
const ERIS: BodyRecord = {
|
||||||
|
...EARTH, id: 'eris', name: 'Eris', kind: 'dwarf', radiusKm: 1163,
|
||||||
|
orbit: { ...EARTH.orbit, semiMajorAxisAu: 67.934, eccentricity: 0.4382 }, rates: keplerRates(67.934, GM_SUN_AU3_PER_DAY2)
|
||||||
|
};
|
||||||
|
|
||||||
|
async function enterTheSun(aspect: number): Promise<FramedScene> {
|
||||||
|
const component = fixture.componentInstance as unknown as FramedScene;
|
||||||
|
component.bodies = [EARTH, ERIS];
|
||||||
|
engine.getPerspectiveCamera().aspect = aspect;
|
||||||
|
navigationStore.selectStar(SUN.id);
|
||||||
|
await flushAsync();
|
||||||
|
await advanceFrames(engine, 2.5);
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('frames the furthest the system draws, Eris’s aphelion, not the ring inside it nor its semi-major axis', async () => {
|
||||||
|
const component = await enterTheSun(1);
|
||||||
|
const camera = engine.getPerspectiveCamera();
|
||||||
|
expect(component.systemRenderer.outermostRadiusAu).toBeCloseTo(67.934 * 1.4382, 9);
|
||||||
|
// 234.7 AU; framed on the 67.9 AU axis the camera would stand at 163 AU and Eris arrive off screen.
|
||||||
|
expect(camera.position.distanceTo(component.controls.target)).toBeCloseTo(
|
||||||
|
systemFramingDistanceAu(component.systemRenderer.outermostRadiusAu, { fovDegrees: camera.fov, aspect: camera.aspect }),
|
||||||
|
6
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the system outwards even from a phone’s framing, which stands past the 400 AU it used to fly to', async () => {
|
||||||
|
const component = await enterTheSun(390 / 844);
|
||||||
|
const camera = engine.getCamera();
|
||||||
|
const arrival = camera.position.length();
|
||||||
|
expect(arrival).toBeGreaterThan(500);
|
||||||
|
|
||||||
|
navigationStore.selectStar(null);
|
||||||
|
await flushAsync(1);
|
||||||
|
// Until the swap: it flew 508 AU in to 400, and the system grew on screen while the reader left it.
|
||||||
|
let previous = arrival;
|
||||||
|
for (let frame = 0; frame < 100 && component.systemGroup.visible; frame++) {
|
||||||
|
engine.tick(0.05);
|
||||||
|
await flushAsync(1);
|
||||||
|
if (component.systemGroup.visible) {
|
||||||
|
expect(camera.position.length()).toBeGreaterThanOrEqual(previous - 1e-9);
|
||||||
|
previous = camera.position.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(component.systemGroup.visible).toBe(false);
|
||||||
|
expect(previous).toBeGreaterThan(arrival);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('performs the floating-origin recenter: the camera lands close to the AU-space origin, not out at parsec-scale coordinates', async () => {
|
it('performs the floating-origin recenter: the camera lands close to the AU-space origin, not out at parsec-scale coordinates', async () => {
|
||||||
navigationStore.selectStar(ALPHA_CENTAURI.id);
|
navigationStore.selectStar(ALPHA_CENTAURI.id);
|
||||||
await flushAsync();
|
await flushAsync();
|
||||||
|
|||||||
@@ -289,8 +289,13 @@ const SYSTEM_MIN_DISTANCE_AU = 0.05;
|
|||||||
const SYSTEM_MAX_DISTANCE_AU = 5000;
|
const SYSTEM_MAX_DISTANCE_AU = 5000;
|
||||||
/** Where the camera lands (AU) immediately after swapping into system space, pre-settle. */
|
/** Where the camera lands (AU) immediately after swapping into system space, pre-settle. */
|
||||||
const SYSTEM_ENTRY_DISTANCE_AU = 200;
|
const SYSTEM_ENTRY_DISTANCE_AU = 200;
|
||||||
/** How far out (AU) the camera flies before swapping back to galaxy/parsec space. */
|
/**
|
||||||
|
* How far out (AU) the camera flies, at least, before swapping back to galaxy/parsec space. A camera
|
||||||
|
* already beyond it flies half as far again: a phone held upright frames the Sun's system from 508 AU,
|
||||||
|
* and flying to 400 drew the system 21 per cent nearer while the reader was leaving it.
|
||||||
|
*/
|
||||||
const SYSTEM_EXIT_DISTANCE_AU = 400;
|
const SYSTEM_EXIT_DISTANCE_AU = 400;
|
||||||
|
const SYSTEM_EXIT_PULL_BACK = 1.5;
|
||||||
|
|
||||||
const APPROACH_DURATION_SECONDS = 1.0;
|
const APPROACH_DURATION_SECONDS = 1.0;
|
||||||
const SETTLE_DURATION_SECONDS = 0.9;
|
const SETTLE_DURATION_SECONDS = 0.9;
|
||||||
@@ -2321,7 +2326,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.rig.flyTo(
|
this.rig.flyTo(
|
||||||
{
|
{
|
||||||
position: direction.clone().multiplyScalar(SYSTEM_EXIT_DISTANCE_AU),
|
position: direction.clone().multiplyScalar(Math.max(SYSTEM_EXIT_DISTANCE_AU, SYSTEM_EXIT_PULL_BACK * camera.position.length())),
|
||||||
target: new THREE.Vector3(0, 0, 0),
|
target: new THREE.Vector3(0, 0, 0),
|
||||||
},
|
},
|
||||||
EXIT_DURATION_SECONDS,
|
EXIT_DURATION_SECONDS,
|
||||||
|
|||||||
@@ -117,7 +117,11 @@ describe('systemFramingDistanceAu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('the grid and the framing together', () => {
|
describe('the grid and the framing together', () => {
|
||||||
/** What the scene actually composes: rings from the orbits, then a distance from the rings. */
|
/**
|
||||||
|
* The grid's half of what the scene composes: rings from the orbits, then a distance from the outer
|
||||||
|
* ring. The scene frames the larger of that ring and the furthest aphelion (`outermostRadiusAu`),
|
||||||
|
* which the Eris test below frames where it runs past the ring, and the scene's own spec checks.
|
||||||
|
*/
|
||||||
function fit(outermostOrbitAu: number, viewport?: SystemViewport): { ring: number; frame: number } {
|
function fit(outermostOrbitAu: number, viewport?: SystemViewport): { ring: number; frame: number } {
|
||||||
const rings = systemGridRingsAu(outermostOrbitAu);
|
const rings = systemGridRingsAu(outermostOrbitAu);
|
||||||
const ring = rings[rings.length - 1];
|
const ring = rings[rings.length - 1];
|
||||||
|
|||||||
Reference in New Issue
Block a user