mesh_shell/offset/
sdf.rs

1//! SDF-based mesh offset for handling complex concavities.
2
3use std::time::Instant;
4use tracing::{debug, info, warn};
5
6use mesh_repair::Mesh;
7
8use crate::error::{ShellError, ShellResult};
9
10use super::extract::extract_isosurface;
11use super::grid::{SdfGrid, SdfOffsetParams};
12use super::transfer::transfer_vertex_data;
13
14/// Statistics from SDF offset operation.
15#[derive(Debug, Clone)]
16pub struct SdfOffsetStats {
17    /// Grid dimensions [x, y, z].
18    pub grid_dims: [usize; 3],
19    /// Total number of voxels in grid.
20    pub total_voxels: usize,
21    /// Time spent computing SDF (ms).
22    pub sdf_time_ms: u64,
23    /// Time spent extracting isosurface (ms).
24    pub extraction_time_ms: u64,
25    /// Time spent transferring vertex data (ms).
26    pub transfer_time_ms: u64,
27    /// Number of vertices in input mesh.
28    pub input_vertices: usize,
29    /// Number of vertices in output mesh.
30    pub output_vertices: usize,
31    /// Number of faces in output mesh.
32    pub output_faces: usize,
33}
34
35/// Result of SDF offset operation.
36#[derive(Debug)]
37pub struct SdfOffsetResult {
38    /// The offset mesh with vertex data transferred.
39    pub mesh: Mesh,
40    /// Statistics about the operation.
41    pub stats: SdfOffsetStats,
42}
43
44/// Apply SDF-based offset to create a new mesh with variable offsets.
45///
46/// This is the main entry point for SDF offset. It takes a mesh with
47/// offset values assigned to vertices and produces a new mesh offset
48/// by those values.
49///
50/// # Arguments
51/// * `mesh` - Input mesh with offset values assigned via `vertex.offset`
52/// * `params` - SDF offset parameters (voxel size, padding, etc.)
53///
54/// # Returns
55/// A new mesh representing the offset surface, with vertex data transferred.
56///
57/// # Example
58/// ```ignore
59/// let params = SdfOffsetParams::default();
60/// let result = apply_sdf_offset(&mesh, &params)?;
61/// println!("Generated {} vertices", result.stats.output_vertices);
62/// ```
63pub fn apply_sdf_offset(mesh: &Mesh, params: &SdfOffsetParams) -> ShellResult<SdfOffsetResult> {
64    let total_start = Instant::now();
65
66    if mesh.vertices.is_empty() {
67        return Err(ShellError::EmptyMesh);
68    }
69
70    // Check that vertices have offset values
71    let missing_offset = mesh
72        .vertices
73        .iter()
74        .filter(|v| v.offset.is_none())
75        .count();
76
77    if missing_offset > 0 {
78        warn!(
79            missing = missing_offset,
80            total = mesh.vertices.len(),
81            "Some vertices missing offset values, using 0.0"
82        );
83    }
84
85    let input_vertices = mesh.vertices.len();
86
87    info!(
88        vertices = input_vertices,
89        faces = mesh.faces.len(),
90        voxel_size_mm = params.voxel_size_mm,
91        padding_mm = params.padding_mm,
92        "Starting SDF offset"
93    );
94
95    // Step 1: Create voxel grid
96    let mut grid = SdfGrid::from_mesh_bounds(
97        mesh,
98        params.voxel_size_mm,
99        params.padding_mm,
100        params.max_voxels,
101    )?;
102
103    info!(
104        dims = ?grid.dims,
105        total_voxels = grid.total_voxels(),
106        "Grid created"
107    );
108
109    // Step 2: Compute base SDF
110    let sdf_start = Instant::now();
111    grid.compute_sdf(mesh);
112    let sdf_time_ms = sdf_start.elapsed().as_millis() as u64;
113
114    debug!(sdf_time_ms, "SDF computation complete");
115
116    // Step 3: Interpolate offsets into grid
117    grid.interpolate_offsets(mesh, params.offset_neighbors);
118
119    // Step 4: Apply variable offset
120    grid.apply_variable_offset();
121
122    // Step 5: Extract isosurface
123    let extract_start = Instant::now();
124    let mut output_mesh = extract_isosurface(&grid)?;
125    let extraction_time_ms = extract_start.elapsed().as_millis() as u64;
126
127    debug!(
128        extraction_time_ms,
129        vertices = output_mesh.vertices.len(),
130        faces = output_mesh.faces.len(),
131        "Surface extraction complete"
132    );
133
134    // Step 6: Transfer vertex data from original mesh
135    let transfer_start = Instant::now();
136    transfer_vertex_data(mesh, &mut output_mesh)?;
137    let transfer_time_ms = transfer_start.elapsed().as_millis() as u64;
138
139    debug!(transfer_time_ms, "Vertex data transfer complete");
140
141    let total_time_ms = total_start.elapsed().as_millis();
142
143    let stats = SdfOffsetStats {
144        grid_dims: grid.dims,
145        total_voxels: grid.total_voxels(),
146        sdf_time_ms,
147        extraction_time_ms,
148        transfer_time_ms,
149        input_vertices,
150        output_vertices: output_mesh.vertices.len(),
151        output_faces: output_mesh.faces.len(),
152    };
153
154    info!(
155        total_time_ms,
156        input_vertices = stats.input_vertices,
157        output_vertices = stats.output_vertices,
158        output_faces = stats.output_faces,
159        "SDF offset complete"
160    );
161
162    Ok(SdfOffsetResult {
163        mesh: output_mesh,
164        stats,
165    })
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use mesh_repair::Vertex;
172
173    fn create_unit_cube() -> Mesh {
174        let mut mesh = Mesh::new();
175
176        // Cube vertices (0-10mm)
177        for z in [0.0, 10.0] {
178            for y in [0.0, 10.0] {
179                for x in [0.0, 10.0] {
180                    let mut v = Vertex::from_coords(x, y, z);
181                    v.offset = Some(1.0); // 1mm uniform offset
182                    v.tag = Some(1);
183                    mesh.vertices.push(v);
184                }
185            }
186        }
187
188        // Cube faces (2 triangles per face)
189        mesh.faces.push([0, 1, 3]);
190        mesh.faces.push([0, 3, 2]);
191        mesh.faces.push([4, 7, 5]);
192        mesh.faces.push([4, 6, 7]);
193        mesh.faces.push([0, 5, 1]);
194        mesh.faces.push([0, 4, 5]);
195        mesh.faces.push([2, 3, 7]);
196        mesh.faces.push([2, 7, 6]);
197        mesh.faces.push([0, 2, 6]);
198        mesh.faces.push([0, 6, 4]);
199        mesh.faces.push([1, 5, 7]);
200        mesh.faces.push([1, 7, 3]);
201
202        mesh
203    }
204
205    #[test]
206    fn test_sdf_offset_cube() {
207        let mesh = create_unit_cube();
208
209        let params = SdfOffsetParams {
210            voxel_size_mm: 1.0,
211            padding_mm: 5.0,
212            max_voxels: 1_000_000,
213            offset_neighbors: 4,
214        };
215
216        let result = apply_sdf_offset(&mesh, &params).unwrap();
217
218        // Should produce a valid mesh
219        assert!(!result.mesh.vertices.is_empty());
220        assert!(!result.mesh.faces.is_empty());
221
222        // Output should be larger than input (we're expanding)
223        let input_bounds = mesh.bounds().unwrap();
224        let output_bounds = result.mesh.bounds().unwrap();
225
226        let input_extent = input_bounds.1 - input_bounds.0;
227        let output_extent = output_bounds.1 - output_bounds.0;
228
229        // With 1mm offset, output should be ~2mm larger in each dimension
230        assert!(
231            output_extent.x > input_extent.x,
232            "Output should be wider: {} vs {}",
233            output_extent.x,
234            input_extent.x
235        );
236    }
237
238    #[test]
239    fn test_sdf_offset_empty_mesh() {
240        let mesh = Mesh::new();
241        let params = SdfOffsetParams::default();
242
243        let result = apply_sdf_offset(&mesh, &params);
244        assert!(result.is_err());
245    }
246}