summaryrefslogtreecommitdiff
path: root/gerbonara/cad
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-04-22 17:16:20 +0200
committerjaseg <git@jaseg.de>2023-04-22 17:16:20 +0200
commita93d118773818fbd47af4965d7b37e1b10bdf9b6 (patch)
tree528a06ac8154dfa70caa8714e7aa5ea62a51dd9f /gerbonara/cad
parent5ce88e4d1b06dcc846c94ec614fb00f64e85c125 (diff)
downloadgerbonara-a93d118773818fbd47af4965d7b37e1b10bdf9b6.tar.gz
gerbonara-a93d118773818fbd47af4965d7b37e1b10bdf9b6.tar.bz2
gerbonara-a93d118773818fbd47af4965d7b37e1b10bdf9b6.zip
kicad unit tests WIP
Diffstat (limited to 'gerbonara/cad')
-rw-r--r--gerbonara/cad/kicad/footprints.py81
-rw-r--r--gerbonara/cad/kicad/graphical_primitives.py65
-rw-r--r--gerbonara/cad/kicad/layer_colors.py70
3 files changed, 162 insertions, 54 deletions
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),
+}
+