mirror of
https://github.com/telavivmakers/at-tami.git
synced 2024-05-25 11:56:54 +03:00
339 lines
9.1 KiB
Python
339 lines
9.1 KiB
Python
|
#!/usr/bin/env python
|
||
|
"""
|
||
|
Parse the job layout specification file.
|
||
|
|
||
|
Requires:
|
||
|
|
||
|
- SimpleParse 2.1 or higher
|
||
|
http://simpleparse.sourceforge.net
|
||
|
|
||
|
|
||
|
--------------------------------------------------------------------
|
||
|
|
||
|
This program is licensed under the GNU General Public License (GPL)
|
||
|
Version 3. See http://www.fsf.org for details of the license.
|
||
|
|
||
|
Rugged Circuits LLC
|
||
|
http://ruggedcircuits.com/gerbmerge
|
||
|
"""
|
||
|
import sys
|
||
|
import string
|
||
|
|
||
|
from simpleparse.parser import Parser
|
||
|
|
||
|
import config
|
||
|
import jobs
|
||
|
|
||
|
declaration = r'''
|
||
|
file := (commentline/nullline/rowspec)+
|
||
|
rowspec := ts, 'Row', ws, '{'!, ts, comment?, '\n', rowjob+, ts, '}'!, ts, comment?, '\n'
|
||
|
rowjob := jobspec/colspec/commentline/nullline
|
||
|
colspec := ts, 'Col', ws, '{'!, ts, comment?, '\n', coljob+, ts, '}'!, ts, comment?, '\n'
|
||
|
coljob := jobspec/rowspec/commentline/nullline
|
||
|
|
||
|
jobspec := ts, (paneljobspec/basicjobspec), ts, comment?, '\n'
|
||
|
basicjobspec := id, (rotation)?
|
||
|
paneljobspec := 'Not yet implemented'
|
||
|
#paneljobspec := int, [xX], int, ws, basicjobspec
|
||
|
comment := ([#;]/'//'), -'\n'*
|
||
|
commentline := ts, comment, '\n'
|
||
|
nullline := ts, '\n'
|
||
|
rotation := ws, 'Rotate', ('90'/'180'/'270')?
|
||
|
ws := [ \t]+
|
||
|
ts := [ \t]*
|
||
|
id := [a-zA-Z0-9], [a-zA-Z0-9_-]*
|
||
|
int := [0-9]+
|
||
|
'''
|
||
|
|
||
|
class Panel: # Meant to be subclassed as either a Row() or Col()
|
||
|
def __init__(self):
|
||
|
self.x = None
|
||
|
self.y = None
|
||
|
self.jobs = [] # List (left-to-right or bottom-to-top) of JobLayout() or Row()/Col() objects
|
||
|
|
||
|
def canonicalize(self): # Return plain list of JobLayout objects at the roots of all trees
|
||
|
L = []
|
||
|
for job in self.jobs:
|
||
|
L = L + job.canonicalize()
|
||
|
return L
|
||
|
|
||
|
def addjob(self, job): # Either a JobLayout class or Panel (sub)class
|
||
|
assert isinstance(job, Panel) or isinstance(job, jobs.JobLayout)
|
||
|
self.jobs.append(job)
|
||
|
|
||
|
def addwidths(self):
|
||
|
"Return width in inches"
|
||
|
width = 0.0
|
||
|
for job in self.jobs:
|
||
|
width += job.width_in() + config.Config['xspacing']
|
||
|
width -= config.Config['xspacing']
|
||
|
return width
|
||
|
|
||
|
def maxwidths(self):
|
||
|
"Return maximum width in inches of any one subpanel"
|
||
|
width = 0.0
|
||
|
for job in self.jobs:
|
||
|
width = max(width,job.width_in())
|
||
|
return width
|
||
|
|
||
|
def addheights(self):
|
||
|
"Return height in inches"
|
||
|
height = 0.0
|
||
|
for job in self.jobs:
|
||
|
height += job.height_in() + config.Config['yspacing']
|
||
|
height -= config.Config['yspacing']
|
||
|
return height
|
||
|
|
||
|
def maxheights(self):
|
||
|
"Return maximum height in inches of any one subpanel"
|
||
|
height = 0.0
|
||
|
for job in self.jobs:
|
||
|
height = max(height,job.height_in())
|
||
|
return height
|
||
|
|
||
|
def writeGerber(self, fid, layername):
|
||
|
for job in self.jobs:
|
||
|
job.writeGerber(fid, layername)
|
||
|
|
||
|
def writeExcellon(self, fid, tool):
|
||
|
for job in self.jobs:
|
||
|
job.writeExcellon(fid, tool)
|
||
|
|
||
|
def writeDrillHits(self, fid, tool, toolNum):
|
||
|
for job in self.jobs:
|
||
|
job.writeDrillHits(fid, tool, toolNum)
|
||
|
|
||
|
def writeCutLines(self, fid, drawing_code, X1, Y1, X2, Y2):
|
||
|
for job in self.jobs:
|
||
|
job.writeCutLines(fid, drawing_code, X1, Y1, X2, Y2)
|
||
|
|
||
|
def drillhits(self, tool):
|
||
|
hits = 0
|
||
|
for job in self.jobs:
|
||
|
hits += job.drillhits(tool)
|
||
|
|
||
|
return hits
|
||
|
|
||
|
def jobarea(self):
|
||
|
area = 0.0
|
||
|
for job in self.jobs:
|
||
|
area += job.jobarea()
|
||
|
|
||
|
return area
|
||
|
|
||
|
class Row(Panel):
|
||
|
def __init__(self):
|
||
|
Panel.__init__(self)
|
||
|
self.LR = 1 # Horizontal arrangement
|
||
|
|
||
|
def width_in(self):
|
||
|
return self.addwidths()
|
||
|
|
||
|
def height_in(self):
|
||
|
return self.maxheights()
|
||
|
|
||
|
def setPosition(self, x, y): # In inches
|
||
|
self.x = x
|
||
|
self.y = y
|
||
|
for job in self.jobs:
|
||
|
job.setPosition(x,y)
|
||
|
x += job.width_in() + config.Config['xspacing']
|
||
|
|
||
|
class Col(Panel):
|
||
|
def __init__(self):
|
||
|
Panel.__init__(self)
|
||
|
self.LR = 0 # Vertical arrangement
|
||
|
|
||
|
def width_in(self):
|
||
|
return self.maxwidths()
|
||
|
|
||
|
def height_in(self):
|
||
|
return self.addheights()
|
||
|
|
||
|
def setPosition(self, x, y): # In inches
|
||
|
self.x = x
|
||
|
self.y = y
|
||
|
for job in self.jobs:
|
||
|
job.setPosition(x,y)
|
||
|
y += job.height_in() + config.Config['yspacing']
|
||
|
|
||
|
def canonicalizePanel(panel):
|
||
|
L = []
|
||
|
for job in panel:
|
||
|
L = L + job.canonicalize()
|
||
|
return L
|
||
|
|
||
|
def findJob(jobname, rotated, Jobs=config.Jobs):
|
||
|
"""
|
||
|
Find a job in config.Jobs, possibly rotating it
|
||
|
If job not in config.Jobs add it for future reference
|
||
|
Return found job
|
||
|
"""
|
||
|
|
||
|
if rotated == 90:
|
||
|
fullname = jobname + '*rotated90'
|
||
|
elif rotated == 180:
|
||
|
fullname = jobname + '*rotated180'
|
||
|
elif rotated == 270:
|
||
|
fullname = jobname + '*rotated270'
|
||
|
else:
|
||
|
fullname = jobname
|
||
|
|
||
|
try:
|
||
|
for existingjob in Jobs.keys():
|
||
|
if existingjob.lower() == fullname.lower(): ## job names are case insensitive
|
||
|
job = Jobs[existingjob]
|
||
|
return jobs.JobLayout(job)
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
# Perhaps we just don't have a rotated job yet
|
||
|
if rotated:
|
||
|
try:
|
||
|
for existingjob in Jobs.keys():
|
||
|
if existingjob.lower() == jobname.lower(): ## job names are case insensitive
|
||
|
job = Jobs[existingjob]
|
||
|
except:
|
||
|
raise RuntimeError, "Job name '%s' not found" % jobname
|
||
|
else:
|
||
|
raise RuntimeError, "Job name '%s' not found" % jobname
|
||
|
|
||
|
# Make a rotated job
|
||
|
job = jobs.rotateJob(job, rotated)
|
||
|
Jobs[fullname] = job
|
||
|
|
||
|
return jobs.JobLayout(job)
|
||
|
|
||
|
def parseJobSpec(spec, data):
|
||
|
for jobspec in spec:
|
||
|
if jobspec[0] in ('ts','comment'): continue
|
||
|
|
||
|
assert jobspec[0] in ('paneljobspec','basicjobspec')
|
||
|
if jobspec[0] == 'basicjobspec':
|
||
|
namefield = jobspec[3][0]
|
||
|
jobname = data[namefield[1]:namefield[2]]
|
||
|
|
||
|
if len(jobspec[3]) > 1:
|
||
|
rotationfield = jobspec[3][1]
|
||
|
rotation = data[ rotationfield[1] + 1: rotationfield[2] ]
|
||
|
|
||
|
if (rotation == "Rotate") or (rotation == "Rotate90"):
|
||
|
rotated = 90
|
||
|
elif rotation == "Rotate180":
|
||
|
rotated = 180
|
||
|
elif rotation == "Rotate270":
|
||
|
rotated = 270
|
||
|
else:
|
||
|
raise RuntimeError, "Unsupported rotation: %s" % rotation
|
||
|
|
||
|
else:
|
||
|
rotated = 0
|
||
|
|
||
|
return findJob(jobname, rotated)
|
||
|
else:
|
||
|
raise RuntimeError, "Matrix panels not yet supported"
|
||
|
|
||
|
def parseColSpec(spec, data):
|
||
|
jobs = Col()
|
||
|
|
||
|
for coljob in spec:
|
||
|
if coljob[0] in ('ts','ws','comment'): continue
|
||
|
|
||
|
assert coljob[0] == 'coljob'
|
||
|
job = coljob[3][0]
|
||
|
if job[0] in ('commentline','nullline'): continue
|
||
|
|
||
|
assert job[0] in ('jobspec','rowspec')
|
||
|
if job[0] == 'jobspec':
|
||
|
jobs.addjob(parseJobSpec(job[3],data))
|
||
|
else:
|
||
|
jobs.addjob(parseRowSpec(job[3],data))
|
||
|
|
||
|
return jobs
|
||
|
|
||
|
def parseRowSpec(spec, data):
|
||
|
jobs = Row()
|
||
|
|
||
|
for rowjob in spec:
|
||
|
if rowjob[0] in ('ts','ws','comment'): continue
|
||
|
|
||
|
assert rowjob[0] == 'rowjob'
|
||
|
job = rowjob[3][0]
|
||
|
if job[0] in ('commentline','nullline'): continue
|
||
|
|
||
|
assert job[0] in ('jobspec','colspec')
|
||
|
if job[0] == 'jobspec':
|
||
|
jobs.addjob(parseJobSpec(job[3],data))
|
||
|
else:
|
||
|
jobs.addjob(parseColSpec(job[3],data))
|
||
|
|
||
|
return jobs
|
||
|
|
||
|
def parseLayoutFile(fname):
|
||
|
"""config.Jobs is a dictionary of ('jobname', Job Object).
|
||
|
|
||
|
The return value is a nested array. The primary dimension
|
||
|
of the array is one row:
|
||
|
|
||
|
[ Row1, Row2, Row3 ]
|
||
|
|
||
|
Each row element consists of a list of jobs or columns (i.e.,
|
||
|
JobLayout or Col objects).
|
||
|
|
||
|
Each column consists of a list of either jobs or rows.
|
||
|
These are recursive, so it can look like:
|
||
|
|
||
|
[
|
||
|
Row([JobLayout(), Col([ Row([JobLayout(), JobLayout()]),
|
||
|
JobLayout() ]), JobLayout() ]), # That was row 0
|
||
|
Row([JobLayout(), JobLayout()]) # That was row 1
|
||
|
]
|
||
|
|
||
|
This is a panel with two rows. In the first row there is
|
||
|
a job, a column, and another job, from left to right. In the
|
||
|
second row there are two jobs, from left to right.
|
||
|
|
||
|
The column in the first row has two jobs side by side, then
|
||
|
another one above them.
|
||
|
"""
|
||
|
|
||
|
try:
|
||
|
fid = file(fname, 'rt')
|
||
|
except Exception, detail:
|
||
|
raise RuntimeError, "Unable to open layout file: %s\n %s" % (fname, str(detail))
|
||
|
|
||
|
data = fid.read()
|
||
|
fid.close()
|
||
|
parser = Parser(declaration, "file")
|
||
|
|
||
|
# Replace all CR's in data with nothing, to convert DOS line endings
|
||
|
# to unix format (all LF's).
|
||
|
data = string.replace(data, '\x0D', '')
|
||
|
|
||
|
tree = parser.parse(data)
|
||
|
|
||
|
# Last element of tree is number of characters parsed
|
||
|
if not tree[0]:
|
||
|
raise RuntimeError, "Layout file cannot be parsed"
|
||
|
|
||
|
if tree[2] != len(data):
|
||
|
raise RuntimeError, "Parse error at character %d in layout file" % tree[2]
|
||
|
|
||
|
Rows = []
|
||
|
for rowspec in tree[1]:
|
||
|
if rowspec[0] in ('nullline', 'commentline'): continue
|
||
|
assert rowspec[0]=='rowspec'
|
||
|
|
||
|
Rows.append(parseRowSpec(rowspec[3], data))
|
||
|
|
||
|
return Rows
|
||
|
|
||
|
if __name__=="__main__":
|
||
|
fid = file(sys.argv[1])
|
||
|
testdata = fid.read()
|
||
|
fid.close()
|
||
|
|
||
|
parser = Parser(declaration, "file")
|
||
|
import pprint
|
||
|
pprint.pprint(parser.parse(testdata))
|