hand.rs
19.4 kB · rust · 672 lines
1#![allow(dead_code)]23use mrlyrs::core::cell::Cell;4use mrlyrs::core::colors::Color;5use mrlyrs::core::error::Error;6use mrlyrs::core::rng::Rng as Stream;7use mrlyrs::core::tensor::{Dtype, Tensor};8use mrlyrs::core::Json;9use mrlyrs::math::bang::Code;10use mrlyrs::math::cell::models::{Cell2d, Cell3d, CellNd};11use mrlyrs::math::six::{Cell6d, Orientation, Projection};12use serde::de::DeserializeOwned;13use serde::Serialize;14use std::collections::HashMap;15use std::fmt::Display;16use std::hash::Hash;17use std::str::FromStr;18use wasm_bindgen::prelude::*;19use wasm_bindgen::JsCast;2021// ERRORS2223pub fn throw(error: Error) -> JsValue {24 js_sys::Error::new(&error.to_string()).into()25}2627pub fn refuse(message: &str) -> JsValue {28 js_sys::Error::new(message).into()29}3031// SERDE3233fn serializer() -> serde_wasm_bindgen::Serializer {34 serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true)35}3637pub fn to_js<T: Serialize + ?Sized>(value: &T) -> Result<JsValue, JsValue> {38 value39 .serialize(&serializer())40 .map_err(|error| refuse(&error.to_string()))41}4243pub fn from_js<T: DeserializeOwned>(value: &JsValue) -> Result<T, JsValue> {44 serde_wasm_bindgen::from_value(value.clone()).map_err(|error| refuse(&error.to_string()))45}4647pub fn json_to_js(value: &Json) -> Result<JsValue, JsValue> {48 to_js(value)49}5051pub fn json_from_js(value: &JsValue) -> Result<Json, JsValue> {52 from_js(value)53}5455pub fn plain(value: &JsValue) -> Result<JsValue, JsValue> {56 if !value.is_object() {57 return Ok(value.clone());58 }59 match field(value, "toJSON")?.dyn_ref::<js_sys::Function>() {60 Some(to_json) => to_json.call0(value),61 None => Ok(value.clone()),62 }63}6465// FIELDS6667fn field(value: &JsValue, name: &str) -> Result<JsValue, JsValue> {68 js_sys::Reflect::get(value, &JsValue::from_str(name))69}7071fn put(object: &js_sys::Object, name: &str, value: JsValue) -> Result<(), JsValue> {72 js_sys::Reflect::set(object, &JsValue::from_str(name), &value)?;73 Ok(())74}7576fn drop_field(object: &js_sys::Object, name: &str) -> Result<(), JsValue> {77 js_sys::Reflect::delete_property(object, &JsValue::from_str(name))?;78 Ok(())79}8081fn object(value: &JsValue) -> Result<&js_sys::Object, JsValue> {82 value83 .dyn_ref::<js_sys::Object>()84 .ok_or_else(|| refuse("an object was wanted here."))85}8687fn missing(value: &JsValue) -> bool {88 value.is_undefined() || value.is_null()89}9091// SCALARS9293pub fn number_from_js(value: &JsValue) -> Result<f64, JsValue> {94 value95 .as_f64()96 .ok_or_else(|| refuse("a number was wanted here."))97}9899pub fn bool_from_js(value: &JsValue) -> Result<bool, JsValue> {100 value101 .as_bool()102 .ok_or_else(|| refuse("a boolean was wanted here."))103}104105pub fn string_from_js(value: &JsValue) -> Result<String, JsValue> {106 value107 .as_string()108 .ok_or_else(|| refuse("a string was wanted here."))109}110111pub fn char_from_js(value: &JsValue) -> Result<char, JsValue> {112 let text = string_from_js(value)?;113 let mut chars = text.chars();114 match (chars.next(), chars.next()) {115 (Some(c), None) => Ok(c),116 _ => Err(refuse("a one-character string was wanted here.")),117 }118}119120fn decimal(value: &JsValue) -> Option<String> {121 if let Some(text) = value.as_string() {122 return Some(text.trim().to_string());123 }124 if value.is_bigint() {125 return value126 .unchecked_ref::<js_sys::BigInt>()127 .to_string(10)128 .ok()129 .map(String::from);130 }131 let number = value.as_f64()?;132 if number.is_finite() && number.fract() == 0.0 {133 Some(format!("{number:.0}"))134 } else {135 None136 }137}138139fn wide_from_js<T: FromStr>(value: &JsValue, name: &str) -> Result<T, JsValue> {140 decimal(value)141 .and_then(|text| text.parse().ok())142 .ok_or_else(|| refuse(&format!("{name} wants a decimal string, a whole number or a bigint.")))143}144145pub fn u128_to_js(value: u128) -> String {146 value.to_string()147}148149pub fn u128_from_js(value: &JsValue) -> Result<u128, JsValue> {150 wide_from_js(value, "a u128")151}152153pub fn i128_from_js(value: &JsValue) -> Result<i128, JsValue> {154 wide_from_js(value, "an i128")155}156157pub fn u64_from_js(value: &JsValue) -> Result<u64, JsValue> {158 wide_from_js(value, "a u64")159}160161pub fn i64_from_js(value: &JsValue) -> Result<i64, JsValue> {162 wide_from_js(value, "an i64")163}164165pub fn code_to_js(code: Code) -> String {166 code.get().to_string()167}168169pub fn code_from_js(value: &JsValue) -> Result<Code, JsValue> {170 Ok(Code::from(u128_from_js(value)?))171}172173// LISTS174175pub fn list_from_js<T>(176 value: &JsValue,177 item: impl Fn(&JsValue) -> Result<T, JsValue>,178) -> Result<Vec<T>, JsValue> {179 if let Some(array) = value.dyn_ref::<js_sys::Array>() {180 return array.iter().map(|x| item(&x)).collect();181 }182 match js_sys::try_iter(value)? {183 Some(iter) => iter.map(|x| item(&x?)).collect(),184 None => Err(refuse("a list was wanted here.")),185 }186}187188pub fn list_to_js<T>(189 items: impl IntoIterator<Item = T>,190 item: impl Fn(T) -> Result<JsValue, JsValue>,191) -> Result<JsValue, JsValue> {192 let out = js_sys::Array::new();193 for x in items {194 out.push(&item(x)?);195 }196 Ok(out.into())197}198199pub fn option_from_js<T>(200 value: &JsValue,201 item: impl Fn(&JsValue) -> Result<T, JsValue>,202) -> Result<Option<T>, JsValue> {203 if missing(value) {204 Ok(None)205 } else {206 item(value).map(Some)207 }208}209210pub fn option_to_js<T>(211 value: Option<T>,212 item: impl Fn(T) -> Result<JsValue, JsValue>,213) -> Result<JsValue, JsValue> {214 match value {215 Some(x) => item(x),216 None => Ok(JsValue::UNDEFINED),217 }218}219220pub fn item(value: &JsValue, index: u32) -> Result<JsValue, JsValue> {221 let array = value222 .dyn_ref::<js_sys::Array>()223 .ok_or_else(|| refuse("an array was wanted here."))?;224 if index >= array.length() {225 return Err(refuse(&format!("an array of at least {} was wanted here.", index + 1)));226 }227 Ok(array.get(index))228}229230pub fn first(value: &JsValue) -> Result<JsValue, JsValue> {231 item(value, 0)232}233234pub fn array_from_js<T, const N: usize>(235 value: &JsValue,236 item: impl Fn(&JsValue) -> Result<T, JsValue>,237) -> Result<[T; N], JsValue> {238 list_from_js(value, item)?239 .try_into()240 .map_err(|_| refuse(&format!("a list of {N} was wanted here.")))241}242243pub fn tuple_to_js(items: &[JsValue]) -> JsValue {244 items.iter().collect::<js_sys::Array>().into()245}246247pub fn map_from_js<K: FromStr + Eq + Hash, V>(248 value: &JsValue,249 each: impl Fn(&JsValue) -> Result<V, JsValue>,250) -> Result<HashMap<K, V>, JsValue> {251 let mut out = HashMap::new();252 for entry in js_sys::Object::entries(object(value)?).iter() {253 let key = string_from_js(&item(&entry, 0)?)?;254 let key = key255 .parse()256 .map_err(|_| refuse(&format!("the key {key:?} does not read.")))?;257 out.insert(key, each(&item(&entry, 1)?)?);258 }259 Ok(out)260}261262pub fn map_to_js<'a, K: Display + 'a, V: 'a>(263 entries: impl IntoIterator<Item = (&'a K, &'a V)>,264 item: impl Fn(&V) -> Result<JsValue, JsValue>,265) -> Result<JsValue, JsValue> {266 let out = js_sys::Object::new();267 for (key, value) in entries {268 put(&out, &key.to_string(), item(value)?)?;269 }270 Ok(out.into())271}272273pub trait Typed {274 fn typed(&self) -> JsValue;275}276277macro_rules! typed_arrays {278 ($($scalar:ty => $array:ident),* $(,)?) => {279 $(280 impl Typed for [$scalar] {281 fn typed(&self) -> JsValue {282 js_sys::$array::from(self).into()283 }284 }285 )*286 };287}288289typed_arrays! {290 u8 => Uint8Array,291 u16 => Uint16Array,292 u32 => Uint32Array,293 i8 => Int8Array,294 i16 => Int16Array,295 i32 => Int32Array,296 f32 => Float32Array,297 f64 => Float64Array,298 u64 => BigUint64Array,299 i64 => BigInt64Array,300}301302impl Typed for [usize] {303 fn typed(&self) -> JsValue {304 let narrow: Vec<u32> = self.iter().map(|&n| n as u32).collect();305 js_sys::Uint32Array::from(&narrow[..]).into()306 }307}308309pub fn typed<T: Typed + ?Sized>(items: &T) -> JsValue {310 items.typed()311}312313// SHAPES314315pub fn shape_to_js(shape: &[usize]) -> JsValue {316 shape317 .iter()318 .map(|&length| JsValue::from_f64(length as f64))319 .collect::<js_sys::Array>()320 .into()321}322323pub fn shape_from_js(value: &JsValue) -> Result<Vec<usize>, JsValue> {324 let array = value325 .dyn_ref::<js_sys::Array>()326 .ok_or_else(|| refuse("a shape wants an array of lengths."))?;327 array328 .iter()329 .map(|length| {330 length331 .as_f64()332 .filter(|n| n.is_finite() && *n >= 0.0)333 .map(|n| n as usize)334 .ok_or_else(|| refuse("a shape wants whole lengths at or above zero."))335 })336 .collect()337}338339// BUFFERS340341pub fn bytes_to_js(bytes: &[u8]) -> JsValue {342 js_sys::Uint8Array::from(bytes).into()343}344345pub fn bytes_from_js(value: &JsValue) -> Result<Vec<u8>, JsValue> {346 if let Some(array) = value.dyn_ref::<js_sys::Uint8Array>() {347 return Ok(array.to_vec());348 }349 if let Some(array) = value.dyn_ref::<js_sys::Uint8ClampedArray>() {350 return Ok(array.to_vec());351 }352 if let Some(array) = value.dyn_ref::<js_sys::Array>() {353 return array354 .iter()355 .map(|item| {356 item.as_f64()357 .filter(|n| (0.0..=255.0).contains(n))358 .map(|n| n as u8)359 .ok_or_else(|| refuse("a byte array wants numbers from zero to 255."))360 })361 .collect();362 }363 Err(refuse("a Uint8Array or an array of bytes was wanted here."))364}365366fn data_to_js(tensor: &Tensor) -> Result<JsValue, JsValue> {367 let data = match tensor.dtype() {368 Dtype::U8 => js_sys::Uint8Array::from(tensor.bytes().map_err(throw)?).into(),369 Dtype::U16 => js_sys::Uint16Array::from(tensor.u16s().map_err(throw)?).into(),370 Dtype::U32 => js_sys::Uint32Array::from(tensor.u32s().map_err(throw)?).into(),371 Dtype::I32 => js_sys::Int32Array::from(tensor.i32s().map_err(throw)?).into(),372 };373 Ok(data)374}375376fn data_from_js(value: &JsValue, shape: Vec<usize>) -> Result<Tensor, JsValue> {377 if let Some(array) = value.dyn_ref::<js_sys::Uint16Array>() {378 return Tensor::u16(array.to_vec(), shape).map_err(throw);379 }380 if let Some(array) = value.dyn_ref::<js_sys::Uint32Array>() {381 return Tensor::u32(array.to_vec(), shape).map_err(throw);382 }383 if let Some(array) = value.dyn_ref::<js_sys::Int32Array>() {384 return Tensor::i32(array.to_vec(), shape).map_err(throw);385 }386 Tensor::u8(bytes_from_js(value)?, shape).map_err(throw)387}388389// TENSORS390391fn fill_tensor(out: &js_sys::Object, tensor: &Tensor) -> Result<(), JsValue> {392 put(out, "shape", shape_to_js(&tensor.shape))?;393 put(out, "data", data_to_js(tensor)?)394}395396pub fn tensor_to_js(tensor: &Tensor) -> Result<JsValue, JsValue> {397 let out = js_sys::Object::new();398 fill_tensor(&out, tensor)?;399 Ok(out.into())400}401402pub fn tensor_into_js(value: &JsValue, tensor: &Tensor) -> Result<(), JsValue> {403 fill_tensor(object(value)?, tensor)404}405406pub fn tensor_from_js(value: &JsValue) -> Result<Tensor, JsValue> {407 let shape = shape_from_js(&field(value, "shape")?)?;408 data_from_js(&field(value, "data")?, shape)409}410411// CELLS412413fn fill_cell(out: &js_sys::Object, cell: &Cell) -> Result<(), JsValue> {414 put(out, "shape", shape_to_js(&cell.types.shape))?;415 put(out, "types", data_to_js(&cell.types)?)?;416 match &cell.colors {417 Some(colors) => {418 let flat: Vec<u8> = colors.iter().flatten().copied().collect();419 put(out, "colors", bytes_to_js(&flat))?;420 }421 None => drop_field(out, "colors")?,422 }423 match &cell.tags {424 Some(tags) => put(out, "tags", data_to_js(tags)?),425 None => drop_field(out, "tags"),426 }427}428429pub fn cell_to_js(cell: &Cell) -> Result<JsValue, JsValue> {430 let out = js_sys::Object::new();431 fill_cell(&out, cell)?;432 Ok(out.into())433}434435pub fn cell_into_js(value: &JsValue, cell: &Cell) -> Result<(), JsValue> {436 fill_cell(object(value)?, cell)437}438439pub fn cell_from_js(value: &JsValue) -> Result<Cell, JsValue> {440 let shape = shape_from_js(&field(value, "shape")?)?;441 let types = data_from_js(&field(value, "types")?, shape.clone())?;442 let painted = field(value, "colors")?;443 let colors = if missing(&painted) {444 None445 } else {446 let flat = bytes_from_js(&painted)?;447 if flat.len() != types.size() * 4 {448 return Err(refuse("cell colors want four bytes a cell."));449 }450 Some(451 flat.chunks_exact(4)452 .map(|rgba| [rgba[0], rgba[1], rgba[2], rgba[3]])453 .collect(),454 )455 };456 let tagged = field(value, "tags")?;457 let tags = if missing(&tagged) {458 None459 } else {460 Some(data_from_js(&tagged, shape)?)461 };462 Ok(Cell {463 types,464 colors,465 tags,466 })467}468469pub fn cell_rank(value: &JsValue) -> Result<usize, JsValue> {470 Ok(shape_from_js(&field(value, "shape")?)?.len())471}472473fn cell_nd_from_js<const N: usize>(value: &JsValue) -> Result<CellNd<N>, JsValue> {474 let cell = cell_from_js(value)?;475 if cell.types.shape.len() != N {476 return Err(refuse(&format!(477 "a {N}d cell wants a shape of {N} lengths."478 )));479 }480 Ok(CellNd { cell })481}482483pub fn cell2d_to_js(cell: &Cell2d) -> Result<JsValue, JsValue> {484 cell_to_js(&cell.cell)485}486487pub fn cell2d_from_js(value: &JsValue) -> Result<Cell2d, JsValue> {488 cell_nd_from_js(value)489}490491pub fn cell3d_to_js(cell: &Cell3d) -> Result<JsValue, JsValue> {492 cell_to_js(&cell.cell)493}494495pub fn cell3d_from_js(value: &JsValue) -> Result<Cell3d, JsValue> {496 cell_nd_from_js(value)497}498499pub fn cell6d_to_js(hex: &Cell6d) -> Result<JsValue, JsValue> {500 let out = js_sys::Object::new();501 put(&out, "cell", cell2d_to_js(&hex.cell)?)?;502 put(&out, "projection", to_js(&hex.projection)?)?;503 put(&out, "orientation", to_js(&hex.orientation)?)?;504 put(&out, "start", JsValue::from_f64(hex.start as f64))?;505 Ok(out.into())506}507508pub fn cell6d_from_js(value: &JsValue) -> Result<Cell6d, JsValue> {509 let cell = cell2d_from_js(&field(value, "cell")?)?;510 let projection: Projection = from_js(&field(value, "projection")?)?;511 let orientation: Orientation = from_js(&field(value, "orientation")?)?;512 let start = number_from_js(&field(value, "start")?)? as u8;513 Ok(Cell6d::new(cell, projection, orientation, start))514}515516// COLORS517518pub fn color_to_js(color: Color) -> JsValue {519 [color.r, color.g, color.b, color.a]520 .iter()521 .map(|&channel| JsValue::from_f64(channel as f64))522 .collect::<js_sys::Array>()523 .into()524}525526pub fn color_from_js(value: &JsValue) -> Result<Color, JsValue> {527 let channels = bytes_from_js(value)?;528 match channels.len() {529 3 => Ok(Color::rgb(channels[0], channels[1], channels[2])),530 4 => Ok(Color::rgba(531 channels[0],532 channels[1],533 channels[2],534 channels[3],535 )),536 _ => Err(refuse("a color wants three or four bytes.")),537 }538}539540pub fn colors_to_js(colors: &[Color]) -> JsValue {541 colors542 .iter()543 .map(|&color| color_to_js(color))544 .collect::<js_sys::Array>()545 .into()546}547548pub fn colors_from_js(value: &JsValue) -> Result<Vec<Color>, JsValue> {549 let array = value550 .dyn_ref::<js_sys::Array>()551 .ok_or_else(|| refuse("a palette wants an array of colors."))?;552 array.iter().map(|color| color_from_js(&color)).collect()553}554555// CHANCE556557/// A seeded random stream, opened from a number or a bigint seed.558#[wasm_bindgen]559pub struct Rng {560 stream: Stream,561}562563#[wasm_bindgen]564impl Rng {565 /// Opens the stream on the seed.566 #[wasm_bindgen(constructor)]567 pub fn new(seed: JsValue) -> Result<Rng, JsValue> {568 Ok(Rng {569 stream: Stream::new(u64_from_js(&seed)?),570 })571 }572 /// Draws a float at or above zero and below one.573 pub fn unit(&mut self) -> f64 {574 self.stream.unit()575 }576 /// Draws an integer below n, or zero when n is zero.577 pub fn below(&mut self, n: usize) -> usize {578 self.stream.below(n)579 }580 /// Draws an integer between lo and hi inclusive, or lo when hi is not above lo.581 pub fn range(&mut self, lo: f64, hi: f64) -> f64 {582 self.stream.range(lo as i64, hi as i64) as f64583 }584 /// Draws a fair coin flip.585 pub fn boolean(&mut self) -> bool {586 self.stream.boolean()587 }588 /// Returns true with probability p.589 pub fn chance(&mut self, p: f64) -> bool {590 self.stream.chance(p)591 }592 /// Draws amount distinct indices below length, or every index when amount is larger.593 pub fn sample_indices(&mut self, length: usize, amount: usize) -> Vec<u32> {594 self.stream595 .sample_indices(length, amount)596 .into_iter()597 .map(|index| index as u32)598 .collect()599 }600 /// Draws one item of the array, the same draw as Rust's choice.601 pub fn choice(&mut self, items: JsValue) -> Result<JsValue, JsValue> {602 let indices: Vec<u32> = (0..length(&items)?).collect();603 let index = *self.stream.choice(&indices).map_err(throw)?;604 js_sys::Reflect::get_u32(&items, index)605 }606 /// Shuffles the array in place, the same permutation as Rust's shuffle.607 pub fn shuffle(&mut self, items: JsValue) -> Result<(), JsValue> {608 let mut order: Vec<u32> = (0..length(&items)?).collect();609 self.stream.shuffle(&mut order);610 let moved = order611 .into_iter()612 .map(|index| js_sys::Reflect::get_u32(&items, index))613 .collect::<Result<Vec<JsValue>, JsValue>>()?;614 for (at, item) in (0..).zip(moved) {615 js_sys::Reflect::set_u32(&items, at, &item)?;616 }617 Ok(())618 }619 /// Prints the stream state, the way an optional stream crosses in.620 #[wasm_bindgen(js_name = "__state")]621 pub fn state(&self) -> Result<String, JsValue> {622 serde_json::to_string(&self.stream).map_err(|error| refuse(&error.to_string()))623 }624 /// Reads a stream state back, the way an optional stream crosses out.625 #[wasm_bindgen(js_name = "__restore")]626 pub fn restore(&mut self, state: &str) -> Result<(), JsValue> {627 self.stream = serde_json::from_str(state).map_err(|error| refuse(&error.to_string()))?;628 Ok(())629 }630}631632impl Rng {633 pub fn wrap(stream: Stream) -> Rng {634 Rng { stream }635 }636 pub fn stream(&mut self) -> &mut Stream {637 &mut self.stream638 }639}640641fn length(items: &JsValue) -> Result<u32, JsValue> {642 if !items.is_object() {643 return Err(refuse("an array was wanted here."));644 }645 field(items, "length")?646 .as_f64()647 .filter(|n| n.is_finite() && *n >= 0.0)648 .map(|n| n as u32)649 .ok_or_else(|| refuse("an array was wanted here."))650}651652fn method(value: &JsValue, name: &str) -> Result<js_sys::Function, JsValue> {653 field(value, name)?654 .dyn_into::<js_sys::Function>()655 .map_err(|_| refuse("an Rng was wanted here."))656}657658pub fn stream_from_js(value: &JsValue) -> Result<Option<Stream>, JsValue> {659 if missing(value) {660 return Ok(None);661 }662 let state = method(value, "__state")?.call0(value)?;663 serde_json::from_str(&string_from_js(&state)?)664 .map(Some)665 .map_err(|error| refuse(&error.to_string()))666}667668pub fn stream_to_js(value: &JsValue, stream: &Stream) -> Result<(), JsValue> {669 let state = serde_json::to_string(stream).map_err(|error| refuse(&error.to_string()))?;670 method(value, "__restore")?.call1(value, &JsValue::from_str(&state))?;671 Ok(())672}