yapCAD 0.3.0__py2.py3-none-any.whl → 0.5.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yapcad/combine.py +82 -376
- yapcad/drawable.py +44 -3
- yapcad/ezdxf_drawable.py +1 -1
- yapcad/geom.py +204 -264
- yapcad/geom3d.py +975 -55
- yapcad/geom3d_util.py +541 -0
- yapcad/geom_util.py +817 -0
- yapcad/geometry.py +441 -49
- yapcad/geometry_checks.py +112 -0
- yapcad/geometry_utils.py +115 -0
- yapcad/io/__init__.py +5 -0
- yapcad/io/stl.py +83 -0
- yapcad/mesh.py +46 -0
- yapcad/metadata.py +109 -0
- yapcad/octtree.py +627 -0
- yapcad/poly.py +153 -299
- yapcad/pyglet_drawable.py +597 -61
- yapcad/triangulator.py +103 -0
- yapcad/xform.py +0 -1
- {yapcad-0.3.0.dist-info → yapcad-0.5.0.dist-info}/METADATA +94 -38
- yapcad-0.5.0.dist-info/RECORD +27 -0
- yapcad-0.3.0.dist-info/RECORD +0 -17
- {yapcad-0.3.0.dist-info → yapcad-0.5.0.dist-info}/WHEEL +0 -0
- {yapcad-0.3.0.dist-info → yapcad-0.5.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {yapcad-0.3.0.dist-info → yapcad-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {yapcad-0.3.0.dist-info → yapcad-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
- {yapcad-0.3.0.dist-info → yapcad-0.5.0.dist-info}/top_level.txt +0 -0
yapcad/geom_util.py
ADDED
@@ -0,0 +1,817 @@
|
|
1
|
+
## utilty functions companion to yapcad.geom
|
2
|
+
## Born on 26 December, 2020
|
3
|
+
## Copyright (c) 2020 Richard DeVaul
|
4
|
+
|
5
|
+
# Permission is hereby granted, free of charge, to any person
|
6
|
+
# obtaining a copy of this software and associated documentation files
|
7
|
+
# (the "Software"), to deal in the Software without restriction,
|
8
|
+
# including without limitation the rights to use, copy, modify, merge,
|
9
|
+
# publish, distribute, sublicense, and/or sell copies of the Software,
|
10
|
+
# and to permit persons to whom the Software is furnished to do so,
|
11
|
+
# subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be
|
14
|
+
# included in all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
20
|
+
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
21
|
+
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
22
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
# SOFTWARE.
|
24
|
+
|
25
|
+
from yapcad.geom import *
|
26
|
+
import math
|
27
|
+
import random
|
28
|
+
|
29
|
+
"""
|
30
|
+
Less-essential yapCAD utility functions to support the creation and
|
31
|
+
operations on yapcad.geom figures.
|
32
|
+
|
33
|
+
"""
|
34
|
+
|
35
|
+
|
36
|
+
def _reverse_element(element):
|
37
|
+
"""Return a geometry element with its orientation reversed."""
|
38
|
+
|
39
|
+
return reverseGeomList([element])[0]
|
40
|
+
|
41
|
+
|
42
|
+
def _poly_signed_area(points):
|
43
|
+
"""Compute signed area of a closed polygon represented by ``points``."""
|
44
|
+
|
45
|
+
if not points:
|
46
|
+
return 0.0
|
47
|
+
total = 0.0
|
48
|
+
for i in range(1, len(points)):
|
49
|
+
x1, y1 = points[i-1][0], points[i-1][1]
|
50
|
+
x2, y2 = points[i][0], points[i][1]
|
51
|
+
total += (x1 * y2) - (x2 * y1)
|
52
|
+
return 0.5 * total
|
53
|
+
|
54
|
+
|
55
|
+
def _stitch_geomlist(gl, tol):
|
56
|
+
"""Group line/arc primitives from ``gl`` into contiguous loops."""
|
57
|
+
|
58
|
+
segments = [g for g in gl if isline(g) or isarc(g)]
|
59
|
+
others = [g for g in gl if not (isline(g) or isarc(g))]
|
60
|
+
|
61
|
+
if not segments:
|
62
|
+
return [], others
|
63
|
+
|
64
|
+
nodes = []
|
65
|
+
|
66
|
+
def node_id(p):
|
67
|
+
for idx, existing in enumerate(nodes):
|
68
|
+
if dist(existing, p) <= tol:
|
69
|
+
return idx
|
70
|
+
nodes.append(point(p))
|
71
|
+
return len(nodes) - 1
|
72
|
+
|
73
|
+
edges = []
|
74
|
+
adjacency = {}
|
75
|
+
|
76
|
+
for idx, seg in enumerate(segments):
|
77
|
+
start = sample(seg, 0.0)
|
78
|
+
end = sample(seg, 1.0)
|
79
|
+
s_id = node_id(start)
|
80
|
+
e_id = node_id(end)
|
81
|
+
edges.append({'geom': seg, 'start': s_id, 'end': e_id, 'used': False})
|
82
|
+
adjacency.setdefault(s_id, []).append(idx)
|
83
|
+
adjacency.setdefault(e_id, []).append(idx)
|
84
|
+
|
85
|
+
loops = []
|
86
|
+
|
87
|
+
for idx in range(len(edges)):
|
88
|
+
edge = edges[idx]
|
89
|
+
if edge['used']:
|
90
|
+
continue
|
91
|
+
loop = []
|
92
|
+
current_idx = idx
|
93
|
+
start_node = edge['start']
|
94
|
+
prev_node = start_node
|
95
|
+
while True:
|
96
|
+
edge = edges[current_idx]
|
97
|
+
if edge['used']:
|
98
|
+
break
|
99
|
+
if edge['start'] != prev_node:
|
100
|
+
if dist(nodes[edge['end']], nodes[prev_node]) <= tol:
|
101
|
+
edge['geom'] = _reverse_element(edge['geom'])
|
102
|
+
edge['start'], edge['end'] = edge['end'], edge['start']
|
103
|
+
elif dist(nodes[edge['start']], nodes[prev_node]) > tol:
|
104
|
+
break
|
105
|
+
loop.append(edge['geom'])
|
106
|
+
edge['used'] = True
|
107
|
+
prev_node = edge['end']
|
108
|
+
if len(loop) > 1 and dist(nodes[prev_node], nodes[start_node]) <= tol:
|
109
|
+
break
|
110
|
+
next_idx = None
|
111
|
+
for cand_idx in adjacency.get(prev_node, []):
|
112
|
+
if edges[cand_idx]['used']:
|
113
|
+
continue
|
114
|
+
next_idx = cand_idx
|
115
|
+
break
|
116
|
+
if next_idx is None:
|
117
|
+
break
|
118
|
+
current_idx = next_idx
|
119
|
+
if loop:
|
120
|
+
loops.append(loop)
|
121
|
+
|
122
|
+
return loops, others
|
123
|
+
|
124
|
+
|
125
|
+
def _finalize_poly_points(points, snap_tol):
|
126
|
+
if not points:
|
127
|
+
return []
|
128
|
+
|
129
|
+
ply = [point(points[0])]
|
130
|
+
for p in points[1:]:
|
131
|
+
if dist(p, ply[-1]) > epsilon:
|
132
|
+
ply.append(point(p))
|
133
|
+
|
134
|
+
if dist(ply[0], ply[-1]) > epsilon:
|
135
|
+
if dist(ply[0], ply[-1]) <= snap_tol:
|
136
|
+
ply[-1] = point(ply[0])
|
137
|
+
else:
|
138
|
+
ply.append(point(ply[0]))
|
139
|
+
else:
|
140
|
+
ply[-1] = point(ply[0])
|
141
|
+
|
142
|
+
return ply
|
143
|
+
|
144
|
+
|
145
|
+
def _convert_geom_sequence(seq, minang, minlen, snap_tol):
|
146
|
+
ply = []
|
147
|
+
lastpoint = None
|
148
|
+
nested_holes = []
|
149
|
+
|
150
|
+
def _snap(p, last):
|
151
|
+
pp = point(p)
|
152
|
+
if last is not None and dist(pp, last) <= snap_tol:
|
153
|
+
return point(last)
|
154
|
+
return pp
|
155
|
+
|
156
|
+
def addpoint(p, last, force=False):
|
157
|
+
candidate = _snap(p, last)
|
158
|
+
if force or last is None or dist(candidate, last) > minlen:
|
159
|
+
last = point(candidate)
|
160
|
+
ply.append(last)
|
161
|
+
return last
|
162
|
+
|
163
|
+
for element in seq:
|
164
|
+
if ispoint(element):
|
165
|
+
continue
|
166
|
+
elif isline(element):
|
167
|
+
lastpoint = addpoint(element[0], lastpoint, force=True)
|
168
|
+
lastpoint = addpoint(element[1], lastpoint, force=True)
|
169
|
+
elif isarc(element):
|
170
|
+
firstpoint = None
|
171
|
+
for ang in range(0, 360, max(1, round(minang))):
|
172
|
+
p = polarSampleArc(element, float(ang), inside=True)
|
173
|
+
if not p:
|
174
|
+
break
|
175
|
+
if firstpoint is None:
|
176
|
+
firstpoint = p
|
177
|
+
lastpoint = addpoint(p, lastpoint)
|
178
|
+
if iscircle(element):
|
179
|
+
lastpoint = addpoint(firstpoint, lastpoint)
|
180
|
+
else:
|
181
|
+
endp = sample(element, 1.0)
|
182
|
+
if endp:
|
183
|
+
lastpoint = addpoint(endp, lastpoint, force=True)
|
184
|
+
elif ispoly(element):
|
185
|
+
for p in element:
|
186
|
+
lastpoint = addpoint(p, lastpoint, force=True)
|
187
|
+
elif isgeomlist(element):
|
188
|
+
sub_outer, sub_holes = geomlist2poly_with_holes(element, minang, minlen, checkcont=False)
|
189
|
+
if sub_outer:
|
190
|
+
for p in sub_outer:
|
191
|
+
lastpoint = addpoint(p, lastpoint, force=True)
|
192
|
+
nested_holes.extend(sub_holes)
|
193
|
+
else:
|
194
|
+
raise ValueError(f'bad object in list passed to geomlist2poly: {element}')
|
195
|
+
|
196
|
+
if not ply:
|
197
|
+
return [], nested_holes
|
198
|
+
|
199
|
+
cleaned = [ply[0]]
|
200
|
+
for p in ply[1:]:
|
201
|
+
if dist(p, cleaned[-1]) > epsilon:
|
202
|
+
cleaned.append(point(p))
|
203
|
+
|
204
|
+
if dist(cleaned[0], cleaned[-1]) > epsilon:
|
205
|
+
if dist(cleaned[0], cleaned[-1]) <= snap_tol:
|
206
|
+
cleaned[-1] = point(cleaned[0])
|
207
|
+
else:
|
208
|
+
cleaned.append(point(cleaned[0]))
|
209
|
+
else:
|
210
|
+
cleaned[-1] = point(cleaned[0])
|
211
|
+
|
212
|
+
return cleaned, nested_holes
|
213
|
+
|
214
|
+
|
215
|
+
def geomlist2poly_with_holes(gl, minang=5.0, minlen=0.25, checkcont=False):
|
216
|
+
|
217
|
+
if checkcont and not iscontinuousgeomlist(gl):
|
218
|
+
raise ValueError('non-continuous geometry list passed to geomlist2poly')
|
219
|
+
|
220
|
+
snap_tol = max(epsilon * 10, minlen * 0.1)
|
221
|
+
|
222
|
+
positive_candidates = []
|
223
|
+
negative_candidates = []
|
224
|
+
|
225
|
+
segments = []
|
226
|
+
|
227
|
+
for element in gl:
|
228
|
+
if ispoint(element):
|
229
|
+
continue
|
230
|
+
elif isline(element) or isarc(element):
|
231
|
+
segments.append(element)
|
232
|
+
elif ispoly(element):
|
233
|
+
poly = _finalize_poly_points(element, snap_tol)
|
234
|
+
area = _poly_signed_area(poly)
|
235
|
+
if area >= 0:
|
236
|
+
positive_candidates.append(poly)
|
237
|
+
else:
|
238
|
+
negative_candidates.append(poly)
|
239
|
+
elif isgeomlist(element):
|
240
|
+
outer, holes = geomlist2poly_with_holes(element, minang, minlen, checkcont)
|
241
|
+
if outer:
|
242
|
+
area = _poly_signed_area(outer)
|
243
|
+
if area >= 0:
|
244
|
+
positive_candidates.append(outer)
|
245
|
+
else:
|
246
|
+
negative_candidates.append(outer)
|
247
|
+
for h in holes:
|
248
|
+
area = _poly_signed_area(h)
|
249
|
+
if area >= 0:
|
250
|
+
positive_candidates.append(h)
|
251
|
+
else:
|
252
|
+
negative_candidates.append(h)
|
253
|
+
else:
|
254
|
+
raise ValueError(f'bad object in list passed to geomlist2poly: {element}')
|
255
|
+
|
256
|
+
if segments:
|
257
|
+
loops, _ = _stitch_geomlist(segments, snap_tol)
|
258
|
+
for loop in loops:
|
259
|
+
poly, holes = _convert_geom_sequence(loop, minang, minlen, snap_tol)
|
260
|
+
if poly:
|
261
|
+
area = _poly_signed_area(poly)
|
262
|
+
if area >= 0:
|
263
|
+
positive_candidates.append(poly)
|
264
|
+
else:
|
265
|
+
negative_candidates.append(poly)
|
266
|
+
for h in holes:
|
267
|
+
area = _poly_signed_area(h)
|
268
|
+
if area >= 0:
|
269
|
+
positive_candidates.append(h)
|
270
|
+
else:
|
271
|
+
negative_candidates.append(h)
|
272
|
+
|
273
|
+
if not positive_candidates and not negative_candidates:
|
274
|
+
return [], []
|
275
|
+
|
276
|
+
all_candidates = positive_candidates + negative_candidates
|
277
|
+
outer = max(all_candidates, key=lambda pts: abs(_poly_signed_area(pts)))
|
278
|
+
outer_area = _poly_signed_area(outer)
|
279
|
+
orientation = 1 if outer_area >= 0 else -1
|
280
|
+
|
281
|
+
holes = []
|
282
|
+
|
283
|
+
for pts in all_candidates:
|
284
|
+
if pts is outer:
|
285
|
+
continue
|
286
|
+
area = _poly_signed_area(pts)
|
287
|
+
if orientation * area < 0:
|
288
|
+
holes.append(pts)
|
289
|
+
continue
|
290
|
+
centroid = _polygon_centroid_xy(pts)
|
291
|
+
if _point_in_polygon_xy(outer, centroid):
|
292
|
+
holes.append(pts)
|
293
|
+
|
294
|
+
return outer, holes
|
295
|
+
|
296
|
+
|
297
|
+
def _polygon_centroid_xy(pts):
|
298
|
+
if not pts:
|
299
|
+
return (0.0, 0.0)
|
300
|
+
sx = sy = 0.0
|
301
|
+
count = 0
|
302
|
+
for p in pts:
|
303
|
+
sx += p[0]
|
304
|
+
sy += p[1]
|
305
|
+
count += 1
|
306
|
+
if count == 0:
|
307
|
+
return (0.0, 0.0)
|
308
|
+
return (sx / count, sy / count)
|
309
|
+
|
310
|
+
|
311
|
+
def _point_in_polygon_xy(poly, pt):
|
312
|
+
x, y = pt
|
313
|
+
inside = False
|
314
|
+
if not poly:
|
315
|
+
return False
|
316
|
+
j = len(poly) - 1
|
317
|
+
for i in range(len(poly)):
|
318
|
+
xi, yi = poly[i][0], poly[i][1]
|
319
|
+
xj, yj = poly[j][0], poly[j][1]
|
320
|
+
intersects = ((yi > y) != (yj > y)) and (
|
321
|
+
x < (xj - xi) * (y - yi) / (yj - yi + 1e-16) + xi)
|
322
|
+
if intersects:
|
323
|
+
inside = not inside
|
324
|
+
j = i
|
325
|
+
return inside
|
326
|
+
|
327
|
+
def randomPoints(bbox,numpoints):
|
328
|
+
"""Given a 3D bounding box and a number of points to generate,
|
329
|
+
return a list of uniformly generated random points within the
|
330
|
+
bounding box"""
|
331
|
+
|
332
|
+
points = []
|
333
|
+
minx = bbox[0][0]
|
334
|
+
maxx = bbox[1][0]
|
335
|
+
miny = bbox[0][1]
|
336
|
+
maxy = bbox[1][1]
|
337
|
+
minz = bbox[0][2]
|
338
|
+
maxz = bbox[1][2]
|
339
|
+
rangex = maxx-minx
|
340
|
+
rangey = maxy-miny
|
341
|
+
rangez = maxz-minz
|
342
|
+
for i in range(numpoints):
|
343
|
+
points.append(point(random.random()*rangex+minx,
|
344
|
+
random.random()*rangey+miny,
|
345
|
+
random.random()*rangez+minz))
|
346
|
+
return points
|
347
|
+
|
348
|
+
def randomCenterInBox(bbox,r):
|
349
|
+
"""given a bounding box and a radius, generate a random center point
|
350
|
+
such that a circle with the specified radius and center point falls
|
351
|
+
completely inside the box
|
352
|
+
|
353
|
+
"""
|
354
|
+
|
355
|
+
minx = bbox[0][0]+r
|
356
|
+
maxx = bbox[1][0]-r
|
357
|
+
miny = bbox[0][1]+r
|
358
|
+
maxy = bbox[1][1]-r
|
359
|
+
minz = bbox[0][2]+r
|
360
|
+
maxz = bbox[1][2]-r
|
361
|
+
rangex = maxx-minx
|
362
|
+
rangey = maxy-miny
|
363
|
+
rangez = maxz-minz
|
364
|
+
x = point(random.random()*rangex+minx,
|
365
|
+
random.random()*rangey+miny,
|
366
|
+
random.random()*rangez+minz)
|
367
|
+
return x
|
368
|
+
|
369
|
+
|
370
|
+
def randomArc(bbox,minr=0.0,maxr=10.0,circle=False):
|
371
|
+
"""given a bounding box and a minimum and maximum radius, generate an
|
372
|
+
arc with random center and radius that falls within the bounding
|
373
|
+
box. If ``circle==False``, use randomly generated start and end
|
374
|
+
angles, otherwise generate only full circles
|
375
|
+
|
376
|
+
"""
|
377
|
+
radr = maxr-minr
|
378
|
+
r = random.random()*radr+minr
|
379
|
+
start = 0
|
380
|
+
end = 360
|
381
|
+
if not circle:
|
382
|
+
start = random.random()*360.0
|
383
|
+
end = start + random.random()*360.0
|
384
|
+
|
385
|
+
x = randomCenterInBox(bbox,r)
|
386
|
+
return arc(x,r,start,end)
|
387
|
+
|
388
|
+
def randomPoly(bbox,numpoints=10,minr = 1.0,maxr = 10.0):
|
389
|
+
"""given a bounding box, a number of vertices, and a minimum and
|
390
|
+
maximum radius, generate a simple polygon that completely lies
|
391
|
+
inside the bounding box, whose vertices are evenly spaced by angle
|
392
|
+
around a center point, with randomly chosen radius between
|
393
|
+
``minr`` and ``maxr``
|
394
|
+
"""
|
395
|
+
|
396
|
+
angles = []
|
397
|
+
rads = []
|
398
|
+
ang = 0.0
|
399
|
+
rr = maxr-minr
|
400
|
+
for i in range(numpoints):
|
401
|
+
a = random.random()
|
402
|
+
angles.append(ang)
|
403
|
+
ang = ang + a
|
404
|
+
# print("a: ",a," ang: ",ang)
|
405
|
+
rads.append(random.random()*rr+minr)
|
406
|
+
|
407
|
+
sf = pi2/ang
|
408
|
+
|
409
|
+
points = []
|
410
|
+
x = randomCenterInBox(bbox,maxr)
|
411
|
+
|
412
|
+
for i in range(numpoints):
|
413
|
+
p = [cos(angles[i]*sf)*rads[i],
|
414
|
+
sin(angles[i]*sf)*rads[i],0,1]
|
415
|
+
points.append(add(p,x))
|
416
|
+
|
417
|
+
return points + [ points[0] ]
|
418
|
+
|
419
|
+
def makeLineSpiral(center, turnRad, # radius after one full turn
|
420
|
+
turns, # number of turns
|
421
|
+
dstep = 10.0, # sampling resolution in degrees
|
422
|
+
minlen = 0.25): # minimum distance between points
|
423
|
+
"""given a center point, the increase in radius per turn, the number
|
424
|
+
of turns, and the angular resolution of the approximation,
|
425
|
+
generate a yapcqad.geom poly approximation of the spiral
|
426
|
+
|
427
|
+
"""
|
428
|
+
|
429
|
+
# make a spiral of points
|
430
|
+
spiral = []
|
431
|
+
rstep = turnRad*dstep/360.0
|
432
|
+
|
433
|
+
def addpoint(p,lastpoint):
|
434
|
+
if not lastpoint or dist(p,lastpoint) > minlen:
|
435
|
+
lastpoint = p
|
436
|
+
spiral.append(p)
|
437
|
+
return lastpoint
|
438
|
+
|
439
|
+
lastpoint = None
|
440
|
+
for i in range(round(360*turns/dstep)):
|
441
|
+
ang = i * dstep*pi2/360.0
|
442
|
+
r = i * rstep
|
443
|
+
p = add(center,
|
444
|
+
point(math.cos(ang)*r,math.sin(ang)*r))
|
445
|
+
lastpoint = addpoint(p,lastpoint)
|
446
|
+
return spiral
|
447
|
+
|
448
|
+
def makeArcSpiral(center, turnRad, # radius after one full turn
|
449
|
+
turns, # number of turns
|
450
|
+
dstep = 45): # sampling resolution in degrees
|
451
|
+
"""given a center point, the increase in radius per turn, the number
|
452
|
+
of turns, and the angular resolution of the approximation,
|
453
|
+
generate an approximation of the spiral using circular arcs as
|
454
|
+
segments instead of straightlines. Return a yapcqad.geom geomlist
|
455
|
+
approximation of the spiral
|
456
|
+
|
457
|
+
"""
|
458
|
+
spPoints = makeLineSpiral(center,turnRad,turns,dstep)
|
459
|
+
|
460
|
+
arcs=[]
|
461
|
+
for i in range(1,len(spPoints)):
|
462
|
+
p0 = spPoints[i-1]
|
463
|
+
p1 = spPoints[i]
|
464
|
+
direct = sub(p1,p0)
|
465
|
+
orth = scale3(orthoXY(direct),-1.1)
|
466
|
+
mid = scale3(add(p0,p1),0.5)
|
467
|
+
cent = add(mid,orth)
|
468
|
+
r= dist(cent,p0)
|
469
|
+
r2 = dist(cent,p1)
|
470
|
+
assert close(r,r2)
|
471
|
+
circ = arc(cent,r)
|
472
|
+
u1 = unsamplearc(circ,p0)
|
473
|
+
u2 = unsamplearc(circ,p1)
|
474
|
+
a = segmentarc(circ,u1,u2)
|
475
|
+
arcs.append(a)
|
476
|
+
return arcs
|
477
|
+
|
478
|
+
def polarSampleArc(c,ang,inside=True):
|
479
|
+
"""given an arc ``c`` , sample that arc at ``ang`` degrees from the
|
480
|
+
start of the arc, proceeding clockwise. If ``samplereverse`` is
|
481
|
+
true, sample the arc at ``-ang`` degrees from the end, proceeding
|
482
|
+
counterclockwise. If ``c`` is not a complete circle and the
|
483
|
+
specified ``ang`` falls outside the closed [start,end] interval
|
484
|
+
and ``inside == True``, return ``False``, otherwise return the
|
485
|
+
sampled value.
|
486
|
+
|
487
|
+
"""
|
488
|
+
p = c[0]
|
489
|
+
r = c[1][0]
|
490
|
+
start=c[1][1]
|
491
|
+
end=c[1][2]
|
492
|
+
samplereverse = (c[1][3] == -2)
|
493
|
+
circle = (start == 0 and end == 360)
|
494
|
+
|
495
|
+
if not circle:
|
496
|
+
start = start % 360.0
|
497
|
+
end = end % 360.0
|
498
|
+
if end < start:
|
499
|
+
end += 360.0
|
500
|
+
if inside and ang > (end - start):
|
501
|
+
return False
|
502
|
+
|
503
|
+
if samplereverse:
|
504
|
+
start=end
|
505
|
+
ang *= -1
|
506
|
+
|
507
|
+
srad = (start+ang)*pi2/360.0
|
508
|
+
q = scale3(vect(cos(srad),sin(srad)),r)
|
509
|
+
return add(p,q)
|
510
|
+
|
511
|
+
|
512
|
+
def triarea(p1,p2,p3):
|
513
|
+
"""
|
514
|
+
utility function to return the area of a triangle
|
515
|
+
"""
|
516
|
+
v1=sub(p2,p1)
|
517
|
+
v2=sub(p3,p1)
|
518
|
+
cp = cross(v1,v2)
|
519
|
+
return mag(cp)/2
|
520
|
+
|
521
|
+
def geomlist2poly(gl,minang=5.0,minlen=0.25,checkcont=False):
|
522
|
+
outer, _ = geomlist2poly_with_holes(gl, minang, minlen, checkcont)
|
523
|
+
return outer
|
524
|
+
|
525
|
+
|
526
|
+
|
527
|
+
def combineglist(g1,g2,operation):
|
528
|
+
"""function to perform set operations on geometry lists.
|
529
|
+
|
530
|
+
``g1`` and ``g2`` are closed figure geometry lists.
|
531
|
+
``operation`` is one of ``["union","intersection","difference"]``
|
532
|
+
|
533
|
+
result is a potentially zero-length geometry list representing the
|
534
|
+
result, which may or may not be simply connected, but should
|
535
|
+
always be a closed figure.
|
536
|
+
|
537
|
+
"""
|
538
|
+
|
539
|
+
def poly2lines(pol):
|
540
|
+
l = []
|
541
|
+
for i in range(1,len(pol)):
|
542
|
+
l.append([pol[i-1],pol[i]])
|
543
|
+
return l
|
544
|
+
|
545
|
+
if ispoly(g1):
|
546
|
+
g1 = poly2lines(g1)
|
547
|
+
|
548
|
+
if ispoly(g2):
|
549
|
+
g2 = poly2lines(g2)
|
550
|
+
|
551
|
+
# if not (isclosedgeomlist(g1) and isclosedgeomlist(g2)):
|
552
|
+
# raise ValueError("bad arguments passed to combineglist")
|
553
|
+
|
554
|
+
if not operation in ["union","intersection","difference"]:
|
555
|
+
raise ValueError("bad operation specified for combineglist")
|
556
|
+
|
557
|
+
bbox1 = bbox(g1)
|
558
|
+
bbox2 = bbox(g2)
|
559
|
+
try :
|
560
|
+
inter = intersectXY(g1,g2,params=True)
|
561
|
+
except ValueError:
|
562
|
+
print("had a problem intersecting following geometries:")
|
563
|
+
print("g1: ",g1)
|
564
|
+
print("g2: ",g2)
|
565
|
+
raise
|
566
|
+
|
567
|
+
if inter != False and (inter[0] == False or inter[1] == False):
|
568
|
+
raise ValueError('bad intersection list: ',inter)
|
569
|
+
# Given parameter values that define two potential sub-arcs of
|
570
|
+
# a figure, determine which sub-arc is valid by looking for
|
571
|
+
# intersections between these parameter values. In the
|
572
|
+
# condition where u2 is smaller than u1, it's possible that
|
573
|
+
# this represents an arc in the counter-clockwise direction
|
574
|
+
# between u2 and u1. It also could represent a clockwise arc
|
575
|
+
# that "wraps around" from u1 to (u2+1). We will check for
|
576
|
+
# values x1, x2 in ilist that are u2 < x1 < u1 and u1 < x2 <
|
577
|
+
# (u2+1.0). The existance of x1 or x2 rules out the
|
578
|
+
# corresponding arc. If neither x1 nor x2 exists, we bias
|
579
|
+
# towards the counter-clockwise arc.
|
580
|
+
|
581
|
+
def between(u1,u2,ilist):
|
582
|
+
if len(ilist) < 2:
|
583
|
+
raise ValueError('bad ilist')
|
584
|
+
|
585
|
+
if u1 > u2 :
|
586
|
+
if len(ilist) == 2:
|
587
|
+
return u1,u2+1.0,False
|
588
|
+
x1s = list(filter(lambda x: x > u2 and x < u1,ilist))
|
589
|
+
x2s = list(filter(lambda x: x > u1 or x < u2,ilist))
|
590
|
+
l1 = len(x1s)
|
591
|
+
l2 = len(x2s)
|
592
|
+
#print("u1: ",u1," u2: ",u2," ilist: ",ilist," x1s: ",x1s," x2s: ",x2s)
|
593
|
+
#if l1 > 0 and l2 > 0:
|
594
|
+
# print('WARNING: intersections on both sides')
|
595
|
+
|
596
|
+
if l1 > l2:
|
597
|
+
# print("AA")
|
598
|
+
if True or operation == 'union':
|
599
|
+
return u1,u2+1.0,False
|
600
|
+
else:
|
601
|
+
return u2,u1,True
|
602
|
+
else:
|
603
|
+
# print("A-")
|
604
|
+
return u2,u1,True
|
605
|
+
|
606
|
+
else:
|
607
|
+
if len(ilist) == 2:
|
608
|
+
return u1,u2,False
|
609
|
+
x1s = list(filter(lambda x: x > u1 and x < u2,ilist))
|
610
|
+
x2s = list(filter(lambda x: x > u2 or x < u1,ilist))
|
611
|
+
l1 = len(x1s)
|
612
|
+
l2 = len(x2s)
|
613
|
+
#print("u1: ",u1," u2: ",u2," ilist: ",ilist," x1s: ",x1s," x2s: ",x2s)
|
614
|
+
#if l1 > 0 and l2 > 0:
|
615
|
+
# print('WARNING: intersections on both sides')
|
616
|
+
|
617
|
+
if l1 > l2:
|
618
|
+
# print("BB")
|
619
|
+
|
620
|
+
if True or operation == 'union':
|
621
|
+
return u2,u1+1,False
|
622
|
+
else:
|
623
|
+
return u1,u2,False
|
624
|
+
else:
|
625
|
+
# print("B-")
|
626
|
+
return u1,u2,False
|
627
|
+
|
628
|
+
|
629
|
+
## utility to perform combination on one "segment"
|
630
|
+
def cmbin(g1,g2,itr):
|
631
|
+
if not isgeomlist(g1):
|
632
|
+
g1 = [ g1 ]
|
633
|
+
|
634
|
+
if not isgeomlist(g2):
|
635
|
+
g2 = [ g2 ]
|
636
|
+
|
637
|
+
g1s = itr[0][0]
|
638
|
+
g1e = itr[0][1]
|
639
|
+
g2s = itr[1][0]
|
640
|
+
g2e = itr[1][1]
|
641
|
+
|
642
|
+
seg = []
|
643
|
+
ZLEN1=close(g1s,g1e)
|
644
|
+
ZLEN2=close(g2s,g2e)
|
645
|
+
|
646
|
+
g1reverse=False
|
647
|
+
g2reverse=False
|
648
|
+
|
649
|
+
if True or operation == 'difference':
|
650
|
+
if g1e < g1s:
|
651
|
+
g1e+=1.0
|
652
|
+
#g1s,g1e,g1reverse = between(g1s,g1e,inter[0])
|
653
|
+
#import pdb ; pdb.set_trace()
|
654
|
+
g2s,g2e,g2reverse = between(g2s,g2e,inter[1])
|
655
|
+
g2reverse = False
|
656
|
+
else:
|
657
|
+
if g1e < g1s:
|
658
|
+
g1e+=1.0
|
659
|
+
if g2e < g2s:
|
660
|
+
g2e += 1.0
|
661
|
+
|
662
|
+
#p1=sample(g1,((g1s+g1e)/2)%1.0)
|
663
|
+
|
664
|
+
p1inside=0
|
665
|
+
for i in range(5):
|
666
|
+
u = (i+1)/6.0
|
667
|
+
p = sample(g1,(u*g1e+(1.0-u)*g1s)%1.0)
|
668
|
+
if isinsideXY(g2,p):
|
669
|
+
p1inside=p1inside+1
|
670
|
+
|
671
|
+
p2inside = 0
|
672
|
+
for i in range(5):
|
673
|
+
u = (i+1)/6.0
|
674
|
+
p = sample(g2,(u*g2e+(1.0-u)*g2s)%1.0)
|
675
|
+
if isinsideXY(g1,p):
|
676
|
+
p2inside=p2inside+1
|
677
|
+
|
678
|
+
#if p1inside > 0 and p2inside > 0:
|
679
|
+
# print("warning: inside test succeeded for both p1s and p2s: ",
|
680
|
+
# p1inside," ",p2inside)
|
681
|
+
|
682
|
+
#if p1inside == 0 and p2inside == 0:
|
683
|
+
# print("warning: inside test failed for both p1s and p2s")
|
684
|
+
|
685
|
+
#p2=sample(g2,((g2s+g2e)/2)%1.0)
|
686
|
+
|
687
|
+
if ZLEN1 and ZLEN2:
|
688
|
+
print ('both segments zero length')
|
689
|
+
return []
|
690
|
+
elif ZLEN2 and not ZLEN1:
|
691
|
+
print ('zero length segment 2')
|
692
|
+
if operation=='union':
|
693
|
+
return segmentgeomlist(g1,g1s,g1e,closed=True)
|
694
|
+
elif operation=='difference':
|
695
|
+
return []
|
696
|
+
else: #intersection
|
697
|
+
return []
|
698
|
+
elif ZLEN1 and not ZLEN2:
|
699
|
+
print ('zero length segment 1')
|
700
|
+
if operation=='union':
|
701
|
+
if g2e < g2s:
|
702
|
+
g2e += 1.0
|
703
|
+
return segmentgeomlist(g2,g2s,g2e,closed=True)
|
704
|
+
else: # difference or intersection
|
705
|
+
return []
|
706
|
+
|
707
|
+
if operation == 'union':
|
708
|
+
#if isinsideXY(g2,p1):
|
709
|
+
if p1inside > p2inside:
|
710
|
+
# if g2e < g2s:
|
711
|
+
# g2e += 1.0
|
712
|
+
seg += segmentgeomlist(g2,g2s,g2e,closed=True)
|
713
|
+
else:
|
714
|
+
seg += segmentgeomlist(g1,g1s,g1e,closed=True)
|
715
|
+
elif operation == 'intersection':
|
716
|
+
#if isinsideXY(g2,p1):
|
717
|
+
if p1inside > p2inside:
|
718
|
+
seg += segmentgeomlist(g1,g1s,g1e,closed=True)
|
719
|
+
else:
|
720
|
+
# if g2e < g2s:
|
721
|
+
# g2e += 1.0
|
722
|
+
#seg += segmentgeomlist(g2,g2s,g2e,reverse=g2reverse)
|
723
|
+
seg += segmentgeomlist(g2,g2s,g2e,closed=True)
|
724
|
+
elif operation == 'difference':
|
725
|
+
s = []
|
726
|
+
#if isinsideXY(g2,p1):
|
727
|
+
if p1inside > p2inside:
|
728
|
+
pass
|
729
|
+
else:
|
730
|
+
# print("rsort: ",vstr(inter))
|
731
|
+
seg += segmentgeomlist(g1,g1s,g1e,closed=True)
|
732
|
+
# print("g2s: ",g2s," g2e: ",g2e," g2reverse: ",g2reverse)
|
733
|
+
s = segmentgeomlist(g2,g2s,g2e,closed=True,reverse=g2reverse)
|
734
|
+
s = reverseGeomList(s)
|
735
|
+
seg += s
|
736
|
+
if len(inter[0]) > 2:
|
737
|
+
pass
|
738
|
+
#combineDebugGL.append(s)
|
739
|
+
# print("seg: ",vstr(seg))
|
740
|
+
|
741
|
+
return seg
|
742
|
+
|
743
|
+
## utility function to sort intersections into non-decreasing
|
744
|
+
## order
|
745
|
+
def rsort(il):
|
746
|
+
nl = []
|
747
|
+
rl = []
|
748
|
+
rr = []
|
749
|
+
for i in range(len(il[0])):
|
750
|
+
nl.append([il[0][i],il[1][i]])
|
751
|
+
nl.sort(key=lambda x: x[0])
|
752
|
+
for i in range(len(nl)):
|
753
|
+
rl.append(nl[i][0])
|
754
|
+
rr.append(nl[i][1])
|
755
|
+
return [rl,rr]
|
756
|
+
|
757
|
+
if inter == False: # disjoint, but bounding boxes might be
|
758
|
+
# null or one could be inside the other
|
759
|
+
if not bbox1 and bbox2: # g1 is empty, but g2 contains geometry
|
760
|
+
if operation=='union':
|
761
|
+
return g2
|
762
|
+
else:
|
763
|
+
return []
|
764
|
+
if bbox1 and not bbox2: # g2 is empty, but g1 isn't
|
765
|
+
if operation=='union' or operation=='difference':
|
766
|
+
return g1
|
767
|
+
if not bbox1 and not bbox2: # no geometry at all
|
768
|
+
return []
|
769
|
+
## OK, no intersection but it is possible that one profile
|
770
|
+
## could be inside the other. Do fast bounding box checks
|
771
|
+
## before doing intersection-based checking.
|
772
|
+
if isinsidebbox(bbox1,bbox2[0]) and isinsidebbox(bbox1,bbox2[1]) \
|
773
|
+
and isinsideXY(g1,sample(g2,0.0)): # g2 is inside g1
|
774
|
+
## g2 is inside g1
|
775
|
+
if operation == 'union':
|
776
|
+
return g1
|
777
|
+
elif operation == 'intersection':
|
778
|
+
return g2
|
779
|
+
else: #difference, g2 is a hole in g1
|
780
|
+
return [g1,g2]
|
781
|
+
elif isinsidebbox(bbox2,bbox1[0]) and isinsidebbox(bbox2,bbox1[1]) \
|
782
|
+
and isinsideXY(g2,sample(g1,0.0)): # g1 is inside g2
|
783
|
+
## g1 is indside g2
|
784
|
+
if operation == 'union':
|
785
|
+
return g2
|
786
|
+
elif operation == 'intersection':
|
787
|
+
return g1
|
788
|
+
else: #difference, g2 has eaten g1
|
789
|
+
return []
|
790
|
+
else: # g1 and g2 are disjoint
|
791
|
+
if operation == 'union':
|
792
|
+
return [g1,g2]
|
793
|
+
elif operation == 'difference':
|
794
|
+
return g1
|
795
|
+
else: #intersection
|
796
|
+
return []
|
797
|
+
if len(inter[0]) == 1 and len(inter[1]) == 1:
|
798
|
+
## single point of intersection:
|
799
|
+
if operation == 'union':
|
800
|
+
return [g1, g2]
|
801
|
+
elif operation == 'difference':
|
802
|
+
return g1
|
803
|
+
else: #intersection
|
804
|
+
return []
|
805
|
+
## There are two or more points of intersection.
|
806
|
+
inter = rsort(inter)
|
807
|
+
#print("rsort: ",vstr(inter))
|
808
|
+
|
809
|
+
if len(inter[0]) %2 != 0:
|
810
|
+
print("WARNING: odd number of intersections (",len(inter[0]),", unpredictable behavior may result")
|
811
|
+
r = []
|
812
|
+
for i in range(1,len(inter[0])+1):
|
813
|
+
r += cmbin(g1,g2,[[inter[0][i-1],
|
814
|
+
inter[0][i%len(inter[0])]],
|
815
|
+
[inter[1][i-1],
|
816
|
+
inter[1][i%len(inter[1])]]])
|
817
|
+
return r
|