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
@@ -0,0 +1,248 @@
1
+ """
2
+ Base collection class for schematic elements.
3
+
4
+ Provides common functionality for all collection types including UUID indexing,
5
+ modification tracking, and standard collection operations.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Callable, Dict, Generic, Iterator, List, Optional, Protocol, TypeVar
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class HasUUID(Protocol):
15
+ """Protocol for objects that have a UUID attribute."""
16
+
17
+ @property
18
+ def uuid(self) -> str:
19
+ """UUID of the object."""
20
+ ...
21
+
22
+
23
+ T = TypeVar("T", bound=HasUUID)
24
+
25
+
26
+ class BaseCollection(Generic[T]):
27
+ """
28
+ Generic base class for schematic element collections.
29
+
30
+ Provides common functionality:
31
+ - UUID-based indexing for fast lookup
32
+ - Modification tracking
33
+ - Standard collection operations (__len__, __iter__, __getitem__)
34
+ - Index rebuilding and management
35
+
36
+ Type parameter T must implement the HasUUID protocol.
37
+ """
38
+
39
+ def __init__(self, items: Optional[List[T]] = None, collection_name: str = "items") -> None:
40
+ """
41
+ Initialize base collection.
42
+
43
+ Args:
44
+ items: Initial list of items
45
+ collection_name: Name for logging (e.g., "wires", "junctions")
46
+ """
47
+ self._items: List[T] = items or []
48
+ self._uuid_index: Dict[str, int] = {}
49
+ self._modified = False
50
+ self._collection_name = collection_name
51
+
52
+ # Build UUID index
53
+ self._rebuild_index()
54
+
55
+ logger.debug(f"{collection_name} collection initialized with {len(self._items)} items")
56
+
57
+ def _rebuild_index(self) -> None:
58
+ """Rebuild UUID index for fast lookups."""
59
+ self._uuid_index = {item.uuid: i for i, item in enumerate(self._items)}
60
+
61
+ def _mark_modified(self) -> None:
62
+ """Mark collection as modified."""
63
+ self._modified = True
64
+
65
+ def is_modified(self) -> bool:
66
+ """Check if collection has been modified."""
67
+ return self._modified
68
+
69
+ def reset_modified_flag(self) -> None:
70
+ """Reset modified flag (typically after save)."""
71
+ self._modified = False
72
+
73
+ # Standard collection protocol methods
74
+ def __len__(self) -> int:
75
+ """Return number of items in collection."""
76
+ return len(self._items)
77
+
78
+ def __iter__(self) -> Iterator[T]:
79
+ """Iterate over items in collection."""
80
+ return iter(self._items)
81
+
82
+ def __getitem__(self, key: Any) -> T:
83
+ """
84
+ Get item by UUID or index.
85
+
86
+ Args:
87
+ key: UUID string or integer index
88
+
89
+ Returns:
90
+ Item at the specified location
91
+
92
+ Raises:
93
+ KeyError: If UUID not found
94
+ IndexError: If index out of range
95
+ TypeError: If key is neither string nor int
96
+ """
97
+ if isinstance(key, str):
98
+ # UUID lookup
99
+ if key not in self._uuid_index:
100
+ raise KeyError(f"Item with UUID '{key}' not found")
101
+ return self._items[self._uuid_index[key]]
102
+ elif isinstance(key, int):
103
+ # Index lookup
104
+ return self._items[key]
105
+ else:
106
+ raise TypeError(f"Key must be string (UUID) or int (index), got {type(key)}")
107
+
108
+ def __contains__(self, key: Any) -> bool:
109
+ """
110
+ Check if item exists in collection.
111
+
112
+ Args:
113
+ key: UUID string or item object
114
+
115
+ Returns:
116
+ True if item exists
117
+ """
118
+ if isinstance(key, str):
119
+ return key in self._uuid_index
120
+ elif hasattr(key, "uuid"):
121
+ return key.uuid in self._uuid_index
122
+ return False
123
+
124
+ def get(self, uuid: str) -> Optional[T]:
125
+ """
126
+ Get item by UUID.
127
+
128
+ Args:
129
+ uuid: Item UUID
130
+
131
+ Returns:
132
+ Item if found, None otherwise
133
+ """
134
+ if uuid not in self._uuid_index:
135
+ return None
136
+ return self._items[self._uuid_index[uuid]]
137
+
138
+ def remove(self, uuid: str) -> bool:
139
+ """
140
+ Remove item by UUID.
141
+
142
+ Args:
143
+ uuid: UUID of item to remove
144
+
145
+ Returns:
146
+ True if item was removed, False if not found
147
+ """
148
+ if uuid not in self._uuid_index:
149
+ return False
150
+
151
+ index = self._uuid_index[uuid]
152
+ del self._items[index]
153
+ self._rebuild_index()
154
+ self._mark_modified()
155
+
156
+ logger.debug(f"Removed item with UUID {uuid} from {self._collection_name}")
157
+ return True
158
+
159
+ def clear(self) -> None:
160
+ """Remove all items from collection."""
161
+ self._items.clear()
162
+ self._uuid_index.clear()
163
+ self._mark_modified()
164
+ logger.debug(f"Cleared all items from {self._collection_name}")
165
+
166
+ def find(self, predicate: Callable[[T], bool]) -> List[T]:
167
+ """
168
+ Find all items matching a predicate.
169
+
170
+ Args:
171
+ predicate: Function that returns True for matching items
172
+
173
+ Returns:
174
+ List of matching items
175
+ """
176
+ return [item for item in self._items if predicate(item)]
177
+
178
+ def filter(self, **criteria) -> List[T]:
179
+ """
180
+ Filter items by attribute values.
181
+
182
+ Args:
183
+ **criteria: Attribute name/value pairs to match
184
+
185
+ Returns:
186
+ List of matching items
187
+
188
+ Example:
189
+ collection.filter(wire_type=WireType.BUS, stroke_width=0.5)
190
+ """
191
+ matches = []
192
+ for item in self._items:
193
+ if all(getattr(item, key, None) == value for key, value in criteria.items()):
194
+ matches.append(item)
195
+ return matches
196
+
197
+ def get_statistics(self) -> Dict[str, Any]:
198
+ """
199
+ Get collection statistics.
200
+
201
+ Returns:
202
+ Dictionary with statistics
203
+ """
204
+ return {
205
+ "total_items": len(self._items),
206
+ "modified": self._modified,
207
+ "indexed_items": len(self._uuid_index),
208
+ }
209
+
210
+ def _add_item(self, item: T) -> None:
211
+ """
212
+ Add item to internal storage and index.
213
+
214
+ Args:
215
+ item: Item to add
216
+
217
+ Raises:
218
+ ValueError: If item UUID already exists
219
+ """
220
+ if item.uuid in self._uuid_index:
221
+ raise ValueError(f"Item with UUID '{item.uuid}' already exists")
222
+
223
+ self._items.append(item)
224
+ self._uuid_index[item.uuid] = len(self._items) - 1
225
+ self._mark_modified()
226
+
227
+ def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
228
+ """
229
+ Update multiple items matching criteria.
230
+
231
+ Args:
232
+ criteria: Attribute name/value pairs to match
233
+ updates: Attribute name/value pairs to update
234
+
235
+ Returns:
236
+ Number of items updated
237
+ """
238
+ matching_items = self.filter(**criteria)
239
+ for item in matching_items:
240
+ for key, value in updates.items():
241
+ if hasattr(item, key):
242
+ setattr(item, key, value)
243
+
244
+ if matching_items:
245
+ self._mark_modified()
246
+
247
+ logger.debug(f"Bulk updated {len(matching_items)} items in {self._collection_name}")
248
+ return len(matching_items)
@@ -384,6 +384,11 @@ def get_component_bounding_box(
384
384
 
385
385
  # Transform to world coordinates
386
386
  # TODO: Handle component rotation in the future
387
+ # NOTE: Currently assumes 0° rotation. For rotated components, bounding box
388
+ # would need to be recalculated after applying rotation matrix. This is a
389
+ # known limitation but doesn't affect most use cases since components are
390
+ # typically placed without rotation, and routing avoids components regardless.
391
+ # Priority: LOW - Would improve accuracy for rotated component placement validation
387
392
  world_bbox = BoundingBox(
388
393
  component.position.x + symbol_bbox.min_x,
389
394
  component.position.y + symbol_bbox.min_y,
@@ -11,6 +11,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..library.cache import SymbolDefinition, get_symbol_cache
13
13
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
14
+ from .collections import BaseCollection
14
15
  from .ic_manager import ICManager
15
16
  from .types import Point, SchematicPin, SchematicSymbol
16
17
 
@@ -261,10 +262,13 @@ class Component:
261
262
  )
262
263
 
263
264
 
264
- class ComponentCollection:
265
+ class ComponentCollection(BaseCollection[Component]):
265
266
  """
266
267
  Collection class for efficient component management.
267
268
 
269
+ Inherits from BaseCollection for standard operations and adds component-specific
270
+ functionality including reference, lib_id, and value-based indexing.
271
+
268
272
  Provides fast lookup, filtering, and bulk operations for schematic components.
269
273
  Optimized for schematics with hundreds or thousands of components.
270
274
  """
@@ -276,19 +280,19 @@ class ComponentCollection:
276
280
  Args:
277
281
  components: Initial list of component data
278
282
  """
279
- self._components: List[Component] = []
283
+ # Initialize base collection
284
+ super().__init__([], collection_name="components")
285
+
286
+ # Additional component-specific indexes
280
287
  self._reference_index: Dict[str, Component] = {}
281
288
  self._lib_id_index: Dict[str, List[Component]] = {}
282
289
  self._value_index: Dict[str, List[Component]] = {}
283
- self._modified = False
284
290
 
285
291
  # Add initial components
286
292
  if components:
287
293
  for comp_data in components:
288
294
  self._add_to_indexes(Component(comp_data, self))
289
295
 
290
- logger.debug(f"ComponentCollection initialized with {len(self._components)} components")
291
-
292
296
  def add(
293
297
  self,
294
298
  lib_id: str,
@@ -375,7 +379,7 @@ class ComponentCollection:
375
379
 
376
380
  # Add to collection
377
381
  self._add_to_indexes(component)
378
- self._modified = True
382
+ self._mark_modified()
379
383
 
380
384
  logger.info(f"Added component: {reference} ({lib_id})")
381
385
  return component
@@ -432,7 +436,7 @@ class ComponentCollection:
432
436
  component = Component(component_data, self)
433
437
  self._add_to_indexes(component)
434
438
 
435
- self._modified = True
439
+ self._mark_modified()
436
440
  logger.info(
437
441
  f"Added multi-unit IC: {reference_prefix} ({lib_id}) with {len(unit_components)} units"
438
442
  )
@@ -444,22 +448,102 @@ class ComponentCollection:
444
448
  Remove component by reference.
445
449
 
446
450
  Args:
447
- reference: Component reference to remove
451
+ reference: Component reference to remove (e.g., "R1")
448
452
 
449
453
  Returns:
450
- True if component was removed
454
+ True if component was removed, False if not found
455
+
456
+ Raises:
457
+ TypeError: If reference is not a string
458
+
459
+ Examples:
460
+ sch.components.remove("R1")
461
+ sch.components.remove("C2")
462
+
463
+ Note:
464
+ For removing by UUID or component object, use remove_by_uuid() or remove_component()
465
+ respectively. This maintains a clear, simple API contract.
451
466
  """
467
+ if not isinstance(reference, str):
468
+ raise TypeError(f"reference must be a string, not {type(reference).__name__}")
469
+
452
470
  component = self._reference_index.get(reference)
453
471
  if not component:
454
472
  return False
455
473
 
456
- # Remove from all indexes
474
+ # Remove from component-specific indexes
457
475
  self._remove_from_indexes(component)
458
- self._modified = True
476
+
477
+ # Remove from base collection using UUID
478
+ super().remove(component.uuid)
459
479
 
460
480
  logger.info(f"Removed component: {reference}")
461
481
  return True
462
482
 
483
+ def remove_by_uuid(self, component_uuid: str) -> bool:
484
+ """
485
+ Remove component by UUID.
486
+
487
+ Args:
488
+ component_uuid: Component UUID to remove
489
+
490
+ Returns:
491
+ True if component was removed, False if not found
492
+
493
+ Raises:
494
+ TypeError: If UUID is not a string
495
+ """
496
+ if not isinstance(component_uuid, str):
497
+ raise TypeError(f"component_uuid must be a string, not {type(component_uuid).__name__}")
498
+
499
+ if component_uuid not in self._uuid_index:
500
+ return False
501
+
502
+ component = self._items[self._uuid_index[component_uuid]]
503
+
504
+ # Remove from component-specific indexes
505
+ self._remove_from_indexes(component)
506
+
507
+ # Remove from base collection
508
+ super().remove(component_uuid)
509
+
510
+ logger.info(f"Removed component by UUID: {component_uuid}")
511
+ return True
512
+
513
+ def remove_component(self, component: "Component") -> bool:
514
+ """
515
+ Remove component by component object.
516
+
517
+ Args:
518
+ component: Component object to remove
519
+
520
+ Returns:
521
+ True if component was removed, False if not found
522
+
523
+ Raises:
524
+ TypeError: If component is not a Component instance
525
+
526
+ Examples:
527
+ comp = sch.components.get("R1")
528
+ sch.components.remove_component(comp)
529
+ """
530
+ if not isinstance(component, Component):
531
+ raise TypeError(
532
+ f"component must be a Component instance, not {type(component).__name__}"
533
+ )
534
+
535
+ if component.uuid not in self._uuid_index:
536
+ return False
537
+
538
+ # Remove from component-specific indexes
539
+ self._remove_from_indexes(component)
540
+
541
+ # Remove from base collection
542
+ super().remove(component.uuid)
543
+
544
+ logger.info(f"Removed component: {component.reference}")
545
+ return True
546
+
463
547
  def get(self, reference: str) -> Optional[Component]:
464
548
  """Get component by reference."""
465
549
  return self._reference_index.get(reference)
@@ -479,7 +563,7 @@ class ComponentCollection:
479
563
  Returns:
480
564
  List of matching components
481
565
  """
482
- results = list(self._components)
566
+ results = list(self._items)
483
567
 
484
568
  # Apply filters
485
569
  if "lib_id" in criteria:
@@ -517,7 +601,7 @@ class ComponentCollection:
517
601
  def filter_by_type(self, component_type: str) -> List[Component]:
518
602
  """Filter components by type (e.g., 'R' for resistors)."""
519
603
  return [
520
- c for c in self._components if c.symbol_name.upper().startswith(component_type.upper())
604
+ c for c in self._items if c.symbol_name.upper().startswith(component_type.upper())
521
605
  ]
522
606
 
523
607
  def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
@@ -532,7 +616,7 @@ class ComponentCollection:
532
616
  point = Point(point[0], point[1])
533
617
 
534
618
  results = []
535
- for component in self._components:
619
+ for component in self._items:
536
620
  if component.position.distance_to(point) <= radius:
537
621
  results.append(component)
538
622
  return results
@@ -564,21 +648,21 @@ class ComponentCollection:
564
648
  component.set_property(key, str(value))
565
649
 
566
650
  if matching:
567
- self._modified = True
651
+ self._mark_modified()
568
652
 
569
653
  logger.info(f"Bulk updated {len(matching)} components")
570
654
  return len(matching)
571
655
 
572
656
  def sort_by_reference(self):
573
657
  """Sort components by reference designator."""
574
- self._components.sort(key=lambda c: c.reference)
658
+ self._items.sort(key=lambda c: c.reference)
575
659
 
576
660
  def sort_by_position(self, by_x: bool = True):
577
661
  """Sort components by position."""
578
662
  if by_x:
579
- self._components.sort(key=lambda c: (c.position.x, c.position.y))
663
+ self._items.sort(key=lambda c: (c.position.x, c.position.y))
580
664
  else:
581
- self._components.sort(key=lambda c: (c.position.y, c.position.x))
665
+ self._items.sort(key=lambda c: (c.position.y, c.position.x))
582
666
 
583
667
  def validate_all(self) -> List[ValidationIssue]:
584
668
  """Validate all components in collection."""
@@ -586,12 +670,12 @@ class ComponentCollection:
586
670
  validator = SchematicValidator()
587
671
 
588
672
  # Validate individual components
589
- for component in self._components:
673
+ for component in self._items:
590
674
  issues = component.validate()
591
675
  all_issues.extend(issues)
592
676
 
593
677
  # Validate collection-level rules
594
- references = [c.reference for c in self._components]
678
+ references = [c.reference for c in self._items]
595
679
  if len(references) != len(set(references)):
596
680
  # Find duplicates
597
681
  seen = set()
@@ -615,7 +699,7 @@ class ComponentCollection:
615
699
  lib_counts = {}
616
700
  value_counts = {}
617
701
 
618
- for component in self._components:
702
+ for component in self._items:
619
703
  # Count by library
620
704
  lib = component.library
621
705
  lib_counts[lib] = lib_counts.get(lib, 0) + 1
@@ -626,36 +710,47 @@ class ComponentCollection:
626
710
  value_counts[value] = value_counts.get(value, 0) + 1
627
711
 
628
712
  return {
629
- "total_components": len(self._components),
713
+ "total_components": len(self._items),
630
714
  "unique_references": len(self._reference_index),
631
715
  "libraries_used": len(lib_counts),
632
716
  "library_breakdown": lib_counts,
633
717
  "most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
634
718
  :10
635
719
  ],
636
- "modified": self._modified,
720
+ "modified": self.is_modified(),
637
721
  }
638
722
 
639
723
  # Collection interface
640
- def __len__(self) -> int:
641
- """Number of components."""
642
- return len(self._components)
643
-
644
- def __iter__(self) -> Iterator[Component]:
645
- """Iterate over components."""
646
- return iter(self._components)
724
+ # __len__, __iter__ inherited from BaseCollection
647
725
 
648
726
  def __getitem__(self, key: Union[int, str]) -> Component:
649
- """Get component by index or reference."""
650
- if isinstance(key, int):
651
- return self._components[key]
652
- elif isinstance(key, str):
727
+ """
728
+ Get component by index, UUID, or reference.
729
+
730
+ Args:
731
+ key: Integer index, UUID string, or reference string
732
+
733
+ Returns:
734
+ Component at the specified location
735
+
736
+ Raises:
737
+ KeyError: If UUID or reference not found
738
+ IndexError: If index out of range
739
+ TypeError: If key is invalid type
740
+ """
741
+ if isinstance(key, str):
742
+ # Try reference first (most common use case)
653
743
  component = self._reference_index.get(key)
654
- if component is None:
744
+ if component is not None:
745
+ return component
746
+ # Fall back to UUID lookup (from base class)
747
+ try:
748
+ return super().__getitem__(key)
749
+ except KeyError:
655
750
  raise KeyError(f"Component not found: {key}")
656
- return component
657
751
  else:
658
- raise TypeError(f"Invalid key type: {type(key)}")
752
+ # Integer index (from base class)
753
+ return super().__getitem__(key)
659
754
 
660
755
  def __contains__(self, reference: str) -> bool:
661
756
  """Check if reference exists."""
@@ -663,8 +758,11 @@ class ComponentCollection:
663
758
 
664
759
  # Internal methods
665
760
  def _add_to_indexes(self, component: Component):
666
- """Add component to all indexes."""
667
- self._components.append(component)
761
+ """Add component to all indexes (base + component-specific)."""
762
+ # Add to base collection (UUID index)
763
+ self._add_item(component)
764
+
765
+ # Add to reference index
668
766
  self._reference_index[component.reference] = component
669
767
 
670
768
  # Add to lib_id index
@@ -681,8 +779,8 @@ class ComponentCollection:
681
779
  self._value_index[value].append(component)
682
780
 
683
781
  def _remove_from_indexes(self, component: Component):
684
- """Remove component from all indexes."""
685
- self._components.remove(component)
782
+ """Remove component from component-specific indexes (not base UUID index)."""
783
+ # Remove from reference index
686
784
  del self._reference_index[component.reference]
687
785
 
688
786
  # Remove from lib_id index
@@ -705,10 +803,7 @@ class ComponentCollection:
705
803
  component = self._reference_index[old_ref]
706
804
  del self._reference_index[old_ref]
707
805
  self._reference_index[new_ref] = component
708
-
709
- def _mark_modified(self):
710
- """Mark collection as modified."""
711
- self._modified = True
806
+ # Note: UUID doesn't change when reference changes, so base index is unaffected
712
807
 
713
808
  def _generate_reference(self, lib_id: str) -> str:
714
809
  """Generate unique reference for component."""
@@ -730,7 +825,7 @@ class ComponentCollection:
730
825
  grid_size = 10.0 # 10mm grid
731
826
  max_per_row = 10
732
827
 
733
- row = len(self._components) // max_per_row
734
- col = len(self._components) % max_per_row
828
+ row = len(self._items) // max_per_row
829
+ col = len(self._items) % max_per_row
735
830
 
736
831
  return Point(col * grid_size, row * grid_size)