diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts index 766c8b5..a88b48f 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts @@ -908,6 +908,14 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => { ); }); + it('holds Earth to its 3-pixel floor at the arrival framing, where its true radius is far under a pixel', async () => { + Object.defineProperty((fixture.nativeElement as HTMLElement).querySelector('canvas')!, 'clientHeight', { value: 1000 }); + const component = await enterTheSun(1.6); + const earth = component.systemRenderer.members.find((member) => member.id === 'earth')!.marker as THREE.Mesh; + const pixelAu = (2 * engine.visibleHalfHeight(engine.getCamera().position.distanceTo(earth.getWorldPosition(new THREE.Vector3())))) / 1000; + expect((earth.scale.x * (earth.geometry as THREE.SphereGeometry).parameters.radius) / pixelAu).toBeCloseTo(3, 3); + }); + 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(); 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 2b08ca8..19ca04c 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -861,8 +861,12 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { } const world = new THREE.Vector3(); const drawnRadiusAu = new Map(); - const radiusOf = (marker: THREE.Object3D): number | undefined => + // A body's marker is a unit sphere scaled to its radius, kept in `userData.radiusAu`; the + // star's is built at its own. + const sphereRadius = (marker: THREE.Object3D): number | undefined => ((marker as THREE.Mesh).geometry as THREE.SphereGeometry | undefined)?.parameters?.radius; + const radiusOf = (marker: THREE.Object3D): number | undefined => + (marker.userData['radiusAu'] as number | undefined) ?? sphereRadius(marker); const floorFor = (marker: THREE.Object3D): number => { marker.getWorldPosition(world); return ( @@ -891,7 +895,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { : Number.POSITIVE_INFINITY; const drawn = Math.min(Math.max(radiusAu, floorFor(marker)), Math.max(radiusAu, ceiling)); drawnRadiusAu.set(id, drawn); - marker.scale.setScalar(drawn / radiusAu); + marker.scale.setScalar(drawn / sphereRadius(marker)!); } } 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 ae69d21..348af9b 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.spec.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.spec.ts @@ -9,6 +9,7 @@ import { BodyRecord, RotationalElements } from '../../shared/models/body.model'; import { ExoplanetRecord } from '../../shared/models/exoplanet.model'; import { SystemOrbitsRenderer } from './system-orbits-renderer'; import { bodyTexturePath, loadCachedTexture } from '../../shared/rendering/texture-catalog'; +import { bodyMarkerRadiusAu } from './system-framing'; /** The clock's UT date that names a TDB one: TT - UT, which moves by under a second a year, earlier. */ const utOf = (jdTdb: number): number => jdTdb - ttMinusUtSeconds(jdTdb) / 86400; @@ -512,6 +513,27 @@ describe('photographs', () => { }); }); +describe('markers', () => { + it('draws every body on the one sphere, scaled to its radius, and leaves that sphere when a system is left', () => { + const records: BodyRecord[] = [2500, 60000].map((radiusKm, index) => ({ + id: `body-${index}`, systemStarId: 0, name: `Body ${index}`, kind: 'planet', radiusKm, orbitSource: 'test', + orbit: { semiMajorAxisAu: 1 + index, eccentricity: 0, inclinationDeg: 0, longitudeOfAscendingNodeDeg: 0, argumentOfPeriapsisDeg: 0, meanAnomalyAtEpochDeg: 0, epochJd: DEFAULT_EPOCH_JD }, + rates: keplerRates(1 + index, GM_SUN_AU3_PER_DAY2) + })); + const renderer = new SystemOrbitsRenderer(records, [exoplanet({ radiusEarth: 1.1 })], undefined, 1); + const meshes = renderer.members.map((member) => member.marker as THREE.Mesh); + expect(new Set(meshes.map((mesh) => mesh.geometry)).size).toBe(1); + [2500, 60000, 1.1 * 6371].forEach((radiusKm, index) => { + const sphere = meshes[index].geometry as THREE.SphereGeometry; + expect(meshes[index].scale.x * sphere.parameters.radius).toBeCloseTo(bodyMarkerRadiusAu(radiusKm), 12); + }); + const disposed = vi.fn(); + meshes[0].geometry.addEventListener('dispose', disposed); + renderer.dispose(); + expect(disposed).not.toHaveBeenCalled(); + }); +}); + describe('derived surfaces', () => { const maps = (renderer: SystemOrbitsRenderer): Array => renderer.members.map((member) => ((member.marker as THREE.Mesh).material as THREE.MeshStandardMaterial).map); @@ -553,7 +575,7 @@ describe('derived surfaces', () => { describe('exoplanet size without a measured radius', () => { const radiusOf = (overrides: Partial): number => { const renderer = new SystemOrbitsRenderer([], [exoplanet(overrides)], undefined, 1); - return ((renderer.members[0].marker as THREE.Mesh).geometry as THREE.SphereGeometry).parameters.radius; + return renderer.members[0].marker.userData['radiusAu']; }; const EARTH_AU = 6371 / 149597870.7; @@ -769,7 +791,7 @@ describe('solar-system bodies against Horizons', () => { const marker = renderer.members.find((member) => member.id === id)!.marker as THREE.Mesh; const centre = worldPosition(id); const towards = point.clone().sub(centre).normalize(); - const radius = (marker.geometry as THREE.SphereGeometry).parameters.radius; + const radius = marker.userData['radiusAu']; const hit = new THREE.Raycaster(centre.clone().addScaledVector(towards, radius * 4), towards.clone().negate()).intersectObject(marker)[0]; return { eastDeg: (hit.uv!.x - 0.5) * 360, latDeg: (hit.uv!.y - 0.5) * 180 }; } diff --git a/src/app/features/galaxy-system/system-orbits-renderer.ts b/src/app/features/galaxy-system/system-orbits-renderer.ts index 08b8041..4d4084e 100644 --- a/src/app/features/galaxy-system/system-orbits-renderer.ts +++ b/src/app/features/galaxy-system/system-orbits-renderer.ts @@ -205,6 +205,11 @@ function reshapeOrbitLine(line: THREE.Line, elements: OrbitalElements): void { * frame: a texture is copied to the GPU in the first frame that draws it, and the 28 maps, which * arrive within 40 ms of each other, made that one frame a 160-210 ms task on entering the Sun's * system (copyExternalImageToTexture, about 20 megapixels of JPEG). + * + * Every marker is the one unit sphere, {@link MARKER_SPHERE}, scaled to the body's radius, which + * it also keeps as `userData.radiusAu`: built one a body, the 38 spheres of the Sun's system took + * 12 ms of the 15 ms the renderer took to build and, with their upload, made a return to the + * system a long task of 52 to 70 ms, where the 18 bodies before had made none. */ function buildMarker( id: string | undefined, @@ -214,7 +219,6 @@ function buildMarker( deferSurface: (paint: () => void) => void, deferPhotograph: (material: THREE.MeshStandardMaterial, texture: THREE.Texture) => void ): THREE.Mesh { - const geometry = new THREE.SphereGeometry(bodyMarkerRadiusAu(radiusKm), MARKER_WIDTH_SEGMENTS, MARKER_HEIGHT_SEGMENTS); const photograph = id ? bodyTexturePath(id) : undefined; // null, not undefined, until there is one: three warns "parameter 'map' has value of // undefined" for every body built so, eleven of them on entering the Sun's system. @@ -235,7 +239,11 @@ function buildMarker( material.needsUpdate = true; }); } - return new THREE.Mesh(geometry, material); + const marker = new THREE.Mesh(MARKER_SPHERE, material); + const radiusAu = bodyMarkerRadiusAu(radiusKm); + marker.scale.setScalar(radiusAu); + marker.userData = { radiusAu }; + return marker; } /** @@ -266,6 +274,8 @@ function starLight(): THREE.PointLight { */ const MARKER_WIDTH_SEGMENTS = 64; const MARKER_HEIGHT_SEGMENTS = 32; +/** Shared by every marker of every system, so it is never disposed; see `buildMarker`. */ +const MARKER_SPHERE = new THREE.SphereGeometry(1, MARKER_WIDTH_SEGMENTS, MARKER_HEIGHT_SEGMENTS); /** * A drawn radius, in Earth radii, for an exoplanet that has a mass and no measured radius — 1 076 @@ -434,7 +444,8 @@ export class SystemOrbitsRenderer { // A child of the sphere, so it lies in the equator the IAU pole turns the sphere into and // is scaled with it where the marker is held to its pixel floor. Jupiter's, Uranus's and // Neptune's rings are left out: dark, narrow or dusty, they are too faint to see here. - const ring = saturnRing(body.radiusKm, bodyMarkerRadiusAu(body.radiusKm)); + // In the sphere's own units, its radius being 1. + const ring = saturnRing(body.radiusKm, 1); tracked.marker.add(ring); this.trackDisposable(ring.geometry, ring.material as THREE.Material); } @@ -613,7 +624,9 @@ export class SystemOrbitsRenderer { this.grid?.dispose(); this.tethers?.dispose(); for (const { geometry, material } of this.disposables) { - geometry.dispose(); + if (geometry !== MARKER_SPHERE) { + geometry.dispose(); + } material.dispose(); } // Detach as well as dispose. A star-to-star hop builds a new renderer and drops the old