Skip to content
2 changes: 1 addition & 1 deletion src/@types/animation/FadeHandle.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* galaxy names, scale bar). Discriminator:
* `layer: LabelLayerId`.
* - overlay — always-on GPU overlay (Milky Way, procedural
* disks, textured impostors). Registered at
* disks, textured disks). Registered at
* opacity 1.0 via setImmediate. Discriminator:
* `id: OverlayId`.
* - volumesMaster — the master enable gate for the whole scalar-
Expand Down
4 changes: 2 additions & 2 deletions src/@types/animation/OverlayId.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
* - milkyWay — single-quad procedural Milky Way impostor.
* - proceduralDisks — LOD-1 procedural-disk pass (per-galaxy disk
* impostor for the close-approach band).
* - texturedImpostors — LOD-2 textured-thumbnail quad pass.
* - texturedDisks — LOD-2 textured-thumbnail quad pass.
*/
export type OverlayId = 'milkyWay' | 'proceduralDisks' | 'texturedImpostors';
export type OverlayId = 'milkyWay' | 'proceduralDisks' | 'texturedDisks';
2 changes: 1 addition & 1 deletion src/@types/engine/BuildPointInterleavedBufferInput.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type BuildPointInterleavedBufferInput = {
source: Source;
/**
* Whether to compute the per-galaxy Schechter ratios as part of this bake.
* Defaults to `'fast'` (slot 10 = 1.0). See `BuildPointInterleavedBufferMode`
* Defaults to `'fast'` (slot 9 = 1.0). See `BuildPointInterleavedBufferMode`
* for the trade-off. Optional so existing callers (and the worker
* structured-clone roundtrip) keep working without recompilation.
*/
Expand Down
6 changes: 3 additions & 3 deletions src/@types/engine/BuildPointInterleavedBufferMode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
* BuildPointInterleavedBufferMode — selector for the bake's
* Schechter-ratio strategy.
*
* - `'fast'` — slot 10 (per-vertex `schechterRatio`) is filled with 1.0,
* - `'fast'` — slot 9 (per-vertex `schechterRatio`) is filled with 1.0,
* so the shader's bias-mode branch is a no-op when None /
* VolumeLimited / V_max are selected. All three modes
* ignore slot 10 anyway, so this is correct AS LONG AS the
* ignore slot 9 anyway, so this is correct AS LONG AS the
* user hasn't picked Schechter LF. This is the default at
* upload time — the .bin lands fast (~2 s saved on a
* fully-loaded deck).
* - `'with-schechter'` — slot 10 holds the real `min(1, sqrt(nRef/n(d)))`
* - `'with-schechter'` — slot 9 holds the real `min(1, sqrt(nRef/n(d)))`
* ratio, computed via `computeSchechterRatios`.
* Used either when an upload happens *while*
* Schechter mode is already active, or as part
Expand Down
4 changes: 2 additions & 2 deletions src/@types/engine/ReadyEngineState.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { PointRenderer } from '../rendering/PointRenderer';
import type { PickRenderer } from '../rendering/PickRenderer';
import type { PostProcess } from '../rendering/PostProcess';
import type { VolumeOffscreen } from '../rendering/VolumeOffscreen';
import type { TexturedImpostorSubsystem } from './subsystems/TexturedImpostorSubsystem';
import type { TexturedDiskSubsystem } from './subsystems/TexturedDiskSubsystem';

export type ReadyEngineState = EngineState & {
cam: OrbitCamera;
Expand All @@ -36,6 +36,6 @@ export type ReadyEngineState = EngineState & {
volumeOffscreen: VolumeOffscreen;
};
subsystems: EngineState['subsystems'] & {
texturedImpostors: TexturedImpostorSubsystem;
texturedDisks: TexturedDiskSubsystem;
};
};
4 changes: 2 additions & 2 deletions src/@types/engine/frame/ReadyFrameContext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import type { Vec3 } from '../../math/Vec3';
import type { PointRenderer } from '../../rendering/PointRenderer';
import type { PostProcess } from '../../rendering/PostProcess';
import type { VolumeOffscreen } from '../../rendering/VolumeOffscreen';
import type { TexturedImpostorSubsystem } from '../subsystems/TexturedImpostorSubsystem';
import type { TexturedDiskSubsystem } from '../subsystems/TexturedDiskSubsystem';

/** The ready case: every per-frame derived value is non-null. */
export type ReadyFrameContext = {
Expand Down Expand Up @@ -85,5 +85,5 @@ export type ReadyFrameContext = {
* `ctx.volumeOffscreen.view` without reaching back into `state`.
*/
volumeOffscreen: VolumeOffscreen;
texturedImpostors: TexturedImpostorSubsystem;
texturedDisks: TexturedDiskSubsystem;
};
6 changes: 3 additions & 3 deletions src/@types/engine/handles/EngineSubsystemHandles.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* `scheduler`. None of these need a GPU device — their callbacks
* queue work that the scheduler will pick up once the IIFE finishes.
* - Lazy (inside the IIFE): `galaxyAtlas` / `proceduralDisks` /
* `texturedImpostors` (need the GPU device + the
* `texturedDisks` (need the GPU device + the
* TexturedQuadRenderer / TexturedDiskRenderer pair), `clickResolver`
* (needs the pick renderer), `inputBindings` (needs the scheduler so
* it can wake the loop on input). These start as null.
Expand All @@ -39,7 +39,7 @@

import type { GalaxyAtlasSubsystem } from '../subsystems/GalaxyAtlasSubsystem';
import type { ProceduralDiskSubsystem } from '../subsystems/ProceduralDiskSubsystem';
import type { TexturedImpostorSubsystem } from '../subsystems/TexturedImpostorSubsystem';
import type { TexturedDiskSubsystem } from '../subsystems/TexturedDiskSubsystem';
import type { SpaceMouseSubsystem } from '../subsystems/SpaceMouseSubsystem';
import type { SelectionSubsystem } from '../subsystems/SelectionSubsystem';
import type { BiasCorrectionSubsystem } from '../subsystems/BiasCorrectionSubsystem';
Expand All @@ -57,7 +57,7 @@ import type { Destroyable } from '../../rendering/Destroyable';
export type EngineSubsystemHandles = {
galaxyAtlas: GalaxyAtlasSubsystem | null;
proceduralDisks: ProceduralDiskSubsystem | null;
texturedImpostors: TexturedImpostorSubsystem | null;
texturedDisks: TexturedDiskSubsystem | null;
spaceMouse: SpaceMouseSubsystem;
tweens: TweenManager;
clickResolver: ClickResolver | null;
Expand Down
10 changes: 5 additions & 5 deletions src/@types/engine/subsystems/GalaxyAtlasSubsystem.d.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
/**
* GalaxyAtlasSubsystem — shared GPU texture atlas + bitmap-fetch queue
* for the LOD-2 (textured-impostor) galaxy path.
* for the LOD-2 (textured-disk) galaxy path.
*
* ### What this owns
*
* The 2048² LRU atlas texture, the LRU clock, the priority-queued bitmap
* fetcher, failure memoisation, and an eviction notification hook. It
* has NO direct connection to per-frame catalog walking — that lives in
* `texturedImpostorSubsystem`, which calls into this atlas to allocate
* `texturedDiskSubsystem`, which calls into this atlas to allocate
* slots and schedule fetches.
*
* ### Why a separate subsystem
*
* Pre-split, this state lived inline in `thumbnailSubsystem` alongside
* per-frame planning + render dispatch. Splitting it out gives the LOD-2
* planner (`texturedImpostorSubsystem`) one focused dependency to inject
* planner (`texturedDiskSubsystem`) one focused dependency to inject
* — and gives future code that wants to read atlas state (debug HUD,
* memory profilers) a typed surface to consume.
*
Expand Down Expand Up @@ -68,7 +68,7 @@ export type GalaxyAtlasSubsystem = Destroyable & {
isFailed(key: string): boolean;

/**
* Number of in-flight fetches. Read by the textured-impostor
* Number of in-flight fetches. Read by the textured-disk
* subsystem's `hasInFlightWork()` (which the engine's render-on-demand
* predicate ORs in).
*/
Expand All @@ -79,7 +79,7 @@ export type GalaxyAtlasSubsystem = Destroyable & {

/**
* Optional handler called when LRU evicts a slot. The
* `texturedImpostorSubsystem` subscribes to clear its bitmapReady /
* `texturedDiskSubsystem` subscribes to clear its bitmapReady /
* bitmapFailed / bitmapReadyTime entries for the ousted key.
*/
setEvictHandler(handler: ((key: string) => void) | undefined): void;
Expand Down
2 changes: 1 addition & 1 deletion src/@types/engine/subsystems/PoiSubsystem.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type PoiSubsystem = LabelProducer & {
* markers (halo + ring). Returns one descriptor per visible POI
* after applying the apparent-size fade-in band AND the
* max-apparent-radius fade-out. Famous-galaxy POIs always return
* empty (they render through the textured-impostor + label paths).
* empty (they render through the textured-disk + label paths).
*
* The producer never mutates engine state directly — the returned
* array is fed to `state.gpu.clusterMarkerRenderer.setMarkers(...)`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* TexturedImpostorSubsystem — LOD-2 per-frame planner.
* TexturedDiskSubsystem — LOD-2 per-frame planner.
*
* Walks the catalog, applies the px ≥ 24 fetch gate, allocates atlas
* slots through the injected `GalaxyAtlasSubsystem`, schedules fetches,
Expand All @@ -25,23 +25,23 @@ import type { OrbitCamera } from '../../camera/OrbitCamera';
import type { FamousMetaEntry } from '../../loading/FamousMetaEntry';
import type { Source } from '../../../data/sources';

export type TexturedImpostorFrameInput = {
export type TexturedDiskFrameInput = {
readonly cam: OrbitCamera;
readonly catalogs: ReadonlyMap<Source, GalaxyCatalog>;
readonly visibleSourceMask: number;
readonly pxPerRad: number;
readonly famousMeta: readonly FamousMetaEntry[];
};

export type TexturedImpostorFrameOutput = {
export type TexturedDiskFrameOutput = {
/** LOD-2 — galaxies with finite orientation, sorted back-to-front. */
readonly disks: readonly DiskInstance[];
};

export type TexturedImpostorSubsystem = Destroyable & {
runFrame(input: TexturedImpostorFrameInput): TexturedImpostorFrameOutput;
export type TexturedDiskSubsystem = Destroyable & {
runFrame(input: TexturedDiskFrameInput): TexturedDiskFrameOutput;

readonly lastOutput: TexturedImpostorFrameOutput;
readonly lastOutput: TexturedDiskFrameOutput;

/**
* OR'd into the engine's render-on-demand predicate. True while any
Expand All @@ -56,10 +56,10 @@ export type TexturedImpostorSubsystem = Destroyable & {
* shape the legacy thumbnailSubsystem did, so the split-out tests can
* inspect the post-extraction subsystem's bookkeeping the same way.
*/
export type TexturedImpostorTestState = {
export type TexturedDiskTestState = {
readonly bitmapReadyTime: ReadonlyMap<string, number>;
};

export type TexturedImpostorSubsystemWithTestSeam = TexturedImpostorSubsystem & {
__testGetState(): TexturedImpostorTestState;
export type TexturedDiskSubsystemWithTestSeam = TexturedDiskSubsystem & {
__testGetState(): TexturedDiskTestState;
};
8 changes: 2 additions & 6 deletions src/@types/gpu/timing/TimingSlotName.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,8 @@
* future inhabitants without forcing a query-set resize.
*
* The strings match the `name` fields on `Pass` objects (e.g.
* `pointSpritesPass.name === 'point-sprites'`). Tests in Task 9 lean
* on that equality to assert each pass plumbs its timing descriptor.
*
* The `textured-disks` slot replaces the legacy `textured-impostors`
* slot (2026-05-18 split + 2026-05-18 quad-removal). Same slot
* indices (4, 5) so historical samples stay comparable.
* `pointSpritesPass.name === 'point-sprites'`). Tests lean on that
* equality to assert each pass plumbs its timing descriptor.
*/

export type TimingSlotName =
Expand Down
6 changes: 3 additions & 3 deletions src/@types/rendering/PointRenderer.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export type PointRenderer = {
setBiasUploadCallback(cb: ((source: Source, cloud: GalaxyCatalog) => void) | null): void;
/** Install the unload-tail callback for the bias-correction subsystem. */
setBiasUnloadCallback(cb: ((source: Source) => void) | null): void;
/** Splice per-row Schechter ratios into slot 10 of the source's interleaved mirror. */
/** Splice per-row Schechter ratios into slot 9 of the source's interleaved mirror. */
spliceSchechterRatios(source: Source, ratios: Float32Array): void;
/** Splice per-row HEALPix angular weights into slot 11. */
/** Splice per-row HEALPix angular weights into slot 10. */
spliceAngularWeights(source: Source, weights: Float32Array): void;
/** Zero slots 10 + 11 for one source or every loaded source. */
/** Zero slots 9 + 10 for one source or every loaded source. */
clearBiasOverlays(source?: Source): void;
/** Total number of points across every loaded source. */
totalCount(): number;
Expand Down
62 changes: 43 additions & 19 deletions src/data/colourIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@
import { Source, type SurveySource } from './sources';
import type { ColourIndexSpec } from '../@types/data/ColourIndexSpec';

/**
* Hubble distance in Mpc — c / H₀ for H₀ = 70 km/s/Mpc. Converts
* Cartesian distance from origin to cosmological redshift via
* z = d / HUBBLE_DISTANCE_MPC, matching the small-z Hubble approximation
* the project's raDecZToCartesian uses to produce these positions.
*/
const HUBBLE_DISTANCE_MPC = 4282.749;

/**
* Ramp-position fallback for rows whose colour cannot be computed (one
* or both bands missing). 1.05 is a deliberately pale, neutral position
* on the 0..2 ramp — close enough to the middle that sentinel rows
* don't draw the eye away from real colour gradients. Both the points
* bake and the procedural-disk subsystem substitute this value, so a
* galaxy without bands renders the same hue in both renderers.
*/
export const UNKNOWN_COLOUR_RAMP_POSITION = 1.05;

// POI source codes (Cluster, Supercluster, Void) have no photometry —
// they're pick-encoding-only markers, not survey rows. The colour-index
// spec is therefore keyed by `SurveySource` (excludes POIs), and the
Expand All @@ -33,9 +51,16 @@ const SPEC: Record<SurveySource, ColourIndexSpec> = {
};

/**
* Look up which slot maps to which mag value, then compute the source-
* appropriate colour index and K coefficient. Returns null when either
* constituent band is NaN (so the caller knows to use the sentinel path).
* Look up which slot maps to which mag value, compute the source-
* appropriate colour index, K-correct it to rest-frame using the row's
* distance, and normalise to the 0..2 ramp range. Returns
* {@link UNKNOWN_COLOUR_RAMP_POSITION} when either constituent band is
* NaN, so callers never need a null-check or fallback constant.
*
* The K-correction is applied here (CPU side) so both consumers
* (`buildPointInterleavedBuffer` and `proceduralDiskSubsystem`) get the
* same rest-frame value, removing a visible hue mismatch between the
* points pass and the procedural-disk impostor for the same galaxy.
*/
export function pickColourIndex(
source: Source,
Expand All @@ -44,31 +69,30 @@ export function pickColourIndex(
magR: number,
magI: number,
magZ: number,
): { colourIndex: number; kPerZ: number } | null {
dMpcFromOrigin: number,
): number {
if (source === Source.Cluster || source === Source.Supercluster || source === Source.Void) {
throw new Error(`pickColourIndex: POI source ${source} has no colour index`);
}
const spec = SPEC[source as SurveySource];
const slotMap = { u: magU, g: magG, r: magR, i: magI, z: magZ };
const a = slotMap[spec.slotA];
const b = slotMap[spec.slotB];
if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
if (!Number.isFinite(a) || !Number.isFinite(b)) return UNKNOWN_COLOUR_RAMP_POSITION;

// ── Normalise raw colour to the 0..2 ramp range ─────────────────────────
//
// The WGSL ramp expects its input in [0, 2]. We pre-bake the linear
// remap here so the shader doesn't need to know any per-source range
// numbers — it just reads a single f32 and indexes the ramp.
//
// kPerZ is passed through unchanged: it's already specified in
// normalised ramp-position units (see the SPEC type's docstring for
// why the literature mag/z values aren't used directly).
// Normalise raw observed colour to the 0..2 ramp range. The WGSL ramp
// expects its input there; pre-baking the linear remap means the
// shader reads a single f32 and indexes the ramp.
const raw = a - b;
const colourIndex = Math.max(
0,
Math.min(2, ((raw - spec.rangeMin) / (spec.rangeMax - spec.rangeMin)) * 2.0),
);
return { colourIndex, kPerZ: spec.kPerZ };
const observedCI = ((raw - spec.rangeMin) / (spec.rangeMax - spec.rangeMin)) * 2.0;

// K-correction: colour_rest ≈ colour_obs − k · z. kPerZ is already in
// normalised ramp-position units, so the subtraction happens directly
// on the ramp coordinate. Re-clamp because the correction can push
// the value outside [0, 2].
const z = dMpcFromOrigin / HUBBLE_DISTANCE_MPC;
const restCI = observedCI - spec.kPerZ * z;
return Math.max(0, Math.min(2, restCI));
}

/** Public read of the spec table — used by `galaxyType.ts` and tests. */
Expand Down
Loading
Loading