Repository: mikedh/trimesh
Pull request #714 (2020-02-08 16:40:26)

Scene Geometries
Release for recent PR's. 

- Adds `hide_geometry`, `unhide_geometry`, and `cleanup_geometries` to windowed viewer objects
- Adds `scene.delete_geometry`
- Adds `Path.centroid` method

Includes #707 #712 


Patch:
diff --git a/trimesh/exchange/obj.py b/trimesh/exchange/obj.py
index 4f3f9c9b..43722b87 100644
--- a/trimesh/exchange/obj.py
+++ b/trimesh/exchange/obj.py
@@ -698,7 +698,7 @@ def export_obj(mesh,
     mesh : trimesh.Trimesh
       Mesh to be exported
     include_normals : bool
-      Inlcude vertex normals in export
+      Include vertex normals in export
     include_color : bool
       Include vertex color in export
     include_texture bool
diff --git a/trimesh/path/path.py b/trimesh/path/path.py
index b8af8fe1..9d38154e 100644
--- a/trimesh/path/path.py
+++ b/trimesh/path/path.py
@@ -337,6 +337,20 @@ class Path(object):
                          points.max(axis=0)],
                         dtype=np.float64)
 
+    @caching.cache_decorator
+    def centroid(self):
+        """
+        Return the centroid of axis aligned bounding box enclosing
+        all entities of the path object.
+
+        Returns
+        -----------
+        centroid : (d,) float
+          Approximate centroid of the path
+        """
+        centroid = self.bounds.mean(axis=0)
+        return centroid
+
     @property
     def extents(self):
         """
@@ -1234,19 +1248,6 @@ class Path2D(Path):
         area = float(sum(i.area for i in self.polygons_full))
         return area
 
-    @caching.cache_decorator
-    def centroid(self):
-        """
-        Return the centroid of the path object.
-
-        Returns
-        -----------
-        centroid : (d,) float
-          Approximate centroid of the path
-        """
-        centroid = self.vertices.mean(axis=0)
-        return centroid
-
     def extrude(self, height, **kwargs):
         """
         Extrude the current 2D path into a 3D mesh.
diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py
index 01c6d3ad..8c43cae1 100644
--- a/trimesh/scene/scene.py
+++ b/trimesh/scene/scene.py
@@ -198,6 +198,26 @@ class Scene(Geometry):
                           geometry_flags={'visible': True})
         return node_name
 
+    def delete_geometry(self, names):
+        """
+        Delete one more multiple geometries from the scene and also
+        remove any node in the transform graph which references it.
+
+        Parameters
+        --------------
+        name : hashable
+          Name that references self.geometry
+        """
+        # make sure we have a set we can check
+        if util.is_string(names):
+            names = [names]
+        names = set(names)
+
+        # remove the geometry reference from relevant nodes
+        self.graph.remove_geometries(names)
+        # remove the geometries from our geometry store
+        [self.geometry.pop(name, None) for name in names]
+
     def md5(self):
         """
         MD5 of scene which will change when meshes or
@@ -1008,7 +1028,9 @@ class Scene(Geometry):
         if viewer is None:
             # check to see if we are in a notebook or not
             from ..viewer import in_notebook
-            viewer = ['gl', 'notebook'][int(in_notebook())]
+            viewer = 'gl'
+            if in_notebook():
+                viewer = 'notebook'
 
         if viewer == 'gl':
             # this imports pyglet, and will raise an ImportError
diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py
index edccf6ce..ed4f8ca4 100644
--- a/trimesh/scene/transforms.py
+++ b/trimesh/scene/transforms.py
@@ -95,7 +95,7 @@ class TransformForest(object):
 
         Returns
         ------------
-        copied: TransformForest
+        copied : TransformForest
         """
         copied = TransformForest()
         copied.base_frame = copy.deepcopy(self.base_frame)
@@ -281,6 +281,32 @@ class TransformForest(object):
 
         return nodes
 
+    def remove_geometries(self, geometries):
+        """
+        Remove the reference for specified geometries from nodes
+        without deleting the node.
+
+        Parameters
+        ------------
+        geometries : list or str
+          Name of scene.geometry to dereference.
+        """
+        # make sure we have a set of geometries to remove
+        if util.is_string(geometries):
+            geometries = [geometries]
+        geometries = set(geometries)
+
+        # remove the geometry reference from the node without deleting nodes
+        # this lets us keep our cached paths, and will not screw up children
+        for node, attrib in self.transforms.nodes(data=True):
+            if 'geometry' in attrib and attrib['geometry'] in geometries:
+                attrib.pop('geometry')
+
+        # it would be safer to just run _cache.clear
+        # but the only property using the geometry should be
+        # nodes_geometry: if this becomes not true change this to clear!
+        self._cache.cache.pop('nodes_geometry', None)
+
     def get(self, frame_to, frame_from=None):
         """
         Get the transform from one frame to another, assuming they are connected
diff --git a/trimesh/version.py b/trimesh/version.py
index 759f5368..8dbe2727 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,1 +1,1 @@
-__version__ = '3.5.19'
+__version__ = '3.5.20'
diff --git a/trimesh/viewer/windowed.py b/trimesh/viewer/windowed.py
index f3eda064..5f9ba31b 100644
--- a/trimesh/viewer/windowed.py
+++ b/trimesh/viewer/windowed.py
@@ -123,6 +123,8 @@ class SceneViewer(pyglet.window.Window):
         self.vertex_list_mode = {}
         # store meshes that don't rotate relative to viewer
         self.fixed = fixed
+        # store a hidden (don't not display) node.
+        self._nodes_hidden = set()
         # name : texture
         self.textures = {}
 
@@ -241,6 +243,50 @@ class SceneViewer(pyglet.window.Window):
             if tex is not None:
                 self.textures[name] = tex
 
+    def cleanup_geometries(self):
+        """
+        Remove any stored vertex lists that no longer
+        exist in the scene.
+        """
+        # shorthand to scene graph
+        graph = self.scene.graph
+        # which parts of the graph still have geometry
+        geom_keep = set([graph[node][1] for
+                         node in graph.nodes_geometry])
+        # which geometries no longer need to be kept
+        geom_delete = [geom for geom in self.vertex_list
+                       if geom not in geom_keep]
+        for geom in geom_delete:
+            # remove stored vertex references
+            self.vertex_list.pop(geom, None)
+            self.vertex_list_hash.pop(geom, None)
+            self.vertex_list_mode.pop(geom, None)
+            self.textures.pop(geom, None)
+
+    def unhide_geometry(self, node):
+        """
+        If a node is hidden remove the flag and show the
+        geometry on the next draw.
+
+        Parameters
+        -------------
+        node : str
+          Node to display
+        """
+        self._nodes_hidden.discard(node)
+
+    def hide_geometry(self, node):
+        """
+        Don't display the geometry contained at a node on
+        the next draw.
+
+        Parameters
+        -------------
+        node : str
+          Node to not display
+        """
+        self._nodes_hidden.add(node)
+
     def reset_view(self, flags=None):
         """
         Set view to the default view.
@@ -649,6 +695,9 @@ class SceneViewer(pyglet.window.Window):
             count += 1
             current_node = node_names.popleft()
 
+            if current_node in self._nodes_hidden:
+                continue
+
             # get the transform from world to geometry and mesh name
             transform, geometry_name = self.scene.graph.get(current_node)
 
diff --git a/trimesh/visual/resolvers.py b/trimesh/visual/resolvers.py
index 085575c8..f557a910 100644
--- a/trimesh/visual/resolvers.py
+++ b/trimesh/visual/resolvers.py
@@ -120,7 +120,7 @@ class ZipResolver(Resolver):
                     break
         # get the stored data
         obj = self.archive[name]
-        # if the dict is storing data as bytes just reture
+        # if the dict is storing data as bytes just return
         if isinstance(obj, (bytes, str)):
             return obj
 


Test patch:
diff --git a/tests/test_scene.py b/tests/test_scene.py
index 9183ab0e..5d13ca3e 100644
--- a/tests/test_scene.py
+++ b/tests/test_scene.py
@@ -182,6 +182,24 @@ class SceneTests(g.unittest.TestCase):
         assert len(a.geometry) == 2
         assert len(a.graph.nodes_geometry) == 2
 
+    def test_delete(self):
+        # check to make sure our geometry delete cleans up
+        a = g.trimesh.creation.icosphere()
+        b = g.trimesh.creation.icosphere().apply_translation([2, 0, 0])
+        s = g.trimesh.Scene({'a': a, 'b': b})
+
+        assert len(s.geometry) == 2
+        assert len(s.graph.nodes_geometry) == 2
+        # make sure every node has a transform
+        [s.graph[n] for n in s.graph.nodes]
+
+        # delete a geometry
+        s.delete_geometry('a')
+        assert len(s.geometry) == 1
+        assert len(s.graph.nodes_geometry) == 1
+        # if we screwed up the delete this will crash
+        [s.graph[n] for n in s.graph.nodes]
+
     def test_dupe(self):
         m = g.get_mesh('tube.obj')
 
diff --git a/tests/test_section.py b/tests/test_section.py
index 7dd87081..ab72fd8f 100644
--- a/tests/test_section.py
+++ b/tests/test_section.py
@@ -57,10 +57,13 @@ class SectionTest(g.unittest.TestCase):
 
                 # Z should be in plane frame
                 assert g.np.allclose(section.vertices[:, 2], z)
+                assert len(section.centroid) == 3
 
                 planar, to_3D = section.to_planar()
                 assert planar.is_closed
                 assert (len(planar.polygons_full) > 0)
+                assert len(planar.centroid) == 2
+
                 sections[index] = planar
                 sections_3D[index] = section