1#![allow(clippy::missing_errors_doc)]
4
5use brepkit_operations::tessellate;
6use wasm_bindgen::prelude::*;
7
8use crate::error::validate_positive;
9use crate::kernel::BrepKernel;
10use crate::shapes::{JsGroupedMesh, JsMesh};
11use crate::types::{GroupedMeshResult, UvMeshResult};
12
13fn resolve_angular_tol(angular_tolerance: Option<f64>) -> Result<f64, JsError> {
16 match angular_tolerance {
17 Some(a) => {
18 validate_positive(a, "angularTolerance")?;
19 Ok(a)
20 }
21 None => Ok(brepkit_math::chord::DEFAULT_ANGULAR_TOL),
22 }
23}
24
25#[wasm_bindgen]
26impl BrepKernel {
27 #[wasm_bindgen(js_name = "tessellateFace")]
35 pub fn tessellate_face(
36 &self,
37 face: u32,
38 deflection: f64,
39 angular_tolerance: Option<f64>,
40 ) -> Result<JsMesh, JsError> {
41 validate_positive(deflection, "deflection")?;
42 let angular_tol = resolve_angular_tol(angular_tolerance)?;
43 let face_id = self.resolve_face(face)?;
44 let mesh =
45 tessellate::tessellate_with_tolerance(&self.topo, face_id, deflection, angular_tol)?;
46 Ok(mesh.into())
47 }
48
49 #[wasm_bindgen(js_name = "tessellateSolid")]
57 pub fn tessellate_solid(
58 &self,
59 solid: u32,
60 deflection: f64,
61 angular_tolerance: Option<f64>,
62 ) -> Result<JsMesh, JsError> {
63 validate_positive(deflection, "deflection")?;
64 let angular_tol = resolve_angular_tol(angular_tolerance)?;
65 let solid_id = self.resolve_solid(solid)?;
66
67 let merged = tessellate::tessellate_solid_with_tolerance(
70 &self.topo,
71 solid_id,
72 deflection,
73 angular_tol,
74 )?;
75
76 Ok(merged.into())
77 }
78
79 #[wasm_bindgen(js_name = "tessellateSolidGrouped")]
89 pub fn tessellate_solid_grouped(
90 &self,
91 solid: u32,
92 deflection: f64,
93 angular_tolerance: Option<f64>,
94 ) -> Result<JsValue, JsError> {
95 validate_positive(deflection, "deflection")?;
96 let angular_tol = resolve_angular_tol(angular_tolerance)?;
97 let solid_id = self.resolve_solid(solid)?;
98
99 let (mesh, face_offsets) = tessellate::tessellate_solid_grouped_with_tolerance(
100 &self.topo,
101 solid_id,
102 deflection,
103 angular_tol,
104 )?;
105
106 let mut all_positions: Vec<f64> = Vec::with_capacity(mesh.positions.len() * 3);
107 for p in &mesh.positions {
108 all_positions.extend_from_slice(&[p.x(), p.y(), p.z()]);
109 }
110 let mut all_normals: Vec<f64> = Vec::with_capacity(mesh.normals.len() * 3);
111 for n in &mesh.normals {
112 all_normals.extend_from_slice(&[n.x(), n.y(), n.z()]);
113 }
114
115 let result = GroupedMeshResult {
116 positions: all_positions,
117 normals: all_normals,
118 indices: mesh.indices,
119 face_offsets,
120 };
121 Ok(serde_json::to_string(&result)
122 .map_err(|e| JsError::new(&e.to_string()))?
123 .into())
124 }
125
126 #[wasm_bindgen(js_name = "tessellateSolidGroupedBinary")]
138 pub fn tessellate_solid_grouped_binary(
139 &self,
140 solid: u32,
141 deflection: f64,
142 angular_tolerance: Option<f64>,
143 ) -> Result<JsGroupedMesh, JsError> {
144 validate_positive(deflection, "deflection")?;
145 let angular_tol = resolve_angular_tol(angular_tolerance)?;
146 let solid_id = self.resolve_solid(solid)?;
147
148 let (mesh, face_offsets) = tessellate::tessellate_solid_grouped_with_tolerance(
149 &self.topo,
150 solid_id,
151 deflection,
152 angular_tol,
153 )?;
154
155 Ok(JsGroupedMesh::new(mesh, face_offsets))
156 }
157
158 #[wasm_bindgen(js_name = "tessellateSolidUV")]
169 pub fn tessellate_solid_uv(
170 &self,
171 solid: u32,
172 deflection: f64,
173 angular_tolerance: Option<f64>,
174 ) -> Result<JsValue, JsError> {
175 validate_positive(deflection, "deflection")?;
176 let angular_tol = resolve_angular_tol(angular_tolerance)?;
177 let solid_id = self.resolve_solid(solid)?;
178 let faces = brepkit_topology::explorer::solid_faces(&self.topo, solid_id)?;
179
180 let mut all_positions: Vec<f64> = Vec::new();
181 let mut all_normals: Vec<f64> = Vec::new();
182 let mut all_uvs: Vec<f64> = Vec::new();
183 let mut all_indices: Vec<u32> = Vec::new();
184
185 for &face_id in &faces {
186 #[allow(clippy::cast_possible_truncation)]
187 let idx_offset = (all_positions.len() / 3) as u32;
188
189 let mesh_uv =
190 tessellate::tessellate_with_uvs_a(&self.topo, face_id, deflection, angular_tol)?;
191 for p in &mesh_uv.mesh.positions {
192 all_positions.extend_from_slice(&[p.x(), p.y(), p.z()]);
193 }
194 for n in &mesh_uv.mesh.normals {
195 all_normals.extend_from_slice(&[n.x(), n.y(), n.z()]);
196 }
197 for uv in &mesh_uv.uvs {
198 all_uvs.extend_from_slice(uv);
199 }
200 for &idx in &mesh_uv.mesh.indices {
201 all_indices.push(idx + idx_offset);
202 }
203 }
204
205 let result = UvMeshResult {
206 positions: all_positions,
207 normals: all_normals,
208 indices: all_indices,
209 uvs: all_uvs,
210 };
211 Ok(serde_json::to_string(&result)
212 .map_err(|e| JsError::new(&e.to_string()))?
213 .into())
214 }
215
216 #[wasm_bindgen(js_name = "meshEdges")]
227 pub fn mesh_edges(
228 &self,
229 solid: u32,
230 deflection: f64,
231 angular_tolerance: Option<f64>,
232 ) -> Result<crate::shapes::JsEdgeLines, JsError> {
233 validate_positive(deflection, "deflection")?;
234 let angular_tol = resolve_angular_tol(angular_tolerance)?;
235 let solid_id = self.resolve_solid(solid)?;
236 let edge_lines = tessellate::sample_solid_edges_filtered(
237 &self.topo,
238 solid_id,
239 deflection,
240 angular_tol,
241 true,
242 )?;
243 Ok(edge_lines.into())
244 }
245
246 #[wasm_bindgen(js_name = "meshEdgesAll")]
251 pub fn mesh_edges_all(
252 &self,
253 solid: u32,
254 deflection: f64,
255 angular_tolerance: Option<f64>,
256 ) -> Result<crate::shapes::JsEdgeLines, JsError> {
257 validate_positive(deflection, "deflection")?;
258 let angular_tol = resolve_angular_tol(angular_tolerance)?;
259 let solid_id = self.resolve_solid(solid)?;
260 let edge_lines = tessellate::sample_solid_edges_filtered(
261 &self.topo,
262 solid_id,
263 deflection,
264 angular_tol,
265 false,
266 )?;
267 Ok(edge_lines.into())
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 #![allow(clippy::unwrap_used, clippy::expect_used)]
274
275 use crate::kernel::BrepKernel;
276
277 fn kernel_with_box() -> (BrepKernel, u32) {
279 let mut k = BrepKernel::new();
280 let solid = k.make_box_solid(2.0, 3.0, 4.0).unwrap();
281 (k, solid)
282 }
283
284 #[test]
287 fn tessellate_solid_box_produces_nonempty_mesh() {
288 let (k, solid) = kernel_with_box();
289 let mesh = k.tessellate_solid(solid, 0.1, None).unwrap();
290 assert!(mesh.vertex_count() > 0, "expected vertices, got 0");
291 assert!(mesh.triangle_count() > 0, "expected triangles, got 0");
292 }
293
294 #[test]
295 fn tessellate_solid_positions_and_normals_lengths_match() {
296 let (k, solid) = kernel_with_box();
297 let mesh = k.tessellate_solid(solid, 0.1, None).unwrap();
298 let positions = mesh.positions();
299 let normals = mesh.normals();
300 assert_eq!(
302 positions.len(),
303 normals.len(),
304 "positions.len()={} normals.len()={}",
305 positions.len(),
306 normals.len()
307 );
308 assert_eq!(positions.len() % 3, 0);
310 }
311
312 #[test]
313 fn tessellate_solid_indices_are_valid_vertex_refs() {
314 let (k, solid) = kernel_with_box();
315 let mesh = k.tessellate_solid(solid, 0.1, None).unwrap();
316 let vertex_count = mesh.vertex_count();
317 let indices = mesh.indices();
318 for &idx in &indices {
319 assert!(
320 idx < vertex_count,
321 "index {idx} out of bounds (vertex_count={vertex_count})"
322 );
323 }
324 }
325
326 #[test]
327 fn tessellate_solid_coarser_deflection_has_fewer_triangles() {
328 let (k, solid) = kernel_with_box();
329 let fine = k.tessellate_solid(solid, 0.01, None).unwrap();
330 let coarse = k.tessellate_solid(solid, 1.0, None).unwrap();
331 assert!(
332 fine.triangle_count() >= coarse.triangle_count(),
333 "fine={} coarse={}",
334 fine.triangle_count(),
335 coarse.triangle_count()
336 );
337 }
338
339 #[test]
345 fn tessellate_solid_grouped_via_operations() {
346 let mut topo = brepkit_topology::topology::Topology::new();
347 let solid = brepkit_operations::primitives::make_box(&mut topo, 2.0, 3.0, 4.0).unwrap();
348
349 let (mesh, face_offsets) =
350 brepkit_operations::tessellate::tessellate_solid_grouped_with_tolerance(
351 &topo,
352 solid,
353 0.1,
354 brepkit_math::chord::DEFAULT_ANGULAR_TOL,
355 )
356 .unwrap();
357
358 assert!(!mesh.positions.is_empty(), "expected vertices");
359 assert!(!mesh.indices.is_empty(), "expected indices");
360 assert_eq!(face_offsets.len(), 7, "expected 7 face offsets for a box");
362 assert_eq!(*face_offsets.last().unwrap() as usize, mesh.indices.len());
363 for w in face_offsets.windows(2) {
365 assert!(w[0] < w[1], "box face groups must be non-empty");
366 assert_eq!((w[1] - w[0]) % 3, 0);
367 }
368 assert!(
369 brepkit_operations::tessellate::is_watertight(&mesh),
370 "grouped box mesh must be watertight"
371 );
372 }
373
374 #[test]
377 fn mesh_edges_all_box_produces_nonempty_edge_lines() {
378 let (k, solid) = kernel_with_box();
379 let edge_lines = k.mesh_edges_all(solid, 0.1, None).unwrap();
380 assert!(edge_lines.edge_count() > 0, "expected edges, got 0");
381 assert!(
382 !edge_lines.positions().is_empty(),
383 "positions must be non-empty"
384 );
385 }
386
387 #[test]
388 fn mesh_edges_all_box_has_twelve_edges() {
389 let (k, solid) = kernel_with_box();
391 let edge_lines = k.mesh_edges_all(solid, 0.1, None).unwrap();
392 assert_eq!(
393 edge_lines.edge_count(),
394 12,
395 "expected 12 box edges, got {}",
396 edge_lines.edge_count()
397 );
398 }
399
400 #[test]
404 fn tessellate_solid_invalid_handle_returns_error() {
405 let mut k = BrepKernel::new();
406 let r = k.execute_batch(
407 r#"[{"op": "tessellateSolid", "args": {"solid": 9999, "deflection": 0.1}}]"#,
408 );
409 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
410 assert!(parsed[0]["error"].is_string());
411 }
412
413 #[test]
414 fn mesh_edges_all_invalid_handle_returns_error() {
415 let mut k = BrepKernel::new();
416 let r = k.execute_batch(
417 r#"[{"op": "meshEdgesAll", "args": {"solid": 9999, "deflection": 0.1}}]"#,
418 );
419 let parsed: serde_json::Value = serde_json::from_str(&r).unwrap();
420 assert!(parsed[0]["error"].is_string());
421 }
422
423 #[test]
428 fn tessellate_solid_zero_deflection_is_invalid() {
429 use crate::error::validate_positive;
430 let result = validate_positive(0.0, "deflection");
431 assert!(result.is_err(), "zero deflection must be rejected");
432 }
433
434 #[test]
435 fn mesh_edges_all_zero_deflection_is_invalid() {
436 use crate::error::validate_positive;
437 let result = validate_positive(0.0, "deflection");
438 assert!(result.is_err(), "zero deflection must be rejected");
439 }
440
441 #[test]
442 fn negative_deflection_is_invalid() {
443 use crate::error::validate_positive;
444 let result = validate_positive(-1.0, "deflection");
445 assert!(result.is_err(), "negative deflection must be rejected");
446 }
447}