mirror of
https://github.com/telavivmakers/at-tami.git
synced 2024-05-25 11:56:54 +03:00
1289 lines
51 KiB
Python
1289 lines
51 KiB
Python
|
#!/usr/bin/env python
|
||
|
"""
|
||
|
This module reads all Gerber and Excellon files and stores the
|
||
|
data for each job.
|
||
|
|
||
|
--------------------------------------------------------------------
|
||
|
|
||
|
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 __builtin__
|
||
|
import copy
|
||
|
import types
|
||
|
|
||
|
import aptable
|
||
|
import config
|
||
|
import makestroke
|
||
|
import amacro
|
||
|
import geometry
|
||
|
import util
|
||
|
|
||
|
# Parsing Gerber/Excellon files is currently very brittle. A more robust
|
||
|
# RS274X/Excellon parser would be a good idea and allow this program to work
|
||
|
# robustly with more than just Eagle CAM files.
|
||
|
|
||
|
# Reminder to self:
|
||
|
#
|
||
|
# D01 -- move and draw line with exposure on
|
||
|
# D02 -- move with exposure off
|
||
|
# D03 -- flash aperture
|
||
|
|
||
|
# Patterns for Gerber RS274X file interpretation
|
||
|
apdef_pat = re.compile(r'^%AD(D\d+)([^*$]+)\*%$') # Aperture definition
|
||
|
apmdef_pat = re.compile(r'^%AM([^*$]+)\*$') # Aperture macro definition
|
||
|
comment_pat = re.compile(r'G0?4[^*]*\*') # Comment (GerbTool comment omits the 0)
|
||
|
tool_pat = re.compile(r'(D\d+)\*') # Aperture selection
|
||
|
gcode_pat = re.compile(r'G(\d{1,2})\*?') # G-codes
|
||
|
drawXY_pat = re.compile(r'X([+-]?\d+)Y([+-]?\d+)D0?([123])\*') # Drawing command
|
||
|
drawX_pat = re.compile(r'X([+-]?\d+)D0?([123])\*') # Drawing command, Y is implied
|
||
|
drawY_pat = re.compile(r'Y([+-]?\d+)D0?([123])\*') # Drawing command, X is implied
|
||
|
format_pat = re.compile(r'%FS(L|T)?(A|I)(N\d+)?(X\d\d)(Y\d\d)\*%') # Format statement
|
||
|
layerpol_pat = re.compile(r'^%LP[CD]\*%') # Layer polarity (D=dark, C=clear)
|
||
|
|
||
|
# Circular interpolation drawing commands (from Protel)
|
||
|
cdrawXY_pat = re.compile(r'X([+-]?\d+)Y([+-]?\d+)I([+-]?\d+)J([+-]?\d+)D0?([123])\*')
|
||
|
cdrawX_pat = re.compile(r'X([+-]?\d+)I([+-]?\d+)J([+-]?\d+)D0?([123])\*') # Y is implied
|
||
|
cdrawY_pat = re.compile(r'Y([+-]?\d+)I([+-]?\d+)J([+-]?\d+)D0?([123])\*') # X is implied
|
||
|
|
||
|
IgnoreList = ( \
|
||
|
# These are for Eagle, and RS274X files in general
|
||
|
re.compile(r'^%OFA0B0\*%$'),
|
||
|
re.compile(r'^%IPPOS\*%'),
|
||
|
re.compile(r'^%AMOC8\*$'), # Eagle's octagon defined by macro with a $1 parameter
|
||
|
re.compile(r'^5,1,8,0,0,1\.08239X\$1,22\.5\*$'), # Eagle's octagon, 22.5 degree rotation
|
||
|
re.compile(r'^5,1,8,0,0,1\.08239X\$1,0\.0\*$'), # Eagle's octagon, 0.0 degree rotation
|
||
|
re.compile(r'^\*?%$'),
|
||
|
re.compile(r'^M0?2\*$'),
|
||
|
|
||
|
# These additional ones are for Orcad Layout, PCB, Protel, etc.
|
||
|
re.compile(r'\*'), # Empty statement
|
||
|
re.compile(r'^%IN.*\*%'),
|
||
|
re.compile(r'^%ICAS\*%'), # Not in RS274X spec.
|
||
|
re.compile(r'^%MOIN\*%'),
|
||
|
re.compile(r'^%ASAXBY\*%'),
|
||
|
re.compile(r'^%AD\*%'), # GerbTool empty aperture definition
|
||
|
re.compile(r'^%LN.*\*%') # Layer name
|
||
|
)
|
||
|
|
||
|
# Patterns for Excellon interpretation
|
||
|
xtool_pat = re.compile(r'^(T\d+)$') # Tool selection
|
||
|
xydraw_pat = re.compile(r'^X([+-]?\d+)Y([+-]?\d+)$') # Plunge command
|
||
|
xdraw_pat = re.compile(r'^X([+-]?\d+)$') # Plunge command, repeat last Y value
|
||
|
ydraw_pat = re.compile(r'^Y([+-]?\d+)$') # Plunge command, repeat last X value
|
||
|
xtdef_pat = re.compile(r'^(T\d+)(?:F\d+)?(?:S\d+)?C([0-9.]+)$') # Tool+diameter definition with optional
|
||
|
# feed/speed (for Protel)
|
||
|
xtdef2_pat = re.compile(r'^(T\d+)C([0-9.]+)(?:F\d+)?(?:S\d+)?$') # Tool+diameter definition with optional
|
||
|
# feed/speed at the end (for OrCAD)
|
||
|
xzsup_pat = re.compile(r'^INCH,([LT])Z$') # Leading/trailing zeros INCLUDED
|
||
|
|
||
|
XIgnoreList = ( \
|
||
|
re.compile(r'^%$'),
|
||
|
re.compile(r'^M30$'), # End of job
|
||
|
re.compile(r'^M48$'), # Program header to first %
|
||
|
re.compile(r'^M72$') # Inches
|
||
|
)
|
||
|
|
||
|
# A Job is a single input board. It is expected to have:
|
||
|
# - a board outline file in RS274X format
|
||
|
# - several (at least one) Gerber files in RS274X format
|
||
|
# - a drill file in Excellon format
|
||
|
#
|
||
|
# The board outline and Excellon filenames must be given separately.
|
||
|
# The board outline file determines the extents of the job.
|
||
|
|
||
|
class Job:
|
||
|
def __init__(self, name):
|
||
|
self.name = name
|
||
|
|
||
|
# Minimum and maximum (X,Y) absolute co-ordinates encountered
|
||
|
# in GERBER data only (not Excellon). Note that coordinates
|
||
|
# are stored in hundred-thousandsths of an inch so 9999999 is 99.99999
|
||
|
# inches.
|
||
|
self.maxx = self.maxy = -9999999 # in the case all coordinates are < 0, this will prevent maxx and maxy from defaulting to 0
|
||
|
self.minx = self.miny = 9999999
|
||
|
|
||
|
# Aperture translation table relative to GAT. This dictionary
|
||
|
# has as each key a layer name for the job. Each key's value
|
||
|
# is itself a dictionary where each key is an aperture in the file.
|
||
|
# The value is the key in the GAT. Example:
|
||
|
# apxlat['TopCopper']['D10'] = 'D12'
|
||
|
# apxlat['TopCopper']['D11'] = 'D15'
|
||
|
# apxlat['BottomCopper']['D10'] = 'D15'
|
||
|
self.apxlat = {}
|
||
|
|
||
|
# Aperture macro translation table relative to GAMT. This dictionary
|
||
|
# has as each key a layer name for the job. Each key's value
|
||
|
# is itself a dictionary where each key is an aperture macro name in the file.
|
||
|
# The value is the key in the GAMT. Example:
|
||
|
# apxlat['TopCopper']['THD10X'] = 'M1'
|
||
|
# apxlat['BottomCopper']['AND10'] = 'M5'
|
||
|
self.apmxlat = {}
|
||
|
|
||
|
# Commands are one of:
|
||
|
# A. strings for:
|
||
|
# - aperture changes like "D12"
|
||
|
# - G-code commands like "G36"
|
||
|
# - RS-274X commands like "%LPD*%" that begin with '%'
|
||
|
# B. (X,Y,D) triples comprising X,Y integers in the range 0 through 999999
|
||
|
# and draw commands that are either D01, D02, or D03. The character
|
||
|
# D in the triple above is the integer 1, 2, or 3.
|
||
|
# C. (X,Y,I,J,D,s) 6-tuples comprising X,Y,I,J integers in the range 0 through 999999
|
||
|
# and D as with (X,Y,D) triples. The 's' integer is non-zero to indicate that
|
||
|
# the (I,J) tuple is a SIGNED offset (for multi-quadrant circular interpolation)
|
||
|
# else the tuple is unsigned.
|
||
|
#
|
||
|
# This variable is, as for apxlat, a dictionary keyed by layer name.
|
||
|
self.commands = {}
|
||
|
|
||
|
# This dictionary stores all GLOBAL apertures actually needed by this
|
||
|
# layer, i.e., apertures specified prior to draw commands. The dictionary
|
||
|
# is indexed by layer name, and each dictionary entry is a list of aperture
|
||
|
# code strings, like 'D12'. This dictionary helps us to figure out the
|
||
|
# minimum number of apertures that need to be written out in the Gerber
|
||
|
# header of the merged file. Once again, the list of apertures refers to
|
||
|
# GLOBAL aperture codes in the GAT, not ones local to this layer.
|
||
|
self.apertures = {}
|
||
|
|
||
|
# Excellon commands are grouped by tool number in a dictionary.
|
||
|
# This is to help sorting all jobs and writing out all plunge
|
||
|
# commands for a single tool.
|
||
|
#
|
||
|
# The key to this dictionary is the full tool name, e.g., T03
|
||
|
# as a string. Each command is an (X,Y) integer tuple.
|
||
|
self.xcommands = {}
|
||
|
|
||
|
# This is a dictionary mapping LOCAL tool names (e.g., T03) to diameters
|
||
|
# in inches for THIS JOB. This dictionary will be initially empty
|
||
|
# for old-style Excellon files with no embedded tool sizes. The
|
||
|
# main program will construct this dictionary from the global tool
|
||
|
# table in this case, once all jobs have been read in.
|
||
|
self.xdiam = {}
|
||
|
|
||
|
# This is a mapping from tool name to diameter for THIS JOB
|
||
|
self.ToolList = None
|
||
|
|
||
|
# How many times to replicate this job if using auto-placement
|
||
|
self.Repeat = 1
|
||
|
|
||
|
# How many decimal digits of precision there are in the Excellon file.
|
||
|
# A value greater than 0 overrides the global ExcellonDecimals setting
|
||
|
# for this file, allowing jobs with different Excellon decimal settings
|
||
|
# to be combined.
|
||
|
self.ExcellonDecimals = 0 # 0 means global value prevails
|
||
|
|
||
|
def width_in(self):
|
||
|
"Return width in INCHES"
|
||
|
return float(self.maxx-self.minx)*0.00001
|
||
|
|
||
|
def height_in(self):
|
||
|
"Return height in INCHES"
|
||
|
return float(self.maxy-self.miny)*0.00001
|
||
|
|
||
|
def jobarea(self):
|
||
|
return self.width_in()*self.height_in()
|
||
|
|
||
|
def maxdimension(self):
|
||
|
return max(self.width_in(),self.height_in())
|
||
|
|
||
|
def mincoordinates(self):
|
||
|
"Return minimum X and Y coordinate"
|
||
|
|
||
|
return self.minx, self.miny
|
||
|
|
||
|
def fixcoordinates(self, x_shift, y_shift):
|
||
|
"Add x_shift and y_shift to all coordinates in the job"
|
||
|
|
||
|
# Shift maximum and minimum coordinates
|
||
|
self.minx += x_shift
|
||
|
self.maxx += x_shift
|
||
|
self.miny += y_shift
|
||
|
self.maxy += y_shift
|
||
|
|
||
|
# Shift all commands
|
||
|
for layer, command in self.commands.iteritems():
|
||
|
|
||
|
# Loop through each command in each layer
|
||
|
for index in range( len(command) ):
|
||
|
c = command[index]
|
||
|
|
||
|
# Shift X and Y coordinate of command
|
||
|
if type(c) == types.TupleType: ## ensure that command is of type tuple
|
||
|
command_list = list(c) ## convert tuple to list
|
||
|
if (type( command_list[0] ) == types.IntType) \
|
||
|
and (type( command_list[1] ) == types.IntType): ## ensure that first two elemenst are integers
|
||
|
command_list[0] += x_shift
|
||
|
command_list[1] += y_shift
|
||
|
command[index] = tuple(command_list) ## convert list back to tuple
|
||
|
|
||
|
self.commands[layer] = command ## set modified command
|
||
|
|
||
|
# Shift all excellon commands
|
||
|
for tool, command in self.xcommands.iteritems():
|
||
|
|
||
|
# Loop through each command in each layer
|
||
|
for index in range( len(command) ):
|
||
|
c = command[index]
|
||
|
|
||
|
# Shift X and Y coordinate of command
|
||
|
command_list = list(c) ## convert tuple to list
|
||
|
if ( type( command_list[0] ) == types.IntType ) \
|
||
|
and ( type( command_list[1] ) == types.IntType ): ## ensure that first two elemenst are integers
|
||
|
command_list[0] += x_shift / 10
|
||
|
command_list[1] += y_shift / 10
|
||
|
command[index] = tuple(command_list) ## convert list back to tuple
|
||
|
|
||
|
self.xcommands[tool] = command ## set modified command
|
||
|
|
||
|
def parseGerber(self, fullname, layername, updateExtents = 0):
|
||
|
"""Do the dirty work. Read the Gerber file given the
|
||
|
global aperture table GAT and global aperture macro table GAMT"""
|
||
|
|
||
|
GAT = config.GAT
|
||
|
GAMT = config.GAMT
|
||
|
# First construct reverse GAT/GAMT, mapping definition to code
|
||
|
RevGAT = config.buildRevDict(GAT) # RevGAT[hash] = aperturename
|
||
|
RevGAMT = config.buildRevDict(GAMT) # RevGAMT[hash] = aperturemacroname
|
||
|
|
||
|
#print 'Reading data from %s ...' % fullname
|
||
|
|
||
|
fid = file(fullname, 'rt')
|
||
|
currtool = None
|
||
|
|
||
|
self.apxlat[layername] = {}
|
||
|
self.apmxlat[layername] = {}
|
||
|
self.commands[layername] = []
|
||
|
self.apertures[layername] = []
|
||
|
|
||
|
# These divisors are used to scale (X,Y) co-ordinates. We store
|
||
|
# everything as integers in hundred-thousandths of an inch (i.e., M.5
|
||
|
# format). If we get something in M.4 format, we must multiply by
|
||
|
# 10. If we get something in M.6 format we must divide by 10, etc.
|
||
|
x_div = 1.0
|
||
|
y_div = 1.0
|
||
|
|
||
|
# Drawing commands can be repeated with X or Y omitted if they are
|
||
|
# the same as before. These variables store the last X/Y value as
|
||
|
# integers in hundred-thousandths of an inch.
|
||
|
last_x = last_y = 0
|
||
|
|
||
|
# Last modal G-code. Some G-codes introduce "modes", such as circular interpolation
|
||
|
# mode, and we want to remember what mode we're in. We're interested in:
|
||
|
# G01 -- linear interpolation, cancels all circular interpolation modes
|
||
|
# G36 -- Turn on polygon area fill
|
||
|
# G37 -- Turn off polygon area fill
|
||
|
last_gmode = 1 # G01 by default, linear interpolation
|
||
|
|
||
|
# We want to know whether to do signed (G75) or unsigned (G74) I/J offsets. These
|
||
|
# modes are independent of G01/G02/G03, e.g., Protel will issue multiple G03/G01
|
||
|
# codes all in G75 mode.
|
||
|
# G74 -- Single-quadrant circular interpolation (disables multi-quadrant interpolation)
|
||
|
# G02/G03 codes set clockwise/counterclockwise arcs in a single quadrant only
|
||
|
# using X/Y/I/J commands with UNSIGNED (I,J).
|
||
|
# G75 -- Multi-quadrant circular interpolation --> X/Y/I/J with signed (I,J)
|
||
|
# G02/G03 codes set clockwise/counterclockwise arcs in all 4 quadrants
|
||
|
# using X/Y/I/J commands with SIGNED (I,J).
|
||
|
circ_signed = True # Assume G75...make sure this matches canned header we write out
|
||
|
|
||
|
# If the very first flash/draw is a shorthand command (i.e., without an Xxxxx or Yxxxx)
|
||
|
# component then we don't really "see" the first point X00000Y00000. To account for this
|
||
|
# we use the following Boolean flag as well as the isLastShorthand flag during parsing
|
||
|
# to manually insert the point X000000Y00000 into the command stream.
|
||
|
firstFlash = True
|
||
|
|
||
|
for line in fid:
|
||
|
# Get rid of CR characters (0x0D) and leading/trailing blanks
|
||
|
line = string.replace(line, '\x0D', '').strip()
|
||
|
|
||
|
# Old location of format_pat search. Now moved down into the sub-line parse loop below.
|
||
|
|
||
|
# RS-274X statement? If so, echo it. Currently, only the "LP" statement is expected
|
||
|
# (from Protel, of course). These will be distinguished from D-code and G-code
|
||
|
# commands by the fact that the first character of the string is '%'.
|
||
|
match = layerpol_pat.match(line)
|
||
|
if match:
|
||
|
self.commands[layername].append(line)
|
||
|
continue
|
||
|
|
||
|
# See if this is an aperture definition, and if so, map it.
|
||
|
match = apdef_pat.match(line)
|
||
|
if match:
|
||
|
if currtool:
|
||
|
raise RuntimeError, "File %s has an aperture definition that comes after drawing commands." % fullname
|
||
|
|
||
|
A = aptable.parseAperture(line, self.apmxlat[layername])
|
||
|
if not A:
|
||
|
raise RuntimeError, "Unknown aperture definition in file %s" % fullname
|
||
|
|
||
|
hash = A.hash()
|
||
|
if not RevGAT.has_key(hash):
|
||
|
#print line
|
||
|
#print self.apmxlat
|
||
|
#print RevGAT
|
||
|
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
|
||
|
# be replaced by aperture self.apxlat[layername][code].
|
||
|
self.apxlat[layername][A.code] = RevGAT[hash]
|
||
|
continue
|
||
|
|
||
|
# Ignore %AMOC8* from Eagle for now as it uses a macro parameter, which
|
||
|
# is not yet supported in GerbMerge.
|
||
|
if line[:7]=='%AMOC8*':
|
||
|
continue
|
||
|
|
||
|
# See if this is an aperture macro definition, and if so, map it.
|
||
|
M = amacro.parseApertureMacro(line,fid)
|
||
|
if M:
|
||
|
if currtool:
|
||
|
raise RuntimeError, "File %s has an aperture macro definition that comes after drawing commands." % fullname
|
||
|
|
||
|
hash = M.hash()
|
||
|
if not RevGAMT.has_key(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
|
||
|
# will be replaced by aperture macro name self.apmxlat[layername][macroname].
|
||
|
self.apmxlat[layername][M.name] = RevGAMT[hash]
|
||
|
continue
|
||
|
|
||
|
# From this point on we may have more than one match on this line, e.g.:
|
||
|
# G54D11*X22400Y22300D02*X22500Y22200D01*
|
||
|
sub_line = line
|
||
|
while sub_line:
|
||
|
# Handle "comment" G-codes first
|
||
|
match = comment_pat.match(sub_line)
|
||
|
if match:
|
||
|
sub_line = sub_line[match.end():]
|
||
|
continue
|
||
|
|
||
|
# See if this is a format statement, and if so, map it. In version 1.3 this was moved down
|
||
|
# from the line-only parse checks above (see comment) to handle OrCAD lines like
|
||
|
# G74*%FSLAN2X34Y34*%
|
||
|
match = format_pat.match(sub_line) # Used to be format_pat.search
|
||
|
if match:
|
||
|
sub_line = sub_line[match.end():]
|
||
|
for item in match.groups():
|
||
|
if item is None: continue # Optional group didn't match
|
||
|
|
||
|
if item[0] in "LA": # omit leading zeroes and absolute co-ordinates
|
||
|
continue
|
||
|
|
||
|
if item[0]=='T': # omit trailing zeroes
|
||
|
raise RuntimeError, "Trailing zeroes not supported in RS274X files"
|
||
|
if item[0]=='I': # incremental co-ordinates
|
||
|
raise RuntimeError, "Incremental co-ordinates not supported in RS274X files"
|
||
|
|
||
|
if item[0]=='N': # Maximum digits for N* commands...ignore it
|
||
|
continue
|
||
|
|
||
|
if item[0]=='X': # M.N specification for X-axis.
|
||
|
fracpart = int(item[2])
|
||
|
x_div = 10.0**(5-fracpart)
|
||
|
if item[0]=='Y': # M.N specification for Y-axis.
|
||
|
fracpart = int(item[2])
|
||
|
y_div = 10.0**(5-fracpart)
|
||
|
continue
|
||
|
|
||
|
# Parse and interpret G-codes
|
||
|
match = gcode_pat.match(sub_line)
|
||
|
if match:
|
||
|
sub_line = sub_line[match.end():]
|
||
|
gcode = int(match.group(1))
|
||
|
|
||
|
# Determine if this is a G-Code that should be ignored because it has no effect
|
||
|
# (e.g., G70 specifies "inches" which is already in effect).
|
||
|
if gcode in [54, 70, 90]:
|
||
|
continue
|
||
|
|
||
|
# Determine if this is a G-Code that we have to emit because it matters.
|
||
|
if gcode in [1, 2, 3, 36, 37, 74, 75]:
|
||
|
self.commands[layername].append("G%02d" % gcode)
|
||
|
|
||
|
# Determine if this is a G-code that sets a new mode
|
||
|
if gcode in [1, 36, 37]:
|
||
|
last_gmode = gcode
|
||
|
|
||
|
# Remember last G74/G75 code so we know whether to do signed or unsigned I/J
|
||
|
# offsets.
|
||
|
if gcode==74:
|
||
|
circ_signed = False
|
||
|
elif gcode==75:
|
||
|
circ_signed = True
|
||
|
|
||
|
continue
|
||
|
|
||
|
raise RuntimeError, "G-Code 'G%02d' is not supported" % gcode
|
||
|
|
||
|
# See if this is a tool change (aperture change) command
|
||
|
match = tool_pat.match(sub_line)
|
||
|
if match:
|
||
|
currtool = match.group(1)
|
||
|
|
||
|
# Protel likes to issue random D01, D02, and D03 commands instead of aperture
|
||
|
# codes. We can ignore D01 because it simply means to move to the current location
|
||
|
# while drawing. Well, that's drawing a point. We can ignore D02 because it means
|
||
|
# to move to the current location without drawing. Truly pointless. We do NOT want
|
||
|
# to ignore D03 because it implies a flash. Protel very inefficiently issues a D02
|
||
|
# move to a location without drawing, then a single-line D03 to flash. However, a D02
|
||
|
# terminates a polygon in G36 mode, so keep D02's in this case.
|
||
|
if currtool=='D01' or (currtool=='D02' and (last_gmode != 36)):
|
||
|
sub_line = sub_line[match.end():]
|
||
|
continue
|
||
|
|
||
|
if (currtool == 'D03') or (currtool=='D02' and (last_gmode == 36)):
|
||
|
self.commands[layername].append(currtool)
|
||
|
sub_line = sub_line[match.end():]
|
||
|
continue
|
||
|
|
||
|
# Map it using our translation table
|
||
|
if not self.apxlat[layername].has_key(currtool):
|
||
|
raise RuntimeError, 'File %s has tool change command "%s" with no corresponding translation' % (fullname, currtool)
|
||
|
|
||
|
currtool = self.apxlat[layername][currtool]
|
||
|
|
||
|
# Add it to the list of things to write out
|
||
|
self.commands[layername].append(currtool)
|
||
|
|
||
|
# Add it to the list of all apertures needed by this layer
|
||
|
self.apertures[layername].append(currtool)
|
||
|
|
||
|
# Move on to next match, if any
|
||
|
sub_line = sub_line[match.end():]
|
||
|
continue
|
||
|
|
||
|
# Is it a simple draw command?
|
||
|
I = J = None # For circular interpolation drawing commands
|
||
|
match = drawXY_pat.match(sub_line)
|
||
|
isLastShorthand = False # By default assume we don't make use of last_x and last_y
|
||
|
if match:
|
||
|
x, y, d = map(__builtin__.int, match.groups())
|
||
|
else:
|
||
|
match = drawX_pat.match(sub_line)
|
||
|
if match:
|
||
|
x, d = map(__builtin__.int, match.groups())
|
||
|
y = last_y
|
||
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||
|
else:
|
||
|
match = drawY_pat.match(sub_line)
|
||
|
if match:
|
||
|
y, d = map(__builtin__.int, match.groups())
|
||
|
x = last_x
|
||
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||
|
|
||
|
# Maybe it's a circular interpolation draw command with IJ components
|
||
|
if match is None:
|
||
|
match = cdrawXY_pat.match(sub_line)
|
||
|
if match:
|
||
|
x, y, I, J, d = map(__builtin__.int, match.groups())
|
||
|
else:
|
||
|
match = cdrawX_pat.match(sub_line)
|
||
|
if match:
|
||
|
x, I, J, d = map(__builtin__.int, match.groups())
|
||
|
y = last_y
|
||
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||
|
else:
|
||
|
match = cdrawY_pat.match(sub_line)
|
||
|
if match:
|
||
|
y, I, J, d = map(__builtin__.int, match.groups())
|
||
|
x = last_x
|
||
|
isLastShorthand = True # Indicate we're making use of last_x/last_y
|
||
|
|
||
|
if match:
|
||
|
if currtool is None:
|
||
|
# It's OK if this is an exposure-off movement command (specified with D02).
|
||
|
# It's also OK if we're in the middle of a G36 polygon fill as we're only defining
|
||
|
# the polygon extents.
|
||
|
if (d != 2) and (last_gmode != 36):
|
||
|
raise RuntimeError, 'File %s has draw command %s with no aperture chosen' % (fullname, sub_line)
|
||
|
|
||
|
# 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!
|
||
|
last_x = x
|
||
|
last_y = y
|
||
|
|
||
|
# Corner case: if this is the first flash/draw and we are using shorthand (i.e., missing Xxxx
|
||
|
# or Yxxxxx) then prepend the point X0000Y0000 into the commands as it is actually the starting
|
||
|
# point of our layer. We prepend the command X0000Y0000D02, i.e., a move to (0,0) without drawing.
|
||
|
if (isLastShorthand and firstFlash):
|
||
|
self.commands[layername].append((0,0,2))
|
||
|
if updateExtents:
|
||
|
self.minx = min(self.minx,0)
|
||
|
self.maxx = max(self.maxx,0)
|
||
|
self.miny = min(self.miny,0)
|
||
|
self.maxy = max(self.maxy,0)
|
||
|
|
||
|
x = int(round(x*x_div))
|
||
|
y = int(round(y*y_div))
|
||
|
if I is not None:
|
||
|
I = int(round(I*x_div))
|
||
|
J = int(round(J*y_div))
|
||
|
self.commands[layername].append((x,y,I,J,d,circ_signed))
|
||
|
else:
|
||
|
self.commands[layername].append((x,y,d))
|
||
|
firstFlash = False
|
||
|
|
||
|
# Update dimensions...this is complicated for circular interpolation commands
|
||
|
# that span more than one quadrant. For now, we ignore this problem since users
|
||
|
# should be using a border layer to indicate extents.
|
||
|
if updateExtents:
|
||
|
if x < self.minx: self.minx = x
|
||
|
if x > self.maxx: self.maxx = x
|
||
|
if y < self.miny: self.miny = y
|
||
|
if y > self.maxy: self.maxy = y
|
||
|
|
||
|
# Move on to next match, if any
|
||
|
sub_line = sub_line[match.end():]
|
||
|
continue
|
||
|
|
||
|
# If it's none of the above, it had better be on our ignore list.
|
||
|
for pat in IgnoreList:
|
||
|
match = pat.match(sub_line)
|
||
|
if match:
|
||
|
break
|
||
|
else:
|
||
|
raise RuntimeError, 'File %s has uninterpretable line:\n %s' % (fullname, line)
|
||
|
|
||
|
sub_line = sub_line[match.end():]
|
||
|
# end while still things to match on this line
|
||
|
# end of for each line in file
|
||
|
|
||
|
fid.close()
|
||
|
if 0:
|
||
|
print layername
|
||
|
print self.commands[layername]
|
||
|
|
||
|
def parseExcellon(self, fullname):
|
||
|
#print 'Reading data from %s ...' % fullname
|
||
|
|
||
|
fid = file(fullname, 'rt')
|
||
|
currtool = None
|
||
|
suppress_leading = True # Suppress leading zeros by default, equivalent to 'INCH,TZ'
|
||
|
|
||
|
# We store Excellon X/Y data in ten-thousandths of an inch. If the Config
|
||
|
# option ExcellonDecimals is not 4, we must adjust the values read from the
|
||
|
# file by a divisor to convert to ten-thousandths. This is only used in
|
||
|
# leading-zero suppression mode. In trailing-zero suppression mode, we must
|
||
|
# trailing-zero-pad all input integers to M+N digits (e.g., 6 digits for 2.4 mode)
|
||
|
# specified by the 'zeropadto' variable.
|
||
|
if self.ExcellonDecimals > 0:
|
||
|
divisor = 10.0**(4 - self.ExcellonDecimals)
|
||
|
zeropadto = 2+self.ExcellonDecimals
|
||
|
else:
|
||
|
divisor = 10.0**(4 - config.Config['excellondecimals'])
|
||
|
zeropadto = 2+config.Config['excellondecimals']
|
||
|
|
||
|
# Protel takes advantage of optional X/Y components when the previous one is the same,
|
||
|
# so we have to remember them.
|
||
|
last_x = last_y = 0
|
||
|
|
||
|
# Helper function to convert X/Y strings into integers in units of ten-thousandth of an inch.
|
||
|
def xln2tenthou(L, divisor=divisor, zeropadto=zeropadto):
|
||
|
V = []
|
||
|
for s in L:
|
||
|
if not suppress_leading:
|
||
|
s = s + '0'*(zeropadto-len(s))
|
||
|
V.append(int(round(int(s)*divisor)))
|
||
|
return tuple(V)
|
||
|
|
||
|
for line in fid.xreadlines():
|
||
|
# Get rid of CR characters
|
||
|
line = string.replace(line, '\x0D', '')
|
||
|
|
||
|
# Protel likes to embed comment lines beginning with ';'
|
||
|
if line[0]==';':
|
||
|
continue
|
||
|
|
||
|
# Check for leading/trailing zeros included ("INCH,LZ" or "INCH,TZ")
|
||
|
match = xzsup_pat.match(line)
|
||
|
if match:
|
||
|
if match.group(1)=='L':
|
||
|
# LZ --> Leading zeros INCLUDED
|
||
|
suppress_leading = False
|
||
|
else:
|
||
|
# TZ --> Trailing zeros INCLUDED
|
||
|
suppress_leading = True
|
||
|
continue
|
||
|
|
||
|
# See if a tool is being defined. First try to match with tool name+size
|
||
|
match = xtdef_pat.match(line) # xtdef_pat and xtdef2_pat expect tool name and diameter
|
||
|
if match is None: # but xtdef_pat expects optional feed/speed between T and C
|
||
|
match = xtdef2_pat.match(line) # and xtdef_2pat expects feed/speed at the end
|
||
|
if match:
|
||
|
currtool, diam = match.groups()
|
||
|
try:
|
||
|
diam = float(diam)
|
||
|
except:
|
||
|
raise RuntimeError, "File %s has illegal tool diameter '%s'" % (fullname, diam)
|
||
|
|
||
|
# Canonicalize tool number because Protel (of course) sometimes specifies it
|
||
|
# as T01 and sometimes as T1. We canonicalize to T01.
|
||
|
currtool = 'T%02d' % int(currtool[1:])
|
||
|
|
||
|
if self.xdiam.has_key(currtool):
|
||
|
raise RuntimeError, "File %s defines tool %s more than once" % (fullname, currtool)
|
||
|
self.xdiam[currtool] = diam
|
||
|
continue
|
||
|
|
||
|
# Didn't match TxxxCyyy. It could be a tool change command 'Tdd'.
|
||
|
match = xtool_pat.match(line)
|
||
|
if match:
|
||
|
currtool = match.group(1)
|
||
|
|
||
|
# Canonicalize tool number because Protel (of course) sometimes specifies it
|
||
|
# as T01 and sometimes as T1. We canonicalize to T01.
|
||
|
currtool = 'T%02d' % int(currtool[1:])
|
||
|
|
||
|
# Diameter will be obtained from embedded tool definition, local tool list or if not found, the global tool list
|
||
|
try:
|
||
|
diam = self.xdiam[currtool]
|
||
|
except:
|
||
|
if self.ToolList:
|
||
|
try:
|
||
|
diam = self.ToolList[currtool]
|
||
|
except:
|
||
|
raise RuntimeError, "File %s uses tool code %s that is not defined in the job's tool list" % (fullname, currtool)
|
||
|
else:
|
||
|
try:
|
||
|
diam = config.DefaultToolList[currtool]
|
||
|
except:
|
||
|
#print config.DefaultToolList
|
||
|
raise RuntimeError, "File %s uses tool code %s that is not defined in default tool list" % (fullname, currtool)
|
||
|
|
||
|
self.xdiam[currtool] = diam
|
||
|
continue
|
||
|
|
||
|
# Plunge command?
|
||
|
match = xydraw_pat.match(line)
|
||
|
if match:
|
||
|
x, y = xln2tenthou(match.groups())
|
||
|
else:
|
||
|
match = xdraw_pat.match(line)
|
||
|
if match:
|
||
|
x = xln2tenthou(match.groups())[0]
|
||
|
y = last_y
|
||
|
else:
|
||
|
match = ydraw_pat.match(line)
|
||
|
if match:
|
||
|
y = xln2tenthou(match.groups())[0]
|
||
|
x = last_x
|
||
|
|
||
|
if match:
|
||
|
if currtool is None:
|
||
|
raise RuntimeError, 'File %s has plunge command without previous tool selection' % fullname
|
||
|
|
||
|
try:
|
||
|
self.xcommands[currtool].append((x,y))
|
||
|
except KeyError:
|
||
|
self.xcommands[currtool] = [(x,y)]
|
||
|
|
||
|
last_x = x
|
||
|
last_y = y
|
||
|
continue
|
||
|
|
||
|
# It had better be an ignorable
|
||
|
for pat in XIgnoreList:
|
||
|
if pat.match(line):
|
||
|
break
|
||
|
else:
|
||
|
raise RuntimeError, 'File %s has uninterpretable line:\n %s' % (fullname, line)
|
||
|
|
||
|
def hasLayer(self, layername):
|
||
|
return self.commands.has_key(layername)
|
||
|
|
||
|
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"
|
||
|
|
||
|
# Maybe we don't have this layer
|
||
|
if not self.hasLayer(layername): return
|
||
|
|
||
|
# First convert given inches to 2.5 co-ordinates
|
||
|
X = int(round(Xoff/0.00001))
|
||
|
Y = int(round(Yoff/0.00001))
|
||
|
|
||
|
# Now calculate displacement for each position so that we end up at specified origin
|
||
|
DX = X - self.minx
|
||
|
DY = Y - self.miny
|
||
|
|
||
|
# Rock and roll. First, write out a dummy flash using code D02
|
||
|
# (exposure off). This prevents an unintentional draw from the end
|
||
|
# of one job to the beginning of the next when a layer is repeated
|
||
|
# due to panelizing.
|
||
|
fid.write('X%07dY%07dD02*\n' % (X, Y))
|
||
|
for cmd in self.commands[layername]:
|
||
|
if type(cmd) is types.TupleType:
|
||
|
if len(cmd)==3:
|
||
|
x, y, d = cmd
|
||
|
fid.write('X%07dY%07dD%02d*\n' % (x+DX, y+DY, d))
|
||
|
else:
|
||
|
x, y, I, J, d, s = cmd
|
||
|
fid.write('X%07dY%07dI%07dJ%07dD%02d*\n' % (x+DX, y+DY, I, J, d)) # I,J are relative
|
||
|
else:
|
||
|
# It's an aperture change, G-code, or RS274-X command that begins with '%'. If
|
||
|
# it's an aperture code, the aperture has already been translated
|
||
|
# to the global aperture table during the parse phase.
|
||
|
if cmd[0]=='%':
|
||
|
fid.write('%s\n' % cmd) # The command already has a * in it (e.g., "%LPD*%")
|
||
|
else:
|
||
|
fid.write('%s*\n' % cmd)
|
||
|
|
||
|
def findTools(self, diameter):
|
||
|
"Find the tools, if any, with the given diameter in inches. There may be more than one!"
|
||
|
L = []
|
||
|
for tool, diam in self.xdiam.items():
|
||
|
if diam==diameter:
|
||
|
L.append(tool)
|
||
|
return L
|
||
|
|
||
|
def writeExcellon(self, fid, diameter, Xoff, Yoff):
|
||
|
"Write out the data such that the lower-left corner of this job is at the given (X,Y) position, in inches"
|
||
|
|
||
|
# First convert given inches to 2.4 co-ordinates. Note that Gerber is 2.5 (as of GerbMerge 1.2)
|
||
|
# and our internal Excellon representation is 2.4 as of GerbMerge
|
||
|
# version 0.91. We use X,Y to calculate DX,DY in 2.4 units (i.e., with a
|
||
|
# resolution of 0.0001".
|
||
|
X = int(round(Xoff/0.00001)) # First work in 2.5 format to match Gerber
|
||
|
Y = int(round(Yoff/0.00001))
|
||
|
|
||
|
# Now calculate displacement for each position so that we end up at specified origin
|
||
|
DX = X - self.minx
|
||
|
DY = Y - self.miny
|
||
|
|
||
|
# Now round down to 2.4 format
|
||
|
DX = int(round(DX/10.0))
|
||
|
DY = int(round(DY/10.0))
|
||
|
|
||
|
ltools = self.findTools(diameter)
|
||
|
|
||
|
if config.Config['excellonleadingzeros']:
|
||
|
fmtstr = 'X%06dY%06d\n'
|
||
|
else:
|
||
|
fmtstr = 'X%dY%d\n'
|
||
|
|
||
|
# Boogie
|
||
|
for ltool in ltools:
|
||
|
if self.xcommands.has_key(ltool):
|
||
|
for cmd in self.xcommands[ltool]:
|
||
|
x, y = cmd
|
||
|
fid.write(fmtstr % (x+DX, y+DY))
|
||
|
|
||
|
def writeDrillHits(self, fid, diameter, toolNum, Xoff, Yoff):
|
||
|
"""Write a drill hit pattern. diameter is tool diameter in inches, while toolNum is
|
||
|
an integer index into strokes.DrillStrokeList"""
|
||
|
|
||
|
# First convert given inches to 2.5 co-ordinates
|
||
|
X = int(round(Xoff/0.00001))
|
||
|
Y = int(round(Yoff/0.00001))
|
||
|
|
||
|
# Now calculate displacement for each position so that we end up at specified origin
|
||
|
DX = X - self.minx
|
||
|
DY = Y - self.miny
|
||
|
|
||
|
# Do NOT round down to 2.4 format. These drill hits are in Gerber 2.5 format, not
|
||
|
# Excellon plunge commands.
|
||
|
|
||
|
ltools = self.findTools(diameter)
|
||
|
|
||
|
for ltool in ltools:
|
||
|
if self.xcommands.has_key(ltool):
|
||
|
for cmd in self.xcommands[ltool]:
|
||
|
x, y = cmd
|
||
|
makestroke.drawDrillHit(fid, 10*x+DX, 10*y+DY, toolNum)
|
||
|
|
||
|
def aperturesAndMacros(self, layername):
|
||
|
"Return dictionaries whose keys are all necessary aperture names and macro names for this layer"
|
||
|
|
||
|
GAT=config.GAT
|
||
|
|
||
|
if self.apertures.has_key(layername):
|
||
|
apdict = {}.fromkeys(self.apertures[layername])
|
||
|
apmlist = [GAT[ap].dimx for ap in self.apertures[layername] if GAT[ap].apname=='Macro']
|
||
|
apmdict = {}.fromkeys(apmlist)
|
||
|
|
||
|
return apdict, apmdict
|
||
|
else:
|
||
|
return {}, {}
|
||
|
|
||
|
def makeLocalApertureCode(self, layername, AP):
|
||
|
"Find or create a layer-specific aperture code to represent the global aperture given"
|
||
|
if AP.code not in self.apxlat[layername].values():
|
||
|
lastCode = aptable.findHighestApertureCode(self.apxlat[layername].keys())
|
||
|
localCode = 'D%d' % (lastCode+1)
|
||
|
self.apxlat[layername][localCode] = AP.code
|
||
|
|
||
|
def inBorders(self, x, y):
|
||
|
return (x >= self.minx) and (x <= self.maxx) and (y >= self.miny) and (y <= self.maxy)
|
||
|
|
||
|
def trimGerberLayer(self, layername):
|
||
|
"Modify drawing commands that are outside job dimensions"
|
||
|
|
||
|
newcmds = []
|
||
|
lastInBorders = True
|
||
|
lastx, lasty, lastd = self.minx, self.miny, 2 # (minx,miny,exposure off)
|
||
|
bordersRect = (self.minx, self.miny, self.maxx, self.maxy)
|
||
|
lastAperture = None
|
||
|
|
||
|
for cmd in self.commands[layername]:
|
||
|
if type(cmd) == types.TupleType:
|
||
|
# It is a data command: tuple (X, Y, D), all integers, or (X, Y, I, J, D), all integers.
|
||
|
if len(cmd)==3:
|
||
|
x, y, d = cmd
|
||
|
# I=J=None # In case we support circular interpolation in the future
|
||
|
else:
|
||
|
# We don't do anything with circular interpolation for now, so just issue
|
||
|
# the command and be done with it.
|
||
|
# x, y, I, J, d, s = cmd
|
||
|
newcmds.append(cmd)
|
||
|
continue
|
||
|
|
||
|
newInBorders = self.inBorders(x,y)
|
||
|
|
||
|
# Flash commands are easy (for now). If they're outside borders,
|
||
|
# ignore them. There's no need to consider the previous command.
|
||
|
# What should we do if the flash is partially inside and partially
|
||
|
# outside the border? Ideally, define a macro that constructs the
|
||
|
# part of the flash that is inside the border. Practically, you've
|
||
|
# got to be kidding.
|
||
|
#
|
||
|
# Actually, it's not that tough for rectangle apertures. We identify
|
||
|
# the intersection rectangle of the aperture and the bounding box,
|
||
|
# determine the new rectangular aperture required along with the
|
||
|
# new flash point, add the aperture to the GAT if necessary, and
|
||
|
# make the change. Spiffy.
|
||
|
#
|
||
|
# For circular interpolation commands, it's definitely harder since
|
||
|
# we have to construct arcs that are a subset of the original arc.
|
||
|
#
|
||
|
# For polygon fills, we similarly have to break up the polygon into
|
||
|
# sub-polygons that are contained within the allowable extents.
|
||
|
#
|
||
|
# Both circular interpolation and polygon fills are a) uncommon,
|
||
|
# and b) hard to handle. The current version of GerbMerge does not
|
||
|
# handle these cases.
|
||
|
if d==3:
|
||
|
if lastAperture.isRectangle():
|
||
|
apertureRect = lastAperture.rectangleAsRect(x,y)
|
||
|
if geometry.isRect1InRect2(apertureRect, bordersRect):
|
||
|
newcmds.append(cmd)
|
||
|
else:
|
||
|
newRect = geometry.intersectExtents(apertureRect, bordersRect)
|
||
|
|
||
|
if newRect:
|
||
|
newRectWidth = geometry.rectWidth(newRect)
|
||
|
newRectHeight = geometry.rectHeight(newRect)
|
||
|
newX, newY = geometry.rectCenter(newRect)
|
||
|
|
||
|
# We arbitrarily remove all flashes that lead to rectangles
|
||
|
# with a width or length less than 1 mil (10 Gerber units).
|
||
|
# Should we make this configurable?
|
||
|
if min(newRectWidth, newRectHeight) >= 10:
|
||
|
# Construct an Aperture that is a Rectangle of dimensions (newRectWidth,newRectHeight)
|
||
|
newAP = aptable.Aperture(aptable.Rectangle, 'D??', \
|
||
|
util.gerb2in(newRectWidth), util.gerb2in(newRectHeight))
|
||
|
global_code = aptable.findOrAddAperture(newAP)
|
||
|
|
||
|
# We need an unused local aperture code to correspond to this newly-created global one.
|
||
|
self.makeLocalApertureCode(layername, newAP)
|
||
|
|
||
|
# Make sure to indicate that the new aperture is one that is used by this layer
|
||
|
if global_code not in self.apertures[layername]:
|
||
|
self.apertures[layername].append(global_code)
|
||
|
|
||
|
# Switch to new aperture code, flash new aperture, switch back to previous aperture code
|
||
|
newcmds.append(global_code)
|
||
|
newcmds.append((newX, newY, 3))
|
||
|
newcmds.append(lastAperture.code)
|
||
|
else:
|
||
|
pass # Ignore this flash...area in common is too thin
|
||
|
else:
|
||
|
pass # Ignore this flash...no area in common
|
||
|
elif self.inBorders(x, y):
|
||
|
# Aperture is not a rectangle and its center is somewhere within our
|
||
|
# borders. Flash it and ignore part outside borders (for now).
|
||
|
newcmds.append(cmd)
|
||
|
else:
|
||
|
pass # Ignore this flash
|
||
|
|
||
|
# If this is a exposure off command, then it doesn't matter what the
|
||
|
# previous command is. This command just updates the (X,Y) position
|
||
|
# and sets the start point for a line draw to a new location.
|
||
|
elif d==2:
|
||
|
if self.inBorders(x, y):
|
||
|
newcmds.append(cmd)
|
||
|
|
||
|
else:
|
||
|
# This is an exposure on (draw line) command. Now things get interesting.
|
||
|
# Regardless of what the last command was (draw, exposure off, flash), we
|
||
|
# are planning on drawing a visible line using the current aperture from
|
||
|
# the (lastx,lasty) position to the new (x,y) position. The cases are:
|
||
|
# A: (lastx,lasty) is outside borders, (x,y) is outside borders.
|
||
|
# (lastx,lasty) have already been eliminated. Just update (lastx,lasty)
|
||
|
# with new (x,y) and remove the new command too. There is one case which
|
||
|
# may be of concern, and that is when the line defined by (lastx,lasty)-(x,y)
|
||
|
# actually crosses through the job. In this case, we have to draw the
|
||
|
# partial line (x1,y1)-(x2,y2) where (x1,y1) and (x2,y2) lie on the
|
||
|
# borders. We will add 3 commands:
|
||
|
# X(x1)Y(y1)D02 # exposure off
|
||
|
# X(x2)Y(y2)D01 # exposure on
|
||
|
# X(x)Y(y)D02 # exposure off
|
||
|
#
|
||
|
# B: (lastx,lasty) is outside borders, (x,y) is inside borders.
|
||
|
# We have to find the intersection of the line (lastx,lasty)-(x,y)
|
||
|
# with the borders and draw only the line segment (x1,y1)-(x,y):
|
||
|
# X(x1)Y(y1)D02 # exposure off
|
||
|
# X(x)Y(y)D01 # exposure on
|
||
|
#
|
||
|
# C: (lastx,lasty) is inside borders, (x,y) is outside borders.
|
||
|
# We have to find the intersection of the line (lastx,lasty)-(x,y)
|
||
|
# with the borders and draw only the line segment (lastx,lasty)-(x1,y1):
|
||
|
# then update to the new position:
|
||
|
# X(x1)Y(y1)D01 # exposure on
|
||
|
# X(x)Y(y)D02 # exposure off
|
||
|
#
|
||
|
# D: (lastx,lasty) is inside borders, (x,y) is inside borders. This is
|
||
|
# the most common and simplest case...just copy the command over:
|
||
|
# X(x)Y(y)D01 # exposure on
|
||
|
#
|
||
|
# All of the above are for linear interpolation. Circular interpolation
|
||
|
# is ignored for now.
|
||
|
if lastInBorders and newInBorders: # Case D
|
||
|
newcmds.append(cmd)
|
||
|
|
||
|
else:
|
||
|
# segmentXbox() returns a list of 0, 1, or 2 points describing the intersection
|
||
|
# points of the segment (lastx,lasty)-(x,y) with the box defined
|
||
|
# by lower-left corner (minx,miny) and upper-right corner (maxx,maxy).
|
||
|
pointsL = geometry.segmentXbox((lastx,lasty), (x,y), (self.minx,self.miny), (self.maxx,self.maxy))
|
||
|
|
||
|
if len(pointsL)==0: # Case A, no intersection
|
||
|
# Both points are outside the box and there is no overlap with box.
|
||
|
d = 2 # Command is effectively removed since newcmds wasn't extended.
|
||
|
# Ensure "last command" is exposure off to reflect this.
|
||
|
|
||
|
elif len(pointsL)==1: # Cases B and C
|
||
|
pt1 = pointsL[0]
|
||
|
if newInBorders: # Case B
|
||
|
newcmds.append((pt1[0], pt1[1], 2)) # Go to intersection point, exposure off
|
||
|
newcmds.append(cmd) # Go to destination point, exposure on
|
||
|
else: # Case C
|
||
|
newcmds.append((pt1[0], pt1[1], 1)) # Go to intersection point, exposure on
|
||
|
newcmds.append((x, y, 2)) # Go to destination point, exposure off
|
||
|
d = 2 # Make next 'lastd' represent exposure off
|
||
|
|
||
|
else: # Case A, two points of intersection
|
||
|
pt1 = pointsL[0]
|
||
|
pt2 = pointsL[1]
|
||
|
|
||
|
newcmds.append((pt1[0], pt1[1], 2)) # Go to first intersection point, exposure off
|
||
|
newcmds.append((pt2[0], pt2[1], 1)) # Draw to second intersection point, exposure on
|
||
|
newcmds.append((x, y, 2)) # Go to destination point, exposure off
|
||
|
d = 2 # Make next 'lastd' represent exposure off
|
||
|
|
||
|
lastx, lasty, lastd = x, y, d
|
||
|
lastInBorders = newInBorders
|
||
|
else:
|
||
|
# It's a string indicating an aperture change, G-code, or RS-274X
|
||
|
# command (e.g., "D13", "G75", "%LPD*%")
|
||
|
newcmds.append(cmd)
|
||
|
if cmd[0]=='D' and int(cmd[1:])>=10: # Don't interpret D01, D02, D03
|
||
|
lastAperture = config.GAT[cmd]
|
||
|
|
||
|
self.commands[layername] = newcmds
|
||
|
|
||
|
def trimGerber(self):
|
||
|
for layername in self.commands.keys():
|
||
|
self.trimGerberLayer(layername)
|
||
|
|
||
|
def trimExcellon(self):
|
||
|
"Remove plunge commands that are outside job dimensions"
|
||
|
keys = self.xcommands.keys()
|
||
|
for toolname in keys:
|
||
|
# Remember Excellon is 2.4 format while Gerber data is 2.5 format
|
||
|
validList = [(x,y) for x,y in self.xcommands[toolname] if self.inBorders(10*x,10*y)]
|
||
|
|
||
|
if validList:
|
||
|
self.xcommands[toolname] = validList
|
||
|
else:
|
||
|
del self.xcommands[toolname]
|
||
|
del self.xdiam[toolname]
|
||
|
|
||
|
# This class encapsulates a Job object, providing absolute
|
||
|
# positioning information.
|
||
|
class JobLayout:
|
||
|
def __init__(self, job):
|
||
|
self.job = job
|
||
|
self.x = None
|
||
|
self.y = None
|
||
|
|
||
|
def canonicalize(self): # Must return a JobLayout object as a list
|
||
|
return [self]
|
||
|
|
||
|
def writeGerber(self, fid, layername):
|
||
|
assert self.x is not None
|
||
|
self.job.writeGerber(fid, layername, self.x, self.y)
|
||
|
|
||
|
def aperturesAndMacros(self, layername):
|
||
|
return self.job.aperturesAndMacros(layername)
|
||
|
|
||
|
def writeExcellon(self, fid, diameter):
|
||
|
assert self.x is not None
|
||
|
self.job.writeExcellon(fid, diameter, self.x, self.y)
|
||
|
|
||
|
def writeDrillHits(self, fid, diameter, toolNum):
|
||
|
assert self.x is not None
|
||
|
self.job.writeDrillHits(fid, diameter, toolNum, self.x, self.y)
|
||
|
|
||
|
def writeCutLines(self, fid, drawing_code, X1, Y1, X2, Y2):
|
||
|
"""Draw a board outline using the given aperture code"""
|
||
|
def notEdge(x, X):
|
||
|
return round(abs(1000*(x-X)))
|
||
|
|
||
|
assert self.x and self.y
|
||
|
|
||
|
radius = config.GAT[drawing_code].dimx/2.0
|
||
|
|
||
|
# Start at lower-left, proceed clockwise
|
||
|
x = self.x - radius
|
||
|
y = self.y - radius
|
||
|
|
||
|
left = notEdge(self.x, X1)
|
||
|
right = notEdge(self.x+self.width_in(), X2)
|
||
|
bot = notEdge(self.y, Y1)
|
||
|
top = notEdge(self.y+self.height_in(), Y2)
|
||
|
|
||
|
BL = ((x), (y))
|
||
|
TL = ((x), (y+self.height_in()+2*radius))
|
||
|
TR = ((x+self.width_in()+2*radius), (y+self.height_in()+2*radius))
|
||
|
BR = ((x+self.width_in()+2*radius), (y))
|
||
|
|
||
|
if not left:
|
||
|
BL = (BL[0]+2*radius, BL[1])
|
||
|
TL = (TL[0]+2*radius, TL[1])
|
||
|
|
||
|
if not top:
|
||
|
TL = (TL[0], TL[1]-2*radius)
|
||
|
TR = (TR[0], TR[1]-2*radius)
|
||
|
|
||
|
if not right:
|
||
|
TR = (TR[0]-2*radius, TR[1])
|
||
|
BR = (BR[0]-2*radius, BR[1])
|
||
|
|
||
|
if not bot:
|
||
|
BL = (BL[0], BL[1]+2*radius)
|
||
|
BR = (BR[0], BR[1]+2*radius)
|
||
|
|
||
|
BL = (util.in2gerb(BL[0]), util.in2gerb(BL[1]))
|
||
|
TL = (util.in2gerb(TL[0]), util.in2gerb(TL[1]))
|
||
|
TR = (util.in2gerb(TR[0]), util.in2gerb(TR[1]))
|
||
|
BR = (util.in2gerb(BR[0]), util.in2gerb(BR[1]))
|
||
|
|
||
|
# The "if 1 or ..." construct draws all four sides of the job. By
|
||
|
# removing the 1 from the expression, only the sides that do not
|
||
|
# correspond to panel edges are drawn. The former is probably better
|
||
|
# since panels tend to have a little slop from the cutting operation
|
||
|
# and it's easier to just cut it smaller when there's a cut line.
|
||
|
# The way it is now with "if 1 or....", much of this function is
|
||
|
# unnecessary. Heck, we could even just use the boardoutline layer
|
||
|
# directly.
|
||
|
if 1 or left:
|
||
|
fid.write('X%07dY%07dD02*\n' % BL)
|
||
|
fid.write('X%07dY%07dD01*\n' % TL)
|
||
|
|
||
|
if 1 or top:
|
||
|
if not left: fid.write('X%07dY%07dD02*\n' % TL)
|
||
|
fid.write('X%07dY%07dD01*\n' % TR)
|
||
|
|
||
|
if 1 or right:
|
||
|
if not top: fid.write('X%07dY%07dD02*\n' % TR)
|
||
|
fid.write('X%07dY%07dD01*\n' % BR)
|
||
|
|
||
|
if 1 or bot:
|
||
|
if not right: fid.write('X%07dY%07dD02*\n' % BR)
|
||
|
fid.write('X%07dY%07dD01*\n' % BL)
|
||
|
|
||
|
def setPosition(self, x, y):
|
||
|
self.x=x
|
||
|
self.y=y
|
||
|
|
||
|
def width_in(self):
|
||
|
return self.job.width_in()
|
||
|
|
||
|
def height_in(self):
|
||
|
return self.job.height_in()
|
||
|
|
||
|
def drillhits(self, diameter):
|
||
|
tools = self.job.findTools(diameter)
|
||
|
total = 0
|
||
|
for tool in tools:
|
||
|
try:
|
||
|
total += len(self.job.xcommands[tool])
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
return total
|
||
|
|
||
|
def jobarea(self):
|
||
|
return self.job.jobarea()
|
||
|
|
||
|
def rotateJob(job, degrees = 90, firstpass = True):
|
||
|
"""Create a new job from an existing one, rotating by specified degrees in 90 degree passes"""
|
||
|
GAT = config.GAT
|
||
|
GAMT = config.GAMT
|
||
|
##print "rotating job:", job.name, degrees, firstpass
|
||
|
if firstpass:
|
||
|
if degrees == 270:
|
||
|
J = Job(job.name+'*rotated270')
|
||
|
elif degrees == 180:
|
||
|
J = Job(job.name+'*rotated180')
|
||
|
else:
|
||
|
J = Job(job.name+'*rotated90')
|
||
|
else:
|
||
|
J = Job(job.name)
|
||
|
|
||
|
# Keep the origin (lower-left) in the same place
|
||
|
J.maxx = job.minx + job.maxy-job.miny
|
||
|
J.maxy = job.miny + job.maxx-job.minx
|
||
|
J.minx = job.minx
|
||
|
J.miny = job.miny
|
||
|
|
||
|
RevGAT = config.buildRevDict(GAT) # RevGAT[hash] = aperturename
|
||
|
RevGAMT = config.buildRevDict(GAMT) # RevGAMT[hash] = aperturemacroname
|
||
|
|
||
|
# Keep list of tool diameters and default tool list
|
||
|
J.xdiam = job.xdiam
|
||
|
J.ToolList = job.ToolList
|
||
|
J.Repeat = job.Repeat
|
||
|
|
||
|
# D-code translation table is the same, except we have to rotate
|
||
|
# those apertures which have an orientation: rectangles, ovals, and macros.
|
||
|
|
||
|
ToolChangeReplace = {}
|
||
|
for layername in job.apxlat.keys():
|
||
|
J.apxlat[layername] = {}
|
||
|
|
||
|
for ap in job.apxlat[layername].keys():
|
||
|
code = job.apxlat[layername][ap]
|
||
|
A = GAT[code]
|
||
|
|
||
|
if A.apname in ('Circle', 'Octagon'):
|
||
|
# This aperture is fine. Copy it over.
|
||
|
J.apxlat[layername][ap] = code
|
||
|
continue
|
||
|
|
||
|
# Must rotate the aperture
|
||
|
APR = A.rotated(RevGAMT)
|
||
|
|
||
|
# Does it already exist in the GAT?
|
||
|
hash = APR.hash()
|
||
|
try:
|
||
|
# Yup...add it to apxlat
|
||
|
newcode = RevGAT[hash]
|
||
|
except KeyError:
|
||
|
# Must add new aperture to GAT
|
||
|
newcode = aptable.addToApertureTable(APR)
|
||
|
|
||
|
# Rebuild RevGAT
|
||
|
#RevGAT = config.buildRevDict(GAT)
|
||
|
RevGAT[hash] = newcode
|
||
|
|
||
|
J.apxlat[layername][ap] = newcode
|
||
|
|
||
|
# Must also replace all tool change commands from
|
||
|
# old code to new command.
|
||
|
ToolChangeReplace[code] = newcode
|
||
|
|
||
|
# Now we copy commands, rotating X,Y positions.
|
||
|
# Rotations will occur counterclockwise about the
|
||
|
# point (minx,miny). Then, we shift to the right
|
||
|
# by the height so that the lower-left point of
|
||
|
# the rotated job continues to be (minx,miny).
|
||
|
#
|
||
|
# We also have to take aperture change commands and
|
||
|
# replace them with the new aperture code if we have
|
||
|
# a rotation.
|
||
|
offset = job.maxy-job.miny
|
||
|
for layername in job.commands.keys():
|
||
|
J.commands[layername] = []
|
||
|
J.apertures[layername] = []
|
||
|
|
||
|
for cmd in job.commands[layername]:
|
||
|
# Is it a drawing command?
|
||
|
if type(cmd) is types.TupleType:
|
||
|
if len(cmd)==3:
|
||
|
x, y, d = map(__builtin__.int, cmd)
|
||
|
II=JJ=None
|
||
|
else:
|
||
|
x, y, II, JJ, d, signed = map(__builtin__.int, cmd) # J is already used as Job object
|
||
|
else:
|
||
|
# No, must be a string indicating aperture change, G-code, or RS274-X command.
|
||
|
if cmd[0] in ('G', '%'):
|
||
|
# G-codes and RS274-X commands are just copied verbatim and not affected by rotation
|
||
|
J.commands[layername].append(cmd)
|
||
|
continue
|
||
|
|
||
|
# It's a D-code. See if we need to replace aperture changes with a rotated aperture.
|
||
|
# But only for D-codes >= 10.
|
||
|
if int(cmd[1:]) < 10:
|
||
|
J.commands[layername].append(cmd)
|
||
|
continue
|
||
|
|
||
|
try:
|
||
|
newcmd = ToolChangeReplace[cmd]
|
||
|
J.commands[layername].append(newcmd)
|
||
|
J.apertures[layername].append(newcmd)
|
||
|
except KeyError:
|
||
|
J.commands[layername].append(cmd)
|
||
|
J.apertures[layername].append(cmd)
|
||
|
continue
|
||
|
|
||
|
# (X,Y) --> (-Y,X) effects a 90-degree counterclockwise shift
|
||
|
# Adding 'offset' to -Y maintains the lower-left origin of (minx,miny).
|
||
|
newx = -(y - job.miny) + job.minx + offset
|
||
|
newy = (x-job.minx) + job.miny
|
||
|
|
||
|
# For circular interpolation commands, (I,J) components are always relative
|
||
|
# so we do not worry about offsets, just reverse their sense, i.e., I becomes J
|
||
|
# and J becomes I. For 360-degree circular interpolation, I/J are signed and we
|
||
|
# must map (I,J) --> (-J,I).
|
||
|
if II is not None:
|
||
|
if signed:
|
||
|
J.commands[layername].append((newx, newy, -JJ, II, d, signed))
|
||
|
else:
|
||
|
J.commands[layername].append((newx, newy, JJ, II, d, signed))
|
||
|
else:
|
||
|
J.commands[layername].append((newx,newy,d))
|
||
|
|
||
|
if 0:
|
||
|
print job.minx, job.miny, offset
|
||
|
print layername
|
||
|
print J.commands[layername]
|
||
|
|
||
|
# Finally, rotate drills. Offset is in hundred-thousandths (2.5) while Excellon
|
||
|
# data is in 2.4 format.
|
||
|
for tool in job.xcommands.keys():
|
||
|
J.xcommands[tool] = []
|
||
|
|
||
|
for x,y in job.xcommands[tool]:
|
||
|
newx = -(10*y - job.miny) + job.minx + offset
|
||
|
newy = (10*x - job.minx) + job.miny
|
||
|
|
||
|
newx = int(round(newx/10.0))
|
||
|
newy = int(round(newy/10.0))
|
||
|
|
||
|
J.xcommands[tool].append((newx,newy))
|
||
|
|
||
|
# Rotate some more if required
|
||
|
degrees -= 90
|
||
|
if degrees > 0:
|
||
|
return rotateJob(J, degrees, False)
|
||
|
else:
|
||
|
##print "rotated:", J.name
|
||
|
return J
|