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::adaptive::{AdaptiveSdfParams, create_adaptive_grid, interpolate_offsets_adaptive};
11use super::extract::extract_isosurface;
12use super::grid::{SdfGrid, SdfOffsetParams};
13use super::transfer::transfer_vertex_data;
14
15/// Statistics from SDF offset operation.
16#[derive(Debug, Clone)]
17pub struct SdfOffsetStats {
18    /// Grid dimensions [x, y, z].
19    pub grid_dims: [usize; 3],
20    /// Total number of voxels in grid.
21    pub total_voxels: usize,
22    /// Time spent computing SDF (ms).
23    pub sdf_time_ms: u64,
24    /// Time spent extracting isosurface (ms).
25    pub extraction_time_ms: u64,
26    /// Time spent transferring vertex data (ms).
27    pub transfer_time_ms: u64,
28    /// Number of vertices in input mesh.
29    pub input_vertices: usize,
30    /// Number of vertices in output mesh.
31    pub output_vertices: usize,
32    /// Number of faces in output mesh.
33    pub output_faces: usize,
34    /// Whether adaptive resolution was used.
35    pub adaptive_resolution: bool,
36    /// Estimated memory savings from adaptive resolution (percentage, 0 if not used).
37    pub memory_savings_percent: f64,
38}
39
40/// Result of SDF offset operation.
41#[derive(Debug)]
42pub struct SdfOffsetResult {
43    /// The offset mesh with vertex data transferred.
44    pub mesh: Mesh,
45    /// Statistics about the operation.
46    pub stats: SdfOffsetStats,
47}
48
49/// Apply SDF-based offset to create a new mesh with variable offsets.
50///
51/// This is the main entry point for SDF offset. It takes a mesh with
52/// offset values assigned to vertices and produces a new mesh offset
53/// by those values.
54///
55/// # Arguments
56/// * `mesh` - Input mesh with offset values assigned via `vertex.offset`
57/// * `params` - SDF offset parameters (voxel size, padding, etc.)
58///
59/// # Returns
60/// A new mesh representing the offset surface, with vertex data transferred.
61///
62/// # Example
63/// ```ignore
64/// let params = SdfOffsetParams::default();
65/// let result = apply_sdf_offset(&mesh, &params)?;
66/// println!("Generated {} vertices", result.stats.output_vertices);
67/// ```
68pub fn apply_sdf_offset(mesh: &Mesh, params: &SdfOffsetParams) -> ShellResult<SdfOffsetResult> {
69    let total_start = Instant::now();
70
71    if mesh.vertices.is_empty() {
72        return Err(ShellError::EmptyMesh);
73    }
74
75    // Check that vertices have offset values
76    let missing_offset = mesh.vertices.iter().filter(|v| v.offset.is_none()).count();
77
78    if missing_offset > 0 {
79        warn!(
80            missing = missing_offset,
81            total = mesh.vertices.len(),
82            "Some vertices missing offset values, using 0.0"
83        );
84    }
85
86    let input_vertices = mesh.vertices.len();
87
88    info!(
89        vertices = input_vertices,
90        faces = mesh.faces.len(),
91        voxel_size_mm = params.voxel_size_mm,
92        padding_mm = params.padding_mm,
93        adaptive = params.adaptive_resolution,
94        "Starting SDF offset"
95    );
96
97    // Choose between adaptive and standard grid based on params
98    if params.adaptive_resolution {
99        apply_sdf_offset_adaptive(mesh, params, input_vertices, total_start)
100    } else {
101        apply_sdf_offset_standard(mesh, params, input_vertices, total_start)
102    }
103}
104
105/// Standard (non-adaptive) SDF offset implementation.
106fn apply_sdf_offset_standard(
107    mesh: &Mesh,
108    params: &SdfOffsetParams,
109    input_vertices: usize,
110    total_start: Instant,
111) -> ShellResult<SdfOffsetResult> {
112    // Step 1: Create voxel grid
113    let mut grid = SdfGrid::from_mesh_bounds(
114        mesh,
115        params.voxel_size_mm,
116        params.padding_mm,
117        params.max_voxels,
118    )?;
119
120    info!(
121        dims = ?grid.dims,
122        total_voxels = grid.total_voxels(),
123        "Grid created (standard)"
124    );
125
126    // Step 2: Compute base SDF
127    let sdf_start = Instant::now();
128    grid.compute_sdf(mesh);
129    let sdf_time_ms = sdf_start.elapsed().as_millis() as u64;
130
131    debug!(sdf_time_ms, "SDF computation complete");
132
133    // Step 3: Interpolate offsets into grid
134    grid.interpolate_offsets(mesh, params.offset_neighbors);
135
136    // Step 4: Apply variable offset
137    grid.apply_variable_offset();
138
139    // Step 5: Extract isosurface
140    let extract_start = Instant::now();
141    let mut output_mesh = extract_isosurface(&grid)?;
142    let extraction_time_ms = extract_start.elapsed().as_millis() as u64;
143
144    debug!(
145        extraction_time_ms,
146        vertices = output_mesh.vertices.len(),
147        faces = output_mesh.faces.len(),
148        "Surface extraction complete"
149    );
150
151    // Step 6: Transfer vertex data from original mesh
152    let transfer_start = Instant::now();
153    transfer_vertex_data(mesh, &mut output_mesh)?;
154    let transfer_time_ms = transfer_start.elapsed().as_millis() as u64;
155
156    debug!(transfer_time_ms, "Vertex data transfer complete");
157
158    let total_time_ms = total_start.elapsed().as_millis();
159
160    let stats = SdfOffsetStats {
161        grid_dims: grid.dims,
162        total_voxels: grid.total_voxels(),
163        sdf_time_ms,
164        extraction_time_ms,
165        transfer_time_ms,
166        input_vertices,
167        output_vertices: output_mesh.vertices.len(),
168        output_faces: output_mesh.faces.len(),
169        adaptive_resolution: false,
170        memory_savings_percent: 0.0,
171    };
172
173    info!(
174        total_time_ms,
175        input_vertices = stats.input_vertices,
176        output_vertices = stats.output_vertices,
177        output_faces = stats.output_faces,
178        "SDF offset complete (standard)"
179    );
180
181    Ok(SdfOffsetResult {
182        mesh: output_mesh,
183        stats,
184    })
185}
186
187/// Adaptive multi-resolution SDF offset implementation.
188fn apply_sdf_offset_adaptive(
189    mesh: &Mesh,
190    params: &SdfOffsetParams,
191    input_vertices: usize,
192    total_start: Instant,
193) -> ShellResult<SdfOffsetResult> {
194    // Convert SdfOffsetParams to AdaptiveSdfParams
195    let adaptive_params = AdaptiveSdfParams {
196        fine_voxel_size_mm: params.voxel_size_mm,
197        coarse_voxel_size_mm: params.coarse_voxel_size_mm(),
198        refinement_distance_mm: params.refinement_distance_mm,
199        padding_mm: params.padding_mm,
200        max_voxels: params.max_voxels,
201        offset_neighbors: params.offset_neighbors,
202    };
203
204    // Step 1-2: Create adaptive grid and compute SDF
205    let sdf_start = Instant::now();
206    let adaptive_result = create_adaptive_grid(mesh, &adaptive_params)?;
207    let mut grid = adaptive_result.grid;
208    let adaptive_stats = adaptive_result.stats;
209    let sdf_time_ms = sdf_start.elapsed().as_millis() as u64;
210
211    info!(
212        dims = ?grid.dims,
213        total_voxels = grid.total_voxels(),
214        coarse_voxels = adaptive_stats.coarse_voxels,
215        refined_voxels = adaptive_stats.refined_coarse_voxels,
216        memory_savings = format!("{:.1}%", adaptive_stats.memory_savings_percent),
217        "Adaptive grid created"
218    );
219
220    debug!(sdf_time_ms, "Adaptive SDF computation complete");
221
222    // Step 3: Interpolate offsets using adaptive method
223    interpolate_offsets_adaptive(&mut grid, mesh, &adaptive_params);
224
225    // Step 4: Apply variable offset
226    grid.apply_variable_offset();
227
228    // Step 5: Extract isosurface
229    let extract_start = Instant::now();
230    let mut output_mesh = extract_isosurface(&grid)?;
231    let extraction_time_ms = extract_start.elapsed().as_millis() as u64;
232
233    debug!(
234        extraction_time_ms,
235        vertices = output_mesh.vertices.len(),
236        faces = output_mesh.faces.len(),
237        "Surface extraction complete"
238    );
239
240    // Step 6: Transfer vertex data from original mesh
241    let transfer_start = Instant::now();
242    transfer_vertex_data(mesh, &mut output_mesh)?;
243    let transfer_time_ms = transfer_start.elapsed().as_millis() as u64;
244
245    debug!(transfer_time_ms, "Vertex data transfer complete");
246
247    let total_time_ms = total_start.elapsed().as_millis();
248
249    let stats = SdfOffsetStats {
250        grid_dims: grid.dims,
251        total_voxels: grid.total_voxels(),
252        sdf_time_ms,
253        extraction_time_ms,
254        transfer_time_ms,
255        input_vertices,
256        output_vertices: output_mesh.vertices.len(),
257        output_faces: output_mesh.faces.len(),
258        adaptive_resolution: true,
259        memory_savings_percent: adaptive_stats.memory_savings_percent,
260    };
261
262    info!(
263        total_time_ms,
264        input_vertices = stats.input_vertices,
265        output_vertices = stats.output_vertices,
266        output_faces = stats.output_faces,
267        memory_savings = format!("{:.1}%", stats.memory_savings_percent),
268        "SDF offset complete (adaptive)"
269    );
270
271    Ok(SdfOffsetResult {
272        mesh: output_mesh,
273        stats,
274    })
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use mesh_repair::Vertex;
281
282    fn create_unit_cube() -> Mesh {
283        let mut mesh = Mesh::new();
284
285        // Cube vertices (0-10mm)
286        for z in [0.0, 10.0] {
287            for y in [0.0, 10.0] {
288                for x in [0.0, 10.0] {
289                    let mut v = Vertex::from_coords(x, y, z);
290                    v.offset = Some(1.0); // 1mm uniform offset
291                    v.tag = Some(1);
292                    mesh.vertices.push(v);
293                }
294            }
295        }
296
297        // Cube faces (2 triangles per face)
298        mesh.faces.push([0, 1, 3]);
299        mesh.faces.push([0, 3, 2]);
300        mesh.faces.push([4, 7, 5]);
301        mesh.faces.push([4, 6, 7]);
302        mesh.faces.push([0, 5, 1]);
303        mesh.faces.push([0, 4, 5]);
304        mesh.faces.push([2, 3, 7]);
305        mesh.faces.push([2, 7, 6]);
306        mesh.faces.push([0, 2, 6]);
307        mesh.faces.push([0, 6, 4]);
308        mesh.faces.push([1, 5, 7]);
309        mesh.faces.push([1, 7, 3]);
310
311        mesh
312    }
313
314    #[test]
315    fn test_sdf_offset_cube() {
316        let mesh = create_unit_cube();
317
318        let params = SdfOffsetParams {
319            voxel_size_mm: 1.0,
320            padding_mm: 5.0,
321            max_voxels: 1_000_000,
322            offset_neighbors: 4,
323            adaptive_resolution: false,
324            coarse_voxel_multiplier: 4.0,
325            refinement_distance_mm: 5.0,
326            use_gpu: false,
327        };
328
329        let result = apply_sdf_offset(&mesh, &params).unwrap();
330
331        // Should produce a valid mesh
332        assert!(!result.mesh.vertices.is_empty());
333        assert!(!result.mesh.faces.is_empty());
334        assert!(!result.stats.adaptive_resolution);
335
336        // Output should be larger than input (we're expanding)
337        let input_bounds = mesh.bounds().unwrap();
338        let output_bounds = result.mesh.bounds().unwrap();
339
340        let input_extent = input_bounds.1 - input_bounds.0;
341        let output_extent = output_bounds.1 - output_bounds.0;
342
343        // With 1mm offset, output should be ~2mm larger in each dimension
344        assert!(
345            output_extent.x > input_extent.x,
346            "Output should be wider: {} vs {}",
347            output_extent.x,
348            input_extent.x
349        );
350    }
351
352    #[test]
353    fn test_sdf_offset_cube_adaptive() {
354        let mesh = create_unit_cube();
355
356        let params = SdfOffsetParams::adaptive();
357
358        let result = apply_sdf_offset(&mesh, &params).unwrap();
359
360        // Should produce a valid mesh
361        assert!(!result.mesh.vertices.is_empty());
362        assert!(!result.mesh.faces.is_empty());
363        assert!(result.stats.adaptive_resolution);
364
365        // Output should be larger than input (we're expanding)
366        let input_bounds = mesh.bounds().unwrap();
367        let output_bounds = result.mesh.bounds().unwrap();
368
369        let input_extent = input_bounds.1 - input_bounds.0;
370        let output_extent = output_bounds.1 - output_bounds.0;
371
372        // With offset, output should be larger
373        assert!(
374            output_extent.x > input_extent.x,
375            "Adaptive output should be wider: {} vs {}",
376            output_extent.x,
377            input_extent.x
378        );
379    }
380
381    #[test]
382    fn test_sdf_offset_adaptive_presets() {
383        let mesh = create_unit_cube();
384
385        // Test all adaptive presets compile and run
386        for params in [
387            SdfOffsetParams::adaptive(),
388            SdfOffsetParams::adaptive_high_quality(),
389            SdfOffsetParams::adaptive_large_mesh(),
390        ] {
391            let result = apply_sdf_offset(&mesh, &params).unwrap();
392            assert!(!result.mesh.vertices.is_empty());
393            assert!(result.stats.adaptive_resolution);
394        }
395    }
396
397    #[test]
398    fn test_sdf_offset_empty_mesh() {
399        let mesh = Mesh::new();
400        let params = SdfOffsetParams::default();
401
402        let result = apply_sdf_offset(&mesh, &params);
403        assert!(result.is_err());
404    }
405}