mesh_shell/shell/
generate.rs

1//! Shell generation algorithm.
2//!
3//! Generates a printable shell from the inner surface.
4
5use tracing::{debug, info, warn};
6
7use mesh_repair::{Mesh, ThicknessMap, compute_vertex_normals};
8
9use super::rim::{generate_rim, generate_rim_for_sdf_shell};
10use super::validation::{ShellValidationResult, validate_shell};
11use crate::offset::extract::extract_isosurface;
12use crate::offset::grid::SdfGrid;
13
14/// Method for generating the outer surface of the shell.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum WallGenerationMethod {
17    /// Normal-based offset (fast, but may have inconsistent thickness at corners).
18    ///
19    /// Each vertex is offset along its normal by the wall thickness.
20    /// Pros: Fast, preserves vertex correspondence with inner surface.
21    /// Cons: Wall thickness varies at corners (thinner at convex, thicker at concave).
22    #[default]
23    Normal,
24
25    /// SDF-based offset (robust, consistent wall thickness).
26    ///
27    /// Computes a signed distance field and extracts an isosurface at the
28    /// desired wall thickness distance. This ensures consistent wall thickness
29    /// regardless of surface curvature.
30    /// Pros: Consistent wall thickness, handles concave regions correctly.
31    /// Cons: Slower, may change vertex count, requires additional memory.
32    Sdf,
33}
34
35impl std::fmt::Display for WallGenerationMethod {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            WallGenerationMethod::Normal => write!(f, "normal"),
39            WallGenerationMethod::Sdf => write!(f, "sdf"),
40        }
41    }
42}
43
44/// Parameters for shell generation.
45#[derive(Debug, Clone)]
46pub struct ShellParams {
47    /// Uniform wall thickness in mm.
48    /// Used when `thickness_map` is None.
49    pub wall_thickness_mm: f64,
50    /// Variable wall thickness map.
51    /// When set, per-vertex thickness values override `wall_thickness_mm`.
52    /// This enables different wall thicknesses in different regions (e.g., thick heel, thin arch).
53    pub thickness_map: Option<ThicknessMap>,
54    /// Minimum acceptable wall thickness.
55    pub min_thickness_mm: f64,
56    /// Whether to validate the shell after generation.
57    pub validate_after_generation: bool,
58    /// Method for generating the outer surface.
59    pub wall_generation_method: WallGenerationMethod,
60    /// Voxel size for SDF-based wall generation (mm).
61    /// Smaller values give more detail but use more memory.
62    /// Only used when `wall_generation_method` is `Sdf`.
63    pub sdf_voxel_size_mm: f64,
64    /// Maximum voxels for SDF grid (memory limit).
65    /// Only used when `wall_generation_method` is `Sdf`.
66    pub sdf_max_voxels: usize,
67}
68
69impl Default for ShellParams {
70    fn default() -> Self {
71        Self {
72            wall_thickness_mm: 2.5,
73            thickness_map: None,
74            min_thickness_mm: 1.5,
75            validate_after_generation: true,
76            wall_generation_method: WallGenerationMethod::Normal,
77            sdf_voxel_size_mm: 0.5,
78            sdf_max_voxels: 50_000_000,
79        }
80    }
81}
82
83impl ShellParams {
84    /// Set the thickness map for variable wall thickness.
85    ///
86    /// # Example
87    ///
88    /// ```
89    /// use mesh_shell::ShellParams;
90    /// use mesh_repair::ThicknessMap;
91    ///
92    /// let thickness_map = ThicknessMap::new(2.0); // 2mm default
93    /// let params = ShellParams::default().with_thickness_map(thickness_map);
94    /// ```
95    pub fn with_thickness_map(mut self, map: ThicknessMap) -> Self {
96        self.thickness_map = Some(map);
97        self
98    }
99
100    /// Create params with a uniform thickness map.
101    ///
102    /// This is equivalent to setting `wall_thickness_mm`, but using the
103    /// thickness map infrastructure.
104    pub fn with_uniform_thickness(mut self, thickness: f64) -> Self {
105        self.thickness_map = Some(ThicknessMap::uniform(thickness));
106        self.wall_thickness_mm = thickness;
107        self
108    }
109
110    /// Create params optimized for high-quality output with consistent wall thickness.
111    ///
112    /// Uses SDF-based wall generation for consistent thickness at corners.
113    pub fn high_quality() -> Self {
114        Self {
115            wall_generation_method: WallGenerationMethod::Sdf,
116            sdf_voxel_size_mm: 0.3,
117            ..Default::default()
118        }
119    }
120
121    /// Create params optimized for fast generation.
122    ///
123    /// Uses normal-based offset which is faster but may have inconsistent thickness.
124    pub fn fast() -> Self {
125        Self {
126            wall_generation_method: WallGenerationMethod::Normal,
127            validate_after_generation: false,
128            ..Default::default()
129        }
130    }
131
132    /// Get the wall thickness for a specific vertex index.
133    ///
134    /// If a thickness map is set, uses the per-vertex value.
135    /// Otherwise, returns the uniform `wall_thickness_mm`.
136    pub fn get_vertex_thickness(&self, vertex_index: u32) -> f64 {
137        self.thickness_map
138            .as_ref()
139            .map(|m| m.get_vertex_thickness(vertex_index))
140            .unwrap_or(self.wall_thickness_mm)
141    }
142}
143
144/// Result of shell generation.
145#[derive(Debug)]
146pub struct ShellResult {
147    /// Number of inner surface vertices.
148    pub inner_vertex_count: usize,
149    /// Number of outer surface vertices.
150    pub outer_vertex_count: usize,
151    /// Number of rim faces generated.
152    pub rim_face_count: usize,
153    /// Total face count.
154    pub total_face_count: usize,
155    /// Boundary loop size (number of edges).
156    pub boundary_size: usize,
157    /// Validation result (if validation was performed).
158    pub validation: Option<ShellValidationResult>,
159    /// Wall generation method used.
160    pub wall_method: WallGenerationMethod,
161    /// Whether variable thickness was used.
162    pub variable_thickness: bool,
163}
164
165/// Generate a printable shell from the inner surface.
166///
167/// Creates outer surface using the configured method (normal or SDF-based),
168/// then connects inner and outer at boundaries with a rim.
169///
170/// # Arguments
171/// * `inner_shell` - The inner surface mesh (from offset stage)
172/// * `params` - Shell generation parameters
173///
174/// # Returns
175/// A tuple of (shell mesh, generation result).
176pub fn generate_shell(inner_shell: &Mesh, params: &ShellParams) -> (Mesh, ShellResult) {
177    let has_variable_thickness = params.thickness_map.is_some();
178
179    if has_variable_thickness {
180        info!(
181            "Generating shell with variable thickness (default={:.2}mm), method={}",
182            params.wall_thickness_mm, params.wall_generation_method
183        );
184    } else {
185        info!(
186            "Generating shell with thickness={:.2}mm, method={}",
187            params.wall_thickness_mm, params.wall_generation_method
188        );
189    }
190
191    match params.wall_generation_method {
192        WallGenerationMethod::Normal => generate_shell_normal(inner_shell, params),
193        WallGenerationMethod::Sdf => generate_shell_sdf(inner_shell, params),
194    }
195}
196
197/// Generate shell using normal-based offset (original fast method).
198fn generate_shell_normal(inner_shell: &Mesh, params: &ShellParams) -> (Mesh, ShellResult) {
199    let n = inner_shell.vertices.len();
200    let mut shell = Mesh::new();
201
202    // Step 1: Copy inner vertices and ensure normals
203    let mut inner_with_normals = inner_shell.clone();
204    compute_vertex_normals(&mut inner_with_normals);
205
206    // Step 2: Generate outer vertices by offsetting along normals
207    // Copy inner vertices first
208    for vertex in &inner_with_normals.vertices {
209        // Inner vertex (copy directly)
210        shell.vertices.push(vertex.clone());
211    }
212
213    // Generate outer vertices with per-vertex thickness
214    for (i, vertex) in inner_with_normals.vertices.iter().enumerate() {
215        // Get thickness for this vertex (uses thickness map if available)
216        let thickness = params.get_vertex_thickness(i as u32);
217
218        // Outer vertex (offset by wall thickness)
219        let normal = vertex
220            .normal
221            .unwrap_or_else(|| nalgebra::Vector3::new(0.0, 0.0, 1.0));
222        let outer_pos = vertex.position + normal * thickness;
223
224        let mut outer_vertex = vertex.clone();
225        outer_vertex.position = outer_pos;
226        // Keep normal for outer surface (points outward)
227        outer_vertex.normal = Some(normal);
228
229        shell.vertices.push(outer_vertex);
230    }
231
232    debug!("Generated {} inner + {} outer vertices", n, n);
233
234    // Step 3: Copy inner faces (reversed winding so normal points inward)
235    for face in &inner_shell.faces {
236        // Reverse winding so normal points inward
237        shell.faces.push([face[0], face[2], face[1]]);
238    }
239
240    // Step 4: Generate outer faces with offset indices (original winding for outward normals)
241    for face in &inner_shell.faces {
242        let n32 = n as u32;
243        shell
244            .faces
245            .push([face[0] + n32, face[1] + n32, face[2] + n32]);
246    }
247
248    let inner_face_count = inner_shell.faces.len();
249    debug!(
250        "Added {} inner + {} outer faces",
251        inner_face_count, inner_face_count
252    );
253
254    // Step 5: Find boundary edges and generate rim
255    let (rim_faces, boundary_size) = generate_rim(&inner_with_normals, n);
256
257    let rim_face_count = rim_faces.len();
258    for face in rim_faces {
259        shell.faces.push(face);
260    }
261
262    info!(
263        "Shell generation complete: {} vertices, {} faces",
264        shell.vertices.len(),
265        shell.faces.len()
266    );
267
268    // Optionally validate the generated shell
269    let validation = if params.validate_after_generation {
270        let validation_result = validate_shell(&shell);
271        if !validation_result.is_printable() {
272            warn!(
273                "Generated shell has {} validation issue(s)",
274                validation_result.issue_count()
275            );
276        }
277        Some(validation_result)
278    } else {
279        None
280    };
281
282    let result = ShellResult {
283        inner_vertex_count: n,
284        outer_vertex_count: n,
285        rim_face_count,
286        total_face_count: shell.faces.len(),
287        boundary_size,
288        validation,
289        wall_method: WallGenerationMethod::Normal,
290        variable_thickness: params.thickness_map.is_some(),
291    };
292
293    (shell, result)
294}
295
296/// Generate shell using SDF-based offset for consistent wall thickness.
297///
298/// Note: Variable thickness (ThicknessMap) is not fully supported with SDF method.
299/// The SDF method uses uniform wall thickness for consistent geometry.
300/// For variable thickness, use `WallGenerationMethod::Normal`.
301fn generate_shell_sdf(inner_shell: &Mesh, params: &ShellParams) -> (Mesh, ShellResult) {
302    let inner_vertex_count = inner_shell.vertices.len();
303
304    // Warn if using variable thickness with SDF (not fully supported)
305    if params.thickness_map.is_some() {
306        warn!(
307            "Variable thickness (ThicknessMap) is not fully supported with SDF wall generation. \
308             Using uniform thickness={:.2}mm. Consider using WallGenerationMethod::Normal for variable thickness.",
309            params.wall_thickness_mm
310        );
311    }
312
313    // Step 1: Ensure inner mesh has normals
314    let mut inner_with_normals = inner_shell.clone();
315    compute_vertex_normals(&mut inner_with_normals);
316
317    // Step 2: Create SDF grid for the inner surface
318    let padding = params.wall_thickness_mm + params.sdf_voxel_size_mm * 3.0;
319    let grid_result = SdfGrid::from_mesh_bounds(
320        &inner_with_normals,
321        params.sdf_voxel_size_mm,
322        padding,
323        params.sdf_max_voxels,
324    );
325
326    let mut grid = match grid_result {
327        Ok(g) => g,
328        Err(e) => {
329            warn!(
330                "SDF grid creation failed: {:?}, falling back to normal method",
331                e
332            );
333            return generate_shell_normal(inner_shell, params);
334        }
335    };
336
337    info!(
338        dims = ?grid.dims,
339        total_voxels = grid.total_voxels(),
340        "Created SDF grid for wall generation"
341    );
342
343    // Step 3: Compute SDF of inner surface
344    grid.compute_sdf(&inner_with_normals);
345
346    // Step 4: Offset the SDF by wall thickness to get outer surface
347    // Adding positive offset shifts isosurface outward
348    for val in &mut grid.values {
349        *val -= params.wall_thickness_mm as f32;
350    }
351
352    debug!("Applied wall thickness offset to SDF");
353
354    // Step 5: Extract outer surface from offset SDF
355    let outer_mesh = match extract_isosurface(&grid) {
356        Ok(m) => m,
357        Err(e) => {
358            warn!(
359                "Isosurface extraction failed: {:?}, falling back to normal method",
360                e
361            );
362            return generate_shell_normal(inner_shell, params);
363        }
364    };
365
366    let outer_vertex_count = outer_mesh.vertices.len();
367    debug!(
368        "Extracted outer surface: {} vertices, {} faces",
369        outer_vertex_count,
370        outer_mesh.faces.len()
371    );
372
373    // Step 6: Combine inner and outer surfaces into shell
374    let mut shell = Mesh::new();
375
376    // Add inner vertices
377    for vertex in &inner_with_normals.vertices {
378        shell.vertices.push(vertex.clone());
379    }
380
381    // Add outer vertices (offset by inner count)
382    let inner_count = inner_with_normals.vertices.len() as u32;
383    for vertex in &outer_mesh.vertices {
384        shell.vertices.push(vertex.clone());
385    }
386
387    // Add inner faces (reversed winding so normal points inward)
388    for face in &inner_with_normals.faces {
389        shell.faces.push([face[0], face[2], face[1]]);
390    }
391
392    // Add outer faces (keep original winding, offset indices)
393    for face in &outer_mesh.faces {
394        shell.faces.push([
395            face[0] + inner_count,
396            face[1] + inner_count,
397            face[2] + inner_count,
398        ]);
399    }
400
401    // Step 7: Generate rim connecting inner and outer boundaries
402    let (rim_faces, boundary_size) =
403        generate_rim_for_sdf_shell(&inner_with_normals, &outer_mesh, inner_count as usize);
404
405    let rim_face_count = rim_faces.len();
406    for face in rim_faces {
407        shell.faces.push(face);
408    }
409
410    info!(
411        "SDF shell generation complete: {} vertices, {} faces (rim: {})",
412        shell.vertices.len(),
413        shell.faces.len(),
414        rim_face_count
415    );
416
417    // Optionally validate the generated shell
418    let validation = if params.validate_after_generation {
419        let validation_result = validate_shell(&shell);
420        if !validation_result.is_printable() {
421            warn!(
422                "Generated shell has {} validation issue(s)",
423                validation_result.issue_count()
424            );
425        }
426        Some(validation_result)
427    } else {
428        None
429    };
430
431    let result = ShellResult {
432        inner_vertex_count,
433        outer_vertex_count,
434        rim_face_count,
435        total_face_count: shell.faces.len(),
436        boundary_size,
437        validation,
438        wall_method: WallGenerationMethod::Sdf,
439        variable_thickness: false, // SDF doesn't support variable thickness
440    };
441
442    (shell, result)
443}
444
445/// Generate a shell without automatic validation.
446///
447/// This is equivalent to calling `generate_shell` with `validate_after_generation = false`.
448pub fn generate_shell_no_validation(
449    inner_shell: &Mesh,
450    params: &ShellParams,
451) -> (Mesh, ShellResult) {
452    let mut params = params.clone();
453    params.validate_after_generation = false;
454    generate_shell(inner_shell, &params)
455}
456
457/// Generate a printable shell with progress reporting.
458///
459/// This is a progress-reporting variant of [`generate_shell`] that allows tracking
460/// the shell generation progress and supports cancellation via the progress callback.
461///
462/// The shell generation proceeds through these phases:
463/// 1. Vertex normal computation
464/// 2. Outer surface generation (normal offset or SDF)
465/// 3. Inner/outer face creation
466/// 4. Rim generation to connect boundaries
467/// 5. Optional validation
468///
469/// # Arguments
470/// * `inner_shell` - The inner surface mesh (from offset stage)
471/// * `params` - Shell generation parameters
472/// * `callback` - Optional progress callback. Returns `false` to request cancellation.
473///
474/// # Returns
475/// A tuple of (shell mesh, generation result).
476/// If cancelled via callback, returns the partial shell.
477///
478/// # Example
479/// ```ignore
480/// use mesh_shell::{generate_shell_with_progress, ShellParams};
481/// use mesh_repair::progress::ProgressCallback;
482///
483/// let callback: ProgressCallback = Box::new(|progress| {
484///     println!("{}% - {}", progress.percent(), progress.message);
485///     true // Continue
486/// });
487///
488/// let (shell, result) = generate_shell_with_progress(&inner_mesh, &ShellParams::default(), Some(&callback));
489/// ```
490pub fn generate_shell_with_progress(
491    inner_shell: &Mesh,
492    params: &ShellParams,
493    callback: Option<&mesh_repair::progress::ProgressCallback>,
494) -> (Mesh, ShellResult) {
495    use mesh_repair::progress::ProgressTracker;
496
497    let has_variable_thickness = params.thickness_map.is_some();
498
499    if has_variable_thickness {
500        info!(
501            "Generating shell with variable thickness (default={:.2}mm), method={}",
502            params.wall_thickness_mm, params.wall_generation_method
503        );
504    } else {
505        info!(
506            "Generating shell with thickness={:.2}mm, method={}",
507            params.wall_thickness_mm, params.wall_generation_method
508        );
509    }
510
511    // Total phases: normal computation (10%), outer generation (40%), faces (20%), rim (20%), validation (10%)
512    let tracker = ProgressTracker::new(100);
513
514    // Phase 1: Start and compute normals
515    tracker.set(5);
516    if !tracker.maybe_callback(callback, "Computing vertex normals".to_string()) {
517        return empty_shell_result(params);
518    }
519
520    let n = inner_shell.vertices.len();
521    let mut inner_with_normals = inner_shell.clone();
522    compute_vertex_normals(&mut inner_with_normals);
523
524    // Dispatch based on wall generation method
525    match params.wall_generation_method {
526        WallGenerationMethod::Normal => generate_shell_normal_with_progress(
527            inner_shell,
528            params,
529            &inner_with_normals,
530            n,
531            &tracker,
532            callback,
533        ),
534        WallGenerationMethod::Sdf => generate_shell_sdf_with_progress(
535            inner_shell,
536            params,
537            &inner_with_normals,
538            &tracker,
539            callback,
540        ),
541    }
542}
543
544/// Helper to create an empty shell result for early cancellation
545fn empty_shell_result(params: &ShellParams) -> (Mesh, ShellResult) {
546    (
547        Mesh::new(),
548        ShellResult {
549            inner_vertex_count: 0,
550            outer_vertex_count: 0,
551            rim_face_count: 0,
552            total_face_count: 0,
553            boundary_size: 0,
554            validation: None,
555            wall_method: params.wall_generation_method,
556            variable_thickness: params.thickness_map.is_some(),
557        },
558    )
559}
560
561/// Generate shell using normal-based offset with progress reporting.
562fn generate_shell_normal_with_progress(
563    inner_shell: &Mesh,
564    params: &ShellParams,
565    inner_with_normals: &Mesh,
566    n: usize,
567    tracker: &mesh_repair::progress::ProgressTracker,
568    callback: Option<&mesh_repair::progress::ProgressCallback>,
569) -> (Mesh, ShellResult) {
570    let mut shell = Mesh::new();
571
572    // Phase 2: Copy inner vertices
573    tracker.set(10);
574    if !tracker.maybe_callback(callback, "Copying inner vertices".to_string()) {
575        return empty_shell_result(params);
576    }
577
578    for vertex in &inner_with_normals.vertices {
579        shell.vertices.push(vertex.clone());
580    }
581
582    // Phase 3: Generate outer vertices by offsetting along normals
583    tracker.set(30);
584    if !tracker.maybe_callback(callback, "Generating outer surface vertices".to_string()) {
585        return empty_shell_result(params);
586    }
587
588    for (i, vertex) in inner_with_normals.vertices.iter().enumerate() {
589        let thickness = params.get_vertex_thickness(i as u32);
590        let normal = vertex
591            .normal
592            .unwrap_or_else(|| nalgebra::Vector3::new(0.0, 0.0, 1.0));
593        let outer_pos = vertex.position + normal * thickness;
594
595        let mut outer_vertex = vertex.clone();
596        outer_vertex.position = outer_pos;
597        outer_vertex.normal = Some(normal);
598
599        shell.vertices.push(outer_vertex);
600    }
601
602    debug!("Generated {} inner + {} outer vertices", n, n);
603
604    // Phase 4: Create inner and outer faces
605    tracker.set(50);
606    if !tracker.maybe_callback(callback, "Creating inner and outer faces".to_string()) {
607        return empty_shell_result(params);
608    }
609
610    // Inner faces (reversed winding so normal points inward)
611    for face in &inner_shell.faces {
612        shell.faces.push([face[0], face[2], face[1]]);
613    }
614
615    // Outer faces with offset indices (original winding for outward normals)
616    for face in &inner_shell.faces {
617        let n32 = n as u32;
618        shell
619            .faces
620            .push([face[0] + n32, face[1] + n32, face[2] + n32]);
621    }
622
623    let inner_face_count = inner_shell.faces.len();
624    debug!(
625        "Added {} inner + {} outer faces",
626        inner_face_count, inner_face_count
627    );
628
629    // Phase 5: Generate rim
630    tracker.set(70);
631    if !tracker.maybe_callback(callback, "Generating rim to connect boundaries".to_string()) {
632        return empty_shell_result(params);
633    }
634
635    let (rim_faces, boundary_size) = generate_rim(inner_with_normals, n);
636
637    let rim_face_count = rim_faces.len();
638    for face in rim_faces {
639        shell.faces.push(face);
640    }
641
642    info!(
643        "Shell generation complete: {} vertices, {} faces",
644        shell.vertices.len(),
645        shell.faces.len()
646    );
647
648    // Phase 6: Optional validation
649    tracker.set(90);
650    let validation = if params.validate_after_generation {
651        if !tracker.maybe_callback(callback, "Validating shell".to_string()) {
652            return (
653                shell.clone(),
654                ShellResult {
655                    inner_vertex_count: n,
656                    outer_vertex_count: n,
657                    rim_face_count,
658                    total_face_count: shell.faces.len(),
659                    boundary_size,
660                    validation: None,
661                    wall_method: WallGenerationMethod::Normal,
662                    variable_thickness: params.thickness_map.is_some(),
663                },
664            );
665        }
666
667        let validation_result = validate_shell(&shell);
668        if !validation_result.is_printable() {
669            warn!(
670                "Generated shell has {} validation issue(s)",
671                validation_result.issue_count()
672            );
673        }
674        Some(validation_result)
675    } else {
676        None
677    };
678
679    tracker.set(100);
680    let _ = tracker.maybe_callback(callback, "Shell generation complete".to_string());
681
682    let result = ShellResult {
683        inner_vertex_count: n,
684        outer_vertex_count: n,
685        rim_face_count,
686        total_face_count: shell.faces.len(),
687        boundary_size,
688        validation,
689        wall_method: WallGenerationMethod::Normal,
690        variable_thickness: params.thickness_map.is_some(),
691    };
692
693    (shell, result)
694}
695
696/// Generate shell using SDF-based offset with progress reporting.
697fn generate_shell_sdf_with_progress(
698    inner_shell: &Mesh,
699    params: &ShellParams,
700    inner_with_normals: &Mesh,
701    tracker: &mesh_repair::progress::ProgressTracker,
702    callback: Option<&mesh_repair::progress::ProgressCallback>,
703) -> (Mesh, ShellResult) {
704    let inner_vertex_count = inner_with_normals.vertices.len();
705
706    // Warn if using variable thickness with SDF (not fully supported)
707    if params.thickness_map.is_some() {
708        warn!(
709            "Variable thickness (ThicknessMap) is not fully supported with SDF wall generation. \
710             Using uniform thickness={:.2}mm. Consider using WallGenerationMethod::Normal for variable thickness.",
711            params.wall_thickness_mm
712        );
713    }
714
715    // Phase 2: Create SDF grid
716    tracker.set(20);
717    if !tracker.maybe_callback(callback, "Creating SDF grid".to_string()) {
718        return empty_shell_result(params);
719    }
720
721    let padding = params.wall_thickness_mm + params.sdf_voxel_size_mm * 3.0;
722    let grid_result = SdfGrid::from_mesh_bounds(
723        inner_with_normals,
724        params.sdf_voxel_size_mm,
725        padding,
726        params.sdf_max_voxels,
727    );
728
729    let mut grid = match grid_result {
730        Ok(g) => g,
731        Err(e) => {
732            warn!(
733                "SDF grid creation failed: {:?}, falling back to normal method",
734                e
735            );
736            return generate_shell_normal(inner_shell, params);
737        }
738    };
739
740    info!(
741        dims = ?grid.dims,
742        total_voxels = grid.total_voxels(),
743        "Created SDF grid for wall generation"
744    );
745
746    // Phase 3: Compute SDF
747    tracker.set(40);
748    if !tracker.maybe_callback(callback, "Computing signed distance field".to_string()) {
749        return empty_shell_result(params);
750    }
751
752    grid.compute_sdf(inner_with_normals);
753
754    // Phase 4: Offset SDF by wall thickness
755    tracker.set(50);
756    if !tracker.maybe_callback(callback, "Applying wall thickness offset".to_string()) {
757        return empty_shell_result(params);
758    }
759
760    for val in &mut grid.values {
761        *val -= params.wall_thickness_mm as f32;
762    }
763
764    debug!("Applied wall thickness offset to SDF");
765
766    // Phase 5: Extract outer surface from offset SDF
767    tracker.set(60);
768    if !tracker.maybe_callback(callback, "Extracting outer surface isosurface".to_string()) {
769        return empty_shell_result(params);
770    }
771
772    let outer_mesh = match extract_isosurface(&grid) {
773        Ok(m) => m,
774        Err(e) => {
775            warn!(
776                "Isosurface extraction failed: {:?}, falling back to normal method",
777                e
778            );
779            return generate_shell_normal(inner_shell, params);
780        }
781    };
782
783    let outer_vertex_count = outer_mesh.vertices.len();
784    debug!(
785        "Extracted outer surface: {} vertices, {} faces",
786        outer_vertex_count,
787        outer_mesh.faces.len()
788    );
789
790    // Phase 6: Combine inner and outer surfaces
791    tracker.set(70);
792    if !tracker.maybe_callback(callback, "Combining inner and outer surfaces".to_string()) {
793        return empty_shell_result(params);
794    }
795
796    let mut shell = Mesh::new();
797
798    // Add inner vertices
799    for vertex in &inner_with_normals.vertices {
800        shell.vertices.push(vertex.clone());
801    }
802
803    // Add outer vertices (offset by inner count)
804    let inner_count = inner_with_normals.vertices.len() as u32;
805    for vertex in &outer_mesh.vertices {
806        shell.vertices.push(vertex.clone());
807    }
808
809    // Add inner faces (reversed winding so normal points inward)
810    for face in &inner_with_normals.faces {
811        shell.faces.push([face[0], face[2], face[1]]);
812    }
813
814    // Add outer faces (keep original winding, offset indices)
815    for face in &outer_mesh.faces {
816        shell.faces.push([
817            face[0] + inner_count,
818            face[1] + inner_count,
819            face[2] + inner_count,
820        ]);
821    }
822
823    // Phase 7: Generate rim connecting inner and outer boundaries
824    tracker.set(80);
825    if !tracker.maybe_callback(callback, "Generating rim to connect boundaries".to_string()) {
826        return empty_shell_result(params);
827    }
828
829    let (rim_faces, boundary_size) =
830        generate_rim_for_sdf_shell(inner_with_normals, &outer_mesh, inner_count as usize);
831
832    let rim_face_count = rim_faces.len();
833    for face in rim_faces {
834        shell.faces.push(face);
835    }
836
837    info!(
838        "SDF shell generation complete: {} vertices, {} faces (rim: {})",
839        shell.vertices.len(),
840        shell.faces.len(),
841        rim_face_count
842    );
843
844    // Phase 8: Optional validation
845    tracker.set(90);
846    let validation = if params.validate_after_generation {
847        if !tracker.maybe_callback(callback, "Validating shell".to_string()) {
848            return (
849                shell.clone(),
850                ShellResult {
851                    inner_vertex_count,
852                    outer_vertex_count,
853                    rim_face_count,
854                    total_face_count: shell.faces.len(),
855                    boundary_size,
856                    validation: None,
857                    wall_method: WallGenerationMethod::Sdf,
858                    variable_thickness: false,
859                },
860            );
861        }
862
863        let validation_result = validate_shell(&shell);
864        if !validation_result.is_printable() {
865            warn!(
866                "Generated shell has {} validation issue(s)",
867                validation_result.issue_count()
868            );
869        }
870        Some(validation_result)
871    } else {
872        None
873    };
874
875    tracker.set(100);
876    let _ = tracker.maybe_callback(callback, "Shell generation complete".to_string());
877
878    let result = ShellResult {
879        inner_vertex_count,
880        outer_vertex_count,
881        rim_face_count,
882        total_face_count: shell.faces.len(),
883        boundary_size,
884        validation,
885        wall_method: WallGenerationMethod::Sdf,
886        variable_thickness: false,
887    };
888
889    (shell, result)
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use mesh_repair::Vertex;
896
897    fn create_open_box() -> Mesh {
898        // A box open on top (5 faces instead of 6)
899        let mut mesh = Mesh::new();
900
901        // Bottom corners
902        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
903        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
904        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 0.0));
905        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 0.0));
906        // Top corners
907        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 10.0));
908        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 10.0));
909        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 10.0));
910        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 10.0));
911
912        // Bottom (2 triangles)
913        mesh.faces.push([0, 2, 1]);
914        mesh.faces.push([0, 3, 2]);
915        // Front
916        mesh.faces.push([0, 1, 5]);
917        mesh.faces.push([0, 5, 4]);
918        // Back
919        mesh.faces.push([2, 3, 7]);
920        mesh.faces.push([2, 7, 6]);
921        // Left
922        mesh.faces.push([0, 4, 7]);
923        mesh.faces.push([0, 7, 3]);
924        // Right
925        mesh.faces.push([1, 2, 6]);
926        mesh.faces.push([1, 6, 5]);
927        // Top is OPEN - boundary is 4-5-6-7
928
929        mesh
930    }
931
932    #[test]
933    fn test_shell_params_default() {
934        let params = ShellParams::default();
935        assert_eq!(params.wall_thickness_mm, 2.5);
936        assert_eq!(params.min_thickness_mm, 1.5);
937        assert!(params.validate_after_generation);
938        assert_eq!(params.wall_generation_method, WallGenerationMethod::Normal);
939    }
940
941    #[test]
942    fn test_shell_params_high_quality() {
943        let params = ShellParams::high_quality();
944        assert_eq!(params.wall_generation_method, WallGenerationMethod::Sdf);
945        assert!(params.sdf_voxel_size_mm < 0.5);
946    }
947
948    #[test]
949    fn test_shell_params_fast() {
950        let params = ShellParams::fast();
951        assert_eq!(params.wall_generation_method, WallGenerationMethod::Normal);
952        assert!(!params.validate_after_generation);
953    }
954
955    #[test]
956    fn test_wall_generation_method_display() {
957        assert_eq!(format!("{}", WallGenerationMethod::Normal), "normal");
958        assert_eq!(format!("{}", WallGenerationMethod::Sdf), "sdf");
959    }
960
961    #[test]
962    fn test_generate_shell_doubles_vertices() {
963        let inner = create_open_box();
964        let params = ShellParams::default();
965
966        let (shell, result) = generate_shell(&inner, &params);
967
968        // Should have 2x vertices (inner + outer) for normal method
969        assert_eq!(shell.vertices.len(), inner.vertices.len() * 2);
970        assert_eq!(result.inner_vertex_count, inner.vertices.len());
971        assert_eq!(result.outer_vertex_count, inner.vertices.len());
972        assert_eq!(result.wall_method, WallGenerationMethod::Normal);
973    }
974
975    #[test]
976    fn test_shell_has_more_faces() {
977        let inner = create_open_box();
978        let params = ShellParams::default();
979
980        let (shell, result) = generate_shell(&inner, &params);
981
982        // Should have inner + outer + rim faces
983        assert!(shell.faces.len() > inner.faces.len() * 2);
984        assert!(result.rim_face_count > 0);
985    }
986
987    #[test]
988    fn test_generate_shell_sdf_method() {
989        let inner = create_open_box();
990        let params = ShellParams {
991            wall_generation_method: WallGenerationMethod::Sdf,
992            sdf_voxel_size_mm: 1.0, // Coarse for fast test
993            validate_after_generation: false,
994            ..Default::default()
995        };
996
997        let (shell, result) = generate_shell(&inner, &params);
998
999        // Should produce a valid mesh
1000        assert!(!shell.vertices.is_empty());
1001        assert!(!shell.faces.is_empty());
1002        assert_eq!(result.wall_method, WallGenerationMethod::Sdf);
1003
1004        // Inner vertex count should match
1005        assert_eq!(result.inner_vertex_count, inner.vertices.len());
1006
1007        // Outer vertex count may differ from inner (SDF remeshes)
1008        assert!(result.outer_vertex_count > 0);
1009    }
1010
1011    #[test]
1012    fn test_sdf_produces_larger_outer_surface() {
1013        let inner = create_open_box();
1014        let wall_thickness = 2.0;
1015
1016        let params = ShellParams {
1017            wall_thickness_mm: wall_thickness,
1018            wall_generation_method: WallGenerationMethod::Sdf,
1019            sdf_voxel_size_mm: 0.5,
1020            validate_after_generation: false,
1021            ..Default::default()
1022        };
1023
1024        let (shell, _result) = generate_shell(&inner, &params);
1025
1026        // Get bounds of inner and combined shell
1027        let inner_bounds = inner.bounds().unwrap();
1028        let shell_bounds = shell.bounds().unwrap();
1029
1030        // Shell should be larger due to wall thickness
1031        let inner_extent = inner_bounds.1 - inner_bounds.0;
1032        let shell_extent = shell_bounds.1 - shell_bounds.0;
1033
1034        // Shell should be ~2*wall_thickness larger in each dimension
1035        assert!(
1036            shell_extent.x > inner_extent.x,
1037            "Shell should be wider: {} vs {}",
1038            shell_extent.x,
1039            inner_extent.x
1040        );
1041        assert!(
1042            shell_extent.y > inner_extent.y,
1043            "Shell should be deeper: {} vs {}",
1044            shell_extent.y,
1045            inner_extent.y
1046        );
1047    }
1048
1049    #[test]
1050    fn test_variable_thickness_params() {
1051        let mut thickness_map = ThicknessMap::new(2.0);
1052        thickness_map.set_vertex_thickness(0, 3.0);
1053        thickness_map.set_vertex_thickness(1, 1.5);
1054
1055        let params = ShellParams::default().with_thickness_map(thickness_map);
1056
1057        assert!(params.thickness_map.is_some());
1058        assert_eq!(params.get_vertex_thickness(0), 3.0);
1059        assert_eq!(params.get_vertex_thickness(1), 1.5);
1060        assert_eq!(params.get_vertex_thickness(2), 2.0); // Default
1061    }
1062
1063    #[test]
1064    fn test_variable_thickness_shell_generation() {
1065        let inner = create_open_box();
1066
1067        // Create thickness map: bottom vertices thin (1mm), top vertices thick (3mm)
1068        let mut thickness_map = ThicknessMap::new(2.0);
1069        // Bottom vertices (0-3) are thin
1070        for i in 0..4 {
1071            thickness_map.set_vertex_thickness(i, 1.0);
1072        }
1073        // Top vertices (4-7) are thick
1074        for i in 4..8 {
1075            thickness_map.set_vertex_thickness(i, 3.0);
1076        }
1077
1078        let params = ShellParams {
1079            wall_generation_method: WallGenerationMethod::Normal,
1080            validate_after_generation: false,
1081            ..ShellParams::default()
1082        }
1083        .with_thickness_map(thickness_map);
1084
1085        let (shell, result) = generate_shell(&inner, &params);
1086
1087        // Check that variable thickness was reported
1088        assert!(result.variable_thickness);
1089
1090        // Verify the shell has correct structure
1091        assert_eq!(shell.vertices.len(), inner.vertices.len() * 2);
1092
1093        // Check that bottom outer vertices are offset less than top outer vertices
1094        let inner_vertex_count = inner.vertices.len();
1095
1096        // Outer vertex 0 (bottom) should be offset ~1mm from inner vertex 0
1097        let inner_v0 = shell.vertices[0].position;
1098        let outer_v0 = shell.vertices[inner_vertex_count].position;
1099        let offset_0 = (outer_v0 - inner_v0).norm();
1100
1101        // Outer vertex 4 (top) should be offset ~3mm from inner vertex 4
1102        let inner_v4 = shell.vertices[4].position;
1103        let outer_v4 = shell.vertices[inner_vertex_count + 4].position;
1104        let offset_4 = (outer_v4 - inner_v4).norm();
1105
1106        assert!(
1107            offset_4 > offset_0,
1108            "Top vertices should have larger offset: {} vs {}",
1109            offset_4,
1110            offset_0
1111        );
1112
1113        // The offsets should be close to their target values (within tolerance due to normal direction)
1114        assert!(
1115            offset_0 < 2.0,
1116            "Bottom offset should be around 1mm: {}",
1117            offset_0
1118        );
1119        assert!(
1120            offset_4 > 2.0,
1121            "Top offset should be around 3mm: {}",
1122            offset_4
1123        );
1124    }
1125
1126    #[test]
1127    fn test_uniform_thickness_via_map() {
1128        let inner = create_open_box();
1129
1130        // Use uniform thickness via map (should behave same as default)
1131        let params = ShellParams::default().with_uniform_thickness(2.5);
1132
1133        let (_shell, result) = generate_shell(&inner, &params);
1134
1135        assert!(result.variable_thickness); // Still counts as using thickness map
1136        assert_eq!(params.wall_thickness_mm, 2.5);
1137    }
1138
1139    #[test]
1140    fn test_get_vertex_thickness_without_map() {
1141        let params = ShellParams {
1142            wall_thickness_mm: 3.0,
1143            ..Default::default()
1144        };
1145
1146        // Without a thickness map, should return the uniform wall thickness
1147        assert_eq!(params.get_vertex_thickness(0), 3.0);
1148        assert_eq!(params.get_vertex_thickness(100), 3.0);
1149    }
1150}