From cfe689a98faf9fb2fa2bccee707c99e372a89f2d Mon Sep 17 00:00:00 2001 From: Madiyar Date: Sun, 25 Feb 2024 11:01:22 +0000 Subject: [PATCH 01/11] Fix typing for lights in Scene.__init__ --- trimesh/scene/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 485434272..2367a0147 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -27,7 +27,7 @@ def __init__( metadata: Optional[Dict] = None, graph: Optional[SceneGraph] = None, camera: Optional[cameras.Camera] = None, - lights: Optional[lighting.Light] = None, + lights: Optional[List[lighting.Light]] = None, camera_transform: Optional[NDArray] = None, ): """ From 01b58fd9c8cd11c21858684b1fccdfa818e5fbc5 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sun, 25 Feb 2024 22:56:33 -0500 Subject: [PATCH 02/11] merge --- Dockerfile | 5 ++++- pyproject.toml | 3 ++- trimesh/exchange/gltf.py | 4 ++-- trimesh/graph.py | 4 ++-- trimesh/path/path.py | 21 ++++++++++----------- trimesh/path/polygons.py | 5 ++--- trimesh/scene/scene.py | 32 ++++++++++++++++---------------- trimesh/scene/transforms.py | 4 ++-- trimesh/util.py | 12 ++++++------ 9 files changed, 46 insertions(+), 44 deletions(-) diff --git a/Dockerfile b/Dockerfile index 96479d6f9..2e9342c41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,10 @@ RUN pip install -e .[all] RUN ruff trimesh # run pytest wrapped with xvfb for simple viewer tests -RUN xvfb-run pytest --cov=trimesh \ +## TODO : get typeguard to pass on more/all of the codebase +RUN xvfb-run pytest \ + --cov=trimesh \ + --typeguard-packages=trimesh.scene \ -p no:ALL_DEPENDENCIES \ -p no:INCLUDE_RENDERING \ -p no:cacheprovider tests diff --git a/pyproject.toml b/pyproject.toml index ca117e911..536f9ee2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,8 @@ test = [ "pymeshlab", "pyinstrument", "matplotlib", - "ruff" + "ruff", + "typeguard" ] # requires pip >= 21.2 diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py index f851fdb33..19c4e55bb 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, diff --git a/trimesh/graph.py b/trimesh/graph.py index f0a5440a6..dd14d891d 100644 --- a/trimesh/graph.py +++ b/trimesh/graph.py @@ -16,7 +16,7 @@ from . import exceptions, grouping, util from .constants import log, tol from .geometry import faces_to_edges -from .typed import Optional +from .typed import List, Optional try: from scipy.sparse import coo_matrix, csgraph @@ -324,7 +324,7 @@ def facets(mesh, engine=None): return components -def split(mesh, only_watertight=True, adjacency=None, engine=None, **kwargs): +def split(mesh, only_watertight=True, adjacency=None, engine=None, **kwargs) -> List: """ Split a mesh into multiple meshes from face connectivity. 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/scene/scene.py b/trimesh/scene/scene.py index 2367a0147..0b9a29d91 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -6,7 +6,7 @@ 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 Dict, List, NDArray, Optional, Sequence, Tuple, Union, float64, int64 from ..util import unique_name from . import cameras, lighting from .transforms import SceneGraph @@ -22,12 +22,12 @@ class Scene(Geometry3D): def __init__( self, - geometry: Union[Geometry, None, List[Geometry]] = None, + geometry: Union[Geometry, Sequence[Geometry], NDArray[Geometry], None] = None, base_frame: str = "world", metadata: Optional[Dict] = None, graph: Optional[SceneGraph] = None, camera: Optional[cameras.Camera] = None, - lights: Optional[List[lighting.Light]] = None, + lights: Optional[Sequence[lighting.Light]] = None, camera_transform: Optional[NDArray] = None, ): """ @@ -71,11 +71,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): """ @@ -242,7 +243,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. @@ -756,7 +757,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 +796,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 +830,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 +876,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 +919,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 +1077,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, Sequence]) -> "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/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 From 8af58bea046c2efb0838a626e7a1a42322fbd979 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 13:36:05 -0500 Subject: [PATCH 03/11] typeguard passing on test_scene --- pyproject.toml | 2 +- trimesh/scene/scene.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 536f9ee2d..7f671a1d5 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." diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 0b9a29d91..75a7897f8 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -11,6 +11,11 @@ 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,7 +27,7 @@ class Scene(Geometry3D): def __init__( self, - geometry: Union[Geometry, Sequence[Geometry], NDArray[Geometry], None] = None, + geometry: Optional[GeometryInput] = None, base_frame: str = "world", metadata: Optional[Dict] = None, graph: Optional[SceneGraph] = None, @@ -59,8 +64,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 = {} @@ -96,7 +102,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, @@ -212,7 +218,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. @@ -637,7 +643,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. From 3bb0a96b13b6b678c5a150f6c5da601bfc94c9f7 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 13:45:16 -0500 Subject: [PATCH 04/11] passing typeguard locally --- trimesh/base.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/trimesh/base.py b/trimesh/base.py index eec6bfa42..dac80bdb4 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. @@ -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]]: @@ -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, From e6223a08cbf975ff75c2b945a57bfea83b3d7127 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 14:01:58 -0500 Subject: [PATCH 05/11] ruff check for implicit string concat --- pyproject.toml | 4 ++++ trimesh/boolean.py | 4 ++-- trimesh/typed.py | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7f671a1d5..4d3de3532 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,7 @@ select = [ "UP", # upgrade "W", # style warnings "YTT", # sys.version + "ISC002" ] ignore = [ @@ -141,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/boolean.py b/trimesh/boolean.py index d6749fed1..9226d53da 100644 --- a/trimesh/boolean.py +++ b/trimesh/boolean.py @@ -175,8 +175,8 @@ def boolean_manifold( 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.", + + " use Manifold ('manifold'), which should be equivalent. In future versions" + + " of Trimesh, attempting to use engine 'scad' may raise an error.", DeprecationWarning, stacklevel=2, ) 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", From 6c3d5272b5d585445aea093b0698618d3254aeec Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 15:43:24 -0500 Subject: [PATCH 06/11] replace trimesh.resources.get with typed equivilants --- trimesh/exchange/gltf.py | 4 +- trimesh/exchange/ply.py | 2 +- trimesh/interfaces/blender.py | 4 +- trimesh/path/exchange/dxf.py | 2 +- trimesh/path/exchange/svg_io.py | 2 +- trimesh/resources/__init__.py | 96 +++++++++++++++++++++++++++++++-- trimesh/units.py | 8 +-- trimesh/viewer/notebook.py | 6 +-- 8 files changed, 103 insertions(+), 21 deletions(-) diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py index 19c4e55bb..caae5f6d6 100644 --- a/trimesh/exchange/gltf.py +++ b/trimesh/exchange/gltf.py @@ -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", " 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/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/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") ) From 5e5a62a8c433c61f997dfdec5d2f115f6aadd4a9 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 15:58:48 -0500 Subject: [PATCH 07/11] run typeguard on all tests that start with *s* --- Dockerfile | 14 ++++++++++++-- trimesh/base.py | 30 +++++++++++++++--------------- trimesh/scene/scene.py | 15 +++++++++++++-- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2e9342c41..cc93105a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,15 +72,25 @@ RUN pip install -e .[all] # check formatting RUN ruff trimesh -# run pytest wrapped with xvfb for simple viewer tests + + ## 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 \ - --typeguard-packages=trimesh.scene \ -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/trimesh/base.py b/trimesh/base.py index dac80bdb4..31081dc2f 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -1155,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. @@ -1221,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. @@ -2168,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 @@ -2212,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. @@ -2224,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 diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 75a7897f8..987b05805 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -6,7 +6,18 @@ 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, Sequence, 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 @@ -1083,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, Sequence]) -> "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. From 6c578152d6d79c2d1619b029bb57b2052faefd06 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 16:10:43 -0500 Subject: [PATCH 08/11] enforce new typeguard --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4d3de3532..a7934d03a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ test = [ "pyinstrument", "matplotlib", "ruff", - "typeguard" + "typeguard>=4.1.5" ] # requires pip >= 21.2 From c783e945b63a29a03d7b2e205d0eab805640f757 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 16:14:00 -0500 Subject: [PATCH 09/11] relax typeguard version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a7934d03a..74a90cea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ test = [ "pyinstrument", "matplotlib", "ruff", - "typeguard>=4.1.5" + "typeguard>=4.1.2" ] # requires pip >= 21.2 From b9759791640196ffcb48b23330c508a6c9ef8991 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 16:27:51 -0500 Subject: [PATCH 10/11] remove interfaces.scad --- README.md | 2 +- pyproject.toml | 1 + trimesh/boolean.py | 14 ------- trimesh/interfaces/scad.py | 76 ----------------------------------- trimesh/resources/__init__.py | 2 +- 5 files changed, 3 insertions(+), 92 deletions(-) delete mode 100644 trimesh/interfaces/scad.py 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 74a90cea6..7f9a97d21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ test = [ "pyinstrument", "matplotlib", "ruff", + "gmsh", "typeguard>=4.1.2" ] diff --git a/trimesh/boolean.py b/trimesh/boolean.py index 9226d53da..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/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/resources/__init__.py b/trimesh/resources/__init__.py index 3ae0712fc..040b017ce 100644 --- a/trimesh/resources/__init__.py +++ b/trimesh/resources/__init__.py @@ -20,7 +20,7 @@ def get( """ warnings.warn( "`trimesh.resources.get` is deprecated " - + +"and will be removed in January 2025: " + + "and will be removed in January 2025: " + "replace with typed `trimesh.resouces.get_*type*`", category=DeprecationWarning, stacklevel=2, From 3c62b0b8dcf6a5de8f4d1a21e7ef86db819f4502 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 26 Feb 2024 17:15:46 -0500 Subject: [PATCH 11/11] remove gmsh from tests --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7f9a97d21..74a90cea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,6 @@ test = [ "pyinstrument", "matplotlib", "ruff", - "gmsh", "typeguard>=4.1.2" ]