diff options
Diffstat (limited to 'gerbonara/gerber/rs274x.py')
-rw-r--r-- | gerbonara/gerber/rs274x.py | 274 |
1 files changed, 136 insertions, 138 deletions
diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py index 1ab030c..0c7b1f4 100644 --- a/gerbonara/gerber/rs274x.py +++ b/gerbonara/gerber/rs274x.py @@ -145,22 +145,34 @@ class GerberFile(CamFile): return ((min_x, max_x), (min_y, max_y)) - # TODO: re-add settings arg - def write(self, filename): + def generate_statements(self): self.settings.notation = 'absolute' self.settings.zeros = 'trailing' self.settings.format = self.format self.units = self.units - with open(filename, 'w') as f: - print(UnitStmt().to_gerber(self.settings), file=f) - print(FormatSpecStmt().to_gerber(self.settings), file=f) - print(ImagePolarityStmt().to_gerber(self.settings), file=f) + yield UnitStmt() + yield FormatSpecStmt() + yield ImagePolarityStmt() + yield SingleQuadrantModeStmt() + + yield from self.aperture_macros.values() + yield from self.aperture_defs + yield from self.main_statements + + yield EofStmt() + + def __str__(self): + return '\n'.join(self.generate_statements()) - for thing in chain(self.aperture_macros.values(), self.aperture_defs, self.main_statements): - print(thing.to_gerber(self.settings), file=f) + def save(self, filename): + with open(filename, 'w', encoding='utf-8') as f: # Encoding is specified as UTF-8 by spec. + for stmt in self.generate_statements(): + print(stmt.to_gerber(self.settings), file=f) - print('M02*', file=f) + def render_primitives(self): + for stmt in self.main_statements: + yield from stmt.render_primitives() def to_inch(self): if self.units == 'metric': @@ -245,6 +257,45 @@ class GerberFile(CamFile): statement.shape = polygon +@dataclass +class GraphicsState: + polarity_dark : bool = True + point : tuple = None + aperture : ApertureDefStmt = None + interpolation_mode : InterpolationModeStmt = None + multi_quadrant_mode : bool = None # used only for syntax checking + + def flash(self, x, y): + self.point = (x, y) + return Aperture(self.aperture, x, y) + + def interpolate(self, x, y, i=None, j=None): + if self.interpolation_mode == LinearModeStmt: + if i is not None or j is not None: + raise SyntaxError("i/j coordinates given for linear D01 operation (which doesn't take i/j)") + + return self._create_line(x, y) + + else: + return self._create_arc(x, y, i, j) + + def _create_line(self, x, y): + old_point, self.point = self.point, (x, y) + return Line(old_point, self.point, self.aperture, self.polarity_dark) + + def _create_arc(self, x, y, i, j): + if self.multi_quadrant_mode is None: + warnings.warn('Circular arc interpolation without explicit G75 Single-Quadrant mode statement. '\ + 'This can cause problems with older gerber interpreters.', SyntaxWarning) + + elif self.multi_quadrant_mode: + raise SyntaxError('Circular arc interpolation in multi-quadrant mode (G74) is not implemented.') + + old_point, self.point = self.point, (x, y) + direction = 'ccw' if self.interpolation_mode == CircularCCWModeStmt else 'cw' + return Arc(old_point, self.point, (i, j), direction, self.aperture, self.polarity_dark): + + class GerberParser: NUMBER = r"[\+-]?\d+" DECIMAL = r"[\+-]?\d+([.]?\d+)?" @@ -260,6 +311,7 @@ class GerberParser: 'comment': r"G0?4(?P<comment>[^*]*)(\*)?", 'format_spec': r"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]*", 'load_polarity': r"LP(?P<polarity>(D|C))", + # FIXME LM, LR, LS 'load_name': r"LN(?P<name>.*)", 'offset': fr"OF(A(?P<a>{DECIMAL}))?(B(?P<b>{DECIMAL}))?", 'include_file': r"IF(?P<filename>.*)", @@ -271,8 +323,8 @@ class GerberParser: 'scale_factor': fr"SF(A(?P<a>{DECIMAL}))?(B(?P<b>{DECIMAL}))?", 'aperture_definition': fr"ADD(?P<number>\d+)(?P<shape>C|R|O|P|{NAME})[,]?(?P<modifiers>[^,%]*)", 'aperture_macro': fr"AM(?P<name>{NAME})\*(?P<macro>[^%]*)", - 'region_mode': r'(?P<mode>G3[67])\*', - 'quadrant_mode': r'(?P<mode>G7[45])\*', + 'region_start': r'G36\*', + 'region_end': r'G37\*', 'old_unit':r'(?P<mode>G7[01])\*', 'old_notation': r'(?P<mode>G9[01])\*', 'eof': r"M0?[02]\*", @@ -287,11 +339,13 @@ class GerberParser: self.include_dir = include_dir self.include_stack = [] self.settings = FileSettings() + self.current_region = None + self.graphics_state = GraphicsState() + self.statements = [] self.primitives = [] self.apertures = {} self.macros = {} - self.current_region = None self.x = 0 self.y = 0 self.last_operation = None @@ -302,13 +356,15 @@ class GerberParser: self.image_polarity = 'positive' self.level_polarity = 'dark' self.region_mode = 'off' - self.quadrant_mode = 'multi-quadrant' self.step_and_repeat = (1, 1, 0, 0) def parse(self, data): for stmt in self._parse(data): + if self.current_region is None: + self.statements.append(stmt) + else: + self.current_region.append(stmt) self.evaluate(stmt) - self.statements.append(stmt) # Initialize statement units for stmt in self.statements: @@ -370,21 +426,26 @@ class GerberParser: def _parse_interpolation_mode(self, match): if match['code'] == 'G01': + self.graphics_state.interpolation_mode = LinearModeStmt yield LinearModeStmt() elif match['code'] == 'G02': + self.graphics_state.interpolation_mode = CircularCWModeStmt yield CircularCWModeStmt() elif match['code'] == 'G03': + self.graphics_state.interpolation_mode = CircularCCWModeStmt yield CircularCCWModeStmt() elif match['code'] == 'G74': - yield MultiQuadrantModeStmt() + self.graphics_state.multi_quadrant_mode = True # used only for syntax checking elif match['code'] == 'G75': - yield SingleQuadrantModeStmt() + self.graphics_state.multi_quadrant_mode = False + # we always emit a G75 at the beginning of the file. def _parse_coord(self, match): - x = parse_gerber_value(match.get('x'), self.settings) - y = parse_gerber_value(match.get('y'), self.settings) - i = parse_gerber_value(match.get('i'), self.settings) - j = parse_gerber_value(match.get('j'), self.settings) + x = parse_gerber_value(match['x'], self.settings) + y = parse_gerber_value(match['y'], self.settings) + i = parse_gerber_value(match['i'], self.settings) + j = parse_gerber_value(match['j'], self.settings) + if not (op := match['operation']): if self.last_operation == 'D01': warnings.warn('Coordinate statement without explicit operation code. This is forbidden by spec.', @@ -395,22 +456,28 @@ class GerberParser: 'mode and the last operation statement was not D01.') if op in ('D1', 'D01'): - yield InterpolateStmt(x, y, i, j) + yield self.graphics_state.interpolate(x, y, i, j) + + else: + if i is not None or j is not None: + raise SyntaxError("i/j coordinates given for D02/D03 operation (which doesn't take i/j)") + + if op in ('D2', 'D02'): + self.graphics_state.point = (x, y) - if i is not None or j is not None: - raise SyntaxError("i/j coordinates given for D02/D03 operation (which doesn't take i/j)") - - if op in ('D2', 'D02'): - yield MoveStmt(x, y, i, j) - else: # D03 - yield FlashStmt(x, y, i, j) + else: # D03 + yield self.graphics_state.flash(x, y) def _parse_aperture(self, match): number = int(match['number']) if number < 10: raise SyntaxError(f'Invalid aperture number {number}: Aperture number must be >= 10.') - yield ApertureStmt(number) + + if number not in self.apertures: + raise SyntaxError(f'Tried to access undefined aperture {number}') + + self.graphics_state.aperture = self.apertures[number] def _parse_format_spec(self, match): # This is a common problem in Eagle files, so just suppress it @@ -421,7 +488,7 @@ class GerberParser: raise SyntaxError(f'FS specifies different coordinate formats for X and Y ({match["x"]} != {match["y"]})') self.settings.number_format = int(match['x'][0]), int(match['x'][1]) - yield FormatSpecStmt() + yield from () # We always force a format spec statement at the beginning of the file def _parse_unit_mode(self, match): if match['unit'] == 'MM': @@ -429,16 +496,17 @@ class GerberParser: else: self.settings.units = 'inch' - yield MOParamStmt() + yield from () # We always force a unit mode statement at the beginning of the file def _parse_load_polarity(self, match): - yield LoadPolarityStmt(dark=(match['polarity'] == 'D')) + yield LoadPolarityStmt(dark=match['polarity'] == 'D') def _parse_offset(self, match): a, b = match['a'], match['b'] a = float(a) if a else 0 b = float(b) if b else 0 - yield OffsetStmt(a, b) + self.settings.offset = a, b + yield from () # Handled by coordinate normalization def _parse_include_file(self, match): if self.include_dir is None: @@ -470,25 +538,25 @@ class GerberParser: warnings.warn('Deprecated AS (axis selection) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.settings.output_axes = match['axes'] - yield AxisSelectionStmt() + yield from () # Handled by coordinate normalization def _parse_image_polarity(self, match): warnings.warn('Deprecated IP (image polarity) statement found. This deprecated since rev. I4 (Oct 2013).', DeprecationWarning) self.settings.image_polarity = match['polarity'] - yield ImagePolarityStmt() + yield from () # We always emit this in the header def _parse_image_rotation(self, match): warnings.warn('Deprecated IR (image rotation) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.settings.image_rotation = int(match['rotation']) - yield ImageRotationStmt() + yield from () # Handled by coordinate normalization def _parse_mirror_image(self, match): warnings.warn('Deprecated MI (mirror image) statement found. This deprecated since rev. I1 (Dec 2012).', DeprecationWarning) self.settings.mirror = bool(int(match['a'] or '0')), bool(int(match['b'] or '1')) - yield MirrorImageStmt() + yield from () # Handled by coordinate normalization def _parse_scale_factor(self, match): warnings.warn('Deprecated SF (scale factor) statement found. This deprecated since rev. I1 (Dec 2012).', @@ -496,26 +564,20 @@ class GerberParser: a = float(match['a']) if match['a'] else 1.0 b = float(match['b']) if match['b'] else 1.0 self.settings.scale_factor = a, b - yield ScaleFactorStmt() + yield from () # Handled by coordinate normalization def _parse_comment(self, match): yield CommentStmt(match["comment"]) - def _parse_region_mode(self, match): - yield RegionStartStatement() if match['mode'] == 'G36' else RegionEndStatement() - - elif param["param"] == "AM": - yield AMParamStmt.from_dict(param, units=self.settings.units) - elif param["param"] == "AD": - yield ADParamStmt.from_dict(param) + def _parse_region_start(self, _match): + current_region = RegionGroup() - def _parse_quadrant_mode(self, match): - if match['mode'] == 'G74': - warnings.warn('Deprecated G74 single quadrant mode statement found. This deprecated since 2021.', - DeprecationWarning) - yield SingleQuadrantModeStmt() - else: - yield MultiQuadrantModeStmt() + def _parse_region_end(self, _match): + if self.current_region is None: + raise SyntaxError('Region end command (G37) outside of region') + + yield self.current_region + self.current_region = None def _parse_old_unit(self, match): self.settings.units = 'inch' if match['mode'] == 'G70' else 'mm' @@ -531,12 +593,33 @@ class GerberParser: DeprecationWarning) yield CommentStmt(f'Replaced deprecated {match["mode"]} notation mode statement with FS statement') - def _parse_eof(self, match): + def _parse_eof(self, _match): yield EofStmt() def _parse_ignored(self, match): yield CommentStmt(f'Ignoring {match{"stmt"]} statement.') + def _parse_aperture_definition(self, match): + modifiers = [ float(mod) for mod in match['modifiers'].split(',') ] + if match['shape'] == 'C': + aperture = ApertureCircle(*modifiers) + + elif match['shape'] == 'R' + aperture = ApertureRectangle(*modifiers) + + elif shape == 'O': + aperture = ApertureObround(*modifiers) + + elif shape == 'P': + aperture = AperturePolygon(*modifiers) + + else: + aperture = self.macros[shape].build(modifiers) + + self.apertures[d] = aperture + + + def evaluate(self, stmt): """ Evaluate Gerber statement and update image accordingly. @@ -567,83 +650,6 @@ class GerberParser: else: raise Exception("Invalid statement to evaluate") - def _define_aperture(self, d, shape, modifiers): - aperture = None - if shape == 'C': - diameter = modifiers[0][0] - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 2: - hole_diameter = modifiers[0][1] - elif len(modifiers[0]) == 3: - rectangular_hole = modifiers[0][1:3] - - aperture = Circle(position=None, diameter=diameter, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - units=self.settings.units) - - elif shape == 'R': - width = modifiers[0][0] - height = modifiers[0][1] - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 3: - hole_diameter = modifiers[0][2] - elif len(modifiers[0]) == 4: - rectangular_hole = modifiers[0][2:4] - - aperture = Rectangle(position=None, width=width, height=height, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - units=self.settings.units) - elif shape == 'O': - width = modifiers[0][0] - height = modifiers[0][1] - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 3: - hole_diameter = modifiers[0][2] - elif len(modifiers[0]) == 4: - rectangular_hole = modifiers[0][2:4] - - aperture = Obround(position=None, width=width, height=height, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - units=self.settings.units) - elif shape == 'P': - outer_diameter = modifiers[0][0] - number_vertices = int(modifiers[0][1]) - if len(modifiers[0]) > 2: - rotation = modifiers[0][2] - else: - rotation = 0 - - hole_diameter = 0 - rectangular_hole = (0, 0) - if len(modifiers[0]) == 4: - hole_diameter = modifiers[0][3] - elif len(modifiers[0]) >= 5: - rectangular_hole = modifiers[0][3:5] - - aperture = Polygon(position=None, sides=number_vertices, - radius=outer_diameter/2.0, - hole_diameter=hole_diameter, - hole_width=rectangular_hole[0], - hole_height=rectangular_hole[1], - rotation=rotation) - else: - aperture = self.macros[shape].build(modifiers) - - aperture.units = self.settings.units - self.apertures[d] = aperture - def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': @@ -658,14 +664,6 @@ class GerberParser: self.quadrant_mode = stmt.mode def _evaluate_param(self, stmt): - if stmt.param == "FS": - self.settings.zero_suppression = stmt.zero_suppression - self.settings.format = stmt.format - self.settings.notation = stmt.notation - elif stmt.param == "MO": - self.settings.units = stmt.mode - elif stmt.param == "IP": - self.image_polarity = stmt.ip elif stmt.param == "LP": self.level_polarity = stmt.lp elif stmt.param == "AM": |