From f721692bf348489ac8444e3ed1560e4f0793b213 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 6 Jul 2024 15:51:08 +0200 Subject: Protoboard generator WIP --- gerbonara/cad/primitives.py | 54 ++++++++++++++++++++++++++++++++++----------- gerbonara/cad/protoboard.py | 29 +++++++++++++++--------- gerbonara/cad/protoserve.py | 13 ++++++----- 3 files changed, 66 insertions(+), 30 deletions(-) (limited to 'gerbonara/cad') 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: @@ -449,17 +452,37 @@ class SMDStack(PadStack): return kls(CircleAperture(dia, unit=unit), mask_expansion, paste_expansion, paste, flip, unit=unit) +@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 -- cgit