Skip to content

Commit 1639141

Browse files
committed
footprint: Various minor fixes after more testing
Some places have left placeholders for items/formats that will be implemented in the future. Also one bigger part will be arcs in zone polygons instead of "pure" positions (check RoyalBlue54L-Feather board)
1 parent 9159e69 commit 1639141

5 files changed

Lines changed: 70 additions & 38 deletions

File tree

src/kiutils/footprint.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
from os import path
2323

2424
from kiutils.items.zones import Zone
25-
from kiutils.items.common import Image, Coordinate, Net, Group, Font
25+
from kiutils.items.common import Image, Coordinate, Net, Group, Font, EmbeddedFile
26+
from kiutils.items.dimensions import Dimension
2627
from kiutils.items.fpitems import *
2728
from kiutils.items.gritems import *
2829
from kiutils.utils.sexpr import sexp_prettify as prettify, sexp_to_string, parse_sexp
@@ -589,11 +590,9 @@ def from_sexpr(cls, exp: list) -> Pad:
589590
elif primitive[0] == 'gr_arc': object.customPadPrimitives.append(GrArc().from_sexpr(primitive))
590591
elif primitive[0] == 'gr_poly': object.customPadPrimitives.append(GrPoly().from_sexpr(primitive))
591592
elif primitive[0] == 'gr_curve': object.customPadPrimitives.append(GrCurve().from_sexpr(primitive))
592-
# XXX: Are dimentions even implemented here?
593-
elif primitive[0] == 'dimension': raise NotImplementedError(
594-
"Dimensions are not yet handled! Please report this bug along with the file being parsed.")
595593
elif item[0] == 'thermal_bridge_width': object.thermal_bridge_width = item[1]
596594
elif item[0] == 'thermal_bridge_angle': object.thermal_bridge_angle = item[1]
595+
# elif item[0] == 'teardrops': continue
597596
else:
598597
raise ValueError(f"Unrecognized property key: {item[0]}. Full expression: {item}")
599598

@@ -848,7 +847,7 @@ def libId(self, symbol_id: str):
848847
for all pads in the footprint. If not set, the zone thermal_gap setting is used. If not set, the
849848
zone thermal_gap setting is used."""
850849

851-
attributes: Attributes = field(default_factory=lambda: Attributes())
850+
attributes: Optional[Attributes] = None
852851
"""The optional ``attributes`` section defines the attributes of the footprint"""
853852

854853
privateLayers: List[str] = field(default_factory=list)
@@ -901,6 +900,9 @@ def libId(self, symbol_id: str):
901900
sheet_file: str = ""
902901
"""The ``sheet_file`` token defines filename of the schematic sheet file associated with this footprint instance."""
903902

903+
embedded_files: list[EmbeddedFile] = field(default_factory=list)
904+
"""The ``embedded_files`` store data of embedded files"""
905+
904906
@classmethod
905907
def from_sexpr(cls, exp: list) -> Footprint:
906908
"""Convert the given S-Expresstion into a Footprint object
@@ -967,10 +969,9 @@ def from_sexpr(cls, exp: list) -> Footprint:
967969
elif item[0] == 'group': object.groups.append(Group.from_sexpr(item))
968970
elif item[0] == 'private_layers': object.privateLayers.extend(item[1:])
969971
elif item[0] == 'net_tie_pad_groups': object.netTiePadGroups.extend(item[1:])
970-
elif item[0] == 'dimension':
971-
raise NotImplementedError(
972-
"Dimensions are not yet handled! Please report this bug along with the file being parsed.")
972+
elif item[0] == 'dimension': object.graphicItems.append(Dimension.from_sexpr(item))
973973
elif item[0] == 'embedded_fonts': object.embedded_fonts = item[1]
974+
elif item[0] == 'embedded_files': object.embedded_files.extend([EmbeddedFile().from_sexpr(f) for f in item[1:]])
974975
else:
975976
raise ValueError(f"Unrecognized property key: {item[0]}. Full expression: {item}")
976977

@@ -1185,6 +1186,11 @@ def _to_sexpr_raw(self):
11851186
if self.embedded_fonts is not None:
11861187
expr.append(['embedded_fonts', self.embedded_fonts])
11871188

1189+
# Embedded files
1190+
if len(self.embedded_files) > 0:
1191+
embedded_files_expr = ['embedded_files'] + [f._to_sexpr_raw() for f in self.embedded_files]
1192+
expr.append(embedded_files_expr)
1193+
11881194
for item in self.models:
11891195
expr.append(item._to_sexpr_raw())
11901196

src/kiutils/items/brditems.py

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -598,25 +598,43 @@ class PlotSettings():
598598
where the plot files will be saved"""
599599

600600
# Available since KiCad v9
601-
# TODO Update docs
602601

603-
pdf_front_fp_property_popups: Optional[str] = None
602+
pdf_front_fp_property_popups: Optional[bool] = None
603+
"""The optional ``pdf_front_fp_property_popups`` token defines if interactive popups for
604+
front-side footprint properties are included in PDF output"""
605+
606+
pdf_back_fp_property_popups: Optional[bool] = None
607+
"""The optional ``pdf_back_fp_property_popups`` token defines if interactive popups for
608+
back-side footprint properties are included in PDF output"""
604609

605-
pdf_back_fp_property_popups: Optional[str] = None
610+
pdf_metadata: Optional[bool] = None
611+
"""The optional ``pdf_metadata`` token defines if document metadata should be embedded
612+
in the PDF output"""
606613

607-
pdf_metadata: Optional[str] = None
614+
pdf_single_document: Optional[bool] = None
615+
"""The optional ``pdf_single_document`` token defines if all layers should be plotted
616+
into a single PDF document"""
608617

609-
pdf_single_document: Optional[str] = None
618+
plot_black_and_white: Optional[bool] = None
619+
"""The optional ``plot_black_and_white`` token defines if the plot should be generated
620+
in black and white"""
610621

611-
plot_black_and_white: Optional[str] = None
622+
hide_dnp_on_fab: Optional[bool] = None
623+
"""The optional ``hide_dnp_on_fab`` token defines if 'Do Not Populate' footprints should
624+
be hidden on fabrication plots"""
612625

613-
hide_dnp_on_fab: Optional[str] = None
626+
crossout_dnp_on_fab: Optional[bool] = None
627+
"""The optional ``crossout_dnp_on_fab`` token defines if 'Do Not Populate' footprints
628+
should be crossed out on fabrication plots"""
614629

615-
crossout_dnp_on_fab: Optional[str] = None
630+
sketch_dnp_on_fab: Optional[bool] = None
631+
"""The optional ``sketch_dnp_on_fab`` token defines if 'Do Not Populate' footprints should
632+
be drawn in sketch mode on fabrication plots"""
616633

617-
sketch_dnp_on_fab: Optional[str] = None
634+
plot_pad_numbers: Optional[bool] = None
635+
"""The optional ``plot_pad_numbers`` token defines if pad numbers should be plotted
636+
on fabrication layers"""
618637

619-
plot_pad_numbers: Optional[str] = None
620638

621639
@classmethod
622640
def from_sexpr(cls, exp: list) -> PlotSettings:
@@ -676,15 +694,15 @@ def from_sexpr(cls, exp: list) -> PlotSettings:
676694
elif item[0] == 'drillshape' : object.drillShape = item[1]
677695
elif item[0] == 'scaleselection' : object.scaleSelection = item[1]
678696
elif item[0] == 'outputdirectory' : object.outputDirectory = item[1]
679-
elif item[0] == 'pdf_front_fp_property_popups': object.pdf_front_fp_property_popups = item[1]
680-
elif item[0] == 'pdf_back_fp_property_popups': object.pdf_back_fp_property_popups = item[1]
681-
elif item[0] == 'pdf_metadata': object.pdf_metadata = item[1]
682-
elif item[0] == 'pdf_single_document': object.pdf_single_document = item[1]
683-
elif item[0] == 'plot_black_and_white': object.plot_black_and_white = item[1]
684-
elif item[0] == 'hidednponfab': object.hide_dnp_on_fab = item[1]
685-
elif item[0] == 'sketchdnponfab': object.sketch_dnp_on_fab = item[1]
686-
elif item[0] == 'crossoutdnponfab': object.crossout_dnp_on_fab = item[1]
687-
elif item[0] == 'plotpadnumbers': object.plot_pad_numbers = item[1]
697+
elif item[0] == 'pdf_front_fp_property_popups': object.pdf_front_fp_property_popups = parse_bool(item, 'pdf_front_fp_property_popups')
698+
elif item[0] == 'pdf_back_fp_property_popups': object.pdf_back_fp_property_popups = parse_bool(item, 'pdf_back_fp_property_popups')
699+
elif item[0] == 'pdf_metadata': object.pdf_metadata = parse_bool(item, 'pdf_metadata')
700+
elif item[0] == 'pdf_single_document': object.pdf_single_document = parse_bool(item, 'pdf_single_document')
701+
elif item[0] == 'plot_black_and_white': object.plot_black_and_white = parse_bool(item, 'plot_black_and_white')
702+
elif item[0] == 'hidednponfab': object.hide_dnp_on_fab = parse_bool(item, 'hidednponfab')
703+
elif item[0] == 'sketchdnponfab': object.sketch_dnp_on_fab = parse_bool(item, 'sketchdnponfab')
704+
elif item[0] == 'crossoutdnponfab': object.crossout_dnp_on_fab = parse_bool(item, 'crossoutdnponfab')
705+
elif item[0] == 'plotpadnumbers': object.plot_pad_numbers = parse_bool(item, 'plotpadnumbers')
688706
else:
689707
raise ValueError("Unrecognized property key: {item[0]}")
690708

@@ -735,22 +753,22 @@ def _to_sexpr_raw(self):
735753
expr.append(['viasonmask', self.viasOnMask])
736754

737755
expr.append(['mode', self.mode])
738-
expr.append(['useauxorigin', 'no'])
756+
expr.append(['useauxorigin', self.useAuxOrigin])
739757
expr.append(['hpglpennumber', self.hpglPenNumber])
740758
expr.append(['hpglpenspeed', self.hpglPenSpeed])
741759
expr.append(['hpglpendiameter', (f"{self.hpglPenDiameter:.6f}")])
742760

743761
if self.pdf_front_fp_property_popups is not None:
744-
expr.append(['pdf_front_fp_property_popups', self.pdf_front_fp_property_popups])
762+
expr.append(format_bool('pdf_front_fp_property_popups', self.pdf_front_fp_property_popups, yesno=True))
745763

746764
if self.pdf_back_fp_property_popups is not None:
747-
expr.append(['pdf_back_fp_property_popups', self.pdf_back_fp_property_popups])
765+
expr.append(format_bool('pdf_back_fp_property_popups', self.pdf_back_fp_property_popups, yesno=True))
748766

749767
if self.pdf_metadata is not None:
750-
expr.append(['pdf_metadata', self.pdf_metadata])
768+
expr.append(format_bool('pdf_metadata', self.pdf_metadata, yesno=True))
751769

752770
if self.pdf_single_document is not None:
753-
expr.append(['pdf_single_document', self.pdf_single_document])
771+
expr.append(format_bool('pdf_single_document', self.pdf_single_document, yesno=True))
754772

755773
expr.append(['dxfpolygonmode', self.dxfPolygonMode])
756774
expr.append(['dxfimperialunits', self.dxfImperialUnits])
@@ -759,21 +777,21 @@ def _to_sexpr_raw(self):
759777
expr.append(['psa4output', self.psA4Output])
760778

761779
if self.plot_black_and_white is not None:
762-
expr.append(['plot_black_and_white', self.plot_black_and_white])
780+
expr.append(format_bool('plot_black_and_white', self.plot_black_and_white, yesno=True))
763781

764782
expr.append(['sketchpadsonfab', self.sketchPadsOnFab])
765783

766784
if self.plot_pad_numbers is not None:
767-
expr.append(['plotpadnumbers', self.plot_pad_numbers])
785+
expr.append(format_bool('plotpadnumbers', self.plot_pad_numbers, yesno=True))
768786

769787
if self.hide_dnp_on_fab is not None:
770-
expr.append(['hidednponfab', self.hide_dnp_on_fab])
788+
expr.append(format_bool('hidednponfab', self.hide_dnp_on_fab, yesno=True))
771789

772790
if self.sketch_dnp_on_fab is not None:
773-
expr.append(['sketchdnponfab', self.sketch_dnp_on_fab])
791+
expr.append(format_bool('sketchdnponfab', self.sketch_dnp_on_fab, yesno=True))
774792

775793
if self.crossout_dnp_on_fab is not None:
776-
expr.append(['crossoutdnponfab', self.crossout_dnp_on_fab])
794+
expr.append(format_bool('crossoutdnponfab', self.crossout_dnp_on_fab, yesno=True))
777795

778796
if self.plotReference == 'yes':
779797
expr.append(['plotreference', self.plotReference])
@@ -1139,6 +1157,7 @@ def from_sexpr(cls, exp: list) -> Via:
11391157
elif item[0] == 'net': object.net = item[1]
11401158
elif item[0] == 'tstamp': object.tstamp = item[1]
11411159
elif item[0] == 'uuid': object.tstamp = item[1] # Haha :)
1160+
# elif item[0] == 'teardrops': continue
11421161
else:
11431162
raise ValueError(f"Unrecognized property key: {item[0]}. Full expression: {item}")
11441163

src/kiutils/items/fpitems.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ class FpText():
7575
unlocked: Optional[bool] = None
7676
"""The optional ``unlocked`` token defines if the object can be edited"""
7777

78+
locked: Optional[bool] = None
79+
"""The optional ``locked`` token defines if the object cannot be edited"""
80+
7881
@classmethod
7982
def from_sexpr(cls, exp: list) -> FpText:
8083
"""Convert the given S-Expresstion into a FpText object
@@ -100,6 +103,7 @@ def from_sexpr(cls, exp: list) -> FpText:
100103
object.text = exp[2]
101104
for item in exp[3:]:
102105
if is_bool_key(item, 'unlocked'): object.unlocked = parse_bool(item, 'unlocked')
106+
elif is_bool_key(item, 'locked'): object.locked = parse_bool(item, 'locked')
103107
elif is_bool_key(item, 'hide'): object.hide = parse_bool(item, 'hide')
104108
elif not isinstance(item, list):
105109
raise ValueError(f"Expected list property [key, value], got: {item}. Full expression: {exp}")
@@ -133,6 +137,9 @@ def to_sexpr(self, indent: int = 2, newline: bool = True) -> str:
133137
def _to_sexpr_raw(self):
134138
expr = ['fp_text', self.type, escape_and_quote(self.text)]
135139

140+
if self.locked:
141+
expr.append(format_bool('locked', self.locked))
142+
136143
pos = ['at', format_float(self.position.X), format_float(self.position.Y)]
137144
if self.position.angle is not None:
138145
pos.append(format_float(self.position.angle))

src/kiutils/items/zones.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@ def from_sexpr(cls, exp: list) -> Zone:
649649
elif item[0] == 'filled_polygon': object.filledPolygons.append(FilledPolygon().from_sexpr(item))
650650
elif item[0] == 'fill_segments': object.fillSegments = FillSegments().from_sexpr(item)
651651
elif item[0] == 'placement': object.placement = PlacementSettings().from_sexpr(item)
652+
# elif item[0] == 'attr': continue
652653
else:
653654
raise ValueError(f"Unrecognized property key: {item[0]}. Full expression: {item}")
654655

tests/test_board.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def test_boardSmartPrintCoreH7x(self):
3939
board = Board().from_file(self.testData.pathToTestFile)
4040
self.assertTrue(to_file_and_compare(board, self.testData))
4141

42-
# TODO - This test contains "dimensions" which are not supported yet
4342
def test_TokayLite(self):
4443
"""Tests the behavior when creating and exporting TokayLite board"""
4544
self.testData.pathToTestFile = Path(BOARD_COMMUNITY) / 'TokayLite'

0 commit comments

Comments
 (0)