summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerbonara/cad/primitives.py54
-rw-r--r--gerbonara/cad/protoboard.py29
-rw-r--r--gerbonara/cad/protoserve.py13
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:
@@ -450,16 +453,36 @@ class SMDStack(PadStack):
@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