Fix four defects a user hits in the first minute
Found by surveying the codebase against the plan; each was verified against the
committed assets or the running app before being touched.
TRAPPIST-1 was orbiting the Sun. The Exoplanet Archive leaves sy_dist blank for
some systems, and fetchExoplanets.ts read it with bare Number() — Number('') is
0, which is finite, so it slipped past the Number.isFinite guard in
resolveHostStarId, placed the host at the origin, and matched Sol at distance
exactly 0. 127 records shipped with hostStarId 0, all seven TRAPPIST-1 planets
among them, and the system view filters on that id, so drilling into Sol drew
127 alien worlds inside the real solar system.
Fixed in three places: resolveHostStarId now rejects a non-positive distance
(the robust guard, covering every caller), fetchExoplanets.ts uses the
parseOptionalNumber that already sat unused in that same file for ra/dec/dist,
and validateExoplanets asserts nothing ever resolves to the Sun again — the Sun
has no exoplanets, so that tripwire costs nothing and is permanent.
The archive's endpoint is blocked by this environment's egress policy, so the
ETL cannot be re-run here. The committed asset was corrected in place instead,
which is safe because the outcome is deterministic: the name path runs first and
none of the 127 resolve by name, so all of them reached id 0 positionally and
the fixed pipeline yields null for exactly that set. Cross-referenced hosts drop
from 761 to 634; record count is unchanged.
Dragging to rotate selected stars. Selection was bound to the raw click event,
which browsers fire on release however far the pointer travelled and which
OrbitControls does not suppress — so any drag ending over a star launched a
camera flight, and in system view routed away to /body/:id. Now tracks
pointerdown and ignores a release more than 5 px from it.
Ghost systems accumulated on every star-to-star hop. SystemOrbitsRenderer.dispose
released geometries and materials but never detached its group, so old orbit
lines stayed parented forever — still traversed and re-uploaded each frame with
disposed geometries, drawn over the new system and unpickable. dispose() now
detaches and clears.
Galaxy star labels stayed pinned inside the system view. They are CSS2D objects
parented to the scene rather than to galaxyGroup, so hiding the group left up to
15 parsec-space names clumped over the system's star. Cleared on entry. Also
gated the per-frame Kepler propagation on actually being in a system; it ran in
galaxy view too, because the renderer is never nulled on exit.
Tests: 116 passing, up from 112. Build, both typechecks and the Playwright suite
are green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
@@ -37,6 +37,8 @@ const DEEP_SKY_LABEL_COUNT = 12;
|
||||
const LABEL_UPDATE_INTERVAL_SECONDS = 0.2;
|
||||
/** Raycast pick tolerance around each star point, in parsecs. */
|
||||
const PICK_THRESHOLD_PC = 1.2;
|
||||
/** Pointer travel (px) above which a press counts as an orbit drag rather than a selection. */
|
||||
const CLICK_DRAG_SLOP_PX = 5;
|
||||
|
||||
const GALAXY_OVERVIEW_POSITION = new THREE.Vector3(0, 15, 30);
|
||||
const GALAXY_OVERVIEW_TARGET = new THREE.Vector3(0, 0, 0);
|
||||
@@ -117,6 +119,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
private resizeObserver?: ResizeObserver;
|
||||
private unsubscribeTick?: () => void;
|
||||
private labelUpdateAccumulator = 0;
|
||||
private pointerDownAt: { x: number; y: number } | null = null;
|
||||
private ready = false;
|
||||
private busy = false;
|
||||
|
||||
@@ -147,6 +150,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeTick?.();
|
||||
this.resizeObserver?.disconnect();
|
||||
this.canvasRef().nativeElement.removeEventListener('pointerdown', this.handlePointerDown);
|
||||
this.canvasRef().nativeElement.removeEventListener('click', this.handleClick);
|
||||
this.controls?.dispose();
|
||||
this.starField?.dispose();
|
||||
@@ -224,6 +228,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
this.labelOverlay.setSize(width, height);
|
||||
|
||||
this.raycaster.params.Points!.threshold = PICK_THRESHOLD_PC;
|
||||
canvas.addEventListener('pointerdown', this.handlePointerDown);
|
||||
canvas.addEventListener('click', this.handleClick);
|
||||
this.observeResize(canvas);
|
||||
|
||||
@@ -246,7 +251,9 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
this.systemRenderer?.update(dateToJulianDate());
|
||||
if (this.currentStarId !== null) {
|
||||
this.systemRenderer?.update(dateToJulianDate());
|
||||
}
|
||||
this.labelOverlay?.render(camera);
|
||||
}
|
||||
|
||||
@@ -271,11 +278,25 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
this.labelOverlay?.update([...starLabels, ...this.deepSkyLabels]);
|
||||
}
|
||||
|
||||
/** Where the current press started, so a drag can be told apart from a click. */
|
||||
private readonly handlePointerDown = (event: PointerEvent): void => {
|
||||
this.pointerDownAt = { x: event.clientX, y: event.clientY };
|
||||
};
|
||||
|
||||
private readonly handleClick = (event: MouseEvent): void => {
|
||||
if (this.rig?.isAnimating) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The browser fires `click` on release however far the pointer travelled, and OrbitControls
|
||||
// does not suppress it — so without this every drag-to-rotate that happens to finish over a
|
||||
// star would launch a camera flight into its system.
|
||||
const pressedAt = this.pointerDownAt;
|
||||
this.pointerDownAt = null;
|
||||
if (pressedAt && Math.hypot(event.clientX - pressedAt.x, event.clientY - pressedAt.y) > CLICK_DRAG_SLOP_PX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.canvasRef().nativeElement;
|
||||
const camera = this.engine.getCamera();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
@@ -391,6 +412,10 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
this.galaxyGroup.visible = false;
|
||||
this.systemGroup.visible = true;
|
||||
// Labels are CSS2D objects parented to the scene, not to galaxyGroup, so hiding the group
|
||||
// does not hide them: without this the galaxy-scale star names stay pinned on screen,
|
||||
// clumped over the system's star.
|
||||
this.labelOverlay?.update([]);
|
||||
|
||||
camera.near = SYSTEM_NEAR_AU;
|
||||
camera.far = SYSTEM_FAR_AU;
|
||||
|
||||
@@ -202,6 +202,12 @@ export class SystemOrbitsRenderer {
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
}
|
||||
// Detach as well as dispose. A star-to-star hop builds a new renderer and drops the old
|
||||
// one, but without this the old orbit lines and markers stay parented to the system group
|
||||
// forever — still traversed and re-uploaded every frame despite their geometries being
|
||||
// disposed, and drawn over the new system while being unpickable.
|
||||
this.object.removeFromParent();
|
||||
this.object.clear();
|
||||
}
|
||||
|
||||
private addTopLevelBody(id: string, kind: SystemMemberKind, elements: OrbitalElements, gmAu3PerDay2: number, radiusKm: number | undefined): TrackedTopLevelBody {
|
||||
|
||||
Reference in New Issue
Block a user