summaryrefslogtreecommitdiff
path: root/gerbonara/cad/primitives.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/cad/primitives.py')
-rw-r--r--gerbonara/cad/primitives.py66
1 files changed, 48 insertions, 18 deletions
diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py
index 6ffd4e2..f581c38 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
@@ -115,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):
@@ -319,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 = self.font_size + self.stroke_width # (max_y - min_y)
if self.h_align == 'left':
x0 = 0
@@ -329,16 +333,16 @@ class Text(Positioned):
else:
raise ValueError('h_align must be one of "left", "center", or "right".')
- if self.v_align == 'top':
- y0 = -(max_y - min_y)
+ if self.v_align == 'bottom':
+ y0 = h
elif self.v_align == 'middle':
- y0 = -(max_y - min_y)/2
- elif self.v_align == 'bottom':
+ y0 = h/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 +352,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)
@@ -365,11 +369,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)
@@ -382,6 +386,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)
@@ -396,20 +401,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, polarity_dark=not ap.invert, 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 +455,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 +497,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
@@ -486,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
@@ -538,6 +564,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