From 17c9af6063c39a9abe86e14b096f84f7eb924027 Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Mon, 15 Aug 2022 23:38:03 +0200 Subject: [PATCH] :sparkles: WIP wasm API --- stackline-cli/Cargo.toml | 2 +- stackline-wasm/Cargo.toml | 10 +- stackline-wasm/README.md | 6 + stackline-wasm/src/lib.rs | 255 +++++++++++++++++++++++++++++++++++- stackline-wasm/src/utils.rs | 16 +++ stackline/Cargo.toml | 3 + stackline/build.rs | 18 ++- stackline/src/pane.rs | 1 + stackline/src/signal.rs | 4 + stackline/src/tile/full.rs | 9 ++ stackline/src/world.rs | 35 ++++- 11 files changed, 350 insertions(+), 9 deletions(-) diff --git a/stackline-cli/Cargo.toml b/stackline-cli/Cargo.toml index 19ba7f1..7afc948 100644 --- a/stackline-cli/Cargo.toml +++ b/stackline-cli/Cargo.toml @@ -7,5 +7,5 @@ description = "A simple runner and editor for Stackline 2" [dependencies] clap = { version = "3.2", features = ["derive"] } -stackline = { path = "../stackline" } +stackline = { path = "../stackline", features = ["time"] } serde_json = "1.0" diff --git a/stackline-wasm/Cargo.toml b/stackline-wasm/Cargo.toml index 49df506..a105efa 100644 --- a/stackline-wasm/Cargo.toml +++ b/stackline-wasm/Cargo.toml @@ -11,8 +11,11 @@ crate-type = ["cdylib", "rlib"] default = ["console_error_panic_hook"] [dependencies] -wasm-bindgen = "0.2.63" -stackline = { path = "../stackline" } +wasm-bindgen = { version = "0.2.63", features = ["serde-serialize"] } +serde_json = "1.0" +serde = { version = "1", features = ["derive"] } + +stackline = { path = "../stackline", features = [], default-features = false } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires @@ -27,5 +30,8 @@ console_error_panic_hook = { version = "0.1.6", optional = true } # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. wee_alloc = { version = "0.4.5", optional = true } +web-sys = { version = "0.3", features = ["console"] } +js-sys = "0.3" + [dev-dependencies] wasm-bindgen-test = "0.3.13" diff --git a/stackline-wasm/README.md b/stackline-wasm/README.md index 51e6e70..3b17b61 100644 --- a/stackline-wasm/README.md +++ b/stackline-wasm/README.md @@ -1,3 +1,9 @@ # stackline-wasm WASM bindings for stackline. + +To build, run + +```sh +wasm-pack build stackline-wasm --target web +``` diff --git a/stackline-wasm/src/lib.rs b/stackline-wasm/src/lib.rs index 7206600..6ea76cf 100644 --- a/stackline-wasm/src/lib.rs +++ b/stackline-wasm/src/lib.rs @@ -1,6 +1,16 @@ mod utils; +use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; +use js_sys::Function; + +use stackline::pane::Pane as SLPane; +use stackline::signal::Signal as SLSignal; +use stackline::signal::Value; +use stackline::tile::AnyTile; +use stackline::tile::FullTile as SLFullTile; +use stackline::utils::Direction; +use stackline::world::World as SLWorld; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. @@ -14,6 +24,247 @@ extern "C" { } #[wasm_bindgen] -pub fn greet() { - alert("Hello, stackline2-wasm!"); +pub struct World(SLWorld); + +#[wasm_bindgen(start)] +pub fn set_panic() { + utils::set_panic_hook(); +} + +#[wasm_bindgen] +pub fn available_tiles() -> Vec { + AnyTile::available().iter().map(|name| { + JsValue::from_str(name) + }).collect() +} + +#[wasm_bindgen] +impl World { + /// Creates a new World instance + pub fn new() -> Self { + Self(SLWorld::new()) + } + + /// Initializes the World, making it ready to run + pub fn init(&mut self) { + self.0.init(); + } + + pub fn set_blink_duration(&mut self, blink_duration: f64) { + use std::time::Duration; + self.0 + .set_blink_duration(Duration::from_secs_f64(blink_duration)); + } + + #[allow(non_snake_case)] + pub fn toString(&self) -> String { + format!("{:#}", self.0) + } + + /// NOTE: We have to [`Clone`] the FullTile + pub fn get(&self, x: i32, y: i32) -> Option { + self.0.get((x, y)).map(|tile| FullTile((*tile).clone())) + } + + pub fn set(&mut self, x: i32, y: i32, tile: &FullTile) { + if let Some(tile_ref) = self.0.get_mut((x, y)) { + *tile_ref = tile.0.clone(); + } + } + + pub fn get_pane(&self, name: String) -> Option { + self.0.get_pane(&name).map(|pane| Pane(pane.clone())) + } + + pub fn set_pane(&mut self, name: String, pane: Pane) { + self.0.set_pane(name, pane.0); + } +} + +#[wasm_bindgen] +pub struct Pane(SLPane); + +#[wasm_bindgen] +impl Pane { + pub fn empty(width: usize, height: usize) -> Option { + SLPane::empty(width, height).map(|pane| Self(pane)) + } + + #[allow(non_snake_case)] + pub fn toString(&self) -> String { + format!("{:#?}", self.0) + } + + #[wasm_bindgen(getter)] + pub fn width(&self) -> usize { + self.0.width().get() + } + + #[wasm_bindgen(getter)] + pub fn height(&self) -> usize { + self.0.height().get() + } + + #[wasm_bindgen(getter)] + pub fn position(&self) -> Vec { + let (x, y) = self.0.position(); + vec![x, y] + } + + #[wasm_bindgen(setter)] + pub fn set_position(&mut self, position: &[i32]) { + if let [x, y] = position[..] { + self.0.set_position((x, y)); + } + } +} + +#[derive(Clone, Debug)] +#[wasm_bindgen] +pub struct FullTile(SLFullTile); + +#[wasm_bindgen] +impl FullTile { + pub fn empty() -> Self { + FullTile(SLFullTile::new(None)) + } + + #[wasm_bindgen(constructor)] + pub fn new(name: &str) -> Option { + AnyTile::new(name).map(|tile| FullTile(SLFullTile::new(Some(tile)))) + } + + #[allow(non_snake_case)] + pub fn toString(&self) -> String { + format!("{:#?}", self.0) + } + + #[wasm_bindgen(getter)] + pub fn signal(&self) -> Option { + self.0.signal().map(|signal| Signal(signal.clone())) + } + + #[wasm_bindgen(setter)] + pub fn set_signal(&mut self, signal: Option) { + self.0.set_signal(signal.map(|s| s.0)); + } + + #[wasm_bindgen(getter)] + pub fn tile(&self) -> JsValue { + self.0.get().map(|tile| { + JsValue::from_serde(tile).map_err(|err| { + err!("Error while serializing AnyTile: {}", err); + }).ok() + }).flatten().unwrap_or(JsValue::UNDEFINED) + } + + #[wasm_bindgen(setter)] + pub fn set_tile(&mut self, tile: JsValue) { + if tile.is_null() || tile.is_undefined() { + self.0.set(None); + } else { + match tile.into_serde::() { + Ok(tile) => self.0.set(Some(tile)), + Err(err) => err!("Error while deserializing AnyTile: {}", err), + } + } + } + + pub fn map_tile(&mut self, callback: &Function) { + let res = callback.call1(&JsValue::NULL, &self.tile()).expect("Error while calling javascript callback"); + self.set_tile(res); + } +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(untagged)] +enum UntaggedValue { + Number(f64), + String(String), +} + +impl From<&'_ Value> for UntaggedValue { + fn from(value: &'_ Value) -> Self { + match value { + Value::Number(x) => Self::Number(*x), + Value::String(x) => Self::String(x.clone()), + } + } +} + +impl From for UntaggedValue { + fn from(value: Value) -> Self { + match value { + Value::Number(x) => Self::Number(x), + Value::String(x) => Self::String(x), + } + } +} + +impl From for Value { + fn from(value: UntaggedValue) -> Self { + match value { + UntaggedValue::Number(x) => Self::Number(x), + UntaggedValue::String(x) => Self::String(x), + } + } +} + +#[derive(Clone, Debug)] +#[wasm_bindgen] +pub struct Signal(SLSignal); + +#[wasm_bindgen] +impl Signal { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self(SLSignal::empty(Direction::Up)) + } + + /// Returns a read-only array + #[wasm_bindgen(getter)] + pub fn stack(&self) -> JsValue { + JsValue::from_serde( + &self + .0 + .stack() + .iter() + .map(>::from) + .collect::>(), + ) + .unwrap() + } + + #[wasm_bindgen(getter)] + pub fn direction(&self) -> u8 { + match self.0.direction() { + Direction::Up => 0, + Direction::Right => 1, + Direction::Down => 2, + Direction::Left => 3, + } + } + + #[wasm_bindgen(setter)] + pub fn set_direction(&mut self, direction: u8) { + let direction = match direction { + 0 => Direction::Up, + 1 => Direction::Right, + 2 => Direction::Down, + 3 => Direction::Left, + _ => return, + }; + + self.0.set_direction(direction); + } + + pub fn push(&mut self, value: JsValue) { + if let Some(num) = value.as_f64() { + self.0.push(Value::Number(num)); + } else if let Some(string) = value.as_string() { + self.0.push(Value::String(string)); + } else { + panic!("Invalid value: expected number or string, got {:?}", value); + } + } } diff --git a/stackline-wasm/src/utils.rs b/stackline-wasm/src/utils.rs index b1d7929..7f7f788 100644 --- a/stackline-wasm/src/utils.rs +++ b/stackline-wasm/src/utils.rs @@ -8,3 +8,19 @@ pub fn set_panic_hook() { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); } + +// A macro to provide `println!(..)`-style syntax for `console.log` logging. +#[macro_export] +macro_rules! log { + ( $( $t:tt )* ) => { + web_sys::console::log_1(&format!( $( $t )* ).into()) + } +} + +// A macro to provide `println!(..)`-style syntax for `console.error` logging. +#[macro_export] +macro_rules! err { + ( $( $t:tt )* ) => { + web_sys::console::error_1(&format!( $( $t )* ).into()) + } +} diff --git a/stackline/Cargo.toml b/stackline/Cargo.toml index 5d04afd..5a9355a 100755 --- a/stackline/Cargo.toml +++ b/stackline/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +time = [] + [dependencies] palette = "0.6" enum_dispatch = "0.3" diff --git a/stackline/build.rs b/stackline/build.rs index de1688d..cf39bbd 100644 --- a/stackline/build.rs +++ b/stackline/build.rs @@ -142,7 +142,23 @@ fn generate_code(files: Vec<(PathBuf, Vec)>, names: Vec) -> Stri } res += " _ => None\n"; - res += " }\n }\n}\n"; + res += " }\n }\n"; + + writeln!( + res, + " pub fn available() -> [&'static str; {}] {{\n [", + names.len() + ).unwrap(); + for name in names.iter() { + writeln!( + res, + " \"{}\",", + name + ) + .unwrap(); + } + res += " ]\n"; + res += " }\n}\n\n"; for name in names { // impl TryInto<&T> for &AnyTile diff --git a/stackline/src/pane.rs b/stackline/src/pane.rs index ed19789..df6eafb 100644 --- a/stackline/src/pane.rs +++ b/stackline/src/pane.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use veccell::{VecCell, VecRef, VecRefMut}; #[derive(Debug, Serialize, Deserialize)] +#[derive(Clone)] pub struct Pane { tiles: VecCell, width: NonZeroUsize, diff --git a/stackline/src/signal.rs b/stackline/src/signal.rs index 345b1c2..4d78d56 100644 --- a/stackline/src/signal.rs +++ b/stackline/src/signal.rs @@ -140,6 +140,10 @@ impl Signal { self.direction } + pub fn set_direction(&mut self, direction: Direction) { + self.direction = direction; + } + /// Pushes a value onto the stack of the signal. /// Signals are pushed on top of the stack and can be [`pop`ped](Signal::pop) in reverse order. /// diff --git a/stackline/src/tile/full.rs b/stackline/src/tile/full.rs index a53d667..598007a 100644 --- a/stackline/src/tile/full.rs +++ b/stackline/src/tile/full.rs @@ -64,6 +64,15 @@ impl FullTile { self.cell.as_mut() } + #[inline] + pub fn set(&mut self, tile: Option) { + if tile.is_none() { + self.signal = None; + self.state = State::Idle; + } + self.cell = tile; + } + /// Returns the signal of this tile #[inline] pub fn signal(&self) -> Option<&Signal> { diff --git a/stackline/src/world.rs b/stackline/src/world.rs index 5afd0d1..715b09c 100644 --- a/stackline/src/world.rs +++ b/stackline/src/world.rs @@ -1,16 +1,25 @@ use super::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::time::{Duration, Instant}; +use std::time::Duration; use veccell::VecRef; +#[cfg(feature = "time")] +use std::time::Instant; + #[derive(Debug, Serialize, Deserialize)] pub struct World { panes: HashMap, #[serde(default = "Instant::now")] #[serde(skip)] + #[cfg(feature = "time")] blink_start: Instant, + + #[serde(skip)] + #[cfg(not(feature = "time"))] + blink_duration: Duration, + #[serde(default)] blink_speed: Duration, } @@ -20,7 +29,12 @@ impl World { Self { panes: HashMap::new(), + #[cfg(feature = "time")] blink_start: Instant::now(), + + #[cfg(not(feature = "time"))] + blink_duration: Duration::default(), + blink_speed: Duration::default(), } } @@ -200,7 +214,7 @@ impl World { } pub fn draw(&self, dx: i32, dy: i32, surface: &mut TextSurface) { - let blink = Blink::new(Instant::now() - self.blink_start, self.blink_speed); + let blink = self.blink(); for pane in self.panes.values() { pane.draw(dx, dy, surface, blink.clone()); } @@ -224,6 +238,21 @@ impl World { pub fn set_blink(&mut self, blink_speed: Duration) { self.blink_speed = blink_speed; } + + #[cfg(not(feature = "time"))] + pub fn set_blink_duration(&mut self, blink_duration: Duration) { + self.blink_duration = blink_duration; + } + + #[cfg(feature = "time")] + fn blink(&self) -> Blink { + Blink::new(Instant::now() - self.blink_start, self.blink_speed) + } + + #[cfg(not(feature = "time"))] + fn blink(&self) -> Blink { + Blink::new(self.blink_duration, self.blink_speed) + } } impl std::fmt::Display for World { @@ -233,7 +262,7 @@ impl std::fmt::Display for World { let height = (bounds.3 - bounds.2) as usize; let mut surface = TextSurface::new(width, height); - let blink = Blink::new(Instant::now() - self.blink_start, self.blink_speed); + let blink = self.blink(); for pane in self.panes.values() { pane.draw(bounds.0, bounds.2, &mut surface, blink.clone());