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,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);
}
}