Add mass driver station system with Lagrange point placement

- Lagrange point computation (L1-L5) for any Sun-planet pair in Rust
- Station generation: auto-place at Lagrange points by priority (inner → outer planets)
- Station panel UI: count slider (5-50), launch velocity slider (5-100 km/s) with info tooltip
- Blue diamond markers on 2D canvas with labels when zoomed in
- Active station list in sidebar (Earth L1, Mars L2, Jupiter L4, etc.)
- WASM API: generate_stations(), get_station_positions(), get_station_names()
- Station positions update every frame (co-rotating with planets at Lagrange points)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 12:00:36 -07:00
parent 067ef1f557
commit a2daa2d617
10 changed files with 449 additions and 25 deletions

View File

@@ -1,3 +1,8 @@
use orbital_mechanics::bodies;
use orbital_mechanics::bodies::CelestialBody;
use orbital_mechanics::lagrange;
use orbital_mechanics::orbits;
use nalgebra::Vector3;
use serde::{Deserialize, Serialize};
/// How a station is placed in the solar system.
@@ -12,29 +17,13 @@ pub enum StationPlacement {
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,
planet_id: usize,
point: u8, // 1-5
},
}
#[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 {
@@ -42,3 +31,116 @@ pub struct Station {
pub name: String,
pub placement: StationPlacement,
}
/// Simulation configuration for mass driver network.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimConfig {
pub stations: Vec<Station>,
pub launch_velocity_kms: f64, // km/s
}
/// Compute position of a station at a given Julian Date.
pub fn station_position(
station: &Station,
all_bodies: &[CelestialBody],
jd: f64,
) -> Vector3<f64> {
match &station.placement {
StationPlacement::SolarOrbit { a, e, i, omega, w, m0 } => {
// Treat station as a body with fixed elements
let body = CelestialBody {
name: "station",
id: 999,
mu: 0.0,
radius_km: 0.0,
elements: orbital_mechanics::bodies::KeplerianElements {
a: *a,
e: *e,
i: *i,
l: *m0 + *w + *omega, // mean longitude = M + w + Omega
w_bar: *w + *omega, // longitude of perihelion
omega: *omega,
},
rates: orbital_mechanics::bodies::KeplerianRates {
a: 0.0, e: 0.0, i: 0.0,
// Mean longitude rate from Kepler's third law: n = sqrt(mu/a^3) in deg/century
l: (orbital_mechanics::constants::MU_SUN
/ (*a * orbital_mechanics::constants::AU_KM).powi(3))
.sqrt()
* orbital_mechanics::constants::SECONDS_PER_DAY
* orbital_mechanics::constants::DAYS_PER_CENTURY
* (180.0 / std::f64::consts::PI),
w_bar: 0.0,
omega: 0.0,
},
color: [255, 255, 255],
};
// Use a temporary slice with just Sun + this station
let temp = vec![all_bodies[0].clone(), body];
orbits::position_at_epoch(&temp, 1, jd)
}
StationPlacement::LagrangePoint { planet_id, point } => {
lagrange::lagrange_point_position(all_bodies, *planet_id, *point, jd)
}
}
}
/// Compute positions of all stations. Returns flat [x,y,z, x,y,z, ...] in AU.
pub fn all_station_positions(
stations: &[Station],
all_bodies: &[CelestialBody],
jd: f64,
) -> Vec<f64> {
let mut result = Vec::with_capacity(stations.len() * 3);
for station in stations {
let pos = station_position(station, all_bodies, jd);
result.push(pos.x);
result.push(pos.y);
result.push(pos.z);
}
result
}
/// Generate a default set of stations for a given target count.
/// Places stations at key Lagrange points of the major planets.
pub fn generate_default_stations(target_count: usize) -> Vec<Station> {
let mut stations = Vec::new();
let mut id = 0;
// Priority order: inner planets first, then outer
let planet_priority = [
(bodies::id::EARTH, "Earth"),
(bodies::id::MARS, "Mars"),
(bodies::id::VENUS, "Venus"),
(bodies::id::JUPITER, "Jupiter"),
(bodies::id::MERCURY, "Mercury"),
(bodies::id::SATURN, "Saturn"),
(bodies::id::NEPTUNE, "Neptune"),
(bodies::id::URANUS, "Uranus"),
(bodies::id::PLUTO, "Pluto"),
(bodies::id::CERES, "Ceres"),
];
// Lagrange points to place at each planet (in priority order)
let lp_priority: &[u8] = &[1, 2, 4, 5, 3];
for &lp in lp_priority {
for &(planet_id, planet_name) in &planet_priority {
if stations.len() >= target_count {
return stations;
}
stations.push(Station {
id,
name: format!("{} L{}", planet_name, lp),
placement: StationPlacement::LagrangePoint {
planet_id,
point: lp,
},
});
id += 1;
}
}
stations.truncate(target_count);
stations
}

View File

@@ -1,3 +1,4 @@
use mass_driver_core::station;
use orbital_mechanics::bodies;
use orbital_mechanics::orbits;
use wasm_bindgen::prelude::*;
@@ -66,3 +67,34 @@ pub fn get_orbit_points(body_id: usize, jd: f64, samples: usize) -> Vec<f64> {
}
orbits::orbit_points(&all, body_id, jd, samples)
}
/// Generate default stations and return their configuration as JSON.
#[wasm_bindgen]
pub fn generate_stations(count: usize) -> String {
let stations = station::generate_default_stations(count);
serde_json::to_string(&stations).unwrap()
}
/// Get station positions at a given Julian Date.
/// Takes a JSON config string (from generate_stations) and returns
/// Float64Array of [x0, y0, z0, x1, y1, z1, ...] in AU.
#[wasm_bindgen]
pub fn get_station_positions(stations_json: &str, jd: f64) -> Vec<f64> {
let stations: Vec<station::Station> = match serde_json::from_str(stations_json) {
Ok(s) => s,
Err(_) => return vec![],
};
let all_bodies = bodies::all_bodies();
station::all_station_positions(&stations, &all_bodies, jd)
}
/// Get station names from a config JSON string.
#[wasm_bindgen]
pub fn get_station_names(stations_json: &str) -> String {
let stations: Vec<station::Station> = match serde_json::from_str(stations_json) {
Ok(s) => s,
Err(_) => return "[]".to_string(),
};
let names: Vec<&str> = stations.iter().map(|s| s.name.as_str()).collect();
serde_json::to_string(&names).unwrap()
}

View File

@@ -0,0 +1,105 @@
use crate::bodies::CelestialBody;
use crate::constants::*;
use crate::orbits::position_at_epoch;
use nalgebra::Vector3;
/// Approximate position of a Lagrange point for a Sun-planet system.
///
/// L1: Between Sun and planet (closer to planet)
/// L2: Beyond planet (away from Sun)
/// L3: Opposite side of Sun from planet
/// L4: 60° ahead of planet in its orbit
/// L5: 60° behind of planet in its orbit
///
/// Returns position in AU, ecliptic coordinates.
pub fn lagrange_point_position(
bodies: &[CelestialBody],
planet_id: usize,
point: u8, // 1-5
jd: f64,
) -> Vector3<f64> {
let planet_pos = position_at_epoch(bodies, planet_id, jd);
let sun_pos = Vector3::zeros();
let r = planet_pos.norm(); // Distance from Sun
if r < 1e-10 {
return sun_pos;
}
let planet = &bodies[planet_id];
// Mass ratio: mu = m_planet / (m_sun + m_planet)
let mu = planet.mu / (MU_SUN + planet.mu);
// Hill sphere radius approximation
let r_hill = r * (mu / 3.0).powf(1.0 / 3.0);
// Unit vector from Sun to planet
let u = planet_pos / r;
// Perpendicular in ecliptic plane (rotate 90° CCW)
let u_perp = Vector3::new(-u.y, u.x, 0.0);
match point {
1 => {
// L1: between Sun and planet, distance r_hill inside planet orbit
planet_pos - u * r_hill
}
2 => {
// L2: beyond planet, distance r_hill outside planet orbit
planet_pos + u * r_hill
}
3 => {
// L3: opposite side of Sun, approximately at planet's orbital radius
-planet_pos * (1.0 + 5.0 * mu / 12.0)
}
4 => {
// L4: 60° ahead of planet (leading)
let angle = std::f64::consts::FRAC_PI_3; // 60°
let cos_a = angle.cos();
let sin_a = angle.sin();
(u * cos_a + u_perp * sin_a) * r
}
5 => {
// L5: 60° behind planet (trailing)
let angle = std::f64::consts::FRAC_PI_3;
let cos_a = angle.cos();
let sin_a = angle.sin();
(u * cos_a - u_perp * sin_a) * r
}
_ => sun_pos,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bodies::all_bodies;
#[test]
fn test_l1_between_sun_and_planet() {
let bodies = all_bodies();
let earth_pos = position_at_epoch(&bodies, bodies::id::EARTH, J2000_JD);
let l1 = lagrange_point_position(&bodies, bodies::id::EARTH, 1, J2000_JD);
// L1 should be between Sun (0,0,0) and Earth
let l1_dist = l1.norm();
let earth_dist = earth_pos.norm();
assert!(l1_dist < earth_dist, "L1 should be closer to Sun than Earth");
assert!(l1_dist > 0.9, "L1 should be close to 1 AU");
}
#[test]
fn test_l4_l5_equilateral() {
let bodies = all_bodies();
let earth_pos = position_at_epoch(&bodies, bodies::id::EARTH, J2000_JD);
let l4 = lagrange_point_position(&bodies, bodies::id::EARTH, 4, J2000_JD);
let l5 = lagrange_point_position(&bodies, bodies::id::EARTH, 5, J2000_JD);
let earth_r = earth_pos.norm();
let l4_r = l4.norm();
let l5_r = l5.norm();
// L4 and L5 should be at roughly the same distance as the planet
assert!((l4_r - earth_r).abs() < 0.01);
assert!((l5_r - earth_r).abs() < 0.01);
}
}

View File

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