1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SkeletonMethod {
16 BallRolling,
18 Thinning,
20 Voronoi,
22 MedialSurface,
24}
25
26#[derive(Debug, Clone)]
28pub struct MedialAxisPoint {
29 pub position: [f64; 3],
31 pub radius: f64,
33 pub tangent: [f64; 3],
35 pub feature_size: f64,
37}
38
39impl MedialAxisPoint {
40 pub fn new(position: [f64; 3], radius: f64, tangent: [f64; 3], feature_size: f64) -> Self {
42 Self {
43 position,
44 radius,
45 tangent,
46 feature_size,
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct MedialAxis {
54 pub points: Vec<MedialAxisPoint>,
56 pub branches: Vec<Vec<usize>>,
58 pub junctions: Vec<usize>,
60}
61
62impl MedialAxis {
63 pub fn from_points(boundary: &[[f64; 3]], method: SkeletonMethod) -> Self {
68 let _ = method; if boundary.is_empty() {
70 return Self {
71 points: vec![],
72 branches: vec![],
73 junctions: vec![],
74 };
75 }
76
77 let n = boundary.len() as f64;
79 let cx = boundary.iter().map(|p| p[0]).sum::<f64>() / n;
80 let cy = boundary.iter().map(|p| p[1]).sum::<f64>() / n;
81 let cz = boundary.iter().map(|p| p[2]).sum::<f64>() / n;
82 let center = [cx, cy, cz];
83
84 let avg_r = boundary.iter().map(|p| dist3(*p, center)).sum::<f64>() / n;
86
87 let mut max_dist = 0.0_f64;
89 let mut far_a = boundary[0];
90 let mut far_b = boundary[0];
91 for i in 0..boundary.len() {
92 for j in (i + 1)..boundary.len() {
93 let d = dist3(boundary[i], boundary[j]);
94 if d > max_dist {
95 max_dist = d;
96 far_a = boundary[i];
97 far_b = boundary[j];
98 }
99 }
100 }
101
102 let steps = 5usize;
104 let mut pts = Vec::with_capacity(steps);
105 for i in 0..steps {
106 let t = i as f64 / (steps - 1) as f64;
107 let pos = lerp3(far_a, far_b, t);
108 let r = boundary
110 .iter()
111 .map(|p| dist3(pos, *p))
112 .fold(f64::INFINITY, f64::min);
113 let r = if r == f64::INFINITY { avg_r } else { r };
114 let tangent = normalize3(sub3(far_b, far_a));
115 pts.push(MedialAxisPoint::new(pos, r, tangent, r));
116 }
117
118 let branch: Vec<usize> = (0..pts.len()).collect();
119 Self {
120 points: pts,
121 branches: vec![branch],
122 junctions: vec![],
123 }
124 }
125
126 pub fn branch_count(&self) -> usize {
128 self.branches.len()
129 }
130
131 pub fn total_length(&self) -> f64 {
133 let mut len = 0.0;
134 for branch in &self.branches {
135 for w in branch.windows(2) {
136 len += dist3(self.points[w[0]].position, self.points[w[1]].position);
137 }
138 }
139 len
140 }
141
142 pub fn is_tubular(&self) -> bool {
144 if self.branches.len() != 1 {
145 return false;
146 }
147 if self.points.len() < 2 {
148 return true;
149 }
150 let radii: Vec<f64> = self.points.iter().map(|p| p.radius).collect();
151 let mean = radii.iter().sum::<f64>() / radii.len() as f64;
152 let variance = radii.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / radii.len() as f64;
153 let cv = variance.sqrt() / mean.max(1e-12); cv < 0.3
155 }
156}
157
158pub struct Voronoi2dSkeleton;
160
161impl Voronoi2dSkeleton {
162 pub fn compute(points: &[[f64; 2]], bbox: [f64; 4]) -> Vec<[f64; 2]> {
167 let n = points.len();
169 if n < 3 {
170 return vec![];
171 }
172 let mut result = Vec::new();
173 for i in 0..n {
174 for j in (i + 1)..n {
175 for k in (j + 1)..n {
176 if let Some(cc) = circumcenter_2d(points[i], points[j], points[k])
177 && cc[0] >= bbox[0]
178 && cc[1] >= bbox[1]
179 && cc[0] <= bbox[2]
180 && cc[1] <= bbox[3]
181 {
182 result.push(cc);
183 }
184 }
185 }
186 }
187 result
188 }
189}
190
191pub struct StraightSkeleton {
193 polygon: Vec<[f64; 2]>,
194}
195
196impl StraightSkeleton {
197 pub fn new(polygon: Vec<[f64; 2]>) -> Self {
199 Self { polygon }
200 }
201
202 pub fn compute_offsets(&self, dist: f64) -> Vec<Vec<[f64; 2]>> {
206 let n = self.polygon.len();
207 if n < 3 {
208 return vec![];
209 }
210 let mut offset = Vec::with_capacity(n);
211 for i in 0..n {
212 let prev = self.polygon[(i + n - 1) % n];
213 let curr = self.polygon[i];
214 let next = self.polygon[(i + 1) % n];
215
216 let d1 = normalize2(sub2(curr, prev));
218 let d2 = normalize2(sub2(next, curr));
219 let n1 = [-d1[1], d1[0]];
220 let n2 = [-d2[1], d2[0]];
221 let bisector = normalize2(add2(n1, n2));
222
223 let dot = n1[0] * n2[0] + n1[1] * n2[1];
225 let sin_half = ((1.0 - dot) / 2.0).sqrt().max(1e-9);
226 let scale = 1.0 / sin_half;
227
228 let op = [
229 curr[0] + bisector[0] * dist * scale,
230 curr[1] + bisector[1] * dist * scale,
231 ];
232 offset.push(op);
233 }
234 vec![offset]
235 }
236
237 pub fn skeleton_edges(&self) -> Vec<([f64; 2], [f64; 2])> {
241 let n = self.polygon.len();
243 if n < 3 {
244 return vec![];
245 }
246 let cx = self.polygon.iter().map(|p| p[0]).sum::<f64>() / n as f64;
247 let cy = self.polygon.iter().map(|p| p[1]).sum::<f64>() / n as f64;
248 let centroid = [cx, cy];
249
250 let mut edges = Vec::with_capacity(n);
251 for i in 0..n {
252 edges.push((self.polygon[i], centroid));
253 }
254 edges
255 }
256}
257
258pub fn point_to_segment_dist(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
262 let ab = sub3(b, a);
263 let ap = sub3(p, a);
264 let len_sq = dot3(ab, ab);
265 if len_sq < 1e-24 {
266 return dist3(p, a);
267 }
268 let t = (dot3(ap, ab) / len_sq).clamp(0.0, 1.0);
269 let closest = add3(a, scale3(ab, t));
270 dist3(p, closest)
271}
272
273pub fn point_to_polygon_dist(p: [f64; 2], poly: &[[f64; 2]]) -> f64 {
275 let n = poly.len();
276 if n == 0 {
277 return f64::INFINITY;
278 }
279 let mut min_d = f64::INFINITY;
280 for i in 0..n {
281 let a = poly[i];
282 let b = poly[(i + 1) % n];
283 let d = seg_dist_2d(p, a, b);
284 if d < min_d {
285 min_d = d;
286 }
287 }
288 min_d
289}
290
291pub fn is_inside_polygon(p: [f64; 2], poly: &[[f64; 2]]) -> bool {
293 let n = poly.len();
294 if n < 3 {
295 return false;
296 }
297 let mut inside = false;
298 let (x, y) = (p[0], p[1]);
299 let mut j = n - 1;
300 for i in 0..n {
301 let xi = poly[i][0];
302 let yi = poly[i][1];
303 let xj = poly[j][0];
304 let yj = poly[j][1];
305 let intersect = ((yi > y) != (yj > y))
306 && (x < (xj - xi) * (y - yi) / (yj - yi + if yj >= yi { 1e-14 } else { -1e-14 }) + xi);
307 if intersect {
308 inside = !inside;
309 }
310 j = i;
311 }
312 inside
313}
314
315pub struct CenterlineExtraction;
319
320impl CenterlineExtraction {
321 pub fn extract_centerline(surface_points: &[[f64; 3]], num_slices: usize) -> Vec<[f64; 3]> {
326 if surface_points.is_empty() || num_slices == 0 {
327 return vec![];
328 }
329
330 let mut x_min = f64::INFINITY;
332 let mut x_max = f64::NEG_INFINITY;
333 for p in surface_points {
334 if p[0] < x_min {
335 x_min = p[0];
336 }
337 if p[0] > x_max {
338 x_max = p[0];
339 }
340 }
341
342 let mut centerline = Vec::with_capacity(num_slices);
343 for s in 0..num_slices {
344 let t = s as f64 / (num_slices - 1).max(1) as f64;
345 let x_c = x_min + t * (x_max - x_min);
346 let half_width = (x_max - x_min) / (num_slices as f64 * 2.0);
347
348 let slice_pts: Vec<[f64; 3]> = surface_points
350 .iter()
351 .filter(|p| (p[0] - x_c).abs() <= half_width)
352 .copied()
353 .collect();
354
355 let centroid = if slice_pts.is_empty() {
356 [x_c, 0.0, 0.0]
357 } else {
358 let m = slice_pts.len() as f64;
359 [
360 slice_pts.iter().map(|p| p[0]).sum::<f64>() / m,
361 slice_pts.iter().map(|p| p[1]).sum::<f64>() / m,
362 slice_pts.iter().map(|p| p[2]).sum::<f64>() / m,
363 ]
364 };
365 centerline.push(centroid);
366 }
367 centerline
368 }
369}
370
371pub fn prune_short_branches(ma: &mut MedialAxis, min_length: f64) {
375 ma.branches.retain(|branch| {
376 let mut len = 0.0;
377 for w in branch.windows(2) {
378 len += dist3(ma.points[w[0]].position, ma.points[w[1]].position);
379 }
380 len >= min_length
381 });
382}
383
384pub fn euler_characteristic(ma: &MedialAxis) -> i32 {
391 let v = ma.points.len() as i32;
392 let mut e = 0i32;
393 for branch in &ma.branches {
394 e += branch.len().saturating_sub(1) as i32;
395 }
396 v - e
397}
398
399#[derive(Debug, Clone)]
404pub struct AxisAlignedSlab {
405 pub x_min: f64,
407 pub x_max: f64,
409 pub point_indices: Vec<usize>,
411}
412
413impl AxisAlignedSlab {
414 pub fn decompose(ma: &MedialAxis, num_slabs: usize) -> Vec<Self> {
417 if num_slabs == 0 || ma.points.is_empty() {
418 return vec![];
419 }
420 let x_vals: Vec<f64> = ma.points.iter().map(|p| p.position[0]).collect();
421 let x_min = x_vals.iter().cloned().fold(f64::INFINITY, f64::min);
422 let x_max = x_vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
423 let width = (x_max - x_min) / num_slabs as f64;
424
425 let mut slabs: Vec<AxisAlignedSlab> = (0..num_slabs)
426 .map(|i| AxisAlignedSlab {
427 x_min: x_min + i as f64 * width,
428 x_max: x_min + (i + 1) as f64 * width,
429 point_indices: vec![],
430 })
431 .collect();
432
433 for (idx, pt) in ma.points.iter().enumerate() {
434 let raw = ((pt.position[0] - x_min) / width.max(1e-15)) as usize;
435 let slab_idx = raw.min(num_slabs - 1);
436 slabs[slab_idx].point_indices.push(idx);
437 }
438 slabs
439 }
440}
441
442pub fn shape_diameter(p: [f64; 3], n: [f64; 3], radius: f64) -> f64 {
449 let _ = p;
453 let _n_norm = normalize3(n);
454 2.0 * radius
455}
456
457fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
460 let dx = a[0] - b[0];
461 let dy = a[1] - b[1];
462 let dz = a[2] - b[2];
463 (dx * dx + dy * dy + dz * dz).sqrt()
464}
465
466fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
467 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
468}
469
470fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
471 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
472}
473
474fn scale3(a: [f64; 3], t: f64) -> [f64; 3] {
475 [a[0] * t, a[1] * t, a[2] * t]
476}
477
478fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
479 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
480}
481
482fn normalize3(v: [f64; 3]) -> [f64; 3] {
483 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
484 if len < 1e-15 {
485 [0.0, 0.0, 1.0]
486 } else {
487 [v[0] / len, v[1] / len, v[2] / len]
488 }
489}
490
491fn lerp3(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
492 [
493 a[0] + (b[0] - a[0]) * t,
494 a[1] + (b[1] - a[1]) * t,
495 a[2] + (b[2] - a[2]) * t,
496 ]
497}
498
499fn sub2(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
500 [a[0] - b[0], a[1] - b[1]]
501}
502
503fn add2(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
504 [a[0] + b[0], a[1] + b[1]]
505}
506
507fn normalize2(v: [f64; 2]) -> [f64; 2] {
508 let len = (v[0] * v[0] + v[1] * v[1]).sqrt();
509 if len < 1e-15 {
510 [1.0, 0.0]
511 } else {
512 [v[0] / len, v[1] / len]
513 }
514}
515
516fn seg_dist_2d(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
517 let ab = sub2(b, a);
518 let ap = sub2(p, a);
519 let len_sq = ab[0] * ab[0] + ab[1] * ab[1];
520 if len_sq < 1e-24 {
521 return (ap[0] * ap[0] + ap[1] * ap[1]).sqrt();
522 }
523 let t = ((ap[0] * ab[0] + ap[1] * ab[1]) / len_sq).clamp(0.0, 1.0);
524 let cx = a[0] + ab[0] * t - p[0];
525 let cy = a[1] + ab[1] * t - p[1];
526 (cx * cx + cy * cy).sqrt()
527}
528
529fn circumcenter_2d(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> Option<[f64; 2]> {
530 let ax = b[0] - a[0];
531 let ay = b[1] - a[1];
532 let bx = c[0] - a[0];
533 let by = c[1] - a[1];
534 let d = 2.0 * (ax * by - ay * bx);
535 if d.abs() < 1e-12 {
536 return None;
537 }
538 let ux = (by * (ax * ax + ay * ay) - ay * (bx * bx + by * by)) / d;
539 let uy = (ax * (bx * bx + by * by) - bx * (ax * ax + ay * ay)) / d;
540 Some([a[0] + ux, a[1] + uy])
541}
542
543#[derive(Debug, Clone)]
547pub struct BinaryImage {
548 pub width: usize,
550 pub height: usize,
552 pub data: Vec<bool>,
554}
555
556impl BinaryImage {
557 pub fn new(width: usize, height: usize, fill: bool) -> Self {
559 Self {
560 width,
561 height,
562 data: vec![fill; width * height],
563 }
564 }
565
566 pub fn get(&self, col: usize, row: usize) -> bool {
568 if col < self.width && row < self.height {
569 self.data[row * self.width + col]
570 } else {
571 false
572 }
573 }
574
575 pub fn set(&mut self, col: usize, row: usize, val: bool) {
577 if col < self.width && row < self.height {
578 self.data[row * self.width + col] = val;
579 }
580 }
581
582 pub fn foreground_count(&self) -> usize {
584 self.data.iter().filter(|&&v| v).count()
585 }
586}
587
588pub fn distance_transform(img: &BinaryImage) -> Vec<f64> {
594 let w = img.width;
595 let h = img.height;
596 let n = w * h;
597 let big = (w + h) as f64;
598 let mut dt = vec![0.0_f64; n];
599
600 for i in 0..n {
602 dt[i] = if img.data[i] { big } else { 0.0 };
603 }
604
605 for r in 0..h {
607 for c in 0..w {
608 let idx = r * w + c;
609 if c > 0 {
610 dt[idx] = dt[idx].min(dt[idx - 1] + 1.0);
611 }
612 if r > 0 {
613 dt[idx] = dt[idx].min(dt[(r - 1) * w + c] + 1.0);
614 }
615 }
616 }
617
618 for r in (0..h).rev() {
620 for c in (0..w).rev() {
621 let idx = r * w + c;
622 if c + 1 < w {
623 dt[idx] = dt[idx].min(dt[idx + 1] + 1.0);
624 }
625 if r + 1 < h {
626 dt[idx] = dt[idx].min(dt[(r + 1) * w + c] + 1.0);
627 }
628 }
629 }
630
631 dt
632}
633
634pub fn distance_transform_exact(img: &BinaryImage) -> Vec<f64> {
637 let w = img.width;
638 let h = img.height;
639 if w == 0 || h == 0 {
640 return vec![];
641 }
642
643 if !img.data.iter().any(|&v| v) {
645 return vec![0.0; w * h];
646 }
647
648 let big = (w * w + h * h) as f64;
649 let mut g = vec![0.0_f64; w * h];
651 for c in 0..w {
652 g[c] = if img.data[c] { big } else { 0.0 };
654 for r in 1..h {
655 g[r * w + c] = if img.data[r * w + c] {
656 g[(r - 1) * w + c] + 1.0
657 } else {
658 0.0
659 };
660 }
661 for r in (0..h.saturating_sub(1)).rev() {
663 if g[(r + 1) * w + c] + 1.0 < g[r * w + c] {
664 g[r * w + c] = g[(r + 1) * w + c] + 1.0;
665 }
666 }
667 for r in 0..h {
669 let v = g[r * w + c];
670 g[r * w + c] = v * v;
671 }
672 }
673
674 let mut dt = vec![0.0_f64; w * h];
676 let mut s_buf = vec![0usize; w];
677 let mut t_buf = vec![0i64; w];
678
679 for r in 0..h {
680 let mut q = 0usize; s_buf[0] = 0;
683 t_buf[0] = 0;
684
685 for u in 1..w {
686 let g_su = g[r * w + s_buf[q]];
687 let g_u = g[r * w + u];
688 while q > 0 {
689 let s_val = s_buf[q];
690 let g_s = g[r * w + s_val];
691 let sep = sep_edt(s_buf[q - 1], s_val, g[r * w + s_buf[q - 1]], g_s, w);
692 if t_buf[q] as usize <= sep {
693 break;
694 }
695 q -= 1;
696 }
697 let _ = g_su;
698 q += 1;
699 s_buf[q] = u;
700 let prev_s = s_buf[q - 1];
701 t_buf[q] = (sep_edt(prev_s, u, g[r * w + prev_s], g_u, w) + 1) as i64;
702 }
703
704 for u in (0..w).rev() {
706 let s = s_buf[q];
707 let diff = u.abs_diff(s);
708 dt[r * w + u] = ((diff * diff) as f64 + g[r * w + s]).sqrt();
709 if u as i64 == t_buf[q] && q > 0 {
710 q -= 1;
711 }
712 }
713 }
714
715 dt
716}
717
718fn sep_edt(i: usize, u: usize, gi: f64, gu: f64, _w: usize) -> usize {
720 let num = (u * u) as f64 - (i * i) as f64 + gu - gi;
722 let den = 2.0 * (u as f64 - i as f64);
723 if den.abs() < 1e-15 {
724 return 0;
725 }
726 (num / den).floor().max(0.0) as usize
727}
728
729pub fn zhang_suen_thinning(img: &mut BinaryImage) {
736 let w = img.width;
737 let h = img.height;
738 if w < 3 || h < 3 {
739 return;
740 }
741
742 loop {
743 let to_remove_1 = zs_subiteration(img, true);
745 for &(c, r) in &to_remove_1 {
746 img.set(c, r, false);
747 }
748
749 let to_remove_2 = zs_subiteration(img, false);
751 for &(c, r) in &to_remove_2 {
752 img.set(c, r, false);
753 }
754
755 if to_remove_1.is_empty() && to_remove_2.is_empty() {
756 break;
757 }
758 }
759}
760
761fn zs_subiteration(img: &BinaryImage, first: bool) -> Vec<(usize, usize)> {
763 let w = img.width;
764 let h = img.height;
765 let mut to_remove = Vec::new();
766
767 for r in 1..h.saturating_sub(1) {
768 for c in 1..w.saturating_sub(1) {
769 if !img.get(c, r) {
770 continue;
771 }
772
773 let p2 = img.get(c, r - 1) as u8;
775 let p3 = img.get(c + 1, r - 1) as u8;
776 let p4 = img.get(c + 1, r) as u8;
777 let p5 = img.get(c + 1, r + 1) as u8;
778 let p6 = img.get(c, r + 1) as u8;
779 let p7 = img.get(c - 1, r + 1) as u8;
780 let p8 = img.get(c - 1, r) as u8;
781 let p9 = img.get(c - 1, r - 1) as u8;
782
783 let b = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9; if !(2..=6).contains(&b) {
785 continue;
786 }
787
788 let seq = [p2, p3, p4, p5, p6, p7, p8, p9, p2];
790 let a: u8 = seq
791 .windows(2)
792 .map(|w| if w[0] == 0 && w[1] == 1 { 1u8 } else { 0 })
793 .sum();
794 if a != 1 {
795 continue;
796 }
797
798 if first {
799 if p2 * p4 * p6 != 0 || p4 * p6 * p8 != 0 {
801 continue;
802 }
803 } else {
804 if p2 * p4 * p8 != 0 || p2 * p6 * p8 != 0 {
806 continue;
807 }
808 }
809
810 to_remove.push((c, r));
811 }
812 }
813
814 to_remove
815}
816
817pub fn skeleton_pixels(img: &BinaryImage) -> Vec<(usize, usize)> {
819 let mut pixels = Vec::new();
820 for r in 0..img.height {
821 for c in 0..img.width {
822 if img.get(c, r) {
823 pixels.push((c, r));
824 }
825 }
826 }
827 pixels
828}
829
830#[derive(Debug, Clone)]
835pub struct MatPoint {
836 pub col: usize,
838 pub row: usize,
840 pub radius: f64,
842}
843
844pub fn medial_axis_transform(img: &BinaryImage) -> Vec<MatPoint> {
849 let dt = distance_transform(img);
850
851 let mut thin = img.clone();
852 zhang_suen_thinning(&mut thin);
853
854 let mut mat_points = Vec::new();
855 for r in 0..thin.height {
856 for c in 0..thin.width {
857 if thin.get(c, r) {
858 mat_points.push(MatPoint {
859 col: c,
860 row: r,
861 radius: dt[r * img.width + c],
862 });
863 }
864 }
865 }
866 mat_points
867}
868
869pub fn reconstruct_from_mat(mat_points: &[MatPoint], width: usize, height: usize) -> BinaryImage {
874 let mut img = BinaryImage::new(width, height, false);
875 for mp in mat_points {
876 let r_int = mp.radius.ceil() as isize;
877 let cx = mp.col as isize;
878 let cy = mp.row as isize;
879 for dy in -r_int..=r_int {
880 for dx in -r_int..=r_int {
881 if (dx * dx + dy * dy) as f64 <= mp.radius * mp.radius {
882 let nx = cx + dx;
883 let ny = cy + dy;
884 if nx >= 0 && ny >= 0 {
885 img.set(nx as usize, ny as usize, true);
886 }
887 }
888 }
889 }
890 }
891 img
892}
893
894pub fn branch_significance(ma: &MedialAxis, branch_idx: usize) -> f64 {
901 if branch_idx >= ma.branches.len() {
902 return 0.0;
903 }
904 let branch = &ma.branches[branch_idx];
905 if branch.is_empty() {
906 return 0.0;
907 }
908
909 let max_r = branch
910 .iter()
911 .map(|&i| ma.points[i].radius)
912 .fold(0.0_f64, f64::max);
913
914 let mut length = 0.0;
915 for w in branch.windows(2) {
916 length += dist3(ma.points[w[0]].position, ma.points[w[1]].position);
917 }
918
919 if length < 1e-15 {
920 return max_r;
921 }
922 max_r / length
923}
924
925pub fn prune_by_significance(ma: &mut MedialAxis, min_sig: f64) {
927 let sigs: Vec<f64> = (0..ma.branches.len())
928 .map(|i| branch_significance(ma, i))
929 .collect();
930 let mut keep = Vec::new();
931 for (i, &s) in sigs.iter().enumerate() {
932 if s >= min_sig {
933 keep.push(ma.branches[i].clone());
934 }
935 }
936 ma.branches = keep;
937}
938
939pub fn radius_function(ma: &MedialAxis, branch_idx: usize) -> Vec<(f64, f64)> {
945 if branch_idx >= ma.branches.len() {
946 return vec![];
947 }
948 let branch = &ma.branches[branch_idx];
949 let mut result = Vec::with_capacity(branch.len());
950 let mut arc = 0.0;
951 for (k, &idx) in branch.iter().enumerate() {
952 if k > 0 {
953 arc += dist3(ma.points[branch[k - 1]].position, ma.points[idx].position);
954 }
955 result.push((arc, ma.points[idx].radius));
956 }
957 result
958}
959
960pub fn average_radius(ma: &MedialAxis, branch_idx: usize) -> f64 {
962 if branch_idx >= ma.branches.len() {
963 return 0.0;
964 }
965 let branch = &ma.branches[branch_idx];
966 if branch.is_empty() {
967 return 0.0;
968 }
969 let sum: f64 = branch.iter().map(|&i| ma.points[i].radius).sum();
970 sum / branch.len() as f64
971}
972
973pub fn max_radius(ma: &MedialAxis, branch_idx: usize) -> f64 {
975 if branch_idx >= ma.branches.len() {
976 return 0.0;
977 }
978 ma.branches[branch_idx]
979 .iter()
980 .map(|&i| ma.points[i].radius)
981 .fold(0.0_f64, f64::max)
982}
983
984#[derive(Debug, Clone)]
988pub struct ShapePart {
989 pub point_indices: Vec<usize>,
991 pub approx_volume: f64,
993 pub centroid: [f64; 3],
995}
996
997pub fn decompose_from_skeleton(ma: &MedialAxis) -> Vec<ShapePart> {
1003 let mut parts = Vec::with_capacity(ma.branches.len());
1004 for branch in &ma.branches {
1005 if branch.is_empty() {
1006 continue;
1007 }
1008
1009 let mut vol = 0.0;
1010 let mut cx = 0.0;
1011 let mut cy = 0.0;
1012 let mut cz = 0.0;
1013
1014 for &idx in branch {
1015 let r = ma.points[idx].radius;
1016 vol += (4.0 / 3.0) * std::f64::consts::PI * r * r * r;
1017 cx += ma.points[idx].position[0];
1018 cy += ma.points[idx].position[1];
1019 cz += ma.points[idx].position[2];
1020 }
1021
1022 let n = branch.len() as f64;
1023 parts.push(ShapePart {
1024 point_indices: branch.clone(),
1025 approx_volume: vol,
1026 centroid: [cx / n, cy / n, cz / n],
1027 });
1028 }
1029 parts
1030}
1031
1032pub fn total_decomposed_volume(parts: &[ShapePart]) -> f64 {
1034 parts.iter().map(|p| p.approx_volume).sum()
1035}
1036
1037#[derive(Debug, Clone)]
1041pub struct CurveSkeletonNode {
1042 pub position: [f64; 3],
1044 pub radius: f64,
1046 pub neighbors: Vec<usize>,
1048}
1049
1050#[derive(Debug, Clone)]
1052pub struct CurveSkeleton {
1053 pub nodes: Vec<CurveSkeletonNode>,
1055}
1056
1057impl CurveSkeleton {
1058 pub fn from_surface_points(boundary: &[[f64; 3]], _contraction_steps: usize) -> Self {
1064 let ma = MedialAxis::from_points(boundary, SkeletonMethod::Voronoi);
1065
1066 let nodes: Vec<CurveSkeletonNode> = ma
1067 .points
1068 .iter()
1069 .map(|p| CurveSkeletonNode {
1070 position: p.position,
1071 radius: p.radius,
1072 neighbors: vec![],
1073 })
1074 .collect();
1075
1076 let mut skeleton = Self { nodes };
1077
1078 for branch in &ma.branches {
1080 for w in branch.windows(2) {
1081 let a = w[0];
1082 let b = w[1];
1083 if a < skeleton.nodes.len() && b < skeleton.nodes.len() {
1084 if !skeleton.nodes[a].neighbors.contains(&b) {
1085 skeleton.nodes[a].neighbors.push(b);
1086 }
1087 if !skeleton.nodes[b].neighbors.contains(&a) {
1088 skeleton.nodes[b].neighbors.push(a);
1089 }
1090 }
1091 }
1092 }
1093
1094 skeleton
1095 }
1096
1097 pub fn node_count(&self) -> usize {
1099 self.nodes.len()
1100 }
1101
1102 pub fn edge_count(&self) -> usize {
1104 let sum: usize = self.nodes.iter().map(|n| n.neighbors.len()).sum();
1105 sum / 2 }
1107
1108 pub fn total_length(&self) -> f64 {
1110 let mut len = 0.0;
1111 for (i, node) in self.nodes.iter().enumerate() {
1112 for &j in &node.neighbors {
1113 if j > i {
1114 len += dist3(node.position, self.nodes[j].position);
1115 }
1116 }
1117 }
1118 len
1119 }
1120
1121 pub fn leaf_nodes(&self) -> Vec<usize> {
1123 self.nodes
1124 .iter()
1125 .enumerate()
1126 .filter(|(_, n)| n.neighbors.len() == 1)
1127 .map(|(i, _)| i)
1128 .collect()
1129 }
1130
1131 pub fn junction_nodes(&self) -> Vec<usize> {
1133 self.nodes
1134 .iter()
1135 .enumerate()
1136 .filter(|(_, n)| n.neighbors.len() >= 3)
1137 .map(|(i, _)| i)
1138 .collect()
1139 }
1140
1141 pub fn prune_leaves(&mut self, min_length: f64) {
1143 loop {
1144 let leaves = self.leaf_nodes();
1145 let mut pruned = false;
1146 for leaf in leaves {
1147 if self.nodes[leaf].neighbors.is_empty() {
1148 continue;
1149 }
1150 let nbr = self.nodes[leaf].neighbors[0];
1151 let d = dist3(self.nodes[leaf].position, self.nodes[nbr].position);
1152 if d < min_length && self.nodes[nbr].neighbors.len() > 1 {
1153 self.nodes[nbr].neighbors.retain(|&n| n != leaf);
1155 self.nodes[leaf].neighbors.clear();
1156 pruned = true;
1157 }
1158 }
1159 if !pruned {
1160 break;
1161 }
1162 }
1163 }
1164}
1165
1166pub fn laplacian_smooth(
1170 points: &[[f64; 3]],
1171 adjacency: &[Vec<usize>],
1172 weight: f64,
1173) -> Vec<[f64; 3]> {
1174 let n = points.len();
1175 let mut result = vec![[0.0; 3]; n];
1176 for i in 0..n {
1177 if adjacency[i].is_empty() {
1178 result[i] = points[i];
1179 continue;
1180 }
1181 let mut avg = [0.0; 3];
1182 for &j in &adjacency[i] {
1183 avg[0] += points[j][0];
1184 avg[1] += points[j][1];
1185 avg[2] += points[j][2];
1186 }
1187 let k = adjacency[i].len() as f64;
1188 avg[0] /= k;
1189 avg[1] /= k;
1190 avg[2] /= k;
1191 result[i][0] = points[i][0] + weight * (avg[0] - points[i][0]);
1192 result[i][1] = points[i][1] + weight * (avg[1] - points[i][1]);
1193 result[i][2] = points[i][2] + weight * (avg[2] - points[i][2]);
1194 }
1195 result
1196}
1197
1198pub fn voronoi_medial_axis_2d(
1206 boundary: &[[f64; 2]],
1207 bbox: [f64; 4],
1208 min_radius: f64,
1209) -> Vec<([f64; 2], f64)> {
1210 let vertices = Voronoi2dSkeleton::compute(boundary, bbox);
1211 let mut result = Vec::new();
1212 for v in &vertices {
1213 let r = point_to_polygon_dist(*v, boundary);
1214 if r >= min_radius {
1215 result.push((*v, r));
1216 }
1217 }
1218 result
1219}
1220
1221pub fn directed_hausdorff(a: &MedialAxis, b: &MedialAxis) -> f64 {
1227 if a.points.is_empty() || b.points.is_empty() {
1228 return f64::INFINITY;
1229 }
1230 let mut max_d = 0.0_f64;
1231 for pa in &a.points {
1232 let min_d = b
1233 .points
1234 .iter()
1235 .map(|pb| dist3(pa.position, pb.position))
1236 .fold(f64::INFINITY, f64::min);
1237 if min_d > max_d {
1238 max_d = min_d;
1239 }
1240 }
1241 max_d
1242}
1243
1244pub fn hausdorff_distance(a: &MedialAxis, b: &MedialAxis) -> f64 {
1246 directed_hausdorff(a, b).max(directed_hausdorff(b, a))
1247}
1248
1249#[cfg(test)]
1252mod tests {
1253 use super::*;
1254
1255 fn cube_boundary() -> Vec<[f64; 3]> {
1257 let mut pts = vec![];
1258 for &x in &[-1.0, 1.0] {
1259 for &y in &[-1.0, 1.0] {
1260 for &z in &[-1.0, 1.0] {
1261 pts.push([x, y, z]);
1262 }
1263 }
1264 }
1265 pts
1266 }
1267
1268 fn unit_square() -> Vec<[f64; 2]> {
1270 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
1271 }
1272
1273 #[test]
1274 fn test_medial_axis_from_empty() {
1275 let ma = MedialAxis::from_points(&[], SkeletonMethod::BallRolling);
1276 assert_eq!(ma.points.len(), 0);
1277 assert_eq!(ma.branch_count(), 0);
1278 }
1279
1280 #[test]
1281 fn test_medial_axis_from_cube() {
1282 let pts = cube_boundary();
1283 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1284 assert!(!ma.points.is_empty());
1285 assert!(ma.branch_count() >= 1);
1286 }
1287
1288 #[test]
1289 fn test_medial_axis_total_length_positive() {
1290 let pts = cube_boundary();
1291 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Thinning);
1292 assert!(ma.total_length() > 0.0);
1293 }
1294
1295 #[test]
1296 fn test_medial_axis_is_tubular() {
1297 let mut pts = vec![];
1299 for i in 0..20 {
1300 let x = i as f64 * 0.5;
1301 for &(dy, dz) in &[(0.5, 0.0), (-0.5, 0.0), (0.0, 0.5), (0.0, -0.5)] {
1302 pts.push([x, dy, dz]);
1303 }
1304 }
1305 let ma = MedialAxis::from_points(&pts, SkeletonMethod::MedialSurface);
1306 let _ = ma.is_tubular(); }
1309
1310 #[test]
1311 fn test_medial_axis_single_point() {
1312 let pts = vec![[0.0, 0.0, 0.0]];
1313 let ma = MedialAxis::from_points(&pts, SkeletonMethod::BallRolling);
1314 assert_eq!(ma.points.len(), 5); }
1316
1317 #[test]
1318 fn test_skeleton_method_variants() {
1319 let methods = [
1320 SkeletonMethod::BallRolling,
1321 SkeletonMethod::Thinning,
1322 SkeletonMethod::Voronoi,
1323 SkeletonMethod::MedialSurface,
1324 ];
1325 for m in methods {
1326 let pts = cube_boundary();
1327 let ma = MedialAxis::from_points(&pts, m);
1328 assert!(ma.total_length() >= 0.0);
1329 }
1330 }
1331
1332 #[test]
1333 fn test_voronoi2d_skeleton_empty() {
1334 let pts: Vec<[f64; 2]> = vec![];
1335 let res = Voronoi2dSkeleton::compute(&pts, [0.0, 0.0, 1.0, 1.0]);
1336 assert!(res.is_empty());
1337 }
1338
1339 #[test]
1340 fn test_voronoi2d_skeleton_two_points() {
1341 let pts = vec![[0.0, 0.0], [1.0, 0.0]];
1342 let res = Voronoi2dSkeleton::compute(&pts, [0.0, 0.0, 1.0, 1.0]);
1343 assert!(res.is_empty()); }
1345
1346 #[test]
1347 fn test_voronoi2d_skeleton_four_points() {
1348 let pts = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1349 let res = Voronoi2dSkeleton::compute(&pts, [-0.5, -0.5, 1.5, 1.5]);
1350 assert!(!res.is_empty());
1352 }
1353
1354 #[test]
1355 fn test_voronoi2d_skeleton_inside_bbox() {
1356 let pts = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
1357 let res = Voronoi2dSkeleton::compute(&pts, [-10.0, -10.0, 10.0, 10.0]);
1358 for p in &res {
1359 assert!(p[0] >= -10.0 && p[0] <= 10.0);
1360 assert!(p[1] >= -10.0 && p[1] <= 10.0);
1361 }
1362 }
1363
1364 #[test]
1365 fn test_straight_skeleton_new() {
1366 let sk = StraightSkeleton::new(unit_square());
1367 let edges = sk.skeleton_edges();
1368 assert_eq!(edges.len(), 4);
1369 }
1370
1371 #[test]
1372 fn test_straight_skeleton_empty_polygon() {
1373 let sk = StraightSkeleton::new(vec![]);
1374 assert!(sk.skeleton_edges().is_empty());
1375 assert!(sk.compute_offsets(0.1).is_empty());
1376 }
1377
1378 #[test]
1379 fn test_straight_skeleton_offset_count() {
1380 let sk = StraightSkeleton::new(unit_square());
1381 let offsets = sk.compute_offsets(0.1);
1382 assert_eq!(offsets.len(), 1);
1383 assert_eq!(offsets[0].len(), 4);
1384 }
1385
1386 #[test]
1387 fn test_straight_skeleton_edges_toward_centroid() {
1388 let sq = unit_square();
1389 let sk = StraightSkeleton::new(sq.clone());
1390 let edges = sk.skeleton_edges();
1391 for (_, end) in &edges {
1393 assert!((end[0] - 0.5).abs() < 1e-10);
1394 assert!((end[1] - 0.5).abs() < 1e-10);
1395 }
1396 }
1397
1398 #[test]
1399 fn test_point_to_segment_dist_endpoint() {
1400 let p = [0.0, 0.0, 0.0];
1401 let a = [1.0, 0.0, 0.0];
1402 let b = [2.0, 0.0, 0.0];
1403 let d = point_to_segment_dist(p, a, b);
1404 assert!((d - 1.0).abs() < 1e-10);
1405 }
1406
1407 #[test]
1408 fn test_point_to_segment_dist_perpendicular() {
1409 let p = [1.0, 1.0, 0.0];
1410 let a = [0.0, 0.0, 0.0];
1411 let b = [2.0, 0.0, 0.0];
1412 let d = point_to_segment_dist(p, a, b);
1413 assert!((d - 1.0).abs() < 1e-10);
1414 }
1415
1416 #[test]
1417 fn test_point_to_segment_dist_degenerate() {
1418 let p = [3.0, 4.0, 0.0];
1419 let a = [0.0, 0.0, 0.0];
1420 let b = [0.0, 0.0, 0.0]; let d = point_to_segment_dist(p, a, b);
1422 assert!((d - 5.0).abs() < 1e-10);
1423 }
1424
1425 #[test]
1426 fn test_point_to_polygon_dist_square() {
1427 let poly = unit_square();
1428 let p = [0.5, 0.5];
1429 let d = point_to_polygon_dist(p, &poly);
1430 assert!((d - 0.5).abs() < 1e-10);
1431 }
1432
1433 #[test]
1434 fn test_point_to_polygon_dist_empty() {
1435 let d = point_to_polygon_dist([0.0, 0.0], &[]);
1436 assert!(d.is_infinite());
1437 }
1438
1439 #[test]
1440 fn test_is_inside_polygon_center() {
1441 let poly = unit_square();
1442 assert!(is_inside_polygon([0.5, 0.5], &poly));
1443 }
1444
1445 #[test]
1446 fn test_is_inside_polygon_outside() {
1447 let poly = unit_square();
1448 assert!(!is_inside_polygon([2.0, 2.0], &poly));
1449 }
1450
1451 #[test]
1452 fn test_is_inside_polygon_too_few_vertices() {
1453 let poly = vec![[0.0, 0.0], [1.0, 0.0]];
1454 assert!(!is_inside_polygon([0.5, 0.0], &poly));
1455 }
1456
1457 #[test]
1458 fn test_centerline_extraction_basic() {
1459 let mut pts = vec![];
1460 for i in 0..10 {
1461 let x = i as f64;
1462 pts.push([x, 1.0, 0.0]);
1463 pts.push([x, -1.0, 0.0]);
1464 pts.push([x, 0.0, 1.0]);
1465 pts.push([x, 0.0, -1.0]);
1466 }
1467 let cl = CenterlineExtraction::extract_centerline(&pts, 5);
1468 assert_eq!(cl.len(), 5);
1469 }
1470
1471 #[test]
1472 fn test_centerline_extraction_empty() {
1473 let cl = CenterlineExtraction::extract_centerline(&[], 5);
1474 assert!(cl.is_empty());
1475 }
1476
1477 #[test]
1478 fn test_centerline_extraction_zero_slices() {
1479 let pts = vec![[0.0, 0.0, 0.0]];
1480 let cl = CenterlineExtraction::extract_centerline(&pts, 0);
1481 assert!(cl.is_empty());
1482 }
1483
1484 #[test]
1485 fn test_prune_short_branches() {
1486 let pts = cube_boundary();
1487 let mut ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1488 let before = ma.branch_count();
1489 prune_short_branches(&mut ma, 1e12); assert!(ma.branch_count() <= before);
1491 }
1492
1493 #[test]
1494 fn test_prune_keeps_long_branches() {
1495 let pts = cube_boundary();
1496 let mut ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1497 prune_short_branches(&mut ma, 0.0); assert!(ma.branch_count() >= 1);
1499 }
1500
1501 #[test]
1502 fn test_euler_characteristic() {
1503 let pts = cube_boundary();
1504 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1505 let chi = euler_characteristic(&ma);
1506 assert_eq!(chi, 1);
1508 }
1509
1510 #[test]
1511 fn test_axis_aligned_slab_decompose() {
1512 let pts = cube_boundary();
1513 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1514 let slabs = AxisAlignedSlab::decompose(&ma, 3);
1515 assert_eq!(slabs.len(), 3);
1516 let total_pts: usize = slabs.iter().map(|s| s.point_indices.len()).sum();
1517 assert_eq!(total_pts, ma.points.len());
1518 }
1519
1520 #[test]
1521 fn test_axis_aligned_slab_zero_slabs() {
1522 let pts = cube_boundary();
1523 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1524 let slabs = AxisAlignedSlab::decompose(&ma, 0);
1525 assert!(slabs.is_empty());
1526 }
1527
1528 #[test]
1529 fn test_shape_diameter_positive() {
1530 let d = shape_diameter([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], 1.0);
1531 assert!((d - 2.0).abs() < 1e-10);
1532 }
1533
1534 #[test]
1535 fn test_shape_diameter_scales_with_radius() {
1536 let d1 = shape_diameter([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 1.0);
1537 let d2 = shape_diameter([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], 2.0);
1538 assert!((d2 - 2.0 * d1).abs() < 1e-10);
1539 }
1540
1541 fn make_square_image(size: usize) -> BinaryImage {
1544 let mut img = BinaryImage::new(size, size, false);
1545 let margin = size / 4;
1546 for r in margin..(size - margin) {
1547 for c in margin..(size - margin) {
1548 img.set(c, r, true);
1549 }
1550 }
1551 img
1552 }
1553
1554 #[test]
1555 fn test_binary_image_basic() {
1556 let img = BinaryImage::new(10, 10, false);
1557 assert_eq!(img.foreground_count(), 0);
1558 assert!(!img.get(0, 0));
1559 }
1560
1561 #[test]
1562 fn test_distance_transform_background() {
1563 let img = BinaryImage::new(10, 10, false);
1564 let dt = distance_transform(&img);
1565 assert!(dt.iter().all(|&v| v == 0.0));
1566 }
1567
1568 #[test]
1569 fn test_distance_transform_foreground() {
1570 let img = make_square_image(20);
1571 let dt = distance_transform(&img);
1572 let center = 10 * 20 + 10;
1574 assert!(dt[center] > 0.0, "Centre distance should be > 0");
1575 assert!((dt[0] - 0.0).abs() < 1e-10);
1577 }
1578
1579 #[test]
1580 fn test_distance_transform_exact_background() {
1581 let img = BinaryImage::new(10, 10, false);
1582 let dt = distance_transform_exact(&img);
1583 assert!(dt.iter().all(|&v| v == 0.0));
1584 }
1585
1586 #[test]
1587 fn test_distance_transform_exact_foreground() {
1588 let img = make_square_image(20);
1589 let dt = distance_transform_exact(&img);
1590 let center = 10 * 20 + 10;
1591 assert!(dt[center] > 0.0);
1592 }
1593
1594 #[test]
1597 fn test_zhang_suen_reduces_pixels() {
1598 let mut img = make_square_image(20);
1599 let before = img.foreground_count();
1600 zhang_suen_thinning(&mut img);
1601 let after = img.foreground_count();
1602 assert!(after < before, "Thinning should reduce pixel count");
1603 assert!(after > 0, "Thinning should leave a skeleton");
1604 }
1605
1606 #[test]
1607 fn test_zhang_suen_empty_image() {
1608 let mut img = BinaryImage::new(10, 10, false);
1609 zhang_suen_thinning(&mut img);
1610 assert_eq!(img.foreground_count(), 0);
1611 }
1612
1613 #[test]
1614 fn test_skeleton_pixels_from_thinned() {
1615 let mut img = make_square_image(20);
1616 zhang_suen_thinning(&mut img);
1617 let pixels = skeleton_pixels(&img);
1618 assert!(!pixels.is_empty());
1619 assert_eq!(pixels.len(), img.foreground_count());
1620 }
1621
1622 #[test]
1625 fn test_mat_has_positive_radii() {
1626 let img = make_square_image(20);
1627 let mat = medial_axis_transform(&img);
1628 assert!(!mat.is_empty(), "MAT should have points");
1629 for mp in &mat {
1630 assert!(mp.radius >= 0.0, "Radius should be non-negative");
1631 }
1632 }
1633
1634 #[test]
1635 fn test_reconstruct_from_mat() {
1636 let img = make_square_image(20);
1637 let mat = medial_axis_transform(&img);
1638 let recon = reconstruct_from_mat(&mat, 20, 20);
1639 assert!(
1640 recon.foreground_count() > 0,
1641 "Reconstructed image should not be empty"
1642 );
1643 }
1644
1645 #[test]
1648 fn test_branch_significance() {
1649 let pts = cube_boundary();
1650 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1651 let sig = branch_significance(&ma, 0);
1652 assert!(sig > 0.0, "Significance should be positive");
1653 }
1654
1655 #[test]
1656 fn test_prune_by_significance() {
1657 let pts = cube_boundary();
1658 let mut ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1659 prune_by_significance(&mut ma, 1e12); assert!(
1661 ma.branches.is_empty()
1662 || ma.branches.iter().all(|b| {
1663 let _ = b;
1664 true })
1666 );
1667 }
1668
1669 #[test]
1672 fn test_radius_function() {
1673 let pts = cube_boundary();
1674 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1675 let rf = radius_function(&ma, 0);
1676 assert!(!rf.is_empty());
1677 for w in rf.windows(2) {
1679 assert!(w[1].0 >= w[0].0, "Arc length should be non-decreasing");
1680 }
1681 }
1682
1683 #[test]
1684 fn test_average_and_max_radius() {
1685 let pts = cube_boundary();
1686 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1687 let avg = average_radius(&ma, 0);
1688 let mx = max_radius(&ma, 0);
1689 assert!(avg > 0.0);
1690 assert!(mx >= avg, "Max radius should be >= average");
1691 }
1692
1693 #[test]
1696 fn test_decompose_from_skeleton() {
1697 let pts = cube_boundary();
1698 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1699 let parts = decompose_from_skeleton(&ma);
1700 assert!(!parts.is_empty());
1701 let vol = total_decomposed_volume(&parts);
1702 assert!(vol > 0.0, "Total volume should be positive");
1703 }
1704
1705 #[test]
1708 fn test_curve_skeleton_from_tube() {
1709 let mut pts = vec![];
1710 for i in 0..20 {
1711 let x = i as f64 * 0.5;
1712 for &(dy, dz) in &[(0.5, 0.0), (-0.5, 0.0), (0.0, 0.5), (0.0, -0.5)] {
1713 pts.push([x, dy, dz]);
1714 }
1715 }
1716 let cs = CurveSkeleton::from_surface_points(&pts, 5);
1717 assert!(cs.node_count() > 0);
1718 assert!(cs.total_length() > 0.0);
1719 }
1720
1721 #[test]
1722 fn test_curve_skeleton_leaf_nodes() {
1723 let pts = cube_boundary();
1724 let cs = CurveSkeleton::from_surface_points(&pts, 3);
1725 let leaves = cs.leaf_nodes();
1726 assert!(leaves.len() >= 2 || cs.node_count() <= 2);
1728 }
1729
1730 #[test]
1731 fn test_curve_skeleton_prune() {
1732 let pts = cube_boundary();
1733 let mut cs = CurveSkeleton::from_surface_points(&pts, 3);
1734 let before = cs.edge_count();
1735 cs.prune_leaves(1e12); let after = cs.edge_count();
1737 assert!(after <= before);
1738 }
1739
1740 #[test]
1741 fn test_laplacian_smooth() {
1742 let points = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1743 let adj = vec![vec![1], vec![0, 2], vec![1]];
1744 let result = laplacian_smooth(&points, &adj, 0.5);
1745 assert!((result[1][0] - 1.0).abs() < 1e-10);
1747 }
1748
1749 #[test]
1752 fn test_voronoi_medial_axis_2d() {
1753 let pts = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1754 let result = voronoi_medial_axis_2d(&pts, [-0.5, -0.5, 1.5, 1.5], 0.0);
1755 assert!(!result.is_empty());
1757 }
1758
1759 #[test]
1762 fn test_hausdorff_self_zero() {
1763 let pts = cube_boundary();
1764 let ma = MedialAxis::from_points(&pts, SkeletonMethod::Voronoi);
1765 let d = hausdorff_distance(&ma, &ma);
1766 assert!(d < 1e-10, "Hausdorff to self should be 0, got {:.6}", d);
1767 }
1768
1769 #[test]
1770 fn test_hausdorff_different_shapes() {
1771 let pts1 = cube_boundary();
1772 let pts2: Vec<[f64; 3]> = pts1.iter().map(|p| [p[0] + 5.0, p[1], p[2]]).collect();
1773 let ma1 = MedialAxis::from_points(&pts1, SkeletonMethod::Voronoi);
1774 let ma2 = MedialAxis::from_points(&pts2, SkeletonMethod::Voronoi);
1775 let d = hausdorff_distance(&ma1, &ma2);
1776 assert!(d > 1.0, "Shifted shapes should have Hausdorff > 1");
1777 }
1778}