From a93d118773818fbd47af4965d7b37e1b10bdf9b6 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 22 Apr 2023 17:16:20 +0200 Subject: kicad unit tests WIP --- gerbonara/cad/kicad/footprints.py | 81 ++++++++++++++++++++--------- gerbonara/cad/kicad/graphical_primitives.py | 65 ++++++++++++----------- gerbonara/cad/kicad/layer_colors.py | 70 +++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 54 deletions(-) create mode 100644 gerbonara/cad/kicad/layer_colors.py (limited to 'gerbonara/cad/kicad') diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py index 8377961..1178c79 100644 --- a/gerbonara/cad/kicad/footprints.py +++ b/gerbonara/cad/kicad/footprints.py @@ -4,12 +4,14 @@ Library for handling KiCad's footprint files (`*.kicad_mod`). import copy import enum +import string import datetime import math import time import fnmatch from itertools import chain from pathlib import Path +from dataclasses import field from .sexp import * from .base_types import * @@ -21,6 +23,7 @@ from ..primitives import Positioned from ... import graphic_primitives as gp from ... import graphic_objects as go from ... import apertures as ap +from ...newstroke import Newstroke from ...utils import MM from ...aperture_macros.parse import GenericMacros, ApertureMacro @@ -50,8 +53,11 @@ class Text: effects: TextEffect = field(default_factory=TextEffect) tstamp: Timestamp = None - def render(self): - raise NotImplementedError() + def render(self, variables={}): + if self.hide: # why + return + + yield from gr.Text.render(self, variables=variables) @sexp_type('fp_text_box') @@ -68,8 +74,8 @@ class TextBox: stroke: Stroke = field(default_factory=Stroke) render_cache: RenderCache = None - def render(self): - raise NotImplementedError() + def render(self, variables={}): + yield from gr.TextBox.render(self, variables=variables) @sexp_type('fp_line') @@ -82,7 +88,7 @@ class Line: locked: Flag() = False tstamp: Timestamp = None - def render(self): + def render(self, variables=None): dasher = Dasher(self) dasher.move(self.start.x, self.start.y) dasher.line(self.end.x, self.end.y) @@ -102,7 +108,7 @@ class Rectangle: locked: Flag() = False tstamp: Timestamp = None - def render(self): + def render(self, variables=None): x1, y1 = self.start.x, self.start.y x2, y2 = self.end.x, self.end.y x1, x2 = min(x1, x2), max(x1, x2) @@ -135,17 +141,19 @@ class Circle: locked: Flag() = False tstamp: Timestamp = None - def render(self): + def render(self, variables=None): x, y = self.center.x, self.center.y r = math.dist((x, y), (self.end.x, self.end.y)) # insane - circle = go.Arc.from_circle(x, y, r, unit=MM) + dasher = Dasher(self) + aperture = ap.CircleAperture(dasher.width or 0, unit=MM) + + circle = go.Arc.from_circle(x, y, r, aperture=aperture, unit=MM) + if self.fill == Atom.solid: yield circle.to_region() - dasher = Dasher(self) if dasher.solid: - circle.aperture = CircleAperture(dasher.width, unit=MM) yield circle else: # pain @@ -168,7 +176,7 @@ class Arc: tstamp: Timestamp = None - def render(self): + def render(self, variables=None): cx, cy = self.mid.x, self.mid.y x1, y1 = self.start.x, self.start.y x2, y2 = self.end.x, self.end.y @@ -198,7 +206,7 @@ class Polygon: locked: Flag() = False tstamp: Timestamp = None - def render(self): + def render(self, variables=None): if len(self.pts.xy) < 2: return @@ -225,7 +233,7 @@ class Curve: locked: Flag() = False tstamp: Timestamp = None - def render(self): + def render(self, variables=None): raise NotImplementedError('Bezier rendering is not yet supported. Please raise an issue and provide an example file.') @@ -265,7 +273,7 @@ class Dimension: format: DimensionFormat = field(default_factory=DimensionFormat) style: DimensionStyle = field(default_factory=DimensionStyle) - def render(self): + def render(self, variables=None): raise NotImplementedError() @@ -351,7 +359,7 @@ class Pad: options: OmitDefault(CustomPadOptions) = None primitives: OmitDefault(CustomPadPrimitives) = None - def render(self): + def render(self, variables=None): if self.type in (Atom.connect, Atom.np_thru_hole): return @@ -380,7 +388,7 @@ class Pad: [x+dx, y+dy, 2*max(dx, dy), 0, 0, # no hole - math.radians(self.at.rotation)]) + math.radians(self.at.rotation)], unit=MM) elif self.shape == Atom.roundrect: x, y = self.size.x, self.size.y @@ -389,7 +397,7 @@ class Pad: [x, y, r, 0, 0, # no hole - math.radians(self.at.rotation)]) + math.radians(self.at.rotation)], unit=MM) elif self.shape == Atom.custom: primitives = [] @@ -398,7 +406,7 @@ class Pad: for gn_obj in obj.render(): primitives += gn_obj._aperture_macro_primitives() # todo: precision params macro = ApertureMacro(primitives=primitives) - return ap.ApertureMacroInstance(macro) + return ap.ApertureMacroInstance(macro, unit=MM) def render_drill(self): if not self.drill: @@ -517,6 +525,7 @@ class Footprint: def objects(self, text=False, pads=True): return chain( (self.texts if text else []), + (self.text_boxes if text else []), self.lines, self.rectangles, self.circles, @@ -524,20 +533,19 @@ class Footprint: self.polygons, self.curves, (self.dimensions if text else []), - (self.pads if pads else []), - self.zones) + (self.pads if pads else [])) - def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, side=None): + def render(self, layer_stack, layer_map, x=0, y=0, rotation=0, text=False, side=None, variables={}): x += self.at.x y += self.at.y rotation += math.radians(self.at.rotation) flip = (side != 'top') if side else (self.layer != 'F.Cu') - for obj in self.objects(pads=False, text=False): + for obj in self.objects(pads=False, text=text): if not (layer := layer_map.get(obj.layer)): continue - for fe in obj.render(): + for fe in obj.render(variables=variables): fe.rotate(rotation) fe.offset(x, y, MM) layer_stack[layer].objects.append(fe) @@ -562,7 +570,7 @@ class Footprint: else: layer_stack.drill_pth.append(fe) -LAYER_MAP = { +LAYER_MAP_K2G = { 'F.Cu': ('top', 'copper'), 'B.Cu': ('bottom', 'copper'), 'F.SilkS': ('top', 'silk'), @@ -571,18 +579,41 @@ LAYER_MAP = { 'B.Paste': ('bottom', 'paste'), 'F.Mask': ('top', 'mask'), 'B.Mask': ('bottom', 'mask'), + 'B.CrtYd': ('bottom', 'courtyard'), + 'F.CrtYd': ('top', 'courtyard'), + 'B.Fab': ('bottom', 'fabrication'), + 'F.Fab': ('top', 'fabrication'), 'Edge.Cuts': ('mechanical', 'outline'), } +LAYER_MAP_G2K = {v: k for k, v in LAYER_MAP_K2G.items()} + @dataclass class FootprintInstance(Positioned): sexp: Footprint = None + hide_text: bool = True + reference: str = 'REF**' + value: str = None + variables: dict = field(default_factory=lambda: {}) def render(self, layer_stack): x, y, rotation = self.abs_pos x, y = MM(x, self.unit), MM(y, self.unit) - self.sexp.render(layer_stack, LAYER_MAP, x=x, y=y, rotation=rotation, side=self.side) + + variables = dict(self.variables) + + if self.reference is not None: + variables['REFERENCE'] = str(self.reference) + + if self.value is not None: + variables['VALUE'] = str(self.value) + + self.sexp.render(layer_stack, LAYER_MAP_K2G, + x=x, y=y, rotation=rotation, + side=self.side, + text=(not self.hide_text), + variables=variables) if __name__ == '__main__': import sys diff --git a/gerbonara/cad/kicad/graphical_primitives.py b/gerbonara/cad/kicad/graphical_primitives.py index 0760342..bc1bfe7 100644 --- a/gerbonara/cad/kicad/graphical_primitives.py +++ b/gerbonara/cad/kicad/graphical_primitives.py @@ -1,4 +1,5 @@ +import string import math from .sexp import * @@ -24,12 +25,14 @@ class Text: tstamp: Timestamp = None effects: TextEffect = field(default_factory=TextEffect) - def render(self): + def render(self, variables={}): if not self.effects or self.effects.hide or not self.effects.font: return font = Newstroke.load() - strokes = list(font.render(self.text, size=self.effects.font.size.y)) + line_width = self.effects.font.thickness + text = string.Template(self.text).safe_substitute(variables) + strokes = list(font.render(text, size=self.effects.font.size.y)) min_x = min(x for st in strokes for x, y in st) min_y = min(y for st in strokes for x, y in st) max_x = max(x for st in strokes for x, y in st) @@ -42,21 +45,25 @@ class Text: Atom.right: -w, Atom.left: 0 }[self.effects.justify.h if self.effects.justify else None] + offy = { - None: -h/2, - Atom.top: -h, + None: self.effects.font.size.y/2, + Atom.top: self.effects.font.size.y, Atom.bottom: 0 }[self.effects.justify.v if self.effects.justify else None] - aperture = ap.CircleAperture(self.effects.font.width or 0.2, unit=MM) + aperture = ap.CircleAperture(line_width or 0.2, unit=MM) for stroke in strokes: out = [] - for point in stroke: - x, y = rotate_point(x, y, math.radians(self.at.rotation or 0)) + + for x, y in stroke: x, y = x+offx, y+offy + x, y = rotate_point(x, y, math.radians(self.at.rotation or 0)) + x, y = x+self.at.x, y+self.at.y out.append((x, y)) + for p1, p2 in zip(out[:-1], out[1:]): - yield go.Line(*p1, *p2, aperture=ap, unit=MM) + yield go.Line(*p1, *p2, aperture=aperture, unit=MM) @sexp_type('gr_text_box') @@ -73,9 +80,13 @@ class TextBox: stroke: Stroke = field(default_factory=Stroke) render_cache: RenderCache = None - def render(self): + def render(self, variables={}): + text = string.Template(self.text).safe_substitute(variables) + if text != self.text: + raise ValueError('Rendering of vector font text with variables not yet supported') + if not render_cache or not render_cache.polygons: - raise ValueError('Text box with empty render cache') + raise ValueError('Vector font text with empty render cache') for poly in render_cache.polygons: reg = go.Region([(p.x, p.y) for p in poly.pts.xy], unit=MM) @@ -98,12 +109,12 @@ class Line: width: Named(float) = None tstamp: Timestamp = None - def render(self): + def render(self, variables=None): if self.angle: raise NotImplementedError('Angles on lines are not implemented. Please raise an issue and provide an example file.') - ap = ap.CircleAperture(self.width, unit=MM) - return go.Line(self.start.x, self.start.y, self.end.x, self.end.y, aperture=ap, unit=MM) + 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) @sexp_type('fill') @@ -128,7 +139,7 @@ class Rectangle: fill: FillMode = False tstamp: Timestamp = None - def render(self): + def render(self, variables=None): rect = go.Region.from_rectangle(self.start.x, self.start.y, self.end.x-self.start.x, self.end.y-self.start.y, unit=MM) @@ -149,12 +160,12 @@ class Circle: fill: FillMode = False tstamp: Timestamp = None - def render(self): + def render(self, variables=None): r = math.dist((self.center.x, self.center.y), (self.end.x, self.end.y)) - arc = go.Arc.from_circle(self.center.x, self.center.y, r, unit=MM) + aperture = ap.CircleAperture(self.width or 0, unit=MM) + arc = go.Arc.from_circle(self.center.x, self.center.y, r, aperture=aperture, unit=MM) if self.width: - arc.aperture = ap.CircleAperture(self.width, unit=MM) yield arc if self.fill: @@ -170,18 +181,14 @@ class Arc: width: Named(float) = None tstamp: Timestamp = None - def render(self): + def render(self, variables=None): + if not self.width: + return + cx, cy = self.mid.x, self.mid.y x1, y1 = self.start.x, self.start.y x2, y2 = self.end.x, self.end.y - arc = go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, unit=MM) - - if self.width: - arc.aperture = ap.CircleAperture(self.width, unit=MM) - yield arc - - if self.fill: - yield arc.to_region() + yield go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, aperture=ap.CircleAperture(self.width or 0, unit=MM), clockwise=True, unit=MM) @sexp_type('gr_poly') @@ -192,7 +199,7 @@ class Polygon: fill: FillMode = True tstamp: Timestamp = None - def render(self): + def render(self, variables=None): reg = go.Region([(pt.x, pt.y) for pt in self.pts.xy], unit=MM) if self.width and self.width >= 0.005: @@ -209,7 +216,7 @@ class Curve: width: Named(float) = None tstamp: Timestamp = None - def render(self): + def render(self, variables=None): raise NotImplementedError('Bezier rendering is not yet supported. Please raise an issue and provide an example file.') @@ -218,6 +225,6 @@ class AnnotationBBox: start: Rename(XYCoord) = None end: Rename(XYCoord) = None - def render(self): + def render(self, variables=None): return [] diff --git a/gerbonara/cad/kicad/layer_colors.py b/gerbonara/cad/kicad/layer_colors.py new file mode 100644 index 0000000..b4caa2d --- /dev/null +++ b/gerbonara/cad/kicad/layer_colors.py @@ -0,0 +1,70 @@ + +# Maps KiCad layer IDs to (r, g, b, a) color tuples. R, G, B are ints in [0...255], a is a float in [0...1] +KICAD_LAYER_COLORS = { + 'F.Cu': (200, 52, 52, 1), + 'In1.Cu': (127, 200, 127, 1), + 'In2.Cu': (206, 125, 44, 1), + 'In3.Cu': (79, 203, 203, 1), + 'In4.Cu': (219, 98, 139, 1), + 'In5.Cu': (167, 165, 198, 1), + 'In6.Cu': (40, 204, 217, 1), + 'In7.Cu': (232, 178, 167, 1), + 'In8.Cu': (242, 237, 161, 1), + 'In9.Cu': (141, 203, 129, 1), + 'In10.Cu': (237, 124, 51, 1), + 'In11.Cu': (91, 195, 235, 1), + 'In12.Cu': (247, 111, 142, 1), + 'In13.Cu': (167, 165, 198, 1), + 'In14.Cu': (40, 204, 217, 1), + 'In15.Cu': (232, 178, 167, 1), + 'In16.Cu': (242, 237, 161, 1), + 'In17.Cu': (237, 124, 51, 1), + 'In18.Cu': (91, 195, 235, 1), + 'In19.Cu': (247, 111, 142, 1), + 'In20.Cu': (167, 165, 198, 1), + 'In21.Cu': (40, 204, 217, 1), + 'In22.Cu': (232, 178, 167, 1), + 'In23.Cu': (242, 237, 161, 1), + 'In24.Cu': (237, 124, 51, 1), + 'In25.Cu': (91, 195, 235, 1), + 'In26.Cu': (247, 111, 142, 1), + 'In27.Cu': (167, 165, 198, 1), + 'In28.Cu': (40, 204, 217, 1), + 'In29.Cu': (232, 178, 167, 1), + 'In30.Cu': (242, 237, 161, 1), + 'B.Cu': (77, 127, 196, 1), + 'B.Adhes': (0, 0, 132, 1), + 'F.Adhes': (132, 0, 132, 1), + 'B.Paste': (0, 194, 194, 0.9), + 'F.Paste': (180, 160, 154, 0.9), + 'B.SilkS': (232, 178, 167, 1), + 'F.SilkS': (242, 237, 161, 1), + 'B.Mask': (2, 255, 238, 0.4), + 'F.Mask': (216, 100, 255, 0.4), + 'Dwgs.User': (194, 194, 194, 1), + 'Cmts.User': (89, 148, 220, 1), + 'Eco1.User': (180, 219, 210, 1), + 'Eco2.User': (216, 200, 82, 1), + 'Edge.Cuts': (208, 210, 205, 1), + 'Margin': (255, 38, 226, 1), + 'B.CrtYd': (38, 233, 255, 1), + 'F.CrtYd': (255, 38, 226, 1), + 'B.Fab': (88, 93, 132, 1), + 'F.Fab': (175, 175, 175, 1), + 'User.1': (194, 194, 194, 1), + 'User.2': (89, 148, 220, 1), + 'User.3': (180, 219, 210, 1), + 'User.4': (216, 200, 82, 1), + 'User.5': (194, 194, 194, 1), + 'User.6': (89, 148, 220, 1), + 'User.7': (180, 219, 210, 1), + 'User.8': (216, 200, 82, 1), + 'User.9': (232, 178, 167, 1), +} + +KICAD_DRILL_COLORS = { + ('drill', 'pth'): (194, 194, 0, 1), + ('drill', 'npth'): (26, 196, 210, 1), + ('drill', 'via'): (227, 183, 46, 1), +} + -- cgit