summaryrefslogtreecommitdiff
path: root/gerbonara/cad/kicad/footprints.py
diff options
context:
space:
mode:
Diffstat (limited to 'gerbonara/cad/kicad/footprints.py')
-rw-r--r--gerbonara/cad/kicad/footprints.py100
1 files changed, 62 insertions, 38 deletions
diff --git a/gerbonara/cad/kicad/footprints.py b/gerbonara/cad/kicad/footprints.py
index 9debaa9..1d7ee08 100644
--- a/gerbonara/cad/kicad/footprints.py
+++ b/gerbonara/cad/kicad/footprints.py
@@ -21,6 +21,7 @@ from . import graphical_primitives as gr
from ..primitives import Positioned
+from ... import __version__
from ... import graphic_primitives as gp
from ... import graphic_objects as go
from ... import apertures as ap
@@ -54,8 +55,9 @@ class Text:
type: AtomChoice(Atom.reference, Atom.value, Atom.user) = Atom.user
text: str = ""
at: AtPos = field(default_factory=AtPos)
- unlocked: Flag() = False
+ unlocked: OmitDefault(Named(YesNoAtom())) = False
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
hide: Flag() = False
effects: TextEffect = field(default_factory=TextEffect)
tstamp: Timestamp = None
@@ -72,12 +74,14 @@ class TextBox:
locked: Flag() = False
text: str = None
start: Rename(XYCoord) = None
- end: Named(XYCoord) = None
+ end: Rename(XYCoord) = None
pts: PointList = None
angle: Named(float) = 0.0
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
tstamp: Timestamp = None
effects: TextEffect = field(default_factory=TextEffect)
+ border: Named(YesNoAtom()) = False
stroke: Stroke = field(default_factory=Stroke)
render_cache: RenderCache = None
@@ -90,6 +94,7 @@ class Line:
start: Rename(XYCoord) = None
end: Rename(XYCoord) = None
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
width: Named(float) = None
stroke: Stroke = None
locked: Flag() = False
@@ -113,6 +118,7 @@ class Rectangle:
start: Rename(XYCoord) = None
end: Rename(XYCoord) = None
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
width: Named(float) = None
stroke: Stroke = None
fill: Named(AtomChoice(Atom.solid, Atom.none)) = None
@@ -146,6 +152,7 @@ class Circle:
center: Rename(XYCoord) = None
end: Rename(XYCoord) = None
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
width: Named(float) = None
stroke: Stroke = None
fill: Named(AtomChoice(Atom.solid, Atom.none)) = None
@@ -184,6 +191,7 @@ class Arc:
width: Named(float) = None
stroke: Stroke = None
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
locked: Flag() = False
tstamp: Timestamp = None
@@ -235,8 +243,9 @@ class Arc:
@sexp_type('fp_poly')
class Polygon:
- pts: PointList = field(default_factory=PointList)
+ pts: PointList = field(default_factory=list)
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
width: Named(float) = None
stroke: Stroke = None
fill: Named(AtomChoice(Atom.solid, Atom.none)) = None
@@ -244,13 +253,13 @@ class Polygon:
tstamp: Timestamp = None
def render(self, variables=None, cache=None):
- if len(self.pts.xy) < 2:
+ if len(self.pts) < 2:
return
dasher = Dasher(self)
- start = self.pts.xy[0]
+ start = self.pts[0]
dasher.move(start.x, start.y)
- for point in self.pts.xy[1:]:
+ for point in self.pts[1:]:
dasher.line(point.x, point.y)
if dasher.width > 0:
@@ -259,13 +268,14 @@ class Polygon:
yield go.Line(x1, -y1, x2, -y2, aperture=aperture, unit=MM)
if self.fill == Atom.solid:
- yield go.Region([(pt.x, -pt.y) for pt in self.pts.xy], unit=MM)
+ yield go.Region([(pt.x, -pt.y) for pt in self.pts], unit=MM)
@sexp_type('fp_curve')
class Curve:
- pts: PointList = field(default_factory=PointList)
+ pts: PointList = field(default_factory=list)
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
width: Named(float) = None
stroke: Stroke = None
locked: Flag() = False
@@ -302,8 +312,9 @@ class Dimension:
locked: Flag() = False
type: AtomChoice(Atom.aligned, Atom.leader, Atom.center, Atom.orthogonal, Atom.radial) = None
layer: Named(str) = None
+ uuid: UUID = field(default_factory=UUID)
tstamp: Timestamp = None
- pts: PointList = field(default_factory=PointList)
+ pts: PointList = field(default_factory=list)
height: Named(float) = None
orientation: Named(int) = 0
leader_length: Named(float) = None
@@ -323,12 +334,6 @@ class Drill:
offset: Rename(XYCoord) = None
-@sexp_type('net')
-class NetDef:
- number: int = None
- name: str = None
-
-
@sexp_type('options')
class CustomPadOptions:
clearance: Named(AtomChoice(Atom.outline, Atom.convexhull)) = Atom.outline
@@ -365,7 +370,7 @@ class Chamfer:
@sexp_type('pad')
-class Pad:
+class Pad(NetMixin):
number: str = None
type: AtomChoice(Atom.thru_hole, Atom.smd, Atom.connect, Atom.np_thru_hole) = None
shape: AtomChoice(Atom.circle, Atom.rect, Atom.oval, Atom.trapezoid, Atom.roundrect, Atom.custom) = None
@@ -375,15 +380,16 @@ class Pad:
drill: Drill = None
layers: Named(Array(str)) = field(default_factory=list)
properties: List(Property) = field(default_factory=list)
- remove_unused_layers: Wrap(Flag()) = False
- keep_end_layers: Wrap(Flag()) = False
+ remove_unused_layers: Named(YesNoAtom()) = False
+ keep_end_layers: Named(YesNoAtom()) = False
+ uuid: UUID = field(default_factory=UUID)
rect_delta: Rename(XYCoord) = None
roundrect_rratio: Named(float) = None
thermal_bridge_angle: Named(int) = 45
thermal_bridge_width: Named(float) = 0.5
chamfer_ratio: Named(float) = None
chamfer: Chamfer = None
- net: NetDef = None
+ net: Net = None
tstamp: Timestamp = None
pin_function: Named(str) = None
pintype: Named(str) = None
@@ -399,7 +405,7 @@ class Pad:
options: OmitDefault(CustomPadOptions) = None
primitives: OmitDefault(CustomPadPrimitives) = None
_: SEXP_END = None
- footprint: object = None
+ footprint: object = field(repr=False, default=None)
def __after_parse__(self, parent=None):
self.layers = unfuck_layers(self.layers)
@@ -595,6 +601,7 @@ class Pad:
@sexp_type('model')
class Model:
name: str = ''
+ hide: Flag() = False
at: Named(XYZCoord) = field(default_factory=XYZCoord)
offset: Named(XYZCoord) = field(default_factory=XYZCoord)
scale: Named(XYZCoord) = field(default_factory=XYZCoord)
@@ -606,7 +613,9 @@ SUPPORTED_FILE_FORMAT_VERSIONS = [20210108, 20211014, 20221018, 20230517]
class Footprint:
name: str = None
_version: Named(int, name='version') = 20221018
- generator: Named(Atom) = Atom.gerbonara
+ uuid: UUID = field(default_factory=UUID)
+ generator: Named(str) = Atom.gerbonara
+ generator_version: Named(str) = __version__
locked: Flag() = False
placed: Flag() = False
layer: Named(str) = 'F.Cu'
@@ -643,11 +652,11 @@ class Footprint:
pads: List(Pad) = field(default_factory=list)
zones: List(Zone) = field(default_factory=list)
groups: List(Group) = field(default_factory=list)
+ embedded_fonts: Named(YesNoAtom()) = False
models: List(Model) = field(default_factory=list)
_ : SEXP_END = None
original_filename: str = None
- _bounding_box: tuple = None
- board: object = None
+ board: object = field(repr=False, default=None)
def __after_parse__(self, parent):
for pad in self.pads:
@@ -694,6 +703,10 @@ class Footprint:
if not self.property_value('Description', None):
self.set_property('Description', self.descr or '', 0, 0, 0)
+ def reset_nets(self):
+ for pad in self.pads:
+ pad.reset_net()
+
@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}
@@ -720,6 +733,14 @@ class Footprint:
def offset(self, x=0, y=0):
self.at = self.at.with_offset(x, y)
+ def copy_placement(self, template):
+ # Fix up rotation of pads - KiCad saves each pad's rotation in *absolute* coordinates, not relative to the
+ # footprint. Because we overwrite the footprint's rotation below, we have to first fix all pads to match the
+ # new rotation.
+ self.rotate(math.radians(template.at.rotation - self.at.rotation))
+ self.at = copy.copy(template.at)
+ self.side = template.side
+
@property
def version(self):
return self._version
@@ -804,7 +825,7 @@ class Footprint:
self.layer = flip_layer(self.layer)
for obj in self.objects():
- if hasattr(obj, 'layer'):
+ if getattr(obj, 'layer', None) is not None:
obj.layer = flip_layer(obj.layer)
if hasattr(obj, 'layers'):
@@ -814,8 +835,9 @@ class Footprint:
obj.effects.justify.mirror = not obj.effects.justify.mirror
for obj in self.properties:
- obj.effects.justify.mirror = not obj.effects.justify.mirror
- obj.layer = flip_layer(obj.layer)
+ if obj.layer is not None:
+ obj.effects.justify.mirror = not obj.effects.justify.mirror
+ obj.layer = flip_layer(obj.layer)
@property
def single_sided(self):
@@ -854,19 +876,20 @@ class Footprint:
around the given coordinates in the global coordinate space. Otherwise rotate around the footprint's origin. """
if (cx, cy) != (None, None):
x, y = self.at.x-cx, self.at.y-cy
- 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.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 = (self.at.rotation - math.degrees(angle)) % 360
+ self.at.rotation = (self.at.rotation + math.degrees(angle)) % 360
for pad in self.pads:
- pad.at.rotation = (pad.at.rotation - math.degrees(angle)) % 360
+ pad.at.rotation = (pad.at.rotation + math.degrees(angle)) % 360
for prop in self.properties:
- prop.at.rotation = (prop.at.rotation - math.degrees(angle)) % 360
+ if prop.at is not None:
+ prop.at.rotation = (prop.at.rotation + math.degrees(angle)) % 360
for text in self.texts:
- text.at.rotation = (text.at.rotation - math.degrees(angle)) % 360
+ text.at.rotation = (text.at.rotation + math.degrees(angle)) % 360
def set_rotation(self, angle):
old_deg = self.at.rotation
@@ -877,7 +900,8 @@ class Footprint:
pad.at.rotation = (pad.at.rotation + delta) % 360
for prop in self.properties:
- prop.at.rotation = (prop.at.rotation + delta) % 360
+ if prop.at is not None:
+ prop.at.rotation = (prop.at.rotation + delta) % 360
for text in self.texts:
text.at.rotation = (text.at.rotation + delta) % 360
@@ -902,13 +926,13 @@ class Footprint:
y += self.at.y
rotation += math.radians(self.at.rotation)
- for obj in self.objects(pads=False, text=text, zones=False):
+ for obj in self.objects(pads=False, text=text, zones=False, groups=False):
if not (layer := layer_map.get(obj.layer)):
continue
for fe in obj.render(variables=variables):
fe.rotate(rotation)
- fe.offset(x, -y, MM)
+ fe.offset(x, y, MM)
layer_stack[layer].objects.append(fe)
for obj in self.pads:
@@ -940,7 +964,7 @@ class Footprint:
for fe in obj.render(margin=margin, cache=cache):
fe.rotate(rotation)
- fe.offset(x, -y, MM)
+ fe.offset(x, y, MM)
if isinstance(fe, go.Flash) and fe.aperture:
fe.aperture = fe.aperture.rotated(rotation)
layer_stack[layer_map[layer]].objects.append(fe)
@@ -948,7 +972,7 @@ class Footprint:
for obj in self.pads:
for fe in obj.render_drill():
fe.rotate(rotation)
- fe.offset(x, -y, MM)
+ fe.offset(x, y, MM)
if obj.type == Atom.np_thru_hole:
layer_stack.drill_npth.append(fe)
@@ -956,7 +980,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={})