1#[derive(Debug, Clone, PartialEq)]
20pub enum SdfPrimitive {
21 Sphere {
23 center: [f64; 3],
25 radius: f64,
27 },
28 Box3d {
30 center: [f64; 3],
32 half_extents: [f64; 3],
34 },
35 Cylinder {
37 center: [f64; 3],
39 radius: f64,
41 height: f64,
43 },
44 Torus {
46 center: [f64; 3],
48 r_major: f64,
50 r_minor: f64,
52 },
53}
54
55impl SdfPrimitive {
56 pub fn evaluate(&self, p: [f64; 3]) -> f64 {
58 match self {
59 SdfPrimitive::Sphere { center, radius } => sdf_sphere(p, *center, *radius),
60 SdfPrimitive::Box3d {
61 center,
62 half_extents,
63 } => sdf_box(p, *center, *half_extents),
64 SdfPrimitive::Cylinder {
65 center,
66 radius,
67 height,
68 } => sdf_cylinder(p, *center, *radius, *height),
69 SdfPrimitive::Torus {
70 center,
71 r_major,
72 r_minor,
73 } => sdf_torus(p, *center, *r_major, *r_minor),
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq)]
84pub enum SdfOp {
85 Union,
87 Intersection,
89 Subtraction,
91 SmoothUnion {
93 k: f64,
95 },
96 Offset {
98 d: f64,
100 },
101}
102
103impl SdfOp {
104 pub fn apply(&self, d1: f64, d2: f64) -> f64 {
107 match self {
108 SdfOp::Union => d1.min(d2),
109 SdfOp::Intersection => d1.max(d2),
110 SdfOp::Subtraction => d1.max(-d2),
111 SdfOp::SmoothUnion { k } => sdf_smooth_union(d1, d2, *k),
112 SdfOp::Offset { d } => d1 + d,
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
123pub enum SdfNode {
124 Leaf(SdfPrimitive),
126 Op {
128 op: SdfOp,
130 left: Box<SdfNode>,
132 right: Box<SdfNode>,
134 },
135}
136
137impl SdfNode {
138 pub fn leaf(prim: SdfPrimitive) -> Self {
140 SdfNode::Leaf(prim)
141 }
142
143 pub fn op(op: SdfOp, left: SdfNode, right: SdfNode) -> Self {
145 SdfNode::Op {
146 op,
147 left: Box::new(left),
148 right: Box::new(right),
149 }
150 }
151
152 pub fn evaluate(&self, p: [f64; 3]) -> f64 {
154 match self {
155 SdfNode::Leaf(prim) => prim.evaluate(p),
156 SdfNode::Op { op, left, right } => {
157 let d1 = left.evaluate(p);
158 let d2 = right.evaluate(p);
159 op.apply(d1, d2)
160 }
161 }
162 }
163}
164
165#[derive(Debug, Clone)]
174pub struct SdfGrid {
175 pub dimensions: [usize; 3],
177 pub spacing: f64,
179 pub origin: [f64; 3],
181 pub data: Vec<f32>,
185}
186
187impl SdfGrid {
188 pub fn new(dimensions: [usize; 3], spacing: f64, origin: [f64; 3]) -> Self {
190 let n = dimensions[0] * dimensions[1] * dimensions[2];
191 Self {
192 dimensions,
193 spacing,
194 origin,
195 data: vec![f32::MAX; n],
196 }
197 }
198
199 pub fn voxel_center(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
201 [
202 self.origin[0] + self.spacing * (i as f64 + 0.5),
203 self.origin[1] + self.spacing * (j as f64 + 0.5),
204 self.origin[2] + self.spacing * (k as f64 + 0.5),
205 ]
206 }
207
208 pub fn index(&self, i: usize, j: usize, k: usize) -> usize {
210 i + self.dimensions[0] * (j + self.dimensions[1] * k)
211 }
212
213 pub fn compute_from_sdf(&mut self, sdf: &dyn Fn([f64; 3]) -> f64) {
218 let [nx, ny, nz] = self.dimensions;
219 for k in 0..nz {
220 for j in 0..ny {
221 for i in 0..nx {
222 let p = self.voxel_center(i, j, k);
223 let idx = self.index(i, j, k);
224 self.data[idx] = sdf(p) as f32;
225 }
226 }
227 }
228 }
229
230 pub fn len(&self) -> usize {
232 self.dimensions[0] * self.dimensions[1] * self.dimensions[2]
233 }
234
235 pub fn is_empty(&self) -> bool {
237 self.len() == 0
238 }
239}
240
241#[derive(Debug, Clone, Default)]
250pub struct GpuSdfCompute;
251
252impl GpuSdfCompute {
253 pub fn new() -> Self {
255 Self
256 }
257
258 pub fn dispatch_compute(&self, grid: &mut SdfGrid, primitives: &[SdfPrimitive]) {
263 if primitives.is_empty() {
264 return;
265 }
266 grid.compute_from_sdf(&|p| {
267 primitives
268 .iter()
269 .map(|prim| prim.evaluate(p))
270 .fold(f64::INFINITY, f64::min)
271 });
272 }
273}
274
275pub fn sdf_sphere(p: [f64; 3], center: [f64; 3], r: f64) -> f64 {
283 let dx = p[0] - center[0];
284 let dy = p[1] - center[1];
285 let dz = p[2] - center[2];
286 (dx * dx + dy * dy + dz * dz).sqrt() - r
287}
288
289pub fn sdf_box(p: [f64; 3], center: [f64; 3], b: [f64; 3]) -> f64 {
292 let qx = (p[0] - center[0]).abs() - b[0];
293 let qy = (p[1] - center[1]).abs() - b[1];
294 let qz = (p[2] - center[2]).abs() - b[2];
295 let outer =
296 (qx.max(0.0) * qx.max(0.0) + qy.max(0.0) * qy.max(0.0) + qz.max(0.0) * qz.max(0.0)).sqrt();
297 let inner = qx.max(qy).max(qz).min(0.0);
298 outer + inner
299}
300
301#[allow(dead_code)]
304pub fn sdf_cylinder(p: [f64; 3], center: [f64; 3], radius: f64, height: f64) -> f64 {
305 let dx = p[0] - center[0];
306 let dz = p[2] - center[2];
307 let radial = (dx * dx + dz * dz).sqrt() - radius;
308 let axial = (p[1] - center[1]).abs() - height * 0.5;
309 let outer = (radial.max(0.0) * radial.max(0.0) + axial.max(0.0) * axial.max(0.0)).sqrt();
310 let inner = radial.max(axial).min(0.0);
311 outer + inner
312}
313
314#[allow(dead_code)]
317pub fn sdf_torus(p: [f64; 3], center: [f64; 3], r_major: f64, r_minor: f64) -> f64 {
318 let dx = p[0] - center[0];
319 let dy = p[1] - center[1];
320 let dz = p[2] - center[2];
321 let xz_dist = (dx * dx + dz * dz).sqrt();
323 let qx = xz_dist - r_major;
324 let qy = dy;
325 (qx * qx + qy * qy).sqrt() - r_minor
326}
327
328pub fn sdf_smooth_union(d1: f64, d2: f64, k: f64) -> f64 {
335 if k <= 0.0 {
336 return d1.min(d2);
337 }
338 let h = (0.5 + 0.5 * (d2 - d1) / k).clamp(0.0, 1.0);
339 d1 * (1.0 - h) + d2 * h - k * h * (1.0 - h)
340}
341
342pub fn sdf_gradient(sdf: &dyn Fn([f64; 3]) -> f64, p: [f64; 3]) -> [f64; 3] {
347 const EPS: f64 = 1e-4;
348 let gx = sdf([p[0] + EPS, p[1], p[2]]) - sdf([p[0] - EPS, p[1], p[2]]);
349 let gy = sdf([p[0], p[1] + EPS, p[2]]) - sdf([p[0], p[1] - EPS, p[2]]);
350 let gz = sdf([p[0], p[1], p[2] + EPS]) - sdf([p[0], p[1], p[2] - EPS]);
351 let len = (gx * gx + gy * gy + gz * gz).sqrt();
352 if len < 1e-15 {
353 [0.0, 1.0, 0.0]
354 } else {
355 [gx / len, gy / len, gz / len]
356 }
357}
358
359#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
370 fn test_sphere_surface() {
371 let d = sdf_sphere([1.0, 0.0, 0.0], [0.0; 3], 1.0);
372 assert!(d.abs() < 1e-10, "expected ~0, got {d}");
373 }
374
375 #[test]
376 fn test_sphere_outside() {
377 let d = sdf_sphere([2.0, 0.0, 0.0], [0.0; 3], 1.0);
378 assert!((d - 1.0).abs() < 1e-10);
379 }
380
381 #[test]
382 fn test_sphere_inside() {
383 let d = sdf_sphere([0.0; 3], [0.0; 3], 1.0);
384 assert!((d - (-1.0)).abs() < 1e-10);
385 }
386
387 #[test]
388 fn test_sphere_offset_center() {
389 let d = sdf_sphere([3.0, 0.0, 0.0], [2.0, 0.0, 0.0], 1.0);
390 assert!(d.abs() < 1e-10);
391 }
392
393 #[test]
394 fn test_sphere_diagonal() {
395 let expected = 3_f64.sqrt() - 1.0;
397 let d = sdf_sphere([1.0, 1.0, 1.0], [0.0; 3], 1.0);
398 assert!((d - expected).abs() < 1e-10);
399 }
400
401 #[test]
404 fn test_box_outside_x() {
405 let d = sdf_box([1.5, 0.0, 0.0], [0.0; 3], [0.5; 3]);
407 assert!((d - 1.0).abs() < 1e-10, "got {d}");
408 }
409
410 #[test]
411 fn test_box_inside() {
412 let d = sdf_box([0.0; 3], [0.0; 3], [1.0; 3]);
414 assert!(d < 0.0, "expected negative, got {d}");
415 }
416
417 #[test]
418 fn test_box_on_face() {
419 let d = sdf_box([1.0, 0.0, 0.0], [0.0; 3], [1.0; 3]);
421 assert!(d.abs() < 1e-10, "got {d}");
422 }
423
424 #[test]
425 fn test_box_corner() {
426 let d = sdf_box([1.0, 1.0, 1.0], [0.0; 3], [1.0; 3]);
428 assert!(d.abs() < 1e-10, "got {d}");
429 }
430
431 #[test]
432 fn test_box_asymmetric_extents() {
433 let d = sdf_box([2.0, 0.0, 0.0], [0.0; 3], [1.0, 2.0, 3.0]);
435 assert!((d - 1.0).abs() < 1e-10, "got {d}");
436 }
437
438 #[test]
441 fn test_smooth_union_zero_k_equals_min() {
442 let d1 = 1.0_f64;
443 let d2 = 2.0_f64;
444 assert!((sdf_smooth_union(d1, d2, 0.0) - d1.min(d2)).abs() < 1e-12);
445 }
446
447 #[test]
448 fn test_smooth_union_equal_inputs() {
449 let d = 1.0_f64;
451 let k = 0.5_f64;
452 let result = sdf_smooth_union(d, d, k);
453 let expected = d - k / 4.0;
454 assert!((result - expected).abs() < 1e-10, "got {result}");
455 }
456
457 #[test]
458 fn test_smooth_union_approaches_min_for_large_k() {
459 let d1 = 0.0_f64;
461 let d2 = 100.0_f64;
462 let result = sdf_smooth_union(d1, d2, 1.0);
463 assert!(result <= d2 + 1e-9);
468 }
469
470 #[test]
471 fn test_smooth_union_negative_k_is_min() {
472 let result = sdf_smooth_union(1.0, 2.0, -1.0);
473 assert!((result - 1.0_f64.min(2.0)).abs() < 1e-12);
474 }
475
476 #[test]
477 fn test_smooth_union_symmetric() {
478 let a = 0.3_f64;
480 let b = 0.7_f64;
481 let k = 0.4_f64;
482 let ab = sdf_smooth_union(a, b, k);
483 let ba = sdf_smooth_union(b, a, k);
484 assert!((ab - ba).abs() < 1e-12, "not symmetric: {ab} vs {ba}");
485 }
486
487 #[test]
490 fn test_gradient_sphere_outward() {
491 let sdf = |p: [f64; 3]| sdf_sphere(p, [0.0; 3], 1.0);
493 let g = sdf_gradient(&sdf, [1.0, 0.0, 0.0]);
494 assert!((g[0] - 1.0).abs() < 1e-3, "gx={}", g[0]);
495 assert!(g[1].abs() < 1e-3);
496 assert!(g[2].abs() < 1e-3);
497 }
498
499 #[test]
500 fn test_gradient_is_unit_length() {
501 let sdf = |p: [f64; 3]| sdf_sphere(p, [0.0; 3], 1.0);
502 let g = sdf_gradient(&sdf, [2.0, 0.0, 0.0]);
503 let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
504 assert!((len - 1.0).abs() < 1e-6, "len={len}");
505 }
506
507 #[test]
508 fn test_gradient_box_face_normal() {
509 let sdf = |p: [f64; 3]| sdf_box(p, [0.0; 3], [1.0; 3]);
511 let g = sdf_gradient(&sdf, [2.0, 0.0, 0.0]);
512 assert!((g[0] - 1.0).abs() < 1e-3, "gx={}", g[0]);
513 }
514
515 #[test]
516 fn test_gradient_smooth_union() {
517 let sdf = |p: [f64; 3]| {
518 let d1 = sdf_sphere(p, [-1.0, 0.0, 0.0], 0.5);
519 let d2 = sdf_sphere(p, [1.0, 0.0, 0.0], 0.5);
520 sdf_smooth_union(d1, d2, 0.3)
521 };
522 let g = sdf_gradient(&sdf, [0.0, 0.0, 3.0]);
523 let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
524 assert!((len - 1.0).abs() < 1e-4, "len={len}");
525 }
526
527 #[test]
530 fn test_primitive_sphere_evaluate() {
531 let prim = SdfPrimitive::Sphere {
532 center: [0.0; 3],
533 radius: 2.0,
534 };
535 assert!((prim.evaluate([2.0, 0.0, 0.0])).abs() < 1e-10);
536 }
537
538 #[test]
539 fn test_primitive_box_evaluate() {
540 let prim = SdfPrimitive::Box3d {
541 center: [0.0; 3],
542 half_extents: [1.0; 3],
543 };
544 let d = prim.evaluate([0.0; 3]);
545 assert!(d < 0.0);
546 }
547
548 #[test]
549 fn test_primitive_cylinder_evaluate() {
550 let prim = SdfPrimitive::Cylinder {
551 center: [0.0; 3],
552 radius: 1.0,
553 height: 2.0,
554 };
555 let d = prim.evaluate([1.0, 0.0, 0.0]);
557 assert!(d.abs() < 1e-10, "d={d}");
558 }
559
560 #[test]
561 fn test_primitive_torus_evaluate() {
562 let prim = SdfPrimitive::Torus {
563 center: [0.0; 3],
564 r_major: 2.0,
565 r_minor: 0.5,
566 };
567 let d = prim.evaluate([2.5, 0.0, 0.0]);
569 assert!(d.abs() < 1e-10, "d={d}");
570 }
571
572 #[test]
575 fn test_op_union() {
576 let op = SdfOp::Union;
577 assert!((op.apply(1.0, 2.0) - 1.0).abs() < 1e-12);
578 }
579
580 #[test]
581 fn test_op_intersection() {
582 let op = SdfOp::Intersection;
583 assert!((op.apply(1.0, 2.0) - 2.0).abs() < 1e-12);
584 }
585
586 #[test]
587 fn test_op_subtraction() {
588 let op = SdfOp::Subtraction;
590 assert!((op.apply(2.0, 3.0) - 2.0).abs() < 1e-12);
591 }
592
593 #[test]
594 fn test_op_smooth_union() {
595 let op = SdfOp::SmoothUnion { k: 0.3 };
596 let result = op.apply(1.0, 1.0);
597 assert!((result - (1.0 - 0.3 / 4.0)).abs() < 1e-10);
599 }
600
601 #[test]
602 fn test_op_offset_positive() {
603 let op = SdfOp::Offset { d: 0.5 };
604 assert!((op.apply(1.0, 0.0) - 1.5).abs() < 1e-12);
605 }
606
607 #[test]
608 fn test_op_offset_negative() {
609 let op = SdfOp::Offset { d: -0.5 };
610 assert!((op.apply(1.0, 0.0) - 0.5).abs() < 1e-12);
611 }
612
613 #[test]
616 fn test_node_leaf_evaluate() {
617 let node = SdfNode::leaf(SdfPrimitive::Sphere {
618 center: [0.0; 3],
619 radius: 1.0,
620 });
621 assert!((node.evaluate([1.0, 0.0, 0.0])).abs() < 1e-10);
622 }
623
624 #[test]
625 fn test_node_op_union() {
626 let a = SdfNode::leaf(SdfPrimitive::Sphere {
627 center: [-2.0, 0.0, 0.0],
628 radius: 1.0,
629 });
630 let b = SdfNode::leaf(SdfPrimitive::Sphere {
631 center: [2.0, 0.0, 0.0],
632 radius: 1.0,
633 });
634 let tree = SdfNode::op(SdfOp::Union, a, b);
635 let d = tree.evaluate([0.0; 3]);
637 assert!((d - 1.0).abs() < 1e-10, "d={d}");
638 }
639
640 #[test]
641 fn test_node_op_intersection() {
642 let a = SdfNode::leaf(SdfPrimitive::Sphere {
643 center: [0.0; 3],
644 radius: 2.0,
645 });
646 let b = SdfNode::leaf(SdfPrimitive::Sphere {
647 center: [0.0; 3],
648 radius: 3.0,
649 });
650 let tree = SdfNode::op(SdfOp::Intersection, a, b);
651 let d = tree.evaluate([0.0; 3]);
653 assert!((d - (-2.0)).abs() < 1e-10);
654 }
655
656 #[test]
657 fn test_node_op_subtraction() {
658 let a = SdfNode::leaf(SdfPrimitive::Sphere {
659 center: [0.0; 3],
660 radius: 2.0,
661 });
662 let b = SdfNode::leaf(SdfPrimitive::Sphere {
663 center: [0.0; 3],
664 radius: 1.0,
665 });
666 let tree = SdfNode::op(SdfOp::Subtraction, a, b);
667 let d = tree.evaluate([0.0; 3]);
669 assert!((d - 1.0).abs() < 1e-10, "d={d}");
670 }
671
672 #[test]
675 fn test_grid_new_size() {
676 let grid = SdfGrid::new([4, 4, 4], 0.1, [0.0; 3]);
677 assert_eq!(grid.len(), 64);
678 assert!(!grid.is_empty());
679 }
680
681 #[test]
682 fn test_grid_empty_dimensions() {
683 let grid = SdfGrid::new([0, 4, 4], 0.1, [0.0; 3]);
684 assert!(grid.is_empty());
685 }
686
687 #[test]
688 fn test_grid_compute_from_sdf_sphere() {
689 let mut grid = SdfGrid::new([4, 4, 4], 1.0, [-2.0, -2.0, -2.0]);
690 let sphere_sdf = |p: [f64; 3]| sdf_sphere(p, [0.0; 3], 1.0);
691 grid.compute_from_sdf(&sphere_sdf);
692 assert!(
694 grid.data.iter().all(|&v| v.is_finite()),
695 "some voxels unfilled"
696 );
697 }
698
699 #[test]
700 fn test_grid_index_roundtrip() {
701 let grid = SdfGrid::new([5, 6, 7], 0.5, [0.0; 3]);
702 for k in 0..7 {
703 for j in 0..6 {
704 for i in 0..5 {
705 let idx = grid.index(i, j, k);
706 let expected = i + 5 * (j + 6 * k);
708 assert_eq!(idx, expected);
709 }
710 }
711 }
712 }
713
714 #[test]
715 fn test_grid_voxel_center() {
716 let grid = SdfGrid::new([2, 2, 2], 1.0, [0.0; 3]);
717 let c = grid.voxel_center(0, 0, 0);
718 assert!((c[0] - 0.5).abs() < 1e-10);
719 assert!((c[1] - 0.5).abs() < 1e-10);
720 assert!((c[2] - 0.5).abs() < 1e-10);
721 }
722
723 #[test]
724 fn test_grid_center_of_last_voxel() {
725 let grid = SdfGrid::new([4, 4, 4], 1.0, [0.0; 3]);
726 let c = grid.voxel_center(3, 3, 3);
727 assert!((c[0] - 3.5).abs() < 1e-10);
728 }
729
730 #[test]
731 fn test_grid_inside_outside_counts() {
732 let mut grid = SdfGrid::new([10, 10, 10], 0.2, [-1.0, -1.0, -1.0]);
733 let sphere_sdf = |p: [f64; 3]| sdf_sphere(p, [0.0; 3], 0.5);
734 grid.compute_from_sdf(&sphere_sdf);
735 let inside = grid.data.iter().filter(|&&v| v < 0.0).count();
736 assert!(inside > 0, "no voxels inside the sphere");
737 let outside = grid.data.iter().filter(|&&v| v > 0.0).count();
738 assert!(outside > 0, "no voxels outside the sphere");
739 }
740
741 #[test]
744 fn test_gpu_sdf_compute_no_primitives() {
745 let compute = GpuSdfCompute::new();
746 let mut grid = SdfGrid::new([4, 4, 4], 1.0, [0.0; 3]);
747 grid.data.iter_mut().for_each(|v| *v = -999.0);
749 compute.dispatch_compute(&mut grid, &[]);
750 assert!(grid.data.iter().all(|&v| (v - (-999.0)).abs() < 1e-3));
752 }
753
754 #[test]
755 fn test_gpu_sdf_compute_sphere_union() {
756 let compute = GpuSdfCompute::new();
757 let mut grid = SdfGrid::new([8, 8, 8], 0.25, [-1.0, -1.0, -1.0]);
758 let primitives = vec![
759 SdfPrimitive::Sphere {
760 center: [-0.5, 0.0, 0.0],
761 radius: 0.3,
762 },
763 SdfPrimitive::Sphere {
764 center: [0.5, 0.0, 0.0],
765 radius: 0.3,
766 },
767 ];
768 compute.dispatch_compute(&mut grid, &primitives);
769 assert!(grid.data.iter().all(|&v| v.is_finite()));
771 let inside = grid.data.iter().filter(|&&v| v < 0.0).count();
773 assert!(inside > 0, "no voxels inside");
774 }
775
776 #[test]
777 fn test_gpu_sdf_compute_single_box() {
778 let compute = GpuSdfCompute::new();
779 let mut grid = SdfGrid::new([4, 4, 4], 1.0, [-2.0, -2.0, -2.0]);
780 let primitives = vec![SdfPrimitive::Box3d {
781 center: [0.0; 3],
782 half_extents: [0.5; 3],
783 }];
784 compute.dispatch_compute(&mut grid, &primitives);
785 assert!(grid.data.iter().all(|&v| v.is_finite()));
786 }
787
788 #[test]
789 fn test_gpu_sdf_compute_torus() {
790 let compute = GpuSdfCompute::new();
791 let mut grid = SdfGrid::new([6, 6, 6], 0.5, [-1.5, -1.5, -1.5]);
792 let primitives = vec![SdfPrimitive::Torus {
793 center: [0.0; 3],
794 r_major: 0.8,
795 r_minor: 0.2,
796 }];
797 compute.dispatch_compute(&mut grid, &primitives);
798 assert!(grid.data.iter().all(|&v| v.is_finite()));
799 }
800
801 #[test]
804 fn test_cylinder_inside() {
805 let d = sdf_cylinder([0.0, 0.0, 0.0], [0.0; 3], 1.0, 2.0);
806 assert!(d < 0.0, "expected inside, d={d}");
807 }
808
809 #[test]
810 fn test_cylinder_outside_radially() {
811 let d = sdf_cylinder([2.0, 0.0, 0.0], [0.0; 3], 1.0, 4.0);
812 assert!((d - 1.0).abs() < 1e-10, "d={d}");
813 }
814
815 #[test]
816 fn test_cylinder_on_curved_surface() {
817 let d = sdf_cylinder([1.0, 0.0, 0.0], [0.0; 3], 1.0, 4.0);
818 assert!(d.abs() < 1e-10, "d={d}");
819 }
820
821 #[test]
824 fn test_torus_centre_is_positive() {
825 let d = sdf_torus([0.0; 3], [0.0; 3], 2.0, 0.5);
827 assert!(d > 0.0, "d={d}");
828 }
829
830 #[test]
831 fn test_torus_on_tube_surface() {
832 let d = sdf_torus([1.5, 0.0, 0.0], [0.0; 3], 2.0, 0.5);
834 assert!(d.abs() < 1e-10, "d={d}");
835 }
836}