feat: catálogo de estrelas embutido (Yale BSC, V<=5.5, 2887 estrelas)

tools/build_catalog.py baixa o bsc5-short.json, converte RA/Dec/V/K e
emite starcatalog.js compacto (65KB) com paleta de cores por
temperatura (corpo negro dessaturado).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HtPHsTnDcLVSCiF5FGHZj
This commit is contained in:
vini 2026-07-19 10:52:05 -03:00
parent 0caf1fbbf8
commit 998de4a945
2 changed files with 3010 additions and 0 deletions

117
tools/build_catalog.py Normal file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Gera plugin/contents/ui/starcatalog.js a partir do Yale Bright Star Catalog.
Baixa bsc5-short.json (RA/Dec/V/K), filtra V <= MAG_LIMIT, converte a
temperatura de cor em RGB (aprox. corpo negro, quantizada numa paleta pequena)
e emite um JS compacto:
const STAR_PALETTE = [[r,g,b], ...];
const STAR_DATA = [[raDeg, decDeg, vmag, colorIdx], ...]; // ordenado por mag
Uso: python3 tools/build_catalog.py
"""
import json
import math
import re
import urllib.request
from pathlib import Path
URL = "https://raw.githubusercontent.com/brettonw/YaleBrightStarCatalog/master/bsc5-short.json"
OUT = Path(__file__).resolve().parent.parent / "plugin/contents/ui/starcatalog.js"
MAG_LIMIT = 5.5
# Limites (K) e centro representativo de cada faixa de temperatura
TEMP_BINS = [
(0, 3500, 3000), # M — alaranjado-avermelhado
(3500, 4500, 4000), # K — laranja
(4500, 5500, 5000), # G — amarelo-branco
(5500, 6500, 6000), # F — branco-amarelado
(6500, 8000, 7200), # A — branco
(8000, 11000, 9500), # B tardio — branco-azulado
(11000, 16000, 13000), # B — azul-branco
(16000, 10**9, 20000), # O/B quente — azul
]
SATURATION = 0.6 # estrelas reais parecem quase brancas; 1.0 = cor plena
def blackbody_rgb(kelvin: float) -> tuple[int, int, int]:
"""Aproximação de Tanner Helland (válida ~1000K-40000K)."""
t = kelvin / 100.0
if t <= 66:
r = 255.0
g = 99.4708025861 * math.log(t) - 161.1195681661
b = 0.0 if t <= 19 else 138.5177312231 * math.log(t - 10) - 305.0447927307
else:
r = 329.698727446 * (t - 60) ** -0.1332047592
g = 288.1221695283 * (t - 60) ** -0.0755148492
b = 255.0
def clamp(x):
return max(0, min(255, round(x)))
return clamp(r), clamp(g), clamp(b)
def desaturate(rgb, amount):
r, g, b = rgb
lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
mix = lambda c: round(lum + (c - lum) * amount)
return mix(r), mix(g), mix(b)
def parse_ra(s: str) -> float:
m = re.match(r"(\d+)h\s*(\d+)m\s*([\d.]+)s", s)
h, mi, se = float(m.group(1)), float(m.group(2)), float(m.group(3))
return (h + mi / 60 + se / 3600) * 15.0
def parse_dec(s: str) -> float:
m = re.match(r"([+-])(\d+)°\s*(\d+)\s*([\d.]+)″", s)
sign = -1.0 if m.group(1) == "-" else 1.0
d, mi, se = float(m.group(2)), float(m.group(3)), float(m.group(4))
return sign * (d + mi / 60 + se / 3600)
def temp_index(kelvin: float) -> int:
for i, (lo, hi, _) in enumerate(TEMP_BINS):
if lo <= kelvin < hi:
return i
return len(TEMP_BINS) - 1
def main():
raw = json.loads(urllib.request.urlopen(URL, timeout=30).read())
stars = []
for entry in raw:
if "V" not in entry or "RA" not in entry or "Dec" not in entry:
continue
vmag = float(entry["V"])
if vmag > MAG_LIMIT:
continue
kelvin = float(entry.get("K", 6000))
stars.append((
round(parse_ra(entry["RA"]), 2),
round(parse_dec(entry["Dec"]), 2),
round(vmag, 2),
temp_index(kelvin),
))
stars.sort(key=lambda s: s[2]) # mais brilhantes primeiro
palette = [desaturate(blackbody_rgb(center), SATURATION)
for _, _, center in TEMP_BINS]
rows = ",\n".join(
f"[{ra},{dec},{mag},{ci}]" for ra, dec, mag, ci in stars
)
OUT.write_text(
"/* Gerado por tools/build_catalog.py — NÃO editar à mão.\n"
f" Yale Bright Star Catalog, V <= {MAG_LIMIT}, {len(stars)} estrelas.\n"
" Formato: [raGraus, decGraus, magV, idxPaleta] ordenado por magnitude. */\n"
f"const STAR_PALETTE = {json.dumps([list(p) for p in palette])};\n"
f"const STAR_DATA = [\n{rows}\n];\n"
)
print(f"{len(stars)} estrelas -> {OUT} ({OUT.stat().st_size / 1024:.0f} KB)")
if __name__ == "__main__":
main()