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,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Text element management for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
This module provides collection classes for managing text elements,
|
|
5
|
+
featuring fast lookup, bulk operations, and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
13
|
+
from .types import Point, Text
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextElement:
|
|
19
|
+
"""
|
|
20
|
+
Enhanced wrapper for schematic text elements with modern API.
|
|
21
|
+
|
|
22
|
+
Provides intuitive access to text properties and operations
|
|
23
|
+
while maintaining exact format preservation.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, text_data: Text, parent_collection: "TextCollection"):
|
|
27
|
+
"""
|
|
28
|
+
Initialize text element wrapper.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
text_data: Underlying text data
|
|
32
|
+
parent_collection: Parent collection for updates
|
|
33
|
+
"""
|
|
34
|
+
self._data = text_data
|
|
35
|
+
self._collection = parent_collection
|
|
36
|
+
self._validator = SchematicValidator()
|
|
37
|
+
|
|
38
|
+
# Core properties with validation
|
|
39
|
+
@property
|
|
40
|
+
def uuid(self) -> str:
|
|
41
|
+
"""Text element UUID."""
|
|
42
|
+
return self._data.uuid
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def text(self) -> str:
|
|
46
|
+
"""Text content."""
|
|
47
|
+
return self._data.text
|
|
48
|
+
|
|
49
|
+
@text.setter
|
|
50
|
+
def text(self, value: str):
|
|
51
|
+
"""Set text content with validation."""
|
|
52
|
+
if not isinstance(value, str):
|
|
53
|
+
raise ValidationError(f"Text content must be string, got {type(value)}")
|
|
54
|
+
self._data.text = value
|
|
55
|
+
self._collection._mark_modified()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def position(self) -> Point:
|
|
59
|
+
"""Text position."""
|
|
60
|
+
return self._data.position
|
|
61
|
+
|
|
62
|
+
@position.setter
|
|
63
|
+
def position(self, value: Union[Point, Tuple[float, float]]):
|
|
64
|
+
"""Set text position."""
|
|
65
|
+
if isinstance(value, tuple):
|
|
66
|
+
value = Point(value[0], value[1])
|
|
67
|
+
elif not isinstance(value, Point):
|
|
68
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
|
|
69
|
+
self._data.position = value
|
|
70
|
+
self._collection._mark_modified()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def rotation(self) -> float:
|
|
74
|
+
"""Text rotation in degrees."""
|
|
75
|
+
return self._data.rotation
|
|
76
|
+
|
|
77
|
+
@rotation.setter
|
|
78
|
+
def rotation(self, value: float):
|
|
79
|
+
"""Set text rotation."""
|
|
80
|
+
self._data.rotation = float(value)
|
|
81
|
+
self._collection._mark_modified()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def size(self) -> float:
|
|
85
|
+
"""Text size."""
|
|
86
|
+
return self._data.size
|
|
87
|
+
|
|
88
|
+
@size.setter
|
|
89
|
+
def size(self, value: float):
|
|
90
|
+
"""Set text size with validation."""
|
|
91
|
+
if value <= 0:
|
|
92
|
+
raise ValidationError(f"Text size must be positive, got {value}")
|
|
93
|
+
self._data.size = float(value)
|
|
94
|
+
self._collection._mark_modified()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def exclude_from_sim(self) -> bool:
|
|
98
|
+
"""Whether text is excluded from simulation."""
|
|
99
|
+
return self._data.exclude_from_sim
|
|
100
|
+
|
|
101
|
+
@exclude_from_sim.setter
|
|
102
|
+
def exclude_from_sim(self, value: bool):
|
|
103
|
+
"""Set exclude from simulation flag."""
|
|
104
|
+
self._data.exclude_from_sim = bool(value)
|
|
105
|
+
self._collection._mark_modified()
|
|
106
|
+
|
|
107
|
+
def validate(self) -> List[ValidationIssue]:
|
|
108
|
+
"""Validate this text element."""
|
|
109
|
+
return self._validator.validate_text(self._data.__dict__)
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
112
|
+
"""Convert text element to dictionary representation."""
|
|
113
|
+
return {
|
|
114
|
+
"uuid": self.uuid,
|
|
115
|
+
"text": self.text,
|
|
116
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
117
|
+
"rotation": self.rotation,
|
|
118
|
+
"size": self.size,
|
|
119
|
+
"exclude_from_sim": self.exclude_from_sim,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
"""String representation."""
|
|
124
|
+
return f"<Text '{self.text}' @ {self.position}>"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TextCollection:
|
|
128
|
+
"""
|
|
129
|
+
Collection class for efficient text element management.
|
|
130
|
+
|
|
131
|
+
Provides fast lookup, filtering, and bulk operations for schematic text elements.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, texts: List[Text] = None):
|
|
135
|
+
"""
|
|
136
|
+
Initialize text collection.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
texts: Initial list of text data
|
|
140
|
+
"""
|
|
141
|
+
self._texts: List[TextElement] = []
|
|
142
|
+
self._uuid_index: Dict[str, TextElement] = {}
|
|
143
|
+
self._content_index: Dict[str, List[TextElement]] = {}
|
|
144
|
+
self._modified = False
|
|
145
|
+
|
|
146
|
+
# Add initial texts
|
|
147
|
+
if texts:
|
|
148
|
+
for text_data in texts:
|
|
149
|
+
self._add_to_indexes(TextElement(text_data, self))
|
|
150
|
+
|
|
151
|
+
logger.debug(f"TextCollection initialized with {len(self._texts)} texts")
|
|
152
|
+
|
|
153
|
+
def add(
|
|
154
|
+
self,
|
|
155
|
+
text: str,
|
|
156
|
+
position: Union[Point, Tuple[float, float]],
|
|
157
|
+
rotation: float = 0.0,
|
|
158
|
+
size: float = 1.27,
|
|
159
|
+
exclude_from_sim: bool = False,
|
|
160
|
+
text_uuid: Optional[str] = None,
|
|
161
|
+
) -> TextElement:
|
|
162
|
+
"""
|
|
163
|
+
Add a new text element to the schematic.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
text: Text content
|
|
167
|
+
position: Text position
|
|
168
|
+
rotation: Text rotation in degrees
|
|
169
|
+
size: Text size
|
|
170
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
171
|
+
text_uuid: Specific UUID for text (auto-generated if None)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Newly created TextElement
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValidationError: If text data is invalid
|
|
178
|
+
"""
|
|
179
|
+
# Validate inputs
|
|
180
|
+
if not isinstance(text, str) or not text.strip():
|
|
181
|
+
raise ValidationError("Text content cannot be empty")
|
|
182
|
+
|
|
183
|
+
if isinstance(position, tuple):
|
|
184
|
+
position = Point(position[0], position[1])
|
|
185
|
+
elif not isinstance(position, Point):
|
|
186
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
|
|
187
|
+
|
|
188
|
+
if size <= 0:
|
|
189
|
+
raise ValidationError(f"Text size must be positive, got {size}")
|
|
190
|
+
|
|
191
|
+
# Generate UUID if not provided
|
|
192
|
+
if not text_uuid:
|
|
193
|
+
text_uuid = str(uuid.uuid4())
|
|
194
|
+
|
|
195
|
+
# Check for duplicate UUID
|
|
196
|
+
if text_uuid in self._uuid_index:
|
|
197
|
+
raise ValidationError(f"Text UUID {text_uuid} already exists")
|
|
198
|
+
|
|
199
|
+
# Create text data
|
|
200
|
+
text_data = Text(
|
|
201
|
+
uuid=text_uuid,
|
|
202
|
+
position=position,
|
|
203
|
+
text=text,
|
|
204
|
+
rotation=rotation,
|
|
205
|
+
size=size,
|
|
206
|
+
exclude_from_sim=exclude_from_sim,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Create wrapper and add to collection
|
|
210
|
+
text_element = TextElement(text_data, self)
|
|
211
|
+
self._add_to_indexes(text_element)
|
|
212
|
+
self._mark_modified()
|
|
213
|
+
|
|
214
|
+
logger.debug(f"Added text: {text_element}")
|
|
215
|
+
return text_element
|
|
216
|
+
|
|
217
|
+
def get(self, text_uuid: str) -> Optional[TextElement]:
|
|
218
|
+
"""Get text by UUID."""
|
|
219
|
+
return self._uuid_index.get(text_uuid)
|
|
220
|
+
|
|
221
|
+
def remove(self, text_uuid: str) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Remove text by UUID.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
text_uuid: UUID of text to remove
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
True if text was removed, False if not found
|
|
230
|
+
"""
|
|
231
|
+
text_element = self._uuid_index.get(text_uuid)
|
|
232
|
+
if not text_element:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
# Remove from indexes
|
|
236
|
+
self._remove_from_indexes(text_element)
|
|
237
|
+
self._mark_modified()
|
|
238
|
+
|
|
239
|
+
logger.debug(f"Removed text: {text_element}")
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
def find_by_content(self, content: str, exact: bool = True) -> List[TextElement]:
|
|
243
|
+
"""
|
|
244
|
+
Find texts by content.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
content: Content to search for
|
|
248
|
+
exact: If True, exact match; if False, substring match
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of matching text elements
|
|
252
|
+
"""
|
|
253
|
+
if exact:
|
|
254
|
+
return self._content_index.get(content, []).copy()
|
|
255
|
+
else:
|
|
256
|
+
matches = []
|
|
257
|
+
for text_element in self._texts:
|
|
258
|
+
if content.lower() in text_element.text.lower():
|
|
259
|
+
matches.append(text_element)
|
|
260
|
+
return matches
|
|
261
|
+
|
|
262
|
+
def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
|
|
263
|
+
"""
|
|
264
|
+
Filter texts by predicate function.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
predicate: Function that returns True for texts to include
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of texts matching predicate
|
|
271
|
+
"""
|
|
272
|
+
return [text for text in self._texts if predicate(text)]
|
|
273
|
+
|
|
274
|
+
def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
|
|
275
|
+
"""
|
|
276
|
+
Update multiple texts matching criteria.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
criteria: Function to select texts to update
|
|
280
|
+
updates: Dictionary of property updates
|
|
281
|
+
"""
|
|
282
|
+
updated_count = 0
|
|
283
|
+
for text_element in self._texts:
|
|
284
|
+
if criteria(text_element):
|
|
285
|
+
for prop, value in updates.items():
|
|
286
|
+
if hasattr(text_element, prop):
|
|
287
|
+
setattr(text_element, prop, value)
|
|
288
|
+
updated_count += 1
|
|
289
|
+
|
|
290
|
+
if updated_count > 0:
|
|
291
|
+
self._mark_modified()
|
|
292
|
+
logger.debug(f"Bulk updated {updated_count} text properties")
|
|
293
|
+
|
|
294
|
+
def clear(self):
|
|
295
|
+
"""Remove all texts from collection."""
|
|
296
|
+
self._texts.clear()
|
|
297
|
+
self._uuid_index.clear()
|
|
298
|
+
self._content_index.clear()
|
|
299
|
+
self._mark_modified()
|
|
300
|
+
|
|
301
|
+
def _add_to_indexes(self, text_element: TextElement):
|
|
302
|
+
"""Add text to internal indexes."""
|
|
303
|
+
self._texts.append(text_element)
|
|
304
|
+
self._uuid_index[text_element.uuid] = text_element
|
|
305
|
+
|
|
306
|
+
# Add to content index
|
|
307
|
+
content = text_element.text
|
|
308
|
+
if content not in self._content_index:
|
|
309
|
+
self._content_index[content] = []
|
|
310
|
+
self._content_index[content].append(text_element)
|
|
311
|
+
|
|
312
|
+
def _remove_from_indexes(self, text_element: TextElement):
|
|
313
|
+
"""Remove text from internal indexes."""
|
|
314
|
+
self._texts.remove(text_element)
|
|
315
|
+
del self._uuid_index[text_element.uuid]
|
|
316
|
+
|
|
317
|
+
# Remove from content index
|
|
318
|
+
content = text_element.text
|
|
319
|
+
if content in self._content_index:
|
|
320
|
+
self._content_index[content].remove(text_element)
|
|
321
|
+
if not self._content_index[content]:
|
|
322
|
+
del self._content_index[content]
|
|
323
|
+
|
|
324
|
+
def _mark_modified(self):
|
|
325
|
+
"""Mark collection as modified."""
|
|
326
|
+
self._modified = True
|
|
327
|
+
|
|
328
|
+
# Collection interface methods
|
|
329
|
+
def __len__(self) -> int:
|
|
330
|
+
"""Return number of texts."""
|
|
331
|
+
return len(self._texts)
|
|
332
|
+
|
|
333
|
+
def __iter__(self) -> Iterator[TextElement]:
|
|
334
|
+
"""Iterate over texts."""
|
|
335
|
+
return iter(self._texts)
|
|
336
|
+
|
|
337
|
+
def __getitem__(self, index: int) -> TextElement:
|
|
338
|
+
"""Get text by index."""
|
|
339
|
+
return self._texts[index]
|
|
340
|
+
|
|
341
|
+
def __bool__(self) -> bool:
|
|
342
|
+
"""Return True if collection has texts."""
|
|
343
|
+
return len(self._texts) > 0
|
kicad_sch_api/core/types.py
CHANGED
|
@@ -372,6 +372,32 @@ class SchematicRectangle:
|
|
|
372
372
|
)
|
|
373
373
|
|
|
374
374
|
|
|
375
|
+
@dataclass
|
|
376
|
+
class Image:
|
|
377
|
+
"""Image element in schematic."""
|
|
378
|
+
|
|
379
|
+
uuid: str
|
|
380
|
+
position: Point
|
|
381
|
+
data: str # Base64-encoded image data
|
|
382
|
+
scale: float = 1.0
|
|
383
|
+
|
|
384
|
+
def __post_init__(self):
|
|
385
|
+
if not self.uuid:
|
|
386
|
+
self.uuid = str(uuid4())
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@dataclass
|
|
390
|
+
class NoConnect:
|
|
391
|
+
"""No-connect symbol in schematic."""
|
|
392
|
+
|
|
393
|
+
uuid: str
|
|
394
|
+
position: Point
|
|
395
|
+
|
|
396
|
+
def __post_init__(self):
|
|
397
|
+
if not self.uuid:
|
|
398
|
+
self.uuid = str(uuid4())
|
|
399
|
+
|
|
400
|
+
|
|
375
401
|
@dataclass
|
|
376
402
|
class Net:
|
|
377
403
|
"""Electrical net connecting components."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geometry module for KiCad schematic symbol bounding box calculations.
|
|
3
|
+
|
|
4
|
+
This module provides accurate bounding box calculations for KiCad symbols,
|
|
5
|
+
including font metrics and symbol geometry analysis.
|
|
6
|
+
|
|
7
|
+
Migrated from circuit-synth to kicad-sch-api for better architectural separation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .font_metrics import (
|
|
11
|
+
DEFAULT_PIN_LENGTH,
|
|
12
|
+
DEFAULT_PIN_NAME_OFFSET,
|
|
13
|
+
DEFAULT_PIN_NUMBER_SIZE,
|
|
14
|
+
DEFAULT_PIN_TEXT_WIDTH_RATIO,
|
|
15
|
+
DEFAULT_TEXT_HEIGHT,
|
|
16
|
+
)
|
|
17
|
+
from .symbol_bbox import SymbolBoundingBoxCalculator
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SymbolBoundingBoxCalculator",
|
|
21
|
+
"DEFAULT_TEXT_HEIGHT",
|
|
22
|
+
"DEFAULT_PIN_LENGTH",
|
|
23
|
+
"DEFAULT_PIN_NAME_OFFSET",
|
|
24
|
+
"DEFAULT_PIN_NUMBER_SIZE",
|
|
25
|
+
"DEFAULT_PIN_TEXT_WIDTH_RATIO",
|
|
26
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Font metrics and text rendering constants for KiCad schematic text.
|
|
3
|
+
|
|
4
|
+
These constants are used for accurate text bounding box calculations
|
|
5
|
+
and symbol spacing in schematic layouts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# KiCad default text size in mm
|
|
9
|
+
# Increased to better match actual KiCad rendering
|
|
10
|
+
DEFAULT_TEXT_HEIGHT = 2.54 # 100 mils (doubled from 50 mils)
|
|
11
|
+
|
|
12
|
+
# Default pin dimensions
|
|
13
|
+
DEFAULT_PIN_LENGTH = 2.54 # 100 mils
|
|
14
|
+
DEFAULT_PIN_NAME_OFFSET = 0.508 # 20 mils - offset from pin endpoint to label text
|
|
15
|
+
DEFAULT_PIN_NUMBER_SIZE = 1.27 # 50 mils
|
|
16
|
+
|
|
17
|
+
# Text width ratio for proportional font rendering
|
|
18
|
+
# KiCad uses proportional fonts where average character width is ~0.65x height
|
|
19
|
+
# This prevents label text from extending beyond calculated bounding boxes
|
|
20
|
+
DEFAULT_PIN_TEXT_WIDTH_RATIO = 0.65 # Width to height ratio for pin text (proportional font average)
|