From 04c4b3ff0c81d13b8aba01ddb79ac3f14d59baa6 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 28 May 2024 18:07:09 +0200 Subject: [PATCH 001/103] kicad_sch render: Fix nightly import and wire rendering --- gerbonara/cad/kicad/base_types.py | 2 +- gerbonara/cad/kicad/primitives.py | 2 +- gerbonara/cad/kicad/schematic.py | 3 ++- gerbonara/cad/kicad/symbols.py | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/gerbonara/cad/kicad/base_types.py b/gerbonara/cad/kicad/base_types.py index 1161996..59a727d 100644 --- a/gerbonara/cad/kicad/base_types.py +++ b/gerbonara/cad/kicad/base_types.py @@ -97,7 +97,7 @@ class Stroke: class Dasher: def __init__(self, obj): if obj.stroke: - w = obj.stroke.width if obj.stroke.width is not None else 0.254 + w = obj.stroke.width if obj.stroke.width not in (None, 0, 0.0) else 0.254 t = obj.stroke.type else: w = obj.width or 0 diff --git a/gerbonara/cad/kicad/primitives.py b/gerbonara/cad/kicad/primitives.py index fa55568..65e83ac 100644 --- a/gerbonara/cad/kicad/primitives.py +++ b/gerbonara/cad/kicad/primitives.py @@ -81,7 +81,7 @@ def kicad_mid_to_center_arc(mid, start, end): cy = ((p1[0] - p2[0]) * cd - (p2[0] - p3[0]) * bc) / det radius = math.sqrt((cx - p1[0])**2 + (cy - p1[1])**2) - return ((cx, cy), radius) + return (cx, cy), radius @sexp_type('hatch') diff --git a/gerbonara/cad/kicad/schematic.py b/gerbonara/cad/kicad/schematic.py index 45a022e..0a2f4de 100644 --- a/gerbonara/cad/kicad/schematic.py +++ b/gerbonara/cad/kicad/schematic.py @@ -260,6 +260,7 @@ class HierarchicalLabel(TextMixin): class Pin: name: str = '1' uuid: UUID = field(default_factory=UUID) + alternate: Named(str) = None # Suddenly, we're doing syntax like this is yaml or something. @@ -354,7 +355,7 @@ class SymbolInstance: pins: List(Pin) = field(default_factory=list) # AFAICT this property, too, is completely redundant. It ultimately just lists paths and references of at most # three other uses of the same symbol in this schematic. - instances: Named(List(SymbolCrosslinkProject)) = field(default_factory=list) + instances: Named(Array(SymbolCrosslinkProject)) = field(default_factory=list) _ : SEXP_END = None schematic: object = None diff --git a/gerbonara/cad/kicad/symbols.py b/gerbonara/cad/kicad/symbols.py index ed93f7b..3c6fab2 100644 --- a/gerbonara/cad/kicad/symbols.py +++ b/gerbonara/cad/kicad/symbols.py @@ -20,7 +20,7 @@ from .base_types import * from ...utils import rotate_point, Tag, arc_bounds from ...newstroke import Newstroke from .schematic_colors import * -from .primitives import center_arc_to_kicad_mid +from .primitives import kicad_mid_to_center_arc PIN_ETYPE = AtomChoice(Atom.input, Atom.output, Atom.bidirectional, Atom.tri_state, Atom.passive, Atom.free, @@ -259,7 +259,7 @@ class Arc: fill: Fill = field(default_factory=Fill) def bounding_box(self, default=None): - (cx, cy), r = center_arc_to_kicad_mid(self.mid, self.start, self.end) + (cx, cy), r = kicad_mid_to_center_arc(self.mid, self.start, self.end) x1, y1 = self.start.x, self.start.y x2, y2 = self.mid.x-x1, self.mid.y-x2 x3, y3 = (self.end.x - x1)/2, (self.end.y - y1)/2 @@ -268,7 +268,7 @@ class Arc: def to_svg(self, colorscheme=Colorscheme.KiCad): - (cx, cy), r = center_arc_to_kicad_mid(self.mid, self.start, self.end) + (cx, cy), r = kicad_mid_to_center_arc(self.mid, self.start, self.end) x1r = self.start.x - cx y1r = self.start.y - cy From f721692bf348489ac8444e3ed1560e4f0793b213 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 6 Jul 2024 15:51:08 +0200 Subject: [PATCH 002/103] Protoboard generator WIP --- gerbonara/cad/primitives.py | 54 ++++++++++++++++++++++++++++--------- gerbonara/cad/protoboard.py | 29 ++++++++++++-------- gerbonara/cad/protoserve.py | 13 ++++----- 3 files changed, 66 insertions(+), 30 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index 6ffd4e2..08fb1e5 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -4,7 +4,7 @@ import math import warnings from copy import copy from itertools import zip_longest, chain -from dataclasses import dataclass, field, KW_ONLY +from dataclasses import dataclass, field, replace, KW_ONLY from collections import defaultdict from ..utils import LengthUnit, MM, rotate_point, svg_arc, sum_bounds, bbox_intersect, Tag, offset_bounds @@ -14,6 +14,9 @@ from ..apertures import Aperture, CircleAperture, ObroundAperture, RectangleAper from ..newstroke import Newstroke +class UNDEFINED: + pass + def sgn(x): return -1 if x < 0 else 1 @@ -329,16 +332,16 @@ class Text(Positioned): else: raise ValueError('h_align must be one of "left", "center", or "right".') - if self.v_align == 'top': + if self.v_align == 'bottom': y0 = -(max_y - min_y) elif self.v_align == 'middle': - y0 = -(max_y - min_y)/2 - elif self.v_align == 'bottom': + y0 = (max_y - min_y)/2 + elif self.v_align == 'top': y0 = 0 else: raise ValueError('v_align must be one of "top", "middle", or "bottom".') - if self.side == 'bottom': + if self.flip: x0 += min_x + max_x x_sign = -1 else: @@ -348,7 +351,7 @@ class Text(Positioned): for stroke in strokes: for (x1, y1), (x2, y2) in zip(stroke[:-1], stroke[1:]): - 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 = 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['bottom' if flip else 'top', self.layer].objects.append(obj) @@ -396,20 +399,20 @@ class PadStack: def flashes(self, x, y, rotation: float = 0, flip: bool = False): for ap in self.apertures: aperture = ap.aperture.rotated(ap.rotation + rotation) - fl = Flash(ap.offset_x, ap.offset_y) + fl = Flash(ap.offset_x, ap.offset_y, aperture, unit=self.unit) fl.rotate(rotation) fl.offset(x, y) - side = fl.side + side = ap.side if flip: side = {'top': 'bottom', 'bottom': 'top'}.get(side, side) - yield side, fl.layer, fl + yield side, ap.layer, fl def render(self, layer_stack, x, y, rotation: float = 0, flip: bool = False): for side, layer, flash in self.flashes(x, y, rotation, flip): - if side == 'drill' and use == 'plated': + if side == 'drill' and layer == 'plated': layer_stack.drill_pth.objects.append(flash) - elif side == 'drill' and use == 'nonplated': + elif side == 'drill' and layer == 'nonplated': layer_stack.drill_npth.objects.append(flash) elif (side, layer) in layer_stack: @@ -449,17 +452,37 @@ class SMDStack(PadStack): return kls(CircleAperture(dia, unit=unit), mask_expansion, paste_expansion, paste, flip, unit=unit) +@dataclass(frozen=True, slots=True) +class MechanicalHoleStack(PadStack): + drill_dia: float + mask_expansion: float = 0.0 + mask_aperture = None + + @property + def apertures(self): + mask_aperture = self.mask_aperture or CircleAperture(self.drill_dia + self.mask_expansion, unit=self.unit) + yield PadStackAperture(mask_aperture, 'top', 'mask') + yield PadStackAperture(mask_aperture, 'bottom', 'mask') + + @property + def single_sided(self): + return False + + @dataclass(frozen=True, slots=True) class THTPad(PadStack): drill_dia: float pad_top: SMDStack pad_bottom: SMDStack = None - aperture_inner: Aperture = None + aperture_inner: Aperture = UNDEFINED plated: bool = True def __post_init__(self): if self.pad_bottom is None: object.__setattr__(self, 'pad_bottom', replace(self.pad_top, flip=True)) + + if self.aperture_inner is UNDEFINED: + object.__setattr__(self, 'aperture_inner', self.pad_top.aperture) if self.pad_top.flip: raise ValueError('top pad cannot be flipped') @@ -472,7 +495,8 @@ class THTPad(PadStack): def apertures(self): yield from self.pad_top.apertures yield from self.pad_bottom.apertures - yield PadStackAperture(self.aperture_inner, 'inner', 'copper') + if self.aperture_inner is not None: + yield PadStackAperture(self.aperture_inner, 'inner', 'copper') yield PadStackAperture(ExcellonTool(self.drill_dia, plated=self.plated, unit=self.unit), 'drill', self.plating) @property @@ -538,6 +562,10 @@ class Via(FrozenPositioned): class Pad(Positioned): pad_stack: PadStack + def render(self, layer_stack, cache=None): + x, y, rotation, flip = self.abs_pos + self.pad_stack.render(layer_stack, x, y, rotation, flip) + @property def single_sided(self): return self.pad_stack.single_sided diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 91b07d1..1e88500 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -29,10 +29,11 @@ class ProtoBoard(Board): mounting_hole_offset = mounting_hole_offset or mounting_hole_dia*2 ko = mounting_hole_offset*2 - self.add(Hole(mounting_hole_offset, mounting_hole_offset, mounting_hole_dia, unit=unit)) - self.add(Hole(w-mounting_hole_offset, mounting_hole_offset, mounting_hole_dia, unit=unit)) - self.add(Hole(mounting_hole_offset, h-mounting_hole_offset, mounting_hole_dia, unit=unit)) - self.add(Hole(w-mounting_hole_offset, h-mounting_hole_offset, mounting_hole_dia, unit=unit)) + stack = MechanicalHoleStack(mounting_hole_dia, unit=unit) + self.add(Pad(mounting_hole_offset, mounting_hole_offset, pad_stack=stack, unit=unit)) + self.add(Pad(w-mounting_hole_offset, mounting_hole_offset, pad_stack=stack, unit=unit)) + self.add(Pad(mounting_hole_offset, h-mounting_hole_offset, pad_stack=stack, unit=unit)) + self.add(Pad(w-mounting_hole_offset, h-mounting_hole_offset, pad_stack=stack, unit=unit)) self.keepouts.append(((0, 0), (ko, ko))) self.keepouts.append(((w-ko, 0), (w, ko))) @@ -235,7 +236,7 @@ class PatternProtoArea: off_y = (h % unit(self.pitch_y, self.unit)) / 2 if self.numbers: - for i, lno_i in list(zip(range(n_y), self.number_y_gen())): + for i, lno_i in list(zip(reversed(range(n_y)), self.number_y_gen())): if i == 0 or i == n_y - 1 or (i+1) % self.interval_y == 0: t_y = off_y + y + (n_y - 1 - i + 0.5) * self.pitch_y @@ -243,13 +244,13 @@ class PatternProtoArea: t_x = x + off_x yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'right', 'middle', unit=self.unit) if not self.single_sided: - yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'right', 'middle', side='bottom', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'right', 'middle', flip=True, unit=self.unit) if border_text[1]: t_x = x + w - off_x yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'left', 'middle', unit=self.unit) if not self.single_sided: - yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'left', 'middle', side='bottom', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'left', 'middle', flip=True, unit=self.unit) for i, lno_i in zip(range(n_x), self.number_x_gen()): if i == 0 or i == n_x - 1 or (i+1) % self.interval_x == 0: @@ -259,18 +260,24 @@ class PatternProtoArea: t_y = y + off_y yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', unit=self.unit) if not self.single_sided: - yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', side='bottom', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', flip=True, unit=self.unit) if border_text[0]: - t_y = y + h - off_y + t_y = y + h + off_y yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit) if not self.single_sided: - yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', side='bottom', unit=self.unit) + yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', flip=True, unit=self.unit) for i in range(n_x): for j in range(n_y): - if hasattr(self.obj, 'inst'): + if isinstance(self.obj, PadStack): + px = self.unit(off_x + x, unit) + (i + 0.5) * self.pitch_x + py = self.unit(off_y + y, unit) + (j + 0.5) * self.pitch_y + yield Pad(px, py, pad_stack=self.obj, unit=self.unit) + continue + + elif hasattr(self.obj, 'inst'): inst = self.obj.inst(i, j, i == n_x-1, j == n_y-1) if not inst: continue diff --git a/gerbonara/cad/protoserve.py b/gerbonara/cad/protoserve.py index 25ef8c6..cc5aae2 100644 --- a/gerbonara/cad/protoserve.py +++ b/gerbonara/cad/protoserve.py @@ -8,6 +8,7 @@ from quart import Quart, request, Response, send_file, abort from . import protoboard as pb from . import protoserve_data +from .primitives import SMDStack from ..utils import MM, Inch @@ -62,10 +63,10 @@ def deserialize(obj, unit): case 'smd': match obj['pad_shape']: case 'rect': - pad = pb.SMDPad.rect(0, 0, pitch_x-clearance, pitch_y-clearance, paste=False, unit=unit) + stack = SMDStack.rect(pitch_x-clearance, pitch_y-clearance, paste=False, unit=unit) case 'circle': - pad = pb.SMDPad.circle(0, 0, min(pitch_x, pitch_y)-clearance, paste=False, unit=unit) - return pb.PatternProtoArea(pitch_x, pitch_y, obj=pad, unit=unit) + stack = SMDStack.circle(min(pitch_x, pitch_y)-clearance, paste=False, unit=unit) + return pb.PatternProtoArea(pitch_x, pitch_y, obj=stack, unit=unit) case 'tht': hole_dia = mil(float(obj['hole_dia'])) @@ -79,11 +80,11 @@ def deserialize(obj, unit): match obj['pad_shape']: case 'rect': - pad = pb.THTPad.rect(0, 0, hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) + pad = pb.THTPad.rect(hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) case 'circle': - pad = pb.THTPad.circle(0, 0, hole_dia, min(pitch_x, pitch_y)-clearance, paste=False, plated=plated, unit=unit) + pad = pb.THTPad.circle(hole_dia, min(pitch_x, pitch_y)-clearance, paste=False, plated=plated, unit=unit) case 'obround': - pad = pb.THTPad.obround(0, 0, hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) + pad = pb.THTPad.obround(hole_dia, pitch_x-clearance, pitch_y-clearance, paste=False, plated=plated, unit=unit) if oneside: pad.pad_bottom = None From 552f30c15dc1fc11b418d08e3f598d170bbea5c4 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 00:32:17 +0200 Subject: [PATCH 003/103] Protoboard: All layouts except for spiky proto work --- gerbonara/cad/primitives.py | 7 +- gerbonara/cad/protoboard.py | 249 +++++++++++++++++++----------------- 2 files changed, 135 insertions(+), 121 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index 08fb1e5..82c06e1 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -368,11 +368,11 @@ class Text(Positioned): x0 = -approx_w if self.v_align == 'top': - y0 = -approx_h + y0 = 0 elif self.v_align == 'middle': y0 = -approx_h/2 elif self.v_align == 'bottom': - y0 = 0 + y0 = -approx_h return (self.x+x0, self.y+y0), (self.x+x0+approx_w, self.y+y0+approx_h) @@ -385,6 +385,7 @@ class PadStackAperture: offset_x: float = 0 # in PadStack units offset_y: float = 0 rotation: float = 0 + invert: bool = False @dataclass(frozen=True, slots=True) @@ -399,7 +400,7 @@ class PadStack: def flashes(self, x, y, rotation: float = 0, flip: bool = False): for ap in self.apertures: aperture = ap.aperture.rotated(ap.rotation + rotation) - fl = Flash(ap.offset_x, ap.offset_y, aperture, unit=self.unit) + fl = Flash(ap.offset_x, ap.offset_y, aperture, polarity_dark=not ap.invert, unit=self.unit) fl.rotate(rotation) fl.offset(x, y) side = ap.side diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 1e88500..f009e7a 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -12,7 +12,7 @@ from ..utils import MM, rotate_point from .primitives import * from ..graphic_objects import Region from ..apertures import RectangleAperture, CircleAperture, ApertureMacroInstance -from ..aperture_macros.parse import ApertureMacro, VariableExpression +from ..aperture_macros.parse import ApertureMacro, ParameterExpression, VariableExpression from ..aperture_macros import primitive as amp from .kicad import footprints as kfp from . import data as package_data @@ -272,9 +272,13 @@ class PatternProtoArea: for i in range(n_x): for j in range(n_y): if isinstance(self.obj, PadStack): + obj = self.obj.grid_variant(i, j, i == n_x-1, j == n_y-1) + if obj is None: + continue + px = self.unit(off_x + x, unit) + (i + 0.5) * self.pitch_x py = self.unit(off_y + y, unit) + (j + 0.5) * self.pitch_y - yield Pad(px, py, pad_stack=self.obj, unit=self.unit) + yield Pad(px, py, pad_stack=obj, unit=self.unit) continue elif hasattr(self.obj, 'inst'): @@ -313,110 +317,126 @@ class EmptyProtoArea: return True -class ManhattanPads(ObjectGroup): - def __init__(self, w, h=None, gap=0.2, unit=MM): - super().__init__(0, 0) - h = h or w - self.gap = gap - self.unit = unit +@dataclass(frozen=True, slots=True) +class ManhattanPads(PadStack): + w: float = None + h: float = None + gap: float = 0.2 - p = (w-2*gap)/2 - q = (h-2*gap)/2 - small_ap = RectangleAperture(p, q, unit=unit) + @property + def apertures(self): + w = self.w + h = self.h or w + + p = (w-2*self.gap)/2 + q = (h-2*self.gap)/2 + small_ap = RectangleAperture(p, q, unit=self.unit) s = min(w, h) / 2 / math.sqrt(2) - large_ap = RectangleAperture(s, s, unit=unit).rotated(math.pi/4) - large_ap_neg = RectangleAperture(s+2*gap, s+2*gap, unit=unit).rotated(math.pi/4) + large_ap = RectangleAperture(s, s, unit=self.unit).rotated(math.pi/4) + large_ap_neg = RectangleAperture(s+2*self.gap, s+2*self.gap, unit=self.unit).rotated(math.pi/4) - a = gap/2 + p/2 - b = gap/2 + q/2 + a = self.gap/2 + p/2 + b = self.gap/2 + q/2 - self.top_copper.append(Flash(-a, -b, aperture=small_ap, unit=unit)) - self.top_copper.append(Flash(-a, b, aperture=small_ap, unit=unit)) - self.top_copper.append(Flash( a, -b, aperture=small_ap, unit=unit)) - self.top_copper.append(Flash( a, b, aperture=small_ap, unit=unit)) - self.top_copper.append(Flash(0, 0, aperture=large_ap_neg, polarity_dark=False, unit=unit)) - self.top_copper.append(Flash(0, 0, aperture=large_ap, unit=unit)) - self.top_mask = self.top_copper + for layer in ('copper', 'mask'): + yield PadStackAperture(small_ap, 'top', layer, -a, -b) + yield PadStackAperture(small_ap, 'top', layer, -a, b) + yield PadStackAperture(small_ap, 'top', layer, a, -b) + yield PadStackAperture(small_ap, 'top', layer, a, b) + yield PadStackAperture(large_ap_neg, 'top', layer, 0, 0, invert=True) + yield PadStackAperture(large_ap, 'top', layer, 0, 0) -class RFGroundProto(ObjectGroup): - def __init__(self, pitch=None, drill=None, clearance=None, via_dia=None, via_drill=None, pad_dia=None, trace_width=None, unit=MM): - super().__init__(0, 0) - self.unit = unit - self.pitch = pitch = pitch or unit(2.54, MM) - self.drill = drill = drill or unit(0.9, MM) - self.clearance = clearance = clearance or unit(0.3, MM) - self.via_drill = via_drill = via_drill or unit(0.4, MM) - self.via_dia = via_dia = via_dia or unit(0.8, MM) +@dataclass(frozen=True, slots=True) +class RFGroundProto(PadStack): + pitch: float = 2.54 + drill: float = 0.9 + clearance: float = 0.3 + via_drill: float = 0.4 + via_dia: float = 0.8 + pad_dia: float = None + trace_width: float = None + _: KW_ONLY = None + suppress_via: bool = False + + @property + def apertures(self): + unit = self.unit + pitch = self.pitch + trace_width, pad_dia = self.trace_width, self.pad_dia if pad_dia is None: - self.trace_width = trace_width = trace_width or unit(0.3, MM) - pad_dia = pitch - trace_width - 2*clearance + if trace_width is None: + trace_width = 0.3 + pad_dia = pitch - trace_width - 2*self.clearance elif trace_width is None: - trace_width = pitch - pad_dia - 2*clearance - self.pad_dia = pad_dia + trace_width = pitch - pad_dia - 2*self.clearance - via_ap = RectangleAperture(via_dia, via_dia, unit=unit).rotated(math.pi/4) + via_ap = RectangleAperture(self.via_dia, self.via_dia, unit=unit).rotated(math.pi/4) pad_ap = CircleAperture(pad_dia, unit=unit) - pad_neg_ap = CircleAperture(pad_dia+2*clearance, unit=unit) + pad_neg_ap = CircleAperture(pad_dia+2*self.clearance, unit=unit) ground_ap = RectangleAperture(pitch + unit(0.01, MM), pitch + unit(0.01, MM), unit=unit) - pad_drill = ExcellonTool(drill, plated=True, unit=unit) - via_drill = ExcellonTool(via_drill, plated=True, unit=unit) + pad_drill = ExcellonTool(self.drill, plated=True, unit=unit) + via_drill = ExcellonTool(self.via_drill, plated=True, unit=unit) - self.top_copper.append(Flash(0, 0, aperture=ground_ap, unit=unit)) - self.top_copper.append(Flash(0, 0, aperture=pad_neg_ap, polarity_dark=False, unit=unit)) - self.top_copper.append(Flash(0, 0, aperture=pad_ap, unit=unit)) - self.top_mask.append(Flash(0, 0, aperture=pad_ap, unit=unit)) - self.top_copper.append(Flash(pitch/2, pitch/2, aperture=via_ap, unit=unit)) - self.top_mask.append(Flash(pitch/2, pitch/2, aperture=via_ap, unit=unit)) - self.drill_pth.append(Flash(0, 0, aperture=pad_drill, unit=unit)) - self.drill_pth.append(Flash(pitch/2, pitch/2, aperture=via_drill, unit=unit)) + for side in 'top', 'bottom': + yield PadStackAperture(ground_ap, side, 'copper') + yield PadStackAperture(pad_neg_ap, side, 'copper', invert=True) + yield PadStackAperture(pad_ap, side, 'copper') + yield PadStackAperture(pad_ap, side, 'mask') - self.bottom_copper = self.top_copper - self.bottom_mask = self.top_mask + if not self.suppress_via: + yield PadStackAperture(via_ap, side, 'copper', pitch/2, pitch/2) + yield PadStackAperture(via_ap, side, 'mask', pitch/2, pitch/2) - def inst(self, x, y, border_x, border_y): - inst = copy(self) + yield PadStackAperture(pad_drill, 'drill', 'plated') + if not self.suppress_via: + yield PadStackAperture(via_drill, 'drill', 'plated', pitch/2, pitch/2) + + def grid_variant(self, x, y, border_x, border_y): if border_x or border_y: - inst.drill_pth = inst.drill_pth[:-1] - inst.top_copper = inst.bottom_copper = inst.top_copper[:-1] - inst.top_mask = inst.bottom_mask = inst.top_mask[:-1] - return inst + return replace(self, suppress_via=True) + else: + return self -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) +@dataclass(frozen=True, slots=True) +class THTFlowerProto(PadStack): + pitch: float = 2.54 + drill: float = 0.9 + diameter: float = 2.0 - 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)) + @property + def apertures(self): + p = self.pitch / 2 - 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 + pad = THTPad.circle(self.drill, self.diameter, paste=False, unit=self.unit) + + for ox, oy in ((-p, 0), (p, 0), (0, -p), (0, p)): + for stack_ap in pad.apertures: + yield replace(stack_ap, offset_x=ox, offset_y=oy) + + middle_ap = CircleAperture(self.diameter, unit=self.unit) + for side in ('top', 'bottom'): + for layer in ('copper', 'mask'): + yield PadStackAperture(middle_ap, side, layer) - def inst(self, x, y, border_x, border_y): + def grid_variant(self, x, y, border_x, border_y): if (x % 2 == 0) and (y % 2 == 0): - return copy(self) + return self if (x % 2 == 1) and (y % 2 == 1): - return copy(self) + return 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))) +# 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): +class PoweredProto(Graphics): """ Cell primitive for "powered" THT breadboards. This cell type is based on regular THT pads in a 100 mil grid, but adds small SMD pads diagonally between the THT pads. These SMD pads are interconnected with traces and vias in such a way that every second one is inter-linked, forming two fully connected grids. Next to every THT pad you have one @@ -493,7 +513,7 @@ class PoweredProto(ObjectGroup): return inst def bounding_box(self, unit=MM): - x, y, rotation = self.abs_pos + x, y, rotation, flip = self.abs_pos p = self.pitch/2 return unit.convert_bounds_from(self.unit, ((x-p, y-p), (x+p, y+p))) @@ -540,7 +560,7 @@ class SpikyProto(ObjectGroup): return inst -class AlioCell(ObjectGroup): +class AlioCell(Positioned): """ Cell primitive for the ALio protoboard designed by arief ibrahim adha and published on hackaday.io at the URL below. Similar to electroniceel's spiky protoboard, this layout has small-ish standard THT pads, but in between these pads it puts a grid of SMD pads that are designed for easy solder bridging to allow for the construction of @@ -571,68 +591,61 @@ class AlioCell(ObjectGroup): return inst def bounding_box(self, unit): - x, y, rotation = self.abs_pos + x, y, rotation, flip = self.abs_pos # FIXME hack return self.unit.convert_bounds_to(unit, ((x-self.pitch/2, y-self.pitch/2), (x+self.pitch/2, y+self.pitch/2))) def render(self, layer_stack, cache=None): - x, y, rotation = self.abs_pos + x, y, rotation, flip = self.abs_pos def xf(fe): fe = copy(fe) fe.rotate(rotation) fe.offset(x, y, self.unit) return fe - var = VariableExpression + var = ParameterExpression + foo = VariableExpression(var(2)/2 - var(1)/2 + var(4)) + bar = VariableExpression(var(4)+var(6)) # parameters: [1: total height = pad width, 2: pitch, 3: trace width, 4: corner radius, 5: rotation, 6: clearance] - alio_main_macro = ApertureMacro('ALIOM', ( + alio_main_macro = ApertureMacro('ALIOM', 6, primitives=( amp.CenterLine(MM, 1, var(2)-var(6), var(2)-var(3)-2*var(6), 0, 0, var(5)), amp.Outline(MM, 0, 5, ( -var(2)/2, -var(2)/2, - -var(2)/2, -(var(7)-var(8)), - -var(7), -(var(7)-var(8)), - -(var(7)-var(8)), -var(7), - -(var(7)-var(8)), -var(2)/2, + -var(2)/2, -(foo-bar), + -foo, -(foo-bar), + -(foo-bar), -foo, + -(foo-bar), -var(2)/2, -var(2)/2, -var(2)/2, ), var(5)), amp.Outline(MM, 0, 5, ( - -var(2)/2, var(2)/2, - -var(2)/2, (var(7)-var(8)), - -var(7), (var(7)-var(8)), - -(var(7)-var(8)), var(7), - -(var(7)-var(8)), var(2)/2, - -var(2)/2, var(2)/2, + -var(2)/2, var(2)/2, + -var(2)/2, (foo-bar), + -foo, (foo-bar), + -(foo-bar), foo, + -(foo-bar), var(2)/2, + -var(2)/2, var(2)/2, ), var(5)), amp.Outline(MM, 0, 5, ( var(2)/2, -var(2)/2, - var(2)/2, -(var(7)-var(8)), - var(7), -(var(7)-var(8)), - (var(7)-var(8)), -var(7), - (var(7)-var(8)), -var(2)/2, + var(2)/2, -(foo-bar), + foo, -(foo-bar), + (foo-bar), -foo, + (foo-bar), -var(2)/2, var(2)/2, -var(2)/2, ), var(5)), amp.Outline(MM, 0, 5, ( - var(2)/2, var(2)/2, - var(2)/2, (var(7)-var(8)), - var(7), (var(7)-var(8)), - (var(7)-var(8)), var(7), - (var(7)-var(8)), var(2)/2, - var(2)/2, var(2)/2, + var(2)/2, var(2)/2, + var(2)/2, (foo-bar), + foo, (foo-bar), + (foo-bar), foo, + (foo-bar), var(2)/2, + var(2)/2, var(2)/2, ), var(5)), - amp.Circle(MM, 0, 2*var(8), -var(7), -var(7), var(5)), - amp.Circle(MM, 0, 2*var(8), -var(7), var(7), var(5)), - amp.Circle(MM, 0, 2*var(8), var(7), -var(7), var(5)), - amp.Circle(MM, 0, 2*var(8), var(7), var(7), var(5)), - ), ( - None, # 1 - None, # 2 - None, # 3 - None, # 4 - None, # 5 - None, # 6 - var(2)/2 - var(1)/2 + var(4), # 7 - var(4)+var(6), # 8 - )) + amp.Circle(MM, 0, 2*bar, -foo, -foo, var(5)), + amp.Circle(MM, 0, 2*bar, -foo, foo, var(5)), + amp.Circle(MM, 0, 2*bar, foo, -foo, var(5)), + amp.Circle(MM, 0, 2*bar, foo, foo, var(5)), + )) corner_radius = (self.link_pad_width - self.link_trace_width)/3 main_ap = ApertureMacroInstance(alio_main_macro, (self.link_pad_width, # 1 self.pitch, # 2 @@ -650,7 +663,7 @@ class AlioCell(ObjectGroup): via_drill = ExcellonTool(self.via_size, plated=True, unit=self.unit) # parameters: [1: total height = pad width, 2: total width, 3: trace width, 4: corner radius, 5: rotation] - alio_macro = ApertureMacro('ALIOP', ( + alio_macro = ApertureMacro('ALIOP', primitives=( amp.CenterLine(MM, 1, var(1)-2*var(4), var(1), 0, 0, var(5)), amp.CenterLine(MM, 1, var(1), var(1)-2*var(4), 0, 0, var(5)), amp.Circle(MM, 1, 2*var(4), -var(1)/2+var(4), -var(1)/2+var(4), var(5)), From ef3b5d5e1c242d98a03b9527862d0216a5188796 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 00:52:38 +0200 Subject: [PATCH 004/103] Spiky proto also works now --- gerbonara/cad/kicad/footprints.py | 6 +++--- gerbonara/cad/primitives.py | 3 ++- gerbonara/cad/protoboard.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py index 9debaa9..31a5f33 100644 --- a/gerbonara/cad/kicad/footprints.py +++ b/gerbonara/cad/kicad/footprints.py @@ -908,7 +908,7 @@ class Footprint: for fe in obj.render(variables=variables): fe.rotate(rotation) - fe.offset(x, -y, MM) + fe.offset(x, y, MM) layer_stack[layer].objects.append(fe) for obj in self.pads: @@ -940,7 +940,7 @@ class Footprint: for fe in obj.render(margin=margin, cache=cache): fe.rotate(rotation) - fe.offset(x, -y, MM) + fe.offset(x, y, MM) if isinstance(fe, go.Flash) and fe.aperture: fe.aperture = fe.aperture.rotated(rotation) layer_stack[layer_map[layer]].objects.append(fe) @@ -948,7 +948,7 @@ class Footprint: for obj in self.pads: for fe in obj.render_drill(): fe.rotate(rotation) - fe.offset(x, -y, MM) + fe.offset(x, y, MM) if obj.type == Atom.np_thru_hole: layer_stack.drill_npth.append(fe) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index 82c06e1..591999c 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -118,7 +118,7 @@ class Board: def layer_stack(self, layer_stack=None): if layer_stack is None: - layer_stack = LayerStack() + layer_stack = LayerStack(board_name='proto') cache = {} for obj in chain(self.objects): @@ -127,6 +127,7 @@ class Board: layer_stack['mechanical', 'outline'].objects.extend(self.outline) layer_stack['top', 'silk'].objects.extend(self.extra_silk_top) layer_stack['bottom', 'silk'].objects.extend(self.extra_silk_bottom) + print('layer stack is', repr(layer_stack['top', 'copper'].objects)[:1000]) return layer_stack diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index f009e7a..0a5bbbe 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -538,7 +538,7 @@ class SpikyProto(ObjectGroup): self.fp_between = kfp.Footprint.load(res.joinpath('pad-between-spiked.kicad_mod').read_text(encoding='utf-8')) self.right_pad = kfp.FootprintInstance(1.27, 0, self.fp_between, unit=MM) - self.top_pad = kfp.FootprintInstance(0, 1.27, self.fp_between, rotation=math.pi/2, unit=MM) + self.top_pad = kfp.FootprintInstance(0, 1.27, self.fp_between, rotation=-math.pi/2, unit=MM) @property def objects(self): From 4c3815b25a23f8df2d6bfdab83962367150bd285 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 14:36:41 +0200 Subject: [PATCH 005/103] Fix THT flower proto area --- gerbonara/cad/primitives.py | 1 - gerbonara/cad/protoboard.py | 34 ++++++++++++++++++++++++++++------ gerbonara/cad/protoserve.py | 3 ++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index 591999c..a5c5209 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -127,7 +127,6 @@ class Board: layer_stack['mechanical', 'outline'].objects.extend(self.outline) layer_stack['top', 'silk'].objects.extend(self.extra_silk_top) layer_stack['bottom', 'silk'].objects.extend(self.extra_silk_bottom) - print('layer stack is', repr(layer_stack['top', 'copper'].objects)[:1000]) return layer_stack diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 0a5bbbe..cc1ab9e 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -271,10 +271,12 @@ class PatternProtoArea: for i in range(n_x): for j in range(n_y): - if isinstance(self.obj, PadStack): - obj = self.obj.grid_variant(i, j, i == n_x-1, j == n_y-1) - if obj is None: - continue + obj = self.obj + if isinstance(obj, PadStack): + if hasattr(obj, 'grid_variant'): + obj = obj.grid_variant(i, j, i == n_x-1, j == n_y-1) + if obj is None: + continue px = self.unit(off_x + x, unit) + (i + 0.5) * self.pitch_x py = self.unit(off_y + y, unit) + (j + 0.5) * self.pitch_y @@ -323,6 +325,10 @@ class ManhattanPads(PadStack): h: float = None gap: float = 0.2 + @property + def single_sided(self): + return True + @property def apertures(self): w = self.w @@ -360,6 +366,10 @@ class RFGroundProto(PadStack): _: KW_ONLY = None suppress_via: bool = False + @property + def single_sided(self): + return False + @property def apertures(self): unit = self.unit @@ -406,12 +416,20 @@ class THTFlowerProto(PadStack): pitch: float = 2.54 drill: float = 0.9 diameter: float = 2.0 + clearance: float = 0.5 + + @property + def single_sided(self): + return False @property def apertures(self): - p = self.pitch / 2 + p = self.diameter / 2 + pad_dist_diag = math.sqrt(2) * (self.pitch - p) - self.drill + pad_dist_ortho = 2*self.pitch - self.diameter - self.drill + pad_dia = self.drill + max(0, min(pad_dist_diag, pad_dist_ortho) - self.clearance) - pad = THTPad.circle(self.drill, self.diameter, paste=False, unit=self.unit) + pad = THTPad.circle(self.drill, pad_dia, paste=False, unit=self.unit) for ox, oy in ((-p, 0), (p, 0), (0, -p), (0, p)): for stack_ap in pad.apertures: @@ -452,6 +470,10 @@ class PoweredProto(Graphics): Yajima Manufacturing Corporation website: http://www.yajima-works.co.jp/index.html """ + @property + def single_sided(self): + return False + 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) self.unit = unit diff --git a/gerbonara/cad/protoserve.py b/gerbonara/cad/protoserve.py index cc5aae2..3271af1 100644 --- a/gerbonara/cad/protoserve.py +++ b/gerbonara/cad/protoserve.py @@ -107,7 +107,8 @@ def deserialize(obj, unit): pitch = mil(float(obj.get('pitch', 2.54))) hole_dia = mil(float(obj['hole_dia'])) pattern_dia = mil(float(obj['pattern_dia'])) - return pb.PatternProtoArea(2*pitch, 2*pitch, pb.THTFlowerProto(pitch, hole_dia, pattern_dia, unit=unit), unit=unit) + clearance = mil(float(obj['clearance'])) + return pb.PatternProtoArea(pitch, pitch, pb.THTFlowerProto(pitch, hole_dia, pattern_dia, clearance, unit=unit), unit=unit) case 'spiky': return pb.PatternProtoArea(2.54, 2.54, pb.SpikyProto(), unit=unit) From cee355ff57e859466e4f74d1f57d68eeedb54ce1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 14:40:27 +0200 Subject: [PATCH 006/103] protoboard: fix column label alignment --- gerbonara/cad/primitives.py | 5 +++-- gerbonara/cad/protoboard.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index a5c5209..c659274 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -322,6 +322,7 @@ class Text(Positioned): 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) + h = (max_y - min_y) if self.h_align == 'left': x0 = 0 @@ -333,9 +334,9 @@ class Text(Positioned): raise ValueError('h_align must be one of "left", "center", or "right".') if self.v_align == 'bottom': - y0 = -(max_y - min_y) + y0 = h elif self.v_align == 'middle': - y0 = (max_y - min_y)/2 + y0 = h/2 elif self.v_align == 'top': y0 = 0 else: diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index cc1ab9e..ebc8151 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -263,7 +263,7 @@ class PatternProtoArea: yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', flip=True, unit=self.unit) if border_text[0]: - t_y = y + h + off_y + t_y = y + h - off_y yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit) if not self.single_sided: yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', flip=True, unit=self.unit) From 224a666219b5d71fb6517459409565c18870fc0d Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 15:14:29 +0200 Subject: [PATCH 007/103] protoboard: improve border handling --- gerbonara/cad/protoboard.py | 92 +++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index ebc8151..fa81952 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -8,7 +8,7 @@ from copy import copy, deepcopy import warnings import importlib.resources -from ..utils import MM, rotate_point +from ..utils import MM, rotate_point, bbox_intersect from .primitives import * from ..graphic_objects import Region from ..apertures import RectangleAperture, CircleAperture, ApertureMacroInstance @@ -45,8 +45,11 @@ class ProtoBoard(Board): def generate(self, unit=MM): bbox = ((self.margin, self.margin), (self.w-self.margin, self.h-self.margin)) bbox = unit.convert_bounds_from(self.unit, bbox) - for obj in self.content.generate(bbox, (True, True, True, True), unit): - self.add(obj, keepout_errors='skip') + for obj in self.content.generate(bbox, (True, True, True, True), self.keepouts, unit): + if isinstance(obj, Text): + self.add(obj, keepout_errors='ignore') + else: + self.add(obj, keepout_errors='skip') class PropLayout: @@ -59,7 +62,7 @@ class PropLayout: if len(content) != len(proportions): raise ValueError('proportions and content must have same length') - def generate(self, bbox, border_text, unit=MM): + def generate(self, bbox, border_text, keepouts, unit=MM): for i, (bbox, child) in enumerate(self.layout_2d(bbox, unit)): first = bool(i == 0) last = bool(i == len(self.content)-1) @@ -68,7 +71,7 @@ class PropLayout: border_text[1] and (last or self.direction == 'v'), border_text[2] and (first or self.direction == 'h'), border_text[3] and (first or self.direction == 'v'), - ), unit) + ), keepouts, unit) def fit_size(self, w, h, unit=MM): widths = [] @@ -150,9 +153,9 @@ class TwoSideLayout: return w1, h1 return max(w1, w2), max(h1, h2) - def generate(self, bbox, border_text, unit=MM): - yield from self.top.generate(bbox, border_text, unit) - for obj in self.bottom.generate(bbox, border_text, unit): + def generate(self, bbox, border_text, keepouts, unit=MM): + yield from self.top.generate(bbox, border_text, keepouts, unit) + for obj in self.bottom.generate(bbox, border_text, keepouts, unit): obj.side = 'bottom' yield obj @@ -226,7 +229,7 @@ class PatternProtoArea: y = y + (h-h_fit)/2 return (x, y), (x+w_fit, y+h_fit) - def generate(self, bbox, border_text, unit=MM): + def generate(self, bbox, border_text, keepouts, unit=MM): (x, y), (w, h) = bbox w, h = w-x, h-y @@ -269,12 +272,38 @@ class PatternProtoArea: yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', flip=True, unit=self.unit) - for i in range(n_x): - for j in range(n_y): + for j in range(n_y): + for i in range(n_x): + x0 = off_x + x + i*self.pitch_x + y0 = off_y + y + j*self.pitch_y + x1 = x0 + self.pitch_x + y1 = y0 + self.pitch_y + + border_n = (j == 0) or any(bbox_intersect(ko, ((x0, y0-self.pitch_y), (x1, y0))) for ko in keepouts) + border_s = (j == n_y-1) or any(bbox_intersect(ko, ((x0, y1), (x1, y1+self.pitch_y))) for ko in keepouts) + border_w = (i == 0) or any(bbox_intersect(ko, ((x0-self.pitch_x, y0), (x0, y1))) for ko in keepouts) + border_e = (i == n_x-1) or any(bbox_intersect(ko, ((x1, y0), (x1+self.pitch_x, y1))) for ko in keepouts) + border = (border_s, border_w, border_n, border_e) + + print({ + (0, 0, 0, 0): '┼', + (1, 0, 0, 0): '┴', + (0, 1, 0, 0): '├', + (0, 0, 1, 0): '┬', + (0, 0, 0, 1): '┤', + (1, 1, 0, 0): '└', + (0, 1, 1, 0): '┌', + (0, 0, 1, 1): '┐', + (1, 0, 0, 1): '┘', + }.get(tuple(map(int, border)), '.'), end=('' if i < n_x-1 else '\n')) + + if any(bbox_intersect(ko, ((x0, y0), (x1, y1))) for ko in keepouts): + continue + obj = self.obj if isinstance(obj, PadStack): if hasattr(obj, 'grid_variant'): - obj = obj.grid_variant(i, j, i == n_x-1, j == n_y-1) + obj = obj.grid_variant(i, j, border) if obj is None: continue @@ -284,7 +313,7 @@ class PatternProtoArea: continue elif hasattr(self.obj, 'inst'): - inst = self.obj.inst(i, j, i == n_x-1, j == n_y-1) + inst = self.obj.inst(i, j, border) if not inst: continue else: @@ -306,7 +335,7 @@ class EmptyProtoArea: def fit_size(self, w, h, unit=MM): return w, h - def generate(self, bbox, border_text, unit=MM): + def generate(self, bbox, border_text, keepouts, unit=MM): if self.copper_fill: (min_x, min_y), (max_x, max_y) = bbox group = ObjectGroup(0, 0, top_copper=[Region([(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)], @@ -404,8 +433,9 @@ class RFGroundProto(PadStack): if not self.suppress_via: yield PadStackAperture(via_drill, 'drill', 'plated', pitch/2, pitch/2) - def grid_variant(self, x, y, border_x, border_y): - if border_x or border_y: + def grid_variant(self, x, y, border): + border_s, border_w, border_n, border_e = border + if border_e or border_s: return replace(self, suppress_via=True) else: return self @@ -417,6 +447,10 @@ class THTFlowerProto(PadStack): drill: float = 0.9 diameter: float = 2.0 clearance: float = 0.5 + border_s: bool = False + border_w: bool = False + border_n: bool = False + border_e: bool = False @property def single_sided(self): @@ -431,21 +465,20 @@ class THTFlowerProto(PadStack): pad = THTPad.circle(self.drill, pad_dia, paste=False, unit=self.unit) - for ox, oy in ((-p, 0), (p, 0), (0, -p), (0, p)): - for stack_ap in pad.apertures: - yield replace(stack_ap, offset_x=ox, offset_y=oy) + for ox, oy, brd in ((-p, 0, self.border_w), (p, 0, self.border_e), (0, -p, self.border_n), (0, p, self.border_s)): + if not brd: + for stack_ap in pad.apertures: + yield replace(stack_ap, offset_x=ox, offset_y=oy) middle_ap = CircleAperture(self.diameter, unit=self.unit) for side in ('top', 'bottom'): for layer in ('copper', 'mask'): yield PadStackAperture(middle_ap, side, layer) - def grid_variant(self, x, y, border_x, border_y): - if (x % 2 == 0) and (y % 2 == 0): - return self - - if (x % 2 == 1) and (y % 2 == 1): - return self + def grid_variant(self, x, y, border): + border_s, border_w, border_n, border_e = border + if ((x % 2 == 0) and (y % 2 == 0)) or ((x % 2 == 1) and (y % 2 == 1)): + return replace(self, border_s=border_s, border_w=border_w, border_n=border_n, border_e=border_e) return None @@ -514,7 +547,7 @@ class PoweredProto(Graphics): self.bottom_copper.append(Line(-pitch/2, -pitch/2, pitch/2, -pitch/2, aperture=self.line_ap, unit=unit)) self.bottom_copper.append(Line(-pitch/2, pitch/2, pitch/2, pitch/2, aperture=self.line_ap, unit=unit)) - def inst(self, x, y, border_x, border_y): + def inst(self, x, y, border): inst = copy(self) if (x + y) % 2 == 0: inst.drill_pth = inst.drill_pth[:-1] @@ -570,13 +603,14 @@ class SpikyProto(ObjectGroup): def objects(self, value): pass - def inst(self, x, y, border_x, border_y): + def inst(self, x, y, border): + border_s, border_w, border_n, border_e = border inst = copy(self) - if border_x: + if border_e: inst.corner_pad = inst.right_pad = None - if border_y: + if border_s: inst.corner_pad = inst.top_pad = None return inst From 6de138bf7c0c261e123997dcd014350bc5e2f9a3 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 15:16:01 +0200 Subject: [PATCH 008/103] protoboard: reduce hole keepout margins --- gerbonara/cad/protoboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index fa81952..004694f 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -27,7 +27,7 @@ class ProtoBoard(Board): if mounting_hole_dia: mounting_hole_offset = mounting_hole_offset or mounting_hole_dia*2 - ko = mounting_hole_offset*2 + ko = mounting_hole_offset + mounting_hole_dia*(0.5 + 0.25) stack = MechanicalHoleStack(mounting_hole_dia, unit=unit) self.add(Pad(mounting_hole_offset, mounting_hole_offset, pad_stack=stack, unit=unit)) From 0150c318bb36083e2c3bef31db0a055097fc8005 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 15:48:36 +0200 Subject: [PATCH 009/103] protoboard: Improve row/column numbering --- gerbonara/cad/protoboard.py | 58 +++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 004694f..4264547 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -45,7 +45,7 @@ class ProtoBoard(Board): def generate(self, unit=MM): bbox = ((self.margin, self.margin), (self.w-self.margin, self.h-self.margin)) bbox = unit.convert_bounds_from(self.unit, bbox) - for obj in self.content.generate(bbox, (True, True, True, True), self.keepouts, unit): + for obj in self.content.generate(bbox, (True, True, True, True), self.keepouts, self.margin, unit): if isinstance(obj, Text): self.add(obj, keepout_errors='ignore') else: @@ -62,7 +62,7 @@ class PropLayout: if len(content) != len(proportions): raise ValueError('proportions and content must have same length') - def generate(self, bbox, border_text, keepouts, unit=MM): + def generate(self, bbox, border_text, keepouts, text_margin, unit=MM): for i, (bbox, child) in enumerate(self.layout_2d(bbox, unit)): first = bool(i == 0) last = bool(i == len(self.content)-1) @@ -71,7 +71,7 @@ class PropLayout: border_text[1] and (last or self.direction == 'v'), border_text[2] and (first or self.direction == 'h'), border_text[3] and (first or self.direction == 'v'), - ), keepouts, unit) + ), keepouts, text_margin, unit) def fit_size(self, w, h, unit=MM): widths = [] @@ -153,9 +153,9 @@ class TwoSideLayout: return w1, h1 return max(w1, w2), max(h1, h2) - def generate(self, bbox, border_text, keepouts, unit=MM): - yield from self.top.generate(bbox, border_text, keepouts, unit) - for obj in self.bottom.generate(bbox, border_text, keepouts, unit): + def generate(self, bbox, border_text, keepouts, text_margin, unit=MM): + yield from self.top.generate(bbox, border_text, keepouts, text_margin, unit) + for obj in self.bottom.generate(bbox, border_text, keepouts, text_margin, unit): obj.side = 'bottom' yield obj @@ -199,7 +199,7 @@ def alphabetic(case='upper'): class PatternProtoArea: - 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, margin=0, 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=None, interval_y=None, margin=0, unit=MM): self.pitch_x = pitch_x self.pitch_y = pitch_y or pitch_x self.margin = margin @@ -209,7 +209,7 @@ class PatternProtoArea: self.font_size = font_size or unit(1.0, MM) self.font_stroke = font_stroke or unit(0.2, MM) self.interval_x = interval_x - self.interval_y = interval_y or (1 if MM(self.pitch_y, unit) >= 2.0 else 5) + self.interval_y = interval_y self.number_x_gen, self.number_y_gen = number_x_gen, number_y_gen def fit_size(self, w, h, unit=MM): @@ -229,7 +229,7 @@ class PatternProtoArea: y = y + (h-h_fit)/2 return (x, y), (x+w_fit, y+h_fit) - def generate(self, bbox, border_text, keepouts, unit=MM): + def generate(self, bbox, border_text, keepouts, text_margin, unit=MM): (x, y), (w, h) = bbox w, h = w-x, h-y @@ -239,34 +239,60 @@ class PatternProtoArea: off_y = (h % unit(self.pitch_y, self.unit)) / 2 if self.numbers: + # Center row/column numbers in available margin. Note the swapped axes below - the Y (row) numbers are + # centered in X direction, and vice versa. + _idx, max_x_num = list(zip(range(n_x), self.number_x_gen()))[-1] + _idx, max_y_num = list(zip(range(n_y), self.number_y_gen()))[-1] + bbox_test_x = Text(0, 0, max_y_num, self.font_size, self.font_stroke, 'left', 'top', unit=self.unit) + bbox_test_y = Text(0, 0, max_x_num, self.font_size, self.font_stroke, 'left', 'top', unit=self.unit) + test_w = abs(bbox_test_x.bounding_box()[1][0] - bbox_test_x.bounding_box()[0][0]) + test_h = abs(bbox_test_y.bounding_box()[1][1] - bbox_test_y.bounding_box()[0][1]) + text_off_x = max(0, (off_x + text_margin - test_w)) / 2 + text_off_y = max(0, (off_y + text_margin - test_h)) / 2 + print(f'{test_w=} {off_x=} {text_margin=} {text_off_x=} {max_y_num=}') + print(f'{test_h=} {off_y=} {text_margin=} {text_off_y=} {max_x_num=}') + + test_w = abs(bbox_test_y.bounding_box()[1][0] - bbox_test_y.bounding_box()[0][0]) + test_h = abs(bbox_test_x.bounding_box()[1][1] - bbox_test_x.bounding_box()[0][1]) + + interval_x, interval_y = self.interval_x, self.interval_y + if interval_x is None: + interval_x = 1 if test_w < 0.8*self.pitch_x else 5 + if interval_y is None: + interval_y = 1 if test_h < 0.8*self.pitch_y else 2 + for i, lno_i in list(zip(reversed(range(n_y)), self.number_y_gen())): - if i == 0 or i == n_y - 1 or (i+1) % self.interval_y == 0: + if i == 0 or i == n_y - 1 or (i+1) % interval_y == 0: t_y = off_y + y + (n_y - 1 - i + 0.5) * self.pitch_y if border_text[3]: - t_x = x + off_x + t_x = x + off_x - text_off_x yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'right', 'middle', unit=self.unit) if not self.single_sided: yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'right', 'middle', flip=True, unit=self.unit) if border_text[1]: - t_x = x + w - off_x + t_x = x + w - off_x + text_off_x yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'left', 'middle', unit=self.unit) if not self.single_sided: yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'left', 'middle', flip=True, unit=self.unit) for i, lno_i in zip(range(n_x), self.number_x_gen()): - if i == 0 or i == n_x - 1 or (i+1) % self.interval_x == 0: + # We print every interval'th number, as well as the first and the last numbers. + # The complex condition below is to avoid the corner case where interval is larger than 1, and the last + # interval'th number is right next to the last number, and the two could overlap. In this case, we + # suppress the last interval'th number. + if i == 0 or i == n_x - 1 or ((i+1) % interval_x == 0 and (interval_x == 1 or i != n_x-2)): t_x = off_x + x + (i + 0.5) * self.pitch_x if border_text[2]: - t_y = y + off_y + t_y = y + off_y - text_off_y yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', unit=self.unit) if not self.single_sided: yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'top', flip=True, unit=self.unit) if border_text[0]: - t_y = y + h - off_y + t_y = y + h - off_y + text_off_y yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit) if not self.single_sided: yield Text(t_x, t_y, lno_i, self.font_size, self.font_stroke, 'center', 'bottom', flip=True, unit=self.unit) @@ -335,7 +361,7 @@ class EmptyProtoArea: def fit_size(self, w, h, unit=MM): return w, h - def generate(self, bbox, border_text, keepouts, unit=MM): + def generate(self, bbox, border_text, keepouts, text_margin, unit=MM): if self.copper_fill: (min_x, min_y), (max_x, max_y) = bbox group = ObjectGroup(0, 0, top_copper=[Region([(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)], From b0274a93c05439f5af8278f3fef58a9a0b5227b5 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 16:08:32 +0200 Subject: [PATCH 010/103] protoboard: Improve layout packing --- gerbonara/cad/protoboard.py | 47 +++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 4264547..79cf7dd 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -88,10 +88,13 @@ class PropLayout: def layout_2d(self, bbox, unit=MM): (x, y), (w, h) = bbox w, h = w-x, h-y + total_w, total_h = w, h actual_l = 0 target_l = 0 + total_l = total_w if self.direction == 'h' else total_h + sizes = [] for l, child in zip(self.layout(w if self.direction == 'h' else h, unit), self.content): this_x, this_y = x, y this_w, this_h = w, h @@ -113,8 +116,32 @@ class PropLayout: actual_l += this_h this_w = w + sizes.append(((this_x, this_y), (this_w, this_h))) + + children_sorted = reversed(sorted(enumerate(self.content), + key=lambda e: e[1].increment_x() if self.direction == 'h' else e[1].increment_y())) + + excess_l = total_l - actual_l + children_extra = [0] * len(self.content) + for child_i, child in children_sorted: + increment = child.increment_x() if self.direction=='h' else child.increment_y() + adjustment = increment * (excess_l//increment) if increment > 0 else excess_l + children_extra[child_i] += adjustment + excess_l -= adjustment + + adjust_l = 0 + for extra, ((this_x, this_y), (this_w, this_h)), child in zip(children_extra, sizes, self.content): + if self.direction == 'h': + this_x += adjust_l + this_w += extra + else: + this_y += adjust_l + this_h += extra + adjust_l += extra + yield ((this_x, this_y), (this_x+this_w, this_y+this_h)), child + def layout(self, length, unit=MM): out = [ eval_value(value, MM(length, unit)) for value in self.proportions ] total_length = sum(value for value in out if value is not None) @@ -142,6 +169,12 @@ class TwoSideLayout: if not top.single_sided or not bottom.single_sided: warnings.warn('Two-sided pattern used on one side of a TwoSideLayout') + def increment_x(self): + return max(self.top.increment_x, self.bottom.increment_x) + + def increment_y(self): + return max(self.top.increment_y, self.bottom.increment_y) + def fit_size(self, w, h, unit=MM): w1, h1 = self.top.fit_size(w, h, unit) w2, h2 = self.bottom.fit_size(w, h, unit) @@ -212,6 +245,12 @@ class PatternProtoArea: self.interval_y = interval_y self.number_x_gen, self.number_y_gen = number_x_gen, number_y_gen + def increment_x(self): + return self.pitch_x + + def increment_y(self): + return self.pitch_y + def fit_size(self, w, h, unit=MM): (min_x, min_y), (max_x, max_y) = self.fit_rect(((0, 0), (max(0, w-2*self.margin), max(0, h-2*self.margin)))) return max_x-min_x + 2*self.margin, max_y-min_y + 2*self.margin @@ -249,8 +288,6 @@ class PatternProtoArea: test_h = abs(bbox_test_y.bounding_box()[1][1] - bbox_test_y.bounding_box()[0][1]) text_off_x = max(0, (off_x + text_margin - test_w)) / 2 text_off_y = max(0, (off_y + text_margin - test_h)) / 2 - print(f'{test_w=} {off_x=} {text_margin=} {text_off_x=} {max_y_num=}') - print(f'{test_h=} {off_y=} {text_margin=} {text_off_y=} {max_x_num=}') test_w = abs(bbox_test_y.bounding_box()[1][0] - bbox_test_y.bounding_box()[0][0]) test_h = abs(bbox_test_x.bounding_box()[1][1] - bbox_test_x.bounding_box()[0][1]) @@ -358,6 +395,12 @@ class EmptyProtoArea: def __init__(self, copper_fill=False): self.copper_fill = copper_fill + def increment_x(self): + return 0 + + def increment_y(self): + return 0 + def fit_size(self, w, h, unit=MM): return w, h From a1d6ebf79ff70b1991cc643fff41751b8718c7fe Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 16:25:01 +0200 Subject: [PATCH 011/103] protoboard: Improve layout distribution and index rendering --- gerbonara/cad/protoboard.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index 79cf7dd..fed3bff 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -30,10 +30,13 @@ class ProtoBoard(Board): ko = mounting_hole_offset + mounting_hole_dia*(0.5 + 0.25) stack = MechanicalHoleStack(mounting_hole_dia, unit=unit) - self.add(Pad(mounting_hole_offset, mounting_hole_offset, pad_stack=stack, unit=unit)) - self.add(Pad(w-mounting_hole_offset, mounting_hole_offset, pad_stack=stack, unit=unit)) - self.add(Pad(mounting_hole_offset, h-mounting_hole_offset, pad_stack=stack, unit=unit)) - self.add(Pad(w-mounting_hole_offset, h-mounting_hole_offset, pad_stack=stack, unit=unit)) + self.mounting_holes = [ + Pad(mounting_hole_offset, mounting_hole_offset, pad_stack=stack, unit=unit), + Pad(w-mounting_hole_offset, mounting_hole_offset, pad_stack=stack, unit=unit), + Pad(mounting_hole_offset, h-mounting_hole_offset, pad_stack=stack, unit=unit), + Pad(w-mounting_hole_offset, h-mounting_hole_offset, pad_stack=stack, unit=unit)] + for hole in self.mounting_holes: + self.add(hole) self.keepouts.append(((0, 0), (ko, ko))) self.keepouts.append(((w-ko, 0), (w, ko))) @@ -45,9 +48,13 @@ class ProtoBoard(Board): def generate(self, unit=MM): bbox = ((self.margin, self.margin), (self.w-self.margin, self.h-self.margin)) bbox = unit.convert_bounds_from(self.unit, bbox) + hole_bboxes = [hole.bounding_box(unit) for hole in self.mounting_holes] for obj in self.content.generate(bbox, (True, True, True, True), self.keepouts, self.margin, unit): if isinstance(obj, Text): - self.add(obj, keepout_errors='ignore') + # It's okay for the text to go into the mounting hole keepouts, we just don't want it to overlap with + # the actual mounting holes. + if not any(bbox_intersect(obj.bounding_box(unit), hole_bbox) for hole_bbox in hole_bboxes): + self.add(obj, keepout_errors='ignore') else: self.add(obj, keepout_errors='skip') @@ -118,6 +125,8 @@ class PropLayout: sizes.append(((this_x, this_y), (this_w, this_h))) + # We don't want to pull in a whole bin packing implementation here, but we also don't want to be too dumb. Thus, + # we just take the leftover space and distribute it to the children in descending increment (grid / pitch size). children_sorted = reversed(sorted(enumerate(self.content), key=lambda e: e[1].increment_x() if self.direction == 'h' else e[1].increment_y())) @@ -272,10 +281,10 @@ class PatternProtoArea: (x, y), (w, h) = bbox w, h = w-x, h-y - 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 + n_x = int((w + 0.001)//unit(self.pitch_x, self.unit)) + n_y = int((h + 0.001)//unit(self.pitch_y, self.unit)) + off_x = (w - n_x*unit(self.pitch_x, self.unit)) / 2 + off_y = (h - n_y*unit(self.pitch_y, self.unit)) / 2 if self.numbers: # Center row/column numbers in available margin. Note the swapped axes below - the Y (row) numbers are @@ -709,9 +718,10 @@ class AlioCell(Positioned): def single_sided(self): return False - def inst(self, x, y, border_x, border_y): + def inst(self, x, y, border): + border_s, border_w, border_n, border_e = border inst = copy(self) - inst.border_x, inst.border_y = border_x, border_y + inst.border_x, inst.border_y = border_e, border_s inst.inst_x, inst.inst_y = x, y return inst From 21218239e49dfadc397ecb6f1d4542bc95b4d340 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 16:37:34 +0200 Subject: [PATCH 012/103] protoboard: Fix alio and two-sided SMD rendering --- gerbonara/cad/protoboard.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py index fed3bff..5c424ef 100644 --- a/gerbonara/cad/protoboard.py +++ b/gerbonara/cad/protoboard.py @@ -69,6 +69,18 @@ class PropLayout: if len(content) != len(proportions): raise ValueError('proportions and content must have same length') + def increment_x(self): + if self.direction == 'h': + return 0 + else: + return max(obj.increment_x() for obj in self.content) + + def increment_y(self): + if self.direction == 'v': + return 0 + else: + return max(obj.increment_y() for obj in self.content) + def generate(self, bbox, border_text, keepouts, text_margin, unit=MM): for i, (bbox, child) in enumerate(self.layout_2d(bbox, unit)): first = bool(i == 0) @@ -179,10 +191,10 @@ class TwoSideLayout: warnings.warn('Two-sided pattern used on one side of a TwoSideLayout') def increment_x(self): - return max(self.top.increment_x, self.bottom.increment_x) + return max(self.top.increment_x(), self.bottom.increment_x()) def increment_y(self): - return max(self.top.increment_y, self.bottom.increment_y) + return max(self.top.increment_y(), self.bottom.increment_y()) def fit_size(self, w, h, unit=MM): w1, h1 = self.top.fit_size(w, h, unit) @@ -198,7 +210,7 @@ class TwoSideLayout: def generate(self, bbox, border_text, keepouts, text_margin, unit=MM): yield from self.top.generate(bbox, border_text, keepouts, text_margin, unit) for obj in self.bottom.generate(bbox, border_text, keepouts, text_margin, unit): - obj.side = 'bottom' + obj.flip = not obj.flip yield obj @@ -711,7 +723,7 @@ class AlioCell(Positioned): self.link_pad_width = link_pad_width or unit(1.1, MM) self.link_trace_width = link_trace_width or unit(0.5, MM) self.via_size = via_size or unit(0.4, MM) - self.border_x, self.border_y = False, False + self.border_s, self.border_w, self.border_n, self.border_e = False, False, False, False self.inst_x, self.inst_y = None, None @property @@ -719,9 +731,8 @@ class AlioCell(Positioned): return False def inst(self, x, y, border): - border_s, border_w, border_n, border_e = border inst = copy(self) - inst.border_x, inst.border_y = border_e, border_s + inst.border_s, inst.border_w, inst.border_n, inst.border_e = border inst.inst_x, inst.inst_y = x, y return inst @@ -822,15 +833,15 @@ class AlioCell(Positioned): for side, use in (('top', 'copper'), ('top', 'mask'), ('bottom', 'copper'), ('bottom', 'mask')): if side == 'top': layer_stack[side, use].objects.insert(0, xf(Flash(0, 0, aperture=main_ap, unit=self.unit))) - if not self.border_y: + if not self.border_s and not self.border_e: layer_stack[side, use].objects.append(xf(Flash(self.pitch/2, self.pitch/2, aperture=alio_dark, unit=self.unit))) else: layer_stack[side, use].objects.insert(0, xf(Flash(0, 0, aperture=main_ap_90, unit=self.unit))) - if not self.border_x: + if not self.border_e and not self.border_n: layer_stack[side, use].objects.append(xf(Flash(self.pitch/2, self.pitch/2, aperture=alio_dark_90, unit=self.unit))) layer_stack.drill_pth.append(Flash(x, y, aperture=main_drill, unit=self.unit)) - if not (self.border_x or self.border_y): + if not (self.border_e or self.border_s): layer_stack.drill_pth.append(xf(Flash(self.pitch/2, self.pitch/2, aperture=via_drill, unit=self.unit))) From 4aab344a187434b48b9d2f09090abe0f998d1f2a Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 7 Jul 2024 16:52:24 +0200 Subject: [PATCH 013/103] protoboard: add split front/back view in webthing --- gerbonara/cad/primitives.py | 1 + gerbonara/cad/protoserve.py | 6 +-- gerbonara/cad/protoserve_data/protoserve.html | 43 ++++++++++++++----- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py index c659274..9c7d9dc 100644 --- a/gerbonara/cad/primitives.py +++ b/gerbonara/cad/primitives.py @@ -135,6 +135,7 @@ class Board: force_bounds=force_bounds) def pretty_svg(self, side='top', margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, inkscape=False, colors=None): + print('Pretty svg', side) return self.layer_stack().to_pretty_svg(side=side, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, force_bounds=force_bounds, inkscape=inkscape, colors=colors) diff --git a/gerbonara/cad/protoserve.py b/gerbonara/cad/protoserve.py index 3271af1..c3224e3 100644 --- a/gerbonara/cad/protoserve.py +++ b/gerbonara/cad/protoserve.py @@ -159,11 +159,11 @@ def to_board(obj): mounting_hole_offset=mounting_hole_offset, unit=unit) -@app.route('/preview.svg', methods=['POST']) -async def preview(): +@app.route('/preview_.svg', methods=['POST']) +async def preview(side): obj = await request.get_json() board = to_board(obj) - return Response(str(board.pretty_svg()), mimetype='image/svg+xml') + return Response(str(board.pretty_svg(side=side)), mimetype='image/svg+xml') @app.route('/gerbers.zip', methods=['POST']) async def gerbers(): diff --git a/gerbonara/cad/protoserve_data/protoserve.html b/gerbonara/cad/protoserve_data/protoserve.html index c42ce6c..7a11626 100644 --- a/gerbonara/cad/protoserve_data/protoserve.html +++ b/gerbonara/cad/protoserve_data/protoserve.html @@ -177,11 +177,14 @@ input[type="text"]:focus:valid { position: relative; grid-area: main; padding: 20px; + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; } -#preview-image { - width: 100%; - height: 100%; +#preview > img { + flex-grow: 1; object-fit: contain; } @@ -316,7 +319,8 @@ input[type="text"]:focus:valid {
- Automatically generated preview image + Automatically generated top side preview image + Automatically generated bottom side preview image
@@ -709,6 +710,53 @@ input[type="text"]:focus:valid { + +