1#![allow(clippy::missing_errors_doc, clippy::too_many_lines)]
4
5use wasm_bindgen::prelude::*;
6
7use brepkit_math::mat::Mat4;
8use brepkit_math::nurbs::curve::NurbsCurve;
9use brepkit_math::nurbs::surface::NurbsSurface;
10use brepkit_math::vec::{Point3, Vec3};
11use brepkit_operations::boolean::{self, BooleanOp, boolean};
12use brepkit_operations::extrude::extrude;
13use brepkit_operations::measure;
14use brepkit_operations::revolve::revolve;
15use brepkit_operations::sweep::sweep;
16use brepkit_operations::transform::transform_solid;
17use brepkit_topology::edge::EdgeCurve;
18
19use crate::error::WasmError;
20use crate::handles::{
21 compound_id_to_u32, edge_id_to_u32, face_id_to_u32, solid_id_to_u32, wire_id_to_u32,
22};
23use crate::helpers::{TOL, classify_to_string, get_f64, get_u32, panic_message, try_fillet};
24use crate::kernel::BrepKernel;
25
26#[wasm_bindgen]
27impl BrepKernel {
28 #[wasm_bindgen(js_name = "executeBatch")]
53 #[allow(clippy::needless_pass_by_value)]
54 pub fn execute_batch(&mut self, json: &str) -> String {
55 let ops: Vec<serde_json::Value> = match serde_json::from_str(json) {
56 Ok(v) => v,
57 Err(e) => {
58 return serde_json::json!([{"error": format!("invalid JSON: {e}")}]).to_string();
59 }
60 };
61
62 let results: Vec<serde_json::Value> = ops
63 .iter()
64 .map(|entry| {
65 let op = match entry["op"].as_str() {
66 Some(s) => s,
67 None => return serde_json::json!({"error": "missing or invalid 'op' field"}),
68 };
69 let args = &entry["args"];
70 match self.dispatch_op(op, args) {
71 Ok(val) => serde_json::json!({"ok": val}),
72 Err(msg) => serde_json::json!({"error": msg}),
73 }
74 })
75 .collect();
76
77 serde_json::Value::Array(results).to_string()
78 }
79}
80
81type UvRanges = ((f64, f64), (f64, f64));
83
84fn plane_frame_axes(normal: Vec3) -> (Vec3, Vec3) {
89 let seed = if normal.x().abs() < 0.9 {
90 Vec3::new(1.0, 0.0, 0.0)
91 } else {
92 Vec3::new(0.0, 1.0, 0.0)
93 };
94 let u_axis = normal
95 .cross(seed)
96 .normalize()
97 .unwrap_or_else(|_| Vec3::new(1.0, 0.0, 0.0));
98 let v_axis = normal.cross(u_axis);
99 (u_axis, v_axis)
100}
101
102impl BrepKernel {
103 pub(crate) fn extract_nurbs_curve(&self, edge: u32) -> Result<NurbsCurve, WasmError> {
110 use brepkit_geometry::convert::{circle_to_nurbs, ellipse_to_nurbs, line_to_nurbs};
111 use std::f64::consts::TAU;
112
113 let edge_id = self.resolve_edge(edge)?;
114 let edge_data = self.topo.edge(edge_id)?;
115 let start_v = edge_data.start();
116 let end_v = edge_data.end();
117 let start_pt = self.topo.vertex(start_v)?.point();
118 let end_pt = self.topo.vertex(end_v)?.point();
119
120 match edge_data.curve() {
121 EdgeCurve::NurbsCurve(c) => Ok(c.clone()),
122 EdgeCurve::Line => {
123 Ok(
124 line_to_nurbs(start_pt, end_pt).map_err(|e| WasmError::InvalidInput {
125 reason: format!("line_to_nurbs failed: {e}"),
126 })?,
127 )
128 }
129 EdgeCurve::Circle(c) => {
130 let (t_start, t_end) = if start_v == end_v {
131 (0.0, TAU)
132 } else {
133 let ts = c.project(start_pt);
134 let mut te = c.project(end_pt);
135 if te <= ts {
136 te += TAU;
137 }
138 (ts, te)
139 };
140 Ok(
141 circle_to_nurbs(c, t_start, t_end).map_err(|e| WasmError::InvalidInput {
142 reason: format!("circle_to_nurbs failed: {e}"),
143 })?,
144 )
145 }
146 EdgeCurve::Ellipse(e) => {
147 let (t_start, t_end) = if start_v == end_v {
148 (0.0, TAU)
149 } else {
150 let ts = e.project(start_pt);
151 let mut te = e.project(end_pt);
152 if te <= ts {
153 te += TAU;
154 }
155 (ts, te)
156 };
157 Ok(
158 ellipse_to_nurbs(e, t_start, t_end).map_err(|err| WasmError::InvalidInput {
159 reason: format!("ellipse_to_nurbs failed: {err}"),
160 })?,
161 )
162 }
163 }
164 }
165
166 pub(crate) fn extract_nurbs_surface(&self, face: u32) -> Result<NurbsSurface, WasmError> {
174 use brepkit_heal::construct::convert_surface;
175 use brepkit_topology::face::FaceSurface;
176
177 let face_id = self.resolve_face(face)?;
178 let face_data = self.topo.face(face_id)?;
179
180 let map_err = |context: &str, e: brepkit_heal::HealError| WasmError::InvalidInput {
181 reason: format!("{context}: {e}"),
182 };
183
184 match face_data.surface() {
185 FaceSurface::Nurbs(s) => Ok(s.clone()),
186 FaceSurface::Plane { normal, d } => {
187 let (u_range, v_range) = self.plane_face_uv_bounds(face_id, *normal, *d)?;
188 convert_surface::plane_to_nurbs(*normal, *d, u_range, v_range)
189 .map_err(|e| map_err("plane_to_nurbs failed", e))
190 }
191 FaceSurface::Cylinder(c) => {
192 let v_range = self.analytic_face_v_bounds(face_id, face_data.surface())?;
193 convert_surface::cylinder_to_nurbs(c, v_range)
194 .map_err(|e| map_err("cylinder_to_nurbs failed", e))
195 }
196 FaceSurface::Cone(c) => {
197 let v_range = self.analytic_face_v_bounds(face_id, face_data.surface())?;
198 convert_surface::cone_to_nurbs(c, v_range)
199 .map_err(|e| map_err("cone_to_nurbs failed", e))
200 }
201 FaceSurface::Sphere(s) => convert_surface::sphere_to_nurbs(s)
202 .map_err(|e| map_err("sphere_to_nurbs failed", e)),
203 FaceSurface::Torus(t) => {
204 convert_surface::torus_to_nurbs(t).map_err(|e| map_err("torus_to_nurbs failed", e))
205 }
206 }
207 }
208
209 #[allow(clippy::cast_precision_loss)]
218 fn plane_face_uv_bounds(
219 &self,
220 face_id: brepkit_topology::face::FaceId,
221 normal: Vec3,
222 d: f64,
223 ) -> Result<UvRanges, WasmError> {
224 const EDGE_SAMPLES: usize = 16;
225
226 let face_data = self.topo.face(face_id)?;
227 let wire = self.topo.wire(face_data.outer_wire())?;
228 let origin = Point3::new(0.0, 0.0, 0.0) + normal * d;
229 let (u_axis, v_axis) = plane_frame_axes(normal);
230
231 let mut u_min = f64::INFINITY;
232 let mut u_max = f64::NEG_INFINITY;
233 let mut v_min = f64::INFINITY;
234 let mut v_max = f64::NEG_INFINITY;
235 for oe in wire.edges() {
236 let edge = self.topo.edge(oe.edge())?;
237 let start = self.topo.vertex(edge.start())?.point();
238 let end = self.topo.vertex(edge.end())?.point();
239 let curve = edge.curve();
240 let (t0, t1) = curve.domain_with_endpoints(start, end);
241 for i in 0..=EDGE_SAMPLES {
242 let t = t0 + (t1 - t0) * (i as f64 / EDGE_SAMPLES as f64);
243 let p = curve.evaluate_with_endpoints(t, start, end);
244 let rel = p - origin;
245 let u = rel.dot(u_axis);
246 let v = rel.dot(v_axis);
247 u_min = u_min.min(u);
248 u_max = u_max.max(u);
249 v_min = v_min.min(v);
250 v_max = v_max.max(v);
251 }
252 }
253 if u_max <= u_min || v_max <= v_min {
254 return Err(WasmError::InvalidInput {
255 reason: "planar face has degenerate parametric extent".to_string(),
256 });
257 }
258 Ok(((u_min, u_max), (v_min, v_max)))
259 }
260
261 fn analytic_face_v_bounds(
264 &self,
265 face_id: brepkit_topology::face::FaceId,
266 surface: &brepkit_topology::face::FaceSurface,
267 ) -> Result<(f64, f64), WasmError> {
268 let verts = brepkit_topology::explorer::face_vertices(&self.topo, face_id)?;
269 let mut v_min = f64::INFINITY;
270 let mut v_max = f64::NEG_INFINITY;
271 for vid in verts {
272 let p = self.topo.vertex(vid)?.point();
273 if let Some((_, v)) = surface.project_point(p) {
274 v_min = v_min.min(v);
275 v_max = v_max.max(v);
276 }
277 }
278 if v_max <= v_min {
279 return Err(WasmError::InvalidInput {
280 reason: "analytic face has degenerate axial extent".to_string(),
281 });
282 }
283 Ok((v_min, v_max))
284 }
285
286 pub(crate) fn nurbs_curve_to_edge(
288 &mut self,
289 points: &[Point3],
290 curve: NurbsCurve,
291 ) -> brepkit_topology::edge::EdgeId {
292 let start = points[0];
293 let end = points[points.len() - 1];
294 brepkit_topology::builder::make_nurbs_edge(self.topo_mut(), start, end, curve, TOL)
295 }
296
297 pub(crate) fn nurbs_curve_to_edge_from_curve(
299 &mut self,
300 curve: &NurbsCurve,
301 ) -> brepkit_topology::edge::EdgeId {
302 brepkit_topology::builder::make_nurbs_edge_from_curve(self.topo_mut(), curve, TOL)
303 }
304
305 pub(crate) fn nurbs_surface_to_face(
307 &mut self,
308 surface: NurbsSurface,
309 ) -> Result<brepkit_topology::face::FaceId, JsError> {
310 Ok(brepkit_topology::builder::make_nurbs_face(
311 self.topo_mut(),
312 surface,
313 TOL,
314 )?)
315 }
316
317 #[allow(clippy::too_many_lines)]
319 fn dispatch_op(
320 &mut self,
321 op: &str,
322 args: &serde_json::Value,
323 ) -> Result<serde_json::Value, String> {
324 match op {
325 "makeBox" => {
326 let w = get_f64(args, "width")?;
327 let h = get_f64(args, "height")?;
328 let d = get_f64(args, "depth")?;
329 let solid = brepkit_operations::primitives::make_box(self.topo_mut(), w, h, d)
330 .map_err(|e| e.to_string())?;
331 Ok(serde_json::json!(solid_id_to_u32(solid)))
332 }
333 "makeCylinder" => {
334 let r = get_f64(args, "radius")?;
335 let h = get_f64(args, "height")?;
336 let solid = brepkit_operations::primitives::make_cylinder(self.topo_mut(), r, h)
337 .map_err(|e| e.to_string())?;
338 Ok(serde_json::json!(solid_id_to_u32(solid)))
339 }
340 "makeSphere" => {
341 let r = get_f64(args, "radius")?;
342 let segments = get_u32(args, "segments").unwrap_or(16);
343 let solid = brepkit_operations::primitives::make_sphere(
344 self.topo_mut(),
345 r,
346 segments as usize,
347 )
348 .map_err(|e| e.to_string())?;
349 Ok(serde_json::json!(solid_id_to_u32(solid)))
350 }
351 "makeCone" => {
352 let br = get_f64(args, "bottomRadius")?;
353 let tr = get_f64(args, "topRadius")?;
354 let h = get_f64(args, "height")?;
355 let solid = brepkit_operations::primitives::make_cone(self.topo_mut(), br, tr, h)
356 .map_err(|e| e.to_string())?;
357 Ok(serde_json::json!(solid_id_to_u32(solid)))
358 }
359 "makeTorus" => {
360 let major = get_f64(args, "majorRadius")?;
361 let minor = get_f64(args, "minorRadius")?;
362 let segments = get_u32(args, "segments").unwrap_or(16);
363 let solid = brepkit_operations::primitives::make_torus(
364 self.topo_mut(),
365 major,
366 minor,
367 segments as usize,
368 )
369 .map_err(|e| e.to_string())?;
370 Ok(serde_json::json!(solid_id_to_u32(solid)))
371 }
372 "makeEllipsoid" => {
373 let rx = get_f64(args, "rx")?;
374 let ry = get_f64(args, "ry")?;
375 let rz = get_f64(args, "rz")?;
376 if rx <= 0.0 || ry <= 0.0 || rz <= 0.0 {
377 return Err("rx, ry, rz must be positive".to_string());
378 }
379 let solid = brepkit_operations::primitives::make_sphere(self.topo_mut(), 1.0, 16)
380 .map_err(|e| e.to_string())?;
381 let mat = brepkit_math::mat::Mat4::scale(rx, ry, rz);
382 transform_solid(self.topo_mut(), solid, &mat).map_err(|e| e.to_string())?;
383 Ok(serde_json::json!(solid_id_to_u32(solid)))
384 }
385 "fuse" | "cut" | "intersect" => {
386 let bool_op = match op {
387 "fuse" => BooleanOp::Fuse,
388 "cut" => BooleanOp::Cut,
389 _ => BooleanOp::Intersect,
390 };
391 let a = get_u32(args, "solidA")?;
392 let b = get_u32(args, "solidB")?;
393 let a_id = self.resolve_solid(a).map_err(|e| e.to_string())?;
394 let b_id = self.resolve_solid(b).map_err(|e| e.to_string())?;
395 let simplify = args["simplify"].as_bool().unwrap_or(false);
396 let result = if simplify {
397 let opts = brepkit_operations::boolean::BooleanOptions {
398 unify_faces: true,
399 ..Default::default()
400 };
401 brepkit_operations::boolean::boolean_with_options(
402 self.topo_mut(),
403 bool_op,
404 a_id,
405 b_id,
406 opts,
407 )
408 } else {
409 boolean(self.topo_mut(), bool_op, a_id, b_id)
410 }
411 .map_err(|e| e.to_string())?;
412 Ok(serde_json::json!(solid_id_to_u32(result)))
413 }
414 "meshFallbackCount" => {
415 #[allow(clippy::cast_precision_loss)]
416 let count = brepkit_operations::boolean::mesh_fallback_count() as f64;
417 Ok(serde_json::json!(count))
418 }
419 "detectCoincidentFaces" => {
420 let a = get_u32(args, "solidA")?;
421 let b = get_u32(args, "solidB")?;
422 let a_id = self.resolve_solid(a).map_err(|e| e.to_string())?;
423 let b_id = self.resolve_solid(b).map_err(|e| e.to_string())?;
424 let pairs = brepkit_algo::diagnostic::detect_coincident_faces(
425 self.topo(),
426 a_id,
427 b_id,
428 brepkit_math::tolerance::Tolerance::default(),
429 )
430 .map_err(|e| e.to_string())?;
431 Ok(crate::bindings::booleans::coincident_face_pairs_to_json(
432 &pairs,
433 ))
434 }
435 "compoundCut" => {
436 let target = get_u32(args, "target")?;
437 let target_id = self.resolve_solid(target).map_err(|e| e.to_string())?;
438 let tool_arr = args["tools"]
439 .as_array()
440 .ok_or("missing or invalid 'tools' array")?;
441 let tools: Vec<brepkit_topology::solid::SolidId> = tool_arr
442 .iter()
443 .enumerate()
444 .map(|(i, v)| {
445 let h = v
446 .as_u64()
447 .ok_or_else(|| format!("tools[{i}] is not a number"))
448 .map(|n| n as u32)?;
449 self.resolve_solid(h).map_err(|e| e.to_string())
450 })
451 .collect::<Result<Vec<_>, String>>()?;
452 let result = boolean::compound_cut(
453 self.topo_mut(),
454 target_id,
455 &tools,
456 boolean::BooleanOptions::default(),
457 )
458 .map_err(|e| e.to_string())?;
459 Ok(serde_json::json!(solid_id_to_u32(result)))
460 }
461 "fuseAll" => {
462 let solid_arr = args["solids"]
463 .as_array()
464 .ok_or("missing or invalid 'solids' array")?;
465 let solids: Vec<brepkit_topology::solid::SolidId> = solid_arr
466 .iter()
467 .enumerate()
468 .map(|(i, v)| {
469 let h = v
470 .as_u64()
471 .ok_or_else(|| format!("solids[{i}] is not a number"))
472 .map(|n| n as u32)?;
473 self.resolve_solid(h).map_err(|e| e.to_string())
474 })
475 .collect::<Result<Vec<_>, String>>()?;
476 let compound = self
477 .topo_mut()
478 .add_compound(brepkit_topology::compound::Compound::new(solids));
479 let result = brepkit_operations::compound_ops::fuse_all(self.topo_mut(), compound)
480 .map_err(|e| e.to_string())?;
481 Ok(serde_json::json!(solid_id_to_u32(result)))
482 }
483 "transform" => {
484 let s = get_u32(args, "solid")?;
485 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
486 let matrix = args["matrix"]
487 .as_array()
488 .ok_or("missing or invalid 'matrix'")?;
489 if matrix.len() != 16 {
490 return Err(format!(
491 "matrix must have 16 elements, got {}",
492 matrix.len()
493 ));
494 }
495 let elems: Vec<f64> = matrix
496 .iter()
497 .enumerate()
498 .map(|(i, v)| {
499 v.as_f64()
500 .ok_or_else(|| format!("matrix[{i}] is not a number"))
501 })
502 .collect::<Result<_, _>>()?;
503 let rows = std::array::from_fn(|i| std::array::from_fn(|j| elems[i * 4 + j]));
504 let mat = Mat4(rows);
505 transform_solid(self.topo_mut(), solid_id, &mat).map_err(|e| e.to_string())?;
506 Ok(serde_json::json!(solid_id_to_u32(solid_id)))
507 }
508 "volume" => {
509 let s = get_u32(args, "solid")?;
510 let deflection = get_f64(args, "deflection").unwrap_or(0.1);
511 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
512 let v = measure::solid_volume(&self.topo, solid_id, deflection)
513 .map_err(|e| e.to_string())?;
514 Ok(serde_json::json!(v))
515 }
516 "surfaceArea" => {
517 let s = get_u32(args, "solid")?;
518 let deflection = get_f64(args, "deflection").unwrap_or(0.1);
519 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
520 let a = measure::solid_surface_area(&self.topo, solid_id, deflection)
521 .map_err(|e| e.to_string())?;
522 Ok(serde_json::json!(a))
523 }
524 "boundingBox" => {
525 let s = get_u32(args, "solid")?;
526 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
527 let aabb =
528 measure::solid_bounding_box(&self.topo, solid_id).map_err(|e| e.to_string())?;
529 Ok(serde_json::json!([
530 aabb.min.x(),
531 aabb.min.y(),
532 aabb.min.z(),
533 aabb.max.x(),
534 aabb.max.y(),
535 aabb.max.z()
536 ]))
537 }
538 "centerOfMass" => {
539 let s = get_u32(args, "solid")?;
540 let deflection = get_f64(args, "deflection").unwrap_or(0.1);
541 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
542 let com = measure::solid_center_of_mass(&self.topo, solid_id, deflection)
543 .map_err(|e| e.to_string())?;
544 Ok(serde_json::json!([com.x(), com.y(), com.z()]))
545 }
546 "solidEdges" => {
547 let s = get_u32(args, "solid")?;
548 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
549 let edges = brepkit_topology::explorer::solid_edges(&self.topo, solid_id)
550 .map_err(|e| e.to_string())?;
551 let handles: Vec<u32> = edges.iter().map(|&e| edge_id_to_u32(e)).collect();
552 Ok(serde_json::json!(handles))
553 }
554 "solidToSolidDistance" => {
555 let a = get_u32(args, "solidA")?;
556 let b = get_u32(args, "solidB")?;
557 let a_id = self.resolve_solid(a).map_err(|e| e.to_string())?;
558 let b_id = self.resolve_solid(b).map_err(|e| e.to_string())?;
559 let result =
560 brepkit_operations::distance::solid_to_solid_distance(&self.topo, a_id, b_id)
561 .map_err(|e| e.to_string())?;
562 Ok(serde_json::json!([
563 result.distance,
564 result.point_a.x(),
565 result.point_a.y(),
566 result.point_a.z(),
567 result.point_b.x(),
568 result.point_b.y(),
569 result.point_b.z(),
570 ]))
571 }
572 "copySolid" => {
573 let s = get_u32(args, "solid")?;
574 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
575 let copy = brepkit_operations::copy::copy_solid(self.topo_mut(), solid_id)
576 .map_err(|e| e.to_string())?;
577 Ok(serde_json::json!(solid_id_to_u32(copy)))
578 }
579 "copyAndTransformSolid" => {
580 let s = get_u32(args, "solid")?;
581 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
582 let matrix = args["matrix"]
583 .as_array()
584 .ok_or("missing or invalid 'matrix'")?;
585 if matrix.len() != 16 {
586 return Err(format!(
587 "matrix must have 16 elements, got {}",
588 matrix.len()
589 ));
590 }
591 let elems: Vec<f64> = matrix
592 .iter()
593 .enumerate()
594 .map(|(i, v)| {
595 v.as_f64()
596 .ok_or_else(|| format!("matrix[{i}] is not a number"))
597 })
598 .collect::<Result<_, _>>()?;
599 let rows = std::array::from_fn(|i| std::array::from_fn(|j| elems[i * 4 + j]));
600 let mat = Mat4(rows);
601 let copy = brepkit_operations::copy::copy_and_transform_solid(
602 self.topo_mut(),
603 solid_id,
604 &mat,
605 )
606 .map_err(|e| e.to_string())?;
607 Ok(serde_json::json!(solid_id_to_u32(copy)))
608 }
609 "extrude" => {
611 let f = get_u32(args, "face")?;
612 let dx = get_f64(args, "dx").unwrap_or(0.0);
613 let dy = get_f64(args, "dy").unwrap_or(0.0);
614 let dz = get_f64(args, "dz").unwrap_or(1.0);
615 let dist = get_f64(args, "distance").unwrap_or(1.0);
616 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
617 let dir = Vec3::new(dx, dy, dz);
618 let solid =
619 extrude(self.topo_mut(), face_id, dir, dist).map_err(|e| e.to_string())?;
620 Ok(serde_json::json!(solid_id_to_u32(solid)))
621 }
622 "revolve" => {
623 let f = get_u32(args, "face")?;
624 let angle_degrees = get_f64(args, "angle")?;
625 let ox = get_f64(args, "originX").unwrap_or(0.0);
626 let oy = get_f64(args, "originY").unwrap_or(0.0);
627 let oz = get_f64(args, "originZ").unwrap_or(0.0);
628 let ax = get_f64(args, "axisX").unwrap_or(0.0);
629 let ay = get_f64(args, "axisY").unwrap_or(0.0);
630 let az = get_f64(args, "axisZ").unwrap_or(1.0);
631 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
632 let solid = revolve(
634 self.topo_mut(),
635 face_id,
636 Point3::new(ox, oy, oz),
637 Vec3::new(ax, ay, az),
638 angle_degrees.to_radians(),
639 )
640 .map_err(|e| e.to_string())?;
641 Ok(serde_json::json!(solid_id_to_u32(solid)))
642 }
643 "sweep" => {
644 let f = get_u32(args, "face")?;
645 let e = get_u32(args, "pathEdge")?;
646 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
647 let edge_id = self.resolve_edge(e).map_err(|e| e.to_string())?;
648 let edge_data = self.topo.edge(edge_id).map_err(|e| e.to_string())?;
649 let curve = match edge_data.curve() {
650 EdgeCurve::NurbsCurve(c) => c.clone(),
651 EdgeCurve::Line | EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) => {
652 return Err("sweep path must be a NURBS edge".into());
653 }
654 };
655 let solid = sweep(self.topo_mut(), face_id, &curve).map_err(|e| e.to_string())?;
656 Ok(serde_json::json!(solid_id_to_u32(solid)))
657 }
658 "multiSectionSweep" => {
659 let faces: Vec<u32> = args["faces"]
660 .as_array()
661 .map(|a| {
662 a.iter()
663 .filter_map(|v| v.as_u64().map(|n| n as u32))
664 .collect()
665 })
666 .unwrap_or_default();
667 let params: Vec<f64> = args["params"]
668 .as_array()
669 .map(|a| a.iter().filter_map(serde_json::Value::as_f64).collect())
670 .unwrap_or_default();
671 if faces.len() != params.len() {
672 return Err("multiSectionSweep: faces and params length mismatch".into());
673 }
674 let spine_edge = get_u32(args, "spineEdge")?;
675 let edge_id = self.resolve_edge(spine_edge).map_err(|e| e.to_string())?;
676 let edge_data = self.topo.edge(edge_id).map_err(|e| e.to_string())?;
677 let spine = match edge_data.curve() {
678 EdgeCurve::NurbsCurve(c) => c.clone(),
679 EdgeCurve::Line | EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) => {
680 return Err("multiSectionSweep spine must be a NURBS edge".into());
681 }
682 };
683 let ruled = args["ruled"].as_bool().unwrap_or(true);
684 let sections: Vec<(brepkit_topology::face::FaceId, f64)> = faces
685 .iter()
686 .zip(params.iter())
687 .map(|(&h, &p)| {
688 self.resolve_face(h)
689 .map(|f| (f, p))
690 .map_err(|e| e.to_string())
691 })
692 .collect::<Result<Vec<_>, _>>()?;
693 let solid = brepkit_operations::sweep::multi_section_sweep(
694 self.topo_mut(),
695 &spine,
696 §ions,
697 ruled,
698 )
699 .map_err(|e| e.to_string())?;
700 Ok(serde_json::json!(solid_id_to_u32(solid)))
701 }
702 "guidedSweep" => {
703 let face_id = self
704 .resolve_face(get_u32(args, "face")?)
705 .map_err(|e| e.to_string())?;
706 let nurbs_of =
707 |this: &Self, edge: u32, label: &str| -> Result<NurbsCurve, String> {
708 let edge_id = this.resolve_edge(edge).map_err(|e| e.to_string())?;
709 let edge_data = this.topo.edge(edge_id).map_err(|e| e.to_string())?;
710 match edge_data.curve() {
711 EdgeCurve::NurbsCurve(c) => Ok(c.clone()),
712 EdgeCurve::Line | EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) => {
713 Err(format!("guidedSweep {label} must be a NURBS edge"))
714 }
715 }
716 };
717 let spine = nurbs_of(self, get_u32(args, "spineEdge")?, "spineEdge")?;
718 let aux = nurbs_of(self, get_u32(args, "auxEdge")?, "auxEdge")?;
719 let solid =
720 brepkit_operations::sweep::sweep_guided(self.topo_mut(), face_id, &spine, aux)
721 .map_err(|e| e.to_string())?;
722 Ok(serde_json::json!(solid_id_to_u32(solid)))
723 }
724 "minkowskiSum" => {
725 let a = self
726 .resolve_solid(get_u32(args, "solidA")?)
727 .map_err(|e| e.to_string())?;
728 let b = self
729 .resolve_solid(get_u32(args, "solidB")?)
730 .map_err(|e| e.to_string())?;
731 let solid =
732 brepkit_operations::primitives::make_minkowski_sum(self.topo_mut(), a, b)
733 .map_err(|e| e.to_string())?;
734 Ok(serde_json::json!(solid_id_to_u32(solid)))
735 }
736 "projectEdges" => {
737 let solid = self
738 .resolve_solid(get_u32(args, "solid")?)
739 .map_err(|e| e.to_string())?;
740 let origin = Point3::new(
741 get_f64(args, "originX")?,
742 get_f64(args, "originY")?,
743 get_f64(args, "originZ")?,
744 );
745 let dir = Vec3::new(
746 get_f64(args, "dirX")?,
747 get_f64(args, "dirY")?,
748 get_f64(args, "dirZ")?,
749 );
750 let x_axis = Vec3::new(
751 get_f64(args, "xAxisX")?,
752 get_f64(args, "xAxisY")?,
753 get_f64(args, "xAxisZ")?,
754 );
755 let hidden_lines = args["hiddenLines"].as_bool().unwrap_or(true);
756 let deflection = get_f64(args, "deflection").unwrap_or(0.1);
757 let result = brepkit_operations::projection::project_edges(
758 &self.topo,
759 solid,
760 origin,
761 dir,
762 x_axis,
763 hidden_lines,
764 deflection,
765 )
766 .map_err(|e| e.to_string())?;
767 let flatten = |polys: &[Vec<brepkit_math::vec::Point2>]| -> Vec<Vec<f64>> {
768 polys
769 .iter()
770 .map(|poly| poly.iter().flat_map(|p| [p.x(), p.y()]).collect())
771 .collect()
772 };
773 Ok(serde_json::json!({
774 "visible": flatten(&result.visible),
775 "hidden": flatten(&result.hidden),
776 }))
777 }
778 "chamfer" => {
779 let s = get_u32(args, "solid")?;
780 let dist = get_f64(args, "distance")?;
781 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
782 let edge_handles: Vec<u32> = args["edges"]
783 .as_array()
784 .map(|arr| {
785 arr.iter()
786 .filter_map(|v| v.as_u64().map(|n| n as u32))
787 .collect()
788 })
789 .unwrap_or_default();
790 let edge_ids: Vec<_> = edge_handles
791 .iter()
792 .map(|&h| self.resolve_edge(h).map_err(|e| e.to_string()))
793 .collect::<Result<Vec<_>, _>>()?;
794 let result = brepkit_operations::chamfer::chamfer(
795 self.topo_mut(),
796 solid_id,
797 &edge_ids,
798 dist,
799 )
800 .map_err(|e| e.to_string())?;
801 Ok(serde_json::json!(solid_id_to_u32(result)))
802 }
803 "fillet" => {
804 let s = get_u32(args, "solid")?;
805 let radius = get_f64(args, "radius")?;
806 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
807 let edge_handles: Vec<u32> = args["edges"]
808 .as_array()
809 .map(|arr| {
810 arr.iter()
811 .filter_map(|v| v.as_u64().map(|n| n as u32))
812 .collect()
813 })
814 .unwrap_or_default();
815 let edge_ids: Vec<_> = edge_handles
816 .iter()
817 .map(|&h| self.resolve_edge(h).map_err(|e| e.to_string()))
818 .collect::<Result<Vec<_>, _>>()?;
819 let fillet_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
820 try_fillet(self.topo_mut(), solid_id, &edge_ids, radius)
821 }));
822 let result = match fillet_result {
823 Ok(inner) => inner.map_err(|e| e.to_string())?,
824 Err(panic_info) => {
825 return Err(panic_message(&panic_info, "Fillet"));
826 }
827 };
828 Ok(serde_json::json!(solid_id_to_u32(result)))
829 }
830 "filletVariable" => {
831 let s = get_u32(args, "solid")?;
832 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
833 let specs = args["specs"]
834 .as_array()
835 .ok_or_else(|| "missing 'specs' array".to_string())?;
836 let mut edge_laws = Vec::with_capacity(specs.len());
837 for spec in specs {
838 let edge_handle = spec["edge"]
839 .as_u64()
840 .ok_or_else(|| "missing 'edge' in fillet spec".to_string())?
841 as u32;
842 let edge_id = self.resolve_edge(edge_handle).map_err(|e| e.to_string())?;
843 let start_val = spec["start"]
844 .as_f64()
845 .or_else(|| spec["startRadius"].as_f64());
846 let end_val = spec["end"].as_f64().or_else(|| spec["endRadius"].as_f64());
847 let law_str =
848 spec["law"]
849 .as_str()
850 .unwrap_or_else(|| match (start_val, end_val) {
851 (Some(sv), Some(ev)) if (sv - ev).abs() > f64::EPSILON => "linear",
852 _ => "constant",
853 });
854 let law = match law_str {
855 "linear" => brepkit_operations::fillet::FilletRadiusLaw::Linear {
856 start: start_val.unwrap_or(1.0),
857 end: end_val.unwrap_or(1.0),
858 },
859 "scurve" => brepkit_operations::fillet::FilletRadiusLaw::SCurve {
860 start: start_val.unwrap_or(1.0),
861 end: end_val.unwrap_or(1.0),
862 },
863 _ => {
864 let r = spec["radius"].as_f64().or(start_val).unwrap_or(1.0);
865 brepkit_operations::fillet::FilletRadiusLaw::Constant(r)
866 }
867 };
868 edge_laws.push((edge_id, law));
869 }
870 let result = brepkit_operations::fillet::fillet_variable(
871 self.topo_mut(),
872 solid_id,
873 &edge_laws,
874 )
875 .map_err(|e| e.to_string())?;
876 Ok(serde_json::json!(solid_id_to_u32(result)))
877 }
878 "filletV2" => {
879 let s = get_u32(args, "solid")?;
880 let radius = get_f64(args, "radius")?;
881 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
882 let edge_handles: Vec<u32> = args["edges"]
883 .as_array()
884 .map(|arr| {
885 arr.iter()
886 .filter_map(|v| v.as_u64().map(|n| n as u32))
887 .collect()
888 })
889 .unwrap_or_default();
890 let edge_ids: Vec<_> = edge_handles
891 .iter()
892 .map(|&h| self.resolve_edge(h).map_err(|e| e.to_string()))
893 .collect::<Result<Vec<_>, _>>()?;
894 let result = brepkit_operations::blend_ops::fillet_v2(
895 self.topo_mut(),
896 solid_id,
897 &edge_ids,
898 radius,
899 )
900 .map_err(|e| e.to_string())?;
901 Ok(serde_json::json!(solid_id_to_u32(result.solid)))
902 }
903 "chamferV2" => {
904 let s = get_u32(args, "solid")?;
905 let d1 = get_f64(args, "d1")?;
906 let d2 = get_f64(args, "d2")?;
907 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
908 let edge_handles: Vec<u32> = args["edges"]
909 .as_array()
910 .map(|arr| {
911 arr.iter()
912 .filter_map(|v| v.as_u64().map(|n| n as u32))
913 .collect()
914 })
915 .unwrap_or_default();
916 let edge_ids: Vec<_> = edge_handles
917 .iter()
918 .map(|&h| self.resolve_edge(h).map_err(|e| e.to_string()))
919 .collect::<Result<Vec<_>, _>>()?;
920 let result = brepkit_operations::blend_ops::chamfer_v2(
921 self.topo_mut(),
922 solid_id,
923 &edge_ids,
924 d1,
925 d2,
926 )
927 .map_err(|e| e.to_string())?;
928 Ok(serde_json::json!(solid_id_to_u32(result.solid)))
929 }
930 "chamferDistanceAngle" => {
931 let s = get_u32(args, "solid")?;
932 let distance = get_f64(args, "distance")?;
933 let angle = get_f64(args, "angle")?;
934 if angle >= std::f64::consts::FRAC_PI_2 {
935 return Err("angle must be less than π/2".into());
936 }
937 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
938 let edge_handles: Vec<u32> = args["edges"]
939 .as_array()
940 .map(|arr| {
941 arr.iter()
942 .filter_map(|v| v.as_u64().map(|n| n as u32))
943 .collect()
944 })
945 .unwrap_or_default();
946 let edge_ids: Vec<_> = edge_handles
947 .iter()
948 .map(|&h| self.resolve_edge(h).map_err(|e| e.to_string()))
949 .collect::<Result<Vec<_>, _>>()?;
950 let result = brepkit_operations::blend_ops::chamfer_distance_angle(
951 self.topo_mut(),
952 solid_id,
953 &edge_ids,
954 distance,
955 angle,
956 )
957 .map_err(|e| e.to_string())?;
958 Ok(serde_json::json!(solid_id_to_u32(result.solid)))
959 }
960 "shell" => {
961 let s = get_u32(args, "solid")?;
962 let thickness = get_f64(args, "thickness")?;
963 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
964 let face_handles: Vec<u32> = args["faces"]
965 .as_array()
966 .map(|arr| {
967 arr.iter()
968 .filter_map(|v| v.as_u64().map(|n| n as u32))
969 .collect()
970 })
971 .unwrap_or_default();
972 let face_ids: Vec<_> = face_handles
973 .iter()
974 .map(|&h| self.resolve_face(h).map_err(|e| e.to_string()))
975 .collect::<Result<Vec<_>, _>>()?;
976 let result = brepkit_operations::shell_op::shell(
977 self.topo_mut(),
978 solid_id,
979 thickness,
980 &face_ids,
981 )
982 .map_err(|e| e.to_string())?;
983 Ok(serde_json::json!(solid_id_to_u32(result)))
984 }
985 "mirror" => {
986 let s = get_u32(args, "solid")?;
987 let px = get_f64(args, "px").unwrap_or(0.0);
988 let py = get_f64(args, "py").unwrap_or(0.0);
989 let pz = get_f64(args, "pz").unwrap_or(0.0);
990 let nx = get_f64(args, "nx").unwrap_or(1.0);
991 let ny = get_f64(args, "ny").unwrap_or(0.0);
992 let nz = get_f64(args, "nz").unwrap_or(0.0);
993 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
994 let result = brepkit_operations::mirror::mirror(
995 self.topo_mut(),
996 solid_id,
997 Point3::new(px, py, pz),
998 Vec3::new(nx, ny, nz),
999 )
1000 .map_err(|e| e.to_string())?;
1001 Ok(serde_json::json!(solid_id_to_u32(result)))
1002 }
1003 "unifyFaces" => {
1004 let s = get_u32(args, "solid")?;
1005 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1006 brepkit_operations::heal::unify_faces(self.topo_mut(), solid_id)
1007 .map_err(|e| e.to_string())?;
1008 Ok(serde_json::json!(solid_id_to_u32(solid_id)))
1009 }
1010 "convertToBspline" => {
1011 let s = get_u32(args, "solid")?;
1012 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1013 let count = brepkit_operations::heal::convert_to_bspline(self.topo_mut(), solid_id)
1014 .map_err(|e| e.to_string())?;
1015 Ok(serde_json::json!({
1016 "solid": solid_id_to_u32(solid_id),
1017 "converted": count,
1018 }))
1019 }
1020 "convertToElementary" => {
1021 let s = get_u32(args, "solid")?;
1022 let tol = get_f64(args, "tolerance").unwrap_or(crate::helpers::TOL);
1023 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1024 let count =
1025 brepkit_operations::heal::convert_to_elementary(self.topo_mut(), solid_id, tol)
1026 .map_err(|e| e.to_string())?;
1027 Ok(serde_json::json!({
1028 "solid": solid_id_to_u32(solid_id),
1029 "converted": count,
1030 }))
1031 }
1032 "healSolid" => {
1033 let s = get_u32(args, "solid")?;
1034 let tol = get_f64(args, "tolerance").unwrap_or(1e-7);
1035 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1036 brepkit_operations::heal::heal_solid(self.topo_mut(), solid_id, tol)
1037 .map_err(|e| e.to_string())?;
1038 Ok(serde_json::json!(solid_id_to_u32(solid_id)))
1039 }
1040 "repairSolid" => {
1041 let s = get_u32(args, "solid")?;
1042 let tol = get_f64(args, "tolerance").unwrap_or(1e-7);
1043 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1044 let report = brepkit_operations::heal::repair_solid(self.topo_mut(), solid_id, tol)
1045 .map_err(|e| e.to_string())?;
1046 Ok(serde_json::json!({
1047 "solid": solid_id_to_u32(solid_id),
1048 "errorsBefore": report.before.error_count(),
1049 "errorsAfter": report.after.error_count(),
1050 "totalRepairs": report.total_repairs(),
1051 }))
1052 }
1053 "classifyPoint" => {
1054 let s = get_u32(args, "solid")?;
1055 let x = get_f64(args, "x")?;
1056 let y = get_f64(args, "y")?;
1057 let z = get_f64(args, "z")?;
1058 let tol = get_f64(args, "tolerance").unwrap_or(1e-7);
1059 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1060 let pt = Point3::new(x, y, z);
1061 let result = brepkit_operations::classify::classify_point(
1062 &self.topo, solid_id, pt, 0.1, tol,
1063 )
1064 .map_err(|e| e.to_string())?;
1065 Ok(serde_json::json!(classify_to_string(result)))
1066 }
1067 "loft" => {
1068 let face_handles: Vec<u32> = args["faces"]
1069 .as_array()
1070 .map(|arr| {
1071 arr.iter()
1072 .filter_map(|v| v.as_u64().map(|n| n as u32))
1073 .collect()
1074 })
1075 .unwrap_or_default();
1076 let face_ids: Vec<_> = face_handles
1077 .iter()
1078 .map(|&h| self.resolve_face(h).map_err(|e| e.to_string()))
1079 .collect::<Result<Vec<_>, _>>()?;
1080 let result = brepkit_operations::loft::loft(self.topo_mut(), &face_ids)
1081 .map_err(|e| e.to_string())?;
1082 Ok(serde_json::json!(solid_id_to_u32(result)))
1083 }
1084 "loftSmooth" => {
1085 let face_handles: Vec<u32> = args["faces"]
1086 .as_array()
1087 .map(|arr| {
1088 arr.iter()
1089 .filter_map(|v| v.as_u64().map(|n| n as u32))
1090 .collect()
1091 })
1092 .unwrap_or_default();
1093 let face_ids: Vec<_> = face_handles
1094 .iter()
1095 .map(|&h| self.resolve_face(h).map_err(|e| e.to_string()))
1096 .collect::<Result<Vec<_>, _>>()?;
1097 let result = brepkit_operations::loft::loft_smooth(self.topo_mut(), &face_ids)
1098 .map_err(|e| e.to_string())?;
1099 Ok(serde_json::json!(solid_id_to_u32(result)))
1100 }
1101 "circularPattern" => {
1102 let s = get_u32(args, "solid")?;
1103 let ax = get_f64(args, "ax").unwrap_or(0.0);
1104 let ay = get_f64(args, "ay").unwrap_or(0.0);
1105 let az = get_f64(args, "az").unwrap_or(1.0);
1106 let count = get_u32(args, "count")?;
1107 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1108 let axis = Vec3::new(ax, ay, az);
1109 let compound = brepkit_operations::pattern::circular_pattern(
1110 self.topo_mut(),
1111 solid_id,
1112 axis,
1113 count as usize,
1114 )
1115 .map_err(|e| e.to_string())?;
1116 Ok(serde_json::json!(compound_id_to_u32(compound)))
1117 }
1118 "gridPattern" => {
1119 let s = get_u32(args, "solid")?;
1120 let dxx = get_f64(args, "dirXx").unwrap_or(1.0);
1121 let dxy = get_f64(args, "dirXy").unwrap_or(0.0);
1122 let dxz = get_f64(args, "dirXz").unwrap_or(0.0);
1123 let dyx = get_f64(args, "dirYx").unwrap_or(0.0);
1124 let dyy = get_f64(args, "dirYy").unwrap_or(1.0);
1125 let dyz = get_f64(args, "dirYz").unwrap_or(0.0);
1126 let sx = get_f64(args, "spacingX")?;
1127 let sy = get_f64(args, "spacingY")?;
1128 let cx = get_u32(args, "countX")?;
1129 let cy = get_u32(args, "countY")?;
1130 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1131 let compound = brepkit_operations::pattern::grid_pattern(
1132 self.topo_mut(),
1133 solid_id,
1134 Vec3::new(dxx, dxy, dxz),
1135 Vec3::new(dyx, dyy, dyz),
1136 sx,
1137 sy,
1138 cx as usize,
1139 cy as usize,
1140 )
1141 .map_err(|e| e.to_string())?;
1142 Ok(serde_json::json!(compound_id_to_u32(compound)))
1143 }
1144 "defeature" => {
1145 let s = get_u32(args, "solid")?;
1146 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1147 let face_handles: Vec<u32> = args["faces"]
1148 .as_array()
1149 .map(|arr| {
1150 arr.iter()
1151 .filter_map(|v| v.as_u64().map(|n| n as u32))
1152 .collect()
1153 })
1154 .unwrap_or_default();
1155 let face_ids: Vec<_> = face_handles
1156 .iter()
1157 .map(|&h| self.resolve_face(h).map_err(|e| e.to_string()))
1158 .collect::<Result<Vec<_>, _>>()?;
1159 let result =
1160 brepkit_operations::defeature::defeature(self.topo_mut(), solid_id, &face_ids)
1161 .map_err(|e| e.to_string())?;
1162 Ok(serde_json::json!(solid_id_to_u32(result)))
1163 }
1164 "copyWire" => {
1165 let w = get_u32(args, "wire")?;
1166 let wire_id = self.resolve_wire(w).map_err(|e| e.to_string())?;
1167 let copy = brepkit_operations::copy::copy_wire(self.topo_mut(), wire_id)
1168 .map_err(|e| e.to_string())?;
1169 Ok(serde_json::json!(wire_id_to_u32(copy)))
1170 }
1171 "copyFace" => {
1172 let f = get_u32(args, "face")?;
1173 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
1174 let copy = brepkit_operations::copy::copy_face(self.topo_mut(), face_id)
1175 .map_err(|e| e.to_string())?;
1176 Ok(serde_json::json!(face_id_to_u32(copy)))
1177 }
1178 "transformWire" => {
1179 let w = get_u32(args, "wire")?;
1180 let wire_id = self.resolve_wire(w).map_err(|e| e.to_string())?;
1181 let matrix = args["matrix"]
1182 .as_array()
1183 .ok_or("missing or invalid 'matrix'")?;
1184 if matrix.len() != 16 {
1185 return Err(format!(
1186 "matrix must have 16 elements, got {}",
1187 matrix.len()
1188 ));
1189 }
1190 let elems: Vec<f64> = matrix
1191 .iter()
1192 .enumerate()
1193 .map(|(i, v)| {
1194 v.as_f64()
1195 .ok_or_else(|| format!("matrix[{i}] is not a number"))
1196 })
1197 .collect::<Result<_, _>>()?;
1198 if let Some(pos) = elems.iter().position(|v| !v.is_finite()) {
1199 return Err(format!("matrix element at index {pos} is not finite"));
1200 }
1201 let rows = std::array::from_fn(|i| std::array::from_fn(|j| elems[i * 4 + j]));
1202 let mat = Mat4(rows);
1203 brepkit_operations::transform::transform_wire(self.topo_mut(), wire_id, &mat)
1204 .map_err(|e| e.to_string())?;
1205 Ok(serde_json::json!(null))
1206 }
1207 "transformFace" => {
1208 let f = get_u32(args, "face")?;
1209 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
1210 let matrix = args["matrix"]
1211 .as_array()
1212 .ok_or("missing or invalid 'matrix'")?;
1213 if matrix.len() != 16 {
1214 return Err(format!(
1215 "matrix must have 16 elements, got {}",
1216 matrix.len()
1217 ));
1218 }
1219 let elems: Vec<f64> = matrix
1220 .iter()
1221 .enumerate()
1222 .map(|(i, v)| {
1223 v.as_f64()
1224 .ok_or_else(|| format!("matrix element {i} is not a number"))
1225 })
1226 .collect::<Result<_, _>>()?;
1227 if let Some(pos) = elems.iter().position(|v| !v.is_finite()) {
1228 return Err(format!("matrix element at index {pos} is not finite"));
1229 }
1230 let rows = std::array::from_fn(|i| std::array::from_fn(|j| elems[i * 4 + j]));
1231 let mat = Mat4(rows);
1232 brepkit_operations::transform::transform_face(self.topo_mut(), face_id, &mat)
1233 .map_err(|e| e.to_string())?;
1234 Ok(serde_json::json!(null))
1235 }
1236 "offsetFace" => {
1237 let f = get_u32(args, "face")?;
1238 let dist = get_f64(args, "distance")?;
1239 let samples = get_u32(args, "samples").unwrap_or(16);
1240 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
1241 let result = brepkit_operations::offset_face::offset_face(
1242 self.topo_mut(),
1243 face_id,
1244 dist,
1245 samples as usize,
1246 )
1247 .map_err(|e| e.to_string())?;
1248 Ok(serde_json::json!(face_id_to_u32(result)))
1249 }
1250 "offsetSolid" => {
1251 let s = get_u32(args, "solid")?;
1252 let dist = get_f64(args, "distance")?;
1253 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1254 let result =
1255 brepkit_operations::offset_v2::offset_solid_v2(self.topo_mut(), solid_id, dist)
1256 .map_err(|e| e.to_string())?;
1257 Ok(serde_json::json!(solid_id_to_u32(result)))
1258 }
1259 "offsetSolidV2" => {
1260 let s = get_u32(args, "solid")?;
1261 let dist = get_f64(args, "distance")?;
1262 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1263 let result =
1264 brepkit_operations::offset_v2::offset_solid_v2(self.topo_mut(), solid_id, dist)
1265 .map_err(|e| e.to_string())?;
1266 Ok(serde_json::json!(solid_id_to_u32(result)))
1267 }
1268 "section" => {
1269 let s = get_u32(args, "solid")?;
1270 let px = get_f64(args, "px").unwrap_or(0.0);
1271 let py = get_f64(args, "py").unwrap_or(0.0);
1272 let pz = get_f64(args, "pz").unwrap_or(0.0);
1273 let nx = get_f64(args, "nx").unwrap_or(0.0);
1274 let ny = get_f64(args, "ny").unwrap_or(0.0);
1275 let nz = get_f64(args, "nz").unwrap_or(1.0);
1276 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1277 let result = brepkit_operations::section::section(
1278 self.topo_mut(),
1279 solid_id,
1280 Point3::new(px, py, pz),
1281 Vec3::new(nx, ny, nz),
1282 )
1283 .map_err(|e| e.to_string())?;
1284 let face_ids: Vec<u32> = result.faces.iter().map(|&f| face_id_to_u32(f)).collect();
1285 Ok(serde_json::json!(face_ids))
1286 }
1287 "split" => {
1288 let s = get_u32(args, "solid")?;
1289 let px = get_f64(args, "px").unwrap_or(0.0);
1290 let py = get_f64(args, "py").unwrap_or(0.0);
1291 let pz = get_f64(args, "pz").unwrap_or(0.0);
1292 let nx = get_f64(args, "nx").unwrap_or(0.0);
1293 let ny = get_f64(args, "ny").unwrap_or(0.0);
1294 let nz = get_f64(args, "nz").unwrap_or(1.0);
1295 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1296 let result = brepkit_operations::split::split(
1297 self.topo_mut(),
1298 solid_id,
1299 Point3::new(px, py, pz),
1300 Vec3::new(nx, ny, nz),
1301 )
1302 .map_err(|e| e.to_string())?;
1303 Ok(serde_json::json!({
1304 "positive": solid_id_to_u32(result.positive),
1305 "negative": solid_id_to_u32(result.negative),
1306 }))
1307 }
1308 "sewFaces" => {
1309 let face_handles: Vec<u32> = args["faces"]
1310 .as_array()
1311 .map(|arr| {
1312 arr.iter()
1313 .filter_map(|v| v.as_u64().map(|n| n as u32))
1314 .collect()
1315 })
1316 .unwrap_or_default();
1317 let tol = get_f64(args, "tolerance").unwrap_or(1e-6);
1318 let face_ids: Vec<_> = face_handles
1319 .iter()
1320 .map(|&h| self.resolve_face(h).map_err(|e| e.to_string()))
1321 .collect::<Result<Vec<_>, _>>()?;
1322 let solid = brepkit_operations::sew::sew_faces(self.topo_mut(), &face_ids, tol)
1323 .map_err(|e| e.to_string())?;
1324 Ok(serde_json::json!(solid_id_to_u32(solid)))
1325 }
1326 "thicken" => {
1327 let f = get_u32(args, "face")?;
1328 let thickness = get_f64(args, "thickness")?;
1329 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
1330 let result =
1331 brepkit_operations::thicken::thicken(self.topo_mut(), face_id, thickness)
1332 .map_err(|e| e.to_string())?;
1333 Ok(serde_json::json!(solid_id_to_u32(result)))
1334 }
1335 "pipe" => {
1336 let f = get_u32(args, "face")?;
1337 let e = get_u32(args, "pathEdge")?;
1338 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
1339 let edge_id = self.resolve_edge(e).map_err(|e| e.to_string())?;
1340 let edge_data = self.topo.edge(edge_id).map_err(|e| e.to_string())?;
1341 let curve = match edge_data.curve() {
1342 EdgeCurve::NurbsCurve(c) => c.clone(),
1343 EdgeCurve::Line | EdgeCurve::Circle(_) | EdgeCurve::Ellipse(_) => {
1344 return Err("pipe path must be a NURBS edge".into());
1345 }
1346 };
1347 let solid = brepkit_operations::pipe::pipe(self.topo_mut(), face_id, &curve, None)
1348 .map_err(|e| e.to_string())?;
1349 Ok(serde_json::json!(solid_id_to_u32(solid)))
1350 }
1351 "linearPattern" => {
1352 let s = get_u32(args, "solid")?;
1353 let dx = get_f64(args, "dx").unwrap_or(1.0);
1354 let dy = get_f64(args, "dy").unwrap_or(0.0);
1355 let dz = get_f64(args, "dz").unwrap_or(0.0);
1356 let spacing = get_f64(args, "spacing")?;
1357 let count = get_u32(args, "count")?;
1358 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1359 let compound = brepkit_operations::pattern::linear_pattern(
1360 self.topo_mut(),
1361 solid_id,
1362 Vec3::new(dx, dy, dz),
1363 spacing,
1364 count as usize,
1365 )
1366 .map_err(|e| e.to_string())?;
1367 Ok(serde_json::json!(compound_id_to_u32(compound)))
1368 }
1369 "draft" => {
1370 let s = get_u32(args, "solid")?;
1371 let angle = get_f64(args, "angle")?;
1372 let solid_id = self.resolve_solid(s).map_err(|e| e.to_string())?;
1373 let face_handles: Vec<u32> = args["faces"]
1374 .as_array()
1375 .map(|arr| {
1376 arr.iter()
1377 .filter_map(|v| v.as_u64().map(|n| n as u32))
1378 .collect()
1379 })
1380 .unwrap_or_default();
1381 let face_ids: Vec<_> = face_handles
1382 .iter()
1383 .map(|&h| self.resolve_face(h).map_err(|e| e.to_string()))
1384 .collect::<Result<Vec<_>, _>>()?;
1385 let dx = get_f64(args, "dirX").unwrap_or(0.0);
1386 let dy = get_f64(args, "dirY").unwrap_or(0.0);
1387 let dz = get_f64(args, "dirZ").unwrap_or(1.0);
1388 let npx = get_f64(args, "neutralX").unwrap_or(0.0);
1389 let npy = get_f64(args, "neutralY").unwrap_or(0.0);
1390 let npz = get_f64(args, "neutralZ").unwrap_or(0.0);
1391 let dir = Vec3::new(dx, dy, dz);
1392 let neutral = Point3::new(npx, npy, npz);
1393 let result = brepkit_operations::draft::draft(
1394 self.topo_mut(),
1395 solid_id,
1396 &face_ids,
1397 dir,
1398 neutral,
1399 angle,
1400 )
1401 .map_err(|e| e.to_string())?;
1402 Ok(serde_json::json!(solid_id_to_u32(result)))
1403 }
1404 "makeTangentArc3d" => {
1405 let sx = get_f64(args, "startX")?;
1406 let sy = get_f64(args, "startY")?;
1407 let sz = get_f64(args, "startZ")?;
1408 let tx = get_f64(args, "tangentX")?;
1409 let ty = get_f64(args, "tangentY")?;
1410 let tz = get_f64(args, "tangentZ")?;
1411 let ex = get_f64(args, "endX")?;
1412 let ey = get_f64(args, "endY")?;
1413 let ez = get_f64(args, "endZ")?;
1414 let eid = self
1415 .make_tangent_arc_3d_impl(sx, sy, sz, tx, ty, tz, ex, ey, ez)
1416 .map_err(|e| e.to_string())?;
1417 Ok(serde_json::json!(eid))
1418 }
1419 "liftCurve2dToPlane" => {
1420 let ct = get_u32(args, "curveType")?;
1421 let params_arr = args["curveParams"]
1422 .as_array()
1423 .ok_or("missing or invalid 'curveParams'")?;
1424 let cp: Vec<f64> = params_arr
1425 .iter()
1426 .enumerate()
1427 .map(|(i, v)| {
1428 v.as_f64()
1429 .ok_or_else(|| format!("curveParams[{i}] is not a number"))
1430 })
1431 .collect::<Result<_, _>>()?;
1432 let ox = get_f64(args, "originX")?;
1433 let oy = get_f64(args, "originY")?;
1434 let oz = get_f64(args, "originZ")?;
1435 let xx = get_f64(args, "xAxisX")?;
1436 let xy = get_f64(args, "xAxisY")?;
1437 let xz = get_f64(args, "xAxisZ")?;
1438 let nx = get_f64(args, "normalX")?;
1439 let ny = get_f64(args, "normalY")?;
1440 let nz = get_f64(args, "normalZ")?;
1441 let t0 = get_f64(args, "tStart")?;
1442 let t1 = get_f64(args, "tEnd")?;
1443 let eid = self
1444 .lift_curve2d_to_plane_impl(ct, cp, ox, oy, oz, xx, xy, xz, nx, ny, nz, t0, t1)
1445 .map_err(|e| e.to_string())?;
1446 Ok(serde_json::json!(eid))
1447 }
1448 "offsetWire" => {
1449 let f = get_u32(args, "face")?;
1450 let dist = get_f64(args, "distance")?;
1451 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
1452 let wire_id =
1453 brepkit_operations::offset_wire::offset_wire(self.topo_mut(), face_id, dist)
1454 .map_err(|e| e.to_string())?;
1455 Ok(serde_json::json!(wire_id_to_u32(wire_id)))
1456 }
1457 "offsetWireWithJoinType" => {
1458 let f = get_u32(args, "face")?;
1459 let dist = get_f64(args, "distance")?;
1460 let jt_str = args["joinType"]
1461 .as_str()
1462 .ok_or("missing or invalid 'joinType' string")?;
1463 let jt =
1464 super::operations::parse_join_type_str(jt_str).map_err(|e| e.to_string())?;
1465 let face_id = self.resolve_face(f).map_err(|e| e.to_string())?;
1466 let wire_id = brepkit_operations::offset_wire::offset_wire_with_join(
1467 self.topo_mut(),
1468 face_id,
1469 dist,
1470 jt,
1471 )
1472 .map_err(|e| e.to_string())?;
1473 Ok(serde_json::json!(wire_id_to_u32(wire_id)))
1474 }
1475 "offsetWire2DWithJoin" => {
1476 let w = get_u32(args, "wire")?;
1477 let dist = get_f64(args, "distance")?;
1478 let jt_str = args["joinType"]
1479 .as_str()
1480 .ok_or("missing or invalid 'joinType' string")?;
1481 let jt =
1482 super::operations::parse_join_type_str(jt_str).map_err(|e| e.to_string())?;
1483 let wire_id = self.resolve_wire(w).map_err(|e| e.to_string())?;
1484 let face_id =
1485 brepkit_topology::builder::make_planar_face_from_wire(self.topo_mut(), wire_id)
1486 .map_err(|e| e.to_string())?;
1487 let result = brepkit_operations::offset_wire::offset_wire_with_join(
1488 self.topo_mut(),
1489 face_id,
1490 dist,
1491 jt,
1492 )
1493 .map_err(|e| e.to_string())?;
1494 Ok(serde_json::json!(wire_id_to_u32(result)))
1495 }
1496 "getNurbsCurveData" => {
1497 let edge = get_u32(args, "edge")?;
1498 let curve = self.extract_nurbs_curve(edge).map_err(|e| e.to_string())?;
1499 Ok(super::nurbs::curve_data_json(&curve))
1500 }
1501 "getNurbsSurfaceData" => {
1502 let face = get_u32(args, "face")?;
1503 let surface = self
1504 .extract_nurbs_surface(face)
1505 .map_err(|e| e.to_string())?;
1506 Ok(super::nurbs::surface_data_json(&surface))
1507 }
1508 "getNurbsSurfaceDataParity" => {
1509 let face = get_u32(args, "face")?;
1510 self.free_form_surface_data_parity(face)
1511 .map_err(|e| e.to_string())
1512 }
1513 _ => Err(format!("unknown operation: {op}")),
1514 }
1515 }
1516}