diff options
Diffstat (limited to 'gerbonara/cad/kicad/footprints.py')
-rw-r--r-- | gerbonara/cad/kicad/footprints.py | 100 |
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={}) |