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.
- kicad_sch_api/__init__.py +67 -2
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/collections/__init__.py +23 -8
- kicad_sch_api/collections/base.py +369 -59
- kicad_sch_api/collections/components.py +1376 -187
- kicad_sch_api/collections/junctions.py +129 -289
- kicad_sch_api/collections/labels.py +391 -287
- kicad_sch_api/collections/wires.py +202 -316
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/component_bounds.py +34 -12
- kicad_sch_api/core/components.py +146 -7
- kicad_sch_api/core/config.py +25 -12
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/element_factory.py +3 -1
- kicad_sch_api/core/formatter.py +24 -7
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/managers/__init__.py +4 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +3 -1
- kicad_sch_api/core/managers/format_sync.py +3 -2
- kicad_sch_api/core/managers/graphics.py +3 -2
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +4 -2
- kicad_sch_api/core/managers/sheet.py +52 -14
- kicad_sch_api/core/managers/text_elements.py +3 -2
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +112 -54
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +343 -29
- kicad_sch_api/core/types.py +79 -7
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +15 -3
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/parsers/elements/label_parser.py +30 -8
- kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
- kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
- kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -382,21 +382,43 @@ def get_component_bounding_box(
|
|
|
382
382
|
# Calculate symbol bounding box
|
|
383
383
|
symbol_bbox = SymbolBoundingBoxCalculator.calculate_bounding_box(symbol, include_properties)
|
|
384
384
|
|
|
385
|
-
# Transform to world coordinates
|
|
386
|
-
#
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
385
|
+
# Transform to world coordinates with rotation
|
|
386
|
+
# Apply rotation matrix to bounding box corners, then find new min/max
|
|
387
|
+
import math
|
|
388
|
+
|
|
389
|
+
angle_rad = math.radians(component.rotation)
|
|
390
|
+
cos_a = math.cos(angle_rad)
|
|
391
|
+
sin_a = math.sin(angle_rad)
|
|
392
|
+
|
|
393
|
+
# Get all 4 corners of the symbol bounding box
|
|
394
|
+
corners = [
|
|
395
|
+
(symbol_bbox.min_x, symbol_bbox.min_y), # Bottom-left
|
|
396
|
+
(symbol_bbox.max_x, symbol_bbox.min_y), # Bottom-right
|
|
397
|
+
(symbol_bbox.max_x, symbol_bbox.max_y), # Top-right
|
|
398
|
+
(symbol_bbox.min_x, symbol_bbox.max_y), # Top-left
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
# Rotate each corner using standard 2D rotation matrix
|
|
402
|
+
rotated_corners = []
|
|
403
|
+
for x, y in corners:
|
|
404
|
+
rotated_x = x * cos_a - y * sin_a
|
|
405
|
+
rotated_y = x * sin_a + y * cos_a
|
|
406
|
+
rotated_corners.append((rotated_x, rotated_y))
|
|
407
|
+
|
|
408
|
+
# Find min/max of rotated corners
|
|
409
|
+
rotated_xs = [x for x, y in rotated_corners]
|
|
410
|
+
rotated_ys = [y for x, y in rotated_corners]
|
|
411
|
+
|
|
392
412
|
world_bbox = BoundingBox(
|
|
393
|
-
component.position.x +
|
|
394
|
-
component.position.y +
|
|
395
|
-
component.position.x +
|
|
396
|
-
component.position.y +
|
|
413
|
+
component.position.x + min(rotated_xs),
|
|
414
|
+
component.position.y + min(rotated_ys),
|
|
415
|
+
component.position.x + max(rotated_xs),
|
|
416
|
+
component.position.y + max(rotated_ys),
|
|
397
417
|
)
|
|
398
418
|
|
|
399
|
-
logger.debug(
|
|
419
|
+
logger.debug(
|
|
420
|
+
f"Component {component.reference} at {component.rotation}° world bbox: {world_bbox}"
|
|
421
|
+
)
|
|
400
422
|
return world_bbox
|
|
401
423
|
|
|
402
424
|
|
kicad_sch_api/core/components.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
|
12
12
|
from ..library.cache import SymbolDefinition, get_symbol_cache
|
|
13
13
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
14
14
|
from .collections import BaseCollection
|
|
15
|
+
from .exceptions import LibraryError
|
|
15
16
|
from .ic_manager import ICManager
|
|
16
17
|
from .types import Point, SchematicPin, SchematicSymbol
|
|
17
18
|
|
|
@@ -106,8 +107,29 @@ class Component:
|
|
|
106
107
|
|
|
107
108
|
@rotation.setter
|
|
108
109
|
def rotation(self, value: float):
|
|
109
|
-
"""Set component rotation.
|
|
110
|
-
|
|
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
|
|
111
133
|
self._collection._mark_modified()
|
|
112
134
|
|
|
113
135
|
@property
|
|
@@ -273,12 +295,13 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
273
295
|
Optimized for schematics with hundreds or thousands of components.
|
|
274
296
|
"""
|
|
275
297
|
|
|
276
|
-
def __init__(self, components: List[SchematicSymbol] = None):
|
|
298
|
+
def __init__(self, components: List[SchematicSymbol] = None, parent_schematic=None):
|
|
277
299
|
"""
|
|
278
300
|
Initialize component collection.
|
|
279
301
|
|
|
280
302
|
Args:
|
|
281
303
|
components: Initial list of component data
|
|
304
|
+
parent_schematic: Reference to parent Schematic object (for hierarchy context)
|
|
282
305
|
"""
|
|
283
306
|
# Initialize base collection
|
|
284
307
|
super().__init__([], collection_name="components")
|
|
@@ -288,6 +311,9 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
288
311
|
self._lib_id_index: Dict[str, List[Component]] = {}
|
|
289
312
|
self._value_index: Dict[str, List[Component]] = {}
|
|
290
313
|
|
|
314
|
+
# Store reference to parent schematic for hierarchy context
|
|
315
|
+
self._parent_schematic = parent_schematic
|
|
316
|
+
|
|
291
317
|
# Add initial components
|
|
292
318
|
if components:
|
|
293
319
|
for comp_data in components:
|
|
@@ -301,6 +327,7 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
301
327
|
position: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
302
328
|
footprint: Optional[str] = None,
|
|
303
329
|
unit: int = 1,
|
|
330
|
+
rotation: float = 0.0,
|
|
304
331
|
component_uuid: Optional[str] = None,
|
|
305
332
|
**properties,
|
|
306
333
|
) -> Component:
|
|
@@ -314,6 +341,7 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
314
341
|
position: Component position (auto-placed if None)
|
|
315
342
|
footprint: Component footprint
|
|
316
343
|
unit: Unit number for multi-unit components (1-based)
|
|
344
|
+
rotation: Component rotation in degrees (0, 90, 180, 270)
|
|
317
345
|
component_uuid: Specific UUID for component (auto-generated if None)
|
|
318
346
|
**properties: Additional component properties
|
|
319
347
|
|
|
@@ -322,6 +350,7 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
322
350
|
|
|
323
351
|
Raises:
|
|
324
352
|
ValidationError: If component data is invalid
|
|
353
|
+
LibraryError: If the KiCAD symbol library is not found
|
|
325
354
|
"""
|
|
326
355
|
# Validate lib_id
|
|
327
356
|
validator = SchematicValidator()
|
|
@@ -356,6 +385,28 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
356
385
|
f"Component {reference} position snapped to grid: ({position.x:.3f}, {position.y:.3f})"
|
|
357
386
|
)
|
|
358
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
|
+
|
|
359
410
|
# Create component data
|
|
360
411
|
component_data = SchematicSymbol(
|
|
361
412
|
uuid=component_uuid if component_uuid else str(uuid.uuid4()),
|
|
@@ -365,14 +416,24 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
365
416
|
value=value,
|
|
366
417
|
footprint=footprint,
|
|
367
418
|
unit=unit,
|
|
419
|
+
rotation=rotation,
|
|
368
420
|
properties=properties,
|
|
369
421
|
)
|
|
370
422
|
|
|
371
423
|
# Get symbol definition and update pins
|
|
372
424
|
symbol_cache = get_symbol_cache()
|
|
373
425
|
symbol_def = symbol_cache.get_symbol(lib_id)
|
|
374
|
-
if symbol_def:
|
|
375
|
-
|
|
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()
|
|
376
437
|
|
|
377
438
|
# Create component wrapper
|
|
378
439
|
component = Component(component_data, self)
|
|
@@ -448,11 +509,25 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
448
509
|
Remove component by reference.
|
|
449
510
|
|
|
450
511
|
Args:
|
|
451
|
-
reference: Component reference to remove
|
|
512
|
+
reference: Component reference to remove (e.g., "R1")
|
|
452
513
|
|
|
453
514
|
Returns:
|
|
454
|
-
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.
|
|
455
527
|
"""
|
|
528
|
+
if not isinstance(reference, str):
|
|
529
|
+
raise TypeError(f"reference must be a string, not {type(reference).__name__}")
|
|
530
|
+
|
|
456
531
|
component = self._reference_index.get(reference)
|
|
457
532
|
if not component:
|
|
458
533
|
return False
|
|
@@ -466,6 +541,70 @@ class ComponentCollection(BaseCollection[Component]):
|
|
|
466
541
|
logger.info(f"Removed component: {reference}")
|
|
467
542
|
return True
|
|
468
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
|
+
|
|
469
608
|
def get(self, reference: str) -> Optional[Component]:
|
|
470
609
|
"""Get component by reference."""
|
|
471
610
|
return self._reference_index.get(reference)
|
kicad_sch_api/core/config.py
CHANGED
|
@@ -174,35 +174,48 @@ class KiCADConfig:
|
|
|
174
174
|
return name.lower() not in self.no_title_block_names
|
|
175
175
|
|
|
176
176
|
def get_property_position(
|
|
177
|
-
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
|
|
178
178
|
) -> Tuple[float, float, float]:
|
|
179
179
|
"""
|
|
180
|
-
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)
|
|
181
187
|
|
|
182
188
|
Returns:
|
|
183
189
|
Tuple of (x, y, rotation) for the property
|
|
184
190
|
"""
|
|
191
|
+
import math
|
|
192
|
+
|
|
185
193
|
x, y = component_pos
|
|
186
194
|
|
|
195
|
+
# Get base offsets (for 0° rotation)
|
|
187
196
|
if property_name == "Reference":
|
|
188
|
-
|
|
197
|
+
dx, dy = self.properties.reference_x, self.properties.reference_y
|
|
189
198
|
elif property_name == "Value":
|
|
190
|
-
|
|
199
|
+
dx, dy = self.properties.value_x, self.properties.value_y
|
|
191
200
|
elif property_name == "Footprint":
|
|
192
201
|
# Footprint positioned to left of component, rotated 90 degrees
|
|
193
|
-
return (x - 1.778, y, self.properties.footprint_rotation)
|
|
202
|
+
return (x - 1.778, y, self.properties.footprint_rotation)
|
|
194
203
|
elif property_name in ["Datasheet", "Description"]:
|
|
195
204
|
# Hidden properties at component center
|
|
196
205
|
return (x, y, 0)
|
|
197
206
|
else:
|
|
198
207
|
# Other properties stacked vertically below
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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)
|
|
206
219
|
|
|
207
220
|
|
|
208
221
|
# Global configuration instance
|