kicad-sch-api 0.4.1__py3-none-any.whl → 0.5.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.
- kicad_sch_api/__init__.py +67 -2
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/collections/__init__.py +23 -8
- kicad_sch_api/collections/base.py +369 -59
- kicad_sch_api/collections/components.py +1376 -187
- kicad_sch_api/collections/junctions.py +129 -289
- kicad_sch_api/collections/labels.py +391 -287
- kicad_sch_api/collections/wires.py +202 -316
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/component_bounds.py +34 -12
- kicad_sch_api/core/components.py +146 -7
- kicad_sch_api/core/config.py +25 -12
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/element_factory.py +3 -1
- kicad_sch_api/core/formatter.py +24 -7
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/managers/__init__.py +4 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +3 -1
- kicad_sch_api/core/managers/format_sync.py +3 -2
- kicad_sch_api/core/managers/graphics.py +3 -2
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +4 -2
- kicad_sch_api/core/managers/sheet.py +52 -14
- kicad_sch_api/core/managers/text_elements.py +3 -2
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +112 -54
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +343 -29
- kicad_sch_api/core/types.py +79 -7
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +15 -3
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/parsers/elements/label_parser.py +30 -8
- kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
- kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
- kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,404 +1,508 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Enhanced label management with IndexRegistry integration.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
text indexing
|
|
4
|
+
Provides LabelElement wrapper and LabelCollection using BaseCollection
|
|
5
|
+
infrastructure with text indexing and position-based queries.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import uuid as uuid_module
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
|
-
from ..core.types import Point
|
|
13
|
-
from .
|
|
12
|
+
from ..core.types import Label, Point
|
|
13
|
+
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
14
|
+
from .base import BaseCollection, IndexSpec, ValidationLevel
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class
|
|
19
|
-
"""
|
|
19
|
+
class LabelElement:
|
|
20
|
+
"""
|
|
21
|
+
Enhanced wrapper for schematic label elements.
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
23
|
+
Provides intuitive access to label properties and operations
|
|
24
|
+
while maintaining exact format preservation. All property
|
|
25
|
+
modifications automatically notify the parent collection.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, label_data: Label, parent_collection: "LabelCollection"):
|
|
29
|
+
"""
|
|
30
|
+
Initialize label element wrapper.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
label_data: Underlying label data
|
|
34
|
+
parent_collection: Parent collection for modification tracking
|
|
35
|
+
"""
|
|
36
|
+
self._data = label_data
|
|
37
|
+
self._collection = parent_collection
|
|
38
|
+
self._validator = SchematicValidator()
|
|
39
|
+
|
|
40
|
+
# Core properties with validation
|
|
41
|
+
@property
|
|
42
|
+
def uuid(self) -> str:
|
|
43
|
+
"""Label element UUID (read-only)."""
|
|
44
|
+
return self._data.uuid
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def text(self) -> str:
|
|
48
|
+
"""Label text (net name)."""
|
|
49
|
+
return self._data.text
|
|
50
|
+
|
|
51
|
+
@text.setter
|
|
52
|
+
def text(self, value: str):
|
|
53
|
+
"""
|
|
54
|
+
Set label text with validation.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
value: New label text
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValidationError: If text is empty
|
|
61
|
+
"""
|
|
62
|
+
if not isinstance(value, str) or not value.strip():
|
|
63
|
+
raise ValidationError("Label text cannot be empty")
|
|
64
|
+
|
|
65
|
+
old_text = self._data.text
|
|
66
|
+
self._data.text = value.strip()
|
|
67
|
+
self._collection._update_text_index(old_text, self)
|
|
68
|
+
self._collection._mark_modified()
|
|
69
|
+
logger.debug(f"Updated label text: '{old_text}' -> '{value}'")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def position(self) -> Point:
|
|
73
|
+
"""Label position in schematic."""
|
|
74
|
+
return self._data.position
|
|
75
|
+
|
|
76
|
+
@position.setter
|
|
77
|
+
def position(self, value: Union[Point, Tuple[float, float]]):
|
|
78
|
+
"""Set label position."""
|
|
79
|
+
if isinstance(value, tuple):
|
|
80
|
+
value = Point(value[0], value[1])
|
|
81
|
+
elif not isinstance(value, Point):
|
|
82
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
|
|
83
|
+
|
|
84
|
+
self._data.position = value
|
|
85
|
+
self._collection._mark_modified()
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def rotation(self) -> float:
|
|
89
|
+
"""Label rotation in degrees."""
|
|
90
|
+
return self._data.rotation
|
|
91
|
+
|
|
92
|
+
@rotation.setter
|
|
93
|
+
def rotation(self, value: float):
|
|
94
|
+
"""Set label rotation."""
|
|
95
|
+
self._data.rotation = float(value)
|
|
96
|
+
self._collection._mark_modified()
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def size(self) -> float:
|
|
100
|
+
"""Label text size."""
|
|
101
|
+
return self._data.size
|
|
102
|
+
|
|
103
|
+
@size.setter
|
|
104
|
+
def size(self, value: float):
|
|
105
|
+
"""
|
|
106
|
+
Set label size with validation.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
value: New text size
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValidationError: If size is not positive
|
|
113
|
+
"""
|
|
114
|
+
if value <= 0:
|
|
115
|
+
raise ValidationError(f"Label size must be positive, got {value}")
|
|
116
|
+
|
|
117
|
+
self._data.size = float(value)
|
|
118
|
+
self._collection._mark_modified()
|
|
119
|
+
|
|
120
|
+
# Utility methods
|
|
121
|
+
def move(self, x: float, y: float):
|
|
122
|
+
"""Move label to absolute position."""
|
|
123
|
+
self.position = Point(x, y)
|
|
124
|
+
|
|
125
|
+
def translate(self, dx: float, dy: float):
|
|
126
|
+
"""Translate label by offset."""
|
|
127
|
+
current = self.position
|
|
128
|
+
self.position = Point(current.x + dx, current.y + dy)
|
|
129
|
+
|
|
130
|
+
def rotate_by(self, angle: float):
|
|
131
|
+
"""Rotate label by angle (cumulative)."""
|
|
132
|
+
self.rotation = (self.rotation + angle) % 360
|
|
133
|
+
|
|
134
|
+
def validate(self) -> List[ValidationIssue]:
|
|
135
|
+
"""
|
|
136
|
+
Validate this label element.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of validation issues (empty if valid)
|
|
140
|
+
"""
|
|
141
|
+
issues = []
|
|
142
|
+
|
|
143
|
+
# Validate text is not empty
|
|
144
|
+
if not self.text or not self.text.strip():
|
|
145
|
+
issues.append(
|
|
146
|
+
ValidationIssue(
|
|
147
|
+
category="label",
|
|
148
|
+
message="Label text is empty",
|
|
149
|
+
level="error"
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Validate size is positive
|
|
154
|
+
if self.size <= 0:
|
|
155
|
+
issues.append(
|
|
156
|
+
ValidationIssue(
|
|
157
|
+
category="label",
|
|
158
|
+
message=f"Label size must be positive, got {self.size}",
|
|
159
|
+
level="error"
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return issues
|
|
164
|
+
|
|
165
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
166
|
+
"""
|
|
167
|
+
Convert label to dictionary representation.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dictionary with label data
|
|
171
|
+
"""
|
|
172
|
+
return {
|
|
173
|
+
"text": self.text,
|
|
174
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
175
|
+
"rotation": self.rotation,
|
|
176
|
+
"size": self.size,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
def __str__(self) -> str:
|
|
180
|
+
"""String representation for display."""
|
|
181
|
+
return f"<Label '{self.text}' @ {self.position}>"
|
|
36
182
|
|
|
37
183
|
def __repr__(self) -> str:
|
|
38
|
-
|
|
184
|
+
"""Detailed representation for debugging."""
|
|
185
|
+
return f"LabelElement(text='{self.text}', pos={self.position}, rotation={self.rotation})"
|
|
39
186
|
|
|
40
187
|
|
|
41
|
-
class LabelCollection(
|
|
188
|
+
class LabelCollection(BaseCollection[LabelElement]):
|
|
42
189
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
190
|
+
Label collection with text indexing and position queries.
|
|
191
|
+
|
|
192
|
+
Inherits from BaseCollection for UUID indexing and adds label-specific
|
|
193
|
+
functionality including text-based searches and filtering.
|
|
194
|
+
|
|
195
|
+
Features:
|
|
196
|
+
- Fast UUID lookup via IndexRegistry
|
|
197
|
+
- Text-based label indexing
|
|
198
|
+
- Position-based queries
|
|
199
|
+
- Lazy index rebuilding
|
|
200
|
+
- Batch mode support
|
|
51
201
|
"""
|
|
52
202
|
|
|
53
|
-
def __init__(
|
|
203
|
+
def __init__(
|
|
204
|
+
self,
|
|
205
|
+
labels: Optional[List[Label]] = None,
|
|
206
|
+
validation_level: ValidationLevel = ValidationLevel.NORMAL,
|
|
207
|
+
):
|
|
54
208
|
"""
|
|
55
209
|
Initialize label collection.
|
|
56
210
|
|
|
57
211
|
Args:
|
|
58
|
-
labels: Initial list of
|
|
212
|
+
labels: Initial list of label data
|
|
213
|
+
validation_level: Validation level for operations
|
|
59
214
|
"""
|
|
60
|
-
|
|
61
|
-
self._position_index: Dict[Tuple[float, float], List[Label]] = {}
|
|
62
|
-
self._type_index: Dict[str, List[Label]] = {}
|
|
215
|
+
super().__init__(validation_level=validation_level)
|
|
63
216
|
|
|
64
|
-
|
|
217
|
+
# Manual text index (non-unique - multiple labels can have same text)
|
|
218
|
+
self._text_index: Dict[str, List[LabelElement]] = {}
|
|
65
219
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
220
|
+
# Add initial labels
|
|
221
|
+
if labels:
|
|
222
|
+
with self.batch_mode():
|
|
223
|
+
for label_data in labels:
|
|
224
|
+
label_element = LabelElement(label_data, self)
|
|
225
|
+
super().add(label_element)
|
|
226
|
+
self._add_to_text_index(label_element)
|
|
70
227
|
|
|
71
|
-
|
|
72
|
-
"""Create a new label with given parameters."""
|
|
73
|
-
# This will be called by add() methods
|
|
74
|
-
raise NotImplementedError("Use add() method instead")
|
|
228
|
+
logger.debug(f"LabelCollection initialized with {len(self)} labels")
|
|
75
229
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
self._position_index.clear()
|
|
81
|
-
self._type_index.clear()
|
|
230
|
+
# BaseCollection abstract method implementations
|
|
231
|
+
def _get_item_uuid(self, item: LabelElement) -> str:
|
|
232
|
+
"""Extract UUID from label element."""
|
|
233
|
+
return item.uuid
|
|
82
234
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self._type_index[label.label_type] = []
|
|
100
|
-
self._type_index[label.label_type].append(label)
|
|
101
|
-
|
|
102
|
-
# Label-specific methods
|
|
235
|
+
def _create_item(self, **kwargs) -> LabelElement:
|
|
236
|
+
"""Create a new label (not typically used directly)."""
|
|
237
|
+
raise NotImplementedError("Use add() method to create labels")
|
|
238
|
+
|
|
239
|
+
def _get_index_specs(self) -> List[IndexSpec]:
|
|
240
|
+
"""Get index specifications for label collection."""
|
|
241
|
+
return [
|
|
242
|
+
IndexSpec(
|
|
243
|
+
name="uuid",
|
|
244
|
+
key_func=lambda l: l.uuid,
|
|
245
|
+
unique=True,
|
|
246
|
+
description="UUID index for fast lookups",
|
|
247
|
+
),
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
# Label-specific add method
|
|
103
251
|
def add(
|
|
104
252
|
self,
|
|
105
253
|
text: str,
|
|
106
254
|
position: Union[Point, Tuple[float, float]],
|
|
107
|
-
label_type: str = "label",
|
|
108
255
|
rotation: float = 0.0,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
256
|
+
size: float = 1.27,
|
|
257
|
+
justify_h: str = "left",
|
|
258
|
+
justify_v: str = "bottom",
|
|
259
|
+
uuid: Optional[str] = None,
|
|
260
|
+
) -> LabelElement:
|
|
112
261
|
"""
|
|
113
|
-
Add a
|
|
262
|
+
Add a label to the collection.
|
|
114
263
|
|
|
115
264
|
Args:
|
|
116
|
-
text: Label text
|
|
265
|
+
text: Label text (net name)
|
|
117
266
|
position: Label position
|
|
118
|
-
label_type: Type of label ("label", "global_label", "hierarchical_label")
|
|
119
267
|
rotation: Label rotation in degrees
|
|
120
|
-
|
|
121
|
-
|
|
268
|
+
size: Text size
|
|
269
|
+
justify_h: Horizontal justification ("left", "right", "center")
|
|
270
|
+
justify_v: Vertical justification ("top", "bottom", "center")
|
|
271
|
+
uuid: Optional UUID (auto-generated if not provided)
|
|
122
272
|
|
|
123
273
|
Returns:
|
|
124
|
-
|
|
274
|
+
LabelElement wrapper for the created label
|
|
125
275
|
|
|
126
276
|
Raises:
|
|
127
|
-
ValueError: If
|
|
277
|
+
ValueError: If UUID already exists or text is empty
|
|
128
278
|
"""
|
|
129
279
|
# Validate text
|
|
130
|
-
if not text.strip():
|
|
280
|
+
if not text or not text.strip():
|
|
131
281
|
raise ValueError("Label text cannot be empty")
|
|
132
282
|
|
|
133
|
-
#
|
|
283
|
+
# Generate UUID if not provided
|
|
284
|
+
if uuid is None:
|
|
285
|
+
uuid = str(uuid_module.uuid4())
|
|
286
|
+
else:
|
|
287
|
+
# Check for duplicate
|
|
288
|
+
self._ensure_indexes_current()
|
|
289
|
+
if self._index_registry.has_key("uuid", uuid):
|
|
290
|
+
raise ValueError(f"Label with UUID '{uuid}' already exists")
|
|
291
|
+
|
|
292
|
+
# Convert position
|
|
134
293
|
if isinstance(position, tuple):
|
|
135
294
|
position = Point(position[0], position[1])
|
|
136
295
|
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# Generate UUID if not provided
|
|
143
|
-
if label_uuid is None:
|
|
144
|
-
label_uuid = str(uuid_module.uuid4())
|
|
145
|
-
|
|
146
|
-
# Create label
|
|
147
|
-
label = Label(
|
|
148
|
-
uuid=label_uuid,
|
|
149
|
-
text=text,
|
|
296
|
+
# Create label data
|
|
297
|
+
label_data = Label(
|
|
298
|
+
uuid=uuid,
|
|
299
|
+
text=text.strip(),
|
|
150
300
|
position=position,
|
|
151
301
|
rotation=rotation,
|
|
152
|
-
|
|
153
|
-
|
|
302
|
+
size=size,
|
|
303
|
+
justify_h=justify_h,
|
|
304
|
+
justify_v=justify_v,
|
|
154
305
|
)
|
|
155
306
|
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def get_labels_by_text(self, text: str, case_sensitive: bool = False) -> List[Label]:
|
|
160
|
-
"""
|
|
161
|
-
Get all labels with specific text.
|
|
307
|
+
# Create label element wrapper
|
|
308
|
+
label_element = LabelElement(label_data, self)
|
|
162
309
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
case_sensitive: Whether search should be case sensitive
|
|
310
|
+
# Add to collection
|
|
311
|
+
super().add(label_element)
|
|
166
312
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"""
|
|
170
|
-
self._ensure_indexes_current()
|
|
313
|
+
# Add to text index
|
|
314
|
+
self._add_to_text_index(label_element)
|
|
171
315
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
else:
|
|
175
|
-
text_key = text.lower()
|
|
176
|
-
return self._text_index.get(text_key, []).copy()
|
|
316
|
+
logger.debug(f"Added label '{text}' at {position}, UUID={uuid}")
|
|
317
|
+
return label_element
|
|
177
318
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
) -> List[Label]:
|
|
319
|
+
# Remove operation (override to update text index)
|
|
320
|
+
def remove(self, uuid: str) -> bool:
|
|
181
321
|
"""
|
|
182
|
-
|
|
322
|
+
Remove label by UUID.
|
|
183
323
|
|
|
184
324
|
Args:
|
|
185
|
-
|
|
186
|
-
tolerance: Position tolerance for matching
|
|
325
|
+
uuid: Label UUID to remove
|
|
187
326
|
|
|
188
327
|
Returns:
|
|
189
|
-
|
|
190
|
-
"""
|
|
191
|
-
self._ensure_indexes_current()
|
|
192
|
-
|
|
193
|
-
if isinstance(position, Point):
|
|
194
|
-
pos_key = (position.x, position.y)
|
|
195
|
-
else:
|
|
196
|
-
pos_key = position
|
|
197
|
-
|
|
198
|
-
if tolerance == 0.0:
|
|
199
|
-
# Exact match
|
|
200
|
-
return self._position_index.get(pos_key, []).copy()
|
|
201
|
-
else:
|
|
202
|
-
# Tolerance-based search
|
|
203
|
-
matching_labels = []
|
|
204
|
-
target_x, target_y = pos_key
|
|
205
|
-
|
|
206
|
-
for label in self._items:
|
|
207
|
-
dx = abs(label.position.x - target_x)
|
|
208
|
-
dy = abs(label.position.y - target_y)
|
|
209
|
-
distance = (dx**2 + dy**2) ** 0.5
|
|
210
|
-
|
|
211
|
-
if distance <= tolerance:
|
|
212
|
-
matching_labels.append(label)
|
|
213
|
-
|
|
214
|
-
return matching_labels
|
|
215
|
-
|
|
216
|
-
def get_labels_by_type(self, label_type: str) -> List[Label]:
|
|
328
|
+
True if label was removed, False if not found
|
|
217
329
|
"""
|
|
218
|
-
Get
|
|
330
|
+
# Get label before removing
|
|
331
|
+
label = self.get(uuid)
|
|
332
|
+
if not label:
|
|
333
|
+
return False
|
|
219
334
|
|
|
220
|
-
|
|
221
|
-
|
|
335
|
+
# Remove from text index
|
|
336
|
+
self._remove_from_text_index(label)
|
|
222
337
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"""
|
|
226
|
-
self._ensure_indexes_current()
|
|
227
|
-
return self._type_index.get(label_type, []).copy()
|
|
338
|
+
# Remove from base collection
|
|
339
|
+
result = super().remove(uuid)
|
|
228
340
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Get all unique net names from labels.
|
|
341
|
+
if result:
|
|
342
|
+
logger.info(f"Removed label '{label.text}'")
|
|
232
343
|
|
|
233
|
-
|
|
234
|
-
List of unique net names
|
|
235
|
-
"""
|
|
236
|
-
return list(set(label.text for label in self._items))
|
|
344
|
+
return result
|
|
237
345
|
|
|
238
|
-
|
|
346
|
+
# Text-based queries
|
|
347
|
+
def get_by_text(self, text: str) -> List[LabelElement]:
|
|
239
348
|
"""
|
|
240
|
-
|
|
349
|
+
Find all labels with specific text.
|
|
241
350
|
|
|
242
351
|
Args:
|
|
243
|
-
|
|
244
|
-
case_sensitive: Whether search should be case sensitive
|
|
352
|
+
text: Text to search for
|
|
245
353
|
|
|
246
354
|
Returns:
|
|
247
|
-
List of labels
|
|
355
|
+
List of labels with matching text
|
|
248
356
|
"""
|
|
249
|
-
return self.
|
|
357
|
+
return self._text_index.get(text, [])
|
|
250
358
|
|
|
251
|
-
def
|
|
252
|
-
self, min_x: float, min_y: float, max_x: float, max_y: float
|
|
253
|
-
) -> List[Label]:
|
|
359
|
+
def filter_by_text_pattern(self, pattern: str) -> List[LabelElement]:
|
|
254
360
|
"""
|
|
255
|
-
Find
|
|
361
|
+
Find labels with text containing a pattern.
|
|
256
362
|
|
|
257
363
|
Args:
|
|
258
|
-
|
|
259
|
-
min_y: Minimum Y coordinate
|
|
260
|
-
max_x: Maximum X coordinate
|
|
261
|
-
max_y: Maximum Y coordinate
|
|
364
|
+
pattern: Text pattern to search for (case-insensitive)
|
|
262
365
|
|
|
263
366
|
Returns:
|
|
264
|
-
List of labels
|
|
367
|
+
List of labels with matching text
|
|
265
368
|
"""
|
|
266
|
-
|
|
369
|
+
pattern_lower = pattern.lower()
|
|
370
|
+
matching = []
|
|
267
371
|
|
|
268
372
|
for label in self._items:
|
|
269
|
-
if
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return matching_labels
|
|
273
|
-
|
|
274
|
-
def update_label_text(self, label_uuid: str, new_text: str) -> bool:
|
|
275
|
-
"""
|
|
276
|
-
Update the text of an existing label.
|
|
277
|
-
|
|
278
|
-
Args:
|
|
279
|
-
label_uuid: UUID of label to update
|
|
280
|
-
new_text: New text content
|
|
281
|
-
|
|
282
|
-
Returns:
|
|
283
|
-
True if label was updated, False if not found
|
|
284
|
-
|
|
285
|
-
Raises:
|
|
286
|
-
ValueError: If new text is empty
|
|
287
|
-
"""
|
|
288
|
-
if not new_text.strip():
|
|
289
|
-
raise ValueError("Label text cannot be empty")
|
|
373
|
+
if pattern_lower in label.text.lower():
|
|
374
|
+
matching.append(label)
|
|
290
375
|
|
|
291
|
-
|
|
292
|
-
if not label:
|
|
293
|
-
return False
|
|
294
|
-
|
|
295
|
-
# Update text
|
|
296
|
-
label.text = new_text
|
|
297
|
-
self._mark_modified()
|
|
298
|
-
self._mark_indexes_dirty()
|
|
299
|
-
|
|
300
|
-
logger.debug(f"Updated label {label_uuid} text to '{new_text}'")
|
|
301
|
-
return True
|
|
376
|
+
return matching
|
|
302
377
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
378
|
+
# Position-based queries
|
|
379
|
+
def get_at_position(
|
|
380
|
+
self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.01
|
|
381
|
+
) -> Optional[LabelElement]:
|
|
306
382
|
"""
|
|
307
|
-
|
|
383
|
+
Find label at or near a specific position.
|
|
308
384
|
|
|
309
385
|
Args:
|
|
310
|
-
|
|
311
|
-
|
|
386
|
+
position: Position to search
|
|
387
|
+
tolerance: Distance tolerance for matching
|
|
312
388
|
|
|
313
389
|
Returns:
|
|
314
|
-
|
|
390
|
+
Label if found, None otherwise
|
|
315
391
|
"""
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return False
|
|
319
|
-
|
|
320
|
-
# Convert tuple to Point if needed
|
|
321
|
-
if isinstance(new_position, tuple):
|
|
322
|
-
new_position = Point(new_position[0], new_position[1])
|
|
392
|
+
if isinstance(position, tuple):
|
|
393
|
+
position = Point(position[0], position[1])
|
|
323
394
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
self._mark_indexes_dirty()
|
|
395
|
+
for label in self._items:
|
|
396
|
+
if label.position.distance_to(position) <= tolerance:
|
|
397
|
+
return label
|
|
328
398
|
|
|
329
|
-
|
|
330
|
-
return True
|
|
399
|
+
return None
|
|
331
400
|
|
|
332
|
-
|
|
333
|
-
|
|
401
|
+
def get_near_point(
|
|
402
|
+
self, point: Union[Point, Tuple[float, float]], radius: float
|
|
403
|
+
) -> List[LabelElement]:
|
|
334
404
|
"""
|
|
335
|
-
|
|
405
|
+
Find all labels within radius of a point.
|
|
336
406
|
|
|
337
407
|
Args:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
case_sensitive: Whether search should be case sensitive
|
|
408
|
+
point: Center point
|
|
409
|
+
radius: Search radius
|
|
341
410
|
|
|
342
411
|
Returns:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
Raises:
|
|
346
|
-
ValueError: If new name is empty
|
|
412
|
+
List of labels within radius
|
|
347
413
|
"""
|
|
348
|
-
if
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
labels_to_rename = self.get_labels_by_text(old_name, case_sensitive)
|
|
352
|
-
|
|
353
|
-
for label in labels_to_rename:
|
|
354
|
-
label.text = new_name
|
|
414
|
+
if isinstance(point, tuple):
|
|
415
|
+
point = Point(point[0], point[1])
|
|
355
416
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
417
|
+
matching = []
|
|
418
|
+
for label in self._items:
|
|
419
|
+
if label.position.distance_to(point) <= radius:
|
|
420
|
+
matching.append(label)
|
|
359
421
|
|
|
360
|
-
|
|
361
|
-
return len(labels_to_rename)
|
|
422
|
+
return matching
|
|
362
423
|
|
|
363
|
-
|
|
424
|
+
# Validation
|
|
425
|
+
def validate_all(self) -> List[ValidationIssue]:
|
|
364
426
|
"""
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
Args:
|
|
368
|
-
net_name: Net name to remove labels for
|
|
369
|
-
case_sensitive: Whether search should be case sensitive
|
|
427
|
+
Validate all labels in collection.
|
|
370
428
|
|
|
371
429
|
Returns:
|
|
372
|
-
|
|
430
|
+
List of validation issues found
|
|
373
431
|
"""
|
|
374
|
-
|
|
432
|
+
all_issues = []
|
|
375
433
|
|
|
376
|
-
for label in
|
|
377
|
-
|
|
434
|
+
for label in self._items:
|
|
435
|
+
issues = label.validate()
|
|
436
|
+
all_issues.extend(issues)
|
|
378
437
|
|
|
379
|
-
|
|
380
|
-
return len(labels_to_remove)
|
|
438
|
+
return all_issues
|
|
381
439
|
|
|
382
|
-
#
|
|
383
|
-
def
|
|
440
|
+
# Statistics
|
|
441
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
384
442
|
"""
|
|
385
|
-
Get label statistics
|
|
443
|
+
Get label collection statistics.
|
|
386
444
|
|
|
387
445
|
Returns:
|
|
388
446
|
Dictionary with label statistics
|
|
389
447
|
"""
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
"
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
|
|
448
|
+
if not self._items:
|
|
449
|
+
base_stats = super().get_statistics()
|
|
450
|
+
base_stats.update({
|
|
451
|
+
"total_labels": 0,
|
|
452
|
+
"unique_texts": 0,
|
|
453
|
+
"avg_size": 0,
|
|
454
|
+
})
|
|
455
|
+
return base_stats
|
|
456
|
+
|
|
457
|
+
unique_texts = len(self._text_index)
|
|
458
|
+
avg_size = sum(l.size for l in self._items) / len(self._items)
|
|
459
|
+
|
|
460
|
+
base_stats = super().get_statistics()
|
|
461
|
+
base_stats.update({
|
|
462
|
+
"total_labels": len(self._items),
|
|
463
|
+
"unique_texts": unique_texts,
|
|
464
|
+
"avg_size": avg_size,
|
|
465
|
+
"text_distribution": {text: len(labels) for text, labels in self._text_index.items()},
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
return base_stats
|
|
469
|
+
|
|
470
|
+
# Internal helper methods
|
|
471
|
+
def _add_to_text_index(self, label: LabelElement):
|
|
472
|
+
"""Add label to text index."""
|
|
473
|
+
text = label.text
|
|
474
|
+
if text not in self._text_index:
|
|
475
|
+
self._text_index[text] = []
|
|
476
|
+
self._text_index[text].append(label)
|
|
477
|
+
|
|
478
|
+
def _remove_from_text_index(self, label: LabelElement):
|
|
479
|
+
"""Remove label from text index."""
|
|
480
|
+
text = label.text
|
|
481
|
+
if text in self._text_index:
|
|
482
|
+
self._text_index[text].remove(label)
|
|
483
|
+
if not self._text_index[text]:
|
|
484
|
+
del self._text_index[text]
|
|
485
|
+
|
|
486
|
+
def _update_text_index(self, old_text: str, label: LabelElement):
|
|
487
|
+
"""Update text index when label text changes."""
|
|
488
|
+
# Remove from old text
|
|
489
|
+
if old_text in self._text_index:
|
|
490
|
+
self._text_index[old_text].remove(label)
|
|
491
|
+
if not self._text_index[old_text]:
|
|
492
|
+
del self._text_index[old_text]
|
|
493
|
+
|
|
494
|
+
# Add to new text
|
|
495
|
+
new_text = label.text
|
|
496
|
+
if new_text not in self._text_index:
|
|
497
|
+
self._text_index[new_text] = []
|
|
498
|
+
self._text_index[new_text].append(label)
|
|
499
|
+
|
|
500
|
+
# Compatibility methods
|
|
501
|
+
@property
|
|
502
|
+
def modified(self) -> bool:
|
|
503
|
+
"""Check if collection has been modified (compatibility)."""
|
|
504
|
+
return self.is_modified
|
|
505
|
+
|
|
506
|
+
def mark_saved(self) -> None:
|
|
507
|
+
"""Mark collection as saved (reset modified flag)."""
|
|
508
|
+
self.mark_clean()
|