From 137c73f3e42281de67bde8f1c0b16938f5b8aeeb Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Wed, 14 Jan 2015 14:33:00 -0200 Subject: Many additions to Excellon parsing/creation. CAUTION: the original code used zero_suppression flags in the opposite sense as Gerber functions. This patch changes it to behave just like Gerber code. * Add metric/inch conversion support * Add settings context variable to to_gerber just like Gerber code. * Add some missing Excellon values. Tests are not entirely updated. --- gerber/excellon.py | 51 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 19 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 9d09576..ee38367 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright 2014 Hamilton Kibbe - + # 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 @@ -13,8 +13,8 @@ # 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. - +# limitations under the License. + """ Excellon File module ==================== @@ -28,6 +28,7 @@ from .excellon_statements import * from .cam import CamFile, FileSettings from .primitives import Drill import math +import re def read(filename): """ Read data from filename and return an ExcellonFile @@ -42,10 +43,7 @@ def read(filename): An ExcellonFile created from the specified file. """ - detected_settings = detect_excellon_format(filename) - settings = FileSettings(**detected_settings) - zeros = '' - return ExcellonParser(settings).parse(filename) + return ExcellonParser(None).parse(filename) class ExcellonFile(CamFile): @@ -104,7 +102,7 @@ class ExcellonFile(CamFile): def write(self, filename): with open(filename, 'w') as f: for statement in self.statements: - f.write(statement.to_excellon() + '\n') + f.write(statement.to_excellon(self.settings) + '\n') class ExcellonParser(object): @@ -118,14 +116,14 @@ class ExcellonParser(object): def __init__(self, settings=None): self.notation = 'absolute' self.units = 'inch' - self.zero_suppression = 'trailing' - self.format = (2, 5) + self.zero_suppression = 'leading' + self.format = (2, 4) self.state = 'INIT' self.statements = [] self.tools = {} self.hits = [] self.active_tool = None - self.pos = [0., 0.] + self.pos = [0., 0.] if settings is not None: self.units = settings.units self.zero_suppression = settings.zero_suppression @@ -166,11 +164,19 @@ class ExcellonParser(object): self._settings(), filename) def _parse(self, line): - #line = line.strip() - zs = self._settings().zero_suppression - fmt = self._settings().format + # skip empty lines + if not line.strip(): + return + if line[0] == ';': - self.statements.append(CommentStmt.from_excellon(line)) + comment_stmt = CommentStmt.from_excellon(line) + self.statements.append(comment_stmt) + + # get format from altium comment + if "FILE_FORMAT" in comment_stmt.comment: + detected_format = tuple([int(x) for x in comment_stmt.comment.split('=')[1].split(":")]) + if detected_format: + self.format = detected_format elif line[:3] == 'M48': self.statements.append(HeaderBeginStmt()) @@ -191,9 +197,11 @@ class ExcellonParser(object): self.statements.append(stmt) elif line[:3] == 'G00': + self.statements.append(RouteModeStmt()) self.state = 'ROUT' elif line[:3] == 'G05': + self.statements.append(DrillModeStmt()) self.state = 'DRILL' elif (('INCH' in line or 'METRIC' in line) and @@ -221,6 +229,9 @@ class ExcellonParser(object): stmt = FormatStmt.from_excellon(line) self.statements.append(stmt) + elif line[:4] == 'G90': + self.statements.append(AbsoluteModeStmt()) + elif line[0] == 'T' and self.state == 'HEADER': tool = ExcellonTool.from_excellon(line, self._settings()) self.tools[tool.number] = tool @@ -228,14 +239,16 @@ class ExcellonParser(object): elif line[0] == 'T' and self.state != 'HEADER': stmt = ToolSelectionStmt.from_excellon(line) - self.active_tool = self.tools[stmt.tool] + # T0 is used as END marker, just ignore + if stmt.tool != 0: + self.active_tool = self.tools[stmt.tool] self.statements.append(stmt) elif line[0] in ['X', 'Y']: - stmt = CoordinateStmt.from_excellon(line, fmt, zs) + stmt = CoordinateStmt.from_excellon(line, self._settings()) x = stmt.x y = stmt.y - self.statements.append(stmt) + self.statements.append(stmt) if self.notation == 'absolute': if x is not None: self.pos[0] = x @@ -246,7 +259,7 @@ class ExcellonParser(object): self.pos[0] += x if y is not None: self.pos[1] += y - if self.state == 'DRILL': + if self.state == 'DRILL': self.hits.append((self.active_tool, tuple(self.pos))) self.active_tool._hit() else: -- cgit From 0f36084aadc85670b96ca63a8258d18db4b18cf8 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Thu, 15 Jan 2015 05:01:40 -0200 Subject: Add Repeat Hole Stmt and fix UnitStmt parsing * Repeat hole support (with no real parsing, just a copy) * Fix UnitStmt to works even when a ,TZ or ,LZ information is not provided. --- gerber/excellon.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index ee38367..17b870a 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -204,8 +204,7 @@ class ExcellonParser(object): self.statements.append(DrillModeStmt()) self.state = 'DRILL' - elif (('INCH' in line or 'METRIC' in line) and - ('LZ' in line or 'TZ' in line)): + elif 'INCH' in line or 'METRIC' in line: stmt = UnitStmt.from_excellon(line) self.units = stmt.units self.zero_suppression = stmt.zero_suppression @@ -244,6 +243,10 @@ class ExcellonParser(object): self.active_tool = self.tools[stmt.tool] self.statements.append(stmt) + elif line[0] == 'R' and self.state != 'HEADER': + stmt = RepeatHoleStmt.from_excellon(line, self._settings()) + self.statements.append(stmt) + elif line[0] in ['X', 'Y']: stmt = CoordinateStmt.from_excellon(line, self._settings()) x = stmt.x -- cgit From b495d51354eff7b858dbbd41740865eba7f39100 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 25 Jan 2015 14:19:48 -0500 Subject: Changed zeros/zero suppression conventions to match file format specs --- gerber/excellon.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 17b870a..235f966 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -43,7 +43,9 @@ def read(filename): An ExcellonFile created from the specified file. """ - return ExcellonParser(None).parse(filename) + # File object should use settings from source file by default. + settings = FileSettings(**detect_excellon_format(filename)) + return ExcellonParser(settings).parse(filename) class ExcellonFile(CamFile): @@ -116,7 +118,7 @@ class ExcellonParser(object): def __init__(self, settings=None): self.notation = 'absolute' self.units = 'inch' - self.zero_suppression = 'leading' + self.zeros = 'leading' self.format = (2, 4) self.state = 'INIT' self.statements = [] @@ -125,8 +127,9 @@ class ExcellonParser(object): self.active_tool = None self.pos = [0., 0.] if settings is not None: + print('Setting shit from settings. zeros: %s' %settings.zeros) self.units = settings.units - self.zero_suppression = settings.zero_suppression + self.zeros = settings.zeros self.notation = settings.notation self.format = settings.format @@ -207,7 +210,7 @@ class ExcellonParser(object): elif 'INCH' in line or 'METRIC' in line: stmt = UnitStmt.from_excellon(line) self.units = stmt.units - self.zero_suppression = stmt.zero_suppression + self.zeros = stmt.zeros self.statements.append(stmt) elif line[:3] == 'M71' or line [:3] == 'M72': @@ -270,8 +273,7 @@ class ExcellonParser(object): def _settings(self): return FileSettings(units=self.units, format=self.format, - zero_suppression=self.zero_suppression, - notation=self.notation) + zeros=self.zeros, notation=self.notation) def detect_excellon_format(filename): @@ -293,7 +295,7 @@ def detect_excellon_format(filename): results = {} detected_zeros = None detected_format = None - zs_options = ('leading', 'trailing', ) + zeros_options = ('leading', 'trailing', ) format_options = ((2, 4), (2, 5), (3, 3),) # Check for obvious clues: @@ -301,7 +303,7 @@ def detect_excellon_format(filename): p.parse(filename) # Get zero_suppression from a unit statement - zero_statements = [stmt.zero_suppression for stmt in p.statements + zero_statements = [stmt.zeros for stmt in p.statements if isinstance(stmt, UnitStmt)] # get format from altium comment @@ -316,19 +318,19 @@ def detect_excellon_format(filename): # Bail out here if possible if detected_format is not None and detected_zeros is not None: - return {'format': detected_format, 'zero_suppression': detected_zeros} + return {'format': detected_format, 'zeros': detected_zeros} # Only look at remaining options if detected_format is not None: format_options = (detected_format,) if detected_zeros is not None: - zs_options = (detected_zeros,) + zeros_options = (detected_zeros,) # Brute force all remaining options, and pick the best looking one... - for zs in zs_options: + for zeros in zeros_options: for fmt in format_options: - key = (fmt, zs) - settings = FileSettings(zero_suppression=zs, format=fmt) + key = (fmt, zeros) + settings = FileSettings(zeros=zeros, format=fmt) try: p = ExcellonParser(settings) p.parse(filename) @@ -351,7 +353,7 @@ def detect_excellon_format(filename): # Bail out here if we got everything.... if detected_format is not None and detected_zeros is not None: - return {'format': detected_format, 'zero_suppression': detected_zeros} + return {'format': detected_format, 'zeros': detected_zeros} # Otherwise score each option and pick the best candidate else: @@ -362,7 +364,7 @@ def detect_excellon_format(filename): minscore = min(scores.values()) for key in scores.iterkeys(): if scores[key] == minscore: - return {'format': key[0], 'zero_suppression': key[1]} + return {'format': key[0], 'zeros': key[1]} def _layer_size_score(size, hole_count, hole_area): -- cgit From 939f782728a1b16f85ad2697b03ef026a88ad354 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 25 Jan 2015 14:22:27 -0500 Subject: ...oops --- gerber/excellon.py | 1 - 1 file changed, 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 235f966..79a6e1f 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -127,7 +127,6 @@ class ExcellonParser(object): self.active_tool = None self.pos = [0., 0.] if settings is not None: - print('Setting shit from settings. zeros: %s' %settings.zeros) self.units = settings.units self.zeros = settings.zeros self.notation = settings.notation -- cgit From 5cf1fa74b42eb8feaab23078bef6f31f6d647c33 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 15 Feb 2015 02:20:02 -0500 Subject: Tests and bugfixes --- gerber/excellon.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 79a6e1f..87eaf03 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -95,11 +95,27 @@ class ExcellonFile(CamFile): ymax = max(y + radius, ymax) return ((xmin, xmax), (ymin, ymax)) - def report(self): - """ Print drill report + def report(self, filename=None): + """ Print or save drill report """ - pass - + toolfmt = ' T%%02d %%%d.%df %%d\n' % self.settings.format + rprt = 'Excellon 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 += (' 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) + if filename is not None: + with open(filename, 'w') as f: + f.write(rprt) + return rprt def write(self, filename): with open(filename, 'w') as f: @@ -195,7 +211,7 @@ class ExcellonParser(object): self.state = 'DRILL' elif line[:3] == 'M30': - stmt = EndOfProgramStmt.from_excellon(line) + stmt = EndOfProgramStmt.from_excellon(line, self._settings()) self.statements.append(stmt) elif line[:3] == 'G00': @@ -230,8 +246,9 @@ class ExcellonParser(object): stmt = FormatStmt.from_excellon(line) self.statements.append(stmt) - elif line[:4] == 'G90': + elif line[:3] == 'G90': self.statements.append(AbsoluteModeStmt()) + self.notation = 'absolute' elif line[0] == 'T' and self.state == 'HEADER': tool = ExcellonTool.from_excellon(line, self._settings()) -- cgit From bfe14841604b6be403e7123e8b6667b1f0aff6f6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 15 Feb 2015 03:29:47 -0500 Subject: Add cairo example code, and use example-generated image in readme --- gerber/excellon.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 87eaf03..a7f3a27 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -265,6 +265,11 @@ class ExcellonParser(object): elif line[0] == 'R' and self.state != 'HEADER': stmt = RepeatHoleStmt.from_excellon(line, self._settings()) self.statements.append(stmt) + for i in xrange(stmt.count): + self.pos[0] += stmt.xdelta + self.pos[1] += stmt.ydelta + self.hits.append((self.active_tool, tuple(self.pos))) + self.active_tool._hit() elif line[0] in ['X', 'Y']: stmt = CoordinateStmt.from_excellon(line, self._settings()) -- cgit From 288ac27084b47166ac662402ea340d0aa25d8f56 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 04:31:23 -0500 Subject: Get unit conversion working for Gerber/Excellon files Started operations module for file operations/transforms --- gerber/excellon.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index a7f3a27..a339827 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -23,12 +23,12 @@ Excellon File module This module provides Excellon file classes and parsing utilities """ +import math from .excellon_statements import * from .cam import CamFile, FileSettings from .primitives import Drill -import math -import re + def read(filename): """ Read data from filename and return an ExcellonFile @@ -122,6 +122,31 @@ class ExcellonFile(CamFile): for statement in self.statements: f.write(statement.to_excellon(self.settings) + '\n') + def to_inch(self): + """ + Convert units to inches + """ + if self.units != 'inch': + self.units = 'inch' + for statement in self.statements: + statement.to_inch() + for tool in self.tools.itervalues(): + tool.to_inch() + for primitive in self.primitives: + primitive.to_inch() + + def to_metric(self): + """ Convert units to metric + """ + if self.units != 'metric': + self.units = 'metric' + for statement in self.statements: + statement.to_metric() + for tool in self.tools.itervalues(): + tool.to_metric() + for primitive in self.primitives: + primitive.to_metric() + class ExcellonParser(object): """ Excellon File Parser -- cgit From e71d7a24b5be3e68d36494869595eec934db4bd2 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 21:14:30 -0500 Subject: Python 3 tests passing --- gerber/excellon.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index a339827..ebc307f 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -142,7 +142,7 @@ class ExcellonFile(CamFile): self.units = 'metric' for statement in self.statements: statement.to_metric() - for tool in self.tools.itervalues(): + for tool in iter(self.tools.values()): tool.to_metric() for primitive in self.primitives: primitive.to_metric() @@ -290,7 +290,7 @@ class ExcellonParser(object): elif line[0] == 'R' and self.state != 'HEADER': stmt = RepeatHoleStmt.from_excellon(line, self._settings()) self.statements.append(stmt) - for i in xrange(stmt.count): + for i in range(stmt.count): self.pos[0] += stmt.xdelta self.pos[1] += stmt.ydelta self.hits.append((self.active_tool, tuple(self.pos))) @@ -390,8 +390,8 @@ def detect_excellon_format(filename): pass # See if any of the dimensions are left with only a single option - formats = set(key[0] for key in results.iterkeys()) - zeros = set(key[1] for key in results.iterkeys()) + formats = set(key[0] for key in iter(results.keys())) + zeros = set(key[1] for key in iter(results.keys())) if len(formats) == 1: detected_format = formats.pop() if len(zeros) == 1: @@ -408,7 +408,7 @@ def detect_excellon_format(filename): size, count, diameter = results[key] scores[key] = _layer_size_score(size, count, diameter) minscore = min(scores.values()) - for key in scores.iterkeys(): + for key in iter(scores.keys()): if scores[key] == minscore: return {'format': key[0], 'zeros': key[1]} -- cgit From 5966d7830bda7f37ed5ddcc1bfccb93e7f780eaa Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 23:13:23 -0500 Subject: Add offset operation --- gerber/excellon.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index ebc307f..900e2df 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -28,6 +28,7 @@ import math from .excellon_statements import * from .cam import CamFile, FileSettings from .primitives import Drill +from .utils import inch, metric def read(filename): @@ -134,6 +135,9 @@ 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] + def to_metric(self): """ Convert units to metric @@ -146,6 +150,16 @@ 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] + + 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] class ExcellonParser(object): -- cgit From b9b20a9644ca7b87493ca5786e2a25ecab132b75 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Wed, 18 Mar 2015 03:38:52 -0300 Subject: Fix Excellon repeat command --- gerber/excellon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 900e2df..930b683 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -54,6 +54,8 @@ class ExcellonFile(CamFile): The ExcellonFile class represents a single excellon file. + http://www.excellon.com/manuals/program.htm + Parameters ---------- tools : list @@ -305,8 +307,8 @@ class ExcellonParser(object): stmt = RepeatHoleStmt.from_excellon(line, self._settings()) self.statements.append(stmt) for i in range(stmt.count): - self.pos[0] += stmt.xdelta - self.pos[1] += stmt.ydelta + 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.active_tool._hit() -- cgit From 390838fc8b70c9b105fdc1d3e35a4533b27faa83 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 24 Apr 2015 10:54:13 -0400 Subject: Fix for #25. Checking was happening at the gerber/excellon file level, but I added units checking at the primitive level so the use case shown in the example is covered. Might want to throw a bunch more assertions in the test code (i started doing a few) to cover multiple calls to unit conversion functions --- gerber/excellon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 930b683..0f70de7 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -81,7 +81,7 @@ class ExcellonFile(CamFile): filename=filename) self.tools = tools self.hits = hits - self.primitives = [Drill(position, tool.diameter) + self.primitives = [Drill(position, tool.diameter, units=settings.units) for tool, position in self.hits] @property -- cgit From 8ec3077be988681bbbafcef18ea3a2f84dd61b2b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 16 May 2015 09:45:34 -0400 Subject: Add checks to ensure statement unit conversions are idempotent --- gerber/excellon.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 0f70de7..f994b67 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -219,6 +219,9 @@ class ExcellonParser(object): with open(filename, 'r') as f: for line in f: self._parse(line.strip()) + + for stmt in self.statements: + stmt.units = self.units return ExcellonFile(self.statements, self.tools, self.hits, self._settings(), filename) -- cgit 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 From dd63b169f177389602e17bc6ced53bd0f1ba0de3 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 16:51:21 -0400 Subject: Allow files to be read from strings per #37 Adds a loads() method to the top level module which generates a GerberFile or ExcellonFile from a string --- gerber/excellon.py | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index d89b349..ba8573d 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -25,6 +25,7 @@ This module provides Excellon file classes and parsing utilities import math import operator +from cStringIO import StringIO from .excellon_statements import * from .cam import CamFile, FileSettings @@ -46,9 +47,28 @@ def read(filename): """ # File object should use settings from source file by default. - settings = FileSettings(**detect_excellon_format(filename)) + with open(filename, 'r') as f: + data = f.read() + settings = FileSettings(**detect_excellon_format(data)) return ExcellonParser(settings).parse(filename) +def loads(data): + """ Read data from string and return an ExcellonFile + Parameters + ---------- + data : string + string containing Excellon file contents + + Returns + ------- + file : :class:`gerber.excellon.ExcellonFile` + An ExcellonFile created from the specified file. + + """ + # File object should use settings from source file by default. + settings = FileSettings(**detect_excellon_format(data)) + return ExcellonParser(settings).parse_raw(data) + class DrillHit(object): def __init__(self, tool, position): @@ -302,9 +322,12 @@ class ExcellonParser(object): def parse(self, filename): with open(filename, 'r') as f: - for line in f: - self._parse(line.strip()) - + data = f.read() + return self.parse_raw(data, filename) + + def parse_raw(self, data, filename=None): + for line in StringIO(data): + self._parse(line.strip()) for stmt in self.statements: stmt.units = self.units return ExcellonFile(self.statements, self.tools, self.hits, @@ -428,14 +451,13 @@ class ExcellonParser(object): zeros=self.zeros, notation=self.notation) -def detect_excellon_format(filename): +def detect_excellon_format(data=None, filename=None): """ Detect excellon file decimal format and zero-suppression settings. Parameters ---------- - filename : string - Name of the file to parse. This does not check if the file is actually - an Excellon file, so do that before calling this. + data : string + String containing contents of Excellon file. Returns ------- @@ -449,10 +471,16 @@ def detect_excellon_format(filename): detected_format = None zeros_options = ('leading', 'trailing', ) format_options = ((2, 4), (2, 5), (3, 3),) + + if data is None and filename is None: + raise ValueError('Either data or filename arguments must be provided') + if data is None: + with open(filename, 'r') as f: + data = f.read() # Check for obvious clues: p = ExcellonParser() - p.parse(filename) + p.parse_raw(data) # Get zero_suppression from a unit statement zero_statements = [stmt.zeros for stmt in p.statements @@ -485,8 +513,8 @@ def detect_excellon_format(filename): settings = FileSettings(zeros=zeros, format=fmt) try: p = ExcellonParser(settings) - p.parse(filename) - size = tuple([t[1] - t[0] for t in p.bounds]) + p.parse_raw(data) + size = tuple([t[0] - t[1] for t in p.bounds]) hole_area = 0.0 for hit in p.hits: tool = hit.tool -- cgit From 10d9028e1fdf7431baee73c7f1474d2134bac5fa Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 17:02:45 -0400 Subject: Python 3 fix --- gerber/excellon.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index ba8573d..7333a98 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -25,7 +25,11 @@ This module provides Excellon file classes and parsing utilities import math import operator -from cStringIO import StringIO + +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO from .excellon_statements import * from .cam import CamFile, FileSettings -- cgit From 9ca75f991a240b0ea233382ff23264a009b0324e Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Fri, 13 Nov 2015 03:31:32 -0200 Subject: Improve Excellon parsing coverage Add some not so used codes that were generating unknown stmt. --- gerber/excellon.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 11 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 7333a98..c953e55 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -78,12 +78,12 @@ 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() @@ -96,6 +96,7 @@ class ExcellonFile(CamFile): The ExcellonFile class represents a single excellon file. http://www.excellon.com/manuals/program.htm + (archived version at https://web.archive.org/web/20150920001043/http://www.excellon.com/manuals/program.htm) Parameters ---------- @@ -122,11 +123,11 @@ class ExcellonFile(CamFile): filename=filename) self.tools = tools self.hits = 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): @@ -169,14 +170,14 @@ class ExcellonFile(CamFile): 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: 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') @@ -184,7 +185,7 @@ class ExcellonFile(CamFile): 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 @@ -235,7 +236,7 @@ class ExcellonFile(CamFile): 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: @@ -270,8 +271,8 @@ class ExcellonFile(CamFile): for hit in self.hits: if hit.tool.number == newtool.number: hit.tool = newtool - - + + class ExcellonParser(object): """ Excellon File Parser @@ -368,6 +369,15 @@ class ExcellonParser(object): if self.state == 'HEADER': self.state = 'DRILL' + elif line[:3] == 'M15': + self.statements.append(ZAxisRoutPositionStmt()) + + elif line[:3] == 'M16': + self.statements.append(RetractWithClampingStmt()) + + elif line[:3] == 'M17': + self.statements.append(RetractWithoutClampingStmt()) + elif line[:3] == 'M30': stmt = EndOfProgramStmt.from_excellon(line, self._settings()) self.statements.append(stmt) @@ -376,6 +386,44 @@ class ExcellonParser(object): self.statements.append(RouteModeStmt()) self.state = 'ROUT' + stmt = CoordinateStmt.from_excellon(line[3:], self._settings()) + stmt.mode = self.state + + x = stmt.x + y = stmt.y + self.statements.append(stmt) + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + + elif line[:3] == 'G01': + self.statements.append(RouteModeStmt()) + self.state = 'LINEAR' + + stmt = CoordinateStmt.from_excellon(line[3:], self._settings()) + stmt.mode = self.state + + x = stmt.x + y = stmt.y + self.statements.append(stmt) + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + elif line[:3] == 'G05': self.statements.append(DrillModeStmt()) self.state = 'DRILL' @@ -404,10 +452,23 @@ class ExcellonParser(object): stmt = FormatStmt.from_excellon(line) self.statements.append(stmt) + elif line[:3] == 'G40': + self.statements.append(CutterCompensationOffStmt()) + + elif line[:3] == 'G41': + self.statements.append(CutterCompensationLeftStmt()) + + elif line[:3] == 'G42': + self.statements.append(CutterCompensationRightStmt()) + elif line[:3] == 'G90': self.statements.append(AbsoluteModeStmt()) self.notation = 'absolute' + elif line[0] == 'F': + infeed_rate_stmt = ZAxisInfeedRateStmt.from_excellon(line) + self.statements.append(infeed_rate_stmt) + elif line[0] == 'T' and self.state == 'HEADER': tool = ExcellonTool.from_excellon(line, self._settings()) self.tools[tool.number] = tool @@ -475,7 +536,7 @@ def detect_excellon_format(data=None, filename=None): detected_format = None zeros_options = ('leading', 'trailing', ) format_options = ((2, 4), (2, 5), (3, 3),) - + if data is None and filename is None: raise ValueError('Either data or filename arguments must be provided') if data is None: -- cgit From cead702f4d7094c3cec1419d6fd79b23cc4196c4 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Fri, 13 Nov 2015 13:18:50 -0200 Subject: Add fix to work with excellon with no tool definition. I found out that Proteus generate some strange Excellon without any tool definition. Gerbv renders it correctly and after digging in I found the heuristic that they use to "guess" the tool diameter. This change replicates this behavior on pcb-tools. --- gerber/excellon.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index c953e55..101c6ea 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -476,10 +476,27 @@ class ExcellonParser(object): elif line[0] == 'T' and self.state != 'HEADER': stmt = ToolSelectionStmt.from_excellon(line) + self.statements.append(stmt) + # T0 is used as END marker, just ignore if stmt.tool != 0: + # FIXME: for weird files with no tools defined, original calc from gerbv + if stmt.tool not in self.tools: + if self._settings().units == "inch": + diameter = (16 + 8 * stmt.tool) / 1000.0; + else: + diameter = metric((16 + 8 * stmt.tool) / 1000.0); + + tool = ExcellonTool(self._settings(), number=stmt.tool, diameter=diameter) + self.tools[tool.number] = tool + + # FIXME: need to add this tool definition inside header to make sure it is properly written + for i, s in enumerate(self.statements): + if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool): + self.statements.insert(i, tool) + break + self.active_tool = self.tools[stmt.tool] - self.statements.append(stmt) elif line[0] == 'R' and self.state != 'HEADER': stmt = RepeatHoleStmt.from_excellon(line, self._settings()) -- cgit From 6e29b9bcae8167dbb9c75e5a79e09886b952e988 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 15 Nov 2015 22:28:56 -0200 Subject: Use Python's universal newlines to open files --- 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 101c6ea..708f50b 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -51,7 +51,7 @@ def read(filename): """ # File object should use settings from source file by default. - with open(filename, 'r') as f: + with open(filename, 'rU') as f: data = f.read() settings = FileSettings(**detect_excellon_format(data)) return ExcellonParser(settings).parse(filename) @@ -326,7 +326,7 @@ class ExcellonParser(object): return len(self.hits) def parse(self, filename): - with open(filename, 'r') as f: + with open(filename, 'rU') as f: data = f.read() return self.parse_raw(data, filename) @@ -557,7 +557,7 @@ def detect_excellon_format(data=None, filename=None): if data is None and filename is None: raise ValueError('Either data or filename arguments must be provided') if data is None: - with open(filename, 'r') as f: + with open(filename, 'rU') as f: data = f.read() # Check for obvious clues: -- cgit From d69f50e0f62570a4c327cb8fe4f886f439196010 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 2 Dec 2015 12:44:30 +0800 Subject: Make the hit accessible from the drawable Hit, fix crash with cario drawing rect --- gerber/excellon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 708f50b..4ff2161 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -126,7 +126,7 @@ class ExcellonFile(CamFile): @property def primitives(self): - return [Drill(hit.position, hit.tool.diameter,units=self.settings.units) for hit in self.hits] + return [Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units) for hit in self.hits] @property -- cgit From 206f4c57ab66f8a6753015340315991b40178c9b Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 16 Dec 2015 18:59:25 +0800 Subject: Fix drawing arcs. Dont crash for arcs with rectangular apertures. Fix crash with board size of zero for only one drill --- gerber/excellon.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 4ff2161..85821e5 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -634,7 +634,11 @@ def _layer_size_score(size, hole_count, hole_area): Lower is better. """ board_area = size[0] * size[1] + if board_area == 0: + return 0 + hole_percentage = hole_area / board_area hole_score = (hole_percentage - 0.25) ** 2 size_score = (board_area - 8) **2 return hole_score * size_score + \ No newline at end of file -- cgit From 4e838df32ac6d283429e30d2a3151b7d7e8e82b2 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 19 Dec 2015 11:44:12 +0800 Subject: Parse misc nc drill files --- gerber/excellon.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 9 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 85821e5..3fb813f 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -32,6 +32,7 @@ except(ImportError): from io import StringIO from .excellon_statements import * +from .excellon_tool import ExcellonToolDefinitionParser from .cam import CamFile, FileSettings from .primitives import Drill from .utils import inch, metric @@ -56,12 +57,15 @@ def read(filename): settings = FileSettings(**detect_excellon_format(data)) return ExcellonParser(settings).parse(filename) -def loads(data): +def loads(data, settings = None, tools = None): """ Read data from string and return an ExcellonFile Parameters ---------- data : string string containing Excellon file contents + + tools: dict (optional) + externally defined tools Returns ------- @@ -70,8 +74,9 @@ def loads(data): """ # File object should use settings from source file by default. - settings = FileSettings(**detect_excellon_format(data)) - return ExcellonParser(settings).parse_raw(data) + if not settings: + settings = FileSettings(**detect_excellon_format(data)) + return ExcellonParser(settings, tools).parse_raw(data) class DrillHit(object): @@ -199,7 +204,7 @@ class ExcellonFile(CamFile): for primitive in self.primitives: primitive.to_inch() for hit in self.hits: - hit.position = tuple(map(inch, hit,position)) + hit.position = tuple(map(inch, hit.position)) def to_metric(self): @@ -282,7 +287,7 @@ class ExcellonParser(object): settings : FileSettings or dict-like Excellon file settings to use when interpreting the excellon file. """ - def __init__(self, settings=None): + def __init__(self, settings=None, ext_tools=None): self.notation = 'absolute' self.units = 'inch' self.zeros = 'leading' @@ -290,6 +295,8 @@ class ExcellonParser(object): self.state = 'INIT' self.statements = [] self.tools = {} + self.ext_tools = ext_tools or {} + self.comment_tools = {} self.hits = [] self.active_tool = None self.pos = [0., 0.] @@ -352,6 +359,18 @@ class ExcellonParser(object): detected_format = tuple([int(x) for x in comment_stmt.comment.split('=')[1].split(":")]) if detected_format: self.format = detected_format + + if "HEADER:" in comment_stmt.comment: + self.state = "HEADER" + + if " Holesize " in comment_stmt.comment: + self.state = "HEADER" + + # Parse this as a hole definition + tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment) + if len(tools) == 1: + tool = tools[tools.keys()[0]] + self.comment_tools[tool.number] = tool elif line[:3] == 'M48': self.statements.append(HeaderBeginStmt()) @@ -363,6 +382,16 @@ class ExcellonParser(object): self.state = 'DRILL' elif self.state == 'INIT': self.state = 'HEADER' + + elif line[:3] == 'M00' and self.state == 'DRILL': + if self.active_tool: + cur_tool_number = self.active_tool.number + next_tool = self._get_tool(cur_tool_number + 1) + + self.statements.append(NextToolSelectionStmt(self.active_tool, next_tool)) + self.active_tool = next_tool + else: + raise Exception('Invalid state exception') elif line[:3] == 'M95': self.statements.append(HeaderEndStmt()) @@ -480,8 +509,10 @@ class ExcellonParser(object): # T0 is used as END marker, just ignore if stmt.tool != 0: - # FIXME: for weird files with no tools defined, original calc from gerbv - if stmt.tool not in self.tools: + tool = self._get_tool(stmt.tool) + + if not tool: + # FIXME: for weird files with no tools defined, original calc from gerbv if self._settings().units == "inch": diameter = (16 + 8 * stmt.tool) / 1000.0; else: @@ -496,7 +527,7 @@ class ExcellonParser(object): self.statements.insert(i, tool) break - self.active_tool = self.tools[stmt.tool] + self.active_tool = tool elif line[0] == 'R' and self.state != 'HEADER': stmt = RepeatHoleStmt.from_excellon(line, self._settings()) @@ -523,6 +554,9 @@ class ExcellonParser(object): if y is not None: self.pos[1] += y if self.state == 'DRILL': + if not self.active_tool: + self.active_tool = self._get_tool(1) + self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) self.active_tool._hit() else: @@ -531,7 +565,23 @@ class ExcellonParser(object): def _settings(self): return FileSettings(units=self.units, format=self.format, zeros=self.zeros, notation=self.notation) - + + def _get_tool(self, toolid): + + tool = self.tools.get(toolid) + if not tool: + tool = self.comment_tools.get(toolid) + if tool: + tool.settings = self._settings() + self.tools[toolid] = tool + + if not tool: + tool = self.ext_tools.get(toolid) + if tool: + tool.settings = self._settings() + self.tools[toolid] = tool + + return tool def detect_excellon_format(data=None, filename=None): """ Detect excellon file decimal format and zero-suppression settings. -- cgit From 1cb269131bc52f0b1a1e69cef0466f2d994d52a8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 19 Dec 2015 21:54:29 -0500 Subject: Allow negative render of soldermask per #50 Update example code and rendering to show change --- gerber/excellon.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 708f50b..3bb8611 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -56,6 +56,7 @@ def read(filename): settings = FileSettings(**detect_excellon_format(data)) return ExcellonParser(settings).parse(filename) + def loads(data): """ Read data from string and return an ExcellonFile Parameters @@ -332,13 +333,13 @@ class ExcellonParser(object): def parse_raw(self, data, filename=None): for line in StringIO(data): - self._parse(line.strip()) + self._parse_line(line.strip()) for stmt in self.statements: stmt.units = self.units return ExcellonFile(self.statements, self.tools, self.hits, self._settings(), filename) - def _parse(self, line): + def _parse_line(self, line): # skip empty lines if not line.strip(): return @@ -477,7 +478,7 @@ class ExcellonParser(object): elif line[0] == 'T' and self.state != 'HEADER': stmt = ToolSelectionStmt.from_excellon(line) self.statements.append(stmt) - + # T0 is used as END marker, just ignore if stmt.tool != 0: # FIXME: for weird files with no tools defined, original calc from gerbv -- cgit From 2e42d1a4705f8cf30a9ae1f987567ce97a39ae11 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Wed, 30 Dec 2015 16:11:25 +0800 Subject: Support KiCad format statement where FMAT,2 is 2:4 with inch --- gerber/excellon.py | 1 + 1 file changed, 1 insertion(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 3fb813f..cdd6d8d 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -480,6 +480,7 @@ class ExcellonParser(object): elif line[:4] == 'FMAT': stmt = FormatStmt.from_excellon(line) self.statements.append(stmt) + self.format = stmt.format_tuple elif line[:3] == 'G40': self.statements.append(CutterCompensationOffStmt()) -- cgit From 60784dfa2107f72fcaeed739b835d647e4c3a7a9 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 16 Jan 2016 18:33:40 +0800 Subject: Skip over a strange excellon statement --- gerber/excellon.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index cdd6d8d..4317e41 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -500,9 +500,12 @@ class ExcellonParser(object): self.statements.append(infeed_rate_stmt) elif line[0] == 'T' and self.state == 'HEADER': - tool = ExcellonTool.from_excellon(line, self._settings()) - self.tools[tool.number] = tool - self.statements.append(tool) + if not ',OFF' in line and not ',ON' in line: + tool = ExcellonTool.from_excellon(line, self._settings()) + self.tools[tool.number] = tool + self.statements.append(tool) + else: + self.statements.append(UnknownStmt.from_excellon(line)) elif line[0] == 'T' and self.state != 'HEADER': stmt = ToolSelectionStmt.from_excellon(line) -- cgit From e84f131720e5952ba0dc20de8729bfd1d7aa0fe7 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 31 Jan 2016 14:17:35 +0800 Subject: Add support for more excellon formats. Dont consider line width when determinging region bounding box --- gerber/excellon.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 4317e41..4456329 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -461,6 +461,8 @@ class ExcellonParser(object): stmt = UnitStmt.from_excellon(line) self.units = stmt.units self.zeros = stmt.zeros + if stmt.format: + self.format = stmt.format self.statements.append(stmt) elif line[:3] == 'M71' or line [:3] == 'M72': -- cgit From 7053d320f0b3e9404edb4c05710001ea58d44995 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 13 Mar 2016 14:27:09 +0800 Subject: Better detection of plated tools --- gerber/excellon.py | 61 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 17 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 4456329..0637b23 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -175,21 +175,12 @@ class ExcellonFile(CamFile): 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: - 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') + self.writes(f) + + def writes(self, f): + # Copy the header verbatim + for statement in self.statements: + f.write(statement.to_excellon(self.settings) + '\n') def to_inch(self): """ @@ -300,6 +291,8 @@ class ExcellonParser(object): self.hits = [] self.active_tool = None self.pos = [0., 0.] + # Default for lated is None, which means we don't know + self.plated = ExcellonTool.PLATED_UNKNOWN if settings is not None: self.units = settings.units self.zeros = settings.zeros @@ -360,6 +353,12 @@ class ExcellonParser(object): if detected_format: self.format = detected_format + if "TYPE=PLATED" in comment_stmt.comment: + self.plated = ExcellonTool.PLATED_YES + + if "TYPE=NON_PLATED" in comment_stmt.comment: + self.plated = ExcellonTool.PLATED_NO + if "HEADER:" in comment_stmt.comment: self.state = "HEADER" @@ -370,7 +369,7 @@ class ExcellonParser(object): tools = ExcellonToolDefinitionParser(self._settings()).parse_raw(comment_stmt.comment) if len(tools) == 1: tool = tools[tools.keys()[0]] - self.comment_tools[tool.number] = tool + self._add_comment_tool(tool) elif line[:3] == 'M48': self.statements.append(HeaderBeginStmt()) @@ -503,7 +502,8 @@ class ExcellonParser(object): elif line[0] == 'T' and self.state == 'HEADER': if not ',OFF' in line and not ',ON' in line: - tool = ExcellonTool.from_excellon(line, self._settings()) + tool = ExcellonTool.from_excellon(line, self._settings(), None, self.plated) + self._merge_properties(tool) self.tools[tool.number] = tool self.statements.append(tool) else: @@ -572,6 +572,33 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zeros=self.zeros, notation=self.notation) + def _add_comment_tool(self, tool): + """ + Add a tool that was defined in the comments to this file. + + If we have already found this tool, then we will merge this comment tool definition into + the information for the tool + """ + + existing = self.tools.get(tool.number) + if existing and existing.plated == None: + existing.plated = tool.plated + + self.comment_tools[tool.number] = tool + + def _merge_properties(self, tool): + """ + When we have externally defined tools, merge the properties of that tool into this one + + For now, this is only plated + """ + + if tool.plated == ExcellonTool.PLATED_UNKNOWN: + ext_tool = self.ext_tools.get(tool.number) + + if ext_tool: + tool.plated = ext_tool.plated + def _get_tool(self, toolid): tool = self.tools.get(toolid) -- cgit From acde19f205898188c03a46e5d8a7a6a4d4637a2d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 26 Mar 2016 15:59:42 +0800 Subject: Support for the G85 slot statement --- gerber/excellon.py | 136 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 27 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 0637b23..f9bb18a 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -34,7 +34,7 @@ except(ImportError): from .excellon_statements import * from .excellon_tool import ExcellonToolDefinitionParser from .cam import CamFile, FileSettings -from .primitives import Drill +from .primitives import Drill, Slot from .utils import inch, metric @@ -93,6 +93,51 @@ class DrillHit(object): if self.tool.units == 'inch': self.tool.to_metric() self.position = tuple(map(metric, self.position)) + + @property + def bounding_box(self): + position = self.position + radius = self.tool.diameter / 2. + + min_x = position[0] - radius + max_x = position[0] + radius + min_y = position[1] - radius + max_y = position[1] + radius + return ((min_x, max_x), (min_y, max_y)) + + +class DrillSlot(object): + """ + A slot is created between two points. The way the slot is created depends on the statement used to create it + """ + + def __init__(self, tool, start, end): + self.tool = tool + self.start = start + self.end = end + + def to_inch(self): + if self.tool.units == 'metric': + self.tool.to_inch() + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) + + def to_metric(self): + if self.tool.units == 'inch': + self.tool.to_metric() + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) + + @property + def bounding_box(self): + start = self.start + end = self.end + radius = self.tool.diameter / 2. + min_x = min(start[0], end[0]) - radius + max_x = max(start[0], end[0]) + radius + min_y = min(start[1], end[1]) - radius + max_y = max(start[1], end[1]) + radius + return ((min_x, max_x), (min_y, max_y)) class ExcellonFile(CamFile): @@ -131,7 +176,17 @@ class ExcellonFile(CamFile): @property def primitives(self): - return [Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units) for hit in self.hits] + + primitives = [] + for hit in self.hits: + if isinstance(hit, DrillHit): + primitives.append(Drill(hit.position, hit.tool.diameter, hit, units=self.settings.units)) + elif isinstance(hit, DrillSlot): + primitives.append(Slot(hit.start, hit.end, hit.tool.diameter, hit, units=self.settings.units)) + else: + raise ValueError('Unknown hit type') + + return primitives @property @@ -139,12 +194,11 @@ class ExcellonFile(CamFile): xmin = ymin = 100000000000 xmax = ymax = -100000000000 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) - ymax = max(y + radius, ymax) + bbox = hit.bounding_box + xmin = min(bbox[0][0], xmin) + xmax = max(bbox[0][1], xmax) + ymin = min(bbox[1][0], ymin) + ymax = max(bbox[1][1], ymax) return ((xmin, xmax), (ymin, ymax)) def report(self, filename=None): @@ -545,26 +599,54 @@ class ExcellonParser(object): self.active_tool._hit() elif line[0] in ['X', 'Y']: - stmt = CoordinateStmt.from_excellon(line, self._settings()) - x = stmt.x - y = stmt.y - self.statements.append(stmt) - if self.notation == 'absolute': - if x is not None: - self.pos[0] = x - if y is not None: - self.pos[1] = y - else: - if x is not None: - self.pos[0] += x - if y is not None: - self.pos[1] += y - if self.state == 'DRILL': - if not self.active_tool: - self.active_tool = self._get_tool(1) + if 'G85' in line: + stmt = SlotStmt.from_excellon(line, self._settings()) - self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) - self.active_tool._hit() + # I don't know if this is actually correct, but it makes sense that this is where the tool would end + x = stmt.x_end + y = stmt.y_end + + self.statements.append(stmt) + + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + + if self.state == 'DRILL': + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end))) + self.active_tool._hit() + else: + stmt = CoordinateStmt.from_excellon(line, self._settings()) + x = stmt.x + y = stmt.y + self.statements.append(stmt) + if self.notation == 'absolute': + if x is not None: + self.pos[0] = x + if y is not None: + self.pos[1] = y + else: + if x is not None: + self.pos[0] += x + if y is not None: + self.pos[1] += y + + if self.state == 'DRILL': + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) + self.active_tool._hit() else: self.statements.append(UnknownStmt.from_excellon(line)) -- cgit From 25515b8ec7016698431b74e5beac8ff2d6691f0b Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 26 Mar 2016 18:18:16 +0800 Subject: Correctly render M15 slot holes --- gerber/excellon.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index f9bb18a..02709fd 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -345,6 +345,7 @@ class ExcellonParser(object): self.hits = [] self.active_tool = None self.pos = [0., 0.] + self.drill_down = False # Default for lated is None, which means we don't know self.plated = ExcellonTool.PLATED_UNKNOWN if settings is not None: @@ -453,12 +454,15 @@ class ExcellonParser(object): elif line[:3] == 'M15': self.statements.append(ZAxisRoutPositionStmt()) + self.drill_down = True elif line[:3] == 'M16': self.statements.append(RetractWithClampingStmt()) + self.drill_down = False elif line[:3] == 'M17': self.statements.append(RetractWithoutClampingStmt()) + self.drill_down = False elif line[:3] == 'M30': stmt = EndOfProgramStmt.from_excellon(line, self._settings()) @@ -491,6 +495,9 @@ class ExcellonParser(object): stmt = CoordinateStmt.from_excellon(line[3:], self._settings()) stmt.mode = self.state + + # The start position is where we were before the rout command + start = (self.pos[0], self.pos[1]) x = stmt.x y = stmt.y @@ -505,9 +512,20 @@ class ExcellonParser(object): self.pos[0] += x if y is not None: self.pos[1] += y - + + # Our ending position + end = (self.pos[0], self.pos[1]) + + if self.drill_down: + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillSlot(self.active_tool, start, end)) + self.active_tool._hit() + elif line[:3] == 'G05': self.statements.append(DrillModeStmt()) + self.drill_down = False self.state = 'DRILL' elif 'INCH' in line or 'METRIC' in line: -- cgit From 288f49955ecc1a811752aa4b1e713f9954e3033b Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 27 Mar 2016 14:24:11 +0800 Subject: Actually fix the rout rendering to be correct --- gerber/excellon.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 02709fd..72cf75c 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -111,10 +111,14 @@ class DrillSlot(object): A slot is created between two points. The way the slot is created depends on the statement used to create it """ - def __init__(self, tool, start, end): + TYPE_ROUT = 1 + TYPE_G85 = 2 + + def __init__(self, tool, start, end, slot_type): self.tool = tool self.start = start self.end = end + self.slot_type = slot_type def to_inch(self): if self.tool.units == 'metric': @@ -520,7 +524,7 @@ class ExcellonParser(object): if not self.active_tool: self.active_tool = self._get_tool(1) - self.hits.append(DrillSlot(self.active_tool, start, end)) + self.hits.append(DrillSlot(self.active_tool, start, end, DrillSlot.TYPE_ROUT)) self.active_tool._hit() elif line[:3] == 'G05': @@ -641,7 +645,7 @@ class ExcellonParser(object): if not self.active_tool: self.active_tool = self._get_tool(1) - self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end))) + self.hits.append(DrillSlot(self.active_tool, (stmt.x_start, stmt.y_start), (stmt.x_end, stmt.y_end), DrillSlot.TYPE_G85)) self.active_tool._hit() else: stmt = CoordinateStmt.from_excellon(line, self._settings()) -- cgit From 2eac1e427ca3264cb6dc36e0712020c1ca73fa9c Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 5 Apr 2016 22:40:12 +0800 Subject: Fix converting values for excellon files. Give error for incremental mode --- gerber/excellon.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 72cf75c..09636aa 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -85,14 +85,10 @@ class DrillHit(object): self.position = position def to_inch(self): - if self.tool.units == 'metric': - self.tool.to_inch() - self.position = tuple(map(inch, self.position)) + 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)) + self.position = tuple(map(metric, self.position)) @property def bounding_box(self): @@ -121,16 +117,12 @@ class DrillSlot(object): self.slot_type = slot_type def to_inch(self): - if self.tool.units == 'metric': - self.tool.to_inch() - self.start = tuple(map(inch, self.start)) - self.end = tuple(map(inch, self.end)) + self.start = tuple(map(inch, self.start)) + self.end = tuple(map(inch, self.end)) def to_metric(self): - if self.tool.units == 'inch': - self.tool.to_metric() - self.start = tuple(map(metric, self.start)) - self.end = tuple(map(metric, self.end)) + self.start = tuple(map(metric, self.start)) + self.end = tuple(map(metric, self.end)) @property def bounding_box(self): @@ -253,7 +245,7 @@ class ExcellonFile(CamFile): for primitive in self.primitives: primitive.to_inch() for hit in self.hits: - hit.position = tuple(map(inch, hit.position)) + hit.to_inch() def to_metric(self): @@ -268,7 +260,7 @@ class ExcellonFile(CamFile): for primitive in self.primitives: primitive.to_metric() for hit in self.hits: - hit.position = tuple(map(metric, hit.position)) + hit.to_metric() def offset(self, x_offset=0, y_offset=0): for statement in self.statements: -- cgit From f1f07d74c41ad74be2b0bbad4cfcd1c6e5923678 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Tue, 10 May 2016 23:16:51 +0800 Subject: Offset of drill hit and slots --- gerber/excellon.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 09636aa..9a69042 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -100,7 +100,9 @@ class DrillHit(object): min_y = position[1] - radius max_y = position[1] + radius return ((min_x, max_x), (min_y, max_y)) - + + def offset(self, x_offset, y_offset): + self.position = tuple(map(operator.add, self.position, (x_offset, y_offset))) class DrillSlot(object): """ @@ -134,6 +136,10 @@ class DrillSlot(object): min_y = min(start[1], end[1]) - radius max_y = max(start[1], end[1]) + radius return ((min_x, max_x), (min_y, max_y)) + + def offset(self, x_offset, y_offset): + self.start = tuple(map(operator.add, self.start, (x_offset, y_offset))) + self.end = tuple(map(operator.add, self.end, (x_offset, y_offset))) class ExcellonFile(CamFile): @@ -268,7 +274,7 @@ class ExcellonFile(CamFile): for primitive in self.primitives: primitive.offset(x_offset, y_offset) for hit in self. hits: - hit.position = tuple(map(operator.add, hit.position, (x_offset, y_offset))) + hit.offset(x_offset, y_offset) def path_length(self, tool_number=None): """ Return the path length for a given tool -- cgit From 8f4b439efcc4dccd327a8fb95ce3bbb6d16adbcf Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Mon, 6 Jun 2016 22:26:06 +0800 Subject: Rout mode doesn't need to specify G01 every time --- gerber/excellon.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 9a69042..a0a639e 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -498,7 +498,7 @@ class ExcellonParser(object): stmt = CoordinateStmt.from_excellon(line[3:], self._settings()) stmt.mode = self.state - # The start position is where we were before the rout command + # The start position is where we were before the rout command start = (self.pos[0], self.pos[1]) x = stmt.x @@ -647,6 +647,10 @@ class ExcellonParser(object): self.active_tool._hit() else: stmt = CoordinateStmt.from_excellon(line, self._settings()) + + # We need this in case we are in rout mode + start = (self.pos[0], self.pos[1]) + x = stmt.x y = stmt.y self.statements.append(stmt) @@ -667,6 +671,13 @@ class ExcellonParser(object): self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) self.active_tool._hit() + + elif self.state == 'LINEAR' and self.drill_down: + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT)) + else: self.statements.append(UnknownStmt.from_excellon(line)) -- cgit From 7e06f3a2f5870d4878f25e391372285263fe5ac6 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sun, 10 Jul 2016 15:41:31 +0800 Subject: Workaround for bad excellon files that don't correctly set the mode --- gerber/excellon.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index a0a639e..becf82d 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -665,19 +665,21 @@ class ExcellonParser(object): if y is not None: self.pos[1] += y - if self.state == 'DRILL': + if self.state == 'LINEAR' and self.drill_down: + if not self.active_tool: + self.active_tool = self._get_tool(1) + + self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT)) + + elif self.state == 'DRILL' or self.state == 'HEADER': + # Yes, drills in the header doesn't follow the specification, but it there are many + # files like this if not self.active_tool: self.active_tool = self._get_tool(1) self.hits.append(DrillHit(self.active_tool, tuple(self.pos))) self.active_tool._hit() - elif self.state == 'LINEAR' and self.drill_down: - if not self.active_tool: - self.active_tool = self._get_tool(1) - - self.hits.append(DrillSlot(self.active_tool, start, tuple(self.pos), DrillSlot.TYPE_ROUT)) - else: self.statements.append(UnknownStmt.from_excellon(line)) -- cgit From 10c7075ad5fc05907e53036b2e308cfc372476c7 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Mon, 11 Jul 2016 23:18:15 +0800 Subject: Allow G85 for invalid files --- gerber/excellon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index becf82d..65e676b 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -639,7 +639,7 @@ class ExcellonParser(object): if y is not None: self.pos[1] += y - if self.state == 'DRILL': + if self.state == 'DRILL' or self.state == 'HEADER': if not self.active_tool: self.active_tool = self._get_tool(1) -- cgit From 7a79d1504e348251740efe622b4018cc26ffcd59 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 16 Jul 2016 14:22:38 +0800 Subject: Setup .gitignore for Eclipse. Start creating doc strings --- gerber/excellon.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index 65e676b..bcd136e 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -80,6 +80,16 @@ def loads(data, settings = None, tools = None): class DrillHit(object): + """Drill feature that is a single drill hole. + + Attributes + ---------- + tool : ExcellonTool + Tool to drill the hole. Defines the size of the hole that is generated. + position : tuple(float, float) + Center position of the drill. + + """ def __init__(self, tool, position): self.tool = tool self.position = position -- cgit From 52c6d4928a1b5fc65b95cf5b0784a560cec2ca1d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 16 Jul 2016 15:49:48 +0800 Subject: Fix most broken tests so that I can safely merge into changes with known expected test result --- gerber/excellon.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index bcd136e..430ee7d 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -113,6 +113,9 @@ class DrillHit(object): def offset(self, x_offset, y_offset): self.position = tuple(map(operator.add, self.position, (x_offset, y_offset))) + + def __str__(self): + return 'Hit (%f, %f) {%s}' % (self.position[0], self.position[1], self.tool) class DrillSlot(object): """ -- cgit From 8cd842a41a55ab3d8f558a2e3e198beba7da58a1 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Manually mere rendering changes --- gerber/excellon.py | 94 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 40 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index a0bad4f..a5da42a 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -81,7 +81,7 @@ def loads(data, settings = None, tools = None): return ExcellonParser(settings, tools).parse_raw(data) -class DrillHit(object): +class DrillHit(object): """Drill feature that is a single drill hole. Attributes @@ -92,6 +92,7 @@ class DrillHit(object): Center position of the drill. """ + def __init__(self, tool, position): self.tool = tool self.position = position @@ -184,6 +185,7 @@ class ExcellonFile(CamFile): either 'inch' or 'metric'. """ + def __init__(self, statements, tools, hits, settings, filename=None): super(ExcellonFile, self).__init__(statements=statements, settings=settings, @@ -193,7 +195,9 @@ class ExcellonFile(CamFile): @property def primitives(self): - + """ + Gets the primitives. Note that unlike Gerber, this generates new objects + """ primitives = [] for hit in self.hits: if isinstance(hit, DrillHit): @@ -203,8 +207,7 @@ class ExcellonFile(CamFile): else: raise ValueError('Unknown hit type') - return primitives - + return primitives @property def bounds(self): @@ -237,7 +240,8 @@ 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.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) @@ -245,13 +249,21 @@ class ExcellonFile(CamFile): def write(self, filename=None): filename = filename if filename is not None else self.filename - with open(filename, 'w') as f: - self.writes(f) - - def writes(self, f): - # Copy the header verbatim - for statement in self.statements: - f.write(statement.to_excellon(self.settings) + '\n') + 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 + + # 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): """ @@ -265,9 +277,8 @@ class ExcellonFile(CamFile): tool.to_inch() for primitive in self.primitives: primitive.to_inch() - for hit in self.hits: - hit.to_inch() - + for hit in self.hits: + hit.to_inch() def to_metric(self): """ Convert units to metric @@ -288,8 +299,8 @@ class ExcellonFile(CamFile): statement.offset(x_offset, y_offset) for primitive in self.primitives: primitive.offset(x_offset, y_offset) - for hit in self. hits: - hit.offset(x_offset, y_offset) + for hit in self. hits: + hit.offset(x_offset, y_offset) def path_length(self, tool_number=None): """ Return the path length for a given tool @@ -299,9 +310,11 @@ class ExcellonFile(CamFile): for hit in self.hits: tool = hit.tool num = tool.number - positions[num] = (0, 0) if positions.get(num) is None else positions[num] + 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))) + lengths[num] = lengths[ + num] + math.hypot(*tuple(map(operator.sub, positions[num], hit.position))) positions[num] = hit.position if tool_number is None: @@ -310,13 +323,13 @@ class ExcellonFile(CamFile): 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) + 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 @@ -340,7 +353,6 @@ class ExcellonFile(CamFile): hit.tool = newtool - class ExcellonParser(object): """ Excellon File Parser @@ -348,8 +360,8 @@ class ExcellonParser(object): ---------- settings : FileSettings or dict-like Excellon file settings to use when interpreting the excellon file. - """ - def __init__(self, settings=None, ext_tools=None): + """ + def __init__(self, settings=None, ext_tools=None): self.notation = 'absolute' self.units = 'inch' self.zeros = 'leading' @@ -371,7 +383,6 @@ class ExcellonParser(object): self.notation = settings.notation self.format = settings.format - @property def coordinates(self): return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)] @@ -421,7 +432,8 @@ class ExcellonParser(object): # get format from altium comment if "FILE_FORMAT" in comment_stmt.comment: - detected_format = tuple([int(x) for x in comment_stmt.comment.split('=')[1].split(":")]) + detected_format = tuple( + [int(x) for x in comment_stmt.comment.split('=')[1].split(":")]) if detected_format: self.format = detected_format @@ -553,7 +565,7 @@ class ExcellonParser(object): self.format = stmt.format self.statements.append(stmt) - elif line[:3] == 'M71' or line [:3] == 'M72': + elif line[:3] == 'M71' or line[:3] == 'M72': stmt = MeasuringModeStmt.from_excellon(line) self.units = stmt.units self.statements.append(stmt) @@ -603,20 +615,22 @@ class ExcellonParser(object): self.statements.append(stmt) # T0 is used as END marker, just ignore - if stmt.tool != 0: + if stmt.tool != 0: tool = self._get_tool(stmt.tool) if not tool: - # FIXME: for weird files with no tools defined, original calc from gerbv + # FIXME: for weird files with no tools defined, original calc from gerbv if self._settings().units == "inch": - diameter = (16 + 8 * stmt.tool) / 1000.0; + diameter = (16 + 8 * stmt.tool) / 1000.0 else: - diameter = metric((16 + 8 * stmt.tool) / 1000.0); + diameter = metric((16 + 8 * stmt.tool) / 1000.0) - tool = ExcellonTool(self._settings(), number=stmt.tool, diameter=diameter) + tool = ExcellonTool( + self._settings(), number=stmt.tool, diameter=diameter) self.tools[tool.number] = tool - # FIXME: need to add this tool definition inside header to make sure it is properly written + # FIXME: need to add this tool definition inside header to + # make sure it is properly written for i, s in enumerate(self.statements): if isinstance(s, ToolSelectionStmt) or isinstance(s, ExcellonTool): self.statements.insert(i, tool) @@ -787,7 +801,7 @@ def detect_excellon_format(data=None, filename=None): and 'FILE_FORMAT' in stmt.comment] detected_format = (tuple([int(val) for val in - format_comment[0].split('=')[1].split(':')]) + format_comment[0].split('=')[1].split(':')]) if len(format_comment) == 1 else None) detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None @@ -852,6 +866,6 @@ def _layer_size_score(size, hole_count, hole_area): hole_percentage = hole_area / board_area hole_score = (hole_percentage - 0.25) ** 2 - size_score = (board_area - 8) **2 + size_score = (board_area - 8) ** 2 return hole_score * size_score \ No newline at end of file -- cgit From 5af19af190c1fb0f0c5be029d46d63e657dde4d9 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Commit partial merge so I can work on the plane --- gerber/excellon.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'gerber/excellon.py') diff --git a/gerber/excellon.py b/gerber/excellon.py index a5da42a..0626819 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -80,7 +80,7 @@ def loads(data, settings = None, tools = None): settings = FileSettings(**detect_excellon_format(data)) return ExcellonParser(settings, tools).parse_raw(data) - + class DrillHit(object): """Drill feature that is a single drill hole. @@ -91,8 +91,7 @@ class DrillHit(object): position : tuple(float, float) Center position of the drill. - """ - + """ def __init__(self, tool, position): self.tool = tool self.position = position @@ -194,7 +193,7 @@ class ExcellonFile(CamFile): self.hits = hits @property - def primitives(self): + def primitives(self): """ Gets the primitives. Note that unlike Gerber, this generates new objects """ @@ -262,7 +261,7 @@ class ExcellonFile(CamFile): for hit in self.hits: if hit.tool.number == tool.number: f.write(CoordinateStmt( - *hit.position).to_excellon(self.settings) + '\n') + *hit.position).to_excellon(self.settings) + '\n') f.write(EndOfProgramStmt().to_excellon() + '\n') def to_inch(self): @@ -276,7 +275,7 @@ class ExcellonFile(CamFile): for tool in iter(self.tools.values()): tool.to_inch() for primitive in self.primitives: - primitive.to_inch() + primitive.to_inch() for hit in self.hits: hit.to_inch() @@ -298,7 +297,7 @@ class ExcellonFile(CamFile): for statement in self.statements: statement.offset(x_offset, y_offset) for primitive in self.primitives: - primitive.offset(x_offset, y_offset) + primitive.offset(x_offset, y_offset) for hit in self. hits: hit.offset(x_offset, y_offset) @@ -359,7 +358,7 @@ class ExcellonParser(object): Parameters ---------- settings : FileSettings or dict-like - Excellon file settings to use when interpreting the excellon file. + Excellon file settings to use when interpreting the excellon file. """ def __init__(self, settings=None, ext_tools=None): self.notation = 'absolute' @@ -614,12 +613,12 @@ class ExcellonParser(object): stmt = ToolSelectionStmt.from_excellon(line) self.statements.append(stmt) - # T0 is used as END marker, just ignore + # T0 is used as END marker, just ignore if stmt.tool != 0: tool = self._get_tool(stmt.tool) if not tool: - # FIXME: for weird files with no tools defined, original calc from gerbv + # FIXME: for weird files with no tools defined, original calc from gerb if self._settings().units == "inch": diameter = (16 + 8 * stmt.tool) / 1000.0 else: -- cgit