diff options
Diffstat (limited to 'gerber/rs274x.py')
-rw-r--r-- | gerber/rs274x.py | 190 |
1 files changed, 161 insertions, 29 deletions
diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 911b8ae..5d64597 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -21,6 +21,7 @@ import copy import json import re +import sys try: from cStringIO import StringIO @@ -30,6 +31,7 @@ except(ImportError): from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings +from .utils import sq_distance def read(filename): @@ -97,9 +99,11 @@ class GerberFile(CamFile): """ - def __init__(self, statements, settings, primitives, filename=None): + def __init__(self, statements, settings, primitives, apertures, filename=None): super(GerberFile, self).__init__(statements, settings, primitives, filename) + self.apertures = apertures + @property def comments(self): return [comment.comment for comment in self.statements @@ -114,13 +118,31 @@ class GerberFile(CamFile): def bounds(self): min_x = min_y = 1000000 max_x = max_y = -1000000 + for stmt in [stmt for stmt in self.statements if isinstance(stmt, CoordStmt)]: if stmt.x is not None: min_x = min(stmt.x, min_x) max_x = max(stmt.x, max_x) + if stmt.y is not None: min_y = min(stmt.y, min_y) max_y = max(stmt.y, max_y) + + return ((min_x, max_x), (min_y, max_y)) + + @property + def bounding_box(self): + min_x = min_y = 1000000 + max_x = max_y = -1000000 + + for prim in self.primitives: + bounds = prim.bounding_box + min_x = min(bounds[0][0], min_x) + max_x = max(bounds[0][1], max_x) + + min_y = min(bounds[1][0], min_y) + max_y = max(bounds[1][1], max_y) + return ((min_x, max_x), (min_y, max_y)) def write(self, filename, settings=None): @@ -162,14 +184,14 @@ class GerberParser(object): STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+" - FS = r"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])" + FS = r"(?P<param>FS)(?P<zero>(L|T|D))?(?P<notation>(A|I))[NG0-9]*X(?P<x>[0-7][0-7])Y(?P<y>[0-7][0-7])[DM0-9]*" MO = r"(?P<param>MO)(?P<mo>(MM|IN))" LP = r"(?P<param>LP)(?P<lp>(D|C))" - AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)?" + AD_CIRCLE = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>C)[,]?(?P<modifiers>[^,%]*)" AD_RECT = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>R)[,](?P<modifiers>[^,%]*)" AD_OBROUND = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>O)[,](?P<modifiers>[^,%]*)" AD_POLY = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>P)[,](?P<modifiers>[^,%]*)" - AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)?".format(name=NAME) + AD_MACRO = r"(?P<param>AD)D(?P<d>\d+)(?P<shape>{name})[,]?(?P<modifiers>[^,%]*)".format(name=NAME) AM = r"(?P<param>AM)(?P<name>{name})\*(?P<macro>[^%]*)".format(name=NAME) # begin deprecated @@ -233,8 +255,7 @@ class GerberParser(object): return self.parse_raw(data, filename) def parse_raw(self, data, filename=None): - lines = [line for line in StringIO(data)] - for stmt in self._parse(lines): + for stmt in self._parse(self._split_commands(data)): self.evaluate(stmt) self.statements.append(stmt) @@ -242,7 +263,38 @@ class GerberParser(object): for stmt in self.statements: stmt.units = self.settings.units - return GerberFile(self.statements, self.settings, self.primitives, filename) + return GerberFile(self.statements, self.settings, self.primitives, self.apertures.values(), filename) + + def _split_commands(self, data): + """ + Split the data into commands. Commands end with * (and also newline to help with some badly formatted files) + """ + + length = len(data) + start = 0 + in_header = True + + for cur in range(0, length): + + val = data[cur] + + if val == '%' and start == cur: + in_header = True + continue + + if val == '\r' or val == '\n': + if start != cur: + yield data[start:cur] + start = cur + 1 + + elif not in_header and val == '*': + yield data[start:cur + 1] + start = cur + 1 + + elif in_header and val == '%': + yield data[start:cur + 1] + start = cur + 1 + in_header = False def dump_json(self): stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} @@ -257,7 +309,7 @@ class GerberParser(object): def _parse(self, data): oldline = '' - for i, line in enumerate(data): + for line in data: line = oldline + line.strip() # skip empty lines @@ -273,6 +325,12 @@ class GerberParser(object): while did_something and len(line) > 0: did_something = False + # consume empty data blocks + if line[0] == '*': + line = line[1:] + did_something = True + continue + # coord (coord, r) = _match_one(self.COORD_STMT, line) if coord: @@ -285,7 +343,6 @@ class GerberParser(object): (aperture, r) = _match_one(self.APERTURE_STMT, line) if aperture: yield ApertureStmt(**aperture) - did_something = True line = r continue @@ -309,7 +366,9 @@ class GerberParser(object): elif param["param"] == "AD": yield ADParamStmt.from_dict(param) elif param["param"] == "AM": - yield AMParamStmt.from_dict(param) + stmt = AMParamStmt.from_dict(param) + stmt.units = self.settings.units + yield stmt elif param["param"] == "OF": yield OFParamStmt.from_dict(param) elif param["param"] == "IN": @@ -432,19 +491,46 @@ class GerberParser(object): aperture = None if shape == 'C': diameter = modifiers[0][0] - aperture = Circle(position=None, diameter=diameter) + + if len(modifiers[0]) >= 2: + hole_diameter = modifiers[0][1] + else: + hole_diameter = None + + aperture = Circle(position=None, diameter=diameter, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'R': width = modifiers[0][0] height = modifiers[0][1] - aperture = Rectangle(position=None, width=width, height=height) + + if len(modifiers[0]) >= 3: + hole_diameter = modifiers[0][2] + else: + hole_diameter = None + + aperture = Rectangle(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'O': width = modifiers[0][0] height = modifiers[0][1] - aperture = Obround(position=None, width=width, height=height) + + if len(modifiers[0]) >= 3: + hole_diameter = modifiers[0][2] + else: + hole_diameter = None + + aperture = Obround(position=None, width=width, height=height, hole_diameter=hole_diameter, units=self.settings.units) elif shape == 'P': - diameter = modifiers[0][0] - sides = modifiers[0][1] - aperture = Polygon(position=None, radius=diameter/2.0, sides=sides) + outer_diameter = modifiers[0][0] + number_vertices = int(modifiers[0][1]) + if len(modifiers[0]) > 2: + rotation = modifiers[0][2] + else: + rotation = 0 + + if len(modifiers[0]) > 3: + hole_diameter = modifiers[0][3] + else: + hole_diameter = None + aperture = Polygon(position=None, sides=number_vertices, radius=outer_diameter/2.0, hole_diameter=hole_diameter, rotation=rotation) else: aperture = self.macros[shape].build(modifiers) @@ -454,8 +540,11 @@ class GerberParser(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self.primitives.append(Region(self.current_region, - level_polarity=self.level_polarity)) + # Sometimes we have regions that have no points. Skip those + if self.current_region: + self.primitives.append(Region(self.current_region, + level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = None self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': @@ -488,13 +577,19 @@ class GerberParser(object): self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') + if stmt.only_function: + # Sometimes we get a coordinate statement + # that only sets the function. If so, don't + # try futher otherwise that might draw/flash something + return + if stmt.op: self.op = stmt.op else: # no implicit op allowed, force here if coord block doesn't have it stmt.op = self.op - if self.op == "D01": + if self.op == "D01" or self.op == "D1": start = (self.x, self.y) end = (x, y) @@ -507,9 +602,10 @@ class GerberParser(object): else: # from gerber spec revision J3, Section 4.5, page 55: # The segments are not graphics objects in themselves; segments are part of region which is the graphics object. The segments have no thickness. - # The current aperture is associated with the region. This - # has no graphical effect, but allows all its attributes to + # The current aperture is associated with the region. + # This has no graphical effect, but allows all its attributes to # be applied to the region. + if self.current_region is None: self.current_region = [Line(start, end, self.apertures.get(self.aperture, @@ -525,30 +621,37 @@ class GerberParser(object): else: i = 0 if stmt.i is None else stmt.i j = 0 if stmt.j is None else stmt.j - center = (start[0] + i, start[1] + j) + center = self._find_center(start, end, (i, j)) if self.region_mode == 'off': self.primitives.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], + quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) else: if self.current_region is None: self.current_region = [Arc(start, end, center, self.direction, - self.apertures[self.aperture], + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, - units=self.settings.units), ] + units=self.settings.units),] else: self.current_region.append(Arc(start, end, center, self.direction, - self.apertures[self.aperture], + self.apertures.get(self.aperture, Circle((0,0), 0)), + quadrant_mode=self.quadrant_mode, level_polarity=self.level_polarity, units=self.settings.units)) - elif self.op == "D02": - pass + elif self.op == "D02" or self.op == "D2": - elif self.op == "D03": - primitive = copy.deepcopy(self.apertures[self.aperture]) + if self.region_mode == "on": + # D02 in the middle of a region finishes that region and starts a new one + if self.current_region and len(self.current_region) > 1: + self.primitives.append(Region(self.current_region, level_polarity=self.level_polarity, units=self.settings.units)) + self.current_region = None + elif self.op == "D03" or self.op == "D3": + primitive = copy.deepcopy(self.apertures[self.aperture]) if primitive is not None: @@ -567,6 +670,35 @@ class GerberParser(object): self.primitives.append(renderable) self.x, self.y = x, y + def _find_center(self, start, end, offsets): + """ + In single quadrant mode, the offsets are always positive, which means there are 4 possible centers. + The correct center is the only one that results in an arc with sweep angle of less than or equal to 90 degrees + """ + + if self.quadrant_mode == 'single-quadrant': + + # The Gerber spec says single quadrant only has one possible center, and you can detect + # based on the angle. But for real files, this seems to work better - there is usually + # only one option that makes sense for the center (since the distance should be the same + # from start and end). Find the center that makes the most sense + sqdist_diff_min = sys.maxint + center = None + for factors in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: + + test_center = (start[0] + offsets[0] * factors[0], start[1] + offsets[1] * factors[1]) + + sqdist_start = sq_distance(start, test_center) + sqdist_end = sq_distance(end, test_center) + + if abs(sqdist_start - sqdist_end) < sqdist_diff_min: + center = test_center + sqdist_diff_min = abs(sqdist_start - sqdist_end) + + return center + else: + return (start[0] + offsets[0], start[1] + offsets[1]) + def _evaluate_aperture(self, stmt): self.aperture = stmt.d |