1#![allow(clippy::missing_errors_doc, clippy::too_many_arguments)]
4
5use wasm_bindgen::prelude::*;
6
7use brepkit_math::mat::Mat4;
8use brepkit_math::vec::{Point3, Vec3};
9use brepkit_operations::transform::transform_solid;
10
11use crate::error::{WasmError, validate_finite, validate_positive};
12use crate::handles::{compound_id_to_u32, face_id_to_u32, solid_id_to_u32, wire_id_to_u32};
13use crate::kernel::BrepKernel;
14
15#[wasm_bindgen]
16impl BrepKernel {
17 #[wasm_bindgen(js_name = "transformSolid")]
26 #[allow(clippy::needless_pass_by_value)] pub fn transform_solid_binding(&mut self, solid: u32, matrix: Vec<f64>) -> Result<(), JsError> {
28 if matrix.len() != 16 {
29 return Err(WasmError::InvalidInput {
30 reason: format!(
31 "transform matrix must have 16 elements, got {}",
32 matrix.len()
33 ),
34 }
35 .into());
36 }
37
38 if let Some(pos) = matrix.iter().position(|v| !v.is_finite()) {
39 return Err(WasmError::InvalidInput {
40 reason: format!("matrix element at index {pos} is not finite"),
41 }
42 .into());
43 }
44
45 let solid_id = self.resolve_solid(solid)?;
46
47 let rows = std::array::from_fn(|i| std::array::from_fn(|j| matrix[i * 4 + j]));
48 let mat = Mat4(rows);
49
50 transform_solid(self.topo_mut(), solid_id, &mat)?;
51 Ok(())
52 }
53
54 #[wasm_bindgen(js_name = "composeTransforms")]
63 #[allow(clippy::needless_pass_by_value, clippy::unused_self)]
64 pub fn compose_transforms(
65 &self,
66 matrix_a: Vec<f64>,
67 matrix_b: Vec<f64>,
68 ) -> Result<Vec<f64>, JsError> {
69 if matrix_a.len() != 16 {
70 return Err(WasmError::InvalidInput {
71 reason: format!("matrix A must have 16 elements, got {}", matrix_a.len()),
72 }
73 .into());
74 }
75 if matrix_b.len() != 16 {
76 return Err(WasmError::InvalidInput {
77 reason: format!("matrix B must have 16 elements, got {}", matrix_b.len()),
78 }
79 .into());
80 }
81 let rows_a = std::array::from_fn(|i| std::array::from_fn(|j| matrix_a[i * 4 + j]));
82 let rows_b = std::array::from_fn(|i| std::array::from_fn(|j| matrix_b[i * 4 + j]));
83 let result = Mat4(rows_a) * Mat4(rows_b);
84 let mut out = Vec::with_capacity(16);
85 for row in &result.0 {
86 out.extend_from_slice(row);
87 }
88 Ok(out)
89 }
90
91 #[wasm_bindgen(js_name = "copySolid")]
99 pub fn copy_solid(&mut self, solid: u32) -> Result<u32, JsError> {
100 let solid_id = self.resolve_solid(solid)?;
101 let copy = brepkit_operations::copy::copy_solid(self.topo_mut(), solid_id)?;
102 Ok(solid_id_to_u32(copy))
103 }
104
105 #[wasm_bindgen(js_name = "copyWire")]
111 pub fn copy_wire(&mut self, wire: u32) -> Result<u32, JsError> {
112 let wire_id = self.resolve_wire(wire)?;
113 let copy = brepkit_operations::copy::copy_wire(self.topo_mut(), wire_id)?;
114 Ok(wire_id_to_u32(copy))
115 }
116
117 #[wasm_bindgen(js_name = "copyFace")]
126 pub fn copy_face(&mut self, face: u32) -> Result<u32, JsError> {
127 let face_id = self.resolve_face(face)?;
128 let copy = brepkit_operations::copy::copy_face(self.topo_mut(), face_id)?;
129 Ok(face_id_to_u32(copy))
130 }
131
132 #[wasm_bindgen(js_name = "transformWire")]
141 #[allow(clippy::needless_pass_by_value)]
142 pub fn transform_wire(&mut self, wire: u32, matrix: Vec<f64>) -> Result<(), JsError> {
143 if matrix.len() != 16 {
144 return Err(WasmError::InvalidInput {
145 reason: format!(
146 "transform matrix must have 16 elements, got {}",
147 matrix.len()
148 ),
149 }
150 .into());
151 }
152
153 if let Some(pos) = matrix.iter().position(|v| !v.is_finite()) {
154 return Err(WasmError::InvalidInput {
155 reason: format!("matrix element at index {pos} is not finite"),
156 }
157 .into());
158 }
159
160 let wire_id = self.resolve_wire(wire)?;
161 let rows = std::array::from_fn(|i| std::array::from_fn(|j| matrix[i * 4 + j]));
162 let mat = Mat4(rows);
163 brepkit_operations::transform::transform_wire(self.topo_mut(), wire_id, &mat)?;
164 Ok(())
165 }
166
167 #[wasm_bindgen(js_name = "transformFace")]
177 #[allow(clippy::needless_pass_by_value)]
178 pub fn transform_face(&mut self, face: u32, matrix: Vec<f64>) -> Result<(), JsError> {
179 if matrix.len() != 16 {
180 return Err(WasmError::InvalidInput {
181 reason: format!(
182 "transform matrix must have 16 elements, got {}",
183 matrix.len()
184 ),
185 }
186 .into());
187 }
188
189 if let Some(pos) = matrix.iter().position(|v| !v.is_finite()) {
190 return Err(WasmError::InvalidInput {
191 reason: format!("matrix element at index {pos} is not finite"),
192 }
193 .into());
194 }
195
196 let face_id = self.resolve_face(face)?;
197 let rows = std::array::from_fn(|i| std::array::from_fn(|j| matrix[i * 4 + j]));
198 let mat = Mat4(rows);
199 brepkit_operations::transform::transform_face(self.topo_mut(), face_id, &mat)?;
200 Ok(())
201 }
202
203 #[wasm_bindgen(js_name = "copyAndTransformSolid")]
213 #[allow(clippy::needless_pass_by_value)]
214 pub fn copy_and_transform_solid(
215 &mut self,
216 solid: u32,
217 matrix: Vec<f64>,
218 ) -> Result<u32, JsError> {
219 if matrix.len() != 16 {
220 return Err(WasmError::InvalidInput {
221 reason: format!(
222 "transform matrix must have 16 elements, got {}",
223 matrix.len()
224 ),
225 }
226 .into());
227 }
228
229 if let Some(pos) = matrix.iter().position(|v| !v.is_finite()) {
230 return Err(WasmError::InvalidInput {
231 reason: format!("matrix element at index {pos} is not finite"),
232 }
233 .into());
234 }
235
236 let solid_id = self.resolve_solid(solid)?;
237
238 let rows = std::array::from_fn(|i| std::array::from_fn(|j| matrix[i * 4 + j]));
239 let mat = Mat4(rows);
240
241 let copy =
242 brepkit_operations::copy::copy_and_transform_solid(self.topo_mut(), solid_id, &mat)?;
243 Ok(solid_id_to_u32(copy))
244 }
245
246 #[wasm_bindgen(js_name = "mirror")]
254 #[allow(clippy::too_many_arguments)]
255 pub fn mirror_solid(
256 &mut self,
257 solid: u32,
258 px: f64,
259 py: f64,
260 pz: f64,
261 nx: f64,
262 ny: f64,
263 nz: f64,
264 ) -> Result<u32, JsError> {
265 validate_finite(px, "px")?;
266 validate_finite(py, "py")?;
267 validate_finite(pz, "pz")?;
268 validate_finite(nx, "nx")?;
269 validate_finite(ny, "ny")?;
270 validate_finite(nz, "nz")?;
271 let solid_id = self.resolve_solid(solid)?;
272 let result = brepkit_operations::mirror::mirror(
273 self.topo_mut(),
274 solid_id,
275 Point3::new(px, py, pz),
276 Vec3::new(nx, ny, nz),
277 )?;
278 Ok(solid_id_to_u32(result))
279 }
280
281 #[wasm_bindgen(js_name = "linearPattern")]
289 #[allow(clippy::too_many_arguments)]
290 pub fn linear_pattern(
291 &mut self,
292 solid: u32,
293 dx: f64,
294 dy: f64,
295 dz: f64,
296 spacing: f64,
297 count: u32,
298 ) -> Result<u32, JsError> {
299 validate_finite(dx, "dx")?;
300 validate_finite(dy, "dy")?;
301 validate_finite(dz, "dz")?;
302 validate_positive(spacing, "spacing")?;
303 let solid_id = self.resolve_solid(solid)?;
304 let compound = brepkit_operations::pattern::linear_pattern(
305 self.topo_mut(),
306 solid_id,
307 Vec3::new(dx, dy, dz),
308 spacing,
309 count as usize,
310 )?;
311 Ok(compound_id_to_u32(compound))
312 }
313
314 #[wasm_bindgen(js_name = "gridPattern")]
320 #[allow(clippy::too_many_arguments)]
321 pub fn grid_pattern(
322 &mut self,
323 solid: u32,
324 dir_x_x: f64,
325 dir_x_y: f64,
326 dir_x_z: f64,
327 dir_y_x: f64,
328 dir_y_y: f64,
329 dir_y_z: f64,
330 spacing_x: f64,
331 spacing_y: f64,
332 count_x: u32,
333 count_y: u32,
334 ) -> Result<u32, JsError> {
335 validate_finite(dir_x_x, "dir_x_x")?;
336 validate_finite(dir_x_y, "dir_x_y")?;
337 validate_finite(dir_x_z, "dir_x_z")?;
338 validate_finite(dir_y_x, "dir_y_x")?;
339 validate_finite(dir_y_y, "dir_y_y")?;
340 validate_finite(dir_y_z, "dir_y_z")?;
341 validate_positive(spacing_x, "spacing_x")?;
342 validate_positive(spacing_y, "spacing_y")?;
343 let solid_id = self.resolve_solid(solid)?;
344 let compound = brepkit_operations::pattern::grid_pattern(
345 self.topo_mut(),
346 solid_id,
347 Vec3::new(dir_x_x, dir_x_y, dir_x_z),
348 Vec3::new(dir_y_x, dir_y_y, dir_y_z),
349 spacing_x,
350 spacing_y,
351 count_x as usize,
352 count_y as usize,
353 )?;
354 Ok(compound_id_to_u32(compound))
355 }
356
357 #[wasm_bindgen(js_name = "circularPattern")]
361 pub fn circular_pattern(
362 &mut self,
363 solid: u32,
364 ax: f64,
365 ay: f64,
366 az: f64,
367 count: u32,
368 ) -> Result<u32, JsError> {
369 let solid_id = self.resolve_solid(solid)?;
370 let axis = Vec3::new(ax, ay, az);
371 let compound = brepkit_operations::pattern::circular_pattern(
372 self.topo_mut(),
373 solid_id,
374 axis,
375 count as usize,
376 )?;
377 Ok(compound_id_to_u32(compound))
378 }
379
380 #[wasm_bindgen(js_name = "mergeCoincidentVertices")]
384 pub fn merge_coincident_vertices(
385 &mut self,
386 solid: u32,
387 tolerance: f64,
388 ) -> Result<u32, JsError> {
389 let solid_id = self.resolve_solid(solid)?;
390 let count = brepkit_operations::heal::merge_coincident_vertices(
391 self.topo_mut(),
392 solid_id,
393 tolerance,
394 )?;
395 #[allow(clippy::cast_possible_truncation)]
396 Ok(count as u32)
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 #![allow(clippy::unwrap_used, clippy::expect_used)]
403 use crate::kernel::BrepKernel;
404
405 fn identity_matrix() -> Vec<f64> {
406 vec![
407 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
411 ]
412 }
413
414 #[test]
417 fn copy_solid_returns_new_handle() {
418 let mut k = BrepKernel::new();
419 let s = k.make_box_solid(1.0, 1.0, 1.0).unwrap();
420 let copy = k.copy_solid(s).unwrap();
421 assert_ne!(s, copy);
422 }
423
424 #[test]
425 fn copy_solid_both_handles_resolve() {
426 let mut k = BrepKernel::new();
427 let s = k.make_box_solid(2.0, 2.0, 2.0).unwrap();
428 let copy = k.copy_solid(s).unwrap();
429 assert!(k.resolve_solid(s).is_ok());
431 assert!(k.resolve_solid(copy).is_ok());
432 }
433
434 #[test]
435 fn copy_solid_invalid_handle_errors() {
436 let mut k = BrepKernel::new();
437 let r = k.execute_batch(r#"[{"op": "copySolid", "args": {"solid": 9999}}]"#);
438 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
439 assert!(parsed[0]["error"].is_string());
440 }
441
442 #[test]
445 fn copy_face_returns_new_handle() {
446 let mut k = BrepKernel::new();
447 let s = k.make_box_solid(1.0, 1.0, 1.0).unwrap();
448 let face = k.get_solid_faces(s).unwrap()[0];
449 let batch = format!(r#"[{{"op": "copyFace", "args": {{"face": {face}}}}}]"#);
450 let r = k.execute_batch(&batch);
451 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
452 let copy = parsed[0]["ok"].as_u64().unwrap();
453 assert_ne!(copy, u64::from(face));
454 }
455
456 #[test]
457 fn copy_face_invalid_handle_errors() {
458 let mut k = BrepKernel::new();
459 let r = k.execute_batch(r#"[{"op": "copyFace", "args": {"face": 9999}}]"#);
460 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
461 assert!(parsed[0]["error"].is_string());
462 }
463
464 #[test]
467 fn identity_transform_preserves_bounding_box() {
468 let mut k = BrepKernel::new();
469 let s = k.make_box_solid(3.0, 4.0, 5.0).unwrap();
470 let before = k.bounding_box(s).unwrap();
471 k.transform_solid_binding(s, identity_matrix()).unwrap();
472 let after = k.bounding_box(s).unwrap();
473 for (a, b) in before.iter().zip(after.iter()) {
474 assert!(
475 (a - b).abs() < 1e-10,
476 "bbox changed after identity: {a} vs {b}"
477 );
478 }
479 }
480
481 #[test]
482 fn transform_solid_translation_shifts_bbox() {
483 let mut k = BrepKernel::new();
484 let s = k.make_box_solid(1.0, 1.0, 1.0).unwrap();
485 let mat = vec![
487 1.0, 0.0, 0.0, 10.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
491 ];
492 k.transform_solid_binding(s, mat).unwrap();
493 let bbox = k.bounding_box(s).unwrap();
494 assert!(
496 (bbox[0] - (bbox[3] - 1.0)).abs() < 1e-6,
497 "unexpected min_x: {}",
498 bbox[0]
499 );
500 assert!(bbox[0] >= 9.0, "bbox min_x not translated: {}", bbox[0]);
501 }
502
503 #[test]
504 fn transform_solid_invalid_handle_errors() {
505 let mut k = BrepKernel::new();
506 let mat_json: Vec<String> = identity_matrix().iter().map(ToString::to_string).collect();
507 let r = k.execute_batch(&format!(
508 r#"[{{"op": "transform", "args": {{"solid": 9999, "matrix": [{}]}}}}]"#,
509 mat_json.join(",")
510 ));
511 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
512 assert!(parsed[0]["error"].is_string());
513 }
514
515 #[test]
516 fn transform_solid_wrong_matrix_length_errors() {
517 let mut k = BrepKernel::new();
518 let r = k.execute_batch(
519 r#"[
520 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
521 {"op": "transform", "args": {"solid": 0, "matrix": [1,0,0,0, 0,1,0,0, 0]}}
522 ]"#,
523 );
524 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
525 let err_msg = parsed[1]["error"].as_str().unwrap();
526 assert!(
527 err_msg.contains("16"),
528 "error should mention 16 elements: {err_msg}"
529 );
530 }
531
532 #[test]
533 fn transform_solid_nan_in_matrix_errors() {
534 let mat = identity_matrix();
540 let has_nan = mat.iter().any(|v| !v.is_finite());
541 assert!(!has_nan, "identity matrix should be finite");
542 assert!(!f64::NAN.is_finite());
544 }
545
546 #[test]
549 fn mirror_solid_returns_new_handle() {
550 let mut k = BrepKernel::new();
551 let s = k.make_box_solid(1.0, 1.0, 1.0).unwrap();
552 let mirrored = k.mirror_solid(s, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0).unwrap();
553 assert_ne!(s, mirrored);
554 assert!(k.resolve_solid(mirrored).is_ok());
555 }
556
557 #[test]
558 fn mirror_solid_invalid_handle_errors() {
559 let mut k = BrepKernel::new();
560 let r = k.execute_batch(
561 r#"[{"op": "mirror", "args": {"solid": 9999, "px": 0, "py": 0, "pz": 0, "nx": 0, "ny": 0, "nz": 1}}]"#,
562 );
563 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
564 assert!(parsed[0]["error"].is_string());
565 }
566
567 #[test]
570 fn linear_pattern_count_matches_compound_children() {
571 let mut k = BrepKernel::new();
572 let s = k.make_box_solid(1.0, 1.0, 1.0).unwrap();
573 let compound = k.linear_pattern(s, 1.0, 0.0, 0.0, 2.0, 4).unwrap();
574 let solids = k.get_compound_solids(compound).unwrap();
575 assert_eq!(
576 solids.len(),
577 4,
578 "expected 4 solids in compound, got {}",
579 solids.len()
580 );
581 }
582
583 #[test]
584 fn linear_pattern_count_one_returns_original() {
585 let mut k = BrepKernel::new();
586 let s = k.make_box_solid(1.0, 1.0, 1.0).unwrap();
587 let compound = k.linear_pattern(s, 0.0, 1.0, 0.0, 5.0, 1).unwrap();
588 let solids = k.get_compound_solids(compound).unwrap();
589 assert_eq!(solids.len(), 1);
590 assert_eq!(solids[0], s);
592 }
593
594 #[test]
595 fn linear_pattern_invalid_solid_errors() {
596 let mut k = BrepKernel::new();
597 let r = k.execute_batch(
598 r#"[{"op": "linearPattern", "args": {"solid": 9999, "dx": 1, "dy": 0, "dz": 0, "spacing": 2, "count": 3}}]"#,
599 );
600 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
601 assert!(parsed[0]["error"].is_string());
602 }
603
604 #[test]
605 fn linear_pattern_zero_spacing_errors() {
606 let mut k = BrepKernel::new();
607 let r = k.execute_batch(
608 r#"[
609 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
610 {"op": "linearPattern", "args": {"solid": 0, "dx": 1, "dy": 0, "dz": 0, "spacing": 0, "count": 3}}
611 ]"#,
612 );
613 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
614 assert!(parsed[1]["error"].is_string());
615 }
616}