Improve auto layout API

autoroute
jaseg 2023-07-07 20:19:36 +02:00
rodzic 572486aa25
commit b2729a46ac
3 zmienionych plików z 258 dodań i 12 usunięć

Wyświetl plik

@ -2,6 +2,7 @@
Library for handling KiCad's footprint files (`*.kicad_mod`).
"""
import re
import copy
import enum
import string
@ -33,6 +34,9 @@ from ...aperture_macros import primitive as amp
class _MISSING:
pass
def angle_difference(a, b):
return (b - a + math.pi) % (2*math.pi) - math.pi
@sexp_type('attr')
class Attribute:
type: AtomChoice(Atom.smd, Atom.through_hole) = None
@ -395,6 +399,16 @@ class Pad:
def __before_sexp__(self):
self.layers = fuck_layers(self.layers)
@property
def abs_pos(self):
if self.footprint:
px, py = self.footprint.at.x, self.footprint.at.y
else:
px, py = 0, 0
x, y = rotate_point(self.at.x, self.at.y, -math.radians(self.at.rotation))
return x+px, y+py, self.at.rotation, False
def find_connected(self, **filters):
""" Find footprints connected to the same net as this pad """
return self.footprint.board.find_footprints(net=self.net.name, **filters)
@ -631,14 +645,73 @@ class Footprint:
raise IndexError(f'Footprint has no property named "{key}"')
def set_property(self, key, value, x=0, y=0, rotation=0, layer='F.Fab', hide=True, effects=None):
for prop in self.properties:
if prop.key == key:
old_value, prop.value = prop.value, value
return old_value
if effects is None:
effects = TextEffect()
self.properties.append(DrawnProperty(key, value,
at=AtPos(x, y, rotation),
layer=layer,
hide=hide,
effects=effects))
@property
def pads_by_number(self):
return {(int(pad.number) if pad.number.isnumeric() else pad.number): pad for pad in self.pads if pad.number}
def find_pads(self, number=None, net=None):
for pad in self.pads:
if number is not None and pad.number == str(number):
print('find_pads', number, net, pad.number)
yield pad
elif isinstance(net, str) and fnmatch.fnmatch(pad.net.name, net):
yield pad
elif net is not None and pad.net.number == net:
yield pad
def pad(self, number=None, net=None):
candidates = list(self.find_pads(number=number, net=net))
if not candidates:
raise IndexError(f'No such pad "{number or net}"')
if len(candidates) > 1:
raise IndexError(f'Ambiguous pad "{number or net}", {len(candidates)} matching pads.')
return candidates[0]
@property
def version(self):
return self._version
@property
def reference(self):
return self.property_value('Reference')
@reference.setter
def reference(self, value):
self.set_property('Reference', value)
@property
def parsed_reference(self):
ref = self.reference
if (m := re.match(r'^.*[^0-9]([0-9]+)$', ref)):
return m.group(0), int(m.group(1))
else:
return ref
@property
def value(self):
return self.property_value('Value')
@reference.setter
def value(self, value):
self.set_property('Value', value)
@version.setter
def version(self, value):
if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
@ -676,7 +749,35 @@ class Footprint:
def single_sided(self):
raise NotImplementedError()
def rotate(self, angle, cx=None, cy=None):
def face(self, direction, pad=None, net=None):
if not net and not pad:
pad = '1'
candidates = list(self.find_pads(net=net, number=pad))
if len(candidates) == 0:
raise KeyError(f'Reference pad "{net or pad}" not found.')
if len(candidates) > 1:
raise KeyError(f'Reference pad "{net or pad}" is ambiguous, {len(candidates)} matching pads found.')
pad = candidates[0]
pad_angle = math.atan2(pad.at.y, pad.at.x)
target_angle = {
'right': 0,
'top right': math.pi/4,
'top': math.pi/2,
'top left': 3*math.pi/4,
'left': math.pi,
'bottom left': -3*math.pi/4,
'bottom': -math.pi/2,
'bottom right': -math.pi/4}.get(direction, direction)
delta = angle_difference(target_angle, pad_angle)
adj = round(delta / (math.pi/2)) * math.pi/2
self.set_rotation(adj)
def rotate(self, angle=None, cx=None, cy=None, **reference_pad):
""" Rotate this footprint by the given angle in radians, counter-clockwise. When (cx, cy) are given, rotate
around the given coordinates in the global coordinate space. Otherwise rotate around the footprint's origin. """
if (cx, cy) != (None, None):
@ -684,9 +785,9 @@ class Footprint:
self.at.x = math.cos(angle)*x - math.sin(angle)*y + cx
self.at.y = math.sin(angle)*x + math.cos(angle)*y + cy
self.at.rotation -= math.degrees(angle)
self.at.rotation = (self.at.rotation - math.degrees(angle)) % 360
for pad in self.pads:
pad.at.rotation -= math.degrees(angle)
pad.at.rotation = (pad.at.rotation - math.degrees(angle)) % 360
def set_rotation(self, angle):
old_deg = self.at.rotation
@ -694,7 +795,7 @@ class Footprint:
delta = new_deg - old_deg
for pad in self.pads:
pad.at.rotation += delta
pad.at.rotation = (pad.at.rotation + delta) % 360
def objects(self, text=False, pads=True):
return chain(

Wyświetl plik

@ -2,6 +2,7 @@
Library for handling KiCad's PCB files (`*.kicad_mod`).
"""
import math
from pathlib import Path
from dataclasses import field
from itertools import chain
@ -14,14 +15,14 @@ from .primitives import *
from .footprints import Footprint
from . import graphical_primitives as gr
from ..primitives import Positioned
from .. import primitives as cad_pr
from ... import graphic_primitives as gp
from ... import graphic_objects as go
from ... import apertures as ap
from ...layers import LayerStack
from ...newstroke import Newstroke
from ...utils import MM
from ...utils import MM, rotate_point
def match_filter(f, value):
@ -29,6 +30,29 @@ def match_filter(f, value):
return True
return value in f
def gn_side_to_kicad(side, layer='Cu'):
if side == 'top':
return f'F.{layer}'
elif side == 'bottom':
return f'B.{layer}'
elif side.startswith('inner'):
return f'In{int(side[5:])}.{layer}'
else:
raise ValueError(f'Cannot parse gerbonara side name "{side}"')
def gn_layer_to_kicad(layer, flip=False):
side = 'B' if flip else 'F'
if layer == 'silk':
return f'{side}.SilkS'
elif layer == 'mask':
return f'{side}.Mask'
elif layer == 'paste':
return f'{side}.Paste'
elif layer == 'copper':
return f'{side}.Cu'
else:
raise ValueError('Cannot translate gerbonara layer name "{layer}" to KiCad')
@sexp_type('general')
class GeneralSection:
@ -160,6 +184,13 @@ class TrackSegment:
aperture = ap.CircleAperture(self.width, unit=MM)
yield go.Line(self.start.x, self.start.y, self.end.x, self.end.y, aperture=aperture, unit=MM)
def rotate(self, angle, cx=None, cy=None):
if cx is None or cy is None:
cx, cy = self.start.x, self.start.y
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
@sexp_type('arc')
class TrackArc:
@ -182,6 +213,14 @@ class TrackArc:
x2, y2 = self.end.x, self.end.y
yield go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, aperture=aperture, clockwise=True, unit=MM)
def rotate(self, angle, cx=None, cy=None):
if cx is None or cy is None:
cx, cy = self.mid.x, self.mid.y
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
self.mid.x, self.mid.y = rotate_point(self.mid.x, self.mid.y, angle, cx, cy)
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
@sexp_type('via')
class Via:
@ -231,8 +270,8 @@ class Board:
images: List(Image) = field(default_factory=list)
# Tracks
track_segments: List(TrackSegment) = field(default_factory=list)
vias: List(Via) = field(default_factory=list)
track_arcs: List(TrackArc) = field(default_factory=list)
vias: List(Via) = field(default_factory=list)
# Other stuff
zones: List(Zone) = field(default_factory=list)
groups: List(Group) = field(default_factory=list)
@ -248,8 +287,98 @@ class Board:
for fp in self.footprints:
fp.board = self
self.nets = {net.index: net.name for net in self.nets}
def __before_sexp__(self):
self.properties = [Property(key, value) for key, value in self.properties.items()]
self.nets = [Net(index, name) for index, name in self.nets.items()]
def add(self, obj):
match obj:
case gr.Text():
self.texts.append(obj)
case gr.TextBox():
self.text_boxes.append(obj)
case gr.Line():
self.lines.append(obj)
case gr.Rectangle():
self.rectangles.append(obj)
case gr.Circle():
self.circles.append(obj)
case gr.Arc():
self.arcs.append(obj)
case gr.Polygon():
self.polygons.append(obj)
case gr.Curve():
self.curves.append(obj)
case gr.Dimension():
self.dimensions.append(obj)
case Image():
self.images.append(obj)
case TrackSegment():
self.track_segments.append(obj)
case TrackArc():
self.track_arcs.append(obj)
case Via():
self.vias.append(obj)
case Zone():
self.zones.append(obj)
case Group():
self.groups.append(obj)
case _:
for elem in self.map_gn_cad(obj):
self.add(elem)
def map_gn_cad(self, obj, locked=False, net_name=None):
match obj:
case cad_pr.Trace():
for elem in obj.to_graphic_objects():
elem.convert_to(MM)
match elem:
case go.Arc(x1, y1, x2, y2, xc, yc, cw, ap):
yield TrackArc(
start=XYCoord(x1, y1),
mid=XYCoord(x1+xc, y1+yc),
end=XYCoord(x2, y2),
width=ap.equivalent_width(MM),
layer=gn_side_to_kicad(obj.side),
locked=locked,
net=self.net_id(net_name))
case go.Line(x1, y1, x2, y2, ap):
yield TrackSegment(
start=XYCoord(x1, y1),
end=XYCoord(x2, y2),
width=ap.equivalent_width(MM),
layer=gn_side_to_kicad(obj.side),
locked=locked,
net=self.net_id(net_name))
case cad_pr.Via(pad_stack=cad_pr.ThroughViaStack(hole, dia, unit=st_unit)):
x, y, _a, _f = obj.abs_pos()
x, y = MM(x, st_unit), MM(y, obj.unit)
yield Via(
locked=locked,
at=XYCoord(x, y),
size=MM(dia, st_unit),
drill=MM(hole, st_unit),
layers='*.Cu',
net=self.net_id(net_name))
case cad_pr.Text(_x, _y, text, font_size, stroke_width, h_align, v_align, layer, dark):
x, y, a, flip = obj.abs_pos()
x, y = MM(x, st_unit), MM(y, st_unit)
size = MM(size, unit)
yield gr.Text(
text,
AtPos(x, y, -math.degrees(a)),
layer=gr.TextLayer(gn_layer_to_kicad(layer, flip), not dark),
effects=TextEffect(font=FontSpec(
size=XYCoord(size, size),
thickness=stroke_width),
justify=Justify(h=Atom(h_align) if h_align != 'center' else None,
v=Atom(v_align) if v_align != 'middle' else None,
mirror=flip)))
def unfill_zones(self):
for zone in self.zones:
@ -306,6 +435,22 @@ class Board:
def single_sided(self):
raise NotImplementedError()
def net_id(self, name, create=True):
if name is None:
return None
for i, n in self.nets.items():
if n == name:
return i
if create:
index = max(self.nets.keys()) + 1
self.nets[index] = name
return index
else:
raise IndexError(f'No such net: "{name}"')
# FIXME vvv
def graphic_objects(self, text=False, images=False):
return chain(
@ -363,7 +508,7 @@ class Board:
@dataclass
class BoardInstance(Positioned):
class BoardInstance(cad_pr.Positioned):
sexp: Board = None
variables: dict = field(default_factory=lambda: {})

Wyświetl plik

@ -700,13 +700,13 @@ class Trace:
yield Line(line_b.x1, line_b.y1, x3, y3, aperture=aperture, unit=self.unit)
def _to_graphic_objects(self):
def to_graphic_objects(self):
start, end = self.start, self.end
if not isinstance(start, tuple):
*start, _rotation = start.abs_pos
*start, _rotation, _flip = start.abs_pos
if not isinstance(end, tuple):
*end, _rotation = end.abs_pos
*end, _rotation, _flip = end.abs_pos
aperture = CircleAperture(diameter=self.width, unit=self.unit)
@ -720,7 +720,7 @@ class Trace:
return self._round_over(points, aperture)
def render(self, layer_stack, cache=None):
layer_stack[self.side, 'copper'].objects.extend(self._to_graphic_objects())
layer_stack[self.side, 'copper'].objects.extend(self.to_graphic_objects())
def _route_demo():
from ..utils import setup_svg, Tag