From b2729a46ac36402b30e567b02f8e67caefed7ac9 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 7 Jul 2023 20:19:36 +0200 Subject: Improve auto layout API --- gerbonara/cad/kicad/pcb.py | 153 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 4 deletions(-) (limited to 'gerbonara/cad/kicad/pcb.py') diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py index 9ab00b3..bfdb46b 100644 --- a/gerbonara/cad/kicad/pcb.py +++ b/gerbonara/cad/kicad/pcb.py @@ -2,6 +2,7 @@ Library for handling KiCad's PCB files (`*.kicad_mod`). """ +import math from pathlib import Path from dataclasses import field from itertools import chain @@ -14,14 +15,14 @@ from .primitives import * from .footprints import Footprint from . import graphical_primitives as gr -from ..primitives import Positioned +from .. import primitives as cad_pr from ... import graphic_primitives as gp from ... import graphic_objects as go from ... import apertures as ap from ...layers import LayerStack from ...newstroke import Newstroke -from ...utils import MM +from ...utils import MM, rotate_point def match_filter(f, value): @@ -29,6 +30,29 @@ def match_filter(f, value): return True return value in f +def gn_side_to_kicad(side, layer='Cu'): + if side == 'top': + return f'F.{layer}' + elif side == 'bottom': + return f'B.{layer}' + elif side.startswith('inner'): + return f'In{int(side[5:])}.{layer}' + else: + raise ValueError(f'Cannot parse gerbonara side name "{side}"') + +def gn_layer_to_kicad(layer, flip=False): + side = 'B' if flip else 'F' + if layer == 'silk': + return f'{side}.SilkS' + elif layer == 'mask': + return f'{side}.Mask' + elif layer == 'paste': + return f'{side}.Paste' + elif layer == 'copper': + return f'{side}.Cu' + else: + raise ValueError('Cannot translate gerbonara layer name "{layer}" to KiCad') + @sexp_type('general') class GeneralSection: @@ -160,6 +184,13 @@ class TrackSegment: aperture = ap.CircleAperture(self.width, unit=MM) yield go.Line(self.start.x, self.start.y, self.end.x, self.end.y, aperture=aperture, unit=MM) + def rotate(self, angle, cx=None, cy=None): + if cx is None or cy is None: + cx, cy = self.start.x, self.start.y + + self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy) + self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy) + @sexp_type('arc') class TrackArc: @@ -182,6 +213,14 @@ class TrackArc: x2, y2 = self.end.x, self.end.y yield go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, aperture=aperture, clockwise=True, unit=MM) + def rotate(self, angle, cx=None, cy=None): + if cx is None or cy is None: + cx, cy = self.mid.x, self.mid.y + + self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy) + self.mid.x, self.mid.y = rotate_point(self.mid.x, self.mid.y, angle, cx, cy) + self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy) + @sexp_type('via') class Via: @@ -231,8 +270,8 @@ class Board: images: List(Image) = field(default_factory=list) # Tracks track_segments: List(TrackSegment) = field(default_factory=list) - vias: List(Via) = field(default_factory=list) track_arcs: List(TrackArc) = field(default_factory=list) + vias: List(Via) = field(default_factory=list) # Other stuff zones: List(Zone) = field(default_factory=list) groups: List(Group) = field(default_factory=list) @@ -248,8 +287,98 @@ class Board: for fp in self.footprints: fp.board = self + self.nets = {net.index: net.name for net in self.nets} + def __before_sexp__(self): self.properties = [Property(key, value) for key, value in self.properties.items()] + self.nets = [Net(index, name) for index, name in self.nets.items()] + + def add(self, obj): + match obj: + case gr.Text(): + self.texts.append(obj) + case gr.TextBox(): + self.text_boxes.append(obj) + case gr.Line(): + self.lines.append(obj) + case gr.Rectangle(): + self.rectangles.append(obj) + case gr.Circle(): + self.circles.append(obj) + case gr.Arc(): + self.arcs.append(obj) + case gr.Polygon(): + self.polygons.append(obj) + case gr.Curve(): + self.curves.append(obj) + case gr.Dimension(): + self.dimensions.append(obj) + case Image(): + self.images.append(obj) + case TrackSegment(): + self.track_segments.append(obj) + case TrackArc(): + self.track_arcs.append(obj) + case Via(): + self.vias.append(obj) + case Zone(): + self.zones.append(obj) + case Group(): + self.groups.append(obj) + case _: + for elem in self.map_gn_cad(obj): + self.add(elem) + + def map_gn_cad(self, obj, locked=False, net_name=None): + match obj: + case cad_pr.Trace(): + for elem in obj.to_graphic_objects(): + elem.convert_to(MM) + match elem: + case go.Arc(x1, y1, x2, y2, xc, yc, cw, ap): + yield TrackArc( + start=XYCoord(x1, y1), + mid=XYCoord(x1+xc, y1+yc), + end=XYCoord(x2, y2), + width=ap.equivalent_width(MM), + layer=gn_side_to_kicad(obj.side), + locked=locked, + net=self.net_id(net_name)) + + case go.Line(x1, y1, x2, y2, ap): + yield TrackSegment( + start=XYCoord(x1, y1), + end=XYCoord(x2, y2), + width=ap.equivalent_width(MM), + layer=gn_side_to_kicad(obj.side), + locked=locked, + net=self.net_id(net_name)) + + case cad_pr.Via(pad_stack=cad_pr.ThroughViaStack(hole, dia, unit=st_unit)): + x, y, _a, _f = obj.abs_pos() + x, y = MM(x, st_unit), MM(y, obj.unit) + yield Via( + locked=locked, + at=XYCoord(x, y), + size=MM(dia, st_unit), + drill=MM(hole, st_unit), + layers='*.Cu', + net=self.net_id(net_name)) + + case cad_pr.Text(_x, _y, text, font_size, stroke_width, h_align, v_align, layer, dark): + x, y, a, flip = obj.abs_pos() + x, y = MM(x, st_unit), MM(y, st_unit) + size = MM(size, unit) + yield gr.Text( + text, + AtPos(x, y, -math.degrees(a)), + layer=gr.TextLayer(gn_layer_to_kicad(layer, flip), not dark), + effects=TextEffect(font=FontSpec( + size=XYCoord(size, size), + thickness=stroke_width), + justify=Justify(h=Atom(h_align) if h_align != 'center' else None, + v=Atom(v_align) if v_align != 'middle' else None, + mirror=flip))) def unfill_zones(self): for zone in self.zones: @@ -306,6 +435,22 @@ class Board: def single_sided(self): raise NotImplementedError() + def net_id(self, name, create=True): + if name is None: + return None + + for i, n in self.nets.items(): + if n == name: + return i + + if create: + index = max(self.nets.keys()) + 1 + self.nets[index] = name + return index + + else: + raise IndexError(f'No such net: "{name}"') + # FIXME vvv def graphic_objects(self, text=False, images=False): return chain( @@ -363,7 +508,7 @@ class Board: @dataclass -class BoardInstance(Positioned): +class BoardInstance(cad_pr.Positioned): sexp: Board = None variables: dict = field(default_factory=lambda: {}) -- cgit