Show the date and the clock on a body's page, and open it on the side of the equator its Sun lights
Since the page turns a body as it stands at the map's date, it has been drawn for a date it never showed, at a rate it gave no way to change: set to 2032 at a day a second in the system view, Saturn's page ran on at that rate with only Search and Bookmarks on its dock, and at a month a second Earth turned about 183 degrees a frame at 60 Hz. The dock now takes `clock`, which offers the clock without the layers, as a tab named Clock, and the page binds it with the date strip the system view already has. Measured on :4301 at 2032-06-01: the strip reads "Date 2032-06-01", the tabs are Search, Bookmarks and Clock, and the Clock panel has the four rates and the date field; Back to now clears the strip. The camera opened 11.3 degrees north of the equator whatever the season, which since 2025 put Saturn's page on the unlit face of its rings, and will until 2039. When a body is shown, the next frame puts the camera on the side of the equator the Sun is on: measured on Saturn's page, the Sun at -26.71 degrees on 2032-06-01 and the camera at -11.31; Earth's in June stays north. Tests, each proved by a guarded mutant that fails it: - the page follows the clock after its first frame (frozen at the first frame: 90 degrees out); - a body with no IAU model shown after Earth gets the page's own light back (left at Earth's Sun); - the page's dock shows the date and a Clock tab, and no date at the present (date never set, or the dock bound without the clock); - the dock offers the clock alone as a Clock tab (no tab, or a tab still named Display); - Saturn's page in 2032 opens with the camera and the Sun both south (camera held north). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import * as THREE from 'three/webgpu';
|
||||
@@ -34,7 +34,9 @@ const SATURN: BodyRecord = {
|
||||
rates: {meanMotionDegPerDay: 0.033459683702669406, longitudeOfAscendingNodeDegPerDay: -0.000006848734291581108, argumentOfPeriapsisDegPerDay: 0.000021682266940451745},
|
||||
rotationalElements: {poleRaDeg: [40.589, -0.036, 0], poleDecDeg: [83.537, -0.004, 0], primeMeridianDeg: [38.9, 810.7939024, 0]}
|
||||
};
|
||||
const BODIES = [EARTH, SATURN];
|
||||
// Eris as it is shipped for this page's purposes: no IAU model, so its page keeps its own light.
|
||||
const ERIS: BodyRecord = { ...EARTH, id: 'eris', name: 'Eris', kind: 'dwarf', radiusKm: 1163, rotationalElements: undefined };
|
||||
const BODIES = [EARTH, SATURN, ERIS];
|
||||
|
||||
/** Stands in for the WebGPU engine: a scene, a camera, and the tick hook, driven by hand. */
|
||||
class FakeEngineService {
|
||||
@@ -85,24 +87,29 @@ describe('BodyDetailSceneComponent', () => {
|
||||
let engine: FakeEngineService;
|
||||
let page: { planet: THREE.Mesh; ring?: THREE.Mesh; sunLight: THREE.DirectionalLight };
|
||||
let time: TimeStore;
|
||||
let route: BehaviorSubject<ReturnType<typeof convertToParamMap>>;
|
||||
|
||||
async function open(id: string): Promise<void> {
|
||||
let fixture: ComponentFixture<BodyDetailSceneComponent>;
|
||||
|
||||
/** Opens a body's page at a date; `whole` keeps the page's own template, dock and panel included. */
|
||||
async function open(id: string, date = '2025-06-01T12:00Z', whole = false): Promise<void> {
|
||||
engine = new FakeEngineService();
|
||||
route = new BehaviorSubject(convertToParamMap({ id }));
|
||||
TestBed.configureTestingModule({
|
||||
imports: [BodyDetailSceneComponent],
|
||||
providers: [
|
||||
{ provide: DataLoaderService, useClass: FakeDataLoaderService },
|
||||
{ provide: ActivatedRoute, useValue: { paramMap: new BehaviorSubject(convertToParamMap({ id })) } },
|
||||
{ provide: ActivatedRoute, useValue: { paramMap: route } },
|
||||
{ provide: Router, useValue: { navigate: vi.fn().mockResolvedValue(true) } }
|
||||
]
|
||||
}).overrideComponent(BodyDetailSceneComponent, {
|
||||
// The scene alone: the info panel and the dock are tested on their own.
|
||||
set: { providers: [{ provide: EngineService, useValue: engine }], imports: [], template: '<canvas #canvas></canvas>' }
|
||||
// The scene alone, unless asked: the info panel and the dock are tested on their own.
|
||||
set: whole ? { providers: [{ provide: EngineService, useValue: engine }] } : { providers: [{ provide: EngineService, useValue: engine }], imports: [], template: '<canvas #canvas></canvas>' }
|
||||
});
|
||||
time = TestBed.inject(TimeStore);
|
||||
time.setRate(0);
|
||||
time.setDate(new Date('2025-06-01T12:00Z'));
|
||||
const fixture = TestBed.createComponent(BodyDetailSceneComponent);
|
||||
time.setDate(new Date(date));
|
||||
fixture = TestBed.createComponent(BodyDetailSceneComponent);
|
||||
fixture.detectChanges();
|
||||
await flushAsync();
|
||||
page = fixture.componentInstance as unknown as typeof page;
|
||||
@@ -128,4 +135,50 @@ describe('BodyDetailSceneComponent', () => {
|
||||
expect(page.planet.quaternion.angleTo(planet)).toBeLessThan(1e-9);
|
||||
expect(page.sunLight.position.clone().normalize().angleTo(sun)).toBeLessThan(1e-9);
|
||||
});
|
||||
|
||||
it('follows the clock once the page is open, as it runs or is set', async () => {
|
||||
await open('earth');
|
||||
time.setDate(new Date('2025-06-01T18:00Z'));
|
||||
engine.tick(0.016);
|
||||
const planet = new THREE.Quaternion();
|
||||
expect(bodyPageView(EARTH, BODIES, time.julianDate(), Math.atan2(4, 5), planet, new THREE.Vector3())).toBe(true);
|
||||
// Six hours on, a quarter turn of Earth: a page frozen at its first frame is 90 degrees out.
|
||||
expect(page.planet.quaternion.angleTo(planet)).toBeLessThan(1e-9);
|
||||
});
|
||||
|
||||
it('puts the page’s own light back when the next body shown has no IAU model to place its Sun', async () => {
|
||||
await open('earth');
|
||||
expect(page.sunLight.position.distanceTo(new THREE.Vector3(4, 3, 5))).toBeGreaterThan(0.1);
|
||||
route.next(convertToParamMap({ id: 'eris' }));
|
||||
await flushAsync();
|
||||
engine.tick(0.016);
|
||||
expect(page.sunLight.position.distanceTo(new THREE.Vector3(4, 3, 5))).toBeLessThan(1e-9);
|
||||
});
|
||||
|
||||
it('says on its dock the date the body is drawn for, and nothing at the present, and offers the clock', async () => {
|
||||
await open('saturn', '2032-06-01T12:00Z', true);
|
||||
fixture.detectChanges();
|
||||
const host = fixture.nativeElement as HTMLElement;
|
||||
expect(host.querySelector('[data-testid="hud-date"]')?.textContent).toContain('2032-06-01');
|
||||
expect([...host.querySelectorAll('[role="tab"]')].map((tab) => tab.textContent?.trim())).toContain('Clock');
|
||||
time.reset();
|
||||
engine.tick(0.016);
|
||||
fixture.detectChanges();
|
||||
expect(host.querySelector('[data-testid="hud-date"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('opens Saturn on the face of its rings the Sun lights: the south, from 2025 to 2039', async () => {
|
||||
await open('saturn', '2032-06-01T12:00Z');
|
||||
const camera = engine.getCamera();
|
||||
// The Sun 26.7 degrees south of the rings, and the camera with it rather than 11 degrees north.
|
||||
expect(page.sunLight.position.y).toBeLessThan(0);
|
||||
expect(camera.position.y).toBeLessThan(0);
|
||||
|
||||
route.next(convertToParamMap({ id: 'earth' }));
|
||||
await flushAsync();
|
||||
engine.tick(0.016);
|
||||
// June: Earth's Sun is in the north, and so is the camera again.
|
||||
expect(page.sunLight.position.y).toBeGreaterThan(0);
|
||||
expect(camera.position.y).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user