Protoboard: All layouts except for spiky proto work

This commit is contained in:
jaseg 2024-07-07 00:32:17 +02:00
parent f721692bf3
commit 552f30c15d
2 changed files with 135 additions and 121 deletions

View file

@ -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

View file

@ -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)),