From d437e06325d929358beb04917bc47d3da8ac5411 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 5 Apr 2023 01:29:33 +0200 Subject: Initial protoboard generation working --- gerbonara/cad/primitives.py | 71 ++++++++++++++++++++++++++++++++++----------- gerbonara/utils.py | 2 +- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index 4eaeb5f..8d1ccdc 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -1,4 +1,5 @@ +import sys import math import warnings from copy import copy @@ -38,11 +39,15 @@ class Board: self.keepouts = [] self.default_via_hole = MM(default_via_hole, unit) self.default_via_diameter = MM(default_via_diameter, unit) + self.unit = unit if w or h: if w and h: self.rounded_rect_outline(w, h, r=corner_radius, center=center) + self.w, self.h = w, h else: raise ValueError('Either both, w and h, or neither of them must be given.') + else: + self.w = self.h = None @property def abs_pos(self): @@ -75,6 +80,9 @@ 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 @@ -136,9 +144,13 @@ class Positioned: y: float _: KW_ONLY rotation: float = 0.0 + side: str = 'top' unit: LengthUnit = MM parent: object = None + def flip(self): + self.side = 'top' if self.side == 'bottom' else 'bottom' + @property def abs_pos(self): if self.parent is None: @@ -153,11 +165,18 @@ class Positioned: self.render(stack) objects = chain(*(l.objects for l in stack.graphic_layers.values()), stack.drill_pth.objects, stack.drill_npth.objects) + objects = list(objects) + #print('foo', type(self).__name__, + # [(type(obj).__name__, [prim.bounding_box() for prim in obj.to_primitives(unit)]) for obj in objects], file=sys.stderr) return sum_bounds(prim.bounding_box() for obj in objects for prim in obj.to_primitives(unit)) def overlaps(self, bbox, unit=MM): return bbox_intersect(self.bounding_box(unit), bbox) + @property + def single_sided(self): + return True + @dataclass class ObjectGroup(Positioned): @@ -171,10 +190,6 @@ class ObjectGroup(Positioned): bottom_paste: list = field(default_factory=list) drill_npth: list = field(default_factory=list) drill_pth: list = field(default_factory=list) - side: str = 'top' - - def flip(self): - self.side = 'top' if self.side == 'bottom' else 'bottom' def render(self, layer_stack): x, y, rotation = self.abs_pos @@ -193,6 +208,13 @@ class ObjectGroup(Positioned): for fe in source: target.objects.append(copy(fe).rotate(rotation).offset(x, y, self.unit)) + @property + def single_sided(self): + any_top = self.top_copper or self.top_mask or self.top_paste or self.top_silk + any_bottom = self.bottom_copper or self.bottom_mask or self.bottom_paste or self.bottom_silk + any_drill = self.drill_npth or self.drill_pth + return not (any_drill or (any_top and any_bottom)) + @dataclass class Text(Positioned): @@ -202,12 +224,8 @@ class Text(Positioned): h_align: str = 'left' v_align: str = 'bottom' layer: str = 'silk' - side: str = 'top' polarity_dark: bool = True - def flip(self): - self.side = 'top' if self.side == 'bottom' else 'bottom' - def render(self, layer_stack): obj_x, obj_y, rotation = self.abs_pos global newstroke_font @@ -259,7 +277,6 @@ class SMDPad(Pad): mask_aperture: Aperture paste_aperture: Aperture silk_features: list = field(default_factory=list) - side: str = 'top' def render(self, layer_stack): x, y, rotation = self.abs_pos @@ -269,9 +286,6 @@ class SMDPad(Pad): layer_stack[self.side, 'silk' ].objects.extend([copy(feature).rotate(rotation).offset(x, y, self.unit) for feature in self.silk_features]) - def flip(self): - self.side = 'top' if self.side == 'bottom' else 'bottom' - @classmethod def rect(kls, x, y, w, h, rotation=0, side='top', mask_expansion=0.0, paste_expansion=0.0, unit=MM): ap_c = RectangleAperture(w, h, unit=unit) @@ -285,8 +299,7 @@ class SMDPad(Pad): ap_c = CircleAperture(dia, unit=unit) ap_m = CircleAperture(dia+2*mask_expansion, unit=unit) ap_p = CircleAperture(dia+2*paste_expansion, unit=unit) - return kls(x, y, side=side, copper_aperture=ap_c, mask_aperture=ap_m, paste_aperture=ap_p, rotation=rotation, - unit=unit) + return kls(x, y, side=side, copper_aperture=ap_c, mask_aperture=ap_m, paste_aperture=ap_p, unit=unit) @dataclass @@ -310,7 +323,9 @@ class THTPad(Pad): def render(self, layer_stack): x, y, rotation = self.abs_pos + self.pad_top.parent = self self.pad_top.render(layer_stack) + self.pad_bottom.parent = self self.pad_bottom.render(layer_stack) if self.aperture_inner is None: @@ -325,12 +340,16 @@ class THTPad(Pad): for (side, use), layer in layer_stack.inner_layers: layer.objects.append(Flash(x, y, self.aperture_inner.rotated(rotation), unit=self.unit)) - hole = Flash(self.x, self.y, ExcellonTool(self.drill_dia, plated=self.plated, unit=self.unit), unit=self.unit) + hole = Flash(x, y, ExcellonTool(self.drill_dia, plated=self.plated, unit=self.unit), unit=self.unit) if self.plated: layer_stack.drill_pth.objects.append(hole) else: layer_stack.drill_npth.objects.append(hole) + @property + def single_sided(self): + return False + @classmethod def rect(kls, x, y, hole_dia, w, h=None, rotation=0, mask_expansion=0.0, paste_expansion=0.0, unit=MM): if h is None: @@ -341,7 +360,7 @@ class THTPad(Pad): @classmethod def circle(kls, x, y, hole_dia, dia, mask_expansion=0.0, paste_expansion=0.0, unit=MM): pad = SMDPad.circle(0, 0, dia, mask_expansion=mask_expansion, paste_expansion=paste_expansion, unit=unit) - return kls(x, y, hole_dia, pad, rotation=rotation, unit=unit) + return kls(x, y, hole_dia, pad, unit=unit) @classmethod def obround(kls, x, y, hole_dia, w, h, rotation=0, mask_expansion=0.0, paste_expanson=0.0, unit=MM): @@ -352,6 +371,21 @@ class THTPad(Pad): return kls(x, y, hole_dia, pad, rotation=rotation, unit=unit) +@dataclass +class Hole(Positioned): + diameter: float + + def render(self, layer_stack): + x, y, rotation = self.abs_pos + + hole = Flash(x, y, ExcellonTool(self.diameter, plated=False, unit=self.unit), unit=self.unit) + layer_stack.drill_npth.objects.append(hole) + + @property + def single_sided(self): + return False + + @dataclass class Via(Positioned): diameter: float @@ -368,13 +402,16 @@ class Via(Positioned): layer_stack.drill_pth.objects.append(Flash(x, y, tool, unit=self.unit)) + @property + def single_sided(self): + return False + @dataclass class Trace: width: float start: object = None end: object = None - side: str = 'top' waypoints: [(float, float)] = field(default_factory=list) style: str = 'oblique' orientation: [str] = tuple() # 'top' or 'bottom' diff --git a/gerbonara/utils.py b/gerbonara/utils.py index 32954bd..47fb178 100644 --- a/gerbonara/utils.py +++ b/gerbonara/utils.py @@ -528,7 +528,7 @@ def point_in_polygon(point, poly): def bbox_intersect(a, b): (xa_min, ya_min), (xa_max, ya_max) = a - (xb_min, yb_min), (xb_mbx, yb_mbx) = b + (xb_min, yb_min), (xb_max, yb_max) = b x_overlap = not (xa_max < xb_min or xb_max < xa_min) y_overlap = not (ya_max < yb_min or yb_max < ya_min) -- cgit