Skip to main content

oxiphysics_gpu/sdf_compute/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use rayon::prelude::*;
7
8use super::types::{SdfGrid, SphereTraceResult, Triangle};
9
10/// One pass of the fast sweeping method.
11pub fn fast_sweeping_update(grid: &mut SdfGrid) {
12    let nx = grid.nx;
13    let ny = grid.ny;
14    let nz = grid.nz;
15    let dx = grid.dx;
16    for i in 0..nx {
17        for j in 0..ny {
18            for k in 0..nz {
19                let cur = grid.get(i, j, k);
20                let mut best = cur;
21                if i > 0 {
22                    let candidate = grid.get(i - 1, j, k) + dx;
23                    if candidate < best {
24                        best = candidate;
25                    }
26                }
27                if i + 1 < nx {
28                    let candidate = grid.get(i + 1, j, k) + dx;
29                    if candidate < best {
30                        best = candidate;
31                    }
32                }
33                if j > 0 {
34                    let candidate = grid.get(i, j - 1, k) + dx;
35                    if candidate < best {
36                        best = candidate;
37                    }
38                }
39                if j + 1 < ny {
40                    let candidate = grid.get(i, j + 1, k) + dx;
41                    if candidate < best {
42                        best = candidate;
43                    }
44                }
45                if k > 0 {
46                    let candidate = grid.get(i, j, k - 1) + dx;
47                    if candidate < best {
48                        best = candidate;
49                    }
50                }
51                if k + 1 < nz {
52                    let candidate = grid.get(i, j, k + 1) + dx;
53                    if candidate < best {
54                        best = candidate;
55                    }
56                }
57                if best < cur {
58                    grid.set(i, j, k, best);
59                }
60            }
61        }
62    }
63}
64/// Compute the union of two SDFs: `min(a, b)` pointwise.
65pub fn union_sdf(a: &SdfGrid, b: &SdfGrid) -> SdfGrid {
66    assert_eq!(a.nx, b.nx, "union_sdf: nx mismatch");
67    assert_eq!(a.ny, b.ny, "union_sdf: ny mismatch");
68    assert_eq!(a.nz, b.nz, "union_sdf: nz mismatch");
69    let values: Vec<f64> = a
70        .values
71        .par_iter()
72        .zip(b.values.par_iter())
73        .map(|(&av, &bv)| av.min(bv))
74        .collect();
75    SdfGrid {
76        nx: a.nx,
77        ny: a.ny,
78        nz: a.nz,
79        dx: a.dx,
80        origin: a.origin,
81        values,
82    }
83}
84/// Compute the intersection of two SDFs: `max(a, b)` pointwise.
85pub fn intersection_sdf(a: &SdfGrid, b: &SdfGrid) -> SdfGrid {
86    assert_eq!(a.nx, b.nx, "intersection_sdf: nx mismatch");
87    assert_eq!(a.ny, b.ny, "intersection_sdf: ny mismatch");
88    assert_eq!(a.nz, b.nz, "intersection_sdf: nz mismatch");
89    let values: Vec<f64> = a
90        .values
91        .par_iter()
92        .zip(b.values.par_iter())
93        .map(|(&av, &bv)| av.max(bv))
94        .collect();
95    SdfGrid {
96        nx: a.nx,
97        ny: a.ny,
98        nz: a.nz,
99        dx: a.dx,
100        origin: a.origin,
101        values,
102    }
103}
104/// Compute the subtraction of SDF b from SDF a: `max(a, -b)` pointwise.
105///
106/// The result is the region inside `a` but outside `b`.
107pub fn subtraction_sdf(a: &SdfGrid, b: &SdfGrid) -> SdfGrid {
108    assert_eq!(a.nx, b.nx, "subtraction_sdf: nx mismatch");
109    assert_eq!(a.ny, b.ny, "subtraction_sdf: ny mismatch");
110    assert_eq!(a.nz, b.nz, "subtraction_sdf: nz mismatch");
111    let values: Vec<f64> = a
112        .values
113        .par_iter()
114        .zip(b.values.par_iter())
115        .map(|(&av, &bv)| av.max(-bv))
116        .collect();
117    SdfGrid {
118        nx: a.nx,
119        ny: a.ny,
120        nz: a.nz,
121        dx: a.dx,
122        origin: a.origin,
123        values,
124    }
125}
126/// Compute the shell SDF: `|sdf(p)| - thickness/2`.
127pub fn shell_sdf(grid: &SdfGrid, thickness: f64) -> SdfGrid {
128    let half = thickness / 2.0;
129    let values: Vec<f64> = grid.values.par_iter().map(|&v| v.abs() - half).collect();
130    SdfGrid {
131        nx: grid.nx,
132        ny: grid.ny,
133        nz: grid.nz,
134        dx: grid.dx,
135        origin: grid.origin,
136        values,
137    }
138}
139/// Smooth union of two SDFs using polynomial smoothing.
140///
141/// `k` controls the smoothing radius. Larger `k` means sharper transition.
142pub fn smooth_union_sdf(a: &SdfGrid, b: &SdfGrid, k: f64) -> SdfGrid {
143    assert_eq!(a.nx, b.nx);
144    assert_eq!(a.ny, b.ny);
145    assert_eq!(a.nz, b.nz);
146    let values: Vec<f64> = a
147        .values
148        .par_iter()
149        .zip(b.values.par_iter())
150        .map(|(&av, &bv)| {
151            let h = (0.5 + 0.5 * (bv - av) / k).clamp(0.0, 1.0);
152            bv * (1.0 - h) + av * h - k * h * (1.0 - h)
153        })
154        .collect();
155    SdfGrid {
156        nx: a.nx,
157        ny: a.ny,
158        nz: a.nz,
159        dx: a.dx,
160        origin: a.origin,
161        values,
162    }
163}
164/// Perform sphere tracing (ray marching) against an SDF grid.
165///
166/// Traces a ray from `origin` in `direction` (must be unit-length)
167/// until the SDF value is below `surface_threshold` or `max_t` is reached.
168pub fn sphere_trace(
169    grid: &SdfGrid,
170    ray_origin: [f64; 3],
171    ray_direction: [f64; 3],
172    max_t: f64,
173    max_iterations: usize,
174    surface_threshold: f64,
175) -> SphereTraceResult {
176    let mut t = 0.0;
177    let mut pos = ray_origin;
178    for iter in 0..max_iterations {
179        let sdf_val = match grid.sample(pos) {
180            Some(v) => v,
181            None => {
182                return SphereTraceResult {
183                    hit: false,
184                    position: pos,
185                    t,
186                    iterations: iter,
187                };
188            }
189        };
190        if sdf_val < surface_threshold {
191            return SphereTraceResult {
192                hit: true,
193                position: pos,
194                t,
195                iterations: iter,
196            };
197        }
198        t += sdf_val;
199        if t > max_t {
200            return SphereTraceResult {
201                hit: false,
202                position: pos,
203                t,
204                iterations: iter,
205            };
206        }
207        pos = [
208            ray_origin[0] + ray_direction[0] * t,
209            ray_origin[1] + ray_direction[1] * t,
210            ray_origin[2] + ray_direction[2] * t,
211        ];
212    }
213    SphereTraceResult {
214        hit: false,
215        position: pos,
216        t,
217        iterations: max_iterations,
218    }
219}
220/// Convert a triangle mesh to an SDF by computing the distance from
221/// each grid cell to the nearest triangle.
222///
223/// This is a brute-force O(N*M) approach where N is grid cells and
224/// M is triangles. For large meshes, use spatial acceleration.
225///
226/// `vertices` - vertex positions
227/// `triangles` - triangle indices (3 per triangle)
228pub fn mesh_to_sdf(grid: &mut SdfGrid, vertices: &[[f64; 3]], triangles: &[[usize; 3]]) {
229    let ny = grid.ny;
230    let nz = grid.nz;
231    let dx = grid.dx;
232    let origin = grid.origin;
233    grid.values.par_iter_mut().enumerate().for_each(|(idx, v)| {
234        let i = idx / (ny * nz);
235        let j = (idx / nz) % ny;
236        let k = idx % nz;
237        let p = [
238            origin[0] + (i as f64 + 0.5) * dx,
239            origin[1] + (j as f64 + 0.5) * dx,
240            origin[2] + (k as f64 + 0.5) * dx,
241        ];
242        let mut min_dist = f64::MAX;
243        for tri in triangles {
244            let a = vertices[tri[0]];
245            let b = vertices[tri[1]];
246            let c = vertices[tri[2]];
247            let dist = point_triangle_distance(&p, &a, &b, &c);
248            if dist < min_dist {
249                min_dist = dist;
250            }
251        }
252        *v = min_dist;
253    });
254}
255/// Compute the distance from a point to a triangle.
256pub(super) fn point_triangle_distance(
257    p: &[f64; 3],
258    a: &[f64; 3],
259    b: &[f64; 3],
260    c: &[f64; 3],
261) -> f64 {
262    let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
263    let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
264    let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
265    let d1 = dot3(&ab, &ap);
266    let d2 = dot3(&ac, &ap);
267    if d1 <= 0.0 && d2 <= 0.0 {
268        return dist3(p, a);
269    }
270    let bp = [p[0] - b[0], p[1] - b[1], p[2] - b[2]];
271    let d3 = dot3(&ab, &bp);
272    let d4 = dot3(&ac, &bp);
273    if d3 >= 0.0 && d4 <= d3 {
274        return dist3(p, b);
275    }
276    let vc = d1 * d4 - d3 * d2;
277    if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
278        let v = d1 / (d1 - d3);
279        let proj = [a[0] + ab[0] * v, a[1] + ab[1] * v, a[2] + ab[2] * v];
280        return dist3(p, &proj);
281    }
282    let cp = [p[0] - c[0], p[1] - c[1], p[2] - c[2]];
283    let d5 = dot3(&ab, &cp);
284    let d6 = dot3(&ac, &cp);
285    if d6 >= 0.0 && d5 <= d6 {
286        return dist3(p, c);
287    }
288    let vb = d5 * d2 - d1 * d6;
289    if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
290        let w = d2 / (d2 - d6);
291        let proj = [a[0] + ac[0] * w, a[1] + ac[1] * w, a[2] + ac[2] * w];
292        return dist3(p, &proj);
293    }
294    let va = d3 * d6 - d5 * d4;
295    if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
296        let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
297        let bc = [c[0] - b[0], c[1] - b[1], c[2] - b[2]];
298        let proj = [b[0] + bc[0] * w, b[1] + bc[1] * w, b[2] + bc[2] * w];
299        return dist3(p, &proj);
300    }
301    let denom = 1.0 / (va + vb + vc);
302    let v = vb * denom;
303    let w = vc * denom;
304    let proj = [
305        a[0] + ab[0] * v + ac[0] * w,
306        a[1] + ab[1] * v + ac[1] * w,
307        a[2] + ab[2] * v + ac[2] * w,
308    ];
309    dist3(p, &proj)
310}
311pub(super) fn dot3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
312    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
313}
314pub(super) fn dist3(a: &[f64; 3], b: &[f64; 3]) -> f64 {
315    let dx = a[0] - b[0];
316    let dy = a[1] - b[1];
317    let dz = a[2] - b[2];
318    (dx * dx + dy * dy + dz * dz).sqrt()
319}
320/// Evaluate an SDF at a batch of query points.
321///
322/// Returns the SDF value at each query point, or `f64::MAX` if outside the grid.
323pub fn evaluate_sdf_batch(grid: &SdfGrid, points: &[[f64; 3]]) -> Vec<f64> {
324    points
325        .par_iter()
326        .map(|&p| grid.sample(p).unwrap_or(f64::MAX))
327        .collect()
328}
329/// Compute the SDF gradient at a batch of query points.
330pub fn gradient_sdf_batch(grid: &SdfGrid, points: &[[f64; 3]]) -> Vec<[f64; 3]> {
331    points
332        .par_iter()
333        .map(|&p| grid.gradient_at_point(p).unwrap_or([0.0; 3]))
334        .collect()
335}
336/// Count the number of cells where the SDF is negative (inside the surface).
337pub fn count_interior_cells(grid: &SdfGrid) -> usize {
338    grid.values.par_iter().filter(|&&v| v < 0.0).count()
339}
340/// Compute the approximate volume enclosed by the zero level-set.
341///
342/// Simply counts interior cells and multiplies by cell volume.
343pub fn approximate_volume(grid: &SdfGrid) -> f64 {
344    let count = count_interior_cells(grid);
345    count as f64 * grid.dx * grid.dx * grid.dx
346}
347/// Marching cubes edge table (12 edges per cube).
348/// Each entry encodes which edges are intersected for a given vertex mask.
349#[rustfmt::skip]
350pub(super) const MC_EDGE_TABLE: [u16; 256] = [
351    0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, 0x80c, 0x905, 0xa0f, 0xb06,
352    0xc0a, 0xd03, 0xe09, 0xf00, 0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
353    0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, 0x230, 0x339, 0x033, 0x13a,
354    0x636, 0x73f, 0x435, 0x53c, 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
355    0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac, 0xbac, 0xaa5, 0x9af, 0x8a6,
356    0xfaa, 0xea3, 0xda9, 0xca0, 0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
357    0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, 0x5f0, 0x4f9, 0x7f3, 0x6fa,
358    0x1f6, 0x0ff, 0x3f5, 0x2fc, 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
359    0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c, 0xe5c, 0xf55, 0xc5f, 0xd56,
360    0xa5a, 0xb53, 0x859, 0x950, 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
361    0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, 0x8c0, 0x9c9, 0xac3, 0xbca,
362    0xcc6, 0xdcf, 0xec5, 0xfcc, 0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
363    0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, 0x15c, 0x055, 0x35f, 0x256,
364    0x55a, 0x453, 0x759, 0x650, 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
365    0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, 0xb60, 0xa69, 0x963, 0x86a,
366    0xf66, 0xe6f, 0xd65, 0xc6c, 0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
367    0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, 0x4ac, 0x5a5, 0x6af, 0x7a6,
368    0x0aa, 0x1a3, 0x2a9, 0x3a0, 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x835, 0xb3f, 0xa36,
369    0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230, 0xe90, 0xf99, 0xc93, 0xd9a,
370    0xa96, 0xb9f, 0x895, 0x99c, 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
371    0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, 0x70c, 0x605, 0x50f, 0x406,
372    0x30a, 0x203, 0x109, 0x000,
373];
374/// Interpolate the position of an edge intersection given two corner SDF values.
375#[inline]
376pub(super) fn interpolate_vertex(
377    p1: [f64; 3],
378    p2: [f64; 3],
379    val1: f64,
380    val2: f64,
381    iso: f64,
382) -> [f64; 3] {
383    if (val2 - val1).abs() < 1e-15 {
384        return p1;
385    }
386    let t = (iso - val1) / (val2 - val1);
387    [
388        p1[0] + t * (p2[0] - p1[0]),
389        p1[1] + t * (p2[1] - p1[1]),
390        p1[2] + t * (p2[2] - p1[2]),
391    ]
392}
393/// Extract a triangle mesh from the SDF grid at the given isovalue
394/// using the marching cubes algorithm (simplified 3-case variant).
395///
396/// Returns a list of triangles. For a signed distance field, `isovalue = 0.0`
397/// extracts the zero level-set (the surface).
398pub fn marching_cubes(grid: &SdfGrid, isovalue: f64) -> Vec<Triangle> {
399    let nx = grid.nx;
400    let ny = grid.ny;
401    let nz = grid.nz;
402    if nx < 2 || ny < 2 || nz < 2 {
403        return Vec::new();
404    }
405    let mut triangles = Vec::new();
406    for i in 0..nx - 1 {
407        for j in 0..ny - 1 {
408            for k in 0..nz - 1 {
409                let corners: [[f64; 3]; 8] = [
410                    grid.world_pos(i, j, k),
411                    grid.world_pos(i + 1, j, k),
412                    grid.world_pos(i + 1, j + 1, k),
413                    grid.world_pos(i, j + 1, k),
414                    grid.world_pos(i, j, k + 1),
415                    grid.world_pos(i + 1, j, k + 1),
416                    grid.world_pos(i + 1, j + 1, k + 1),
417                    grid.world_pos(i, j + 1, k + 1),
418                ];
419                let vals: [f64; 8] = [
420                    grid.get(i, j, k),
421                    grid.get(i + 1, j, k),
422                    grid.get(i + 1, j + 1, k),
423                    grid.get(i, j + 1, k),
424                    grid.get(i, j, k + 1),
425                    grid.get(i + 1, j, k + 1),
426                    grid.get(i + 1, j + 1, k + 1),
427                    grid.get(i, j + 1, k + 1),
428                ];
429                let mut cube_idx = 0u8;
430                for c in 0..8 {
431                    if vals[c] < isovalue {
432                        cube_idx |= 1 << c;
433                    }
434                }
435                let edge_flags = MC_EDGE_TABLE[cube_idx as usize];
436                if edge_flags == 0 {
437                    continue;
438                }
439                let mut verts = [[0.0f64; 3]; 12];
440                if edge_flags & 0x001 != 0 {
441                    verts[0] =
442                        interpolate_vertex(corners[0], corners[1], vals[0], vals[1], isovalue);
443                }
444                if edge_flags & 0x002 != 0 {
445                    verts[1] =
446                        interpolate_vertex(corners[1], corners[2], vals[1], vals[2], isovalue);
447                }
448                if edge_flags & 0x004 != 0 {
449                    verts[2] =
450                        interpolate_vertex(corners[2], corners[3], vals[2], vals[3], isovalue);
451                }
452                if edge_flags & 0x008 != 0 {
453                    verts[3] =
454                        interpolate_vertex(corners[3], corners[0], vals[3], vals[0], isovalue);
455                }
456                if edge_flags & 0x010 != 0 {
457                    verts[4] =
458                        interpolate_vertex(corners[4], corners[5], vals[4], vals[5], isovalue);
459                }
460                if edge_flags & 0x020 != 0 {
461                    verts[5] =
462                        interpolate_vertex(corners[5], corners[6], vals[5], vals[6], isovalue);
463                }
464                if edge_flags & 0x040 != 0 {
465                    verts[6] =
466                        interpolate_vertex(corners[6], corners[7], vals[6], vals[7], isovalue);
467                }
468                if edge_flags & 0x080 != 0 {
469                    verts[7] =
470                        interpolate_vertex(corners[7], corners[4], vals[7], vals[4], isovalue);
471                }
472                if edge_flags & 0x100 != 0 {
473                    verts[8] =
474                        interpolate_vertex(corners[0], corners[4], vals[0], vals[4], isovalue);
475                }
476                if edge_flags & 0x200 != 0 {
477                    verts[9] =
478                        interpolate_vertex(corners[1], corners[5], vals[1], vals[5], isovalue);
479                }
480                if edge_flags & 0x400 != 0 {
481                    verts[10] =
482                        interpolate_vertex(corners[2], corners[6], vals[2], vals[6], isovalue);
483                }
484                if edge_flags & 0x800 != 0 {
485                    verts[11] =
486                        interpolate_vertex(corners[3], corners[7], vals[3], vals[7], isovalue);
487                }
488                let active: Vec<[f64; 3]> = (0..12)
489                    .filter(|&e| edge_flags & (1 << e) != 0)
490                    .map(|e| verts[e])
491                    .collect();
492                if active.len() >= 3 {
493                    for tri_idx in 1..active.len() - 1 {
494                        triangles.push(Triangle {
495                            v: [active[0], active[tri_idx], active[tri_idx + 1]],
496                        });
497                    }
498                }
499            }
500        }
501    }
502    triangles
503}
504/// Count the number of triangles in the extracted mesh.
505pub fn mesh_triangle_count(grid: &SdfGrid, isovalue: f64) -> usize {
506    marching_cubes(grid, isovalue).len()
507}
508/// Apply a 3×3×3 box-filter (Gaussian-like) convolution to an SDF grid.
509///
510/// Each cell is replaced by the weighted average of its 3×3×3 neighbourhood.
511/// `sigma` controls the Gaussian kernel width (in cells).
512pub fn sdf_gaussian_blur(grid: &SdfGrid, sigma: f64) -> SdfGrid {
513    let nx = grid.nx;
514    let ny = grid.ny;
515    let nz = grid.nz;
516    let mut kernel = [[[0.0f64; 3]; 3]; 3];
517    let mut kernel_sum = 0.0;
518    let s2 = 2.0 * sigma * sigma;
519    for di in -1i32..=1 {
520        for dj in -1i32..=1 {
521            for dk in -1i32..=1 {
522                let r2 = (di * di + dj * dj + dk * dk) as f64;
523                let w = (-r2 / s2).exp();
524                kernel[(di + 1) as usize][(dj + 1) as usize][(dk + 1) as usize] = w;
525                kernel_sum += w;
526            }
527        }
528    }
529    let mut out = SdfGrid::new(nx, ny, nz, grid.dx, grid.origin);
530    for i in 0..nx {
531        for j in 0..ny {
532            for k in 0..nz {
533                let mut acc = 0.0;
534                let mut wt = 0.0;
535                for di in -1i32..=1 {
536                    for dj in -1i32..=1 {
537                        for dk in -1i32..=1 {
538                            let ni = i as i32 + di;
539                            let nj = j as i32 + dj;
540                            let nk = k as i32 + dk;
541                            if ni >= 0
542                                && ni < nx as i32
543                                && nj >= 0
544                                && nj < ny as i32
545                                && nk >= 0
546                                && nk < nz as i32
547                            {
548                                let w =
549                                    kernel[(di + 1) as usize][(dj + 1) as usize][(dk + 1) as usize];
550                                acc += w * grid.get(ni as usize, nj as usize, nk as usize);
551                                wt += w;
552                            }
553                        }
554                    }
555                }
556                let v = if wt > 1e-15 {
557                    acc / wt
558                } else {
559                    grid.get(i, j, k)
560                };
561                out.set(i, j, k, v);
562            }
563        }
564    }
565    let _ = kernel_sum;
566    out
567}
568/// Laplacian (sharpening) convolution on the SDF.
569///
570/// Returns grid + `amount` * Laplacian(grid), which sharpens features.
571pub fn sdf_laplacian_sharpen(grid: &SdfGrid, amount: f64) -> SdfGrid {
572    let nx = grid.nx;
573    let ny = grid.ny;
574    let nz = grid.nz;
575    let inv_dx2 = 1.0 / (grid.dx * grid.dx);
576    let mut out = SdfGrid::new(nx, ny, nz, grid.dx, grid.origin);
577    for i in 0..nx {
578        for j in 0..ny {
579            for k in 0..nz {
580                let v = grid.get(i, j, k);
581                let lx = if i > 0 && i + 1 < nx {
582                    (grid.get(i + 1, j, k) - 2.0 * v + grid.get(i - 1, j, k)) * inv_dx2
583                } else {
584                    0.0
585                };
586                let ly = if j > 0 && j + 1 < ny {
587                    (grid.get(i, j + 1, k) - 2.0 * v + grid.get(i, j - 1, k)) * inv_dx2
588                } else {
589                    0.0
590                };
591                let lz = if k > 0 && k + 1 < nz {
592                    (grid.get(i, j, k + 1) - 2.0 * v + grid.get(i, j, k - 1)) * inv_dx2
593                } else {
594                    0.0
595                };
596                out.set(i, j, k, v + amount * (lx + ly + lz));
597            }
598        }
599    }
600    out
601}
602/// SDF dilation: offset the surface outward by `offset` units.
603///
604/// Simply subtracts `offset` from all SDF values.
605/// Positive offset = dilation (expand solid), negative = erosion.
606pub fn sdf_dilate(grid: &SdfGrid, offset: f64) -> SdfGrid {
607    let values: Vec<f64> = grid.values.par_iter().map(|&v| v - offset).collect();
608    SdfGrid {
609        nx: grid.nx,
610        ny: grid.ny,
611        nz: grid.nz,
612        dx: grid.dx,
613        origin: grid.origin,
614        values,
615    }
616}
617/// SDF erosion: offset the surface inward by `offset` units.
618///
619/// Equivalent to `sdf_dilate(grid, -offset)`.
620pub fn sdf_erode(grid: &SdfGrid, offset: f64) -> SdfGrid {
621    sdf_dilate(grid, -offset)
622}
623/// SDF morphological opening: erode then dilate.
624///
625/// Removes small protrusions (blobs smaller than `offset`).
626pub fn sdf_open(grid: &SdfGrid, offset: f64) -> SdfGrid {
627    let eroded = sdf_erode(grid, offset);
628    sdf_dilate(&eroded, offset)
629}
630/// SDF morphological closing: dilate then erode.
631///
632/// Fills small holes (gaps smaller than `offset`).
633pub fn sdf_close(grid: &SdfGrid, offset: f64) -> SdfGrid {
634    let dilated = sdf_dilate(grid, offset);
635    sdf_erode(&dilated, offset)
636}
637/// Signed distance field offset: produce an iso-surface at `offset`.
638///
639/// The new surface is the locus of points where the original SDF = `offset`.
640pub fn sdf_offset_surface(grid: &SdfGrid, offset: f64) -> SdfGrid {
641    let values: Vec<f64> = grid.values.par_iter().map(|&v| v - offset).collect();
642    SdfGrid {
643        nx: grid.nx,
644        ny: grid.ny,
645        nz: grid.nz,
646        dx: grid.dx,
647        origin: grid.origin,
648        values,
649    }
650}
651/// Iterative Laplacian smoothing of the SDF.
652///
653/// Applies `n_iterations` of a simple diffusion smoother.
654/// `dt` is the pseudo-time step (small values → mild smoothing).
655pub fn sdf_laplacian_smooth(grid: &SdfGrid, n_iterations: usize, dt: f64) -> SdfGrid {
656    let mut current = SdfGrid {
657        nx: grid.nx,
658        ny: grid.ny,
659        nz: grid.nz,
660        dx: grid.dx,
661        origin: grid.origin,
662        values: grid.values.clone(),
663    };
664    for _ in 0..n_iterations {
665        let sharpened = sdf_laplacian_sharpen(&current, -dt);
666        current = sharpened;
667    }
668    current
669}
670/// Mean-curvature smoothing of the SDF (simplified).
671///
672/// Uses the divergence of the gradient (Laplacian as proxy for mean curvature).
673/// `step` is the smoothing step size.
674pub fn sdf_mean_curvature_smooth(grid: &SdfGrid, step: f64) -> SdfGrid {
675    sdf_laplacian_smooth(grid, 1, step)
676}
677#[cfg(test)]
678mod tests {
679    use super::*;
680    fn make_sphere_grid(nx: usize, dx: f64, center: [f64; 3], radius: f64) -> SdfGrid {
681        let origin = [0.0; 3];
682        let mut g = SdfGrid::new(nx, nx, nx, dx, origin);
683        g.compute_sphere_sdf(center, radius);
684        g
685    }
686    #[test]
687    fn test_sphere_center_is_negative_radius() {
688        let n = 21usize;
689        let dx = 0.1;
690        let radius = 0.4;
691        let mid = (n / 2) as f64 + 0.5;
692        let center = [mid * dx, mid * dx, mid * dx];
693        let g = make_sphere_grid(n, dx, center, radius);
694        let c = n / 2;
695        let sdf_val = g.get(c, c, c);
696        assert!(
697            (sdf_val - (-radius)).abs() < dx,
698            "centre value {sdf_val} should be close to -{radius}"
699        );
700    }
701    #[test]
702    fn test_box_far_outside_positive() {
703        let n = 11usize;
704        let dx = 0.1;
705        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
706        let box_center = [0.55, 0.55, 0.55];
707        let half_extents = [0.1, 0.1, 0.1];
708        g.compute_box_sdf(box_center, half_extents);
709        let v = g.get(0, 0, 0);
710        assert!(
711            v > 0.0,
712            "far-outside cell should have positive SDF, got {v}"
713        );
714    }
715    #[test]
716    fn test_gradient_on_sphere_surface_is_unit() {
717        let n = 41usize;
718        let dx = 0.05;
719        let radius = 0.5;
720        let mid = (n / 2) as f64 + 0.5;
721        let center = [mid * dx, mid * dx, mid * dx];
722        let g = make_sphere_grid(n, dx, center, radius);
723        let c = n / 2;
724        let surface_i = c + (radius / dx) as usize;
725        let grad = g.gradient_at(surface_i, c, c);
726        let mag = (grad[0].powi(2) + grad[1].powi(2) + grad[2].powi(2)).sqrt();
727        assert!(
728            (mag - 1.0).abs() < 0.1,
729            "gradient magnitude should be close to 1.0, got {mag}"
730        );
731        assert!(grad[0] > 0.5, "gradient should point outward, got {grad:?}");
732    }
733    #[test]
734    fn test_union_sdf_inside_either() {
735        let n = 21usize;
736        let dx = 0.1;
737        let radius = 0.3;
738        let origin = [0.0; 3];
739        let c_float = (n / 2) as f64 + 0.5;
740        let center_a = [(c_float - 3.0) * dx, c_float * dx, c_float * dx];
741        let center_b = [(c_float + 3.0) * dx, c_float * dx, c_float * dx];
742        let mut ga = SdfGrid::new(n, n, n, dx, origin);
743        ga.compute_sphere_sdf(center_a, radius);
744        let mut gb = SdfGrid::new(n, n, n, dx, origin);
745        gb.compute_sphere_sdf(center_b, radius);
746        let u = union_sdf(&ga, &gb);
747        let cy = n / 2;
748        let cz = n / 2;
749        let ia = n / 2 - 3;
750        assert!(
751            u.get(ia, cy, cz) < 0.0,
752            "inside sphere A should be negative in union"
753        );
754        let ib = n / 2 + 3;
755        assert!(
756            u.get(ib, cy, cz) < 0.0,
757            "inside sphere B should be negative in union"
758        );
759    }
760    #[test]
761    fn test_total_cells() {
762        let g = SdfGrid::new(4, 5, 6, 0.1, [0.0; 3]);
763        assert_eq!(g.total_cells(), 4 * 5 * 6);
764    }
765    /// Subtracting a large sphere from a small sphere should produce
766    /// positive values everywhere.
767    #[test]
768    fn test_subtraction_sdf() {
769        let n = 11usize;
770        let dx = 0.2;
771        let origin = [0.0; 3];
772        let center = [1.1, 1.1, 1.1];
773        let mut small = SdfGrid::new(n, n, n, dx, origin);
774        small.compute_sphere_sdf(center, 0.3);
775        let mut large = SdfGrid::new(n, n, n, dx, origin);
776        large.compute_sphere_sdf(center, 0.5);
777        let result = subtraction_sdf(&small, &large);
778        let c = n / 2;
779        assert!(
780            result.get(c, c, c) > 0.0,
781            "subtraction centre should be positive, got {}",
782            result.get(c, c, c)
783        );
784    }
785    /// Intersection of two overlapping spheres should be smaller than either.
786    #[test]
787    fn test_intersection_sdf() {
788        let n = 21usize;
789        let dx = 0.1;
790        let origin = [0.0; 3];
791        let radius = 0.5;
792        let c = (n / 2) as f64 + 0.5;
793        let center_a = [(c - 1.0) * dx, c * dx, c * dx];
794        let center_b = [(c + 1.0) * dx, c * dx, c * dx];
795        let mut ga = SdfGrid::new(n, n, n, dx, origin);
796        ga.compute_sphere_sdf(center_a, radius);
797        let mut gb = SdfGrid::new(n, n, n, dx, origin);
798        gb.compute_sphere_sdf(center_b, radius);
799        let inter = intersection_sdf(&ga, &gb);
800        let mid = n / 2;
801        let val = inter.get(mid, mid, mid);
802        assert!(
803            val < 0.0,
804            "midpoint of intersection should be inside, got {val}"
805        );
806    }
807    /// Smooth union should produce smaller values than min at the seam.
808    #[test]
809    fn test_smooth_union() {
810        let n = 11usize;
811        let dx = 0.2;
812        let origin = [0.0; 3];
813        let radius = 0.3;
814        let c = (n / 2) as f64 + 0.5;
815        let center_a = [(c - 2.0) * dx, c * dx, c * dx];
816        let center_b = [(c + 2.0) * dx, c * dx, c * dx];
817        let mut ga = SdfGrid::new(n, n, n, dx, origin);
818        ga.compute_sphere_sdf(center_a, radius);
819        let mut gb = SdfGrid::new(n, n, n, dx, origin);
820        gb.compute_sphere_sdf(center_b, radius);
821        let su = smooth_union_sdf(&ga, &gb, 0.5);
822        let u = union_sdf(&ga, &gb);
823        let mid = n / 2;
824        assert!(
825            su.get(mid, mid, mid) <= u.get(mid, mid, mid) + 0.1,
826            "smooth union should not be much larger than union"
827        );
828    }
829    /// Sample at a cell centre should match the cell value.
830    #[test]
831    fn test_sample_at_cell_center() {
832        let n = 11usize;
833        let dx = 0.1;
834        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
835        g.compute_sphere_sdf([0.55, 0.55, 0.55], 0.3);
836        let c = n / 2;
837        let pos = g.world_pos(c, c, c);
838        let sampled = g.sample(pos);
839        assert!(sampled.is_some());
840        let cell_val = g.get(c, c, c);
841        assert!(
842            (sampled.unwrap() - cell_val).abs() < 0.05,
843            "sampled = {:?}, cell = {cell_val}",
844            sampled
845        );
846    }
847    /// Sample outside the grid should return None.
848    #[test]
849    fn test_sample_outside_grid() {
850        let g = SdfGrid::new(5, 5, 5, 0.1, [0.0; 3]);
851        assert!(g.sample([-1.0, -1.0, -1.0]).is_none());
852    }
853    /// Ray pointing toward a sphere should hit.
854    #[test]
855    fn test_sphere_trace_hit() {
856        let n = 41usize;
857        let dx = 0.05;
858        let center = [1.0, 1.0, 1.0];
859        let radius = 0.3;
860        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
861        g.compute_sphere_sdf(center, radius);
862        let result = sphere_trace(&g, [0.1, 1.0, 1.0], [1.0, 0.0, 0.0], 5.0, 100, dx * 0.5);
863        assert!(result.hit, "ray should hit the sphere");
864        assert!(result.t > 0.0, "t should be positive");
865    }
866    /// Ray pointing away from a sphere should miss.
867    #[test]
868    fn test_sphere_trace_miss() {
869        let n = 21usize;
870        let dx = 0.1;
871        let center = [1.0, 1.0, 1.0];
872        let radius = 0.3;
873        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
874        g.compute_sphere_sdf(center, radius);
875        let result = sphere_trace(&g, [0.1, 1.0, 1.0], [-1.0, 0.0, 0.0], 5.0, 100, dx * 0.5);
876        assert!(!result.hit, "ray should miss the sphere");
877    }
878    /// Distance from a point to a triangle vertex.
879    #[test]
880    fn test_point_triangle_distance_at_vertex() {
881        let a = [0.0, 0.0, 0.0];
882        let b = [1.0, 0.0, 0.0];
883        let c = [0.0, 1.0, 0.0];
884        let p = [-1.0, 0.0, 0.0];
885        let d = point_triangle_distance(&p, &a, &b, &c);
886        assert!((d - 1.0).abs() < 1e-10, "distance = {d}, expected 1.0");
887    }
888    /// Distance from a point directly above the triangle center.
889    #[test]
890    fn test_point_triangle_distance_above() {
891        let a = [0.0, 0.0, 0.0];
892        let b = [1.0, 0.0, 0.0];
893        let c = [0.0, 1.0, 0.0];
894        let p = [0.2, 0.2, 1.0];
895        let d = point_triangle_distance(&p, &a, &b, &c);
896        assert!((d - 1.0).abs() < 1e-10, "distance = {d}, expected 1.0");
897    }
898    /// Batch evaluation should return one value per point.
899    #[test]
900    fn test_evaluate_sdf_batch() {
901        let n = 11usize;
902        let dx = 0.2;
903        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
904        g.compute_sphere_sdf([1.1, 1.1, 1.1], 0.5);
905        let points = vec![[1.1, 1.1, 1.1], [0.1, 0.1, 0.1]];
906        let vals = evaluate_sdf_batch(&g, &points);
907        assert_eq!(vals.len(), 2);
908        assert!(vals[0] < 0.0, "centre should be negative, got {}", vals[0]);
909    }
910    /// Volume of a sphere should be approximately 4/3 * pi * r^3.
911    #[test]
912    fn test_approximate_volume_sphere() {
913        let n = 41usize;
914        let dx = 0.05;
915        let radius = 0.5;
916        let center = [1.0, 1.0, 1.0];
917        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
918        g.compute_sphere_sdf(center, radius);
919        let vol = approximate_volume(&g);
920        let expected = 4.0 / 3.0 * std::f64::consts::PI * radius.powi(3);
921        assert!(
922            (vol - expected).abs() / expected < 0.2,
923            "volume = {vol}, expected ~{expected}"
924        );
925    }
926    /// Cylinder SDF: centre should be inside.
927    #[test]
928    fn test_cylinder_sdf_center_inside() {
929        let n = 21usize;
930        let dx = 0.1;
931        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
932        g.compute_cylinder_sdf([1.0, 1.0], 0.3, 1.2);
933        let c = n / 2;
934        let val = g.get(10, 10, c);
935        assert!(val < 0.0, "cylinder centre should be inside, got {val}");
936    }
937    /// Torus SDF: ring centre should be inside.
938    #[test]
939    fn test_torus_sdf() {
940        let n = 21usize;
941        let dx = 0.1;
942        let center = [1.0, 1.0, 1.0];
943        let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
944        g.compute_torus_sdf(center, 0.4, 0.1);
945        let ring_x = ((center[0] + 0.4) / dx - 0.5) as usize;
946        let ring_y = (center[1] / dx - 0.5) as usize;
947        let ring_z = (center[2] / dx - 0.5) as usize;
948        if ring_x < n && ring_y < n && ring_z < n {
949            let val = g.get(ring_x, ring_y, ring_z);
950            assert!(val < 0.1, "ring point should be near surface, got {val}");
951        }
952    }
953}