aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerberex/excellon.py242
-rw-r--r--tests/data/ref_drill_inch.txt10
-rw-r--r--tests/data/ref_drill_metric.txt24
-rw-r--r--tests/expects/excellon_offset.txt8
-rw-r--r--tests/expects/excellon_rotate.txt8
-rw-r--r--tests/expects/excellon_save.txt8
-rw-r--r--tests/expects/excellon_to_inch.txt10
-rw-r--r--tests/expects/excellon_to_metric.txt8
8 files changed, 213 insertions, 105 deletions
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index 04d10e9..7014a6e 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -5,8 +5,10 @@
import operator
+import gerber.excellon
from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot
-from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \
+from gerber.excellon_statements import ExcellonStatement, UnitStmt, CoordinateStmt, UnknownStmt, \
+ SlotStmt, DrillModeStmt, RouteModeStmt, LinearModeStmt, \
ToolSelectionStmt, ZAxisRoutPositionStmt, \
RetractWithClampingStmt, RetractWithoutClampingStmt, \
EndOfProgramStmt
@@ -19,6 +21,8 @@ def loads(data, filename=None, settings=None, tools=None, format=None):
settings = FileSettings(**detect_excellon_format(data))
if format:
settings.format = format
+ gerber.excellon.CoordinateStmt = CoordinateStmtEx
+ gerber.excellon.UnitStmt = UnitStmtEx
file = ExcellonParser(settings, tools).parse_raw(data, filename)
return ExcellonFileEx.from_file(file)
@@ -26,49 +30,78 @@ class ExcellonFileEx(ExcellonFile):
@classmethod
def from_file(cls, file):
def correct_statements():
- lazy_stmt = None
- modifier = None
for stmt in file.statements:
- modifier = lazy_stmt
- lazy_stmt = None
- if isinstance(stmt, UnitStmt):
- yield UnitStmtEx.from_statement(stmt)
- elif isinstance(stmt, CoordinateStmt):
- new_stmt = CoordinateStmtEx.from_statement(stmt)
- if modifier and new_stmt.mode is None:
- new_stmt.mode = modifier
- yield new_stmt
- elif isinstance(stmt, UnknownStmt):
+ if isinstance(stmt, UnknownStmt):
line = stmt.stmt.strip()
- mode = None
if line[:3] == 'G02':
- mode = CoordinateStmtEx.MODE_CIRCULER_CW
+ yield CircularCWModeStmt()
+ if len(line) > 3:
+ yield CoordinateStmtEx.from_excellon(line[3:], file.settings)
elif line[:3] == 'G03':
- mode = CoordinateStmtEx.MODE_CIRCULER_CCW
+ yield CircularCCWModeStmt()
+ if len(line) > 3:
+ yield CoordinateStmtEx.from_excellon(line[3:], file.settings)
+ elif line[0] == 'X' or line[0] == 'Y' or line[0] == 'A' or line[0] == 'I':
+ yield CoordinateStmtEx.from_excellon(line, file.settings)
else:
yield stmt
- continue
- if len(line) == 3:
- lazy_stmt = mode
- continue
- new_stmt = CoordinateStmtEx.from_excellon(line[3:], file.settings)
- new_stmt.mode = mode
- yield new_stmt
else:
yield stmt
def generate_hits(statements):
+ class CoordinateCtx:
+ def __init__(self, notation):
+ self.notation = notation
+ self.x = 0.
+ self.y = 0.
+ self.radius = None
+ self.center_offset = None
+
+ def update(self, x=None, y=None, radius=None, center_offset=None):
+ if self.notation == 'absolute':
+ if x is not None:
+ self.x = x
+ if y is not None:
+ self.y = y
+ else:
+ if x is not None:
+ self.x += x
+ if y is not None:
+ self.y += y
+ if radius is not None:
+ self.radius = radius
+ if center_offset is not None:
+ self.center_offset = center_offset
+
+ def node(self, mode, center_offset):
+ radius, offset = None, None
+ if mode == DrillRout.MODE_CIRCULER_CW or mode == DrillRout.MODE_CIRCULER_CCW:
+ if center_offset is None:
+ radius = self.radius
+ offset = self.center_offset
+ else:
+ radius = None
+ offset = center_offset
+ return DrillRout.Node(mode, self.x, self.y, radius, offset)
+
STAT_DRILL = 0
STAT_ROUT_UP = 1
STAT_ROUT_DOWN = 2
+
status = STAT_DRILL
current_tool = None
- rout_statements = []
+ rout_mode = None
+ coordinate_ctx = CoordinateCtx(file.notation)
+ rout_nodes = []
+
+ last_position = (0., 0.)
+ last_radius = None
+ last_center_offset = None
- def make_rout(status, statements):
- if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None:
+ def make_rout(status, nodes):
+ if status != STAT_ROUT_DOWN or len(nodes) == 0 or current_tool is None:
return None
- return DrillRout.from_coordinates(current_tool, statements)
+ return DrillRout(current_tool, nodes)
for stmt in statements:
if isinstance(stmt, ToolSelectionStmt):
@@ -79,31 +112,48 @@ class ExcellonFileEx(ExcellonFile):
if rout is not None:
yield rout
status = STAT_DRILL
+ rout_mode = None
+ elif isinstance(stmt, RouteModeStmt):
+ if status == STAT_DRILL:
+ status = STAT_ROUT_UP
+ rout_mode = DrillRout.MODE_ROUT
+ else:
+ rout_mode = DrillRout.MODE_LINEAR
+
+ elif isinstance(stmt, LinearModeStmt):
+ rout_mode = DrillRout.MODE_LINEAR
+ elif isinstance(stmt, CircularCWModeStmt):
+ rout_mode = DrillRout.MODE_CIRCULER_CW
+ elif isinstance(stmt, CircularCCWModeStmt):
+ rout_mode = DrillRout.MODE_CIRCULER_CCW
elif isinstance(stmt, ZAxisRoutPositionStmt) and status == STAT_ROUT_UP:
status = STAT_ROUT_DOWN
elif isinstance(stmt, RetractWithClampingStmt) or isinstance(stmt, RetractWithoutClampingStmt):
- rout = make_rout(status, rout_statements)
+ rout = make_rout(status, rout_nodes)
rout_statements = []
if rout is not None:
yield rout
status = STAT_ROUT_UP
elif isinstance(stmt, SlotStmt):
- yield DrillSlotEx(current_tool, (stmt.x_start, stmt.y_start),
- (stmt.x_end, stmt.y_end), DrillSlotEx.TYPE_G85)
+ coordinate_ctx.update(stmt.x_start, stmt.y_start)
+ x_start = coordinate_ctx.x
+ y_start = coordinate_ctx.y
+ coordinate_ctx.update(stmt.x_end, stmt.y_end)
+ x_end = coordinate_ctx.x
+ y_end = coordinate_ctx.y
+ yield DrillSlotEx(current_tool, (x_start, y_start),
+ (x_end, y_end), DrillSlotEx.TYPE_G85)
elif isinstance(stmt, CoordinateStmtEx):
- if stmt.mode is None:
- if status != STAT_DRILL:
- raise Exception('invalid statement sequence')
- yield DrillHitEx(current_tool, (stmt.x, stmt.y))
- else:
- if stmt.mode == stmt.MODE_ROUT:
- status = STAT_ROUT_UP
- if status == STAT_ROUT_UP:
- rout_statements = [stmt]
+ center_offset = (stmt.i, stmt.j) \
+ if stmt.i is not None and stmt.j is not None else None
+ coordinate_ctx.update(stmt.x, stmt.y, stmt.radius, center_offset)
+ if stmt.x is not None or stmt.y is not None:
+ if status == STAT_DRILL:
+ yield DrillHitEx(current_tool, (coordinate_ctx.x, coordinate_ctx.y))
+ elif status == STAT_ROUT_UP:
+ rout_nodes = [coordinate_ctx.node(DrillRout.MODE_ROUT, None)]
elif status == STAT_ROUT_DOWN:
- rout_statements.append(stmt)
- else:
- raise Exception('invalid statement sequence')
+ rout_nodes.append(coordinate_ctx.node(rout_mode, center_offset))
statements = [s for s in correct_statements()]
hits = [h for h in generate_hits(statements)]
@@ -188,28 +238,33 @@ class DrillSlotEx(DrillSlot):
return SlotStmt(*self.start, *self.end).to_excellon(settings)
class DrillRout(object):
+ MODE_ROUT = 'G00'
+ MODE_LINEAR = 'G01'
+ MODE_CIRCULER_CW = 'G02'
+ MODE_CIRCULER_CCW = 'G03'
+
class Node(object):
- def __init__(self, mode, x, y, radius):
+ def __init__(self, mode, x, y, radius=None, center_offset=None):
self.mode = mode
self.position = (x, y)
self.radius = radius
+ self.center_offset = center_offset
- @classmethod
- def from_coordinates(cls, tool, coordinates):
- nodes = [cls.Node(c.mode, c.x, c.y, c.radius) for c in coordinates]
- return cls(tool, nodes)
+ def to_excellon(self, settings):
+ center_offset = self.center_offset \
+ if self.center_offset is not None else (None, None)
+ return self.mode + CoordinateStmtEx(
+ *self.position, self.radius, *center_offset).to_excellon(settings)
def __init__(self, tool, nodes):
self.tool = tool
self.nodes = nodes
+ self.nodes[0].mode = self.MODE_ROUT
def to_excellon(self, settings):
- node = self.nodes[0]
- excellon = CoordinateStmtEx(*node.position, node.radius,
- CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n'
+ excellon = self.nodes[0].to_excellon(settings) + '\nM15\n'
for node in self.nodes[1:]:
- excellon += CoordinateStmtEx(*node.position, node.radius,
- node.mode).to_excellon(settings) + '\n'
+ excellon += node.to_excellon(settings) + '\n'
excellon += 'M16\nG05'
return excellon
@@ -218,12 +273,16 @@ class DrillRout(object):
node.position = tuple(map(inch, node.position))
node.radius = inch(
node.radius) if node.radius is not None else None
+ if node.center_offset is not None:
+ node.center_offset = tuple(map(inch, node.center_offset))
def to_metric(self):
for node in self.nodes:
node.position = tuple(map(metric, node.position))
node.radius = metric(
node.radius) if node.radius is not None else None
+ if node.center_offset is not None:
+ node.center_offset = tuple(map(metric, node.center_offset))
def offset(self, x_offset=0, y_offset=0):
for node in self.nodes:
@@ -232,6 +291,8 @@ class DrillRout(object):
def rotate(self, angle, center=(0, 0)):
for node in self.nodes:
node.position = rotate(*node.position, angle, center)
+ if node.center_offset is not None:
+ node.center_offset = rotate(*node.center_offset, angle, (0., 0.))
class UnitStmtEx(UnitStmt):
@classmethod
@@ -243,48 +304,69 @@ class UnitStmtEx(UnitStmt):
def to_excellon(self, settings=None):
format = settings.format if settings else self.format
- stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
- 'LZ' if self.zeros == 'leading' else 'TZ',
- '0' * format[0], '0' * format[1])
+ stmt = None
+ if self.units == 'inch' and format == (2, 4):
+ stmt = 'INCH,%s' % ('LZ' if self.zeros == 'leading' else 'TZ')
+ else:
+ stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
+ 'LZ' if self.zeros == 'leading' else 'TZ',
+ '0' * format[0], '0' * format[1])
return stmt
-class CoordinateStmtEx(CoordinateStmt):
- MODE_ROUT = 'ROUT'
- MODE_LINEAR = 'LINEAR'
- MODE_CIRCULER_CW = 'CW'
- MODE_CIRCULER_CCW = 'CCW'
+class CircularCWModeStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(CircularCWModeStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G02'
+class CircularCCWModeStmt(ExcellonStatement):
+
+ def __init__(self, **kwargs):
+ super(CircularCCWModeStmt, self).__init__(**kwargs)
+
+ def to_excellon(self, settings=None):
+ return 'G02'
+
+class CoordinateStmtEx(CoordinateStmt):
@classmethod
def from_statement(cls, stmt):
newStmt = cls(x=stmt.x, y=stmt.y)
- newStmt.mode = stmt.mode
newStmt.radius = stmt.radius if isinstance(stmt, CoordinateStmtEx) else None
return newStmt
@classmethod
def from_excellon(cls, line, settings, **kwargs):
- parts = line.split('A')
- stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings))
- if len(parts) > 1:
+ stmt = None
+ if 'A' in line:
+ parts = line.split('A')
+ stmt = cls.from_statement(CoordinateStmt.from_excellon(parts[0], settings)) \
+ if parts[0] != '' else cls()
stmt.radius = parse_gerber_value(
parts[1], settings.format, settings.zero_suppression)
+ elif 'I' in line:
+ jparts = line.split('J')
+ iparts = jparts[0].split('I')
+ stmt = cls.from_statement(CoordinateStmt.from_excellon(iparts[0], settings)) \
+ if iparts[0] != '' else cls()
+ stmt.i = parse_gerber_value(
+ iparts[1], settings.format, settings.zero_suppression)
+ stmt.j = parse_gerber_value(
+ jparts[1], settings.format, settings.zero_suppression)
+ else:
+ stmt = cls.from_statement(CoordinateStmt.from_excellon(line, settings))
+
return stmt
- def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs):
+ def __init__(self, x=None, y=None, radius=None, i=None, j=None, **kwargs):
super(CoordinateStmtEx, self).__init__(x, y, **kwargs)
- self.mode = mode
self.radius = radius
+ self.i = i
+ self.j = j
def to_excellon(self, settings):
stmt = ''
- if self.mode == self.MODE_ROUT:
- stmt += "G00"
- if self.mode == self.MODE_LINEAR:
- stmt += "G01"
- if self.mode == self.MODE_CIRCULER_CW:
- stmt += "G02"
- if self.mode == self.MODE_CIRCULER_CCW:
- stmt += "G03"
if self.x is not None:
stmt += 'X%s' % write_gerber_value(self.x, settings.format,
settings.zero_suppression)
@@ -294,6 +376,11 @@ class CoordinateStmtEx(CoordinateStmt):
if self.radius is not None:
stmt += 'A%s' % write_gerber_value(self.radius, settings.format,
settings.zero_suppression)
+ elif self.i is not None and self.j is not None:
+ stmt += 'I%sJ%s' % (write_gerber_value(self.i, settings.format,
+ settings.zero_suppression),
+ write_gerber_value(self.j, settings.format,
+ settings.zero_suppression))
return stmt
def __str__(self):
@@ -304,6 +391,9 @@ class CoordinateStmtEx(CoordinateStmt):
coord_str += 'Y: %g ' % self.y
if self.radius is not None:
coord_str += 'A: %g ' % self.radius
+ if self.i is not None:
+ coord_str += 'I: %g ' % self.i
+ if self.j is not None:
+ coord_str += 'J: %g ' % self.j
- return '<Coordinate Statement: %s(%s)>' % \
- (coord_str, self.mode if self.mode else 'HIT')
+ return '<Coordinate Statement: %s>' % (coord_str)
diff --git a/tests/data/ref_drill_inch.txt b/tests/data/ref_drill_inch.txt
index 6aea2e0..6af5494 100644
--- a/tests/data/ref_drill_inch.txt
+++ b/tests/data/ref_drill_inch.txt
@@ -1,7 +1,7 @@
M48
FMAT,2
ICI,OFF
-INCH,TZ,00.0000
+INCH,TZ
T01C0.0236
T02C0.0275
T03C0.0314
@@ -28,10 +28,12 @@ X1417Y2795
T04
G00X236Y669
M15
-G03X630Y276A394
+G02X630Y276I0J-394
+G03X1024Y-118A394
+G03X1417Y276A394
G01X2598Y276
-G01X2598Y1614
-G03X2205Y2008A394
+G01X2598Y1024
+G03X1614Y2008I-984J0
G01X236Y2008
G01X236Y669
M16
diff --git a/tests/data/ref_drill_metric.txt b/tests/data/ref_drill_metric.txt
index 903fc48..3a50856 100644
--- a/tests/data/ref_drill_metric.txt
+++ b/tests/data/ref_drill_metric.txt
@@ -24,18 +24,24 @@ M15
G01X2140Y2925
M16
G05
-
X3600Y7100
T04
-G00X600Y1700
+G00
+X600Y1700
M15
-G03X1600Y700A1000
-G01X6600Y700
-G01X6600Y4100
-G03X5600Y5100A1000
-G01X600Y5100
-G01X600Y1700
+I0J-1000
+G02X1600Y700
+G03
+A1000
+I-2000J0
+X2600Y-300
+X3600Y700
+G01X6600
+Y2600
+G03X4100Y5100I-2500J0
+G01
+X600
+Y1700
M16
G05
-
M30
diff --git a/tests/expects/excellon_offset.txt b/tests/expects/excellon_offset.txt
index 41dbf3e..9a0b430 100644
--- a/tests/expects/excellon_offset.txt
+++ b/tests/expects/excellon_offset.txt
@@ -28,10 +28,12 @@ X14600Y12100
T04
G00X11600Y6700
M15
-G03X12600Y5700A1000
+G02X12600Y5700I0J-1000
+G03X13600Y4700A1000
+G03X14600Y5700A1000
G01X17600Y5700
-G01X17600Y9100
-G03X16600Y10100A1000
+G01X17600Y7600
+G03X15100Y10100I-2500J0
G01X11600Y10100
G01X11600Y6700
M16
diff --git a/tests/expects/excellon_rotate.txt b/tests/expects/excellon_rotate.txt
index 9983909..f33dcfd 100644
--- a/tests/expects/excellon_rotate.txt
+++ b/tests/expects/excellon_rotate.txt
@@ -28,10 +28,12 @@ X4978Y5086
T04
G00X4006Y-1014
M15
-G03X5287Y-1612A1000
+G02X5287Y-1612I342J-940
+G03X6569Y-2210A1000
+G03X7167Y-928A1000
G01X9986Y98
-G01X8823Y3293
-G03X7541Y3891A1000
+G01X9336Y1883
+G03X6132Y3378I-2349J-855
G01X2843Y2181
G01X4006Y-1014
M16
diff --git a/tests/expects/excellon_save.txt b/tests/expects/excellon_save.txt
index 6a22a7d..18fdcc4 100644
--- a/tests/expects/excellon_save.txt
+++ b/tests/expects/excellon_save.txt
@@ -28,10 +28,12 @@ X3600Y7100
T04
G00X600Y1700
M15
-G03X1600Y700A1000
+G02X1600Y700I0J-1000
+G03X2600Y-300A1000
+G03X3600Y700A1000
G01X6600Y700
-G01X6600Y4100
-G03X5600Y5100A1000
+G01X6600Y2600
+G03X4100Y5100I-2500J0
G01X600Y5100
G01X600Y1700
M16
diff --git a/tests/expects/excellon_to_inch.txt b/tests/expects/excellon_to_inch.txt
index 6aea2e0..6af5494 100644
--- a/tests/expects/excellon_to_inch.txt
+++ b/tests/expects/excellon_to_inch.txt
@@ -1,7 +1,7 @@
M48
FMAT,2
ICI,OFF
-INCH,TZ,00.0000
+INCH,TZ
T01C0.0236
T02C0.0275
T03C0.0314
@@ -28,10 +28,12 @@ X1417Y2795
T04
G00X236Y669
M15
-G03X630Y276A394
+G02X630Y276I0J-394
+G03X1024Y-118A394
+G03X1417Y276A394
G01X2598Y276
-G01X2598Y1614
-G03X2205Y2008A394
+G01X2598Y1024
+G03X1614Y2008I-984J0
G01X236Y2008
G01X236Y669
M16
diff --git a/tests/expects/excellon_to_metric.txt b/tests/expects/excellon_to_metric.txt
index 1bb4287..20f698e 100644
--- a/tests/expects/excellon_to_metric.txt
+++ b/tests/expects/excellon_to_metric.txt
@@ -28,10 +28,12 @@ X3599Y7099
T04
G00X599Y1699
M15
-G03X1600Y701A1001
+G02X1600Y701I0J-1001
+G03X2601Y-300A1001
+G03X3599Y701A1001
G01X6599Y701
-G01X6599Y4100
-G03X5601Y5100A1001
+G01X6599Y2601
+G03X4100Y5100I-2499J0
G01X599Y5100
G01X599Y1699
M16