"""
Label element management for KiCAD schematics.
This module provides collection classes for managing label elements,
featuring fast lookup, bulk operations, and validation.
"""
import logging
import uuid
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
from .collections import BaseCollection
from .types import Label, Point
logger = logging.getLogger(__name__)
[docs]
class LabelElement:
"""
Enhanced wrapper for schematic label elements with modern API.
Provides intuitive access to label properties and operations
while maintaining exact format preservation.
"""
[docs]
def __init__(self, label_data: Label, parent_collection: "LabelCollection"):
"""
Initialize label element wrapper.
Args:
label_data: Underlying label data
parent_collection: Parent collection for updates
"""
self._data = label_data
self._collection = parent_collection
self._validator = SchematicValidator()
# Core properties with validation
@property
def uuid(self) -> str:
"""Label element UUID."""
return self._data.uuid
@property
def text(self) -> str:
"""Label text (net name)."""
return self._data.text
@text.setter
def text(self, value: str):
"""Set label text with validation."""
if not isinstance(value, str) or not value.strip():
raise ValidationError("Label text cannot be empty")
old_text = self._data.text
self._data.text = value.strip()
self._collection._update_text_index(old_text, self)
self._collection._mark_modified()
@property
def position(self) -> Point:
"""Label position."""
return self._data.position
@position.setter
def position(self, value: Union[Point, Tuple[float, float]]):
"""Set label position."""
if isinstance(value, tuple):
value = Point(value[0], value[1])
elif not isinstance(value, Point):
raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
self._data.position = value
self._collection._mark_modified()
@property
def rotation(self) -> float:
"""Label rotation in degrees."""
return self._data.rotation
@rotation.setter
def rotation(self, value: float):
"""Set label rotation."""
self._data.rotation = float(value)
self._collection._mark_modified()
@property
def size(self) -> float:
"""Label text size."""
return self._data.size
@size.setter
def size(self, value: float):
"""Set label size with validation."""
if value <= 0:
raise ValidationError(f"Label size must be positive, got {value}")
self._data.size = float(value)
self._collection._mark_modified()
[docs]
def validate(self) -> List[ValidationIssue]:
"""Validate this label element."""
return self._validator.validate_label(self._data.__dict__)
[docs]
def to_dict(self) -> Dict[str, Any]:
"""Convert label element to dictionary representation."""
return {
"uuid": self.uuid,
"text": self.text,
"position": {"x": self.position.x, "y": self.position.y},
"rotation": self.rotation,
"size": self.size,
}
[docs]
def __str__(self) -> str:
"""String representation."""
return f"<Label '{self.text}' @ {self.position}>"
[docs]
class LabelCollection(BaseCollection[LabelElement]):
"""
Collection class for efficient label element management.
Inherits from BaseCollection for standard operations and adds label-specific
functionality including text-based indexing.
Provides fast lookup, filtering, and bulk operations for schematic label elements.
"""
[docs]
def __init__(self, labels: List[Label] = None):
"""
Initialize label collection.
Args:
labels: Initial list of label data
"""
# Initialize base collection
super().__init__([], collection_name="labels")
# Additional label-specific index
self._text_index: Dict[str, List[LabelElement]] = {}
# Add initial labels
if labels:
for label_data in labels:
self._add_to_indexes(LabelElement(label_data, self))
[docs]
def add(
self,
text: str,
position: Union[Point, Tuple[float, float]],
rotation: float = 0.0,
size: float = 1.27,
label_uuid: Optional[str] = None,
) -> LabelElement:
"""
Add a new label element to the schematic.
Args:
text: Label text (net name)
position: Label position
rotation: Label rotation in degrees
size: Label text size
label_uuid: Specific UUID for label (auto-generated if None)
Returns:
Newly created LabelElement
Raises:
ValidationError: If label data is invalid
"""
# Validate inputs
if not isinstance(text, str) or not text.strip():
raise ValidationError("Label text cannot be empty")
if isinstance(position, tuple):
position = Point(position[0], position[1])
elif not isinstance(position, Point):
raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
if size <= 0:
raise ValidationError(f"Label size must be positive, got {size}")
# Generate UUID if not provided
if not label_uuid:
label_uuid = str(uuid.uuid4())
# Check for duplicate UUID
if label_uuid in self._uuid_index:
raise ValidationError(f"Label UUID {label_uuid} already exists")
# Create label data
label_data = Label(
uuid=label_uuid,
position=position,
text=text.strip(),
rotation=rotation,
size=size,
)
# Create wrapper and add to collection
label_element = LabelElement(label_data, self)
self._add_to_indexes(label_element)
self._mark_modified()
logger.debug(f"Added label: {label_element}")
return label_element
# get() method inherited from BaseCollection
[docs]
def get_by_text(self, text: str) -> List[LabelElement]:
"""Get all labels with the given text."""
return self._text_index.get(text, []).copy()
[docs]
def remove(self, label_uuid: str) -> bool:
"""
Remove label by UUID.
Args:
label_uuid: UUID of label to remove
Returns:
True if label was removed, False if not found
"""
label_element = self.get(label_uuid)
if not label_element:
return False
# Remove from text index
text = label_element.text
if text in self._text_index:
self._text_index[text].remove(label_element)
if not self._text_index[text]:
del self._text_index[text]
# Remove using base class method
super().remove(label_uuid)
logger.debug(f"Removed label: {label_element}")
return True
[docs]
def find_by_text(self, text: str, exact: bool = True) -> List[LabelElement]:
"""
Find labels by text.
Args:
text: Text to search for
exact: If True, exact match; if False, substring match
Returns:
List of matching label elements
"""
if exact:
return self._text_index.get(text, []).copy()
else:
matches = []
for label_element in self._items:
if text.lower() in label_element.text.lower():
matches.append(label_element)
return matches
[docs]
def filter(self, predicate: Callable[[LabelElement], bool]) -> List[LabelElement]:
"""
Filter labels by predicate function.
Args:
predicate: Function that returns True for labels to include
Returns:
List of labels matching predicate
"""
return [label for label in self._items if predicate(label)]
[docs]
def bulk_update(self, criteria: Callable[[LabelElement], bool], updates: Dict[str, Any]):
"""
Update multiple labels matching criteria.
Args:
criteria: Function to select labels to update
updates: Dictionary of property updates
"""
updated_count = 0
for label_element in self._items:
if criteria(label_element):
for prop, value in updates.items():
if hasattr(label_element, prop):
setattr(label_element, prop, value)
updated_count += 1
if updated_count > 0:
self._mark_modified()
logger.debug(f"Bulk updated {updated_count} label properties")
[docs]
def clear(self):
"""Remove all labels from collection."""
self._text_index.clear()
super().clear()
def _add_to_indexes(self, label_element: LabelElement):
"""Add label to internal indexes (base + text index)."""
self._add_item(label_element)
# Add to text index
text = label_element.text
if text not in self._text_index:
self._text_index[text] = []
self._text_index[text].append(label_element)
def _update_text_index(self, old_text: str, label_element: LabelElement):
"""Update text index when label text changes."""
# Remove from old text index
if old_text in self._text_index:
self._text_index[old_text].remove(label_element)
if not self._text_index[old_text]:
del self._text_index[old_text]
# Add to new text index
new_text = label_element.text
if new_text not in self._text_index:
self._text_index[new_text] = []
self._text_index[new_text].append(label_element)
# Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
[docs]
def __bool__(self) -> bool:
"""Return True if collection has labels."""
return len(self._items) > 0