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.py252
1 files changed, 211 insertions, 41 deletions
diff --git a/gerbonara/gerber/layers.py b/gerbonara/gerber/layers.py
index 9e34bd8..26dd9c1 100644
--- a/gerbonara/gerber/layers.py
+++ b/gerbonara/gerber/layers.py
@@ -23,6 +23,20 @@ from collections import namedtuple
from .excellon import ExcellonFile
from .ipc356 import IPCNetlist
+
+STANDARD_LAYERS = [
+ 'outline',
+ 'top copper',
+ 'top mask',
+ 'top silk',
+ 'top paste',
+ 'bottom copper',
+ 'bottom mask',
+ 'bottom silk',
+ 'bottom paste',
+ ]
+
+
def match_files(filenames):
matches = {}
for generator, rules in MATCH_RULES.items():
@@ -84,51 +98,43 @@ def layername_autoguesser(fn):
fn, _, _ext = fn.lower().rpartition('.')
side, use = 'unknown', 'unknown'
- hint = ''
if re.match('top|front|pri?m?(ary)?', fn):
side = 'top'
use = 'copper'
- if re.match('bot|bottom|back|sec(ondary)?', fn):
+ if re.match('bot(tom)?|back|sec(ondary)?', fn):
side = 'bottom'
use = 'copper'
if re.match('silks?(creen)?', fn):
use = 'silk'
+
elif re.match('(solder)?paste', fn):
use = 'paste'
+
elif re.match('(solder)?mask', fn):
use = 'mask'
- elif (m := re.match('([tbcps])sm([tbcps])', fn)):
- use = 'mask'
- hint = m[1] + m[2]
- elif (m := re.match('([tbcps])sp([tbcps])', fn)):
- use = 'paste'
- hint = m[1] + m[2]
- elif (m := re.match('([tbcps])sl?k([tbcps])', fn)):
- use = 'silk'
- hint = m[1] + m[2]
- elif (m := re.match('(la?y?e?r?|inn?e?r?)\W*([0-9]+)', fn)):
+
+ elif (m := re.match('(la?y?e?r?|in(ner)?)\W*(?P<num>[0-9]+)', fn)):
use = 'copper'
- side = f'inner_{m[1]}'
+ side = f'inner_{m["num"]:02d}'
+
elif re.match('film', fn):
use = 'copper'
- elif re.match('drill|rout?e?|outline'):
+
+ elif re.match('out(line)?'):
+ use = 'drill'
+ side = 'outline'
+
+ elif re.match('drill|rout?e?'):
use = 'drill'
side = 'unknown'
if re.match('np(th)?|(non|un)\W*plated|(non|un)\Wgalv', fn):
side = 'nonplated'
+
elif re.match('pth|plated|galv', fn):
side = 'plated'
- if side is None and hint:
- hint = set(hint)
- if len(hint) == 1:
- and hint[0] in 'tpc':
- side = 'top'
- else
- side = 'bottom'
-
return f'{use} {side}'
class LayerStack:
@@ -175,35 +181,199 @@ class LayerStack:
if any(len(value) > 1 for value in filemap.values()):
raise SystemError('Ambgiuous layer names')
- filemap = { key: values[0] for key, value in filemap.items() }
+ drill_layers = []
+ layers = { key: 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 'nonplated' in key:
+ plated = False
+ elif 'plated' in key:
+ plated = True
+ else:
+ plated = None
+ layer = ExcellonFile.open(path, plated=plated, settings=excellon_settings)
+ else:
+ layer = GerberFile.open(path)
+
+ if key == 'drill outline':
+ layers['outline'] = layer
+
+ elif 'drill' in key:
+ drill_layers.append(layer)
- layers = {}
- for key, path in filemap.items():
- 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:
- plated = True
else:
- plated = None
- layers[key] = ExcellonFile.open(path, plated=plated, settings=excellon_settings)
- else:
- layers[key] = GerberFile.open(path)
+ side, _, use = key.partition(' ')
+ layers[(side, use)] = layer
- hints = { layers[key].generator_hints } + { generator }
- if len(hints) > 1:
- 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.')
+ hints = { layer.generator_hints } + { generator }
+ if len(hints) > 1:
+ 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('^\W+', '', board_name)
board_name = re.subs('\W+$', '', board_name)
- return kls(layers, board_name=board_name)
+ return kls(layers, drill_layers, board_name=board_name)
- def __init__(self, layers, board_name=None):
- self.layers = layers
+ def __init__(self, graphic_layers, drill_layers, board_name=None):
+ self.graphic_layers = graphic_layers
+ self.-drill_layers = drill_layers
self.board_name = board_name
+ def merge_drill_layers(self):
+ target = ExcellonFile(comments='Drill files merged by gerbonara')
+
+ for layer in self.drill_layers:
+ if isinstance(layer, GerberFile):
+ layer = layer.to_excellon()
+
+ target.merge(layer)
+
+ self.drill_layers = [target]
+
+ def normalize_drill_layers(self):
+ drill_pth, drill_npth, drill_aux = [], [], []
+
+ for layer in self.drill_layers:
+ if isinstance(layer, GerberFile):
+ layer = layer.to_excellon()
+
+ if layer.is_plated:
+ drill_pth.append(layer)
+ elif layer.is_nonplated:
+ drill_pth.append(layer)
+ else:
+ drill_aux.append(layer)
+
+ pth_out, *rest = drill_pth or [ExcellonFile()]
+ for layer in rest:
+ pth_out.merge(layer)
+
+ npth_out, *rest = drill_npth or [ExcellonFile()]
+ for layer in rest:
+ npth_out.merge(layer)
+
+ unknown_out = ExcellonFile()
+ for layer in drill_aux:
+ for obj in layer.objects:
+ if obj.plated is None:
+ unknown_out.append(obj)
+ elif obj.plated:
+ pth_out.append(obj)
+ else:
+ npth_out.append(obj)
+
+ self.drill_pth, self.drill_npth = pth_out, npth_out
+ self.drill_unknown = unknown_out if unknown_out else None
+ self._drill_layers = []
+
+ @property
+ def drill_layers(self):
+ if self._drill_layers:
+ return self._drill_layers
+ return [self.drill_pth, self.drill_npth, self.drill_unknown]
+
+ @drill_layers.setter
+ def drill_layers(self, value):
+ self._drill_layers = value
+ self.drill_pth = self.drill_npth = self.drill_unknown = None
+
def __len__(self):
return len(self.layers)
+ def __getitem__(self, index):
+ if isinstance(index, str):
+ side, _, use = index.partition(' ')
+ return self.layers.get((side, use))
+
+ elif isinstance(index, tuple):
+ return self.layers.get(index)
+
+ return self.copper_layers[index]
+
+ @property
+ def copper_layers(self):
+ copper_layers = [ (key, layer) for key, layer in self.layers.items() if key.endswith('copper') ]
+
+ def sort_layername(val):
+ key, _layer = val
+ if key.startswith('top'):
+ return -1
+ if key.startswith('bottom'):
+ return 1e99
+ assert key.startswith('inner_')
+ return int(key[len('inner_'):])
+
+ return [ layer for _key, layer in sorted(copper_layers, key=sort_layername) ]
+
+ @property
+ def top_side(self):
+ return { key: self[key] for key in ('top copper', 'top mask', 'top silk', 'top paste', 'outline') }
+
+ @property
+ def bottom_side(self):
+ return { key: self[key] for key in ('bottom copper', 'bottom mask', 'bottom silk', 'bottom paste', 'outline') }
+
+ @property
+ def outline(self):
+ return self['outline']
+
+ def _merge_layer(self, target, source):
+ if source is None:
+ return
+
+ if self[target] is None:
+ self[target] = source
+
+ else:
+ self[target].merge(source)
+
+ def merge(self, other):
+ all_keys = set(self.layers.keys()) | set(other.layers.keys())
+ exclude = { key.split() for key in STANDARD_LAYERS }
+ all_keys = { key for key in all_keys if key not in exclude }
+ if all_keys:
+ warnings.warn('Cannot merge unknown layer types: {" ".join(all_keys)}')
+
+ for side in 'top', 'bottom':
+ for use in 'copper', 'mask', 'silk', 'paste':
+ self._merge_layer((side, use), other[side, use])
+
+ our_inner, their_inner = self.copper_layers[1:-1], other.copper_layers[1:-1]
+
+ if bool(our_inner) != bool(their_inner):
+ warnings.warn('Merging board without inner layers into board with inner layers, inner layers will be empty on first board.')
+
+ elif our_inner and their_inner:
+ warnings.warn('Merging boards with different inner layer counts. Will fill inner layers starting at core.')
+
+ diff = len(our_inner) - len(their_inner)
+ their_inner = ([None] * max(0, diff//2)) + their_inner + ([None] * max(0, diff//2))
+ our_inner = ([None] * max(0, -diff//2)) + their_inner + ([None] * max(0, -diff//2))
+
+ new_inner = []
+ for ours, theirs in zip(our_inner, their_inner):
+ if ours is None:
+ new_inner.append(theirs)
+ elif theirs is None:
+ new_inner.append(ours)
+ else:
+ ours.merge(theirs)
+ new_inner.append(ours)
+
+ for i, layer in enumerate(new_inner, start=1):
+ self[f'inner_{i} copper'] = layer
+
+ self._merge_layer('outline', other['outline'])
+
+ self.normalize_drill_layers()
+ other.normalize_drill_layers()
+
+ self.drill_pth.merge(other.drill_pth)
+ self.drill_npth.merge(other.drill_npth)
+ self.drill_unknown.merge(other.drill_unknown)
+