Skip to content

Commit 7fb5941

Browse files
ADD measure.
1 parent 05144e7 commit 7fb5941

File tree

6 files changed

+262
-0
lines changed

6 files changed

+262
-0
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,4 @@ endfunction()
228228
add_nanobind_module(booleans_ext src/booleans.cpp)
229229
add_nanobind_module(meshing_ext src/meshing.cpp)
230230
add_nanobind_module(intersections_ext src/intersections.cpp)
231+
add_nanobind_module(measure_ext src/measure.cpp)

docs/examples/example_measure.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from compas.datastructures import Mesh
2+
from compas.geometry import Box
3+
from line_profiler import profile
4+
5+
from compas_cgal.measure import mesh_area
6+
from compas_cgal.measure import mesh_centroid
7+
from compas_cgal.measure import mesh_volume
8+
9+
10+
@profile
11+
def main(mesh):
12+
"""Mesh measurement methods."""
13+
area = mesh_area(mesh)
14+
volume = mesh_volume(mesh)
15+
centroid = mesh_centroid(mesh)
16+
return area, volume, centroid
17+
18+
19+
# ==============================================================================
20+
# Input geometry
21+
# ==============================================================================
22+
23+
box = Box(1)
24+
v, f = box.to_vertices_and_faces()
25+
mesh = Mesh.from_vertices_and_faces(v, f)
26+
mesh.quads_to_triangles()
27+
V, F = mesh.to_vertices_and_faces()
28+
29+
30+
# ==============================================================================
31+
# Compute
32+
# ==============================================================================
33+
34+
result = main((V, F))
35+
36+
print("Area:", result[0])
37+
print("Volume:", result[1])
38+
print("Centroid:", result[2])

src/compas_cgal/measure.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import numpy as np
2+
from compas.geometry import Point
3+
from compas.plugins import plugin
4+
5+
from compas_cgal import measure_ext
6+
7+
from .types import VerticesFaces
8+
9+
10+
def mesh_area(mesh: VerticesFaces) -> float:
11+
"""Compute the area of a triangle mesh.
12+
13+
Parameters
14+
----------
15+
mesh : :attr:`compas_cgal.types.VerticesFaces`
16+
The mesh.
17+
18+
Returns
19+
-------
20+
float
21+
The area of the mesh.
22+
23+
"""
24+
V, F = mesh
25+
V = np.asarray(V, dtype=np.float64, order="C")
26+
F = np.asarray(F, dtype=np.int32, order="C")
27+
return measure_ext.area(V, F)
28+
29+
30+
@plugin(category="trimesh", pluggable_name="trimesh_volume")
31+
def mesh_volume(mesh: VerticesFaces) -> float:
32+
"""Compute the volume of a closed triangle mesh.
33+
34+
Parameters
35+
----------
36+
mesh : :attr:`compas_cgal.types.VerticesFaces`
37+
The mesh.
38+
39+
Returns
40+
-------
41+
float
42+
The volume of the mesh.
43+
44+
Examples
45+
--------
46+
>>> from compas.geometry import Box
47+
>>> from compas_cgal.measure import mesh_volume
48+
49+
>>> box = Box(1)
50+
>>> mesh = box.to_vertices_and_faces(triangulated=True)
51+
52+
>>> mesh_volume(mesh)
53+
1.0
54+
55+
"""
56+
V, F = mesh
57+
V = np.asarray(V, dtype=np.float64, order="C")
58+
F = np.asarray(F, dtype=np.int32, order="C")
59+
return measure_ext.volume(V, F)
60+
61+
62+
def mesh_centroid(mesh: VerticesFaces) -> list[float]:
63+
"""Compute the centroid of a the volume of a closed triangle mesh.
64+
65+
Parameters
66+
----------
67+
mesh : :attr:`compas_cgal.types.VerticesFaces`
68+
The mesh.
69+
70+
Returns
71+
-------
72+
list
73+
The centroid of the mesh.
74+
75+
"""
76+
V, F = mesh
77+
V = np.asarray(V, dtype=np.float64, order="C")
78+
F = np.asarray(F, dtype=np.int32, order="C")
79+
vector_of_double: measure_ext.VectorDouble = measure_ext.centroid(V, F)
80+
point = Point(*vector_of_double)
81+
return point

src/measure.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#include "measure.h"
2+
3+
double
4+
pmp_area(
5+
Eigen::Ref<const compas::RowMatrixXd> vertices,
6+
Eigen::Ref<const compas::RowMatrixXi> faces)
7+
{
8+
compas::Mesh mesh = compas::mesh_from_vertices_and_faces(vertices, faces);
9+
double area = CGAL::Polygon_mesh_processing::area(mesh);
10+
return area;
11+
}
12+
13+
double
14+
pmp_volume(
15+
Eigen::Ref<const compas::RowMatrixXd> vertices,
16+
Eigen::Ref<const compas::RowMatrixXi> faces)
17+
{
18+
compas::Mesh mesh = compas::mesh_from_vertices_and_faces(vertices, faces);
19+
double volume = CGAL::Polygon_mesh_processing::volume(mesh);
20+
int a = 0;
21+
return volume;
22+
}
23+
24+
std::vector<double>
25+
pmp_centroid(
26+
Eigen::Ref<const compas::RowMatrixXd> vertices,
27+
Eigen::Ref<const compas::RowMatrixXi> faces)
28+
{
29+
compas::Mesh mesh = compas::mesh_from_vertices_and_faces(vertices, faces);
30+
compas::Kernel::Point_3 centroid = CGAL::Polygon_mesh_processing::centroid(mesh);
31+
return std::vector<double>{centroid.x(), centroid.y(), centroid.z()};
32+
}
33+
34+
35+
NB_MODULE(measure_ext, m) {
36+
37+
nb::bind_vector<std::vector<double>>(m, "VectorDouble"); // Be aware that both Pybind11 and Nanobind makes copy for vectors
38+
39+
m.def(
40+
"area",
41+
&pmp_area,
42+
"Calculate the surface area of a mesh",
43+
"vertices"_a,
44+
"faces"_a);
45+
46+
m.def(
47+
"volume",
48+
&pmp_volume,
49+
"Calculate the volume enclosed by a mesh",
50+
"vertices"_a,
51+
"faces"_a);
52+
53+
m.def(
54+
"centroid",
55+
&pmp_centroid,
56+
"Calculate the centroid of a mesh",
57+
"vertices"_a,
58+
"faces"_a);
59+
}

src/measure.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#pragma once
2+
3+
#include "compas.h"
4+
5+
// CGAL measure
6+
#include <CGAL/Polygon_mesh_processing/measure.h>
7+
#include <CGAL/Point_3.h>
8+
9+
/**
10+
* @brief Computes the surface area of a triangle mesh.
11+
*
12+
* @param vertices Matrix of vertex positions as Nx3 matrix in row-major order (float64)
13+
* @param faces Matrix of face indices as Mx3 matrix in row-major order (int32)
14+
* @return double The total surface area of the mesh
15+
*/
16+
double
17+
pmp_area(
18+
Eigen::Ref<const compas::RowMatrixXd> vertices,
19+
Eigen::Ref<const compas::RowMatrixXi> faces);
20+
21+
/**
22+
* @brief Computes the volume of a closed triangle mesh.
23+
*
24+
* @param vertices Matrix of vertex positions as Nx3 matrix in row-major order (float64)
25+
* @param faces Matrix of face indices as Mx3 matrix in row-major order (int32)
26+
* @return double The volume enclosed by the mesh. Returns 0 if the mesh is not closed.
27+
*/
28+
double
29+
pmp_volume(
30+
Eigen::Ref<const compas::RowMatrixXd> vertices,
31+
Eigen::Ref<const compas::RowMatrixXi> faces);
32+
33+
/**
34+
* @brief Computes the centroid (center of mass) of a triangle mesh.
35+
*
36+
* @param vertices Matrix of vertex positions as Nx3 matrix in row-major order (float64)
37+
* @param faces Matrix of face indices as Mx3 matrix in row-major order (int32)
38+
* @return std::vector<double> A vector containing the x,y,z coordinates of the mesh centroid
39+
*/
40+
std::vector<double>
41+
pmp_centroid(
42+
Eigen::Ref<const compas::RowMatrixXd> vertices,
43+
Eigen::Ref<const compas::RowMatrixXi> faces);

tests/test_measure.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Tests for COMPAS CGAL measure functionality."""
2+
3+
import pytest
4+
from compas.geometry import Box
5+
from compas.datastructures import Mesh
6+
from compas_cgal.measure import mesh_area, mesh_volume, mesh_centroid
7+
8+
9+
def test_area():
10+
"""Test area computation with a unit cube."""
11+
box = Box.from_width_height_depth(1, 1, 1)
12+
mesh = Mesh.from_vertices_and_faces(*box.to_vertices_and_faces())
13+
mesh.quads_to_triangles()
14+
V, F = mesh.to_vertices_and_faces()
15+
16+
# Unit cube has 6 faces, each 1x1 = total area of 6
17+
assert pytest.approx(mesh_area((V, F))) == 6.0
18+
19+
20+
def test_volume():
21+
"""Test volume computation with a unit cube."""
22+
box = Box.from_width_height_depth(1, 1, 1)
23+
mesh = Mesh.from_vertices_and_faces(*box.to_vertices_and_faces())
24+
mesh.quads_to_triangles()
25+
V, F = mesh.to_vertices_and_faces()
26+
27+
# Unit cube has volume of 1
28+
assert pytest.approx(mesh_volume((V, F))) == 1.0
29+
30+
31+
def test_centroid():
32+
"""Test centroid computation with a unit cube."""
33+
box = Box.from_width_height_depth(1, 1, 1)
34+
mesh = Mesh.from_vertices_and_faces(*box.to_vertices_and_faces())
35+
mesh.quads_to_triangles()
36+
V, F = mesh.to_vertices_and_faces()
37+
38+
# Centroid should be at origin (0,0,0)
39+
result = mesh_centroid((V, F))
40+
assert pytest.approx(result) == [0.0, 0.0, 0.0]

0 commit comments

Comments
 (0)