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