Skip to content

Commit 5c3e56b

Browse files
committed
add triangulate method to paths
1 parent 52f8569 commit 5c3e56b

7 files changed

Lines changed: 105 additions & 40 deletions

File tree

tests/test_bounds.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ def test_obb_order(self):
175175
uT, uE = g.trimesh.bounds.oriented_bounds(b, ordered=False)
176176
assert g.np.allclose(g.np.sort(uE), extents_ordered)
177177
# create a box from the unordered OBB information
178-
uB = g.trimesh.creation.box(extents=uE, transform=g.np.linalg.inv(uT))
178+
uB = g.trimesh.creation.box(
179+
extents=uE, transform=g.np.linalg.inv(uT))
179180
# make sure it is a real OBB too
180181
assert g.np.allclose(uB.bounds, b.bounds)
181182

tests/test_creation.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,27 +132,45 @@ def test_triangulate(self):
132132
for poly in [bigger, smaller, donut, bench]:
133133
v, f = g.trimesh.creation.triangulate_polygon(
134134
poly, engine=engine)
135-
assert g.trimesh.util.is_shape(v, (-1, 2))
136-
assert v.dtype.kind == 'f'
137-
assert g.trimesh.util.is_shape(f, (-1, 3))
138-
assert f.dtype.kind == 'i'
139-
tri = g.trimesh.util.three_dimensionalize(v)[1][f]
140-
area = g.trimesh.triangles.area(tri).sum()
141-
assert g.np.isclose(area, poly.area)
142135

136+
# run asserts
137+
check_triangulation(v, f, poly.area)
143138
try:
144139
# do a quick benchmark per engine
145-
# in general triangle appears to be 2x faster than meshpy
140+
# in general triangle appears to be 2x
141+
# faster than meshpy
146142
times[engine] = min(
147-
g.timeit.repeat('t(p, engine=e)',
148-
repeat=3,
149-
number=iterations,
150-
globals={'t': g.trimesh.creation.triangulate_polygon,
151-
'p': bench,
152-
'e': engine})) / iterations
143+
g.timeit.repeat(
144+
't(p, engine=e)',
145+
repeat=3,
146+
number=iterations,
147+
globals={'t': g.trimesh.creation.triangulate_polygon,
148+
'p': bench,
149+
'e': engine})) / iterations
153150
except BaseException:
154-
g.log.error('failed to benchmark triangle', exc_info=True)
155-
g.log.warning('benchmarked triangle interfaces: {}'.format(str(times)))
151+
g.log.error(
152+
'failed to benchmark triangle', exc_info=True)
153+
g.log.warning(
154+
'benchmarked triangle interfaces: {}'.format(str(times)))
155+
156+
def test_triangulate_plumbing(self):
157+
"""
158+
Check the plumbing of path triangulation
159+
"""
160+
p = g.get_mesh('2D/ChuteHolderPrint.DXF')
161+
v, f = p.triangulate()
162+
check_triangulation(v, f, p.area)
163+
164+
165+
def check_triangulation(v, f, true_area):
166+
assert g.trimesh.util.is_shape(v, (-1, 2))
167+
assert v.dtype.kind == 'f'
168+
assert g.trimesh.util.is_shape(f, (-1, 3))
169+
assert f.dtype.kind == 'i'
170+
171+
tri = g.trimesh.util.three_dimensionalize(v)[1][f]
172+
area = g.trimesh.triangles.area(tri).sum()
173+
assert g.np.isclose(area, true_area)
156174

157175

158176
if __name__ == '__main__':

tests/test_paths.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test_discrete(self):
4343

4444
# copy should have saved the metadata
4545
assert set(copied.metadata.keys()) == set(d.metadata.keys())
46-
46+
4747
# file_name should be populated, and if we have a DXF file
4848
# the layer field should be populated with layer names
4949
if d.metadata['file_name'][-3:] == 'dxf':

tests/test_vector.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,26 @@
77
class SphericalTests(g.unittest.TestCase):
88

99
def test_spherical(self):
10+
"""
11+
Convert vectors to spherical coordinates
12+
"""
13+
# random unit vectors
1014
v = g.trimesh.unitize(g.np.random.random((1000, 3)) - .5)
15+
# (n, 2) angles in radians
1116
spherical = g.trimesh.util.vector_to_spherical(v)
17+
# back to unit vectors
1218
v2 = g.trimesh.util.spherical_to_vector(spherical)
13-
self.assertTrue((g.np.abs(v - v2) < g.trimesh.constants.tol.merge).all())
19+
20+
assert g.np.allclose(v, v2)
1421

1522

1623
class HemisphereTests(g.unittest.TestCase):
1724

1825
def test_hemisphere(self):
1926
for dimension in [2, 3]:
20-
v = g.trimesh.unitize(g.np.random.random((10000, dimension)) - .5)
27+
# random unit vectors
28+
v = g.trimesh.unitize(
29+
g.np.random.random((10000, dimension)) - .5)
2130

2231
# add some on- axis points
2332
v[:dimension] = g.np.eye(dimension)

tests/test_visual.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def test_concatenate(self):
3030

3131
def test_data_model(self):
3232
"""
33-
Test the probably too- magical color caching and storage
33+
Test the probably too- magical color caching and storage
3434
system.
3535
"""
3636
m = g.get_mesh('featuretype.STL')

trimesh/path/path.py

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,8 @@ def polygons_full(self):
10291029
children = [closed[child]
10301030
for child in enclosure[root].keys()]
10311031
# all polygons_closed are CCW, so for interiors reverse them
1032-
holes = [np.array(p.exterior.coords)[::-1] for p in children]
1032+
holes = [np.array(p.exterior.coords)[::-1]
1033+
for p in children]
10331034
# a single Polygon object
10341035
shell = closed[root].exterior
10351036
# create a polygon with interiors
@@ -1095,6 +1096,36 @@ def extrude(self, height, **kwargs):
10951096
return result[0]
10961097
return result
10971098

1099+
def triangulate(self, **kwargs):
1100+
"""
1101+
Create a region- aware triangulation of the 2D path.
1102+
1103+
Parameters
1104+
-------------
1105+
**kwargs : dict
1106+
Passed to trimesh.creation.triangulate_polygon
1107+
1108+
Returns
1109+
-------------
1110+
vertices : (n, 2) float
1111+
2D vertices of triangulation
1112+
faces : (n, 3) int
1113+
Indexes of vertices for triangles
1114+
"""
1115+
from ..creation import triangulate_polygon
1116+
1117+
# append vertices and faces into sequence
1118+
v_seq = []
1119+
f_seq = []
1120+
1121+
# loop through polygons with interiors
1122+
for polygon in self.polygons_full:
1123+
v, f = triangulate_polygon(polygon, **kwargs)
1124+
v_seq.append(v)
1125+
f_seq.append(f)
1126+
1127+
return util.append_faces(v_seq, f_seq)
1128+
10981129
def medial_axis(self, resolution=None, clip=None):
10991130
"""
11001131
Find the approximate medial axis based
@@ -1103,16 +1134,15 @@ def medial_axis(self, resolution=None, clip=None):
11031134
11041135
Parameters
11051136
----------
1106-
resolution: target distance between each sample on the polygon boundary
1107-
clip: [minimum number of samples, maximum number of samples]
1108-
specifying a very fine resolution can cause the sample count to
1109-
explode, so clip specifies a minimum and maximum number of samples
1110-
to use per boundary region. To not clip, this can be specified as:
1111-
[0, np.inf]
1137+
resolution : None or float
1138+
Distance between each sample on the polygon boundary
1139+
clip : None, or (2,) float
1140+
Min, max number of samples
11121141
11131142
Returns
11141143
----------
11151144
medial : Path2D object
1145+
Contains only medial axis of Path
11161146
"""
11171147
if resolution is None:
11181148
resolution = self.scale / 1000.0
@@ -1124,23 +1154,27 @@ def medial_axis(self, resolution=None, clip=None):
11241154

11251155
def connected_paths(self, path_id, include_self=False):
11261156
"""
1127-
Given an index of self.paths, find other paths which overlap with
1128-
that path.
1157+
Given an index of self.paths find other paths which
1158+
overlap with that path.
11291159
11301160
Parameters
11311161
-----------
1132-
path_id: int, index of self.paths
1133-
include_self: bool, should the result include path_id or not
1162+
path_id : int
1163+
Index of self.paths
1164+
include_self : bool
1165+
Should the result include path_id or not
11341166
11351167
Returns
11361168
-----------
1137-
path_ids: (n,) int, indexes of self.paths that overlap input path_id
1169+
path_ids : (n, ) int
1170+
Indexes of self.paths that overlap input path_id
11381171
"""
11391172
if len(self.root) == 1:
11401173
path_ids = np.arange(len(self.polygons_closed))
11411174
else:
1142-
path_ids = list(nx.node_connected_component(self.enclosure,
1143-
path_id))
1175+
path_ids = list(nx.node_connected_component(
1176+
self.enclosure,
1177+
path_id))
11441178
if include_self:
11451179
return np.array(path_ids)
11461180
return np.setdiff1d(path_ids, [path_id])
@@ -1152,7 +1186,7 @@ def simplify(self, **kwargs):
11521186
11531187
Returns
11541188
---------
1155-
simplified: Path2D object
1189+
simplified : Path2D object
11561190
"""
11571191
return simplify.simplify_basic(self, **kwargs)
11581192

@@ -1162,8 +1196,10 @@ def simplify_spline(self, path_indexes=None, smooth=.0002):
11621196
11631197
Parameters
11641198
-----------
1165-
path_indexes: (n) int list of indexes for self.paths
1166-
smooth: float, how much the spline should smooth the curve
1199+
path_indexes : (n) int
1200+
List of indexes of self.paths to convert
1201+
smooth : float
1202+
How much the spline should smooth the curve
11671203
11681204
Returns
11691205
------------
@@ -1180,7 +1216,8 @@ def split(self):
11801216
11811217
Returns
11821218
----------
1183-
split: (n,) list of Path2D objects
1219+
split: (n,) list of Path2D objects
1220+
Each connected region and interiors
11841221
"""
11851222
return traversal.split(self)
11861223

trimesh/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '2.35.10'
1+
__version__ = '2.35.11'

0 commit comments

Comments
 (0)