use ifc_lite_geometry::space_dcel::{
BuildOptions, EditError, FaceId, FacePatch, HalfEdgeId, InputSegment, SpacePlate, VertexId,
};
use serde::Serialize;
use wasm_bindgen::prelude::*;
const MAX_INPUT_SEGMENTS: usize = 16384;
const MAX_INPUT_RECTS: usize = 4096;
#[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],
seg_half_thickness: &[f64],
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 n > MAX_INPUT_SEGMENTS {
return Err(JsValue::from_str(
"too many wall segments for the space-plate arrangement",
));
}
if seg_sources.len() != n {
return Err(JsValue::from_str(
"segSources length must equal the segment count (segCoords.len / 4)",
));
}
if !seg_half_thickness.is_empty() && seg_half_thickness.len() != n {
return Err(JsValue::from_str(
"segHalfThickness must be empty or have one entry per segment",
));
}
let segments: Vec<InputSegment> = (0..n)
.map(|i| {
let o = i * 4;
let src = seg_sources[i];
let half = seg_half_thickness.get(i).copied().unwrap_or(0.0);
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) },
)
.with_half_thickness(half.max(0.0))
})
.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(js_name = fromWallRects)]
pub fn from_wall_rects(rect_coords: &[f64], snap_tolerance: f64, min_area: f64) -> Result<SpacePlateHandle, JsValue> {
if !rect_coords.len().is_multiple_of(8) {
return Err(JsValue::from_str(
"rectCoords length must be a multiple of 8 (4 corners × x,y per wall)",
));
}
let rects: Vec<[[f64; 2]; 4]> = rect_coords
.chunks_exact(8)
.map(|c| [[c[0], c[1]], [c[2], c[3]], [c[4], c[5]], [c[6], c[7]]])
.collect();
if rects.len() > MAX_INPUT_RECTS {
return Err(JsValue::from_str(
"too many wall rects for the space-plate arrangement",
));
}
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_from_wall_rects(&rects, opts) })
}
#[wasm_bindgen(js_name = gapBoundary)]
pub fn gap_boundary(&self, face: u32, factor: f64) -> Vec<f64> {
self.inner
.gap_boundary(FaceId(face), factor)
.into_iter()
.flat_map(|p| [p[0], p[1]])
.collect()
}
#[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 = netOutline)]
pub fn net_outline(&self, face: u32, inset: bool) -> Vec<f64> {
self.inner
.net_outline(FaceId(face), inset)
.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)
}
#[wasm_bindgen(js_name = dissolveVertex)]
pub fn dissolve_vertex(&mut self, v: u32) -> Result<JsValue, JsValue> {
let patches = self.inner.dissolve_vertex(VertexId(v)).map_err(edit_err)?;
patches_to_js(patches)
}
#[wasm_bindgen(js_name = addFace)]
pub fn add_face(&mut self, coords: &[f64], source: i32) -> Result<JsValue, JsValue> {
if !coords.len().is_multiple_of(2) {
return Err(JsValue::from_str("coords length must be even (x, y per vertex)"));
}
let pts: Vec<[f64; 2]> = coords.chunks_exact(2).map(|c| [c[0], c[1]]).collect();
let src = if source < 0 { None } else { Some(source as u32) };
let patch = self.inner.add_face(&pts, src).map_err(edit_err)?;
patches_to_js(vec![patch])
}
#[wasm_bindgen(js_name = removeEdge)]
pub fn remove_edge(&mut self, edge: u32) -> Result<JsValue, JsValue> {
let patches = self.inner.remove_edge(HalfEdgeId(edge)).map_err(edit_err)?;
patches_to_js(patches)
}
#[wasm_bindgen(js_name = prune)]
pub fn prune(&mut self) -> usize {
self.inner.prune_orphans()
}
}
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 (code, msg) = match e {
EditError::StaleHandle => ("StaleHandle", "this element no longer exists (it was removed or merged)"),
EditError::VerticesNotOnFace => ("VerticesNotOnFace", "both split points must lie on the same room"),
EditError::DegenerateCut => ("DegenerateCut", "the two points are the same or already share a wall"),
EditError::BordersExterior => ("BordersExterior", "this wall is the room's outer edge — removing it would open the room"),
EditError::BridgeEdge => ("BridgeEdge", "this wall bridges the room to itself"),
EditError::VertexNotDissolvable => ("VertexNotDissolvable", "this node joins three or more walls"),
EditError::InvalidPolygon => ("InvalidPolygon", "a room needs a simple ring of 3+ points enclosing real area"),
};
let err = js_sys::Error::new(msg);
err.set_name(code);
err.into()
}