More parsing speedups
Some checks failed
Some checks failed
This commit is contained in:
parent
c31aabc227
commit
77f2da8761
3 changed files with 72 additions and 71 deletions
|
|
@ -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. """
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue