Platonic Solids

Source code, also inline below: tedoGen.py
Results: tedo0727.ply and tedo0727.ply. Use MeshLab to view these models.
Not Ready for Prime Time: WebGL Hack

Approximately 35 years ago I saw an early model of stereo lithographs while I was working at Computervision, then the leading CAD/CAM Company.. Then about 15 years ago, they started to become more affordable and common, so, I decided to make something... but what? So, another decade went by until the Goodnow Library setup a makerspace ... So, here we are. I added a tiny bit of documentation just incase anyone finds this interesting and perhaps, develops an enterest in Computer Graphics.

The first row of images are from the first version. See the RollingTetraDoDec.mov. The next version will rotate the dodec face down to be flat. Then I need to figure out how to build in the supports such that the cleanup is minimal. The second row is from meshlab.

image IMG_3155.JPG
IMG_3155.JPG - 4032x3024
image IMG_3157.JPG
IMG_3157.JPG - 4032x3024
image IMG_3159.JPG
IMG_3159.JPG - 4032x3024
image ScreenShot20180807T21_50_28.png
ScreenShot20180807T21_50_28.png - 1194x1372
image Screen Shot 2018-07-26 at 9.05.04 PM.png
Screen Shot 2018-07-26 at 9.05.04 PM.png - 1106x1218
image ScreenShot2018-07-22T10.59.png
ScreenShot2018-07-22T10.59.png - 1240x1464

Python Code

# tedoGen.py - generate a testrahedron trapped in a dodecahedron.  jch.com/jch/art/platonic
#
# This version generates an .obj file. Use the 'meshlab' app to view it.
#
# Copyright 2018 YON Jan C. Hardenbergh - jch.com/
# License: Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) https://creativecommons.org/licenses/by-sa/4.0/
#  Share - copy and redistribute the material in any medium or format
#  Adapt - remix, transform, and build upon the materialfor any purpose, even commercially.
# This license is acceptable for Free Cultural Works.The licensor cannot revoke these freedoms as long as you follow the license terms.
#
# dodecGen.py - make a dodecahedron solid.  - https://en.wikipedia.org/wiki/Dodecahedron
# 2018-07-15 T 18:25 started right after finishing dodec
# 2018-07-22 DONE! (almost, still need spandrels? tendrils?

# tetraGen.py - make a tetrahedron solid.  - https://en.wikipedia.org/wiki/Tetrahedron
# 2018-07-08 - make triStrip take 4 points - spit out .svg to start debugging.
# 2018-07-15 - DONE! spit .ply - https://en.wikipedia.org/wiki/PLY_(file_format)

# 2018-07-26 - Added TrianguarPrism for 3D printing and change to .obj https://en.wikipedia.org/wiki/Wavefront_.obj_file
#
# Approximately 35 years ago I saw an early model of stereo lithographs while I was working at Computervision, then the 
#  leading CAD/CAM Company: https://en.wikipedia.org/wiki/Computervision. Then about 15 years ago, they started to become
#  more affordable and common, so, I decided to make something... but what? So, another decade went by until the
#  Goodnow Library setup a makerspace - https://goodnowlibrary.org/now-lab/ ... So, here we are. I added a tiny bit of 
#  documentation just incase anyone finds this interesting and perhaps, develops an enterest in Computer Graphics.

import math, sys, time

rad5 = math.sqrt(5)

kCNNN = 0
kCPNN = 1
kCNPN = 2
kCPPN = 3
kCNNP = 4
kCPNP = 5
kCNPP = 6
kCPPP = 7
kpXNN = 8
kpXPN = 9
kpXNP = 10
kpXPP = 11
kpZNN = 12
kpZPN = 13
kpZNP = 14
kpZPP = 15
kpYNN = 16
kpYPN = 17
kpYNP = 18
kpYPP = 19

################################### Point/Vector

class Point:
    def __init__(self, value):
        self.data = [value[0], value[1], value[2]]
    def __getitem__(self, index):
        return self.data[index]
    def __add__(self, other):
        return  Point([self.data[0]+other[0], self.data[1]+other[1], self.data[2]+other[2]])
    def __radd__(self, other):
        return  Point([self.data[0]+other[0], self.data[1]+other[1], self.data[2]+other[2]])
    def __sub__(self, other):
        return  Point([self.data[0]-other[0], self.data[1]-other[1], self.data[2]-other[2]])
    def __mul__(self, other):
        return Point([self.data[0]*other, self.data[1]*other, self.data[2]*other])
    def __rmul__(self, other):
        return Point([self.data[0]*other, self.data[1]*other, self.data[2]*other])
    def Dot(self, other):
        return self.data[0]*other[0] + self.data[1]*other[1] + self.data[2]*other[2]
    def Length(self):
        sqr = self.data[0] * self.data[0] +  self.data[1] * self.data[1] +  self.data[2] * self.data[2]
        #len = math.sqrt(sqr)
        # print 'len', len, sqr,  self.Print()
        return math.sqrt(sqr)
    def Norm(self):
        len1 = self.Length()
        if len1 < 0.000000001:
            return Point([0,0,0])
        wonOver = 1.0/len1
        return self * wonOver
    def Dot(self, other):
        return  self.data[0]*other[0] + self.data[1] * other[1] + self.data[2]*other[2]
    def Print(self):
        print "Point : %g, %g, %g" % (self.data[0], self.data[1], self.data[2])

def Cross(v1, v2):
    return Point([v1[1]*v2[2] - v1[2]*v2[1], v1[2]*v2[0] - v1[0]*v2[2], v1[0]*v2[1] - v1[1]*v2[0]])

#######################################################################################################
#
# We are creating polygonal surfaces made of Indexed Triangles.
# These consist of a list of vertices and a list of triangles specified by 3 indices.
# To render a trianlge, you use the indices to pull out three points from the list of vertices.
# Here are the vertices and indices. While Kurt Akely would use indexes and vertexes, I stick to English (Latin?)

TriangleVertices = []
TriangleIndices = []

def getVertexIndex(point):
    roundedPoint = Point([round(point[0],3),round(point[1],3),round(point[2],3)])
    for idx in range(len(TriangleVertices)):
        delta = roundedPoint - TriangleVertices[idx]
        if delta.Length() < 0.001:
            return idx
    
    TriangleVertices.append(roundedPoint)
    return len(TriangleVertices) - 1

def addTriStrip(ptLongSt, ptLongEnd, ptShortSt, ptShortEnd, count, red, green, blue ):
    pt = ptLongSt   # the longer line
    ptB = ptShortSt  # one less here
    diffLong = ptLongEnd - ptLongSt
    deltaLong = diffLong * (1.0/count)
    diffShort = ptShortEnd - ptShortSt
    deltaShort = diffShort * (1.0/(count -1))

    pIdx0 = getVertexIndex(pt)
    pIdx1 = pIdx2 = 0

    for idx in range(count-1):
        # print idx, pt[0], pt[1], pt[2], pIdx0
        pIdx1 = getVertexIndex(ptB)
        # print idx, ptB[0], ptB[1], ptB[2], pIdx1
        pt0 = pt  # first point in longer line
        pt += deltaLong
        pIdx2 = getVertexIndex(pt)
        #print '[ %d %d %d ]'%(pIdx0, pIdx1, pIdx2) # 0, 1, 2
        TriangleIndices.append([pIdx0, pIdx1, pIdx2, red, green, blue])
        ptB0 = ptB
        ptB += deltaShort
        pIdx0 = getVertexIndex(ptB)
        #print '[ %d %d %d ]'%(pIdx2, pIdx1, pIdx0) # 2, 1, 3
        TriangleIndices.append([pIdx2, pIdx1, pIdx0, red, green, blue])
        pIdx0 = pIdx2
    
    pIdx1 = getVertexIndex(ptB)
    # print idx, ptB[0], ptB[1], ptB[2], pIdx1
    pt0 = pt  # first point in longer line
    pt += deltaLong
    pIdx2 = getVertexIndex(pt)
    #print '[ %d %d %d ]'%(pIdx0, pIdx1, pIdx2) # 0, 1, 2
    TriangleIndices.append([pIdx0, pIdx1, pIdx2, red, green, blue])

def addQuadStrip( ptLongSt, ptLongEnd, ptShortSt, ptShortEnd, count, red, green, blue ):
    pt = ptLongSt   # the longer line
    ptB = ptShortSt  # one less here
    diffLong = ptLongEnd - ptLongSt
    deltaLong = diffLong * (1.0/count)
    diffShort = ptShortEnd - ptShortSt
    deltaShort = diffShort * (1.0/(count))

    pIdx0 = getVertexIndex(pt)
    pIdx1 = pIdx2 = 0
    for idx in range(count):
        # print idx, pt[0], pt[1], pt[2], pIdx0
        pIdx1 = getVertexIndex(ptB)
        # print idx, ptB[0], ptB[1], ptB[2], pIdx1
        pt0 = pt  # first point in longer line
        pt += deltaLong
        pIdx2 = getVertexIndex(pt)
        TriangleIndices.append([pIdx0, pIdx1, pIdx2, red, green, blue])
        ptB0 = ptB
        ptB += deltaShort
        pIdx0 = getVertexIndex(ptB)
        #print '[ %d %d %d ]'%(pIdx2, pIdx1, pIdx0) # 2, 1, 3
        TriangleIndices.append([pIdx2, pIdx1, pIdx0, red, green, blue])
        pIdx0 = pIdx2


def addTriAngle(point1, point2, point3, red, green, blue ):
    pIdx0 = getVertexIndex(point1)
    pIdx1 = getVertexIndex(point2)
    pIdx2 = getVertexIndex(point3)
    TriangleIndices.append([pIdx0, pIdx1, pIdx2, red, green, blue])

def addPentagon(indices, red, green, blue ):
    ptMiddle = Point([0,0,0])
    for vIdx in range(5):
        ptMiddle = ptMiddle + TriangleVertices[indices[vIdx]]
    ptMiddle = ptMiddle * (1.0/5.0)
    pIdxMiddle = getVertexIndex(ptMiddle)
    for vIdx in range(5):
        idx2 = (vIdx + 1) % 5
        TriangleIndices.append([ pIdxMiddle, indices[idx2], indices[vIdx], red, green, blue])

# make a connector

# give a pentagon, create points that are towards the middle of the pentagon AND towards the center of the dodec
def addPentagonStrips(center, segmentLength, indices, red, green, blue ):
    ptMiddle = Point([0,0,0])
    for vIdx in range(5):
        ptMiddle = ptMiddle + TriangleVertices[indices[vIdx]]
    ptMiddle = ptMiddle * (1.0/5.0)
    #pIdxMiddle = getVertexIndex(ptMiddle)
    ptsToMiddle = []
    ptsToCenter = []
    for vIdx in range(5):
        ptToMid =  TriangleVertices[indices[vIdx]] + (ptMiddle - TriangleVertices[indices[vIdx]]) * segmentLength
        ptToCtr =  TriangleVertices[indices[vIdx]] + (center - TriangleVertices[indices[vIdx]]) * segmentLength
        ptsToMiddle.append(ptToMid)
        ptsToCenter.append(ptToCtr)

    count = int(round(1.0/segmentLength))
    for vIdx in range(5):
        idx2 = (vIdx + 1) % 5
        addQuadStrip(  TriangleVertices[indices[vIdx]],  TriangleVertices[indices[idx2]], ptsToMiddle[vIdx], ptsToMiddle[idx2], count, red, green, blue )
        addQuadStrip( ptsToMiddle[vIdx], ptsToMiddle[idx2], ptsToCenter[vIdx], ptsToCenter[idx2], count, red, green, blue )
        
# Create a triangular prism between 2 points.
# Using vectors in clumsy way, but, it works.
def AddTriangularPrism(pt1, pt2, scale, red, green, blue):
    delta = pt2 - pt1
    shrinkPt1 = pt1 + delta * 0.05
    shrinkPt2 = pt2 - delta * 0.05
    norm0 = delta.Norm()
    testPt = Point([norm0[1], -norm0[0], norm0[2]])
    dot = norm0.Dot(testPt)
    # testPt is just a guess, now make a point that is perpendicular to that w.r.t. delta
    cross1 = Cross(norm0,testPt)
    norm1 = cross1.Norm()
    # now do the same thing to get the other perpendicular
    cross2 = Cross(norm0, norm1)
    norm2 = cross2.Norm()
    # now make a third pont that forms an approximate equilateral triangle, get the mid vector and flip it
    diff = norm2 - norm1
    mid = norm1 + diff * 0.5
    # TODO: -0.73 is a guess. We have a 90-45-45 triangle. We want to add 15 each 45 to get an =lat tri.
    other = -0.73 * mid  # and scale it when we flip it by an empirical amount.
    end1Pt0 = shrinkPt1 + scale * norm1
    end1Pt1 = shrinkPt1 + scale * norm2
    end1Pt2 = shrinkPt1 + scale * other
    end2Pt0 = shrinkPt2 + scale * norm1
    end2Pt1 = shrinkPt2 + scale * norm2
    end2Pt2 = shrinkPt2 + scale * other
    addQuadStrip( end1Pt0, end2Pt0, end1Pt1, end2Pt1, 6, red, green, blue)
    addQuadStrip( end1Pt1, end2Pt1, end1Pt2, end2Pt2, 6, red, green, blue)
    addQuadStrip( end1Pt2, end2Pt2, end1Pt0, end2Pt0, 6, red, green, blue)
    
# Write .obj - file header https://en.wikipedia.org/wiki/Wavefront_.obj_file
def writeObjHeader(file, nVerts, nTris, timestamp):
    file.write('# OBJ File Generated by tedoGen.py - see jch.com/jch/art/platonic\n')
    file.write('# Vertices: %d\n'%(nVerts))
    file.write('# Faces:  %d\n'%(nTris))

# Write .ply file header  - https://en.wikipedia.org/wiki/PLY_(file_format)
def writePlyHeader(file, nVerts, nTris, timestamp):
    file.write('ply\n')
    file.write('format ascii 1.0\n')
    file.write('comment platonic solid art jch.com/jch/art/platonic\n')
    file.write('element vertex %d\n'%( nVerts))
    file.write('property float x\n')
    file.write('property float y\n')
    file.write('property float z\n')
    file.write('element face %d\n'%( nTris))
    file.write('property list uchar int vertex_indices\n')
    file.write('property uchar red\n')
    file.write('property uchar green\n')
    file.write('property uchar blue\n')
    file.write('end_header\n')

# main ***************************************************************
######################################################################
# dodecahedron
#
# This formulates the points of the dodec as a cube [+/-1, +/-1, +/-1] and 12 more = 20 total
# [ +/-1, +/-1, +/-1]   kCNNN = [-1,-1,-1] ... kCPNP = [ 1, -1, 1] ... kCPPP = [1,1,1]
# The coordinates of the 12 vertices of the cross-edges are:
# [ 0, +/-hVal, +/-1/hVal ] kpXNN = [ 0, -1.618, -0.618 ] - points on the X = 0 plane
# [ +/-hVal, +/-1/hVal, 0 ] kpZNN .. kpZPP - points on the Z = 0 plane
# [ +/-1/hVal, 0, +/-hVal ] kpYNN .. kpYPP - points on the Y = 0 plane
# When hVal = ( 1+ sqrt(5) ) / 2, which is the inverse of the golden ratio, the result is a regular dodecahedron
# sqrt(5) = 2.236 and hVal = 1.618

rad5 = math.sqrt(5)
rad3 = math.sqrt(3)

def main():

    timestamp = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(time.time()))
    print sys.argv[0], 'running at ', timestamp

    # original points 
    hVal = (rad5 + 1.0)/2.0
    hInv = 1.0/hVal
    dodecPoints = [ Point([-1,-1,-1]), Point([ 1,-1,-1]), Point([-1, 1,-1]), Point([ 1, 1,-1]), 
                    Point([-1,-1, 1]), Point([ 1,-1, 1]), Point([-1, 1, 1]), Point([ 1, 1, 1]), 
                    Point([0,-hVal,-hInv]), Point([0, hVal,-hInv]), 
                    Point([0,-hVal, hInv]), Point([0, hVal, hInv]), 
                    Point([-hVal,-hInv, 0]), Point([ hVal,-hInv, 0]), 
                    Point([-hVal, hInv, 0]), Point([ hVal, hInv, 0]), 
                    Point([-hInv, 0, -hVal]), Point([ hInv, 0,-hVal]), 
                    Point([-hInv, 0,  hVal]), Point([ hInv, 0, hVal]) ]

    dodecOffset = Point([0,0,0]) # [-0.5,-0.24*rad3,0])
    dodecScale = 5.0
    dodecSegLength = 0.15

    orgIndices = [ kCNNN, kCPNN, kCNPN, kCPPN, kCNNP, kCPNP, kCNPP, kCPPP, kpXNN, kpXPN, kpXNP, kpXPP, kpZNN, kpZPN, kpZNP, kpZPP, kpYNN, kpYPN, kpYNP, kpYPP ]
    orgNames = [ 'kCNNN', 'kCPNN', 'kCNPN', 'kCPPN', 'kCNNP', 'kCPNP', 'kCNPP', 'kCPPP', 'kpXNN', 'kpXPN', 'kpXNP', 'kpXPP', 'kpZNN', 'kpZPN', 'kpZNP', 'kpZPP', 'kpYNN', 'kpYPN', 'kpYNP', 'kpYPP' ]

    # print 'dbg', len(orgIndices), len(dodecPoints)
    dodecVerts = []

    #for pt in dodecPoints:
    
    for idx in range(len(dodecPoints)):
        dodecVerts.append(dodecScale*(dodecPoints[idx]+dodecOffset))
        vIdx = getVertexIndex(dodecVerts[-1])
        # print '%2d  %s %8.3f %8.3f %8.3f'%(idx, orgNames[idx], dodecVerts[-1][0], dodecVerts[-1][1], dodecVerts[-1][2])

    addPentagonStrips(dodecOffset, dodecSegLength, [kCPPP, kpZPP, kpZPN, kCPNP, kpYPP], 255, 127, 127) # F0
    addPentagonStrips(dodecOffset, dodecSegLength, [kCPPP, kpYPP, kpYNP, kCNPP, kpXPP], 127, 127, 127) # F1
    addPentagonStrips(dodecOffset, dodecSegLength, [kCPPP, kpXPP, kpXPN, kCPPN, kpZPP], 127, 127, 127) # F2
    addPentagonStrips(dodecOffset, dodecSegLength, [kCPNN, kpZPN, kpZPP, kCPPN, kpYPN], 127, 127, 127) # F3
    addPentagonStrips(dodecOffset, dodecSegLength, [kCPNN, kpXNN, kpXNP, kCPNP, kpZPN], 127, 127, 127) # F4
    addPentagonStrips(dodecOffset, dodecSegLength, [kCNNP, kpYNP, kpYPP, kCPNP, kpXNP], 127, 127, 127) # F5
    addPentagonStrips(dodecOffset, dodecSegLength, [kCNNP, kpZNN, kpZNP, kCNPP, kpYNP], 127, 127, 127) # F6
    addPentagonStrips(dodecOffset, dodecSegLength, [kCNPN, kpXPN, kpXPP, kCNPP, kpZNP], 127, 127, 127) # F7
    addPentagonStrips(dodecOffset, dodecSegLength, [kCNPN, kpYNN, kpYPN, kCPPN, kpXPN], 127, 127, 127) # F8
    addPentagonStrips(dodecOffset, dodecSegLength, [kCPNN, kpYPN, kpYNN, kCNNN, kpXNN], 127, 127, 127) # F9
    addPentagonStrips(dodecOffset, dodecSegLength, [kCNNP, kpXNP, kpXNN, kCNNN, kpZNN], 127, 127, 127) # F10
    addPentagonStrips(dodecOffset, dodecSegLength, [kCNPN, kpZNP, kpZNN, kCNNN, kpYNN], 127, 255, 127) # F11


    ##################################################
    # tetrahedron - written before dodec, so, code is a bit rougher!
    #
    tetraSteps = 8
    tetraSelLength = 1.0/tetraSteps # 0.125
    tetraScale = 12.0
              
    # original points 
    tetraPoints = [ Point([0,0,0]), Point([1,0,0]), Point([.5,.5*rad3,0]), Point([0.5, (0.5/3.0)*rad3, math.sqrt(1.0-(0.25 + 0.25*0.25*3.0))])]
    tetraOffset = Point([-0.5, -0.4,-0.1]) # move it to be inside the dodec

    tetraVerts = []
    for pt in tetraPoints:
        tetraVerts.append(pt+tetraOffset)
        # print 'tetra %8.3f %8.3f %8.3f'%(tetraScale*tetraVerts[-1][0], tetraScale*tetraVerts[-1][1], tetraScale*tetraVerts[-1][2])

    pt01 = tetraVerts[0] + (tetraVerts[1] - tetraVerts[0]) * tetraSelLength
    pt21 = tetraVerts[2] - (tetraVerts[2] - tetraVerts[1]) * tetraSelLength
    tetraVerts.append(pt01)
    tetraVerts.append(pt21)
    #print 'pt01  %6.3f  %6.3f pt21 %6.3f %6.3f'%(pt21[0], pt21[1], pt01[0], pt01[1]) # , pt.Norm().Print(), pt.Print()

    pt02 = tetraVerts[0] + (tetraVerts[2] - tetraVerts[0]) * tetraSelLength
    pt12 = tetraVerts[1] - (tetraVerts[1] - tetraVerts[2]) * tetraSelLength
    tetraVerts.append(pt02)
    tetraVerts.append(pt12)

    pt20 = tetraVerts[0] + (tetraVerts[2] - tetraVerts[0]) * (1.0-tetraSelLength)
    pt10 = tetraVerts[0] + (tetraVerts[1] - tetraVerts[0]) * (1.0-tetraSelLength)
    tetraVerts.append(pt20)
    tetraVerts.append(pt10)

    # tristrips
    # facet 0, 1, 2
    innerPt021 = pt01 + (tetraVerts[2] - tetraVerts[0]) * tetraSelLength
    innerPt120 = pt10 + (tetraVerts[2] - tetraVerts[1]) * tetraSelLength
    innerPt201 = pt20 - (tetraVerts[2] - tetraVerts[1]) * tetraSelLength

    addTriStrip( tetraScale*tetraVerts[0], tetraScale*tetraVerts[1], tetraScale*pt02, tetraScale*pt12, tetraSteps, 200, 200, 200) 
    addTriStrip( tetraScale*tetraVerts[2], tetraScale*pt02, tetraScale*pt21, tetraScale*innerPt021, tetraSteps-1, 200, 200, 200)
    addTriStrip( tetraScale*pt12, tetraScale*pt21, tetraScale*innerPt120, tetraScale*innerPt201, tetraSteps-2, 200, 200, 200)
    
    pt03 = tetraVerts[0] + (tetraVerts[3] - tetraVerts[0]) * tetraSelLength
    pt23 = tetraVerts[2] - (tetraVerts[2] - tetraVerts[3]) * tetraSelLength
    pt13 = tetraVerts[1] - (tetraVerts[1] - tetraVerts[3]) * tetraSelLength
    pt31 = tetraVerts[3] - (tetraVerts[3] - tetraVerts[1]) * tetraSelLength
    pt30 = tetraVerts[3] - (tetraVerts[3] - tetraVerts[0]) * tetraSelLength

    # facet 0, 1, 3
    innerPt031 = pt01 + (tetraVerts[3] - tetraVerts[0]) * tetraSelLength
    innerPt130 = pt10 - (tetraVerts[1] - tetraVerts[3]) * tetraSelLength
    innerPt301 = pt30 - (tetraVerts[3] - tetraVerts[1]) * tetraSelLength

    addTriStrip( tetraScale*tetraVerts[1], tetraScale*tetraVerts[0], tetraScale*pt13, tetraScale*pt03, tetraSteps, 180, 180, 180)
    addTriStrip( tetraScale*pt03, tetraScale*tetraVerts[3], tetraScale*innerPt031, tetraScale*pt31, tetraSteps-1, 180, 180, 180)
    addTriStrip( tetraScale*pt31, tetraScale*pt13, tetraScale*innerPt301, tetraScale*innerPt130, tetraSteps-2, 180, 180, 180)

    # facet 0, 2, 3
    pt32 = tetraVerts[3] + ((tetraVerts[2] - tetraVerts[3]) * tetraSelLength)
    innerPt032 = pt02 + (tetraVerts[3] - tetraVerts[0]) * tetraSelLength
    innerPt230 = pt20 + (tetraVerts[3] - tetraVerts[2]) * tetraSelLength
    innerPt320 = pt30 - (tetraVerts[3] - tetraVerts[2]) * tetraSelLength

    addTriStrip( tetraScale*tetraVerts[0], tetraScale*tetraVerts[2], tetraScale*pt03, tetraScale*pt23, tetraSteps, 180, 180, 180)
    addTriStrip( tetraScale*tetraVerts[3], tetraScale*pt03, tetraScale*pt32, tetraScale*innerPt032, tetraSteps-1, 180, 180, 180)
    addTriStrip( tetraScale*pt23, tetraScale*pt32, tetraScale*innerPt230, tetraScale*innerPt320, tetraSteps-2, 180, 180, 180)

    # facet 1, 2, 3
    innerPt123 = pt12 + (tetraVerts[3] - tetraVerts[1]) * tetraSelLength
    innerPt321 = pt32 - (tetraVerts[3] - tetraVerts[1]) * tetraSelLength
    innerPt213 = pt21 + (tetraVerts[3] - tetraVerts[2]) * tetraSelLength

    addTriStrip( tetraScale*tetraVerts[2], tetraScale*tetraVerts[1], tetraScale*pt23, tetraScale*pt13, tetraSteps, 180, 180, 180)
    addTriStrip( tetraScale*pt13, tetraScale*tetraVerts[3], tetraScale*innerPt123, tetraScale*pt32, tetraSteps-1, 180, 180, 180)
    addTriStrip( tetraScale*pt32, tetraScale*pt23, tetraScale*innerPt321, tetraScale*innerPt213, tetraSteps-2, 180, 180, 180)

    # four triangles - red, green, blue and gray for Pts 0, 1, 2, 3
    addTriAngle( tetraScale*innerPt213, tetraScale*innerPt230, tetraScale*innerPt201, 180, 180, 255)
    addTriAngle( tetraScale*innerPt123, tetraScale*innerPt120, tetraScale*innerPt130, 180, 255, 180)
    addTriAngle( tetraScale*innerPt032, tetraScale*innerPt031, tetraScale*innerPt021, 255, 180, 180)
    addTriAngle( tetraScale*innerPt301, tetraScale*innerPt320, tetraScale*innerPt321, 180, 180, 180)

    # six inner quad strips between the vertices, 3 from pt0: 0-1, 0-2, 0-3,  .. 3-1, 3-2, .. 1-2
    addQuadStrip( tetraScale*innerPt021, tetraScale*innerPt120, tetraScale*innerPt031, tetraScale*innerPt130, tetraSteps-3, 100, 255, 189)
    addQuadStrip( tetraScale*innerPt032, tetraScale*innerPt230, tetraScale*innerPt021, tetraScale*innerPt201, tetraSteps-3, 127, 255, 189)
    addQuadStrip( tetraScale*innerPt031, tetraScale*innerPt301, tetraScale*innerPt032, tetraScale*innerPt320, tetraSteps-3, 127, 255, 189)
    addQuadStrip( tetraScale*innerPt301, tetraScale*innerPt130, tetraScale*innerPt321, tetraScale*innerPt123, tetraSteps-3, 127, 255, 189)
    addQuadStrip( tetraScale*innerPt321, tetraScale*innerPt213, tetraScale*innerPt320, tetraScale*innerPt230, tetraSteps-3, 180, 180, 255)
    addQuadStrip( tetraScale*innerPt120, tetraScale*innerPt201, tetraScale*innerPt123, tetraScale*innerPt213, tetraSteps-3, 180, 255, 180)


    # OK, we need two prisms to connect the tetra and dodec - so that they stick together during the 3D printing
    connectorWidth = 0.1
    span1End1Pt0 = (innerPt320 + innerPt301 + innerPt321) * 0.33 * tetraScale
    span1End2Pt0 = dodecVerts[kpYNP] + (dodecVerts[kpYPP] - dodecVerts[kpYNP]) * 0.45 
    span1End2Pt0 += (dodecOffset - span1End2Pt0) * 0.05
    AddTriangularPrism( span1End1Pt0, span1End2Pt0, connectorWidth, 255, 0, 0)
    span2End1Pt0 = (innerPt032 + innerPt031 + innerPt021) * 0.33 * tetraScale
    span2End2Pt0 = dodecVerts[kpZNN] + (dodecVerts[kCNNN] - dodecVerts[kpZNN]) * 0.45
    span2End2Pt0 += (dodecOffset - span2End2Pt0) * 0.05
    AddTriangularPrism( span2End1Pt0, span2End2Pt0, connectorWidth, 255, 0, 0)

    saveObj = True # Obj is taken by 3D print setup SW. PLY has facet colors, which is useful for debugging
    if saveObj:
        plyFile = 'tedo%s.obj'%(time.strftime("%m%d", time.localtime(time.time())))
        savefile = open(plyFile, 'w')
        writeObjHeader( savefile, len(TriangleVertices), len(TriangleIndices), timestamp)
        vertexFormat = 'v %8.3f %8.3f %8.3f\n'
    else:
        plyFile = 'tedo%s.ply'%(time.strftime("%m%d", time.localtime(time.time())))
        savefile = open(plyFile, 'w')
        writePlyHeader( savefile, len(TriangleVertices), len(TriangleIndices), timestamp)
        vertexFormat = '%8.3f %8.3f %8.3f\n'

    for index in range(len(TriangleVertices)):
        #print '%8.3f, %8.3f, %8.3f,  '%(TriangleVertices[index][0], TriangleVertices[index][1], TriangleVertices[index][2])
        savefile.write(vertexFormat%(TriangleVertices[index][0], TriangleVertices[index][1], TriangleVertices[index][2]))
    for index in range(len(TriangleIndices)):
        if saveObj:
            savefile.write('f %d %d %d\n'%(TriangleIndices[index][0]+1, TriangleIndices[index][1]+1, TriangleIndices[index][2]+1))
        else:
            savefile.write('3 %d %d %d %d %d %d\n'%(TriangleIndices[index][0], TriangleIndices[index][1], TriangleIndices[index][2],
                                                    TriangleIndices[index][3], TriangleIndices[index][4], TriangleIndices[index][5]))


    # note = "%s -jch"%( time.strftime("  %Y-%m-%d T %H:%M:%S", time.localtime(time.time())))
    savefile.close()
    print "Created %s with %d vertices and %d indexed triangles "%(plyFile, len(TriangleVertices), len(TriangleIndices))

if __name__ == '__main__':
    main()

2018-07-27 jch