Skip to main content

oxihuman_mesh/
mesh_slice_stack.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Slice mesh at multiple Z heights and return 2D contours.
6
7/// A 2D contour from a Z-slice.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct SliceContour {
11    pub z: f32,
12    pub points: Vec<[f32; 2]>,
13}
14
15/// Result of stack slicing.
16#[allow(dead_code)]
17pub struct SliceStackResult {
18    pub contours: Vec<SliceContour>,
19}
20
21/// Intersect a triangle edge with a Z-plane. Returns None if no intersection.
22fn edge_z_intersect(p0: [f32; 3], p1: [f32; 3], z: f32) -> Option<[f32; 2]> {
23    let dz = p1[2] - p0[2];
24    if dz.abs() < 1e-9 {
25        return None;
26    }
27    let t = (z - p0[2]) / dz;
28    if !(0.0..=1.0).contains(&t) {
29        return None;
30    }
31    let x = p0[0] + t * (p1[0] - p0[0]);
32    let y = p0[1] + t * (p1[1] - p0[1]);
33    Some([x, y])
34}
35
36/// Slice the mesh at a single Z height, returning intersection points.
37#[allow(dead_code)]
38pub fn slice_at_z(positions: &[[f32; 3]], indices: &[u32], z: f32) -> Vec<[f32; 2]> {
39    let mut pts = Vec::new();
40    let n_tri = indices.len() / 3;
41    for t in 0..n_tri {
42        let p0 = positions[indices[t * 3] as usize];
43        let p1 = positions[indices[t * 3 + 1] as usize];
44        let p2 = positions[indices[t * 3 + 2] as usize];
45        for &(a, b) in &[(p0, p1), (p1, p2), (p2, p0)] {
46            if let Some(pt) = edge_z_intersect(a, b, z) {
47                pts.push(pt);
48            }
49        }
50    }
51    pts
52}
53
54/// Slice the mesh at multiple Z heights.
55#[allow(dead_code)]
56pub fn slice_stack(positions: &[[f32; 3]], indices: &[u32], z_heights: &[f32]) -> SliceStackResult {
57    let contours = z_heights
58        .iter()
59        .map(|&z| SliceContour {
60            z,
61            points: slice_at_z(positions, indices, z),
62        })
63        .collect();
64    SliceStackResult { contours }
65}
66
67/// Generate evenly-spaced Z heights between min and max.
68#[allow(dead_code)]
69pub fn uniform_z_heights(z_min: f32, z_max: f32, count: usize) -> Vec<f32> {
70    if count == 0 {
71        return vec![];
72    }
73    if count == 1 {
74        return vec![(z_min + z_max) * 0.5];
75    }
76    let step = (z_max - z_min) / (count - 1) as f32;
77    (0..count).map(|i| z_min + i as f32 * step).collect()
78}
79
80/// Total number of intersection points across all contours.
81#[allow(dead_code)]
82pub fn total_contour_points(result: &SliceStackResult) -> usize {
83    result.contours.iter().map(|c| c.points.len()).sum()
84}
85
86/// Count non-empty contours.
87#[allow(dead_code)]
88pub fn non_empty_contour_count(result: &SliceStackResult) -> usize {
89    result
90        .contours
91        .iter()
92        .filter(|c| !c.points.is_empty())
93        .count()
94}
95
96/// Bounding box of all intersection points in a contour.
97#[allow(dead_code)]
98pub fn contour_bounds(contour: &SliceContour) -> ([f32; 2], [f32; 2]) {
99    if contour.points.is_empty() {
100        return ([0.0; 2], [0.0; 2]);
101    }
102    let mut mn = contour.points[0];
103    let mut mx = contour.points[0];
104    for p in &contour.points {
105        mn[0] = mn[0].min(p[0]);
106        mn[1] = mn[1].min(p[1]);
107        mx[0] = mx[0].max(p[0]);
108        mx[1] = mx[1].max(p[1]);
109    }
110    (mn, mx)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    fn unit_cube_mesh() -> (Vec<[f32; 3]>, Vec<u32>) {
118        let positions = vec![
119            [0.0, 0.0, 0.0],
120            [1.0, 0.0, 0.0],
121            [1.0, 1.0, 0.0],
122            [0.0, 1.0, 0.0],
123            [0.0, 0.0, 1.0],
124            [1.0, 0.0, 1.0],
125            [1.0, 1.0, 1.0],
126            [0.0, 1.0, 1.0],
127        ];
128        let indices: Vec<u32> = vec![
129            0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 5, 1, 0, 4, 5, 2, 6, 3, 3, 6, 7, 1, 6, 2, 1, 5,
130            6, 0, 3, 7, 0, 7, 4,
131        ];
132        (positions, indices)
133    }
134
135    #[test]
136    fn slice_middle_has_points() {
137        let (pos, idx) = unit_cube_mesh();
138        let pts = slice_at_z(&pos, &idx, 0.5);
139        assert!(!pts.is_empty());
140    }
141
142    #[test]
143    fn slice_above_has_no_points() {
144        let (pos, idx) = unit_cube_mesh();
145        let pts = slice_at_z(&pos, &idx, 2.0);
146        assert!(pts.is_empty());
147    }
148
149    #[test]
150    fn slice_stack_contour_count() {
151        let (pos, idx) = unit_cube_mesh();
152        let heights = uniform_z_heights(0.1, 0.9, 5);
153        let result = slice_stack(&pos, &idx, &heights);
154        assert_eq!(result.contours.len(), 5);
155    }
156
157    #[test]
158    fn total_contour_points_sum() {
159        let (pos, idx) = unit_cube_mesh();
160        let heights = uniform_z_heights(0.1, 0.9, 3);
161        let result = slice_stack(&pos, &idx, &heights);
162        let total = total_contour_points(&result);
163        assert!(total > 0);
164    }
165
166    #[test]
167    fn non_empty_contour_count_cube() {
168        let (pos, idx) = unit_cube_mesh();
169        let heights = vec![0.5, 1.5];
170        let result = slice_stack(&pos, &idx, &heights);
171        assert_eq!(non_empty_contour_count(&result), 1);
172    }
173
174    #[test]
175    fn uniform_z_heights_count() {
176        let h = uniform_z_heights(0.0, 1.0, 5);
177        assert_eq!(h.len(), 5);
178        assert!((h[0] - 0.0).abs() < 1e-5);
179        assert!((h[4] - 1.0).abs() < 1e-5);
180    }
181
182    #[test]
183    fn uniform_z_heights_single() {
184        let h = uniform_z_heights(0.0, 1.0, 1);
185        assert_eq!(h.len(), 1);
186        assert!((h[0] - 0.5).abs() < 1e-5);
187    }
188
189    #[test]
190    fn contour_bounds_nonempty() {
191        let contour = SliceContour {
192            z: 0.5,
193            points: vec![[0.0, 0.0], [1.0, 0.5], [0.5, 1.0]],
194        };
195        let (mn, mx) = contour_bounds(&contour);
196        assert!((mn[0] - 0.0).abs() < 1e-5);
197        assert!((mx[0] - 1.0).abs() < 1e-5);
198    }
199
200    #[test]
201    fn edge_z_intersect_midpoint() {
202        let r = edge_z_intersect([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], 0.5);
203        assert!(r.is_some());
204        let pt = r.expect("should succeed");
205        assert!((pt[0] - 0.0).abs() < 1e-5);
206    }
207
208    #[test]
209    fn edge_z_intersect_none_outside() {
210        let r = edge_z_intersect([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], 2.0);
211        assert!(r.is_none());
212    }
213}