Skip to content

Commit f5ad028

Browse files
authored
add: KiCad8 symbol properties parser and render (#138)
* add: use a class to record symbol properties * add: render symbol properties in footprint * fix: new test for P.dict * add: simple test for symbol_properties parser
1 parent 93b29dc commit f5ad028

File tree

8 files changed

+467
-16
lines changed

8 files changed

+467
-16
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ dist/
44
www/kicanvas
55
coverage/
66
esbuild-meta.json
7+
8+
.DS_Store

src/kicad/board.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,14 @@ export class DimensionStyle {
731731
}
732732
}
733733

734-
type FootprintDrawings = FpLine | FpCircle | FpArc | FpPoly | FpRect | FpText;
734+
type FootprintDrawings =
735+
| FpLine
736+
| FpCircle
737+
| FpArc
738+
| FpPoly
739+
| FpRect
740+
| FpText
741+
| SymbolProperty;
735742

736743
export class Footprint {
737744
at: At;
@@ -776,7 +783,7 @@ export class Footprint {
776783
allow_solder_mask_bridges: false,
777784
allow_missing_courtyard: false,
778785
};
779-
properties: Record<string, string> = {};
786+
properties: Record<string, SymbolProperty> = {};
780787
drawings: FootprintDrawings[] = [];
781788
pads: Pad[] = [];
782789
#pads_by_number = new Map<string, Pad>();
@@ -826,7 +833,7 @@ export class Footprint {
826833
P.atom("allow_solder_mask_bridges"),
827834
P.atom("allow_missing_courtyard"),
828835
),
829-
P.dict("properties", "property", T.string),
836+
P.dict("properties", "property", T.item(SymbolProperty, this)),
830837
P.collection("drawings", "fp_line", T.item(FpLine, this)),
831838
P.collection("drawings", "fp_circle", T.item(FpCircle, this)),
832839
P.collection("drawings", "fp_arc", T.item(FpArc, this)),
@@ -860,13 +867,13 @@ export class Footprint {
860867
// KiCad correctly parses both definitions
861868
// (fp_text reference XXXX) and (property "Reference" "XXXX")
862869
// https://dev-docs.kicad.org/en/file-formats/sexpr-intro/index.html#_symbol_properties
863-
for (const [prop_name, prop_value] of Object.entries(this.properties)) {
870+
for (const [prop_name, prop] of Object.entries(this.properties)) {
864871
if (this.reference === undefined && prop_name == "Reference") {
865-
this.reference = prop_value;
872+
this.reference = prop.value;
866873
}
867874

868875
if (this.value === undefined && prop_name == "Value") {
869-
this.value = prop_value;
876+
this.value = prop.value;
870877
}
871878
}
872879
}
@@ -879,6 +886,10 @@ export class Footprint {
879886
yield* this.drawings ?? [];
880887
yield* this.zones ?? [];
881888
yield* this.pads.values() ?? [];
889+
890+
yield* Object.values(this.properties).filter(
891+
(prop) => prop.has_symbol_prop,
892+
);
882893
}
883894

884895
resolve_text_var(name: string): string | undefined {
@@ -914,7 +925,7 @@ export class Footprint {
914925
}
915926

916927
if (this.properties[name] !== undefined) {
917-
return this.properties[name]!;
928+
return this.properties[name]!.value;
918929
}
919930

920931
return this.parent.resolve_text_var(name);
@@ -949,7 +960,7 @@ export class Footprint {
949960
).rotate_self(Angle.deg_to_rad(this.at.rotation));
950961

951962
for (const item of this.drawings) {
952-
if (item instanceof FpText) {
963+
if (item instanceof FpText || item instanceof SymbolProperty) {
953964
continue;
954965
}
955966

@@ -963,6 +974,63 @@ export class Footprint {
963974
}
964975
}
965976

977+
export class SymbolProperty {
978+
has_symbol_prop: boolean;
979+
980+
// generic properties
981+
// https://dev-docs.kicad.org/en/file-formats/sexpr-intro/index.html#_properties
982+
value: string;
983+
984+
// symbol properties
985+
// https://dev-docs.kicad.org/en/file-formats/sexpr-intro/index.html#_symbol_properties
986+
id: number = 0;
987+
unlocked = false;
988+
hide = false;
989+
at: At = new At();
990+
effects: Effects = new Effects();
991+
layer: string = "F.SilkS";
992+
uuid?: string;
993+
994+
constructor(
995+
expr: Parseable,
996+
public parent: Footprint,
997+
) {
998+
// for some reasons, I cannot get kicad_version here
999+
// const is_newer = parent.parent.version >= 20240108;
1000+
const is_newer = expr instanceof Array && expr.length > 3;
1001+
1002+
if (is_newer) {
1003+
this.has_symbol_prop = true;
1004+
// parsed as 'symbol_property' node
1005+
Object.assign(
1006+
this,
1007+
parse_expr(
1008+
expr,
1009+
P.positional("value", T.string),
1010+
P.pair("id", T.number),
1011+
P.item("at", At),
1012+
P.pair("layer", T.string),
1013+
P.pair("uuid", T.string),
1014+
P.atom("unlocked"),
1015+
P.atom("hide"),
1016+
P.item("effects", Effects),
1017+
),
1018+
);
1019+
} else {
1020+
this.has_symbol_prop = false;
1021+
// parsed as 'property' node
1022+
Object.assign(
1023+
this,
1024+
parse_expr(expr, P.positional("value", T.string)),
1025+
);
1026+
}
1027+
}
1028+
1029+
get shown_text() {
1030+
return expand_text_vars(this.value, this.parent);
1031+
}
1032+
}
1033+
9661034
class GraphicItem {
9671035
parent?: Footprint;
9681036
layer: string;

src/kicad/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export const P = {
222222
fn: (obj: Obj, name: string, e: ListOrAtom) => {
223223
const el = e as [string, string, any];
224224
const rec = obj[name] ?? {};
225-
rec[el[1]] = typefn(obj, name, el[2]);
225+
rec[el[1]] = typefn(obj, name, el.slice(2));
226226
return rec;
227227
},
228228
};

src/kicanvas/elements/kc-board/properties-panel.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ export class KCBoardPropertiesPanelElement extends KCUIElement {
5454
} else {
5555
const itm = this.selected_item;
5656

57-
const properties = Object.entries(itm.properties).map(([k, v]) => {
58-
return entry(k, v);
59-
});
57+
const properties = Object.entries(itm.properties).map(
58+
([k, prop]) => {
59+
return entry(k, prop.value);
60+
},
61+
);
6062

6163
entries = html`
6264
${header("Basic properties")}

src/viewers/board/painter.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,55 @@ class FpTextPainter extends BoardItemPainter {
645645
}
646646
}
647647

648+
class PropertyTextPainter extends BoardItemPainter {
649+
classes = [board_items.SymbolProperty];
650+
651+
layers_for(t: board_items.SymbolProperty) {
652+
return [t.layer];
653+
}
654+
655+
paint(layer: ViewLayer, t: board_items.SymbolProperty) {
656+
if (this.filter_net) return;
657+
658+
if (t.hide || !t.shown_text || !t.has_symbol_prop) {
659+
return;
660+
}
661+
662+
const edatext = new EDAText(t.shown_text);
663+
664+
edatext.apply_effects(t.effects);
665+
edatext.apply_at(t.at);
666+
667+
edatext.attributes.color = layer.color;
668+
669+
// Looks like the rotation angle for KiCad's symbol attribute rendering
670+
// is standalone, so we need to sub it from parent's rotation angle.
671+
if (t.parent) {
672+
const rot = t.parent.at.rotation;
673+
edatext.text_angle.degrees -= rot;
674+
}
675+
676+
// keep the text upright if needed
677+
if (edatext.attributes.keep_upright) {
678+
while (edatext.text_angle.degrees > 90) {
679+
edatext.text_angle.degrees -= 180;
680+
}
681+
while (edatext.text_angle.degrees <= -90) {
682+
edatext.text_angle.degrees += 180;
683+
}
684+
}
685+
686+
this.gfx.state.push();
687+
StrokeFont.default().draw(
688+
this.gfx,
689+
edatext.shown_text,
690+
edatext.text_pos,
691+
edatext.attributes,
692+
);
693+
this.gfx.state.pop();
694+
}
695+
}
696+
648697
class DimensionPainter extends BoardItemPainter {
649698
classes = [board_items.Dimension];
650699

@@ -984,6 +1033,7 @@ export class BoardPainter extends DocumentPainter {
9841033
new FootprintPainter(this, gfx),
9851034
new GrTextPainter(this, gfx),
9861035
new FpTextPainter(this, gfx),
1036+
new PropertyTextPainter(this, gfx),
9871037
new DimensionPainter(this, gfx),
9881038
];
9891039
}

test/kicad/board.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import vias_pcb_src from "./files/vias.kicad_pcb";
2121
import footprint_graphics_pcb_src from "./files/footprint-graphics.kicad_pcb";
2222
import footprint_pads_pcb_src from "./files/footprint-pads.kicad_pcb";
2323
import footprint_text_locked_pcb_src from "./files/footprint-text-locked.kicad_pcb";
24+
import footprint_symbol_properties_pcb_src from "./files/footprint-properties.kicad_pcb";
2425
import { first } from "../../src/base/iterator";
2526

2627
suite("kicad.board.KicadPCB(): board parsing", function () {
@@ -1045,4 +1046,43 @@ suite("kicad.board.Footprint()", function () {
10451046
clearance: 0.5,
10461047
} as Partial<board.Pad>);
10471048
});
1049+
1050+
test("with footprint symbol properties", function () {
1051+
const pcb = new board.KicadPCB(
1052+
"test.kicad_pcb",
1053+
footprint_symbol_properties_pcb_src,
1054+
);
1055+
assert.equal(pcb.footprints.length, 1);
1056+
1057+
const fp0 = pcb.footprints[0]!;
1058+
1059+
assert.isDefined(fp0.properties["Reference"]);
1060+
const ref_prop = fp0.properties["Reference"]!;
1061+
1062+
assert.deepInclude(ref_prop, {
1063+
has_symbol_prop: true,
1064+
value: "C1",
1065+
id: 0,
1066+
unlocked: false,
1067+
hide: false,
1068+
at: { position: { x: 0, y: -1.43 }, rotation: 0, unlocked: false },
1069+
layer: "F.SilkS",
1070+
effects: {
1071+
font: {
1072+
face: undefined,
1073+
size: { x: 1, y: 1 },
1074+
thickness: 0.15,
1075+
italic: false,
1076+
bold: false,
1077+
color: { r: 0, g: 0, b: 0, a: 0 },
1078+
},
1079+
justify: {
1080+
horizontal: "center",
1081+
vertical: "center",
1082+
mirror: false,
1083+
},
1084+
hide: false,
1085+
},
1086+
} as Partial<board.SymbolProperty>);
1087+
});
10481088
});

0 commit comments

Comments
 (0)