kicad-sch-api 0.3.5__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.
Files changed (81) 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/collections/__init__.py +2 -2
  10. kicad_sch_api/collections/base.py +5 -7
  11. kicad_sch_api/collections/components.py +24 -12
  12. kicad_sch_api/collections/junctions.py +31 -43
  13. kicad_sch_api/collections/labels.py +19 -27
  14. kicad_sch_api/collections/wires.py +17 -18
  15. kicad_sch_api/core/collections/__init__.py +5 -0
  16. kicad_sch_api/core/collections/base.py +248 -0
  17. kicad_sch_api/core/component_bounds.py +5 -0
  18. kicad_sch_api/core/components.py +67 -45
  19. kicad_sch_api/core/config.py +85 -3
  20. kicad_sch_api/core/factories/__init__.py +5 -0
  21. kicad_sch_api/core/factories/element_factory.py +276 -0
  22. kicad_sch_api/core/formatter.py +3 -1
  23. kicad_sch_api/core/junctions.py +26 -75
  24. kicad_sch_api/core/labels.py +29 -53
  25. kicad_sch_api/core/managers/__init__.py +26 -0
  26. kicad_sch_api/core/managers/file_io.py +244 -0
  27. kicad_sch_api/core/managers/format_sync.py +501 -0
  28. kicad_sch_api/core/managers/graphics.py +579 -0
  29. kicad_sch_api/core/managers/metadata.py +269 -0
  30. kicad_sch_api/core/managers/sheet.py +454 -0
  31. kicad_sch_api/core/managers/text_elements.py +536 -0
  32. kicad_sch_api/core/managers/validation.py +475 -0
  33. kicad_sch_api/core/managers/wire.py +352 -0
  34. kicad_sch_api/core/nets.py +38 -43
  35. kicad_sch_api/core/no_connects.py +33 -55
  36. kicad_sch_api/core/parser.py +75 -1731
  37. kicad_sch_api/core/schematic.py +951 -1192
  38. kicad_sch_api/core/texts.py +28 -55
  39. kicad_sch_api/core/types.py +60 -22
  40. kicad_sch_api/core/wires.py +27 -75
  41. kicad_sch_api/geometry/font_metrics.py +3 -1
  42. kicad_sch_api/geometry/symbol_bbox.py +40 -21
  43. kicad_sch_api/interfaces/__init__.py +1 -1
  44. kicad_sch_api/interfaces/parser.py +1 -1
  45. kicad_sch_api/interfaces/repository.py +1 -1
  46. kicad_sch_api/interfaces/resolver.py +1 -1
  47. kicad_sch_api/parsers/__init__.py +2 -2
  48. kicad_sch_api/parsers/base.py +7 -10
  49. kicad_sch_api/parsers/elements/__init__.py +22 -0
  50. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  51. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  52. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  53. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  54. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  55. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  56. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  57. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  58. kicad_sch_api/parsers/registry.py +4 -2
  59. kicad_sch_api/parsers/utils.py +80 -0
  60. kicad_sch_api/symbols/__init__.py +1 -1
  61. kicad_sch_api/symbols/cache.py +9 -12
  62. kicad_sch_api/symbols/resolver.py +20 -26
  63. kicad_sch_api/symbols/validators.py +188 -137
  64. kicad_sch_api/validation/__init__.py +25 -0
  65. kicad_sch_api/validation/erc.py +171 -0
  66. kicad_sch_api/validation/erc_models.py +203 -0
  67. kicad_sch_api/validation/pin_matrix.py +243 -0
  68. kicad_sch_api/validation/validators.py +391 -0
  69. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
  70. kicad_sch_api-0.4.1.dist-info/RECORD +87 -0
  71. kicad_sch_api/core/manhattan_routing.py +0 -430
  72. kicad_sch_api/core/simple_manhattan.py +0 -228
  73. kicad_sch_api/core/wire_routing.py +0 -380
  74. kicad_sch_api/parsers/label_parser.py +0 -254
  75. kicad_sch_api/parsers/symbol_parser.py +0 -227
  76. kicad_sch_api/parsers/wire_parser.py +0 -99
  77. kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
  78. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
  79. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
  80. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
  81. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.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 Point, Text
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -124,10 +125,13 @@ class TextElement:
124
125
  return f"<Text '{self.text}' @ {self.position}>"
125
126
 
126
127
 
127
- class TextCollection:
128
+ class TextCollection(BaseCollection[TextElement]):
128
129
  """
129
130
  Collection class for efficient text element management.
130
131
 
132
+ Inherits from BaseCollection for standard operations and adds text-specific
133
+ functionality including content-based indexing.
134
+
131
135
  Provides fast lookup, filtering, and bulk operations for schematic text elements.
132
136
  """
133
137
 
@@ -138,18 +142,17 @@ class TextCollection:
138
142
  Args:
139
143
  texts: Initial list of text data
140
144
  """
141
- self._texts: List[TextElement] = []
142
- self._uuid_index: Dict[str, TextElement] = {}
145
+ # Initialize base collection with empty list (we'll add elements below)
146
+ super().__init__([], collection_name="texts")
147
+
148
+ # Additional text-specific index
143
149
  self._content_index: Dict[str, List[TextElement]] = {}
144
- self._modified = False
145
150
 
146
151
  # Add initial texts
147
152
  if texts:
148
153
  for text_data in texts:
149
154
  self._add_to_indexes(TextElement(text_data, self))
150
155
 
151
- logger.debug(f"TextCollection initialized with {len(self._texts)} texts")
152
-
153
156
  def add(
154
157
  self,
155
158
  text: str,
@@ -209,15 +212,10 @@ class TextCollection:
209
212
  # Create wrapper and add to collection
210
213
  text_element = TextElement(text_data, self)
211
214
  self._add_to_indexes(text_element)
212
- self._mark_modified()
213
215
 
214
216
  logger.debug(f"Added text: {text_element}")
215
217
  return text_element
216
218
 
217
- def get(self, text_uuid: str) -> Optional[TextElement]:
218
- """Get text by UUID."""
219
- return self._uuid_index.get(text_uuid)
220
-
221
219
  def remove(self, text_uuid: str) -> bool:
222
220
  """
223
221
  Remove text by UUID.
@@ -228,13 +226,19 @@ class TextCollection:
228
226
  Returns:
229
227
  True if text was removed, False if not found
230
228
  """
231
- text_element = self._uuid_index.get(text_uuid)
229
+ text_element = self.get(text_uuid)
232
230
  if not text_element:
233
231
  return False
234
232
 
235
- # Remove from indexes
236
- self._remove_from_indexes(text_element)
237
- self._mark_modified()
233
+ # Remove from content index
234
+ content = text_element.text
235
+ if content in self._content_index:
236
+ self._content_index[content].remove(text_element)
237
+ if not self._content_index[content]:
238
+ del self._content_index[content]
239
+
240
+ # Remove using base class method
241
+ super().remove(text_uuid)
238
242
 
239
243
  logger.debug(f"Removed text: {text_element}")
240
244
  return True
@@ -254,14 +258,14 @@ class TextCollection:
254
258
  return self._content_index.get(content, []).copy()
255
259
  else:
256
260
  matches = []
257
- for text_element in self._texts:
261
+ for text_element in self._items:
258
262
  if content.lower() in text_element.text.lower():
259
263
  matches.append(text_element)
260
264
  return matches
261
265
 
262
266
  def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
263
267
  """
264
- Filter texts by predicate function.
268
+ Filter texts by predicate function (delegates to base class find).
265
269
 
266
270
  Args:
267
271
  predicate: Function that returns True for texts to include
@@ -269,7 +273,7 @@ class TextCollection:
269
273
  Returns:
270
274
  List of texts matching predicate
271
275
  """
272
- return [text for text in self._texts if predicate(text)]
276
+ return self.find(predicate)
273
277
 
274
278
  def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
275
279
  """
@@ -280,7 +284,7 @@ class TextCollection:
280
284
  updates: Dictionary of property updates
281
285
  """
282
286
  updated_count = 0
283
- for text_element in self._texts:
287
+ for text_element in self._items:
284
288
  if criteria(text_element):
285
289
  for prop, value in updates.items():
286
290
  if hasattr(text_element, prop):
@@ -293,15 +297,12 @@ class TextCollection:
293
297
 
294
298
  def clear(self):
295
299
  """Remove all texts from collection."""
296
- self._texts.clear()
297
- self._uuid_index.clear()
298
300
  self._content_index.clear()
299
- self._mark_modified()
301
+ super().clear()
300
302
 
301
303
  def _add_to_indexes(self, text_element: TextElement):
302
- """Add text to internal indexes."""
303
- self._texts.append(text_element)
304
- self._uuid_index[text_element.uuid] = text_element
304
+ """Add text to internal indexes (base + content index)."""
305
+ self._add_item(text_element)
305
306
 
306
307
  # Add to content index
307
308
  content = text_element.text
@@ -309,35 +310,7 @@ class TextCollection:
309
310
  self._content_index[content] = []
310
311
  self._content_index[content].append(text_element)
311
312
 
312
- def _remove_from_indexes(self, text_element: TextElement):
313
- """Remove text from internal indexes."""
314
- self._texts.remove(text_element)
315
- del self._uuid_index[text_element.uuid]
316
-
317
- # Remove from content index
318
- content = text_element.text
319
- if content in self._content_index:
320
- self._content_index[content].remove(text_element)
321
- if not self._content_index[content]:
322
- del self._content_index[content]
323
-
324
- def _mark_modified(self):
325
- """Mark collection as modified."""
326
- self._modified = True
327
-
328
- # Collection interface methods
329
- def __len__(self) -> int:
330
- """Return number of texts."""
331
- return len(self._texts)
332
-
333
- def __iter__(self) -> Iterator[TextElement]:
334
- """Iterate over texts."""
335
- return iter(self._texts)
336
-
337
- def __getitem__(self, index: int) -> TextElement:
338
- """Get text by index."""
339
- return self._texts[index]
340
-
313
+ # Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
341
314
  def __bool__(self) -> bool:
342
315
  """Return True if collection has texts."""
343
- return len(self._texts) > 0
316
+ return len(self._items) > 0
@@ -18,14 +18,14 @@ class Point:
18
18
  x: float
19
19
  y: float
20
20
 
21
- def __post_init__(self):
21
+ def __post_init__(self) -> None:
22
22
  # Ensure coordinates are float
23
23
  object.__setattr__(self, "x", float(self.x))
24
24
  object.__setattr__(self, "y", float(self.y))
25
25
 
26
26
  def distance_to(self, other: "Point") -> float:
27
27
  """Calculate distance to another point."""
28
- return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
28
+ return float(((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5)
29
29
 
30
30
  def offset(self, dx: float, dy: float) -> "Point":
31
31
  """Create new point offset by dx, dy."""
@@ -35,6 +35,43 @@ class Point:
35
35
  return f"({self.x:.3f}, {self.y:.3f})"
36
36
 
37
37
 
38
+ def point_from_dict_or_tuple(
39
+ position: Union[Point, Dict[str, float], Tuple[float, float], List[float], Any]
40
+ ) -> Point:
41
+ """
42
+ Convert various position formats to a Point object.
43
+
44
+ Supports multiple input formats for maximum flexibility:
45
+ - Point: Returns as-is
46
+ - Dict with 'x' and 'y' keys: Extracts and creates Point
47
+ - Tuple/List with 2 elements: Creates Point from coordinates
48
+ - Other: Returns as-is (assumes it's already a Point-like object)
49
+
50
+ Args:
51
+ position: Position in any supported format
52
+
53
+ Returns:
54
+ Point object
55
+
56
+ Example:
57
+ >>> point_from_dict_or_tuple({"x": 10, "y": 20})
58
+ Point(x=10.0, y=20.0)
59
+ >>> point_from_dict_or_tuple((10, 20))
60
+ Point(x=10.0, y=20.0)
61
+ >>> point_from_dict_or_tuple(Point(10, 20))
62
+ Point(x=10.0, y=20.0)
63
+ """
64
+ if isinstance(position, Point):
65
+ return position
66
+ elif isinstance(position, dict):
67
+ return Point(position.get("x", 0), position.get("y", 0))
68
+ elif isinstance(position, (list, tuple)) and len(position) >= 2:
69
+ return Point(position[0], position[1])
70
+ else:
71
+ # Assume it's already a Point-like object or will be handled by caller
72
+ return position
73
+
74
+
38
75
  @dataclass(frozen=True)
39
76
  class Rectangle:
40
77
  """Rectangle defined by two corner points."""
@@ -110,7 +147,7 @@ class SchematicPin:
110
147
  length: float = 2.54 # Standard pin length in mm
111
148
  rotation: float = 0.0 # Rotation in degrees
112
149
 
113
- def __post_init__(self):
150
+ def __post_init__(self) -> None:
114
151
  # Ensure types are correct
115
152
  self.pin_type = PinType(self.pin_type) if isinstance(self.pin_type, str) else self.pin_type
116
153
  self.pin_shape = (
@@ -135,7 +172,7 @@ class SchematicSymbol:
135
172
  on_board: bool = True
136
173
  unit: int = 1
137
174
 
138
- def __post_init__(self):
175
+ def __post_init__(self) -> None:
139
176
  # Generate UUID if not provided
140
177
  if not self.uuid:
141
178
  self.uuid = str(uuid4())
@@ -163,6 +200,10 @@ class SchematicSymbol:
163
200
  if not pin:
164
201
  return None
165
202
  # TODO: Apply rotation and symbol position transformation
203
+ # NOTE: Currently assumes 0° rotation. For rotated components, pin positions
204
+ # would need to be transformed using rotation matrix before adding to component position.
205
+ # This affects pin-to-pin wiring accuracy for rotated components.
206
+ # Priority: MEDIUM - Would improve wiring accuracy for rotated components
166
207
  return Point(self.position.x + pin.position.x, self.position.y + pin.position.y)
167
208
 
168
209
 
@@ -183,7 +224,7 @@ class Wire:
183
224
  stroke_width: float = 0.0
184
225
  stroke_type: str = "default"
185
226
 
186
- def __post_init__(self):
227
+ def __post_init__(self) -> None:
187
228
  if not self.uuid:
188
229
  self.uuid = str(uuid4())
189
230
 
@@ -196,7 +237,7 @@ class Wire:
196
237
  raise ValueError("Wire must have at least 2 points")
197
238
 
198
239
  @classmethod
199
- def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs) -> "Wire":
240
+ def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs: Any) -> "Wire":
200
241
  """Create wire from start and end points (convenience method)."""
201
242
  return cls(uuid=uuid, points=[start, end], **kwargs)
202
243
 
@@ -244,7 +285,7 @@ class Junction:
244
285
  diameter: float = 0 # KiCAD default diameter
245
286
  color: Tuple[int, int, int, int] = (0, 0, 0, 0) # RGBA color
246
287
 
247
- def __post_init__(self):
288
+ def __post_init__(self) -> None:
248
289
  if not self.uuid:
249
290
  self.uuid = str(uuid4())
250
291
 
@@ -280,7 +321,7 @@ class Label:
280
321
  size: float = 1.27
281
322
  shape: Optional[HierarchicalLabelShape] = None # Only for hierarchical labels
282
323
 
283
- def __post_init__(self):
324
+ def __post_init__(self) -> None:
284
325
  if not self.uuid:
285
326
  self.uuid = str(uuid4())
286
327
 
@@ -305,7 +346,7 @@ class Text:
305
346
  size: float = 1.27
306
347
  exclude_from_sim: bool = False
307
348
 
308
- def __post_init__(self):
349
+ def __post_init__(self) -> None:
309
350
  if not self.uuid:
310
351
  self.uuid = str(uuid4())
311
352
 
@@ -333,7 +374,7 @@ class TextBox:
333
374
  justify_vertical: str = "top"
334
375
  exclude_from_sim: bool = False
335
376
 
336
- def __post_init__(self):
377
+ def __post_init__(self) -> None:
337
378
  if not self.uuid:
338
379
  self.uuid = str(uuid4())
339
380
 
@@ -349,7 +390,7 @@ class SchematicRectangle:
349
390
  stroke_type: str = "default"
350
391
  fill_type: str = "none"
351
392
 
352
- def __post_init__(self):
393
+ def __post_init__(self) -> None:
353
394
  if not self.uuid:
354
395
  self.uuid = str(uuid4())
355
396
 
@@ -366,10 +407,7 @@ class SchematicRectangle:
366
407
  @property
367
408
  def center(self) -> Point:
368
409
  """Rectangle center point."""
369
- return Point(
370
- (self.start.x + self.end.x) / 2,
371
- (self.start.y + self.end.y) / 2
372
- )
410
+ return Point((self.start.x + self.end.x) / 2, (self.start.y + self.end.y) / 2)
373
411
 
374
412
 
375
413
  @dataclass
@@ -381,7 +419,7 @@ class Image:
381
419
  data: str # Base64-encoded image data
382
420
  scale: float = 1.0
383
421
 
384
- def __post_init__(self):
422
+ def __post_init__(self) -> None:
385
423
  if not self.uuid:
386
424
  self.uuid = str(uuid4())
387
425
 
@@ -393,7 +431,7 @@ class NoConnect:
393
431
  uuid: str
394
432
  position: Point
395
433
 
396
- def __post_init__(self):
434
+ def __post_init__(self) -> None:
397
435
  if not self.uuid:
398
436
  self.uuid = str(uuid4())
399
437
 
@@ -407,13 +445,13 @@ class Net:
407
445
  wires: List[str] = field(default_factory=list) # Wire UUIDs
408
446
  labels: List[str] = field(default_factory=list) # Label UUIDs
409
447
 
410
- def add_connection(self, reference: str, pin: str):
448
+ def add_connection(self, reference: str, pin: str) -> None:
411
449
  """Add component pin to net."""
412
450
  connection = (reference, pin)
413
451
  if connection not in self.components:
414
452
  self.components.append(connection)
415
453
 
416
- def remove_connection(self, reference: str, pin: str):
454
+ def remove_connection(self, reference: str, pin: str) -> None:
417
455
  """Remove component pin from net."""
418
456
  connection = (reference, pin)
419
457
  if connection in self.components:
@@ -439,7 +477,7 @@ class Sheet:
439
477
  stroke_type: str = "solid"
440
478
  fill_color: Tuple[float, float, float, float] = (0, 0, 0, 0.0)
441
479
 
442
- def __post_init__(self):
480
+ def __post_init__(self) -> None:
443
481
  if not self.uuid:
444
482
  self.uuid = str(uuid4())
445
483
 
@@ -454,7 +492,7 @@ class SheetPin:
454
492
  pin_type: PinType = PinType.BIDIRECTIONAL
455
493
  size: float = 1.27
456
494
 
457
- def __post_init__(self):
495
+ def __post_init__(self) -> None:
458
496
  if not self.uuid:
459
497
  self.uuid = str(uuid4())
460
498
 
@@ -497,7 +535,7 @@ class Schematic:
497
535
  rectangles: List[SchematicRectangle] = field(default_factory=list)
498
536
  lib_symbols: Dict[str, Any] = field(default_factory=dict)
499
537
 
500
- def __post_init__(self):
538
+ def __post_init__(self) -> None:
501
539
  if not self.uuid:
502
540
  self.uuid = str(uuid4())
503
541
 
@@ -9,56 +9,36 @@ import logging
9
9
  import uuid as uuid_module
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
11
 
12
+ from .collections import BaseCollection
12
13
  from .types import Point, Wire, WireType
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
16
17
 
17
- class WireCollection:
18
+ class WireCollection(BaseCollection[Wire]):
18
19
  """
19
20
  Professional wire collection with enhanced management features.
20
21
 
22
+ Inherits from BaseCollection for standard operations and adds wire-specific
23
+ functionality.
24
+
21
25
  Features:
22
- - Fast UUID-based lookup and indexing
23
- - Bulk operations for performance
26
+ - Fast UUID-based lookup and indexing (inherited)
27
+ - Bulk operations for performance (inherited)
24
28
  - Multi-point wire support
25
29
  - Validation and conflict detection
26
30
  - Junction management integration
31
+ - Wire geometry queries (horizontal, vertical, by-point)
27
32
  """
28
33
 
29
- def __init__(self, wires: Optional[List[Wire]] = None):
34
+ def __init__(self, wires: Optional[List[Wire]] = None) -> None:
30
35
  """
31
36
  Initialize wire collection.
32
37
 
33
38
  Args:
34
39
  wires: Initial list of wires
35
40
  """
36
- self._wires: List[Wire] = wires or []
37
- self._uuid_index: Dict[str, int] = {}
38
- self._modified = False
39
-
40
- # Build UUID index
41
- self._rebuild_index()
42
-
43
- logger.debug(f"WireCollection initialized with {len(self._wires)} wires")
44
-
45
- def _rebuild_index(self):
46
- """Rebuild UUID index for fast lookups."""
47
- self._uuid_index = {wire.uuid: i for i, wire in enumerate(self._wires)}
48
-
49
- def __len__(self) -> int:
50
- """Number of wires in collection."""
51
- return len(self._wires)
52
-
53
- def __iter__(self):
54
- """Iterate over wires."""
55
- return iter(self._wires)
56
-
57
- def __getitem__(self, uuid: str) -> Wire:
58
- """Get wire by UUID."""
59
- if uuid not in self._uuid_index:
60
- raise KeyError(f"Wire with UUID '{uuid}' not found")
61
- return self._wires[self._uuid_index[uuid]]
41
+ super().__init__(wires, collection_name="wires")
62
42
 
63
43
  def add(
64
44
  self,
@@ -114,37 +94,14 @@ class WireCollection:
114
94
  # Create wire
115
95
  wire = Wire(uuid=uuid, points=wire_points, wire_type=wire_type, stroke_width=stroke_width)
116
96
 
117
- # Add to collection
118
- self._wires.append(wire)
119
- self._uuid_index[uuid] = len(self._wires) - 1
120
- self._modified = True
97
+ # Add to collection using base class method
98
+ self._add_item(wire)
121
99
 
122
100
  logger.debug(f"Added wire: {len(wire_points)} points, UUID={uuid}")
123
101
  return uuid
124
102
 
125
- def remove(self, uuid: str) -> bool:
126
- """
127
- Remove wire by UUID.
128
-
129
- Args:
130
- uuid: Wire UUID to remove
131
-
132
- Returns:
133
- True if wire was removed, False if not found
134
- """
135
- if uuid not in self._uuid_index:
136
- return False
137
-
138
- index = self._uuid_index[uuid]
139
- del self._wires[index]
140
- self._rebuild_index()
141
- self._modified = True
142
-
143
- logger.debug(f"Removed wire: {uuid}")
144
- return True
145
-
146
103
  def get_by_point(
147
- self, point: Union[Point, Tuple[float, float]], tolerance: float = None
104
+ self, point: Union[Point, Tuple[float, float]], tolerance: Optional[float] = None
148
105
  ) -> List[Wire]:
149
106
  """
150
107
  Find wires that pass through or near a point.
@@ -164,7 +121,7 @@ class WireCollection:
164
121
  point = Point(point[0], point[1])
165
122
 
166
123
  matching_wires = []
167
- for wire in self._wires:
124
+ for wire in self._items:
168
125
  # Check if any wire point is close
169
126
  for wire_point in wire.points:
170
127
  if wire_point.distance_to(point) <= tolerance:
@@ -213,40 +170,35 @@ class WireCollection:
213
170
 
214
171
  def get_horizontal_wires(self) -> List[Wire]:
215
172
  """Get all horizontal wires."""
216
- return [wire for wire in self._wires if wire.is_horizontal()]
173
+ return [wire for wire in self._items if wire.is_horizontal()]
217
174
 
218
175
  def get_vertical_wires(self) -> List[Wire]:
219
176
  """Get all vertical wires."""
220
- return [wire for wire in self._wires if wire.is_vertical()]
177
+ return [wire for wire in self._items if wire.is_vertical()]
221
178
 
222
179
  def get_statistics(self) -> Dict[str, Any]:
223
- """Get wire collection statistics."""
224
- total_length = sum(wire.length for wire in self._wires)
225
- simple_wires = sum(1 for wire in self._wires if wire.is_simple())
226
- multi_point_wires = len(self._wires) - simple_wires
180
+ """Get wire collection statistics (extends base statistics)."""
181
+ base_stats = super().get_statistics()
182
+ total_length = sum(wire.length for wire in self._items)
183
+ simple_wires = sum(1 for wire in self._items if wire.is_simple())
184
+ multi_point_wires = len(self._items) - simple_wires
227
185
 
228
186
  return {
229
- "total_wires": len(self._wires),
187
+ **base_stats,
188
+ "total_wires": len(self._items),
230
189
  "simple_wires": simple_wires,
231
190
  "multi_point_wires": multi_point_wires,
232
191
  "total_length": total_length,
233
- "avg_length": total_length / len(self._wires) if self._wires else 0,
192
+ "avg_length": total_length / len(self._items) if self._items else 0,
234
193
  "horizontal_wires": len(self.get_horizontal_wires()),
235
194
  "vertical_wires": len(self.get_vertical_wires()),
236
195
  }
237
196
 
238
- def clear(self):
239
- """Remove all wires from collection."""
240
- self._wires.clear()
241
- self._uuid_index.clear()
242
- self._modified = True
243
- logger.debug("Cleared all wires")
244
-
245
197
  @property
246
198
  def modified(self) -> bool:
247
199
  """Check if collection has been modified."""
248
- return self._modified
200
+ return self.is_modified()
249
201
 
250
- def mark_saved(self):
202
+ def mark_saved(self) -> None:
251
203
  """Mark collection as saved (reset modified flag)."""
252
- self._modified = False
204
+ self.reset_modified_flag()
@@ -17,4 +17,6 @@ DEFAULT_PIN_NUMBER_SIZE = 1.27 # 50 mils
17
17
  # Text width ratio for proportional font rendering
18
18
  # KiCad uses proportional fonts where average character width is ~0.65x height
19
19
  # This prevents label text from extending beyond calculated bounding boxes
20
- DEFAULT_PIN_TEXT_WIDTH_RATIO = 0.65 # Width to height ratio for pin text (proportional font average)
20
+ DEFAULT_PIN_TEXT_WIDTH_RATIO = (
21
+ 0.65 # Width to height ratio for pin text (proportional font average)
22
+ )