From d0894b252225b89054db379a5f328885f7b046ea Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 4 Apr 2023 01:31:19 +0200 Subject: Add beginnings of CAD module --- gerbonara/cad/primitives.py | 233 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 gerbonara/cad/primitives.py (limited to 'gerbonara/cad/primitives.py') diff --git a/gerbonara/cad/primitives.py b/gerbonara/cad/primitives.py new file mode 100644 index 0000000..f757e67 --- /dev/null +++ b/gerbonara/cad/primitives.py @@ -0,0 +1,233 @@ + +import math +from copy import copy +from itertools import zip_longest +from dataclasses import dataclass, field, KW_ONLY + +from ..utils import LengthUnit, MM, rotate_point +from ..layers import LayerStack +from ..graphic_objects import Line, Arc, Flash +from ..apertures import Aperture, CircleAperture, RectangleAperture, ExcellonTool + + +def sgn(x): + return -1 if x < 0 else 1 + + +class Board: + def __init__(self): + self.objects = set() + + def to_layer_stack(self, layer_stack): + if layer_stack is None: + layer_stack = LayerStack() + + for obj in self.objects: + obj.render(stack) + + +@dataclass +class Positioned: + x: float + y: float + _: KW_ONLY + rotation: float = 0.0 + unit: LengthUnit = MM + parent: object = None + + @property + def abs_pos(self, dx, dy, da): + x, y = rotate_point(self.x, self.y, da) + + if self.parent is None: + px, py, pa = dx, dy, 0 + else: + px, py, pa = self.parent.abs_pos(dx, dy, da) + + return x+px, y+py, self.rotation+da+pa + + +@dataclass +class Pad(Positioned): + pass + +@dataclass +class SMDPad(Pad): + copper_aperture: Aperture + mask_aperture: Aperture + paste_aperture: Aperture + silk_features: list + side: str = 'top' + + def to_layer_stack(self, layer_stack): + x, y, rotation = self.abs_pos + stack[self.side, 'copper'].objects.append(Flash(x, y, self.copper_aperture.rotated(rotation), unit=self.unit)) + stack[self.side, 'mask' ].objects.append(Flash(x, y, self.mask_aperture.rotated(rotation), unit=self.unit)) + stack[self.side, 'paste' ].objects.append(Flash(x, y, self.paste_aperture.rotated(rotation), unit=self.unit)) + stack[self.side, 'silk' ].objects.extend([copy(feature).rotate(rotation).offset(x, y, self.unit) + for feature in self.silk_features]) + + def flip(self): + self.side = 'top' if self.side == 'bottom' else 'top' + + +class THTPad(Pad): + drill_dia: float + pad_top: SMDPad + pad_bottom: SMDPad = None + aperture_inner: Aperture = None + plated: bool = True + + def __post_init__(self): + if self.pad_bottom is None: + self.pad_bottom = copy(self.pad_top) + self.pad_bottom.flip() + + self.pad_top.parent = self.pad_bottom.parent = self + + 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}.') + + def to_layer_stack(self, layer_stack, x, y, rotation): + x, y, rotation = self.abs_pos + self.top_pad.to_layer_stack(layer_stack) + self.bottom_pad.to_layer_stack(layer_stack) + + 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(self.x, self.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) + + +@dataclass +class Via(Positioned): + diameter: float + hole: float + + def to_layer_stack(self, layer_stack): + 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)) + + +@dataclass +class Trace: + width: float + start: object = None + end: object = None + side: str = 'top' + waypoints: [(float, float)] = field(default_factory=list) + style: str = 'direct' + orientation: [str] = tuple() # 'top' or 'bottom' + roundover: float = 0 + unit: LengthUnit = MM + parent: object = None + + DIRECT = 'direct' + OBLIQUE = 'oblique' + ORTHO = 'ortho' + + CW = 'cw' + CCW = 'ccw' + + def _route(self, p1, p2, orientation): + x1, y1 = p1 + x2, y2 = p2 + dx = x2-x1 + dy = y2-y1 + + yield p1 + + if self.style == 'direct' or \ + math.isclose(x1, x2, abs_tol=1e-6) or math.isclose(y1, y2, abs_tol=1e-6) or \ + (self.style == 'oblique' and math.isclose(dx, dy, abs_tol=1e-6)): + yield p2 + return + + p = (abs(dy) > abs(dx)) == ((dx >= 0) == (dy >= 0)) + if self.style == 'oblique': + if p == (orientation == 'cw'): + if abs(dy) > abs(dx): + yield (0, sgn(dy)*(abs(dy)-abs(dx))) + else: + yield (sgn(dx)*(abs(dx)-abs(dy)), 0) + else: + if abs(dy) > abs(dx): + yield (dx, sgn(dy)*abs(dx)) + else: + yield (sgn(dx)*abs(dy), dy) + else: # self.style == 'ortho' + pass + #if p == (orientation == 'cw'): + + #else: + + yield p2 + + def to_layer_stack(self, layer_stack, x, y, rotation): + start, end = self.start, self.end + + if not isinstance(start, tuple): + start = start.abs_pos + if not isinstance(end, tuple): + end = end.abs_pos + + points = [start, *self.waypoints, end] + aperture = CircleAperture(diameter=self.width, unit=self.unit) + + for p1, p2, orientation in zip_longest(points[:-1], points[1:], self.orientation): + layer_stack[self.side, 'copper'].extend(self._route(p1, p2, orientation, aperture)) + +if __name__ == '__main__': + from ..utils import setup_svg, Tag + from ..newstroke import Newstroke + + font = Newstroke() + + tags = [] + for n in range(0, 8*6): + theta = 2*math.pi / (8*6) * n + dx, dy = math.cos(theta), math.sin(theta) + tr = Trace(0.1, style='oblique') + points_cw = list(tr._route((0, 0), (dx, dy), 'cw')) + points_ccw = list(tr._route((0, 0), (dx, dy), 'ccw')) + + pd = lambda points: f'M {points[0][0]}, {points[0][1]} ' + ' '.join(f'L {x}, {y}' for x, y in points[1:]) + + strokes = list(font.render(f'α={n/(8*6)*360}', size=0.2)) + xs = [x for st in strokes for x, _y in st] + ys = [y for st in strokes for _x, y in st] + min_x, min_y, max_x, max_y = min(xs), min(ys), max(xs), max(ys) + + xf = f'translate({n//6*1.1 + 0.1} {n%6*1.3 + 0.3}) scale(0.5 0.5) translate(1 1)' + txf = f'{xf} translate(0 -1.2) translate({-(max_x-min_x)/2} {-max_y})' + + tags.append(Tag('circle', cx='0', cy='0', r='1', + fill='none', stroke='black', opacity='0.5', stroke_width='0.05', + transform=xf)) + tags.append(Tag('path', + fill='none', + stroke='red', opacity='0.5', stroke_width='0.05', stroke_linecap='round', + transform=xf, d=pd(points_cw))) + tags.append(Tag('path', + fill='none', + stroke='blue', opacity='0.5', stroke_width='0.05', stroke_linecap='round', + transform=xf, d=pd(points_ccw))) + tags.append(Tag('path', + fill='none', + stroke='black', opacity='0.5', stroke_width='0.02', stroke_linejoin='round', stroke_linecap='round', + transform=txf, d=' '.join(pd(points) for points in strokes))) + + print(setup_svg([Tag('g', tags, transform='scale(20 20)')], [(0, 0), (20*10*1.1 + 0.1, 20*10*1.3 + 0.1)])) + + -- cgit