use ifc_lite_geometry::space_dcel::{
BuildOptions, EditError, FaceId, FacePatch, HalfEdgeId, InputSegment, SpacePlate, VertexId,
};
use serde::Serialize;
use wasm_bindgen::prelude::*;
#[derive(Serialize)]
struct FacePatchJs {
face: u32,
area: f64,
simple: bool,
outline: Vec<[f64; 2]>,
}
impl From<FacePatch> for FacePatchJs {
fn from(p: FacePatch) -> Self {
FacePatchJs { face: p.face.0, area: p.area, simple: p.simple, outline: p.outline }
}
}
#[derive(Serialize)]
struct BoundaryJs {
edge: u32,
source: Option<u32>,
}
#[wasm_bindgen]
pub struct SpacePlateHandle {
inner: SpacePlate,
}
#[wasm_bindgen]
impl SpacePlateHandle {
#[wasm_bindgen(constructor)]
pub fn new(
seg_coords: &[f64],
seg_sources: &[i32],
snap_tolerance: f64,
min_area: f64,
) -> Result<SpacePlateHandle, JsValue> {
if !seg_coords.len().is_multiple_of(4) {
return Err(JsValue::from_str(
"segCoords length must be a multiple of 4 (ax, ay, bx, by per segment)",
));
}
let n = seg_coords.len() / 4;
if seg_sources.len() != n {
return Err(JsValue::from_str(
"segSources length must equal the segment count (segCoords.len / 4)",
));
}
let segments: Vec<InputSegment> = (0..n)
.map(|i| {
let o = i * 4;
let src = seg_sources[i];
InputSegment::new(
[seg_coords[o], seg_coords[o + 1]],
[seg_coords[o + 2], seg_coords[o + 3]],
if src < 0 { None } else { Some(src as u32) },
)
})
.collect();
let defaults = BuildOptions::default();
let opts = BuildOptions {
snap_tolerance: if snap_tolerance > 0.0 { snap_tolerance } else { defaults.snap_tolerance },
min_area: if min_area > 0.0 { min_area } else { defaults.min_area },
};
Ok(SpacePlateHandle { inner: SpacePlate::build(&segments, opts) })
}
#[wasm_bindgen(getter, js_name = roomCount)]
pub fn room_count(&self) -> usize {
self.inner.room_count()
}
#[wasm_bindgen(js_name = duplicate)]
pub fn duplicate(&self) -> SpacePlateHandle {
SpacePlateHandle { inner: self.inner.clone() }
}
#[wasm_bindgen(js_name = roomIds)]
pub fn room_ids(&self) -> Vec<u32> {
self.inner.rooms().map(|f| f.0).collect()
}
#[wasm_bindgen(js_name = snapshot)]
pub fn snapshot(&self) -> Result<JsValue, JsValue> {
let rooms: Vec<FacePatchJs> =
self.inner.room_patches().into_iter().map(Into::into).collect();
to_js(&rooms)
}
#[wasm_bindgen(js_name = faceArea)]
pub fn face_area(&self, face: u32) -> f64 {
self.inner.face_area(FaceId(face))
}
#[wasm_bindgen(js_name = faceOutline)]
pub fn face_outline(&self, face: u32) -> Vec<f64> {
self.inner
.face_outline(FaceId(face))
.into_iter()
.flat_map(|p| [p[0], p[1]])
.collect()
}
#[wasm_bindgen(js_name = findVertexNear)]
pub fn find_vertex_near(&self, x: f64, y: f64, tol: f64) -> Option<u32> {
self.inner.find_vertex_near(x, y, tol).map(|v| v.0)
}
#[wasm_bindgen(js_name = neighborAcross)]
pub fn neighbor_across(&self, edge: u32) -> Option<u32> {
self.inner.neighbor_across(HalfEdgeId(edge)).map(|f| f.0)
}
#[wasm_bindgen(js_name = boundingElements)]
pub fn bounding_elements(&self, face: u32) -> Result<JsValue, JsValue> {
let v: Vec<BoundaryJs> = self
.inner
.bounding_elements(FaceId(face))
.into_iter()
.map(|(e, source)| BoundaryJs { edge: e.0, source })
.collect();
to_js(&v)
}
#[wasm_bindgen(js_name = setFaceHeight)]
pub fn set_face_height(&mut self, face: u32, floor_z: f64, ceiling_z: f64, non_planar: bool) {
self.inner.set_face_height(FaceId(face), floor_z, ceiling_z, non_planar);
}
#[wasm_bindgen(js_name = dragVertex)]
pub fn drag_vertex(&mut self, v: u32, x: f64, y: f64) -> Result<JsValue, JsValue> {
let patches = self.inner.drag_vertex(VertexId(v), x, y).map_err(edit_err)?;
patches_to_js(patches)
}
#[wasm_bindgen(js_name = splitFace)]
pub fn split_face(&mut self, face: u32, va: u32, vb: u32, source: i32) -> Result<JsValue, JsValue> {
let src = if source < 0 { None } else { Some(source as u32) };
let patches = self
.inner
.split_face(FaceId(face), VertexId(va), VertexId(vb), src)
.map_err(edit_err)?;
patches_to_js(patches)
}
#[wasm_bindgen(js_name = splitEdge)]
pub fn split_edge(&mut self, edge: u32, x: f64, y: f64) -> Result<u32, JsValue> {
self.inner.split_edge(HalfEdgeId(edge), x, y).map(|v| v.0).map_err(edit_err)
}
#[wasm_bindgen(js_name = mergeFaces)]
pub fn merge_faces(&mut self, edge: u32) -> Result<JsValue, JsValue> {
let patches = self.inner.merge_faces(HalfEdgeId(edge)).map_err(edit_err)?;
patches_to_js(patches)
}
}
fn patches_to_js(patches: Vec<FacePatch>) -> Result<JsValue, JsValue> {
let v: Vec<FacePatchJs> = patches.into_iter().map(Into::into).collect();
to_js(&v)
}
fn to_js<T: Serialize>(value: &T) -> Result<JsValue, JsValue> {
serde_wasm_bindgen::to_value(value).map_err(|e| JsValue::from_str(&e.to_string()))
}
fn edit_err(e: EditError) -> JsValue {
let msg = match e {
EditError::StaleHandle => "StaleHandle: id is tombstoned or out of range",
EditError::VerticesNotOnFace => "VerticesNotOnFace: both split vertices must lie on the face",
EditError::DegenerateCut => "DegenerateCut: split endpoints are equal or already adjacent",
EditError::BordersExterior => "BordersExterior: edge borders the exterior — nothing to merge",
EditError::BridgeEdge => "BridgeEdge: edge is a bridge — removing it would split connectivity",
};
JsValue::from_str(msg)
}