From 59374373adcfd104064e779526b2fdec4a530f99 Mon Sep 17 00:00:00 2001 From: Andrei Date: Thu, 28 Jan 2021 20:37:41 +0200 Subject: [PATCH 1/8] Excellon parser fix --- gerbmerge/gerbmerge.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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: From 0cbd3fbb81c4ed985716efeaace5e44138118cad Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 8 Feb 2021 17:33:06 +0200 Subject: [PATCH 2/8] Use local gerber units for mm<->inch conversion --- gerbmerge/aptable.py | 14 ++++++++++++++ gerbmerge/jobs.py | 32 ++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) 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/jobs.py b/gerbmerge/jobs.py index 586cdb3..49a3760 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 @@ -294,6 +294,9 @@ class Job: x_div = 1.0 y_div = 1.0 + # [andreika]: use local units conversion + units_div = 1.0 + # 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 +349,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 +375,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 +443,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]) @@ -582,11 +601,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)) From 8dd292fa5b14c2a3550e22d7a67e6b669b3ec835 Mon Sep 17 00:00:00 2001 From: Bob Cousins Date: Wed, 26 Apr 2017 20:42:29 +0100 Subject: [PATCH 3/8] Fix for issue with KiCad Excellon drill file "Decimal format" --- gerbmerge/jobs.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gerbmerge/jobs.py b/gerbmerge/jobs.py index 49a3760..0b600c1 100644 --- a/gerbmerge/jobs.py +++ b/gerbmerge/jobs.py @@ -89,6 +89,7 @@ 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_pat2 = re.compile(r'^X([+-]?\d+\.\d*)Y([+-]?\d+\.\d*)$') # Plunge command 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 @@ -675,6 +676,13 @@ class Job: V.append(int(round(int(s)*divisor))) 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: + V.append(int(float(s)*1000*divisor)) + return tuple(V) + for line in fid.xreadlines(): # Get rid of CR characters line = string.replace(line, '\x0D', '') @@ -764,6 +772,10 @@ class Job: if match: x, y = xln2tenthou(match.groups()) else: + match = xydraw_pat2.match(line) + if match: + x, y = xln2tenthou2(match.groups()) + else: match = xdraw_pat.match(line) if match: x = xln2tenthou(match.groups())[0] From c896556260f0e72cf9add7a76244eb8c861076f7 Mon Sep 17 00:00:00 2001 From: Eyal Soha Date: Mon, 15 May 2017 21:13:37 +0300 Subject: [PATCH 4/8] fix bad merge from upstream --- gerbmerge/jobs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gerbmerge/jobs.py b/gerbmerge/jobs.py index 0b600c1..0ed9a5c 100644 --- a/gerbmerge/jobs.py +++ b/gerbmerge/jobs.py @@ -776,15 +776,15 @@ class Job: if match: x, y = xln2tenthou2(match.groups()) else: - match = xdraw_pat.match(line) - if match: - x = xln2tenthou(match.groups())[0] - y = last_y - 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: From 05200a1b2088a5b2cf72007611e07aec809dd117 Mon Sep 17 00:00:00 2001 From: Eyal Soha Date: Wed, 17 May 2017 22:31:57 +0300 Subject: [PATCH 5/8] Add support for Excellon G85 commands. The Excellon file format allows G85 commands. They specify a start and end point for drilling and result should be a line of closely spaced drills, so close that the edges are only protuding by 0.0005inches or less. --- gerbmerge/jobs.py | 68 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/gerbmerge/jobs.py b/gerbmerge/jobs.py index 0ed9a5c..aa66ae8 100644 --- a/gerbmerge/jobs.py +++ b/gerbmerge/jobs.py @@ -88,8 +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_pat2 = re.compile(r'^X([+-]?\d+\.\d*)Y([+-]?\d+\.\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 @@ -173,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 @@ -264,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 @@ -671,16 +677,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: - V.append(int(float(s)*1000*divisor)) + if s is not None: + V.append(int(float(s)*1000*divisor)) + else: + V.append(None) return tuple(V) for line in fid.xreadlines(): @@ -770,11 +782,11 @@ 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 = xydraw_pat2.match(line) if match: - x, y = xln2tenthou2(match.groups()) + x, y, stop_x, stop_y = xln2tenthou2(match.groups()) else: match = xdraw_pat.match(line) if match: @@ -791,9 +803,9 @@ class Job: 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 @@ -906,10 +918,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 @@ -937,10 +956,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" @@ -1156,8 +1177,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: @@ -1442,7 +1464,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 @@ -1452,8 +1474,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 From 1ad1952274734f0968b8f3e9a48bec640be7c2a7 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sun, 21 Mar 2021 12:32:52 +0200 Subject: [PATCH 6/8] Add FixedRotationOrigin option --- gerbmerge/config.py | 1 + gerbmerge/jobs.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) 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/jobs.py b/gerbmerge/jobs.py index aa66ae8..8990c76 100644 --- a/gerbmerge/jobs.py +++ b/gerbmerge/jobs.py @@ -1402,7 +1402,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] = [] From 59e982d30fec97c33c7e26856a256eb01a2d3b78 Mon Sep 17 00:00:00 2001 From: Andrei Date: Sun, 21 Mar 2021 12:33:43 +0200 Subject: [PATCH 7/8] Fix G36+G01 runtime error (in_fill_mode) --- gerbmerge/jobs.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/gerbmerge/jobs.py b/gerbmerge/jobs.py index 8990c76..410aa37 100644 --- a/gerbmerge/jobs.py +++ b/gerbmerge/jobs.py @@ -304,6 +304,9 @@ class Job: # [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. @@ -497,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 @@ -590,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! From fea2bf8868d958c8dcfe1764de602f944cb07a2c Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 16 Apr 2021 10:14:58 +0300 Subject: [PATCH 8/8] fix circle rotation in aperture macros --- gerbmerge/amacro.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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)