kicad-sch-api 0.3.2__py3-none-any.whl → 0.3.5__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.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/collections/__init__.py +21 -0
- kicad_sch_api/collections/base.py +296 -0
- kicad_sch_api/collections/components.py +422 -0
- kicad_sch_api/collections/junctions.py +378 -0
- kicad_sch_api/collections/labels.py +412 -0
- kicad_sch_api/collections/wires.py +407 -0
- kicad_sch_api/core/formatter.py +31 -0
- kicad_sch_api/core/labels.py +348 -0
- kicad_sch_api/core/nets.py +310 -0
- kicad_sch_api/core/no_connects.py +274 -0
- kicad_sch_api/core/parser.py +72 -0
- kicad_sch_api/core/schematic.py +185 -9
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +26 -0
- kicad_sch_api/geometry/__init__.py +26 -0
- kicad_sch_api/geometry/font_metrics.py +20 -0
- kicad_sch_api/geometry/symbol_bbox.py +589 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +148 -0
- kicad_sch_api/parsers/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +153 -0
- kicad_sch_api/parsers/symbol_parser.py +227 -0
- kicad_sch_api/parsers/wire_parser.py +99 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +470 -0
- kicad_sch_api/symbols/resolver.py +367 -0
- kicad_sch_api/symbols/validators.py +453 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
- kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
- kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Label collection with specialized indexing and label-specific operations.
|
|
3
|
+
|
|
4
|
+
Extends the base IndexedCollection to provide label-specific features like
|
|
5
|
+
text indexing, position-based queries, and label type classification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid as uuid_module
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from ..core.types import Point
|
|
13
|
+
from .base import IndexedCollection
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Label:
|
|
19
|
+
"""Label data structure."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
uuid: str,
|
|
24
|
+
text: str,
|
|
25
|
+
position: Point,
|
|
26
|
+
rotation: float = 0.0,
|
|
27
|
+
label_type: str = "label",
|
|
28
|
+
effects: Optional[Dict[str, Any]] = None
|
|
29
|
+
):
|
|
30
|
+
self.uuid = uuid
|
|
31
|
+
self.text = text
|
|
32
|
+
self.position = position
|
|
33
|
+
self.rotation = rotation
|
|
34
|
+
self.label_type = label_type # "label", "global_label", "hierarchical_label"
|
|
35
|
+
self.effects = effects or {}
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
return f"Label(text='{self.text}', pos={self.position}, type='{self.label_type}')"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LabelCollection(IndexedCollection[Label]):
|
|
42
|
+
"""
|
|
43
|
+
Collection class for efficient label management.
|
|
44
|
+
|
|
45
|
+
Extends IndexedCollection with label-specific features:
|
|
46
|
+
- Text indexing for finding labels by text content
|
|
47
|
+
- Position indexing for spatial queries
|
|
48
|
+
- Type classification (local, global, hierarchical)
|
|
49
|
+
- Net name management
|
|
50
|
+
- Connectivity analysis support
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, labels: Optional[List[Label]] = None):
|
|
54
|
+
"""
|
|
55
|
+
Initialize label collection.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
labels: Initial list of labels
|
|
59
|
+
"""
|
|
60
|
+
self._text_index: Dict[str, List[Label]] = {}
|
|
61
|
+
self._position_index: Dict[Tuple[float, float], List[Label]] = {}
|
|
62
|
+
self._type_index: Dict[str, List[Label]] = {}
|
|
63
|
+
|
|
64
|
+
super().__init__(labels)
|
|
65
|
+
|
|
66
|
+
# Abstract method implementations
|
|
67
|
+
def _get_item_uuid(self, item: Label) -> str:
|
|
68
|
+
"""Extract UUID from label."""
|
|
69
|
+
return item.uuid
|
|
70
|
+
|
|
71
|
+
def _create_item(self, **kwargs) -> Label:
|
|
72
|
+
"""Create a new label with given parameters."""
|
|
73
|
+
# This will be called by add() methods
|
|
74
|
+
raise NotImplementedError("Use add() method instead")
|
|
75
|
+
|
|
76
|
+
def _build_additional_indexes(self) -> None:
|
|
77
|
+
"""Build label-specific indexes."""
|
|
78
|
+
# Clear existing indexes
|
|
79
|
+
self._text_index.clear()
|
|
80
|
+
self._position_index.clear()
|
|
81
|
+
self._type_index.clear()
|
|
82
|
+
|
|
83
|
+
# Rebuild indexes from current items
|
|
84
|
+
for label in self._items:
|
|
85
|
+
# Text index
|
|
86
|
+
text_key = label.text.lower() # Case-insensitive
|
|
87
|
+
if text_key not in self._text_index:
|
|
88
|
+
self._text_index[text_key] = []
|
|
89
|
+
self._text_index[text_key].append(label)
|
|
90
|
+
|
|
91
|
+
# Position index
|
|
92
|
+
pos_key = (label.position.x, label.position.y)
|
|
93
|
+
if pos_key not in self._position_index:
|
|
94
|
+
self._position_index[pos_key] = []
|
|
95
|
+
self._position_index[pos_key].append(label)
|
|
96
|
+
|
|
97
|
+
# Type index
|
|
98
|
+
if label.label_type not in self._type_index:
|
|
99
|
+
self._type_index[label.label_type] = []
|
|
100
|
+
self._type_index[label.label_type].append(label)
|
|
101
|
+
|
|
102
|
+
# Label-specific methods
|
|
103
|
+
def add(
|
|
104
|
+
self,
|
|
105
|
+
text: str,
|
|
106
|
+
position: Union[Point, Tuple[float, float]],
|
|
107
|
+
label_type: str = "label",
|
|
108
|
+
rotation: float = 0.0,
|
|
109
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
110
|
+
label_uuid: Optional[str] = None,
|
|
111
|
+
) -> Label:
|
|
112
|
+
"""
|
|
113
|
+
Add a new label to the collection.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
text: Label text content
|
|
117
|
+
position: Label position
|
|
118
|
+
label_type: Type of label ("label", "global_label", "hierarchical_label")
|
|
119
|
+
rotation: Label rotation in degrees
|
|
120
|
+
effects: Label effects (font, size, etc.)
|
|
121
|
+
label_uuid: Specific UUID for label (auto-generated if None)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Newly created Label
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If invalid parameters provided
|
|
128
|
+
"""
|
|
129
|
+
# Validate text
|
|
130
|
+
if not text.strip():
|
|
131
|
+
raise ValueError("Label text cannot be empty")
|
|
132
|
+
|
|
133
|
+
# Convert tuple to Point if needed
|
|
134
|
+
if isinstance(position, tuple):
|
|
135
|
+
position = Point(position[0], position[1])
|
|
136
|
+
|
|
137
|
+
# Validate label type
|
|
138
|
+
valid_types = ["label", "global_label", "hierarchical_label"]
|
|
139
|
+
if label_type not in valid_types:
|
|
140
|
+
raise ValueError(f"Invalid label type: {label_type}. Must be one of {valid_types}")
|
|
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,
|
|
150
|
+
position=position,
|
|
151
|
+
rotation=rotation,
|
|
152
|
+
label_type=label_type,
|
|
153
|
+
effects=effects or {}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Add to collection using base class method
|
|
157
|
+
return super().add(label)
|
|
158
|
+
|
|
159
|
+
def get_labels_by_text(self, text: str, case_sensitive: bool = False) -> List[Label]:
|
|
160
|
+
"""
|
|
161
|
+
Get all labels with specific text.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
text: Text to search for
|
|
165
|
+
case_sensitive: Whether search should be case sensitive
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of matching labels
|
|
169
|
+
"""
|
|
170
|
+
self._ensure_indexes_current()
|
|
171
|
+
|
|
172
|
+
if case_sensitive:
|
|
173
|
+
return [label for label in self._items if label.text == text]
|
|
174
|
+
else:
|
|
175
|
+
text_key = text.lower()
|
|
176
|
+
return self._text_index.get(text_key, []).copy()
|
|
177
|
+
|
|
178
|
+
def get_labels_at_position(
|
|
179
|
+
self,
|
|
180
|
+
position: Union[Point, Tuple[float, float]],
|
|
181
|
+
tolerance: float = 0.0
|
|
182
|
+
) -> List[Label]:
|
|
183
|
+
"""
|
|
184
|
+
Get all labels at a specific position.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
position: Position to search at
|
|
188
|
+
tolerance: Position tolerance for matching
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
List of labels at the position
|
|
192
|
+
"""
|
|
193
|
+
self._ensure_indexes_current()
|
|
194
|
+
|
|
195
|
+
if isinstance(position, Point):
|
|
196
|
+
pos_key = (position.x, position.y)
|
|
197
|
+
else:
|
|
198
|
+
pos_key = position
|
|
199
|
+
|
|
200
|
+
if tolerance == 0.0:
|
|
201
|
+
# Exact match
|
|
202
|
+
return self._position_index.get(pos_key, []).copy()
|
|
203
|
+
else:
|
|
204
|
+
# Tolerance-based search
|
|
205
|
+
matching_labels = []
|
|
206
|
+
target_x, target_y = pos_key
|
|
207
|
+
|
|
208
|
+
for label in self._items:
|
|
209
|
+
dx = abs(label.position.x - target_x)
|
|
210
|
+
dy = abs(label.position.y - target_y)
|
|
211
|
+
distance = (dx ** 2 + dy ** 2) ** 0.5
|
|
212
|
+
|
|
213
|
+
if distance <= tolerance:
|
|
214
|
+
matching_labels.append(label)
|
|
215
|
+
|
|
216
|
+
return matching_labels
|
|
217
|
+
|
|
218
|
+
def get_labels_by_type(self, label_type: str) -> List[Label]:
|
|
219
|
+
"""
|
|
220
|
+
Get all labels of a specific type.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
label_type: Type of labels to find
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List of labels of the specified type
|
|
227
|
+
"""
|
|
228
|
+
self._ensure_indexes_current()
|
|
229
|
+
return self._type_index.get(label_type, []).copy()
|
|
230
|
+
|
|
231
|
+
def get_net_names(self) -> List[str]:
|
|
232
|
+
"""
|
|
233
|
+
Get all unique net names from labels.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of unique net names
|
|
237
|
+
"""
|
|
238
|
+
return list(set(label.text for label in self._items))
|
|
239
|
+
|
|
240
|
+
def get_labels_for_net(self, net_name: str, case_sensitive: bool = False) -> List[Label]:
|
|
241
|
+
"""
|
|
242
|
+
Get all labels for a specific net.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
net_name: Net name to search for
|
|
246
|
+
case_sensitive: Whether search should be case sensitive
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of labels for the net
|
|
250
|
+
"""
|
|
251
|
+
return self.get_labels_by_text(net_name, case_sensitive)
|
|
252
|
+
|
|
253
|
+
def find_labels_in_region(
|
|
254
|
+
self,
|
|
255
|
+
min_x: float,
|
|
256
|
+
min_y: float,
|
|
257
|
+
max_x: float,
|
|
258
|
+
max_y: float
|
|
259
|
+
) -> List[Label]:
|
|
260
|
+
"""
|
|
261
|
+
Find all labels within a rectangular region.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
min_x: Minimum X coordinate
|
|
265
|
+
min_y: Minimum Y coordinate
|
|
266
|
+
max_x: Maximum X coordinate
|
|
267
|
+
max_y: Maximum Y coordinate
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of labels in the region
|
|
271
|
+
"""
|
|
272
|
+
matching_labels = []
|
|
273
|
+
|
|
274
|
+
for label in self._items:
|
|
275
|
+
if (min_x <= label.position.x <= max_x and
|
|
276
|
+
min_y <= label.position.y <= max_y):
|
|
277
|
+
matching_labels.append(label)
|
|
278
|
+
|
|
279
|
+
return matching_labels
|
|
280
|
+
|
|
281
|
+
def update_label_text(self, label_uuid: str, new_text: str) -> bool:
|
|
282
|
+
"""
|
|
283
|
+
Update the text of an existing label.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
label_uuid: UUID of label to update
|
|
287
|
+
new_text: New text content
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
True if label was updated, False if not found
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
ValueError: If new text is empty
|
|
294
|
+
"""
|
|
295
|
+
if not new_text.strip():
|
|
296
|
+
raise ValueError("Label text cannot be empty")
|
|
297
|
+
|
|
298
|
+
label = self.get(label_uuid)
|
|
299
|
+
if not label:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
# Update text
|
|
303
|
+
label.text = new_text
|
|
304
|
+
self._mark_modified()
|
|
305
|
+
self._mark_indexes_dirty()
|
|
306
|
+
|
|
307
|
+
logger.debug(f"Updated label {label_uuid} text to '{new_text}'")
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
def update_label_position(
|
|
311
|
+
self,
|
|
312
|
+
label_uuid: str,
|
|
313
|
+
new_position: Union[Point, Tuple[float, float]]
|
|
314
|
+
) -> bool:
|
|
315
|
+
"""
|
|
316
|
+
Update the position of an existing label.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
label_uuid: UUID of label to update
|
|
320
|
+
new_position: New position
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
True if label was updated, False if not found
|
|
324
|
+
"""
|
|
325
|
+
label = self.get(label_uuid)
|
|
326
|
+
if not label:
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
# Convert tuple to Point if needed
|
|
330
|
+
if isinstance(new_position, tuple):
|
|
331
|
+
new_position = Point(new_position[0], new_position[1])
|
|
332
|
+
|
|
333
|
+
# Update position
|
|
334
|
+
label.position = new_position
|
|
335
|
+
self._mark_modified()
|
|
336
|
+
self._mark_indexes_dirty()
|
|
337
|
+
|
|
338
|
+
logger.debug(f"Updated label {label_uuid} position to {new_position}")
|
|
339
|
+
return True
|
|
340
|
+
|
|
341
|
+
# Bulk operations
|
|
342
|
+
def rename_net(self, old_name: str, new_name: str, case_sensitive: bool = False) -> int:
|
|
343
|
+
"""
|
|
344
|
+
Rename all labels for a net.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
old_name: Current net name
|
|
348
|
+
new_name: New net name
|
|
349
|
+
case_sensitive: Whether search should be case sensitive
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Number of labels renamed
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
ValueError: If new name is empty
|
|
356
|
+
"""
|
|
357
|
+
if not new_name.strip():
|
|
358
|
+
raise ValueError("New net name cannot be empty")
|
|
359
|
+
|
|
360
|
+
labels_to_rename = self.get_labels_by_text(old_name, case_sensitive)
|
|
361
|
+
|
|
362
|
+
for label in labels_to_rename:
|
|
363
|
+
label.text = new_name
|
|
364
|
+
|
|
365
|
+
if labels_to_rename:
|
|
366
|
+
self._mark_modified()
|
|
367
|
+
self._mark_indexes_dirty()
|
|
368
|
+
|
|
369
|
+
logger.info(f"Renamed {len(labels_to_rename)} labels from '{old_name}' to '{new_name}'")
|
|
370
|
+
return len(labels_to_rename)
|
|
371
|
+
|
|
372
|
+
def remove_labels_for_net(self, net_name: str, case_sensitive: bool = False) -> int:
|
|
373
|
+
"""
|
|
374
|
+
Remove all labels for a specific net.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
net_name: Net name to remove labels for
|
|
378
|
+
case_sensitive: Whether search should be case sensitive
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Number of labels removed
|
|
382
|
+
"""
|
|
383
|
+
labels_to_remove = self.get_labels_by_text(net_name, case_sensitive)
|
|
384
|
+
|
|
385
|
+
for label in labels_to_remove:
|
|
386
|
+
self.remove(label.uuid)
|
|
387
|
+
|
|
388
|
+
logger.info(f"Removed {len(labels_to_remove)} labels for net '{net_name}'")
|
|
389
|
+
return len(labels_to_remove)
|
|
390
|
+
|
|
391
|
+
# Collection statistics
|
|
392
|
+
def get_label_statistics(self) -> Dict[str, Any]:
|
|
393
|
+
"""
|
|
394
|
+
Get label statistics for the collection.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Dictionary with label statistics
|
|
398
|
+
"""
|
|
399
|
+
stats = super().get_statistics()
|
|
400
|
+
|
|
401
|
+
# Add label-specific statistics
|
|
402
|
+
stats.update({
|
|
403
|
+
"unique_texts": len(self._text_index),
|
|
404
|
+
"unique_positions": len(self._position_index),
|
|
405
|
+
"label_types": {
|
|
406
|
+
label_type: len(labels)
|
|
407
|
+
for label_type, labels in self._type_index.items()
|
|
408
|
+
},
|
|
409
|
+
"net_count": len(self.get_net_names())
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
return stats
|