From 1dbe7f1f7365ddd97d1ebbfc9e7298667bcdca44 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 25 Apr 2023 00:02:24 +0200 Subject: Fix more tests --- gerbonara/aperture_macros/parse.py | 31 ++++++++++ gerbonara/cad/kicad/footprints.py | 97 ++++++++++++++++++++++++++------ gerbonara/tests/test_kicad_footprints.py | 18 ++++-- 3 files changed, 124 insertions(+), 22 deletions(-) diff --git a/gerbonara/aperture_macros/parse.py b/gerbonara/aperture_macros/parse.py index 57de857..16c6585 100644 --- a/gerbonara/aperture_macros/parse.py +++ b/gerbonara/aperture_macros/parse.py @@ -190,6 +190,37 @@ class GenericMacros: var(6) * -deg_per_rad]), *_generic_hole(4)]) + # params: width, height, length difference between narrow side (top) and wide side (bottom), margin, *hole, rotation + rounded_isosceles_trapezoid = ApertureMacro('GRTR', [ + ap.Outline('mm', [1, 4, + var(1)/-2, var(2)/-2, + var(1)/-2+var(3)/2, var(2)/2, + var(1)/2-var(3)/2, var(2)/2, + var(1)/2, var(2)/-2, + var(1)/-2, var(2)/-2, + var(6) * -deg_per_rad]), + ap.VectorLine('mm', [1, var(4)*2, + var(1)/-2, var(2)/-2, + var(1)/-2+var(3)/2, var(2)/2,]), + ap.VectorLine('mm', [1, var(4)*2, + var(1)/-2+var(3)/2, var(2)/2, + var(1)/2-var(3)/2, var(2)/2,]), + ap.VectorLine('mm', [1, var(4)*2, + var(1)/2-var(3)/2, var(2)/2, + var(1)/2, var(2)/-2,]), + ap.VectorLine('mm', [1, var(4)*2, + var(1)/2, var(2)/-2, + var(1)/-2, var(2)/-2,]), + ap.Circle('mm', [1, var(4)*2, + var(1)/-2, var(2)/-2,]), + ap.Circle('mm', [1, var(4)*2, + var(1)/-2+var(3)/2, var(2)/2,]), + ap.Circle('mm', [1, var(4)*2, + var(1)/2-var(3)/2, var(2)/2,]), + ap.Circle('mm', [1, var(4)*2, + var(1)/2, var(2)/-2,]), + *_generic_hole(5)]) + # w must be larger than h # params: width, height, *hole, rotation obround = ApertureMacro('GNO', [ diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py index 6cd34e1..00fa5f5 100644 --- a/gerbonara/cad/kicad/footprints.py +++ b/gerbonara/cad/kicad/footprints.py @@ -182,14 +182,31 @@ class Arc: x1, y1 = self.start.x, self.start.y x2, y2 = self.end.x, self.end.y dasher = Dasher(self) + aperture = ap.CircleAperture(dasher.width, unit=MM) + + if math.isclose(x1, x2, abs_tol=1e-6) and math.isclose(y1, y2, abs_tol=1e-6): + cx = (x1 + mx) / 2 + cy = (y1 + my) / 2 + arc = go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, clockwise=True, aperture=aperture, unit=MM) + if dasher.solid: + yield arc - # https://stackoverflow.com/questions/56224824/how-do-i-find-the-circumcenter-of-the-triangle-using-python-without-external-lib - d = 2 * (x1 * (y2 - my) + x2 * (my - y1) + mx * (y1 - y2)) - cx = ((x1 * x1 + y1 * y1) * (y2 - my) + (x2 * x2 + y2 * y2) * (my - y1) + (mx * mx + my * my) * (y1 - y2)) / d - cy = ((x1 * x1 + y1 * y1) * (mx - x2) + (x2 * x2 + y2 * y2) * (x1 - mx) + (mx * mx + my * my) * (x2 - x1)) / d + else: + # use approximation from graphic object arc class + for line in arc.approximate(): + dasher.segments.append((line.x1, line.y1, line.x2, line.y2)) + + for line in dasher: + yield go.Line(x1, y1, x2, y2, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM) + + else: + # https://stackoverflow.com/questions/56224824/how-do-i-find-the-circumcenter-of-the-triangle-using-python-without-external-lib + d = 2 * (x1 * (y2 - my) + x2 * (my - y1) + mx * (y1 - y2)) + cx = ((x1 * x1 + y1 * y1) * (y2 - my) + (x2 * x2 + y2 * y2) * (my - y1) + (mx * mx + my * my) * (y1 - y2)) / d + cy = ((x1 * x1 + y1 * y1) * (mx - x2) + (x2 * x2 + y2 * y2) * (x1 - mx) + (mx * mx + my * my) * (x2 - x1)) / d # KiCad only has clockwise arcs. - arc = go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, clockwise=False, aperture=ap.CircleAperture(dasher.width, unit=MM), unit=MM) + arc = go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, clockwise=False, aperture=aperture, unit=MM) if dasher.solid: yield arc @@ -377,15 +394,23 @@ class Pad: def aperture(self, margin=None): rotation = -math.radians(self.at.rotation) + margin = margin or 0 if self.shape == Atom.circle: - return ap.CircleAperture(self.size.x, unit=MM) + return ap.CircleAperture(self.size.x+2*margin, unit=MM) elif self.shape == Atom.rect: - return ap.RectangleAperture(self.size.x, self.size.y, unit=MM).rotated(rotation) + if margin: + return ap.ApertureMacroInstance(GenericMacros.rounded_rect, + [x+2*margin, 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) elif self.shape == Atom.oval: - return ap.ObroundAperture(self.size.x, self.size.y, unit=MM).rotated(rotation) + return ap.ObroundAperture(self.size.x+2*margin, self.size.y+2*margin, 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 @@ -398,19 +423,33 @@ class Pad: 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 - rotation], unit=MM) + if dx != 0: + x, y = y, x + dy = dx + rotation -= math.pi/2 + + if not margin: + # Note: KiCad already uses MM units, so no conversion needed here. + + return ap.ApertureMacroInstance(GenericMacros.isosceles_trapezoid, + [x+dy, y, + dy, + 0, 0, # no hole + rotation], unit=MM) + + else: + return ap.ApertureMacroInstance(GenericMacros.rounded_isosceles_trapezoid, + [x+dy, y, + dy, margin, + 0, 0, # no hole + rotation], unit=MM) 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, y, - r, + [x+2*margin, y+2*margin, + r+margin, 0, 0, # no hole rotation], unit=MM) @@ -591,9 +630,33 @@ class Footprint: layer_stack[layer].objects.append(fe) for obj in self.pads: + if obj.solder_mask_margin is not None: + solder_mask_margin = obj.solder_mask_margin + elif self.solder_mask_margin is not None: + solder_mask_margin = self.solder_mask_margin + else: + solder_mask_margin = None + + if obj.solder_paste_margin is not None: + solder_paste_margin = obj.solder_paste_margin + elif obj.solder_paste_margin_ratio is not None: + solder_paste_margin = max(obj.size.x, obj.size.y) * obj.solder_paste_margin_ratio + elif self.solder_paste_margin is not None: + solder_paste_margin = self.solder_paste_margin + else: + solder_paste_margin = None + for glob in obj.layers or []: for layer in fnmatch.filter(layer_map, glob): - for fe in obj.render(): + + if layer.endswith('.Mask'): + margin = solder_mask_margin + elif layer.endswith('.Paste'): + margin = solder_paste_margin + else: + margin = None + + for fe in obj.render(margin=margin): fe.rotate(rotation) fe.offset(x, y, MM) if isinstance(fe, go.Flash) and fe.aperture: diff --git a/gerbonara/tests/test_kicad_footprints.py b/gerbonara/tests/test_kicad_footprints.py index df483fa..3334593 100644 --- a/gerbonara/tests/test_kicad_footprints.py +++ b/gerbonara/tests/test_kicad_footprints.py @@ -98,10 +98,13 @@ def _parse_path_d(path): raise ValueError('Path contains cubic beziers') last_x, last_y = None, None - 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): + # 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: + if ml or not a: x, y = float(x), float(y) last_x, last_y = x, y yield x-sr, y-sr @@ -109,7 +112,7 @@ def _parse_path_d(path): yield x+sr, y-sr yield x+sr, y+sr - elif a: + else: # a rx, ry = float(rx), float(ry) ax, ay = float(ax), float(ay) angle = float(angle) @@ -247,8 +250,13 @@ def test_render(kicad_mod_file, tmpfile, print_on_error): root_h = root['height'] = f'{h:.6f}mm' root['viewBox'] = f'{min_x:.6f} {min_y:.6f} {w:.6f} {h:.6f}' - for group in soup.find_all('g', attrs={'class': 'stroked-text'}): - group.decompose() + # nuke text since kicad-cli's text positioning looks sligthly wonky and we failed to replicate that wonkyness + # exactly. + for elem in soup.find_all('g', attrs={'class': 'stroked-text'}): + elem.decompose() + + for elem in soup.find_all('text'): + elem.decompose() # Currently, there is a bug in resvg leading to mis-rendering. On the file below from the KiCad standard lib, resvg # renders all round pads in a wrong color (?). Interestingly, passing the file through usvg before rendering fixes -- cgit