use ifc_lite_geometry::{intersection_solid, DegenerateReason, IntersectionSolid, Mesh};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct ClashIntersectionSolidJs {
positions: Vec<f64>,
indices: Vec<u32>,
volume_m3: f64,
is_solid: bool,
reason: &'static str,
thickness_m: f64,
required_m: f64,
}
#[wasm_bindgen]
impl ClashIntersectionSolidJs {
#[wasm_bindgen(getter, js_name = isSolid)]
pub fn is_solid(&self) -> bool {
self.is_solid
}
#[wasm_bindgen(getter, js_name = degenerateReason)]
pub fn degenerate_reason(&self) -> String {
self.reason.to_string()
}
#[wasm_bindgen(getter, js_name = volumeM3)]
pub fn volume_m3(&self) -> f64 {
self.volume_m3
}
#[wasm_bindgen(getter)]
pub fn positions(&self) -> Vec<f64> {
self.positions.clone()
}
#[wasm_bindgen(getter)]
pub fn indices(&self) -> Vec<u32> {
self.indices.clone()
}
#[wasm_bindgen(getter, js_name = triangleCount)]
pub fn triangle_count(&self) -> u32 {
(self.indices.len() / 3) as u32
}
#[wasm_bindgen(getter, js_name = thicknessM)]
pub fn thickness_m(&self) -> f64 {
self.thickness_m
}
#[wasm_bindgen(getter, js_name = requiredM)]
pub fn required_m(&self) -> f64 {
self.required_m
}
}
fn mesh_from(positions: &[f32], indices: &[u32]) -> Option<Mesh> {
if !positions.len().is_multiple_of(3) || !indices.len().is_multiple_of(3) {
return None;
}
if positions.iter().any(|p| !p.is_finite()) {
return None;
}
let vertex_count = (positions.len() / 3) as u32;
if indices.iter().any(|&i| i >= vertex_count) {
return None;
}
let mut m = Mesh::new();
m.positions.extend_from_slice(positions);
m.indices.extend_from_slice(indices);
Some(m)
}
fn malformed_operand_result() -> ClashIntersectionSolidJs {
ClashIntersectionSolidJs {
positions: Vec::new(),
indices: Vec::new(),
volume_m3: 0.0,
is_solid: false,
reason: "malformed-operand",
thickness_m: 0.0,
required_m: 0.0,
}
}
#[wasm_bindgen(js_name = clashIntersectionSolid)]
pub fn clash_intersection_solid(
positions_a: &[f32],
indices_a: &[u32],
positions_b: &[f32],
indices_b: &[u32],
) -> ClashIntersectionSolidJs {
let (Some(a), Some(b)) = (mesh_from(positions_a, indices_a), mesh_from(positions_b, indices_b)) else {
return malformed_operand_result();
};
match intersection_solid(&a, &b) {
IntersectionSolid::Solid {
positions,
indices,
volume_m3,
} => ClashIntersectionSolidJs {
positions,
indices,
volume_m3,
is_solid: true,
reason: "",
thickness_m: 0.0,
required_m: 0.0,
},
IntersectionSolid::Degenerate(reason) => {
let (name, thickness_m, required_m) = match reason {
DegenerateReason::EmptyOperand => ("empty-operand", 0.0, 0.0),
DegenerateReason::NoOverlap => ("no-overlap", 0.0, 0.0),
DegenerateReason::BudgetExhausted => ("budget-exhausted", 0.0, 0.0),
DegenerateReason::BelowKernelResolution {
thickness_m,
required_m,
} => ("below-kernel-resolution", thickness_m, required_m),
};
ClashIntersectionSolidJs {
positions: Vec::new(),
indices: Vec::new(),
volume_m3: 0.0,
is_solid: false,
reason: name,
thickness_m,
required_m,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn box_positions_indices(lo: [f32; 3], hi: [f32; 3]) -> (Vec<f32>, Vec<u32>) {
let corners: [[f32; 3]; 8] = [
[lo[0], lo[1], lo[2]],
[hi[0], lo[1], lo[2]],
[hi[0], hi[1], lo[2]],
[lo[0], hi[1], lo[2]],
[lo[0], lo[1], hi[2]],
[hi[0], lo[1], hi[2]],
[hi[0], hi[1], hi[2]],
[lo[0], hi[1], hi[2]],
];
let positions: Vec<f32> = corners.iter().flat_map(|c| c.iter().copied()).collect();
#[rustfmt::skip]
let indices: Vec<u32> = vec![
0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 5, 1, 0, 4, 5, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5, 6, 1, 6, 2, ];
(positions, indices)
}
#[test]
fn well_formed_deep_overlap_still_returns_a_solid() {
let (pos_a, idx_a) = box_positions_indices([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let (pos_b, idx_b) = box_positions_indices([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
let result = clash_intersection_solid(&pos_a, &idx_a, &pos_b, &idx_b);
assert!(result.is_solid(), "well-formed deep overlap must not be rejected as malformed");
assert_eq!(result.degenerate_reason(), "");
assert!(result.volume_m3() > 0.0);
}
#[test]
fn a_positions_buffer_not_a_multiple_of_three_is_reported_malformed_not_computed() {
let (pos_a, idx_a) = box_positions_indices([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let (mut pos_b, idx_b) = box_positions_indices([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
pos_b.pop();
let result = clash_intersection_solid(&pos_a, &idx_a, &pos_b, &idx_b);
assert!(!result.is_solid());
assert_eq!(result.degenerate_reason(), "malformed-operand");
assert_eq!(result.volume_m3(), 0.0);
assert!(result.positions().is_empty());
assert!(result.indices().is_empty());
}
#[test]
fn an_index_past_its_own_operands_vertex_count_is_reported_malformed_not_silently_dropped() {
let (pos_a, idx_a) = box_positions_indices([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let (pos_b, mut idx_b) = box_positions_indices([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
let last = idx_b.len() - 1;
idx_b[last] = 99;
let result = clash_intersection_solid(&pos_a, &idx_a, &pos_b, &idx_b);
assert!(!result.is_solid());
assert_eq!(result.degenerate_reason(), "malformed-operand");
assert_eq!(result.volume_m3(), 0.0);
}
#[test]
fn an_out_of_range_index_on_operand_a_is_also_caught() {
let (pos_a, mut idx_a) = box_positions_indices([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let (pos_b, idx_b) = box_positions_indices([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
idx_a[0] = 1000;
let result = clash_intersection_solid(&pos_a, &idx_a, &pos_b, &idx_b);
assert!(!result.is_solid());
assert_eq!(result.degenerate_reason(), "malformed-operand");
}
#[test]
fn nan_corner_off_the_overlap_face_must_be_rejected_not_silently_absorbed() {
let (mut pos_a, idx_a) = box_positions_indices([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let (pos_b, idx_b) = box_positions_indices([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
pos_a[0] = f32::NAN;
let result = clash_intersection_solid(&pos_a, &idx_a, &pos_b, &idx_b);
assert!(
!result.is_solid(),
"NaN operand must never be reported as a trustworthy solid; got isSolid=true, volume={}",
result.volume_m3()
);
assert_eq!(result.degenerate_reason(), "malformed-operand");
}
#[test]
fn nan_corner_on_the_overlap_face_must_be_rejected_not_misreported_as_no_overlap() {
let (mut pos_a, idx_a) = box_positions_indices([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let (pos_b, idx_b) = box_positions_indices([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
pos_a[6 * 3] = f32::NAN;
let result = clash_intersection_solid(&pos_a, &idx_a, &pos_b, &idx_b);
assert!(!result.is_solid());
assert_eq!(
result.degenerate_reason(),
"malformed-operand",
"a genuinely overlapping pair corrupted by NaN must not be reported as no-overlap"
);
}
#[test]
fn infinite_corner_must_be_rejected_as_malformed_operand() {
let (mut pos_a, idx_a) = box_positions_indices([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let (pos_b, idx_b) = box_positions_indices([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
pos_a[0] = f32::INFINITY;
let result = clash_intersection_solid(&pos_a, &idx_a, &pos_b, &idx_b);
assert!(!result.is_solid());
assert_eq!(result.degenerate_reason(), "malformed-operand");
}
}