1#![allow(clippy::missing_errors_doc)]
4
5use wasm_bindgen::prelude::*;
6
7use brepkit_topology::face::Face;
8
9use crate::handles::{face_id_to_u32, solid_id_to_u32};
10use crate::helpers::{TOL, serialize_feature};
11use crate::kernel::BrepKernel;
12
13#[wasm_bindgen]
14impl BrepKernel {
15 #[wasm_bindgen(js_name = "sewFaces")]
25 #[allow(clippy::needless_pass_by_value)]
26 pub fn sew_faces(&mut self, face_handles: Vec<u32>, tolerance: f64) -> Result<u32, JsError> {
27 let face_ids: Vec<brepkit_topology::face::FaceId> = face_handles
28 .iter()
29 .map(|&h| self.resolve_face(h))
30 .collect::<Result<_, _>>()?;
31 let solid = brepkit_operations::sew::sew_faces(self.topo_mut(), &face_ids, tolerance)?;
32 Ok(solid_id_to_u32(solid))
33 }
34
35 #[wasm_bindgen(js_name = "makeSolid")]
40 #[allow(clippy::needless_pass_by_value)]
41 pub fn make_solid_from_faces(&mut self, face_handles: Vec<u32>) -> Result<u32, JsError> {
42 let face_ids: Vec<brepkit_topology::face::FaceId> = face_handles
43 .iter()
44 .map(|&h| self.resolve_face(h))
45 .collect::<Result<_, _>>()?;
46 let tolerance = brepkit_math::tolerance::Tolerance::new().linear;
47 let solid = brepkit_operations::sew::sew_faces(self.topo_mut(), &face_ids, tolerance)?;
48 Ok(solid_id_to_u32(solid))
49 }
50
51 #[wasm_bindgen(js_name = "removeHolesFromFace")]
53 pub fn remove_holes_from_face(&mut self, face: u32) -> Result<u32, JsError> {
54 let face_id = self.resolve_face(face)?;
55 let face_data = self.topo.face(face_id)?;
56 let outer_wire = face_data.outer_wire();
57 let surface = face_data.surface().clone();
58 let new_face = Face::new(outer_wire, vec![], surface);
59 let fid = self.topo_mut().add_face(new_face);
60 Ok(face_id_to_u32(fid))
61 }
62
63 #[wasm_bindgen(js_name = "weldShellsAndFaces")]
68 #[allow(clippy::needless_pass_by_value)]
69 pub fn weld_shells_and_faces(
70 &mut self,
71 face_handles: Vec<u32>,
72 tolerance: f64,
73 ) -> Result<u32, JsError> {
74 let face_ids: Vec<brepkit_topology::face::FaceId> = face_handles
75 .iter()
76 .map(|&h| self.resolve_face(h))
77 .collect::<Result<_, _>>()?;
78 let tol = if tolerance > 0.0 {
79 tolerance
80 } else {
81 brepkit_math::tolerance::Tolerance::new().linear
82 };
83 let solid = brepkit_operations::sew::sew_faces(self.topo_mut(), &face_ids, tol)?;
84 Ok(solid_id_to_u32(solid))
85 }
86
87 #[wasm_bindgen(js_name = "unifyFaces")]
95 pub fn unify_faces(&mut self, solid: u32) -> Result<u32, JsError> {
96 let solid_id = self.resolve_solid(solid)?;
97 let removed = brepkit_operations::heal::unify_faces(self.topo_mut(), solid_id)?;
98 #[allow(clippy::cast_possible_truncation)]
99 Ok(removed as u32)
100 }
101
102 #[wasm_bindgen(js_name = "convertToBspline")]
117 pub fn convert_to_bspline(&mut self, solid: u32) -> Result<u32, JsError> {
118 let solid_id = self.resolve_solid(solid)?;
119 let count = brepkit_operations::heal::convert_to_bspline(self.topo_mut(), solid_id)?;
120 #[allow(clippy::cast_possible_truncation)]
121 Ok(count as u32)
122 }
123
124 #[wasm_bindgen(js_name = "convertToElementary")]
136 pub fn convert_to_elementary(&mut self, solid: u32) -> Result<u32, JsError> {
137 let solid_id = self.resolve_solid(solid)?;
138 let count =
139 brepkit_operations::heal::convert_to_elementary(self.topo_mut(), solid_id, TOL)?;
140 #[allow(clippy::cast_possible_truncation)]
141 Ok(count as u32)
142 }
143
144 #[wasm_bindgen(js_name = "healSolid")]
148 pub fn heal_solid(&mut self, solid: u32) -> Result<u32, JsError> {
149 let solid_id = self.resolve_solid(solid)?;
150 let report = brepkit_operations::heal::heal_solid(self.topo_mut(), solid_id, TOL)?;
151 #[allow(clippy::cast_possible_truncation)]
152 Ok((report.vertices_merged
153 + report.degenerate_edges_removed
154 + report.orientations_fixed
155 + report.wire_gaps_closed
156 + report.small_faces_removed
157 + report.duplicate_faces_removed) as u32)
158 }
159
160 #[wasm_bindgen(js_name = "repairSolid")]
169 pub fn repair_solid(&mut self, solid: u32) -> Result<u32, JsError> {
170 let solid_id = self.resolve_solid(solid)?;
171 let report = brepkit_operations::heal::repair_solid(self.topo_mut(), solid_id, TOL)?;
172 #[allow(clippy::cast_possible_truncation)]
173 Ok(report.after.error_count() as u32)
174 }
175
176 #[wasm_bindgen(js_name = "removeDegenerateEdges")]
180 pub fn remove_degenerate_edges(&mut self, solid: u32, tolerance: f64) -> Result<u32, JsError> {
181 let solid_id = self.resolve_solid(solid)?;
182 let count = brepkit_operations::heal::remove_degenerate_edges(
183 self.topo_mut(),
184 solid_id,
185 tolerance,
186 )?;
187 #[allow(clippy::cast_possible_truncation)]
188 Ok(count as u32)
189 }
190
191 #[wasm_bindgen(js_name = "fixFaceOrientations")]
195 pub fn fix_face_orientations(&mut self, solid: u32) -> Result<u32, JsError> {
196 let solid_id = self.resolve_solid(solid)?;
197 let count = brepkit_operations::heal::fix_face_orientations(self.topo_mut(), solid_id)?;
198 #[allow(clippy::cast_possible_truncation)]
199 Ok(count as u32)
200 }
201
202 #[wasm_bindgen(js_name = "defeature")]
209 #[allow(clippy::needless_pass_by_value)]
210 pub fn defeature(&mut self, solid: u32, face_handles: Vec<u32>) -> Result<u32, JsError> {
211 let solid_id = self.resolve_solid(solid)?;
212 let face_ids: Vec<_> = face_handles
213 .iter()
214 .map(|&h| self.resolve_face(h))
215 .collect::<Result<Vec<_>, _>>()?;
216 let result =
217 brepkit_operations::defeature::defeature(self.topo_mut(), solid_id, &face_ids)?;
218 Ok(solid_id_to_u32(result))
219 }
220
221 #[wasm_bindgen(js_name = "detectSmallFeatures")]
225 pub fn detect_small_features(
226 &self,
227 solid: u32,
228 area_threshold: f64,
229 deflection: f64,
230 ) -> Result<Vec<u32>, JsError> {
231 let solid_id = self.resolve_solid(solid)?;
232 let faces = brepkit_operations::defeature::detect_small_features(
233 &self.topo,
234 solid_id,
235 area_threshold,
236 deflection,
237 )?;
238 Ok(faces.iter().map(|f| face_id_to_u32(*f)).collect())
239 }
240
241 #[wasm_bindgen(js_name = "recognizeFeatures")]
245 pub fn recognize_features(&self, solid: u32, deflection: f64) -> Result<String, JsError> {
246 let solid_id = self.resolve_solid(solid)?;
247 let features = brepkit_operations::feature_recognition::recognize_features(
248 &self.topo, solid_id, deflection,
249 )?;
250 let json_features: Vec<serde_json::Value> =
251 features.iter().map(serialize_feature).collect();
252 Ok(serde_json::Value::Array(json_features).to_string())
253 }
254}
255
256#[cfg(test)]
257#[allow(clippy::unwrap_used, clippy::expect_used)]
258mod tests {
259 use crate::kernel::BrepKernel;
260
261 #[test]
262 fn convert_to_bspline_returns_count_and_solid() {
263 let mut k = BrepKernel::new();
264 let r = k.execute_batch(
265 r#"[
266 {"op": "makeCylinder", "args": {"radius": 1, "height": 2}},
267 {"op": "convertToBspline", "args": {"solid": 0}}
268 ]"#,
269 );
270 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
271 let ok = parsed[1]["ok"].as_object().expect("expected ok object");
272 assert!(ok.get("solid").is_some(), "missing 'solid' field");
273 let converted = ok["converted"].as_u64().expect("expected 'converted' u64");
274 assert!(converted >= 5, "expected >=5 conversions, got {converted}");
277 }
278
279 #[test]
280 fn convert_to_bspline_invalid_handle_errors() {
281 let mut k = BrepKernel::new();
282 let r = k.execute_batch(r#"[{"op": "convertToBspline", "args": {"solid": 999}}]"#);
283 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
284 assert!(
285 parsed[0]["error"].is_string(),
286 "expected error for invalid handle, got: {}",
287 parsed[0]
288 );
289 }
290
291 #[test]
292 fn convert_to_bspline_idempotent_second_call_is_zero() {
293 let mut k = BrepKernel::new();
294 let r = k.execute_batch(
295 r#"[
296 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
297 {"op": "convertToBspline", "args": {"solid": 0}},
298 {"op": "convertToBspline", "args": {"solid": 0}}
299 ]"#,
300 );
301 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
302 let first = parsed[1]["ok"]["converted"].as_u64().unwrap();
303 let second = parsed[2]["ok"]["converted"].as_u64().unwrap();
304 assert!(first > 0);
305 assert_eq!(second, 0, "second pass should convert nothing");
306 }
307
308 #[test]
309 fn convert_to_elementary_via_batch_round_trip() {
310 let mut k = BrepKernel::new();
316 let r = k.execute_batch(
317 r#"[
318 {"op": "makeCylinder", "args": {"radius": 1, "height": 2}},
319 {"op": "convertToBspline", "args": {"solid": 0}},
320 {"op": "convertToElementary", "args": {"solid": 0}}
321 ]"#,
322 );
323 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
324 let ok = parsed[2]["ok"]
327 .as_object()
328 .expect("convertToElementary should be dispatched, got error");
329 assert!(ok.get("solid").is_some(), "missing 'solid' field");
330 let converted = ok["converted"].as_u64().expect("expected 'converted' u64");
331 assert!(
332 converted > 0,
333 "expected >=1 recognition (the cylinder lateral face at minimum), got {converted}"
334 );
335 }
336
337 #[test]
338 fn convert_to_elementary_via_batch_invalid_handle_errors() {
339 let mut k = BrepKernel::new();
340 let r = k.execute_batch(r#"[{"op": "convertToElementary", "args": {"solid": 999}}]"#);
341 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
342 assert!(
343 parsed[0]["error"].is_string(),
344 "expected error for invalid handle, got: {}",
345 parsed[0]
346 );
347 }
348}