summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerbonara/aperture_macros/parse.py2
-rw-r--r--gerbonara/aperture_macros/primitive.py12
-rw-r--r--gerbonara/apertures.py4
-rw-r--r--gerbonara/cad/kicad/footprints.py43
-rw-r--r--gerbonara/tests/test_kicad_footprints.py10
5 files changed, 49 insertions, 22 deletions
diff --git a/gerbonara/aperture_macros/parse.py b/gerbonara/aperture_macros/parse.py
index c1a776f..57de857 100644
--- a/gerbonara/aperture_macros/parse.py
+++ b/gerbonara/aperture_macros/parse.py
@@ -13,6 +13,7 @@ from . import primitive as ap
from .expression import *
from ..utils import MM
+# we make our own here instead of using math.degrees to make sure this works with expressions, too.
def rad_to_deg(x):
return (x / math.pi) * 180
@@ -190,6 +191,7 @@ class GenericMacros:
*_generic_hole(4)])
# w must be larger than h
+ # params: width, height, *hole, rotation
obround = ApertureMacro('GNO', [
ap.CenterLine('mm', [1, var(1), var(2), 0, 0, var(5) * -deg_per_rad]),
ap.Circle('mm', [1, var(2), +var(1)/2, 0, var(5) * -deg_per_rad]),
diff --git a/gerbonara/aperture_macros/primitive.py b/gerbonara/aperture_macros/primitive.py
index 4a991f1..8d4bf4f 100644
--- a/gerbonara/aperture_macros/primitive.py
+++ b/gerbonara/aperture_macros/primitive.py
@@ -12,6 +12,7 @@ from .expression import Expression, UnitExpression, ConstantExpression, expr
from .. import graphic_primitives as gp
from .. import graphic_objects as go
+from ..utils import rotate_point
def point_distance(a, b):
@@ -20,9 +21,11 @@ def point_distance(a, b):
return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
+# we make our own here instead of using math.degrees to make sure this works with expressions, too.
def deg_to_rad(a):
return a * (math.pi / 180)
+
def rad_to_deg(a):
return a * (180 / math.pi)
@@ -92,7 +95,7 @@ class Circle(Primitive):
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
with self.Calculator(self, variable_binding, unit) as calc:
- x, y = gp.rotate_point(calc.x, calc.y, deg_to_rad(calc.rotation) + rotation, 0, 0)
+ x, y = rotate_point(calc.x, calc.y, deg_to_rad(calc.rotation) + rotation, 0, 0)
x, y = x+offset[0], y+offset[1]
return [ gp.Circle(x, y, calc.diameter/2, polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
@@ -123,6 +126,7 @@ class VectorLine(Primitive):
delta_y = calc.end_y - calc.start_y
length = point_distance((calc.start_x, calc.start_y), (calc.end_x, calc.end_y))
+ center_x, center_y = rotate_point(center_x, center_y, deg_to_rad(calc.rotation) + rotation, 0, 0)
center_x, center_y = center_x+offset[0], center_y+offset[1]
rotation += deg_to_rad(calc.rotation) + math.atan2(delta_y, delta_x)
@@ -181,7 +185,7 @@ class Polygon(Primitive):
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
with self.Calculator(self, variable_binding, unit) as calc:
rotation += deg_to_rad(calc.rotation)
- x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
+ x, y = rotate_point(calc.x, calc.y, rotation, 0, 0)
x, y = x+offset[0], y+offset[1]
return [ gp.ArcPoly.from_regular_polygon(calc.x, calc.y, calc.diameter/2, calc.n_vertices, rotation,
polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
@@ -209,7 +213,7 @@ class Thermal(Primitive):
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
with self.Calculator(self, variable_binding, unit) as calc:
rotation += deg_to_rad(calc.rotation)
- x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
+ x, y = rotate_point(calc.x, calc.y, rotation, 0, 0)
x, y = x+offset[0], y+offset[1]
dark = (bool(calc.exposure) == polarity_dark)
@@ -271,7 +275,7 @@ class Outline(Primitive):
def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
with self.Calculator(self, variable_binding, unit) as calc:
rotation += deg_to_rad(calc.rotation)
- bound_coords = [ gp.rotate_point(calc(x), calc(y), rotation, 0, 0) for x, y in self.coords ]
+ bound_coords = [ rotate_point(calc(x), calc(y), rotation, 0, 0) for x, y in self.coords ]
bound_coords = [ (x+offset[0], y+offset[1]) for x, y in bound_coords ]
bound_radii = [None] * len(bound_coords)
return [gp.ArcPoly(bound_coords, bound_radii, polarity_dark=(bool(calc.exposure) == polarity_dark))]
diff --git a/gerbonara/apertures.py b/gerbonara/apertures.py
index d5670a2..415eeee 100644
--- a/gerbonara/apertures.py
+++ b/gerbonara/apertures.py
@@ -392,8 +392,8 @@ class ObroundAperture(Aperture):
return ApertureMacroInstance(GenericMacros.obround,
[MM(inst.w, self.unit),
MM(inst.h, self.unit),
- MM(inst.hole_dia, self.unit),
- MM(inst.hole_rect_h, self.unit),
+ MM(inst.hole_dia, self.unit) or 0,
+ MM(inst.hole_rect_h, self.unit) or 0,
inst.rotation])
def _params(self, unit=None):
diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py
index 4581c3b..6cd34e1 100644
--- a/gerbonara/cad/kicad/footprints.py
+++ b/gerbonara/cad/kicad/footprints.py
@@ -24,8 +24,9 @@ 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 ...utils import MM, rotate_point
from ...aperture_macros.parse import GenericMacros, ApertureMacro
+from ...aperture_macros import primitive as amp
@sexp_type('property')
@@ -113,10 +114,10 @@ class Rectangle:
x2, y2 = self.end.x, self.end.y
x1, x2 = min(x1, x2), max(x1, x2)
y1, y2 = min(y1, y2), max(y1, y2)
- w, h = x2-x1, y1-y2
+ w, h = x2-x1, y2-y1
if self.fill == Atom.solid:
- yield go.Region.from_rectangle(x1, y1, w, y, unit=MM)
+ yield go.Region.from_rectangle(x1, y1, w, h, unit=MM)
dasher = Dasher(self)
dasher.move(x1, y1)
@@ -364,21 +365,27 @@ class Pad:
options: OmitDefault(CustomPadOptions) = None
primitives: OmitDefault(CustomPadPrimitives) = None
- def render(self, variables=None):
+ def render(self, variables=None, margin=None):
#if self.type in (Atom.connect, Atom.np_thru_hole):
# return
+ if self.drill and self.drill.offset:
+ ox, oy = rotate_point(self.drill.offset.x, self.drill.offset.y, math.radians(self.at.rotation))
+ else:
+ ox, oy = 0, 0
- yield go.Flash(self.at.x, self.at.y, self.aperture(), unit=MM)
+ yield go.Flash(self.at.x+ox, self.at.y+oy, self.aperture(margin), unit=MM)
+
+ def aperture(self, margin=None):
+ rotation = -math.radians(self.at.rotation)
- def aperture(self):
if self.shape == Atom.circle:
return ap.CircleAperture(self.size.x, unit=MM)
elif self.shape == Atom.rect:
- return ap.RectangleAperture(self.size.x, self.size.y, unit=MM).rotated(self.at.rotation)
+ return ap.RectangleAperture(self.size.x, self.size.y, unit=MM).rotated(rotation)
elif self.shape == Atom.oval:
- return ap.ObroundAperture(self.size.x, self.size.y, unit=MM).rotated(self.at.rotation)
+ return ap.ObroundAperture(self.size.x, self.size.y, unit=MM).rotated(rotation)
elif self.shape == Atom.trapezoid:
# KiCad's trapezoid aperture "rect_delta" param is just weird to the point that I think it's probably
@@ -386,14 +393,17 @@ class Pad:
# original bounding box, and the trapezoid's base and tip length are 3mm and 1mm.
x, y = self.size.x, self.size.y
- dx, dy = self.rect_delta.x, self.rect_delta.y
+ if self.rect_delta:
+ dx, dy = self.rect_delta.x, self.rect_delta.y
+ else: # RF_Antenna/Pulse_W3011 has trapezoid pads w/o rect_delta, which KiCad renders as plain rects.
+ dx, dy = 0, 0
# Note: KiCad already uses MM units, so no conversion needed here.
return ap.ApertureMacroInstance(GenericMacros.isosceles_trapezoid,
[x+dx, y+dy,
2*max(dx, dy),
0, 0, # no hole
- math.radians(self.at.rotation)], unit=MM)
+ rotation], unit=MM)
elif self.shape == Atom.roundrect:
x, y = self.size.x, self.size.y
@@ -402,7 +412,7 @@ class Pad:
[x, y,
r,
0, 0, # no hole
- math.radians(self.at.rotation)], unit=MM)
+ rotation], unit=MM)
elif self.shape == Atom.custom:
primitives = []
@@ -410,7 +420,16 @@ class Pad:
for obj in self.primitives.all():
for gn_obj in obj.render():
primitives += gn_obj._aperture_macro_primitives() # todo: precision params
- macro = ApertureMacro(primitives=primitives).rotated(self.at.rotation)
+
+ if self.options:
+ if self.options.anchor == Atom.rect and self.size.x > 0 and self.size.y > 0:
+ primitives.append(amp.CenterLine(MM, [1, self.size.x, self.size.y, 0, 0, 0]))
+
+ elif self.options.anchor == Atom.circle and self.size.x > 0:
+ primitives.append(amp.Circle(MM, [1, self.size.x, 0, 0, 0]))
+
+
+ macro = ApertureMacro(primitives=primitives).rotated(rotation)
return ap.ApertureMacroInstance(macro, unit=MM)
def render_drill(self):
diff --git a/gerbonara/tests/test_kicad_footprints.py b/gerbonara/tests/test_kicad_footprints.py
index 0d7c11b..df483fa 100644
--- a/gerbonara/tests/test_kicad_footprints.py
+++ b/gerbonara/tests/test_kicad_footprints.py
@@ -68,6 +68,8 @@ def test_round_trip(kicad_mod_file):
assert original == stage1
+# Regrettably, we have to re-implement a significant part of the SVG spec to fix up the SVGs that kicad-cli produces.
+
def _compute_style(elem):
current_style = {}
for elem in [*reversed(list(elem.parents)), elem]:
@@ -187,14 +189,14 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
layer = stack[('top', 'courtyard')]
bounds = []
- print('===== BOUNDS =====')
+ #print('===== BOUNDS =====')
for obj in layer.objects:
if isinstance(obj, (go.Line, go.Arc)):
bbox = (min_x, min_y), (max_x, max_y) = obj.bounding_box(unit=MM)
- import textwrap
- print(f'{min_x: 3.6f} {min_y: 3.6f} {max_x: 3.6f} {max_y: 3.6f}', '\n'.join(textwrap.wrap(str(obj), width=80, subsequent_indent=' '*(3+4*(3+1+6)))))
+ #import textwrap
+ #print(f'{min_x: 3.6f} {min_y: 3.6f} {max_x: 3.6f} {max_y: 3.6f}', '\n'.join(textwrap.wrap(str(obj), width=80, subsequent_indent=' '*(3+4*(3+1+6)))))
bounds.append(bbox)
- print('===== END =====')
+ #print('===== END =====')
if not bounds:
print('Footprint has no paths on courtyard layer')