From 94f3976915d64a77135a1fdc8983085ee8d2e1f9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 11 Jun 2015 11:20:56 -0400 Subject: Add keys to statements for linking to primitives. Add some API features to ExcellonFile, such as getting a tool path length and changing tool parameters. Excellonfiles write method generates statements based on the drill hits in the hits member, so drill hits in a generated file can be re-ordered by re-ordering the drill hits in ExcellonFile.hits. see #30 --- gerber/excellon.py | 120 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 24 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index f994b67..1f0c570 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -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,20 +120,23 @@ 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' + rprt += '\nTool List:\n----------\n\n' + rprt += ' Code Size Hits Path Length\n' + rprt += ' --------------------------------------\n' for tool in self.tools.itervalues(): - rprt += toolfmt % (tool.number, tool.diameter, tool.hit_count) + rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.tool_path_length(tool.number)) if filename is not None: with open(filename, 'w') as f: f.write(rprt) @@ -122,9 +144,22 @@ class ExcellonFile(CamFile): def write(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') - + print(statement) + 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 self.tools.itervalues(): + 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 @@ -137,8 +172,8 @@ class ExcellonFile(CamFile): 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 +187,52 @@ 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 tool_path_length(self, tool_number): + """ Return the path length for a given tool + """ + length = 0.0 + pos = (0, 0) + for hit in self.hits: + tool = hit.tool + if tool.number == tool_number: + length = length + math.hypot(*tuple(map(operator.sub, pos, hit.position))) + pos = hit.position + return length + + + 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 +318,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 +384,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 +403,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 +474,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: -- cgit From ec2ca92da6e3dac1d56bfba28d4c2cadc35a9811 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 11 Jun 2015 14:00:40 -0400 Subject: Python 3 fix remove dict itervalues() calls --- gerber/excellon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 1f0c570..65d014f 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -135,7 +135,7 @@ class ExcellonFile(CamFile): rprt += '\nTool List:\n----------\n\n' rprt += ' Code Size Hits Path Length\n' rprt += ' --------------------------------------\n' - for tool in self.tools.itervalues(): + for tool in iter(self.tools.values()): rprt += toolfmt.format(tool.number, tool.diameter, tool.hit_count, self.tool_path_length(tool.number)) if filename is not None: with open(filename, 'w') as f: @@ -153,7 +153,7 @@ class ExcellonFile(CamFile): break # Write out coordinates for drill hits by tool - for tool in self.tools.itervalues(): + 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: @@ -168,7 +168,7 @@ 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() -- cgit From 15254a5bb7ad866e09374c5a99e9be4468e4d3c7 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Jul 2015 12:13:59 -0400 Subject: Add tool path optimization example Add example demonstrating use of tsp-solver with pcb-tools to optimize tool paths in an excellon file. This is based on @koppi's script in #30 --- gerber/excellon.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 65d014f..d89b349 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -136,17 +136,18 @@ class ExcellonFile(CamFile): 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.tool_path_length(tool.number)) + 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: - print(statement) if not isinstance(statement, ToolSelectionStmt): f.write(statement.to_excellon(self.settings) + '\n') else: @@ -198,18 +199,32 @@ class ExcellonFile(CamFile): for hit in self. hits: hit.position = tuple(map(operator.add, hit.position, (x_offset, y_offset))) - def tool_path_length(self, tool_number): + def path_length(self, tool_number=None): """ Return the path length for a given tool """ - length = 0.0 - pos = (0, 0) + lengths = {} + positions = {} for hit in self.hits: tool = hit.tool - if tool.number == tool_number: - length = length + math.hypot(*tuple(map(operator.sub, pos, hit.position))) - pos = hit.position - return length + 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 -- cgit