# -*- python -*-
# The first line tells emacs to use python mode for editing


## It would be nice to separate the cadletters functions out into an
## importable module, but I haven't yet worked out the scope issues
## related to accessing things inside the base cad.py code.
## When this is figured out, a *.cad file would need to do the
## import in a manner like the following:
#import sys
#if "." not in sys.path:
#   sys.path.append(".")
#import cadletters




def circle(x0, y0, r):
   part = "(((X-x0)**2 + (Y-y0)**2) <= r**2)"
   part = replace(part,'x0',str(x0))
   part = replace(part,'y0',str(y0))
   part = replace(part,'r',str(r))
   return part

def rectangle(x0, x1, y0, y1):
   part = "((X >= x0) & (X <= x1) & (Y >= y0) & (Y <= y1))"
   part = replace(part,'x0',str(x0))
   part = replace(part,'x1',str(x1))
   part = replace(part,'y0',str(y0))
   part = replace(part,'y1',str(y1))
   return part

def add(part1, part2):
   part = "part1 | part2"
   part = replace(part,'part1',part1)
   part = replace(part,'part2',part2)
   return part

def subtract(part1, part2):
   part = "(part1) & ~(part2)"
   part = replace(part,'part1',part1)
   part = replace(part,'part2',part2)
   return part

def move(part,dx,dy):
   part = replace(part,'X','(X-'+str(dx)+')')
   part = replace(part,'Y','(Y-'+str(dy)+')')
   return part   

def translate(part,dx,dy,dz):
   part = replace(part,'X','(X-'+str(dx)+')')
   part = replace(part,'Y','(Y-'+str(dy)+')')
   part = replace(part,'Z','(Z-'+str(dz)+')')
   return part   

def rotate(part, angle):
   angle = angle*pi/180
   part = replace(part,'X','(cos(angle)*X+sin(angle)*y)')
   part = replace(part,'Y','(-sin(angle)*X+cos(angle)*y)')
   part = replace(part,'y','Y')
   part = replace(part,'angle',str(angle))
   return part

def rotate_90(part):
   part = reflect_xy(part)
   part = reflect_y(part)
   return part

def rotate_180(part):
   part = reflect_xy(part)
   part = reflect_y(part)
   part = reflect_xy(part)
   part = reflect_y(part)
   return part

def rotate_270(part):
   part = reflect_xy(part)
   part = reflect_y(part)
   part = reflect_xy(part)
   part = reflect_y(part)
   part = reflect_xy(part)
   part = reflect_y(part)
   return part

def reflect_x(part):
   part = replace(part,'X','-X')
   return part

def reflect_y(part):
   part = replace(part,'Y','-Y')
   return part

def reflect_z(part):
   part = replace(part,'Z','-Z')
   return part

def reflect_xy(part):
   part = replace(part,'X','temp')
   part = replace(part,'Y','X')
   part = replace(part,'temp','Y')
   return part

def reflect_xz(part):
   part = replace(part,'X','temp')
   part = replace(part,'Z','X')
   part = replace(part,'temp','Z')
   return part

def reflect_yz(part):
   part = replace(part,'Y','temp')
   part = replace(part,'Z','Y')
   part = replace(part,'temp','Z')
   return part

#
# PCB classes and definitions
#

cad.labels = []

class PCB:
   def __init__(self,x0,y0,width,height,mask):
      self.board = "False"
      self.interior = rectangle(x0,x0+width,y0,y0+height)
      self.exterior = subtract("True",rectangle(x0,x0+width,y0,y0+height))
      self.mask = "False"
   def add(self,part):
      self.board = add(self.board,part)
      self.mask = add(self.mask,move(part,-mask,mask))
      self.mask = add(self.mask,move(part,-mask,-mask))
      self.mask = add(self.mask,move(part,mask,mask))
      self.mask = add(self.mask,move(part,mask,-mask))
      return self

class part:
   def add(self,pcb,x,y,z=0,angle=0):
      self.x = x
      self.y = y
      self.z = z
      self.angle = angle
      if (angle == 90):
         self.shape = rotate_90(self.shape)
      elif (angle == 180):
         self.shape = rotate_180(self.shape)
      elif ((angle == 270) | (angle == -90)):
         self.shape = rotate_270(self.shape)
      angle = pi*angle/180
      self.shape = translate(self.shape,x,y,z)
      for i in range(len(self.pad)):
         xnew = cos(angle)*self.pad[i].x + sin(angle)*self.pad[i].y
         ynew = -sin(angle)*self.pad[i].x + cos(angle)*self.pad[i].y
         self.pad[i].x = x + xnew
         self.pad[i].y = y + ynew
         self.pad[i].z += z
      cad.labels.append(cad_text(x,y,z,self.value,size=14))
      for i in range(len(self.labels)):
         xnew = cos(angle)*self.labels[i].x + sin(angle)*self.labels[i].y
         ynew = -sin(angle)*self.labels[i].x + cos(angle)*self.labels[i].y
         self.labels[i].x = x + xnew
         self.labels[i].y = y + ynew
         self.labels[i].z += z
         cad.labels.append(self.labels[i])
      pcb = pcb.add(self.shape)
      return pcb



def rotateToMatchVector(part,vecx,vecy):
   # part will be rotated theta degrees, where theta is the angle
   # between the supplied vector and the x axis.
   # Equivalent to drawing the vector [1,0], and then rotating both the
   # part and the [1,0] vector until it aligns with the supplied vector.
   length = sqrt(vecx*vecx + vecy*vecy)
   if (length == 0):
      return part
   part = replace(part,'X','((COSANGLE)*X+(SINANGLE)*y)')
   part = replace(part,'Y','(-(SINANGLE)*X+(COSANGLE)*y)')
   part = replace(part,'y','Y')
   part = replace(part,'COSANGLE',str(vecx/length))
   part = replace(part,'SINANGLE',str(vecy/length))
   return part

def lineWithThickness(x0,y0,x1,y1,w):
   length = sqrt((y1-y0)*(y1-y0) + (x1-x0)*(x1-x0))
   line = rectangle(0,length,-w/2,w/2)
   line = rotateToMatchVector(line,x1-x0,y1-y0)
   line = move(line,x0,y0)
   return line

def getPointOnLine(point0,point1,distance):
   # returns a point which is the specified distance away from point0,
   # in the direction from point0 to point1.
   # Currently ignores z coordinate
   x0 = point0.x
   y0 = point0.y
   z0 = point0.z
   x1 = point1.x
   y1 = point1.y
   z1 = point1.z
   length = sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0))
   xdiff = x1-x0
   ydiff = y1-y0
   x = xdiff*distance/length
   y = ydiff*distance/length
   x = x + x0
   y = y + y0
   return point(x,y,z0)
   
def getPointInDirection(point0,angle,distance):
   # returns a point which is the specified distance away from point0,
   # in the direction specified by the angle.
   # Currently ignores z coordinate
   angle = positiveAngle(angle)
   angle = angle*pi/180
   x0 = point0.x
   y0 = point0.y
   z0 = point0.z
   x = distance*cos(angle)
   y = distance*sin(angle)
   x = x + x0
   y = y + y0
   return point(x,y,z0)   

def arc(centerX,centerY,innerRad,outerRad,startAngle,endAngle):
   # Creates an arc formed by concentric curves.
   # outerRad must be greater than innerRad.
   # startAngle must be less than endAngle
   # Both angles are specified in degrees (CCW from the x axis).
   # endAngle - startAngle must be less than or equal to 180
   outer = circle(0,0,outerRad)
   inner = circle(0,0,innerRad)
   block1 = rectangle(-outerRad,outerRad,-outerRad,0)
   block2 = rectangle(-outerRad,outerRad,0,outerRad)
   block1 = rotate(block1,startAngle)
   block2 = rotate(block2,endAngle)
   part = subtract(outer,inner)
   part = subtract(part,block1)
   part = subtract(part,block2)
   part = move(part,centerX,centerY)
   return part

def positiveAngle(angle):
   # Remaps angle (in degrees) to a number in the range 0 to 360.
   return (angle - 360 * floor(angle/360))

def polyline(width,cornerRadius,*points):
   # Creates one or more line segments, sequentially connecting the points in the list.
   # Corners will be rounded, with the inner radius of the curve equal to the specified
   # cornerRadius, and the outer radius equal to inner radius plus the width of the
   # line.
   # width must be greater than zero.
   # cornerRadius must be greater than or equal to zero.
   part = ""

   # Store the end of the previous line, truncated to allow for arc
   previousEndPoint = None

   # Store the angle of the previous line
   previousLineAngle = None
   
   for i in range(1,len(points)):
      x0 = points[i-1].x
      y0 = points[i-1].y
      z0 = points[i-1].z
      x1 = points[i].x
      y1 = points[i].y
      z1 = points[i].z

      # omitFromStart and omitFromEnd are given positive numbers in order to shrink the trace
      # the appropriate amount to allow for curved corners, and given negative numbers in order
      # to extend the trace a half-wdith past the specified terminus point.
      
      if (i==1):
         omitFromStart = -width/2
      else:
         omitFromStart = cornerRadius + width/2
      if (i == len(points)-1):
         omitFromEnd = -width/2
      else:
         omitFromEnd = cornerRadius + width/2
      startPoint = getPointOnLine(points[i-1],points[i],omitFromStart)
      endPoint = getPointOnLine(points[i],points[i-1],omitFromEnd)
      if (i==1):
         part = lineWithThickness(startPoint.x,startPoint.y,endPoint.x,endPoint.y,width)
      else:
         part = add(part,lineWithThickness(startPoint.x,startPoint.y,endPoint.x,endPoint.y,width))
      nextLineAngle = (math.atan2(y1-y0,x1-x0))*180/pi
      if (previousLineAngle != None):
         nextLineAngle = positiveAngle(nextLineAngle)
         previousLineAngle = positiveAngle(previousLineAngle)
         if (positiveAngle(nextLineAngle - previousLineAngle) < 180):
            # Left turn
            arcCenter = getPointInDirection(previousEndPoint,previousLineAngle+90,cornerRadius + width/2)
            startAngle = previousLineAngle-90
            endAngle = nextLineAngle-90
         else:
            # Right turn
            arcCenter = getPointInDirection(previousEndPoint,previousLineAngle-90,cornerRadius + width/2)
            startAngle = nextLineAngle+90
            endAngle = previousLineAngle+90
         part = add(part,arc(arcCenter.x,arcCenter.y,cornerRadius,cornerRadius+width,startAngle,endAngle))
      previousLineAngle = nextLineAngle
      previousEndPoint = endPoint
      
   return part

def wire(pcb,width,*points):
   # The following logic adds intermediate points in order to avoid diagonals, and then calls
   # the polyline function, specifying the width of the trace as the inner radius of curvature
   # for corners
   newpoints = [points[0]]
   for i in range(1,len(points)):
      x0 = points[i-1].x
      y0 = points[i-1].y
      z0 = points[i-1].z
      x1 = points[i].x
      y1 = points[i].y
      z1 = points[i].z
      if (x0 != x1 and y0 != y1):
         newpoints.append(point(x1,y0,z0))
      newpoints.append(point(x1,y1,z0))
   pcb.board = add(pcb.board,polyline(width,width,*newpoints))
   return pcb
   

class letter(part):
   def __init__(self,w,letter):
      self.value = ''
      self.pad = [point(0,0,0)]
      self.labels = []
      self.shape = ''
      
      H = w*7
      h = w*5
      W = w*3

      self.width = W+2*w
      self.height = H+2*w

      # p0     p1
      # p2     p3
      # p4     p5
      # p6 p9  p7 p8

      # Layout of points in characters:
      #
      # p12 p13   p14 p15     H
      # p8  p9    p10 p11     h
      # p4  p5    p6  p7      h-2*w
      # p0  p1    p2  p3      0
      # p16 p17   p18 p19     -2*w
      #
      # 0   w     W   W+w

      self.p0 = point(0, 0, 0)
      self.p1 = point(w, 0, 0)
      self.p2 = point(W, 0, 0)
      self.p3 = point(W+w, 0, 0)

      self.p4 = point(0, h-2*w, 0)
      self.p5 = point(w, h-2*w, 0)
      self.p6 = point(W, h-2*w, 0)
      self.p7 = point(W+w, h-2*w, 0)

      self.p8 = point(0, h, 0)
      self.p9 = point(w, h, 0)
      self.p10 = point(W, h, 0)
      self.p11 = point(W+w, h, 0)

      self.p12 = point(0, H, 0)
      self.p13 = point(w, H, 0)
      self.p14 = point(W, H, 0)
      self.p15 = point(W+w, H, 0)

      self.p16 = point(0, -2*w, 0)
      self.p17 = point(w, -2*w, 0)
      self.p18 = point(W, -2*w, 0)
      self.p19 = point(W+w, -2*w, 0)

      self.w2 = W
      self.h2 = H
      self.h1 = h
      self.w1 = w

      # The eval call was a cute way to avoid the work of making a
      # mapping from input characters to corresponding implementation
      # methods, but it doesn't play nicely with non-alphabetic or
      # unimplemented characters.
      ## eval('self.' + letter + '()')


      funcmap = { "a": self.a, "b": self.b, "c": self.c, "d": self.d,
         "e": self.e, "f": self.f, "g": self.g, "h": self.h, "i":
         self.i, "j": self.j, "k": self.k, "l": self.l, "m": self.m,
         "n": self.n, "o": self.o, "p": self.p, "q": self.q, "r":
         self.r, "s": self.s, "t": self.t, "u": self.u, "v": self.v,
         "w": self.w, "x": self.x, "y": self.y, "z": self.z, " ":
         self.space, "_": self.underscore, ".": self.dot, ",": self.comma}
      
      funcmap.get(letter,self.errhandler)()

   def errhandler(self):
      # Unimplemented character.
      self.underscore()

   def space(self):
      self.shape = 'False'
      self.width = 2*self.w1

   def underscore(self):
      self.shape = polyline(self.w1, self.w1, self.p0, self.p2)

   def dot(self):
      self.shape = polyline(self.w1, self.w1, self.p0, self.p1)
      
   def comma(self):
      self.shape = polyline(self.w1, self.w1, self.p0, self.p1)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p1, self.p16))
      
   def a(self):
      self.o()
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p10, self.p2, self.p3))

   def b(self):
      self.o()
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p12, self.p0))

   def c(self):
      self.shape = polyline(self.w1, self.w1, self.p6, self.p10, self.p8, self.p0, self.p2)

   def d(self):
      self.o()
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p14, self.p2, self.p3))

   def e(self):
      self.c()
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p4, self.p6))

   def f(self):
      self.shape = polyline(self.w1, self.w1, self.p14, self.p13, self.p1)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p8, self.p10))
      
   def g(self):
      self.o()
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p10, self.p18, self.p16))

   def h(self):
      self.shape = polyline(self.w1, self.w1, self.p12, self.p0)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p0, self.p8, self.p10, self.p2))
      
   def i(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p0, self.p1)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p12, self.p12))
      self.width = 3*self.w1

   def j(self):
      self.shape = polyline(self.w1, self.w1, self.p9, self.p17, self.p16)
      self.width = 3*self.w1
      
   def k(self):
      self.shape = polyline(self.w1, self.w1, self.p12, self.p0)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p4, self.p2))
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p4, self.p10))
      
   def l(self):
      self.shape = polyline(self.w1, self.w1, self.p12, self.p0, self.p1)
      self.width = 3*self.w1

   def m(self):
      midhigh = point(2*self.w1, self.h1, 0)
      midlow = point(2*self.w1, 0, 0)
      self.shape = polyline(self.w1, self.w1, self.p8, self.p0)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p0, self.p8, midhigh, midlow))
      self.shape = add(self.shape, polyline(self.w1, self.w1, midlow, midhigh, self.p11, self.p3))
      self.width = self.width + self.w1

   def n(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p0)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p0, self.p8, self.p10, self.p2))

   def o(self):
      self.shape = polyline(self.w1,self.w1,self.p6,self.p10,self.p8,self.p0,self.p2,self.p6)

   def p(self):
      self.o()
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p8, self.p16))
      
   def q(self):
      self.o()
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p10, self.p18, self.p19))

   def r(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p0)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p0, self.p8, self.p10, self.p6))
      
   def s(self):
      self.shape = polyline(self.w1, self.w1, self.p10, self.p8, self.p4, self.p6, self.p2, self.p0)
      
   def t(self):
      self.shape = polyline(self.w1, self.w1, self.p9, self.p1, self.p2)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p8, self.p10))

   def u(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p0, self.p2, self.p10)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p10, self.p2, self.p3))

   def v(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p1)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p1, self.p6))
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p6, self.p10))
      
   def w(self):
      midhigh = point(2*self.w1, self.h1, 0)
      self.shape = polyline(self.w1, self.w1, self.p8, self.p0)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p0, midhigh))
      self.shape = add(self.shape, polyline(self.w1, self.w1, midhigh, self.p2))
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p2, self.p11))
      self.width = self.width + self.w1
      
   def x(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p2)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p0, self.p10))
      
   def y(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p1, self.p2)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p10, self.p18, self.p17))
      
   def z(self):
      self.shape = polyline(self.w1, self.w1, self.p8, self.p10)
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p10, self.p0))
      self.shape = add(self.shape, polyline(self.w1, self.w1, self.p0, self.p2))


      
class word(part):
   def __init__(self,w,wordstring):
      self.value = ''
      self.pad = [point(0,0,0)]
      self.labels = []
      self.shape = 'False'
      offsetX = 0
      maxheight = 0

      for character in wordstring:
         letterObject = letter(w, character)
         letterObject.shape = move(letterObject.shape,offsetX,0)
         self.shape = add(self.shape, letterObject.shape)
         offsetX = offsetX + letterObject.width
         if letterObject.height > maxheight:
            maxheight = letterObject.height
         
      self.width = offsetX
      self.height = maxheight


#
# define board
#

x0 = 1
y0 = 1
width = 1.4
height = .74
z = -.005
w = .018
mask = .004

pcb = PCB(x0,y0,width,height,mask)

WORD1 = word(w,'abcdefghijk')
WORD2 = word(w,'lmnopqrstuv')
WORD3 = word(w,'wxyz _.,')
pcb = WORD1.add(pcb,x0+w*2,y0+0.55,z)
pcb = WORD2.add(pcb,WORD1.x,WORD1.y-WORD1.height,z)
pcb = WORD3.add(pcb,WORD2.x,WORD2.y-WORD2.height,z)

#
# assign board and exterior to cad.function for milling
#
# use single-sided FR2
# use 1/64 end-mill
# set tool diameter = .0156
# set xy, z speed = 4
# set # contours = -1
#

cad.function = add(pcb.board,pcb.exterior)

#
# uncomment to export traces
#

#cad.function = pcb.board

#
# uncomment to export exterior
#

#cad.function = pcb.exterior

#
# uncomment to export solder mask
#

#cad.function = pcb.mask

#
# uncomment to mill out board
#
# use 1/32 end-mill
# set tool diameter = .0312
# set xy, z speed = .5
# set # contours = 1
#

#cad.function = pcb.interior
#z = -.065

#
# define limits and parameters
#

d = 0.05
cad.xmin = x0-d # min x to render
cad.xmax = x0+width+d # max x to render
cad.ymin = y0-d # min y to render
cad.ymax = y0+height+d # max y to render
cad.zmin = z
cad.zmax = .050
dpi = 500 # low resolution for previewing
#dpi = 500 # high resolution for machining
nxy = int(dpi*(cad.xmax-cad.xmin))
cad.nx = int(dpi*(cad.xmax-cad.xmin)) # x points to render
cad.ny = int(dpi*(cad.ymax-cad.ymin)) # y points to render
cad.nz = 1
cad.inches_per_unit = 1.0 # use inch units
cad.view('xy') # 2D view