kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.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 +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 +62 -45
- 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/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.1.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.1.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.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/top_level.txt +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.
|
|
@@ -412,912 +443,67 @@ class SExpressionParser:
|
|
|
412
443
|
|
|
413
444
|
def _parse_title_block(self, item: List[Any]) -> Dict[str, Any]:
|
|
414
445
|
"""Parse title block information."""
|
|
415
|
-
|
|
416
|
-
for sub_item in item[1:]:
|
|
417
|
-
if isinstance(sub_item, list) and len(sub_item) >= 2:
|
|
418
|
-
key = str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
|
|
419
|
-
if key:
|
|
420
|
-
title_block[key] = sub_item[1] if len(sub_item) > 1 else None
|
|
421
|
-
return title_block
|
|
422
|
-
|
|
446
|
+
return self._metadata_parser._parse_title_block(item)
|
|
423
447
|
def _parse_symbol(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
424
448
|
"""Parse a symbol (component) definition."""
|
|
425
|
-
|
|
426
|
-
symbol_data = {
|
|
427
|
-
"lib_id": None,
|
|
428
|
-
"position": Point(0, 0),
|
|
429
|
-
"rotation": 0,
|
|
430
|
-
"uuid": None,
|
|
431
|
-
"reference": None,
|
|
432
|
-
"value": None,
|
|
433
|
-
"footprint": None,
|
|
434
|
-
"properties": {},
|
|
435
|
-
"pins": [],
|
|
436
|
-
"in_bom": True,
|
|
437
|
-
"on_board": True,
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
for sub_item in item[1:]:
|
|
441
|
-
if not isinstance(sub_item, list) or len(sub_item) == 0:
|
|
442
|
-
continue
|
|
443
|
-
|
|
444
|
-
element_type = (
|
|
445
|
-
str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
if element_type == "lib_id":
|
|
449
|
-
symbol_data["lib_id"] = sub_item[1] if len(sub_item) > 1 else None
|
|
450
|
-
elif element_type == "at":
|
|
451
|
-
if len(sub_item) >= 3:
|
|
452
|
-
symbol_data["position"] = Point(float(sub_item[1]), float(sub_item[2]))
|
|
453
|
-
if len(sub_item) > 3:
|
|
454
|
-
symbol_data["rotation"] = float(sub_item[3])
|
|
455
|
-
elif element_type == "uuid":
|
|
456
|
-
symbol_data["uuid"] = sub_item[1] if len(sub_item) > 1 else None
|
|
457
|
-
elif element_type == "property":
|
|
458
|
-
prop_data = self._parse_property(sub_item)
|
|
459
|
-
if prop_data:
|
|
460
|
-
prop_name = prop_data.get("name")
|
|
461
|
-
if prop_name == "Reference":
|
|
462
|
-
symbol_data["reference"] = prop_data.get("value")
|
|
463
|
-
elif prop_name == "Value":
|
|
464
|
-
symbol_data["value"] = prop_data.get("value")
|
|
465
|
-
elif prop_name == "Footprint":
|
|
466
|
-
symbol_data["footprint"] = prop_data.get("value")
|
|
467
|
-
else:
|
|
468
|
-
# Unescape quotes in property values when loading
|
|
469
|
-
prop_value = prop_data.get("value")
|
|
470
|
-
if prop_value:
|
|
471
|
-
prop_value = str(prop_value).replace('\\"', '"')
|
|
472
|
-
symbol_data["properties"][prop_name] = prop_value
|
|
473
|
-
elif element_type == "in_bom":
|
|
474
|
-
symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
|
|
475
|
-
elif element_type == "on_board":
|
|
476
|
-
symbol_data["on_board"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
|
|
477
|
-
|
|
478
|
-
return symbol_data
|
|
479
|
-
|
|
480
|
-
except Exception as e:
|
|
481
|
-
logger.warning(f"Error parsing symbol: {e}")
|
|
482
|
-
return None
|
|
483
|
-
|
|
449
|
+
return self._symbol_parser._parse_symbol(item)
|
|
484
450
|
def _parse_property(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
485
451
|
"""Parse a property definition."""
|
|
486
|
-
|
|
487
|
-
return None
|
|
488
|
-
|
|
489
|
-
return {
|
|
490
|
-
"name": item[1] if len(item) > 1 else None,
|
|
491
|
-
"value": item[2] if len(item) > 2 else None,
|
|
492
|
-
}
|
|
493
|
-
|
|
452
|
+
return self._symbol_parser._parse_property(item)
|
|
494
453
|
def _parse_wire(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
495
454
|
"""Parse a wire definition."""
|
|
496
|
-
|
|
497
|
-
"points": [],
|
|
498
|
-
"stroke_width": 0.0,
|
|
499
|
-
"stroke_type": "default",
|
|
500
|
-
"uuid": None,
|
|
501
|
-
"wire_type": "wire", # Default to wire (vs bus)
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
for elem in item[1:]:
|
|
505
|
-
if not isinstance(elem, list):
|
|
506
|
-
continue
|
|
507
|
-
|
|
508
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
509
|
-
|
|
510
|
-
if elem_type == "pts":
|
|
511
|
-
# Parse points: (pts (xy x1 y1) (xy x2 y2) ...)
|
|
512
|
-
for pt in elem[1:]:
|
|
513
|
-
if isinstance(pt, list) and len(pt) >= 3:
|
|
514
|
-
if str(pt[0]) == "xy":
|
|
515
|
-
x, y = float(pt[1]), float(pt[2])
|
|
516
|
-
wire_data["points"].append({"x": x, "y": y})
|
|
517
|
-
|
|
518
|
-
elif elem_type == "stroke":
|
|
519
|
-
# Parse stroke: (stroke (width 0) (type default))
|
|
520
|
-
for stroke_elem in elem[1:]:
|
|
521
|
-
if isinstance(stroke_elem, list) and len(stroke_elem) >= 2:
|
|
522
|
-
stroke_type = str(stroke_elem[0])
|
|
523
|
-
if stroke_type == "width":
|
|
524
|
-
wire_data["stroke_width"] = float(stroke_elem[1])
|
|
525
|
-
elif stroke_type == "type":
|
|
526
|
-
wire_data["stroke_type"] = str(stroke_elem[1])
|
|
527
|
-
|
|
528
|
-
elif elem_type == "uuid":
|
|
529
|
-
wire_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
530
|
-
|
|
531
|
-
# Only return wire if it has at least 2 points
|
|
532
|
-
if len(wire_data["points"]) >= 2:
|
|
533
|
-
return wire_data
|
|
534
|
-
else:
|
|
535
|
-
logger.warning(f"Wire has insufficient points: {len(wire_data['points'])}")
|
|
536
|
-
return None
|
|
537
|
-
|
|
455
|
+
return self._wire_parser._parse_wire(item)
|
|
538
456
|
def _parse_junction(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
539
457
|
"""Parse a junction definition."""
|
|
540
|
-
|
|
541
|
-
"position": {"x": 0, "y": 0},
|
|
542
|
-
"diameter": 0,
|
|
543
|
-
"color": (0, 0, 0, 0),
|
|
544
|
-
"uuid": None,
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
for elem in item[1:]:
|
|
548
|
-
if not isinstance(elem, list):
|
|
549
|
-
continue
|
|
550
|
-
|
|
551
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
552
|
-
|
|
553
|
-
if elem_type == "at":
|
|
554
|
-
# Parse position: (at x y)
|
|
555
|
-
if len(elem) >= 3:
|
|
556
|
-
junction_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
557
|
-
|
|
558
|
-
elif elem_type == "diameter":
|
|
559
|
-
# Parse diameter: (diameter value)
|
|
560
|
-
if len(elem) >= 2:
|
|
561
|
-
junction_data["diameter"] = float(elem[1])
|
|
562
|
-
|
|
563
|
-
elif elem_type == "color":
|
|
564
|
-
# Parse color: (color r g b a)
|
|
565
|
-
if len(elem) >= 5:
|
|
566
|
-
junction_data["color"] = (
|
|
567
|
-
int(elem[1]),
|
|
568
|
-
int(elem[2]),
|
|
569
|
-
int(elem[3]),
|
|
570
|
-
int(elem[4]),
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
elif elem_type == "uuid":
|
|
574
|
-
junction_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
575
|
-
|
|
576
|
-
return junction_data
|
|
577
|
-
|
|
458
|
+
return self._wire_parser._parse_junction(item)
|
|
578
459
|
def _parse_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
579
460
|
"""Parse a label definition."""
|
|
580
|
-
|
|
581
|
-
if len(item) < 2:
|
|
582
|
-
return None
|
|
583
|
-
|
|
584
|
-
label_data = {
|
|
585
|
-
"text": str(item[1]), # Label text is second element
|
|
586
|
-
"position": {"x": 0, "y": 0},
|
|
587
|
-
"rotation": 0,
|
|
588
|
-
"size": 1.27,
|
|
589
|
-
"uuid": None,
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
for elem in item[2:]: # Skip label keyword and text
|
|
593
|
-
if not isinstance(elem, list):
|
|
594
|
-
continue
|
|
595
|
-
|
|
596
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
597
|
-
|
|
598
|
-
if elem_type == "at":
|
|
599
|
-
# Parse position: (at x y rotation)
|
|
600
|
-
if len(elem) >= 3:
|
|
601
|
-
label_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
602
|
-
if len(elem) >= 4:
|
|
603
|
-
label_data["rotation"] = float(elem[3])
|
|
604
|
-
|
|
605
|
-
elif elem_type == "effects":
|
|
606
|
-
# Parse effects for font size: (effects (font (size x y)) ...)
|
|
607
|
-
for effect_elem in elem[1:]:
|
|
608
|
-
if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
|
|
609
|
-
for font_elem in effect_elem[1:]:
|
|
610
|
-
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
611
|
-
if len(font_elem) >= 2:
|
|
612
|
-
label_data["size"] = float(font_elem[1])
|
|
613
|
-
|
|
614
|
-
elif elem_type == "uuid":
|
|
615
|
-
label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
616
|
-
|
|
617
|
-
return label_data
|
|
618
|
-
|
|
461
|
+
return self._label_parser._parse_label(item)
|
|
619
462
|
def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
620
463
|
"""Parse a hierarchical label definition."""
|
|
621
|
-
|
|
622
|
-
if len(item) < 2:
|
|
623
|
-
return None
|
|
624
|
-
|
|
625
|
-
hlabel_data = {
|
|
626
|
-
"text": str(item[1]), # Hierarchical label text is second element
|
|
627
|
-
"shape": "input", # input/output/bidirectional/tri_state/passive
|
|
628
|
-
"position": {"x": 0, "y": 0},
|
|
629
|
-
"rotation": 0,
|
|
630
|
-
"size": 1.27,
|
|
631
|
-
"justify": "left",
|
|
632
|
-
"uuid": None,
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
for elem in item[2:]: # Skip hierarchical_label keyword and text
|
|
636
|
-
if not isinstance(elem, list):
|
|
637
|
-
continue
|
|
638
|
-
|
|
639
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
640
|
-
|
|
641
|
-
if elem_type == "shape":
|
|
642
|
-
# Parse shape: (shape input)
|
|
643
|
-
if len(elem) >= 2:
|
|
644
|
-
hlabel_data["shape"] = str(elem[1])
|
|
645
|
-
|
|
646
|
-
elif elem_type == "at":
|
|
647
|
-
# Parse position: (at x y rotation)
|
|
648
|
-
if len(elem) >= 3:
|
|
649
|
-
hlabel_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
650
|
-
if len(elem) >= 4:
|
|
651
|
-
hlabel_data["rotation"] = float(elem[3])
|
|
652
|
-
|
|
653
|
-
elif elem_type == "effects":
|
|
654
|
-
# Parse effects for font size and justification: (effects (font (size x y)) (justify left))
|
|
655
|
-
for effect_elem in elem[1:]:
|
|
656
|
-
if isinstance(effect_elem, list):
|
|
657
|
-
effect_type = (
|
|
658
|
-
str(effect_elem[0])
|
|
659
|
-
if isinstance(effect_elem[0], sexpdata.Symbol)
|
|
660
|
-
else None
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
if effect_type == "font":
|
|
664
|
-
# Parse font size
|
|
665
|
-
for font_elem in effect_elem[1:]:
|
|
666
|
-
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
667
|
-
if len(font_elem) >= 2:
|
|
668
|
-
hlabel_data["size"] = float(font_elem[1])
|
|
669
|
-
|
|
670
|
-
elif effect_type == "justify":
|
|
671
|
-
# Parse justification (e.g., "left", "right")
|
|
672
|
-
if len(effect_elem) >= 2:
|
|
673
|
-
hlabel_data["justify"] = str(effect_elem[1])
|
|
674
|
-
|
|
675
|
-
elif elem_type == "uuid":
|
|
676
|
-
hlabel_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
677
|
-
|
|
678
|
-
return hlabel_data
|
|
679
|
-
|
|
464
|
+
return self._label_parser._parse_hierarchical_label(item)
|
|
680
465
|
def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
681
466
|
"""Parse a no_connect symbol."""
|
|
682
|
-
|
|
683
|
-
no_connect_data = {"position": {"x": 0, "y": 0}, "uuid": None}
|
|
684
|
-
|
|
685
|
-
for elem in item[1:]:
|
|
686
|
-
if not isinstance(elem, list):
|
|
687
|
-
continue
|
|
688
|
-
|
|
689
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
690
|
-
|
|
691
|
-
if elem_type == "at":
|
|
692
|
-
if len(elem) >= 3:
|
|
693
|
-
no_connect_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
694
|
-
elif elem_type == "uuid":
|
|
695
|
-
no_connect_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
696
|
-
|
|
697
|
-
return no_connect_data
|
|
698
|
-
|
|
467
|
+
return self._wire_parser._parse_no_connect(item)
|
|
699
468
|
def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
700
469
|
"""Parse a text element."""
|
|
701
|
-
|
|
702
|
-
if len(item) < 2:
|
|
703
|
-
return None
|
|
704
|
-
|
|
705
|
-
text_data = {
|
|
706
|
-
"text": str(item[1]),
|
|
707
|
-
"exclude_from_sim": False,
|
|
708
|
-
"position": {"x": 0, "y": 0},
|
|
709
|
-
"rotation": 0,
|
|
710
|
-
"size": 1.27,
|
|
711
|
-
"uuid": None,
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
for elem in item[2:]:
|
|
715
|
-
if not isinstance(elem, list):
|
|
716
|
-
continue
|
|
717
|
-
|
|
718
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
719
|
-
|
|
720
|
-
if elem_type == "exclude_from_sim":
|
|
721
|
-
if len(elem) >= 2:
|
|
722
|
-
text_data["exclude_from_sim"] = str(elem[1]) == "yes"
|
|
723
|
-
elif elem_type == "at":
|
|
724
|
-
if len(elem) >= 3:
|
|
725
|
-
text_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
726
|
-
if len(elem) >= 4:
|
|
727
|
-
text_data["rotation"] = float(elem[3])
|
|
728
|
-
elif elem_type == "effects":
|
|
729
|
-
for effect_elem in elem[1:]:
|
|
730
|
-
if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
|
|
731
|
-
for font_elem in effect_elem[1:]:
|
|
732
|
-
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
733
|
-
if len(font_elem) >= 2:
|
|
734
|
-
text_data["size"] = float(font_elem[1])
|
|
735
|
-
elif elem_type == "uuid":
|
|
736
|
-
text_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
737
|
-
|
|
738
|
-
return text_data
|
|
739
|
-
|
|
470
|
+
return self._text_parser._parse_text(item)
|
|
740
471
|
def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
741
472
|
"""Parse a text_box element."""
|
|
742
|
-
|
|
743
|
-
if len(item) < 2:
|
|
744
|
-
return None
|
|
745
|
-
|
|
746
|
-
text_box_data = {
|
|
747
|
-
"text": str(item[1]),
|
|
748
|
-
"exclude_from_sim": False,
|
|
749
|
-
"position": {"x": 0, "y": 0},
|
|
750
|
-
"rotation": 0,
|
|
751
|
-
"size": {"width": 0, "height": 0},
|
|
752
|
-
"margins": (0.9525, 0.9525, 0.9525, 0.9525),
|
|
753
|
-
"stroke_width": 0,
|
|
754
|
-
"stroke_type": "solid",
|
|
755
|
-
"fill_type": "none",
|
|
756
|
-
"font_size": 1.27,
|
|
757
|
-
"justify_horizontal": "left",
|
|
758
|
-
"justify_vertical": "top",
|
|
759
|
-
"uuid": None,
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
for elem in item[2:]:
|
|
763
|
-
if not isinstance(elem, list):
|
|
764
|
-
continue
|
|
765
|
-
|
|
766
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
767
|
-
|
|
768
|
-
if elem_type == "exclude_from_sim":
|
|
769
|
-
if len(elem) >= 2:
|
|
770
|
-
text_box_data["exclude_from_sim"] = str(elem[1]) == "yes"
|
|
771
|
-
elif elem_type == "at":
|
|
772
|
-
if len(elem) >= 3:
|
|
773
|
-
text_box_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
774
|
-
if len(elem) >= 4:
|
|
775
|
-
text_box_data["rotation"] = float(elem[3])
|
|
776
|
-
elif elem_type == "size":
|
|
777
|
-
if len(elem) >= 3:
|
|
778
|
-
text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
|
|
779
|
-
elif elem_type == "margins":
|
|
780
|
-
if len(elem) >= 5:
|
|
781
|
-
text_box_data["margins"] = (
|
|
782
|
-
float(elem[1]),
|
|
783
|
-
float(elem[2]),
|
|
784
|
-
float(elem[3]),
|
|
785
|
-
float(elem[4]),
|
|
786
|
-
)
|
|
787
|
-
elif elem_type == "stroke":
|
|
788
|
-
for stroke_elem in elem[1:]:
|
|
789
|
-
if isinstance(stroke_elem, list):
|
|
790
|
-
stroke_type = str(stroke_elem[0])
|
|
791
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
792
|
-
text_box_data["stroke_width"] = float(stroke_elem[1])
|
|
793
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
794
|
-
text_box_data["stroke_type"] = str(stroke_elem[1])
|
|
795
|
-
elif elem_type == "fill":
|
|
796
|
-
for fill_elem in elem[1:]:
|
|
797
|
-
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
798
|
-
text_box_data["fill_type"] = (
|
|
799
|
-
str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
800
|
-
)
|
|
801
|
-
elif elem_type == "effects":
|
|
802
|
-
for effect_elem in elem[1:]:
|
|
803
|
-
if isinstance(effect_elem, list):
|
|
804
|
-
effect_type = str(effect_elem[0])
|
|
805
|
-
if effect_type == "font":
|
|
806
|
-
for font_elem in effect_elem[1:]:
|
|
807
|
-
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
808
|
-
if len(font_elem) >= 2:
|
|
809
|
-
text_box_data["font_size"] = float(font_elem[1])
|
|
810
|
-
elif effect_type == "justify":
|
|
811
|
-
if len(effect_elem) >= 2:
|
|
812
|
-
text_box_data["justify_horizontal"] = str(effect_elem[1])
|
|
813
|
-
if len(effect_elem) >= 3:
|
|
814
|
-
text_box_data["justify_vertical"] = str(effect_elem[2])
|
|
815
|
-
elif elem_type == "uuid":
|
|
816
|
-
text_box_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
817
|
-
|
|
818
|
-
return text_box_data
|
|
819
|
-
|
|
473
|
+
return self._text_parser._parse_text_box(item)
|
|
820
474
|
def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
821
475
|
"""Parse a hierarchical sheet."""
|
|
822
|
-
|
|
823
|
-
sheet_data = {
|
|
824
|
-
"position": {"x": 0, "y": 0},
|
|
825
|
-
"size": {"width": 0, "height": 0},
|
|
826
|
-
"exclude_from_sim": False,
|
|
827
|
-
"in_bom": True,
|
|
828
|
-
"on_board": True,
|
|
829
|
-
"dnp": False,
|
|
830
|
-
"fields_autoplaced": True,
|
|
831
|
-
"stroke_width": 0.1524,
|
|
832
|
-
"stroke_type": "solid",
|
|
833
|
-
"fill_color": (0, 0, 0, 0.0),
|
|
834
|
-
"uuid": None,
|
|
835
|
-
"name": "Sheet",
|
|
836
|
-
"filename": "sheet.kicad_sch",
|
|
837
|
-
"pins": [],
|
|
838
|
-
"project_name": "",
|
|
839
|
-
"page_number": "2",
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
for elem in item[1:]:
|
|
843
|
-
if not isinstance(elem, list):
|
|
844
|
-
continue
|
|
845
|
-
|
|
846
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
847
|
-
|
|
848
|
-
if elem_type == "at":
|
|
849
|
-
if len(elem) >= 3:
|
|
850
|
-
sheet_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
851
|
-
elif elem_type == "size":
|
|
852
|
-
if len(elem) >= 3:
|
|
853
|
-
sheet_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
|
|
854
|
-
elif elem_type == "exclude_from_sim":
|
|
855
|
-
sheet_data["exclude_from_sim"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
856
|
-
elif elem_type == "in_bom":
|
|
857
|
-
sheet_data["in_bom"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
858
|
-
elif elem_type == "on_board":
|
|
859
|
-
sheet_data["on_board"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
860
|
-
elif elem_type == "dnp":
|
|
861
|
-
sheet_data["dnp"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
862
|
-
elif elem_type == "fields_autoplaced":
|
|
863
|
-
sheet_data["fields_autoplaced"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
864
|
-
elif elem_type == "stroke":
|
|
865
|
-
for stroke_elem in elem[1:]:
|
|
866
|
-
if isinstance(stroke_elem, list):
|
|
867
|
-
stroke_type = str(stroke_elem[0])
|
|
868
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
869
|
-
sheet_data["stroke_width"] = float(stroke_elem[1])
|
|
870
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
871
|
-
sheet_data["stroke_type"] = str(stroke_elem[1])
|
|
872
|
-
elif elem_type == "fill":
|
|
873
|
-
for fill_elem in elem[1:]:
|
|
874
|
-
if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
|
|
875
|
-
if len(fill_elem) >= 5:
|
|
876
|
-
sheet_data["fill_color"] = (
|
|
877
|
-
int(fill_elem[1]),
|
|
878
|
-
int(fill_elem[2]),
|
|
879
|
-
int(fill_elem[3]),
|
|
880
|
-
float(fill_elem[4]),
|
|
881
|
-
)
|
|
882
|
-
elif elem_type == "uuid":
|
|
883
|
-
sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
884
|
-
elif elem_type == "property":
|
|
885
|
-
if len(elem) >= 3:
|
|
886
|
-
prop_name = str(elem[1])
|
|
887
|
-
prop_value = str(elem[2])
|
|
888
|
-
if prop_name == "Sheetname":
|
|
889
|
-
sheet_data["name"] = prop_value
|
|
890
|
-
elif prop_name == "Sheetfile":
|
|
891
|
-
sheet_data["filename"] = prop_value
|
|
892
|
-
elif elem_type == "pin":
|
|
893
|
-
# Parse sheet pin - reuse existing _parse_sheet_pin helper
|
|
894
|
-
pin_data = self._parse_sheet_pin_for_read(elem)
|
|
895
|
-
if pin_data:
|
|
896
|
-
sheet_data["pins"].append(pin_data)
|
|
897
|
-
elif elem_type == "instances":
|
|
898
|
-
# Parse instances for project name and page number
|
|
899
|
-
for inst_elem in elem[1:]:
|
|
900
|
-
if isinstance(inst_elem, list) and str(inst_elem[0]) == "project":
|
|
901
|
-
if len(inst_elem) >= 2:
|
|
902
|
-
sheet_data["project_name"] = str(inst_elem[1])
|
|
903
|
-
for path_elem in inst_elem[2:]:
|
|
904
|
-
if isinstance(path_elem, list) and str(path_elem[0]) == "path":
|
|
905
|
-
for page_elem in path_elem[1:]:
|
|
906
|
-
if isinstance(page_elem, list) and str(page_elem[0]) == "page":
|
|
907
|
-
sheet_data["page_number"] = (
|
|
908
|
-
str(page_elem[1]) if len(page_elem) > 1 else "2"
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
return sheet_data
|
|
912
|
-
|
|
476
|
+
return self._sheet_parser._parse_sheet(item)
|
|
913
477
|
def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
914
478
|
"""Parse a sheet pin (for reading during sheet parsing)."""
|
|
915
|
-
|
|
916
|
-
if len(item) < 3:
|
|
917
|
-
return None
|
|
918
|
-
|
|
919
|
-
pin_data = {
|
|
920
|
-
"name": str(item[1]),
|
|
921
|
-
"pin_type": str(item[2]) if len(item) > 2 else "input",
|
|
922
|
-
"position": {"x": 0, "y": 0},
|
|
923
|
-
"rotation": 0,
|
|
924
|
-
"size": 1.27,
|
|
925
|
-
"justify": "right",
|
|
926
|
-
"uuid": None,
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
for elem in item[3:]:
|
|
930
|
-
if not isinstance(elem, list):
|
|
931
|
-
continue
|
|
932
|
-
|
|
933
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
934
|
-
|
|
935
|
-
if elem_type == "at":
|
|
936
|
-
if len(elem) >= 3:
|
|
937
|
-
pin_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
938
|
-
if len(elem) >= 4:
|
|
939
|
-
pin_data["rotation"] = float(elem[3])
|
|
940
|
-
elif elem_type == "uuid":
|
|
941
|
-
pin_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
942
|
-
elif elem_type == "effects":
|
|
943
|
-
for effect_elem in elem[1:]:
|
|
944
|
-
if isinstance(effect_elem, list):
|
|
945
|
-
effect_type = str(effect_elem[0])
|
|
946
|
-
if effect_type == "font":
|
|
947
|
-
for font_elem in effect_elem[1:]:
|
|
948
|
-
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
949
|
-
if len(font_elem) >= 2:
|
|
950
|
-
pin_data["size"] = float(font_elem[1])
|
|
951
|
-
elif effect_type == "justify":
|
|
952
|
-
if len(effect_elem) >= 2:
|
|
953
|
-
pin_data["justify"] = str(effect_elem[1])
|
|
954
|
-
|
|
955
|
-
return pin_data
|
|
956
|
-
|
|
479
|
+
return self._sheet_parser._parse_sheet_pin_for_read(item)
|
|
957
480
|
def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
958
481
|
"""Parse a polyline graphical element."""
|
|
959
|
-
|
|
960
|
-
polyline_data = {"points": [], "stroke_width": 0, "stroke_type": "default", "uuid": None}
|
|
961
|
-
|
|
962
|
-
for elem in item[1:]:
|
|
963
|
-
if not isinstance(elem, list):
|
|
964
|
-
continue
|
|
965
|
-
|
|
966
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
967
|
-
|
|
968
|
-
if elem_type == "pts":
|
|
969
|
-
for pt in elem[1:]:
|
|
970
|
-
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
971
|
-
polyline_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
972
|
-
elif elem_type == "stroke":
|
|
973
|
-
for stroke_elem in elem[1:]:
|
|
974
|
-
if isinstance(stroke_elem, list):
|
|
975
|
-
stroke_type = str(stroke_elem[0])
|
|
976
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
977
|
-
polyline_data["stroke_width"] = float(stroke_elem[1])
|
|
978
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
979
|
-
polyline_data["stroke_type"] = str(stroke_elem[1])
|
|
980
|
-
elif elem_type == "uuid":
|
|
981
|
-
polyline_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
982
|
-
|
|
983
|
-
return polyline_data if polyline_data["points"] else None
|
|
984
|
-
|
|
482
|
+
return self._graphics_parser._parse_polyline(item)
|
|
985
483
|
def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
986
484
|
"""Parse an arc graphical element."""
|
|
987
|
-
|
|
988
|
-
arc_data = {
|
|
989
|
-
"start": {"x": 0, "y": 0},
|
|
990
|
-
"mid": {"x": 0, "y": 0},
|
|
991
|
-
"end": {"x": 0, "y": 0},
|
|
992
|
-
"stroke_width": 0,
|
|
993
|
-
"stroke_type": "default",
|
|
994
|
-
"fill_type": "none",
|
|
995
|
-
"uuid": None,
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
for elem in item[1:]:
|
|
999
|
-
if not isinstance(elem, list):
|
|
1000
|
-
continue
|
|
1001
|
-
|
|
1002
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1003
|
-
|
|
1004
|
-
if elem_type == "start" and len(elem) >= 3:
|
|
1005
|
-
arc_data["start"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1006
|
-
elif elem_type == "mid" and len(elem) >= 3:
|
|
1007
|
-
arc_data["mid"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1008
|
-
elif elem_type == "end" and len(elem) >= 3:
|
|
1009
|
-
arc_data["end"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1010
|
-
elif elem_type == "stroke":
|
|
1011
|
-
for stroke_elem in elem[1:]:
|
|
1012
|
-
if isinstance(stroke_elem, list):
|
|
1013
|
-
stroke_type = str(stroke_elem[0])
|
|
1014
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1015
|
-
arc_data["stroke_width"] = float(stroke_elem[1])
|
|
1016
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1017
|
-
arc_data["stroke_type"] = str(stroke_elem[1])
|
|
1018
|
-
elif elem_type == "fill":
|
|
1019
|
-
for fill_elem in elem[1:]:
|
|
1020
|
-
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1021
|
-
arc_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1022
|
-
elif elem_type == "uuid":
|
|
1023
|
-
arc_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1024
|
-
|
|
1025
|
-
return arc_data
|
|
1026
|
-
|
|
485
|
+
return self._graphics_parser._parse_arc(item)
|
|
1027
486
|
def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1028
487
|
"""Parse a circle graphical element."""
|
|
1029
|
-
|
|
1030
|
-
circle_data = {
|
|
1031
|
-
"center": {"x": 0, "y": 0},
|
|
1032
|
-
"radius": 0,
|
|
1033
|
-
"stroke_width": 0,
|
|
1034
|
-
"stroke_type": "default",
|
|
1035
|
-
"fill_type": "none",
|
|
1036
|
-
"uuid": None,
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
for elem in item[1:]:
|
|
1040
|
-
if not isinstance(elem, list):
|
|
1041
|
-
continue
|
|
1042
|
-
|
|
1043
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1044
|
-
|
|
1045
|
-
if elem_type == "center" and len(elem) >= 3:
|
|
1046
|
-
circle_data["center"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1047
|
-
elif elem_type == "radius" and len(elem) >= 2:
|
|
1048
|
-
circle_data["radius"] = float(elem[1])
|
|
1049
|
-
elif elem_type == "stroke":
|
|
1050
|
-
for stroke_elem in elem[1:]:
|
|
1051
|
-
if isinstance(stroke_elem, list):
|
|
1052
|
-
stroke_type = str(stroke_elem[0])
|
|
1053
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1054
|
-
circle_data["stroke_width"] = float(stroke_elem[1])
|
|
1055
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1056
|
-
circle_data["stroke_type"] = str(stroke_elem[1])
|
|
1057
|
-
elif elem_type == "fill":
|
|
1058
|
-
for fill_elem in elem[1:]:
|
|
1059
|
-
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1060
|
-
circle_data["fill_type"] = (
|
|
1061
|
-
str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1062
|
-
)
|
|
1063
|
-
elif elem_type == "uuid":
|
|
1064
|
-
circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1065
|
-
|
|
1066
|
-
return circle_data
|
|
1067
|
-
|
|
488
|
+
return self._graphics_parser._parse_circle(item)
|
|
1068
489
|
def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1069
490
|
"""Parse a bezier curve graphical element."""
|
|
1070
|
-
|
|
1071
|
-
bezier_data = {
|
|
1072
|
-
"points": [],
|
|
1073
|
-
"stroke_width": 0,
|
|
1074
|
-
"stroke_type": "default",
|
|
1075
|
-
"fill_type": "none",
|
|
1076
|
-
"uuid": None,
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
for elem in item[1:]:
|
|
1080
|
-
if not isinstance(elem, list):
|
|
1081
|
-
continue
|
|
1082
|
-
|
|
1083
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1084
|
-
|
|
1085
|
-
if elem_type == "pts":
|
|
1086
|
-
for pt in elem[1:]:
|
|
1087
|
-
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
1088
|
-
bezier_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
1089
|
-
elif elem_type == "stroke":
|
|
1090
|
-
for stroke_elem in elem[1:]:
|
|
1091
|
-
if isinstance(stroke_elem, list):
|
|
1092
|
-
stroke_type = str(stroke_elem[0])
|
|
1093
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1094
|
-
bezier_data["stroke_width"] = float(stroke_elem[1])
|
|
1095
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1096
|
-
bezier_data["stroke_type"] = str(stroke_elem[1])
|
|
1097
|
-
elif elem_type == "fill":
|
|
1098
|
-
for fill_elem in elem[1:]:
|
|
1099
|
-
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1100
|
-
bezier_data["fill_type"] = (
|
|
1101
|
-
str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1102
|
-
)
|
|
1103
|
-
elif elem_type == "uuid":
|
|
1104
|
-
bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1105
|
-
|
|
1106
|
-
return bezier_data if bezier_data["points"] else None
|
|
1107
|
-
|
|
491
|
+
return self._graphics_parser._parse_bezier(item)
|
|
1108
492
|
def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1109
493
|
"""Parse a rectangle graphical element."""
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
for elem in item[1:]:
|
|
1113
|
-
if not isinstance(elem, list):
|
|
1114
|
-
continue
|
|
1115
|
-
|
|
1116
|
-
elem_type = str(elem[0])
|
|
1117
|
-
|
|
1118
|
-
if elem_type == "start" and len(elem) >= 3:
|
|
1119
|
-
rectangle["start"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1120
|
-
elif elem_type == "end" and len(elem) >= 3:
|
|
1121
|
-
rectangle["end"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1122
|
-
elif elem_type == "stroke":
|
|
1123
|
-
for stroke_elem in elem[1:]:
|
|
1124
|
-
if isinstance(stroke_elem, list):
|
|
1125
|
-
stroke_type = str(stroke_elem[0])
|
|
1126
|
-
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1127
|
-
rectangle["stroke_width"] = float(stroke_elem[1])
|
|
1128
|
-
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1129
|
-
rectangle["stroke_type"] = str(stroke_elem[1])
|
|
1130
|
-
elif elem_type == "fill":
|
|
1131
|
-
for fill_elem in elem[1:]:
|
|
1132
|
-
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1133
|
-
rectangle["fill_type"] = (
|
|
1134
|
-
str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1135
|
-
)
|
|
1136
|
-
elif elem_type == "uuid" and len(elem) >= 2:
|
|
1137
|
-
rectangle["uuid"] = str(elem[1])
|
|
1138
|
-
|
|
1139
|
-
return rectangle if rectangle else None
|
|
1140
|
-
|
|
494
|
+
return self._graphics_parser._parse_rectangle(item)
|
|
1141
495
|
def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1142
496
|
"""Parse an image element."""
|
|
1143
|
-
|
|
1144
|
-
image = {"position": {"x": 0, "y": 0}, "data": "", "scale": 1.0, "uuid": None}
|
|
1145
|
-
|
|
1146
|
-
for elem in item[1:]:
|
|
1147
|
-
if not isinstance(elem, list):
|
|
1148
|
-
continue
|
|
1149
|
-
|
|
1150
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1151
|
-
|
|
1152
|
-
if elem_type == "at" and len(elem) >= 3:
|
|
1153
|
-
image["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1154
|
-
elif elem_type == "scale" and len(elem) >= 2:
|
|
1155
|
-
image["scale"] = float(elem[1])
|
|
1156
|
-
elif elem_type == "data" and len(elem) >= 2:
|
|
1157
|
-
# The data can be spread across multiple string elements
|
|
1158
|
-
data_parts = []
|
|
1159
|
-
for data_elem in elem[1:]:
|
|
1160
|
-
data_parts.append(str(data_elem).strip('"'))
|
|
1161
|
-
image["data"] = "".join(data_parts)
|
|
1162
|
-
elif elem_type == "uuid" and len(elem) >= 2:
|
|
1163
|
-
image["uuid"] = str(elem[1]).strip('"')
|
|
1164
|
-
|
|
1165
|
-
return image if image.get("uuid") and image.get("data") else None
|
|
1166
|
-
|
|
497
|
+
return self._graphics_parser._parse_image(item)
|
|
1167
498
|
def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
|
|
1168
499
|
"""Parse lib_symbols section."""
|
|
1169
|
-
|
|
1170
|
-
return {}
|
|
1171
|
-
|
|
1172
|
-
# Conversion methods from internal format to S-expression
|
|
500
|
+
return self._library_parser._parse_lib_symbols(item)
|
|
1173
501
|
def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
|
|
1174
502
|
"""Convert title block to S-expression."""
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
# Add standard fields
|
|
1178
|
-
for key in ["title", "date", "rev", "company"]:
|
|
1179
|
-
if key in title_block and title_block[key]:
|
|
1180
|
-
sexp.append([sexpdata.Symbol(key), title_block[key]])
|
|
1181
|
-
|
|
1182
|
-
# Add comments with special formatting
|
|
1183
|
-
comments = title_block.get("comments", {})
|
|
1184
|
-
if isinstance(comments, dict):
|
|
1185
|
-
for comment_num, comment_text in comments.items():
|
|
1186
|
-
sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
|
|
1187
|
-
|
|
1188
|
-
return sexp
|
|
1189
|
-
|
|
503
|
+
return self._metadata_parser._title_block_to_sexp(title_block)
|
|
1190
504
|
def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
|
|
1191
505
|
"""Convert symbol to S-expression."""
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
if symbol_data.get("lib_id"):
|
|
1195
|
-
sexp.append([sexpdata.Symbol("lib_id"), symbol_data["lib_id"]])
|
|
1196
|
-
|
|
1197
|
-
# Add position and rotation (preserve original format)
|
|
1198
|
-
pos = symbol_data.get("position", Point(0, 0))
|
|
1199
|
-
rotation = symbol_data.get("rotation", 0)
|
|
1200
|
-
# Format numbers as integers if they are whole numbers
|
|
1201
|
-
x = int(pos.x) if pos.x == int(pos.x) else pos.x
|
|
1202
|
-
y = int(pos.y) if pos.y == int(pos.y) else pos.y
|
|
1203
|
-
r = int(rotation) if rotation == int(rotation) else rotation
|
|
1204
|
-
# Always include rotation for format consistency with KiCAD
|
|
1205
|
-
sexp.append([sexpdata.Symbol("at"), x, y, r])
|
|
1206
|
-
|
|
1207
|
-
# Add unit (required by KiCAD)
|
|
1208
|
-
unit = symbol_data.get("unit", 1)
|
|
1209
|
-
sexp.append([sexpdata.Symbol("unit"), unit])
|
|
1210
|
-
|
|
1211
|
-
# Add simulation and board settings (required by KiCAD)
|
|
1212
|
-
sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
|
|
1213
|
-
sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
|
|
1214
|
-
sexp.append(
|
|
1215
|
-
[sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
|
|
1216
|
-
)
|
|
1217
|
-
sexp.append([sexpdata.Symbol("dnp"), "no"])
|
|
1218
|
-
sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
|
|
1219
|
-
|
|
1220
|
-
if symbol_data.get("uuid"):
|
|
1221
|
-
sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
|
|
1222
|
-
|
|
1223
|
-
# Add properties with proper positioning and effects
|
|
1224
|
-
lib_id = symbol_data.get("lib_id", "")
|
|
1225
|
-
is_power_symbol = "power:" in lib_id
|
|
1226
|
-
|
|
1227
|
-
if symbol_data.get("reference"):
|
|
1228
|
-
# Power symbol references should be hidden by default
|
|
1229
|
-
ref_hide = is_power_symbol
|
|
1230
|
-
ref_prop = self._create_property_with_positioning(
|
|
1231
|
-
"Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
|
|
1232
|
-
)
|
|
1233
|
-
sexp.append(ref_prop)
|
|
1234
|
-
|
|
1235
|
-
if symbol_data.get("value"):
|
|
1236
|
-
# Power symbol values need different positioning
|
|
1237
|
-
if is_power_symbol:
|
|
1238
|
-
val_prop = self._create_power_symbol_value_property(
|
|
1239
|
-
symbol_data["value"], pos, lib_id
|
|
1240
|
-
)
|
|
1241
|
-
else:
|
|
1242
|
-
val_prop = self._create_property_with_positioning(
|
|
1243
|
-
"Value", symbol_data["value"], pos, 1, "left"
|
|
1244
|
-
)
|
|
1245
|
-
sexp.append(val_prop)
|
|
1246
|
-
|
|
1247
|
-
footprint = symbol_data.get("footprint")
|
|
1248
|
-
if footprint is not None: # Include empty strings but not None
|
|
1249
|
-
fp_prop = self._create_property_with_positioning(
|
|
1250
|
-
"Footprint", footprint, pos, 2, "left", hide=True
|
|
1251
|
-
)
|
|
1252
|
-
sexp.append(fp_prop)
|
|
1253
|
-
|
|
1254
|
-
for prop_name, prop_value in symbol_data.get("properties", {}).items():
|
|
1255
|
-
escaped_value = str(prop_value).replace('"', '\\"')
|
|
1256
|
-
prop = self._create_property_with_positioning(
|
|
1257
|
-
prop_name, escaped_value, pos, 3, "left", hide=True
|
|
1258
|
-
)
|
|
1259
|
-
sexp.append(prop)
|
|
1260
|
-
|
|
1261
|
-
# Add pin UUID assignments (required by KiCAD)
|
|
1262
|
-
for pin in symbol_data.get("pins", []):
|
|
1263
|
-
pin_uuid = str(uuid.uuid4())
|
|
1264
|
-
# Ensure pin number is a string for proper quoting
|
|
1265
|
-
pin_number = str(pin.number)
|
|
1266
|
-
sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
|
|
1267
|
-
|
|
1268
|
-
# Add instances section (required by KiCAD)
|
|
1269
|
-
from .config import config
|
|
1270
|
-
|
|
1271
|
-
# Get project name from config or properties
|
|
1272
|
-
project_name = symbol_data.get("properties", {}).get("project_name")
|
|
1273
|
-
if not project_name:
|
|
1274
|
-
project_name = getattr(self, "project_name", config.defaults.project_name)
|
|
1275
|
-
|
|
1276
|
-
# CRITICAL FIX: Use the FULL hierarchy_path from properties if available
|
|
1277
|
-
# For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
|
|
1278
|
-
# This ensures KiCad can properly annotate components in sub-sheets
|
|
1279
|
-
hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
|
|
1280
|
-
if hierarchy_path:
|
|
1281
|
-
# Use the full hierarchical path (includes root + all sheet symbols)
|
|
1282
|
-
instance_path = hierarchy_path
|
|
1283
|
-
logger.debug(
|
|
1284
|
-
f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
|
|
1285
|
-
)
|
|
1286
|
-
else:
|
|
1287
|
-
# Fallback: use root_uuid or schematic_uuid for flat designs
|
|
1288
|
-
root_uuid = (
|
|
1289
|
-
symbol_data.get("properties", {}).get("root_uuid")
|
|
1290
|
-
or schematic_uuid
|
|
1291
|
-
or str(uuid.uuid4())
|
|
1292
|
-
)
|
|
1293
|
-
instance_path = f"/{root_uuid}"
|
|
1294
|
-
logger.debug(
|
|
1295
|
-
f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
|
|
1296
|
-
)
|
|
1297
|
-
|
|
1298
|
-
logger.debug(
|
|
1299
|
-
f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
|
|
1300
|
-
)
|
|
1301
|
-
logger.debug(f"🔧 Using project name: '{project_name}'")
|
|
1302
|
-
|
|
1303
|
-
sexp.append(
|
|
1304
|
-
[
|
|
1305
|
-
sexpdata.Symbol("instances"),
|
|
1306
|
-
[
|
|
1307
|
-
sexpdata.Symbol("project"),
|
|
1308
|
-
project_name,
|
|
1309
|
-
[
|
|
1310
|
-
sexpdata.Symbol("path"),
|
|
1311
|
-
instance_path,
|
|
1312
|
-
[sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
|
|
1313
|
-
[sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
|
|
1314
|
-
],
|
|
1315
|
-
],
|
|
1316
|
-
]
|
|
1317
|
-
)
|
|
1318
|
-
|
|
1319
|
-
return sexp
|
|
1320
|
-
|
|
506
|
+
return self._symbol_parser._symbol_to_sexp(symbol_data, schematic_uuid)
|
|
1321
507
|
def _create_property_with_positioning(
|
|
1322
508
|
self,
|
|
1323
509
|
prop_name: str,
|
|
@@ -1401,950 +587,74 @@ class SExpressionParser:
|
|
|
1401
587
|
|
|
1402
588
|
def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
|
|
1403
589
|
"""Convert wire to S-expression."""
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
# Add points (pts section)
|
|
1407
|
-
points = wire_data.get("points", [])
|
|
1408
|
-
if len(points) >= 2:
|
|
1409
|
-
pts_sexp = [sexpdata.Symbol("pts")]
|
|
1410
|
-
for point in points:
|
|
1411
|
-
if isinstance(point, dict):
|
|
1412
|
-
x, y = point["x"], point["y"]
|
|
1413
|
-
elif isinstance(point, (list, tuple)) and len(point) >= 2:
|
|
1414
|
-
x, y = point[0], point[1]
|
|
1415
|
-
else:
|
|
1416
|
-
# Assume it's a Point object
|
|
1417
|
-
x, y = point.x, point.y
|
|
1418
|
-
|
|
1419
|
-
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
1420
|
-
if isinstance(x, float) and x.is_integer():
|
|
1421
|
-
x = int(x)
|
|
1422
|
-
if isinstance(y, float) and y.is_integer():
|
|
1423
|
-
y = int(y)
|
|
1424
|
-
|
|
1425
|
-
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
1426
|
-
sexp.append(pts_sexp)
|
|
1427
|
-
|
|
1428
|
-
# Add stroke information
|
|
1429
|
-
stroke_width = wire_data.get("stroke_width", 0)
|
|
1430
|
-
stroke_type = wire_data.get("stroke_type", "default")
|
|
1431
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1432
|
-
|
|
1433
|
-
# Format stroke width (use int for 0, preserve float for others)
|
|
1434
|
-
if isinstance(stroke_width, float) and stroke_width == 0.0:
|
|
1435
|
-
stroke_width = 0
|
|
1436
|
-
|
|
1437
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1438
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1439
|
-
sexp.append(stroke_sexp)
|
|
1440
|
-
|
|
1441
|
-
# Add UUID
|
|
1442
|
-
if "uuid" in wire_data:
|
|
1443
|
-
sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
|
|
1444
|
-
|
|
1445
|
-
return sexp
|
|
1446
|
-
|
|
590
|
+
return self._wire_parser._wire_to_sexp(wire_data)
|
|
1447
591
|
def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
|
|
1448
592
|
"""Convert junction to S-expression."""
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
# Add position
|
|
1452
|
-
pos = junction_data["position"]
|
|
1453
|
-
if isinstance(pos, dict):
|
|
1454
|
-
x, y = pos["x"], pos["y"]
|
|
1455
|
-
elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
|
|
1456
|
-
x, y = pos[0], pos[1]
|
|
1457
|
-
else:
|
|
1458
|
-
# Assume it's a Point object
|
|
1459
|
-
x, y = pos.x, pos.y
|
|
1460
|
-
|
|
1461
|
-
# Format coordinates properly
|
|
1462
|
-
if isinstance(x, float) and x.is_integer():
|
|
1463
|
-
x = int(x)
|
|
1464
|
-
if isinstance(y, float) and y.is_integer():
|
|
1465
|
-
y = int(y)
|
|
1466
|
-
|
|
1467
|
-
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
1468
|
-
|
|
1469
|
-
# Add diameter
|
|
1470
|
-
diameter = junction_data.get("diameter", 0)
|
|
1471
|
-
sexp.append([sexpdata.Symbol("diameter"), diameter])
|
|
1472
|
-
|
|
1473
|
-
# Add color (RGBA)
|
|
1474
|
-
color = junction_data.get("color", (0, 0, 0, 0))
|
|
1475
|
-
if isinstance(color, (list, tuple)) and len(color) >= 4:
|
|
1476
|
-
sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
|
|
1477
|
-
else:
|
|
1478
|
-
sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
|
|
1479
|
-
|
|
1480
|
-
# Add UUID
|
|
1481
|
-
if "uuid" in junction_data:
|
|
1482
|
-
sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
|
|
1483
|
-
|
|
1484
|
-
return sexp
|
|
1485
|
-
|
|
593
|
+
return self._wire_parser._junction_to_sexp(junction_data)
|
|
1486
594
|
def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
|
|
1487
595
|
"""Convert local label to S-expression."""
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
# Add position
|
|
1491
|
-
pos = label_data["position"]
|
|
1492
|
-
x, y = pos["x"], pos["y"]
|
|
1493
|
-
rotation = label_data.get("rotation", 0)
|
|
1494
|
-
|
|
1495
|
-
# Format coordinates properly
|
|
1496
|
-
if isinstance(x, float) and x.is_integer():
|
|
1497
|
-
x = int(x)
|
|
1498
|
-
if isinstance(y, float) and y.is_integer():
|
|
1499
|
-
y = int(y)
|
|
1500
|
-
|
|
1501
|
-
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
1502
|
-
|
|
1503
|
-
# Add effects (font properties)
|
|
1504
|
-
size = label_data.get("size", 1.27)
|
|
1505
|
-
effects = [sexpdata.Symbol("effects")]
|
|
1506
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
1507
|
-
effects.append(font)
|
|
1508
|
-
effects.append(
|
|
1509
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]
|
|
1510
|
-
)
|
|
1511
|
-
sexp.append(effects)
|
|
1512
|
-
|
|
1513
|
-
# Add UUID
|
|
1514
|
-
if "uuid" in label_data:
|
|
1515
|
-
sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
|
|
1516
|
-
|
|
1517
|
-
return sexp
|
|
1518
|
-
|
|
596
|
+
return self._label_parser._label_to_sexp(label_data)
|
|
1519
597
|
def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
|
|
1520
598
|
"""Convert hierarchical label to S-expression."""
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
# Add shape
|
|
1524
|
-
shape = hlabel_data.get("shape", "input")
|
|
1525
|
-
sexp.append([sexpdata.Symbol("shape"), sexpdata.Symbol(shape)])
|
|
1526
|
-
|
|
1527
|
-
# Add position
|
|
1528
|
-
pos = hlabel_data["position"]
|
|
1529
|
-
x, y = pos["x"], pos["y"]
|
|
1530
|
-
rotation = hlabel_data.get("rotation", 0)
|
|
1531
|
-
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
1532
|
-
|
|
1533
|
-
# Add effects (font properties)
|
|
1534
|
-
size = hlabel_data.get("size", 1.27)
|
|
1535
|
-
effects = [sexpdata.Symbol("effects")]
|
|
1536
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
1537
|
-
effects.append(font)
|
|
1538
|
-
|
|
1539
|
-
# Use justification from data if provided, otherwise default to "left"
|
|
1540
|
-
justify = hlabel_data.get("justify", "left")
|
|
1541
|
-
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
1542
|
-
sexp.append(effects)
|
|
1543
|
-
|
|
1544
|
-
# Add UUID
|
|
1545
|
-
if "uuid" in hlabel_data:
|
|
1546
|
-
sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
|
|
1547
|
-
|
|
1548
|
-
return sexp
|
|
1549
|
-
|
|
599
|
+
return self._label_parser._hierarchical_label_to_sexp(hlabel_data)
|
|
1550
600
|
def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
|
|
1551
601
|
"""Convert no_connect to S-expression."""
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
# Add position
|
|
1555
|
-
pos = no_connect_data["position"]
|
|
1556
|
-
x, y = pos["x"], pos["y"]
|
|
1557
|
-
|
|
1558
|
-
# Format coordinates properly
|
|
1559
|
-
if isinstance(x, float) and x.is_integer():
|
|
1560
|
-
x = int(x)
|
|
1561
|
-
if isinstance(y, float) and y.is_integer():
|
|
1562
|
-
y = int(y)
|
|
1563
|
-
|
|
1564
|
-
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
1565
|
-
|
|
1566
|
-
# Add UUID
|
|
1567
|
-
if "uuid" in no_connect_data:
|
|
1568
|
-
sexp.append([sexpdata.Symbol("uuid"), no_connect_data["uuid"]])
|
|
1569
|
-
|
|
1570
|
-
return sexp
|
|
1571
|
-
|
|
602
|
+
return self._wire_parser._no_connect_to_sexp(no_connect_data)
|
|
1572
603
|
def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
|
|
1573
604
|
"""Convert polyline to S-expression."""
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
# Add points
|
|
1577
|
-
points = polyline_data.get("points", [])
|
|
1578
|
-
if points:
|
|
1579
|
-
pts_sexp = [sexpdata.Symbol("pts")]
|
|
1580
|
-
for point in points:
|
|
1581
|
-
x, y = point["x"], point["y"]
|
|
1582
|
-
# Format coordinates properly
|
|
1583
|
-
if isinstance(x, float) and x.is_integer():
|
|
1584
|
-
x = int(x)
|
|
1585
|
-
if isinstance(y, float) and y.is_integer():
|
|
1586
|
-
y = int(y)
|
|
1587
|
-
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
1588
|
-
sexp.append(pts_sexp)
|
|
1589
|
-
|
|
1590
|
-
# Add stroke
|
|
1591
|
-
stroke_width = polyline_data.get("stroke_width", 0)
|
|
1592
|
-
stroke_type = polyline_data.get("stroke_type", "default")
|
|
1593
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1594
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1595
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1596
|
-
sexp.append(stroke_sexp)
|
|
1597
|
-
|
|
1598
|
-
# Add UUID
|
|
1599
|
-
if "uuid" in polyline_data:
|
|
1600
|
-
sexp.append([sexpdata.Symbol("uuid"), polyline_data["uuid"]])
|
|
1601
|
-
|
|
1602
|
-
return sexp
|
|
1603
|
-
|
|
605
|
+
return self._graphics_parser._polyline_to_sexp(polyline_data)
|
|
1604
606
|
def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
|
|
1605
607
|
"""Convert arc to S-expression."""
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
# Add start, mid, end points
|
|
1609
|
-
for point_name in ["start", "mid", "end"]:
|
|
1610
|
-
point = arc_data.get(point_name, {"x": 0, "y": 0})
|
|
1611
|
-
x, y = point["x"], point["y"]
|
|
1612
|
-
# Format coordinates properly
|
|
1613
|
-
if isinstance(x, float) and x.is_integer():
|
|
1614
|
-
x = int(x)
|
|
1615
|
-
if isinstance(y, float) and y.is_integer():
|
|
1616
|
-
y = int(y)
|
|
1617
|
-
sexp.append([sexpdata.Symbol(point_name), x, y])
|
|
1618
|
-
|
|
1619
|
-
# Add stroke
|
|
1620
|
-
stroke_width = arc_data.get("stroke_width", 0)
|
|
1621
|
-
stroke_type = arc_data.get("stroke_type", "default")
|
|
1622
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1623
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1624
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1625
|
-
sexp.append(stroke_sexp)
|
|
1626
|
-
|
|
1627
|
-
# Add fill
|
|
1628
|
-
fill_type = arc_data.get("fill_type", "none")
|
|
1629
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1630
|
-
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1631
|
-
sexp.append(fill_sexp)
|
|
1632
|
-
|
|
1633
|
-
# Add UUID
|
|
1634
|
-
if "uuid" in arc_data:
|
|
1635
|
-
sexp.append([sexpdata.Symbol("uuid"), arc_data["uuid"]])
|
|
1636
|
-
|
|
1637
|
-
return sexp
|
|
1638
|
-
|
|
608
|
+
return self._graphics_parser._arc_to_sexp(arc_data)
|
|
1639
609
|
def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
|
|
1640
610
|
"""Convert circle to S-expression."""
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
# Add center
|
|
1644
|
-
center = circle_data.get("center", {"x": 0, "y": 0})
|
|
1645
|
-
x, y = center["x"], center["y"]
|
|
1646
|
-
# Format coordinates properly
|
|
1647
|
-
if isinstance(x, float) and x.is_integer():
|
|
1648
|
-
x = int(x)
|
|
1649
|
-
if isinstance(y, float) and y.is_integer():
|
|
1650
|
-
y = int(y)
|
|
1651
|
-
sexp.append([sexpdata.Symbol("center"), x, y])
|
|
1652
|
-
|
|
1653
|
-
# Add radius
|
|
1654
|
-
radius = circle_data.get("radius", 0)
|
|
1655
|
-
sexp.append([sexpdata.Symbol("radius"), radius])
|
|
1656
|
-
|
|
1657
|
-
# Add stroke
|
|
1658
|
-
stroke_width = circle_data.get("stroke_width", 0)
|
|
1659
|
-
stroke_type = circle_data.get("stroke_type", "default")
|
|
1660
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1661
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1662
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1663
|
-
sexp.append(stroke_sexp)
|
|
1664
|
-
|
|
1665
|
-
# Add fill
|
|
1666
|
-
fill_type = circle_data.get("fill_type", "none")
|
|
1667
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1668
|
-
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1669
|
-
sexp.append(fill_sexp)
|
|
1670
|
-
|
|
1671
|
-
# Add UUID
|
|
1672
|
-
if "uuid" in circle_data:
|
|
1673
|
-
sexp.append([sexpdata.Symbol("uuid"), circle_data["uuid"]])
|
|
1674
|
-
|
|
1675
|
-
return sexp
|
|
1676
|
-
|
|
611
|
+
return self._graphics_parser._circle_to_sexp(circle_data)
|
|
1677
612
|
def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
|
|
1678
613
|
"""Convert bezier curve to S-expression."""
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
# Add points
|
|
1682
|
-
points = bezier_data.get("points", [])
|
|
1683
|
-
if points:
|
|
1684
|
-
pts_sexp = [sexpdata.Symbol("pts")]
|
|
1685
|
-
for point in points:
|
|
1686
|
-
x, y = point["x"], point["y"]
|
|
1687
|
-
# Format coordinates properly
|
|
1688
|
-
if isinstance(x, float) and x.is_integer():
|
|
1689
|
-
x = int(x)
|
|
1690
|
-
if isinstance(y, float) and y.is_integer():
|
|
1691
|
-
y = int(y)
|
|
1692
|
-
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
1693
|
-
sexp.append(pts_sexp)
|
|
1694
|
-
|
|
1695
|
-
# Add stroke
|
|
1696
|
-
stroke_width = bezier_data.get("stroke_width", 0)
|
|
1697
|
-
stroke_type = bezier_data.get("stroke_type", "default")
|
|
1698
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1699
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1700
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1701
|
-
sexp.append(stroke_sexp)
|
|
1702
|
-
|
|
1703
|
-
# Add fill
|
|
1704
|
-
fill_type = bezier_data.get("fill_type", "none")
|
|
1705
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1706
|
-
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1707
|
-
sexp.append(fill_sexp)
|
|
1708
|
-
|
|
1709
|
-
# Add UUID
|
|
1710
|
-
if "uuid" in bezier_data:
|
|
1711
|
-
sexp.append([sexpdata.Symbol("uuid"), bezier_data["uuid"]])
|
|
1712
|
-
|
|
1713
|
-
return sexp
|
|
1714
|
-
|
|
614
|
+
return self._graphics_parser._bezier_to_sexp(bezier_data)
|
|
1715
615
|
def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
|
|
1716
616
|
"""Convert hierarchical sheet to S-expression."""
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
# Add position
|
|
1720
|
-
pos = sheet_data["position"]
|
|
1721
|
-
x, y = pos["x"], pos["y"]
|
|
1722
|
-
if isinstance(x, float) and x.is_integer():
|
|
1723
|
-
x = int(x)
|
|
1724
|
-
if isinstance(y, float) and y.is_integer():
|
|
1725
|
-
y = int(y)
|
|
1726
|
-
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
1727
|
-
|
|
1728
|
-
# Add size
|
|
1729
|
-
size = sheet_data["size"]
|
|
1730
|
-
w, h = size["width"], size["height"]
|
|
1731
|
-
sexp.append([sexpdata.Symbol("size"), w, h])
|
|
1732
|
-
|
|
1733
|
-
# Add basic properties
|
|
1734
|
-
sexp.append(
|
|
1735
|
-
[
|
|
1736
|
-
sexpdata.Symbol("exclude_from_sim"),
|
|
1737
|
-
sexpdata.Symbol("yes" if sheet_data.get("exclude_from_sim", False) else "no"),
|
|
1738
|
-
]
|
|
1739
|
-
)
|
|
1740
|
-
sexp.append(
|
|
1741
|
-
[
|
|
1742
|
-
sexpdata.Symbol("in_bom"),
|
|
1743
|
-
sexpdata.Symbol("yes" if sheet_data.get("in_bom", True) else "no"),
|
|
1744
|
-
]
|
|
1745
|
-
)
|
|
1746
|
-
sexp.append(
|
|
1747
|
-
[
|
|
1748
|
-
sexpdata.Symbol("on_board"),
|
|
1749
|
-
sexpdata.Symbol("yes" if sheet_data.get("on_board", True) else "no"),
|
|
1750
|
-
]
|
|
1751
|
-
)
|
|
1752
|
-
sexp.append(
|
|
1753
|
-
[
|
|
1754
|
-
sexpdata.Symbol("dnp"),
|
|
1755
|
-
sexpdata.Symbol("yes" if sheet_data.get("dnp", False) else "no"),
|
|
1756
|
-
]
|
|
1757
|
-
)
|
|
1758
|
-
sexp.append(
|
|
1759
|
-
[
|
|
1760
|
-
sexpdata.Symbol("fields_autoplaced"),
|
|
1761
|
-
sexpdata.Symbol("yes" if sheet_data.get("fields_autoplaced", True) else "no"),
|
|
1762
|
-
]
|
|
1763
|
-
)
|
|
1764
|
-
|
|
1765
|
-
# Add stroke
|
|
1766
|
-
stroke_width = sheet_data.get("stroke_width", 0.1524)
|
|
1767
|
-
stroke_type = sheet_data.get("stroke_type", "solid")
|
|
1768
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1769
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1770
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1771
|
-
sexp.append(stroke_sexp)
|
|
1772
|
-
|
|
1773
|
-
# Add fill
|
|
1774
|
-
fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
|
|
1775
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1776
|
-
fill_sexp.append(
|
|
1777
|
-
[sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]]
|
|
1778
|
-
)
|
|
1779
|
-
sexp.append(fill_sexp)
|
|
1780
|
-
|
|
1781
|
-
# Add UUID
|
|
1782
|
-
if "uuid" in sheet_data:
|
|
1783
|
-
sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
|
|
1784
|
-
|
|
1785
|
-
# Add sheet properties (name and filename)
|
|
1786
|
-
name = sheet_data.get("name", "Sheet")
|
|
1787
|
-
filename = sheet_data.get("filename", "sheet.kicad_sch")
|
|
1788
|
-
|
|
1789
|
-
# Sheetname property
|
|
1790
|
-
from .config import config
|
|
1791
|
-
|
|
1792
|
-
name_prop = [sexpdata.Symbol("property"), "Sheetname", name]
|
|
1793
|
-
name_prop.append(
|
|
1794
|
-
[sexpdata.Symbol("at"), x, round(y + config.sheet.name_offset_y, 4), 0]
|
|
1795
|
-
) # Above sheet
|
|
1796
|
-
name_prop.append(
|
|
1797
|
-
[
|
|
1798
|
-
sexpdata.Symbol("effects"),
|
|
1799
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1800
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")],
|
|
1801
|
-
]
|
|
1802
|
-
)
|
|
1803
|
-
sexp.append(name_prop)
|
|
1804
|
-
|
|
1805
|
-
# Sheetfile property
|
|
1806
|
-
file_prop = [sexpdata.Symbol("property"), "Sheetfile", filename]
|
|
1807
|
-
file_prop.append(
|
|
1808
|
-
[sexpdata.Symbol("at"), x, round(y + h + config.sheet.file_offset_y, 4), 0]
|
|
1809
|
-
) # Below sheet
|
|
1810
|
-
file_prop.append(
|
|
1811
|
-
[
|
|
1812
|
-
sexpdata.Symbol("effects"),
|
|
1813
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1814
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("top")],
|
|
1815
|
-
]
|
|
1816
|
-
)
|
|
1817
|
-
sexp.append(file_prop)
|
|
1818
|
-
|
|
1819
|
-
# Add sheet pins if any
|
|
1820
|
-
for pin in sheet_data.get("pins", []):
|
|
1821
|
-
pin_sexp = self._sheet_pin_to_sexp(pin)
|
|
1822
|
-
sexp.append(pin_sexp)
|
|
1823
|
-
|
|
1824
|
-
# Add instances
|
|
1825
|
-
if schematic_uuid:
|
|
1826
|
-
instances_sexp = [sexpdata.Symbol("instances")]
|
|
1827
|
-
project_name = sheet_data.get("project_name", "")
|
|
1828
|
-
page_number = sheet_data.get("page_number", "2")
|
|
1829
|
-
project_sexp = [sexpdata.Symbol("project"), project_name]
|
|
1830
|
-
path_sexp = [sexpdata.Symbol("path"), f"/{schematic_uuid}"]
|
|
1831
|
-
path_sexp.append([sexpdata.Symbol("page"), page_number])
|
|
1832
|
-
project_sexp.append(path_sexp)
|
|
1833
|
-
instances_sexp.append(project_sexp)
|
|
1834
|
-
sexp.append(instances_sexp)
|
|
1835
|
-
|
|
1836
|
-
return sexp
|
|
1837
|
-
|
|
617
|
+
return self._sheet_parser._sheet_to_sexp(sheet_data, schematic_uuid)
|
|
1838
618
|
def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
|
|
1839
619
|
"""Convert sheet pin to S-expression."""
|
|
1840
|
-
|
|
1841
|
-
sexpdata.Symbol("pin"),
|
|
1842
|
-
pin_data["name"],
|
|
1843
|
-
sexpdata.Symbol(pin_data.get("pin_type", "input")),
|
|
1844
|
-
]
|
|
1845
|
-
|
|
1846
|
-
# Add position
|
|
1847
|
-
pos = pin_data["position"]
|
|
1848
|
-
x, y = pos["x"], pos["y"]
|
|
1849
|
-
rotation = pin_data.get("rotation", 0)
|
|
1850
|
-
pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
1851
|
-
|
|
1852
|
-
# Add UUID
|
|
1853
|
-
if "uuid" in pin_data:
|
|
1854
|
-
pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
|
|
1855
|
-
|
|
1856
|
-
# Add effects
|
|
1857
|
-
size = pin_data.get("size", 1.27)
|
|
1858
|
-
effects = [sexpdata.Symbol("effects")]
|
|
1859
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
1860
|
-
effects.append(font)
|
|
1861
|
-
justify = pin_data.get("justify", "right")
|
|
1862
|
-
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
1863
|
-
pin_sexp.append(effects)
|
|
1864
|
-
|
|
1865
|
-
return pin_sexp
|
|
1866
|
-
|
|
620
|
+
return self._sheet_parser._sheet_pin_to_sexp(pin_data)
|
|
1867
621
|
def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
|
|
1868
622
|
"""Convert text element to S-expression."""
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
# Add exclude_from_sim
|
|
1872
|
-
exclude_sim = text_data.get("exclude_from_sim", False)
|
|
1873
|
-
sexp.append(
|
|
1874
|
-
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
|
|
1875
|
-
)
|
|
1876
|
-
|
|
1877
|
-
# Add position
|
|
1878
|
-
pos = text_data["position"]
|
|
1879
|
-
x, y = pos["x"], pos["y"]
|
|
1880
|
-
rotation = text_data.get("rotation", 0)
|
|
1881
|
-
|
|
1882
|
-
# Format coordinates properly
|
|
1883
|
-
if isinstance(x, float) and x.is_integer():
|
|
1884
|
-
x = int(x)
|
|
1885
|
-
if isinstance(y, float) and y.is_integer():
|
|
1886
|
-
y = int(y)
|
|
1887
|
-
|
|
1888
|
-
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
1889
|
-
|
|
1890
|
-
# Add effects (font properties)
|
|
1891
|
-
size = text_data.get("size", 1.27)
|
|
1892
|
-
effects = [sexpdata.Symbol("effects")]
|
|
1893
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
1894
|
-
effects.append(font)
|
|
1895
|
-
sexp.append(effects)
|
|
1896
|
-
|
|
1897
|
-
# Add UUID
|
|
1898
|
-
if "uuid" in text_data:
|
|
1899
|
-
sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
|
|
1900
|
-
|
|
1901
|
-
return sexp
|
|
1902
|
-
|
|
623
|
+
return self._text_parser._text_to_sexp(text_data)
|
|
1903
624
|
def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
|
|
1904
625
|
"""Convert text box element to S-expression."""
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
# Add exclude_from_sim
|
|
1908
|
-
exclude_sim = text_box_data.get("exclude_from_sim", False)
|
|
1909
|
-
sexp.append(
|
|
1910
|
-
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
|
|
1911
|
-
)
|
|
1912
|
-
|
|
1913
|
-
# Add position
|
|
1914
|
-
pos = text_box_data["position"]
|
|
1915
|
-
x, y = pos["x"], pos["y"]
|
|
1916
|
-
rotation = text_box_data.get("rotation", 0)
|
|
1917
|
-
|
|
1918
|
-
# Format coordinates properly
|
|
1919
|
-
if isinstance(x, float) and x.is_integer():
|
|
1920
|
-
x = int(x)
|
|
1921
|
-
if isinstance(y, float) and y.is_integer():
|
|
1922
|
-
y = int(y)
|
|
1923
|
-
|
|
1924
|
-
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
1925
|
-
|
|
1926
|
-
# Add size
|
|
1927
|
-
size = text_box_data["size"]
|
|
1928
|
-
w, h = size["width"], size["height"]
|
|
1929
|
-
sexp.append([sexpdata.Symbol("size"), w, h])
|
|
1930
|
-
|
|
1931
|
-
# Add margins
|
|
1932
|
-
margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
|
|
1933
|
-
sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
|
|
1934
|
-
|
|
1935
|
-
# Add stroke
|
|
1936
|
-
stroke_width = text_box_data.get("stroke_width", 0)
|
|
1937
|
-
stroke_type = text_box_data.get("stroke_type", "solid")
|
|
1938
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1939
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1940
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1941
|
-
sexp.append(stroke_sexp)
|
|
1942
|
-
|
|
1943
|
-
# Add fill
|
|
1944
|
-
fill_type = text_box_data.get("fill_type", "none")
|
|
1945
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1946
|
-
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1947
|
-
sexp.append(fill_sexp)
|
|
1948
|
-
|
|
1949
|
-
# Add effects (font properties and justification)
|
|
1950
|
-
font_size = text_box_data.get("font_size", 1.27)
|
|
1951
|
-
justify_h = text_box_data.get("justify_horizontal", "left")
|
|
1952
|
-
justify_v = text_box_data.get("justify_vertical", "top")
|
|
1953
|
-
|
|
1954
|
-
effects = [sexpdata.Symbol("effects")]
|
|
1955
|
-
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
|
|
1956
|
-
effects.append(font)
|
|
1957
|
-
effects.append(
|
|
1958
|
-
[sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
|
|
1959
|
-
)
|
|
1960
|
-
sexp.append(effects)
|
|
1961
|
-
|
|
1962
|
-
# Add UUID
|
|
1963
|
-
if "uuid" in text_box_data:
|
|
1964
|
-
sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
|
|
1965
|
-
|
|
1966
|
-
return sexp
|
|
1967
|
-
|
|
626
|
+
return self._text_parser._text_box_to_sexp(text_box_data)
|
|
1968
627
|
def _rectangle_to_sexp(self, rectangle_data: Dict[str, Any]) -> List[Any]:
|
|
1969
628
|
"""Convert rectangle element to S-expression."""
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
# Add start point
|
|
1973
|
-
start = rectangle_data["start"]
|
|
1974
|
-
start_x, start_y = start["x"], start["y"]
|
|
1975
|
-
sexp.append([sexpdata.Symbol("start"), start_x, start_y])
|
|
1976
|
-
|
|
1977
|
-
# Add end point
|
|
1978
|
-
end = rectangle_data["end"]
|
|
1979
|
-
end_x, end_y = end["x"], end["y"]
|
|
1980
|
-
sexp.append([sexpdata.Symbol("end"), end_x, end_y])
|
|
1981
|
-
|
|
1982
|
-
# Add stroke
|
|
1983
|
-
stroke_width = rectangle_data.get("stroke_width", 0)
|
|
1984
|
-
stroke_type = rectangle_data.get("stroke_type", "default")
|
|
1985
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1986
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1987
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1988
|
-
# Add stroke color if present
|
|
1989
|
-
if "stroke_color" in rectangle_data:
|
|
1990
|
-
r, g, b, a = rectangle_data["stroke_color"]
|
|
1991
|
-
stroke_sexp.append([sexpdata.Symbol("color"), r, g, b, a])
|
|
1992
|
-
sexp.append(stroke_sexp)
|
|
1993
|
-
|
|
1994
|
-
# Add fill
|
|
1995
|
-
fill_type = rectangle_data.get("fill_type", "none")
|
|
1996
|
-
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1997
|
-
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1998
|
-
# Add fill color if present
|
|
1999
|
-
if "fill_color" in rectangle_data:
|
|
2000
|
-
r, g, b, a = rectangle_data["fill_color"]
|
|
2001
|
-
fill_sexp.append([sexpdata.Symbol("color"), r, g, b, a])
|
|
2002
|
-
sexp.append(fill_sexp)
|
|
2003
|
-
|
|
2004
|
-
# Add UUID
|
|
2005
|
-
if "uuid" in rectangle_data:
|
|
2006
|
-
sexp.append([sexpdata.Symbol("uuid"), rectangle_data["uuid"]])
|
|
2007
|
-
|
|
2008
|
-
return sexp
|
|
2009
|
-
|
|
629
|
+
return self._graphics_parser._rectangle_to_sexp(rectangle_data)
|
|
2010
630
|
def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
|
|
2011
631
|
"""Convert image element to S-expression."""
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
# Add position
|
|
2015
|
-
position = image_data.get("position", {"x": 0, "y": 0})
|
|
2016
|
-
pos_x, pos_y = position["x"], position["y"]
|
|
2017
|
-
sexp.append([sexpdata.Symbol("at"), pos_x, pos_y])
|
|
2018
|
-
|
|
2019
|
-
# Add UUID
|
|
2020
|
-
if "uuid" in image_data:
|
|
2021
|
-
sexp.append([sexpdata.Symbol("uuid"), image_data["uuid"]])
|
|
2022
|
-
|
|
2023
|
-
# Add scale if not default
|
|
2024
|
-
scale = image_data.get("scale", 1.0)
|
|
2025
|
-
if scale != 1.0:
|
|
2026
|
-
sexp.append([sexpdata.Symbol("scale"), scale])
|
|
2027
|
-
|
|
2028
|
-
# Add image data
|
|
2029
|
-
# KiCad splits base64 data into multiple lines for readability
|
|
2030
|
-
# Each line is roughly 76 characters (standard base64 line length)
|
|
2031
|
-
data = image_data.get("data", "")
|
|
2032
|
-
if data:
|
|
2033
|
-
data_sexp = [sexpdata.Symbol("data")]
|
|
2034
|
-
# Split the data into 76-character chunks
|
|
2035
|
-
chunk_size = 76
|
|
2036
|
-
for i in range(0, len(data), chunk_size):
|
|
2037
|
-
data_sexp.append(data[i : i + chunk_size])
|
|
2038
|
-
sexp.append(data_sexp)
|
|
2039
|
-
|
|
2040
|
-
return sexp
|
|
2041
|
-
|
|
632
|
+
return self._graphics_parser._image_to_sexp(image_data)
|
|
2042
633
|
def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
|
|
2043
634
|
"""Convert lib_symbols to S-expression."""
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
# Add each symbol definition
|
|
2047
|
-
for symbol_name, symbol_def in lib_symbols.items():
|
|
2048
|
-
if isinstance(symbol_def, list):
|
|
2049
|
-
# Raw S-expression data from parsed library file - use directly
|
|
2050
|
-
sexp.append(symbol_def)
|
|
2051
|
-
elif isinstance(symbol_def, dict):
|
|
2052
|
-
# Dictionary format - convert to S-expression
|
|
2053
|
-
symbol_sexp = self._create_basic_symbol_definition(symbol_name)
|
|
2054
|
-
sexp.append(symbol_sexp)
|
|
2055
|
-
|
|
2056
|
-
return sexp
|
|
2057
|
-
|
|
635
|
+
return self._library_parser._lib_symbols_to_sexp(lib_symbols)
|
|
2058
636
|
def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
|
|
2059
637
|
"""Create a basic symbol definition for KiCAD compatibility."""
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
# Add basic symbol properties
|
|
2063
|
-
symbol_sexp.extend(
|
|
2064
|
-
[
|
|
2065
|
-
[sexpdata.Symbol("pin_numbers"), [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]],
|
|
2066
|
-
[sexpdata.Symbol("pin_names"), [sexpdata.Symbol("offset"), 0]],
|
|
2067
|
-
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("no")],
|
|
2068
|
-
[sexpdata.Symbol("in_bom"), sexpdata.Symbol("yes")],
|
|
2069
|
-
[sexpdata.Symbol("on_board"), sexpdata.Symbol("yes")],
|
|
2070
|
-
]
|
|
2071
|
-
)
|
|
2072
|
-
|
|
2073
|
-
# Add basic properties for the symbol
|
|
2074
|
-
if "R" in lib_id: # Resistor
|
|
2075
|
-
symbol_sexp.extend(
|
|
2076
|
-
[
|
|
2077
|
-
[
|
|
2078
|
-
sexpdata.Symbol("property"),
|
|
2079
|
-
"Reference",
|
|
2080
|
-
"R",
|
|
2081
|
-
[sexpdata.Symbol("at"), 2.032, 0, 90],
|
|
2082
|
-
[
|
|
2083
|
-
sexpdata.Symbol("effects"),
|
|
2084
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2085
|
-
],
|
|
2086
|
-
],
|
|
2087
|
-
[
|
|
2088
|
-
sexpdata.Symbol("property"),
|
|
2089
|
-
"Value",
|
|
2090
|
-
"R",
|
|
2091
|
-
[sexpdata.Symbol("at"), 0, 0, 90],
|
|
2092
|
-
[
|
|
2093
|
-
sexpdata.Symbol("effects"),
|
|
2094
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2095
|
-
],
|
|
2096
|
-
],
|
|
2097
|
-
[
|
|
2098
|
-
sexpdata.Symbol("property"),
|
|
2099
|
-
"Footprint",
|
|
2100
|
-
"",
|
|
2101
|
-
[sexpdata.Symbol("at"), -1.778, 0, 90],
|
|
2102
|
-
[
|
|
2103
|
-
sexpdata.Symbol("effects"),
|
|
2104
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2105
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
2106
|
-
],
|
|
2107
|
-
],
|
|
2108
|
-
[
|
|
2109
|
-
sexpdata.Symbol("property"),
|
|
2110
|
-
"Datasheet",
|
|
2111
|
-
"~",
|
|
2112
|
-
[sexpdata.Symbol("at"), 0, 0, 0],
|
|
2113
|
-
[
|
|
2114
|
-
sexpdata.Symbol("effects"),
|
|
2115
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2116
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
2117
|
-
],
|
|
2118
|
-
],
|
|
2119
|
-
]
|
|
2120
|
-
)
|
|
2121
|
-
|
|
2122
|
-
elif "C" in lib_id: # Capacitor
|
|
2123
|
-
symbol_sexp.extend(
|
|
2124
|
-
[
|
|
2125
|
-
[
|
|
2126
|
-
sexpdata.Symbol("property"),
|
|
2127
|
-
"Reference",
|
|
2128
|
-
"C",
|
|
2129
|
-
[sexpdata.Symbol("at"), 0.635, 2.54, 0],
|
|
2130
|
-
[
|
|
2131
|
-
sexpdata.Symbol("effects"),
|
|
2132
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2133
|
-
],
|
|
2134
|
-
],
|
|
2135
|
-
[
|
|
2136
|
-
sexpdata.Symbol("property"),
|
|
2137
|
-
"Value",
|
|
2138
|
-
"C",
|
|
2139
|
-
[sexpdata.Symbol("at"), 0.635, -2.54, 0],
|
|
2140
|
-
[
|
|
2141
|
-
sexpdata.Symbol("effects"),
|
|
2142
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2143
|
-
],
|
|
2144
|
-
],
|
|
2145
|
-
[
|
|
2146
|
-
sexpdata.Symbol("property"),
|
|
2147
|
-
"Footprint",
|
|
2148
|
-
"",
|
|
2149
|
-
[sexpdata.Symbol("at"), 0, -1.27, 0],
|
|
2150
|
-
[
|
|
2151
|
-
sexpdata.Symbol("effects"),
|
|
2152
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2153
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
2154
|
-
],
|
|
2155
|
-
],
|
|
2156
|
-
[
|
|
2157
|
-
sexpdata.Symbol("property"),
|
|
2158
|
-
"Datasheet",
|
|
2159
|
-
"~",
|
|
2160
|
-
[sexpdata.Symbol("at"), 0, 0, 0],
|
|
2161
|
-
[
|
|
2162
|
-
sexpdata.Symbol("effects"),
|
|
2163
|
-
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
2164
|
-
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
2165
|
-
],
|
|
2166
|
-
],
|
|
2167
|
-
]
|
|
2168
|
-
)
|
|
2169
|
-
|
|
2170
|
-
# Add basic graphics and pins (minimal for now)
|
|
2171
|
-
symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
|
|
2172
|
-
|
|
2173
|
-
return symbol_sexp
|
|
2174
|
-
|
|
638
|
+
return self._library_parser._create_basic_symbol_definition(lib_id)
|
|
2175
639
|
def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
|
|
2176
640
|
"""Parse sheet_instances section."""
|
|
2177
|
-
|
|
2178
|
-
for sheet_item in item[1:]: # Skip 'sheet_instances' header
|
|
2179
|
-
if isinstance(sheet_item, list) and len(sheet_item) > 0:
|
|
2180
|
-
sheet_data = {"path": "/", "page": "1"}
|
|
2181
|
-
for element in sheet_item[1:]: # Skip element header
|
|
2182
|
-
if isinstance(element, list) and len(element) >= 2:
|
|
2183
|
-
key = (
|
|
2184
|
-
str(element[0])
|
|
2185
|
-
if isinstance(element[0], sexpdata.Symbol)
|
|
2186
|
-
else str(element[0])
|
|
2187
|
-
)
|
|
2188
|
-
if key == "path":
|
|
2189
|
-
sheet_data["path"] = element[1]
|
|
2190
|
-
elif key == "page":
|
|
2191
|
-
sheet_data["page"] = element[1]
|
|
2192
|
-
sheet_instances.append(sheet_data)
|
|
2193
|
-
return sheet_instances
|
|
2194
|
-
|
|
641
|
+
return self._sheet_parser._parse_sheet_instances(item)
|
|
2195
642
|
def _parse_symbol_instances(self, item: List[Any]) -> List[Any]:
|
|
2196
643
|
"""Parse symbol_instances section."""
|
|
2197
|
-
|
|
2198
|
-
return item[1:] if len(item) > 1 else []
|
|
2199
|
-
|
|
644
|
+
return self._metadata_parser._parse_symbol_instances(item)
|
|
2200
645
|
def _sheet_instances_to_sexp(self, sheet_instances: List[Dict[str, Any]]) -> List[Any]:
|
|
2201
646
|
"""Convert sheet_instances to S-expression."""
|
|
2202
|
-
|
|
2203
|
-
for sheet in sheet_instances:
|
|
2204
|
-
# Create: (path "/" (page "1"))
|
|
2205
|
-
sheet_sexp = [
|
|
2206
|
-
sexpdata.Symbol("path"),
|
|
2207
|
-
sheet.get("path", "/"),
|
|
2208
|
-
[sexpdata.Symbol("page"), str(sheet.get("page", "1"))],
|
|
2209
|
-
]
|
|
2210
|
-
sexp.append(sheet_sexp)
|
|
2211
|
-
return sexp
|
|
2212
|
-
|
|
647
|
+
return self._sheet_parser._sheet_instances_to_sexp(sheet_instances)
|
|
2213
648
|
def _graphic_to_sexp(self, graphic_data: Dict[str, Any]) -> List[Any]:
|
|
2214
649
|
"""Convert graphics (rectangles, etc.) to S-expression."""
|
|
2215
|
-
|
|
2216
|
-
sexp = [sexpdata.Symbol("rectangle")]
|
|
2217
|
-
|
|
2218
|
-
# Add start position
|
|
2219
|
-
start = graphic_data.get("start", {})
|
|
2220
|
-
start_x = start.get("x", 0)
|
|
2221
|
-
start_y = start.get("y", 0)
|
|
2222
|
-
|
|
2223
|
-
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
2224
|
-
if isinstance(start_x, float) and start_x.is_integer():
|
|
2225
|
-
start_x = int(start_x)
|
|
2226
|
-
if isinstance(start_y, float) and start_y.is_integer():
|
|
2227
|
-
start_y = int(start_y)
|
|
2228
|
-
|
|
2229
|
-
sexp.append([sexpdata.Symbol("start"), start_x, start_y])
|
|
2230
|
-
|
|
2231
|
-
# Add end position
|
|
2232
|
-
end = graphic_data.get("end", {})
|
|
2233
|
-
end_x = end.get("x", 0)
|
|
2234
|
-
end_y = end.get("y", 0)
|
|
2235
|
-
|
|
2236
|
-
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
2237
|
-
if isinstance(end_x, float) and end_x.is_integer():
|
|
2238
|
-
end_x = int(end_x)
|
|
2239
|
-
if isinstance(end_y, float) and end_y.is_integer():
|
|
2240
|
-
end_y = int(end_y)
|
|
2241
|
-
|
|
2242
|
-
sexp.append([sexpdata.Symbol("end"), end_x, end_y])
|
|
2243
|
-
|
|
2244
|
-
# Add stroke information (KiCAD format: width, type, and optionally color)
|
|
2245
|
-
stroke = graphic_data.get("stroke", {})
|
|
2246
|
-
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
2247
|
-
|
|
2248
|
-
# Stroke width - default to 0 to match KiCAD behavior
|
|
2249
|
-
stroke_width = stroke.get("width", 0)
|
|
2250
|
-
if isinstance(stroke_width, float) and stroke_width == 0.0:
|
|
2251
|
-
stroke_width = 0
|
|
2252
|
-
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
2253
|
-
|
|
2254
|
-
# Stroke type - normalize to KiCAD format and validate
|
|
2255
|
-
stroke_type = stroke.get("type", "default")
|
|
2256
|
-
|
|
2257
|
-
# KiCAD only supports these exact stroke types
|
|
2258
|
-
valid_kicad_types = {"solid", "dash", "dash_dot", "dash_dot_dot", "dot", "default"}
|
|
2259
|
-
|
|
2260
|
-
# Map common variations to KiCAD format
|
|
2261
|
-
stroke_type_map = {
|
|
2262
|
-
"dashdot": "dash_dot",
|
|
2263
|
-
"dash-dot": "dash_dot",
|
|
2264
|
-
"dashdotdot": "dash_dot_dot",
|
|
2265
|
-
"dash-dot-dot": "dash_dot_dot",
|
|
2266
|
-
"solid": "solid",
|
|
2267
|
-
"dash": "dash",
|
|
2268
|
-
"dot": "dot",
|
|
2269
|
-
"default": "default",
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
# Normalize and validate
|
|
2273
|
-
normalized_stroke_type = stroke_type_map.get(stroke_type.lower(), stroke_type)
|
|
2274
|
-
if normalized_stroke_type not in valid_kicad_types:
|
|
2275
|
-
normalized_stroke_type = "default" # Fallback to default for invalid types
|
|
2276
|
-
|
|
2277
|
-
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(normalized_stroke_type)])
|
|
2278
|
-
|
|
2279
|
-
# Stroke color (if specified) - KiCAD format uses RGB 0-255 values plus alpha
|
|
2280
|
-
stroke_color = stroke.get("color")
|
|
2281
|
-
if stroke_color:
|
|
2282
|
-
if isinstance(stroke_color, str):
|
|
2283
|
-
# Convert string color names to RGB 0-255 values
|
|
2284
|
-
color_rgb = self._color_to_rgb255(stroke_color)
|
|
2285
|
-
stroke_sexp.append([sexpdata.Symbol("color")] + color_rgb + [1]) # Add alpha=1
|
|
2286
|
-
elif isinstance(stroke_color, (list, tuple)) and len(stroke_color) >= 3:
|
|
2287
|
-
# Use provided RGB values directly
|
|
2288
|
-
stroke_sexp.append([sexpdata.Symbol("color")] + list(stroke_color))
|
|
2289
|
-
|
|
2290
|
-
sexp.append(stroke_sexp)
|
|
2291
|
-
|
|
2292
|
-
# Add fill information
|
|
2293
|
-
fill = graphic_data.get("fill", {"type": "none"})
|
|
2294
|
-
fill_type = fill.get("type", "none")
|
|
2295
|
-
fill_sexp = [sexpdata.Symbol("fill"), [sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)]]
|
|
2296
|
-
sexp.append(fill_sexp)
|
|
2297
|
-
|
|
2298
|
-
# Add UUID (no quotes around UUID in KiCAD format)
|
|
2299
|
-
if "uuid" in graphic_data:
|
|
2300
|
-
uuid_str = graphic_data["uuid"]
|
|
2301
|
-
# Remove quotes and convert to Symbol to match KiCAD format
|
|
2302
|
-
uuid_clean = uuid_str.replace('"', "")
|
|
2303
|
-
sexp.append([sexpdata.Symbol("uuid"), sexpdata.Symbol(uuid_clean)])
|
|
2304
|
-
|
|
2305
|
-
return sexp
|
|
2306
|
-
|
|
650
|
+
return self._graphics_parser._graphic_to_sexp(graphic_data)
|
|
2307
651
|
def _color_to_rgba(self, color_name: str) -> List[float]:
|
|
2308
652
|
"""Convert color name to RGBA values (0.0-1.0) for KiCAD compatibility."""
|
|
2309
|
-
|
|
2310
|
-
color_map = {
|
|
2311
|
-
"red": [1.0, 0.0, 0.0, 1.0],
|
|
2312
|
-
"blue": [0.0, 0.0, 1.0, 1.0],
|
|
2313
|
-
"green": [0.0, 1.0, 0.0, 1.0],
|
|
2314
|
-
"yellow": [1.0, 1.0, 0.0, 1.0],
|
|
2315
|
-
"magenta": [1.0, 0.0, 1.0, 1.0],
|
|
2316
|
-
"cyan": [0.0, 1.0, 1.0, 1.0],
|
|
2317
|
-
"black": [0.0, 0.0, 0.0, 1.0],
|
|
2318
|
-
"white": [1.0, 1.0, 1.0, 1.0],
|
|
2319
|
-
"gray": [0.5, 0.5, 0.5, 1.0],
|
|
2320
|
-
"grey": [0.5, 0.5, 0.5, 1.0],
|
|
2321
|
-
"orange": [1.0, 0.5, 0.0, 1.0],
|
|
2322
|
-
"purple": [0.5, 0.0, 0.5, 1.0],
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
# Return RGBA values, default to black if color not found
|
|
2326
|
-
return color_map.get(color_name.lower(), [0.0, 0.0, 0.0, 1.0])
|
|
653
|
+
return color_to_rgba(color_name)
|
|
2327
654
|
|
|
2328
655
|
def _color_to_rgb255(self, color_name: str) -> List[int]:
|
|
2329
656
|
"""Convert color name to RGB values (0-255) for KiCAD rectangle graphics."""
|
|
2330
|
-
|
|
2331
|
-
color_map = {
|
|
2332
|
-
"red": [255, 0, 0],
|
|
2333
|
-
"blue": [0, 0, 255],
|
|
2334
|
-
"green": [0, 255, 0],
|
|
2335
|
-
"yellow": [255, 255, 0],
|
|
2336
|
-
"magenta": [255, 0, 255],
|
|
2337
|
-
"cyan": [0, 255, 255],
|
|
2338
|
-
"black": [0, 0, 0],
|
|
2339
|
-
"white": [255, 255, 255],
|
|
2340
|
-
"gray": [128, 128, 128],
|
|
2341
|
-
"grey": [128, 128, 128],
|
|
2342
|
-
"orange": [255, 128, 0],
|
|
2343
|
-
"purple": [128, 0, 128],
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
# Return RGB values, default to black if color not found
|
|
2347
|
-
return color_map.get(color_name.lower(), [0, 0, 0])
|
|
657
|
+
return color_to_rgb255(color_name)
|
|
2348
658
|
|
|
2349
659
|
def get_validation_issues(self) -> List[ValidationIssue]:
|
|
2350
660
|
"""Get list of validation issues from last parse operation."""
|