kicad-sch-api 0.4.1__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 (66) hide show
  1. kicad_sch_api/__init__.py +67 -2
  2. kicad_sch_api/cli/kicad_to_python.py +169 -0
  3. kicad_sch_api/collections/__init__.py +23 -8
  4. kicad_sch_api/collections/base.py +369 -59
  5. kicad_sch_api/collections/components.py +1376 -187
  6. kicad_sch_api/collections/junctions.py +129 -289
  7. kicad_sch_api/collections/labels.py +391 -287
  8. kicad_sch_api/collections/wires.py +202 -316
  9. kicad_sch_api/core/__init__.py +37 -2
  10. kicad_sch_api/core/component_bounds.py +34 -12
  11. kicad_sch_api/core/components.py +146 -7
  12. kicad_sch_api/core/config.py +25 -12
  13. kicad_sch_api/core/connectivity.py +692 -0
  14. kicad_sch_api/core/exceptions.py +175 -0
  15. kicad_sch_api/core/factories/element_factory.py +3 -1
  16. kicad_sch_api/core/formatter.py +24 -7
  17. kicad_sch_api/core/geometry.py +94 -5
  18. kicad_sch_api/core/managers/__init__.py +4 -0
  19. kicad_sch_api/core/managers/base.py +76 -0
  20. kicad_sch_api/core/managers/file_io.py +3 -1
  21. kicad_sch_api/core/managers/format_sync.py +3 -2
  22. kicad_sch_api/core/managers/graphics.py +3 -2
  23. kicad_sch_api/core/managers/hierarchy.py +661 -0
  24. kicad_sch_api/core/managers/metadata.py +4 -2
  25. kicad_sch_api/core/managers/sheet.py +52 -14
  26. kicad_sch_api/core/managers/text_elements.py +3 -2
  27. kicad_sch_api/core/managers/validation.py +3 -2
  28. kicad_sch_api/core/managers/wire.py +112 -54
  29. kicad_sch_api/core/parsing_utils.py +63 -0
  30. kicad_sch_api/core/pin_utils.py +103 -9
  31. kicad_sch_api/core/schematic.py +343 -29
  32. kicad_sch_api/core/types.py +79 -7
  33. kicad_sch_api/exporters/__init__.py +10 -0
  34. kicad_sch_api/exporters/python_generator.py +610 -0
  35. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  36. kicad_sch_api/geometry/__init__.py +15 -3
  37. kicad_sch_api/geometry/routing.py +211 -0
  38. kicad_sch_api/parsers/elements/label_parser.py +30 -8
  39. kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
  40. kicad_sch_api/utils/logging.py +555 -0
  41. kicad_sch_api/utils/logging_decorators.py +587 -0
  42. kicad_sch_api/utils/validation.py +16 -22
  43. kicad_sch_api/wrappers/__init__.py +14 -0
  44. kicad_sch_api/wrappers/base.py +89 -0
  45. kicad_sch_api/wrappers/wire.py +198 -0
  46. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  47. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  48. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  49. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  50. mcp_server/__init__.py +34 -0
  51. mcp_server/example_logging_integration.py +506 -0
  52. mcp_server/models.py +252 -0
  53. mcp_server/server.py +357 -0
  54. mcp_server/tools/__init__.py +32 -0
  55. mcp_server/tools/component_tools.py +516 -0
  56. mcp_server/tools/connectivity_tools.py +532 -0
  57. mcp_server/tools/consolidated_tools.py +1216 -0
  58. mcp_server/tools/pin_discovery.py +333 -0
  59. mcp_server/utils/__init__.py +38 -0
  60. mcp_server/utils/logging.py +127 -0
  61. mcp_server/utils.py +36 -0
  62. kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
  63. kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
  64. kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
  65. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  66. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -11,11 +11,12 @@ from pathlib import Path
11
11
  from typing import Any, Dict, List, Optional, Tuple, Union
12
12
 
13
13
  from ..types import Point
14
+ from .base import BaseManager
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
17
18
 
18
- class SheetManager:
19
+ class SheetManager(BaseManager):
19
20
  """
20
21
  Manages hierarchical sheets and multi-sheet project coordination.
21
22
 
@@ -34,7 +35,7 @@ class SheetManager:
34
35
  Args:
35
36
  schematic_data: Reference to schematic data
36
37
  """
37
- self._data = schematic_data
38
+ super().__init__(schematic_data)
38
39
 
39
40
  def add_sheet(
40
41
  self,
@@ -121,29 +122,30 @@ class SheetManager:
121
122
  sheet_uuid: str,
122
123
  name: str,
123
124
  pin_type: str,
124
- position: Union[Point, Tuple[float, float]],
125
- rotation: float = 0,
126
- justify: str = "left",
125
+ edge: str,
126
+ position_along_edge: float,
127
127
  uuid_str: Optional[str] = None,
128
128
  ) -> Optional[str]:
129
129
  """
130
- Add a pin to an existing sheet.
130
+ Add a pin to an existing sheet using edge-based positioning.
131
131
 
132
132
  Args:
133
133
  sheet_uuid: UUID of target sheet
134
134
  name: Pin name
135
135
  pin_type: Pin type (input, output, bidirectional, tri_state, passive)
136
- position: Pin position relative to sheet
137
- rotation: Pin rotation in degrees
138
- justify: Text justification (left, right, center)
136
+ edge: Edge to place pin on ("right", "bottom", "left", "top")
137
+ position_along_edge: Distance along edge from reference corner (mm)
139
138
  uuid_str: Optional pin UUID
140
139
 
141
140
  Returns:
142
141
  UUID of created pin, or None if sheet not found
143
- """
144
- if isinstance(position, tuple):
145
- position = Point(position[0], position[1])
146
142
 
143
+ Edge positioning (clockwise from right):
144
+ - "right": rotation=0°, justify="right", position from top edge
145
+ - "bottom": rotation=270°, justify="left", position from left edge
146
+ - "left": rotation=180°, justify="left", position from bottom edge
147
+ - "top": rotation=90°, justify="right", position from left edge
148
+ """
147
149
  if uuid_str is None:
148
150
  uuid_str = str(uuid.uuid4())
149
151
 
@@ -152,15 +154,49 @@ class SheetManager:
152
154
  logger.warning(f"Invalid sheet pin type: {pin_type}. Using 'input'")
153
155
  pin_type = "input"
154
156
 
157
+ valid_edges = ["right", "bottom", "left", "top"]
158
+ if edge not in valid_edges:
159
+ logger.error(f"Invalid edge: {edge}. Must be one of {valid_edges}")
160
+ return None
161
+
155
162
  # Find the sheet
156
163
  sheets = self._data.get("sheets", [])
157
164
  for sheet in sheets:
158
165
  if sheet.get("uuid") == sheet_uuid:
166
+ # Get sheet bounds
167
+ sheet_x = sheet["position"]["x"]
168
+ sheet_y = sheet["position"]["y"]
169
+ sheet_width = sheet["size"]["width"]
170
+ sheet_height = sheet["size"]["height"]
171
+
172
+ # Calculate position, rotation, and justification based on edge
173
+ # Clockwise: right (0°) → bottom (270°) → left (180°) → top (90°)
174
+ if edge == "right":
175
+ x = sheet_x + sheet_width
176
+ y = sheet_y + position_along_edge
177
+ rotation = 0
178
+ justify = "right"
179
+ elif edge == "bottom":
180
+ x = sheet_x + position_along_edge
181
+ y = sheet_y + sheet_height
182
+ rotation = 270
183
+ justify = "left"
184
+ elif edge == "left":
185
+ x = sheet_x
186
+ y = sheet_y + sheet_height - position_along_edge
187
+ rotation = 180
188
+ justify = "left"
189
+ elif edge == "top":
190
+ x = sheet_x + position_along_edge
191
+ y = sheet_y
192
+ rotation = 90
193
+ justify = "right"
194
+
159
195
  pin_data = {
160
196
  "uuid": uuid_str,
161
197
  "name": name,
162
198
  "pin_type": pin_type,
163
- "position": {"x": position.x, "y": position.y},
199
+ "position": {"x": x, "y": y},
164
200
  "rotation": rotation,
165
201
  "size": 1.27,
166
202
  "justify": justify,
@@ -169,7 +205,9 @@ class SheetManager:
169
205
  # Add to sheet's pins array (already initialized in add_sheet)
170
206
  sheet["pins"].append(pin_data)
171
207
 
172
- logger.debug(f"Added pin '{name}' to sheet {sheet_uuid}")
208
+ logger.debug(
209
+ f"Added pin '{name}' to sheet {sheet_uuid} on {edge} edge at ({x}, {y})"
210
+ )
173
211
  return uuid_str
174
212
 
175
213
  logger.warning(f"Sheet not found: {sheet_uuid}")
@@ -11,11 +11,12 @@ import uuid
11
11
  from typing import Any, Dict, List, Optional, Tuple, Union
12
12
 
13
13
  from ..types import Point
14
+ from .base import BaseManager
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
17
18
 
18
- class TextElementManager:
19
+ class TextElementManager(BaseManager):
19
20
  """
20
21
  Manages text elements and labeling in KiCAD schematics.
21
22
 
@@ -34,7 +35,7 @@ class TextElementManager:
34
35
  Args:
35
36
  schematic_data: Reference to schematic data
36
37
  """
37
- self._data = schematic_data
38
+ super().__init__(schematic_data)
38
39
 
39
40
  def add_label(
40
41
  self,
@@ -11,11 +11,12 @@ from typing import Any, Dict, List, Optional, Set, Tuple
11
11
 
12
12
  from ...utils.validation import ValidationError, ValidationIssue
13
13
  from ..types import Point
14
+ from .base import BaseManager
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
17
18
 
18
- class ValidationManager:
19
+ class ValidationManager(BaseManager):
19
20
  """
20
21
  Comprehensive validation manager for schematic integrity.
21
22
 
@@ -39,7 +40,7 @@ class ValidationManager:
39
40
  component_collection: Component collection for validation
40
41
  wire_collection: Wire collection for connectivity analysis
41
42
  """
42
- self._data = schematic_data
43
+ super().__init__(schematic_data)
43
44
  self._components = component_collection
44
45
  self._wires = wire_collection
45
46
  self._validation_rules = self._initialize_validation_rules()
@@ -10,13 +10,15 @@ import uuid
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
11
 
12
12
  from ...library.cache import get_symbol_cache
13
+ from ..connectivity import ConnectivityAnalyzer, Net
13
14
  from ..types import Point, Wire, WireType
14
15
  from ..wires import WireCollection
16
+ from .base import BaseManager
15
17
 
16
18
  logger = logging.getLogger(__name__)
17
19
 
18
20
 
19
- class WireManager:
21
+ class WireManager(BaseManager):
20
22
  """
21
23
  Manages wire operations and pin connectivity in KiCAD schematics.
22
24
 
@@ -29,7 +31,11 @@ class WireManager:
29
31
  """
30
32
 
31
33
  def __init__(
32
- self, schematic_data: Dict[str, Any], wire_collection: WireCollection, component_collection
34
+ self,
35
+ schematic_data: Dict[str, Any],
36
+ wire_collection: WireCollection,
37
+ component_collection,
38
+ schematic,
33
39
  ):
34
40
  """
35
41
  Initialize WireManager.
@@ -38,12 +44,18 @@ class WireManager:
38
44
  schematic_data: Reference to schematic data
39
45
  wire_collection: Wire collection for management
40
46
  component_collection: Component collection for pin lookups
47
+ schematic: Reference to parent Schematic object for connectivity analysis
41
48
  """
42
- self._data = schematic_data
49
+ super().__init__(schematic_data)
43
50
  self._wires = wire_collection
44
51
  self._components = component_collection
52
+ self._schematic = schematic
45
53
  self._symbol_cache = get_symbol_cache()
46
54
 
55
+ # Lazy-initialized connectivity analyzer (always hierarchical)
56
+ self._connectivity_analyzer: Optional[ConnectivityAnalyzer] = None
57
+ self._connectivity_valid = False
58
+
47
59
  def add_wire(
48
60
  self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
49
61
  ) -> str:
@@ -65,6 +77,9 @@ class WireManager:
65
77
  # Use the wire collection to add the wire
66
78
  wire_uuid = self._wires.add(start=start, end=end)
67
79
 
80
+ # Invalidate connectivity cache
81
+ self._invalidate_connectivity()
82
+
68
83
  logger.debug(f"Added wire: {start} -> {end}")
69
84
  return wire_uuid
70
85
 
@@ -92,6 +107,8 @@ class WireManager:
92
107
 
93
108
  success = removed_from_collection or removed_from_data
94
109
  if success:
110
+ # Invalidate connectivity cache
111
+ self._invalidate_connectivity()
95
112
  logger.debug(f"Removed wire: {wire_uuid}")
96
113
 
97
114
  return success
@@ -151,7 +168,8 @@ class WireManager:
151
168
  """
152
169
  Get absolute position of a component pin.
153
170
 
154
- This consolidates the duplicate implementations in the original schematic class.
171
+ Uses the same geometry transformation as the connectivity analyzer
172
+ to ensure consistent pin positions.
155
173
 
156
174
  Args:
157
175
  component_ref: Component reference (e.g., "R1")
@@ -160,27 +178,20 @@ class WireManager:
160
178
  Returns:
161
179
  Absolute pin position or None if not found
162
180
  """
181
+ from ..pin_utils import list_component_pins
182
+
163
183
  # Find component
164
184
  component = self._components.get(component_ref)
165
185
  if not component:
166
186
  logger.warning(f"Component not found: {component_ref}")
167
187
  return None
168
188
 
169
- # Get symbol definition from cache
170
- symbol_def = self._symbol_cache.get_symbol(component.lib_id)
171
- if not symbol_def:
172
- logger.warning(f"Symbol definition not found: {component.lib_id}")
173
- return None
174
-
175
- # Find pin in symbol definition
176
- for pin in symbol_def.pins:
177
- if pin.number == pin_number:
178
- # Calculate absolute position
179
- # Apply component rotation/mirroring if needed (simplified for now)
180
- absolute_x = component.position.x + pin.position.x
181
- absolute_y = component.position.y + pin.position.y
189
+ # Use pin_utils to get correct transformed positions
190
+ pins = list_component_pins(component)
182
191
 
183
- return Point(absolute_x, absolute_y)
192
+ for pin_num, pin_pos in pins:
193
+ if pin_num == pin_number:
194
+ return pin_pos
184
195
 
185
196
  logger.warning(f"Pin {pin_number} not found on component {component_ref}")
186
197
  return None
@@ -189,31 +200,24 @@ class WireManager:
189
200
  """
190
201
  List all pins and their positions for a component.
191
202
 
203
+ Uses the same geometry transformation as the connectivity analyzer
204
+ to ensure consistent pin positions.
205
+
192
206
  Args:
193
207
  component_ref: Component reference
194
208
 
195
209
  Returns:
196
210
  List of (pin_number, absolute_position) tuples
197
211
  """
198
- pins = []
212
+ from ..pin_utils import list_component_pins
199
213
 
200
214
  # Find component
201
215
  component = self._components.get(component_ref)
202
216
  if not component:
203
- return pins
204
-
205
- # Get symbol definition
206
- symbol_def = self._symbol_cache.get_symbol(component.lib_id)
207
- if not symbol_def:
208
- return pins
209
-
210
- # Calculate absolute positions for all pins
211
- for pin in symbol_def.pins:
212
- absolute_x = component.position.x + pin.position.x
213
- absolute_y = component.position.y + pin.position.y
214
- pins.append((pin.number, Point(absolute_x, absolute_y)))
217
+ return []
215
218
 
216
- return pins
219
+ # Use pin_utils to get correct transformed positions
220
+ return list_component_pins(component)
217
221
 
218
222
  def auto_route_pins(
219
223
  self,
@@ -280,7 +284,14 @@ class WireManager:
280
284
  self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
281
285
  ) -> bool:
282
286
  """
283
- Check if two pins are connected via wires.
287
+ Check if two pins are electrically connected.
288
+
289
+ Performs full connectivity analysis including:
290
+ - Direct wire connections
291
+ - Connections through junctions
292
+ - Connections through labels (local/global/hierarchical)
293
+ - Connections through power symbols
294
+ - Hierarchical sheet connections
284
295
 
285
296
  Args:
286
297
  component1_ref: First component reference
@@ -289,29 +300,15 @@ class WireManager:
289
300
  pin2_number: Second component pin number
290
301
 
291
302
  Returns:
292
- True if pins are connected, False otherwise
303
+ True if pins are electrically connected, False otherwise
293
304
  """
294
- pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
295
- pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
305
+ self._ensure_connectivity()
306
+
307
+ if self._connectivity_analyzer:
308
+ return self._connectivity_analyzer.are_connected(
309
+ component1_ref, pin1_number, component2_ref, pin2_number
310
+ )
296
311
 
297
- if pin1_pos is None or pin2_pos is None:
298
- return False
299
-
300
- # Check for direct wire connection
301
- for wire in self._wires:
302
- if (wire.start == pin1_pos and wire.end == pin2_pos) or (
303
- wire.start == pin2_pos and wire.end == pin1_pos
304
- ):
305
- return True
306
-
307
- # TODO: Implement more sophisticated connectivity analysis
308
- # NOTE: Current implementation only checks for direct wire connections between pins.
309
- # A full implementation would:
310
- # 1. Follow wire networks through junctions (connection points)
311
- # 2. Trace through labels (global/hierarchical net connections)
312
- # 3. Build a complete net connectivity graph
313
- # This is a known limitation - use ValidationManager for full electrical rule checking.
314
- # Priority: MEDIUM - Would enable better automated wiring validation
315
312
  return False
316
313
 
317
314
  def connect_pins_with_wire(
@@ -350,3 +347,64 @@ class WireManager:
350
347
  "bus": len([w for w in self._wires if w.wire_type == WireType.BUS]),
351
348
  },
352
349
  }
350
+
351
+ def get_net_for_pin(self, component_ref: str, pin_number: str) -> Optional[Net]:
352
+ """
353
+ Get the electrical net connected to a specific pin.
354
+
355
+ Args:
356
+ component_ref: Component reference (e.g., "R1")
357
+ pin_number: Pin number
358
+
359
+ Returns:
360
+ Net object if pin is connected, None otherwise
361
+ """
362
+ self._ensure_connectivity()
363
+
364
+ if self._connectivity_analyzer:
365
+ return self._connectivity_analyzer.get_net_for_pin(component_ref, pin_number)
366
+
367
+ return None
368
+
369
+ def get_connected_pins(self, component_ref: str, pin_number: str) -> List[Tuple[str, str]]:
370
+ """
371
+ Get all pins electrically connected to a specific pin.
372
+
373
+ Args:
374
+ component_ref: Component reference (e.g., "R1")
375
+ pin_number: Pin number
376
+
377
+ Returns:
378
+ List of (reference, pin_number) tuples for all connected pins
379
+ """
380
+ self._ensure_connectivity()
381
+
382
+ if self._connectivity_analyzer:
383
+ return self._connectivity_analyzer.get_connected_pins(component_ref, pin_number)
384
+
385
+ return []
386
+
387
+ def _ensure_connectivity(self):
388
+ """
389
+ Ensure connectivity analysis is up-to-date.
390
+
391
+ Lazily initializes and runs connectivity analyzer on first query.
392
+ Re-runs analysis if schematic has changed since last analysis.
393
+ """
394
+ if not self._connectivity_valid:
395
+ logger.debug("Running connectivity analysis (hierarchical)...")
396
+ self._connectivity_analyzer = ConnectivityAnalyzer()
397
+ self._connectivity_analyzer.analyze(self._schematic, hierarchical=True)
398
+ self._connectivity_valid = True
399
+ logger.debug("Connectivity analysis complete")
400
+
401
+ def _invalidate_connectivity(self):
402
+ """
403
+ Invalidate connectivity cache.
404
+
405
+ Called when schematic changes that affect connectivity (wires, components, etc.).
406
+ Next connectivity query will trigger re-analysis.
407
+ """
408
+ if self._connectivity_valid:
409
+ logger.debug("Invalidating connectivity cache")
410
+ self._connectivity_valid = False
@@ -0,0 +1,63 @@
1
+ """
2
+ Utility functions for parsing S-expression data.
3
+
4
+ This module contains helper functions used by various parsers
5
+ to handle common parsing patterns safely and consistently.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ import sexpdata
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_bool_property(value: Any, default: bool = True) -> bool:
17
+ """
18
+ Parse a boolean property from S-expression data.
19
+
20
+ Handles both sexpdata.Symbol and string types, converting yes/no to bool.
21
+ This is the canonical way to parse boolean properties from KiCad files.
22
+
23
+ Args:
24
+ value: Value from S-expression (Symbol, str, bool, or None)
25
+ default: Default value if parsing fails or value is None
26
+
27
+ Returns:
28
+ bool: Parsed boolean value
29
+
30
+ Examples:
31
+ >>> parse_bool_property(sexpdata.Symbol('yes'))
32
+ True
33
+ >>> parse_bool_property('no')
34
+ False
35
+ >>> parse_bool_property(None, default=False)
36
+ False
37
+ >>> parse_bool_property('YES') # Case insensitive
38
+ True
39
+
40
+ Note:
41
+ This function was added to fix a critical bug where Symbol('yes') == 'yes'
42
+ returned False, causing properties like in_bom and on_board to be parsed
43
+ incorrectly.
44
+ """
45
+ # If value is None, use default
46
+ if value is None:
47
+ return default
48
+
49
+ # Convert Symbol to string
50
+ if isinstance(value, sexpdata.Symbol):
51
+ value = str(value)
52
+
53
+ # Handle string values (case-insensitive)
54
+ if isinstance(value, str):
55
+ return value.lower() == "yes"
56
+
57
+ # Handle boolean values directly
58
+ if isinstance(value, bool):
59
+ return value
60
+
61
+ # Unexpected type - use default
62
+ logger.warning(f"Unexpected type for boolean property: {type(value)}, using default={default}")
63
+ return default
@@ -66,14 +66,14 @@ def get_component_pin_position(component: SchematicSymbol, pin_number: str) -> O
66
66
 
67
67
  # Look for pin in symbol definition
68
68
  pins_found = []
69
- for pin_def in symbol_def.get("pins", []):
70
- pins_found.append(pin_def.get("number", "unknown"))
71
- if pin_def.get("number") == pin_number:
69
+ for pin_def in symbol_def.pins:
70
+ pins_found.append(pin_def.number)
71
+ if pin_def.number == pin_number:
72
72
  logger.info(f" Found pin {pin_number} in symbol definition")
73
73
 
74
74
  # Get pin position from definition
75
- pin_x = pin_def.get("x", 0)
76
- pin_y = pin_def.get("y", 0)
75
+ pin_x = pin_def.position.x
76
+ pin_y = pin_def.position.y
77
77
  logger.info(f" Symbol pin position: ({pin_x}, {pin_y})")
78
78
 
79
79
  # Apply component transformations
@@ -96,6 +96,100 @@ def get_component_pin_position(component: SchematicSymbol, pin_number: str) -> O
96
96
  return None
97
97
 
98
98
 
99
+ def get_component_pin_info(
100
+ component: SchematicSymbol, pin_number: str
101
+ ) -> Optional[Tuple[Point, float]]:
102
+ """
103
+ Get the absolute position and rotation of a component pin.
104
+
105
+ Args:
106
+ component: Component containing the pin
107
+ pin_number: Pin number to find
108
+
109
+ Returns:
110
+ Tuple of (absolute_position, absolute_rotation_degrees), or None if not found
111
+ """
112
+ logger.info(f"Getting pin info for {component.reference} pin {pin_number}")
113
+ logger.info(f" Component position: ({component.position.x}, {component.position.y})")
114
+ component_rotation = getattr(component, "rotation", 0)
115
+ logger.info(f" Component rotation: {component_rotation}°")
116
+ logger.info(f" Component mirror: {getattr(component, 'mirror', None)}")
117
+
118
+ # First check if pin is already in component data
119
+ for pin in component.pins:
120
+ if pin.number == pin_number:
121
+ logger.info(f" Found pin {pin_number} in component data")
122
+ logger.info(f" Pin relative position: ({pin.position.x}, {pin.position.y})")
123
+ logger.info(f" Pin rotation: {pin.rotation}°")
124
+
125
+ # Apply component transformations to position
126
+ absolute_pos = apply_transformation(
127
+ (pin.position.x, pin.position.y),
128
+ component.position,
129
+ component_rotation,
130
+ getattr(component, "mirror", None),
131
+ )
132
+
133
+ # Calculate absolute rotation (pin rotation + component rotation)
134
+ absolute_rotation = (pin.rotation + component_rotation) % 360
135
+
136
+ result_pos = Point(absolute_pos[0], absolute_pos[1])
137
+ logger.info(
138
+ f" Final absolute position: ({result_pos.x}, {result_pos.y}), rotation: {absolute_rotation}°"
139
+ )
140
+ return (result_pos, absolute_rotation)
141
+
142
+ # If not in component data, try to get from symbol library
143
+ logger.info(f" Pin {pin_number} not in component data, checking symbol library")
144
+
145
+ try:
146
+ symbol_cache = get_symbol_cache()
147
+ symbol_def = symbol_cache.get_symbol(component.lib_id)
148
+
149
+ if not symbol_def:
150
+ logger.warning(f" Symbol definition not found for {component.lib_id}")
151
+ return None
152
+
153
+ logger.info(f" Found symbol definition for {component.lib_id}")
154
+
155
+ # Look for pin in symbol definition
156
+ pins_found = []
157
+ for pin_def in symbol_def.pins:
158
+ pins_found.append(pin_def.number)
159
+ if pin_def.number == pin_number:
160
+ logger.info(f" Found pin {pin_number} in symbol definition")
161
+
162
+ # Get pin position and rotation from definition
163
+ pin_x = pin_def.position.x
164
+ pin_y = pin_def.position.y
165
+ pin_rotation = pin_def.rotation
166
+ logger.info(f" Symbol pin position: ({pin_x}, {pin_y}), rotation: {pin_rotation}°")
167
+
168
+ # Apply component transformations to position
169
+ absolute_pos = apply_transformation(
170
+ (pin_x, pin_y),
171
+ component.position,
172
+ component_rotation,
173
+ getattr(component, "mirror", None),
174
+ )
175
+
176
+ # Calculate absolute rotation
177
+ absolute_rotation = (pin_rotation + component_rotation) % 360
178
+
179
+ result_pos = Point(absolute_pos[0], absolute_pos[1])
180
+ logger.info(
181
+ f" Final absolute position: ({result_pos.x}, {result_pos.y}), rotation: {absolute_rotation}°"
182
+ )
183
+ return (result_pos, absolute_rotation)
184
+
185
+ logger.warning(f" Pin {pin_number} not found in symbol. Available pins: {pins_found}")
186
+
187
+ except Exception as e:
188
+ logger.error(f" Error accessing symbol cache: {e}")
189
+
190
+ return None
191
+
192
+
99
193
  def list_component_pins(component: SchematicSymbol) -> List[Tuple[str, Point]]:
100
194
  """
101
195
  List all pins for a component with their absolute positions.
@@ -127,10 +221,10 @@ def list_component_pins(component: SchematicSymbol) -> List[Tuple[str, Point]]:
127
221
  symbol_def = symbol_cache.get_symbol(component.lib_id)
128
222
 
129
223
  if symbol_def:
130
- for pin_def in symbol_def.get("pins", []):
131
- pin_number = pin_def.get("number")
132
- pin_x = pin_def.get("x", 0)
133
- pin_y = pin_def.get("y", 0)
224
+ for pin_def in symbol_def.pins:
225
+ pin_number = pin_def.number
226
+ pin_x = pin_def.position.x
227
+ pin_y = pin_def.position.y
134
228
 
135
229
  absolute_pos = apply_transformation(
136
230
  (pin_x, pin_y),