Repository: mikedh/trimesh
Pull request #2254 (2024-07-21 15:14:52)

Update transformations.py
In the function rotation_matrix the input array direction is being normalised by the the function unit_vector anyways, so there is no need to explicitly require direction to only consist of unit_vectors in the description of rotation_matrix.


Patch:
diff --git a/trimesh/base.py b/trimesh/base.py
index 08200731..aafec90e 100644
--- a/trimesh/base.py
+++ b/trimesh/base.py
@@ -2667,7 +2667,7 @@ class Trimesh(Geometry3D):
         area_faces : (n, ) float
           Area of each face
         """
-        return triangles.area(crosses=self.triangles_cross)
+        return triangles.area(crosses=self.triangles_cross, sum=False)
 
     @caching.cache_decorator
     def mass_properties(self) -> MassProperties:
diff --git a/trimesh/convex.py b/trimesh/convex.py
index 3e507b4c..396589e7 100644
--- a/trimesh/convex.py
+++ b/trimesh/convex.py
@@ -91,7 +91,7 @@ def convex_hull(obj, qhull_options="QbB Pp Qt", repair=True):
     crosses = crosses[valid]
 
     # each triangle area and mean center
-    triangles_area = triangles.area(crosses=crosses)
+    triangles_area = triangles.area(crosses=crosses, sum=False)
     triangles_center = vertices[faces].mean(axis=1)
 
     # since the convex hull is (hopefully) convex, the vector from
diff --git a/trimesh/path/exchange/misc.py b/trimesh/path/exchange/misc.py
index de76fdcd..d035244d 100644
--- a/trimesh/path/exchange/misc.py
+++ b/trimesh/path/exchange/misc.py
@@ -111,7 +111,6 @@ def polygon_to_path(polygon):
         "entities": entities,
         "vertices": np.vstack(vertices) if len(vertices) > 0 else vertices,
     }
-
     return kwargs
 
 
diff --git a/trimesh/path/polygons.py b/trimesh/path/polygons.py
index c2d8edf4..dda5af7a 100644
--- a/trimesh/path/polygons.py
+++ b/trimesh/path/polygons.py
@@ -50,14 +50,6 @@ def enclosure_tree(polygons):
        contained by another polygon
     """
 
-    # nodes are indexes in polygons
-    contains = nx.DiGraph()
-
-    if len(polygons) == 1:
-        # add an early exit for only a single polygon
-        contains.add_node(0)
-        return np.array([0], dtype=np.int64), contains
-
     # get the bounds for every valid polygon
     bounds = {
         k: v
@@ -67,6 +59,8 @@ def enclosure_tree(polygons):
         if len(v) == 4
     }
 
+    # nodes are indexes in polygons
+    contains = nx.DiGraph()
     # make sure we don't have orphaned polygon
     contains.add_nodes_from(bounds.keys())
 
@@ -557,18 +551,13 @@ def paths_to_polygons(paths, scale=None):
             # non-zero area
             continue
         try:
-            polygon = Polygon(path)
-            if polygon.is_valid:
-                polygons[i] = polygon
-            else:
-                polygons[i] = repair_invalid(polygon, scale)
+            polygons[i] = repair_invalid(Polygon(path), scale)
         except ValueError:
             # raised if a polygon is unrecoverable
             continue
         except BaseException:
             log.error("unrecoverable polygon", exc_info=True)
     polygons = np.array(polygons)
-
     return polygons
 
 
diff --git a/trimesh/transformations.py b/trimesh/transformations.py
index 6ef16374..6a3ad9da 100644
--- a/trimesh/transformations.py
+++ b/trimesh/transformations.py
@@ -323,7 +323,7 @@ def rotation_matrix(angle, direction, point=None):
     angle     : float, or sympy.Symbol
       Angle, in radians or symbolic angle
     direction : (3,) float
-      Unit vector along rotation axis
+      Any vector along rotation axis
     point     : (3, ) float, or None
       Origin point of rotation axis
 
diff --git a/trimesh/triangles.py b/trimesh/triangles.py
index b6efa328..7af0513b 100644
--- a/trimesh/triangles.py
+++ b/trimesh/triangles.py
@@ -16,7 +16,7 @@ from .typed import NDArray, Optional, float64
 from .util import diagonal_dot, unitize
 
 
-def cross(triangles: NDArray) -> NDArray:
+def cross(triangles):
     """
     Returns the cross product of two edges from input triangles
 
@@ -30,19 +30,13 @@ def cross(triangles: NDArray) -> NDArray:
     crosses : (n, 3) float
       Cross product of two edge vectors
     """
-    vectors = triangles[:, 1:, :] - triangles[:, :2, :]
-    if triangles.shape[2] == 3:
-        return np.cross(vectors[:, 0], vectors[:, 1])
-    elif triangles.shape[2] == 2:
-        a = vectors[:, 0]
-        b = vectors[:, 1]
-        # numpy 2.0 deprecated 2D cross productes
-        return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
+    vectors = np.diff(triangles, axis=1)
+    crosses = np.cross(vectors[:, 0], vectors[:, 1])
 
-    raise ValueError(triangles.shape)
+    return crosses
 
 
-def area(triangles=None, crosses=None):
+def area(triangles=None, crosses=None, sum=False):
     """
     Calculates the sum area of input triangles
 
@@ -61,8 +55,11 @@ def area(triangles=None, crosses=None):
       Individual or summed area depending on `sum` argument
     """
     if crosses is None:
-        crosses = cross(np.asanyarray(triangles, dtype=np.float64))
-    return np.sqrt((crosses**2).sum(axis=1)) / 2.0
+        crosses = cross(triangles)
+    areas = np.sqrt((crosses**2).sum(axis=1)) / 2.0
+    if sum:
+        return areas.sum()
+    return areas
 
 
 def normals(triangles=None, crosses=None):
@@ -83,8 +80,6 @@ def normals(triangles=None, crosses=None):
     valid : (n,) bool
       Was the face nonzero area or not
     """
-    if triangles is not None and triangles.shape[-1] == 2:
-        return np.tile([0.0, 0.0, 1.0], (triangles.shape[0], 1))
     if crosses is None:
         crosses = cross(triangles)
     # unitize the cross product vectors
@@ -440,7 +435,7 @@ def extents(triangles, areas=None):
         raise ValueError("Triangles must be (n, 3, 3)!")
 
     if areas is None:
-        areas = area(triangles=triangles)
+        areas = area(triangles=triangles, sum=False)
 
     # the edge vectors which define the triangle
     a = triangles[:, 1] - triangles[:, 0]
diff --git a/trimesh/visual/gloss.py b/trimesh/visual/gloss.py
index ffad0e12..8c406568 100644
--- a/trimesh/visual/gloss.py
+++ b/trimesh/visual/gloss.py
@@ -360,7 +360,7 @@ def specular_to_pbr(
         # we need to use RGB textures, because 2 channel textures can cause problems
         result["metallicRoughnessTexture"] = toPIL(
             np.concatenate(
-                [np.zeros_like(metallic), 1.0 - glossiness, metallic], axis=-1
+                [metallic, 1.0 - glossiness, np.zeros_like(metallic)], axis=-1
             ),
             mode="RGB",
         )


Test patch:
diff --git a/tests/test_gltf.py b/tests/test_gltf.py
index 10207f89..8794c48b 100644
--- a/tests/test_gltf.py
+++ b/tests/test_gltf.py
@@ -307,8 +307,7 @@ class GLTFTest(g.unittest.TestCase):
         )
         assert metallic_roughness.shape[0] == 84 and metallic_roughness.shape[1] == 71
 
-        # https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#metallic-roughness-material
-        metallic = metallic_roughness[:, :, 2]
+        metallic = metallic_roughness[:, :, 0]
         roughness = metallic_roughness[:, :, 1]
 
         assert g.np.allclose(metallic[0, 0], 0.231, atol=0.004)