kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__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/__init__.py +2 -2
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +5 -0
- kicad_sch_api/core/components.py +142 -47
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +22 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +28 -52
- kicad_sch_api/core/managers/file_io.py +3 -2
- kicad_sch_api/core/managers/metadata.py +6 -5
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +7 -1
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +29 -53
- kicad_sch_api/core/parser.py +75 -1765
- kicad_sch_api/core/schematic.py +211 -148
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +59 -18
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +194 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -222
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base collection class for schematic elements.
|
|
3
|
+
|
|
4
|
+
Provides common functionality for all collection types including UUID indexing,
|
|
5
|
+
modification tracking, and standard collection operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Callable, Dict, Generic, Iterator, List, Optional, Protocol, TypeVar
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HasUUID(Protocol):
|
|
15
|
+
"""Protocol for objects that have a UUID attribute."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def uuid(self) -> str:
|
|
19
|
+
"""UUID of the object."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T", bound=HasUUID)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseCollection(Generic[T]):
|
|
27
|
+
"""
|
|
28
|
+
Generic base class for schematic element collections.
|
|
29
|
+
|
|
30
|
+
Provides common functionality:
|
|
31
|
+
- UUID-based indexing for fast lookup
|
|
32
|
+
- Modification tracking
|
|
33
|
+
- Standard collection operations (__len__, __iter__, __getitem__)
|
|
34
|
+
- Index rebuilding and management
|
|
35
|
+
|
|
36
|
+
Type parameter T must implement the HasUUID protocol.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, items: Optional[List[T]] = None, collection_name: str = "items") -> None:
|
|
40
|
+
"""
|
|
41
|
+
Initialize base collection.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
items: Initial list of items
|
|
45
|
+
collection_name: Name for logging (e.g., "wires", "junctions")
|
|
46
|
+
"""
|
|
47
|
+
self._items: List[T] = items or []
|
|
48
|
+
self._uuid_index: Dict[str, int] = {}
|
|
49
|
+
self._modified = False
|
|
50
|
+
self._collection_name = collection_name
|
|
51
|
+
|
|
52
|
+
# Build UUID index
|
|
53
|
+
self._rebuild_index()
|
|
54
|
+
|
|
55
|
+
logger.debug(f"{collection_name} collection initialized with {len(self._items)} items")
|
|
56
|
+
|
|
57
|
+
def _rebuild_index(self) -> None:
|
|
58
|
+
"""Rebuild UUID index for fast lookups."""
|
|
59
|
+
self._uuid_index = {item.uuid: i for i, item in enumerate(self._items)}
|
|
60
|
+
|
|
61
|
+
def _mark_modified(self) -> None:
|
|
62
|
+
"""Mark collection as modified."""
|
|
63
|
+
self._modified = True
|
|
64
|
+
|
|
65
|
+
def is_modified(self) -> bool:
|
|
66
|
+
"""Check if collection has been modified."""
|
|
67
|
+
return self._modified
|
|
68
|
+
|
|
69
|
+
def reset_modified_flag(self) -> None:
|
|
70
|
+
"""Reset modified flag (typically after save)."""
|
|
71
|
+
self._modified = False
|
|
72
|
+
|
|
73
|
+
# Standard collection protocol methods
|
|
74
|
+
def __len__(self) -> int:
|
|
75
|
+
"""Return number of items in collection."""
|
|
76
|
+
return len(self._items)
|
|
77
|
+
|
|
78
|
+
def __iter__(self) -> Iterator[T]:
|
|
79
|
+
"""Iterate over items in collection."""
|
|
80
|
+
return iter(self._items)
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, key: Any) -> T:
|
|
83
|
+
"""
|
|
84
|
+
Get item by UUID or index.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
key: UUID string or integer index
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Item at the specified location
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
KeyError: If UUID not found
|
|
94
|
+
IndexError: If index out of range
|
|
95
|
+
TypeError: If key is neither string nor int
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(key, str):
|
|
98
|
+
# UUID lookup
|
|
99
|
+
if key not in self._uuid_index:
|
|
100
|
+
raise KeyError(f"Item with UUID '{key}' not found")
|
|
101
|
+
return self._items[self._uuid_index[key]]
|
|
102
|
+
elif isinstance(key, int):
|
|
103
|
+
# Index lookup
|
|
104
|
+
return self._items[key]
|
|
105
|
+
else:
|
|
106
|
+
raise TypeError(f"Key must be string (UUID) or int (index), got {type(key)}")
|
|
107
|
+
|
|
108
|
+
def __contains__(self, key: Any) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Check if item exists in collection.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
key: UUID string or item object
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if item exists
|
|
117
|
+
"""
|
|
118
|
+
if isinstance(key, str):
|
|
119
|
+
return key in self._uuid_index
|
|
120
|
+
elif hasattr(key, "uuid"):
|
|
121
|
+
return key.uuid in self._uuid_index
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def get(self, uuid: str) -> Optional[T]:
|
|
125
|
+
"""
|
|
126
|
+
Get item by UUID.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
uuid: Item UUID
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Item if found, None otherwise
|
|
133
|
+
"""
|
|
134
|
+
if uuid not in self._uuid_index:
|
|
135
|
+
return None
|
|
136
|
+
return self._items[self._uuid_index[uuid]]
|
|
137
|
+
|
|
138
|
+
def remove(self, uuid: str) -> bool:
|
|
139
|
+
"""
|
|
140
|
+
Remove item by UUID.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
uuid: UUID of item to remove
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if item was removed, False if not found
|
|
147
|
+
"""
|
|
148
|
+
if uuid not in self._uuid_index:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
index = self._uuid_index[uuid]
|
|
152
|
+
del self._items[index]
|
|
153
|
+
self._rebuild_index()
|
|
154
|
+
self._mark_modified()
|
|
155
|
+
|
|
156
|
+
logger.debug(f"Removed item with UUID {uuid} from {self._collection_name}")
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
def clear(self) -> None:
|
|
160
|
+
"""Remove all items from collection."""
|
|
161
|
+
self._items.clear()
|
|
162
|
+
self._uuid_index.clear()
|
|
163
|
+
self._mark_modified()
|
|
164
|
+
logger.debug(f"Cleared all items from {self._collection_name}")
|
|
165
|
+
|
|
166
|
+
def find(self, predicate: Callable[[T], bool]) -> List[T]:
|
|
167
|
+
"""
|
|
168
|
+
Find all items matching a predicate.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
predicate: Function that returns True for matching items
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of matching items
|
|
175
|
+
"""
|
|
176
|
+
return [item for item in self._items if predicate(item)]
|
|
177
|
+
|
|
178
|
+
def filter(self, **criteria) -> List[T]:
|
|
179
|
+
"""
|
|
180
|
+
Filter items by attribute values.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
**criteria: Attribute name/value pairs to match
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of matching items
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
collection.filter(wire_type=WireType.BUS, stroke_width=0.5)
|
|
190
|
+
"""
|
|
191
|
+
matches = []
|
|
192
|
+
for item in self._items:
|
|
193
|
+
if all(getattr(item, key, None) == value for key, value in criteria.items()):
|
|
194
|
+
matches.append(item)
|
|
195
|
+
return matches
|
|
196
|
+
|
|
197
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
198
|
+
"""
|
|
199
|
+
Get collection statistics.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Dictionary with statistics
|
|
203
|
+
"""
|
|
204
|
+
return {
|
|
205
|
+
"total_items": len(self._items),
|
|
206
|
+
"modified": self._modified,
|
|
207
|
+
"indexed_items": len(self._uuid_index),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
def _add_item(self, item: T) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Add item to internal storage and index.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
item: Item to add
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ValueError: If item UUID already exists
|
|
219
|
+
"""
|
|
220
|
+
if item.uuid in self._uuid_index:
|
|
221
|
+
raise ValueError(f"Item with UUID '{item.uuid}' already exists")
|
|
222
|
+
|
|
223
|
+
self._items.append(item)
|
|
224
|
+
self._uuid_index[item.uuid] = len(self._items) - 1
|
|
225
|
+
self._mark_modified()
|
|
226
|
+
|
|
227
|
+
def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
|
|
228
|
+
"""
|
|
229
|
+
Update multiple items matching criteria.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
criteria: Attribute name/value pairs to match
|
|
233
|
+
updates: Attribute name/value pairs to update
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Number of items updated
|
|
237
|
+
"""
|
|
238
|
+
matching_items = self.filter(**criteria)
|
|
239
|
+
for item in matching_items:
|
|
240
|
+
for key, value in updates.items():
|
|
241
|
+
if hasattr(item, key):
|
|
242
|
+
setattr(item, key, value)
|
|
243
|
+
|
|
244
|
+
if matching_items:
|
|
245
|
+
self._mark_modified()
|
|
246
|
+
|
|
247
|
+
logger.debug(f"Bulk updated {len(matching_items)} items in {self._collection_name}")
|
|
248
|
+
return len(matching_items)
|
|
@@ -384,6 +384,11 @@ def get_component_bounding_box(
|
|
|
384
384
|
|
|
385
385
|
# Transform to world coordinates
|
|
386
386
|
# TODO: Handle component rotation in the future
|
|
387
|
+
# NOTE: Currently assumes 0° rotation. For rotated components, bounding box
|
|
388
|
+
# would need to be recalculated after applying rotation matrix. This is a
|
|
389
|
+
# known limitation but doesn't affect most use cases since components are
|
|
390
|
+
# typically placed without rotation, and routing avoids components regardless.
|
|
391
|
+
# Priority: LOW - Would improve accuracy for rotated component placement validation
|
|
387
392
|
world_bbox = BoundingBox(
|
|
388
393
|
component.position.x + symbol_bbox.min_x,
|
|
389
394
|
component.position.y + symbol_bbox.min_y,
|
kicad_sch_api/core/components.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
|
11
11
|
|
|
12
12
|
from ..library.cache import SymbolDefinition, get_symbol_cache
|
|
13
13
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
14
|
+
from .collections import BaseCollection
|
|
14
15
|
from .ic_manager import ICManager
|
|
15
16
|
from .types import Point, SchematicPin, SchematicSymbol
|
|
16
17
|
|
|
@@ -261,10 +262,13 @@ class Component:
|
|
|
261
262
|
)
|
|
262
263
|
|
|
263
264
|
|
|
264
|
-
class ComponentCollection:
|
|
265
|
+
class ComponentCollection(BaseCollection[Component]):
|
|
265
266
|
"""
|
|
266
267
|
Collection class for efficient component management.
|
|
267
268
|
|
|
269
|
+
Inherits from BaseCollection for standard operations and adds component-specific
|
|
270
|
+
functionality including reference, lib_id, and value-based indexing.
|
|
271
|
+
|
|
268
272
|
Provides fast lookup, filtering, and bulk operations for schematic components.
|
|
269
273
|
Optimized for schematics with hundreds or thousands of components.
|
|
270
274
|
"""
|
|
@@ -276,19 +280,19 @@ class ComponentCollection:
|
|
|
276
280
|
Args:
|
|
277
281
|
components: Initial list of component data
|
|
278
282
|
"""
|
|
279
|
-
|
|
283
|
+
# Initialize base collection
|
|
284
|
+
super().__init__([], collection_name="components")
|
|
285
|
+
|
|
286
|
+
# Additional component-specific indexes
|
|
280
287
|
self._reference_index: Dict[str, Component] = {}
|
|
281
288
|
self._lib_id_index: Dict[str, List[Component]] = {}
|
|
282
289
|
self._value_index: Dict[str, List[Component]] = {}
|
|
283
|
-
self._modified = False
|
|
284
290
|
|
|
285
291
|
# Add initial components
|
|
286
292
|
if components:
|
|
287
293
|
for comp_data in components:
|
|
288
294
|
self._add_to_indexes(Component(comp_data, self))
|
|
289
295
|
|
|
290
|
-
logger.debug(f"ComponentCollection initialized with {len(self._components)} components")
|
|
291
|
-
|
|
292
296
|
def add(
|
|
293
297
|
self,
|
|
294
298
|
lib_id: str,
|
|
@@ -375,7 +379,7 @@ class ComponentCollection:
|
|
|
375
379
|
|
|
376
380
|
# Add to collection
|
|
377
381
|
self._add_to_indexes(component)
|
|
378
|
-
self.
|
|
382
|
+
self._mark_modified()
|
|
379
383
|
|
|
380
384
|
logger.info(f"Added component: {reference} ({lib_id})")
|
|
381
385
|
return component
|
|
@@ -432,7 +436,7 @@ class ComponentCollection:
|
|
|
432
436
|
component = Component(component_data, self)
|
|
433
437
|
self._add_to_indexes(component)
|
|
434
438
|
|
|
435
|
-
self.
|
|
439
|
+
self._mark_modified()
|
|
436
440
|
logger.info(
|
|
437
441
|
f"Added multi-unit IC: {reference_prefix} ({lib_id}) with {len(unit_components)} units"
|
|
438
442
|
)
|
|
@@ -444,22 +448,102 @@ class ComponentCollection:
|
|
|
444
448
|
Remove component by reference.
|
|
445
449
|
|
|
446
450
|
Args:
|
|
447
|
-
reference: Component reference to remove
|
|
451
|
+
reference: Component reference to remove (e.g., "R1")
|
|
448
452
|
|
|
449
453
|
Returns:
|
|
450
|
-
True if component was removed
|
|
454
|
+
True if component was removed, False if not found
|
|
455
|
+
|
|
456
|
+
Raises:
|
|
457
|
+
TypeError: If reference is not a string
|
|
458
|
+
|
|
459
|
+
Examples:
|
|
460
|
+
sch.components.remove("R1")
|
|
461
|
+
sch.components.remove("C2")
|
|
462
|
+
|
|
463
|
+
Note:
|
|
464
|
+
For removing by UUID or component object, use remove_by_uuid() or remove_component()
|
|
465
|
+
respectively. This maintains a clear, simple API contract.
|
|
451
466
|
"""
|
|
467
|
+
if not isinstance(reference, str):
|
|
468
|
+
raise TypeError(f"reference must be a string, not {type(reference).__name__}")
|
|
469
|
+
|
|
452
470
|
component = self._reference_index.get(reference)
|
|
453
471
|
if not component:
|
|
454
472
|
return False
|
|
455
473
|
|
|
456
|
-
# Remove from
|
|
474
|
+
# Remove from component-specific indexes
|
|
457
475
|
self._remove_from_indexes(component)
|
|
458
|
-
|
|
476
|
+
|
|
477
|
+
# Remove from base collection using UUID
|
|
478
|
+
super().remove(component.uuid)
|
|
459
479
|
|
|
460
480
|
logger.info(f"Removed component: {reference}")
|
|
461
481
|
return True
|
|
462
482
|
|
|
483
|
+
def remove_by_uuid(self, component_uuid: str) -> bool:
|
|
484
|
+
"""
|
|
485
|
+
Remove component by UUID.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
component_uuid: Component UUID to remove
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
True if component was removed, False if not found
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
TypeError: If UUID is not a string
|
|
495
|
+
"""
|
|
496
|
+
if not isinstance(component_uuid, str):
|
|
497
|
+
raise TypeError(f"component_uuid must be a string, not {type(component_uuid).__name__}")
|
|
498
|
+
|
|
499
|
+
if component_uuid not in self._uuid_index:
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
component = self._items[self._uuid_index[component_uuid]]
|
|
503
|
+
|
|
504
|
+
# Remove from component-specific indexes
|
|
505
|
+
self._remove_from_indexes(component)
|
|
506
|
+
|
|
507
|
+
# Remove from base collection
|
|
508
|
+
super().remove(component_uuid)
|
|
509
|
+
|
|
510
|
+
logger.info(f"Removed component by UUID: {component_uuid}")
|
|
511
|
+
return True
|
|
512
|
+
|
|
513
|
+
def remove_component(self, component: "Component") -> bool:
|
|
514
|
+
"""
|
|
515
|
+
Remove component by component object.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
component: Component object to remove
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
True if component was removed, False if not found
|
|
522
|
+
|
|
523
|
+
Raises:
|
|
524
|
+
TypeError: If component is not a Component instance
|
|
525
|
+
|
|
526
|
+
Examples:
|
|
527
|
+
comp = sch.components.get("R1")
|
|
528
|
+
sch.components.remove_component(comp)
|
|
529
|
+
"""
|
|
530
|
+
if not isinstance(component, Component):
|
|
531
|
+
raise TypeError(
|
|
532
|
+
f"component must be a Component instance, not {type(component).__name__}"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
if component.uuid not in self._uuid_index:
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
# Remove from component-specific indexes
|
|
539
|
+
self._remove_from_indexes(component)
|
|
540
|
+
|
|
541
|
+
# Remove from base collection
|
|
542
|
+
super().remove(component.uuid)
|
|
543
|
+
|
|
544
|
+
logger.info(f"Removed component: {component.reference}")
|
|
545
|
+
return True
|
|
546
|
+
|
|
463
547
|
def get(self, reference: str) -> Optional[Component]:
|
|
464
548
|
"""Get component by reference."""
|
|
465
549
|
return self._reference_index.get(reference)
|
|
@@ -479,7 +563,7 @@ class ComponentCollection:
|
|
|
479
563
|
Returns:
|
|
480
564
|
List of matching components
|
|
481
565
|
"""
|
|
482
|
-
results = list(self.
|
|
566
|
+
results = list(self._items)
|
|
483
567
|
|
|
484
568
|
# Apply filters
|
|
485
569
|
if "lib_id" in criteria:
|
|
@@ -517,7 +601,7 @@ class ComponentCollection:
|
|
|
517
601
|
def filter_by_type(self, component_type: str) -> List[Component]:
|
|
518
602
|
"""Filter components by type (e.g., 'R' for resistors)."""
|
|
519
603
|
return [
|
|
520
|
-
c for c in self.
|
|
604
|
+
c for c in self._items if c.symbol_name.upper().startswith(component_type.upper())
|
|
521
605
|
]
|
|
522
606
|
|
|
523
607
|
def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
|
|
@@ -532,7 +616,7 @@ class ComponentCollection:
|
|
|
532
616
|
point = Point(point[0], point[1])
|
|
533
617
|
|
|
534
618
|
results = []
|
|
535
|
-
for component in self.
|
|
619
|
+
for component in self._items:
|
|
536
620
|
if component.position.distance_to(point) <= radius:
|
|
537
621
|
results.append(component)
|
|
538
622
|
return results
|
|
@@ -564,21 +648,21 @@ class ComponentCollection:
|
|
|
564
648
|
component.set_property(key, str(value))
|
|
565
649
|
|
|
566
650
|
if matching:
|
|
567
|
-
self.
|
|
651
|
+
self._mark_modified()
|
|
568
652
|
|
|
569
653
|
logger.info(f"Bulk updated {len(matching)} components")
|
|
570
654
|
return len(matching)
|
|
571
655
|
|
|
572
656
|
def sort_by_reference(self):
|
|
573
657
|
"""Sort components by reference designator."""
|
|
574
|
-
self.
|
|
658
|
+
self._items.sort(key=lambda c: c.reference)
|
|
575
659
|
|
|
576
660
|
def sort_by_position(self, by_x: bool = True):
|
|
577
661
|
"""Sort components by position."""
|
|
578
662
|
if by_x:
|
|
579
|
-
self.
|
|
663
|
+
self._items.sort(key=lambda c: (c.position.x, c.position.y))
|
|
580
664
|
else:
|
|
581
|
-
self.
|
|
665
|
+
self._items.sort(key=lambda c: (c.position.y, c.position.x))
|
|
582
666
|
|
|
583
667
|
def validate_all(self) -> List[ValidationIssue]:
|
|
584
668
|
"""Validate all components in collection."""
|
|
@@ -586,12 +670,12 @@ class ComponentCollection:
|
|
|
586
670
|
validator = SchematicValidator()
|
|
587
671
|
|
|
588
672
|
# Validate individual components
|
|
589
|
-
for component in self.
|
|
673
|
+
for component in self._items:
|
|
590
674
|
issues = component.validate()
|
|
591
675
|
all_issues.extend(issues)
|
|
592
676
|
|
|
593
677
|
# Validate collection-level rules
|
|
594
|
-
references = [c.reference for c in self.
|
|
678
|
+
references = [c.reference for c in self._items]
|
|
595
679
|
if len(references) != len(set(references)):
|
|
596
680
|
# Find duplicates
|
|
597
681
|
seen = set()
|
|
@@ -615,7 +699,7 @@ class ComponentCollection:
|
|
|
615
699
|
lib_counts = {}
|
|
616
700
|
value_counts = {}
|
|
617
701
|
|
|
618
|
-
for component in self.
|
|
702
|
+
for component in self._items:
|
|
619
703
|
# Count by library
|
|
620
704
|
lib = component.library
|
|
621
705
|
lib_counts[lib] = lib_counts.get(lib, 0) + 1
|
|
@@ -626,36 +710,47 @@ class ComponentCollection:
|
|
|
626
710
|
value_counts[value] = value_counts.get(value, 0) + 1
|
|
627
711
|
|
|
628
712
|
return {
|
|
629
|
-
"total_components": len(self.
|
|
713
|
+
"total_components": len(self._items),
|
|
630
714
|
"unique_references": len(self._reference_index),
|
|
631
715
|
"libraries_used": len(lib_counts),
|
|
632
716
|
"library_breakdown": lib_counts,
|
|
633
717
|
"most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
|
|
634
718
|
:10
|
|
635
719
|
],
|
|
636
|
-
"modified": self.
|
|
720
|
+
"modified": self.is_modified(),
|
|
637
721
|
}
|
|
638
722
|
|
|
639
723
|
# Collection interface
|
|
640
|
-
|
|
641
|
-
"""Number of components."""
|
|
642
|
-
return len(self._components)
|
|
643
|
-
|
|
644
|
-
def __iter__(self) -> Iterator[Component]:
|
|
645
|
-
"""Iterate over components."""
|
|
646
|
-
return iter(self._components)
|
|
724
|
+
# __len__, __iter__ inherited from BaseCollection
|
|
647
725
|
|
|
648
726
|
def __getitem__(self, key: Union[int, str]) -> Component:
|
|
649
|
-
"""
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
727
|
+
"""
|
|
728
|
+
Get component by index, UUID, or reference.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
key: Integer index, UUID string, or reference string
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
Component at the specified location
|
|
735
|
+
|
|
736
|
+
Raises:
|
|
737
|
+
KeyError: If UUID or reference not found
|
|
738
|
+
IndexError: If index out of range
|
|
739
|
+
TypeError: If key is invalid type
|
|
740
|
+
"""
|
|
741
|
+
if isinstance(key, str):
|
|
742
|
+
# Try reference first (most common use case)
|
|
653
743
|
component = self._reference_index.get(key)
|
|
654
|
-
if component is None:
|
|
744
|
+
if component is not None:
|
|
745
|
+
return component
|
|
746
|
+
# Fall back to UUID lookup (from base class)
|
|
747
|
+
try:
|
|
748
|
+
return super().__getitem__(key)
|
|
749
|
+
except KeyError:
|
|
655
750
|
raise KeyError(f"Component not found: {key}")
|
|
656
|
-
return component
|
|
657
751
|
else:
|
|
658
|
-
|
|
752
|
+
# Integer index (from base class)
|
|
753
|
+
return super().__getitem__(key)
|
|
659
754
|
|
|
660
755
|
def __contains__(self, reference: str) -> bool:
|
|
661
756
|
"""Check if reference exists."""
|
|
@@ -663,8 +758,11 @@ class ComponentCollection:
|
|
|
663
758
|
|
|
664
759
|
# Internal methods
|
|
665
760
|
def _add_to_indexes(self, component: Component):
|
|
666
|
-
"""Add component to all indexes."""
|
|
667
|
-
|
|
761
|
+
"""Add component to all indexes (base + component-specific)."""
|
|
762
|
+
# Add to base collection (UUID index)
|
|
763
|
+
self._add_item(component)
|
|
764
|
+
|
|
765
|
+
# Add to reference index
|
|
668
766
|
self._reference_index[component.reference] = component
|
|
669
767
|
|
|
670
768
|
# Add to lib_id index
|
|
@@ -681,8 +779,8 @@ class ComponentCollection:
|
|
|
681
779
|
self._value_index[value].append(component)
|
|
682
780
|
|
|
683
781
|
def _remove_from_indexes(self, component: Component):
|
|
684
|
-
"""Remove component from
|
|
685
|
-
|
|
782
|
+
"""Remove component from component-specific indexes (not base UUID index)."""
|
|
783
|
+
# Remove from reference index
|
|
686
784
|
del self._reference_index[component.reference]
|
|
687
785
|
|
|
688
786
|
# Remove from lib_id index
|
|
@@ -705,10 +803,7 @@ class ComponentCollection:
|
|
|
705
803
|
component = self._reference_index[old_ref]
|
|
706
804
|
del self._reference_index[old_ref]
|
|
707
805
|
self._reference_index[new_ref] = component
|
|
708
|
-
|
|
709
|
-
def _mark_modified(self):
|
|
710
|
-
"""Mark collection as modified."""
|
|
711
|
-
self._modified = True
|
|
806
|
+
# Note: UUID doesn't change when reference changes, so base index is unaffected
|
|
712
807
|
|
|
713
808
|
def _generate_reference(self, lib_id: str) -> str:
|
|
714
809
|
"""Generate unique reference for component."""
|
|
@@ -730,7 +825,7 @@ class ComponentCollection:
|
|
|
730
825
|
grid_size = 10.0 # 10mm grid
|
|
731
826
|
max_per_row = 10
|
|
732
827
|
|
|
733
|
-
row = len(self.
|
|
734
|
-
col = len(self.
|
|
828
|
+
row = len(self._items) // max_per_row
|
|
829
|
+
col = len(self._items) % max_per_row
|
|
735
830
|
|
|
736
831
|
return Point(col * grid_size, row * grid_size)
|