From bf5a2968e227af90c9742a585939583f5d3da738 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 22 Jan 2022 20:51:22 +0100 Subject: Fix a bunch of bugs --- gerbonara/gerber/aperture_macros/parse.py | 2 +- gerbonara/gerber/aperture_macros/primitive.py | 2 +- gerbonara/gerber/apertures.py | 2 +- gerbonara/gerber/gerber_primitives.py | 14 ---- gerbonara/gerber/graphic_objects.py | 24 +++--- gerbonara/gerber/graphic_primitives.py | 4 +- gerbonara/gerber/rs274x.py | 106 +++++++++++++++++--------- 7 files changed, 87 insertions(+), 67 deletions(-) delete mode 100644 gerbonara/gerber/gerber_primitives.py diff --git a/gerbonara/gerber/aperture_macros/parse.py b/gerbonara/gerber/aperture_macros/parse.py index c1aa2d0..2126e0f 100644 --- a/gerbonara/gerber/aperture_macros/parse.py +++ b/gerbonara/gerber/aperture_macros/parse.py @@ -94,7 +94,7 @@ class ApertureMacro: return f'' def __eq__(self, other): - return hasattr(other, to_gerber) and self.to_gerber() == other.to_gerber() + return hasattr(other, 'to_gerber') and self.to_gerber() == other.to_gerber() def __hash__(self): return hash(self.to_gerber()) diff --git a/gerbonara/gerber/aperture_macros/primitive.py b/gerbonara/gerber/aperture_macros/primitive.py index 76268d2..7832960 100644 --- a/gerbonara/gerber/aperture_macros/primitive.py +++ b/gerbonara/gerber/aperture_macros/primitive.py @@ -223,7 +223,7 @@ class Outline(Primitive): self.coords = [(UnitExpression(x, unit), UnitExpression(y, unit)) for x, y in zip(args[1::2], args[2::2])] def to_gerber(self, unit=None): - coords = ','.join(coord.to_gerber(unit) for coord in self.coords) + coords = ','.join(coord.to_gerber(unit) for xy in self.coords for coord in xy) return f'{self.code},{self.exposure.to_gerber()},{len(self.coords)//2-1},{coords},{self.rotation.to_gerber()}' def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None): diff --git a/gerbonara/gerber/apertures.py b/gerbonara/gerber/apertures.py index 872de9f..1e6c555 100644 --- a/gerbonara/gerber/apertures.py +++ b/gerbonara/gerber/apertures.py @@ -81,7 +81,7 @@ class Aperture: def __eq__(self, other): # We need to choose some unit here. - return hasattr(other, to_gerber) and self.to_gerber(MM) == other.to_gerber(MM) + return hasattr(other, 'to_gerber') and self.to_gerber(MM) == other.to_gerber(MM) def _rotate_hole_90(self): if self.hole_rect_h is None: diff --git a/gerbonara/gerber/gerber_primitives.py b/gerbonara/gerber/gerber_primitives.py deleted file mode 100644 index 1a40948..0000000 --- a/gerbonara/gerber/gerber_primitives.py +++ /dev/null @@ -1,14 +0,0 @@ - -from apertures import * - -class GerberPrimitive: - pass - - def to_graphic_primitives(self): - pass - -class Line(GerberPrimitive): - pass - -class Arc(GerberPrimitive): - pass diff --git a/gerbonara/gerber/graphic_objects.py b/gerbonara/gerber/graphic_objects.py index b42a01c..29f0d38 100644 --- a/gerbonara/gerber/graphic_objects.py +++ b/gerbonara/gerber/graphic_objects.py @@ -82,7 +82,7 @@ class Flash(GerberObject): x = gs.file_settings.write_gerber_value(self.x, self.unit) y = gs.file_settings.write_gerber_value(self.y, self.unit) - yield f'D03X{x}Y{y}*' + yield f'X{x}Y{y}D03*' gs.update_point(self.x, self.y, unit=self.unit) @@ -122,8 +122,8 @@ class Region(GerberObject): def _rotate(self, angle, cx=0, cy=0): self.poly.outline = [ gp.rotate_point(x, y, angle, cx, cy) for x, y in self.poly.outline ] self.poly.arc_centers = [ - (arc[0], gp.rotate_point(*arc[1], angle, cx, cy)) if arc else None - for arc in self.poly.arc_centers ] + (arc[0], gp.rotate_point(*arc[1], angle, cx-p[0], cy-p[1])) if arc else None + for p, arc in zip(self.poly.outline, self.poly.arc_centers) ] def append(self, obj): if obj.unit != self.unit: @@ -133,7 +133,7 @@ class Region(GerberObject): self.poly.outline.append(obj.p2) if isinstance(obj, Arc): - self.poly.arc_centers.append((obj.clockwise, obj.center)) + self.poly.arc_centers.append((obj.clockwise, obj.center_relative)) else: self.poly.arc_centers.append(None) @@ -162,7 +162,7 @@ class Region(GerberObject): x = gs.file_settings.write_gerber_value(point[0], self.unit) y = gs.file_settings.write_gerber_value(point[1], self.unit) - yield f'D01X{x}Y{y}*' + yield f'X{x}Y{y}D01*' gs.update_point(*point, unit=self.unit) @@ -174,9 +174,9 @@ class Region(GerberObject): x = gs.file_settings.write_gerber_value(x2, self.unit) y = gs.file_settings.write_gerber_value(y2, self.unit) # TODO are these coordinates absolute or relative now?! - i = gs.file_settings.write_gerber_value(cx-x2, self.unit) - j = gs.file_settings.write_gerber_value(cy-y2, self.unit) - yield f'D01X{x}Y{y}I{i}J{j}*' + i = gs.file_settings.write_gerber_value(cx, self.unit) + j = gs.file_settings.write_gerber_value(cy, self.unit) + yield f'X{x}Y{y}I{i}J{j}D01*' gs.update_point(x2, y2, unit=self.unit) @@ -236,7 +236,7 @@ class Line(GerberObject): x = gs.file_settings.write_gerber_value(self.x2, self.unit) y = gs.file_settings.write_gerber_value(self.y2, self.unit) - yield f'D01X{x}Y{y}*' + yield f'X{x}Y{y}D01*' gs.update_point(*self.p2, unit=self.unit) @@ -281,6 +281,10 @@ class Arc(GerberObject): def center(self): return self.cx + self.x1, self.cy + self.y1 + @property + def center_relative(self): + return self.cx, self.cy + @property def end_point(self): return self.p2 @@ -324,7 +328,7 @@ class Arc(GerberObject): y = gs.file_settings.write_gerber_value(self.y2, self.unit) i = gs.file_settings.write_gerber_value(self.cx, self.unit) j = gs.file_settings.write_gerber_value(self.cy, self.unit) - yield f'D01X{x}Y{y}I{i}J{j}*' + yield f'X{x}Y{y}I{i}J{j}D01*' gs.update_point(*self.p2, unit=self.unit) diff --git a/gerbonara/gerber/graphic_primitives.py b/gerbonara/gerber/graphic_primitives.py index 644071c..629c1c2 100644 --- a/gerbonara/gerber/graphic_primitives.py +++ b/gerbonara/gerber/graphic_primitives.py @@ -213,8 +213,8 @@ class ArcPoly(GraphicPrimitive): bbox = (None, None), (None, None) for (x1, y1), (x2, y2), arc in self.segments: if arc: - clockwise, center = arc - bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, *center, clockwise)) + clockwise, (cx, cy) = arc + bbox = add_bounds(bbox, arc_bounds(x1, y1, x2, y2, (cx+x1, cy+y1), clockwise)) else: line_bounds = (min(x1, x2), min(y1, y2)), (max(x1, x2), max(y1, y2)) diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py index 42e4230..6d373ba 100644 --- a/gerbonara/gerber/rs274x.py +++ b/gerbonara/gerber/rs274x.py @@ -364,10 +364,14 @@ class GraphicsState: self.point = (0, 0) old_point = self.map_coord(*self.update_point(x, y)) - if aperture and math.isclose(self.aperture.equivalent_width(), 0): - warnings.warn('D01 interpolation with a zero-size aperture. This is invalid according to spec, however, we ' - 'pass through the created objects here. Note that these will not show up in e.g. SVG output since ' - 'their line width is zero.', SyntaxWarning) + if aperture: + if not self.aperture: + raise SyntaxError('Interpolation attempted without selecting aperture first') + + if math.isclose(self.aperture.equivalent_width(), 0): + warnings.warn('D01 interpolation with a zero-size aperture. This is invalid according to spec, ' + 'however, we pass through the created objects here. Note that these will not show up in e.g. ' + 'SVG output since their line width is zero.', SyntaxWarning) if self.interpolation_mode == InterpMode.LINEAR: if i is not None or j is not None: @@ -431,7 +435,7 @@ class GraphicsState: self.point = point_mm x = self.file_settings.write_gerber_value(point[0], unit=unit) y = self.file_settings.write_gerber_value(point[1], unit=unit) - yield f'D02X{x}Y{y}*' + yield f'X{x}Y{y}D02*' def set_interpolation_mode(self, mode): if self.interpolation_mode != mode: @@ -445,14 +449,18 @@ class GerberParser: NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+" STATEMENT_REGEXES = { - 'unit_mode': r"MO(?P(MM|IN))", - 'interpolation_mode': r"(?PG0?[123]|G74|G75)$", - 'coord': fr"(X(?P{NUMBER}))?(Y(?P{NUMBER}))?" \ + 'region_start': r'G36$', + 'region_end': r'G37$', + 'coord': fr"(?PG0?[123]|G74|G75)?(X(?P{NUMBER}))?(Y(?P{NUMBER}))?" \ fr"(I(?P{NUMBER}))?(J(?P{NUMBER}))?" \ fr"(?PD0?[123])$", 'aperture': r"(G54|G55)?D(?P\d+)", 'comment': r"G0?4(?P[^*]*)", + # Allegro combines format spec and unit into one long illegal extended command. + 'allegro_format_spec': r"FS(?P(L|T|D))?(?P(A|I))[NG0-9]*X(?P[0-7][0-7])Y(?P[0-7][0-7])[DM0-9]*\*MO(?PIN|MM)", + 'unit_mode': r"MO(?P(MM|IN))", 'format_spec': r"FS(?P(L|T|D))?(?P(A|I))[NG0-9]*X(?P[0-7][0-7])Y(?P[0-7][0-7])[DM0-9]*", + 'allegro_legacy_params': r'IR(?P[0-9]+)\*IP(?P(POS|NEG))\*OF(A(?P{DECIMAL}))?(B(?P{DECIMAL}))?\*MI(A(?P0|1))?(B(?P0|1))?\*SF(A(?P{DECIMAL}))?(B(?P{DECIMAL}))?', 'load_polarity': r"LP(?P(D|C))", # FIXME LM, LR, LS 'load_name': r"LN(?P.*)", @@ -462,12 +470,10 @@ class GerberParser: 'axis_selection': r"AS(?PAXBY|AYBX)", 'image_polarity': r"IP(?P(POS|NEG))", 'image_rotation': fr"IR(?P{NUMBER})", - 'mirror_image': r"MI(A(?P0|1))?(B(?P0|1))?", - 'scale_factor': fr"SF(A(?P{DECIMAL}))?(B(?P{DECIMAL}))?", + 'mirror_image': r"MI(A(?P0|1))?(B(?P0|1))?", + 'scale_factor': fr"SF(A(?P{DECIMAL}))?(B(?P{DECIMAL}))?", 'aperture_definition': fr"ADD(?P\d+)(?PC|R|O|P|{NAME})(?P,[^,%]*)?$", 'aperture_macro': fr"AM(?P{NAME})\*(?P[^%]*)", - 'region_start': r'G36', - 'region_end': r'G37', 'old_unit':r'(?PG7[01])', 'old_notation': r'(?PG9[01])', 'eof': r"M0?[02]", @@ -502,11 +508,15 @@ class GerberParser: start = 0 extended_command = False + lineno = 1 for pos, c in enumerate(data): + if c == '\n': + lineno += 1 + if c == '%': if extended_command: - yield data[start:pos] + yield lineno, data[start:pos] extended_command = False else: @@ -518,14 +528,14 @@ class GerberParser: elif extended_command: continue - if c == '\r' or c == '\n' or c == '*': + if c in '*\r\n': word_command = data[start:pos].strip() if word_command and word_command != '*': - yield word_command + yield lineno, word_command start = pos + 1 def parse(self, data): - for line in self._split_commands(data): + for lineno, line in self._split_commands(data): if not line.strip(): continue line = line.rstrip('*').strip() @@ -533,14 +543,16 @@ class GerberParser: # multiple statements from one line. if line.strip() and self.eof_found: warnings.warn('Data found in gerber file after EOF.', SyntaxWarning) + #print(f'Line {lineno}: {line}') for name, le_regex in self.STATEMENT_REGEXES.items(): if (match := le_regex.match(line)): + #print(f' match: {name} / {match}') try: getattr(self, f'_parse_{name}')(match.groupdict()) except: - print('Original line was:', line) - print(' match:', match) + print(f'Line {lineno}: {line}') + print(f' match: {name} / {match}') raise line = line[match.end(0):] break @@ -556,20 +568,21 @@ class GerberParser: if not self.eof_found: warnings.warn('File is missing mandatory M02 EOF marker. File may be truncated.', SyntaxWarning) - def _parse_interpolation_mode(self, match): - if match['code'] == 'G01': + def _parse_coord(self, match): + if match['interpolation'] == 'G01': self.graphics_state.interpolation_mode = InterpMode.LINEAR - elif match['code'] == 'G02': + elif match['interpolation'] == 'G02': self.graphics_state.interpolation_mode = InterpMode.CIRCULAR_CW - elif match['code'] == 'G03': + elif match['interpolation'] == 'G03': self.graphics_state.interpolation_mode = InterpMode.CIRCULAR_CCW - elif match['code'] == 'G74': + elif match['interpolation'] == 'G74': self.multi_quadrant_mode = True # used only for syntax checking - elif match['code'] == 'G75': + elif match['interpolation'] == 'G75': self.multi_quadrant_mode = False - # we always emit a G75 at the beginning of the file. - def _parse_coord(self, match): + if match['interpolation'] in ('G74', 'G75') and match[0] != match['interpolation']: + raise SyntaxError('G74/G75 combined with coord') + x = self.file_settings.parse_gerber_value(match['x']) y = self.file_settings.parse_gerber_value(match['y']) i = self.file_settings.parse_gerber_value(match['i']) @@ -677,6 +690,10 @@ class GerberParser: else: self.file_settings.unit = Inch + def _parse_allegro_format_spec(self, match): + self._parse_format_spec(match) + self._parse_unit_mode(match) + def _parse_load_polarity(self, match): self.graphics_state.polarity_dark = match['polarity'] == 'D' @@ -686,6 +703,13 @@ class GerberParser: b = float(b) if b else 0 self.graphics_state.offset = a, b + def _parse_allegro_legacy_params(self, match): + self._parse_image_rotation(match) + self._parse_offset(match) + self._parse_image_polarity(match) + self._parse_mirror_image(match) + self._parse_scale_factor(match) + def _parse_include_file(self, match): if self.include_dir is None: warnings.warn('IF include statement found, but includes are deactivated.', ResourceWarning) @@ -718,27 +742,33 @@ class GerberParser: warnings.warn('Deprecated LN (load name) statement found. This deprecated since rev. I4 (Oct 2013).', DeprecationWarning) def _parse_axis_selection(self, match): - warnings.warn('Deprecated AS (axis selection) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) + if match['axes'] != 'AXBY': + warnings.warn('Deprecated AS (axis selection) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.graphics_state.output_axes = match['axes'] def _parse_image_polarity(self, match): - # Do not warn, this is still common. - # warnings.warn('Deprecated IP (image polarity) statement found. This deprecated since rev. I4 (Oct 2013).', - # DeprecationWarning) - self.graphics_state.image_polarity = dict(POS='positive', NEG='negative')[match['polarity']] + polarity = dict(POS='positive', NEG='negative')[match['polarity']] + if polarity != 'positive': + warnings.warn('Deprecated IP (image polarity) statement found. This deprecated since rev. I4 (Oct 2013).', DeprecationWarning) + self.graphics_state.image_polarity = polarity def _parse_image_rotation(self, match): - warnings.warn('Deprecated IR (image rotation) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) - self.graphics_state.image_rotation = int(match['rotation']) + rotation = int(match['rotation']) + if rotation: + warnings.warn('Deprecated IR (image rotation) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) + self.graphics_state.image_rotation = rotation def _parse_mirror_image(self, match): - warnings.warn('Deprecated MI (mirror image) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) - self.graphics_state.mirror = bool(int(match['a'] or '0')), bool(int(match['b'] or '1')) + mirror = bool(int(match['ma'] or '0')), bool(int(match['mb'] or '1')) + if mirror != (False, False): + warnings.warn('Deprecated MI (mirror image) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) + self.graphics_state.mirror = mirror def _parse_scale_factor(self, match): - warnings.warn('Deprecated SF (scale factor) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) - a = float(match['a']) if match['a'] else 1.0 - b = float(match['b']) if match['b'] else 1.0 + a = float(match['sa']) if match['sa'] else 1.0 + b = float(match['sb']) if match['sb'] else 1.0 + if not math.isclose(math.dist((a, b), (1, 1)), 0): + warnings.warn('Deprecated SF (scale factor) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.graphics_state.scale_factor = a, b def _parse_comment(self, match): -- cgit