Add animated package along trajectory with Kepler propagation

- Trajectory sampling: propagate Lambert transfer orbits using universal
  variable Kepler propagation, sample 60 points per leg
- Smooth curved trajectory lines on 2D canvas (real orbital arcs, not
  straight lines)
- Animated green package dot with radial glow traveling along trajectory
- Route progress slider (0-100%) with play/pause animation toggle
- Auto-animating ~20-second cycle through the full route
- WASM API: sample_route_trajectory() with per-leg NaN separators

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 12:21:58 -07:00
parent 22dcc5b6ec
commit 21c842acdc
8 changed files with 329 additions and 38 deletions

View File

@@ -72,7 +72,7 @@ pub fn lagrange_point_position(
#[cfg(test)]
mod tests {
use super::*;
use crate::bodies::all_bodies;
use crate::bodies::{self, all_bodies};
#[test]
fn test_l1_between_sun_and_planet() {

View File

@@ -214,6 +214,114 @@ pub fn transfer_delta_v(
Some((dv1, dv2))
}
/// Sample points along a Lambert transfer trajectory.
/// Returns positions at evenly spaced times between departure and arrival.
///
/// # Arguments
/// * `r1`, `r2` - Start/end positions (km)
/// * `tof` - Time of flight (seconds)
/// * `mu` - Gravitational parameter (km³/s²)
/// * `num_samples` - Number of sample points (including start and end)
///
/// Returns flat [x,y,z, x,y,z, ...] in km, or empty if Lambert fails.
pub fn sample_trajectory(
r1: Vector3<f64>,
r2: Vector3<f64>,
tof: f64,
mu: f64,
num_samples: usize,
) -> Vec<f64> {
let sol = match solve_lambert(r1, r2, tof, mu, true) {
Some(s) => s,
None => return vec![],
};
if num_samples < 2 {
return vec![r1.x, r1.y, r1.z];
}
let mut result = Vec::with_capacity(num_samples * 3);
for i in 0..num_samples {
let t_frac = i as f64 / (num_samples - 1) as f64;
let dt = t_frac * tof;
if i == 0 {
result.push(r1.x);
result.push(r1.y);
result.push(r1.z);
continue;
}
if i == num_samples - 1 {
result.push(r2.x);
result.push(r2.y);
result.push(r2.z);
continue;
}
// Propagate from r1 with v1 using the universal variable
let pos = propagate_kepler(r1, sol.v1, dt, mu);
result.push(pos.x);
result.push(pos.y);
result.push(pos.z);
}
result
}
/// Propagate a Keplerian orbit forward by time dt from given state (r, v).
fn propagate_kepler(r0: Vector3<f64>, v0: Vector3<f64>, dt: f64, mu: f64) -> Vector3<f64> {
let r0_mag = r0.norm();
let v0_mag = v0.norm();
let vr0 = r0.dot(&v0) / r0_mag; // radial velocity
let alpha = 2.0 / r0_mag - v0_mag * v0_mag / mu; // 1/a
// Solve for universal variable chi using Newton-Raphson
let sqrt_mu = mu.sqrt();
// Initial guess for universal variable
let mut chi = if alpha > 1e-10 {
sqrt_mu * dt * alpha
} else {
sqrt_mu * dt / r0_mag
};
for _ in 0..50 {
let chi2 = chi * chi;
let z = alpha * chi2;
let (c2, c3) = super::lambert::stumpff(z);
let _r = chi2 * c2 + vr0 / sqrt_mu * chi * (1.0 - z * c3) + r0_mag * (1.0 - z * c2);
// f(chi) = r0 * chi * (1 - z*c3) + vr0/sqrt_mu * chi^2 * c2 + r0_mag * chi^3 * c3 - sqrt_mu * dt
let f_chi = vr0 / sqrt_mu * chi2 * c2
+ (1.0 - r0_mag * alpha) * chi * chi2 * c3
+ r0_mag * chi
- sqrt_mu * dt;
let f_prime = chi2 * c2 + vr0 / sqrt_mu * chi * (1.0 - z * c3) + r0_mag * (1.0 - z * c2);
if f_prime.abs() < 1e-20 {
break;
}
chi -= f_chi / f_prime;
if (f_chi / f_prime).abs() < 1e-10 {
break;
}
}
let chi2 = chi * chi;
let z = alpha * chi2;
let (c2, c3) = stumpff(z);
// Lagrange coefficients
let f = 1.0 - chi2 / r0_mag * c2;
let g = dt - chi * chi2 / sqrt_mu * c3;
f * r0 + g * v0
}
#[cfg(test)]
mod tests {
use super::*;