aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--gerberex/composition.py5
-rw-r--r--gerberex/excellon.py251
3 files changed, 249 insertions, 15 deletions
diff --git a/README.md b/README.md
index 0429e54..d688edf 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,18 @@ pcb-tools-extension adds following function to pcb-tools.
Only RS-274x format and Excellon drill format data can be handled by current version of this library.
## Installation
+You can install a stable version by following step.
+
```shell
$ pip install pcb-tools-extension
```
+If you have a intention to try latest developing version, please install as follows.
+
+```shell
+$ pip install git+https://github.com/opiopan/pcb-tools-extension.git
+```
+
## How to panelize
Following code is a example to panelize two top metal layer files.
diff --git a/gerberex/composition.py b/gerberex/composition.py
index 29725ba..7f691f5 100644
--- a/gerberex/composition.py
+++ b/gerberex/composition.py
@@ -150,10 +150,7 @@ class DrillComposition(Composition):
yield ToolSelectionStmt(t.number).to_excellon(self.settings)
for h in self.hits:
if h.tool.number == t.number:
- if type(h) == DrillSlot:
- yield SlotStmt(*h.start, *h.end).to_excellon(self.settings)
- elif type(h) == DrillHit:
- yield CoordinateStmt(*h.position).to_excellon(self.settings)
+ yield h.to_excellon(self.settings)
for num, statement in self.dxf_statements:
if num == t.number:
yield statement.to_excellon(self.settings)
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
index b72b95b..657f02a 100644
--- a/gerberex/excellon.py
+++ b/gerberex/excellon.py
@@ -3,10 +3,15 @@
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
-from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile)
-from gerber.excellon_statements import UnitStmt
+import operator
+
+from gerber.excellon import ExcellonParser, detect_excellon_format, ExcellonFile, DrillHit, DrillSlot
+from gerber.excellon_statements import UnitStmt, CoordinateStmt, UnknownStmt, SlotStmt, DrillModeStmt, \
+ ToolSelectionStmt, ZAxisRoutPositionStmt, \
+ RetractWithClampingStmt, RetractWithoutClampingStmt, \
+ EndOfProgramStmt
from gerber.cam import FileSettings
-from gerber.utils import inch, metric
+from gerber.utils import inch, metric, write_gerber_value, parse_gerber_value
from gerberex.utility import rotate
def loads(data, filename=None, settings=None, tools=None, format=None):
@@ -20,11 +25,93 @@ def loads(data, filename=None, settings=None, tools=None, format=None):
class ExcellonFileEx(ExcellonFile):
@classmethod
def from_file(cls, file):
- statements = [
- UnitStmtEx.from_statement(s) if isinstance(s, UnitStmt) else s \
- for s in file.statements
- ]
- return cls(statements, file.tools, file.hits, file.settings, file.filename)
+ 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):
+ line = stmt.stmt.strip()
+ mode = None
+ if line[:3] == 'G02':
+ mode = CoordinateStmtEx.MODE_CIRCULER_CW
+ elif line[:3] == 'G03':
+ mode = CoordinateStmtEx.MODE_CIRCULER_CCW
+ 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):
+ STAT_DRILL = 0
+ STAT_ROUT_UP = 1
+ STAT_ROUT_DOWN = 2
+ status = STAT_DRILL
+ current_tool = None
+ rout_statements = []
+
+ def make_rout(status, statements):
+ if status != STAT_ROUT_DOWN or len(statements) == 0 or current_tool is None:
+ return None
+ return DrillRout.from_coordinates(current_tool, statements)
+
+ for stmt in statements:
+ if isinstance(stmt, ToolSelectionStmt):
+ current_tool = file.tools[stmt.tool]
+ elif isinstance(stmt, DrillModeStmt):
+ rout = make_rout(status, rout_statements)
+ rout_statements = []
+ if rout is not None:
+ yield rout
+ status = STAT_DRILL
+ 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_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)
+ 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]
+ elif status == STAT_ROUT_DOWN:
+ rout_statements.append(stmt)
+ else:
+ raise Exception('invalid statement sequence')
+
+ statements = [s for s in correct_statements()]
+ hits = [h for h in generate_hits(statements)]
+ return cls(statements, file.tools, hits, file.settings, file.filename)
+
+ @property
+ def primitives(self):
+ return []
def __init__(self, statements, tools, hits, settings, filename=None):
super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename)
@@ -33,20 +120,102 @@ class ExcellonFileEx(ExcellonFile):
if angle % 360 == 0:
return
for hit in self.hits:
- hit.position = rotate(hit.position[0], hit.position[1], angle, center)
+ hit.rotate(angle, center)
def to_inch(self):
if self.units == 'metric':
super(ExcellonFileEx, self).to_inch()
for hit in self.hits:
- hit.position = (inch(hit.position[0]), inch(hit.position[1]))
+ hit.to_inch()
def to_metric(self):
if self.units == 'inch':
super(ExcellonFileEx, self).to_metric()
for hit in self.hits:
- hit.position = (metric(hit.position[0]), metric(hit.position[1]))
+ hit.to_metric()
+
+ def write(self, filename=None):
+ filename = filename if filename is not None else self.filename
+ with open(filename, 'w') as f:
+
+ for statement in self.statements:
+ if not isinstance(statement, ToolSelectionStmt):
+ f.write(statement.to_excellon(self.settings) + '\n')
+ else:
+ break
+
+ for tool in iter(self.tools.values()):
+ f.write(ToolSelectionStmt(
+ tool.number).to_excellon(self.settings) + '\n')
+ for hit in self.hits:
+ if hit.tool.number == tool.number:
+ f.write(hit.to_excellon(self.settings) + '\n')
+ f.write(EndOfProgramStmt().to_excellon() + '\n')
+
+class DrillHitEx(DrillHit):
+ def rotate(self, angle, center=(0,0)):
+ self.position = rotate(*self.position, angle, center)
+
+ def to_excellon(self, settings):
+ return CoordinateStmtEx(*self.position).to_excellon(settings)
+
+class DrillSlotEx(DrillSlot):
+ def rotate(self, angle, center=(0,0)):
+ self.start = rotate(*self.start, angle, center)
+ self.end = rotate(*self.end, angle, center)
+
+ def to_excellon(self, settings):
+ return SlotStmt(*self.start, *self.end).to_excellon(settings)
+
+class DrillRout(object):
+ class Node(object):
+ def __init__(self, mode, x, y, radius):
+ self.mode = mode
+ self.position = (x, y)
+ self.radius = radius
+
+ @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 __init__(self, tool, nodes):
+ self.tool = tool
+ self.nodes = nodes
+ def to_excellon(self, settings):
+ node = self.nodes[0]
+ excellon = CoordinateStmtEx(*node.position, node.radius,
+ CoordinateStmtEx.MODE_ROUT).to_excellon(settings) + '\nM15\n'
+ for node in self.nodes[1:]:
+ excellon += CoordinateStmtEx(*node.position, node.radius,
+ node.mode).to_excellon(settings) + '\n'
+ excellon += 'M16\nG05\n'
+ return excellon
+
+ def to_inch(self):
+ if self.tool.settings.units == 'metric':
+ self.tool.to_inch()
+ for node in self.nodes:
+ node.position = tuple(map(inch, node.position))
+ node.radius = inch(
+ node.radius) if node.radius is not None else None
+
+ def to_metric(self):
+ if self.tool.settings.units == 'inch':
+ self.tool.to_metric()
+ 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
+
+ def offset(self, x_offset=0, y_offset=0):
+ for node in self.nodes:
+ node.position = tuple(map(operator.add, node.position, (x_offset, y_offset)))
+
+ def rotate(self, angle, center=(0, 0)):
+ for node in self.nodes:
+ node.position = rotate(*node.position, angle, center)
class UnitStmtEx(UnitStmt):
@classmethod
@@ -62,3 +231,63 @@ class UnitStmtEx(UnitStmt):
'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'
+
+ @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.radius = parse_gerber_value(
+ parts[1], settings.format, settings.zero_suppression)
+ return stmt
+
+ def __init__(self, x=None, y=None, radius=None, mode=None, **kwargs):
+ super(CoordinateStmtEx, self).__init__(x, y, **kwargs)
+ self.mode = mode
+ self.radius = radius
+
+ 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)
+ if self.y is not None:
+ stmt += 'Y%s' % write_gerber_value(self.y, settings.format,
+ settings.zero_suppression)
+ if self.radius is not None:
+ stmt += 'A%s' % write_gerber_value(self.radius, settings.format,
+ settings.zero_suppression)
+ return stmt
+
+ def __str__(self):
+ coord_str = ''
+ if self.x is not None:
+ coord_str += 'X: %g ' % self.x
+ if self.y is not None:
+ coord_str += 'Y: %g ' % self.y
+ if self.radius is not None:
+ coord_str += 'A: %g ' % self.radius
+
+ return '<Coordinate Statement: %s(%s)>' % \
+ (coord_str, self.mode if self.mode else 'HIT')