Skip to main content

brep_kernel/abi/
objects.rs

1use super::*;
2
3#[wasm_bindgen]
4pub struct WasmBrepSolid {
5    pub(crate) solid: BrepSolid,
6}
7
8#[wasm_bindgen]
9impl WasmBrepSolid {
10    #[wasm_bindgen(constructor)]
11    pub fn new(solid_json: &str) -> Result<WasmBrepSolid, JsValue> {
12        let solid: BrepSolid = serde_json::from_str(solid_json)
13            .map_err(|error| javascript_error(error.to_string()))?;
14        let solid = validate_imported_solid(solid, "WasmBrepSolid")?;
15        Ok(Self { solid })
16    }
17
18    /// Construct from the flat f64 codec instead of JSON — same arena
19    /// validation as the JSON constructor.
20    pub fn from_buffer(data: &[f64], names_json: &str) -> Result<WasmBrepSolid, JsValue> {
21        let solid = decode_solid_buffer(data, names_json)?;
22        let solid = validate_imported_solid(solid, "WasmBrepSolid")?;
23        Ok(Self { solid })
24    }
25
26    pub fn to_buffer(&self) -> Result<WasmSolidBuffer, JsValue> {
27        solid_buffer(&self.solid, "{}".into())
28    }
29
30    pub fn to_json(&self) -> Result<String, JsValue> {
31        serde_json::to_string(&self.solid).map_err(|error| javascript_error(error.to_string()))
32    }
33
34    pub fn validate_json(&self) -> Result<String, JsValue> {
35        serde_json::to_string(&self.solid.validate())
36            .map_err(|error| javascript_error(error.to_string()))
37    }
38
39    pub fn mass_properties_json(&self) -> Result<String, JsValue> {
40        let properties = solid_mass_properties(&self.solid).map_err(javascript_error)?;
41        serde_json::to_string(&properties).map_err(|error| javascript_error(error.to_string()))
42    }
43
44    pub fn full_mass_properties_json(&self) -> Result<String, JsValue> {
45        let properties = solid_mass_properties_full(&self.solid).map_err(javascript_error)?;
46        serde_json::to_string(&properties).map_err(|error| javascript_error(error.to_string()))
47    }
48
49    /// Full mass properties scaled to a physical `density` (Golovanov §8.11):
50    /// `mass = density * volume`, inertia and the principal moments scale by
51    /// `density`, while volume, surface area, centroid, and the principal axes
52    /// are density-independent. Returns the `DensityMassProperties` JSON.
53    pub fn full_mass_properties_with_density_json(&self, density: f64) -> Result<String, JsValue> {
54        let properties = solid_mass_properties_full(&self.solid).map_err(javascript_error)?;
55        serde_json::to_string(&properties.with_density(density))
56            .map_err(|error| javascript_error(error.to_string()))
57    }
58
59    pub fn tessellate_json(
60        &self,
61        slabs_per_span_u: usize,
62        steps_per_span_v: usize,
63    ) -> Result<String, JsValue> {
64        let mesh = tessellate_brep(
65            &self.solid,
66            TessellationOptions {
67                slabs_per_span_u: slabs_per_span_u.max(1),
68                steps_per_span_v: steps_per_span_v.max(1),
69            },
70        )
71        .map_err(javascript_error)?;
72        serde_json::to_string(&mesh).map_err(|error| javascript_error(error.to_string()))
73    }
74
75    pub fn transform_json(
76        &self,
77        matrix: &[f64],
78        reverse_orientation: bool,
79    ) -> Result<String, JsValue> {
80        let matrix: [f64; 16] = matrix
81            .try_into()
82            .map_err(|_| javascript_error("transform matrix must contain 16 values".into()))?;
83        let transform = AffineTransform::new(matrix).map_err(javascript_error)?;
84        let solid = transform_brep(&self.solid, transform, reverse_orientation)
85            .map_err(javascript_error)?;
86        serde_json::to_string(&solid).map_err(|error| javascript_error(error.to_string()))
87    }
88
89    pub fn transform_buffer(
90        &self,
91        matrix: &[f64],
92        reverse_orientation: bool,
93    ) -> Result<WasmSolidBuffer, JsValue> {
94        let matrix: [f64; 16] = matrix
95            .try_into()
96            .map_err(|_| javascript_error("transform matrix must contain 16 values".into()))?;
97        let transform = AffineTransform::new(matrix).map_err(javascript_error)?;
98        let solid = transform_brep(&self.solid, transform, reverse_orientation)
99            .map_err(javascript_error)?;
100        solid_buffer(&solid, "{}".into())
101    }
102
103    pub fn boolean_buffer(
104        &self,
105        other: &WasmBrepSolid,
106        operation: &str,
107        tolerance: f64,
108        merge_coplanar_faces: bool,
109    ) -> Result<WasmSolidBuffer, JsValue> {
110        let operation = match operation {
111            "union" => BooleanOperation::Union,
112            "intersect" => BooleanOperation::Intersect,
113            "subtract" => BooleanOperation::Subtract,
114            _ => return Err(javascript_error("unknown boolean operation".into())),
115        };
116        let solid = boolean_operation(
117            &self.solid,
118            &other.solid,
119            operation,
120            &BooleanOptions {
121                tolerance,
122                merge_coplanar_faces,
123                ..BooleanOptions::default()
124            },
125        )
126        .map_err(javascript_refusal)?;
127        solid_buffer(&solid, "{}".into())
128    }
129
130    pub fn offset_shell_buffer(
131        &self,
132        opening_face_ids: &[f64],
133        distance: f64,
134    ) -> Result<WasmSolidBuffer, JsValue> {
135        crate::panic_hook::set_once();
136        let face_ids = opening_face_ids
137            .iter()
138            .map(|id| {
139                if !id.is_finite() || *id < 0.0 || id.fract() != 0.0 {
140                    Err(javascript_error("opening face IDs must be integers".into()))
141                } else {
142                    Ok(*id as u64)
143                }
144            })
145            .collect::<Result<Vec<_>, _>>()?;
146        let result = offset_shell(&self.solid, &face_ids, distance).map_err(javascript_error)?;
147        let metadata = serde_json::json!({ "face_images": result.face_images });
148        solid_buffer(&result.solid, metadata.to_string())
149    }
150
151    pub fn tessellate_watertight_buffers(
152        &self,
153        chord_tolerance: f64,
154    ) -> Result<WasmMeshBuffers, JsValue> {
155        let mesh =
156            tessellate_brep_watertight(&self.solid, chord_tolerance).map_err(javascript_error)?;
157        Ok(WasmMeshBuffers {
158            positions: mesh.positions,
159            normals: mesh.normals,
160            indices: mesh.indices,
161            face_ids: mesh.face_ids,
162        })
163    }
164
165    /// Tessellate only the faces whose global sequential index `i` satisfies
166    /// `i % stride == offset` (a face subset), so a heavy solid can be meshed
167    /// in parallel across worker instances (dispatch `stride` calls, offsets
168    /// 0..stride-1, concatenate the fragments on the caller side). No
169    /// closed-shell validation — a fragment is not a closed mesh; the caller
170    /// validates the concatenated whole. See
171    /// `tessellate_brep_watertight_face_stride`.
172    pub fn tessellate_watertight_face_stride_buffers(
173        &self,
174        chord_tolerance: f64,
175        stride: usize,
176        offset: usize,
177    ) -> Result<WasmMeshBuffers, JsValue> {
178        let mesh =
179            tessellate_brep_watertight_face_stride(&self.solid, chord_tolerance, stride, offset)
180                .map_err(javascript_error)?;
181        Ok(WasmMeshBuffers {
182            positions: mesh.positions,
183            normals: mesh.normals,
184            indices: mesh.indices,
185            face_ids: mesh.face_ids,
186        })
187    }
188
189    /// Compute + serialize every edge's shared samples once, so the face-range
190    /// split can hand the same buffer to each worker instead of recomputing
191    /// `sample_all_edges` in all of them. See `sample_edges_encoded`.
192    pub fn sample_edges_buffer(&self, chord_tolerance: f64) -> Result<Vec<f64>, JsValue> {
193        sample_edges_encoded(&self.solid, chord_tolerance).map_err(javascript_error)
194    }
195
196    /// Face-stride tessellation using pre-computed edge samples (from
197    /// `sample_edges_buffer`), skipping the redundant per-worker resampling.
198    pub fn tessellate_watertight_face_stride_with_samples_buffers(
199        &self,
200        chord_tolerance: f64,
201        stride: usize,
202        offset: usize,
203        samples: &[f64],
204    ) -> Result<WasmMeshBuffers, JsValue> {
205        let mesh = tessellate_brep_watertight_face_stride_with_samples(
206            &self.solid,
207            chord_tolerance,
208            stride,
209            offset,
210            samples,
211        )
212        .map_err(javascript_error)?;
213        Ok(WasmMeshBuffers {
214            positions: mesh.positions,
215            normals: mesh.normals,
216            indices: mesh.indices,
217            face_ids: mesh.face_ids,
218        })
219    }
220
221    pub fn tessellate_buffers(
222        &self,
223        slabs_per_span_u: usize,
224        steps_per_span_v: usize,
225    ) -> Result<WasmMeshBuffers, JsValue> {
226        let mesh = tessellate_brep(
227            &self.solid,
228            TessellationOptions {
229                slabs_per_span_u: slabs_per_span_u.max(1),
230                steps_per_span_v: steps_per_span_v.max(1),
231            },
232        )
233        .map_err(javascript_error)?;
234        Ok(WasmMeshBuffers {
235            positions: mesh.positions,
236            normals: mesh.normals,
237            indices: mesh.indices,
238            face_ids: mesh.face_ids,
239        })
240    }
241
242    pub fn boolean_json(
243        &self,
244        other: &WasmBrepSolid,
245        operation: &str,
246        tolerance: f64,
247        merge_coplanar_faces: bool,
248    ) -> Result<String, JsValue> {
249        let operation = match operation {
250            "union" => BooleanOperation::Union,
251            "intersect" => BooleanOperation::Intersect,
252            "subtract" => BooleanOperation::Subtract,
253            _ => return Err(javascript_error("unknown boolean operation".into())),
254        };
255        let solid = boolean_operation(
256            &self.solid,
257            &other.solid,
258            operation,
259            &BooleanOptions {
260                tolerance,
261                merge_coplanar_faces,
262                ..BooleanOptions::default()
263            },
264        )
265        .map_err(javascript_refusal)?;
266        serde_json::to_string(&solid).map_err(|error| javascript_error(error.to_string()))
267    }
268
269    pub fn offset_shell_json(
270        &self,
271        opening_face_ids: &[f64],
272        distance: f64,
273    ) -> Result<String, JsValue> {
274        crate::panic_hook::set_once();
275        let face_ids = opening_face_ids
276            .iter()
277            .map(|id| {
278                if !id.is_finite() || *id < 0.0 || id.fract() != 0.0 {
279                    Err(javascript_error("opening face IDs must be integers".into()))
280                } else {
281                    Ok(*id as u64)
282                }
283            })
284            .collect::<Result<Vec<_>, _>>()?;
285        let result = offset_shell(&self.solid, &face_ids, distance).map_err(javascript_error)?;
286        serde_json::to_string(&result).map_err(|error| javascript_error(error.to_string()))
287    }
288
289    /// Golovanov §6.12 direct editing — delete a transition face (chamfer /
290    /// fillet / simple 4-sided face) and heal the hole by extending and
291    /// re-intersecting its neighbours. `face_id` is the wire id of the face to
292    /// remove.
293    pub fn delete_face_and_heal_buffer(&self, face_id: f64) -> Result<WasmSolidBuffer, JsValue> {
294        crate::panic_hook::set_once();
295        if !face_id.is_finite() || face_id < 0.0 || face_id.fract() != 0.0 {
296            return Err(javascript_error(
297                "delete_face_and_heal: face id must be a non-negative integer".into(),
298            ));
299        }
300        let solid = delete_face_and_heal(&self.solid, face_id as u64).map_err(javascript_error)?;
301        solid_buffer(&solid, "{}".into())
302    }
303
304    pub fn delete_face_and_heal_json(&self, face_id: f64) -> Result<String, JsValue> {
305        crate::panic_hook::set_once();
306        if !face_id.is_finite() || face_id < 0.0 || face_id.fract() != 0.0 {
307            return Err(javascript_error(
308                "delete_face_and_heal: face id must be a non-negative integer".into(),
309            ));
310        }
311        let solid = delete_face_and_heal(&self.solid, face_id as u64).map_err(javascript_error)?;
312        serde_json::to_string(&solid).map_err(|error| javascript_error(error.to_string()))
313    }
314
315    /// Golovanov §6.12 direct editing — translate a face group rigidly by
316    /// (dx, dy, dz) and heal the adjacency by re-intersecting the planar
317    /// neighbours. Mirrors `delete_face_and_heal_json`; `face_ids` follows
318    /// `offset_shell_json`'s integer-id validation.
319    pub fn move_faces_json(
320        &self,
321        face_ids: &[f64],
322        dx: f64,
323        dy: f64,
324        dz: f64,
325    ) -> Result<String, JsValue> {
326        crate::panic_hook::set_once();
327        let face_ids = face_ids
328            .iter()
329            .map(|id| {
330                if !id.is_finite() || *id < 0.0 || id.fract() != 0.0 {
331                    Err(javascript_error(
332                        "move_faces: face ids must be non-negative integers".into(),
333                    ))
334                } else {
335                    Ok(*id as u64)
336                }
337            })
338            .collect::<Result<Vec<_>, _>>()?;
339        let solid =
340            move_faces(&self.solid, &face_ids, Vec3::new(dx, dy, dz)).map_err(javascript_error)?;
341        serde_json::to_string(&solid).map_err(|error| javascript_error(error.to_string()))
342    }
343
344    pub fn export_step(&self, name: &str, unit: &str, timestamp: &str) -> Result<String, JsValue> {
345        export_step(std::slice::from_ref(&self.solid), name, unit, timestamp)
346            .map_err(javascript_error)
347    }
348
349    /// Standalone heal/sew: pair coincident one-use boundary edges (open
350    /// chains and closed rims), merge the shells they join, and re-orient the
351    /// result outward. Best-effort — unsewable gaps stay open and are counted
352    /// in the returned report. Returns `{ "solid": …, "report": … }`.
353    pub fn sew_json(&self, tolerance: f64) -> Result<String, JsValue> {
354        crate::panic_hook::set_once();
355        let tolerance = if tolerance.is_finite() && tolerance > 0.0 {
356            tolerance
357        } else {
358            1e-4
359        };
360        let (solid, report) = sew_solid(&self.solid, tolerance).map_err(javascript_error)?;
361        serde_json::to_string(&serde_json::json!({ "solid": solid, "report": report }))
362            .map_err(|error| javascript_error(error.to_string()))
363    }
364}
365
366#[wasm_bindgen]
367impl WasmNurbsSurface {
368    #[wasm_bindgen(constructor)]
369    pub fn new(surface_json: &str) -> Result<WasmNurbsSurface, JsValue> {
370        let serialized: NurbsSurface = serde_json::from_str(surface_json)
371            .map_err(|error| javascript_error(error.to_string()))?;
372        let surface = NurbsSurface::new(
373            serialized.degree_u,
374            serialized.degree_v,
375            serialized.knots_u,
376            serialized.knots_v,
377            serialized.control_points,
378        )
379        .map_err(javascript_error)?;
380        Ok(Self { surface })
381    }
382
383    pub fn evaluate_many(&self, parameters_uv: &[f64]) -> Result<Vec<f64>, JsValue> {
384        if parameters_uv.len() % 2 != 0 {
385            return Err(javascript_error(
386                "surface parameter buffer must contain u,v pairs".into(),
387            ));
388        }
389        let mut points = Vec::with_capacity(parameters_uv.len() / 2 * 3);
390        for pair in parameters_uv.chunks_exact(2) {
391            let point = self
392                .surface
393                .evaluate(pair[0], pair[1])
394                .map_err(javascript_error)?;
395            points.extend([point.x, point.y, point.z]);
396        }
397        Ok(points)
398    }
399
400    pub fn normals_many(&self, parameters_uv: &[f64]) -> Result<Vec<f64>, JsValue> {
401        if parameters_uv.len() % 2 != 0 {
402            return Err(javascript_error(
403                "surface parameter buffer must contain u,v pairs".into(),
404            ));
405        }
406        let mut normals = Vec::with_capacity(parameters_uv.len() / 2 * 3);
407        for pair in parameters_uv.chunks_exact(2) {
408            let normal = self
409                .surface
410                .normal(pair[0], pair[1])
411                .map_err(javascript_error)?;
412            normals.extend([normal.x, normal.y, normal.z]);
413        }
414        Ok(normals)
415    }
416
417    pub fn derivatives_flat(
418        &self,
419        u: f64,
420        v: f64,
421        derivative_count: usize,
422    ) -> Result<Vec<f64>, JsValue> {
423        let derivatives = self
424            .surface
425            .derivatives(u, v, derivative_count)
426            .map_err(javascript_error)?;
427        let side = derivative_count + 1;
428        let mut flat = vec![0.0; side * side * 3];
429        for k in 0..side {
430            for l in 0..side {
431                let offset = (k * side + l) * 3;
432                flat[offset] = derivatives[k][l].x;
433                flat[offset + 1] = derivatives[k][l].y;
434                flat[offset + 2] = derivatives[k][l].z;
435            }
436        }
437        Ok(flat)
438    }
439}
440
441#[wasm_bindgen]
442impl WasmNurbsCurve {
443    #[wasm_bindgen(constructor)]
444    pub fn new(curve_json: &str) -> Result<WasmNurbsCurve, JsValue> {
445        let serialized: NurbsCurve = serde_json::from_str(curve_json)
446            .map_err(|error| javascript_error(error.to_string()))?;
447        let curve = NurbsCurve::new(
448            serialized.degree,
449            serialized.knots,
450            serialized.control_points,
451        )
452        .map_err(javascript_error)?;
453        Ok(Self { curve })
454    }
455
456    pub fn evaluate_many(&self, parameters: &[f64]) -> Result<Vec<f64>, JsValue> {
457        let mut points = Vec::with_capacity(parameters.len() * 3);
458        for parameter in parameters {
459            let point = self.curve.evaluate(*parameter).map_err(javascript_error)?;
460            points.extend([point.x, point.y, point.z]);
461        }
462        Ok(points)
463    }
464
465    pub fn derivatives_flat(
466        &self,
467        parameter: f64,
468        derivative_count: usize,
469    ) -> Result<Vec<f64>, JsValue> {
470        let derivatives = self
471            .curve
472            .derivatives(parameter, derivative_count)
473            .map_err(javascript_error)?;
474        let mut result = Vec::with_capacity(derivatives.len() * 3);
475        for derivative in derivatives {
476            result.extend([derivative.x, derivative.y, derivative.z]);
477        }
478        Ok(result)
479    }
480}