Add Lambert solver, transfer matrix, Dijkstra routing, and route planner UI

- Lambert's problem solver using universal variable method with bisection
  (handles elliptic, parabolic, hyperbolic transfers + anti-podal cases)
- Transfer matrix: precompute pairwise station transfers over time window
  using Lambert solver with configurable launch velocity
- Dijkstra routing on time-expanded graph (station × week nodes, transfer
  + wait edges) to find minimum-time routes
- Route Planner UI: from/to station dropdowns, search window selector
  (1-10 years), "Find Optimal Route" button with results card
- Route visualization: orange dashed trajectory lines with arrow heads
  and leg numbers on the 2D canvas
- Tested: Mercury L1 → Jupiter L1 computes 6-month direct transfer at
  30 km/s — physically reasonable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 12:14:37 -07:00
parent a2daa2d617
commit 22dcc5b6ec
12 changed files with 977 additions and 2 deletions

View File

@@ -20,6 +20,7 @@ export class SolarSystem2DRenderer {
private stationPositions: Float64Array = new Float64Array(0);
private stationNames: string[] = [];
private showStations: boolean = true;
private routeLegs: { from: number; to: number }[] = [];
private isDragging = false;
private lastMouse = { x: 0, y: 0 };
private animFrameId: number = 0;
@@ -86,6 +87,10 @@ export class SolarSystem2DRenderer {
this.showStations = visible;
}
updateRoute(legs: { from: number; to: number }[]) {
this.routeLegs = legs;
}
render() {
const { canvas, ctx } = this;
const dpr = window.devicePixelRatio || 1;
@@ -133,6 +138,11 @@ export class SolarSystem2DRenderer {
}
}
// Draw route legs
if (this.routeLegs.length > 0 && this.stationPositions.length > 0) {
this.drawRoute();
}
// Scale indicator
this.drawScaleBar(rect.width, rect.height);
}
@@ -287,6 +297,58 @@ export class SolarSystem2DRenderer {
ctx.fillText(info.name, sx + size + 4, sy + 3);
}
private drawRoute() {
const ctx = this.ctx;
const colors = ['#ff6a33', '#33ff88', '#ff33aa', '#33aaff', '#ffaa33'];
for (let i = 0; i < this.routeLegs.length; i++) {
const leg = this.routeLegs[i];
const fromX = this.stationPositions[leg.from * 3];
const fromY = this.stationPositions[leg.from * 3 + 1];
const toX = this.stationPositions[leg.to * 3];
const toY = this.stationPositions[leg.to * 3 + 1];
const [sx1, sy1] = this.auToScreen(fromX, fromY);
const [sx2, sy2] = this.auToScreen(toX, toY);
const color = colors[i % colors.length];
// Draw line
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.beginPath();
ctx.moveTo(sx1, sy1);
ctx.lineTo(sx2, sy2);
ctx.stroke();
ctx.setLineDash([]);
// Arrow head
const angle = Math.atan2(sy2 - sy1, sx2 - sx1);
const arrowLen = 8;
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(sx2, sy2);
ctx.lineTo(
sx2 - arrowLen * Math.cos(angle - 0.3),
sy2 - arrowLen * Math.sin(angle - 0.3),
);
ctx.lineTo(
sx2 - arrowLen * Math.cos(angle + 0.3),
sy2 - arrowLen * Math.sin(angle + 0.3),
);
ctx.closePath();
ctx.fill();
// Leg number
const mx = (sx1 + sx2) / 2;
const my = (sy1 + sy2) / 2;
ctx.fillStyle = color;
ctx.font = 'bold 10px monospace';
ctx.fillText(`${i + 1}`, mx + 5, my - 5);
}
}
private drawStation(xAU: number, yAU: number, name: string) {
const ctx = this.ctx;
const [sx, sy] = this.auToScreen(xAU, yAU);