#!/usr/bin/env python """ Manage apertures, read aperture table, etc. -------------------------------------------------------------------- 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 config import amacro import util # Recognized apertures and re pattern that matches its definition Thermals and # annuli are generated using macros (see the eagle.def file) but only on inner # layers. Octagons are also generated as macros (%AMOC8) but we handle these # specially as the Eagle macro uses a replaceable macro parameter ($1) and # GerbMerge doesn't handle these yet...only fixed macros (no parameters) are # currently supported. Apertures = ( ('Rectangle', re.compile(r'^%AD(D\d+)R,([^X]+)X([^*]+)\*%$'), '%%AD%sR,%.5fX%.5f*%%\n'), ('Circle', re.compile(r'^%AD(D\d+)C,([^*]+)\*%$'), '%%AD%sC,%.5f*%%\n'), ('Oval', re.compile(r'^%AD(D\d+)O,([^X]+)X([^*]+)\*%$'), '%%AD%sO,%.5fX%.5f*%%\n'), ('Octagon', re.compile(r'^%AD(D\d+)OC8,([^*]+)\*%$'), '%%AD%sOC8,%.5f*%%\n'), # Specific to Eagle ('Macro', re.compile(r'^%AD(D\d+)([^*]+)\*%$'), '%%AD%s%s*%%\n') ) # This loop defines names in this module like 'Rectangle', # which are element 0 of the Apertures list above. So code # will be like: # import aptable # A = aptable.Aperture(aptable.Rectangle, ......) for ap in Apertures: globals()[ap[0]] = ap class Aperture: def __init__(self, aptype, code, dimx, dimy=None): assert aptype in Apertures self.apname, self.pat, self.format = aptype self.code = code self.dimx = dimx # Macro name for Macro apertures self.dimy = dimy # None for Macro apertures if self.apname in ('Circle', 'Octagon', 'Macro'): assert (dimy is None) def isRectangle(self): return self.apname == 'Rectangle' def rectangleAsRect(self, X, Y): """Return a 4-tuple (minx,miny,maxx,maxy) describing the area covered by this Rectangle aperture when flashed at center co-ordinates (X,Y)""" dx = util.in2gerb(self.dimx) dy = util.in2gerb(self.dimy) if dx & 1: # Odd-sized: X extents are (dx+1)/2 on the left and (dx-1)/2 on the right xm = (dx+1)/2 xp = xm-1 else: # Even-sized: X extents are X-dx/2 and X+dx/2 xm = xp = dx/2 if dy & 1: # Odd-sized: Y extents are (dy+1)/2 below and (dy-1)/2 above ym = (dy+1)/2 yp = ym-1 else: # Even-sized: Y extents are Y-dy/2 and Y+dy/2 ym = yp = dy/2 return (X-xm, Y-ym, X+xp, Y+yp) def getAdjusted(self, minimum): """ Adjust aperture properties to conform to minimum feature dimensions Return new aperture if required, else return False """ dimx = dimy = None # Check for X and Y dimensions less than minimum if (self.dimx != None) and (self.dimx < minimum): dimx = minimum if (self.dimy != None) and (self.dimx < minimum): dimy = minimum # Return new aperture if needed if (dimx != None) or (dimy != None): if dimx==None: dimx=self.dimx if dimy==None: dimy=self.dimy return Aperture( (self.apname, self.pat, self.format), self.code, dimx, dimy ) else: return False ## no new aperture needs to be created def rotate(self, RevGAMT, flip): if self.apname in ('Macro',): # Construct a rotated macro, see if it's in the GAMT, and set self.dimx # to its name if so. If not, add the rotated macro to the GAMT and set # self.dimx to the new name. Recall that GAMT maps name to macro # (e.g., GAMT['M9'] = ApertureMacro(...)) while RevGAMT maps hash to # macro name (e.g., RevGAMT[hash] = 'M9') AMR = config.GAMT[self.dimx].rotated(flip) hash = AMR.hash() try: self.dimx = RevGAMT[hash] except KeyError: AMR = amacro.addToApertureMacroTable(AMR) # adds to GAMT and modifies name to global name self.dimx = RevGAMT[hash] = AMR.name elif self.dimy is not None: # Rectangles and Ovals have a dimy setting and need to be rotated if (flip == 0): t = self.dimx self.dimx = self.dimy self.dimy = t def rotated(self, RevGAMT, flip): # deepcopy doesn't work on re patterns for some reason so we copy ourselves manually APR = Aperture((self.apname, self.pat, self.format), self.code, self.dimx, self.dimy) APR.rotate(RevGAMT, flip) return APR def dump(self, fid=sys.stdout): fid.write(str(self)) def __str__(self): return '%s: %s' % (self.code, self.hash()) #if 0: # if self.dimy: # return ('%s: %s (%.4f x %.4f)' % (self.code, self.apname, self.dimx, self.dimy)) # else: # if self.apname in ('Macro'): # return ('%s: %s (%s)' % (self.code, self.apname, self.dimx)) # else: # return ('%s: %s (%.4f)' % (self.code, self.apname, self.dimx)) def hash(self): if self.dimy: return ('%s (%.5f x %.5f)' % (self.apname, self.dimx, self.dimy)) else: if self.apname in ('Macro',): return ('%s (%s)' % (self.apname, self.dimx)) else: return ('%s (%.5f)' % (self.apname, self.dimx)) def writeDef(self, fid): if self.dimy: fid.write(self.format % (self.code, self.dimx, self.dimy)) else: fid.write(self.format % (self.code, self.dimx)) # Parse the aperture definition in line 's'. macroNames is an aperture macro dictionary # that translates macro names local to this file to global names in the GAMT. We make # the translation right away so that the return value from this function is an aperture # definition with a global macro name, e.g., 'ADD10M5' def parseAperture(s, knownMacroNames): for ap in Apertures: match = ap[1].match(s) if match: dimy = None if ap[0] in ('Circle', 'Octagon', 'Macro'): code, dimx = match.groups() else: code, dimx, dimy = match.groups() if ap[0] in ('Macro',): if dimx in knownMacroNames: dimx = knownMacroNames[dimx] # dimx is now GLOBAL, permanent macro name (e.g., 'M2') else: raise RuntimeError('Aperture Macro name "%s" not defined' % dimx) else: try: dimx = float(dimx) if dimy: dimy = float(dimy) except: raise RuntimeError("Illegal floating point aperture size") return Aperture(ap, code, dimx, dimy) return None # This function returns a dictionary where each key is an # aperture code string (e.g., "D11") and the value is the # Aperture object that represents it. For example: # # %ADD12R,0.0630X0.0630*% # # from a Gerber file would result in the dictionary entry: # # "D12": Aperture(ap, 'D10', 0.063, 0.063) # # The input fileList is a list of pathnames which will be read to construct the # aperture table for a job. All the files in the given list will be so # examined, and a global aperture table will be constructed as a dictionary. # Same goes for the global aperture macro table. tool_pat = re.compile(r'^(?:G54)?D\d+\*$') def constructApertureTable(fileList): # First we construct a dictionary where each key is the # string representation of the aperture. Then we go back and assign # numbers. For aperture macros, we construct their final version # (i.e., 'M1', 'M2', etc.) right away, as they are parsed. Thus, # we translate from 'THX10N' or whatever to 'M2' right away. GAT = config.GAT # Global Aperture Table GAT.clear() GAMT = config.GAMT # Global Aperture Macro Table GAMT.clear() RevGAMT = {} # Dictionary keyed by aperture macro hash and returning macro name AT = {} # Aperture Table for this file for fname in fileList: #print 'Reading apertures from %s ...' % fname knownMacroNames = {} # [andreika]: units conversion units_div = 1.0 fid = open(fname,'rt') for line in fid: # Get rid of CR line = line.replace('\x0D', '') if tool_pat.match(line): break # When tools start, no more apertures are being defined # If this is an aperture macro definition, add its string # representation to the dictionary. It might already exist. # Ignore %AMOC8* from Eagle for now as it uses a macro parameter. if line[:7]=='%AMOC8*': continue # [andreika]: units conversion if line[:7]=='%MOMM*%' and config.Config['measurementunits'] == 'inch': units_div = 1.0 / 25.4 continue if line[:7]=='%MOIN*%' and config.Config['measurementunits'] == 'mm': units_div = 25.4 continue # parseApertureMacro() sucks up all macro lines up to terminating '%' AM = amacro.parseApertureMacro(line, fid) if AM: # Has this macro definition already been defined (perhaps by another name # in another layer)? try: # If this macro has already been encountered anywhere in any job, # RevGAMT will map the macro hash to the global macro name. Then, # make the local association knownMacroNames[localMacroName] = globalMacroName. knownMacroNames[AM.name] = RevGAMT[AM.hash()] except KeyError: # No, so define the global macro and do the translation. Note that # addToApertureMacroTable() MODIFIES AM.name to the new M-name. localMacroName = AM.name AM = amacro.addToApertureMacroTable(AM) knownMacroNames[localMacroName] = AM.name RevGAMT[AM.hash()] = AM.name else: A = parseAperture(line, knownMacroNames) # If this is an aperture definition, add the string representation # to the dictionary. It might already exist. if A: # [andreika]: apply units if type(A.dimx) == float or type(A.dimx) == int: A.dimx *= units_div if type(A.dimy) == float or type(A.dimy) == int: A.dimy *= units_div AT[A.hash()] = A fid.close() # Now, go through and assign sequential codes to all apertures code = 10 for val in list(AT.values()): key = 'D%d' % code GAT[key] = val val.code = key code += 1 if 0: keylist = list(config.GAT.keys()) keylist.sort() print('Apertures') print('=========') for key in keylist: print('%s' % config.GAT[key]) sys.exit(0) def findHighestApertureCode(keys): "Find the highest integer value in a list of aperture codes: ['D10', 'D23', 'D35', ...]" # Must sort keys by integer value, not string since 99 comes before 100 # as an integer but not a string. keys = [int(K[1:]) for K in keys] keys.sort() return keys[-1] def addToApertureTable(AP): GAT = config.GAT lastCode = findHighestApertureCode(list(GAT.keys())) code = 'D%d' % (lastCode+1) GAT[code] = AP AP.code = code return code def findInApertureTable(AP): """Return 'D10', for example in response to query for an object of type Aperture()""" hash = AP.hash() for key, val in list(config.GAT.items()): if hash==val.hash(): return key return None def findOrAddAperture(AP): """If the aperture exists in the GAT, modify the AP.code field to reflect the global code and return the code. Otherwise, create a new aperture in the GAT and return the new code for it.""" code = findInApertureTable(AP) if code: AP.code = code return code else: return addToApertureTable(AP) if __name__=="__main__": constructApertureTable(sys.argv[1:]) keylist = list(config.GAMT.keys()) keylist.sort() print('Aperture Macros') print('===============') for key in keylist: print('%s' % config.GAMT[key]) keylist = list(config.GAT.keys()) keylist.sort() print('Apertures') print('=========') for key in keylist: print('%s' % config.GAT[key])