kopia lustrzana https://gitlab.com/gerbolyze/gerbonara
Improve auto layout API
rodzic
572486aa25
commit
b2729a46ac
|
@ -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(
|
||||
|
|
|
@ -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: {})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue