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.

Files changed (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
@@ -10,6 +10,7 @@ import uuid
10
10
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
+ from .collections import BaseCollection
13
14
  from .types import Label, Point
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -114,10 +115,13 @@ class LabelElement:
114
115
  return f"<Label '{self.text}' @ {self.position}>"
115
116
 
116
117
 
117
- class LabelCollection:
118
+ class LabelCollection(BaseCollection[LabelElement]):
118
119
  """
119
120
  Collection class for efficient label element management.
120
121
 
122
+ Inherits from BaseCollection for standard operations and adds label-specific
123
+ functionality including text-based indexing.
124
+
121
125
  Provides fast lookup, filtering, and bulk operations for schematic label elements.
122
126
  """
123
127
 
@@ -128,18 +132,17 @@ class LabelCollection:
128
132
  Args:
129
133
  labels: Initial list of label data
130
134
  """
131
- self._labels: List[LabelElement] = []
132
- self._uuid_index: Dict[str, LabelElement] = {}
135
+ # Initialize base collection
136
+ super().__init__([], collection_name="labels")
137
+
138
+ # Additional label-specific index
133
139
  self._text_index: Dict[str, List[LabelElement]] = {}
134
- self._modified = False
135
140
 
136
141
  # Add initial labels
137
142
  if labels:
138
143
  for label_data in labels:
139
144
  self._add_to_indexes(LabelElement(label_data, self))
140
145
 
141
- logger.debug(f"LabelCollection initialized with {len(self._labels)} labels")
142
-
143
146
  def add(
144
147
  self,
145
148
  text: str,
@@ -201,9 +204,7 @@ class LabelCollection:
201
204
  logger.debug(f"Added label: {label_element}")
202
205
  return label_element
203
206
 
204
- def get(self, label_uuid: str) -> Optional[LabelElement]:
205
- """Get label by UUID."""
206
- return self._uuid_index.get(label_uuid)
207
+ # get() method inherited from BaseCollection
207
208
 
208
209
  def get_by_text(self, text: str) -> List[LabelElement]:
209
210
  """Get all labels with the given text."""
@@ -219,13 +220,19 @@ class LabelCollection:
219
220
  Returns:
220
221
  True if label was removed, False if not found
221
222
  """
222
- label_element = self._uuid_index.get(label_uuid)
223
+ label_element = self.get(label_uuid)
223
224
  if not label_element:
224
225
  return False
225
226
 
226
- # Remove from indexes
227
- self._remove_from_indexes(label_element)
228
- self._mark_modified()
227
+ # Remove from text index
228
+ text = label_element.text
229
+ if text in self._text_index:
230
+ self._text_index[text].remove(label_element)
231
+ if not self._text_index[text]:
232
+ del self._text_index[text]
233
+
234
+ # Remove using base class method
235
+ super().remove(label_uuid)
229
236
 
230
237
  logger.debug(f"Removed label: {label_element}")
231
238
  return True
@@ -245,7 +252,7 @@ class LabelCollection:
245
252
  return self._text_index.get(text, []).copy()
246
253
  else:
247
254
  matches = []
248
- for label_element in self._labels:
255
+ for label_element in self._items:
249
256
  if text.lower() in label_element.text.lower():
250
257
  matches.append(label_element)
251
258
  return matches
@@ -260,7 +267,7 @@ class LabelCollection:
260
267
  Returns:
261
268
  List of labels matching predicate
262
269
  """
263
- return [label for label in self._labels if predicate(label)]
270
+ return [label for label in self._items if predicate(label)]
264
271
 
265
272
  def bulk_update(self, criteria: Callable[[LabelElement], bool], updates: Dict[str, Any]):
266
273
  """
@@ -271,7 +278,7 @@ class LabelCollection:
271
278
  updates: Dictionary of property updates
272
279
  """
273
280
  updated_count = 0
274
- for label_element in self._labels:
281
+ for label_element in self._items:
275
282
  if criteria(label_element):
276
283
  for prop, value in updates.items():
277
284
  if hasattr(label_element, prop):
@@ -284,15 +291,12 @@ class LabelCollection:
284
291
 
285
292
  def clear(self):
286
293
  """Remove all labels from collection."""
287
- self._labels.clear()
288
- self._uuid_index.clear()
289
294
  self._text_index.clear()
290
- self._mark_modified()
295
+ super().clear()
291
296
 
292
297
  def _add_to_indexes(self, label_element: LabelElement):
293
- """Add label to internal indexes."""
294
- self._labels.append(label_element)
295
- self._uuid_index[label_element.uuid] = label_element
298
+ """Add label to internal indexes (base + text index)."""
299
+ self._add_item(label_element)
296
300
 
297
301
  # Add to text index
298
302
  text = label_element.text
@@ -300,18 +304,6 @@ class LabelCollection:
300
304
  self._text_index[text] = []
301
305
  self._text_index[text].append(label_element)
302
306
 
303
- def _remove_from_indexes(self, label_element: LabelElement):
304
- """Remove label from internal indexes."""
305
- self._labels.remove(label_element)
306
- del self._uuid_index[label_element.uuid]
307
-
308
- # Remove from text index
309
- text = label_element.text
310
- if text in self._text_index:
311
- self._text_index[text].remove(label_element)
312
- if not self._text_index[text]:
313
- del self._text_index[text]
314
-
315
307
  def _update_text_index(self, old_text: str, label_element: LabelElement):
316
308
  """Update text index when label text changes."""
317
309
  # Remove from old text index
@@ -326,23 +318,7 @@ class LabelCollection:
326
318
  self._text_index[new_text] = []
327
319
  self._text_index[new_text].append(label_element)
328
320
 
329
- def _mark_modified(self):
330
- """Mark collection as modified."""
331
- self._modified = True
332
-
333
- # Collection interface methods
334
- def __len__(self) -> int:
335
- """Return number of labels."""
336
- return len(self._labels)
337
-
338
- def __iter__(self) -> Iterator[LabelElement]:
339
- """Iterate over labels."""
340
- return iter(self._labels)
341
-
342
- def __getitem__(self, index: int) -> LabelElement:
343
- """Get label by index."""
344
- return self._labels[index]
345
-
321
+ # Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
346
322
  def __bool__(self) -> bool:
347
323
  """Return True if collection has labels."""
348
- return len(self._labels) > 0
324
+ return len(self._items) > 0
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
  from typing import Any, Dict, Optional, Union
12
12
 
13
13
  from ...utils.validation import ValidationError
14
+ from ..config import config
14
15
  from ..formatter import ExactFormatter
15
16
  from ..parser import SExpressionParser
16
17
 
@@ -219,9 +220,9 @@ class FileIOManager:
219
220
  return {
220
221
  "kicad_sch": {
221
222
  "version": 20230819,
222
- "generator": "kicad-sch-api",
223
+ "generator": config.file_format.generator_default,
223
224
  "uuid": None, # Will be set by calling code
224
- "paper": "A4",
225
+ "paper": config.paper.default,
225
226
  "lib_symbols": {},
226
227
  "symbol": [],
227
228
  "wire": [],
@@ -39,10 +39,10 @@ class MetadataManager:
39
39
  Args:
40
40
  paper: Paper size (e.g., "A4", "A3", "Letter", "Legal")
41
41
  """
42
- valid_sizes = ["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]
42
+ from ..config import config
43
43
 
44
- if paper not in valid_sizes:
45
- logger.warning(f"Unusual paper size: {paper}. Valid sizes: {valid_sizes}")
44
+ if paper not in config.paper.valid_sizes:
45
+ logger.warning(f"Unusual paper size: {paper}. Valid sizes: {config.paper.valid_sizes}")
46
46
 
47
47
  self._data["paper"] = paper
48
48
  logger.debug(f"Set paper size: {paper}")
@@ -260,9 +260,10 @@ class MetadataManager:
260
260
  issues.append("Title block missing title")
261
261
 
262
262
  # Check paper size
263
+ from ..config import config
264
+
263
265
  paper = self.get_paper_size()
264
- valid_sizes = ["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]
265
- if paper and paper not in valid_sizes:
266
+ if paper and paper not in config.paper.valid_sizes:
266
267
  issues.append(f"Non-standard paper size: {paper}")
267
268
 
268
269
  return issues
@@ -274,9 +274,10 @@ class ValidationManager:
274
274
  )
275
275
 
276
276
  # Check paper size
277
+ from ..config import config
278
+
277
279
  paper = self._data.get("paper")
278
- valid_papers = ["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]
279
- if paper and paper not in valid_papers:
280
+ if paper and paper not in config.paper.valid_sizes:
280
281
  issues.append(
281
282
  ValidationIssue(
282
283
  category="metadata",
@@ -305,7 +305,13 @@ class WireManager:
305
305
  return True
306
306
 
307
307
  # TODO: Implement more sophisticated connectivity analysis
308
- # This would involve following wire networks through junctions
308
+ # NOTE: Current implementation only checks for direct wire connections between pins.
309
+ # A full implementation would:
310
+ # 1. Follow wire networks through junctions (connection points)
311
+ # 2. Trace through labels (global/hierarchical net connections)
312
+ # 3. Build a complete net connectivity graph
313
+ # This is a known limitation - use ValidationManager for full electrical rule checking.
314
+ # Priority: MEDIUM - Would enable better automated wiring validation
309
315
  return False
310
316
 
311
317
  def connect_pins_with_wire(
@@ -9,6 +9,7 @@ import logging
9
9
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
10
10
 
11
11
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
12
+ from .collections import BaseCollection
12
13
  from .types import Net
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -35,6 +36,11 @@ class NetElement:
35
36
  self._validator = SchematicValidator()
36
37
 
37
38
  # Core properties with validation
39
+ @property
40
+ def uuid(self) -> str:
41
+ """Net UUID (uses name as unique identifier)."""
42
+ return self._data.name
43
+
38
44
  @property
39
45
  def name(self) -> str:
40
46
  """Net name."""
@@ -47,7 +53,9 @@ class NetElement:
47
53
  raise ValidationError("Net name cannot be empty")
48
54
  old_name = self._data.name
49
55
  self._data.name = value.strip()
56
+ # Update name index and rebuild base UUID index since UUID changed
50
57
  self._collection._update_name_index(old_name, self)
58
+ self._collection._rebuild_index()
51
59
  self._collection._mark_modified()
52
60
 
53
61
  @property
@@ -117,11 +125,15 @@ class NetElement:
117
125
  return f"<Net '{self.name}' ({len(self.components)} connections)>"
118
126
 
119
127
 
120
- class NetCollection:
128
+ class NetCollection(BaseCollection[NetElement]):
121
129
  """
122
130
  Collection class for efficient net management.
123
131
 
132
+ Inherits from BaseCollection for standard operations and adds net-specific
133
+ functionality including name-based indexing.
134
+
124
135
  Provides fast lookup, filtering, and bulk operations for schematic nets.
136
+ Note: Nets use name as the unique identifier (exposed as .uuid for protocol compatibility).
125
137
  """
126
138
 
127
139
  def __init__(self, nets: List[Net] = None):
@@ -131,17 +143,17 @@ class NetCollection:
131
143
  Args:
132
144
  nets: Initial list of net data
133
145
  """
134
- self._nets: List[NetElement] = []
146
+ # Initialize base collection
147
+ super().__init__([], collection_name="nets")
148
+
149
+ # Additional net-specific index (for convenience, duplicates base UUID index)
135
150
  self._name_index: Dict[str, NetElement] = {}
136
- self._modified = False
137
151
 
138
152
  # Add initial nets
139
153
  if nets:
140
154
  for net_data in nets:
141
155
  self._add_to_indexes(NetElement(net_data, self))
142
156
 
143
- logger.debug(f"NetCollection initialized with {len(self._nets)} nets")
144
-
145
157
  def add(
146
158
  self,
147
159
  name: str,
@@ -190,9 +202,11 @@ class NetCollection:
190
202
  logger.debug(f"Added net: {net_element}")
191
203
  return net_element
192
204
 
193
- def get(self, name: str) -> Optional[NetElement]:
194
- """Get net by name."""
195
- return self._name_index.get(name)
205
+ def get_by_name(self, name: str) -> Optional[NetElement]:
206
+ """Get net by name (convenience method, equivalent to get(name))."""
207
+ return self.get(name)
208
+
209
+ # get() method inherited from BaseCollection (uses name as UUID)
196
210
 
197
211
  def remove(self, name: str) -> bool:
198
212
  """
@@ -204,13 +218,16 @@ class NetCollection:
204
218
  Returns:
205
219
  True if net was removed, False if not found
206
220
  """
207
- net_element = self._name_index.get(name)
221
+ net_element = self.get(name)
208
222
  if not net_element:
209
223
  return False
210
224
 
211
- # Remove from indexes
212
- self._remove_from_indexes(net_element)
213
- self._mark_modified()
225
+ # Remove from name index
226
+ if net_element.name in self._name_index:
227
+ del self._name_index[net_element.name]
228
+
229
+ # Remove using base class method
230
+ super().remove(name)
214
231
 
215
232
  logger.debug(f"Removed net: {net_element}")
216
233
  return True
@@ -227,7 +244,7 @@ class NetCollection:
227
244
  List of matching net elements
228
245
  """
229
246
  matches = []
230
- for net_element in self._nets:
247
+ for net_element in self._items:
231
248
  for comp_ref, comp_pin in net_element.components:
232
249
  if comp_ref == reference and (pin is None or comp_pin == pin):
233
250
  matches.append(net_element)
@@ -236,7 +253,7 @@ class NetCollection:
236
253
 
237
254
  def filter(self, predicate: Callable[[NetElement], bool]) -> List[NetElement]:
238
255
  """
239
- Filter nets by predicate function.
256
+ Filter nets by predicate function (delegates to base class find).
240
257
 
241
258
  Args:
242
259
  predicate: Function that returns True for nets to include
@@ -244,7 +261,7 @@ class NetCollection:
244
261
  Returns:
245
262
  List of nets matching predicate
246
263
  """
247
- return [net for net in self._nets if predicate(net)]
264
+ return self.find(predicate)
248
265
 
249
266
  def bulk_update(self, criteria: Callable[[NetElement], bool], updates: Dict[str, Any]):
250
267
  """
@@ -255,7 +272,7 @@ class NetCollection:
255
272
  updates: Dictionary of property updates
256
273
  """
257
274
  updated_count = 0
258
- for net_element in self._nets:
275
+ for net_element in self._items:
259
276
  if criteria(net_element):
260
277
  for prop, value in updates.items():
261
278
  if hasattr(net_element, prop):
@@ -268,43 +285,21 @@ class NetCollection:
268
285
 
269
286
  def clear(self):
270
287
  """Remove all nets from collection."""
271
- self._nets.clear()
272
288
  self._name_index.clear()
273
- self._mark_modified()
289
+ super().clear()
274
290
 
275
291
  def _add_to_indexes(self, net_element: NetElement):
276
- """Add net to internal indexes."""
277
- self._nets.append(net_element)
292
+ """Add net to internal indexes (base + name index)."""
293
+ self._add_item(net_element)
278
294
  self._name_index[net_element.name] = net_element
279
295
 
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
296
  def _update_name_index(self, old_name: str, net_element: NetElement):
286
297
  """Update name index when net name changes."""
287
298
  if old_name in self._name_index:
288
299
  del self._name_index[old_name]
289
300
  self._name_index[net_element.name] = net_element
290
301
 
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
-
302
+ # Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
308
303
  def __bool__(self) -> bool:
309
304
  """Return True if collection has nets."""
310
- return len(self._nets) > 0
305
+ return len(self._items) > 0
@@ -10,6 +10,7 @@ import uuid
10
10
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
+ from .collections import BaseCollection
13
14
  from .types import NoConnect, Point
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -72,10 +73,13 @@ class NoConnectElement:
72
73
  return f"<NoConnect @ {self.position}>"
73
74
 
74
75
 
75
- class NoConnectCollection:
76
+ class NoConnectCollection(BaseCollection[NoConnectElement]):
76
77
  """
77
78
  Collection class for efficient no-connect element management.
78
79
 
80
+ Inherits from BaseCollection for standard operations and adds no-connect-specific
81
+ functionality including position-based indexing.
82
+
79
83
  Provides fast lookup, filtering, and bulk operations for schematic no-connect elements.
80
84
  """
81
85
 
@@ -86,18 +90,17 @@ class NoConnectCollection:
86
90
  Args:
87
91
  no_connects: Initial list of no-connect data
88
92
  """
89
- self._no_connects: List[NoConnectElement] = []
90
- self._uuid_index: Dict[str, NoConnectElement] = {}
93
+ # Initialize base collection
94
+ super().__init__([], collection_name="no_connects")
95
+
96
+ # Additional no-connect-specific index
91
97
  self._position_index: Dict[Tuple[float, float], List[NoConnectElement]] = {}
92
- self._modified = False
93
98
 
94
99
  # Add initial no-connects
95
100
  if no_connects:
96
101
  for no_connect_data in no_connects:
97
102
  self._add_to_indexes(NoConnectElement(no_connect_data, self))
98
103
 
99
- logger.debug(f"NoConnectCollection initialized with {len(self._no_connects)} no-connects")
100
-
101
104
  def add(
102
105
  self,
103
106
  position: Union[Point, Tuple[float, float]],
@@ -144,9 +147,7 @@ class NoConnectCollection:
144
147
  logger.debug(f"Added no-connect: {no_connect_element}")
145
148
  return no_connect_element
146
149
 
147
- def get(self, no_connect_uuid: str) -> Optional[NoConnectElement]:
148
- """Get no-connect by UUID."""
149
- return self._uuid_index.get(no_connect_uuid)
150
+ # get() method inherited from BaseCollection
150
151
 
151
152
  def remove(self, no_connect_uuid: str) -> bool:
152
153
  """
@@ -158,13 +159,19 @@ class NoConnectCollection:
158
159
  Returns:
159
160
  True if no-connect was removed, False if not found
160
161
  """
161
- no_connect_element = self._uuid_index.get(no_connect_uuid)
162
+ no_connect_element = self.get(no_connect_uuid)
162
163
  if not no_connect_element:
163
164
  return False
164
165
 
165
- # Remove from indexes
166
- self._remove_from_indexes(no_connect_element)
167
- self._mark_modified()
166
+ # Remove from position index
167
+ pos_key = (no_connect_element.position.x, no_connect_element.position.y)
168
+ if pos_key in self._position_index:
169
+ self._position_index[pos_key].remove(no_connect_element)
170
+ if not self._position_index[pos_key]:
171
+ del self._position_index[pos_key]
172
+
173
+ # Remove using base class method
174
+ super().remove(no_connect_uuid)
168
175
 
169
176
  logger.debug(f"Removed no-connect: {no_connect_element}")
170
177
  return True
@@ -186,7 +193,7 @@ class NoConnectCollection:
186
193
  position = Point(position[0], position[1])
187
194
 
188
195
  matches = []
189
- for no_connect_element in self._no_connects:
196
+ for no_connect_element in self._items:
190
197
  distance = no_connect_element.position.distance_to(position)
191
198
  if distance <= tolerance:
192
199
  matches.append(no_connect_element)
@@ -194,7 +201,7 @@ class NoConnectCollection:
194
201
 
195
202
  def filter(self, predicate: Callable[[NoConnectElement], bool]) -> List[NoConnectElement]:
196
203
  """
197
- Filter no-connects by predicate function.
204
+ Filter no-connects by predicate function (delegates to base class find).
198
205
 
199
206
  Args:
200
207
  predicate: Function that returns True for no-connects to include
@@ -202,7 +209,7 @@ class NoConnectCollection:
202
209
  Returns:
203
210
  List of no-connects matching predicate
204
211
  """
205
- return [no_connect for no_connect in self._no_connects if predicate(no_connect)]
212
+ return self.find(predicate)
206
213
 
207
214
  def bulk_update(self, criteria: Callable[[NoConnectElement], bool], updates: Dict[str, Any]):
208
215
  """
@@ -213,7 +220,7 @@ class NoConnectCollection:
213
220
  updates: Dictionary of property updates
214
221
  """
215
222
  updated_count = 0
216
- for no_connect_element in self._no_connects:
223
+ for no_connect_element in self._items:
217
224
  if criteria(no_connect_element):
218
225
  for prop, value in updates.items():
219
226
  if hasattr(no_connect_element, prop):
@@ -226,15 +233,12 @@ class NoConnectCollection:
226
233
 
227
234
  def clear(self):
228
235
  """Remove all no-connects from collection."""
229
- self._no_connects.clear()
230
- self._uuid_index.clear()
231
236
  self._position_index.clear()
232
- self._mark_modified()
237
+ super().clear()
233
238
 
234
239
  def _add_to_indexes(self, no_connect_element: NoConnectElement):
235
- """Add no-connect to internal indexes."""
236
- self._no_connects.append(no_connect_element)
237
- self._uuid_index[no_connect_element.uuid] = no_connect_element
240
+ """Add no-connect to internal indexes (base + position index)."""
241
+ self._add_item(no_connect_element)
238
242
 
239
243
  # Add to position index
240
244
  pos_key = (no_connect_element.position.x, no_connect_element.position.y)
@@ -242,35 +246,7 @@ class NoConnectCollection:
242
246
  self._position_index[pos_key] = []
243
247
  self._position_index[pos_key].append(no_connect_element)
244
248
 
245
- def _remove_from_indexes(self, no_connect_element: NoConnectElement):
246
- """Remove no-connect from internal indexes."""
247
- self._no_connects.remove(no_connect_element)
248
- del self._uuid_index[no_connect_element.uuid]
249
-
250
- # Remove from position index
251
- pos_key = (no_connect_element.position.x, no_connect_element.position.y)
252
- if pos_key in self._position_index:
253
- self._position_index[pos_key].remove(no_connect_element)
254
- if not self._position_index[pos_key]:
255
- del self._position_index[pos_key]
256
-
257
- def _mark_modified(self):
258
- """Mark collection as modified."""
259
- self._modified = True
260
-
261
- # Collection interface methods
262
- def __len__(self) -> int:
263
- """Return number of no-connects."""
264
- return len(self._no_connects)
265
-
266
- def __iter__(self) -> Iterator[NoConnectElement]:
267
- """Iterate over no-connects."""
268
- return iter(self._no_connects)
269
-
270
- def __getitem__(self, index: int) -> NoConnectElement:
271
- """Get no-connect by index."""
272
- return self._no_connects[index]
273
-
249
+ # Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
274
250
  def __bool__(self) -> bool:
275
251
  """Return True if collection has no-connects."""
276
- return len(self._no_connects) > 0
252
+ return len(self._items) > 0