mirror of
https://github.com/telavivmakers/at-tami.git
synced 2024-05-25 11:56:54 +03:00
349 lines
12 KiB
Python
349 lines
12 KiB
Python
|
#!/usr/bin/env python
|
||
|
"""
|
||
|
Define and manage aperture macros (%AM command). Currently,
|
||
|
only macros without replaceable parameters (e.g., $1, $2, etc.)
|
||
|
are supported.
|
||
|
|
||
|
--------------------------------------------------------------------
|
||
|
|
||
|
This program is licensed under the GNU General Public License (GPL)
|
||
|
Version 3. See http://www.fsf.org for details of the license.
|
||
|
|
||
|
Rugged Circuits LLC
|
||
|
http://ruggedcircuits.com/gerbmerge
|
||
|
"""
|
||
|
|
||
|
import sys
|
||
|
import re
|
||
|
import string
|
||
|
import copy
|
||
|
|
||
|
import config
|
||
|
|
||
|
_macro_pat = re.compile(r'^%AM([^*]+)\*$')
|
||
|
|
||
|
# This list stores the expected types of parameters for each primitive type
|
||
|
# (e.g., outline, line, circle, polygon, etc.). None is used for undefined
|
||
|
# primitives. Each entry corresponds to the defined primitive code, and
|
||
|
# comprises a tuple of conversion functions (i.e., built-in int() and float()
|
||
|
# functions) that apply to all parameters AFTER the primitive code. For example,
|
||
|
# code 1 (circle) may be instantiated as:
|
||
|
# 1,1,0.025,0.0,0.0
|
||
|
# (the parameters are code, exposure type, diameter, X center, Y center).
|
||
|
# After the integer code, we expect an int for exposure type, then floats
|
||
|
# for the remaining three parameters. Thus, the entry for code 1 is
|
||
|
# (int, float, float, float).
|
||
|
PrimitiveParmTypes = (
|
||
|
None, # Code 0 -- undefined
|
||
|
(int, float, float, float), # Code 1 -- circle
|
||
|
(int, float, float, float, float, float, float), # Code 2 -- line (vector)
|
||
|
None, # Code 3 -- end-of-file for .DES files
|
||
|
(int, int, float, float, float, float, float), # Code 4 -- outline...takes any number of additional floats
|
||
|
(int, int, float, float, float, float), # Code 5 -- regular polygon
|
||
|
(float, float, float, float, float, int, float, float, float), # Code 6 -- moire
|
||
|
(float, float, float, float, float, float), # Code 7 -- thermal
|
||
|
None, # Code 8 -- undefined
|
||
|
None, # Code 9 -- undefined
|
||
|
None, # Code 10 -- undefined
|
||
|
None, # Code 11 -- undefined
|
||
|
None, # Code 12 -- undefined
|
||
|
None, # Code 13 -- undefined
|
||
|
None, # Code 14 -- undefined
|
||
|
None, # Code 15 -- undefined
|
||
|
None, # Code 16 -- undefined
|
||
|
None, # Code 17 -- undefined
|
||
|
None, # Code 18 -- undefined
|
||
|
None, # Code 19 -- undefined
|
||
|
(int, float, float, float, float, float), # Code 20 -- line (vector)...alias for code 2
|
||
|
(int, float, float, float, float, float), # Code 21 -- line (center)
|
||
|
(int, float, float, float, float, float) # Code 22 -- line (lower-left)
|
||
|
)
|
||
|
|
||
|
def rotatexy(x,y):
|
||
|
# Rotate point (x,y) counterclockwise 90 degrees about the origin
|
||
|
return (-y,x)
|
||
|
|
||
|
def rotatexypair(L, ix):
|
||
|
# Rotate list items L[ix],L[ix+1] by 90 degrees
|
||
|
L[ix],L[ix+1] = rotatexy(L[ix],L[ix+1])
|
||
|
|
||
|
def swapxypair(L, ix):
|
||
|
# Swap two list elements
|
||
|
L[ix],L[ix+1] = L[ix+1],L[ix]
|
||
|
|
||
|
def rotatetheta(th):
|
||
|
# Increase angle th in degrees by +90 degrees (counterclockwise).
|
||
|
# Handle modulo 360 issues
|
||
|
th += 90
|
||
|
if th >= 360:
|
||
|
th -= 360
|
||
|
return th
|
||
|
|
||
|
def rotatethelem(L, ix):
|
||
|
# Increase angle th by +90 degrees for a list element
|
||
|
L[ix] = rotatetheta(L[ix])
|
||
|
|
||
|
class ApertureMacroPrimitive:
|
||
|
def __init__(self, code=-1, fields=None):
|
||
|
self.code = code
|
||
|
self.parms = []
|
||
|
if fields is not None:
|
||
|
self.setFromFields(code, fields)
|
||
|
|
||
|
def setFromFields(self, code, fields):
|
||
|
# code is an integer describing the primitive type, and fields is
|
||
|
# a list of STRINGS for each parameter
|
||
|
self.code = code
|
||
|
|
||
|
# valids will be one of the PrimitiveParmTypes tuples above. Some are
|
||
|
# None to indicate illegal codes. We also set valids to None to indicate
|
||
|
# the macro primitive code is outside the range of known types.
|
||
|
try:
|
||
|
valids = PrimitiveParmTypes[code]
|
||
|
except:
|
||
|
valids = None
|
||
|
|
||
|
if valids is None:
|
||
|
raise RuntimeError, 'Undefined aperture macro primitive code %d' % code
|
||
|
|
||
|
# We expect exactly the number of fields required, except for macro
|
||
|
# type 4 which is an outline and has a variable number of points.
|
||
|
# For outlines, the second parameter indicates the number of points,
|
||
|
# each of which has an (X,Y) co-ordinate. Thus, we expect an Outline
|
||
|
# specification to have 1+1+2*N+1=3+2N fields:
|
||
|
# - first field is exposure
|
||
|
# - second field is number of points
|
||
|
# - 2*N fields for X,Y points
|
||
|
# - last field is rotation
|
||
|
if self.code==4:
|
||
|
if len(fields) < 2:
|
||
|
raise RuntimeError, 'Outline macro primitive has way too few fields'
|
||
|
|
||
|
try:
|
||
|
N = int(fields[1])
|
||
|
except:
|
||
|
raise RuntimeError, 'Outline macro primitive has non-integer number of points'
|
||
|
|
||
|
if len(fields) != (3+2*N):
|
||
|
raise RuntimeError, 'Outline macro primitive has %d fields...expecting %d fields' % (len(fields), 3+2*N)
|
||
|
else:
|
||
|
if 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
|
||
|
# list, using either int() or float() conversion.
|
||
|
for parmix in range(len(fields)):
|
||
|
try:
|
||
|
converter = valids[parmix]
|
||
|
except:
|
||
|
converter = float # To handle variable number of points in Outline type
|
||
|
|
||
|
try:
|
||
|
self.parms.append(converter(fields[parmix]))
|
||
|
except:
|
||
|
raise RuntimeError, 'Aperture macro primitive parameter %d has incorrect type' % (parmix+1)
|
||
|
|
||
|
def setFromLine(self, line):
|
||
|
# Account for DOS line endings and get rid of line ending and '*' at the end
|
||
|
line = line.replace('\x0D', '')
|
||
|
line = line.rstrip()
|
||
|
line = line.rstrip('*')
|
||
|
|
||
|
fields = line.split(',')
|
||
|
|
||
|
try:
|
||
|
try:
|
||
|
code = int(fields[0])
|
||
|
except:
|
||
|
raise RuntimeError, 'Illegal aperture macro primitive code "%s"' % fields[0]
|
||
|
self.setFromFields(code, fields[1:])
|
||
|
except:
|
||
|
print '='*20
|
||
|
print "==> ", line
|
||
|
print '='*20
|
||
|
raise
|
||
|
|
||
|
def rotate(self):
|
||
|
if self.code == 1: # Circle: nothing to do
|
||
|
pass
|
||
|
elif self.code in (2,20): # Line (vector): fields (2,3) and (4,5) must be rotated, no need to
|
||
|
# rotate field 6
|
||
|
rotatexypair(self.parms, 2)
|
||
|
rotatexypair(self.parms, 4)
|
||
|
elif self.code == 21: # Line (center): fields (3,4) must be rotated, and field 5 incremented by +90
|
||
|
rotatexypair(self.parms, 3)
|
||
|
rotatethelem(self.parms, 5)
|
||
|
elif self.code == 22: # Line (lower-left): fields (3,4) must be rotated, and field 5 incremented by +90
|
||
|
rotatexypair(self.parms, 3)
|
||
|
rotatethelem(self.parms, 5)
|
||
|
elif self.code == 4: # Outline: fields (2,3), (4,5), etc. must be rotated, the last field need not be incremented
|
||
|
ix = 2
|
||
|
for pts in range(self.parms[1]): # parms[1] is the number of points
|
||
|
rotatexypair(self.parms, ix)
|
||
|
ix += 2
|
||
|
#rotatethelem(self.parms, ix)
|
||
|
elif self.code == 5: # Polygon: fields (2,3) must be rotated, and field 5 incremented by +90
|
||
|
rotatexypair(self.parms, 2)
|
||
|
rotatethelem(self.parms, 5)
|
||
|
elif self.code == 6: # Moire: fields (0,1) must be rotated, and field 8 incremented by +90
|
||
|
rotatexypair(self.parms, 0)
|
||
|
rotatethelem(self.parms, 8)
|
||
|
elif self.code == 7: # Thermal: fields (0,1) must be rotated, and field 5 incremented by +90
|
||
|
rotatexypair(self.parms, 0)
|
||
|
rotatethelem(self.parms, 5)
|
||
|
|
||
|
def __str__(self):
|
||
|
# Construct a string with ints as ints and floats as floats
|
||
|
s = '%d' % self.code
|
||
|
for parmix in range(len(self.parms)):
|
||
|
valids = PrimitiveParmTypes[self.code]
|
||
|
|
||
|
format = ',%f'
|
||
|
try:
|
||
|
if valids[parmix] is int:
|
||
|
format = ',%d'
|
||
|
except:
|
||
|
pass # '%f' is OK for Outline extra points
|
||
|
|
||
|
s += format % self.parms[parmix]
|
||
|
|
||
|
return s
|
||
|
|
||
|
def writeDef(self, fid):
|
||
|
fid.write('%s*\n' % str(self))
|
||
|
|
||
|
class ApertureMacro:
|
||
|
def __init__(self, name):
|
||
|
self.name = name
|
||
|
self.prim = []
|
||
|
|
||
|
def add(self, prim):
|
||
|
self.prim.append(prim)
|
||
|
|
||
|
def rotate(self):
|
||
|
for prim in self.prim:
|
||
|
prim.rotate()
|
||
|
|
||
|
def rotated(self):
|
||
|
# Return copy of ourselves, rotated. Replace 'R' as the first letter of the
|
||
|
# macro name. We don't append because we like to be able to count the
|
||
|
# number of aperture macros by stripping off the leading character.
|
||
|
M = copy.deepcopy(self)
|
||
|
M.rotate()
|
||
|
M.name = 'R'+M.name[1:]
|
||
|
return M
|
||
|
|
||
|
def dump(self, fid=sys.stdout):
|
||
|
fid.write(str(self))
|
||
|
|
||
|
def __str__(self):
|
||
|
s = '%s:\n' % self.name
|
||
|
s += self.hash()
|
||
|
return s
|
||
|
|
||
|
def hash(self):
|
||
|
s = ''
|
||
|
for prim in self.prim:
|
||
|
s += ' '+str(prim)+'\n'
|
||
|
return s
|
||
|
|
||
|
def writeDef(self, fid):
|
||
|
fid.write('%%AM%s*\n' % self.name)
|
||
|
for prim in self.prim:
|
||
|
prim.writeDef(fid)
|
||
|
fid.write('%\n')
|
||
|
|
||
|
def parseApertureMacro(s, fid):
|
||
|
match = _macro_pat.match(s)
|
||
|
if match:
|
||
|
name = match.group(1)
|
||
|
|
||
|
M = ApertureMacro(name)
|
||
|
|
||
|
for line in fid:
|
||
|
if line[0]=='%':
|
||
|
return M
|
||
|
|
||
|
P = ApertureMacroPrimitive()
|
||
|
P.setFromLine(line)
|
||
|
|
||
|
M.add(P)
|
||
|
else:
|
||
|
raise RuntimeError, "Premature end-of-file while parsing aperture macro"
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
# This function adds the new aperture macro AM to the global aperture macro
|
||
|
# table. The return value is the modified macro (name modified to be its global
|
||
|
# name). macro.
|
||
|
def addToApertureMacroTable(AM):
|
||
|
GAMT = config.GAMT
|
||
|
|
||
|
# Must sort keys by integer value, not string since 99 comes before 100
|
||
|
# as an integer but not a string.
|
||
|
keys = map(int, map(lambda K: K[1:], GAMT.keys()))
|
||
|
keys.sort()
|
||
|
|
||
|
if len(keys):
|
||
|
lastCode = keys[-1]
|
||
|
else:
|
||
|
lastCode = 0
|
||
|
|
||
|
mcode = 'M%d' % (lastCode+1)
|
||
|
AM.name = mcode
|
||
|
GAMT[mcode] = AM
|
||
|
|
||
|
return AM
|
||
|
|
||
|
if __name__=="__main__":
|
||
|
# Create a funky aperture macro with all the fixins, and make sure
|
||
|
# it rotates properly.
|
||
|
M = ApertureMacro('TEST')
|
||
|
|
||
|
# X and Y axes
|
||
|
M.add(ApertureMacroPrimitive(2, ('1', '0.0025', '0.0', '-0.1', '0.0', '0.1', '0.0')))
|
||
|
M.add(ApertureMacroPrimitive(2, ('1', '0.0025', '0.0', '-0.1', '0.0', '0.1', '90.0')))
|
||
|
|
||
|
# A circle in the top-right quadrant, touching the axes
|
||
|
M.add(ApertureMacroPrimitive(1, ('1', '0.02', '0.01', '0.01')))
|
||
|
# A line of slope -1 centered on the above circle, of thickness 5mil, length 0.05
|
||
|
M.add(ApertureMacroPrimitive(2, ('1', '0.005', '0.0', '0.02', '0.02', '0.0', '0.0')))
|
||
|
# A narrow vertical rectangle centered on the circle of width 2.5mil
|
||
|
M.add(ApertureMacroPrimitive(21, ('1', '0.0025', '0.03', '0.01', '0.01', '0.0')))
|
||
|
# A 45-degree line in the third quadrant, not quite touching the origin
|
||
|
M.add(ApertureMacroPrimitive(22, ('1', '0.02', '0.01', '-0.03', '-0.03', '45')))
|
||
|
# A right triangle in the second quadrant
|
||
|
M.add(ApertureMacroPrimitive(4, ('1', '4', '-0.03', '0.01', '-0.03', '0.03', '-0.01', '0.01', '-0.03', '0.01', '0.0')))
|
||
|
# A pentagon in the fourth quadrant, rotated by 15 degrees
|
||
|
M.add(ApertureMacroPrimitive(5, ('1', '5', '0.03', '-0.03', '0.02', '15')))
|
||
|
# A moire in the first quadrant, beyond the circle, with 2 annuli
|
||
|
M.add(ApertureMacroPrimitive(6, ('0.07', '0.07', '0.04', '0.005', '0.01', '2', '0.005', '0.04', '0.0')))
|
||
|
# A thermal in the second quadrant, beyond the right triangle
|
||
|
M.add(ApertureMacroPrimitive(7, ('-0.07', '0.07', '0.03', '0.02', '0.005', '15')))
|
||
|
|
||
|
MR = M.rotated()
|
||
|
|
||
|
# Generate the Gerber so we can view it
|
||
|
fid = file('amacro.ger', 'wt')
|
||
|
print >> fid, \
|
||
|
"""G75*
|
||
|
G70*
|
||
|
%OFA0B0*%
|
||
|
%FSLAX24Y24*%
|
||
|
%IPPOS*%
|
||
|
%LPD*%"""
|
||
|
M.writeDef(fid)
|
||
|
MR.writeDef(fid)
|
||
|
print >> fid, \
|
||
|
"""%ADD10TEST*%
|
||
|
%ADD11TESTR*%
|
||
|
D10*
|
||
|
X010000Y010000D03*
|
||
|
D11*
|
||
|
X015000Y010000D03*
|
||
|
M02*"""
|
||
|
fid.close()
|
||
|
|
||
|
print M
|
||
|
print MR
|