Skip to content

Commit c0d480d

Browse files
authored
Merge pull request #1497 from compas-dev/copilot/add-volume-for-mesh
Add volume() method to Mesh class with optional performance parameters
2 parents 8221339 + 84dbabb commit c0d480d

File tree

3 files changed

+119
-0
lines changed

3 files changed

+119
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
* Added `compas_rhino.install_with_pip` with corresponding command line utility `install_in_rhino`.
1313
* Added support for `.stp` file extension in addition to `.step` for `RhinoBrep.from_step()` and `RhinoBrep.to_step()` methods.
14+
* Added `volume()` method to `compas.datastructures.Mesh` for computing the volume of closed meshes using signed volume of triangles.
1415

1516
### Changed
1617

src/compas/datastructures/mesh/mesh.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from compas.geometry import distance_line_line
4343
from compas.geometry import distance_point_plane
4444
from compas.geometry import distance_point_point
45+
from compas.geometry import dot_vectors
4546
from compas.geometry import length_vector
4647
from compas.geometry import midpoint_line
4748
from compas.geometry import normal_polygon
@@ -3882,6 +3883,75 @@ def area(self):
38823883
"""
38833884
return sum(self.face_area(fkey) for fkey in self.faces())
38843885

3886+
def volume(self, copy=True, unify_cycles=True):
3887+
"""Calculate the volume of the mesh.
3888+
3889+
Parameters
3890+
----------
3891+
copy : bool, optional
3892+
If True, a copy of the mesh is made before computation to avoid modifying the original.
3893+
Default is True.
3894+
unify_cycles : bool, optional
3895+
If True, face cycles are unified to ensure consistent orientation.
3896+
Default is True.
3897+
3898+
Returns
3899+
-------
3900+
float | None
3901+
The volume of the mesh if the mesh is closed, None otherwise.
3902+
3903+
Notes
3904+
-----
3905+
The volume is computed using the signed volume of tetrahedra formed by each
3906+
triangulated face and the origin. This method works for both convex and
3907+
non-convex meshes, as long as they are closed and properly oriented.
3908+
3909+
The volume is only meaningful for closed meshes. For open meshes, this method
3910+
returns None.
3911+
3912+
When faces are non-convex, the triangulation might not be correct, since it uses
3913+
the centroid of the face. For accurate results with non-convex faces, consider
3914+
using a mesh with triangulated faces.
3915+
3916+
By default, the mesh is copied internally and face cycles are unified to ensure
3917+
correct orientation before computing the volume. These operations can be disabled
3918+
by setting ``copy=False`` and ``unify_cycles=False`` for performance in cases where
3919+
the mesh is already correctly oriented or when the original mesh can be modified.
3920+
3921+
Examples
3922+
--------
3923+
>>> from compas.datastructures import Mesh
3924+
>>> mesh = Mesh.from_polyhedron(6) # Create a cube
3925+
>>> volume = mesh.volume()
3926+
>>> volume is not None
3927+
True
3928+
3929+
"""
3930+
if not self.is_closed():
3931+
return None
3932+
3933+
# Make a copy to avoid modifying the original mesh
3934+
mesh_to_use = self.copy() if copy else self
3935+
3936+
# Unify cycles to ensure consistent face orientation
3937+
if unify_cycles:
3938+
mesh_to_use.unify_cycles()
3939+
3940+
# Use built-in triangulation to get triangulated faces
3941+
vertices, faces = mesh_to_use.to_vertices_and_faces(triangulated=True)
3942+
3943+
volume = 0.0
3944+
for face in faces:
3945+
# Each face is now a triangle (3 vertices)
3946+
a, b, c = [vertices[i] for i in face]
3947+
# Signed volume of tetrahedron formed by triangle and origin
3948+
# V = (1/6) * (a dot (b cross c)) where a, b, c are the vertices
3949+
bc = cross_vectors(b, c)
3950+
vol = dot_vectors(a, bc) / 6.0
3951+
volume += vol
3952+
3953+
return abs(volume)
3954+
38853955
def centroid(self):
38863956
"""Calculate the mesh centroid.
38873957

tests/compas/datastructures/test_mesh.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,54 @@ def test_normal():
10691069
)
10701070

10711071

1072+
def test_volume():
1073+
import math
1074+
1075+
# Test with a box (3x4x5)
1076+
box = Box.from_width_height_depth(3, 4, 5)
1077+
mesh = Mesh.from_shape(box)
1078+
volume = mesh.volume()
1079+
expected_volume = 3 * 4 * 5 # 60
1080+
assert TOL.is_close(volume, expected_volume)
1081+
1082+
# Test with a smaller box (2x2x2)
1083+
box2 = Box.from_width_height_depth(2, 2, 2)
1084+
mesh2 = Mesh.from_shape(box2)
1085+
volume2 = mesh2.volume()
1086+
expected_volume2 = 2 * 2 * 2 # 8
1087+
assert TOL.is_close(volume2, expected_volume2)
1088+
1089+
# Test with a tetrahedron from polyhedron
1090+
# Regular tetrahedron with edge length ~1.633 has volume = edge^3 / (6*sqrt(2))
1091+
tet = Mesh.from_polyhedron(4)
1092+
volume = tet.volume()
1093+
# Expected volume for the platonic tetrahedron from polyhedron(4)
1094+
expected_tet_volume = 0.5132002392796675
1095+
assert TOL.is_close(volume, expected_tet_volume)
1096+
1097+
# Test with a sphere approximation
1098+
sphere_mesh = Mesh.from_shape(Sphere(radius=1.0), u=32, v=32)
1099+
volume = sphere_mesh.volume()
1100+
assert volume is not None
1101+
expected_sphere_volume = (4.0 / 3.0) * math.pi
1102+
# Allow for ~1% error due to discretization
1103+
assert TOL.is_close(volume, expected_sphere_volume, rtol=0.02)
1104+
1105+
# Test with an open mesh (should return None)
1106+
mesh = Mesh.from_obj(compas.get("faces.obj"))
1107+
volume = mesh.volume()
1108+
assert volume is None
1109+
1110+
# Test optional parameters
1111+
box3 = Box.from_width_height_depth(2, 3, 4)
1112+
mesh3 = Mesh.from_shape(box3)
1113+
1114+
# Test with copy=False and unify_cycles=False (should still work for well-oriented mesh)
1115+
volume3 = mesh3.volume(copy=False, unify_cycles=False)
1116+
expected_volume3 = 2 * 3 * 4 # 24
1117+
assert TOL.is_close(volume3, expected_volume3)
1118+
1119+
10721120
# --------------------------------------------------------------------------
10731121
# vertex geometry
10741122
# --------------------------------------------------------------------------

0 commit comments

Comments
 (0)