kicad: Add bounding box support to lots of s-expr objects

This commit is contained in:
jaseg 2024-07-19 19:15:52 +02:00
parent 689ce748db
commit d7efa57732
6 changed files with 66 additions and 29 deletions

View file

@ -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)

View file

@ -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={})

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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