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.
- kicad_sch_api/collections/__init__.py +2 -2
- kicad_sch_api/collections/base.py +5 -7
- kicad_sch_api/collections/components.py +24 -12
- kicad_sch_api/collections/junctions.py +31 -43
- kicad_sch_api/collections/labels.py +19 -27
- kicad_sch_api/collections/wires.py +17 -18
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +2 -2
- 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 +1 -1
- kicad_sch_api/core/no_connects.py +5 -3
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +779 -1083
- kicad_sch_api/core/texts.py +1 -1
- kicad_sch_api/core/types.py +1 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +40 -21
- kicad_sch_api/interfaces/__init__.py +1 -1
- kicad_sch_api/interfaces/parser.py +1 -1
- kicad_sch_api/interfaces/repository.py +1 -1
- kicad_sch_api/interfaces/resolver.py +1 -1
- kicad_sch_api/parsers/__init__.py +2 -2
- kicad_sch_api/parsers/base.py +7 -10
- kicad_sch_api/parsers/label_parser.py +7 -7
- kicad_sch_api/parsers/registry.py +4 -2
- kicad_sch_api/parsers/symbol_parser.py +5 -10
- kicad_sch_api/parsers/wire_parser.py +2 -2
- kicad_sch_api/symbols/__init__.py +1 -1
- kicad_sch_api/symbols/cache.py +9 -12
- kicad_sch_api/symbols/resolver.py +20 -26
- kicad_sch_api/symbols/validators.py +188 -137
- {kicad_sch_api-0.3.5.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.5.dist-info/RECORD +0 -58
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
}
|
kicad_sch_api/core/nets.py
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
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
|