Repository: mikedh/trimesh

Issue:
Error in Version 3.6.24 - AttributeError: 'VertexColor' object has no attribute 'crc'
This is a regression introduced recently. It happens with v3.6.24 and not on v3.6.20.

```
AttributeError                            Traceback (most recent call last)
<ipython-input-6-88efc28c4028> in <module>
      7 scene.add_geometry(.....)
      8 
----> 9 scene.show(viewer='gl')
     10 Image(scene.save_image())

~/venv3.7/lib/python3.7/site-packages/trimesh/scene/scene.py in show(self, viewer, **kwargs)
   1088             # if pyglet is not available
   1089             from ..viewer import SceneViewer
-> 1090             return SceneViewer(self, **kwargs)
   1091         elif viewer == 'notebook':
   1092             from ..viewer import scene_to_notebook

~/venv3.7/lib/python3.7/site-packages/trimesh/viewer/windowed.py in __init__(self, scene, smooth, flags, visible, resolution, start_loop, callback, callback_period, caption, fixed, offset_lines, background, window_conf, profile, **kwargs)
    168 
    169         # add scene geometry to viewer geometry
--> 170         self._update_vertex_list()
    171 
    172         # call after geometry is added

~/venv3.7/lib/python3.7/site-packages/trimesh/viewer/windowed.py in _update_vertex_list(self)
    197             if geom.is_empty:
    198                 continue
--> 199             if geometry_hash(geom) == self.vertex_list_hash.get(name):
    200                 continue
    201             self.add_geometry(name=name,

~/venv3.7/lib/python3.7/site-packages/trimesh/viewer/windowed.py in geometry_hash(geometry)
    815     if hasattr(geometry, 'visual'):
    816         # if visual properties are defined
--> 817         md5 += str(geometry.visual.crc())
    818     return md5
    819 

AttributeError: 'VertexColor' object has no attribute 'crc'
```


Discussion:

Patch:
diff --git a/README.md b/README.md
index d6d197a6..861fa6a0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 [![trimesh](https://trimsh.org/images/logotype-a.svg)](http://trimsh.org)
 
 -----------
-[![Github Actions](https://github.com/mikedh/trimesh/workflows/Run%20Tests/badge.svg)](https://github.com/mikedh/trimesh/actions) [![Coverage Status](https://coveralls.io/repos/github/mikedh/trimesh/badge.svg)](https://coveralls.io/github/mikedh/trimesh) [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![Join the chat at https://gitter.im/trimsh/Lobby](https://badges.gitter.im/trimsh/Lobby.svg)](https://gitter.im/trimsh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions) [![Coverage Status](https://coveralls.io/repos/github/mikedh/trimesh/badge.svg)](https://coveralls.io/github/mikedh/trimesh) [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![Join the chat at https://gitter.im/trimsh/Lobby](https://badges.gitter.im/trimsh/Lobby.svg)](https://gitter.im/trimsh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
 
 
diff --git a/examples/dockerRender/render.supervisor.conf b/examples/dockerRender/render.supervisor.conf
index 541e1ddc..15063c51 100644
--- a/examples/dockerRender/render.supervisor.conf
+++ b/examples/dockerRender/render.supervisor.conf
@@ -2,7 +2,7 @@
 nodaemon=true ;
 
 [program:render_worker]
-command=/home/user/conda/bin/python -u /home/user/render.py
+command=python -u /home/user/render.py
 stdout_logfile=/dev/stdout
 stdout_logfile_maxbytes=0
 stderr_logfile=/dev/stderr
diff --git a/trimesh/parent.py b/trimesh/parent.py
index 39d7b941..12472914 100644
--- a/trimesh/parent.py
+++ b/trimesh/parent.py
@@ -40,6 +40,14 @@ class Geometry(ABC):
     def is_empty(self):
         pass
 
+    @abc.abstractmethod
+    def crc(self):
+        pass
+
+    @abc.abstractmethod
+    def md5(self):
+        pass
+
     def __repr__(self):
         """
         Print quick summary of the current geometry without
diff --git a/trimesh/points.py b/trimesh/points.py
index 07bcdb9b..c12a4106 100644
--- a/trimesh/points.py
+++ b/trimesh/points.py
@@ -475,6 +475,17 @@ class PointCloud(Geometry):
         """
         return self._data.md5()
 
+    def crc(self):
+        """
+        Get a CRC hash of the current vertices.
+
+        Returns
+        ----------
+        crc : int
+          Hash of self.vertices
+        """
+        return self._data.crc()
+
     def merge_vertices(self):
         """
         Merge vertices closer than tol.merge (default: 1e-8)
diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py
index a12cfc5c..ece0440b 100644
--- a/trimesh/scene/scene.py
+++ b/trimesh/scene/scene.py
@@ -234,6 +234,12 @@ class Scene(Geometry):
           MD5 hash of scene
         """
         # start with transforms hash
+        return util.md5_object(self._hashable())
+
+    def crc(self):
+        return caching.crc32(self._hashable())
+
+    def _hashable(self):
         hashes = [self.graph.md5()]
         for g in self.geometry.values():
             if hasattr(g, 'md5'):
@@ -244,10 +250,8 @@ class Scene(Geometry):
                 # try to just straight up hash
                 # this may raise errors
                 hashes.append(str(hash(g)))
-
-        md5 = util.md5_object(''.join(hashes))
-
-        return md5
+        hashable = ''.join(sorted(hashes)).encode('utf-8')
+        return hashable
 
     @property
     def is_empty(self):
diff --git a/trimesh/version.py b/trimesh/version.py
index 4ec5c6fa..f460593b 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,1 +1,1 @@
-__version__ = '3.6.24'
+__version__ = '3.6.25'
diff --git a/trimesh/visual/base.py b/trimesh/visual/base.py
index 20f34ffd..37424c41 100644
--- a/trimesh/visual/base.py
+++ b/trimesh/visual/base.py
@@ -30,6 +30,10 @@ class Visuals(ABC):
     def concatenate(self, other):
         pass
 
+    @abc.abstractmethod
+    def crc(self):
+        pass
+
     @abc.abstractmethod
     def copy(self):
         pass
diff --git a/trimesh/visual/color.py b/trimesh/visual/color.py
index 0e6e1d0e..fd418db9 100644
--- a/trimesh/visual/color.py
+++ b/trimesh/visual/color.py
@@ -490,6 +490,9 @@ class VertexColor(Visuals):
     def kind(self):
         return 'vertex'
 
+    def crc(self):
+        return self._colors.crc()
+
     def update_vertices(self, mask):
         if self._colors is not None:
             self._colors = self._colors[mask]
@@ -504,13 +507,14 @@ class VertexColor(Visuals):
     @vertex_colors.setter
     def vertex_colors(self, data):
         if data is None:
-            self._colors = None
+            self._colors = caching.tracked_array(None)
         else:
             # tile single color into color array
             data = np.asanyarray(data)
             if data.shape in [(3,), (4,)]:
                 data = np.tile(data, (len(self.obj.vertices), 1))
-            self._colors = data
+            # track changes in colors and convert to RGBA
+            self._colors = caching.tracked_array(to_rgba(data))
 
     def copy(self):
         """


Test patch:
diff --git a/tests/generic.py b/tests/generic.py
index 1737c08a..e3861220 100644
--- a/tests/generic.py
+++ b/tests/generic.py
@@ -3,6 +3,12 @@
 Module which contains most imports and data unit tests
 might need, to reduce the amount of boilerplate.
 """
+from trimesh.base import Trimesh
+from trimesh.constants import tol, tol_path
+from collections import deque
+from copy import deepcopy
+import collections
+import trimesh
 from distutils.spawn import find_executable
 import os
 import sys
@@ -34,18 +40,20 @@ except ImportError:
 
 import numpy as np
 
+# should we require all soft dependencies
+# this is set in the docker images to catch missing packages
+all_dep = 'alldep' in ''.join(sys.argv)
+
+if all_dep:
+    # make sure pyembree is importable
+    from pyembree import rtcore_scene
+
 try:
     import sympy as sp
-except ImportError:
-    pass
-
-import trimesh
-import collections
+except ImportError as E:
+    if all_dep:
+        raise E
 
-from copy import deepcopy
-from collections import deque
-from trimesh.constants import tol, tol_path
-from trimesh.base import Trimesh
 
 # make sure functions know they should run additional
 # potentially slow validation checks and raise exceptions
@@ -53,20 +61,21 @@ trimesh.util._STRICT = True
 trimesh.constants.tol.strict = True
 trimesh.constants.tol_path.strict = True
 
-# should we require all soft dependencies
-# this is set in the docker images to catch missing packages
-all_dep = 'alldep' in ''.join(sys.argv)
-
-if all_dep:
-    # make sure pyembree is importable
-    from pyembree import rtcore_scene
 
 try:
     from shapely.geometry import Point, Polygon, LineString
     has_path = True
-except ImportError:
+except ImportError as E:
+    if all_dep:
+        raise E
     has_path = False
 
+try:
+    from scipy import spatial, csgraph
+except BaseException as E:
+    if all_dep:
+        raise E
+
 # find_executable for binvox
 has_binvox = trimesh.exchange.binvox.binvox_encoder is not None
 
diff --git a/tests/test_points.py b/tests/test_points.py
index 6ca16bbd..a2663324 100644
--- a/tests/test_points.py
+++ b/tests/test_points.py
@@ -66,14 +66,6 @@ class PointsTest(g.unittest.TestCase):
         assert g.np.allclose(cloud.vertices,
                              cloud.copy().vertices)
 
-    def test_init_arguments(self):
-        vertices = [[0, 0, 0], [1, 1, 1]]
-        colors = [[0, 0, 1.0], [1.0, 0, 0]]
-
-        # introduced colors
-        p = g.trimesh.PointCloud(vertices=vertices, colors=colors)
-        g.np.testing.assert_allclose(p.colors, colors)
-
     def test_empty(self):
         p = g.trimesh.PointCloud(None)
         assert p.is_empty
@@ -201,14 +193,22 @@ class PointsTest(g.unittest.TestCase):
         p = g.get_mesh('points_agisoft.xyz')
         assert isinstance(p, g.trimesh.PointCloud)
         assert len(p.vertices) > 0
-        p.visual.colors = (g.np.random.random(
-            (len(p.vertices), 4)) * 255).astype(g.np.uint8)
+
+        # initial color CRC
+        initial = p.visual.crc()
+        # set to random colors
+        p.colors = g.np.random.random(
+            (len(p.vertices), 4))
+        # visual CRC should have changed
+        assert p.visual.crc() != initial
+
         # test exporting a pointcloud to a PLY file
         r = g.trimesh.load(g.trimesh.util.wrap_as_stream(
             p.export(file_type='ply')),
             file_type='ply')
         assert r.vertices.shape == p.vertices.shape
         # make sure colors survived the round trip
+
         assert g.np.allclose(r.colors, p.colors)
 
     def test_remove_close(self):
diff --git a/tests/test_scene.py b/tests/test_scene.py
index 5d13ca3e..0cb72103 100644
--- a/tests/test_scene.py
+++ b/tests/test_scene.py
@@ -28,6 +28,8 @@ class SceneTests(g.unittest.TestCase):
             assert scene_split.md5() == pre[0]
             assert scene_base.md5() == pre[1]
 
+            assert isinstance(scene_base.crc(), int)
+
             # try out scene appending
             concat = scene_split + scene_base