From 39efd54f21e2e8d505022bf5e7d5310be09e4f61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:01:03 +0000 Subject: [PATCH 01/12] Initial plan From 9c0c8351cb0706988d5855c4216893267be14120 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:10:53 +0000 Subject: [PATCH 02/12] Add volume() method to Mesh class with tests Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- src/compas/datastructures/mesh/mesh.py | 58 ++++++++++++++++++++++++ tests/compas/datastructures/test_mesh.py | 26 +++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 246023a48315..4c7e9950643f 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -39,6 +39,7 @@ from compas.geometry import centroid_points from compas.geometry import centroid_polygon from compas.geometry import cross_vectors +from compas.geometry import dot_vectors from compas.geometry import distance_line_line from compas.geometry import distance_point_plane from compas.geometry import distance_point_point @@ -3882,6 +3883,63 @@ def area(self): """ return sum(self.face_area(fkey) for fkey in self.faces()) + def volume(self): + """Calculate the volume of the mesh. + + Returns + ------- + float | None + The volume of the mesh if the mesh is closed, None otherwise. + + Notes + ----- + The volume is computed using the signed volume of tetrahedra formed by each + triangulated face and the origin. This method works for both convex and + non-convex meshes, as long as they are closed and properly oriented. + + The volume is only meaningful for closed meshes. For open meshes, this method + returns None. + + Examples + -------- + >>> from compas.datastructures import Mesh + >>> mesh = Mesh.from_polyhedron(6) # Create a cube + >>> volume = mesh.volume() + >>> volume is not None + True + + """ + if not self.is_closed(): + return None + + volume = 0.0 + for fkey in self.faces(): + vertices = self.face_vertices(fkey) + # Get coordinates for all vertices of the face + coords = [self.vertex_coordinates(v) for v in vertices] + + # Triangulate the face if it has more than 3 vertices + if len(coords) == 3: + triangles = [coords] + else: + # Use simple fan triangulation from first vertex + triangles = [] + for i in range(1, len(coords) - 1): + triangles.append([coords[0], coords[i], coords[i + 1]]) + + # Calculate signed volume contribution from each triangle + for triangle in triangles: + # Signed volume of tetrahedron formed by triangle and origin + # V = (1/6) * |a · (b × c)| where a, b, c are the vertices + a, b, c = triangle + # Calculate cross product of b and c + bc = cross_vectors(b, c) + # Calculate dot product with a + vol = dot_vectors(a, bc) / 6.0 + volume += vol + + return abs(volume) + def centroid(self): """Calculate the mesh centroid. diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index f88d92bb30f8..778ca281016f 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1069,6 +1069,32 @@ def test_normal(): ) +def test_volume(): + # Test with a cube + mesh = Mesh.from_stl(compas.get("cube_binary.stl")) + volume = mesh.volume() + assert volume is not None + # The cube in cube_binary.stl has side length 1, so volume should be 1 + assert TOL.is_close(volume, 1.0) + + # Test with a tetrahedron + tet = Mesh.from_polyhedron(4) + volume = tet.volume() + assert volume is not None + assert volume > 0 + + # Test with a cube from polyhedron + cube = Mesh.from_polyhedron(6) + volume = cube.volume() + assert volume is not None + assert volume > 0 + + # Test with an open mesh (should return None) + mesh = Mesh.from_obj(compas.get("faces.obj")) + volume = mesh.volume() + assert volume is None + + # -------------------------------------------------------------------------- # vertex geometry # -------------------------------------------------------------------------- From 0ebac33d992ab6b6a387f7784293e4a3c86a2490 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:12:29 +0000 Subject: [PATCH 03/12] Add sphere volume test to verify accuracy Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- tests/compas/datastructures/test_mesh.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index 778ca281016f..add3649be7cd 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1070,6 +1070,8 @@ def test_normal(): def test_volume(): + import math + # Test with a cube mesh = Mesh.from_stl(compas.get("cube_binary.stl")) volume = mesh.volume() @@ -1089,6 +1091,14 @@ def test_volume(): assert volume is not None assert volume > 0 + # Test with a sphere approximation + sphere_mesh = Mesh.from_shape(Sphere(radius=1.0), u=32, v=32) + volume = sphere_mesh.volume() + assert volume is not None + expected_sphere_volume = (4.0/3.0) * math.pi * (1.0 ** 3) + # Allow for ~1% error due to discretization + assert TOL.is_close(volume, expected_sphere_volume, rtol=0.02) + # Test with an open mesh (should return None) mesh = Mesh.from_obj(compas.get("faces.obj")) volume = mesh.volume() From 3895ec12f6e025d78b66303fc7bc43df04c22bd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:14:36 +0000 Subject: [PATCH 04/12] Apply code formatting with ruff and black Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- src/compas/datastructures/mesh/mesh.py | 6 +++--- tests/compas/datastructures/test_mesh.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 4c7e9950643f..14545e480469 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -39,10 +39,10 @@ from compas.geometry import centroid_points from compas.geometry import centroid_polygon from compas.geometry import cross_vectors -from compas.geometry import dot_vectors from compas.geometry import distance_line_line from compas.geometry import distance_point_plane from compas.geometry import distance_point_point +from compas.geometry import dot_vectors from compas.geometry import length_vector from compas.geometry import midpoint_line from compas.geometry import normal_polygon @@ -3917,7 +3917,7 @@ def volume(self): vertices = self.face_vertices(fkey) # Get coordinates for all vertices of the face coords = [self.vertex_coordinates(v) for v in vertices] - + # Triangulate the face if it has more than 3 vertices if len(coords) == 3: triangles = [coords] @@ -3926,7 +3926,7 @@ def volume(self): triangles = [] for i in range(1, len(coords) - 1): triangles.append([coords[0], coords[i], coords[i + 1]]) - + # Calculate signed volume contribution from each triangle for triangle in triangles: # Signed volume of tetrahedron formed by triangle and origin diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index add3649be7cd..392037762b29 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1071,7 +1071,7 @@ def test_normal(): def test_volume(): import math - + # Test with a cube mesh = Mesh.from_stl(compas.get("cube_binary.stl")) volume = mesh.volume() @@ -1095,7 +1095,7 @@ def test_volume(): sphere_mesh = Mesh.from_shape(Sphere(radius=1.0), u=32, v=32) volume = sphere_mesh.volume() assert volume is not None - expected_sphere_volume = (4.0/3.0) * math.pi * (1.0 ** 3) + expected_sphere_volume = (4.0 / 3.0) * math.pi * (1.0**3) # Allow for ~1% error due to discretization assert TOL.is_close(volume, expected_sphere_volume, rtol=0.02) From 77c3fab7c44d1428e280a19a4bd17a470f3d0966 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:16:21 +0000 Subject: [PATCH 05/12] Address code review feedback - simplify sphere volume calculation Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- tests/compas/datastructures/test_mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index 392037762b29..dd5dc1860715 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1095,7 +1095,7 @@ def test_volume(): sphere_mesh = Mesh.from_shape(Sphere(radius=1.0), u=32, v=32) volume = sphere_mesh.volume() assert volume is not None - expected_sphere_volume = (4.0 / 3.0) * math.pi * (1.0**3) + expected_sphere_volume = (4.0 / 3.0) * math.pi # Allow for ~1% error due to discretization assert TOL.is_close(volume, expected_sphere_volume, rtol=0.02) From 504be2ad14231099964de1253db895b74001c813 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:55:48 +0000 Subject: [PATCH 06/12] Refactor volume() to use existing volume_polyhedron function Simplified implementation by reusing the existing volume_polyhedron function from compas.geometry instead of reimplementing the algorithm. This reduces code duplication and improves maintainability. Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- src/compas/datastructures/mesh/mesh.py | 31 +++----------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 14545e480469..024fd51cdfdc 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -42,7 +42,6 @@ from compas.geometry import distance_line_line from compas.geometry import distance_point_plane from compas.geometry import distance_point_point -from compas.geometry import dot_vectors from compas.geometry import length_vector from compas.geometry import midpoint_line from compas.geometry import normal_polygon @@ -53,6 +52,7 @@ from compas.geometry import sum_vectors from compas.geometry import transform_points from compas.geometry import vector_average +from compas.geometry import volume_polyhedron from compas.itertools import linspace from compas.itertools import pairwise from compas.itertools import window @@ -3912,33 +3912,8 @@ def volume(self): if not self.is_closed(): return None - volume = 0.0 - for fkey in self.faces(): - vertices = self.face_vertices(fkey) - # Get coordinates for all vertices of the face - coords = [self.vertex_coordinates(v) for v in vertices] - - # Triangulate the face if it has more than 3 vertices - if len(coords) == 3: - triangles = [coords] - else: - # Use simple fan triangulation from first vertex - triangles = [] - for i in range(1, len(coords) - 1): - triangles.append([coords[0], coords[i], coords[i + 1]]) - - # Calculate signed volume contribution from each triangle - for triangle in triangles: - # Signed volume of tetrahedron formed by triangle and origin - # V = (1/6) * |a · (b × c)| where a, b, c are the vertices - a, b, c = triangle - # Calculate cross product of b and c - bc = cross_vectors(b, c) - # Calculate dot product with a - vol = dot_vectors(a, bc) / 6.0 - volume += vol - - return abs(volume) + vertices, faces = self.to_vertices_and_faces() + return abs(volume_polyhedron((vertices, faces))) def centroid(self): """Calculate the mesh centroid. From c88b6e852488692c901a53363e8ab0a687976779 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Oct 2025 09:51:30 +0000 Subject: [PATCH 07/12] Add mesh copy and unify_cycles before volume calculation - Create a copy of the mesh to avoid in-place modifications - Call unify_cycles() on the copy to ensure consistent face orientation - Deep copy vertices list since volume_polyhedron modifies it in place - Updated documentation to reflect internal operations Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- src/compas/datastructures/mesh/mesh.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 024fd51cdfdc..3a8686a6c2d1 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -3900,6 +3900,9 @@ def volume(self): The volume is only meaningful for closed meshes. For open meshes, this method returns None. + The mesh is copied internally and face cycles are unified to ensure correct + orientation before computing the volume. + Examples -------- >>> from compas.datastructures import Mesh @@ -3912,7 +3915,16 @@ def volume(self): if not self.is_closed(): return None - vertices, faces = self.to_vertices_and_faces() + # Make a copy to avoid modifying the original mesh + mesh_copy = self.copy() + # Unify cycles to ensure consistent face orientation + mesh_copy.unify_cycles() + + # Get vertices and faces from the unified copy + # Make a copy of vertices list since volume_polyhedron modifies it in place + vertices, faces = mesh_copy.to_vertices_and_faces() + vertices = [v[:] for v in vertices] # Deep copy of vertex coordinates + return abs(volume_polyhedron((vertices, faces))) def centroid(self): From 6e1bc6746cc6521020768f664bf306769e606655 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 07:10:04 +0000 Subject: [PATCH 08/12] Replace volume_polyhedron with signed volume approach for non-convex support - volume_polyhedron only works for convex meshes (as per its documentation) - Implemented signed volume of tetrahedra approach which works for both convex and non-convex meshes - Removed volume_polyhedron import, added dot_vectors import - Updated CHANGELOG.md to document the new volume() method - All tests pass with same accuracy Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- CHANGELOG.md | 1 + src/compas/datastructures/mesh/mesh.py | 33 +++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d25fbad153cb..17b07122794c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas_rhino.install_with_pip` with corresponding command line utility `install_in_rhino`. * Added support for `.stp` file extension in addition to `.step` for `RhinoBrep.from_step()` and `RhinoBrep.to_step()` methods. +* Added `volume()` method to `compas.datastructures.Mesh` for computing the volume of closed meshes using signed volume of triangles. ### Changed diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 3a8686a6c2d1..307ac5501482 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -42,6 +42,7 @@ from compas.geometry import distance_line_line from compas.geometry import distance_point_plane from compas.geometry import distance_point_point +from compas.geometry import dot_vectors from compas.geometry import length_vector from compas.geometry import midpoint_line from compas.geometry import normal_polygon @@ -52,7 +53,6 @@ from compas.geometry import sum_vectors from compas.geometry import transform_points from compas.geometry import vector_average -from compas.geometry import volume_polyhedron from compas.itertools import linspace from compas.itertools import pairwise from compas.itertools import window @@ -3920,12 +3920,33 @@ def volume(self): # Unify cycles to ensure consistent face orientation mesh_copy.unify_cycles() - # Get vertices and faces from the unified copy - # Make a copy of vertices list since volume_polyhedron modifies it in place - vertices, faces = mesh_copy.to_vertices_and_faces() - vertices = [v[:] for v in vertices] # Deep copy of vertex coordinates + volume = 0.0 + for fkey in mesh_copy.faces(): + vertices = mesh_copy.face_vertices(fkey) + # Get coordinates for all vertices of the face + coords = [mesh_copy.vertex_coordinates(v) for v in vertices] - return abs(volume_polyhedron((vertices, faces))) + # Triangulate the face if it has more than 3 vertices + if len(coords) == 3: + triangles = [coords] + else: + # Use simple fan triangulation from first vertex + triangles = [] + for i in range(1, len(coords) - 1): + triangles.append([coords[0], coords[i], coords[i + 1]]) + + # Calculate signed volume contribution from each triangle + for triangle in triangles: + # Signed volume of tetrahedron formed by triangle and origin + # V = (1/6) * (a · (b × c)) where a, b, c are the vertices + a, b, c = triangle + # Calculate cross product of b and c + bc = cross_vectors(b, c) + # Calculate dot product with a + vol = dot_vectors(a, bc) / 6.0 + volume += vol + + return abs(volume) def centroid(self): """Calculate the mesh centroid. From 066c0f5c1a4e8e0f2e7eb254d0104e15a513ae1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 07:20:09 +0000 Subject: [PATCH 09/12] Use built-in triangulation from to_vertices_and_faces - Replaced manual fan triangulation with to_vertices_and_faces(triangulated=True) - Cleaner and more maintainable code - Uses the mesh's built-in triangulation method - All tests pass with same accuracy Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- src/compas/datastructures/mesh/mesh.py | 35 ++++++++------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 307ac5501482..8aff59e6d578 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -3920,31 +3920,18 @@ def volume(self): # Unify cycles to ensure consistent face orientation mesh_copy.unify_cycles() + # Use built-in triangulation to get triangulated faces + vertices, faces = mesh_copy.to_vertices_and_faces(triangulated=True) + volume = 0.0 - for fkey in mesh_copy.faces(): - vertices = mesh_copy.face_vertices(fkey) - # Get coordinates for all vertices of the face - coords = [mesh_copy.vertex_coordinates(v) for v in vertices] - - # Triangulate the face if it has more than 3 vertices - if len(coords) == 3: - triangles = [coords] - else: - # Use simple fan triangulation from first vertex - triangles = [] - for i in range(1, len(coords) - 1): - triangles.append([coords[0], coords[i], coords[i + 1]]) - - # Calculate signed volume contribution from each triangle - for triangle in triangles: - # Signed volume of tetrahedron formed by triangle and origin - # V = (1/6) * (a · (b × c)) where a, b, c are the vertices - a, b, c = triangle - # Calculate cross product of b and c - bc = cross_vectors(b, c) - # Calculate dot product with a - vol = dot_vectors(a, bc) / 6.0 - volume += vol + for face in faces: + # Each face is now a triangle (3 vertices) + a, b, c = [vertices[i] for i in face] + # Signed volume of tetrahedron formed by triangle and origin + # V = (1/6) * (a · (b × c)) where a, b, c are the vertices + bc = cross_vectors(b, c) + vol = dot_vectors(a, bc) / 6.0 + volume += vol return abs(volume) From 856f48ed4ba74898554904ec5ed56adf2856a3dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 07:34:41 +0000 Subject: [PATCH 10/12] Fix encoding error for IronPython compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace non-ASCII characters (· and ×) with ASCII equivalents (dot and cross) in comment on line 3931 to fix IronPython syntax error. Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- src/compas/datastructures/mesh/mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 8aff59e6d578..c676cef4e704 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -3928,7 +3928,7 @@ def volume(self): # Each face is now a triangle (3 vertices) a, b, c = [vertices[i] for i in face] # Signed volume of tetrahedron formed by triangle and origin - # V = (1/6) * (a · (b × c)) where a, b, c are the vertices + # V = (1/6) * (a dot (b cross c)) where a, b, c are the vertices bc = cross_vectors(b, c) vol = dot_vectors(a, bc) / 6.0 volume += vol From d7e80c8f1c8fd65e3eb185b1909c92fe0f045053 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:33:39 +0000 Subject: [PATCH 11/12] Add optional parameters and improve tests per review feedback 1. Added copy and unify_cycles parameters (both default to True) for performance optimization 2. Updated tests to use Box instead of STL files with non-unit dimensions (3x4x5, 2x2x2, 2x3x4) 3. All tests now check actual expected volumes with proper math verification 4. Added docstring note about non-convex face triangulation limitations 5. Added test for optional parameters (copy=False, unify_cycles=False) Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- src/compas/datastructures/mesh/mesh.py | 29 +++++++++++++++---- tests/compas/datastructures/test_mesh.py | 37 +++++++++++++++--------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index c676cef4e704..81e371780d70 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -3883,9 +3883,18 @@ def area(self): """ return sum(self.face_area(fkey) for fkey in self.faces()) - def volume(self): + def volume(self, copy=True, unify_cycles=True): """Calculate the volume of the mesh. + Parameters + ---------- + copy : bool, optional + If True, a copy of the mesh is made before computation to avoid modifying the original. + Default is True. + unify_cycles : bool, optional + If True, face cycles are unified to ensure consistent orientation. + Default is True. + Returns ------- float | None @@ -3900,8 +3909,14 @@ def volume(self): The volume is only meaningful for closed meshes. For open meshes, this method returns None. - The mesh is copied internally and face cycles are unified to ensure correct - orientation before computing the volume. + When faces are non-convex, the triangulation might not be correct, since it uses + the centroid of the face. For accurate results with non-convex faces, consider + using a mesh with triangulated faces. + + By default, the mesh is copied internally and face cycles are unified to ensure + correct orientation before computing the volume. These operations can be disabled + by setting ``copy=False`` and ``unify_cycles=False`` for performance in cases where + the mesh is already correctly oriented or when the original mesh can be modified. Examples -------- @@ -3916,12 +3931,14 @@ def volume(self): return None # Make a copy to avoid modifying the original mesh - mesh_copy = self.copy() + mesh_to_use = self.copy() if copy else self + # Unify cycles to ensure consistent face orientation - mesh_copy.unify_cycles() + if unify_cycles: + mesh_to_use.unify_cycles() # Use built-in triangulation to get triangulated faces - vertices, faces = mesh_copy.to_vertices_and_faces(triangulated=True) + vertices, faces = mesh_to_use.to_vertices_and_faces(triangulated=True) volume = 0.0 for face in faces: diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index dd5dc1860715..9ee8ac8c2886 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1072,25 +1072,27 @@ def test_normal(): def test_volume(): import math - # Test with a cube - mesh = Mesh.from_stl(compas.get("cube_binary.stl")) + # Test with a box (3x4x5) + box = Box.from_width_height_depth(3, 4, 5) + mesh = Mesh.from_shape(box) volume = mesh.volume() - assert volume is not None - # The cube in cube_binary.stl has side length 1, so volume should be 1 - assert TOL.is_close(volume, 1.0) - - # Test with a tetrahedron + expected_volume = 3 * 4 * 5 # 60 + assert TOL.is_close(volume, expected_volume) + + # Test with a smaller box (2x2x2) + box2 = Box.from_width_height_depth(2, 2, 2) + mesh2 = Mesh.from_shape(box2) + volume2 = mesh2.volume() + expected_volume2 = 2 * 2 * 2 # 8 + assert TOL.is_close(volume2, expected_volume2) + + # Test with a tetrahedron from polyhedron + # Platonic tetrahedron has known volume based on edge length tet = Mesh.from_polyhedron(4) volume = tet.volume() assert volume is not None assert volume > 0 - # Test with a cube from polyhedron - cube = Mesh.from_polyhedron(6) - volume = cube.volume() - assert volume is not None - assert volume > 0 - # Test with a sphere approximation sphere_mesh = Mesh.from_shape(Sphere(radius=1.0), u=32, v=32) volume = sphere_mesh.volume() @@ -1104,6 +1106,15 @@ def test_volume(): volume = mesh.volume() assert volume is None + # Test optional parameters + box3 = Box.from_width_height_depth(2, 3, 4) + mesh3 = Mesh.from_shape(box3) + + # Test with copy=False and unify_cycles=False (should still work for well-oriented mesh) + volume3 = mesh3.volume(copy=False, unify_cycles=False) + expected_volume3 = 2 * 3 * 4 # 24 + assert TOL.is_close(volume3, expected_volume3) + # -------------------------------------------------------------------------- # vertex geometry From 84dbabb327332d91309f2f371e962d930a3dc372 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:43:23 +0000 Subject: [PATCH 12/12] Update tetrahedron test to check actual volume value Changed tetrahedron test from checking > 0 to verifying the actual expected volume of 0.5132002392796675 for the platonic tetrahedron from polyhedron(4). Co-authored-by: Licini <17893605+Licini@users.noreply.github.com> --- tests/compas/datastructures/test_mesh.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index 9ee8ac8c2886..319756f284b8 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1087,11 +1087,12 @@ def test_volume(): assert TOL.is_close(volume2, expected_volume2) # Test with a tetrahedron from polyhedron - # Platonic tetrahedron has known volume based on edge length + # Regular tetrahedron with edge length ~1.633 has volume = edge^3 / (6*sqrt(2)) tet = Mesh.from_polyhedron(4) volume = tet.volume() - assert volume is not None - assert volume > 0 + # Expected volume for the platonic tetrahedron from polyhedron(4) + expected_tet_volume = 0.5132002392796675 + assert TOL.is_close(volume, expected_tet_volume) # Test with a sphere approximation sphere_mesh = Mesh.from_shape(Sphere(radius=1.0), u=32, v=32)