mesh_shell/builder.rs
1//! Fluent builder APIs for shell generation.
2//!
3//! This module provides ergonomic builder patterns for configuring and executing
4//! shell generation operations. Builders allow chaining configuration methods
5//! before executing the operation.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use mesh_repair::Mesh;
11//! use mesh_shell::ShellBuilder;
12//!
13//! let mesh = Mesh::load("scan.stl").unwrap();
14//!
15//! // Fluent API for shell generation
16//! let result = ShellBuilder::new(&mesh)
17//! .offset(2.0) // 2mm outward offset
18//! .wall_thickness(2.5) // 2.5mm walls
19//! .high_quality() // Use SDF-based wall generation
20//! .validate(true) // Validate result
21//! .build()
22//! .unwrap();
23//!
24//! result.mesh.save("shell.3mf").unwrap();
25//! ```
26
27use mesh_repair::{Mesh, ThicknessMap};
28
29use crate::error::ShellResult;
30use crate::offset::{SdfOffsetParams, SdfOffsetResult, apply_sdf_offset};
31use crate::shell::{
32 ShellParams, ShellResult as ShellStats, WallGenerationMethod, generate_shell,
33 generate_shell_with_progress,
34};
35
36/// Result from ShellBuilder containing the generated mesh and statistics.
37#[derive(Debug)]
38pub struct ShellBuildResult {
39 /// The generated shell mesh.
40 pub mesh: Mesh,
41 /// Statistics from the offset operation.
42 pub offset_stats: Option<crate::offset::SdfOffsetStats>,
43 /// Statistics from shell generation.
44 pub shell_stats: ShellStats,
45}
46
47/// Fluent builder for shell generation.
48///
49/// ShellBuilder provides a chainable API for configuring shell generation
50/// parameters before executing the operation. This is the recommended way
51/// to generate shells when you need custom configuration.
52///
53/// # Example
54///
55/// ```no_run
56/// use mesh_repair::Mesh;
57/// use mesh_shell::ShellBuilder;
58///
59/// let mesh = Mesh::load("scan.stl").unwrap();
60///
61/// // Simple usage with defaults
62/// let result = ShellBuilder::new(&mesh)
63/// .offset(2.0)
64/// .build()
65/// .unwrap();
66///
67/// // Advanced usage with custom settings
68/// let result = ShellBuilder::new(&mesh)
69/// .offset(3.0)
70/// .wall_thickness(2.0)
71/// .voxel_size(0.5)
72/// .use_gpu(true)
73/// .high_quality()
74/// .build()
75/// .unwrap();
76/// ```
77pub struct ShellBuilder<'a> {
78 mesh: &'a Mesh,
79 // Offset parameters
80 offset_mm: f64,
81 voxel_size_mm: f64,
82 padding_mm: f64,
83 max_voxels: usize,
84 adaptive_resolution: bool,
85 use_gpu: bool,
86 // Shell parameters
87 wall_thickness_mm: f64,
88 thickness_map: Option<ThicknessMap>,
89 min_thickness_mm: f64,
90 validate: bool,
91 wall_method: WallGenerationMethod,
92 sdf_voxel_size_mm: f64,
93 // Progress callback (uses the standard ProgressCallback type)
94 progress_callback: Option<mesh_repair::progress::ProgressCallback>,
95}
96
97impl<'a> ShellBuilder<'a> {
98 /// Create a new ShellBuilder for the given mesh.
99 ///
100 /// # Arguments
101 ///
102 /// * `mesh` - The input mesh to generate a shell around
103 ///
104 /// # Example
105 ///
106 /// ```no_run
107 /// use mesh_repair::Mesh;
108 /// use mesh_shell::ShellBuilder;
109 ///
110 /// let mesh = Mesh::load("scan.stl").unwrap();
111 /// let builder = ShellBuilder::new(&mesh);
112 /// ```
113 pub fn new(mesh: &'a Mesh) -> Self {
114 Self {
115 mesh,
116 // Sensible defaults for custom-fit products
117 offset_mm: 2.0,
118 voxel_size_mm: 0.75,
119 padding_mm: 12.0,
120 max_voxels: 50_000_000,
121 adaptive_resolution: false,
122 use_gpu: false,
123 wall_thickness_mm: 2.5,
124 thickness_map: None,
125 min_thickness_mm: 1.5,
126 validate: true,
127 wall_method: WallGenerationMethod::Normal,
128 sdf_voxel_size_mm: 0.5,
129 progress_callback: None,
130 }
131 }
132
133 // =========================================================================
134 // Offset Configuration
135 // =========================================================================
136
137 /// Set the offset distance in mm.
138 ///
139 /// This is the distance to offset the surface outward (positive)
140 /// or inward (negative). For custom-fit products like shoe insoles,
141 /// this is typically 1-5mm.
142 ///
143 /// # Arguments
144 ///
145 /// * `offset` - Offset distance in millimeters
146 ///
147 /// # Example
148 ///
149 /// ```no_run
150 /// use mesh_repair::Mesh;
151 /// use mesh_shell::ShellBuilder;
152 ///
153 /// let mesh = Mesh::load("scan.stl").unwrap();
154 /// let result = ShellBuilder::new(&mesh)
155 /// .offset(2.5) // 2.5mm outward
156 /// .build()
157 /// .unwrap();
158 /// ```
159 pub fn offset(mut self, offset: f64) -> Self {
160 self.offset_mm = offset;
161 self
162 }
163
164 /// Set the voxel size for SDF computation in mm.
165 ///
166 /// Smaller voxels give more detail but use more memory and time.
167 /// The default (0.75mm) is a good balance for most use cases.
168 ///
169 /// # Arguments
170 ///
171 /// * `size` - Voxel size in millimeters
172 ///
173 /// # Recommendations
174 ///
175 /// - **High quality**: 0.3-0.5mm
176 /// - **Standard**: 0.5-1.0mm
177 /// - **Fast/large meshes**: 1.0-2.0mm
178 pub fn voxel_size(mut self, size: f64) -> Self {
179 self.voxel_size_mm = size;
180 self
181 }
182
183 /// Set padding beyond mesh bounds in mm.
184 ///
185 /// This ensures the SDF grid extends far enough beyond the mesh
186 /// to capture the full offset surface.
187 pub fn padding(mut self, padding: f64) -> Self {
188 self.padding_mm = padding;
189 self
190 }
191
192 /// Set maximum number of voxels (memory limit).
193 ///
194 /// If the required grid would exceed this, an error is returned.
195 /// Default is 50 million voxels (~200MB for SDF values).
196 pub fn max_voxels(mut self, max: usize) -> Self {
197 self.max_voxels = max;
198 self
199 }
200
201 /// Enable adaptive multi-resolution SDF.
202 ///
203 /// Uses coarse voxels far from the surface and fine voxels near it.
204 /// This significantly reduces memory usage for large meshes while
205 /// maintaining detail quality near the surface.
206 pub fn adaptive(mut self, enable: bool) -> Self {
207 self.adaptive_resolution = enable;
208 self
209 }
210
211 /// Enable GPU acceleration for SDF computation.
212 ///
213 /// When enabled and a GPU is available, SDF computation uses
214 /// GPU compute shaders for significant speedup (3-68x faster
215 /// for small-medium meshes).
216 ///
217 /// Falls back to CPU if GPU is unavailable.
218 pub fn use_gpu(mut self, enable: bool) -> Self {
219 self.use_gpu = enable;
220 self
221 }
222
223 // =========================================================================
224 // Wall/Shell Configuration
225 // =========================================================================
226
227 /// Set uniform wall thickness in mm.
228 ///
229 /// This is the thickness of the shell walls. For 3D printing,
230 /// typical values are 1.5-4mm depending on the application.
231 ///
232 /// # Arguments
233 ///
234 /// * `thickness` - Wall thickness in millimeters
235 pub fn wall_thickness(mut self, thickness: f64) -> Self {
236 self.wall_thickness_mm = thickness;
237 self
238 }
239
240 /// Set variable wall thickness using a thickness map.
241 ///
242 /// This allows different wall thicknesses in different regions
243 /// (e.g., thick heel cup, thin arch in a shoe insole).
244 ///
245 /// # Arguments
246 ///
247 /// * `map` - ThicknessMap with per-vertex or per-region thickness values
248 pub fn thickness_map(mut self, map: ThicknessMap) -> Self {
249 self.thickness_map = Some(map);
250 self
251 }
252
253 /// Set minimum acceptable wall thickness in mm.
254 ///
255 /// Used during validation to flag walls that are too thin
256 /// for reliable 3D printing.
257 pub fn min_thickness(mut self, thickness: f64) -> Self {
258 self.min_thickness_mm = thickness;
259 self
260 }
261
262 /// Enable or disable post-generation validation.
263 ///
264 /// When enabled, the generated shell is validated for
265 /// manifoldness, wall thickness, and other quality metrics.
266 pub fn validate(mut self, enable: bool) -> Self {
267 self.validate = enable;
268 self
269 }
270
271 /// Use normal-based wall generation (fast but less accurate).
272 ///
273 /// Each vertex is offset along its normal. Fast, but wall thickness
274 /// may vary at corners (thinner at convex, thicker at concave).
275 pub fn fast_walls(mut self) -> Self {
276 self.wall_method = WallGenerationMethod::Normal;
277 self
278 }
279
280 /// Use SDF-based wall generation (slower but consistent thickness).
281 ///
282 /// Computes a signed distance field and extracts an isosurface.
283 /// This ensures consistent wall thickness regardless of curvature.
284 pub fn sdf_walls(mut self) -> Self {
285 self.wall_method = WallGenerationMethod::Sdf;
286 self
287 }
288
289 // =========================================================================
290 // Presets
291 // =========================================================================
292
293 /// Apply high-quality preset settings.
294 ///
295 /// Uses SDF-based wall generation with fine voxel resolution
296 /// for consistent wall thickness and smooth surfaces.
297 pub fn high_quality(mut self) -> Self {
298 self.wall_method = WallGenerationMethod::Sdf;
299 self.sdf_voxel_size_mm = 0.3;
300 self.voxel_size_mm = 0.5;
301 self.validate = true;
302 self
303 }
304
305 /// Apply fast preset settings.
306 ///
307 /// Uses normal-based wall generation with coarser resolution.
308 /// Good for quick previews or when speed is more important than quality.
309 pub fn fast(mut self) -> Self {
310 self.wall_method = WallGenerationMethod::Normal;
311 self.voxel_size_mm = 1.0;
312 self.validate = false;
313 self
314 }
315
316 /// Apply settings optimized for large meshes.
317 ///
318 /// Uses adaptive resolution and larger voxels to handle
319 /// meshes with hundreds of thousands of triangles.
320 pub fn large_mesh(mut self) -> Self {
321 self.adaptive_resolution = true;
322 self.voxel_size_mm = 0.75;
323 self.max_voxels = 30_000_000;
324 self
325 }
326
327 // =========================================================================
328 // Progress Reporting
329 // =========================================================================
330
331 /// Set a progress callback.
332 ///
333 /// The callback receives:
334 /// - `progress`: 0.0-1.0 completion percentage
335 /// - `stage`: Description of current stage
336 ///
337 /// Return `false` from the callback to cancel the operation.
338 ///
339 /// # Example
340 ///
341 /// ```no_run
342 /// use mesh_repair::Mesh;
343 /// use mesh_repair::progress::ProgressCallback;
344 /// use mesh_shell::ShellBuilder;
345 ///
346 /// let mesh = Mesh::load("scan.stl").unwrap();
347 ///
348 /// let callback: ProgressCallback = Box::new(|progress| {
349 /// println!("{}%: {}", progress.percent(), progress.message);
350 /// true // continue
351 /// });
352 ///
353 /// let result = ShellBuilder::new(&mesh)
354 /// .offset(2.0)
355 /// .with_progress(callback)
356 /// .build();
357 /// ```
358 pub fn with_progress(mut self, callback: mesh_repair::progress::ProgressCallback) -> Self {
359 self.progress_callback = Some(callback);
360 self
361 }
362
363 // =========================================================================
364 // Build
365 // =========================================================================
366
367 /// Build the shell with the configured parameters.
368 ///
369 /// This executes the full shell generation pipeline:
370 /// 1. Apply SDF offset to create inner surface
371 /// 2. Generate outer surface with walls
372 /// 3. Create rim connecting inner and outer
373 /// 4. Validate result (if enabled)
374 ///
375 /// # Returns
376 ///
377 /// A `ShellBuildResult` containing the generated mesh and statistics.
378 ///
379 /// # Errors
380 ///
381 /// Returns an error if:
382 /// - The mesh is empty or invalid
383 /// - The voxel grid would exceed memory limits
384 /// - Shell generation fails for any reason
385 pub fn build(self) -> ShellResult<ShellBuildResult> {
386 // Prepare the mesh with offset values
387 let mut mesh = self.mesh.clone();
388 for v in &mut mesh.vertices {
389 v.offset = Some(self.offset_mm as f32);
390 }
391
392 // Build SDF offset params
393 let offset_params = SdfOffsetParams {
394 voxel_size_mm: self.voxel_size_mm,
395 padding_mm: self.padding_mm,
396 max_voxels: self.max_voxels,
397 offset_neighbors: 8,
398 adaptive_resolution: self.adaptive_resolution,
399 coarse_voxel_multiplier: 4.0,
400 refinement_distance_mm: 5.0,
401 use_gpu: self.use_gpu,
402 };
403
404 // Apply SDF offset
405 let offset_result = apply_sdf_offset(&mesh, &offset_params)?;
406 let inner_shell = offset_result.mesh;
407 let offset_stats = Some(offset_result.stats);
408
409 // Build shell params
410 let shell_params = ShellParams {
411 wall_thickness_mm: self.wall_thickness_mm,
412 thickness_map: self.thickness_map,
413 min_thickness_mm: self.min_thickness_mm,
414 validate_after_generation: self.validate,
415 wall_generation_method: self.wall_method,
416 sdf_voxel_size_mm: self.sdf_voxel_size_mm,
417 sdf_max_voxels: self.max_voxels,
418 };
419
420 // Generate shell (with or without progress)
421 let (shell_mesh, shell_stats) = if let Some(ref callback) = self.progress_callback {
422 generate_shell_with_progress(&inner_shell, &shell_params, Some(callback))
423 } else {
424 generate_shell(&inner_shell, &shell_params)
425 };
426
427 Ok(ShellBuildResult {
428 mesh: shell_mesh,
429 offset_stats,
430 shell_stats,
431 })
432 }
433
434 /// Build only the offset surface (no walls).
435 ///
436 /// This is useful when you want the inner surface without
437 /// generating a full shell with walls.
438 ///
439 /// # Returns
440 ///
441 /// The offset result containing the inner surface mesh.
442 pub fn build_offset_only(self) -> ShellResult<SdfOffsetResult> {
443 // Prepare the mesh with offset values
444 let mut mesh = self.mesh.clone();
445 for v in &mut mesh.vertices {
446 v.offset = Some(self.offset_mm as f32);
447 }
448
449 // Build SDF offset params
450 let offset_params = SdfOffsetParams {
451 voxel_size_mm: self.voxel_size_mm,
452 padding_mm: self.padding_mm,
453 max_voxels: self.max_voxels,
454 offset_neighbors: 8,
455 adaptive_resolution: self.adaptive_resolution,
456 coarse_voxel_multiplier: 4.0,
457 refinement_distance_mm: 5.0,
458 use_gpu: self.use_gpu,
459 };
460
461 apply_sdf_offset(&mesh, &offset_params)
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use mesh_repair::Vertex;
469
470 fn create_test_cube() -> Mesh {
471 let mut mesh = Mesh::new();
472
473 // 8 vertices of a unit cube
474 mesh.vertices = vec![
475 Vertex::from_coords(0.0, 0.0, 0.0),
476 Vertex::from_coords(10.0, 0.0, 0.0),
477 Vertex::from_coords(10.0, 10.0, 0.0),
478 Vertex::from_coords(0.0, 10.0, 0.0),
479 Vertex::from_coords(0.0, 0.0, 10.0),
480 Vertex::from_coords(10.0, 0.0, 10.0),
481 Vertex::from_coords(10.0, 10.0, 10.0),
482 Vertex::from_coords(0.0, 10.0, 10.0),
483 ];
484
485 // 12 triangles (2 per face)
486 mesh.faces = vec![
487 // Bottom
488 [0, 2, 1],
489 [0, 3, 2],
490 // Top
491 [4, 5, 6],
492 [4, 6, 7],
493 // Front
494 [0, 1, 5],
495 [0, 5, 4],
496 // Back
497 [2, 3, 7],
498 [2, 7, 6],
499 // Left
500 [0, 4, 7],
501 [0, 7, 3],
502 // Right
503 [1, 2, 6],
504 [1, 6, 5],
505 ];
506
507 mesh
508 }
509
510 #[test]
511 fn test_builder_defaults() {
512 let mesh = create_test_cube();
513 let builder = ShellBuilder::new(&mesh);
514
515 // Check defaults
516 assert!((builder.offset_mm - 2.0).abs() < 1e-6);
517 assert!((builder.wall_thickness_mm - 2.5).abs() < 1e-6);
518 assert!(builder.validate);
519 assert!(!builder.use_gpu);
520 }
521
522 #[test]
523 fn test_builder_chaining() {
524 let mesh = create_test_cube();
525 let builder = ShellBuilder::new(&mesh)
526 .offset(3.0)
527 .wall_thickness(2.0)
528 .voxel_size(0.5)
529 .use_gpu(true)
530 .high_quality()
531 .validate(false);
532
533 assert!((builder.offset_mm - 3.0).abs() < 1e-6);
534 assert!((builder.wall_thickness_mm - 2.0).abs() < 1e-6);
535 assert!((builder.voxel_size_mm - 0.5).abs() < 1e-6);
536 assert!(builder.use_gpu);
537 assert!(!builder.validate);
538 assert_eq!(builder.wall_method, WallGenerationMethod::Sdf);
539 }
540
541 #[test]
542 fn test_fast_preset() {
543 let mesh = create_test_cube();
544 let builder = ShellBuilder::new(&mesh).fast();
545
546 assert_eq!(builder.wall_method, WallGenerationMethod::Normal);
547 assert!(!builder.validate);
548 }
549
550 #[test]
551 fn test_large_mesh_preset() {
552 let mesh = create_test_cube();
553 let builder = ShellBuilder::new(&mesh).large_mesh();
554
555 assert!(builder.adaptive_resolution);
556 assert_eq!(builder.max_voxels, 30_000_000);
557 }
558}