From 4a68a86f6ffb9358c3d38449b6aac7d4582e1144 Mon Sep 17 00:00:00 2001 From: Kentaro Wada Date: Wed, 11 Dec 2019 14:23:05 +0000 Subject: [PATCH 01/10] Add SceneWidget.clear() to clear the opengl materials --- trimesh/viewer/widget.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/trimesh/viewer/widget.py b/trimesh/viewer/widget.py index 33037d697..ba7de7092 100644 --- a/trimesh/viewer/widget.py +++ b/trimesh/viewer/widget.py @@ -159,6 +159,15 @@ def scene_group(self): ) return self._scene_group + def clear(self): + self._scene_group = None + self.mesh_group = {} + while self.vertex_list: + _, vertex = self.vertex_list.popitem() + vertex.delete() + self.vertex_list_hash = {} + self.textures = {} + def reset_view(self): self.view = { 'ball': Trackball( From 01393adfe15703745906682463eeda86d0afb8d0 Mon Sep 17 00:00:00 2001 From: Kentaro Wada Date: Wed, 11 Dec 2019 14:30:41 +0000 Subject: [PATCH 02/10] Add Path.colors to change color like PointCloud --- trimesh/path/path.py | 12 +++++++++++- trimesh/rendering.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/trimesh/path/path.py b/trimesh/path/path.py index 8e41c6df7..770243b20 100644 --- a/trimesh/path/path.py +++ b/trimesh/path/path.py @@ -68,7 +68,8 @@ def __init__(self, entities=None, vertices=None, metadata=None, - process=True): + process=True, + colors=None): """ Instantiate a path object. @@ -86,6 +87,7 @@ def __init__(self, self.entities = entities self.vertices = vertices + self.colors = colors self.metadata = dict() if metadata.__class__.__name__ == 'dict': @@ -117,6 +119,14 @@ def process(self): func() return self + @property + def colors(self): + return self._colors + + @colors.setter + def colors(self, values): + self._colors = caching.tracked_array(values) + @property def vertices(self): return self._vertices diff --git a/trimesh/rendering.py b/trimesh/rendering.py index 89df649f5..75d407b6d 100644 --- a/trimesh/rendering.py +++ b/trimesh/rendering.py @@ -52,7 +52,7 @@ def convert_to_vertexlist(geometry, **kwargs): elif util.is_instance_named(geometry, 'Path'): # works for Path3D and Path2D # both of which inherit from Path - return path_to_vertexlist(geometry, **kwargs) + return path_to_vertexlist(geometry, colors=geometry.colors, **kwargs) elif util.is_instance_named(geometry, 'PointCloud'): # pointcloud objects contain colors return points_to_vertexlist(geometry.vertices, From 6be22fa55b604667735090b40987fa7a67cacf10 Mon Sep 17 00:00:00 2001 From: Kentaro Wada Date: Wed, 11 Dec 2019 15:14:20 +0000 Subject: [PATCH 03/10] Allow to share camera_transform numpy array in different scenes --- trimesh/viewer/widget.py | 4 ++-- trimesh/viewer/windowed.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/trimesh/viewer/widget.py b/trimesh/viewer/widget.py index 33037d697..8347caecb 100644 --- a/trimesh/viewer/widget.py +++ b/trimesh/viewer/widget.py @@ -138,7 +138,7 @@ def __init__(self, scene, **kwargs): self.vertex_list_hash = {} self.textures = {} - self._initial_camera_transform = self.scene.camera_transform + self._initial_camera_transform = self.scene.camera_transform.copy() self.reset_view() self._background = kwargs.pop('background', None) @@ -166,7 +166,7 @@ def reset_view(self): size=self.scene.camera.resolution, scale=self.scene.scale, target=self.scene.centroid)} - self.scene.camera_transform = self.view['ball'].pose + self.scene.camera_transform[...] = self.view['ball'].pose def do_claim(self): return 0, 0 diff --git a/trimesh/viewer/windowed.py b/trimesh/viewer/windowed.py index 2b11484aa..4b54dbb32 100644 --- a/trimesh/viewer/windowed.py +++ b/trimesh/viewer/windowed.py @@ -527,7 +527,7 @@ def on_resize(self, width, height): width, height = self._update_perspective(width, height) self.scene.camera.resolution = (width, height) self.view['ball'].resize(self.scene.camera.resolution) - self.scene.camera_transform = self.view['ball'].pose + self.scene.camera_transform[...] = self.view['ball'].pose def on_mouse_press(self, x, y, buttons, modifiers): """ @@ -549,21 +549,21 @@ def on_mouse_press(self, x, y, buttons, modifiers): self.view['ball'].set_state(Trackball.STATE_ZOOM) self.view['ball'].down(np.array([x, y])) - self.scene.camera_transform = self.view['ball'].pose + self.scene.camera_transform[...] = self.view['ball'].pose def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): """ Pan or rotate the view. """ self.view['ball'].drag(np.array([x, y])) - self.scene.camera_transform = self.view['ball'].pose + self.scene.camera_transform[...] = self.view['ball'].pose def on_mouse_scroll(self, x, y, dx, dy): """ Zoom the view. """ self.view['ball'].scroll(dy) - self.scene.camera_transform = self.view['ball'].pose + self.scene.camera_transform[...] = self.view['ball'].pose def on_key_press(self, symbol, modifiers): """ @@ -601,7 +601,7 @@ def on_key_press(self, symbol, modifiers): self.view['ball'].drag([0, -magnitude]) elif symbol == pyglet.window.key.UP: self.view['ball'].drag([0, magnitude]) - self.scene.camera_transform = self.view['ball'].pose + self.scene.camera_transform[...] = self.view['ball'].pose def on_draw(self): """ From 67b79da9a64343accf944a2ed1bd15300e25c8ea Mon Sep 17 00:00:00 2001 From: Kentaro Wada Date: Wed, 11 Dec 2019 18:04:34 +0000 Subject: [PATCH 04/10] Support (3,) float in apply_scale --- trimesh/parent.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/trimesh/parent.py b/trimesh/parent.py index c73880519..86a75d3f6 100644 --- a/trimesh/parent.py +++ b/trimesh/parent.py @@ -9,6 +9,7 @@ import numpy as np from . import caching +from . import transformations from .util import ABC @@ -83,19 +84,14 @@ def apply_translation(self, translation): def apply_scale(self, scaling): """ - Scale the mesh equally on all axis. + Scale the mesh. Parameters ---------- - scaling : float + scaling : float or (3,) float Scale factor to apply to the mesh """ - scaling = float(scaling) - if not np.isfinite(scaling): - raise ValueError('Scaling factor must be finite number!') - - matrix = np.eye(4) - matrix[:3, :3] *= scaling + matrix = transformations.scale_and_translate(scale=scaling) # apply_transform will work nicely even on negative scales return self.apply_transform(matrix) From fa53f391660c42f1249f24d41d91db7811f32665 Mon Sep 17 00:00:00 2001 From: Kentaro Wada Date: Wed, 11 Dec 2019 19:29:35 +0000 Subject: [PATCH 05/10] Use apply_scale in voxel.ops.multibox to avoid warnings --- trimesh/voxel/ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trimesh/voxel/ops.py b/trimesh/voxel/ops.py index 2cdc70054..aafd25878 100644 --- a/trimesh/voxel/ops.py +++ b/trimesh/voxel/ops.py @@ -255,7 +255,7 @@ def multibox(centers, pitch=1.0, colors=None): # get a basic box b = primitives.Box() # apply the pitch - b.vertices *= float(pitch) + b.apply_scale(float(pitch)) # tile into one box vertex per center v = np.tile( centers, From cef9193dbd40d0d369733f940470b7df19341f3b Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 11 Dec 2019 14:38:17 -0500 Subject: [PATCH 06/10] add line segment resampling --- tests/test_segments.py | 15 ++++++ trimesh/path/segments.py | 97 +++++++++++++++++++++++++++++++++++++ trimesh/scene/transforms.py | 2 +- 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/tests/test_segments.py b/tests/test_segments.py index e0a05e109..a7d081fae 100644 --- a/tests/test_segments.py +++ b/tests/test_segments.py @@ -60,6 +60,21 @@ def test_extrude(self): # compare area of mesh with source path assert g.np.isclose(mesh.area, path.length * height) + def test_resample(self): + from trimesh.path.segments import length, resample + # create some random segments + seg = g.random((1000, 2, 3)) + # set a maximum segment length + maxlen = 0.1 + # one of the original segments should be longer than maxlen + assert (length(seg, summed=False) > maxlen).any() + # resample to be all shorter than maxlen + res = resample(seg, maxlen=maxlen) + # check lengths of the resampled result + assert (length(res, summed=False) < maxlen).all() + # make sure overall length hasn't changed + assert g.np.isclose(length(res), length(seg)) + if __name__ == '__main__': g.trimesh.util.attach_to_log() diff --git a/trimesh/path/segments.py b/trimesh/path/segments.py index 64154f8ab..36baca499 100644 --- a/trimesh/path/segments.py +++ b/trimesh/path/segments.py @@ -363,3 +363,100 @@ def extrude(segments, height, double_sided=False): faces, np.fliplr(faces))) return vertices, faces + + +def length(segments, summed=True): + """ + Extrude 2D line segments into 3D triangles. + + Parameters + ------------- + segments : (n, 2, 2) float + 2D line segments + height : float + Distance to extrude along Z + double_sided : bool + If true, return 4 triangles per segment + + Returns + ------------- + vertices : (n, 3) float + Vertices in space + faces : (n, 3) int + Indices of vertices forming triangles + """ + segments = np.asanyarray(segments) + norms = util.row_norm(segments[:, 0, :] - segments[:, 1, :]) + if summed: + return norms.sum() + return norms + + +def resample(segments, maxlen): + """ + Resample line segments until no segment + is longer than maxlen. + + Parameters + ------------- + segments : (n, 2, 2) float + 2D line segments + maxlen : float + The maximum length of a line segment + + Returns + ------------- + resampled : (m, 2, 3) float + Line segments where no segment is longer than maxlen + """ + maxlen = float(maxlen) + segments = np.asanyarray(segments, dtype=np.float64) + + # shortcut for endpoints + pt1 = segments[:, 0] + pt2 = segments[:, 1] + # vector between endpoints + vec = pt2 - pt1 + # the integer number of times a segment needs to be split + splits = np.ceil(util.row_norm(vec) / maxlen).astype(np.int64) + + # save resulting segments + result = [] + + # loop through each count of unique splits needed + for split in np.unique(splits): + # get a mask of which segments need to be split + mask = splits == split + # the vector for each incremental length + increment = vec[mask] / split + # stack the increment vector into the shape needed + v = np.tile(increment, split + 1).reshape((-1, 3)) + # apply integer multiples of the increment + v *= np.tile(np.arange(split + 1), + len(increment)).reshape((-1, 1)) + # stack the origin points correctly + o = np.tile(pt1[mask], split + 1).reshape((-1, 3)) + # now get each segment as an (split, 3) polyline + poly = (o + v).reshape((-1, split + 1, 3)) + # get indexes to stack polyline into segments + stack = util.stack_lines(np.arange(split + 1)) + # save the resulting segments + result.extend([p[stack] for p in poly]) + + if tol.strict: + # check to make sure every start and end point + # from the reconstructed result corresponds + for original, recon in zip(segments[mask], poly): + assert np.allclose(original[0], recon[0]) + assert np.allclose(original[-1], recon[-1]) + + # stack into (n, 2, 3) segments + result = np.concatenate(result) + + if tol.strict: + # make sure resampled segments have the same length as input + assert np.isclose(length(segments), + length(result), + atol=1e-3) + + return result diff --git a/trimesh/scene/transforms.py b/trimesh/scene/transforms.py index 331418348..edccf6ce7 100644 --- a/trimesh/scene/transforms.py +++ b/trimesh/scene/transforms.py @@ -147,7 +147,7 @@ def to_gltf(self, scene): # start with base frame as first node index result = [{'name': self.base_frame}] # {node name : node index in gltf} - lookup = {0: self.base_frame} + lookup = {self.base_frame: 0} # collect the nodes in order for node in node_data.keys(): From a2fbca50337e17a47cafded69b9b3be1225876a0 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 11 Dec 2019 15:26:08 -0500 Subject: [PATCH 07/10] move path colors to entities --- tests/generic.py | 1 + tests/test_paths.py | 14 +++++++++++++ trimesh/path/entities.py | 2 ++ trimesh/path/path.py | 45 ++++++++++++++++++++++++++++++++++++---- trimesh/rendering.py | 26 ++++++++++++++++------- trimesh/util.py | 4 +++- 6 files changed, 79 insertions(+), 13 deletions(-) diff --git a/tests/generic.py b/tests/generic.py index 091e0f2c3..a6e29ea07 100644 --- a/tests/generic.py +++ b/tests/generic.py @@ -300,6 +300,7 @@ def check_path2D(path): assert all(path.polygons_closed[i] is not None for i in path.enclosure_directed.nodes()) + assert path.colors.shape == (len(path.entities), 4) def scene_equal(a, b): """ diff --git a/tests/test_paths.py b/tests/test_paths.py index 678661a1d..afd30013b 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -166,6 +166,8 @@ def test_text(self): raise ValueError( 'type bases:', [i.__name__ for i in g.trimesh.util.type_bases(t)]) + # make sure this doesn't crash with text entities + g.trimesh.rendering.convert_to_vertexlist(p) def test_empty(self): # make sure empty paths perform as expected @@ -261,6 +263,18 @@ def test_sample(self): s = path.sample(count=count) assert s.shape[1] == 2 + def test_color(self): + p = g.get_mesh('2D/wrench.dxf') + # make sure we have entities + assert len(p.entities) > 0 + # make sure shape of colors is correct + assert p.colors.shape == (len(p.entities), 4) + color = [255, 0, 0, 255] + # assign a color to the entity + p.entities[0].color = color + # make sure this is reflected in the path color + assert g.np.allclose(p.colors[0], color) + class SplitTest(g.unittest.TestCase): diff --git a/trimesh/path/entities.py b/trimesh/path/entities.py index 06666f6eb..07e5dc46b 100644 --- a/trimesh/path/entities.py +++ b/trimesh/path/entities.py @@ -31,6 +31,8 @@ def __init__(self, self.closed = closed # save the passed layer self.layer = layer + # save the passed color + self.color = color # save any other kwargs for general use self.kwargs = kwargs diff --git a/trimesh/path/path.py b/trimesh/path/path.py index 770243b20..849caf771 100644 --- a/trimesh/path/path.py +++ b/trimesh/path/path.py @@ -10,7 +10,7 @@ from ..points import plane_fit from ..geometry import plane_transform - +from ..visual import to_rgba from ..constants import log from ..constants import tol_path as tol @@ -87,12 +87,15 @@ def __init__(self, self.entities = entities self.vertices = vertices + + # assign each color to each entity self.colors = colors + # collect metadata into new dictionary self.metadata = dict() - if metadata.__class__.__name__ == 'dict': self.metadata.update(metadata) + # cache will dump whenever self.crc changes self._cache = caching.Cache(id_function=self.crc) if process: @@ -121,11 +124,45 @@ def process(self): @property def colors(self): - return self._colors + """ + Colors are stored per-entity. + + Returns + ------------ + colors : (len(entities), 4) uint8 + RGBA colors for each entity + """ + # start with default colors + colors = np.ones((len(self.entities), 4)) + colors = (colors * [100, 100, 100, 255]).astype(np.uint8) + # collect colors from entities + for i, e in enumerate(self.entities): + if hasattr(e, 'color') and e.color is not None: + colors[i] = to_rgba(e.color) + # don't allow parts of the color array to be written + colors.flags['WRITEABLE'] = False + return colors @colors.setter def colors(self, values): - self._colors = caching.tracked_array(values) + """ + Set the color for every entity in the Path. + + Parameters + ------------ + values : (len(entities), 4) uint8 + Color of each entity + """ + # if not set return + if values is None: + return + # make sure colors are RGBA + colors = to_rgba(values) + if len(colors) != len(self.entities): + raise ValueError('colors must be per-entity!') + # otherwise assign each color to the entity + for c, e in zip(colors, self.entities): + e.color = c @property def vertices(self): diff --git a/trimesh/rendering.py b/trimesh/rendering.py index 75d407b6d..f6031e97a 100644 --- a/trimesh/rendering.py +++ b/trimesh/rendering.py @@ -52,7 +52,8 @@ def convert_to_vertexlist(geometry, **kwargs): elif util.is_instance_named(geometry, 'Path'): # works for Path3D and Path2D # both of which inherit from Path - return path_to_vertexlist(geometry, colors=geometry.colors, **kwargs) + return path_to_vertexlist(geometry, + **kwargs) elif util.is_instance_named(geometry, 'PointCloud'): # pointcloud objects contain colors return points_to_vertexlist(geometry.vertices, @@ -154,7 +155,7 @@ def mesh_to_vertexlist(mesh, return args -def path_to_vertexlist(path, group=None, colors=None, **kwargs): +def path_to_vertexlist(path, group=None, **kwargs): """ Convert a Path3D object to arguments for a pyglet indexed vertex list constructor. @@ -175,24 +176,32 @@ def path_to_vertexlist(path, group=None, colors=None, **kwargs): vertices = path.vertices # get (n, 2, (2|3)) lines - lines = np.vstack([util.stack_lines(e.discrete(vertices)) - for e in path.entities]) + stacked = [util.stack_lines(e.discrete(vertices)) + for e in path.entities] + lines = util.vstack_empty(stacked) count = len(lines) # stack zeros for 2D lines if util.is_shape(vertices, (-1, 2)): lines = lines.reshape((-1, 2)) lines = np.column_stack((lines, np.zeros(len(lines)))) - # index for GL is one per point index = np.arange(count).tolist() - + # convert from entity color to the color of + # each vertex in the line segments + vcolor = np.vstack( + [(np.ones((len(s), 4)) * c).astype(np.uint8) + for s, c in zip(stacked, path.colors)]) + # convert to gl-friendly colors + gl_colors = colors_to_gl(vcolor, count=count) + + # collect args for vertexlist constructor args = (count, # number of lines GL_LINES, # mode group, # group index, # indices ('v3f/static', lines.reshape(-1)), - colors_to_gl(colors, count=count)) # default colors + gl_colors) return args @@ -238,7 +247,8 @@ def points_to_vertexlist(points, def colors_to_gl(colors, count): """ - Given a list of colors (or None) return a GL-acceptable list of colors + Given a list of colors (or None) return a GL-acceptable + list of colors. Parameters ------------ diff --git a/trimesh/util.py b/trimesh/util.py index 4391f068e..ca31eca55 100644 --- a/trimesh/util.py +++ b/trimesh/util.py @@ -1018,7 +1018,9 @@ def stack_lines(indices): """ indices = np.asanyarray(indices) - if is_sequence(indices[0]): + if len(indices) == 0: + return np.array([]) + elif is_sequence(indices[0]): shape = (-1, len(indices[0])) else: shape = (-1, 2) From be4fecb761673e6d145dda805e96bc5923dcc5e5 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 11 Dec 2019 15:30:40 -0500 Subject: [PATCH 08/10] bump for #655 #656 #657 #658 #659 --- trimesh/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trimesh/version.py b/trimesh/version.py index b51de07f7..01bd03cec 100644 --- a/trimesh/version.py +++ b/trimesh/version.py @@ -1 +1 @@ -__version__ = '3.4.10' +__version__ = '3.5.0' From a0e010e719a275ad42909b59d06a39332f7f2383 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 11 Dec 2019 15:40:59 -0500 Subject: [PATCH 09/10] pep8 whitespace --- tests/generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/generic.py b/tests/generic.py index a6e29ea07..a5afc82d5 100644 --- a/tests/generic.py +++ b/tests/generic.py @@ -302,6 +302,7 @@ def check_path2D(path): assert path.colors.shape == (len(path.entities), 4) + def scene_equal(a, b): """ Do a simple check on two scenes and assert From 23544910305c82db8369ff8b6481256c158da153 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Wed, 11 Dec 2019 15:59:36 -0500 Subject: [PATCH 10/10] fix color reference in DXF export --- trimesh/path/exchange/dxf.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/trimesh/path/exchange/dxf.py b/trimesh/path/exchange/dxf.py index 04e20ae70..150c52b33 100644 --- a/trimesh/path/exchange/dxf.py +++ b/trimesh/path/exchange/dxf.py @@ -555,16 +555,11 @@ def entity_info(entity): subs : dict Has keys 'COLOR', 'LAYER', 'NAME' """ + # TODO : convert RGBA entity.color to index subs = {'COLOR': 255, # default is ByLayer 'LAYER': 0, 'NAME': str(id(entity))[:16]} - if hasattr(entity, 'color'): - # all colors must be integers between 0-255 - color = str(entity.color) - if str.isnumeric(color): - subs['COLOR'] = int(color) % 256 - if hasattr(entity, 'layer'): subs['LAYER'] = str(entity.layer)