gerbmerge/gerbmerge/amacro.py

361 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, flip, ix):
if (flip == 1):
# flip horizontally
L[ix],L[ix+1] = (-L[ix], L[ix+1])
elif (flip == -1):
# flip vertically
L[ix],L[ix+1] = (L[ix], -L[ix+1])
else:
# 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, flip, ix):
# Increase angle th by +90 degrees for a list element
if (flip == 0):
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+2*N+1=5+2N fields:
# - first field is exposure
# - second field is number of points
# - thrid and forth fields is for the initial point
# - 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) != (5+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, flip):
if self.code == 1: # Circle: [andreika]: FIX circle rotation
rotatexypair(self.parms, flip, 2)
elif self.code in (2,20): # Line (vector): fields (2,3) and (4,5) must be rotated, no need to
# rotate field 6
rotatexypair(self.parms, flip, 2)
rotatexypair(self.parms, flip, 4)
elif self.code == 21: # Line (center): fields (3,4) must be rotated, and field 5 incremented by +90
rotatexypair(self.parms, flip, 3)
rotatethelem(self.parms, flip, 5)
elif self.code == 22: # Line (lower-left): fields (3,4) must be rotated, and field 5 incremented by +90
rotatexypair(self.parms, flip, 3)
rotatethelem(self.parms, flip, 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, flip, 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, flip, 2)
rotatethelem(self.parms, flip, 5)
elif self.code == 6: # Moire: fields (0,1) must be rotated, and field 8 incremented by +90
rotatexypair(self.parms, flip, 0)
rotatethelem(self.parms, flip, 8)
elif self.code == 7: # Thermal: fields (0,1) must be rotated, and field 5 incremented by +90
rotatexypair(self.parms, flip, 0)
rotatethelem(self.parms, flip, 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, flip):
for prim in self.prim:
prim.rotate(flip)
def rotated(self, flip):
# 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(flip)
if (flip == 1):
M.name = 'FH'+M.name[1:]
elif (flip == -1):
M.name = 'FV'+M.name[1:]
else:
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 = list(map(int, [K[1:] for K in list(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(0)
# Generate the Gerber so we can view it
fid = open('amacro.ger', 'wt')
print("""G75*
G70*
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%""", file=fid)
M.writeDef(fid)
MR.writeDef(fid)
print("""%ADD10TEST*%
%ADD11TESTR*%
D10*
X010000Y010000D03*
D11*
X015000Y010000D03*
M02*""", file=fid)
fid.close()
print(M)
print(MR)