kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.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 +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 +62 -45
- 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/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.1.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.1.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.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.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
|
)
|
|
@@ -453,9 +457,11 @@ class ComponentCollection:
|
|
|
453
457
|
if not component:
|
|
454
458
|
return False
|
|
455
459
|
|
|
456
|
-
# Remove from
|
|
460
|
+
# Remove from component-specific indexes
|
|
457
461
|
self._remove_from_indexes(component)
|
|
458
|
-
|
|
462
|
+
|
|
463
|
+
# Remove from base collection using UUID
|
|
464
|
+
super().remove(component.uuid)
|
|
459
465
|
|
|
460
466
|
logger.info(f"Removed component: {reference}")
|
|
461
467
|
return True
|
|
@@ -479,7 +485,7 @@ class ComponentCollection:
|
|
|
479
485
|
Returns:
|
|
480
486
|
List of matching components
|
|
481
487
|
"""
|
|
482
|
-
results = list(self.
|
|
488
|
+
results = list(self._items)
|
|
483
489
|
|
|
484
490
|
# Apply filters
|
|
485
491
|
if "lib_id" in criteria:
|
|
@@ -517,7 +523,7 @@ class ComponentCollection:
|
|
|
517
523
|
def filter_by_type(self, component_type: str) -> List[Component]:
|
|
518
524
|
"""Filter components by type (e.g., 'R' for resistors)."""
|
|
519
525
|
return [
|
|
520
|
-
c for c in self.
|
|
526
|
+
c for c in self._items if c.symbol_name.upper().startswith(component_type.upper())
|
|
521
527
|
]
|
|
522
528
|
|
|
523
529
|
def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
|
|
@@ -532,7 +538,7 @@ class ComponentCollection:
|
|
|
532
538
|
point = Point(point[0], point[1])
|
|
533
539
|
|
|
534
540
|
results = []
|
|
535
|
-
for component in self.
|
|
541
|
+
for component in self._items:
|
|
536
542
|
if component.position.distance_to(point) <= radius:
|
|
537
543
|
results.append(component)
|
|
538
544
|
return results
|
|
@@ -564,21 +570,21 @@ class ComponentCollection:
|
|
|
564
570
|
component.set_property(key, str(value))
|
|
565
571
|
|
|
566
572
|
if matching:
|
|
567
|
-
self.
|
|
573
|
+
self._mark_modified()
|
|
568
574
|
|
|
569
575
|
logger.info(f"Bulk updated {len(matching)} components")
|
|
570
576
|
return len(matching)
|
|
571
577
|
|
|
572
578
|
def sort_by_reference(self):
|
|
573
579
|
"""Sort components by reference designator."""
|
|
574
|
-
self.
|
|
580
|
+
self._items.sort(key=lambda c: c.reference)
|
|
575
581
|
|
|
576
582
|
def sort_by_position(self, by_x: bool = True):
|
|
577
583
|
"""Sort components by position."""
|
|
578
584
|
if by_x:
|
|
579
|
-
self.
|
|
585
|
+
self._items.sort(key=lambda c: (c.position.x, c.position.y))
|
|
580
586
|
else:
|
|
581
|
-
self.
|
|
587
|
+
self._items.sort(key=lambda c: (c.position.y, c.position.x))
|
|
582
588
|
|
|
583
589
|
def validate_all(self) -> List[ValidationIssue]:
|
|
584
590
|
"""Validate all components in collection."""
|
|
@@ -586,12 +592,12 @@ class ComponentCollection:
|
|
|
586
592
|
validator = SchematicValidator()
|
|
587
593
|
|
|
588
594
|
# Validate individual components
|
|
589
|
-
for component in self.
|
|
595
|
+
for component in self._items:
|
|
590
596
|
issues = component.validate()
|
|
591
597
|
all_issues.extend(issues)
|
|
592
598
|
|
|
593
599
|
# Validate collection-level rules
|
|
594
|
-
references = [c.reference for c in self.
|
|
600
|
+
references = [c.reference for c in self._items]
|
|
595
601
|
if len(references) != len(set(references)):
|
|
596
602
|
# Find duplicates
|
|
597
603
|
seen = set()
|
|
@@ -615,7 +621,7 @@ class ComponentCollection:
|
|
|
615
621
|
lib_counts = {}
|
|
616
622
|
value_counts = {}
|
|
617
623
|
|
|
618
|
-
for component in self.
|
|
624
|
+
for component in self._items:
|
|
619
625
|
# Count by library
|
|
620
626
|
lib = component.library
|
|
621
627
|
lib_counts[lib] = lib_counts.get(lib, 0) + 1
|
|
@@ -626,36 +632,47 @@ class ComponentCollection:
|
|
|
626
632
|
value_counts[value] = value_counts.get(value, 0) + 1
|
|
627
633
|
|
|
628
634
|
return {
|
|
629
|
-
"total_components": len(self.
|
|
635
|
+
"total_components": len(self._items),
|
|
630
636
|
"unique_references": len(self._reference_index),
|
|
631
637
|
"libraries_used": len(lib_counts),
|
|
632
638
|
"library_breakdown": lib_counts,
|
|
633
639
|
"most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
|
|
634
640
|
:10
|
|
635
641
|
],
|
|
636
|
-
"modified": self.
|
|
642
|
+
"modified": self.is_modified(),
|
|
637
643
|
}
|
|
638
644
|
|
|
639
645
|
# 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)
|
|
646
|
+
# __len__, __iter__ inherited from BaseCollection
|
|
647
647
|
|
|
648
648
|
def __getitem__(self, key: Union[int, str]) -> Component:
|
|
649
|
-
"""
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
649
|
+
"""
|
|
650
|
+
Get component by index, UUID, or reference.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
key: Integer index, UUID string, or reference string
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
Component at the specified location
|
|
657
|
+
|
|
658
|
+
Raises:
|
|
659
|
+
KeyError: If UUID or reference not found
|
|
660
|
+
IndexError: If index out of range
|
|
661
|
+
TypeError: If key is invalid type
|
|
662
|
+
"""
|
|
663
|
+
if isinstance(key, str):
|
|
664
|
+
# Try reference first (most common use case)
|
|
653
665
|
component = self._reference_index.get(key)
|
|
654
|
-
if component is None:
|
|
666
|
+
if component is not None:
|
|
667
|
+
return component
|
|
668
|
+
# Fall back to UUID lookup (from base class)
|
|
669
|
+
try:
|
|
670
|
+
return super().__getitem__(key)
|
|
671
|
+
except KeyError:
|
|
655
672
|
raise KeyError(f"Component not found: {key}")
|
|
656
|
-
return component
|
|
657
673
|
else:
|
|
658
|
-
|
|
674
|
+
# Integer index (from base class)
|
|
675
|
+
return super().__getitem__(key)
|
|
659
676
|
|
|
660
677
|
def __contains__(self, reference: str) -> bool:
|
|
661
678
|
"""Check if reference exists."""
|
|
@@ -663,8 +680,11 @@ class ComponentCollection:
|
|
|
663
680
|
|
|
664
681
|
# Internal methods
|
|
665
682
|
def _add_to_indexes(self, component: Component):
|
|
666
|
-
"""Add component to all indexes."""
|
|
667
|
-
|
|
683
|
+
"""Add component to all indexes (base + component-specific)."""
|
|
684
|
+
# Add to base collection (UUID index)
|
|
685
|
+
self._add_item(component)
|
|
686
|
+
|
|
687
|
+
# Add to reference index
|
|
668
688
|
self._reference_index[component.reference] = component
|
|
669
689
|
|
|
670
690
|
# Add to lib_id index
|
|
@@ -681,8 +701,8 @@ class ComponentCollection:
|
|
|
681
701
|
self._value_index[value].append(component)
|
|
682
702
|
|
|
683
703
|
def _remove_from_indexes(self, component: Component):
|
|
684
|
-
"""Remove component from
|
|
685
|
-
|
|
704
|
+
"""Remove component from component-specific indexes (not base UUID index)."""
|
|
705
|
+
# Remove from reference index
|
|
686
706
|
del self._reference_index[component.reference]
|
|
687
707
|
|
|
688
708
|
# Remove from lib_id index
|
|
@@ -705,10 +725,7 @@ class ComponentCollection:
|
|
|
705
725
|
component = self._reference_index[old_ref]
|
|
706
726
|
del self._reference_index[old_ref]
|
|
707
727
|
self._reference_index[new_ref] = component
|
|
708
|
-
|
|
709
|
-
def _mark_modified(self):
|
|
710
|
-
"""Mark collection as modified."""
|
|
711
|
-
self._modified = True
|
|
728
|
+
# Note: UUID doesn't change when reference changes, so base index is unaffected
|
|
712
729
|
|
|
713
730
|
def _generate_reference(self, lib_id: str) -> str:
|
|
714
731
|
"""Generate unique reference for component."""
|
|
@@ -730,7 +747,7 @@ class ComponentCollection:
|
|
|
730
747
|
grid_size = 10.0 # 10mm grid
|
|
731
748
|
max_per_row = 10
|
|
732
749
|
|
|
733
|
-
row = len(self.
|
|
734
|
-
col = len(self.
|
|
750
|
+
row = len(self._items) // max_per_row
|
|
751
|
+
col = len(self._items) % max_per_row
|
|
735
752
|
|
|
736
753
|
return Point(col * grid_size, row * grid_size)
|
kicad_sch_api/core/config.py
CHANGED
|
@@ -6,8 +6,8 @@ This module centralizes all magic numbers and configuration values
|
|
|
6
6
|
to make them easily configurable and maintainable.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, Dict, Tuple
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Dict, List, Tuple
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
@@ -57,20 +57,102 @@ class DefaultValues:
|
|
|
57
57
|
|
|
58
58
|
project_name: str = "untitled"
|
|
59
59
|
stroke_width: float = 0.0
|
|
60
|
+
stroke_type: str = "default"
|
|
61
|
+
fill_type: str = "none"
|
|
60
62
|
font_size: float = 1.27
|
|
61
63
|
pin_name_size: float = 1.27
|
|
62
64
|
pin_number_size: float = 1.27
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
@dataclass
|
|
68
|
+
class FileFormatConstants:
|
|
69
|
+
"""KiCAD file format identifiers and version strings."""
|
|
70
|
+
|
|
71
|
+
file_type: str = "kicad_sch"
|
|
72
|
+
generator_default: str = "eeschema"
|
|
73
|
+
version_default: str = "20250114"
|
|
74
|
+
generator_version_default: str = "9.0"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class PaperSizeConstants:
|
|
79
|
+
"""Standard paper size definitions."""
|
|
80
|
+
|
|
81
|
+
default: str = "A4"
|
|
82
|
+
valid_sizes: List[str] = field(
|
|
83
|
+
default_factory=lambda: ["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class FieldNames:
|
|
89
|
+
"""Common S-expression field names to avoid typos."""
|
|
90
|
+
|
|
91
|
+
# File structure
|
|
92
|
+
version: str = "version"
|
|
93
|
+
generator: str = "generator"
|
|
94
|
+
generator_version: str = "generator_version"
|
|
95
|
+
uuid: str = "uuid"
|
|
96
|
+
paper: str = "paper"
|
|
97
|
+
|
|
98
|
+
# Positioning
|
|
99
|
+
at: str = "at"
|
|
100
|
+
xy: str = "xy"
|
|
101
|
+
pts: str = "pts"
|
|
102
|
+
start: str = "start"
|
|
103
|
+
end: str = "end"
|
|
104
|
+
mid: str = "mid"
|
|
105
|
+
center: str = "center"
|
|
106
|
+
radius: str = "radius"
|
|
107
|
+
|
|
108
|
+
# Styling
|
|
109
|
+
stroke: str = "stroke"
|
|
110
|
+
fill: str = "fill"
|
|
111
|
+
width: str = "width"
|
|
112
|
+
type: str = "type"
|
|
113
|
+
color: str = "color"
|
|
114
|
+
|
|
115
|
+
# Text/Font
|
|
116
|
+
font: str = "font"
|
|
117
|
+
size: str = "size"
|
|
118
|
+
effects: str = "effects"
|
|
119
|
+
|
|
120
|
+
# Components
|
|
121
|
+
pin: str = "pin"
|
|
122
|
+
property: str = "property"
|
|
123
|
+
symbol: str = "symbol"
|
|
124
|
+
lib_id: str = "lib_id"
|
|
125
|
+
|
|
126
|
+
# Graphics
|
|
127
|
+
polyline: str = "polyline"
|
|
128
|
+
arc: str = "arc"
|
|
129
|
+
circle: str = "circle"
|
|
130
|
+
rectangle: str = "rectangle"
|
|
131
|
+
bezier: str = "bezier"
|
|
132
|
+
|
|
133
|
+
# Connection elements
|
|
134
|
+
wire: str = "wire"
|
|
135
|
+
junction: str = "junction"
|
|
136
|
+
no_connect: str = "no_connect"
|
|
137
|
+
label: str = "label"
|
|
138
|
+
|
|
139
|
+
# Hierarchical
|
|
140
|
+
sheet: str = "sheet"
|
|
141
|
+
sheet_instances: str = "sheet_instances"
|
|
142
|
+
|
|
143
|
+
|
|
65
144
|
class KiCADConfig:
|
|
66
145
|
"""Central configuration class for KiCAD schematic API."""
|
|
67
146
|
|
|
68
|
-
def __init__(self):
|
|
147
|
+
def __init__(self) -> None:
|
|
69
148
|
self.properties = PropertyOffsets()
|
|
70
149
|
self.grid = GridSettings()
|
|
71
150
|
self.sheet = SheetSettings()
|
|
72
151
|
self.tolerance = ToleranceSettings()
|
|
73
152
|
self.defaults = DefaultValues()
|
|
153
|
+
self.file_format = FileFormatConstants()
|
|
154
|
+
self.paper = PaperSizeConstants()
|
|
155
|
+
self.fields = FieldNames()
|
|
74
156
|
|
|
75
157
|
# Names that should not generate title_block (for backward compatibility)
|
|
76
158
|
# Include test schematic names to maintain reference compatibility
|