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
@@ -11,6 +11,8 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..library.cache import SymbolDefinition, get_symbol_cache
13
13
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
14
+ from .collections import BaseCollection
15
+ from .exceptions import LibraryError
14
16
  from .ic_manager import ICManager
15
17
  from .types import Point, SchematicPin, SchematicSymbol
16
18
 
@@ -38,6 +40,11 @@ class Component:
38
40
  self._validator = SchematicValidator()
39
41
 
40
42
  # Core properties with validation
43
+ @property
44
+ def uuid(self) -> str:
45
+ """Component UUID."""
46
+ return self._data.uuid
47
+
41
48
  @property
42
49
  def reference(self) -> str:
43
50
  """Component reference (e.g., 'R1')."""
@@ -100,8 +107,29 @@ class Component:
100
107
 
101
108
  @rotation.setter
102
109
  def rotation(self, value: float):
103
- """Set component rotation."""
104
- self._data.rotation = float(value) % 360
110
+ """Set component rotation (must be 0, 90, 180, or 270 degrees).
111
+
112
+ KiCad only supports these four rotation angles for components.
113
+
114
+ Args:
115
+ value: Rotation angle in degrees (0, 90, 180, or 270)
116
+
117
+ Raises:
118
+ ValueError: If rotation is not 0, 90, 180, or 270
119
+ """
120
+ # Normalize rotation to 0-360 range
121
+ normalized = float(value) % 360
122
+
123
+ # KiCad only accepts 0, 90, 180, or 270 degrees
124
+ VALID_ROTATIONS = {0, 90, 180, 270}
125
+ if normalized not in VALID_ROTATIONS:
126
+ raise ValueError(
127
+ f"Component rotation must be 0, 90, 180, or 270 degrees. "
128
+ f"Got {value}° (normalized to {normalized}°). "
129
+ f"KiCad does not support arbitrary rotation angles."
130
+ )
131
+
132
+ self._data.rotation = normalized
105
133
  self._collection._mark_modified()
106
134
 
107
135
  @property
@@ -256,34 +284,41 @@ class Component:
256
284
  )
257
285
 
258
286
 
259
- class ComponentCollection:
287
+ class ComponentCollection(BaseCollection[Component]):
260
288
  """
261
289
  Collection class for efficient component management.
262
290
 
291
+ Inherits from BaseCollection for standard operations and adds component-specific
292
+ functionality including reference, lib_id, and value-based indexing.
293
+
263
294
  Provides fast lookup, filtering, and bulk operations for schematic components.
264
295
  Optimized for schematics with hundreds or thousands of components.
265
296
  """
266
297
 
267
- def __init__(self, components: List[SchematicSymbol] = None):
298
+ def __init__(self, components: List[SchematicSymbol] = None, parent_schematic=None):
268
299
  """
269
300
  Initialize component collection.
270
301
 
271
302
  Args:
272
303
  components: Initial list of component data
304
+ parent_schematic: Reference to parent Schematic object (for hierarchy context)
273
305
  """
274
- self._components: List[Component] = []
306
+ # Initialize base collection
307
+ super().__init__([], collection_name="components")
308
+
309
+ # Additional component-specific indexes
275
310
  self._reference_index: Dict[str, Component] = {}
276
311
  self._lib_id_index: Dict[str, List[Component]] = {}
277
312
  self._value_index: Dict[str, List[Component]] = {}
278
- self._modified = False
313
+
314
+ # Store reference to parent schematic for hierarchy context
315
+ self._parent_schematic = parent_schematic
279
316
 
280
317
  # Add initial components
281
318
  if components:
282
319
  for comp_data in components:
283
320
  self._add_to_indexes(Component(comp_data, self))
284
321
 
285
- logger.debug(f"ComponentCollection initialized with {len(self._components)} components")
286
-
287
322
  def add(
288
323
  self,
289
324
  lib_id: str,
@@ -292,6 +327,7 @@ class ComponentCollection:
292
327
  position: Optional[Union[Point, Tuple[float, float]]] = None,
293
328
  footprint: Optional[str] = None,
294
329
  unit: int = 1,
330
+ rotation: float = 0.0,
295
331
  component_uuid: Optional[str] = None,
296
332
  **properties,
297
333
  ) -> Component:
@@ -305,6 +341,7 @@ class ComponentCollection:
305
341
  position: Component position (auto-placed if None)
306
342
  footprint: Component footprint
307
343
  unit: Unit number for multi-unit components (1-based)
344
+ rotation: Component rotation in degrees (0, 90, 180, 270)
308
345
  component_uuid: Specific UUID for component (auto-generated if None)
309
346
  **properties: Additional component properties
310
347
 
@@ -313,6 +350,7 @@ class ComponentCollection:
313
350
 
314
351
  Raises:
315
352
  ValidationError: If component data is invalid
353
+ LibraryError: If the KiCAD symbol library is not found
316
354
  """
317
355
  # Validate lib_id
318
356
  validator = SchematicValidator()
@@ -347,6 +385,28 @@ class ComponentCollection:
347
385
  f"Component {reference} position snapped to grid: ({position.x:.3f}, {position.y:.3f})"
348
386
  )
349
387
 
388
+ # Normalize and validate rotation
389
+ rotation = rotation % 360
390
+
391
+ # KiCad only accepts 0, 90, 180, or 270 degrees
392
+ VALID_ROTATIONS = {0, 90, 180, 270}
393
+ if rotation not in VALID_ROTATIONS:
394
+ raise ValidationError(
395
+ f"Component rotation must be 0, 90, 180, or 270 degrees. "
396
+ f"Got {rotation}°. KiCad does not support arbitrary rotation angles."
397
+ )
398
+
399
+ # Check if parent schematic has hierarchy context set
400
+ # If so, add hierarchy_path to properties for proper KiCad instance paths
401
+ if self._parent_schematic and hasattr(self._parent_schematic, '_hierarchy_path'):
402
+ if self._parent_schematic._hierarchy_path:
403
+ properties = dict(properties) # Make a copy to avoid modifying caller's dict
404
+ properties['hierarchy_path'] = self._parent_schematic._hierarchy_path
405
+ logger.debug(
406
+ f"Setting hierarchy_path for component {reference}: "
407
+ f"{self._parent_schematic._hierarchy_path}"
408
+ )
409
+
350
410
  # Create component data
351
411
  component_data = SchematicSymbol(
352
412
  uuid=component_uuid if component_uuid else str(uuid.uuid4()),
@@ -356,21 +416,31 @@ class ComponentCollection:
356
416
  value=value,
357
417
  footprint=footprint,
358
418
  unit=unit,
419
+ rotation=rotation,
359
420
  properties=properties,
360
421
  )
361
422
 
362
423
  # Get symbol definition and update pins
363
424
  symbol_cache = get_symbol_cache()
364
425
  symbol_def = symbol_cache.get_symbol(lib_id)
365
- if symbol_def:
366
- component_data.pins = symbol_def.pins.copy()
426
+ if not symbol_def:
427
+ # Provide helpful error message with library name
428
+ library_name = lib_id.split(":")[0] if ":" in lib_id else "unknown"
429
+ raise LibraryError(
430
+ f"Symbol '{lib_id}' not found in KiCAD libraries. "
431
+ f"Please verify the library name '{library_name}' and symbol name are correct. "
432
+ f"Common libraries include: Device, Connector_Generic, Regulator_Linear, RF_Module",
433
+ field="lib_id",
434
+ value=lib_id
435
+ )
436
+ component_data.pins = symbol_def.pins.copy()
367
437
 
368
438
  # Create component wrapper
369
439
  component = Component(component_data, self)
370
440
 
371
441
  # Add to collection
372
442
  self._add_to_indexes(component)
373
- self._modified = True
443
+ self._mark_modified()
374
444
 
375
445
  logger.info(f"Added component: {reference} ({lib_id})")
376
446
  return component
@@ -427,7 +497,7 @@ class ComponentCollection:
427
497
  component = Component(component_data, self)
428
498
  self._add_to_indexes(component)
429
499
 
430
- self._modified = True
500
+ self._mark_modified()
431
501
  logger.info(
432
502
  f"Added multi-unit IC: {reference_prefix} ({lib_id}) with {len(unit_components)} units"
433
503
  )
@@ -439,22 +509,102 @@ class ComponentCollection:
439
509
  Remove component by reference.
440
510
 
441
511
  Args:
442
- reference: Component reference to remove
512
+ reference: Component reference to remove (e.g., "R1")
443
513
 
444
514
  Returns:
445
- True if component was removed
515
+ True if component was removed, False if not found
516
+
517
+ Raises:
518
+ TypeError: If reference is not a string
519
+
520
+ Examples:
521
+ sch.components.remove("R1")
522
+ sch.components.remove("C2")
523
+
524
+ Note:
525
+ For removing by UUID or component object, use remove_by_uuid() or remove_component()
526
+ respectively. This maintains a clear, simple API contract.
446
527
  """
528
+ if not isinstance(reference, str):
529
+ raise TypeError(f"reference must be a string, not {type(reference).__name__}")
530
+
447
531
  component = self._reference_index.get(reference)
448
532
  if not component:
449
533
  return False
450
534
 
451
- # Remove from all indexes
535
+ # Remove from component-specific indexes
452
536
  self._remove_from_indexes(component)
453
- self._modified = True
537
+
538
+ # Remove from base collection using UUID
539
+ super().remove(component.uuid)
454
540
 
455
541
  logger.info(f"Removed component: {reference}")
456
542
  return True
457
543
 
544
+ def remove_by_uuid(self, component_uuid: str) -> bool:
545
+ """
546
+ Remove component by UUID.
547
+
548
+ Args:
549
+ component_uuid: Component UUID to remove
550
+
551
+ Returns:
552
+ True if component was removed, False if not found
553
+
554
+ Raises:
555
+ TypeError: If UUID is not a string
556
+ """
557
+ if not isinstance(component_uuid, str):
558
+ raise TypeError(f"component_uuid must be a string, not {type(component_uuid).__name__}")
559
+
560
+ if component_uuid not in self._uuid_index:
561
+ return False
562
+
563
+ component = self._items[self._uuid_index[component_uuid]]
564
+
565
+ # Remove from component-specific indexes
566
+ self._remove_from_indexes(component)
567
+
568
+ # Remove from base collection
569
+ super().remove(component_uuid)
570
+
571
+ logger.info(f"Removed component by UUID: {component_uuid}")
572
+ return True
573
+
574
+ def remove_component(self, component: "Component") -> bool:
575
+ """
576
+ Remove component by component object.
577
+
578
+ Args:
579
+ component: Component object to remove
580
+
581
+ Returns:
582
+ True if component was removed, False if not found
583
+
584
+ Raises:
585
+ TypeError: If component is not a Component instance
586
+
587
+ Examples:
588
+ comp = sch.components.get("R1")
589
+ sch.components.remove_component(comp)
590
+ """
591
+ if not isinstance(component, Component):
592
+ raise TypeError(
593
+ f"component must be a Component instance, not {type(component).__name__}"
594
+ )
595
+
596
+ if component.uuid not in self._uuid_index:
597
+ return False
598
+
599
+ # Remove from component-specific indexes
600
+ self._remove_from_indexes(component)
601
+
602
+ # Remove from base collection
603
+ super().remove(component.uuid)
604
+
605
+ logger.info(f"Removed component: {component.reference}")
606
+ return True
607
+
458
608
  def get(self, reference: str) -> Optional[Component]:
459
609
  """Get component by reference."""
460
610
  return self._reference_index.get(reference)
@@ -474,7 +624,7 @@ class ComponentCollection:
474
624
  Returns:
475
625
  List of matching components
476
626
  """
477
- results = list(self._components)
627
+ results = list(self._items)
478
628
 
479
629
  # Apply filters
480
630
  if "lib_id" in criteria:
@@ -512,7 +662,7 @@ class ComponentCollection:
512
662
  def filter_by_type(self, component_type: str) -> List[Component]:
513
663
  """Filter components by type (e.g., 'R' for resistors)."""
514
664
  return [
515
- c for c in self._components if c.symbol_name.upper().startswith(component_type.upper())
665
+ c for c in self._items if c.symbol_name.upper().startswith(component_type.upper())
516
666
  ]
517
667
 
518
668
  def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
@@ -527,7 +677,7 @@ class ComponentCollection:
527
677
  point = Point(point[0], point[1])
528
678
 
529
679
  results = []
530
- for component in self._components:
680
+ for component in self._items:
531
681
  if component.position.distance_to(point) <= radius:
532
682
  results.append(component)
533
683
  return results
@@ -559,21 +709,21 @@ class ComponentCollection:
559
709
  component.set_property(key, str(value))
560
710
 
561
711
  if matching:
562
- self._modified = True
712
+ self._mark_modified()
563
713
 
564
714
  logger.info(f"Bulk updated {len(matching)} components")
565
715
  return len(matching)
566
716
 
567
717
  def sort_by_reference(self):
568
718
  """Sort components by reference designator."""
569
- self._components.sort(key=lambda c: c.reference)
719
+ self._items.sort(key=lambda c: c.reference)
570
720
 
571
721
  def sort_by_position(self, by_x: bool = True):
572
722
  """Sort components by position."""
573
723
  if by_x:
574
- self._components.sort(key=lambda c: (c.position.x, c.position.y))
724
+ self._items.sort(key=lambda c: (c.position.x, c.position.y))
575
725
  else:
576
- self._components.sort(key=lambda c: (c.position.y, c.position.x))
726
+ self._items.sort(key=lambda c: (c.position.y, c.position.x))
577
727
 
578
728
  def validate_all(self) -> List[ValidationIssue]:
579
729
  """Validate all components in collection."""
@@ -581,12 +731,12 @@ class ComponentCollection:
581
731
  validator = SchematicValidator()
582
732
 
583
733
  # Validate individual components
584
- for component in self._components:
734
+ for component in self._items:
585
735
  issues = component.validate()
586
736
  all_issues.extend(issues)
587
737
 
588
738
  # Validate collection-level rules
589
- references = [c.reference for c in self._components]
739
+ references = [c.reference for c in self._items]
590
740
  if len(references) != len(set(references)):
591
741
  # Find duplicates
592
742
  seen = set()
@@ -610,7 +760,7 @@ class ComponentCollection:
610
760
  lib_counts = {}
611
761
  value_counts = {}
612
762
 
613
- for component in self._components:
763
+ for component in self._items:
614
764
  # Count by library
615
765
  lib = component.library
616
766
  lib_counts[lib] = lib_counts.get(lib, 0) + 1
@@ -621,36 +771,47 @@ class ComponentCollection:
621
771
  value_counts[value] = value_counts.get(value, 0) + 1
622
772
 
623
773
  return {
624
- "total_components": len(self._components),
774
+ "total_components": len(self._items),
625
775
  "unique_references": len(self._reference_index),
626
776
  "libraries_used": len(lib_counts),
627
777
  "library_breakdown": lib_counts,
628
778
  "most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
629
779
  :10
630
780
  ],
631
- "modified": self._modified,
781
+ "modified": self.is_modified(),
632
782
  }
633
783
 
634
784
  # Collection interface
635
- def __len__(self) -> int:
636
- """Number of components."""
637
- return len(self._components)
638
-
639
- def __iter__(self) -> Iterator[Component]:
640
- """Iterate over components."""
641
- return iter(self._components)
785
+ # __len__, __iter__ inherited from BaseCollection
642
786
 
643
787
  def __getitem__(self, key: Union[int, str]) -> Component:
644
- """Get component by index or reference."""
645
- if isinstance(key, int):
646
- return self._components[key]
647
- elif isinstance(key, str):
788
+ """
789
+ Get component by index, UUID, or reference.
790
+
791
+ Args:
792
+ key: Integer index, UUID string, or reference string
793
+
794
+ Returns:
795
+ Component at the specified location
796
+
797
+ Raises:
798
+ KeyError: If UUID or reference not found
799
+ IndexError: If index out of range
800
+ TypeError: If key is invalid type
801
+ """
802
+ if isinstance(key, str):
803
+ # Try reference first (most common use case)
648
804
  component = self._reference_index.get(key)
649
- if component is None:
805
+ if component is not None:
806
+ return component
807
+ # Fall back to UUID lookup (from base class)
808
+ try:
809
+ return super().__getitem__(key)
810
+ except KeyError:
650
811
  raise KeyError(f"Component not found: {key}")
651
- return component
652
812
  else:
653
- raise TypeError(f"Invalid key type: {type(key)}")
813
+ # Integer index (from base class)
814
+ return super().__getitem__(key)
654
815
 
655
816
  def __contains__(self, reference: str) -> bool:
656
817
  """Check if reference exists."""
@@ -658,8 +819,11 @@ class ComponentCollection:
658
819
 
659
820
  # Internal methods
660
821
  def _add_to_indexes(self, component: Component):
661
- """Add component to all indexes."""
662
- self._components.append(component)
822
+ """Add component to all indexes (base + component-specific)."""
823
+ # Add to base collection (UUID index)
824
+ self._add_item(component)
825
+
826
+ # Add to reference index
663
827
  self._reference_index[component.reference] = component
664
828
 
665
829
  # Add to lib_id index
@@ -676,8 +840,8 @@ class ComponentCollection:
676
840
  self._value_index[value].append(component)
677
841
 
678
842
  def _remove_from_indexes(self, component: Component):
679
- """Remove component from all indexes."""
680
- self._components.remove(component)
843
+ """Remove component from component-specific indexes (not base UUID index)."""
844
+ # Remove from reference index
681
845
  del self._reference_index[component.reference]
682
846
 
683
847
  # Remove from lib_id index
@@ -700,10 +864,7 @@ class ComponentCollection:
700
864
  component = self._reference_index[old_ref]
701
865
  del self._reference_index[old_ref]
702
866
  self._reference_index[new_ref] = component
703
-
704
- def _mark_modified(self):
705
- """Mark collection as modified."""
706
- self._modified = True
867
+ # Note: UUID doesn't change when reference changes, so base index is unaffected
707
868
 
708
869
  def _generate_reference(self, lib_id: str) -> str:
709
870
  """Generate unique reference for component."""
@@ -725,7 +886,7 @@ class ComponentCollection:
725
886
  grid_size = 10.0 # 10mm grid
726
887
  max_per_row = 10
727
888
 
728
- row = len(self._components) // max_per_row
729
- col = len(self._components) % max_per_row
889
+ row = len(self._items) // max_per_row
890
+ col = len(self._items) % max_per_row
730
891
 
731
892
  return Point(col * grid_size, row * grid_size)
@@ -6,8 +6,8 @@ This module centralizes all magic numbers and configuration values
6
6
  to make them easily configurable and maintainable.
7
7
  """
8
8
 
9
- from dataclasses import dataclass
10
- from typing import Any, Dict, Tuple
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Dict, List, Tuple
11
11
 
12
12
 
13
13
  @dataclass
@@ -57,20 +57,102 @@ class DefaultValues:
57
57
 
58
58
  project_name: str = "untitled"
59
59
  stroke_width: float = 0.0
60
+ stroke_type: str = "default"
61
+ fill_type: str = "none"
60
62
  font_size: float = 1.27
61
63
  pin_name_size: float = 1.27
62
64
  pin_number_size: float = 1.27
63
65
 
64
66
 
67
+ @dataclass
68
+ class FileFormatConstants:
69
+ """KiCAD file format identifiers and version strings."""
70
+
71
+ file_type: str = "kicad_sch"
72
+ generator_default: str = "eeschema"
73
+ version_default: str = "20250114"
74
+ generator_version_default: str = "9.0"
75
+
76
+
77
+ @dataclass
78
+ class PaperSizeConstants:
79
+ """Standard paper size definitions."""
80
+
81
+ default: str = "A4"
82
+ valid_sizes: List[str] = field(
83
+ default_factory=lambda: ["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]
84
+ )
85
+
86
+
87
+ @dataclass
88
+ class FieldNames:
89
+ """Common S-expression field names to avoid typos."""
90
+
91
+ # File structure
92
+ version: str = "version"
93
+ generator: str = "generator"
94
+ generator_version: str = "generator_version"
95
+ uuid: str = "uuid"
96
+ paper: str = "paper"
97
+
98
+ # Positioning
99
+ at: str = "at"
100
+ xy: str = "xy"
101
+ pts: str = "pts"
102
+ start: str = "start"
103
+ end: str = "end"
104
+ mid: str = "mid"
105
+ center: str = "center"
106
+ radius: str = "radius"
107
+
108
+ # Styling
109
+ stroke: str = "stroke"
110
+ fill: str = "fill"
111
+ width: str = "width"
112
+ type: str = "type"
113
+ color: str = "color"
114
+
115
+ # Text/Font
116
+ font: str = "font"
117
+ size: str = "size"
118
+ effects: str = "effects"
119
+
120
+ # Components
121
+ pin: str = "pin"
122
+ property: str = "property"
123
+ symbol: str = "symbol"
124
+ lib_id: str = "lib_id"
125
+
126
+ # Graphics
127
+ polyline: str = "polyline"
128
+ arc: str = "arc"
129
+ circle: str = "circle"
130
+ rectangle: str = "rectangle"
131
+ bezier: str = "bezier"
132
+
133
+ # Connection elements
134
+ wire: str = "wire"
135
+ junction: str = "junction"
136
+ no_connect: str = "no_connect"
137
+ label: str = "label"
138
+
139
+ # Hierarchical
140
+ sheet: str = "sheet"
141
+ sheet_instances: str = "sheet_instances"
142
+
143
+
65
144
  class KiCADConfig:
66
145
  """Central configuration class for KiCAD schematic API."""
67
146
 
68
- def __init__(self):
147
+ def __init__(self) -> None:
69
148
  self.properties = PropertyOffsets()
70
149
  self.grid = GridSettings()
71
150
  self.sheet = SheetSettings()
72
151
  self.tolerance = ToleranceSettings()
73
152
  self.defaults = DefaultValues()
153
+ self.file_format = FileFormatConstants()
154
+ self.paper = PaperSizeConstants()
155
+ self.fields = FieldNames()
74
156
 
75
157
  # Names that should not generate title_block (for backward compatibility)
76
158
  # Include test schematic names to maintain reference compatibility
@@ -92,35 +174,48 @@ class KiCADConfig:
92
174
  return name.lower() not in self.no_title_block_names
93
175
 
94
176
  def get_property_position(
95
- self, property_name: str, component_pos: Tuple[float, float], offset_index: int = 0
177
+ self, property_name: str, component_pos: Tuple[float, float], offset_index: int = 0, component_rotation: float = 0
96
178
  ) -> Tuple[float, float, float]:
97
179
  """
98
- Calculate property position relative to component.
180
+ Calculate property position relative to component, accounting for component rotation.
181
+
182
+ Args:
183
+ property_name: Name of the property (Reference, Value, etc.)
184
+ component_pos: (x, y) position of component
185
+ offset_index: Stacking offset for multiple properties
186
+ component_rotation: Rotation of the component in degrees (0, 90, 180, 270)
99
187
 
100
188
  Returns:
101
189
  Tuple of (x, y, rotation) for the property
102
190
  """
191
+ import math
192
+
103
193
  x, y = component_pos
104
194
 
195
+ # Get base offsets (for 0° rotation)
105
196
  if property_name == "Reference":
106
- return (x + self.properties.reference_x, y + self.properties.reference_y, 0)
197
+ dx, dy = self.properties.reference_x, self.properties.reference_y
107
198
  elif property_name == "Value":
108
- return (x + self.properties.value_x, y + self.properties.value_y, 0)
199
+ dx, dy = self.properties.value_x, self.properties.value_y
109
200
  elif property_name == "Footprint":
110
201
  # Footprint positioned to left of component, rotated 90 degrees
111
- return (x - 1.778, y, self.properties.footprint_rotation) # Exact match for reference
202
+ return (x - 1.778, y, self.properties.footprint_rotation)
112
203
  elif property_name in ["Datasheet", "Description"]:
113
204
  # Hidden properties at component center
114
205
  return (x, y, 0)
115
206
  else:
116
207
  # Other properties stacked vertically below
117
- return (
118
- x + self.properties.reference_x,
119
- y
120
- + self.properties.value_y
121
- + (self.properties.hidden_property_offset * offset_index),
122
- 0,
123
- )
208
+ dx = self.properties.reference_x
209
+ dy = self.properties.value_y + (self.properties.hidden_property_offset * offset_index)
210
+
211
+ # Apply rotation transform to offsets
212
+ # Text stays at 0° rotation (readable), but position rotates around component
213
+ # KiCad uses clockwise rotation, so negate the angle
214
+ rotation_rad = math.radians(-component_rotation)
215
+ dx_rotated = dx * math.cos(rotation_rad) - dy * math.sin(rotation_rad)
216
+ dy_rotated = dx * math.sin(rotation_rad) + dy * math.cos(rotation_rad)
217
+
218
+ return (x + dx_rotated, y + dy_rotated, 0)
124
219
 
125
220
 
126
221
  # Global configuration instance