#![cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Serialize, Deserialize, tsify::Tsify)]
#[serde(deny_unknown_fields)]
struct Point2D {
x: f64,
y: f64,
}
#[derive(Serialize, Deserialize, tsify::Tsify)]
struct Aabb {
min: Point2D,
max: Point2D,
}
impl Point2D {
fn to_tuple(&self) -> (f64, f64) {
(self.x, self.y)
}
fn from_tuple(t: (f64, f64)) -> Self {
Self { x: t.0, y: t.1 }
}
}
fn refuse(code: &str, parameter: &str, message: String) -> JsValue {
let err = js_sys::Error::new(&message);
let _ = js_sys::Reflect::set(&err, &"code".into(), &code.into());
let _ = js_sys::Reflect::set(&err, &"parameter".into(), ¶meter.into());
err.into()
}
fn refuse_non_finite(found: &NonFinite) -> JsValue {
let err = refuse("value_not_finite", &found.parameter, found.message());
let index = found
.index
.map_or(JsValue::NULL, |i| JsValue::from(i as u32));
let _ = js_sys::Reflect::set(&err, &"index".into(), &index);
err
}
fn finite_arg(value: f64, parameter: &str) -> Result<f64, JsValue> {
if value.is_finite() {
Ok(value)
} else {
Err(refuse_non_finite(&NonFinite {
parameter: parameter.to_string(),
index: None,
value,
}))
}
}
struct NonFinite {
parameter: String,
index: Option<usize>,
value: f64,
}
impl NonFinite {
fn message(&self) -> String {
let at = match self.index {
Some(i) => format!("{}[{i}]", self.parameter),
None => self.parameter.clone(),
};
let got = if self.value.is_nan() {
"NaN"
} else if self.value > 0.0 {
"Infinity"
} else {
"-Infinity"
};
format!("{at}: expected a finite number, got {got}")
}
}
fn find_non_finite(value: &JsValue, parameter: &str, allow_nan: bool) -> Option<NonFinite> {
let refused = |n: f64| !n.is_finite() && !(allow_nan && n.is_nan());
let found = |index: Option<usize>, value: f64| NonFinite {
parameter: parameter.to_string(),
index,
value,
};
if let Some(n) = value.as_f64() {
return refused(n).then(|| found(None, n));
}
if !value.is_object() {
return None;
}
if let Ok(Some(items)) = js_sys::try_iter(value) {
for (i, item) in items.enumerate() {
let item = item.ok()?;
match item.as_f64() {
Some(n) if refused(n) => return Some(found(Some(i), n)),
Some(_) => {}
None => {
let inner = find_non_finite(&item, &format!("{parameter}[{i}]"), allow_nan);
if inner.is_some() {
return inner;
}
}
}
}
return None;
}
let object: &js_sys::Object = wasm_bindgen::JsCast::unchecked_ref(value);
for entry in js_sys::Object::entries(object).iter() {
let pair: js_sys::Array = wasm_bindgen::JsCast::unchecked_into(entry);
let key = pair.get(0).as_string().unwrap_or_default();
let inner = find_non_finite(&pair.get(1), &format!("{parameter}.{key}"), allow_nan);
if inner.is_some() {
return inner;
}
}
None
}
fn to_js<T: Serialize>(value: &T) -> Result<JsValue, JsValue> {
serde_wasm_bindgen::to_value(value)
.map_err(|e| refuse("malformed_input", "result", e.to_string()))
}
fn from_js<T: serde::de::DeserializeOwned>(value: JsValue, param: &str) -> Result<T, JsValue> {
if value.as_string().is_some() {
return Err(refuse(
"malformed_input",
param,
format!(
"{param}: expected a native JS object/array, got a string — \
pass the value directly, not JSON.stringify(...)"
),
));
}
if let Some(found) = find_non_finite(&value, param, false) {
return Err(refuse_non_finite(&found));
}
let json: serde_json::Value = serde_wasm_bindgen::from_value(value)
.map_err(|e| refuse("malformed_input", param, format!("{param}: {e}")))?;
serde_json::from_value(json)
.map_err(|e| refuse("malformed_input", param, format!("{param}: {e}")))
}
fn parse_points(js: JsValue, param: &str) -> Result<Vec<Point2D>, JsValue> {
from_js(js, param)
}
#[wasm_bindgen]
pub fn polygon_area(
#[wasm_bindgen(unchecked_param_type = "Point2D[]")] points: JsValue,
) -> Result<f64, JsValue> {
let points = parse_points(points, "points")?;
let tuples: Vec<(f64, f64)> = points.iter().map(|p| p.to_tuple()).collect();
Ok(crate::polygon::area(&tuples))
}
#[wasm_bindgen(unchecked_return_type = "Point2D[]")]
pub fn convex_hull(
#[wasm_bindgen(unchecked_param_type = "Point2D[]")] points: JsValue,
) -> Result<JsValue, JsValue> {
let points = parse_points(points, "points")?;
let tuples: Vec<(f64, f64)> = points.iter().map(|p| p.to_tuple()).collect();
let hull = crate::polygon::convex_hull(&tuples);
let result: Vec<Point2D> = hull.into_iter().map(Point2D::from_tuple).collect();
to_js(&result)
}
#[wasm_bindgen]
pub fn point_in_polygon(
#[wasm_bindgen(unchecked_param_type = "Point2D")] point: JsValue,
#[wasm_bindgen(unchecked_param_type = "Point2D[]")] polygon: JsValue,
) -> Result<bool, JsValue> {
let pt: Point2D = from_js(point, "point")?;
let polygon = parse_points(polygon, "polygon")?;
let tuples: Vec<(f64, f64)> = polygon.iter().map(|p| p.to_tuple()).collect();
Ok(crate::polygon::contains_point(&tuples, pt.to_tuple()))
}
#[wasm_bindgen]
pub fn polygons_intersect(
#[wasm_bindgen(unchecked_param_type = "Point2D[]")] poly_a: JsValue,
#[wasm_bindgen(unchecked_param_type = "Point2D[]")] poly_b: JsValue,
) -> Result<bool, JsValue> {
let a = parse_points(poly_a, "poly_a")?;
let b = parse_points(poly_b, "poly_b")?;
let ta: Vec<(f64, f64)> = a.iter().map(|p| p.to_tuple()).collect();
let tb: Vec<(f64, f64)> = b.iter().map(|p| p.to_tuple()).collect();
Ok(crate::collision::polygons_intersect(&ta, &tb))
}
#[wasm_bindgen(unchecked_return_type = "Aabb")]
pub fn polygon_bounds(
#[wasm_bindgen(unchecked_param_type = "Point2D[]")] points: JsValue,
) -> Result<JsValue, JsValue> {
let points = parse_points(points, "points")?;
if points.is_empty() {
return Err(refuse(
"empty_input",
"points",
"points: expected a non-empty array".to_string(),
));
}
let mut min_x = points[0].x;
let mut min_y = points[0].y;
let mut max_x = points[0].x;
let mut max_y = points[0].y;
for p in points.iter().skip(1) {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
let aabb = Aabb {
min: Point2D { x: min_x, y: min_y },
max: Point2D { x: max_x, y: max_y },
};
to_js(&aabb)
}
#[wasm_bindgen(unchecked_return_type = "Point2D[]")]
pub fn transform_points(
#[wasm_bindgen(unchecked_param_type = "Point2D[]")] points: JsValue,
tx: f64,
ty: f64,
angle: f64,
) -> Result<JsValue, JsValue> {
let points = parse_points(points, "points")?;
let (tx, ty, angle) = (
finite_arg(tx, "tx")?,
finite_arg(ty, "ty")?,
finite_arg(angle, "angle")?,
);
let tuples: Vec<(f64, f64)> = points.iter().map(|p| p.to_tuple()).collect();
let t = crate::transform::Transform2D::new(tx, ty, angle);
let out: Vec<Point2D> = t
.apply_points(&tuples)
.into_iter()
.map(Point2D::from_tuple)
.collect();
to_js(&out)
}