diff --git a/gerbmerge/amacro.py b/gerbmerge/amacro.py index dae1c5c..a240d88 100644 --- a/gerbmerge/amacro.py +++ b/gerbmerge/amacro.py @@ -165,8 +165,8 @@ class ApertureMacroPrimitive: raise def rotate(self): - if self.code == 1: # Circle: nothing to do - pass + if self.code == 1: # Circle: [andreika]: FIX circle rotation + rotatexypair(self.parms, 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, 2) diff --git a/gerbmerge/aptable.py b/gerbmerge/aptable.py index daacd8c..051f614 100644 --- a/gerbmerge/aptable.py +++ b/gerbmerge/aptable.py @@ -217,6 +217,8 @@ def constructApertureTable(fileList): #print 'Reading apertures from %s ...' % fname knownMacroNames = {} + # [andreika]: units conversion + units_div = 1.0 fid = file(fname,'rt') for line in fid: @@ -231,6 +233,13 @@ def constructApertureTable(fileList): # 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) @@ -255,6 +264,11 @@ def constructApertureTable(fileList): # 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() diff --git a/gerbmerge/config.py b/gerbmerge/config.py index 0eeefe9..721e9ee 100644 --- a/gerbmerge/config.py +++ b/gerbmerge/config.py @@ -52,6 +52,7 @@ Config = { 'fiducialpoints': None, # List of X,Y co-ordinates at which to draw fiducials 'fiducialcopperdiameter': 0.08, # Diameter of copper part of fiducial 'fiducialmaskdiameter': 0.32, # Diameter of fiducial soldermask opening + 'fixedrotationorigin': 0, # [andreika]: add settings to disable shifting of the rotating origin } # This dictionary is indexed by lowercase layer name and has as values a file diff --git a/gerbmerge/gerbmerge.py b/gerbmerge/gerbmerge.py index b73e790..cb83bf0 100755 --- a/gerbmerge/gerbmerge.py +++ b/gerbmerge/gerbmerge.py @@ -172,6 +172,7 @@ def writeExcellonHeader(fid): fid.write("INCH,%s\n" % zerosDef) else: # metric - mm fid.write("METRIC,%s\n" % zerosDef) +def writeExcellonHeaderEnd(fid): fid.write('%\n') def writeExcellonFooter(fid): @@ -180,6 +181,9 @@ def writeExcellonFooter(fid): def writeExcellonTool(fid, tool, size): fid.write('%sC%f\n' % (tool, size)) +def writeExcellonToolSelection(fid, tool, size): + fid.write('%s\n' % (tool)) + def writeFiducials(fid, drawcode, OriginX, OriginY, MaxXExtent, MaxYExtent): """Place fiducials at arbitrary points. The FiducialPoints list in the config specifies sets of X,Y co-ordinates. Positive values of X/Y represent offsets from the lower left @@ -706,9 +710,13 @@ def merge(opts, args, gui = None): size = config.GlobalToolMap[tool] except: raise RuntimeError, "INTERNAL ERROR: Tool code %s not found in global tool map" % tool - writeExcellonTool(fid, tool, size) + writeExcellonHeaderEnd(fid) + + for tool in Tools: + size = config.GlobalToolMap[tool] + writeExcellonToolSelection(fid, tool, size) #for row in Layout: # row.writeExcellon(fid, size) for job in Place.jobs: diff --git a/gerbmerge/jobs.py b/gerbmerge/jobs.py index 586cdb3..410aa37 100644 --- a/gerbmerge/jobs.py +++ b/gerbmerge/jobs.py @@ -80,7 +80,7 @@ IgnoreList = ( \ re.compile(r'\*'), # Empty statement re.compile(r'^%IN.*\*%'), re.compile(r'^%ICAS\*%'), # Not in RS274X spec. - re.compile(r'^%MOIN\*%'), + #re.compile(r'^%MOIN\*%'), # [andreika]: don't ignore re.compile(r'^%ASAXBY\*%'), re.compile(r'^%AD\*%'), # GerbTool empty aperture definition re.compile(r'^%LN.*\*%') # Layer name @@ -88,7 +88,8 @@ IgnoreList = ( \ # Patterns for Excellon interpretation xtool_pat = re.compile(r'^(T\d+)$') # Tool selection -xydraw_pat = re.compile(r'^X([+-]?\d+)Y([+-]?\d+)$') # Plunge command +xydraw_pat = re.compile(r'^X([+-]?\d+)Y([+-]?\d+)(?:G85X([+-]?\d+)Y([+-]?\d+))?$') # Plunge command with optional G85 +xydraw_pat2 = re.compile(r'^X([+-]?\d+\.\d*)Y([+-]?\d+\.\d*)(?:G85X([+-]?\d+\.\d*)Y([+-]?\d+\.\d*))?$') # Plunge command with optional G85 xdraw_pat = re.compile(r'^X([+-]?\d+)$') # Plunge command, repeat last Y value ydraw_pat = re.compile(r'^Y([+-]?\d+)$') # Plunge command, repeat last X value xtdef_pat = re.compile(r'^(T\d+)(?:F\d+)?(?:S\d+)?C([0-9.]+)$') # Tool+diameter definition with optional @@ -172,8 +173,9 @@ class Job: # This is to help sorting all jobs and writing out all plunge # commands for a single tool. # - # The key to this dictionary is the full tool name, e.g., T03 - # as a string. Each command is an (X,Y) integer tuple. + # The key to this dictionary is the full tool name, e.g., T03 as a + # string. Each command is an (X,Y,STOP_X,STOP_Y) integer tuple. + # STOP_X and STOP_Y are not none only if this is a G85 command. self.xcommands = {} # This is a dictionary mapping LOCAL tool names (e.g., T03) to diameters @@ -263,6 +265,11 @@ class Job: and ( type( command_list[1] ) == types.IntType ): ## ensure that first two elemenst are integers command_list[0] += x_shift / 10 command_list[1] += y_shift / 10 + if ( type( command_list[2] ) == types.IntType ) \ + and ( type( command_list[3] ) == types.IntType ): ## ensure that first two elemenst are integerslen(command_list) == 4: + # G85 command, need to shift the second pair of xy, too. + command_list[2] += x_shift / 10 + command_list[3] += y_shift / 10 command[index] = tuple(command_list) ## convert list back to tuple self.xcommands[tool] = command ## set modified command @@ -294,6 +301,12 @@ class Job: x_div = 1.0 y_div = 1.0 + # [andreika]: use local units conversion + units_div = 1.0 + + # [andreika]: store fill mode separately because of G01 can be inside G36/37 + in_fill_mode = False + # Drawing commands can be repeated with X or Y omitted if they are # the same as before. These variables store the last X/Y value as # integers in hundred-thousandths of an inch. @@ -346,6 +359,11 @@ class Job: A = aptable.parseAperture(line, self.apmxlat[layername]) if not A: raise RuntimeError, "Unknown aperture definition in file %s" % fullname + # [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 hash = A.hash() if not RevGAT.has_key(hash): @@ -367,11 +385,21 @@ class Job: # DipTrace specific fixes, but could be emitted by any CAD program. They are Standard Gerber RS-274X # a hack to fix lack of recognition for metric direction from DipTrace - %MOMM*% if (line[:7] == '%MOMM*%'): + # [andreika]: just set units to mm, no error if (config.Config['measurementunits'] == 'inch'): - raise RuntimeError, "File %s units do match config file" % fullname + #raise RuntimeError, "File %s units do match config file" % fullname + units_div = 1.0 / 25.4 + continue else: #print "ignoring metric directive: " + line continue # ignore it so func doesn't choke on it + # [andreika]: add reciprocal conversion + if (line[:7] == '%MOIN*%'): + if (config.Config['measurementunits'] == 'mm'): + units_div = 25.4 + continue + else: + continue # ignore it so func doesn't choke on it if line[:3] == '%SF': # scale factor - we will ignore it print 'Scale factor parameter ignored: ' + line @@ -425,6 +453,7 @@ class Job: continue # allow for metric - scale to 1/1000 mm + # [andreika]: use local units if config.Config['measurementunits'] == 'inch': if item[0]=='X': # M.N specification for X-axis. fracpart = int(item[2]) @@ -471,6 +500,12 @@ class Job: elif gcode==75: circ_signed = True + # [andreika]: we store fill mode separately + if gcode==36: + in_fill_mode = True + elif gcode==37: + in_fill_mode = False + continue raise RuntimeError, "G-Code 'G%02d' is not supported" % gcode @@ -564,7 +599,9 @@ class Job: # It's also OK if we're in the middle of a G36 polygon fill as we're only defining # the polygon extents. if (d != 2) and (last_gmode != 36): - raise RuntimeError, 'File %s has draw command %s with no aperture chosen' % (fullname, sub_line) + # [andreika]: check for fill mode more accurately + if not in_fill_mode: + raise RuntimeError, 'File %s has draw command %s with no aperture chosen' % (fullname, sub_line) # Save last_x/y BEFORE scaling to 2.5 format else subsequent single-ordinate # flashes (e.g., Y with no X) will be scaled twice! @@ -582,11 +619,12 @@ class Job: self.miny = min(self.miny,0) self.maxy = max(self.maxy,0) - x = int(round(x*x_div)) - y = int(round(y*y_div)) + # [andreika]: add units_div + x = int(round(x*x_div*units_div)) + y = int(round(y*y_div*units_div)) if I is not None: - I = int(round(I*x_div)) - J = int(round(J*y_div)) + I = int(round(I*x_div*units_div)) + J = int(round(J*y_div*units_div)) self.commands[layername].append((x,y,I,J,d,circ_signed)) else: self.commands[layername].append((x,y,d)) @@ -650,9 +688,22 @@ class Job: def xln2tenthou(L, divisor=divisor, zeropadto=zeropadto): V = [] for s in L: - if not suppress_leading: - s = s + '0'*(zeropadto-len(s)) - V.append(int(round(int(s)*divisor))) + if s is not None: + if not suppress_leading: + s = s + '0'*(zeropadto-len(s)) + V.append(int(round(int(s)*divisor))) + else: + V.append(None) + return tuple(V) + + # Helper function to convert X/Y strings into integers in units of ten-thousandth of an inch. + def xln2tenthou2 (L, divisor=divisor, zeropadto=zeropadto): + V = [] + for s in L: + if s is not None: + V.append(int(float(s)*1000*divisor)) + else: + V.append(None) return tuple(V) for line in fid.xreadlines(): @@ -742,26 +793,30 @@ class Job: # Plunge command? match = xydraw_pat.match(line) if match: - x, y = xln2tenthou(match.groups()) + x, y, stop_x, stop_y = xln2tenthou(match.groups()) else: - match = xdraw_pat.match(line) + match = xydraw_pat2.match(line) if match: - x = xln2tenthou(match.groups())[0] - y = last_y + x, y, stop_x, stop_y = xln2tenthou2(match.groups()) else: - match = ydraw_pat.match(line) + match = xdraw_pat.match(line) if match: - y = xln2tenthou(match.groups())[0] - x = last_x + x = xln2tenthou(match.groups())[0] + y = last_y + else: + match = ydraw_pat.match(line) + if match: + y = xln2tenthou(match.groups())[0] + x = last_x if match: if currtool is None: raise RuntimeError, 'File %s has plunge command without previous tool selection' % fullname try: - self.xcommands[currtool].append((x,y)) + self.xcommands[currtool].append((x,y,stop_x,stop_y)) except KeyError: - self.xcommands[currtool] = [(x,y)] + self.xcommands[currtool] = [(x,y,stop_x,stop_y)] last_x = x last_y = y @@ -874,10 +929,17 @@ class Job: for ltool in ltools: if self.xcommands.has_key(ltool): for cmd in self.xcommands[ltool]: - x, y = cmd + x, y, stop_x, stop_y = cmd new_x = x+DX new_y = y+DY - fid.write('X%sY%s\n' % (formatForXln(new_x), formatForXln(new_y))) + if stop_x is None: + fid.write('X%sY%s\n' % (formatForXln(new_x), formatForXln(new_y))) + else: + new_stop_x = stop_x+DX + new_stop_y = stop_y+DY + fid.write('X%sY%sG85X%sY%s\n' % + (formatForXln(new_x), formatForXln(new_y), + formatForXln(new_stop_x), formatForXln(new_stop_y))) def writeDrillHits(self, fid, diameter, toolNum, Xoff, Yoff): """Write a drill hit pattern. diameter is tool diameter in inches, while toolNum is @@ -905,10 +967,12 @@ class Job: for ltool in ltools: if self.xcommands.has_key(ltool): for cmd in self.xcommands[ltool]: - x, y = cmd + x, y, stop_x, stop_y = cmd # add metric support (1/1000 mm vs. 1/100,000 inch) # TODO - verify metric scaling is correct??? makestroke.drawDrillHit(fid, 10*x+DX, 10*y+DY, toolNum) + if stop_x is not None: + makestroke.drawDrillHit(fid, 10*stop_x+DX, 10*stop_y+DY, toolNum) def aperturesAndMacros(self, layername): "Return dictionaries whose keys are all necessary aperture names and macro names for this layer" @@ -1124,8 +1188,9 @@ class Job: keys = self.xcommands.keys() for toolname in keys: # Remember Excellon is 2.4 format while Gerber data is 2.5 format - validList = [(x,y) for x,y in self.xcommands[toolname] if self.inBorders(10*x,10*y)] - + validList = [tup for tup in self.xcommands[toolname] + if (self.inBorders(10*tup[0],10*tup[1]) and + (tup[2] is None or self.inBorders(10*tup[2],10*tup[3])))] if validList: self.xcommands[toolname] = validList else: @@ -1348,7 +1413,11 @@ def rotateJob(job, degrees = 90, firstpass = True): # We also have to take aperture change commands and # replace them with the new aperture code if we have # a rotation. - offset = job.maxy-job.miny + # [andreika]: add 'fixedrotationorigin' setting to disable shifting + if config.Config['fixedrotationorigin']: + offset = 0 + else: + offset = job.maxy-job.miny for layername in job.commands.keys(): J.commands[layername] = [] J.apertures[layername] = [] @@ -1410,7 +1479,7 @@ def rotateJob(job, degrees = 90, firstpass = True): for tool in job.xcommands.keys(): J.xcommands[tool] = [] - for x,y in job.xcommands[tool]: + for x,y,stop_x,stop_y in job.xcommands[tool]: # add metric support (1/1000 mm vs. 1/100,000 inch) # NOTE: There don't appear to be any need for a change. The usual x10 factor seems to apply @@ -1420,8 +1489,16 @@ def rotateJob(job, degrees = 90, firstpass = True): newx = int(round(newx/10.0)) newy = int(round(newy/10.0)) + if stop_x is not None: + newstop_x = -(10*stop_y - job.miny) + job.minx + offset + newstop_y = (10*stop_x - job.minx) + job.miny - J.xcommands[tool].append((newx,newy)) + newstop_x = int(round(newstop_x/10.0)) + newstop_y = int(round(newstop_y/10.0)) + else: + newstop_x = None + newstop_y = None + J.xcommands[tool].append((newx,newy,newstop_x,newstop_y)) # Rotate some more if required degrees -= 90