summaryrefslogtreecommitdiff
path: root/gerbonara/gerber/layers.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/gerber/layers.py')
-rw-r--r--gerbonara/gerber/layers.py136
1 files changed, 87 insertions, 49 deletions
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<num>[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<num>[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'<LayerStack {self.board_name} [{", ".join(names)}] and {len(self.drill_layers)} drill layers>'
+
+ 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()