kicad-sch-api 0.3.0__py3-none-any.whl → 0.5.1__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.
- kicad_sch_api/__init__.py +68 -3
- 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/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- 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 +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- 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 +216 -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 +485 -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/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- 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/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -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-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
kicad_sch_api/core/parser.py
CHANGED
|
@@ -12,6 +12,15 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
12
12
|
|
|
13
13
|
import sexpdata
|
|
14
14
|
|
|
15
|
+
from ..parsers.elements.graphics_parser import GraphicsParser
|
|
16
|
+
from ..parsers.elements.label_parser import LabelParser
|
|
17
|
+
from ..parsers.elements.library_parser import LibraryParser
|
|
18
|
+
from ..parsers.elements.metadata_parser import MetadataParser
|
|
19
|
+
from ..parsers.elements.sheet_parser import SheetParser
|
|
20
|
+
from ..parsers.elements.symbol_parser import SymbolParser
|
|
21
|
+
from ..parsers.elements.text_parser import TextParser
|
|
22
|
+
from ..parsers.elements.wire_parser import WireParser
|
|
23
|
+
from ..parsers.utils import color_to_rgb255, color_to_rgba
|
|
15
24
|
from ..utils.validation import ValidationError, ValidationIssue
|
|
16
25
|
from .formatter import ExactFormatter
|
|
17
26
|
from .types import Junction, Label, Net, Point, SchematicSymbol, Wire
|
|
@@ -40,8 +49,30 @@ class SExpressionParser:
|
|
|
40
49
|
self.preserve_format = preserve_format
|
|
41
50
|
self._formatter = ExactFormatter() if preserve_format else None
|
|
42
51
|
self._validation_issues = []
|
|
52
|
+
self._graphics_parser = GraphicsParser()
|
|
53
|
+
self._wire_parser = WireParser()
|
|
54
|
+
self._label_parser = LabelParser()
|
|
55
|
+
self._text_parser = TextParser()
|
|
56
|
+
self._sheet_parser = SheetParser()
|
|
57
|
+
self._library_parser = LibraryParser()
|
|
58
|
+
self._symbol_parser = SymbolParser()
|
|
59
|
+
self._metadata_parser = MetadataParser()
|
|
60
|
+
self._project_name = None
|
|
43
61
|
logger.info(f"S-expression parser initialized (format preservation: {preserve_format})")
|
|
44
62
|
|
|
63
|
+
@property
|
|
64
|
+
def project_name(self):
|
|
65
|
+
"""Get project name."""
|
|
66
|
+
return self._project_name
|
|
67
|
+
|
|
68
|
+
@project_name.setter
|
|
69
|
+
def project_name(self, value):
|
|
70
|
+
"""Set project name on parser and propagate to sub-parsers."""
|
|
71
|
+
self._project_name = value
|
|
72
|
+
# Propagate to symbol parser which needs it for instances
|
|
73
|
+
if hasattr(self, '_symbol_parser'):
|
|
74
|
+
self._symbol_parser.project_name = value
|
|
75
|
+
|
|
45
76
|
def parse_file(self, filepath: Union[str, Path]) -> Dict[str, Any]:
|
|
46
77
|
"""
|
|
47
78
|
Parse a KiCAD schematic file with comprehensive validation.
|
|
@@ -187,7 +218,17 @@ class SExpressionParser:
|
|
|
187
218
|
"wires": [],
|
|
188
219
|
"junctions": [],
|
|
189
220
|
"labels": [],
|
|
221
|
+
"hierarchical_labels": [],
|
|
222
|
+
"no_connects": [],
|
|
223
|
+
"texts": [],
|
|
224
|
+
"text_boxes": [],
|
|
225
|
+
"sheets": [],
|
|
226
|
+
"polylines": [],
|
|
227
|
+
"arcs": [],
|
|
228
|
+
"circles": [],
|
|
229
|
+
"beziers": [],
|
|
190
230
|
"rectangles": [],
|
|
231
|
+
"images": [],
|
|
191
232
|
"nets": [],
|
|
192
233
|
"lib_symbols": {},
|
|
193
234
|
"sheet_instances": [],
|
|
@@ -233,10 +274,50 @@ class SExpressionParser:
|
|
|
233
274
|
label = self._parse_label(item)
|
|
234
275
|
if label:
|
|
235
276
|
schematic_data["labels"].append(label)
|
|
277
|
+
elif element_type == "hierarchical_label":
|
|
278
|
+
hlabel = self._parse_hierarchical_label(item)
|
|
279
|
+
if hlabel:
|
|
280
|
+
schematic_data["hierarchical_labels"].append(hlabel)
|
|
281
|
+
elif element_type == "no_connect":
|
|
282
|
+
no_connect = self._parse_no_connect(item)
|
|
283
|
+
if no_connect:
|
|
284
|
+
schematic_data["no_connects"].append(no_connect)
|
|
285
|
+
elif element_type == "text":
|
|
286
|
+
text = self._parse_text(item)
|
|
287
|
+
if text:
|
|
288
|
+
schematic_data["texts"].append(text)
|
|
289
|
+
elif element_type == "text_box":
|
|
290
|
+
text_box = self._parse_text_box(item)
|
|
291
|
+
if text_box:
|
|
292
|
+
schematic_data["text_boxes"].append(text_box)
|
|
293
|
+
elif element_type == "sheet":
|
|
294
|
+
sheet = self._parse_sheet(item)
|
|
295
|
+
if sheet:
|
|
296
|
+
schematic_data["sheets"].append(sheet)
|
|
297
|
+
elif element_type == "polyline":
|
|
298
|
+
polyline = self._parse_polyline(item)
|
|
299
|
+
if polyline:
|
|
300
|
+
schematic_data["polylines"].append(polyline)
|
|
301
|
+
elif element_type == "arc":
|
|
302
|
+
arc = self._parse_arc(item)
|
|
303
|
+
if arc:
|
|
304
|
+
schematic_data["arcs"].append(arc)
|
|
305
|
+
elif element_type == "circle":
|
|
306
|
+
circle = self._parse_circle(item)
|
|
307
|
+
if circle:
|
|
308
|
+
schematic_data["circles"].append(circle)
|
|
309
|
+
elif element_type == "bezier":
|
|
310
|
+
bezier = self._parse_bezier(item)
|
|
311
|
+
if bezier:
|
|
312
|
+
schematic_data["beziers"].append(bezier)
|
|
236
313
|
elif element_type == "rectangle":
|
|
237
314
|
rectangle = self._parse_rectangle(item)
|
|
238
315
|
if rectangle:
|
|
239
316
|
schematic_data["rectangles"].append(rectangle)
|
|
317
|
+
elif element_type == "image":
|
|
318
|
+
image = self._parse_image(item)
|
|
319
|
+
if image:
|
|
320
|
+
schematic_data["images"].append(image)
|
|
240
321
|
elif element_type == "lib_symbols":
|
|
241
322
|
schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
|
|
242
323
|
elif element_type == "sheet_instances":
|
|
@@ -297,25 +378,48 @@ class SExpressionParser:
|
|
|
297
378
|
for hlabel in schematic_data.get("hierarchical_labels", []):
|
|
298
379
|
sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
|
|
299
380
|
|
|
300
|
-
# Add
|
|
301
|
-
for
|
|
302
|
-
sexp_data.append(self.
|
|
381
|
+
# Add no_connects
|
|
382
|
+
for no_connect in schematic_data.get("no_connects", []):
|
|
383
|
+
sexp_data.append(self._no_connect_to_sexp(no_connect))
|
|
384
|
+
|
|
385
|
+
# Add graphical elements (in KiCad element order)
|
|
386
|
+
# Beziers
|
|
387
|
+
for bezier in schematic_data.get("beziers", []):
|
|
388
|
+
sexp_data.append(self._bezier_to_sexp(bezier))
|
|
389
|
+
|
|
390
|
+
# Rectangles (both from API and graphics)
|
|
391
|
+
for rectangle in schematic_data.get("rectangles", []):
|
|
392
|
+
sexp_data.append(self._rectangle_to_sexp(rectangle))
|
|
393
|
+
for graphic in schematic_data.get("graphics", []):
|
|
394
|
+
sexp_data.append(self._graphic_to_sexp(graphic))
|
|
303
395
|
|
|
304
|
-
#
|
|
396
|
+
# Images
|
|
397
|
+
for image in schematic_data.get("images", []):
|
|
398
|
+
sexp_data.append(self._image_to_sexp(image))
|
|
399
|
+
|
|
400
|
+
# Circles
|
|
401
|
+
for circle in schematic_data.get("circles", []):
|
|
402
|
+
sexp_data.append(self._circle_to_sexp(circle))
|
|
403
|
+
|
|
404
|
+
# Arcs
|
|
405
|
+
for arc in schematic_data.get("arcs", []):
|
|
406
|
+
sexp_data.append(self._arc_to_sexp(arc))
|
|
407
|
+
|
|
408
|
+
# Polylines
|
|
409
|
+
for polyline in schematic_data.get("polylines", []):
|
|
410
|
+
sexp_data.append(self._polyline_to_sexp(polyline))
|
|
411
|
+
|
|
412
|
+
# Text elements
|
|
305
413
|
for text in schematic_data.get("texts", []):
|
|
306
414
|
sexp_data.append(self._text_to_sexp(text))
|
|
307
415
|
|
|
308
|
-
#
|
|
416
|
+
# Text boxes
|
|
309
417
|
for text_box in schematic_data.get("text_boxes", []):
|
|
310
418
|
sexp_data.append(self._text_box_to_sexp(text_box))
|
|
311
419
|
|
|
312
|
-
#
|
|
313
|
-
for
|
|
314
|
-
sexp_data.append(self.
|
|
315
|
-
|
|
316
|
-
# Add rectangles (rectangles from add_rectangle API)
|
|
317
|
-
for rectangle in schematic_data.get("rectangles", []):
|
|
318
|
-
sexp_data.append(self._rectangle_to_sexp(rectangle))
|
|
420
|
+
# Hierarchical sheets
|
|
421
|
+
for sheet in schematic_data.get("sheets", []):
|
|
422
|
+
sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
|
|
319
423
|
|
|
320
424
|
# Add sheet_instances (required by KiCAD)
|
|
321
425
|
sheet_instances = schematic_data.get("sheet_instances", [])
|
|
@@ -339,276 +443,67 @@ class SExpressionParser:
|
|
|
339
443
|
|
|
340
444
|
def _parse_title_block(self, item: List[Any]) -> Dict[str, Any]:
|
|
341
445
|
"""Parse title block information."""
|
|
342
|
-
|
|
343
|
-
for sub_item in item[1:]:
|
|
344
|
-
if isinstance(sub_item, list) and len(sub_item) >= 2:
|
|
345
|
-
key = str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
|
|
346
|
-
if key:
|
|
347
|
-
title_block[key] = sub_item[1] if len(sub_item) > 1 else None
|
|
348
|
-
return title_block
|
|
349
|
-
|
|
446
|
+
return self._metadata_parser._parse_title_block(item)
|
|
350
447
|
def _parse_symbol(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
351
448
|
"""Parse a symbol (component) definition."""
|
|
352
|
-
|
|
353
|
-
symbol_data = {
|
|
354
|
-
"lib_id": None,
|
|
355
|
-
"position": Point(0, 0),
|
|
356
|
-
"rotation": 0,
|
|
357
|
-
"uuid": None,
|
|
358
|
-
"reference": None,
|
|
359
|
-
"value": None,
|
|
360
|
-
"footprint": None,
|
|
361
|
-
"properties": {},
|
|
362
|
-
"pins": [],
|
|
363
|
-
"in_bom": True,
|
|
364
|
-
"on_board": True,
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
for sub_item in item[1:]:
|
|
368
|
-
if not isinstance(sub_item, list) or len(sub_item) == 0:
|
|
369
|
-
continue
|
|
370
|
-
|
|
371
|
-
element_type = (
|
|
372
|
-
str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
if element_type == "lib_id":
|
|
376
|
-
symbol_data["lib_id"] = sub_item[1] if len(sub_item) > 1 else None
|
|
377
|
-
elif element_type == "at":
|
|
378
|
-
if len(sub_item) >= 3:
|
|
379
|
-
symbol_data["position"] = Point(float(sub_item[1]), float(sub_item[2]))
|
|
380
|
-
if len(sub_item) > 3:
|
|
381
|
-
symbol_data["rotation"] = float(sub_item[3])
|
|
382
|
-
elif element_type == "uuid":
|
|
383
|
-
symbol_data["uuid"] = sub_item[1] if len(sub_item) > 1 else None
|
|
384
|
-
elif element_type == "property":
|
|
385
|
-
prop_data = self._parse_property(sub_item)
|
|
386
|
-
if prop_data:
|
|
387
|
-
prop_name = prop_data.get("name")
|
|
388
|
-
if prop_name == "Reference":
|
|
389
|
-
symbol_data["reference"] = prop_data.get("value")
|
|
390
|
-
elif prop_name == "Value":
|
|
391
|
-
symbol_data["value"] = prop_data.get("value")
|
|
392
|
-
elif prop_name == "Footprint":
|
|
393
|
-
symbol_data["footprint"] = prop_data.get("value")
|
|
394
|
-
else:
|
|
395
|
-
# Unescape quotes in property values when loading
|
|
396
|
-
prop_value = prop_data.get("value")
|
|
397
|
-
if prop_value:
|
|
398
|
-
prop_value = str(prop_value).replace('\\"', '"')
|
|
399
|
-
symbol_data["properties"][prop_name] = prop_value
|
|
400
|
-
elif element_type == "in_bom":
|
|
401
|
-
symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
|
|
402
|
-
elif element_type == "on_board":
|
|
403
|
-
symbol_data["on_board"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
|
|
404
|
-
|
|
405
|
-
return symbol_data
|
|
406
|
-
|
|
407
|
-
except Exception as e:
|
|
408
|
-
logger.warning(f"Error parsing symbol: {e}")
|
|
409
|
-
return None
|
|
410
|
-
|
|
449
|
+
return self._symbol_parser._parse_symbol(item)
|
|
411
450
|
def _parse_property(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
412
451
|
"""Parse a property definition."""
|
|
413
|
-
|
|
414
|
-
return None
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
"name": item[1] if len(item) > 1 else None,
|
|
418
|
-
"value": item[2] if len(item) > 2 else None,
|
|
419
|
-
}
|
|
420
|
-
|
|
452
|
+
return self._symbol_parser._parse_property(item)
|
|
421
453
|
def _parse_wire(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
422
454
|
"""Parse a wire definition."""
|
|
423
|
-
|
|
424
|
-
# This would parse pts, stroke, uuid elements
|
|
425
|
-
return {}
|
|
426
|
-
|
|
455
|
+
return self._wire_parser._parse_wire(item)
|
|
427
456
|
def _parse_junction(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
428
457
|
"""Parse a junction definition."""
|
|
429
|
-
|
|
430
|
-
return {}
|
|
431
|
-
|
|
458
|
+
return self._wire_parser._parse_junction(item)
|
|
432
459
|
def _parse_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
433
460
|
"""Parse a label definition."""
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
461
|
+
return self._label_parser._parse_label(item)
|
|
462
|
+
def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
463
|
+
"""Parse a hierarchical label definition."""
|
|
464
|
+
return self._label_parser._parse_hierarchical_label(item)
|
|
465
|
+
def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
466
|
+
"""Parse a no_connect symbol."""
|
|
467
|
+
return self._wire_parser._parse_no_connect(item)
|
|
468
|
+
def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
469
|
+
"""Parse a text element."""
|
|
470
|
+
return self._text_parser._parse_text(item)
|
|
471
|
+
def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
472
|
+
"""Parse a text_box element."""
|
|
473
|
+
return self._text_parser._parse_text_box(item)
|
|
474
|
+
def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
475
|
+
"""Parse a hierarchical sheet."""
|
|
476
|
+
return self._sheet_parser._parse_sheet(item)
|
|
477
|
+
def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
478
|
+
"""Parse a sheet pin (for reading during sheet parsing)."""
|
|
479
|
+
return self._sheet_parser._parse_sheet_pin_for_read(item)
|
|
480
|
+
def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
481
|
+
"""Parse a polyline graphical element."""
|
|
482
|
+
return self._graphics_parser._parse_polyline(item)
|
|
483
|
+
def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
484
|
+
"""Parse an arc graphical element."""
|
|
485
|
+
return self._graphics_parser._parse_arc(item)
|
|
486
|
+
def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
487
|
+
"""Parse a circle graphical element."""
|
|
488
|
+
return self._graphics_parser._parse_circle(item)
|
|
489
|
+
def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
490
|
+
"""Parse a bezier curve graphical element."""
|
|
491
|
+
return self._graphics_parser._parse_bezier(item)
|
|
437
492
|
def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
438
493
|
"""Parse a rectangle graphical element."""
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
continue
|
|
444
|
-
|
|
445
|
-
elem_type = str(elem[0])
|
|
446
|
-
|
|
447
|
-
if elem_type == "start" and len(elem) >= 3:
|
|
448
|
-
rectangle["start"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
449
|
-
elif elem_type == "end" and len(elem) >= 3:
|
|
450
|
-
rectangle["end"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
451
|
-
elif elem_type == "stroke":
|
|
452
|
-
for stroke_elem in elem[1:]:
|
|
453
|
-
if isinstance(stroke_elem, list):
|
|
454
|
-
stroke_type = str(stroke_elem[0])
|
|
455
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
456
|
-
rectangle["stroke_width"] = float(stroke_elem[1])
|
|
457
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
458
|
-
rectangle["stroke_type"] = str(stroke_elem[1])
|
|
459
|
-
elif elem_type == "fill":
|
|
460
|
-
for fill_elem in elem[1:]:
|
|
461
|
-
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
462
|
-
rectangle["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
463
|
-
elif elem_type == "uuid" and len(elem) >= 2:
|
|
464
|
-
rectangle["uuid"] = str(elem[1])
|
|
465
|
-
|
|
466
|
-
return rectangle if rectangle else None
|
|
467
|
-
|
|
494
|
+
return self._graphics_parser._parse_rectangle(item)
|
|
495
|
+
def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
496
|
+
"""Parse an image element."""
|
|
497
|
+
return self._graphics_parser._parse_image(item)
|
|
468
498
|
def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
|
|
469
499
|
"""Parse lib_symbols section."""
|
|
470
|
-
|
|
471
|
-
return {}
|
|
472
|
-
|
|
473
|
-
# Conversion methods from internal format to S-expression
|
|
500
|
+
return self._library_parser._parse_lib_symbols(item)
|
|
474
501
|
def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
|
|
475
502
|
"""Convert title block to S-expression."""
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
# Add standard fields
|
|
479
|
-
for key in ["title", "date", "rev", "company"]:
|
|
480
|
-
if key in title_block and title_block[key]:
|
|
481
|
-
sexp.append([sexpdata.Symbol(key), title_block[key]])
|
|
482
|
-
|
|
483
|
-
# Add comments with special formatting
|
|
484
|
-
comments = title_block.get("comments", {})
|
|
485
|
-
if isinstance(comments, dict):
|
|
486
|
-
for comment_num, comment_text in comments.items():
|
|
487
|
-
sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
|
|
488
|
-
|
|
489
|
-
return sexp
|
|
490
|
-
|
|
503
|
+
return self._metadata_parser._title_block_to_sexp(title_block)
|
|
491
504
|
def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
|
|
492
505
|
"""Convert symbol to S-expression."""
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if symbol_data.get("lib_id"):
|
|
496
|
-
sexp.append([sexpdata.Symbol("lib_id"), symbol_data["lib_id"]])
|
|
497
|
-
|
|
498
|
-
# Add position and rotation (preserve original format)
|
|
499
|
-
pos = symbol_data.get("position", Point(0, 0))
|
|
500
|
-
rotation = symbol_data.get("rotation", 0)
|
|
501
|
-
# Format numbers as integers if they are whole numbers
|
|
502
|
-
x = int(pos.x) if pos.x == int(pos.x) else pos.x
|
|
503
|
-
y = int(pos.y) if pos.y == int(pos.y) else pos.y
|
|
504
|
-
r = int(rotation) if rotation == int(rotation) else rotation
|
|
505
|
-
# Always include rotation for format consistency with KiCAD
|
|
506
|
-
sexp.append([sexpdata.Symbol("at"), x, y, r])
|
|
507
|
-
|
|
508
|
-
# Add unit (required by KiCAD)
|
|
509
|
-
unit = symbol_data.get("unit", 1)
|
|
510
|
-
sexp.append([sexpdata.Symbol("unit"), unit])
|
|
511
|
-
|
|
512
|
-
# Add simulation and board settings (required by KiCAD)
|
|
513
|
-
sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
|
|
514
|
-
sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
|
|
515
|
-
sexp.append(
|
|
516
|
-
[sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
|
|
517
|
-
)
|
|
518
|
-
sexp.append([sexpdata.Symbol("dnp"), "no"])
|
|
519
|
-
sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
|
|
520
|
-
|
|
521
|
-
if symbol_data.get("uuid"):
|
|
522
|
-
sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
|
|
523
|
-
|
|
524
|
-
# Add properties with proper positioning and effects
|
|
525
|
-
lib_id = symbol_data.get("lib_id", "")
|
|
526
|
-
is_power_symbol = "power:" in lib_id
|
|
527
|
-
|
|
528
|
-
if symbol_data.get("reference"):
|
|
529
|
-
# Power symbol references should be hidden by default
|
|
530
|
-
ref_hide = is_power_symbol
|
|
531
|
-
ref_prop = self._create_property_with_positioning(
|
|
532
|
-
"Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
|
|
533
|
-
)
|
|
534
|
-
sexp.append(ref_prop)
|
|
535
|
-
|
|
536
|
-
if symbol_data.get("value"):
|
|
537
|
-
# Power symbol values need different positioning
|
|
538
|
-
if is_power_symbol:
|
|
539
|
-
val_prop = self._create_power_symbol_value_property(
|
|
540
|
-
symbol_data["value"], pos, lib_id
|
|
541
|
-
)
|
|
542
|
-
else:
|
|
543
|
-
val_prop = self._create_property_with_positioning(
|
|
544
|
-
"Value", symbol_data["value"], pos, 1, "left"
|
|
545
|
-
)
|
|
546
|
-
sexp.append(val_prop)
|
|
547
|
-
|
|
548
|
-
footprint = symbol_data.get("footprint")
|
|
549
|
-
if footprint is not None: # Include empty strings but not None
|
|
550
|
-
fp_prop = self._create_property_with_positioning(
|
|
551
|
-
"Footprint", footprint, pos, 2, "left", hide=True
|
|
552
|
-
)
|
|
553
|
-
sexp.append(fp_prop)
|
|
554
|
-
|
|
555
|
-
for prop_name, prop_value in symbol_data.get("properties", {}).items():
|
|
556
|
-
escaped_value = str(prop_value).replace('"', '\\"')
|
|
557
|
-
prop = self._create_property_with_positioning(
|
|
558
|
-
prop_name, escaped_value, pos, 3, "left", hide=True
|
|
559
|
-
)
|
|
560
|
-
sexp.append(prop)
|
|
561
|
-
|
|
562
|
-
# Add pin UUID assignments (required by KiCAD)
|
|
563
|
-
for pin in symbol_data.get("pins", []):
|
|
564
|
-
pin_uuid = str(uuid.uuid4())
|
|
565
|
-
# Ensure pin number is a string for proper quoting
|
|
566
|
-
pin_number = str(pin.number)
|
|
567
|
-
sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
|
|
568
|
-
|
|
569
|
-
# Add instances section (required by KiCAD)
|
|
570
|
-
from .config import config
|
|
571
|
-
|
|
572
|
-
# Get project name from config or properties
|
|
573
|
-
project_name = symbol_data.get("properties", {}).get("project_name")
|
|
574
|
-
if not project_name:
|
|
575
|
-
project_name = getattr(self, "project_name", config.defaults.project_name)
|
|
576
|
-
|
|
577
|
-
# CRITICAL FIX: Use the FULL hierarchy_path from properties if available
|
|
578
|
-
# For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
|
|
579
|
-
# This ensures KiCad can properly annotate components in sub-sheets
|
|
580
|
-
hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
|
|
581
|
-
if hierarchy_path:
|
|
582
|
-
# Use the full hierarchical path (includes root + all sheet symbols)
|
|
583
|
-
instance_path = hierarchy_path
|
|
584
|
-
logger.debug(f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}")
|
|
585
|
-
else:
|
|
586
|
-
# Fallback: use root_uuid or schematic_uuid for flat designs
|
|
587
|
-
root_uuid = symbol_data.get("properties", {}).get("root_uuid") or schematic_uuid or str(uuid.uuid4())
|
|
588
|
-
instance_path = f"/{root_uuid}"
|
|
589
|
-
logger.debug(f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}")
|
|
590
|
-
|
|
591
|
-
logger.debug(f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}")
|
|
592
|
-
logger.debug(f"🔧 Using project name: '{project_name}'")
|
|
593
|
-
|
|
594
|
-
sexp.append(
|
|
595
|
-
[
|
|
596
|
-
sexpdata.Symbol("instances"),
|
|
597
|
-
[
|
|
598
|
-
sexpdata.Symbol("project"),
|
|
599
|
-
project_name,
|
|
600
|
-
[
|
|
601
|
-
sexpdata.Symbol("path"),
|
|
602
|
-
instance_path,
|
|
603
|
-
[sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
|
|
604
|
-
[sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
|
|
605
|
-
],
|
|
606
|
-
],
|
|
607
|
-
]
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
return sexp
|
|
611
|
-
|
|
506
|
+
return self._symbol_parser._symbol_to_sexp(symbol_data, schematic_uuid)
|
|
612
507
|
def _create_property_with_positioning(
|
|
613
508
|
self,
|
|
614
509
|
prop_name: str,
|
|
@@ -692,745 +587,74 @@ class SExpressionParser:
|
|
|
692
587
|
|
|
693
588
|
def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
|
|
694
589
|
"""Convert wire to S-expression."""
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
# Add points (pts section)
|
|
698
|
-
points = wire_data.get("points", [])
|
|
699
|
-
if len(points) >= 2:
|
|
700
|
-
pts_sexp = [sexpdata.Symbol("pts")]
|
|
701
|
-
for point in points:
|
|
702
|
-
if isinstance(point, dict):
|
|
703
|
-
x, y = point["x"], point["y"]
|
|
704
|
-
elif isinstance(point, (list, tuple)) and len(point) >= 2:
|
|
705
|
-
x, y = point[0], point[1]
|
|
706
|
-
else:
|
|
707
|
-
# Assume it's a Point object
|
|
708
|
-
x, y = point.x, point.y
|
|
709
|
-
|
|
710
|
-
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
711
|
-
if isinstance(x, float) and x.is_integer():
|
|
712
|
-
x = int(x)
|
|
713
|
-
if isinstance(y, float) and y.is_integer():
|
|
714
|
-
y = int(y)
|
|
715
|
-
|
|
716
|
-
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
717
|
-
sexp.append(pts_sexp)
|
|
718
|
-
|
|
719
|
-
# Add stroke information
|
|
720
|
-
stroke_width = wire_data.get("stroke_width", 0)
|
|
721
|
-
stroke_type = wire_data.get("stroke_type", "default")
|
|
722
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
723
|
-
|
|
724
|
-
# Format stroke width (use int for 0, preserve float for others)
|
|
725
|
-
if isinstance(stroke_width, float) and stroke_width == 0.0:
|
|
726
|
-
stroke_width = 0
|
|
727
|
-
|
|
728
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
729
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
730
|
-
sexp.append(stroke_sexp)
|
|
731
|
-
|
|
732
|
-
# Add UUID
|
|
733
|
-
if "uuid" in wire_data:
|
|
734
|
-
sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
|
|
735
|
-
|
|
736
|
-
return sexp
|
|
737
|
-
|
|
590
|
+
return self._wire_parser._wire_to_sexp(wire_data)
|
|
738
591
|
def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
|
|
739
592
|
"""Convert junction to S-expression."""
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
# Add position
|
|
743
|
-
pos = junction_data["position"]
|
|
744
|
-
if isinstance(pos, dict):
|
|
745
|
-
x, y = pos["x"], pos["y"]
|
|
746
|
-
elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
|
|
747
|
-
x, y = pos[0], pos[1]
|
|
748
|
-
else:
|
|
749
|
-
# Assume it's a Point object
|
|
750
|
-
x, y = pos.x, pos.y
|
|
751
|
-
|
|
752
|
-
# Format coordinates properly
|
|
753
|
-
if isinstance(x, float) and x.is_integer():
|
|
754
|
-
x = int(x)
|
|
755
|
-
if isinstance(y, float) and y.is_integer():
|
|
756
|
-
y = int(y)
|
|
757
|
-
|
|
758
|
-
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
759
|
-
|
|
760
|
-
# Add diameter
|
|
761
|
-
diameter = junction_data.get("diameter", 0)
|
|
762
|
-
sexp.append([sexpdata.Symbol("diameter"), diameter])
|
|
763
|
-
|
|
764
|
-
# Add color (RGBA)
|
|
765
|
-
color = junction_data.get("color", (0, 0, 0, 0))
|
|
766
|
-
if isinstance(color, (list, tuple)) and len(color) >= 4:
|
|
767
|
-
sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
|
|
768
|
-
else:
|
|
769
|
-
sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
|
|
770
|
-
|
|
771
|
-
# Add UUID
|
|
772
|
-
if "uuid" in junction_data:
|
|
773
|
-
sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
|
|
774
|
-
|
|
775
|
-
return sexp
|
|
776
|
-
|
|
593
|
+
return self._wire_parser._junction_to_sexp(junction_data)
|
|
777
594
|
def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
|
|
778
595
|
"""Convert local label to S-expression."""
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
# Add position
|
|
782
|
-
pos = label_data["position"]
|
|
783
|
-
x, y = pos["x"], pos["y"]
|
|
784
|
-
rotation = label_data.get("rotation", 0)
|
|
785
|
-
|
|
786
|
-
# Format coordinates properly
|
|
787
|
-
if isinstance(x, float) and x.is_integer():
|
|
788
|
-
x = int(x)
|
|
789
|
-
if isinstance(y, float) and y.is_integer():
|
|
790
|
-
y = int(y)
|
|
791
|
-
|
|
792
|
-
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
793
|
-
|
|
794
|
-
# Add effects (font properties)
|
|
795
|
-
size = label_data.get("size", 1.27)
|
|
796
|
-
effects = [sexpdata.Symbol("effects")]
|
|
797
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
798
|
-
effects.append(font)
|
|
799
|
-
effects.append(
|
|
800
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]
|
|
801
|
-
)
|
|
802
|
-
sexp.append(effects)
|
|
803
|
-
|
|
804
|
-
# Add UUID
|
|
805
|
-
if "uuid" in label_data:
|
|
806
|
-
sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
|
|
807
|
-
|
|
808
|
-
return sexp
|
|
809
|
-
|
|
596
|
+
return self._label_parser._label_to_sexp(label_data)
|
|
810
597
|
def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
|
|
811
598
|
"""Convert hierarchical label to S-expression."""
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
effects.append(font)
|
|
829
|
-
|
|
830
|
-
# Use justification from data if provided, otherwise default to "left"
|
|
831
|
-
justify = hlabel_data.get("justify", "left")
|
|
832
|
-
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
833
|
-
sexp.append(effects)
|
|
834
|
-
|
|
835
|
-
# Add UUID
|
|
836
|
-
if "uuid" in hlabel_data:
|
|
837
|
-
sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
|
|
838
|
-
|
|
839
|
-
return sexp
|
|
840
|
-
|
|
599
|
+
return self._label_parser._hierarchical_label_to_sexp(hlabel_data)
|
|
600
|
+
def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
|
|
601
|
+
"""Convert no_connect to S-expression."""
|
|
602
|
+
return self._wire_parser._no_connect_to_sexp(no_connect_data)
|
|
603
|
+
def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
|
|
604
|
+
"""Convert polyline to S-expression."""
|
|
605
|
+
return self._graphics_parser._polyline_to_sexp(polyline_data)
|
|
606
|
+
def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
|
|
607
|
+
"""Convert arc to S-expression."""
|
|
608
|
+
return self._graphics_parser._arc_to_sexp(arc_data)
|
|
609
|
+
def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
|
|
610
|
+
"""Convert circle to S-expression."""
|
|
611
|
+
return self._graphics_parser._circle_to_sexp(circle_data)
|
|
612
|
+
def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
|
|
613
|
+
"""Convert bezier curve to S-expression."""
|
|
614
|
+
return self._graphics_parser._bezier_to_sexp(bezier_data)
|
|
841
615
|
def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
|
|
842
616
|
"""Convert hierarchical sheet to S-expression."""
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
# Add position
|
|
846
|
-
pos = sheet_data["position"]
|
|
847
|
-
x, y = pos["x"], pos["y"]
|
|
848
|
-
if isinstance(x, float) and x.is_integer():
|
|
849
|
-
x = int(x)
|
|
850
|
-
if isinstance(y, float) and y.is_integer():
|
|
851
|
-
y = int(y)
|
|
852
|
-
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
853
|
-
|
|
854
|
-
# Add size
|
|
855
|
-
size = sheet_data["size"]
|
|
856
|
-
w, h = size["width"], size["height"]
|
|
857
|
-
sexp.append([sexpdata.Symbol("size"), w, h])
|
|
858
|
-
|
|
859
|
-
# Add basic properties
|
|
860
|
-
sexp.append(
|
|
861
|
-
[
|
|
862
|
-
sexpdata.Symbol("exclude_from_sim"),
|
|
863
|
-
sexpdata.Symbol("yes" if sheet_data.get("exclude_from_sim", False) else "no"),
|
|
864
|
-
]
|
|
865
|
-
)
|
|
866
|
-
sexp.append(
|
|
867
|
-
[
|
|
868
|
-
sexpdata.Symbol("in_bom"),
|
|
869
|
-
sexpdata.Symbol("yes" if sheet_data.get("in_bom", True) else "no"),
|
|
870
|
-
]
|
|
871
|
-
)
|
|
872
|
-
sexp.append(
|
|
873
|
-
[
|
|
874
|
-
sexpdata.Symbol("on_board"),
|
|
875
|
-
sexpdata.Symbol("yes" if sheet_data.get("on_board", True) else "no"),
|
|
876
|
-
]
|
|
877
|
-
)
|
|
878
|
-
sexp.append(
|
|
879
|
-
[
|
|
880
|
-
sexpdata.Symbol("dnp"),
|
|
881
|
-
sexpdata.Symbol("yes" if sheet_data.get("dnp", False) else "no"),
|
|
882
|
-
]
|
|
883
|
-
)
|
|
884
|
-
sexp.append(
|
|
885
|
-
[
|
|
886
|
-
sexpdata.Symbol("fields_autoplaced"),
|
|
887
|
-
sexpdata.Symbol("yes" if sheet_data.get("fields_autoplaced", True) else "no"),
|
|
888
|
-
]
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
# Add stroke
|
|
892
|
-
stroke_width = sheet_data.get("stroke_width", 0.1524)
|
|
893
|
-
stroke_type = sheet_data.get("stroke_type", "solid")
|
|
894
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
895
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
896
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
897
|
-
sexp.append(stroke_sexp)
|
|
898
|
-
|
|
899
|
-
# Add fill
|
|
900
|
-
fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
|
|
901
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
902
|
-
fill_sexp.append(
|
|
903
|
-
[sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]]
|
|
904
|
-
)
|
|
905
|
-
sexp.append(fill_sexp)
|
|
906
|
-
|
|
907
|
-
# Add UUID
|
|
908
|
-
if "uuid" in sheet_data:
|
|
909
|
-
sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
|
|
910
|
-
|
|
911
|
-
# Add sheet properties (name and filename)
|
|
912
|
-
name = sheet_data.get("name", "Sheet")
|
|
913
|
-
filename = sheet_data.get("filename", "sheet.kicad_sch")
|
|
914
|
-
|
|
915
|
-
# Sheetname property
|
|
916
|
-
from .config import config
|
|
917
|
-
|
|
918
|
-
name_prop = [sexpdata.Symbol("property"), "Sheetname", name]
|
|
919
|
-
name_prop.append(
|
|
920
|
-
[sexpdata.Symbol("at"), x, round(y + config.sheet.name_offset_y, 4), 0]
|
|
921
|
-
) # Above sheet
|
|
922
|
-
name_prop.append(
|
|
923
|
-
[
|
|
924
|
-
sexpdata.Symbol("effects"),
|
|
925
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
926
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")],
|
|
927
|
-
]
|
|
928
|
-
)
|
|
929
|
-
sexp.append(name_prop)
|
|
930
|
-
|
|
931
|
-
# Sheetfile property
|
|
932
|
-
file_prop = [sexpdata.Symbol("property"), "Sheetfile", filename]
|
|
933
|
-
file_prop.append(
|
|
934
|
-
[sexpdata.Symbol("at"), x, round(y + h + config.sheet.file_offset_y, 4), 0]
|
|
935
|
-
) # Below sheet
|
|
936
|
-
file_prop.append(
|
|
937
|
-
[
|
|
938
|
-
sexpdata.Symbol("effects"),
|
|
939
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
940
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("top")],
|
|
941
|
-
]
|
|
942
|
-
)
|
|
943
|
-
sexp.append(file_prop)
|
|
944
|
-
|
|
945
|
-
# Add sheet pins if any
|
|
946
|
-
for pin in sheet_data.get("pins", []):
|
|
947
|
-
pin_sexp = self._sheet_pin_to_sexp(pin)
|
|
948
|
-
sexp.append(pin_sexp)
|
|
949
|
-
|
|
950
|
-
# Add instances
|
|
951
|
-
if schematic_uuid:
|
|
952
|
-
instances_sexp = [sexpdata.Symbol("instances")]
|
|
953
|
-
project_name = sheet_data.get("project_name", "")
|
|
954
|
-
page_number = sheet_data.get("page_number", "2")
|
|
955
|
-
project_sexp = [sexpdata.Symbol("project"), project_name]
|
|
956
|
-
path_sexp = [sexpdata.Symbol("path"), f"/{schematic_uuid}"]
|
|
957
|
-
path_sexp.append([sexpdata.Symbol("page"), page_number])
|
|
958
|
-
project_sexp.append(path_sexp)
|
|
959
|
-
instances_sexp.append(project_sexp)
|
|
960
|
-
sexp.append(instances_sexp)
|
|
961
|
-
|
|
962
|
-
return sexp
|
|
963
|
-
|
|
617
|
+
return self._sheet_parser._sheet_to_sexp(sheet_data, schematic_uuid)
|
|
964
618
|
def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
|
|
965
619
|
"""Convert sheet pin to S-expression."""
|
|
966
|
-
|
|
967
|
-
sexpdata.Symbol("pin"),
|
|
968
|
-
pin_data["name"],
|
|
969
|
-
sexpdata.Symbol(pin_data.get("pin_type", "input")),
|
|
970
|
-
]
|
|
971
|
-
|
|
972
|
-
# Add position
|
|
973
|
-
pos = pin_data["position"]
|
|
974
|
-
x, y = pos["x"], pos["y"]
|
|
975
|
-
rotation = pin_data.get("rotation", 0)
|
|
976
|
-
pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
977
|
-
|
|
978
|
-
# Add UUID
|
|
979
|
-
if "uuid" in pin_data:
|
|
980
|
-
pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
|
|
981
|
-
|
|
982
|
-
# Add effects
|
|
983
|
-
size = pin_data.get("size", 1.27)
|
|
984
|
-
effects = [sexpdata.Symbol("effects")]
|
|
985
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
986
|
-
effects.append(font)
|
|
987
|
-
justify = pin_data.get("justify", "right")
|
|
988
|
-
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
989
|
-
pin_sexp.append(effects)
|
|
990
|
-
|
|
991
|
-
return pin_sexp
|
|
992
|
-
|
|
620
|
+
return self._sheet_parser._sheet_pin_to_sexp(pin_data)
|
|
993
621
|
def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
|
|
994
622
|
"""Convert text element to S-expression."""
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
# Add exclude_from_sim
|
|
998
|
-
exclude_sim = text_data.get("exclude_from_sim", False)
|
|
999
|
-
sexp.append(
|
|
1000
|
-
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
|
|
1001
|
-
)
|
|
1002
|
-
|
|
1003
|
-
# Add position
|
|
1004
|
-
pos = text_data["position"]
|
|
1005
|
-
x, y = pos["x"], pos["y"]
|
|
1006
|
-
rotation = text_data.get("rotation", 0)
|
|
1007
|
-
|
|
1008
|
-
# Format coordinates properly
|
|
1009
|
-
if isinstance(x, float) and x.is_integer():
|
|
1010
|
-
x = int(x)
|
|
1011
|
-
if isinstance(y, float) and y.is_integer():
|
|
1012
|
-
y = int(y)
|
|
1013
|
-
|
|
1014
|
-
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
1015
|
-
|
|
1016
|
-
# Add effects (font properties)
|
|
1017
|
-
size = text_data.get("size", 1.27)
|
|
1018
|
-
effects = [sexpdata.Symbol("effects")]
|
|
1019
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
1020
|
-
effects.append(font)
|
|
1021
|
-
sexp.append(effects)
|
|
1022
|
-
|
|
1023
|
-
# Add UUID
|
|
1024
|
-
if "uuid" in text_data:
|
|
1025
|
-
sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
|
|
1026
|
-
|
|
1027
|
-
return sexp
|
|
1028
|
-
|
|
623
|
+
return self._text_parser._text_to_sexp(text_data)
|
|
1029
624
|
def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
|
|
1030
625
|
"""Convert text box element to S-expression."""
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
# Add exclude_from_sim
|
|
1034
|
-
exclude_sim = text_box_data.get("exclude_from_sim", False)
|
|
1035
|
-
sexp.append(
|
|
1036
|
-
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
|
|
1037
|
-
)
|
|
1038
|
-
|
|
1039
|
-
# Add position
|
|
1040
|
-
pos = text_box_data["position"]
|
|
1041
|
-
x, y = pos["x"], pos["y"]
|
|
1042
|
-
rotation = text_box_data.get("rotation", 0)
|
|
1043
|
-
|
|
1044
|
-
# Format coordinates properly
|
|
1045
|
-
if isinstance(x, float) and x.is_integer():
|
|
1046
|
-
x = int(x)
|
|
1047
|
-
if isinstance(y, float) and y.is_integer():
|
|
1048
|
-
y = int(y)
|
|
1049
|
-
|
|
1050
|
-
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
1051
|
-
|
|
1052
|
-
# Add size
|
|
1053
|
-
size = text_box_data["size"]
|
|
1054
|
-
w, h = size["width"], size["height"]
|
|
1055
|
-
sexp.append([sexpdata.Symbol("size"), w, h])
|
|
1056
|
-
|
|
1057
|
-
# Add margins
|
|
1058
|
-
margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
|
|
1059
|
-
sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
|
|
1060
|
-
|
|
1061
|
-
# Add stroke
|
|
1062
|
-
stroke_width = text_box_data.get("stroke_width", 0)
|
|
1063
|
-
stroke_type = text_box_data.get("stroke_type", "solid")
|
|
1064
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1065
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1066
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1067
|
-
sexp.append(stroke_sexp)
|
|
1068
|
-
|
|
1069
|
-
# Add fill
|
|
1070
|
-
fill_type = text_box_data.get("fill_type", "none")
|
|
1071
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1072
|
-
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1073
|
-
sexp.append(fill_sexp)
|
|
1074
|
-
|
|
1075
|
-
# Add effects (font properties and justification)
|
|
1076
|
-
font_size = text_box_data.get("font_size", 1.27)
|
|
1077
|
-
justify_h = text_box_data.get("justify_horizontal", "left")
|
|
1078
|
-
justify_v = text_box_data.get("justify_vertical", "top")
|
|
1079
|
-
|
|
1080
|
-
effects = [sexpdata.Symbol("effects")]
|
|
1081
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
|
|
1082
|
-
effects.append(font)
|
|
1083
|
-
effects.append(
|
|
1084
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
|
|
1085
|
-
)
|
|
1086
|
-
sexp.append(effects)
|
|
1087
|
-
|
|
1088
|
-
# Add UUID
|
|
1089
|
-
if "uuid" in text_box_data:
|
|
1090
|
-
sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
|
|
1091
|
-
|
|
1092
|
-
return sexp
|
|
1093
|
-
|
|
626
|
+
return self._text_parser._text_box_to_sexp(text_box_data)
|
|
1094
627
|
def _rectangle_to_sexp(self, rectangle_data: Dict[str, Any]) -> List[Any]:
|
|
1095
628
|
"""Convert rectangle element to S-expression."""
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
start_x, start_y = start["x"], start["y"]
|
|
1101
|
-
sexp.append([sexpdata.Symbol("start"), start_x, start_y])
|
|
1102
|
-
|
|
1103
|
-
# Add end point
|
|
1104
|
-
end = rectangle_data["end"]
|
|
1105
|
-
end_x, end_y = end["x"], end["y"]
|
|
1106
|
-
sexp.append([sexpdata.Symbol("end"), end_x, end_y])
|
|
1107
|
-
|
|
1108
|
-
# Add stroke
|
|
1109
|
-
stroke_width = rectangle_data.get("stroke_width", 0)
|
|
1110
|
-
stroke_type = rectangle_data.get("stroke_type", "default")
|
|
1111
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1112
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1113
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1114
|
-
sexp.append(stroke_sexp)
|
|
1115
|
-
|
|
1116
|
-
# Add fill
|
|
1117
|
-
fill_type = rectangle_data.get("fill_type", "none")
|
|
1118
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1119
|
-
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1120
|
-
sexp.append(fill_sexp)
|
|
1121
|
-
|
|
1122
|
-
# Add UUID
|
|
1123
|
-
if "uuid" in rectangle_data:
|
|
1124
|
-
sexp.append([sexpdata.Symbol("uuid"), rectangle_data["uuid"]])
|
|
1125
|
-
|
|
1126
|
-
return sexp
|
|
1127
|
-
|
|
629
|
+
return self._graphics_parser._rectangle_to_sexp(rectangle_data)
|
|
630
|
+
def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
|
|
631
|
+
"""Convert image element to S-expression."""
|
|
632
|
+
return self._graphics_parser._image_to_sexp(image_data)
|
|
1128
633
|
def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
|
|
1129
634
|
"""Convert lib_symbols to S-expression."""
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
# Add each symbol definition
|
|
1133
|
-
for symbol_name, symbol_def in lib_symbols.items():
|
|
1134
|
-
if isinstance(symbol_def, list):
|
|
1135
|
-
# Raw S-expression data from parsed library file - use directly
|
|
1136
|
-
sexp.append(symbol_def)
|
|
1137
|
-
elif isinstance(symbol_def, dict):
|
|
1138
|
-
# Dictionary format - convert to S-expression
|
|
1139
|
-
symbol_sexp = self._create_basic_symbol_definition(symbol_name)
|
|
1140
|
-
sexp.append(symbol_sexp)
|
|
1141
|
-
|
|
1142
|
-
return sexp
|
|
1143
|
-
|
|
635
|
+
return self._library_parser._lib_symbols_to_sexp(lib_symbols)
|
|
1144
636
|
def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
|
|
1145
637
|
"""Create a basic symbol definition for KiCAD compatibility."""
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
# Add basic symbol properties
|
|
1149
|
-
symbol_sexp.extend(
|
|
1150
|
-
[
|
|
1151
|
-
[sexpdata.Symbol("pin_numbers"), [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]],
|
|
1152
|
-
[sexpdata.Symbol("pin_names"), [sexpdata.Symbol("offset"), 0]],
|
|
1153
|
-
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("no")],
|
|
1154
|
-
[sexpdata.Symbol("in_bom"), sexpdata.Symbol("yes")],
|
|
1155
|
-
[sexpdata.Symbol("on_board"), sexpdata.Symbol("yes")],
|
|
1156
|
-
]
|
|
1157
|
-
)
|
|
1158
|
-
|
|
1159
|
-
# Add basic properties for the symbol
|
|
1160
|
-
if "R" in lib_id: # Resistor
|
|
1161
|
-
symbol_sexp.extend(
|
|
1162
|
-
[
|
|
1163
|
-
[
|
|
1164
|
-
sexpdata.Symbol("property"),
|
|
1165
|
-
"Reference",
|
|
1166
|
-
"R",
|
|
1167
|
-
[sexpdata.Symbol("at"), 2.032, 0, 90],
|
|
1168
|
-
[
|
|
1169
|
-
sexpdata.Symbol("effects"),
|
|
1170
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1171
|
-
],
|
|
1172
|
-
],
|
|
1173
|
-
[
|
|
1174
|
-
sexpdata.Symbol("property"),
|
|
1175
|
-
"Value",
|
|
1176
|
-
"R",
|
|
1177
|
-
[sexpdata.Symbol("at"), 0, 0, 90],
|
|
1178
|
-
[
|
|
1179
|
-
sexpdata.Symbol("effects"),
|
|
1180
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1181
|
-
],
|
|
1182
|
-
],
|
|
1183
|
-
[
|
|
1184
|
-
sexpdata.Symbol("property"),
|
|
1185
|
-
"Footprint",
|
|
1186
|
-
"",
|
|
1187
|
-
[sexpdata.Symbol("at"), -1.778, 0, 90],
|
|
1188
|
-
[
|
|
1189
|
-
sexpdata.Symbol("effects"),
|
|
1190
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1191
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1192
|
-
],
|
|
1193
|
-
],
|
|
1194
|
-
[
|
|
1195
|
-
sexpdata.Symbol("property"),
|
|
1196
|
-
"Datasheet",
|
|
1197
|
-
"~",
|
|
1198
|
-
[sexpdata.Symbol("at"), 0, 0, 0],
|
|
1199
|
-
[
|
|
1200
|
-
sexpdata.Symbol("effects"),
|
|
1201
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1202
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1203
|
-
],
|
|
1204
|
-
],
|
|
1205
|
-
]
|
|
1206
|
-
)
|
|
1207
|
-
|
|
1208
|
-
elif "C" in lib_id: # Capacitor
|
|
1209
|
-
symbol_sexp.extend(
|
|
1210
|
-
[
|
|
1211
|
-
[
|
|
1212
|
-
sexpdata.Symbol("property"),
|
|
1213
|
-
"Reference",
|
|
1214
|
-
"C",
|
|
1215
|
-
[sexpdata.Symbol("at"), 0.635, 2.54, 0],
|
|
1216
|
-
[
|
|
1217
|
-
sexpdata.Symbol("effects"),
|
|
1218
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1219
|
-
],
|
|
1220
|
-
],
|
|
1221
|
-
[
|
|
1222
|
-
sexpdata.Symbol("property"),
|
|
1223
|
-
"Value",
|
|
1224
|
-
"C",
|
|
1225
|
-
[sexpdata.Symbol("at"), 0.635, -2.54, 0],
|
|
1226
|
-
[
|
|
1227
|
-
sexpdata.Symbol("effects"),
|
|
1228
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1229
|
-
],
|
|
1230
|
-
],
|
|
1231
|
-
[
|
|
1232
|
-
sexpdata.Symbol("property"),
|
|
1233
|
-
"Footprint",
|
|
1234
|
-
"",
|
|
1235
|
-
[sexpdata.Symbol("at"), 0, -1.27, 0],
|
|
1236
|
-
[
|
|
1237
|
-
sexpdata.Symbol("effects"),
|
|
1238
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1239
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1240
|
-
],
|
|
1241
|
-
],
|
|
1242
|
-
[
|
|
1243
|
-
sexpdata.Symbol("property"),
|
|
1244
|
-
"Datasheet",
|
|
1245
|
-
"~",
|
|
1246
|
-
[sexpdata.Symbol("at"), 0, 0, 0],
|
|
1247
|
-
[
|
|
1248
|
-
sexpdata.Symbol("effects"),
|
|
1249
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1250
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1251
|
-
],
|
|
1252
|
-
],
|
|
1253
|
-
]
|
|
1254
|
-
)
|
|
1255
|
-
|
|
1256
|
-
# Add basic graphics and pins (minimal for now)
|
|
1257
|
-
symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
|
|
1258
|
-
|
|
1259
|
-
return symbol_sexp
|
|
1260
|
-
|
|
638
|
+
return self._library_parser._create_basic_symbol_definition(lib_id)
|
|
1261
639
|
def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
|
|
1262
640
|
"""Parse sheet_instances section."""
|
|
1263
|
-
|
|
1264
|
-
for sheet_item in item[1:]: # Skip 'sheet_instances' header
|
|
1265
|
-
if isinstance(sheet_item, list) and len(sheet_item) > 0:
|
|
1266
|
-
sheet_data = {"path": "/", "page": "1"}
|
|
1267
|
-
for element in sheet_item[1:]: # Skip element header
|
|
1268
|
-
if isinstance(element, list) and len(element) >= 2:
|
|
1269
|
-
key = (
|
|
1270
|
-
str(element[0])
|
|
1271
|
-
if isinstance(element[0], sexpdata.Symbol)
|
|
1272
|
-
else str(element[0])
|
|
1273
|
-
)
|
|
1274
|
-
if key == "path":
|
|
1275
|
-
sheet_data["path"] = element[1]
|
|
1276
|
-
elif key == "page":
|
|
1277
|
-
sheet_data["page"] = element[1]
|
|
1278
|
-
sheet_instances.append(sheet_data)
|
|
1279
|
-
return sheet_instances
|
|
1280
|
-
|
|
641
|
+
return self._sheet_parser._parse_sheet_instances(item)
|
|
1281
642
|
def _parse_symbol_instances(self, item: List[Any]) -> List[Any]:
|
|
1282
643
|
"""Parse symbol_instances section."""
|
|
1283
|
-
|
|
1284
|
-
return item[1:] if len(item) > 1 else []
|
|
1285
|
-
|
|
644
|
+
return self._metadata_parser._parse_symbol_instances(item)
|
|
1286
645
|
def _sheet_instances_to_sexp(self, sheet_instances: List[Dict[str, Any]]) -> List[Any]:
|
|
1287
646
|
"""Convert sheet_instances to S-expression."""
|
|
1288
|
-
|
|
1289
|
-
for sheet in sheet_instances:
|
|
1290
|
-
# Create: (path "/" (page "1"))
|
|
1291
|
-
sheet_sexp = [
|
|
1292
|
-
sexpdata.Symbol("path"),
|
|
1293
|
-
sheet.get("path", "/"),
|
|
1294
|
-
[sexpdata.Symbol("page"), str(sheet.get("page", "1"))],
|
|
1295
|
-
]
|
|
1296
|
-
sexp.append(sheet_sexp)
|
|
1297
|
-
return sexp
|
|
1298
|
-
|
|
647
|
+
return self._sheet_parser._sheet_instances_to_sexp(sheet_instances)
|
|
1299
648
|
def _graphic_to_sexp(self, graphic_data: Dict[str, Any]) -> List[Any]:
|
|
1300
649
|
"""Convert graphics (rectangles, etc.) to S-expression."""
|
|
1301
|
-
|
|
1302
|
-
sexp = [sexpdata.Symbol("rectangle")]
|
|
1303
|
-
|
|
1304
|
-
# Add start position
|
|
1305
|
-
start = graphic_data.get("start", {})
|
|
1306
|
-
start_x = start.get("x", 0)
|
|
1307
|
-
start_y = start.get("y", 0)
|
|
1308
|
-
|
|
1309
|
-
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
1310
|
-
if isinstance(start_x, float) and start_x.is_integer():
|
|
1311
|
-
start_x = int(start_x)
|
|
1312
|
-
if isinstance(start_y, float) and start_y.is_integer():
|
|
1313
|
-
start_y = int(start_y)
|
|
1314
|
-
|
|
1315
|
-
sexp.append([sexpdata.Symbol("start"), start_x, start_y])
|
|
1316
|
-
|
|
1317
|
-
# Add end position
|
|
1318
|
-
end = graphic_data.get("end", {})
|
|
1319
|
-
end_x = end.get("x", 0)
|
|
1320
|
-
end_y = end.get("y", 0)
|
|
1321
|
-
|
|
1322
|
-
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
1323
|
-
if isinstance(end_x, float) and end_x.is_integer():
|
|
1324
|
-
end_x = int(end_x)
|
|
1325
|
-
if isinstance(end_y, float) and end_y.is_integer():
|
|
1326
|
-
end_y = int(end_y)
|
|
1327
|
-
|
|
1328
|
-
sexp.append([sexpdata.Symbol("end"), end_x, end_y])
|
|
1329
|
-
|
|
1330
|
-
# Add stroke information (KiCAD format: width, type, and optionally color)
|
|
1331
|
-
stroke = graphic_data.get("stroke", {})
|
|
1332
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1333
|
-
|
|
1334
|
-
# Stroke width - default to 0 to match KiCAD behavior
|
|
1335
|
-
stroke_width = stroke.get("width", 0)
|
|
1336
|
-
if isinstance(stroke_width, float) and stroke_width == 0.0:
|
|
1337
|
-
stroke_width = 0
|
|
1338
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1339
|
-
|
|
1340
|
-
# Stroke type - normalize to KiCAD format and validate
|
|
1341
|
-
stroke_type = stroke.get("type", "default")
|
|
1342
|
-
|
|
1343
|
-
# KiCAD only supports these exact stroke types
|
|
1344
|
-
valid_kicad_types = {"solid", "dash", "dash_dot", "dash_dot_dot", "dot", "default"}
|
|
1345
|
-
|
|
1346
|
-
# Map common variations to KiCAD format
|
|
1347
|
-
stroke_type_map = {
|
|
1348
|
-
"dashdot": "dash_dot",
|
|
1349
|
-
"dash-dot": "dash_dot",
|
|
1350
|
-
"dashdotdot": "dash_dot_dot",
|
|
1351
|
-
"dash-dot-dot": "dash_dot_dot",
|
|
1352
|
-
"solid": "solid",
|
|
1353
|
-
"dash": "dash",
|
|
1354
|
-
"dot": "dot",
|
|
1355
|
-
"default": "default",
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
# Normalize and validate
|
|
1359
|
-
normalized_stroke_type = stroke_type_map.get(stroke_type.lower(), stroke_type)
|
|
1360
|
-
if normalized_stroke_type not in valid_kicad_types:
|
|
1361
|
-
normalized_stroke_type = "default" # Fallback to default for invalid types
|
|
1362
|
-
|
|
1363
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(normalized_stroke_type)])
|
|
1364
|
-
|
|
1365
|
-
# Stroke color (if specified) - KiCAD format uses RGB 0-255 values plus alpha
|
|
1366
|
-
stroke_color = stroke.get("color")
|
|
1367
|
-
if stroke_color:
|
|
1368
|
-
if isinstance(stroke_color, str):
|
|
1369
|
-
# Convert string color names to RGB 0-255 values
|
|
1370
|
-
color_rgb = self._color_to_rgb255(stroke_color)
|
|
1371
|
-
stroke_sexp.append([sexpdata.Symbol("color")] + color_rgb + [1]) # Add alpha=1
|
|
1372
|
-
elif isinstance(stroke_color, (list, tuple)) and len(stroke_color) >= 3:
|
|
1373
|
-
# Use provided RGB values directly
|
|
1374
|
-
stroke_sexp.append([sexpdata.Symbol("color")] + list(stroke_color))
|
|
1375
|
-
|
|
1376
|
-
sexp.append(stroke_sexp)
|
|
1377
|
-
|
|
1378
|
-
# Add fill information
|
|
1379
|
-
fill = graphic_data.get("fill", {"type": "none"})
|
|
1380
|
-
fill_type = fill.get("type", "none")
|
|
1381
|
-
fill_sexp = [sexpdata.Symbol("fill"), [sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)]]
|
|
1382
|
-
sexp.append(fill_sexp)
|
|
1383
|
-
|
|
1384
|
-
# Add UUID (no quotes around UUID in KiCAD format)
|
|
1385
|
-
if "uuid" in graphic_data:
|
|
1386
|
-
uuid_str = graphic_data["uuid"]
|
|
1387
|
-
# Remove quotes and convert to Symbol to match KiCAD format
|
|
1388
|
-
uuid_clean = uuid_str.replace('"', "")
|
|
1389
|
-
sexp.append([sexpdata.Symbol("uuid"), sexpdata.Symbol(uuid_clean)])
|
|
1390
|
-
|
|
1391
|
-
return sexp
|
|
1392
|
-
|
|
650
|
+
return self._graphics_parser._graphic_to_sexp(graphic_data)
|
|
1393
651
|
def _color_to_rgba(self, color_name: str) -> List[float]:
|
|
1394
652
|
"""Convert color name to RGBA values (0.0-1.0) for KiCAD compatibility."""
|
|
1395
|
-
|
|
1396
|
-
color_map = {
|
|
1397
|
-
"red": [1.0, 0.0, 0.0, 1.0],
|
|
1398
|
-
"blue": [0.0, 0.0, 1.0, 1.0],
|
|
1399
|
-
"green": [0.0, 1.0, 0.0, 1.0],
|
|
1400
|
-
"yellow": [1.0, 1.0, 0.0, 1.0],
|
|
1401
|
-
"magenta": [1.0, 0.0, 1.0, 1.0],
|
|
1402
|
-
"cyan": [0.0, 1.0, 1.0, 1.0],
|
|
1403
|
-
"black": [0.0, 0.0, 0.0, 1.0],
|
|
1404
|
-
"white": [1.0, 1.0, 1.0, 1.0],
|
|
1405
|
-
"gray": [0.5, 0.5, 0.5, 1.0],
|
|
1406
|
-
"grey": [0.5, 0.5, 0.5, 1.0],
|
|
1407
|
-
"orange": [1.0, 0.5, 0.0, 1.0],
|
|
1408
|
-
"purple": [0.5, 0.0, 0.5, 1.0],
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
# Return RGBA values, default to black if color not found
|
|
1412
|
-
return color_map.get(color_name.lower(), [0.0, 0.0, 0.0, 1.0])
|
|
653
|
+
return color_to_rgba(color_name)
|
|
1413
654
|
|
|
1414
655
|
def _color_to_rgb255(self, color_name: str) -> List[int]:
|
|
1415
656
|
"""Convert color name to RGB values (0-255) for KiCAD rectangle graphics."""
|
|
1416
|
-
|
|
1417
|
-
color_map = {
|
|
1418
|
-
"red": [255, 0, 0],
|
|
1419
|
-
"blue": [0, 0, 255],
|
|
1420
|
-
"green": [0, 255, 0],
|
|
1421
|
-
"yellow": [255, 255, 0],
|
|
1422
|
-
"magenta": [255, 0, 255],
|
|
1423
|
-
"cyan": [0, 255, 255],
|
|
1424
|
-
"black": [0, 0, 0],
|
|
1425
|
-
"white": [255, 255, 255],
|
|
1426
|
-
"gray": [128, 128, 128],
|
|
1427
|
-
"grey": [128, 128, 128],
|
|
1428
|
-
"orange": [255, 128, 0],
|
|
1429
|
-
"purple": [128, 0, 128],
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
# Return RGB values, default to black if color not found
|
|
1433
|
-
return color_map.get(color_name.lower(), [0, 0, 0])
|
|
657
|
+
return color_to_rgb255(color_name)
|
|
1434
658
|
|
|
1435
659
|
def get_validation_issues(self) -> List[ValidationIssue]:
|
|
1436
660
|
"""Get list of validation issues from last parse operation."""
|