diff --git a/Dockerfile b/Dockerfile index 96479d6f9..cc93105a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,12 +72,25 @@ RUN pip install -e .[all] # check formatting RUN ruff trimesh + + +## TODO : get typeguard to pass on more/all of the codebase +## this is running on a very arbitrary subset right now! +RUN pytest \ + --typeguard-packages=trimesh.scene,trimesh.base \ + -p no:ALL_DEPENDENCIES \ + -p no:INCLUDE_RENDERING \ + -p no:cacheprovider tests/test_s* + + # run pytest wrapped with xvfb for simple viewer tests -RUN xvfb-run pytest --cov=trimesh \ +RUN xvfb-run pytest \ + --cov=trimesh \ -p no:ALL_DEPENDENCIES \ -p no:INCLUDE_RENDERING \ -p no:cacheprovider tests + # set codecov token as a build arg to upload ARG CODECOV_TOKEN="" RUN curl -Os https://uploader.codecov.io/latest/linux/codecov && \ diff --git a/README.md b/README.md index b4a1e760a..edb177736 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ print(mesh.bounding_box_oriented.volume, * Determine if a mesh is watertight, convex, etc. * Uniformly sample the surface of a mesh * Ray-mesh queries including location, triangle index, etc. -* Boolean operations on meshes (intersection, union, difference) using OpenSCAD or Blender as a back end. Note that mesh booleans in general are usually slow and unreliable +* Boolean operations on meshes (intersection, union, difference) using Manifold3D or Blender Note that mesh booleans in general are usually slow and unreliable * Voxelize watertight meshes * Volume mesh generation (TETgen) using Gmsh SDK * Smooth watertight meshes using laplacian smoothing algorithms (Classic, Taubin, Humphrey) diff --git a/pyproject.toml b/pyproject.toml index ca117e911..74a90cea6 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.5" +version = "4.1.6" authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}] license = {file = "LICENSE.md"} description = "Import, export, process, analyze and view triangular meshes." @@ -103,7 +103,8 @@ test = [ "pymeshlab", "pyinstrument", "matplotlib", - "ruff" + "ruff", + "typeguard>=4.1.2" ] # requires pip >= 21.2 @@ -129,6 +130,7 @@ select = [ "UP", # upgrade "W", # style warnings "YTT", # sys.version + "ISC002" ] ignore = [ @@ -140,6 +142,9 @@ ignore = [ "B905", # zip() without an explicit strict= parameter ] +# don't allow implicit string concatenation +flake8-implicit-str-concat = {"allow-multiline" = false} + [tool.codespell] skip = "*.js*,./docs/built/*,./docs/generate/*,./models*,*.toml" ignore-words-list = "nd,coo,whats,bu,childs,mis,filetests" \ No newline at end of file diff --git a/trimesh/base.py b/trimesh/base.py index eec6bfa42..31081dc2f 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -7,7 +7,6 @@ import copy import warnings -from typing import Any, Dict, List, Optional, Tuple, Union import numpy as np from numpy import float64, int64, ndarray @@ -43,7 +42,7 @@ from .parent import Geometry3D from .scene import Scene from .triangles import MassProperties -from .typed import ArrayLike, NDArray +from .typed import Any, ArrayLike, Dict, List, NDArray, Optional, Tuple, Union from .visual import ColorVisuals, TextureVisuals, create_visual try: @@ -72,8 +71,8 @@ class Trimesh(Geometry3D): def __init__( self, - vertices: Optional[NDArray[float64]] = None, - faces: Optional[NDArray[int64]] = None, + vertices: Optional[ArrayLike] = None, + faces: Optional[ArrayLike] = None, face_normals: Optional[NDArray[float64]] = None, vertex_normals: Optional[NDArray[float64]] = None, face_colors: Optional[NDArray[float64]] = None, @@ -451,7 +450,7 @@ def vertices(self) -> NDArray[float64]: return self._data.get("vertices", np.empty(shape=(0, 3), dtype=float64)) @vertices.setter - def vertices(self, values: NDArray[float64]): + def vertices(self, values: ArrayLike): """ Assign vertex values to the mesh. @@ -491,7 +490,7 @@ def vertex_normals(self) -> NDArray[float64]: return vertex_normals @vertex_normals.setter - def vertex_normals(self, values: NDArray[float64]) -> None: + def vertex_normals(self, values: ArrayLike) -> None: """ Assign values to vertex normals. @@ -1156,8 +1155,8 @@ def merge_vertices( def update_vertices( self, - mask: NDArray[bool], - inverse: Optional[NDArray] = None, + mask: ArrayLike, + inverse: Optional[ArrayLike] = None, ) -> None: """ Update vertices with a mask. @@ -1222,7 +1221,7 @@ def update_vertices( except BaseException: pass - def update_faces(self, mask: NDArray[bool]) -> None: + def update_faces(self, mask: ArrayLike) -> None: """ In many cases, we will want to remove specific faces. However, there is additional bookkeeping to do this cleanly. @@ -1551,8 +1550,7 @@ def vertex_adjacency_graph(self) -> Graph: > [1, 2, 3, 4] """ - adjacency_g = graph.vertex_adjacency_graph(mesh=self) - return adjacency_g + return graph.vertex_adjacency_graph(mesh=self) @caching.cache_decorator def vertex_neighbors(self) -> List[List[int64]]: @@ -2170,23 +2168,23 @@ def visual(self, value): self._visual = value def section( - self, plane_normal: List[int], plane_origin: List[int], **kwargs - ) -> Path3D: + self, plane_normal: ArrayLike, plane_origin: ArrayLike, **kwargs + ) -> Optional[Path3D]: """ Returns a 3D cross section of the current mesh and a plane defined by origin and normal. Parameters ------------ - plane_normal: (3) vector for plane normal - Normal vector of section plane + plane_normal : (3,) float + Normal vector of section plane. plane_origin : (3, ) float - Point on the cross section plane + Point on the cross section plane. Returns --------- - intersections: Path3D or None - Curve of intersection + intersections + Curve of intersection or None if it was not hit by plane. """ # turn line segments into Path2D/Path3D objects from .exchange.load import load_path @@ -2214,10 +2212,10 @@ def section( def section_multiplane( self, - plane_origin: NDArray[float64], - plane_normal: NDArray[float64], - heights: NDArray[float64], - ): + plane_origin: ArrayLike, + plane_normal: ArrayLike, + heights: ArrayLike, + ) -> List[Optional[Path2D]]: """ Return multiple parallel cross sections of the current mesh in 2D. @@ -2226,7 +2224,7 @@ def section_multiplane( ------------ plane_origin : (3, ) float Point on the cross section plane - plane_normal: (3) vector for plane normal + plane_normal : (3) float Normal vector of section plane heights : (n, ) float Each section is offset by height along @@ -2367,8 +2365,7 @@ def convex_hull(self) -> "Trimesh": convex : trimesh.Trimesh Mesh of convex hull of current mesh """ - hull = convex.convex_hull(self) - return hull + return convex.convex_hull(self) def sample( self, diff --git a/trimesh/boolean.py b/trimesh/boolean.py index d6749fed1..38da7893e 100644 --- a/trimesh/boolean.py +++ b/trimesh/boolean.py @@ -4,7 +4,6 @@ Do boolean operations on meshes using either Blender or Manifold. """ -import warnings import numpy as np @@ -172,22 +171,9 @@ def boolean_manifold( return out_mesh -def boolean_scad(*args, **kwargs): - warnings.warn( - "The OpenSCAD interface is deprecated, and Trimesh will instead" - " use Manifold ('manifold'), which should be equivalent. In future versions" - " of Trimesh, attempting to use engine 'scad' may raise an error.", - DeprecationWarning, - stacklevel=2, - ) - return boolean_manifold(*args, **kwargs) - - # which backend boolean engines _engines = { None: boolean_manifold, - "auto": boolean_manifold, "manifold": boolean_manifold, - "scad": boolean_scad, "blender": interfaces.blender.boolean, } diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py index f851fdb33..caae5f6d6 100644 --- a/trimesh/exchange/gltf.py +++ b/trimesh/exchange/gltf.py @@ -15,7 +15,7 @@ from .. import rendering, resources, transformations, util, visual from ..caching import hash_fast from ..constants import log, tol -from ..typed import NDArray +from ..typed import NDArray, Optional from ..util import unique_name from ..visual.gloss import specular_to_pbr @@ -775,7 +775,7 @@ def _append_mesh( name, tree, buffer_items, - include_normals: bool, + include_normals: Optional[bool], unitize_normals: bool, mat_hashes: dict, extension_webp: bool, @@ -2057,9 +2057,9 @@ def get_schema(): from ..schemas import resolve # get a blob of a zip file including the GLTF 2.0 schema - blob = resources.get("schema/gltf2.schema.zip", decode=False) + stream = resources.get_stream("schema/gltf2.schema.zip") # get the zip file as a dict keyed by file name - archive = util.decompress(util.wrap_as_stream(blob), "zip") + archive = util.decompress(stream, "zip") # get a resolver object for accessing the schema resolver = ZipResolver(archive) # get a loaded dict from the base file diff --git a/trimesh/exchange/ply.py b/trimesh/exchange/ply.py index f494dc9a6..1deac93ea 100644 --- a/trimesh/exchange/ply.py +++ b/trimesh/exchange/ply.py @@ -285,7 +285,7 @@ def export_ply( dtype_color = ("rgba", " List: """ Split a mesh into multiple meshes from face connectivity. diff --git a/trimesh/interfaces/blender.py b/trimesh/interfaces/blender.py index 0492b825c..9136920a9 100644 --- a/trimesh/interfaces/blender.py +++ b/trimesh/interfaces/blender.py @@ -72,7 +72,7 @@ def boolean( else: solver_options = "FAST" # get the template from our resources folder - template = resources.get("templates/blender_boolean.py.tmpl") + template = resources.get_string("templates/blender_boolean.py.tmpl") script = template.replace("$OPERATION", operation) script = script.replace("$SOLVER_OPTIONS", solver_options) script = script.replace("$USE_SELF", f"{use_self}") @@ -97,7 +97,7 @@ def unwrap( raise ValueError("No blender available!") # get the template from our resources folder - template = resources.get("templates/blender_unwrap.py") + template = resources.get_string("templates/blender_unwrap.py") script = template.replace("$ANGLE_LIMIT", "%.6f" % angle_limit).replace( "$ISLAND_MARGIN", "%.6f" % island_margin ) diff --git a/trimesh/interfaces/scad.py b/trimesh/interfaces/scad.py deleted file mode 100644 index 4c97238e2..000000000 --- a/trimesh/interfaces/scad.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import platform -from subprocess import CalledProcessError - -from ..constants import log -from ..util import which -from .generic import MeshScript - -# start the search with the user's PATH -_search_path = os.environ.get("PATH", "") -# add additional search locations on windows -if platform.system() == "Windows": - # split existing path by delimiter - _search_path = [i for i in _search_path.split(";") if len(i) > 0] - _search_path.append(os.path.normpath(r"C:\Program Files\OpenSCAD")) - _search_path.append(os.path.normpath(r"C:\Program Files (x86)\OpenSCAD")) - _search_path = ";".join(_search_path) - log.debug("searching for scad in: %s", _search_path) -# add mac-specific search locations -if platform.system() == "Darwin": - _search_path = [i for i in _search_path.split(":") if len(i) > 0] - _search_path.append("/Applications/OpenSCAD.app/Contents/MacOS") - _search_path = ":".join(_search_path) - log.debug("searching for scad in: %s", _search_path) -# try to find the SCAD executable by name -_scad_executable = which("openscad", path=_search_path) -if _scad_executable is None: - _scad_executable = which("OpenSCAD", path=_search_path) -exists = _scad_executable is not None - - -def interface_scad(meshes, script, debug=False, **kwargs): - """ - A way to interface with openSCAD which is itself an interface - to the CGAL CSG bindings. - CGAL is very stable if difficult to install/use, so this function provides a - tempfile- happy solution for getting the basic CGAL CSG functionality. - - Parameters - --------- - meshes: list of Trimesh objects - script: string of the script to send to scad. - Trimesh objects can be referenced in the script as - $mesh_0, $mesh_1, etc. - """ - if not exists: - raise ValueError("No SCAD available!") - # OFF is a simple text format that references vertices by-index - # making it slightly preferable to STL for this kind of exchange duty - try: - with MeshScript( - meshes=meshes, script=script, debug=debug, exchange="off" - ) as scad: - result = scad.run(_scad_executable + " $SCRIPT -o $MESH_POST") - except CalledProcessError as e: - # Check if scad is complaining about an empty top level geometry. - # If so, just return an empty Trimesh object. - if "Current top level object is empty." in e.output.decode(): - from .. import Trimesh - - return Trimesh() - else: - raise - - return result - - -def boolean(meshes, operation="difference", debug=False, **kwargs): - """ - Run an operation on a set of meshes - """ - script = operation + "(){" - for i in range(len(meshes)): - script += 'import("$MESH_' + str(i) + '");' - script += "}" - return interface_scad(meshes, script, debug=debug, **kwargs) diff --git a/trimesh/path/exchange/dxf.py b/trimesh/path/exchange/dxf.py index 4e53759cc..32140681e 100644 --- a/trimesh/path/exchange/dxf.py +++ b/trimesh/path/exchange/dxf.py @@ -558,7 +558,7 @@ def export_dxf(path, only_layers=None): Path formatted as a DXF file """ # get the template for exporting DXF files - template = resources.get("templates/dxf.json", decode_json=True) + template = resources.get_json("templates/dxf.json") def format_points(points, as_2D=False, increment=True): """ diff --git a/trimesh/path/exchange/svg_io.py b/trimesh/path/exchange/svg_io.py index 8d2af6851..1c0cd874d 100644 --- a/trimesh/path/exchange/svg_io.py +++ b/trimesh/path/exchange/svg_io.py @@ -558,7 +558,7 @@ def export_svg(drawing, return_path=False, only_layers=None, digits=None, **kwar return " ".join(v[1] for v in pairs) # fetch the export template for the base SVG file - template_svg = resources.get("templates/base.svg") + template_svg = resources.get_string("templates/base.svg") elements = [] for meta, path_string in pairs: diff --git a/trimesh/path/path.py b/trimesh/path/path.py index 8c1f01ddf..dc5be5528 100644 --- a/trimesh/path/path.py +++ b/trimesh/path/path.py @@ -17,7 +17,7 @@ from ..constants import tol_path as tol from ..geometry import plane_transform from ..points import plane_fit -from ..typed import Dict, List, NDArray, Optional, float64 +from ..typed import Dict, List, NDArray, Optional, Sequence, float64 from ..visual import to_rgba from . import ( creation, # NOQA @@ -71,7 +71,7 @@ class Path(parent.Geometry): def __init__( self, - entities: Optional[List[Entity]] = None, + entities: Optional[Sequence[Entity]] = None, vertices: Optional[NDArray[float64]] = None, metadata: Optional[Dict] = None, process: bool = True, @@ -98,17 +98,17 @@ def __init__( # assign each color to each entity self.colors = colors - # collect metadata into new dictionary + # collect metadata self.metadata = {} - if metadata.__class__.__name__ == "dict": + if isinstance(metadata, dict): self.metadata.update(metadata) # cache will dump whenever self.crc changes self._cache = caching.Cache(id_function=self.__hash__) if process: - # literally nothing will work if vertices - # aren't merged properly + # if our input had disconnected but identical points + # pretty much nothing will work if vertices aren't merged properly self.merge_vertices() def __repr__(self): @@ -314,8 +314,7 @@ def centroid(self): centroid : (d,) float Approximate centroid of the path """ - centroid = self.bounds.mean(axis=0) - return centroid + return self.bounds.mean(axis=0) @property def extents(self): @@ -1069,7 +1068,7 @@ def to_3D(self, transform=None): return path_3D @caching.cache_decorator - def polygons_closed(self): + def polygons_closed(self) -> NDArray: """ Cycles in the vertex graph, as shapely.geometry.Polygons. These are polygon objects for every closed circuit, with no notion @@ -1078,14 +1077,14 @@ def polygons_closed(self): Returns --------- - polygons_closed: (n,) list of shapely.geometry.Polygon objects + polygons_closed : (n,) list of shapely.geometry.Polygon objects """ # will attempt to recover invalid garbage geometry # and will be None if geometry is unrecoverable return polygons.paths_to_polygons(self.discrete) @caching.cache_decorator - def polygons_full(self): + def polygons_full(self) -> NDArray: """ A list of shapely.geometry.Polygon objects with interiors created by checking which closed polygons enclose which other polygons. diff --git a/trimesh/path/polygons.py b/trimesh/path/polygons.py index 2075c9039..4c051fd9e 100644 --- a/trimesh/path/polygons.py +++ b/trimesh/path/polygons.py @@ -27,7 +27,7 @@ Index = ExceptionWrapper(E) -def enclosure_tree(polygons: List[Polygon]): +def enclosure_tree(polygons): """ Given a list of shapely polygons with only exteriors, find which curves represent the exterior shell or root curve @@ -365,8 +365,7 @@ def stack_boundaries(boundaries): """ if len(boundaries["holes"]) == 0: return boundaries["shell"] - result = np.vstack((boundaries["shell"], np.vstack(boundaries["holes"]))) - return result + return np.vstack((boundaries["shell"], np.vstack(boundaries["holes"]))) def medial_axis(polygon: Polygon, resolution: Optional[float] = None, clip=None): diff --git a/trimesh/resources/__init__.py b/trimesh/resources/__init__.py index 1d99c9073..040b017ce 100644 --- a/trimesh/resources/__init__.py +++ b/trimesh/resources/__init__.py @@ -1,6 +1,8 @@ import json import os +import warnings +from ..typed import IO, Dict from ..util import decode_text, wrap_as_stream # find the current absolute path to this directory @@ -10,7 +12,23 @@ _cache = {} -def get(name, decode=True, decode_json=False, as_stream=False): +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.resouces.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. @@ -58,7 +76,7 @@ def get(name, decode=True, decode_json=False, as_stream=False): return resource -def get_schema(name): +def get_schema(name: str) -> Dict: """ Load a schema and evaluate the referenced files. @@ -69,7 +87,7 @@ def get_schema(name): Returns ---------- - schema : dict + schema Loaded and resolved schema. """ from ..resolvers import FilePathResolver @@ -78,5 +96,73 @@ def get_schema(name): # get a resolver for our base path resolver = FilePathResolver(os.path.join(_pwd, "schema", name)) # recursively load $ref keys - schema = resolve(json.loads(decode_text(resolver.get(name))), resolver=resolver) - return schema + return resolve(json.loads(decode_text(resolver.get(name))), resolver=resolver) + + +def get_json(name: str) -> Dict: + """ + Get a resource from the `trimesh/resources` folder as a decoded string. + + Parameters + ------------- + name : str + File path relative to `trimesh/resources` + + Returns + ------------- + resource + File data decoded from JSON. + """ + return _get(name, decode=True, decode_json=True, as_stream=False) + + +def get_string(name: str) -> str: + """ + Get a resource from the `trimesh/resources` folder as a decoded string. + + Parameters + ------------- + name : str + File path relative to `trimesh/resources` + + Returns + ------------- + resource + File data as a string. + """ + return _get(name, decode=True, decode_json=False, as_stream=False) + + +def get_bytes(name: str) -> bytes: + """ + Get a resource from the `trimesh/resources` folder as binary data. + + Parameters + ------------- + name : str + File path relative to `trimesh/resources` + + Returns + ------------- + resource + File data as raw bytes. + """ + return _get(name, decode=False, decode_json=False, as_stream=False) + + +def get_stream(name: str) -> IO: + """ + Get a resource from the `trimesh/resources` folder as a binary stream. + + Parameters + ------------- + name : str + File path relative to `trimesh/resources` + + Returns + ------------- + resource + File data as a binary stream. + """ + + return _get(name, decode=False, decode_json=False, as_stream=True) diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 485434272..987b05805 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -6,11 +6,27 @@ from .. import caching, convex, grouping, inertia, transformations, units, util from ..exchange import export from ..parent import Geometry, Geometry3D -from ..typed import Dict, List, NDArray, Optional, Tuple, Union, float64, int64 +from ..typed import ( + ArrayLike, + Dict, + List, + NDArray, + Optional, + Sequence, + Tuple, + Union, + float64, + int64, +) from ..util import unique_name from . import cameras, lighting from .transforms import SceneGraph +# the types of objects we can create a scene from +GeometryInput = Union[ + Geometry, Sequence[Geometry], NDArray[Geometry], Dict[str, Geometry] +] + class Scene(Geometry3D): """ @@ -22,12 +38,12 @@ class Scene(Geometry3D): def __init__( self, - geometry: Union[Geometry, None, List[Geometry]] = None, + geometry: Optional[GeometryInput] = None, base_frame: str = "world", metadata: Optional[Dict] = None, graph: Optional[SceneGraph] = None, camera: Optional[cameras.Camera] = None, - lights: Optional[lighting.Light] = None, + lights: Optional[Sequence[lighting.Light]] = None, camera_transform: Optional[NDArray] = None, ): """ @@ -59,8 +75,9 @@ def __init__( # create our cache self._cache = caching.Cache(id_function=self.__hash__) - # add passed geometry to scene - self.add_geometry(geometry) + if geometry is not None: + # add passed geometry to scene + self.add_geometry(geometry) # hold metadata about the scene self.metadata = {} @@ -71,11 +88,12 @@ def __init__( # if we've been passed a graph override the default self.graph = graph - self.camera = camera - self.lights = lights - - if camera is not None and camera_transform is not None: - self.camera_transform = camera_transform + if lights is not None: + self.lights = lights + if camera is not None: + self.camera = camera + if camera_transform is not None: + self.camera_transform = camera_transform def apply_transform(self, transform): """ @@ -95,7 +113,7 @@ def apply_transform(self, transform): def add_geometry( self, - geometry: Union[Geometry, None, List[Geometry]] = None, + geometry: GeometryInput, node_name: Optional[str] = None, geom_name: Optional[str] = None, parent_node_name: Optional[str] = None, @@ -211,7 +229,7 @@ def add_geometry( return node_name - def delete_geometry(self, names: Union[set, str]) -> None: + def delete_geometry(self, names: Union[set, str, Sequence]) -> None: """ Delete one more multiple geometries from the scene and also remove any node in the transform graph which references it. @@ -242,7 +260,7 @@ def strip_visuals(self) -> None: if util.is_instance_named(geometry, "Trimesh"): geometry.visual = ColorVisuals(mesh=geometry) - def __hash__(self) -> str: + def __hash__(self) -> int: """ Return information about scene which is hashable. @@ -636,7 +654,7 @@ def deduplicated(self) -> "Scene": def set_camera( self, angles=None, distance=None, center=None, resolution=None, fov=None - ) -> None: + ) -> cameras.Camera: """ Create a camera object for self.camera, and add a transform to self.graph for it. @@ -756,7 +774,7 @@ def camera(self) -> cameras.Camera: return self._camera @camera.setter - def camera(self, camera: cameras.Camera): + def camera(self, camera: Optional[cameras.Camera]): """ Set a camera object for the Scene. @@ -795,7 +813,7 @@ def lights(self) -> List[lighting.Light]: return self._lights @lights.setter - def lights(self, lights: List[lighting.Light]): + def lights(self, lights: Sequence[lighting.Light]): """ Assign a list of light objects to the scene @@ -829,7 +847,7 @@ def rezero(self) -> None: ) self.graph.base_frame = new_base - def dump(self, concatenate: bool = False) -> Union["Scene", List[Geometry]]: + def dump(self, concatenate: bool = False) -> Union[Geometry, List[Geometry]]: """ Append all meshes in scene freezing transforms. @@ -875,7 +893,7 @@ def dump(self, concatenate: bool = False) -> Union["Scene", List[Geometry]]: # if scene has mixed geometry this may drop some of it return util.concatenate(result) - return np.array(result) + return result def subscene(self, node: str) -> "Scene": """ @@ -918,8 +936,7 @@ def convex_hull(self): Trimesh object which is a convex hull of all meshes in scene """ points = util.vstack_empty([m.vertices for m in self.dump()]) - hull = convex.convex_hull(points) - return hull + return convex.convex_hull(points) def export(self, file_obj=None, file_type=None, **kwargs): """ @@ -1077,7 +1094,7 @@ def explode(self, vector=None, origin=None) -> None: T_new[:3, 3] += offset self.graph[node_name] = T_new - def scaled(self, scale: Union[float, NDArray[float64]]) -> "Scene": + def scaled(self, scale: Union[float, ArrayLike]) -> "Scene": """ Return a copy of the current scene, with meshes and scene transforms scaled to the requested factor. diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py index bc9d54d56..1b3aef51e 100644 --- a/trimesh/scene/transforms.py +++ b/trimesh/scene/transforms.py @@ -1,6 +1,6 @@ import collections from copy import deepcopy -from typing import Sequence +from typing import Sequence, Union import numpy as np @@ -474,7 +474,7 @@ def geometry_nodes(self): res[attr["geometry"]].append(node) return res - def remove_geometries(self, geometries: Sequence): + def remove_geometries(self, geometries: Union[str, set, Sequence]): """ Remove the reference for specified geometries from nodes without deleting the node. diff --git a/trimesh/typed.py b/trimesh/typed.py index 35a462753..81845d04b 100644 --- a/trimesh/typed.py +++ b/trimesh/typed.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import IO, Dict, List, Optional, Sequence, Tuple, Union +from typing import IO, Any, Dict, List, Optional, Sequence, Tuple, Union # our default integer and floating point types from numpy import float64, int64 @@ -22,6 +22,7 @@ "IO", "List", "Dict", + "Any", "Tuple", "float64", "int64", diff --git a/trimesh/units.py b/trimesh/units.py index 1cf9ab370..281257a41 100644 --- a/trimesh/units.py +++ b/trimesh/units.py @@ -11,7 +11,7 @@ from .parent import Geometry # scaling factors from various unit systems to inches -_lookup = resources.get("units_to_inches.json", decode_json=True) +_lookup = resources.get_json("units_to_inches.json") def unit_conversion(current: str, desired: str) -> float: @@ -98,7 +98,6 @@ def units_from_metadata(obj: Geometry, guess: bool = True) -> str: units A guess of what the units might be """ - to_inch = resources.get("units_to_inches.json", decode_json=True) # try to guess from metadata for key in ["file_name", "name"]: @@ -112,13 +111,10 @@ def units_from_metadata(obj: Geometry, guess: bool = True) -> str: hints = hints.replace(delim, " ") # loop through each hint for hint in hints.strip().split(): - # key word is "unit" or "units" - if "unit" not in hint: - continue # get rid of keyword and whitespace hint = hint.replace("units", "").replace("unit", "").strip() # if the hint is a valid unit return it - if hint in to_inch: + if hint in _lookup: return hint if not guess: diff --git a/trimesh/util.py b/trimesh/util.py index 2272be79c..aeeb5ee3c 100644 --- a/trimesh/util.py +++ b/trimesh/util.py @@ -257,7 +257,7 @@ def is_sequence(obj) -> bool: return seq -def is_shape(obj, shape, allow_zeros=False): +def is_shape(obj, shape, allow_zeros: bool = False) -> bool: """ Compare the shape of a numpy.ndarray to a target shape, with any value less than zero being considered a wildcard @@ -817,7 +817,7 @@ def distance_to_end(file_obj): return distance -def decimal_to_digits(decimal, min_digits=None): +def decimal_to_digits(decimal, min_digits=None) -> int: """ Return the number of digits to the first nonzero decimal. @@ -834,7 +834,7 @@ def decimal_to_digits(decimal, min_digits=None): digits = abs(int(np.log10(decimal))) if min_digits is not None: digits = np.clip(digits, min_digits, 20) - return digits + return int(digits) def attach_to_log( @@ -1616,14 +1616,14 @@ def submesh( ) for v, f, n, c in zip(vertices, faces, normals, visuals) ] - result = np.array(result) + if only_watertight or repair: # fill_holes will attempt a repair and returns the # watertight status at the end of the repair attempt - watertight = np.array([i.fill_holes() and len(i.faces) >= 4 for i in result]) + watertight = [i.fill_holes() and len(i.faces) >= 4 for i in result] if only_watertight: # remove unrepairable meshes - result = result[watertight] + return [i for i, w in zip(result, watertight) if w] return result diff --git a/trimesh/viewer/notebook.py b/trimesh/viewer/notebook.py index 4820d5e40..32e07bdf4 100644 --- a/trimesh/viewer/notebook.py +++ b/trimesh/viewer/notebook.py @@ -30,9 +30,9 @@ def scene_to_html(scene): # fetch HTML template from ZIP archive # it is bundling all of three.js so compression is nice base = ( - util.decompress( - resources.get("templates/viewer.zip", decode=False), file_type="zip" - )["viewer.html.template"] + util.decompress(resources.get_bytes("templates/viewer.zip"), file_type="zip")[ + "viewer.html.template" + ] .read() .decode("utf-8") )