#!/usr/bin/env python """ Define and manage aperture macros (%AM command). Currently, only macros without replaceable parameters (e.g., $1, $2, etc.) are supported. -------------------------------------------------------------------- This program is licensed under the GNU General Public License (GPL) Version 3. See http://www.fsf.org for details of the license. Rugged Circuits LLC http://ruggedcircuits.com/gerbmerge """ import sys import re import string import copy import config _macro_pat = re.compile(r'^%AM([^*]+)\*$') # This list stores the expected types of parameters for each primitive type # (e.g., outline, line, circle, polygon, etc.). None is used for undefined # primitives. Each entry corresponds to the defined primitive code, and # comprises a tuple of conversion functions (i.e., built-in int() and float() # functions) that apply to all parameters AFTER the primitive code. For example, # code 1 (circle) may be instantiated as: # 1,1,0.025,0.0,0.0 # (the parameters are code, exposure type, diameter, X center, Y center). # After the integer code, we expect an int for exposure type, then floats # for the remaining three parameters. Thus, the entry for code 1 is # (int, float, float, float). PrimitiveParmTypes = ( None, # Code 0 -- undefined (int, float, float, float), # Code 1 -- circle (int, float, float, float, float, float, float), # Code 2 -- line (vector) None, # Code 3 -- end-of-file for .DES files (int, int, float, float, float, float, float), # Code 4 -- outline...takes any number of additional floats (int, int, float, float, float, float), # Code 5 -- regular polygon (float, float, float, float, float, int, float, float, float), # Code 6 -- moire (float, float, float, float, float, float), # Code 7 -- thermal None, # Code 8 -- undefined None, # Code 9 -- undefined None, # Code 10 -- undefined None, # Code 11 -- undefined None, # Code 12 -- undefined None, # Code 13 -- undefined None, # Code 14 -- undefined None, # Code 15 -- undefined None, # Code 16 -- undefined None, # Code 17 -- undefined None, # Code 18 -- undefined None, # Code 19 -- undefined (int, float, float, float, float, float), # Code 20 -- line (vector)...alias for code 2 (int, float, float, float, float, float), # Code 21 -- line (center) (int, float, float, float, float, float) # Code 22 -- line (lower-left) ) def rotatexy(x,y): # Rotate point (x,y) counterclockwise 90 degrees about the origin return (-y,x) def rotatexypair(L, flip, ix): if (flip == 1): # flip horizontally L[ix],L[ix+1] = (-L[ix], L[ix+1]) elif (flip == -1): # flip vertically L[ix],L[ix+1] = (L[ix], -L[ix+1]) else: # Rotate list items L[ix],L[ix+1] by 90 degrees L[ix],L[ix+1] = rotatexy(L[ix],L[ix+1]) def swapxypair(L, ix): # Swap two list elements L[ix],L[ix+1] = L[ix+1],L[ix] def rotatetheta(th): # Increase angle th in degrees by +90 degrees (counterclockwise). # Handle modulo 360 issues th += 90 if th >= 360: th -= 360 return th def rotatethelem(L, flip, ix): # Increase angle th by +90 degrees for a list element if (flip == 0): L[ix] = rotatetheta(L[ix]) class ApertureMacroPrimitive: def __init__(self, code=-1, fields=None): self.code = code self.parms = [] if fields is not None: self.setFromFields(code, fields) def setFromFields(self, code, fields): # code is an integer describing the primitive type, and fields is # a list of STRINGS for each parameter self.code = code # valids will be one of the PrimitiveParmTypes tuples above. Some are # None to indicate illegal codes. We also set valids to None to indicate # the macro primitive code is outside the range of known types. try: valids = PrimitiveParmTypes[code] except: valids = None if valids is None: raise RuntimeError('Undefined aperture macro primitive code %d' % code) # We expect exactly the number of fields required, except for macro # type 4 which is an outline and has a variable number of points. # For outlines, the second parameter indicates the number of points, # each of which has an (X,Y) co-ordinate. Thus, we expect an Outline # specification to have 1+1+2+2*N+1=5+2N fields: # - first field is exposure # - second field is number of points # - thrid and forth fields is for the initial point # - 2*N fields for X,Y points # - last field is rotation if self.code==4: if len(fields) < 2: raise RuntimeError('Outline macro primitive has way too few fields') try: N = int(fields[1]) except: raise RuntimeError('Outline macro primitive has non-integer number of points') if len(fields) != (5+2*N): raise RuntimeError('Outline macro primitive has %d fields...expecting %d fields' % (len(fields), 3+2*N)) else: if len(fields) != len(valids): raise RuntimeError('Macro primitive has %d fields...expecting %d fields' % (len(fields), len(valids))) # Convert each parameter on the input line to an entry in the self.parms # list, using either int() or float() conversion. for parmix in range(len(fields)): try: converter = valids[parmix] except: converter = float # To handle variable number of points in Outline type try: self.parms.append(converter(fields[parmix])) except: raise RuntimeError('Aperture macro primitive parameter %d has incorrect type' % (parmix+1)) def setFromLine(self, line): # Account for DOS line endings and get rid of line ending and '*' at the end line = line.replace('\x0D', '') line = line.rstrip() line = line.rstrip('*') fields = line.split(',') try: try: code = int(fields[0]) except: raise RuntimeError('Illegal aperture macro primitive code "%s"' % fields[0]) self.setFromFields(code, fields[1:]) except: print('='*20) print("==> ", line) print('='*20) raise def rotate(self, flip): if self.code == 1: # Circle: [andreika]: FIX circle rotation rotatexypair(self.parms, flip, 2) elif self.code in (2,20): # Line (vector): fields (2,3) and (4,5) must be rotated, no need to # rotate field 6 rotatexypair(self.parms, flip, 2) rotatexypair(self.parms, flip, 4) elif self.code == 21: # Line (center): fields (3,4) must be rotated, and field 5 incremented by +90 rotatexypair(self.parms, flip, 3) rotatethelem(self.parms, flip, 5) elif self.code == 22: # Line (lower-left): fields (3,4) must be rotated, and field 5 incremented by +90 rotatexypair(self.parms, flip, 3) rotatethelem(self.parms, flip, 5) elif self.code == 4: # Outline: fields (2,3), (4,5), etc. must be rotated, the last field need not be incremented ix = 2 for pts in range(self.parms[1]): # parms[1] is the number of points rotatexypair(self.parms, flip, ix) ix += 2 #rotatethelem(self.parms, ix) elif self.code == 5: # Polygon: fields (2,3) must be rotated, and field 5 incremented by +90 rotatexypair(self.parms, flip, 2) rotatethelem(self.parms, flip, 5) elif self.code == 6: # Moire: fields (0,1) must be rotated, and field 8 incremented by +90 rotatexypair(self.parms, flip, 0) rotatethelem(self.parms, flip, 8) elif self.code == 7: # Thermal: fields (0,1) must be rotated, and field 5 incremented by +90 rotatexypair(self.parms, flip, 0) rotatethelem(self.parms, flip, 5) def __str__(self): # Construct a string with ints as ints and floats as floats s = '%d' % self.code for parmix in range(len(self.parms)): valids = PrimitiveParmTypes[self.code] format = ',%f' try: if valids[parmix] is int: format = ',%d' except: pass # '%f' is OK for Outline extra points s += format % self.parms[parmix] return s def writeDef(self, fid): fid.write('%s*\n' % str(self)) class ApertureMacro: def __init__(self, name): self.name = name self.prim = [] def add(self, prim): self.prim.append(prim) def rotate(self, flip): for prim in self.prim: prim.rotate(flip) def rotated(self, flip): # Return copy of ourselves, rotated. Replace 'R' as the first letter of the # macro name. We don't append because we like to be able to count the # number of aperture macros by stripping off the leading character. M = copy.deepcopy(self) M.rotate(flip) if (flip == 1): M.name = 'FH'+M.name[1:] elif (flip == -1): M.name = 'FV'+M.name[1:] else: M.name = 'R'+M.name[1:] return M def dump(self, fid=sys.stdout): fid.write(str(self)) def __str__(self): s = '%s:\n' % self.name s += self.hash() return s def hash(self): s = '' for prim in self.prim: s += ' '+str(prim)+'\n' return s def writeDef(self, fid): fid.write('%%AM%s*\n' % self.name) for prim in self.prim: prim.writeDef(fid) fid.write('%\n') def parseApertureMacro(s, fid): match = _macro_pat.match(s) if match: name = match.group(1) M = ApertureMacro(name) for line in fid: if line[0]=='%': return M P = ApertureMacroPrimitive() P.setFromLine(line) M.add(P) else: raise RuntimeError("Premature end-of-file while parsing aperture macro") else: return None # This function adds the new aperture macro AM to the global aperture macro # table. The return value is the modified macro (name modified to be its global # name). macro. def addToApertureMacroTable(AM): GAMT = config.GAMT # Must sort keys by integer value, not string since 99 comes before 100 # as an integer but not a string. keys = list(map(int, [K[1:] for K in list(GAMT.keys())])) keys.sort() if len(keys): lastCode = keys[-1] else: lastCode = 0 mcode = 'M%d' % (lastCode+1) AM.name = mcode GAMT[mcode] = AM return AM if __name__=="__main__": # Create a funky aperture macro with all the fixins, and make sure # it rotates properly. M = ApertureMacro('TEST') # X and Y axes M.add(ApertureMacroPrimitive(2, ('1', '0.0025', '0.0', '-0.1', '0.0', '0.1', '0.0'))) M.add(ApertureMacroPrimitive(2, ('1', '0.0025', '0.0', '-0.1', '0.0', '0.1', '90.0'))) # A circle in the top-right quadrant, touching the axes M.add(ApertureMacroPrimitive(1, ('1', '0.02', '0.01', '0.01'))) # A line of slope -1 centered on the above circle, of thickness 5mil, length 0.05 M.add(ApertureMacroPrimitive(2, ('1', '0.005', '0.0', '0.02', '0.02', '0.0', '0.0'))) # A narrow vertical rectangle centered on the circle of width 2.5mil M.add(ApertureMacroPrimitive(21, ('1', '0.0025', '0.03', '0.01', '0.01', '0.0'))) # A 45-degree line in the third quadrant, not quite touching the origin M.add(ApertureMacroPrimitive(22, ('1', '0.02', '0.01', '-0.03', '-0.03', '45'))) # A right triangle in the second quadrant M.add(ApertureMacroPrimitive(4, ('1', '4', '-0.03', '0.01', '-0.03', '0.03', '-0.01', '0.01', '-0.03', '0.01', '0.0'))) # A pentagon in the fourth quadrant, rotated by 15 degrees M.add(ApertureMacroPrimitive(5, ('1', '5', '0.03', '-0.03', '0.02', '15'))) # A moire in the first quadrant, beyond the circle, with 2 annuli M.add(ApertureMacroPrimitive(6, ('0.07', '0.07', '0.04', '0.005', '0.01', '2', '0.005', '0.04', '0.0'))) # A thermal in the second quadrant, beyond the right triangle M.add(ApertureMacroPrimitive(7, ('-0.07', '0.07', '0.03', '0.02', '0.005', '15'))) MR = M.rotated(0) # Generate the Gerber so we can view it fid = open('amacro.ger', 'wt') print("""G75* G70* %OFA0B0*% %FSLAX24Y24*% %IPPOS*% %LPD*%""", file=fid) M.writeDef(fid) MR.writeDef(fid) print("""%ADD10TEST*% %ADD11TESTR*% D10* X010000Y010000D03* D11* X015000Y010000D03* M02*""", file=fid) fid.close() print(M) print(MR)