Merge pull request #31 from curtacircuitos/statement_link

Add support for tool reordering and drill path optimization example.
This commit is contained in:
Paulo Henrique Silva 2015-07-14 13:32:17 -03:00
commit c92d2d9ea2
9 changed files with 647 additions and 83 deletions

View file

@ -0,0 +1,90 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Example using pcb-tools with tsp-solver (github.com/dmishin/tsp-solver) to
# optimize tool paths in an Excellon file.
#
#
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
# Based on a script by https://github.com/koppi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
import sys
import math
import gerber
from operator import sub
from gerber.excellon import DrillHit
try:
from tsp_solver.greedy import solve_tsp
except ImportError:
print('\n=================================================================\n'
'This example requires tsp-solver be installed in order to run.\n\n'
'tsp-solver can be downloaded from:\n'
' http://github.com/dmishin/tsp-solver.\n'
'=================================================================')
sys.exit(0)
if __name__ == '__main__':
# Get file name to open
if len(sys.argv) < 2:
fname = 'gerbers/shld.drd'
else:
fname = sys.argv[1]
# Read the excellon file
f = gerber.read(fname)
positions = {}
tools = {}
hit_counts = f.hit_count()
oldpath = sum(f.path_length().values())
#Get hit positions
for hit in f.hits:
tool_num = hit.tool.number
if tool_num not in positions.keys():
positions[tool_num] = []
positions[tool_num].append(hit.position)
hits = []
# Optimize tool path for each tool
for tool, count in iter(hit_counts.items()):
# Calculate distance matrix
distance_matrix = [[math.hypot(*tuple(map(sub,
positions[tool][i],
positions[tool][j])))
for j in iter(range(count))]
for i in iter(range(count))]
# Calculate new path
path = solve_tsp(distance_matrix, 50)
# Create new hits list
hits += [DrillHit(f.tools[tool], positions[tool][p]) for p in path]
# Update the file
f.hits = hits
f.filename = f.filename + '.optimized'
f.write()
# Print drill report
print(f.report())
print('Original path length: %1.4f' % oldpath)
print('Optimized path length: %1.4f' % sum(f.path_length().values()))

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

354
examples/gerbers/shld.drd Normal file
View file

@ -0,0 +1,354 @@
%
M48
M72
T01C0.03200
T02C0.03543
T03C0.04000
%
T01
X11212Y16343
X80212Y16343
X21212Y16343
X99212Y22143
X99212Y12143
X40212Y16343
T02
X10812Y191043
X70812Y111043
X130812Y111043
X80812Y141043
X110812Y71043
X160812Y51043
X20812Y171043
X30812Y91043
X50812Y111043
X50812Y121043
X20812Y161043
X90812Y111043
X70812Y61043
X40812Y171043
X50812Y81043
X160812Y61043
X40812Y191043
X30812Y31043
X90812Y131043
X10812Y31043
X150812Y111043
X170812Y51043
X110812Y151043
X10812Y51043
X150812Y51043
X140812Y121043
X170812Y61043
X30812Y61043
X70812Y91043
X70812Y101043
X160812Y161043
X40812Y81043
X220812Y151043
X180812Y71043
X30812Y151043
X50812Y161043
X150812Y131043
X40812Y61043
X130812Y91043
X90812Y61043
X80812Y101043
X30812Y191043
X130812Y151043
X60812Y31043
X50812Y91043
X40812Y111043
X220812Y141043
X30812Y81043
X140812Y81043
X60812Y61043
X210812Y131043
X160812Y71043
X90812Y41043
X120812Y151043
X10812Y161043
X80812Y151043
X50812Y71043
X160812Y151043
X110812Y111043
X30812Y121043
X10812Y41043
X20812Y41043
X40812Y51043
X10812Y151043
X200812Y101043
X70812Y41043
X120812Y51043
X40812Y41043
X80812Y91043
X170812Y161043
X100812Y71043
X40812Y31043
X30812Y141043
X180812Y131043
X10812Y61043
X120812Y141043
X200812Y151043
X90812Y121043
X50812Y31043
X170812Y121043
X170812Y111043
X60812Y121043
X40812Y101043
X120812Y121043
X100812Y161043
X10812Y81043
X130812Y131043
X60812Y81043
X200812Y111043
X140812Y51043
X150812Y71043
X160812Y111043
X120812Y111043
X130812Y101043
X20812Y51043
X20812Y201043
X90812Y71043
X190812Y61043
X170812Y81043
X70812Y71043
X50812Y101043
X150812Y81043
X60812Y131043
X190812Y121043
X170812Y131043
X130812Y121043
X20812Y91043
X70812Y151043
X70812Y141043
X180812Y111043
X10812Y181043
X40812Y131043
X80812Y121043
X120812Y61043
X160812Y101043
X90812Y31043
X10812Y91043
X80812Y71043
X100812Y121043
X100812Y51043
X160812Y121043
X40812Y71043
X50812Y51043
X180812Y81043
X90812Y51043
X60812Y71043
X40812Y161043
X190812Y141043
X20812Y31043
X100812Y151043
X200812Y141043
X180812Y151043
X60812Y51043
X120812Y131043
X150812Y141043
X180812Y51043
X150812Y101043
X170812Y101043
X150812Y151043
X30812Y111043
X90812Y151043
X80812Y131043
X170812Y151043
X80812Y51043
X10812Y201043
X60812Y151043
X140812Y111043
X100812Y91043
X90812Y161043
X130812Y81043
X190812Y111043
X140812Y101043
X20812Y71043
X150812Y121043
X90812Y141043
X60812Y111043
X110812Y121043
X30812Y71043
X30812Y51043
X210812Y141043
X50812Y61043
X140812Y131043
X30812Y201043
X190812Y101043
X70812Y81043
X20812Y121043
X20812Y191043
X80812Y161043
X80812Y81043
X20812Y151043
X40812Y121043
X80812Y31043
X80812Y111043
X190812Y151043
X30812Y181043
X60812Y91043
X110812Y61043
X180812Y61043
X10812Y141043
X50812Y131043
X130812Y51043
X50812Y151043
X110812Y51043
X70812Y131043
X60812Y41043
X200812Y161043
X80812Y61043
X140812Y161043
X190812Y81043
X20812Y141043
X70812Y161043
X140812Y151043
X20812Y61043
X20812Y81043
X100812Y131043
X200812Y131043
X140812Y141043
X40812Y151043
X40812Y91043
X60812Y101043
X160812Y81043
X130812Y71043
X30812Y41043
X10812Y71043
X180812Y141043
X170812Y141043
X180812Y91043
X180812Y101043
X150812Y61043
X120812Y161043
X90812Y101043
X200812Y121043
X190812Y91043
X160812Y141043
X130812Y161043
X20812Y101043
X90812Y81043
X190812Y161043
X30812Y171043
X40812Y181043
X70812Y51043
X110812Y101043
X60812Y141043
X120812Y101043
X30812Y161043
X100812Y141043
X220812Y131043
X50812Y141043
X30812Y101043
X60812Y161043
X150812Y161043
X20812Y131043
X150812Y91043
X100812Y61043
X10812Y131043
X30812Y131043
X100812Y41043
X140812Y61043
X210812Y151043
X70812Y121043
X100812Y101043
X180812Y121043
X40812Y201043
X190812Y71043
X10812Y171043
X110812Y141043
X130812Y61043
X110812Y81043
X80812Y41043
X50812Y41043
X110812Y131043
X190812Y131043
X130812Y141043
X140812Y91043
X20812Y111043
X140812Y71043
X170812Y91043
X120812Y91043
X190812Y51043
X120812Y81043
X160812Y91043
X100812Y81043
X120812Y71043
X10812Y121043
X170812Y71043
X110812Y91043
X100812Y111043
X110812Y161043
X70812Y31043
X90812Y91043
X40812Y141043
X20812Y181043
X210812Y161043
X180812Y161043
X160812Y131043
T03
X86712Y189043
X213012Y23043
X126732Y201114
X96712Y189043
X86732Y201114
X56732Y201114
X142812Y23443
X106712Y189043
X112754Y11450
X182720Y200950
X106732Y201114
X207259Y55639
X207259Y81239
X203131Y11150
X76732Y201114
X192720Y200950
X66712Y189043
X96732Y201114
X193131Y11150
X66732Y201114
X203012Y23043
X122754Y11450
X76712Y189043
X173131Y11150
X192712Y188843
X116712Y189043
X116732Y201114
X213131Y11150
X162720Y200950
X225059Y55639
X183131Y11150
X126712Y189043
X183012Y23043
X212712Y188843
X163131Y11150
X213563Y110846
X122812Y23443
X132812Y23443
X182712Y188843
X212720Y200950
X202720Y200950
X193012Y23043
X213563Y120846
X172720Y200950
X225059Y81239
X223563Y120846
X56712Y189043
X172712Y188843
X213563Y100846
X142720Y200950
X163012Y23043
X142754Y11450
X223563Y110846
X132754Y11450
X142712Y188843
X162712Y188843
X152712Y188843
X223563Y100846
X202712Y188843
X112812Y23443
X173012Y23043
X152720Y200950
M30

View file

@ -220,7 +220,8 @@ class CamFile(object):
self.zeros = 'leading'
self.format = (2, 5)
self.statements = statements if statements is not None else []
self.primitives = primitives
if primitives is not None:
self.primitives = primitives
self.filename = filename
self.layer_name = layer_name

View file

@ -24,6 +24,7 @@ This module provides Excellon file classes and parsing utilities
"""
import math
import operator
from .excellon_statements import *
from .cam import CamFile, FileSettings
@ -49,6 +50,22 @@ def read(filename):
return ExcellonParser(settings).parse(filename)
class DrillHit(object):
def __init__(self, tool, position):
self.tool = tool
self.position = position
def to_inch(self):
if self.tool.units == 'metric':
self.tool.to_inch()
self.position = tuple(map(inch, self.position))
def to_metric(self):
if self.tool.units == 'inch':
self.tool.to_metric()
self.position = tuple(map(metric, self.position))
class ExcellonFile(CamFile):
""" A class representing a single excellon file
@ -81,17 +98,19 @@ class ExcellonFile(CamFile):
filename=filename)
self.tools = tools
self.hits = hits
self.primitives = [Drill(position, tool.diameter, units=settings.units)
for tool, position in self.hits]
@property
def primitives(self):
return [Drill(hit.position, hit.tool.diameter,units=self.settings.units) for hit in self.hits]
@property
def bounds(self):
xmin = ymin = 100000000000
xmax = ymax = -100000000000
for tool, position in self.hits:
radius = tool.diameter / 2.
x = position[0]
y = position[1]
for hit in self.hits:
radius = hit.tool.diameter / 2.
x, y = hit.position
xmin = min(x - radius, xmin)
xmax = max(x + radius, xmax)
ymin = min(y - radius, ymin)
@ -101,30 +120,47 @@ class ExcellonFile(CamFile):
def report(self, filename=None):
""" Print or save drill report
"""
toolfmt = ' T%%02d %%%d.%df %%d\n' % self.settings.format
rprt = 'Excellon Drill Report\n\n'
if self.settings.units == 'inch':
toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}in.\n' % self.settings.format
else:
toolfmt = ' T{:0>2d} {:%d.%df} {: >3d} {:f}mm\n' % self.settings.format
rprt = '=====================\nExcellon Drill Report\n=====================\n'
if self.filename is not None:
rprt += 'NC Drill File: %s\n\n' % self.filename
rprt += 'Drill File Info:\n\n'
rprt += 'Drill File Info:\n----------------\n'
rprt += (' Data Mode %s\n' % 'Absolute'
if self.settings.notation == 'absolute' else 'Incremental')
rprt += (' Units %s\n' % 'Inches'
if self.settings.units == 'inch' else 'Millimeters')
rprt += '\nTool List:\n\n'
rprt += ' Code Size Hits\n'
rprt += ' --------------------------\n'
for tool in self.tools.itervalues():
rprt += toolfmt % (tool.number, tool.diameter, tool.hit_count)
rprt += '\nTool List:\n----------\n\n'
rprt += ' Code Size Hits Path Length\n'
rprt += ' --------------------------------------\n'
for tool in iter(self.tools.values()):
rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.path_length(tool.number))
if filename is not None:
with open(filename, 'w') as f:
f.write(rprt)
return rprt
def write(self, filename):
def write(self, filename=None):
filename = filename if filename is not None else self.filename
with open(filename, 'w') as f:
# Copy the header verbatim
for statement in self.statements:
f.write(statement.to_excellon(self.settings) + '\n')
if not isinstance(statement, ToolSelectionStmt):
f.write(statement.to_excellon(self.settings) + '\n')
else:
break
# Write out coordinates for drill hits by tool
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(CoordinateStmt(*hit.position).to_excellon(self.settings) + '\n')
f.write(EndOfProgramStmt().to_excellon() + '\n')
def to_inch(self):
"""
Convert units to inches
@ -133,12 +169,12 @@ class ExcellonFile(CamFile):
self.units = 'inch'
for statement in self.statements:
statement.to_inch()
for tool in self.tools.itervalues():
for tool in iter(self.tools.values()):
tool.to_inch()
for primitive in self.primitives:
primitive.to_inch()
self.hits = [(tool, tuple(map(inch, pos)))
for tool, pos in self.hits]
for hit in self.hits:
hit.position = tuple(map(inch, hit,position))
def to_metric(self):
@ -152,17 +188,66 @@ class ExcellonFile(CamFile):
tool.to_metric()
for primitive in self.primitives:
primitive.to_metric()
self.hits = [(tool, tuple(map(metric, pos)))
for tool, pos in self.hits]
for hit in self.hits:
hit.position = tuple(map(metric, hit.position))
def offset(self, x_offset=0, y_offset=0):
for statement in self.statements:
statement.offset(x_offset, y_offset)
for primitive in self.primitives:
primitive.offset(x_offset, y_offset)
self.hits = [(tool, (pos[0] + x_offset, pos[1] + y_offset))
for tool, pos in self.hits]
for hit in self. hits:
hit.position = tuple(map(operator.add, hit.position, (x_offset, y_offset)))
def path_length(self, tool_number=None):
""" Return the path length for a given tool
"""
lengths = {}
positions = {}
for hit in self.hits:
tool = hit.tool
num = tool.number
positions[num] = (0, 0) if positions.get(num) is None else positions[num]
lengths[num] = 0.0 if lengths.get(num) is None else lengths[num]
lengths[num] = lengths[num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position)))
positions[num] = hit.position
if tool_number is None:
return lengths
else:
return lengths.get(tool_number)
def hit_count(self, tool_number=None):
counts = {}
for tool in iter(self.tools.values()):
counts[tool.number] = tool.hit_count
if tool_number is None:
return counts
else:
return counts.get(tool_number)
def update_tool(self, tool_number, **kwargs):
""" Change parameters of a tool
"""
if kwargs.get('feed_rate') is not None:
self.tools[tool_number].feed_rate = kwargs.get('feed_rate')
if kwargs.get('retract_rate') is not None:
self.tools[tool_number].retract_rate = kwargs.get('retract_rate')
if kwargs.get('rpm') is not None:
self.tools[tool_number].rpm = kwargs.get('rpm')
if kwargs.get('diameter') is not None:
self.tools[tool_number].diameter = kwargs.get('diameter')
if kwargs.get('max_hit_count') is not None:
self.tools[tool_number].max_hit_count = kwargs.get('max_hit_count')
if kwargs.get('depth_offset') is not None:
self.tools[tool_number].depth_offset = kwargs.get('depth_offset')
# Update drill hits
newtool = self.tools[tool_number]
for hit in self.hits:
if hit.tool.number == newtool.number:
hit.tool = newtool
class ExcellonParser(object):
""" Excellon File Parser
@ -248,6 +333,8 @@ class ExcellonParser(object):
self.statements.append(RewindStopStmt())
if self.state == 'HEADER':
self.state = 'DRILL'
elif self.state == 'INIT':
self.state = 'HEADER'
elif line[:3] == 'M95':
self.statements.append(HeaderEndStmt())
@ -312,7 +399,7 @@ class ExcellonParser(object):
for i in range(stmt.count):
self.pos[0] += stmt.xdelta if stmt.xdelta is not None else 0
self.pos[1] += stmt.ydelta if stmt.ydelta is not None else 0
self.hits.append((self.active_tool, tuple(self.pos)))
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
elif line[0] in ['X', 'Y']:
@ -331,7 +418,7 @@ class ExcellonParser(object):
if y is not None:
self.pos[1] += y
if self.state == 'DRILL':
self.hits.append((self.active_tool, tuple(self.pos)))
self.hits.append(DrillHit(self.active_tool, tuple(self.pos)))
self.active_tool._hit()
else:
self.statements.append(UnknownStmt.from_excellon(line))
@ -402,7 +489,7 @@ def detect_excellon_format(filename):
size = tuple([t[1] - t[0] for t in p.bounds])
hole_area = 0.0
for hit in p.hits:
tool = hit[0]
tool = hit.tool
hole_area += math.pow(math.pi * tool.diameter / 2., 2)
results[key] = (size, p.hole_count, hole_area)
except:

View file

@ -22,7 +22,7 @@ Excellon Statements
"""
import re
import uuid
from .utils import (parse_gerber_value, write_gerber_value, decimal_string,
inch, metric)
@ -40,13 +40,15 @@ class ExcellonStatement(object):
""" Excellon Statement abstract base class
"""
units = 'inch'
@classmethod
def from_excellon(cls, line):
raise NotImplementedError('from_excellon must be implemented in a '
'subclass')
def __init__(self, unit='inch', id=None):
self.units = unit
self.id = uuid.uuid4().int if id is None else id
def to_excellon(self, settings=None):
raise NotImplementedError('to_excellon must be implemented in a '
'subclass')
@ -107,7 +109,7 @@ class ExcellonTool(ExcellonStatement):
"""
@classmethod
def from_excellon(cls, line, settings):
def from_excellon(cls, line, settings, id=None):
""" Create a Tool from an excellon file tool definition line.
Parameters
@ -126,6 +128,7 @@ class ExcellonTool(ExcellonStatement):
commands = re.split('([BCFHSTZ])', line)[1:]
commands = [(command, value) for command, value in pairwise(commands)]
args = {}
args['id'] = id
nformat = settings.format
zero_suppression = settings.zero_suppression
for cmd, val in commands:
@ -165,6 +168,8 @@ class ExcellonTool(ExcellonStatement):
return cls(settings, **tool_dict)
def __init__(self, settings, **kwargs):
if kwargs.get('id') is not None:
super(ExcellonTool, self).__init__(id=kwargs.get('id'))
self.settings = settings
self.number = kwargs.get('number')
self.feed_rate = kwargs.get('feed_rate')
@ -221,7 +226,7 @@ class ExcellonTool(ExcellonStatement):
class ToolSelectionStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
""" Create a ToolSelectionStmt from an excellon file line.
Parameters
@ -244,9 +249,10 @@ class ToolSelectionStmt(ExcellonStatement):
tool = int(line[:2])
compensation_index = int(line[2:])
return cls(tool, compensation_index)
return cls(tool, compensation_index, **kwargs)
def __init__(self, tool, compensation_index=None):
def __init__(self, tool, compensation_index=None, **kwargs):
super(ToolSelectionStmt, self).__init__(**kwargs)
tool = int(tool)
compensation_index = (int(compensation_index) if compensation_index
is not None else None)
@ -263,7 +269,7 @@ class ToolSelectionStmt(ExcellonStatement):
class CoordinateStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings):
def from_excellon(cls, line, settings, **kwargs):
x_coord = None
y_coord = None
if line[0] == 'X':
@ -276,11 +282,12 @@ class CoordinateStmt(ExcellonStatement):
else:
y_coord = parse_gerber_value(line.strip(' Y'), settings.format,
settings.zero_suppression)
c = cls(x_coord, y_coord)
c = cls(x_coord, y_coord, **kwargs)
c.units = settings.units
return c
def __init__(self, x=None, y=None):
def __init__(self, x=None, y=None, **kwargs):
super(CoordinateStmt, self).__init__(**kwargs)
self.x = x
self.y = y
@ -329,7 +336,7 @@ class CoordinateStmt(ExcellonStatement):
class RepeatHoleStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings):
def from_excellon(cls, line, settings, **kwargs):
match = re.compile(r'R(?P<rcount>[0-9]*)X?(?P<xdelta>[+\-]?\d*\.?\d*)?Y?'
'(?P<ydelta>[+\-]?\d*\.?\d*)?').match(line)
stmt = match.groupdict()
@ -340,11 +347,12 @@ class RepeatHoleStmt(ExcellonStatement):
ydelta = (parse_gerber_value(stmt['ydelta'], settings.format,
settings.zero_suppression)
if stmt['ydelta'] is not '' else None)
c = cls(count, xdelta, ydelta)
c = cls(count, xdelta, ydelta, **kwargs)
c.units = settings.units
return c
def __init__(self, count, xdelta=0.0, ydelta=0.0):
def __init__(self, count, xdelta=0.0, ydelta=0.0, **kwargs):
super(RepeatHoleStmt, self).__init__(**kwargs)
self.count = count
self.xdelta = xdelta
self.ydelta = ydelta
@ -385,10 +393,11 @@ class RepeatHoleStmt(ExcellonStatement):
class CommentStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
return cls(line.lstrip(';'))
def __init__(self, comment):
def __init__(self, comment, **kwargs):
super(CommentStmt, self).__init__(**kwargs)
self.comment = comment
def to_excellon(self, settings=None):
@ -397,8 +406,8 @@ class CommentStmt(ExcellonStatement):
class HeaderBeginStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(HeaderBeginStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M48'
@ -406,8 +415,8 @@ class HeaderBeginStmt(ExcellonStatement):
class HeaderEndStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(HeaderEndStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'M95'
@ -415,8 +424,8 @@ class HeaderEndStmt(ExcellonStatement):
class RewindStopStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(RewindStopStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return '%'
@ -425,7 +434,7 @@ class RewindStopStmt(ExcellonStatement):
class EndOfProgramStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line, settings):
def from_excellon(cls, line, settings, **kwargs):
match = re.compile(r'M30X?(?P<x>\d*\.?\d*)?Y?'
'(?P<y>\d*\.?\d*)?').match(line)
stmt = match.groupdict()
@ -435,11 +444,12 @@ class EndOfProgramStmt(ExcellonStatement):
y = (parse_gerber_value(stmt['y'], settings.format,
settings.zero_suppression)
if stmt['y'] is not '' else None)
c = cls(x, y)
c = cls(x, y, **kwargs)
c.units = settings.units
return c
def __init__(self, x=None, y=None):
def __init__(self, x=None, y=None, **kwargs):
super(EndOfProgramStmt, self).__init__(**kwargs)
self.x = x
self.y = y
@ -476,12 +486,13 @@ class EndOfProgramStmt(ExcellonStatement):
class UnitStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
units = 'inch' if 'INCH' in line else 'metric'
zeros = 'leading' if 'LZ' in line else 'trailing'
return cls(units, zeros)
return cls(units, zeros, **kwargs)
def __init__(self, units='inch', zeros='leading'):
def __init__(self, units='inch', zeros='leading', **kwargs):
super(UnitStmt, self).__init__(**kwargs)
self.units = units.lower()
self.zeros = zeros
@ -500,10 +511,11 @@ class UnitStmt(ExcellonStatement):
class IncrementalModeStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
return cls('off') if 'OFF' in line else cls('on')
def from_excellon(cls, line, **kwargs):
return cls('off', **kwargs) if 'OFF' in line else cls('on', **kwargs)
def __init__(self, mode='off'):
def __init__(self, mode='off', **kwargs):
super(IncrementalModeStmt, self).__init__(**kwargs)
if mode.lower() not in ['on', 'off']:
raise ValueError('Mode may be "on" or "off"')
self.mode = mode
@ -515,11 +527,12 @@ class IncrementalModeStmt(ExcellonStatement):
class VersionStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
version = int(line.split(',')[1])
return cls(version)
return cls(version, **kwargs)
def __init__(self, version=1):
def __init__(self, version=1, **kwargs):
super(VersionStmt, self).__init__(**kwargs)
version = int(version)
if version not in [1, 2]:
raise ValueError('Valid versions are 1 or 2')
@ -532,11 +545,12 @@ class VersionStmt(ExcellonStatement):
class FormatStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
fmt = int(line.split(',')[1])
return cls(fmt)
return cls(fmt, **kwargs)
def __init__(self, format=1):
def __init__(self, format=1, **kwargs):
super(FormatStmt, self).__init__(**kwargs)
format = int(format)
if format not in [1, 2]:
raise ValueError('Valid formats are 1 or 2')
@ -549,11 +563,12 @@ class FormatStmt(ExcellonStatement):
class LinkToolStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
linked = [int(tool) for tool in line.split('/')]
return cls(linked)
return cls(linked, **kwargs)
def __init__(self, linked_tools):
def __init__(self, linked_tools, **kwargs):
super(LinkToolStmt, self).__init__(**kwargs)
self.linked_tools = [int(x) for x in linked_tools]
def to_excellon(self, settings=None):
@ -563,12 +578,13 @@ class LinkToolStmt(ExcellonStatement):
class MeasuringModeStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
def from_excellon(cls, line, **kwargs):
if not ('M71' in line or 'M72' in line):
raise ValueError('Not a measuring mode statement')
return cls('inch') if 'M72' in line else cls('metric')
return cls('inch', **kwargs) if 'M72' in line else cls('metric', **kwargs)
def __init__(self, units='inch'):
def __init__(self, units='inch', **kwargs):
super(MeasuringModeStmt, self).__init__(**kwargs)
units = units.lower()
if units not in ['inch', 'metric']:
raise ValueError('units must be "inch" or "metric"')
@ -585,8 +601,8 @@ class MeasuringModeStmt(ExcellonStatement):
class RouteModeStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(RouteModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G00'
@ -594,8 +610,8 @@ class RouteModeStmt(ExcellonStatement):
class DrillModeStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(DrillModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G05'
@ -603,8 +619,8 @@ class DrillModeStmt(ExcellonStatement):
class AbsoluteModeStmt(ExcellonStatement):
def __init__(self):
pass
def __init__(self, **kwargs):
super(AbsoluteModeStmt, self).__init__(**kwargs)
def to_excellon(self, settings=None):
return 'G90'
@ -613,10 +629,11 @@ class AbsoluteModeStmt(ExcellonStatement):
class UnknownStmt(ExcellonStatement):
@classmethod
def from_excellon(cls, line):
return cls(line)
def from_excellon(cls, line, **kwargs):
return cls(line, **kwargs)
def __init__(self, stmt):
def __init__(self, stmt, **kwargs):
super(UnknownStmt, self).__init__(**kwargs)
self.stmt = stmt
def to_excellon(self, settings=None):

View file

@ -36,11 +36,13 @@ class Primitive(object):
Rotation of a primitive about its origin in degrees. Positive rotation
is counter-clockwise as viewed from the board top.
"""
def __init__(self, level_polarity='dark', rotation=0, units=None):
def __init__(self, level_polarity='dark', rotation=0, units=None, id=None, statement_id=None):
self.level_polarity = level_polarity
self.rotation = rotation
self.units = units
self._to_convert = list()
self.id = id
self.statement_id = statement_id
def bounding_box(self):
""" Calculate bounding box

View file

@ -24,6 +24,17 @@ def test_read():
ncdrill = read(NCDRILL_FILE)
assert(isinstance(ncdrill, ExcellonFile))
def test_write():
ncdrill = read(NCDRILL_FILE)
ncdrill.write('test.ncd')
with open(NCDRILL_FILE) as src:
srclines = src.readlines()
with open('test.ncd') as res:
for idx, line in enumerate(res):
assert_equal(line.strip(), srclines[idx].strip())
os.remove('test.ncd')
def test_read_settings():
ncdrill = read(NCDRILL_FILE)
assert_equal(ncdrill.settings['format'], (2, 4))
@ -47,9 +58,11 @@ def test_conversion():
ncdrill.to_metric()
assert_equal(ncdrill.settings.units, 'metric')
inch_primitives = ncdrill_inch.primitives
for tool in iter(ncdrill_inch.tools.values()):
tool.to_metric()
for primitive in ncdrill_inch.primitives:
for primitive in inch_primitives:
primitive.to_metric()
for statement in ncdrill_inch.statements:
statement.to_metric()
@ -57,7 +70,7 @@ def test_conversion():
for m_tool, i_tool in zip(iter(ncdrill.tools.values()), iter(ncdrill_inch.tools.values())):
assert_equal(i_tool, m_tool)
for m, i in zip(ncdrill.primitives,ncdrill_inch.primitives):
for m, i in zip(ncdrill.primitives,inch_primitives):
assert_equal(m, i)