kicad-sch-api 0.4.1__py3-none-any.whl → 0.5.1__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.
- kicad_sch_api/__init__.py +67 -2
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/collections/__init__.py +23 -8
- kicad_sch_api/collections/base.py +369 -59
- kicad_sch_api/collections/components.py +1376 -187
- kicad_sch_api/collections/junctions.py +129 -289
- kicad_sch_api/collections/labels.py +391 -287
- kicad_sch_api/collections/wires.py +202 -316
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/component_bounds.py +34 -12
- kicad_sch_api/core/components.py +146 -7
- kicad_sch_api/core/config.py +25 -12
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/element_factory.py +3 -1
- kicad_sch_api/core/formatter.py +24 -7
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/managers/__init__.py +4 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +3 -1
- kicad_sch_api/core/managers/format_sync.py +3 -2
- kicad_sch_api/core/managers/graphics.py +3 -2
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +4 -2
- kicad_sch_api/core/managers/sheet.py +52 -14
- kicad_sch_api/core/managers/text_elements.py +3 -2
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +112 -54
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +343 -29
- kicad_sch_api/core/types.py +79 -7
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +15 -3
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/parsers/elements/label_parser.py +30 -8
- kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
- kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
- kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Enhanced component management with IndexRegistry integration.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
This module provides the Component wrapper and ComponentCollection using the new
|
|
5
|
+
BaseCollection infrastructure with centralized index management, lazy rebuilding,
|
|
6
|
+
and batch mode support.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
9
10
|
import uuid
|
|
10
|
-
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
11
12
|
|
|
12
|
-
from ..core.
|
|
13
|
-
from ..
|
|
14
|
-
from ..
|
|
15
|
-
from .
|
|
13
|
+
from ..core.ic_manager import ICManager
|
|
14
|
+
from ..core.types import Point, PinInfo, SchematicPin, SchematicSymbol
|
|
15
|
+
from ..library.cache import SymbolDefinition, get_symbol_cache
|
|
16
|
+
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
17
|
+
from .base import BaseCollection, IndexSpec, ValidationLevel
|
|
16
18
|
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class Component:
|
|
21
23
|
"""
|
|
22
|
-
Enhanced wrapper for schematic components
|
|
24
|
+
Enhanced wrapper for schematic components.
|
|
23
25
|
|
|
24
26
|
Provides intuitive access to component properties, pins, and operations
|
|
25
|
-
while maintaining exact format preservation
|
|
27
|
+
while maintaining exact format preservation. All property modifications
|
|
28
|
+
automatically notify the parent collection for tracking.
|
|
26
29
|
"""
|
|
27
30
|
|
|
28
31
|
def __init__(self, symbol_data: SchematicSymbol, parent_collection: "ComponentCollection"):
|
|
@@ -31,30 +34,39 @@ class Component:
|
|
|
31
34
|
|
|
32
35
|
Args:
|
|
33
36
|
symbol_data: Underlying symbol data
|
|
34
|
-
parent_collection: Parent collection for
|
|
37
|
+
parent_collection: Parent collection for modification tracking
|
|
35
38
|
"""
|
|
36
39
|
self._data = symbol_data
|
|
37
40
|
self._collection = parent_collection
|
|
38
41
|
self._validator = SchematicValidator()
|
|
39
42
|
|
|
43
|
+
# Core properties with validation
|
|
40
44
|
@property
|
|
41
45
|
def uuid(self) -> str:
|
|
42
|
-
"""Component UUID."""
|
|
46
|
+
"""Component UUID (read-only)."""
|
|
43
47
|
return self._data.uuid
|
|
44
48
|
|
|
45
49
|
@property
|
|
46
50
|
def reference(self) -> str:
|
|
47
|
-
"""Component reference (e.g., 'R1')."""
|
|
51
|
+
"""Component reference designator (e.g., 'R1', 'U2')."""
|
|
48
52
|
return self._data.reference
|
|
49
53
|
|
|
50
54
|
@reference.setter
|
|
51
55
|
def reference(self, value: str):
|
|
52
|
-
"""
|
|
56
|
+
"""
|
|
57
|
+
Set component reference with validation and duplicate checking.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
value: New reference designator
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValidationError: If reference format is invalid or already exists
|
|
64
|
+
"""
|
|
53
65
|
if not self._validator.validate_reference(value):
|
|
54
66
|
raise ValidationError(f"Invalid reference format: {value}")
|
|
55
67
|
|
|
56
68
|
# Check for duplicates in parent collection
|
|
57
|
-
if self._collection.
|
|
69
|
+
if self._collection.get(value) is not None:
|
|
58
70
|
raise ValidationError(f"Reference {value} already exists")
|
|
59
71
|
|
|
60
72
|
old_ref = self._data.reference
|
|
@@ -65,28 +77,41 @@ class Component:
|
|
|
65
77
|
|
|
66
78
|
@property
|
|
67
79
|
def value(self) -> str:
|
|
68
|
-
"""Component value (e.g., '10k')."""
|
|
80
|
+
"""Component value (e.g., '10k', '100nF')."""
|
|
69
81
|
return self._data.value
|
|
70
82
|
|
|
71
83
|
@value.setter
|
|
72
84
|
def value(self, value: str):
|
|
73
85
|
"""Set component value."""
|
|
86
|
+
old_value = self._data.value
|
|
74
87
|
self._data.value = value
|
|
88
|
+
self._collection._update_value_index(self, old_value, value)
|
|
75
89
|
self._collection._mark_modified()
|
|
76
90
|
|
|
77
91
|
@property
|
|
78
|
-
def
|
|
79
|
-
"""Component
|
|
80
|
-
return self._data.
|
|
92
|
+
def footprint(self) -> Optional[str]:
|
|
93
|
+
"""Component footprint (e.g., 'Resistor_SMD:R_0603_1608Metric')."""
|
|
94
|
+
return self._data.footprint
|
|
95
|
+
|
|
96
|
+
@footprint.setter
|
|
97
|
+
def footprint(self, value: Optional[str]):
|
|
98
|
+
"""Set component footprint."""
|
|
99
|
+
self._data.footprint = value
|
|
100
|
+
self._collection._mark_modified()
|
|
81
101
|
|
|
82
102
|
@property
|
|
83
103
|
def position(self) -> Point:
|
|
84
|
-
"""Component position."""
|
|
104
|
+
"""Component position in schematic (mm)."""
|
|
85
105
|
return self._data.position
|
|
86
106
|
|
|
87
107
|
@position.setter
|
|
88
108
|
def position(self, value: Union[Point, Tuple[float, float]]):
|
|
89
|
-
"""
|
|
109
|
+
"""
|
|
110
|
+
Set component position.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
value: Position as Point or (x, y) tuple
|
|
114
|
+
"""
|
|
90
115
|
if isinstance(value, tuple):
|
|
91
116
|
value = Point(value[0], value[1])
|
|
92
117
|
self._data.position = value
|
|
@@ -94,106 +119,425 @@ class Component:
|
|
|
94
119
|
|
|
95
120
|
@property
|
|
96
121
|
def rotation(self) -> float:
|
|
97
|
-
"""Component rotation in degrees."""
|
|
122
|
+
"""Component rotation in degrees (0, 90, 180, or 270)."""
|
|
98
123
|
return self._data.rotation
|
|
99
124
|
|
|
100
125
|
@rotation.setter
|
|
101
126
|
def rotation(self, value: float):
|
|
102
|
-
"""
|
|
103
|
-
|
|
127
|
+
"""
|
|
128
|
+
Set component rotation.
|
|
129
|
+
|
|
130
|
+
KiCad only supports 0, 90, 180, or 270 degree rotations.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
value: Rotation angle in degrees
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: If rotation is not 0, 90, 180, or 270
|
|
137
|
+
"""
|
|
138
|
+
# Normalize rotation to 0-360 range
|
|
139
|
+
normalized = float(value) % 360
|
|
140
|
+
|
|
141
|
+
# KiCad only accepts specific angles
|
|
142
|
+
VALID_ROTATIONS = {0, 90, 180, 270}
|
|
143
|
+
if normalized not in VALID_ROTATIONS:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Component rotation must be 0, 90, 180, or 270 degrees. "
|
|
146
|
+
f"Got {value}° (normalized to {normalized}°). "
|
|
147
|
+
f"KiCad does not support arbitrary rotation angles."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self._data.rotation = normalized
|
|
104
151
|
self._collection._mark_modified()
|
|
105
152
|
|
|
106
153
|
@property
|
|
107
|
-
def
|
|
108
|
-
"""
|
|
109
|
-
return self._data.
|
|
154
|
+
def lib_id(self) -> str:
|
|
155
|
+
"""Library identifier (e.g., 'Device:R')."""
|
|
156
|
+
return self._data.lib_id
|
|
110
157
|
|
|
111
|
-
@
|
|
112
|
-
def
|
|
113
|
-
"""
|
|
114
|
-
self._data.
|
|
115
|
-
|
|
158
|
+
@property
|
|
159
|
+
def library(self) -> str:
|
|
160
|
+
"""Library name (e.g., 'Device' from 'Device:R')."""
|
|
161
|
+
return self._data.library
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def symbol_name(self) -> str:
|
|
165
|
+
"""Symbol name within library (e.g., 'R' from 'Device:R')."""
|
|
166
|
+
return self._data.symbol_name
|
|
167
|
+
|
|
168
|
+
# Properties dictionary
|
|
169
|
+
@property
|
|
170
|
+
def properties(self) -> Dict[str, str]:
|
|
171
|
+
"""Dictionary of all component properties."""
|
|
172
|
+
return self._data.properties
|
|
173
|
+
|
|
174
|
+
def get_property(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
175
|
+
"""
|
|
176
|
+
Get property value by name.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
name: Property name
|
|
180
|
+
default: Default value if property doesn't exist
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Property value or default
|
|
184
|
+
"""
|
|
185
|
+
return self._data.properties.get(name, default)
|
|
186
|
+
|
|
187
|
+
def set_property(self, name: str, value: str):
|
|
188
|
+
"""
|
|
189
|
+
Set property value with validation.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
name: Property name
|
|
193
|
+
value: Property value
|
|
116
194
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
195
|
+
Raises:
|
|
196
|
+
ValidationError: If name or value are not strings
|
|
197
|
+
"""
|
|
198
|
+
if not isinstance(name, str) or not isinstance(value, str):
|
|
199
|
+
raise ValidationError("Property name and value must be strings")
|
|
120
200
|
|
|
121
|
-
def set_property(self, name: str, value: str) -> None:
|
|
122
|
-
"""Set component property value."""
|
|
123
201
|
self._data.properties[name] = value
|
|
124
202
|
self._collection._mark_modified()
|
|
203
|
+
logger.debug(f"Set property {self.reference}.{name} = {value}")
|
|
204
|
+
|
|
205
|
+
def remove_property(self, name: str) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Remove property by name.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
name: Property name to remove
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if property was removed, False if it didn't exist
|
|
214
|
+
"""
|
|
215
|
+
if name in self._data.properties:
|
|
216
|
+
del self._data.properties[name]
|
|
217
|
+
self._collection._mark_modified()
|
|
218
|
+
return True
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
# Pin access
|
|
222
|
+
@property
|
|
223
|
+
def pins(self) -> List[SchematicPin]:
|
|
224
|
+
"""List of component pins."""
|
|
225
|
+
return self._data.pins
|
|
226
|
+
|
|
227
|
+
def get_pin(self, pin_number: str) -> Optional[SchematicPin]:
|
|
228
|
+
"""
|
|
229
|
+
Get pin by number.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
pin_number: Pin number to find
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
SchematicPin if found, None otherwise
|
|
236
|
+
"""
|
|
237
|
+
return self._data.get_pin(pin_number)
|
|
238
|
+
|
|
239
|
+
def get_pin_position(self, pin_number: str) -> Optional[Point]:
|
|
240
|
+
"""
|
|
241
|
+
Get absolute position of a pin.
|
|
242
|
+
|
|
243
|
+
Calculates the pin position accounting for component position,
|
|
244
|
+
rotation, and mirroring.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
pin_number: Pin number to find position for
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Absolute pin position in schematic coordinates, or None if pin not found
|
|
251
|
+
"""
|
|
252
|
+
return self._data.get_pin_position(pin_number)
|
|
253
|
+
|
|
254
|
+
# Component state flags
|
|
255
|
+
@property
|
|
256
|
+
def in_bom(self) -> bool:
|
|
257
|
+
"""Whether component appears in bill of materials."""
|
|
258
|
+
return self._data.in_bom
|
|
259
|
+
|
|
260
|
+
@in_bom.setter
|
|
261
|
+
def in_bom(self, value: bool):
|
|
262
|
+
"""Set BOM inclusion flag."""
|
|
263
|
+
self._data.in_bom = bool(value)
|
|
264
|
+
self._collection._mark_modified()
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def on_board(self) -> bool:
|
|
268
|
+
"""Whether component appears on PCB."""
|
|
269
|
+
return self._data.on_board
|
|
270
|
+
|
|
271
|
+
@on_board.setter
|
|
272
|
+
def on_board(self, value: bool):
|
|
273
|
+
"""Set board inclusion flag."""
|
|
274
|
+
self._data.on_board = bool(value)
|
|
275
|
+
self._collection._mark_modified()
|
|
276
|
+
|
|
277
|
+
# Utility methods
|
|
278
|
+
def move(self, x: float, y: float):
|
|
279
|
+
"""
|
|
280
|
+
Move component to absolute position.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
x: X coordinate in mm
|
|
284
|
+
y: Y coordinate in mm
|
|
285
|
+
"""
|
|
286
|
+
self.position = Point(x, y)
|
|
287
|
+
|
|
288
|
+
def translate(self, dx: float, dy: float):
|
|
289
|
+
"""
|
|
290
|
+
Translate component by offset.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
dx: X offset in mm
|
|
294
|
+
dy: Y offset in mm
|
|
295
|
+
"""
|
|
296
|
+
current = self.position
|
|
297
|
+
self.position = Point(current.x + dx, current.y + dy)
|
|
298
|
+
|
|
299
|
+
def rotate(self, angle: float):
|
|
300
|
+
"""
|
|
301
|
+
Rotate component by angle (cumulative).
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
angle: Rotation angle in degrees (will be normalized to 0/90/180/270)
|
|
305
|
+
"""
|
|
306
|
+
self.rotation = (self.rotation + angle) % 360
|
|
307
|
+
|
|
308
|
+
def align_pin(
|
|
309
|
+
self, pin_number: str, target_position: Union[Point, Tuple[float, float]]
|
|
310
|
+
) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Move component so that the specified pin is at the target position.
|
|
313
|
+
|
|
314
|
+
This adjusts the component's position to align a specific pin with the
|
|
315
|
+
target coordinates while maintaining the component's current rotation.
|
|
316
|
+
Useful for aligning existing components in horizontal signal flows.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
pin_number: Pin number to align (e.g., "1", "2")
|
|
320
|
+
target_position: Desired position for the pin (Point or (x, y) tuple)
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
ValueError: If pin_number doesn't exist in the component
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
# Move resistor so pin 2 is at (150, 100)
|
|
327
|
+
r1 = sch.components.get("R1")
|
|
328
|
+
r1.align_pin("2", (150, 100))
|
|
329
|
+
|
|
330
|
+
# Align capacitor pin 1 on same horizontal line
|
|
331
|
+
c1 = sch.components.get("C1")
|
|
332
|
+
c1.align_pin("1", (200, 100)) # Same Y as resistor pin 2
|
|
333
|
+
"""
|
|
334
|
+
from ..core.geometry import calculate_position_for_pin
|
|
335
|
+
|
|
336
|
+
# Get symbol definition to find the pin's local position
|
|
337
|
+
symbol_def = self.get_symbol_definition()
|
|
338
|
+
if not symbol_def:
|
|
339
|
+
raise ValueError(f"Symbol definition not found for {self.reference} ({self.lib_id})")
|
|
340
|
+
|
|
341
|
+
# Find the pin in the symbol definition
|
|
342
|
+
pin_def = None
|
|
343
|
+
for pin in symbol_def.pins:
|
|
344
|
+
if pin.number == pin_number:
|
|
345
|
+
pin_def = pin
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
if not pin_def:
|
|
349
|
+
available_pins = [p.number for p in symbol_def.pins]
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"Pin '{pin_number}' not found in component {self.reference} ({self.lib_id}). "
|
|
352
|
+
f"Available pins: {', '.join(available_pins)}"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
logger.debug(
|
|
356
|
+
f"Aligning {self.reference} pin {pin_number} "
|
|
357
|
+
f"(local position: {pin_def.position}) to target {target_position}"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Calculate new component position
|
|
361
|
+
new_position = calculate_position_for_pin(
|
|
362
|
+
pin_local_position=pin_def.position,
|
|
363
|
+
desired_pin_position=target_position,
|
|
364
|
+
rotation=self.rotation,
|
|
365
|
+
mirror=None, # TODO: Add mirror support when needed
|
|
366
|
+
grid_size=1.27,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
old_position = self.position
|
|
370
|
+
self.position = new_position
|
|
371
|
+
|
|
372
|
+
logger.info(
|
|
373
|
+
f"Aligned {self.reference} pin {pin_number} to {target_position}: "
|
|
374
|
+
f"moved from {old_position} to {new_position}"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def copy_properties_from(self, other: "Component"):
|
|
378
|
+
"""
|
|
379
|
+
Copy all properties from another component.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
other: Component to copy properties from
|
|
383
|
+
"""
|
|
384
|
+
for name, value in other.properties.items():
|
|
385
|
+
self.set_property(name, value)
|
|
386
|
+
|
|
387
|
+
def get_symbol_definition(self) -> Optional[SymbolDefinition]:
|
|
388
|
+
"""
|
|
389
|
+
Get the symbol definition from library cache.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
SymbolDefinition if found, None otherwise
|
|
393
|
+
"""
|
|
394
|
+
cache = get_symbol_cache()
|
|
395
|
+
return cache.get_symbol(self.lib_id)
|
|
396
|
+
|
|
397
|
+
def update_from_library(self) -> bool:
|
|
398
|
+
"""
|
|
399
|
+
Update component pins and metadata from library definition.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
True if update successful, False if symbol not found
|
|
403
|
+
"""
|
|
404
|
+
symbol_def = self.get_symbol_definition()
|
|
405
|
+
if not symbol_def:
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
# Update pins
|
|
409
|
+
self._data.pins = symbol_def.pins.copy()
|
|
410
|
+
|
|
411
|
+
# Warn if reference prefix doesn't match
|
|
412
|
+
if not self.reference.startswith(symbol_def.reference_prefix):
|
|
413
|
+
logger.warning(
|
|
414
|
+
f"Reference {self.reference} doesn't match expected prefix {symbol_def.reference_prefix}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
self._collection._mark_modified()
|
|
418
|
+
return True
|
|
419
|
+
|
|
420
|
+
def validate(self) -> List[ValidationIssue]:
|
|
421
|
+
"""
|
|
422
|
+
Validate this component.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
List of validation issues (empty if valid)
|
|
426
|
+
"""
|
|
427
|
+
return self._validator.validate_component(self._data.__dict__)
|
|
428
|
+
|
|
429
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
430
|
+
"""
|
|
431
|
+
Convert component to dictionary representation.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Dictionary with component data
|
|
435
|
+
"""
|
|
436
|
+
return {
|
|
437
|
+
"reference": self.reference,
|
|
438
|
+
"lib_id": self.lib_id,
|
|
439
|
+
"value": self.value,
|
|
440
|
+
"footprint": self.footprint,
|
|
441
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
442
|
+
"rotation": self.rotation,
|
|
443
|
+
"properties": self.properties.copy(),
|
|
444
|
+
"in_bom": self.in_bom,
|
|
445
|
+
"on_board": self.on_board,
|
|
446
|
+
"pin_count": len(self.pins),
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
def __str__(self) -> str:
|
|
450
|
+
"""String representation for display."""
|
|
451
|
+
return f"<Component {self.reference}: {self.lib_id} = '{self.value}' @ {self.position}>"
|
|
125
452
|
|
|
126
453
|
def __repr__(self) -> str:
|
|
127
|
-
"""Detailed representation."""
|
|
454
|
+
"""Detailed representation for debugging."""
|
|
128
455
|
return (
|
|
129
456
|
f"Component(ref='{self.reference}', lib_id='{self.lib_id}', "
|
|
130
457
|
f"value='{self.value}', pos={self.position}, rotation={self.rotation})"
|
|
131
458
|
)
|
|
132
459
|
|
|
133
460
|
|
|
134
|
-
class ComponentCollection(
|
|
461
|
+
class ComponentCollection(BaseCollection[Component]):
|
|
135
462
|
"""
|
|
136
|
-
Collection class for efficient component management.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
- Value indexing for filtering by component value
|
|
142
|
-
- Automatic reference generation
|
|
143
|
-
- Component validation and conflict detection
|
|
463
|
+
Collection class for efficient component management using IndexRegistry.
|
|
464
|
+
|
|
465
|
+
Provides fast lookup, filtering, and bulk operations with lazy index rebuilding
|
|
466
|
+
and batch mode support. Uses centralized IndexRegistry for managing all indexes
|
|
467
|
+
(UUID, reference, lib_id, value).
|
|
144
468
|
"""
|
|
145
469
|
|
|
146
|
-
def __init__(
|
|
470
|
+
def __init__(
|
|
471
|
+
self,
|
|
472
|
+
components: Optional[List[SchematicSymbol]] = None,
|
|
473
|
+
parent_schematic=None,
|
|
474
|
+
validation_level: ValidationLevel = ValidationLevel.NORMAL,
|
|
475
|
+
):
|
|
147
476
|
"""
|
|
148
477
|
Initialize component collection.
|
|
149
478
|
|
|
150
479
|
Args:
|
|
151
480
|
components: Initial list of component data
|
|
481
|
+
parent_schematic: Reference to parent Schematic (for hierarchy context)
|
|
482
|
+
validation_level: Validation level for operations
|
|
152
483
|
"""
|
|
153
|
-
|
|
484
|
+
# Initialize base collection with validation level
|
|
485
|
+
super().__init__(validation_level=validation_level)
|
|
486
|
+
|
|
487
|
+
# Store parent schematic reference for hierarchy context
|
|
488
|
+
self._parent_schematic = parent_schematic
|
|
489
|
+
|
|
490
|
+
# Manual indexes for special cases not handled by IndexRegistry
|
|
491
|
+
# (These are maintained separately for complex operations)
|
|
154
492
|
self._lib_id_index: Dict[str, List[Component]] = {}
|
|
155
493
|
self._value_index: Dict[str, List[Component]] = {}
|
|
156
494
|
|
|
157
|
-
#
|
|
158
|
-
wrapped_components = []
|
|
495
|
+
# Add initial components
|
|
159
496
|
if components:
|
|
160
|
-
|
|
497
|
+
with self.batch_mode():
|
|
498
|
+
for comp_data in components:
|
|
499
|
+
component = Component(comp_data, self)
|
|
500
|
+
super().add(component)
|
|
501
|
+
self._add_to_manual_indexes(component)
|
|
161
502
|
|
|
162
|
-
|
|
503
|
+
logger.debug(f"ComponentCollection initialized with {len(self)} components")
|
|
163
504
|
|
|
164
|
-
#
|
|
505
|
+
# BaseCollection abstract method implementations
|
|
165
506
|
def _get_item_uuid(self, item: Component) -> str:
|
|
166
507
|
"""Extract UUID from component."""
|
|
167
508
|
return item.uuid
|
|
168
509
|
|
|
169
510
|
def _create_item(self, **kwargs) -> Component:
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
raise NotImplementedError("Use add() method instead")
|
|
173
|
-
|
|
174
|
-
def _build_additional_indexes(self) -> None:
|
|
175
|
-
"""Build component-specific indexes."""
|
|
176
|
-
# Clear existing indexes
|
|
177
|
-
self._reference_index.clear()
|
|
178
|
-
self._lib_id_index.clear()
|
|
179
|
-
self._value_index.clear()
|
|
180
|
-
|
|
181
|
-
# Rebuild indexes from current items
|
|
182
|
-
for component in self._items:
|
|
183
|
-
# Reference index
|
|
184
|
-
self._reference_index[component.reference] = component
|
|
511
|
+
"""
|
|
512
|
+
Create a new component (not typically used directly).
|
|
185
513
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
self._lib_id_index[component.lib_id].append(component)
|
|
514
|
+
Use add() method instead for proper component creation.
|
|
515
|
+
"""
|
|
516
|
+
raise NotImplementedError("Use add() method to create components")
|
|
190
517
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
self._value_index[component.value].append(component)
|
|
518
|
+
def _get_index_specs(self) -> List[IndexSpec]:
|
|
519
|
+
"""
|
|
520
|
+
Get index specifications for component collection.
|
|
195
521
|
|
|
196
|
-
|
|
522
|
+
Returns:
|
|
523
|
+
List of IndexSpec for UUID and reference indexes
|
|
524
|
+
"""
|
|
525
|
+
return [
|
|
526
|
+
IndexSpec(
|
|
527
|
+
name="uuid",
|
|
528
|
+
key_func=lambda c: c.uuid,
|
|
529
|
+
unique=True,
|
|
530
|
+
description="UUID index for fast lookups",
|
|
531
|
+
),
|
|
532
|
+
IndexSpec(
|
|
533
|
+
name="reference",
|
|
534
|
+
key_func=lambda c: c.reference,
|
|
535
|
+
unique=True,
|
|
536
|
+
description="Reference designator index (R1, U2, etc.)",
|
|
537
|
+
),
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
# Component-specific add method
|
|
197
541
|
def add(
|
|
198
542
|
self,
|
|
199
543
|
lib_id: str,
|
|
@@ -202,6 +546,7 @@ class ComponentCollection(IndexedCollection[Component]):
|
|
|
202
546
|
position: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
203
547
|
footprint: Optional[str] = None,
|
|
204
548
|
unit: int = 1,
|
|
549
|
+
rotation: float = 0.0,
|
|
205
550
|
component_uuid: Optional[str] = None,
|
|
206
551
|
**properties,
|
|
207
552
|
) -> Component:
|
|
@@ -215,6 +560,7 @@ class ComponentCollection(IndexedCollection[Component]):
|
|
|
215
560
|
position: Component position (auto-placed if None)
|
|
216
561
|
footprint: Component footprint
|
|
217
562
|
unit: Unit number for multi-unit components (1-based)
|
|
563
|
+
rotation: Component rotation in degrees (0, 90, 180, 270)
|
|
218
564
|
component_uuid: Specific UUID for component (auto-generated if None)
|
|
219
565
|
**properties: Additional component properties
|
|
220
566
|
|
|
@@ -223,6 +569,7 @@ class ComponentCollection(IndexedCollection[Component]):
|
|
|
223
569
|
|
|
224
570
|
Raises:
|
|
225
571
|
ValidationError: If component data is invalid
|
|
572
|
+
LibraryError: If symbol library not found
|
|
226
573
|
"""
|
|
227
574
|
# Validate lib_id
|
|
228
575
|
validator = SchematicValidator()
|
|
@@ -239,7 +586,7 @@ class ComponentCollection(IndexedCollection[Component]):
|
|
|
239
586
|
|
|
240
587
|
# Check for duplicate reference
|
|
241
588
|
self._ensure_indexes_current()
|
|
242
|
-
if reference
|
|
589
|
+
if self._index_registry.has_key("reference", reference):
|
|
243
590
|
raise ValidationError(f"Reference {reference} already exists")
|
|
244
591
|
|
|
245
592
|
# Set default position if not provided
|
|
@@ -254,181 +601,1023 @@ class ComponentCollection(IndexedCollection[Component]):
|
|
|
254
601
|
snapped_pos = snap_to_grid((position.x, position.y), grid_size=1.27)
|
|
255
602
|
position = Point(snapped_pos[0], snapped_pos[1])
|
|
256
603
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
604
|
+
logger.debug(
|
|
605
|
+
f"Component {reference} position snapped to grid: ({position.x:.3f}, {position.y:.3f})"
|
|
606
|
+
)
|
|
260
607
|
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
608
|
+
# Normalize and validate rotation
|
|
609
|
+
rotation = rotation % 360
|
|
610
|
+
VALID_ROTATIONS = {0, 90, 180, 270}
|
|
611
|
+
if rotation not in VALID_ROTATIONS:
|
|
612
|
+
raise ValidationError(
|
|
613
|
+
f"Component rotation must be 0, 90, 180, or 270 degrees. "
|
|
614
|
+
f"Got {rotation}°. KiCad does not support arbitrary rotation angles."
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Add hierarchy_path if parent schematic has hierarchy context
|
|
618
|
+
if self._parent_schematic and hasattr(self._parent_schematic, "_hierarchy_path"):
|
|
619
|
+
if self._parent_schematic._hierarchy_path:
|
|
620
|
+
properties = dict(properties)
|
|
621
|
+
properties["hierarchy_path"] = self._parent_schematic._hierarchy_path
|
|
622
|
+
logger.debug(
|
|
623
|
+
f"Setting hierarchy_path for component {reference}: "
|
|
624
|
+
f"{self._parent_schematic._hierarchy_path}"
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# Create component data
|
|
628
|
+
component_data = SchematicSymbol(
|
|
629
|
+
uuid=component_uuid if component_uuid else str(uuid.uuid4()),
|
|
264
630
|
lib_id=lib_id,
|
|
631
|
+
position=position,
|
|
265
632
|
reference=reference,
|
|
266
633
|
value=value,
|
|
267
|
-
position=position,
|
|
268
|
-
rotation=0.0,
|
|
269
|
-
unit=unit,
|
|
270
|
-
in_bom=True,
|
|
271
|
-
on_board=True,
|
|
272
634
|
footprint=footprint,
|
|
273
|
-
|
|
635
|
+
unit=unit,
|
|
636
|
+
rotation=rotation,
|
|
637
|
+
properties=properties,
|
|
274
638
|
)
|
|
275
639
|
|
|
640
|
+
# Get symbol definition and update pins
|
|
641
|
+
from ..core.exceptions import LibraryError
|
|
642
|
+
|
|
643
|
+
symbol_cache = get_symbol_cache()
|
|
644
|
+
symbol_def = symbol_cache.get_symbol(lib_id)
|
|
645
|
+
if not symbol_def:
|
|
646
|
+
library_name = lib_id.split(":")[0] if ":" in lib_id else "unknown"
|
|
647
|
+
raise LibraryError(
|
|
648
|
+
f"Symbol '{lib_id}' not found in KiCAD libraries. "
|
|
649
|
+
f"Please verify the library name '{library_name}' and symbol name are correct. "
|
|
650
|
+
f"Common libraries include: Device, Connector_Generic, Regulator_Linear, RF_Module",
|
|
651
|
+
field="lib_id",
|
|
652
|
+
value=lib_id,
|
|
653
|
+
)
|
|
654
|
+
component_data.pins = symbol_def.pins.copy()
|
|
655
|
+
|
|
276
656
|
# Create component wrapper
|
|
277
|
-
component = Component(
|
|
657
|
+
component = Component(component_data, self)
|
|
658
|
+
|
|
659
|
+
# Add to collection (includes IndexRegistry)
|
|
660
|
+
super().add(component)
|
|
661
|
+
|
|
662
|
+
# Add to manual indexes (lib_id, value)
|
|
663
|
+
self._add_to_manual_indexes(component)
|
|
664
|
+
|
|
665
|
+
logger.info(f"Added component: {reference} ({lib_id})")
|
|
666
|
+
return component
|
|
667
|
+
|
|
668
|
+
def add_with_pin_at(
|
|
669
|
+
self,
|
|
670
|
+
lib_id: str,
|
|
671
|
+
pin_number: str,
|
|
672
|
+
pin_position: Union[Point, Tuple[float, float]],
|
|
673
|
+
reference: Optional[str] = None,
|
|
674
|
+
value: str = "",
|
|
675
|
+
rotation: float = 0.0,
|
|
676
|
+
footprint: Optional[str] = None,
|
|
677
|
+
unit: int = 1,
|
|
678
|
+
component_uuid: Optional[str] = None,
|
|
679
|
+
**properties,
|
|
680
|
+
) -> Component:
|
|
681
|
+
"""
|
|
682
|
+
Add component positioned by a specific pin location.
|
|
683
|
+
|
|
684
|
+
Instead of specifying the component's center position, this method allows
|
|
685
|
+
you to specify where a particular pin should be placed. This is extremely
|
|
686
|
+
useful for aligning components in horizontal signal flows without manual
|
|
687
|
+
offset calculations.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
lib_id: Library identifier (e.g., "Device:R", "Device:C")
|
|
691
|
+
pin_number: Pin number to position (e.g., "1", "2")
|
|
692
|
+
pin_position: Desired position for the specified pin
|
|
693
|
+
reference: Component reference (auto-generated if None)
|
|
694
|
+
value: Component value
|
|
695
|
+
rotation: Component rotation in degrees (0, 90, 180, 270)
|
|
696
|
+
footprint: Component footprint
|
|
697
|
+
unit: Unit number for multi-unit components (1-based)
|
|
698
|
+
component_uuid: Specific UUID for component (auto-generated if None)
|
|
699
|
+
**properties: Additional component properties
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
Newly created Component with the specified pin at pin_position
|
|
703
|
+
|
|
704
|
+
Raises:
|
|
705
|
+
ValidationError: If component data is invalid
|
|
706
|
+
LibraryError: If symbol library not found
|
|
707
|
+
ValueError: If pin_number doesn't exist in the component
|
|
708
|
+
|
|
709
|
+
Example:
|
|
710
|
+
# Place resistor with pin 2 at (150, 100)
|
|
711
|
+
r1 = sch.components.add_with_pin_at(
|
|
712
|
+
lib_id="Device:R",
|
|
713
|
+
pin_number="2",
|
|
714
|
+
pin_position=(150, 100),
|
|
715
|
+
value="10k"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Place capacitor with pin 1 aligned on same horizontal line
|
|
719
|
+
c1 = sch.components.add_with_pin_at(
|
|
720
|
+
lib_id="Device:C",
|
|
721
|
+
pin_number="1",
|
|
722
|
+
pin_position=(200, 100), # Same Y as resistor pin 2
|
|
723
|
+
value="100nF"
|
|
724
|
+
)
|
|
725
|
+
"""
|
|
726
|
+
from ..core.exceptions import LibraryError
|
|
727
|
+
from ..core.geometry import calculate_position_for_pin
|
|
728
|
+
|
|
729
|
+
# Get symbol definition to find the pin's local position
|
|
730
|
+
symbol_cache = get_symbol_cache()
|
|
731
|
+
symbol_def = symbol_cache.get_symbol(lib_id)
|
|
732
|
+
if not symbol_def:
|
|
733
|
+
library_name = lib_id.split(":")[0] if ":" in lib_id else "unknown"
|
|
734
|
+
raise LibraryError(
|
|
735
|
+
f"Symbol '{lib_id}' not found in KiCAD libraries. "
|
|
736
|
+
f"Please verify the library name '{library_name}' and symbol name are correct. "
|
|
737
|
+
f"Common libraries include: Device, Connector_Generic, Regulator_Linear, RF_Module",
|
|
738
|
+
field="lib_id",
|
|
739
|
+
value=lib_id,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# Find the pin in the symbol definition
|
|
743
|
+
pin_def = None
|
|
744
|
+
for pin in symbol_def.pins:
|
|
745
|
+
if pin.number == pin_number:
|
|
746
|
+
pin_def = pin
|
|
747
|
+
break
|
|
748
|
+
|
|
749
|
+
if not pin_def:
|
|
750
|
+
available_pins = [p.number for p in symbol_def.pins]
|
|
751
|
+
raise ValueError(
|
|
752
|
+
f"Pin '{pin_number}' not found in symbol '{lib_id}'. "
|
|
753
|
+
f"Available pins: {', '.join(available_pins)}"
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
logger.debug(
|
|
757
|
+
f"Pin {pin_number} found at local position ({pin_def.position.x}, {pin_def.position.y})"
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
# Calculate component position that will place the pin at the desired location
|
|
761
|
+
component_position = calculate_position_for_pin(
|
|
762
|
+
pin_local_position=pin_def.position,
|
|
763
|
+
desired_pin_position=pin_position,
|
|
764
|
+
rotation=rotation,
|
|
765
|
+
mirror=None, # TODO: Add mirror support when needed
|
|
766
|
+
grid_size=1.27,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
logger.info(
|
|
770
|
+
f"Calculated component position ({component_position.x}, {component_position.y}) "
|
|
771
|
+
f"to place pin {pin_number} at ({pin_position if isinstance(pin_position, Point) else Point(*pin_position)})"
|
|
772
|
+
)
|
|
278
773
|
|
|
279
|
-
#
|
|
280
|
-
return
|
|
774
|
+
# Use the regular add() method with the calculated position
|
|
775
|
+
return self.add(
|
|
776
|
+
lib_id=lib_id,
|
|
777
|
+
reference=reference,
|
|
778
|
+
value=value,
|
|
779
|
+
position=component_position,
|
|
780
|
+
footprint=footprint,
|
|
781
|
+
unit=unit,
|
|
782
|
+
rotation=rotation,
|
|
783
|
+
component_uuid=component_uuid,
|
|
784
|
+
**properties,
|
|
785
|
+
)
|
|
281
786
|
|
|
282
|
-
def
|
|
787
|
+
def add_ic(
|
|
788
|
+
self,
|
|
789
|
+
lib_id: str,
|
|
790
|
+
reference_prefix: str,
|
|
791
|
+
position: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
792
|
+
value: str = "",
|
|
793
|
+
footprint: Optional[str] = None,
|
|
794
|
+
layout_style: str = "vertical",
|
|
795
|
+
**properties,
|
|
796
|
+
) -> ICManager:
|
|
283
797
|
"""
|
|
284
|
-
|
|
798
|
+
Add a multi-unit IC with automatic unit placement.
|
|
285
799
|
|
|
286
800
|
Args:
|
|
287
|
-
|
|
801
|
+
lib_id: Library identifier for the IC (e.g., "74xx:7400")
|
|
802
|
+
reference_prefix: Base reference (e.g., "U1" → U1A, U1B, etc.)
|
|
803
|
+
position: Base position for auto-layout (auto-placed if None)
|
|
804
|
+
value: IC value (defaults to symbol name)
|
|
805
|
+
footprint: IC footprint
|
|
806
|
+
layout_style: Layout algorithm ("vertical", "grid", "functional")
|
|
807
|
+
**properties: Common properties for all units
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
ICManager object for position overrides and management
|
|
811
|
+
|
|
812
|
+
Example:
|
|
813
|
+
ic = sch.components.add_ic("74xx:7400", "U1", position=(100, 100))
|
|
814
|
+
ic.place_unit(1, position=(150, 80)) # Override Gate A position
|
|
815
|
+
"""
|
|
816
|
+
# Set default position if not provided
|
|
817
|
+
if position is None:
|
|
818
|
+
position = self._find_available_position()
|
|
819
|
+
elif isinstance(position, tuple):
|
|
820
|
+
position = Point(position[0], position[1])
|
|
821
|
+
|
|
822
|
+
# Set default value to symbol name if not provided
|
|
823
|
+
if not value:
|
|
824
|
+
value = lib_id.split(":")[-1] # "74xx:7400" → "7400"
|
|
825
|
+
|
|
826
|
+
# Create IC manager for this multi-unit component
|
|
827
|
+
ic_manager = ICManager(lib_id, reference_prefix, position, self)
|
|
828
|
+
|
|
829
|
+
# Generate all unit components
|
|
830
|
+
unit_components = ic_manager.generate_components(
|
|
831
|
+
value=value, footprint=footprint, properties=properties
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Add all units to the collection using batch mode for performance
|
|
835
|
+
with self.batch_mode():
|
|
836
|
+
for component_data in unit_components:
|
|
837
|
+
component = Component(component_data, self)
|
|
838
|
+
super().add(component)
|
|
839
|
+
self._add_to_manual_indexes(component)
|
|
840
|
+
|
|
841
|
+
logger.info(
|
|
842
|
+
f"Added multi-unit IC: {reference_prefix} ({lib_id}) with {len(unit_components)} units"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
return ic_manager
|
|
846
|
+
|
|
847
|
+
# Remove operations
|
|
848
|
+
def remove(self, reference: str) -> bool:
|
|
849
|
+
"""
|
|
850
|
+
Remove component by reference designator.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
reference: Component reference to remove (e.g., "R1")
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
True if component was removed, False if not found
|
|
857
|
+
|
|
858
|
+
Raises:
|
|
859
|
+
TypeError: If reference is not a string
|
|
860
|
+
"""
|
|
861
|
+
if not isinstance(reference, str):
|
|
862
|
+
raise TypeError(f"reference must be a string, not {type(reference).__name__}")
|
|
863
|
+
|
|
864
|
+
self._ensure_indexes_current()
|
|
865
|
+
|
|
866
|
+
# Get component from reference index
|
|
867
|
+
ref_idx = self._index_registry.get("reference", reference)
|
|
868
|
+
if ref_idx is None:
|
|
869
|
+
return False
|
|
870
|
+
|
|
871
|
+
component = self._items[ref_idx]
|
|
872
|
+
|
|
873
|
+
# Remove from manual indexes
|
|
874
|
+
self._remove_from_manual_indexes(component)
|
|
875
|
+
|
|
876
|
+
# Remove from base collection (UUID index)
|
|
877
|
+
super().remove(component.uuid)
|
|
878
|
+
|
|
879
|
+
logger.info(f"Removed component: {reference}")
|
|
880
|
+
return True
|
|
881
|
+
|
|
882
|
+
def remove_by_uuid(self, component_uuid: str) -> bool:
|
|
883
|
+
"""
|
|
884
|
+
Remove component by UUID.
|
|
885
|
+
|
|
886
|
+
Args:
|
|
887
|
+
component_uuid: Component UUID to remove
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
True if component was removed, False if not found
|
|
891
|
+
|
|
892
|
+
Raises:
|
|
893
|
+
TypeError: If UUID is not a string
|
|
894
|
+
"""
|
|
895
|
+
if not isinstance(component_uuid, str):
|
|
896
|
+
raise TypeError(f"component_uuid must be a string, not {type(component_uuid).__name__}")
|
|
897
|
+
|
|
898
|
+
# Get component from UUID index
|
|
899
|
+
component = self.get_by_uuid(component_uuid)
|
|
900
|
+
if not component:
|
|
901
|
+
return False
|
|
902
|
+
|
|
903
|
+
# Remove from manual indexes
|
|
904
|
+
self._remove_from_manual_indexes(component)
|
|
905
|
+
|
|
906
|
+
# Remove from base collection
|
|
907
|
+
super().remove(component_uuid)
|
|
908
|
+
|
|
909
|
+
logger.info(f"Removed component by UUID: {component_uuid}")
|
|
910
|
+
return True
|
|
911
|
+
|
|
912
|
+
def remove_component(self, component: Component) -> bool:
|
|
913
|
+
"""
|
|
914
|
+
Remove component by component object.
|
|
915
|
+
|
|
916
|
+
Args:
|
|
917
|
+
component: Component object to remove
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
True if component was removed, False if not found
|
|
921
|
+
|
|
922
|
+
Raises:
|
|
923
|
+
TypeError: If component is not a Component instance
|
|
924
|
+
"""
|
|
925
|
+
if not isinstance(component, Component):
|
|
926
|
+
raise TypeError(
|
|
927
|
+
f"component must be a Component instance, not {type(component).__name__}"
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
# Check if component exists
|
|
931
|
+
if component.uuid not in self:
|
|
932
|
+
return False
|
|
933
|
+
|
|
934
|
+
# Remove from manual indexes
|
|
935
|
+
self._remove_from_manual_indexes(component)
|
|
936
|
+
|
|
937
|
+
# Remove from base collection
|
|
938
|
+
super().remove(component.uuid)
|
|
939
|
+
|
|
940
|
+
logger.info(f"Removed component: {component.reference}")
|
|
941
|
+
return True
|
|
942
|
+
|
|
943
|
+
# Lookup methods
|
|
944
|
+
def get(self, reference: str) -> Optional[Component]:
|
|
945
|
+
"""
|
|
946
|
+
Get component by reference designator.
|
|
947
|
+
|
|
948
|
+
Args:
|
|
949
|
+
reference: Component reference (e.g., "R1")
|
|
288
950
|
|
|
289
951
|
Returns:
|
|
290
952
|
Component if found, None otherwise
|
|
291
953
|
"""
|
|
292
954
|
self._ensure_indexes_current()
|
|
293
|
-
|
|
955
|
+
ref_idx = self._index_registry.get("reference", reference)
|
|
956
|
+
if ref_idx is not None:
|
|
957
|
+
return self._items[ref_idx]
|
|
958
|
+
return None
|
|
959
|
+
|
|
960
|
+
def get_by_uuid(self, component_uuid: str) -> Optional[Component]:
|
|
961
|
+
"""
|
|
962
|
+
Get component by UUID.
|
|
294
963
|
|
|
295
|
-
|
|
964
|
+
Args:
|
|
965
|
+
component_uuid: Component UUID
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
Component if found, None otherwise
|
|
296
969
|
"""
|
|
297
|
-
|
|
970
|
+
return super().get(component_uuid)
|
|
971
|
+
|
|
972
|
+
# Filter and search methods
|
|
973
|
+
def filter(self, **criteria) -> List[Component]:
|
|
974
|
+
"""
|
|
975
|
+
Filter components by various criteria.
|
|
976
|
+
|
|
977
|
+
Supported criteria:
|
|
978
|
+
lib_id: Filter by library ID (exact match)
|
|
979
|
+
value: Filter by value (exact match)
|
|
980
|
+
value_pattern: Filter by value pattern (contains)
|
|
981
|
+
reference_pattern: Filter by reference pattern (regex)
|
|
982
|
+
footprint: Filter by footprint (exact match)
|
|
983
|
+
in_area: Filter by area (tuple of (x1, y1, x2, y2))
|
|
984
|
+
has_property: Filter components that have a specific property
|
|
298
985
|
|
|
299
986
|
Args:
|
|
300
|
-
|
|
987
|
+
**criteria: Filter criteria
|
|
301
988
|
|
|
302
989
|
Returns:
|
|
303
990
|
List of matching components
|
|
304
991
|
"""
|
|
305
|
-
self.
|
|
306
|
-
|
|
992
|
+
results = list(self._items)
|
|
993
|
+
|
|
994
|
+
# Apply filters
|
|
995
|
+
if "lib_id" in criteria:
|
|
996
|
+
lib_id = criteria["lib_id"]
|
|
997
|
+
results = [c for c in results if c.lib_id == lib_id]
|
|
998
|
+
|
|
999
|
+
if "value" in criteria:
|
|
1000
|
+
value = criteria["value"]
|
|
1001
|
+
results = [c for c in results if c.value == value]
|
|
1002
|
+
|
|
1003
|
+
if "value_pattern" in criteria:
|
|
1004
|
+
pattern = criteria["value_pattern"].lower()
|
|
1005
|
+
results = [c for c in results if pattern in c.value.lower()]
|
|
1006
|
+
|
|
1007
|
+
if "reference_pattern" in criteria:
|
|
1008
|
+
import re
|
|
1009
|
+
|
|
1010
|
+
pattern = re.compile(criteria["reference_pattern"])
|
|
1011
|
+
results = [c for c in results if pattern.match(c.reference)]
|
|
307
1012
|
|
|
308
|
-
|
|
1013
|
+
if "footprint" in criteria:
|
|
1014
|
+
footprint = criteria["footprint"]
|
|
1015
|
+
results = [c for c in results if c.footprint == footprint]
|
|
1016
|
+
|
|
1017
|
+
if "in_area" in criteria:
|
|
1018
|
+
x1, y1, x2, y2 = criteria["in_area"]
|
|
1019
|
+
results = [c for c in results if x1 <= c.position.x <= x2 and y1 <= c.position.y <= y2]
|
|
1020
|
+
|
|
1021
|
+
if "has_property" in criteria:
|
|
1022
|
+
prop_name = criteria["has_property"]
|
|
1023
|
+
results = [c for c in results if prop_name in c.properties]
|
|
1024
|
+
|
|
1025
|
+
return results
|
|
1026
|
+
|
|
1027
|
+
def filter_by_type(self, component_type: str) -> List[Component]:
|
|
309
1028
|
"""
|
|
310
|
-
|
|
1029
|
+
Filter components by type prefix.
|
|
311
1030
|
|
|
312
1031
|
Args:
|
|
313
|
-
|
|
1032
|
+
component_type: Type prefix (e.g., 'R' for resistors, 'C' for capacitors)
|
|
314
1033
|
|
|
315
1034
|
Returns:
|
|
316
1035
|
List of matching components
|
|
317
1036
|
"""
|
|
318
|
-
|
|
319
|
-
|
|
1037
|
+
return [
|
|
1038
|
+
c for c in self._items if c.symbol_name.upper().startswith(component_type.upper())
|
|
1039
|
+
]
|
|
320
1040
|
|
|
321
|
-
def
|
|
1041
|
+
def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
|
|
322
1042
|
"""
|
|
323
|
-
|
|
1043
|
+
Get components within rectangular area.
|
|
324
1044
|
|
|
325
1045
|
Args:
|
|
326
|
-
|
|
1046
|
+
x1, y1: Top-left corner
|
|
1047
|
+
x2, y2: Bottom-right corner
|
|
327
1048
|
|
|
328
1049
|
Returns:
|
|
329
|
-
|
|
1050
|
+
List of components in area
|
|
330
1051
|
"""
|
|
331
|
-
|
|
332
|
-
if ":" in lib_id:
|
|
333
|
-
base_ref = lib_id.split(":")[-1]
|
|
334
|
-
else:
|
|
335
|
-
base_ref = lib_id
|
|
336
|
-
|
|
337
|
-
# Map common component types to standard prefixes
|
|
338
|
-
ref_prefixes = {
|
|
339
|
-
"R": "R",
|
|
340
|
-
"Resistor": "R",
|
|
341
|
-
"C": "C",
|
|
342
|
-
"Capacitor": "C",
|
|
343
|
-
"L": "L",
|
|
344
|
-
"Inductor": "L",
|
|
345
|
-
"D": "D",
|
|
346
|
-
"Diode": "D",
|
|
347
|
-
"Q": "Q",
|
|
348
|
-
"Transistor": "Q",
|
|
349
|
-
"U": "U",
|
|
350
|
-
"IC": "U",
|
|
351
|
-
"Amplifier": "U",
|
|
352
|
-
"J": "J",
|
|
353
|
-
"Connector": "J",
|
|
354
|
-
"SW": "SW",
|
|
355
|
-
"Switch": "SW",
|
|
356
|
-
"F": "F",
|
|
357
|
-
"Fuse": "F",
|
|
358
|
-
"TP": "TP",
|
|
359
|
-
"TestPoint": "TP",
|
|
360
|
-
}
|
|
1052
|
+
return self.filter(in_area=(x1, y1, x2, y2))
|
|
361
1053
|
|
|
362
|
-
|
|
1054
|
+
def near_point(
|
|
1055
|
+
self, point: Union[Point, Tuple[float, float]], radius: float
|
|
1056
|
+
) -> List[Component]:
|
|
1057
|
+
"""
|
|
1058
|
+
Get components within radius of a point.
|
|
363
1059
|
|
|
364
|
-
|
|
365
|
-
|
|
1060
|
+
Args:
|
|
1061
|
+
point: Center point (Point or (x, y) tuple)
|
|
1062
|
+
radius: Search radius in mm
|
|
366
1063
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1064
|
+
Returns:
|
|
1065
|
+
List of components within radius
|
|
1066
|
+
"""
|
|
1067
|
+
if isinstance(point, tuple):
|
|
1068
|
+
point = Point(point[0], point[1])
|
|
371
1069
|
|
|
372
|
-
|
|
1070
|
+
results = []
|
|
1071
|
+
for component in self._items:
|
|
1072
|
+
if component.position.distance_to(point) <= radius:
|
|
1073
|
+
results.append(component)
|
|
1074
|
+
return results
|
|
373
1075
|
|
|
374
|
-
def
|
|
1076
|
+
def find_pins_by_name(
|
|
1077
|
+
self, reference: str, name_pattern: str, case_sensitive: bool = False
|
|
1078
|
+
) -> Optional[List[str]]:
|
|
375
1079
|
"""
|
|
376
|
-
Find
|
|
1080
|
+
Find pin numbers matching a name pattern.
|
|
1081
|
+
|
|
1082
|
+
Supports both exact matches and wildcard patterns (e.g., "CLK*", "*IN*").
|
|
1083
|
+
By default, matching is case-insensitive for maximum flexibility.
|
|
1084
|
+
|
|
1085
|
+
Args:
|
|
1086
|
+
reference: Component reference designator (e.g., "R1", "U2")
|
|
1087
|
+
name_pattern: Name pattern to search for (e.g., "VCC", "CLK", "OUT", "CLK*", "*IN*")
|
|
1088
|
+
case_sensitive: If False (default), matching is case-insensitive
|
|
377
1089
|
|
|
378
1090
|
Returns:
|
|
379
|
-
|
|
1091
|
+
List of matching pin numbers (e.g., ["1", "2"]), or None if component not found
|
|
1092
|
+
|
|
1093
|
+
Raises:
|
|
1094
|
+
ValueError: If name_pattern is empty
|
|
1095
|
+
|
|
1096
|
+
Example:
|
|
1097
|
+
# Find all clock pins
|
|
1098
|
+
pins = sch.components.find_pins_by_name("U1", "CLK*")
|
|
1099
|
+
# Returns: ["5", "10"] (whatever the clock pins are numbered)
|
|
1100
|
+
|
|
1101
|
+
# Find power pins
|
|
1102
|
+
pins = sch.components.find_pins_by_name("U1", "VCC")
|
|
1103
|
+
# Returns: ["1", "20"] for a common IC
|
|
380
1104
|
"""
|
|
381
|
-
|
|
382
|
-
base_x, base_y = 100.0, 100.0
|
|
383
|
-
spacing = 25.4 # 1 inch spacing
|
|
1105
|
+
import fnmatch
|
|
384
1106
|
|
|
385
|
-
|
|
386
|
-
|
|
1107
|
+
logger.debug(f"[PIN_DISCOVERY] find_pins_by_name() called for {reference}")
|
|
1108
|
+
logger.debug(f"[PIN_DISCOVERY] Pattern: '{name_pattern}' (case_sensitive={case_sensitive})")
|
|
387
1109
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
for col in range(10): # Check up to 10 columns
|
|
391
|
-
x = base_x + col * spacing
|
|
392
|
-
y = base_y + row * spacing
|
|
393
|
-
if (x, y) not in used_positions:
|
|
394
|
-
return Point(x, y)
|
|
1110
|
+
if not name_pattern:
|
|
1111
|
+
raise ValueError("name_pattern cannot be empty")
|
|
395
1112
|
|
|
396
|
-
#
|
|
397
|
-
|
|
1113
|
+
# Step 1: Get component
|
|
1114
|
+
component = self.get(reference)
|
|
1115
|
+
if not component:
|
|
1116
|
+
logger.warning(f"[PIN_DISCOVERY] Component not found: {reference}")
|
|
1117
|
+
return None
|
|
398
1118
|
|
|
399
|
-
|
|
1119
|
+
logger.debug(f"[PIN_DISCOVERY] Found component {reference} ({component.lib_id})")
|
|
1120
|
+
|
|
1121
|
+
# Step 2: Get symbol definition
|
|
1122
|
+
symbol_def = component.get_symbol_definition()
|
|
1123
|
+
if not symbol_def:
|
|
1124
|
+
logger.warning(
|
|
1125
|
+
f"[PIN_DISCOVERY] Symbol definition not found for {reference} ({component.lib_id})"
|
|
1126
|
+
)
|
|
1127
|
+
return None
|
|
1128
|
+
|
|
1129
|
+
logger.debug(
|
|
1130
|
+
f"[PIN_DISCOVERY] Symbol has {len(symbol_def.pins)} total pins to search"
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
# Step 3: Match pins by name
|
|
1134
|
+
matching_pins = []
|
|
1135
|
+
search_pattern = name_pattern if case_sensitive else name_pattern.lower()
|
|
1136
|
+
|
|
1137
|
+
for pin in symbol_def.pins:
|
|
1138
|
+
pin_name = pin.name if case_sensitive else pin.name.lower()
|
|
1139
|
+
|
|
1140
|
+
# Use fnmatch for wildcard matching
|
|
1141
|
+
if fnmatch.fnmatch(pin_name, search_pattern):
|
|
1142
|
+
logger.debug(
|
|
1143
|
+
f"[PIN_DISCOVERY] Pin {pin.number} ({pin.name}) matches pattern '{name_pattern}'"
|
|
1144
|
+
)
|
|
1145
|
+
matching_pins.append(pin.number)
|
|
1146
|
+
|
|
1147
|
+
logger.info(
|
|
1148
|
+
f"[PIN_DISCOVERY] Found {len(matching_pins)} pins matching '{name_pattern}' "
|
|
1149
|
+
f"in {reference}: {matching_pins}"
|
|
1150
|
+
)
|
|
1151
|
+
return matching_pins
|
|
1152
|
+
|
|
1153
|
+
def find_pins_by_type(
|
|
1154
|
+
self, reference: str, pin_type: Union[str, "PinType"]
|
|
1155
|
+
) -> Optional[List[str]]:
|
|
400
1156
|
"""
|
|
401
|
-
|
|
1157
|
+
Find pin numbers by electrical type.
|
|
1158
|
+
|
|
1159
|
+
Returns all pins of a specific electrical type (e.g., all inputs, all power pins).
|
|
1160
|
+
|
|
1161
|
+
Args:
|
|
1162
|
+
reference: Component reference designator (e.g., "R1", "U2")
|
|
1163
|
+
pin_type: Electrical type filter. Can be:
|
|
1164
|
+
- String: "input", "output", "passive", "power_in", "power_out", etc.
|
|
1165
|
+
- PinType enum value
|
|
1166
|
+
|
|
1167
|
+
Returns:
|
|
1168
|
+
List of matching pin numbers, or None if component not found
|
|
1169
|
+
|
|
1170
|
+
Example:
|
|
1171
|
+
# Find all input pins
|
|
1172
|
+
pins = sch.components.find_pins_by_type("U1", "input")
|
|
1173
|
+
# Returns: ["1", "2", "3"]
|
|
1174
|
+
|
|
1175
|
+
# Find all power pins
|
|
1176
|
+
pins = sch.components.find_pins_by_type("U1", "power_in")
|
|
1177
|
+
# Returns: ["20", "40"] for a common IC
|
|
1178
|
+
"""
|
|
1179
|
+
from ..core.types import PinType
|
|
1180
|
+
|
|
1181
|
+
logger.debug(f"[PIN_DISCOVERY] find_pins_by_type() called for {reference}")
|
|
1182
|
+
|
|
1183
|
+
# Normalize pin_type to PinType enum
|
|
1184
|
+
if isinstance(pin_type, str):
|
|
1185
|
+
try:
|
|
1186
|
+
pin_type_enum = PinType(pin_type)
|
|
1187
|
+
logger.debug(f"[PIN_DISCOVERY] Type filter: {pin_type}")
|
|
1188
|
+
except ValueError:
|
|
1189
|
+
logger.error(f"[PIN_DISCOVERY] Invalid pin type: {pin_type}")
|
|
1190
|
+
raise ValueError(
|
|
1191
|
+
f"Invalid pin type: {pin_type}. "
|
|
1192
|
+
f"Must be one of: {', '.join(pt.value for pt in PinType)}"
|
|
1193
|
+
)
|
|
1194
|
+
else:
|
|
1195
|
+
pin_type_enum = pin_type
|
|
1196
|
+
logger.debug(f"[PIN_DISCOVERY] Type filter: {pin_type_enum.value}")
|
|
1197
|
+
|
|
1198
|
+
# Step 1: Get component
|
|
1199
|
+
component = self.get(reference)
|
|
1200
|
+
if not component:
|
|
1201
|
+
logger.warning(f"[PIN_DISCOVERY] Component not found: {reference}")
|
|
1202
|
+
return None
|
|
1203
|
+
|
|
1204
|
+
logger.debug(f"[PIN_DISCOVERY] Found component {reference} ({component.lib_id})")
|
|
1205
|
+
|
|
1206
|
+
# Step 2: Get symbol definition
|
|
1207
|
+
symbol_def = component.get_symbol_definition()
|
|
1208
|
+
if not symbol_def:
|
|
1209
|
+
logger.warning(
|
|
1210
|
+
f"[PIN_DISCOVERY] Symbol definition not found for {reference} ({component.lib_id})"
|
|
1211
|
+
)
|
|
1212
|
+
return None
|
|
1213
|
+
|
|
1214
|
+
logger.debug(
|
|
1215
|
+
f"[PIN_DISCOVERY] Symbol has {len(symbol_def.pins)} total pins to filter"
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
# Step 3: Filter pins by type
|
|
1219
|
+
matching_pins = []
|
|
1220
|
+
for pin in symbol_def.pins:
|
|
1221
|
+
if pin.pin_type == pin_type_enum:
|
|
1222
|
+
logger.debug(
|
|
1223
|
+
f"[PIN_DISCOVERY] Pin {pin.number} ({pin.name}) is type {pin_type_enum.value}"
|
|
1224
|
+
)
|
|
1225
|
+
matching_pins.append(pin.number)
|
|
1226
|
+
|
|
1227
|
+
logger.info(
|
|
1228
|
+
f"[PIN_DISCOVERY] Found {len(matching_pins)} pins of type '{pin_type_enum.value}' "
|
|
1229
|
+
f"in {reference}: {matching_pins}"
|
|
1230
|
+
)
|
|
1231
|
+
return matching_pins
|
|
1232
|
+
|
|
1233
|
+
def get_pins_info(self, reference: str) -> Optional[List[PinInfo]]:
|
|
1234
|
+
"""
|
|
1235
|
+
Get comprehensive pin information for a component.
|
|
1236
|
+
|
|
1237
|
+
Returns all pins for the specified component with complete metadata
|
|
1238
|
+
including electrical type, shape, absolute position (accounting for
|
|
1239
|
+
rotation and mirroring), and orientation.
|
|
402
1240
|
|
|
403
1241
|
Args:
|
|
404
|
-
|
|
405
|
-
|
|
1242
|
+
reference: Component reference designator (e.g., "R1", "U2")
|
|
1243
|
+
|
|
1244
|
+
Returns:
|
|
1245
|
+
List of PinInfo objects with complete pin metadata, or None if component not found
|
|
1246
|
+
|
|
1247
|
+
Raises:
|
|
1248
|
+
LibraryError: If component's symbol library is not available
|
|
1249
|
+
|
|
1250
|
+
Example:
|
|
1251
|
+
pins = sch.components.get_pins_info("U1")
|
|
1252
|
+
if pins:
|
|
1253
|
+
for pin in pins:
|
|
1254
|
+
print(f"Pin {pin.number}: {pin.name} @ {pin.position}")
|
|
1255
|
+
print(f" Electrical type: {pin.electrical_type.value}")
|
|
1256
|
+
print(f" Shape: {pin.shape.value}")
|
|
406
1257
|
"""
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1258
|
+
logger.debug(f"[PIN_DISCOVERY] get_pins_info() called for reference: {reference}")
|
|
1259
|
+
|
|
1260
|
+
# Step 1: Find the component
|
|
1261
|
+
component = self.get(reference)
|
|
1262
|
+
if not component:
|
|
1263
|
+
logger.warning(f"[PIN_DISCOVERY] Component not found: {reference}")
|
|
1264
|
+
return None
|
|
1265
|
+
|
|
1266
|
+
logger.debug(f"[PIN_DISCOVERY] Found component {reference} ({component.lib_id})")
|
|
1267
|
+
|
|
1268
|
+
# Step 2: Get symbol definition from library cache
|
|
1269
|
+
symbol_def = component.get_symbol_definition()
|
|
1270
|
+
if not symbol_def:
|
|
1271
|
+
from ..core.exceptions import LibraryError
|
|
1272
|
+
|
|
1273
|
+
lib_id = component.lib_id
|
|
1274
|
+
logger.error(
|
|
1275
|
+
f"[PIN_DISCOVERY] Symbol library not found for component {reference}: {lib_id}"
|
|
1276
|
+
)
|
|
1277
|
+
raise LibraryError(
|
|
1278
|
+
f"Symbol '{lib_id}' not found in KiCAD libraries. "
|
|
1279
|
+
f"Please verify the library name and symbol name are correct.",
|
|
1280
|
+
field="lib_id",
|
|
1281
|
+
value=lib_id,
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
logger.debug(
|
|
1285
|
+
f"[PIN_DISCOVERY] Retrieved symbol definition for {reference}: "
|
|
1286
|
+
f"{len(symbol_def.pins)} pins"
|
|
1287
|
+
)
|
|
410
1288
|
|
|
411
|
-
|
|
1289
|
+
# Step 3: Build PinInfo list with absolute positions
|
|
1290
|
+
pins_info = []
|
|
1291
|
+
for pin in symbol_def.pins:
|
|
1292
|
+
logger.debug(
|
|
1293
|
+
f"[PIN_DISCOVERY] Processing pin {pin.number} ({pin.name}) "
|
|
1294
|
+
f"in local coords: {pin.position}"
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
# Get absolute position accounting for component rotation
|
|
1298
|
+
absolute_position = component.get_pin_position(pin.number)
|
|
1299
|
+
if not absolute_position:
|
|
1300
|
+
logger.warning(
|
|
1301
|
+
f"[PIN_DISCOVERY] Could not calculate position for pin {pin.number} on {reference}"
|
|
1302
|
+
)
|
|
1303
|
+
continue
|
|
1304
|
+
|
|
1305
|
+
logger.debug(
|
|
1306
|
+
f"[PIN_DISCOVERY] Pin {pin.number} absolute position: {absolute_position}"
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
# Create PinInfo with absolute position
|
|
1310
|
+
pin_info = PinInfo(
|
|
1311
|
+
number=pin.number,
|
|
1312
|
+
name=pin.name,
|
|
1313
|
+
position=absolute_position,
|
|
1314
|
+
electrical_type=pin.pin_type,
|
|
1315
|
+
shape=pin.pin_shape,
|
|
1316
|
+
length=pin.length,
|
|
1317
|
+
orientation=pin.rotation, # Note: pin rotation in symbol space
|
|
1318
|
+
uuid=f"{component.uuid}:{pin.number}", # Composite UUID
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
logger.debug(
|
|
1322
|
+
f"[PIN_DISCOVERY] Created PinInfo for pin {pin.number}: "
|
|
1323
|
+
f"type={pin_info.electrical_type.value}, shape={pin_info.shape.value}"
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
pins_info.append(pin_info)
|
|
1327
|
+
|
|
1328
|
+
logger.info(
|
|
1329
|
+
f"[PIN_DISCOVERY] Successfully retrieved {len(pins_info)} pins for {reference}"
|
|
1330
|
+
)
|
|
1331
|
+
return pins_info
|
|
1332
|
+
|
|
1333
|
+
# Bulk operations
|
|
412
1334
|
def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
|
|
413
1335
|
"""
|
|
414
|
-
|
|
1336
|
+
Update multiple components matching criteria.
|
|
415
1337
|
|
|
416
1338
|
Args:
|
|
417
|
-
criteria:
|
|
418
|
-
updates:
|
|
1339
|
+
criteria: Filter criteria (same as filter method)
|
|
1340
|
+
updates: Dictionary of property updates
|
|
419
1341
|
|
|
420
1342
|
Returns:
|
|
421
1343
|
Number of components updated
|
|
1344
|
+
|
|
1345
|
+
Example:
|
|
1346
|
+
# Update all 10k resistors to 1% tolerance
|
|
1347
|
+
count = sch.components.bulk_update(
|
|
1348
|
+
criteria={'value': '10k'},
|
|
1349
|
+
updates={'properties': {'Tolerance': '1%'}}
|
|
1350
|
+
)
|
|
1351
|
+
"""
|
|
1352
|
+
matching = self.filter(**criteria)
|
|
1353
|
+
|
|
1354
|
+
for component in matching:
|
|
1355
|
+
for key, value in updates.items():
|
|
1356
|
+
if key == "properties" and isinstance(value, dict):
|
|
1357
|
+
# Handle properties dictionary specially
|
|
1358
|
+
for prop_name, prop_value in value.items():
|
|
1359
|
+
component.set_property(prop_name, str(prop_value))
|
|
1360
|
+
elif hasattr(component, key) and key not in ["properties"]:
|
|
1361
|
+
setattr(component, key, value)
|
|
1362
|
+
else:
|
|
1363
|
+
# Add as custom property
|
|
1364
|
+
component.set_property(key, str(value))
|
|
1365
|
+
|
|
1366
|
+
if matching:
|
|
1367
|
+
self._mark_modified()
|
|
1368
|
+
|
|
1369
|
+
logger.info(f"Bulk updated {len(matching)} components")
|
|
1370
|
+
return len(matching)
|
|
1371
|
+
|
|
1372
|
+
# Sorting
|
|
1373
|
+
def sort_by_reference(self):
|
|
1374
|
+
"""Sort components by reference designator (in-place)."""
|
|
1375
|
+
self._items.sort(key=lambda c: c.reference)
|
|
1376
|
+
self._index_registry.mark_dirty()
|
|
1377
|
+
|
|
1378
|
+
def sort_by_position(self, by_x: bool = True):
|
|
1379
|
+
"""
|
|
1380
|
+
Sort components by position (in-place).
|
|
1381
|
+
|
|
1382
|
+
Args:
|
|
1383
|
+
by_x: If True, sort by X then Y; if False, sort by Y then X
|
|
1384
|
+
"""
|
|
1385
|
+
if by_x:
|
|
1386
|
+
self._items.sort(key=lambda c: (c.position.x, c.position.y))
|
|
1387
|
+
else:
|
|
1388
|
+
self._items.sort(key=lambda c: (c.position.y, c.position.x))
|
|
1389
|
+
self._index_registry.mark_dirty()
|
|
1390
|
+
|
|
1391
|
+
# Validation
|
|
1392
|
+
def validate_all(self) -> List[ValidationIssue]:
|
|
422
1393
|
"""
|
|
423
|
-
|
|
1394
|
+
Validate all components in collection.
|
|
424
1395
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
1396
|
+
Returns:
|
|
1397
|
+
List of validation issues found
|
|
1398
|
+
"""
|
|
1399
|
+
all_issues = []
|
|
1400
|
+
validator = SchematicValidator()
|
|
429
1401
|
|
|
430
|
-
|
|
431
|
-
self.
|
|
1402
|
+
# Validate individual components
|
|
1403
|
+
for component in self._items:
|
|
1404
|
+
issues = component.validate()
|
|
1405
|
+
all_issues.extend(issues)
|
|
1406
|
+
|
|
1407
|
+
# Validate collection-level rules (e.g., duplicate references)
|
|
1408
|
+
self._ensure_indexes_current()
|
|
1409
|
+
references = [c.reference for c in self._items]
|
|
1410
|
+
if len(references) != len(set(references)):
|
|
1411
|
+
# Find duplicates
|
|
1412
|
+
seen = set()
|
|
1413
|
+
duplicates = set()
|
|
1414
|
+
for ref in references:
|
|
1415
|
+
if ref in seen:
|
|
1416
|
+
duplicates.add(ref)
|
|
1417
|
+
seen.add(ref)
|
|
1418
|
+
|
|
1419
|
+
for ref in duplicates:
|
|
1420
|
+
all_issues.append(
|
|
1421
|
+
ValidationIssue(
|
|
1422
|
+
category="reference", message=f"Duplicate reference: {ref}", level="error"
|
|
1423
|
+
)
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
return all_issues
|
|
1427
|
+
|
|
1428
|
+
# Statistics
|
|
1429
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
1430
|
+
"""
|
|
1431
|
+
Get collection statistics.
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
Dictionary with component statistics
|
|
1435
|
+
"""
|
|
1436
|
+
lib_counts = {}
|
|
1437
|
+
value_counts = {}
|
|
1438
|
+
|
|
1439
|
+
for component in self._items:
|
|
1440
|
+
# Count by library
|
|
1441
|
+
lib = component.library
|
|
1442
|
+
lib_counts[lib] = lib_counts.get(lib, 0) + 1
|
|
1443
|
+
|
|
1444
|
+
# Count by value
|
|
1445
|
+
value = component.value
|
|
1446
|
+
if value:
|
|
1447
|
+
value_counts[value] = value_counts.get(value, 0) + 1
|
|
1448
|
+
|
|
1449
|
+
# Get base statistics and extend
|
|
1450
|
+
base_stats = super().get_statistics()
|
|
1451
|
+
base_stats.update(
|
|
1452
|
+
{
|
|
1453
|
+
"unique_references": len(self._items), # After rebuild, should equal item_count
|
|
1454
|
+
"libraries_used": len(lib_counts),
|
|
1455
|
+
"library_breakdown": lib_counts,
|
|
1456
|
+
"most_common_values": sorted(
|
|
1457
|
+
value_counts.items(), key=lambda x: x[1], reverse=True
|
|
1458
|
+
)[:10],
|
|
1459
|
+
}
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
return base_stats
|
|
1463
|
+
|
|
1464
|
+
# Collection interface
|
|
1465
|
+
def __getitem__(self, key: Union[int, str]) -> Component:
|
|
1466
|
+
"""
|
|
1467
|
+
Get component by index, UUID, or reference.
|
|
1468
|
+
|
|
1469
|
+
Args:
|
|
1470
|
+
key: Integer index, UUID string, or reference string
|
|
1471
|
+
|
|
1472
|
+
Returns:
|
|
1473
|
+
Component at the specified location
|
|
1474
|
+
|
|
1475
|
+
Raises:
|
|
1476
|
+
KeyError: If UUID or reference not found
|
|
1477
|
+
IndexError: If index out of range
|
|
1478
|
+
TypeError: If key is invalid type
|
|
1479
|
+
"""
|
|
1480
|
+
if isinstance(key, int):
|
|
1481
|
+
# Integer index
|
|
1482
|
+
return self._items[key]
|
|
1483
|
+
elif isinstance(key, str):
|
|
1484
|
+
# Try reference first (most common)
|
|
1485
|
+
component = self.get(key)
|
|
1486
|
+
if component is not None:
|
|
1487
|
+
return component
|
|
1488
|
+
|
|
1489
|
+
# Try UUID
|
|
1490
|
+
component = self.get_by_uuid(key)
|
|
1491
|
+
if component is not None:
|
|
1492
|
+
return component
|
|
1493
|
+
|
|
1494
|
+
raise KeyError(f"Component not found: {key}")
|
|
1495
|
+
else:
|
|
1496
|
+
raise TypeError(f"Invalid key type: {type(key).__name__}")
|
|
1497
|
+
|
|
1498
|
+
def __contains__(self, item: Union[str, Component]) -> bool:
|
|
1499
|
+
"""
|
|
1500
|
+
Check if reference, UUID, or component exists in collection.
|
|
1501
|
+
|
|
1502
|
+
Args:
|
|
1503
|
+
item: Reference string, UUID string, or Component instance
|
|
1504
|
+
|
|
1505
|
+
Returns:
|
|
1506
|
+
True if item exists, False otherwise
|
|
1507
|
+
"""
|
|
1508
|
+
if isinstance(item, str):
|
|
1509
|
+
# Check reference or UUID
|
|
1510
|
+
return self.get(item) is not None or self.get_by_uuid(item) is not None
|
|
1511
|
+
elif isinstance(item, Component):
|
|
1512
|
+
# Check by UUID
|
|
1513
|
+
return item.uuid in self
|
|
1514
|
+
else:
|
|
1515
|
+
return False
|
|
1516
|
+
|
|
1517
|
+
# Internal helper methods
|
|
1518
|
+
def _add_to_manual_indexes(self, component: Component):
|
|
1519
|
+
"""Add component to manual indexes (lib_id, value)."""
|
|
1520
|
+
# Add to lib_id index (non-unique)
|
|
1521
|
+
lib_id = component.lib_id
|
|
1522
|
+
if lib_id not in self._lib_id_index:
|
|
1523
|
+
self._lib_id_index[lib_id] = []
|
|
1524
|
+
self._lib_id_index[lib_id].append(component)
|
|
1525
|
+
|
|
1526
|
+
# Add to value index (non-unique)
|
|
1527
|
+
value = component.value
|
|
1528
|
+
if value:
|
|
1529
|
+
if value not in self._value_index:
|
|
1530
|
+
self._value_index[value] = []
|
|
1531
|
+
self._value_index[value].append(component)
|
|
1532
|
+
|
|
1533
|
+
def _remove_from_manual_indexes(self, component: Component):
|
|
1534
|
+
"""Remove component from manual indexes (lib_id, value)."""
|
|
1535
|
+
# Remove from lib_id index
|
|
1536
|
+
lib_id = component.lib_id
|
|
1537
|
+
if lib_id in self._lib_id_index:
|
|
1538
|
+
self._lib_id_index[lib_id].remove(component)
|
|
1539
|
+
if not self._lib_id_index[lib_id]:
|
|
1540
|
+
del self._lib_id_index[lib_id]
|
|
1541
|
+
|
|
1542
|
+
# Remove from value index
|
|
1543
|
+
value = component.value
|
|
1544
|
+
if value and value in self._value_index:
|
|
1545
|
+
self._value_index[value].remove(component)
|
|
1546
|
+
if not self._value_index[value]:
|
|
1547
|
+
del self._value_index[value]
|
|
1548
|
+
|
|
1549
|
+
def _update_reference_index(self, old_ref: str, new_ref: str):
|
|
1550
|
+
"""
|
|
1551
|
+
Update reference index when component reference changes.
|
|
1552
|
+
|
|
1553
|
+
This marks the index as dirty so it will be rebuilt with the new reference.
|
|
1554
|
+
"""
|
|
1555
|
+
self._index_registry.mark_dirty()
|
|
1556
|
+
logger.debug(f"Reference index marked dirty: {old_ref} -> {new_ref}")
|
|
1557
|
+
|
|
1558
|
+
def _update_value_index(self, component: Component, old_value: str, new_value: str):
|
|
1559
|
+
"""Update value index when component value changes."""
|
|
1560
|
+
# Remove from old value
|
|
1561
|
+
if old_value and old_value in self._value_index:
|
|
1562
|
+
self._value_index[old_value].remove(component)
|
|
1563
|
+
if not self._value_index[old_value]:
|
|
1564
|
+
del self._value_index[old_value]
|
|
1565
|
+
|
|
1566
|
+
# Add to new value
|
|
1567
|
+
if new_value:
|
|
1568
|
+
if new_value not in self._value_index:
|
|
1569
|
+
self._value_index[new_value] = []
|
|
1570
|
+
self._value_index[new_value].append(component)
|
|
1571
|
+
|
|
1572
|
+
def _generate_reference(self, lib_id: str) -> str:
|
|
1573
|
+
"""
|
|
1574
|
+
Generate unique reference for component.
|
|
1575
|
+
|
|
1576
|
+
Args:
|
|
1577
|
+
lib_id: Library identifier to determine prefix
|
|
1578
|
+
|
|
1579
|
+
Returns:
|
|
1580
|
+
Generated reference (e.g., "R1", "U2")
|
|
1581
|
+
"""
|
|
1582
|
+
# Get reference prefix from symbol definition
|
|
1583
|
+
symbol_cache = get_symbol_cache()
|
|
1584
|
+
symbol_def = symbol_cache.get_symbol(lib_id)
|
|
1585
|
+
prefix = symbol_def.reference_prefix if symbol_def else "U"
|
|
1586
|
+
|
|
1587
|
+
# Ensure indexes are current
|
|
1588
|
+
self._ensure_indexes_current()
|
|
1589
|
+
|
|
1590
|
+
# Find next available number
|
|
1591
|
+
counter = 1
|
|
1592
|
+
while self._index_registry.has_key("reference", f"{prefix}{counter}"):
|
|
1593
|
+
counter += 1
|
|
1594
|
+
|
|
1595
|
+
return f"{prefix}{counter}"
|
|
1596
|
+
|
|
1597
|
+
def _find_available_position(self) -> Point:
|
|
1598
|
+
"""
|
|
1599
|
+
Find an available position for automatic placement.
|
|
1600
|
+
|
|
1601
|
+
Uses simple grid layout algorithm.
|
|
1602
|
+
|
|
1603
|
+
Returns:
|
|
1604
|
+
Point for component placement
|
|
1605
|
+
"""
|
|
1606
|
+
# Simple grid placement - could be enhanced with collision detection
|
|
1607
|
+
grid_size = 10.0 # 10mm grid
|
|
1608
|
+
max_per_row = 10
|
|
1609
|
+
|
|
1610
|
+
row = len(self._items) // max_per_row
|
|
1611
|
+
col = len(self._items) % max_per_row
|
|
1612
|
+
|
|
1613
|
+
return Point(col * grid_size, row * grid_size)
|
|
1614
|
+
|
|
1615
|
+
# Compatibility methods for legacy Schematic integration
|
|
1616
|
+
@property
|
|
1617
|
+
def modified(self) -> bool:
|
|
1618
|
+
"""Check if collection has been modified (compatibility)."""
|
|
1619
|
+
return self.is_modified
|
|
432
1620
|
|
|
433
|
-
|
|
434
|
-
|
|
1621
|
+
def mark_saved(self) -> None:
|
|
1622
|
+
"""Mark collection as saved (reset modified flag)."""
|
|
1623
|
+
self.mark_clean()
|