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