From 25ebdbe625b69454edfc6d505d692e44f3848fcc Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 24 Jun 2022 13:21:44 +0200 Subject: protoboard: add tight layout option --- gerbolyze/protoboard.py | 110 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 34 deletions(-) (limited to 'gerbolyze/protoboard.py') diff --git a/gerbolyze/protoboard.py b/gerbolyze/protoboard.py index 0ff7aea..4de98aa 100644 --- a/gerbolyze/protoboard.py +++ b/gerbolyze/protoboard.py @@ -90,6 +90,11 @@ class PatternProtoArea: raise ValueError('Pattern has different X and Y pitches') return self.pitch_x + def fit_size(self, defs, w, h): + x, y, w, h = self.fit_rect(0, 0, w, h, False) + t, r, b, l = self.border + return (w+l+r), (h+t+b) + def fit_rect(self, x, y, w, h, center=True): t, r, b, l = self.border x, y, w, h = (x+l), (y+t), (w-l-r), (h-t-b) @@ -105,8 +110,8 @@ class PatternProtoArea: else: return x, y, w_fit, h_fit - def generate(self, x, y, w, h, defs=None, center=True, clip=''): - return {} + def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False): + yield {} class EmptyProtoArea: def __init__(self, copper=False, border=None): @@ -122,13 +127,16 @@ class EmptyProtoArea: else: self.border = (border, border, border, border) - def generate(self, x, y, w, h, defs=None, center=True, clip=''): + def fit_size(self, defs, w, h): + return w, h + + def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False): if self.copper: t, r, b, l = self.border x, y, w, h = x+l, y+t, w-l-r, h-t-b - return { 'top copper': f'' } + yield { 'top copper': f'' } else: - return {} + yield {} class THTProtoAreaCircles(PatternProtoArea): def __init__(self, pad_dia=2.0, drill=1.0, pitch=2.54, sides='both', plated=True, border=None): @@ -141,7 +149,7 @@ class THTProtoAreaCircles(PatternProtoArea): self.plated = plated self.sides = sides - def generate(self, x, y, w, h, defs=None, center=True, clip=''): + def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False): x, y, w, h = self.fit_rect(x, y, w, h, center) drill = 'plated drill' if self.plated else 'nonplated drill' @@ -160,7 +168,7 @@ class THTProtoAreaCircles(PatternProtoArea): d['bottom copper'] = make_rect(pad_id, x, y, w, h, clip) d['bottom mask'] = make_rect(pad_id, x, y, w, h, clip) - return d + yield d def __repr__(self): return f'THTCircles(d={self.pad_dia}, h={self.drill}, p={self.pitch}, sides={self.sides}, plated={self.plated})' @@ -174,10 +182,10 @@ class SMDProtoAreaRectangles(PatternProtoArea): self.pad_pattern = RectPattern(w, h, pitch_x, pitch_y) self.patterns = [self.pad_pattern] - def generate(self, x, y, w, h, defs=None, center=True, clip=''): + def generate(self, x, y, w, h, defs=None, center=True, clip='', tight_layout=False): x, y, w, h = self.fit_rect(x, y, w, h, center) pad_id = str(uuid.uuid4()) - return {'defs': [self.pad_pattern.svg_def(pad_id, x, y)], + yield {'defs': [self.pad_pattern.svg_def(pad_id, x, y)], 'top copper': make_rect(pad_id, x, y, w, h, clip), 'top mask': make_rect(pad_id, x, y, w, h, clip)} @@ -196,11 +204,12 @@ LAYERS = [ ] class ProtoBoard: - def __init__(self, defs, expr, mounting_holes=None, border=None, center=True): + def __init__(self, defs, expr, mounting_holes=None, border=None, center=True, tight_layout=False): self.defs = eval_defs(defs) self.layout = parse_layout(expr) self.mounting_holes = mounting_holes self.center = center + self.tight_layout = tight_layout if border is None: self.border = (0, 0, 0, 0) @@ -242,7 +251,7 @@ class ProtoBoard: f'' ]) t, r, b, l = self.border - for layer_dict in self.layout.generate(l, t, w-l-r, h-t-b, self.defs, self.center, clip): + for layer_dict in self.layout.generate(l, t, w-l-r, h-t-b, self.defs, self.center, clip, self.tight_layout): for l in LAYERS: if l in layer_dict: out[l].append(layer_dict[l]) @@ -288,25 +297,47 @@ class PropLayout: if len(content) != len(proportions): raise ValueError('proportions and content must have same length') - def generate(self, x, y, w, h, defs, center=True, clip=''): - for (c_x, c_y, c_w, c_h), child in self.layout_2d(x, y, w, h): + def generate(self, x, y, w, h, defs, center=True, clip='', tight_layout=False): + for (c_x, c_y, c_w, c_h), child in self.layout_2d(defs, x, y, w, h, tight_layout): + yield from child.generate(c_x, c_y, c_w, c_h, defs, center, clip, tight_layout) + + def fit_size(self, defs, w, h): + widths = [] + heights = [] + for (_x, _y, w, h), child in self.layout_2d(defs, 0, 0, w, h, True): + if not isinstance(child, EmptyProtoArea): + widths.append(w) + heights.append(h) + if self.direction == 'h': + return sum(widths), max(heights) + else: + return max(widths), sum(heights) + + def layout_2d(self, defs, x, y, w, h, tight_layout=False): + actual_l = 0 + target_l = 0 + for l, child in zip(self.layout(w if self.direction == 'h' else h), self.content): + this_x, this_y = x, y + this_w, this_h = w, h + target_l += l + if isinstance(child, str): - yield defs[child].generate(c_x, c_y, c_w, c_h, defs, center, clip) + child = defs[child] + if self.direction == 'h': + this_w = target_l - actual_l else: - yield from child.generate(c_x, c_y, c_w, c_h, defs, center, clip) + this_h = target_l - actual_l - def layout_2d(self, x, y, w, h): - for l, child in zip(self.layout(w if self.direction == 'h' else h), self.content): - this_w, this_h = w, h - this_x, this_y = x, y + if tight_layout: + this_w, this_h = child.fit_size(defs, this_w, this_h) if self.direction == 'h': - this_w = l - x += l + x += this_w + actual_l += this_w else: - this_h = l - y += l + y += this_h + actual_l += this_h yield (this_x, this_y, this_w, this_h), child @@ -327,7 +358,7 @@ class PropLayout: class TwoSideLayout: def __init__(self, top, bottom): - self.top, self.bottom = top, bottom + self._top, self._bottom = top, bottom def flip(self, defs): out = dict(defs) @@ -347,16 +378,27 @@ class TwoSideLayout: return defs - def generate(self, x, y, w, h, defs, center=True, clip=''): - if isinstance(self.top, str): - yield defs[self.top].generate(x, y, w, h, defs, center, clip) - else: - yield from self.top.generate(x, y, w, h, defs, center, clip) + def top(self, defs): + return defs[self._top] if isinstance(self._top, str) else self._top - if isinstance(self.bottom, str): - yield self.flip(defs[self.bottom].generate(x, y, w, h, defs, center, clip)) - else: - yield from map(self.flip, self.bottom.generate(x, y, w, h, defs, center, clip)) + def bottom(self, defs): + return defs[self._bottom] if isinstance(self._bottom, str) else self._bottom + + def fit_size(self, defs, w, h): + top, bottom = self.top(defs), self.bottom(defs) + w1, h1 = top.fit_size(defs, w, h) + w2, h2 = bottom.fit_size(defs, w, h) + if isinstance(top, EmptyProtoArea): + if isinstance(bottom, EmptyProtoArea): + return w1, h1 + return w2, h2 + if isinstance(bottom, EmptyProtoArea): + return w1, h1 + return max(w1, w2), max(h1, h2) + + def generate(self, x, y, w, h, defs, center=True, clip='', tight_layout=False): + yield from self.top(defs).generate(x, y, w, h, defs, center, clip, tight_layout) + yield from map(self.flip, self.bottom(defs).generate(x, y, w, h, defs, center, clip, tight_layout)) def _map_expression(node): if isinstance(node, ast.Name): @@ -496,5 +538,5 @@ if __name__ == '__main__': # print('===== Proto board =====') #b = ProtoBoard('tht = THTCircles(); tht_small = THTCircles(pad_dia=1.0, drill=0.6, pitch=1.27)', # 'tht@1in|(tht_small@2/tht@1)', mounting_holes=(3.2, 5.0, 5.0), border=2, center=False) - b = ProtoBoard('tht = THTCircles(); smd1 = SMDPads(0.8, 1.27); smd2 = SMDPads(0.95, 1.895); plane=Empty(copper=True)', 'tht@1in | (smd1 + plane)', mounting_holes=(3.2, 5.0, 5.0), border=2) + b = ProtoBoard('tht = THTCircles(); smd1 = SMDPads(2.0, 2.0); smd2 = SMDPads(0.95, 1.895); plane=Empty(copper=True)', 'tht@25mm | (smd1 + plane)', mounting_holes=(3.2, 5.0, 5.0), border=2, tight_layout=True) print(b.generate(80, 60)) -- cgit