From d7efa577320cbb1af3d4399b008b5901309b5ea4 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 19 Jul 2024 19:15:52 +0200 Subject: kicad: Add bounding box support to lots of s-expr objects --- gerbonara/cad/kicad/base_types.py | 13 ++++++++++++- gerbonara/cad/kicad/footprints.py | 3 +-- gerbonara/cad/kicad/graphical_primitives.py | 30 +++++++++++++++++------------ gerbonara/cad/kicad/pcb.py | 16 +++------------ gerbonara/cad/kicad/primitives.py | 20 ++++++++++++++++++- 5 files changed, 53 insertions(+), 29 deletions(-) (limited to 'gerbonara/cad/kicad') diff --git a/gerbonara/cad/kicad/base_types.py b/gerbonara/cad/kicad/base_types.py index 69042fc..6e9fe72 100644 --- a/gerbonara/cad/kicad/base_types.py +++ b/gerbonara/cad/kicad/base_types.py @@ -9,7 +9,8 @@ from itertools import cycle from .sexp import * from .sexp_mapper import * from ...newstroke import Newstroke -from ...utils import rotate_point, Tag, MM +from ...utils import rotate_point, sum_bounds, Tag, MM +from ...layers import LayerStack from ... import apertures as ap from ... import graphic_objects as go @@ -37,6 +38,16 @@ LAYER_MAP_K2G = { LAYER_MAP_G2K = {v: k for k, v in LAYER_MAP_K2G.items()} +class BBoxMixin: + def bounding_box(self, unit=MM): + if not hasattr(self, '_bounding_box'): + (min_x, min_y), (max_x, max_y) = sum_bounds(fe.bounding_box(unit) for fe in self.render()) + # Convert back from gerbonara's coordinates to kicad coordinates. + self._bounding_box = (min_x, -max_y), (max_x, -min_y) + + return self._bounding_box + + @sexp_type('uuid') class UUID: value: str = field(default_factory=uuid.uuid4) diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py index 25e2329..ce9e86c 100644 --- a/gerbonara/cad/kicad/footprints.py +++ b/gerbonara/cad/kicad/footprints.py @@ -662,7 +662,6 @@ class Footprint: models: List(Model) = field(default_factory=list) _ : SEXP_END = None original_filename: str = None - _bounding_box: tuple = None board: object = None def __after_parse__(self, parent): @@ -975,7 +974,7 @@ class Footprint: layer_stack.drill_pth.append(fe) def bounding_box(self, unit=MM): - if not self._bounding_box: + if not hasattr(self, '_bounding_box'): stack = LayerStack() layer_map = {kc_id: gn_id for kc_id, gn_id in LAYER_MAP_K2G.items() if gn_id in stack} self.render(stack, layer_map, x=0, y=0, rotation=0, flip=False, text=False, variables={}) diff --git a/gerbonara/cad/kicad/graphical_primitives.py b/gerbonara/cad/kicad/graphical_primitives.py index a4f0f51..e068d23 100644 --- a/gerbonara/cad/kicad/graphical_primitives.py +++ b/gerbonara/cad/kicad/graphical_primitives.py @@ -9,7 +9,7 @@ from .primitives import * from ... import graphic_objects as go from ... import apertures as ap from ...newstroke import Newstroke -from ...utils import rotate_point, MM +from ...utils import rotate_point, MM, arc_bounds @sexp_type('layer') class TextLayer: @@ -18,7 +18,7 @@ class TextLayer: @sexp_type('gr_text') -class Text(TextMixin): +class Text(TextMixin, BBoxMixin): text: str = '' at: AtPos = field(default_factory=AtPos) layer: TextLayer = field(default_factory=TextLayer) @@ -32,7 +32,7 @@ class Text(TextMixin): @sexp_type('gr_text_box') -class TextBox: +class TextBox(BBoxMixin): locked: Flag() = False text: str = '' start: Named(XYCoord) = None @@ -101,6 +101,12 @@ class Line: self.start = self.start.with_offset(x, y) self.end = self.end.with_offset(x, y) + def bounding_box(self, unit=MM): + x_min, x_max = min(self.start.x, self.end.x), max(self.start.x, self.end.x) + y_min, y_max = min(self.start.y, self.end.y), max(self.start.y, self.end.y) + w = self.stroke.width if self.stroke else self.width + return (x_min-w, y_max-w), (x_max+w, y_max+w) + @sexp_type('fill') class FillMode: @@ -116,7 +122,7 @@ class FillMode: yield [Atom.fill, Atom.solid if value else Atom.none] @sexp_type('gr_rect') -class Rectangle: +class Rectangle(BBoxMixin): start: Rename(XYCoord) = None end: Rename(XYCoord) = None layer: Named(str) = None @@ -149,7 +155,7 @@ class Rectangle: @sexp_type('gr_circle') -class Circle: +class Circle(BBoxMixin): center: Rename(XYCoord) = None end: Rename(XYCoord) = None layer: Named(str) = None @@ -177,7 +183,7 @@ class Circle: @sexp_type('gr_arc') -class Arc: +class Arc(BBoxMixin): start: Rename(XYCoord) = None mid: Rename(XYCoord) = None end: Rename(XYCoord) = None @@ -211,8 +217,8 @@ class Arc: aperture = ap.CircleAperture(self.width, unit=MM) x1, y1 = self.start.x, self.start.y x2, y2 = self.end.x, self.end.y - (cx, cy), _r = kicad_mid_to_center_arc(self.mid, self.start, self.end) - yield go.Arc(x1, -y1, x2, -y2, cx-x1, -(cy-y1), aperture=aperture, clockwise=False, unit=MM) + (cx, cy), _r, clockwise = kicad_mid_to_center_arc(self.mid, self.start, self.end) + yield go.Arc(x1, -y1, x2, -y2, cx-x1, -(cy-y1), aperture=aperture, clockwise=not clockwise, unit=MM) def offset(self, x=0, y=0): self.start = self.start.with_offset(x, y) @@ -221,7 +227,7 @@ class Arc: @sexp_type('gr_poly') -class Polygon: +class Polygon(BBoxMixin): pts: ArcPointList = field(default_factory=list) layer: Named(str) = None width: Named(float) = None @@ -243,8 +249,8 @@ class Polygon: else: # base_types.Arc points.append((point_or_arc.start.x, -point_or_arc.start.y)) points.append((point_or_arc.end.x, -point_or_arc.end.y)) - (cx, cy), _r = kicad_mid_to_center_arc(point_or_arc.mid, point_or_arc.start, point_or_arc.end) - centers.append((False, (cx, -cy))) + (cx, cy), _r, clockwise = kicad_mid_to_center_arc(point_or_arc.mid, point_or_arc.start, point_or_arc.end) + centers.append((not clockwise, (cx, -cy))) reg = go.Region(points, centers, unit=MM) reg.close() @@ -261,7 +267,7 @@ class Polygon: @sexp_type('gr_curve') -class Curve: +class Curve(BBoxMixin): pts: PointList = field(default_factory=PointList) layer: Named(str) = None width: Named(float) = None diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py index ef86a71..11abf3c 100644 --- a/gerbonara/cad/kicad/pcb.py +++ b/gerbonara/cad/kicad/pcb.py @@ -157,7 +157,7 @@ class Net: @sexp_type('segment') -class TrackSegment: +class TrackSegment(BBoxMixin): start: Rename(XYCoord) = field(default_factory=XYCoord) end: Rename(XYCoord) = field(default_factory=XYCoord) width: Named(float) = 0.5 @@ -200,7 +200,7 @@ class TrackSegment: @sexp_type('arc') -class TrackArc: +class TrackArc(BBoxMixin): start: Rename(XYCoord) = field(default_factory=XYCoord) mid: Rename(XYCoord) = field(default_factory=XYCoord) end: Rename(XYCoord) = field(default_factory=XYCoord) @@ -245,7 +245,7 @@ class TrackArc: @sexp_type('via') -class Via: +class Via(BBoxMixin): via_type: AtomChoice(Atom.blind, Atom.micro) = None locked: Flag() = False at: Rename(XYCoord) = field(default_factory=XYCoord) @@ -345,7 +345,6 @@ class Board: _ : SEXP_END = None original_filename: str = None - _bounding_box: tuple = None _trace_index: rtree.index.Index = None _trace_index_map: dict = None @@ -789,15 +788,6 @@ class Board: fe.offset(x, -y, MM) layer_stack.drill_pth.append(fe) - def bounding_box(self, unit=MM): - if not self._bounding_box: - stack = LayerStack() - layer_map = {kc_id: gn_id for kc_id, gn_id in LAYER_MAP_K2G.items() if gn_id in stack} - self.render(stack, layer_map, x=0, y=0, rotation=0, flip=False, text=False, variables={}) - self._bounding_box = stack.bounding_box(unit) - return self._bounding_box - - @dataclass class BoardInstance(cad_pr.Positioned): sexp: Board = None diff --git a/gerbonara/cad/kicad/primitives.py b/gerbonara/cad/kicad/primitives.py index 65566d0..5be0422 100644 --- a/gerbonara/cad/kicad/primitives.py +++ b/gerbonara/cad/kicad/primitives.py @@ -62,6 +62,8 @@ def center_arc_to_kicad_mid(center, start, end): def kicad_mid_to_center_arc(mid, start, end): """ Convert kicad's slightly insane midpoint notation to standrad center/p1/p2 notation. + returns a ((center_x, center_y), radius, clockwise) tuple in KiCad coordinates. + Returns the center and radius of the circle passing the given 3 points. In case the 3 points form a line, raises a ValueError. """ @@ -81,7 +83,7 @@ def kicad_mid_to_center_arc(mid, start, end): cy = ((p1[0] - p2[0]) * cd - (p2[0] - p3[0]) * bc) / det radius = math.sqrt((cx - p1[0])**2 + (cy - p1[1])**2) - return (cx, cy), radius + return (cx, cy), radius, det < 0 @sexp_type('hatch') @@ -178,6 +180,22 @@ class Zone: self.fill_polygons = [] self.fill_segments = [] + def rotate(self, angle, cx=None, cy=None): + self.unfill() + self.polygon.pts = [pt.with_rotation(angle, cx, cy) for pt in self.polygon.pts] + + def offset(self, x=0, y=0): + self.unfill() + self.polygon.pts = [pt.with_offset(x, y) for pt in self.polygon.pts] + + + def bounding_box(self): + min_x = min(pt.x for pt in self.polygon.pts) + min_y = min(pt.y for pt in self.polygon.pts) + max_x = max(pt.x for pt in self.polygon.pts) + max_y = max(pt.y for pt in self.polygon.pts) + return (min_x, min_y), (max_x, max_y) + @sexp_type('polygon') class RenderCachePolygon: -- cgit