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.
- kicad_sch_api/__init__.py +68 -3
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- 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 +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +216 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- 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/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- 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.3.0.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/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
kicad_sch_api/core/components.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
535
|
+
# Remove from component-specific indexes
|
|
452
536
|
self._remove_from_indexes(component)
|
|
453
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
724
|
+
self._items.sort(key=lambda c: (c.position.x, c.position.y))
|
|
575
725
|
else:
|
|
576
|
-
self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
781
|
+
"modified": self.is_modified(),
|
|
632
782
|
}
|
|
633
783
|
|
|
634
784
|
# Collection interface
|
|
635
|
-
|
|
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
|
-
"""
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
680
|
-
|
|
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.
|
|
729
|
-
col = len(self.
|
|
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)
|
kicad_sch_api/core/config.py
CHANGED
|
@@ -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
|
-
|
|
197
|
+
dx, dy = self.properties.reference_x, self.properties.reference_y
|
|
107
198
|
elif property_name == "Value":
|
|
108
|
-
|
|
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)
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|