From 2b4ae63f499acb85fef13b41fd1ac9077d24bf41 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 22 Jan 2020 12:08:08 -0500 Subject: [PATCH] add deduplicated method to scenes --- tests/test_scene.py | 20 +++++++++++++++++++- trimesh/base.py | 31 ++++++++++++++++--------------- trimesh/comparison.py | 33 ++++++++++++++++++++++----------- trimesh/exchange/gltf.py | 6 ++++++ trimesh/scene/scene.py | 35 ++++++++++++++++++++++++++++------- trimesh/viewer/windowed.py | 6 +++++- 6 files changed, 96 insertions(+), 35 deletions(-) diff --git a/tests/test_scene.py b/tests/test_scene.py index 031fe807f..9183ab0eb 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -204,9 +204,27 @@ def test_dupe(self): assert len(u.duplicate_nodes) == 1 assert len(u.duplicate_nodes[0]) == 1 + def test_dedupe(self): + # create a scene with two identical meshes + a = g.trimesh.creation.box() + b = g.trimesh.creation.box().apply_translation([2, 2, 2]) + s = g.trimesh.Scene([a, b]) + + # should have 2 geometries + assert len(s.geometry) == 2 + assert len(s.graph.nodes_geometry) == 2 + + # get a de-duplicated scene + d = s.deduplicated() + # should not have mutated original + assert len(s.geometry) == 2 + assert len(s.graph.nodes_geometry) == 2 + # should only have one geometry + assert len(d.geometry) == 1 + assert len(d.graph.nodes_geometry) == 1 + def test_3DXML(self): s = g.get_mesh('rod.3DXML') - assert len(s.geometry) == 3 assert len(s.graph.nodes_geometry) == 29 diff --git a/trimesh/base.py b/trimesh/base.py index 4771340ae..42902b69e 100644 --- a/trimesh/base.py +++ b/trimesh/base.py @@ -6,10 +6,6 @@ Library for importing, exporting and doing simple operations on triangular meshes. """ -import numpy as np - -import copy - from . import ray from . import util from . import units @@ -45,6 +41,9 @@ from .scene import Scene from .parent import Geometry +import copy +import numpy as np + class Trimesh(Geometry): @@ -542,7 +541,7 @@ def extents(self): if self.bounds is None: return None extents = self.bounds.ptp(axis=0) - extents.flags.writeable = False + return extents @caching.cache_decorator @@ -566,8 +565,8 @@ def scale(self): @caching.cache_decorator def centroid(self): """ - The point in space which is the average of the triangle centroids - weighted by the area of each triangle. + The point in space which is the average of the triangle + centroids weighted by the area of each triangle. This will be valid even for non-watertight meshes, unlike self.center_mass @@ -580,10 +579,13 @@ def centroid(self): # use the centroid of each triangle weighted by # the area of the triangle to find the overall centroid - centroid = np.average(self.triangles_center, - axis=0, - weights=self.area_faces) - centroid.flags.writeable = False + try: + centroid = np.average(self.triangles_center, + weights=self.area_faces, + axis=0) + except BaseException: + # if all triangles are zero-area weights will not work + centroid = self.triangles_center.mean(axis=0) return centroid @property @@ -591,8 +593,8 @@ def center_mass(self): """ The point in space which is the center of mass/volume. - If the current mesh is not watertight, this is meaningless garbage - unless it was explicitly set. + If the current mesh is not watertight this is meaningless + garbage unless it was explicitly set. Returns ----------- @@ -793,8 +795,7 @@ def triangles(self): # trigger a change flag which means the MD5 will have to be # recomputed. We can escape this check by viewing the array. triangles = self.vertices.view(np.ndarray)[self.faces] - # make triangles (which are derived from faces/vertices) not writeable - triangles.flags.writeable = False + return triangles @caching.cache_decorator diff --git a/trimesh/comparison.py b/trimesh/comparison.py index c6041becf..9bbb598bc 100644 --- a/trimesh/comparison.py +++ b/trimesh/comparison.py @@ -52,7 +52,9 @@ def identifier_simple(mesh): # note that we're going to try to make all parameters relative # to area so other values don't get blown up at weird scales identifier[0] = mesh_area - + # avoid divide-by-zero later + if mesh_area < tol.merge: + mesh_area = 1.0 # topological constant and the only thing we can really # trust in this fallen world identifier[1] = mesh.euler_number @@ -75,30 +77,39 @@ def identifier_simple(mesh): if mesh.symmetry == 'radial': # cylinder height h = np.dot(vertices, mesh.symmetry_axis).ptp() - # section radius - R2 = (np.dot(vertices, mesh.symmetry_section.T) - ** 2).sum(axis=1).max() + # section radius summed per row then overall max + R2 = np.dot((np.dot(vertices, mesh.symmetry_section.T) + ** 2), [1, 1]).max() # area of a cylinder primitive area = (2 * np.pi * (R2**.5) * h) + (2 * np.pi * R2) # replace area in this case with area ratio identifier[0] = mesh_area / area elif mesh.symmetry == 'spherical': # handle a spherically symmetric mesh - R2 = (vertices ** 2).sum(axis=1).max() + R2 = np.dot((vertices ** 2), [1, 1, 1]).max() area = 4 * np.pi * R2 identifier[0] = mesh_area / area else: # if we don't have a watertight mesh add information about the - # convex hull, which is slow to compute and unreliable + # convex hull which is slow to compute and unreliable + try: + # get the hull area and volume + hull = mesh.convex_hull + hull_area = hull.area + hull_volume = hull.volume + except BaseException: + # in-plane or single point geometry has no hull + hull_area = 6.0 + hull_volume = 1.0 # just what we're looking for in a hash but hey - identifier[3] = mesh_area / mesh.convex_hull.area + identifier[3] = mesh_area / hull_area # cube side length ratio for the hull - identifier[4] = (((mesh.convex_hull.area / 6.0) ** (1.0 / 2.0)) / - (mesh.convex_hull.volume ** (1.0 / 3.0))) + identifier[4] = (((hull_area / 6.0) ** (1.0 / 2.0)) / + (hull_volume ** (1.0 / 3.0))) + # calculate maximum mesh radius vertices = mesh.vertices - mesh.centroid - # add in max radius^2 to area ratio - R2 = (vertices ** 2).sum(axis=1).max() + R2 = np.dot((vertices ** 2), [1, 1, 1]).max() identifier[5] = R2 / mesh_area return identifier diff --git a/trimesh/exchange/gltf.py b/trimesh/exchange/gltf.py index dea7601e5..88a397d9b 100644 --- a/trimesh/exchange/gltf.py +++ b/trimesh/exchange/gltf.py @@ -491,6 +491,11 @@ def _append_mesh(mesh, mat_hashes : dict Which materials have already been added """ + # return early from empty meshes to avoid crashing later + if len(mesh.faces) == 0: + log.warning('skipping empty mesh!') + return + # meshes reference accessor indexes # mode 4 is GL_TRIANGLES tree["meshes"].append({ @@ -857,6 +862,7 @@ def _read_buffers(header, buffers, mesh_kwargs, resolver=None): # if we don't have a triangular mesh continue # if not specified assume it is a mesh if "mode" in p and p["mode"] != 4: + log.warning('skipping primitive with mode {}!'.format(p['mode'])) continue # store those units diff --git a/trimesh/scene/scene.py b/trimesh/scene/scene.py index 0464f167b..01c6d3ad6 100644 --- a/trimesh/scene/scene.py +++ b/trimesh/scene/scene.py @@ -1,3 +1,4 @@ +import os import numpy as np import collections @@ -439,22 +440,41 @@ def duplicate_nodes(self): # geometry name : md5 of mesh mesh_hash = {k: int(m.identifier_md5, 16) for k, m in self.geometry.items()} - # the name of nodes in the scene graph with geometry node_names = np.array(self.graph.nodes_geometry) # the geometry names for each node in the same order node_geom = np.array([self.graph[i][1] for i in node_names]) - # the mesh md5 for each node in the same order node_hash = np.array([mesh_hash[v] for v in node_geom]) - # indexes of identical hashes node_groups = grouping.group(node_hash) - - # sequence of node names, where each sublist has identical geometry - duplicates = [np.sort(node_names[g]).tolist() for g in node_groups] + # sequence of node names where each + # sublist has identical geometry + duplicates = [np.sort(node_names[g]).tolist() + for g in node_groups] return duplicates + def deduplicated(self): + """ + Return a new scene where each unique geometry is only + included once and transforms are discarded. + + Returns + ------------- + 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) + def set_camera(self, angles=None, distance=None, @@ -735,7 +755,8 @@ def export(self, file_obj=None, file_type=None, **kwargs): file_obj.write(data) elif util.is_string(file_obj): # assume strings are file paths - with open(file_obj, 'wb') as f: + file_path = os.path.expanduser(os.path.abspath(file_obj)) + with open(file_path, 'wb') as f: f.write(data) else: # no writeable file object so return data diff --git a/trimesh/viewer/windowed.py b/trimesh/viewer/windowed.py index 9fca737a0..f3eda0642 100644 --- a/trimesh/viewer/windowed.py +++ b/trimesh/viewer/windowed.py @@ -482,13 +482,17 @@ def update_flags(self): from ..path.creation import grid bounds = self.scene.bounds center = bounds.mean(axis=0) - center[2] = bounds[0][2] + # set the grid to the lowest Z position + # also offset by the scale to avoid interference + center[2] = bounds[0][2] - (bounds[:, 2].ptp() / 100) + # choose the side length by maximum XY length side = bounds.ptp(axis=0)[:2].max() # create an axis marker sized relative to the scene grid_mesh = grid( side=side, count=4, transform=translation_matrix(center)) + # convert the path to vertexlist args args = rendering.convert_to_vertexlist(grid_mesh) # create ordered args for a vertex list self._grid = self.batch.add_indexed(*args)