From 8db15a2f14efa22a0ca9f463e03515594afc149b Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sat, 31 Aug 2024 15:48:00 -0400 Subject: [PATCH 01/27] avoid broken camera in scaled scenes --- tests/test_camera.py | 11 +++++++++++ trimesh/scene/scene.py | 8 ++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_camera.py b/tests/test_camera.py index 98ea3ecb6..a9b99a707 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -116,6 +116,17 @@ def test_ray_index(self): assert all(rid.min(axis=0) == 0) assert all(rid.max(axis=0) == current - 1) + def test_scaled_copy(self): + s = g.get_mesh("cycloidal.3DXML") + + s.units = "mm" + assert s.camera_transform.shape == (4, 4) + + # the camera node should have been removed on copy + b = s.convert_units("m") + b.camera_transform + assert b.camera_transform.shape == (4, 4) + if __name__ == "__main__": g.trimesh.util.attach_to_log() diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index bbc31a9a4..c7a1ec0ea 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -10,6 +10,7 @@ from ..typed import ( ArrayLike, Dict, + Floating, List, NDArray, Optional, @@ -1096,7 +1097,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, ArrayLike]) -> "Scene": + def scaled(self, scale: Union[Floating, ArrayLike]) -> "Scene": """ Return a copy of the current scene, with meshes and scene transforms scaled to the requested factor. @@ -1155,7 +1156,6 @@ def scaled(self, scale: Union[float, ArrayLike]) -> "Scene": if result.geometry[geom_name].vertices.shape[1] == 2: result.geometry[geom_name] = result.geometry[geom_name].to_3D() - # Scale all geometries by un-doing their local rotations first for key in result.graph.nodes_geometry: T, geom_name = result.graph.get(key) # transform from graph should be read-only @@ -1222,6 +1222,10 @@ def scaled(self, scale: Union[float, ArrayLike]) -> "Scene": result.graph.update( frame_to=node, matrix=transform, geometry=geometry ) + + # remove camera from copied + result._camera = None + return result def copy(self) -> "Scene": From c4a0b62c554381cfa2fb7c472a5e7f1898f24de8 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sat, 31 Aug 2024 16:53:24 -0400 Subject: [PATCH 02/27] docker changes --- Dockerfile | 14 +++----------- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 70c4b50af..f134a1bfd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ RUN python3.12 -m venv venv # So scripts installed from pip are in $PATH ENV PATH="/home/user/venv/bin:$PATH" +ENV VIRTUAL_ENV="/home/user/venv" # Install helper script to PATH. COPY --chmod=755 docker/trimesh-setup /home/user/venv/bin @@ -29,13 +30,6 @@ COPY --chmod=755 docker/trimesh-setup /home/user/venv/bin ## install things that need building FROM base AS build -USER root -# install wget for fetching wheels -RUN apt-get update && \ - apt-get install --no-install-recommends -qq -y wget ca-certificates && \ - apt-get clean -y -USER user - # copy in essential files COPY --chown=499 trimesh/ /home/user/trimesh COPY --chown=499 pyproject.toml /home/user/ @@ -43,10 +37,8 @@ COPY --chown=499 pyproject.toml /home/user/ # install trimesh into the venv RUN pip install /home/user[easy] -# install FCL, which currently has broken wheels on pypi -RUN wget https://github.com/BerkeleyAutomation/python-fcl/releases/download/v0.7.0.7/python_fcl-0.7.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl && \ - pip install python_fcl*.whl && \ - rm python_fcl*.whl +# install FCL which currently has broken wheels on PyPi +RUN pip install https://github.com/BerkeleyAutomation/python-fcl/releases/download/v0.7.0.7/python_fcl-0.7.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl #################################### ### Build output image most things should run on diff --git a/pyproject.toml b/pyproject.toml index 7c3b08fa7..d5cc5e60f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"] [project] name = "trimesh" requires-python = ">=3.8" -version = "4.4.8" +version = "4.4.9" authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}] license = {file = "LICENSE.md"} description = "Import, export, process, analyze and view triangular meshes." From 63cb6923b87c1a70d6eb2aa5e663c4977848d032 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sun, 1 Sep 2024 14:28:09 -0400 Subject: [PATCH 03/27] merge --- trimesh/__init__.py | 11 +++++++++-- trimesh/scene/transforms.py | 8 ++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/trimesh/__init__.py b/trimesh/__init__.py index 7a0472089..00ebec5d2 100644 --- a/trimesh/__init__.py +++ b/trimesh/__init__.py @@ -11,6 +11,7 @@ # avoid a circular import in trimesh.base from . import ( boolean, + bounds, caching, collision, comparison, @@ -24,6 +25,7 @@ grouping, inertia, intersections, + nsphere, permutate, poses, primitives, @@ -45,7 +47,13 @@ from .constants import tol # loader functions -from .exchange.load import available_formats, load, load_mesh, load_path, load_remote +from .exchange.load import ( + available_formats, + load, + load_mesh, + load_path, + load_remote, +) # geometry objects from .parent import Geometry @@ -118,6 +126,5 @@ "unitize", "units", "util", - "utilScene", "voxel", ] diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py index 7b006c3a9..d837d0cba 100644 --- a/trimesh/scene/transforms.py +++ b/trimesh/scene/transforms.py @@ -6,7 +6,7 @@ from .. import caching, util from ..caching import hash_fast from ..transformations import fix_rigid, quaternion_matrix, rotation_matrix -from ..typed import Sequence, Union +from ..typed import ArrayLike, NDArray, Sequence, Tuple, Union # we compare to identity a lot _identity = np.eye(4) @@ -504,11 +504,11 @@ def remove_geometries(self, geometries: Union[str, set, Sequence]): def __contains__(self, key): return key in self.transforms.node_data - def __getitem__(self, key): + def __getitem__(self, key: str) -> Tuple[NDArray, str]: return self.get(key) - def __setitem__(self, key, value): - value = np.asanyarray(value) + def __setitem__(self, key: str, value: ArrayLike): + value = np.asanyarray(value, dtype=np.float64) if value.shape != (4, 4): raise ValueError("Matrix must be specified!") return self.update(key, matrix=value) From 0a6f395f1d5909b9b3fb6fe3c660dcd054705534 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sun, 1 Sep 2024 14:29:58 -0400 Subject: [PATCH 04/27] ruff --- tests/test_camera.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_camera.py b/tests/test_camera.py index a9b99a707..1d6e5e2b5 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -124,7 +124,6 @@ def test_scaled_copy(self): # the camera node should have been removed on copy b = s.convert_units("m") - b.camera_transform assert b.camera_transform.shape == (4, 4) From 125902cb4336a2d1b302b92887f11b550cf2ac5b Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Sun, 1 Sep 2024 15:06:54 -0400 Subject: [PATCH 05/27] add a scene wrapper for simplification --- tests/test_scenegraph.py | 9 +++++++++ trimesh/registration.py | 35 +++++++++++++++++++++++++++-------- trimesh/scene/scene.py | 38 ++++++++++++++++++++++++++++++++++---- trimesh/typed.py | 5 +++-- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py index 03dc9ec37..2a323df4a 100644 --- a/tests/test_scenegraph.py +++ b/tests/test_scenegraph.py @@ -160,6 +160,15 @@ def test_scene_transform(self): # should have moved from original position assert not g.np.allclose(m.convex_hull.bounds, b) + def test_simplify(self): + # get a scene graph + scene: g.trimesh.Scene = g.get_mesh("cycloidal.3DXML") + + original = scene.dump(concatenate=True) + + scene.simplify_quadric_decimation(percent=0.0, aggression=0) + assert len(scene.dump(concatenate=True).vertices) < len(original.vertices) + def test_reverse(self): tf = g.trimesh.transformations diff --git a/trimesh/registration.py b/trimesh/registration.py index 5eef31afd..15dba1781 100644 --- a/trimesh/registration.py +++ b/trimesh/registration.py @@ -12,6 +12,7 @@ from .points import PointCloud, plane_fit from .transformations import transform_points from .triangles import angles, cross, normals +from .typed import ArrayLike, Integer, Optional try: import scipy.sparse as sparse @@ -26,7 +27,13 @@ def mesh_other( - mesh, other, samples=500, scale=False, icp_first=10, icp_final=50, **kwargs + mesh, + other, + samples: Integer = 500, + scale: bool = False, + icp_first: Integer = 10, + icp_final: Integer = 50, + **kwargs, ): """ Align a mesh with another mesh or a PointCloud using @@ -185,14 +192,25 @@ def key_points(m, count): def procrustes( - a, b, weights=None, reflection=True, translation=True, scale=True, return_cost=True + a: ArrayLike, + b: ArrayLike, + weights: Optional[ArrayLike] = None, + reflection: bool = True, + translation: bool = True, + scale: bool = True, + return_cost: bool = True, ): """ - Perform Procrustes' analysis subject to constraints. Finds the - transformation T mapping a to b which minimizes the square sum - distances between Ta and b, also called the cost. Optionally - specify different weights for the points in a to minimize the - weighted square sum distances between Ta and b. This can + Perform Procrustes' analysis to quickly align two corresponding + point clouds subject to constraints. This is much cheaper than + any other registration method but only applies if the two inputs + correspond in order. + + Finds the transformation T mapping a to b which minimizes the + square sum distances between Ta and b, also called the cost. + + Optionally specify different weights for the points in a to minimize + the weighted square sum distances between Ta and b, which can improve transformation robustness on noisy data if the points' probability distribution is known. @@ -207,7 +225,7 @@ def procrustes( reflection : bool If the transformation is allowed reflections translation : bool - If the transformation is allowed translations + If the transformation is allowed translation and rotation. scale : bool If the transformation is allowed scaling return_cost : bool @@ -291,6 +309,7 @@ def procrustes( if return_cost: transformed = transform_points(a, matrix) + # return the mean euclidean distance squared as the cost cost = ((b - transformed) ** 2).mean() return matrix, transformed, cost else: diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index c7a1ec0ea..06f04ffd3 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -11,6 +11,8 @@ ArrayLike, Dict, Floating, + Integer, + Iterable, List, NDArray, Optional, @@ -25,9 +27,7 @@ from .transforms import SceneGraph # the types of objects we can create a scene from -GeometryInput = Union[ - Geometry, Sequence[Geometry], NDArray[Geometry], Dict[str, Geometry] -] +GeometryInput = Union[Geometry, Iterable[Geometry], Dict[str, Geometry]] class Scene(Geometry3D): @@ -165,7 +165,7 @@ def add_geometry( transform=transform, metadata=metadata, ) - for value in geometry + for value in geometry # type: ignore ] elif isinstance(geometry, dict): # if someone passed us a dict of geometry @@ -262,6 +262,36 @@ def strip_visuals(self) -> None: if util.is_instance_named(geometry, "Trimesh"): geometry.visual = ColorVisuals(mesh=geometry) + def simplify_quadric_decimation( + self, + percent: Optional[Floating] = None, + face_count: Optional[Integer] = None, + aggression: Optional[Integer] = None, + ) -> None: + """ + Apply in-place `mesh.simplify_quadric_decimation` to any meshes + in the scene. + + Parameters + ----------- + percent + A number between 0.0 and 1.0 for how much + face_count + Target number of faces desired in the resulting mesh. + agression + An integer between `0` and `10`, the scale being roughly + `0` is "slow and good" and `10` being "fast and bad." + + """ + # save the updates for after the loop + updates = {} + for k, v in self.geometry.items(): + if hasattr(v, "simplify_quadric_decimation"): + updates[k] = v.simplify_quadric_decimation( + percent=percent, face_count=face_count, aggression=aggression + ) + self.geometry.update(updates) + def __hash__(self) -> int: """ Return information about scene which is hashable. diff --git a/trimesh/typed.py b/trimesh/typed.py index 67289b2d9..18c408011 100644 --- a/trimesh/typed.py +++ b/trimesh/typed.py @@ -42,8 +42,9 @@ # > isinstance(np.ones(1, dtype=np.float32)[0], float) # False Floating = Union[float, floating] -# Many arguments take "any valid number." -Number = Union[float, floating, Integer] +# Many arguments take "any valid number" and don't care if it +# is an integer or a floating point input. +Number = Union[Floating, Integer] __all__ = [ "IO", From 5948baa858149926388ed620a4777fe63dbdf95b Mon Sep 17 00:00:00 2001 From: Doruk Cetin Date: Sun, 1 Sep 2024 21:59:54 +0200 Subject: [PATCH 06/27] docs: fix documentation for path.repair.fill_gaps function had the docstring from segments_to_parameters, copying documentation from path.Path.fill_gaps instead --- trimesh/path/repair.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/trimesh/path/repair.py b/trimesh/path/repair.py index 231b75b73..568c9fb62 100644 --- a/trimesh/path/repair.py +++ b/trimesh/path/repair.py @@ -14,24 +14,13 @@ def fill_gaps(path, distance=0.025): """ - For 3D line segments defined by two points, turn - them in to an origin defined as the closest point along - the line to the zero origin as well as a direction vector - and start and end parameter. + Find vertices without degree 2 and try to connect to + other vertices. Operations are done in-place. Parameters ------------ - segments : (n, 2, 3) float + segments : trimesh.path.Path2D Line segments defined by start and end points - - Returns - -------------- - origins : (n, 3) float - Point on line closest to [0, 0, 0] - vectors : (n, 3) float - Unit line directions - parameters : (n, 2) float - Start and end distance pairs for each line """ # find any vertex without degree 2 (connected to two things) From 03339d874e7f2d53ad7bfd7fdc942c9c6a9f2e50 Mon Sep 17 00:00:00 2001 From: Doruk Cetin Date: Sun, 1 Sep 2024 22:00:33 +0200 Subject: [PATCH 07/27] docs: update ruff commands in contributing guide --- docs/content/contributing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/contributing.md b/docs/content/contributing.md index a82e9f043..16b9ac2de 100644 --- a/docs/content/contributing.md +++ b/docs/content/contributing.md @@ -73,8 +73,8 @@ When you remove the embed and see the profile result you can then tweak the line ### Automatic Formatting Trimesh uses `ruff` for both linting and formatting which is configured in `pyproject.toml`, you can run with: ``` -ruff . --fix -ruff format . +ruff check --fix +ruff format ``` ## Docstrings From 2740d805fb630fc35bec22b42c200c8214c0eaf5 Mon Sep 17 00:00:00 2001 From: Doruk Cetin Date: Sun, 1 Sep 2024 22:04:24 +0200 Subject: [PATCH 08/27] docs: add links to qhull options in oriented_bounds_2D and hull_points --- trimesh/bounds.py | 3 +++ trimesh/convex.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/trimesh/bounds.py b/trimesh/bounds.py index b86a9c249..08b48f4df 100644 --- a/trimesh/bounds.py +++ b/trimesh/bounds.py @@ -28,6 +28,9 @@ def oriented_bounds_2D(points, qhull_options="QbB"): """ Find an oriented bounding box for an array of 2D points. + Details on qhull options: + http://www.qhull.org/html/qh-quick.htm#options + Parameters ---------- points : (n,2) float diff --git a/trimesh/convex.py b/trimesh/convex.py index 3e507b4cb..90806e8d9 100644 --- a/trimesh/convex.py +++ b/trimesh/convex.py @@ -219,6 +219,9 @@ def hull_points(obj, qhull_options="QbB Pp"): """ Try to extract a convex set of points from multiple input formats. + Details on qhull options: + http://www.qhull.org/html/qh-quick.htm#options + Parameters --------- obj: Trimesh object From e620ee0da8dfa153dd9ba577eebb7bc04b16f702 Mon Sep 17 00:00:00 2001 From: Sadi Muhammad Wali Date: Sun, 1 Sep 2024 22:05:36 -0400 Subject: [PATCH 09/27] Update gmsh.py Added optional parameter to load_gmsh to allow running outside the main thread. --- trimesh/interfaces/gmsh.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/trimesh/interfaces/gmsh.py b/trimesh/interfaces/gmsh.py index 4455b73f8..77dc04e70 100644 --- a/trimesh/interfaces/gmsh.py +++ b/trimesh/interfaces/gmsh.py @@ -23,7 +23,7 @@ ) -def load_gmsh(file_name, gmsh_args=None): +def load_gmsh(file_name, gmsh_args=None, interruptible=True): """ Returns a surface mesh from CAD model in Open Cascade Breap (.brep), Step (.stp or .step) and Iges formats @@ -45,6 +45,9 @@ def load_gmsh(file_name, gmsh_args=None): gmsh.option.setNumber max_element : float or None Maximum length of an element in the volume mesh + interruptible : bool + Allows load_gmsh to run outside of the main thread if False, + default behaviour if set to True. Added in 4.12.0 Returns ------------ @@ -115,7 +118,7 @@ def load_gmsh(file_name, gmsh_args=None): log.debug("gmsh unexpected", exc_info=True) if not init: - gmsh.initialize() + gmsh.initialize(interruptible=interruptible) gmsh.option.setNumber("General.Terminal", 1) gmsh.model.add("Surface_Mesh_Generation") From e13a07778583e6bd99a418652662f96a54dae286 Mon Sep 17 00:00:00 2001 From: Sadi Muhammad Wali Date: Mon, 2 Sep 2024 13:04:16 -0400 Subject: [PATCH 10/27] Update gmsh.py Fixed trailing whitespace issue. Added optional argument to load_gmsh to enable it to run outside the main thread. --- trimesh/interfaces/gmsh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trimesh/interfaces/gmsh.py b/trimesh/interfaces/gmsh.py index 77dc04e70..d70f6bddf 100644 --- a/trimesh/interfaces/gmsh.py +++ b/trimesh/interfaces/gmsh.py @@ -46,7 +46,7 @@ def load_gmsh(file_name, gmsh_args=None, interruptible=True): max_element : float or None Maximum length of an element in the volume mesh interruptible : bool - Allows load_gmsh to run outside of the main thread if False, + Allows load_gmsh to run outside of the main thread if False, default behaviour if set to True. Added in 4.12.0 Returns From b8725095cccb53ed73b9723975fa72dfc8c6b8a7 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 2 Sep 2024 15:16:19 -0400 Subject: [PATCH 11/27] procrustes works pretty well --- tests/test_scenegraph.py | 18 ++++++++ trimesh/scene/scene.py | 91 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py index 2a323df4a..5b5ee6607 100644 --- a/tests/test_scenegraph.py +++ b/tests/test_scenegraph.py @@ -307,6 +307,24 @@ def test_translation_origin(self): s.apply_translation(-s.bounds[0]) assert g.np.allclose(s.bounds[0], 0) + def test_reconstruct(self): + original = g.get_mesh("cycloidal.3DXML") + assert isinstance(original, g.trimesh.Scene) + + # get the scene as "baked" meshes with no scene graph + dupe = g.trimesh.Scene(original.dump()) + assert len(dupe.geometry) > len(original.geometry) + + with g.Profiler() as P: + # reconstruct the instancing using `duplicate_nodes` and `procrustes` + rec = dupe.reconstruct_instances() + g.log.info(P.output_text()) + + assert len(rec.graph.nodes_geometry) == len(original.graph.nodes_geometry) + assert len(rec.geometry) == len(original.geometry) + assert g.np.allclose(rec.extents, original.extents, rtol=1e-8) + assert g.np.allclose(rec.center_mass, original.center_mass, rtol=1e-8) + if __name__ == "__main__": g.trimesh.util.attach_to_log() diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 06f04ffd3..bb760af7b 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -7,6 +7,7 @@ from ..constants import log from ..exchange import export from ..parent import Geometry, Geometry3D +from ..registration import procrustes from ..typed import ( ArrayLike, Dict, @@ -682,6 +683,9 @@ def deduplicated(self) -> "Scene": return Scene(geometry) + def reconstruct_instances(self, cost_threshold: Floating = 1e-5) -> "Scene": + return reconstruct_instances(self, cost_threshold=cost_threshold) + def set_camera( self, angles=None, distance=None, center=None, resolution=None, fov=None ) -> cameras.Camera: @@ -925,6 +929,24 @@ def dump(self, concatenate: bool = False) -> Union[Geometry, List[Geometry]]: return result + def to_mesh(self) -> "Trimesh": # noqa: F821 + """ + Concatenate all mesh instances in the scene into a single mesh + which can be manipulated as a regular mesh. + + Returns + ---------- + mesh + All meshes in the scene concatenated into one. + """ + from ..base import Trimesh + + # dump the scene into a list of meshes + dump = self.dump() + + # concatenate all meshes + return util.concatenate([d for d in dump if isinstance(d, Trimesh)]) + def subscene(self, node: str) -> "Scene": """ Get part of a scene that succeeds a specified node. @@ -1496,3 +1518,72 @@ def node_remap(node): result.geometry.update(geometry) return result + + +def reconstruct_instances(scene: Scene, cost_threshold: Floating = 1e-6) -> Scene: + """ + If a scene has been "baked" with meshes it means that + the duplicate nodes have *corresponding vertices* but are + rigidly transformed to different places. + + This means the problem of finding ab instance transform can + use the `procrustes` analysis which is *very* fast relative + to more complicated registration problems that require ICP + and nearest-point-on-surface calculations. + + TODO : construct a parent non-geometry node for containing every group. + + Parameters + ---------- + scene + The scene to handle. + cost_threshold + The maximum value for `procrustes + + Returns + --------- + dedupe + A copy of the scene de-duplicated as much as possible. + """ + # start with the original scene graph and modify in-loop + graph = scene.graph.copy() + + for group in scene.duplicate_nodes: + # not sure if this ever includes + if len(group) < 2: + continue + + # we are going to use one of the geometries and try to register the others to it + node_base = group[0] + # get the geometry name for this base node + _, geom_base = scene.graph[node_base] + # get the vertices of the base model + base: NDArray = scene.geometry[geom_base].vertices.view(np.ndarray) + + for node in group[1:]: + # the original pose of this node in the scene + node_mat, node_geom = scene.graph[node] + # procrustes matches corresponding point arrays very quickly + # but we have to make sure that they actual correspond in shape + node_vertices = scene.geometry[node_geom].vertices.view(np.ndarray) + + # procrustes only works on corresponding point clouds! + if node_vertices.shape != base.shape: + continue + + # solve for a pose moving this instance into position + matrix, _p, cost = procrustes( + base, node_vertices, translation=True, scale=False, reflection=False + ) + if cost < cost_threshold: + # add the transform we found + graph.update(node, matrix=np.dot(node_mat, matrix), geometry=geom_base) + + # get from the new graph which geometry ends up with a reference + referenced = set(graph.geometry_nodes.keys()) + + # return a scene with the de-duplicated graph and a copy of any geometry + return Scene( + geometry={k: v.copy() for k, v in scene.geometry.items() if k in referenced}, + graph=graph, + ) From 0b4d5624ca83dc7e3f8a252111b32b7481f2a7f0 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 2 Sep 2024 15:29:32 -0400 Subject: [PATCH 12/27] try using a head_commit message for the release body --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb3ca2e28..209a0deae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,4 +143,5 @@ jobs: tag_name: ${{ steps.set_tag.outputs.tag_name }} release_name: Release ${{ steps.set_tag.outputs.tag_name }} draft: false + body: ${{ github.event.head_commit.message }} prerelease: false From b503b0bcd1004e08b10918342ea69b7e70a222eb Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 2 Sep 2024 15:45:50 -0400 Subject: [PATCH 13/27] gate test on installed --- tests/test_scenegraph.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py index 5b5ee6607..cb8f236f3 100644 --- a/tests/test_scenegraph.py +++ b/tests/test_scenegraph.py @@ -161,6 +161,9 @@ def test_scene_transform(self): assert not g.np.allclose(m.convex_hull.bounds, b) def test_simplify(self): + if not g.trimesh.util.has_module("fast_simplification"): + return + # get a scene graph scene: g.trimesh.Scene = g.get_mesh("cycloidal.3DXML") From 3c1ef655d2974262974e25fbd07b81243543ffc2 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 2 Sep 2024 15:51:37 -0400 Subject: [PATCH 14/27] return geometry type is optional --- trimesh/scene/transforms.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py index d837d0cba..7fbaf8e81 100644 --- a/trimesh/scene/transforms.py +++ b/trimesh/scene/transforms.py @@ -6,7 +6,7 @@ from .. import caching, util from ..caching import hash_fast from ..transformations import fix_rigid, quaternion_matrix, rotation_matrix -from ..typed import ArrayLike, NDArray, Sequence, Tuple, Union +from ..typed import ArrayLike, NDArray, Optional, Sequence, Tuple, Union # we compare to identity a lot _identity = np.eye(4) @@ -92,7 +92,9 @@ def update(self, frame_to, frame_from=None, **kwargs): if "geometry" in kwargs: self.transforms.node_data[frame_to]["geometry"] = kwargs["geometry"] - def get(self, frame_to, frame_from=None): + def get( + self, frame_to: str, frame_from: Optional[str] = None + ) -> Tuple[NDArray[np.float64], Optional[str]]: """ Get the transform from one frame to another. @@ -108,6 +110,8 @@ def get(self, frame_to, frame_from=None): ---------- transform : (4, 4) float Homogeneous transformation matrix + geometry + The name of the geometry if it exists Raises ----------- @@ -501,10 +505,10 @@ def remove_geometries(self, geometries: Union[str, set, Sequence]): self._cache.cache.pop("nodes_geometry", None) self.transforms._hash = None - def __contains__(self, key): + def __contains__(self, key: str) -> bool: return key in self.transforms.node_data - def __getitem__(self, key: str) -> Tuple[NDArray, str]: + def __getitem__(self, key: str) -> Tuple[NDArray[np.float64], Optional[str]]: return self.get(key) def __setitem__(self, key: str, value: ArrayLike): From 1efc20531e29f8434a165e63727a565fbf559ced Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 2 Sep 2024 16:04:02 -0400 Subject: [PATCH 15/27] deprecate Scene.deduplicated --- trimesh/scene/scene.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index bb760af7b..1cbdb080f 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -1,5 +1,6 @@ import collections import uuid +import warnings import numpy as np @@ -664,6 +665,8 @@ def duplicate_nodes(self) -> List[List[str]]: def deduplicated(self) -> "Scene": """ + DEPRECATED: REMOVAL JANUARY 2025, this is one line and not that useful. + Return a new scene where each unique geometry is only included once and transforms are discarded. @@ -672,16 +675,17 @@ def deduplicated(self) -> "Scene": dedupe : Scene One copy of each unique geometry from scene """ - # collect geometry - geometry = {} - # loop through groups of identical nodes - for group in self.duplicate_nodes: - # get the name of the geometry - name = self.graph[group[0]][1] - # collect our unique collection of geometry - geometry[name] = self.geometry[name] - return Scene(geometry) + warnings.warn( + "DEPRECATED: REMOVAL JANUARY 2025, this is one line and not that useful.", + category=DeprecationWarning, + stacklevel=2, + ) + + # keying by `identifier_hash` will mean every geometry is unique + return Scene( + list({g.identifier_hash: g for g in self.geometry.values()}.values()) + ) def reconstruct_instances(self, cost_threshold: Floating = 1e-5) -> "Scene": return reconstruct_instances(self, cost_threshold=cost_threshold) From 6bff5a1787239f602633b3c9c2394340938aef51 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 2 Sep 2024 16:06:25 -0400 Subject: [PATCH 16/27] use Hashable type --- trimesh/scene/transforms.py | 14 ++++++++------ trimesh/typed.py | 5 +++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py index 7fbaf8e81..5b2ca9d81 100644 --- a/trimesh/scene/transforms.py +++ b/trimesh/scene/transforms.py @@ -6,7 +6,7 @@ from .. import caching, util from ..caching import hash_fast from ..transformations import fix_rigid, quaternion_matrix, rotation_matrix -from ..typed import ArrayLike, NDArray, Optional, Sequence, Tuple, Union +from ..typed import ArrayLike, Hashable, NDArray, Optional, Sequence, Tuple, Union # we compare to identity a lot _identity = np.eye(4) @@ -93,8 +93,8 @@ def update(self, frame_to, frame_from=None, **kwargs): self.transforms.node_data[frame_to]["geometry"] = kwargs["geometry"] def get( - self, frame_to: str, frame_from: Optional[str] = None - ) -> Tuple[NDArray[np.float64], Optional[str]]: + self, frame_to: Hashable, frame_from: Optional[Hashable] = None + ) -> Tuple[NDArray[np.float64], Optional[Hashable]]: """ Get the transform from one frame to another. @@ -505,13 +505,15 @@ def remove_geometries(self, geometries: Union[str, set, Sequence]): self._cache.cache.pop("nodes_geometry", None) self.transforms._hash = None - def __contains__(self, key: str) -> bool: + def __contains__(self, key: Hashable) -> bool: return key in self.transforms.node_data - def __getitem__(self, key: str) -> Tuple[NDArray[np.float64], Optional[str]]: + def __getitem__( + self, key: Hashable + ) -> Tuple[NDArray[np.float64], Optional[Hashable]]: return self.get(key) - def __setitem__(self, key: str, value: ArrayLike): + def __setitem__(self, key: Hashable, value: ArrayLike): value = np.asanyarray(value, dtype=np.float64) if value.shape != (4, 4): raise ValueError("Matrix must be specified!") diff --git a/trimesh/typed.py b/trimesh/typed.py index 18c408011..b27698b51 100644 --- a/trimesh/typed.py +++ b/trimesh/typed.py @@ -20,9 +20,9 @@ List = list Tuple = tuple Dict = dict - from collections.abc import Callable, Iterable, Mapping, Sequence + from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence else: - from typing import Callable, Dict, Iterable, List, Mapping, Sequence, Tuple + from typing import Callable, Dict, Hashable, Iterable, List, Mapping, Sequence, Tuple # most loader routes take `file_obj` which can either be # a file-like object or a file path, or sometimes a dict @@ -66,4 +66,5 @@ "int64", "Mapping", "Callable", + "Hashable", ] From 8b2d46e7cb5a17233075a2c32ec7f617b2efeb6e Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Tue, 3 Sep 2024 14:58:22 -0400 Subject: [PATCH 17/27] add QhullOptions dataclass helper --- trimesh/convex.py | 156 +++++++++++++++++++++++++++++++++++++++-- trimesh/scene/scene.py | 49 +++++++++++-- 2 files changed, 192 insertions(+), 13 deletions(-) diff --git a/trimesh/convex.py b/trimesh/convex.py index 90806e8d9..2740361b8 100644 --- a/trimesh/convex.py +++ b/trimesh/convex.py @@ -9,10 +9,13 @@ 3) (of a polygon) having only interior angles measuring less than 180 """ +from dataclasses import dataclass, fields + import numpy as np from . import triangles, util from .constants import tol +from .typed import Optional, Union try: from scipy.spatial import ConvexHull @@ -27,20 +30,149 @@ QhullError = BaseException -def convex_hull(obj, qhull_options="QbB Pp Qt", repair=True): +@dataclass +class QhullOptions: + """ + A helper class for constructing correct Qhull option strings, with more + # details availble at: http://www.qhull.org/html/qh-quick.htm#options + + Currently only includes the boolean flag options which is most of them. + + Parameters + ----------- + Qa + Allow input with fewer or more points than coordinates + Qc + Keep coplanar points with nearest facet + Qi + Keep interior points with nearest facet. + QJ + Joggled input to avoid precision problems + Qt + Triangulated output. + Qu + Compute upper hull for furthest-site Delaunay triangulation + Qw + Allow warnings about Qhull options + Qbb + Scale last coordinate to [0,m] for Delaunay + Qs + Search all points for the initial simplex + Qv + Test vertex neighbors for convexity + Qx + Exact pre-merges (allows coplanar facets) + Qz + Add a point-at-infinity for Delaunay triangulations + QbB + Scale input to fit the unit cube + QR0 + Random rotation (n=seed, n=0 time, n=-1 time/no rotate) + Qg + only build good facets (needs 'QGn', 'QVn', or 'Pdk') + Pp + Do not print statistics about precision problems, + and it remove some of the warnings. + It removes the narrow hull warning. + """ + + Qa: Optional[bool] = None + """ Allow input with fewer or more points than coordinates""" + + Qc: Optional[bool] = None + """ Keep coplanar points with nearest facet""" + + Qi: Optional[bool] = None + """ Keep interior points with nearest facet. """ + + QJ: Optional[bool] = None + """ Joggled input to avoid precision problems """ + + Qt: Optional[bool] = None + """ Triangulated output. """ + + Qu: Optional[bool] = None + """ Compute upper hull for furthest-site Delaunay triangulation """ + + Qw: Optional[bool] = None + """ Allow warnings about Qhull options """ + + # Precision handling + Qbb: Optional[bool] = None + """ Scale last coordinate to [0,m] for Delaunay """ + + Qs: Optional[bool] = None + """ Search all points for the initial simplex """ + + Qv: Optional[bool] = None + """ Test vertex neighbors for convexity """ + + Qx: Optional[bool] = None + """ Exact pre-merges (allows coplanar facets) """ + + Qz: Optional[bool] = None + """ Add a point-at-infinity for Delaunay triangulations """ + + QbB: Optional[bool] = None + """ Scale input to fit the unit cube """ + + QR0: Optional[bool] = None + """ Random rotation (n=seed, n=0 time, n=-1 time/no rotate) """ + + # Select facets + Qg: Optional[bool] = None + """ only build good facets (needs 'QGn', 'QVn', or 'Pdk') """ + + Pp: Optional[bool] = None + """ Do not print statistics about precision problems, + and it remove some of the warnings. + It removes the narrow hull warning. """ + + # TODO : not included non-boolean options + # QBk: Optional[Floating] = None + # """ Scale coord[k] to upper bound of n (default 0.5) """ + + # Qbk: Optional[Floating] = None + # """ Scale coord[k] to low bound of n (default -0.5) """ + + # Qbk:0Bk:0 + # """ drop dimension k from input """ + + # QGn + # good facet if visible from point n, -n for not visible + + # QVn + # good facet if it includes point n, -n if not + + def __str__(self) -> str: + """ + Construct the `qhull_options` string used by `scipy.spatial` + objects and functions. + + Returns + ---------- + qhull_options + Can be passed to `scipy.spatial.[ConvexHull,Delaunay,Voronoi]` + """ + return " ".join(f.name for f in fields(self) if getattr(self, f.name, False)) + + +def convex_hull(obj, qhull_options: Union[QhullOptions, str, None] = None, repair=True): """ Get a new Trimesh object representing the convex hull of the current mesh attempting to return a watertight mesh with correct normals. + Details on qhull options: http://www.qhull.org/html/qh-quick.htm#options + Arguments -------- obj : Trimesh, or (n,3) float Mesh or cartesian points - qhull_options : str + qhull_options Options to pass to qhull. Returns @@ -50,7 +182,19 @@ def convex_hull(obj, qhull_options="QbB Pp Qt", repair=True): """ from .base import Trimesh - if isinstance(obj, Trimesh): + if qhull_options is None: + # construct a default option set with suppressed warnings, + # triangulation, and scaling to a unit bounding box + qhull_options = QhullOptions(QbB=True, Pp=True, Qt=True) + + if isinstance(qhull_options, QhullOptions): + qhull_str = str(qhull_options) + elif isinstance(qhull_options, str): + qhull_str = qhull_options + else: + raise TypeError(type(qhull_options)) + + if hasattr(obj, "vertices"): points = obj.vertices.view(np.ndarray) else: # will remove subclassing @@ -59,7 +203,7 @@ def convex_hull(obj, qhull_options="QbB Pp Qt", repair=True): raise ValueError("Object must be Trimesh or (n,3) points!") try: - hull = ConvexHull(points, qhull_options=qhull_options) + hull = ConvexHull(points, qhull_options=qhull_str) except QhullError: util.log.debug("Failed to compute convex hull: retrying with `QJ`", exc_info=True) # try with "joggle" enabled @@ -137,8 +281,8 @@ def convex_hull(obj, qhull_options="QbB Pp Qt", repair=True): # sometimes the QbB option will cause precision issues # so try the hull again without it and # check for qhull_options is None to avoid infinite recursion - if qhull_options is not None and not convex.is_winding_consistent: - return convex_hull(convex, qhull_options=None) + if len(qhull_str) > 0 and not convex.is_winding_consistent: + return convex_hull(convex, qhull_options=QhullOptions()) return convex diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 1cbdb080f..80c451863 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -688,6 +688,32 @@ def deduplicated(self) -> "Scene": ) def reconstruct_instances(self, cost_threshold: Floating = 1e-5) -> "Scene": + """ + If a scene has been "baked" with meshes it means that + the duplicate nodes have *corresponding vertices* but are + rigidly transformed to different places. + + This means the problem of finding ab instance transform can + use the `procrustes` analysis which is *very* fast relative + to more complicated registration problems that require ICP + and nearest-point-on-surface calculations. + + TODO : construct a parent non-geometry node for containing every group. + + Parameters + ---------- + scene + The scene to handle. + cost_threshold + The maximum value for `procrustes` cost which is "squared mean + vertex distance between pair". If the fit is above this value + the instance will be left even if it is a duplicate. + + Returns + --------- + dedupe + A copy of the scene de-duplicated as much as possible. + """ return reconstruct_instances(self, cost_threshold=cost_threshold) def set_camera( @@ -885,14 +911,17 @@ def rezero(self) -> None: ) self.graph.base_frame = new_base - def dump(self, concatenate: bool = False) -> Union[Geometry, List[Geometry]]: + def dump(self, concatenate: bool = False) -> List[Geometry]: """ Append all meshes in scene freezing transforms. Parameters ------------ concatenate - If True, concatenate results into single mesh + Concatenate results into single mesh. This keyword argument will + make the type hint incorrect and you should replace + `Scene.dump(concatenate=True)` with `Scene.to_mesh()` + DEPRECATED FOR REMOVAL APRIL 2025 Returns ---------- @@ -928,15 +957,19 @@ def dump(self, concatenate: bool = False) -> Union[Geometry, List[Geometry]]: result.append(current) if concatenate: + warnings.warn( + "`Scene.dump(concatenate=True)` DEPRECATED FOR REMOVAL APRIL 2025: replace with `Scene.to_mesh()`", + category=DeprecationWarning, + stacklevel=2, + ) # if scene has mixed geometry this may drop some of it - return util.concatenate(result) + return util.concatenate(result) # type: ignore return result def to_mesh(self) -> "Trimesh": # noqa: F821 """ - Concatenate all mesh instances in the scene into a single mesh - which can be manipulated as a regular mesh. + Concatenate all mesh instances in the scene into a single mesh. Returns ---------- @@ -991,7 +1024,7 @@ def convex_hull(self): hull : trimesh.Trimesh Trimesh object which is a convex hull of all meshes in scene """ - points = util.vstack_empty([m.vertices for m in self.dump()]) + points = util.vstack_empty([m.vertices for m in self.dump()]) # type: ignore return convex.convex_hull(points) def export(self, file_obj=None, file_type=None, **kwargs): @@ -1542,7 +1575,9 @@ def reconstruct_instances(scene: Scene, cost_threshold: Floating = 1e-6) -> Scen scene The scene to handle. cost_threshold - The maximum value for `procrustes + The maximum value for `procrustes` cost which is "squared mean + vertex distance between pair". If the fit is above this value + the instance will be left even if it is a duplicate. Returns --------- From cc9ba720300f9438ffcfee823be4c9a26996d494 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Tue, 3 Sep 2024 15:15:27 -0400 Subject: [PATCH 18/27] add vertices ABC to Geometry3D --- trimesh/convex.py | 57 ++++++++++++++++++++++++----------------------- trimesh/parent.py | 11 +++++++-- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/trimesh/convex.py b/trimesh/convex.py index 2740361b8..32e18969e 100644 --- a/trimesh/convex.py +++ b/trimesh/convex.py @@ -15,7 +15,8 @@ from . import triangles, util from .constants import tol -from .typed import Optional, Union +from .parent import Geometry3D +from .typed import NDArray, Optional, Union try: from scipy.spatial import ConvexHull @@ -33,10 +34,10 @@ @dataclass class QhullOptions: """ - A helper class for constructing correct Qhull option strings, with more - # details availble at: http://www.qhull.org/html/qh-quick.htm#options + A helper class for constructing correct Qhull option strings. + More details available at: http://www.qhull.org/html/qh-quick.htm#options - Currently only includes the boolean flag options which is most of them. + Currently only includes the boolean flag options, which is most of them. Parameters ----------- @@ -71,9 +72,8 @@ class QhullOptions: Qg only build good facets (needs 'QGn', 'QVn', or 'Pdk') Pp - Do not print statistics about precision problems, - and it remove some of the warnings. - It removes the narrow hull warning. + Do not print statistics about precision problems and remove + some of the warnings including the narrow hull warning. """ Qa: Optional[bool] = None @@ -121,12 +121,11 @@ class QhullOptions: # Select facets Qg: Optional[bool] = None - """ only build good facets (needs 'QGn', 'QVn', or 'Pdk') """ + """ Only build good facets (needs 'QGn', 'QVn', or 'Pdk') """ Pp: Optional[bool] = None - """ Do not print statistics about precision problems, - and it remove some of the warnings. - It removes the narrow hull warning. """ + """ Do not print statistics about precision problems and remove + some of the warnings including the narrow hull warning. """ # TODO : not included non-boolean options # QBk: Optional[Floating] = None @@ -157,37 +156,39 @@ def __str__(self) -> str: return " ".join(f.name for f in fields(self) if getattr(self, f.name, False)) -def convex_hull(obj, qhull_options: Union[QhullOptions, str, None] = None, repair=True): +QHULL_DEFAULT = QhullOptions(QbB=True, Pp=True, Qt=True) + + +def convex_hull( + obj: Union[Geometry3D, NDArray], + qhull_options: Union[QhullOptions, str, None] = QHULL_DEFAULT, + repair: bool = True, +) -> "Trimesh": # noqa: F821 """ Get a new Trimesh object representing the convex hull of the current mesh attempting to return a watertight mesh with correct normals. - - Details on qhull options: - http://www.qhull.org/html/qh-quick.htm#options - - Arguments -------- - obj : Trimesh, or (n,3) float - Mesh or cartesian points + obj + Mesh or `(n, 3)` points. qhull_options Options to pass to qhull. Returns -------- - convex : Trimesh - Mesh of convex hull + convex + Mesh of convex hull. """ + # would be a circular import at the module level from .base import Trimesh + # compose the if qhull_options is None: - # construct a default option set with suppressed warnings, - # triangulation, and scaling to a unit bounding box - qhull_options = QhullOptions(QbB=True, Pp=True, Qt=True) - - if isinstance(qhull_options, QhullOptions): + qhull_str = None + elif isinstance(qhull_options, QhullOptions): + # use the __str__ method to compose this options string qhull_str = str(qhull_options) elif isinstance(qhull_options, str): qhull_str = qhull_options @@ -281,8 +282,8 @@ def convex_hull(obj, qhull_options: Union[QhullOptions, str, None] = None, repai # sometimes the QbB option will cause precision issues # so try the hull again without it and # check for qhull_options is None to avoid infinite recursion - if len(qhull_str) > 0 and not convex.is_winding_consistent: - return convex_hull(convex, qhull_options=QhullOptions()) + if qhull_options is None and not convex.is_winding_consistent: + return convex_hull(convex, qhull_options=None) return convex diff --git a/trimesh/parent.py b/trimesh/parent.py index 6fde5ea3e..96b62a9ce 100644 --- a/trimesh/parent.py +++ b/trimesh/parent.py @@ -13,7 +13,7 @@ from . import transformations as tf from .caching import cache_decorator from .constants import tol -from .typed import Any, ArrayLike, Dict, Optional +from .typed import Any, ArrayLike, Dict, NDArray, Optional from .util import ABC @@ -213,8 +213,15 @@ class Geometry3D(Geometry): and Scene objects. """ + @property + @abc.abstractmethod + def vertices(self) -> NDArray[np.float64]: + """ + The 3D vertices of the geometry. + """ + @caching.cache_decorator - def bounding_box(self): + def bounding_box(self) -> NDArray[np.float64]: """ An axis aligned bounding box for the current mesh. From 598206bc0cbf33e5a86c06494a525e2f65a5571b Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Tue, 3 Sep 2024 15:18:24 -0400 Subject: [PATCH 19/27] remove vertices abc --- trimesh/parent.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/trimesh/parent.py b/trimesh/parent.py index 96b62a9ce..ecb24ced7 100644 --- a/trimesh/parent.py +++ b/trimesh/parent.py @@ -213,13 +213,6 @@ class Geometry3D(Geometry): and Scene objects. """ - @property - @abc.abstractmethod - def vertices(self) -> NDArray[np.float64]: - """ - The 3D vertices of the geometry. - """ - @caching.cache_decorator def bounding_box(self) -> NDArray[np.float64]: """ From 60359f51efb9c5ef9a1d07d5b1b5121c2747d8b4 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Tue, 3 Sep 2024 15:49:06 -0400 Subject: [PATCH 20/27] add for exact behavior --- tests/test_gltf.py | 2 +- tests/test_inertia.py | 2 +- tests/test_scene.py | 12 ++++++------ tests/test_scenegraph.py | 6 +++--- trimesh/convex.py | 2 +- trimesh/exchange/export.py | 4 ++-- trimesh/parent.py | 22 +++++++++------------ trimesh/scene/scene.py | 39 ++++++++++++++++++++++++-------------- trimesh/util.py | 5 ++++- 9 files changed, 52 insertions(+), 42 deletions(-) diff --git a/tests/test_gltf.py b/tests/test_gltf.py index 10207f895..c43a5e9e1 100644 --- a/tests/test_gltf.py +++ b/tests/test_gltf.py @@ -743,7 +743,7 @@ def test_load_empty_nodes(self): def test_same_name(self): s = g.get_mesh("TestScene.gltf") # hardcode correct bounds to check against - bounds = s.dump(concatenate=True).bounds + bounds = s.to_mesh().bounds # icosahedrons have two primitives each g.log.debug(len(s.geometry), len(s.graph.nodes_geometry)) diff --git a/tests/test_inertia.py b/tests/test_inertia.py index 86797200a..e184c22de 100644 --- a/tests/test_inertia.py +++ b/tests/test_inertia.py @@ -402,7 +402,7 @@ def test_scene(self): s._cache.clear() with g.Profiler() as P: - ms = s.dump(concatenate=True) + ms = s.to_mesh() total_dump = ms.moment_inertia g.log.debug(P.output_text()) diff --git a/tests/test_scene.py b/tests/test_scene.py index dd6f56356..2faeb6c68 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -461,7 +461,7 @@ def test_exact_bounds(self): m = g.get_mesh("cycloidal.3DXML") assert isinstance(m, g.trimesh.Scene) - dump = m.dump(concatenate=True) + dump = m.to_mesh() assert isinstance(dump, g.trimesh.Trimesh) # scene bounds should exactly match mesh bounds @@ -475,7 +475,7 @@ def test_concatenate_mixed(self): ] ) - dump = scene.dump(concatenate=True) + dump = scene.to_mesh() assert isinstance(dump, g.trimesh.Trimesh) def test_append_scenes(self): @@ -493,7 +493,7 @@ def test_scene_concat(self): a = g.trimesh.Scene( [g.trimesh.primitives.Sphere(center=[5, 5, 5]), g.trimesh.primitives.Box()] ) - c = a.dump(concatenate=True) + c = a.to_mesh() assert isinstance(c, g.trimesh.Trimesh) assert g.np.allclose(c.bounds, a.bounds) @@ -502,7 +502,7 @@ def test_scene_concat(self): # scene 2D scene_2D = g.trimesh.Scene(g.get_mesh("2D/250_cycloidal.DXF").split()) - concat = scene_2D.dump(concatenate=True) + concat = scene_2D.concatentate() assert isinstance(concat, g.trimesh.path.Path2D) dump = scene_2D.dump(concatenate=False) @@ -518,7 +518,7 @@ def test_scene_concat(self): assert len(dump) >= 5 assert all(isinstance(i, g.trimesh.path.Path3D) for i in dump) - concat = scene_3D.dump(concatenate=True) + concat = scene_3D.concatentate() assert isinstance(concat, g.trimesh.path.Path3D) mixed = list(scene_2D.geometry.values()) @@ -528,7 +528,7 @@ def test_scene_concat(self): dump = scene_mixed.dump(concatenate=False) assert len(dump) == len(mixed) - concat = scene_mixed.dump(concatenate=True) + concat = scene_mixed.concatentate() assert isinstance(concat, g.trimesh.path.Path3D) diff --git a/tests/test_scenegraph.py b/tests/test_scenegraph.py index cb8f236f3..313324ffd 100644 --- a/tests/test_scenegraph.py +++ b/tests/test_scenegraph.py @@ -142,7 +142,7 @@ def test_scene_transform(self): # copy the original bounds of the scene's convex hull b = scene.convex_hull.bounds.tolist() # dump it into a single mesh - m = scene.dump(concatenate=True) + m = scene.to_mesh() # mesh bounds should match exactly assert g.np.allclose(m.bounds, b) @@ -167,10 +167,10 @@ def test_simplify(self): # get a scene graph scene: g.trimesh.Scene = g.get_mesh("cycloidal.3DXML") - original = scene.dump(concatenate=True) + original = scene.to_mesh() scene.simplify_quadric_decimation(percent=0.0, aggression=0) - assert len(scene.dump(concatenate=True).vertices) < len(original.vertices) + assert len(scene.to_mesh().vertices) < len(original.vertices) def test_reverse(self): tf = g.trimesh.transformations diff --git a/trimesh/convex.py b/trimesh/convex.py index 32e18969e..74d5f4638 100644 --- a/trimesh/convex.py +++ b/trimesh/convex.py @@ -163,7 +163,7 @@ def convex_hull( obj: Union[Geometry3D, NDArray], qhull_options: Union[QhullOptions, str, None] = QHULL_DEFAULT, repair: bool = True, -) -> "Trimesh": # noqa: F821 +) -> "trimesh.Trimesh": # noqa: F821 """ Get a new Trimesh object representing the convex hull of the current mesh attempting to return a watertight mesh with correct diff --git a/trimesh/exchange/export.py b/trimesh/exchange/export.py index 438d6be9d..c2c64a157 100644 --- a/trimesh/exchange/export.py +++ b/trimesh/exchange/export.py @@ -276,9 +276,9 @@ def export_scene(scene, file_obj, file_type=None, resolver=None, **kwargs): data = svg_io.export_svg(scene, **kwargs) elif file_type == "ply": - data = _mesh_exporters["ply"](scene.dump(concatenate=True), **kwargs) + data = _mesh_exporters["ply"](scene.to_mesh(), **kwargs) elif file_type == "stl": - data = export_stl(scene.dump(concatenate=True), **kwargs) + data = export_stl(scene.to_mesh(), **kwargs) elif file_type == "3mf": data = _mesh_exporters["3mf"](scene, **kwargs) else: diff --git a/trimesh/parent.py b/trimesh/parent.py index ecb24ced7..6b0e50bbe 100644 --- a/trimesh/parent.py +++ b/trimesh/parent.py @@ -31,12 +31,12 @@ class Geometry(ABC): @property @abc.abstractmethod - def bounds(self): + def bounds(self) -> NDArray[np.float64]: pass @property @abc.abstractmethod - def extents(self): + def extents(self) -> NDArray[np.float64]: pass @abc.abstractmethod @@ -57,7 +57,7 @@ def __hash__(self): hash : int Hash of current graph and geometry. """ - return self._data.__hash__() + return self._data.__hash__() # type: ignore @abc.abstractmethod def copy(self): @@ -103,7 +103,7 @@ def __repr__(self): elements.append(f"name=`{display}`") return "".format(type(self).__name__, ", ".join(elements)) - def apply_translation(self, translation): + def apply_translation(self, translation: ArrayLike): """ Translate the current mesh. @@ -214,7 +214,7 @@ class Geometry3D(Geometry): """ @caching.cache_decorator - def bounding_box(self) -> NDArray[np.float64]: + def bounding_box(self): """ An axis aligned bounding box for the current mesh. @@ -230,8 +230,7 @@ def bounding_box(self) -> NDArray[np.float64]: # translate to center of axis aligned bounds transform[:3, 3] = self.bounds.mean(axis=0) - aabb = primitives.Box(transform=transform, extents=self.extents, mutable=False) - return aabb + return primitives.Box(transform=transform, extents=self.extents, mutable=False) @caching.cache_decorator def bounding_box_oriented(self): @@ -271,8 +270,7 @@ def bounding_sphere(self): from . import nsphere, primitives center, radius = nsphere.minimum_nsphere(self) - minball = primitives.Sphere(center=center, radius=radius, mutable=False) - return minball + return primitives.Sphere(center=center, radius=radius, mutable=False) @caching.cache_decorator def bounding_cylinder(self): @@ -287,8 +285,7 @@ def bounding_cylinder(self): from . import bounds, primitives kwargs = bounds.minimum_cylinder(self) - mincyl = primitives.Cylinder(mutable=False, **kwargs) - return mincyl + return primitives.Cylinder(mutable=False, **kwargs) @caching.cache_decorator def bounding_primitive(self): @@ -310,8 +307,7 @@ def bounding_primitive(self): self.bounding_cylinder, ] volume_min = np.argmin([i.volume for i in options]) - bounding_primitive = options[volume_min] - return bounding_primitive + return options[volume_min] def apply_obb(self, **kwargs): """ diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 80c451863..75f1ce3f3 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -913,21 +913,23 @@ def rezero(self) -> None: def dump(self, concatenate: bool = False) -> List[Geometry]: """ - Append all meshes in scene freezing transforms. + Get a list of every geometry moved to it's instance position, + i.e. freezing or "baking" transforms. Parameters ------------ concatenate - Concatenate results into single mesh. This keyword argument will - make the type hint incorrect and you should replace - `Scene.dump(concatenate=True)` with `Scene.to_mesh()` DEPRECATED FOR REMOVAL APRIL 2025 + Concatenate results into single geometry. + This keyword argument will make the type hint incorrect and + you should replace `Scene.dump(concatenate=True)` with: + - `Scene.concatenate()` for a Trimesh, Path2D or Path3D + - `Scene.to_mesh()` for only `Trimesh`. Returns ---------- - dumped : (n,) Trimesh, Path2D, Path3D, PointCloud - Depending on what the scene contains. If `concatenate` - then some geometry may be dropped if it doesn't match. + dumped + Copies of `Scene.geometry` transformed to their instance position. """ result = [] @@ -958,7 +960,7 @@ def dump(self, concatenate: bool = False) -> List[Geometry]: if concatenate: warnings.warn( - "`Scene.dump(concatenate=True)` DEPRECATED FOR REMOVAL APRIL 2025: replace with `Scene.to_mesh()`", + "`Scene.dump(concatenate=True)` DEPRECATED FOR REMOVAL APRIL 2025: replace with `Scene.concatenate()`", category=DeprecationWarning, stacklevel=2, ) @@ -967,9 +969,9 @@ def dump(self, concatenate: bool = False) -> List[Geometry]: return result - def to_mesh(self) -> "Trimesh": # noqa: F821 + def to_mesh(self) -> "trimesh.Trimesh": # noqa: F821 """ - Concatenate all mesh instances in the scene into a single mesh. + Concatenate mesh instances in the scene into a single mesh. Returns ---------- @@ -978,11 +980,20 @@ def to_mesh(self) -> "Trimesh": # noqa: F821 """ from ..base import Trimesh - # dump the scene into a list of meshes - dump = self.dump() + # concatenate only meshes + return util.concatenate([d for d in self.dump() if isinstance(d, Trimesh)]) - # concatenate all meshes - return util.concatenate([d for d in dump if isinstance(d, Trimesh)]) + def concatentate(self) -> Geometry: + """ + Concatenate geometry in the scene into a single like-typed geometry. + + Returns + --------- + concat + Either a Trimesh, Path2D, or Path3D depending on what is in the scene. + """ + # concatenate everything and return the most-occurring type. + return util.concatenate(self.dump()) def subscene(self, node: str) -> "Scene": """ diff --git a/trimesh/util.py b/trimesh/util.py index 309a77b71..0ff69b8d8 100644 --- a/trimesh/util.py +++ b/trimesh/util.py @@ -26,6 +26,7 @@ # for type checking from collections.abc import Mapping from io import BytesIO, StringIO +from typing import Union import numpy as np @@ -1411,7 +1412,9 @@ class : Optional[Callable] raise ValueError("Unable to extract class of name " + name) -def concatenate(a, b=None): +def concatenate( + a, b=None +) -> Union["trimesh.Trimesh", "trimesh.path.Path2D", "trimesh.path.Path3D"]: # noqa: F821 """ Concatenate two or more meshes. From eec471ec22f927b2ce4dfe09f24440fcdcddd1bc Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Tue, 3 Sep 2024 16:13:29 -0400 Subject: [PATCH 21/27] tuples aren't ArrayLike --- tests/test_voxel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_voxel.py b/tests/test_voxel.py index db3893633..79a4bb524 100644 --- a/tests/test_voxel.py +++ b/tests/test_voxel.py @@ -163,7 +163,7 @@ def test_as_boxes(self): voxel = g.trimesh.voxel pitch = 0.1 - origin = (0, 0, 1) + origin = [0, 0, 1] matrix = g.np.eye(9, dtype=bool).reshape((-1, 3, 3)) centers = g.trimesh.voxel.ops.matrix_to_points( From e0e371124b20248b0e39c96e730a5ba55c0cb512 Mon Sep 17 00:00:00 2001 From: Brandon Ayers Date: Tue, 3 Sep 2024 18:12:57 -0700 Subject: [PATCH 22/27] Fix typing bug in merge_vertices function --- trimesh/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trimesh/base.py b/trimesh/base.py index 5e5ea49f8..ec375a898 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -1112,9 +1112,9 @@ def merge_vertices( self, merge_tex: Optional[bool] = None, merge_norm: Optional[bool] = None, - digits_vertex: Optional[bool] = None, - digits_norm: Optional[bool] = None, - digits_uv: Optional[bool] = None, + digits_vertex: Optional[int] = None, + digits_norm: Optional[int] = None, + digits_uv: Optional[int] = None, ) -> None: """ Removes duplicate vertices grouped by position and From c5b9a6b7618c262e340de6d1b0d8da49751b459f Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 4 Sep 2024 13:48:54 -0400 Subject: [PATCH 23/27] use typed.Integer --- trimesh/base.py | 6 +++--- trimesh/grouping.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/trimesh/base.py b/trimesh/base.py index ec375a898..388d251a4 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -1112,9 +1112,9 @@ def merge_vertices( self, merge_tex: Optional[bool] = None, merge_norm: Optional[bool] = None, - digits_vertex: Optional[int] = None, - digits_norm: Optional[int] = None, - digits_uv: Optional[int] = None, + digits_vertex: Optional[Integer] = None, + digits_norm: Optional[Integer] = None, + digits_uv: Optional[Integer] = None, ) -> None: """ Removes duplicate vertices grouped by position and diff --git a/trimesh/grouping.py b/trimesh/grouping.py index d5cccfb20..36c84896a 100644 --- a/trimesh/grouping.py +++ b/trimesh/grouping.py @@ -23,11 +23,11 @@ def merge_vertices( mesh, - merge_tex=None, - merge_norm=None, - digits_vertex=None, - digits_norm=None, - digits_uv=None, + merge_tex: Optional[bool] = None, + merge_norm: Optional[bool] = None, + digits_vertex: Optional[Integer] = None, + digits_norm: Optional[Integer] = None, + digits_uv: Optional[Integer] = None, ): """ Removes duplicate vertices, grouped by position and From 33751e96e788e2d099b5dc3668d2d825bfc78ade Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 4 Sep 2024 13:49:20 -0400 Subject: [PATCH 24/27] add early exit to Scene.scaled --- trimesh/scene/scene.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 75f1ce3f3..d8625a9eb 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -919,12 +919,12 @@ def dump(self, concatenate: bool = False) -> List[Geometry]: Parameters ------------ concatenate - DEPRECATED FOR REMOVAL APRIL 2025 + KWARG IS DEPRECATED FOR REMOVAL APRIL 2025 Concatenate results into single geometry. This keyword argument will make the type hint incorrect and you should replace `Scene.dump(concatenate=True)` with: - `Scene.concatenate()` for a Trimesh, Path2D or Path3D - - `Scene.to_mesh()` for only `Trimesh`. + - `Scene.to_mesh()` for only `Trimesh` components. Returns ---------- @@ -1147,11 +1147,8 @@ def convert_units(self, desired: str, guess: bool = False) -> "Scene": # find the float conversion scale = units.unit_conversion(current=current, desired=desired) - # exit early if our current units are the same as desired units - if np.isclose(scale, 1.0): - result = self.copy() - else: - result = self.scaled(scale=scale) + # apply scaling factor or exit early if scale ~= 1.0 + result = self.scaled(scale=scale) # apply the units to every geometry of the scaled result result.units = desired @@ -1212,6 +1209,12 @@ def scaled(self, scale: Union[Floating, ArrayLike]) -> "Scene": scaled : trimesh.Scene A copy of the current scene but scaled """ + result = self.copy() + + # a scale of 1.0 is a no-op + if np.allclose(scale, 1.0): + return result + # convert 2D geometries to 3D for 3D scaling factors scale_is_3D = isinstance(scale, (list, tuple, np.ndarray)) and len(scale) == 3 @@ -1223,7 +1226,6 @@ def scaled(self, scale: Union[Floating, ArrayLike]) -> "Scene": scale = float(scale) # result is a copy - result = self.copy() if scale_is_3D: # Copy all geometries that appear multiple times in the scene, From ea5712a5b10335bfd9a9d924a98b49ae77a5b116 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 4 Sep 2024 14:00:42 -0400 Subject: [PATCH 25/27] no reason for None-able flag options --- trimesh/convex.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/trimesh/convex.py b/trimesh/convex.py index 74d5f4638..e535aaf5d 100644 --- a/trimesh/convex.py +++ b/trimesh/convex.py @@ -16,7 +16,7 @@ from . import triangles, util from .constants import tol from .parent import Geometry3D -from .typed import NDArray, Optional, Union +from .typed import NDArray, Union try: from scipy.spatial import ConvexHull @@ -76,54 +76,54 @@ class QhullOptions: some of the warnings including the narrow hull warning. """ - Qa: Optional[bool] = None + Qa: bool = False """ Allow input with fewer or more points than coordinates""" - Qc: Optional[bool] = None + Qc: bool = False """ Keep coplanar points with nearest facet""" - Qi: Optional[bool] = None + Qi: bool = False """ Keep interior points with nearest facet. """ - QJ: Optional[bool] = None + QJ: bool = False """ Joggled input to avoid precision problems """ - Qt: Optional[bool] = None + Qt: bool = False """ Triangulated output. """ - Qu: Optional[bool] = None + Qu: bool = False """ Compute upper hull for furthest-site Delaunay triangulation """ - Qw: Optional[bool] = None + Qw: bool = False """ Allow warnings about Qhull options """ # Precision handling - Qbb: Optional[bool] = None + Qbb: bool = False """ Scale last coordinate to [0,m] for Delaunay """ - Qs: Optional[bool] = None + Qs: bool = False """ Search all points for the initial simplex """ - Qv: Optional[bool] = None + Qv: bool = False """ Test vertex neighbors for convexity """ - Qx: Optional[bool] = None + Qx: bool = False """ Exact pre-merges (allows coplanar facets) """ - Qz: Optional[bool] = None + Qz: bool = False """ Add a point-at-infinity for Delaunay triangulations """ - QbB: Optional[bool] = None + QbB: bool = False """ Scale input to fit the unit cube """ - QR0: Optional[bool] = None + QR0: bool = False """ Random rotation (n=seed, n=0 time, n=-1 time/no rotate) """ # Select facets - Qg: Optional[bool] = None + Qg: bool = False """ Only build good facets (needs 'QGn', 'QVn', or 'Pdk') """ - Pp: Optional[bool] = None + Pp: bool = False """ Do not print statistics about precision problems and remove some of the warnings including the narrow hull warning. """ @@ -153,7 +153,7 @@ def __str__(self) -> str: qhull_options Can be passed to `scipy.spatial.[ConvexHull,Delaunay,Voronoi]` """ - return " ".join(f.name for f in fields(self) if getattr(self, f.name, False)) + return " ".join(f.name for f in fields(self) if getattr(self, f.name)) QHULL_DEFAULT = QhullOptions(QbB=True, Pp=True, Qt=True) From 97e279d2ded42c3b94f689e1edfac8b04a020aea Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 4 Sep 2024 14:21:17 -0400 Subject: [PATCH 26/27] make max_len None rather than inf --- trimesh/grouping.py | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/trimesh/grouping.py b/trimesh/grouping.py index 36c84896a..07e30cb73 100644 --- a/trimesh/grouping.py +++ b/trimesh/grouping.py @@ -110,7 +110,7 @@ def merge_vertices( mesh.update_vertices(mask=mask, inverse=inverse) -def group(values, min_len=0, max_len=np.inf): +def group(values, min_len: Optional[Integer] = None, max_len: Optional[Integer] = None): """ Return the indices of values that are identical @@ -149,10 +149,20 @@ def group(values, min_len=0, max_len=np.inf): nondupe = values[1:] != values[:-1] dupe_idx = np.append(0, np.nonzero(nondupe)[0] + 1) + + # start with a mask that marks everything as ok + dupe_ok = np.ones(len(dupe_idx), dtype=bool) + + # calculate the length of each group from their index dupe_len = np.diff(np.concatenate((dupe_idx, [len(values)]))) - dupe_ok = np.logical_and( - np.greater_equal(dupe_len, min_len), np.less_equal(dupe_len, max_len) - ) + + # cull by length if requested + if min_len is not None or max_len is not None: + if min_len is not None: + dupe_ok &= dupe_len >= min_len + if max_len is not None: + dupe_ok &= dupe_len <= max_len + groups = [order[i : (i + j)] for i, j in zip(dupe_idx[dupe_ok], dupe_len[dupe_ok])] return groups @@ -264,7 +274,9 @@ def float_to_int(data, digits: Optional[Integer] = None) -> NDArray[np.int64]: return np.round((data * 10**digits) - 1e-6).astype(np.int64) -def unique_ordered(data, return_index=False, return_inverse=False): +def unique_ordered( + data: ArrayLike, return_index: bool = False, return_inverse: bool = False +): """ Returns the same as np.unique, but ordered as per the first occurrence of the unique value in data. @@ -306,7 +318,12 @@ def unique_ordered(data, return_index=False, return_inverse=False): return result -def unique_bincount(values, minlength=0, return_inverse=False, return_counts=False): +def unique_bincount( + values: ArrayLike, + minlength: Integer = 0, + return_inverse: bool = False, + return_counts: bool = False, +): """ For arrays of integers find unique values using bin counting. Roughly 10x faster for correct input than np.unique @@ -372,7 +389,7 @@ def unique_bincount(values, minlength=0, return_inverse=False, return_counts=Fal return ret -def merge_runs(data, digits=None): +def merge_runs(data: ArrayLike, digits: Optional[Integer] = None): """ Merge duplicate sequential values. This differs from unique_ordered in that values can occur in multiple places in the sequence, but @@ -397,15 +414,25 @@ def merge_runs(data, digits=None): In [2]: trimesh.grouping.merge_runs(a) Out[2]: array([-1, 0, 1, 2, 0, 3, 4, 5, 6, 7, 8, 9]) """ + if digits is None: + epsilon = tol.merge + else: + epsilon = 10 ** (-digits) + data = np.asanyarray(data) mask = np.zeros(len(data), dtype=bool) mask[0] = True - mask[1:] = np.abs(data[1:] - data[:-1]) > tol.merge + mask[1:] = np.abs(data[1:] - data[:-1]) > epsilon return data[mask] -def unique_float(data, return_index=False, return_inverse=False, digits=None): +def unique_float( + data, + return_index: bool = False, + return_inverse: bool = False, + digits: Optional[Integer] = None, +): """ Identical to the numpy.unique command, except evaluates floating point numbers, using a specified number of digits. From 21a92eaa2357b56596c0a5f0b73348f40225bb3c Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 4 Sep 2024 14:59:48 -0400 Subject: [PATCH 27/27] rename concatenate to to_geometry for consistency --- tests/test_scene.py | 6 +++--- trimesh/scene/scene.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_scene.py b/tests/test_scene.py index 2faeb6c68..e9e783433 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -502,7 +502,7 @@ def test_scene_concat(self): # scene 2D scene_2D = g.trimesh.Scene(g.get_mesh("2D/250_cycloidal.DXF").split()) - concat = scene_2D.concatentate() + concat = scene_2D.to_geometry() assert isinstance(concat, g.trimesh.path.Path2D) dump = scene_2D.dump(concatenate=False) @@ -518,7 +518,7 @@ def test_scene_concat(self): assert len(dump) >= 5 assert all(isinstance(i, g.trimesh.path.Path3D) for i in dump) - concat = scene_3D.concatentate() + concat = scene_3D.to_geometry() assert isinstance(concat, g.trimesh.path.Path3D) mixed = list(scene_2D.geometry.values()) @@ -528,7 +528,7 @@ def test_scene_concat(self): dump = scene_mixed.dump(concatenate=False) assert len(dump) == len(mixed) - concat = scene_mixed.concatentate() + concat = scene_mixed.to_geometry() assert isinstance(concat, g.trimesh.path.Path3D) diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index d8625a9eb..a777989c9 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -923,7 +923,7 @@ def dump(self, concatenate: bool = False) -> List[Geometry]: Concatenate results into single geometry. This keyword argument will make the type hint incorrect and you should replace `Scene.dump(concatenate=True)` with: - - `Scene.concatenate()` for a Trimesh, Path2D or Path3D + - `Scene.to_geometry()` for a Trimesh, Path2D or Path3D - `Scene.to_mesh()` for only `Trimesh` components. Returns @@ -960,7 +960,7 @@ def dump(self, concatenate: bool = False) -> List[Geometry]: if concatenate: warnings.warn( - "`Scene.dump(concatenate=True)` DEPRECATED FOR REMOVAL APRIL 2025: replace with `Scene.concatenate()`", + "`Scene.dump(concatenate=True)` DEPRECATED FOR REMOVAL APRIL 2025: replace with `Scene.to_geometry()`", category=DeprecationWarning, stacklevel=2, ) @@ -971,7 +971,9 @@ def dump(self, concatenate: bool = False) -> List[Geometry]: def to_mesh(self) -> "trimesh.Trimesh": # noqa: F821 """ - Concatenate mesh instances in the scene into a single mesh. + Concatenate every mesh instances in the scene into a single mesh, + applying transforms and "baking" the result. Will drop any geometry + in the scene that is not a `Trimesh` object. Returns ---------- @@ -983,9 +985,11 @@ def to_mesh(self) -> "trimesh.Trimesh": # noqa: F821 # concatenate only meshes return util.concatenate([d for d in self.dump() if isinstance(d, Trimesh)]) - def concatentate(self) -> Geometry: + def to_geometry(self) -> Geometry: """ - Concatenate geometry in the scene into a single like-typed geometry. + Concatenate geometry in the scene into a single like-typed geometry, + applying the transforms and "baking" the result. May drop geometry + if the scene has mixed geometry. Returns ---------