diff options
Diffstat (limited to 'gerbonara/gerber/excellon.py')
-rwxr-xr-x | gerbonara/gerber/excellon.py | 99 |
1 files changed, 85 insertions, 14 deletions
diff --git a/gerbonara/gerber/excellon.py b/gerbonara/gerber/excellon.py index 3931660..b3fbd97 100755 --- a/gerbonara/gerber/excellon.py +++ b/gerbonara/gerber/excellon.py @@ -71,15 +71,22 @@ def parse_allegro_ncparam(data, settings=None): # still be able to extract the same information from the human-readable ncdrill.log. if settings is None: - self.settings = FileSettings(number_format=(None, None)) + settings = FileSettings(number_format=(None, None)) lz_supp, tz_supp = False, False + nf_int, nf_frac = settings.number_format for line in data.splitlines(): line = re.sub(r'\s+', ' ', line.strip()) if (match := re.fullmatch(r'FORMAT ([0-9]+\.[0-9]+)', line)): x, _, y = match[1].partition('.') - settings.number_format = int(x), int(y) + nf_int, nf_frac = int(x), int(y) + + elif (match := re.fullmatch(r'INTEGER-PLACES ([0-9]+)', line)): + nf_int = int(match[1]) + + elif (match := re.fullmatch(r'DECIMAL-PLACES ([0-9]+)', line)): + nf_frac = int(match[1]) elif (match := re.fullmatch(r'COORDINATES (ABSOLUTE|.*)', line)): # I have not been able to find a single incremental-notation allegro file. Probably that is for the better. @@ -100,17 +107,59 @@ def parse_allegro_ncparam(data, settings=None): raise SyntaxError('Allegro Excellon parameters specify both leading and trailing zero suppression. We do not ' 'know how to parse this. Please raise an issue on our issue tracker and provide an example file.') + settings.number_format = nf_int, nf_frac settings.zeros = 'leading' if lz_supp else 'trailing' + return settings + + +def parse_allegro_logfile(data): + found_tools = {} + unit = None + + for line in data.splitlines(): + line = line.strip() + line = re.sub('\s+', ' ', line) + + if (m := re.match(r'OUTPUT-UNITS (METRIC|ENGLISH|INCHES)', line)): + # I have no idea wth is the difference between "ENGLISH" and "INCHES". I think one might just be the one + # Allegro uses in footprint files, with the other one being used in gerber exports. + unit = MM if m[1] == 'METRIC' else Inch + + elif (m := re.match(r'T(?P<index1>[0-9]+) (?P<index2>[0-9]+)\. (?P<diameter>[0-9/.]+) [0-9. /+-]* (?P<plated>PLATED|NON_PLATED|OPTIONAL) [0-9]+', line)): + index1, index2 = int(m['index1']), int(m['index2']) + if index1 != index2: + return {} + diameter = float(m['diameter']) + if unit == Inch: + diameter /= 1000 + is_plated = None if m['plated'] is None else (m['plated'] in ('PLATED', 'OPTIONAL')) + found_tools[index1] = ExcellonTool(diameter=diameter, plated=is_plated, unit=unit) + return found_tools class ExcellonFile(CamFile): - def __init__(self, objects=None, comments=None, import_settings=None, filename=None, generator_hints=None): - super().__init__(filename=filename) + def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None): + super().__init__(original_path=original_path) self.objects = objects or [] self.comments = comments or [] self.import_settings = import_settings self.generator_hints = generator_hints or [] # This is a purely informational goodie from the parser. Use it as you wish. + def __str__(self): + name = f'{self.original_path.name} ' if self.original_path else '' + if self.is_plated: + plating = 'plated' + elif self.is_nonplated: + plating = 'nonplated' + elif self.is_mixed_plating: + plating = 'mixed plating' + else: + plating = 'unknown plating' + return f'<ExcellonFile {name}{plating} with {len(list(self.drills()))} drills, {len(list(self.slots()))} slots using {len(self.drill_sizes())} tools>' + + def __repr__(self): + return str(self) + def __bool__(self): return not self.is_empty @@ -165,6 +214,7 @@ class ExcellonFile(CamFile): @classmethod def open(kls, filename, plated=None, settings=None): filename = Path(filename) + logfile_tools = None # Parse allegro parameter files. # Prefer nc_param.txt over ncparam.log since the txt is the machine-readable one. @@ -172,16 +222,23 @@ class ExcellonFile(CamFile): for fn in 'nc_param.txt', 'ncdrill.log': if (param_file := filename.parent / fn).is_file(): settings = parse_allegro_ncparam(param_file.read_text()) + warnings.warn(f'Loaded allegro-style excellon settings file {param_file}') break - return kls.from_string(filename.read_text(), settings=settings, filename=filename, plated=plated) + # TODO add try/except aronud this + log_file = filename.parent / 'ncdrill.log' + if log_file.is_file(): + logfile_tools = parse_allegro_logfile(log_file.read_text()) + + return kls.from_string(filename.read_text(), settings=settings, + filename=filename, plated=plated, logfile_tools=logfile_tools) @classmethod - def from_string(kls, data, settings=None, filename=None, plated=None): - parser = ExcellonParser(settings) + def from_string(kls, data, settings=None, filename=None, plated=None, logfile_tools=None): + parser = ExcellonParser(settings, logfile_tools=logfile_tools) parser.do_parse(data, filename=filename) return kls(objects=parser.objects, comments=parser.comments, import_settings=settings, - generator_hints=parser.generator_hints, filename=filename) + generator_hints=parser.generator_hints, original_path=filename) def _generate_statements(self, settings, drop_comments=True): @@ -309,6 +366,12 @@ class ExcellonFile(CamFile): def drill_sizes(self): return sorted({ obj.tool.diameter for obj in self.objects }) + def drills(self): + return (obj for obj in self.objects if isinstance(obj, Flash)) + + def slots(self): + return (obj for obj in self.objects if not isinstance(obj, Flash)) + @property def bounds(self): if not self.objects: @@ -330,7 +393,7 @@ class ProgramState(Enum): class ExcellonParser(object): - def __init__(self, settings=None): + def __init__(self, settings=None, logfile_tools=None): # NOTE XNC files do not contain an explicit number format specification, but all values have decimal points. # Thus, we set the default number format to (None, None). If the file does not contain an explicit specification # and FileSettings.parse_gerber_value encounters a number without an explicit decimal point, it will throw a @@ -352,6 +415,7 @@ class ExcellonParser(object): self.generator_hints = [] self.lineno = None self.filename = None + self.logfile_tools = logfile_tools or {} def warn(self, msg): warnings.warn(f'{self.filename}:{self.lineno} "{self.line}": {msg}', SyntaxWarning) @@ -462,9 +526,17 @@ class ExcellonParser(object): if index == 0: # T0 is used as END marker, just ignore return elif index not in self.tools: - raise SyntaxError(f'Undefined tool index {index} selected.') + if not self.tools and index in self.logfile_tools: + # allegro is just wonderful. + self.warn(f'Undefined tool index {index} selected. We found an allegro drill log file next to this, so ' + 'we will use tool definitions from there.') + self.active_tool = self.logfile_tools[index] + + else: + raise SyntaxError(f'Undefined tool index {index} selected.') - self.active_tool = self.tools[index] + else: + self.active_tool = self.tools[index] coord = lambda name, key=None: fr'({name}(?P<{key or name}>[+-]?[0-9]*\.?[0-9]*))?' xy_coord = coord('X') + coord('Y') @@ -478,8 +550,7 @@ class ExcellonParser(object): dy = int(match['Y'] or '0') for i in range(int(match['count'])): - self.pos[0] += dx - self.pos[1] += dy + self.pos = (self.pos[0] + dx, self.pos[1] + dy) # FIXME fix API below if not self.ensure_active_tool(): return @@ -689,7 +760,7 @@ class ExcellonParser(object): if match[2] not in ('', '2'): raise SyntaxError(f'Unsupported FMAT format version {match["version"]}') - @exprs.match('G40|G41|G42|{coord("F")}') + @exprs.match(r'G40|G41|G42|F[0-9]+') def handle_unhandled(self, match): self.warn(f'{match[0]} excellon command intended for CAM tools found in EDA file.') |