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.
Files changed (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {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
@@ -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
- # TODO: Apply rotation and symbol position transformation
166
- return Point(self.position.x + pin.position.x, self.position.y + pin.position.y)
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
- (self.start.x + self.end.x) / 2,
371
- (self.start.y + self.end.y) / 2
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