diff options
Diffstat (limited to 'gerbonara')
-rw-r--r-- | gerbonara/cam.py | 13 | ||||
-rwxr-xr-x | gerbonara/excellon.py | 75 | ||||
-rw-r--r-- | gerbonara/rs274x.py | 55 |
3 files changed, 72 insertions, 71 deletions
diff --git a/gerbonara/cam.py b/gerbonara/cam.py index 593ede8..2edf8ed 100644 --- a/gerbonara/cam.py +++ b/gerbonara/cam.py @@ -141,22 +141,13 @@ class FileSettings: integer_digits, decimal_digits = self.number_format - sign = 1 - - if value[0] == '-': - sign = -1 - value = value[1:] - - elif value[0] == '+': - value = value[1:] - if self.zeros == 'leading': value = self._pad + value # pad with zeros to ensure we have enough decimals - return sign*float(value[:-decimal_digits] + '.' + value[-decimal_digits:]) + return float(value[:-decimal_digits] + '.' + value[-decimal_digits:]) else: # no or trailing zero suppression value = value + self._pad - return sign*float(value[:integer_digits] + '.' + value[integer_digits:]) + return float(value[:integer_digits] + '.' + value[integer_digits:]) def write_gerber_value(self, value, unit=None): """ Convert a floating point number to a Gerber-formatted string. """ diff --git a/gerbonara/excellon.py b/gerbonara/excellon.py index f3e2d64..54fcb51 100755 --- a/gerbonara/excellon.py +++ b/gerbonara/excellon.py @@ -657,19 +657,24 @@ class ExcellonParser(object): else: self.active_tool = self.tools[index] - coord = lambda name, key=None: fr'({name}(?P<{key or name}>[+-]?[0-9]*\.?[0-9]*))?' + coord = lambda name: fr'(?:{name}\+?(-?)([0-9]*\.?[0-9]*))?' xy_coord = coord('X') + coord('Y') xyaij_coord = xy_coord + coord('A') + coord('I') + coord('J') - @exprs.match(r'R(?P<count>[0-9]+)' + xy_coord) + @exprs.match(r'R([0-9]+)' + xy_coord) def handle_repeat_hole(self, match): if self.program_state == ProgramState.HEADER: return - dx = int(match['X'] or '0') - dy = int(match['Y'] or '0') + count, x_s, x, y_s, y = match.groups() + dx = self.settings.parse_gerber_value(x) or 0 + if x_s: + dx = -dx + dy = self.settings.parse_gerber_value(y) or 0 + if y_s: + dy = -dy - for i in range(int(match['count'])): + for i in range(int(count)): self.pos = (self.pos[0] + dx, self.pos[1] + dy) # FIXME fix API below if not self.ensure_active_tool(): @@ -724,18 +729,24 @@ class ExcellonParser(object): if match[0] == 'M00': self.generator_hints.append('zuken') - def do_move(self, match=None, x='X', y='Y'): - if self.settings.number_format == (None, None) and not '.' in match['X']: + def do_move(self, coord_groups): + x_s, x, y_s, y = coord_groups + + if self.settings.number_format == (None, None) and '.' not in x: # TARGET3001! exports zeros as "00" even when it uses an explicit decimal point everywhere else. - if match['X'] != '00': + if x != '00': raise SyntaxError('No number format set and value does not contain a decimal point. If this is an Allegro ' 'Excellon drill file make sure either nc_param.txt or ncdrill.log ends up in the same folder as ' 'it, because Allegro does not include this critical information in their Excellon output. If you ' 'call this through ExcellonFile.from_string, you must manually supply from_string with a ' 'FileSettings object from excellon.parse_allegro_ncparam.') - x = self.settings.parse_gerber_value(match['X']) - y = self.settings.parse_gerber_value(match['Y']) + x = self.settings.parse_gerber_value(x) + if x_s: + x = -x + y = self.settings.parse_gerber_value(y) + if y_s: + y = -y old_pos = self.pos @@ -757,7 +768,7 @@ class ExcellonParser(object): if self.program_state is None: self.warn('Routing mode command found before header.') self.program_state = ProgramState.ROUTING - self.do_move(match) + self.do_move(match.groups()) @exprs.match('%') def handle_rewind_shorthand(self, match): @@ -779,25 +790,26 @@ class ExcellonParser(object): self.warn('Routing command found before first tool definition.') return None - @exprs.match('(?P<mode>G01|G02|G03)' + xyaij_coord) + @exprs.match('(G01|G02|G03)' + xyaij_coord) def handle_linear_mode(self, match): - if match['mode'] == 'G01': + mode, *coord_groups = match.groups() + if mode == 'G01': self.interpolation_mode = InterpMode.LINEAR else: - clockwise = (match['mode'] == 'G02') + clockwise = (mode == 'G02') self.interpolation_mode = InterpMode.CIRCULAR_CW if clockwise else InterpMode.CIRCULAR_CCW - self.do_interpolation(match) + self.do_interpolation(coord_groups) - def do_interpolation(self, match): - x, y, a, i, j = match['X'], match['Y'], match['A'], match['I'], match['J'] + def do_interpolation(self, coord_groups): + x_s, x, y_s, y, a_s, a, i_s, i, j_s, j = coord_groups - start, end = self.do_move(match) + start, end = self.do_move((x_s, x, y_s, y)) if self.program_state != ProgramState.ROUTING: return - if not self.drill_down or not (match['X'] or match['Y']) or not self.ensure_active_tool(): + if not self.drill_down or not (x or y) or not self.ensure_active_tool(): return if self.interpolation_mode == InterpMode.LINEAR: @@ -819,6 +831,8 @@ class ExcellonParser(object): # Convert endpoint-radius-endpoint notation to endpoint-center-endpoint notation. We always use the # smaller arc here. # from https://math.stackexchange.com/a/1781546 + if a_s: + raise ValueError('Negative arc radius given') r = settings.parse_gerber_value(a) x1, y1 = start x2, y2 = end @@ -835,7 +849,11 @@ class ExcellonParser(object): else: # explicit center given i = settings.parse_gerber_value(i) + if i_s: + i = -i j = settings.parse_gerber_value(j) + if j_s: + j = -i self.objects.append(Arc(*start, *end, i, j, True, self.active_tool, unit=self.settings.unit)) @@ -865,7 +883,8 @@ class ExcellonParser(object): @exprs.match('G93' + xy_coord) def handle_absolute_mode(self, match): - if int(match['X'] or 0) != 0 or int(match['Y'] or 0) != 0: + _x_s, x, _y_s, y = match.groups() + if int(x or 0) != 0 or int(y or 0) != 0: # Siemens tooling likes to include a meaningless G93X0Y0 after its header. raise NotImplementedError('G93 zero set command is not supported.') self.generator_hints.append('siemens') @@ -890,23 +909,11 @@ class ExcellonParser(object): def handle_unhandled(self, match): self.warn(f'{match[0]} excellon command intended for CAM tools found in EDA file.') - @exprs.match(coord('X', 'x1') + coord('Y', 'y1') + 'G85' + coord('X', 'x2') + coord('Y', 'y2')) - def handle_slot_dotted(self, match): - self.warn('Weird G85 excellon slot command used. Please raise an issue on our issue tracker and provide this file for testing.') - self.do_move(match, 'X1', 'Y1') - start, end = self.do_move(match, 'X2', 'Y2') - - if self.program_state in (ProgramState.DRILLING, ProgramState.HEADER): # FIXME should we realy handle this in header? - if self.ensure_active_tool(): - # We ignore whether a slot is a "routed" G00/G01 slot or a "drilled" G85 slot and export both as routed - # slots. - self.objects.append(Line(*start, *end, self.active_tool, unit=self.settings.unit)) - @exprs.match(xyaij_coord) def handle_bare_coordinate(self, match): # Yes, drills in the header doesn't follow the specification, but it there are many files like this. if self.program_state in (ProgramState.DRILLING, ProgramState.HEADER): - _start, end = self.do_move(match) + _start, end = self.do_move(match.groups()[:4]) if not self.ensure_active_tool(): return @@ -916,7 +923,7 @@ class ExcellonParser(object): elif self.program_state == ProgramState.ROUTING: # Bare coordinates for routing also seem illegal, but Siemens actually uses these. # Example file: siemens/80101_0125_F200_ContourPlated.ncd - self.do_interpolation(match) + self.do_interpolation(match.groups()) else: self.warn('Bare coordinate after end of file') diff --git a/gerbonara/rs274x.py b/gerbonara/rs274x.py index 464a947..ea1ea1d 100644 --- a/gerbonara/rs274x.py +++ b/gerbonara/rs274x.py @@ -292,7 +292,6 @@ class GraphicsState: self.polarity_dark = True self.point = None self.aperture = None - self.file_settings = None self.interpolation_mode = InterpMode.LINEAR self.multi_quadrant_mode = None # used only for syntax checking self.aperture_mirroring = (False, False) # LM mirroring (x, y) @@ -305,6 +304,7 @@ class GraphicsState: self.image_scale = (1.0, 1.0) # SF image scaling (x, y); deprecated self._mat = None self.file_settings = file_settings + self.unit = file_settings.unit if file_settings else None self.aperture_map = aperture_map or {} self.warn = warn self.unit_warning = False @@ -362,27 +362,23 @@ class GraphicsState: return rx, ry def flash(self, x, y): - if self.file_settings.unit is None and not self.unit_warning: - self.warn('Gerber file does not contain a unit definition.') - self.unit_warning = True + if self.unit is None: + raise SyntaxError('Gerber file does not contain a unit definition.') self.update_point_native(x, y) obj = go.Flash(*self.map_coord(*self.point), self.aperture, polarity_dark=self._polarity_dark, - unit=self.file_settings.unit, + unit=self.unit, attrs=self.object_attrs) return obj def interpolate(self, x, y, i=None, j=None, aperture=True, multi_quadrant=False): old_point = self.map_coord(*self.update_point_native(x, y)) - unit = self.file_settings.unit - if not self.unit_warning and unit is None: - self.warn('Gerber file does not contain a unit definition.') - self.unit_warning = True + if (unit := self.unit) is None: + raise SyntaxError('Gerber file does not contain a unit definition.') if aperture: - aperture = self.aperture - if not aperture: + if (aperture := self.aperture) is None: raise SyntaxError('Interpolation attempted without selecting aperture first') if math.isclose(aperture.equivalent_width(), 0): @@ -401,7 +397,6 @@ class GraphicsState: polarity_dark=self._polarity_dark, unit=unit, attrs=self.object_attrs) else: - if i is None and j is None: self.warn('Linear segment implied during arc interpolation mode through D01 w/o I, J values') return go.Line(*old_point, *self.map_coord(*self.point), aperture, @@ -508,8 +503,8 @@ class GerberParser: NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+" STATEMENT_REGEXES = { - 'coord': fr"(G0?[123]|G74|G75|G54|G55)?\s*(?:X({NUMBER}))?(?:Y({NUMBER}))?" \ - fr"(?:I({NUMBER}))?(?:J({NUMBER}))?\s*" \ + 'coord': fr"(G0?[123]|G74|G75|G54|G55)?\s*(?:X\+?(-?)({NUMBER}))?(?:Y\+?(-?)({NUMBER}))?" \ + fr"(?:I\+?(-?)({NUMBER}))?(?:J\+?(-?)({NUMBER}))?\s*" \ fr"(?:D0?([123]))?$", 'region_start': r'G36$', 'region_end': r'G37$', @@ -578,14 +573,16 @@ class GerberParser: def _split_commands(self, data): # Ignore '%' signs within G04 commments because eagle likes to put completely broken file attributes inside G04 # comments, and those contain % signs. Best of all, they're not even balanced. - self.lineno = 0 - for match in re.finditer(r'G04.*?\*|%.*?%|[^*%]*\*', data, re.DOTALL): - cmd = match[0].strip().strip('%').rstrip('*') + self.lineno = 1 + for match in re.finditer(r'G04.*?\*\s*|%.*?%\s*|[^*%]*\*\s*', data, re.DOTALL): + cmd = match[0] + newlines = cmd.count('\n') + cmd = cmd.strip().strip('%').rstrip('*') if cmd: # Expensive, but only used in case something goes wrong. self.line = cmd yield cmd - self.lineno += cmd.count('\n') + self.lineno += newlines self.lineno = 0 self.line = '' @@ -622,7 +619,7 @@ class GerberParser: self.warn('File is missing mandatory M02 EOF marker. File may be truncated.') def _parse_coord(self, match): - interp, x, y, i, j, op = match.groups() # faster than name-based group access + interp, x_s, x, y_s, y, i_s, i, j_s, j, op = match.groups() # faster than name-based group access has_coord = x or y or i or j if not interp: @@ -647,7 +644,11 @@ class GerberParser: self.generator_hints.append('zuken') x = self.file_settings.parse_gerber_value(x) + if x_s: + x = -x y = self.file_settings.parse_gerber_value(y) + if y_s: + y = -y if not op and has_coord: if self.last_operation == '1': @@ -680,17 +681,19 @@ class GerberParser: self.warn('Deprecated G74 multi-quadant mode arc found. G74 is bad and you should feel bad.') i = self.file_settings.parse_gerber_value(i) + if i_s: + i = -i j = self.file_settings.parse_gerber_value(j) + if j_s: + j = -j if self.current_region is None: # in multi-quadrant mode this may return None if start and end point of the arc are the same. - obj = self.graphics_state.interpolate(x, y, i, j, - multi_quadrant=bool(self.multi_quadrant_mode)) + obj = self.graphics_state.interpolate(x, y, i, j, multi_quadrant=self.multi_quadrant_mode) if obj is not None: self.target.objects.append(obj) else: - obj = self.graphics_state.interpolate(x, y, i, j, aperture=False, - multi_quadrant=bool(self.multi_quadrant_mode)) + obj = self.graphics_state.interpolate(x, y, i, j, aperture=False, multi_quadrant=self.multi_quadrant_mode) if obj is not None: self.current_region.append(obj) @@ -774,9 +777,9 @@ class GerberParser: def _parse_unit_mode(self, match): if match['unit'] == 'MM': - self.file_settings.unit = MM + self.graphics_state.unit = self.file_settings.unit = MM else: - self.file_settings.unit = Inch + self.graphics_state.unit = self.file_settings.unit = Inch def _parse_allegro_format_spec(self, match): self._parse_format_spec(match) @@ -921,7 +924,7 @@ class GerberParser: self.current_region = None def _parse_old_unit(self, match): - self.file_settings.unit = Inch if match['mode'] == 'G70' else MM + self.graphics_state.unit = self.file_settings.unit = Inch if match['mode'] == 'G70' else MM self.warn(f'Deprecated {match["mode"]} unit mode statement found. This deprecated since 2012.', DeprecationWarning) self.target.comments.append('Replaced deprecated {match["mode"]} unit mode statement with MO statement') |