Repository: mikedh/trimesh
Pull request #2336 (2024-12-27 12:41:35)

[BUG] fix rounding issue in uv_to_color()
Hi Mike,

I discovered an **index out of bounds** error in the `to_color()` function when converting TextureVisual to ColorVisuals.

The issue occurs in the `uv_to_color()` function [here](https://github.com/mikedh/trimesh/blob/2fcb2b2ea8085d253e692ecd4f71b8f450890d51/trimesh/visual/color.py#L869):
```python
# get texture image pixel positions of UV coordinates
x = (uv[:, 0] * (image.width - 1)) % image.width
y = ((1 - uv[:, 1]) * (image.height - 1)) % image.height

# access colors from pixel locations
# make sure image is RGBA before getting values
colors = np.asanyarray(image.convert("RGBA"))[
    y.round().astype(np.int64), x.round().astype(np.int64)
]
```

The modulus (`%`) operation on float values can cause index out of bounds errors when rounding values near the bounds. For example:
1. Given `uv[i, 0] == -26.05739974975586` and `image.width == 456`
2. Results in `x[i] == 455.883113861084` after modulus operation `x = (uv[:, 0] * (image.width - 1)) % image.width
`
3. When rounded, `x.round().astype(np.int64) == 456`
4. This exceeds valid image indices `[0, 455]`, causing the error

I have fixed this issue in this pull request, please let me know if you have any further concerns!

Best,

Zeyu




Patch:
diff --git a/pyproject.toml b/pyproject.toml
index eac53178..3609dcd9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -120,7 +120,6 @@ test_more = [
     "matplotlib",
     "pymeshlab",
     "triangle",
-    "ipython",
 ]
 
 # interfaces.gmsh will be dropped Jan 2025
diff --git a/trimesh/__init__.py b/trimesh/__init__.py
index ec75e39c..40d3b85c 100644
--- a/trimesh/__init__.py
+++ b/trimesh/__init__.py
@@ -54,7 +54,6 @@ from .exchange.load import (
     load_mesh,
     load_path,
     load_remote,
-    load_scene,
 )
 
 # geometry objects
@@ -109,7 +108,6 @@ __all__ = [
     "load_mesh",
     "load_path",
     "load_remote",
-    "load_scene",
     "nsphere",
     "path",
     "permutate",
diff --git a/trimesh/__main__.py b/trimesh/__main__.py
index 8240cda8..3f988994 100644
--- a/trimesh/__main__.py
+++ b/trimesh/__main__.py
@@ -30,8 +30,7 @@ def main():
     args = parser.parse_args()
 
     if args.file_name is None:
-        parser.print_help()
-        return
+        scene = None
     else:
         scene = load(args.file_name)
 
diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py
index 29962894..63217877 100644
--- a/trimesh/exchange/gltf.py
+++ b/trimesh/exchange/gltf.py
@@ -15,9 +15,9 @@ import numpy as np
 from .. import rendering, resources, transformations, util, visual
 from ..caching import hash_fast
 from ..constants import log, tol
-from ..resolvers import ResolverLike, ZipResolver
+from ..resolvers import Resolver, ZipResolver
 from ..scene.cameras import Camera
-from ..typed import NDArray, Optional, Stream
+from ..typed import Mapping, NDArray, Optional, Stream, Union
 from ..util import triangle_strips_to_faces, unique_name
 from ..visual.gloss import specular_to_pbr
 
@@ -50,6 +50,9 @@ _default_material = {
     }
 }
 
+# we can accept dict resolvers
+ResolverLike = Union[Resolver, Mapping]
+
 # GL geometry modes
 _GL_LINES = 1
 _GL_POINTS = 0
@@ -452,22 +455,22 @@ def load_glb(
     return kwargs
 
 
-def _uri_to_bytes(uri: str, resolver: ResolverLike) -> bytes:
+def _uri_to_bytes(uri, resolver):
     """
     Take a URI string and load it as a
     a filename or as base64.
 
     Parameters
     --------------
-    uri
+    uri : string
       Usually a filename or something like:
       "data:object/stuff,base64,AABA112A..."
-    resolver
+    resolver : trimesh.visual.Resolver
       A resolver to load referenced assets
 
     Returns
     ---------------
-    data
+    data : bytes
       Loaded data from URI
     """
     # see if the URI has base64 data
diff --git a/trimesh/exchange/load.py b/trimesh/exchange/load.py
index e45ba912..b6951dc7 100644
--- a/trimesh/exchange/load.py
+++ b/trimesh/exchange/load.py
@@ -1,6 +1,5 @@
 import json
 import os
-from dataclasses import dataclass
 
 import numpy as np
 
@@ -10,8 +9,8 @@ from ..exceptions import ExceptionWrapper
 from ..parent import Geometry
 from ..points import PointCloud
 from ..scene.scene import Scene, append_scenes
-from ..typed import Loadable, Optional, Stream
-from ..util import log
+from ..typed import Dict, List, Loadable, Optional, Union
+from ..util import log, now
 from . import misc
 from .binvox import _binvox_loaders
 from .cascade import _cascade_loaders
@@ -32,8 +31,8 @@ try:
 except BaseException as E:
     # save a traceback to see why path didn't import
     load_path = ExceptionWrapper(E)
-
     # no path formats available
+
     def path_formats() -> set:
         return set()
 
@@ -72,100 +71,13 @@ def available_formats() -> set:
 def load(
     file_obj: Loadable,
     file_type: Optional[str] = None,
-    resolver: Optional[resolvers.ResolverLike] = None,
+    resolver: Union[resolvers.Resolver, Dict, None] = None,
     force: Optional[str] = None,
-    allow_remote: bool = False,
-    **kwargs,
-) -> Geometry:
-    """
-
-    For new code the typed load functions `trimesh.load_scene` or `trimesh.load_mesh`
-    are recommended over `trimesh.load` which is a backwards-compatibility wrapper
-    that mimics the behavior of the old function and can return any geometry type.
-
-    Parameters
-    -----------
-    file_obj : str, or file- like object
-      The source of the data to be loadeded
-    file_type: str
-      What kind of file type do we have (eg: 'stl')
-    resolver : trimesh.visual.Resolver
-      Object to load referenced assets like materials and textures
-    force : None or str
-      For 'mesh': try to coerce scenes into a single mesh
-      For 'scene': try to coerce everything into a scene
-    allow_remote
-      If True allow this load call to work on a remote URL.
-    kwargs : dict
-      Passed to geometry __init__
-
-    Returns
-    ---------
-    geometry : Trimesh, Path2D, Path3D, Scene
-      Loaded geometry as trimesh classes
-    """
-
-    # call the most general loading case into a `Scene`.
-    loaded = load_scene(
-        file_obj=file_obj,
-        file_type=file_type,
-        resolver=resolver,
-        allow_remote=allow_remote,
-        **kwargs,
-    )
-
-    if force == "mesh":
-        # new code should use `load_mesh` for this
-        log.debug(
-            "`trimesh.load(force='mesh')` is a compatibility wrapper for `trimesh.load_mesh`"
-        )
-        return loaded.to_mesh()
-    elif force == "scene":
-        # new code should use `load_scene` for this
-        log.debug(
-            "`trimesh.load(force='scene')` is a compatibility wrapper for `trimesh.load_scene`"
-        )
-        return loaded
-
-    # else:
-    #    log.debug(
-    #        "For new code the typed load functions `trimesh.load_scene` or `trimesh.load_mesh` "
-    #        + "are recommended over `trimesh.load` which is a backwards-compatibility wrapper "
-    #        + "that mimics the behavior of the old function and can return any geometry type."
-    #    )
-
-    ###########################################
-    # we are matching deprecated behavior here!
-    # matching old behavior you should probably use `load_scene`
-    if len(loaded.geometry) == 1:
-        kind = loaded.metadata.get("file_type", file_type)
-        geom = next(iter(loaded.geometry.values()))
-        if (kind not in {"glb", "gltf"} and isinstance(geom, PointCloud)) or kind in {
-            "obj",
-            "stl",
-            "ply",
-            "svg",
-            "binvox",
-            "xaml",
-            "dxf",
-            "off",
-        }:
-            return geom
-
-    return loaded
-
-
-def load_scene(
-    file_obj: Loadable,
-    file_type: Optional[str] = None,
-    resolver: Optional[resolvers.ResolverLike] = None,
-    allow_remote: bool = False,
     **kwargs,
-) -> Scene:
+) -> Union[Geometry, List[Geometry]]:
     """
-    Load geometry into the `trimesh.Scene` container. This may contain
-    any `parent.Geometry` object, including `Trimesh`, `Path2D`, `Path3D`,
-    or a `PointCloud`.
+    Load a mesh or vectorized path into objects like
+    Trimesh, Path2D, Path3D, Scene
 
     Parameters
     -----------
@@ -178,8 +90,6 @@ def load_scene(
     force : None or str
       For 'mesh': try to coerce scenes into a single mesh
       For 'scene': try to coerce everything into a scene
-    allow_remote
-      If True allow this load call to work on a remote URL.
     kwargs : dict
       Passed to geometry __init__
 
@@ -188,77 +98,77 @@ def load_scene(
     geometry : Trimesh, Path2D, Path3D, Scene
       Loaded geometry as trimesh classes
     """
+    # check to see if we're trying to load something
+    # that is already a native trimesh Geometry subclass
+    if isinstance(file_obj, Geometry):
+        log.info("Load called on %s object, returning input", file_obj.__class__.__name__)
+        return file_obj
 
-    # parse all possible values of file objects into simple types
-    arg = _parse_file_args(
-        file_obj=file_obj,
-        file_type=file_type,
-        resolver=resolver,
-        allow_remote=allow_remote,
-        **kwargs,
-    )
+    # parse the file arguments into clean loadable form
+    (
+        file_obj,  # file- like object
+        file_type,  # str, what kind of file
+        metadata,  # dict, any metadata from file name
+        opened,  # bool, did we open the file ourselves
+        resolver,  # object to load referenced resources
+    ) = _parse_file_args(file_obj=file_obj, file_type=file_type, resolver=resolver)
 
     try:
-        if arg.file_type in path_formats():
+        if isinstance(file_obj, dict):
+            # if we've been passed a dict treat it as kwargs
+            kwargs.update(file_obj)
+            loaded = load_kwargs(kwargs)
+        elif file_type in path_formats():
             # path formats get loaded with path loader
-            loaded = load_path(
-                file_obj=arg.file_obj,
-                file_type=arg.file_type,
-                metadata=arg.metadata,
-                **kwargs,
-            )
-        elif arg.file_type in ["svg", "dxf"]:
-            # call the dummy function to raise the import error
-            # this prevents the exception from being super opaque
-            load_path()
-        elif isinstance(arg.file_obj, dict):
-            loaded = _load_kwargs(arg.file_obj)
-        elif arg.file_type in mesh_loaders:
+            loaded = load_path(file_obj, file_type=file_type, **kwargs)
+        elif file_type in mesh_loaders:
             # mesh loaders use mesh loader
-
-            loaded = _load_kwargs(
-                mesh_loaders[arg.file_type](
-                    file_obj=arg.file_obj,
-                    file_type=arg.file_type,
-                    resolver=arg.resolver,
-                    metadata=arg.metadata,
-                    **kwargs,
-                )
-            )
-        elif arg.file_type in compressed_loaders:
+            loaded = load_mesh(file_obj, file_type=file_type, resolver=resolver, **kwargs)
+        elif file_type in compressed_loaders:
             # for archives, like ZIP files
-            loaded = _load_compressed(arg.file_obj, file_type=arg.file_type, **kwargs)
-        elif arg.file_type in voxel_loaders:
-            loaded = voxel_loaders[arg.file_type](
-                file_obj=arg.file_obj,
-                file_type=arg.file_type,
-                resolver=arg.resolver,
-                **kwargs,
+            loaded = load_compressed(file_obj, file_type=file_type, **kwargs)
+        elif file_type in voxel_loaders:
+            loaded = voxel_loaders[file_type](
+                file_obj, file_type=file_type, resolver=resolver, **kwargs
             )
         else:
-            raise ValueError(f"file_type: '{arg.file_type}' not supported")
-
+            if file_type in ["svg", "dxf"]:
+                # call the dummy function to raise the import error
+                # this prevents the exception from being super opaque
+                load_path()
+            else:
+                raise ValueError(f"File type: {file_type} not supported")
     finally:
-        # if we opened the file ourselves from a file name
         # close any opened files even if we crashed out
-        if arg.was_opened:
-            arg.file_obj.close()
+        if opened:
+            file_obj.close()
+
+    # add load metadata ('file_name') to each loaded geometry
+    for i in util.make_sequence(loaded):
+        i.metadata.update(metadata)
 
-    if not isinstance(loaded, Scene):
-        loaded = Scene(loaded)
+    # if we opened the file in this function ourselves from a
+    # file name clean up after ourselves by closing it
+    if opened:
+        file_obj.close()
 
-    # add the "file_path" information to the overall scene metadata
-    if "metadata" not in kwargs:
-        loaded.metadata.update(arg.metadata)
-    # add the load path metadata to every geometry
-    # [g.metadata.update(arg.metadata) for g in loaded.geometry.values()]
+    # combine a scene into a single mesh
+    if force == "mesh" and isinstance(loaded, Scene):
+        return util.concatenate(loaded.dump())
+    if force == "scene" and not isinstance(loaded, Scene):
+        return Scene(loaded)
 
     return loaded
 
 
-def load_mesh(*args, **kwargs) -> Trimesh:
+def load_mesh(
+    file_obj: Loadable,
+    file_type: Optional[str] = None,
+    resolver: Union[resolvers.Resolver, Dict, None] = None,
+    **kwargs,
+) -> Union[Geometry, List[Geometry]]:
     """
-    Load a file into a Trimesh object.
+    Load a mesh file into a Trimesh object.
 
     Parameters
     -----------
@@ -274,10 +184,46 @@ def load_mesh(*args, **kwargs) -> Trimesh:
     mesh
       Loaded geometry data.
     """
-    return load_scene(*args, **kwargs).to_mesh()
 
+    # parse the file arguments into clean loadable form
+    (
+        file_obj,  # file-like object
+        file_type,  # str: what kind of file
+        metadata,  # dict: any metadata from file name
+        opened,  # bool: did we open the file ourselves
+        resolver,  # Resolver: to load referenced resources
+    ) = _parse_file_args(file_obj=file_obj, file_type=file_type, resolver=resolver)
+
+    try:
+        # make sure we keep passed kwargs to loader
+        # but also make sure loader keys override passed keys
+        loader = mesh_loaders[file_type]
+        tic = now()
+        results = loader(file_obj, file_type=file_type, resolver=resolver, **kwargs)
+        if not isinstance(results, list):
+            results = [results]
+
+        loaded = []
+        for result in results:
+            kwargs.update(result)
+            loaded.append(load_kwargs(kwargs))
+            loaded[-1].metadata.update(metadata)
+
+        # todo : remove this
+        if len(loaded) == 1:
+            loaded = loaded[0]
+
+        # show the repr for loaded, loader used, and time
+        log.debug(f"loaded {loaded!s} using `{loader.__name__}` in {now() - tic:0.4f}s")
+    finally:
+        # if we failed to load close file
+        if opened:
+            file_obj.close()
+
+    return loaded
 
-def _load_compressed(file_obj, file_type=None, resolver=None, mixed=False, **kwargs):
+
+def load_compressed(file_obj, file_type=None, resolver=None, mixed=False, **kwargs):
     """
     Given a compressed archive load all the geometry that
     we can from it.
@@ -299,74 +245,86 @@ def _load_compressed(file_obj, file_type=None, resolver=None, mixed=False, **kwa
     """
 
     # parse the file arguments into clean loadable form
-    arg = _parse_file_args(file_obj=file_obj, file_type=file_type, resolver=resolver)
+    (
+        file_obj,  # file- like object
+        file_type,  # str, what kind of file
+        metadata,  # dict, any metadata from file name
+        opened,  # bool, did we open the file ourselves
+        resolver,  # object to load referenced resources
+    ) = _parse_file_args(file_obj=file_obj, file_type=file_type, resolver=resolver)
 
-    # store loaded geometries as a list
-    geometries = []
+    try:
+        # a dict of 'name' : file-like object
+        files = util.decompress(file_obj=file_obj, file_type=file_type)
+        # store loaded geometries as a list
+        geometries = []
 
-    # so loaders can access textures/etc
-    archive = util.decompress(file_obj=arg.file_obj, file_type=arg.file_type)
-    resolver = resolvers.ZipResolver(archive)
-
-    # try to save the files with meaningful metadata
-    archive_name = arg.metadata.get("file_path", "archive")
-    if archive_name is not None:
-        meta_archive = {
-            "file_name": os.path.basename(archive_name),
-            "file_path": os.path.join(archive_name),
-        }
-    else:
-        meta_archive = {}
+        # so loaders can access textures/etc
+        resolver = resolvers.ZipResolver(files)
 
-    # populate our available formats
-    if mixed:
-        available = available_formats()
-    else:
-        # all types contained in ZIP archive
-        contains = {util.split_extension(n).lower() for n in resolver.keys()}
-        # if there are no mesh formats available
-        if contains.isdisjoint(mesh_formats()):
-            available = path_formats()
+        # try to save the files with meaningful metadata
+        if "file_path" in metadata:
+            archive_name = metadata["file_path"]
         else:
-            available = mesh_formats()
+            archive_name = "archive"
 
-    for file_name, file_obj in archive.items():
-        try:
-            # only load formats that we support
-            compressed_type = util.split_extension(file_name).lower()
-
-            # if file has metadata type include it
-            if compressed_type in ("yaml", "yml"):
-                import yaml
-
-                continue
-                meta_archive[file_name] = yaml.safe_load(file_obj)
-            elif compressed_type == "json":
-                import json
-
-                meta_archive[file_name] = json.loads(file_obj)
-                continue
-            elif compressed_type not in available:
-                # don't raise an exception, just try the next one
-                continue
-
-            # load the individual geometry
-            geometries.append(
-                load_scene(
-                    file_obj=file_obj,
+        # populate our available formats
+        if mixed:
+            available = available_formats()
+        else:
+            # all types contained in ZIP archive
+            contains = {util.split_extension(n).lower() for n in files.keys()}
+            # if there are no mesh formats available
+            if contains.isdisjoint(mesh_formats()):
+                available = path_formats()
+            else:
+                available = mesh_formats()
+
+        meta_archive = {}
+        for name, data in files.items():
+            try:
+                # only load formats that we support
+                compressed_type = util.split_extension(name).lower()
+
+                # if file has metadata type include it
+                if compressed_type in "yaml":
+                    import yaml
+
+                    meta_archive[name] = yaml.safe_load(data)
+                elif compressed_type in "json":
+                    import json
+
+                    meta_archive[name] = json.loads(data)
+
+                if compressed_type not in available:
+                    # don't raise an exception, just try the next one
+                    continue
+                # store the file name relative to the archive
+                metadata["file_name"] = archive_name + "/" + os.path.basename(name)
+                # load the individual geometry
+                loaded = load(
+                    file_obj=data,
                     file_type=compressed_type,
                     resolver=resolver,
+                    metadata=metadata,
                     **kwargs,
                 )
-            )
 
-        except BaseException:
-            log.debug("failed to load file in zip", exc_info=True)
+                # some loaders return multiple geometries
+                if util.is_sequence(loaded):
+                    # if the loader has returned a list of meshes
+                    geometries.extend(loaded)
+                else:
+                    # if the loader has returned a single geometry
+                    geometries.append(loaded)
+            except BaseException:
+                log.debug("failed to load file in zip", exc_info=True)
 
-    # if we opened the file in this function
-    # clean up after ourselves
-    if arg.was_opened:
-        arg.file_obj.close()
+    finally:
+        # if we opened the file in this function
+        # clean up after ourselves
+        if opened:
+            file_obj.close()
 
     # append meshes or scenes into a single Scene object
     result = append_scenes(geometries)
@@ -378,34 +336,61 @@ def _load_compressed(file_obj, file_type=None, resolver=None, mixed=False, **kwa
     return result
 
 
-def load_remote(url: str, **kwargs) -> Scene:
+def load_remote(url, **kwargs):
     """
     Load a mesh at a remote URL into a local trimesh object.
 
-    This is a thin wrapper around:
-      `trimesh.load_scene(file_obj=url, allow_remote=True, **kwargs)`
+    This must be called explicitly rather than automatically
+    from trimesh.load to ensure users don't accidentally make
+    network requests.
 
     Parameters
     ------------
-    url
+    url : string
       URL containing mesh file
-    **kwargs
-      Passed to `load_scene`
+    **kwargs : passed to `load`
 
     Returns
     ------------
     loaded : Trimesh, Path, Scene
       Loaded result
     """
-    return load_scene(file_obj=url, allow_remote=True, **kwargs)
+    # import here to keep requirement soft
+    import httpx
+
+    # download the mesh
+    response = httpx.get(url, follow_redirects=True)
+    response.raise_for_status()
+
+    # wrap as file object
+    file_obj = util.wrap_as_stream(response.content)
+
+    # so loaders can access textures/etc
+    resolver = resolvers.WebResolver(url)
+
+    try:
+        # if we have a bunch of query parameters the type
+        # will be wrong so try to clean up the URL
+        # urllib is Python 3 only
+        import urllib
+
+        # remove the url-safe encoding then split off query params
+        file_type = urllib.parse.unquote(url).split("?", 1)[0].split("/")[-1].strip()
+    except BaseException:
+        # otherwise just use the last chunk of URL
+        file_type = url.split("/")[-1].split("?", 1)[0]
+
+    # actually load the data from the retrieved bytes
+    loaded = load(file_obj=file_obj, file_type=file_type, resolver=resolver, **kwargs)
+    return loaded
 
 
-def _load_kwargs(*args, **kwargs) -> Geometry:
+def load_kwargs(*args, **kwargs) -> Geometry:
     """
     Load geometry from a properly formatted dict or kwargs
     """
 
-    def handle_scene() -> Scene:
+    def handle_scene():
         """
         Load a scene from our kwargs.
 
@@ -415,7 +400,7 @@ def _load_kwargs(*args, **kwargs) -> Geometry:
         base_frame: str, base frame of graph
         """
         graph = kwargs.get("graph", None)
-        geometry = {k: _load_kwargs(v) for k, v in kwargs["geometry"].items()}
+        geometry = {k: load_kwargs(v) for k, v in kwargs["geometry"].items()}
 
         if graph is not None:
             scene = Scene()
@@ -458,7 +443,7 @@ def _load_kwargs(*args, **kwargs) -> Geometry:
 
         return scene
 
-    def handle_mesh() -> Trimesh:
+    def handle_mesh():
         """
         Handle the keyword arguments for a Trimesh object
         """
@@ -515,36 +500,21 @@ def _load_kwargs(*args, **kwargs) -> Geometry:
     for func, expected in handlers:
         if all(i in kwargs for i in expected):
             # all expected kwargs exist
-            return func()
-
-    raise ValueError(f"unable to determine type: {kwargs.keys()}")
-
-
-@dataclass
-class _FileArgs:
-    # a file-like object that can be accessed
-    file_obj: Optional[Stream]
-
-    # a cleaned file type string, i.e. "stl"
-    file_type: str
-
-    # any metadata generated from the file path
-    metadata: dict
-
-    # did we open `file_obj` ourselves?
-    was_opened: bool
+            handler = func
+            # exit the loop as we found one
+            break
+    else:
+        raise ValueError(f"unable to determine type: {kwargs.keys()}")
 
-    # a resolver for loading assets next to the file
-    resolver: Optional[resolvers.ResolverLike]
+    return handler()
 
 
 def _parse_file_args(
     file_obj: Loadable,
     file_type: Optional[str],
-    resolver: Optional[resolvers.ResolverLike] = None,
-    allow_remote: bool = False,
+    resolver: Union[None, Dict, resolvers.Resolver] = None,
     **kwargs,
-) -> _FileArgs:
+):
     """
     Given a file_obj and a file_type try to magically convert
     arguments to a file-like object and a lowercase string of
@@ -583,13 +553,21 @@ def _parse_file_args(
 
     Returns
     -----------
-    args
-      Populated `_FileArg` message
+    file_obj : file-like object
+      Contains data
+    file_type : str
+      Lower case of the type of file (eg 'stl', 'dae', etc)
+    metadata : dict
+      Any metadata gathered
+    opened : bool
+      Did we open the file or not
+    resolver : trimesh.visual.Resolver
+      Resolver to load other assets
     """
-
     metadata = {}
     opened = False
-    file_path = None
+    if "metadata" in kwargs and isinstance(kwargs["metadata"], dict):
+        metadata.update(kwargs["metadata"])
 
     if util.is_pathlib(file_obj):
         # convert pathlib objects to string
@@ -597,16 +575,15 @@ def _parse_file_args(
 
     if util.is_file(file_obj) and file_type is None:
         raise ValueError("file_type must be set for file objects!")
-
     if isinstance(file_obj, str):
         try:
             # os.path.isfile will return False incorrectly
             # if we don't give it an absolute path
-            file_path = os.path.abspath(os.path.expanduser(file_obj))
+            file_path = os.path.expanduser(file_obj)
+            file_path = os.path.abspath(file_path)
             exists = os.path.isfile(file_path)
         except BaseException:
             exists = False
-            file_path = None
 
         # file obj is a string which exists on filesystm
         if exists:
@@ -614,6 +591,8 @@ def _parse_file_args(
             if resolver is None:
                 resolver = resolvers.FilePathResolver(file_path)
             # save the file name and path to metadata
+            metadata["file_path"] = file_path
+            metadata["file_name"] = os.path.basename(file_obj)
             # if file_obj is a path that exists use extension as file_type
             if file_type is None:
                 file_type = util.split_extension(file_path, special=["tar.gz", "tar.bz2"])
@@ -622,23 +601,13 @@ def _parse_file_args(
             opened = True
         else:
             if "{" in file_obj:
-                # if a bracket is in the string it's probably straight JSON
+                # if a dict bracket is in the string, its probably a straight
+                # JSON
                 file_type = "json"
             elif "https://" in file_obj or "http://" in file_obj:
-                if not allow_remote:
-                    raise ValueError("unable to load URL with `allow_remote=False`")
-
-                import urllib
-
-                # remove the url-safe encoding and query params
-                file_type = util.split_extension(
-                    urllib.parse.unquote(file_obj).split("?", 1)[0].split("/")[-1].strip()
-                )
-                # create a web resolver to do the fetching and whatnot
-                resolver = resolvers.WebResolver(url=file_obj)
-                # fetch the base file
-                file_obj = util.wrap_as_stream(resolver.get_base())
-
+                # we've been passed a URL, warn to use explicit function
+                # and don't do network calls via magical pipeline
+                raise ValueError(f"use load_remote to load URL: {file_obj}")
             elif file_type is None:
                 raise ValueError(f"string is not a file: {file_obj}")
 
@@ -648,7 +617,9 @@ def _parse_file_args(
     if isinstance(file_type, str) and "." in file_type:
         # if someone has passed the whole filename as the file_type
         # use the file extension as the file_type
-        file_path = file_type
+        if "file_path" not in metadata:
+            metadata["file_path"] = file_type
+        metadata["file_name"] = os.path.basename(file_type)
         file_type = util.split_extension(file_type)
         if resolver is None and os.path.exists(file_type):
             resolver = resolvers.FilePathResolver(file_type)
@@ -656,16 +627,6 @@ def _parse_file_args(
     # all our stored extensions reference in lower case
     file_type = file_type.lower()
 
-    # if user passed in a metadata dict add it
-    if len(kwargs.get("metadata", {})) > 0:
-        metadata = kwargs["metadata"]
-    else:
-        metadata["file_type"] = file_type
-        if file_path is not None:
-            metadata.update(
-                {"file_path": file_path, "file_name": os.path.basename(file_path)}
-            )
-
     # if we still have no resolver try using file_obj name
     if (
         resolver is None
@@ -675,21 +636,15 @@ def _parse_file_args(
     ):
         resolver = resolvers.FilePathResolver(file_obj.name)
 
-    return _FileArgs(
-        file_obj=file_obj,
-        file_type=file_type,
-        metadata=metadata,
-        was_opened=opened,
-        resolver=resolver,
-    )
+    return file_obj, file_type, metadata, opened, resolver
 
 
 # loader functions for compressed extensions
 compressed_loaders = {
-    "zip": _load_compressed,
-    "tar.bz2": _load_compressed,
-    "tar.gz": _load_compressed,
-    "bz2": _load_compressed,
+    "zip": load_compressed,
+    "tar.bz2": load_compressed,
+    "tar.gz": load_compressed,
+    "bz2": load_compressed,
 }
 
 # map file_type to loader function
diff --git a/trimesh/exchange/obj.py b/trimesh/exchange/obj.py
index bff36248..841d183c 100644
--- a/trimesh/exchange/obj.py
+++ b/trimesh/exchange/obj.py
@@ -163,12 +163,10 @@ def load_obj(
             log.debug("faces have mixed data: using slow fallback!")
             faces, faces_tex, faces_norm = _parse_faces_fallback(face_lines)
 
-        if group_material and len(materials) > 1:
+        if group_material:
             name = material
-        elif current_object is not None:
-            name = current_object
         else:
-            name = kwargs.get("metadata", {}).get("file_name", "geometry")
+            name = current_object
 
         # ensure the name is always unique
         name = util.unique_name(name, geometry)
@@ -220,13 +218,9 @@ def load_obj(
                     faces, faces_norm, maintain_faces=maintain_order
                 )
             else:
-                # face_tex is None and
                 # generate the mask so we only include
                 # referenced vertices in every new mesh
-                if maintain_order:
-                    mask_v = np.ones(len(v), dtype=bool)
-                else:
-                    mask_v = np.zeros(len(v), dtype=bool)
+                mask_v = np.zeros(len(v), dtype=bool)
                 mask_v[faces] = True
 
                 # reconstruct the faces with the new vertex indices
@@ -275,11 +269,17 @@ def load_obj(
         # store geometry by name
         geometry[name] = mesh
 
+    if len(geometry) == 1:
+        # TODO : should this be removed to always return a scene?
+        return next(iter(geometry.values()))
+
     # add an identity transform for every geometry
     graph = [{"geometry": k, "frame_to": k} for k in geometry.keys()]
 
     # convert to scene kwargs
-    return {"geometry": geometry, "graph": graph}
+    result = {"geometry": geometry, "graph": graph}
+
+    return result
 
 
 def parse_mtl(mtl, resolver=None):
diff --git a/trimesh/graph.py b/trimesh/graph.py
index 8fa73964..0e2894c9 100644
--- a/trimesh/graph.py
+++ b/trimesh/graph.py
@@ -705,7 +705,8 @@ def edges_to_coo(edges, count=None, data=None):
     if data is None:
         data = np.ones(len(edges), dtype=bool)
 
-    return coo_matrix((data, edges.T), dtype=data.dtype, shape=(count, count))
+    matrix = coo_matrix((data, edges.T), dtype=data.dtype, shape=(count, count))
+    return matrix
 
 
 def neighbors(edges, max_index=None, directed=False):
diff --git a/trimesh/path/exchange/load.py b/trimesh/path/exchange/load.py
index e2805bf3..16dea51e 100644
--- a/trimesh/path/exchange/load.py
+++ b/trimesh/path/exchange/load.py
@@ -32,7 +32,7 @@ def load_path(file_obj, file_type: Optional[str] = None, **kwargs):
       Data as a native trimesh Path file_object
     """
     # avoid a circular import
-    from ...exchange.load import _load_kwargs
+    from ...exchange.load import load_kwargs
 
     if isinstance(file_type, str):
         # we accept full file names here so make sure we
@@ -72,14 +72,14 @@ def load_path(file_obj, file_type: Optional[str] = None, **kwargs):
         kwargs.update(misc.linestrings_to_path(file_obj))
     elif isinstance(file_obj, dict):
         # load as kwargs
-        return _load_kwargs(file_obj)
+        return load_kwargs(file_obj)
     elif util.is_sequence(file_obj):
         # load as lines in space
         kwargs.update(misc.lines_to_path(file_obj))
     else:
         raise ValueError("Not a supported object type!")
 
-    result = _load_kwargs(kwargs)
+    result = load_kwargs(kwargs)
     util.log.debug(f"loaded {result!s} in {util.now() - tic:0.4f}s")
 
     return result
diff --git a/trimesh/resolvers.py b/trimesh/resolvers.py
index 226ae350..02e0ba62 100644
--- a/trimesh/resolvers.py
+++ b/trimesh/resolvers.py
@@ -12,7 +12,7 @@ import itertools
 import os
 
 from . import caching, util
-from .typed import Dict, Mapping, Optional, Union
+from .typed import Optional, Union
 
 # URL parsing for remote resources via WebResolver
 try:
@@ -37,24 +37,20 @@ class Resolver(util.ABC):
         raise NotImplementedError()
 
     @abc.abstractmethod
-    def write(self, name: str, data):
+    def write(self, name, data):
         raise NotImplementedError("`write` not implemented!")
 
     @abc.abstractmethod
-    def namespaced(self, namespace: str):
+    def namespaced(self, namespace):
         raise NotImplementedError("`namespaced` not implemented!")
 
-    @abc.abstractmethod
-    def keys(self):
-        raise NotImplementedError("`keys` not implemented!")
-
-    def __getitem__(self, key: str):
+    def __getitem__(self, key):
         return self.get(key)
 
-    def __setitem__(self, key: str, value):
+    def __setitem__(self, key, value):
         return self.write(key, value)
 
-    def __contains__(self, key: str) -> bool:
+    def __contains__(self, key):
         return key in self.keys()
 
 
@@ -167,7 +163,7 @@ class ZipResolver(Resolver):
     Resolve files inside a ZIP archive.
     """
 
-    def __init__(self, archive: Optional[Dict] = None, namespace: Optional[str] = None):
+    def __init__(self, archive=None, namespace=None):
         """
         Resolve files inside a ZIP archive as loaded by
         trimesh.util.decompress
@@ -209,7 +205,7 @@ class ZipResolver(Resolver):
             ]
         return self.archive.keys()
 
-    def write(self, key: str, value) -> None:
+    def write(self, key: str, value):
         """
         Store a value in the current archive.
 
@@ -224,7 +220,7 @@ class ZipResolver(Resolver):
             self.archive = {}
         self.archive[key] = value
 
-    def get(self, name: str) -> bytes:
+    def get(self, name: str):
         """
         Get an asset from the ZIP archive.
 
@@ -306,7 +302,7 @@ class WebResolver(Resolver):
     Resolve assets from a remote URL.
     """
 
-    def __init__(self, url: str):
+    def __init__(self, url):
         """
         Resolve assets from a base URL.
 
@@ -321,6 +317,7 @@ class WebResolver(Resolver):
 
         # parse string into namedtuple
         parsed = urlparse(url)
+
         # we want a base url
         split = [i for i in parsed.path.split("/") if len(i) > 0]
 
@@ -334,11 +331,6 @@ class WebResolver(Resolver):
         else:
             # recombine into string ignoring any double slashes
             path = "/".join(split)
-
-        # save the URL we were created with, i.e.
-        # `https://stuff.com/models/thing.glb`
-        self.url = url
-        # save the root url, i.e. `https://stuff.com/models`
         self.base_url = (
             "/".join(
                 i
@@ -353,7 +345,7 @@ class WebResolver(Resolver):
         # we should always have ended with a single slash
         assert self.base_url.endswith("/")
 
-    def get(self, name: str) -> bytes:
+    def get(self, name):
         """
         Get a resource from the remote site.
 
@@ -386,25 +378,7 @@ class WebResolver(Resolver):
         # return the bytes of the response
         return response.content
 
-    def get_base(self) -> bytes:
-        """
-        Fetch the data at the full URL this resolver was
-        instantiated with, i.e. `https://stuff.com/hi.glb`
-        this will return the response.
-
-        Returns
-        --------
-        content
-          The value at `self.url`
-        """
-        import httpx
-
-        # just fetch the url we were created with
-        response = httpx.get(self.url, follow_redirects=True)
-        response.raise_for_status()
-        return response.content
-
-    def namespaced(self, namespace: str) -> "WebResolver":
+    def namespaced(self, namespace):
         """
         Return a namespaced version of current resolver.
 
@@ -422,10 +396,7 @@ class WebResolver(Resolver):
         return WebResolver(url=self.base_url + namespace)
 
     def write(self, key, value):
-        raise NotImplementedError("`WebResolver` is read-only!")
-
-    def keys(self):
-        raise NotImplementedError("`WebResolver` can't list keys")
+        raise NotImplementedError("can't write to remote")
 
 
 class GithubResolver(Resolver):
@@ -484,6 +455,7 @@ class GithubResolver(Resolver):
     @property
     def zipped(self) -> ZipResolver:
         """
+
         - opened zip file
         - locally saved zip file
         - retrieve zip file and saved
@@ -597,7 +569,3 @@ def nearby_names(name, namespace=None):
         strip = namespace.strip("/").split("/")[: -name.count("..")]
         strip.extend(name.split("..")[-1].strip("/").split("/"))
         yield "/".join(strip)
-
-
-# most loaders can use a mapping in additon to a resolver
-ResolverLike = Union[Resolver, Mapping]
diff --git a/trimesh/resources/__init__.py b/trimesh/resources/__init__.py
index 77b48a38..f0c66241 100644
--- a/trimesh/resources/__init__.py
+++ b/trimesh/resources/__init__.py
@@ -1,15 +1,81 @@
 import json
 import os
-from io import BytesIO
+import warnings
 
-from ..typed import Dict
+from ..typed import Dict, Stream
+from ..util import decode_text, wrap_as_stream
 
 # find the current absolute path to this directory
 _pwd = os.path.expanduser(os.path.abspath(os.path.dirname(__file__)))
+
 # once resources are loaded cache them
 _cache = {}
 
 
+def get(
+    name: str, decode: bool = True, decode_json: bool = False, as_stream: bool = False
+):
+    """
+    DERECATED JANUARY 2025 REPLACE WITH TYPED `get_json`, `get_string`, etc.
+    """
+    warnings.warn(
+        "`trimesh.resources.get` is deprecated "
+        + "and will be removed in January 2025: "
+        + "replace with typed `trimesh.resources.get_*type*`",
+        category=DeprecationWarning,
+        stacklevel=2,
+    )
+    return _get(name=name, decode=decode, decode_json=decode_json, as_stream=as_stream)
+
+
+def _get(name: str, decode: bool, decode_json: bool, as_stream: bool):
+    """
+    Get a resource from the `trimesh/resources` folder.
+
+    Parameters
+    -------------
+    name : str
+      File path relative to `trimesh/resources`
+    decode : bool
+      Whether or not to decode result as UTF-8
+    decode_json : bool
+      Run `json.loads` on resource if True.
+    as_stream : bool
+      Return as a file-like object
+
+    Returns
+    -------------
+    resource : str, bytes, or decoded JSON
+      File data
+    """
+    # key by name and decode
+    cache_key = (name, bool(decode), bool(decode_json), bool(as_stream))
+    cached = _cache.get(cache_key)
+    if hasattr(cached, "seek"):
+        cached.seek(0)
+    if cached is not None:
+        return cached
+
+    # get the resource using relative names
+    with open(os.path.join(_pwd, name), "rb") as f:
+        resource = f.read()
+
+    # make sure we return it as a string if asked
+    if decode:
+        # will decode into text if possibly
+        resource = decode_text(resource)
+
+    if decode_json:
+        resource = json.loads(resource)
+    elif as_stream:
+        resource = wrap_as_stream(resource)
+
+    # store for later access
+    _cache[cache_key] = resource
+
+    return resource
+
+
 def get_schema(name: str) -> Dict:
     """
     Load a schema and evaluate the referenced files.
@@ -29,8 +95,8 @@ def get_schema(name: str) -> Dict:
 
     # get a resolver for our base path
     resolver = FilePathResolver(os.path.join(_pwd, "schema", name))
-    # recursively load `$ref` keys
-    return resolve(json.loads(resolver.get(name).decode("utf-8")), resolver=resolver)
+    # recursively load $ref keys
+    return resolve(json.loads(decode_text(resolver.get(name))), resolver=resolver)
 
 
 def get_json(name: str) -> Dict:
@@ -40,14 +106,14 @@ def get_json(name: str) -> Dict:
     Parameters
     -------------
     name : str
-      File path relative to `trimesh/resources/{name}`
+      File path relative to `trimesh/resources`
 
     Returns
     -------------
     resource
       File data decoded from JSON.
     """
-    return json.loads(get_bytes(name).decode("utf-8"))
+    return _get(name, decode=True, decode_json=True, as_stream=False)
 
 
 def get_string(name: str) -> str:
@@ -56,7 +122,7 @@ def get_string(name: str) -> str:
 
     Parameters
     -------------
-    name
+    name : str
       File path relative to `trimesh/resources`
 
     Returns
@@ -64,7 +130,7 @@ def get_string(name: str) -> str:
     resource
       File data as a string.
     """
-    return get_bytes(name).decode("utf-8")
+    return _get(name, decode=True, decode_json=False, as_stream=False)
 
 
 def get_bytes(name: str) -> bytes:
@@ -73,7 +139,7 @@ def get_bytes(name: str) -> bytes:
 
     Parameters
     -------------
-    name
+    name : str
       File path relative to `trimesh/resources`
 
     Returns
@@ -81,21 +147,10 @@ def get_bytes(name: str) -> bytes:
     resource
       File data as raw bytes.
     """
-    cached = _cache.get(name, None)
-    if cached is not None:
-        return cached
-
-    # get the resource using relative names
-    # all templates are using POSIX relative paths
-    # so fix them to be platform-specific
-    with open(os.path.join(_pwd, *name.split("/")), "rb") as f:
-        resource = f.read()
-
-    _cache[name] = resource
-    return resource
+    return _get(name, decode=False, decode_json=False, as_stream=False)
 
 
-def get_stream(name: str) -> BytesIO:
+def get_stream(name: str) -> Stream:
     """
     Get a resource from the `trimesh/resources` folder as a binary stream.
 
@@ -110,4 +165,4 @@ def get_stream(name: str) -> BytesIO:
       File data as a binary stream.
     """
 
-    return BytesIO(get_bytes(name))
+    return _get(name, decode=False, decode_json=False, as_stream=True)
diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py
index 6797d493..51946c47 100644
--- a/trimesh/scene/scene.py
+++ b/trimesh/scene/scene.py
@@ -186,6 +186,10 @@ class Scene(Geometry3D):
             self.graph.transforms = concat.graph.transforms
             return
 
+        if not hasattr(geometry, "vertices"):
+            util.log.debug(f"unknown type ({type(geometry).__name__}) added to scene!")
+            return
+
         # get or create a name to reference the geometry by
         if geom_name is not None:
             # if name is passed use it
@@ -359,8 +363,9 @@ class Scene(Geometry3D):
         corners = {}
         # collect vertices for every mesh
         vertices = {
-            k: m.vertices if hasattr(m, "vertices") and len(m.vertices) > 0 else m.bounds
+            k: m.vertices
             for k, m in self.geometry.items()
+            if hasattr(m, "vertices") and len(m.vertices) > 0
         }
         # handle 2D geometries
         vertices.update(
diff --git a/trimesh/visual/color.py b/trimesh/visual/color.py
index d0c9f364..9967523f 100644
--- a/trimesh/visual/color.py
+++ b/trimesh/visual/color.py
@@ -866,7 +866,8 @@ def uv_to_color(uv, image):
     # access colors from pixel locations
     # make sure image is RGBA before getting values
     colors = np.asanyarray(image.convert("RGBA"))[
-        y.round().astype(np.int64), x.round().astype(np.int64)
+        y.round().astype(np.int64) % image.height, 
+        x.round().astype(np.int64) % image.width
     ]
 
     # conversion to RGBA should have corrected shape


Test patch:
diff --git a/tests/test_export.py b/tests/test_export.py
index ea750297..e671fc78 100644
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -336,6 +336,8 @@ class ExportTest(g.unittest.TestCase):
         # it's wordy
         f = g.trimesh.exchange.load._parse_file_args
 
+        RET_COUNT = 5
+
         # a path that doesn't exist
         nonexists = f"/banana{g.random()}"
         assert not g.os.path.exists(nonexists)
@@ -346,11 +348,13 @@ class ExportTest(g.unittest.TestCase):
 
         # should be able to extract type from passed filename
         args = f(file_obj=exists, file_type=None)
-        assert args.file_type == "obj"
+        assert len(args) == RET_COUNT
+        assert args[1] == "obj"
 
         # should be able to extract correct type from longer name
         args = f(file_obj=exists, file_type="YOYOMA.oBj")
-        assert args.file_type == "obj"
+        assert len(args) == RET_COUNT
+        assert args[1] == "obj"
 
         # with a nonexistent file and no extension it should raise
         try:
@@ -363,13 +367,15 @@ class ExportTest(g.unittest.TestCase):
         # nonexistent file with extension passed should return
         # file name anyway, maybe something else can handle it
         args = f(file_obj=nonexists, file_type=".ObJ")
+        assert len(args) == RET_COUNT
         # should have cleaned up case
-        assert args.file_type == "obj"
+        assert args[1] == "obj"
 
         # make sure overriding type works for string filenames
         args = f(file_obj=exists, file_type="STL")
+        assert len(args) == RET_COUNT
         # should have used manually passed type over .obj
-        assert args.file_type == "stl"
+        assert args[1] == "stl"
 
     def test_buffered_random(self):
         """Test writing to non-standard file"""
diff --git a/tests/test_gltf.py b/tests/test_gltf.py
index c2c78c8e..c43a5e9e 100644
--- a/tests/test_gltf.py
+++ b/tests/test_gltf.py
@@ -53,9 +53,6 @@ def validate_glb(data, name=None):
         raise ValueError("gltf_validator failed")
 
 
-load_kwargs = g.trimesh.exchange.load._load_kwargs
-
-
 class GLTFTest(g.unittest.TestCase):
     def test_duck(self):
         scene = g.get_mesh("Duck.glb", process=False)
@@ -199,7 +196,7 @@ class GLTFTest(g.unittest.TestCase):
 
         kwargs = g.trimesh.exchange.gltf.load_glb(g.trimesh.util.wrap_as_stream(export))
         # roundtrip it
-        reloaded = load_kwargs(kwargs)
+        reloaded = g.trimesh.exchange.load.load_kwargs(kwargs)
         # make basic assertions
         g.scene_equal(original, reloaded)
 
@@ -267,7 +264,7 @@ class GLTFTest(g.unittest.TestCase):
         assert len(export.keys()) == 2
 
         # reload the export
-        reloaded = load_kwargs(
+        reloaded = g.trimesh.exchange.load.load_kwargs(
             g.trimesh.exchange.gltf.load_gltf(
                 file_obj=None, resolver=g.trimesh.visual.resolvers.ZipResolver(export)
             )
diff --git a/tests/test_html.py b/tests/test_html.py
index a56bb544..135ef9b3 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -24,9 +24,6 @@ class ViewTest(g.unittest.TestCase):
         children = list(h.body.iterchildren())
         assert len(children) >= 2
 
-        # make sure this is returning anything
-        assert js.scene_to_notebook(s) is not None
-
     def test_inNB(self):
         import trimesh.viewer.notebook as js
 
diff --git a/tests/test_loaded.py b/tests/test_loaded.py
index 09105545..92b8a10c 100644
--- a/tests/test_loaded.py
+++ b/tests/test_loaded.py
@@ -11,9 +11,8 @@ class LoaderTest(g.unittest.TestCase):
         """
         # get a unit cube from localhost
         with g.serve_meshes() as address:
-            scene = g.trimesh.exchange.load.load_remote(url=address + "/unit_cube.STL")
+            mesh = g.trimesh.exchange.load.load_remote(url=address + "/unit_cube.STL")
 
-        mesh = scene.to_mesh()
         assert g.np.isclose(mesh.volume, 1.0)
         assert isinstance(mesh, g.trimesh.Trimesh)
 
@@ -36,7 +35,7 @@ class LoaderTest(g.unittest.TestCase):
         # 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_mesh(file_obj=file_obj, file_type="stl")
+        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
diff --git a/tests/test_obj.py b/tests/test_obj.py
index e5480675..c241e436 100644
--- a/tests/test_obj.py
+++ b/tests/test_obj.py
@@ -28,18 +28,6 @@ class OBJTest(g.unittest.TestCase):
         rec = g.roundtrip(m.export(file_type="obj"), file_type="obj")
         assert g.np.isclose(m.area, rec.area)
 
-    def test_keep_unreferenced(self):
-        # try loading a short mesh with 2 vertices, one of which is referenced
-        m = g.trimesh.load(
-            g.trimesh.util.wrap_as_stream("o mesh\nv 1 1 1\nv 1 1 2\nf 1 1 1"),
-            file_type="obj",
-            process=False,
-            maintain_order=True,
-        )
-
-        assert g.np.allclose(m.faces[0], [0, 0, 0])
-        assert g.np.allclose(m.vertices, [[1, 1, 1], [1, 1, 2]])
-
     def test_trailing(self):
         # test files with texture and trailing slashes
         m = g.get_mesh("jacked.obj")
@@ -347,9 +335,6 @@ class OBJTest(g.unittest.TestCase):
 
     def test_scene_export_material_name(self):
         s = g.get_mesh("fuze.obj", force="scene")
-
-        g.log.warning(s.geometry)
-
         dummy = "fuxx"
         s.geometry["fuze.obj"].visual.material.name = dummy
 
diff --git a/tests/test_svg.py b/tests/test_svg.py
index 8cebf8ab..04127806 100644
--- a/tests/test_svg.py
+++ b/tests/test_svg.py
@@ -126,7 +126,7 @@ class ExportTest(g.unittest.TestCase):
                 assert g.np.isclose(a.area, b.area)
                 assert a.body_count == b.body_count
 
-            # assert r.metadata["file_path"].endswith(fn[3:])
+            assert r.metadata["file_path"].endswith(fn[3:])
 
 
 if __name__ == "__main__":
diff --git a/tests/test_voxel.py b/tests/test_voxel.py
index 341ecf62..f5d82c98 100644
--- a/tests/test_voxel.py
+++ b/tests/test_voxel.py
@@ -10,17 +10,13 @@ class VoxelGridTest(g.unittest.TestCase):
         Test that voxels work at all
         """
         for m in [
-            g.get_mesh("featuretype.STL", force="mesh"),
+            g.get_mesh("featuretype.STL"),
             g.trimesh.primitives.Box(),
             g.trimesh.primitives.Sphere(),
         ]:
             for pitch in [0.1, 0.1 - g.tol.merge]:
                 surface = m.voxelized(pitch=pitch)
 
-                scene = g.trimesh.Scene(surface)
-                assert len(scene.geometry) == 1
-                assert g.np.allclose(scene.bounds, surface.bounds)
-
                 # make sure the voxelized pitch is similar to passed
                 assert g.np.allclose(surface.pitch, pitch)