kicad-sch-api 0.3.4__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  7. kicad_sch_api/core/components.py +5 -0
  8. kicad_sch_api/core/formatter.py +3 -1
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/managers/__init__.py +26 -0
  11. kicad_sch_api/core/managers/file_io.py +243 -0
  12. kicad_sch_api/core/managers/format_sync.py +501 -0
  13. kicad_sch_api/core/managers/graphics.py +579 -0
  14. kicad_sch_api/core/managers/metadata.py +268 -0
  15. kicad_sch_api/core/managers/sheet.py +454 -0
  16. kicad_sch_api/core/managers/text_elements.py +536 -0
  17. kicad_sch_api/core/managers/validation.py +474 -0
  18. kicad_sch_api/core/managers/wire.py +346 -0
  19. kicad_sch_api/core/nets.py +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
  42. kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
  43. kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,346 @@
1
+ """
2
+ Wire Manager for KiCAD schematic wire operations.
3
+
4
+ Handles wire creation, removal, pin connections, and auto-routing functionality
5
+ while managing component pin position calculations and connectivity analysis.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional, Tuple, Union
11
+
12
+ from ...library.cache import get_symbol_cache
13
+ from ..types import Point, Wire, WireType
14
+ from ..wires import WireCollection
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class WireManager:
20
+ """
21
+ Manages wire operations and pin connectivity in KiCAD schematics.
22
+
23
+ Responsible for:
24
+ - Wire creation and removal
25
+ - Pin position calculations
26
+ - Auto-routing between pins
27
+ - Connectivity analysis
28
+ - Wire-to-pin connections
29
+ """
30
+
31
+ def __init__(
32
+ self, schematic_data: Dict[str, Any], wire_collection: WireCollection, component_collection
33
+ ):
34
+ """
35
+ Initialize WireManager.
36
+
37
+ Args:
38
+ schematic_data: Reference to schematic data
39
+ wire_collection: Wire collection for management
40
+ component_collection: Component collection for pin lookups
41
+ """
42
+ self._data = schematic_data
43
+ self._wires = wire_collection
44
+ self._components = component_collection
45
+ self._symbol_cache = get_symbol_cache()
46
+
47
+ def add_wire(
48
+ self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
49
+ ) -> str:
50
+ """
51
+ Add a wire connection.
52
+
53
+ Args:
54
+ start: Start point
55
+ end: End point
56
+
57
+ Returns:
58
+ UUID of created wire
59
+ """
60
+ if isinstance(start, tuple):
61
+ start = Point(start[0], start[1])
62
+ if isinstance(end, tuple):
63
+ end = Point(end[0], end[1])
64
+
65
+ # Use the wire collection to add the wire
66
+ wire_uuid = self._wires.add(start=start, end=end)
67
+
68
+ logger.debug(f"Added wire: {start} -> {end}")
69
+ return wire_uuid
70
+
71
+ def remove_wire(self, wire_uuid: str) -> bool:
72
+ """
73
+ Remove wire by UUID.
74
+
75
+ Args:
76
+ wire_uuid: UUID of wire to remove
77
+
78
+ Returns:
79
+ True if wire was removed, False if not found
80
+ """
81
+ # Remove from wire collection
82
+ removed_from_collection = self._wires.remove(wire_uuid)
83
+
84
+ # Also remove from data structure for consistency
85
+ wires = self._data.get("wires", [])
86
+ removed_from_data = False
87
+ for i, wire in enumerate(wires):
88
+ if wire.get("uuid") == wire_uuid:
89
+ del wires[i]
90
+ removed_from_data = True
91
+ break
92
+
93
+ success = removed_from_collection or removed_from_data
94
+ if success:
95
+ logger.debug(f"Removed wire: {wire_uuid}")
96
+
97
+ return success
98
+
99
+ def add_wire_to_pin(
100
+ self, start: Union[Point, Tuple[float, float]], component_ref: str, pin_number: str
101
+ ) -> str:
102
+ """
103
+ Add wire from a point to a component pin.
104
+
105
+ Args:
106
+ start: Starting point
107
+ component_ref: Component reference (e.g., "R1")
108
+ pin_number: Pin number on component
109
+
110
+ Returns:
111
+ UUID of created wire
112
+
113
+ Raises:
114
+ ValueError: If component or pin not found
115
+ """
116
+ pin_position = self.get_component_pin_position(component_ref, pin_number)
117
+ if pin_position is None:
118
+ raise ValueError(f"Pin {pin_number} not found on component {component_ref}")
119
+
120
+ return self.add_wire(start, pin_position)
121
+
122
+ def add_wire_between_pins(
123
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
124
+ ) -> str:
125
+ """
126
+ Add wire between two component pins.
127
+
128
+ Args:
129
+ component1_ref: First component reference
130
+ pin1_number: First component pin number
131
+ component2_ref: Second component reference
132
+ pin2_number: Second component pin number
133
+
134
+ Returns:
135
+ UUID of created wire
136
+
137
+ Raises:
138
+ ValueError: If components or pins not found
139
+ """
140
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
141
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
142
+
143
+ if pin1_pos is None:
144
+ raise ValueError(f"Pin {pin1_number} not found on component {component1_ref}")
145
+ if pin2_pos is None:
146
+ raise ValueError(f"Pin {pin2_number} not found on component {component2_ref}")
147
+
148
+ return self.add_wire(pin1_pos, pin2_pos)
149
+
150
+ def get_component_pin_position(self, component_ref: str, pin_number: str) -> Optional[Point]:
151
+ """
152
+ Get absolute position of a component pin.
153
+
154
+ This consolidates the duplicate implementations in the original schematic class.
155
+
156
+ Args:
157
+ component_ref: Component reference (e.g., "R1")
158
+ pin_number: Pin number
159
+
160
+ Returns:
161
+ Absolute pin position or None if not found
162
+ """
163
+ # Find component
164
+ component = self._components.get(component_ref)
165
+ if not component:
166
+ logger.warning(f"Component not found: {component_ref}")
167
+ return None
168
+
169
+ # Get symbol definition from cache
170
+ symbol_def = self._symbol_cache.get_symbol(component.lib_id)
171
+ if not symbol_def:
172
+ logger.warning(f"Symbol definition not found: {component.lib_id}")
173
+ return None
174
+
175
+ # Find pin in symbol definition
176
+ for pin in symbol_def.pins:
177
+ if pin.number == pin_number:
178
+ # Calculate absolute position
179
+ # Apply component rotation/mirroring if needed (simplified for now)
180
+ absolute_x = component.position.x + pin.position.x
181
+ absolute_y = component.position.y + pin.position.y
182
+
183
+ return Point(absolute_x, absolute_y)
184
+
185
+ logger.warning(f"Pin {pin_number} not found on component {component_ref}")
186
+ return None
187
+
188
+ def list_component_pins(self, component_ref: str) -> List[Tuple[str, Point]]:
189
+ """
190
+ List all pins and their positions for a component.
191
+
192
+ Args:
193
+ component_ref: Component reference
194
+
195
+ Returns:
196
+ List of (pin_number, absolute_position) tuples
197
+ """
198
+ pins = []
199
+
200
+ # Find component
201
+ component = self._components.get(component_ref)
202
+ if not component:
203
+ return pins
204
+
205
+ # Get symbol definition
206
+ symbol_def = self._symbol_cache.get_symbol(component.lib_id)
207
+ if not symbol_def:
208
+ return pins
209
+
210
+ # Calculate absolute positions for all pins
211
+ for pin in symbol_def.pins:
212
+ absolute_x = component.position.x + pin.position.x
213
+ absolute_y = component.position.y + pin.position.y
214
+ pins.append((pin.number, Point(absolute_x, absolute_y)))
215
+
216
+ return pins
217
+
218
+ def auto_route_pins(
219
+ self,
220
+ component1_ref: str,
221
+ pin1_number: str,
222
+ component2_ref: str,
223
+ pin2_number: str,
224
+ routing_strategy: str = "direct",
225
+ ) -> List[str]:
226
+ """
227
+ Auto-route between two pins with different strategies.
228
+
229
+ Args:
230
+ component1_ref: First component reference
231
+ pin1_number: First component pin number
232
+ component2_ref: Second component reference
233
+ pin2_number: Second component pin number
234
+ routing_strategy: "direct" or "manhattan"
235
+
236
+ Returns:
237
+ List of wire UUIDs created
238
+
239
+ Raises:
240
+ ValueError: If components or pins not found
241
+ """
242
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
243
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
244
+
245
+ if pin1_pos is None:
246
+ raise ValueError(f"Pin {pin1_number} not found on component {component1_ref}")
247
+ if pin2_pos is None:
248
+ raise ValueError(f"Pin {pin2_number} not found on component {component2_ref}")
249
+
250
+ wire_uuids = []
251
+
252
+ if routing_strategy == "direct":
253
+ # Direct wire between pins
254
+ wire_uuid = self.add_wire(pin1_pos, pin2_pos)
255
+ wire_uuids.append(wire_uuid)
256
+
257
+ elif routing_strategy == "manhattan":
258
+ # Manhattan routing (L-shaped path)
259
+ # Route horizontally first, then vertically
260
+ intermediate_point = Point(pin2_pos.x, pin1_pos.y)
261
+
262
+ # Only add intermediate wire if it has length
263
+ if abs(pin1_pos.x - pin2_pos.x) > 0.1: # Minimum wire length
264
+ wire1_uuid = self.add_wire(pin1_pos, intermediate_point)
265
+ wire_uuids.append(wire1_uuid)
266
+
267
+ if abs(pin1_pos.y - pin2_pos.y) > 0.1: # Minimum wire length
268
+ wire2_uuid = self.add_wire(intermediate_point, pin2_pos)
269
+ wire_uuids.append(wire2_uuid)
270
+
271
+ else:
272
+ raise ValueError(f"Unknown routing strategy: {routing_strategy}")
273
+
274
+ logger.info(
275
+ f"Auto-routed {component1_ref}:{pin1_number} to {component2_ref}:{pin2_number} using {routing_strategy}"
276
+ )
277
+ return wire_uuids
278
+
279
+ def are_pins_connected(
280
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
281
+ ) -> bool:
282
+ """
283
+ Check if two pins are connected via wires.
284
+
285
+ Args:
286
+ component1_ref: First component reference
287
+ pin1_number: First component pin number
288
+ component2_ref: Second component reference
289
+ pin2_number: Second component pin number
290
+
291
+ Returns:
292
+ True if pins are connected, False otherwise
293
+ """
294
+ pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
295
+ pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
296
+
297
+ if pin1_pos is None or pin2_pos is None:
298
+ return False
299
+
300
+ # Check for direct wire connection
301
+ for wire in self._wires:
302
+ if (wire.start == pin1_pos and wire.end == pin2_pos) or (
303
+ wire.start == pin2_pos and wire.end == pin1_pos
304
+ ):
305
+ return True
306
+
307
+ # TODO: Implement more sophisticated connectivity analysis
308
+ # This would involve following wire networks through junctions
309
+ return False
310
+
311
+ def connect_pins_with_wire(
312
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
313
+ ) -> str:
314
+ """
315
+ Legacy alias for add_wire_between_pins.
316
+
317
+ Args:
318
+ component1_ref: First component reference
319
+ pin1_number: First component pin number
320
+ component2_ref: Second component reference
321
+ pin2_number: Second component pin number
322
+
323
+ Returns:
324
+ UUID of created wire
325
+ """
326
+ return self.add_wire_between_pins(component1_ref, pin1_number, component2_ref, pin2_number)
327
+
328
+ def get_wire_statistics(self) -> Dict[str, Any]:
329
+ """
330
+ Get statistics about wires in the schematic.
331
+
332
+ Returns:
333
+ Dictionary with wire statistics
334
+ """
335
+ total_wires = len(self._wires)
336
+ total_length = sum(wire.start.distance_to(wire.end) for wire in self._wires)
337
+
338
+ return {
339
+ "total_wires": total_wires,
340
+ "total_length": total_length,
341
+ "average_length": total_length / total_wires if total_wires > 0 else 0,
342
+ "wire_types": {
343
+ "normal": len([w for w in self._wires if w.wire_type == WireType.WIRE]),
344
+ "bus": len([w for w in self._wires if w.wire_type == WireType.BUS]),
345
+ },
346
+ }
@@ -0,0 +1,310 @@
1
+ """
2
+ Net management for KiCAD schematics.
3
+
4
+ This module provides collection classes for managing electrical nets,
5
+ featuring fast lookup, bulk operations, and validation.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
10
+
11
+ from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
12
+ from .types import Net
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class NetElement:
18
+ """
19
+ Enhanced wrapper for schematic net elements with modern API.
20
+
21
+ Provides intuitive access to net properties and operations
22
+ while maintaining exact format preservation.
23
+ """
24
+
25
+ def __init__(self, net_data: Net, parent_collection: "NetCollection"):
26
+ """
27
+ Initialize net element wrapper.
28
+
29
+ Args:
30
+ net_data: Underlying net data
31
+ parent_collection: Parent collection for updates
32
+ """
33
+ self._data = net_data
34
+ self._collection = parent_collection
35
+ self._validator = SchematicValidator()
36
+
37
+ # Core properties with validation
38
+ @property
39
+ def name(self) -> str:
40
+ """Net name."""
41
+ return self._data.name
42
+
43
+ @name.setter
44
+ def name(self, value: str):
45
+ """Set net name with validation."""
46
+ if not isinstance(value, str) or not value.strip():
47
+ raise ValidationError("Net name cannot be empty")
48
+ old_name = self._data.name
49
+ self._data.name = value.strip()
50
+ self._collection._update_name_index(old_name, self)
51
+ self._collection._mark_modified()
52
+
53
+ @property
54
+ def components(self) -> List[Tuple[str, str]]:
55
+ """List of component connections (reference, pin) tuples."""
56
+ return self._data.components.copy()
57
+
58
+ @property
59
+ def wires(self) -> List[str]:
60
+ """List of wire UUIDs in this net."""
61
+ return self._data.wires.copy()
62
+
63
+ @property
64
+ def labels(self) -> List[str]:
65
+ """List of label UUIDs in this net."""
66
+ return self._data.labels.copy()
67
+
68
+ def add_connection(self, reference: str, pin: str):
69
+ """Add component pin to net."""
70
+ self._data.add_connection(reference, pin)
71
+ self._collection._mark_modified()
72
+
73
+ def remove_connection(self, reference: str, pin: str):
74
+ """Remove component pin from net."""
75
+ self._data.remove_connection(reference, pin)
76
+ self._collection._mark_modified()
77
+
78
+ def add_wire(self, wire_uuid: str):
79
+ """Add wire to net."""
80
+ if wire_uuid not in self._data.wires:
81
+ self._data.wires.append(wire_uuid)
82
+ self._collection._mark_modified()
83
+
84
+ def remove_wire(self, wire_uuid: str):
85
+ """Remove wire from net."""
86
+ if wire_uuid in self._data.wires:
87
+ self._data.wires.remove(wire_uuid)
88
+ self._collection._mark_modified()
89
+
90
+ def add_label(self, label_uuid: str):
91
+ """Add label to net."""
92
+ if label_uuid not in self._data.labels:
93
+ self._data.labels.append(label_uuid)
94
+ self._collection._mark_modified()
95
+
96
+ def remove_label(self, label_uuid: str):
97
+ """Remove label from net."""
98
+ if label_uuid in self._data.labels:
99
+ self._data.labels.remove(label_uuid)
100
+ self._collection._mark_modified()
101
+
102
+ def validate(self) -> List[ValidationIssue]:
103
+ """Validate this net element."""
104
+ return self._validator.validate_net(self._data.__dict__)
105
+
106
+ def to_dict(self) -> Dict[str, Any]:
107
+ """Convert net element to dictionary representation."""
108
+ return {
109
+ "name": self.name,
110
+ "components": self.components,
111
+ "wires": self.wires,
112
+ "labels": self.labels,
113
+ }
114
+
115
+ def __str__(self) -> str:
116
+ """String representation."""
117
+ return f"<Net '{self.name}' ({len(self.components)} connections)>"
118
+
119
+
120
+ class NetCollection:
121
+ """
122
+ Collection class for efficient net management.
123
+
124
+ Provides fast lookup, filtering, and bulk operations for schematic nets.
125
+ """
126
+
127
+ def __init__(self, nets: List[Net] = None):
128
+ """
129
+ Initialize net collection.
130
+
131
+ Args:
132
+ nets: Initial list of net data
133
+ """
134
+ self._nets: List[NetElement] = []
135
+ self._name_index: Dict[str, NetElement] = {}
136
+ self._modified = False
137
+
138
+ # Add initial nets
139
+ if nets:
140
+ for net_data in nets:
141
+ self._add_to_indexes(NetElement(net_data, self))
142
+
143
+ logger.debug(f"NetCollection initialized with {len(self._nets)} nets")
144
+
145
+ def add(
146
+ self,
147
+ name: str,
148
+ components: List[Tuple[str, str]] = None,
149
+ wires: List[str] = None,
150
+ labels: List[str] = None,
151
+ ) -> NetElement:
152
+ """
153
+ Add a new net to the schematic.
154
+
155
+ Args:
156
+ name: Net name
157
+ components: Initial component connections
158
+ wires: Initial wire UUIDs
159
+ labels: Initial label UUIDs
160
+
161
+ Returns:
162
+ Newly created NetElement
163
+
164
+ Raises:
165
+ ValidationError: If net data is invalid
166
+ """
167
+ # Validate inputs
168
+ if not isinstance(name, str) or not name.strip():
169
+ raise ValidationError("Net name cannot be empty")
170
+
171
+ name = name.strip()
172
+
173
+ # Check for duplicate name
174
+ if name in self._name_index:
175
+ raise ValidationError(f"Net name {name} already exists")
176
+
177
+ # Create net data
178
+ net_data = Net(
179
+ name=name,
180
+ components=components or [],
181
+ wires=wires or [],
182
+ labels=labels or [],
183
+ )
184
+
185
+ # Create wrapper and add to collection
186
+ net_element = NetElement(net_data, self)
187
+ self._add_to_indexes(net_element)
188
+ self._mark_modified()
189
+
190
+ logger.debug(f"Added net: {net_element}")
191
+ return net_element
192
+
193
+ def get(self, name: str) -> Optional[NetElement]:
194
+ """Get net by name."""
195
+ return self._name_index.get(name)
196
+
197
+ def remove(self, name: str) -> bool:
198
+ """
199
+ Remove net by name.
200
+
201
+ Args:
202
+ name: Name of net to remove
203
+
204
+ Returns:
205
+ True if net was removed, False if not found
206
+ """
207
+ net_element = self._name_index.get(name)
208
+ if not net_element:
209
+ return False
210
+
211
+ # Remove from indexes
212
+ self._remove_from_indexes(net_element)
213
+ self._mark_modified()
214
+
215
+ logger.debug(f"Removed net: {net_element}")
216
+ return True
217
+
218
+ def find_by_component(self, reference: str, pin: Optional[str] = None) -> List[NetElement]:
219
+ """
220
+ Find nets connected to a component.
221
+
222
+ Args:
223
+ reference: Component reference
224
+ pin: Specific pin (if None, returns all nets for component)
225
+
226
+ Returns:
227
+ List of matching net elements
228
+ """
229
+ matches = []
230
+ for net_element in self._nets:
231
+ for comp_ref, comp_pin in net_element.components:
232
+ if comp_ref == reference and (pin is None or comp_pin == pin):
233
+ matches.append(net_element)
234
+ break
235
+ return matches
236
+
237
+ def filter(self, predicate: Callable[[NetElement], bool]) -> List[NetElement]:
238
+ """
239
+ Filter nets by predicate function.
240
+
241
+ Args:
242
+ predicate: Function that returns True for nets to include
243
+
244
+ Returns:
245
+ List of nets matching predicate
246
+ """
247
+ return [net for net in self._nets if predicate(net)]
248
+
249
+ def bulk_update(self, criteria: Callable[[NetElement], bool], updates: Dict[str, Any]):
250
+ """
251
+ Update multiple nets matching criteria.
252
+
253
+ Args:
254
+ criteria: Function to select nets to update
255
+ updates: Dictionary of property updates
256
+ """
257
+ updated_count = 0
258
+ for net_element in self._nets:
259
+ if criteria(net_element):
260
+ for prop, value in updates.items():
261
+ if hasattr(net_element, prop):
262
+ setattr(net_element, prop, value)
263
+ updated_count += 1
264
+
265
+ if updated_count > 0:
266
+ self._mark_modified()
267
+ logger.debug(f"Bulk updated {updated_count} net properties")
268
+
269
+ def clear(self):
270
+ """Remove all nets from collection."""
271
+ self._nets.clear()
272
+ self._name_index.clear()
273
+ self._mark_modified()
274
+
275
+ def _add_to_indexes(self, net_element: NetElement):
276
+ """Add net to internal indexes."""
277
+ self._nets.append(net_element)
278
+ self._name_index[net_element.name] = net_element
279
+
280
+ def _remove_from_indexes(self, net_element: NetElement):
281
+ """Remove net from internal indexes."""
282
+ self._nets.remove(net_element)
283
+ del self._name_index[net_element.name]
284
+
285
+ def _update_name_index(self, old_name: str, net_element: NetElement):
286
+ """Update name index when net name changes."""
287
+ if old_name in self._name_index:
288
+ del self._name_index[old_name]
289
+ self._name_index[net_element.name] = net_element
290
+
291
+ def _mark_modified(self):
292
+ """Mark collection as modified."""
293
+ self._modified = True
294
+
295
+ # Collection interface methods
296
+ def __len__(self) -> int:
297
+ """Return number of nets."""
298
+ return len(self._nets)
299
+
300
+ def __iter__(self) -> Iterator[NetElement]:
301
+ """Iterate over nets."""
302
+ return iter(self._nets)
303
+
304
+ def __getitem__(self, index: int) -> NetElement:
305
+ """Get net by index."""
306
+ return self._nets[index]
307
+
308
+ def __bool__(self) -> bool:
309
+ """Return True if collection has nets."""
310
+ return len(self._nets) > 0