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