rework WIP

This commit is contained in:
jaseg 2023-05-08 23:22:55 +02:00
parent 732c58f70b
commit 03f2ec0a30
2 changed files with 180 additions and 136 deletions

View file

@ -150,6 +150,8 @@ class ExcellonTool(Aperture):
# Internal use, for layer dilation.
def dilated(self, offset, unit=MM):
offset = unit(offset, self.unit)
if math.isclose(offset, 0, abs_tol=1e-6):
return self
return replace(self, diameter=self.diameter+2*offset)
@lru_cache()
@ -188,6 +190,8 @@ class CircleAperture(Aperture):
def dilated(self, offset, unit=MM):
offset = self.unit(offset, unit)
if math.isclose(offset, 0, abs_tol=1e-6):
return self
return replace(self, diameter=self.diameter+2*offset, hole_dia=None)
@lru_cache()
@ -235,13 +239,15 @@ class RectangleAperture(Aperture):
def dilated(self, offset, unit=MM):
offset = self.unit(offset, unit)
if math.isclose(offset, 0, abs_tol=1e-6):
return self
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None)
@lru_cache()
def rotated(self, angle=0):
if math.isclose(angle % math.pi, 0):
if math.isclose(angle % math.pi, 0, abs_tol=1e-6):
return self
elif math.isclose(angle % math.pi, math.pi/2):
elif math.isclose(angle % math.pi, math.pi/2, abs_tol=1e-6):
return replace(self, w=self.h, h=self.w, hole_dia=self.hole_dia)
else: # odd angle
return self.to_macro(angle)
@ -295,6 +301,8 @@ class ObroundAperture(Aperture):
def dilated(self, offset, unit=MM):
offset = self.unit(offset, unit)
if math.isclose(offset, 0, abs_tol=1e-6):
return self
return replace(self, w=self.w+2*offset, h=self.h+2*offset, hole_dia=None)
@lru_cache()
@ -362,6 +370,8 @@ class PolygonAperture(Aperture):
def dilated(self, offset, unit=MM):
offset = self.unit(offset, unit)
if math.isclose(offset, 0, abs_tol=1e-6):
return self
return replace(self, diameter=self.diameter+2*offset, hole_dia=None)
flash = _flash_hole
@ -388,7 +398,7 @@ class PolygonAperture(Aperture):
if self.hole_dia is not None:
return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation, self.unit.convert_to(unit, self.hole_dia)
elif rotation is not None and not math.isclose(rotation, 0):
elif rotation is not None and not math.isclose(rotation, 0, abs_tol=1e-6):
return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation
else:
return self.unit.convert_to(unit, self.diameter), self.n_vertices
@ -418,11 +428,13 @@ class ApertureMacroInstance(Aperture):
return out
def dilated(self, offset, unit=MM):
if math.isclose(offset, 0, abs_tol=1e-6):
return self
return replace(self, macro=self.macro.dilated(offset, unit))
@lru_cache()
def rotated(self, angle=0.0):
if math.isclose(angle % (2*math.pi), 0):
if math.isclose(angle % (2*math.pi), 0, abs_tol=1e-6):
return self
else:
return self.to_macro(angle)

View file

@ -51,7 +51,7 @@ class Board:
@property
def abs_pos(self):
return self.x, self.y, self.rotation
return self.x, self.y, self.rotation, False
def add_silk(self, side, obj):
if side not in ('top', 'bottom'):
@ -142,21 +142,18 @@ class Positioned:
y: float
_: KW_ONLY
rotation: float = 0.0
side: str = 'top'
flip: bool = False
unit: LengthUnit = MM
parent: object = None
def flip(self):
self.side = 'top' if self.side == 'bottom' else 'bottom'
@property
def abs_pos(self):
if self.parent is None:
px, py, pa = 0, 0, 0
px, py, pa, pf = 0, 0, 0, False
else:
px, py, pa = self.parent.abs_pos
px, py, pa, pf = self.parent.abs_pos
return self.x+px, self.y+py, self.rotation+pa
return self.x+px, self.y+py, self.rotation+pa, (bool(self.flip) != bool(pf))
def bounding_box(self, unit=MM):
stack = LayerStack()
@ -177,7 +174,7 @@ class Positioned:
@dataclass
class ObjectGroup(Positioned):
class Graphics(Positioned):
top_copper: list = field(default_factory=list)
top_mask: list = field(default_factory=list)
top_silk: list = field(default_factory=list)
@ -188,15 +185,10 @@ class ObjectGroup(Positioned):
bottom_paste: list = field(default_factory=list)
drill_npth: list = field(default_factory=list)
drill_pth: list = field(default_factory=list)
objects: list = field(default_factory=list)
def render(self, layer_stack, cache=None):
x, y, rotation = self.abs_pos
top, bottom = ('bottom', 'top') if self.side == 'bottom' else ('top', 'bottom')
for obj in self.objects:
obj.parent = self
obj.render(layer_stack, cache=cache)
x, y, rotation, flip = self.abs_pos
top, bottom = ('bottom', 'top') if flip else ('top', 'bottom')
for target, source in [
(layer_stack[top, 'copper'], self.top_copper),
@ -229,7 +221,6 @@ class ObjectGroup(Positioned):
self.bottom_paste,
self.drill_npth,
self.drill_pth,
self.objects,
))), unit(self.x, self.unit), unit(self.y, self.unit))
else:
return super().bounding_box(unit)
@ -242,6 +233,30 @@ class ObjectGroup(Positioned):
return not (any_drill or (any_top and any_bottom))
@dataclass
class ObjectGroup(Positioned):
objects: list = field(default_factory=list)
def render(self, layer_stack, cache=None):
for obj in self.objects:
if not isinstance(obj, Positioned):
raise ValueError(f'ObjectGroup members must be children of Positioned, not {type(obj)}')
obj.parent = self
obj.render(layer_stack, cache=cache)
def bounding_box(self, unit=MM):
if math.isclose(self.rotation, 0, abs_tol=1e-3):
return offset_bounds(sum_bounds((obj.bounding_box(unit=unit) for obj in self.objects)),
unit(self.x, self.unit), unit(self.y, self.unit))
else:
return super().bounding_box(unit)
@property
def single_sided(self):
return all(obj.single_sided for obj in self.objects)
@dataclass
class Text(Positioned):
text: str
@ -253,7 +268,7 @@ class Text(Positioned):
polarity_dark: bool = True
def render(self, layer_stack, cache=None):
obj_x, obj_y, rotation = self.abs_pos
obj_x, obj_y, rotation, flip = self.abs_pos
global newstroke_font
if newstroke_font is None:
@ -298,7 +313,7 @@ class Text(Positioned):
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[self.side, self.layer].objects.append(obj)
layer_stack['bottom' if flip else 'top', self.layer].objects.append(obj)
def bounding_box(self, unit=MM):
approx_w = len(self.text)*self.font_size*0.75 + self.stroke_width
@ -323,155 +338,172 @@ class Text(Positioned):
@dataclass
class Pad(Positioned):
pass
pad_stack: PadStack
@property
def single_sided(self):
return self.pad_stack.single_sided
@dataclass
class SMDPad(Pad):
copper_aperture: Aperture
mask_aperture: Aperture
paste_aperture: Aperture
silk_features: list = field(default_factory=list)
@dataclass(frozen=True, slots=True)
class PadStackAperture:
aperture: Aperture
side: str
layer: str
offset_x: float = 0 # in PadStack units
offset_y: float = 0
rotation: float = 0
def render(self, layer_stack, cache=None):
x, y, rotation = self.abs_pos
layer_stack[self.side, 'copper'].objects.append(Flash(x, y, self.copper_aperture.rotated(rotation), unit=self.unit))
layer_stack[self.side, 'mask' ].objects.append(Flash(x, y, self.mask_aperture.rotated(rotation), unit=self.unit))
if self.paste_aperture:
layer_stack[self.side, 'paste' ].objects.append(Flash(x, y, self.paste_aperture.rotated(rotation), unit=self.unit))
layer_stack[self.side, 'silk' ].objects.extend([copy(feature).rotate(rotation).offset(x, y, self.unit)
for feature in self.silk_features])
@dataclass(frozen=True, slots=True)
class PadStack:
_: KW_ONLY
unit: LengthUnit = MM
@property
def apertures(self):
raise NotImplementedError()
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.rotate(rotation)
fl.offset(x, y)
side = fl.side
if flip:
side = {'top': 'bottom', 'bottom': 'top'}.get(side, side)
yield side, fl.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':
layer_stack.drill_pth.objects.append(flash)
elif side == 'drill' and use == 'nonplated':
layer_stack.drill_npth.objects.append(flash)
elif (side, layer) in layer_stack:
layer_stack[side, layer].objects.append(flash)
@property
def single_sided(self):
return len({ap.side for ap in self.apertures}) <= 1
@dataclass(frozen=True, slots=True)
class SMDStack(PadStack):
aperture: Aperture
mask_expansion: float = 0.0
paste_expansion: float = 0.0
paste: bool = True
flip: bool = False
@property
def side(self):
return 'bottom' if self.flip else 'top'
@property
def apertures(self):
yield PadStackAperture(self.aperture, self.side, 'copper')
yield PadStackAperture(self.aperture.dilated(self.mask_expansion, self.unit), self.side, 'mask')
if self.paste:
yield PadStackAperture(self.aperture.dilated(self.paste_expansion, self.unit), self.side, 'paste')
@classmethod
def rect(kls, x, y, w, h, rotation=0, side='top', mask_expansion=0.0, paste_expansion=0.0, paste=True, unit=MM):
ap_c = RectangleAperture(w, h, unit=unit)
ap_m = RectangleAperture(w+2*mask_expansion, h+2*mask_expansion, unit=unit)
ap_p = RectangleAperture(w+2*paste_expansion, h+2*paste_expansion, unit=unit) if paste else None
return kls(x, y, side=side, copper_aperture=ap_c, mask_aperture=ap_m, paste_aperture=ap_p, rotation=rotation,
unit=unit)
def rect(kls, w, h, rotation=0, mask_expansion=0.0, paste_expansion=0.0, paste=True, flip=False, unit=MM):
ap = RectangleAperture(w, h, unit=unit).rotated(rotation)
return kls(ap, mask_expansion, paste_expansion, paste, flip, unit=unit)
@classmethod
def circle(kls, x, y, dia, side='top', mask_expansion=0.0, paste_expansion=0.0, paste=True, unit=MM):
ap_c = CircleAperture(dia, unit=unit)
ap_m = CircleAperture(dia+2*mask_expansion, unit=unit)
ap_p = CircleAperture(dia+2*paste_expansion, unit=unit) if paste else None
return kls(x, y, side=side, copper_aperture=ap_c, mask_aperture=ap_m, paste_aperture=ap_p, unit=unit)
def circle(kls, dia, mask_expansion=0.0, paste_expansion=0.0, paste=True, flip=False, unit=MM):
return kls(CircleAperture(dia, unit=unit), mask_expansion, paste_expansion, paste, flip, unit=unit)
@dataclass
class THTPad(Pad):
@dataclass(frozen=True, slots=True)
class THTPad(PadStack):
drill_dia: float
pad_top: SMDPad
pad_bottom: SMDPad = None
pad_top: SMDStack
pad_bottom: SMDStack = None
aperture_inner: Aperture = None
plated: bool = True
def __post_init__(self):
if self.pad_bottom is None:
import sys
self.pad_bottom = copy(self.pad_top)
self.pad_bottom.flip()
object.__setattr__(self, 'pad_bottom', replace(self.pad_top, flip=True))
self.pad_top.parent = self.pad_bottom.parent = self
if self.pad_top.flip:
raise ValueError('top pad cannot be flipped')
if (self.pad_top.side, self.pad_bottom.side) != ('top', 'bottom'):
raise ValueError(f'The top and bottom pads must have side set to top and bottom, respectively. Currently, the top pad side is set to "{self.pad_top.side}" and the bottom pad side to "{self.pad_bottom.side}".')
@property
def plating(self):
return 'plated' if self.plated else 'nonplated'
def render(self, layer_stack, cache=None):
x, y, rotation = self.abs_pos
self.pad_top.parent = self
self.pad_top.render(layer_stack)
if self.pad_bottom:
self.pad_bottom.parent = self
self.pad_bottom.render(layer_stack)
if self.aperture_inner is None:
(x_min, y_min), (x_max, y_max) = self.pad_top.bounding_box(MM)
w_top = x_max - x_min
h_top = y_max - y_min
if self.pad_bottom:
(x_min, y_min), (x_max, y_max) = self.pad_bottom.bounding_box(MM)
w_bottom = x_max - x_min
h_bottom = y_max - y_min
w_top = min(w_top, w_bottom)
h_top = min(h_top, h_bottom)
self.aperture_inner = CircleAperture(min(w_top, h_top), unit=MM)
for (side, use), layer in layer_stack.inner_layers:
layer.objects.append(Flash(x, y, self.aperture_inner.rotated(rotation), unit=self.unit))
hole = Flash(x, y, ExcellonTool(self.drill_dia, plated=self.plated, unit=self.unit), unit=self.unit)
if self.plated:
layer_stack.drill_pth.objects.append(hole)
else:
layer_stack.drill_npth.objects.append(hole)
@property
def apertures(self):
yield from self.pad_top.apertures
yield from self.pad_bottom.apertures
yield PadStackAperture(self.aperture_inner, 'inner', 'copper')
yield PadStackAperture(ExcellonTool(self.drill_dia, plated=self.plated, unit=self.unit), 'drill', self.plating)
@property
def single_sided(self):
return False
@classmethod
def rect(kls, x, y, hole_dia, w, h=None, rotation=0, mask_expansion=0.0, paste_expansion=0.0, paste=True, plated=True, unit=MM):
if h is None:
h = w
pad = SMDPad.rect(0, 0, w, h, mask_expansion=mask_expansion, paste_expansion=paste_expansion, paste=paste, unit=unit)
return kls(x, y, hole_dia, pad, rotation=rotation, plated=plated, unit=unit)
def rect(kls, drill_dia, w, h, rotation=0, mask_expansion=0.0, paste_expansion=0.0, paste=True, plated=True, unit=MM):
pad = SMDStack.rect(w, h, rotation, mask_expansion, paste_expansion, paste, unit=unit)
return kls(drill_dia, pad, plated=plated)
@classmethod
def circle(kls, x, y, hole_dia, dia, mask_expansion=0.0, paste_expansion=0.0, paste=True, plated=True, unit=MM):
pad = SMDPad.circle(0, 0, dia, mask_expansion=mask_expansion, paste_expansion=paste_expansion, paste=paste, unit=unit)
return kls(x, y, hole_dia, pad, plated=plated, unit=unit)
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)
return kls(drill_dia, pad, plated=plated)
@classmethod
def obround(kls, x, y, hole_dia, w, h, rotation=0, mask_expansion=0.0, paste_expanson=0.0, paste=True, plated=True, unit=MM):
ap_c = ObroundAperture(w, h, unit=unit)
ap_m = ObroundAperture(w+2*mask_expansion, h+2*mask_expansion, unit=unit)
ap_p = ObroundAperture(w, h, unit=unit) if paste else None
pad = SMDPad(0, 0, side='top', copper_aperture=ap_c, mask_aperture=ap_m, paste_aperture=ap_p, unit=unit)
return kls(x, y, hole_dia, pad, rotation=rotation, plated=plated, unit=unit)
def obround(kls, drill_dia, w, h, rotation=0, mask_expansion=0.0, paste_expansion=0.0, paste=True, plated=True, unit=MM):
ap = ObroundAperture(w, h, unit=unit).rotated(rotation)
pad = SMDStack(ap, mask_expansion, paste_expansion, paste, unit=unit)
return kls(drill_dia, pad, plated=plated)
@dataclass
class Hole(Positioned):
diameter: float
mask_copper_margin: float = 0.2
def render(self, layer_stack, cache=None):
x, y, rotation = self.abs_pos
hole = Flash(x, y, ExcellonTool(self.diameter, plated=False, unit=self.unit), unit=self.unit)
layer_stack.drill_npth.objects.append(hole)
if self.mask_copper_margin > 0:
mask = Flash(x, y, CircleAperture(self.mask_copper_margin, unit=self.unit), polarity_dark=False, unit=self.unit)
layer_stack['top', 'copper'].objects.append(mask)
layer_stack['bottom', 'copper'].objects.append(mask)
@property
def single_sided(self):
return False
@dataclass
class Via(Positioned):
diameter: float
@dataclass(frozen=True, slots=True)
class ThroughViaStack(PadStack):
hole: float
dia: float = None
tented: bool = True
def render(self, layer_stack, cache=None):
x, y, rotation = self.abs_pos
aperture = CircleAperture(diameter=self.diameter, unit=self.unit)
tool = ExcellonTool(diameter=self.hole, unit=self.unit)
for (side, use), layer in layer_stack.copper_layers:
layer.objects.append(Flash(x, y, aperture, unit=self.unit))
layer_stack.drill_pth.objects.append(Flash(x, y, tool, unit=self.unit))
def __post_init__(self):
if self.dia == None:
object.__setattr__(self, 'dia', self.hole*2)
@property
def single_sided(self):
return False
@property
def apertures(self):
copper_aperture = CircleAperture(self.dia, unit=self.unit)
yield PadStackAperture(copper_aperture, 'top', 'copper')
yield PadStackAperture(copper_aperture, 'bottom', 'copper')
yield PadStackAperture(copper_aperture, 'inner', 'copper')
if self.tented:
yield PadStackAperture(copper_aperture, 'top', 'mask')
yield PadStackAperture(copper_aperture, 'bottom', 'mask')
yield PadStackAperture(ExcellonTool(self.hole, plated=True, unit=self.unit), 'drill', 'plated')
@dataclass(frozen=True, slots=True)
class Via(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)
@classmethod
def at(kls, x, y, hole, dia=None, tented=True, unit=MM):
return kls(x, y, ThroughViaStack(hole, dia, tented, unit=unit), unit=unit)
@dataclass
class Trace: