Skip to main content

brepkit_wasm/bindings/
shapes.rs

1//! Shape creation bindings (vertices, edges, wires, faces, compounds).
2
3#![allow(clippy::missing_errors_doc, clippy::too_many_arguments)]
4
5use std::f64::consts::PI;
6
7use brepkit_math::nurbs::curve::NurbsCurve;
8use brepkit_math::vec::{Point3, Vec3};
9use brepkit_topology::edge::{Edge, EdgeCurve};
10use brepkit_topology::vertex::Vertex;
11use brepkit_topology::wire::{OrientedEdge, Wire};
12use wasm_bindgen::prelude::*;
13
14use crate::error::{WasmError, validate_finite, validate_positive};
15use crate::handles::{
16    edge_id_to_u32, face_id_to_u32, solid_id_to_u32, vertex_id_to_u32, wire_id_to_u32,
17};
18use crate::helpers::{TOL, parse_points};
19use crate::kernel::BrepKernel;
20
21#[wasm_bindgen]
22impl BrepKernel {
23    // ── Shape creation ─────────────────────────────────────────────
24
25    /// Create a rectangular face on the XY plane centered at the origin.
26    ///
27    /// Returns a face handle (`u32`).
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if `width` or `height` is non-positive, NaN,
32    /// or infinite, or if the face geometry cannot be constructed.
33    #[wasm_bindgen(js_name = "makeRectangle")]
34    pub fn make_rectangle(&mut self, width: f64, height: f64) -> Result<u32, JsError> {
35        validate_positive(width, "width")?;
36        validate_positive(height, "height")?;
37
38        let hw = width / 2.0;
39        let hh = height / 2.0;
40
41        let points = [
42            Point3::new(-hw, -hh, 0.0),
43            Point3::new(hw, -hh, 0.0),
44            Point3::new(hw, hh, 0.0),
45            Point3::new(-hw, hh, 0.0),
46        ];
47
48        let face_id = self.make_planar_face(&points)?;
49        Ok(face_id_to_u32(face_id))
50    }
51
52    /// Create a polygonal face from flat coordinate triples `[x,y,z, ...]`.
53    ///
54    /// Requires at least 3 points (9 `f64` values).
55    /// Returns a face handle (`u32`).
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if `coords` length is not a multiple of 3,
60    /// fewer than 3 points are provided, or the face normal is degenerate.
61    #[wasm_bindgen(js_name = "makePolygon")]
62    #[allow(clippy::needless_pass_by_value)] // wasm-bindgen requires owned Vec
63    pub fn make_polygon(&mut self, coords: Vec<f64>) -> Result<u32, JsError> {
64        if !coords.len().is_multiple_of(3) {
65            return Err(WasmError::InvalidInput {
66                reason: format!(
67                    "coordinate array length must be a multiple of 3, got {}",
68                    coords.len()
69                ),
70            }
71            .into());
72        }
73        let n = coords.len() / 3;
74        if n < 3 {
75            return Err(WasmError::InvalidInput {
76                reason: format!("polygon requires at least 3 points, got {n}"),
77            }
78            .into());
79        }
80
81        if let Some(pos) = coords.iter().position(|v| !v.is_finite()) {
82            return Err(WasmError::InvalidInput {
83                reason: format!("coordinate at index {pos} is not finite"),
84            }
85            .into());
86        }
87
88        let points: Vec<Point3> = coords
89            .chunks_exact(3)
90            .map(|c| Point3::new(c[0], c[1], c[2]))
91            .collect();
92
93        let face_id = self.make_planar_face(&points)?;
94        Ok(face_id_to_u32(face_id))
95    }
96
97    /// Create a circular polygon approximation on the XY plane.
98    ///
99    /// The circle is centered at the origin with the given `radius`,
100    /// approximated by `segments` straight edges.
101    /// Returns a face handle (`u32`).
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if fewer than 3 segments are specified.
106    #[wasm_bindgen(js_name = "makeCircle")]
107    pub fn make_circle(&mut self, radius: f64, segments: u32) -> Result<u32, JsError> {
108        validate_positive(radius, "radius")?;
109        if segments < 3 {
110            return Err(WasmError::InvalidInput {
111                reason: format!("circle requires at least 3 segments, got {segments}"),
112            }
113            .into());
114        }
115
116        let n = segments as usize;
117        let mut points = Vec::with_capacity(n);
118        for i in 0..n {
119            #[allow(clippy::cast_precision_loss)]
120            let angle = 2.0 * PI * (i as f64) / (n as f64);
121            points.push(Point3::new(radius * angle.cos(), radius * angle.sin(), 0.0));
122        }
123
124        let face_id = self.make_planar_face(&points)?;
125        Ok(face_id_to_u32(face_id))
126    }
127
128    // ── Shape construction (low-level) ────────────────────────────
129
130    /// Create a vertex at the given position.
131    ///
132    /// Returns a vertex handle (`u32`).
133    #[wasm_bindgen(js_name = "makeVertex")]
134    pub fn make_vertex(&mut self, x: f64, y: f64, z: f64) -> Result<u32, JsError> {
135        validate_finite(x, "x")?;
136        validate_finite(y, "y")?;
137        validate_finite(z, "z")?;
138        let id = self
139            .topo_mut()
140            .add_vertex(Vertex::new(Point3::new(x, y, z), TOL));
141        Ok(vertex_id_to_u32(id))
142    }
143
144    /// Create a straight-line edge between two points.
145    ///
146    /// Returns an edge handle (`u32`).
147    #[wasm_bindgen(js_name = "makeLineEdge")]
148    pub fn make_line_edge(
149        &mut self,
150        x1: f64,
151        y1: f64,
152        z1: f64,
153        x2: f64,
154        y2: f64,
155        z2: f64,
156    ) -> Result<u32, JsError> {
157        let start = Point3::new(x1, y1, z1);
158        let end = Point3::new(x2, y2, z2);
159        let eid = brepkit_topology::builder::make_line_edge(self.topo_mut(), start, end, TOL)?;
160        Ok(edge_id_to_u32(eid))
161    }
162
163    /// Create a closed circular edge with true `Circle` curve geometry.
164    ///
165    /// Unlike `makeCircle` (which returns a polygon face approximation),
166    /// this creates a single closed edge with an [`EdgeCurve::Circle`]
167    /// backing curve and parameter domain `[0, 2π]`. The start and end
168    /// vertex are shared at the seam point `circle.evaluate(0.0)`.
169    ///
170    /// Returns an edge handle (`u32`).
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if any coordinate is NaN/infinite, `radius` is
175    /// non-positive, or the normal vector is zero.
176    #[wasm_bindgen(js_name = "makeCircleEdge")]
177    pub fn make_circle_edge(
178        &mut self,
179        cx: f64,
180        cy: f64,
181        cz: f64,
182        nx: f64,
183        ny: f64,
184        nz: f64,
185        radius: f64,
186    ) -> Result<u32, JsError> {
187        validate_finite(cx, "cx")?;
188        validate_finite(cy, "cy")?;
189        validate_finite(cz, "cz")?;
190        validate_finite(nx, "nx")?;
191        validate_finite(ny, "ny")?;
192        validate_finite(nz, "nz")?;
193        validate_positive(radius, "radius")?;
194
195        let center = Point3::new(cx, cy, cz);
196        let normal = Vec3::new(nx, ny, nz);
197        normal.normalize().map_err(|e| WasmError::InvalidInput {
198            reason: format!("invalid normal: {e}"),
199        })?;
200        let eid = brepkit_topology::builder::make_circle_edge(
201            self.topo_mut(),
202            center,
203            normal,
204            radius,
205            TOL,
206        )?;
207        Ok(edge_id_to_u32(eid))
208    }
209
210    /// Create a closed elliptical edge with true `Ellipse` curve geometry.
211    ///
212    /// Creates a single closed edge with an [`EdgeCurve::Ellipse`] backing
213    /// curve and parameter domain `[0, 2π]`. The start and end vertex are
214    /// shared at the seam point `ellipse.evaluate(0.0)`.
215    ///
216    /// Returns an edge handle (`u32`).
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if any coordinate is NaN/infinite, either
221    /// semi-axis is non-positive, `semi_minor` exceeds `semi_major`, or
222    /// the normal vector is zero.
223    #[wasm_bindgen(js_name = "makeEllipseEdge")]
224    pub fn make_ellipse_edge(
225        &mut self,
226        cx: f64,
227        cy: f64,
228        cz: f64,
229        nx: f64,
230        ny: f64,
231        nz: f64,
232        semi_major: f64,
233        semi_minor: f64,
234    ) -> Result<u32, JsError> {
235        validate_finite(cx, "cx")?;
236        validate_finite(cy, "cy")?;
237        validate_finite(cz, "cz")?;
238        validate_finite(nx, "nx")?;
239        validate_finite(ny, "ny")?;
240        validate_finite(nz, "nz")?;
241        validate_positive(semi_major, "semi_major")?;
242        validate_positive(semi_minor, "semi_minor")?;
243        if semi_minor > semi_major {
244            return Err(WasmError::InvalidInput {
245                reason: format!(
246                    "semi_minor ({semi_minor}) must not exceed semi_major ({semi_major})"
247                ),
248            }
249            .into());
250        }
251
252        let center = Point3::new(cx, cy, cz);
253        let normal = Vec3::new(nx, ny, nz);
254        normal.normalize().map_err(|e| WasmError::InvalidInput {
255            reason: format!("invalid normal: {e}"),
256        })?;
257        let eid = brepkit_topology::builder::make_ellipse_edge(
258            self.topo_mut(),
259            center,
260            normal,
261            semi_major,
262            semi_minor,
263            TOL,
264        )?;
265        Ok(edge_id_to_u32(eid))
266    }
267
268    /// Create a closed circular edge with a caller-supplied reference x-direction.
269    ///
270    /// Like [`makeCircleEdge`](Self::make_circle_edge), but `ref_dir = (rx, ry, rz)`
271    /// is projected onto the plane perpendicular to the normal to fix the
272    /// circle's `u_axis` — which controls the seam vertex position at
273    /// `circle.evaluate(0.0)`. Use when downstream code (PCurve computation,
274    /// extrusion frame) depends on a specific seam placement.
275    ///
276    /// `ref_dir` must be non-zero (rejected at this boundary) and ideally
277    /// not parallel to the normal — `Frame3::from_normal_and_ref` falls
278    /// back to an arbitrary perpendicular when the projection of `ref_dir`
279    /// onto the plane is degenerate, defeating the purpose of this call.
280    ///
281    /// Returns an edge handle (`u32`).
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if any coordinate is NaN/infinite, `radius` is
286    /// non-positive, or the normal vector or `ref_dir` is zero.
287    #[wasm_bindgen(js_name = "makeCircleEdgeWithRef")]
288    pub fn make_circle_edge_with_ref(
289        &mut self,
290        cx: f64,
291        cy: f64,
292        cz: f64,
293        nx: f64,
294        ny: f64,
295        nz: f64,
296        radius: f64,
297        rx: f64,
298        ry: f64,
299        rz: f64,
300    ) -> Result<u32, JsError> {
301        validate_finite(cx, "cx")?;
302        validate_finite(cy, "cy")?;
303        validate_finite(cz, "cz")?;
304        validate_finite(nx, "nx")?;
305        validate_finite(ny, "ny")?;
306        validate_finite(nz, "nz")?;
307        validate_finite(rx, "rx")?;
308        validate_finite(ry, "ry")?;
309        validate_finite(rz, "rz")?;
310        validate_positive(radius, "radius")?;
311
312        let center = Point3::new(cx, cy, cz);
313        let normal = Vec3::new(nx, ny, nz);
314        normal.normalize().map_err(|e| WasmError::InvalidInput {
315            reason: format!("invalid normal: {e}"),
316        })?;
317        let ref_dir = Vec3::new(rx, ry, rz);
318        ref_dir.normalize().map_err(|e| WasmError::InvalidInput {
319            reason: format!("invalid ref_dir: {e}"),
320        })?;
321        let eid = brepkit_topology::builder::make_circle_edge_with_ref(
322            self.topo_mut(),
323            center,
324            normal,
325            radius,
326            ref_dir,
327            TOL,
328        )?;
329        Ok(edge_id_to_u32(eid))
330    }
331
332    /// Create a closed elliptical edge with a caller-supplied reference major-axis.
333    ///
334    /// Like [`makeEllipseEdge`](Self::make_ellipse_edge), but `ref_dir = (rx, ry, rz)`
335    /// is projected onto the plane perpendicular to the normal to fix the
336    /// ellipse's major-axis direction (`u_axis`, carrying `semi_major`).
337    /// Use this when the caller has an intended major-axis orientation —
338    /// otherwise the default-frame variant chooses an arbitrary
339    /// perpendicular, which can cause adapters to fall back to NURBS
340    /// approximations to preserve their requested orientation.
341    ///
342    /// `ref_dir` must be non-zero (rejected at this boundary) and ideally
343    /// not parallel to the normal — `Frame3::from_normal_and_ref` falls
344    /// back to an arbitrary perpendicular when the projection of `ref_dir`
345    /// onto the plane is degenerate, defeating the purpose of this call.
346    ///
347    /// Returns an edge handle (`u32`).
348    ///
349    /// # Errors
350    ///
351    /// Returns an error if any coordinate is NaN/infinite, either
352    /// semi-axis is non-positive, `semi_minor` exceeds `semi_major`, or
353    /// the normal vector or `ref_dir` is zero.
354    #[wasm_bindgen(js_name = "makeEllipseEdgeWithRef")]
355    pub fn make_ellipse_edge_with_ref(
356        &mut self,
357        cx: f64,
358        cy: f64,
359        cz: f64,
360        nx: f64,
361        ny: f64,
362        nz: f64,
363        semi_major: f64,
364        semi_minor: f64,
365        rx: f64,
366        ry: f64,
367        rz: f64,
368    ) -> Result<u32, JsError> {
369        validate_finite(cx, "cx")?;
370        validate_finite(cy, "cy")?;
371        validate_finite(cz, "cz")?;
372        validate_finite(nx, "nx")?;
373        validate_finite(ny, "ny")?;
374        validate_finite(nz, "nz")?;
375        validate_finite(rx, "rx")?;
376        validate_finite(ry, "ry")?;
377        validate_finite(rz, "rz")?;
378        validate_positive(semi_major, "semi_major")?;
379        validate_positive(semi_minor, "semi_minor")?;
380        if semi_minor > semi_major {
381            return Err(WasmError::InvalidInput {
382                reason: format!(
383                    "semi_minor ({semi_minor}) must not exceed semi_major ({semi_major})"
384                ),
385            }
386            .into());
387        }
388
389        let center = Point3::new(cx, cy, cz);
390        let normal = Vec3::new(nx, ny, nz);
391        normal.normalize().map_err(|e| WasmError::InvalidInput {
392            reason: format!("invalid normal: {e}"),
393        })?;
394        let ref_dir = Vec3::new(rx, ry, rz);
395        ref_dir.normalize().map_err(|e| WasmError::InvalidInput {
396            reason: format!("invalid ref_dir: {e}"),
397        })?;
398        let eid = brepkit_topology::builder::make_ellipse_edge_with_ref(
399            self.topo_mut(),
400            center,
401            normal,
402            semi_major,
403            semi_minor,
404            ref_dir,
405            TOL,
406        )?;
407        Ok(edge_id_to_u32(eid))
408    }
409
410    /// Create a circular arc edge between two points.
411    ///
412    /// The arc lies on a circle with the given center, normal axis, and
413    /// radius derived from `|start − center|`. The arc goes from start
414    /// to end counter-clockwise when viewed along the normal.
415    ///
416    /// Returns an edge handle (`u32`).
417    #[wasm_bindgen(js_name = "makeCircleArc3d")]
418    pub fn make_circle_arc_3d(
419        &mut self,
420        start_x: f64,
421        start_y: f64,
422        start_z: f64,
423        end_x: f64,
424        end_y: f64,
425        end_z: f64,
426        center_x: f64,
427        center_y: f64,
428        center_z: f64,
429        axis_x: f64,
430        axis_y: f64,
431        axis_z: f64,
432    ) -> Result<u32, JsError> {
433        let start_pt = Point3::new(start_x, start_y, start_z);
434        let end_pt = Point3::new(end_x, end_y, end_z);
435        let center = Point3::new(center_x, center_y, center_z);
436        let axis = Vec3::new(axis_x, axis_y, axis_z);
437
438        let n = axis.normalize().map_err(|e| WasmError::InvalidInput {
439            reason: format!("invalid axis: {e}"),
440        })?;
441
442        // u_axis = normalized(start − center), v_axis = n × u
443        let radial = start_pt - center;
444        let radius = radial.length();
445        if radius < 1e-12 {
446            return Err(WasmError::InvalidInput {
447                reason: "start point coincides with center".into(),
448            }
449            .into());
450        }
451        let u_axis = Vec3::new(
452            radial.x() / radius,
453            radial.y() / radius,
454            radial.z() / radius,
455        );
456        let v_axis = n.cross(u_axis);
457
458        let circle = brepkit_math::curves::Circle3D::with_axes(center, n, radius, u_axis, v_axis)
459            .map_err(|e| WasmError::InvalidInput {
460            reason: format!("invalid circle: {e}"),
461        })?;
462
463        let v_start = self.topo_mut().add_vertex(Vertex::new(start_pt, TOL));
464        let v_end = if (start_pt - end_pt).length() < TOL * 100.0 {
465            v_start
466        } else {
467            self.topo_mut().add_vertex(Vertex::new(end_pt, TOL))
468        };
469        let eid = self
470            .topo_mut()
471            .add_edge(Edge::new(v_start, v_end, EdgeCurve::Circle(circle)));
472        Ok(edge_id_to_u32(eid))
473    }
474
475    /// Create a trimmed elliptical arc edge.
476    ///
477    /// The ellipse is defined by `center`, `axis` (plane normal), the
478    /// `ref` major-axis direction, and `semi_major`/`semi_minor`. The
479    /// `start`/`end` points trim it to the CCW arc between them (they must
480    /// lie on the ellipse). Produces an `EdgeCurve::Ellipse` edge — not a
481    /// NURBS approximation — so it reports CIRCLE/ELLIPSE-class geometry.
482    ///
483    /// Returns an edge handle (`u32`).
484    ///
485    /// # Errors
486    ///
487    /// Returns an error if any coordinate is NaN/infinite, a semi-axis is
488    /// non-positive, `semi_minor` exceeds `semi_major`, or `axis`/`ref` is
489    /// a zero vector.
490    #[wasm_bindgen(js_name = "makeEllipseArc3d")]
491    #[allow(clippy::too_many_arguments)]
492    pub fn make_ellipse_arc_3d(
493        &mut self,
494        start_x: f64,
495        start_y: f64,
496        start_z: f64,
497        end_x: f64,
498        end_y: f64,
499        end_z: f64,
500        center_x: f64,
501        center_y: f64,
502        center_z: f64,
503        axis_x: f64,
504        axis_y: f64,
505        axis_z: f64,
506        ref_x: f64,
507        ref_y: f64,
508        ref_z: f64,
509        semi_major: f64,
510        semi_minor: f64,
511    ) -> Result<u32, JsError> {
512        for (v, name) in [
513            (start_x, "start_x"),
514            (start_y, "start_y"),
515            (start_z, "start_z"),
516            (end_x, "end_x"),
517            (end_y, "end_y"),
518            (end_z, "end_z"),
519            (center_x, "center_x"),
520            (center_y, "center_y"),
521            (center_z, "center_z"),
522            (axis_x, "axis_x"),
523            (axis_y, "axis_y"),
524            (axis_z, "axis_z"),
525            (ref_x, "ref_x"),
526            (ref_y, "ref_y"),
527            (ref_z, "ref_z"),
528        ] {
529            validate_finite(v, name)?;
530        }
531        validate_positive(semi_major, "semi_major")?;
532        validate_positive(semi_minor, "semi_minor")?;
533
534        let center = Point3::new(center_x, center_y, center_z);
535        let axis = Vec3::new(axis_x, axis_y, axis_z);
536        let ref_dir = Vec3::new(ref_x, ref_y, ref_z);
537        let start_pt = Point3::new(start_x, start_y, start_z);
538        let end_pt = Point3::new(end_x, end_y, end_z);
539
540        let eid = brepkit_topology::builder::make_ellipse_arc(
541            self.topo_mut(),
542            center,
543            axis,
544            semi_major,
545            semi_minor,
546            ref_dir,
547            start_pt,
548            end_pt,
549            TOL,
550        )?;
551        Ok(edge_id_to_u32(eid))
552    }
553
554    /// Create a NURBS curve edge.
555    ///
556    /// Returns an edge handle (`u32`).
557    #[wasm_bindgen(js_name = "makeNurbsEdge")]
558    #[allow(clippy::needless_pass_by_value)]
559    pub fn make_nurbs_edge(
560        &mut self,
561        start_x: f64,
562        start_y: f64,
563        start_z: f64,
564        end_x: f64,
565        end_y: f64,
566        end_z: f64,
567        degree: u32,
568        knots: Vec<f64>,
569        control_points: Vec<f64>,
570        weights: Vec<f64>,
571    ) -> Result<u32, JsError> {
572        if !control_points.len().is_multiple_of(3) {
573            return Err(WasmError::InvalidInput {
574                reason: format!(
575                    "control_points length must be a multiple of 3, got {}",
576                    control_points.len()
577                ),
578            }
579            .into());
580        }
581        let cp: Vec<Point3> = control_points
582            .chunks_exact(3)
583            .map(|c| Point3::new(c[0], c[1], c[2]))
584            .collect();
585        let curve = NurbsCurve::new(degree as usize, knots, cp, weights)?;
586
587        let start_pt = Point3::new(start_x, start_y, start_z);
588        let end_pt = Point3::new(end_x, end_y, end_z);
589        let v_start = self.topo_mut().add_vertex(Vertex::new(start_pt, TOL));
590        // When start ≈ end (closed curve), reuse the same vertex so
591        // downstream code correctly identifies the edge as closed.
592        let v_end = if (start_pt - end_pt).length() < TOL * 100.0 {
593            v_start
594        } else {
595            self.topo_mut().add_vertex(Vertex::new(end_pt, TOL))
596        };
597        let eid = self
598            .topo_mut()
599            .add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(curve)));
600        Ok(edge_id_to_u32(eid))
601    }
602
603    /// Create a circular arc edge defined by start point, tangent direction
604    /// at start, and end point.
605    ///
606    /// If the tangent is parallel to the start→end chord (collinear), falls
607    /// back to a straight line edge.
608    ///
609    /// Returns an edge handle (`u32`).
610    #[wasm_bindgen(js_name = "makeTangentArc3d")]
611    pub fn make_tangent_arc_3d(
612        &mut self,
613        start_x: f64,
614        start_y: f64,
615        start_z: f64,
616        tangent_x: f64,
617        tangent_y: f64,
618        tangent_z: f64,
619        end_x: f64,
620        end_y: f64,
621        end_z: f64,
622    ) -> Result<u32, JsError> {
623        Ok(self.make_tangent_arc_3d_impl(
624            start_x, start_y, start_z, tangent_x, tangent_y, tangent_z, end_x, end_y, end_z,
625        )?)
626    }
627    /// Lift a 2D curve onto a 3D plane, producing an edge.
628    ///
629    /// `curve_type`: 0 = Line, 1 = Circle, 2 = Ellipse, 3 = NURBS.
630    /// `curve_params` layout varies by type (see docs).
631    /// The plane is defined by an origin, x-axis, and normal.
632    /// `t_start`/`t_end` specify the parameter range on the 2D curve.
633    ///
634    /// Returns an edge handle (`u32`).
635    #[wasm_bindgen(js_name = "liftCurve2dToPlane")]
636    #[allow(
637        clippy::too_many_arguments,
638        clippy::too_many_lines,
639        clippy::needless_pass_by_value
640    )]
641    pub fn lift_curve2d_to_plane(
642        &mut self,
643        curve_type: u32,
644        curve_params: Vec<f64>,
645        origin_x: f64,
646        origin_y: f64,
647        origin_z: f64,
648        x_axis_x: f64,
649        x_axis_y: f64,
650        x_axis_z: f64,
651        normal_x: f64,
652        normal_y: f64,
653        normal_z: f64,
654        t_start: f64,
655        t_end: f64,
656    ) -> Result<u32, JsError> {
657        Ok(self.lift_curve2d_to_plane_impl(
658            curve_type,
659            curve_params,
660            origin_x,
661            origin_y,
662            origin_z,
663            x_axis_x,
664            x_axis_y,
665            x_axis_z,
666            normal_x,
667            normal_y,
668            normal_z,
669            t_start,
670            t_end,
671        )?)
672    }
673
674    /// Create a closed wire from an ordered array of edge handles.
675    ///
676    /// Returns a wire handle (`u32`).
677    #[wasm_bindgen(js_name = "makeWire")]
678    #[allow(clippy::needless_pass_by_value)]
679    pub fn make_wire(&mut self, edge_handles: Vec<u32>, closed: bool) -> Result<u32, JsError> {
680        let tol = brepkit_math::tolerance::Tolerance::new();
681
682        let edge_ids: Vec<brepkit_topology::edge::EdgeId> = edge_handles
683            .iter()
684            .map(|&h| self.resolve_edge(h))
685            .collect::<Result<_, WasmError>>()?;
686
687        // Merge coincident vertices between adjacent edges.
688        // When edge[i].end is at the same position as edge[i+1].start,
689        // replace edge[i+1].start with edge[i].end so they share a vertex.
690        if edge_ids.len() > 1 {
691            for i in 0..edge_ids.len() {
692                let next = if i + 1 < edge_ids.len() {
693                    i + 1
694                } else if closed {
695                    0 // wrap around for closed wires
696                } else {
697                    continue;
698                };
699                if next == i {
700                    continue; // single-edge closed wire
701                }
702
703                let end_vid = self.topo.edge(edge_ids[i])?.end();
704                let start_vid = self.topo.edge(edge_ids[next])?.start();
705
706                if end_vid == start_vid {
707                    continue; // already shared
708                }
709
710                let end_pos = self.topo.vertex(end_vid)?.point();
711                let start_pos = self.topo.vertex(start_vid)?.point();
712
713                let dist = (end_pos - start_pos).length();
714                if dist < tol.linear {
715                    // Merge: replace the next edge's start with the current edge's end
716                    self.topo_mut().edge_mut(edge_ids[next])?.set_start(end_vid);
717                }
718            }
719        }
720
721        let oriented: Vec<OrientedEdge> = edge_ids
722            .iter()
723            .map(|&eid| OrientedEdge::new(eid, true))
724            .collect();
725        let wire = Wire::new(oriented, closed)?;
726        let wid = self.topo_mut().add_wire(wire);
727        Ok(wire_id_to_u32(wid))
728    }
729
730    /// Create a face from a wire.
731    ///
732    /// Samples the wire's edges and attaches a planar surface only if the
733    /// geometry lies within tolerance of a single plane; otherwise a
734    /// non-planar surface is attached, so `getSurfaceType` never reports
735    /// `"plane"` for a non-coplanar wire.
736    ///
737    /// Returns a face handle (`u32`).
738    #[wasm_bindgen(js_name = "makeFaceFromWire")]
739    pub fn make_face_from_wire(&mut self, wire: u32) -> Result<u32, JsError> {
740        let wid = self.resolve_wire(wire)?;
741        let fid = brepkit_topology::builder::make_face_from_wire(self.topo_mut(), wid)?;
742        Ok(face_id_to_u32(fid))
743    }
744
745    /// Create a strictly planar face from a wire.
746    ///
747    /// Fails with a "wire is not planar" error if the wire's geometry does
748    /// not lie within tolerance of a single plane. Use this for planar-only
749    /// construction intent (probing whether a wire is planar).
750    ///
751    /// Returns a face handle (`u32`).
752    #[wasm_bindgen(js_name = "makePlanarFaceFromWire")]
753    pub fn make_planar_face_from_wire(&mut self, wire: u32) -> Result<u32, JsError> {
754        let wid = self.resolve_wire(wire)?;
755        let fid = brepkit_topology::builder::make_planar_face_from_wire(self.topo_mut(), wid)?;
756        Ok(face_id_to_u32(fid))
757    }
758
759    /// Create a solid from a shell.
760    ///
761    /// Returns a solid handle (`u32`).
762    #[wasm_bindgen(js_name = "solidFromShell")]
763    pub fn solid_from_shell(&mut self, shell: u32) -> Result<u32, JsError> {
764        let shell_id = self.resolve_shell(shell)?;
765        let solid = brepkit_topology::solid::Solid::new(shell_id, vec![]);
766        let sid = self.topo_mut().add_solid(solid);
767        Ok(solid_id_to_u32(sid))
768    }
769
770    /// Create a compound from multiple solid handles.
771    ///
772    /// Returns a compound handle (stored as `u32`).
773    #[wasm_bindgen(js_name = "makeCompound")]
774    #[allow(clippy::needless_pass_by_value)]
775    pub fn make_compound(&mut self, solid_handles: Vec<u32>) -> Result<u32, JsError> {
776        let solid_ids: Vec<brepkit_topology::solid::SolidId> = solid_handles
777            .iter()
778            .map(|&h| self.resolve_solid(h))
779            .collect::<Result<_, _>>()?;
780        let compound = brepkit_topology::compound::Compound::new(solid_ids);
781        #[allow(clippy::cast_possible_truncation)]
782        let cid = self.topo_mut().add_compound(compound);
783        Ok(cid.index() as u32)
784    }
785
786    /// Build a convex hull solid from a point cloud.
787    ///
788    /// Uses the Quickhull algorithm for 3D point sets.
789    ///
790    /// Returns a solid handle (`u32`).
791    ///
792    /// # Errors
793    ///
794    /// Returns an error if fewer than 4 non-coplanar points are provided.
795    #[wasm_bindgen(js_name = "convexHull")]
796    #[allow(clippy::needless_pass_by_value)]
797    pub fn convex_hull(&mut self, coords: Vec<f64>) -> Result<u32, JsError> {
798        if !coords.len().is_multiple_of(3) {
799            return Err(WasmError::InvalidInput {
800                reason: format!(
801                    "coordinate array length must be a multiple of 3, got {}",
802                    coords.len()
803                ),
804            }
805            .into());
806        }
807        let points: Vec<Point3> = coords
808            .chunks_exact(3)
809            .map(|c| Point3::new(c[0], c[1], c[2]))
810            .collect();
811        if points.len() < 4 {
812            return Err(WasmError::InvalidInput {
813                reason: format!(
814                    "convex hull requires at least 4 points, got {}",
815                    points.len()
816                ),
817            }
818            .into());
819        }
820
821        let solid_id = brepkit_operations::primitives::make_convex_hull(self.topo_mut(), &points)?;
822        Ok(solid_id_to_u32(solid_id))
823    }
824
825    /// Create a closed polygon wire from flat coordinates.
826    ///
827    /// Returns a wire handle.
828    #[wasm_bindgen(js_name = "makePolygonWire")]
829    #[allow(clippy::needless_pass_by_value)]
830    pub fn make_polygon_wire(&mut self, coords: Vec<f64>) -> Result<u32, JsError> {
831        let points = parse_points(&coords)?;
832        if points.len() < 3 {
833            return Err(WasmError::InvalidInput {
834                reason: format!("polygon wire needs at least 3 points, got {}", points.len()),
835            }
836            .into());
837        }
838        let n = points.len();
839        let verts: Vec<_> = points
840            .iter()
841            .map(|p| self.topo_mut().add_vertex(Vertex::new(*p, TOL)))
842            .collect();
843        let edges: Vec<_> = (0..n)
844            .map(|i| {
845                self.topo_mut()
846                    .add_edge(Edge::new(verts[i], verts[(i + 1) % n], EdgeCurve::Line))
847            })
848            .collect();
849        let oriented: Vec<_> = edges
850            .iter()
851            .map(|&eid| OrientedEdge::new(eid, true))
852            .collect();
853        let wire = Wire::new(oriented, true)?;
854        let wid = self.topo_mut().add_wire(wire);
855        Ok(wire_id_to_u32(wid))
856    }
857
858    /// Create a regular polygon wire on the XY plane.
859    ///
860    /// Returns a wire handle.
861    #[wasm_bindgen(js_name = "makeRegularPolygonWire")]
862    pub fn make_regular_polygon_wire(&mut self, radius: f64, n_sides: u32) -> Result<u32, JsError> {
863        validate_positive(radius, "radius")?;
864        if n_sides < 3 {
865            return Err(WasmError::InvalidInput {
866                reason: format!("polygon needs at least 3 sides, got {n_sides}"),
867            }
868            .into());
869        }
870        let wid = brepkit_topology::builder::make_regular_polygon_wire(
871            self.topo_mut(),
872            radius,
873            n_sides as usize,
874            TOL,
875        )?;
876        Ok(wire_id_to_u32(wid))
877    }
878
879    /// Create a circular face on the XY plane (using NURBS arcs).
880    ///
881    /// Returns a face handle.
882    #[wasm_bindgen(js_name = "makeCircleFace")]
883    pub fn make_circle_face(&mut self, radius: f64, segments: u32) -> Result<u32, JsError> {
884        validate_positive(radius, "radius")?;
885        if segments < 3 {
886            return Err(WasmError::InvalidInput {
887                reason: format!("circle face needs at least 3 segments, got {segments}"),
888            }
889            .into());
890        }
891        let fid = brepkit_topology::builder::make_circle_face(
892            self.topo_mut(),
893            radius,
894            segments as usize,
895            TOL,
896        )?;
897        Ok(face_id_to_u32(fid))
898    }
899}
900
901#[cfg(test)]
902mod tests {
903    #![allow(clippy::unwrap_used, clippy::expect_used)]
904
905    use super::*;
906    use brepkit_topology::face::FaceSurface;
907
908    // ── make_rectangle ────────────────────────────────────────────
909
910    #[test]
911    fn make_rectangle_returns_valid_face() {
912        let mut k = BrepKernel::new();
913        let h = k.make_rectangle(4.0, 2.0).unwrap();
914        let fid = k.resolve_face(h).unwrap();
915        let face = k.topo.face(fid).unwrap();
916        assert!(
917            matches!(face.surface(), FaceSurface::Plane { .. }),
918            "expected a Plane surface"
919        );
920    }
921
922    #[test]
923    fn make_rectangle_zero_width_is_error() {
924        use crate::error::validate_positive;
925        assert!(validate_positive(0.0, "width").is_err());
926    }
927
928    #[test]
929    fn make_rectangle_negative_height_is_error() {
930        use crate::error::validate_positive;
931        assert!(validate_positive(-3.0, "height").is_err());
932    }
933
934    #[test]
935    fn make_rectangle_nan_is_error() {
936        use crate::error::validate_positive;
937        assert!(validate_positive(f64::NAN, "width").is_err());
938    }
939
940    // ── make_polygon ──────────────────────────────────────────────
941
942    #[test]
943    fn make_polygon_triangle_returns_valid_face() {
944        let mut k = BrepKernel::new();
945        let coords = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
946        let h = k.make_polygon(coords).unwrap();
947        let fid = k.resolve_face(h).unwrap();
948        let face = k.topo.face(fid).unwrap();
949        assert!(matches!(face.surface(), FaceSurface::Plane { .. }));
950    }
951
952    #[test]
953    fn make_polygon_odd_length_coords_is_error() {
954        // 7 values — not a multiple of 3
955        assert_ne!(7 % 3, 0, "length 7 should fail the multiple-of-3 check");
956    }
957
958    #[test]
959    fn validate_positive_rejects_zero() {
960        assert!(crate::error::validate_positive(0.0, "x").is_err());
961    }
962
963    #[test]
964    fn validate_finite_rejects_nan() {
965        assert!(crate::error::validate_finite(f64::NAN, "x").is_err());
966    }
967
968    // ── make_vertex ───────────────────────────────────────────────
969
970    #[test]
971    fn make_vertex_stores_position() {
972        let mut k = BrepKernel::new();
973        let h = k.make_vertex(1.0, 2.0, 3.0).unwrap();
974        let vid = k.resolve_vertex(h).unwrap();
975        let v = k.topo.vertex(vid).unwrap();
976        let p = v.point();
977        assert!((p.x() - 1.0).abs() < 1e-10);
978        assert!((p.y() - 2.0).abs() < 1e-10);
979        assert!((p.z() - 3.0).abs() < 1e-10);
980    }
981
982    #[test]
983    fn validate_finite_rejects_infinity() {
984        assert!(crate::error::validate_finite(f64::INFINITY, "x").is_err());
985    }
986
987    // ── make_line_edge ────────────────────────────────────────────
988
989    #[test]
990    fn make_line_edge_creates_line_curve() {
991        let mut k = BrepKernel::new();
992        let h = k.make_line_edge(0.0, 0.0, 0.0, 1.0, 0.0, 0.0).unwrap();
993        let eid = k.resolve_edge(h).unwrap();
994        let edge = k.topo.edge(eid).unwrap();
995        assert!(
996            matches!(edge.curve(), EdgeCurve::Line),
997            "expected EdgeCurve::Line"
998        );
999    }
1000
1001    #[test]
1002    fn make_line_edge_endpoints_are_distinct_vertices() {
1003        let mut k = BrepKernel::new();
1004        let h = k.make_line_edge(0.0, 0.0, 0.0, 3.0, 4.0, 0.0).unwrap();
1005        let eid = k.resolve_edge(h).unwrap();
1006        let edge = k.topo.edge(eid).unwrap();
1007        assert_ne!(edge.start(), edge.end(), "start and end should differ");
1008    }
1009
1010    // ── make_wire ─────────────────────────────────────────────────
1011
1012    #[test]
1013    fn make_wire_from_three_edges_succeeds() {
1014        let mut k = BrepKernel::new();
1015        let e0 = k.make_line_edge(0.0, 0.0, 0.0, 1.0, 0.0, 0.0).unwrap();
1016        let e1 = k.make_line_edge(1.0, 0.0, 0.0, 1.0, 1.0, 0.0).unwrap();
1017        let e2 = k.make_line_edge(1.0, 1.0, 0.0, 0.0, 0.0, 0.0).unwrap();
1018        let wh = k.make_wire(vec![e0, e1, e2], true).unwrap();
1019        let wid = k.resolve_wire(wh).unwrap();
1020        let wire = k.topo.wire(wid).unwrap();
1021        assert_eq!(wire.edges().len(), 3);
1022        assert!(wire.is_closed());
1023    }
1024
1025    #[test]
1026    fn resolve_edge_invalid_handle_is_error() {
1027        let k = BrepKernel::new();
1028        assert!(k.resolve_edge(999).is_err());
1029    }
1030}