1#![allow(
8 clippy::missing_errors_doc,
9 clippy::too_many_arguments,
10 clippy::redundant_closure,
11 clippy::redundant_closure_for_method_calls,
12 clippy::map_unwrap_or,
13 clippy::expect_used
14)]
15
16use std::rc::Rc;
17
18use brepkit_math::curves::{Circle3D, Ellipse3D};
19use brepkit_math::curves2d::Line2D;
20use brepkit_math::nurbs::curve::NurbsCurve;
21use brepkit_math::nurbs::surface::NurbsSurface;
22use brepkit_math::surfaces::{
23 ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface,
24};
25use brepkit_math::vec::{Point2, Point3, Vec2, Vec3};
26use brepkit_topology::Topology;
27use brepkit_topology::edge::{Edge, EdgeCurve};
28use brepkit_topology::face::{Face, FaceSurface};
29use brepkit_topology::vertex::Vertex;
30use brepkit_topology::wire::{OrientedEdge, Wire};
31use wasm_bindgen::prelude::*;
32
33use crate::error::{WasmError, validate_finite};
34use crate::handles::{edge_id_to_u32, solid_id_to_u32};
35use crate::helpers::TOL;
36use crate::state::{Checkpoint, SketchState};
37
38#[wasm_bindgen]
43pub struct BrepKernel {
44 pub(crate) topo: Rc<Topology>,
45 pub(crate) assemblies: Vec<brepkit_operations::assembly::Assembly>,
46 pub(crate) sketches: Vec<SketchState>,
47 pub(crate) checkpoints: Vec<Checkpoint>,
48 pub(crate) poisoned: bool,
49}
50
51#[wasm_bindgen]
52impl BrepKernel {
53 #[wasm_bindgen(constructor)]
55 #[must_use]
56 pub fn new() -> Self {
57 crate::panics::install_hook();
58 Self {
59 topo: Rc::new(Topology::new()),
60 assemblies: Vec::new(),
61 sketches: Vec::new(),
62 checkpoints: Vec::new(),
63 poisoned: false,
64 }
65 }
66}
67
68impl Default for BrepKernel {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl BrepKernel {
77 pub(crate) fn topo_mut(&mut self) -> &mut Topology {
80 Rc::make_mut(&mut self.topo)
81 }
82
83 pub(crate) fn topo(&self) -> &Topology {
85 &self.topo
86 }
87
88 #[allow(clippy::too_many_arguments)]
90 pub(crate) fn make_tangent_arc_3d_impl(
91 &mut self,
92 start_x: f64,
93 start_y: f64,
94 start_z: f64,
95 tangent_x: f64,
96 tangent_y: f64,
97 tangent_z: f64,
98 end_x: f64,
99 end_y: f64,
100 end_z: f64,
101 ) -> Result<u32, WasmError> {
102 for (v, name) in [
103 (start_x, "startX"),
104 (start_y, "startY"),
105 (start_z, "startZ"),
106 (tangent_x, "tangentX"),
107 (tangent_y, "tangentY"),
108 (tangent_z, "tangentZ"),
109 (end_x, "endX"),
110 (end_y, "endY"),
111 (end_z, "endZ"),
112 ] {
113 validate_finite(v, name)?;
114 }
115
116 let start = Point3::new(start_x, start_y, start_z);
117 let end = Point3::new(end_x, end_y, end_z);
118 let tangent = Vec3::new(tangent_x, tangent_y, tangent_z);
119
120 let chord = end - start;
121 if chord.length() < TOL {
122 return Err(WasmError::InvalidInput {
123 reason: "start and end points coincide".into(),
124 });
125 }
126
127 let t_norm = tangent.normalize().map_err(|e| WasmError::InvalidInput {
128 reason: format!("invalid tangent: {e}"),
129 })?;
130
131 let cross = t_norm.cross(chord);
133 if cross.length() < 1e-10 * chord.length() {
134 let v_start = self.topo_mut().add_vertex(Vertex::new(start, TOL));
135 let v_end = self.topo_mut().add_vertex(Vertex::new(end, TOL));
136 let eid = self
137 .topo_mut()
138 .add_edge(Edge::new(v_start, v_end, EdgeCurve::Line));
139 return Ok(edge_id_to_u32(eid));
140 }
141
142 let normal = cross.normalize().map_err(|e| WasmError::InvalidInput {
144 reason: format!("degenerate arc plane: {e}"),
145 })?;
146 let perp = normal.cross(t_norm);
147 let half_proj = chord.length_squared() / (2.0 * perp.dot(chord));
148 let center = start + perp * half_proj;
149 let radius = half_proj.abs();
150
151 let u_axis = (start - center)
152 .normalize()
153 .map_err(|e| WasmError::InvalidInput {
154 reason: format!("degenerate u_axis: {e}"),
155 })?;
156 let v_axis = normal.cross(u_axis);
157
158 let circle = Circle3D::with_axes(center, normal, radius, u_axis, v_axis).map_err(|e| {
159 WasmError::InvalidInput {
160 reason: format!("invalid circle: {e}"),
161 }
162 })?;
163
164 let v_start = self.topo_mut().add_vertex(Vertex::new(start, TOL));
165 let v_end = if (start - end).length() < TOL * 100.0 {
166 v_start
167 } else {
168 self.topo_mut().add_vertex(Vertex::new(end, TOL))
169 };
170 let eid = self
171 .topo_mut()
172 .add_edge(Edge::new(v_start, v_end, EdgeCurve::Circle(circle)));
173 Ok(edge_id_to_u32(eid))
174 }
175
176 #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
178 pub(crate) fn lift_curve2d_to_plane_impl(
179 &mut self,
180 curve_type: u32,
181 curve_params: Vec<f64>,
182 origin_x: f64,
183 origin_y: f64,
184 origin_z: f64,
185 x_axis_x: f64,
186 x_axis_y: f64,
187 x_axis_z: f64,
188 normal_x: f64,
189 normal_y: f64,
190 normal_z: f64,
191 t_start: f64,
192 t_end: f64,
193 ) -> Result<u32, WasmError> {
194 validate_finite(origin_x, "originX")?;
195 validate_finite(origin_y, "originY")?;
196 validate_finite(origin_z, "originZ")?;
197 validate_finite(x_axis_x, "xAxisX")?;
198 validate_finite(x_axis_y, "xAxisY")?;
199 validate_finite(x_axis_z, "xAxisZ")?;
200 validate_finite(normal_x, "normalX")?;
201 validate_finite(normal_y, "normalY")?;
202 validate_finite(normal_z, "normalZ")?;
203 validate_finite(t_start, "tStart")?;
204 validate_finite(t_end, "tEnd")?;
205
206 if curve_type > 3 {
207 return Err(WasmError::InvalidInput {
208 reason: format!("curve_type must be 0–3, got {curve_type}"),
209 });
210 }
211
212 for (i, &v) in curve_params.iter().enumerate() {
213 validate_finite(v, &format!("curveParams[{i}]"))?;
214 }
215
216 let normal = Vec3::new(normal_x, normal_y, normal_z)
217 .normalize()
218 .map_err(|e| WasmError::InvalidInput {
219 reason: format!("invalid normal: {e}"),
220 })?;
221 let x_raw = Vec3::new(x_axis_x, x_axis_y, x_axis_z);
222 let x_axis = (x_raw - normal * x_raw.dot(normal))
223 .normalize()
224 .map_err(|e| WasmError::InvalidInput {
225 reason: format!("invalid x_axis (parallel to normal?): {e}"),
226 })?;
227 let y_axis = normal.cross(x_axis);
228 let origin = Point3::new(origin_x, origin_y, origin_z);
229
230 let lift = |x: f64, y: f64| -> Point3 { origin + x_axis * x + y_axis * y };
231
232 match curve_type {
233 0 => {
234 if curve_params.len() != 4 {
235 return Err(WasmError::InvalidInput {
236 reason: format!(
237 "Line2D expects 4 params [ox,oy,dx,dy], got {}",
238 curve_params.len()
239 ),
240 });
241 }
242 let line2d = Line2D::new(
243 Point2::new(curve_params[0], curve_params[1]),
244 Vec2::new(curve_params[2], curve_params[3]),
245 )
246 .map_err(|e| WasmError::InvalidInput {
247 reason: format!("invalid Line2D: {e}"),
248 })?;
249 let p0 = line2d.evaluate(t_start);
250 let p1 = line2d.evaluate(t_end);
251 let start_3d = lift(p0.x(), p0.y());
252 let end_3d = lift(p1.x(), p1.y());
253 if (end_3d - start_3d).length() < TOL {
254 return Err(WasmError::InvalidInput {
255 reason: "degenerate line segment (start ≈ end)".into(),
256 });
257 }
258 let v_start = self.topo_mut().add_vertex(Vertex::new(start_3d, TOL));
259 let v_end = self.topo_mut().add_vertex(Vertex::new(end_3d, TOL));
260 let eid = self
261 .topo_mut()
262 .add_edge(Edge::new(v_start, v_end, EdgeCurve::Line));
263 Ok(edge_id_to_u32(eid))
264 }
265 1 => {
266 if curve_params.len() != 3 {
267 return Err(WasmError::InvalidInput {
268 reason: format!(
269 "Circle expects 3 params [cx,cy,r], got {}",
270 curve_params.len()
271 ),
272 });
273 }
274 let center_3d = lift(curve_params[0], curve_params[1]);
275 let radius = curve_params[2];
276 let circle = Circle3D::with_axes(center_3d, normal, radius, x_axis, y_axis)
277 .map_err(|e| WasmError::InvalidInput {
278 reason: format!("invalid Circle3D: {e}"),
279 })?;
280
281 let start_3d = circle.evaluate(t_start);
282 let end_3d = circle.evaluate(t_end);
283
284 let full_circle = (t_end - t_start).abs() >= std::f64::consts::TAU - 1e-10;
285 let v_start = self.topo_mut().add_vertex(Vertex::new(start_3d, TOL));
286 let v_end = if full_circle {
287 v_start
288 } else {
289 self.topo_mut().add_vertex(Vertex::new(end_3d, TOL))
290 };
291 let eid =
292 self.topo_mut()
293 .add_edge(Edge::new(v_start, v_end, EdgeCurve::Circle(circle)));
294 Ok(edge_id_to_u32(eid))
295 }
296 2 => {
297 if curve_params.len() != 5 {
298 return Err(WasmError::InvalidInput {
299 reason: format!(
300 "Ellipse expects 5 params [cx,cy,a,b,rot], got {}",
301 curve_params.len()
302 ),
303 });
304 }
305 let semi_major = curve_params[2];
306 let semi_minor = curve_params[3];
307 let rotation = curve_params[4];
308
309 let center_3d = lift(curve_params[0], curve_params[1]);
310 let (sin_r, cos_r) = rotation.sin_cos();
311 let u3d = x_axis * cos_r + y_axis * sin_r;
312 let v3d = y_axis * cos_r - x_axis * sin_r;
313 let ellipse =
314 Ellipse3D::with_axes(center_3d, normal, semi_major, semi_minor, u3d, v3d)
315 .map_err(|e| WasmError::InvalidInput {
316 reason: format!("invalid Ellipse3D: {e}"),
317 })?;
318
319 let start_3d = ellipse.evaluate(t_start);
320 let end_3d = ellipse.evaluate(t_end);
321
322 let full_ellipse = (t_end - t_start).abs() >= std::f64::consts::TAU - 1e-10;
323 let v_start = self.topo_mut().add_vertex(Vertex::new(start_3d, TOL));
324 let v_end = if full_ellipse {
325 v_start
326 } else {
327 self.topo_mut().add_vertex(Vertex::new(end_3d, TOL))
328 };
329 let eid = self.topo_mut().add_edge(Edge::new(
330 v_start,
331 v_end,
332 EdgeCurve::Ellipse(ellipse),
333 ));
334 Ok(edge_id_to_u32(eid))
335 }
336 3 => {
337 if curve_params.len() < 2 {
338 return Err(WasmError::InvalidInput {
339 reason: "NURBS params too short (need at least degree + n_cp)".into(),
340 });
341 }
342 let raw_degree = curve_params[0];
343 let raw_n_cp = curve_params[1];
344 if !(1.0..=16.0).contains(&raw_degree) || raw_degree.fract() != 0.0 {
345 return Err(WasmError::InvalidInput {
346 reason: format!(
347 "NURBS degree must be an integer in [1, 16], got {raw_degree}"
348 ),
349 });
350 }
351 if !(1.0..=4096.0).contains(&raw_n_cp) || raw_n_cp.fract() != 0.0 {
352 return Err(WasmError::InvalidInput {
353 reason: format!(
354 "NURBS n_cp must be an integer in [1, 4096], got {raw_n_cp}"
355 ),
356 });
357 }
358 #[allow(clippy::cast_possible_truncation)]
359 let degree = raw_degree as usize;
360 #[allow(clippy::cast_possible_truncation)]
361 let n_cp = raw_n_cp as usize;
362 let n_knots = n_cp + degree + 1;
363 let expected_len = 2 + n_knots + 3 * n_cp;
364 if curve_params.len() != expected_len {
365 return Err(WasmError::InvalidInput {
366 reason: format!(
367 "NURBS params: expected {expected_len} elements \
368 (2 + {n_knots} knots + {} coords + {n_cp} weights), got {}",
369 2 * n_cp,
370 curve_params.len()
371 ),
372 });
373 }
374 let knots = curve_params[2..2 + n_knots].to_vec();
375 let coords_start = 2 + n_knots;
376 let weights_start = coords_start + 2 * n_cp;
377 let control_points_3d: Vec<Point3> = curve_params[coords_start..weights_start]
378 .chunks_exact(2)
379 .map(|c| lift(c[0], c[1]))
380 .collect();
381 let weights = curve_params[weights_start..weights_start + n_cp].to_vec();
382
383 let curve = NurbsCurve::new(degree, knots, control_points_3d, weights)?;
384 let start_3d = curve.evaluate(t_start);
385 let end_3d = curve.evaluate(t_end);
386
387 let v_start = self.topo_mut().add_vertex(Vertex::new(start_3d, TOL));
388 let v_end = if (start_3d - end_3d).length() < TOL * 100.0 {
389 v_start
390 } else {
391 self.topo_mut().add_vertex(Vertex::new(end_3d, TOL))
392 };
393 let eid = self.topo_mut().add_edge(Edge::new(
394 v_start,
395 v_end,
396 EdgeCurve::NurbsCurve(curve),
397 ));
398 Ok(edge_id_to_u32(eid))
399 }
400 _ => Err(WasmError::InvalidInput {
401 reason: format!("curve_type must be 0–3, got {curve_type}"),
402 }),
403 }
404 }
405
406 pub(crate) fn make_planar_face(
408 &mut self,
409 points: &[Point3],
410 ) -> Result<brepkit_topology::face::FaceId, WasmError> {
411 Ok(brepkit_topology::builder::make_planar_face(
412 self.topo_mut(),
413 points,
414 TOL,
415 )?)
416 }
417
418 fn compute_plane_from_wire(
423 &self,
424 wire_id: brepkit_topology::wire::WireId,
425 ) -> Result<FaceSurface, WasmError> {
426 let wire = self.topo.wire(wire_id)?;
427 let mut points = Vec::new();
428 for oe in wire.edges() {
429 let edge = self.topo.edge(oe.edge())?;
430 let start_pos = self.topo.vertex(edge.start())?.point();
431 points.push(start_pos);
432 }
433 if points.len() < 3 {
434 return Err(WasmError::InvalidInput {
435 reason: "need at least 3 vertices to compute a plane".into(),
436 });
437 }
438 let e1 = points[1] - points[0];
439 let e2 = points[2] - points[0];
440 let normal = e1.cross(e2).normalize()?;
441 let p0 = points[0];
442 let d = normal
443 .x()
444 .mul_add(p0.x(), normal.y().mul_add(p0.y(), normal.z() * p0.z()));
445 Ok(FaceSurface::Plane { normal, d })
446 }
447
448 fn parse_vec3(arr: &[serde_json::Value], name: &str) -> Result<Vec3, WasmError> {
450 let x = arr
451 .first()
452 .and_then(|v| v.as_f64())
453 .ok_or_else(|| WasmError::InvalidInput {
454 reason: format!("{name}[0] is not a number"),
455 })?;
456 let y = arr
457 .get(1)
458 .and_then(|v| v.as_f64())
459 .ok_or_else(|| WasmError::InvalidInput {
460 reason: format!("{name}[1] is not a number"),
461 })?;
462 let z = arr
463 .get(2)
464 .and_then(|v| v.as_f64())
465 .ok_or_else(|| WasmError::InvalidInput {
466 reason: format!("{name}[2] is not a number"),
467 })?;
468 Ok(Vec3::new(x, y, z))
469 }
470
471 fn parse_point3(arr: &[serde_json::Value], name: &str) -> Result<Point3, WasmError> {
473 let v = Self::parse_vec3(arr, name)?;
474 Ok(Point3::new(v.x(), v.y(), v.z()))
475 }
476
477 #[allow(clippy::too_many_lines, clippy::wrong_self_convention)]
480 pub(crate) fn from_brep_impl(&mut self, json: &str) -> Result<u32, WasmError> {
481 let parsed: serde_json::Value =
482 serde_json::from_str(json).map_err(|e| WasmError::InvalidInput {
483 reason: format!("invalid BREP JSON: {e}"),
484 })?;
485
486 let vertices = parsed["vertices"]
488 .as_array()
489 .ok_or_else(|| WasmError::InvalidInput {
490 reason: "missing vertices array".into(),
491 })?;
492 let mut vertex_map: std::collections::HashMap<u32, brepkit_topology::vertex::VertexId> =
493 std::collections::HashMap::new();
494
495 for v in vertices {
496 let id = v["id"].as_u64().ok_or_else(|| WasmError::InvalidInput {
497 reason: "vertex missing id".into(),
498 })? as u32;
499 let pos = v["position"]
500 .as_array()
501 .ok_or_else(|| WasmError::InvalidInput {
502 reason: "vertex missing position".into(),
503 })?;
504 let x =
505 pos.first()
506 .and_then(|v| v.as_f64())
507 .ok_or_else(|| WasmError::InvalidInput {
508 reason: "invalid vertex x coordinate".into(),
509 })?;
510 let y = pos
511 .get(1)
512 .and_then(|v| v.as_f64())
513 .ok_or_else(|| WasmError::InvalidInput {
514 reason: "invalid vertex y coordinate".into(),
515 })?;
516 let z = pos
517 .get(2)
518 .and_then(|v| v.as_f64())
519 .ok_or_else(|| WasmError::InvalidInput {
520 reason: "invalid vertex z coordinate".into(),
521 })?;
522 let vid = self
523 .topo_mut()
524 .add_vertex(Vertex::new(Point3::new(x, y, z), TOL));
525 vertex_map.insert(id, vid);
526 }
527
528 let edges = parsed["edges"]
530 .as_array()
531 .ok_or_else(|| WasmError::InvalidInput {
532 reason: "missing edges array".into(),
533 })?;
534 let mut edge_map: std::collections::HashMap<u32, brepkit_topology::edge::EdgeId> =
535 std::collections::HashMap::new();
536
537 for e in edges {
538 let id = e["id"].as_u64().ok_or_else(|| WasmError::InvalidInput {
539 reason: "edge missing id".into(),
540 })? as u32;
541 let start_v = e["startVertex"]
542 .as_u64()
543 .ok_or_else(|| WasmError::InvalidInput {
544 reason: "edge missing startVertex".into(),
545 })? as u32;
546 let end_v = e["endVertex"]
547 .as_u64()
548 .ok_or_else(|| WasmError::InvalidInput {
549 reason: "edge missing endVertex".into(),
550 })? as u32;
551
552 let start_vid = *vertex_map
553 .get(&start_v)
554 .ok_or_else(|| WasmError::InvalidInput {
555 reason: format!("edge {id} references unknown start vertex {start_v}"),
556 })?;
557 let end_vid = *vertex_map
558 .get(&end_v)
559 .ok_or_else(|| WasmError::InvalidInput {
560 reason: format!("edge {id} references unknown end vertex {end_v}"),
561 })?;
562
563 let curve_type = e["curveType"].as_str().unwrap_or("line");
564 let params = e.get("curveParams").and_then(|p| p.as_object());
565 let curve = match curve_type {
566 "line" => EdgeCurve::Line,
567 "circle" => {
568 let p = params.ok_or_else(|| WasmError::InvalidInput {
569 reason: format!("edge {id}: circle missing curveParams"),
570 })?;
571 let center_arr =
572 p.get("center").and_then(|v| v.as_array()).ok_or_else(|| {
573 WasmError::InvalidInput {
574 reason: format!("edge {id}: circle missing center"),
575 }
576 })?;
577 let axis_arr = p.get("axis").and_then(|v| v.as_array()).ok_or_else(|| {
578 WasmError::InvalidInput {
579 reason: format!("edge {id}: circle missing axis"),
580 }
581 })?;
582 let radius = p.get("radius").and_then(|v| v.as_f64()).ok_or_else(|| {
583 WasmError::InvalidInput {
584 reason: format!("edge {id}: circle missing radius"),
585 }
586 })?;
587 let center = Self::parse_point3(center_arr, "center")?;
588 let axis = Self::parse_vec3(axis_arr, "axis")?;
589 let circle = if let Some(x_axis_arr) = p.get("xAxis").and_then(|v| v.as_array())
591 {
592 let x_axis = Self::parse_vec3(x_axis_arr, "xAxis")?;
593 let v_axis = axis.cross(x_axis);
594 Circle3D::with_axes(center, axis, radius, x_axis, v_axis)?
595 } else {
596 Circle3D::new(center, axis, radius)?
597 };
598 EdgeCurve::Circle(circle)
599 }
600 "ellipse" => {
601 let p = params.ok_or_else(|| WasmError::InvalidInput {
602 reason: format!("edge {id}: ellipse missing curveParams"),
603 })?;
604 let center_arr =
605 p.get("center").and_then(|v| v.as_array()).ok_or_else(|| {
606 WasmError::InvalidInput {
607 reason: format!("edge {id}: ellipse missing center"),
608 }
609 })?;
610 let axis_arr = p.get("axis").and_then(|v| v.as_array()).ok_or_else(|| {
611 WasmError::InvalidInput {
612 reason: format!("edge {id}: ellipse missing axis"),
613 }
614 })?;
615 let major_r =
616 p.get("majorRadius")
617 .and_then(|v| v.as_f64())
618 .ok_or_else(|| WasmError::InvalidInput {
619 reason: format!("edge {id}: ellipse missing majorRadius"),
620 })?;
621 let minor_r =
622 p.get("minorRadius")
623 .and_then(|v| v.as_f64())
624 .ok_or_else(|| WasmError::InvalidInput {
625 reason: format!("edge {id}: ellipse missing minorRadius"),
626 })?;
627 let center = Self::parse_point3(center_arr, "center")?;
628 let axis = Self::parse_vec3(axis_arr, "axis")?;
629 let ellipse =
631 if let Some(major_arr) = p.get("majorAxis").and_then(|v| v.as_array()) {
632 let u_axis = Self::parse_vec3(major_arr, "majorAxis")?;
633 let v_axis = axis.cross(u_axis);
634 Ellipse3D::with_axes(center, axis, major_r, minor_r, u_axis, v_axis)?
635 } else {
636 Ellipse3D::new(center, axis, major_r, minor_r)?
637 };
638 EdgeCurve::Ellipse(ellipse)
639 }
640 "nurbs" => {
641 let p = params.ok_or_else(|| WasmError::InvalidInput {
642 reason: format!("edge {id}: nurbs missing curveParams"),
643 })?;
644 let degree = p.get("degree").and_then(|v| v.as_u64()).ok_or_else(|| {
645 WasmError::InvalidInput {
646 reason: format!("edge {id}: nurbs missing degree"),
647 }
648 })? as usize;
649 let knots_arr = p.get("knots").and_then(|v| v.as_array()).ok_or_else(|| {
650 WasmError::InvalidInput {
651 reason: format!("edge {id}: nurbs missing knots"),
652 }
653 })?;
654 let knots: Vec<f64> = knots_arr
655 .iter()
656 .enumerate()
657 .map(|(i, v)| {
658 v.as_f64().ok_or_else(|| WasmError::InvalidInput {
659 reason: format!("edge {id}: knot[{i}] is not a number"),
660 })
661 })
662 .collect::<Result<_, _>>()?;
663 let weights_arr =
664 p.get("weights").and_then(|v| v.as_array()).ok_or_else(|| {
665 WasmError::InvalidInput {
666 reason: format!("edge {id}: nurbs missing weights"),
667 }
668 })?;
669 let weights: Vec<f64> = weights_arr
670 .iter()
671 .enumerate()
672 .map(|(i, v)| {
673 v.as_f64().ok_or_else(|| WasmError::InvalidInput {
674 reason: format!("edge {id}: weight[{i}] is not a number"),
675 })
676 })
677 .collect::<Result<_, _>>()?;
678 let cps_arr = p
679 .get("controlPoints")
680 .and_then(|v| v.as_array())
681 .ok_or_else(|| WasmError::InvalidInput {
682 reason: format!("edge {id}: nurbs missing controlPoints"),
683 })?;
684 let control_points: Vec<Point3> = cps_arr
685 .iter()
686 .enumerate()
687 .map(|(i, cp)| -> Result<Point3, WasmError> {
688 let arr = cp.as_array().ok_or_else(|| WasmError::InvalidInput {
689 reason: format!("edge {id}: controlPoints[{i}] is not an array"),
690 })?;
691 Self::parse_point3(arr, &format!("controlPoints[{i}]"))
692 })
693 .collect::<Result<_, _>>()?;
694 let nc = NurbsCurve::new(degree, knots, control_points, weights)?;
695 EdgeCurve::NurbsCurve(nc)
696 }
697 other => {
698 log::warn!(
699 "fromBREP: edge {id} has unsupported curve type '{other}', \
700 approximating as line"
701 );
702 EdgeCurve::Line
703 }
704 };
705
706 let eid = self
707 .topo_mut()
708 .add_edge(Edge::new(start_vid, end_vid, curve));
709 edge_map.insert(id, eid);
710 }
711
712 let faces = parsed["faces"]
714 .as_array()
715 .ok_or_else(|| WasmError::InvalidInput {
716 reason: "missing faces array".into(),
717 })?;
718 let mut face_ids: Vec<brepkit_topology::face::FaceId> = Vec::new();
719
720 for f in faces {
721 let outer_edge_ids =
722 f["outerWireEdges"]
723 .as_array()
724 .ok_or_else(|| WasmError::InvalidInput {
725 reason: "face missing outerWireEdges".into(),
726 })?;
727 let outer_orientations = f
728 .get("outerWireOrientations")
729 .and_then(|v| v.as_array().cloned());
730
731 let mut oriented_edges = Vec::new();
733 for (i, eid_val) in outer_edge_ids.iter().enumerate() {
734 let eid = eid_val.as_u64().ok_or_else(|| WasmError::InvalidInput {
735 reason: "invalid edge id in outerWireEdges".into(),
736 })? as u32;
737 let edge_id = *edge_map.get(&eid).ok_or_else(|| WasmError::InvalidInput {
738 reason: format!("wire references unknown edge {eid}"),
739 })?;
740 let forward = outer_orientations
741 .as_ref()
742 .and_then(|arr| arr.get(i))
743 .and_then(|v| v.as_bool())
744 .unwrap_or(true);
745 oriented_edges.push(OrientedEdge::new(edge_id, forward));
746 }
747
748 let wire = Wire::new(oriented_edges, true)?;
749 let wire_id = self.topo_mut().add_wire(wire);
750
751 let surface_type = f["surfaceType"].as_str().unwrap_or("plane");
753 let reversed = f["reversed"].as_bool().unwrap_or(false);
754
755 let surface_params = f.get("surfaceParams").and_then(|p| p.as_object());
756 let surface = match surface_type {
757 "plane" => {
758 if let Some(params) = surface_params {
759 if let (Some(normal_arr), Some(d)) = (
760 params.get("normal").and_then(|n| n.as_array()),
761 params.get("d").and_then(|d| d.as_f64()),
762 ) {
763 let nx = normal_arr.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
764 let ny = normal_arr.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0);
765 let nz = normal_arr.get(2).and_then(|v| v.as_f64()).unwrap_or(1.0);
766 FaceSurface::Plane {
767 normal: Vec3::new(nx, ny, nz),
768 d,
769 }
770 } else {
771 self.compute_plane_from_wire(wire_id)?
772 }
773 } else {
774 self.compute_plane_from_wire(wire_id)?
775 }
776 }
777 "cylinder" => {
778 let p = surface_params.ok_or_else(|| WasmError::InvalidInput {
779 reason: "cylinder face missing surfaceParams".into(),
780 })?;
781 let origin_arr =
782 p.get("origin").and_then(|v| v.as_array()).ok_or_else(|| {
783 WasmError::InvalidInput {
784 reason: "cylinder missing origin".into(),
785 }
786 })?;
787 let axis_arr = p.get("axis").and_then(|v| v.as_array()).ok_or_else(|| {
788 WasmError::InvalidInput {
789 reason: "cylinder missing axis".into(),
790 }
791 })?;
792 let radius = p.get("radius").and_then(|v| v.as_f64()).ok_or_else(|| {
793 WasmError::InvalidInput {
794 reason: "cylinder missing radius".into(),
795 }
796 })?;
797 let origin = Self::parse_point3(origin_arr, "origin")?;
798 let axis = Self::parse_vec3(axis_arr, "axis")?;
799 let cyl = if let Some(ref_arr) = p.get("refDir").and_then(|v| v.as_array()) {
800 let ref_dir = Self::parse_vec3(ref_arr, "refDir")?;
801 CylindricalSurface::with_ref_dir(origin, axis, radius, ref_dir)?
802 } else {
803 CylindricalSurface::new(origin, axis, radius)?
804 };
805 FaceSurface::Cylinder(cyl)
806 }
807 "cone" => {
808 let p = surface_params.ok_or_else(|| WasmError::InvalidInput {
809 reason: "cone face missing surfaceParams".into(),
810 })?;
811 let apex_arr = p.get("apex").and_then(|v| v.as_array()).ok_or_else(|| {
812 WasmError::InvalidInput {
813 reason: "cone missing apex".into(),
814 }
815 })?;
816 let axis_arr = p.get("axis").and_then(|v| v.as_array()).ok_or_else(|| {
817 WasmError::InvalidInput {
818 reason: "cone missing axis".into(),
819 }
820 })?;
821 let half_angle =
822 p.get("halfAngle").and_then(|v| v.as_f64()).ok_or_else(|| {
823 WasmError::InvalidInput {
824 reason: "cone missing halfAngle".into(),
825 }
826 })?;
827 let apex = Self::parse_point3(apex_arr, "apex")?;
828 let axis = Self::parse_vec3(axis_arr, "axis")?;
829 let cone = if let Some(ref_arr) = p.get("refDir").and_then(|v| v.as_array()) {
830 let ref_dir = Self::parse_vec3(ref_arr, "refDir")?;
831 ConicalSurface::with_ref_dir(apex, axis, half_angle, ref_dir)?
832 } else {
833 ConicalSurface::new(apex, axis, half_angle)?
834 };
835 FaceSurface::Cone(cone)
836 }
837 "sphere" => {
838 let p = surface_params.ok_or_else(|| WasmError::InvalidInput {
839 reason: "sphere face missing surfaceParams".into(),
840 })?;
841 let center_arr =
842 p.get("center").and_then(|v| v.as_array()).ok_or_else(|| {
843 WasmError::InvalidInput {
844 reason: "sphere missing center".into(),
845 }
846 })?;
847 let radius = p.get("radius").and_then(|v| v.as_f64()).ok_or_else(|| {
848 WasmError::InvalidInput {
849 reason: "sphere missing radius".into(),
850 }
851 })?;
852 let center = Self::parse_point3(center_arr, "center")?;
853 let sphere = SphericalSurface::new(center, radius)?;
854 FaceSurface::Sphere(sphere)
855 }
856 "torus" => {
857 let p = surface_params.ok_or_else(|| WasmError::InvalidInput {
858 reason: "torus face missing surfaceParams".into(),
859 })?;
860 let center_arr =
861 p.get("center").and_then(|v| v.as_array()).ok_or_else(|| {
862 WasmError::InvalidInput {
863 reason: "torus missing center".into(),
864 }
865 })?;
866 let axis_arr = p.get("axis").and_then(|v| v.as_array()).ok_or_else(|| {
867 WasmError::InvalidInput {
868 reason: "torus missing axis".into(),
869 }
870 })?;
871 let major_r =
872 p.get("majorRadius")
873 .and_then(|v| v.as_f64())
874 .ok_or_else(|| WasmError::InvalidInput {
875 reason: "torus missing majorRadius".into(),
876 })?;
877 let minor_r =
878 p.get("minorRadius")
879 .and_then(|v| v.as_f64())
880 .ok_or_else(|| WasmError::InvalidInput {
881 reason: "torus missing minorRadius".into(),
882 })?;
883 let center = Self::parse_point3(center_arr, "center")?;
884 let axis = Self::parse_vec3(axis_arr, "axis")?;
885 let torus = ToroidalSurface::with_axis(center, major_r, minor_r, axis)?;
886 FaceSurface::Torus(torus)
887 }
888 "nurbs" => {
889 let p = surface_params.ok_or_else(|| WasmError::InvalidInput {
890 reason: "nurbs face missing surfaceParams".into(),
891 })?;
892 let degree_u = p.get("degreeU").and_then(|v| v.as_u64()).ok_or_else(|| {
893 WasmError::InvalidInput {
894 reason: "nurbs surface missing degreeU".into(),
895 }
896 })? as usize;
897 let degree_v = p.get("degreeV").and_then(|v| v.as_u64()).ok_or_else(|| {
898 WasmError::InvalidInput {
899 reason: "nurbs surface missing degreeV".into(),
900 }
901 })? as usize;
902 let knots_u_arr =
903 p.get("knotsU").and_then(|v| v.as_array()).ok_or_else(|| {
904 WasmError::InvalidInput {
905 reason: "nurbs surface missing knotsU".into(),
906 }
907 })?;
908 let knots_u: Vec<f64> = knots_u_arr
909 .iter()
910 .enumerate()
911 .map(|(i, v)| {
912 v.as_f64().ok_or_else(|| WasmError::InvalidInput {
913 reason: format!("nurbs surface knotsU[{i}] is not a number"),
914 })
915 })
916 .collect::<Result<_, _>>()?;
917 let knots_v_arr =
918 p.get("knotsV").and_then(|v| v.as_array()).ok_or_else(|| {
919 WasmError::InvalidInput {
920 reason: "nurbs surface missing knotsV".into(),
921 }
922 })?;
923 let knots_v: Vec<f64> = knots_v_arr
924 .iter()
925 .enumerate()
926 .map(|(i, v)| {
927 v.as_f64().ok_or_else(|| WasmError::InvalidInput {
928 reason: format!("nurbs surface knotsV[{i}] is not a number"),
929 })
930 })
931 .collect::<Result<_, _>>()?;
932 let cps_grid = p
933 .get("controlPoints")
934 .and_then(|v| v.as_array())
935 .ok_or_else(|| WasmError::InvalidInput {
936 reason: "nurbs surface missing controlPoints".into(),
937 })?;
938 let control_points: Vec<Vec<Point3>> = cps_grid
939 .iter()
940 .enumerate()
941 .map(|(ri, row)| -> Result<Vec<Point3>, WasmError> {
942 let row_arr =
943 row.as_array().ok_or_else(|| WasmError::InvalidInput {
944 reason: format!("nurbs controlPoints[{ri}] is not an array"),
945 })?;
946 row_arr
947 .iter()
948 .enumerate()
949 .map(|(ci, cp)| -> Result<Point3, WasmError> {
950 let arr =
951 cp.as_array().ok_or_else(|| WasmError::InvalidInput {
952 reason: format!(
953 "nurbs controlPoints[{ri}][{ci}] is not an array"
954 ),
955 })?;
956 Self::parse_point3(arr, &format!("controlPoints[{ri}][{ci}]"))
957 })
958 .collect()
959 })
960 .collect::<Result<_, _>>()?;
961 let weights_grid =
962 p.get("weights").and_then(|v| v.as_array()).ok_or_else(|| {
963 WasmError::InvalidInput {
964 reason: "nurbs surface missing weights".into(),
965 }
966 })?;
967 let weights: Vec<Vec<f64>> = weights_grid
968 .iter()
969 .enumerate()
970 .map(|(ri, row)| -> Result<Vec<f64>, WasmError> {
971 let row_arr =
972 row.as_array().ok_or_else(|| WasmError::InvalidInput {
973 reason: format!("nurbs weights[{ri}] is not an array"),
974 })?;
975 row_arr
976 .iter()
977 .enumerate()
978 .map(|(ci, v)| {
979 v.as_f64().ok_or_else(|| WasmError::InvalidInput {
980 reason: format!(
981 "nurbs surface weights[{ri}][{ci}] is not a number"
982 ),
983 })
984 })
985 .collect::<Result<_, _>>()
986 })
987 .collect::<Result<_, _>>()?;
988 let ns = NurbsSurface::new(
989 degree_u,
990 degree_v,
991 knots_u,
992 knots_v,
993 control_points,
994 weights,
995 )?;
996 FaceSurface::Nurbs(ns)
997 }
998 other => {
999 log::warn!(
1000 "fromBREP: face has unsupported surface type '{other}', \
1001 approximating as plane from vertices"
1002 );
1003 self.compute_plane_from_wire(wire_id)?
1004 }
1005 };
1006
1007 let mut inner_wire_ids = Vec::new();
1009 if let Some(inner_wires) = f["innerWires"].as_array() {
1010 for iw in inner_wires {
1011 let (edge_arr, orient_arr) = if let Some(obj) = iw.as_object() {
1014 let e = obj
1015 .get("edges")
1016 .and_then(|v| v.as_array())
1017 .cloned()
1018 .unwrap_or_default();
1019 let o = obj.get("orientations").and_then(|v| v.as_array()).cloned();
1020 (e, o)
1021 } else if let Some(arr) = iw.as_array() {
1022 (arr.clone(), None)
1023 } else {
1024 continue;
1025 };
1026
1027 let mut inner_oriented = Vec::new();
1028 for (i, eid_val) in edge_arr.iter().enumerate() {
1029 if let Some(eid) = eid_val.as_u64()
1030 && let Some(&edge_id) = edge_map.get(&(eid as u32))
1031 {
1032 let fwd = orient_arr
1033 .as_ref()
1034 .and_then(|arr| arr.get(i))
1035 .and_then(|v| v.as_bool())
1036 .unwrap_or(true);
1037 inner_oriented.push(OrientedEdge::new(edge_id, fwd));
1038 }
1039 }
1040 if !inner_oriented.is_empty() {
1041 let inner_wire = Wire::new(inner_oriented, true)?;
1042 inner_wire_ids.push(self.topo_mut().add_wire(inner_wire));
1043 }
1044 }
1045 }
1046
1047 let mut face = Face::new(wire_id, inner_wire_ids, surface);
1048 if reversed {
1049 face.set_reversed(true);
1050 }
1051
1052 face_ids.push(self.topo_mut().add_face(face));
1053 }
1054
1055 if face_ids.is_empty() {
1057 return Err(WasmError::InvalidInput {
1058 reason: "fromBREP: no faces reconstructed".into(),
1059 });
1060 }
1061
1062 let shell = brepkit_topology::shell::Shell::new(face_ids)?;
1063 let shell_id = self.topo_mut().add_shell(shell);
1064 let solid = brepkit_topology::solid::Solid::new(shell_id, vec![]);
1065 let solid_id = self.topo_mut().add_solid(solid);
1066
1067 Ok(solid_id_to_u32(solid_id))
1068 }
1069}
1070
1071#[cfg(test)]
1074pub(crate) mod test_fixtures {
1075 #![allow(clippy::unwrap_used, dead_code)]
1076 use super::*;
1077
1078 pub fn kernel_with_box() -> (BrepKernel, u32) {
1079 let mut k = BrepKernel::new();
1080 let id = brepkit_operations::primitives::make_box(k.topo_mut(), 1.0, 1.0, 1.0).unwrap();
1081 #[allow(clippy::cast_possible_truncation)]
1082 (k, id.index() as u32)
1083 }
1084
1085 pub fn kernel_with_two_boxes() -> (BrepKernel, u32, u32) {
1086 let mut k = BrepKernel::new();
1087 let a = brepkit_operations::primitives::make_box(k.topo_mut(), 2.0, 2.0, 2.0).unwrap();
1088 let b = brepkit_operations::primitives::make_box(k.topo_mut(), 1.0, 1.0, 1.0).unwrap();
1089 #[allow(clippy::cast_possible_truncation)]
1090 (k, a.index() as u32, b.index() as u32)
1091 }
1092
1093 pub fn kernel_with_cylinder() -> (BrepKernel, u32) {
1094 let mut k = BrepKernel::new();
1095 let id = brepkit_operations::primitives::make_cylinder(k.topo_mut(), 1.0, 2.0).unwrap();
1096 #[allow(clippy::cast_possible_truncation)]
1097 (k, id.index() as u32)
1098 }
1099}
1100
1101#[cfg(test)]
1104mod batch_tests {
1105 #![allow(clippy::unwrap_used, clippy::expect_used)]
1106
1107 use super::*;
1108
1109 #[test]
1110 fn batch_single_op() {
1111 let mut kernel = BrepKernel::new();
1112 let result = kernel
1113 .execute_batch(r#"[{"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}}]"#);
1114 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1115 assert!(
1116 parsed[0]["ok"].is_number(),
1117 "expected ok number, got {parsed}"
1118 );
1119 }
1120
1121 #[test]
1122 fn batch_multiple_ops() {
1123 let mut kernel = BrepKernel::new();
1124 let result = kernel.execute_batch(
1125 r#"[
1126 {"op": "makeBox", "args": {"width": 2, "height": 2, "depth": 2}},
1127 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
1128 {"op": "volume", "args": {"solid": 0}}
1129 ]"#,
1130 );
1131 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1132 assert_eq!(parsed.as_array().unwrap().len(), 3);
1133 assert!(parsed[0]["ok"].is_number());
1134 assert!(parsed[1]["ok"].is_number());
1135 assert!(parsed[2]["ok"].is_number());
1136 }
1137
1138 #[test]
1139 fn batch_error_doesnt_stop_rest() {
1140 let mut kernel = BrepKernel::new();
1141 let result = kernel.execute_batch(
1142 r#"[
1143 {"op": "unknownOp", "args": {}},
1144 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}}
1145 ]"#,
1146 );
1147 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1148 assert!(parsed[0]["error"].is_string());
1149 assert!(parsed[1]["ok"].is_number());
1150 }
1151
1152 #[test]
1153 fn batch_invalid_json() {
1154 let mut kernel = BrepKernel::new();
1155 let result = kernel.execute_batch("not valid json");
1156 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1157 assert!(
1158 parsed[0]["error"]
1159 .as_str()
1160 .unwrap()
1161 .contains("invalid JSON")
1162 );
1163 }
1164
1165 #[test]
1166 fn batch_missing_op_field() {
1167 let mut kernel = BrepKernel::new();
1168 let result = kernel.execute_batch(r#"[{"args": {"width": 1}}]"#);
1169 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1170 assert!(parsed[0]["error"].as_str().unwrap().contains("op"));
1171 }
1172
1173 #[test]
1174 fn batch_boolean_ops() {
1175 let mut kernel = BrepKernel::new();
1176 let result = kernel.execute_batch(
1177 r#"[
1178 {"op": "makeBox", "args": {"width": 2, "height": 2, "depth": 2}},
1179 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
1180 {"op": "fuse", "args": {"solidA": 0, "solidB": 1}}
1181 ]"#,
1182 );
1183 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1184 assert!(parsed[0]["ok"].is_number());
1185 assert!(parsed[1]["ok"].is_number());
1186 assert!(parsed[2]["ok"].is_number());
1187 }
1188
1189 #[test]
1190 fn batch_bounding_box() {
1191 let mut kernel = BrepKernel::new();
1192 let result = kernel.execute_batch(
1193 r#"[
1194 {"op": "makeBox", "args": {"width": 2, "height": 4, "depth": 6}},
1195 {"op": "boundingBox", "args": {"solid": 0}}
1196 ]"#,
1197 );
1198 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1199 assert!(parsed[0]["ok"].is_number());
1200 let bbox = &parsed[1]["ok"];
1201 assert!(bbox.is_array());
1202 assert_eq!(bbox.as_array().unwrap().len(), 6);
1203 }
1204
1205 #[test]
1206 fn batch_copy_solid() {
1207 let mut kernel = BrepKernel::new();
1208 let result = kernel.execute_batch(
1209 r#"[
1210 {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
1211 {"op": "copySolid", "args": {"solid": 0}}
1212 ]"#,
1213 );
1214 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
1215 assert!(parsed[0]["ok"].is_number());
1216 assert!(parsed[1]["ok"].is_number());
1217 assert_ne!(parsed[0]["ok"].as_u64(), parsed[1]["ok"].as_u64());
1218 }
1219}
1220
1221#[cfg(test)]
1222mod tangent_arc_tests {
1223 #![allow(clippy::unwrap_used, clippy::expect_used)]
1224 use super::*;
1225
1226 fn get_edge(k: &BrepKernel, handle: u32) -> &Edge {
1227 let id = k.resolve_edge(handle).unwrap();
1228 k.topo.edge(id).unwrap()
1229 }
1230
1231 #[test]
1232 fn semicircle() {
1233 let mut k = BrepKernel::new();
1234 let eid = k
1235 .make_tangent_arc_3d_impl(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0)
1236 .unwrap();
1237 let edge = get_edge(&k, eid);
1238 assert!(matches!(edge.curve(), EdgeCurve::Circle(_)));
1239 if let EdgeCurve::Circle(c) = edge.curve() {
1240 assert!((c.radius() - 1.0).abs() < 1e-10);
1241 let center = c.center();
1242 assert!(center.x().abs() < 1e-10);
1243 assert!(center.y().abs() < 1e-10);
1244 assert!(center.z().abs() < 1e-10);
1245 }
1246 }
1247
1248 #[test]
1249 fn quarter_circle() {
1250 let mut k = BrepKernel::new();
1251 let eid = k
1252 .make_tangent_arc_3d_impl(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0)
1253 .unwrap();
1254 let edge = get_edge(&k, eid);
1255 assert!(matches!(edge.curve(), EdgeCurve::Circle(_)));
1256 if let EdgeCurve::Circle(c) = edge.curve() {
1257 assert!((c.radius() - 1.0).abs() < 1e-10);
1258 }
1259 let s = k.topo.vertex(edge.start()).unwrap().point();
1260 let e = k.topo.vertex(edge.end()).unwrap().point();
1261 assert!((s.x() - 1.0).abs() < 1e-10);
1262 assert!((e.y() - 1.0).abs() < 1e-10);
1263 }
1264
1265 #[test]
1266 fn tilted_3d_arc() {
1267 let mut k = BrepKernel::new();
1268 let eid = k
1269 .make_tangent_arc_3d_impl(1.0, 0.0, 1.0, 0.0, 1.0, 0.0, -1.0, 0.0, 1.0)
1270 .unwrap();
1271 let edge = get_edge(&k, eid);
1272 assert!(matches!(edge.curve(), EdgeCurve::Circle(_)));
1273 if let EdgeCurve::Circle(c) = edge.curve() {
1274 assert!((c.radius() - 1.0).abs() < 1e-10);
1275 }
1276 }
1277
1278 #[test]
1279 fn collinear_fallback() {
1280 let mut k = BrepKernel::new();
1281 let eid = k
1282 .make_tangent_arc_3d_impl(0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 5.0, 0.0, 0.0)
1283 .unwrap();
1284 assert!(matches!(get_edge(&k, eid).curve(), EdgeCurve::Line));
1285 }
1286
1287 #[test]
1288 fn large_arc_gt_pi() {
1289 let mut k = BrepKernel::new();
1290 let eid = k
1291 .make_tangent_arc_3d_impl(1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, 0.0)
1292 .unwrap();
1293 assert!(matches!(get_edge(&k, eid).curve(), EdgeCurve::Circle(_)));
1294 }
1295
1296 #[test]
1297 fn coincident_points_error() {
1298 let mut k = BrepKernel::new();
1299 let err = k
1300 .make_tangent_arc_3d_impl(1.0, 2.0, 3.0, 0.0, 1.0, 0.0, 1.0, 2.0, 3.0)
1301 .unwrap_err();
1302 assert!(err.to_string().contains("coincide"));
1303 }
1304
1305 #[test]
1306 fn zero_tangent_error() {
1307 let mut k = BrepKernel::new();
1308 let err = k
1309 .make_tangent_arc_3d_impl(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0)
1310 .unwrap_err();
1311 assert!(err.to_string().contains("tangent"));
1312 }
1313}
1314
1315#[cfg(test)]
1316mod lift_curve2d_tests {
1317 #![allow(clippy::unwrap_used, clippy::expect_used)]
1318 use super::*;
1319 use std::f64::consts::{FRAC_PI_2, PI, TAU};
1320
1321 #[test]
1322 fn line2d_on_xy_plane() {
1323 let mut k = BrepKernel::new();
1324 let eid = k
1325 .lift_curve2d_to_plane_impl(
1326 0,
1327 vec![1.0, 0.0, 1.0, 0.0],
1328 0.0,
1329 0.0,
1330 0.0,
1331 1.0,
1332 0.0,
1333 0.0,
1334 0.0,
1335 0.0,
1336 1.0,
1337 0.0,
1338 3.0,
1339 )
1340 .unwrap();
1341 let edge_id = k.resolve_edge(eid).unwrap();
1342 let edge = k.topo.edge(edge_id).unwrap();
1343 let s = k.topo.vertex(edge.start()).unwrap().point();
1344 let e = k.topo.vertex(edge.end()).unwrap().point();
1345 assert!((s.x() - 1.0).abs() < 1e-10);
1346 assert!(s.y().abs() < 1e-10);
1347 assert!((e.x() - 4.0).abs() < 1e-10);
1348 assert!(e.y().abs() < 1e-10);
1349 assert!(matches!(edge.curve(), EdgeCurve::Line));
1350 }
1351
1352 #[test]
1353 fn circle2d_quarter_arc_xy() {
1354 let mut k = BrepKernel::new();
1355 let eid = k
1356 .lift_curve2d_to_plane_impl(
1357 1,
1358 vec![0.0, 0.0, 1.0],
1359 0.0,
1360 0.0,
1361 0.0,
1362 1.0,
1363 0.0,
1364 0.0,
1365 0.0,
1366 0.0,
1367 1.0,
1368 0.0,
1369 FRAC_PI_2,
1370 )
1371 .unwrap();
1372 let edge_id = k.resolve_edge(eid).unwrap();
1373 let edge = k.topo.edge(edge_id).unwrap();
1374 let s = k.topo.vertex(edge.start()).unwrap().point();
1375 let e = k.topo.vertex(edge.end()).unwrap().point();
1376 assert!((s.x() - 1.0).abs() < 1e-10);
1377 assert!(s.y().abs() < 1e-10);
1378 assert!(e.x().abs() < 1e-10);
1379 assert!((e.y() - 1.0).abs() < 1e-10);
1380 assert!(matches!(edge.curve(), EdgeCurve::Circle(_)));
1381 }
1382
1383 #[test]
1384 fn circle2d_on_xz_plane() {
1385 let mut k = BrepKernel::new();
1386 let eid = k
1387 .lift_curve2d_to_plane_impl(
1388 1,
1389 vec![0.0, 0.0, 2.0],
1390 0.0,
1391 0.0,
1392 0.0,
1393 1.0,
1394 0.0,
1395 0.0,
1396 0.0,
1397 1.0,
1398 0.0,
1399 0.0,
1400 FRAC_PI_2,
1401 )
1402 .unwrap();
1403 let edge_id = k.resolve_edge(eid).unwrap();
1404 let edge = k.topo.edge(edge_id).unwrap();
1405 let s = k.topo.vertex(edge.start()).unwrap().point();
1406 let e = k.topo.vertex(edge.end()).unwrap().point();
1407 assert!((s.x() - 2.0).abs() < 1e-10);
1408 assert!(s.y().abs() < 1e-10);
1409 assert!(s.z().abs() < 1e-10);
1410 assert!(e.x().abs() < 1e-10);
1411 assert!(e.y().abs() < 1e-10);
1412 assert!((e.z() + 2.0).abs() < 1e-10);
1413 }
1414
1415 #[test]
1416 fn circle2d_full_circle() {
1417 let mut k = BrepKernel::new();
1418 let eid = k
1419 .lift_curve2d_to_plane_impl(
1420 1,
1421 vec![0.0, 0.0, 1.0],
1422 0.0,
1423 0.0,
1424 0.0,
1425 1.0,
1426 0.0,
1427 0.0,
1428 0.0,
1429 0.0,
1430 1.0,
1431 0.0,
1432 TAU,
1433 )
1434 .unwrap();
1435 let edge_id = k.resolve_edge(eid).unwrap();
1436 let edge = k.topo.edge(edge_id).unwrap();
1437 assert_eq!(edge.start(), edge.end());
1438 }
1439
1440 #[test]
1441 fn ellipse2d_with_rotation() {
1442 let mut k = BrepKernel::new();
1443 let eid = k
1444 .lift_curve2d_to_plane_impl(
1445 2,
1446 vec![0.0, 0.0, 2.0, 1.0, PI / 4.0],
1447 0.0,
1448 0.0,
1449 0.0,
1450 1.0,
1451 0.0,
1452 0.0,
1453 0.0,
1454 0.0,
1455 1.0,
1456 0.0,
1457 FRAC_PI_2,
1458 )
1459 .unwrap();
1460 let edge_id = k.resolve_edge(eid).unwrap();
1461 let edge = k.topo.edge(edge_id).unwrap();
1462 assert!(matches!(edge.curve(), EdgeCurve::Ellipse(_)));
1463 let s = k.topo.vertex(edge.start()).unwrap().point();
1464 let dist = (s.x().powi(2) + s.y().powi(2) + s.z().powi(2)).sqrt();
1465 assert!((dist - 2.0).abs() < 1e-10);
1466 }
1467
1468 #[test]
1469 fn ellipse2d_full() {
1470 let mut k = BrepKernel::new();
1471 let eid = k
1472 .lift_curve2d_to_plane_impl(
1473 2,
1474 vec![0.0, 0.0, 3.0, 1.0, 0.0],
1475 0.0,
1476 0.0,
1477 0.0,
1478 1.0,
1479 0.0,
1480 0.0,
1481 0.0,
1482 0.0,
1483 1.0,
1484 0.0,
1485 TAU,
1486 )
1487 .unwrap();
1488 let edge_id = k.resolve_edge(eid).unwrap();
1489 let edge = k.topo.edge(edge_id).unwrap();
1490 assert_eq!(edge.start(), edge.end());
1491 }
1492
1493 #[test]
1494 fn nurbs2d_degree1_line() {
1495 let mut k = BrepKernel::new();
1496 let eid = k
1497 .lift_curve2d_to_plane_impl(
1498 3,
1499 vec![1.0, 2.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 3.0, 4.0, 1.0, 1.0],
1500 0.0,
1501 0.0,
1502 0.0,
1503 1.0,
1504 0.0,
1505 0.0,
1506 0.0,
1507 0.0,
1508 1.0,
1509 0.0,
1510 1.0,
1511 )
1512 .unwrap();
1513 let edge_id = k.resolve_edge(eid).unwrap();
1514 let edge = k.topo.edge(edge_id).unwrap();
1515 let s = k.topo.vertex(edge.start()).unwrap().point();
1516 let e = k.topo.vertex(edge.end()).unwrap().point();
1517 assert!(s.x().abs() < 1e-10);
1518 assert!(s.y().abs() < 1e-10);
1519 assert!((e.x() - 3.0).abs() < 1e-10);
1520 assert!((e.y() - 4.0).abs() < 1e-10);
1521 assert!(matches!(edge.curve(), EdgeCurve::NurbsCurve(_)));
1522 }
1523
1524 #[test]
1525 fn invalid_curve_type() {
1526 let mut k = BrepKernel::new();
1527 let err = k
1528 .lift_curve2d_to_plane_impl(
1529 5,
1530 vec![],
1531 0.0,
1532 0.0,
1533 0.0,
1534 1.0,
1535 0.0,
1536 0.0,
1537 0.0,
1538 0.0,
1539 1.0,
1540 0.0,
1541 1.0,
1542 )
1543 .unwrap_err();
1544 assert!(err.to_string().contains("curve_type"));
1545 }
1546
1547 #[test]
1548 fn wrong_param_count() {
1549 let mut k = BrepKernel::new();
1550 let err = k
1551 .lift_curve2d_to_plane_impl(
1552 1,
1553 vec![0.0, 0.0],
1554 0.0,
1555 0.0,
1556 0.0,
1557 1.0,
1558 0.0,
1559 0.0,
1560 0.0,
1561 0.0,
1562 1.0,
1563 0.0,
1564 1.0,
1565 )
1566 .unwrap_err();
1567 assert!(err.to_string().contains("Circle expects 3 params"));
1568 }
1569}