mirror of https://github.com/rusefi/gerbmerge.git
Python 3.x
This commit is contained in:
parent
371354d056
commit
f78db83293
|
@ -112,7 +112,7 @@ class ApertureMacroPrimitive:
|
||||||
valids = None
|
valids = None
|
||||||
|
|
||||||
if valids is None:
|
if valids is None:
|
||||||
raise RuntimeError, 'Undefined aperture macro primitive code %d' % code
|
raise RuntimeError('Undefined aperture macro primitive code %d' % code)
|
||||||
|
|
||||||
# We expect exactly the number of fields required, except for macro
|
# We expect exactly the number of fields required, except for macro
|
||||||
# type 4 which is an outline and has a variable number of points.
|
# type 4 which is an outline and has a variable number of points.
|
||||||
|
@ -126,18 +126,18 @@ class ApertureMacroPrimitive:
|
||||||
# - last field is rotation
|
# - last field is rotation
|
||||||
if self.code==4:
|
if self.code==4:
|
||||||
if len(fields) < 2:
|
if len(fields) < 2:
|
||||||
raise RuntimeError, 'Outline macro primitive has way too few fields'
|
raise RuntimeError('Outline macro primitive has way too few fields')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
N = int(fields[1])
|
N = int(fields[1])
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, 'Outline macro primitive has non-integer number of points'
|
raise RuntimeError('Outline macro primitive has non-integer number of points')
|
||||||
|
|
||||||
if len(fields) != (5+2*N):
|
if len(fields) != (5+2*N):
|
||||||
raise RuntimeError, 'Outline macro primitive has %d fields...expecting %d fields' % (len(fields), 3+2*N)
|
raise RuntimeError('Outline macro primitive has %d fields...expecting %d fields' % (len(fields), 3+2*N))
|
||||||
else:
|
else:
|
||||||
if len(fields) != len(valids):
|
if len(fields) != len(valids):
|
||||||
raise RuntimeError, 'Macro primitive has %d fields...expecting %d fields' % (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
|
# Convert each parameter on the input line to an entry in the self.parms
|
||||||
# list, using either int() or float() conversion.
|
# list, using either int() or float() conversion.
|
||||||
|
@ -150,7 +150,7 @@ class ApertureMacroPrimitive:
|
||||||
try:
|
try:
|
||||||
self.parms.append(converter(fields[parmix]))
|
self.parms.append(converter(fields[parmix]))
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, 'Aperture macro primitive parameter %d has incorrect type' % (parmix+1)
|
raise RuntimeError('Aperture macro primitive parameter %d has incorrect type' % (parmix+1))
|
||||||
|
|
||||||
def setFromLine(self, line):
|
def setFromLine(self, line):
|
||||||
# Account for DOS line endings and get rid of line ending and '*' at the end
|
# Account for DOS line endings and get rid of line ending and '*' at the end
|
||||||
|
@ -164,12 +164,12 @@ class ApertureMacroPrimitive:
|
||||||
try:
|
try:
|
||||||
code = int(fields[0])
|
code = int(fields[0])
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, 'Illegal aperture macro primitive code "%s"' % fields[0]
|
raise RuntimeError('Illegal aperture macro primitive code "%s"' % fields[0])
|
||||||
self.setFromFields(code, fields[1:])
|
self.setFromFields(code, fields[1:])
|
||||||
except:
|
except:
|
||||||
print '='*20
|
print('='*20)
|
||||||
print "==> ", line
|
print("==> ", line)
|
||||||
print '='*20
|
print('='*20)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def rotate(self, flip):
|
def rotate(self, flip):
|
||||||
|
@ -283,7 +283,7 @@ def parseApertureMacro(s, fid):
|
||||||
|
|
||||||
M.add(P)
|
M.add(P)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Premature end-of-file while parsing aperture macro"
|
raise RuntimeError("Premature end-of-file while parsing aperture macro")
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -295,7 +295,7 @@ def addToApertureMacroTable(AM):
|
||||||
|
|
||||||
# Must sort keys by integer value, not string since 99 comes before 100
|
# Must sort keys by integer value, not string since 99 comes before 100
|
||||||
# as an integer but not a string.
|
# as an integer but not a string.
|
||||||
keys = map(int, map(lambda K: K[1:], GAMT.keys()))
|
keys = list(map(int, [K[1:] for K in list(GAMT.keys())]))
|
||||||
keys.sort()
|
keys.sort()
|
||||||
|
|
||||||
if len(keys):
|
if len(keys):
|
||||||
|
@ -338,25 +338,23 @@ if __name__=="__main__":
|
||||||
MR = M.rotated(0)
|
MR = M.rotated(0)
|
||||||
|
|
||||||
# Generate the Gerber so we can view it
|
# Generate the Gerber so we can view it
|
||||||
fid = file('amacro.ger', 'wt')
|
fid = open('amacro.ger', 'wt')
|
||||||
print >> fid, \
|
print("""G75*
|
||||||
"""G75*
|
|
||||||
G70*
|
G70*
|
||||||
%OFA0B0*%
|
%OFA0B0*%
|
||||||
%FSLAX24Y24*%
|
%FSLAX24Y24*%
|
||||||
%IPPOS*%
|
%IPPOS*%
|
||||||
%LPD*%"""
|
%LPD*%""", file=fid)
|
||||||
M.writeDef(fid)
|
M.writeDef(fid)
|
||||||
MR.writeDef(fid)
|
MR.writeDef(fid)
|
||||||
print >> fid, \
|
print("""%ADD10TEST*%
|
||||||
"""%ADD10TEST*%
|
|
||||||
%ADD11TESTR*%
|
%ADD11TESTR*%
|
||||||
D10*
|
D10*
|
||||||
X010000Y010000D03*
|
X010000Y010000D03*
|
||||||
D11*
|
D11*
|
||||||
X015000Y010000D03*
|
X015000Y010000D03*
|
||||||
M02*"""
|
M02*""", file=fid)
|
||||||
fid.close()
|
fid.close()
|
||||||
|
|
||||||
print M
|
print(M)
|
||||||
print MR
|
print(MR)
|
||||||
|
|
|
@ -168,17 +168,17 @@ def parseAperture(s, knownMacroNames):
|
||||||
code, dimx, dimy = match.groups()
|
code, dimx, dimy = match.groups()
|
||||||
|
|
||||||
if ap[0] in ('Macro',):
|
if ap[0] in ('Macro',):
|
||||||
if knownMacroNames.has_key(dimx):
|
if dimx in knownMacroNames:
|
||||||
dimx = knownMacroNames[dimx] # dimx is now GLOBAL, permanent macro name (e.g., 'M2')
|
dimx = knownMacroNames[dimx] # dimx is now GLOBAL, permanent macro name (e.g., 'M2')
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, 'Aperture Macro name "%s" not defined' % dimx
|
raise RuntimeError('Aperture Macro name "%s" not defined' % dimx)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
dimx = float(dimx)
|
dimx = float(dimx)
|
||||||
if dimy:
|
if dimy:
|
||||||
dimy = float(dimy)
|
dimy = float(dimy)
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "Illegal floating point aperture size"
|
raise RuntimeError("Illegal floating point aperture size")
|
||||||
|
|
||||||
return Aperture(ap, code, dimx, dimy)
|
return Aperture(ap, code, dimx, dimy)
|
||||||
|
|
||||||
|
@ -221,7 +221,7 @@ def constructApertureTable(fileList):
|
||||||
# [andreika]: units conversion
|
# [andreika]: units conversion
|
||||||
units_div = 1.0
|
units_div = 1.0
|
||||||
|
|
||||||
fid = file(fname,'rt')
|
fid = open(fname,'rt')
|
||||||
for line in fid:
|
for line in fid:
|
||||||
# Get rid of CR
|
# Get rid of CR
|
||||||
line = line.replace('\x0D', '')
|
line = line.replace('\x0D', '')
|
||||||
|
@ -276,19 +276,19 @@ def constructApertureTable(fileList):
|
||||||
|
|
||||||
# Now, go through and assign sequential codes to all apertures
|
# Now, go through and assign sequential codes to all apertures
|
||||||
code = 10
|
code = 10
|
||||||
for val in AT.values():
|
for val in list(AT.values()):
|
||||||
key = 'D%d' % code
|
key = 'D%d' % code
|
||||||
GAT[key] = val
|
GAT[key] = val
|
||||||
val.code = key
|
val.code = key
|
||||||
code += 1
|
code += 1
|
||||||
|
|
||||||
if 0:
|
if 0:
|
||||||
keylist = config.GAT.keys()
|
keylist = list(config.GAT.keys())
|
||||||
keylist.sort()
|
keylist.sort()
|
||||||
print 'Apertures'
|
print('Apertures')
|
||||||
print '========='
|
print('=========')
|
||||||
for key in keylist:
|
for key in keylist:
|
||||||
print '%s' % config.GAT[key]
|
print('%s' % config.GAT[key])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def findHighestApertureCode(keys):
|
def findHighestApertureCode(keys):
|
||||||
|
@ -304,7 +304,7 @@ def findHighestApertureCode(keys):
|
||||||
def addToApertureTable(AP):
|
def addToApertureTable(AP):
|
||||||
GAT = config.GAT
|
GAT = config.GAT
|
||||||
|
|
||||||
lastCode = findHighestApertureCode(GAT.keys())
|
lastCode = findHighestApertureCode(list(GAT.keys()))
|
||||||
code = 'D%d' % (lastCode+1)
|
code = 'D%d' % (lastCode+1)
|
||||||
GAT[code] = AP
|
GAT[code] = AP
|
||||||
AP.code = code
|
AP.code = code
|
||||||
|
@ -315,7 +315,7 @@ def findInApertureTable(AP):
|
||||||
"""Return 'D10', for example in response to query for an object
|
"""Return 'D10', for example in response to query for an object
|
||||||
of type Aperture()"""
|
of type Aperture()"""
|
||||||
hash = AP.hash()
|
hash = AP.hash()
|
||||||
for key, val in config.GAT.items():
|
for key, val in list(config.GAT.items()):
|
||||||
if hash==val.hash():
|
if hash==val.hash():
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
@ -335,16 +335,16 @@ def findOrAddAperture(AP):
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
constructApertureTable(sys.argv[1:])
|
constructApertureTable(sys.argv[1:])
|
||||||
|
|
||||||
keylist = config.GAMT.keys()
|
keylist = list(config.GAMT.keys())
|
||||||
keylist.sort()
|
keylist.sort()
|
||||||
print 'Aperture Macros'
|
print('Aperture Macros')
|
||||||
print '==============='
|
print('===============')
|
||||||
for key in keylist:
|
for key in keylist:
|
||||||
print '%s' % config.GAMT[key]
|
print('%s' % config.GAMT[key])
|
||||||
|
|
||||||
keylist = config.GAT.keys()
|
keylist = list(config.GAT.keys())
|
||||||
keylist.sort()
|
keylist.sort()
|
||||||
print 'Apertures'
|
print('Apertures')
|
||||||
print '========='
|
print('=========')
|
||||||
for key in keylist:
|
for key in keylist:
|
||||||
print '%s' % config.GAT[key]
|
print('%s' % config.GAT[key])
|
||||||
|
|
|
@ -15,7 +15,7 @@ http://github.com/unwireddevices/gerbmerge
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import ConfigParser
|
import configparser
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ MinimumFeatureDimension = {}
|
||||||
# hash string. The value is the aperture code (e.g., 'D10') or macro name (e.g., 'M5').
|
# hash string. The value is the aperture code (e.g., 'D10') or macro name (e.g., 'M5').
|
||||||
def buildRevDict(D):
|
def buildRevDict(D):
|
||||||
RevD = {}
|
RevD = {}
|
||||||
for key,val in D.items():
|
for key,val in list(D.items()):
|
||||||
RevD[val.hash()] = key
|
RevD[val.hash()] = key
|
||||||
return RevD
|
return RevD
|
||||||
|
|
||||||
|
@ -127,12 +127,12 @@ def parseStringList(L):
|
||||||
if 0:
|
if 0:
|
||||||
if L[0]=="'":
|
if L[0]=="'":
|
||||||
if L[-1] != "'":
|
if L[-1] != "'":
|
||||||
raise RuntimeError, "Illegal configuration string '%s'" % L
|
raise RuntimeError("Illegal configuration string '%s'" % L)
|
||||||
L = L[1:-1]
|
L = L[1:-1]
|
||||||
|
|
||||||
elif L[0]=='"':
|
elif L[0]=='"':
|
||||||
if L[-1] != '"':
|
if L[-1] != '"':
|
||||||
raise RuntimeError, "Illegal configuration string '%s'" % L
|
raise RuntimeError("Illegal configuration string '%s'" % L)
|
||||||
L = L[1:-1]
|
L = L[1:-1]
|
||||||
|
|
||||||
# This pattern matches quotes at the beginning and end...quotes must match
|
# This pattern matches quotes at the beginning and end...quotes must match
|
||||||
|
@ -153,14 +153,14 @@ def parseToolList(fname):
|
||||||
TL = {}
|
TL = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fid = file(fname, 'rt')
|
fid = open(fname, 'rt')
|
||||||
except Exception, detail:
|
except Exception as detail:
|
||||||
raise RuntimeError, "Unable to open tool list file '%s':\n %s" % (fname, str(detail))
|
raise RuntimeError("Unable to open tool list file '%s':\n %s" % (fname, str(detail)))
|
||||||
|
|
||||||
pat_in = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*in\s*')
|
pat_in = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*in\s*')
|
||||||
pat_mm = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*mm\s*')
|
pat_mm = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*mm\s*')
|
||||||
pat_mil = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*(?:mil)?')
|
pat_mil = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*(?:mil)?')
|
||||||
for line in fid.xreadlines():
|
for line in fid:
|
||||||
line = string.strip(line)
|
line = string.strip(line)
|
||||||
if (not line) or (line[0] in ('#', ';')): continue
|
if (not line) or (line[0] in ('#', ';')): continue
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ def parseToolList(fname):
|
||||||
try:
|
try:
|
||||||
size = float(size)
|
size = float(size)
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "Tool size in file '%s' is not a valid floating-point number:\n %s" % (fname,line)
|
raise RuntimeError("Tool size in file '%s' is not a valid floating-point number:\n %s" % (fname,line))
|
||||||
|
|
||||||
if mil:
|
if mil:
|
||||||
size = size*0.001 # Convert mil to inches
|
size = size*0.001 # Convert mil to inches
|
||||||
|
@ -192,8 +192,8 @@ def parseToolList(fname):
|
||||||
# Canonicalize tool so that T1 becomes T01
|
# Canonicalize tool so that T1 becomes T01
|
||||||
tool = 'T%02d' % int(tool[1:])
|
tool = 'T%02d' % int(tool[1:])
|
||||||
|
|
||||||
if TL.has_key(tool):
|
if tool in TL:
|
||||||
raise RuntimeError, "Tool '%s' defined more than once in tool list file '%s'" % (tool,fname)
|
raise RuntimeError("Tool '%s' defined more than once in tool list file '%s'" % (tool,fname))
|
||||||
|
|
||||||
TL[tool]=size
|
TL[tool]=size
|
||||||
fid.close()
|
fid.close()
|
||||||
|
@ -215,38 +215,38 @@ def parseToolList(fname):
|
||||||
def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
||||||
global DefaultToolList
|
global DefaultToolList
|
||||||
|
|
||||||
CP = ConfigParser.ConfigParser()
|
CP = configparser.ConfigParser()
|
||||||
CP.readfp(file(fname.rstrip(),'rt'))
|
CP.readfp(open(fname.rstrip(),'rt'))
|
||||||
|
|
||||||
# First parse global options
|
# First parse global options
|
||||||
if CP.has_section('Options'):
|
if CP.has_section('Options'):
|
||||||
for opt in CP.options('Options'):
|
for opt in CP.options('Options'):
|
||||||
# Is it one we expect
|
# Is it one we expect
|
||||||
if Config.has_key(opt):
|
if opt in Config:
|
||||||
# Yup...override it
|
# Yup...override it
|
||||||
Config[opt] = CP.get('Options', opt)
|
Config[opt] = CP.get('Options', opt)
|
||||||
|
|
||||||
elif CP.defaults().has_key(opt):
|
elif opt in CP.defaults():
|
||||||
pass # Ignore DEFAULTS section keys
|
pass # Ignore DEFAULTS section keys
|
||||||
|
|
||||||
elif opt in ('fabricationdrawing', 'outlinelayer'):
|
elif opt in ('fabricationdrawing', 'outlinelayer'):
|
||||||
print '*'*73
|
print('*'*73)
|
||||||
print '\nThe FabricationDrawing and OutlineLayer configuration options have been'
|
print('\nThe FabricationDrawing and OutlineLayer configuration options have been')
|
||||||
print 'renamed as of GerbMerge version 1.0. Please consult the documentation for'
|
print('renamed as of GerbMerge version 1.0. Please consult the documentation for')
|
||||||
print 'a description of the new options, then modify your configuration file.\n'
|
print('a description of the new options, then modify your configuration file.\n')
|
||||||
print '*'*73
|
print('*'*73)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Unknown option '%s' in [Options] section of configuration file" % opt
|
raise RuntimeError("Unknown option '%s' in [Options] section of configuration file" % opt)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Missing [Options] section in configuration file"
|
raise RuntimeError("Missing [Options] section in configuration file")
|
||||||
|
|
||||||
# Ensure we got a tool list
|
# Ensure we got a tool list
|
||||||
if not Config.has_key('toollist'):
|
if 'toollist' not in Config:
|
||||||
raise RuntimeError, "INTERNAL ERROR: Missing tool list assignment in [Options] section"
|
raise RuntimeError("INTERNAL ERROR: Missing tool list assignment in [Options] section")
|
||||||
|
|
||||||
# Make integers integers, floats floats
|
# Make integers integers, floats floats
|
||||||
for key,val in Config.items():
|
for key,val in list(Config.items()):
|
||||||
try:
|
try:
|
||||||
val = int(val)
|
val = int(val)
|
||||||
Config[key]=val
|
Config[key]=val
|
||||||
|
@ -283,7 +283,7 @@ def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
||||||
for index in range(0, len(temp), 2):
|
for index in range(0, len(temp), 2):
|
||||||
MinimumFeatureDimension[ temp[index] ] = float( temp[index + 1] )
|
MinimumFeatureDimension[ temp[index] ] = float( temp[index + 1] )
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "Illegal configuration string:" + Config['minimumfeaturesize']
|
raise RuntimeError("Illegal configuration string:" + Config['minimumfeaturesize'])
|
||||||
|
|
||||||
# Process MergeOutputFiles section to set output file names
|
# Process MergeOutputFiles section to set output file names
|
||||||
if CP.has_section('MergeOutputFiles'):
|
if CP.has_section('MergeOutputFiles'):
|
||||||
|
@ -303,10 +303,10 @@ def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
||||||
|
|
||||||
# Ensure all jobs have a board outline
|
# Ensure all jobs have a board outline
|
||||||
if not CP.has_option(jobname, 'boardoutline'):
|
if not CP.has_option(jobname, 'boardoutline'):
|
||||||
raise RuntimeError, "Job '%s' does not have a board outline specified" % jobname
|
raise RuntimeError("Job '%s' does not have a board outline specified" % jobname)
|
||||||
|
|
||||||
if not CP.has_option(jobname, 'drills'):
|
if not CP.has_option(jobname, 'drills'):
|
||||||
raise RuntimeError, "Job '%s' does not have a drills layer specified" % jobname
|
raise RuntimeError("Job '%s' does not have a drills layer specified" % jobname)
|
||||||
|
|
||||||
for layername in CP.options(jobname):
|
for layername in CP.options(jobname):
|
||||||
if layername[0]=='*' or layername=='boardoutline':
|
if layername[0]=='*' or layername=='boardoutline':
|
||||||
|
@ -323,10 +323,10 @@ def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
||||||
del apfiles
|
del apfiles
|
||||||
|
|
||||||
if 0:
|
if 0:
|
||||||
keylist = GAMT.keys()
|
keylist = list(GAMT.keys())
|
||||||
keylist.sort()
|
keylist.sort()
|
||||||
for key in keylist:
|
for key in keylist:
|
||||||
print '%s' % GAMT[key]
|
print('%s' % GAMT[key])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Parse the tool list
|
# Parse the tool list
|
||||||
|
@ -350,8 +350,8 @@ def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
||||||
if jobname=='MergeOutputFiles': continue
|
if jobname=='MergeOutputFiles': continue
|
||||||
if jobname=='GerbMergeGUI': continue
|
if jobname=='GerbMergeGUI': continue
|
||||||
|
|
||||||
print '' # empty line before hand for readability
|
print('') # empty line before hand for readability
|
||||||
print 'Reading data from', jobname, '...'
|
print('Reading data from', jobname, '...')
|
||||||
|
|
||||||
J = jobs.Job(jobname)
|
J = jobs.Job(jobname)
|
||||||
|
|
||||||
|
@ -369,12 +369,12 @@ def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
||||||
try:
|
try:
|
||||||
J.ExcellonDecimals = int(fname)
|
J.ExcellonDecimals = int(fname)
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "Excellon decimals '%s' in config file is not a valid integer" % fname
|
raise RuntimeError("Excellon decimals '%s' in config file is not a valid integer" % fname)
|
||||||
elif layername=='repeat':
|
elif layername=='repeat':
|
||||||
try:
|
try:
|
||||||
J.Repeat = int(fname)
|
J.Repeat = int(fname)
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "Repeat count '%s' in config file is not a valid integer" % fname
|
raise RuntimeError("Repeat count '%s' in config file is not a valid integer" % fname)
|
||||||
|
|
||||||
for layername in CP.options(jobname):
|
for layername in CP.options(jobname):
|
||||||
fname = CP.get(jobname, layername)
|
fname = CP.get(jobname, layername)
|
||||||
|
@ -388,34 +388,34 @@ def parseConfigFile(fname, Config=Config, Jobs=Jobs):
|
||||||
|
|
||||||
# Emit warnings if some layers are missing
|
# Emit warnings if some layers are missing
|
||||||
LL = LayerList.copy()
|
LL = LayerList.copy()
|
||||||
for layername in J.apxlat.keys():
|
for layername in list(J.apxlat.keys()):
|
||||||
assert LL.has_key(layername)
|
assert layername in LL
|
||||||
del LL[layername]
|
del LL[layername]
|
||||||
|
|
||||||
if LL:
|
if LL:
|
||||||
if errstr=='ERROR':
|
if errstr=='ERROR':
|
||||||
do_abort=1
|
do_abort=1
|
||||||
|
|
||||||
print '%s: Job %s is missing the following layers:' % (errstr, jobname)
|
print('%s: Job %s is missing the following layers:' % (errstr, jobname))
|
||||||
for layername in LL.keys():
|
for layername in list(LL.keys()):
|
||||||
print ' %s' % layername
|
print(' %s' % layername)
|
||||||
|
|
||||||
# Store the job in the global Jobs dictionary, keyed by job name
|
# Store the job in the global Jobs dictionary, keyed by job name
|
||||||
Jobs[jobname] = J
|
Jobs[jobname] = J
|
||||||
|
|
||||||
if do_abort:
|
if do_abort:
|
||||||
raise RuntimeError, 'Exiting since jobs are missing layers. Set AllowMissingLayers=1\nto override.'
|
raise RuntimeError('Exiting since jobs are missing layers. Set AllowMissingLayers=1\nto override.')
|
||||||
|
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
CP = parseConfigFile(sys.argv[1])
|
CP = parseConfigFile(sys.argv[1])
|
||||||
print Config
|
print(Config)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if 0:
|
if 0:
|
||||||
for key, val in CP.defaults().items():
|
for key, val in list(CP.defaults().items()):
|
||||||
print '%s: %s' % (key,val)
|
print('%s: %s' % (key,val))
|
||||||
|
|
||||||
for section in CP.sections():
|
for section in CP.sections():
|
||||||
print '[%s]' % section
|
print('[%s]' % section)
|
||||||
for opt in CP.options(section):
|
for opt in CP.options(section):
|
||||||
print ' %s=%s' % (opt, CP.get(section, opt))
|
print(' %s=%s' % (opt, CP.get(section, opt)))
|
||||||
|
|
|
@ -34,7 +34,7 @@ def cluster(drills, tolerance, debug = _DEBUG):
|
||||||
debug_print("Clustering drill sizes ...", True)
|
debug_print("Clustering drill sizes ...", True)
|
||||||
|
|
||||||
# Loop through all drill sizes
|
# Loop through all drill sizes
|
||||||
sizes = drills.keys()
|
sizes = list(drills.keys())
|
||||||
sizes.sort()
|
sizes.sort()
|
||||||
for size in sizes:
|
for size in sizes:
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ def remap(jobs, globalToolMap, debug = _DEBUG):
|
||||||
debug_print( str(job.xcommands) )
|
debug_print( str(job.xcommands) )
|
||||||
new_tools = {}
|
new_tools = {}
|
||||||
new_commands = {}
|
new_commands = {}
|
||||||
for tool, diam in job.xdiam.items():
|
for tool, diam in list(job.xdiam.items()):
|
||||||
|
|
||||||
##debug_print("\n Current tool: " + tool + " (" + str_d(diam) + ")")
|
##debug_print("\n Current tool: " + tool + " (" + str_d(diam) + ")")
|
||||||
|
|
||||||
|
@ -147,9 +147,9 @@ def debug_print(text, status = False, newLine = True):
|
||||||
|
|
||||||
if _DEBUG or (status and _STATUS):
|
if _DEBUG or (status and _STATUS):
|
||||||
if newLine:
|
if newLine:
|
||||||
print " ", text
|
print(" ", text)
|
||||||
else:
|
else:
|
||||||
print " ", text,
|
print(" ", text, end=' ')
|
||||||
|
|
||||||
def str_d(drills):
|
def str_d(drills):
|
||||||
"""
|
"""
|
||||||
|
@ -180,7 +180,7 @@ def drillsToString(drills):
|
||||||
"""
|
"""
|
||||||
string = ""
|
string = ""
|
||||||
|
|
||||||
drills = drills.items()
|
drills = list(drills.items())
|
||||||
drills.sort()
|
drills.sort()
|
||||||
for size, drill in drills:
|
for size, drill in drills:
|
||||||
string += drill + " = " + str_d(size) + "\n "
|
string += drill + " = " + str_d(size) + "\n "
|
||||||
|
@ -194,7 +194,7 @@ def drillsToString(drills):
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
import random
|
import random
|
||||||
|
|
||||||
print " Clustering random drills..."
|
print(" Clustering random drills...")
|
||||||
|
|
||||||
old = {}
|
old = {}
|
||||||
tool_num = 0
|
tool_num = 0
|
||||||
|
|
|
@ -25,7 +25,7 @@ def writeDrillHits(fid, Place, Tools):
|
||||||
try:
|
try:
|
||||||
size = config.GlobalToolMap[tool]
|
size = config.GlobalToolMap[tool]
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "INTERNAL ERROR: Tool code %s not found in global tool list" % tool
|
raise RuntimeError("INTERNAL ERROR: Tool code %s not found in global tool list" % tool)
|
||||||
|
|
||||||
#for row in Layout:
|
#for row in Layout:
|
||||||
# row.writeDrillHits(fid, size, toolNumber)
|
# row.writeDrillHits(fid, size, toolNumber)
|
||||||
|
@ -157,9 +157,9 @@ def writeUserText(fid, X, Y):
|
||||||
if not fname: return
|
if not fname: return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tfile = file(fname, 'rt')
|
tfile = open(fname, 'rt')
|
||||||
except Exception, detail:
|
except Exception as detail:
|
||||||
raise RuntimeError, "Could not open fabrication drawing text file '%s':\n %s" % (fname,str(detail))
|
raise RuntimeError("Could not open fabrication drawing text file '%s':\n %s" % (fname,str(detail)))
|
||||||
|
|
||||||
lines = tfile.readlines()
|
lines = tfile.readlines()
|
||||||
tfile.close()
|
tfile.close()
|
||||||
|
@ -170,7 +170,7 @@ def writeUserText(fid, X, Y):
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
# Get rid of CR
|
# Get rid of CR
|
||||||
line = string.replace(line, '\x0D', '')
|
line = str.replace(line, '\x0D', '')
|
||||||
|
|
||||||
# Chop off \n
|
# Chop off \n
|
||||||
#if line[-1] in string.whitespace:
|
#if line[-1] in string.whitespace:
|
||||||
|
|
|
@ -15,7 +15,7 @@ import math
|
||||||
|
|
||||||
# Ensure all list elements are unique
|
# Ensure all list elements are unique
|
||||||
def uniqueify(L):
|
def uniqueify(L):
|
||||||
return {}.fromkeys(L).keys()
|
return list({}.fromkeys(L).keys())
|
||||||
|
|
||||||
# This function rounds an (X,Y) point to integer co-ordinates
|
# This function rounds an (X,Y) point to integer co-ordinates
|
||||||
def roundPoint(pt):
|
def roundPoint(pt):
|
||||||
|
@ -343,4 +343,4 @@ if __name__=="__main__":
|
||||||
assert isRect1InRect2( (100,100,500,500), (0,600,300,300) ) == False
|
assert isRect1InRect2( (100,100,500,500), (0,600,300,300) ) == False
|
||||||
assert isRect1InRect2( (100,100,500,500), (0,0,500,500) ) == True
|
assert isRect1InRect2( (100,100,500,500), (0,0,500,500) ) == True
|
||||||
|
|
||||||
print 'All tests pass'
|
print('All tests pass')
|
||||||
|
|
|
@ -60,8 +60,7 @@ config.PlacementFile = None
|
||||||
GUI = None
|
GUI = None
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print \
|
print("""
|
||||||
"""
|
|
||||||
Usage: gerbmerge [Options] configfile [layoutfile]
|
Usage: gerbmerge [Options] configfile [layoutfile]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
@ -89,7 +88,7 @@ the layout file (if any) is ignored.
|
||||||
|
|
||||||
NOTE: The dimensions of each job are determined solely by the maximum extent of
|
NOTE: The dimensions of each job are determined solely by the maximum extent of
|
||||||
the board outline layer for each job.
|
the board outline layer for each job.
|
||||||
"""
|
""")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# changed these two writeGerberHeader files to take metric units (mm) into account:
|
# changed these two writeGerberHeader files to take metric units (mm) into account:
|
||||||
|
@ -146,14 +145,14 @@ G71*
|
||||||
writeGerberHeader = writeGerberHeader22degrees
|
writeGerberHeader = writeGerberHeader22degrees
|
||||||
|
|
||||||
def writeApertureMacros(fid, usedDict):
|
def writeApertureMacros(fid, usedDict):
|
||||||
keys = config.GAMT.keys()
|
keys = list(config.GAMT.keys())
|
||||||
keys.sort()
|
keys.sort()
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key in usedDict:
|
if key in usedDict:
|
||||||
config.GAMT[key].writeDef(fid)
|
config.GAMT[key].writeDef(fid)
|
||||||
|
|
||||||
def writeApertures(fid, usedDict):
|
def writeApertures(fid, usedDict):
|
||||||
keys = config.GAT.keys()
|
keys = list(config.GAT.keys())
|
||||||
keys.sort()
|
keys.sort()
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key in usedDict:
|
if key in usedDict:
|
||||||
|
@ -254,7 +253,7 @@ def writeCropMarks(fid, drawing_code, OriginX, OriginY, MaxXExtent, MaxYExtent):
|
||||||
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+cropW), util.in2gerb(y+0.000)))
|
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+cropW), util.in2gerb(y+0.000)))
|
||||||
|
|
||||||
def disclaimer():
|
def disclaimer():
|
||||||
print """
|
print("""
|
||||||
****************************************************
|
****************************************************
|
||||||
* R E A D C A R E F U L L Y *
|
* R E A D C A R E F U L L Y *
|
||||||
* *
|
* *
|
||||||
|
@ -279,14 +278,14 @@ def disclaimer():
|
||||||
To agree to the above terms, press 'y' then Enter.
|
To agree to the above terms, press 'y' then Enter.
|
||||||
Any other key will exit the program.
|
Any other key will exit the program.
|
||||||
|
|
||||||
"""
|
""")
|
||||||
|
|
||||||
s = raw_input()
|
s = input()
|
||||||
if s == 'y':
|
if s == 'y':
|
||||||
print
|
print()
|
||||||
return
|
return
|
||||||
|
|
||||||
print "\nExiting..."
|
print("\nExiting...")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def tile_jobs(Jobs):
|
def tile_jobs(Jobs):
|
||||||
|
@ -318,9 +317,9 @@ def tile_jobs(Jobs):
|
||||||
if not tile:
|
if not tile:
|
||||||
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
raise RuntimeError, 'Panel size %.2f"x%.2f" is too small to hold jobs' % (PX,PY)
|
raise RuntimeError('Panel size %.2f"x%.2f" is too small to hold jobs' % (PX,PY))
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, 'Panel size %.2fmmx%.2fmm is too small to hold jobs' % (PX,PY)
|
raise RuntimeError('Panel size %.2fmmx%.2fmm is too small to hold jobs' % (PX,PY))
|
||||||
|
|
||||||
return tile
|
return tile
|
||||||
|
|
||||||
|
@ -339,7 +338,7 @@ def merge(opts, args, gui = None):
|
||||||
elif arg=='normal':
|
elif arg=='normal':
|
||||||
writeGerberHeader = writeGerberHeader22degrees
|
writeGerberHeader = writeGerberHeader22degrees
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, 'Unknown octagon format'
|
raise RuntimeError('Unknown octagon format')
|
||||||
elif opt in ('--random-search',):
|
elif opt in ('--random-search',):
|
||||||
config.AutoSearchType = RANDOM_SEARCH
|
config.AutoSearchType = RANDOM_SEARCH
|
||||||
elif opt in ('--full-search',):
|
elif opt in ('--full-search',):
|
||||||
|
@ -358,10 +357,10 @@ def merge(opts, args, gui = None):
|
||||||
elif opt in ('-s', '--skipdisclaimer'):
|
elif opt in ('-s', '--skipdisclaimer'):
|
||||||
skipDisclaimer = 1
|
skipDisclaimer = 1
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Unknown option: %s" % opt
|
raise RuntimeError("Unknown option: %s" % opt)
|
||||||
|
|
||||||
if len(args) > 2 or len(args) < 1:
|
if len(args) > 2 or len(args) < 1:
|
||||||
raise RuntimeError, 'Invalid number of arguments'
|
raise RuntimeError('Invalid number of arguments')
|
||||||
|
|
||||||
if (skipDisclaimer == 0):
|
if (skipDisclaimer == 0):
|
||||||
disclaimer()
|
disclaimer()
|
||||||
|
@ -372,7 +371,7 @@ def merge(opts, args, gui = None):
|
||||||
config.parseConfigFile(args[0])
|
config.parseConfigFile(args[0])
|
||||||
|
|
||||||
# Force all X and Y coordinates positive by adding absolute value of minimum X and Y
|
# Force all X and Y coordinates positive by adding absolute value of minimum X and Y
|
||||||
for name, job in config.Jobs.iteritems():
|
for name, job in config.Jobs.items():
|
||||||
min_x, min_y = job.mincoordinates()
|
min_x, min_y = job.mincoordinates()
|
||||||
shift_x = shift_y = 0
|
shift_x = shift_y = 0
|
||||||
if min_x < 0: shift_x = abs(min_x)
|
if min_x < 0: shift_x = abs(min_x)
|
||||||
|
@ -381,31 +380,31 @@ def merge(opts, args, gui = None):
|
||||||
job.fixcoordinates( shift_x, shift_y )
|
job.fixcoordinates( shift_x, shift_y )
|
||||||
|
|
||||||
# Display job properties
|
# Display job properties
|
||||||
for job in config.Jobs.values():
|
for job in list(config.Jobs.values()):
|
||||||
print 'Job %s:' % job.name,
|
print('Job %s:' % job.name, end=' ')
|
||||||
if job.Repeat > 1:
|
if job.Repeat > 1:
|
||||||
print '(%d instances)' % job.Repeat
|
print('(%d instances)' % job.Repeat)
|
||||||
else:
|
else:
|
||||||
print
|
print()
|
||||||
print ' Extents: (%d,%d)-(%d,%d)' % (job.minx,job.miny,job.maxx,job.maxy)
|
print(' Extents: (%d,%d)-(%d,%d)' % (job.minx,job.miny,job.maxx,job.maxy))
|
||||||
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
print ' Size: %f" x %f"' % (job.width_in(), job.height_in())
|
print(' Size: %f" x %f"' % (job.width_in(), job.height_in()))
|
||||||
else:
|
else:
|
||||||
print ' Size: %5.3fmm x %5.3fmm' % (job.width_in(), job.height_in())
|
print(' Size: %5.3fmm x %5.3fmm' % (job.width_in(), job.height_in()))
|
||||||
print
|
print()
|
||||||
|
|
||||||
# Trim drill locations and flash data to board extents
|
# Trim drill locations and flash data to board extents
|
||||||
if config.TrimExcellon:
|
if config.TrimExcellon:
|
||||||
updateGUI("Trimming Excellon data...")
|
updateGUI("Trimming Excellon data...")
|
||||||
print 'Trimming Excellon data to board outlines ...'
|
print('Trimming Excellon data to board outlines ...')
|
||||||
for job in config.Jobs.values():
|
for job in list(config.Jobs.values()):
|
||||||
job.trimExcellon()
|
job.trimExcellon()
|
||||||
|
|
||||||
if config.TrimGerber:
|
if config.TrimGerber:
|
||||||
updateGUI("Trimming Gerber data...")
|
updateGUI("Trimming Gerber data...")
|
||||||
print 'Trimming Gerber data to board outlines ...'
|
print('Trimming Gerber data to board outlines ...')
|
||||||
for job in config.Jobs.values():
|
for job in list(config.Jobs.values()):
|
||||||
job.trimGerber()
|
job.trimGerber()
|
||||||
|
|
||||||
# We start origin at (0.1", 0.1") just so we don't get numbers close to 0
|
# We start origin at (0.1", 0.1") just so we don't get numbers close to 0
|
||||||
|
@ -416,7 +415,7 @@ def merge(opts, args, gui = None):
|
||||||
# Read the layout file and construct the nested list of jobs. If there
|
# Read the layout file and construct the nested list of jobs. If there
|
||||||
# is no layout file, do auto-layout.
|
# is no layout file, do auto-layout.
|
||||||
updateGUI("Performing layout...")
|
updateGUI("Performing layout...")
|
||||||
print 'Performing layout ...'
|
print('Performing layout ...')
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
Layout = parselayout.parseLayoutFile(args[1])
|
Layout = parselayout.parseLayoutFile(args[1])
|
||||||
|
|
||||||
|
@ -439,7 +438,7 @@ def merge(opts, args, gui = None):
|
||||||
Place.addFromFile(config.PlacementFile, config.Jobs)
|
Place.addFromFile(config.PlacementFile, config.Jobs)
|
||||||
else:
|
else:
|
||||||
# Do an automatic layout based on our tiling algorithm.
|
# Do an automatic layout based on our tiling algorithm.
|
||||||
tile = tile_jobs(config.Jobs.values())
|
tile = tile_jobs(list(config.Jobs.values()))
|
||||||
|
|
||||||
Place = placement.Placement()
|
Place = placement.Placement()
|
||||||
Place.addFromTiling(tile, OriginX + config.Config['leftmargin'], OriginY + config.Config['bottommargin'])
|
Place.addFromTiling(tile, OriginX + config.Config['leftmargin'], OriginY + config.Config['bottommargin'])
|
||||||
|
@ -491,9 +490,9 @@ def merge(opts, args, gui = None):
|
||||||
drawing_code1 = aptable.addToApertureTable(AP)
|
drawing_code1 = aptable.addToApertureTable(AP)
|
||||||
|
|
||||||
updateGUI("Writing merged files...")
|
updateGUI("Writing merged files...")
|
||||||
print 'Writing merged output files ...'
|
print('Writing merged output files ...')
|
||||||
|
|
||||||
for layername in config.LayerList.keys():
|
for layername in list(config.LayerList.keys()):
|
||||||
lname = layername
|
lname = layername
|
||||||
if lname[0]=='*':
|
if lname[0]=='*':
|
||||||
lname = lname[1:]
|
lname = lname[1:]
|
||||||
|
@ -504,7 +503,7 @@ def merge(opts, args, gui = None):
|
||||||
fullname = 'merged.%s.ger' % lname
|
fullname = 'merged.%s.ger' % lname
|
||||||
OutputFiles.append(fullname)
|
OutputFiles.append(fullname)
|
||||||
#print 'Writing %s ...' % fullname
|
#print 'Writing %s ...' % fullname
|
||||||
fid = file(fullname, 'wt')
|
fid = open(fullname, 'wt')
|
||||||
writeGerberHeader(fid)
|
writeGerberHeader(fid)
|
||||||
|
|
||||||
# Determine which apertures and macros are truly needed
|
# Determine which apertures and macros are truly needed
|
||||||
|
@ -516,12 +515,12 @@ def merge(opts, args, gui = None):
|
||||||
apmUsedDict.update(apmd)
|
apmUsedDict.update(apmd)
|
||||||
|
|
||||||
# Increase aperature sizes to match minimum feature dimension
|
# Increase aperature sizes to match minimum feature dimension
|
||||||
if config.MinimumFeatureDimension.has_key(layername):
|
if layername in config.MinimumFeatureDimension:
|
||||||
|
|
||||||
print ' Thickening', lname, 'feature dimensions ...'
|
print(' Thickening', lname, 'feature dimensions ...')
|
||||||
|
|
||||||
# Fix each aperture used in this layer
|
# Fix each aperture used in this layer
|
||||||
for ap in apUsedDict.keys():
|
for ap in list(apUsedDict.keys()):
|
||||||
new = config.GAT[ap].getAdjusted( config.MinimumFeatureDimension[layername] )
|
new = config.GAT[ap].getAdjusted( config.MinimumFeatureDimension[layername] )
|
||||||
if not new: ## current aperture size met minimum requirement
|
if not new: ## current aperture size met minimum requirement
|
||||||
continue
|
continue
|
||||||
|
@ -595,7 +594,7 @@ def merge(opts, args, gui = None):
|
||||||
if fullname and fullname.lower() != "none":
|
if fullname and fullname.lower() != "none":
|
||||||
OutputFiles.append(fullname)
|
OutputFiles.append(fullname)
|
||||||
#print 'Writing %s ...' % fullname
|
#print 'Writing %s ...' % fullname
|
||||||
fid = file(fullname, 'wt')
|
fid = open(fullname, 'wt')
|
||||||
writeGerberHeader(fid)
|
writeGerberHeader(fid)
|
||||||
|
|
||||||
# Write width-1 aperture to file
|
# Write width-1 aperture to file
|
||||||
|
@ -624,7 +623,7 @@ def merge(opts, args, gui = None):
|
||||||
if fullname and fullname.lower() != "none":
|
if fullname and fullname.lower() != "none":
|
||||||
OutputFiles.append(fullname)
|
OutputFiles.append(fullname)
|
||||||
#print 'Writing %s ...' % fullname
|
#print 'Writing %s ...' % fullname
|
||||||
fid = file(fullname, 'wt')
|
fid = open(fullname, 'wt')
|
||||||
writeGerberHeader(fid)
|
writeGerberHeader(fid)
|
||||||
|
|
||||||
# Write width-1 aperture to file
|
# Write width-1 aperture to file
|
||||||
|
@ -644,19 +643,19 @@ def merge(opts, args, gui = None):
|
||||||
# of tools.
|
# of tools.
|
||||||
if 0:
|
if 0:
|
||||||
Tools = {}
|
Tools = {}
|
||||||
for job in config.Jobs.values():
|
for job in list(config.Jobs.values()):
|
||||||
for key in job.xcommands.keys():
|
for key in list(job.xcommands.keys()):
|
||||||
Tools[key] = 1
|
Tools[key] = 1
|
||||||
|
|
||||||
Tools = Tools.keys()
|
Tools = list(Tools.keys())
|
||||||
Tools.sort()
|
Tools.sort()
|
||||||
else:
|
else:
|
||||||
toolNum = 0
|
toolNum = 0
|
||||||
|
|
||||||
# First construct global mapping of diameters to tool numbers
|
# First construct global mapping of diameters to tool numbers
|
||||||
for job in config.Jobs.values():
|
for job in list(config.Jobs.values()):
|
||||||
for tool,diam in job.xdiam.items():
|
for tool,diam in list(job.xdiam.items()):
|
||||||
if config.GlobalToolRMap.has_key(diam):
|
if diam in config.GlobalToolRMap:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
toolNum += 1
|
toolNum += 1
|
||||||
|
@ -665,24 +664,24 @@ def merge(opts, args, gui = None):
|
||||||
# Cluster similar tool sizes to reduce number of drills
|
# Cluster similar tool sizes to reduce number of drills
|
||||||
if config.Config['drillclustertolerance'] > 0:
|
if config.Config['drillclustertolerance'] > 0:
|
||||||
config.GlobalToolRMap = drillcluster.cluster( config.GlobalToolRMap, config.Config['drillclustertolerance'] )
|
config.GlobalToolRMap = drillcluster.cluster( config.GlobalToolRMap, config.Config['drillclustertolerance'] )
|
||||||
drillcluster.remap( Place.jobs, config.GlobalToolRMap.items() )
|
drillcluster.remap( Place.jobs, list(config.GlobalToolRMap.items()) )
|
||||||
|
|
||||||
# Now construct mapping of tool numbers to diameters
|
# Now construct mapping of tool numbers to diameters
|
||||||
for diam,tool in config.GlobalToolRMap.items():
|
for diam,tool in list(config.GlobalToolRMap.items()):
|
||||||
config.GlobalToolMap[tool] = diam
|
config.GlobalToolMap[tool] = diam
|
||||||
|
|
||||||
# Tools is just a list of tool names
|
# Tools is just a list of tool names
|
||||||
Tools = config.GlobalToolMap.keys()
|
Tools = list(config.GlobalToolMap.keys())
|
||||||
Tools.sort()
|
Tools.sort()
|
||||||
|
|
||||||
fullname = config.Config['fabricationdrawingfile']
|
fullname = config.Config['fabricationdrawingfile']
|
||||||
if fullname and fullname.lower() != 'none':
|
if fullname and fullname.lower() != 'none':
|
||||||
if len(Tools) > strokes.MaxNumDrillTools:
|
if len(Tools) > strokes.MaxNumDrillTools:
|
||||||
raise RuntimeError, "Only %d different tool sizes supported for fabrication drawing." % strokes.MaxNumDrillTools
|
raise RuntimeError("Only %d different tool sizes supported for fabrication drawing." % strokes.MaxNumDrillTools)
|
||||||
|
|
||||||
OutputFiles.append(fullname)
|
OutputFiles.append(fullname)
|
||||||
#print 'Writing %s ...' % fullname
|
#print 'Writing %s ...' % fullname
|
||||||
fid = file(fullname, 'wt')
|
fid = open(fullname, 'wt')
|
||||||
writeGerberHeader(fid)
|
writeGerberHeader(fid)
|
||||||
writeApertures(fid, {drawing_code1: None})
|
writeApertures(fid, {drawing_code1: None})
|
||||||
fid.write('%s*\n' % drawing_code1) # Choose drawing aperture
|
fid.write('%s*\n' % drawing_code1) # Choose drawing aperture
|
||||||
|
@ -699,7 +698,7 @@ def merge(opts, args, gui = None):
|
||||||
fullname = 'merged.drills.xln'
|
fullname = 'merged.drills.xln'
|
||||||
OutputFiles.append(fullname)
|
OutputFiles.append(fullname)
|
||||||
#print 'Writing %s ...' % fullname
|
#print 'Writing %s ...' % fullname
|
||||||
fid = file(fullname, 'wt')
|
fid = open(fullname, 'wt')
|
||||||
|
|
||||||
writeExcellonHeader(fid)
|
writeExcellonHeader(fid)
|
||||||
|
|
||||||
|
@ -709,7 +708,7 @@ def merge(opts, args, gui = None):
|
||||||
try:
|
try:
|
||||||
size = config.GlobalToolMap[tool]
|
size = config.GlobalToolMap[tool]
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "INTERNAL ERROR: Tool code %s not found in global tool map" % tool
|
raise RuntimeError("INTERNAL ERROR: Tool code %s not found in global tool map" % tool)
|
||||||
writeExcellonTool(fid, tool, size)
|
writeExcellonTool(fid, tool, size)
|
||||||
|
|
||||||
writeExcellonHeaderEnd(fid)
|
writeExcellonHeaderEnd(fid)
|
||||||
|
@ -755,57 +754,57 @@ def merge(opts, args, gui = None):
|
||||||
fullname = 'merged.toollist.drl'
|
fullname = 'merged.toollist.drl'
|
||||||
OutputFiles.append(fullname)
|
OutputFiles.append(fullname)
|
||||||
#print 'Writing %s ...' % fullname
|
#print 'Writing %s ...' % fullname
|
||||||
fid = file(fullname, 'wt')
|
fid = open(fullname, 'wt')
|
||||||
|
|
||||||
print '-'*50
|
print('-'*50)
|
||||||
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
print ' Job Size : %f" x %f"' % (MaxXExtent-OriginX, MaxYExtent-OriginY)
|
print(' Job Size : %f" x %f"' % (MaxXExtent-OriginX, MaxYExtent-OriginY))
|
||||||
print ' Job Area : %.2f sq. in.' % totalarea
|
print(' Job Area : %.2f sq. in.' % totalarea)
|
||||||
else:
|
else:
|
||||||
print ' Job Size : %.2fmm x %.2fmm' % (MaxXExtent-OriginX, MaxYExtent-OriginY)
|
print(' Job Size : %.2fmm x %.2fmm' % (MaxXExtent-OriginX, MaxYExtent-OriginY))
|
||||||
print ' Job Area : %.0f mm2' % totalarea
|
print(' Job Area : %.0f mm2' % totalarea)
|
||||||
|
|
||||||
print ' Area Usage : %.1f%%' % (jobarea/totalarea*100)
|
print(' Area Usage : %.1f%%' % (jobarea/totalarea*100))
|
||||||
print ' Drill hits : %d' % drillhits
|
print(' Drill hits : %d' % drillhits)
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
print 'Drill density : %.1f hits/sq.in.' % (drillhits/totalarea)
|
print('Drill density : %.1f hits/sq.in.' % (drillhits/totalarea))
|
||||||
else:
|
else:
|
||||||
print 'Drill density : %.2f hits/cm2' % (100*drillhits/totalarea)
|
print('Drill density : %.2f hits/cm2' % (100*drillhits/totalarea))
|
||||||
|
|
||||||
print '\nTool List:'
|
print('\nTool List:')
|
||||||
smallestDrill = 999.9
|
smallestDrill = 999.9
|
||||||
for tool in Tools:
|
for tool in Tools:
|
||||||
if ToolStats[tool]:
|
if ToolStats[tool]:
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
fid.write('%s %.4fin\n' % (tool, config.GlobalToolMap[tool]))
|
fid.write('%s %.4fin\n' % (tool, config.GlobalToolMap[tool]))
|
||||||
print ' %s %.4f" %5d hits' % (tool, config.GlobalToolMap[tool], ToolStats[tool])
|
print(' %s %.4f" %5d hits' % (tool, config.GlobalToolMap[tool], ToolStats[tool]))
|
||||||
else:
|
else:
|
||||||
fid.write('%s %.4fmm\n' % (tool, config.GlobalToolMap[tool]))
|
fid.write('%s %.4fmm\n' % (tool, config.GlobalToolMap[tool]))
|
||||||
print ' %s %.4fmm %5d hits' % (tool, config.GlobalToolMap[tool], ToolStats[tool])
|
print(' %s %.4fmm %5d hits' % (tool, config.GlobalToolMap[tool], ToolStats[tool]))
|
||||||
smallestDrill = min(smallestDrill, config.GlobalToolMap[tool])
|
smallestDrill = min(smallestDrill, config.GlobalToolMap[tool])
|
||||||
|
|
||||||
fid.close()
|
fid.close()
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
print "Smallest Tool: %.4fin" % smallestDrill
|
print("Smallest Tool: %.4fin" % smallestDrill)
|
||||||
else:
|
else:
|
||||||
print "Smallest Tool: %.4fmm" % smallestDrill
|
print("Smallest Tool: %.4fmm" % smallestDrill)
|
||||||
|
|
||||||
print
|
print()
|
||||||
print 'Output Files :'
|
print('Output Files :')
|
||||||
for f in OutputFiles:
|
for f in OutputFiles:
|
||||||
print ' ', f
|
print(' ', f)
|
||||||
|
|
||||||
if (MaxXExtent-OriginX)>config.Config['panelwidth'] or (MaxYExtent-OriginY)>config.Config['panelheight']:
|
if (MaxXExtent-OriginX)>config.Config['panelwidth'] or (MaxYExtent-OriginY)>config.Config['panelheight']:
|
||||||
print '*'*75
|
print('*'*75)
|
||||||
print '*'
|
print('*')
|
||||||
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
print '* ERROR: Merged job exceeds panel dimensions of %.1f"x%.1f"' % (config.Config['panelwidth'],config.Config['panelheight'])
|
print('* ERROR: Merged job exceeds panel dimensions of %.1f"x%.1f"' % (config.Config['panelwidth'],config.Config['panelheight']))
|
||||||
else:
|
else:
|
||||||
print '* ERROR: Merged job exceeds panel dimensions of %.1fmmx%.1fmm' % (config.Config['panelwidth'],config.Config['panelheight'])
|
print('* ERROR: Merged job exceeds panel dimensions of %.1fmmx%.1fmm' % (config.Config['panelwidth'],config.Config['panelheight']))
|
||||||
print '*'
|
print('*')
|
||||||
print '*'*75
|
print('*'*75)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Done!
|
# Done!
|
||||||
|
@ -826,19 +825,19 @@ def main():
|
||||||
if opt in ('-h', '--help'):
|
if opt in ('-h', '--help'):
|
||||||
usage()
|
usage()
|
||||||
elif opt in ('-v', '--version'):
|
elif opt in ('-v', '--version'):
|
||||||
print """
|
print("""
|
||||||
GerbMerge Version %s -- Combine multiple Gerber/Excellon files
|
GerbMerge Version %s -- Combine multiple Gerber/Excellon files
|
||||||
|
|
||||||
This program is licensed under the GNU General Public License (GPL)
|
This program is licensed under the GNU General Public License (GPL)
|
||||||
Version 3. See LICENSE file or http://www.fsf.org for details of this license.
|
Version 3. See LICENSE file or http://www.fsf.org for details of this license.
|
||||||
|
|
||||||
ProvideYourOwn - http://provideyourown.com
|
ProvideYourOwn - http://provideyourown.com
|
||||||
""" % (__version__)
|
""" % (__version__))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif opt in ('--octagons', '--random-search','--full-search','--rs-fsjobs','--place-file','--no-trim-gerber','--no-trim-excellon', '--search-timeout', '-s', '--skipdisclaimer'):
|
elif opt in ('--octagons', '--random-search','--full-search','--rs-fsjobs','--place-file','--no-trim-gerber','--no-trim-excellon', '--search-timeout', '-s', '--skipdisclaimer'):
|
||||||
pass ## arguments are valid
|
pass ## arguments are valid
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Unknown option: %s" % opt
|
raise RuntimeError("Unknown option: %s" % opt)
|
||||||
|
|
||||||
if len(args) > 2 or len(args) < 1:
|
if len(args) > 2 or len(args) < 1:
|
||||||
usage()
|
usage()
|
||||||
|
|
|
@ -18,7 +18,7 @@ http://github.com/unwireddevices/gerbmerge
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import __builtin__
|
import builtins
|
||||||
import copy
|
import copy
|
||||||
import types
|
import types
|
||||||
|
|
||||||
|
@ -235,17 +235,17 @@ class Job:
|
||||||
self.maxy += y_shift
|
self.maxy += y_shift
|
||||||
|
|
||||||
# Shift all commands
|
# Shift all commands
|
||||||
for layer, command in self.commands.iteritems():
|
for layer, command in self.commands.items():
|
||||||
|
|
||||||
# Loop through each command in each layer
|
# Loop through each command in each layer
|
||||||
for index in range( len(command) ):
|
for index in range( len(command) ):
|
||||||
c = command[index]
|
c = command[index]
|
||||||
|
|
||||||
# Shift X and Y coordinate of command
|
# Shift X and Y coordinate of command
|
||||||
if type(c) == types.TupleType: ## ensure that command is of type tuple
|
if type(c) == tuple: ## ensure that command is of type tuple
|
||||||
command_list = list(c) ## convert tuple to list
|
command_list = list(c) ## convert tuple to list
|
||||||
if (type( command_list[0] ) == types.IntType) \
|
if (type( command_list[0] ) == int) \
|
||||||
and (type( command_list[1] ) == types.IntType): ## ensure that first two elemenst are integers
|
and (type( command_list[1] ) == int): ## ensure that first two elemenst are integers
|
||||||
command_list[0] += x_shift
|
command_list[0] += x_shift
|
||||||
command_list[1] += y_shift
|
command_list[1] += y_shift
|
||||||
command[index] = tuple(command_list) ## convert list back to tuple
|
command[index] = tuple(command_list) ## convert list back to tuple
|
||||||
|
@ -253,7 +253,7 @@ class Job:
|
||||||
self.commands[layer] = command ## set modified command
|
self.commands[layer] = command ## set modified command
|
||||||
|
|
||||||
# Shift all excellon commands
|
# Shift all excellon commands
|
||||||
for tool, command in self.xcommands.iteritems():
|
for tool, command in self.xcommands.items():
|
||||||
|
|
||||||
# Loop through each command in each layer
|
# Loop through each command in each layer
|
||||||
for index in range( len(command) ):
|
for index in range( len(command) ):
|
||||||
|
@ -261,12 +261,12 @@ class Job:
|
||||||
|
|
||||||
# Shift X and Y coordinate of command
|
# Shift X and Y coordinate of command
|
||||||
command_list = list(c) ## convert tuple to list
|
command_list = list(c) ## convert tuple to list
|
||||||
if ( type( command_list[0] ) == types.IntType ) \
|
if ( type( command_list[0] ) == int ) \
|
||||||
and ( type( command_list[1] ) == types.IntType ): ## ensure that first two elemenst are integers
|
and ( type( command_list[1] ) == int ): ## ensure that first two elemenst are integers
|
||||||
command_list[0] += x_shift / 10
|
command_list[0] += x_shift / 10
|
||||||
command_list[1] += y_shift / 10
|
command_list[1] += y_shift / 10
|
||||||
if ( type( command_list[2] ) == types.IntType ) \
|
if ( type( command_list[2] ) == int ) \
|
||||||
and ( type( command_list[3] ) == types.IntType ): ## ensure that first two elemenst are integerslen(command_list) == 4:
|
and ( type( command_list[3] ) == int ): ## ensure that first two elemenst are integerslen(command_list) == 4:
|
||||||
# G85 command, need to shift the second pair of xy, too.
|
# G85 command, need to shift the second pair of xy, too.
|
||||||
command_list[2] += x_shift / 10
|
command_list[2] += x_shift / 10
|
||||||
command_list[3] += y_shift / 10
|
command_list[3] += y_shift / 10
|
||||||
|
@ -286,7 +286,7 @@ class Job:
|
||||||
|
|
||||||
#print 'Reading data from %s ...' % fullname
|
#print 'Reading data from %s ...' % fullname
|
||||||
|
|
||||||
fid = file(fullname, 'rt')
|
fid = open(fullname, 'rt')
|
||||||
currtool = None
|
currtool = None
|
||||||
|
|
||||||
self.apxlat[layername] = {}
|
self.apxlat[layername] = {}
|
||||||
|
@ -338,7 +338,7 @@ class Job:
|
||||||
|
|
||||||
for line in fid:
|
for line in fid:
|
||||||
# Get rid of CR characters (0x0D) and leading/trailing blanks
|
# Get rid of CR characters (0x0D) and leading/trailing blanks
|
||||||
line = string.replace(line, '\x0D', '').strip()
|
line = str.replace(line, '\x0D', '').strip()
|
||||||
|
|
||||||
# Old location of format_pat search. Now moved down into the sub-line parse loop below.
|
# Old location of format_pat search. Now moved down into the sub-line parse loop below.
|
||||||
|
|
||||||
|
@ -354,11 +354,11 @@ class Job:
|
||||||
match = apdef_pat.match(line)
|
match = apdef_pat.match(line)
|
||||||
if match:
|
if match:
|
||||||
if currtool:
|
if currtool:
|
||||||
raise RuntimeError, "File %s has an aperture definition that comes after drawing commands." % fullname
|
raise RuntimeError("File %s has an aperture definition that comes after drawing commands." % fullname)
|
||||||
|
|
||||||
A = aptable.parseAperture(line, self.apmxlat[layername])
|
A = aptable.parseAperture(line, self.apmxlat[layername])
|
||||||
if not A:
|
if not A:
|
||||||
raise RuntimeError, "Unknown aperture definition in file %s" % fullname
|
raise RuntimeError("Unknown aperture definition in file %s" % fullname)
|
||||||
# [andreika]: apply units
|
# [andreika]: apply units
|
||||||
if type(A.dimx) == float or type(A.dimx) == int:
|
if type(A.dimx) == float or type(A.dimx) == int:
|
||||||
A.dimx *= units_div
|
A.dimx *= units_div
|
||||||
|
@ -366,11 +366,11 @@ class Job:
|
||||||
A.dimy *= units_div
|
A.dimy *= units_div
|
||||||
|
|
||||||
hash = A.hash()
|
hash = A.hash()
|
||||||
if not RevGAT.has_key(hash):
|
if hash not in RevGAT:
|
||||||
#print line
|
#print line
|
||||||
#print self.apmxlat
|
#print self.apmxlat
|
||||||
#print RevGAT
|
#print RevGAT
|
||||||
raise RuntimeError, 'File %s has aperture definition "%s" not in global aperture table.' % (fullname, hash)
|
raise RuntimeError('File %s has aperture definition "%s" not in global aperture table.' % (fullname, hash))
|
||||||
|
|
||||||
# This says that all draw commands with this aperture code will
|
# This says that all draw commands with this aperture code will
|
||||||
# be replaced by aperture self.apxlat[layername][code].
|
# be replaced by aperture self.apxlat[layername][code].
|
||||||
|
@ -402,7 +402,7 @@ class Job:
|
||||||
continue # ignore it so func doesn't choke on it
|
continue # ignore it so func doesn't choke on it
|
||||||
|
|
||||||
if line[:3] == '%SF': # scale factor - we will ignore it
|
if line[:3] == '%SF': # scale factor - we will ignore it
|
||||||
print 'Scale factor parameter ignored: ' + line
|
print('Scale factor parameter ignored: ' + line)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# end basic diptrace fixes
|
# end basic diptrace fixes
|
||||||
|
@ -411,11 +411,11 @@ class Job:
|
||||||
M = amacro.parseApertureMacro(line,fid)
|
M = amacro.parseApertureMacro(line,fid)
|
||||||
if M:
|
if M:
|
||||||
if currtool:
|
if currtool:
|
||||||
raise RuntimeError, "File %s has an aperture macro definition that comes after drawing commands." % fullname
|
raise RuntimeError("File %s has an aperture macro definition that comes after drawing commands." % fullname)
|
||||||
|
|
||||||
hash = M.hash()
|
hash = M.hash()
|
||||||
if not RevGAMT.has_key(hash):
|
if hash not in RevGAMT:
|
||||||
raise RuntimeError, 'File %s has aperture macro definition not in global aperture macro table:\n%s' % (fullname, hash)
|
raise RuntimeError('File %s has aperture macro definition not in global aperture macro table:\n%s' % (fullname, hash))
|
||||||
|
|
||||||
# This says that all aperture definition commands that reference this macro name
|
# This says that all aperture definition commands that reference this macro name
|
||||||
# will be replaced by aperture macro name self.apmxlat[layername][macroname].
|
# will be replaced by aperture macro name self.apmxlat[layername][macroname].
|
||||||
|
@ -445,9 +445,9 @@ class Job:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if item[0]=='T': # omit trailing zeroes
|
if item[0]=='T': # omit trailing zeroes
|
||||||
raise RuntimeError, "Trailing zeroes not supported in RS274X files"
|
raise RuntimeError("Trailing zeroes not supported in RS274X files")
|
||||||
if item[0]=='I': # incremental co-ordinates
|
if item[0]=='I': # incremental co-ordinates
|
||||||
raise RuntimeError, "Incremental co-ordinates not supported in RS274X files"
|
raise RuntimeError("Incremental co-ordinates not supported in RS274X files")
|
||||||
|
|
||||||
if item[0]=='N': # Maximum digits for N* commands...ignore it
|
if item[0]=='N': # Maximum digits for N* commands...ignore it
|
||||||
continue
|
continue
|
||||||
|
@ -508,7 +508,7 @@ class Job:
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raise RuntimeError, "G-Code 'G%02d' is not supported" % gcode
|
raise RuntimeError("G-Code 'G%02d' is not supported" % gcode)
|
||||||
|
|
||||||
# See if this is a tool change (aperture change) command
|
# See if this is a tool change (aperture change) command
|
||||||
match = tool_pat.match(sub_line)
|
match = tool_pat.match(sub_line)
|
||||||
|
@ -541,8 +541,8 @@ class Job:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Map it using our translation table
|
# Map it using our translation table
|
||||||
if not self.apxlat[layername].has_key(currtool):
|
if currtool not in self.apxlat[layername]:
|
||||||
raise RuntimeError, 'File %s has tool change command "%s" with no corresponding translation' % (fullname, currtool)
|
raise RuntimeError('File %s has tool change command "%s" with no corresponding translation' % (fullname, currtool))
|
||||||
|
|
||||||
currtool = self.apxlat[layername][currtool]
|
currtool = self.apxlat[layername][currtool]
|
||||||
|
|
||||||
|
@ -561,17 +561,17 @@ class Job:
|
||||||
match = drawXY_pat.match(sub_line)
|
match = drawXY_pat.match(sub_line)
|
||||||
isLastShorthand = False # By default assume we don't make use of last_x and last_y
|
isLastShorthand = False # By default assume we don't make use of last_x and last_y
|
||||||
if match:
|
if match:
|
||||||
x, y, d = map(__builtin__.int, match.groups())
|
x, y, d = list(map(builtins.int, match.groups()))
|
||||||
else:
|
else:
|
||||||
match = drawX_pat.match(sub_line)
|
match = drawX_pat.match(sub_line)
|
||||||
if match:
|
if match:
|
||||||
x, d = map(__builtin__.int, match.groups())
|
x, d = list(map(builtins.int, match.groups()))
|
||||||
y = last_y
|
y = last_y
|
||||||
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||||||
else:
|
else:
|
||||||
match = drawY_pat.match(sub_line)
|
match = drawY_pat.match(sub_line)
|
||||||
if match:
|
if match:
|
||||||
y, d = map(__builtin__.int, match.groups())
|
y, d = list(map(builtins.int, match.groups()))
|
||||||
x = last_x
|
x = last_x
|
||||||
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||||||
|
|
||||||
|
@ -579,17 +579,17 @@ class Job:
|
||||||
if match is None:
|
if match is None:
|
||||||
match = cdrawXY_pat.match(sub_line)
|
match = cdrawXY_pat.match(sub_line)
|
||||||
if match:
|
if match:
|
||||||
x, y, I, J, d = map(__builtin__.int, match.groups())
|
x, y, I, J, d = list(map(builtins.int, match.groups()))
|
||||||
else:
|
else:
|
||||||
match = cdrawX_pat.match(sub_line)
|
match = cdrawX_pat.match(sub_line)
|
||||||
if match:
|
if match:
|
||||||
x, I, J, d = map(__builtin__.int, match.groups())
|
x, I, J, d = list(map(builtins.int, match.groups()))
|
||||||
y = last_y
|
y = last_y
|
||||||
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||||||
else:
|
else:
|
||||||
match = cdrawY_pat.match(sub_line)
|
match = cdrawY_pat.match(sub_line)
|
||||||
if match:
|
if match:
|
||||||
y, I, J, d = map(__builtin__.int, match.groups())
|
y, I, J, d = list(map(builtins.int, match.groups()))
|
||||||
x = last_x
|
x = last_x
|
||||||
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||||||
|
|
||||||
|
@ -601,7 +601,7 @@ class Job:
|
||||||
if (d != 2) and (last_gmode != 36):
|
if (d != 2) and (last_gmode != 36):
|
||||||
# [andreika]: check for fill mode more accurately
|
# [andreika]: check for fill mode more accurately
|
||||||
if not in_fill_mode:
|
if not in_fill_mode:
|
||||||
raise RuntimeError, 'File %s has draw command %s with no aperture chosen' % (fullname, sub_line)
|
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
|
# 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!
|
# flashes (e.g., Y with no X) will be scaled twice!
|
||||||
|
@ -649,7 +649,7 @@ class Job:
|
||||||
if match:
|
if match:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, 'File %s has uninterpretable line:\n %s' % (fullname, line)
|
raise RuntimeError('File %s has uninterpretable line:\n %s' % (fullname, line))
|
||||||
|
|
||||||
sub_line = sub_line[match.end():]
|
sub_line = sub_line[match.end():]
|
||||||
# end while still things to match on this line
|
# end while still things to match on this line
|
||||||
|
@ -657,13 +657,13 @@ class Job:
|
||||||
|
|
||||||
fid.close()
|
fid.close()
|
||||||
if 0:
|
if 0:
|
||||||
print layername
|
print(layername)
|
||||||
print self.commands[layername]
|
print(self.commands[layername])
|
||||||
|
|
||||||
def parseExcellon(self, fullname):
|
def parseExcellon(self, fullname):
|
||||||
#print 'Reading data from %s ...' % fullname
|
#print 'Reading data from %s ...' % fullname
|
||||||
|
|
||||||
fid = file(fullname, 'rt')
|
fid = open(fullname, 'rt')
|
||||||
currtool = None
|
currtool = None
|
||||||
suppress_leading = True # Suppress leading zeros by default, equivalent to 'INCH,TZ'
|
suppress_leading = True # Suppress leading zeros by default, equivalent to 'INCH,TZ'
|
||||||
|
|
||||||
|
@ -706,14 +706,14 @@ class Job:
|
||||||
V.append(None)
|
V.append(None)
|
||||||
return tuple(V)
|
return tuple(V)
|
||||||
|
|
||||||
for line in fid.xreadlines():
|
for line in fid:
|
||||||
# Get rid of CR characters
|
# Get rid of CR characters
|
||||||
line = string.replace(line, '\x0D', '')
|
line = str.replace(line, '\x0D', '')
|
||||||
|
|
||||||
# add support for DipTrace
|
# add support for DipTrace
|
||||||
if line[:6]=='METRIC':
|
if line[:6]=='METRIC':
|
||||||
if (config.Config['measurementunits'] == 'inch'):
|
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)
|
||||||
else:
|
else:
|
||||||
#print "ignoring METRIC directive: " + line
|
#print "ignoring METRIC directive: " + line
|
||||||
continue # ignore it so func doesn't choke on it
|
continue # ignore it so func doesn't choke on it
|
||||||
|
@ -746,14 +746,14 @@ class Job:
|
||||||
try:
|
try:
|
||||||
diam = float(diam)
|
diam = float(diam)
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "File %s has illegal tool diameter '%s'" % (fullname, diam)
|
raise RuntimeError("File %s has illegal tool diameter '%s'" % (fullname, diam))
|
||||||
|
|
||||||
# Canonicalize tool number because Protel (of course) sometimes specifies it
|
# Canonicalize tool number because Protel (of course) sometimes specifies it
|
||||||
# as T01 and sometimes as T1. We canonicalize to T01.
|
# as T01 and sometimes as T1. We canonicalize to T01.
|
||||||
currtool = 'T%02d' % int(currtool[1:])
|
currtool = 'T%02d' % int(currtool[1:])
|
||||||
|
|
||||||
if self.xdiam.has_key(currtool):
|
if currtool in self.xdiam:
|
||||||
raise RuntimeError, "File %s defines tool %s more than once" % (fullname, currtool)
|
raise RuntimeError("File %s defines tool %s more than once" % (fullname, currtool))
|
||||||
self.xdiam[currtool] = diam
|
self.xdiam[currtool] = diam
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -779,13 +779,13 @@ class Job:
|
||||||
try:
|
try:
|
||||||
diam = self.ToolList[currtool]
|
diam = self.ToolList[currtool]
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "File %s uses tool code %s that is not defined in the job's tool list" % (fullname, currtool)
|
raise RuntimeError("File %s uses tool code %s that is not defined in the job's tool list" % (fullname, currtool))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
diam = config.DefaultToolList[currtool]
|
diam = config.DefaultToolList[currtool]
|
||||||
except:
|
except:
|
||||||
#print config.DefaultToolList
|
#print config.DefaultToolList
|
||||||
raise RuntimeError, "File %s uses tool code %s that is not defined in default tool list" % (fullname, currtool)
|
raise RuntimeError("File %s uses tool code %s that is not defined in default tool list" % (fullname, currtool))
|
||||||
|
|
||||||
self.xdiam[currtool] = diam
|
self.xdiam[currtool] = diam
|
||||||
continue
|
continue
|
||||||
|
@ -811,7 +811,7 @@ class Job:
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
if currtool is None:
|
if currtool is None:
|
||||||
raise RuntimeError, 'File %s has plunge command without previous tool selection' % fullname
|
raise RuntimeError('File %s has plunge command without previous tool selection' % fullname)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.xcommands[currtool].append((x,y,stop_x,stop_y))
|
self.xcommands[currtool].append((x,y,stop_x,stop_y))
|
||||||
|
@ -827,10 +827,10 @@ class Job:
|
||||||
if pat.match(line):
|
if pat.match(line):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, 'File %s has uninterpretable line:\n %s' % (fullname, line)
|
raise RuntimeError('File %s has uninterpretable line:\n %s' % (fullname, line))
|
||||||
|
|
||||||
def hasLayer(self, layername):
|
def hasLayer(self, layername):
|
||||||
return self.commands.has_key(layername)
|
return layername in self.commands
|
||||||
|
|
||||||
def writeGerber(self, fid, layername, Xoff, Yoff):
|
def writeGerber(self, fid, layername, Xoff, Yoff):
|
||||||
"Write out the data such that the lower-left corner of this job is at the given (X,Y) position, in inches"
|
"Write out the data such that the lower-left corner of this job is at the given (X,Y) position, in inches"
|
||||||
|
@ -858,7 +858,7 @@ class Job:
|
||||||
# due to panelizing.
|
# due to panelizing.
|
||||||
fid.write('X%07dY%07dD02*\n' % (X, Y))
|
fid.write('X%07dY%07dD02*\n' % (X, Y))
|
||||||
for cmd in self.commands[layername]:
|
for cmd in self.commands[layername]:
|
||||||
if type(cmd) is types.TupleType:
|
if type(cmd) is tuple:
|
||||||
if len(cmd)==3:
|
if len(cmd)==3:
|
||||||
x, y, d = cmd
|
x, y, d = cmd
|
||||||
fid.write('X%07dY%07dD%02d*\n' % (x+DX, y+DY, d))
|
fid.write('X%07dY%07dD%02d*\n' % (x+DX, y+DY, d))
|
||||||
|
@ -877,7 +877,7 @@ class Job:
|
||||||
def findTools(self, diameter):
|
def findTools(self, diameter):
|
||||||
"Find the tools, if any, with the given diameter in inches. There may be more than one!"
|
"Find the tools, if any, with the given diameter in inches. There may be more than one!"
|
||||||
L = []
|
L = []
|
||||||
for tool, diam in self.xdiam.items():
|
for tool, diam in list(self.xdiam.items()):
|
||||||
if diam==diameter:
|
if diam==diameter:
|
||||||
L.append(tool)
|
L.append(tool)
|
||||||
return L
|
return L
|
||||||
|
@ -927,7 +927,7 @@ class Job:
|
||||||
|
|
||||||
# Boogie
|
# Boogie
|
||||||
for ltool in ltools:
|
for ltool in ltools:
|
||||||
if self.xcommands.has_key(ltool):
|
if ltool in self.xcommands:
|
||||||
for cmd in self.xcommands[ltool]:
|
for cmd in self.xcommands[ltool]:
|
||||||
x, y, stop_x, stop_y = cmd
|
x, y, stop_x, stop_y = cmd
|
||||||
new_x = x+DX
|
new_x = x+DX
|
||||||
|
@ -965,7 +965,7 @@ class Job:
|
||||||
ltools = self.findTools(diameter)
|
ltools = self.findTools(diameter)
|
||||||
|
|
||||||
for ltool in ltools:
|
for ltool in ltools:
|
||||||
if self.xcommands.has_key(ltool):
|
if ltool in self.xcommands:
|
||||||
for cmd in self.xcommands[ltool]:
|
for cmd in self.xcommands[ltool]:
|
||||||
x, y, stop_x, stop_y = cmd
|
x, y, stop_x, stop_y = cmd
|
||||||
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
||||||
|
@ -979,7 +979,7 @@ class Job:
|
||||||
|
|
||||||
GAT=config.GAT
|
GAT=config.GAT
|
||||||
|
|
||||||
if self.apertures.has_key(layername):
|
if layername in self.apertures:
|
||||||
apdict = {}.fromkeys(self.apertures[layername])
|
apdict = {}.fromkeys(self.apertures[layername])
|
||||||
apmlist = [GAT[ap].dimx for ap in self.apertures[layername] if GAT[ap].apname=='Macro']
|
apmlist = [GAT[ap].dimx for ap in self.apertures[layername] if GAT[ap].apname=='Macro']
|
||||||
apmdict = {}.fromkeys(apmlist)
|
apmdict = {}.fromkeys(apmlist)
|
||||||
|
@ -990,8 +990,8 @@ class Job:
|
||||||
|
|
||||||
def makeLocalApertureCode(self, layername, AP):
|
def makeLocalApertureCode(self, layername, AP):
|
||||||
"Find or create a layer-specific aperture code to represent the global aperture given"
|
"Find or create a layer-specific aperture code to represent the global aperture given"
|
||||||
if AP.code not in self.apxlat[layername].values():
|
if AP.code not in list(self.apxlat[layername].values()):
|
||||||
lastCode = aptable.findHighestApertureCode(self.apxlat[layername].keys())
|
lastCode = aptable.findHighestApertureCode(list(self.apxlat[layername].keys()))
|
||||||
localCode = 'D%d' % (lastCode+1)
|
localCode = 'D%d' % (lastCode+1)
|
||||||
self.apxlat[layername][localCode] = AP.code
|
self.apxlat[layername][localCode] = AP.code
|
||||||
|
|
||||||
|
@ -1008,7 +1008,7 @@ class Job:
|
||||||
lastAperture = None
|
lastAperture = None
|
||||||
|
|
||||||
for cmd in self.commands[layername]:
|
for cmd in self.commands[layername]:
|
||||||
if type(cmd) == types.TupleType:
|
if type(cmd) == tuple:
|
||||||
# It is a data command: tuple (X, Y, D), all integers, or (X, Y, I, J, D), all integers.
|
# It is a data command: tuple (X, Y, D), all integers, or (X, Y, I, J, D), all integers.
|
||||||
if len(cmd)==3:
|
if len(cmd)==3:
|
||||||
x, y, d = cmd
|
x, y, d = cmd
|
||||||
|
@ -1180,12 +1180,12 @@ class Job:
|
||||||
self.commands[layername] = newcmds
|
self.commands[layername] = newcmds
|
||||||
|
|
||||||
def trimGerber(self):
|
def trimGerber(self):
|
||||||
for layername in self.commands.keys():
|
for layername in list(self.commands.keys()):
|
||||||
self.trimGerberLayer(layername)
|
self.trimGerberLayer(layername)
|
||||||
|
|
||||||
def trimExcellon(self):
|
def trimExcellon(self):
|
||||||
"Remove plunge commands that are outside job dimensions"
|
"Remove plunge commands that are outside job dimensions"
|
||||||
keys = self.xcommands.keys()
|
keys = list(self.xcommands.keys())
|
||||||
for toolname in keys:
|
for toolname in keys:
|
||||||
# Remember Excellon is 2.4 format while Gerber data is 2.5 format
|
# Remember Excellon is 2.4 format while Gerber data is 2.5 format
|
||||||
validList = [tup for tup in self.xcommands[toolname]
|
validList = [tup for tup in self.xcommands[toolname]
|
||||||
|
@ -1390,10 +1390,10 @@ def rotateJob(job, degrees = 90, flip = 0, firstpass = True):
|
||||||
# those apertures which have an orientation: rectangles, ovals, and macros.
|
# those apertures which have an orientation: rectangles, ovals, and macros.
|
||||||
|
|
||||||
ToolChangeReplace = {}
|
ToolChangeReplace = {}
|
||||||
for layername in job.apxlat.keys():
|
for layername in list(job.apxlat.keys()):
|
||||||
J.apxlat[layername] = {}
|
J.apxlat[layername] = {}
|
||||||
|
|
||||||
for ap in job.apxlat[layername].keys():
|
for ap in list(job.apxlat[layername].keys()):
|
||||||
code = job.apxlat[layername][ap]
|
code = job.apxlat[layername][ap]
|
||||||
A = GAT[code]
|
A = GAT[code]
|
||||||
|
|
||||||
|
@ -1438,18 +1438,18 @@ def rotateJob(job, degrees = 90, flip = 0, firstpass = True):
|
||||||
offset = 0
|
offset = 0
|
||||||
else:
|
else:
|
||||||
offset = job.maxy-job.miny
|
offset = job.maxy-job.miny
|
||||||
for layername in job.commands.keys():
|
for layername in list(job.commands.keys()):
|
||||||
J.commands[layername] = []
|
J.commands[layername] = []
|
||||||
J.apertures[layername] = []
|
J.apertures[layername] = []
|
||||||
|
|
||||||
for cmd in job.commands[layername]:
|
for cmd in job.commands[layername]:
|
||||||
# Is it a drawing command?
|
# Is it a drawing command?
|
||||||
if type(cmd) is types.TupleType:
|
if type(cmd) is tuple:
|
||||||
if len(cmd)==3:
|
if len(cmd)==3:
|
||||||
x, y, d = map(__builtin__.int, cmd)
|
x, y, d = list(map(builtins.int, cmd))
|
||||||
II=JJ=None
|
II=JJ=None
|
||||||
else:
|
else:
|
||||||
x, y, II, JJ, d, signed = map(__builtin__.int, cmd) # J is already used as Job object
|
x, y, II, JJ, d, signed = list(map(builtins.int, cmd)) # J is already used as Job object
|
||||||
else:
|
else:
|
||||||
# No, must be a string indicating aperture change, G-code, or RS274-X command.
|
# No, must be a string indicating aperture change, G-code, or RS274-X command.
|
||||||
if cmd[0] in ('G', '%'):
|
if cmd[0] in ('G', '%'):
|
||||||
|
@ -1497,13 +1497,13 @@ def rotateJob(job, degrees = 90, flip = 0, firstpass = True):
|
||||||
J.commands[layername].append((newx,newy,d))
|
J.commands[layername].append((newx,newy,d))
|
||||||
|
|
||||||
if 0:
|
if 0:
|
||||||
print job.minx, job.miny, offset
|
print(job.minx, job.miny, offset)
|
||||||
print layername
|
print(layername)
|
||||||
print J.commands[layername]
|
print(J.commands[layername])
|
||||||
|
|
||||||
# Finally, rotate drills. Offset is in hundred-thousandths (2.5) while Excellon
|
# Finally, rotate drills. Offset is in hundred-thousandths (2.5) while Excellon
|
||||||
# data is in 2.4 format.
|
# data is in 2.4 format.
|
||||||
for tool in job.xcommands.keys():
|
for tool in list(job.xcommands.keys()):
|
||||||
J.xcommands[tool] = []
|
J.xcommands[tool] = []
|
||||||
|
|
||||||
for x,y,stop_x,stop_y in job.xcommands[tool]:
|
for x,y,stop_x,stop_y in job.xcommands[tool]:
|
||||||
|
|
|
@ -93,7 +93,7 @@ def writeChar(fid, c, X, Y, degrees):
|
||||||
try:
|
try:
|
||||||
glyph = strokes.StrokeMap[c]
|
glyph = strokes.StrokeMap[c]
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, 'No glyph for character %s' % hex(ord(c))
|
raise RuntimeError('No glyph for character %s' % hex(ord(c)))
|
||||||
|
|
||||||
writeGlyph(fid, glyph, X, Y, degrees, c)
|
writeGlyph(fid, glyph, X, Y, degrees, c)
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ if __name__=="__main__":
|
||||||
s = string.digits+string.letters+string.punctuation
|
s = string.digits+string.letters+string.punctuation
|
||||||
#s = "The quick brown fox jumped over the lazy dog!"
|
#s = "The quick brown fox jumped over the lazy dog!"
|
||||||
|
|
||||||
fid = file('test.ger','wt')
|
fid = open('test.ger','wt')
|
||||||
fid.write("""G75*
|
fid.write("""G75*
|
||||||
G70*
|
G70*
|
||||||
%OFA0B0*%
|
%OFA0B0*%
|
||||||
|
|
|
@ -184,7 +184,7 @@ def findJob(jobname, rotatedFlipped, Jobs=config.Jobs):
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for existingjob in Jobs.keys():
|
for existingjob in list(Jobs.keys()):
|
||||||
if existingjob.lower() == fullname.lower(): ## job names are case insensitive
|
if existingjob.lower() == fullname.lower(): ## job names are case insensitive
|
||||||
job = Jobs[existingjob]
|
job = Jobs[existingjob]
|
||||||
return jobs.JobLayout(job)
|
return jobs.JobLayout(job)
|
||||||
|
@ -194,13 +194,13 @@ def findJob(jobname, rotatedFlipped, Jobs=config.Jobs):
|
||||||
# Perhaps we just don't have a rotated or flipped job yet
|
# Perhaps we just don't have a rotated or flipped job yet
|
||||||
if rotatedFlipped[0] or rotatedFlipped[1]:
|
if rotatedFlipped[0] or rotatedFlipped[1]:
|
||||||
try:
|
try:
|
||||||
for existingjob in Jobs.keys():
|
for existingjob in list(Jobs.keys()):
|
||||||
if existingjob.lower() == jobname.lower(): ## job names are case insensitive
|
if existingjob.lower() == jobname.lower(): ## job names are case insensitive
|
||||||
job = Jobs[existingjob]
|
job = Jobs[existingjob]
|
||||||
except:
|
except:
|
||||||
raise RuntimeError, "Job name '%s' not found" % jobname
|
raise RuntimeError("Job name '%s' not found" % jobname)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Job name '%s' not found" % jobname
|
raise RuntimeError("Job name '%s' not found" % jobname)
|
||||||
|
|
||||||
# Make a rotated/flipped job
|
# Make a rotated/flipped job
|
||||||
job = jobs.rotateJob(job, rotatedFlipped[0], rotatedFlipped[1])
|
job = jobs.rotateJob(job, rotatedFlipped[0], rotatedFlipped[1])
|
||||||
|
@ -228,14 +228,14 @@ def parseJobSpec(spec, data):
|
||||||
elif rotation == "Rotate270":
|
elif rotation == "Rotate270":
|
||||||
rotated = 270
|
rotated = 270
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Unsupported rotation: %s" % rotation
|
raise RuntimeError("Unsupported rotation: %s" % rotation)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
rotated = 0
|
rotated = 0
|
||||||
|
|
||||||
return findJob(jobname, [rotated, 0])
|
return findJob(jobname, [rotated, 0])
|
||||||
else:
|
else:
|
||||||
raise RuntimeError, "Matrix panels not yet supported"
|
raise RuntimeError("Matrix panels not yet supported")
|
||||||
|
|
||||||
def parseColSpec(spec, data):
|
def parseColSpec(spec, data):
|
||||||
jobs = Col()
|
jobs = Col()
|
||||||
|
@ -302,9 +302,9 @@ def parseLayoutFile(fname):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fid = file(fname, 'rt')
|
fid = open(fname, 'rt')
|
||||||
except Exception, detail:
|
except Exception as detail:
|
||||||
raise RuntimeError, "Unable to open layout file: %s\n %s" % (fname, str(detail))
|
raise RuntimeError("Unable to open layout file: %s\n %s" % (fname, str(detail)))
|
||||||
|
|
||||||
data = fid.read()
|
data = fid.read()
|
||||||
fid.close()
|
fid.close()
|
||||||
|
@ -312,16 +312,16 @@ def parseLayoutFile(fname):
|
||||||
|
|
||||||
# Replace all CR's in data with nothing, to convert DOS line endings
|
# Replace all CR's in data with nothing, to convert DOS line endings
|
||||||
# to unix format (all LF's).
|
# to unix format (all LF's).
|
||||||
data = string.replace(data, '\x0D', '')
|
data = str.replace(data, '\x0D', '')
|
||||||
|
|
||||||
tree = parser.parse(data)
|
tree = parser.parse(data)
|
||||||
|
|
||||||
# Last element of tree is number of characters parsed
|
# Last element of tree is number of characters parsed
|
||||||
if not tree[0]:
|
if not tree[0]:
|
||||||
raise RuntimeError, "Layout file cannot be parsed"
|
raise RuntimeError("Layout file cannot be parsed")
|
||||||
|
|
||||||
if tree[2] != len(data):
|
if tree[2] != len(data):
|
||||||
raise RuntimeError, "Parse error at character %d in layout file" % tree[2]
|
raise RuntimeError("Parse error at character %d in layout file" % tree[2])
|
||||||
|
|
||||||
Rows = []
|
Rows = []
|
||||||
for rowspec in tree[1]:
|
for rowspec in tree[1]:
|
||||||
|
@ -333,7 +333,7 @@ def parseLayoutFile(fname):
|
||||||
return Rows
|
return Rows
|
||||||
|
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
fid = file(sys.argv[1])
|
fid = open(sys.argv[1])
|
||||||
testdata = fid.read()
|
testdata = fid.read()
|
||||||
fid.close()
|
fid.close()
|
||||||
|
|
||||||
|
|
|
@ -52,11 +52,11 @@ class Placement:
|
||||||
|
|
||||||
def write(self, fname):
|
def write(self, fname):
|
||||||
"""Write placement to a file"""
|
"""Write placement to a file"""
|
||||||
fid = file(fname, 'wt')
|
fid = open(fname, 'wt')
|
||||||
for job in self.jobs:
|
for job in self.jobs:
|
||||||
fid.write('%s %.3f %.3f\n' % (job.job.name, job.x, job.y))
|
fid.write('%s %.3f %.3f\n' % (job.job.name, job.x, job.y))
|
||||||
# added; thought it would be useful to know
|
# added; thought it would be useful to know
|
||||||
print "job locations: job - %s x,y(%f,%f)" % (job.job.name, job.x, job.y)
|
print("job locations: job - %s x,y(%f,%f)" % (job.job.name, job.x, job.y))
|
||||||
fid.close()
|
fid.close()
|
||||||
|
|
||||||
def addFromFile(self, fname, Jobs):
|
def addFromFile(self, fname, Jobs):
|
||||||
|
@ -65,9 +65,9 @@ class Placement:
|
||||||
comment = re.compile(r'\s*(?:#.+)?$')
|
comment = re.compile(r'\s*(?:#.+)?$')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fid = file(fname, 'rt')
|
fid = open(fname, 'rt')
|
||||||
except:
|
except:
|
||||||
print 'Unable to open placement file: "%s"' % fname
|
print('Unable to open placement file: "%s"' % fname)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
lines = fid.readlines()
|
lines = fid.readlines()
|
||||||
|
@ -78,7 +78,7 @@ class Placement:
|
||||||
|
|
||||||
match = pat.match(line)
|
match = pat.match(line)
|
||||||
if not match:
|
if not match:
|
||||||
print 'Cannot interpret placement line in placement file:\n %s' % line
|
print('Cannot interpret placement line in placement file:\n %s' % line)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
jobname, X, Y = match.groups()
|
jobname, X, Y = match.groups()
|
||||||
|
@ -86,7 +86,7 @@ class Placement:
|
||||||
X = float(X)
|
X = float(X)
|
||||||
Y = float(Y)
|
Y = float(Y)
|
||||||
except:
|
except:
|
||||||
print 'Illegal (X,Y) co-ordinates in placement file:\n %s' % line
|
print('Illegal (X,Y) co-ordinates in placement file:\n %s' % line)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# rotated or flipped
|
# rotated or flipped
|
||||||
|
|
|
@ -18,9 +18,9 @@ def schwartz(List, Metric):
|
||||||
def pairing(element, M = Metric):
|
def pairing(element, M = Metric):
|
||||||
return (M(element), element)
|
return (M(element), element)
|
||||||
|
|
||||||
paired = map(pairing, List)
|
paired = list(map(pairing, List))
|
||||||
paired.sort()
|
paired.sort()
|
||||||
return map(stripit, paired)
|
return list(map(stripit, paired))
|
||||||
|
|
||||||
def stripit2(pair):
|
def stripit2(pair):
|
||||||
return pair[0]
|
return pair[0]
|
||||||
|
@ -31,8 +31,8 @@ def schwartz2(List, Metric):
|
||||||
def pairing(element, M = Metric):
|
def pairing(element, M = Metric):
|
||||||
return (M(element), element)
|
return (M(element), element)
|
||||||
|
|
||||||
paired = map(pairing, List)
|
paired = list(map(pairing, List))
|
||||||
paired.sort()
|
paired.sort()
|
||||||
theList = map(stripit, paired)
|
theList = list(map(stripit, paired))
|
||||||
theMetrics = map(stripit2, paired)
|
theMetrics = list(map(stripit2, paired))
|
||||||
return (theList, theMetrics)
|
return (theList, theMetrics)
|
||||||
|
|
|
@ -177,7 +177,7 @@ def mergeLines(Lines):
|
||||||
|
|
||||||
# Extend horizontal lines
|
# Extend horizontal lines
|
||||||
NewHLines = {}
|
NewHLines = {}
|
||||||
for yval,lines in HLines.items():
|
for yval,lines in list(HLines.items()):
|
||||||
# yval is the Y ordinate of this group of lines. lines is the set of all
|
# yval is the Y ordinate of this group of lines. lines is the set of all
|
||||||
# lines with this Y ordinate.
|
# lines with this Y ordinate.
|
||||||
NewHLines[yval] = []
|
NewHLines[yval] = []
|
||||||
|
@ -199,7 +199,7 @@ def mergeLines(Lines):
|
||||||
|
|
||||||
# Extend vertical lines
|
# Extend vertical lines
|
||||||
NewVLines = {}
|
NewVLines = {}
|
||||||
for xval,lines in VLines.items():
|
for xval,lines in list(VLines.items()):
|
||||||
# xval is the X ordinate of this group of lines. lines is the set of all
|
# xval is the X ordinate of this group of lines. lines is the set of all
|
||||||
# lines with this X ordinate.
|
# lines with this X ordinate.
|
||||||
NewVLines[xval] = []
|
NewVLines[xval] = []
|
||||||
|
@ -228,7 +228,7 @@ def mergeLines(Lines):
|
||||||
# or within each other. We will have to sort all horizontal lines by their
|
# or within each other. We will have to sort all horizontal lines by their
|
||||||
# Y ordinates and group them according to Y ordinates that are close enough
|
# Y ordinates and group them according to Y ordinates that are close enough
|
||||||
# to each other.
|
# to each other.
|
||||||
yvals = HLines.keys()
|
yvals = list(HLines.keys())
|
||||||
clusters = clusterOrdinates(yvals) # A list of clustered tuples containing yvals
|
clusters = clusterOrdinates(yvals) # A list of clustered tuples containing yvals
|
||||||
|
|
||||||
for cluster in clusters:
|
for cluster in clusters:
|
||||||
|
@ -240,7 +240,7 @@ def mergeLines(Lines):
|
||||||
# Y ordinate. Merge them together.
|
# Y ordinate. Merge them together.
|
||||||
NewHLines.extend(mergeHLines(clusterLines))
|
NewHLines.extend(mergeHLines(clusterLines))
|
||||||
|
|
||||||
xvals = VLines.keys()
|
xvals = list(VLines.keys())
|
||||||
clusters = clusterOrdinates(xvals)
|
clusters = clusterOrdinates(xvals)
|
||||||
for cluster in clusters:
|
for cluster in clusters:
|
||||||
clusterLines = []
|
clusterLines = []
|
||||||
|
|
|
@ -19,11 +19,11 @@ import gerbmerge
|
||||||
|
|
||||||
_StartTime = 0.0 # Start time of tiling
|
_StartTime = 0.0 # Start time of tiling
|
||||||
_CkpointTime = 0.0 # Next time to print stats
|
_CkpointTime = 0.0 # Next time to print stats
|
||||||
_Placements = 0L # Number of placements attempted
|
_Placements = 0 # Number of placements attempted
|
||||||
_PossiblePermutations = 0L # Number of different ways of ordering jobs
|
_PossiblePermutations = 0 # Number of different ways of ordering jobs
|
||||||
_Permutations = 0L # Number of different job orderings already computed
|
_Permutations = 0 # Number of different job orderings already computed
|
||||||
_TBestTiling = None # Best tiling so far
|
_TBestTiling = None # Best tiling so far
|
||||||
_TBestScore = float(sys.maxint) # Smallest area so far
|
_TBestScore = float(sys.maxsize) # Smallest area so far
|
||||||
_PrintStats = 1 # Print statistics every 3 seconds
|
_PrintStats = 1 # Print statistics every 3 seconds
|
||||||
|
|
||||||
def printTilingStats():
|
def printTilingStats():
|
||||||
|
@ -42,11 +42,11 @@ def printTilingStats():
|
||||||
|
|
||||||
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
print "\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
|
print("\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
|
||||||
(percent, _Permutations, _Placements, area, utilization),
|
(percent, _Permutations, _Placements, area, utilization), end=' ')
|
||||||
else:
|
else:
|
||||||
print "\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. mm / Best utilization: %.1f%%" % \
|
print("\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. mm / Best utilization: %.1f%%" % \
|
||||||
(percent, _Permutations, _Placements, area, utilization),
|
(percent, _Permutations, _Placements, area, utilization), end=' ')
|
||||||
|
|
||||||
|
|
||||||
if gerbmerge.GUI is not None:
|
if gerbmerge.GUI is not None:
|
||||||
|
@ -86,7 +86,7 @@ def _tile_search1(Jobs, TSoFar, firstAddPoint, cfg=config.Config):
|
||||||
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PrintStats
|
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PrintStats
|
||||||
|
|
||||||
if not TSoFar:
|
if not TSoFar:
|
||||||
return (None, float(sys.maxint))
|
return (None, float(sys.maxsize))
|
||||||
|
|
||||||
if not Jobs:
|
if not Jobs:
|
||||||
# Update the best tiling and score. If the new tiling matches
|
# Update the best tiling and score. If the new tiling matches
|
||||||
|
@ -118,12 +118,12 @@ def _tile_search1(Jobs, TSoFar, firstAddPoint, cfg=config.Config):
|
||||||
remaining_jobs = Jobs[:job_ix]+Jobs[job_ix+1:]
|
remaining_jobs = Jobs[:job_ix]+Jobs[job_ix+1:]
|
||||||
|
|
||||||
if 0:
|
if 0:
|
||||||
print "Level %d (%s)" % (level, job.name)
|
print("Level %d (%s)" % (level, job.name))
|
||||||
TSoFar.joblist()
|
TSoFar.joblist()
|
||||||
for J in remaining_jobs:
|
for J in remaining_jobs:
|
||||||
print J[2].name, ", ",
|
print(J[2].name, ", ", end=' ')
|
||||||
print
|
print()
|
||||||
print '-'*75
|
print('-'*75)
|
||||||
|
|
||||||
# Construct add-points for the non-rotated and rotated job.
|
# Construct add-points for the non-rotated and rotated job.
|
||||||
# As an optimization, do not construct add-points for the rotated
|
# As an optimization, do not construct add-points for the rotated
|
||||||
|
@ -154,7 +154,7 @@ def _tile_search1(Jobs, TSoFar, firstAddPoint, cfg=config.Config):
|
||||||
# Premature prune due to not being able to put this job anywhere. We
|
# Premature prune due to not being able to put this job anywhere. We
|
||||||
# have pruned off 2^M permutations where M is the length of the remaining
|
# have pruned off 2^M permutations where M is the length of the remaining
|
||||||
# jobs.
|
# jobs.
|
||||||
_Permutations += 2L**len(remaining_jobs)
|
_Permutations += 2**len(remaining_jobs)
|
||||||
|
|
||||||
if addpoints2:
|
if addpoints2:
|
||||||
for ix in addpoints2:
|
for ix in addpoints2:
|
||||||
|
@ -170,7 +170,7 @@ def _tile_search1(Jobs, TSoFar, firstAddPoint, cfg=config.Config):
|
||||||
# Premature prune due to not being able to put this job anywhere. We
|
# Premature prune due to not being able to put this job anywhere. We
|
||||||
# have pruned off 2^M permutations where M is the length of the remaining
|
# have pruned off 2^M permutations where M is the length of the remaining
|
||||||
# jobs.
|
# jobs.
|
||||||
_Permutations += 2L**len(remaining_jobs)
|
_Permutations += 2**len(remaining_jobs)
|
||||||
|
|
||||||
# If we've been at this for 3 seconds, print some status information
|
# If we've been at this for 3 seconds, print some status information
|
||||||
if _PrintStats and time.time() > _CkpointTime:
|
if _PrintStats and time.time() > _CkpointTime:
|
||||||
|
@ -185,9 +185,9 @@ def _tile_search1(Jobs, TSoFar, firstAddPoint, cfg=config.Config):
|
||||||
# end for each job in job list
|
# end for each job in job list
|
||||||
|
|
||||||
def factorial(N):
|
def factorial(N):
|
||||||
if (N <= 1): return 1L
|
if (N <= 1): return 1
|
||||||
|
|
||||||
prod = long(N)
|
prod = int(N)
|
||||||
while (N > 2):
|
while (N > 2):
|
||||||
N -= 1
|
N -= 1
|
||||||
prod *= N
|
prod *= N
|
||||||
|
@ -198,10 +198,10 @@ def initialize(printStats=1):
|
||||||
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PossiblePermutations, _PrintStats
|
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PossiblePermutations, _PrintStats
|
||||||
|
|
||||||
_PrintStats = printStats
|
_PrintStats = printStats
|
||||||
_Placements = 0L
|
_Placements = 0
|
||||||
_Permutations = 0L
|
_Permutations = 0
|
||||||
_TBestTiling = None
|
_TBestTiling = None
|
||||||
_TBestScore = float(sys.maxint)
|
_TBestScore = float(sys.maxsize)
|
||||||
|
|
||||||
def tile_search1(Jobs, X, Y):
|
def tile_search1(Jobs, X, Y):
|
||||||
"""Wrapper around _tile_search1 to handle keyboard interrupt, etc."""
|
"""Wrapper around _tile_search1 to handle keyboard interrupt, etc."""
|
||||||
|
@ -215,36 +215,36 @@ def tile_search1(Jobs, X, Y):
|
||||||
# This is assuming all jobs are unique and each job has a rotation (i.e., is not
|
# This is assuming all jobs are unique and each job has a rotation (i.e., is not
|
||||||
# square). Practically, these assumptions make no difference because the software
|
# square). Practically, these assumptions make no difference because the software
|
||||||
# currently doesn't optimize for cases of repeated jobs.
|
# currently doesn't optimize for cases of repeated jobs.
|
||||||
_PossiblePermutations = (2L**len(Jobs))*factorial(len(Jobs))
|
_PossiblePermutations = (2**len(Jobs))*factorial(len(Jobs))
|
||||||
#print "Possible permutations:", _PossiblePermutations
|
#print "Possible permutations:", _PossiblePermutations
|
||||||
|
|
||||||
print '='*70
|
print('='*70)
|
||||||
print "Starting placement using exhaustive search."
|
print("Starting placement using exhaustive search.")
|
||||||
print "There are %ld possible permutations..." % _PossiblePermutations,
|
print("There are %ld possible permutations..." % _PossiblePermutations, end=' ')
|
||||||
if _PossiblePermutations < 1e4:
|
if _PossiblePermutations < 1e4:
|
||||||
print "this'll take no time at all."
|
print("this'll take no time at all.")
|
||||||
elif _PossiblePermutations < 1e5:
|
elif _PossiblePermutations < 1e5:
|
||||||
print "surf the web for a few minutes."
|
print("surf the web for a few minutes.")
|
||||||
elif _PossiblePermutations < 1e6:
|
elif _PossiblePermutations < 1e6:
|
||||||
print "take a long lunch."
|
print("take a long lunch.")
|
||||||
elif _PossiblePermutations < 1e7:
|
elif _PossiblePermutations < 1e7:
|
||||||
print "come back tomorrow."
|
print("come back tomorrow.")
|
||||||
else:
|
else:
|
||||||
print "don't hold your breath."
|
print("don't hold your breath.")
|
||||||
print "Press Ctrl-C to stop and use the best placement so far."
|
print("Press Ctrl-C to stop and use the best placement so far.")
|
||||||
print "Estimated maximum possible utilization is %.1f%%." % (tiling.maxUtilization(Jobs)*100)
|
print("Estimated maximum possible utilization is %.1f%%." % (tiling.maxUtilization(Jobs)*100))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_tile_search1(Jobs, tiling.Tiling(X,Y), 1)
|
_tile_search1(Jobs, tiling.Tiling(X,Y), 1)
|
||||||
printTilingStats()
|
printTilingStats()
|
||||||
print
|
print()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
printTilingStats()
|
printTilingStats()
|
||||||
print
|
print()
|
||||||
print "Interrupted."
|
print("Interrupted.")
|
||||||
|
|
||||||
computeTime = time.time() - _StartTime
|
computeTime = time.time() - _StartTime
|
||||||
print "Computed %ld placements in %d seconds / %.1f placements/second" % (_Placements, computeTime, _Placements/computeTime)
|
print("Computed %ld placements in %d seconds / %.1f placements/second" % (_Placements, computeTime, _Placements/computeTime))
|
||||||
print '='*70
|
print('='*70)
|
||||||
|
|
||||||
return _TBestTiling
|
return _TBestTiling
|
||||||
|
|
|
@ -21,9 +21,9 @@ import gerbmerge
|
||||||
|
|
||||||
_StartTime = 0.0 # Start time of tiling
|
_StartTime = 0.0 # Start time of tiling
|
||||||
_CkpointTime = 0.0 # Next time to print stats
|
_CkpointTime = 0.0 # Next time to print stats
|
||||||
_Placements = 0L # Number of placements attempted
|
_Placements = 0 # Number of placements attempted
|
||||||
_TBestTiling = None # Best tiling so far
|
_TBestTiling = None # Best tiling so far
|
||||||
_TBestScore = float(sys.maxint) # Smallest area so far
|
_TBestScore = float(sys.maxsize) # Smallest area so far
|
||||||
|
|
||||||
def printTilingStats():
|
def printTilingStats():
|
||||||
global _CkpointTime
|
global _CkpointTime
|
||||||
|
@ -38,11 +38,11 @@ def printTilingStats():
|
||||||
|
|
||||||
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
# add metric support (1/1000 mm vs. 1/100,000 inch)
|
||||||
if config.Config['measurementunits'] == 'inch':
|
if config.Config['measurementunits'] == 'inch':
|
||||||
print "\r %ld placements / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
|
print("\r %ld placements / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
|
||||||
(_Placements, area, utilization),
|
(_Placements, area, utilization), end=' ')
|
||||||
else:
|
else:
|
||||||
print "\r %ld placements / Smallest area: %.1f sq. mm / Best utilization: %.0f%%" % \
|
print("\r %ld placements / Smallest area: %.1f sq. mm / Best utilization: %.0f%%" % \
|
||||||
(_Placements, area, utilization),
|
(_Placements, area, utilization), end=' ')
|
||||||
|
|
||||||
if gerbmerge.GUI is not None:
|
if gerbmerge.GUI is not None:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
@ -64,7 +64,7 @@ def _tile_search2(Jobs, X, Y, cfg=config.Config):
|
||||||
# Must escape with Ctrl-C
|
# Must escape with Ctrl-C
|
||||||
while 1:
|
while 1:
|
||||||
T = tiling.Tiling(X,Y)
|
T = tiling.Tiling(X,Y)
|
||||||
joborder = r.sample(range(N), N)
|
joborder = r.sample(list(range(N)), N)
|
||||||
|
|
||||||
minInletSize = tiling.minDimension(Jobs)
|
minInletSize = tiling.minDimension(Jobs)
|
||||||
|
|
||||||
|
@ -127,32 +127,32 @@ def tile_search2(Jobs, X, Y):
|
||||||
|
|
||||||
_StartTime = time.time()
|
_StartTime = time.time()
|
||||||
_CkpointTime = _StartTime + 3
|
_CkpointTime = _StartTime + 3
|
||||||
_Placements = 0L
|
_Placements = 0
|
||||||
_TBestTiling = None
|
_TBestTiling = None
|
||||||
_TBestScore = float(sys.maxint)
|
_TBestScore = float(sys.maxsize)
|
||||||
|
|
||||||
print '='*70
|
print('='*70)
|
||||||
if (config.Config['searchtimeout'] > 0):
|
if (config.Config['searchtimeout'] > 0):
|
||||||
print "Starting random placement trials. You can press Ctrl-C to"
|
print("Starting random placement trials. You can press Ctrl-C to")
|
||||||
print "stop the process and use the best placement so far, or wait"
|
print("stop the process and use the best placement so far, or wait")
|
||||||
print "for the automatic timeout in %i seconds." % config.Config['searchtimeout']
|
print("for the automatic timeout in %i seconds." % config.Config['searchtimeout'])
|
||||||
else:
|
else:
|
||||||
print "Starting random placement trials. You must press Ctrl-C to"
|
print("Starting random placement trials. You must press Ctrl-C to")
|
||||||
print "stop the process and use the best placement so far."
|
print("stop the process and use the best placement so far.")
|
||||||
print "You can specify a timeout by setting 'SearchTimeout' in Layout.cfg"
|
print("You can specify a timeout by setting 'SearchTimeout' in Layout.cfg")
|
||||||
print "Estimated maximum possible utilization is %.1f%%." % (tiling.maxUtilization(Jobs)*100)
|
print("Estimated maximum possible utilization is %.1f%%." % (tiling.maxUtilization(Jobs)*100))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_tile_search2(Jobs, X, Y)
|
_tile_search2(Jobs, X, Y)
|
||||||
printTilingStats()
|
printTilingStats()
|
||||||
print
|
print()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
printTilingStats()
|
printTilingStats()
|
||||||
print
|
print()
|
||||||
print "Interrupted."
|
print("Interrupted.")
|
||||||
|
|
||||||
computeTime = time.time() - _StartTime
|
computeTime = time.time() - _StartTime
|
||||||
print "Computed %ld placements in %d seconds / %.1f placements/second" % (_Placements, computeTime, _Placements/computeTime)
|
print("Computed %ld placements in %d seconds / %.1f placements/second" % (_Placements, computeTime, _Placements/computeTime))
|
||||||
print '='*70
|
print('='*70)
|
||||||
|
|
||||||
return _TBestTiling
|
return _TBestTiling
|
||||||
|
|
|
@ -319,7 +319,7 @@ mirrored-L corner __ | |
|
||||||
|
|
||||||
def bounds(self):
|
def bounds(self):
|
||||||
"""Return 2-tuple ((minX, minY), (maxX, maxY)) of rectangular region defined by all jobs"""
|
"""Return 2-tuple ((minX, minY), (maxX, maxY)) of rectangular region defined by all jobs"""
|
||||||
minX = minY = float(sys.maxint)
|
minX = minY = float(sys.maxsize)
|
||||||
maxX = maxY = 0.0
|
maxX = maxY = 0.0
|
||||||
|
|
||||||
for bl,tr,job in self.jobs:
|
for bl,tr,job in self.jobs:
|
||||||
|
@ -368,7 +368,7 @@ def maxUtilization(Jobs):
|
||||||
# Utility function to compute the minimum dimension along any axis of all jobs.
|
# Utility function to compute the minimum dimension along any axis of all jobs.
|
||||||
# Used to remove inlets.
|
# Used to remove inlets.
|
||||||
def minDimension(Jobs):
|
def minDimension(Jobs):
|
||||||
M = float(sys.maxint)
|
M = float(sys.maxsize)
|
||||||
for Xdim,Ydim,job,rjob in Jobs:
|
for Xdim,Ydim,job,rjob in Jobs:
|
||||||
M = min(M,Xdim)
|
M = min(M,Xdim)
|
||||||
M = min(M,Ydim)
|
M = min(M,Ydim)
|
||||||
|
|
Loading…
Reference in New Issue