Skip to main content

polyscope_structures/volume_mesh/
slice_geometry.rs

1//! Geometry generation for slicing tetrahedra and hexahedra with a plane.
2//!
3//! This module provides algorithms for computing the intersection of volume mesh cells
4//! with slice planes, producing polygon geometry for rendering cross-section caps.
5
6use glam::Vec3;
7
8/// Result of slicing a volumetric cell with a plane.
9/// Can produce 0, 3, 4, 5, or 6 intersection points forming a convex polygon.
10#[derive(Debug, Clone)]
11pub struct CellSliceResult {
12    /// Intersection points (vertices of the slice polygon)
13    pub vertices: Vec<Vec3>,
14    /// Interpolation data for vertex attributes.
15    /// Each entry is (`vert_a`, `vert_b`, t) where result = lerp(a, b, t)
16    pub interpolation: Vec<(u32, u32, f32)>,
17}
18
19impl CellSliceResult {
20    /// Creates an empty slice result (no intersection).
21    #[must_use]
22    pub fn empty() -> Self {
23        Self {
24            vertices: Vec::new(),
25            interpolation: Vec::new(),
26        }
27    }
28
29    /// Returns true if the slice produced a valid polygon.
30    #[must_use]
31    pub fn has_intersection(&self) -> bool {
32        self.vertices.len() >= 3
33    }
34}
35
36/// Computes the intersection of a tetrahedron with a slice plane.
37///
38/// Returns the intersection polygon vertices and interpolation data
39/// for computing attribute values at intersection points.
40///
41/// # Arguments
42/// * `v0`, `v1`, `v2`, `v3` - Tetrahedron vertex positions
43/// * `plane_origin` - A point on the plane
44/// * `plane_normal` - The plane normal (points toward kept geometry)
45///
46/// # Returns
47/// A `CellSliceResult` containing 0, 3, or 4 vertices depending on the intersection.
48#[must_use]
49pub fn slice_tet(
50    v0: Vec3,
51    v1: Vec3,
52    v2: Vec3,
53    v3: Vec3,
54    plane_origin: Vec3,
55    plane_normal: Vec3,
56) -> CellSliceResult {
57    let verts = [v0, v1, v2, v3];
58
59    // Compute signed distances from each vertex to the plane
60    let d: [f32; 4] = std::array::from_fn(|i| (verts[i] - plane_origin).dot(plane_normal));
61
62    // Find edge intersections where sign changes
63    let mut intersections = Vec::new();
64    let mut interp_data = Vec::new();
65
66    // All 6 edges of a tetrahedron
67    let edges = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
68
69    for &(i, j) in &edges {
70        // Check if the edge crosses the plane (different signs)
71        if d[i] * d[j] < 0.0 {
72            // Compute interpolation parameter
73            let t = d[i] / (d[i] - d[j]);
74            let point = verts[i].lerp(verts[j], t);
75            intersections.push(point);
76            interp_data.push((i as u32, j as u32, t));
77        }
78    }
79
80    // Order vertices to form valid polygon (convex hull in 2D projection)
81    if intersections.len() >= 3 {
82        order_polygon_vertices(&mut intersections, &mut interp_data, plane_normal);
83    }
84
85    CellSliceResult {
86        vertices: intersections,
87        interpolation: interp_data,
88    }
89}
90
91/// Slice a hexahedron by decomposing into 5 tetrahedra.
92///
93/// Hexahedra are sliced by treating them as 5 tetrahedra (using the standard
94/// symmetric decomposition), then merging the resulting polygons.
95///
96/// # Arguments
97/// * `vertices` - The 8 vertices of the hexahedron in standard ordering
98/// * `plane_origin` - A point on the plane
99/// * `plane_normal` - The plane normal (points toward kept geometry)
100///
101/// # Returns
102/// A `CellSliceResult` containing 0, 3-6 vertices depending on the intersection.
103#[must_use]
104pub fn slice_hex(vertices: [Vec3; 8], plane_origin: Vec3, plane_normal: Vec3) -> CellSliceResult {
105    // Standard decomposition of a hex into 5 tets
106    // This decomposition is symmetric and works for any hex orientation
107    let tet_indices = [
108        [0, 1, 3, 4],
109        [1, 2, 3, 6],
110        [1, 4, 5, 6],
111        [3, 4, 6, 7],
112        [1, 3, 4, 6], // Central tet connecting all others
113    ];
114
115    let mut all_vertices = Vec::new();
116    let mut all_interp = Vec::new();
117
118    for tet in &tet_indices {
119        let result = slice_tet(
120            vertices[tet[0]],
121            vertices[tet[1]],
122            vertices[tet[2]],
123            vertices[tet[3]],
124            plane_origin,
125            plane_normal,
126        );
127
128        // Remap interpolation indices from local tet indices to hex indices
129        for (local_a, local_b, t) in result.interpolation {
130            let hex_a = tet[local_a as usize] as u32;
131            let hex_b = tet[local_b as usize] as u32;
132            all_interp.push((hex_a, hex_b, t));
133        }
134        all_vertices.extend(result.vertices);
135    }
136
137    // Merge and deduplicate vertices that are close together
138    merge_slice_vertices(&mut all_vertices, &mut all_interp);
139
140    // Order vertices to form valid polygon
141    if all_vertices.len() >= 3 {
142        order_polygon_vertices(&mut all_vertices, &mut all_interp, plane_normal);
143    }
144
145    CellSliceResult {
146        vertices: all_vertices,
147        interpolation: all_interp,
148    }
149}
150
151/// Orders polygon vertices in counter-clockwise order around the centroid.
152///
153/// This ensures the resulting polygon is suitable for rendering with correct face winding.
154fn order_polygon_vertices(
155    vertices: &mut Vec<Vec3>,
156    interp: &mut Vec<(u32, u32, f32)>,
157    normal: Vec3,
158) {
159    if vertices.len() < 3 {
160        return;
161    }
162
163    // Compute centroid
164    let centroid: Vec3 = vertices.iter().copied().sum::<Vec3>() / vertices.len() as f32;
165
166    // Create a reference direction perpendicular to the normal
167    let ref_dir = if let Some(first) = vertices.first() {
168        (*first - centroid).normalize()
169    } else {
170        return;
171    };
172
173    // Sort by angle around centroid using signed angle
174    let mut indices: Vec<usize> = (0..vertices.len()).collect();
175
176    indices.sort_by(|&a, &b| {
177        let va = (vertices[a] - centroid).normalize();
178        let vb = (vertices[b] - centroid).normalize();
179
180        // Compute signed angle relative to ref_dir around normal axis
181        // angle = atan2(cross.dot(normal), dot(ref_dir, v))
182        let cross_a = ref_dir.cross(va);
183        let cross_b = ref_dir.cross(vb);
184
185        let angle_a = cross_a.dot(normal).atan2(ref_dir.dot(va));
186        let angle_b = cross_b.dot(normal).atan2(ref_dir.dot(vb));
187
188        angle_a
189            .partial_cmp(&angle_b)
190            .unwrap_or(std::cmp::Ordering::Equal)
191    });
192
193    // Reorder both vertices and interpolation data
194    let sorted_verts: Vec<Vec3> = indices.iter().map(|&i| vertices[i]).collect();
195    let sorted_interp: Vec<_> = indices.iter().map(|&i| interp[i]).collect();
196
197    *vertices = sorted_verts;
198    *interp = sorted_interp;
199}
200
201/// Merges vertices that are very close together (within epsilon).
202///
203/// This is needed after hex decomposition where multiple tets may produce
204/// nearly identical intersection points.
205fn merge_slice_vertices(vertices: &mut Vec<Vec3>, interp: &mut Vec<(u32, u32, f32)>) {
206    // Use a larger epsilon for merging since floating point errors can accumulate
207    const EPSILON: f32 = 1e-4;
208
209    if vertices.len() <= 1 {
210        return;
211    }
212
213    let mut merged_verts = Vec::new();
214    let mut merged_interp = Vec::new();
215
216    for i in 0..vertices.len() {
217        let v = vertices[i];
218        let mut is_duplicate = false;
219
220        for merged_v in &merged_verts {
221            let diff: Vec3 = v - *merged_v;
222            if diff.length_squared() < EPSILON * EPSILON {
223                is_duplicate = true;
224                break;
225            }
226        }
227
228        if !is_duplicate {
229            merged_verts.push(v);
230            merged_interp.push(interp[i]);
231        }
232    }
233
234    *vertices = merged_verts;
235    *interp = merged_interp;
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_slice_tet_no_intersection() {
244        // Tet entirely above the plane (all positive distances)
245        let result = slice_tet(
246            Vec3::new(0.0, 1.0, 0.0),
247            Vec3::new(1.0, 1.0, 0.0),
248            Vec3::new(0.5, 1.0, 1.0),
249            Vec3::new(0.5, 2.0, 0.5),
250            Vec3::ZERO,
251            Vec3::Y,
252        );
253        assert!(result.vertices.is_empty());
254        assert!(!result.has_intersection());
255    }
256
257    #[test]
258    fn test_slice_tet_triangle() {
259        // Plane cuts one vertex off (one vertex below, three above)
260        // This should produce 3 intersection points (triangle)
261        let result = slice_tet(
262            Vec3::new(0.0, -1.0, 0.0), // Below plane
263            Vec3::new(1.0, 1.0, 0.0),  // Above plane
264            Vec3::new(-1.0, 1.0, 0.0), // Above plane
265            Vec3::new(0.0, 1.0, 1.0),  // Above plane
266            Vec3::ZERO,
267            Vec3::Y,
268        );
269        assert_eq!(result.vertices.len(), 3);
270        assert!(result.has_intersection());
271
272        // Verify interpolation data
273        assert_eq!(result.interpolation.len(), 3);
274        for (_, _, t) in &result.interpolation {
275            assert!(*t >= 0.0 && *t <= 1.0);
276        }
277    }
278
279    #[test]
280    fn test_slice_tet_quad() {
281        // Plane cuts through middle (two vertices on each side)
282        // This should produce 4 intersection points (quad)
283        let result = slice_tet(
284            Vec3::new(0.0, -1.0, 0.0), // Below plane
285            Vec3::new(1.0, -1.0, 0.0), // Below plane
286            Vec3::new(0.5, 1.0, 0.0),  // Above plane
287            Vec3::new(0.5, 1.0, 1.0),  // Above plane
288            Vec3::ZERO,
289            Vec3::Y,
290        );
291        assert_eq!(result.vertices.len(), 4);
292        assert!(result.has_intersection());
293    }
294
295    #[test]
296    fn test_slice_tet_interpolation_values() {
297        // Test that interpolation produces correct positions
298        let v0 = Vec3::new(0.0, -1.0, 0.0);
299        let v1 = Vec3::new(0.0, 1.0, 0.0);
300        let v2 = Vec3::new(1.0, 0.0, 0.0);
301        let v3 = Vec3::new(0.0, 0.0, 1.0);
302
303        let result = slice_tet(v0, v1, v2, v3, Vec3::ZERO, Vec3::Y);
304
305        // The intersection with edge (0,1) should be at y=0
306        // v0.y = -1, v1.y = 1, so t = 0.5 gives y = 0
307        for (i, (a, b, t)) in result.interpolation.iter().enumerate() {
308            let verts = [v0, v1, v2, v3];
309            let computed = verts[*a as usize].lerp(verts[*b as usize], *t);
310            let diff = (computed - result.vertices[i]).length();
311            assert!(diff < 1e-5, "Interpolation mismatch at index {}", i);
312        }
313    }
314
315    #[test]
316    fn test_slice_hex_no_intersection() {
317        // Hex entirely above the plane
318        let vertices = [
319            Vec3::new(0.0, 1.0, 0.0),
320            Vec3::new(1.0, 1.0, 0.0),
321            Vec3::new(1.0, 1.0, 1.0),
322            Vec3::new(0.0, 1.0, 1.0),
323            Vec3::new(0.0, 2.0, 0.0),
324            Vec3::new(1.0, 2.0, 0.0),
325            Vec3::new(1.0, 2.0, 1.0),
326            Vec3::new(0.0, 2.0, 1.0),
327        ];
328        let result = slice_hex(vertices, Vec3::ZERO, Vec3::Y);
329        assert!(result.vertices.is_empty());
330    }
331
332    #[test]
333    fn test_slice_hex_quad() {
334        // Unit cube centered at origin, sliced by XZ plane
335        // Should produce a polygon intersection (4 unique vertices after merge)
336        let vertices = [
337            Vec3::new(-0.5, -0.5, -0.5),
338            Vec3::new(0.5, -0.5, -0.5),
339            Vec3::new(0.5, -0.5, 0.5),
340            Vec3::new(-0.5, -0.5, 0.5),
341            Vec3::new(-0.5, 0.5, -0.5),
342            Vec3::new(0.5, 0.5, -0.5),
343            Vec3::new(0.5, 0.5, 0.5),
344            Vec3::new(-0.5, 0.5, 0.5),
345        ];
346        let result = slice_hex(vertices, Vec3::ZERO, Vec3::Y);
347
348        // Should have at least 3 vertices forming a valid polygon
349        assert!(result.has_intersection());
350        assert!(
351            result.vertices.len() >= 3,
352            "Expected at least 3 vertices, got {}",
353            result.vertices.len()
354        );
355
356        // All vertices should be at y=0
357        for v in &result.vertices {
358            assert!(v.y.abs() < 1e-5, "Vertex y={} should be 0", v.y);
359        }
360
361        // The vertices should cover the square corners approximately
362        // Check that we have vertices near each corner of the unit square at y=0
363        let expected_corners = [
364            Vec3::new(-0.5, 0.0, -0.5),
365            Vec3::new(0.5, 0.0, -0.5),
366            Vec3::new(0.5, 0.0, 0.5),
367            Vec3::new(-0.5, 0.0, 0.5),
368        ];
369
370        for corner in &expected_corners {
371            let has_near = result
372                .vertices
373                .iter()
374                .any(|v| (*v - *corner).length() < 0.1);
375            assert!(has_near, "Expected vertex near corner {:?}", corner);
376        }
377    }
378
379    #[test]
380    fn test_polygon_ordering() {
381        // Verify vertices are ordered correctly (counter-clockwise)
382        let v0 = Vec3::new(0.0, -1.0, 0.0);
383        let v1 = Vec3::new(1.0, 1.0, 0.0);
384        let v2 = Vec3::new(-1.0, 1.0, 0.0);
385        let v3 = Vec3::new(0.0, 1.0, 1.0);
386
387        let result = slice_tet(v0, v1, v2, v3, Vec3::ZERO, Vec3::Y);
388
389        // Check that triangle has correct winding (normal should point in +Y direction)
390        if result.vertices.len() >= 3 {
391            let e1 = result.vertices[1] - result.vertices[0];
392            let e2 = result.vertices[2] - result.vertices[0];
393            let computed_normal = e1.cross(e2).normalize();
394
395            // The computed normal should roughly align with the plane normal
396            let dot = computed_normal.dot(Vec3::Y);
397            assert!(
398                dot.abs() > 0.9,
399                "Polygon normal should align with plane normal"
400            );
401        }
402    }
403}