Repository: mikedh/trimesh
Pull request #1327 (2021-08-17 17:22:02)

Release: Integral Mean Curvature + Voxel Fix
- release #1322 
- release #1324 


Patch:
diff --git a/trimesh/base.py b/trimesh/base.py
index 359e7fae..5ee5c52e 100644
--- a/trimesh/base.py
+++ b/trimesh/base.py
@@ -1449,6 +1449,21 @@ class Trimesh(Geometry3D):
         populate = self.face_adjacency_radius
         return self._cache['face_adjacency_span']
 
+    @caching.cache_decorator
+    def integral_mean_curvature(self):
+        """
+        The integral mean curvature, or the surface integral of the mean curvature.
+
+        Returns
+        ---------
+        area : float
+          Integral mean curvature of mesh
+        """
+        edges_length = np.linalg.norm(np.subtract(
+            *self.vertices[self.face_adjacency_edges.T]), axis=1)
+        imc = (self.face_adjacency_angles * edges_length).sum() * 0.5
+        return imc
+
     @caching.cache_decorator
     def vertex_adjacency_graph(self):
         """
diff --git a/trimesh/version.py b/trimesh/version.py
index 2c006aa1..636f44a8 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,1 +1,1 @@
-__version__ = '3.9.27'
+__version__ = '3.9.28'
diff --git a/trimesh/voxel/runlength.py b/trimesh/voxel/runlength.py
index 1fc331cc..f6b268f6 100644
--- a/trimesh/voxel/runlength.py
+++ b/trimesh/voxel/runlength.py
@@ -514,7 +514,10 @@ def sorted_brle_gather_1d(brle_data, ordered_indices):
     """
     data_iter = iter(brle_data)
     index_iter = iter(ordered_indices)
-    index = next(index_iter)
+    try:
+        index = next(index_iter)
+    except StopIteration:
+        return
     start = 0
     value = True
     while True:


Test patch:
diff --git a/tests/test_integralmeancurvature.py b/tests/test_integralmeancurvature.py
new file mode 100644
index 00000000..eb5f941a
--- /dev/null
+++ b/tests/test_integralmeancurvature.py
@@ -0,0 +1,46 @@
+try:
+    from . import generic as g
+except BaseException:
+    import generic as g
+
+
+class IntegralMeanCurvatureTest(g.unittest.TestCase):
+
+    def test_IMCsphere(self):
+        # how close do we need to be - relative tolerance
+        tol = 1e-3
+        for radius in [0.1, 1.0, 3.1459, 29.20]:
+            m = g.trimesh.creation.icosphere(
+                subdivisions=4, radius=radius)
+            IMC = m.integral_mean_curvature
+            ref = 4 * g.np.pi * radius
+            assert g.np.isclose(IMC, ref, rtol=tol)
+
+    def test_IMCcapsule(self):
+        # how close do we need to be - relative tolerance
+        tol = 1e-3
+        radius = 1.2
+        for aspect_ratio in [0.0, 0.5, 1.0, 4.0, 100]:
+            L = aspect_ratio * radius
+            m = g.trimesh.creation.capsule(height=L, radius=radius, count=[64, 64])
+            IMC = m.integral_mean_curvature
+            ref = g.np.pi * (L + 4 * radius)
+            assert g.np.isclose(IMC, ref, rtol=tol)
+
+    def test_IMCbox(self):
+        # how close do we need to be - relative tolerance
+        tol = 1e-3
+        n_tests = 4
+        extents = 1 - g.np.random.random((n_tests, 3))
+        for extent in extents:
+            m = g.trimesh.creation.box(extents=extent)
+            IMC = m.integral_mean_curvature
+            # only the right angle (=pi/2) edges contribute,
+            # edges that lie on the faces of the box do not
+            ref = 4 * extent.sum() * g.np.pi / 2 * 0.5
+            assert g.np.isclose(IMC, ref, rtol=tol)
+
+
+if __name__ == '__main__':
+    g.trimesh.util.attach_to_log()
+    g.unittest.main()