diff options
-rw-r--r-- | gerbonara/aperture_macros/parse.py | 2 | ||||
-rw-r--r-- | gerbonara/aperture_macros/primitive.py | 12 | ||||
-rw-r--r-- | gerbonara/apertures.py | 4 | ||||
-rw-r--r-- | gerbonara/cad/kicad/footprints.py | 43 | ||||
-rw-r--r-- | gerbonara/tests/test_kicad_footprints.py | 10 |
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') |