kicad-sch-api 0.3.5__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. kicad_sch_api/__init__.py +2 -2
  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/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/collections/__init__.py +2 -2
  10. kicad_sch_api/collections/base.py +5 -7
  11. kicad_sch_api/collections/components.py +24 -12
  12. kicad_sch_api/collections/junctions.py +31 -43
  13. kicad_sch_api/collections/labels.py +19 -27
  14. kicad_sch_api/collections/wires.py +17 -18
  15. kicad_sch_api/core/collections/__init__.py +5 -0
  16. kicad_sch_api/core/collections/base.py +248 -0
  17. kicad_sch_api/core/component_bounds.py +5 -0
  18. kicad_sch_api/core/components.py +67 -45
  19. kicad_sch_api/core/config.py +85 -3
  20. kicad_sch_api/core/factories/__init__.py +5 -0
  21. kicad_sch_api/core/factories/element_factory.py +276 -0
  22. kicad_sch_api/core/formatter.py +3 -1
  23. kicad_sch_api/core/junctions.py +26 -75
  24. kicad_sch_api/core/labels.py +29 -53
  25. kicad_sch_api/core/managers/__init__.py +26 -0
  26. kicad_sch_api/core/managers/file_io.py +244 -0
  27. kicad_sch_api/core/managers/format_sync.py +501 -0
  28. kicad_sch_api/core/managers/graphics.py +579 -0
  29. kicad_sch_api/core/managers/metadata.py +269 -0
  30. kicad_sch_api/core/managers/sheet.py +454 -0
  31. kicad_sch_api/core/managers/text_elements.py +536 -0
  32. kicad_sch_api/core/managers/validation.py +475 -0
  33. kicad_sch_api/core/managers/wire.py +352 -0
  34. kicad_sch_api/core/nets.py +38 -43
  35. kicad_sch_api/core/no_connects.py +33 -55
  36. kicad_sch_api/core/parser.py +75 -1731
  37. kicad_sch_api/core/schematic.py +951 -1192
  38. kicad_sch_api/core/texts.py +28 -55
  39. kicad_sch_api/core/types.py +60 -22
  40. kicad_sch_api/core/wires.py +27 -75
  41. kicad_sch_api/geometry/font_metrics.py +3 -1
  42. kicad_sch_api/geometry/symbol_bbox.py +40 -21
  43. kicad_sch_api/interfaces/__init__.py +1 -1
  44. kicad_sch_api/interfaces/parser.py +1 -1
  45. kicad_sch_api/interfaces/repository.py +1 -1
  46. kicad_sch_api/interfaces/resolver.py +1 -1
  47. kicad_sch_api/parsers/__init__.py +2 -2
  48. kicad_sch_api/parsers/base.py +7 -10
  49. kicad_sch_api/parsers/elements/__init__.py +22 -0
  50. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  51. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  52. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  53. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  54. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  55. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  56. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  57. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  58. kicad_sch_api/parsers/registry.py +4 -2
  59. kicad_sch_api/parsers/utils.py +80 -0
  60. kicad_sch_api/symbols/__init__.py +1 -1
  61. kicad_sch_api/symbols/cache.py +9 -12
  62. kicad_sch_api/symbols/resolver.py +20 -26
  63. kicad_sch_api/symbols/validators.py +188 -137
  64. kicad_sch_api/validation/__init__.py +25 -0
  65. kicad_sch_api/validation/erc.py +171 -0
  66. kicad_sch_api/validation/erc_models.py +203 -0
  67. kicad_sch_api/validation/pin_matrix.py +243 -0
  68. kicad_sch_api/validation/validators.py +391 -0
  69. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
  70. kicad_sch_api-0.4.1.dist-info/RECORD +87 -0
  71. kicad_sch_api/core/manhattan_routing.py +0 -430
  72. kicad_sch_api/core/simple_manhattan.py +0 -228
  73. kicad_sch_api/core/wire_routing.py +0 -380
  74. kicad_sch_api/parsers/label_parser.py +0 -254
  75. kicad_sch_api/parsers/symbol_parser.py +0 -227
  76. kicad_sch_api/parsers/wire_parser.py +0 -99
  77. kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
  78. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
  79. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
  80. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
  81. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/top_level.txt +0 -0
@@ -120,7 +120,9 @@ class SymbolBoundingBoxCalculator:
120
120
  if min_x == float("inf") or max_x == float("-inf"):
121
121
  raise ValueError(f"No valid geometry found in symbol data")
122
122
 
123
- logger.debug(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
123
+ logger.debug(
124
+ f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
125
+ )
124
126
  logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
125
127
 
126
128
  # Add small margin for text that might extend beyond shapes
@@ -252,7 +254,9 @@ class SymbolBoundingBoxCalculator:
252
254
  if min_x == float("inf") or max_x == float("-inf"):
253
255
  raise ValueError(f"No valid geometry found in symbol data")
254
256
 
255
- logger.debug(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
257
+ logger.debug(
258
+ f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
259
+ )
256
260
  logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
257
261
 
258
262
  # Add small margin for visual spacing
@@ -265,12 +269,16 @@ class SymbolBoundingBoxCalculator:
265
269
  # Add minimal space for component properties (Reference above, Value below)
266
270
  # Use adaptive spacing based on component height for better visual hierarchy
267
271
  component_height = max_y - min_y
268
- 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
269
275
  property_height = 1.27 # Reduced from 2.54mm
270
276
  min_y -= property_spacing + property_height # Reference above
271
277
  max_y += property_spacing + property_height # Value below
272
278
 
273
- logger.debug(f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
279
+ logger.debug(
280
+ f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
281
+ )
274
282
  logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
275
283
  logger.debug("=" * 50)
276
284
 
@@ -359,7 +367,10 @@ class SymbolBoundingBoxCalculator:
359
367
 
360
368
  @classmethod
361
369
  def get_symbol_dimensions(
362
- 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,
363
374
  ) -> Tuple[float, float]:
364
375
  """
365
376
  Get the width and height of a symbol.
@@ -488,24 +499,28 @@ class SymbolBoundingBoxCalculator:
488
499
  # If no net name match, use minimal fallback to avoid oversized bounding boxes
489
500
  if pin_net_map and pin_number in pin_net_map:
490
501
  label_text = pin_net_map[pin_number]
491
- logger.debug(f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}")
502
+ logger.debug(
503
+ f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}"
504
+ )
492
505
  else:
493
506
  # No net match - use minimal size (3 chars) instead of potentially long pin name
494
507
  label_text = "XXX" # 3-character placeholder for unmatched pins
495
- logger.debug(f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})")
508
+ logger.debug(
509
+ f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})"
510
+ )
496
511
 
497
512
  if label_text and label_text != "~": # ~ means no name
498
513
  # Calculate text dimensions
499
514
  # For horizontal text: width = char_count * char_width
500
515
  name_width = (
501
- len(label_text)
502
- * cls.DEFAULT_TEXT_HEIGHT
503
- * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
516
+ len(label_text) * cls.DEFAULT_TEXT_HEIGHT * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
504
517
  )
505
518
  # For vertical text: height = char_count * char_height (characters stack vertically)
506
519
  name_height = len(label_text) * cls.DEFAULT_TEXT_HEIGHT
507
520
 
508
- logger.debug(f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})")
521
+ logger.debug(
522
+ f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})"
523
+ )
509
524
 
510
525
  # Adjust bounds based on pin orientation
511
526
  # Labels are placed at PIN ENDPOINT with offset, extending AWAY from the component
@@ -515,32 +530,36 @@ class SymbolBoundingBoxCalculator:
515
530
 
516
531
  if angle == 0: # Pin points right - label extends LEFT from endpoint
517
532
  label_x = end_x - offset - name_width
518
- logger.debug(f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})")
533
+ logger.debug(
534
+ f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
535
+ )
519
536
  min_x = min(min_x, label_x)
520
537
  elif angle == 180: # Pin points left - label extends RIGHT from endpoint
521
538
  label_x = end_x + offset + name_width
522
- logger.debug(f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})")
539
+ logger.debug(
540
+ f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
541
+ )
523
542
  max_x = max(max_x, label_x)
524
543
  elif angle == 90: # Pin points up - label extends DOWN from endpoint
525
544
  label_y = end_y - offset - name_height
526
- logger.debug(f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})")
545
+ logger.debug(
546
+ f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
547
+ )
527
548
  min_y = min(min_y, label_y)
528
549
  elif angle == 270: # Pin points down - label extends UP from endpoint
529
550
  label_y = end_y + offset + name_height
530
- logger.debug(f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})")
551
+ logger.debug(
552
+ f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
553
+ )
531
554
  max_y = max(max_y, label_y)
532
555
 
533
556
  # Pin numbers are typically placed near the component body
534
557
  if pin_number:
535
558
  num_width = (
536
- len(pin_number)
537
- * cls.DEFAULT_PIN_NUMBER_SIZE
538
- * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
559
+ len(pin_number) * cls.DEFAULT_PIN_NUMBER_SIZE * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
539
560
  )
540
561
  # Add some space for the pin number
541
- margin = (
542
- cls.DEFAULT_PIN_NUMBER_SIZE * 1.5
543
- ) # Increase margin for better spacing
562
+ margin = cls.DEFAULT_PIN_NUMBER_SIZE * 1.5 # Increase margin for better spacing
544
563
  min_x -= margin
545
564
  min_y -= margin
546
565
  max_x += margin
@@ -14,4 +14,4 @@ __all__ = [
14
14
  "ISchematicParser",
15
15
  "ISchematicRepository",
16
16
  "ISymbolResolver",
17
- ]
17
+ ]
@@ -73,4 +73,4 @@ class ISchematicParser(Protocol):
73
73
  Raises:
74
74
  ParseError: If content format is invalid
75
75
  """
76
- ...
76
+ ...
@@ -67,4 +67,4 @@ class ISchematicRepository(Protocol):
67
67
  Raises:
68
68
  FileNotFoundError: If file doesn't exist
69
69
  """
70
- ...
70
+ ...
@@ -114,4 +114,4 @@ class ISymbolCache(Protocol):
114
114
  Returns:
115
115
  Dictionary with cache statistics (hits, misses, size, etc.)
116
116
  """
117
- ...
117
+ ...
@@ -5,10 +5,10 @@ This package provides specialized parsers for different types of KiCAD
5
5
  S-expression elements, organized by responsibility and testable in isolation.
6
6
  """
7
7
 
8
- from .registry import ElementParserRegistry
9
8
  from .base import BaseElementParser
9
+ from .registry import ElementParserRegistry
10
10
 
11
11
  __all__ = [
12
12
  "ElementParserRegistry",
13
13
  "BaseElementParser",
14
- ]
14
+ ]
@@ -92,10 +92,7 @@ class BaseElementParser(IElementParser):
92
92
  return None
93
93
 
94
94
  try:
95
- result = {
96
- "x": float(element[1]),
97
- "y": float(element[2])
98
- }
95
+ result = {"x": float(element[1]), "y": float(element[2])}
99
96
 
100
97
  # Optional angle parameter
101
98
  if len(element) > 3:
@@ -105,7 +102,9 @@ class BaseElementParser(IElementParser):
105
102
  except (ValueError, IndexError):
106
103
  return None
107
104
 
108
- def _extract_property_list(self, elements: List[Any], property_name: str) -> List[Dict[str, Any]]:
105
+ def _extract_property_list(
106
+ self, elements: List[Any], property_name: str
107
+ ) -> List[Dict[str, Any]]:
109
108
  """
110
109
  Extract all instances of a property from a list of elements.
111
110
 
@@ -118,9 +117,7 @@ class BaseElementParser(IElementParser):
118
117
  """
119
118
  properties = []
120
119
  for element in elements:
121
- if (isinstance(element, list) and
122
- len(element) > 0 and
123
- element[0] == property_name):
120
+ if isinstance(element, list) and len(element) > 0 and element[0] == property_name:
124
121
  prop = self._parse_property_element(element)
125
122
  if prop:
126
123
  properties.append(prop)
@@ -144,5 +141,5 @@ class BaseElementParser(IElementParser):
144
141
  return {
145
142
  "type": element[0],
146
143
  "value": element[1] if len(element) > 1 else None,
147
- "raw": element
148
- }
144
+ "raw": element,
145
+ }
@@ -0,0 +1,22 @@
1
+ """
2
+ Element-specific parsers for KiCAD schematic elements.
3
+
4
+ This module contains specialized parsers for different schematic element types,
5
+ extracted from the monolithic parser.py for better maintainability.
6
+ """
7
+
8
+ from .graphics_parser import GraphicsParser
9
+
10
+ # Additional element parsers will be imported here as they are created
11
+ # from .wire_parser import WireParser
12
+ # from .label_parser import LabelParser
13
+ # from .text_parser import TextParser
14
+ # from .sheet_parser import SheetParser
15
+ # from .library_parser import LibraryParser
16
+ # from .symbol_parser import SymbolParser
17
+ # from .metadata_parser import MetadataParser
18
+
19
+ __all__ = [
20
+ "GraphicsParser",
21
+ # Will be populated as parsers are added
22
+ ]