Repository: mikedh/trimesh

Issue:
Export Scene Creates invalid stl file
**How to reproduce:**

- Create a empty scene;
- Add Meshes from file on it;
- export scene as stl file;

**Expected result:**

- STL file with snapshot of current scene with all added mesh.

**Current Result:**

- STL file only with header content but filled by zeros;

Sample Code:
```python
import sys
import trimesh

def main(args):
   scene = trimesh.Scene()
   temp_mesh = trimesh.load('input.stl')
   scene.add_geometry(temp_mesh)
   scene.export('output.stl')

if __name__ == '__main__':
    main(sys.argv)
```




Discussion:

Patch:
diff --git a/README.md b/README.md
index c86a292c..4c87ed50 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,14 @@
 [![trimesh](https://trimsh.org/images/logotype-a.svg)](http://trimsh.org)
 
 -----------
-[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions)  [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![codecov](https://codecov.io/gh/mikedh/trimesh/branch/master/graph/badge.svg?token=4PVRQXyl2h)](https://codecov.io/gh/mikedh/trimesh)
+[![Github Actions](https://github.com/mikedh/trimesh/workflows/Release%20Trimesh/badge.svg)](https://github.com/mikedh/trimesh/actions)  [![PyPI version](https://badge.fury.io/py/trimesh.svg)](https://badge.fury.io/py/trimesh) [![codecov](https://codecov.io/gh/mikedh/trimesh/branch/main/graph/badge.svg?token=4PVRQXyl2h)](https://codecov.io/gh/mikedh/trimesh)
 
 
 Trimesh is a pure Python (2.7-3.4+) library for loading and using [triangular meshes](https://en.wikipedia.org/wiki/Triangle_mesh) with an emphasis on watertight surfaces. The goal of the library is to provide a full featured and well tested Trimesh object which allows for easy manipulation and analysis, in the style of the Polygon object in the [Shapely library](https://github.com/Toblerity/Shapely).
 
 The API is mostly stable, but this should not be relied on and is not guaranteed: install a specific version if you plan on deploying something using trimesh.
 
-Pull requests are appreciated and responded to promptly! If you'd like to contribute, here is an [up to date list of potential enhancements](https://github.com/mikedh/trimesh/issues/199) although things not on that list are also welcome. Here are some [tips for writing mesh code in Python.](https://github.com/mikedh/trimesh/blob/master/trimesh/exchange/README.md)
+Pull requests are appreciated and responded to promptly! If you'd like to contribute, here is an [up to date list of potential enhancements](https://github.com/mikedh/trimesh/issues/199) although things not on that list are also welcome. Here are some [tips for writing mesh code in Python.](https://github.com/mikedh/trimesh/blob/main/trimesh/exchange/README.md)
 
 
 ## Basic Installation
@@ -29,7 +29,7 @@ Further information is available in the [advanced installation documentation](ht
 ## Quick Start
 
 Here is an example of loading a mesh from file and colorizing its faces. Here is a nicely formatted
-[ipython notebook version](https://trimsh.org/examples/quick_start.html) of this example. Also check out the [cross section example](https://trimsh.org/examples/section.html) or possibly the [integration of a function over a mesh example](https://github.com/mikedh/trimesh/blob/master/examples/integrate.ipynb).
+[ipython notebook version](https://trimsh.org/examples/quick_start.html) of this example. Also check out the [cross section example](https://trimsh.org/examples/section.html) or possibly the [integration of a function over a mesh example](https://github.com/mikedh/trimesh/blob/main/examples/integrate.ipynb).
 
 ```python
 import numpy as np
@@ -216,9 +216,9 @@ A question that comes up pretty frequently is [how to cite the library.](https:/
 
 ## Containers
 
-If you want to deploy something in a container that uses trimesh, automated `debian:buster-slim` based builds with trimesh and dependencies are available on [Docker Hub](https://hub.docker.com/repository/docker/trimesh/trimesh) with image tags for `latest`, git short hash for the commit in master (i.e. `trimesh/trimesh:0c1298d`), and version (i.e. `trimesh/trimesh:3.5.27`):
+If you want to deploy something in a container that uses trimesh, automated `debian:buster-slim` based builds with trimesh and dependencies are available on [Docker Hub](https://hub.docker.com/repository/docker/trimesh/trimesh) with image tags for `latest`, git short hash for the commit in main (i.e. `trimesh/trimesh:0c1298d`), and version (i.e. `trimesh/trimesh:3.5.27`):
 
 `docker pull trimesh/trimesh`
 
-[Here's an example](https://github.com/mikedh/trimesh/tree/master/examples/dockerRender) of how to render meshes using LLVMpipe and XVFB inside a container.
+[Here's an example](https://github.com/mikedh/trimesh/tree/main/examples/dockerRender) of how to render meshes using LLVMpipe and XVFB inside a container.
 
diff --git a/trimesh/exchange/export.py b/trimesh/exchange/export.py
index efabcca4..657f2615 100644
--- a/trimesh/exchange/export.py
+++ b/trimesh/exchange/export.py
@@ -218,6 +218,9 @@ def export_scene(scene,
     export : bytes
       Only returned if file_obj is None
     """
+    if len(scene.geometry) == 0:
+        raise ValueError("Can't export empty scenes!")
+
     # if we weren't passed a file type extract from file_obj
     if file_type is None:
         if util.is_string(file_obj):
@@ -245,9 +248,9 @@ def export_scene(scene,
         from trimesh.path.exchange import svg_io
         data = svg_io.export_svg(scene, **kwargs)
     elif file_type == 'ply':
-        data = export_ply(scene)
+        data = export_ply(scene.dump(concatenate=True))
     elif file_type == 'stl':
-        data = export_stl(scene)
+        data = export_stl(scene.dump(concatenate=True))
     else:
         raise ValueError(
             'unsupported export format: {}'.format(file_type))
diff --git a/trimesh/util.py b/trimesh/util.py
index 7f9a5df3..d863f234 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -1488,6 +1488,8 @@ def concatenate(a, b=None):
     # if there is only one mesh just return the first
     if len(meshes) == 1:
         return meshes[0].copy()
+    elif len(meshes) == 0:
+        return []
 
     # extract the trimesh type to avoid a circular import
     # and assert that both inputs are Trimesh objects
@@ -1518,7 +1520,6 @@ def concatenate(a, b=None):
                         face_normals=face_normals,
                         visual=visual,
                         process=False)
-
     return mesh
 
 
diff --git a/trimesh/version.py b/trimesh/version.py
index 04c47604..4bb78648 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,1 +1,1 @@
-__version__ = '3.10.4'
+__version__ = '3.10.5'


Test patch:
diff --git a/tests/test_obj.py b/tests/test_obj.py
index 2f7ebecb..bf6819c7 100644
--- a/tests/test_obj.py
+++ b/tests/test_obj.py
@@ -239,17 +239,16 @@ class OBJTest(g.unittest.TestCase):
             e = g.get_mesh('emptyIO/' + empty_file)
 
             # create export
-            export = e.export(file_type='ply')
-            reconstructed = g.wrapload(export, file_type='ply')
-
             if 'empty' in empty_file:
-                # result should be an empty scene without vertices
-                assert isinstance(e, g.trimesh.Scene)
-                assert not hasattr(e, 'vertices')
-                # export should not contain geometry
-                assert isinstance(reconstructed, g.trimesh.Scene)
-                assert not hasattr(reconstructed, 'vertices')
+                try:
+                    export = e.export(file_type='ply')
+                except BaseException:
+                    continue
+                raise ValueError('cannot export empty')
             elif 'points' in empty_file:
+                export = e.export(file_type='ply')
+                reconstructed = g.wrapload(export, file_type='ply')
+
                 # result should be a point cloud instance
                 assert isinstance(e, g.trimesh.PointCloud)
                 assert hasattr(e, 'vertices')
diff --git a/tests/test_ply.py b/tests/test_ply.py
index baca2512..aec47f39 100644
--- a/tests/test_ply.py
+++ b/tests/test_ply.py
@@ -160,19 +160,18 @@ class PlyTest(g.unittest.TestCase):
 
         for empty_file in empty_files:
             e = g.get_mesh('emptyIO/' + empty_file)
-
-            # create export
-            export = e.export(file_type='ply')
-            reconstructed = g.wrapload(export, file_type='ply')
-
             if 'empty' in empty_file:
-                # result should be an empty scene without vertices
-                assert isinstance(e, g.trimesh.Scene)
-                assert not hasattr(e, 'vertices')
-                # export should not contain geometry
-                assert isinstance(reconstructed, g.trimesh.Scene)
-                assert not hasattr(reconstructed, 'vertices')
+                # result should be an empty scene
+                try:
+                    e.export(file_type='ply')
+                except BaseException:
+                    continue
+                raise ValueError('should not export empty')
             elif 'points' in empty_file:
+                # create export
+                export = e.export(file_type='ply')
+                reconstructed = g.wrapload(export, file_type='ply')
+
                 # result should be a point cloud instance
                 assert isinstance(e, g.trimesh.PointCloud)
                 assert hasattr(e, 'vertices')
diff --git a/tests/test_scene.py b/tests/test_scene.py
index 90738a04..01fabd3b 100644
--- a/tests/test_scene.py
+++ b/tests/test_scene.py
@@ -107,9 +107,8 @@ class SceneTests(g.unittest.TestCase):
                 s.explode()
 
     def test_scaling(self):
-        """
-        Test the scaling of scenes including unit conversion.
-        """
+        # Test the scaling of scenes including unit conversion.
+
         scene = g.get_mesh('cycloidal.3DXML')
 
         md5 = scene.md5()
@@ -296,10 +295,9 @@ class SceneTests(g.unittest.TestCase):
         assert len(n) == 0
 
     def test_zipped(self):
-        """
-        Make sure a zip file with multiple file types
-        is returned as a single scene.
-        """
+        # Make sure a zip file with multiple file types
+        # is returned as a single scene.
+
         # allow mixed 2D and 3D geometry
         m = g.get_mesh('scenes.zip', mixed=True)
 
@@ -372,6 +370,26 @@ class SceneTests(g.unittest.TestCase):
         s = g.get_mesh('box.obj', group_material=False)
         assert set(s.geometry.keys()) != {'Material', 'SecondMaterial'}
 
+    def test_export_concat(self):
+        # Scenes exported in mesh formats should be
+        # concatenating the meshes somewhere.
+        original = g.trimesh.creation.icosphere(
+            radius=0.123312)
+        original_hash = original.identifier_md5
+
+        scene = g.trimesh.Scene()
+        scene.add_geometry(original)
+
+        with g.TemporaryDirectory() as d:
+            for ext in ['stl', 'ply']:
+                file_name = g.os.path.join(d, 'mesh.' + ext)
+                scene.export(file_name)
+                loaded = g.trimesh.load(file_name)
+                assert g.np.isclose(loaded.volume,
+                                    original.volume)
+        # nothing should have changed
+        assert original.identifier_md5 == original_hash
+
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()
diff --git a/tests/test_stl.py b/tests/test_stl.py
index 9e90f52d..a0f8c3c0 100644
--- a/tests/test_stl.py
+++ b/tests/test_stl.py
@@ -84,11 +84,11 @@ class STLTests(g.unittest.TestCase):
             assert not hasattr(e, 'vertices')
 
             # create export
-            export = e.export(file_type='ply')
-            reconstructed = g.wrapload(export, file_type='ply')
-            # export should not contain geometry
-            assert isinstance(reconstructed, g.trimesh.Scene)
-            assert not hasattr(reconstructed, 'vertices')
+            try:
+                e.export(file_type='ply')
+            except BaseException:
+                return
+            raise ValueError("Shouldn't export empty scenes!")
 
 
 if __name__ == '__main__':