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:
@@ -17,6 +17,9 @@ export class SolarSystem2DRenderer {
|
||||
private positions: Float64Array = new Float64Array(0);
|
||||
private velocities: Float64Array = new Float64Array(0);
|
||||
private orbitPoints: Map<number, Float64Array> = new Map();
|
||||
private stationPositions: Float64Array = new Float64Array(0);
|
||||
private stationNames: string[] = [];
|
||||
private showStations: boolean = true;
|
||||
private isDragging = false;
|
||||
private lastMouse = { x: 0, y: 0 };
|
||||
private animFrameId: number = 0;
|
||||
@@ -77,6 +80,12 @@ export class SolarSystem2DRenderer {
|
||||
this.orbitPoints.set(bodyId, points);
|
||||
}
|
||||
|
||||
updateStations(positions: Float64Array, names: string[], visible: boolean) {
|
||||
this.stationPositions = positions;
|
||||
this.stationNames = names;
|
||||
this.showStations = visible;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { canvas, ctx } = this;
|
||||
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
|
||||
this.drawScaleBar(rect.width, rect.height);
|
||||
}
|
||||
@@ -269,6 +287,32 @@ export class SolarSystem2DRenderer {
|
||||
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) {
|
||||
const ctx = this.ctx;
|
||||
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);
|
||||
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 { SolarSystem2DRenderer } from '$lib/render/canvas2d/SolarSystem2D';
|
||||
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 renderer: SolarSystem2DRenderer;
|
||||
@@ -41,7 +42,12 @@
|
||||
const velocities = getBodyVelocities(simulation.currentJD);
|
||||
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) {
|
||||
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 TimeControls from './(simulator)/components/TimeControls.svelte';
|
||||
import ViewToggle from './(simulator)/components/ViewToggle.svelte';
|
||||
import StationPanel from './(simulator)/components/StationPanel.svelte';
|
||||
|
||||
let wasmError = $state('');
|
||||
|
||||
@@ -14,7 +15,6 @@
|
||||
await initWasm();
|
||||
simulation.bodyInfos = getBodyInfos();
|
||||
simulation.wasmReady = true;
|
||||
// Start in Jan 2025 for a recognizable view
|
||||
simulation.setDate(2025, 1, 1);
|
||||
simulation.isPlaying = true;
|
||||
} catch (e) {
|
||||
@@ -37,23 +37,30 @@
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="flex-1 relative overflow-hidden">
|
||||
<main class="flex-1 flex overflow-hidden">
|
||||
{#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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{: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="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>
|
||||
</div>
|
||||
</div>
|
||||
{: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}
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user