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}