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.
- kicad_sch_api/collections/__init__.py +21 -0
- kicad_sch_api/collections/base.py +294 -0
- kicad_sch_api/collections/components.py +434 -0
- kicad_sch_api/collections/junctions.py +366 -0
- kicad_sch_api/collections/labels.py +404 -0
- kicad_sch_api/collections/wires.py +406 -0
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +348 -0
- kicad_sch_api/core/managers/__init__.py +26 -0
- kicad_sch_api/core/managers/file_io.py +243 -0
- kicad_sch_api/core/managers/format_sync.py +501 -0
- kicad_sch_api/core/managers/graphics.py +579 -0
- kicad_sch_api/core/managers/metadata.py +268 -0
- kicad_sch_api/core/managers/sheet.py +454 -0
- kicad_sch_api/core/managers/text_elements.py +536 -0
- kicad_sch_api/core/managers/validation.py +474 -0
- kicad_sch_api/core/managers/wire.py +346 -0
- kicad_sch_api/core/nets.py +310 -0
- kicad_sch_api/core/no_connects.py +276 -0
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +904 -1074
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +13 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +56 -43
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- kicad_sch_api/parsers/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/symbol_parser.py +222 -0
- kicad_sch_api/parsers/wire_parser.py +99 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
- kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
- kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
kicad_sch_api/core/parser.py
CHANGED
|
@@ -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"] = (
|
|
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 =
|
|
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"] = (
|
|
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"] =
|
|
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"] = (
|
|
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"] =
|
|
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"] =
|
|
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"] =
|
|
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"] =
|
|
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(
|
|
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 =
|
|
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(
|
|
1294
|
+
logger.debug(
|
|
1295
|
+
f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
|
|
1296
|
+
)
|
|
1273
1297
|
|
|
1274
|
-
logger.debug(
|
|
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
|