"""
Enhanced component management for KiCAD schematics.
This module provides a modern, intuitive API for working with schematic components,
featuring fast lookup, bulk operations, and advanced filtering capabilities.
"""
import logging
import uuid
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
from ..library.cache import SymbolDefinition, get_symbol_cache
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
from .collections import BaseCollection
from .exceptions import LibraryError
from .ic_manager import ICManager
from .types import Point, SchematicPin, SchematicSymbol
logger = logging.getLogger(__name__)
[docs]
class Component:
"""
Enhanced wrapper for schematic components with modern API.
Provides intuitive access to component properties, pins, and operations
while maintaining exact format preservation for professional use.
"""
[docs]
def __init__(self, symbol_data: SchematicSymbol, parent_collection: "ComponentCollection"):
"""
Initialize component wrapper.
Args:
symbol_data: Underlying symbol data
parent_collection: Parent collection for updates
"""
self._data = symbol_data
self._collection = parent_collection
self._validator = SchematicValidator()
# Core properties with validation
@property
def uuid(self) -> str:
"""Component UUID."""
return self._data.uuid
@property
def reference(self) -> str:
"""Component reference (e.g., 'R1')."""
return self._data.reference
@reference.setter
def reference(self, value: str):
"""Set component reference with validation."""
if not self._validator.validate_reference(value):
raise ValidationError(f"Invalid reference format: {value}")
# Check for duplicates in parent collection
if self._collection.get(value) is not None:
raise ValidationError(f"Reference {value} already exists")
old_ref = self._data.reference
self._data.reference = value
self._collection._update_reference_index(old_ref, value)
logger.debug(f"Updated reference: {old_ref} -> {value}")
@property
def value(self) -> str:
"""Component value (e.g., '10k')."""
return self._data.value
@value.setter
def value(self, value: str):
"""Set component value."""
self._data.value = value
self._collection._mark_modified()
@property
def footprint(self) -> Optional[str]:
"""Component footprint."""
return self._data.footprint
@footprint.setter
def footprint(self, value: Optional[str]):
"""Set component footprint."""
self._data.footprint = value
self._collection._mark_modified()
@property
def position(self) -> Point:
"""Component position."""
return self._data.position
@position.setter
def position(self, value: Union[Point, Tuple[float, float]]):
"""Set component position."""
if isinstance(value, tuple):
value = Point(value[0], value[1])
self._data.position = value
self._collection._mark_modified()
@property
def rotation(self) -> float:
"""Component rotation in degrees."""
return self._data.rotation
@rotation.setter
def rotation(self, value: float):
"""Set component rotation (must be 0, 90, 180, or 270 degrees).
KiCad only supports these four rotation angles for components.
Args:
value: Rotation angle in degrees (0, 90, 180, or 270)
Raises:
ValueError: If rotation is not 0, 90, 180, or 270
"""
# Normalize rotation to 0-360 range
normalized = float(value) % 360
# KiCad only accepts 0, 90, 180, or 270 degrees
VALID_ROTATIONS = {0, 90, 180, 270}
if normalized not in VALID_ROTATIONS:
raise ValueError(
f"Component rotation must be 0, 90, 180, or 270 degrees. "
f"Got {value}Β° (normalized to {normalized}Β°). "
f"KiCad does not support arbitrary rotation angles."
)
self._data.rotation = normalized
self._collection._mark_modified()
@property
def lib_id(self) -> str:
"""Library ID (e.g., 'Device:R')."""
return self._data.lib_id
@property
def library(self) -> str:
"""Library name."""
return self._data.library
@property
def symbol_name(self) -> str:
"""Symbol name within library."""
return self._data.symbol_name
# Properties dictionary
@property
def properties(self) -> Dict[str, str]:
"""Dictionary of all component properties."""
return self._data.properties
[docs]
def get_property(self, name: str, default: Optional[str] = None) -> Optional[str]:
"""Get property value by name."""
return self._data.properties.get(name, default)
[docs]
def set_property(self, name: str, value: str):
"""Set property value with validation."""
if not isinstance(name, str) or not isinstance(value, str):
raise ValidationError("Property name and value must be strings")
self._data.properties[name] = value
self._collection._mark_modified()
logger.debug(f"Set property {self.reference}.{name} = {value}")
[docs]
def remove_property(self, name: str) -> bool:
"""Remove property by name."""
if name in self._data.properties:
del self._data.properties[name]
self._collection._mark_modified()
return True
return False
# Pin access
@property
def pins(self) -> List[SchematicPin]:
"""List of component pins."""
return self._data.pins
@property
def pin_uuids(self) -> Dict[str, str]:
"""Dictionary mapping pin numbers to their UUIDs."""
return self._data.pin_uuids
[docs]
def get_pin(self, pin_number: str) -> Optional[SchematicPin]:
"""Get pin by number."""
return self._data.get_pin(pin_number)
[docs]
def get_pin_position(self, pin_number: str) -> Optional[Point]:
"""Get absolute position of pin."""
return self._data.get_pin_position(pin_number)
[docs]
def list_pins(self) -> List[Dict[str, Any]]:
"""
List all pins for this component.
Returns:
List of pin dictionaries with keys:
- number: Pin number (str)
- name: Pin name (str)
- type: Pin electrical type (str)
- position: Absolute pin position (Point)
Example:
>>> comp = sch.components.get("U1")
>>> pins = comp.list_pins()
>>> for pin in pins:
... print(f"Pin {pin['number']}: {pin['name']} ({pin['type']})")
Pin 1: GND (POWER_IN)
Pin 2: 3V3 (POWER_IN)
"""
return [
{
"number": pin.number,
"name": pin.name,
"type": pin.pin_type.value,
"position": pin.position,
}
for pin in self.pins
]
[docs]
def show_pins(self) -> None:
"""
Display pin information in readable table format.
Prints a formatted table showing pin numbers, names, and types
for all pins on this component. Useful for interactive exploration
and debugging.
Example:
>>> comp = sch.components.get("U1")
>>> comp.show_pins()
Pins for U1 (RF_Module:ESP32-WROOM-32):
Pin# Name Type
----------------------------------------
1 GND POWER_IN
2 3V3 POWER_IN
3 EN INPUT
...
"""
print(f"\nPins for {self.reference} ({self.lib_id}):")
print(f"{'Pin#':<6} {'Name':<20} {'Type':<12}")
print("-" * 40)
for pin in self.pins:
print(f"{pin.number:<6} {pin.name:<20} {pin.pin_type.value:<12}")
# Component state
@property
def in_bom(self) -> bool:
"""Whether component appears in bill of materials."""
return self._data.in_bom
@in_bom.setter
def in_bom(self, value: bool):
"""Set BOM inclusion."""
self._data.in_bom = bool(value)
self._collection._mark_modified()
@property
def on_board(self) -> bool:
"""Whether component appears on PCB."""
return self._data.on_board
@on_board.setter
def on_board(self, value: bool):
"""Set board inclusion."""
self._data.on_board = bool(value)
self._collection._mark_modified()
# Utility methods
[docs]
def move(self, x: float, y: float):
"""Move component to new position."""
self.position = Point(x, y)
[docs]
def translate(self, dx: float, dy: float):
"""Translate component by offset."""
current = self.position
self.position = Point(current.x + dx, current.y + dy)
[docs]
def rotate(self, angle: float):
"""Rotate component by angle (degrees)."""
self.rotation = (self.rotation + angle) % 360
[docs]
def copy_properties_from(self, other: "Component"):
"""Copy all properties from another component."""
for name, value in other.properties.items():
self.set_property(name, value)
[docs]
def get_symbol_definition(self) -> Optional[SymbolDefinition]:
"""Get the symbol definition from library cache."""
cache = get_symbol_cache()
return cache.get_symbol(self.lib_id)
[docs]
def update_from_library(self) -> bool:
"""Update component pins and metadata from library definition."""
symbol_def = self.get_symbol_definition()
if not symbol_def:
return False
# Update pins
self._data.pins = symbol_def.pins.copy()
# Update reference prefix if needed
if not self.reference.startswith(symbol_def.reference_prefix):
logger.warning(
f"Reference {self.reference} doesn't match expected prefix {symbol_def.reference_prefix}"
)
self._collection._mark_modified()
return True
[docs]
def validate(self) -> List[ValidationIssue]:
"""Validate this component."""
return self._validator.validate_component(self._data.__dict__)
[docs]
def to_dict(self) -> Dict[str, Any]:
"""Convert component to dictionary representation."""
return {
"reference": self.reference,
"lib_id": self.lib_id,
"value": self.value,
"footprint": self.footprint,
"position": {"x": self.position.x, "y": self.position.y},
"rotation": self.rotation,
"properties": self.properties.copy(),
"in_bom": self.in_bom,
"on_board": self.on_board,
"pin_count": len(self.pins),
}
[docs]
def __str__(self) -> str:
"""String representation."""
return f"<Component {self.reference}: {self.lib_id} = '{self.value}' @ {self.position}>"
[docs]
def __repr__(self) -> str:
"""Detailed representation."""
return (
f"Component(ref='{self.reference}', lib_id='{self.lib_id}', "
f"value='{self.value}', pos={self.position}, rotation={self.rotation})"
)
[docs]
class ComponentCollection(BaseCollection[Component]):
"""
Collection class for efficient component management.
Inherits from BaseCollection for standard operations and adds component-specific
functionality including reference, lib_id, and value-based indexing.
Provides fast lookup, filtering, and bulk operations for schematic components.
Optimized for schematics with hundreds or thousands of components.
"""
[docs]
def __init__(self, components: List[SchematicSymbol] = None, parent_schematic=None):
"""
Initialize component collection.
Args:
components: Initial list of component data
parent_schematic: Reference to parent Schematic object (for hierarchy context)
"""
# Initialize base collection
super().__init__([], collection_name="components")
# Additional component-specific indexes
self._reference_index: Dict[str, Component] = {}
self._lib_id_index: Dict[str, List[Component]] = {}
self._value_index: Dict[str, List[Component]] = {}
# Store reference to parent schematic for hierarchy context
self._parent_schematic = parent_schematic
# Add initial components
if components:
for comp_data in components:
self._add_to_indexes(Component(comp_data, self))
[docs]
def add(
self,
lib_id: str,
reference: Optional[str] = None,
value: str = "",
position: Optional[Union[Point, Tuple[float, float]]] = None,
footprint: Optional[str] = None,
unit: int = 1,
rotation: float = 0.0,
component_uuid: Optional[str] = None,
grid_units: bool = False,
grid_size: float = 1.27,
**properties,
) -> Component:
"""
Add a new component to the schematic.
Args:
lib_id: Library identifier (e.g., "Device:R")
reference: Component reference (auto-generated if None)
value: Component value
position: Component position in mm (or grid units if grid_units=True)
footprint: Component footprint
unit: Unit number for multi-unit components (1-based)
rotation: Component rotation in degrees (0, 90, 180, 270)
component_uuid: Specific UUID for component (auto-generated if None)
grid_units: If True, interpret position as grid units instead of mm
grid_size: Grid size in mm (default 1.27mm = 50 mil KiCAD standard)
**properties: Additional component properties
Returns:
Newly created Component
Raises:
ValidationError: If component data is invalid
LibraryError: If the KiCAD symbol library is not found
Examples:
# Position in millimeters (default)
sch.components.add('Device:R', 'R1', '10k', position=(25.4, 50.8))
# Position in grid units (cleaner for parametric design)
sch.components.add('Device:R', 'R1', '10k', position=(20, 40), grid_units=True)
"""
# Validate lib_id
validator = SchematicValidator()
if not validator.validate_lib_id(lib_id):
raise ValidationError(f"Invalid lib_id format: {lib_id}")
# Generate reference if not provided
if not reference:
reference = self._generate_reference(lib_id)
# Validate reference
if not validator.validate_reference(reference):
raise ValidationError(f"Invalid reference format: {reference}")
# Check for duplicate reference
if reference in self._reference_index:
raise ValidationError(f"Reference {reference} already exists")
# Set default position if not provided
if position is None:
position = self._find_available_position()
elif isinstance(position, tuple):
# Convert grid units to mm if requested
if grid_units:
logger.debug(f"Component {reference}: Converting grid position {position} to mm")
position = Point(position[0] * grid_size, position[1] * grid_size)
logger.debug(
f"Component {reference}: After conversion: ({position.x:.3f}, {position.y:.3f}) mm"
)
else:
position = Point(position[0], position[1])
elif grid_units and isinstance(position, Point):
# Convert Point from grid units to mm
logger.debug(
f"Component {reference}: Converting Point grid position ({position.x}, {position.y}) to mm"
)
position = Point(position.x * grid_size, position.y * grid_size)
logger.debug(
f"Component {reference}: After conversion: ({position.x:.3f}, {position.y:.3f}) mm"
)
# Always snap component position to KiCAD grid (1.27mm = 50mil)
from .geometry import snap_to_grid
logger.debug(f"Component {reference}: Before snap: ({position.x:.3f}, {position.y:.3f}) mm")
snapped_pos = snap_to_grid((position.x, position.y), grid_size=1.27)
position = Point(snapped_pos[0], snapped_pos[1])
logger.debug(
f"Component {reference}: Final position after snap: ({position.x:.3f}, {position.y:.3f}) mm"
)
# Normalize and validate rotation
rotation = rotation % 360
# KiCad only accepts 0, 90, 180, or 270 degrees
VALID_ROTATIONS = {0, 90, 180, 270}
if rotation not in VALID_ROTATIONS:
raise ValidationError(
f"Component rotation must be 0, 90, 180, or 270 degrees. "
f"Got {rotation}Β°. KiCad does not support arbitrary rotation angles."
)
# Check if parent schematic has hierarchy context set
# If so, add hierarchy_path to properties for proper KiCad instance paths
if self._parent_schematic and hasattr(self._parent_schematic, "_hierarchy_path"):
if self._parent_schematic._hierarchy_path:
properties = dict(properties) # Make a copy to avoid modifying caller's dict
properties["hierarchy_path"] = self._parent_schematic._hierarchy_path
logger.debug(
f"Setting hierarchy_path for component {reference}: "
f"{self._parent_schematic._hierarchy_path}"
)
# Create component data
component_data = SchematicSymbol(
uuid=component_uuid if component_uuid else str(uuid.uuid4()),
lib_id=lib_id,
position=position,
reference=reference,
value=value,
footprint=footprint,
unit=unit,
rotation=rotation,
properties=properties,
)
# Get symbol definition and update pins
symbol_cache = get_symbol_cache()
symbol_def = symbol_cache.get_symbol(lib_id)
if not symbol_def:
# Provide helpful error message with library name
library_name = lib_id.split(":")[0] if ":" in lib_id else "unknown"
raise LibraryError(
f"Symbol '{lib_id}' not found in KiCAD libraries. "
f"Please verify the library name '{library_name}' and symbol name are correct. "
f"Common libraries include: Device, Connector_Generic, Regulator_Linear, RF_Module",
field="lib_id",
value=lib_id,
)
component_data.pins = symbol_def.pins.copy()
# Create component wrapper
component = Component(component_data, self)
# Add to collection
self._add_to_indexes(component)
self._mark_modified()
logger.info(f"Added component: {reference} ({lib_id})")
return component
[docs]
def add_ic(
self,
lib_id: str,
reference_prefix: str,
position: Optional[Union[Point, Tuple[float, float]]] = None,
value: str = "",
footprint: Optional[str] = None,
layout_style: str = "vertical",
**properties,
) -> ICManager:
"""
Add a multi-unit IC with automatic unit placement.
Args:
lib_id: Library identifier for the IC (e.g., "74xx:7400")
reference_prefix: Base reference (e.g., "U1" β U1A, U1B, etc.)
position: Base position for auto-layout (auto-placed if None)
value: IC value (defaults to symbol name)
footprint: IC footprint
layout_style: Layout algorithm ("vertical", "grid", "functional")
**properties: Common properties for all units
Returns:
ICManager object for position overrides and management
Example:
ic = sch.components.add_ic("74xx:7400", "U1", position=(100, 100))
ic.place_unit(1, position=(150, 80)) # Override Gate A position
"""
# Set default position if not provided
if position is None:
position = self._find_available_position()
elif isinstance(position, tuple):
position = Point(position[0], position[1])
# Set default value to symbol name if not provided
if not value:
value = lib_id.split(":")[-1] # "74xx:7400" β "7400"
# Create IC manager for this multi-unit component
ic_manager = ICManager(lib_id, reference_prefix, position, self)
# Generate all unit components
unit_components = ic_manager.generate_components(
value=value, footprint=footprint, properties=properties
)
# Add all units to the collection
for component_data in unit_components:
component = Component(component_data, self)
self._add_to_indexes(component)
self._mark_modified()
logger.info(
f"Added multi-unit IC: {reference_prefix} ({lib_id}) with {len(unit_components)} units"
)
return ic_manager
[docs]
def remove(self, reference: str) -> bool:
"""
Remove component by reference.
Args:
reference: Component reference to remove (e.g., "R1")
Returns:
True if component was removed, False if not found
Raises:
TypeError: If reference is not a string
Examples:
sch.components.remove("R1")
sch.components.remove("C2")
Note:
For removing by UUID or component object, use remove_by_uuid() or remove_component()
respectively. This maintains a clear, simple API contract.
"""
if not isinstance(reference, str):
raise TypeError(f"reference must be a string, not {type(reference).__name__}")
component = self._reference_index.get(reference)
if not component:
return False
# Remove from component-specific indexes
self._remove_from_indexes(component)
# Remove from base collection using UUID
super().remove(component.uuid)
logger.info(f"Removed component: {reference}")
return True
[docs]
def remove_by_uuid(self, component_uuid: str) -> bool:
"""
Remove component by UUID.
Args:
component_uuid: Component UUID to remove
Returns:
True if component was removed, False if not found
Raises:
TypeError: If UUID is not a string
"""
if not isinstance(component_uuid, str):
raise TypeError(f"component_uuid must be a string, not {type(component_uuid).__name__}")
if component_uuid not in self._uuid_index:
return False
component = self._items[self._uuid_index[component_uuid]]
# Remove from component-specific indexes
self._remove_from_indexes(component)
# Remove from base collection
super().remove(component_uuid)
logger.info(f"Removed component by UUID: {component_uuid}")
return True
[docs]
def remove_component(self, component: "Component") -> bool:
"""
Remove component by component object.
Args:
component: Component object to remove
Returns:
True if component was removed, False if not found
Raises:
TypeError: If component is not a Component instance
Examples:
comp = sch.components.get("R1")
sch.components.remove_component(comp)
"""
if not isinstance(component, Component):
raise TypeError(
f"component must be a Component instance, not {type(component).__name__}"
)
if component.uuid not in self._uuid_index:
return False
# Remove from component-specific indexes
self._remove_from_indexes(component)
# Remove from base collection
super().remove(component.uuid)
logger.info(f"Removed component: {component.reference}")
return True
[docs]
def get(self, reference: str) -> Optional[Component]:
"""Get component by reference."""
return self._reference_index.get(reference)
[docs]
def filter(self, **criteria) -> List[Component]:
"""
Filter components by various criteria.
Args:
lib_id: Filter by library ID
value: Filter by value (exact match)
value_pattern: Filter by value pattern (contains)
reference_pattern: Filter by reference pattern
footprint: Filter by footprint
in_area: Filter by area (tuple of (x1, y1, x2, y2))
Returns:
List of matching components
"""
results = list(self._items)
# Apply filters
if "lib_id" in criteria:
lib_id = criteria["lib_id"]
results = [c for c in results if c.lib_id == lib_id]
if "value" in criteria:
value = criteria["value"]
results = [c for c in results if c.value == value]
if "value_pattern" in criteria:
pattern = criteria["value_pattern"].lower()
results = [c for c in results if pattern in c.value.lower()]
if "reference_pattern" in criteria:
import re
pattern = re.compile(criteria["reference_pattern"])
results = [c for c in results if pattern.match(c.reference)]
if "footprint" in criteria:
footprint = criteria["footprint"]
results = [c for c in results if c.footprint == footprint]
if "in_area" in criteria:
x1, y1, x2, y2 = criteria["in_area"]
results = [c for c in results if x1 <= c.position.x <= x2 and y1 <= c.position.y <= y2]
if "has_property" in criteria:
prop_name = criteria["has_property"]
results = [c for c in results if prop_name in c.properties]
return results
[docs]
def filter_by_type(self, component_type: str) -> List[Component]:
"""Filter components by type (e.g., 'R' for resistors)."""
return [c for c in self._items if c.symbol_name.upper().startswith(component_type.upper())]
[docs]
def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
"""Get components within rectangular area."""
return self.filter(in_area=(x1, y1, x2, y2))
[docs]
def near_point(
self, point: Union[Point, Tuple[float, float]], radius: float
) -> List[Component]:
"""Get components within radius of a point."""
if isinstance(point, tuple):
point = Point(point[0], point[1])
results = []
for component in self._items:
if component.position.distance_to(point) <= radius:
results.append(component)
return results
[docs]
def bulk_update(self, criteria: Dict[str, Any], updates: Dict[str, Any]) -> int:
"""
Update multiple components matching criteria.
Args:
criteria: Filter criteria (same as filter method)
updates: Dictionary of property updates
Returns:
Number of components updated
"""
matching = self.filter(**criteria)
for component in matching:
# Update basic properties and handle special cases
for key, value in updates.items():
if key == "properties" and isinstance(value, dict):
# Handle properties dictionary specially
for prop_name, prop_value in value.items():
component.set_property(prop_name, str(prop_value))
elif hasattr(component, key) and key not in ["properties"]:
setattr(component, key, value)
else:
# Add as custom property
component.set_property(key, str(value))
if matching:
self._mark_modified()
logger.info(f"Bulk updated {len(matching)} components")
return len(matching)
[docs]
def sort_by_reference(self):
"""Sort components by reference designator."""
self._items.sort(key=lambda c: c.reference)
[docs]
def sort_by_position(self, by_x: bool = True):
"""Sort components by position."""
if by_x:
self._items.sort(key=lambda c: (c.position.x, c.position.y))
else:
self._items.sort(key=lambda c: (c.position.y, c.position.x))
[docs]
def validate_all(self) -> List[ValidationIssue]:
"""Validate all components in collection."""
all_issues = []
validator = SchematicValidator()
# Validate individual components
for component in self._items:
issues = component.validate()
all_issues.extend(issues)
# Validate collection-level rules
references = [c.reference for c in self._items]
if len(references) != len(set(references)):
# Find duplicates
seen = set()
duplicates = set()
for ref in references:
if ref in seen:
duplicates.add(ref)
seen.add(ref)
for ref in duplicates:
all_issues.append(
ValidationIssue(
category="reference", message=f"Duplicate reference: {ref}", level="error"
)
)
return all_issues
[docs]
def get_statistics(self) -> Dict[str, Any]:
"""Get collection statistics."""
lib_counts = {}
value_counts = {}
for component in self._items:
# Count by library
lib = component.library
lib_counts[lib] = lib_counts.get(lib, 0) + 1
# Count by value
value = component.value
if value:
value_counts[value] = value_counts.get(value, 0) + 1
return {
"total_components": len(self._items),
"unique_references": len(self._reference_index),
"libraries_used": len(lib_counts),
"library_breakdown": lib_counts,
"most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
:10
],
"modified": self.is_modified(),
}
# Collection interface
# __len__, __iter__ inherited from BaseCollection
[docs]
def __getitem__(self, key: Union[int, str]) -> Component:
"""
Get component by index, UUID, or reference.
Args:
key: Integer index, UUID string, or reference string
Returns:
Component at the specified location
Raises:
KeyError: If UUID or reference not found
IndexError: If index out of range
TypeError: If key is invalid type
"""
if isinstance(key, str):
# Try reference first (most common use case)
component = self._reference_index.get(key)
if component is not None:
return component
# Fall back to UUID lookup (from base class)
try:
return super().__getitem__(key)
except KeyError:
raise KeyError(f"Component not found: {key}")
else:
# Integer index (from base class)
return super().__getitem__(key)
[docs]
def __contains__(self, reference: str) -> bool:
"""Check if reference exists."""
return reference in self._reference_index
# Internal methods
def _add_to_indexes(self, component: Component):
"""Add component to all indexes (base + component-specific)."""
# Add to base collection (UUID index)
self._add_item(component)
# Add to reference index
self._reference_index[component.reference] = component
# Add to lib_id index
lib_id = component.lib_id
if lib_id not in self._lib_id_index:
self._lib_id_index[lib_id] = []
self._lib_id_index[lib_id].append(component)
# Add to value index
value = component.value
if value:
if value not in self._value_index:
self._value_index[value] = []
self._value_index[value].append(component)
def _remove_from_indexes(self, component: Component):
"""Remove component from component-specific indexes (not base UUID index)."""
# Remove from reference index
del self._reference_index[component.reference]
# Remove from lib_id index
lib_id = component.lib_id
if lib_id in self._lib_id_index:
self._lib_id_index[lib_id].remove(component)
if not self._lib_id_index[lib_id]:
del self._lib_id_index[lib_id]
# Remove from value index
value = component.value
if value and value in self._value_index:
self._value_index[value].remove(component)
if not self._value_index[value]:
del self._value_index[value]
def _update_reference_index(self, old_ref: str, new_ref: str):
"""Update reference index when component reference changes."""
if old_ref in self._reference_index:
component = self._reference_index[old_ref]
del self._reference_index[old_ref]
self._reference_index[new_ref] = component
# Note: UUID doesn't change when reference changes, so base index is unaffected
def _generate_reference(self, lib_id: str) -> str:
"""Generate unique reference for component."""
# Get reference prefix from symbol definition
symbol_cache = get_symbol_cache()
symbol_def = symbol_cache.get_symbol(lib_id)
prefix = symbol_def.reference_prefix if symbol_def else "U"
# Find next available number
counter = 1
while f"{prefix}{counter}" in self._reference_index:
counter += 1
return f"{prefix}{counter}"
def _find_available_position(self) -> Point:
"""Find an available position for automatic placement."""
# Simple grid placement - could be enhanced with collision detection
grid_size = 10.0 # 10mm grid
max_per_row = 10
row = len(self._items) // max_per_row
col = len(self._items) % max_per_row
return Point(col * grid_size, row * grid_size)