diff --git a/src/app/features/body-detail/body-view-model.ts b/src/app/features/body-detail/body-view-model.ts index dae4bb0..744b552 100644 --- a/src/app/features/body-detail/body-view-model.ts +++ b/src/app/features/body-detail/body-view-model.ts @@ -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 { + const temperatures = new Map(); + 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 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 199a358..a79b5e7 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 @@ -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)!; 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 1d95359..3dcf282 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -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, diff --git a/src/app/features/galaxy-system/star-field-renderer.ts b/src/app/features/galaxy-system/star-field-renderer.ts index fda38ce..860c263 100644 --- a/src/app/features/galaxy-system/star-field-renderer.ts +++ b/src/app/features/galaxy-system/star-field-renderer.ts @@ -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 ) { 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;