summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gerbonara/aperture_macros/primitive.py2
-rw-r--r--gerbonara/apertures.py4
-rw-r--r--gerbonara/cad/kicad/footprints.py57
-rw-r--r--gerbonara/graphic_objects.py10
-rw-r--r--gerbonara/layers.py11
-rw-r--r--gerbonara/tests/test_kicad_footprints.py27
6 files changed, 81 insertions, 30 deletions
diff --git a/gerbonara/aperture_macros/primitive.py b/gerbonara/aperture_macros/primitive.py
index 8d4bf4f..25c9bd1 100644
--- a/gerbonara/aperture_macros/primitive.py
+++ b/gerbonara/aperture_macros/primitive.py
@@ -242,7 +242,7 @@ class Outline(Primitive):
code = 4
def __init__(self, unit, args):
- if len(args) < 11:
+ if len(args) < 10:
raise ValueError(f'Invalid aperture macro outline primitive, not enough parameters ({len(args)}).')
if len(args) > 5004:
raise ValueError(f'Invalid aperture macro outline primitive, too many points ({len(args)//2-2}).')
diff --git a/gerbonara/apertures.py b/gerbonara/apertures.py
index 415eeee..feccb7f 100644
--- a/gerbonara/apertures.py
+++ b/gerbonara/apertures.py
@@ -387,14 +387,14 @@ class ObroundAperture(Aperture):
if self.w > self.h:
inst = self
else:
- inst = replace(self, w=self.h, h=self.w, **self._rotate_hole_90(), rotation=rotation+self.rotation-90)
+ inst = replace(self, w=self.h, h=self.w, **self._rotate_hole_90(), rotation=self.rotation-math.pi/2)
return ApertureMacroInstance(GenericMacros.obround,
[MM(inst.w, self.unit),
MM(inst.h, self.unit),
MM(inst.hole_dia, self.unit) or 0,
MM(inst.hole_rect_h, self.unit) or 0,
- inst.rotation])
+ inst.rotation + rotation])
def _params(self, unit=None):
return _strip_right(
diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py
index 13cad7d..4066384 100644
--- a/gerbonara/cad/kicad/footprints.py
+++ b/gerbonara/cad/kicad/footprints.py
@@ -400,14 +400,14 @@ class Pad:
return ap.CircleAperture(self.size.x+2*margin, unit=MM)
elif self.shape == Atom.rect:
- if margin:
+ if margin > 0:
return ap.ApertureMacroInstance(GenericMacros.rounded_rect,
[self.size.x+2*margin, self.size.y+2*margin,
margin,
0, 0, # no hole
rotation], unit=MM)
else:
- return ap.RectangleAperture(self.size.x, self.size.y, unit=MM).rotated(rotation)
+ return ap.RectangleAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(rotation)
elif self.shape == Atom.oval:
return ap.ObroundAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(rotation)
@@ -428,11 +428,12 @@ class Pad:
dy = dx
rotation -= math.pi/2
- if not margin:
+ if margin <= 0:
# Note: KiCad already uses MM units, so no conversion needed here.
+ alpha = math.atan(y / (dy/2))
return ap.ApertureMacroInstance(GenericMacros.isosceles_trapezoid,
- [x+dy, y,
+ [x+dy+margin*math.cos(alpha), y+margin,
dy,
0, 0, # no hole
rotation], unit=MM)
@@ -447,26 +448,54 @@ class Pad:
elif self.shape == Atom.roundrect:
x, y = self.size.x, self.size.y
r = min(x, y) * self.roundrect_rratio
- return ap.ApertureMacroInstance(GenericMacros.rounded_rect,
- [x+2*margin, y+2*margin,
- r+margin,
- 0, 0, # no hole
- rotation], unit=MM)
+ if margin > -r:
+ return ap.ApertureMacroInstance(GenericMacros.rounded_rect,
+ [x+2*margin, y+2*margin,
+ r+margin,
+ 0, 0, # no hole
+ rotation], unit=MM)
+ else:
+ return ap.RectangleAperture(x+margin, y+margin, unit=MM).rotated(rotation)
elif self.shape == Atom.custom:
primitives = []
+
# One round trip through the Gerbonara APIs, please!
for obj in self.primitives.all():
for gn_obj in obj.render():
- primitives += gn_obj._aperture_macro_primitives() # todo: precision params
+ if margin and isinstance(gn_obj, (go.Line, go.Arc)):
+ gn_obj = gn_obj.dilated(margin)
+
+ if isinstance(gn_obj, go.Region) and margin > 0:
+ for line in gn_obj.outline_objects(ap.CircleAperture(2*margin, unit=MM)):
+ primitives += line._aperture_macro_primitives()
+
+ new_primitives = list(gn_obj._aperture_macro_primitives()) # todo: precision params
+ primitives += new_primitives
+
+ # inexact, only works with convex shapes. But whatever, the only other way to do this would require
+ # an entire polygon clipping/offsetting library. Probably a bad choice to put something this complex
+ # into a file format.
+ if isinstance(gn_obj, go.Region) and margin < 0:
+ for line in gn_obj.outline_objects(ap.CircleAperture(2*margin, unit=MM)):
+ line.polarity_dark = False
+ primitives += line._aperture_macro_primitives()
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]))
+ if margin <= 0:
+ primitives.append(amp.CenterLine(MM, [1, self.size.x+2*margin, self.size.y+2*margin, 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]))
+ else: # margin > 0
+ primitives.append(amp.CenterLine(MM, [1, self.size.x+2*margin, self.size.y, 0, 0, 0]))
+ primitives.append(amp.CenterLine(MM, [1, self.size.x, self.size.y+2*margin, 0, 0, 0]))
+ primitives.append(amp.Circle(MM, [1, 2*margin, -self.size.x/2, -self.size.y/2]))
+ primitives.append(amp.Circle(MM, [1, 2*margin, -self.size.x/2, +self.size.y/2]))
+ primitives.append(amp.Circle(MM, [1, 2*margin, +self.size.x/2, -self.size.y/2]))
+ primitives.append(amp.Circle(MM, [1, 2*margin, +self.size.x/2, +self.size.y/2]))
+ elif self.options.anchor == Atom.circle and self.size.x > 0:
+ primitives.append(amp.Circle(MM, [1, self.size.x+2*margin, 0, 0, 0]))
macro = ApertureMacro(primitives=primitives).rotated(rotation)
return ap.ApertureMacroInstance(macro, unit=MM)
@@ -686,6 +715,8 @@ LAYER_MAP_K2G = {
'F.CrtYd': ('top', 'courtyard'),
'B.Fab': ('bottom', 'fabrication'),
'F.Fab': ('top', 'fabrication'),
+ 'B.Adhes': ('bottom', 'adhesive'),
+ 'F.Adhes': ('top', 'adhesive'),
'Dwgs.User': ('mechanical', 'drawings'),
'Cmts.User': ('mechanical', 'comments'),
'Edge.Cuts': ('mechanical', 'outline'),
diff --git a/gerbonara/graphic_objects.py b/gerbonara/graphic_objects.py
index b9217bb..432a988 100644
--- a/gerbonara/graphic_objects.py
+++ b/gerbonara/graphic_objects.py
@@ -295,6 +295,9 @@ class Region(GraphicObject):
def __bool__(self):
return bool(self.outline)
+ def __str__(self):
+ return f'<Region with {len(self.outline)} points and {sum(1 if c else 0 for c in self.arc_centers)} arc segments at {hex(id(self))}'
+
def _offset(self, dx, dy):
self.outline = [ (x+dx, y+dy) for x, y in self.outline ]
@@ -340,7 +343,7 @@ class Region(GraphicObject):
self.outline.append(self.outline[0])
def outline_objects(self, aperture=None):
- for p1, p2, arc in zip_longest(self.outline[:-1], self.outline[1:], self.arc_centers):
+ for p1, p2, arc in zip_longest(self.outline, self.outline[1:] + self.outline[:1], self.arc_centers):
if arc:
clockwise, pc = arc
yield Arc(*p1, *p2, *pc, clockwise, aperture=aperture, unit=self.unit, polarity_dark=self.polarity_dark)
@@ -350,7 +353,7 @@ class Region(GraphicObject):
def _aperture_macro_primitives(self, max_error=1e-2, unit=MM):
# unit is only for max_error, the resulting primitives will always be in MM
- if len(self.outline) < 3:
+ if len(self.outline) < 2:
return
points = [self.outline[0]]
@@ -361,7 +364,7 @@ class Region(GraphicObject):
#d = math.dist(p1, p2)
#err = r - math.sqrt(r**2 - (d/(2*n))**2)
#n = math.ceil(1/(2*math.sqrt(r**2 - (r - max_err)**2)/d))
- arc = Arc(*p1, *p2, *pc, clockwise, unit=self.unit, polarity_dark=self.polarity_dark)
+ arc = Arc(*p1, *p2, *pc, clockwise, unit=self.unit, polarity_dark=self.polarity_dark, aperture=None)
for line in arc.approximate(max_error=max_error, unit=unit):
points.append(line.p2)
@@ -370,6 +373,7 @@ class Region(GraphicObject):
if points[-1] != points[0]:
points.append(points[0])
+
yield amp.Outline(self.unit, [int(self.polarity_dark), len(points)-1, *(coord for p in points for coord in p)])
def to_primitives(self, unit=None):
diff --git a/gerbonara/layers.py b/gerbonara/layers.py
index 76ae12e..97e15b0 100644
--- a/gerbonara/layers.py
+++ b/gerbonara/layers.py
@@ -82,6 +82,8 @@ class NamingScheme:
'other drawings': '{board_name}-Dwgs.User.gbr',
'top fabrication': '{board_name}-F.Fab.gbr',
'bottom fabrication': '{board_name}-B.Fab.gbr',
+ 'top adhesive': '{board_name}-F.Adhes.gbr',
+ 'bottom adhesive': '{board_name}-B.Adhes.gbr',
'top courtyard': '{board_name}-F.CrtYd.gbr',
'bottom courtyard': '{board_name}-B.CrtYd.gbr',
'other netlist': '{board_name}.d356',
@@ -290,7 +292,9 @@ class LayerStack:
:py:obj:`"altium"`
"""
- def __init__(self, graphic_layers=None, drill_pth=None, drill_npth=None, drill_layers=(), netlist=None, board_name=None, original_path=None, was_zipped=False, generator=None, courtyard=False, fabrication=False):
+ def __init__(self, graphic_layers=None, drill_pth=None, drill_npth=None, drill_layers=(), netlist=None,
+ board_name=None, original_path=None, was_zipped=False, generator=None, courtyard=False,
+ fabrication=False, adhesive=False):
if not drill_layers and (graphic_layers, drill_pth, drill_npth) == (None, None, None):
graphic_layers = {tuple(layer.split()): GerberFile()
for layer in ('top paste', 'top silk', 'top mask', 'top copper',
@@ -307,6 +311,11 @@ class LayerStack:
**graphic_layers,
('bottom', 'fabrication'): GerberFile()}
+ if adhesive:
+ graphic_layers = {('top', 'adhesive'): GerberFile(),
+ **graphic_layers,
+ ('bottom', 'adhesive'): GerberFile()}
+
drill_pth = ExcellonFile()
drill_npth = ExcellonFile()
diff --git a/gerbonara/tests/test_kicad_footprints.py b/gerbonara/tests/test_kicad_footprints.py
index 3334593..e71c2e9 100644
--- a/gerbonara/tests/test_kicad_footprints.py
+++ b/gerbonara/tests/test_kicad_footprints.py
@@ -101,7 +101,6 @@ def _parse_path_d(path):
# NOTE: kicad-cli exports oddly broken svgs. One of the weirder issues is that in some paths, the "L" command is
# simply ommitted.
for match in re.finditer(r'([ML]?) ?([0-9.]+) *,? *([0-9.]+)|(A) ?([0-9.]+) *,? *([0-9.]+) *,? *([0-9.]+) *,? * ([01]) *,? *([01]) *,? *([0-9.]+) *,? *([0-9.]+)', path_d):
- print('P:', match.group(0))
ml, x, y, a, rx, ry, angle, large_arc, sweep, ax, ay = match.groups()
if ml or not a:
@@ -127,16 +126,21 @@ def _parse_path_d(path):
dx = ax - last_x
dy = ay - last_y
l = math.hypot(dx, dy)
+
# clockwise normal
nx = -dy/l
ny = dx/l
- nl = math.sqrt(rx**2 - (l/2)**2)
- if sweep != large_arc:
- cx = mx + nx*nl
- cy = my + ny*nl
+ arg = rx**2 - (l/2)**2
+ if arg < 0 or math.isclose(arg, 0, abs_tol=1e-6):
+ cx, cy = mx, my
else:
- cx = mx - nx*nl
- cy = my - ny*nl
+ nl = math.sqrt(arg)
+ if sweep != large_arc:
+ cx = mx + nx*nl
+ cy = my + ny*nl
+ else:
+ cx = mx - nx*nl
+ cy = my - ny*nl
(min_x, min_y), (max_x, max_y) = arc_bounds(last_x, last_y, ax, ay, cx-last_x, cy-last_y, clockwise=(not sweep))
min_x -= sr
@@ -178,7 +182,7 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
# actual content, and text that is slightly off from where it should be. The difference is only a few hundred
# micrometers, but it's enough to really throw off our error calculation, so we just ignore text.
fp = FootprintInstance(0, 0, sexp=Footprint.open_mod(kicad_mod_file), hide_text=True)
- stack = LayerStack(courtyard=True, fabrication=True)
+ stack = LayerStack(courtyard=True, fabrication=True, adhesive=True)
stack.add_layer('mechanical drawings')
stack.add_layer('mechanical comments')
fp.render(stack)
@@ -298,7 +302,10 @@ def test_render(kicad_mod_file, tmpfile, print_on_error):
svg_to_png(ref_svg, tmpfile('Reference render', '.png'), bg=None, dpi=600)
svg_to_png(out_svg, tmpfile('Output render', '.png'), bg=None, dpi=600)
mean, _max, hist = svg_difference(ref_svg, out_svg, dpi=600, diff_out=tmpfile('Difference', '.png'))
- assert mean < 1e-3
+
+ # compensate for circular pads aliasing badly
+ aliasing_artifacts = 1e-4 * len(fp.sexp.pads)/50
+ assert mean < 1e-3 + aliasing_artifacts
assert hist[9] < 100
- assert hist[3:].sum() < 1e-3*hist.size
+ assert hist[3:].sum() < (1e-3 + 10*aliasing_artifacts)*hist.size