yapCAD 0.3.0__py2.py3-none-any.whl → 0.3.1__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/octtree.py ADDED
@@ -0,0 +1,627 @@
1
+ ## quadtree and octtree representations for yapCAD geometry
2
+ ## Born on 15 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
+ """quadtree and octtree representations for yapCAD geometry"""
26
+
27
+ from yapcad.geom import *
28
+
29
+ # determine if two bounding boxes overlap. This is a more challenging
30
+ # problem than it may appear at first glance. It is not enough to
31
+ # test the corner points of one box to see if they fall inside the
32
+ # other, as illustrated by the following cases:
33
+
34
+ # case 1: +--------------+
35
+ # inside | box1 |
36
+ # | +--------+ |
37
+ # | | box2 | |
38
+ # | +--------+ |
39
+ # +--------------+
40
+
41
+ # No corner points of box1 lie inside box2, no lines intersect.
42
+
43
+ # case 2: +------+
44
+ # cross | box1 |
45
+ # +-------+------+------+
46
+ # | box2 | | |
47
+ # +-------+------+------+
48
+ # | |
49
+ # +------+
50
+
51
+ # No corner point of either box lie inside the other, projected lines
52
+ # intersect.
53
+
54
+ def boxoverlap2(bbx1,bbx2,dim3=True):
55
+ """Determine if two bounding boxes overlap. if dim3==True, treat the
56
+ bounding boxes as 3D, otherwise treat them as co-planar 2D boxes.
57
+
58
+ First, check to see if the maximum coordinates of one box are
59
+ smaller than the minimum coordinates of the other, or vice versa.
60
+ If so, no overlap is possible; return False
61
+
62
+ if overlap is possible by test #1, check for the box-in-box
63
+ special case for each box. If so, return True
64
+
65
+ Finally, for the 2D case: determine if horizontal lines of box1
66
+ intersect with vertical lines of box2, and vice versa. If any
67
+ intersections found, return True, else return False
68
+
69
+ For the 3D case, project the boxes into the XY, YZ, and XZ planes,
70
+ and perform the 2D lines intersection check, as above. Return
71
+ True if and only if intersections are reported for each
72
+ projection, otherwise return False
73
+ """
74
+
75
+ # check minmax
76
+ if ((bbx1[1][0] < bbx2[0][0] and
77
+ bbx1[1][1] < bbx2[0][1] and
78
+ (not dim3 or bbx1[1][2] < bbx2[0][2])) or
79
+ (bbx2[1][0] < bbx1[0][0] and
80
+ bbx2[1][1] < bbx1[0][1] and
81
+ (not dim3 or bbx2[1][2] < bbx1[0][2]))):
82
+ return False # no overlap possible
83
+
84
+ # check for box-in-box
85
+ if ((bbx1[0][0] >= bbx2[0][0] and bbx1[1][0] <= bbx2[1][0] and
86
+ bbx1[0][1] >= bbx2[0][1] and bbx1[1][1] <= bbx2[1][1] and
87
+ (not dim3 or (bbx1[0][2] >= bbx2[0][2]
88
+ and bbx1[1][2] <= bbx2[1][2]))) or
89
+ (bbx2[0][0] >= bbx1[0][0] and bbx2[1][0] <= bbx1[1][0] and
90
+ bbx2[0][1] >= bbx1[0][1] and bbx2[1][1] <= bbx1[1][1] and
91
+ (not dim3 or (bbx2[0][2] >= bbx1[0][2]
92
+ and bbx2[1][2] <= bbx1[1][2])))):
93
+ return True
94
+
95
+ def int2D(bb1,bb2,plane='XY'):
96
+ """utility function for 2D box line intersection finding"""
97
+ i = 0
98
+ j = 0
99
+ if plane == 'XY':
100
+ i = 0
101
+ j = 1
102
+ elif plane == 'YZ':
103
+ i = 1
104
+ j = 2
105
+ elif plane == 'XZ':
106
+ i = 0
107
+ j = 2
108
+ else:
109
+ raise ValueError('bad plane in int2D')
110
+
111
+ def intHV(hln,vln):
112
+ """ do a horizontal and vertial line intersect? """
113
+ minx=hln[0][0]
114
+ maxx=hln[1][0]
115
+ if minx>maxx:
116
+ swap = minx ; minx = maxx; maxx = swap
117
+
118
+ if vln[0][0] < minx or vln[0][0] > maxx:
119
+ return False
120
+ miny=vln[0][1]
121
+ maxy=vln[1][1]
122
+ if miny > maxy:
123
+ swap = miny ; miny = maxy ; maxy = swap
124
+ if miny > hln[0][1] or maxy < hln[0][1]:
125
+ return False
126
+ return True
127
+
128
+ # check for projected box-in-box
129
+ if ((bb1[0][i] >= bb2[0][i] and bb1[1][i] <= bb2[1][i] and
130
+ bb1[0][j] >= bb2[0][j] and bb1[1][j] <= bb2[1][j])
131
+ or
132
+ (bb2[0][i] >= bb1[0][i] and bb2[1][i] <= bb1[1][i] and
133
+ bb2[0][j] >= bb1[0][j] and bb2[1][j] <= bb1[1][j])):
134
+ return True
135
+
136
+ # check for projected line intersections
137
+
138
+ len1 = bb1[1][i] - bb1[0][i] # length
139
+ wid1 = bb1[1][j] - bb1[0][j] # width
140
+ len2 = bb2[1][i] - bb2[0][i] # length
141
+ wid2 = bb2[1][j] - bb2[0][j] # width
142
+
143
+ p0 = point(bb1[0][i],bb1[0][j])
144
+ p1 = add(p0,point(len1,0))
145
+ p2 = add(p1,point(0,wid1))
146
+ p3 = add(p0,point(0,wid1))
147
+
148
+ p4 = point(bb2[0][i],bb2[0][j])
149
+ p5 = add(p4,point(len2,0))
150
+ p6 = add(p5,point(0,wid2))
151
+ p7 = add(p4,point(0,wid2))
152
+
153
+ box1 = [[p0,p1],
154
+ [p1,p2],
155
+ [p2,p3],
156
+ [p3,p0]]
157
+ box2 = [[p4,p5],
158
+ [p5,p6],
159
+ [p6,p7],
160
+ [p7,p4]]
161
+ if intHV(box1[0],box2[1]):
162
+ return True
163
+ if intHV(box1[0],box2[3]):
164
+ return True
165
+ if intHV(box1[2],box2[1]):
166
+ return True
167
+ if intHV(box1[2],box2[3]):
168
+ return True
169
+
170
+ if intHV(box2[0],box1[1]):
171
+ return True
172
+ if intHV(box2[0],box1[3]):
173
+ return True
174
+ if intHV(box2[2],box1[1]):
175
+ return True
176
+ if intHV(box2[2],box1[3]):
177
+ return True
178
+ # if lineLineIntersectXY(box1[0],box2[1]):
179
+ # return True
180
+ # if lineLineIntersectXY(box1[0],box2[3]):
181
+ # return True
182
+ # if lineLineIntersectXY(box1[2],box2[1]):
183
+ # return True
184
+ # if lineLineIntersectXY(box1[2],box2[3]):
185
+ # return True
186
+
187
+ # if lineLineIntersectXY(box2[0],box1[1]):
188
+ # return True
189
+ # if lineLineIntersectXY(box2[0],box1[3]):
190
+ # return True
191
+ # if lineLineIntersectXY(box2[2],box1[1]):
192
+ # return True
193
+ # if lineLineIntersectXY(box2[2],box1[3]):
194
+ # return True
195
+
196
+ # for l1 in box1:
197
+ # for l2 in box2:
198
+ # if lineLineIntersectXY(l1,l2):
199
+ # return True
200
+ return False
201
+
202
+ # do projeted box line intersection tests
203
+ return (int2D(bbx1,bbx2,'XY') and
204
+ (not dim3 or int2D(bbx1,bbx2,'YZ')) and
205
+ (not dim3 or int2D(bbx1,bbx2,'XZ')))
206
+
207
+
208
+
209
+ def boxoverlap(bbx1,bbx2,dim3=True):
210
+
211
+ """determine if two bounding boxes overlap"""
212
+ minx = bbx1[0][0]
213
+ miny = bbx1[0][1]
214
+ minz = bbx1[0][2]
215
+
216
+ len1 = bbx1[1][0] - bbx1[0][0] # length
217
+ wid1 = bbx1[1][1] - bbx1[0][1] # width
218
+ hei1 = bbx1[1][2] - bbx1[0][2] # height
219
+
220
+ p0=bbx1[0]
221
+ p1=add(p0,point(len1,0,0))
222
+ p2=add(p1,point(0,wid1,0))
223
+ p3=add(p0,point(0,wid1,0))
224
+
225
+ points = [p0,p1,p2,p3]
226
+
227
+ if not dim3:
228
+ for p in points:
229
+ if isinsidebbox2D(bbx2,p):
230
+ return True
231
+ return False
232
+
233
+ p4=add(p0,point(0,0,hei1))
234
+ p5=add(p4,point(len1,0,0))
235
+ p6=bbx1[1]
236
+ p7=add(p4,point(0,wid1,0))
237
+
238
+ points += [p4,p5,p6,p7]
239
+ #print("boxoverlap points; ",vstr(points))
240
+ #print("boxoverlap bbx2: ",vstr(bbx2))
241
+ for p in points:
242
+ if isinsidebbox(bbx2,p):
243
+ return True
244
+ return False
245
+
246
+ def bbox2oct(bbx,refbox,center):
247
+ """
248
+ Utility function to take a bounding box representation and assign
249
+ it to zero or more octants.
250
+
251
+ box2oct(bbx,refbox,center)
252
+
253
+ bbx: 3D bounding box to assign
254
+ refbox: reference 3D bounding box
255
+ center: center point for purposes of assignment
256
+
257
+ returns (potentially empty) list of octants, numbered 0 to 7
258
+ """
259
+ # print("bbox2oct :: bbx: ",vstr(bbx)," refbox: ",vstr(refbox)," center: ",vstr(center))
260
+ rlist = []
261
+ if not boxoverlap2(refbox,bbx):
262
+ return rlist # no overlap
263
+
264
+ if bbx[0][2] < center[2]:
265
+ rlist += bbox2quad(bbx,refbox,center)
266
+
267
+ if bbx[1][2] >= center[2]:
268
+ rlist += list(map(lambda x: x+4, bbox2quad(bbx,refbox,center)))
269
+
270
+ return rlist
271
+
272
+ def bbox2quad(bbx,refbox,center):
273
+ """Utility Function to take a bounding box representation and assign
274
+ it to zero or more quads
275
+
276
+ box2quad(bbx,refbox,center)
277
+
278
+ bbx: 2D bounding box to assign
279
+ refbox: reference 2D bounding box
280
+ center: center point for purposes of assignment
281
+
282
+ returns (potentially empty) list of quadrants, numbered 0 to 3
283
+ """
284
+ rlist = []
285
+ if not boxoverlap2(refbox,bbx,dim3=False):
286
+ return rlist # no overlap
287
+
288
+ if bbx[0][0] < center[0]:
289
+ if bbx[0][1] < center[1]:
290
+ rlist.append(2)
291
+ if bbx[1][1] >= center[1]:
292
+ rlist.append(1)
293
+ if bbx[1][0] >= center[0]:
294
+ if bbx[0][1] < center[1]:
295
+ rlist.append(3)
296
+ if bbx[1][1] >= center[1]:
297
+ rlist.append(0)
298
+ return rlist
299
+
300
+
301
+ def box2boxes(bbox,center,n,type='centersplit',elm=[]):
302
+ """Function to take a bounding box (2d or 3D) and return a quad- or
303
+ octtree decomposition of the box based on the value of center
304
+ point, the type of split, and (potentially) the list of bounding
305
+ boxes to be divvied up.
306
+ """
307
+ def boxmid(box):
308
+ p0 = box[0]
309
+ p1 = box[1]
310
+ return scale3(add(p0,p1),0.5)
311
+
312
+ # print("box2boxes :: bbox: ",vstr(bbox)," center: ",vstr(center))
313
+ if not isinsidebbox(bbox,center):
314
+ raise ValueError('center point does not lie inside the bounding box')
315
+ if type != 'centersplit':
316
+ raise NotImplementedError('we are only doing center splits for now')
317
+ if not n in (4,8):
318
+ raise ValueError('bad tree dimension')
319
+
320
+ cx = center[0]
321
+ cy = center[1]
322
+ cz = center[2]
323
+
324
+ maxx = bbox[1][0]
325
+ maxy = bbox[1][1]
326
+ maxz = bbox[1][2]
327
+ minx = bbox[0][0]
328
+ miny = bbox[0][1]
329
+ minz = bbox[0][2]
330
+
331
+ if (maxx-minx < epsilon or
332
+ maxy-miny < epsilon or
333
+ (n == 8 and maxz-minz < epsilon)) :
334
+ raise ValueError('zero dimension box')
335
+
336
+ if n == 4:
337
+ z0 = 0
338
+ z1 = 0
339
+ else:
340
+ z0 = minz
341
+ z1 = cz
342
+
343
+ box1 = [point(cx,cy,z0),
344
+ point(maxx,maxy,z1)]
345
+ box2 = [point(minx,cy,z0),
346
+ point(cx,maxy,z1)]
347
+ box3 = [point(minx,miny,z0),
348
+ point(cx,cy,z1)]
349
+ box4 = [point(cx,miny,z0),
350
+ point(maxx,cy,z1)]
351
+
352
+ r1 = [ [box1, boxmid(box1)],
353
+ [box2, boxmid(box2)],
354
+ [box3, boxmid(box3)],
355
+ [box4, boxmid(box4)] ]
356
+
357
+ if n == 4:
358
+ return r1
359
+ else:
360
+ z0 = cz
361
+ z1 = maxz
362
+
363
+ box5 = [point(cx,cy,z0),
364
+ point(maxx,maxy,z1)]
365
+ box6 = [point(minx,cy,z0),
366
+ point(cx,maxy,z1)]
367
+ box7 = [point(minx,miny,z0),
368
+ point(cx,cy,z1)]
369
+ box8 = [point(cx,miny,z0),
370
+ point(maxx,cy,z1)]
371
+
372
+ r2 = [ [box5, boxmid(box5)],
373
+ [box6, boxmid(box6)],
374
+ [box7, boxmid(box7)],
375
+ [box8, boxmid(box8)] ]
376
+
377
+ return r1 + r2
378
+
379
+ def bboxdim(box):
380
+ """ return length, width, and height of a bounding box"""
381
+
382
+ length = box[1][0] - box[0][0]
383
+ width = box[1][1] - box[0][1]
384
+ height = box[1][2] - box[0][2]
385
+ return [length, width, height]
386
+
387
+ def untag(tglist):
388
+ """remove the (optional) tags that can be associated with elements in
389
+ a geometry list."""
390
+ glist = []
391
+ for e in tglist:
392
+ if isinstance(e,tuple):
393
+ glist += [ e[0] ]
394
+ else:
395
+ glist += [ e ]
396
+ return glist
397
+
398
+ class NTree():
399
+
400
+ """Generalized n-tree representation for yapCAD geometry"""
401
+
402
+ def __init__(self,n=8,geom=None,center=None,
403
+ mindim=None,maxdepth=None):
404
+
405
+ if not n in [4,8]:
406
+ raise ValueError('only quad- or octtrees supported')
407
+
408
+ self.__n = n
409
+ self.__depth = 0
410
+ if mindim:
411
+ if isinstance(mindim,(int,float)) and mindim > epsilon:
412
+ self.__mindim = mindim
413
+ else:
414
+ raise ValueError('bad mindim value: '+str(mindim))
415
+ else:
416
+ self.__mindim = 1.0 # minimum dimension for tree element
417
+
418
+ if maxdepth:
419
+ if isinstance(maxdepth,int) and maxdepth > 0:
420
+ self.__maxdepth = maxdepth
421
+ else:
422
+ raise ValueError('bad max depth value: '+str(maxdepth))
423
+ elif self.__n == 4:
424
+ self.__maxdepth = 8
425
+ else: # n=8
426
+ self.__maxdepth = 7
427
+
428
+ self.__geom=[]
429
+ if geom:
430
+ ugeom = untag(geom)
431
+ if isgeomlist(ugeom):
432
+ self.__bbox= bbox(ugeom)
433
+ self.__geom= geom
434
+ else:
435
+ raise ValueError('geom must be valid geometry list, or None')
436
+
437
+ self.__center = None
438
+ if center:
439
+ if not ispoint(center):
440
+ raise ValueError('center must be a valid point')
441
+ self.__center = center
442
+ else:
443
+ if self.__geom != []:
444
+ self.updateCenter()
445
+
446
+ self.__tree = []
447
+ self.__update=True
448
+
449
+ def __repr__(self):
450
+ return 'NTree(n={},depth={},mindim={},\n geom={},\n tree={})'.format(self.__n,self.__depth,self.__mindim,vstr(self.__geom),vstr(self.__tree))
451
+
452
+ def addElement(self,element,tag=None):
453
+ """ add a geometry element to the collection, don't update
454
+ the tree -- yet """
455
+ if not isgeomlist(element):
456
+ raise ValueError('bad element passed to addElement')
457
+ if not tag:
458
+ gl = [ element ]
459
+ else:
460
+ gl = [ (element,tag) ]
461
+ self.__geom += gl
462
+
463
+ self.__update=True
464
+
465
+
466
+ def updateCenter(self,center=None):
467
+ """specify or compute new geometric center (or split poit) for tree,
468
+ flag tree for rebuilding.
469
+
470
+ """
471
+ if self.__geom == []: #nothing to do
472
+ return
473
+
474
+ if not center:
475
+ self.__center = scale3(add(self.__bbox[0],
476
+ self.__bbox[1]),0.5)
477
+ else:
478
+ if ispoint(center):
479
+ self.__center = center
480
+ else:
481
+ raise ValueError('bad center point passed to updateCenter')
482
+ self.__update = True
483
+
484
+
485
+ def updateTree(self):
486
+ """
487
+ build the tree from the current contents of the self.__geom list
488
+ """
489
+ if not self.__update or self.__geom == []:
490
+ # nothing to do
491
+ return
492
+ self.__update = False
493
+
494
+ # striptags for bbox
495
+ ugeom = untag(self.__geom)
496
+ self.__bbox = bbox(ugeom)
497
+ if not self.__center:
498
+ self.updateCenter()
499
+
500
+ bxlist = list(map(lambda x: bbox(x), ugeom))
501
+ bxidxlist = []
502
+ for i in range(len(bxlist)):
503
+ bxidxlist.append([i,bxlist[i]])
504
+
505
+ self.__elem_idx_bbox = bxidxlist
506
+
507
+ if not self.__center:
508
+ self.updateCenter()
509
+
510
+ # recursively build the tree
511
+ def recurse(box,center,elements,depth=0):
512
+ # print("recurse :: box: ",vstr(box)," center: ",vstr(center))
513
+ bbdim = bboxdim(box)
514
+ if depth > self.__depth:
515
+ self.__depth = depth
516
+
517
+ if elements==[] or elements==None:
518
+ return []
519
+ elif (len(elements) <= self.__n or
520
+ depth > self.__maxdepth or
521
+ bbdim[0] < self.__mindim or
522
+ bbdim[1] < self.__mindim or
523
+ (self.__n == 8 and bbdim[2] < self.__mindim)):
524
+
525
+ return [ 'e', box, center ] + list(map(lambda x:
526
+ x[0], elements))
527
+ else:
528
+ boxlist = ['b'] + box2boxes(box,center,self.__n)
529
+
530
+ # print("boxlist: ",vstr(boxlist))
531
+ # print("elements: ",vstr(elements))
532
+ for e in elements:
533
+ func = None
534
+ box = e[1]
535
+ if self.__n == 8:
536
+ func = bbox2oct
537
+ else:
538
+ func = bbox2quad
539
+ ind = func(box,box,center)
540
+ # print("ind: ",ind," box: ",box," box: ",box," center: ",center)
541
+ #print ("e[0]: ",e[0], " e[1]: ",vstr(e[1])," ind: ",ind)
542
+ # print ("box: ",vstr(box))
543
+ for j in ind:
544
+ boxlist[j+1].append(e)
545
+
546
+ for i in range(1,len(boxlist)):
547
+ boxlist[i] = recurse(boxlist[i][0],boxlist[i][1],
548
+ boxlist[i][2:],depth+1)
549
+ return boxlist
550
+
551
+ self.__tree = recurse(self.__bbox, self.__center,
552
+ bxidxlist)
553
+ # print("bxidxlist: ",vstr(bxidxlist))
554
+ # print("self.__tree",self.__tree)
555
+
556
+ return
557
+
558
+ @property
559
+ def depth(self):
560
+ return self.__depth
561
+
562
+ @depth.setter
563
+ def depth(self,n):
564
+ raise ValueError("can't set tree depth")
565
+
566
+ @property
567
+ def maxdepth(self):
568
+ return self.__maxdepth
569
+
570
+ @maxdepth.setter
571
+ def maxdepth(self,d):
572
+ if not isinstance(d,int) or d < 1:
573
+ raise ValueError('bad maxdepth value: '+str(d))
574
+ else:
575
+ self.__maxdepth = d
576
+
577
+ @property
578
+ def mindim(self):
579
+ return self.__mindim
580
+
581
+ @mindim.setter
582
+ def mindim(self,d):
583
+ if not isinstance(d,(float,int)) or d < epsilon:
584
+ raise ValueError('bad mindim value: '+str(d))
585
+ else:
586
+ self.__mindim = d
587
+
588
+ def getElements(self,bbox):
589
+ """return a list of geometry elements with bounding boxes that
590
+ overalp the provided bounding box, or the empty list if none.
591
+
592
+ """
593
+ if self.__update:
594
+ self.updateTree()
595
+
596
+ bxidxlist = self.__elem_idx_bbox
597
+
598
+ self.mxd = 0
599
+
600
+ def recurse(subtree,depth=0):
601
+ indices = []
602
+ if depth > self.mxd:
603
+ self.mxd = depth
604
+
605
+ if subtree == None or subtree == []:
606
+ return []
607
+ elif subtree[0] == 'e':
608
+ if boxoverlap2(subtree[1],bbox,dim3 = (self.__n==8)):
609
+ for ind in subtree[3:]:
610
+ box = bxidxlist[ind][1]
611
+ if boxoverlap2(bbox,box,dim3 = (self.__n==8)):
612
+ indices.append(ind)
613
+ else:
614
+ for boxlist in subtree[1:]:
615
+ if len(subtree) == 1:
616
+ print('subtree ',subtree)
617
+ if len(boxlist) > 0:
618
+ indices += recurse(boxlist,depth+1)
619
+ return indices
620
+
621
+ idx = set(recurse(self.__tree))
622
+
623
+ #print("unique indices: ",idx)
624
+
625
+ elements = list(map(lambda x: self.__geom[x], list(idx)))
626
+
627
+ return elements