kicad: Add bounding box support to lots of s-expr objects
This commit is contained in:
parent
689ce748db
commit
d7efa57732
6 changed files with 66 additions and 29 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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={})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue