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_util.py ADDED
@@ -0,0 +1,541 @@
1
+ ## geom3d_util, additional 3D geometry support for yapCAD
2
+ ## started on Mon Feb 15 20:23:57 PST 2021 Richard W. DeVaul
3
+
4
+ from yapcad.geom import *
5
+ from yapcad.geom_util import *
6
+ from yapcad.xform import *
7
+ from yapcad.geom3d import *
8
+
9
+ import math
10
+
11
+ """
12
+ ==================================================
13
+ Utility functions to support 3D geometry in yapCAD
14
+ ==================================================
15
+
16
+ This module is mostly a collection of
17
+ parametric soids and surfaces, and supporting functions.
18
+
19
+ """
20
+
21
+
22
+ def sphere2cartesian(lat,lon,rad):
23
+ """
24
+ Utility function to convert spherical polar coordinates to
25
+ cartesian coordinates for a sphere centered at the origin.
26
+ ``lat`` -- latitude
27
+ ``lon`` -- longitude
28
+ ``rad`` -- sphere radius
29
+
30
+ returns a ``yapcad.geom`` point
31
+ """
32
+ if lat == 90:
33
+ return [0,0,rad,1]
34
+ elif lat == -90:
35
+ return [0,0,-rad,1]
36
+ else:
37
+ latr = (((lat+90)%180)-90)*pi2/360.0
38
+ lonr = (lon%360)*pi2/360.0
39
+
40
+ smallrad = math.cos(latr)*rad
41
+ z = math.sin(latr)*rad
42
+ x = math.cos(lonr)*smallrad
43
+ y = math.sin(lonr)*smallrad
44
+ return [x,y,z,1]
45
+
46
+ ## icosohedron-specific function, generate intial geometry
47
+ def makeIcoPoints(center,radius):
48
+ """
49
+ Procedure to generate the verticies of an icosohedron with the specified
50
+ ``center`` and ``radius``.
51
+ """
52
+ points = []
53
+ normals = []
54
+ p = sphere2cartesian(90,0,radius)
55
+ n = scale3(p,1.0/mag(p))
56
+ n[3] = 0.0
57
+ points.append(p)
58
+ normals.append(n)
59
+ for i in range(10):
60
+ sgn = 1
61
+ if i%2 == 0:
62
+ sgn =-1
63
+ lat = math.atan(0.5)*360.0*sgn/pi2
64
+ lon = i*36.0
65
+ p = sphere2cartesian(lat,lon,radius)
66
+ n = scale3(p,1.0/mag(p))
67
+ n[3] = 0.0
68
+ points.append(p)
69
+ normals.append(n)
70
+
71
+ p = sphere2cartesian(-90,0,radius)
72
+ n = scale3(p,1.0/mag(p))
73
+ n[3] = 0.0
74
+ points.append(p)
75
+ normals.append(n)
76
+ return list(map( lambda x: add(x,center),points)),normals
77
+
78
+ # face indices for icosahedron
79
+ icaIndices = [ [1,11,3],[3,11,5],[5,11,7],[7,11,9],[9,11,1],
80
+ [2,1,3],[2,3,4],[4,3,5],[4,5,6],[6,5,7],[6,7,8],[8,7,9],[8,9,10],[10,9,1],[10,1,2],
81
+ [0,2,4],[0,4,6],[0,6,8],[0,8,10],[0,10,2] ]
82
+
83
+ vertexHash = {}
84
+ def addVertex(nv,nn,verts,normals):
85
+ """
86
+ Utility function that takes a vertex and associated normal and a
87
+ list of corresponding vertices and normals, and returns an index
88
+ that corresponds to the given vertex/normal. If the vertex
89
+ doesn't exist in the list, the lists are updated to include it and
90
+ the corresponding normal.
91
+
92
+ returns the index, and the (potentiall updated) lists
93
+ """
94
+ global vertexHash
95
+ if len(verts) == 0:
96
+ vertexHash = {}
97
+
98
+ found = False
99
+ vkey = f"{nv[0]:.2f}{nv[1]:.2f}{nv[2]:.2f}"
100
+ if vkey in vertexHash:
101
+ found = True
102
+ inds = vertexHash[vkey]
103
+ for i in inds:
104
+ if vclose(nv,verts[i]):
105
+ return i,verts,normals
106
+
107
+ verts.append(nv)
108
+ normals.append(nn)
109
+ i = len(verts)-1
110
+ if found:
111
+ vertexHash[vkey] += [ i ]
112
+ else:
113
+ vertexHash[vkey] = [ i ]
114
+ return i,verts,normals
115
+
116
+ def subdivide(f,verts,normals,rad):
117
+ """
118
+ Given a face (a list of three vertex indices), a list of vertices,
119
+ normals, and a radius, subdivide that face into four new faces and
120
+ update the lists of vertices and normals accordingly.
121
+
122
+ return the updated vertex and normal lists, and a list of the four
123
+ new faces.
124
+ """
125
+
126
+ ind1 = f[0]
127
+ ind2 = f[1]
128
+ ind3 = f[2]
129
+ v1 = verts[ind1]
130
+ v2 = verts[ind2]
131
+ v3 = verts[ind3]
132
+ n1 = normals[ind1]
133
+ n2 = normals[ind2]
134
+ n3 = normals[ind3]
135
+ va = add(v1,v2)
136
+ vb = add(v2,v3)
137
+ vc = add(v3,v1)
138
+ ma = rad/mag(va)
139
+ mb = rad/mag(vb)
140
+ mc = rad/mag(vc)
141
+ va = scale3(va,ma)
142
+ vb = scale3(vb,mb)
143
+ vc = scale3(vc,mc)
144
+
145
+ na = add4(n1,n2)
146
+ na = scale4(na,1.0/mag(na))
147
+ nb = add4(n2,n3)
148
+ nb = scale4(nb,1.0/mag(nb))
149
+ nc = add4(n3,n1)
150
+ nc = scale4(nc,1.0/mag(nc))
151
+
152
+ inda,verts,normals = addVertex(va,na,verts,normals)
153
+ indb,verts,normals = addVertex(vb,nb,verts,normals)
154
+ indc,verts,normals = addVertex(vc,nc,verts,normals)
155
+
156
+ f1 = [ind1,inda,indc]
157
+ f2 = [inda,ind2,indb]
158
+ f3 = [indb,ind3,indc]
159
+ f4 = [inda,indb,indc]
160
+
161
+ return verts,normals, [f1,f2,f3,f4]
162
+
163
+ # make the sphere, return a surface representation
164
+ def sphereSurface(diameter,center=point(0,0,0),depth=2):
165
+ rad = diameter/2
166
+ ## subdivision works only when center is origin, so we add
167
+ ## any center offset after we subdivide
168
+ verts,normals = makeIcoPoints(point(0,0,0),rad)
169
+ faces = icaIndices
170
+
171
+ for i in range(depth):
172
+ ff = []
173
+ for f in faces:
174
+ verts, norms, newfaces = subdivide(f,verts,normals,rad)
175
+ ff+=newfaces
176
+ faces = ff
177
+
178
+ if not vclose(center,point(0,0,0)):
179
+ verts = list(map(lambda x: add(x,center),verts))
180
+ return ['surface',verts,normals,faces,[],[]]
181
+
182
+ # make sphere, return solid representation
183
+ def sphere(diameter,center=point(0,0,0),depth=2):
184
+ call = f"yapcad.geom3d_util.sphere({diameter},center={center},depth={depth})"
185
+ return solid( [ sphereSurface(diameter,center,depth)],
186
+ [],['procedure',call] )
187
+
188
+
189
+ def rectangularPlane(length,width,center=point(0,0,0)):
190
+ """ return a rectangular surface with the normals oriented in the
191
+ positive z direction """
192
+ c = center
193
+ l = point(length,0,0)
194
+ w = point(0,width,0)
195
+ p0 = add(c,(scale3(add(l,w),-0.5)))
196
+ p1 = add(p0,l)
197
+ p2 = add(p1,w)
198
+ p3 = add(p0,w)
199
+ n = vect(0,0,1,0)
200
+
201
+ surf = surface( [p0,p1,p2,p3],[n,n,n,n],
202
+ [[0,1,2],
203
+ [2,3,0]])
204
+
205
+ return surf
206
+
207
+ # make a rectangular prism from six surfaces, return a solid
208
+ def prism(length,width,height,center=point(0,0,0)):
209
+ """make a rectangular prism solid composed of six independent
210
+ faces"""
211
+ call = f"yapcad.geom3d_util.prism({length},{width},{height},{center})"
212
+
213
+ l2 = length/2
214
+ w2 = width/2
215
+ h2 = height/2
216
+
217
+ topS = rectangularPlane(length,width,point(0,0,h2))
218
+ bottomS = rotatesurface(topS,180,axis=point(1,0,0))
219
+ frontS = rectangularPlane(length,height,point(0,0,w2))
220
+ frontS = rotatesurface(frontS,90,axis=point(1,0,0))
221
+ backS = rotatesurface(frontS,180)
222
+ rightS = rectangularPlane(height,width,point(0,0,l2))
223
+ rightS = rotatesurface(rightS,90,axis=point(0,1,0))
224
+ leftS = rotatesurface(rightS,180)
225
+
226
+ surfaces = [topS,bottomS,frontS,backS,rightS,leftS]
227
+ if not vclose(center,[0,0,0,1]):
228
+ surfaces = list(map(lambda x: translatesurface(x,center),surfaces))
229
+
230
+ sol = solid(surfaces,
231
+ [],
232
+ ['procedure',call])
233
+ return sol
234
+
235
+ def circleSurface(center,radius,angr=10,zup=True):
236
+ """make a circular surface centered at ``center`` lying in the XY
237
+ plane with normals pointing in the positive z direction if ``zup
238
+ == True``, negative z otherwise"""
239
+
240
+ if angr < 1 or angr > 45:
241
+ raise ValueError('angular resolution must be between 1 and 45 degrees')
242
+
243
+ samples = round(360.0/angr)
244
+ angr = 360.0/samples
245
+ basep=[center]
246
+
247
+ for i in range(samples):
248
+ theta = i*angr*pi2/360.0
249
+ pp = [math.cos(theta)*radius,math.sin(theta)*radius,0.0,1.0]
250
+ pp = add(pp,center)
251
+ basep.append(pp)
252
+
253
+
254
+ basef=[]
255
+
256
+ rng = range(1,len(basep))
257
+ if not zup:
258
+ rng = reversed(rng)
259
+
260
+ ll = len(basep)-1
261
+ for i in rng:
262
+ face = []
263
+ if zup:
264
+ face = [0,i,1+i%ll]
265
+ else:
266
+ face = [0,1+i%ll,i]
267
+
268
+ basef.append(face)
269
+
270
+ z=-1
271
+ if zup:
272
+ z=1
273
+ n = vect(0,0,z,0)
274
+ basen= [ n ] * len(basep)
275
+
276
+ return surface(basep,basen,basef)
277
+
278
+ def conic(baser,topr,height, center=point(0,0,0),angr=10):
279
+
280
+ """Make a conic frustum splid, center is center of first 'base'
281
+ circle, main axis aligns with positive z. This function can be
282
+ used to make a cone, a conic frustum, or a cylinder, depending on
283
+ the parameters.
284
+
285
+ ``baser`` is the radius of the base, must be greater than
286
+ zero (epsilon).
287
+
288
+ ``topr`` is the radius of the top, may be zero or
289
+ positive. If ``0 <= topr < epsilon``, then the top
290
+ is treated as a single point.
291
+
292
+ ``height`` is distance from base to top, must be greater than
293
+ epsilon.
294
+
295
+ ``center`` is the location of the center of the base.
296
+
297
+ ``angr`` is the requested angular resolution in degrees for
298
+ sampling circles. Actual angular resolution will be
299
+ ``360/round(360/angr)``
300
+
301
+ """
302
+ call = f"yapcad.geom3d_util.conic({baser},{topr},{height},{center},{angr})"
303
+ if baser < epsilon:
304
+ raise ValueError('bad base radius for conic')
305
+ base = arc(center,baser)
306
+
307
+ toppoint = False
308
+ if topr < 0:
309
+ raise ValueError('bad top radius for conic')
310
+ if topr < epsilon:
311
+ toppoint = True
312
+
313
+ if height < epsilon:
314
+ raise ValueError('bad height in conic')
315
+
316
+ baseS = circleSurface(center,baser,zup=False)
317
+ baseV = baseS[1]
318
+ ll = len(baseV)
319
+
320
+ if not toppoint:
321
+ topS = circleSurface(add(center,point(0,0,height)),
322
+ topr,zup=True)
323
+ topV = topS[1]
324
+ cylV = baseV[1:] + topV[1:]
325
+ ll = ll-1
326
+ baseN = []
327
+ topN = []
328
+ cylF = []
329
+ for i in range(ll):
330
+ p0 = cylV[(i-1)%ll]
331
+ p1 = cylV[(i+1)%ll]
332
+ p2 = cylV[ll+i]
333
+
334
+ cylF.append([i,(i+1)%ll,ll+(i+1)%ll])
335
+ cylF.append([i,ll+(i+1)%ll,ll+i])
336
+
337
+ pp,n0 = tri2p0n([p0,p1,p2])
338
+
339
+ baseN.append(n0)
340
+ topN.append(n0)
341
+
342
+ cylN = baseN+topN
343
+
344
+ cylS = surface(cylV,cylN,cylF)
345
+
346
+ return solid([baseS,cylS,topS],[],
347
+ ['procedure',call])
348
+ else:
349
+ topP = add(center,point(0,0,height))
350
+ conV = [ topP ] + baseV
351
+ ll = len(conV)
352
+ conN = [[0,0,1,0]]
353
+ conF = []
354
+
355
+ for i in range(1,ll):
356
+ p0= conV[0]
357
+ p1= conV[(i-1)%ll]
358
+ p2= conV[(i+1)%ll]
359
+
360
+ conF.append([0,i,(i+1)%ll])
361
+ pp,n0 = tri2p0n([p0,p1,p2])
362
+
363
+ conN.append(n0)
364
+
365
+ conS = surface(conV,conN,conF)
366
+
367
+ return solid([baseS,conS],[],
368
+ ['procedure',call])
369
+
370
+ def makeRevolutionSurface(contour,zStart,zEnd,steps,arcSamples=36):
371
+ """
372
+ Take a countour (any function z->y mapped over the interval
373
+
374
+ ``zStart`` and ``zEnd`` and produce the surface of revolution
375
+ around the z axis. Sample ``steps`` contours of the function,
376
+ which in turn are turned into circles sampled `arcSamples`` times.
377
+ """
378
+
379
+ sV=[]
380
+ sN=[]
381
+ sF=[]
382
+ zRange = zEnd-zStart
383
+ zD = zRange/steps
384
+
385
+ degStep = 360.0/arcSamples
386
+ radStep = pi2/arcSamples
387
+ for i in range(steps):
388
+ z = i*zD+zStart
389
+ r0 = contour(z)
390
+ r1 = contour(z+zD)
391
+ if r0 < epsilon*10:
392
+ r0 = epsilon*10
393
+ if r1 < epsilon*10:
394
+ r1 = epsilon*10
395
+ for j in range(arcSamples):
396
+ a0 = (j-1)*radStep
397
+ a1 = j*radStep
398
+ a2 = (j+1)*radStep
399
+
400
+ p0 = [math.cos(a0)*r0,math.sin(a0)*r0,z,1.0]
401
+ p1 = [math.cos(a1)*r0,math.sin(a1)*r0,z,1.0]
402
+ p2 = [math.cos(a2)*r0,math.sin(a2)*r0,z,1.0]
403
+
404
+ pp1 = [math.cos(a1)*r1,math.sin(a1)*r1,z+zD,1.0]
405
+ pp2 = [math.cos(a2)*r1,math.sin(a2)*r1,z+zD,1.0]
406
+
407
+ p,n = tri2p0n([p0,p2,pp1])
408
+
409
+ k1,sV,sN = addVertex(p1,n,sV,sN)
410
+ k2,sV,sN = addVertex(p2,n,sV,sN)
411
+ k3,sV,sN = addVertex(pp2,n,sV,sN)
412
+ k4,sV,sN = addVertex(pp1,n,sV,sN)
413
+ sF.append([k1,k2,k3])
414
+ sF.append([k1,k3,k4])
415
+
416
+ return surface(sV,sN,sF)
417
+
418
+ def contour(poly,distance,direction, samples,scalefunc= lambda x: (1,1,1)):
419
+ """take a closed polygon and apply a scaling function defined on the
420
+ interval 0,1 that returns a tuple of x,y,z scaling values. For
421
+ each of ``samples`` number of samples, translate in ``direction``
422
+ direction and scale the contour according to ``scalefunc()``,
423
+ producing a surface."""
424
+ if not ispolygon(poly):
425
+ raise ValueError('invalid polygon passed to contour')
426
+ if samples < 2:
427
+ raise ValueError('number of samples must be 2 or greater')
428
+ if not close(mag(direction),1.0):
429
+ raise ValueError('bad direction vector')
430
+ if distance <= epsilon:
431
+ raise ValueError('bad distance passed to contour')
432
+
433
+ raise NotImplemented("this function is a work in progress")
434
+
435
+ p0 = poly
436
+ p1 = []
437
+
438
+ u = 0.0
439
+ scle = scalefunc(u)
440
+ sctx = Scale(scle[0],scle[1],scle[2])
441
+ ply = list(map(lambda p: sctx.mul(p),poly))
442
+ surf = poly2surface(ply)
443
+ s1 = reversesurface(surf)
444
+
445
+ u = 1.0
446
+ scle = scalefunc(u)
447
+ sctx = Scale(scle[0],scle[1],scle[2])
448
+ ply2 = list(map(lambda p: sctx.mul(p),poly))
449
+ surf2 = poly2surface(ply2)
450
+ s2 = translatesurface(surf2,scale4(direction,distance))
451
+
452
+ vrts = surf[1]
453
+ nrms = surf[2]
454
+ facs = surf[3]
455
+ bndr = surf[4]
456
+
457
+ vrts1 = list(map(lambda i: vrts[i],bndr))
458
+ for ii in range(1,samples):
459
+ u = ii/samples
460
+
461
+ stripF = []
462
+ #vrts2 = list(map(lambda i:
463
+ for i in range(len(bndr)):
464
+ j0 = bndry1[(i-1)%len(bndry1)]
465
+ j1 = bndry1[i]
466
+ j2 = bndry1[(i+1)%len(bndry1)]
467
+ j3 = j2+len(s2[1])
468
+ j4 = j1+len(s2[1])
469
+ p0 = stripV[j0]
470
+ p1 = stripV[j2]
471
+ p2 = stripV[j3]
472
+ try:
473
+ pp,n0 = tri2p0n([p0,p1,p2])
474
+ except ValueError:
475
+ # bad face, skip
476
+ continue
477
+ stripN[j1]=n0
478
+ stripN[j4]=n0
479
+ stripF.append([j1,j2,j3])
480
+ stripF.append([j1,j3,j4])
481
+
482
+
483
+
484
+
485
+ def extrude(surf,distance,direction=vect(0,0,1,0)):
486
+
487
+ """ Take a surface and extrude it in the specified direction to
488
+ create a solid. Return the solid. """
489
+ call = f"yapcad.geom3d_util.extrude({surf},{distance},{direction})"
490
+
491
+ if not issurface(surf):
492
+ raise ValueError('invalid surface passed to extrude')
493
+
494
+ if distance <= epsilon:
495
+ raise ValueError('bad distance passed to extrude')
496
+
497
+ s1 = translatesurface(surf,scale4(direction,distance))
498
+ s2 = reversesurface(surf)
499
+
500
+ loops = []
501
+ if s2[4]:
502
+ loops.append(s2[4])
503
+ loops.extend([loop for loop in s2[5] if loop])
504
+
505
+ stripV = s2[1] + s1[1] # vertices for the edge strips
506
+ stripN = [vect(0, 0, 1, 0)] * len(stripV) # placeholder normals
507
+ stripF: list[list[int]] = []
508
+ offset = len(s2[1])
509
+
510
+ for bndry in loops:
511
+ if len(bndry) < 2:
512
+ continue
513
+ for i in range(len(bndry)):
514
+ j0 = bndry[(i - 1) % len(bndry)]
515
+ j1 = bndry[i]
516
+ j2 = bndry[(i + 1) % len(bndry)]
517
+ j3 = j2 + offset
518
+ j4 = j1 + offset
519
+ p0 = stripV[j0]
520
+ p1 = stripV[j2]
521
+ p2 = stripV[j3]
522
+ try:
523
+ pp, n0 = tri2p0n([p0, p1, p2])
524
+ except ValueError:
525
+ continue
526
+ stripN[j1] = n0
527
+ stripN[j4] = n0
528
+ stripF.append([j1, j2, j3])
529
+ stripF.append([j1, j3, j4])
530
+
531
+ #import pdb ; pdb.set_trace()
532
+ strip = surface(stripV,stripN,stripF)
533
+
534
+ return solid([s2,strip,s1],
535
+ [],
536
+ ['procedure',call])
537
+
538
+
539
+
540
+
541
+