Initial project setup: Rust/WASM solar system simulator with SvelteKit frontend

- Rust workspace with 4 crates: orbital-mechanics, mass-driver-core, mass-driver-wasm, mass-driver-backend
- Keplerian orbital mechanics engine with JPL elements for 14 bodies (Sun, 8 planets, Pluto, Ceres, Europa, Titan, Ganymede)
- Kepler equation solver and orbital position computation compiled to WASM
- SvelteKit frontend with Tailwind CSS, Canvas2D renderer showing animated solar system
- Orbit ellipses, planet dots with labels, Sun glow, grid, scale bar, pan/zoom controls
- Time controls (play/pause, 5 speed levels, date picker) driving live simulation
- 2D/3D view toggle (3D placeholder for Threlte integration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 22:06:30 -07:00
commit 5efe0736ac
45 changed files with 4626 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
[package]
name = "mass-driver-backend"
version = "0.1.0"
edition = "2021"
[dependencies]
orbital-mechanics = { path = "../orbital-mechanics" }
mass-driver-core = { path = "../mass-driver-core" }
axum = "0.8"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
serde = { workspace = true }
serde_json = { workspace = true }
tower-http = { version = "0.6", features = ["cors", "compression-gzip"] }
tracing = "0.1"
tracing-subscriber = "0.3"

View File

@@ -0,0 +1,3 @@
fn main() {
println!("mass-driver backend — not yet implemented");
}

View File

@@ -0,0 +1,13 @@
[package]
name = "mass-driver-core"
version = "0.1.0"
edition = "2021"
[dependencies]
orbital-mechanics = { path = "../orbital-mechanics" }
nalgebra = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
petgraph = "0.8"
ordered-float = "5"
rmp-serde = "1"

View File

@@ -0,0 +1,2 @@
pub mod station;
pub mod route;

View File

@@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
/// A single leg of a route between two stations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteLeg {
pub from_station: usize,
pub to_station: usize,
/// Departure week index (0 = first week of simulation)
pub departure_week: u32,
/// Arrival week index
pub arrival_week: u32,
/// Weeks spent waiting at departure station before this leg
pub wait_weeks: u32,
}
/// The result of a route computation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteResult {
pub legs: Vec<RouteLeg>,
/// Total elapsed time in weeks from departure to final arrival
pub total_time_weeks: u32,
/// The week index when the package departs the origin
pub departure_week: u32,
}

View File

@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
/// How a station is placed in the solar system.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StationPlacement {
/// Station in heliocentric orbit
SolarOrbit {
a: f64, // Semi-major axis (AU)
e: f64, // Eccentricity
i: f64, // Inclination (degrees)
omega: f64, // Longitude of ascending node (degrees)
w: f64, // Argument of perihelion (degrees)
m0: f64, // Mean anomaly at epoch (degrees)
},
/// Station in orbit around a planet
PlanetaryOrbit {
parent_body_id: usize,
altitude_km: f64,
inclination: f64,
},
/// Station at a Lagrange point
LagrangePoint {
primary_body_id: usize, // e.g., Sun
secondary_body_id: usize, // e.g., Earth
point: LagrangePointId,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum LagrangePointId {
L1,
L2,
L3,
L4,
L5,
}
/// A mass driver relay station.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Station {
pub id: usize,
pub name: String,
pub placement: StationPlacement,
}

View File

@@ -0,0 +1,8 @@
{
"hash": "bef44088",
"configHash": "3d8fe0fc",
"lockfileHash": "e3b0c442",
"browserHash": "7d9faa18",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,20 @@
[package]
name = "mass-driver-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
orbital-mechanics = { path = "../orbital-mechanics" }
mass-driver-core = { path = "../mass-driver-core" }
wasm-bindgen = "0.2"
serde-wasm-bindgen = "0.6"
serde = { workspace = true }
serde_json = { workspace = true }
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }
[dev-dependencies]
wasm-bindgen-test = "0.3"

View File

@@ -0,0 +1,59 @@
use orbital_mechanics::bodies;
use orbital_mechanics::orbits;
use wasm_bindgen::prelude::*;
/// Initialize the WASM module. Called once on page load.
#[wasm_bindgen]
pub fn init() -> Result<(), JsValue> {
web_sys::console::log_1(&"mass-driver WASM initialized".into());
Ok(())
}
/// Get the number of celestial bodies in the simulation.
#[wasm_bindgen]
pub fn get_body_count() -> usize {
bodies::all_bodies().len()
}
/// Get positions of all celestial bodies at a given Julian Date.
///
/// Returns a Float64Array of [x0, y0, z0, x1, y1, z1, ...] in AU.
#[wasm_bindgen]
pub fn get_body_positions_at_epoch(jd: f64) -> Vec<f64> {
let all = bodies::all_bodies();
orbits::all_positions_at_epoch(&all, jd)
}
/// Get body names as a JSON array of strings.
#[wasm_bindgen]
pub fn get_body_names() -> String {
let all = bodies::all_bodies();
let names: Vec<&str> = all.iter().map(|b| b.name).collect();
serde_json::to_string(&names).unwrap()
}
/// Get body colors as a flat array [r0, g0, b0, r1, g1, b1, ...].
#[wasm_bindgen]
pub fn get_body_colors() -> Vec<u8> {
let all = bodies::all_bodies();
all.iter().flat_map(|b| b.color.iter().copied()).collect()
}
/// Get body radii in km as a Float64Array.
#[wasm_bindgen]
pub fn get_body_radii() -> Vec<f64> {
let all = bodies::all_bodies();
all.iter().map(|b| b.radius_km).collect()
}
/// Get orbit points for a given body, sampled around its full orbit.
/// Returns a Float64Array of [x0, y0, z0, x1, y1, z1, ...] in AU.
/// `samples` is the number of points around the orbit.
#[wasm_bindgen]
pub fn get_orbit_points(body_id: usize, jd: f64, samples: usize) -> Vec<f64> {
let all = bodies::all_bodies();
if body_id >= all.len() || body_id == bodies::id::SUN {
return vec![];
}
orbits::orbit_points(&all, body_id, jd, samples)
}

View File

@@ -0,0 +1,3 @@
mod api;
pub use api::*;

View File

@@ -0,0 +1,8 @@
[package]
name = "orbital-mechanics"
version = "0.1.0"
edition = "2021"
[dependencies]
nalgebra = { workspace = true }
serde = { workspace = true }

View File

@@ -0,0 +1,413 @@
use serde::{Deserialize, Serialize};
/// Keplerian orbital elements at J2000.0 epoch.
///
/// For planets: heliocentric, ecliptic coordinates.
/// Values from JPL "Approximate Positions of the Planets"
/// (https://ssd.jpl.nasa.gov/planets/approx_pos.html)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeplerianElements {
/// Semi-major axis (AU)
pub a: f64,
/// Eccentricity
pub e: f64,
/// Inclination (degrees)
pub i: f64,
/// Mean longitude (degrees)
pub l: f64,
/// Longitude of perihelion (degrees)
pub w_bar: f64,
/// Longitude of ascending node (degrees)
pub omega: f64,
}
/// Rates of change of Keplerian elements per Julian century.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeplerianRates {
pub a: f64,
pub e: f64,
pub i: f64,
pub l: f64,
pub w_bar: f64,
pub omega: f64,
}
/// A celestial body with orbital elements and physical properties.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CelestialBody {
pub name: &'static str,
pub id: usize,
/// Gravitational parameter (km³/s²)
pub mu: f64,
/// Mean radius (km)
pub radius_km: f64,
/// Keplerian elements at J2000.0
pub elements: KeplerianElements,
/// Rates of change per century
pub rates: KeplerianRates,
/// Display color as [r, g, b]
pub color: [u8; 3],
}
/// Enumeration of body IDs for quick lookup.
pub mod id {
pub const SUN: usize = 0;
pub const MERCURY: usize = 1;
pub const VENUS: usize = 2;
pub const EARTH: usize = 3;
pub const MARS: usize = 4;
pub const JUPITER: usize = 5;
pub const SATURN: usize = 6;
pub const URANUS: usize = 7;
pub const NEPTUNE: usize = 8;
pub const PLUTO: usize = 9;
pub const CERES: usize = 10;
pub const EUROPA: usize = 11;
pub const TITAN: usize = 12;
pub const GANYMEDE: usize = 13;
}
/// Returns all celestial bodies with JPL Keplerian elements.
///
/// Planet elements from JPL "Approximate Positions of the Planets"
/// valid for 3000 BC to 3000 AD.
pub fn all_bodies() -> Vec<CelestialBody> {
vec![
// Sun (placeholder — no orbit, always at origin)
CelestialBody {
name: "Sun",
id: id::SUN,
mu: 1.327_124_400_41e11,
radius_km: 695_700.0,
elements: KeplerianElements {
a: 0.0, e: 0.0, i: 0.0, l: 0.0, w_bar: 0.0, omega: 0.0,
},
rates: KeplerianRates {
a: 0.0, e: 0.0, i: 0.0, l: 0.0, w_bar: 0.0, omega: 0.0,
},
color: [255, 204, 0],
},
// Mercury
CelestialBody {
name: "Mercury",
id: id::MERCURY,
mu: 2.2032e4,
radius_km: 2_439.7,
elements: KeplerianElements {
a: 0.387_099_27,
e: 0.205_301_04,
i: 7.004_986_8,
l: 252.250_32,
w_bar: 77.457_796,
omega: 48.330_893,
},
rates: KeplerianRates {
a: 0.000_000_37,
e: 0.000_010_02,
i: -0.005_946_30,
l: 149_472.674_11,
w_bar: 0.160_476_24,
omega: -0.125_340_2,
},
color: [183, 168, 153],
},
// Venus
CelestialBody {
name: "Venus",
id: id::VENUS,
mu: 3.24859e5,
radius_km: 6_051.8,
elements: KeplerianElements {
a: 0.723_332_01,
e: 0.006_773_23,
i: 3.394_662_0,
l: 181.979_73,
w_bar: 131.602_467,
omega: 76.679_920,
},
rates: KeplerianRates {
a: 0.000_000_60,
e: -0.000_048_98,
i: -0.007_889_0,
l: 58_517.815_39,
w_bar: 0.002_688_29,
omega: -0.278_008_0,
},
color: [231, 196, 150],
},
// Earth
CelestialBody {
name: "Earth",
id: id::EARTH,
mu: 3.986e5,
radius_km: 6_371.0,
elements: KeplerianElements {
a: 1.000_002_61,
e: 0.016_711_23,
i: -0.000_015_31,
l: 100.464_572,
w_bar: 102.937_682,
omega: 0.0,
},
rates: KeplerianRates {
a: 0.000_005_62,
e: -0.000_043_92,
i: -0.012_946_68,
l: 35_999.372_47,
w_bar: 0.323_281_26,
omega: 0.0,
},
color: [100, 149, 237],
},
// Mars
CelestialBody {
name: "Mars",
id: id::MARS,
mu: 4.2828e4,
radius_km: 3_389.5,
elements: KeplerianElements {
a: 1.523_679_40,
e: 0.093_400_62,
i: 1.849_726_0,
l: -4.553_432,
w_bar: -23.943_629_5,
omega: 49.559_539,
},
rates: KeplerianRates {
a: 0.000_018_47,
e: 0.000_079_48,
i: -0.006_813_52,
l: 19_140.303_00,
w_bar: 0.445_265_40,
omega: -0.295_257_0,
},
color: [193, 68, 14],
},
// Jupiter
CelestialBody {
name: "Jupiter",
id: id::JUPITER,
mu: 1.26687e8,
radius_km: 69_911.0,
elements: KeplerianElements {
a: 5.202_603_0,
e: 0.048_498_26,
i: 1.303_270,
l: 34.396_441,
w_bar: 14.728_479,
omega: 100.473_909,
},
rates: KeplerianRates {
a: 0.000_016_05,
e: 0.000_163_94,
i: -0.001_983_72,
l: 3_034.746_12,
w_bar: 0.216_519_4,
omega: 0.205_831_6,
},
color: [216, 165, 108],
},
// Saturn
CelestialBody {
name: "Saturn",
id: id::SATURN,
mu: 3.7931e7,
radius_km: 58_232.0,
elements: KeplerianElements {
a: 9.554_909_6,
e: 0.055_086_22,
i: 2.488_878,
l: 49.954_244,
w_bar: 92.598_78,
omega: 113.662_424,
},
rates: KeplerianRates {
a: -0.000_025_06,
e: -0.000_050_32,
i: 0.004_530_33,
l: 1_222.113_94,
w_bar: -0.411_897_0,
omega: -0.288_668_0,
},
color: [210, 180, 140],
},
// Uranus
CelestialBody {
name: "Uranus",
id: id::URANUS,
mu: 5.7940e6,
radius_km: 25_362.0,
elements: KeplerianElements {
a: 19.218_446_2,
e: 0.047_317_65,
i: 0.773_196,
l: 313.238_105,
w_bar: 170.954_276,
omega: 74.016_925,
},
rates: KeplerianRates {
a: -0.000_198_58,
e: -0.000_004_07,
i: -0.002_446_1,
l: 428.481_40,
w_bar: 0.408_030_0,
omega: 0.046_863_9,
},
color: [172, 229, 238],
},
// Neptune
CelestialBody {
name: "Neptune",
id: id::NEPTUNE,
mu: 6.8351e6,
radius_km: 24_622.0,
elements: KeplerianElements {
a: 30.110_386_9,
e: 0.008_590_48,
i: 1.769_952,
l: -55.120_029,
w_bar: 44.964_762,
omega: 131.784_057,
},
rates: KeplerianRates {
a: 0.000_006_08,
e: 0.000_006_30,
i: 0.000_062_6,
l: 218.457_64,
w_bar: -0.327_55,
omega: -0.006_066_2,
},
color: [63, 84, 186],
},
// Pluto (approximate elements)
CelestialBody {
name: "Pluto",
id: id::PLUTO,
mu: 8.71e2,
radius_km: 1_188.3,
elements: KeplerianElements {
a: 39.481_686_77,
e: 0.248_905_87,
i: 17.140_175,
l: 238.928_06,
w_bar: 224.068_76,
omega: 110.303_47,
},
rates: KeplerianRates {
a: -0.007_608_12,
e: 0.000_060_65,
i: 0.003_681_9,
l: 145.205_80,
w_bar: -0.045_07,
omega: -0.018_16,
},
color: [190, 180, 170],
},
// Ceres (approximate)
CelestialBody {
name: "Ceres",
id: id::CERES,
mu: 63.13,
radius_km: 469.73,
elements: KeplerianElements {
a: 2.769_1,
e: 0.075_8,
i: 10.594,
l: 153.94,
w_bar: 73.597 + 80.329,
omega: 80.329,
},
rates: KeplerianRates {
a: 0.0,
e: 0.0,
i: 0.0,
l: 78_362.73, // ~4.6 yr period
w_bar: 0.0,
omega: 0.0,
},
color: [148, 137, 121],
},
// Europa (orbits Jupiter — elements are Jovicentric)
// For visualization, we compute Jupiter's position and add Europa's offset
CelestialBody {
name: "Europa",
id: id::EUROPA,
mu: 3_202.7,
radius_km: 1_560.8,
elements: KeplerianElements {
a: 0.004_486, // ~671,100 km in AU
e: 0.009_4,
i: 0.47,
l: 171.016,
w_bar: 0.0,
omega: 0.0,
},
rates: KeplerianRates {
a: 0.0,
e: 0.0,
i: 0.0,
l: 101_375.3, // ~3.55 day period
w_bar: 0.0,
omega: 0.0,
},
color: [200, 190, 160],
},
// Titan (orbits Saturn)
CelestialBody {
name: "Titan",
id: id::TITAN,
mu: 8_978.1,
radius_km: 2_574.7,
elements: KeplerianElements {
a: 0.008_168, // ~1,221,870 km in AU
e: 0.028_8,
i: 0.34,
l: 120.0,
w_bar: 0.0,
omega: 0.0,
},
rates: KeplerianRates {
a: 0.0,
e: 0.0,
i: 0.0,
l: 22_577.0, // ~15.95 day period
w_bar: 0.0,
omega: 0.0,
},
color: [230, 190, 100],
},
// Ganymede (orbits Jupiter)
CelestialBody {
name: "Ganymede",
id: id::GANYMEDE,
mu: 9_887.8,
radius_km: 2_634.1,
elements: KeplerianElements {
a: 0.007_155, // ~1,070,400 km in AU
e: 0.001_3,
i: 0.20,
l: 317.54,
w_bar: 0.0,
omega: 0.0,
},
rates: KeplerianRates {
a: 0.0,
e: 0.0,
i: 0.0,
l: 50_317.6, // ~7.15 day period
w_bar: 0.0,
omega: 0.0,
},
color: [160, 145, 130],
},
]
}
/// Returns the parent body ID for moons (None for planets/Sun).
pub fn parent_body(body_id: usize) -> Option<usize> {
match body_id {
id::EUROPA | id::GANYMEDE => Some(id::JUPITER),
id::TITAN => Some(id::SATURN),
_ => None,
}
}

View File

@@ -0,0 +1,26 @@
/// Gravitational parameter of the Sun (km³/s²)
pub const MU_SUN: f64 = 1.327_124_400_41e11;
/// One Astronomical Unit in kilometers
pub const AU_KM: f64 = 1.495_978_707e8;
/// Seconds per day
pub const SECONDS_PER_DAY: f64 = 86_400.0;
/// Days per Julian century
pub const DAYS_PER_CENTURY: f64 = 36_525.0;
/// J2000.0 epoch in Julian Date
pub const J2000_JD: f64 = 2_451_545.0;
/// Days per week
pub const DAYS_PER_WEEK: f64 = 7.0;
/// Pi
pub const PI: f64 = std::f64::consts::PI;
/// Two Pi
pub const TAU: f64 = std::f64::consts::TAU;
/// Degrees to radians
pub const DEG_TO_RAD: f64 = PI / 180.0;

View File

@@ -0,0 +1,56 @@
use crate::constants::TAU;
/// Solve Kepler's equation M = E - e*sin(E) for eccentric anomaly E.
///
/// Uses Newton-Raphson iteration. Converges in 3-6 iterations for
/// eccentricities < 0.9. All angles in radians.
pub fn solve_kepler(mean_anomaly: f64, eccentricity: f64) -> f64 {
let m = mean_anomaly % TAU;
let e = eccentricity;
// Initial guess (Markley's starting value for better convergence)
let mut ea = if e < 0.8 {
m + e * m.sin()
} else {
std::f64::consts::PI
};
// Newton-Raphson iteration
for _ in 0..50 {
let delta = (ea - e * ea.sin() - m) / (1.0 - e * ea.cos());
ea -= delta;
if delta.abs() < 1e-12 {
break;
}
}
ea
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circular_orbit() {
// For e=0, E should equal M
let m = 1.0;
let e = solve_kepler(m, 0.0);
assert!((e - m).abs() < 1e-12);
}
#[test]
fn test_known_values() {
// For M=π, any eccentricity, E should be π
let e = solve_kepler(std::f64::consts::PI, 0.5);
assert!((e - std::f64::consts::PI).abs() < 1e-10);
}
#[test]
fn test_high_eccentricity() {
let ea = solve_kepler(1.0, 0.9);
// Verify it satisfies Kepler's equation
let m_check = ea - 0.9 * ea.sin();
assert!((m_check - 1.0).abs() < 1e-10);
}
}

View File

@@ -0,0 +1,6 @@
pub mod bodies;
pub mod constants;
pub mod kepler;
pub mod orbits;
pub use nalgebra::Vector3;

View File

@@ -0,0 +1,192 @@
use crate::bodies::{self, CelestialBody};
use crate::constants::*;
use crate::kepler::solve_kepler;
use nalgebra::Vector3;
/// Compute the heliocentric ecliptic position of a body at a given Julian Date.
///
/// Returns position in AU in the ecliptic coordinate frame:
/// - X toward vernal equinox
/// - Y in ecliptic plane, 90° from X
/// - Z toward ecliptic north pole
///
/// For moons, returns the heliocentric position (parent position + moon offset).
pub fn position_at_epoch(bodies: &[CelestialBody], body_id: usize, jd: f64) -> Vector3<f64> {
let body = &bodies[body_id];
// Sun is at origin
if body_id == bodies::id::SUN {
return Vector3::zeros();
}
// Centuries since J2000.0
let t = (jd - J2000_JD) / DAYS_PER_CENTURY;
// Compute current elements
let a = body.elements.a + body.rates.a * t;
let e = body.elements.e + body.rates.e * t;
let i = (body.elements.i + body.rates.i * t) * DEG_TO_RAD;
let l = (body.elements.l + body.rates.l * t) * DEG_TO_RAD;
let w_bar = (body.elements.w_bar + body.rates.w_bar * t) * DEG_TO_RAD;
let omega = (body.elements.omega + body.rates.omega * t) * DEG_TO_RAD;
// Argument of perihelion
let w = w_bar - omega;
// Mean anomaly
let m = l - w_bar;
// Solve Kepler's equation for eccentric anomaly
let ea = solve_kepler(m, e);
// True anomaly
let nu = 2.0 * ((1.0 + e).sqrt() * (ea / 2.0).sin())
.atan2((1.0 - e).sqrt() * (ea / 2.0).cos());
// Distance from focus
let r = a * (1.0 - e * ea.cos());
// Position in orbital plane
let x_orb = r * nu.cos();
let y_orb = r * nu.sin();
// Rotate to ecliptic coordinates
let cos_w = w.cos();
let sin_w = w.sin();
let cos_o = omega.cos();
let sin_o = omega.sin();
let cos_i = i.cos();
let sin_i = i.sin();
let x_ecl = (cos_w * cos_o - sin_w * sin_o * cos_i) * x_orb
+ (-sin_w * cos_o - cos_w * sin_o * cos_i) * y_orb;
let y_ecl = (cos_w * sin_o + sin_w * cos_o * cos_i) * x_orb
+ (-sin_w * sin_o + cos_w * cos_o * cos_i) * y_orb;
let z_ecl = (sin_w * sin_i) * x_orb + (cos_w * sin_i) * y_orb;
let pos = Vector3::new(x_ecl, y_ecl, z_ecl);
// For moons, add parent body position
if let Some(parent_id) = bodies::parent_body(body_id) {
let parent_pos = position_at_epoch(bodies, parent_id, jd);
parent_pos + pos
} else {
pos
}
}
/// Compute positions of all bodies at a given epoch.
/// Returns a flat Vec of [x, y, z, x, y, z, ...] in AU.
pub fn all_positions_at_epoch(bodies: &[CelestialBody], jd: f64) -> Vec<f64> {
let mut result = Vec::with_capacity(bodies.len() * 3);
for i in 0..bodies.len() {
let pos = position_at_epoch(bodies, i, jd);
result.push(pos.x);
result.push(pos.y);
result.push(pos.z);
}
result
}
/// Sample points around a body's orbit for drawing orbit ellipses.
/// For planets, samples one full heliocentric orbit.
/// For moons, samples one full orbit around the parent and adds parent position.
/// Returns flat [x, y, z, x, y, z, ...] in AU.
pub fn orbit_points(
bodies: &[CelestialBody],
body_id: usize,
jd: f64,
samples: usize,
) -> Vec<f64> {
let body = &bodies[body_id];
let t = (jd - J2000_JD) / DAYS_PER_CENTURY;
let a = body.elements.a + body.rates.a * t;
let e = body.elements.e + body.rates.e * t;
let i = (body.elements.i + body.rates.i * t) * DEG_TO_RAD;
let w_bar = (body.elements.w_bar + body.rates.w_bar * t) * DEG_TO_RAD;
let omega = (body.elements.omega + body.rates.omega * t) * DEG_TO_RAD;
let w = w_bar - omega;
let cos_w = w.cos();
let sin_w = w.sin();
let cos_o = omega.cos();
let sin_o = omega.sin();
let cos_i = i.cos();
let sin_i = i.sin();
// Parent offset for moons
let parent_pos = if let Some(parent_id) = bodies::parent_body(body_id) {
position_at_epoch(bodies, parent_id, jd)
} else {
Vector3::zeros()
};
let mut result = Vec::with_capacity(samples * 3);
for s in 0..samples {
let nu = TAU * (s as f64) / (samples as f64);
let r = a * (1.0 - e * e) / (1.0 + e * nu.cos());
let x_orb = r * nu.cos();
let y_orb = r * nu.sin();
let x = (cos_w * cos_o - sin_w * sin_o * cos_i) * x_orb
+ (-sin_w * cos_o - cos_w * sin_o * cos_i) * y_orb
+ parent_pos.x;
let y = (cos_w * sin_o + sin_w * cos_o * cos_i) * x_orb
+ (-sin_w * sin_o + cos_w * cos_o * cos_i) * y_orb
+ parent_pos.y;
let z = (sin_w * sin_i) * x_orb + (cos_w * sin_i) * y_orb + parent_pos.z;
result.push(x);
result.push(y);
result.push(z);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bodies::all_bodies;
#[test]
fn test_sun_at_origin() {
let bodies = all_bodies();
let pos = position_at_epoch(&bodies, bodies::id::SUN, J2000_JD);
assert!(pos.norm() < 1e-15);
}
#[test]
fn test_earth_distance_from_sun() {
let bodies = all_bodies();
let pos = position_at_epoch(&bodies, bodies::id::EARTH, J2000_JD);
let distance_au = pos.norm();
// Earth should be ~1 AU from the Sun
assert!(
(distance_au - 1.0).abs() < 0.02,
"Earth distance: {} AU",
distance_au
);
}
#[test]
fn test_jupiter_distance() {
let bodies = all_bodies();
let pos = position_at_epoch(&bodies, bodies::id::JUPITER, J2000_JD);
let distance_au = pos.norm();
// Jupiter should be ~5.2 AU
assert!(
(distance_au - 5.2).abs() < 0.3,
"Jupiter distance: {} AU",
distance_au
);
}
#[test]
fn test_all_positions_length() {
let bodies = all_bodies();
let positions = all_positions_at_epoch(&bodies, J2000_JD);
assert_eq!(positions.len(), bodies.len() * 3);
}
}