Skip to content

Commit

Permalink
Merge pull request #660 from mikedh/merge/widget
Browse files Browse the repository at this point in the history
Widget And Path Updates
  • Loading branch information
mikedh committed Dec 11, 2019
2 parents 592d847 + 2354491 commit 5f2980f
Show file tree
Hide file tree
Showing 15 changed files with 225 additions and 36 deletions.
2 changes: 2 additions & 0 deletions tests/generic.py
Expand Up @@ -300,6 +300,8 @@ 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):
"""
Expand Down
14 changes: 14 additions & 0 deletions tests/test_paths.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand Down
15 changes: 15 additions & 0 deletions tests/test_segments.py
Expand Up @@ -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()
Expand Down
12 changes: 4 additions & 8 deletions trimesh/parent.py
Expand Up @@ -9,6 +9,7 @@
import numpy as np

from . import caching
from . import transformations
from .util import ABC


Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions trimesh/path/entities.py
Expand Up @@ -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

Expand Down
7 changes: 1 addition & 6 deletions trimesh/path/exchange/dxf.py
Expand Up @@ -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)

Expand Down
53 changes: 50 additions & 3 deletions trimesh/path/path.py
Expand Up @@ -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

Expand Down Expand Up @@ -68,7 +68,8 @@ def __init__(self,
entities=None,
vertices=None,
metadata=None,
process=True):
process=True,
colors=None):
"""
Instantiate a path object.
Expand All @@ -86,11 +87,15 @@ def __init__(self,

self.entities = entities
self.vertices = vertices
self.metadata = dict()

# 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:
Expand All @@ -117,6 +122,48 @@ def process(self):
func()
return self

@property
def colors(self):
"""
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):
"""
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):
return self._vertices
Expand Down
97 changes: 97 additions & 0 deletions trimesh/path/segments.py
Expand Up @@ -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
26 changes: 18 additions & 8 deletions trimesh/rendering.py
Expand Up @@ -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, **kwargs)
return path_to_vertexlist(geometry,
**kwargs)
elif util.is_instance_named(geometry, 'PointCloud'):
# pointcloud objects contain colors
return points_to_vertexlist(geometry.vertices,
Expand Down Expand Up @@ -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.
Expand All @@ -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


Expand Down Expand Up @@ -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
------------
Expand Down
2 changes: 1 addition & 1 deletion trimesh/scene/transforms.py
Expand Up @@ -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():
Expand Down
4 changes: 3 additions & 1 deletion trimesh/util.py
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion trimesh/version.py
@@ -1 +1 @@
__version__ = '3.4.10'
__version__ = '3.5.0'

0 comments on commit 5f2980f

Please sign in to comment.