mesh_shell/offset/
grid.rs

1//! Voxel grid for SDF (Signed Distance Field) computation.
2
3use nalgebra::Point3;
4use rayon::prelude::*;
5use tracing::{debug, info};
6
7use mesh_repair::Mesh;
8
9use crate::error::{ShellError, ShellResult};
10
11/// Parameters for SDF offset operation.
12#[derive(Debug, Clone)]
13pub struct SdfOffsetParams {
14    /// Voxel size in mm (smaller = more detail, more memory).
15    /// When `adaptive_resolution` is true, this is the fine voxel size.
16    pub voxel_size_mm: f64,
17    /// Padding beyond mesh bounds in mm.
18    pub padding_mm: f64,
19    /// Maximum number of voxels before error (memory safety).
20    pub max_voxels: usize,
21    /// Number of nearest neighbors for offset interpolation.
22    pub offset_neighbors: usize,
23    /// Enable adaptive multi-resolution SDF for memory efficiency.
24    /// When true, uses coarse voxels far from the surface and fine voxels near it.
25    pub adaptive_resolution: bool,
26    /// Coarse voxel size multiplier (relative to voxel_size_mm).
27    /// Only used when `adaptive_resolution` is true.
28    /// Default: 4.0 (coarse voxels are 4x larger than fine voxels).
29    pub coarse_voxel_multiplier: f64,
30    /// Distance from surface (in mm) within which to use fine voxels.
31    /// Only used when `adaptive_resolution` is true.
32    /// Default: 5.0mm
33    pub refinement_distance_mm: f64,
34    /// Use GPU acceleration if available (requires `gpu` feature).
35    /// When enabled and a GPU is available, SDF computation will use
36    /// GPU compute shaders for significant speedup on large meshes.
37    /// Falls back to CPU if GPU is unavailable or initialization fails.
38    pub use_gpu: bool,
39}
40
41impl Default for SdfOffsetParams {
42    fn default() -> Self {
43        Self {
44            voxel_size_mm: 0.75,
45            padding_mm: 12.0,
46            max_voxels: 50_000_000,
47            offset_neighbors: 8,
48            adaptive_resolution: false,
49            coarse_voxel_multiplier: 4.0,
50            refinement_distance_mm: 5.0,
51            use_gpu: false,
52        }
53    }
54}
55
56impl SdfOffsetParams {
57    /// Create params with adaptive resolution enabled for large meshes.
58    ///
59    /// Uses coarser voxels far from the surface to reduce memory usage
60    /// while maintaining detail quality near the surface.
61    pub fn adaptive() -> Self {
62        Self {
63            voxel_size_mm: 0.5,
64            padding_mm: 10.0,
65            max_voxels: 50_000_000,
66            offset_neighbors: 8,
67            adaptive_resolution: true,
68            coarse_voxel_multiplier: 4.0,
69            refinement_distance_mm: 5.0,
70            use_gpu: false,
71        }
72    }
73
74    /// Create high-quality params with adaptive resolution.
75    pub fn adaptive_high_quality() -> Self {
76        Self {
77            voxel_size_mm: 0.4,
78            padding_mm: 12.0,
79            max_voxels: 80_000_000,
80            offset_neighbors: 12,
81            adaptive_resolution: true,
82            coarse_voxel_multiplier: 3.0,
83            refinement_distance_mm: 8.0,
84            use_gpu: false,
85        }
86    }
87
88    /// Create params optimized for very large meshes.
89    pub fn adaptive_large_mesh() -> Self {
90        Self {
91            voxel_size_mm: 0.75,
92            padding_mm: 8.0,
93            max_voxels: 30_000_000,
94            offset_neighbors: 6,
95            adaptive_resolution: true,
96            coarse_voxel_multiplier: 5.0,
97            refinement_distance_mm: 4.0,
98            use_gpu: false,
99        }
100    }
101
102    /// Create params with GPU acceleration enabled.
103    ///
104    /// Requires the `gpu` feature to be enabled. Falls back to CPU
105    /// automatically if no GPU is available.
106    pub fn with_gpu(mut self) -> Self {
107        self.use_gpu = true;
108        self
109    }
110
111    /// Get the coarse voxel size when adaptive resolution is enabled.
112    pub fn coarse_voxel_size_mm(&self) -> f64 {
113        self.voxel_size_mm * self.coarse_voxel_multiplier
114    }
115}
116
117/// 3D voxel grid for SDF computation.
118#[derive(Debug)]
119pub struct SdfGrid {
120    /// Grid dimensions [x, y, z].
121    pub dims: [usize; 3],
122    /// Origin point (min corner) in world coordinates.
123    pub origin: Point3<f64>,
124    /// Voxel size in mm.
125    pub voxel_size: f64,
126    /// Signed distance values (negative = inside mesh).
127    pub values: Vec<f32>,
128    /// Offset values at each voxel (for variable offset).
129    pub offsets: Vec<f32>,
130}
131
132#[allow(dead_code)] // Public API methods for library consumers
133impl SdfGrid {
134    /// Create a grid sized to contain mesh with padding.
135    pub fn from_mesh_bounds(
136        mesh: &Mesh,
137        voxel_size: f64,
138        padding: f64,
139        max_voxels: usize,
140    ) -> ShellResult<Self> {
141        let (min, max) = mesh.bounds().ok_or(ShellError::EmptyMesh)?;
142
143        // Add padding to bounds
144        let origin = Point3::new(min.x - padding, min.y - padding, min.z - padding);
145        let max_padded = Point3::new(max.x + padding, max.y + padding, max.z + padding);
146
147        // Compute grid dimensions
148        let extent = max_padded - origin;
149        let dims = [
150            ((extent.x / voxel_size).ceil() as usize).max(1),
151            ((extent.y / voxel_size).ceil() as usize).max(1),
152            ((extent.z / voxel_size).ceil() as usize).max(1),
153        ];
154
155        let total_voxels = dims[0] * dims[1] * dims[2];
156        if total_voxels > max_voxels {
157            return Err(ShellError::GridTooLarge {
158                dims,
159                total: total_voxels,
160                max: max_voxels,
161            });
162        }
163
164        info!(
165            dims = ?dims,
166            total = total_voxels,
167            voxel_size_mm = voxel_size,
168            "Creating SDF grid"
169        );
170
171        Ok(Self {
172            dims,
173            origin,
174            voxel_size,
175            values: vec![0.0; total_voxels],
176            offsets: vec![0.0; total_voxels],
177        })
178    }
179
180    /// Total number of voxels in the grid.
181    #[inline]
182    pub fn total_voxels(&self) -> usize {
183        self.dims[0] * self.dims[1] * self.dims[2]
184    }
185
186    /// Convert 3D grid coordinates to linear index.
187    #[inline]
188    pub fn linearize(&self, x: usize, y: usize, z: usize) -> usize {
189        x + y * self.dims[0] + z * self.dims[0] * self.dims[1]
190    }
191
192    /// Convert linear index to 3D grid coordinates.
193    #[inline]
194    pub fn delinearize(&self, idx: usize) -> [usize; 3] {
195        let z = idx / (self.dims[0] * self.dims[1]);
196        let rem = idx % (self.dims[0] * self.dims[1]);
197        let y = rem / self.dims[0];
198        let x = rem % self.dims[0];
199        [x, y, z]
200    }
201
202    /// Get world position of voxel center.
203    #[inline]
204    pub fn voxel_center(&self, x: usize, y: usize, z: usize) -> Point3<f64> {
205        Point3::new(
206            self.origin.x + (x as f64 + 0.5) * self.voxel_size,
207            self.origin.y + (y as f64 + 0.5) * self.voxel_size,
208            self.origin.z + (z as f64 + 0.5) * self.voxel_size,
209        )
210    }
211
212    /// Compute SDF values using mesh_to_sdf crate (CPU).
213    pub fn compute_sdf(&mut self, mesh: &Mesh) {
214        self.compute_sdf_cpu(mesh);
215    }
216
217    /// Compute SDF values with optional GPU acceleration.
218    ///
219    /// When `use_gpu` is true and the `gpu` feature is enabled, this will
220    /// attempt to use GPU compute shaders. Falls back to CPU if GPU is
221    /// unavailable or the feature is not enabled.
222    pub fn compute_sdf_with_params(&mut self, mesh: &Mesh, params: &SdfOffsetParams) {
223        #[cfg(feature = "gpu")]
224        if params.use_gpu {
225            if self.try_compute_sdf_gpu(mesh) {
226                return;
227            }
228            info!("GPU unavailable or failed, falling back to CPU");
229        }
230
231        #[cfg(not(feature = "gpu"))]
232        if params.use_gpu {
233            info!("GPU feature not enabled, using CPU");
234        }
235
236        self.compute_sdf_cpu(mesh);
237    }
238
239    /// Compute SDF values using mesh_to_sdf crate (CPU implementation).
240    fn compute_sdf_cpu(&mut self, mesh: &Mesh) {
241        use mesh_to_sdf::{Grid, SignMethod, Topology, generate_grid_sdf};
242
243        info!(vertices = mesh.vertices.len(), "Computing SDF (CPU)");
244
245        // Convert Mesh to mesh_to_sdf format
246        let vertices: Vec<[f32; 3]> = mesh
247            .vertices
248            .iter()
249            .map(|v| {
250                [
251                    v.position.x as f32,
252                    v.position.y as f32,
253                    v.position.z as f32,
254                ]
255            })
256            .collect();
257
258        let indices: Vec<u32> = mesh.faces.iter().flat_map(|f| f.iter().copied()).collect();
259
260        // Create grid specification for mesh_to_sdf
261        let grid = Grid::from_bounding_box(
262            &[
263                self.origin.x as f32,
264                self.origin.y as f32,
265                self.origin.z as f32,
266            ],
267            &[
268                (self.origin.x + self.dims[0] as f64 * self.voxel_size) as f32,
269                (self.origin.y + self.dims[1] as f64 * self.voxel_size) as f32,
270                (self.origin.z + self.dims[2] as f64 * self.voxel_size) as f32,
271            ],
272            [self.dims[0], self.dims[1], self.dims[2]],
273        );
274
275        // Generate SDF using Raycast for robust sign determination
276        let sdf_values = generate_grid_sdf(
277            &vertices,
278            Topology::TriangleList(Some(&indices)),
279            &grid,
280            SignMethod::Raycast,
281        );
282
283        self.values = sdf_values;
284
285        debug!(
286            min_sdf = self.values.iter().copied().fold(f32::INFINITY, f32::min),
287            max_sdf = self
288                .values
289                .iter()
290                .copied()
291                .fold(f32::NEG_INFINITY, f32::max),
292            "SDF computed (CPU)"
293        );
294    }
295
296    /// Try to compute SDF using GPU acceleration.
297    ///
298    /// Returns true if GPU computation succeeded, false otherwise.
299    #[cfg(feature = "gpu")]
300    fn try_compute_sdf_gpu(&mut self, mesh: &Mesh) -> bool {
301        use mesh_gpu::{GpuSdfParams, try_compute_sdf_gpu};
302
303        let params = GpuSdfParams {
304            dims: self.dims,
305            origin: [
306                self.origin.x as f32,
307                self.origin.y as f32,
308                self.origin.z as f32,
309            ],
310            voxel_size: self.voxel_size as f32,
311        };
312
313        match try_compute_sdf_gpu(mesh, &params) {
314            Some(result) => {
315                info!(time_ms = result.compute_time_ms, "SDF computed (GPU)");
316                self.values = result.values;
317                true
318            }
319            None => false,
320        }
321    }
322
323    /// Interpolate offset values from mesh vertices into the grid.
324    ///
325    /// Uses inverse distance weighted interpolation from K nearest vertices.
326    pub fn interpolate_offsets(&mut self, mesh: &Mesh, k_neighbors: usize) {
327        use kiddo::KdTree;
328
329        info!(
330            voxels = self.total_voxels(),
331            vertices = mesh.vertices.len(),
332            k = k_neighbors,
333            "Interpolating offsets (building KD-tree)"
334        );
335
336        // Build KD-tree for fast K-nearest-neighbor queries
337        let mut kdtree: KdTree<f64, 3> = KdTree::new();
338        for (i, v) in mesh.vertices.iter().enumerate() {
339            kdtree.add(&[v.position.x, v.position.y, v.position.z], i as u64);
340        }
341
342        // Pre-extract offsets for fast lookup
343        let vertex_offsets: Vec<f32> = mesh
344            .vertices
345            .iter()
346            .map(|v| v.offset.unwrap_or(0.0))
347            .collect();
348
349        info!(
350            "KD-tree built, interpolating {} voxels",
351            self.total_voxels()
352        );
353
354        let [_dim_x, dim_y, dim_z] = self.dims;
355        let voxel_size = self.voxel_size;
356        let origin = self.origin;
357
358        // Parallel interpolation with KD-tree queries
359        // Use ZYX ordering to match mesh_to_sdf's SDF value array layout
360        let offsets: Vec<f32> = (0..self.total_voxels())
361            .into_par_iter()
362            .map(|idx| {
363                // Delinearize using ZYX order (Z varies fastest) to match mesh_to_sdf
364                let z = idx % dim_z;
365                let y = (idx / dim_z) % dim_y;
366                let x = idx / (dim_y * dim_z);
367
368                // Voxel center in world coordinates
369                let voxel_pos = nalgebra::Point3::new(
370                    origin.x + (x as f64 + 0.5) * voxel_size,
371                    origin.y + (y as f64 + 0.5) * voxel_size,
372                    origin.z + (z as f64 + 0.5) * voxel_size,
373                );
374
375                // Query K nearest neighbors from KD-tree
376                let query = [voxel_pos.x, voxel_pos.y, voxel_pos.z];
377                let nearest = kdtree.nearest_n::<kiddo::SquaredEuclidean>(&query, k_neighbors);
378
379                // Inverse distance weighted interpolation
380                let mut total_weight = 0.0;
381                let mut weighted_offset = 0.0;
382                for neighbor in nearest {
383                    let dist = neighbor.distance.sqrt();
384                    let w = 1.0 / (dist + 0.001);
385                    total_weight += w;
386                    weighted_offset += w * vertex_offsets[neighbor.item as usize] as f64;
387                }
388
389                if total_weight > 0.0 {
390                    (weighted_offset / total_weight) as f32
391                } else {
392                    0.0
393                }
394            })
395            .collect();
396
397        self.offsets = offsets;
398
399        debug!(
400            min_offset = self.offsets.iter().copied().fold(f32::INFINITY, f32::min),
401            max_offset = self
402                .offsets
403                .iter()
404                .copied()
405                .fold(f32::NEG_INFINITY, f32::max),
406            "Offsets interpolated"
407        );
408    }
409
410    /// Apply variable offset by adjusting SDF with offset values.
411    ///
412    /// After this, extracting isosurface at SDF=0 gives the variable offset surface.
413    pub fn apply_variable_offset(&mut self) {
414        info!("Applying variable offset to SDF");
415
416        // Subtract offset to expand surface (positive offset = outward)
417        self.values
418            .par_iter_mut()
419            .zip(self.offsets.par_iter())
420            .for_each(|(sdf, offset)| {
421                *sdf -= *offset;
422            });
423
424        debug!(
425            min_adjusted = self.values.iter().copied().fold(f32::INFINITY, f32::min),
426            max_adjusted = self
427                .values
428                .iter()
429                .copied()
430                .fold(f32::NEG_INFINITY, f32::max),
431            "Variable offset applied"
432        );
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439    use mesh_repair::Vertex;
440
441    fn create_unit_cube() -> Mesh {
442        let mut mesh = Mesh::new();
443
444        // Cube vertices (0-10mm)
445        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
446        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
447        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 0.0));
448        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 0.0));
449        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 10.0));
450        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 10.0));
451        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 10.0));
452        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 10.0));
453
454        // Faces
455        mesh.faces.push([0, 1, 2]);
456        mesh.faces.push([0, 2, 3]);
457        mesh.faces.push([4, 6, 5]);
458        mesh.faces.push([4, 7, 6]);
459        mesh.faces.push([0, 5, 1]);
460        mesh.faces.push([0, 4, 5]);
461        mesh.faces.push([2, 7, 3]);
462        mesh.faces.push([2, 6, 7]);
463        mesh.faces.push([0, 3, 7]);
464        mesh.faces.push([0, 7, 4]);
465        mesh.faces.push([1, 5, 6]);
466        mesh.faces.push([1, 6, 2]);
467
468        mesh
469    }
470
471    #[test]
472    fn test_grid_construction() {
473        let mesh = create_unit_cube();
474        let grid = SdfGrid::from_mesh_bounds(&mesh, 1.0, 5.0, 1_000_000).unwrap();
475
476        // Grid should be 20x20x20 (10mm cube + 5mm padding on each side = 20mm)
477        assert_eq!(grid.dims[0], 20);
478        assert_eq!(grid.dims[1], 20);
479        assert_eq!(grid.dims[2], 20);
480        assert_eq!(grid.total_voxels(), 8000);
481    }
482
483    #[test]
484    fn test_grid_too_large() {
485        let mesh = create_unit_cube();
486        let result = SdfGrid::from_mesh_bounds(&mesh, 0.1, 5.0, 1000);
487        assert!(result.is_err());
488    }
489
490    #[test]
491    fn test_sdf_offset_params_default() {
492        let params = SdfOffsetParams::default();
493        assert!(!params.adaptive_resolution);
494        assert_eq!(params.voxel_size_mm, 0.75);
495    }
496
497    #[test]
498    fn test_sdf_offset_params_adaptive() {
499        let params = SdfOffsetParams::adaptive();
500        assert!(params.adaptive_resolution);
501        assert!(params.coarse_voxel_size_mm() > params.voxel_size_mm);
502    }
503
504    #[test]
505    fn test_sdf_offset_params_presets() {
506        let hq = SdfOffsetParams::adaptive_high_quality();
507        let large = SdfOffsetParams::adaptive_large_mesh();
508
509        // High quality should have finer voxels
510        assert!(hq.voxel_size_mm < large.voxel_size_mm);
511
512        // Both should have adaptive enabled
513        assert!(hq.adaptive_resolution);
514        assert!(large.adaptive_resolution);
515    }
516
517    #[test]
518    fn test_coarse_voxel_size() {
519        let params = SdfOffsetParams {
520            voxel_size_mm: 1.0,
521            coarse_voxel_multiplier: 4.0,
522            ..Default::default()
523        };
524        assert_eq!(params.coarse_voxel_size_mm(), 4.0);
525    }
526
527    #[test]
528    fn test_sdf_offset_params_with_gpu() {
529        let params = SdfOffsetParams::default().with_gpu();
530        assert!(params.use_gpu);
531        assert!(!params.adaptive_resolution); // Should preserve other settings
532    }
533
534    #[test]
535    fn test_sdf_offset_params_gpu_default() {
536        let params = SdfOffsetParams::default();
537        assert!(!params.use_gpu); // GPU disabled by default
538    }
539}