Repository: mikedh/trimesh

Issue:
[Bug] AttributeError: module 'trimesh.voxel' has no attribute 'creation'
# Trimesh version: 3.9.1

Hi. I discovered that trimesh reports the following error
```
AttributeError: module 'trimesh.voxel' has no attribute 'creation'
```
when trying to access the module. However, if a method from it is called, e.g.
```
m = trimesh.creation.box()
m.voxelized(pitch=.5)
```
then the module `trimesh.voxel.creation` can be correctly found by the program.
Null vector normals makes GLB export unreadable by external viewers
GLB/GLTF exports unitize by default the vertex normals of the mesh.

However, this normalization is not safe regarding null normal vectors.

This causes an issue when reading the exported GLB/GLTF from external viewers (windows 3D viewer for exemple) the null normals have been unitized to NaN vectors.

Example code illustrating the problem
```python
import numpy as np
import trimesh

# Define the vertices
vertices = np.array([
    [0, 0, 0],  # Vertex 0
    [1, 0, 0],  # Vertex 1
    [0, 1, 0],  # Vertex 2
    [1, 1, 0],  # Vertex 3
    [0, 0, 1],  # Vertex 4
    [1, 0, 1]   # Vertex 5
])

# Define the faces
faces = np.array([
    [0, 1, 2],  # Triangle 0
    [1, 3, 2],  # Triangle 1
    [0, 1, 4]   # Triangle 2
])

# Create the mesh
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)

# Set the normal of the first vertex to null
modified_normals = mesh.vertex_normals.copy()
modified_normals[0] = [0,0,0]

mesh.vertex_normals = modified_normals

# Export the mesh to GLB
mesh.export("test_glb_export_null_normals.glb")

```
The exported tree looks like this, with NaN values as min/max values in the vertex normals buffer
```json
{"scene":0,"scenes":[{"nodes":[0]}],"asset":{"version":"2.0","generator":"https://github.com/mikedh/trimesh"},"accessors":[{"componentType":5125,"type":"SCALAR","bufferView":0,"count":9,"max":[4],"min":[0]},{"componentType":5126,"type":"VEC3","byteOffset":0,"bufferView":1,"count":5,"max":[1.0,1.0,1.0],"min":[0.0,0.0,0.0]},{"componentType":5126,"count":5,"type":"VEC3","byteOffset":0,"bufferView":2,"max":[NaN,NaN,NaN],"min":[NaN,NaN,NaN]}],"meshes":[{"name":"geometry_0","extras":{"processed":true},"primitives":[{"attributes":{"POSITION":1,"NORMAL":2},"indices":0,"mode":4}]}],"nodes":[{"name":"world","children":[1]},{"name":"geometry_0","mesh":0}],"buffers":[{"byteLength":156}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36},{"buffer":0,"byteOffset":36,"byteLength":60},{"buffer":0,"byteOffset":96,"byteLength":60}]}
```

I would recommend having a null safe unitize operation, where null vectors are kept as is in the exported GLB/GLTF.


Discussion:
Meet the same problem. Would like to follow this channel.
same here. able to solve this by import creation myself
```
from trimesh.voxel import creation
```
Hmm, that exact `m.voxelized(pitch)` line [is called in unit tests](https://github.com/mikedh/trimesh/blob/master/tests/test_voxel.py#L17-L21). Do you have a full traceback? I suspect it could be a missing dep with a confusing error message?

> same here. able to solve this by import creation myself
> 
> ```
> from trimesh.voxel import creation
> ```

It works, thanks
Problem still present with version `3.12.6` and now in addition `rtree` is a missing dependency.
I've just bumped into this issue with version `3.15.8`.
Hey, I checked a minimal install on Python 3.10, 3.7, 3.5, 2.7 and this issue didn't occur in a simple example, does anyone have a reproducible docker snippet or full traceback including the line number where this was called from?

3.10:
```
mikedh@orion:~/trimesh$ docker run -it python:3.10-slim-bullseye /bin/bash
root@3d8b25c2742e:/# pip install trimesh
Collecting trimesh
  Downloading trimesh-3.16.4-py3-none-any.whl (663 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 663.9/663.9 kB 6.2 MB/s eta 0:00:00
Collecting numpy
  Downloading numpy-1.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.1 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 17.1/17.1 MB 41.9 MB/s eta 0:00:00
Installing collected packages: numpy, trimesh
Successfully installed numpy-1.23.4 trimesh-3.16.4
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

[notice] A new release of pip available: 22.2.2 -> 22.3.1
[notice] To update, run: pip install --upgrade pip
root@3d8b25c2742e:/# pythonPython 3.10.8 (main, Nov 15 2022, 21:06:05) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import trimesh
>>> m = trimesh.creation.box()
>>> m.voxelized(pitch=0.5)
<trimesh.VoxelGrid(3, 3, 3)>
>>> m.voxelized(pitch=0.5).volume
3.250000000000001
```

3.7:
```
mikedh@orion:~/trimesh$ docker run -it python:3.5-slim-bullseye /bin/bash
Unable to find image 'python:3.5-slim-bullseye' locally
docker: Error response from daemon: manifest for python:3.5-slim-bullseye not found: manifest unknown: manifest unknown.
See 'docker run --help'.
mikedh@orion:~/trimesh$ docker run -it python:3.7-slim-bullseye /bin/bash
Unable to find image 'python:3.7-slim-bullseye' locally
3.7-slim-bullseye: Pulling from library/python
a603fa5e3b41: Already exists 
b00aaacf759c: Already exists 
bfd5215df481: Pull complete 
646c640d7dca: Pull complete 
2ca40dc2918a: Pull complete 
Digest: sha256:7816ff9e4f2c31671cc7db97ea99e05a2e5fd508eaeced99e0939dbadf4f670a
Status: Downloaded newer image for python:3.7-slim-bullseye
root@3e51ac02d5f2:/# pip install trimesh
Collecting trimesh
  Downloading trimesh-3.16.4-py3-none-any.whl (663 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 663.9/663.9 KB 5.9 MB/s eta 0:00:00
Collecting numpy
  Downloading numpy-1.21.6-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (15.7 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 15.7/15.7 MB 10.9 MB/s eta 0:00:00
Installing collected packages: numpy, trimesh
Successfully installed numpy-1.21.6 trimesh-3.16.4
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/usr/local/bin/python -m pip install --upgrade pip' command.
root@3e51ac02d5f2:/# python
Python 3.7.15 (default, Nov 15 2022, 22:40:13) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import trimesh
>>> m = trimesh.creation.box()
>>> m.voxelized(pitch=0.5)
<trimesh.VoxelGrid(3, 3, 3)>
```

3.5:
```
mikedh@orion:~/trimesh$ docker run -it python:3.5-slim-jessie /bin/bash
Unable to find image 'python:3.5-slim-jessie' locally
3.5-slim-jessie: Pulling from library/python
0f4b58aabdee: Pull complete 
b8b3b2216ffd: Pull complete 
b9e422555895: Pull complete 
da961b72154c: Pull complete 
60f1166a469c: Pull complete 
Digest: sha256:65565b3fe96165605eed956f744578f4b441d43a034c4295e7f0c75d4b633bdd
Status: Downloaded newer image for python:3.5-slim-jessie
root@5478cfe719ba:/# pip install trimesh
Collecting trimesh
  Downloading https://files.pythonhosted.org/packages/b4/42/a492d2b067bde58696d447127f8df374302cbc4cd30371590982998b7af0/trimesh-3.16.4-py3-none-any.whl (663kB)
     |████████████████████████████████| 665kB 2.3MB/s 
Collecting numpy (from trimesh)
  Downloading https://files.pythonhosted.org/packages/b5/36/88723426b4ff576809fec7d73594fe17a35c27f8d01f93637637a29ae25b/numpy-1.18.5-cp35-cp35m-manylinux1_x86_64.whl (19.9MB)
     |████████████████████████████████| 19.9MB 19.2MB/s 
Installing collected packages: numpy, trimesh
Successfully installed numpy-1.18.5 trimesh-3.16.4
WARNING: You are using pip version 19.1.1, however version 20.3.4 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
root@5478cfe719ba:/# python
Python 3.5.7 (default, Jul 10 2019, 03:30:50) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import trimesh
>>> m = trimesh.creation.box()
>>> m.voxelized(pitch=0.5)
<trimesh.VoxelGrid(3, 3, 3)>
``` 

2.7:
```
mikedh@orion:~/trimesh$ docker run -it python:2.7-slim-jessie /bin/bash
Unable to find image 'python:2.7-slim-jessie' locally
2.7-slim-jessie: Pulling from library/python
0f4b58aabdee: Already exists 
79c9d6017da5: Pull complete 
a1d950e6ac24: Pull complete 
8ca451ccc0e0: Pull complete 
Digest: sha256:969a97a1cae4f1796104ebee4c4ec6ecb6bc81120d03498d56feebd5820cba93
Status: Downloaded newer image for python:2.7-slim-jessie
root@b8585ef6b82e:/# pip install trimesh
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7.
Collecting trimesh
  Downloading https://files.pythonhosted.org/packages/66/7c/713c0b5e18babb223e80507dae1e6ca414283254d59db8007ebc72c89316/trimesh-3.16.4.tar.gz (617kB)
     |████████████████████████████████| 624kB 5.7MB/s 
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Collecting numpy (from trimesh)
  Downloading https://files.pythonhosted.org/packages/3a/5f/47e578b3ae79e2624e205445ab77a1848acdaa2929a00eeef6b16eaaeb20/numpy-1.16.6-cp27-cp27mu-manylinux1_x86_64.whl (17.0MB)
     |████████████████████████████████| 17.0MB 11.2MB/s 
Building wheels for collected packages: trimesh
  Building wheel for trimesh (PEP 517) ... done
  Stored in directory: /root/.cache/pip/wheels/3d/00/bc/93b8cae8cd6a524d0d452a14b2fa5cc26b30f8d995940380a7
Successfully built trimesh
Installing collected packages: numpy, trimesh
Successfully installed numpy-1.16.6 trimesh-3.16.4
WARNING: You are using pip version 19.1.1, however version 20.3.4 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
root@b8585ef6b82e:/# python
Python 2.7.16 (default, Jul 10 2019, 03:46:34) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import trimesh
>>> m = trimesh.creation.box()
>>> m.voxelized(pitch=0.5)
<trimesh.VoxelGrid(3, 3, 3)>
>>> m.voxelized(pitch=0.5).volume
3.250000000000001
```
My bad, it's not exactly the same error, it's rather related to `trimesh.voxel.creation`, but it looks very similar.

```
merlin@L-0219:/workspace$ python3
Python 3.10.6 (main, Nov  2 2022, 18:53:38) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import trimesh
>>> mesh = trimesh.Trimesh()
>>> n = trimesh.voxel.creation.local_voxelize(mesh, (0, 0, 0), 0.0, 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'trimesh.voxel' has no attribute 'creation'
>>> from trimesh.voxel import creation
>>> n = trimesh.voxel.creation.local_voxelize(mesh, (0, 0, 0), 0.0, 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.10/dist-packages/trimesh/voxel/creation.py", line 120, in local_voxelize
    faces = list(mesh.triangles_tree.intersection(bounds))
  File "/usr/local/lib/python3.10/dist-packages/trimesh/caching.py", line 139, in get_cached
    value = function(*args, **kwargs)
  File "/usr/local/lib/python3.10/dist-packages/trimesh/base.py", line 797, in triangles_tree
    tree = triangles.bounds_tree(self.triangles)
  File "/usr/local/lib/python3.10/dist-packages/trimesh/triangles.py", line 344, in bounds_tree
    tree = util.bounds_tree(triangle_bounds)
  File "/usr/local/lib/python3.10/dist-packages/trimesh/util.py", line 1733, in bounds_tree
    import rtree
ModuleNotFoundError: No module named 'rtree'
>>> 
```
The call fails in the "working" (i.e. 2nd) case because the missing `rtree` dependency (as already reported by @alexge233), but we see that the first call fails because `trimesh.voxel` has no `creation` attribute.





Patch:
diff --git a/pyproject.toml b/pyproject.toml
index acdcf1b32..ec9f97aab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
 [project]
 name = "trimesh"
 requires-python = ">=3.7"
-version = "4.1.2"
+version = "4.1.3"
 authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
 license = {file = "LICENSE.md"}
 description = "Import, export, process, analyze and view triangular meshes."
@@ -112,7 +112,12 @@ all = ["trimesh[easy,recommend,test]"]
 
 [tool.ruff]
 target-version = "py37"
+line-length = 90
+
+
 # See https://github.com/charliermarsh/ruff#rules for error code definitions.
+
+[tool.ruff.lint]
 select = [
     # "ANN", # annotations
     "B", # bugbear
@@ -121,7 +126,7 @@ select = [
     "F", # flakes
     "I", # import sorting
     "RUF100", # meta
-    "U", # upgrade
+    "UP", # upgrade
     "W", # style warnings
     "YTT", # sys.version
 ]
@@ -133,9 +138,4 @@ ignore = [
   "E501", # Line too long ({width} > {limit} characters)
   "B904", # raise ... from err
   "B905", # zip() without an explicit strict= parameter
-  "ANN101", # type hint for `self`
-  "ANN002", # type hint for *args
-  "ANN003", # type hint for **kwargs
 ]
-line-length = 90
-
diff --git a/trimesh/__init__.py b/trimesh/__init__.py
index a200c7af4..327680aa6 100644
--- a/trimesh/__init__.py
+++ b/trimesh/__init__.py
@@ -17,6 +17,7 @@
     creation,
     curvature,
     decomposition,
+    exceptions,
     geometry,
     graph,
     grouping,
@@ -59,9 +60,15 @@
     from . import path
 except BaseException as E:
     # raise a useful error if path hasn't loaded
-    from .exceptions import ExceptionWrapper
+    path = exceptions.ExceptionWrapper(E)
+
+
+try:
+    from . import voxel
+except BaseException as E:
+    # requires non-minimal imports
+    voxel = exceptions.ExceptionWrapper(E)
 
-    path = ExceptionWrapper(E)
 
 __all__ = [
     "PointCloud",
@@ -108,4 +115,5 @@
     "units",
     "utilScene",
     "voxel",
+    "exceptions",
 ]
diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py
index 72de72039..f851fdb33 100644
--- a/trimesh/exchange/gltf.py
+++ b/trimesh/exchange/gltf.py
@@ -7,14 +7,15 @@
 """
 
 import base64
-import collections
 import json
+from collections import OrderedDict, defaultdict, deque
 
 import numpy as np
 
 from .. import rendering, resources, transformations, util, visual
 from ..caching import hash_fast
 from ..constants import log, tol
+from ..typed import NDArray
 from ..util import unique_name
 from ..visual.gloss import specular_to_pbr
 
@@ -484,7 +485,7 @@ def _buffer_append(ordered, data):
 
     Parameters
     ----------
-    od : collections.OrderedDict
+    od : OrderedDict
       Keyed like { hash : data }
     data : bytes
       To be stored
@@ -505,19 +506,19 @@ def _buffer_append(ordered, data):
     return len(ordered) - 1
 
 
-def _data_append(acc, buff, blob, data):
+def _data_append(acc: OrderedDict, buff: OrderedDict, blob: dict, data: NDArray):
     """
     Append a new accessor to an OrderedDict.
 
     Parameters
     ------------
-    acc : collections.OrderedDict
+    acc
       Collection of accessors, will be mutated in-place
-    buff : collections.OrderedDict
+    buff
       Collection of buffer bytes, will be mutated in-place
-    blob : dict
+    blob
       Candidate accessor
-    data : numpy.array
+    data
       Data to fill in details to blob
 
     Returns
@@ -562,6 +563,11 @@ def _data_append(acc, buff, blob, data):
     # see if we're an array, matrix, etc
     kind = blob["type"]
 
+    if tol.strict:
+        # in unit tests make sure everything we're trying to export
+        # is finite, which also checks for accidental NaN values
+        assert np.isfinite(data).all()
+
     if kind == "SCALAR":
         # is probably (n, 1)
         blob["count"] = int(np.prod(data.shape))
@@ -665,7 +671,7 @@ def _create_gltf_structure(
         "scene": 0,
         "scenes": [{"nodes": [0]}],
         "asset": {"version": "2.0", "generator": "https://github.com/mikedh/trimesh"},
-        "accessors": collections.OrderedDict(),
+        "accessors": OrderedDict(),
         "meshes": [],
         "images": [],
         "textures": [],
@@ -689,7 +695,7 @@ def _create_gltf_structure(
     # store materials as {hash : index} to avoid duplicates
     mat_hashes = {}
     # store data from geometries
-    buffer_items = collections.OrderedDict()
+    buffer_items = OrderedDict()
 
     # map the name of each mesh to the index in tree['meshes']
     mesh_index = {}
@@ -922,10 +928,7 @@ def _append_mesh(
     ):
         # store vertex normals if requested
         if unitize_normals:
-            normals = mesh.vertex_normals.copy()
-            norms = np.linalg.norm(normals, axis=1)
-            if not util.allclose(norms, 1.0, atol=1e-4):
-                normals /= norms.reshape((-1, 1))
+            normals = util.unitize(mesh.vertex_normals)
         else:
             # we don't have to copy them since
             # they aren't being altered
@@ -978,7 +981,7 @@ def _build_views(buffer_items):
 
     Parameters
     --------------
-    buffer_items : collections.OrderedDict
+    buffer_items : OrderedDict
       Buffers to build views for
 
     Returns
@@ -1461,9 +1464,9 @@ def _read_buffers(
         else:
             materials = _parse_materials(header, views=views, resolver=resolver)
 
-    mesh_prim = collections.defaultdict(list)
+    mesh_prim = defaultdict(list)
     # load data from accessors into Trimesh objects
-    meshes = collections.OrderedDict()
+    meshes = OrderedDict()
 
     # keep track of how many times each name has been attempted to
     # be inserted to avoid a potentially slow search through our
@@ -1661,9 +1664,9 @@ def _read_buffers(
     names[base_frame] = base_frame
 
     # visited, kwargs for scene.graph.update
-    graph = collections.deque()
+    graph = deque()
     # unvisited, pairs of node indexes
-    queue = collections.deque()
+    queue = deque()
 
     if "scene" in header:
         # specify the index of scenes if specified
diff --git a/trimesh/voxel/__init__.py b/trimesh/voxel/__init__.py
index 83599cd2b..112975981 100644
--- a/trimesh/voxel/__init__.py
+++ b/trimesh/voxel/__init__.py
@@ -1,3 +1,4 @@
+from . import creation
 from .base import VoxelGrid
 
-__all__ = ["VoxelGrid"]
+__all__ = ["VoxelGrid", "creation"]


Test patch:
diff --git a/tests/generic.py b/tests/generic.py
index b3fa46fc7..88f4c92e7 100644
--- a/tests/generic.py
+++ b/tests/generic.py
@@ -543,7 +543,7 @@ def check_fuze(fuze):
     fuze.visual.material.copy()
 
 
-def wrapload(exported, file_type, **kwargs):
+def roundtrip(exported, file_type, **kwargs):
     """
     Reload an exported byte blob into a mesh.
 
diff --git a/tests/test_export.py b/tests/test_export.py
index a18565a2b..ac13a1b93 100644
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -200,7 +200,7 @@ def test_export(self):
     def test_obj(self):
         m = g.get_mesh("textured_tetrahedron.obj", process=False)
         export = m.export(file_type="obj")
-        reconstructed = g.wrapload(export, file_type="obj", process=False)
+        reconstructed = g.roundtrip(export, file_type="obj", process=False)
         # test that we get at least the same number of normals and texcoords out;
         # the loader may reorder vertices, so we shouldn't check direct
         # equality
@@ -243,7 +243,7 @@ def test_scene(self):
         export = source.export(file_type="glb")
 
         # re- load the file as a trimesh.Scene object again
-        loaded = g.wrapload(export, file_type="glb")
+        loaded = g.roundtrip(export, file_type="glb")
 
         # the scene should be identical after export-> import cycle
         assert g.np.allclose(loaded.extents / source.extents, 1.0)
diff --git a/tests/test_gltf.py b/tests/test_gltf.py
index 2261f8c1f..3e00b7984 100644
--- a/tests/test_gltf.py
+++ b/tests/test_gltf.py
@@ -1032,6 +1032,30 @@ def post(tree):
         # export with a postprocessor
         s.export(file_type="glb", tree_postprocessor=post)
 
+    def test_unitize_normals_null_values(self):
+        # Create the mesh
+        mesh = g.trimesh.Trimesh(
+            vertices=[[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1]],
+            faces=[[0, 1, 2], [1, 3, 2], [0, 1, 4]],
+        )
+
+        # Set the normal of the first vertex to null
+        modified_normals = mesh.vertex_normals.copy()
+        modified_normals[0] = [0, 0, 0]
+
+        mesh.vertex_normals = modified_normals
+
+        # Export the mesh
+        export = mesh.export(file_type="glb", unitize_normals=True)
+        reimported_mesh = list(
+            g.trimesh.load(
+                g.trimesh.util.wrap_as_stream(export), file_type="glb"
+            ).geometry.values()
+        )[0]
+
+        # Check that the normals are still null
+        assert g.np.allclose(reimported_mesh.vertex_normals[0], [0, 0, 0])
+
 
 if __name__ == "__main__":
     g.trimesh.util.attach_to_log()
diff --git a/tests/test_obj.py b/tests/test_obj.py
index 01aac6b8b..8000ff327 100644
--- a/tests/test_obj.py
+++ b/tests/test_obj.py
@@ -11,7 +11,7 @@ def test_rabbit(self):
         # this should test the non-vectorized load path
         m = g.get_mesh("rabbit.obj")
         assert len(m.faces) == 1252
-        rec = g.wrapload(m.export(file_type="obj"), file_type="obj")
+        rec = g.roundtrip(m.export(file_type="obj"), file_type="obj")
         assert g.np.isclose(m.area, rec.area)
 
     def test_no_img(self):
@@ -25,14 +25,14 @@ def test_no_img(self):
         assert m.visual.uv.min() > -1e-5
         # check to make sure it's not all zeros
         assert m.visual.uv.ptp() > 0.5
-        rec = g.wrapload(m.export(file_type="obj"), file_type="obj")
+        rec = g.roundtrip(m.export(file_type="obj"), file_type="obj")
         assert g.np.isclose(m.area, rec.area)
 
     def test_trailing(self):
         # test files with texture and trailing slashes
         m = g.get_mesh("jacked.obj")
         assert len(m.visual.uv) == len(m.vertices)
-        rec = g.wrapload(m.export(file_type="obj"), file_type="obj")
+        rec = g.roundtrip(m.export(file_type="obj"), file_type="obj")
         assert g.np.isclose(m.area, rec.area)
 
     def test_obj_groups(self):
@@ -66,7 +66,7 @@ def test_obj_quad(self):
 
         assert mesh.is_watertight
         assert mesh.is_winding_consistent
-        rec = g.wrapload(mesh.export(file_type="obj"), file_type="obj")
+        rec = g.roundtrip(mesh.export(file_type="obj"), file_type="obj")
         assert g.np.isclose(mesh.area, rec.area)
 
     def test_obj_multiobj(self):
@@ -242,7 +242,7 @@ def test_empty_or_pointcloud(self):
                 raise ValueError("cannot export empty")
             elif "points" in empty_file:
                 export = e.export(file_type="ply")
-                reconstructed = g.wrapload(export, file_type="ply")
+                reconstructed = g.roundtrip(export, file_type="ply")
 
                 # result should be a point cloud instance
                 assert isinstance(e, g.trimesh.PointCloud)
@@ -258,7 +258,7 @@ def test_backslash_continuation_character(self):
 
     def test_no_uv(self):
         mesh = g.get_mesh("box.obj")
-        rec = g.wrapload(mesh.export(file_type="obj"), file_type="obj")
+        rec = g.roundtrip(mesh.export(file_type="obj"), file_type="obj")
         assert g.np.isclose(mesh.area, rec.area)
 
     def test_no_uv_but_mtl(self):
diff --git a/tests/test_ply.py b/tests/test_ply.py
index dad2d8ff1..4747b9ff5 100644
--- a/tests/test_ply.py
+++ b/tests/test_ply.py
@@ -19,7 +19,7 @@ def test_ply(self):
         assert m.visual.face_colors.ptp(axis=0).max() > 0
 
         export = m.export(file_type="ply")
-        reconstructed = g.wrapload(export, file_type="ply")
+        reconstructed = g.roundtrip(export, file_type="ply")
 
         assert reconstructed.visual.kind == "face"
 
@@ -31,7 +31,7 @@ def test_ply(self):
         assert m.visual.vertex_colors.ptp(axis=0).max() > 0
 
         export = m.export(file_type="ply")
-        reconstructed = g.wrapload(export, file_type="ply")
+        reconstructed = g.roundtrip(export, file_type="ply")
         assert reconstructed.visual.kind == "vertex"
 
         assert g.np.allclose(reconstructed.visual.vertex_colors, m.visual.vertex_colors)
@@ -90,7 +90,7 @@ def test_vertex_attributes(self):
         m.vertex_attributes["test_nd_attribute"] = test_nd_attribute
 
         export = m.export(file_type="ply")
-        reconstructed = g.wrapload(export, file_type="ply")
+        reconstructed = g.roundtrip(export, file_type="ply")
 
         vertex_attributes = reconstructed.metadata["_ply_raw"]["vertex"]["data"]
         result_1d = vertex_attributes["test_1d_attribute"]
@@ -110,7 +110,7 @@ def test_face_attributes(self):
         m.face_attributes["test_nd_attribute"] = test_nd_attribute
 
         export = m.export(file_type="ply")
-        reconstructed = g.wrapload(export, file_type="ply")
+        reconstructed = g.roundtrip(export, file_type="ply")
 
         face_attributes = reconstructed.metadata["_ply_raw"]["face"]["data"]
         result_1d = face_attributes["test_1d_attribute"]
@@ -133,19 +133,19 @@ def test_cases(self):
 
     def test_ascii_color(self):
         mesh = g.trimesh.creation.box()
-        en = g.wrapload(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
+        en = g.roundtrip(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
         assert en.visual.kind is None
 
         color = [255, 0, 0, 255]
         mesh.visual.vertex_colors = color
 
         # try exporting and reloading raw
-        eb = g.wrapload(mesh.export(file_type="ply"), file_type="ply")
+        eb = g.roundtrip(mesh.export(file_type="ply"), file_type="ply")
 
         assert g.np.allclose(eb.visual.vertex_colors[0], color)
         assert eb.visual.kind == "vertex"
 
-        ea = g.wrapload(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
+        ea = g.roundtrip(mesh.export(file_type="ply", encoding="ascii"), file_type="ply")
         assert g.np.allclose(ea.visual.vertex_colors, color)
         assert ea.visual.kind == "vertex"
 
@@ -171,7 +171,7 @@ def test_empty_or_pointcloud(self):
             elif "points" in empty_file:
                 # create export
                 export = e.export(file_type="ply")
-                reconstructed = g.wrapload(export, file_type="ply")
+                reconstructed = g.roundtrip(export, file_type="ply")
 
                 # result should be a point cloud instance
                 assert isinstance(e, g.trimesh.PointCloud)
diff --git a/tests/test_points.py b/tests/test_points.py
index 716d0a065..4c28953ba 100644
--- a/tests/test_points.py
+++ b/tests/test_points.py
@@ -222,7 +222,7 @@ def test_ply(self):
         assert hash(p.visual) != initial
 
         # test exporting a pointcloud to a PLY file
-        r = g.wrapload(p.export(file_type="ply"), file_type="ply")
+        r = g.roundtrip(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)