Merge pull request #31 from curtacircuitos/statement_link
Add support for tool reordering and drill path optimization example.
This commit is contained in:
commit
c92d2d9ea2
9 changed files with 647 additions and 83 deletions
90
examples/excellon_optimize.py
Normal file
90
examples/excellon_optimize.py
Normal 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()))
|
||||
|
||||
BIN
examples/excellon_optimize_after.PNG
Normal file
BIN
examples/excellon_optimize_after.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
examples/excellon_optimize_before.PNG
Normal file
BIN
examples/excellon_optimize_before.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
354
examples/gerbers/shld.drd
Normal file
354
examples/gerbers/shld.drd
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue