Skip to main content

brepkit_wasm/bindings/
primitives.rs

1//! Primitive solid creation bindings.
2
3#![allow(clippy::missing_errors_doc)]
4
5use wasm_bindgen::prelude::*;
6
7use brepkit_operations::transform::transform_solid;
8
9use crate::error::{validate_finite, validate_positive};
10use crate::handles::solid_id_to_u32;
11use crate::kernel::BrepKernel;
12
13#[wasm_bindgen]
14impl BrepKernel {
15    /// Create a box solid with the given dimensions, centered at the origin.
16    ///
17    /// Returns a solid handle (`u32`).
18    ///
19    /// # Errors
20    ///
21    /// Returns an error if any dimension is non-positive or non-finite.
22    #[wasm_bindgen(js_name = "makeBox")]
23    pub fn make_box_solid(&mut self, dx: f64, dy: f64, dz: f64) -> Result<u32, JsError> {
24        validate_positive(dx, "dx")?;
25        validate_positive(dy, "dy")?;
26        validate_positive(dz, "dz")?;
27        let solid_id = brepkit_operations::primitives::make_box(self.topo_mut(), dx, dy, dz)?;
28        Ok(solid_id_to_u32(solid_id))
29    }
30
31    /// Create a cylinder solid centered at the origin, axis along +Z.
32    ///
33    /// Returns a solid handle (`u32`).
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if radius or height is non-positive.
38    #[wasm_bindgen(js_name = "makeCylinder")]
39    pub fn make_cylinder_solid(&mut self, radius: f64, height: f64) -> Result<u32, JsError> {
40        validate_positive(radius, "radius")?;
41        validate_positive(height, "height")?;
42        let solid_id =
43            brepkit_operations::primitives::make_cylinder(self.topo_mut(), radius, height)?;
44        Ok(solid_id_to_u32(solid_id))
45    }
46
47    /// Create a sphere solid centered at the origin.
48    ///
49    /// Returns a solid handle (`u32`).
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if radius is non-positive or segments < 4.
54    #[wasm_bindgen(js_name = "makeSphere")]
55    pub fn make_sphere_solid(&mut self, radius: f64, segments: u32) -> Result<u32, JsError> {
56        validate_positive(radius, "radius")?;
57        let solid_id = brepkit_operations::primitives::make_sphere(
58            self.topo_mut(),
59            radius,
60            segments as usize,
61        )?;
62        Ok(solid_id_to_u32(solid_id))
63    }
64
65    /// Create a cone or frustum solid centered at the origin, axis along +Z.
66    ///
67    /// Returns a solid handle (`u32`).
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if height is non-positive or both radii are zero.
72    #[wasm_bindgen(js_name = "makeCone")]
73    pub fn make_cone_solid(
74        &mut self,
75        bottom_radius: f64,
76        top_radius: f64,
77        height: f64,
78    ) -> Result<u32, JsError> {
79        validate_finite(bottom_radius, "bottom_radius")?;
80        validate_finite(top_radius, "top_radius")?;
81        validate_positive(height, "height")?;
82        let solid_id = brepkit_operations::primitives::make_cone(
83            self.topo_mut(),
84            bottom_radius,
85            top_radius,
86            height,
87        )?;
88        Ok(solid_id_to_u32(solid_id))
89    }
90
91    /// Create a torus solid centered at the origin in the XY plane.
92    ///
93    /// Returns a solid handle (`u32`).
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if radii are non-positive or minor >= major.
98    #[wasm_bindgen(js_name = "makeTorus")]
99    pub fn make_torus_solid(
100        &mut self,
101        major_radius: f64,
102        minor_radius: f64,
103        segments: u32,
104    ) -> Result<u32, JsError> {
105        validate_positive(major_radius, "major_radius")?;
106        validate_positive(minor_radius, "minor_radius")?;
107        let solid_id = brepkit_operations::primitives::make_torus(
108            self.topo_mut(),
109            major_radius,
110            minor_radius,
111            segments as usize,
112        )?;
113        Ok(solid_id_to_u32(solid_id))
114    }
115
116    /// Create an ellipsoid solid centered at the origin.
117    ///
118    /// Built by creating a unit sphere and scaling it by `(rx, ry, rz)`.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if any radius is non-positive.
123    #[wasm_bindgen(js_name = "makeEllipsoid")]
124    pub fn make_ellipsoid_solid(&mut self, rx: f64, ry: f64, rz: f64) -> Result<u32, JsError> {
125        validate_positive(rx, "rx")?;
126        validate_positive(ry, "ry")?;
127        validate_positive(rz, "rz")?;
128        // Create a unit sphere, then scale it non-uniformly.
129        let solid_id = brepkit_operations::primitives::make_sphere(self.topo_mut(), 1.0, 16)?;
130        let mat = brepkit_math::mat::Mat4::scale(rx, ry, rz);
131        transform_solid(self.topo_mut(), solid_id, &mat)?;
132        Ok(solid_id_to_u32(solid_id))
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    #![allow(clippy::unwrap_used, clippy::expect_used)]
139
140    use crate::kernel::BrepKernel;
141
142    /// Helper: parse batch result and check a single op returned ok or error.
143    fn batch_has_ok(result: &str, idx: usize) -> bool {
144        let parsed: serde_json::Value = serde_json::from_str(result).unwrap();
145        parsed[idx]["ok"].is_number()
146    }
147
148    fn batch_has_error(result: &str, idx: usize) -> bool {
149        let parsed: serde_json::Value = serde_json::from_str(result).unwrap();
150        parsed[idx]["error"].is_string()
151    }
152
153    // ── make_box ────────────────────────────────────────────────────
154
155    #[test]
156    fn make_box_happy_path() {
157        let mut k = BrepKernel::new();
158        let r = k
159            .execute_batch(r#"[{"op": "makeBox", "args": {"width": 1, "height": 2, "depth": 3}}]"#);
160        assert!(batch_has_ok(&r, 0));
161    }
162
163    #[test]
164    fn make_box_negative_width() {
165        let mut k = BrepKernel::new();
166        let r = k.execute_batch(
167            r#"[{"op": "makeBox", "args": {"width": -1, "height": 2, "depth": 3}}]"#,
168        );
169        assert!(batch_has_error(&r, 0));
170    }
171
172    #[test]
173    fn make_box_zero_height() {
174        let mut k = BrepKernel::new();
175        let r = k
176            .execute_batch(r#"[{"op": "makeBox", "args": {"width": 1, "height": 0, "depth": 3}}]"#);
177        assert!(batch_has_error(&r, 0));
178    }
179
180    #[test]
181    fn make_box_volume() {
182        let mut k = BrepKernel::new();
183        let r = k.execute_batch(
184            r#"[
185                {"op": "makeBox", "args": {"width": 2, "height": 3, "depth": 4}},
186                {"op": "volume", "args": {"solid": 0}}
187            ]"#,
188        );
189        let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
190        let vol = parsed[1]["ok"].as_f64().unwrap();
191        assert!((vol - 24.0).abs() < 0.1, "expected ~24.0, got {vol}");
192    }
193
194    // ── make_cylinder ───────────────────────────────────────────────
195
196    #[test]
197    fn make_cylinder_happy_path() {
198        let mut k = BrepKernel::new();
199        let r = k.execute_batch(r#"[{"op": "makeCylinder", "args": {"radius": 1, "height": 2}}]"#);
200        assert!(batch_has_ok(&r, 0));
201    }
202
203    #[test]
204    fn make_cylinder_negative_radius() {
205        let mut k = BrepKernel::new();
206        let r = k.execute_batch(r#"[{"op": "makeCylinder", "args": {"radius": -1, "height": 2}}]"#);
207        assert!(batch_has_error(&r, 0));
208    }
209
210    #[test]
211    fn make_cylinder_zero_height() {
212        let mut k = BrepKernel::new();
213        let r = k.execute_batch(r#"[{"op": "makeCylinder", "args": {"radius": 1, "height": 0}}]"#);
214        assert!(batch_has_error(&r, 0));
215    }
216
217    // ── make_sphere ─────────────────────────────────────────────────
218
219    #[test]
220    fn make_sphere_happy_path() {
221        let mut k = BrepKernel::new();
222        let r = k.execute_batch(r#"[{"op": "makeSphere", "args": {"radius": 1}}]"#);
223        assert!(batch_has_ok(&r, 0));
224    }
225
226    #[test]
227    fn make_sphere_negative_radius() {
228        let mut k = BrepKernel::new();
229        let r = k.execute_batch(r#"[{"op": "makeSphere", "args": {"radius": -1}}]"#);
230        assert!(batch_has_error(&r, 0));
231    }
232
233    // ── make_cone ───────────────────────────────────────────────────
234
235    #[test]
236    fn make_cone_happy_path_full_cone() {
237        let mut k = BrepKernel::new();
238        let r = k.execute_batch(
239            r#"[{"op": "makeCone", "args": {"bottomRadius": 2, "topRadius": 0, "height": 3}}]"#,
240        );
241        assert!(batch_has_ok(&r, 0));
242    }
243
244    #[test]
245    fn make_cone_happy_path_frustum() {
246        let mut k = BrepKernel::new();
247        let r = k.execute_batch(
248            r#"[{"op": "makeCone", "args": {"bottomRadius": 2, "topRadius": 1, "height": 3}}]"#,
249        );
250        assert!(batch_has_ok(&r, 0));
251    }
252
253    #[test]
254    fn make_cone_both_radii_zero() {
255        let mut k = BrepKernel::new();
256        let r = k.execute_batch(
257            r#"[{"op": "makeCone", "args": {"bottomRadius": 0, "topRadius": 0, "height": 3}}]"#,
258        );
259        assert!(batch_has_error(&r, 0));
260    }
261
262    #[test]
263    fn make_cone_negative_radius() {
264        let mut k = BrepKernel::new();
265        let r = k.execute_batch(
266            r#"[{"op": "makeCone", "args": {"bottomRadius": -1, "topRadius": 1, "height": 3}}]"#,
267        );
268        assert!(batch_has_error(&r, 0));
269    }
270
271    #[test]
272    fn make_cone_zero_height() {
273        let mut k = BrepKernel::new();
274        let r = k.execute_batch(
275            r#"[{"op": "makeCone", "args": {"bottomRadius": 1, "topRadius": 0.5, "height": 0}}]"#,
276        );
277        assert!(batch_has_error(&r, 0));
278    }
279
280    // ── make_torus ──────────────────────────────────────────────────
281
282    #[test]
283    fn make_torus_happy_path() {
284        let mut k = BrepKernel::new();
285        let r = k.execute_batch(
286            r#"[{"op": "makeTorus", "args": {"majorRadius": 3, "minorRadius": 1}}]"#,
287        );
288        assert!(batch_has_ok(&r, 0));
289    }
290
291    #[test]
292    fn make_torus_minor_equals_major() {
293        let mut k = BrepKernel::new();
294        let r = k.execute_batch(
295            r#"[{"op": "makeTorus", "args": {"majorRadius": 2, "minorRadius": 2}}]"#,
296        );
297        assert!(batch_has_error(&r, 0));
298    }
299
300    #[test]
301    fn make_torus_minor_greater_than_major() {
302        let mut k = BrepKernel::new();
303        let r = k.execute_batch(
304            r#"[{"op": "makeTorus", "args": {"majorRadius": 1, "minorRadius": 2}}]"#,
305        );
306        assert!(batch_has_error(&r, 0));
307    }
308
309    #[test]
310    fn make_torus_zero_major_radius() {
311        let mut k = BrepKernel::new();
312        let r = k.execute_batch(
313            r#"[{"op": "makeTorus", "args": {"majorRadius": 0, "minorRadius": 1}}]"#,
314        );
315        assert!(batch_has_error(&r, 0));
316    }
317
318    // ── make_ellipsoid ──────────────────────────────────────────────
319
320    #[test]
321    fn make_ellipsoid_happy_path() {
322        let mut k = BrepKernel::new();
323        let r =
324            k.execute_batch(r#"[{"op": "makeEllipsoid", "args": {"rx": 1, "ry": 2, "rz": 3}}]"#);
325        assert!(batch_has_ok(&r, 0));
326    }
327
328    #[test]
329    fn make_ellipsoid_negative_rx() {
330        let mut k = BrepKernel::new();
331        let r =
332            k.execute_batch(r#"[{"op": "makeEllipsoid", "args": {"rx": -1, "ry": 2, "rz": 3}}]"#);
333        assert!(batch_has_error(&r, 0));
334    }
335
336    #[test]
337    fn make_ellipsoid_zero_ry() {
338        let mut k = BrepKernel::new();
339        let r =
340            k.execute_batch(r#"[{"op": "makeEllipsoid", "args": {"rx": 1, "ry": 0, "rz": 3}}]"#);
341        assert!(batch_has_error(&r, 0));
342    }
343
344    // ── multiple primitives in one kernel ─────────────────────────────
345
346    #[test]
347    fn multiple_primitives_independent_handles() {
348        let mut k = BrepKernel::new();
349        let r = k.execute_batch(
350            r#"[
351                {"op": "makeBox", "args": {"width": 1, "height": 1, "depth": 1}},
352                {"op": "makeCylinder", "args": {"radius": 0.5, "height": 2}},
353                {"op": "makeSphere", "args": {"radius": 0.5}}
354            ]"#,
355        );
356        let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
357        let h0 = parsed[0]["ok"].as_u64().unwrap();
358        let h1 = parsed[1]["ok"].as_u64().unwrap();
359        let h2 = parsed[2]["ok"].as_u64().unwrap();
360        assert_ne!(h0, h1);
361        assert_ne!(h1, h2);
362        assert_ne!(h0, h2);
363    }
364
365    #[test]
366    fn all_six_primitives_in_one_kernel() {
367        let mut k = BrepKernel::new();
368        let r = k.execute_batch(
369            r#"[
370                {"op": "makeBox", "args": {"width": 1, "height": 2, "depth": 3}},
371                {"op": "makeCylinder", "args": {"radius": 1, "height": 2}},
372                {"op": "makeSphere", "args": {"radius": 1}},
373                {"op": "makeCone", "args": {"bottomRadius": 1, "topRadius": 0.5, "height": 2}},
374                {"op": "makeTorus", "args": {"majorRadius": 3, "minorRadius": 1}},
375                {"op": "makeEllipsoid", "args": {"rx": 1, "ry": 2, "rz": 3}}
376            ]"#,
377        );
378        let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
379        for i in 0..6 {
380            assert!(
381                parsed[i]["ok"].is_number(),
382                "primitive {i} failed: {}",
383                parsed[i]
384            );
385        }
386    }
387
388    #[test]
389    fn invalid_handle_returns_error() {
390        let mut k = BrepKernel::new();
391        let r = k.execute_batch(r#"[{"op": "volume", "args": {"solid": 9999}}]"#);
392        assert!(batch_has_error(&r, 0));
393    }
394}