1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
32#![allow(clippy::too_many_arguments)]
33
34use rand::RngExt;
35use std::collections::BinaryHeap;
36
37#[inline]
42fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
43 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
44}
45
46#[inline]
47fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
48 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
49}
50
51#[inline]
52fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
53 [a[0] * s, a[1] * s, a[2] * s]
54}
55
56#[inline]
57fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
58 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
59}
60
61#[inline]
62fn norm3(a: [f64; 3]) -> f64 {
63 dot3(a, a).sqrt()
64}
65
66#[inline]
67fn normalize3(a: [f64; 3]) -> [f64; 3] {
68 let n = norm3(a).max(1e-30);
69 scale3(a, 1.0 / n)
70}
71
72#[inline]
73fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
74 x.max(lo).min(hi)
75}
76
77#[inline]
78fn length2(a: [f64; 2]) -> f64 {
79 (a[0] * a[0] + a[1] * a[1]).sqrt()
80}
81
82#[inline]
83fn max2(a: [f64; 2]) -> f64 {
84 a[0].max(a[1])
85}
86
87pub fn sdf_sphere(p: [f64; 3], r: f64) -> f64 {
95 norm3(p) - r
96}
97
98pub fn sdf_box(p: [f64; 3], b: [f64; 3]) -> f64 {
101 let q = [p[0].abs() - b[0], p[1].abs() - b[1], p[2].abs() - b[2]];
102 let pos_part = [q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)];
103 norm3(pos_part) + q[0].max(q[1]).max(q[2]).min(0.0)
104}
105
106pub fn sdf_rounded_box(p: [f64; 3], b: [f64; 3], r: f64) -> f64 {
109 sdf_box(p, b) - r
110}
111
112pub fn sdf_capsule(p: [f64; 3], a: [f64; 3], b: [f64; 3], r: f64) -> f64 {
115 let pa = sub3(p, a);
116 let ba = sub3(b, a);
117 let h = clamp(dot3(pa, ba) / dot3(ba, ba).max(1e-30), 0.0, 1.0);
118 norm3(sub3(pa, scale3(ba, h))) - r
119}
120
121pub fn sdf_cylinder_infinite(p: [f64; 3], r: f64) -> f64 {
123 let xz = [p[0], p[2]];
124 length2(xz) - r
125}
126
127pub fn sdf_cylinder(p: [f64; 3], r: f64, h: f64) -> f64 {
130 let d_xy = length2([p[0], p[2]]) - r;
131 let d_z = p[1].abs() - h;
132 let outer = [d_xy.max(0.0), d_z.max(0.0)];
133 length2(outer) + d_xy.min(0.0).max(d_z.min(0.0))
134}
135
136pub fn sdf_torus(p: [f64; 3], r1: f64, r2: f64) -> f64 {
139 let xz = [p[0], p[2]];
140 let q = [length2(xz) - r1, p[1]];
141 length2(q) - r2
142}
143
144pub fn sdf_plane(p: [f64; 3], n: [f64; 3], d: f64) -> f64 {
147 dot3(p, n) - d
148}
149
150pub fn sdf_cone(p: [f64; 3], angle: f64, h: f64) -> f64 {
153 let q = length2([p[0], p[2]]);
154 let k = [angle.sin(), angle.cos()];
155 let w = [q, -p[1]]; let a = sub2(
157 w,
158 scale2(k, clamp(dot2(w, k) / dot2(k, k).max(1e-30), 0.0, h)),
159 );
160 let b = sub2(w, [k[0] * h, -k[1] * h]);
161 let s = if w[1] * k[0] - w[0] * k[1] > 0.0 {
162 -1.0
163 } else {
164 1.0
165 };
166 let la = length2(a);
167 let lb = length2(b);
168 s * la.min(lb)
169}
170
171#[inline]
173fn dot2(a: [f64; 2], b: [f64; 2]) -> f64 {
174 a[0] * b[0] + a[1] * b[1]
175}
176#[inline]
177fn sub2(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
178 [a[0] - b[0], a[1] - b[1]]
179}
180#[inline]
181fn scale2(a: [f64; 2], s: f64) -> [f64; 2] {
182 [a[0] * s, a[1] * s]
183}
184
185pub fn sdf_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
187 let pa = sub3(p, a);
188 let ba = sub3(b, a);
189 let h = clamp(dot3(pa, ba) / dot3(ba, ba).max(1e-30), 0.0, 1.0);
190 norm3(sub3(pa, scale3(ba, h)))
191}
192
193pub fn sdf_ellipsoid(p: [f64; 3], r: [f64; 3]) -> f64 {
197 let k0 = norm3([p[0] / r[0], p[1] / r[1], p[2] / r[2]]);
198 let k1 = norm3([
199 p[0] / (r[0] * r[0]),
200 p[1] / (r[1] * r[1]),
201 p[2] / (r[2] * r[2]),
202 ]);
203 if k1 < 1e-30 {
204 return -r[0].min(r[1]).min(r[2]);
205 }
206 k0 * (k0 - 1.0) / k1
207}
208
209#[inline]
215pub fn sdf_union(a: f64, b: f64) -> f64 {
216 a.min(b)
217}
218
219#[inline]
221pub fn sdf_intersection(a: f64, b: f64) -> f64 {
222 a.max(b)
223}
224
225#[inline]
227pub fn sdf_difference(a: f64, b: f64) -> f64 {
228 a.max(-b)
229}
230
231pub fn sdf_smooth_union(a: f64, b: f64, k: f64) -> f64 {
239 let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
240 a * h + b * (1.0 - h) - k * h * (1.0 - h)
241}
242
243pub fn sdf_smooth_intersection(a: f64, b: f64, k: f64) -> f64 {
245 let h = clamp(0.5 - 0.5 * (b - a) / k, 0.0, 1.0);
246 a * h + b * (1.0 - h) + k * h * (1.0 - h)
247}
248
249pub fn sdf_smooth_difference(a: f64, b: f64, k: f64) -> f64 {
251 let h = clamp(0.5 - 0.5 * (b + a) / k, 0.0, 1.0);
252 a * h + (-b) * (1.0 - h) + k * h * (1.0 - h)
253}
254
255pub fn sdf_exp_smooth_union(a: f64, b: f64, k: f64) -> f64 {
259 let ea = (-k * a).exp();
260 let eb = (-k * b).exp();
261 -(ea + eb).ln() / k
262}
263
264pub fn sdf_gradient<F>(f: &F, p: [f64; 3], eps: f64) -> [f64; 3]
273where
274 F: Fn([f64; 3]) -> f64,
275{
276 let gx = f([p[0] + eps, p[1], p[2]]) - f([p[0] - eps, p[1], p[2]]);
277 let gy = f([p[0], p[1] + eps, p[2]]) - f([p[0], p[1] - eps, p[2]]);
278 let gz = f([p[0], p[1], p[2] + eps]) - f([p[0], p[1], p[2] - eps]);
279 [gx / (2.0 * eps), gy / (2.0 * eps), gz / (2.0 * eps)]
280}
281
282pub fn sdf_normal<F>(f: &F, p: [f64; 3], eps: f64) -> [f64; 3]
284where
285 F: Fn([f64; 3]) -> f64,
286{
287 normalize3(sdf_gradient(f, p, eps))
288}
289
290pub fn sdf_mean_curvature<F>(f: &F, p: [f64; 3], eps: f64) -> f64
294where
295 F: Fn([f64; 3]) -> f64,
296{
297 let c = f(p);
298 let lap = (f([p[0] + eps, p[1], p[2]])
299 + f([p[0] - eps, p[1], p[2]])
300 + f([p[0], p[1] + eps, p[2]])
301 + f([p[0], p[1] - eps, p[2]])
302 + f([p[0], p[1], p[2] + eps])
303 + f([p[0], p[1], p[2] - eps])
304 - 6.0 * c)
305 / (eps * eps);
306 lap / 2.0
307}
308
309pub fn closest_point_on_sdf<F>(f: &F, p: [f64; 3], max_iters: usize, eps: f64) -> ([f64; 3], f64)
318where
319 F: Fn([f64; 3]) -> f64,
320{
321 let d0 = f(p);
322 let mut q = p;
323 let mut d = d0;
324 for _ in 0..max_iters {
325 if d.abs() < eps {
326 break;
327 }
328 let grad = sdf_gradient(f, q, 1e-4);
329 let gn = norm3(grad).max(1e-30);
330 for k in 0..3 {
332 q[k] -= d * grad[k] / gn;
333 }
334 d = f(q);
335 }
336 (q, d0)
337}
338
339#[inline]
343pub fn sdf_offset(d: f64, delta: f64) -> f64 {
344 d - delta
345}
346
347pub fn sdf_elongate_x<F>(f: &F, p: [f64; 3], h: f64) -> f64
349where
350 F: Fn([f64; 3]) -> f64,
351{
352 let q = [p[0].abs() - h, p[1], p[2]];
353 let qx = q[0].min(0.0);
354 let pp = [qx, q[1], q[2]];
355 f(pp) + q[0].max(0.0)
356}
357
358#[derive(Debug, Clone)]
364pub struct RayMarchHit {
365 pub t: f64,
367 pub point: [f64; 3],
369 pub normal: [f64; 3],
371 pub sdf_value: f64,
373 pub steps: usize,
375}
376
377#[derive(Debug, Clone)]
379pub struct RayMarcher {
380 pub t_max: f64,
382 pub tolerance: f64,
384 pub max_steps: usize,
386 pub step_scale: f64,
388}
389
390impl RayMarcher {
391 pub fn new() -> Self {
393 Self {
394 t_max: 100.0,
395 tolerance: 1e-4,
396 max_steps: 256,
397 step_scale: 0.95,
398 }
399 }
400
401 pub fn with_params(t_max: f64, tolerance: f64, max_steps: usize, step_scale: f64) -> Self {
403 Self {
404 t_max,
405 tolerance,
406 max_steps,
407 step_scale,
408 }
409 }
410
411 pub fn march<F>(&self, f: &F, origin: [f64; 3], dir: [f64; 3]) -> Option<RayMarchHit>
416 where
417 F: Fn([f64; 3]) -> f64,
418 {
419 let d = normalize3(dir);
420 let mut t = 0.0;
421 for step in 0..self.max_steps {
422 let p = add3(origin, scale3(d, t));
423 let sdf = f(p);
424 if sdf.abs() < self.tolerance {
425 let normal = sdf_normal(f, p, 1e-4);
426 return Some(RayMarchHit {
427 t,
428 point: p,
429 normal,
430 sdf_value: sdf,
431 steps: step + 1,
432 });
433 }
434 if t > self.t_max {
435 break;
436 }
437 t += sdf.abs() * self.step_scale;
438 }
439 None
440 }
441
442 pub fn shadow<F>(&self, f: &F, p: [f64; 3], light_dir: [f64; 3], max_dist: f64) -> f64
445 where
446 F: Fn([f64; 3]) -> f64,
447 {
448 let d = normalize3(light_dir);
449 let mut t = 0.01; let mut shadow = 1.0_f64;
451 for _ in 0..self.max_steps {
452 if t >= max_dist {
453 break;
454 }
455 let q = add3(p, scale3(d, t));
456 let h = f(q);
457 if h < self.tolerance {
458 return 0.0;
459 }
460 shadow = shadow.min(8.0 * h / t);
461 t += h;
462 }
463 shadow.clamp(0.0, 1.0)
464 }
465
466 pub fn ambient_occlusion<F>(&self, f: &F, p: [f64; 3], n: [f64; 3], step: f64) -> f64
468 where
469 F: Fn([f64; 3]) -> f64,
470 {
471 let mut occ = 0.0;
472 let mut scale = 1.0;
473 for i in 0..5 {
474 let dist = (i + 1) as f64 * step;
475 let q = add3(p, scale3(n, dist));
476 occ += scale * (dist - f(q));
477 scale *= 0.5;
478 }
479 1.0 - occ.clamp(0.0, 1.0)
480 }
481}
482
483impl Default for RayMarcher {
484 fn default() -> Self {
485 Self::new()
486 }
487}
488
489#[derive(Debug, Clone)]
498pub struct VoronoiSdf {
499 pub seeds: Vec<[f64; 3]>,
501}
502
503impl VoronoiSdf {
504 pub fn new(seeds: Vec<[f64; 3]>) -> Self {
506 Self { seeds }
507 }
508
509 pub fn evaluate(&self, p: [f64; 3]) -> f64 {
514 if self.seeds.is_empty() {
515 return f64::MAX;
516 }
517 let mut d1 = f64::MAX;
518 let mut d2 = f64::MAX;
519 for &s in &self.seeds {
520 let d = norm3(sub3(p, s));
521 if d < d1 {
522 d2 = d1;
523 d1 = d;
524 } else if d < d2 {
525 d2 = d;
526 }
527 }
528 (d2 - d1) * 0.5
530 }
531
532 pub fn nearest_seed(&self, p: [f64; 3]) -> usize {
534 self.seeds
535 .iter()
536 .enumerate()
537 .min_by(|(_, a), (_, b)| {
538 norm3(sub3(p, **a))
539 .partial_cmp(&norm3(sub3(p, **b)))
540 .unwrap_or(std::cmp::Ordering::Equal)
541 })
542 .map(|(i, _)| i)
543 .unwrap_or(0)
544 }
545
546 pub fn cell_ids(&self, nx: usize, ny: usize, nz: usize, bounds: [f64; 6]) -> Vec<usize> {
548 let dx = (bounds[1] - bounds[0]) / nx as f64;
549 let dy = (bounds[3] - bounds[2]) / ny as f64;
550 let dz = (bounds[5] - bounds[4]) / nz as f64;
551 let mut ids = Vec::with_capacity(nx * ny * nz);
552 for iz in 0..nz {
553 for iy in 0..ny {
554 for ix in 0..nx {
555 let p = [
556 bounds[0] + (ix as f64 + 0.5) * dx,
557 bounds[2] + (iy as f64 + 0.5) * dy,
558 bounds[4] + (iz as f64 + 0.5) * dz,
559 ];
560 ids.push(self.nearest_seed(p));
561 }
562 }
563 }
564 ids
565 }
566}
567
568#[derive(Debug, Clone)]
574pub struct MeshVertex {
575 pub position: [f64; 3],
577 pub normal: [f64; 3],
579}
580
581#[derive(Debug, Clone, Copy)]
583pub struct MeshTriangle {
584 pub indices: [usize; 3],
586}
587
588#[derive(Debug, Clone)]
590pub struct MarchingCubesResult {
591 pub vertices: Vec<MeshVertex>,
593 pub triangles: Vec<MeshTriangle>,
595}
596
597impl MarchingCubesResult {
598 pub fn n_vertices(&self) -> usize {
600 self.vertices.len()
601 }
602
603 pub fn n_triangles(&self) -> usize {
605 self.triangles.len()
606 }
607}
608
609#[derive(Debug, Clone)]
611pub struct MarchingCubes {
612 pub nx: usize,
614 pub ny: usize,
616 pub nz: usize,
618 pub bounds: [f64; 6],
620 pub sdf: Vec<f64>,
622}
623
624impl MarchingCubes {
625 pub fn from_function<F>(f: &F, nx: usize, ny: usize, nz: usize, bounds: [f64; 6]) -> Self
627 where
628 F: Fn([f64; 3]) -> f64,
629 {
630 let dx = (bounds[1] - bounds[0]) / nx as f64;
631 let dy = (bounds[3] - bounds[2]) / ny as f64;
632 let dz = (bounds[5] - bounds[4]) / nz as f64;
633 let mut sdf = Vec::with_capacity((nx + 1) * (ny + 1) * (nz + 1));
634 for iz in 0..=(nz) {
635 for iy in 0..=(ny) {
636 for ix in 0..=(nx) {
637 let p = [
638 bounds[0] + ix as f64 * dx,
639 bounds[2] + iy as f64 * dy,
640 bounds[4] + iz as f64 * dz,
641 ];
642 sdf.push(f(p));
643 }
644 }
645 }
646 Self {
647 nx,
648 ny,
649 nz,
650 bounds,
651 sdf,
652 }
653 }
654
655 pub fn spacing(&self) -> [f64; 3] {
657 [
658 (self.bounds[1] - self.bounds[0]) / self.nx as f64,
659 (self.bounds[3] - self.bounds[2]) / self.ny as f64,
660 (self.bounds[5] - self.bounds[4]) / self.nz as f64,
661 ]
662 }
663
664 pub fn at(&self, ix: usize, iy: usize, iz: usize) -> f64 {
666 let stride_z = (self.ny + 1) * (self.nx + 1);
667 let stride_y = self.nx + 1;
668 self.sdf[iz * stride_z + iy * stride_y + ix]
669 }
670
671 pub fn extract(&self, iso_value: f64) -> MarchingCubesResult {
676 let mut vertices: Vec<MeshVertex> = Vec::new();
677 let mut triangles: Vec<MeshTriangle> = Vec::new();
678 let sp = self.spacing();
679
680 for iz in 0..self.nz {
681 for iy in 0..self.ny {
682 for ix in 0..self.nx {
683 let corners: [[usize; 3]; 8] = [
684 [ix, iy, iz],
685 [ix + 1, iy, iz],
686 [ix + 1, iy + 1, iz],
687 [ix, iy + 1, iz],
688 [ix, iy, iz + 1],
689 [ix + 1, iy, iz + 1],
690 [ix + 1, iy + 1, iz + 1],
691 [ix, iy + 1, iz + 1],
692 ];
693
694 let vals: [f64; 8] = std::array::from_fn(|k| {
695 self.at(corners[k][0], corners[k][1], corners[k][2])
696 });
697
698 let mut cube_idx = 0u8;
699 for k in 0..8 {
700 if vals[k] < iso_value {
701 cube_idx |= 1 << k;
702 }
703 }
704
705 if cube_idx == 0 || cube_idx == 0xFF {
706 continue;
707 }
708
709 let pos: [[f64; 3]; 8] = std::array::from_fn(|k| {
711 [
712 self.bounds[0] + corners[k][0] as f64 * sp[0],
713 self.bounds[2] + corners[k][1] as f64 * sp[1],
714 self.bounds[4] + corners[k][2] as f64 * sp[2],
715 ]
716 });
717
718 let interp = |i: usize, j: usize| -> [f64; 3] {
720 let t = if (vals[j] - vals[i]).abs() > 1e-10 {
721 (iso_value - vals[i]) / (vals[j] - vals[i])
722 } else {
723 0.5
724 };
725 add3(pos[i], scale3(sub3(pos[j], pos[i]), t))
726 };
727
728 let edges: [[f64; 3]; 12] = [
730 interp(0, 1),
731 interp(1, 2),
732 interp(2, 3),
733 interp(3, 0),
734 interp(4, 5),
735 interp(5, 6),
736 interp(6, 7),
737 interp(7, 4),
738 interp(0, 4),
739 interp(1, 5),
740 interp(2, 6),
741 interp(3, 7),
742 ];
743
744 let tris = mc_triangulate(cube_idx);
746 let base = vertices.len();
747 for e in &edges {
748 vertices.push(MeshVertex {
750 position: *e,
751 normal: [0.0, 0.0, 1.0],
752 });
753 }
754 for tri in &tris {
755 triangles.push(MeshTriangle {
756 indices: [base + tri[0], base + tri[1], base + tri[2]],
757 });
758 }
759 }
760 }
761 }
762
763 MarchingCubesResult {
764 vertices,
765 triangles,
766 }
767 }
768}
769
770fn mc_triangulate(cube_idx: u8) -> Vec<[usize; 3]> {
775 let mut tris = Vec::new();
777 let n_set = cube_idx.count_ones() as usize;
778 if n_set > 0 && n_set < 8 {
780 tris.push([0, 1, 8]);
782 if n_set > 2 {
783 tris.push([1, 2, 9]);
784 }
785 if n_set > 4 {
786 tris.push([4, 5, 10]);
787 }
788 }
789 tris
790}
791
792#[derive(Debug, Clone)]
798pub enum OctreeNode {
799 Leaf {
801 value: f64,
803 centre: [f64; 3],
805 half_size: f64,
807 },
808 Internal {
810 centre: [f64; 3],
812 half_size: f64,
814 children: Box<[OctreeNode; 8]>,
816 },
817}
818
819impl OctreeNode {
820 pub fn evaluate(&self, p: [f64; 3]) -> f64 {
822 match self {
823 OctreeNode::Leaf { value, .. } => *value,
824 OctreeNode::Internal {
825 centre,
826 children,
827 half_size: _,
828 } => {
829 let ix = if p[0] >= centre[0] { 1 } else { 0 };
830 let iy = if p[1] >= centre[1] { 1 } else { 0 };
831 let iz = if p[2] >= centre[2] { 1 } else { 0 };
832 let child_idx = ix + 2 * iy + 4 * iz;
833 children[child_idx].evaluate(p)
834 }
835 }
836 }
837
838 pub fn half_size(&self) -> f64 {
840 match self {
841 OctreeNode::Leaf { half_size, .. } => *half_size,
842 OctreeNode::Internal { half_size, .. } => *half_size,
843 }
844 }
845}
846
847#[derive(Debug, Clone)]
849pub struct OctreeSdf {
850 pub root: OctreeNode,
852 pub max_depth: usize,
854 pub refine_threshold: f64,
856}
857
858impl OctreeSdf {
859 pub fn build<F>(
861 f: &F,
862 centre: [f64; 3],
863 half_size: f64,
864 max_depth: usize,
865 refine_threshold: f64,
866 ) -> Self
867 where
868 F: Fn([f64; 3]) -> f64,
869 {
870 let root = Self::build_node(f, centre, half_size, 0, max_depth, refine_threshold);
871 Self {
872 root,
873 max_depth,
874 refine_threshold,
875 }
876 }
877
878 fn build_node<F>(
879 f: &F,
880 centre: [f64; 3],
881 half_size: f64,
882 depth: usize,
883 max_depth: usize,
884 threshold: f64,
885 ) -> OctreeNode
886 where
887 F: Fn([f64; 3]) -> f64,
888 {
889 let value = f(centre);
890 if depth >= max_depth || value.abs() > threshold {
891 return OctreeNode::Leaf {
892 value,
893 centre,
894 half_size,
895 };
896 }
897 let hs = half_size * 0.5;
898 let children: [OctreeNode; 8] = std::array::from_fn(|k| {
899 let cx = centre[0] + if k & 1 != 0 { hs } else { -hs };
900 let cy = centre[1] + if k & 2 != 0 { hs } else { -hs };
901 let cz = centre[2] + if k & 4 != 0 { hs } else { -hs };
902 Self::build_node(f, [cx, cy, cz], hs, depth + 1, max_depth, threshold)
903 });
904 OctreeNode::Internal {
905 centre,
906 half_size,
907 children: Box::new(children),
908 }
909 }
910
911 pub fn evaluate(&self, p: [f64; 3]) -> f64 {
913 self.root.evaluate(p)
914 }
915
916 pub fn count_leaves(&self) -> usize {
918 Self::count_leaves_node(&self.root)
919 }
920
921 fn count_leaves_node(node: &OctreeNode) -> usize {
922 match node {
923 OctreeNode::Leaf { .. } => 1,
924 OctreeNode::Internal { children, .. } => {
925 children.iter().map(Self::count_leaves_node).sum()
926 }
927 }
928 }
929}
930
931#[derive(Debug, Clone)]
937pub struct NarrowBandSdf {
938 pub nx: usize,
940 pub ny: usize,
942 pub nz: usize,
944 pub dx: f64,
946 pub origin: [f64; 3],
948 pub bandwidth: f64,
950 pub values: Vec<f64>,
952}
953
954impl NarrowBandSdf {
955 pub fn from_function<F>(
957 f: &F,
958 nx: usize,
959 ny: usize,
960 nz: usize,
961 dx: f64,
962 origin: [f64; 3],
963 bandwidth: f64,
964 ) -> Self
965 where
966 F: Fn([f64; 3]) -> f64,
967 {
968 let mut values = vec![f64::MAX; nx * ny * nz];
969 for iz in 0..nz {
970 for iy in 0..ny {
971 for ix in 0..nx {
972 let p = [
973 origin[0] + ix as f64 * dx,
974 origin[1] + iy as f64 * dy_from(origin[1], iy, dx),
975 origin[2] + iz as f64 * dx,
976 ];
977 let d = f(p);
978 if d.abs() <= bandwidth {
979 values[iz * ny * nx + iy * nx + ix] = d;
980 }
981 }
982 }
983 }
984 Self {
985 nx,
986 ny,
987 nz,
988 dx,
989 origin,
990 bandwidth,
991 values,
992 }
993 }
994
995 pub fn at(&self, ix: usize, iy: usize, iz: usize) -> f64 {
997 self.values[iz * self.ny * self.nx + iy * self.nx + ix]
998 }
999
1000 pub fn in_band(&self, ix: usize, iy: usize, iz: usize) -> bool {
1002 self.at(ix, iy, iz).abs() < self.bandwidth
1003 }
1004
1005 pub fn active_count(&self) -> usize {
1007 self.values.iter().filter(|&&v| v != f64::MAX).count()
1008 }
1009}
1010
1011#[inline]
1013fn dy_from(_origin_y: f64, iy: usize, dx: f64) -> f64 {
1014 iy as f64 * dx / dx }
1016
1017#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1023enum FmmState {
1024 Known,
1026 Trial,
1028 Far,
1030}
1031
1032#[derive(Debug, Clone, Copy)]
1034struct FmmEntry {
1035 neg_dist: f64,
1037 idx: usize,
1039}
1040
1041impl PartialEq for FmmEntry {
1042 fn eq(&self, other: &Self) -> bool {
1043 self.neg_dist == other.neg_dist
1044 }
1045}
1046impl Eq for FmmEntry {}
1047impl PartialOrd for FmmEntry {
1048 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1049 Some(self.cmp(other))
1050 }
1051}
1052impl Ord for FmmEntry {
1053 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1054 self.neg_dist
1055 .partial_cmp(&other.neg_dist)
1056 .unwrap_or(std::cmp::Ordering::Equal)
1057 }
1058}
1059
1060#[derive(Debug, Clone)]
1065pub struct FastMarchingMethod {
1066 pub nx: usize,
1068 pub ny: usize,
1070 pub nz: usize,
1072 pub dx: f64,
1074 pub dist: Vec<f64>,
1076 state: Vec<FmmState>,
1078}
1079
1080impl FastMarchingMethod {
1081 pub fn new(nx: usize, ny: usize, nz: usize, dx: f64) -> Self {
1083 let n = nx * ny * nz;
1084 Self {
1085 nx,
1086 ny,
1087 nz,
1088 dx,
1089 dist: vec![f64::MAX; n],
1090 state: vec![FmmState::Far; n],
1091 }
1092 }
1093
1094 #[inline]
1095 fn flat(&self, ix: usize, iy: usize, iz: usize) -> usize {
1096 iz * self.ny * self.nx + iy * self.nx + ix
1097 }
1098
1099 pub fn set_known(&mut self, known: &[(usize, f64)]) {
1101 for &(idx, d) in known {
1102 if idx < self.dist.len() {
1103 self.dist[idx] = d;
1104 self.state[idx] = FmmState::Known;
1105 }
1106 }
1107 }
1108
1109 pub fn run(&mut self) {
1111 let mut heap: BinaryHeap<FmmEntry> = BinaryHeap::new();
1112
1113 for iz in 0..self.nz {
1115 for iy in 0..self.ny {
1116 for ix in 0..self.nx {
1117 let idx = self.flat(ix, iy, iz);
1118 if self.state[idx] == FmmState::Known {
1119 self.push_neighbours(ix, iy, iz, &mut heap);
1120 }
1121 }
1122 }
1123 }
1124
1125 while let Some(entry) = heap.pop() {
1126 let cidx = entry.idx;
1127 if self.state[cidx] == FmmState::Known {
1128 continue;
1129 }
1130 self.state[cidx] = FmmState::Known;
1131 let iz = cidx / (self.ny * self.nx);
1132 let rem = cidx % (self.ny * self.nx);
1133 let iy = rem / self.nx;
1134 let ix = rem % self.nx;
1135 self.push_neighbours(ix, iy, iz, &mut heap);
1136 }
1137 }
1138
1139 fn push_neighbours(
1140 &mut self,
1141 ix: usize,
1142 iy: usize,
1143 iz: usize,
1144 heap: &mut BinaryHeap<FmmEntry>,
1145 ) {
1146 let neighbors = self.get_neighbors(ix, iy, iz);
1147 for (nx_i, ny_i, nz_i) in neighbors {
1148 let nidx = self.flat(nx_i, ny_i, nz_i);
1149 if self.state[nidx] == FmmState::Known {
1150 continue;
1151 }
1152 let d = self.solve_eikonal(nx_i, ny_i, nz_i);
1153 if d < self.dist[nidx] {
1154 self.dist[nidx] = d;
1155 self.state[nidx] = FmmState::Trial;
1156 heap.push(FmmEntry {
1157 neg_dist: -d,
1158 idx: nidx,
1159 });
1160 }
1161 }
1162 }
1163
1164 fn get_neighbors(&self, ix: usize, iy: usize, iz: usize) -> Vec<(usize, usize, usize)> {
1165 let mut ns = Vec::with_capacity(6);
1166 if ix > 0 {
1167 ns.push((ix - 1, iy, iz));
1168 }
1169 if ix + 1 < self.nx {
1170 ns.push((ix + 1, iy, iz));
1171 }
1172 if iy > 0 {
1173 ns.push((ix, iy - 1, iz));
1174 }
1175 if iy + 1 < self.ny {
1176 ns.push((ix, iy + 1, iz));
1177 }
1178 if iz > 0 {
1179 ns.push((ix, iy, iz - 1));
1180 }
1181 if iz + 1 < self.nz {
1182 ns.push((ix, iy, iz + 1));
1183 }
1184 ns
1185 }
1186
1187 fn solve_eikonal(&self, ix: usize, iy: usize, iz: usize) -> f64 {
1188 let dx = self.dx;
1190 let mut terms: [f64; 3] = [f64::MAX; 3];
1191
1192 let mut d_x = f64::MAX;
1194 if ix > 0 {
1195 d_x = d_x.min(self.dist[self.flat(ix - 1, iy, iz)]);
1196 }
1197 if ix + 1 < self.nx {
1198 d_x = d_x.min(self.dist[self.flat(ix + 1, iy, iz)]);
1199 }
1200 terms[0] = d_x;
1201
1202 let mut d_y = f64::MAX;
1204 if iy > 0 {
1205 d_y = d_y.min(self.dist[self.flat(ix, iy - 1, iz)]);
1206 }
1207 if iy + 1 < self.ny {
1208 d_y = d_y.min(self.dist[self.flat(ix, iy + 1, iz)]);
1209 }
1210 terms[1] = d_y;
1211
1212 let mut d_z = f64::MAX;
1214 if iz > 0 {
1215 d_z = d_z.min(self.dist[self.flat(ix, iy, iz - 1)]);
1216 }
1217 if iz + 1 < self.nz {
1218 d_z = d_z.min(self.dist[self.flat(ix, iy, iz + 1)]);
1219 }
1220 terms[2] = d_z;
1221
1222 terms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1223
1224 for k in 1..=3 {
1226 let valid: Vec<f64> = terms[..k]
1227 .iter()
1228 .filter(|&&t| t < f64::MAX)
1229 .copied()
1230 .collect();
1231 if valid.is_empty() {
1232 continue;
1233 }
1234 let sum_t = valid.iter().sum::<f64>();
1235 let sum_t2 = valid.iter().map(|t| t * t).sum::<f64>();
1236 let n_v = valid.len() as f64;
1237 let discriminant = sum_t * sum_t - n_v * (sum_t2 - dx * dx);
1238 if discriminant >= 0.0 {
1239 let sol = (sum_t + discriminant.sqrt()) / n_v;
1240 if k == 1 || sol > *valid.last().expect("collection should not be empty") {
1241 return sol;
1242 }
1243 }
1244 }
1245
1246 terms
1248 .iter()
1249 .copied()
1250 .filter(|&t| t < f64::MAX)
1251 .fold(f64::MAX, f64::min)
1252 + dx
1253 }
1254
1255 pub fn distance_at(&self, ix: usize, iy: usize, iz: usize) -> f64 {
1257 self.dist[self.flat(ix, iy, iz)]
1258 }
1259}
1260
1261#[derive(Debug, Clone)]
1267pub struct SdfObject {
1268 pub name: String,
1270 pub translation: [f64; 3],
1272 pub scale: f64,
1274 pub kind: SdfKind,
1276}
1277
1278#[derive(Debug, Clone)]
1280pub enum SdfKind {
1281 Sphere(f64),
1283 Box([f64; 3]),
1285 Capsule([f64; 3], [f64; 3], f64),
1287 Cylinder(f64, f64),
1289 Torus(f64, f64),
1291 Plane([f64; 3], f64),
1293}
1294
1295impl SdfObject {
1296 pub fn sphere(name: &str, radius: f64, translation: [f64; 3]) -> Self {
1298 Self {
1299 name: name.to_string(),
1300 translation,
1301 scale: 1.0,
1302 kind: SdfKind::Sphere(radius),
1303 }
1304 }
1305
1306 pub fn box_shape(name: &str, half_extents: [f64; 3], translation: [f64; 3]) -> Self {
1308 Self {
1309 name: name.to_string(),
1310 translation,
1311 scale: 1.0,
1312 kind: SdfKind::Box(half_extents),
1313 }
1314 }
1315
1316 pub fn evaluate(&self, p: [f64; 3]) -> f64 {
1318 let local = scale3(sub3(p, self.translation), 1.0 / self.scale);
1320 let raw = match &self.kind {
1321 SdfKind::Sphere(r) => sdf_sphere(local, *r),
1322 SdfKind::Box(b) => sdf_box(local, *b),
1323 SdfKind::Capsule(a, b, r) => sdf_capsule(local, *a, *b, *r),
1324 SdfKind::Cylinder(r, h) => sdf_cylinder(local, *r, *h),
1325 SdfKind::Torus(r1, r2) => sdf_torus(local, *r1, *r2),
1326 SdfKind::Plane(n, d) => sdf_plane(local, *n, *d),
1327 };
1328 raw * self.scale
1329 }
1330}
1331
1332#[derive(Debug, Clone, Default)]
1334pub struct SdfScene {
1335 pub objects: Vec<SdfObject>,
1337 pub blend_k: f64,
1339}
1340
1341impl SdfScene {
1342 pub fn new() -> Self {
1344 Self {
1345 objects: Vec::new(),
1346 blend_k: 0.0,
1347 }
1348 }
1349
1350 pub fn add(&mut self, obj: SdfObject) {
1352 self.objects.push(obj);
1353 }
1354
1355 pub fn evaluate(&self, p: [f64; 3]) -> f64 {
1357 if self.objects.is_empty() {
1358 return f64::MAX;
1359 }
1360 let mut d = self.objects[0].evaluate(p);
1361 for obj in &self.objects[1..] {
1362 let di = obj.evaluate(p);
1363 d = if self.blend_k > 0.0 {
1364 sdf_smooth_union(d, di, self.blend_k)
1365 } else {
1366 sdf_union(d, di)
1367 };
1368 }
1369 d
1370 }
1371
1372 pub fn ray_cast(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<RayMarchHit> {
1374 let marcher = RayMarcher::new();
1375 marcher.march(&|p| self.evaluate(p), origin, dir)
1376 }
1377
1378 pub fn normal(&self, p: [f64; 3]) -> [f64; 3] {
1380 sdf_normal(&|q| self.evaluate(q), p, 1e-4)
1381 }
1382}
1383
1384pub fn sdf_twist<F>(f: &F, p: [f64; 3], twist_k: f64) -> f64
1390where
1391 F: Fn([f64; 3]) -> f64,
1392{
1393 let c = (twist_k * p[1]).cos();
1394 let s = (twist_k * p[1]).sin();
1395 let q = [c * p[0] - s * p[2], p[1], s * p[0] + c * p[2]];
1396 f(q)
1397}
1398
1399pub fn sdf_bend<F>(f: &F, p: [f64; 3], bend_k: f64) -> f64
1401where
1402 F: Fn([f64; 3]) -> f64,
1403{
1404 let c = (bend_k * p[0]).cos();
1405 let s = (bend_k * p[0]).sin();
1406 let q = [c * p[0] - s * p[1], s * p[0] + c * p[1], p[2]];
1407 f(q)
1408}
1409
1410pub fn sdf_mirror_y<F>(f: &F, p: [f64; 3]) -> f64
1412where
1413 F: Fn([f64; 3]) -> f64,
1414{
1415 f([p[0], p[1].abs(), p[2]])
1416}
1417
1418pub fn sdf_repeat<F>(f: &F, p: [f64; 3], c: [f64; 3]) -> f64
1420where
1421 F: Fn([f64; 3]) -> f64,
1422{
1423 let q = [
1424 p[0] - c[0] * (p[0] / c[0]).round(),
1425 p[1] - c[1] * (p[1] / c[1]).round(),
1426 p[2] - c[2] * (p[2] / c[2]).round(),
1427 ];
1428 f(q)
1429}
1430
1431pub fn sdf_displace<F, G>(base: &F, displacement: &G, p: [f64; 3]) -> f64
1433where
1434 F: Fn([f64; 3]) -> f64,
1435 G: Fn([f64; 3]) -> f64,
1436{
1437 base(p) + displacement(p)
1438}
1439
1440pub fn sdf_morph<F, G>(a: &F, b: &G, p: [f64; 3], t: f64) -> f64
1444where
1445 F: Fn([f64; 3]) -> f64,
1446 G: Fn([f64; 3]) -> f64,
1447{
1448 (1.0 - t) * a(p) + t * b(p)
1449}
1450
1451pub fn sdf_volume_estimate<F>(f: &F, bounds: [f64; 6], n_samples: usize) -> f64
1455where
1456 F: Fn([f64; 3]) -> f64,
1457{
1458 let mut rng = rand::rng();
1459 let lx = bounds[1] - bounds[0];
1460 let ly = bounds[3] - bounds[2];
1461 let lz = bounds[5] - bounds[4];
1462 let total_vol = lx * ly * lz;
1463
1464 let mut inside = 0usize;
1465 for _ in 0..n_samples {
1466 let x = bounds[0] + rng.random_range(0.0..lx);
1467 let y = bounds[2] + rng.random_range(0.0..ly);
1468 let z = bounds[4] + rng.random_range(0.0..lz);
1469 if f([x, y, z]) <= 0.0 {
1470 inside += 1;
1471 }
1472 }
1473 total_vol * inside as f64 / n_samples as f64
1474}
1475
1476pub fn sdf_surface_area_estimate<F>(f: &F, bounds: [f64; 6], nx: usize, ny: usize, nz: usize) -> f64
1480where
1481 F: Fn([f64; 3]) -> f64,
1482{
1483 let dx = (bounds[1] - bounds[0]) / nx as f64;
1484 let dy = (bounds[3] - bounds[2]) / ny as f64;
1485 let dz = (bounds[5] - bounds[4]) / nz as f64;
1486 let eps = dx.min(dy).min(dz) * 0.5;
1487 let cell_vol = dx * dy * dz;
1488
1489 let mut area = 0.0;
1490 for iz in 0..nz {
1491 for iy in 0..ny {
1492 for ix in 0..nx {
1493 let p = [
1494 bounds[0] + (ix as f64 + 0.5) * dx,
1495 bounds[2] + (iy as f64 + 0.5) * dy,
1496 bounds[4] + (iz as f64 + 0.5) * dz,
1497 ];
1498 let d = f(p);
1499 if d.abs() < eps * 2.0 {
1501 let grad = sdf_gradient(f, p, eps);
1502 let gn = norm3(grad);
1503 area += gn * cell_vol / (2.0 * eps);
1504 }
1505 }
1506 }
1507 }
1508 area
1509}
1510
1511pub fn sdf_bounding_box<F>(f: &F, search: [f64; 6], n: usize) -> [f64; 6]
1516where
1517 F: Fn([f64; 3]) -> f64,
1518{
1519 let dx = (search[1] - search[0]) / n as f64;
1520 let dy = (search[3] - search[2]) / n as f64;
1521 let dz = (search[5] - search[4]) / n as f64;
1522
1523 let mut xmin = f64::MAX;
1524 let mut xmax = f64::MIN;
1525 let mut ymin = f64::MAX;
1526 let mut ymax = f64::MIN;
1527 let mut zmin = f64::MAX;
1528 let mut zmax = f64::MIN;
1529
1530 for iz in 0..=n {
1531 for iy in 0..=n {
1532 for ix in 0..=n {
1533 let p = [
1534 search[0] + ix as f64 * dx,
1535 search[2] + iy as f64 * dy,
1536 search[4] + iz as f64 * dz,
1537 ];
1538 if f(p) <= 0.0 {
1539 xmin = xmin.min(p[0]);
1540 xmax = xmax.max(p[0]);
1541 ymin = ymin.min(p[1]);
1542 ymax = ymax.max(p[1]);
1543 zmin = zmin.min(p[2]);
1544 zmax = zmax.max(p[2]);
1545 }
1546 }
1547 }
1548 }
1549 [xmin, xmax, ymin, ymax, zmin, zmax]
1550}
1551
1552#[cfg(test)]
1557mod tests {
1558 use super::*;
1559 use std::f64::consts::PI;
1560
1561 #[test]
1564 fn test_sdf_sphere_inside() {
1565 let d = sdf_sphere([0.0, 0.0, 0.0], 1.0);
1566 assert!(d < 0.0, "origin should be inside sphere, d={:.6}", d);
1567 }
1568
1569 #[test]
1570 fn test_sdf_sphere_outside() {
1571 let d = sdf_sphere([2.0, 0.0, 0.0], 1.0);
1572 assert!(
1573 d > 0.0,
1574 "point outside sphere should have positive SDF, d={:.6}",
1575 d
1576 );
1577 }
1578
1579 #[test]
1580 fn test_sdf_sphere_on_surface() {
1581 let d = sdf_sphere([1.0, 0.0, 0.0], 1.0);
1582 assert!(d.abs() < 1e-10, "point on sphere surface: d={:.6}", d);
1583 }
1584
1585 #[test]
1586 fn test_sdf_box_inside() {
1587 let d = sdf_box([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1588 assert!(d < 0.0, "origin inside box should be negative: d={:.6}", d);
1589 }
1590
1591 #[test]
1592 fn test_sdf_box_outside() {
1593 let d = sdf_box([2.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
1594 assert!(d > 0.0, "point outside box: d={:.6}", d);
1595 }
1596
1597 #[test]
1598 fn test_sdf_capsule_inside() {
1599 let d = sdf_capsule([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], 0.5);
1600 assert!(d < 0.0, "origin inside capsule: d={:.6}", d);
1601 }
1602
1603 #[test]
1604 fn test_sdf_cylinder_inside() {
1605 let d = sdf_cylinder([0.0, 0.0, 0.0], 1.0, 2.0);
1606 assert!(d < 0.0, "origin inside cylinder: d={:.6}", d);
1607 }
1608
1609 #[test]
1610 fn test_sdf_torus_outside_tube() {
1611 let d = sdf_torus([0.0, 0.0, 0.0], 2.0, 0.5);
1613 assert!(d > 0.0, "origin outside torus tube: d={:.6}", d);
1615 }
1616
1617 #[test]
1620 fn test_sdf_union_takes_min() {
1621 assert_eq!(sdf_union(1.0, -0.5), -0.5);
1622 assert_eq!(sdf_union(-1.0, 0.5), -1.0);
1623 }
1624
1625 #[test]
1626 fn test_sdf_intersection_takes_max() {
1627 assert_eq!(sdf_intersection(1.0, -0.5), 1.0);
1628 assert_eq!(sdf_intersection(-1.0, 0.5), 0.5);
1629 }
1630
1631 #[test]
1632 fn test_sdf_difference_example() {
1633 let d = sdf_difference(1.0, -0.5);
1635 assert!(d > 0.0, "difference should be positive here: d={:.6}", d);
1636 }
1637
1638 #[test]
1641 fn test_smooth_union_between_values() {
1642 let k = 0.5;
1643 let su = sdf_smooth_union(1.0, 1.0, k);
1644 assert!(
1646 su <= 1.0,
1647 "smooth union should be <= min(a,b) at equal values: {:.6}",
1648 su
1649 );
1650 }
1651
1652 #[test]
1653 fn test_smooth_union_far_apart_is_like_union() {
1654 let k = 0.1;
1656 let a = 10.0;
1657 let b = -5.0;
1658 let su = sdf_smooth_union(a, b, k);
1659 let u = sdf_union(a, b);
1660 assert!(
1661 (su - u).abs() < 0.5,
1662 "smooth union far apart: su={:.6}, u={:.6}",
1663 su,
1664 u
1665 );
1666 }
1667
1668 #[test]
1669 fn test_smooth_intersection_ge_max_sometimes() {
1670 let k = 1.0;
1671 let si = sdf_smooth_intersection(0.5, 0.5, k);
1672 assert!((si - 0.5).abs() < 0.3);
1674 }
1675
1676 #[test]
1679 fn test_gradient_points_outward_sphere() {
1680 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1681 let p = [1.5, 0.0, 0.0];
1682 let g = sdf_gradient(&f, p, 1e-4);
1683 assert!(
1685 g[0] > 0.0,
1686 "gradient x-component should be positive: {:.6}",
1687 g[0]
1688 );
1689 assert!(
1690 g[1].abs() < 0.01,
1691 "gradient y-component should be ~0: {:.6}",
1692 g[1]
1693 );
1694 }
1695
1696 #[test]
1697 fn test_normal_is_unit_length() {
1698 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1699 let p = [1.5, 0.5, 0.0];
1700 let n = sdf_normal(&f, p, 1e-4);
1701 let len = norm3(n);
1702 assert!(
1703 (len - 1.0).abs() < 1e-4,
1704 "normal should be unit length: {:.6}",
1705 len
1706 );
1707 }
1708
1709 #[test]
1712 fn test_ray_march_hits_sphere() {
1713 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1714 let marcher = RayMarcher::new();
1715 let hit = marcher.march(&f, [5.0, 0.0, 0.0], [-1.0, 0.0, 0.0]);
1716 assert!(hit.is_some(), "ray along -x should hit sphere at origin");
1717 let h = hit.unwrap();
1718 assert!(
1719 (h.t - 4.0).abs() < 0.01,
1720 "hit distance should be ~4: {:.6}",
1721 h.t
1722 );
1723 }
1724
1725 #[test]
1726 fn test_ray_march_misses_sphere() {
1727 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1728 let marcher = RayMarcher::new();
1729 let hit = marcher.march(&f, [5.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1730 assert!(hit.is_none(), "ray away from sphere should not hit");
1731 }
1732
1733 #[test]
1736 fn test_voronoi_sdf_nearest_seed() {
1737 let seeds = vec![[0.0, 0.0, 0.0], [5.0, 0.0, 0.0]];
1738 let vsdf = VoronoiSdf::new(seeds);
1739 let nearest = vsdf.nearest_seed([1.0, 0.0, 0.0]);
1740 assert_eq!(nearest, 0, "nearest seed to [1,0,0] should be index 0");
1741 }
1742
1743 #[test]
1744 fn test_voronoi_sdf_positive_near_boundary() {
1745 let seeds = vec![[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]];
1746 let vsdf = VoronoiSdf::new(seeds);
1747 let d = vsdf.evaluate([2.0, 0.0, 0.0]);
1749 assert!(
1750 d.abs() < 1e-10,
1751 "midpoint Voronoi SDF should be ~0: {:.6}",
1752 d
1753 );
1754 }
1755
1756 #[test]
1759 fn test_octree_sdf_inside_sphere() {
1760 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1761 let octree = OctreeSdf::build(&f, [0.0, 0.0, 0.0], 2.0, 4, 0.5);
1762 let d = octree.evaluate([0.0, 0.0, 0.0]);
1763 assert!(d < 0.0, "octree SDF at origin inside sphere: d={:.6}", d);
1764 }
1765
1766 #[test]
1767 fn test_octree_sdf_has_leaves() {
1768 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1769 let octree = OctreeSdf::build(&f, [0.0, 0.0, 0.0], 2.0, 3, 1.0);
1770 assert!(
1771 octree.count_leaves() > 0,
1772 "octree should have at least one leaf"
1773 );
1774 }
1775
1776 #[test]
1779 fn test_marching_cubes_sphere_produces_vertices() {
1780 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1781 let mc = MarchingCubes::from_function(&f, 8, 8, 8, [-2.0, 2.0, -2.0, 2.0, -2.0, 2.0]);
1782 let result = mc.extract(0.0);
1783 assert!(result.n_vertices() > 0 || result.n_triangles() == 0);
1785 }
1786
1787 #[test]
1788 fn test_marching_cubes_spacing() {
1789 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1790 let mc = MarchingCubes::from_function(&f, 4, 4, 4, [0.0, 4.0, 0.0, 4.0, 0.0, 4.0]);
1791 let sp = mc.spacing();
1792 assert!((sp[0] - 1.0).abs() < 1e-10);
1793 }
1794
1795 #[test]
1798 fn test_narrow_band_active_count() {
1799 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1800 let nb = NarrowBandSdf::from_function(&f, 10, 10, 10, 0.4, [-2.0, -2.0, -2.0], 1.0);
1801 assert!(
1803 nb.active_count() > 0,
1804 "narrow band should have active cells"
1805 );
1806 }
1807
1808 #[test]
1811 fn test_fmm_propagates_distance() {
1812 let mut fmm = FastMarchingMethod::new(5, 5, 5, 1.0);
1813 let centre = fmm.flat(2, 2, 2);
1815 fmm.set_known(&[(centre, 0.0)]);
1816 fmm.run();
1817 let d = fmm.distance_at(3, 2, 2);
1819 assert!(d > 0.0 && d <= 2.0, "FMM neighbour distance: {:.6}", d);
1820 }
1821
1822 #[test]
1823 fn test_fmm_distance_increases_with_steps() {
1824 let mut fmm = FastMarchingMethod::new(7, 1, 1, 1.0);
1825 let seed = fmm.flat(3, 0, 0);
1826 fmm.set_known(&[(seed, 0.0)]);
1827 fmm.run();
1828 let d1 = fmm.distance_at(4, 0, 0);
1829 let d2 = fmm.distance_at(5, 0, 0);
1830 assert!(
1831 d2 >= d1,
1832 "FMM distance should increase with steps: d1={:.6}, d2={:.6}",
1833 d1,
1834 d2
1835 );
1836 }
1837
1838 #[test]
1841 fn test_sdf_scene_single_sphere() {
1842 let mut scene = SdfScene::new();
1843 scene.add(SdfObject::sphere("s", 1.0, [0.0, 0.0, 0.0]));
1844 let d = scene.evaluate([0.0, 0.0, 0.0]);
1845 assert!(d < 0.0, "origin should be inside scene sphere: d={:.6}", d);
1846 }
1847
1848 #[test]
1849 fn test_sdf_scene_union_two_spheres() {
1850 let mut scene = SdfScene::new();
1851 scene.add(SdfObject::sphere("a", 1.0, [-2.0, 0.0, 0.0]));
1852 scene.add(SdfObject::sphere("b", 1.0, [2.0, 0.0, 0.0]));
1853 let d = scene.evaluate([0.0, 0.0, 0.0]);
1855 assert!(
1856 d > 0.0,
1857 "midpoint between two spheres should be outside: d={:.6}",
1858 d
1859 );
1860 }
1861
1862 #[test]
1865 fn test_closest_point_on_sphere() {
1866 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1867 let (closest, _d0) = closest_point_on_sdf(&f, [3.0, 0.0, 0.0], 100, 1e-5);
1868 let dist_from_origin = norm3(closest);
1869 assert!(
1870 (dist_from_origin - 1.0).abs() < 0.01,
1871 "closest point should be on sphere surface: dist={:.6}",
1872 dist_from_origin
1873 );
1874 }
1875
1876 #[test]
1879 fn test_volume_estimate_sphere() {
1880 let f = |p: [f64; 3]| sdf_sphere(p, 1.0);
1881 let vol = sdf_volume_estimate(&f, [-1.5, 1.5, -1.5, 1.5, -1.5, 1.5], 10000);
1882 let exact = 4.0 / 3.0 * PI;
1883 assert!(
1885 (vol - exact).abs() / exact < 0.15,
1886 "volume estimate should be close to {:.6}: got {:.6}",
1887 exact,
1888 vol
1889 );
1890 }
1891}