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.

Files changed (39) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/collections/__init__.py +21 -0
  3. kicad_sch_api/collections/base.py +296 -0
  4. kicad_sch_api/collections/components.py +422 -0
  5. kicad_sch_api/collections/junctions.py +378 -0
  6. kicad_sch_api/collections/labels.py +412 -0
  7. kicad_sch_api/collections/wires.py +407 -0
  8. kicad_sch_api/core/formatter.py +31 -0
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/nets.py +310 -0
  11. kicad_sch_api/core/no_connects.py +274 -0
  12. kicad_sch_api/core/parser.py +72 -0
  13. kicad_sch_api/core/schematic.py +185 -9
  14. kicad_sch_api/core/texts.py +343 -0
  15. kicad_sch_api/core/types.py +26 -0
  16. kicad_sch_api/geometry/__init__.py +26 -0
  17. kicad_sch_api/geometry/font_metrics.py +20 -0
  18. kicad_sch_api/geometry/symbol_bbox.py +589 -0
  19. kicad_sch_api/interfaces/__init__.py +17 -0
  20. kicad_sch_api/interfaces/parser.py +76 -0
  21. kicad_sch_api/interfaces/repository.py +70 -0
  22. kicad_sch_api/interfaces/resolver.py +117 -0
  23. kicad_sch_api/parsers/__init__.py +14 -0
  24. kicad_sch_api/parsers/base.py +148 -0
  25. kicad_sch_api/parsers/label_parser.py +254 -0
  26. kicad_sch_api/parsers/registry.py +153 -0
  27. kicad_sch_api/parsers/symbol_parser.py +227 -0
  28. kicad_sch_api/parsers/wire_parser.py +99 -0
  29. kicad_sch_api/symbols/__init__.py +18 -0
  30. kicad_sch_api/symbols/cache.py +470 -0
  31. kicad_sch_api/symbols/resolver.py +367 -0
  32. kicad_sch_api/symbols/validators.py +453 -0
  33. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  34. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  35. kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
  36. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  37. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  38. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  39. {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
@@ -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)