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/layers.py | 136 +++++++++++++++++++++++++++++---------------- 1 file changed, 87 insertions(+), 49 deletions(-) (limited to 'gerbonara/gerber/layers.py') 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() -- cgit