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/geom3d.py CHANGED
@@ -3,60 +3,980 @@
3
3
  ## Richard W. DeVaul
4
4
 
5
5
  from yapcad.geom import *
6
+ from yapcad.geom_util import *
7
+ from yapcad.xform import *
8
+ from functools import reduce
9
+ from yapcad.triangulator import triangulate_polygon
6
10
 
7
- ## the geometric representations of point, line, arc, poly, and
8
- ## geomlist provided by yapcad.geom are suitable for representing
9
- ## zero- or one-dimensional figures embedded in a three-dimensional
10
- ## space. And while there is no requirement that the representations
11
- ## provided by yapcad.geom (such as an arc, line, or polyline) lie in
12
- ## the XY plane, many of the functions in yapcad.geom are explicitly
13
- ## intended to perform computational geometry operations on XY-planar
14
- ## entities.
15
-
16
- ## Further, while a closed figure described by yapcad.geom may
17
- ## implicitly bound a two-dimensional face, there is little support
18
- ## for working with two-dimensional surfaces provided by that module.
19
- ## There is no direct support of any kind for working with
20
- ## three-dimemnsional volumes in yapcad.geom.
21
-
22
- ## in this module, we define the concept of a parametric
23
- ## two-dimensional surface and three-dimensional volume, and provide
24
- ## implicit and explicit geometry operations for working with them.
25
-
26
- ## The goal of the geom3d.yapcad module is to allow for the
27
- ## construction of two-dimensinal surfaces and three-dimensional
28
- ## geometry for the purposes of modeling, computational geometry, and
29
- ## rendering. Specifically, we wish to support the following:
30
-
31
- ## (1) Support the implicit representation of three-dimensional
32
- ## geometry, and the performance of constructive solid geometry
33
- ## operations on this implicit geometry (union, intersection,
34
- ## difference) to produce more complex implicit three dimensional
35
- ## forms.
36
-
37
- ## (2) Support the implicit representation of two dimensional
38
- ## surfaces, such as a planar surface specified by three points, or
39
- ## the surface of a three-dimensional object like a sphere, and allow
40
- ## for computational geometry operations on these surfaces, such as
41
- ## intersection operations, to produce explicit one-dimensional
42
- ## objects, such as lines, arcs, etc.
43
-
44
- ## (3) support the conversion of an implicit two-dimensional surface
45
- ## to an explicit, teselated triangluar geometry that may be easily
46
- ## rendered using a conventional 3D graphics rendering pipline, such
47
- ## as OpenGL
48
-
49
- ## (4) Support for the conversion of implicit three-dimenaional
50
- ## constructive solid geometry into an explicit, contiguous closed
51
- ## surface representation using the marching cubes algortihm, or any
52
- ## other user-specified conversion algoritm, for the purposes of
53
- ## interactive 3D rendering and conversion to 3D CAM formats, such as
54
- ## STL.
55
-
56
- ## Structure for 2D/3D geometry:
57
-
58
- ## geom3d in tuple( <type>, <transform>, <primary representation>,
59
- ## <sampled representation>, <rendering hints> )
60
- ## where:
61
- ## type in tuple ('surface' | 'solid' | 'transform', <subtype>)
62
11
 
12
+ """
13
+ ==========================================================
14
+ geom3d -- functional 3D geometry representation for yapCAD
15
+ ==========================================================
16
+
17
+ The geometric representations of point, line, arc, poly, and
18
+ geomlist provided by ``yapcad.geom`` are suitable for representing
19
+ zero- or one-dimensional figures embedded in a three-dimensional
20
+ space. And while there is no requirement that the representations
21
+ provided by ``yapcad.geom`` (such as an arc, line, or polyline) lie in
22
+ the XY plane, many of the functions in yapcad.geom are explicitly
23
+ intended to perform computational geometry operations on XY-planar
24
+ entities.
25
+
26
+ Further, while a closed figure described by ``yapcad.geom`` may
27
+ implicitly bound a two-dimensional face, there is no direct support
28
+ for working with two-dimensional surfaces provided by that module.
29
+ There is no direct support of any kind for working with
30
+ three-dimemnsional volumes in ``yapcad.geom``.
31
+
32
+ In this module we specify representations for two-dimensional surfaces
33
+ and bounded three-dimensional volumes, and provide tools for working
34
+ with them in implicit, parametric form as well as in explicit,
35
+ triangulated form.
36
+
37
+ The goal of the ``geom3d.yapcad module`` is to allow for the
38
+ construction of two-dimensinal surfaces and three-dimensional
39
+ geometry for the purposes of modeling, computational geometry, and
40
+ rendering. Specifically, we wish to support the following:
41
+
42
+ (1) Support the implicit representation of three-dimensional
43
+ geometry, and the performance of constructive solid geometry
44
+ operations on this implicit geometry (union, intersection,
45
+ difference) to produce more complex implicit three dimensional
46
+ forms.
47
+
48
+ (2) Support the implicit representation of two dimensional
49
+ surfaces, such as a planar surface specified by three points, or
50
+ the surface of a three-dimensional object like a sphere, and allow
51
+ for computational geometry operations on these surfaces, such as
52
+ intersection operations, to produce explicit one-dimensional
53
+ objects, such as lines, arcs, etc.
54
+
55
+ (3) support the conversion of an implicit two-dimensional surface
56
+ to an explicit, teselated triangluar geometry that may be easily
57
+ rendered using a conventional 3D graphics rendering pipline, such
58
+ as OpenGL
59
+
60
+ (4) Support for the conversion of implicit three-dimenaional
61
+ constructive solid geometry into an explicit, contiguous closed
62
+ surface representation using the marching cubes algortihm, or any
63
+ other user-specified conversion algoritm, for the purposes of
64
+ interactive 3D rendering and conversion to 3D CAM formats, such as
65
+ STL.
66
+
67
+ Structures for 2D/3D geometry
68
+ =============================
69
+
70
+ surfaces
71
+ --------
72
+
73
+ ``surface = ['surface',vertices,normals,faces,boundary,holes]``, where:
74
+
75
+ ``vertices`` is a list of ``yapcad.geom`` points,
76
+
77
+ ``normals`` is a list of ``yapcad.geom`` direction vectors
78
+ of the same length as ``vertices``,
79
+
80
+ ``faces`` is the list of faces, which is to say lists
81
+ of three indices that refer to the vertices of the triangle
82
+ that represents each face,
83
+
84
+ ``boundary`` is a list of indices for the vertices that form
85
+ the outer perimeter of the surface, or [] if the surface
86
+ has no boundary, such as that of a torus or sphere
87
+
88
+ ``holes`` is a (potentially zero-length) list of lists of
89
+ holes, each of which is a non-zero list of three or more
90
+ indices of vertices that form the perimeter of any holes in
91
+ the surface.
92
+
93
+ A surface has an inside and an outside. To deterine which side of a
94
+ surface a point lies on, find the closest face and determine if the
95
+ point lies on the positive or negative side of the face.
96
+
97
+ Solids
98
+ ------
99
+
100
+ To represent a completely bounded space (a space for which any point
101
+ can be unambiguously determined to be inside or outside) there is the
102
+ ``solid`` representation. A solid is composed of zero or more
103
+ surfaces, and may be completely empty, as empty solids are legal
104
+ products of constructive solid geometry operations like intersection
105
+ and difference.
106
+
107
+ The gurantee, which is not enforced by the representation, is that for
108
+ any point inside the bounding box of the solid, and for any point
109
+ chosen outside the bounding box of the solid, a line drawn between the
110
+ two will have either even (or zero), or odd point intersections (as
111
+ opposed to tangent intersections), regardless of the choice of the
112
+ outside-the-bounding-box point.
113
+
114
+ Solids have optional associated metadata about the material properties
115
+ of the solid and about how it was constructed.
116
+
117
+ For example, material properties might include OpenGL-type rendering
118
+ data, mechanical properties, or a reference to a material dictionary
119
+ that includes both.
120
+
121
+ Construction meta-data might include the model-file and material-file
122
+ name from which the geometry was loaded, the polygon from which the
123
+ solid was extruded (and associated extrusion parameters), or the
124
+ function call with parameters for algorithmically-generated geometry.
125
+
126
+ ``solid = ['solid', surfaces, material, construction ]``, where:
127
+
128
+ ``surfaces`` is a list of surfaces with contiguous boundaries
129
+ that completely encloses an interior space,
130
+
131
+ ``material`` is a list of domain-specific representation of
132
+ the material properties of the solid, which may be empty.
133
+ This information may be used for rendering or simulating
134
+ the properties of the solid.
135
+
136
+ ``construction`` is a list that contains information about
137
+ how the solid was constructed, and may be empty
138
+
139
+
140
+ Assembly
141
+ --------
142
+
143
+ Assemblies are lists of elements, which are solids or assemblies, in
144
+ which each list has an associated geometric transformation.
145
+
146
+ ``assembly = ['assembly', transform, elementlist]``, where:
147
+
148
+ ``transform = [xformF, xformR]``, a pair of forward and inverse
149
+ transformation matricies such that ``xformF`` * ``xformR`` =
150
+ the identity matrix.
151
+
152
+ ``elementlist = [element0, elememnt1, ... ]``, in which each
153
+ list element is either a valid ``solid`` or ``assembly``.
154
+
155
+ """
156
+
157
+ def signedPlaneDistance(p,p0,n):
158
+ """Given a point on the plane ``p0``, and the unit-length plane
159
+ normal ``n``, determine the signed distance to the plane.
160
+ """
161
+ return dot(sub(p,p0),n)
162
+
163
+ def tri2p0n(face,basis=False):
164
+ """Given ``face``, a non-degenrate poly of length 3, return the center
165
+ point and normal of the face, otherwise known as the Hessian
166
+ Normal Form: https://mathworld.wolfram.com/HessianNormalForm.html
167
+
168
+ In addition, if ``basis==True``, calculate an orthnormal basis
169
+ vectors implied by the triangle, with the x' vector aligned with
170
+ the p1-p2 edge, the z' vector as the nornal vector, and the y'
171
+ vector as -1 * x' x z', and return the transformation matrix that
172
+ will transform a point in world coordinates to a point in the
173
+ orthonormal coordinate system with the origin at the center
174
+ point. Return that transformation matrix and its inverse.
175
+
176
+ """
177
+ # import pdb ; pdb.set_trace()
178
+ p1=face[0]
179
+ p2=face[1]
180
+ p3=face[2]
181
+ p0 = scale3(add(p1,add(p2,p3)),1.0/3.0)
182
+ v1=sub(p2,p1)
183
+ v2=sub(p3,p2)
184
+ c= cross(v1,v2)
185
+ m = mag(c)
186
+ if m < epsilon:
187
+ raise ValueError('degenerate face in tri2p0n')
188
+ n= scale3(c,1.0/m)
189
+ n[3] = 0.0 #direction vectors lie in the w=0 hyperplane
190
+ if not basis:
191
+ return [p0,n]
192
+ else:
193
+ # compute orthonormal basis vectors
194
+ x=scale3(v1,1.0/mag(v1))
195
+ z=n
196
+ y=scale3(cross(x,z),-1)
197
+ # direction vectors lie in the w=0 hyperplane
198
+ x[3] = y[3] = z[3] = 0.0
199
+ # build the rotation matrix
200
+ T = [x,
201
+ y,
202
+ z,
203
+ [0,0,0,1]]
204
+ rm = Matrix(T)
205
+ # build translation matrix
206
+ tm = Translation(scale3(p0,-1))
207
+ # make composed matrix
208
+ forward = rm.mul(tm)
209
+ # make inverse matrix
210
+ rm.trans=True
211
+ inverse = Translation(p0).mul(rm)
212
+ return [p0,n,forward,inverse]
213
+
214
+
215
+ def signedFaceDistance(p,face):
216
+ """given a test point ``p`` and a three-point face ``face``, determine
217
+ the minimum signed distance of the test point from the face. Any
218
+ point lying in the positive normal direction or on the surface of
219
+ the plane will result in a zero or positive distace. Any point
220
+ lying in the negative normal direction from the surface of the
221
+ plane will result in a negative distance.
222
+
223
+ """
224
+ p0,n = tuple(tri2p0n(face))
225
+ d = sub(p,face[0])
226
+ m = -dot(d,n) # negative of distance of p from plane
227
+ a = add(p,scale3(n,m)) # projection of point p into plane
228
+
229
+ # create a coordinate system based on the first face edge and the
230
+ # plane normal
231
+ v1 = sub(face[1],face[0])
232
+ v2 = sub(face[2],face[0])
233
+ vx = scale3(v1,1.0/mag(v1))
234
+ vy = scale3(cross(vx,n),-1)
235
+ vz = n
236
+ p1=[0,0,0,1]
237
+ p2=[mag(v1),0,0,1]
238
+ p3=[dot(v2,vx),dot(v2,vy),0,1]
239
+ aa= sub(a,face[0])
240
+ aa=[dot(aa,vx),dot(aa,vy),0,1]
241
+
242
+ #barycentric coordinates for derermining if projected point falls
243
+ #inside face
244
+ lam1,lam2,lam3 = tuple(barycentricXY(aa,p1,p2,p3))
245
+ inside = ( (lam1 >= 0.0 and lam1 <= 1.0) and
246
+ (lam2 >= 0.0 and lam2 <= 1.0) and
247
+ (lam3 >= 0.0 and lam3 <= 1.0) )
248
+
249
+ ind = 0 # in-plane distance is zero if projected point inside
250
+ # triangle
251
+ if not inside:
252
+ d1 = linePointXY([p1,p2],aa,distance=True)
253
+ d2 = linePointXY([p2,p3],aa,distance=True)
254
+ d3 = linePointXY([p3,p1],aa,distance=True)
255
+ ind = min(d1,d2,d3) #in-plane distance is smallest distance from each edge
256
+
257
+ if close(m,0): # point lies in plane
258
+ return ind
259
+ else:
260
+ dist = sqrt(m*m+ind*ind) # total distance is hypotenuse of
261
+ # right-triangle of in-plane and
262
+ # out-plane distance
263
+ return copysign(dist,-1*m)
264
+
265
+ def linePlaneIntersect(lne,plane="xy",inside=True):
266
+ """Function to calculate the intersection of a line and a plane.
267
+
268
+ ``line`` is specified in the usual way as two points. ``plane``
269
+ is either specified symbolicly as one of ``["xy","yz","xz"]``, a
270
+ list of three points, or a planar coordinate system in the form of
271
+ ``[p0,n,forward,reverse]``, where ``p0`` specifies the origin in
272
+ world coordinates, ``n`` is the normal (equivalent to the ``z``
273
+ vector), and ``forward`` and ``reverse`` are transformation
274
+ matricies that map from world into local and local into world
275
+ coordinates respectively.
276
+
277
+ Returns ``False`` if the line and plane do not intersect, or if
278
+ ``inside==True`` and the point of intersection is outside the line
279
+ interval. Returns the point of intersection otherwise.
280
+
281
+ NOTE: if plane is specified as three points, then setting
282
+ ``inside=True`` will also force a check to see if the intersection
283
+ point falls within the specified triangle.
284
+
285
+ """
286
+
287
+ def lineCardinalPlaneIntersect(lne,idx,inside=True):
288
+ if close(lne[0][idx]-lne[1][idx],0.0): #degenerate
289
+ return False
290
+ # (1-u)*l[0][idx] + u*l[1][idx] = 0.0
291
+ # u*(l[1][idx]-l[0][idx]) = l[0][idx]
292
+ # u = l[0][idx]/(l[1][idx]-l[0][idx])
293
+ u = lne[0][idx]/(lne[0][idx]-lne[1][idx])
294
+ if inside and (u < 0.0 or u > 1.0):
295
+ return False
296
+ else:
297
+ return sampleline(lne,u)
298
+
299
+ # is the plane specified symbolicaly?
300
+ trangle = False
301
+ idx = -1
302
+ if plane=="xy":
303
+ idx=2
304
+ elif plane=="yz":
305
+ idx=0
306
+ elif plane=="xz":
307
+ idx=1
308
+
309
+ if idx > -1:
310
+ return lineCardinalPlaneIntersect(lne,idx,inside)
311
+ else:
312
+ if istriangle(plane):
313
+ triangle = True
314
+ tri = plane
315
+ plane = tri2p0n(plane,basis=True)
316
+ tri2 = [ plane[2].mul(tri[0]),
317
+ plane[2].mul(tri[1]),
318
+ plane[2].mul(tri[2]) ]
319
+ else:
320
+ raise ValueError('non-plane passed to linePlaneIntersect')
321
+ # otherwise assume that plane is a valid planar basis
322
+
323
+ # transform into basis with plane at z=0
324
+ l2 = [plane[2].mul(lne[0]),plane[2].mul(lne[1])]
325
+ p = lineCardinalPlaneIntersect(l2,2,inside)
326
+ if not p:
327
+ return False
328
+ else:
329
+ if triangle and not isInsideTriangleXY(p,tri2):
330
+ return False
331
+ return plane[3].mul(p)
332
+
333
+
334
+ def triTriIntersect(t1,t2,inside=True,inPlane=False,basis=None):
335
+
336
+ """Function to compute the intersection of two triangles. Returns
337
+ ``False`` if no intersection, a line (a list of two points) if the
338
+ planes do not overlap and there is a linear intersection, and a
339
+ polygon (list of three or more points) if the triangles are
340
+ co-planar and overlap.
341
+
342
+ If ``inside == True`` (default) return line-segment or poly
343
+ intersection that falls inside both bounded triangles, otherwise
344
+ return a line segment that lies on the infinite linear
345
+ intersection of two planes, or False if planes are degenerate.
346
+
347
+ If ``inPlane==True``, return the intersection as a poly in the
348
+ planar coordinate system implied by ``t1``, or in the planar
349
+ coordinate system specified by ``basis``
350
+
351
+ If ``basis`` is not ``False``, it should be planar coordinate
352
+ system in the form of ``[p0,n,forward,reverse]``, where ``p0``
353
+ specifies the origin in world coordinates, ``n`` is the normal
354
+ (equivalent to the ``z`` vector), and ``forward`` and ``reverse``
355
+ are transformation matricies that map from world into local and
356
+ local into world coordinates respectively.
357
+
358
+ NOTE: when ``basis`` is True and ``inPlane`` is False, it is
359
+ assumed that ``basis`` is a planar basis computed by tri2p0n
360
+ coplanar with ``t1``.
361
+
362
+ """
363
+ if not basis:
364
+ #create basis from t1
365
+ basis = tri2p0n(t1,basis=True)
366
+ p01,n1,tfor,tinv = tuple(basis)
367
+
368
+ p02,n2 = tuple(tri2p0n(t2,basis=False))
369
+
370
+ #transform both triangles into new coordinate system
371
+ t1p = list(map(lambda x: tfor.mul(x),t1))
372
+ t2p = list(map(lambda x: tfor.mul(x),t2))
373
+
374
+ # check for coplanar case
375
+ if (abs(t2p[0][2]) <= epsilon and abs(t2p[1][2]) <= epsilon
376
+ and abs(t2p[2][2]) <= epsilon):
377
+ if not inside:
378
+ if inPlane:
379
+ return t2p
380
+ else:
381
+ return t2
382
+ else: # return poly that is in-plane intersection
383
+ intr = combineglist(t1p,t2p,'intersection')
384
+ if len(intr) < 1: #no intersection
385
+ return False
386
+ else:
387
+ if inPlane:
388
+ return intr
389
+ else:
390
+ return transform(intr,tinv)
391
+ # not coplanar, check to see if planes are parallel
392
+ if vclose(n1,n2):
393
+ return False #yep, degenerate
394
+
395
+ if inside:
396
+ # check to see if t2p lies entirely above or below the z=0 plane
397
+ if ((t2p[0][2] > epsilon and t2p[1][2] > epsilon and t2p[2][2] > epsilon)
398
+ or
399
+ (t2p[0][2] < -epsilon and t2p[1][2] < -epsilon and
400
+ t2p[2][2] < -epsilon)):
401
+ return False
402
+ # linear intersection. Figure out which two of three lines
403
+ # cross the z=0 plane
404
+
405
+ # this should work whether or not the intersection is
406
+ # inside t2.
407
+ ip1 = linePlaneIntersect([t2p[0],t2p[1]],"xy",False)
408
+ ip2 = linePlaneIntersect([t2p[1],t2p[2]],"xy",False)
409
+ ip3 = linePlaneIntersect([t2p[2],t2p[0]],"xy",False)
410
+
411
+ a=ip1
412
+ b=ip2
413
+ if not a:
414
+ a=ip3
415
+ if not b:
416
+ b=ip3
417
+ if inPlane:
418
+ return [a,b]
419
+ else:
420
+ return [tinv.mul(a),tinv.mul(b)]
421
+
422
+ def surface(*args):
423
+ """given a surface or a list of surface parameters as arguments,
424
+ return a conforming surface representation. Checks arguments
425
+ for data-type correctness.
426
+
427
+ """
428
+ if args==[]:
429
+ # empty surface
430
+ return ['surface',[],[],[],[],[] ]
431
+ if len(args) == 1:
432
+ # one argument, produce a deep copy of surface (it that is
433
+ # what it is)
434
+ if issurface(args[0],fast=False):
435
+ return deepcopy(args[0])
436
+ if len(args) >= 3 and len(args) <= 6:
437
+ vrts = args[0]
438
+ nrms = args[1]
439
+ facs = args[2]
440
+ bndr = []
441
+ hle = []
442
+ metadata = None
443
+
444
+ if not (isinstance(vrts,list) and isinstance(nrms,list)
445
+ and isinstance(facs,list) and len(vrts) == len(nrms)):
446
+ raise ValueError('bad arguments to surface')
447
+
448
+ extras = list(args[3:])
449
+ for item in extras:
450
+ if isinstance(item, dict):
451
+ if metadata is not None:
452
+ raise ValueError('multiple metadata dictionaries passed to surface')
453
+ metadata = item
454
+ elif isinstance(item, list):
455
+ if not bndr:
456
+ bndr = item
457
+ elif not hle:
458
+ hle = item
459
+ else:
460
+ raise ValueError('too many list arguments passed to surface')
461
+ else:
462
+ raise ValueError('bad arguments to surface')
463
+
464
+ surf = ['surface',vrts,nrms,facs,bndr,hle]
465
+ if metadata is not None:
466
+ if not isinstance(metadata, dict):
467
+ raise ValueError('surface metadata must be a dict')
468
+ surf.append(metadata)
469
+ if issurface(surf,fast=False):
470
+ return surf
471
+ raise ValueError('bad arguments to surface')
472
+
473
+ def surfacebbox(s):
474
+ """return bounding box for surface"""
475
+ if not issurface(s):
476
+ raise ValueError('bad surface passed to surfacebbox')
477
+ return polybbox(s[1])
478
+
479
+ def issurface(s,fast=True):
480
+ """
481
+ Check to see if ``s`` is a valid surface.
482
+ """
483
+ def filterInds(inds,verts):
484
+ l = len(verts)
485
+ if l < 3:
486
+ return False
487
+ return (len(list(filter(lambda x: not (isinstance(x,int) or
488
+ x < 0 or x >= l),
489
+ inds))) == 0)
490
+
491
+ if not isinstance(s,list) or s[0] != 'surface' or len(s) not in (6,7):
492
+ return False
493
+ if fast:
494
+ return True
495
+ else:
496
+ verts=s[1]
497
+ norms=s[2]
498
+ faces=s[3]
499
+ boundary= s[4]
500
+ holes= s[5]
501
+ metadata = s[6] if len(s) == 7 else None
502
+ if (not ispoly(verts) or
503
+ not isdirectlist(norms) or
504
+ len(verts) != len(norms)):
505
+ return False
506
+ l = len(verts)
507
+ if (len(list(filter(lambda x: not len(x) == 3, faces))) > 0):
508
+ return False
509
+ if not filterInds(reduce( (lambda x,y: x + y),faces),verts):
510
+ return False
511
+ if not filterInds(boundary,verts):
512
+ return False
513
+ if len(holes)>0:
514
+ for h in holes:
515
+ if not filterInds(h,verts):
516
+ return False
517
+ if metadata is not None and not isinstance(metadata, dict):
518
+ return False
519
+ return True
520
+
521
+ ## save pointers to the yapcad.geom transformation functions
522
+ geom_rotate = rotate
523
+ geom_translate = translate
524
+ geom_scale = scale
525
+ geom_mirror = mirror
526
+
527
+ def rotatesurface(s,ang,cent=point(0,0,0),axis=point(0,0,1.0),mat=False):
528
+ """ return a rotated copy of the surface"""
529
+ if close(ang,0.0):
530
+ return deepcopy(s)
531
+ if not mat: # if matrix isn't pre-specified, calculate it
532
+ if vclose(cent,point(0,0,0)):
533
+ mat = xform.Rotation(axis,ang)
534
+ else:
535
+ mat = xform.Translation(cent)
536
+ mat = mat.mul(xform.Rotation(axis,ang))
537
+ mat = mat.mul(xform.Translation(cent,inverse=True))
538
+ s2 = deepcopy(s)
539
+ #import pdb ; pdb.set_trace()
540
+ s2[1] = geom_rotate(s2[1],ang,cent,axis,mat)
541
+ s2[2] = geom_rotate(s2[2],ang,cent,axis,mat)
542
+ return s2
543
+
544
+ def translatesurface(s,delta):
545
+ """ return a translated copy of the surface"""
546
+ if vclose(delta,point(0,0,0)):
547
+ return deepcopy(s)
548
+ s2 = deepcopy(s)
549
+ for i in range(len(s2[1])):
550
+ s2[1][i] = add(s2[1][i],delta)
551
+ return s2
552
+
553
+ def mirrorsurface(s,plane):
554
+ """return a mirrored version of a surface. Currently, the following
555
+ values of "plane" are allowed: 'xz', 'yz', xy'. Generalized
556
+ arbitrary reflection plane specification will be added in the
557
+ future.
558
+
559
+ Note that this is a full surface geometry reflection, and not
560
+ simply a normal reverser.
561
+ """
562
+ s2 = deepcopy(s)
563
+ s2[1] = mirror(s[1],plane)
564
+ s2[2] = mirror(s[2],plane)
565
+ s2[3] = list(map(lambda x: [x[0],x[2],x[1]],s2[3]))
566
+ return s2
567
+
568
+ def reversesurface(s):
569
+ """ return a nomal-reversed copy of the surface """
570
+ s2 = deepcopy(s)
571
+ s2[2] = list(map(lambda x: scale4(x,-1.0),s2[2]))
572
+ s2[3] = list(map(lambda x: [x[0],x[2],x[1]],s2[3]))
573
+ return s2
574
+
575
+ def solid(*args):
576
+ """given a solid or a list of solid parameters as arguments,
577
+ return a conforming solid representation. Checks arguments
578
+ for data-type correctness.
579
+
580
+ """
581
+ if args==[] or (len(args) == 1 and args[0] == []):
582
+ # empty solid, which is legal because we must support
583
+ # empty results of CSG operations, etc.
584
+ return ['solid',[],[],[] ]
585
+
586
+ # check for "copy constructor" case
587
+ if len(args) == 1 and issolid(args[0],fast=False):
588
+ # one argument, it's a solid. produce a deep copy of solid
589
+ return deepcopy(args[0])
590
+
591
+ # OK, step through arguments
592
+ if len(args) >= 1 and len(args) <= 4:
593
+ if not isinstance(args[0],list):
594
+ raise ValueError('bad arguments to solid')
595
+ for srf in args[0]:
596
+ if not issurface(srf):
597
+ raise ValueError('bad arguments to solid')
598
+
599
+ surfaces = args[0]
600
+ material = []
601
+ construction = []
602
+ metadata = None
603
+
604
+ for item in args[1:]:
605
+ if isinstance(item, dict):
606
+ if metadata is not None:
607
+ raise ValueError('multiple metadata dictionaries passed to solid')
608
+ metadata = item
609
+ elif isinstance(item, list):
610
+ if material == []:
611
+ material = item
612
+ elif construction == []:
613
+ construction = item
614
+ else:
615
+ raise ValueError('too many list arguments passed to solid')
616
+ else:
617
+ raise ValueError('bad arguments to solid')
618
+
619
+ sld = ['solid', surfaces, material, construction]
620
+ if metadata is not None:
621
+ sld.append(metadata)
622
+ return sld
623
+
624
+ raise ValueError('bad arguments to solid')
625
+
626
+
627
+
628
+ def issolid(s,fast=True):
629
+
630
+ """
631
+ Check to see if ``s`` is a solid. NOTE: this function only determines
632
+ if th data structure is correct, it does not verify that the collection
633
+ of surfaces completely bounds a volume of space without holes
634
+ """
635
+
636
+ if not isinstance(s,list) or s[0] != 'solid' or len(s) not in (4,5):
637
+ return False
638
+ if fast:
639
+ return True
640
+ else:
641
+
642
+ for surface in s[1]:
643
+ if not issurface(surface,fast=fast):
644
+ return False
645
+ if not (isinstance(s[2],list) and isinstance(s[3],list)):
646
+ return False
647
+ if len(s) == 5 and not isinstance(s[4], dict):
648
+ return False
649
+ return True
650
+
651
+ def solidbbox(sld):
652
+ if not issolid(sld):
653
+ raise ValueError('bad argument to solidbbox')
654
+
655
+ box = []
656
+ for s in sld[1]:
657
+ box = surfacebbox(s + box)
658
+
659
+ return box
660
+
661
+ def translatesolid(x,delta):
662
+ if not issolid(x):
663
+ raise ValueError('bad solid passed to translatesolid')
664
+ s2 = deepcopy(x)
665
+ surfs = []
666
+ for s in x[1]:
667
+ surfs.append(translatesurface(s,delta))
668
+ s2[1] = surfs
669
+ return s2
670
+
671
+ def rotatesolid(x,ang,cent=point(0,0,0),axis=point(0,0,1.0),mat=False):
672
+ if not issolid(x):
673
+ raise ValueError('bad solid passed to rotatesolid')
674
+ s2 = deepcopy(x)
675
+ surfs=[]
676
+ for s in x[1]:
677
+ surfs.append(rotatesurface(s,ang,cent=cent,axis=axis,mat=mat))
678
+ s2[1] = surfs
679
+ return s2
680
+
681
+ def mirrorsolid(x,plane,preserveNormal=True):
682
+ if not issolid(x):
683
+ raise ValueError('bad solid passed to mirrorsolid')
684
+ s2 = deepcopy(x)
685
+ surfs=[]
686
+ for s in x[1]:
687
+ surf = mirrorsurface(s,plane)
688
+ if preserveNormal and False:
689
+ surf = reversesurface(surf)
690
+ surfs.append(surf)
691
+ s2[1] = surfs
692
+ return s2
693
+
694
+ def normfunc(tri):
695
+ """
696
+ utility funtion to compute normals for a flat facet triangle
697
+ """
698
+ v1 = sub(tri[1],tri[0])
699
+ v2 = sub(tri[2],tri[1])
700
+ d = cross(v1,v2)
701
+ n = scale3(d,1.0/mag(d))
702
+ n[3] = 0.0 # direction vectors lie in the w=0 hyperplane
703
+ return n,n,n
704
+
705
+ def addTri2Surface(tri,s,check=False,nfunc=normfunc):
706
+ """
707
+ Add triangle ``tri`` (a list of three points) to a surface ``s``,
708
+ returning the updated surface. *NOTE:* There is no enforcement of
709
+ contiguousness or coplainarity -- this function will add any triangle.
710
+ """
711
+
712
+ def addVert(p,n,vrts,nrms):
713
+ for i in range(len(vrts)):
714
+ if vclose(p,vrts[i]):
715
+ return i,vrts,nrms
716
+ vrts.append(p)
717
+ nrms.append(n)
718
+ return len(vrts)-1,vrts,nrms
719
+
720
+ if check and (not issurface(s) or not istriangle(tri)):
721
+ raise ValueError(f'bad arguments to addTri2Surface({tri},{s})')
722
+
723
+ vrts = s[1]
724
+ nrms = s[2]
725
+ faces = s[3]
726
+ boundary = s[4]
727
+ holes = s[5]
728
+
729
+ n1,n2,n3 = nfunc(tri)
730
+ i1,vrts,nrms = addVert(tri[0],n1,vrts,nrms)
731
+ i2,vrts,nrms = addVert(tri[1],n2,vrts,nrms)
732
+ i3,vrts,nrms = addVert(tri[2],n3,vrts,nrms)
733
+ faces.append([i1,i2,i3])
734
+
735
+ return ['surface',vrts,nrms,faces,boundary,holes]
736
+
737
+
738
+ def surfacearea(surf):
739
+ """
740
+ given a surface, return the surface area
741
+ """
742
+ area = 0.0
743
+ vertices = surf[1]
744
+ faces= surf[3]
745
+ for f in faces:
746
+ area += triarea(vertices[f[0]],
747
+ vertices[f[1]],
748
+ vertices[f[2]])
749
+
750
+ return area
751
+
752
+ def surf2lines(surf):
753
+ """
754
+ convert a surface representation to a non-redundant set of lines
755
+ for line-based rendering purposes
756
+ """
757
+
758
+ drawn = []
759
+
760
+ verts = surf[1]
761
+ norms = surf[2]
762
+ faces = surf[3]
763
+
764
+ lines = []
765
+
766
+ def inds2key(i1,i2):
767
+ if i1 <= i2:
768
+ return f"{i1}-{i2}"
769
+ else:
770
+ return f"{i2}-{i1}"
771
+
772
+ def addLine(i1,i2,lines):
773
+ key = inds2key(i1,i2)
774
+ if not key in drawn:
775
+ lines.append(line(verts[i1],
776
+ verts[i2]))
777
+ drawn.append(key)
778
+ return lines
779
+
780
+ for f in faces:
781
+ lines = addLine(f[0],f[1],lines)
782
+ lines = addLine(f[1],f[2],lines)
783
+ lines = addLine(f[2],f[0],lines)
784
+
785
+ return lines
786
+
787
+ def poly2surface(ply,holepolys=[],minlen=0.5,minarea=0.0001,
788
+ checkclosed=False,basis=None):
789
+
790
+ """Given ``ply``, a coplanar polygon, return the triangulated surface
791
+ representation of that polygon and its boundary. If ``holepolys``
792
+ is not the empty list, treat each polygon in that list as a hole
793
+ in ``ply``. If ``checkclosed`` is true, make sure ``ply`` and all
794
+ members of ``holepolys`` are a vaid, closed, coplanar polygons.
795
+ if ``box`` exists, use it as the bounding box.
796
+
797
+ if ``basis`` exists, use it as the planar coordinate basis to
798
+ transform the poly into the z=0 plane.
799
+
800
+ Returns surface and boundary
801
+
802
+ """
803
+
804
+ if len(ply) < 3:
805
+ raise ValueError(f'poly must be at least length 3, got {len(ply)}')
806
+
807
+ if not basis:
808
+ v0 = sub(ply[1],ply[0])
809
+ v1 = None
810
+ for i in range(2,len(ply)):
811
+ v1 = sub(ply[i],ply[1])
812
+ if mag(cross(v0,v1)) > epsilon:
813
+ break
814
+ if not v1:
815
+ raise ValueError(f'degenerate poly passed to poly2surface')
816
+ basis = tri2p0n([ply[0],ply[1],ply[i]],basis=True)
817
+
818
+ ply2 = list(map(lambda x: basis[2].mul(x),ply))
819
+ holes2 = []
820
+ for hole in holepolys:
821
+ holes2.append(list(map(lambda x: basis[2].mul(x), hole)))
822
+
823
+ surf,bnd = poly2surfaceXY(ply2,holes2,minlen,minarea,checkclosed)
824
+
825
+ verts2 = list(map(lambda x: basis[3].mul(x),surf[1]))
826
+ norm2 = list(map(lambda x: basis[3].mul([x[0],x[1],x[2],0]),surf[2]))
827
+ bnd2 = list(map(lambda x: basis[3].mul(x),bnd))
828
+
829
+ surf[1]=verts2
830
+ surf[2]=norm2
831
+
832
+ return surf,bnd2
833
+
834
+ def poly2surfaceXY(ply,holepolys=[],minlen=0.5,minarea=0.0001,
835
+ checkclosed=False,box=None):
836
+ """Given ``ply``, return a triangulated XY surface (holes supported)."""
837
+
838
+ if checkclosed:
839
+ polys = holepolys + [ply]
840
+ if not isgeomlistXYPlanar(polys):
841
+ raise ValueError('non-XY-coplanar arguments')
842
+ for p in polys:
843
+ if not ispolygonXY(p):
844
+ raise ValueError(f'{p} is not a closed polygon')
845
+
846
+ if not box:
847
+ box = bbox(ply)
848
+
849
+ def _normalize_loop(poly):
850
+ pts = [point(p) for p in poly]
851
+ if pts and dist(pts[0], pts[-1]) <= epsilon:
852
+ pts = pts[:-1]
853
+ return pts
854
+
855
+ outer_loop = _normalize_loop(ply)
856
+ if len(outer_loop) < 3:
857
+ raise ValueError('degenerate polygon passed to poly2surfaceXY')
858
+
859
+ hole_loops = [_normalize_loop(loop) for loop in holepolys]
860
+
861
+ triangles = triangulate_polygon([(p[0], p[1]) for p in outer_loop],
862
+ [[(q[0], q[1]) for q in loop]
863
+ for loop in hole_loops])
864
+
865
+ def makeboundary(poly,vertices,normals):
866
+ bndry = []
867
+ i = len(vertices)
868
+ for p in poly:
869
+ vertices.append(point(p))
870
+ normals.append([0,0,1,0])
871
+ bndry.append(i)
872
+ i+=1
873
+ return bndry,vertices,normals
874
+
875
+ vrts=[]
876
+ nrms=[]
877
+ faces=[]
878
+ boundary=[]
879
+ holes=[]
880
+
881
+ boundary,vrts,nrms = makeboundary(outer_loop,vrts,nrms)
882
+ for loop in hole_loops:
883
+ hole,vrts,nrms = makeboundary(loop,vrts,nrms)
884
+ holes.append(hole)
885
+
886
+ surf=['surface',vrts,nrms,faces,boundary,holes]
887
+
888
+ def _signed_triangle(tri):
889
+ (x1,y1),(x2,y2),(x3,y3) = tri
890
+ return ((x2 - x1)*(y3 - y1) - (x3 - x1)*(y2 - y1)) / 2.0
891
+
892
+ outer_area = sum(outer_loop[i][0]*outer_loop[(i+1)%len(outer_loop)][1]
893
+ - outer_loop[(i+1)%len(outer_loop)][0]*outer_loop[i][1]
894
+ for i in range(len(outer_loop)))
895
+ orientation = 1 if outer_area >= 0 else -1
896
+
897
+ for tri in triangles:
898
+ tri_points = [point(x, y, 0, 1) for (x, y) in tri]
899
+ area = _signed_triangle(tri)
900
+ if area * orientation < 0:
901
+ tri_points[1], tri_points[2] = tri_points[2], tri_points[1]
902
+ area = -area
903
+ if abs(area) > minarea:
904
+ surf = addTri2Surface(tri_points,surf,
905
+ nfunc=lambda x: ([0,0,1,0],
906
+ [0,0,1,0],
907
+ [0,0,1,0]))
908
+
909
+ return surf,[]
910
+
911
+ ### updated, surface- and solid-aware generalized geometry functions
912
+
913
+ # length -- scalar length doesn't make sense for sufface or solid
914
+
915
+ geom_center = center
916
+ def center(x):
917
+ """Return the point corresponding to the center of surface, solid, or
918
+ figure x.
919
+
920
+ """
921
+ if issurface(x):
922
+ box = surfacebbox(x)
923
+ return scale3(add(box[0],box[1]),0.5)
924
+ elif issolid(x):
925
+ box = solidbbox(x)
926
+ return scale3(add(box[0],box[1]),0.5)
927
+ else:
928
+ return geom_center(x)
929
+
930
+ geom_bbox = bbox
931
+ def bbox(x):
932
+ """Given a figure, surface, or solid x, return the three-dimensional
933
+ bounding box of that entity."""
934
+ if issolid(x):
935
+ return solidbbox(x)
936
+ elif issurface(x):
937
+ return surfacebbox(x)
938
+ else:
939
+ return geom_bbox(x)
940
+
941
+ # sample -- doesn't make sense for surface or solid
942
+ # unsample -- doesn't make sense for surface or solid
943
+ # segment -- doesn't make sense for surface or solid
944
+ # isnsideXY -- doesn't make sense for a suface or solid
945
+
946
+ def translate(x,delta):
947
+ """ return a translated version of the surface, solid, or figure"""
948
+ if issolid(x):
949
+ return translatesolid(x,delta)
950
+ elif issurface(x):
951
+ return translatesurface(x,delta)
952
+ else:
953
+ return geom_translate(x,delta)
954
+
955
+ def rotate(x,ang,cent=point(0,0),axis=point(0,0,1.0),mat=False):
956
+ """ return a rotated version of the surface, solid, or figure"""
957
+ if issolid(x):
958
+ return rotatesolid(x,ang,cent=cent,axis=axis,mat=mat)
959
+ elif issurface(x):
960
+ return rotatesurface(x,ang,cent=cent,axis=axis,mat=mat)
961
+ else:
962
+ return geom_rotate(x,ang,cent=cent,axis=axis,mat=mat)
963
+
964
+
965
+ def mirror(x,plane):
966
+ """
967
+ return a mirrored version of a figure. Currently, the following
968
+ values of "plane" are allowed: 'xz', 'yz', xy'. Generalized
969
+ arbitrary reflection plane specification will be added in the
970
+ future.
971
+
972
+ NOTE: this operation will reverse the sign of the area of ``x`` if
973
+ x is a closed polyline or geometry list
974
+ """
975
+ if issolid(x):
976
+ return mirrorsolid(x,plane)
977
+ elif issurface(x):
978
+ return mirrorsurface(x,plane)
979
+ else:
980
+ return geom_mirror(x,plane)
981
+
982
+