1#![allow(clippy::missing_errors_doc)]
4
5use wasm_bindgen::prelude::*;
6
7use brepkit_math::vec::Point3;
8use brepkit_operations::measure;
9
10use crate::error::validate_positive;
11use crate::kernel::BrepKernel;
12
13#[wasm_bindgen]
14impl BrepKernel {
15 #[wasm_bindgen(js_name = "boundingBox")]
25 pub fn bounding_box(&self, solid: u32) -> Result<Vec<f64>, JsError> {
26 let solid_id = self.resolve_solid(solid)?;
27 let aabb = measure::solid_bounding_box(&self.topo, solid_id)?;
28 Ok(vec![
29 aabb.min.x(),
30 aabb.min.y(),
31 aabb.min.z(),
32 aabb.max.x(),
33 aabb.max.y(),
34 aabb.max.z(),
35 ])
36 }
37
38 #[wasm_bindgen(js_name = "volume")]
44 pub fn volume(&self, solid: u32, deflection: f64) -> Result<f64, JsError> {
45 validate_positive(deflection, "deflection")?;
46 let solid_id = self.resolve_solid(solid)?;
47 Ok(measure::solid_volume(&self.topo, solid_id, deflection)?)
48 }
49
50 #[wasm_bindgen(js_name = "surfaceArea")]
56 pub fn surface_area(&self, solid: u32, deflection: f64) -> Result<f64, JsError> {
57 validate_positive(deflection, "deflection")?;
58 let solid_id = self.resolve_solid(solid)?;
59 Ok(measure::solid_surface_area(
60 &self.topo, solid_id, deflection,
61 )?)
62 }
63
64 #[wasm_bindgen(js_name = "faceArea")]
70 pub fn face_area(&self, face: u32, deflection: f64) -> Result<f64, JsError> {
71 validate_positive(deflection, "deflection")?;
72 let face_id = self.resolve_face(face)?;
73 Ok(measure::face_area(&self.topo, face_id, deflection)?)
74 }
75
76 #[wasm_bindgen(js_name = "centerOfMass")]
84 pub fn center_of_mass(&self, solid: u32, deflection: f64) -> Result<Vec<f64>, JsError> {
85 validate_positive(deflection, "deflection")?;
86 let solid_id = self.resolve_solid(solid)?;
87 let com = measure::solid_center_of_mass(&self.topo, solid_id, deflection)?;
88 Ok(vec![com.x(), com.y(), com.z()])
89 }
90
91 #[wasm_bindgen(js_name = "classifyPoint")]
99 pub fn classify_point(
100 &self,
101 solid: u32,
102 x: f64,
103 y: f64,
104 z: f64,
105 tolerance: f64,
106 ) -> Result<String, JsError> {
107 let solid_id = self.resolve_solid(solid)?;
108 let point = brepkit_math::vec::Point3::new(x, y, z);
109 let result = brepkit_operations::classify::classify_point(
110 &self.topo, solid_id, point, 0.1, tolerance,
111 )?;
112 Ok(match result {
113 brepkit_operations::classify::PointClassification::Inside => "inside".into(),
114 brepkit_operations::classify::PointClassification::Outside => "outside".into(),
115 brepkit_operations::classify::PointClassification::OnBoundary => "boundary".into(),
116 })
117 }
118
119 #[wasm_bindgen(js_name = "edgeLength")]
125 pub fn edge_length(&self, edge: u32) -> Result<f64, JsError> {
126 let edge_id = self.resolve_edge(edge)?;
127 Ok(measure::edge_length(&self.topo, edge_id)?)
128 }
129
130 #[wasm_bindgen(js_name = "facePerimeter")]
136 pub fn face_perimeter(&self, face: u32) -> Result<f64, JsError> {
137 let face_id = self.resolve_face(face)?;
138 Ok(measure::face_perimeter(&self.topo, face_id)?)
139 }
140
141 #[wasm_bindgen(js_name = "validateSolid")]
149 pub fn validate_solid(&self, solid: u32) -> Result<u32, JsError> {
150 let solid_id = self.resolve_solid(solid)?;
151 let report = brepkit_operations::validate::validate_solid(&self.topo, solid_id)?;
152 #[allow(clippy::cast_possible_truncation)]
153 Ok(report.error_count() as u32)
154 }
155
156 #[wasm_bindgen(js_name = "validateSolidRelaxed")]
170 pub fn validate_solid_relaxed(&self, solid: u32) -> Result<u32, JsError> {
171 let solid_id = self.resolve_solid(solid)?;
172 let report = brepkit_operations::validate::validate_solid_relaxed(&self.topo, solid_id)?;
173 #[allow(clippy::cast_possible_truncation)]
174 Ok(report.error_count() as u32)
175 }
176
177 #[wasm_bindgen(js_name = "validateSolidWithOptions")]
189 pub fn validate_solid_with_options(
190 &self,
191 solid: u32,
192 tolerance_scale: f64,
193 ) -> Result<u32, JsError> {
194 let solid_id = self.resolve_solid(solid)?;
195 let options = brepkit_operations::validate::ValidationOptions {
196 tolerance_scale,
197 ..Default::default()
198 };
199 let report = brepkit_operations::validate::validate_solid_with_options(
200 &self.topo, solid_id, &options,
201 )?;
202 #[allow(clippy::cast_possible_truncation)]
203 Ok(report.error_count() as u32)
204 }
205
206 #[wasm_bindgen(js_name = "pointToSolidDistance")]
216 pub fn point_to_solid_distance(
217 &self,
218 px: f64,
219 py: f64,
220 pz: f64,
221 solid: u32,
222 ) -> Result<Vec<f64>, JsError> {
223 let solid_id = self.resolve_solid(solid)?;
224 let result = brepkit_operations::distance::point_to_solid_distance(
225 &self.topo,
226 Point3::new(px, py, pz),
227 solid_id,
228 )?;
229 Ok(vec![
230 result.distance,
231 result.point_b.x(),
232 result.point_b.y(),
233 result.point_b.z(),
234 ])
235 }
236
237 #[wasm_bindgen(js_name = "solidToSolidDistance")]
245 pub fn solid_to_solid_distance(&self, a: u32, b: u32) -> Result<Vec<f64>, JsError> {
246 let a_id = self.resolve_solid(a)?;
247 let b_id = self.resolve_solid(b)?;
248 let result = brepkit_operations::distance::solid_to_solid_distance(&self.topo, a_id, b_id)?;
249 Ok(vec![
250 result.distance,
251 result.point_a.x(),
252 result.point_a.y(),
253 result.point_a.z(),
254 result.point_b.x(),
255 result.point_b.y(),
256 result.point_b.z(),
257 ])
258 }
259
260 #[wasm_bindgen(js_name = "pointToFaceDistance")]
268 pub fn point_to_face_distance(
269 &self,
270 px: f64,
271 py: f64,
272 pz: f64,
273 face: u32,
274 ) -> Result<Vec<f64>, JsError> {
275 let face_id = self.resolve_face(face)?;
276 let result = brepkit_operations::distance::point_to_face(
277 &self.topo,
278 Point3::new(px, py, pz),
279 face_id,
280 )?;
281 Ok(vec![
282 result.distance,
283 result.point_b.x(),
284 result.point_b.y(),
285 result.point_b.z(),
286 ])
287 }
288
289 #[wasm_bindgen(js_name = "pointToEdgeDistance")]
297 pub fn point_to_edge_distance(
298 &self,
299 px: f64,
300 py: f64,
301 pz: f64,
302 edge: u32,
303 ) -> Result<Vec<f64>, JsError> {
304 let edge_id = self.resolve_edge(edge)?;
305 let result = brepkit_operations::distance::point_to_edge(
306 &self.topo,
307 Point3::new(px, py, pz),
308 edge_id,
309 )?;
310 Ok(vec![
311 result.distance,
312 result.point_b.x(),
313 result.point_b.y(),
314 result.point_b.z(),
315 ])
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 #![allow(clippy::unwrap_used, clippy::expect_used)]
322
323 use crate::kernel::BrepKernel;
324
325 fn batch_has_error(result: &str, idx: usize) -> bool {
326 let parsed: serde_json::Value = serde_json::from_str(result).unwrap();
327 parsed[idx]["error"].is_string()
328 }
329
330 #[test]
333 fn box_volume_matches_w_times_h_times_d() {
334 let mut k = BrepKernel::new();
335 let r = k.execute_batch(
336 r#"[
337 {"op": "makeBox", "args": {"width": 2, "height": 3, "depth": 4}},
338 {"op": "volume", "args": {"solid": 0}}
339 ]"#,
340 );
341 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
342 let v = parsed[1]["ok"].as_f64().unwrap();
343 assert!(
344 (v - 24.0).abs() < 0.1,
345 "2x3x4 box volume should be 24.0, got {v}"
346 );
347 }
348
349 #[test]
350 fn unit_box_volume_is_one() {
351 let mut k = BrepKernel::new();
352 let r = k.execute_batch(
353 r#"[
354 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
355 {"op": "volume", "args": {"solid": 0}}
356 ]"#,
357 );
358 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
359 let v = parsed[1]["ok"].as_f64().unwrap();
360 assert!(
361 (v - 1.0).abs() < 1e-6,
362 "unit box volume should be 1.0, got {v}"
363 );
364 }
365
366 #[test]
367 fn volume_invalid_handle_is_error() {
368 let mut k = BrepKernel::new();
369 let r = k.execute_batch(r#"[{"op": "volume", "args": {"solid": 9999}}]"#);
370 assert!(batch_has_error(&r, 0));
371 }
372
373 #[test]
376 fn box_surface_area_matches_formula() {
377 let (w, h, d) = (2.0_f64, 3.0_f64, 4.0_f64);
378 let expected = 2.0 * (w * h + w * d + h * d);
379 let mut k = BrepKernel::new();
380 let r = k.execute_batch(
381 r#"[
382 {"op": "makeBox", "args": {"width": 2, "height": 3, "depth": 4}},
383 {"op": "surfaceArea", "args": {"solid": 0}}
384 ]"#,
385 );
386 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
387 let area = parsed[1]["ok"].as_f64().unwrap();
388 assert!(
389 (area - expected).abs() < 0.1,
390 "surface area should be {expected}, got {area}"
391 );
392 }
393
394 #[test]
395 fn surface_area_invalid_handle_is_error() {
396 let mut k = BrepKernel::new();
397 let r = k.execute_batch(r#"[{"op": "surfaceArea", "args": {"solid": 9999}}]"#);
398 assert!(batch_has_error(&r, 0));
399 }
400
401 #[test]
404 fn box_bounding_box_corner_at_origin() {
405 let mut k = BrepKernel::new();
406 let r = k.execute_batch(
407 r#"[
408 {"op": "makeBox", "args": {"width": 2, "height": 3, "depth": 5}},
409 {"op": "boundingBox", "args": {"solid": 0}}
410 ]"#,
411 );
412 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
413 let bb = parsed[1]["ok"].as_array().unwrap();
414 assert_eq!(bb.len(), 6, "bounding box must have 6 components");
415 let min_x = bb[0].as_f64().unwrap();
416 let min_y = bb[1].as_f64().unwrap();
417 let min_z = bb[2].as_f64().unwrap();
418 let max_x = bb[3].as_f64().unwrap();
419 let max_y = bb[4].as_f64().unwrap();
420 let max_z = bb[5].as_f64().unwrap();
421 assert!(min_x.abs() < 1e-6, "min_x should be ~0, got {min_x}");
422 assert!(min_y.abs() < 1e-6, "min_y should be ~0, got {min_y}");
423 assert!(min_z.abs() < 1e-6, "min_z should be ~0, got {min_z}");
424 assert!(
425 (max_x - 2.0).abs() < 1e-6,
426 "max_x should be ~2, got {max_x}"
427 );
428 assert!(
429 (max_y - 3.0).abs() < 1e-6,
430 "max_y should be ~3, got {max_y}"
431 );
432 assert!(
433 (max_z - 5.0).abs() < 1e-6,
434 "max_z should be ~5, got {max_z}"
435 );
436 }
437
438 #[test]
439 fn bounding_box_invalid_handle_is_error() {
440 let mut k = BrepKernel::new();
441 let r = k.execute_batch(r#"[{"op": "boundingBox", "args": {"solid": 9999}}]"#);
442 assert!(batch_has_error(&r, 0));
443 }
444
445 #[test]
448 fn box_center_of_mass_is_geometric_center() {
449 let (w, h, d) = (4.0_f64, 6.0_f64, 2.0_f64);
450 let mut k = BrepKernel::new();
451 let r = k.execute_batch(
452 r#"[
453 {"op": "makeBox", "args": {"width": 4, "height": 6, "depth": 2}},
454 {"op": "centerOfMass", "args": {"solid": 0}}
455 ]"#,
456 );
457 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
458 let com = parsed[1]["ok"].as_array().unwrap();
459 assert_eq!(com.len(), 3);
460 let cx = com[0].as_f64().unwrap();
461 let cy = com[1].as_f64().unwrap();
462 let cz = com[2].as_f64().unwrap();
463 assert!(
464 (cx - w / 2.0).abs() < 0.1,
465 "CoM x should be ~{}, got {cx}",
466 w / 2.0
467 );
468 assert!(
469 (cy - h / 2.0).abs() < 0.1,
470 "CoM y should be ~{}, got {cy}",
471 h / 2.0
472 );
473 assert!(
474 (cz - d / 2.0).abs() < 0.1,
475 "CoM z should be ~{}, got {cz}",
476 d / 2.0
477 );
478 }
479
480 #[test]
485 fn box_edge_length_via_operations() {
486 let mut topo = brepkit_topology::topology::Topology::new();
488 let solid = brepkit_operations::primitives::make_box(&mut topo, 3.0, 3.0, 3.0).unwrap();
489 let edges = brepkit_topology::explorer::solid_edges(&topo, solid).unwrap();
490 assert_eq!(edges.len(), 12, "box must have 12 edges");
491 for &e in &edges {
492 let len = brepkit_operations::measure::edge_length(&topo, e).unwrap();
493 assert!(
494 (len - 3.0).abs() < 1e-6,
495 "all edges of a 3x3x3 box should have length 3, got {len}"
496 );
497 }
498 }
499
500 #[test]
501 fn edge_length_rectangular_box_has_three_distinct_lengths() {
502 let mut topo = brepkit_topology::topology::Topology::new();
503 let solid = brepkit_operations::primitives::make_box(&mut topo, 1.0, 2.0, 4.0).unwrap();
504 let edges = brepkit_topology::explorer::solid_edges(&topo, solid).unwrap();
505 let mut lengths: Vec<f64> = edges
506 .iter()
507 .map(|&e| brepkit_operations::measure::edge_length(&topo, e).unwrap())
508 .collect();
509 lengths.sort_by(|a, b| a.partial_cmp(b).unwrap());
510 assert!((lengths[0] - 1.0).abs() < 1e-6);
511 assert!((lengths[4] - 2.0).abs() < 1e-6);
512 assert!((lengths[8] - 4.0).abs() < 1e-6);
513 }
514
515 #[test]
518 fn classify_point_inside_box() {
519 let mut k = BrepKernel::new();
520 let r = k.execute_batch(
521 r#"[
522 {"op": "makeBox", "args": {"width": 4, "height": 4, "depth": 4}},
523 {"op": "classifyPoint", "args": {"solid": 0, "x": 2, "y": 2, "z": 2}}
524 ]"#,
525 );
526 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
527 assert_eq!(parsed[1]["ok"].as_str().unwrap(), "inside");
528 }
529
530 #[test]
531 fn classify_point_outside_box() {
532 let mut k = BrepKernel::new();
533 let r = k.execute_batch(
534 r#"[
535 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
536 {"op": "classifyPoint", "args": {"solid": 0, "x": 5, "y": 5, "z": 5}}
537 ]"#,
538 );
539 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
540 assert_eq!(parsed[1]["ok"].as_str().unwrap(), "outside");
541 }
542
543 #[test]
544 fn classify_point_returns_valid_string() {
545 let mut k = BrepKernel::new();
546 let r = k.execute_batch(
547 r#"[
548 {"op": "makeBox", "args": {"width": 2, "height": 2, "depth": 2}},
549 {"op": "classifyPoint", "args": {"solid": 0, "x": 1, "y": 1, "z": 1}}
550 ]"#,
551 );
552 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
553 let result = parsed[1]["ok"].as_str().unwrap();
554 assert!(
555 matches!(result, "inside" | "outside" | "boundary"),
556 "classify_point must return inside/outside/boundary, got {result}"
557 );
558 }
559
560 #[test]
561 fn classify_point_invalid_handle_is_error() {
562 let mut k = BrepKernel::new();
563 let r = k.execute_batch(
564 r#"[{"op": "classifyPoint", "args": {"solid": 9999, "x": 0, "y": 0, "z": 0}}]"#,
565 );
566 assert!(batch_has_error(&r, 0));
567 }
568
569 #[test]
572 fn face_area_of_box_face_is_correct() {
573 let mut topo = brepkit_topology::topology::Topology::new();
574 let solid = brepkit_operations::primitives::make_box(&mut topo, 2.0, 3.0, 4.0).unwrap();
575 let faces = brepkit_topology::explorer::solid_faces(&topo, solid).unwrap();
576 assert_eq!(faces.len(), 6);
577 let valid_areas = [6.0_f64, 8.0_f64, 12.0_f64];
578 for &f in &faces {
579 let area = brepkit_operations::measure::face_area(&topo, f, 0.01).unwrap();
580 let ok = valid_areas.iter().any(|&a| (area - a).abs() < 0.05);
581 assert!(ok, "face area {area} should be one of {valid_areas:?}");
582 }
583 }
584
585 #[test]
588 fn point_to_solid_distance_from_outside() {
589 let mut topo = brepkit_topology::topology::Topology::new();
590 let solid = brepkit_operations::primitives::make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
591 let result = brepkit_operations::distance::point_to_solid_distance(
592 &topo,
593 brepkit_math::vec::Point3::new(3.0, 0.5, 0.5),
594 solid,
595 )
596 .unwrap();
597 assert!(
598 (result.distance - 2.0).abs() < 1e-4,
599 "point (3,0.5,0.5) should be 2.0 from box, got {}",
600 result.distance
601 );
602 }
603}