summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2024-07-19 19:15:52 +0200
committerjaseg <git@jaseg.de>2024-07-19 19:15:52 +0200
commitd7efa577320cbb1af3d4399b008b5901309b5ea4 (patch)
tree67820c886ee48192ff44ff9873f179234dafe371
parent689ce748dba83102775871df0e49bdf615f8fe7e (diff)
downloadgerbonara-d7efa577320cbb1af3d4399b008b5901309b5ea4.tar.gz
gerbonara-d7efa577320cbb1af3d4399b008b5901309b5ea4.tar.bz2
gerbonara-d7efa577320cbb1af3d4399b008b5901309b5ea4.zip
kicad: Add bounding box support to lots of s-expr objects
-rw-r--r--gerbonara/cad/kicad/base_types.py13
-rw-r--r--gerbonara/cad/kicad/footprints.py3
-rw-r--r--gerbonara/cad/kicad/graphical_primitives.py30
-rw-r--r--gerbonara/cad/kicad/pcb.py16
-rw-r--r--gerbonara/cad/kicad/primitives.py20
-rw-r--r--gerbonara/utils.py13
6 files changed, 66 insertions, 29 deletions
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:
diff --git a/gerbonara/utils.py b/gerbonara/utils.py
index 6d8445d..fa23f52 100644
--- a/gerbonara/utils.py
+++ b/gerbonara/utils.py
@@ -617,3 +617,16 @@ def bbox_intersect(a, b):
return x_overlap and y_overlap
+
+def bbox_contains(outer, inner):
+ if outer is None or inner is None:
+ return False
+
+ (xa_min, ya_min), (xa_max, ya_max) = outer
+ (xb_min, yb_min), (xb_max, yb_max) = inner
+
+ contained_x = xa_min < xb_min and xb_max < xa_max
+ contained_y = ya_min < yb_min and yb_max < ya_max
+
+ return contained_x and contained_y
+