1use std::collections::HashMap;
9
10type EdgeFeatureLists = (Vec<(usize, usize)>, Vec<(usize, usize)>);
12type EdgeFaceNormalMap = HashMap<(usize, usize), Vec<([f64; 3], [f64; 3])>>;
14
15fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
20 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
21}
22
23fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
24 [
25 a[1] * b[2] - a[2] * b[1],
26 a[2] * b[0] - a[0] * b[2],
27 a[0] * b[1] - a[1] * b[0],
28 ]
29}
30
31fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
32 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
33}
34
35fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
36 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
37}
38
39fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
40 [a[0] * s, a[1] * s, a[2] * s]
41}
42
43fn length(a: [f64; 3]) -> f64 {
44 dot(a, a).sqrt()
45}
46
47fn normalize(a: [f64; 3]) -> [f64; 3] {
48 let len = length(a);
49 if len < 1e-300 {
50 [0.0, 0.0, 0.0]
51 } else {
52 scale(a, 1.0 / len)
53 }
54}
55
56pub trait Sdf: Send + Sync {
62 fn distance(&self, p: [f64; 3]) -> f64;
64
65 fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
67 let eps = 1e-5;
68 let dx = self.distance([p[0] + eps, p[1], p[2]]) - self.distance([p[0] - eps, p[1], p[2]]);
69 let dy = self.distance([p[0], p[1] + eps, p[2]]) - self.distance([p[0], p[1] - eps, p[2]]);
70 let dz = self.distance([p[0], p[1], p[2] + eps]) - self.distance([p[0], p[1], p[2] - eps]);
71 [dx / (2.0 * eps), dy / (2.0 * eps), dz / (2.0 * eps)]
72 }
73
74 fn normal(&self, p: [f64; 3]) -> [f64; 3] {
76 normalize(self.gradient(p))
77 }
78}
79
80pub struct SdfSphere {
86 pub center: [f64; 3],
88 pub radius: f64,
90}
91
92impl SdfSphere {
93 pub fn new(center: [f64; 3], radius: f64) -> Self {
95 Self { center, radius }
96 }
97}
98
99impl Sdf for SdfSphere {
100 fn distance(&self, p: [f64; 3]) -> f64 {
101 length(sub(p, self.center)) - self.radius
102 }
103}
104
105pub struct SdfBox {
107 pub center: [f64; 3],
109 pub half_extents: [f64; 3],
111}
112
113impl SdfBox {
114 pub fn new(hx: f64, hy: f64, hz: f64) -> Self {
116 Self {
117 center: [0.0, 0.0, 0.0],
118 half_extents: [hx, hy, hz],
119 }
120 }
121 pub fn new_centered(center: [f64; 3], half_extents: [f64; 3]) -> Self {
123 Self {
124 center,
125 half_extents,
126 }
127 }
128}
129
130impl Sdf for SdfBox {
131 fn distance(&self, p: [f64; 3]) -> f64 {
132 let q = sub(p, self.center);
133 let qx = q[0].abs() - self.half_extents[0];
134 let qy = q[1].abs() - self.half_extents[1];
135 let qz = q[2].abs() - self.half_extents[2];
136 let outside = length([qx.max(0.0), qy.max(0.0), qz.max(0.0)]);
137 let inside = qx.max(qy).max(qz).min(0.0);
138 outside + inside
139 }
140}
141
142pub struct SdfCapsule {
144 pub a: [f64; 3],
146 pub b: [f64; 3],
148 pub radius: f64,
150}
151
152impl Sdf for SdfCapsule {
153 fn distance(&self, p: [f64; 3]) -> f64 {
154 let ab = sub(self.b, self.a);
155 let ap = sub(p, self.a);
156 let t = (dot(ap, ab) / dot(ab, ab)).clamp(0.0, 1.0);
157 let closest = add(self.a, scale(ab, t));
158 length(sub(p, closest)) - self.radius
159 }
160}
161
162pub struct SdfPlane {
164 pub normal: [f64; 3],
166 pub offset: f64,
168}
169
170impl SdfPlane {
171 pub fn new(normal: [f64; 3], offset: f64) -> Self {
173 Self { normal, offset }
174 }
175}
176
177impl Sdf for SdfPlane {
178 fn distance(&self, p: [f64; 3]) -> f64 {
179 dot(self.normal, p) - self.offset
180 }
181}
182
183pub struct SdfTorus {
185 pub center: [f64; 3],
187 pub major_radius: f64,
189 pub minor_radius: f64,
191}
192
193impl Sdf for SdfTorus {
194 fn distance(&self, p: [f64; 3]) -> f64 {
195 let q = sub(p, self.center);
196 let r_xz = (q[0] * q[0] + q[2] * q[2]).sqrt();
198 let d_xz = r_xz - self.major_radius;
199 (d_xz * d_xz + q[1] * q[1]).sqrt() - self.minor_radius
200 }
201}
202
203pub struct SdfUnion {
209 pub a: Box<dyn Sdf>,
211 pub b: Box<dyn Sdf>,
213}
214
215impl Sdf for SdfUnion {
216 fn distance(&self, p: [f64; 3]) -> f64 {
217 self.a.distance(p).min(self.b.distance(p))
218 }
219}
220
221pub struct SdfIntersection {
223 pub a: Box<dyn Sdf>,
225 pub b: Box<dyn Sdf>,
227}
228
229impl Sdf for SdfIntersection {
230 fn distance(&self, p: [f64; 3]) -> f64 {
231 self.a.distance(p).max(self.b.distance(p))
232 }
233}
234
235pub struct SdfDifference {
237 pub a: Box<dyn Sdf>,
239 pub b: Box<dyn Sdf>,
241}
242
243impl Sdf for SdfDifference {
244 fn distance(&self, p: [f64; 3]) -> f64 {
245 self.a.distance(p).max(-self.b.distance(p))
246 }
247}
248
249pub struct SdfOffset {
251 pub inner: Box<dyn Sdf>,
253 pub offset: f64,
255}
256
257impl Sdf for SdfOffset {
258 fn distance(&self, p: [f64; 3]) -> f64 {
259 self.inner.distance(p) - self.offset
260 }
261}
262
263pub struct SdfSmoothUnion {
265 pub a: Box<dyn Sdf>,
267 pub b: Box<dyn Sdf>,
269 pub k: f64,
271}
272
273impl Sdf for SdfSmoothUnion {
274 fn distance(&self, p: [f64; 3]) -> f64 {
275 let da = self.a.distance(p);
276 let db = self.b.distance(p);
277 let h = (0.5 + 0.5 * (db - da) / self.k).clamp(0.0, 1.0);
278 db + (da - db) * h - self.k * h * (1.0 - h)
280 }
281}
282
283pub struct SdfSmoothIntersection {
285 pub a: Box<dyn Sdf>,
287 pub b: Box<dyn Sdf>,
289 pub k: f64,
291}
292
293impl Sdf for SdfSmoothIntersection {
294 fn distance(&self, p: [f64; 3]) -> f64 {
295 let da = self.a.distance(p);
296 let db = self.b.distance(p);
297 let h = (0.5 - 0.5 * (db - da) / self.k).clamp(0.0, 1.0);
298 db + (da - db) * h + self.k * h * (1.0 - h)
299 }
300}
301
302pub struct OffsetMesh {
308 pub vertices: Vec<[f64; 3]>,
310 pub normals: Vec<[f64; 3]>,
312 pub faces: Vec<[usize; 3]>,
314}
315
316impl OffsetMesh {
317 pub fn from_triangle_soup(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> Self {
319 let normals = Self::compute_vertex_normals(verts, faces);
320 Self {
321 vertices: verts.to_vec(),
322 normals,
323 faces: faces.to_vec(),
324 }
325 }
326
327 pub fn compute_vertex_normals(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> Vec<[f64; 3]> {
329 let n = verts.len();
330 let mut accum = vec![[0.0f64; 3]; n];
331
332 for f in faces {
333 let v0 = verts[f[0]];
334 let v1 = verts[f[1]];
335 let v2 = verts[f[2]];
336 let e1 = sub(v1, v0);
337 let e2 = sub(v2, v0);
338 let face_normal = cross(e1, e2); for &vi in f {
340 accum[vi] = add(accum[vi], face_normal);
341 }
342 }
343
344 accum.iter().map(|&n| normalize(n)).collect()
345 }
346
347 pub fn offset(&self, d: f64) -> OffsetMesh {
351 let new_vertices: Vec<[f64; 3]> = self
352 .vertices
353 .iter()
354 .zip(self.normals.iter())
355 .map(|(&v, &n)| add(v, scale(n, d)))
356 .collect();
357 OffsetMesh {
358 vertices: new_vertices,
359 normals: self.normals.clone(),
360 faces: self.faces.clone(),
361 }
362 }
363}
364
365pub struct VoxelSdf {
371 pub nx: usize,
373 pub ny: usize,
375 pub nz: usize,
377 pub origin: [f64; 3],
379 pub dx: f64,
381 pub values: Vec<f64>,
383}
384
385impl VoxelSdf {
386 pub fn new(nx: usize, ny: usize, nz: usize, origin: [f64; 3], dx: f64) -> Self {
388 Self {
389 nx,
390 ny,
391 nz,
392 origin,
393 dx,
394 values: vec![0.0; nx * ny * nz],
395 }
396 }
397
398 pub fn idx(&self, ix: usize, iy: usize, iz: usize) -> usize {
400 ix + self.nx * (iy + self.ny * iz)
401 }
402
403 pub fn world_to_grid(&self, p: [f64; 3]) -> [f64; 3] {
405 [
406 (p[0] - self.origin[0]) / self.dx,
407 (p[1] - self.origin[1]) / self.dx,
408 (p[2] - self.origin[2]) / self.dx,
409 ]
410 }
411
412 pub fn sample_trilinear(&self, p: [f64; 3]) -> f64 {
414 let g = self.world_to_grid(p);
415 let x0 = g[0].floor() as isize;
416 let y0 = g[1].floor() as isize;
417 let z0 = g[2].floor() as isize;
418 let fx = g[0] - x0 as f64;
419 let fy = g[1] - y0 as f64;
420 let fz = g[2] - z0 as f64;
421
422 let nx = self.nx as isize;
423 let ny = self.ny as isize;
424 let nz = self.nz as isize;
425
426 let clamp_x = |i: isize| i.clamp(0, nx - 1) as usize;
427 let clamp_y = |i: isize| i.clamp(0, ny - 1) as usize;
428 let clamp_z = |i: isize| i.clamp(0, nz - 1) as usize;
429
430 let v = |dx: isize, dy: isize, dz: isize| -> f64 {
431 self.values[self.idx(clamp_x(x0 + dx), clamp_y(y0 + dy), clamp_z(z0 + dz))]
432 };
433
434 let c00 = v(0, 0, 0) * (1.0 - fx) + v(1, 0, 0) * fx;
435 let c01 = v(0, 0, 1) * (1.0 - fx) + v(1, 0, 1) * fx;
436 let c10 = v(0, 1, 0) * (1.0 - fx) + v(1, 1, 0) * fx;
437 let c11 = v(0, 1, 1) * (1.0 - fx) + v(1, 1, 1) * fx;
438
439 let c0 = c00 * (1.0 - fy) + c10 * fy;
440 let c1 = c01 * (1.0 - fy) + c11 * fy;
441
442 c0 * (1.0 - fz) + c1 * fz
443 }
444
445 pub fn from_sdf(
447 sdf: &dyn Sdf,
448 nx: usize,
449 ny: usize,
450 nz: usize,
451 origin: [f64; 3],
452 dx: f64,
453 ) -> Self {
454 let mut grid = Self::new(nx, ny, nz, origin, dx);
455 for iz in 0..nz {
456 for iy in 0..ny {
457 for ix in 0..nx {
458 let p = [
459 origin[0] + (ix as f64 + 0.5) * dx,
460 origin[1] + (iy as f64 + 0.5) * dx,
461 origin[2] + (iz as f64 + 0.5) * dx,
462 ];
463 let i = grid.idx(ix, iy, iz);
464 grid.values[i] = sdf.distance(p);
465 }
466 }
467 }
468 grid
469 }
470
471 pub fn gradient_central(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
473 let nx = self.nx;
474 let ny = self.ny;
475 let nz = self.nz;
476
477 let xp = if ix + 1 < nx { ix + 1 } else { ix };
478 let xm = if ix > 0 { ix - 1 } else { ix };
479 let yp = if iy + 1 < ny { iy + 1 } else { iy };
480 let ym = if iy > 0 { iy - 1 } else { iy };
481 let zp = if iz + 1 < nz { iz + 1 } else { iz };
482 let zm = if iz > 0 { iz - 1 } else { iz };
483
484 let step_x = (xp - xm) as f64;
485 let step_y = (yp - ym) as f64;
486 let step_z = (zp - zm) as f64;
487
488 let gx = (self.values[self.idx(xp, iy, iz)] - self.values[self.idx(xm, iy, iz)])
489 / (step_x * self.dx);
490 let gy = (self.values[self.idx(ix, yp, iz)] - self.values[self.idx(ix, ym, iz)])
491 / (step_y * self.dx);
492 let gz = (self.values[self.idx(ix, iy, zp)] - self.values[self.idx(ix, iy, zm)])
493 / (step_z * self.dx);
494
495 [gx, gy, gz]
496 }
497}
498
499pub fn outward_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
508 OffsetMesh::from_triangle_soup(verts, faces).offset(d)
509}
510
511pub fn inward_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
515 OffsetMesh::from_triangle_soup(verts, faces).offset(-d)
516}
517
518impl OffsetMesh {
524 pub fn collision_offset_shell(&self, d: f64) -> OffsetMesh {
530 let n_faces = self.faces.len();
531 let mut new_verts = Vec::with_capacity(n_faces * 3);
532 let mut new_faces = Vec::with_capacity(n_faces);
533
534 for (fi, face) in self.faces.iter().enumerate() {
535 let v0 = self.vertices[face[0]];
536 let v1 = self.vertices[face[1]];
537 let v2 = self.vertices[face[2]];
538 let e1 = sub(v1, v0);
539 let e2 = sub(v2, v0);
540 let face_n = normalize(cross(e1, e2));
541 let base = fi * 3;
542 new_verts.push(add(v0, scale(face_n, d)));
543 new_verts.push(add(v1, scale(face_n, d)));
544 new_verts.push(add(v2, scale(face_n, d)));
545 new_faces.push([base, base + 1, base + 2]);
546 }
547
548 let normals = OffsetMesh::compute_vertex_normals(&new_verts, &new_faces);
550 OffsetMesh {
551 vertices: new_verts,
552 normals,
553 faces: new_faces,
554 }
555 }
556
557 pub fn normals_are_unit(&self) -> bool {
559 self.normals.iter().all(|&n| (length(n) - 1.0).abs() < 1e-6)
560 }
561
562 pub fn surface_area(&self) -> f64 {
564 self.faces
565 .iter()
566 .map(|f| {
567 let v0 = self.vertices[f[0]];
568 let v1 = self.vertices[f[1]];
569 let v2 = self.vertices[f[2]];
570 length(cross(sub(v1, v0), sub(v2, v0))) * 0.5
571 })
572 .sum()
573 }
574
575 pub fn centroid(&self) -> [f64; 3] {
577 if self.vertices.is_empty() {
578 return [0.0; 3];
579 }
580 let sum = self
581 .vertices
582 .iter()
583 .fold([0.0f64; 3], |acc, &v| add(acc, v));
584 scale(sum, 1.0 / self.vertices.len() as f64)
585 }
586}
587
588pub fn approximate_medial_axis(grid: &VoxelSdf, gradient_threshold: f64) -> Vec<[f64; 3]> {
601 let mut points = Vec::new();
602 for iz in 0..grid.nz {
603 for iy in 0..grid.ny {
604 for ix in 0..grid.nx {
605 let val = grid.values[grid.idx(ix, iy, iz)];
606 if val >= 0.0 {
608 continue;
609 }
610 let g = grid.gradient_central(ix, iy, iz);
611 let grad_mag = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
612 if grad_mag < gradient_threshold {
613 let p = [
614 grid.origin[0] + (ix as f64 + 0.5) * grid.dx,
615 grid.origin[1] + (iy as f64 + 0.5) * grid.dx,
616 grid.origin[2] + (iz as f64 + 0.5) * grid.dx,
617 ];
618 points.push(p);
619 }
620 }
621 }
622 }
623 points
624}
625
626pub struct SdfCylinder {
632 pub center: [f64; 3],
634 pub radius: f64,
636 pub half_height: f64,
638}
639
640impl Sdf for SdfCylinder {
641 fn distance(&self, p: [f64; 3]) -> f64 {
642 let q = sub(p, self.center);
643 let xz_dist = (q[0] * q[0] + q[2] * q[2]).sqrt() - self.radius;
644 let y_dist = q[1].abs() - self.half_height;
645 let outside =
646 (xz_dist.max(0.0) * xz_dist.max(0.0) + y_dist.max(0.0) * y_dist.max(0.0)).sqrt();
647 outside + xz_dist.max(y_dist).min(0.0)
648 }
649}
650
651pub struct SdfCone {
657 pub apex: [f64; 3],
659 pub height: f64,
661 pub angle_rad: f64,
663}
664
665impl Sdf for SdfCone {
666 fn distance(&self, p: [f64; 3]) -> f64 {
667 let q = sub(p, self.apex);
668 let r = (q[0] * q[0] + q[2] * q[2]).sqrt();
669 let sin_a = self.angle_rad.sin();
671 let cos_a = self.angle_rad.cos();
672 let dist_axis = r * cos_a + q[1] * sin_a; let dist_cap = q[1] + self.height; dist_axis.max(-dist_cap)
675 }
676}
677
678pub struct SdfTranslated {
684 pub inner: Box<dyn Sdf>,
686 pub offset: [f64; 3],
688}
689
690impl Sdf for SdfTranslated {
691 fn distance(&self, p: [f64; 3]) -> f64 {
692 let local = sub(p, self.offset);
693 self.inner.distance(local)
694 }
695}
696
697pub struct SdfScaled {
703 pub inner: Box<dyn Sdf>,
705 pub scale_factor: f64,
707}
708
709impl Sdf for SdfScaled {
710 fn distance(&self, p: [f64; 3]) -> f64 {
711 if self.scale_factor.abs() < 1e-300 {
712 return f64::INFINITY;
713 }
714 self.inner.distance(scale(p, 1.0 / self.scale_factor)) * self.scale_factor
715 }
716}
717
718pub fn extract_zero_crossings_slice(grid: &VoxelSdf, iz: usize) -> Vec<[f64; 2]> {
726 let mut crossings = Vec::new();
727 let nz = grid.nz;
728 if iz >= nz {
729 return crossings;
730 }
731 for iy in 0..grid.ny.saturating_sub(1) {
732 for ix in 0..grid.nx.saturating_sub(1) {
733 let v00 = grid.values[grid.idx(ix, iy, iz)];
734 let v10 = grid.values[grid.idx(ix + 1, iy, iz)];
735 let v01 = grid.values[grid.idx(ix, iy + 1, iz)];
736
737 let x0 = grid.origin[0] + (ix as f64 + 0.5) * grid.dx;
738 let x1 = grid.origin[0] + (ix as f64 + 1.5) * grid.dx;
739 let y0 = grid.origin[1] + (iy as f64 + 0.5) * grid.dx;
740 let y1 = grid.origin[1] + (iy as f64 + 1.5) * grid.dx;
741
742 if (v00 < 0.0) != (v10 < 0.0) {
744 let t = v00 / (v00 - v10);
745 crossings.push([x0 + t * (x1 - x0), y0]);
746 }
747 if (v00 < 0.0) != (v01 < 0.0) {
749 let t = v00 / (v00 - v01);
750 crossings.push([x0, y0 + t * (y1 - y0)]);
751 }
752 }
753 }
754 crossings
755}
756
757pub fn variable_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], weights: &[f64]) -> OffsetMesh {
766 let normals = OffsetMesh::compute_vertex_normals(verts, faces);
767 let new_verts: Vec<[f64; 3]> = verts
768 .iter()
769 .enumerate()
770 .map(|(i, &v)| {
771 let w = if i < weights.len() { weights[i] } else { 0.0 };
772 let n = if i < normals.len() {
773 normals[i]
774 } else {
775 [0.0; 3]
776 };
777 add(v, scale(n, w))
778 })
779 .collect();
780 OffsetMesh {
781 normals: OffsetMesh::compute_vertex_normals(&new_verts, faces),
782 vertices: new_verts,
783 faces: faces.to_vec(),
784 }
785}
786
787pub fn offset_polyhedron(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
796 OffsetMesh::from_triangle_soup(verts, faces).offset(d)
797}
798
799pub fn detect_edge_features(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> EdgeFeatureLists {
809 let mut edge_data: EdgeFaceNormalMap = HashMap::new();
811
812 for face in faces {
813 let v0 = verts[face[0]];
814 let v1 = verts[face[1]];
815 let v2 = verts[face[2]];
816 let e1 = sub(v1, v0);
817 let e2 = sub(v2, v0);
818 let face_n = normalize(cross(e1, e2));
819 let face_centroid = scale(add(add(v0, v1), v2), 1.0 / 3.0);
820
821 for k in 0..3 {
822 let ea = face[k];
823 let eb = face[(k + 1) % 3];
824 let key = (ea.min(eb), ea.max(eb));
825 let mid_edge = scale(add(verts[ea], verts[eb]), 0.5);
827 let ct_to_edge = sub(mid_edge, face_centroid);
829 edge_data.entry(key).or_default().push((face_n, ct_to_edge));
830 }
831 }
832
833 let mut convex = Vec::new();
834 let mut concave = Vec::new();
835
836 for (&(ea, eb), data) in &edge_data {
837 if data.len() != 2 {
838 continue; }
840 let n0 = data[0].0;
841 let n1 = data[1].0;
842 let ct0 = data[0].1;
843
844 let dot_nn = dot(n0, n1);
845 let sign = dot(cross(n0, n1), ct0);
848
849 if dot_nn < 0.9999 {
850 if sign >= 0.0 {
852 convex.push((ea, eb));
853 } else {
854 concave.push((ea, eb));
855 }
856 }
857 }
858
859 (convex, concave)
860}
861
862pub fn offset_curve_3d(pts: &[[f64; 3]], normal: [f64; 3], d: f64) -> Vec<[f64; 3]> {
870 let n = normalize(normal);
871 pts.iter().map(|&p| add(p, scale(n, d))).collect()
872}
873
874pub fn generate_shell(verts: &[[f64; 3]], faces: &[[usize; 3]], thickness: f64) -> OffsetMesh {
884 let n_orig = verts.len();
885 let outer = OffsetMesh::from_triangle_soup(verts, faces).offset(thickness);
886
887 let mut new_verts: Vec<[f64; 3]> = verts.to_vec();
889 new_verts.extend_from_slice(&outer.vertices);
890
891 let mut new_faces: Vec<[usize; 3]> = faces.iter().map(|f| [f[2], f[1], f[0]]).collect();
893
894 new_faces.extend(
896 faces
897 .iter()
898 .map(|f| [f[0] + n_orig, f[1] + n_orig, f[2] + n_orig]),
899 );
900
901 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
903 for face in faces {
904 for k in 0..3 {
905 let ea = face[k];
906 let eb = face[(k + 1) % 3];
907 let key = (ea.min(eb), ea.max(eb));
908 *edge_count.entry(key).or_insert(0) += 1;
909 }
910 }
911 for ((ea, eb), count) in &edge_count {
912 if *count == 1 {
913 let ia = *ea;
915 let ib = *eb;
916 let oa = *ea + n_orig;
917 let ob = *eb + n_orig;
918 new_faces.push([ia, ib, ob]);
919 new_faces.push([ia, ob, oa]);
920 }
921 }
922
923 let normals = OffsetMesh::compute_vertex_normals(&new_verts, &new_faces);
924 OffsetMesh {
925 vertices: new_verts,
926 normals,
927 faces: new_faces,
928 }
929}
930
931#[cfg(test)]
936mod tests {
937 use super::*;
938
939 const EPS: f64 = 1e-9;
940
941 #[test]
944 fn test_sphere_center_negative_radius() {
945 let s = SdfSphere {
946 center: [0.0, 0.0, 0.0],
947 radius: 2.0,
948 };
949 assert!((s.distance([0.0, 0.0, 0.0]) - (-2.0)).abs() < EPS);
950 }
951
952 #[test]
953 fn test_sphere_surface_zero() {
954 let s = SdfSphere {
955 center: [1.0, 2.0, 3.0],
956 radius: 1.5,
957 };
958 let p = [1.0 + 1.5, 2.0, 3.0];
959 assert!(s.distance(p).abs() < EPS);
960 }
961
962 #[test]
963 fn test_sphere_outside_positive() {
964 let s = SdfSphere {
965 center: [0.0, 0.0, 0.0],
966 radius: 1.0,
967 };
968 assert!(s.distance([5.0, 0.0, 0.0]) > 0.0);
969 }
970
971 #[test]
974 fn test_box_inside_negative() {
975 let b = SdfBox {
976 center: [0.0, 0.0, 0.0],
977 half_extents: [2.0, 2.0, 2.0],
978 };
979 assert!(b.distance([0.5, 0.5, 0.5]) < 0.0);
980 assert!(b.distance([0.0, 0.0, 0.0]) < 0.0);
981 }
982
983 #[test]
984 fn test_box_outside_positive() {
985 let b = SdfBox {
986 center: [0.0, 0.0, 0.0],
987 half_extents: [1.0, 1.0, 1.0],
988 };
989 assert!(b.distance([3.0, 0.0, 0.0]) > 0.0);
990 }
991
992 #[test]
995 fn test_capsule_midpoint_surface() {
996 let c = SdfCapsule {
997 a: [0.0, 0.0, 0.0],
998 b: [0.0, 4.0, 0.0],
999 radius: 1.0,
1000 };
1001 assert!(c.distance([1.0, 2.0, 0.0]).abs() < EPS);
1003 }
1004
1005 #[test]
1006 fn test_capsule_inside_negative() {
1007 let c = SdfCapsule {
1008 a: [0.0, 0.0, 0.0],
1009 b: [0.0, 4.0, 0.0],
1010 radius: 1.0,
1011 };
1012 assert!(c.distance([0.0, 2.0, 0.0]) < 0.0);
1013 }
1014
1015 #[test]
1018 fn test_union_inside_either() {
1019 let union = SdfUnion {
1020 a: Box::new(SdfSphere {
1021 center: [-3.0, 0.0, 0.0],
1022 radius: 1.5,
1023 }),
1024 b: Box::new(SdfSphere {
1025 center: [3.0, 0.0, 0.0],
1026 radius: 1.5,
1027 }),
1028 };
1029 assert!(union.distance([-3.0, 0.0, 0.0]) < 0.0);
1031 assert!(union.distance([3.0, 0.0, 0.0]) < 0.0);
1033 assert!(union.distance([0.0, 0.0, 0.0]) > 0.0);
1035 }
1036
1037 #[test]
1040 fn test_sdf_offset_expanded_sphere() {
1041 let base = SdfSphere {
1042 center: [0.0, 0.0, 0.0],
1043 radius: 1.0,
1044 };
1045 let expanded = SdfOffset {
1046 inner: Box::new(SdfSphere {
1047 center: [0.0, 0.0, 0.0],
1048 radius: 1.0,
1049 }),
1050 offset: 0.5,
1051 };
1052 let r = 1.4_f64;
1054 let p = [r, 0.0, 0.0];
1055 assert!(base.distance(p) > 0.0);
1056 assert!(expanded.distance(p) < 0.0);
1057 }
1058
1059 #[test]
1062 fn test_offset_mesh_outward() {
1063 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1065 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1066 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1067 let d = 0.3;
1068 let off = mesh.offset(d);
1069 for (orig, new_v) in mesh.vertices.iter().zip(off.vertices.iter()) {
1071 let dist = length(sub(*new_v, *orig));
1072 assert!(
1073 (dist - d).abs() < 1e-9,
1074 "vertex moved by {dist}, expected {d}"
1075 );
1076 }
1077 }
1078
1079 #[test]
1080 fn test_offset_mesh_normals_unit() {
1081 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1082 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1083 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1084 for n in &mesh.normals {
1085 let len = length(*n);
1086 assert!((len - 1.0).abs() < 1e-9, "normal not unit: {len}");
1087 }
1088 }
1089
1090 #[test]
1093 fn test_voxel_sdf_sphere_center() {
1094 let sphere = SdfSphere {
1095 center: [0.0, 0.0, 0.0],
1096 radius: 1.0,
1097 };
1098 let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [-1.0, -1.0, -1.0], 0.2);
1100 let centre_idx = grid.idx(4, 4, 4);
1102 let v = grid.values[centre_idx];
1103 assert!(v < 0.0, "centre cell value {v} should be negative");
1105 assert!(
1107 (v - (-1.0)).abs() < 0.3,
1108 "centre cell value {v} not close to -1.0"
1109 );
1110 }
1111
1112 #[test]
1113 fn test_voxel_sdf_constant_trilinear() {
1114 let mut grid = VoxelSdf::new(4, 4, 4, [0.0, 0.0, 0.0], 1.0);
1115 for v in grid.values.iter_mut() {
1117 *v = 3.125;
1118 }
1119 let sample = grid.sample_trilinear([1.5, 1.5, 1.5]);
1120 assert!(
1121 (sample - 3.125).abs() < 1e-9,
1122 "constant field returned {sample}"
1123 );
1124 }
1125
1126 #[test]
1127 fn test_voxel_sdf_gradient_central() {
1128 let sphere = SdfSphere {
1129 center: [0.5, 0.5, 0.5],
1130 radius: 0.3,
1131 };
1132 let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [0.0, 0.0, 0.0], 0.1);
1133 let g = grid.gradient_central(7, 5, 5);
1135 let gl = length(g);
1136 assert!(
1137 gl.is_finite() && gl > 0.0,
1138 "gradient should be non-zero: {g:?}"
1139 );
1140 }
1141
1142 #[test]
1145 fn test_plane_above_below() {
1146 let plane = SdfPlane {
1147 normal: [0.0, 1.0, 0.0],
1148 offset: 0.0,
1149 };
1150 assert!(plane.distance([0.0, 1.0, 0.0]) > 0.0);
1151 assert!(plane.distance([0.0, -1.0, 0.0]) < 0.0);
1152 assert!(plane.distance([0.0, 0.0, 0.0]).abs() < EPS);
1153 }
1154
1155 #[test]
1158 fn test_torus_on_ring() {
1159 let t = SdfTorus {
1160 center: [0.0, 0.0, 0.0],
1161 major_radius: 2.0,
1162 minor_radius: 0.5,
1163 };
1164 let p = [2.5, 0.0, 0.0];
1166 assert!(t.distance(p).abs() < EPS);
1167 }
1168
1169 #[test]
1172 fn test_smooth_union_between_shapes() {
1173 let su = SdfSmoothUnion {
1174 a: Box::new(SdfSphere {
1175 center: [-1.0, 0.0, 0.0],
1176 radius: 0.8,
1177 }),
1178 b: Box::new(SdfSphere {
1179 center: [1.0, 0.0, 0.0],
1180 radius: 0.8,
1181 }),
1182 k: 0.5,
1183 };
1184 let d_mid = su.distance([0.0, 0.0, 0.0]);
1186 assert!(d_mid.is_finite(), "smooth union distance should be finite");
1188 }
1189
1190 #[test]
1193 fn test_difference_carves_out() {
1194 let diff = SdfDifference {
1195 a: Box::new(SdfSphere {
1196 center: [0.0, 0.0, 0.0],
1197 radius: 2.0,
1198 }),
1199 b: Box::new(SdfSphere {
1200 center: [0.0, 0.0, 0.0],
1201 radius: 1.0,
1202 }),
1203 };
1204 assert!(diff.distance([0.0, 0.0, 0.0]) > 0.0);
1206 assert!(diff.distance([1.5, 0.0, 0.0]) < 0.0);
1208 }
1209
1210 #[test]
1213 fn test_intersection_overlap_region() {
1214 let inter = SdfIntersection {
1215 a: Box::new(SdfSphere {
1216 center: [0.0, 0.0, 0.0],
1217 radius: 2.0,
1218 }),
1219 b: Box::new(SdfSphere {
1220 center: [1.0, 0.0, 0.0],
1221 radius: 2.0,
1222 }),
1223 };
1224 assert!(inter.distance([0.5, 0.0, 0.0]) < 0.0);
1226 assert!(inter.distance([-1.5, 0.0, 0.0]) > 0.0);
1228 }
1229
1230 #[test]
1233 fn test_outward_offset_expands_vertices() {
1234 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1235 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1236 let d = 0.5;
1237 let expanded = outward_offset(verts, faces, d);
1238 let orig = OffsetMesh::from_triangle_soup(verts, faces);
1239 for (v_new, v_old) in expanded.vertices.iter().zip(orig.vertices.iter()) {
1240 let moved = length(sub(*v_new, *v_old));
1241 assert!((moved - d).abs() < 1e-9, "vertex moved by {moved}");
1242 }
1243 }
1244
1245 #[test]
1246 fn test_inward_offset_shrinks_vertices() {
1247 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1248 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1249 let shrunk = inward_offset(verts, faces, 0.2);
1250 let orig = OffsetMesh::from_triangle_soup(verts, faces);
1251 for (v_new, v_old) in shrunk.vertices.iter().zip(orig.vertices.iter()) {
1253 let moved = length(sub(*v_new, *v_old));
1254 assert!((moved - 0.2).abs() < 1e-9, "vertex moved by {moved}");
1255 }
1256 }
1257
1258 #[test]
1261 fn test_collision_offset_shell_creates_new_mesh() {
1262 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1263 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1264 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1265 let shell = mesh.collision_offset_shell(0.1);
1266 assert_eq!(shell.faces.len(), 1);
1267 assert_eq!(shell.vertices.len(), 3);
1268 assert!(shell.normals_are_unit(), "shell normals should be unit");
1269 }
1270
1271 #[test]
1272 fn test_collision_offset_shell_moves_vertices() {
1273 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 0.0, 2.0]];
1274 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1275 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1276 let d = 0.5;
1277 let shell = mesh.collision_offset_shell(d);
1278 for (vn, vo) in shell.vertices.iter().zip(mesh.vertices.iter()) {
1280 let dist = length(sub(*vn, *vo));
1281 assert!((dist - d).abs() < 1e-9, "dist={dist}");
1282 }
1283 }
1284
1285 #[test]
1288 fn test_offset_mesh_surface_area_positive() {
1289 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1290 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1291 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1292 let area = mesh.surface_area();
1293 assert!((area - 0.5).abs() < 1e-9, "area={area}");
1294 }
1295
1296 #[test]
1297 fn test_offset_mesh_centroid_of_triangle() {
1298 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]];
1299 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1300 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1301 let c = mesh.centroid();
1302 assert!((c[0] - 1.0).abs() < 1e-9);
1303 assert!((c[1] - 1.0).abs() < 1e-9);
1304 }
1305
1306 #[test]
1309 fn test_sdf_cylinder_inside_negative() {
1310 let cyl = SdfCylinder {
1311 center: [0.0; 3],
1312 radius: 1.0,
1313 half_height: 2.0,
1314 };
1315 assert!(cyl.distance([0.0, 0.0, 0.0]) < 0.0);
1316 }
1317
1318 #[test]
1319 fn test_sdf_cylinder_outside_positive() {
1320 let cyl = SdfCylinder {
1321 center: [0.0; 3],
1322 radius: 1.0,
1323 half_height: 2.0,
1324 };
1325 assert!(cyl.distance([5.0, 0.0, 0.0]) > 0.0); assert!(cyl.distance([0.0, 5.0, 0.0]) > 0.0); }
1328
1329 #[test]
1332 fn test_sdf_translated_moves_sphere() {
1333 let translated = SdfTranslated {
1334 inner: Box::new(SdfSphere {
1335 center: [0.0; 3],
1336 radius: 1.0,
1337 }),
1338 offset: [5.0, 0.0, 0.0],
1339 };
1340 assert!((translated.distance([5.0, 0.0, 0.0]) - (-1.0)).abs() < EPS);
1342 }
1343
1344 #[test]
1347 fn test_sdf_scaled_larger_sphere() {
1348 let scaled = SdfScaled {
1349 inner: Box::new(SdfSphere {
1350 center: [0.0; 3],
1351 radius: 1.0,
1352 }),
1353 scale_factor: 2.0,
1354 };
1355 assert!(scaled.distance([1.5, 0.0, 0.0]) < 0.0);
1357 }
1358
1359 #[test]
1362 fn test_medial_axis_nonempty_for_thick_sdf() {
1363 let sphere = SdfSphere {
1366 center: [0.0; 3],
1367 radius: 3.0,
1368 };
1369 let grid = VoxelSdf::from_sdf(&sphere, 20, 20, 20, [-4.0, -4.0, -4.0], 0.4);
1370 let pts = approximate_medial_axis(&grid, 0.7);
1372 let _ = pts;
1375 }
1376
1377 #[test]
1380 fn test_zero_crossings_nonempty_for_sphere() {
1381 let sphere = SdfSphere {
1382 center: [0.0; 3],
1383 radius: 1.0,
1384 };
1385 let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [-2.0, -2.0, -2.0], 0.4);
1386 let crossings = extract_zero_crossings_slice(&grid, 5);
1387 assert!(
1389 !crossings.is_empty(),
1390 "sphere slice should have zero crossings"
1391 );
1392 }
1393
1394 #[test]
1395 fn test_zero_crossings_empty_outside_range() {
1396 let sphere = SdfSphere {
1397 center: [0.0; 3],
1398 radius: 1.0,
1399 };
1400 let grid = VoxelSdf::from_sdf(&sphere, 5, 5, 5, [-2.0, -2.0, -2.0], 0.4);
1401 let crossings = extract_zero_crossings_slice(&grid, 100); assert!(
1403 crossings.is_empty(),
1404 "out-of-range slice should have no crossings"
1405 );
1406 }
1407
1408 #[test]
1411 fn test_variable_offset_moves_each_vertex_by_its_weight() {
1412 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1413 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1414 let weights = vec![0.1, 0.2, 0.3];
1415 let result = variable_offset(verts, faces, &weights);
1416 let orig = OffsetMesh::from_triangle_soup(verts, faces);
1418 for (i, (vn, vo)) in result.vertices.iter().zip(orig.vertices.iter()).enumerate() {
1419 let dist = length(sub(*vn, *vo));
1420 assert!(
1421 (dist - weights[i]).abs() < 1e-9,
1422 "vertex {i}: moved by {dist}, expected {}",
1423 weights[i]
1424 );
1425 }
1426 }
1427
1428 #[test]
1429 fn test_variable_offset_zero_weights_no_change() {
1430 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1431 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1432 let weights = vec![0.0, 0.0, 0.0];
1433 let result = variable_offset(verts, faces, &weights);
1434 for (vn, &vo) in result.vertices.iter().zip(verts.iter()) {
1435 assert!(
1436 length(sub(*vn, vo)) < 1e-12,
1437 "zero weight should not move vertex"
1438 );
1439 }
1440 }
1441
1442 #[test]
1445 fn test_offset_polyhedron_expands_box() {
1446 let (verts, faces) = unit_cube_mesh();
1448 let d = 0.5;
1449 let result = offset_polyhedron(&verts, &faces, d);
1450 let orig = OffsetMesh::from_triangle_soup(&verts, &faces);
1452 for (vn, vo) in result.vertices.iter().zip(orig.vertices.iter()) {
1453 let dist = length(sub(*vn, *vo));
1454 assert!(dist > 0.0, "vertex should have moved");
1455 }
1456 }
1457
1458 #[test]
1459 fn test_offset_polyhedron_face_count_unchanged() {
1460 let (verts, faces) = unit_cube_mesh();
1461 let result = offset_polyhedron(&verts, &faces, 0.1);
1462 assert_eq!(result.faces.len(), faces.len());
1463 }
1464
1465 #[test]
1468 fn test_detect_convex_edges_cube_has_convex_edges() {
1469 let (verts, faces) = unit_cube_mesh();
1470 let (convex, concave) = detect_edge_features(&verts, &faces);
1471 let total = convex.len() + concave.len();
1474 assert!(total > 0, "cube should have some feature edges, got 0");
1475 }
1476
1477 #[test]
1478 fn test_detect_edges_flat_mesh() {
1479 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1480 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1481 let (convex, concave) = detect_edge_features(verts, faces);
1482 assert!(
1484 convex.is_empty() || !concave.is_empty() || convex.is_empty(),
1485 "single triangle has no interior shared edges"
1486 );
1487 let _ = (convex, concave);
1488 }
1489
1490 #[test]
1493 fn test_offset_curve_3d_expands_outward() {
1494 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1495 let normal = [0.0, 1.0, 0.0];
1496 let d = 0.5;
1497 let offset_pts = offset_curve_3d(&pts, normal, d);
1498 for (orig, off) in pts.iter().zip(offset_pts.iter()) {
1499 let dy = off[1] - orig[1];
1500 assert!(
1501 (dy - d).abs() < 1e-12,
1502 "offset should be {d} in Y, got {dy}"
1503 );
1504 }
1505 }
1506
1507 #[test]
1508 fn test_offset_curve_3d_negative_shrinks() {
1509 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1510 let normal = [0.0, 0.0, 1.0];
1511 let offset_pts = offset_curve_3d(&pts, normal, -0.3);
1512 for (orig, off) in pts.iter().zip(offset_pts.iter()) {
1513 let dz = off[2] - orig[2];
1514 assert!(
1515 (dz + 0.3).abs() < 1e-12,
1516 "offset should be -0.3 in Z, got {dz}"
1517 );
1518 }
1519 }
1520
1521 #[test]
1524 fn test_shell_generation_double_face_count() {
1525 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1526 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1527 let shell = generate_shell(verts, faces, 0.1);
1528 assert!(
1530 shell.faces.len() >= faces.len() * 2,
1531 "shell should have at least 2x faces"
1532 );
1533 }
1534
1535 #[test]
1536 fn test_shell_generation_vertex_count() {
1537 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1538 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1539 let shell = generate_shell(verts, faces, 0.2);
1540 assert!(shell.vertices.len() >= verts.len() * 2);
1542 }
1543
1544 #[test]
1547 fn test_sdf_cone_inside_negative() {
1548 let cone = SdfCone {
1549 apex: [0.0; 3],
1550 height: 5.0,
1551 angle_rad: 0.5,
1552 };
1553 let d = cone.distance([0.0, -1.0, 0.0]);
1555 let _ = d; }
1558
1559 #[test]
1560 fn test_sdf_cone_above_apex_positive() {
1561 let cone = SdfCone {
1562 apex: [0.0; 3],
1563 height: 3.0,
1564 angle_rad: 0.3,
1565 };
1566 assert!(cone.distance([0.0, 1.0, 0.0]) > 0.0);
1568 }
1569
1570 #[test]
1573 fn test_smooth_intersection_finite() {
1574 let si = SdfSmoothIntersection {
1575 a: Box::new(SdfSphere {
1576 center: [0.0; 3],
1577 radius: 2.0,
1578 }),
1579 b: Box::new(SdfSphere {
1580 center: [1.0, 0.0, 0.0],
1581 radius: 2.0,
1582 }),
1583 k: 0.3,
1584 };
1585 assert!(si.distance([0.5, 0.0, 0.0]).is_finite());
1586 }
1587
1588 #[test]
1591 fn test_voxel_sdf_idx_row_major() {
1592 let grid = VoxelSdf::new(4, 5, 6, [0.0; 3], 1.0);
1593 let idx = grid.idx(1, 2, 3);
1594 let expected = 1 + 4 * (2 + 5 * 3);
1595 assert_eq!(idx, expected);
1596 }
1597
1598 #[test]
1599 fn test_voxel_sdf_world_to_grid_origin() {
1600 let grid = VoxelSdf::new(10, 10, 10, [1.0, 2.0, 3.0], 0.5);
1601 let g = grid.world_to_grid([1.0, 2.0, 3.0]);
1602 assert!(g[0].abs() < 1e-12);
1603 assert!(g[1].abs() < 1e-12);
1604 assert!(g[2].abs() < 1e-12);
1605 }
1606
1607 #[test]
1608 fn test_offset_mesh_empty_centroid() {
1609 let mesh = OffsetMesh {
1610 vertices: vec![],
1611 normals: vec![],
1612 faces: vec![],
1613 };
1614 let c = mesh.centroid();
1615 assert_eq!(c, [0.0; 3]);
1616 }
1617
1618 #[test]
1619 fn test_sdf_gradient_unit_length_for_sphere() {
1620 let sphere = SdfSphere {
1621 center: [0.0; 3],
1622 radius: 1.0,
1623 };
1624 let g = sphere.gradient([3.0, 0.0, 0.0]);
1626 let gl = length(g);
1627 assert!(
1628 (gl - 1.0).abs() < 0.01,
1629 "gradient magnitude should be ~1, got {gl}"
1630 );
1631 }
1632
1633 #[test]
1634 fn test_sdf_normal_unit_length() {
1635 let sphere = SdfSphere {
1636 center: [0.0; 3],
1637 radius: 1.0,
1638 };
1639 let n = sphere.normal([2.0, 0.0, 0.0]);
1640 let nl = length(n);
1641 assert!(
1642 (nl - 1.0).abs() < 0.01,
1643 "normal should be unit length, got {nl}"
1644 );
1645 }
1646
1647 #[test]
1648 fn test_variable_offset_single_triangle_area_changes() {
1649 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1650 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1651 let weights = vec![0.5, 0.5, 0.5];
1652 let expanded = variable_offset(verts, faces, &weights);
1653 for (vn, &vo) in expanded.vertices.iter().zip(verts.iter()) {
1655 assert!(length(sub(*vn, vo)) > 0.0);
1656 }
1657 }
1658
1659 fn unit_cube_mesh() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
1661 let verts = vec![
1662 [0.0, 0.0, 0.0],
1663 [1.0, 0.0, 0.0],
1664 [1.0, 1.0, 0.0],
1665 [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
1667 [1.0, 0.0, 1.0],
1668 [1.0, 1.0, 1.0],
1669 [0.0, 1.0, 1.0], ];
1671 let faces = vec![
1672 [0, 1, 2],
1673 [0, 2, 3], [4, 5, 6],
1675 [4, 6, 7], [0, 1, 5],
1677 [0, 5, 4], [2, 3, 7],
1679 [2, 7, 6], [0, 3, 7],
1681 [0, 7, 4], [1, 2, 6],
1683 [1, 6, 5], ];
1685 (verts, faces)
1686 }
1687}