mesh_shell/shell/
generate.rs

1//! Shell generation algorithm.
2//!
3//! Generates a printable shell from the inner surface.
4
5use tracing::{debug, info};
6
7use mesh_repair::{compute_vertex_normals, Mesh};
8
9use super::rim::generate_rim;
10
11/// Parameters for shell generation.
12#[derive(Debug, Clone)]
13pub struct ShellParams {
14    /// Uniform wall thickness in mm.
15    pub wall_thickness_mm: f64,
16    /// Minimum acceptable wall thickness.
17    pub min_thickness_mm: f64,
18}
19
20impl Default for ShellParams {
21    fn default() -> Self {
22        Self {
23            wall_thickness_mm: 2.5,
24            min_thickness_mm: 1.5,
25        }
26    }
27}
28
29/// Result of shell generation.
30#[derive(Debug)]
31pub struct ShellResult {
32    /// Number of inner surface vertices.
33    pub inner_vertex_count: usize,
34    /// Number of outer surface vertices.
35    pub outer_vertex_count: usize,
36    /// Number of rim faces generated.
37    pub rim_face_count: usize,
38    /// Total face count.
39    pub total_face_count: usize,
40    /// Boundary loop size (number of edges).
41    pub boundary_size: usize,
42}
43
44/// Generate a printable shell from the inner surface.
45///
46/// Creates outer surface by offsetting along normals, then connects
47/// inner and outer at boundaries with a rim.
48///
49/// # Arguments
50/// * `inner_shell` - The inner surface mesh (from offset stage)
51/// * `params` - Shell generation parameters
52///
53/// # Returns
54/// A tuple of (shell mesh, generation result).
55pub fn generate_shell(inner_shell: &Mesh, params: &ShellParams) -> (Mesh, ShellResult) {
56    info!("Generating shell with thickness={:.2}mm", params.wall_thickness_mm);
57
58    let n = inner_shell.vertices.len();
59    let mut shell = Mesh::new();
60
61    // Step 1: Copy inner vertices and ensure normals
62    let mut inner_with_normals = inner_shell.clone();
63    compute_vertex_normals(&mut inner_with_normals);
64
65    // Step 2: Generate outer vertices by offsetting along normals
66    for vertex in &inner_with_normals.vertices {
67        // Inner vertex (copy directly)
68        shell.vertices.push(vertex.clone());
69    }
70
71    for vertex in &inner_with_normals.vertices {
72        // Outer vertex (offset by wall thickness)
73        let normal = vertex.normal.unwrap_or_else(|| nalgebra::Vector3::new(0.0, 0.0, 1.0));
74        let outer_pos = vertex.position + normal * params.wall_thickness_mm;
75
76        let mut outer_vertex = vertex.clone();
77        outer_vertex.position = outer_pos;
78        // Keep normal for outer surface (points outward)
79        outer_vertex.normal = Some(normal);
80
81        shell.vertices.push(outer_vertex);
82    }
83
84    debug!("Generated {} inner + {} outer vertices", n, n);
85
86    // Step 3: Copy inner faces (reversed winding so normal points inward)
87    for face in &inner_shell.faces {
88        // Reverse winding so normal points inward
89        shell.faces.push([face[0], face[2], face[1]]);
90    }
91
92    // Step 4: Generate outer faces with offset indices (original winding for outward normals)
93    for face in &inner_shell.faces {
94        let n32 = n as u32;
95        shell.faces.push([face[0] + n32, face[1] + n32, face[2] + n32]);
96    }
97
98    let inner_face_count = inner_shell.faces.len();
99    debug!("Added {} inner + {} outer faces", inner_face_count, inner_face_count);
100
101    // Step 5: Find boundary edges and generate rim
102    let (rim_faces, boundary_size) = generate_rim(&inner_with_normals, n);
103
104    let rim_face_count = rim_faces.len();
105    for face in rim_faces {
106        shell.faces.push(face);
107    }
108
109    info!(
110        "Shell generation complete: {} vertices, {} faces",
111        shell.vertices.len(),
112        shell.faces.len()
113    );
114
115    let result = ShellResult {
116        inner_vertex_count: n,
117        outer_vertex_count: n,
118        rim_face_count,
119        total_face_count: shell.faces.len(),
120        boundary_size,
121    };
122
123    (shell, result)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use mesh_repair::Vertex;
130
131    fn create_open_box() -> Mesh {
132        // A box open on top (5 faces instead of 6)
133        let mut mesh = Mesh::new();
134
135        // Bottom corners
136        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
137        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
138        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 0.0));
139        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 0.0));
140        // Top corners
141        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 10.0));
142        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 10.0));
143        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 10.0));
144        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 10.0));
145
146        // Bottom (2 triangles)
147        mesh.faces.push([0, 2, 1]);
148        mesh.faces.push([0, 3, 2]);
149        // Front
150        mesh.faces.push([0, 1, 5]);
151        mesh.faces.push([0, 5, 4]);
152        // Back
153        mesh.faces.push([2, 3, 7]);
154        mesh.faces.push([2, 7, 6]);
155        // Left
156        mesh.faces.push([0, 4, 7]);
157        mesh.faces.push([0, 7, 3]);
158        // Right
159        mesh.faces.push([1, 2, 6]);
160        mesh.faces.push([1, 6, 5]);
161        // Top is OPEN - boundary is 4-5-6-7
162
163        mesh
164    }
165
166    #[test]
167    fn test_shell_params_default() {
168        let params = ShellParams::default();
169        assert_eq!(params.wall_thickness_mm, 2.5);
170        assert_eq!(params.min_thickness_mm, 1.5);
171    }
172
173    #[test]
174    fn test_generate_shell_doubles_vertices() {
175        let inner = create_open_box();
176        let params = ShellParams::default();
177
178        let (shell, result) = generate_shell(&inner, &params);
179
180        // Should have 2x vertices (inner + outer)
181        assert_eq!(shell.vertices.len(), inner.vertices.len() * 2);
182        assert_eq!(result.inner_vertex_count, inner.vertices.len());
183        assert_eq!(result.outer_vertex_count, inner.vertices.len());
184    }
185
186    #[test]
187    fn test_shell_has_more_faces() {
188        let inner = create_open_box();
189        let params = ShellParams::default();
190
191        let (shell, result) = generate_shell(&inner, &params);
192
193        // Should have inner + outer + rim faces
194        assert!(shell.faces.len() > inner.faces.len() * 2);
195        assert!(result.rim_face_count > 0);
196    }
197}