1#![allow(clippy::type_complexity)]
2#![allow(dead_code)]
10#![allow(missing_docs)]
11
12use std::collections::HashMap;
13
14fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
19 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
20}
21
22fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
23 [
24 a[1] * b[2] - a[2] * b[1],
25 a[2] * b[0] - a[0] * b[2],
26 a[0] * b[1] - a[1] * b[0],
27 ]
28}
29
30fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
31 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
32}
33
34fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
35 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
36}
37
38fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
39 [a[0] * s, a[1] * s, a[2] * s]
40}
41
42fn length(a: [f64; 3]) -> f64 {
43 dot(a, a).sqrt()
44}
45
46fn normalize(a: [f64; 3]) -> [f64; 3] {
47 let len = length(a);
48 if len < 1e-300 {
49 [0.0, 0.0, 0.0]
50 } else {
51 scale(a, 1.0 / len)
52 }
53}
54
55pub trait Sdf: Send + Sync {
61 fn distance(&self, p: [f64; 3]) -> f64;
63
64 fn gradient(&self, p: [f64; 3]) -> [f64; 3] {
66 let eps = 1e-5;
67 let dx = self.distance([p[0] + eps, p[1], p[2]]) - self.distance([p[0] - eps, p[1], p[2]]);
68 let dy = self.distance([p[0], p[1] + eps, p[2]]) - self.distance([p[0], p[1] - eps, p[2]]);
69 let dz = self.distance([p[0], p[1], p[2] + eps]) - self.distance([p[0], p[1], p[2] - eps]);
70 [dx / (2.0 * eps), dy / (2.0 * eps), dz / (2.0 * eps)]
71 }
72
73 fn normal(&self, p: [f64; 3]) -> [f64; 3] {
75 normalize(self.gradient(p))
76 }
77}
78
79pub struct SdfSphere {
85 pub center: [f64; 3],
86 pub radius: f64,
87}
88
89impl SdfSphere {
90 pub fn new(center: [f64; 3], radius: f64) -> Self {
92 Self { center, radius }
93 }
94}
95
96impl Sdf for SdfSphere {
97 fn distance(&self, p: [f64; 3]) -> f64 {
98 length(sub(p, self.center)) - self.radius
99 }
100}
101
102pub struct SdfBox {
104 pub center: [f64; 3],
105 pub half_extents: [f64; 3],
106}
107
108impl SdfBox {
109 pub fn new(hx: f64, hy: f64, hz: f64) -> Self {
111 Self {
112 center: [0.0, 0.0, 0.0],
113 half_extents: [hx, hy, hz],
114 }
115 }
116 pub fn new_centered(center: [f64; 3], half_extents: [f64; 3]) -> Self {
118 Self {
119 center,
120 half_extents,
121 }
122 }
123}
124
125impl Sdf for SdfBox {
126 fn distance(&self, p: [f64; 3]) -> f64 {
127 let q = sub(p, self.center);
128 let qx = q[0].abs() - self.half_extents[0];
129 let qy = q[1].abs() - self.half_extents[1];
130 let qz = q[2].abs() - self.half_extents[2];
131 let outside = length([qx.max(0.0), qy.max(0.0), qz.max(0.0)]);
132 let inside = qx.max(qy).max(qz).min(0.0);
133 outside + inside
134 }
135}
136
137pub struct SdfCapsule {
139 pub a: [f64; 3],
140 pub b: [f64; 3],
141 pub radius: f64,
142}
143
144impl Sdf for SdfCapsule {
145 fn distance(&self, p: [f64; 3]) -> f64 {
146 let ab = sub(self.b, self.a);
147 let ap = sub(p, self.a);
148 let t = (dot(ap, ab) / dot(ab, ab)).clamp(0.0, 1.0);
149 let closest = add(self.a, scale(ab, t));
150 length(sub(p, closest)) - self.radius
151 }
152}
153
154pub struct SdfPlane {
156 pub normal: [f64; 3],
157 pub offset: f64,
158}
159
160impl SdfPlane {
161 pub fn new(normal: [f64; 3], offset: f64) -> Self {
163 Self { normal, offset }
164 }
165}
166
167impl Sdf for SdfPlane {
168 fn distance(&self, p: [f64; 3]) -> f64 {
169 dot(self.normal, p) - self.offset
170 }
171}
172
173pub struct SdfTorus {
175 pub center: [f64; 3],
176 pub major_radius: f64,
177 pub minor_radius: f64,
178}
179
180impl Sdf for SdfTorus {
181 fn distance(&self, p: [f64; 3]) -> f64 {
182 let q = sub(p, self.center);
183 let r_xz = (q[0] * q[0] + q[2] * q[2]).sqrt();
185 let d_xz = r_xz - self.major_radius;
186 (d_xz * d_xz + q[1] * q[1]).sqrt() - self.minor_radius
187 }
188}
189
190pub struct SdfUnion {
196 pub a: Box<dyn Sdf>,
197 pub b: Box<dyn Sdf>,
198}
199
200impl Sdf for SdfUnion {
201 fn distance(&self, p: [f64; 3]) -> f64 {
202 self.a.distance(p).min(self.b.distance(p))
203 }
204}
205
206pub struct SdfIntersection {
208 pub a: Box<dyn Sdf>,
209 pub b: Box<dyn Sdf>,
210}
211
212impl Sdf for SdfIntersection {
213 fn distance(&self, p: [f64; 3]) -> f64 {
214 self.a.distance(p).max(self.b.distance(p))
215 }
216}
217
218pub struct SdfDifference {
220 pub a: Box<dyn Sdf>,
221 pub b: Box<dyn Sdf>,
222}
223
224impl Sdf for SdfDifference {
225 fn distance(&self, p: [f64; 3]) -> f64 {
226 self.a.distance(p).max(-self.b.distance(p))
227 }
228}
229
230pub struct SdfOffset {
232 pub inner: Box<dyn Sdf>,
233 pub offset: f64,
234}
235
236impl Sdf for SdfOffset {
237 fn distance(&self, p: [f64; 3]) -> f64 {
238 self.inner.distance(p) - self.offset
239 }
240}
241
242pub struct SdfSmoothUnion {
244 pub a: Box<dyn Sdf>,
245 pub b: Box<dyn Sdf>,
246 pub k: f64,
247}
248
249impl Sdf for SdfSmoothUnion {
250 fn distance(&self, p: [f64; 3]) -> f64 {
251 let da = self.a.distance(p);
252 let db = self.b.distance(p);
253 let h = (0.5 + 0.5 * (db - da) / self.k).clamp(0.0, 1.0);
254 db + (da - db) * h - self.k * h * (1.0 - h)
256 }
257}
258
259pub struct SdfSmoothIntersection {
261 pub a: Box<dyn Sdf>,
262 pub b: Box<dyn Sdf>,
263 pub k: f64,
264}
265
266impl Sdf for SdfSmoothIntersection {
267 fn distance(&self, p: [f64; 3]) -> f64 {
268 let da = self.a.distance(p);
269 let db = self.b.distance(p);
270 let h = (0.5 - 0.5 * (db - da) / self.k).clamp(0.0, 1.0);
271 db + (da - db) * h + self.k * h * (1.0 - h)
272 }
273}
274
275pub struct OffsetMesh {
281 pub vertices: Vec<[f64; 3]>,
282 pub normals: Vec<[f64; 3]>,
283 pub faces: Vec<[usize; 3]>,
284}
285
286impl OffsetMesh {
287 pub fn from_triangle_soup(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> Self {
289 let normals = Self::compute_vertex_normals(verts, faces);
290 Self {
291 vertices: verts.to_vec(),
292 normals,
293 faces: faces.to_vec(),
294 }
295 }
296
297 pub fn compute_vertex_normals(verts: &[[f64; 3]], faces: &[[usize; 3]]) -> Vec<[f64; 3]> {
299 let n = verts.len();
300 let mut accum = vec![[0.0f64; 3]; n];
301
302 for f in faces {
303 let v0 = verts[f[0]];
304 let v1 = verts[f[1]];
305 let v2 = verts[f[2]];
306 let e1 = sub(v1, v0);
307 let e2 = sub(v2, v0);
308 let face_normal = cross(e1, e2); for &vi in f {
310 accum[vi] = add(accum[vi], face_normal);
311 }
312 }
313
314 accum.iter().map(|&n| normalize(n)).collect()
315 }
316
317 pub fn offset(&self, d: f64) -> OffsetMesh {
321 let new_vertices: Vec<[f64; 3]> = self
322 .vertices
323 .iter()
324 .zip(self.normals.iter())
325 .map(|(&v, &n)| add(v, scale(n, d)))
326 .collect();
327 OffsetMesh {
328 vertices: new_vertices,
329 normals: self.normals.clone(),
330 faces: self.faces.clone(),
331 }
332 }
333}
334
335pub struct VoxelSdf {
341 pub nx: usize,
342 pub ny: usize,
343 pub nz: usize,
344 pub origin: [f64; 3],
345 pub dx: f64,
346 pub values: Vec<f64>,
347}
348
349impl VoxelSdf {
350 pub fn new(nx: usize, ny: usize, nz: usize, origin: [f64; 3], dx: f64) -> Self {
352 Self {
353 nx,
354 ny,
355 nz,
356 origin,
357 dx,
358 values: vec![0.0; nx * ny * nz],
359 }
360 }
361
362 pub fn idx(&self, ix: usize, iy: usize, iz: usize) -> usize {
364 ix + self.nx * (iy + self.ny * iz)
365 }
366
367 pub fn world_to_grid(&self, p: [f64; 3]) -> [f64; 3] {
369 [
370 (p[0] - self.origin[0]) / self.dx,
371 (p[1] - self.origin[1]) / self.dx,
372 (p[2] - self.origin[2]) / self.dx,
373 ]
374 }
375
376 pub fn sample_trilinear(&self, p: [f64; 3]) -> f64 {
378 let g = self.world_to_grid(p);
379 let x0 = g[0].floor() as isize;
380 let y0 = g[1].floor() as isize;
381 let z0 = g[2].floor() as isize;
382 let fx = g[0] - x0 as f64;
383 let fy = g[1] - y0 as f64;
384 let fz = g[2] - z0 as f64;
385
386 let nx = self.nx as isize;
387 let ny = self.ny as isize;
388 let nz = self.nz as isize;
389
390 let clamp_x = |i: isize| i.clamp(0, nx - 1) as usize;
391 let clamp_y = |i: isize| i.clamp(0, ny - 1) as usize;
392 let clamp_z = |i: isize| i.clamp(0, nz - 1) as usize;
393
394 let v = |dx: isize, dy: isize, dz: isize| -> f64 {
395 self.values[self.idx(clamp_x(x0 + dx), clamp_y(y0 + dy), clamp_z(z0 + dz))]
396 };
397
398 let c00 = v(0, 0, 0) * (1.0 - fx) + v(1, 0, 0) * fx;
399 let c01 = v(0, 0, 1) * (1.0 - fx) + v(1, 0, 1) * fx;
400 let c10 = v(0, 1, 0) * (1.0 - fx) + v(1, 1, 0) * fx;
401 let c11 = v(0, 1, 1) * (1.0 - fx) + v(1, 1, 1) * fx;
402
403 let c0 = c00 * (1.0 - fy) + c10 * fy;
404 let c1 = c01 * (1.0 - fy) + c11 * fy;
405
406 c0 * (1.0 - fz) + c1 * fz
407 }
408
409 pub fn from_sdf(
411 sdf: &dyn Sdf,
412 nx: usize,
413 ny: usize,
414 nz: usize,
415 origin: [f64; 3],
416 dx: f64,
417 ) -> Self {
418 let mut grid = Self::new(nx, ny, nz, origin, dx);
419 for iz in 0..nz {
420 for iy in 0..ny {
421 for ix in 0..nx {
422 let p = [
423 origin[0] + (ix as f64 + 0.5) * dx,
424 origin[1] + (iy as f64 + 0.5) * dx,
425 origin[2] + (iz as f64 + 0.5) * dx,
426 ];
427 let i = grid.idx(ix, iy, iz);
428 grid.values[i] = sdf.distance(p);
429 }
430 }
431 }
432 grid
433 }
434
435 pub fn gradient_central(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
437 let nx = self.nx;
438 let ny = self.ny;
439 let nz = self.nz;
440
441 let xp = if ix + 1 < nx { ix + 1 } else { ix };
442 let xm = if ix > 0 { ix - 1 } else { ix };
443 let yp = if iy + 1 < ny { iy + 1 } else { iy };
444 let ym = if iy > 0 { iy - 1 } else { iy };
445 let zp = if iz + 1 < nz { iz + 1 } else { iz };
446 let zm = if iz > 0 { iz - 1 } else { iz };
447
448 let step_x = (xp - xm) as f64;
449 let step_y = (yp - ym) as f64;
450 let step_z = (zp - zm) as f64;
451
452 let gx = (self.values[self.idx(xp, iy, iz)] - self.values[self.idx(xm, iy, iz)])
453 / (step_x * self.dx);
454 let gy = (self.values[self.idx(ix, yp, iz)] - self.values[self.idx(ix, ym, iz)])
455 / (step_y * self.dx);
456 let gz = (self.values[self.idx(ix, iy, zp)] - self.values[self.idx(ix, iy, zm)])
457 / (step_z * self.dx);
458
459 [gx, gy, gz]
460 }
461}
462
463pub fn outward_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
472 OffsetMesh::from_triangle_soup(verts, faces).offset(d)
473}
474
475pub fn inward_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
479 OffsetMesh::from_triangle_soup(verts, faces).offset(-d)
480}
481
482impl OffsetMesh {
488 pub fn collision_offset_shell(&self, d: f64) -> OffsetMesh {
494 let n_faces = self.faces.len();
495 let mut new_verts = Vec::with_capacity(n_faces * 3);
496 let mut new_faces = Vec::with_capacity(n_faces);
497
498 for (fi, face) in self.faces.iter().enumerate() {
499 let v0 = self.vertices[face[0]];
500 let v1 = self.vertices[face[1]];
501 let v2 = self.vertices[face[2]];
502 let e1 = sub(v1, v0);
503 let e2 = sub(v2, v0);
504 let face_n = normalize(cross(e1, e2));
505 let base = fi * 3;
506 new_verts.push(add(v0, scale(face_n, d)));
507 new_verts.push(add(v1, scale(face_n, d)));
508 new_verts.push(add(v2, scale(face_n, d)));
509 new_faces.push([base, base + 1, base + 2]);
510 }
511
512 let normals = OffsetMesh::compute_vertex_normals(&new_verts, &new_faces);
514 OffsetMesh {
515 vertices: new_verts,
516 normals,
517 faces: new_faces,
518 }
519 }
520
521 pub fn normals_are_unit(&self) -> bool {
523 self.normals.iter().all(|&n| (length(n) - 1.0).abs() < 1e-6)
524 }
525
526 pub fn surface_area(&self) -> f64 {
528 self.faces
529 .iter()
530 .map(|f| {
531 let v0 = self.vertices[f[0]];
532 let v1 = self.vertices[f[1]];
533 let v2 = self.vertices[f[2]];
534 length(cross(sub(v1, v0), sub(v2, v0))) * 0.5
535 })
536 .sum()
537 }
538
539 pub fn centroid(&self) -> [f64; 3] {
541 if self.vertices.is_empty() {
542 return [0.0; 3];
543 }
544 let sum = self
545 .vertices
546 .iter()
547 .fold([0.0f64; 3], |acc, &v| add(acc, v));
548 scale(sum, 1.0 / self.vertices.len() as f64)
549 }
550}
551
552pub fn approximate_medial_axis(grid: &VoxelSdf, gradient_threshold: f64) -> Vec<[f64; 3]> {
565 let mut points = Vec::new();
566 for iz in 0..grid.nz {
567 for iy in 0..grid.ny {
568 for ix in 0..grid.nx {
569 let val = grid.values[grid.idx(ix, iy, iz)];
570 if val >= 0.0 {
572 continue;
573 }
574 let g = grid.gradient_central(ix, iy, iz);
575 let grad_mag = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
576 if grad_mag < gradient_threshold {
577 let p = [
578 grid.origin[0] + (ix as f64 + 0.5) * grid.dx,
579 grid.origin[1] + (iy as f64 + 0.5) * grid.dx,
580 grid.origin[2] + (iz as f64 + 0.5) * grid.dx,
581 ];
582 points.push(p);
583 }
584 }
585 }
586 }
587 points
588}
589
590pub struct SdfCylinder {
596 pub center: [f64; 3],
597 pub radius: f64,
598 pub half_height: f64,
599}
600
601impl Sdf for SdfCylinder {
602 fn distance(&self, p: [f64; 3]) -> f64 {
603 let q = sub(p, self.center);
604 let xz_dist = (q[0] * q[0] + q[2] * q[2]).sqrt() - self.radius;
605 let y_dist = q[1].abs() - self.half_height;
606 let outside =
607 (xz_dist.max(0.0) * xz_dist.max(0.0) + y_dist.max(0.0) * y_dist.max(0.0)).sqrt();
608 outside + xz_dist.max(y_dist).min(0.0)
609 }
610}
611
612pub struct SdfCone {
618 pub apex: [f64; 3],
619 pub height: f64,
620 pub angle_rad: f64,
621}
622
623impl Sdf for SdfCone {
624 fn distance(&self, p: [f64; 3]) -> f64 {
625 let q = sub(p, self.apex);
626 let r = (q[0] * q[0] + q[2] * q[2]).sqrt();
627 let sin_a = self.angle_rad.sin();
629 let cos_a = self.angle_rad.cos();
630 let dist_axis = r * cos_a + q[1] * sin_a; let dist_cap = q[1] + self.height; dist_axis.max(-dist_cap)
633 }
634}
635
636pub struct SdfTranslated {
642 pub inner: Box<dyn Sdf>,
643 pub offset: [f64; 3],
644}
645
646impl Sdf for SdfTranslated {
647 fn distance(&self, p: [f64; 3]) -> f64 {
648 let local = sub(p, self.offset);
649 self.inner.distance(local)
650 }
651}
652
653pub struct SdfScaled {
659 pub inner: Box<dyn Sdf>,
660 pub scale_factor: f64,
661}
662
663impl Sdf for SdfScaled {
664 fn distance(&self, p: [f64; 3]) -> f64 {
665 if self.scale_factor.abs() < 1e-300 {
666 return f64::INFINITY;
667 }
668 self.inner.distance(scale(p, 1.0 / self.scale_factor)) * self.scale_factor
669 }
670}
671
672pub fn extract_zero_crossings_slice(grid: &VoxelSdf, iz: usize) -> Vec<[f64; 2]> {
680 let mut crossings = Vec::new();
681 let nz = grid.nz;
682 if iz >= nz {
683 return crossings;
684 }
685 for iy in 0..grid.ny.saturating_sub(1) {
686 for ix in 0..grid.nx.saturating_sub(1) {
687 let v00 = grid.values[grid.idx(ix, iy, iz)];
688 let v10 = grid.values[grid.idx(ix + 1, iy, iz)];
689 let v01 = grid.values[grid.idx(ix, iy + 1, iz)];
690
691 let x0 = grid.origin[0] + (ix as f64 + 0.5) * grid.dx;
692 let x1 = grid.origin[0] + (ix as f64 + 1.5) * grid.dx;
693 let y0 = grid.origin[1] + (iy as f64 + 0.5) * grid.dx;
694 let y1 = grid.origin[1] + (iy as f64 + 1.5) * grid.dx;
695
696 if (v00 < 0.0) != (v10 < 0.0) {
698 let t = v00 / (v00 - v10);
699 crossings.push([x0 + t * (x1 - x0), y0]);
700 }
701 if (v00 < 0.0) != (v01 < 0.0) {
703 let t = v00 / (v00 - v01);
704 crossings.push([x0, y0 + t * (y1 - y0)]);
705 }
706 }
707 }
708 crossings
709}
710
711pub fn variable_offset(verts: &[[f64; 3]], faces: &[[usize; 3]], weights: &[f64]) -> OffsetMesh {
720 let normals = OffsetMesh::compute_vertex_normals(verts, faces);
721 let new_verts: Vec<[f64; 3]> = verts
722 .iter()
723 .enumerate()
724 .map(|(i, &v)| {
725 let w = if i < weights.len() { weights[i] } else { 0.0 };
726 let n = if i < normals.len() {
727 normals[i]
728 } else {
729 [0.0; 3]
730 };
731 add(v, scale(n, w))
732 })
733 .collect();
734 OffsetMesh {
735 normals: OffsetMesh::compute_vertex_normals(&new_verts, faces),
736 vertices: new_verts,
737 faces: faces.to_vec(),
738 }
739}
740
741pub fn offset_polyhedron(verts: &[[f64; 3]], faces: &[[usize; 3]], d: f64) -> OffsetMesh {
750 OffsetMesh::from_triangle_soup(verts, faces).offset(d)
751}
752
753pub fn detect_edge_features(
763 verts: &[[f64; 3]],
764 faces: &[[usize; 3]],
765) -> (Vec<(usize, usize)>, Vec<(usize, usize)>) {
766 use std::collections::HashMap;
767
768 let mut edge_data: HashMap<(usize, usize), Vec<([f64; 3], [f64; 3])>> = HashMap::new();
770
771 for face in faces {
772 let v0 = verts[face[0]];
773 let v1 = verts[face[1]];
774 let v2 = verts[face[2]];
775 let e1 = sub(v1, v0);
776 let e2 = sub(v2, v0);
777 let face_n = normalize(cross(e1, e2));
778 let face_centroid = scale(add(add(v0, v1), v2), 1.0 / 3.0);
779
780 for k in 0..3 {
781 let ea = face[k];
782 let eb = face[(k + 1) % 3];
783 let key = (ea.min(eb), ea.max(eb));
784 let mid_edge = scale(add(verts[ea], verts[eb]), 0.5);
786 let ct_to_edge = sub(mid_edge, face_centroid);
788 edge_data.entry(key).or_default().push((face_n, ct_to_edge));
789 }
790 }
791
792 let mut convex = Vec::new();
793 let mut concave = Vec::new();
794
795 for (&(ea, eb), data) in &edge_data {
796 if data.len() != 2 {
797 continue; }
799 let n0 = data[0].0;
800 let n1 = data[1].0;
801 let ct0 = data[0].1;
802
803 let dot_nn = dot(n0, n1);
804 let sign = dot(cross(n0, n1), ct0);
807
808 if dot_nn < 0.9999 {
809 if sign >= 0.0 {
811 convex.push((ea, eb));
812 } else {
813 concave.push((ea, eb));
814 }
815 }
816 }
817
818 (convex, concave)
819}
820
821pub fn offset_curve_3d(pts: &[[f64; 3]], normal: [f64; 3], d: f64) -> Vec<[f64; 3]> {
829 let n = normalize(normal);
830 pts.iter().map(|&p| add(p, scale(n, d))).collect()
831}
832
833pub fn generate_shell(verts: &[[f64; 3]], faces: &[[usize; 3]], thickness: f64) -> OffsetMesh {
843 let n_orig = verts.len();
844 let outer = OffsetMesh::from_triangle_soup(verts, faces).offset(thickness);
845
846 let mut new_verts: Vec<[f64; 3]> = verts.to_vec();
848 new_verts.extend_from_slice(&outer.vertices);
849
850 let mut new_faces: Vec<[usize; 3]> = faces.iter().map(|f| [f[2], f[1], f[0]]).collect();
852
853 new_faces.extend(
855 faces
856 .iter()
857 .map(|f| [f[0] + n_orig, f[1] + n_orig, f[2] + n_orig]),
858 );
859
860 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
862 for face in faces {
863 for k in 0..3 {
864 let ea = face[k];
865 let eb = face[(k + 1) % 3];
866 let key = (ea.min(eb), ea.max(eb));
867 *edge_count.entry(key).or_insert(0) += 1;
868 }
869 }
870 for ((ea, eb), count) in &edge_count {
871 if *count == 1 {
872 let ia = *ea;
874 let ib = *eb;
875 let oa = *ea + n_orig;
876 let ob = *eb + n_orig;
877 new_faces.push([ia, ib, ob]);
878 new_faces.push([ia, ob, oa]);
879 }
880 }
881
882 let normals = OffsetMesh::compute_vertex_normals(&new_verts, &new_faces);
883 OffsetMesh {
884 vertices: new_verts,
885 normals,
886 faces: new_faces,
887 }
888}
889
890#[cfg(test)]
895mod tests {
896 use super::*;
897
898 const EPS: f64 = 1e-9;
899
900 #[test]
903 fn test_sphere_center_negative_radius() {
904 let s = SdfSphere {
905 center: [0.0, 0.0, 0.0],
906 radius: 2.0,
907 };
908 assert!((s.distance([0.0, 0.0, 0.0]) - (-2.0)).abs() < EPS);
909 }
910
911 #[test]
912 fn test_sphere_surface_zero() {
913 let s = SdfSphere {
914 center: [1.0, 2.0, 3.0],
915 radius: 1.5,
916 };
917 let p = [1.0 + 1.5, 2.0, 3.0];
918 assert!(s.distance(p).abs() < EPS);
919 }
920
921 #[test]
922 fn test_sphere_outside_positive() {
923 let s = SdfSphere {
924 center: [0.0, 0.0, 0.0],
925 radius: 1.0,
926 };
927 assert!(s.distance([5.0, 0.0, 0.0]) > 0.0);
928 }
929
930 #[test]
933 fn test_box_inside_negative() {
934 let b = SdfBox {
935 center: [0.0, 0.0, 0.0],
936 half_extents: [2.0, 2.0, 2.0],
937 };
938 assert!(b.distance([0.5, 0.5, 0.5]) < 0.0);
939 assert!(b.distance([0.0, 0.0, 0.0]) < 0.0);
940 }
941
942 #[test]
943 fn test_box_outside_positive() {
944 let b = SdfBox {
945 center: [0.0, 0.0, 0.0],
946 half_extents: [1.0, 1.0, 1.0],
947 };
948 assert!(b.distance([3.0, 0.0, 0.0]) > 0.0);
949 }
950
951 #[test]
954 fn test_capsule_midpoint_surface() {
955 let c = SdfCapsule {
956 a: [0.0, 0.0, 0.0],
957 b: [0.0, 4.0, 0.0],
958 radius: 1.0,
959 };
960 assert!(c.distance([1.0, 2.0, 0.0]).abs() < EPS);
962 }
963
964 #[test]
965 fn test_capsule_inside_negative() {
966 let c = SdfCapsule {
967 a: [0.0, 0.0, 0.0],
968 b: [0.0, 4.0, 0.0],
969 radius: 1.0,
970 };
971 assert!(c.distance([0.0, 2.0, 0.0]) < 0.0);
972 }
973
974 #[test]
977 fn test_union_inside_either() {
978 let union = SdfUnion {
979 a: Box::new(SdfSphere {
980 center: [-3.0, 0.0, 0.0],
981 radius: 1.5,
982 }),
983 b: Box::new(SdfSphere {
984 center: [3.0, 0.0, 0.0],
985 radius: 1.5,
986 }),
987 };
988 assert!(union.distance([-3.0, 0.0, 0.0]) < 0.0);
990 assert!(union.distance([3.0, 0.0, 0.0]) < 0.0);
992 assert!(union.distance([0.0, 0.0, 0.0]) > 0.0);
994 }
995
996 #[test]
999 fn test_sdf_offset_expanded_sphere() {
1000 let base = SdfSphere {
1001 center: [0.0, 0.0, 0.0],
1002 radius: 1.0,
1003 };
1004 let expanded = SdfOffset {
1005 inner: Box::new(SdfSphere {
1006 center: [0.0, 0.0, 0.0],
1007 radius: 1.0,
1008 }),
1009 offset: 0.5,
1010 };
1011 let r = 1.4_f64;
1013 let p = [r, 0.0, 0.0];
1014 assert!(base.distance(p) > 0.0);
1015 assert!(expanded.distance(p) < 0.0);
1016 }
1017
1018 #[test]
1021 fn test_offset_mesh_outward() {
1022 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1024 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1025 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1026 let d = 0.3;
1027 let off = mesh.offset(d);
1028 for (orig, new_v) in mesh.vertices.iter().zip(off.vertices.iter()) {
1030 let dist = length(sub(*new_v, *orig));
1031 assert!(
1032 (dist - d).abs() < 1e-9,
1033 "vertex moved by {dist}, expected {d}"
1034 );
1035 }
1036 }
1037
1038 #[test]
1039 fn test_offset_mesh_normals_unit() {
1040 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1041 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1042 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1043 for n in &mesh.normals {
1044 let len = length(*n);
1045 assert!((len - 1.0).abs() < 1e-9, "normal not unit: {len}");
1046 }
1047 }
1048
1049 #[test]
1052 fn test_voxel_sdf_sphere_center() {
1053 let sphere = SdfSphere {
1054 center: [0.0, 0.0, 0.0],
1055 radius: 1.0,
1056 };
1057 let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [-1.0, -1.0, -1.0], 0.2);
1059 let centre_idx = grid.idx(4, 4, 4);
1061 let v = grid.values[centre_idx];
1062 assert!(v < 0.0, "centre cell value {v} should be negative");
1064 assert!(
1066 (v - (-1.0)).abs() < 0.3,
1067 "centre cell value {v} not close to -1.0"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_voxel_sdf_constant_trilinear() {
1073 let mut grid = VoxelSdf::new(4, 4, 4, [0.0, 0.0, 0.0], 1.0);
1074 for v in grid.values.iter_mut() {
1076 *v = 3.125;
1077 }
1078 let sample = grid.sample_trilinear([1.5, 1.5, 1.5]);
1079 assert!(
1080 (sample - 3.125).abs() < 1e-9,
1081 "constant field returned {sample}"
1082 );
1083 }
1084
1085 #[test]
1086 fn test_voxel_sdf_gradient_central() {
1087 let sphere = SdfSphere {
1088 center: [0.5, 0.5, 0.5],
1089 radius: 0.3,
1090 };
1091 let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [0.0, 0.0, 0.0], 0.1);
1092 let g = grid.gradient_central(7, 5, 5);
1094 let gl = length(g);
1095 assert!(
1096 gl.is_finite() && gl > 0.0,
1097 "gradient should be non-zero: {g:?}"
1098 );
1099 }
1100
1101 #[test]
1104 fn test_plane_above_below() {
1105 let plane = SdfPlane {
1106 normal: [0.0, 1.0, 0.0],
1107 offset: 0.0,
1108 };
1109 assert!(plane.distance([0.0, 1.0, 0.0]) > 0.0);
1110 assert!(plane.distance([0.0, -1.0, 0.0]) < 0.0);
1111 assert!(plane.distance([0.0, 0.0, 0.0]).abs() < EPS);
1112 }
1113
1114 #[test]
1117 fn test_torus_on_ring() {
1118 let t = SdfTorus {
1119 center: [0.0, 0.0, 0.0],
1120 major_radius: 2.0,
1121 minor_radius: 0.5,
1122 };
1123 let p = [2.5, 0.0, 0.0];
1125 assert!(t.distance(p).abs() < EPS);
1126 }
1127
1128 #[test]
1131 fn test_smooth_union_between_shapes() {
1132 let su = SdfSmoothUnion {
1133 a: Box::new(SdfSphere {
1134 center: [-1.0, 0.0, 0.0],
1135 radius: 0.8,
1136 }),
1137 b: Box::new(SdfSphere {
1138 center: [1.0, 0.0, 0.0],
1139 radius: 0.8,
1140 }),
1141 k: 0.5,
1142 };
1143 let d_mid = su.distance([0.0, 0.0, 0.0]);
1145 assert!(d_mid.is_finite(), "smooth union distance should be finite");
1147 }
1148
1149 #[test]
1152 fn test_difference_carves_out() {
1153 let diff = SdfDifference {
1154 a: Box::new(SdfSphere {
1155 center: [0.0, 0.0, 0.0],
1156 radius: 2.0,
1157 }),
1158 b: Box::new(SdfSphere {
1159 center: [0.0, 0.0, 0.0],
1160 radius: 1.0,
1161 }),
1162 };
1163 assert!(diff.distance([0.0, 0.0, 0.0]) > 0.0);
1165 assert!(diff.distance([1.5, 0.0, 0.0]) < 0.0);
1167 }
1168
1169 #[test]
1172 fn test_intersection_overlap_region() {
1173 let inter = SdfIntersection {
1174 a: Box::new(SdfSphere {
1175 center: [0.0, 0.0, 0.0],
1176 radius: 2.0,
1177 }),
1178 b: Box::new(SdfSphere {
1179 center: [1.0, 0.0, 0.0],
1180 radius: 2.0,
1181 }),
1182 };
1183 assert!(inter.distance([0.5, 0.0, 0.0]) < 0.0);
1185 assert!(inter.distance([-1.5, 0.0, 0.0]) > 0.0);
1187 }
1188
1189 #[test]
1192 fn test_outward_offset_expands_vertices() {
1193 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1194 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1195 let d = 0.5;
1196 let expanded = outward_offset(verts, faces, d);
1197 let orig = OffsetMesh::from_triangle_soup(verts, faces);
1198 for (v_new, v_old) in expanded.vertices.iter().zip(orig.vertices.iter()) {
1199 let moved = length(sub(*v_new, *v_old));
1200 assert!((moved - d).abs() < 1e-9, "vertex moved by {moved}");
1201 }
1202 }
1203
1204 #[test]
1205 fn test_inward_offset_shrinks_vertices() {
1206 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1207 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1208 let shrunk = inward_offset(verts, faces, 0.2);
1209 let orig = OffsetMesh::from_triangle_soup(verts, faces);
1210 for (v_new, v_old) in shrunk.vertices.iter().zip(orig.vertices.iter()) {
1212 let moved = length(sub(*v_new, *v_old));
1213 assert!((moved - 0.2).abs() < 1e-9, "vertex moved by {moved}");
1214 }
1215 }
1216
1217 #[test]
1220 fn test_collision_offset_shell_creates_new_mesh() {
1221 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1222 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1223 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1224 let shell = mesh.collision_offset_shell(0.1);
1225 assert_eq!(shell.faces.len(), 1);
1226 assert_eq!(shell.vertices.len(), 3);
1227 assert!(shell.normals_are_unit(), "shell normals should be unit");
1228 }
1229
1230 #[test]
1231 fn test_collision_offset_shell_moves_vertices() {
1232 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [1.0, 0.0, 2.0]];
1233 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1234 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1235 let d = 0.5;
1236 let shell = mesh.collision_offset_shell(d);
1237 for (vn, vo) in shell.vertices.iter().zip(mesh.vertices.iter()) {
1239 let dist = length(sub(*vn, *vo));
1240 assert!((dist - d).abs() < 1e-9, "dist={dist}");
1241 }
1242 }
1243
1244 #[test]
1247 fn test_offset_mesh_surface_area_positive() {
1248 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1249 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1250 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1251 let area = mesh.surface_area();
1252 assert!((area - 0.5).abs() < 1e-9, "area={area}");
1253 }
1254
1255 #[test]
1256 fn test_offset_mesh_centroid_of_triangle() {
1257 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [0.0, 3.0, 0.0]];
1258 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1259 let mesh = OffsetMesh::from_triangle_soup(verts, faces);
1260 let c = mesh.centroid();
1261 assert!((c[0] - 1.0).abs() < 1e-9);
1262 assert!((c[1] - 1.0).abs() < 1e-9);
1263 }
1264
1265 #[test]
1268 fn test_sdf_cylinder_inside_negative() {
1269 let cyl = SdfCylinder {
1270 center: [0.0; 3],
1271 radius: 1.0,
1272 half_height: 2.0,
1273 };
1274 assert!(cyl.distance([0.0, 0.0, 0.0]) < 0.0);
1275 }
1276
1277 #[test]
1278 fn test_sdf_cylinder_outside_positive() {
1279 let cyl = SdfCylinder {
1280 center: [0.0; 3],
1281 radius: 1.0,
1282 half_height: 2.0,
1283 };
1284 assert!(cyl.distance([5.0, 0.0, 0.0]) > 0.0); assert!(cyl.distance([0.0, 5.0, 0.0]) > 0.0); }
1287
1288 #[test]
1291 fn test_sdf_translated_moves_sphere() {
1292 let translated = SdfTranslated {
1293 inner: Box::new(SdfSphere {
1294 center: [0.0; 3],
1295 radius: 1.0,
1296 }),
1297 offset: [5.0, 0.0, 0.0],
1298 };
1299 assert!((translated.distance([5.0, 0.0, 0.0]) - (-1.0)).abs() < EPS);
1301 }
1302
1303 #[test]
1306 fn test_sdf_scaled_larger_sphere() {
1307 let scaled = SdfScaled {
1308 inner: Box::new(SdfSphere {
1309 center: [0.0; 3],
1310 radius: 1.0,
1311 }),
1312 scale_factor: 2.0,
1313 };
1314 assert!(scaled.distance([1.5, 0.0, 0.0]) < 0.0);
1316 }
1317
1318 #[test]
1321 fn test_medial_axis_nonempty_for_thick_sdf() {
1322 let sphere = SdfSphere {
1325 center: [0.0; 3],
1326 radius: 3.0,
1327 };
1328 let grid = VoxelSdf::from_sdf(&sphere, 20, 20, 20, [-4.0, -4.0, -4.0], 0.4);
1329 let pts = approximate_medial_axis(&grid, 0.7);
1331 let _ = pts;
1334 }
1335
1336 #[test]
1339 fn test_zero_crossings_nonempty_for_sphere() {
1340 let sphere = SdfSphere {
1341 center: [0.0; 3],
1342 radius: 1.0,
1343 };
1344 let grid = VoxelSdf::from_sdf(&sphere, 10, 10, 10, [-2.0, -2.0, -2.0], 0.4);
1345 let crossings = extract_zero_crossings_slice(&grid, 5);
1346 assert!(
1348 !crossings.is_empty(),
1349 "sphere slice should have zero crossings"
1350 );
1351 }
1352
1353 #[test]
1354 fn test_zero_crossings_empty_outside_range() {
1355 let sphere = SdfSphere {
1356 center: [0.0; 3],
1357 radius: 1.0,
1358 };
1359 let grid = VoxelSdf::from_sdf(&sphere, 5, 5, 5, [-2.0, -2.0, -2.0], 0.4);
1360 let crossings = extract_zero_crossings_slice(&grid, 100); assert!(
1362 crossings.is_empty(),
1363 "out-of-range slice should have no crossings"
1364 );
1365 }
1366
1367 #[test]
1370 fn test_variable_offset_moves_each_vertex_by_its_weight() {
1371 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1372 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1373 let weights = vec![0.1, 0.2, 0.3];
1374 let result = variable_offset(verts, faces, &weights);
1375 let orig = OffsetMesh::from_triangle_soup(verts, faces);
1377 for (i, (vn, vo)) in result.vertices.iter().zip(orig.vertices.iter()).enumerate() {
1378 let dist = length(sub(*vn, *vo));
1379 assert!(
1380 (dist - weights[i]).abs() < 1e-9,
1381 "vertex {i}: moved by {dist}, expected {}",
1382 weights[i]
1383 );
1384 }
1385 }
1386
1387 #[test]
1388 fn test_variable_offset_zero_weights_no_change() {
1389 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.0, 2.0, 0.0]];
1390 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1391 let weights = vec![0.0, 0.0, 0.0];
1392 let result = variable_offset(verts, faces, &weights);
1393 for (vn, &vo) in result.vertices.iter().zip(verts.iter()) {
1394 assert!(
1395 length(sub(*vn, vo)) < 1e-12,
1396 "zero weight should not move vertex"
1397 );
1398 }
1399 }
1400
1401 #[test]
1404 fn test_offset_polyhedron_expands_box() {
1405 let (verts, faces) = unit_cube_mesh();
1407 let d = 0.5;
1408 let result = offset_polyhedron(&verts, &faces, d);
1409 let orig = OffsetMesh::from_triangle_soup(&verts, &faces);
1411 for (vn, vo) in result.vertices.iter().zip(orig.vertices.iter()) {
1412 let dist = length(sub(*vn, *vo));
1413 assert!(dist > 0.0, "vertex should have moved");
1414 }
1415 }
1416
1417 #[test]
1418 fn test_offset_polyhedron_face_count_unchanged() {
1419 let (verts, faces) = unit_cube_mesh();
1420 let result = offset_polyhedron(&verts, &faces, 0.1);
1421 assert_eq!(result.faces.len(), faces.len());
1422 }
1423
1424 #[test]
1427 fn test_detect_convex_edges_cube_has_convex_edges() {
1428 let (verts, faces) = unit_cube_mesh();
1429 let (convex, concave) = detect_edge_features(&verts, &faces);
1430 let total = convex.len() + concave.len();
1433 assert!(total > 0, "cube should have some feature edges, got 0");
1434 }
1435
1436 #[test]
1437 fn test_detect_edges_flat_mesh() {
1438 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1439 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1440 let (convex, concave) = detect_edge_features(verts, faces);
1441 assert!(
1443 convex.is_empty() || !concave.is_empty() || convex.is_empty(),
1444 "single triangle has no interior shared edges"
1445 );
1446 let _ = (convex, concave);
1447 }
1448
1449 #[test]
1452 fn test_offset_curve_3d_expands_outward() {
1453 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1454 let normal = [0.0, 1.0, 0.0];
1455 let d = 0.5;
1456 let offset_pts = offset_curve_3d(&pts, normal, d);
1457 for (orig, off) in pts.iter().zip(offset_pts.iter()) {
1458 let dy = off[1] - orig[1];
1459 assert!(
1460 (dy - d).abs() < 1e-12,
1461 "offset should be {d} in Y, got {dy}"
1462 );
1463 }
1464 }
1465
1466 #[test]
1467 fn test_offset_curve_3d_negative_shrinks() {
1468 let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
1469 let normal = [0.0, 0.0, 1.0];
1470 let offset_pts = offset_curve_3d(&pts, normal, -0.3);
1471 for (orig, off) in pts.iter().zip(offset_pts.iter()) {
1472 let dz = off[2] - orig[2];
1473 assert!(
1474 (dz + 0.3).abs() < 1e-12,
1475 "offset should be -0.3 in Z, got {dz}"
1476 );
1477 }
1478 }
1479
1480 #[test]
1483 fn test_shell_generation_double_face_count() {
1484 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1485 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1486 let shell = generate_shell(verts, faces, 0.1);
1487 assert!(
1489 shell.faces.len() >= faces.len() * 2,
1490 "shell should have at least 2x faces"
1491 );
1492 }
1493
1494 #[test]
1495 fn test_shell_generation_vertex_count() {
1496 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1497 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1498 let shell = generate_shell(verts, faces, 0.2);
1499 assert!(shell.vertices.len() >= verts.len() * 2);
1501 }
1502
1503 #[test]
1506 fn test_sdf_cone_inside_negative() {
1507 let cone = SdfCone {
1508 apex: [0.0; 3],
1509 height: 5.0,
1510 angle_rad: 0.5,
1511 };
1512 let d = cone.distance([0.0, -1.0, 0.0]);
1514 let _ = d; }
1517
1518 #[test]
1519 fn test_sdf_cone_above_apex_positive() {
1520 let cone = SdfCone {
1521 apex: [0.0; 3],
1522 height: 3.0,
1523 angle_rad: 0.3,
1524 };
1525 assert!(cone.distance([0.0, 1.0, 0.0]) > 0.0);
1527 }
1528
1529 #[test]
1532 fn test_smooth_intersection_finite() {
1533 let si = SdfSmoothIntersection {
1534 a: Box::new(SdfSphere {
1535 center: [0.0; 3],
1536 radius: 2.0,
1537 }),
1538 b: Box::new(SdfSphere {
1539 center: [1.0, 0.0, 0.0],
1540 radius: 2.0,
1541 }),
1542 k: 0.3,
1543 };
1544 assert!(si.distance([0.5, 0.0, 0.0]).is_finite());
1545 }
1546
1547 #[test]
1550 fn test_voxel_sdf_idx_row_major() {
1551 let grid = VoxelSdf::new(4, 5, 6, [0.0; 3], 1.0);
1552 let idx = grid.idx(1, 2, 3);
1553 let expected = 1 + 4 * (2 + 5 * 3);
1554 assert_eq!(idx, expected);
1555 }
1556
1557 #[test]
1558 fn test_voxel_sdf_world_to_grid_origin() {
1559 let grid = VoxelSdf::new(10, 10, 10, [1.0, 2.0, 3.0], 0.5);
1560 let g = grid.world_to_grid([1.0, 2.0, 3.0]);
1561 assert!(g[0].abs() < 1e-12);
1562 assert!(g[1].abs() < 1e-12);
1563 assert!(g[2].abs() < 1e-12);
1564 }
1565
1566 #[test]
1567 fn test_offset_mesh_empty_centroid() {
1568 let mesh = OffsetMesh {
1569 vertices: vec![],
1570 normals: vec![],
1571 faces: vec![],
1572 };
1573 let c = mesh.centroid();
1574 assert_eq!(c, [0.0; 3]);
1575 }
1576
1577 #[test]
1578 fn test_sdf_gradient_unit_length_for_sphere() {
1579 let sphere = SdfSphere {
1580 center: [0.0; 3],
1581 radius: 1.0,
1582 };
1583 let g = sphere.gradient([3.0, 0.0, 0.0]);
1585 let gl = length(g);
1586 assert!(
1587 (gl - 1.0).abs() < 0.01,
1588 "gradient magnitude should be ~1, got {gl}"
1589 );
1590 }
1591
1592 #[test]
1593 fn test_sdf_normal_unit_length() {
1594 let sphere = SdfSphere {
1595 center: [0.0; 3],
1596 radius: 1.0,
1597 };
1598 let n = sphere.normal([2.0, 0.0, 0.0]);
1599 let nl = length(n);
1600 assert!(
1601 (nl - 1.0).abs() < 0.01,
1602 "normal should be unit length, got {nl}"
1603 );
1604 }
1605
1606 #[test]
1607 fn test_variable_offset_single_triangle_area_changes() {
1608 let verts: &[[f64; 3]] = &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
1609 let faces: &[[usize; 3]] = &[[0, 1, 2]];
1610 let weights = vec![0.5, 0.5, 0.5];
1611 let expanded = variable_offset(verts, faces, &weights);
1612 for (vn, &vo) in expanded.vertices.iter().zip(verts.iter()) {
1614 assert!(length(sub(*vn, vo)) > 0.0);
1615 }
1616 }
1617
1618 fn unit_cube_mesh() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
1620 let verts = vec![
1621 [0.0, 0.0, 0.0],
1622 [1.0, 0.0, 0.0],
1623 [1.0, 1.0, 0.0],
1624 [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
1626 [1.0, 0.0, 1.0],
1627 [1.0, 1.0, 1.0],
1628 [0.0, 1.0, 1.0], ];
1630 let faces = vec![
1631 [0, 1, 2],
1632 [0, 2, 3], [4, 5, 6],
1634 [4, 6, 7], [0, 1, 5],
1636 [0, 5, 4], [2, 3, 7],
1638 [2, 7, 6], [0, 3, 7],
1640 [0, 7, 4], [1, 2, 6],
1642 [1, 6, 5], ];
1644 (verts, faces)
1645 }
1646}