phys_geom/volume.rs
1// Copyright (C) 2020-2025 phys-geom authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Volume computation and mesh processing utilities.
16//!
17//! This module provides functionality for computing volumes of geometric shapes and
18//! processing triangle meshes. It includes both individual shape volume calculations
19//! and mesh-based operations like signed volume computation and bounding box calculation.
20//!
21//! # Volume Computation
22//!
23//! The module provides a [`ComputeVolume`] trait that is implemented for all
24//! supported geometric shapes. This allows for uniform volume calculation across
25//! different shape types.
26//!
27//! # Mesh Processing
28//!
29//! Additional utilities are provided for working with triangle meshes:
30//! - Signed volume computation for watertight meshes
31//! - Axis-aligned bounding box computation
32//!
33//! # Examples
34//!
35//! ```rust
36//! use phys_geom::shape::{Sphere, Cuboid};
37//! use phys_geom::volume::ComputeVolume;
38//!
39//! // Compute volume of individual shapes
40//! let sphere = Sphere::new(1.0);
41//! let sphere_volume = sphere.compute_volume(); // 4π/3 ≈ 4.1888
42//!
43//! let cuboid = Cuboid::new([2.0, 3.0, 1.0]);
44//! let cuboid_volume = cuboid.compute_volume(); // 6.0
45//! ```
46//!
47//! # Mathematical Accuracy
48//!
49//! All volume calculations use mathematically accurate formulas:
50//! - Sphere: V = (4/3)πr³
51//! - Capsule: V = πr²(h + 4r/3) where h is the cylindrical height
52//! - Cuboid: V = l × w × h
53//! - Cylinder: V = πr²h
54//!
55//! The calculations are optimized for performance while maintaining numerical precision.
56
57use crate::math::{Real, PI};
58use crate::shape::{Capsule, Cuboid, Cylinder, InfinitePlane, Sphere, Tetrahedron};
59use crate::Aabb3;
60
61/// Trait for computing the volume of geometric shapes.
62///
63/// This trait provides a uniform interface for volume calculation across different
64/// geometric shape types. All shapes implement this trait, allowing for generic
65/// volume computation algorithms.
66///
67/// # Examples
68///
69/// ```rust
70/// use phys_geom::shape::{Sphere, Cuboid};
71/// use phys_geom::volume::ComputeVolume;
72/// use phys_geom::Real;
73///
74/// fn calculate_total_volume<T: ComputeVolume>(shapes: &[T]) -> Real {
75/// shapes.iter().map(|s| s.compute_volume()).sum()
76/// }
77///
78/// // Works with homogeneous collections
79/// let spheres = vec![Sphere::new(1.0), Sphere::new(2.0)];
80/// let sphere_total = calculate_total_volume(&spheres);
81///
82/// let cuboids = vec![Cuboid::new([1.0, 1.0, 1.0]), Cuboid::new([2.0, 2.0, 2.0])];
83/// let cuboid_total = calculate_total_volume(&cuboids);
84/// ```
85///
86/// # Implementation Notes
87///
88/// Implementations should use mathematically accurate formulas and be optimized
89/// for performance. Volume calculations should be deterministic and return
90/// positive values for valid shapes.
91pub trait ComputeVolume {
92 /// Computes the volume of this shape.
93 ///
94 /// # Returns
95 ///
96 /// The volume as a `Real` (f32 or f64 depending on features).
97 /// Always returns a non-negative value for valid shapes.
98 ///
99 /// # Examples
100 ///
101 /// ```rust
102 /// use phys_geom::shape::Sphere;
103 /// use phys_geom::volume::ComputeVolume;
104 ///
105 /// let sphere = Sphere::new(2.0);
106 /// let volume = sphere.compute_volume();
107 /// assert!(volume > 0.0);
108 /// ```
109 fn compute_volume(&self) -> Real;
110}
111
112impl ComputeVolume for Sphere {
113 #[inline]
114 fn compute_volume(&self) -> Real {
115 const A: Real = 4.0 * PI / 3.0;
116 let r = self.radius();
117 A * r * r * r
118 }
119}
120
121impl ComputeVolume for Capsule {
122 #[inline]
123 fn compute_volume(&self) -> Real {
124 const A: Real = 4.0 / 3.0;
125 let r = self.radius();
126 let h = self.height();
127 PI * r * r * (h + A * r)
128 }
129}
130
131impl ComputeVolume for Cuboid {
132 #[inline]
133 fn compute_volume(&self) -> Real {
134 let half_length = self.half_length();
135 half_length[0] * half_length[1] * half_length[2] * 8.0
136 }
137}
138
139impl ComputeVolume for Cylinder {
140 #[inline]
141 fn compute_volume(&self) -> Real {
142 let r = self.radius();
143 let half_v = PI * r * r * self.half_height();
144 half_v + half_v
145 }
146}
147
148impl ComputeVolume for InfinitePlane {
149 #[inline]
150 fn compute_volume(&self) -> Real {
151 0.0
152 }
153}
154
155/// Computes the signed volume of a triangle mesh using the divergence theorem.
156///
157/// This function calculates the signed volume of a closed, watertight triangle mesh
158/// by summing the signed volumes of tetrahedra formed by each triangle face and the origin.
159/// The sign of the volume depends on the winding order of the triangle vertices.
160///
161/// # Mathematical Background
162///
163/// The signed volume is computed using the formula:
164/// V = Σ(V_tetrahedron) for each triangle, where each tetrahedron is formed
165/// by the origin and the triangle vertices.
166///
167/// # Vertex Ordering
168///
169/// - **Counter-clockwise** (when viewed from outside): Positive volume
170/// - **Clockwise** (when viewed from outside): Negative volume
171///
172/// The vertex ordering should be consistent across all triangles for accurate results.
173///
174/// # Use Cases
175///
176/// - **Mesh validation**: Checking if a mesh is watertight (volume should be consistent)
177/// - **Physics simulations**: Computing mass properties of complex objects
178/// - **3D modeling**: Calculating volumes of imported meshes
179/// - **Computational geometry**: Determining inside/outside relationships
180///
181/// # Arguments
182///
183/// * `vertices` - An iterator over triangle vertices, yielding 3 vertices per triangle
184///
185/// # Returns
186///
187/// The signed volume as a `Real` value.
188///
189/// # Panics
190///
191/// Panics if the number of vertices is not a multiple of 3, indicating malformed
192/// triangle data.
193///
194/// # Examples
195///
196/// ```rust
197/// use phys_geom::volume::triangle_mesh_signed;
198///
199/// // A simple tetrahedron represented as 4 triangles
200/// let vertices = vec![
201/// // Triangle 1
202/// [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0],
203/// // Triangle 2
204/// [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0],
205/// // ... more triangles
206/// ];
207///
208/// let volume = triangle_mesh_signed(vertices.into_iter());
209/// ```
210///
211/// # Performance
212///
213/// The algorithm processes each triangle exactly once, making it O(n) where n is
214/// the number of triangles. The computation is efficient and suitable for large meshes.
215#[inline]
216pub fn triangle_mesh_signed(mut vertices: impl Iterator<Item = [Real; 3]>) -> Real {
217 let mut volume = 0.0;
218 while let Some(v0) = vertices.next() {
219 let v1 = vertices
220 .next()
221 .expect("The vertices should be a multiple of 3.");
222 let v2 = vertices
223 .next()
224 .expect("The vertices should be a multiple of 3.");
225 let dv = Tetrahedron {
226 a: [0.0, 0.0, 0.0],
227 b: v0,
228 c: v1,
229 d: v2,
230 }
231 .signed_volume();
232 volume += dv;
233 }
234 volume
235}
236
237/// Computes the axis-aligned bounding box (AABB) of a mesh or point cloud.
238///
239/// This function calculates the smallest axis-aligned bounding box that contains
240/// all the provided vertices. It's useful for:
241/// - Quick collision detection broad-phase
242/// - Spatial partitioning and culling
243/// - Mesh visualization and processing
244/// - Bounding volume hierarchy construction
245///
246/// # Algorithm
247///
248/// The function iterates through all vertices, tracking the minimum and maximum
249/// coordinate values along each axis. The resulting AABB is defined by these
250/// extreme points.
251///
252/// # Arguments
253///
254/// * `vertices` - An iterator over 3D points [x, y, z]
255///
256/// # Returns
257///
258/// An [`Aabb3`] that contains all input vertices.
259///
260/// # Special Cases
261///
262/// - **Empty iterator**: Returns an AABB with MAX/MIN values (anti-infinite)
263/// - **Single point**: Returns a zero-volume AABB at that point
264/// - **Coplanar points**: Returns a flat AABB with zero volume in one or more dimensions
265///
266/// # Examples
267///
268/// ```rust
269/// use phys_geom::volume::compute_mesh_bound;
270///
271/// let vertices = vec![
272/// [0.0, 0.0, 0.0],
273/// [1.0, 0.0, 0.0],
274/// [0.5, 1.0, 0.0],
275/// [0.5, 0.5, 1.0],
276/// ];
277///
278/// let aabb = compute_mesh_bound(vertices.into_iter());
279///
280/// // The AABB should contain all points
281/// assert_eq!(aabb.min(), [0.0, 0.0, 0.0].into());
282/// assert_eq!(aabb.max(), [1.0, 1.0, 1.0].into());
283/// ```
284///
285/// # Performance
286///
287/// The algorithm runs in O(n) time where n is the number of vertices, with O(1)
288/// additional memory usage. It processes each vertex exactly once.
289///
290/// # Numerical Considerations
291///
292/// The function uses the floating-point min/max operations directly. For very
293/// large or very small coordinate values, consider the precision limitations
294/// of the underlying `Real` type.
295pub fn compute_mesh_bound(vertices: impl Iterator<Item = [Real; 3]>) -> Aabb3 {
296 let mut min = [Real::MAX; 3];
297 let mut max = [Real::MIN; 3];
298 for point in vertices {
299 min = [
300 min[0].min(point[0]),
301 min[1].min(point[1]),
302 min[2].min(point[2]),
303 ];
304 max = [
305 max[0].max(point[0]),
306 max[1].max(point[1]),
307 max[2].max(point[2]),
308 ];
309 }
310 Aabb3::new_unchecked(min, max)
311}
312
313#[cfg(test)]
314mod tests {
315 use crate::shape::{InfinitePlane, Tetrahedron};
316 use crate::volume::ComputeVolume;
317
318 #[test]
319 fn test_infinite_plane() {
320 let infinite_plane = InfinitePlane::default();
321 assert_eq!(infinite_plane.compute_volume(), 0.0);
322 }
323
324 #[test]
325 fn test_triangle_mesh() {
326 let tet = [
327 [1.0, 0.0, 0.0],
328 [2.0, 0.0, 0.0],
329 [1.0, 1.0, 0.0],
330 [1.0, 0.0, 1.0],
331 ];
332 let target_volume = Tetrahedron {
333 a: tet[0],
334 b: tet[1],
335 c: tet[2],
336 d: tet[3],
337 }
338 .signed_volume();
339 let indices = [0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3];
340
341 let triangle_iter = indices.iter().map(|&i| tet[i]);
342
343 let mesh_volume = crate::volume::triangle_mesh_signed(triangle_iter);
344 assert_eq!(target_volume, mesh_volume);
345 assert_eq!(mesh_volume, 1.0 / 6.0);
346 }
347}