Tint a planet host in the star field at the archive's temperature, which its disc is drawn at

95ffb00 said the field took "the same function and the same temperature the disc is drawn with".
It did for stars without planets, but a host's disc is drawn at the archive's st_teff
(starSurfaceOf), and the field read the host's temperature off its colour. Of the 4 485 hosts with
a temperature on the published catalogue, 1 755 were more than 0.1 in RGB from their own disc:
Kepler-186 at 3 096 K in the field and 3 788 K on its disc, HD 97048 at 6 825 and 10 000.

The scene now hands StarFieldRenderer each host's first published temperature among its planets,
the rule starSurfaceOf uses (publishedTemperaturesK, beside it), and the field tints that star at
it. Over the catalogue the hosts more than 0.1 off their disc go from 1 755 to 0. Measured in the
running app at 1600x1000, field tint against the disc's starTint after entering each: Kepler-186
(1, 0.620, 0.326) against (1, 0.619, 0.326), where the field was (1, 0.498, 0.174); Teegarden's
Star 0.001 apart; Proxima, HD 97048 and Sirius 0.

The field's test compared colorIndexToRgb with the same formula. The new scene test enters Proxima,
whose B-V 1.8 reads 3 070 K and whose disc is drawn at the archive's 2 900, and compares its field
colour with the disc's. Controls, each failing that test alone: the field ignoring the map it is
given, and the scene passing an empty one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 15:48:09 +02:00
co-authored by Claude Opus 5.5
parent aafc788352
commit 17cf11b6b1
4 changed files with 46 additions and 7 deletions
@@ -50,6 +50,21 @@ export interface StarSurface {
luminosityDerived: boolean;
}
/**
* The temperature each host's disc is drawn at where the archive gives one, by star id: the first
* among its planets, in their order, as {@link starSurfaceOf} takes it — for the star field, which
* tints every star the colour of its own disc.
*/
export function publishedTemperaturesK(exoplanets: readonly ExoplanetRecord[]): Map<number, number> {
const temperatures = new Map<number, number>();
for (const { hostStarId, hostStarTemperatureK } of exoplanets) {
if (hostStarId !== null && hostStarTemperatureK && !temperatures.has(hostStarId)) {
temperatures.set(hostStarId, hostStarTemperatureK);
}
}
return temperatures;
}
/**
* A star's radius, effective temperature and luminosity: the archive's `st_rad`, `st_teff` and
* `st_lum` for a planet host, from any of its planets' rows, and otherwise derived — the
@@ -1016,6 +1016,15 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
expect(scene.hudReadouts().find((readout) => readout.label === 'Radius')?.value).toBe('0.141 solar radii');
});
it('tints a host in the star field the colour its disc is drawn in, at the temperature the archive gives it', async () => {
// Proxima's B−V 1.8 reads 3 070 K; its disc is drawn at the archive's 2 900.
const scene = await enter(PROXIMA);
const field = (scene as unknown as { starField: { catalogueColors: Float32Array } }).starField.catalogueColors;
const at = STARS.indexOf(PROXIMA) * 3;
const disc = scene.starTint.value;
expect([field[at], field[at + 1], field[at + 2]].map((channel) => channel.toFixed(4))).toEqual([disc.r, disc.g, disc.b].map((channel) => channel.toFixed(4)));
});
it("warms a host's planets by the luminosity the archive gives it, in the system as on their own page", async () => {
const scene = await enter(PROXIMA);
const planet = (scene.systemRenderer as unknown as { members: { id: string; marker: THREE.Mesh }[] }).members.find((member) => member.id === PROXIMA_B.id)!;
@@ -58,7 +58,7 @@ import {
type ScaleBar,
} from '../../shared/format/scale-bar';
import { BodyDetailViewModel } from '../body-detail/body-detail.model';
import { buildBodyViewModel, starSurfaceOf, StarSurface } from '../body-detail/body-view-model';
import { buildBodyViewModel, publishedTemperaturesK, starSurfaceOf, StarSurface } from '../body-detail/body-view-model';
import {
DEFAULT_HUD_DISPLAY,
HudDisplay,
@@ -762,6 +762,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
positions,
starRenderBudgetFromUrl(window.location.search),
this.starsByBrightness,
publishedTemperaturesK(exoplanets),
);
this.hostStars = Uint8Array.from(stars, (star) =>
this.starIdsWithBodies.has(star.id) ? 1 : 0,
@@ -101,9 +101,11 @@ const NEUTRAL_STAR_COLOR = new THREE.Color(1.0, 1.0, 1.0);
/**
* A star's tint in the field: the colour of a blackbody at its effective temperature against the
* display's white — the tint its own disc is drawn in, in its system. The temperature is the one
* the disc is drawn at (`effectiveTemperatureK`): off the dwarf sequence at the star's colour, in
* B−V or Gaia's BP−RP, off the type where there is no colour, and off the type for a giant.
* display's white — the tint its own disc is drawn in, in its system. For a star with no planets
* the temperature is the one the disc is drawn at (`effectiveTemperatureK`): off the dwarf sequence
* at the star's colour, in B−V or Gaia's BP−RP, off the type where there is no colour, and off the
* type for a giant. A host's disc is drawn at the archive's temperature instead, which the renderer
* is given apart and tints it at ({@link temperatureTint}).
*
* `colorIndex` is `null` for the ~10% of stars HYG never photometered. Those fall back to their
* `spectralType`, and to neutral white only when the catalog records no classification at all —
@@ -117,7 +119,11 @@ const NEUTRAL_STAR_COLOR = new THREE.Color(1.0, 1.0, 1.0);
*/
export function colorIndexToRgb(colorIndex: number | null, spectralType?: string, colorSystem?: 'B-V' | 'BP-RP'): THREE.Color {
// The distance is only there to say the star is not the Sun; the temperature reads none of the rest.
const temperatureK = effectiveTemperatureK({ magnitude: 0, distancePc: 1, spectralType, colorIndex, colorSystem });
return temperatureTint(effectiveTemperatureK({ magnitude: 0, distancePc: 1, spectralType, colorIndex, colorSystem }));
}
/** The field's tint at a temperature: a blackbody against the display's white, neutral with none. */
export function temperatureTint(temperatureK: number | null): THREE.Color {
if (temperatureK === null) {
return new THREE.Color().copy(NEUTRAL_STAR_COLOR);
}
@@ -291,7 +297,14 @@ export class StarFieldRenderer {
private readonly catalogue: readonly StarRecord[],
private readonly cataloguePositions: Float32Array,
budget = STAR_RENDER_BUDGET,
brightness?: BrightnessIndex
brightness?: BrightnessIndex,
/**
* The temperature the archive gives each planet host, by star id, which its disc is drawn at
* (`publishedTemperaturesK`). Tinted off its colour instead, 1 755 of the 4 485 hosts differed
* from their own disc by more than 0.1 in RGB: Kepler-186 at 3 096 K in the field and 3 788 on
* its disc, HD 97048 at 6 825 and 10 000.
*/
publishedTemperaturesK?: ReadonlyMap<number, number>
) {
this.budget = budget;
this.brightness = brightness ?? brightnessIndex(catalogue);
@@ -305,7 +318,8 @@ export class StarFieldRenderer {
this.catalogueColors = new Float32Array(catalogue.length * 3);
this.catalogueSizes = new Float32Array(catalogue.length);
catalogue.forEach((star, index) => {
const color = colorIndexToRgb(star.colorIndex, star.spectralType, star.colorSystem);
const published = publishedTemperaturesK?.get(star.id);
const color = published === undefined ? colorIndexToRgb(star.colorIndex, star.spectralType, star.colorSystem) : temperatureTint(published);
this.catalogueColors[index * 3] = color.r;
this.catalogueColors[index * 3 + 1] = color.g;
this.catalogueColors[index * 3 + 2] = color.b;