kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +5 -0
- kicad_sch_api/core/components.py +142 -47
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +22 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +28 -52
- kicad_sch_api/core/managers/file_io.py +3 -2
- kicad_sch_api/core/managers/metadata.py +6 -5
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +7 -1
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +29 -53
- kicad_sch_api/core/parser.py +75 -1765
- kicad_sch_api/core/schematic.py +211 -148
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +59 -18
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +194 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -222
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hierarchical sheet elements parser for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
Handles parsing and serialization of Hierarchical sheet elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
import sexpdata
|
|
11
|
+
|
|
12
|
+
from ...core.config import config
|
|
13
|
+
from ..base import BaseElementParser
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SheetParser(BaseElementParser):
|
|
19
|
+
"""Parser for Hierarchical sheet elements."""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize sheet parser."""
|
|
23
|
+
super().__init__("sheet")
|
|
24
|
+
|
|
25
|
+
def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
26
|
+
"""Parse a hierarchical sheet."""
|
|
27
|
+
# Complex format with position, size, properties, pins, instances
|
|
28
|
+
sheet_data = {
|
|
29
|
+
"position": {"x": 0, "y": 0},
|
|
30
|
+
"size": {"width": 0, "height": 0},
|
|
31
|
+
"exclude_from_sim": False,
|
|
32
|
+
"in_bom": True,
|
|
33
|
+
"on_board": True,
|
|
34
|
+
"dnp": False,
|
|
35
|
+
"fields_autoplaced": True,
|
|
36
|
+
"stroke_width": 0.1524,
|
|
37
|
+
"stroke_type": "solid",
|
|
38
|
+
"fill_color": (0, 0, 0, 0.0),
|
|
39
|
+
"uuid": None,
|
|
40
|
+
"name": "Sheet",
|
|
41
|
+
"filename": "sheet.kicad_sch",
|
|
42
|
+
"pins": [],
|
|
43
|
+
"project_name": "",
|
|
44
|
+
"page_number": "2",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for elem in item[1:]:
|
|
48
|
+
if not isinstance(elem, list):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
52
|
+
|
|
53
|
+
if elem_type == "at":
|
|
54
|
+
if len(elem) >= 3:
|
|
55
|
+
sheet_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
56
|
+
elif elem_type == "size":
|
|
57
|
+
if len(elem) >= 3:
|
|
58
|
+
sheet_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
|
|
59
|
+
elif elem_type == "exclude_from_sim":
|
|
60
|
+
sheet_data["exclude_from_sim"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
61
|
+
elif elem_type == "in_bom":
|
|
62
|
+
sheet_data["in_bom"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
63
|
+
elif elem_type == "on_board":
|
|
64
|
+
sheet_data["on_board"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
65
|
+
elif elem_type == "dnp":
|
|
66
|
+
sheet_data["dnp"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
67
|
+
elif elem_type == "fields_autoplaced":
|
|
68
|
+
sheet_data["fields_autoplaced"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
69
|
+
elif elem_type == "stroke":
|
|
70
|
+
for stroke_elem in elem[1:]:
|
|
71
|
+
if isinstance(stroke_elem, list):
|
|
72
|
+
stroke_type = str(stroke_elem[0])
|
|
73
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
74
|
+
sheet_data["stroke_width"] = float(stroke_elem[1])
|
|
75
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
76
|
+
sheet_data["stroke_type"] = str(stroke_elem[1])
|
|
77
|
+
elif elem_type == "fill":
|
|
78
|
+
for fill_elem in elem[1:]:
|
|
79
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
|
|
80
|
+
if len(fill_elem) >= 5:
|
|
81
|
+
sheet_data["fill_color"] = (
|
|
82
|
+
int(fill_elem[1]),
|
|
83
|
+
int(fill_elem[2]),
|
|
84
|
+
int(fill_elem[3]),
|
|
85
|
+
float(fill_elem[4]),
|
|
86
|
+
)
|
|
87
|
+
elif elem_type == "uuid":
|
|
88
|
+
sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
89
|
+
elif elem_type == "property":
|
|
90
|
+
if len(elem) >= 3:
|
|
91
|
+
prop_name = str(elem[1])
|
|
92
|
+
prop_value = str(elem[2])
|
|
93
|
+
if prop_name == "Sheetname":
|
|
94
|
+
sheet_data["name"] = prop_value
|
|
95
|
+
elif prop_name == "Sheetfile":
|
|
96
|
+
sheet_data["filename"] = prop_value
|
|
97
|
+
elif elem_type == "pin":
|
|
98
|
+
# Parse sheet pin - reuse existing _parse_sheet_pin helper
|
|
99
|
+
pin_data = self._parse_sheet_pin_for_read(elem)
|
|
100
|
+
if pin_data:
|
|
101
|
+
sheet_data["pins"].append(pin_data)
|
|
102
|
+
elif elem_type == "instances":
|
|
103
|
+
# Parse instances for project name and page number
|
|
104
|
+
for inst_elem in elem[1:]:
|
|
105
|
+
if isinstance(inst_elem, list) and str(inst_elem[0]) == "project":
|
|
106
|
+
if len(inst_elem) >= 2:
|
|
107
|
+
sheet_data["project_name"] = str(inst_elem[1])
|
|
108
|
+
for path_elem in inst_elem[2:]:
|
|
109
|
+
if isinstance(path_elem, list) and str(path_elem[0]) == "path":
|
|
110
|
+
for page_elem in path_elem[1:]:
|
|
111
|
+
if isinstance(page_elem, list) and str(page_elem[0]) == "page":
|
|
112
|
+
sheet_data["page_number"] = (
|
|
113
|
+
str(page_elem[1]) if len(page_elem) > 1 else "2"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return sheet_data
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
120
|
+
"""Parse a sheet pin (for reading during sheet parsing)."""
|
|
121
|
+
# Format: (pin "name" type (at x y rotation) (uuid ...) (effects ...))
|
|
122
|
+
if len(item) < 3:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
pin_data = {
|
|
126
|
+
"name": str(item[1]),
|
|
127
|
+
"pin_type": str(item[2]) if len(item) > 2 else "input",
|
|
128
|
+
"position": {"x": 0, "y": 0},
|
|
129
|
+
"rotation": 0,
|
|
130
|
+
"size": config.defaults.font_size,
|
|
131
|
+
"justify": "right",
|
|
132
|
+
"uuid": None,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for elem in item[3:]:
|
|
136
|
+
if not isinstance(elem, list):
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
140
|
+
|
|
141
|
+
if elem_type == "at":
|
|
142
|
+
if len(elem) >= 3:
|
|
143
|
+
pin_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
144
|
+
if len(elem) >= 4:
|
|
145
|
+
pin_data["rotation"] = float(elem[3])
|
|
146
|
+
elif elem_type == "uuid":
|
|
147
|
+
pin_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
148
|
+
elif elem_type == "effects":
|
|
149
|
+
for effect_elem in elem[1:]:
|
|
150
|
+
if isinstance(effect_elem, list):
|
|
151
|
+
effect_type = str(effect_elem[0])
|
|
152
|
+
if effect_type == "font":
|
|
153
|
+
for font_elem in effect_elem[1:]:
|
|
154
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
155
|
+
if len(font_elem) >= 2:
|
|
156
|
+
pin_data["size"] = float(font_elem[1])
|
|
157
|
+
elif effect_type == "justify":
|
|
158
|
+
if len(effect_elem) >= 2:
|
|
159
|
+
pin_data["justify"] = str(effect_elem[1])
|
|
160
|
+
|
|
161
|
+
return pin_data
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
|
|
165
|
+
"""Parse sheet_instances section."""
|
|
166
|
+
sheet_instances = []
|
|
167
|
+
for sheet_item in item[1:]: # Skip 'sheet_instances' header
|
|
168
|
+
if isinstance(sheet_item, list) and len(sheet_item) > 0:
|
|
169
|
+
sheet_data = {"path": "/", "page": "1"}
|
|
170
|
+
for element in sheet_item[1:]: # Skip element header
|
|
171
|
+
if isinstance(element, list) and len(element) >= 2:
|
|
172
|
+
key = (
|
|
173
|
+
str(element[0])
|
|
174
|
+
if isinstance(element[0], sexpdata.Symbol)
|
|
175
|
+
else str(element[0])
|
|
176
|
+
)
|
|
177
|
+
if key == "path":
|
|
178
|
+
sheet_data["path"] = element[1]
|
|
179
|
+
elif key == "page":
|
|
180
|
+
sheet_data["page"] = element[1]
|
|
181
|
+
sheet_instances.append(sheet_data)
|
|
182
|
+
return sheet_instances
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
|
|
186
|
+
"""Convert hierarchical sheet to S-expression."""
|
|
187
|
+
sexp = [sexpdata.Symbol("sheet")]
|
|
188
|
+
|
|
189
|
+
# Add position
|
|
190
|
+
pos = sheet_data["position"]
|
|
191
|
+
x, y = pos["x"], pos["y"]
|
|
192
|
+
if isinstance(x, float) and x.is_integer():
|
|
193
|
+
x = int(x)
|
|
194
|
+
if isinstance(y, float) and y.is_integer():
|
|
195
|
+
y = int(y)
|
|
196
|
+
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
197
|
+
|
|
198
|
+
# Add size
|
|
199
|
+
size = sheet_data["size"]
|
|
200
|
+
w, h = size["width"], size["height"]
|
|
201
|
+
sexp.append([sexpdata.Symbol("size"), w, h])
|
|
202
|
+
|
|
203
|
+
# Add basic properties
|
|
204
|
+
sexp.append(
|
|
205
|
+
[
|
|
206
|
+
sexpdata.Symbol("exclude_from_sim"),
|
|
207
|
+
sexpdata.Symbol("yes" if sheet_data.get("exclude_from_sim", False) else "no"),
|
|
208
|
+
]
|
|
209
|
+
)
|
|
210
|
+
sexp.append(
|
|
211
|
+
[
|
|
212
|
+
sexpdata.Symbol("in_bom"),
|
|
213
|
+
sexpdata.Symbol("yes" if sheet_data.get("in_bom", True) else "no"),
|
|
214
|
+
]
|
|
215
|
+
)
|
|
216
|
+
sexp.append(
|
|
217
|
+
[
|
|
218
|
+
sexpdata.Symbol("on_board"),
|
|
219
|
+
sexpdata.Symbol("yes" if sheet_data.get("on_board", True) else "no"),
|
|
220
|
+
]
|
|
221
|
+
)
|
|
222
|
+
sexp.append(
|
|
223
|
+
[
|
|
224
|
+
sexpdata.Symbol("dnp"),
|
|
225
|
+
sexpdata.Symbol("yes" if sheet_data.get("dnp", False) else "no"),
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
sexp.append(
|
|
229
|
+
[
|
|
230
|
+
sexpdata.Symbol("fields_autoplaced"),
|
|
231
|
+
sexpdata.Symbol("yes" if sheet_data.get("fields_autoplaced", True) else "no"),
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Add stroke
|
|
236
|
+
stroke_width = sheet_data.get("stroke_width", 0.1524)
|
|
237
|
+
stroke_type = sheet_data.get("stroke_type", "solid")
|
|
238
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
239
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
240
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
241
|
+
sexp.append(stroke_sexp)
|
|
242
|
+
|
|
243
|
+
# Add fill
|
|
244
|
+
fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
|
|
245
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
246
|
+
fill_sexp.append(
|
|
247
|
+
[sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]]
|
|
248
|
+
)
|
|
249
|
+
sexp.append(fill_sexp)
|
|
250
|
+
|
|
251
|
+
# Add UUID
|
|
252
|
+
if "uuid" in sheet_data:
|
|
253
|
+
sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
|
|
254
|
+
|
|
255
|
+
# Add sheet properties (name and filename)
|
|
256
|
+
name = sheet_data.get("name", "Sheet")
|
|
257
|
+
filename = sheet_data.get("filename", "sheet.kicad_sch")
|
|
258
|
+
|
|
259
|
+
# Sheetname property
|
|
260
|
+
from ...core.config import config
|
|
261
|
+
|
|
262
|
+
name_prop = [sexpdata.Symbol("property"), "Sheetname", name]
|
|
263
|
+
name_prop.append(
|
|
264
|
+
[sexpdata.Symbol("at"), x, round(y + config.sheet.name_offset_y, 4), 0]
|
|
265
|
+
) # Above sheet
|
|
266
|
+
name_prop.append(
|
|
267
|
+
[
|
|
268
|
+
sexpdata.Symbol("effects"),
|
|
269
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), config.defaults.font_size, config.defaults.font_size]],
|
|
270
|
+
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")],
|
|
271
|
+
]
|
|
272
|
+
)
|
|
273
|
+
sexp.append(name_prop)
|
|
274
|
+
|
|
275
|
+
# Sheetfile property
|
|
276
|
+
file_prop = [sexpdata.Symbol("property"), "Sheetfile", filename]
|
|
277
|
+
file_prop.append(
|
|
278
|
+
[sexpdata.Symbol("at"), x, round(y + h + config.sheet.file_offset_y, 4), 0]
|
|
279
|
+
) # Below sheet
|
|
280
|
+
file_prop.append(
|
|
281
|
+
[
|
|
282
|
+
sexpdata.Symbol("effects"),
|
|
283
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), config.defaults.font_size, config.defaults.font_size]],
|
|
284
|
+
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("top")],
|
|
285
|
+
]
|
|
286
|
+
)
|
|
287
|
+
sexp.append(file_prop)
|
|
288
|
+
|
|
289
|
+
# Add sheet pins if any
|
|
290
|
+
for pin in sheet_data.get("pins", []):
|
|
291
|
+
pin_sexp = self._sheet_pin_to_sexp(pin)
|
|
292
|
+
sexp.append(pin_sexp)
|
|
293
|
+
|
|
294
|
+
# Add instances
|
|
295
|
+
if schematic_uuid:
|
|
296
|
+
instances_sexp = [sexpdata.Symbol("instances")]
|
|
297
|
+
project_name = sheet_data.get("project_name", "")
|
|
298
|
+
page_number = sheet_data.get("page_number", "2")
|
|
299
|
+
project_sexp = [sexpdata.Symbol("project"), project_name]
|
|
300
|
+
path_sexp = [sexpdata.Symbol("path"), f"/{schematic_uuid}"]
|
|
301
|
+
path_sexp.append([sexpdata.Symbol("page"), page_number])
|
|
302
|
+
project_sexp.append(path_sexp)
|
|
303
|
+
instances_sexp.append(project_sexp)
|
|
304
|
+
sexp.append(instances_sexp)
|
|
305
|
+
|
|
306
|
+
return sexp
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
|
|
310
|
+
"""Convert sheet pin to S-expression."""
|
|
311
|
+
pin_sexp = [
|
|
312
|
+
sexpdata.Symbol("pin"),
|
|
313
|
+
pin_data["name"],
|
|
314
|
+
sexpdata.Symbol(pin_data.get("pin_type", "input")),
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
# Add position
|
|
318
|
+
pos = pin_data["position"]
|
|
319
|
+
x, y = pos["x"], pos["y"]
|
|
320
|
+
rotation = pin_data.get("rotation", 0)
|
|
321
|
+
pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
322
|
+
|
|
323
|
+
# Add UUID
|
|
324
|
+
if "uuid" in pin_data:
|
|
325
|
+
pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
|
|
326
|
+
|
|
327
|
+
# Add effects
|
|
328
|
+
size = pin_data.get("size", config.defaults.font_size)
|
|
329
|
+
effects = [sexpdata.Symbol("effects")]
|
|
330
|
+
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
331
|
+
effects.append(font)
|
|
332
|
+
justify = pin_data.get("justify", "right")
|
|
333
|
+
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
334
|
+
pin_sexp.append(effects)
|
|
335
|
+
|
|
336
|
+
return pin_sexp
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _sheet_instances_to_sexp(self, sheet_instances: List[Dict[str, Any]]) -> List[Any]:
|
|
340
|
+
"""Convert sheet_instances to S-expression."""
|
|
341
|
+
sexp = [sexpdata.Symbol("sheet_instances")]
|
|
342
|
+
for sheet in sheet_instances:
|
|
343
|
+
# Create: (path "/" (page "1"))
|
|
344
|
+
sheet_sexp = [
|
|
345
|
+
sexpdata.Symbol("path"),
|
|
346
|
+
sheet.get("path", "/"),
|
|
347
|
+
[sexpdata.Symbol("page"), str(sheet.get("page", "1"))],
|
|
348
|
+
]
|
|
349
|
+
sexp.append(sheet_sexp)
|
|
350
|
+
return sexp
|
|
351
|
+
|
|
352
|
+
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component symbol elements parser for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
Handles parsing and serialization of Component symbol elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import sexpdata
|
|
12
|
+
|
|
13
|
+
from ...core.types import Point
|
|
14
|
+
from ..base import BaseElementParser
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SymbolParser(BaseElementParser):
|
|
20
|
+
"""Parser for Component symbol elements."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""Initialize symbol parser."""
|
|
24
|
+
super().__init__("symbol")
|
|
25
|
+
|
|
26
|
+
def _parse_symbol(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
27
|
+
"""Parse a symbol (component) definition."""
|
|
28
|
+
try:
|
|
29
|
+
symbol_data = {
|
|
30
|
+
"lib_id": None,
|
|
31
|
+
"position": Point(0, 0),
|
|
32
|
+
"rotation": 0,
|
|
33
|
+
"uuid": None,
|
|
34
|
+
"reference": None,
|
|
35
|
+
"value": None,
|
|
36
|
+
"footprint": None,
|
|
37
|
+
"properties": {},
|
|
38
|
+
"pins": [],
|
|
39
|
+
"in_bom": True,
|
|
40
|
+
"on_board": True,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for sub_item in item[1:]:
|
|
44
|
+
if not isinstance(sub_item, list) or len(sub_item) == 0:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
element_type = (
|
|
48
|
+
str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if element_type == "lib_id":
|
|
52
|
+
symbol_data["lib_id"] = sub_item[1] if len(sub_item) > 1 else None
|
|
53
|
+
elif element_type == "at":
|
|
54
|
+
if len(sub_item) >= 3:
|
|
55
|
+
symbol_data["position"] = Point(float(sub_item[1]), float(sub_item[2]))
|
|
56
|
+
if len(sub_item) > 3:
|
|
57
|
+
symbol_data["rotation"] = float(sub_item[3])
|
|
58
|
+
elif element_type == "uuid":
|
|
59
|
+
symbol_data["uuid"] = sub_item[1] if len(sub_item) > 1 else None
|
|
60
|
+
elif element_type == "property":
|
|
61
|
+
prop_data = self._parse_property(sub_item)
|
|
62
|
+
if prop_data:
|
|
63
|
+
prop_name = prop_data.get("name")
|
|
64
|
+
if prop_name == "Reference":
|
|
65
|
+
symbol_data["reference"] = prop_data.get("value")
|
|
66
|
+
elif prop_name == "Value":
|
|
67
|
+
symbol_data["value"] = prop_data.get("value")
|
|
68
|
+
elif prop_name == "Footprint":
|
|
69
|
+
symbol_data["footprint"] = prop_data.get("value")
|
|
70
|
+
else:
|
|
71
|
+
# Unescape quotes in property values when loading
|
|
72
|
+
prop_value = prop_data.get("value")
|
|
73
|
+
if prop_value:
|
|
74
|
+
prop_value = str(prop_value).replace('\\"', '"')
|
|
75
|
+
symbol_data["properties"][prop_name] = prop_value
|
|
76
|
+
elif element_type == "in_bom":
|
|
77
|
+
symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
|
|
78
|
+
elif element_type == "on_board":
|
|
79
|
+
symbol_data["on_board"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
|
|
80
|
+
|
|
81
|
+
return symbol_data
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(f"Error parsing symbol: {e}")
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _parse_property(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
89
|
+
"""Parse a property definition."""
|
|
90
|
+
if len(item) < 3:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"name": item[1] if len(item) > 1 else None,
|
|
95
|
+
"value": item[2] if len(item) > 2 else None,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
|
|
100
|
+
"""Convert symbol to S-expression."""
|
|
101
|
+
sexp = [sexpdata.Symbol("symbol")]
|
|
102
|
+
|
|
103
|
+
if symbol_data.get("lib_id"):
|
|
104
|
+
sexp.append([sexpdata.Symbol("lib_id"), symbol_data["lib_id"]])
|
|
105
|
+
|
|
106
|
+
# Add position and rotation (preserve original format)
|
|
107
|
+
pos = symbol_data.get("position", Point(0, 0))
|
|
108
|
+
rotation = symbol_data.get("rotation", 0)
|
|
109
|
+
# Format numbers as integers if they are whole numbers
|
|
110
|
+
x = int(pos.x) if pos.x == int(pos.x) else pos.x
|
|
111
|
+
y = int(pos.y) if pos.y == int(pos.y) else pos.y
|
|
112
|
+
r = int(rotation) if rotation == int(rotation) else rotation
|
|
113
|
+
# Always include rotation for format consistency with KiCAD
|
|
114
|
+
sexp.append([sexpdata.Symbol("at"), x, y, r])
|
|
115
|
+
|
|
116
|
+
# Add unit (required by KiCAD)
|
|
117
|
+
unit = symbol_data.get("unit", 1)
|
|
118
|
+
sexp.append([sexpdata.Symbol("unit"), unit])
|
|
119
|
+
|
|
120
|
+
# Add simulation and board settings (required by KiCAD)
|
|
121
|
+
sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
|
|
122
|
+
sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
|
|
123
|
+
sexp.append(
|
|
124
|
+
[sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
|
|
125
|
+
)
|
|
126
|
+
sexp.append([sexpdata.Symbol("dnp"), "no"])
|
|
127
|
+
sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
|
|
128
|
+
|
|
129
|
+
if symbol_data.get("uuid"):
|
|
130
|
+
sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
|
|
131
|
+
|
|
132
|
+
# Add properties with proper positioning and effects
|
|
133
|
+
lib_id = symbol_data.get("lib_id", "")
|
|
134
|
+
is_power_symbol = "power:" in lib_id
|
|
135
|
+
|
|
136
|
+
if symbol_data.get("reference"):
|
|
137
|
+
# Power symbol references should be hidden by default
|
|
138
|
+
ref_hide = is_power_symbol
|
|
139
|
+
ref_prop = self._create_property_with_positioning(
|
|
140
|
+
"Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
|
|
141
|
+
)
|
|
142
|
+
sexp.append(ref_prop)
|
|
143
|
+
|
|
144
|
+
if symbol_data.get("value"):
|
|
145
|
+
# Power symbol values need different positioning
|
|
146
|
+
if is_power_symbol:
|
|
147
|
+
val_prop = self._create_power_symbol_value_property(
|
|
148
|
+
symbol_data["value"], pos, lib_id
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
val_prop = self._create_property_with_positioning(
|
|
152
|
+
"Value", symbol_data["value"], pos, 1, "left"
|
|
153
|
+
)
|
|
154
|
+
sexp.append(val_prop)
|
|
155
|
+
|
|
156
|
+
footprint = symbol_data.get("footprint")
|
|
157
|
+
if footprint is not None: # Include empty strings but not None
|
|
158
|
+
fp_prop = self._create_property_with_positioning(
|
|
159
|
+
"Footprint", footprint, pos, 2, "left", hide=True
|
|
160
|
+
)
|
|
161
|
+
sexp.append(fp_prop)
|
|
162
|
+
|
|
163
|
+
for prop_name, prop_value in symbol_data.get("properties", {}).items():
|
|
164
|
+
escaped_value = str(prop_value).replace('"', '\\"')
|
|
165
|
+
prop = self._create_property_with_positioning(
|
|
166
|
+
prop_name, escaped_value, pos, 3, "left", hide=True
|
|
167
|
+
)
|
|
168
|
+
sexp.append(prop)
|
|
169
|
+
|
|
170
|
+
# Add pin UUID assignments (required by KiCAD)
|
|
171
|
+
for pin in symbol_data.get("pins", []):
|
|
172
|
+
pin_uuid = str(uuid.uuid4())
|
|
173
|
+
# Ensure pin number is a string for proper quoting
|
|
174
|
+
pin_number = str(pin.number)
|
|
175
|
+
sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
|
|
176
|
+
|
|
177
|
+
# Add instances section (required by KiCAD)
|
|
178
|
+
from ...core.config import config
|
|
179
|
+
|
|
180
|
+
# Get project name from config or properties
|
|
181
|
+
project_name = symbol_data.get("properties", {}).get("project_name")
|
|
182
|
+
if not project_name:
|
|
183
|
+
project_name = getattr(self, "project_name", config.defaults.project_name)
|
|
184
|
+
|
|
185
|
+
# CRITICAL FIX: Use the FULL hierarchy_path from properties if available
|
|
186
|
+
# For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
|
|
187
|
+
# This ensures KiCad can properly annotate components in sub-sheets
|
|
188
|
+
hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
|
|
189
|
+
if hierarchy_path:
|
|
190
|
+
# Use the full hierarchical path (includes root + all sheet symbols)
|
|
191
|
+
instance_path = hierarchy_path
|
|
192
|
+
logger.debug(
|
|
193
|
+
f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
# Fallback: use root_uuid or schematic_uuid for flat designs
|
|
197
|
+
root_uuid = (
|
|
198
|
+
symbol_data.get("properties", {}).get("root_uuid")
|
|
199
|
+
or schematic_uuid
|
|
200
|
+
or str(uuid.uuid4())
|
|
201
|
+
)
|
|
202
|
+
instance_path = f"/{root_uuid}"
|
|
203
|
+
logger.debug(
|
|
204
|
+
f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
|
|
209
|
+
)
|
|
210
|
+
logger.debug(f"🔧 Using project name: '{project_name}'")
|
|
211
|
+
|
|
212
|
+
sexp.append(
|
|
213
|
+
[
|
|
214
|
+
sexpdata.Symbol("instances"),
|
|
215
|
+
[
|
|
216
|
+
sexpdata.Symbol("project"),
|
|
217
|
+
project_name,
|
|
218
|
+
[
|
|
219
|
+
sexpdata.Symbol("path"),
|
|
220
|
+
instance_path,
|
|
221
|
+
[sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
|
|
222
|
+
[sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
|
|
223
|
+
],
|
|
224
|
+
],
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return sexp
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _create_property_with_positioning(
|
|
232
|
+
self,
|
|
233
|
+
prop_name: str,
|
|
234
|
+
prop_value: str,
|
|
235
|
+
component_pos: Point,
|
|
236
|
+
offset_index: int,
|
|
237
|
+
justify: str = "left",
|
|
238
|
+
hide: bool = False,
|
|
239
|
+
) -> List[Any]:
|
|
240
|
+
"""Create a property with proper positioning and effects like KiCAD."""
|
|
241
|
+
from ...core.config import config
|
|
242
|
+
|
|
243
|
+
# Calculate property position using configuration
|
|
244
|
+
prop_x, prop_y, rotation = config.get_property_position(
|
|
245
|
+
prop_name, (component_pos.x, component_pos.y), offset_index
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Build effects section based on hide status
|
|
249
|
+
effects = [
|
|
250
|
+
sexpdata.Symbol("effects"),
|
|
251
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
# Only add justify for visible properties or Reference/Value
|
|
255
|
+
if not hide or prop_name in ["Reference", "Value"]:
|
|
256
|
+
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
257
|
+
|
|
258
|
+
if hide:
|
|
259
|
+
effects.append([sexpdata.Symbol("hide"), sexpdata.Symbol("yes")])
|
|
260
|
+
|
|
261
|
+
prop_sexp = [
|
|
262
|
+
sexpdata.Symbol("property"),
|
|
263
|
+
prop_name,
|
|
264
|
+
prop_value,
|
|
265
|
+
[
|
|
266
|
+
sexpdata.Symbol("at"),
|
|
267
|
+
round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
|
|
268
|
+
round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
|
|
269
|
+
rotation,
|
|
270
|
+
],
|
|
271
|
+
effects,
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
return prop_sexp
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _create_power_symbol_value_property(
|
|
278
|
+
self, value: str, component_pos: Point, lib_id: str
|
|
279
|
+
) -> List[Any]:
|
|
280
|
+
"""Create Value property for power symbols with correct positioning."""
|
|
281
|
+
# Power symbols have different value positioning based on type
|
|
282
|
+
if "GND" in lib_id:
|
|
283
|
+
# GND value goes below the symbol
|
|
284
|
+
prop_x = component_pos.x
|
|
285
|
+
prop_y = component_pos.y + 5.08 # Below GND symbol
|
|
286
|
+
elif "+3.3V" in lib_id or "VDD" in lib_id:
|
|
287
|
+
# Positive voltage values go below the symbol
|
|
288
|
+
prop_x = component_pos.x
|
|
289
|
+
prop_y = component_pos.y - 5.08 # Above symbol (negative offset)
|
|
290
|
+
else:
|
|
291
|
+
# Default power symbol positioning
|
|
292
|
+
prop_x = component_pos.x
|
|
293
|
+
prop_y = component_pos.y + 3.556
|
|
294
|
+
|
|
295
|
+
prop_sexp = [
|
|
296
|
+
sexpdata.Symbol("property"),
|
|
297
|
+
"Value",
|
|
298
|
+
value,
|
|
299
|
+
[
|
|
300
|
+
sexpdata.Symbol("at"),
|
|
301
|
+
round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
|
|
302
|
+
round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
|
|
303
|
+
0,
|
|
304
|
+
],
|
|
305
|
+
[
|
|
306
|
+
sexpdata.Symbol("effects"),
|
|
307
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
308
|
+
],
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
return prop_sexp
|
|
312
|
+
|
|
313
|
+
|