#!/usr/bin/env python # -*- coding: utf-8 -*- #=========================================================================================================== # "Negative Poisson Ratio" Mask Generator #  Sabrina Paseman 2010 All rights reserved. # Generates Vector instructions to cut "cells" into a sheet of material. # Material has a "Negative Poisson Ratio" (i.e. expands in the y direction when stretched in the x direction) # # Mask Generation procedure # 1. Input parameters in this file and run program to generate pdf file(s). # 2. Open pdf in Adobe Illustrator and save as svg # 3. In CorelDRAW X5, create new page # 4. change size to 8.5 x 11.0 # 5. import svg # 6. Select all and ungroup # 7. Select triangles and select hairline from outline pen tool # # You now have a mask. Printing it will produce the material. # # Epilog Helix 60 watts CO2 Laser printing parameters # Horozontal: 8.5 # Vertical: 11.0 # Vector only # paper gum rubber # Speed: 80% 20% # Power: 20% 90% # Freg: 500 100 #=========================================================================================================== from reportlab.pdfgen import canvas from reportlab.lib.units import inch from time import strftime from math import cos, sin, pi, sqrt #=========================================================================================================== #=========================================================================================================== # These functions are based the PLOTLIB package used on the Tektronix 4013 in 1975. PLOTLIB was a "Vector # drawing language" that etched the 4013's monochromatic 1024 x 1024 resolution Mica sheet display with an # electron gun. The system used vector as opposed to raster since memory/storage was precious back then. # Vector Graphics plus no color make PLOTLIBs abstractions a good fit with the Laser cutter, which has the # same two limitations. PLOTLIB "Figures", implemented as arrays, were the central data abstraction. # Representing an entire figure as a single array let PLOTLIB transform and render it with a single line of code: # draw(c,translate(x,y,rotate(theta,translate(-tWidth/2.0,-tHeight/3.0,figure)))) # PLOTLIB represented "Figures" using nx3 arrays with control codes in column 0, x in column 1 and y in column 2. # The 4013 control codes were 0 for "moveto" and 1 for "drawto". # Normally, one would use linear algebra to create 2 x 2 matrices for translation, rotation and scaling operations. # However, PLOTLIB used 3x3 matrices for 2D ops and preserved the control codes as transformations were performed. # This took advantage of the fact that the -commands- were the same no matter where the points were moved. # Here, nested lists replace arrays. The 4013 did not have a bezier or a "close" command, so we add a "bezier" # command code (2) to prepend to each of the of the 3 points used in curveto and a "close" command code (3). # # "draw" is an interpreter that takes a "Figure" array and executes all pdf ops from beginPath to drawPath. # Note that this division puts knowledge of the rendering package in the top level routine (which creates the # canvas) and the rendering commnad ("draw"). The transformation routines: translate, rotate, scale and # the figure routines: KlingonTriangle, have no dependencies on the renderer other than the control column. # # It is important to note that while close and bezier transform "properly" using this representation, other # pdf commands such as rectangle, circle, etc. will not. E.g. rotating a pdf rectable will rotate the # corners properly, but the resulting pdf will create a rectangle with horizontal and vertical lines # which connects these corners. #=========================================================================================================== def translate(x,y,points): # translate a list of [x,y] points return [[point[0], point[1]+x,point[2]+y] for point in points] def scale(sx,sy,points): # scale a list of [x,y] points (relative to origin of 0,0) return [[point[0], point[1]*sx,point[2]*sy] for point in points] def rotate(theta,points): # rotate relative to 0,0 http://en.wikipedia.org/wiki/Rotation_%28mathematics%29 return [[point[0], point[1]*cos(theta) - point[2]*sin(theta),point[1]*sin(theta) + point[2]*cos(theta)] for point in points] def draw(c,ins): # Figure is interpreted as a list of instructions (ins). ip = 0 # instruction pointer p = c.beginPath() while ip < len(ins): cmd = ins[ip][0] if cmd == 0: p.moveTo (ins[ip][1],ins[ip][2]) elif cmd == 1: p.lineTo (ins[ip][1],ins[ip][2]) elif cmd == 2: p.curveTo (ins[ip][1],ins[ip][2],ins[ip+1][1],ins[ip+1][2],ins[ip+2][1],ins[ip+2][2]); ip+=2 elif cmd == 3: p.close() else: print "Illegal instruction %d at ip = %d" % (cmd,ip) ip+=1 c.drawPath(p) #=========================================================================================================== #=========================================================================================================== # Assume Portrait mode # Origin top Left Hand Corner # (Positive) X across top of paper (8.5") # (Negative) Y down side of Paper (11") #=========================================================================================================== def drawHourglassTriangle(c,xOrigin,yOrigin,tWidth,tHeight,direction='dn'): p = c.beginPath() p.moveTo(xOrigin,yOrigin) p.lineTo(xOrigin+tWidth,yOrigin) if 'dn' == direction: p.lineTo(xOrigin+tWidth/2.0,yOrigin-tHeight) else: p.lineTo(xOrigin+tWidth/2.0,yOrigin+tHeight) p.lineTo(xOrigin,yOrigin) p.close() c.drawPath(p) def drawHourglassCell(c,cWidth,cHeight,tWidth,tHeight,sWidth,xOrigin,yOrigin): sWidth /= 2.0 drawHourglassTriangle(c,xOrigin,yOrigin-sWidth,tWidth,tHeight) drawHourglassTriangle(c,xOrigin,yOrigin-cHeight+sWidth,tWidth,tHeight,'up') drawHourglassTriangle(c,xOrigin+cWidth/2.0,yOrigin-sWidth-cHeight/2.0,tWidth,tHeight) drawHourglassTriangle(c,xOrigin+cWidth/2.0,yOrigin+sWidth-cHeight/2.0,tWidth,tHeight,'up') #=========================================================================================================== # Draw an array of "cells" of a given width and height at the origin # A cell consists of four triangles, each the same width and height, arranged in a "square" configuration # A "strut" of width sWidth separates the triangles' bases. #=========================================================================================================== def HourGlass(filename, cWidth, #cell cHeight, #cell tWidth, #triangle tHeight, #triangle sWidth, #strut xOrigin=0.1*inch,yOrigin=11.2*inch): c = canvas.Canvas(filename) xCount = int(8.5*inch/cWidth) yCount = int(11.0*inch/cHeight) c.drawString(0,11.5*inch,"HourGlass NPR Mask  S. Paseman cW:%d, cH:%d, tW:%d, tH:%d, sW:%d, gened: %s" % \ (cWidth,cHeight,tWidth,tHeight,sWidth,strftime("%Y-%m-%d %H:%M:%S"))) #print "xCount:%d yCount:%d" % (xCount, yCount) for ix in range(xCount): x = xOrigin+(ix*cWidth) for iy in range(yCount): y = yOrigin-(iy*cHeight) drawHourglassCell(c,cWidth,cHeight,tWidth,tHeight,sWidth,x,y) c.showPage() c.save() def HourGlassIt(): # http://www.paseman.com/public/IMG_0205.JPG shows that the hourglass pattern corners twist when stretched. # This torsion is transmitted across the entire upper member, causing the "top strut" (the vertical piece) # to buckle "outwards". This is apparent when the hourglass pattern is replicated in a fine pattern as well. # One way to mitigate this effect is to make the upper member narrower and the strut wider. # This reduces the amount of torsion the upper member can deliver and also makes the strut stiffer, # and less amenable to buckling. HourGlass("NPR1.pdf",1.5*inch,1.9*inch,1.0*inch,1.0*inch,0.125*inch) HourGlass("NPR2.pdf",0.75*inch,0.95*inch,0.5*inch,0.5*inch,0.0625*inch) HourGlass("NPR3.pdf",0.375*inch,0.475*inch,0.25*inch,0.25*inch,0.03125*inch) HourGlass("hourglass11.pdf",1.5*inch,1.9*inch,0.85*inch,1.7*inch,0.125*inch) HourGlass("hourglass21.pdf",1.5*inch,1.9*inch,0.85*inch,1.6*inch,0.25*inch) # Wider strut HourGlass("hourglass22.pdf",0.75*inch,0.95*inch,0.425*inch,0.8*inch,0.125*inch) # Wider strut HourGlass("hourglass23.pdf",0.375*inch,0.475*inch,0.2125*inch,0.4*inch,0.0625*inch) # Wider strut HourGlass("hourglass23a.pdf",0.375*inch,0.475*inch,0.2*inch,0.39*inch,0.0625*inch) # Wider strut #=========================================================================================================== #=========================================================================================================== # The base pattern looks like a modified Klingon Insignia. http://en.wikipedia.org/wiki/Klingon # The triangle base is defined in terms of its bezier parameters. # Copies of the base are rotated and translated to create the Triangles sides. # The triangle is then translated to its center # Finally it is rotated theta degrees and translated to its offset position #=========================================================================================================== def KlingonTriangle(x,y,tWidth,theta,r1=0.25,r2=-0.25,r3=0.75,r4=0.25): tHeight = sqrt(3) * tWidth/2.0 #triangle height = square root(3) the width of an equilateral triangle # Base of "unit" figure at origin is expressed in terms of "bezier points" base = [[2,r1*tWidth,r2*tWidth],[2,r3*tWidth,r4*tWidth],[2,tWidth,0.0]] # The first element is "moveTo origin", next we append the 3 points which define the base, followed by # rotate/translate copies of base to create right (120 degrees) and left (-120 degrees) sides of figure # and finally a "close" command figure = [[0,0.0,0.0]] + base + translate(tWidth, 0.0, rotate( 2.0*pi/3.0,base)) + \ translate(tWidth/2.0,tHeight,rotate(-2.0*pi/3.0,base)) + [[3,0.0,0.0]] # To rotate the figure correctly, we first move the figure origin to its center of mass (which is # 1/3 up the median). we then rotate the specified theta. Finally, translate to the specified position. return translate(x,y,rotate(theta,translate(-tWidth/2.0,-tHeight/3.0,figure))) #=========================================================================================================== # Determining the appropriate "tile" to use took a while to figure out. # We tile the figure in a hexaagonal pattern using the the twelve points a-L below. # Assuming the hexagons have unit sides, the x,y co-ordinates of each repeated corner is shown below, # together with the rotation angle of the figure. # corner X Y Theta Theta in Radians # o a - 0, 0 30 degrees 1 pi/6 # o b - sqrt(3)/2, 1/2 90 degrees 3 pi/6 # o c - sqrt(3)/2, 3/2 150 degrees 5 pi/6 # o d o - 0, 2 210 degrees 7 pi/6 # o e o - 0, 3 270 degrees 9 pi/6 # o f - sqrt(3)/2, 7/2 330 degrees 11 pi/6 # o g - sqrt(3)/2, 9/2 30 degrees 1 pi/6 Note: Theta same as "a", but x position is shifted # o h o - 0, 5 90 degrees 3 pi/6 # o i o - 0, 6 150 degrees 5 pi/6 # o j - sqrt(3)/2, 13/2 210 degrees 7 pi/6 # o k - sqrt(3)/2, 15/2 270 degrees 9 pi/6 # o L o - 0, 8 330 degrees 11 pi/6 # # The design is parametrized by: # o tWidth - the center to center spacing of the triangles # o scale - the amount each triangle is "scaled" in its cell # o p - the bezier parameters of the curve. # - the default bezier is probably too "sharp" where it forms the circle #=========================================================================================================== def Klingon(filename, tWidth, #triangle scale, p = [0.25,-0.25,0.65,0.55], xOrigin=0.5*inch,yOrigin=11.0*inch): c = canvas.Canvas(filename) c.drawString(0,11.5*inch,"Klingon NPR Mask  S. Paseman tWidth:%2.2f scale:%2.2f p:%s time: %s" % \ (tWidth, scale, ["%2.2f" % v for v in p], strftime("%Y-%m-%d %H:%M:%S"))) x1 = tWidth * sqrt(3)/2.0 scale = tWidth * scale cellHeight = 9*tWidth cellWidth = sqrt(3)*tWidth xCount = int(8.5*inch/cellWidth) yCount = int(11.0*inch/cellHeight) for ix in range(xCount): x = xOrigin+ix*cellWidth for iy in range(yCount): y = yOrigin -(iy*cellHeight) draw(c,KlingonTriangle(x ,y ,scale, - 1.0*pi/6.0,*p)) draw(c,KlingonTriangle(x+x1,y- 1.0*tWidth/2.0,scale, - 3.0*pi/6.0,*p)) draw(c,KlingonTriangle(x+x1,y- 3.0*tWidth/2.0,scale, - 5.0*pi/6.0,*p)) draw(c,KlingonTriangle(x ,y- 4.0*tWidth/2.0,scale, - 7.0*pi/6.0,*p)) draw(c,KlingonTriangle(x ,y- 6.0*tWidth/2.0,scale, - 9.0*pi/6.0,*p)) draw(c,KlingonTriangle(x+x1,y- 7.0*tWidth/2.0,scale, -11.0*pi/6.0,*p)) draw(c,KlingonTriangle(x+x1,y- 9.0*tWidth/2.0,scale, - 1.0*pi/6.0,*p)) draw(c,KlingonTriangle(x ,y-10.0*tWidth/2.0,scale, - 3.0*pi/6.0,*p)) draw(c,KlingonTriangle(x ,y-12.0*tWidth/2.0,scale, - 5.0*pi/6.0,*p)) draw(c,KlingonTriangle(x+x1,y-13.0*tWidth/2.0,scale, - 7.0*pi/6.0,*p)) draw(c,KlingonTriangle(x+x1,y-15.0*tWidth/2.0,scale, - 9.0*pi/6.0,*p)) draw(c,KlingonTriangle(x ,y-16.0*tWidth/2.0,scale, -11.0*pi/6.0,*p)) c.showPage() c.save() #=========================================================================================================== #=========================================================================================================== if __name__ == '__main__': #HourGlassIt() Klingon("klingon_5.pdf",0.5*inch,1.3) #Klingon("klingon_25.pdf",0.25*inch,1.3) Klingon("klingon_125.pdf",0.125*inch,1.1)