diff --git a/gpx-rs/src/engine/command/command.rs b/gpx-rs/src/engine/command/command.rs new file mode 100644 index 000000000..055a18888 --- /dev/null +++ b/gpx-rs/src/engine/command/command.rs @@ -0,0 +1,106 @@ +use wasm_bindgen::prelude::*; + +use crate::{FileId, LngLatBounds}; + +/// A user action, decoded from the WASM boundary. +/// +/// Bulk data (coordinates, file bytes) is borrowed straight from the wasm-bindgen argument +/// buffers, so a command is built without copying; the engine only copies what it keeps. +pub enum Command<'a> { + // File commands + New { + name: &'a str, + }, + Load { + data: &'a [u8], + }, + Duplicate, + Delete, + DeleteAll, + // Edit commands + Metadata { + name: &'a str, + desc: &'a str, + }, + Style { + color: Option<&'a str>, + opacity: Option, + width: Option, + }, + NewTrack, + NewTrackSegment, + // Tools + Reverse, + Append { + lng: &'a [f64], + lat: &'a [f64], + ele: &'a [f64], + }, + Replace { + start: u32, + end: u32, + lng: &'a [f64], + lat: &'a [f64], + ele: &'a [f64], + }, + NewWaypoint { + lng: f64, + lat: f64, + ele: f64, + name: &'a str, + desc: &'a str, + icon: &'a str, + link: &'a str, + }, + MoveWaypoint { + lng: f64, + lat: f64, + ele: f64, + }, + Crop { + start: u32, + end: u32, + }, + Split { + at: u32, + }, + Time, + Merge { + type_: MergeType, + }, + Extract, + Elevation { + ele: &'a [f64], + }, + Clean { + bounds: LngLatBounds, + type_: CleanType, + trkpt: bool, + wpt: bool, + }, + // Undo-redo + Undo, + Redo, + // Selection + Select { + file_ids: Vec, + }, + AddSelect { + file_ids: Vec, + }, + SelectAll, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MergeType { + Connect, + Group, +} + +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CleanType { + Inside, + Outside, +} diff --git a/gpx-rs/src/engine/command/mod.rs b/gpx-rs/src/engine/command/mod.rs new file mode 100644 index 000000000..a30f72292 --- /dev/null +++ b/gpx-rs/src/engine/command/mod.rs @@ -0,0 +1,3 @@ +mod command; + +pub use command::*; diff --git a/gpx-rs/src/engine/engine.rs b/gpx-rs/src/engine/engine.rs index 24f74461a..1b4bf4790 100644 --- a/gpx-rs/src/engine/engine.rs +++ b/gpx-rs/src/engine/engine.rs @@ -4,36 +4,22 @@ use std::rc::Rc; use js_sys::{Float64Array, Function, Int32Array}; use uuid::Uuid; -use wasm_bindgen::prelude::*; -use crate::{File, FileId, Selection, Stack, StackEntry, StatisticsBuffer, StatisticsCache, parse}; +use crate::{ + Command, File, FileId, Selection, Stack, StackEntry, StatisticsBuffer, StatisticsCache, parse, +}; extern crate console_error_panic_hook; #[derive(Debug, Default)] -#[wasm_bindgen] pub struct Engine { stack: Stack, selection: Selection, statistics_cache: StatisticsCache, statistics_buffer: StatisticsBuffer, - callback: Option, } -#[wasm_bindgen] impl Engine { - #[wasm_bindgen(constructor)] - pub fn new(callback: Function) -> Self { - console_error_panic_hook::set_once(); - Self { - stack: Default::default(), - selection: Default::default(), - statistics_cache: Default::default(), - statistics_buffer: Default::default(), - callback: Some(callback), - } - } - pub fn total_distance(&self) -> Float64Array { unsafe { Float64Array::view(&self.statistics_buffer.total_distance) } } @@ -74,16 +60,16 @@ impl Engine { unsafe { Float64Array::view(&self.statistics_buffer.slope_segment_distance) } } - pub fn create_file(&mut self, name: &str) { + pub fn create_file(&mut self, name: &str) -> bool { self.event_loop(|entry| { let mut file = File::default(); file.info.name = name.to_string(); entry.insert(file.id, Rc::new(file)); true - }); + }) } - pub fn load_file(&mut self, data: &[u8]) { + pub fn load_file(&mut self, data: &[u8]) -> bool { self.event_loop(|entry| { if let Ok(file) = parse(data) { entry.insert(file.id, Rc::new(file)); @@ -91,10 +77,10 @@ impl Engine { } else { false } - }); + }) } - pub fn delete_file(&mut self, id: &[u8]) { + pub fn delete_file(&mut self, id: &[u8]) -> bool { self.event_loop(|entry| { if let Ok(id) = Uuid::from_slice(id) { let id = FileId(id); @@ -102,10 +88,42 @@ impl Engine { } else { false } - }); + }) } - fn event_loop(&mut self, f: F) + /// Single entry point for every user action coming from the frontend. + pub fn execute(&mut self, command: Command) -> bool { + match command { + Command::New { name } => self.create_file(name), + Command::Load { data } => self.load_file(data), + Command::Delete + | Command::DeleteAll + | Command::Duplicate + | Command::Metadata { .. } + | Command::Style { .. } + | Command::NewTrack + | Command::NewTrackSegment + | Command::Reverse + | Command::Append { .. } + | Command::Replace { .. } + | Command::NewWaypoint { .. } + | Command::MoveWaypoint { .. } + | Command::Crop { .. } + | Command::Split { .. } + | Command::Time + | Command::Merge { .. } + | Command::Extract + | Command::Elevation { .. } + | Command::Clean { .. } + | Command::Undo + | Command::Redo + | Command::Select { .. } + | Command::AddSelect { .. } + | Command::SelectAll => todo!("command not implemented yet"), + } + } + + fn event_loop(&mut self, f: F) -> bool where F: FnOnce(&mut StackEntry) -> bool, { @@ -117,9 +135,9 @@ impl Engine { .statistics_cache .get(self.stack.current(), &self.selection), ); - if let Some(c) = &self.callback { - let _ = c.call0(&JsValue::NULL); - } + true + } else { + false } } } diff --git a/gpx-rs/src/engine/mod.rs b/gpx-rs/src/engine/mod.rs index cad4b73ca..b58143013 100644 --- a/gpx-rs/src/engine/mod.rs +++ b/gpx-rs/src/engine/mod.rs @@ -1,8 +1,10 @@ +mod command; mod derived; mod engine; mod selection; mod state; +pub use command::*; pub use derived::*; pub use engine::*; pub use selection::*; diff --git a/gpx-rs/src/lib.rs b/gpx-rs/src/lib.rs index d6ea07f75..1630bdaf8 100644 --- a/gpx-rs/src/lib.rs +++ b/gpx-rs/src/lib.rs @@ -1,5 +1,7 @@ mod core; mod engine; +mod wasm; pub use core::*; pub use engine::*; +pub use wasm::*; diff --git a/gpx-rs/src/wasm/mod.rs b/gpx-rs/src/wasm/mod.rs new file mode 100644 index 000000000..6dabc001e --- /dev/null +++ b/gpx-rs/src/wasm/mod.rs @@ -0,0 +1,246 @@ +//! Frontier between the SvelteKit frontend and the engine. +//! +//! Conventions, chosen to keep calls cheap: +//! - Coordinates and elevations cross as `Float64Array` (`&[f64]`): one memcpy into linear +//! memory, no per-point calls. `lng`/`lat`/`ele` arrays must have the same length. +//! - File bytes cross as `Uint8Array` (`&[u8]`), strings as `&str`. +//! - File ids cross as one flat `Uint8Array` of concatenated 16-byte UUIDs (no `Array`). +//! - Rectangles cross as four numbers instead of an object. +//! - Every function returns `false` when its arguments are invalid or the command did nothing. +use std::cell::RefCell; + +use wasm_bindgen::prelude::*; + +use crate::{CleanType, Command, Engine, FileId, LngLat, LngLatBounds, MergeType}; + +thread_local! { + static ENGINE: RefCell> = RefCell::new(None); +} + +fn execute(command: Command) -> bool { + ENGINE.with(|engine| match engine.borrow_mut().as_mut() { + Some(engine) => engine.execute(command), + None => false, + }) +} + +/// Decodes concatenated 16-byte UUIDs. +fn file_ids(bytes: &[u8]) -> Option> { + if bytes.len() % 16 != 0 { + return None; + } + bytes + .chunks_exact(16) + .map(|chunk| uuid::Uuid::from_slice(chunk).ok().map(FileId)) + .collect() +} + +fn same_len(a: &[f64], b: &[f64], c: &[f64]) -> bool { + a.len() == b.len() && b.len() == c.len() +} + +#[wasm_bindgen] +pub fn start() { + console_error_panic_hook::set_once(); + ENGINE.with(|engine| { + *engine.borrow_mut() = Some(Engine::default()); + }); +} + +// File commands + +#[wasm_bindgen] +pub fn new_file(name: &str) -> bool { + execute(Command::New { name }) +} + +#[wasm_bindgen] +pub fn load_file(data: &[u8]) -> bool { + execute(Command::Load { data }) +} + +#[wasm_bindgen] +pub fn duplicate() -> bool { + execute(Command::Duplicate) +} + +#[wasm_bindgen] +pub fn delete() -> bool { + execute(Command::Delete) +} + +#[wasm_bindgen] +pub fn delete_all() -> bool { + execute(Command::DeleteAll) +} + +// Edit commands + +#[wasm_bindgen] +pub fn metadata(name: &str, desc: &str) -> bool { + execute(Command::Metadata { name, desc }) +} + +#[wasm_bindgen] +pub fn style(color: Option, opacity: Option, width: Option) -> bool { + execute(Command::Style { + color: color.as_deref(), + opacity, + width, + }) +} + +#[wasm_bindgen] +pub fn new_track() -> bool { + execute(Command::NewTrack) +} + +#[wasm_bindgen] +pub fn new_track_segment() -> bool { + execute(Command::NewTrackSegment) +} + +// Tools + +#[wasm_bindgen] +pub fn reverse() -> bool { + execute(Command::Reverse) +} + +#[wasm_bindgen] +pub fn append(lng: &[f64], lat: &[f64], ele: &[f64]) -> bool { + same_len(lng, lat, ele) && execute(Command::Append { lng, lat, ele }) +} + +#[wasm_bindgen] +pub fn replace(start: u32, end: u32, lng: &[f64], lat: &[f64], ele: &[f64]) -> bool { + start <= end + && same_len(lng, lat, ele) + && execute(Command::Replace { + start, + end, + lng, + lat, + ele, + }) +} + +#[wasm_bindgen] +pub fn new_waypoint( + lng: f64, + lat: f64, + ele: f64, + name: &str, + desc: &str, + icon: &str, + link: &str, +) -> bool { + execute(Command::NewWaypoint { + lng, + lat, + ele, + name, + desc, + icon, + link, + }) +} + +#[wasm_bindgen] +pub fn move_waypoint(lng: f64, lat: f64, ele: f64) -> bool { + execute(Command::MoveWaypoint { lng, lat, ele }) +} + +#[wasm_bindgen] +pub fn crop(start: u32, end: u32) -> bool { + start <= end && execute(Command::Crop { start, end }) +} + +#[wasm_bindgen] +pub fn split(at: u32) -> bool { + execute(Command::Split { at }) +} + +#[wasm_bindgen] +pub fn time() -> bool { + execute(Command::Time) +} + +#[wasm_bindgen] +pub fn merge(type_: MergeType) -> bool { + execute(Command::Merge { type_ }) +} + +#[wasm_bindgen] +pub fn extract() -> bool { + execute(Command::Extract) +} + +#[wasm_bindgen] +pub fn elevation(ele: &[f64]) -> bool { + execute(Command::Elevation { ele }) +} + +#[wasm_bindgen] +pub fn clean( + west: f64, + south: f64, + east: f64, + north: f64, + type_: CleanType, + trkpt: bool, + wpt: bool, +) -> bool { + execute(Command::Clean { + bounds: LngLatBounds { + sw: LngLat { + lng: west, + lat: south, + }, + ne: LngLat { + lng: east, + lat: north, + }, + }, + type_, + trkpt, + wpt, + }) +} + +// Undo-redo + +#[wasm_bindgen] +pub fn undo() -> bool { + execute(Command::Undo) +} + +#[wasm_bindgen] +pub fn redo() -> bool { + execute(Command::Redo) +} + +// Selection + +/// `file_ids`: concatenated 16-byte UUIDs. +#[wasm_bindgen] +pub fn select(file_ids_bytes: &[u8]) -> bool { + match file_ids(file_ids_bytes) { + Some(file_ids) => execute(Command::Select { file_ids }), + None => false, + } +} + +/// `file_ids`: concatenated 16-byte UUIDs. +#[wasm_bindgen] +pub fn add_select(file_ids_bytes: &[u8]) -> bool { + match file_ids(file_ids_bytes) { + Some(file_ids) => execute(Command::AddSelect { file_ids }), + None => false, + } +} + +#[wasm_bindgen] +pub fn select_all() -> bool { + execute(Command::SelectAll) +}