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:
@@ -102,6 +102,76 @@ pub fn get_station_names(stations_json: &str) -> String {
|
|||||||
serde_json::to_string(&names).unwrap()
|
serde_json::to_string(&names).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sample trajectory points along a route for visualization.
|
||||||
|
/// Takes the stations JSON, route JSON (from compute_route), and current JD.
|
||||||
|
/// Returns flat Float64Array of [x,y,z, ...] in AU for all legs concatenated,
|
||||||
|
/// with a separator of NaN,NaN,NaN between legs.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn sample_route_trajectory(
|
||||||
|
stations_json: &str,
|
||||||
|
route_json: &str,
|
||||||
|
jd: f64,
|
||||||
|
samples_per_leg: usize,
|
||||||
|
) -> Vec<f64> {
|
||||||
|
use mass_driver_core::route::RouteResult;
|
||||||
|
use orbital_mechanics::lambert;
|
||||||
|
|
||||||
|
let stations: Vec<station::Station> = match serde_json::from_str(stations_json) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return vec![],
|
||||||
|
};
|
||||||
|
let route: RouteResult = match serde_json::from_str(route_json) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let all_bodies = bodies::all_bodies();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
for (i, leg) in route.legs.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
// Separator between legs
|
||||||
|
result.push(f64::NAN);
|
||||||
|
result.push(f64::NAN);
|
||||||
|
result.push(f64::NAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get station positions at departure/arrival times
|
||||||
|
let dep_jd = jd + (leg.departure_week as f64) * 7.0;
|
||||||
|
let arr_jd = jd + (leg.arrival_week as f64) * 7.0;
|
||||||
|
let tof_seconds = (arr_jd - dep_jd) * orbital_mechanics::constants::SECONDS_PER_DAY;
|
||||||
|
|
||||||
|
let r1_au = station::station_position(
|
||||||
|
&stations[leg.from_station], &all_bodies, dep_jd,
|
||||||
|
);
|
||||||
|
let r2_au = station::station_position(
|
||||||
|
&stations[leg.to_station], &all_bodies, arr_jd,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert to km for Lambert solver
|
||||||
|
let au_km = orbital_mechanics::constants::AU_KM;
|
||||||
|
let r1_km = r1_au * au_km;
|
||||||
|
let r2_km = r2_au * au_km;
|
||||||
|
|
||||||
|
let points = lambert::sample_trajectory(
|
||||||
|
r1_km, r2_km, tof_seconds,
|
||||||
|
orbital_mechanics::constants::MU_SUN,
|
||||||
|
samples_per_leg,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert back to AU
|
||||||
|
for chunk in points.chunks(3) {
|
||||||
|
if chunk.len() == 3 {
|
||||||
|
result.push(chunk[0] / au_km);
|
||||||
|
result.push(chunk[1] / au_km);
|
||||||
|
result.push(chunk[2] / au_km);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute transfer matrix and find optimal route between two stations.
|
/// Compute transfer matrix and find optimal route between two stations.
|
||||||
///
|
///
|
||||||
/// This is the main computation function. It:
|
/// This is the main computation function. It:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ pub fn lagrange_point_position(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::bodies::all_bodies;
|
use crate::bodies::{self, all_bodies};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_l1_between_sun_and_planet() {
|
fn test_l1_between_sun_and_planet() {
|
||||||
|
|||||||
@@ -214,6 +214,114 @@ pub fn transfer_delta_v(
|
|||||||
Some((dv1, dv2))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export class SolarSystem2DRenderer {
|
|||||||
private stationNames: string[] = [];
|
private stationNames: string[] = [];
|
||||||
private showStations: boolean = true;
|
private showStations: boolean = true;
|
||||||
private routeLegs: { from: number; to: number }[] = [];
|
private routeLegs: { from: number; to: number }[] = [];
|
||||||
|
private trajectoryPoints: Float64Array = new Float64Array(0);
|
||||||
|
private routeProgress: number = 0;
|
||||||
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;
|
||||||
@@ -87,8 +89,14 @@ export class SolarSystem2DRenderer {
|
|||||||
this.showStations = visible;
|
this.showStations = visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRoute(legs: { from: number; to: number }[]) {
|
updateRoute(
|
||||||
|
legs: { from: number; to: number }[],
|
||||||
|
trajectoryPoints?: Float64Array,
|
||||||
|
progress?: number,
|
||||||
|
) {
|
||||||
this.routeLegs = legs;
|
this.routeLegs = legs;
|
||||||
|
if (trajectoryPoints) this.trajectoryPoints = trajectoryPoints;
|
||||||
|
if (progress !== undefined) this.routeProgress = progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -299,10 +307,79 @@ export class SolarSystem2DRenderer {
|
|||||||
|
|
||||||
private drawRoute() {
|
private drawRoute() {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const colors = ['#ff6a33', '#33ff88', '#ff33aa', '#33aaff', '#ffaa33'];
|
|
||||||
|
|
||||||
|
// Draw smooth trajectory curve from sampled points
|
||||||
|
if (this.trajectoryPoints.length >= 6) {
|
||||||
|
ctx.strokeStyle = 'rgba(255, 106, 51, 0.5)';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.beginPath();
|
||||||
|
let moved = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.trajectoryPoints.length; i += 3) {
|
||||||
|
const x = this.trajectoryPoints[i];
|
||||||
|
const y = this.trajectoryPoints[i + 1];
|
||||||
|
|
||||||
|
if (isNaN(x) || isNaN(y)) {
|
||||||
|
// Leg separator — start new sub-path
|
||||||
|
moved = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [sx, sy] = this.auToScreen(x, y);
|
||||||
|
if (!moved) {
|
||||||
|
ctx.moveTo(sx, sy);
|
||||||
|
moved = true;
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(sx, sy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Draw animated package dot along trajectory
|
||||||
|
const totalPoints = Math.floor(this.trajectoryPoints.length / 3);
|
||||||
|
// Filter out NaN separators for interpolation
|
||||||
|
const validPoints: number[] = [];
|
||||||
|
for (let i = 0; i < this.trajectoryPoints.length; i += 3) {
|
||||||
|
if (!isNaN(this.trajectoryPoints[i])) {
|
||||||
|
validPoints.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validPoints.length > 1) {
|
||||||
|
const idx = Math.min(
|
||||||
|
Math.floor(this.routeProgress * (validPoints.length - 1)),
|
||||||
|
validPoints.length - 1,
|
||||||
|
);
|
||||||
|
const pIdx = validPoints[idx];
|
||||||
|
const px = this.trajectoryPoints[pIdx];
|
||||||
|
const py = this.trajectoryPoints[pIdx + 1];
|
||||||
|
const [sx, sy] = this.auToScreen(px, py);
|
||||||
|
|
||||||
|
// Package glow
|
||||||
|
const gradient = ctx.createRadialGradient(sx, sy, 0, sx, sy, 12);
|
||||||
|
gradient.addColorStop(0, 'rgba(51, 255, 136, 0.8)');
|
||||||
|
gradient.addColorStop(0.5, 'rgba(51, 255, 136, 0.2)');
|
||||||
|
gradient.addColorStop(1, 'rgba(51, 255, 136, 0)');
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(sx, sy, 12, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Package core
|
||||||
|
ctx.fillStyle = '#33ff88';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(sx, sy, 3, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw station-to-station connection lines (faded, for reference)
|
||||||
|
const colors = ['#ff6a33', '#33ff88', '#ff33aa', '#33aaff', '#ffaa33'];
|
||||||
for (let i = 0; i < this.routeLegs.length; i++) {
|
for (let i = 0; i < this.routeLegs.length; i++) {
|
||||||
const leg = this.routeLegs[i];
|
const leg = this.routeLegs[i];
|
||||||
|
if (leg.from * 3 + 1 >= this.stationPositions.length) continue;
|
||||||
|
if (leg.to * 3 + 1 >= this.stationPositions.length) continue;
|
||||||
|
|
||||||
const fromX = this.stationPositions[leg.from * 3];
|
const fromX = this.stationPositions[leg.from * 3];
|
||||||
const fromY = this.stationPositions[leg.from * 3 + 1];
|
const fromY = this.stationPositions[leg.from * 3 + 1];
|
||||||
const toX = this.stationPositions[leg.to * 3];
|
const toX = this.stationPositions[leg.to * 3];
|
||||||
@@ -310,42 +387,17 @@ export class SolarSystem2DRenderer {
|
|||||||
|
|
||||||
const [sx1, sy1] = this.auToScreen(fromX, fromY);
|
const [sx1, sy1] = this.auToScreen(fromX, fromY);
|
||||||
const [sx2, sy2] = this.auToScreen(toX, toY);
|
const [sx2, sy2] = this.auToScreen(toX, toY);
|
||||||
|
|
||||||
const color = colors[i % colors.length];
|
const color = colors[i % colors.length];
|
||||||
|
|
||||||
// Draw line
|
// Faint dashed reference line
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color + '40';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 1;
|
||||||
ctx.setLineDash([6, 4]);
|
ctx.setLineDash([4, 4]);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(sx1, sy1);
|
ctx.moveTo(sx1, sy1);
|
||||||
ctx.lineTo(sx2, sy2);
|
ctx.lineTo(sx2, sy2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ class RoutingState {
|
|||||||
isComputing = $state(false);
|
isComputing = $state(false);
|
||||||
error = $state('');
|
error = $state('');
|
||||||
weekWindow = $state(260); // ~5 years search window
|
weekWindow = $state(260); // ~5 years search window
|
||||||
|
|
||||||
|
// Route animation
|
||||||
|
trajectoryPoints = $state<Float64Array>(new Float64Array(0));
|
||||||
|
routeProgress = $state(0); // 0..1 along the full route
|
||||||
|
isAnimating = $state(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routing = new RoutingState();
|
export const routing = new RoutingState();
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ export interface RouteResult {
|
|||||||
departure_week: number;
|
departure_week: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sampleRouteTrajectory(
|
||||||
|
stationsJson: string,
|
||||||
|
routeJson: string,
|
||||||
|
jd: number,
|
||||||
|
samplesPerLeg: number = 50,
|
||||||
|
): Float64Array {
|
||||||
|
const wasm = getWasm();
|
||||||
|
if (!wasm) return new Float64Array(0);
|
||||||
|
return wasm.sample_route_trajectory(stationsJson, routeJson, jd, samplesPerLeg);
|
||||||
|
}
|
||||||
|
|
||||||
export function computeRoute(
|
export function computeRoute(
|
||||||
stationsJson: string,
|
stationsJson: string,
|
||||||
fromStation: number,
|
fromStation: number,
|
||||||
|
|||||||
@@ -51,10 +51,20 @@
|
|||||||
|
|
||||||
// Update route visualization
|
// Update route visualization
|
||||||
if (routing.route) {
|
if (routing.route) {
|
||||||
renderer.updateRoute(routing.route.legs.map(l => ({
|
// Advance animation
|
||||||
|
if (routing.isAnimating) {
|
||||||
|
routing.routeProgress += dt * 0.05; // ~20 seconds for full route
|
||||||
|
if (routing.routeProgress > 1) routing.routeProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.updateRoute(
|
||||||
|
routing.route.legs.map(l => ({
|
||||||
from: l.from_station,
|
from: l.from_station,
|
||||||
to: l.to_station,
|
to: l.to_station,
|
||||||
})));
|
})),
|
||||||
|
routing.trajectoryPoints,
|
||||||
|
routing.routeProgress,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
renderer.updateRoute([]);
|
renderer.updateRoute([]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { simulation } from '$lib/stores/simulation.svelte';
|
import { simulation } from '$lib/stores/simulation.svelte';
|
||||||
import { stations } from '$lib/stores/stations.svelte';
|
import { stations } from '$lib/stores/stations.svelte';
|
||||||
import { routing } from '$lib/stores/routing.svelte';
|
import { routing } from '$lib/stores/routing.svelte';
|
||||||
import { computeRoute } from '$lib/wasm/bridge';
|
import { computeRoute, sampleRouteTrajectory } from '$lib/wasm/bridge';
|
||||||
import { formatWeeks } from '$lib/utils/format';
|
import { formatWeeks } from '$lib/utils/format';
|
||||||
|
|
||||||
function handleCompute() {
|
function handleCompute() {
|
||||||
@@ -18,8 +18,10 @@
|
|||||||
routing.isComputing = true;
|
routing.isComputing = true;
|
||||||
routing.error = '';
|
routing.error = '';
|
||||||
routing.route = null;
|
routing.route = null;
|
||||||
|
routing.trajectoryPoints = new Float64Array(0);
|
||||||
|
routing.routeProgress = 0;
|
||||||
|
routing.isAnimating = false;
|
||||||
|
|
||||||
// Use setTimeout to let the UI update before blocking computation
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const result = computeRoute(
|
const result = computeRoute(
|
||||||
stations.stationsJson,
|
stations.stationsJson,
|
||||||
@@ -32,6 +34,13 @@
|
|||||||
|
|
||||||
if (result && result.legs.length > 0) {
|
if (result && result.legs.length > 0) {
|
||||||
routing.route = result;
|
routing.route = result;
|
||||||
|
|
||||||
|
// Sample trajectory for visualization
|
||||||
|
const routeJson = JSON.stringify(result);
|
||||||
|
routing.trajectoryPoints = sampleRouteTrajectory(
|
||||||
|
stations.stationsJson, routeJson, simulation.currentJD, 60,
|
||||||
|
);
|
||||||
|
routing.isAnimating = true;
|
||||||
} else {
|
} else {
|
||||||
routing.error = 'No route found. Try increasing velocity or search window.';
|
routing.error = 'No route found. Try increasing velocity or search window.';
|
||||||
}
|
}
|
||||||
@@ -153,6 +162,32 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Animation controls -->
|
||||||
|
{#if routing.trajectoryPoints.length > 0}
|
||||||
|
<div class="mt-2 pt-2 border-t border-white/5">
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<span class="text-[10px] text-[var(--text-secondary)]">Package Progress</span>
|
||||||
|
<button
|
||||||
|
onclick={() => { routing.isAnimating = !routing.isAnimating; }}
|
||||||
|
class="text-[10px] px-1.5 py-0.5 rounded {routing.isAnimating ? 'bg-[var(--accent-green)]/20 text-[var(--accent-green)]' : 'bg-white/5 text-[var(--text-secondary)]'}"
|
||||||
|
>
|
||||||
|
{routing.isAnimating ? 'Animating' : 'Paused'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.002"
|
||||||
|
bind:value={routing.routeProgress}
|
||||||
|
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-green)] [&::-webkit-slider-thumb]:cursor-pointer"
|
||||||
|
/>
|
||||||
|
<div class="text-[9px] text-[var(--text-secondary)] text-center mt-0.5">
|
||||||
|
{Math.round(routing.routeProgress * 100)}% complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user