Skip to main content

oxiphysics_geometry/level_set/
types.rs

1//! Auto-generated module
2//!
3//! ๐Ÿค– Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use super::functions::*;
8use super::functions::{MC_EDGE_CORNERS, MC_EDGE_TABLE, MC_TRI_TABLE};
9
10/// Signed distance transform from a point cloud or triangle mesh.
11///
12/// Provides brute-force SDF computation (exact for small point sets) and a
13/// fast alternating-sweep approximation for larger inputs.
14pub struct SignedDistanceTransform {
15    /// Source points used to define the zero level set.
16    pub source_points: Vec<[f64; 3]>,
17}
18impl SignedDistanceTransform {
19    /// Create a new [`SignedDistanceTransform`] from a set of source points.
20    ///
21    /// # Arguments
22    /// * `points` โ€“ positions on or near the zero level set
23    pub fn new(points: Vec<[f64; 3]>) -> Self {
24        Self {
25            source_points: points,
26        }
27    }
28    /// Brute-force signed distance from world point `p` to the point cloud.
29    ///
30    /// Returns the distance to the nearest source point (always non-negative;
31    /// sign must be determined by additional topology information).
32    pub fn unsigned_distance(&self, p: [f64; 3]) -> f64 {
33        self.source_points
34            .iter()
35            .map(|&q| {
36                ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2)).sqrt()
37            })
38            .fold(f64::MAX, f64::min)
39    }
40    /// Build an unsigned distance field on a regular grid.
41    ///
42    /// For each grid node, computes the nearest point distance using brute force.
43    ///
44    /// # Arguments
45    /// * `nx`, `ny`, `nz` โ€“ grid dimensions
46    /// * `dx`             โ€“ grid spacing
47    /// * `origin`         โ€“ grid origin
48    pub fn compute_grid_unsigned(
49        &self,
50        nx: usize,
51        ny: usize,
52        nz: usize,
53        dx: f64,
54        origin: [f64; 3],
55    ) -> LevelSetField {
56        let mut field = LevelSetField::new(nx, ny, nz, dx, origin);
57        for iz in 0..nz {
58            for iy in 0..ny {
59                for ix in 0..nx {
60                    let p = field.node_pos(ix, iy, iz);
61                    let d = self.unsigned_distance(p);
62                    field.set(ix, iy, iz, d);
63                }
64            }
65        }
66        field
67    }
68    /// Compute a signed distance field on a regular grid.
69    ///
70    /// The sign is determined by the winding number test relative to the
71    /// first source point (interior if distance < median distance).
72    /// For a production use case this should use a proper inside/outside test.
73    pub fn compute_grid_signed(
74        &self,
75        nx: usize,
76        ny: usize,
77        nz: usize,
78        dx: f64,
79        origin: [f64; 3],
80    ) -> LevelSetField {
81        let mut field = self.compute_grid_unsigned(nx, ny, nz, dx, origin);
82        if self.source_points.is_empty() {
83            return field;
84        }
85        let centroid: [f64; 3] = {
86            let n = self.source_points.len() as f64;
87            let s = self.source_points.iter().fold([0.0_f64; 3], |acc, p| {
88                [acc[0] + p[0], acc[1] + p[1], acc[2] + p[2]]
89            });
90            [s[0] / n, s[1] / n, s[2] / n]
91        };
92        let radius = self.unsigned_distance(centroid);
93        for iz in 0..nz {
94            for iy in 0..ny {
95                for ix in 0..nx {
96                    let p = field.node_pos(ix, iy, iz);
97                    let dc = ((p[0] - centroid[0]).powi(2)
98                        + (p[1] - centroid[1]).powi(2)
99                        + (p[2] - centroid[2]).powi(2))
100                    .sqrt();
101                    let val = field.get(ix, iy, iz);
102                    if dc < radius {
103                        field.set(ix, iy, iz, -val);
104                    }
105                }
106            }
107        }
108        field
109    }
110    /// Distance from point `p` to a line segment \[a, b\].
111    pub fn point_to_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
112        let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
113        let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
114        let len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
115        if len2 < 1e-30 {
116            return (ap[0] * ap[0] + ap[1] * ap[1] + ap[2] * ap[2]).sqrt();
117        }
118        let t = ((ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / len2).clamp(0.0, 1.0);
119        let closest = [a[0] + t * ab[0], a[1] + t * ab[1], a[2] + t * ab[2]];
120        ((p[0] - closest[0]).powi(2) + (p[1] - closest[1]).powi(2) + (p[2] - closest[2]).powi(2))
121            .sqrt()
122    }
123}
124/// Level-set evolution by advection and mean curvature flow.
125///
126/// Implements the upwind finite difference scheme for advection and the
127/// explicit mean-curvature-flow equation for smoothing.
128pub struct LevelSetEvolution {
129    /// Time step for explicit integration.
130    pub dt: f64,
131    /// Number of time steps per evolution call.
132    pub n_steps: usize,
133}
134impl LevelSetEvolution {
135    /// Create a new [`LevelSetEvolution`] solver.
136    ///
137    /// # Arguments
138    /// * `dt`      โ€“ explicit time step (should satisfy CFL: dt โ‰ค dx/|v|_max)
139    /// * `n_steps` โ€“ number of time steps per `advect` or `mean_curvature_flow` call
140    pub fn new(dt: f64, n_steps: usize) -> Self {
141        Self { dt, n_steps }
142    }
143    /// Advect the level-set field by a uniform velocity field using the upwind scheme.
144    ///
145    /// Solves โˆ‚ฯ†/โˆ‚t + v ยท โˆ‡ฯ† = 0 with first-order upwind spatial discretisation.
146    ///
147    /// # Arguments
148    /// * `field`    โ€“ level-set field (modified in place)
149    /// * `velocity` โ€“ uniform velocity vector \[vx, vy, vz\]
150    pub fn advect(&self, field: &mut LevelSetField, velocity: [f64; 3]) {
151        let dx = field.dx;
152        let nx = field.nx;
153        let ny = field.ny;
154        let nz = field.nz;
155        for _ in 0..self.n_steps {
156            let old = field.data.clone();
157            for iz in 0..nz {
158                for iy in 0..ny {
159                    for ix in 0..nx {
160                        let phi_c = old[field.idx(ix, iy, iz)];
161                        let dphi_x = if velocity[0] > 0.0 {
162                            if ix > 0 {
163                                (phi_c - old[field.idx(ix - 1, iy, iz)]) / dx
164                            } else {
165                                0.0
166                            }
167                        } else {
168                            if ix + 1 < nx {
169                                (old[field.idx(ix + 1, iy, iz)] - phi_c) / dx
170                            } else {
171                                0.0
172                            }
173                        };
174                        let dphi_y = if velocity[1] > 0.0 {
175                            if iy > 0 {
176                                (phi_c - old[field.idx(ix, iy - 1, iz)]) / dx
177                            } else {
178                                0.0
179                            }
180                        } else {
181                            if iy + 1 < ny {
182                                (old[field.idx(ix, iy + 1, iz)] - phi_c) / dx
183                            } else {
184                                0.0
185                            }
186                        };
187                        let dphi_z = if velocity[2] > 0.0 {
188                            if iz > 0 {
189                                (phi_c - old[field.idx(ix, iy, iz - 1)]) / dx
190                            } else {
191                                0.0
192                            }
193                        } else {
194                            if iz + 1 < nz {
195                                (old[field.idx(ix, iy, iz + 1)] - phi_c) / dx
196                            } else {
197                                0.0
198                            }
199                        };
200                        let rhs =
201                            velocity[0] * dphi_x + velocity[1] * dphi_y + velocity[2] * dphi_z;
202                        let idx = field.idx(ix, iy, iz);
203                        field.data[idx] = phi_c - self.dt * rhs;
204                    }
205                }
206            }
207        }
208    }
209    /// Evolve the level-set by mean curvature flow.
210    ///
211    /// Solves โˆ‚ฯ†/โˆ‚t = ฮบ |โˆ‡ฯ†| where ฮบ is the mean curvature.
212    /// Uses explicit Euler integration.
213    ///
214    /// # Arguments
215    /// * `field` โ€“ level-set field (modified in place)
216    pub fn mean_curvature_flow(&self, field: &mut LevelSetField) {
217        let dx = field.dx;
218        let nx = field.nx;
219        let ny = field.ny;
220        let nz = field.nz;
221        for _ in 0..self.n_steps {
222            let old = field.data.clone();
223            let old_field = LevelSetField {
224                nx,
225                ny,
226                nz,
227                dx,
228                origin: field.origin,
229                data: old,
230            };
231            for iz in 0..nz {
232                for iy in 0..ny {
233                    for ix in 0..nx {
234                        let kappa = old_field.mean_curvature(ix, iy, iz);
235                        let grad = old_field.gradient(ix, iy, iz);
236                        let grad_mag =
237                            (grad[0] * grad[0] + grad[1] * grad[1] + grad[2] * grad[2]).sqrt();
238                        let idx = field.idx(ix, iy, iz);
239                        field.data[idx] = old_field.data[idx] + self.dt * kappa * grad_mag;
240                    }
241                }
242            }
243        }
244    }
245    /// Normal velocity extension: propagate the interface normal velocity outward.
246    ///
247    /// For each node computes the update ฯ†_new = ฯ† - dt * F |โˆ‡ฯ†| where F is
248    /// a prescribed scalar normal velocity.
249    ///
250    /// # Arguments
251    /// * `field`            โ€“ level-set field (modified in place)
252    /// * `normal_velocity`  โ€“ scalar normal speed (positive = expansion)
253    pub fn normal_velocity_extension(&self, field: &mut LevelSetField, normal_velocity: f64) {
254        let nx = field.nx;
255        let ny = field.ny;
256        let nz = field.nz;
257        for _ in 0..self.n_steps {
258            let old = field.data.clone();
259            let old_field = LevelSetField {
260                nx,
261                ny,
262                nz,
263                dx: field.dx,
264                origin: field.origin,
265                data: old,
266            };
267            for iz in 0..nz {
268                for iy in 0..ny {
269                    for ix in 0..nx {
270                        let grad = old_field.gradient(ix, iy, iz);
271                        let grad_mag =
272                            (grad[0] * grad[0] + grad[1] * grad[1] + grad[2] * grad[2]).sqrt();
273                        let idx = field.idx(ix, iy, iz);
274                        field.data[idx] =
275                            old_field.data[idx] - self.dt * normal_velocity * grad_mag;
276                    }
277                }
278            }
279        }
280    }
281}
282/// Boolean operations on level-set fields via min/max composition.
283///
284/// All operations create a new [`LevelSetField`] with the same grid layout as
285/// `field_a`.  The fields must have identical grid dimensions, spacing, and origin.
286pub struct LevelSetBoolean;
287impl LevelSetBoolean {
288    /// Union of two level-set fields: ฯ† = min(ฯ†_a, ฯ†_b).
289    ///
290    /// Corresponds to the union of the two implicit surfaces.
291    ///
292    /// # Arguments
293    /// * `field_a` โ€“ first operand
294    /// * `field_b` โ€“ second operand (must have same grid as `field_a`)
295    pub fn union(field_a: &LevelSetField, field_b: &LevelSetField) -> LevelSetField {
296        let mut result = LevelSetField::new(
297            field_a.nx,
298            field_a.ny,
299            field_a.nz,
300            field_a.dx,
301            field_a.origin,
302        );
303        for i in 0..result.data.len() {
304            let va = field_a.data.get(i).copied().unwrap_or(f64::MAX);
305            let vb = field_b.data.get(i).copied().unwrap_or(f64::MAX);
306            result.data[i] = va.min(vb);
307        }
308        result
309    }
310    /// Intersection of two level-set fields: ฯ† = max(ฯ†_a, ฯ†_b).
311    ///
312    /// Corresponds to the intersection of the two implicit surfaces.
313    ///
314    /// # Arguments
315    /// * `field_a` โ€“ first operand
316    /// * `field_b` โ€“ second operand (must have same grid as `field_a`)
317    pub fn intersection(field_a: &LevelSetField, field_b: &LevelSetField) -> LevelSetField {
318        let mut result = LevelSetField::new(
319            field_a.nx,
320            field_a.ny,
321            field_a.nz,
322            field_a.dx,
323            field_a.origin,
324        );
325        for i in 0..result.data.len() {
326            let va = field_a.data.get(i).copied().unwrap_or(f64::MAX);
327            let vb = field_b.data.get(i).copied().unwrap_or(f64::MAX);
328            result.data[i] = va.max(vb);
329        }
330        result
331    }
332    /// Difference A โˆ’ B: ฯ† = max(ฯ†_a, โˆ’ฯ†_b).
333    ///
334    /// Corresponds to removing the region of `field_b` from `field_a`.
335    ///
336    /// # Arguments
337    /// * `field_a` โ€“ base operand
338    /// * `field_b` โ€“ region to subtract
339    pub fn difference(field_a: &LevelSetField, field_b: &LevelSetField) -> LevelSetField {
340        let mut result = LevelSetField::new(
341            field_a.nx,
342            field_a.ny,
343            field_a.nz,
344            field_a.dx,
345            field_a.origin,
346        );
347        for i in 0..result.data.len() {
348            let va = field_a.data.get(i).copied().unwrap_or(f64::MAX);
349            let vb = field_b.data.get(i).copied().unwrap_or(f64::MAX);
350            result.data[i] = va.max(-vb);
351        }
352        result
353    }
354    /// Complement (negation): ฯ† = โˆ’ฯ†_a.
355    ///
356    /// Flips the inside/outside classification of the level-set.
357    ///
358    /// # Arguments
359    /// * `field_a` โ€“ the level-set field to negate
360    pub fn complement(field_a: &LevelSetField) -> LevelSetField {
361        let mut result = LevelSetField::new(
362            field_a.nx,
363            field_a.ny,
364            field_a.nz,
365            field_a.dx,
366            field_a.origin,
367        );
368        for i in 0..result.data.len() {
369            result.data[i] = -field_a.data[i];
370        }
371        result
372    }
373    /// Smooth minimum blend (Cยน union): ฯ† = smin(ฯ†_a, ฯ†_b, k).
374    ///
375    /// Produces a smooth blend at the union boundary with blend radius `k`.
376    ///
377    /// # Arguments
378    /// * `field_a` โ€“ first operand
379    /// * `field_b` โ€“ second operand
380    /// * `k`       โ€“ blend radius (larger k โ†’ smoother blend)
381    pub fn smooth_union(field_a: &LevelSetField, field_b: &LevelSetField, k: f64) -> LevelSetField {
382        let mut result = LevelSetField::new(
383            field_a.nx,
384            field_a.ny,
385            field_a.nz,
386            field_a.dx,
387            field_a.origin,
388        );
389        for i in 0..result.data.len() {
390            let va = field_a.data.get(i).copied().unwrap_or(f64::MAX);
391            let vb = field_b.data.get(i).copied().unwrap_or(f64::MAX);
392            let h = (0.5 + 0.5 * (vb - va) / k.max(1e-30)).clamp(0.0, 1.0);
393            result.data[i] = va * h + vb * (1.0 - h) - k * h * (1.0 - h);
394        }
395        result
396    }
397}
398/// Signed distance function sampled on a uniform 3-D Cartesian grid.
399///
400/// The grid has `nx ร— ny ร— nz` cells.  Values are stored in row-major order
401/// as `data[iz * ny * nx + iy * nx + ix]`.  Positive values are outside the
402/// surface; negative values are inside.
403pub struct LevelSetField {
404    /// Number of grid points along x.
405    pub nx: usize,
406    /// Number of grid points along y.
407    pub ny: usize,
408    /// Number of grid points along z.
409    pub nz: usize,
410    /// Grid spacing (uniform in all directions).
411    pub dx: f64,
412    /// Origin of the grid (coordinates of node \[0,0,0\]).
413    pub origin: [f64; 3],
414    /// Flat array of signed distance values, length `nx * ny * nz`.
415    pub data: Vec<f64>,
416}
417impl LevelSetField {
418    /// Create a new [`LevelSetField`] filled with `f64::MAX` (uninitialized).
419    ///
420    /// # Arguments
421    /// * `nx`, `ny`, `nz` โ€“ grid dimensions
422    /// * `dx`             โ€“ uniform grid spacing
423    /// * `origin`         โ€“ world-space coordinates of node (0,0,0)
424    pub fn new(nx: usize, ny: usize, nz: usize, dx: f64, origin: [f64; 3]) -> Self {
425        let data = vec![f64::MAX; nx * ny * nz];
426        Self {
427            nx,
428            ny,
429            nz,
430            dx,
431            origin,
432            data,
433        }
434    }
435    /// Linear index for grid node (ix, iy, iz).
436    #[inline]
437    pub fn idx(&self, ix: usize, iy: usize, iz: usize) -> usize {
438        iz * self.ny * self.nx + iy * self.nx + ix
439    }
440    /// World-space coordinates of grid node (ix, iy, iz).
441    #[inline]
442    pub fn node_pos(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
443        [
444            self.origin[0] + ix as f64 * self.dx,
445            self.origin[1] + iy as f64 * self.dx,
446            self.origin[2] + iz as f64 * self.dx,
447        ]
448    }
449    /// Get the level-set value at grid node (ix, iy, iz).
450    ///
451    /// Returns `f64::MAX` for out-of-bounds nodes.
452    pub fn get(&self, ix: usize, iy: usize, iz: usize) -> f64 {
453        if ix >= self.nx || iy >= self.ny || iz >= self.nz {
454            return f64::MAX;
455        }
456        self.data[self.idx(ix, iy, iz)]
457    }
458    /// Set the level-set value at grid node (ix, iy, iz).
459    ///
460    /// Silently ignores out-of-bounds writes.
461    pub fn set(&mut self, ix: usize, iy: usize, iz: usize, value: f64) {
462        if ix >= self.nx || iy >= self.ny || iz >= self.nz {
463            return;
464        }
465        let i = self.idx(ix, iy, iz);
466        self.data[i] = value;
467    }
468    /// Tri-linear interpolation of the level-set field at world position `p`.
469    ///
470    /// Returns `f64::MAX` for positions outside the grid.
471    pub fn interpolate(&self, p: [f64; 3]) -> f64 {
472        let fx = (p[0] - self.origin[0]) / self.dx;
473        let fy = (p[1] - self.origin[1]) / self.dx;
474        let fz = (p[2] - self.origin[2]) / self.dx;
475        if fx < 0.0 || fy < 0.0 || fz < 0.0 {
476            return f64::MAX;
477        }
478        let ix = fx as usize;
479        let iy = fy as usize;
480        let iz = fz as usize;
481        if ix + 1 >= self.nx || iy + 1 >= self.ny || iz + 1 >= self.nz {
482            return f64::MAX;
483        }
484        let tx = fx - ix as f64;
485        let ty = fy - iy as f64;
486        let tz = fz - iz as f64;
487        let c000 = self.get(ix, iy, iz);
488        let c100 = self.get(ix + 1, iy, iz);
489        let c010 = self.get(ix, iy + 1, iz);
490        let c110 = self.get(ix + 1, iy + 1, iz);
491        let c001 = self.get(ix, iy, iz + 1);
492        let c101 = self.get(ix + 1, iy, iz + 1);
493        let c011 = self.get(ix, iy + 1, iz + 1);
494        let c111 = self.get(ix + 1, iy + 1, iz + 1);
495        let c00 = c000 * (1.0 - tx) + c100 * tx;
496        let c10 = c010 * (1.0 - tx) + c110 * tx;
497        let c01 = c001 * (1.0 - tx) + c101 * tx;
498        let c11 = c011 * (1.0 - tx) + c111 * tx;
499        let c0 = c00 * (1.0 - ty) + c10 * ty;
500        let c1 = c01 * (1.0 - ty) + c11 * ty;
501        c0 * (1.0 - tz) + c1 * tz
502    }
503    /// Numerical gradient of the level-set field at grid node (ix, iy, iz).
504    ///
505    /// Uses central differences where possible, one-sided differences at boundaries.
506    pub fn gradient(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
507        let gx = if ix == 0 {
508            (self.get(1, iy, iz) - self.get(0, iy, iz)) / self.dx
509        } else if ix + 1 == self.nx {
510            (self.get(ix, iy, iz) - self.get(ix - 1, iy, iz)) / self.dx
511        } else {
512            (self.get(ix + 1, iy, iz) - self.get(ix - 1, iy, iz)) / (2.0 * self.dx)
513        };
514        let gy = if iy == 0 {
515            (self.get(ix, 1, iz) - self.get(ix, 0, iz)) / self.dx
516        } else if iy + 1 == self.ny {
517            (self.get(ix, iy, iz) - self.get(ix, iy - 1, iz)) / self.dx
518        } else {
519            (self.get(ix, iy + 1, iz) - self.get(ix, iy - 1, iz)) / (2.0 * self.dx)
520        };
521        let gz = if iz == 0 {
522            (self.get(ix, iy, 1) - self.get(ix, iy, 0)) / self.dx
523        } else if iz + 1 == self.nz {
524            (self.get(ix, iy, iz) - self.get(ix, iy, iz - 1)) / self.dx
525        } else {
526            (self.get(ix, iy, iz + 1) - self.get(ix, iy, iz - 1)) / (2.0 * self.dx)
527        };
528        [gx, gy, gz]
529    }
530    /// Mean curvature ฮบ = div(โˆ‡ฯ† / |โˆ‡ฯ†|) at grid node (ix, iy, iz).
531    ///
532    /// Computed via second-order finite differences.
533    pub fn mean_curvature(&self, ix: usize, iy: usize, iz: usize) -> f64 {
534        let phi = self.get(ix, iy, iz);
535        let dx2 = self.dx * self.dx;
536        let get = |x: usize, y: usize, z: usize| -> f64 {
537            let xc = x.min(self.nx - 1);
538            let yc = y.min(self.ny - 1);
539            let zc = z.min(self.nz - 1);
540            self.get(xc, yc, zc)
541        };
542        let pxp = if ix + 1 < self.nx {
543            get(ix + 1, iy, iz)
544        } else {
545            phi
546        };
547        let pxm = if ix > 0 { get(ix - 1, iy, iz) } else { phi };
548        let pyp = if iy + 1 < self.ny {
549            get(ix, iy + 1, iz)
550        } else {
551            phi
552        };
553        let pym = if iy > 0 { get(ix, iy - 1, iz) } else { phi };
554        let pzp = if iz + 1 < self.nz {
555            get(ix, iy, iz + 1)
556        } else {
557            phi
558        };
559        let pzm = if iz > 0 { get(ix, iy, iz - 1) } else { phi };
560        let phix = (pxp - pxm) / (2.0 * self.dx);
561        let phiy = (pyp - pym) / (2.0 * self.dx);
562        let phiz = (pzp - pzm) / (2.0 * self.dx);
563        let phixx = (pxp - 2.0 * phi + pxm) / dx2;
564        let phiyy = (pyp - 2.0 * phi + pym) / dx2;
565        let phizz = (pzp - 2.0 * phi + pzm) / dx2;
566        let grad2 = phix * phix + phiy * phiy + phiz * phiz;
567        let grad_mag = grad2.sqrt().max(1e-30);
568
569        (phixx + phiyy + phizz) / grad_mag
570            - (phix * phix * phixx
571                + phiy * phiy * phiyy
572                + phiz * phiz * phizz
573                + 2.0
574                    * (phix * phiy * ((pxp + pyp - pxm - pym) / (4.0 * self.dx * self.dx) - 0.0)
575                        + phix * phiz * 0.0
576                        + phiy * phiz * 0.0))
577                / (grad_mag * grad2).max(1e-30)
578    }
579    /// Initialize the level-set as a sphere SDF.
580    ///
581    /// Sets ฯ†(x) = |x โˆ’ center| โˆ’ radius for all nodes.
582    pub fn init_sphere(&mut self, center: [f64; 3], radius: f64) {
583        for iz in 0..self.nz {
584            for iy in 0..self.ny {
585                for ix in 0..self.nx {
586                    let p = self.node_pos(ix, iy, iz);
587                    let d = ((p[0] - center[0]).powi(2)
588                        + (p[1] - center[1]).powi(2)
589                        + (p[2] - center[2]).powi(2))
590                    .sqrt();
591                    self.set(ix, iy, iz, d - radius);
592                }
593            }
594        }
595    }
596    /// Initialize the level-set as an axis-aligned box SDF.
597    ///
598    /// Sets ฯ†(x) to the signed distance to the box \[lo, hi\].
599    pub fn init_box(&mut self, lo: [f64; 3], hi: [f64; 3]) {
600        for iz in 0..self.nz {
601            for iy in 0..self.ny {
602                for ix in 0..self.nx {
603                    let p = self.node_pos(ix, iy, iz);
604                    let dx = (lo[0] - p[0]).max(p[0] - hi[0]).max(0.0);
605                    let dy = (lo[1] - p[1]).max(p[1] - hi[1]).max(0.0);
606                    let dz = (lo[2] - p[2]).max(p[2] - hi[2]).max(0.0);
607                    let outside = (dx * dx + dy * dy + dz * dz).sqrt();
608                    let inside = (p[0] - lo[0])
609                        .min(hi[0] - p[0])
610                        .min(p[1] - lo[1])
611                        .min(hi[1] - p[1])
612                        .min(p[2] - lo[2])
613                        .min(hi[2] - p[2]);
614                    let sdf = if inside < 0.0 { outside } else { -inside };
615                    self.set(ix, iy, iz, sdf);
616                }
617            }
618        }
619    }
620    /// Count nodes with ฯ† โ‰ค 0 (inside or on the interface).
621    pub fn count_inside(&self) -> usize {
622        self.data.iter().filter(|&&v| v <= 0.0).count()
623    }
624    /// Minimum value of ฯ† over all nodes.
625    pub fn min_value(&self) -> f64 {
626        self.data.iter().cloned().fold(f64::MAX, f64::min)
627    }
628    /// Maximum value of ฯ† over all nodes.
629    pub fn max_value(&self) -> f64 {
630        self.data.iter().cloned().fold(f64::MIN, f64::max)
631    }
632}
633/// Level-set reinitialization via fast marching method (redistancing).
634///
635/// Converts an arbitrary level-set function into a proper signed distance
636/// function while preserving the zero-level-set location.
637pub struct LevelSetReinit {
638    /// Convergence tolerance for iterative reinitialization.
639    pub tol: f64,
640    /// Maximum number of pseudo-time iterations.
641    pub max_iter: usize,
642}
643impl LevelSetReinit {
644    /// Create a new reinitialization solver with given tolerance and iteration limit.
645    ///
646    /// # Arguments
647    /// * `tol`      โ€“ convergence tolerance (โˆž-norm of update)
648    /// * `max_iter` โ€“ maximum pseudo-time steps
649    pub fn new(tol: f64, max_iter: usize) -> Self {
650        Self { tol, max_iter }
651    }
652    /// Reinitialize a level-set field using the PDE-based Sussman scheme.
653    ///
654    /// Integrates โˆ‚ฯ†/โˆ‚ฯ„ = sign(ฯ†โ‚€)(1 โˆ’ |โˆ‡ฯ†|) until steady state.
655    /// Modifies `field.data` in place.
656    ///
657    /// # Arguments
658    /// * `field` โ€“ the level-set field to reinitialize
659    pub fn reinitialize(&self, field: &mut LevelSetField) {
660        let dx = field.dx;
661        let dt = 0.5 * dx;
662        let nx = field.nx;
663        let ny = field.ny;
664        let nz = field.nz;
665        let phi0: Vec<f64> = field.data.clone();
666        let sign0: Vec<f64> = phi0
667            .iter()
668            .map(|&v| {
669                if v > 0.0 {
670                    1.0
671                } else if v < 0.0 {
672                    -1.0
673                } else {
674                    0.0
675                }
676            })
677            .collect();
678        for _iter in 0..self.max_iter {
679            let mut new_data = field.data.clone();
680            let mut max_change = 0.0_f64;
681            for iz in 0..nz {
682                for iy in 0..ny {
683                    for ix in 0..nx {
684                        let idx = field.idx(ix, iy, iz);
685                        let s = sign0[idx];
686                        if s == 0.0 {
687                            continue;
688                        }
689                        let phi_c = field.data[idx];
690                        let grad_mag = godunov_grad_mag(field, ix, iy, iz, s);
691                        let update = s * (1.0 - grad_mag);
692                        new_data[idx] = phi_c + dt * update;
693                        max_change = max_change.max((dt * update).abs());
694                    }
695                }
696            }
697            field.data = new_data;
698            if max_change < self.tol {
699                break;
700            }
701        }
702    }
703    /// Simple iterative fast marching sweep reinitialization (alternating directions).
704    ///
705    /// Faster than the PDE approach for well-behaved fields; performs `n_sweeps`
706    /// alternating-direction passes of the first-order update.
707    ///
708    /// # Arguments
709    /// * `field`   โ€“ the level-set field to reinitialize
710    /// * `n_sweeps`โ€“ number of alternating-direction sweep passes
711    pub fn fast_sweep(&self, field: &mut LevelSetField, n_sweeps: usize) {
712        let dx = field.dx;
713        let nx = field.nx;
714        let ny = field.ny;
715        let nz = field.nz;
716        for sweep in 0..n_sweeps {
717            let ix_range: Vec<usize> = if sweep % 2 == 0 {
718                (0..nx).collect()
719            } else {
720                (0..nx).rev().collect()
721            };
722            let iy_range: Vec<usize> = if (sweep / 2) % 2 == 0 {
723                (0..ny).collect()
724            } else {
725                (0..ny).rev().collect()
726            };
727            let iz_range: Vec<usize> = if (sweep / 4) % 2 == 0 {
728                (0..nz).collect()
729            } else {
730                (0..nz).rev().collect()
731            };
732            for &iz in &iz_range {
733                for &iy in &iy_range {
734                    for &ix in &ix_range {
735                        let phi_c = field.get(ix, iy, iz);
736                        let axm = if ix > 0 {
737                            field.get(ix - 1, iy, iz)
738                        } else {
739                            phi_c
740                        };
741                        let axp = if ix + 1 < nx {
742                            field.get(ix + 1, iy, iz)
743                        } else {
744                            phi_c
745                        };
746                        let aym = if iy > 0 {
747                            field.get(ix, iy - 1, iz)
748                        } else {
749                            phi_c
750                        };
751                        let ayp = if iy + 1 < ny {
752                            field.get(ix, iy + 1, iz)
753                        } else {
754                            phi_c
755                        };
756                        let azm = if iz > 0 {
757                            field.get(ix, iy, iz - 1)
758                        } else {
759                            phi_c
760                        };
761                        let azp = if iz + 1 < nz {
762                            field.get(ix, iy, iz + 1)
763                        } else {
764                            phi_c
765                        };
766                        let ax = if phi_c >= 0.0 {
767                            axm.min(axp)
768                        } else {
769                            axm.max(axp)
770                        };
771                        let ay = if phi_c >= 0.0 {
772                            aym.min(ayp)
773                        } else {
774                            aym.max(ayp)
775                        };
776                        let az = if phi_c >= 0.0 {
777                            azm.min(azp)
778                        } else {
779                            azm.max(azp)
780                        };
781                        let new_val = if phi_c >= 0.0 {
782                            (ax.abs().min(ay.abs()).min(az.abs()) + dx).min(phi_c.abs())
783                        } else {
784                            let d = (ax.abs().min(ay.abs()).min(az.abs()) + dx).min(phi_c.abs());
785                            -d
786                        };
787                        let _ = ax;
788                        let _ = ay;
789                        let _ = az;
790                        let _ = new_val;
791                        let candidate = if phi_c >= 0.0 {
792                            axm.max(0.0).min(axp.max(0.0)).min(phi_c).max(0.0) + dx
793                        } else {
794                            (axm.min(0.0).max(axp.min(0.0)).max(phi_c).min(0.0)) - dx
795                        };
796                        if (candidate.abs() < phi_c.abs()) && (candidate * phi_c >= 0.0) {
797                            field.set(ix, iy, iz, candidate);
798                        }
799                    }
800                }
801            }
802        }
803    }
804}
805/// Output triangle from marching cubes isosurface extraction.
806#[derive(Debug, Clone)]
807pub struct McTriangle {
808    /// Vertex positions (3 vertices, each an \[x, y, z\] world-space coordinate).
809    pub vertices: [[f64; 3]; 3],
810}
811/// Isosurface extraction via the Marching Cubes algorithm.
812///
813/// Implements the full 256-case lookup table for robust triangle generation
814/// at the specified isovalue.
815pub struct IsosurfaceExtraction {
816    /// Isovalue defining the surface (default: 0.0 for signed distance fields).
817    pub isovalue: f64,
818}
819impl IsosurfaceExtraction {
820    /// Create a new [`IsosurfaceExtraction`] with the given isovalue.
821    ///
822    /// # Arguments
823    /// * `isovalue` โ€“ the scalar value defining the isosurface
824    pub fn new(isovalue: f64) -> Self {
825        Self { isovalue }
826    }
827    /// Extract the isosurface from a [`LevelSetField`] as a list of triangles.
828    ///
829    /// Iterates over all cells in the grid, classifies each cube, and uses the
830    /// Marching Cubes lookup table to generate triangles.
831    pub fn extract(&self, field: &LevelSetField) -> Vec<McTriangle> {
832        let mut triangles = Vec::new();
833        let nx = field.nx;
834        let ny = field.ny;
835        let nz = field.nz;
836        if nx < 2 || ny < 2 || nz < 2 {
837            return triangles;
838        }
839        for iz in 0..nz - 1 {
840            for iy in 0..ny - 1 {
841                for ix in 0..nx - 1 {
842                    self.process_cell(field, ix, iy, iz, &mut triangles);
843                }
844            }
845        }
846        triangles
847    }
848    /// Process a single cube cell and append any generated triangles.
849    fn process_cell(
850        &self,
851        field: &LevelSetField,
852        ix: usize,
853        iy: usize,
854        iz: usize,
855        triangles: &mut Vec<McTriangle>,
856    ) {
857        let v: [f64; 8] = [
858            field.get(ix, iy, iz),
859            field.get(ix + 1, iy, iz),
860            field.get(ix + 1, iy + 1, iz),
861            field.get(ix, iy + 1, iz),
862            field.get(ix, iy, iz + 1),
863            field.get(ix + 1, iy, iz + 1),
864            field.get(ix + 1, iy + 1, iz + 1),
865            field.get(ix, iy + 1, iz + 1),
866        ];
867        let p: [[f64; 3]; 8] = [
868            field.node_pos(ix, iy, iz),
869            field.node_pos(ix + 1, iy, iz),
870            field.node_pos(ix + 1, iy + 1, iz),
871            field.node_pos(ix, iy + 1, iz),
872            field.node_pos(ix, iy, iz + 1),
873            field.node_pos(ix + 1, iy, iz + 1),
874            field.node_pos(ix + 1, iy + 1, iz + 1),
875            field.node_pos(ix, iy + 1, iz + 1),
876        ];
877        let mut cube_idx = 0u8;
878        for i in 0..8 {
879            if v[i] < self.isovalue {
880                cube_idx |= 1 << i;
881            }
882        }
883        if cube_idx == 0 || cube_idx == 255 {
884            return;
885        }
886        let edge_flags = MC_EDGE_TABLE[cube_idx as usize];
887        let mut edge_verts = [[0.0f64; 3]; 12];
888        for e in 0..12 {
889            if edge_flags & (1 << e) != 0 {
890                let (a, b) = MC_EDGE_CORNERS[e];
891                edge_verts[e] = interp_vertex(p[a], p[b], v[a], v[b], self.isovalue);
892            }
893        }
894        let tris = &MC_TRI_TABLE[cube_idx as usize];
895        let mut i = 0;
896        while i < tris.len() && tris[i] != -1 {
897            let tri = McTriangle {
898                vertices: [
899                    edge_verts[tris[i] as usize],
900                    edge_verts[tris[i + 1] as usize],
901                    edge_verts[tris[i + 2] as usize],
902                ],
903            };
904            triangles.push(tri);
905            i += 3;
906        }
907    }
908    /// Count the number of triangles in the isosurface.
909    pub fn count_triangles(&self, field: &LevelSetField) -> usize {
910        self.extract(field).len()
911    }
912}