kicad-sch-api 0.3.5__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 +2 -2
  2. kicad_sch_api/collections/base.py +5 -7
  3. kicad_sch_api/collections/components.py +24 -12
  4. kicad_sch_api/collections/junctions.py +31 -43
  5. kicad_sch_api/collections/labels.py +19 -27
  6. kicad_sch_api/collections/wires.py +17 -18
  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 +2 -2
  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 +1 -1
  20. kicad_sch_api/core/no_connects.py +5 -3
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +779 -1083
  23. kicad_sch_api/core/texts.py +1 -1
  24. kicad_sch_api/core/types.py +1 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +40 -21
  27. kicad_sch_api/interfaces/__init__.py +1 -1
  28. kicad_sch_api/interfaces/parser.py +1 -1
  29. kicad_sch_api/interfaces/repository.py +1 -1
  30. kicad_sch_api/interfaces/resolver.py +1 -1
  31. kicad_sch_api/parsers/__init__.py +2 -2
  32. kicad_sch_api/parsers/base.py +7 -10
  33. kicad_sch_api/parsers/label_parser.py +7 -7
  34. kicad_sch_api/parsers/registry.py +4 -2
  35. kicad_sch_api/parsers/symbol_parser.py +5 -10
  36. kicad_sch_api/parsers/wire_parser.py +2 -2
  37. kicad_sch_api/symbols/__init__.py +1 -1
  38. kicad_sch_api/symbols/cache.py +9 -12
  39. kicad_sch_api/symbols/resolver.py +20 -26
  40. kicad_sch_api/symbols/validators.py +188 -137
  41. {kicad_sch_api-0.3.5.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.5.dist-info/RECORD +0 -58
  44. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,346 @@
1
+ """
2
+ Wire Manager for KiCAD schematic wire operations.
3
+
4
+ Handles wire creation, removal, pin connections, and auto-routing functionality
5
+ while managing component pin position calculations and connectivity analysis.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ from ...library.cache import get_symbol_cache
13
+ from ..types import Point, Wire, WireType
14
+ from ..wires import WireCollection
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class WireManager:
20
+ """
21
+ Manages wire operations and pin connectivity in KiCAD schematics.
22
+
23
+ Responsible for:
24
+ - Wire creation and removal
25
+ - Pin position calculations
26
+ - Auto-routing between pins
27
+ - Connectivity analysis
28
+ - Wire-to-pin connections
29
+ """
30
+
31
+ def __init__(
32
+ self, schematic_data: Dict[str, Any], wire_collection: WireCollection, component_collection
33
+ ):
34
+ """
35
+ Initialize WireManager.
36
+
37
+ Args:
38
+ schematic_data: Reference to schematic data
39
+ wire_collection: Wire collection for management
40
+ component_collection: Component collection for pin lookups
41
+ """
42
+ self._data = schematic_data
43
+ self._wires = wire_collection
44
+ self._components = component_collection
45
+ self._symbol_cache = get_symbol_cache()
46
+
47
+ def add_wire(
48
+ self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
49
+ ) -> str:
50
+ """
51
+ Add a wire connection.
52
+
53
+ Args:
54
+ start: Start point
55
+ end: End point
56
+
57
+ Returns:
58
+ UUID of created wire
59
+ """
60
+ if isinstance(start, tuple):
61
+ start = Point(start[0], start[1])
62
+ if isinstance(end, tuple):
63
+ end = Point(end[0], end[1])
64
+
65
+ # Use the wire collection to add the wire
66
+ wire_uuid = self._wires.add(start=start, end=end)
67
+
68
+ logger.debug(f"Added wire: {start} -> {end}")
69
+ return wire_uuid
70
+
71
+ def remove_wire(self, wire_uuid: str) -> bool:
72
+ """
73
+ Remove wire by UUID.
74
+
75
+ Args:
76
+ wire_uuid: UUID of wire to remove
77
+
78
+ Returns:
79
+ True if wire was removed, False if not found
80
+ """
81
+ # Remove from wire collection
82
+ removed_from_collection = self._wires.remove(wire_uuid)
83
+
84
+ # Also remove from data structure for consistency
85
+ wires = self._data.get("wires", [])
86
+ removed_from_data = False
87
+ for i, wire in enumerate(wires):
88
+ if wire.get("uuid") == wire_uuid:
89
+ del wires[i]
90
+ removed_from_data = True
91
+ break
92
+
93
+ success = removed_from_collection or removed_from_data
94
+ if success:
95
+ logger.debug(f"Removed wire: {wire_uuid}")
96
+
97
+ return success
98
+
99
+ def add_wire_to_pin(
100
+ self, start: Union[Point, Tuple[float, float]], component_ref: str, pin_number: str
101
+ ) -> str:
102
+ """
103
+ Add wire from a point to a component pin.
104
+
105
+ Args:
106
+ start: Starting point
107
+ component_ref: Component reference (e.g., "R1")
108
+ pin_number: Pin number on component
109
+
110
+ Returns:
111
+ UUID of created wire
112
+
113
+ Raises:
114
+ ValueError: If component or pin not found
115
+ """
116
+ pin_position = self.get_component_pin_position(component_ref, pin_number)
117
+ if pin_position is None:
118
+ raise ValueError(f"Pin {pin_number} not found on component {component_ref}")
119
+
120
+ return self.add_wire(start, pin_position)
121
+
122
+ def add_wire_between_pins(
123
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
124
+ ) -> str:
125
+ """
126
+ Add wire between two component pins.
127
+
128
+ Args:
129
+ component1_ref: First component reference
130
+ pin1_number: First component pin number
131
+ component2_ref: Second component reference
132
+ pin2_number: Second component pin number
133
+
134
+ Returns:
135
+ UUID of created wire
136
+
137
+ Raises:
138
+ ValueError: If components or pins not found
139
+ """
140
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
141
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
142
+
143
+ if pin1_pos is None:
144
+ raise ValueError(f"Pin {pin1_number} not found on component {component1_ref}")
145
+ if pin2_pos is None:
146
+ raise ValueError(f"Pin {pin2_number} not found on component {component2_ref}")
147
+
148
+ return self.add_wire(pin1_pos, pin2_pos)
149
+
150
+ def get_component_pin_position(self, component_ref: str, pin_number: str) -> Optional[Point]:
151
+ """
152
+ Get absolute position of a component pin.
153
+
154
+ This consolidates the duplicate implementations in the original schematic class.
155
+
156
+ Args:
157
+ component_ref: Component reference (e.g., "R1")
158
+ pin_number: Pin number
159
+
160
+ Returns:
161
+ Absolute pin position or None if not found
162
+ """
163
+ # Find component
164
+ component = self._components.get(component_ref)
165
+ if not component:
166
+ logger.warning(f"Component not found: {component_ref}")
167
+ return None
168
+
169
+ # Get symbol definition from cache
170
+ symbol_def = self._symbol_cache.get_symbol(component.lib_id)
171
+ if not symbol_def:
172
+ logger.warning(f"Symbol definition not found: {component.lib_id}")
173
+ return None
174
+
175
+ # Find pin in symbol definition
176
+ for pin in symbol_def.pins:
177
+ if pin.number == pin_number:
178
+ # Calculate absolute position
179
+ # Apply component rotation/mirroring if needed (simplified for now)
180
+ absolute_x = component.position.x + pin.position.x
181
+ absolute_y = component.position.y + pin.position.y
182
+
183
+ return Point(absolute_x, absolute_y)
184
+
185
+ logger.warning(f"Pin {pin_number} not found on component {component_ref}")
186
+ return None
187
+
188
+ def list_component_pins(self, component_ref: str) -> List[Tuple[str, Point]]:
189
+ """
190
+ List all pins and their positions for a component.
191
+
192
+ Args:
193
+ component_ref: Component reference
194
+
195
+ Returns:
196
+ List of (pin_number, absolute_position) tuples
197
+ """
198
+ pins = []
199
+
200
+ # Find component
201
+ component = self._components.get(component_ref)
202
+ if not component:
203
+ return pins
204
+
205
+ # Get symbol definition
206
+ symbol_def = self._symbol_cache.get_symbol(component.lib_id)
207
+ if not symbol_def:
208
+ return pins
209
+
210
+ # Calculate absolute positions for all pins
211
+ for pin in symbol_def.pins:
212
+ absolute_x = component.position.x + pin.position.x
213
+ absolute_y = component.position.y + pin.position.y
214
+ pins.append((pin.number, Point(absolute_x, absolute_y)))
215
+
216
+ return pins
217
+
218
+ def auto_route_pins(
219
+ self,
220
+ component1_ref: str,
221
+ pin1_number: str,
222
+ component2_ref: str,
223
+ pin2_number: str,
224
+ routing_strategy: str = "direct",
225
+ ) -> List[str]:
226
+ """
227
+ Auto-route between two pins with different strategies.
228
+
229
+ Args:
230
+ component1_ref: First component reference
231
+ pin1_number: First component pin number
232
+ component2_ref: Second component reference
233
+ pin2_number: Second component pin number
234
+ routing_strategy: "direct" or "manhattan"
235
+
236
+ Returns:
237
+ List of wire UUIDs created
238
+
239
+ Raises:
240
+ ValueError: If components or pins not found
241
+ """
242
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
243
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
244
+
245
+ if pin1_pos is None:
246
+ raise ValueError(f"Pin {pin1_number} not found on component {component1_ref}")
247
+ if pin2_pos is None:
248
+ raise ValueError(f"Pin {pin2_number} not found on component {component2_ref}")
249
+
250
+ wire_uuids = []
251
+
252
+ if routing_strategy == "direct":
253
+ # Direct wire between pins
254
+ wire_uuid = self.add_wire(pin1_pos, pin2_pos)
255
+ wire_uuids.append(wire_uuid)
256
+
257
+ elif routing_strategy == "manhattan":
258
+ # Manhattan routing (L-shaped path)
259
+ # Route horizontally first, then vertically
260
+ intermediate_point = Point(pin2_pos.x, pin1_pos.y)
261
+
262
+ # Only add intermediate wire if it has length
263
+ if abs(pin1_pos.x - pin2_pos.x) > 0.1: # Minimum wire length
264
+ wire1_uuid = self.add_wire(pin1_pos, intermediate_point)
265
+ wire_uuids.append(wire1_uuid)
266
+
267
+ if abs(pin1_pos.y - pin2_pos.y) > 0.1: # Minimum wire length
268
+ wire2_uuid = self.add_wire(intermediate_point, pin2_pos)
269
+ wire_uuids.append(wire2_uuid)
270
+
271
+ else:
272
+ raise ValueError(f"Unknown routing strategy: {routing_strategy}")
273
+
274
+ logger.info(
275
+ f"Auto-routed {component1_ref}:{pin1_number} to {component2_ref}:{pin2_number} using {routing_strategy}"
276
+ )
277
+ return wire_uuids
278
+
279
+ def are_pins_connected(
280
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
281
+ ) -> bool:
282
+ """
283
+ Check if two pins are connected via wires.
284
+
285
+ Args:
286
+ component1_ref: First component reference
287
+ pin1_number: First component pin number
288
+ component2_ref: Second component reference
289
+ pin2_number: Second component pin number
290
+
291
+ Returns:
292
+ True if pins are connected, False otherwise
293
+ """
294
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
295
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
296
+
297
+ if pin1_pos is None or pin2_pos is None:
298
+ return False
299
+
300
+ # Check for direct wire connection
301
+ for wire in self._wires:
302
+ if (wire.start == pin1_pos and wire.end == pin2_pos) or (
303
+ wire.start == pin2_pos and wire.end == pin1_pos
304
+ ):
305
+ return True
306
+
307
+ # TODO: Implement more sophisticated connectivity analysis
308
+ # This would involve following wire networks through junctions
309
+ return False
310
+
311
+ def connect_pins_with_wire(
312
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
313
+ ) -> str:
314
+ """
315
+ Legacy alias for add_wire_between_pins.
316
+
317
+ Args:
318
+ component1_ref: First component reference
319
+ pin1_number: First component pin number
320
+ component2_ref: Second component reference
321
+ pin2_number: Second component pin number
322
+
323
+ Returns:
324
+ UUID of created wire
325
+ """
326
+ return self.add_wire_between_pins(component1_ref, pin1_number, component2_ref, pin2_number)
327
+
328
+ def get_wire_statistics(self) -> Dict[str, Any]:
329
+ """
330
+ Get statistics about wires in the schematic.
331
+
332
+ Returns:
333
+ Dictionary with wire statistics
334
+ """
335
+ total_wires = len(self._wires)
336
+ total_length = sum(wire.start.distance_to(wire.end) for wire in self._wires)
337
+
338
+ return {
339
+ "total_wires": total_wires,
340
+ "total_length": total_length,
341
+ "average_length": total_length / total_wires if total_wires > 0 else 0,
342
+ "wire_types": {
343
+ "normal": len([w for w in self._wires if w.wire_type == WireType.WIRE]),
344
+ "bus": len([w for w in self._wires if w.wire_type == WireType.BUS]),
345
+ },
346
+ }
@@ -307,4 +307,4 @@ class NetCollection:
307
307
 
308
308
  def __bool__(self) -> bool:
309
309
  """Return True if collection has nets."""
310
- return len(self._nets) > 0
310
+ return len(self._nets) > 0
@@ -10,7 +10,7 @@ import uuid
10
10
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
- from .types import Point, NoConnect
13
+ from .types import NoConnect, Point
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -169,7 +169,9 @@ class NoConnectCollection:
169
169
  logger.debug(f"Removed no-connect: {no_connect_element}")
170
170
  return True
171
171
 
172
- def find_at_position(self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.1) -> List[NoConnectElement]:
172
+ def find_at_position(
173
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.1
174
+ ) -> List[NoConnectElement]:
173
175
  """
174
176
  Find no-connects at or near a position.
175
177
 
@@ -271,4 +273,4 @@ class NoConnectCollection:
271
273
 
272
274
  def __bool__(self) -> bool:
273
275
  """Return True if collection has no-connects."""
274
- return len(self._no_connects) > 0
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