From 3e1bd33b6c331dbab6e4f69f84f47fb21a8b6b4b Mon Sep 17 00:00:00 2001 From: Senrokai Date: Wed, 30 Sep 2026 15:14:02 +0200 Subject: [PATCH] Draw every body in a system on one shared sphere, so returning to the Sun's system is no long task Each marker built its own 64 by 32 SphereGeometry, and the Sun's system now has 38 of them: the renderer's constructor took 15 ms, 12 of them building spheres, and with their first upload a return to the system made a long task of 52 to 70 ms that the base's 18 bodies never did. Every marker is now the one unit sphere, scaled to its radius, which it keeps in userData.radiusAu. The shared sphere is never disposed; Saturn's ring is built in the sphere's own units, since it is the marker's child; keepMarkersLegible reads the stored radius and scales against the sphere's. Measured on :4301, eight returns to the Sun's system each (select null, then 0, at 1600x1000): before, a long task on 3 of 8 (52-57 ms), swapToSystemSpace 13-17 ms and the first render 30-40; after, no long task on 8 of 8, the swap 2.7-4.4 ms and the first render 20-38. Earth is drawn at the same 0.656 AU at arrival, and every member shares one geometry. Tests: one sphere for every marker, each at bodyMarkerRadiusAu of its radius, and not disposed with its system; Earth held to its 3-pixel floor at the arrival framing, which no test covered. Guarded mutants, each failing only its named test: a sphere per marker, the shared sphere disposed, the ring built in AU inside the scaled marker ('picks Saturn through its rings'), and the legibility scale divided by the body's radius. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../galaxy-system-scene.component.spec.ts | 8 ++++++ .../galaxy-system-scene.component.ts | 8 ++++-- .../system-orbits-renderer.spec.ts | 26 +++++++++++++++++-- .../galaxy-system/system-orbits-renderer.ts | 21 ++++++++++++--- 4 files changed, 55 insertions(+), 8 deletions(-) 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