1#![allow(
4 clippy::missing_errors_doc,
5 clippy::too_many_arguments,
6 clippy::too_many_lines
7)]
8
9use wasm_bindgen::prelude::*;
10
11use brepkit_math::nurbs::curve::NurbsCurve;
12use brepkit_math::vec::{Point3, Vec3};
13use brepkit_topology::edge::Edge;
14use brepkit_topology::face::{Face, FaceSurface};
15
16use crate::error::{WasmError, validate_finite, validate_positive};
17use crate::handles::{edge_id_to_u32, face_id_to_u32, solid_id_to_u32, wire_id_to_u32};
18use brepkit_geometry::extrema::point_to_nurbs_surface;
19
20use crate::helpers::{
21 classify_to_string, create_apex_face, panic_message, parse_points, try_fillet,
22};
23use crate::kernel::BrepKernel;
24
25use brepkit_operations::extrude::extrude;
26use brepkit_operations::offset_wire::JoinType;
27use brepkit_operations::revolve::revolve;
28use brepkit_operations::sweep::sweep;
29
30pub fn parse_join_type_str(s: &str) -> Result<JoinType, WasmError> {
34 match s {
35 "intersection" => Ok(JoinType::Intersection),
36 "arc" => Ok(JoinType::Arc),
37 "chamfer" => Ok(JoinType::Chamfer),
38 _ => Err(WasmError::InvalidInput {
39 reason: format!(
40 "unknown join type '{s}', expected 'intersection', 'arc', or 'chamfer'"
41 ),
42 }),
43 }
44}
45
46#[wasm_bindgen]
47impl BrepKernel {
48 #[wasm_bindgen(js_name = "section")]
59 #[allow(clippy::too_many_arguments)]
60 pub fn section_solid(
61 &mut self,
62 solid: u32,
63 px: f64,
64 py: f64,
65 pz: f64,
66 nx: f64,
67 ny: f64,
68 nz: f64,
69 ) -> Result<Vec<u32>, JsError> {
70 validate_finite(px, "px")?;
71 validate_finite(py, "py")?;
72 validate_finite(pz, "pz")?;
73 validate_finite(nx, "nx")?;
74 validate_finite(ny, "ny")?;
75 validate_finite(nz, "nz")?;
76 let solid_id = self.resolve_solid(solid)?;
77 let result = brepkit_operations::section::section(
78 self.topo_mut(),
79 solid_id,
80 Point3::new(px, py, pz),
81 Vec3::new(nx, ny, nz),
82 )?;
83 #[allow(clippy::cast_possible_truncation)]
84 Ok(result.faces.iter().map(|f| f.index() as u32).collect())
85 }
86
87 #[wasm_bindgen(js_name = "loft")]
98 #[allow(clippy::needless_pass_by_value)]
99 pub fn loft_faces(&mut self, faces: Vec<u32>) -> Result<u32, JsError> {
100 let face_ids: Vec<brepkit_topology::face::FaceId> = faces
101 .iter()
102 .map(|&h| self.resolve_face(h))
103 .collect::<Result<_, _>>()?;
104 let solid_id = brepkit_operations::loft::loft(self.topo_mut(), &face_ids)?;
105 Ok(solid_id_to_u32(solid_id))
106 }
107
108 #[wasm_bindgen(js_name = "loftSmooth")]
121 #[allow(clippy::needless_pass_by_value)]
122 pub fn loft_smooth_faces(&mut self, faces: Vec<u32>) -> Result<u32, JsError> {
123 let face_ids: Vec<brepkit_topology::face::FaceId> = faces
124 .iter()
125 .map(|&h| self.resolve_face(h))
126 .collect::<Result<_, _>>()?;
127 let solid_id = brepkit_operations::loft::loft_smooth(self.topo_mut(), &face_ids)?;
128 Ok(solid_id_to_u32(solid_id))
129 }
130
131 #[wasm_bindgen(js_name = "loftWithOptions")]
138 #[allow(clippy::needless_pass_by_value)]
139 pub fn loft_with_options(&mut self, faces: Vec<u32>, options: &str) -> Result<u32, JsError> {
140 let opts: serde_json::Value =
141 serde_json::from_str(options).unwrap_or(serde_json::Value::Null);
142
143 let mut face_ids: Vec<brepkit_topology::face::FaceId> = faces
144 .iter()
145 .map(|&h| self.resolve_face(h))
146 .collect::<Result<_, _>>()?;
147
148 if let Some(sp) = opts.get("startPoint").and_then(|v| v.as_array())
151 && sp.len() >= 3
152 {
153 let x = sp[0].as_f64().unwrap_or(0.0);
154 let y = sp[1].as_f64().unwrap_or(0.0);
155 let z = sp[2].as_f64().unwrap_or(0.0);
156 let apex_face = create_apex_face(self.topo_mut(), Point3::new(x, y, z), &face_ids)?;
157 face_ids.insert(0, apex_face);
158 }
159
160 if let Some(ep) = opts.get("endPoint").and_then(|v| v.as_array())
162 && ep.len() >= 3
163 {
164 let x = ep[0].as_f64().unwrap_or(0.0);
165 let y = ep[1].as_f64().unwrap_or(0.0);
166 let z = ep[2].as_f64().unwrap_or(0.0);
167 let apex_face = create_apex_face(self.topo_mut(), Point3::new(x, y, z), &face_ids)?;
168 face_ids.push(apex_face);
169 }
170
171 let ruled = opts
172 .get("ruled")
173 .and_then(serde_json::Value::as_bool)
174 .unwrap_or(true);
175
176 let solid_id = if ruled {
177 brepkit_operations::loft::loft(self.topo_mut(), &face_ids)?
178 } else {
179 brepkit_operations::loft::loft_smooth(self.topo_mut(), &face_ids)?
180 };
181 Ok(solid_id_to_u32(solid_id))
182 }
183
184 #[wasm_bindgen(js_name = "shell")]
195 #[allow(clippy::needless_pass_by_value)]
196 pub fn shell_solid(
197 &mut self,
198 solid: u32,
199 thickness: f64,
200 open_faces: Vec<u32>,
201 ) -> Result<u32, JsError> {
202 validate_positive(thickness, "thickness")?;
203 let solid_id = self.resolve_solid(solid)?;
204 let open_face_ids: Vec<brepkit_topology::face::FaceId> = open_faces
205 .iter()
206 .map(|&h| self.resolve_face(h))
207 .collect::<Result<_, _>>()?;
208 let result = brepkit_operations::shell_op::shell(
209 self.topo_mut(),
210 solid_id,
211 thickness,
212 &open_face_ids,
213 )?;
214 Ok(solid_id_to_u32(result))
215 }
216
217 #[wasm_bindgen(js_name = "chamfer")]
227 #[allow(clippy::needless_pass_by_value)]
228 pub fn chamfer_solid(
229 &mut self,
230 solid: u32,
231 edge_handles: Vec<u32>,
232 distance: f64,
233 ) -> Result<u32, JsError> {
234 validate_positive(distance, "distance")?;
235 let solid_id = self.resolve_solid(solid)?;
236 let edge_ids: Vec<brepkit_topology::edge::EdgeId> = edge_handles
237 .iter()
238 .map(|&h| self.resolve_edge(h))
239 .collect::<Result<_, _>>()?;
240 let result =
241 brepkit_operations::chamfer::chamfer(self.topo_mut(), solid_id, &edge_ids, distance)?;
242 Ok(solid_id_to_u32(result))
243 }
244
245 #[wasm_bindgen(js_name = "fillet")]
255 #[allow(clippy::needless_pass_by_value)]
256 pub fn fillet_solid(
257 &mut self,
258 solid: u32,
259 edge_handles: Vec<u32>,
260 radius: f64,
261 ) -> Result<u32, JsError> {
262 validate_positive(radius, "radius")?;
263 let solid_id = self.resolve_solid(solid)?;
264 let edge_ids: Vec<brepkit_topology::edge::EdgeId> = edge_handles
265 .iter()
266 .map(|&h| self.resolve_edge(h))
267 .collect::<Result<_, _>>()?;
268 let result =
276 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> Result<u32, JsError> {
277 let solid = if let Ok(s) = try_fillet(self.topo_mut(), solid_id, &edge_ids, radius)
278 {
279 s
280 } else {
281 let planar_edges = brepkit_operations::query::filter_planar_edges(
283 &self.topo, solid_id, &edge_ids,
284 )?;
285 if planar_edges.is_empty() {
286 solid_id
287 } else {
288 try_fillet(self.topo_mut(), solid_id, &planar_edges, radius)
289 .map_err(|e| JsError::new(&e.to_string()))?
290 }
291 };
292 Ok(solid_id_to_u32(solid))
293 }));
294 match result {
295 Ok(inner) => inner,
296 Err(panic_info) => {
297 let msg = panic_message(&panic_info, "Fillet");
298 Err(JsError::new(&msg))
299 }
300 }
301 }
302
303 #[wasm_bindgen(js_name = "filletWithEvolution")]
316 #[allow(clippy::needless_pass_by_value)]
317 pub fn fillet_with_evolution(
318 &mut self,
319 solid: u32,
320 edge_handles: Vec<u32>,
321 radius: f64,
322 ) -> Result<JsValue, JsError> {
323 validate_positive(radius, "radius")?;
324 let solid_id = self.resolve_solid(solid)?;
325 let edge_ids: Vec<brepkit_topology::edge::EdgeId> = edge_handles
326 .iter()
327 .map(|&h| self.resolve_edge(h))
328 .collect::<Result<_, _>>()?;
329
330 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(
333 || -> Result<String, JsError> {
334 let input_faces =
335 brepkit_operations::boolean::collect_face_signatures(&self.topo, solid_id)?;
336 let result = try_fillet(self.topo_mut(), solid_id, &edge_ids, radius)?;
337 let output_faces =
338 brepkit_operations::boolean::collect_face_signatures(&self.topo, result)?;
339 let evo = brepkit_operations::evolution::build_evolution_by_geometry(
340 &input_faces,
341 &output_faces,
342 );
343 Ok(format!(
344 "{{\"solid\":{},\"evolution\":{}}}",
345 solid_id_to_u32(result),
346 evo.to_json()
347 ))
348 },
349 ));
350 match result {
351 Ok(inner) => inner.map(|json| JsValue::from_str(&json)),
352 Err(panic_info) => Err(JsError::new(&panic_message(&panic_info, "Fillet"))),
353 }
354 }
355
356 #[wasm_bindgen(js_name = "extrude")]
366 pub fn extrude_face(
367 &mut self,
368 face: u32,
369 dir_x: f64,
370 dir_y: f64,
371 dir_z: f64,
372 distance: f64,
373 ) -> Result<u32, JsError> {
374 validate_finite(dir_x, "dir_x")?;
375 validate_finite(dir_y, "dir_y")?;
376 validate_finite(dir_z, "dir_z")?;
377 validate_finite(distance, "distance")?;
378
379 let face_id = self.resolve_face(face)?;
380 let direction = Vec3::new(dir_x, dir_y, dir_z);
381 let solid_id = extrude(self.topo_mut(), face_id, direction, distance)?;
382
383 Ok(solid_id_to_u32(solid_id))
384 }
385
386 #[wasm_bindgen(js_name = "revolve")]
398 #[allow(clippy::too_many_arguments)]
399 pub fn revolve_face(
400 &mut self,
401 face: u32,
402 ox: f64,
403 oy: f64,
404 oz: f64,
405 dx: f64,
406 dy: f64,
407 dz: f64,
408 angle_degrees: f64,
409 ) -> Result<u32, JsError> {
410 validate_finite(ox, "ox")?;
411 validate_finite(oy, "oy")?;
412 validate_finite(oz, "oz")?;
413 validate_finite(dx, "dx")?;
414 validate_finite(dy, "dy")?;
415 validate_finite(dz, "dz")?;
416 validate_finite(angle_degrees, "angle_degrees")?;
417 if angle_degrees <= 0.0 || angle_degrees > 360.0 {
418 return Err(WasmError::InvalidInput {
419 reason: format!("angle_degrees must be in (0, 360], got {angle_degrees}"),
420 }
421 .into());
422 }
423
424 let face_id = self.resolve_face(face)?;
425 let origin = Point3::new(ox, oy, oz);
426 let direction = Vec3::new(dx, dy, dz);
427 let angle_radians = angle_degrees.to_radians();
428
429 let solid_id = revolve(self.topo_mut(), face_id, origin, direction, angle_radians)?;
430
431 Ok(solid_id_to_u32(solid_id))
432 }
433
434 #[wasm_bindgen(js_name = "sweep")]
449 #[allow(clippy::needless_pass_by_value)] pub fn sweep_face(
451 &mut self,
452 face: u32,
453 path_degree: u32,
454 path_knots: Vec<f64>,
455 path_control_points: Vec<f64>,
456 path_weights: Vec<f64>,
457 ) -> Result<u32, JsError> {
458 if !path_control_points.len().is_multiple_of(3) {
460 return Err(WasmError::InvalidInput {
461 reason: format!(
462 "path_control_points length must be a multiple of 3, got {}",
463 path_control_points.len()
464 ),
465 }
466 .into());
467 }
468 let num_pts = path_control_points.len() / 3;
469
470 if path_weights.len() != num_pts {
471 return Err(WasmError::InvalidInput {
472 reason: format!(
473 "path_weights length ({}) must match number of control points ({num_pts})",
474 path_weights.len()
475 ),
476 }
477 .into());
478 }
479
480 if let Some(pos) = path_knots.iter().position(|v| !v.is_finite()) {
482 return Err(WasmError::InvalidInput {
483 reason: format!("path_knots[{pos}] is not finite"),
484 }
485 .into());
486 }
487 if let Some(pos) = path_control_points.iter().position(|v| !v.is_finite()) {
488 return Err(WasmError::InvalidInput {
489 reason: format!("path_control_points[{pos}] is not finite"),
490 }
491 .into());
492 }
493 if let Some(pos) = path_weights.iter().position(|v| !v.is_finite()) {
494 return Err(WasmError::InvalidInput {
495 reason: format!("path_weights[{pos}] is not finite"),
496 }
497 .into());
498 }
499
500 let face_id = self.resolve_face(face)?;
501
502 let control_points: Vec<Point3> = path_control_points
503 .chunks_exact(3)
504 .map(|c| Point3::new(c[0], c[1], c[2]))
505 .collect();
506
507 let path_curve = NurbsCurve::new(
508 path_degree as usize,
509 path_knots,
510 control_points,
511 path_weights,
512 )?;
513
514 let solid_id = sweep(self.topo_mut(), face_id, &path_curve)?;
515
516 Ok(solid_id_to_u32(solid_id))
517 }
518
519 #[wasm_bindgen(js_name = "multiSectionSweep")]
533 #[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
534 pub fn multi_section_sweep(
535 &mut self,
536 face_handles: Vec<u32>,
537 params: Vec<f64>,
538 spine_degree: u32,
539 spine_knots: Vec<f64>,
540 spine_control_points: Vec<f64>,
541 spine_weights: Vec<f64>,
542 ruled: bool,
543 ) -> Result<u32, JsError> {
544 if face_handles.len() != params.len() {
545 return Err(WasmError::InvalidInput {
546 reason: format!(
547 "face_handles ({}) and params ({}) must have equal length",
548 face_handles.len(),
549 params.len()
550 ),
551 }
552 .into());
553 }
554 if !spine_control_points.len().is_multiple_of(3) {
555 return Err(WasmError::InvalidInput {
556 reason: format!(
557 "spine_control_points length must be a multiple of 3, got {}",
558 spine_control_points.len()
559 ),
560 }
561 .into());
562 }
563 let num_pts = spine_control_points.len() / 3;
564 if spine_weights.len() != num_pts {
565 return Err(WasmError::InvalidInput {
566 reason: format!(
567 "spine_weights length ({}) must match control point count ({num_pts})",
568 spine_weights.len()
569 ),
570 }
571 .into());
572 }
573 for p in ¶ms {
574 validate_finite(*p, "param")?;
575 }
576 for (name, arr) in [
577 ("spine_knots", &spine_knots),
578 ("spine_control_points", &spine_control_points),
579 ("spine_weights", &spine_weights),
580 ] {
581 if let Some(pos) = arr.iter().position(|v| !v.is_finite()) {
582 return Err(WasmError::InvalidInput {
583 reason: format!("{name}[{pos}] is not finite"),
584 }
585 .into());
586 }
587 }
588
589 let control_points: Vec<Point3> = spine_control_points
590 .chunks_exact(3)
591 .map(|c| Point3::new(c[0], c[1], c[2]))
592 .collect();
593 let spine = NurbsCurve::new(
594 spine_degree as usize,
595 spine_knots,
596 control_points,
597 spine_weights,
598 )?;
599
600 let sections: Vec<(brepkit_topology::face::FaceId, f64)> = face_handles
601 .iter()
602 .zip(params.iter())
603 .map(|(&h, &p)| self.resolve_face(h).map(|f| (f, p)))
604 .collect::<Result<_, _>>()?;
605
606 let solid_id = brepkit_operations::sweep::multi_section_sweep(
607 self.topo_mut(),
608 &spine,
609 §ions,
610 ruled,
611 )?;
612 Ok(solid_id_to_u32(solid_id))
613 }
614
615 #[wasm_bindgen(js_name = "sweepSmooth")]
627 #[allow(clippy::needless_pass_by_value)]
628 pub fn sweep_smooth_face(
629 &mut self,
630 face: u32,
631 path_degree: u32,
632 path_knots: Vec<f64>,
633 path_control_points: Vec<f64>,
634 path_weights: Vec<f64>,
635 ) -> Result<u32, JsError> {
636 if !path_control_points.len().is_multiple_of(3) {
637 return Err(WasmError::InvalidInput {
638 reason: format!(
639 "path_control_points length must be a multiple of 3, got {}",
640 path_control_points.len()
641 ),
642 }
643 .into());
644 }
645
646 let face_id = self.resolve_face(face)?;
647 let n_cp = path_control_points.len() / 3;
648 let control_points: Vec<Point3> = (0..n_cp)
649 .map(|i| {
650 Point3::new(
651 path_control_points[i * 3],
652 path_control_points[i * 3 + 1],
653 path_control_points[i * 3 + 2],
654 )
655 })
656 .collect();
657
658 let weights = if path_weights.is_empty() {
659 vec![1.0; n_cp]
660 } else {
661 path_weights
662 };
663
664 #[allow(clippy::cast_possible_truncation)]
665 let path_curve = brepkit_math::nurbs::curve::NurbsCurve::new(
666 path_degree as usize,
667 path_knots,
668 control_points,
669 weights,
670 )?;
671
672 let solid_id =
673 brepkit_operations::sweep::sweep_smooth(self.topo_mut(), face_id, &path_curve)?;
674 Ok(solid_id_to_u32(solid_id))
675 }
676
677 #[wasm_bindgen(js_name = "offsetFace")]
687 pub fn offset_face(&mut self, face: u32, distance: f64, samples: u32) -> Result<u32, JsError> {
688 validate_finite(distance, "distance")?;
689 let face_id = self.resolve_face(face)?;
690 let result = brepkit_operations::offset_face::offset_face(
691 self.topo_mut(),
692 face_id,
693 distance,
694 samples as usize,
695 )?;
696 Ok(face_id_to_u32(result))
697 }
698
699 #[wasm_bindgen(js_name = "helicalSweep")]
710 #[allow(clippy::too_many_arguments)]
711 pub fn helical_sweep(
712 &mut self,
713 profile: u32,
714 axis_origin_x: f64,
715 axis_origin_y: f64,
716 axis_origin_z: f64,
717 axis_dir_x: f64,
718 axis_dir_y: f64,
719 axis_dir_z: f64,
720 radius: f64,
721 pitch: f64,
722 turns: f64,
723 ) -> Result<u32, JsError> {
724 validate_positive(radius, "radius")?;
725 validate_positive(pitch, "pitch")?;
726 let face_id = self.resolve_face(profile)?;
727
728 let origin = brepkit_math::vec::Point3::new(axis_origin_x, axis_origin_y, axis_origin_z);
729 let axis_dir = brepkit_math::vec::Vec3::new(axis_dir_x, axis_dir_y, axis_dir_z);
730
731 let solid_id = brepkit_operations::helix::helical_sweep(
732 self.topo_mut(),
733 face_id,
734 origin,
735 axis_dir,
736 radius,
737 pitch,
738 turns,
739 8,
740 )?;
741 Ok(solid_id_to_u32(solid_id))
742 }
743
744 #[wasm_bindgen(js_name = "split")]
754 #[allow(clippy::too_many_arguments)]
755 pub fn split_solid(
756 &mut self,
757 solid: u32,
758 px: f64,
759 py: f64,
760 pz: f64,
761 nx: f64,
762 ny: f64,
763 nz: f64,
764 ) -> Result<Vec<u32>, JsError> {
765 validate_finite(px, "px")?;
766 validate_finite(py, "py")?;
767 validate_finite(pz, "pz")?;
768 validate_finite(nx, "nx")?;
769 validate_finite(ny, "ny")?;
770 validate_finite(nz, "nz")?;
771 let solid_id = self.resolve_solid(solid)?;
772 let result = brepkit_operations::split::split(
773 self.topo_mut(),
774 solid_id,
775 Point3::new(px, py, pz),
776 Vec3::new(nx, ny, nz),
777 )?;
778 Ok(vec![
779 solid_id_to_u32(result.positive),
780 solid_id_to_u32(result.negative),
781 ])
782 }
783
784 #[wasm_bindgen(js_name = "draft")]
795 #[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)]
796 pub fn draft_solid(
797 &mut self,
798 solid: u32,
799 face_handles: Vec<u32>,
800 pull_x: f64,
801 pull_y: f64,
802 pull_z: f64,
803 neutral_x: f64,
804 neutral_y: f64,
805 neutral_z: f64,
806 angle_degrees: f64,
807 ) -> Result<u32, JsError> {
808 validate_finite(angle_degrees, "angle_degrees")?;
809 let solid_id = self.resolve_solid(solid)?;
810 let face_ids: Vec<brepkit_topology::face::FaceId> = face_handles
811 .iter()
812 .map(|&h| self.resolve_face(h))
813 .collect::<Result<_, _>>()?;
814 let result = brepkit_operations::draft::draft(
815 self.topo_mut(),
816 solid_id,
817 &face_ids,
818 Vec3::new(pull_x, pull_y, pull_z),
819 Point3::new(neutral_x, neutral_y, neutral_z),
820 angle_degrees.to_radians(),
821 )?;
822 Ok(solid_id_to_u32(result))
823 }
824
825 #[wasm_bindgen(js_name = "pipe")]
835 #[allow(clippy::needless_pass_by_value)]
836 pub fn pipe_solid(
837 &mut self,
838 face: u32,
839 path_degree: u32,
840 path_knots: Vec<f64>,
841 path_control_points: Vec<f64>,
842 path_weights: Vec<f64>,
843 ) -> Result<u32, JsError> {
844 if !path_control_points.len().is_multiple_of(3) {
845 return Err(WasmError::InvalidInput {
846 reason: format!(
847 "path_control_points length must be a multiple of 3, got {}",
848 path_control_points.len()
849 ),
850 }
851 .into());
852 }
853
854 let face_id = self.resolve_face(face)?;
855 let control_points: Vec<Point3> = path_control_points
856 .chunks_exact(3)
857 .map(|c| Point3::new(c[0], c[1], c[2]))
858 .collect();
859
860 let path_curve = NurbsCurve::new(
861 path_degree as usize,
862 path_knots,
863 control_points,
864 path_weights,
865 )?;
866
867 let solid_id = brepkit_operations::pipe::pipe(self.topo_mut(), face_id, &path_curve, None)?;
868 Ok(solid_id_to_u32(solid_id))
869 }
870
871 #[wasm_bindgen(js_name = "sweepAlongEdges")]
886 #[allow(clippy::needless_pass_by_value)]
887 pub fn sweep_along_edges(&mut self, face: u32, edge_handles: Vec<u32>) -> Result<u32, JsError> {
888 if edge_handles.is_empty() {
889 return Err(WasmError::InvalidInput {
890 reason: "sweepAlongEdges requires at least one edge".into(),
891 }
892 .into());
893 }
894 let edge_ids: Vec<brepkit_topology::edge::EdgeId> = edge_handles
895 .iter()
896 .map(|&eh| self.resolve_edge(eh))
897 .collect::<Result<_, _>>()?;
898 let face_id = self.resolve_face(face)?;
899 let solid_id =
900 brepkit_operations::sweep::sweep_along_edges(self.topo_mut(), face_id, &edge_ids)?;
901 Ok(solid_id_to_u32(solid_id))
902 }
903
904 #[wasm_bindgen(js_name = "offsetSolid")]
914 pub fn offset_solid(&mut self, solid: u32, distance: f64) -> Result<u32, JsError> {
915 validate_finite(distance, "distance")?;
916 let solid_id = self.resolve_solid(solid)?;
917 let result =
918 brepkit_operations::offset_v2::offset_solid_v2(self.topo_mut(), solid_id, distance)?;
919 Ok(solid_id_to_u32(result))
920 }
921
922 #[wasm_bindgen(js_name = "offsetSolidV2")]
930 pub fn offset_solid_v2(&mut self, solid: u32, distance: f64) -> Result<u32, JsError> {
931 validate_finite(distance, "distance")?;
932 let sid = self.resolve_solid(solid)?;
933 let result =
934 brepkit_operations::offset_v2::offset_solid_v2(self.topo_mut(), sid, distance)?;
935 Ok(solid_id_to_u32(result))
936 }
937
938 #[wasm_bindgen(js_name = "thicken")]
947 pub fn thicken_face(&mut self, face: u32, thickness: f64) -> Result<u32, JsError> {
948 validate_finite(thickness, "thickness")?;
949 let face_id = self.resolve_face(face)?;
950 let result = brepkit_operations::thicken::thicken(self.topo_mut(), face_id, thickness)?;
951 Ok(solid_id_to_u32(result))
952 }
953
954 #[wasm_bindgen(js_name = "filletVariable")]
965 pub fn fillet_variable(&mut self, solid: u32, json: &str) -> Result<u32, JsError> {
966 let solid_id = self.resolve_solid(solid)?;
967 let specs: Vec<serde_json::Value> =
968 serde_json::from_str(json).map_err(|e| WasmError::InvalidInput {
969 reason: format!("invalid JSON: {e}"),
970 })?;
971 let mut edge_laws = Vec::with_capacity(specs.len());
972 for spec in &specs {
973 let edge_handle = spec["edge"]
974 .as_u64()
975 .ok_or_else(|| WasmError::InvalidInput {
976 reason: "missing 'edge' in fillet spec".into(),
977 })? as u32;
978 let edge_id = self.resolve_edge(edge_handle)?;
979 let start_val = spec["start"]
981 .as_f64()
982 .or_else(|| spec["startRadius"].as_f64());
983 let end_val = spec["end"].as_f64().or_else(|| spec["endRadius"].as_f64());
984
985 let law_str = spec["law"]
987 .as_str()
988 .unwrap_or_else(|| match (start_val, end_val) {
989 (Some(s), Some(e)) if (s - e).abs() > f64::EPSILON => "linear",
990 _ => "constant",
991 });
992 let law = match law_str {
993 "linear" => {
994 let s = start_val.unwrap_or(1.0);
995 let e = end_val.unwrap_or(1.0);
996 brepkit_operations::fillet::FilletRadiusLaw::Linear { start: s, end: e }
997 }
998 "scurve" => {
999 let s = start_val.unwrap_or(1.0);
1000 let e = end_val.unwrap_or(1.0);
1001 brepkit_operations::fillet::FilletRadiusLaw::SCurve { start: s, end: e }
1002 }
1003 _ => {
1004 let r = spec["radius"].as_f64().or(start_val).unwrap_or(1.0);
1005 brepkit_operations::fillet::FilletRadiusLaw::Constant(r)
1006 }
1007 };
1008 edge_laws.push((edge_id, law));
1009 }
1010 let result =
1011 brepkit_operations::fillet::fillet_variable(self.topo_mut(), solid_id, &edge_laws)?;
1012 Ok(solid_id_to_u32(result))
1013 }
1014
1015 #[wasm_bindgen(js_name = "sweepWithOptions")]
1022 #[allow(clippy::needless_pass_by_value)]
1023 pub fn sweep_with_options(
1024 &mut self,
1025 profile: u32,
1026 path_edge: u32,
1027 contact_mode: &str,
1028 scale_values: Vec<f64>,
1029 segments: u32,
1030 corner_mode: &str,
1031 ) -> Result<u32, JsError> {
1032 use brepkit_operations::sweep::{SweepContactMode, SweepCornerMode, SweepOptions};
1033
1034 let face_id = self.resolve_face(profile)?;
1035 let path_curve = self.extract_nurbs_curve(path_edge)?;
1036
1037 let mode = if contact_mode == "fixed" {
1038 SweepContactMode::Fixed
1039 } else if let Some(rest) = contact_mode.strip_prefix("constantNormal:") {
1040 let parts: Vec<f64> = rest
1041 .split(',')
1042 .filter_map(|s| s.trim().parse().ok())
1043 .collect();
1044 if parts.len() >= 3 {
1045 SweepContactMode::ConstantNormal(Vec3::new(parts[0], parts[1], parts[2]))
1046 } else {
1047 SweepContactMode::RotationMinimizing
1048 }
1049 } else {
1050 SweepContactMode::RotationMinimizing
1051 };
1052
1053 let scale_law: Option<Box<dyn Fn(f64) -> f64 + Send + Sync>> =
1054 if scale_values.len() >= 4 && scale_values.len().is_multiple_of(2) {
1055 let pairs: Vec<(f64, f64)> =
1056 scale_values.chunks_exact(2).map(|c| (c[0], c[1])).collect();
1057 Some(Box::new(move |t: f64| -> f64 {
1058 if pairs.is_empty() {
1060 return 1.0;
1061 }
1062 if t <= pairs[0].0 {
1063 return pairs[0].1;
1064 }
1065 if t >= pairs[pairs.len() - 1].0 {
1066 return pairs[pairs.len() - 1].1;
1067 }
1068 for w in pairs.windows(2) {
1069 if t >= w[0].0 && t <= w[1].0 {
1070 let frac = (t - w[0].0) / (w[1].0 - w[0].0);
1071 return w[0].1 + frac * (w[1].1 - w[0].1);
1072 }
1073 }
1074 1.0
1075 }))
1076 } else {
1077 None
1078 };
1079
1080 let cm = match corner_mode {
1081 "miter" => SweepCornerMode::Miter,
1082 "round" => SweepCornerMode::Round,
1083 _ => SweepCornerMode::Smooth,
1084 };
1085
1086 let options = SweepOptions {
1087 contact_mode: mode,
1088 corner_mode: cm,
1089 scale_law,
1090 segments: segments as usize,
1091 aux_spine: None,
1092 };
1093
1094 let result = brepkit_operations::sweep::sweep_with_options(
1095 self.topo_mut(),
1096 face_id,
1097 &path_curve,
1098 &options,
1099 )?;
1100 Ok(solid_id_to_u32(result))
1101 }
1102
1103 #[wasm_bindgen(js_name = "guidedSweep")]
1115 #[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
1116 pub fn guided_sweep(
1117 &mut self,
1118 face: u32,
1119 spine_degree: u32,
1120 spine_knots: Vec<f64>,
1121 spine_control_points: Vec<f64>,
1122 spine_weights: Vec<f64>,
1123 aux_degree: u32,
1124 aux_knots: Vec<f64>,
1125 aux_control_points: Vec<f64>,
1126 aux_weights: Vec<f64>,
1127 ) -> Result<u32, JsError> {
1128 let build = |degree: u32,
1129 knots: Vec<f64>,
1130 cps: Vec<f64>,
1131 weights: Vec<f64>,
1132 label: &str|
1133 -> Result<NurbsCurve, JsError> {
1134 if degree < 1 {
1135 return Err(WasmError::InvalidInput {
1136 reason: format!("{label}_degree must be at least 1"),
1137 }
1138 .into());
1139 }
1140 if !cps.len().is_multiple_of(3) {
1141 return Err(WasmError::InvalidInput {
1142 reason: format!("{label}_control_points length must be a multiple of 3"),
1143 }
1144 .into());
1145 }
1146 if weights.len() != cps.len() / 3 {
1147 return Err(WasmError::InvalidInput {
1148 reason: format!("{label}_weights length must match control point count"),
1149 }
1150 .into());
1151 }
1152 for (name, arr) in [
1153 ("knots", &knots),
1154 ("control_points", &cps),
1155 ("weights", &weights),
1156 ] {
1157 if let Some(pos) = arr.iter().position(|v| !v.is_finite()) {
1158 return Err(WasmError::InvalidInput {
1159 reason: format!("{label}_{name}[{pos}] is not finite"),
1160 }
1161 .into());
1162 }
1163 }
1164 let control_points: Vec<Point3> = cps
1165 .chunks_exact(3)
1166 .map(|c| Point3::new(c[0], c[1], c[2]))
1167 .collect();
1168 Ok(NurbsCurve::new(
1169 degree as usize,
1170 knots,
1171 control_points,
1172 weights,
1173 )?)
1174 };
1175
1176 let spine = build(
1177 spine_degree,
1178 spine_knots,
1179 spine_control_points,
1180 spine_weights,
1181 "spine",
1182 )?;
1183 let aux = build(
1184 aux_degree,
1185 aux_knots,
1186 aux_control_points,
1187 aux_weights,
1188 "aux",
1189 )?;
1190 let face_id = self.resolve_face(face)?;
1191 let solid = brepkit_operations::sweep::sweep_guided(self.topo_mut(), face_id, &spine, aux)?;
1192 Ok(solid_id_to_u32(solid))
1193 }
1194
1195 #[wasm_bindgen(js_name = "minkowskiSum")]
1206 pub fn minkowski_sum(&mut self, solid_a: u32, solid_b: u32) -> Result<u32, JsError> {
1207 let a = self.resolve_solid(solid_a)?;
1208 let b = self.resolve_solid(solid_b)?;
1209 let result = brepkit_operations::primitives::make_minkowski_sum(self.topo_mut(), a, b)?;
1210 Ok(solid_id_to_u32(result))
1211 }
1212
1213 #[wasm_bindgen(js_name = "projectEdges")]
1225 #[allow(clippy::too_many_arguments)]
1226 pub fn project_edges(
1227 &self,
1228 solid: u32,
1229 origin_x: f64,
1230 origin_y: f64,
1231 origin_z: f64,
1232 dir_x: f64,
1233 dir_y: f64,
1234 dir_z: f64,
1235 x_axis_x: f64,
1236 x_axis_y: f64,
1237 x_axis_z: f64,
1238 hidden_lines: bool,
1239 deflection: f64,
1240 ) -> Result<JsValue, JsError> {
1241 validate_positive(deflection, "deflection")?;
1242 for (v, name) in [
1243 (origin_x, "origin_x"),
1244 (origin_y, "origin_y"),
1245 (origin_z, "origin_z"),
1246 (dir_x, "dir_x"),
1247 (dir_y, "dir_y"),
1248 (dir_z, "dir_z"),
1249 (x_axis_x, "x_axis_x"),
1250 (x_axis_y, "x_axis_y"),
1251 (x_axis_z, "x_axis_z"),
1252 ] {
1253 validate_finite(v, name)?;
1254 }
1255 let solid_id = self.resolve_solid(solid)?;
1256 let result = brepkit_operations::projection::project_edges(
1257 &self.topo,
1258 solid_id,
1259 Point3::new(origin_x, origin_y, origin_z),
1260 Vec3::new(dir_x, dir_y, dir_z),
1261 Vec3::new(x_axis_x, x_axis_y, x_axis_z),
1262 hidden_lines,
1263 deflection,
1264 )?;
1265 let flatten = |polys: &[Vec<brepkit_math::vec::Point2>]| -> Vec<Vec<f64>> {
1266 polys
1267 .iter()
1268 .map(|poly| poly.iter().flat_map(|p| [p.x(), p.y()]).collect())
1269 .collect()
1270 };
1271 let json = serde_json::json!({
1272 "visible": flatten(&result.visible),
1273 "hidden": flatten(&result.hidden),
1274 });
1275 Ok(JsValue::from_str(&json.to_string()))
1276 }
1277
1278 #[wasm_bindgen(js_name = "classifyPointWinding")]
1284 pub fn classify_point_winding(
1285 &self,
1286 solid: u32,
1287 x: f64,
1288 y: f64,
1289 z: f64,
1290 tolerance: f64,
1291 ) -> Result<String, JsError> {
1292 let solid_id = self.resolve_solid(solid)?;
1293 let point = Point3::new(x, y, z);
1294 let result = brepkit_operations::classify::classify_point_winding(
1295 &self.topo, solid_id, point, 0.1, tolerance,
1296 )?;
1297 Ok(classify_to_string(result))
1298 }
1299
1300 #[wasm_bindgen(js_name = "classifyPointRobust")]
1304 pub fn classify_point_robust(
1305 &self,
1306 solid: u32,
1307 x: f64,
1308 y: f64,
1309 z: f64,
1310 tolerance: f64,
1311 ) -> Result<String, JsError> {
1312 let solid_id = self.resolve_solid(solid)?;
1313 let point = Point3::new(x, y, z);
1314 let result = brepkit_operations::classify::classify_point_robust(
1315 &self.topo, solid_id, point, 0.1, tolerance,
1316 )?;
1317 Ok(classify_to_string(result))
1318 }
1319
1320 #[wasm_bindgen(js_name = "fillCoonsPatch")]
1328 #[allow(clippy::needless_pass_by_value)]
1329 pub fn fill_coons_patch(
1330 &mut self,
1331 boundary_coords: Vec<f64>,
1332 curve_lengths: Vec<u32>,
1333 ) -> Result<u32, JsError> {
1334 if curve_lengths.len() != 4 {
1335 return Err(WasmError::InvalidInput {
1336 reason: format!(
1337 "Coons patch requires exactly 4 boundary curves, got {}",
1338 curve_lengths.len()
1339 ),
1340 }
1341 .into());
1342 }
1343 let points = parse_points(&boundary_coords)?;
1344 let mut curves: Vec<Vec<Point3>> = Vec::with_capacity(4);
1345 let mut offset = 0usize;
1346 for &len in &curve_lengths {
1347 let l = len as usize;
1348 if offset + l > points.len() {
1349 return Err(WasmError::InvalidInput {
1350 reason: "curve_lengths exceed total coordinate count".into(),
1351 }
1352 .into());
1353 }
1354 curves.push(points[offset..offset + l].to_vec());
1355 offset += l;
1356 }
1357 let face_id = brepkit_operations::fill_face::fill_coons_patch(self.topo_mut(), &curves)?;
1358 Ok(face_id_to_u32(face_id))
1359 }
1360
1361 #[wasm_bindgen(js_name = "untrimFace")]
1365 pub fn untrim_face(
1366 &mut self,
1367 face: u32,
1368 samples_per_curve: u32,
1369 interior_samples: u32,
1370 ) -> Result<u32, JsError> {
1371 let face_id = self.resolve_face(face)?;
1372 let face_data = self.topo.face(face_id)?;
1373 let surface = match face_data.surface() {
1374 FaceSurface::Nurbs(s) => s.clone(),
1375 _ => {
1376 return Err(WasmError::InvalidInput {
1377 reason: "untrim only works on NURBS faces".into(),
1378 }
1379 .into());
1380 }
1381 };
1382 let wire_id = face_data.outer_wire();
1384 let wire = self.topo.wire(wire_id)?;
1385 let mut trim_curves = Vec::new();
1386 for oe in wire.edges() {
1387 let edge = self.topo.edge(oe.edge())?;
1388 let v_start = self.topo.vertex(edge.start())?;
1389 let v_end = self.topo.vertex(edge.end())?;
1390 let proj_start = point_to_nurbs_surface(v_start.point(), &surface);
1392 let uv_start = brepkit_math::vec::Point2::new(proj_start.u, proj_start.v);
1393 let proj_end = point_to_nurbs_surface(v_end.point(), &surface);
1394 let uv_end = brepkit_math::vec::Point2::new(proj_end.u, proj_end.v);
1395 trim_curves.push(brepkit_operations::untrim::TrimCurve {
1396 curve: vec![uv_start, uv_end],
1397 });
1398 }
1399 let new_surface = brepkit_operations::untrim::untrim_face(
1400 &surface,
1401 &trim_curves,
1402 samples_per_curve as usize,
1403 interior_samples as usize,
1404 )?;
1405 Ok(face_id_to_u32(self.nurbs_surface_to_face(new_surface)?))
1406 }
1407
1408 #[wasm_bindgen(js_name = "offsetWire")]
1412 pub fn offset_wire(&mut self, face: u32, distance: f64) -> Result<u32, JsError> {
1413 let face_id = self.resolve_face(face)?;
1414 let wire_id =
1415 brepkit_operations::offset_wire::offset_wire(self.topo_mut(), face_id, distance)?;
1416 Ok(wire_id_to_u32(wire_id))
1417 }
1418
1419 #[wasm_bindgen(js_name = "offsetWireWithJoinType")]
1429 pub fn offset_wire_with_join_type(
1430 &mut self,
1431 face: u32,
1432 distance: f64,
1433 join_type: &str,
1434 ) -> Result<u32, JsError> {
1435 let face_id = self.resolve_face(face)?;
1436 let jt = parse_join_type_str(join_type)?;
1437 let wire_id = brepkit_operations::offset_wire::offset_wire_with_join(
1438 self.topo_mut(),
1439 face_id,
1440 distance,
1441 jt,
1442 )?;
1443 Ok(wire_id_to_u32(wire_id))
1444 }
1445
1446 #[wasm_bindgen(js_name = "offsetWire2DWithJoin")]
1464 pub fn offset_wire_2d_with_join(
1465 &mut self,
1466 wire: u32,
1467 distance: f64,
1468 join_type: &str,
1469 ) -> Result<u32, JsError> {
1470 let wire_id = self.resolve_wire(wire)?;
1471 let jt = parse_join_type_str(join_type)?;
1472 let face_id =
1473 brepkit_topology::builder::make_planar_face_from_wire(self.topo_mut(), wire_id)?;
1474 let result = brepkit_operations::offset_wire::offset_wire_with_join(
1475 self.topo_mut(),
1476 face_id,
1477 distance,
1478 jt,
1479 )?;
1480 Ok(wire_id_to_u32(result))
1481 }
1482
1483 #[allow(clippy::unused_self)]
1490 #[must_use]
1491 #[wasm_bindgen(js_name = "getShapeOrientation")]
1492 pub fn get_shape_orientation(&self, _id: u32) -> String {
1493 "forward".to_string()
1496 }
1497
1498 #[wasm_bindgen(js_name = "reverseShape")]
1508 pub fn reverse_shape(&mut self, id: u32) -> Result<u32, JsError> {
1509 if let Ok(face_id) = self.resolve_face(id) {
1511 let face = self.topo.face(face_id)?;
1512 let outer_wire = face.outer_wire();
1513 let inner_wires: Vec<_> = face.inner_wires().to_vec();
1514 let new_surface = match face.surface() {
1515 FaceSurface::Plane { normal, d } => FaceSurface::Plane {
1516 normal: -*normal,
1517 d: -*d,
1518 },
1519 other => other.clone(),
1520 };
1521 let new_face = Face::new(outer_wire, inner_wires, new_surface);
1522 let new_fid = self.topo_mut().add_face(new_face);
1523 return Ok(face_id_to_u32(new_fid));
1524 }
1525 if let Ok(edge_id) = self.resolve_edge(id) {
1527 let edge = self.topo.edge(edge_id)?;
1528 let new_edge = Edge::new(edge.end(), edge.start(), edge.curve().clone());
1529 let new_eid = self.topo_mut().add_edge(new_edge);
1530 return Ok(edge_id_to_u32(new_eid));
1531 }
1532 Err(WasmError::InvalidInput {
1533 reason: "reverseShape requires a face or edge handle".into(),
1534 }
1535 .into())
1536 }
1537
1538 #[wasm_bindgen(js_name = "filletV2")]
1549 #[allow(clippy::needless_pass_by_value)]
1550 pub fn fillet_v2(
1551 &mut self,
1552 solid: u32,
1553 edge_handles: Vec<u32>,
1554 radius: f64,
1555 ) -> Result<u32, JsError> {
1556 validate_positive(radius, "radius")?;
1557 let solid_id = self.resolve_solid(solid)?;
1558 let edge_ids: Vec<_> = edge_handles
1559 .iter()
1560 .map(|&h| self.resolve_edge(h))
1561 .collect::<Result<_, _>>()?;
1562 let result =
1563 brepkit_operations::blend_ops::fillet_v2(self.topo_mut(), solid_id, &edge_ids, radius)?;
1564 Ok(solid_id_to_u32(result.solid))
1565 }
1566
1567 #[wasm_bindgen(js_name = "chamferV2")]
1576 #[allow(clippy::needless_pass_by_value)]
1577 pub fn chamfer_v2(
1578 &mut self,
1579 solid: u32,
1580 edge_handles: Vec<u32>,
1581 d1: f64,
1582 d2: f64,
1583 ) -> Result<u32, JsError> {
1584 validate_positive(d1, "d1")?;
1585 validate_positive(d2, "d2")?;
1586 let solid_id = self.resolve_solid(solid)?;
1587 let edge_ids: Vec<_> = edge_handles
1588 .iter()
1589 .map(|&h| self.resolve_edge(h))
1590 .collect::<Result<_, _>>()?;
1591 let result = brepkit_operations::blend_ops::chamfer_v2(
1592 self.topo_mut(),
1593 solid_id,
1594 &edge_ids,
1595 d1,
1596 d2,
1597 )?;
1598 Ok(solid_id_to_u32(result.solid))
1599 }
1600
1601 #[wasm_bindgen(js_name = "chamferDistanceAngle")]
1610 #[allow(clippy::needless_pass_by_value)]
1611 pub fn chamfer_distance_angle(
1612 &mut self,
1613 solid: u32,
1614 edge_handles: Vec<u32>,
1615 distance: f64,
1616 angle: f64,
1617 ) -> Result<u32, JsError> {
1618 validate_positive(distance, "distance")?;
1619 validate_positive(angle, "angle")?;
1620 if angle >= std::f64::consts::FRAC_PI_2 {
1621 return Err(JsError::new("angle must be less than π/2"));
1622 }
1623 let solid_id = self.resolve_solid(solid)?;
1624 let edge_ids: Vec<_> = edge_handles
1625 .iter()
1626 .map(|&h| self.resolve_edge(h))
1627 .collect::<Result<_, _>>()?;
1628 let result = brepkit_operations::blend_ops::chamfer_distance_angle(
1629 self.topo_mut(),
1630 solid_id,
1631 &edge_ids,
1632 distance,
1633 angle,
1634 )?;
1635 Ok(solid_id_to_u32(result.solid))
1636 }
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641 #![allow(clippy::unwrap_used, clippy::expect_used)]
1642
1643 use brepkit_math::vec::Point3;
1644 use brepkit_topology::builder::make_polygon_wire;
1645
1646 use crate::handles::{solid_id_to_u32, wire_id_to_u32};
1647 use crate::helpers::TOL;
1648 use crate::kernel::BrepKernel;
1649
1650 fn square_wire(k: &mut BrepKernel) -> u32 {
1651 let pts = [
1652 Point3::new(0.0, 0.0, 0.0),
1653 Point3::new(10.0, 0.0, 0.0),
1654 Point3::new(10.0, 10.0, 0.0),
1655 Point3::new(0.0, 10.0, 0.0),
1656 ];
1657 let wid = make_polygon_wire(k.topo_mut(), &pts, TOL).unwrap();
1658 wire_id_to_u32(wid)
1659 }
1660
1661 fn dispatch(k: &mut BrepKernel, op: &str, args: serde_json::Value) -> serde_json::Value {
1662 let batch = serde_json::json!([{ "op": op, "args": args }]);
1663 let out = k.execute_batch(&batch.to_string());
1664 let parsed: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
1665 parsed[0].clone()
1666 }
1667
1668 fn wire_perimeter(k: &BrepKernel, wire_handle: u32) -> f64 {
1669 let wid = k.resolve_wire(wire_handle).unwrap();
1670 brepkit_operations::measure::wire_length(&k.topo, wid).unwrap()
1671 }
1672
1673 #[test]
1674 fn multi_section_sweep_lofts_circles_along_line() {
1675 let mut k = BrepKernel::new();
1676 let big = k.make_circle_face(10.0, 24).unwrap();
1677 let small = k.make_circle_face(5.0, 24).unwrap();
1678 let solid = k
1680 .multi_section_sweep(
1681 vec![big, small],
1682 vec![0.0, 1.0],
1683 1,
1684 vec![0.0, 0.0, 1.0, 1.0],
1685 vec![0.0, 0.0, 0.0, 0.0, 0.0, 50.0],
1686 vec![1.0, 1.0],
1687 true,
1688 )
1689 .unwrap();
1690 let vol = k.volume(solid, 0.5).unwrap();
1691 assert!(
1692 vol > 0.0,
1693 "tapered tube should have positive volume, got {vol}"
1694 );
1695 }
1699
1700 #[test]
1701 fn multi_section_sweep_batch_dispatch_lofts_circles() {
1702 let mut k = BrepKernel::new();
1703 let big = k.make_circle_face(10.0, 24).unwrap();
1704 let small = k.make_circle_face(5.0, 24).unwrap();
1705 let spine = k
1707 .make_nurbs_edge(
1708 0.0,
1709 0.0,
1710 0.0,
1711 0.0,
1712 0.0,
1713 50.0,
1714 1,
1715 vec![0.0, 0.0, 1.0, 1.0],
1716 vec![0.0, 0.0, 0.0, 0.0, 0.0, 50.0],
1717 vec![1.0, 1.0],
1718 )
1719 .unwrap();
1720 let out = dispatch(
1721 &mut k,
1722 "multiSectionSweep",
1723 serde_json::json!({
1724 "faces": [big, small],
1725 "params": [0.0, 1.0],
1726 "spineEdge": spine,
1727 "ruled": true,
1728 }),
1729 );
1730 assert!(
1731 out.get("ok").and_then(serde_json::Value::as_u64).is_some(),
1732 "expected an ok solid handle, got {out}"
1733 );
1734 }
1735
1736 #[test]
1737 fn guided_sweep_produces_solid() {
1738 let mut k = BrepKernel::new();
1739 let profile = k.make_circle_face(2.0, 24).unwrap();
1740 let solid = k
1742 .guided_sweep(
1743 profile,
1744 1,
1745 vec![0.0, 0.0, 1.0, 1.0],
1746 vec![0.0, 0.0, 0.0, 0.0, 0.0, 20.0],
1747 vec![1.0, 1.0],
1748 1,
1749 vec![0.0, 0.0, 1.0, 1.0],
1750 vec![10.0, 0.0, 0.0, 10.0, 0.0, 20.0],
1751 vec![1.0, 1.0],
1752 )
1753 .unwrap();
1754 let vol = k.volume(solid, 0.5).unwrap();
1755 assert!(vol > 0.0, "guided sweep volume, got {vol}");
1756 }
1757
1758 #[test]
1759 fn guided_sweep_batch_dispatch() {
1760 let mut k = BrepKernel::new();
1761 let profile = k.make_circle_face(2.0, 24).unwrap();
1762 let mk_line = |k: &mut BrepKernel, x: f64| {
1763 k.make_nurbs_edge(
1764 x,
1765 0.0,
1766 0.0,
1767 x,
1768 0.0,
1769 20.0,
1770 1,
1771 vec![0.0, 0.0, 1.0, 1.0],
1772 vec![x, 0.0, 0.0, x, 0.0, 20.0],
1773 vec![1.0, 1.0],
1774 )
1775 .unwrap()
1776 };
1777 let spine = mk_line(&mut k, 0.0);
1778 let aux = mk_line(&mut k, 10.0);
1779 let out = dispatch(
1780 &mut k,
1781 "guidedSweep",
1782 serde_json::json!({ "face": profile, "spineEdge": spine, "auxEdge": aux }),
1783 );
1784 assert!(
1785 out.get("ok").and_then(serde_json::Value::as_u64).is_some(),
1786 "expected an ok solid handle, got {out}"
1787 );
1788 }
1789
1790 #[test]
1791 fn minkowski_sum_binding_box10_box2_is_box12() {
1792 let mut k = BrepKernel::new();
1793 let a = brepkit_operations::primitives::make_box(k.topo_mut(), 10.0, 10.0, 10.0).unwrap();
1794 let b = brepkit_operations::primitives::make_box(k.topo_mut(), 2.0, 2.0, 2.0).unwrap();
1795 let sum = k
1796 .minkowski_sum(solid_id_to_u32(a), solid_id_to_u32(b))
1797 .unwrap();
1798 let vol = k.volume(sum, 0.1).unwrap();
1799 assert!(
1800 (vol - 1728.0).abs() < 0.5,
1801 "expected ~1728 (12³), got {vol}"
1802 );
1803 }
1804
1805 #[test]
1806 fn minkowski_sum_batch_dispatch() {
1807 let mut k = BrepKernel::new();
1808 let a = brepkit_operations::primitives::make_box(k.topo_mut(), 4.0, 4.0, 4.0).unwrap();
1809 let b = brepkit_operations::primitives::make_box(k.topo_mut(), 2.0, 2.0, 2.0).unwrap();
1810 let out = dispatch(
1811 &mut k,
1812 "minkowskiSum",
1813 serde_json::json!({ "solidA": solid_id_to_u32(a), "solidB": solid_id_to_u32(b) }),
1814 );
1815 assert!(
1816 out.get("ok").and_then(serde_json::Value::as_u64).is_some(),
1817 "expected an ok solid handle, got {out}"
1818 );
1819 }
1820
1821 #[test]
1822 fn project_edges_batch_dispatch_box_oblique() {
1823 let mut k = BrepKernel::new();
1824 let solid =
1825 brepkit_operations::primitives::make_box(k.topo_mut(), 10.0, 10.0, 10.0).unwrap();
1826 let out = dispatch(
1827 &mut k,
1828 "projectEdges",
1829 serde_json::json!({
1830 "solid": solid_id_to_u32(solid),
1831 "originX": -100.0, "originY": -100.0, "originZ": -100.0,
1832 "dirX": 1.0, "dirY": 1.0, "dirZ": 1.0,
1833 "xAxisX": 1.0, "xAxisY": -1.0, "xAxisZ": 0.0,
1834 "hiddenLines": true, "deflection": 0.1,
1835 }),
1836 );
1837 let ok = out.get("ok").expect("projectEdges batch should return ok");
1838 let nonempty = |key: &str| {
1839 ok.get(key)
1840 .and_then(serde_json::Value::as_array)
1841 .is_some_and(|a| !a.is_empty())
1842 };
1843 assert!(nonempty("visible"), "visible polylines expected, got {out}");
1844 assert!(nonempty("hidden"), "hidden polylines expected, got {out}");
1845 }
1846
1847 #[test]
1848 fn offset_wire_2d_with_join_routes_arc_distinct_from_chamfer() {
1849 let mut k = BrepKernel::new();
1850 let w_int = square_wire(&mut k);
1851 let w_arc = square_wire(&mut k);
1852 let w_chamfer = square_wire(&mut k);
1853
1854 let intersection = dispatch(
1855 &mut k,
1856 "offsetWire2DWithJoin",
1857 serde_json::json!({"wire": w_int, "distance": 2.0, "joinType": "intersection"}),
1858 );
1859 let arc = dispatch(
1860 &mut k,
1861 "offsetWire2DWithJoin",
1862 serde_json::json!({"wire": w_arc, "distance": 2.0, "joinType": "arc"}),
1863 );
1864 let chamfer = dispatch(
1865 &mut k,
1866 "offsetWire2DWithJoin",
1867 serde_json::json!({"wire": w_chamfer, "distance": 2.0, "joinType": "chamfer"}),
1868 );
1869
1870 for (label, entry) in [
1871 ("intersection", &intersection),
1872 ("arc", &arc),
1873 ("chamfer", &chamfer),
1874 ] {
1875 assert!(entry.get("error").is_none(), "{label} errored: {entry}");
1876 }
1877
1878 let int_wire = intersection["ok"].as_u64().unwrap() as u32;
1879 let arc_wire = arc["ok"].as_u64().unwrap() as u32;
1880 let chamfer_wire = chamfer["ok"].as_u64().unwrap() as u32;
1881
1882 let int_len = wire_perimeter(&k, int_wire);
1883 let arc_len = wire_perimeter(&k, arc_wire);
1884 let chamfer_len = wire_perimeter(&k, chamfer_wire);
1885
1886 assert!(int_len > 0.0 && arc_len > 0.0 && chamfer_len > 0.0);
1887 assert!(
1891 (arc_len - chamfer_len).abs() > 1.0,
1892 "arc ({arc_len}) should differ from chamfer ({chamfer_len})"
1893 );
1894 }
1895
1896 #[test]
1897 fn offset_wire_2d_with_join_rejects_unknown_join_type() {
1898 let mut k = BrepKernel::new();
1899 let w = square_wire(&mut k);
1900 let entry = dispatch(
1901 &mut k,
1902 "offsetWire2DWithJoin",
1903 serde_json::json!({"wire": w, "distance": 2.0, "joinType": "bogus"}),
1904 );
1905 assert!(
1906 entry.get("error").is_some(),
1907 "unknown join type should error: {entry}"
1908 );
1909 }
1910}