Repository: mikedh/trimesh
Pull request #1918 (2023-05-03 13:14:23)

Checks that only chunks having faces are added to faces tuples
Covers a rare case where a chunk could contain 'f ' without having a face when loading an obj.

This rare case happens when a comment in a group having no faces contains 'f '.

The fix checks that at least one line in the chunk starts with 'f ', which is the proper definition of a face.

An alternative solution would be to remove comment lines (starting with #) but checking for the actual occurence of at least one face felt more appropriate.


Patch:
diff --git a/README.md b/README.md
index afc64497..ab6afec2 100644
--- a/README.md
+++ b/README.md
@@ -189,7 +189,7 @@ You can check out the [Github network](https://github.com/mikedh/trimesh/network
 - [urdfpy](https://github.com/mmatl/urdfpy) Load URDF robot descriptions in Python.
 - [moderngl-window](https://github.com/moderngl/moderngl-window) A helper to create GL contexts and load meshes.
 - [vedo](https://github.com/marcomusy/vedo) Visualize meshes interactively (see example [gallery](https://github.com/marcomusy/vedo/tree/master/examples/other/trimesh/)).
-- [FSLeyes](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FSLeyes) View MRI images and brain data.
+- [fsleyes](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/userdoc/latest/quick_start.html) View MRI images and brain data.
 
 ## Which Mesh Format Should I Use?
 
diff --git a/docker/trimesh-setup b/docker/trimesh-setup
index 5ce3335f..3de7fea3 100644
--- a/docker/trimesh-setup
+++ b/docker/trimesh-setup
@@ -8,7 +8,6 @@ this exact configuration.
 """
 import os
 import sys
-import json
 import shutil
 import tarfile
 import tempfile
@@ -21,91 +20,69 @@ from fnmatch import fnmatch
 # define system packages for our debian docker image
 # someday possibly add this to the `pyproject.toml` config
 # but for now store them locally in the installer script
-config_json = """
-{
-  "apt": {
-    "base": [
-      "wget",
-      "curl",
-      "xz-utils",
-      "git"
-    ],
-    "build": [
-      "build-essential",
-      "g++",
-      "make"
-    ],
-    "docs": [
-      "make",
-      "pandoc"
-    ],
-    "llvmpipe": [
-      "libgl1-mesa-glx",
-      "libgl1-mesa-dri",
-      "xvfb",
-      "xauth",
-      "ca-certificates",
-      "freeglut3-dev"
-    ],
-    "test": [
-      "blender",
-      "openscad"
-    ]
-  },
-  "fetch": {
-    "embree2": {
-      "url": "https://github.com/embree/embree/releases/download/v2.17.7/embree-2.17.7.x86_64.linux.tar.gz",
-      "sha256": "2c4bdacd8f3c3480991b99e85b8f584975ac181373a75f3e9675bf7efae501fe",
-      "target": "/usr/local",
-      "extract_skip": [
-        "bin/*",
-        "doc/*"
-      ],
-      "strip_components": 1,
-      "chmod": 755
-    },
-    "embree3": {
-      "url": "https://github.com/embree/embree/releases/download/v3.12.1/embree-3.12.1.x86_64.linux.tar.gz",
-      "sha256": "5e218dd4e95c035c04aa893f7b1169ade11ecc57580e0027eae2ec84cdc9baff",
-      "target": "/usr/local",
-      "extract_skip": [
-        "bin/*",
-        "doc/*"
-      ],
-      "strip_components": 1
-    },
-    "embree4": {
-      "url": "https://github.com/embree/embree/releases/download/v4.0.0/embree-4.0.0.x86_64.linux.tar.gz",
-      "sha256": "524842e2f141dca0db584c33a0821176373e7058f3ec2201bfb19d9e9a1b80b9",
-      "target": "/usr/local",
-      "extract_skip": [
-        "bin/*",
-        "doc/*"
-      ],
-      "strip_components": 1
-    },
-    "gltf_validator": {
-      "url": "https://github.com/KhronosGroup/glTF-Validator/releases/download/2.0.0-dev.3.8/gltf_validator-2.0.0-dev.3.8-linux64.tar.xz",
-      "sha256": "374c7807e28fe481b5075f3bb271f580ddfc0af3e930a0449be94ec2c1f6f49a",
-      "target": "$PATH",
-      "chmod": 755,
-      "extract_only": "gltf_validator"
-    },
-    "pandoc": {
-      "url": "https://github.com/jgm/pandoc/releases/download/3.1.1/pandoc-3.1.1-linux-amd64.tar.gz",
-      "sha256": "52b25f0115517e32047a06d821e63729108027bd06d9605fe8eac0fa83e0bf81",
-      "target": "$PATH",
-      "chmod": 755,
-      "extract_only": "pandoc"
-    },
-    "binvox": {
-      "url": "https://trimesh.s3-us-west-1.amazonaws.com/binvox",
-      "sha256": "82ee314a75986f67f1d2b5b3ccdfb3661fe57a6b428aa0e0f798fdb3e1734fe0",
-      "target": "$PATH",
-      "chmod": 755
-    }
-  }
-}
+config_toml = """
+[tool.trimesh-setup.apt]
+# for running with `trimesh-setup` usually in a Docker image
+# here are named groups of `apt-get install` Debian packages
+base = ["wget", "curl", "xz-utils", "git"]
+build = ["build-essential", "g++", "make"]
+docs = ["make", "pandoc"]
+llvmpipe = [
+  "libgl1-mesa-glx",
+  "libgl1-mesa-dri",
+  "xvfb",
+  "xauth",
+  "ca-certificates",
+  "freeglut3-dev"
+]
+test = ["blender", "openscad"]
+
+# libraries and tools that need to be downloaded are under `fetch`
+[tool.trimesh-setup.fetch.embree2]
+url = "https://github.com/embree/embree/releases/download/v2.17.7/embree-2.17.7.x86_64.linux.tar.gz"
+sha256 = "2c4bdacd8f3c3480991b99e85b8f584975ac181373a75f3e9675bf7efae501fe"
+target = "/usr/local"
+# the `bin` subdirectory is not required
+extract_skip = [ "bin/*", "doc/*", ]
+# skip the first component of the path in the archive
+# same behavior as in the `tar` command line interface
+strip_components = 1
+chmod = 755
+
+[tool.trimesh-setup.fetch.embree3]
+url = 'https://github.com/embree/embree/releases/download/v3.12.1/embree-3.12.1.x86_64.linux.tar.gz'
+sha256 = '5e218dd4e95c035c04aa893f7b1169ade11ecc57580e0027eae2ec84cdc9baff'
+target = "/usr/local"
+extract_skip = [ "bin/*", "doc/*", ]
+strip_components = 1
+
+
+[tool.trimesh-setup.fetch.embree4]
+url = 'https://github.com/embree/embree/releases/download/v4.0.0/embree-4.0.0.x86_64.linux.tar.gz'
+sha256 = '524842e2f141dca0db584c33a0821176373e7058f3ec2201bfb19d9e9a1b80b9'
+target = "/usr/local"
+extract_skip = [ "bin/*", "doc/*", ]
+strip_components = 1
+
+[tool.trimesh-setup.fetch.gltf_validator]
+url = 'https://github.com/KhronosGroup/glTF-Validator/releases/download/2.0.0-dev.3.8/gltf_validator-2.0.0-dev.3.8-linux64.tar.xz'
+sha256 = '374c7807e28fe481b5075f3bb271f580ddfc0af3e930a0449be94ec2c1f6f49a'
+target = "$PATH"
+chmod = 755
+extract_only = "gltf_validator"
+
+[tool.trimesh-setup.fetch.pandoc]
+url = 'https://github.com/jgm/pandoc/releases/download/3.1.1/pandoc-3.1.1-linux-amd64.tar.gz'
+sha256 = '52b25f0115517e32047a06d821e63729108027bd06d9605fe8eac0fa83e0bf81'
+target = "$PATH"
+chmod = 755
+extract_only = "pandoc"
+
+[tool.trimesh-setup.fetch.binvox]
+url = 'https://trimesh.s3-us-west-1.amazonaws.com/binvox'
+sha256 = '82ee314a75986f67f1d2b5b3ccdfb3661fe57a6b428aa0e0f798fdb3e1734fe0'
+target = "$PATH"
+chmod = 755
 """
 
 
@@ -367,11 +344,11 @@ def handle_fetch(url,
             # python os.chmod takes an octal value
             os.chmod(path, int(str(chmod), base=8))
 
-
 def load_config():
     """
     """
-    return json.loads(config_json)
+    from tomllib import loads
+    return loads(config_toml).get('tool', {}).get('trimesh-setup', {})
 
 
 if __name__ == '__main__':
@@ -388,7 +365,7 @@ if __name__ == '__main__':
         '--install',
         type=str,
         action='append',
-        help=f'Install metapackages: {options}')
+        help=f'Install packages: {options}')
     args = parser.parse_args()
 
     # collect `apt-get install`-able package
@@ -398,12 +375,11 @@ if __name__ == '__main__':
 
     # allow comma delimeters and de-duplicate
     if args.install is None:
-        parser.print_help()
-        exit()
+        select = set()
     else:
         select = set(' '.join(args.install).replace(',', ' ').split())
 
-    log.debug(f'installing metapackages: `{", ".join(select)}`')
+    log.debug(f'installing metapackages: {select}')
 
     for key in select:
         for handle_name, handler in handlers.items():
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 3bbfa416..14b15cb6 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -4,9 +4,9 @@ sphinx==6.1.3
 jupyter==1.0.0
 sphinx_rtd_theme==1.2.0
 myst-parser==1.0.0
-pyopenssl==23.1.1
+pyopenssl==23.1.0
 autodocsumm==0.2.10
 jinja2==3.1.2
 matplotlib==3.7.1
-nbconvert==7.3.1
+nbconvert==7.2.10
 
diff --git a/models/obj_with_no_face_in_chunk.obj b/models/obj_with_no_face_in_chunk.obj
new file mode 100644
index 00000000..1a185552
--- /dev/null
+++ b/models/obj_with_no_face_in_chunk.obj
@@ -0,0 +1,9 @@
+g group1
+# of and space
+v 1 4 5
+v 1 2 3
+v 4 5 6
+
+g group2
+usemtl mat-1
+f 1 2 3
\ No newline at end of file
diff --git a/trimesh/caching.py b/trimesh/caching.py
index a821bab4..1f7509f7 100644
--- a/trimesh/caching.py
+++ b/trimesh/caching.py
@@ -240,8 +240,14 @@ class TrackedArray(np.ndarray):
             # we have a valid hash without recomputing.
             return self._hashed
 
-        # run a hashing function on the C-order bytes copy
-        hashed = hash_fast(self.tobytes(order='C'))
+        if self.flags['C_CONTIGUOUS']:
+            hashed = hash_fast(self)
+        else:
+            # the case where we have sliced our nice
+            # contiguous array into a non- contiguous block
+            # for example (note slice *after* track operation):
+            # t = tracked_array(np.random.random(10))[::-1]
+            hashed = hash_fast(np.ascontiguousarray(self))
 
         # assign the value and set the flag
         self._hashed = hashed
@@ -266,41 +272,6 @@ class TrackedArray(np.ndarray):
         return super(self.__class__, self).__isub__(
             *args, **kwargs)
 
-    def fill(self, *args, **kwargs):
-        self._dirty_hash = True
-        return super(self.__class__, self).fill(
-            *args, **kwargs)
-
-    def partition(self, *args, **kwargs):
-        self._dirty_hash = True
-        return super(self.__class__, self).partition(
-            *args, **kwargs)
-
-    def put(self, *args, **kwargs):
-        self._dirty_hash = True
-        return super(self.__class__, self).put(
-            *args, **kwargs)
-
-    def byteswap(self, *args, **kwargs):
-        self._dirty_hash = True
-        return super(self.__class__, self).byteswap(
-            *args, **kwargs)
-
-    def itemset(self, *args, **kwargs):
-        self._dirty_hash = True
-        return super(self.__class__, self).itemset(
-            *args, **kwargs)
-
-    def sort(self, *args, **kwargs):
-        self._dirty_hash = True
-        return super(self.__class__, self).sort(
-            *args, **kwargs)
-
-    def setflags(self, *args, **kwargs):
-        self._dirty_hash = True
-        return super(self.__class__, self).setflags(
-            *args, **kwargs)
-
     def __imul__(self, *args, **kwargs):
         self._dirty_hash = True
         return super(self.__class__, self).__imul__(
@@ -363,12 +334,12 @@ class TrackedArray(np.ndarray):
 
     def __setitem__(self, *args, **kwargs):
         self._dirty_hash = True
-        return super(self.__class__, self).__setitem__(
+        super(self.__class__, self).__setitem__(
             *args, **kwargs)
 
     def __setslice__(self, *args, **kwargs):
         self._dirty_hash = True
-        return super(self.__class__, self).__setslice__(
+        super(self.__class__, self).__setslice__(
             *args, **kwargs)
 
 
diff --git a/trimesh/convex.py b/trimesh/convex.py
index d6e4091d..ee896811 100644
--- a/trimesh/convex.py
+++ b/trimesh/convex.py
@@ -197,10 +197,6 @@ def is_convex(mesh):
     if not mesh.is_watertight:
         return False
 
-    # meshes with multiple bodies are not convex
-    if mesh.body_count != 1:
-        return False
-
     # don't consider zero- area faces
     nonzero = mesh.area_faces > tol.zero
     # adjacencies with two nonzero faces
diff --git a/trimesh/creation.py b/trimesh/creation.py
index b7c06de0..5fe11fe6 100644
--- a/trimesh/creation.py
+++ b/trimesh/creation.py
@@ -164,7 +164,6 @@ def revolve(linestring,
         # it should really be a valid volume, unless the sign
         # reversed on the input linestring
         assert mesh.is_volume
-        assert mesh.body_count == 1
 
     return mesh
 
@@ -706,7 +705,10 @@ def icosphere(subdivisions=3, radius=1.0, color=None):
     return ico
 
 
-def uv_sphere(radius=1.0, count=None, transform=None):
+def uv_sphere(radius=1.0,
+              count=None,
+              theta=None,
+              phi=None):
     """
     Create a UV sphere (latitude + longitude) centered at the
     origin. Roughly one order of magnitude faster than an
@@ -718,14 +720,16 @@ def uv_sphere(radius=1.0, count=None, transform=None):
       Radius of sphere
     count : (2,) int
       Number of latitude and longitude lines
+    theta : (n,) float
+      Optional theta angles in radians
+    phi :   (n,) float
+      Optional phi angles in radians
 
     Returns
     ----------
     mesh : trimesh.Trimesh
        Mesh of UV sphere with specified parameters
     """
-
-    # set the resolution of the uv sphere
     if count is None:
         count = np.array([32, 64], dtype=np.int64)
     else:
@@ -733,16 +737,50 @@ def uv_sphere(radius=1.0, count=None, transform=None):
         count += np.mod(count, 2)
         count[1] *= 2
 
-    # generate the 2D curve for the UV sphere
-    theta = np.linspace(0.0, np.pi, num=count[0])
-    linestring = np.column_stack((np.sin(theta), -np.cos(theta))) * radius
-
-    # revolve the curve to create a volume
-    return revolve(linestring=linestring,
-                   sections=count[1],
-                   transform=transform,
+    # generate vertices on a sphere using spherical coordinates
+    if theta is None:
+        theta = np.linspace(0, np.pi, count[0])
+    if phi is None:
+        phi = np.linspace(0, np.pi * 2, count[1])[:-1]
+    spherical = np.dstack((np.tile(phi, (len(theta), 1)).T,
+                           np.tile(theta, (len(phi), 1)))).reshape((-1, 2))
+    vertices = util.spherical_to_vector(spherical) * radius
+
+    # generate faces by creating a bunch of pie wedges
+    c = len(theta)
+    # a quad face as two triangles
+    pairs = np.array([[c, 0, 1],
+                      [c + 1, c, 1]])
+
+    # increment both triangles in each quad face by the same offset
+    incrementor = np.tile(np.arange(c - 1), (2, 1)).T.reshape((-1, 1))
+    # create the faces for a single pie wedge of the sphere
+    strip = np.tile(pairs, (c - 1, 1))
+    strip += incrementor
+    # the first and last faces will be degenerate since the first
+    # and last vertex are identical in the two rows
+    strip = strip[1:-1]
+
+    # tile pie wedges into a sphere
+    faces = np.vstack([strip + (i * c) for i in range(len(phi))])
+
+    # poles are repeated in every strip, so a mask to merge them
+    mask = np.arange(len(vertices))
+    # the top pole are all the same vertex
+    mask[0::c] = 0
+    # the bottom pole are all the same vertex
+    mask[c - 1::c] = c - 1
+
+    # faces masked to remove the duplicated pole vertices
+    # and mod to wrap to fill in the last pie wedge
+    faces = mask[np.mod(faces, len(vertices))]
+
+    # we save a lot of time by not processing again
+    # since we did some bookkeeping mesh is watertight
+    mesh = Trimesh(vertices=vertices, faces=faces, process=False,
                    metadata={'shape': 'sphere',
                              'radius': radius})
+    return mesh
 
 
 def capsule(height=1.0,
diff --git a/trimesh/exchange/obj.py b/trimesh/exchange/obj.py
index abfbd680..643733e3 100644
--- a/trimesh/exchange/obj.py
+++ b/trimesh/exchange/obj.py
@@ -751,7 +751,9 @@ def _preprocess_faces(text):
         # Discard the g tag line in the list of faces
         elif chunk.startswith('g '):
             _, chunk = chunk.split('\n', 1)
-        if 'f ' in chunk:
+        # If we have an f at the beginning of a line
+        # then add it to the list of faces chunks
+        if chunk.startswith('f ') or '\nf' in chunk:
             face_tuples.append((current_mtl, current_obj, chunk))
     return face_tuples
 
diff --git a/trimesh/registration.py b/trimesh/registration.py
index 42f5e2dc..eeb3eaa6 100644
--- a/trimesh/registration.py
+++ b/trimesh/registration.py
@@ -71,7 +71,7 @@ def mesh_other(mesh,
         """
         Return a combination of mesh vertices and surface samples
         with vertices chosen by likelihood to be important
-        to registration.
+        to registation.
         """
         if len(m.vertices) < (count / 2):
             return np.vstack((
diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py
index 80f9f1ec..f014c80e 100644
--- a/trimesh/scene/scene.py
+++ b/trimesh/scene/scene.py
@@ -834,29 +834,15 @@ class Scene(Geometry3D):
 
         Returns
         ----------
-        dumped : (n,) Trimesh, Path2D, Path3D, PointCloud
-          Depending on what the scene contains. If `concatenate`
-          then some geometry may be dropped if it doesn't match.
+        dumped : (n,) Trimesh or Trimesh
+          Trimesh objects transformed to their
+          location the scene.graph
         """
-
         result = []
         for node_name in self.graph.nodes_geometry:
             transform, geometry_name = self.graph[node_name]
             # get a copy of the geometry
             current = self.geometry[geometry_name].copy()
-
-            # if the geometry is 2D see if we have to upgrade to 3D
-            if hasattr(current, 'to_3D'):
-                # check to see if the scene is transforming the path out of plane
-                check = util.isclose(transform, util._IDENTITY, atol=1e-8)
-                check[:2, :3] = True
-                if not check.all():
-                    # transform moves in 3D
-                    current = current.to_3D()
-                else:
-                    # transform moves in 2D
-                    transform = transform[:3, :3]
-
             # move the geometry vertices into the requested frame
             current.apply_transform(transform)
             current.metadata['name'] = geometry_name
@@ -866,7 +852,6 @@ class Scene(Geometry3D):
             result.append(current)
 
         if concatenate:
-            # if scene has mixed geometry this may drop some of it
             return util.concatenate(result)
 
         return np.array(result)
diff --git a/trimesh/transformations.py b/trimesh/transformations.py
index 48d25498..2a905ff4 100644
--- a/trimesh/transformations.py
+++ b/trimesh/transformations.py
@@ -2149,14 +2149,14 @@ def transform_points(points,
 
 def fix_rigid(matrix, max_deviance=1e-5):
     """
-    If a homogeneous transformation matrix is *almost* a rigid
+    If a homogenous transformation matrix is *almost* a rigid
     transform but many matrix-multiplies have accumulated some
     floating point error try to restore the matrix using SVD.
 
     Parameters
     -----------
     matrix : (4, 4) or (3, 3) float
-      Homogeneous transformation matrix.
+      Homogenous transformation matrix.
     max_deviance : float
       Do not alter the matrix if it is not rigid by more
       than this amount.
@@ -2164,7 +2164,7 @@ def fix_rigid(matrix, max_deviance=1e-5):
     Returns
     ----------
     repaired : (4, 4) or (3, 3) float
-      Repaired homogeneous transformation matrix
+      Repaired homogenous transformation matrix
     """
     dim = matrix.shape[0] - 1
     check = np.abs(np.dot(matrix[:dim, :dim], matrix[:dim, :dim].T)
diff --git a/trimesh/util.py b/trimesh/util.py
index 433710a7..62f26b61 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -1466,9 +1466,8 @@ def concatenate(a, b=None):
         return []
 
     # extract the trimesh type to avoid a circular import
-    # and assert that all inputs are Trimesh objects
+    # and assert that both inputs are Trimesh objects
     trimesh_type = type_named(meshes[0], 'Trimesh')
-    assert all(isinstance(mesh, trimesh_type) for mesh in meshes)
 
     # append faces and vertices of meshes
     vertices, faces = append_faces(


Test patch:
diff --git a/tests/test_cache.py b/tests/test_cache.py
index f402f523..4119f80f 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.py
@@ -86,10 +86,6 @@ class CacheTest(g.unittest.TestCase):
             # in place floor division :|
             a //= 2
             modified.append(hash(a))
-            # in-place sort
-            a.sort()
-            modified.append(hash(a))
-
             # these operations altered data and
             # the hash SHOULD have changed
             modified = g.np.array(modified, dtype=g.np.int64)
@@ -186,79 +182,6 @@ class CacheTest(g.unittest.TestCase):
         # for some reason return a different type?
         assert isinstance(a.max(), type(o.max()))
 
-    def test_method_combinations(self):
-        # run combinatorial guesses of arguments on every method
-        # of the subclassed `ndarray` and see if we can get trackedarray
-        # to return incorrect hashes. Hopefully this catches new methods
-
-        # only run this test on python 3+ as they changed tobytes
-        if not g.PY3:
-            return
-
-        import itertools
-        import numpy as np
-        from trimesh.caching import tracked_array
-
-        dim = (100, 3)
-
-        # generate a bunch of arguments for every function of an `ndarray` so
-        # we can see if the functions mutate
-        flat = [2.3,
-                1,
-                10,
-                4.2,
-                [3, -1],
-                {'shape': 10},
-                np.int64,
-                np.float64,
-                True, True,
-                False, False,
-                g.random(dim),
-                g.random(dim[::1]),
-                'shape']
-
-        # start with no arguments
-        attempts = [tuple()]
-        # add a single argument from our guesses
-        attempts.extend([(A,) for A in flat])
-        # add 2 and 3 length permutations of our guesses
-        attempts.extend([tuple(G) for G in itertools.permutations(flat, 2)])
-
-        # adding 3-length permuations makes this test 10x slower but if you
-        # are suspicious of a method caching you could uncomment this out:
-        # attempts.extend([tuple(G) for G in itertools.permutations(flat, 3)])
-
-        # collect functions which mutate arrays but don't change our hash
-        broken = []
-
-        for method in list(dir(tracked_array(g.random(dim)))):
-            failures = []
-            g.log.debug('hash check: `{}`'.format(method))
-            for A in attempts:
-                m = g.random((100, 3))
-                true_pre = m.tobytes()
-                m = tracked_array(m)
-                hash_pre = hash(m)
-
-                try:
-                    eval('m.{method}(*A)'.format(method=method))
-                except BaseException as J:
-                    failures.append(str(J))
-
-                hash_post = hash(m)
-                true_post = m.tobytes()
-
-                # if tobytes disagrees with our hashing logic
-                # it indicates we have cached incorrectly
-                if (hash_pre == hash_post) != (true_pre == true_post):
-                    broken.append((method, A))
-
-        if len(broken) > 0:
-            method_busted = set([method for method, _ in broken])
-            raise ValueError(
-                '`TrackedArray` incorrectly hashing methods: {}'.format(
-                    method_busted))
-
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()
diff --git a/tests/test_convex.py b/tests/test_convex.py
index 7c0e1d8d..f7e75be3 100644
--- a/tests/test_convex.py
+++ b/tests/test_convex.py
@@ -18,10 +18,7 @@ class ConvexTest(g.unittest.TestCase):
                   (True, g.trimesh.creation.box()),
                   (True, g.trimesh.creation.cylinder(
                       radius=1, height=10)),
-                  (True, g.trimesh.creation.capsule()),
-                  (False, g.trimesh.creation.box(extents=(1,1,1)).union( \
-                      g.trimesh.creation.box(extents=(1,1,1), \
-                      transform=g.trimesh.transformations.translation_matrix((10,0,0)))))]
+                  (True, g.trimesh.creation.capsule())]
 
         for is_convex, mesh in meshes:
             assert mesh.is_watertight
diff --git a/tests/test_creation.py b/tests/test_creation.py
index f1d214ff..4ed74ca5 100644
--- a/tests/test_creation.py
+++ b/tests/test_creation.py
@@ -95,8 +95,6 @@ class CreationTest(g.unittest.TestCase):
             assert sphere.is_convex
             assert sphere.is_watertight
             assert sphere.is_winding_consistent
-
-            assert sphere.body_count == 1
             assert sphere.metadata['shape'] == 'sphere'
 
             # all vertices should have radius of exactly 1.0
diff --git a/tests/test_obj.py b/tests/test_obj.py
index 51c4ac57..532831f4 100644
--- a/tests/test_obj.py
+++ b/tests/test_obj.py
@@ -437,6 +437,12 @@ class OBJTest(g.unittest.TestCase):
         m = g.get_mesh('face_in_group_name_mid_file.obj')
         assert len(m.vertices) == 5
         assert len(m.faces) == 2
+        
+    def test_chunk_parsing_with_no_faces_but_with_f_in_chunk(self):
+        # Checks that a chunk with no faces but with 'f ' in it loads properly
+        m = g.get_mesh('obj_with_no_face_in_chunk.obj')
+        assert len(m.vertices) == 3
+        assert len(m.faces) == 1
 
 
 def simple_load(text):
diff --git a/tests/test_scene.py b/tests/test_scene.py
index a4a87d80..95dbbd1c 100644
--- a/tests/test_scene.py
+++ b/tests/test_scene.py
@@ -449,13 +449,6 @@ class SceneTests(g.unittest.TestCase):
         # scene bounds should exactly match mesh bounds
         assert g.np.allclose(m.bounds, dump.bounds)
 
-    def test_concatenate_mixed(self):
-        scene = g.trimesh.Scene([g.trimesh.creation.icosphere(),
-                                 g.trimesh.path.creation.rectangle([[0, 0], [1, 1]])])
-
-        dump = scene.dump(concatenate=True)
-        assert isinstance(dump, g.trimesh.Trimesh)
-
     def test_append_scenes(self):
         scene_0 = g.trimesh.Scene(base_frame='not_world')
         scene_1 = g.trimesh.Scene(base_frame='not_world')