kicad-sch-api 0.3.5__py3-none-any.whl → 0.4.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 +2 -2
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +2 -2
- kicad_sch_api/collections/base.py +5 -7
- kicad_sch_api/collections/components.py +24 -12
- kicad_sch_api/collections/junctions.py +31 -43
- kicad_sch_api/collections/labels.py +19 -27
- kicad_sch_api/collections/wires.py +17 -18
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +5 -0
- kicad_sch_api/core/components.py +67 -45
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +29 -53
- kicad_sch_api/core/managers/__init__.py +26 -0
- kicad_sch_api/core/managers/file_io.py +244 -0
- kicad_sch_api/core/managers/format_sync.py +501 -0
- kicad_sch_api/core/managers/graphics.py +579 -0
- kicad_sch_api/core/managers/metadata.py +269 -0
- kicad_sch_api/core/managers/sheet.py +454 -0
- kicad_sch_api/core/managers/text_elements.py +536 -0
- kicad_sch_api/core/managers/validation.py +475 -0
- kicad_sch_api/core/managers/wire.py +352 -0
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +33 -55
- kicad_sch_api/core/parser.py +75 -1731
- kicad_sch_api/core/schematic.py +951 -1192
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +60 -22
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +40 -21
- kicad_sch_api/interfaces/__init__.py +1 -1
- kicad_sch_api/interfaces/parser.py +1 -1
- kicad_sch_api/interfaces/repository.py +1 -1
- kicad_sch_api/interfaces/resolver.py +1 -1
- kicad_sch_api/parsers/__init__.py +2 -2
- kicad_sch_api/parsers/base.py +7 -10
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +194 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/registry.py +4 -2
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +1 -1
- kicad_sch_api/symbols/cache.py +9 -12
- kicad_sch_api/symbols/resolver.py +20 -26
- kicad_sch_api/symbols/validators.py +188 -137
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.1.dist-info/RECORD +87 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -227
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/texts.py
CHANGED
|
@@ -10,6 +10,7 @@ import uuid
|
|
|
10
10
|
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
12
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
13
|
+
from .collections import BaseCollection
|
|
13
14
|
from .types import Point, Text
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -124,10 +125,13 @@ class TextElement:
|
|
|
124
125
|
return f"<Text '{self.text}' @ {self.position}>"
|
|
125
126
|
|
|
126
127
|
|
|
127
|
-
class TextCollection:
|
|
128
|
+
class TextCollection(BaseCollection[TextElement]):
|
|
128
129
|
"""
|
|
129
130
|
Collection class for efficient text element management.
|
|
130
131
|
|
|
132
|
+
Inherits from BaseCollection for standard operations and adds text-specific
|
|
133
|
+
functionality including content-based indexing.
|
|
134
|
+
|
|
131
135
|
Provides fast lookup, filtering, and bulk operations for schematic text elements.
|
|
132
136
|
"""
|
|
133
137
|
|
|
@@ -138,18 +142,17 @@ class TextCollection:
|
|
|
138
142
|
Args:
|
|
139
143
|
texts: Initial list of text data
|
|
140
144
|
"""
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
# Initialize base collection with empty list (we'll add elements below)
|
|
146
|
+
super().__init__([], collection_name="texts")
|
|
147
|
+
|
|
148
|
+
# Additional text-specific index
|
|
143
149
|
self._content_index: Dict[str, List[TextElement]] = {}
|
|
144
|
-
self._modified = False
|
|
145
150
|
|
|
146
151
|
# Add initial texts
|
|
147
152
|
if texts:
|
|
148
153
|
for text_data in texts:
|
|
149
154
|
self._add_to_indexes(TextElement(text_data, self))
|
|
150
155
|
|
|
151
|
-
logger.debug(f"TextCollection initialized with {len(self._texts)} texts")
|
|
152
|
-
|
|
153
156
|
def add(
|
|
154
157
|
self,
|
|
155
158
|
text: str,
|
|
@@ -209,15 +212,10 @@ class TextCollection:
|
|
|
209
212
|
# Create wrapper and add to collection
|
|
210
213
|
text_element = TextElement(text_data, self)
|
|
211
214
|
self._add_to_indexes(text_element)
|
|
212
|
-
self._mark_modified()
|
|
213
215
|
|
|
214
216
|
logger.debug(f"Added text: {text_element}")
|
|
215
217
|
return text_element
|
|
216
218
|
|
|
217
|
-
def get(self, text_uuid: str) -> Optional[TextElement]:
|
|
218
|
-
"""Get text by UUID."""
|
|
219
|
-
return self._uuid_index.get(text_uuid)
|
|
220
|
-
|
|
221
219
|
def remove(self, text_uuid: str) -> bool:
|
|
222
220
|
"""
|
|
223
221
|
Remove text by UUID.
|
|
@@ -228,13 +226,19 @@ class TextCollection:
|
|
|
228
226
|
Returns:
|
|
229
227
|
True if text was removed, False if not found
|
|
230
228
|
"""
|
|
231
|
-
text_element = self.
|
|
229
|
+
text_element = self.get(text_uuid)
|
|
232
230
|
if not text_element:
|
|
233
231
|
return False
|
|
234
232
|
|
|
235
|
-
# Remove from
|
|
236
|
-
|
|
237
|
-
self.
|
|
233
|
+
# Remove from content index
|
|
234
|
+
content = text_element.text
|
|
235
|
+
if content in self._content_index:
|
|
236
|
+
self._content_index[content].remove(text_element)
|
|
237
|
+
if not self._content_index[content]:
|
|
238
|
+
del self._content_index[content]
|
|
239
|
+
|
|
240
|
+
# Remove using base class method
|
|
241
|
+
super().remove(text_uuid)
|
|
238
242
|
|
|
239
243
|
logger.debug(f"Removed text: {text_element}")
|
|
240
244
|
return True
|
|
@@ -254,14 +258,14 @@ class TextCollection:
|
|
|
254
258
|
return self._content_index.get(content, []).copy()
|
|
255
259
|
else:
|
|
256
260
|
matches = []
|
|
257
|
-
for text_element in self.
|
|
261
|
+
for text_element in self._items:
|
|
258
262
|
if content.lower() in text_element.text.lower():
|
|
259
263
|
matches.append(text_element)
|
|
260
264
|
return matches
|
|
261
265
|
|
|
262
266
|
def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
|
|
263
267
|
"""
|
|
264
|
-
Filter texts by predicate function.
|
|
268
|
+
Filter texts by predicate function (delegates to base class find).
|
|
265
269
|
|
|
266
270
|
Args:
|
|
267
271
|
predicate: Function that returns True for texts to include
|
|
@@ -269,7 +273,7 @@ class TextCollection:
|
|
|
269
273
|
Returns:
|
|
270
274
|
List of texts matching predicate
|
|
271
275
|
"""
|
|
272
|
-
return
|
|
276
|
+
return self.find(predicate)
|
|
273
277
|
|
|
274
278
|
def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
|
|
275
279
|
"""
|
|
@@ -280,7 +284,7 @@ class TextCollection:
|
|
|
280
284
|
updates: Dictionary of property updates
|
|
281
285
|
"""
|
|
282
286
|
updated_count = 0
|
|
283
|
-
for text_element in self.
|
|
287
|
+
for text_element in self._items:
|
|
284
288
|
if criteria(text_element):
|
|
285
289
|
for prop, value in updates.items():
|
|
286
290
|
if hasattr(text_element, prop):
|
|
@@ -293,15 +297,12 @@ class TextCollection:
|
|
|
293
297
|
|
|
294
298
|
def clear(self):
|
|
295
299
|
"""Remove all texts from collection."""
|
|
296
|
-
self._texts.clear()
|
|
297
|
-
self._uuid_index.clear()
|
|
298
300
|
self._content_index.clear()
|
|
299
|
-
|
|
301
|
+
super().clear()
|
|
300
302
|
|
|
301
303
|
def _add_to_indexes(self, text_element: TextElement):
|
|
302
|
-
"""Add text to internal indexes."""
|
|
303
|
-
self.
|
|
304
|
-
self._uuid_index[text_element.uuid] = text_element
|
|
304
|
+
"""Add text to internal indexes (base + content index)."""
|
|
305
|
+
self._add_item(text_element)
|
|
305
306
|
|
|
306
307
|
# Add to content index
|
|
307
308
|
content = text_element.text
|
|
@@ -309,35 +310,7 @@ class TextCollection:
|
|
|
309
310
|
self._content_index[content] = []
|
|
310
311
|
self._content_index[content].append(text_element)
|
|
311
312
|
|
|
312
|
-
|
|
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
|
-
|
|
313
|
+
# Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
|
|
341
314
|
def __bool__(self) -> bool:
|
|
342
315
|
"""Return True if collection has texts."""
|
|
343
|
-
return len(self.
|
|
316
|
+
return len(self._items) > 0
|
kicad_sch_api/core/types.py
CHANGED
|
@@ -18,14 +18,14 @@ class Point:
|
|
|
18
18
|
x: float
|
|
19
19
|
y: float
|
|
20
20
|
|
|
21
|
-
def __post_init__(self):
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
22
|
# Ensure coordinates are float
|
|
23
23
|
object.__setattr__(self, "x", float(self.x))
|
|
24
24
|
object.__setattr__(self, "y", float(self.y))
|
|
25
25
|
|
|
26
26
|
def distance_to(self, other: "Point") -> float:
|
|
27
27
|
"""Calculate distance to another point."""
|
|
28
|
-
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
|
|
28
|
+
return float(((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5)
|
|
29
29
|
|
|
30
30
|
def offset(self, dx: float, dy: float) -> "Point":
|
|
31
31
|
"""Create new point offset by dx, dy."""
|
|
@@ -35,6 +35,43 @@ class Point:
|
|
|
35
35
|
return f"({self.x:.3f}, {self.y:.3f})"
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def point_from_dict_or_tuple(
|
|
39
|
+
position: Union[Point, Dict[str, float], Tuple[float, float], List[float], Any]
|
|
40
|
+
) -> Point:
|
|
41
|
+
"""
|
|
42
|
+
Convert various position formats to a Point object.
|
|
43
|
+
|
|
44
|
+
Supports multiple input formats for maximum flexibility:
|
|
45
|
+
- Point: Returns as-is
|
|
46
|
+
- Dict with 'x' and 'y' keys: Extracts and creates Point
|
|
47
|
+
- Tuple/List with 2 elements: Creates Point from coordinates
|
|
48
|
+
- Other: Returns as-is (assumes it's already a Point-like object)
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
position: Position in any supported format
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Point object
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> point_from_dict_or_tuple({"x": 10, "y": 20})
|
|
58
|
+
Point(x=10.0, y=20.0)
|
|
59
|
+
>>> point_from_dict_or_tuple((10, 20))
|
|
60
|
+
Point(x=10.0, y=20.0)
|
|
61
|
+
>>> point_from_dict_or_tuple(Point(10, 20))
|
|
62
|
+
Point(x=10.0, y=20.0)
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(position, Point):
|
|
65
|
+
return position
|
|
66
|
+
elif isinstance(position, dict):
|
|
67
|
+
return Point(position.get("x", 0), position.get("y", 0))
|
|
68
|
+
elif isinstance(position, (list, tuple)) and len(position) >= 2:
|
|
69
|
+
return Point(position[0], position[1])
|
|
70
|
+
else:
|
|
71
|
+
# Assume it's already a Point-like object or will be handled by caller
|
|
72
|
+
return position
|
|
73
|
+
|
|
74
|
+
|
|
38
75
|
@dataclass(frozen=True)
|
|
39
76
|
class Rectangle:
|
|
40
77
|
"""Rectangle defined by two corner points."""
|
|
@@ -110,7 +147,7 @@ class SchematicPin:
|
|
|
110
147
|
length: float = 2.54 # Standard pin length in mm
|
|
111
148
|
rotation: float = 0.0 # Rotation in degrees
|
|
112
149
|
|
|
113
|
-
def __post_init__(self):
|
|
150
|
+
def __post_init__(self) -> None:
|
|
114
151
|
# Ensure types are correct
|
|
115
152
|
self.pin_type = PinType(self.pin_type) if isinstance(self.pin_type, str) else self.pin_type
|
|
116
153
|
self.pin_shape = (
|
|
@@ -135,7 +172,7 @@ class SchematicSymbol:
|
|
|
135
172
|
on_board: bool = True
|
|
136
173
|
unit: int = 1
|
|
137
174
|
|
|
138
|
-
def __post_init__(self):
|
|
175
|
+
def __post_init__(self) -> None:
|
|
139
176
|
# Generate UUID if not provided
|
|
140
177
|
if not self.uuid:
|
|
141
178
|
self.uuid = str(uuid4())
|
|
@@ -163,6 +200,10 @@ class SchematicSymbol:
|
|
|
163
200
|
if not pin:
|
|
164
201
|
return None
|
|
165
202
|
# TODO: Apply rotation and symbol position transformation
|
|
203
|
+
# NOTE: Currently assumes 0° rotation. For rotated components, pin positions
|
|
204
|
+
# would need to be transformed using rotation matrix before adding to component position.
|
|
205
|
+
# This affects pin-to-pin wiring accuracy for rotated components.
|
|
206
|
+
# Priority: MEDIUM - Would improve wiring accuracy for rotated components
|
|
166
207
|
return Point(self.position.x + pin.position.x, self.position.y + pin.position.y)
|
|
167
208
|
|
|
168
209
|
|
|
@@ -183,7 +224,7 @@ class Wire:
|
|
|
183
224
|
stroke_width: float = 0.0
|
|
184
225
|
stroke_type: str = "default"
|
|
185
226
|
|
|
186
|
-
def __post_init__(self):
|
|
227
|
+
def __post_init__(self) -> None:
|
|
187
228
|
if not self.uuid:
|
|
188
229
|
self.uuid = str(uuid4())
|
|
189
230
|
|
|
@@ -196,7 +237,7 @@ class Wire:
|
|
|
196
237
|
raise ValueError("Wire must have at least 2 points")
|
|
197
238
|
|
|
198
239
|
@classmethod
|
|
199
|
-
def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs) -> "Wire":
|
|
240
|
+
def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs: Any) -> "Wire":
|
|
200
241
|
"""Create wire from start and end points (convenience method)."""
|
|
201
242
|
return cls(uuid=uuid, points=[start, end], **kwargs)
|
|
202
243
|
|
|
@@ -244,7 +285,7 @@ class Junction:
|
|
|
244
285
|
diameter: float = 0 # KiCAD default diameter
|
|
245
286
|
color: Tuple[int, int, int, int] = (0, 0, 0, 0) # RGBA color
|
|
246
287
|
|
|
247
|
-
def __post_init__(self):
|
|
288
|
+
def __post_init__(self) -> None:
|
|
248
289
|
if not self.uuid:
|
|
249
290
|
self.uuid = str(uuid4())
|
|
250
291
|
|
|
@@ -280,7 +321,7 @@ class Label:
|
|
|
280
321
|
size: float = 1.27
|
|
281
322
|
shape: Optional[HierarchicalLabelShape] = None # Only for hierarchical labels
|
|
282
323
|
|
|
283
|
-
def __post_init__(self):
|
|
324
|
+
def __post_init__(self) -> None:
|
|
284
325
|
if not self.uuid:
|
|
285
326
|
self.uuid = str(uuid4())
|
|
286
327
|
|
|
@@ -305,7 +346,7 @@ class Text:
|
|
|
305
346
|
size: float = 1.27
|
|
306
347
|
exclude_from_sim: bool = False
|
|
307
348
|
|
|
308
|
-
def __post_init__(self):
|
|
349
|
+
def __post_init__(self) -> None:
|
|
309
350
|
if not self.uuid:
|
|
310
351
|
self.uuid = str(uuid4())
|
|
311
352
|
|
|
@@ -333,7 +374,7 @@ class TextBox:
|
|
|
333
374
|
justify_vertical: str = "top"
|
|
334
375
|
exclude_from_sim: bool = False
|
|
335
376
|
|
|
336
|
-
def __post_init__(self):
|
|
377
|
+
def __post_init__(self) -> None:
|
|
337
378
|
if not self.uuid:
|
|
338
379
|
self.uuid = str(uuid4())
|
|
339
380
|
|
|
@@ -349,7 +390,7 @@ class SchematicRectangle:
|
|
|
349
390
|
stroke_type: str = "default"
|
|
350
391
|
fill_type: str = "none"
|
|
351
392
|
|
|
352
|
-
def __post_init__(self):
|
|
393
|
+
def __post_init__(self) -> None:
|
|
353
394
|
if not self.uuid:
|
|
354
395
|
self.uuid = str(uuid4())
|
|
355
396
|
|
|
@@ -366,10 +407,7 @@ class SchematicRectangle:
|
|
|
366
407
|
@property
|
|
367
408
|
def center(self) -> Point:
|
|
368
409
|
"""Rectangle center point."""
|
|
369
|
-
return Point(
|
|
370
|
-
(self.start.x + self.end.x) / 2,
|
|
371
|
-
(self.start.y + self.end.y) / 2
|
|
372
|
-
)
|
|
410
|
+
return Point((self.start.x + self.end.x) / 2, (self.start.y + self.end.y) / 2)
|
|
373
411
|
|
|
374
412
|
|
|
375
413
|
@dataclass
|
|
@@ -381,7 +419,7 @@ class Image:
|
|
|
381
419
|
data: str # Base64-encoded image data
|
|
382
420
|
scale: float = 1.0
|
|
383
421
|
|
|
384
|
-
def __post_init__(self):
|
|
422
|
+
def __post_init__(self) -> None:
|
|
385
423
|
if not self.uuid:
|
|
386
424
|
self.uuid = str(uuid4())
|
|
387
425
|
|
|
@@ -393,7 +431,7 @@ class NoConnect:
|
|
|
393
431
|
uuid: str
|
|
394
432
|
position: Point
|
|
395
433
|
|
|
396
|
-
def __post_init__(self):
|
|
434
|
+
def __post_init__(self) -> None:
|
|
397
435
|
if not self.uuid:
|
|
398
436
|
self.uuid = str(uuid4())
|
|
399
437
|
|
|
@@ -407,13 +445,13 @@ class Net:
|
|
|
407
445
|
wires: List[str] = field(default_factory=list) # Wire UUIDs
|
|
408
446
|
labels: List[str] = field(default_factory=list) # Label UUIDs
|
|
409
447
|
|
|
410
|
-
def add_connection(self, reference: str, pin: str):
|
|
448
|
+
def add_connection(self, reference: str, pin: str) -> None:
|
|
411
449
|
"""Add component pin to net."""
|
|
412
450
|
connection = (reference, pin)
|
|
413
451
|
if connection not in self.components:
|
|
414
452
|
self.components.append(connection)
|
|
415
453
|
|
|
416
|
-
def remove_connection(self, reference: str, pin: str):
|
|
454
|
+
def remove_connection(self, reference: str, pin: str) -> None:
|
|
417
455
|
"""Remove component pin from net."""
|
|
418
456
|
connection = (reference, pin)
|
|
419
457
|
if connection in self.components:
|
|
@@ -439,7 +477,7 @@ class Sheet:
|
|
|
439
477
|
stroke_type: str = "solid"
|
|
440
478
|
fill_color: Tuple[float, float, float, float] = (0, 0, 0, 0.0)
|
|
441
479
|
|
|
442
|
-
def __post_init__(self):
|
|
480
|
+
def __post_init__(self) -> None:
|
|
443
481
|
if not self.uuid:
|
|
444
482
|
self.uuid = str(uuid4())
|
|
445
483
|
|
|
@@ -454,7 +492,7 @@ class SheetPin:
|
|
|
454
492
|
pin_type: PinType = PinType.BIDIRECTIONAL
|
|
455
493
|
size: float = 1.27
|
|
456
494
|
|
|
457
|
-
def __post_init__(self):
|
|
495
|
+
def __post_init__(self) -> None:
|
|
458
496
|
if not self.uuid:
|
|
459
497
|
self.uuid = str(uuid4())
|
|
460
498
|
|
|
@@ -497,7 +535,7 @@ class Schematic:
|
|
|
497
535
|
rectangles: List[SchematicRectangle] = field(default_factory=list)
|
|
498
536
|
lib_symbols: Dict[str, Any] = field(default_factory=dict)
|
|
499
537
|
|
|
500
|
-
def __post_init__(self):
|
|
538
|
+
def __post_init__(self) -> None:
|
|
501
539
|
if not self.uuid:
|
|
502
540
|
self.uuid = str(uuid4())
|
|
503
541
|
|
kicad_sch_api/core/wires.py
CHANGED
|
@@ -9,56 +9,36 @@ import logging
|
|
|
9
9
|
import uuid as uuid_module
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
|
+
from .collections import BaseCollection
|
|
12
13
|
from .types import Point, Wire, WireType
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class WireCollection:
|
|
18
|
+
class WireCollection(BaseCollection[Wire]):
|
|
18
19
|
"""
|
|
19
20
|
Professional wire collection with enhanced management features.
|
|
20
21
|
|
|
22
|
+
Inherits from BaseCollection for standard operations and adds wire-specific
|
|
23
|
+
functionality.
|
|
24
|
+
|
|
21
25
|
Features:
|
|
22
|
-
- Fast UUID-based lookup and indexing
|
|
23
|
-
- Bulk operations for performance
|
|
26
|
+
- Fast UUID-based lookup and indexing (inherited)
|
|
27
|
+
- Bulk operations for performance (inherited)
|
|
24
28
|
- Multi-point wire support
|
|
25
29
|
- Validation and conflict detection
|
|
26
30
|
- Junction management integration
|
|
31
|
+
- Wire geometry queries (horizontal, vertical, by-point)
|
|
27
32
|
"""
|
|
28
33
|
|
|
29
|
-
def __init__(self, wires: Optional[List[Wire]] = None):
|
|
34
|
+
def __init__(self, wires: Optional[List[Wire]] = None) -> None:
|
|
30
35
|
"""
|
|
31
36
|
Initialize wire collection.
|
|
32
37
|
|
|
33
38
|
Args:
|
|
34
39
|
wires: Initial list of wires
|
|
35
40
|
"""
|
|
36
|
-
|
|
37
|
-
self._uuid_index: Dict[str, int] = {}
|
|
38
|
-
self._modified = False
|
|
39
|
-
|
|
40
|
-
# Build UUID index
|
|
41
|
-
self._rebuild_index()
|
|
42
|
-
|
|
43
|
-
logger.debug(f"WireCollection initialized with {len(self._wires)} wires")
|
|
44
|
-
|
|
45
|
-
def _rebuild_index(self):
|
|
46
|
-
"""Rebuild UUID index for fast lookups."""
|
|
47
|
-
self._uuid_index = {wire.uuid: i for i, wire in enumerate(self._wires)}
|
|
48
|
-
|
|
49
|
-
def __len__(self) -> int:
|
|
50
|
-
"""Number of wires in collection."""
|
|
51
|
-
return len(self._wires)
|
|
52
|
-
|
|
53
|
-
def __iter__(self):
|
|
54
|
-
"""Iterate over wires."""
|
|
55
|
-
return iter(self._wires)
|
|
56
|
-
|
|
57
|
-
def __getitem__(self, uuid: str) -> Wire:
|
|
58
|
-
"""Get wire by UUID."""
|
|
59
|
-
if uuid not in self._uuid_index:
|
|
60
|
-
raise KeyError(f"Wire with UUID '{uuid}' not found")
|
|
61
|
-
return self._wires[self._uuid_index[uuid]]
|
|
41
|
+
super().__init__(wires, collection_name="wires")
|
|
62
42
|
|
|
63
43
|
def add(
|
|
64
44
|
self,
|
|
@@ -114,37 +94,14 @@ class WireCollection:
|
|
|
114
94
|
# Create wire
|
|
115
95
|
wire = Wire(uuid=uuid, points=wire_points, wire_type=wire_type, stroke_width=stroke_width)
|
|
116
96
|
|
|
117
|
-
# Add to collection
|
|
118
|
-
self.
|
|
119
|
-
self._uuid_index[uuid] = len(self._wires) - 1
|
|
120
|
-
self._modified = True
|
|
97
|
+
# Add to collection using base class method
|
|
98
|
+
self._add_item(wire)
|
|
121
99
|
|
|
122
100
|
logger.debug(f"Added wire: {len(wire_points)} points, UUID={uuid}")
|
|
123
101
|
return uuid
|
|
124
102
|
|
|
125
|
-
def remove(self, uuid: str) -> bool:
|
|
126
|
-
"""
|
|
127
|
-
Remove wire by UUID.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
uuid: Wire UUID to remove
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
True if wire was removed, False if not found
|
|
134
|
-
"""
|
|
135
|
-
if uuid not in self._uuid_index:
|
|
136
|
-
return False
|
|
137
|
-
|
|
138
|
-
index = self._uuid_index[uuid]
|
|
139
|
-
del self._wires[index]
|
|
140
|
-
self._rebuild_index()
|
|
141
|
-
self._modified = True
|
|
142
|
-
|
|
143
|
-
logger.debug(f"Removed wire: {uuid}")
|
|
144
|
-
return True
|
|
145
|
-
|
|
146
103
|
def get_by_point(
|
|
147
|
-
self, point: Union[Point, Tuple[float, float]], tolerance: float = None
|
|
104
|
+
self, point: Union[Point, Tuple[float, float]], tolerance: Optional[float] = None
|
|
148
105
|
) -> List[Wire]:
|
|
149
106
|
"""
|
|
150
107
|
Find wires that pass through or near a point.
|
|
@@ -164,7 +121,7 @@ class WireCollection:
|
|
|
164
121
|
point = Point(point[0], point[1])
|
|
165
122
|
|
|
166
123
|
matching_wires = []
|
|
167
|
-
for wire in self.
|
|
124
|
+
for wire in self._items:
|
|
168
125
|
# Check if any wire point is close
|
|
169
126
|
for wire_point in wire.points:
|
|
170
127
|
if wire_point.distance_to(point) <= tolerance:
|
|
@@ -213,40 +170,35 @@ class WireCollection:
|
|
|
213
170
|
|
|
214
171
|
def get_horizontal_wires(self) -> List[Wire]:
|
|
215
172
|
"""Get all horizontal wires."""
|
|
216
|
-
return [wire for wire in self.
|
|
173
|
+
return [wire for wire in self._items if wire.is_horizontal()]
|
|
217
174
|
|
|
218
175
|
def get_vertical_wires(self) -> List[Wire]:
|
|
219
176
|
"""Get all vertical wires."""
|
|
220
|
-
return [wire for wire in self.
|
|
177
|
+
return [wire for wire in self._items if wire.is_vertical()]
|
|
221
178
|
|
|
222
179
|
def get_statistics(self) -> Dict[str, Any]:
|
|
223
|
-
"""Get wire collection statistics."""
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
180
|
+
"""Get wire collection statistics (extends base statistics)."""
|
|
181
|
+
base_stats = super().get_statistics()
|
|
182
|
+
total_length = sum(wire.length for wire in self._items)
|
|
183
|
+
simple_wires = sum(1 for wire in self._items if wire.is_simple())
|
|
184
|
+
multi_point_wires = len(self._items) - simple_wires
|
|
227
185
|
|
|
228
186
|
return {
|
|
229
|
-
|
|
187
|
+
**base_stats,
|
|
188
|
+
"total_wires": len(self._items),
|
|
230
189
|
"simple_wires": simple_wires,
|
|
231
190
|
"multi_point_wires": multi_point_wires,
|
|
232
191
|
"total_length": total_length,
|
|
233
|
-
"avg_length": total_length / len(self.
|
|
192
|
+
"avg_length": total_length / len(self._items) if self._items else 0,
|
|
234
193
|
"horizontal_wires": len(self.get_horizontal_wires()),
|
|
235
194
|
"vertical_wires": len(self.get_vertical_wires()),
|
|
236
195
|
}
|
|
237
196
|
|
|
238
|
-
def clear(self):
|
|
239
|
-
"""Remove all wires from collection."""
|
|
240
|
-
self._wires.clear()
|
|
241
|
-
self._uuid_index.clear()
|
|
242
|
-
self._modified = True
|
|
243
|
-
logger.debug("Cleared all wires")
|
|
244
|
-
|
|
245
197
|
@property
|
|
246
198
|
def modified(self) -> bool:
|
|
247
199
|
"""Check if collection has been modified."""
|
|
248
|
-
return self.
|
|
200
|
+
return self.is_modified()
|
|
249
201
|
|
|
250
|
-
def mark_saved(self):
|
|
202
|
+
def mark_saved(self) -> None:
|
|
251
203
|
"""Mark collection as saved (reset modified flag)."""
|
|
252
|
-
self.
|
|
204
|
+
self.reset_modified_flag()
|
|
@@ -17,4 +17,6 @@ DEFAULT_PIN_NUMBER_SIZE = 1.27 # 50 mils
|
|
|
17
17
|
# Text width ratio for proportional font rendering
|
|
18
18
|
# KiCad uses proportional fonts where average character width is ~0.65x height
|
|
19
19
|
# This prevents label text from extending beyond calculated bounding boxes
|
|
20
|
-
DEFAULT_PIN_TEXT_WIDTH_RATIO =
|
|
20
|
+
DEFAULT_PIN_TEXT_WIDTH_RATIO = (
|
|
21
|
+
0.65 # Width to height ratio for pin text (proportional font average)
|
|
22
|
+
)
|