Repository: mikedh/trimesh

Issue:
trimesh.exchange.load.load_mesh closes passed in file objects
https://github.com/mikedh/trimesh/blob/f5ac70d1e289e262946bc908ca78053b33ad19c9/trimesh/exchange/load.py#L211-L212

In all other instances `opened` is evaluated to determine whether the file should be closed. For consistency I believe this should be:

```
        if util.is_file(file_obj) and opened:
            file_obj.close()
```

Discussion:

Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5cbf8a7c..a3199b86 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,37 +29,14 @@ jobs:
         autopep8 --recursive --aggressive --diff --exit-code examples/
         autopep8 --recursive --aggressive --diff --exit-code tests/
 
-  wintests:
-    name: Run Windows Unit Tests
-    needs: formatting
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
-        os: [windows-latest]
-    steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v1
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install Pytest
-      run: |
-        python -m pip install --upgrade pip
-        pip install pytest pytest-cov setuptools
-    - name: Install Trimesh Minimal
-      run: pip install .[easy]
-    - name: Run Pytest
-      run: python tests/test_inertia.py
-
   tests:
     name: Run Unit Tests
     needs: formatting
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
-        os: [ubuntu-latest, macos-latest]
+        python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9]
+        os: [ubuntu-latest, macos-latest, windows-latest]
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python ${{ matrix.python-version }}
diff --git a/trimesh/collision.py b/trimesh/collision.py
index 068cde33..2257bc35 100644
--- a/trimesh/collision.py
+++ b/trimesh/collision.py
@@ -158,7 +158,7 @@ class CollisionManager(object):
         Initialize a mesh-mesh collision manager.
         """
         if fcl is None:
-            raise ValueError('No FCL Available!')
+            raise ValueError('No FCL Available! Please install the python-fcl library')
         # {name: {geom:, obj}}
         self._objs = {}
         # {id(bvh) : str, name}
diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py
index abfa2356..d0ef013e 100644
--- a/trimesh/exchange/gltf.py
+++ b/trimesh/exchange/gltf.py
@@ -1294,7 +1294,7 @@ def _read_buffers(header, buffers, mesh_kwargs, merge_primitives=False, resolver
                     kwargs['frame_to'] = frame_to
                     # append the edge with the mesh frame
                     graph.append(kwargs.copy())
-            else:
+            elif len(geometries) == 1:
                 kwargs["geometry"] = geometries[0]
                 if 'name' in child:
                     kwargs['frame_to'] = names[b]
diff --git a/trimesh/exchange/load.py b/trimesh/exchange/load.py
index df152642..e85c9dea 100644
--- a/trimesh/exchange/load.py
+++ b/trimesh/exchange/load.py
@@ -208,9 +208,6 @@ def load_mesh(file_obj,
                                           resolver=resolver,
                                           **kwargs)
 
-        if util.is_file(file_obj):
-            file_obj.close()
-
         if not isinstance(results, list):
             results = [results]
 
diff --git a/trimesh/path/exchange/load.py b/trimesh/path/exchange/load.py
index 21aeb895..7089d642 100644
--- a/trimesh/path/exchange/load.py
+++ b/trimesh/path/exchange/load.py
@@ -40,7 +40,6 @@ def load_path(obj, file_type=None, **kwargs):
         # for open file objects use loaders
         kwargs.update(path_loaders[file_type](
             obj, file_type=file_type))
-        obj.close()
     elif util.is_string(obj):
         # strings passed are evaluated as file objects
         with open(obj, 'rb') as file_obj:
diff --git a/trimesh/util.py b/trimesh/util.py
index 3540e861..30b397fb 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -1955,7 +1955,7 @@ def decompress(file_obj, file_type):
     raise ValueError('Unsupported type passed!')
 
 
-def compress(info):
+def compress(info, **kwargs):
     """
     Compress data stored in a dict.
 
@@ -1964,7 +1964,8 @@ def compress(info):
     info : dict
       Data to compress in form:
       {file name in archive: bytes or file-like object}
-
+    kwargs : dict
+      Passed to zipfile.ZipFile
     Returns
     -----------
     compressed : bytes
@@ -1978,7 +1979,7 @@ def compress(info):
     with zipfile.ZipFile(
             file_obj,
             mode='w',
-            compression=zipfile.ZIP_DEFLATED) as zipper:
+            compression=zipfile.ZIP_DEFLATED, **kwargs) as zipper:
         for name, data in info.items():
             if hasattr(data, 'read'):
                 # if we were passed a file object, read it
@@ -2178,24 +2179,26 @@ def unique_id(length=12, increment=0):
     return unique
 
 
-def generate_basis(z):
+def generate_basis(z, epsilon=1e-12):
     """
     Generate an arbitrary basis (also known as a coordinate frame)
     from a given z-axis vector.
 
     Parameters
     ------------
-    z: (3,) float
-      A vector along the positive z-axis
+    z : (3,) float
+      A vector along the positive z-axis.
+    epsilon : float
+      Numbers smaller than this considered zero.
 
     Returns
     ---------
     x : (3,) float
-      Vector along x axis
+      Vector along x axis.
     y : (3,) float
-      Vector along y axis
+      Vector along y axis.
     z : (3,) float
-      Vector along z axis
+      Vector along z axis.
     """
     # get a copy of input vector
     z = np.array(z, dtype=np.float64, copy=True)
@@ -2203,13 +2206,17 @@ def generate_basis(z):
     if z.shape != (3,):
         raise ValueError('z must be (3,) float!')
 
+    z_norm = np.linalg.norm(z)
+    if z_norm < epsilon:
+        return np.eye(3)
+
     # normalize vector in-place
-    z /= np.linalg.norm(z)
+    z /= z_norm
     # X as arbitrary perpendicular vector
     x = np.array([-z[1], z[0], 0.0])
     # avoid degenerate case
     x_norm = np.linalg.norm(x)
-    if x_norm < 1e-12:
+    if x_norm < epsilon:
         # this means that
         # so a perpendicular X is just X
         x = np.array([-z[2], z[1], 0.0])
diff --git a/trimesh/version.py b/trimesh/version.py
index f7daba47..971f710e 100644
--- a/trimesh/version.py
+++ b/trimesh/version.py
@@ -1,1 +1,1 @@
-__version__ = '3.8.19'
+__version__ = '3.8.20'


Test patch:
diff --git a/tests/test_loaded.py b/tests/test_loaded.py
index 5cbd4518..268b8288 100644
--- a/tests/test_loaded.py
+++ b/tests/test_loaded.py
@@ -39,6 +39,36 @@ class LoaderTest(g.unittest.TestCase):
         assert len(m.faces) > 0
         assert m.area > 1e-5
 
+    def test_fileobj(self):
+        # make sure we don't close file objects that were passed
+        # check load_mesh
+        file_obj = open(
+            g.os.path.join(g.dir_models, 'featuretype.STL'), 'rb')
+        assert not file_obj.closed
+        mesh = g.trimesh.load(
+            file_obj=file_obj,
+            file_type='stl')
+        # should have actually loaded the mesh
+        assert len(mesh.faces) == 3476
+        # should not close the file object
+        assert not file_obj.closed
+        # clean up
+        file_obj.close()
+
+        # check load_path
+        file_obj = open(
+            g.os.path.join(g.dir_models, '2D', 'wrench.dxf'), 'rb')
+        assert not file_obj.closed
+        path = g.trimesh.load(
+            file_obj=file_obj,
+            file_type='dxf')
+        assert g.np.isclose(path.area, 1.667, atol=1e-2)
+        # should have actually loaded the path
+        # should not close the file object
+        assert not file_obj.closed
+        # clean up
+        file_obj.close()
+
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()