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:
@@ -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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// How a station is placed in the solar system.
|
/// How a station is placed in the solar system.
|
||||||
@@ -12,29 +17,13 @@ pub enum StationPlacement {
|
|||||||
w: f64, // Argument of perihelion (degrees)
|
w: f64, // Argument of perihelion (degrees)
|
||||||
m0: f64, // Mean anomaly at epoch (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
|
/// Station at a Lagrange point
|
||||||
LagrangePoint {
|
LagrangePoint {
|
||||||
primary_body_id: usize, // e.g., Sun
|
planet_id: usize,
|
||||||
secondary_body_id: usize, // e.g., Earth
|
point: u8, // 1-5
|
||||||
point: LagrangePointId,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub enum LagrangePointId {
|
|
||||||
L1,
|
|
||||||
L2,
|
|
||||||
L3,
|
|
||||||
L4,
|
|
||||||
L5,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A mass driver relay station.
|
/// A mass driver relay station.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Station {
|
pub struct Station {
|
||||||
@@ -42,3 +31,116 @@ pub struct Station {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub placement: StationPlacement,
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use mass_driver_core::station;
|
||||||
use orbital_mechanics::bodies;
|
use orbital_mechanics::bodies;
|
||||||
use orbital_mechanics::orbits;
|
use orbital_mechanics::orbits;
|
||||||
use wasm_bindgen::prelude::*;
|
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)
|
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()
|
||||||
|
}
|
||||||
|
|||||||
105
crates/orbital-mechanics/src/lagrange.rs
Normal file
105
crates/orbital-mechanics/src/lagrange.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod bodies;
|
pub mod bodies;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod kepler;
|
pub mod kepler;
|
||||||
|
pub mod lagrange;
|
||||||
pub mod orbits;
|
pub mod orbits;
|
||||||
|
|
||||||
pub use nalgebra::Vector3;
|
pub use nalgebra::Vector3;
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export class SolarSystem2DRenderer {
|
|||||||
private positions: Float64Array = new Float64Array(0);
|
private positions: Float64Array = new Float64Array(0);
|
||||||
private velocities: Float64Array = new Float64Array(0);
|
private velocities: Float64Array = new Float64Array(0);
|
||||||
private orbitPoints: Map<number, Float64Array> = new Map();
|
private orbitPoints: Map<number, Float64Array> = new Map();
|
||||||
|
private stationPositions: Float64Array = new Float64Array(0);
|
||||||
|
private stationNames: string[] = [];
|
||||||
|
private showStations: boolean = true;
|
||||||
private isDragging = false;
|
private isDragging = false;
|
||||||
private lastMouse = { x: 0, y: 0 };
|
private lastMouse = { x: 0, y: 0 };
|
||||||
private animFrameId: number = 0;
|
private animFrameId: number = 0;
|
||||||
@@ -77,6 +80,12 @@ export class SolarSystem2DRenderer {
|
|||||||
this.orbitPoints.set(bodyId, points);
|
this.orbitPoints.set(bodyId, points);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateStations(positions: Float64Array, names: string[], visible: boolean) {
|
||||||
|
this.stationPositions = positions;
|
||||||
|
this.stationNames = names;
|
||||||
|
this.showStations = visible;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { canvas, ctx } = this;
|
const { canvas, ctx } = this;
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
@@ -115,6 +124,15 @@ export class SolarSystem2DRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw stations
|
||||||
|
if (this.showStations && this.stationPositions.length > 0) {
|
||||||
|
for (let i = 0; i < this.stationNames.length; i++) {
|
||||||
|
const x = this.stationPositions[i * 3];
|
||||||
|
const y = this.stationPositions[i * 3 + 1];
|
||||||
|
this.drawStation(x, y, this.stationNames[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Scale indicator
|
// Scale indicator
|
||||||
this.drawScaleBar(rect.width, rect.height);
|
this.drawScaleBar(rect.width, rect.height);
|
||||||
}
|
}
|
||||||
@@ -269,6 +287,32 @@ export class SolarSystem2DRenderer {
|
|||||||
ctx.fillText(info.name, sx + size + 4, sy + 3);
|
ctx.fillText(info.name, sx + size + 4, sy + 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private drawStation(xAU: number, yAU: number, name: string) {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const [sx, sy] = this.auToScreen(xAU, yAU);
|
||||||
|
|
||||||
|
// Diamond shape
|
||||||
|
const s = 3;
|
||||||
|
ctx.fillStyle = 'rgba(74, 158, 255, 0.6)';
|
||||||
|
ctx.strokeStyle = 'rgba(74, 158, 255, 0.8)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(sx, sy - s);
|
||||||
|
ctx.lineTo(sx + s, sy);
|
||||||
|
ctx.lineTo(sx, sy + s);
|
||||||
|
ctx.lineTo(sx - s, sy);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Label (only show when zoomed in enough)
|
||||||
|
if (this.getScale() > 50) {
|
||||||
|
ctx.fillStyle = 'rgba(74, 158, 255, 0.5)';
|
||||||
|
ctx.font = '8px monospace';
|
||||||
|
ctx.fillText(name, sx + s + 3, sy + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private drawScaleBar(w: number, h: number) {
|
private drawScaleBar(w: number, h: number) {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const scale = this.getScale();
|
const scale = this.getScale();
|
||||||
|
|||||||
10
frontend/src/lib/stores/stations.svelte.ts
Normal file
10
frontend/src/lib/stores/stations.svelte.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class StationsState {
|
||||||
|
stationCount = $state(20);
|
||||||
|
launchVelocityKms = $state(30); // km/s — sci-fi default
|
||||||
|
stationsJson = $state('[]');
|
||||||
|
stationNames = $state<string[]>([]);
|
||||||
|
stationPositions = $state<Float64Array>(new Float64Array(0));
|
||||||
|
visible = $state(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stations = new StationsState();
|
||||||
@@ -39,3 +39,21 @@ export function getOrbitPoints(bodyId: number, jd: number, samples: number = 180
|
|||||||
if (!wasm) return new Float64Array(0);
|
if (!wasm) return new Float64Array(0);
|
||||||
return wasm.get_orbit_points(bodyId, jd, samples);
|
return wasm.get_orbit_points(bodyId, jd, samples);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateStations(count: number): string {
|
||||||
|
const wasm = getWasm();
|
||||||
|
if (!wasm) return '[]';
|
||||||
|
return wasm.generate_stations(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStationPositions(stationsJson: string, jd: number): Float64Array {
|
||||||
|
const wasm = getWasm();
|
||||||
|
if (!wasm) return new Float64Array(0);
|
||||||
|
return wasm.get_station_positions(stationsJson, jd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStationNames(stationsJson: string): string[] {
|
||||||
|
const wasm = getWasm();
|
||||||
|
if (!wasm) return [];
|
||||||
|
return JSON.parse(wasm.get_station_names(stationsJson));
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { SolarSystem2DRenderer } from '$lib/render/canvas2d/SolarSystem2D';
|
import { SolarSystem2DRenderer } from '$lib/render/canvas2d/SolarSystem2D';
|
||||||
import { simulation } from '$lib/stores/simulation.svelte';
|
import { simulation } from '$lib/stores/simulation.svelte';
|
||||||
import { getBodyPositions, getBodyVelocities, getBodyInfos, getOrbitPoints } from '$lib/wasm/bridge';
|
import { stations } from '$lib/stores/stations.svelte';
|
||||||
|
import { getBodyPositions, getBodyVelocities, getBodyInfos, getOrbitPoints, getStationPositions } from '$lib/wasm/bridge';
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
let renderer: SolarSystem2DRenderer;
|
let renderer: SolarSystem2DRenderer;
|
||||||
@@ -41,7 +42,12 @@
|
|||||||
const velocities = getBodyVelocities(simulation.currentJD);
|
const velocities = getBodyVelocities(simulation.currentJD);
|
||||||
renderer.updateBodies(simulation.bodyInfos, simulation.bodyPositions, velocities);
|
renderer.updateBodies(simulation.bodyInfos, simulation.bodyPositions, velocities);
|
||||||
|
|
||||||
// Compute orbits on first frame and refresh every 600 frames (~10s)
|
// Update station positions
|
||||||
|
if (stations.stationsJson !== '[]') {
|
||||||
|
stations.stationPositions = getStationPositions(stations.stationsJson, simulation.currentJD);
|
||||||
|
renderer.updateStations(stations.stationPositions, stations.stationNames, stations.visible);
|
||||||
|
}
|
||||||
|
|
||||||
if (!orbitsComputed || frameCount % 600 === 0) {
|
if (!orbitsComputed || frameCount % 600 === 0) {
|
||||||
computeOrbits();
|
computeOrbits();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { stations } from '$lib/stores/stations.svelte';
|
||||||
|
import { generateStations, getStationNames } from '$lib/wasm/bridge';
|
||||||
|
import { simulation } from '$lib/stores/simulation.svelte';
|
||||||
|
|
||||||
|
function updateStations() {
|
||||||
|
if (!simulation.wasmReady) return;
|
||||||
|
stations.stationsJson = generateStations(stations.stationCount);
|
||||||
|
stations.stationNames = getStationNames(stations.stationsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on first load
|
||||||
|
$effect(() => {
|
||||||
|
if (simulation.wasmReady) {
|
||||||
|
updateStations();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Regenerate when count changes
|
||||||
|
$effect(() => {
|
||||||
|
const _ = stations.stationCount;
|
||||||
|
if (simulation.wasmReady) {
|
||||||
|
updateStations();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 p-3 bg-[var(--bg-panel)] rounded-lg border border-white/5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-[var(--text-secondary)]">
|
||||||
|
Relay Stations
|
||||||
|
</h3>
|
||||||
|
<label class="flex items-center gap-1.5 text-xs text-[var(--text-secondary)] cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={stations.visible}
|
||||||
|
class="rounded border-white/20 bg-transparent"
|
||||||
|
/>
|
||||||
|
Show
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Station count slider -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-[var(--text-secondary)]">Count</span>
|
||||||
|
<span class="text-[var(--text-primary)] font-mono">{stations.stationCount}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="5"
|
||||||
|
max="50"
|
||||||
|
step="1"
|
||||||
|
bind:value={stations.stationCount}
|
||||||
|
class="w-full h-1 rounded-full appearance-none bg-white/10 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[var(--accent-blue)] [&::-webkit-slider-thumb]:cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Launch velocity slider -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-[var(--text-secondary)] flex items-center gap-1">
|
||||||
|
Launch Velocity
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full border border-white/20 text-[8px] text-[var(--text-secondary)] cursor-help"
|
||||||
|
title="Current railgun tech: ~2-3 km/s Sci-fi mass drivers: 10-50 km/s Theoretical limit: ~0.01c (3,000 km/s) Higher velocity means fewer relay stations needed but more energy per launch."
|
||||||
|
>i</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-[var(--text-primary)] font-mono">{stations.launchVelocityKms} km/s</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="5"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
bind:value={stations.launchVelocityKms}
|
||||||
|
class="w-full h-1 rounded-full appearance-none bg-white/10 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[var(--accent-orange)] [&::-webkit-slider-thumb]:cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div class="flex justify-between text-[8px] text-[var(--text-secondary)] mt-0.5">
|
||||||
|
<span>5</span>
|
||||||
|
<span>50</span>
|
||||||
|
<span>100</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Station list (condensed) -->
|
||||||
|
{#if stations.stationNames.length > 0}
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="text-[10px] text-[var(--text-secondary)] mb-1">Active stations:</div>
|
||||||
|
<div class="flex flex-wrap gap-1 max-h-24 overflow-y-auto">
|
||||||
|
{#each stations.stationNames as name}
|
||||||
|
<span class="px-1.5 py-0.5 text-[9px] bg-white/5 rounded text-[var(--text-secondary)]">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
import Viewport from './(simulator)/components/Viewport.svelte';
|
import Viewport from './(simulator)/components/Viewport.svelte';
|
||||||
import TimeControls from './(simulator)/components/TimeControls.svelte';
|
import TimeControls from './(simulator)/components/TimeControls.svelte';
|
||||||
import ViewToggle from './(simulator)/components/ViewToggle.svelte';
|
import ViewToggle from './(simulator)/components/ViewToggle.svelte';
|
||||||
|
import StationPanel from './(simulator)/components/StationPanel.svelte';
|
||||||
|
|
||||||
let wasmError = $state('');
|
let wasmError = $state('');
|
||||||
|
|
||||||
@@ -14,7 +15,6 @@
|
|||||||
await initWasm();
|
await initWasm();
|
||||||
simulation.bodyInfos = getBodyInfos();
|
simulation.bodyInfos = getBodyInfos();
|
||||||
simulation.wasmReady = true;
|
simulation.wasmReady = true;
|
||||||
// Start in Jan 2025 for a recognizable view
|
|
||||||
simulation.setDate(2025, 1, 1);
|
simulation.setDate(2025, 1, 1);
|
||||||
simulation.isPlaying = true;
|
simulation.isPlaying = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -37,23 +37,30 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="flex-1 relative overflow-hidden">
|
<main class="flex-1 flex overflow-hidden">
|
||||||
{#if wasmError}
|
{#if wasmError}
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="flex-1 flex items-center justify-center">
|
||||||
<div class="bg-red-900/50 border border-red-500/30 rounded-lg p-6 max-w-md">
|
<div class="bg-red-900/50 border border-red-500/30 rounded-lg p-6 max-w-md">
|
||||||
<h2 class="text-red-400 font-bold mb-2">Failed to load simulation engine</h2>
|
<h2 class="text-red-400 font-bold mb-2">Failed to load simulation engine</h2>
|
||||||
<p class="text-sm text-red-300/80">{wasmError}</p>
|
<p class="text-sm text-red-300/80">{wasmError}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if !simulation.wasmReady}
|
{:else if !simulation.wasmReady}
|
||||||
<div class="absolute inset-0 flex items-center justify-center">
|
<div class="flex-1 flex items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="w-8 h-8 border-2 border-[var(--accent-blue)] border-t-transparent rounded-full animate-spin mx-auto mb-3"></div>
|
<div class="w-8 h-8 border-2 border-[var(--accent-blue)] border-t-transparent rounded-full animate-spin mx-auto mb-3"></div>
|
||||||
<p class="text-sm text-[var(--text-secondary)]">Initializing simulation engine...</p>
|
<p class="text-sm text-[var(--text-secondary)]">Initializing simulation engine...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Viewport />
|
<!-- Viewport -->
|
||||||
|
<div class="flex-1 relative">
|
||||||
|
<Viewport />
|
||||||
|
</div>
|
||||||
|
<!-- Right sidebar -->
|
||||||
|
<aside class="w-64 shrink-0 p-2 bg-[var(--bg-secondary)] border-l border-white/5 overflow-y-auto">
|
||||||
|
<StationPanel />
|
||||||
|
</aside>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user