src/data/planets.js

// ————————————————————————————————————————————————
// Planetary data — the single source of truth.
//
// All values are REAL astronomical quantities (NASA planetary fact sheets,
// orbital elements from the JPL approximate-elements table — fit valid
// AD 1800–2050 to about an arcminute, multi-century to a few arcminutes).
//   - radiusKm:         mean equatorial radius, km
//   - orbitKm:          semi-major axis of orbit around the Sun, km
//                       (omitted on the Sun itself)
//   - orbitalPeriodDays: sidereal orbital period, Earth days
//                       (omitted on the Sun itself)
//   - axialTiltRad:     obliquity (tilt of rotation axis relative to orbit normal)
//   - color:            CSS/Three.js hex (approximate, for stylized rendering)
//   - isStar:           true on the Sun only
//
// Kepler orbital elements at J2000.0 (JD 2451545.0):
//   - e:                 eccentricity
//   - iRad:              inclination to the ecliptic (radians)
//   - ascNodeRad:        Ω, longitude of ascending node (radians)
//   - argPerihelionRad:  ω, argument of perihelion (radians)
//                        For Earth (i ≈ 0) Ω = 0 by convention and ω
//                        carries the longitude of perihelion ϖ.
//   - meanLongitudeJ2000Rad: L₀, mean longitude at J2000.0 (radians)
//
// Linear secular rates (per Julian century, 36525 days), same units as
// above. Per the JPL approximate-elements table; semi-major-axis drift
// is intentionally not stored (Mercury's a changes ≈ 55 km per Julian
// century, far below this project's display scaling).
//   - eRate, iRadRate, ascNodeRadRate, argPerihelionRadRate,
//     meanLongitudeRadRate
//
// Each entry has:
//   - id:    stable, lowercase, language-independent identifier used as a key
//            in view state, preset dropdowns, and lookup tables.
//   - names: localized display names, keyed by SUPPORTED_LANGS from src/i18n.js.
//            Extend by adding new language keys — no schema change needed.
//
// Views apply their own display scaling on top of these real values — see
// docs/scales.md for each view's scaling rationale.
// ————————————————————————————————————————————————

const D2R = Math.PI / 180;

// Deriving ω = ϖ − Ω (and similarly for rates) from the JPL form, kept as
// inline arithmetic so the JPL-table values are visible as comments.
const _mercury = {
  e: 0.20563593,
  iRad: 7.00497902 * D2R,
  ascNodeRad: 48.33076593 * D2R, // Ω
  argPerihelionRad: (77.45779628 - 48.33076593) * D2R, // ω = ϖ − Ω = 29.127°
  meanLongitudeJ2000Rad: 252.2503235 * D2R, // L₀
  eRate: 0.00001906,
  iRadRate: -0.00594749 * D2R,
  ascNodeRadRate: -0.12534081 * D2R,
  argPerihelionRadRate: (0.16047689 - -0.12534081) * D2R, // dω/dt = dϖ/dt − dΩ/dt
  meanLongitudeRadRate: 149472.67411175 * D2R,
};
const _venus = {
  e: 0.00677672,
  iRad: 3.39467605 * D2R,
  ascNodeRad: 76.67984255 * D2R,
  argPerihelionRad: (131.60246718 - 76.67984255) * D2R, // ω = 54.923°
  meanLongitudeJ2000Rad: 181.9790995 * D2R,
  eRate: -0.00004107,
  iRadRate: -0.0007889 * D2R,
  ascNodeRadRate: -0.27769418 * D2R,
  argPerihelionRadRate: (0.00268329 - -0.27769418) * D2R,
  meanLongitudeRadRate: 58517.81538729 * D2R,
};
const _earth = {
  e: 0.01671123,
  iRad: -0.00001531 * D2R, // ≈ 0 (JPL fit residual)
  ascNodeRad: 0, // undefined at i ≈ 0 → 0
  argPerihelionRad: 102.93768193 * D2R, // ϖ acts as ω here
  meanLongitudeJ2000Rad: 100.46457166 * D2R,
  eRate: -0.00004392,
  iRadRate: -0.01294668 * D2R,
  ascNodeRadRate: 0,
  argPerihelionRadRate: 0.32327364 * D2R, // dϖ/dt
  meanLongitudeRadRate: 35999.37244981 * D2R,
};
const _mars = {
  e: 0.0933941,
  iRad: 1.84969142 * D2R,
  ascNodeRad: 49.55953891 * D2R,
  argPerihelionRad: (-23.94362959 - 49.55953891) * D2R, // ω = 286.497°  (kept signed; consumers normalize)
  meanLongitudeJ2000Rad: -4.55343205 * D2R,
  eRate: 0.00007882,
  iRadRate: -0.00813131 * D2R,
  ascNodeRadRate: -0.29257343 * D2R,
  argPerihelionRadRate: (0.44441088 - -0.29257343) * D2R,
  meanLongitudeRadRate: 19140.30268499 * D2R,
};
const _jupiter = {
  e: 0.04838624,
  iRad: 1.30439695 * D2R,
  ascNodeRad: 100.47390909 * D2R,
  argPerihelionRad: (14.72847983 - 100.47390909) * D2R, // ω = 274.254°
  meanLongitudeJ2000Rad: 34.39644051 * D2R,
  eRate: -0.00013253,
  iRadRate: -0.00183714 * D2R,
  ascNodeRadRate: 0.20469106 * D2R,
  argPerihelionRadRate: (0.21252668 - 0.20469106) * D2R,
  meanLongitudeRadRate: 3034.74612775 * D2R,
};
const _saturn = {
  e: 0.05386179,
  iRad: 2.48599187 * D2R,
  ascNodeRad: 113.66242448 * D2R,
  argPerihelionRad: (92.59887831 - 113.66242448) * D2R, // ω = 338.937°
  meanLongitudeJ2000Rad: 49.95424423 * D2R,
  eRate: -0.00050991,
  iRadRate: 0.00193609 * D2R,
  ascNodeRadRate: -0.28867794 * D2R,
  argPerihelionRadRate: (-0.41897216 - -0.28867794) * D2R,
  meanLongitudeRadRate: 1222.49362201 * D2R,
};
const _uranus = {
  e: 0.04725744,
  iRad: 0.77263783 * D2R,
  ascNodeRad: 74.01692503 * D2R,
  argPerihelionRad: (170.9542763 - 74.01692503) * D2R, // ω = 96.937°
  meanLongitudeJ2000Rad: 313.23810451 * D2R,
  eRate: -0.00004397,
  iRadRate: -0.00242939 * D2R,
  ascNodeRadRate: 0.04240589 * D2R,
  argPerihelionRadRate: (0.40805281 - 0.04240589) * D2R,
  meanLongitudeRadRate: 428.48202785 * D2R,
};
const _neptune = {
  e: 0.00859048,
  iRad: 1.77004347 * D2R,
  ascNodeRad: 131.78422574 * D2R,
  argPerihelionRad: (44.96476227 - 131.78422574) * D2R, // ω = 273.181°
  meanLongitudeJ2000Rad: -55.12002969 * D2R,
  eRate: 0.00005105,
  iRadRate: 0.00035372 * D2R,
  ascNodeRadRate: -0.00508664 * D2R,
  argPerihelionRadRate: (-0.32241464 - -0.00508664) * D2R,
  meanLongitudeRadRate: 218.45945325 * D2R,
};

export const PLANETS = [
  {
    id: 'sun',
    names: { fr: 'Soleil', en: 'Sun' },
    isStar: true,
    color: 0xfff2b3,
    radiusKm: 696340,
    axialTiltRad: 0.1265, // ~7.25° relative to the ecliptic
  },
  {
    id: 'mercury',
    names: { fr: 'Mercure', en: 'Mercury' },
    color: 0xa39a8b,
    radiusKm: 2439.7,
    orbitKm: 57.91e6,
    orbitalPeriodDays: 87.969,
    axialTiltRad: 0.001,
    ..._mercury,
  },
  {
    id: 'venus',
    names: { fr: 'Vénus', en: 'Venus' },
    color: 0xe6b56b,
    radiusKm: 6051.8,
    orbitKm: 108.21e6,
    orbitalPeriodDays: 224.701,
    axialTiltRad: 3.096, // retrograde — but we don't simulate spin direction
    ..._venus,
  },
  {
    id: 'earth',
    names: { fr: 'Terre', en: 'Earth' },
    color: 0x5b8fd9,
    radiusKm: 6371.0,
    orbitKm: 149.6e6,
    orbitalPeriodDays: 365.256,
    axialTiltRad: 0.4091, // 23.4°
    ..._earth,
  },
  {
    id: 'mars',
    names: { fr: 'Mars', en: 'Mars' },
    color: 0xd1553b,
    radiusKm: 3389.5,
    orbitKm: 227.94e6,
    orbitalPeriodDays: 686.971,
    axialTiltRad: 0.4398, // 25.2°
    ..._mars,
  },
  {
    id: 'jupiter',
    names: { fr: 'Jupiter', en: 'Jupiter' },
    color: 0xd3a87a,
    radiusKm: 69911,
    orbitKm: 778.48e6,
    orbitalPeriodDays: 4332.589,
    axialTiltRad: 0.0546, // 3.1°
    ..._jupiter,
  },
  {
    id: 'saturn',
    names: { fr: 'Saturne', en: 'Saturn' },
    color: 0xe3c98a,
    radiusKm: 58232,
    orbitKm: 1432.04e6,
    orbitalPeriodDays: 10759.22,
    axialTiltRad: 0.4668, // 26.7°
    rings: { innerKm: 74500, outerKm: 140220 },
    ..._saturn,
  },
  {
    id: 'uranus',
    names: { fr: 'Uranus', en: 'Uranus' },
    color: 0x9fd8e0,
    radiusKm: 25362,
    orbitKm: 2867.04e6,
    orbitalPeriodDays: 30688.5,
    axialTiltRad: 1.7063, // 97.8° — essentially rolling on its side
    ..._uranus,
  },
  {
    id: 'neptune',
    names: { fr: 'Neptune', en: 'Neptune' },
    color: 0x4a6fd9,
    radiusKm: 24622,
    orbitKm: 4514.95e6,
    orbitalPeriodDays: 60190.03,
    axialTiltRad: 0.4943,
    ..._neptune,
  },
];

// Lookup helpers
export const planetById = (id) => PLANETS.find((p) => p.id === id);

// Derived quantities — the orbital period ratio drives the "1× speed" animation.
// Earth = 1.0 by convention. Mercury = 0.24 means it orbits 4.15× faster than Earth.
// Returns null for objects with no orbital period (e.g. the Sun), so callers
// can filter or skip rather than propagate NaN.
export const orbitalPeriodYears = (p) =>
  p.orbitalPeriodDays != null ? p.orbitalPeriodDays / 365.25 : null;
← Astrarium