Skip to content

Commit 9c0c835

Browse files
CopilotLicini
andcommitted
Add volume() method to Mesh class with tests
Co-authored-by: Licini <17893605+Licini@users.noreply.github.com>
1 parent 39efd54 commit 9c0c835

File tree

2 files changed

+84
-0
lines changed

2 files changed

+84
-0
lines changed

src/compas/datastructures/mesh/mesh.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from compas.geometry import centroid_points
4040
from compas.geometry import centroid_polygon
4141
from compas.geometry import cross_vectors
42+
from compas.geometry import dot_vectors
4243
from compas.geometry import distance_line_line
4344
from compas.geometry import distance_point_plane
4445
from compas.geometry import distance_point_point
@@ -3882,6 +3883,63 @@ def area(self):
38823883
"""
38833884
return sum(self.face_area(fkey) for fkey in self.faces())
38843885

3886+
def volume(self):
3887+
"""Calculate the volume of the mesh.
3888+
3889+
Returns
3890+
-------
3891+
float | None
3892+
The volume of the mesh if the mesh is closed, None otherwise.
3893+
3894+
Notes
3895+
-----
3896+
The volume is computed using the signed volume of tetrahedra formed by each
3897+
triangulated face and the origin. This method works for both convex and
3898+
non-convex meshes, as long as they are closed and properly oriented.
3899+
3900+
The volume is only meaningful for closed meshes. For open meshes, this method
3901+
returns None.
3902+
3903+
Examples
3904+
--------
3905+
>>> from compas.datastructures import Mesh
3906+
>>> mesh = Mesh.from_polyhedron(6) # Create a cube
3907+
>>> volume = mesh.volume()
3908+
>>> volume is not None
3909+
True
3910+
3911+
"""
3912+
if not self.is_closed():
3913+
return None
3914+
3915+
volume = 0.0
3916+
for fkey in self.faces():
3917+
vertices = self.face_vertices(fkey)
3918+
# Get coordinates for all vertices of the face
3919+
coords = [self.vertex_coordinates(v) for v in vertices]
3920+
3921+
# Triangulate the face if it has more than 3 vertices
3922+
if len(coords) == 3:
3923+
triangles = [coords]
3924+
else:
3925+
# Use simple fan triangulation from first vertex
3926+
triangles = []
3927+
for i in range(1, len(coords) - 1):
3928+
triangles.append([coords[0], coords[i], coords[i + 1]])
3929+
3930+
# Calculate signed volume contribution from each triangle
3931+
for triangle in triangles:
3932+
# Signed volume of tetrahedron formed by triangle and origin
3933+
# V = (1/6) * |a · (b × c)| where a, b, c are the vertices
3934+
a, b, c = triangle
3935+
# Calculate cross product of b and c
3936+
bc = cross_vectors(b, c)
3937+
# Calculate dot product with a
3938+
vol = dot_vectors(a, bc) / 6.0
3939+
volume += vol
3940+
3941+
return abs(volume)
3942+
38853943
def centroid(self):
38863944
"""Calculate the mesh centroid.
38873945

tests/compas/datastructures/test_mesh.py

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

10711071

1072+
def test_volume():
1073+
# Test with a cube
1074+
mesh = Mesh.from_stl(compas.get("cube_binary.stl"))
1075+
volume = mesh.volume()
1076+
assert volume is not None
1077+
# The cube in cube_binary.stl has side length 1, so volume should be 1
1078+
assert TOL.is_close(volume, 1.0)
1079+
1080+
# Test with a tetrahedron
1081+
tet = Mesh.from_polyhedron(4)
1082+
volume = tet.volume()
1083+
assert volume is not None
1084+
assert volume > 0
1085+
1086+
# Test with a cube from polyhedron
1087+
cube = Mesh.from_polyhedron(6)
1088+
volume = cube.volume()
1089+
assert volume is not None
1090+
assert volume > 0
1091+
1092+
# Test with an open mesh (should return None)
1093+
mesh = Mesh.from_obj(compas.get("faces.obj"))
1094+
volume = mesh.volume()
1095+
assert volume is None
1096+
1097+
10721098
# --------------------------------------------------------------------------
10731099
# vertex geometry
10741100
# --------------------------------------------------------------------------

0 commit comments

Comments
 (0)