kicad-sch-api 0.0.2__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/core/components.py +63 -0
- kicad_sch_api/core/formatter.py +56 -11
- kicad_sch_api/core/ic_manager.py +187 -0
- kicad_sch_api/core/junctions.py +206 -0
- kicad_sch_api/core/parser.py +606 -26
- kicad_sch_api/core/schematic.py +739 -8
- kicad_sch_api/core/types.py +102 -7
- kicad_sch_api/core/wires.py +248 -0
- kicad_sch_api/library/cache.py +321 -10
- kicad_sch_api/utils/validation.py +1 -1
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/METADATA +13 -17
- kicad_sch_api-0.1.0.dist-info/RECORD +21 -0
- kicad_sch_api-0.0.2.dist-info/RECORD +0 -18
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.0.2.dist-info → kicad_sch_api-0.1.0.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/schematic.py
CHANGED
|
@@ -11,12 +11,16 @@ import uuid
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
13
13
|
|
|
14
|
+
import sexpdata
|
|
15
|
+
|
|
14
16
|
from ..library.cache import get_symbol_cache
|
|
15
17
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
16
18
|
from .components import ComponentCollection
|
|
17
19
|
from .formatter import ExactFormatter
|
|
18
20
|
from .parser import SExpressionParser
|
|
19
|
-
from .types import Junction, Label, Net, Point, SchematicSymbol, TitleBlock, Wire
|
|
21
|
+
from .types import Junction, Label, Net, Point, SchematicSymbol, TitleBlock, Wire, LabelType, HierarchicalLabelShape, WireType, Sheet, Text, TextBox
|
|
22
|
+
from .wires import WireCollection
|
|
23
|
+
from .junctions import JunctionCollection
|
|
20
24
|
|
|
21
25
|
logger = logging.getLogger(__name__)
|
|
22
26
|
|
|
@@ -62,6 +66,54 @@ class Schematic:
|
|
|
62
66
|
]
|
|
63
67
|
self._components = ComponentCollection(component_symbols)
|
|
64
68
|
|
|
69
|
+
# Initialize wire collection
|
|
70
|
+
wire_data = self._data.get("wires", [])
|
|
71
|
+
wires = []
|
|
72
|
+
for wire_dict in wire_data:
|
|
73
|
+
if isinstance(wire_dict, dict):
|
|
74
|
+
# Convert dict to Wire object
|
|
75
|
+
points = []
|
|
76
|
+
for point_data in wire_dict.get("points", []):
|
|
77
|
+
if isinstance(point_data, dict):
|
|
78
|
+
points.append(Point(point_data["x"], point_data["y"]))
|
|
79
|
+
elif isinstance(point_data, (list, tuple)):
|
|
80
|
+
points.append(Point(point_data[0], point_data[1]))
|
|
81
|
+
else:
|
|
82
|
+
points.append(point_data)
|
|
83
|
+
|
|
84
|
+
wire = Wire(
|
|
85
|
+
uuid=wire_dict.get("uuid", str(uuid.uuid4())),
|
|
86
|
+
points=points,
|
|
87
|
+
wire_type=WireType(wire_dict.get("wire_type", "wire")),
|
|
88
|
+
stroke_width=wire_dict.get("stroke_width", 0.0),
|
|
89
|
+
stroke_type=wire_dict.get("stroke_type", "default")
|
|
90
|
+
)
|
|
91
|
+
wires.append(wire)
|
|
92
|
+
self._wires = WireCollection(wires)
|
|
93
|
+
|
|
94
|
+
# Initialize junction collection
|
|
95
|
+
junction_data = self._data.get("junctions", [])
|
|
96
|
+
junctions = []
|
|
97
|
+
for junction_dict in junction_data:
|
|
98
|
+
if isinstance(junction_dict, dict):
|
|
99
|
+
# Convert dict to Junction object
|
|
100
|
+
position = junction_dict.get("position", {"x": 0, "y": 0})
|
|
101
|
+
if isinstance(position, dict):
|
|
102
|
+
pos = Point(position["x"], position["y"])
|
|
103
|
+
elif isinstance(position, (list, tuple)):
|
|
104
|
+
pos = Point(position[0], position[1])
|
|
105
|
+
else:
|
|
106
|
+
pos = position
|
|
107
|
+
|
|
108
|
+
junction = Junction(
|
|
109
|
+
uuid=junction_dict.get("uuid", str(uuid.uuid4())),
|
|
110
|
+
position=pos,
|
|
111
|
+
diameter=junction_dict.get("diameter", 0),
|
|
112
|
+
color=junction_dict.get("color", (0, 0, 0, 0))
|
|
113
|
+
)
|
|
114
|
+
junctions.append(junction)
|
|
115
|
+
self._junctions = JunctionCollection(junctions)
|
|
116
|
+
|
|
65
117
|
# Track modifications for save optimization
|
|
66
118
|
self._modified = False
|
|
67
119
|
self._last_save_time = None
|
|
@@ -70,7 +122,7 @@ class Schematic:
|
|
|
70
122
|
self._operation_count = 0
|
|
71
123
|
self._total_operation_time = 0.0
|
|
72
124
|
|
|
73
|
-
logger.debug(f"Schematic initialized with {len(self._components)} components")
|
|
125
|
+
logger.debug(f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, and {len(self._junctions)} junctions")
|
|
74
126
|
|
|
75
127
|
@classmethod
|
|
76
128
|
def load(cls, file_path: Union[str, Path]) -> "Schematic":
|
|
@@ -101,19 +153,30 @@ class Schematic:
|
|
|
101
153
|
return cls(schematic_data, str(file_path))
|
|
102
154
|
|
|
103
155
|
@classmethod
|
|
104
|
-
def create(cls, name: str = "Untitled", version: str = "
|
|
156
|
+
def create(cls, name: str = "Untitled", version: str = "20250114",
|
|
157
|
+
generator: str = "eeschema", generator_version: str = "9.0",
|
|
158
|
+
paper: str = "A4", uuid: str = None) -> "Schematic":
|
|
105
159
|
"""
|
|
106
|
-
Create a new empty schematic.
|
|
160
|
+
Create a new empty schematic with configurable parameters.
|
|
107
161
|
|
|
108
162
|
Args:
|
|
109
163
|
name: Schematic name
|
|
110
|
-
version: KiCAD version
|
|
164
|
+
version: KiCAD version (default: "20250114")
|
|
165
|
+
generator: Generator name (default: "eeschema")
|
|
166
|
+
generator_version: Generator version (default: "9.0")
|
|
167
|
+
paper: Paper size (default: "A4")
|
|
168
|
+
uuid: Specific UUID (auto-generated if None)
|
|
111
169
|
|
|
112
170
|
Returns:
|
|
113
171
|
New empty Schematic object
|
|
114
172
|
"""
|
|
115
173
|
schematic_data = cls._create_empty_schematic_data()
|
|
116
174
|
schematic_data["version"] = version
|
|
175
|
+
schematic_data["generator"] = generator
|
|
176
|
+
schematic_data["generator_version"] = generator_version
|
|
177
|
+
schematic_data["paper"] = paper
|
|
178
|
+
if uuid:
|
|
179
|
+
schematic_data["uuid"] = uuid
|
|
117
180
|
schematic_data["title_block"] = {"title": name}
|
|
118
181
|
|
|
119
182
|
logger.info(f"Created new schematic: {name}")
|
|
@@ -125,6 +188,16 @@ class Schematic:
|
|
|
125
188
|
"""Collection of all components in the schematic."""
|
|
126
189
|
return self._components
|
|
127
190
|
|
|
191
|
+
@property
|
|
192
|
+
def wires(self) -> WireCollection:
|
|
193
|
+
"""Collection of all wires in the schematic."""
|
|
194
|
+
return self._wires
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def junctions(self) -> JunctionCollection:
|
|
198
|
+
"""Collection of all junctions in the schematic."""
|
|
199
|
+
return self._junctions
|
|
200
|
+
|
|
128
201
|
@property
|
|
129
202
|
def version(self) -> Optional[str]:
|
|
130
203
|
"""KiCAD version string."""
|
|
@@ -184,8 +257,10 @@ class Schematic:
|
|
|
184
257
|
if errors:
|
|
185
258
|
raise ValidationError("Cannot save schematic with validation errors", errors)
|
|
186
259
|
|
|
187
|
-
# Update data structure with current component state
|
|
260
|
+
# Update data structure with current component, wire, and junction state
|
|
188
261
|
self._sync_components_to_data()
|
|
262
|
+
self._sync_wires_to_data()
|
|
263
|
+
self._sync_junctions_to_data()
|
|
189
264
|
|
|
190
265
|
# Write file
|
|
191
266
|
if preserve_format and self._original_content:
|
|
@@ -258,6 +333,42 @@ class Schematic:
|
|
|
258
333
|
issues.extend(component_issues)
|
|
259
334
|
|
|
260
335
|
return issues
|
|
336
|
+
|
|
337
|
+
# Focused helper functions for specific KiCAD sections
|
|
338
|
+
def add_lib_symbols_section(self, lib_symbols: Dict[str, Any]):
|
|
339
|
+
"""Add or update lib_symbols section with specific symbol definitions."""
|
|
340
|
+
self._data["lib_symbols"] = lib_symbols
|
|
341
|
+
self._modified = True
|
|
342
|
+
|
|
343
|
+
def add_instances_section(self, instances: Dict[str, Any]):
|
|
344
|
+
"""Add instances section for component placement tracking."""
|
|
345
|
+
self._data["instances"] = instances
|
|
346
|
+
self._modified = True
|
|
347
|
+
|
|
348
|
+
def add_sheet_instances_section(self, sheet_instances: List[Dict]):
|
|
349
|
+
"""Add sheet_instances section for hierarchical design."""
|
|
350
|
+
self._data["sheet_instances"] = sheet_instances
|
|
351
|
+
self._modified = True
|
|
352
|
+
|
|
353
|
+
def set_paper_size(self, paper: str):
|
|
354
|
+
"""Set paper size (A4, A3, etc.)."""
|
|
355
|
+
self._data["paper"] = paper
|
|
356
|
+
self._modified = True
|
|
357
|
+
|
|
358
|
+
def set_version_info(self, version: str, generator: str = "eeschema", generator_version: str = "9.0"):
|
|
359
|
+
"""Set version and generator information."""
|
|
360
|
+
self._data["version"] = version
|
|
361
|
+
self._data["generator"] = generator
|
|
362
|
+
self._data["generator_version"] = generator_version
|
|
363
|
+
self._modified = True
|
|
364
|
+
|
|
365
|
+
def copy_metadata_from(self, source_schematic: "Schematic"):
|
|
366
|
+
"""Copy all metadata from another schematic (version, generator, paper, etc.)."""
|
|
367
|
+
metadata_fields = ["version", "generator", "generator_version", "paper", "uuid", "title_block"]
|
|
368
|
+
for field in metadata_fields:
|
|
369
|
+
if field in source_schematic._data:
|
|
370
|
+
self._data[field] = source_schematic._data[field]
|
|
371
|
+
self._modified = True
|
|
261
372
|
|
|
262
373
|
def get_summary(self) -> Dict[str, Any]:
|
|
263
374
|
"""Get summary information about the schematic."""
|
|
@@ -326,6 +437,382 @@ class Schematic:
|
|
|
326
437
|
return True
|
|
327
438
|
return False
|
|
328
439
|
|
|
440
|
+
# Label management
|
|
441
|
+
def add_hierarchical_label(
|
|
442
|
+
self,
|
|
443
|
+
text: str,
|
|
444
|
+
position: Union[Point, Tuple[float, float]],
|
|
445
|
+
shape: HierarchicalLabelShape = HierarchicalLabelShape.INPUT,
|
|
446
|
+
rotation: float = 0.0,
|
|
447
|
+
size: float = 1.27
|
|
448
|
+
) -> str:
|
|
449
|
+
"""
|
|
450
|
+
Add a hierarchical label.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
text: Label text
|
|
454
|
+
position: Label position
|
|
455
|
+
shape: Label shape/direction
|
|
456
|
+
rotation: Text rotation in degrees
|
|
457
|
+
size: Font size
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
UUID of created hierarchical label
|
|
461
|
+
"""
|
|
462
|
+
if isinstance(position, tuple):
|
|
463
|
+
position = Point(position[0], position[1])
|
|
464
|
+
|
|
465
|
+
label = Label(
|
|
466
|
+
uuid=str(uuid.uuid4()),
|
|
467
|
+
position=position,
|
|
468
|
+
text=text,
|
|
469
|
+
label_type=LabelType.HIERARCHICAL,
|
|
470
|
+
rotation=rotation,
|
|
471
|
+
size=size,
|
|
472
|
+
shape=shape
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if "hierarchical_labels" not in self._data:
|
|
476
|
+
self._data["hierarchical_labels"] = []
|
|
477
|
+
|
|
478
|
+
self._data["hierarchical_labels"].append({
|
|
479
|
+
"uuid": label.uuid,
|
|
480
|
+
"position": {"x": label.position.x, "y": label.position.y},
|
|
481
|
+
"text": label.text,
|
|
482
|
+
"shape": label.shape.value,
|
|
483
|
+
"rotation": label.rotation,
|
|
484
|
+
"size": label.size
|
|
485
|
+
})
|
|
486
|
+
self._modified = True
|
|
487
|
+
|
|
488
|
+
logger.debug(f"Added hierarchical label: {text} at {position}")
|
|
489
|
+
return label.uuid
|
|
490
|
+
|
|
491
|
+
def remove_hierarchical_label(self, label_uuid: str) -> bool:
|
|
492
|
+
"""Remove hierarchical label by UUID."""
|
|
493
|
+
labels = self._data.get("hierarchical_labels", [])
|
|
494
|
+
for i, label in enumerate(labels):
|
|
495
|
+
if label.get("uuid") == label_uuid:
|
|
496
|
+
del labels[i]
|
|
497
|
+
self._modified = True
|
|
498
|
+
logger.debug(f"Removed hierarchical label: {label_uuid}")
|
|
499
|
+
return True
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
def add_label(
|
|
503
|
+
self,
|
|
504
|
+
text: str,
|
|
505
|
+
position: Union[Point, Tuple[float, float]],
|
|
506
|
+
rotation: float = 0.0,
|
|
507
|
+
size: float = 1.27
|
|
508
|
+
) -> str:
|
|
509
|
+
"""
|
|
510
|
+
Add a local label.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
text: Label text
|
|
514
|
+
position: Label position
|
|
515
|
+
rotation: Text rotation in degrees
|
|
516
|
+
size: Font size
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
UUID of created label
|
|
520
|
+
"""
|
|
521
|
+
if isinstance(position, tuple):
|
|
522
|
+
position = Point(position[0], position[1])
|
|
523
|
+
|
|
524
|
+
label = Label(
|
|
525
|
+
uuid=str(uuid.uuid4()),
|
|
526
|
+
position=position,
|
|
527
|
+
text=text,
|
|
528
|
+
label_type=LabelType.LOCAL,
|
|
529
|
+
rotation=rotation,
|
|
530
|
+
size=size
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if "labels" not in self._data:
|
|
534
|
+
self._data["labels"] = []
|
|
535
|
+
|
|
536
|
+
self._data["labels"].append({
|
|
537
|
+
"uuid": label.uuid,
|
|
538
|
+
"position": {"x": label.position.x, "y": label.position.y},
|
|
539
|
+
"text": label.text,
|
|
540
|
+
"rotation": label.rotation,
|
|
541
|
+
"size": label.size
|
|
542
|
+
})
|
|
543
|
+
self._modified = True
|
|
544
|
+
|
|
545
|
+
logger.debug(f"Added local label: {text} at {position}")
|
|
546
|
+
return label.uuid
|
|
547
|
+
|
|
548
|
+
def remove_label(self, label_uuid: str) -> bool:
|
|
549
|
+
"""Remove local label by UUID."""
|
|
550
|
+
labels = self._data.get("labels", [])
|
|
551
|
+
for i, label in enumerate(labels):
|
|
552
|
+
if label.get("uuid") == label_uuid:
|
|
553
|
+
del labels[i]
|
|
554
|
+
self._modified = True
|
|
555
|
+
logger.debug(f"Removed local label: {label_uuid}")
|
|
556
|
+
return True
|
|
557
|
+
return False
|
|
558
|
+
|
|
559
|
+
def add_sheet(
|
|
560
|
+
self,
|
|
561
|
+
name: str,
|
|
562
|
+
filename: str,
|
|
563
|
+
position: Union[Point, Tuple[float, float]],
|
|
564
|
+
size: Union[Point, Tuple[float, float]],
|
|
565
|
+
stroke_width: float = 0.1524,
|
|
566
|
+
stroke_type: str = "solid",
|
|
567
|
+
exclude_from_sim: bool = False,
|
|
568
|
+
in_bom: bool = True,
|
|
569
|
+
on_board: bool = True,
|
|
570
|
+
project_name: str = "",
|
|
571
|
+
page_number: str = "2"
|
|
572
|
+
) -> str:
|
|
573
|
+
"""
|
|
574
|
+
Add a hierarchical sheet.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
name: Sheet name (displayed above sheet)
|
|
578
|
+
filename: Sheet filename (.kicad_sch file)
|
|
579
|
+
position: Sheet position (top-left corner)
|
|
580
|
+
size: Sheet size (width, height)
|
|
581
|
+
stroke_width: Border line width
|
|
582
|
+
stroke_type: Border line type
|
|
583
|
+
exclude_from_sim: Exclude from simulation
|
|
584
|
+
in_bom: Include in BOM
|
|
585
|
+
on_board: Include on board
|
|
586
|
+
project_name: Project name for instances
|
|
587
|
+
page_number: Page number for instances
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
UUID of created sheet
|
|
591
|
+
"""
|
|
592
|
+
if isinstance(position, tuple):
|
|
593
|
+
position = Point(position[0], position[1])
|
|
594
|
+
if isinstance(size, tuple):
|
|
595
|
+
size = Point(size[0], size[1])
|
|
596
|
+
|
|
597
|
+
sheet = Sheet(
|
|
598
|
+
uuid=str(uuid.uuid4()),
|
|
599
|
+
position=position,
|
|
600
|
+
size=size,
|
|
601
|
+
name=name,
|
|
602
|
+
filename=filename,
|
|
603
|
+
exclude_from_sim=exclude_from_sim,
|
|
604
|
+
in_bom=in_bom,
|
|
605
|
+
on_board=on_board,
|
|
606
|
+
stroke_width=stroke_width,
|
|
607
|
+
stroke_type=stroke_type
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
if "sheets" not in self._data:
|
|
611
|
+
self._data["sheets"] = []
|
|
612
|
+
|
|
613
|
+
self._data["sheets"].append({
|
|
614
|
+
"uuid": sheet.uuid,
|
|
615
|
+
"position": {"x": sheet.position.x, "y": sheet.position.y},
|
|
616
|
+
"size": {"width": sheet.size.x, "height": sheet.size.y},
|
|
617
|
+
"name": sheet.name,
|
|
618
|
+
"filename": sheet.filename,
|
|
619
|
+
"exclude_from_sim": sheet.exclude_from_sim,
|
|
620
|
+
"in_bom": sheet.in_bom,
|
|
621
|
+
"on_board": sheet.on_board,
|
|
622
|
+
"dnp": sheet.dnp,
|
|
623
|
+
"fields_autoplaced": sheet.fields_autoplaced,
|
|
624
|
+
"stroke_width": sheet.stroke_width,
|
|
625
|
+
"stroke_type": sheet.stroke_type,
|
|
626
|
+
"fill_color": sheet.fill_color,
|
|
627
|
+
"pins": [], # Sheet pins added separately
|
|
628
|
+
"project_name": project_name,
|
|
629
|
+
"page_number": page_number
|
|
630
|
+
})
|
|
631
|
+
self._modified = True
|
|
632
|
+
|
|
633
|
+
logger.debug(f"Added hierarchical sheet: {name} ({filename}) at {position}")
|
|
634
|
+
return sheet.uuid
|
|
635
|
+
|
|
636
|
+
def add_sheet_pin(
|
|
637
|
+
self,
|
|
638
|
+
sheet_uuid: str,
|
|
639
|
+
name: str,
|
|
640
|
+
pin_type: str = "input",
|
|
641
|
+
position: Union[Point, Tuple[float, float]] = (0, 0),
|
|
642
|
+
rotation: float = 0,
|
|
643
|
+
size: float = 1.27,
|
|
644
|
+
justify: str = "right"
|
|
645
|
+
) -> str:
|
|
646
|
+
"""
|
|
647
|
+
Add a pin to a hierarchical sheet.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
sheet_uuid: UUID of the sheet to add pin to
|
|
651
|
+
name: Pin name (NET1, NET2, etc.)
|
|
652
|
+
pin_type: Pin type (input, output, bidirectional, etc.)
|
|
653
|
+
position: Pin position relative to sheet
|
|
654
|
+
rotation: Pin rotation in degrees
|
|
655
|
+
size: Font size for pin label
|
|
656
|
+
justify: Text justification (left, right, center)
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
UUID of created sheet pin
|
|
660
|
+
"""
|
|
661
|
+
if isinstance(position, tuple):
|
|
662
|
+
position = Point(position[0], position[1])
|
|
663
|
+
|
|
664
|
+
pin_uuid = str(uuid.uuid4())
|
|
665
|
+
|
|
666
|
+
# Find the sheet in the data
|
|
667
|
+
sheets = self._data.get("sheets", [])
|
|
668
|
+
for sheet in sheets:
|
|
669
|
+
if sheet.get("uuid") == sheet_uuid:
|
|
670
|
+
# Add pin to the sheet's pins list
|
|
671
|
+
pin_data = {
|
|
672
|
+
"uuid": pin_uuid,
|
|
673
|
+
"name": name,
|
|
674
|
+
"pin_type": pin_type,
|
|
675
|
+
"position": {"x": position.x, "y": position.y},
|
|
676
|
+
"rotation": rotation,
|
|
677
|
+
"size": size,
|
|
678
|
+
"justify": justify
|
|
679
|
+
}
|
|
680
|
+
sheet["pins"].append(pin_data)
|
|
681
|
+
self._modified = True
|
|
682
|
+
|
|
683
|
+
logger.debug(f"Added sheet pin: {name} ({pin_type}) to sheet {sheet_uuid}")
|
|
684
|
+
return pin_uuid
|
|
685
|
+
|
|
686
|
+
raise ValueError(f"Sheet with UUID '{sheet_uuid}' not found")
|
|
687
|
+
|
|
688
|
+
def add_text(
|
|
689
|
+
self,
|
|
690
|
+
text: str,
|
|
691
|
+
position: Union[Point, Tuple[float, float]],
|
|
692
|
+
rotation: float = 0.0,
|
|
693
|
+
size: float = 1.27,
|
|
694
|
+
exclude_from_sim: bool = False
|
|
695
|
+
) -> str:
|
|
696
|
+
"""
|
|
697
|
+
Add a text element.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
text: Text content
|
|
701
|
+
position: Text position
|
|
702
|
+
rotation: Text rotation in degrees
|
|
703
|
+
size: Font size
|
|
704
|
+
exclude_from_sim: Exclude from simulation
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
UUID of created text element
|
|
708
|
+
"""
|
|
709
|
+
if isinstance(position, tuple):
|
|
710
|
+
position = Point(position[0], position[1])
|
|
711
|
+
|
|
712
|
+
text_element = Text(
|
|
713
|
+
uuid=str(uuid.uuid4()),
|
|
714
|
+
position=position,
|
|
715
|
+
text=text,
|
|
716
|
+
rotation=rotation,
|
|
717
|
+
size=size,
|
|
718
|
+
exclude_from_sim=exclude_from_sim
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
if "texts" not in self._data:
|
|
722
|
+
self._data["texts"] = []
|
|
723
|
+
|
|
724
|
+
self._data["texts"].append({
|
|
725
|
+
"uuid": text_element.uuid,
|
|
726
|
+
"position": {"x": text_element.position.x, "y": text_element.position.y},
|
|
727
|
+
"text": text_element.text,
|
|
728
|
+
"rotation": text_element.rotation,
|
|
729
|
+
"size": text_element.size,
|
|
730
|
+
"exclude_from_sim": text_element.exclude_from_sim
|
|
731
|
+
})
|
|
732
|
+
self._modified = True
|
|
733
|
+
|
|
734
|
+
logger.debug(f"Added text: '{text}' at {position}")
|
|
735
|
+
return text_element.uuid
|
|
736
|
+
|
|
737
|
+
def add_text_box(
|
|
738
|
+
self,
|
|
739
|
+
text: str,
|
|
740
|
+
position: Union[Point, Tuple[float, float]],
|
|
741
|
+
size: Union[Point, Tuple[float, float]],
|
|
742
|
+
rotation: float = 0.0,
|
|
743
|
+
font_size: float = 1.27,
|
|
744
|
+
margins: Tuple[float, float, float, float] = (0.9525, 0.9525, 0.9525, 0.9525),
|
|
745
|
+
stroke_width: float = 0.0,
|
|
746
|
+
stroke_type: str = "solid",
|
|
747
|
+
fill_type: str = "none",
|
|
748
|
+
justify_horizontal: str = "left",
|
|
749
|
+
justify_vertical: str = "top",
|
|
750
|
+
exclude_from_sim: bool = False
|
|
751
|
+
) -> str:
|
|
752
|
+
"""
|
|
753
|
+
Add a text box element.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
text: Text content
|
|
757
|
+
position: Text box position (top-left corner)
|
|
758
|
+
size: Text box size (width, height)
|
|
759
|
+
rotation: Text rotation in degrees
|
|
760
|
+
font_size: Font size
|
|
761
|
+
margins: Margins (top, right, bottom, left)
|
|
762
|
+
stroke_width: Border line width
|
|
763
|
+
stroke_type: Border line type
|
|
764
|
+
fill_type: Fill type (none, solid, etc.)
|
|
765
|
+
justify_horizontal: Horizontal text alignment
|
|
766
|
+
justify_vertical: Vertical text alignment
|
|
767
|
+
exclude_from_sim: Exclude from simulation
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
UUID of created text box element
|
|
771
|
+
"""
|
|
772
|
+
if isinstance(position, tuple):
|
|
773
|
+
position = Point(position[0], position[1])
|
|
774
|
+
if isinstance(size, tuple):
|
|
775
|
+
size = Point(size[0], size[1])
|
|
776
|
+
|
|
777
|
+
text_box = TextBox(
|
|
778
|
+
uuid=str(uuid.uuid4()),
|
|
779
|
+
position=position,
|
|
780
|
+
size=size,
|
|
781
|
+
text=text,
|
|
782
|
+
rotation=rotation,
|
|
783
|
+
font_size=font_size,
|
|
784
|
+
margins=margins,
|
|
785
|
+
stroke_width=stroke_width,
|
|
786
|
+
stroke_type=stroke_type,
|
|
787
|
+
fill_type=fill_type,
|
|
788
|
+
justify_horizontal=justify_horizontal,
|
|
789
|
+
justify_vertical=justify_vertical,
|
|
790
|
+
exclude_from_sim=exclude_from_sim
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
if "text_boxes" not in self._data:
|
|
794
|
+
self._data["text_boxes"] = []
|
|
795
|
+
|
|
796
|
+
self._data["text_boxes"].append({
|
|
797
|
+
"uuid": text_box.uuid,
|
|
798
|
+
"position": {"x": text_box.position.x, "y": text_box.position.y},
|
|
799
|
+
"size": {"width": text_box.size.x, "height": text_box.size.y},
|
|
800
|
+
"text": text_box.text,
|
|
801
|
+
"rotation": text_box.rotation,
|
|
802
|
+
"font_size": text_box.font_size,
|
|
803
|
+
"margins": text_box.margins,
|
|
804
|
+
"stroke_width": text_box.stroke_width,
|
|
805
|
+
"stroke_type": text_box.stroke_type,
|
|
806
|
+
"fill_type": text_box.fill_type,
|
|
807
|
+
"justify_horizontal": text_box.justify_horizontal,
|
|
808
|
+
"justify_vertical": text_box.justify_vertical,
|
|
809
|
+
"exclude_from_sim": text_box.exclude_from_sim
|
|
810
|
+
})
|
|
811
|
+
self._modified = True
|
|
812
|
+
|
|
813
|
+
logger.debug(f"Added text box: '{text}' at {position} size {size}")
|
|
814
|
+
return text_box.uuid
|
|
815
|
+
|
|
329
816
|
# Library management
|
|
330
817
|
@property
|
|
331
818
|
def libraries(self) -> "LibraryManager":
|
|
@@ -390,14 +877,250 @@ class Schematic:
|
|
|
390
877
|
def _sync_components_to_data(self):
|
|
391
878
|
"""Sync component collection state back to data structure."""
|
|
392
879
|
self._data["components"] = [comp._data.__dict__ for comp in self._components]
|
|
880
|
+
|
|
881
|
+
# Populate lib_symbols with actual symbol definitions used by components
|
|
882
|
+
lib_symbols = {}
|
|
883
|
+
cache = get_symbol_cache()
|
|
884
|
+
|
|
885
|
+
for comp in self._components:
|
|
886
|
+
if comp.lib_id and comp.lib_id not in lib_symbols:
|
|
887
|
+
logger.debug(f"🔧 SCHEMATIC: Processing component {comp.lib_id}")
|
|
888
|
+
|
|
889
|
+
# Get the actual symbol definition
|
|
890
|
+
symbol_def = cache.get_symbol(comp.lib_id)
|
|
891
|
+
if symbol_def:
|
|
892
|
+
logger.debug(f"🔧 SCHEMATIC: Loaded symbol {comp.lib_id}")
|
|
893
|
+
lib_symbols[comp.lib_id] = self._convert_symbol_to_kicad_format(symbol_def, comp.lib_id)
|
|
894
|
+
|
|
895
|
+
# Check if this symbol extends another symbol using multiple methods
|
|
896
|
+
extends_parent = None
|
|
897
|
+
|
|
898
|
+
# Method 1: Check raw_kicad_data
|
|
899
|
+
if hasattr(symbol_def, 'raw_kicad_data') and symbol_def.raw_kicad_data:
|
|
900
|
+
extends_parent = self._check_symbol_extends(symbol_def.raw_kicad_data)
|
|
901
|
+
logger.debug(f"🔧 SCHEMATIC: Checked raw_kicad_data for {comp.lib_id}, extends: {extends_parent}")
|
|
902
|
+
|
|
903
|
+
# Method 2: Check raw_data attribute
|
|
904
|
+
if not extends_parent and hasattr(symbol_def, '__dict__'):
|
|
905
|
+
for attr_name, attr_value in symbol_def.__dict__.items():
|
|
906
|
+
if attr_name == 'raw_data':
|
|
907
|
+
logger.debug(f"🔧 SCHEMATIC: Checking raw_data for extends: {type(attr_value)}")
|
|
908
|
+
extends_parent = self._check_symbol_extends(attr_value)
|
|
909
|
+
if extends_parent:
|
|
910
|
+
logger.debug(f"🔧 SCHEMATIC: Found extends in raw_data: {extends_parent}")
|
|
911
|
+
|
|
912
|
+
# Method 3: Check the extends attribute directly
|
|
913
|
+
if not extends_parent and hasattr(symbol_def, 'extends'):
|
|
914
|
+
extends_parent = symbol_def.extends
|
|
915
|
+
logger.debug(f"🔧 SCHEMATIC: Found extends attribute: {extends_parent}")
|
|
916
|
+
|
|
917
|
+
if extends_parent:
|
|
918
|
+
# Load the parent symbol too
|
|
919
|
+
parent_lib_id = f"{comp.lib_id.split(':')[0]}:{extends_parent}"
|
|
920
|
+
logger.debug(f"🔧 SCHEMATIC: Loading parent symbol: {parent_lib_id}")
|
|
921
|
+
|
|
922
|
+
if parent_lib_id not in lib_symbols:
|
|
923
|
+
parent_symbol_def = cache.get_symbol(parent_lib_id)
|
|
924
|
+
if parent_symbol_def:
|
|
925
|
+
lib_symbols[parent_lib_id] = self._convert_symbol_to_kicad_format(parent_symbol_def, parent_lib_id)
|
|
926
|
+
logger.debug(f"🔧 SCHEMATIC: Successfully loaded parent symbol: {parent_lib_id} for {comp.lib_id}")
|
|
927
|
+
else:
|
|
928
|
+
logger.warning(f"🔧 SCHEMATIC: Failed to load parent symbol: {parent_lib_id}")
|
|
929
|
+
else:
|
|
930
|
+
logger.debug(f"🔧 SCHEMATIC: Parent symbol {parent_lib_id} already loaded")
|
|
931
|
+
else:
|
|
932
|
+
logger.debug(f"🔧 SCHEMATIC: No extends found for {comp.lib_id}")
|
|
933
|
+
else:
|
|
934
|
+
# Fallback for unknown symbols
|
|
935
|
+
logger.warning(f"🔧 SCHEMATIC: Failed to load symbol {comp.lib_id}, using fallback")
|
|
936
|
+
lib_symbols[comp.lib_id] = {"definition": "basic"}
|
|
937
|
+
|
|
938
|
+
self._data["lib_symbols"] = lib_symbols
|
|
939
|
+
|
|
940
|
+
# Debug: Log the final lib_symbols structure
|
|
941
|
+
logger.debug(f"🔧 FINAL: lib_symbols contains {len(lib_symbols)} symbols:")
|
|
942
|
+
for sym_id in lib_symbols.keys():
|
|
943
|
+
logger.debug(f"🔧 FINAL: - {sym_id}")
|
|
944
|
+
# Check if this symbol has extends
|
|
945
|
+
sym_data = lib_symbols[sym_id]
|
|
946
|
+
if isinstance(sym_data, list) and len(sym_data) > 2:
|
|
947
|
+
for item in sym_data[1:]:
|
|
948
|
+
if isinstance(item, list) and len(item) >= 2:
|
|
949
|
+
if item[0] == sexpdata.Symbol('extends'):
|
|
950
|
+
logger.debug(f"🔧 FINAL: - {sym_id} extends {item[1]}")
|
|
951
|
+
break
|
|
952
|
+
|
|
953
|
+
def _check_symbol_extends(self, symbol_data: Any) -> Optional[str]:
|
|
954
|
+
"""Check if symbol extends another symbol and return parent name."""
|
|
955
|
+
logger.debug(f"🔧 EXTENDS: Checking symbol data type: {type(symbol_data)}")
|
|
956
|
+
|
|
957
|
+
if not isinstance(symbol_data, list):
|
|
958
|
+
logger.debug(f"🔧 EXTENDS: Not a list, returning None")
|
|
959
|
+
return None
|
|
960
|
+
|
|
961
|
+
logger.debug(f"🔧 EXTENDS: Checking {len(symbol_data)} items for extends directive")
|
|
962
|
+
|
|
963
|
+
for i, item in enumerate(symbol_data[1:], 1):
|
|
964
|
+
logger.debug(f"🔧 EXTENDS: Item {i}: {type(item)} - {item if not isinstance(item, list) else f'list[{len(item)}]'}")
|
|
965
|
+
if isinstance(item, list) and len(item) >= 2:
|
|
966
|
+
if item[0] == sexpdata.Symbol('extends'):
|
|
967
|
+
parent_name = str(item[1]).strip('"')
|
|
968
|
+
logger.debug(f"🔧 EXTENDS: Found extends directive: {parent_name}")
|
|
969
|
+
return parent_name
|
|
970
|
+
|
|
971
|
+
logger.debug(f"🔧 EXTENDS: No extends directive found")
|
|
972
|
+
return None
|
|
973
|
+
|
|
974
|
+
def _sync_wires_to_data(self):
|
|
975
|
+
"""Sync wire collection state back to data structure."""
|
|
976
|
+
wire_data = []
|
|
977
|
+
for wire in self._wires:
|
|
978
|
+
wire_dict = {
|
|
979
|
+
"uuid": wire.uuid,
|
|
980
|
+
"points": [{"x": p.x, "y": p.y} for p in wire.points],
|
|
981
|
+
"wire_type": wire.wire_type.value,
|
|
982
|
+
"stroke_width": wire.stroke_width,
|
|
983
|
+
"stroke_type": wire.stroke_type
|
|
984
|
+
}
|
|
985
|
+
wire_data.append(wire_dict)
|
|
986
|
+
|
|
987
|
+
self._data["wires"] = wire_data
|
|
988
|
+
|
|
989
|
+
def _sync_junctions_to_data(self):
|
|
990
|
+
"""Sync junction collection state back to data structure."""
|
|
991
|
+
junction_data = []
|
|
992
|
+
for junction in self._junctions:
|
|
993
|
+
junction_dict = {
|
|
994
|
+
"uuid": junction.uuid,
|
|
995
|
+
"position": {"x": junction.position.x, "y": junction.position.y},
|
|
996
|
+
"diameter": junction.diameter,
|
|
997
|
+
"color": junction.color
|
|
998
|
+
}
|
|
999
|
+
junction_data.append(junction_dict)
|
|
1000
|
+
|
|
1001
|
+
self._data["junctions"] = junction_data
|
|
1002
|
+
|
|
1003
|
+
def _convert_symbol_to_kicad_format(self, symbol: "SymbolDefinition", lib_id: str) -> Dict[str, Any]:
|
|
1004
|
+
"""Convert SymbolDefinition to KiCAD lib_symbols format using raw parsed data."""
|
|
1005
|
+
# If we have raw KiCAD data from the library file, use it directly
|
|
1006
|
+
if hasattr(symbol, 'raw_kicad_data') and symbol.raw_kicad_data:
|
|
1007
|
+
return self._convert_raw_symbol_data(symbol.raw_kicad_data, lib_id)
|
|
1008
|
+
|
|
1009
|
+
# Fallback: create basic symbol structure
|
|
1010
|
+
return {
|
|
1011
|
+
"pin_numbers": {"hide": "yes"},
|
|
1012
|
+
"pin_names": {"offset": 0},
|
|
1013
|
+
"exclude_from_sim": "no",
|
|
1014
|
+
"in_bom": "yes",
|
|
1015
|
+
"on_board": "yes",
|
|
1016
|
+
"properties": {
|
|
1017
|
+
"Reference": {
|
|
1018
|
+
"value": symbol.reference_prefix,
|
|
1019
|
+
"at": [2.032, 0, 90],
|
|
1020
|
+
"effects": {"font": {"size": [1.27, 1.27]}}
|
|
1021
|
+
},
|
|
1022
|
+
"Value": {
|
|
1023
|
+
"value": symbol.reference_prefix,
|
|
1024
|
+
"at": [0, 0, 90],
|
|
1025
|
+
"effects": {"font": {"size": [1.27, 1.27]}}
|
|
1026
|
+
},
|
|
1027
|
+
"Footprint": {
|
|
1028
|
+
"value": "",
|
|
1029
|
+
"at": [-1.778, 0, 90],
|
|
1030
|
+
"effects": {
|
|
1031
|
+
"font": {"size": [1.27, 1.27]},
|
|
1032
|
+
"hide": "yes"
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
"Datasheet": {
|
|
1036
|
+
"value": getattr(symbol, 'Datasheet', None) or getattr(symbol, 'datasheet', None) or "~",
|
|
1037
|
+
"at": [0, 0, 0],
|
|
1038
|
+
"effects": {
|
|
1039
|
+
"font": {"size": [1.27, 1.27]},
|
|
1040
|
+
"hide": "yes"
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
"Description": {
|
|
1044
|
+
"value": getattr(symbol, 'Description', None) or getattr(symbol, 'description', None) or "Resistor",
|
|
1045
|
+
"at": [0, 0, 0],
|
|
1046
|
+
"effects": {
|
|
1047
|
+
"font": {"size": [1.27, 1.27]},
|
|
1048
|
+
"hide": "yes"
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
"embedded_fonts": "no"
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
def _convert_raw_symbol_data(self, raw_data: List, lib_id: str) -> Dict[str, Any]:
|
|
1056
|
+
"""Convert raw parsed KiCAD symbol data to dictionary format for S-expression generation."""
|
|
1057
|
+
import copy
|
|
1058
|
+
import sexpdata
|
|
1059
|
+
|
|
1060
|
+
# Make a copy and fix symbol name and string/symbol issues
|
|
1061
|
+
modified_data = copy.deepcopy(raw_data)
|
|
1062
|
+
|
|
1063
|
+
# Replace the symbol name with the full lib_id
|
|
1064
|
+
if len(modified_data) >= 2:
|
|
1065
|
+
modified_data[1] = lib_id # Change 'R' to 'Device:R'
|
|
1066
|
+
|
|
1067
|
+
# Fix extends directive to use full lib_id
|
|
1068
|
+
logger.debug(f"🔧 CONVERT: Processing {len(modified_data)} items for {lib_id}")
|
|
1069
|
+
for i, item in enumerate(modified_data[1:], 1):
|
|
1070
|
+
if isinstance(item, list) and len(item) >= 2:
|
|
1071
|
+
logger.debug(f"🔧 CONVERT: Item {i}: {item[0]} = {item[1] if len(item) > 1 else 'N/A'}")
|
|
1072
|
+
if item[0] == sexpdata.Symbol('extends'):
|
|
1073
|
+
# Convert bare symbol name to full lib_id
|
|
1074
|
+
parent_name = str(item[1]).strip('"')
|
|
1075
|
+
parent_lib_id = f"{lib_id.split(':')[0]}:{parent_name}"
|
|
1076
|
+
modified_data[i][1] = parent_lib_id
|
|
1077
|
+
logger.debug(f"🔧 CONVERT: Fixed extends directive: {parent_name} -> {parent_lib_id}")
|
|
1078
|
+
break
|
|
1079
|
+
|
|
1080
|
+
# Fix string/symbol conversion issues in pin definitions
|
|
1081
|
+
print(f"🔧 DEBUG: Before fix - checking for pin definitions...")
|
|
1082
|
+
self._fix_symbol_strings_recursively(modified_data)
|
|
1083
|
+
print(f"🔧 DEBUG: After fix - symbol strings fixed")
|
|
1084
|
+
|
|
1085
|
+
return modified_data
|
|
1086
|
+
|
|
1087
|
+
def _fix_symbol_strings_recursively(self, data):
|
|
1088
|
+
"""Recursively fix string/symbol issues in parsed S-expression data."""
|
|
1089
|
+
import sexpdata
|
|
1090
|
+
|
|
1091
|
+
if isinstance(data, list):
|
|
1092
|
+
for i, item in enumerate(data):
|
|
1093
|
+
if isinstance(item, list):
|
|
1094
|
+
# Check for pin definitions that need fixing
|
|
1095
|
+
if (len(item) >= 3 and
|
|
1096
|
+
item[0] == sexpdata.Symbol('pin')):
|
|
1097
|
+
print(f"🔧 DEBUG: Found pin definition: {item[:3]} - types: {[type(x) for x in item[:3]]}")
|
|
1098
|
+
# Fix pin type and shape - ensure they are symbols not strings
|
|
1099
|
+
if isinstance(item[1], str):
|
|
1100
|
+
print(f"🔧 DEBUG: Converting pin type '{item[1]}' to symbol")
|
|
1101
|
+
item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
|
|
1102
|
+
if len(item) >= 3 and isinstance(item[2], str):
|
|
1103
|
+
print(f"🔧 DEBUG: Converting pin shape '{item[2]}' to symbol")
|
|
1104
|
+
item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
|
|
1105
|
+
|
|
1106
|
+
# Recursively process nested lists
|
|
1107
|
+
self._fix_symbol_strings_recursively(item)
|
|
1108
|
+
elif isinstance(item, str):
|
|
1109
|
+
# Fix common KiCAD keywords that should be symbols
|
|
1110
|
+
if item in ['yes', 'no', 'default', 'none', 'left', 'right', 'center']:
|
|
1111
|
+
data[i] = sexpdata.Symbol(item)
|
|
1112
|
+
|
|
1113
|
+
return data
|
|
393
1114
|
|
|
394
1115
|
@staticmethod
|
|
395
1116
|
def _create_empty_schematic_data() -> Dict[str, Any]:
|
|
396
1117
|
"""Create empty schematic data structure."""
|
|
397
1118
|
return {
|
|
398
|
-
"version": "
|
|
399
|
-
"generator": "
|
|
1119
|
+
"version": "20250114",
|
|
1120
|
+
"generator": "eeschema",
|
|
1121
|
+
"generator_version": "9.0",
|
|
400
1122
|
"uuid": str(uuid.uuid4()),
|
|
1123
|
+
"paper": "A4",
|
|
401
1124
|
"title_block": {
|
|
402
1125
|
"title": "Untitled",
|
|
403
1126
|
"date": "",
|
|
@@ -411,6 +1134,14 @@ class Schematic:
|
|
|
411
1134
|
"labels": [],
|
|
412
1135
|
"nets": [],
|
|
413
1136
|
"lib_symbols": {},
|
|
1137
|
+
"sheet_instances": [
|
|
1138
|
+
{
|
|
1139
|
+
"path": "/",
|
|
1140
|
+
"page": "1"
|
|
1141
|
+
}
|
|
1142
|
+
],
|
|
1143
|
+
"symbol_instances": [],
|
|
1144
|
+
"embedded_fonts": "no",
|
|
414
1145
|
}
|
|
415
1146
|
|
|
416
1147
|
# Context manager support for atomic operations
|