kicad-sch-api 0.3.4__py3-none-any.whl → 0.4.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.

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  7. kicad_sch_api/core/components.py +5 -0
  8. kicad_sch_api/core/formatter.py +3 -1
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/managers/__init__.py +26 -0
  11. kicad_sch_api/core/managers/file_io.py +243 -0
  12. kicad_sch_api/core/managers/format_sync.py +501 -0
  13. kicad_sch_api/core/managers/graphics.py +579 -0
  14. kicad_sch_api/core/managers/metadata.py +268 -0
  15. kicad_sch_api/core/managers/sheet.py +454 -0
  16. kicad_sch_api/core/managers/text_elements.py +536 -0
  17. kicad_sch_api/core/managers/validation.py +474 -0
  18. kicad_sch_api/core/managers/wire.py +346 -0
  19. kicad_sch_api/core/nets.py +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
  42. kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
  43. kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,276 @@
1
+ """
2
+ No-connect element management for KiCAD schematics.
3
+
4
+ This module provides collection classes for managing no-connect elements,
5
+ featuring fast lookup, bulk operations, and validation.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
+
12
+ from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
+ from .types import NoConnect, Point
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class NoConnectElement:
19
+ """
20
+ Enhanced wrapper for schematic no-connect elements with modern API.
21
+
22
+ Provides intuitive access to no-connect properties and operations
23
+ while maintaining exact format preservation.
24
+ """
25
+
26
+ def __init__(self, no_connect_data: NoConnect, parent_collection: "NoConnectCollection"):
27
+ """
28
+ Initialize no-connect element wrapper.
29
+
30
+ Args:
31
+ no_connect_data: Underlying no-connect data
32
+ parent_collection: Parent collection for updates
33
+ """
34
+ self._data = no_connect_data
35
+ self._collection = parent_collection
36
+ self._validator = SchematicValidator()
37
+
38
+ # Core properties with validation
39
+ @property
40
+ def uuid(self) -> str:
41
+ """No-connect element UUID."""
42
+ return self._data.uuid
43
+
44
+ @property
45
+ def position(self) -> Point:
46
+ """No-connect position."""
47
+ return self._data.position
48
+
49
+ @position.setter
50
+ def position(self, value: Union[Point, Tuple[float, float]]):
51
+ """Set no-connect position."""
52
+ if isinstance(value, tuple):
53
+ value = Point(value[0], value[1])
54
+ elif not isinstance(value, Point):
55
+ raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
56
+ self._data.position = value
57
+ self._collection._mark_modified()
58
+
59
+ def validate(self) -> List[ValidationIssue]:
60
+ """Validate this no-connect element."""
61
+ return self._validator.validate_no_connect(self._data.__dict__)
62
+
63
+ def to_dict(self) -> Dict[str, Any]:
64
+ """Convert no-connect element to dictionary representation."""
65
+ return {
66
+ "uuid": self.uuid,
67
+ "position": {"x": self.position.x, "y": self.position.y},
68
+ }
69
+
70
+ def __str__(self) -> str:
71
+ """String representation."""
72
+ return f"<NoConnect @ {self.position}>"
73
+
74
+
75
+ class NoConnectCollection:
76
+ """
77
+ Collection class for efficient no-connect element management.
78
+
79
+ Provides fast lookup, filtering, and bulk operations for schematic no-connect elements.
80
+ """
81
+
82
+ def __init__(self, no_connects: List[NoConnect] = None):
83
+ """
84
+ Initialize no-connect collection.
85
+
86
+ Args:
87
+ no_connects: Initial list of no-connect data
88
+ """
89
+ self._no_connects: List[NoConnectElement] = []
90
+ self._uuid_index: Dict[str, NoConnectElement] = {}
91
+ self._position_index: Dict[Tuple[float, float], List[NoConnectElement]] = {}
92
+ self._modified = False
93
+
94
+ # Add initial no-connects
95
+ if no_connects:
96
+ for no_connect_data in no_connects:
97
+ self._add_to_indexes(NoConnectElement(no_connect_data, self))
98
+
99
+ logger.debug(f"NoConnectCollection initialized with {len(self._no_connects)} no-connects")
100
+
101
+ def add(
102
+ self,
103
+ position: Union[Point, Tuple[float, float]],
104
+ no_connect_uuid: Optional[str] = None,
105
+ ) -> NoConnectElement:
106
+ """
107
+ Add a new no-connect element to the schematic.
108
+
109
+ Args:
110
+ position: No-connect position
111
+ no_connect_uuid: Specific UUID for no-connect (auto-generated if None)
112
+
113
+ Returns:
114
+ Newly created NoConnectElement
115
+
116
+ Raises:
117
+ ValidationError: If no-connect data is invalid
118
+ """
119
+ # Validate inputs
120
+ if isinstance(position, tuple):
121
+ position = Point(position[0], position[1])
122
+ elif not isinstance(position, Point):
123
+ raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
124
+
125
+ # Generate UUID if not provided
126
+ if not no_connect_uuid:
127
+ no_connect_uuid = str(uuid.uuid4())
128
+
129
+ # Check for duplicate UUID
130
+ if no_connect_uuid in self._uuid_index:
131
+ raise ValidationError(f"NoConnect UUID {no_connect_uuid} already exists")
132
+
133
+ # Create no-connect data
134
+ no_connect_data = NoConnect(
135
+ uuid=no_connect_uuid,
136
+ position=position,
137
+ )
138
+
139
+ # Create wrapper and add to collection
140
+ no_connect_element = NoConnectElement(no_connect_data, self)
141
+ self._add_to_indexes(no_connect_element)
142
+ self._mark_modified()
143
+
144
+ logger.debug(f"Added no-connect: {no_connect_element}")
145
+ return no_connect_element
146
+
147
+ def get(self, no_connect_uuid: str) -> Optional[NoConnectElement]:
148
+ """Get no-connect by UUID."""
149
+ return self._uuid_index.get(no_connect_uuid)
150
+
151
+ def remove(self, no_connect_uuid: str) -> bool:
152
+ """
153
+ Remove no-connect by UUID.
154
+
155
+ Args:
156
+ no_connect_uuid: UUID of no-connect to remove
157
+
158
+ Returns:
159
+ True if no-connect was removed, False if not found
160
+ """
161
+ no_connect_element = self._uuid_index.get(no_connect_uuid)
162
+ if not no_connect_element:
163
+ return False
164
+
165
+ # Remove from indexes
166
+ self._remove_from_indexes(no_connect_element)
167
+ self._mark_modified()
168
+
169
+ logger.debug(f"Removed no-connect: {no_connect_element}")
170
+ return True
171
+
172
+ def find_at_position(
173
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.1
174
+ ) -> List[NoConnectElement]:
175
+ """
176
+ Find no-connects at or near a position.
177
+
178
+ Args:
179
+ position: Position to search around
180
+ tolerance: Search tolerance in mm
181
+
182
+ Returns:
183
+ List of matching no-connect elements
184
+ """
185
+ if isinstance(position, tuple):
186
+ position = Point(position[0], position[1])
187
+
188
+ matches = []
189
+ for no_connect_element in self._no_connects:
190
+ distance = no_connect_element.position.distance_to(position)
191
+ if distance <= tolerance:
192
+ matches.append(no_connect_element)
193
+ return matches
194
+
195
+ def filter(self, predicate: Callable[[NoConnectElement], bool]) -> List[NoConnectElement]:
196
+ """
197
+ Filter no-connects by predicate function.
198
+
199
+ Args:
200
+ predicate: Function that returns True for no-connects to include
201
+
202
+ Returns:
203
+ List of no-connects matching predicate
204
+ """
205
+ return [no_connect for no_connect in self._no_connects if predicate(no_connect)]
206
+
207
+ def bulk_update(self, criteria: Callable[[NoConnectElement], bool], updates: Dict[str, Any]):
208
+ """
209
+ Update multiple no-connects matching criteria.
210
+
211
+ Args:
212
+ criteria: Function to select no-connects to update
213
+ updates: Dictionary of property updates
214
+ """
215
+ updated_count = 0
216
+ for no_connect_element in self._no_connects:
217
+ if criteria(no_connect_element):
218
+ for prop, value in updates.items():
219
+ if hasattr(no_connect_element, prop):
220
+ setattr(no_connect_element, prop, value)
221
+ updated_count += 1
222
+
223
+ if updated_count > 0:
224
+ self._mark_modified()
225
+ logger.debug(f"Bulk updated {updated_count} no-connect properties")
226
+
227
+ def clear(self):
228
+ """Remove all no-connects from collection."""
229
+ self._no_connects.clear()
230
+ self._uuid_index.clear()
231
+ self._position_index.clear()
232
+ self._mark_modified()
233
+
234
+ def _add_to_indexes(self, no_connect_element: NoConnectElement):
235
+ """Add no-connect to internal indexes."""
236
+ self._no_connects.append(no_connect_element)
237
+ self._uuid_index[no_connect_element.uuid] = no_connect_element
238
+
239
+ # Add to position index
240
+ pos_key = (no_connect_element.position.x, no_connect_element.position.y)
241
+ if pos_key not in self._position_index:
242
+ self._position_index[pos_key] = []
243
+ self._position_index[pos_key].append(no_connect_element)
244
+
245
+ def _remove_from_indexes(self, no_connect_element: NoConnectElement):
246
+ """Remove no-connect from internal indexes."""
247
+ self._no_connects.remove(no_connect_element)
248
+ del self._uuid_index[no_connect_element.uuid]
249
+
250
+ # Remove from position index
251
+ pos_key = (no_connect_element.position.x, no_connect_element.position.y)
252
+ if pos_key in self._position_index:
253
+ self._position_index[pos_key].remove(no_connect_element)
254
+ if not self._position_index[pos_key]:
255
+ del self._position_index[pos_key]
256
+
257
+ def _mark_modified(self):
258
+ """Mark collection as modified."""
259
+ self._modified = True
260
+
261
+ # Collection interface methods
262
+ def __len__(self) -> int:
263
+ """Return number of no-connects."""
264
+ return len(self._no_connects)
265
+
266
+ def __iter__(self) -> Iterator[NoConnectElement]:
267
+ """Iterate over no-connects."""
268
+ return iter(self._no_connects)
269
+
270
+ def __getitem__(self, index: int) -> NoConnectElement:
271
+ """Get no-connect by index."""
272
+ return self._no_connects[index]
273
+
274
+ def __bool__(self) -> bool:
275
+ """Return True if collection has no-connects."""
276
+ return len(self._no_connects) > 0
@@ -498,7 +498,7 @@ class SExpressionParser:
498
498
  "stroke_width": 0.0,
499
499
  "stroke_type": "default",
500
500
  "uuid": None,
501
- "wire_type": "wire" # Default to wire (vs bus)
501
+ "wire_type": "wire", # Default to wire (vs bus)
502
502
  }
503
503
 
504
504
  for elem in item[1:]:
@@ -541,7 +541,7 @@ class SExpressionParser:
541
541
  "position": {"x": 0, "y": 0},
542
542
  "diameter": 0,
543
543
  "color": (0, 0, 0, 0),
544
- "uuid": None
544
+ "uuid": None,
545
545
  }
546
546
 
547
547
  for elem in item[1:]:
@@ -563,7 +563,12 @@ class SExpressionParser:
563
563
  elif elem_type == "color":
564
564
  # Parse color: (color r g b a)
565
565
  if len(elem) >= 5:
566
- junction_data["color"] = (int(elem[1]), int(elem[2]), int(elem[3]), int(elem[4]))
566
+ junction_data["color"] = (
567
+ int(elem[1]),
568
+ int(elem[2]),
569
+ int(elem[3]),
570
+ int(elem[4]),
571
+ )
567
572
 
568
573
  elif elem_type == "uuid":
569
574
  junction_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
@@ -581,7 +586,7 @@ class SExpressionParser:
581
586
  "position": {"x": 0, "y": 0},
582
587
  "rotation": 0,
583
588
  "size": 1.27,
584
- "uuid": None
589
+ "uuid": None,
585
590
  }
586
591
 
587
592
  for elem in item[2:]: # Skip label keyword and text
@@ -624,7 +629,7 @@ class SExpressionParser:
624
629
  "rotation": 0,
625
630
  "size": 1.27,
626
631
  "justify": "left",
627
- "uuid": None
632
+ "uuid": None,
628
633
  }
629
634
 
630
635
  for elem in item[2:]: # Skip hierarchical_label keyword and text
@@ -649,7 +654,11 @@ class SExpressionParser:
649
654
  # Parse effects for font size and justification: (effects (font (size x y)) (justify left))
650
655
  for effect_elem in elem[1:]:
651
656
  if isinstance(effect_elem, list):
652
- effect_type = str(effect_elem[0]) if isinstance(effect_elem[0], sexpdata.Symbol) else None
657
+ effect_type = (
658
+ str(effect_elem[0])
659
+ if isinstance(effect_elem[0], sexpdata.Symbol)
660
+ else None
661
+ )
653
662
 
654
663
  if effect_type == "font":
655
664
  # Parse font size
@@ -671,10 +680,7 @@ class SExpressionParser:
671
680
  def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
672
681
  """Parse a no_connect symbol."""
673
682
  # Format: (no_connect (at x y) (uuid ...))
674
- no_connect_data = {
675
- "position": {"x": 0, "y": 0},
676
- "uuid": None
677
- }
683
+ no_connect_data = {"position": {"x": 0, "y": 0}, "uuid": None}
678
684
 
679
685
  for elem in item[1:]:
680
686
  if not isinstance(elem, list):
@@ -702,7 +708,7 @@ class SExpressionParser:
702
708
  "position": {"x": 0, "y": 0},
703
709
  "rotation": 0,
704
710
  "size": 1.27,
705
- "uuid": None
711
+ "uuid": None,
706
712
  }
707
713
 
708
714
  for elem in item[2:]:
@@ -750,7 +756,7 @@ class SExpressionParser:
750
756
  "font_size": 1.27,
751
757
  "justify_horizontal": "left",
752
758
  "justify_vertical": "top",
753
- "uuid": None
759
+ "uuid": None,
754
760
  }
755
761
 
756
762
  for elem in item[2:]:
@@ -772,7 +778,12 @@ class SExpressionParser:
772
778
  text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
773
779
  elif elem_type == "margins":
774
780
  if len(elem) >= 5:
775
- text_box_data["margins"] = (float(elem[1]), float(elem[2]), float(elem[3]), float(elem[4]))
781
+ text_box_data["margins"] = (
782
+ float(elem[1]),
783
+ float(elem[2]),
784
+ float(elem[3]),
785
+ float(elem[4]),
786
+ )
776
787
  elif elem_type == "stroke":
777
788
  for stroke_elem in elem[1:]:
778
789
  if isinstance(stroke_elem, list):
@@ -784,7 +795,9 @@ class SExpressionParser:
784
795
  elif elem_type == "fill":
785
796
  for fill_elem in elem[1:]:
786
797
  if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
787
- text_box_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
798
+ text_box_data["fill_type"] = (
799
+ str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
800
+ )
788
801
  elif elem_type == "effects":
789
802
  for effect_elem in elem[1:]:
790
803
  if isinstance(effect_elem, list):
@@ -823,7 +836,7 @@ class SExpressionParser:
823
836
  "filename": "sheet.kicad_sch",
824
837
  "pins": [],
825
838
  "project_name": "",
826
- "page_number": "2"
839
+ "page_number": "2",
827
840
  }
828
841
 
829
842
  for elem in item[1:]:
@@ -860,7 +873,12 @@ class SExpressionParser:
860
873
  for fill_elem in elem[1:]:
861
874
  if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
862
875
  if len(fill_elem) >= 5:
863
- sheet_data["fill_color"] = (int(fill_elem[1]), int(fill_elem[2]), int(fill_elem[3]), float(fill_elem[4]))
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
+ )
864
882
  elif elem_type == "uuid":
865
883
  sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
866
884
  elif elem_type == "property":
@@ -886,7 +904,9 @@ class SExpressionParser:
886
904
  if isinstance(path_elem, list) and str(path_elem[0]) == "path":
887
905
  for page_elem in path_elem[1:]:
888
906
  if isinstance(page_elem, list) and str(page_elem[0]) == "page":
889
- sheet_data["page_number"] = str(page_elem[1]) if len(page_elem) > 1 else "2"
907
+ sheet_data["page_number"] = (
908
+ str(page_elem[1]) if len(page_elem) > 1 else "2"
909
+ )
890
910
 
891
911
  return sheet_data
892
912
 
@@ -903,7 +923,7 @@ class SExpressionParser:
903
923
  "rotation": 0,
904
924
  "size": 1.27,
905
925
  "justify": "right",
906
- "uuid": None
926
+ "uuid": None,
907
927
  }
908
928
 
909
929
  for elem in item[3:]:
@@ -937,12 +957,7 @@ class SExpressionParser:
937
957
  def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
938
958
  """Parse a polyline graphical element."""
939
959
  # Format: (polyline (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (uuid ...))
940
- polyline_data = {
941
- "points": [],
942
- "stroke_width": 0,
943
- "stroke_type": "default",
944
- "uuid": None
945
- }
960
+ polyline_data = {"points": [], "stroke_width": 0, "stroke_type": "default", "uuid": None}
946
961
 
947
962
  for elem in item[1:]:
948
963
  if not isinstance(elem, list):
@@ -977,7 +992,7 @@ class SExpressionParser:
977
992
  "stroke_width": 0,
978
993
  "stroke_type": "default",
979
994
  "fill_type": "none",
980
- "uuid": None
995
+ "uuid": None,
981
996
  }
982
997
 
983
998
  for elem in item[1:]:
@@ -1018,7 +1033,7 @@ class SExpressionParser:
1018
1033
  "stroke_width": 0,
1019
1034
  "stroke_type": "default",
1020
1035
  "fill_type": "none",
1021
- "uuid": None
1036
+ "uuid": None,
1022
1037
  }
1023
1038
 
1024
1039
  for elem in item[1:]:
@@ -1042,7 +1057,9 @@ class SExpressionParser:
1042
1057
  elif elem_type == "fill":
1043
1058
  for fill_elem in elem[1:]:
1044
1059
  if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1045
- circle_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1060
+ circle_data["fill_type"] = (
1061
+ str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1062
+ )
1046
1063
  elif elem_type == "uuid":
1047
1064
  circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1048
1065
 
@@ -1056,7 +1073,7 @@ class SExpressionParser:
1056
1073
  "stroke_width": 0,
1057
1074
  "stroke_type": "default",
1058
1075
  "fill_type": "none",
1059
- "uuid": None
1076
+ "uuid": None,
1060
1077
  }
1061
1078
 
1062
1079
  for elem in item[1:]:
@@ -1080,7 +1097,9 @@ class SExpressionParser:
1080
1097
  elif elem_type == "fill":
1081
1098
  for fill_elem in elem[1:]:
1082
1099
  if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1083
- bezier_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1100
+ bezier_data["fill_type"] = (
1101
+ str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1102
+ )
1084
1103
  elif elem_type == "uuid":
1085
1104
  bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1086
1105
 
@@ -1111,7 +1130,9 @@ class SExpressionParser:
1111
1130
  elif elem_type == "fill":
1112
1131
  for fill_elem in elem[1:]:
1113
1132
  if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1114
- rectangle["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1133
+ rectangle["fill_type"] = (
1134
+ str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1135
+ )
1115
1136
  elif elem_type == "uuid" and len(elem) >= 2:
1116
1137
  rectangle["uuid"] = str(elem[1])
1117
1138
 
@@ -1120,12 +1141,7 @@ class SExpressionParser:
1120
1141
  def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1121
1142
  """Parse an image element."""
1122
1143
  # Format: (image (at x y) (uuid "...") (data "base64..."))
1123
- image = {
1124
- "position": {"x": 0, "y": 0},
1125
- "data": "",
1126
- "scale": 1.0,
1127
- "uuid": None
1128
- }
1144
+ image = {"position": {"x": 0, "y": 0}, "data": "", "scale": 1.0, "uuid": None}
1129
1145
 
1130
1146
  for elem in item[1:]:
1131
1147
  if not isinstance(elem, list):
@@ -1264,14 +1280,24 @@ class SExpressionParser:
1264
1280
  if hierarchy_path:
1265
1281
  # Use the full hierarchical path (includes root + all sheet symbols)
1266
1282
  instance_path = hierarchy_path
1267
- logger.debug(f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}")
1283
+ logger.debug(
1284
+ f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
1285
+ )
1268
1286
  else:
1269
1287
  # Fallback: use root_uuid or schematic_uuid for flat designs
1270
- root_uuid = symbol_data.get("properties", {}).get("root_uuid") or schematic_uuid or str(uuid.uuid4())
1288
+ root_uuid = (
1289
+ symbol_data.get("properties", {}).get("root_uuid")
1290
+ or schematic_uuid
1291
+ or str(uuid.uuid4())
1292
+ )
1271
1293
  instance_path = f"/{root_uuid}"
1272
- logger.debug(f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}")
1294
+ logger.debug(
1295
+ f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
1296
+ )
1273
1297
 
1274
- logger.debug(f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}")
1298
+ logger.debug(
1299
+ f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
1300
+ )
1275
1301
  logger.debug(f"🔧 Using project name: '{project_name}'")
1276
1302
 
1277
1303
  sexp.append(
@@ -1959,12 +1985,20 @@ class SExpressionParser:
1959
1985
  stroke_sexp = [sexpdata.Symbol("stroke")]
1960
1986
  stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1961
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])
1962
1992
  sexp.append(stroke_sexp)
1963
1993
 
1964
1994
  # Add fill
1965
1995
  fill_type = rectangle_data.get("fill_type", "none")
1966
1996
  fill_sexp = [sexpdata.Symbol("fill")]
1967
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])
1968
2002
  sexp.append(fill_sexp)
1969
2003
 
1970
2004
  # Add UUID
@@ -2000,7 +2034,7 @@ class SExpressionParser:
2000
2034
  # Split the data into 76-character chunks
2001
2035
  chunk_size = 76
2002
2036
  for i in range(0, len(data), chunk_size):
2003
- data_sexp.append(data[i:i+chunk_size])
2037
+ data_sexp.append(data[i : i + chunk_size])
2004
2038
  sexp.append(data_sexp)
2005
2039
 
2006
2040
  return sexp