Repository: mikedh/trimesh
Pull request #1568 (2022-04-27 00:18:36)

Add test and fix for preserving colors when adding point clouds
This fixes the problem with:
```
pc1 = trimesh.PointCloud(np.random.rand(10, 3), colors=[[255, 0, 0, 255]] * 10)
pc2 = trimesh.PointCloud(np.random.rand(10, 3), colors=[[0, 255, 0, 255]] * 10)
(pc1 + pc2).show()   # this currently won't show any colored point clouds
```


Patch:
diff --git a/trimesh/path/simplify.py b/trimesh/path/simplify.py
index a6a58567..329be371 100644
--- a/trimesh/path/simplify.py
+++ b/trimesh/path/simplify.py
@@ -202,7 +202,6 @@ def merge_colinear(points, scale):
     # and direction vectors A-B, B-C, etc
     # these will be perpendicular to the vectors A-C, B-D, etc
     perp = (points[2:] - points[:-2]).T[::-1].T
-    perp[:, 0] *= -1
     perp_norm = util.row_norm(perp)
     perp_nonzero = perp_norm > tol.merge
     perp[perp_nonzero] /= perp_norm[perp_nonzero].reshape((-1, 1))
diff --git a/trimesh/points.py b/trimesh/points.py
index 3df29840..629736dc 100644
--- a/trimesh/points.py
+++ b/trimesh/points.py
@@ -646,4 +646,15 @@ class PointCloud(Geometry3D):
                            **kwargs)
 
     def __add__(self, other):
-        return PointCloud(vertices=np.vstack((self.vertices, other.vertices)))
+        if len(other.colors) == len(self.colors) == 0:
+            colors = None
+        else:
+            # preserve colors
+            # if one point cloud has no color property use black
+            other_colors = [[0, 0, 0, 255]] * \
+                len(other.vertices) if len(other.colors) == 0 else other.colors
+            self_colors = [[0, 0, 0, 255]] * \
+                len(self.vertices) if len(self.colors) == 0 else self.colors
+            colors = np.vstack((self_colors, other_colors))
+        return PointCloud(vertices=np.vstack(
+            (self.vertices, other.vertices)), colors=colors)
diff --git a/trimesh/ray/ray_pyembree.py b/trimesh/ray/ray_pyembree.py
index 5b9603c5..470cc337 100644
--- a/trimesh/ray/ray_pyembree.py
+++ b/trimesh/ray/ray_pyembree.py
@@ -144,13 +144,11 @@ class RayMeshIntersector(object):
           Intersection points, only returned if return_locations
         """
         # make sure input is _dtype for embree
-        ray_origins = np.array(
+        ray_origins = np.asanyarray(
             deepcopy(ray_origins),
             dtype=np.float64)
         ray_directions = np.asanyarray(ray_directions,
                                        dtype=np.float64)
-        if ray_origins.shape != ray_directions.shape:
-            raise ValueError('Ray origin and direction don\'t match!')
         ray_directions = util.unitize(ray_directions)
 
         # since we are constructing all hits, save them to a deque then
@@ -180,10 +178,10 @@ class RayMeshIntersector(object):
             # run the pyembree query
             # if you set output=1 it will calculate distance along
             # ray, which is bizzarely slower than our calculation
-
             query = self._scene.run(
                 ray_origins[current],
                 ray_directions[current])
+
             # basically we need to reduce the rays to the ones that hit
             # something
             hit = query != -1


Test patch:
diff --git a/tests/test_points.py b/tests/test_points.py
index 5009c9d5..a3a6333f 100644
--- a/tests/test_points.py
+++ b/tests/test_points.py
@@ -225,6 +225,35 @@ class PointsTest(g.unittest.TestCase):
         assert culled.shape == (100, 3)
         assert mask.shape == (200,)
 
+    def test_add_operator(self):
+        points_1 = g.np.random.random((10, 3))
+        points_2 = g.np.random.random((20, 3))
+        colors_1 = [[123, 123, 123, 255]] * len(points_1)
+        colors_2 = [[255, 0, 123, 255]] * len(points_2)
+
+        # Test: Both cloud's colors are preserved
+        cloud_1 = g.trimesh.points.PointCloud(points_1, colors=colors_1)
+        cloud_2 = g.trimesh.points.PointCloud(points_2, colors=colors_2)
+
+        cloud_sum = cloud_1 + cloud_2
+        assert g.np.allclose(
+            cloud_sum.colors, g.np.vstack(
+                (cloud_1.colors, cloud_2.colors)))
+
+        # Next test: Only second cloud has colors
+        cloud_1 = g.trimesh.points.PointCloud(points_1)
+        cloud_2 = g.trimesh.points.PointCloud(points_2, colors=colors_2)
+
+        cloud_sum = cloud_1 + cloud_2
+        assert g.np.allclose(cloud_sum.colors[len(cloud_1.vertices):], cloud_2.colors)
+
+        # Next test: Only first cloud has colors
+        cloud_1 = g.trimesh.points.PointCloud(points_1, colors=colors_1)
+        cloud_2 = g.trimesh.points.PointCloud(points_2)
+
+        cloud_sum = cloud_1 + cloud_2
+        assert g.np.allclose(cloud_sum.colors[:len(cloud_1.vertices)], cloud_1.colors)
+
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()
diff --git a/tests/test_simplify.py b/tests/test_simplify.py
index 8880b904..01eed496 100644
--- a/tests/test_simplify.py
+++ b/tests/test_simplify.py
@@ -24,7 +24,7 @@ class SimplifyTest(g.unittest.TestCase):
             # the simplified version shouldn't have lost area
             assert g.np.allclose(path.area,
                                  simplified.area,
-                                 rtol=1e-2)
+                                 rtol=1e-3)
 
             # see if we fit as many arcs as existed in the original drawing
             new_count = sum(int(type(i).__name__ == 'Arc')
@@ -87,17 +87,6 @@ class SimplifyTest(g.unittest.TestCase):
         simple = path_2D.simplify_spline(smooth=0.01)
         assert g.np.isclose(path_2D.area, simple.area, rtol=.01)
 
-    def test_merge_colinear(self):
-        num = 100
-        dists = g.np.linspace(0, 1000, num=num)
-        direction = g.trimesh.unitize([1, g.np.random.rand()])
-        points = direction * dists.reshape((-1, 1))
-        merged = g.trimesh.path.simplify.merge_colinear(points, scale=1000)
-        print('direction:', direction)
-        print('points:', points.shape)
-        print('merged:', merged.shape)
-        assert merged.shape[0] == 2
-
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()