summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2024-07-07 21:42:10 +0200
committerjaseg <git@jaseg.de>2024-07-07 21:42:10 +0200
commit1ee6b6587a0a0db1ef91284e9f4c413101520ba6 (patch)
tree189a60e1dc87446d32bbf62f9dfb2b79052ce444
parente98f3f3ace38060aaf90afc5d071200b254426dd (diff)
downloadgerbonara-1ee6b6587a0a0db1ef91284e9f4c413101520ba6.tar.gz
gerbonara-1ee6b6587a0a0db1ef91284e9f4c413101520ba6.tar.bz2
gerbonara-1ee6b6587a0a0db1ef91284e9f4c413101520ba6.zip
protoboard: Add permanent breadboard rendering
-rw-r--r--gerbonara/cad/primitives.py4
-rw-r--r--gerbonara/cad/protoboard.py258
-rw-r--r--gerbonara/cad/protoserve.py9
-rw-r--r--gerbonara/cad/protoserve_data/protoserve.html40
4 files changed, 288 insertions, 23 deletions
diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py
index c659274..f581c38 100644
--- a/gerbonara/cad/primitives.py
+++ b/gerbonara/cad/primitives.py
@@ -322,7 +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)
+ h = self.font_size + self.stroke_width # (max_y - min_y)
if self.h_align == 'left':
x0 = 0
@@ -512,7 +512,7 @@ class THTPad(PadStack):
@classmethod
def circle(kls, drill_dia, dia, rotation=0, mask_expansion=0.0, paste_expansion=0.0, paste=True, plated=True, unit=MM):
- pad = SMDStack.circle(dia, rotation, mask_expansion, paste_expansion, paste, unit=unit)
+ pad = SMDStack.circle(dia, mask_expansion, paste_expansion, paste, unit=unit)
return kls(drill_dia, pad, plated=plated)
@classmethod
diff --git a/gerbonara/cad/protoboard.py b/gerbonara/cad/protoboard.py
index ff8bfd8..364dc70 100644
--- a/gerbonara/cad/protoboard.py
+++ b/gerbonara/cad/protoboard.py
@@ -10,7 +10,7 @@ import importlib.resources
from ..utils import MM, rotate_point, bbox_intersect
from .primitives import *
-from ..graphic_objects import Region
+from ..graphic_objects import Region, Line
from ..apertures import RectangleAperture, CircleAperture, ApertureMacroInstance
from ..aperture_macros.parse import ApertureMacro, ParameterExpression, VariableExpression
from ..aperture_macros import primitive as amp
@@ -252,6 +252,230 @@ def alphabetic(case='upper'):
return gen
+@dataclass
+class BreadboardArea:
+ drill: float = 0.9
+ clearance: float = 0.5
+ signal_trace_width: float = 0.8
+ power_trace_width: float = 1.5
+ pitch_x: float = 2.54
+ pitch_y: float = 2.54
+ power_rail_pitch: float = 2.54
+ power_rail_space: float = 2.54
+ num_power_rails: int = 2
+ num_holes: int = 5
+ center_space: float = 5.08
+ horizontal: bool = True
+ margin: float = 0
+ font_size: float = 1.0
+ font_stroke: float = 0.2
+ unit: object = MM
+
+ def fit_size(self, w, h, unit=MM):
+ m = unit(self.margin, self.unit)
+ w = max(0, w-2*m)
+ h = max(0, h-2*m)
+
+ pitch_x = self.width_across
+ pitch_y = self.pitch_y
+ if self.horizontal:
+ pitch_x, pitch_y = pitch_y, pitch_x
+
+ w_mod = round((w + 5e-7) % unit(pitch_x, self.unit), 6)
+ h_mod = round((h + 5e-7) % unit(pitch_y, self.unit), 6)
+ w_fit, h_fit = round(w - w_mod, 6), round(h - h_mod, 6)
+ return w_fit + 2*m, h_fit + 2*m
+
+ @property
+ def width_across(self):
+ w = self.pitch_x * num_holes * 2 + self.center_space
+ if self.num_power_rails > 0:
+ # include one power rail pitch unit for the space between adjacent tiles.
+ w += 2*self.power_rail_space + (2*self.num_power_rails-1) * self.power_rail_pitch
+ return w
+
+ def increment_x(self):
+ if self.horizontal:
+ return self.pitch_y
+ else:
+ return self.width_across
+
+ def increment_y(self):
+ if self.horizontal:
+ return self.width_across
+ else:
+ return self.pitch_y
+
+ @property
+ def single_sided(self):
+ return False
+
+ def generate(self, bbox, border_text, keepouts, text_margin, two_sided, unit=MM):
+ (x, y), (w, h) = self.unit.convert_bounds_from(unit, bbox)
+ w, h = w-x-self.margin, h-y-self.margin
+ ox, oy = (y, x) if self.horizontal else (x, y)
+
+ signal_ap = CircleAperture(self.signal_trace_width, unit=self.unit)
+ power_ap = CircleAperture(self.power_trace_width, unit=self.unit)
+
+ pad_dia = min(self.pitch_x, self.pitch_y) - self.clearance
+ tht_pad = THTPad.circle(self.drill, pad_dia)
+
+ available_width = h if self.horizontal else w
+ length_along = w if self.horizontal else h
+
+ # Key:
+ # H - signal pad
+ # C - center space
+ # P - power pad
+ # R - power rail space
+
+ pitch_key = {
+ 'H': self.pitch_x,
+ 'C': self.center_space,
+ 'P': self.power_rail_pitch,
+ 'R': self.power_rail_space}
+
+ layouts = []
+
+ for i in range(self.num_holes):
+ sig = 'H' * (i+1)
+ layouts.append(sig)
+
+ layouts.append(f'{sig}C{sig}')
+
+ for i in range(self.num_power_rails):
+ pwr = 'P' * (i+1)
+ layouts.append(f'{pwr}R{sig}C{sig}')
+ layouts.append(f'{pwr}R{sig}C{sig}R{pwr}')
+
+ while len(layouts[-1]) <= available_width // self.pitch_x:
+ pre = layouts[-1]
+
+ for i in range(self.num_holes):
+ sig = 'H' * (i+1)
+ layouts.append(f'{pre}R{sig}')
+
+ layouts.append(f'{pre}R{sig}C{sig}')
+
+ for i in range(self.num_power_rails):
+ pwr = 'P' * (i+1)
+ layouts.append(f'{pre}R{sig}C{sig}R{pwr}')
+
+ best_layout, leftover_space = None, None
+ for layout in layouts:
+ actual_width = sum(pitch_key[e] for e in layout)
+
+ if actual_width <= available_width:
+ best_layout = layout
+ leftover_space = available_width - actual_width
+
+ if best_layout is None:
+ return # We don't have enough space to do anything
+ print(f'Chosen layout: {best_layout} with {leftover_space} left over')
+
+ rail_start = {}
+ rail_end = {}
+ n_y = round(length_along//self.pitch_y)
+ for j in range(n_y):
+ y = oy + self.margin + self.pitch_y*(j + 0.5) + (length_along - (n_y*self.pitch_y))/2
+ pos_across = ox + self.margin + leftover_space/2
+ last_e = 'R'
+ for e, group in itertools.groupby(enumerate(best_layout), key=lambda e: e[1]):
+ group = list(group)
+ num = len(group)
+ local_pitch = pitch_key[e]
+
+ points = []
+ for k, _e in group:
+ x = pos_across + local_pitch/2
+ ax, ay = (y, x) if self.horizontal else (x, y)
+ px, py = (self.pitch_y, local_pitch) if self.horizontal else (local_pitch, self.pitch_y)
+
+ if not any(bbox_intersect(ko, ((ax-px/2, ay-py/2), (ax+px/2, ay+py/2))) for ko in keepouts):
+ points.append((ax, ay))
+
+ if e == 'H':
+ yield Pad(ax, ay, pad_stack=tht_pad, unit=self.unit)
+
+ elif e == 'P':
+ yield Pad(ax, ay, pad_stack=tht_pad, unit=self.unit)
+
+ if k not in rail_start:
+ rail_start[k] = (ax, ay)
+ rail_end[k] = (ax, ay)
+
+ pos_across += local_pitch
+
+ if e == 'H':
+ if len(points) > 1:
+ yield Trace(self.signal_trace_width, points[0], points[-1], unit=self.unit)
+
+ label = f'{j+1}'
+
+ if last_e == 'R':
+ tx, ty = points[0]
+
+ if self.horizontal:
+ ty -= self.pitch_x/2
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'center', 'top', unit=self.unit)
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'center', 'top', unit=self.unit, flip=True)
+ else:
+ tx -= self.pitch_x/2
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'right', 'middle', unit=self.unit)
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'right', 'middle', unit=self.unit, flip=True)
+
+ else:
+ tx, ty = points[-1]
+
+ if self.horizontal:
+ ty += self.pitch_x/2
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit)
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit, flip=True)
+ else:
+ tx += self.pitch_x/2
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'left', 'middle', unit=self.unit)
+ yield Text(tx, ty, label, self.font_size, self.font_stroke, 'left', 'middle', unit=self.unit, flip=True)
+ last_e = e
+
+ if self.num_power_rails == 2 and best_layout.count('P') >= 2:
+ power_rail_labels = ['-', '+'] * best_layout.count('P')
+ signal_labels = alphabetic()() # yes, twice.
+
+ line_ap = CircleAperture(self.power_trace_width, unit=self.unit)
+
+ for i, e in enumerate(best_layout):
+ start = rail_start.get(i)
+ end = rail_end.get(i)
+
+ if e == 'P':
+ if start not in (None, end):
+ yield Trace(self.power_trace_width, start, end, unit=self.unit)
+ le_line = [Line(*start, *end, aperture=line_ap, unit=self.unit)]
+ yield Graphics(0, 0, top_silk=le_line, bottom_silk=le_line, unit=self.unit)
+
+ label = power_rail_labels.pop()
+
+ elif e == 'H':
+ label = next(signal_labels)
+ else:
+ label = None
+
+ if label:
+ tx1, ty1 = start
+ tx2, ty2 = end
+ if self.horizontal:
+ pass
+ else:
+ ty1 -= self.pitch_y/2
+ ty2 += self.pitch_y/2
+
+ yield Text(tx1, ty1, label, self.font_size, self.font_stroke, 'center', 'top', unit=self.unit)
+ yield Text(tx1, ty1, label, self.font_size, self.font_stroke, 'center', 'top', unit=self.unit, flip=True)
+ yield Text(tx2, ty2, label, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit)
+ yield Text(tx2, ty2, label, self.font_size, self.font_stroke, 'center', 'bottom', unit=self.unit, flip=True)
+
+
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=None, interval_y=None, margin=0, unit=MM):
self.pitch_x = pitch_x
@@ -278,8 +502,7 @@ class PatternProtoArea:
def fit_rect(self, bbox, unit=MM):
(x, y), (w, h) = bbox
- x, y = x+self.margin, y+self.margin
- w, h = w-x-self.margin, h-y-self.margin
+ w, h = w-x, h-y
w_mod = round((w + 5e-7) % unit(self.pitch_x, self.unit), 6)
h_mod = round((h + 5e-7) % unit(self.pitch_y, self.unit), 6)
@@ -722,7 +945,8 @@ class StarburstPad(PadStack):
pitch_y: float = 2.54
trace_width_x: float = 1.4
trace_width_y: float = 1.4
- clearance: float = 0.5
+ solder_clearance: float = 0.4
+ mask_width: float = 0.5
drill: float = 0.9
annular_ring: float = 1.2
@@ -743,19 +967,19 @@ class StarburstPad(PadStack):
amp.Circle(MM, 1, var(6)),
))
- main_ap = ApertureMacroInstance(starburst_macro, (self.pitch_x - self.clearance,# 1
- self.trace_width_x, # 2
- self.pitch_y - self.clearance,# 3
- self.trace_width_y, # 4
- self.clearance, # 5
- self.annular_ring), unit=self.unit) # 6
-
- mask_ap = ApertureMacroInstance(starburst_macro, (self.pitch_x, # 1
- self.trace_width_x, # 2
- self.pitch_y, # 3
- self.trace_width_y, # 4
- self.clearance, # 5
- self.annular_ring), unit=self.unit) # 6
+ main_ap = ApertureMacroInstance(starburst_macro, (self.pitch_x - self.solder_clearance, # 1
+ self.trace_width_x, # 2
+ self.pitch_y - self.solder_clearance, # 3
+ self.trace_width_y, # 4
+ self.mask_width, # 5
+ self.annular_ring), unit=self.unit) # 6
+
+ mask_ap = ApertureMacroInstance(starburst_macro, (self.pitch_x, # 1
+ self.trace_width_x, # 2
+ self.pitch_y, # 3
+ self.trace_width_y, # 4
+ self.mask_width, # 5
+ self.annular_ring), unit=self.unit) # 6
yield PadStackAperture(main_ap, 'top', 'copper')
yield PadStackAperture(mask_ap, 'top', 'mask')
diff --git a/gerbonara/cad/protoserve.py b/gerbonara/cad/protoserve.py
index 68f401c..605f399 100644
--- a/gerbonara/cad/protoserve.py
+++ b/gerbonara/cad/protoserve.py
@@ -129,12 +129,19 @@ def deserialize(obj, unit):
via_size=via_size
), margin=unit(1.5, MM), unit=unit)
+ case 'breadboard':
+ horizontal = obj.get('direction', 'v') == 'h'
+ drill = float(obj.get('hole_dia', 0.9))
+ return pb.BreadboardArea(clearance=clearance, drill=drill, horizontal=horizontal, unit=unit)
+
case 'starburst':
trace_width_x = float(obj.get('trace_width_x', 1.8))
trace_width_y = float(obj.get('trace_width_y', 1.8))
drill = float(obj.get('hole_dia', 0.9))
annular_ring = float(obj.get('annular', 1.2))
- return pb.PatternProtoArea(pitch_x, pitch_y, pb.StarburstPad(pitch_x, pitch_y, trace_width_x, trace_width_y, clearance, drill, annular_ring, unit=unit), unit=unit)
+ clearance = float(obj.get('clearance', 0.4))
+ mask_width = float(obj.get('mask_width', 0.5))
+ return pb.PatternProtoArea(pitch_x, pitch_y, pb.StarburstPad(pitch_x, pitch_y, trace_width_x, trace_width_y, clearance, mask_width, drill, annular_ring, unit=unit), unit=unit)
case 'rf':
pitch = float(obj.get('pitch', 2.54))
diff --git a/gerbonara/cad/protoserve_data/protoserve.html b/gerbonara/cad/protoserve_data/protoserve.html
index e454f87..4da027a 100644
--- a/gerbonara/cad/protoserve_data/protoserve.html
+++ b/gerbonara/cad/protoserve_data/protoserve.html
@@ -412,6 +412,7 @@ input[type="text"]:focus:valid {
<a href="#" data-placeholder="spiky" class="double-sided-only">Spiky hybrid area</a>
<a href="#" data-placeholder="alio" class="double-sided-only">ALio hybrid area</a>
<a href="#" data-placeholder="starburst" class="double-sided-only">THT starburst area</a>
+ <a href="#" data-placeholder="breadboard" class="double-sided-only">Permanent breadboard area</a>
</div>
</div>
</template>
@@ -479,7 +480,7 @@ input[type="text"]:focus:valid {
<span class="unit us">mil</span>
</label>
<label>Plating
- <select name="plating" value="through">
+ <select name="plating" value="plated">
<option value="plated">Double-sided, through-plated</option>
<option value="nonplated">Double-sided, non-plated</option>
<option value="singleside">Single-sided, non-plated</option>
@@ -505,6 +506,34 @@ input[type="text"]:focus:valid {
</div>
</template>
+ <template id="tpl-g-breadboard">
+ <div data-type="breadboard" class="group breadboard">
+ <h4>Permanent breadboard area</h4>
+ <span class="content area-controls">(<a href="#" class="area-remove">Remove</a><a href="#" class="area-move">Move</a>)</span>
+ <label class="proportion">Proportion
+ <input type="text" name="layout_prop" value="1" pattern="[0-9]+\.?[0-9]*"/>
+ </label>
+
+ <h5>Area Settings</h5>
+ <label>Direction
+ <select name="direction" value="v">
+ <option value="v">Vertical</option>
+ <option value="h">Horizontal</option>
+ </select>
+ </label>
+ <label>Clearance
+ <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
+ <span class="unit metric">mm</span>
+ <span class="unit us">mil</span>
+ </label>
+ <label>Hole diameter
+ <input type="text" name="hole_dia" placeholder="length" value="0.9" pattern="[0-9]+\.?[0-9]*"/>
+ <span class="unit metric">mm</span>
+ <span class="unit us">mil</span>
+ </label>
+ </div>
+ </template>
+
<template id="tpl-g-manhattan">
<div data-type="manhattan" class="group manhattan">
<h4>Manhattan area</h4>
@@ -739,8 +768,13 @@ input[type="text"]:focus:valid {
<span class="unit metric">mm</span>
<span class="unit us">mil</span>
</label>
- <label>Clearance
- <input type="text" name="clearance" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
+ <label>Pad clearance
+ <input type="text" name="clearance" placeholder="length" value="0.4" pattern="[0-9]+\.?[0-9]*"/>
+ <span class="unit metric">mm</span>
+ <span class="unit us">mil</span>
+ </label>
+ <label>Soldermask wall
+ <input type="text" name="mask_width" placeholder="length" value="0.5" pattern="[0-9]+\.?[0-9]*"/>
<span class="unit metric">mm</span>
<span class="unit us">mil</span>
</label>