kicad-sch-api 0.3.4__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  7. kicad_sch_api/core/components.py +5 -0
  8. kicad_sch_api/core/formatter.py +3 -1
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/managers/__init__.py +26 -0
  11. kicad_sch_api/core/managers/file_io.py +243 -0
  12. kicad_sch_api/core/managers/format_sync.py +501 -0
  13. kicad_sch_api/core/managers/graphics.py +579 -0
  14. kicad_sch_api/core/managers/metadata.py +268 -0
  15. kicad_sch_api/core/managers/sheet.py +454 -0
  16. kicad_sch_api/core/managers/text_elements.py +536 -0
  17. kicad_sch_api/core/managers/validation.py +474 -0
  18. kicad_sch_api/core/managers/wire.py +346 -0
  19. kicad_sch_api/core/nets.py +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
  42. kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
  43. kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ Text element management for KiCAD schematics.
3
+
4
+ This module provides collection classes for managing text elements,
5
+ featuring fast lookup, bulk operations, and validation.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
+
12
+ from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
+ from .types import Point, Text
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class TextElement:
19
+ """
20
+ Enhanced wrapper for schematic text elements with modern API.
21
+
22
+ Provides intuitive access to text properties and operations
23
+ while maintaining exact format preservation.
24
+ """
25
+
26
+ def __init__(self, text_data: Text, parent_collection: "TextCollection"):
27
+ """
28
+ Initialize text element wrapper.
29
+
30
+ Args:
31
+ text_data: Underlying text data
32
+ parent_collection: Parent collection for updates
33
+ """
34
+ self._data = text_data
35
+ self._collection = parent_collection
36
+ self._validator = SchematicValidator()
37
+
38
+ # Core properties with validation
39
+ @property
40
+ def uuid(self) -> str:
41
+ """Text element UUID."""
42
+ return self._data.uuid
43
+
44
+ @property
45
+ def text(self) -> str:
46
+ """Text content."""
47
+ return self._data.text
48
+
49
+ @text.setter
50
+ def text(self, value: str):
51
+ """Set text content with validation."""
52
+ if not isinstance(value, str):
53
+ raise ValidationError(f"Text content must be string, got {type(value)}")
54
+ self._data.text = value
55
+ self._collection._mark_modified()
56
+
57
+ @property
58
+ def position(self) -> Point:
59
+ """Text position."""
60
+ return self._data.position
61
+
62
+ @position.setter
63
+ def position(self, value: Union[Point, Tuple[float, float]]):
64
+ """Set text position."""
65
+ if isinstance(value, tuple):
66
+ value = Point(value[0], value[1])
67
+ elif not isinstance(value, Point):
68
+ raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
69
+ self._data.position = value
70
+ self._collection._mark_modified()
71
+
72
+ @property
73
+ def rotation(self) -> float:
74
+ """Text rotation in degrees."""
75
+ return self._data.rotation
76
+
77
+ @rotation.setter
78
+ def rotation(self, value: float):
79
+ """Set text rotation."""
80
+ self._data.rotation = float(value)
81
+ self._collection._mark_modified()
82
+
83
+ @property
84
+ def size(self) -> float:
85
+ """Text size."""
86
+ return self._data.size
87
+
88
+ @size.setter
89
+ def size(self, value: float):
90
+ """Set text size with validation."""
91
+ if value <= 0:
92
+ raise ValidationError(f"Text size must be positive, got {value}")
93
+ self._data.size = float(value)
94
+ self._collection._mark_modified()
95
+
96
+ @property
97
+ def exclude_from_sim(self) -> bool:
98
+ """Whether text is excluded from simulation."""
99
+ return self._data.exclude_from_sim
100
+
101
+ @exclude_from_sim.setter
102
+ def exclude_from_sim(self, value: bool):
103
+ """Set exclude from simulation flag."""
104
+ self._data.exclude_from_sim = bool(value)
105
+ self._collection._mark_modified()
106
+
107
+ def validate(self) -> List[ValidationIssue]:
108
+ """Validate this text element."""
109
+ return self._validator.validate_text(self._data.__dict__)
110
+
111
+ def to_dict(self) -> Dict[str, Any]:
112
+ """Convert text element to dictionary representation."""
113
+ return {
114
+ "uuid": self.uuid,
115
+ "text": self.text,
116
+ "position": {"x": self.position.x, "y": self.position.y},
117
+ "rotation": self.rotation,
118
+ "size": self.size,
119
+ "exclude_from_sim": self.exclude_from_sim,
120
+ }
121
+
122
+ def __str__(self) -> str:
123
+ """String representation."""
124
+ return f"<Text '{self.text}' @ {self.position}>"
125
+
126
+
127
+ class TextCollection:
128
+ """
129
+ Collection class for efficient text element management.
130
+
131
+ Provides fast lookup, filtering, and bulk operations for schematic text elements.
132
+ """
133
+
134
+ def __init__(self, texts: List[Text] = None):
135
+ """
136
+ Initialize text collection.
137
+
138
+ Args:
139
+ texts: Initial list of text data
140
+ """
141
+ self._texts: List[TextElement] = []
142
+ self._uuid_index: Dict[str, TextElement] = {}
143
+ self._content_index: Dict[str, List[TextElement]] = {}
144
+ self._modified = False
145
+
146
+ # Add initial texts
147
+ if texts:
148
+ for text_data in texts:
149
+ self._add_to_indexes(TextElement(text_data, self))
150
+
151
+ logger.debug(f"TextCollection initialized with {len(self._texts)} texts")
152
+
153
+ def add(
154
+ self,
155
+ text: str,
156
+ position: Union[Point, Tuple[float, float]],
157
+ rotation: float = 0.0,
158
+ size: float = 1.27,
159
+ exclude_from_sim: bool = False,
160
+ text_uuid: Optional[str] = None,
161
+ ) -> TextElement:
162
+ """
163
+ Add a new text element to the schematic.
164
+
165
+ Args:
166
+ text: Text content
167
+ position: Text position
168
+ rotation: Text rotation in degrees
169
+ size: Text size
170
+ exclude_from_sim: Whether to exclude from simulation
171
+ text_uuid: Specific UUID for text (auto-generated if None)
172
+
173
+ Returns:
174
+ Newly created TextElement
175
+
176
+ Raises:
177
+ ValidationError: If text data is invalid
178
+ """
179
+ # Validate inputs
180
+ if not isinstance(text, str) or not text.strip():
181
+ raise ValidationError("Text content cannot be empty")
182
+
183
+ if isinstance(position, tuple):
184
+ position = Point(position[0], position[1])
185
+ elif not isinstance(position, Point):
186
+ raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
187
+
188
+ if size <= 0:
189
+ raise ValidationError(f"Text size must be positive, got {size}")
190
+
191
+ # Generate UUID if not provided
192
+ if not text_uuid:
193
+ text_uuid = str(uuid.uuid4())
194
+
195
+ # Check for duplicate UUID
196
+ if text_uuid in self._uuid_index:
197
+ raise ValidationError(f"Text UUID {text_uuid} already exists")
198
+
199
+ # Create text data
200
+ text_data = Text(
201
+ uuid=text_uuid,
202
+ position=position,
203
+ text=text,
204
+ rotation=rotation,
205
+ size=size,
206
+ exclude_from_sim=exclude_from_sim,
207
+ )
208
+
209
+ # Create wrapper and add to collection
210
+ text_element = TextElement(text_data, self)
211
+ self._add_to_indexes(text_element)
212
+ self._mark_modified()
213
+
214
+ logger.debug(f"Added text: {text_element}")
215
+ return text_element
216
+
217
+ def get(self, text_uuid: str) -> Optional[TextElement]:
218
+ """Get text by UUID."""
219
+ return self._uuid_index.get(text_uuid)
220
+
221
+ def remove(self, text_uuid: str) -> bool:
222
+ """
223
+ Remove text by UUID.
224
+
225
+ Args:
226
+ text_uuid: UUID of text to remove
227
+
228
+ Returns:
229
+ True if text was removed, False if not found
230
+ """
231
+ text_element = self._uuid_index.get(text_uuid)
232
+ if not text_element:
233
+ return False
234
+
235
+ # Remove from indexes
236
+ self._remove_from_indexes(text_element)
237
+ self._mark_modified()
238
+
239
+ logger.debug(f"Removed text: {text_element}")
240
+ return True
241
+
242
+ def find_by_content(self, content: str, exact: bool = True) -> List[TextElement]:
243
+ """
244
+ Find texts by content.
245
+
246
+ Args:
247
+ content: Content to search for
248
+ exact: If True, exact match; if False, substring match
249
+
250
+ Returns:
251
+ List of matching text elements
252
+ """
253
+ if exact:
254
+ return self._content_index.get(content, []).copy()
255
+ else:
256
+ matches = []
257
+ for text_element in self._texts:
258
+ if content.lower() in text_element.text.lower():
259
+ matches.append(text_element)
260
+ return matches
261
+
262
+ def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
263
+ """
264
+ Filter texts by predicate function.
265
+
266
+ Args:
267
+ predicate: Function that returns True for texts to include
268
+
269
+ Returns:
270
+ List of texts matching predicate
271
+ """
272
+ return [text for text in self._texts if predicate(text)]
273
+
274
+ def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
275
+ """
276
+ Update multiple texts matching criteria.
277
+
278
+ Args:
279
+ criteria: Function to select texts to update
280
+ updates: Dictionary of property updates
281
+ """
282
+ updated_count = 0
283
+ for text_element in self._texts:
284
+ if criteria(text_element):
285
+ for prop, value in updates.items():
286
+ if hasattr(text_element, prop):
287
+ setattr(text_element, prop, value)
288
+ updated_count += 1
289
+
290
+ if updated_count > 0:
291
+ self._mark_modified()
292
+ logger.debug(f"Bulk updated {updated_count} text properties")
293
+
294
+ def clear(self):
295
+ """Remove all texts from collection."""
296
+ self._texts.clear()
297
+ self._uuid_index.clear()
298
+ self._content_index.clear()
299
+ self._mark_modified()
300
+
301
+ def _add_to_indexes(self, text_element: TextElement):
302
+ """Add text to internal indexes."""
303
+ self._texts.append(text_element)
304
+ self._uuid_index[text_element.uuid] = text_element
305
+
306
+ # Add to content index
307
+ content = text_element.text
308
+ if content not in self._content_index:
309
+ self._content_index[content] = []
310
+ self._content_index[content].append(text_element)
311
+
312
+ def _remove_from_indexes(self, text_element: TextElement):
313
+ """Remove text from internal indexes."""
314
+ self._texts.remove(text_element)
315
+ del self._uuid_index[text_element.uuid]
316
+
317
+ # Remove from content index
318
+ content = text_element.text
319
+ if content in self._content_index:
320
+ self._content_index[content].remove(text_element)
321
+ if not self._content_index[content]:
322
+ del self._content_index[content]
323
+
324
+ def _mark_modified(self):
325
+ """Mark collection as modified."""
326
+ self._modified = True
327
+
328
+ # Collection interface methods
329
+ def __len__(self) -> int:
330
+ """Return number of texts."""
331
+ return len(self._texts)
332
+
333
+ def __iter__(self) -> Iterator[TextElement]:
334
+ """Iterate over texts."""
335
+ return iter(self._texts)
336
+
337
+ def __getitem__(self, index: int) -> TextElement:
338
+ """Get text by index."""
339
+ return self._texts[index]
340
+
341
+ def __bool__(self) -> bool:
342
+ """Return True if collection has texts."""
343
+ return len(self._texts) > 0
@@ -366,10 +366,7 @@ class SchematicRectangle:
366
366
  @property
367
367
  def center(self) -> Point:
368
368
  """Rectangle center point."""
369
- return Point(
370
- (self.start.x + self.end.x) / 2,
371
- (self.start.y + self.end.y) / 2
372
- )
369
+ return Point((self.start.x + self.end.x) / 2, (self.start.y + self.end.y) / 2)
373
370
 
374
371
 
375
372
  @dataclass
@@ -386,6 +383,18 @@ class Image:
386
383
  self.uuid = str(uuid4())
387
384
 
388
385
 
386
+ @dataclass
387
+ class NoConnect:
388
+ """No-connect symbol in schematic."""
389
+
390
+ uuid: str
391
+ position: Point
392
+
393
+ def __post_init__(self):
394
+ if not self.uuid:
395
+ self.uuid = str(uuid4())
396
+
397
+
389
398
  @dataclass
390
399
  class Net:
391
400
  """Electrical net connecting components."""
@@ -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 = 0.65 # Width to height ratio for pin text (proportional font average)
20
+ DEFAULT_PIN_TEXT_WIDTH_RATIO = (
21
+ 0.65 # Width to height ratio for pin text (proportional font average)
22
+ )
@@ -59,12 +59,9 @@ class SymbolBoundingBoxCalculator:
59
59
  if not symbol_data:
60
60
  raise ValueError("Symbol data is None or empty")
61
61
 
62
- import sys
63
- # Reduced logging frequency - only log if DEBUG environment variable is set
64
- debug_enabled = os.getenv("CIRCUIT_SYNTH_DEBUG", "").lower() == "true"
65
- if debug_enabled:
66
- print(f"\n=== CALCULATING BOUNDING BOX ===", file=sys.stderr, flush=True)
67
- print(f"include_properties={include_properties}", file=sys.stderr, flush=True)
62
+ # Use proper logging instead of print statements
63
+ logger.debug("=== CALCULATING BOUNDING BOX ===")
64
+ logger.debug(f"include_properties={include_properties}")
68
65
 
69
66
  min_x = float("inf")
70
67
  min_y = float("inf")
@@ -73,7 +70,7 @@ class SymbolBoundingBoxCalculator:
73
70
 
74
71
  # Process main symbol shapes (handle both 'shapes' and 'graphics' keys)
75
72
  shapes = symbol_data.get("shapes", []) or symbol_data.get("graphics", [])
76
- print(f"Processing {len(shapes)} main shapes", file=sys.stderr, flush=True)
73
+ logger.debug(f"Processing {len(shapes)} main shapes")
77
74
  for shape in shapes:
78
75
  shape_bounds = cls._get_shape_bounds(shape)
79
76
  if shape_bounds:
@@ -85,7 +82,7 @@ class SymbolBoundingBoxCalculator:
85
82
 
86
83
  # Process pins (including their labels)
87
84
  pins = symbol_data.get("pins", [])
88
- print(f"Processing {len(pins)} main pins", file=sys.stderr, flush=True)
85
+ logger.debug(f"Processing {len(pins)} main pins")
89
86
  for pin in pins:
90
87
  pin_bounds = cls._get_pin_bounds(pin, pin_net_map)
91
88
  if pin_bounds:
@@ -123,8 +120,10 @@ class SymbolBoundingBoxCalculator:
123
120
  if min_x == float("inf") or max_x == float("-inf"):
124
121
  raise ValueError(f"No valid geometry found in symbol data")
125
122
 
126
- print(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
127
- print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
123
+ logger.debug(
124
+ f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
125
+ )
126
+ logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
128
127
 
129
128
  # Add small margin for text that might extend beyond shapes
130
129
  margin = 0.254 # 10 mils
@@ -162,9 +161,9 @@ class SymbolBoundingBoxCalculator:
162
161
  f"Calculated bounding box: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
163
162
  )
164
163
 
165
- print(f"FINAL BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
166
- print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
167
- print("=" * 50 + "\n", file=sys.stderr, flush=True)
164
+ logger.debug(f"FINAL BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
165
+ logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
166
+ logger.debug("=" * 50)
168
167
 
169
168
  return (min_x, min_y, max_x, max_y)
170
169
 
@@ -196,8 +195,7 @@ class SymbolBoundingBoxCalculator:
196
195
  if not symbol_data:
197
196
  raise ValueError("Symbol data is None or empty")
198
197
 
199
- import sys
200
- print(f"\n=== CALCULATING PLACEMENT BOUNDING BOX (NO PIN LABELS) ===", file=sys.stderr, flush=True)
198
+ logger.debug("=== CALCULATING PLACEMENT BOUNDING BOX (NO PIN LABELS) ===")
201
199
 
202
200
  min_x = float("inf")
203
201
  min_y = float("inf")
@@ -206,7 +204,7 @@ class SymbolBoundingBoxCalculator:
206
204
 
207
205
  # Process main symbol shapes (handle both 'shapes' and 'graphics' keys)
208
206
  shapes = symbol_data.get("shapes", []) or symbol_data.get("graphics", [])
209
- print(f"Processing {len(shapes)} main shapes", file=sys.stderr, flush=True)
207
+ logger.debug(f"Processing {len(shapes)} main shapes")
210
208
  for shape in shapes:
211
209
  shape_bounds = cls._get_shape_bounds(shape)
212
210
  if shape_bounds:
@@ -218,7 +216,7 @@ class SymbolBoundingBoxCalculator:
218
216
 
219
217
  # Process pins WITHOUT labels (just pin endpoints)
220
218
  pins = symbol_data.get("pins", [])
221
- print(f"Processing {len(pins)} main pins (NO LABELS)", file=sys.stderr, flush=True)
219
+ logger.debug(f"Processing {len(pins)} main pins (NO LABELS)")
222
220
  for pin in pins:
223
221
  pin_bounds = cls._get_pin_bounds_no_labels(pin)
224
222
  if pin_bounds:
@@ -256,8 +254,10 @@ class SymbolBoundingBoxCalculator:
256
254
  if min_x == float("inf") or max_x == float("-inf"):
257
255
  raise ValueError(f"No valid geometry found in symbol data")
258
256
 
259
- print(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
260
- print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
257
+ logger.debug(
258
+ f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
259
+ )
260
+ logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
261
261
 
262
262
  # Add small margin for visual spacing
263
263
  margin = 0.635 # 25mil margin (reduced from 50mil)
@@ -269,14 +269,18 @@ class SymbolBoundingBoxCalculator:
269
269
  # Add minimal space for component properties (Reference above, Value below)
270
270
  # Use adaptive spacing based on component height for better visual hierarchy
271
271
  component_height = max_y - min_y
272
- property_spacing = max(3.0, component_height * 0.15) # Adaptive: minimum 3mm or 15% of height
272
+ property_spacing = max(
273
+ 3.0, component_height * 0.15
274
+ ) # Adaptive: minimum 3mm or 15% of height
273
275
  property_height = 1.27 # Reduced from 2.54mm
274
276
  min_y -= property_spacing + property_height # Reference above
275
277
  max_y += property_spacing + property_height # Value below
276
278
 
277
- print(f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
278
- print(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}", file=sys.stderr, flush=True)
279
- print("=" * 50 + "\n", file=sys.stderr, flush=True)
279
+ logger.debug(
280
+ f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
281
+ )
282
+ logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
283
+ logger.debug("=" * 50)
280
284
 
281
285
  return (min_x, min_y, max_x, max_y)
282
286
 
@@ -363,7 +367,10 @@ class SymbolBoundingBoxCalculator:
363
367
 
364
368
  @classmethod
365
369
  def get_symbol_dimensions(
366
- cls, symbol_data: Dict[str, Any], include_properties: bool = True, pin_net_map: Optional[Dict[str, str]] = None
370
+ cls,
371
+ symbol_data: Dict[str, Any],
372
+ include_properties: bool = True,
373
+ pin_net_map: Optional[Dict[str, str]] = None,
367
374
  ) -> Tuple[float, float]:
368
375
  """
369
376
  Get the width and height of a symbol.
@@ -459,7 +466,6 @@ class SymbolBoundingBoxCalculator:
459
466
  cls, pin: Dict[str, Any], pin_net_map: Optional[Dict[str, str]] = None
460
467
  ) -> Optional[Tuple[float, float, float, float]]:
461
468
  """Get bounding box for a pin including its labels."""
462
- import sys
463
469
 
464
470
  # Handle both formats: 'at' array or separate x/y/orientation
465
471
  if "at" in pin:
@@ -493,24 +499,28 @@ class SymbolBoundingBoxCalculator:
493
499
  # If no net name match, use minimal fallback to avoid oversized bounding boxes
494
500
  if pin_net_map and pin_number in pin_net_map:
495
501
  label_text = pin_net_map[pin_number]
496
- print(f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}", file=sys.stderr, flush=True)
502
+ logger.debug(
503
+ f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}"
504
+ )
497
505
  else:
498
506
  # No net match - use minimal size (3 chars) instead of potentially long pin name
499
507
  label_text = "XXX" # 3-character placeholder for unmatched pins
500
- print(f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})", file=sys.stderr, flush=True)
508
+ logger.debug(
509
+ f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})"
510
+ )
501
511
 
502
512
  if label_text and label_text != "~": # ~ means no name
503
513
  # Calculate text dimensions
504
514
  # For horizontal text: width = char_count * char_width
505
515
  name_width = (
506
- len(label_text)
507
- * cls.DEFAULT_TEXT_HEIGHT
508
- * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
516
+ len(label_text) * cls.DEFAULT_TEXT_HEIGHT * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
509
517
  )
510
518
  # For vertical text: height = char_count * char_height (characters stack vertically)
511
519
  name_height = len(label_text) * cls.DEFAULT_TEXT_HEIGHT
512
520
 
513
- print(f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})", file=sys.stderr, flush=True)
521
+ logger.debug(
522
+ f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})"
523
+ )
514
524
 
515
525
  # Adjust bounds based on pin orientation
516
526
  # Labels are placed at PIN ENDPOINT with offset, extending AWAY from the component
@@ -520,38 +530,42 @@ class SymbolBoundingBoxCalculator:
520
530
 
521
531
  if angle == 0: # Pin points right - label extends LEFT from endpoint
522
532
  label_x = end_x - offset - name_width
523
- print(f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
533
+ logger.debug(
534
+ f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
535
+ )
524
536
  min_x = min(min_x, label_x)
525
537
  elif angle == 180: # Pin points left - label extends RIGHT from endpoint
526
538
  label_x = end_x + offset + name_width
527
- print(f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
539
+ logger.debug(
540
+ f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
541
+ )
528
542
  max_x = max(max_x, label_x)
529
543
  elif angle == 90: # Pin points up - label extends DOWN from endpoint
530
544
  label_y = end_y - offset - name_height
531
- print(f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
545
+ logger.debug(
546
+ f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
547
+ )
532
548
  min_y = min(min_y, label_y)
533
549
  elif angle == 270: # Pin points down - label extends UP from endpoint
534
550
  label_y = end_y + offset + name_height
535
- print(f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})", file=sys.stderr, flush=True)
551
+ logger.debug(
552
+ f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
553
+ )
536
554
  max_y = max(max_y, label_y)
537
555
 
538
556
  # Pin numbers are typically placed near the component body
539
557
  if pin_number:
540
558
  num_width = (
541
- len(pin_number)
542
- * cls.DEFAULT_PIN_NUMBER_SIZE
543
- * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
559
+ len(pin_number) * cls.DEFAULT_PIN_NUMBER_SIZE * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
544
560
  )
545
561
  # Add some space for the pin number
546
- margin = (
547
- cls.DEFAULT_PIN_NUMBER_SIZE * 1.5
548
- ) # Increase margin for better spacing
562
+ margin = cls.DEFAULT_PIN_NUMBER_SIZE * 1.5 # Increase margin for better spacing
549
563
  min_x -= margin
550
564
  min_y -= margin
551
565
  max_x += margin
552
566
  max_y += margin
553
567
 
554
- print(f" Pin bounds: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})", file=sys.stderr, flush=True)
568
+ logger.debug(f" Pin bounds: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
555
569
  return (min_x, min_y, max_x, max_y)
556
570
 
557
571
  @classmethod
@@ -559,7 +573,6 @@ class SymbolBoundingBoxCalculator:
559
573
  cls, pin: Dict[str, Any]
560
574
  ) -> Optional[Tuple[float, float, float, float]]:
561
575
  """Get bounding box for a pin WITHOUT labels - for placement calculations only."""
562
- import sys
563
576
 
564
577
  # Handle both formats: 'at' array or separate x/y/orientation
565
578
  if "at" in pin:
@@ -0,0 +1,17 @@
1
+ """
2
+ Core interfaces for KiCAD schematic API.
3
+
4
+ This module provides abstract interfaces for the main components of the system,
5
+ enabling better separation of concerns and testability.
6
+ """
7
+
8
+ from .parser import IElementParser, ISchematicParser
9
+ from .repository import ISchematicRepository
10
+ from .resolver import ISymbolResolver
11
+
12
+ __all__ = [
13
+ "IElementParser",
14
+ "ISchematicParser",
15
+ "ISchematicRepository",
16
+ "ISymbolResolver",
17
+ ]