From 51327ccfeb9da40326a5f5b16e4925f77c92188b Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 5 Apr 2023 17:44:31 +0200 Subject: cad: Add pad numbering for protoboards --- gerbonara/cad/primitives.py | 25 +++++++--- gerbonara/cad/protoboard.py | 114 +++++++++++++++++++++++++++++++++++++++++++- gerbonara/utils.py | 3 ++ 3 files changed, 134 insertions(+), 8 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index 9000910..78fc238 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -80,9 +80,6 @@ class Board: warnings.warn(msg) elif keepout_errors == 'raise': raise KeepoutError(obj, ko, msg) - else: - import sys - #print('skip', obj.bounding_box(MM), ko, file=sys.stderr) return obj.parent = self @@ -190,10 +187,16 @@ class ObjectGroup(Positioned): bottom_paste: list = field(default_factory=list) drill_npth: list = field(default_factory=list) drill_pth: list = field(default_factory=list) + objects: list = field(default_factory=list) def render(self, layer_stack): x, y, rotation = self.abs_pos top, bottom = ('bottom', 'top') if self.side == 'bottom' else ('top', 'bottom') + + for obj in self.objects: + obj.parent = self + obj.render(layer_stack) + for target, source in [ (layer_stack[top, 'copper'], self.top_copper), (layer_stack[top, 'mask'], self.top_mask), @@ -205,6 +208,7 @@ class ObjectGroup(Positioned): (layer_stack[bottom, 'paste'], self.bottom_paste), (layer_stack.drill_pth, self.drill_pth), (layer_stack.drill_npth, self.drill_npth)]: + for fe in source: fe = copy(fe) fe.rotate(rotation) @@ -237,6 +241,9 @@ class Text(Positioned): newstroke_font = Newstroke() strokes = list(newstroke_font.render(self.text, size=self.font_size)) + if not strokes: + return + xs = [x for points in strokes for x, _y in points] ys = [y for points in strokes for _x, y in points] min_x, min_y, max_x, max_y = min(xs), min(ys), max(xs), max(ys) @@ -251,19 +258,25 @@ class Text(Positioned): raise ValueError('h_align must be one of "left", "center", or "right".') if self.v_align == 'top': - y0 = -max_y + y0 = -(max_y - min_y) elif self.v_align == 'middle': - y0 = -max_y/2 + y0 = -(max_y - min_y)/2 elif self.v_align == 'bottom': y0 = 0 else: raise ValueError('v_align must be one of "top", "middle", or "bottom".') + if self.side == 'bottom': + x0 += min_x + max_x + x_sign = -1 + else: + x_sign = 1 + ap = CircleAperture(self.stroke_width, unit=self.unit) for stroke in strokes: for (x1, y1), (x2, y2) in zip(stroke[:-1], stroke[1:]): - obj = Line(x0+x1, y0-y1, x0+x2, y0-y2, aperture=ap, unit=self.unit, polarity_dark=self.polarity_dark) + obj = Line(x0+x_sign*x1, y0-y1, x0+x_sign*x2, y0-y2, aperture=ap, unit=self.unit, polarity_dark=self.polarity_dark) obj.rotate(rotation) obj.offset(obj_x, obj_y) layer_stack[self.side, self.layer].objects.append(obj) diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 9eaabed..4a66b67 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -2,6 +2,8 @@ import sys import re import math +import string +import itertools from copy import copy, deepcopy import warnings @@ -137,12 +139,51 @@ class TwoSideLayout: yield obj +def numeric(start=1): + def gen(): + nonlocal start + for i in itertools.count(start): + yield str(i) + + return gen + + +def alphabetic(case='upper'): + if case not in ('lower', 'upper'): + raise ValueError('case must be one of "lower" or "upper".') + + index = string.ascii_lowercase if case == 'lower' else string.ascii_uppercase + + def gen(): + nonlocal index + + for i in itertools.count(): + out = '' + mod = 26 + + while i > 0: + rem = i % mod + i //= mod + mod *= 26 + out = index[rem] + out + + yield out + + return gen + + class PatternProtoArea: - def __init__(self, pitch_x, pitch_y=None, obj=None, unit=MM): + def __init__(self, pitch_x, pitch_y=None, obj=None, numbers=True, font_size=None, font_stroke=None, number_x_gen=alphabetic(), number_y_gen=numeric(), interval_x=5, interval_y=None, unit=MM): self.pitch_x = pitch_x self.pitch_y = pitch_y or pitch_x self.obj = obj self.unit = unit + self.numbers = numbers + self.font_size = font_size or unit(1.5, MM) + self.font_stroke = font_stroke or unit(0.2, MM) + self.interval_x = interval_x + self.interval_y = interval_y or (1 if self.pitch_y > self.font_size*1.2 else 5) + self.number_x_gen, self.number_y_gen = number_x_gen, number_y_gen def fit_size(self, w, h, unit=MM): (min_x, min_y), (max_x, max_y) = self.fit_rect(((0, 0), (w, h))) @@ -163,15 +204,50 @@ class PatternProtoArea: def generate(self, bbox, unit=MM): (x, y), (w, h) = bbox w, h = w-x, h-y + + if self.numbers: + x += self.font_size + y += self.font_size + w -= 2*self.font_size + h -= 2*self.font_size + n_x = int(w//unit(self.pitch_x, self.unit)) n_y = int(h//unit(self.pitch_y, self.unit)) off_x = (w % unit(self.pitch_x, self.unit)) / 2 off_y = (h % unit(self.pitch_y, self.unit)) / 2 + if self.numbers: + for i, lno_i in zip(range(n_y), self.number_x_gen()): + if (i+1) % self.interval_y == 0: + t_y = off_y + y + (i + 0.5) * self.pitch_y + + t_x = x + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'right', 'middle', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'right', 'middle', side='bottom', unit=self.unit) + + t_x = x + w + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'left', 'middle', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'left', 'middle', side='bottom', unit=self.unit) + + for i, lno_i in zip(range(n_x), self.number_y_gen()): + if (i+1) % self.interval_x == 0: + t_x = off_x + x + (i + 0.5) * self.pitch_x + + t_y = y + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', side='bottom', unit=self.unit) + + t_y = y + h + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', side='bottom', unit=self.unit) + + for i in range(n_x): for j in range(n_y): if hasattr(self.obj, 'inst'): inst = self.obj.inst(i, j, i == n_x-1, j == n_y-1) + if not inst: + continue else: inst = copy(self.obj) @@ -274,6 +350,37 @@ class RFGroundProto(ObjectGroup): return inst +class THTFlowerProto(ObjectGroup): + def __init__(self, pitch=None, drill=None, diameter=None, unit=MM): + super().__init__(0, 0, unit=unit) + self.pitch = pitch = pitch or unit(2.54, MM) + drill = drill or unit(0.9, MM) + diameter = diameter or unit(2.0, MM) + + p = pitch / 2 + self.objects.append(THTPad.circle(-p, 0, drill, diameter, paste=False, unit=unit)) + self.objects.append(THTPad.circle( p, 0, drill, diameter, paste=False, unit=unit)) + self.objects.append(THTPad.circle(0, -p, drill, diameter, paste=False, unit=unit)) + self.objects.append(THTPad.circle(0, p, drill, diameter, paste=False, unit=unit)) + + middle_ap = CircleAperture(diameter, unit=unit) + self.top_copper.append(Flash(0, 0, aperture=middle_ap, unit=unit)) + self.bottom_copper = self.top_mask = self.bottom_mask = self.top_copper + + def inst(self, x, y, border_x, border_y): + if (x % 2 == 0) and (y % 2 == 0): + return copy(self) + + if (x % 2 == 1) and (y % 2 == 1): + return copy(self) + + return None + + def bounding_box(self, unit=MM): + x, y, rotation = self.abs_pos + p = self.pitch/2 + return unit.convert_bounds_from(self.unit, ((x-p, y-p), (x+p, y+p))) + class PoweredProto(ObjectGroup): def __init__(self, pitch=None, drill=None, clearance=None, power_pad_dia=None, via_size=None, trace_width=None, unit=MM): super().__init__(0, 0) @@ -377,7 +484,10 @@ def _demo(): #layout = PropLayout([pattern1, stack], 'h', [0.5, 0.5]) #pattern = PatternProtoArea(2.54, obj=ManhattanPads(2.54)) #pattern = PatternProtoArea(2.54, obj=PoweredProto()) - pattern = PatternProtoArea(2.54, obj=RFGroundProto()) + #pattern = PatternProtoArea(2.54, obj=RFGroundProto()) + #pattern = PatternProtoArea(2.54*1.5, obj=THTFlowerProto()) + #pattern = PatternProtoArea(2.54, obj=THTPad.circle(0, 0, 0.9, 1.8, paste=False)) + pattern = PatternProtoArea(2.54, obj=PoweredProto()) pb = ProtoBoard(100, 80, pattern, mounting_hole_dia=3.2, mounting_hole_offset=5) print(pb.pretty_svg()) pb.layer_stack().save_to_directory('/tmp/testdir') diff --git a/gerbonara/utils.py b/gerbonara/utils.py index 47fb178..ce5425a 100644 --- a/gerbonara/utils.py +++ b/gerbonara/utils.py @@ -527,6 +527,9 @@ def point_in_polygon(point, poly): def bbox_intersect(a, b): + if a is None or b is None: + return False + (xa_min, ya_min), (xa_max, ya_max) = a (xb_min, yb_min), (xb_max, yb_max) = b -- cgit