From 35f24607fe65385370cd6e267ea5afffbfbd1e34 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 29 Jan 2022 02:42:29 +0100 Subject: Layer matcher WIP --- gerbonara/gerber/cam.py | 4 +- gerbonara/gerber/excellon.py | 99 +++++++++++++++++++++---- gerbonara/gerber/layer_rules.py | 26 +++---- gerbonara/gerber/layers.py | 136 ++++++++++++++++++++++------------ gerbonara/gerber/rs274x.py | 10 ++- gerbonara/gerber/tests/test_layers.py | 76 ++++++++++--------- gerbonara/gerber/utils.py | 1 - 7 files changed, 235 insertions(+), 117 deletions(-) diff --git a/gerbonara/gerber/cam.py b/gerbonara/gerber/cam.py index ccfa7a2..411bc65 100644 --- a/gerbonara/gerber/cam.py +++ b/gerbonara/gerber/cam.py @@ -152,8 +152,8 @@ class FileSettings: class CamFile: - def __init__(self, filename=None, layer_name=None): - self.filename = filename + def __init__(self, original_path=None, layer_name=None): + self.original_path = original_path self.layer_name = layer_name self.import_settings = None self.objects = [] 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[0-9]+) (?P[0-9]+)\. (?P[0-9/.]+) [0-9. /+-]* (?PPLATED|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'' + + 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.') diff --git a/gerbonara/gerber/layer_rules.py b/gerbonara/gerber/layer_rules.py index 1733964..871e44f 100644 --- a/gerbonara/gerber/layer_rules.py +++ b/gerbonara/gerber/layer_rules.py @@ -11,7 +11,7 @@ MATCH_RULES = { 'bottom silk': r'.*\.gbo', 'bottom paste': r'.*\.gbp', 'inner copper': r'.*\.gp?([0-9]+)', - 'drill outline': r'.*\.(gko|gm[0-9]+)', + 'mechanical outline': r'.*\.(gko|gm[0-9]+)', 'drill unknown': r'.*\.(txt)', }, @@ -25,7 +25,7 @@ MATCH_RULES = { 'bottom silk': r'.*\.gbo|.*b.silks.*', 'bottom paste': r'.*\.gbp|.*b.paste.*', 'inner copper': r'.*\.gp?([0-9]+)|.*inn?e?r?([0-9]+).cu.*', - 'drill outline': r'.*\.(gm[0-9]+)|.*edge.cuts.*', + 'mechanical outline': r'.*\.(gm[0-9]+)|.*edge.cuts.*', 'drill plated': r'.*\.(drl)', }, @@ -39,7 +39,7 @@ MATCH_RULES = { 'bottom silk': r'.*\.bottomsilk\.\w+', 'bottom paste': r'.*\.bottompaste\.\w+', 'inner copper': r'.*\.inner_l([0-9]+)\.\w+', # FIXME verify this - 'drill outline': r'.*\.outline\.gbr', + 'mechanical outline': r'.*\.outline\.gbr', 'drill plated': r'.*\.plated-drill.cnc', 'drill nonplated': r'.*\.unplated-drill.cnc', }, @@ -64,10 +64,10 @@ MATCH_RULES = { 'top silk': r'.*\.PosiTop', 'top paste': r'.*\.PasteTop', 'bottom copper': r'.*\.Bot', - 'bottop mask': r'.*\.StopBot', - 'bottop silk': r'.*\.PosiBot', - 'bottop paste': r'.*\.PasteBot', - 'drill outline': r'.*\.Outline', + 'bottom mask': r'.*\.StopBot', + 'bottom silk': r'.*\.PosiBot', + 'bottom paste': r'.*\.PasteBot', + 'mechanical outline': r'.*\.Outline', 'drill plated': r'.*\.Drill', }, @@ -77,11 +77,11 @@ MATCH_RULES = { 'top silk': r'.*\.sst', 'top paste': r'.*\.spt', 'bottom copper': r'.*\.bot', - 'bottop mask': r'.*\.smb', - 'bottop silk': r'.*\.ssb', - 'bottop paste': r'.*\.spb', + 'bottom mask': r'.*\.smb', + 'bottom silk': r'.*\.ssb', + 'bottom paste': r'.*\.spb', 'inner copper': r'.*\.in([0-9]+)', - 'drill outline': r'.*\.(fab|drd)', + 'mechanical outline': r'.*\.(fab|drd)', 'drill plated': r'.*\.tap', 'drill nonplated': r'.*\.npt', }, @@ -97,12 +97,12 @@ MATCH_RULES = { 'bottom silk': r'.*(\.pls|\.bsk|\.bottomsilkscreen\.ger)|.*(silkscreen_bottom|bottom_silk).*', 'bottom paste': r'.*(\.crs|\.bsp|\.bcream\.ger)|.*(solderpaste_bottom|bottom_paste).*', 'inner copper': r'.*\.ly([0-9]+)|.*\.internalplane([0-9]+)\.ger', - 'drill outline': r'.*(\.dim|\.mil|\.gml)|.*\.(?:board)?outline\.ger|profile\.gbr', + 'mechanical outline': r'.*(\.dim|\.mil|\.gml)|.*\.(?:board)?outline\.ger|profile\.gbr', 'drill plated': r'.*\.(txt|exc|drd|xln)', }, 'siemens': { - 'drill outline': r'.*ContourPlated.ncd', + 'mechanical outline': r'.*ContourPlated.ncd', 'inner copper': r'.*L([0-9]+).gdo', 'bottom silk': r'.*SilkscreenBottom.gdo', 'top silk': r'.*SilkscreenTop.gdo', diff --git a/gerbonara/gerber/layers.py b/gerbonara/gerber/layers.py index a439de3..69f9316 100644 --- a/gerbonara/gerber/layers.py +++ b/gerbonara/gerber/layers.py @@ -30,7 +30,7 @@ from .layer_rules import MATCH_RULES STANDARD_LAYERS = [ - 'outline', + 'mechanical outline', 'top copper', 'top mask', 'top silk', @@ -49,10 +49,12 @@ def match_files(filenames): matches[generator] = gen for layer, regex in rules.items(): for fn in filenames: - if (m := re.fullmatch(regex, fn.name.lower())): + if (m := re.fullmatch(regex, fn.name, re.IGNORECASE)): if layer == 'inner copper': - layer = 'inner_' + ''.join(m.groups()) + ' copper' - gen[layer] = gen.get(layer, []) + [fn] + target = 'inner_' + ''.join(e or '' for e in m.groups()) + ' copper' + else: + target = layer + gen[target] = gen.get(target, []) + [fn] return matches def best_match(filenames): @@ -88,59 +90,67 @@ def common_prefix(l): def autoguess(filenames): prefix = common_prefix([f.name for f in filenames]) - matches = { layername_autoguesser(f.name[len(prefix):] if f.name.startswith(prefix) else f.name): f - for f in filenames } + matches = {} + for f in filenames: + name = layername_autoguesser(f.name[len(prefix):] if f.name.startswith(prefix) else f.name) + if name != 'unknown unknown': + matches[name] = matches.get(name, []) + [f] inner_layers = [ m for m in matches if 'inner' in m ] - if len(inner_layers) >= 4 and not 'copper top' in matches and not 'copper bottom' in matches: - matches['copper top'] = matches.pop('copper inner1') - last_inner = sorted(inner_layers, key=lambda name: int(name.partition(' ')[0].partition('_')[2]))[-1] - matches['copper bottom'] = matches.pop(last_inner) + if len(inner_layers) >= 4 and 'copper top' not in matches and 'copper bottom' not in matches: + if 'inner_01 copper' in matches: + warnings.warn('Could not find copper layer. Re-assigning outermost inner layers to top/bottom copper.') + matches['top copper'] = matches.pop('inner_01 copper') + last_inner = sorted(inner_layers, key=lambda name: int(name.partition(' ')[0].partition('_')[2]))[-1] + matches['bottom copper'] = matches.pop(last_inner) return matches def layername_autoguesser(fn): - fn, _, _ext = fn.lower().rpartition('.') + fn, _, ext = fn.lower().rpartition('.') + + if ext == 'log': + return 'unknown unknown' side, use = 'unknown', 'unknown' - if re.match('top|front|pri?m?(ary)?', fn): + if re.search('top|front|pri?m?(ary)?', fn): side = 'top' use = 'copper' - if re.match('bot(tom)?|back|sec(ondary)?', fn): + if re.search('bot(tom)?|back|sec(ondary)?', fn): side = 'bottom' use = 'copper' - if re.match('silks?(creen)?', fn): + if re.search('silks?(creen)?', fn): use = 'silk' - elif re.match('(solder)?paste', fn): + elif re.search('(solder)?paste', fn): use = 'paste' - elif re.match('(solder)?mask', fn): + elif re.search('(solder)?mask', fn): use = 'mask' - elif (m := re.match(r'(la?y?e?r?|in(ner)?)\W*(?P[0-9]+)', fn)): - use = 'copper' - side = f'inner_{m["num"]:02d}' - - elif re.match('film', fn): - use = 'copper' - - elif re.match('out(line)?', fn): - use = 'drill' - side = 'outline' - - elif re.match('drill|rout?e?', fn): + elif re.search('drill|rout?e?', fn): use = 'drill' side = 'unknown' - if re.match(r'np(th)?|(non|un)\W*plated|(non|un)\Wgalv', fn): + if re.search(r'np(th)?|(non|un)\W*plated|(non|un)\Wgalv', fn): side = 'nonplated' - elif re.match('pth|plated|galv', fn): + elif re.search('pth|plated|galv', fn): side = 'plated' - return f'{use} {side}' + elif (m := re.search(r'(la?y?e?r?|in(ner)?)\W*(?P[0-9]+)', fn)): + use = 'copper' + side = f'inner_{int(m["num"]):02d}' + + elif re.search('film', fn): + use = 'copper' + + elif re.search('out(line)?', fn): + use = 'mechanical' + side = 'outline' + + return f'{side} {use}' class LayerStack: @classmethod @@ -160,7 +170,7 @@ class LayerStack: if len(filemap) < 6: raise ValueError('Cannot figure out gerber file mapping. Partial map is: ', filemap) - elif generator == 'geda': + if generator == 'geda': # geda is written by geniuses who waste no bytes of unnecessary output so it doesn't actually include the # number format in files that use imperial units. Unfortunately it also doesn't include any hints that the # file was generated by geda, so we have to guess by context whether this is just geda being geda or @@ -182,20 +192,39 @@ class LayerStack: raise SystemError('Cannot figure out gerber file mapping') # FIXME use layer metadata from comments and ipc file if available + elif generator == 'altium': + excellon_settings = None + + if 'mechanical outline' in filemap: + # Use lowest-numbered mechanical layer as outline, ignore others. + mechs = {} + for layer in filemap['mechanical outline']: + if layer.name.lower().endswith('gko'): + filemap['mechanical outline'] = [layer] + break + + if (m := re.match(r'.*\.gm([0-9]+)', layer.name, re.IGNORECASE)): + mechs[int(m[1])] = layer + else: + break + else: + filemap['mechanical outline'] = [sorted(mechs.items(), key=lambda x: x[0])[0][1]] + else: excellon_settings = None - if any(len(value) > 1 for value in filemap.values()): - raise SystemError('Ambgiuous layer names') + ambiguous = [ key for key, value in filemap.items() if len(value) > 1 and not 'drill' in key ] + if ambiguous: + raise SystemError(f'Ambiguous layer names for {", ".join(ambiguous)}') drill_layers = [] - layers = { key: None for key in STANDARD_LAYERS } + layers = {} # { tuple(key.split()): None for key in STANDARD_LAYERS } for key, paths in filemap.items(): if len(paths) > 1 and not 'drill' in key: raise ValueError(f'Multiple matching files found for {key} layer: {", ".join(value)}') for path in paths: - if 'outline' in key or 'drill' in key and identify_file(path.read_text()) != 'gerber': + if ('outline' in key or 'drill' in key) and identify_file(path.read_text()) != 'gerber': if 'nonplated' in key: plated = False elif 'plated' in key: @@ -206,8 +235,8 @@ class LayerStack: else: layer = GerberFile.open(path) - if key == 'drill outline': - layers['outline'] = layer + if key == 'mechanical outline': + layers['mechanical', 'outline'] = layer elif 'drill' in key: drill_layers.append(layer) @@ -221,9 +250,9 @@ class LayerStack: warnings.warn('File identification returned ambiguous results. Please raise an issue on the gerbonara ' 'tracker and if possible please provide these input files for reference.') - board_name = common_prefix([f.name for f in filemap.values()]) - board_name = re.subs(r'^\W+', '', board_name) - board_name = re.subs(r'\W+$', '', board_name) + board_name = common_prefix([l.original_path.name for l in layers.values() if l is not None]) + board_name = re.sub(r'^\W+', '', board_name) + board_name = re.sub(r'\W+$', '', board_name) return kls(layers, drill_layers, board_name=board_name) def __init__(self, graphic_layers, drill_layers, board_name=None): @@ -231,6 +260,13 @@ class LayerStack: self.drill_layers = drill_layers self.board_name = board_name + def __str__(self): + names = [ f'{side} {use}' for side, use in self.graphic_layers ] + return f'' + + def __repr__(self): + return str(self) + def merge_drill_layers(self): target = ExcellonFile(comments='Drill files merged by gerbonara') @@ -283,7 +319,9 @@ class LayerStack: def drill_layers(self): if self._drill_layers: return self._drill_layers - return [self.drill_pth, self.drill_npth, self.drill_unknown] + if self.drill_pth or self.drill_npth or self.drill_unknown: + return [self.drill_pth, self.drill_npth, self.drill_unknown] + return [] @drill_layers.setter def drill_layers(self, value): @@ -305,17 +343,17 @@ class LayerStack: return (side, use) in self.layers elif isinstance(index, tuple): - return index in self.layers + return index in self.graphic_layers return index < len(self.copper_layers) def __getitem__(self, index): if isinstance(index, str): side, _, use = index.partition(' ') - return self.layers[(side, use)] + return self.graphic_layers[(side, use)] elif isinstance(index, tuple): - return self.layers[index] + return self.graphic_layers[index] return self.copper_layers[index] @@ -336,15 +374,15 @@ class LayerStack: @property def top_side(self): - return { key: self[key] for key in ('top copper', 'top mask', 'top silk', 'top paste', 'outline') } + return { key: self[key] for key in ('top copper', 'top mask', 'top silk', 'top paste', 'mechanical outline') } @property def bottom_side(self): - return { key: self[key] for key in ('bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'outline') } + return { key: self[key] for key in ('bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'mechanical outline') } @property def outline(self): - return self['outline'] + return self['mechanical outline'] def _merge_layer(self, target, source): if source is None: @@ -392,7 +430,7 @@ class LayerStack: for i, layer in enumerate(new_inner, start=1): self[f'inner_{i} copper'] = layer - self._merge_layer('outline', other['outline']) + self._merge_layer('mechanical outline', other['mechanical outline']) self.normalize_drill_layers() other.normalize_drill_layers() diff --git a/gerbonara/gerber/rs274x.py b/gerbonara/gerber/rs274x.py index 5973ead..07dbac4 100644 --- a/gerbonara/gerber/rs274x.py +++ b/gerbonara/gerber/rs274x.py @@ -53,9 +53,9 @@ class GerberFile(CamFile): The GerberFile class represents a single gerber file. """ - def __init__(self, objects=None, comments=None, import_settings=None, filename=None, generator_hints=None, + def __init__(self, objects=None, comments=None, import_settings=None, original_path=None, generator_hints=None, layer_hints=None, file_attrs=None): - super().__init__(filename=filename) + super().__init__(original_path=original_path) self.objects = objects or [] self.comments = comments or [] self.generator_hints = generator_hints or [] @@ -157,7 +157,7 @@ class GerberFile(CamFile): with open(filename, "r") as f: if enable_includes and enable_include_dir is None: enable_include_dir = filename.parent - return kls.from_string(f.read(), enable_include_dir, filename=filename.name) + return kls.from_string(f.read(), enable_include_dir, filename=filename) @classmethod def from_string(kls, data, enable_include_dir=None, filename=None): @@ -217,7 +217,8 @@ class GerberFile(CamFile): yield 'M02*' def __str__(self): - return f'' + name = f'{self.original_path.name} ' if self.original_path else '' + return f'' def save(self, filename, settings=None, drop_comments=True): with open(filename, 'w', encoding='utf-8') as f: # Encoding is specified as UTF-8 by spec. @@ -637,6 +638,7 @@ class GerberParser: self.target.import_settings = self.file_settings self.target.unit = self.file_settings.unit self.target.file_attrs = self.file_attrs + self.target.original_path = filename if not self.eof_found: self.warn('File is missing mandatory M02 EOF marker. File may be truncated.') diff --git a/gerbonara/gerber/tests/test_layers.py b/gerbonara/gerber/tests/test_layers.py index 62ef522..482d340 100644 --- a/gerbonara/gerber/tests/test_layers.py +++ b/gerbonara/gerber/tests/test_layers.py @@ -31,11 +31,11 @@ REFERENCE_DIRS = { 'IRNASIoTbank1.2.Bot': 'bottom copper', 'IRNASIoTbank1.2.Drill': 'drill plated', 'IRNASIoTbank1.2.Info': None, - 'IRNASIoTbank1.2.Outline': 'drill outline', + 'IRNASIoTbank1.2.Outline': 'mechanical outline', 'IRNASIoTbank1.2.PasteBot': 'bottom paste', 'IRNASIoTbank1.2.PasteTop': 'top paste', - 'IRNASIoTbank1.2.PosiBot': 'bottom silkscreen', - 'IRNASIoTbank1.2.PosiTop': 'top silkscreen', + 'IRNASIoTbank1.2.PosiBot': 'bottom silk', + 'IRNASIoTbank1.2.PosiTop': 'top silk', 'IRNASIoTbank1.2.StopBot': 'bottom mask', 'IRNASIoTbank1.2.StopTop': 'top mask', 'IRNASIoTbank1.2.Tool': None, @@ -45,7 +45,7 @@ REFERENCE_DIRS = { 'allegro': { '08_057494d-ipc356.ipc': None, - '08_057494d.rou': 'drill outline', + '08_057494d.rou': 'mechanical outline', 'Read_Me.1': None, 'art_param.txt': None, 'assy1.art': None, @@ -73,7 +73,7 @@ REFERENCE_DIRS = { 'MINNOWMAX_REVA2_PUBLIC_TOPSIDE.pdf': None, 'MinnowMax_RevA1_IPC356A.ipc': None, 'MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCDRILL.drl': 'drill unknown', - 'MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCROUTE.rou': 'drill outline', + 'MinnowMax_RevA1_DRILL/MinnowMax_RevA1_NCROUTE.rou': 'drill unknown', 'MinnowMax_RevA1_DRILL/nc_param.txt': None, 'MinnowMax_RevA1_DRILL/ncdrill.log': None, 'MinnowMax_RevA1_DRILL/ncroute.log': None, @@ -116,15 +116,15 @@ REFERENCE_DIRS = { 'Gerber/LimeSDR-QPCIe_1v2.GBO': 'bottom silk', 'Gerber/LimeSDR-QPCIe_1v2.GBP': 'bottom paste', 'Gerber/LimeSDR-QPCIe_1v2.GBS': 'bottom mask', - 'Gerber/LimeSDR-QPCIe_1v2.GM1': 'dirll outlinej', + 'Gerber/LimeSDR-QPCIe_1v2.GM1': 'mechanical outline', 'Gerber/LimeSDR-QPCIe_1v2.GM14': None, 'Gerber/LimeSDR-QPCIe_1v2.GM15': None, 'Gerber/LimeSDR-QPCIe_1v2.GPB': None, 'Gerber/LimeSDR-QPCIe_1v2.GPT': None, - 'Gerber/LimeSDR-QPCIe_1v2.GTL': 'bottom copper', - 'Gerber/LimeSDR-QPCIe_1v2.GTO': 'bottom silk', - 'Gerber/LimeSDR-QPCIe_1v2.GTP': 'bottom paste', - 'Gerber/LimeSDR-QPCIe_1v2.GTS': 'bottom mask', + 'Gerber/LimeSDR-QPCIe_1v2.GTL': 'top copper', + 'Gerber/LimeSDR-QPCIe_1v2.GTO': 'top silk', + 'Gerber/LimeSDR-QPCIe_1v2.GTP': 'top paste', + 'Gerber/LimeSDR-QPCIe_1v2.GTS': 'top mask', 'Gerber/LimeSDR-QPCIe_1v2.REP': None, 'Gerber/LimeSDR-QPCIe_1v2.RUL': None, 'Gerber/LimeSDR-QPCIe_1v2.apr': None, @@ -134,22 +134,23 @@ REFERENCE_DIRS = { 'NC Drill/LimeSDR-QPCIe_1v2.LDP': None, }, - 'diptrace': { - 'mainboard.drl': 'drill plated', - 'mainboard_BoardOutline.gbr': 'drill outline', - 'mainboard_Bottom.gbr': 'bottom copper', - 'mainboard_BottomMask.gbr': 'bottom mask', - 'mainboard_Top.gbr': 'top copper', - 'mainboard_TopMask.gbr': 'top mask', - 'mainboard_TopSilk.gbr': 'top silk', - }, +# TODO there are three designs in this folder. make test work with that. +# 'diptrace': { +# 'mainboard.drl': 'drill plated', +# 'mainboard_BoardOutline.gbr': 'mechanical outline', +# 'mainboard_Bottom.gbr': 'bottom copper', +# 'mainboard_BottomMask.gbr': 'bottom mask', +# 'mainboard_Top.gbr': 'top copper', +# 'mainboard_TopMask.gbr': 'top mask', +# 'mainboard_TopSilk.gbr': 'top silk', +# }, 'eagle-newer': { 'copper_bottom.gbr': 'bottom copper', 'copper_top.gbr': 'top copper', 'drills.xln': 'drill unknown', 'gerber_job.gbrjob': None, - 'profile.gbr': 'drill outline', + 'profile.gbr': 'mechanical outline', 'silkscreen_bottom.gbr': 'bottom silk', 'silkscreen_top.gbr': 'top silk', 'soldermask_bottom.gbr': 'bottom mask', @@ -163,7 +164,7 @@ REFERENCE_DIRS = { 'copper_inner_l2.gbr': 'inner_2 copper', 'copper_inner_l3.gbr': 'inner_3 copper', 'copper_top_l1.gbr': 'top copper', - 'profile.gbr': 'drill outline', + 'profile.gbr': 'mechanical outline', 'silkscreen_bottom.gbr': 'bottom silk', 'silkscreen_top.gbr': 'top silk', 'soldermask_bottom.gbr': 'bottom mask', @@ -173,7 +174,7 @@ REFERENCE_DIRS = { }, 'easyeda': { - 'Gerber_BoardOutline.GKO': 'drill outline', + 'Gerber_BoardOutline.GKO': 'mechanical outline', 'Gerber_BottomLayer.GBL': 'bottom copper', 'Gerber_BottomSolderMaskLayer.GBS': 'bottom mask', 'Gerber_Drill_NPTH.DRL': 'drill nonplated', @@ -190,7 +191,7 @@ REFERENCE_DIRS = { }, 'fritzing': { - 'combined.GKO': 'drill outline', + 'combined.GKO': 'mechanical outline', 'combined.gbl': 'bottom copper', 'combined.gbo': 'bottom silk', 'combined.gbs': 'bottom mask', @@ -222,7 +223,7 @@ REFERENCE_DIRS = { 'power-art.gbo': 'bottom silk', 'power-art.gbp': 'bottom paste', 'power-art.gbs': 'bottom mask', - 'power-art.gko': 'drill outline', + 'power-art.gko': 'mechanical outline', 'power-art.gtl': 'top copper', 'power-art.gto': 'top silk', 'power-art.gtp': 'top paste', @@ -232,7 +233,7 @@ REFERENCE_DIRS = { }, 'siemens': { - '80101_0125_F200_ContourPlated.ncd': 'drill outline', + '80101_0125_F200_ContourPlated.ncd': 'mechanical outline', '80101_0125_F200_DrillDrawingThrough.gdo': None, '80101_0125_F200_L01_Top.gdo': 'top copper', '80101_0125_F200_L02.gdo': 'inner_2 copper', @@ -257,7 +258,7 @@ REFERENCE_DIRS = { }, 'siemens-2': { - 'Gerber/BoardOutlline.gdo': 'drill outline', + 'Gerber/BoardOutlline.gdo': 'mechanical outline', 'Gerber/DrillDrawingThrough.gdo': None, 'Gerber/EtchLayerBottom.gdo': 'bottom copper', 'Gerber/EtchLayerTop.gdo': 'top copper', @@ -267,7 +268,7 @@ REFERENCE_DIRS = { 'Gerber/SolderPasteTop.gdo': 'top paste', 'Gerber/SoldermaskBottom.gdo': 'bottom mask', 'Gerber/SoldermaskTop.gdo': 'top mask', - 'NCDrill/ContourPlated.ncd': 'drill outline', + 'NCDrill/ContourPlated.ncd': 'mechanical outline', 'NCDrill/ThruHoleNonPlated.ncd': 'drill nonplated', 'NCDrill/ThruHolePlated.ncd': 'drill plated', }, @@ -278,7 +279,7 @@ REFERENCE_DIRS = { 'design_export.gbo': 'bottom silk', 'design_export.gbp': 'bottom paste', 'design_export.gbs': 'bottom mask', - 'design_export.gko': 'drill outline', + 'design_export.gko': 'mechanical outline', 'design_export.gtl': 'top copper', 'design_export.gto': 'top silk', 'design_export.gtp': 'top paste', @@ -295,27 +296,34 @@ def test_layer_classifier(ref_dir): path = reference_path(ref_dir) print('Reference path is', path) file_map = { filename: role for filename, role in file_map.items() if role is not None } - rev_file_map = { value: key for key, value in file_map.items() } + rev_file_map = { tuple(value.split()): key for key, value in file_map.items() } drill_files = { filename: role for filename, role in file_map.items() if role.startswith('drill') } stack = LayerStack.from_directory(path) + print('loaded layers:', ', '.join(f'{side} {use}' for side, use in stack.graphic_layers)) for side in 'top', 'bottom': for layer in 'copper', 'silk', 'mask', 'paste': + if 'allegro-2' in ref_dir and layer in ('silk', 'mask', 'paste'): + # This particular example has very poorly named files + continue if (side, layer) in rev_file_map: assert (side, layer) in stack found = stack[side, layer] assert isinstance(found, GerberFile) - assert found.filename == Path(rev_file_map[side, layer]).name + assert found.original_path.name == Path(rev_file_map[side, layer]).name else: # not in file_map assert (side, layer) not in stack - for filename, role in drill_files: - assert any(layer.filename == Path(filename).name for layer in stack.drill_layers) - assert len(stack.drill_layers) == len(drill_files) + assert len(stack.drill_layers) == len(drill_files) + + for filename, role in drill_files.items(): + print('drill:', filename, role) + print([(layer.original_path, layer.original_path == Path(filename).name) for layer in stack.drill_layers]) + assert any(layer.original_path.name == Path(filename).name for layer in stack.drill_layers) - for layer in stack.drill_files: + for layer in stack.drill_layers: assert isinstance(layer, ExcellonFile) diff --git a/gerbonara/gerber/utils.py b/gerbonara/gerber/utils.py index 00ba41e..5bc03e5 100644 --- a/gerbonara/gerber/utils.py +++ b/gerbonara/gerber/utils.py @@ -46,7 +46,6 @@ class RegexMatcher: def handle(self, inst, line): for regex, handler in self.mapping.items(): if (match := re.fullmatch(regex, line)): - #print(' handler', handler.__name__) handler(inst, match) return True else: -- cgit