From 468c98b14a7387e9a40a22693afc46b18a0a285e Mon Sep 17 00:00:00 2001 From: Senrokai Date: Tue, 22 Sep 2026 12:03:24 +0200 Subject: [PATCH] Surface the system view with the photographs it already had, lit by its star MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every body in the system view was an unlit sphere wearing a 32 by 16 pixel procedural texture — the size chosen when a marker was a few pixels across and what survived was its average colour. The thirteen real photographs in `src/assets/textures/bodies/` were used only by the detail page. So Mars was a pale grey ball with invented polar caps while its own NASA mosaic sat unread in the repository, and nothing had a day side or a night side. Each marker now takes its own photograph where one exists, at the size the detail page uses, and the derived texture only where none does — the five moons no probe mapped, and every exoplanet, none of which has ever been imaged. The material is lit, and the light is a point at the star, so each world shows the terminator where it really falls. The light does not fall off with distance. Under the inverse square that real light obeys, Neptune receives a thousandth of what Mercury does and reads as black; the map is a set of worlds to look at rather than a light meter, so each is lit as a photograph of it would be. That is the same concession the pixel floor makes for size, and it is only about brightness: the *direction* is real. Spheres are 32 by 24 rather than 16 by 12, since at true scale a body is drawn anywhere from a pixel to the whole frame and the old silhouette was visibly faceted at the near end. Co-Authored-By: Claude Opus 5 (1M context) --- .../galaxy-system-scene.component.ts | 690 ++++++++++++++---- .../galaxy-system/system-orbits-renderer.ts | 55 +- src/app/features/hud/hud-dock.component.ts | 222 +++++- src/app/shared/state/time.store.ts | 2 +- 4 files changed, 754 insertions(+), 215 deletions(-) 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 47d85fb..4b45aae 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -1,4 +1,13 @@ -import { AfterViewInit, Component, computed, effect, ElementRef, OnDestroy, signal, viewChild } from '@angular/core'; +import { + AfterViewInit, + Component, + computed, + effect, + ElementRef, + OnDestroy, + signal, + viewChild, +} from '@angular/core'; import { Router } from '@angular/router'; import * as THREE from 'three/webgpu'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; @@ -8,7 +17,7 @@ import { MILKY_WAY_ARMS, SUN_GALACTOCENTRIC_RADIUS_PC, galacticCentrePositionPc, - galacticToEquatorial + galacticToEquatorial, } from '../../shared/astro/galaxy'; import { DataLoaderService } from '../../core/data/data-loader.service'; import { EngineService, SceneCamera } from '../../core/engine/engine.service'; @@ -16,7 +25,11 @@ import { BodyRecord } from '../../shared/models/body.model'; import { DeepSkyRecord } from '../../shared/models/deepsky.model'; import { ExoplanetRecord } from '../../shared/models/exoplanet.model'; import { applyMilkyWaySkybox } from '../../shared/rendering/skybox'; -import { loadCachedTexture, MILKY_WAY_SKYBOX_PATH, SUN_TEXTURE_PATH } from '../../shared/rendering/texture-catalog'; +import { + loadCachedTexture, + MILKY_WAY_SKYBOX_PATH, + SUN_TEXTURE_PATH, +} from '../../shared/rendering/texture-catalog'; import { isDesignation } from '../../shared/models/star-catalog'; import { StarRecord } from '../../shared/models/star.model'; import { Bookmark } from '../../shared/state/bookmarks.store'; @@ -26,18 +39,41 @@ import { CameraRigController } from './camera-rig-controller'; import { DeepSkyRenderer } from './deep-sky-renderer'; import { galacticNormal, PolarGridPlane, TetherField } from './grid-plane'; import { MilkyWayRenderer } from './milky-way-renderer'; -import { starMarkerRadiusAu, SUN_RADIUS_AU, systemFrameRadiusAu, systemFramingDistanceAu, systemViewDirection } from './system-framing'; +import { + starMarkerRadiusAu, + SUN_RADIUS_AU, + systemFrameRadiusAu, + systemFramingDistanceAu, + systemViewDirection, +} from './system-framing'; import { formatAu, formatLuminosity, formatParsecs } from '../../shared/format/quantity'; -import { distanceRings, formatRoundLength, scaleBar, type LengthUnit, type ScaleBar } from '../../shared/format/scale-bar'; +import { + distanceRings, + formatRoundLength, + scaleBar, + type LengthUnit, + type ScaleBar, +} from '../../shared/format/scale-bar'; import { BodyDetailViewModel } from '../body-detail/body-detail.model'; import { buildBodyViewModel, luminosityOf } from '../body-detail/body-view-model'; -import { DEFAULT_HUD_DISPLAY, HudDisplay, HudDockComponent, HudReadout } from '../hud/hud-dock.component'; +import { + DEFAULT_HUD_DISPLAY, + HudDisplay, + HudDockComponent, + HudReadout, +} from '../hud/hud-dock.component'; import { RouteRequest, RouteResult, RouteStarOption } from '../hud/routes-panel.component'; import { buildSearchIndex, IndexedSearchEntry, rankSearchResults } from '../search/search-ranking'; import { StarmapHudComponent } from './starmap-hud.component'; import { SystemObjectCardComponent } from './system-object-card.component'; import { RoutingClient } from './routing-client'; -import { colorIndexToRgb, FOCUS_RADIUS_PC, StarFieldRenderer, starRenderBudgetFromUrl, VIEW_MARGIN } from './star-field-renderer'; +import { + colorIndexToRgb, + FOCUS_RADIUS_PC, + StarFieldRenderer, + starRenderBudgetFromUrl, + VIEW_MARGIN, +} from './star-field-renderer'; import { BrightnessIndex, brightestWithin, brightnessIndex } from '../../shared/astro/brightest'; import { LinkBudget } from '../../shared/astro/jump-links'; import { StarNeighbourhood } from '../../shared/astro/star-neighbourhood'; @@ -99,8 +135,14 @@ function servesTheSame(asked: LinkBudget | undefined, now: LinkBudget | undefine if (!asked || !now) { return asked === now; } - const moved = Math.hypot(now.centre.x - asked.centre.x, now.centre.y - asked.centre.y, now.centre.z - asked.centre.z); - return Math.abs(now.lengthPc / asked.lengthPc - 1) <= VIEW_MARGIN / 2 && moved <= STAR_FIELD_REFOCUS_PC; + const moved = Math.hypot( + now.centre.x - asked.centre.x, + now.centre.y - asked.centre.y, + now.centre.z - asked.centre.z, + ); + return ( + Math.abs(now.lengthPc / asked.lengthPc - 1) <= VIEW_MARGIN / 2 && moved <= STAR_FIELD_REFOCUS_PC + ); } /** @@ -263,7 +305,10 @@ function galacticOverviewPose(): { position: THREE.Vector3; target: THREE.Vector const centre = galacticCentrePositionPc(); const target = new THREE.Vector3(centre.x, centre.y, centre.z); const awayFromCentre = target.clone().negate().normalize(); - const position = target.clone().add(galacticNormal().multiplyScalar(GALACTIC_OVERVIEW_HEIGHT_PC)).add(awayFromCentre.multiplyScalar(GALACTIC_OVERVIEW_BACK_PC)); + const position = target + .clone() + .add(galacticNormal().multiplyScalar(GALACTIC_OVERVIEW_HEIGHT_PC)) + .add(awayFromCentre.multiplyScalar(GALACTIC_OVERVIEW_BACK_PC)); return { position, target }; } @@ -290,12 +335,33 @@ function galacticOverviewPose(): { position: THREE.Vector3; target: THREE.Vector
-

{{ eyebrow() }}

-

{{ title() }}

+

+ {{ title() }} +

@if (keepableStarId() !== null) { + type="button" + [attr.aria-label]=" + (bookmarks.has('star', keepableStarId()!) ? 'Forget ' : 'Keep ') + title() + " + [attr.aria-pressed]="bookmarks.has('star', keepableStarId()!)" + (click)=" + bookmarks.toggle({ kind: 'star', id: keepableStarId()!, name: title() }) + " + class="shrink-0 p-1 transition-colors focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-accent" + [class]=" + bookmarks.has('star', keepableStarId()!) + ? 'text-accent' + : 'text-muted hover:text-accent' + " + > + + }
@if (subtitle()) { @@ -118,30 +182,54 @@ function isWideViewport(): boolean {
@for (readout of readouts(); track readout.label) {
-
{{ readout.label }}@if (readout.derived) {}
+
+ {{ readout.label }} + @if (readout.derived) { + + } +
{{ readout.value }}
}
} @if (note() || hasDerived()) { -

@if (hasDerived()) {* Derived, not catalogued. }{{ note() }}

+

+ @if (hasDerived()) { + * Derived, not catalogued. + } + {{ note() }} +

} } @case ('bookmarks') { -
+
@if (bookmarks.bookmarks().length) {
    - @for (bookmark of bookmarks.bookmarks(); track bookmark.kind + ':' + bookmark.id) { + @for ( + bookmark of bookmarks.bookmarks(); + track bookmark.kind + ':' + bookmark.id + ) {
  • @@ -158,13 +254,20 @@ function isWideViewport(): boolean {
} @else {

- Nothing kept yet. The on a readout or a body keeps it here, in this browser. + Nothing kept yet. The + on a + readout or a body keeps it here, in this browser.

}
} @case ('display') { -
+

Layers

@for (layer of layers; track layer.key) { @@ -173,10 +276,18 @@ function isWideViewport(): boolean { [attr.aria-pressed]="isOn(layer.key)" (click)="toggleLayer(layer.key)" class="type-label flex items-center gap-2 border px-3 py-1.5 transition-colors focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-accent" - [class]="isOn(layer.key) ? 'border-accent/60 bg-accent/12 text-accent hover:bg-accent/18' : 'border-border/60 text-muted hover:border-border hover:text-text'" + [class]=" + isOn(layer.key) + ? 'border-accent/60 bg-accent/12 text-accent hover:bg-accent/18' + : 'border-border/60 text-muted hover:border-border hover:text-text' + " > - + {{ layer.label }} } @@ -187,11 +298,19 @@ function isWideViewport(): boolean {

Clock

-
+
@for (rate of timeRates; track rate.secondsPerSecond) {
- ` + `, }) export class HudDockComponent implements OnInit { /** Readout panel contents. An empty title means there is nothing to read out, and no tab for it. */ @@ -315,7 +450,7 @@ export class HudDockComponent implements OnInit { // Always offered, even with nothing in it: it is the only place that says the map can keep // anything at all, and a tab that appears once you already know is a tab that never taught. 'bookmarks', - ...(this.display() ? (['display'] as const) : []) + ...(this.display() ? (['display'] as const) : []), ]); readonly activeTab = signal(null); @@ -377,7 +512,10 @@ export class HudDockComponent implements OnInit { return; } const target = event.target as HTMLElement | null; - if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) { + if ( + target && + (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) + ) { return; } event.preventDefault(); @@ -389,7 +527,11 @@ export class HudDockComponent implements OnInit { /** On a narrow viewport the panel is a sheet over the scene: tapping the scene folds it away. */ @HostListener('document:pointerdown', ['$event']) onDocumentPointerDown(event: PointerEvent): void { - if (this.activeTab() && !isWideViewport() && !this.host.nativeElement.contains(event.target as Node)) { + if ( + this.activeTab() && + !isWideViewport() && + !this.host.nativeElement.contains(event.target as Node) + ) { this.activeTab.set(null); } } diff --git a/src/app/shared/state/time.store.ts b/src/app/shared/state/time.store.ts index d911b2a..3be5656 100644 --- a/src/app/shared/state/time.store.ts +++ b/src/app/shared/state/time.store.ts @@ -19,7 +19,7 @@ export const TIME_RATES = [ { label: 'Real time', secondsPerSecond: 1 }, { label: '1 h/s', secondsPerSecond: 3600 }, { label: '1 d/s', secondsPerSecond: 86_400 }, - { label: '1 mo/s', secondsPerSecond: 2_629_800 } + { label: '1 mo/s', secondsPerSecond: 2_629_800 }, ] as const; const MS_PER_DAY = 86_400_000;