protoboard: Add permanent breadboard rendering
This commit is contained in:
parent
e98f3f3ace
commit
1ee6b6587a
4 changed files with 287 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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.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.mask_width, # 5
|
||||
self.annular_ring), unit=self.unit) # 6
|
||||
|
||||
yield PadStackAperture(main_ap, 'top', 'copper')
|
||||
yield PadStackAperture(mask_ap, 'top', 'mask')
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue