Skip to content

Commit

Permalink
add triangulate method to paths
Browse files Browse the repository at this point in the history
  • Loading branch information
mikedh committed Oct 26, 2018
1 parent 52f8569 commit 5c3e56b
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 40 deletions.
3 changes: 2 additions & 1 deletion tests/test_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ def test_obb_order(self):
uT, uE = g.trimesh.bounds.oriented_bounds(b, ordered=False)
assert g.np.allclose(g.np.sort(uE), extents_ordered)
# create a box from the unordered OBB information
uB = g.trimesh.creation.box(extents=uE, transform=g.np.linalg.inv(uT))
uB = g.trimesh.creation.box(
extents=uE, transform=g.np.linalg.inv(uT))
# make sure it is a real OBB too
assert g.np.allclose(uB.bounds, b.bounds)

Expand Down
50 changes: 34 additions & 16 deletions tests/test_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,27 +132,45 @@ def test_triangulate(self):
for poly in [bigger, smaller, donut, bench]:
v, f = g.trimesh.creation.triangulate_polygon(
poly, engine=engine)
assert g.trimesh.util.is_shape(v, (-1, 2))
assert v.dtype.kind == 'f'
assert g.trimesh.util.is_shape(f, (-1, 3))
assert f.dtype.kind == 'i'
tri = g.trimesh.util.three_dimensionalize(v)[1][f]
area = g.trimesh.triangles.area(tri).sum()
assert g.np.isclose(area, poly.area)

# run asserts
check_triangulation(v, f, poly.area)
try:
# do a quick benchmark per engine
# in general triangle appears to be 2x faster than meshpy
# in general triangle appears to be 2x
# faster than meshpy
times[engine] = min(
g.timeit.repeat('t(p, engine=e)',
repeat=3,
number=iterations,
globals={'t': g.trimesh.creation.triangulate_polygon,
'p': bench,
'e': engine})) / iterations
g.timeit.repeat(
't(p, engine=e)',
repeat=3,
number=iterations,
globals={'t': g.trimesh.creation.triangulate_polygon,
'p': bench,
'e': engine})) / iterations
except BaseException:
g.log.error('failed to benchmark triangle', exc_info=True)
g.log.warning('benchmarked triangle interfaces: {}'.format(str(times)))
g.log.error(
'failed to benchmark triangle', exc_info=True)
g.log.warning(
'benchmarked triangle interfaces: {}'.format(str(times)))

def test_triangulate_plumbing(self):
"""
Check the plumbing of path triangulation
"""
p = g.get_mesh('2D/ChuteHolderPrint.DXF')
v, f = p.triangulate()
check_triangulation(v, f, p.area)


def check_triangulation(v, f, true_area):
assert g.trimesh.util.is_shape(v, (-1, 2))
assert v.dtype.kind == 'f'
assert g.trimesh.util.is_shape(f, (-1, 3))
assert f.dtype.kind == 'i'

tri = g.trimesh.util.three_dimensionalize(v)[1][f]
area = g.trimesh.triangles.area(tri).sum()
assert g.np.isclose(area, true_area)


if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion tests/test_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_discrete(self):

# copy should have saved the metadata
assert set(copied.metadata.keys()) == set(d.metadata.keys())

# file_name should be populated, and if we have a DXF file
# the layer field should be populated with layer names
if d.metadata['file_name'][-3:] == 'dxf':
Expand Down
13 changes: 11 additions & 2 deletions tests/test_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
class SphericalTests(g.unittest.TestCase):

def test_spherical(self):
"""
Convert vectors to spherical coordinates
"""
# random unit vectors
v = g.trimesh.unitize(g.np.random.random((1000, 3)) - .5)
# (n, 2) angles in radians
spherical = g.trimesh.util.vector_to_spherical(v)
# back to unit vectors
v2 = g.trimesh.util.spherical_to_vector(spherical)
self.assertTrue((g.np.abs(v - v2) < g.trimesh.constants.tol.merge).all())

assert g.np.allclose(v, v2)


class HemisphereTests(g.unittest.TestCase):

def test_hemisphere(self):
for dimension in [2, 3]:
v = g.trimesh.unitize(g.np.random.random((10000, dimension)) - .5)
# random unit vectors
v = g.trimesh.unitize(
g.np.random.random((10000, dimension)) - .5)

# add some on- axis points
v[:dimension] = g.np.eye(dimension)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_visual.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_concatenate(self):

def test_data_model(self):
"""
Test the probably too- magical color caching and storage
Test the probably too- magical color caching and storage
system.
"""
m = g.get_mesh('featuretype.STL')
Expand Down
73 changes: 55 additions & 18 deletions trimesh/path/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,8 @@ def polygons_full(self):
children = [closed[child]
for child in enclosure[root].keys()]
# all polygons_closed are CCW, so for interiors reverse them
holes = [np.array(p.exterior.coords)[::-1] for p in children]
holes = [np.array(p.exterior.coords)[::-1]
for p in children]
# a single Polygon object
shell = closed[root].exterior
# create a polygon with interiors
Expand Down Expand Up @@ -1095,6 +1096,36 @@ def extrude(self, height, **kwargs):
return result[0]
return result

def triangulate(self, **kwargs):
"""
Create a region- aware triangulation of the 2D path.
Parameters
-------------
**kwargs : dict
Passed to trimesh.creation.triangulate_polygon
Returns
-------------
vertices : (n, 2) float
2D vertices of triangulation
faces : (n, 3) int
Indexes of vertices for triangles
"""
from ..creation import triangulate_polygon

# append vertices and faces into sequence
v_seq = []
f_seq = []

# loop through polygons with interiors
for polygon in self.polygons_full:
v, f = triangulate_polygon(polygon, **kwargs)
v_seq.append(v)
f_seq.append(f)

return util.append_faces(v_seq, f_seq)

def medial_axis(self, resolution=None, clip=None):
"""
Find the approximate medial axis based
Expand All @@ -1103,16 +1134,15 @@ def medial_axis(self, resolution=None, clip=None):
Parameters
----------
resolution: target distance between each sample on the polygon boundary
clip: [minimum number of samples, maximum number of samples]
specifying a very fine resolution can cause the sample count to
explode, so clip specifies a minimum and maximum number of samples
to use per boundary region. To not clip, this can be specified as:
[0, np.inf]
resolution : None or float
Distance between each sample on the polygon boundary
clip : None, or (2,) float
Min, max number of samples
Returns
----------
medial : Path2D object
Contains only medial axis of Path
"""
if resolution is None:
resolution = self.scale / 1000.0
Expand All @@ -1124,23 +1154,27 @@ def medial_axis(self, resolution=None, clip=None):

def connected_paths(self, path_id, include_self=False):
"""
Given an index of self.paths, find other paths which overlap with
that path.
Given an index of self.paths find other paths which
overlap with that path.
Parameters
-----------
path_id: int, index of self.paths
include_self: bool, should the result include path_id or not
path_id : int
Index of self.paths
include_self : bool
Should the result include path_id or not
Returns
-----------
path_ids: (n,) int, indexes of self.paths that overlap input path_id
path_ids : (n, ) int
Indexes of self.paths that overlap input path_id
"""
if len(self.root) == 1:
path_ids = np.arange(len(self.polygons_closed))
else:
path_ids = list(nx.node_connected_component(self.enclosure,
path_id))
path_ids = list(nx.node_connected_component(
self.enclosure,
path_id))
if include_self:
return np.array(path_ids)
return np.setdiff1d(path_ids, [path_id])
Expand All @@ -1152,7 +1186,7 @@ def simplify(self, **kwargs):
Returns
---------
simplified: Path2D object
simplified : Path2D object
"""
return simplify.simplify_basic(self, **kwargs)

Expand All @@ -1162,8 +1196,10 @@ def simplify_spline(self, path_indexes=None, smooth=.0002):
Parameters
-----------
path_indexes: (n) int list of indexes for self.paths
smooth: float, how much the spline should smooth the curve
path_indexes : (n) int
List of indexes of self.paths to convert
smooth : float
How much the spline should smooth the curve
Returns
------------
Expand All @@ -1180,7 +1216,8 @@ def split(self):
Returns
----------
split: (n,) list of Path2D objects
split: (n,) list of Path2D objects
Each connected region and interiors
"""
return traversal.split(self)

Expand Down
2 changes: 1 addition & 1 deletion trimesh/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.35.10'
__version__ = '2.35.11'

0 comments on commit 5c3e56b

Please sign in to comment.