diff --git a/.gitignore b/.gitignore
index c0e913a6..8b895c3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/dist
/target
Cargo.lock
*.anis*
@@ -10,4 +11,4 @@ cspice.tar.Z
.venv
*.pca
*.sca
-*.epa
\ No newline at end of file
+*.epa
diff --git a/Cargo.toml b/Cargo.toml
index 00a397c4..8273ebaf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,7 +25,7 @@ exclude = [
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-hifitime = "3.8"
+hifitime = "3.8.6"
memmap2 = "=0.9.0"
crc32fast = "=1.3.2"
der = { version = "0.7.8", features = ["derive", "alloc", "real"] }
@@ -52,6 +52,10 @@ egui_extras = { version = "0.24.0", features = [
egui-toast = { version = "0.10.0", optional = true }
rfd = { version = "0.12.1", optional = true }
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+wasm-bindgen-futures = "0.4"
+poll-promise = { version = "0.3.0", features = ["web"] }
+
[dev-dependencies]
rust-spice = "0.7.6"
parquet = "49.0.0"
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..5aa96521
--- /dev/null
+++ b/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/almanac/mod.rs b/src/almanac/mod.rs
index b2a2d0cf..50679647 100644
--- a/src/almanac/mod.rs
+++ b/src/almanac/mod.rs
@@ -8,6 +8,7 @@
* Documentation: https://nyxspace.com/
*/
+use bytes::Bytes;
use log::info;
use snafu::ResultExt;
use std::fs::File;
@@ -94,7 +95,11 @@ impl Almanac {
let bytes = file2heap!(path).with_context(|_| LoadingSnafu {
path: path.to_string(),
})?;
+ info!("Loading almanac from {path}");
+ self.load_from_bytes(bytes)
+ }
+ pub fn load_from_bytes(&self, bytes: Bytes) -> Result {
// Try to load as a SPICE DAF first (likely the most typical use case)
// Load the header only
@@ -103,7 +108,7 @@ impl Almanac {
if let Ok(fileid) = file_record.identification() {
match fileid {
"PCK" => {
- info!("Loading {path} as DAF/PCK");
+ info!("Loading as DAF/PCK");
let bpc = BPC::parse(bytes)
.with_context(|_| BPCSnafu {
action: "parsing bytes",
@@ -116,7 +121,7 @@ impl Almanac {
})
}
"SPK" => {
- info!("Loading {path:?} as DAF/SPK");
+ info!("Loading as DAF/SPK");
let spk = SPK::parse(bytes)
.with_context(|_| SPKSnafu {
action: "parsing bytes",
diff --git a/src/bin/anise-gui/main.rs b/src/bin/anise-gui/main.rs
index 7c750d7f..e2abaddd 100644
--- a/src/bin/anise-gui/main.rs
+++ b/src/bin/anise-gui/main.rs
@@ -1,13 +1,14 @@
-use pretty_env_logger;
-use std::env::{set_var, var};
-
+#[allow(dead_code)]
const LOG_VAR: &str = "ANISE_LOG";
mod ui;
use ui::UiApp;
+#[cfg(not(target_arch = "wasm32"))]
fn main() {
+ use std::env::{set_var, var};
+
if var(LOG_VAR).is_err() {
set_var(LOG_VAR, "INFO");
}
@@ -20,3 +21,24 @@ fn main() {
Box::new(|cc| Box::new(UiApp::new(cc))),
);
}
+
+// Entrypoint for WebAssembly
+#[cfg(target_arch = "wasm32")]
+fn main() {
+ use log::info;
+
+ eframe::WebLogger::init(log::LevelFilter::Debug).ok();
+ let web_options = eframe::WebOptions::default();
+
+ info!("Starting ANISE in WebAssembly mode");
+ wasm_bindgen_futures::spawn_local(async {
+ eframe::WebRunner::new()
+ .start(
+ "anise_canvas",
+ web_options,
+ Box::new(|cc| Box::new(UiApp::new(cc))),
+ )
+ .await
+ .expect("failed to start eframe");
+ });
+}
diff --git a/src/bin/anise-gui/ui.rs b/src/bin/anise-gui/ui.rs
index f203b8ce..8e6b257d 100644
--- a/src/bin/anise-gui/ui.rs
+++ b/src/bin/anise-gui/ui.rs
@@ -1,5 +1,5 @@
use anise::{
- almanac::Almanac, constants::orientations::orientation_name_from_id,
+ almanac::Almanac, constants::orientations::orientation_name_from_id, errors::AlmanacError,
naif::daf::NAIFSummaryRecord,
};
use eframe::egui;
@@ -8,12 +8,26 @@ use egui_extras::{Column, TableBuilder};
use egui_toast::{Toast, ToastKind, ToastOptions, Toasts};
use hifitime::TimeScale;
+#[cfg(target_arch = "wasm32")]
+use poll_promise::Promise;
+
+#[cfg(target_arch = "wasm32")]
+type AlmanacFile = Option<(String, Vec)>;
+
#[derive(Default)]
pub struct UiApp {
selected_time_scale: TimeScale,
show_unix: bool,
almanac: Almanac,
path: Option,
+ #[cfg(target_arch = "wasm32")]
+ promise: Option>,
+}
+
+enum FileLoadResult {
+ NoFileSelectedYet,
+ Ok((String, Almanac)),
+ Error(AlmanacError),
}
impl UiApp {
@@ -24,6 +38,43 @@ impl UiApp {
// for e.g. egui::PaintCallback.
Self::default()
}
+
+ #[cfg(target_arch = "wasm32")]
+ fn load_almanac(&mut self) -> FileLoadResult {
+ if let Some(promise) = self.promise.as_ref() {
+ // We are already waiting for a file, so we don't need to show the dialog again
+ if let Some(result) = promise.ready() {
+ let (file_name, data) = result.as_ref().map(|x| x.clone()).unwrap();
+ self.promise = None;
+ match self.almanac.load_from_bytes(bytes::Bytes::from(data)) {
+ Ok(almanac) => FileLoadResult::Ok((file_name, almanac)),
+ Err(e) => FileLoadResult::Error(e),
+ }
+ } else {
+ FileLoadResult::NoFileSelectedYet
+ }
+ } else {
+ // Show the dialog and start loading the file
+ self.promise = Some(Promise::spawn_local(async move {
+ let fh = rfd::AsyncFileDialog::new().pick_file().await?;
+ Some((fh.file_name(), fh.read().await))
+ }));
+ FileLoadResult::NoFileSelectedYet
+ }
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ fn load_almanac(&mut self) -> FileLoadResult {
+ if let Some(path_buf) = rfd::FileDialog::new().pick_file() {
+ let path = path_buf.to_str().unwrap().to_string();
+ match self.almanac.load(&path) {
+ Ok(almanac) => FileLoadResult::Ok((path, almanac)),
+ Err(e) => FileLoadResult::Error(e),
+ }
+ } else {
+ FileLoadResult::NoFileSelectedYet
+ }
+ }
}
impl eframe::App for UiApp {
@@ -53,12 +104,24 @@ impl eframe::App for UiApp {
ui.vertical_centered(|ui| {
match &self.path {
None => {
+ let mut trigger_file_load = false;
+ trigger_file_load |= ui.button("Select file to inspect...").clicked();
+
+ // If we are in the browser, we need to also check if the file
+ // is ready to be loaded instead of just checking if the button
+ // was clicked
+ #[cfg(target_arch = "wasm32")]
+ {
+ trigger_file_load |= self.promise.is_some();
+ }
+
// Show the open file dialog
- if ui.button("Select file to inspect...").clicked() {
- if let Some(path) = rfd::FileDialog::new().pick_file() {
+ if trigger_file_load {
// Try to load this file
- match self.almanac.load(path.to_str().unwrap()) {
- Ok(almanac) => {
+ match self.load_almanac() {
+ FileLoadResult::NoFileSelectedYet => {
+ }
+ FileLoadResult::Ok((path, almanac)) => {
toasts.add(Toast {
text: format!("Loaded {path:?}").into(),
kind: ToastKind::Success,
@@ -67,10 +130,9 @@ impl eframe::App for UiApp {
.show_progress(true),
});
self.almanac = almanac;
- self.path =
- Some(path.to_str().unwrap().to_string());
+ self.path = Some(path);
}
- Err(e) => {
+ FileLoadResult::Error(e) => {
toasts.add(Toast {
text: format!("{e}").into(),
kind: ToastKind::Error,
@@ -80,7 +142,6 @@ impl eframe::App for UiApp {
});
}
}
- }
}
}
Some(path) => {