kicad-sch-api 0.3.0__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 +68 -3
- 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/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- 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 +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- 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 +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -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 +145 -0
- 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 +216 -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 +485 -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 +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- 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/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/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.3.0.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/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-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
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 .collections import BaseCollection
|
|
14
|
+
from .types import Point, Text
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TextElement:
|
|
20
|
+
"""
|
|
21
|
+
Enhanced wrapper for schematic text elements with modern API.
|
|
22
|
+
|
|
23
|
+
Provides intuitive access to text properties and operations
|
|
24
|
+
while maintaining exact format preservation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, text_data: Text, parent_collection: "TextCollection"):
|
|
28
|
+
"""
|
|
29
|
+
Initialize text element wrapper.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
text_data: Underlying text data
|
|
33
|
+
parent_collection: Parent collection for updates
|
|
34
|
+
"""
|
|
35
|
+
self._data = text_data
|
|
36
|
+
self._collection = parent_collection
|
|
37
|
+
self._validator = SchematicValidator()
|
|
38
|
+
|
|
39
|
+
# Core properties with validation
|
|
40
|
+
@property
|
|
41
|
+
def uuid(self) -> str:
|
|
42
|
+
"""Text element UUID."""
|
|
43
|
+
return self._data.uuid
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def text(self) -> str:
|
|
47
|
+
"""Text content."""
|
|
48
|
+
return self._data.text
|
|
49
|
+
|
|
50
|
+
@text.setter
|
|
51
|
+
def text(self, value: str):
|
|
52
|
+
"""Set text content with validation."""
|
|
53
|
+
if not isinstance(value, str):
|
|
54
|
+
raise ValidationError(f"Text content must be string, got {type(value)}")
|
|
55
|
+
self._data.text = value
|
|
56
|
+
self._collection._mark_modified()
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def position(self) -> Point:
|
|
60
|
+
"""Text position."""
|
|
61
|
+
return self._data.position
|
|
62
|
+
|
|
63
|
+
@position.setter
|
|
64
|
+
def position(self, value: Union[Point, Tuple[float, float]]):
|
|
65
|
+
"""Set text position."""
|
|
66
|
+
if isinstance(value, tuple):
|
|
67
|
+
value = Point(value[0], value[1])
|
|
68
|
+
elif not isinstance(value, Point):
|
|
69
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
|
|
70
|
+
self._data.position = value
|
|
71
|
+
self._collection._mark_modified()
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def rotation(self) -> float:
|
|
75
|
+
"""Text rotation in degrees."""
|
|
76
|
+
return self._data.rotation
|
|
77
|
+
|
|
78
|
+
@rotation.setter
|
|
79
|
+
def rotation(self, value: float):
|
|
80
|
+
"""Set text rotation."""
|
|
81
|
+
self._data.rotation = float(value)
|
|
82
|
+
self._collection._mark_modified()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def size(self) -> float:
|
|
86
|
+
"""Text size."""
|
|
87
|
+
return self._data.size
|
|
88
|
+
|
|
89
|
+
@size.setter
|
|
90
|
+
def size(self, value: float):
|
|
91
|
+
"""Set text size with validation."""
|
|
92
|
+
if value <= 0:
|
|
93
|
+
raise ValidationError(f"Text size must be positive, got {value}")
|
|
94
|
+
self._data.size = float(value)
|
|
95
|
+
self._collection._mark_modified()
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def exclude_from_sim(self) -> bool:
|
|
99
|
+
"""Whether text is excluded from simulation."""
|
|
100
|
+
return self._data.exclude_from_sim
|
|
101
|
+
|
|
102
|
+
@exclude_from_sim.setter
|
|
103
|
+
def exclude_from_sim(self, value: bool):
|
|
104
|
+
"""Set exclude from simulation flag."""
|
|
105
|
+
self._data.exclude_from_sim = bool(value)
|
|
106
|
+
self._collection._mark_modified()
|
|
107
|
+
|
|
108
|
+
def validate(self) -> List[ValidationIssue]:
|
|
109
|
+
"""Validate this text element."""
|
|
110
|
+
return self._validator.validate_text(self._data.__dict__)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
113
|
+
"""Convert text element to dictionary representation."""
|
|
114
|
+
return {
|
|
115
|
+
"uuid": self.uuid,
|
|
116
|
+
"text": self.text,
|
|
117
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
118
|
+
"rotation": self.rotation,
|
|
119
|
+
"size": self.size,
|
|
120
|
+
"exclude_from_sim": self.exclude_from_sim,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def __str__(self) -> str:
|
|
124
|
+
"""String representation."""
|
|
125
|
+
return f"<Text '{self.text}' @ {self.position}>"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TextCollection(BaseCollection[TextElement]):
|
|
129
|
+
"""
|
|
130
|
+
Collection class for efficient text element management.
|
|
131
|
+
|
|
132
|
+
Inherits from BaseCollection for standard operations and adds text-specific
|
|
133
|
+
functionality including content-based indexing.
|
|
134
|
+
|
|
135
|
+
Provides fast lookup, filtering, and bulk operations for schematic text elements.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, texts: List[Text] = None):
|
|
139
|
+
"""
|
|
140
|
+
Initialize text collection.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
texts: Initial list of text data
|
|
144
|
+
"""
|
|
145
|
+
# Initialize base collection with empty list (we'll add elements below)
|
|
146
|
+
super().__init__([], collection_name="texts")
|
|
147
|
+
|
|
148
|
+
# Additional text-specific index
|
|
149
|
+
self._content_index: Dict[str, List[TextElement]] = {}
|
|
150
|
+
|
|
151
|
+
# Add initial texts
|
|
152
|
+
if texts:
|
|
153
|
+
for text_data in texts:
|
|
154
|
+
self._add_to_indexes(TextElement(text_data, self))
|
|
155
|
+
|
|
156
|
+
def add(
|
|
157
|
+
self,
|
|
158
|
+
text: str,
|
|
159
|
+
position: Union[Point, Tuple[float, float]],
|
|
160
|
+
rotation: float = 0.0,
|
|
161
|
+
size: float = 1.27,
|
|
162
|
+
exclude_from_sim: bool = False,
|
|
163
|
+
text_uuid: Optional[str] = None,
|
|
164
|
+
) -> TextElement:
|
|
165
|
+
"""
|
|
166
|
+
Add a new text element to the schematic.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
text: Text content
|
|
170
|
+
position: Text position
|
|
171
|
+
rotation: Text rotation in degrees
|
|
172
|
+
size: Text size
|
|
173
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
174
|
+
text_uuid: Specific UUID for text (auto-generated if None)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Newly created TextElement
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ValidationError: If text data is invalid
|
|
181
|
+
"""
|
|
182
|
+
# Validate inputs
|
|
183
|
+
if not isinstance(text, str) or not text.strip():
|
|
184
|
+
raise ValidationError("Text content cannot be empty")
|
|
185
|
+
|
|
186
|
+
if isinstance(position, tuple):
|
|
187
|
+
position = Point(position[0], position[1])
|
|
188
|
+
elif not isinstance(position, Point):
|
|
189
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
|
|
190
|
+
|
|
191
|
+
if size <= 0:
|
|
192
|
+
raise ValidationError(f"Text size must be positive, got {size}")
|
|
193
|
+
|
|
194
|
+
# Generate UUID if not provided
|
|
195
|
+
if not text_uuid:
|
|
196
|
+
text_uuid = str(uuid.uuid4())
|
|
197
|
+
|
|
198
|
+
# Check for duplicate UUID
|
|
199
|
+
if text_uuid in self._uuid_index:
|
|
200
|
+
raise ValidationError(f"Text UUID {text_uuid} already exists")
|
|
201
|
+
|
|
202
|
+
# Create text data
|
|
203
|
+
text_data = Text(
|
|
204
|
+
uuid=text_uuid,
|
|
205
|
+
position=position,
|
|
206
|
+
text=text,
|
|
207
|
+
rotation=rotation,
|
|
208
|
+
size=size,
|
|
209
|
+
exclude_from_sim=exclude_from_sim,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Create wrapper and add to collection
|
|
213
|
+
text_element = TextElement(text_data, self)
|
|
214
|
+
self._add_to_indexes(text_element)
|
|
215
|
+
|
|
216
|
+
logger.debug(f"Added text: {text_element}")
|
|
217
|
+
return text_element
|
|
218
|
+
|
|
219
|
+
def remove(self, text_uuid: str) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Remove text by UUID.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
text_uuid: UUID of text to remove
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
True if text was removed, False if not found
|
|
228
|
+
"""
|
|
229
|
+
text_element = self.get(text_uuid)
|
|
230
|
+
if not text_element:
|
|
231
|
+
return False
|
|
232
|
+
|
|
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)
|
|
242
|
+
|
|
243
|
+
logger.debug(f"Removed text: {text_element}")
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
def find_by_content(self, content: str, exact: bool = True) -> List[TextElement]:
|
|
247
|
+
"""
|
|
248
|
+
Find texts by content.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
content: Content to search for
|
|
252
|
+
exact: If True, exact match; if False, substring match
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
List of matching text elements
|
|
256
|
+
"""
|
|
257
|
+
if exact:
|
|
258
|
+
return self._content_index.get(content, []).copy()
|
|
259
|
+
else:
|
|
260
|
+
matches = []
|
|
261
|
+
for text_element in self._items:
|
|
262
|
+
if content.lower() in text_element.text.lower():
|
|
263
|
+
matches.append(text_element)
|
|
264
|
+
return matches
|
|
265
|
+
|
|
266
|
+
def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
|
|
267
|
+
"""
|
|
268
|
+
Filter texts by predicate function (delegates to base class find).
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
predicate: Function that returns True for texts to include
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
List of texts matching predicate
|
|
275
|
+
"""
|
|
276
|
+
return self.find(predicate)
|
|
277
|
+
|
|
278
|
+
def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
|
|
279
|
+
"""
|
|
280
|
+
Update multiple texts matching criteria.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
criteria: Function to select texts to update
|
|
284
|
+
updates: Dictionary of property updates
|
|
285
|
+
"""
|
|
286
|
+
updated_count = 0
|
|
287
|
+
for text_element in self._items:
|
|
288
|
+
if criteria(text_element):
|
|
289
|
+
for prop, value in updates.items():
|
|
290
|
+
if hasattr(text_element, prop):
|
|
291
|
+
setattr(text_element, prop, value)
|
|
292
|
+
updated_count += 1
|
|
293
|
+
|
|
294
|
+
if updated_count > 0:
|
|
295
|
+
self._mark_modified()
|
|
296
|
+
logger.debug(f"Bulk updated {updated_count} text properties")
|
|
297
|
+
|
|
298
|
+
def clear(self):
|
|
299
|
+
"""Remove all texts from collection."""
|
|
300
|
+
self._content_index.clear()
|
|
301
|
+
super().clear()
|
|
302
|
+
|
|
303
|
+
def _add_to_indexes(self, text_element: TextElement):
|
|
304
|
+
"""Add text to internal indexes (base + content index)."""
|
|
305
|
+
self._add_item(text_element)
|
|
306
|
+
|
|
307
|
+
# Add to content index
|
|
308
|
+
content = text_element.text
|
|
309
|
+
if content not in self._content_index:
|
|
310
|
+
self._content_index[content] = []
|
|
311
|
+
self._content_index[content].append(text_element)
|
|
312
|
+
|
|
313
|
+
# Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
|
|
314
|
+
def __bool__(self) -> bool:
|
|
315
|
+
"""Return True if collection has texts."""
|
|
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 = (
|
|
@@ -118,6 +155,54 @@ class SchematicPin:
|
|
|
118
155
|
)
|
|
119
156
|
|
|
120
157
|
|
|
158
|
+
@dataclass
|
|
159
|
+
class PinInfo:
|
|
160
|
+
"""
|
|
161
|
+
Complete pin information for a component pin.
|
|
162
|
+
|
|
163
|
+
This dataclass provides comprehensive pin metadata including position,
|
|
164
|
+
electrical properties, and graphical representation. Positions are in
|
|
165
|
+
schematic coordinates (absolute positions accounting for component
|
|
166
|
+
rotation and mirroring).
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
number: str # Pin number (e.g., "1", "2", "A1")
|
|
170
|
+
name: str # Pin name (e.g., "VCC", "GND", "CLK")
|
|
171
|
+
position: Point # Absolute position in schematic coordinates (mm)
|
|
172
|
+
electrical_type: PinType = PinType.PASSIVE # Electrical type (input, output, passive, etc.)
|
|
173
|
+
shape: PinShape = PinShape.LINE # Graphical shape (line, inverted, clock, etc.)
|
|
174
|
+
length: float = 2.54 # Pin length in mm
|
|
175
|
+
orientation: float = 0.0 # Pin orientation in degrees (0, 90, 180, 270)
|
|
176
|
+
uuid: str = "" # Unique identifier for this pin instance
|
|
177
|
+
|
|
178
|
+
def __post_init__(self) -> None:
|
|
179
|
+
"""Validate and normalize pin information."""
|
|
180
|
+
# Ensure types are correct
|
|
181
|
+
self.electrical_type = (
|
|
182
|
+
PinType(self.electrical_type)
|
|
183
|
+
if isinstance(self.electrical_type, str)
|
|
184
|
+
else self.electrical_type
|
|
185
|
+
)
|
|
186
|
+
self.shape = PinShape(self.shape) if isinstance(self.shape, str) else self.shape
|
|
187
|
+
|
|
188
|
+
# Generate UUID if not provided
|
|
189
|
+
if not self.uuid:
|
|
190
|
+
self.uuid = str(uuid4())
|
|
191
|
+
|
|
192
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
193
|
+
"""Convert pin info to dictionary representation."""
|
|
194
|
+
return {
|
|
195
|
+
"number": self.number,
|
|
196
|
+
"name": self.name,
|
|
197
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
198
|
+
"electrical_type": self.electrical_type.value,
|
|
199
|
+
"shape": self.shape.value,
|
|
200
|
+
"length": self.length,
|
|
201
|
+
"orientation": self.orientation,
|
|
202
|
+
"uuid": self.uuid,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
121
206
|
@dataclass
|
|
122
207
|
class SchematicSymbol:
|
|
123
208
|
"""Component symbol in a schematic."""
|
|
@@ -134,8 +219,9 @@ class SchematicSymbol:
|
|
|
134
219
|
in_bom: bool = True
|
|
135
220
|
on_board: bool = True
|
|
136
221
|
unit: int = 1
|
|
222
|
+
instances: List["SymbolInstance"] = field(default_factory=list) # FIX: Add instances field for hierarchical support
|
|
137
223
|
|
|
138
|
-
def __post_init__(self):
|
|
224
|
+
def __post_init__(self) -> None:
|
|
139
225
|
# Generate UUID if not provided
|
|
140
226
|
if not self.uuid:
|
|
141
227
|
self.uuid = str(uuid4())
|
|
@@ -158,12 +244,37 @@ class SchematicSymbol:
|
|
|
158
244
|
return None
|
|
159
245
|
|
|
160
246
|
def get_pin_position(self, pin_number: str) -> Optional[Point]:
|
|
161
|
-
"""Get absolute position of a pin.
|
|
247
|
+
"""Get absolute position of a pin with rotation transformation.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
pin_number: Pin number to get position for
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Absolute position of the pin in schematic coordinates, or None if pin not found
|
|
254
|
+
|
|
255
|
+
Note:
|
|
256
|
+
Applies standard 2D rotation matrix to transform pin position from
|
|
257
|
+
symbol's local coordinate system to schematic's global coordinate system.
|
|
258
|
+
"""
|
|
259
|
+
import math
|
|
260
|
+
|
|
162
261
|
pin = self.get_pin(pin_number)
|
|
163
262
|
if not pin:
|
|
164
263
|
return None
|
|
165
|
-
|
|
166
|
-
|
|
264
|
+
|
|
265
|
+
# Apply rotation transformation using standard 2D rotation matrix
|
|
266
|
+
# [x'] = [cos(θ) -sin(θ)] [x]
|
|
267
|
+
# [y'] [sin(θ) cos(θ)] [y]
|
|
268
|
+
angle_rad = math.radians(self.rotation)
|
|
269
|
+
cos_a = math.cos(angle_rad)
|
|
270
|
+
sin_a = math.sin(angle_rad)
|
|
271
|
+
|
|
272
|
+
# Rotate pin position from symbol's local coordinates
|
|
273
|
+
rotated_x = pin.position.x * cos_a - pin.position.y * sin_a
|
|
274
|
+
rotated_y = pin.position.x * sin_a + pin.position.y * cos_a
|
|
275
|
+
|
|
276
|
+
# Add to component position to get absolute position
|
|
277
|
+
return Point(self.position.x + rotated_x, self.position.y + rotated_y)
|
|
167
278
|
|
|
168
279
|
|
|
169
280
|
class WireType(Enum):
|
|
@@ -183,7 +294,7 @@ class Wire:
|
|
|
183
294
|
stroke_width: float = 0.0
|
|
184
295
|
stroke_type: str = "default"
|
|
185
296
|
|
|
186
|
-
def __post_init__(self):
|
|
297
|
+
def __post_init__(self) -> None:
|
|
187
298
|
if not self.uuid:
|
|
188
299
|
self.uuid = str(uuid4())
|
|
189
300
|
|
|
@@ -196,7 +307,7 @@ class Wire:
|
|
|
196
307
|
raise ValueError("Wire must have at least 2 points")
|
|
197
308
|
|
|
198
309
|
@classmethod
|
|
199
|
-
def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs) -> "Wire":
|
|
310
|
+
def from_start_end(cls, uuid: str, start: Point, end: Point, **kwargs: Any) -> "Wire":
|
|
200
311
|
"""Create wire from start and end points (convenience method)."""
|
|
201
312
|
return cls(uuid=uuid, points=[start, end], **kwargs)
|
|
202
313
|
|
|
@@ -244,7 +355,7 @@ class Junction:
|
|
|
244
355
|
diameter: float = 0 # KiCAD default diameter
|
|
245
356
|
color: Tuple[int, int, int, int] = (0, 0, 0, 0) # RGBA color
|
|
246
357
|
|
|
247
|
-
def __post_init__(self):
|
|
358
|
+
def __post_init__(self) -> None:
|
|
248
359
|
if not self.uuid:
|
|
249
360
|
self.uuid = str(uuid4())
|
|
250
361
|
|
|
@@ -279,8 +390,10 @@ class Label:
|
|
|
279
390
|
rotation: float = 0.0
|
|
280
391
|
size: float = 1.27
|
|
281
392
|
shape: Optional[HierarchicalLabelShape] = None # Only for hierarchical labels
|
|
393
|
+
justify_h: str = "left" # Horizontal justification: "left", "right", "center"
|
|
394
|
+
justify_v: str = "bottom" # Vertical justification: "top", "bottom", "center"
|
|
282
395
|
|
|
283
|
-
def __post_init__(self):
|
|
396
|
+
def __post_init__(self) -> None:
|
|
284
397
|
if not self.uuid:
|
|
285
398
|
self.uuid = str(uuid4())
|
|
286
399
|
|
|
@@ -305,7 +418,7 @@ class Text:
|
|
|
305
418
|
size: float = 1.27
|
|
306
419
|
exclude_from_sim: bool = False
|
|
307
420
|
|
|
308
|
-
def __post_init__(self):
|
|
421
|
+
def __post_init__(self) -> None:
|
|
309
422
|
if not self.uuid:
|
|
310
423
|
self.uuid = str(uuid4())
|
|
311
424
|
|
|
@@ -333,7 +446,7 @@ class TextBox:
|
|
|
333
446
|
justify_vertical: str = "top"
|
|
334
447
|
exclude_from_sim: bool = False
|
|
335
448
|
|
|
336
|
-
def __post_init__(self):
|
|
449
|
+
def __post_init__(self) -> None:
|
|
337
450
|
if not self.uuid:
|
|
338
451
|
self.uuid = str(uuid4())
|
|
339
452
|
|
|
@@ -349,7 +462,7 @@ class SchematicRectangle:
|
|
|
349
462
|
stroke_type: str = "default"
|
|
350
463
|
fill_type: str = "none"
|
|
351
464
|
|
|
352
|
-
def __post_init__(self):
|
|
465
|
+
def __post_init__(self) -> None:
|
|
353
466
|
if not self.uuid:
|
|
354
467
|
self.uuid = str(uuid4())
|
|
355
468
|
|
|
@@ -366,10 +479,33 @@ class SchematicRectangle:
|
|
|
366
479
|
@property
|
|
367
480
|
def center(self) -> Point:
|
|
368
481
|
"""Rectangle center point."""
|
|
369
|
-
return Point(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
482
|
+
return Point((self.start.x + self.end.x) / 2, (self.start.y + self.end.y) / 2)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@dataclass
|
|
486
|
+
class Image:
|
|
487
|
+
"""Image element in schematic."""
|
|
488
|
+
|
|
489
|
+
uuid: str
|
|
490
|
+
position: Point
|
|
491
|
+
data: str # Base64-encoded image data
|
|
492
|
+
scale: float = 1.0
|
|
493
|
+
|
|
494
|
+
def __post_init__(self) -> None:
|
|
495
|
+
if not self.uuid:
|
|
496
|
+
self.uuid = str(uuid4())
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@dataclass
|
|
500
|
+
class NoConnect:
|
|
501
|
+
"""No-connect symbol in schematic."""
|
|
502
|
+
|
|
503
|
+
uuid: str
|
|
504
|
+
position: Point
|
|
505
|
+
|
|
506
|
+
def __post_init__(self) -> None:
|
|
507
|
+
if not self.uuid:
|
|
508
|
+
self.uuid = str(uuid4())
|
|
373
509
|
|
|
374
510
|
|
|
375
511
|
@dataclass
|
|
@@ -381,13 +517,13 @@ class Net:
|
|
|
381
517
|
wires: List[str] = field(default_factory=list) # Wire UUIDs
|
|
382
518
|
labels: List[str] = field(default_factory=list) # Label UUIDs
|
|
383
519
|
|
|
384
|
-
def add_connection(self, reference: str, pin: str):
|
|
520
|
+
def add_connection(self, reference: str, pin: str) -> None:
|
|
385
521
|
"""Add component pin to net."""
|
|
386
522
|
connection = (reference, pin)
|
|
387
523
|
if connection not in self.components:
|
|
388
524
|
self.components.append(connection)
|
|
389
525
|
|
|
390
|
-
def remove_connection(self, reference: str, pin: str):
|
|
526
|
+
def remove_connection(self, reference: str, pin: str) -> None:
|
|
391
527
|
"""Remove component pin from net."""
|
|
392
528
|
connection = (reference, pin)
|
|
393
529
|
if connection in self.components:
|
|
@@ -413,7 +549,7 @@ class Sheet:
|
|
|
413
549
|
stroke_type: str = "solid"
|
|
414
550
|
fill_color: Tuple[float, float, float, float] = (0, 0, 0, 0.0)
|
|
415
551
|
|
|
416
|
-
def __post_init__(self):
|
|
552
|
+
def __post_init__(self) -> None:
|
|
417
553
|
if not self.uuid:
|
|
418
554
|
self.uuid = str(uuid4())
|
|
419
555
|
|
|
@@ -428,7 +564,7 @@ class SheetPin:
|
|
|
428
564
|
pin_type: PinType = PinType.BIDIRECTIONAL
|
|
429
565
|
size: float = 1.27
|
|
430
566
|
|
|
431
|
-
def __post_init__(self):
|
|
567
|
+
def __post_init__(self) -> None:
|
|
432
568
|
if not self.uuid:
|
|
433
569
|
self.uuid = str(uuid4())
|
|
434
570
|
|
|
@@ -471,7 +607,7 @@ class Schematic:
|
|
|
471
607
|
rectangles: List[SchematicRectangle] = field(default_factory=list)
|
|
472
608
|
lib_symbols: Dict[str, Any] = field(default_factory=dict)
|
|
473
609
|
|
|
474
|
-
def __post_init__(self):
|
|
610
|
+
def __post_init__(self) -> None:
|
|
475
611
|
if not self.uuid:
|
|
476
612
|
self.uuid = str(uuid4())
|
|
477
613
|
|