kicad-sch-api 0.0.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.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +112 -0
- kicad_sch_api/core/__init__.py +23 -0
- kicad_sch_api/core/components.py +652 -0
- kicad_sch_api/core/formatter.py +312 -0
- kicad_sch_api/core/parser.py +434 -0
- kicad_sch_api/core/schematic.py +478 -0
- kicad_sch_api/core/types.py +369 -0
- kicad_sch_api/library/__init__.py +10 -0
- kicad_sch_api/library/cache.py +548 -0
- kicad_sch_api/mcp/__init__.py +5 -0
- kicad_sch_api/mcp/server.py +500 -0
- kicad_sch_api/py.typed +1 -0
- kicad_sch_api/utils/__init__.py +15 -0
- kicad_sch_api/utils/validation.py +447 -0
- kicad_sch_api-0.0.1.dist-info/METADATA +226 -0
- kicad_sch_api-0.0.1.dist-info/RECORD +20 -0
- kicad_sch_api-0.0.1.dist-info/WHEEL +5 -0
- kicad_sch_api-0.0.1.dist-info/entry_points.txt +2 -0
- kicad_sch_api-0.0.1.dist-info/licenses/LICENSE +21 -0
- kicad_sch_api-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced component management for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
This module provides a modern, intuitive API for working with schematic components,
|
|
5
|
+
featuring fast lookup, bulk operations, and advanced filtering capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from ..library.cache import SymbolDefinition, get_symbol_cache
|
|
13
|
+
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
14
|
+
from .types import Point, SchematicPin, SchematicSymbol
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Component:
|
|
20
|
+
"""
|
|
21
|
+
Enhanced wrapper for schematic components with modern API.
|
|
22
|
+
|
|
23
|
+
Provides intuitive access to component properties, pins, and operations
|
|
24
|
+
while maintaining exact format preservation for professional use.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, symbol_data: SchematicSymbol, parent_collection: "ComponentCollection"):
|
|
28
|
+
"""
|
|
29
|
+
Initialize component wrapper.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
symbol_data: Underlying symbol data
|
|
33
|
+
parent_collection: Parent collection for updates
|
|
34
|
+
"""
|
|
35
|
+
self._data = symbol_data
|
|
36
|
+
self._collection = parent_collection
|
|
37
|
+
self._validator = SchematicValidator()
|
|
38
|
+
|
|
39
|
+
# Core properties with validation
|
|
40
|
+
@property
|
|
41
|
+
def reference(self) -> str:
|
|
42
|
+
"""Component reference (e.g., 'R1')."""
|
|
43
|
+
return self._data.reference
|
|
44
|
+
|
|
45
|
+
@reference.setter
|
|
46
|
+
def reference(self, value: str):
|
|
47
|
+
"""Set component reference with validation."""
|
|
48
|
+
if not self._validator.validate_reference(value):
|
|
49
|
+
raise ValidationError(f"Invalid reference format: {value}")
|
|
50
|
+
|
|
51
|
+
# Check for duplicates in parent collection
|
|
52
|
+
if self._collection.get(value) is not None:
|
|
53
|
+
raise ValidationError(f"Reference {value} already exists")
|
|
54
|
+
|
|
55
|
+
old_ref = self._data.reference
|
|
56
|
+
self._data.reference = value
|
|
57
|
+
self._collection._update_reference_index(old_ref, value)
|
|
58
|
+
logger.debug(f"Updated reference: {old_ref} -> {value}")
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def value(self) -> str:
|
|
62
|
+
"""Component value (e.g., '10k')."""
|
|
63
|
+
return self._data.value
|
|
64
|
+
|
|
65
|
+
@value.setter
|
|
66
|
+
def value(self, value: str):
|
|
67
|
+
"""Set component value."""
|
|
68
|
+
self._data.value = value
|
|
69
|
+
self._collection._mark_modified()
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def footprint(self) -> Optional[str]:
|
|
73
|
+
"""Component footprint."""
|
|
74
|
+
return self._data.footprint
|
|
75
|
+
|
|
76
|
+
@footprint.setter
|
|
77
|
+
def footprint(self, value: Optional[str]):
|
|
78
|
+
"""Set component footprint."""
|
|
79
|
+
self._data.footprint = value
|
|
80
|
+
self._collection._mark_modified()
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def position(self) -> Point:
|
|
84
|
+
"""Component position."""
|
|
85
|
+
return self._data.position
|
|
86
|
+
|
|
87
|
+
@position.setter
|
|
88
|
+
def position(self, value: Union[Point, Tuple[float, float]]):
|
|
89
|
+
"""Set component position."""
|
|
90
|
+
if isinstance(value, tuple):
|
|
91
|
+
value = Point(value[0], value[1])
|
|
92
|
+
self._data.position = value
|
|
93
|
+
self._collection._mark_modified()
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def rotation(self) -> float:
|
|
97
|
+
"""Component rotation in degrees."""
|
|
98
|
+
return self._data.rotation
|
|
99
|
+
|
|
100
|
+
@rotation.setter
|
|
101
|
+
def rotation(self, value: float):
|
|
102
|
+
"""Set component rotation."""
|
|
103
|
+
self._data.rotation = float(value) % 360
|
|
104
|
+
self._collection._mark_modified()
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def lib_id(self) -> str:
|
|
108
|
+
"""Library ID (e.g., 'Device:R')."""
|
|
109
|
+
return self._data.lib_id
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def library(self) -> str:
|
|
113
|
+
"""Library name."""
|
|
114
|
+
return self._data.library
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def symbol_name(self) -> str:
|
|
118
|
+
"""Symbol name within library."""
|
|
119
|
+
return self._data.symbol_name
|
|
120
|
+
|
|
121
|
+
# Properties dictionary
|
|
122
|
+
@property
|
|
123
|
+
def properties(self) -> Dict[str, str]:
|
|
124
|
+
"""Dictionary of all component properties."""
|
|
125
|
+
return self._data.properties
|
|
126
|
+
|
|
127
|
+
def get_property(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
128
|
+
"""Get property value by name."""
|
|
129
|
+
return self._data.properties.get(name, default)
|
|
130
|
+
|
|
131
|
+
def set_property(self, name: str, value: str):
|
|
132
|
+
"""Set property value with validation."""
|
|
133
|
+
if not isinstance(name, str) or not isinstance(value, str):
|
|
134
|
+
raise ValidationError("Property name and value must be strings")
|
|
135
|
+
|
|
136
|
+
self._data.properties[name] = value
|
|
137
|
+
self._collection._mark_modified()
|
|
138
|
+
logger.debug(f"Set property {self.reference}.{name} = {value}")
|
|
139
|
+
|
|
140
|
+
def remove_property(self, name: str) -> bool:
|
|
141
|
+
"""Remove property by name."""
|
|
142
|
+
if name in self._data.properties:
|
|
143
|
+
del self._data.properties[name]
|
|
144
|
+
self._collection._mark_modified()
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
# Pin access
|
|
149
|
+
@property
|
|
150
|
+
def pins(self) -> List[SchematicPin]:
|
|
151
|
+
"""List of component pins."""
|
|
152
|
+
return self._data.pins
|
|
153
|
+
|
|
154
|
+
def get_pin(self, pin_number: str) -> Optional[SchematicPin]:
|
|
155
|
+
"""Get pin by number."""
|
|
156
|
+
return self._data.get_pin(pin_number)
|
|
157
|
+
|
|
158
|
+
def get_pin_position(self, pin_number: str) -> Optional[Point]:
|
|
159
|
+
"""Get absolute position of pin."""
|
|
160
|
+
return self._data.get_pin_position(pin_number)
|
|
161
|
+
|
|
162
|
+
# Component state
|
|
163
|
+
@property
|
|
164
|
+
def in_bom(self) -> bool:
|
|
165
|
+
"""Whether component appears in bill of materials."""
|
|
166
|
+
return self._data.in_bom
|
|
167
|
+
|
|
168
|
+
@in_bom.setter
|
|
169
|
+
def in_bom(self, value: bool):
|
|
170
|
+
"""Set BOM inclusion."""
|
|
171
|
+
self._data.in_bom = bool(value)
|
|
172
|
+
self._collection._mark_modified()
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def on_board(self) -> bool:
|
|
176
|
+
"""Whether component appears on PCB."""
|
|
177
|
+
return self._data.on_board
|
|
178
|
+
|
|
179
|
+
@on_board.setter
|
|
180
|
+
def on_board(self, value: bool):
|
|
181
|
+
"""Set board inclusion."""
|
|
182
|
+
self._data.on_board = bool(value)
|
|
183
|
+
self._collection._mark_modified()
|
|
184
|
+
|
|
185
|
+
# Utility methods
|
|
186
|
+
def move(self, x: float, y: float):
|
|
187
|
+
"""Move component to new position."""
|
|
188
|
+
self.position = Point(x, y)
|
|
189
|
+
|
|
190
|
+
def translate(self, dx: float, dy: float):
|
|
191
|
+
"""Translate component by offset."""
|
|
192
|
+
current = self.position
|
|
193
|
+
self.position = Point(current.x + dx, current.y + dy)
|
|
194
|
+
|
|
195
|
+
def rotate(self, angle: float):
|
|
196
|
+
"""Rotate component by angle (degrees)."""
|
|
197
|
+
self.rotation = (self.rotation + angle) % 360
|
|
198
|
+
|
|
199
|
+
def copy_properties_from(self, other: "Component"):
|
|
200
|
+
"""Copy all properties from another component."""
|
|
201
|
+
for name, value in other.properties.items():
|
|
202
|
+
self.set_property(name, value)
|
|
203
|
+
|
|
204
|
+
def get_symbol_definition(self) -> Optional[SymbolDefinition]:
|
|
205
|
+
"""Get the symbol definition from library cache."""
|
|
206
|
+
cache = get_symbol_cache()
|
|
207
|
+
return cache.get_symbol(self.lib_id)
|
|
208
|
+
|
|
209
|
+
def update_from_library(self) -> bool:
|
|
210
|
+
"""Update component pins and metadata from library definition."""
|
|
211
|
+
symbol_def = self.get_symbol_definition()
|
|
212
|
+
if not symbol_def:
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
# Update pins
|
|
216
|
+
self._data.pins = symbol_def.pins.copy()
|
|
217
|
+
|
|
218
|
+
# Update reference prefix if needed
|
|
219
|
+
if not self.reference.startswith(symbol_def.reference_prefix):
|
|
220
|
+
logger.warning(
|
|
221
|
+
f"Reference {self.reference} doesn't match expected prefix {symbol_def.reference_prefix}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self._collection._mark_modified()
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
def validate(self) -> List[ValidationIssue]:
|
|
228
|
+
"""Validate this component."""
|
|
229
|
+
return self._validator.validate_component(self._data.__dict__)
|
|
230
|
+
|
|
231
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
232
|
+
"""Convert component to dictionary representation."""
|
|
233
|
+
return {
|
|
234
|
+
"reference": self.reference,
|
|
235
|
+
"lib_id": self.lib_id,
|
|
236
|
+
"value": self.value,
|
|
237
|
+
"footprint": self.footprint,
|
|
238
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
239
|
+
"rotation": self.rotation,
|
|
240
|
+
"properties": self.properties.copy(),
|
|
241
|
+
"in_bom": self.in_bom,
|
|
242
|
+
"on_board": self.on_board,
|
|
243
|
+
"pin_count": len(self.pins),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
def __str__(self) -> str:
|
|
247
|
+
"""String representation."""
|
|
248
|
+
return f"<Component {self.reference}: {self.lib_id} = '{self.value}' @ {self.position}>"
|
|
249
|
+
|
|
250
|
+
def __repr__(self) -> str:
|
|
251
|
+
"""Detailed representation."""
|
|
252
|
+
return (
|
|
253
|
+
f"Component(ref='{self.reference}', lib_id='{self.lib_id}', "
|
|
254
|
+
f"value='{self.value}', pos={self.position}, rotation={self.rotation})"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class ComponentCollection:
|
|
259
|
+
"""
|
|
260
|
+
Collection class for efficient component management.
|
|
261
|
+
|
|
262
|
+
Provides fast lookup, filtering, and bulk operations for schematic components.
|
|
263
|
+
Optimized for schematics with hundreds or thousands of components.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, components: List[SchematicSymbol] = None):
|
|
267
|
+
"""
|
|
268
|
+
Initialize component collection.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
components: Initial list of component data
|
|
272
|
+
"""
|
|
273
|
+
self._components: List[Component] = []
|
|
274
|
+
self._reference_index: Dict[str, Component] = {}
|
|
275
|
+
self._lib_id_index: Dict[str, List[Component]] = {}
|
|
276
|
+
self._value_index: Dict[str, List[Component]] = {}
|
|
277
|
+
self._modified = False
|
|
278
|
+
|
|
279
|
+
# Add initial components
|
|
280
|
+
if components:
|
|
281
|
+
for comp_data in components:
|
|
282
|
+
self._add_to_indexes(Component(comp_data, self))
|
|
283
|
+
|
|
284
|
+
logger.debug(f"ComponentCollection initialized with {len(self._components)} components")
|
|
285
|
+
|
|
286
|
+
def add(
|
|
287
|
+
self,
|
|
288
|
+
lib_id: str,
|
|
289
|
+
reference: Optional[str] = None,
|
|
290
|
+
value: str = "",
|
|
291
|
+
position: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
292
|
+
footprint: Optional[str] = None,
|
|
293
|
+
**properties,
|
|
294
|
+
) -> Component:
|
|
295
|
+
"""
|
|
296
|
+
Add a new component to the schematic.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
lib_id: Library identifier (e.g., "Device:R")
|
|
300
|
+
reference: Component reference (auto-generated if None)
|
|
301
|
+
value: Component value
|
|
302
|
+
position: Component position (auto-placed if None)
|
|
303
|
+
footprint: Component footprint
|
|
304
|
+
**properties: Additional component properties
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Newly created Component
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
ValidationError: If component data is invalid
|
|
311
|
+
"""
|
|
312
|
+
# Validate lib_id
|
|
313
|
+
validator = SchematicValidator()
|
|
314
|
+
if not validator.validate_lib_id(lib_id):
|
|
315
|
+
raise ValidationError(f"Invalid lib_id format: {lib_id}")
|
|
316
|
+
|
|
317
|
+
# Generate reference if not provided
|
|
318
|
+
if not reference:
|
|
319
|
+
reference = self._generate_reference(lib_id)
|
|
320
|
+
|
|
321
|
+
# Validate reference
|
|
322
|
+
if not validator.validate_reference(reference):
|
|
323
|
+
raise ValidationError(f"Invalid reference format: {reference}")
|
|
324
|
+
|
|
325
|
+
# Check for duplicate reference
|
|
326
|
+
if reference in self._reference_index:
|
|
327
|
+
raise ValidationError(f"Reference {reference} already exists")
|
|
328
|
+
|
|
329
|
+
# Set default position if not provided
|
|
330
|
+
if position is None:
|
|
331
|
+
position = self._find_available_position()
|
|
332
|
+
elif isinstance(position, tuple):
|
|
333
|
+
position = Point(position[0], position[1])
|
|
334
|
+
|
|
335
|
+
# Create component data
|
|
336
|
+
component_data = SchematicSymbol(
|
|
337
|
+
uuid=str(uuid.uuid4()),
|
|
338
|
+
lib_id=lib_id,
|
|
339
|
+
position=position,
|
|
340
|
+
reference=reference,
|
|
341
|
+
value=value,
|
|
342
|
+
footprint=footprint,
|
|
343
|
+
properties=properties,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Get symbol definition and update pins
|
|
347
|
+
symbol_cache = get_symbol_cache()
|
|
348
|
+
symbol_def = symbol_cache.get_symbol(lib_id)
|
|
349
|
+
if symbol_def:
|
|
350
|
+
component_data.pins = symbol_def.pins.copy()
|
|
351
|
+
|
|
352
|
+
# Create component wrapper
|
|
353
|
+
component = Component(component_data, self)
|
|
354
|
+
|
|
355
|
+
# Add to collection
|
|
356
|
+
self._add_to_indexes(component)
|
|
357
|
+
self._modified = True
|
|
358
|
+
|
|
359
|
+
logger.info(f"Added component: {reference} ({lib_id})")
|
|
360
|
+
return component
|
|
361
|
+
|
|
362
|
+
def remove(self, reference: str) -> bool:
|
|
363
|
+
"""
|
|
364
|
+
Remove component by reference.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
reference: Component reference to remove
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
True if component was removed
|
|
371
|
+
"""
|
|
372
|
+
component = self._reference_index.get(reference)
|
|
373
|
+
if not component:
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
# Remove from all indexes
|
|
377
|
+
self._remove_from_indexes(component)
|
|
378
|
+
self._modified = True
|
|
379
|
+
|
|
380
|
+
logger.info(f"Removed component: {reference}")
|
|
381
|
+
return True
|
|
382
|
+
|
|
383
|
+
def get(self, reference: str) -> Optional[Component]:
|
|
384
|
+
"""Get component by reference."""
|
|
385
|
+
return self._reference_index.get(reference)
|
|
386
|
+
|
|
387
|
+
def filter(self, **criteria) -> List[Component]:
|
|
388
|
+
"""
|
|
389
|
+
Filter components by various criteria.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
lib_id: Filter by library ID
|
|
393
|
+
value: Filter by value (exact match)
|
|
394
|
+
value_pattern: Filter by value pattern (contains)
|
|
395
|
+
reference_pattern: Filter by reference pattern
|
|
396
|
+
footprint: Filter by footprint
|
|
397
|
+
in_area: Filter by area (tuple of (x1, y1, x2, y2))
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
List of matching components
|
|
401
|
+
"""
|
|
402
|
+
results = list(self._components)
|
|
403
|
+
|
|
404
|
+
# Apply filters
|
|
405
|
+
if "lib_id" in criteria:
|
|
406
|
+
lib_id = criteria["lib_id"]
|
|
407
|
+
results = [c for c in results if c.lib_id == lib_id]
|
|
408
|
+
|
|
409
|
+
if "value" in criteria:
|
|
410
|
+
value = criteria["value"]
|
|
411
|
+
results = [c for c in results if c.value == value]
|
|
412
|
+
|
|
413
|
+
if "value_pattern" in criteria:
|
|
414
|
+
pattern = criteria["value_pattern"].lower()
|
|
415
|
+
results = [c for c in results if pattern in c.value.lower()]
|
|
416
|
+
|
|
417
|
+
if "reference_pattern" in criteria:
|
|
418
|
+
import re
|
|
419
|
+
|
|
420
|
+
pattern = re.compile(criteria["reference_pattern"])
|
|
421
|
+
results = [c for c in results if pattern.match(c.reference)]
|
|
422
|
+
|
|
423
|
+
if "footprint" in criteria:
|
|
424
|
+
footprint = criteria["footprint"]
|
|
425
|
+
results = [c for c in results if c.footprint == footprint]
|
|
426
|
+
|
|
427
|
+
if "in_area" in criteria:
|
|
428
|
+
x1, y1, x2, y2 = criteria["in_area"]
|
|
429
|
+
results = [c for c in results if x1 <= c.position.x <= x2 and y1 <= c.position.y <= y2]
|
|
430
|
+
|
|
431
|
+
if "has_property" in criteria:
|
|
432
|
+
prop_name = criteria["has_property"]
|
|
433
|
+
results = [c for c in results if prop_name in c.properties]
|
|
434
|
+
|
|
435
|
+
return results
|
|
436
|
+
|
|
437
|
+
def filter_by_type(self, component_type: str) -> List[Component]:
|
|
438
|
+
"""Filter components by type (e.g., 'R' for resistors)."""
|
|
439
|
+
return [
|
|
440
|
+
c for c in self._components if c.symbol_name.upper().startswith(component_type.upper())
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
|
|
444
|
+
"""Get components within rectangular area."""
|
|
445
|
+
return self.filter(in_area=(x1, y1, x2, y2))
|
|
446
|
+
|
|
447
|
+
def near_point(
|
|
448
|
+
self, point: Union[Point, Tuple[float, float]], radius: float
|
|
449
|
+
) -> List[Component]:
|
|
450
|
+
"""Get components within radius of a point."""
|
|
451
|
+
if isinstance(point, tuple):
|
|
452
|
+
point = Point(point[0], point[1])
|
|
453
|
+
|
|
454
|
+
results = []
|
|
455
|
+
for component in self._components:
|
|
456
|
+
if component.position.distance_to(point) <= radius:
|
|
457
|
+
results.append(component)
|
|
458
|
+
return results
|
|
459
|
+
|
|
460
|
+
def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
|
|
461
|
+
"""
|
|
462
|
+
Update multiple components matching criteria.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
criteria: Filter criteria (same as filter method)
|
|
466
|
+
updates: Dictionary of property updates
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Number of components updated
|
|
470
|
+
"""
|
|
471
|
+
matching = self.filter(**criteria)
|
|
472
|
+
|
|
473
|
+
for component in matching:
|
|
474
|
+
# Update basic properties
|
|
475
|
+
for key, value in updates.items():
|
|
476
|
+
if hasattr(component, key):
|
|
477
|
+
setattr(component, key, value)
|
|
478
|
+
else:
|
|
479
|
+
# Add as custom property
|
|
480
|
+
component.set_property(key, str(value))
|
|
481
|
+
|
|
482
|
+
if matching:
|
|
483
|
+
self._modified = True
|
|
484
|
+
|
|
485
|
+
logger.info(f"Bulk updated {len(matching)} components")
|
|
486
|
+
return len(matching)
|
|
487
|
+
|
|
488
|
+
def sort_by_reference(self):
|
|
489
|
+
"""Sort components by reference designator."""
|
|
490
|
+
self._components.sort(key=lambda c: c.reference)
|
|
491
|
+
|
|
492
|
+
def sort_by_position(self, by_x: bool = True):
|
|
493
|
+
"""Sort components by position."""
|
|
494
|
+
if by_x:
|
|
495
|
+
self._components.sort(key=lambda c: (c.position.x, c.position.y))
|
|
496
|
+
else:
|
|
497
|
+
self._components.sort(key=lambda c: (c.position.y, c.position.x))
|
|
498
|
+
|
|
499
|
+
def validate_all(self) -> List[ValidationIssue]:
|
|
500
|
+
"""Validate all components in collection."""
|
|
501
|
+
all_issues = []
|
|
502
|
+
validator = SchematicValidator()
|
|
503
|
+
|
|
504
|
+
# Validate individual components
|
|
505
|
+
for component in self._components:
|
|
506
|
+
issues = component.validate()
|
|
507
|
+
all_issues.extend(issues)
|
|
508
|
+
|
|
509
|
+
# Validate collection-level rules
|
|
510
|
+
references = [c.reference for c in self._components]
|
|
511
|
+
if len(references) != len(set(references)):
|
|
512
|
+
# Find duplicates
|
|
513
|
+
seen = set()
|
|
514
|
+
duplicates = set()
|
|
515
|
+
for ref in references:
|
|
516
|
+
if ref in seen:
|
|
517
|
+
duplicates.add(ref)
|
|
518
|
+
seen.add(ref)
|
|
519
|
+
|
|
520
|
+
for ref in duplicates:
|
|
521
|
+
all_issues.append(
|
|
522
|
+
ValidationIssue(
|
|
523
|
+
category="reference", message=f"Duplicate reference: {ref}", level="error"
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
return all_issues
|
|
528
|
+
|
|
529
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
530
|
+
"""Get collection statistics."""
|
|
531
|
+
lib_counts = {}
|
|
532
|
+
value_counts = {}
|
|
533
|
+
|
|
534
|
+
for component in self._components:
|
|
535
|
+
# Count by library
|
|
536
|
+
lib = component.library
|
|
537
|
+
lib_counts[lib] = lib_counts.get(lib, 0) + 1
|
|
538
|
+
|
|
539
|
+
# Count by value
|
|
540
|
+
value = component.value
|
|
541
|
+
if value:
|
|
542
|
+
value_counts[value] = value_counts.get(value, 0) + 1
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
"total_components": len(self._components),
|
|
546
|
+
"unique_references": len(self._reference_index),
|
|
547
|
+
"libraries_used": len(lib_counts),
|
|
548
|
+
"library_breakdown": lib_counts,
|
|
549
|
+
"most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
|
|
550
|
+
:10
|
|
551
|
+
],
|
|
552
|
+
"modified": self._modified,
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Collection interface
|
|
556
|
+
def __len__(self) -> int:
|
|
557
|
+
"""Number of components."""
|
|
558
|
+
return len(self._components)
|
|
559
|
+
|
|
560
|
+
def __iter__(self) -> Iterator[Component]:
|
|
561
|
+
"""Iterate over components."""
|
|
562
|
+
return iter(self._components)
|
|
563
|
+
|
|
564
|
+
def __getitem__(self, key: Union[int, str]) -> Component:
|
|
565
|
+
"""Get component by index or reference."""
|
|
566
|
+
if isinstance(key, int):
|
|
567
|
+
return self._components[key]
|
|
568
|
+
elif isinstance(key, str):
|
|
569
|
+
component = self._reference_index.get(key)
|
|
570
|
+
if component is None:
|
|
571
|
+
raise KeyError(f"Component not found: {key}")
|
|
572
|
+
return component
|
|
573
|
+
else:
|
|
574
|
+
raise TypeError(f"Invalid key type: {type(key)}")
|
|
575
|
+
|
|
576
|
+
def __contains__(self, reference: str) -> bool:
|
|
577
|
+
"""Check if reference exists."""
|
|
578
|
+
return reference in self._reference_index
|
|
579
|
+
|
|
580
|
+
# Internal methods
|
|
581
|
+
def _add_to_indexes(self, component: Component):
|
|
582
|
+
"""Add component to all indexes."""
|
|
583
|
+
self._components.append(component)
|
|
584
|
+
self._reference_index[component.reference] = component
|
|
585
|
+
|
|
586
|
+
# Add to lib_id index
|
|
587
|
+
lib_id = component.lib_id
|
|
588
|
+
if lib_id not in self._lib_id_index:
|
|
589
|
+
self._lib_id_index[lib_id] = []
|
|
590
|
+
self._lib_id_index[lib_id].append(component)
|
|
591
|
+
|
|
592
|
+
# Add to value index
|
|
593
|
+
value = component.value
|
|
594
|
+
if value:
|
|
595
|
+
if value not in self._value_index:
|
|
596
|
+
self._value_index[value] = []
|
|
597
|
+
self._value_index[value].append(component)
|
|
598
|
+
|
|
599
|
+
def _remove_from_indexes(self, component: Component):
|
|
600
|
+
"""Remove component from all indexes."""
|
|
601
|
+
self._components.remove(component)
|
|
602
|
+
del self._reference_index[component.reference]
|
|
603
|
+
|
|
604
|
+
# Remove from lib_id index
|
|
605
|
+
lib_id = component.lib_id
|
|
606
|
+
if lib_id in self._lib_id_index:
|
|
607
|
+
self._lib_id_index[lib_id].remove(component)
|
|
608
|
+
if not self._lib_id_index[lib_id]:
|
|
609
|
+
del self._lib_id_index[lib_id]
|
|
610
|
+
|
|
611
|
+
# Remove from value index
|
|
612
|
+
value = component.value
|
|
613
|
+
if value and value in self._value_index:
|
|
614
|
+
self._value_index[value].remove(component)
|
|
615
|
+
if not self._value_index[value]:
|
|
616
|
+
del self._value_index[value]
|
|
617
|
+
|
|
618
|
+
def _update_reference_index(self, old_ref: str, new_ref: str):
|
|
619
|
+
"""Update reference index when component reference changes."""
|
|
620
|
+
if old_ref in self._reference_index:
|
|
621
|
+
component = self._reference_index[old_ref]
|
|
622
|
+
del self._reference_index[old_ref]
|
|
623
|
+
self._reference_index[new_ref] = component
|
|
624
|
+
|
|
625
|
+
def _mark_modified(self):
|
|
626
|
+
"""Mark collection as modified."""
|
|
627
|
+
self._modified = True
|
|
628
|
+
|
|
629
|
+
def _generate_reference(self, lib_id: str) -> str:
|
|
630
|
+
"""Generate unique reference for component."""
|
|
631
|
+
# Get reference prefix from symbol definition
|
|
632
|
+
symbol_cache = get_symbol_cache()
|
|
633
|
+
symbol_def = symbol_cache.get_symbol(lib_id)
|
|
634
|
+
prefix = symbol_def.reference_prefix if symbol_def else "U"
|
|
635
|
+
|
|
636
|
+
# Find next available number
|
|
637
|
+
counter = 1
|
|
638
|
+
while f"{prefix}{counter}" in self._reference_index:
|
|
639
|
+
counter += 1
|
|
640
|
+
|
|
641
|
+
return f"{prefix}{counter}"
|
|
642
|
+
|
|
643
|
+
def _find_available_position(self) -> Point:
|
|
644
|
+
"""Find an available position for automatic placement."""
|
|
645
|
+
# Simple grid placement - could be enhanced with collision detection
|
|
646
|
+
grid_size = 10.0 # 10mm grid
|
|
647
|
+
max_per_row = 10
|
|
648
|
+
|
|
649
|
+
row = len(self._components) // max_per_row
|
|
650
|
+
col = len(self._components) % max_per_row
|
|
651
|
+
|
|
652
|
+
return Point(col * grid_size, row * grid_size)
|