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.
- kicad_sch_api/__init__.py +2 -2
- 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/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +2 -2
- kicad_sch_api/collections/base.py +5 -7
- kicad_sch_api/collections/components.py +24 -12
- kicad_sch_api/collections/junctions.py +31 -43
- kicad_sch_api/collections/labels.py +19 -27
- kicad_sch_api/collections/wires.py +17 -18
- 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 +5 -0
- kicad_sch_api/core/components.py +67 -45
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +29 -53
- kicad_sch_api/core/managers/__init__.py +26 -0
- kicad_sch_api/core/managers/file_io.py +244 -0
- kicad_sch_api/core/managers/format_sync.py +501 -0
- kicad_sch_api/core/managers/graphics.py +579 -0
- kicad_sch_api/core/managers/metadata.py +269 -0
- kicad_sch_api/core/managers/sheet.py +454 -0
- kicad_sch_api/core/managers/text_elements.py +536 -0
- kicad_sch_api/core/managers/validation.py +475 -0
- kicad_sch_api/core/managers/wire.py +352 -0
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +33 -55
- kicad_sch_api/core/parser.py +75 -1731
- kicad_sch_api/core/schematic.py +951 -1192
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +60 -22
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +40 -21
- kicad_sch_api/interfaces/__init__.py +1 -1
- kicad_sch_api/interfaces/parser.py +1 -1
- kicad_sch_api/interfaces/repository.py +1 -1
- kicad_sch_api/interfaces/resolver.py +1 -1
- kicad_sch_api/parsers/__init__.py +2 -2
- kicad_sch_api/parsers/base.py +7 -10
- 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 +194 -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 +313 -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 +4 -2
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +1 -1
- kicad_sch_api/symbols/cache.py +9 -12
- kicad_sch_api/symbols/resolver.py +20 -26
- kicad_sch_api/symbols/validators.py +188 -137
- 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-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.1.dist-info/RECORD +87 -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/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -227
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -384,6 +384,11 @@ def get_component_bounding_box(
|
|
|
384
384
|
|
|
385
385
|
# Transform to world coordinates
|
|
386
386
|
# TODO: Handle component rotation in the future
|
|
387
|
+
# NOTE: Currently assumes 0° rotation. For rotated components, bounding box
|
|
388
|
+
# would need to be recalculated after applying rotation matrix. This is a
|
|
389
|
+
# known limitation but doesn't affect most use cases since components are
|
|
390
|
+
# typically placed without rotation, and routing avoids components regardless.
|
|
391
|
+
# Priority: LOW - Would improve accuracy for rotated component placement validation
|
|
387
392
|
world_bbox = BoundingBox(
|
|
388
393
|
component.position.x + symbol_bbox.min_x,
|
|
389
394
|
component.position.y + symbol_bbox.min_y,
|
kicad_sch_api/core/components.py
CHANGED
|
@@ -11,6 +11,7 @@ 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
|
|
14
15
|
from .ic_manager import ICManager
|
|
15
16
|
from .types import Point, SchematicPin, SchematicSymbol
|
|
16
17
|
|
|
@@ -38,6 +39,11 @@ class Component:
|
|
|
38
39
|
self._validator = SchematicValidator()
|
|
39
40
|
|
|
40
41
|
# Core properties with validation
|
|
42
|
+
@property
|
|
43
|
+
def uuid(self) -> str:
|
|
44
|
+
"""Component UUID."""
|
|
45
|
+
return self._data.uuid
|
|
46
|
+
|
|
41
47
|
@property
|
|
42
48
|
def reference(self) -> str:
|
|
43
49
|
"""Component reference (e.g., 'R1')."""
|
|
@@ -256,10 +262,13 @@ class Component:
|
|
|
256
262
|
)
|
|
257
263
|
|
|
258
264
|
|
|
259
|
-
class ComponentCollection:
|
|
265
|
+
class ComponentCollection(BaseCollection[Component]):
|
|
260
266
|
"""
|
|
261
267
|
Collection class for efficient component management.
|
|
262
268
|
|
|
269
|
+
Inherits from BaseCollection for standard operations and adds component-specific
|
|
270
|
+
functionality including reference, lib_id, and value-based indexing.
|
|
271
|
+
|
|
263
272
|
Provides fast lookup, filtering, and bulk operations for schematic components.
|
|
264
273
|
Optimized for schematics with hundreds or thousands of components.
|
|
265
274
|
"""
|
|
@@ -271,19 +280,19 @@ class ComponentCollection:
|
|
|
271
280
|
Args:
|
|
272
281
|
components: Initial list of component data
|
|
273
282
|
"""
|
|
274
|
-
|
|
283
|
+
# Initialize base collection
|
|
284
|
+
super().__init__([], collection_name="components")
|
|
285
|
+
|
|
286
|
+
# Additional component-specific indexes
|
|
275
287
|
self._reference_index: Dict[str, Component] = {}
|
|
276
288
|
self._lib_id_index: Dict[str, List[Component]] = {}
|
|
277
289
|
self._value_index: Dict[str, List[Component]] = {}
|
|
278
|
-
self._modified = False
|
|
279
290
|
|
|
280
291
|
# Add initial components
|
|
281
292
|
if components:
|
|
282
293
|
for comp_data in components:
|
|
283
294
|
self._add_to_indexes(Component(comp_data, self))
|
|
284
295
|
|
|
285
|
-
logger.debug(f"ComponentCollection initialized with {len(self._components)} components")
|
|
286
|
-
|
|
287
296
|
def add(
|
|
288
297
|
self,
|
|
289
298
|
lib_id: str,
|
|
@@ -370,7 +379,7 @@ class ComponentCollection:
|
|
|
370
379
|
|
|
371
380
|
# Add to collection
|
|
372
381
|
self._add_to_indexes(component)
|
|
373
|
-
self.
|
|
382
|
+
self._mark_modified()
|
|
374
383
|
|
|
375
384
|
logger.info(f"Added component: {reference} ({lib_id})")
|
|
376
385
|
return component
|
|
@@ -427,7 +436,7 @@ class ComponentCollection:
|
|
|
427
436
|
component = Component(component_data, self)
|
|
428
437
|
self._add_to_indexes(component)
|
|
429
438
|
|
|
430
|
-
self.
|
|
439
|
+
self._mark_modified()
|
|
431
440
|
logger.info(
|
|
432
441
|
f"Added multi-unit IC: {reference_prefix} ({lib_id}) with {len(unit_components)} units"
|
|
433
442
|
)
|
|
@@ -448,9 +457,11 @@ class ComponentCollection:
|
|
|
448
457
|
if not component:
|
|
449
458
|
return False
|
|
450
459
|
|
|
451
|
-
# Remove from
|
|
460
|
+
# Remove from component-specific indexes
|
|
452
461
|
self._remove_from_indexes(component)
|
|
453
|
-
|
|
462
|
+
|
|
463
|
+
# Remove from base collection using UUID
|
|
464
|
+
super().remove(component.uuid)
|
|
454
465
|
|
|
455
466
|
logger.info(f"Removed component: {reference}")
|
|
456
467
|
return True
|
|
@@ -474,7 +485,7 @@ class ComponentCollection:
|
|
|
474
485
|
Returns:
|
|
475
486
|
List of matching components
|
|
476
487
|
"""
|
|
477
|
-
results = list(self.
|
|
488
|
+
results = list(self._items)
|
|
478
489
|
|
|
479
490
|
# Apply filters
|
|
480
491
|
if "lib_id" in criteria:
|
|
@@ -512,7 +523,7 @@ class ComponentCollection:
|
|
|
512
523
|
def filter_by_type(self, component_type: str) -> List[Component]:
|
|
513
524
|
"""Filter components by type (e.g., 'R' for resistors)."""
|
|
514
525
|
return [
|
|
515
|
-
c for c in self.
|
|
526
|
+
c for c in self._items if c.symbol_name.upper().startswith(component_type.upper())
|
|
516
527
|
]
|
|
517
528
|
|
|
518
529
|
def in_area(self, x1: float, y1: float, x2: float, y2: float) -> List[Component]:
|
|
@@ -527,7 +538,7 @@ class ComponentCollection:
|
|
|
527
538
|
point = Point(point[0], point[1])
|
|
528
539
|
|
|
529
540
|
results = []
|
|
530
|
-
for component in self.
|
|
541
|
+
for component in self._items:
|
|
531
542
|
if component.position.distance_to(point) <= radius:
|
|
532
543
|
results.append(component)
|
|
533
544
|
return results
|
|
@@ -559,21 +570,21 @@ class ComponentCollection:
|
|
|
559
570
|
component.set_property(key, str(value))
|
|
560
571
|
|
|
561
572
|
if matching:
|
|
562
|
-
self.
|
|
573
|
+
self._mark_modified()
|
|
563
574
|
|
|
564
575
|
logger.info(f"Bulk updated {len(matching)} components")
|
|
565
576
|
return len(matching)
|
|
566
577
|
|
|
567
578
|
def sort_by_reference(self):
|
|
568
579
|
"""Sort components by reference designator."""
|
|
569
|
-
self.
|
|
580
|
+
self._items.sort(key=lambda c: c.reference)
|
|
570
581
|
|
|
571
582
|
def sort_by_position(self, by_x: bool = True):
|
|
572
583
|
"""Sort components by position."""
|
|
573
584
|
if by_x:
|
|
574
|
-
self.
|
|
585
|
+
self._items.sort(key=lambda c: (c.position.x, c.position.y))
|
|
575
586
|
else:
|
|
576
|
-
self.
|
|
587
|
+
self._items.sort(key=lambda c: (c.position.y, c.position.x))
|
|
577
588
|
|
|
578
589
|
def validate_all(self) -> List[ValidationIssue]:
|
|
579
590
|
"""Validate all components in collection."""
|
|
@@ -581,12 +592,12 @@ class ComponentCollection:
|
|
|
581
592
|
validator = SchematicValidator()
|
|
582
593
|
|
|
583
594
|
# Validate individual components
|
|
584
|
-
for component in self.
|
|
595
|
+
for component in self._items:
|
|
585
596
|
issues = component.validate()
|
|
586
597
|
all_issues.extend(issues)
|
|
587
598
|
|
|
588
599
|
# Validate collection-level rules
|
|
589
|
-
references = [c.reference for c in self.
|
|
600
|
+
references = [c.reference for c in self._items]
|
|
590
601
|
if len(references) != len(set(references)):
|
|
591
602
|
# Find duplicates
|
|
592
603
|
seen = set()
|
|
@@ -610,7 +621,7 @@ class ComponentCollection:
|
|
|
610
621
|
lib_counts = {}
|
|
611
622
|
value_counts = {}
|
|
612
623
|
|
|
613
|
-
for component in self.
|
|
624
|
+
for component in self._items:
|
|
614
625
|
# Count by library
|
|
615
626
|
lib = component.library
|
|
616
627
|
lib_counts[lib] = lib_counts.get(lib, 0) + 1
|
|
@@ -621,36 +632,47 @@ class ComponentCollection:
|
|
|
621
632
|
value_counts[value] = value_counts.get(value, 0) + 1
|
|
622
633
|
|
|
623
634
|
return {
|
|
624
|
-
"total_components": len(self.
|
|
635
|
+
"total_components": len(self._items),
|
|
625
636
|
"unique_references": len(self._reference_index),
|
|
626
637
|
"libraries_used": len(lib_counts),
|
|
627
638
|
"library_breakdown": lib_counts,
|
|
628
639
|
"most_common_values": sorted(value_counts.items(), key=lambda x: x[1], reverse=True)[
|
|
629
640
|
:10
|
|
630
641
|
],
|
|
631
|
-
"modified": self.
|
|
642
|
+
"modified": self.is_modified(),
|
|
632
643
|
}
|
|
633
644
|
|
|
634
645
|
# 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)
|
|
646
|
+
# __len__, __iter__ inherited from BaseCollection
|
|
642
647
|
|
|
643
648
|
def __getitem__(self, key: Union[int, str]) -> Component:
|
|
644
|
-
"""
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
649
|
+
"""
|
|
650
|
+
Get component by index, UUID, or reference.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
key: Integer index, UUID string, or reference string
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
Component at the specified location
|
|
657
|
+
|
|
658
|
+
Raises:
|
|
659
|
+
KeyError: If UUID or reference not found
|
|
660
|
+
IndexError: If index out of range
|
|
661
|
+
TypeError: If key is invalid type
|
|
662
|
+
"""
|
|
663
|
+
if isinstance(key, str):
|
|
664
|
+
# Try reference first (most common use case)
|
|
648
665
|
component = self._reference_index.get(key)
|
|
649
|
-
if component is None:
|
|
666
|
+
if component is not None:
|
|
667
|
+
return component
|
|
668
|
+
# Fall back to UUID lookup (from base class)
|
|
669
|
+
try:
|
|
670
|
+
return super().__getitem__(key)
|
|
671
|
+
except KeyError:
|
|
650
672
|
raise KeyError(f"Component not found: {key}")
|
|
651
|
-
return component
|
|
652
673
|
else:
|
|
653
|
-
|
|
674
|
+
# Integer index (from base class)
|
|
675
|
+
return super().__getitem__(key)
|
|
654
676
|
|
|
655
677
|
def __contains__(self, reference: str) -> bool:
|
|
656
678
|
"""Check if reference exists."""
|
|
@@ -658,8 +680,11 @@ class ComponentCollection:
|
|
|
658
680
|
|
|
659
681
|
# Internal methods
|
|
660
682
|
def _add_to_indexes(self, component: Component):
|
|
661
|
-
"""Add component to all indexes."""
|
|
662
|
-
|
|
683
|
+
"""Add component to all indexes (base + component-specific)."""
|
|
684
|
+
# Add to base collection (UUID index)
|
|
685
|
+
self._add_item(component)
|
|
686
|
+
|
|
687
|
+
# Add to reference index
|
|
663
688
|
self._reference_index[component.reference] = component
|
|
664
689
|
|
|
665
690
|
# Add to lib_id index
|
|
@@ -676,8 +701,8 @@ class ComponentCollection:
|
|
|
676
701
|
self._value_index[value].append(component)
|
|
677
702
|
|
|
678
703
|
def _remove_from_indexes(self, component: Component):
|
|
679
|
-
"""Remove component from
|
|
680
|
-
|
|
704
|
+
"""Remove component from component-specific indexes (not base UUID index)."""
|
|
705
|
+
# Remove from reference index
|
|
681
706
|
del self._reference_index[component.reference]
|
|
682
707
|
|
|
683
708
|
# Remove from lib_id index
|
|
@@ -700,10 +725,7 @@ class ComponentCollection:
|
|
|
700
725
|
component = self._reference_index[old_ref]
|
|
701
726
|
del self._reference_index[old_ref]
|
|
702
727
|
self._reference_index[new_ref] = component
|
|
703
|
-
|
|
704
|
-
def _mark_modified(self):
|
|
705
|
-
"""Mark collection as modified."""
|
|
706
|
-
self._modified = True
|
|
728
|
+
# Note: UUID doesn't change when reference changes, so base index is unaffected
|
|
707
729
|
|
|
708
730
|
def _generate_reference(self, lib_id: str) -> str:
|
|
709
731
|
"""Generate unique reference for component."""
|
|
@@ -725,7 +747,7 @@ class ComponentCollection:
|
|
|
725
747
|
grid_size = 10.0 # 10mm grid
|
|
726
748
|
max_per_row = 10
|
|
727
749
|
|
|
728
|
-
row = len(self.
|
|
729
|
-
col = len(self.
|
|
750
|
+
row = len(self._items) // max_per_row
|
|
751
|
+
col = len(self._items) % max_per_row
|
|
730
752
|
|
|
731
753
|
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
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Element Factory for creating schematic elements from dictionaries.
|
|
3
|
+
|
|
4
|
+
Centralizes object creation logic that was previously duplicated in Schematic.__init__.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
from ..types import (
|
|
11
|
+
HierarchicalLabelShape,
|
|
12
|
+
Junction,
|
|
13
|
+
Label,
|
|
14
|
+
LabelType,
|
|
15
|
+
Net,
|
|
16
|
+
NoConnect,
|
|
17
|
+
Point,
|
|
18
|
+
Text,
|
|
19
|
+
Wire,
|
|
20
|
+
WireType,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def point_from_dict_or_tuple(position: Any) -> Point:
|
|
25
|
+
"""Convert position data (dict or tuple) to Point object."""
|
|
26
|
+
if isinstance(position, dict):
|
|
27
|
+
return Point(position.get("x", 0), position.get("y", 0))
|
|
28
|
+
elif isinstance(position, (list, tuple)):
|
|
29
|
+
return Point(position[0], position[1])
|
|
30
|
+
elif isinstance(position, Point):
|
|
31
|
+
return position
|
|
32
|
+
else:
|
|
33
|
+
return Point(0, 0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ElementFactory:
|
|
37
|
+
"""Factory for creating schematic elements from dictionary data."""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def create_wire(wire_dict: Dict[str, Any]) -> Wire:
|
|
41
|
+
"""
|
|
42
|
+
Create Wire object from dictionary.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
wire_dict: Dictionary containing wire data
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Wire object
|
|
49
|
+
"""
|
|
50
|
+
points = []
|
|
51
|
+
for point_data in wire_dict.get("points", []):
|
|
52
|
+
if isinstance(point_data, dict):
|
|
53
|
+
points.append(Point(point_data["x"], point_data["y"]))
|
|
54
|
+
elif isinstance(point_data, (list, tuple)):
|
|
55
|
+
points.append(Point(point_data[0], point_data[1]))
|
|
56
|
+
else:
|
|
57
|
+
points.append(point_data)
|
|
58
|
+
|
|
59
|
+
return Wire(
|
|
60
|
+
uuid=wire_dict.get("uuid", str(uuid.uuid4())),
|
|
61
|
+
points=points,
|
|
62
|
+
wire_type=WireType(wire_dict.get("wire_type", "wire")),
|
|
63
|
+
stroke_width=wire_dict.get("stroke_width", 0.0),
|
|
64
|
+
stroke_type=wire_dict.get("stroke_type", "default"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def create_junction(junction_dict: Dict[str, Any]) -> Junction:
|
|
69
|
+
"""
|
|
70
|
+
Create Junction object from dictionary.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
junction_dict: Dictionary containing junction data
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Junction object
|
|
77
|
+
"""
|
|
78
|
+
position = junction_dict.get("position", {"x": 0, "y": 0})
|
|
79
|
+
pos = point_from_dict_or_tuple(position)
|
|
80
|
+
|
|
81
|
+
return Junction(
|
|
82
|
+
uuid=junction_dict.get("uuid", str(uuid.uuid4())),
|
|
83
|
+
position=pos,
|
|
84
|
+
diameter=junction_dict.get("diameter", 0),
|
|
85
|
+
color=junction_dict.get("color", (0, 0, 0, 0)),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def create_text(text_dict: Dict[str, Any]) -> Text:
|
|
90
|
+
"""
|
|
91
|
+
Create Text object from dictionary.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
text_dict: Dictionary containing text data
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Text object
|
|
98
|
+
"""
|
|
99
|
+
position = text_dict.get("position", {"x": 0, "y": 0})
|
|
100
|
+
pos = point_from_dict_or_tuple(position)
|
|
101
|
+
|
|
102
|
+
return Text(
|
|
103
|
+
uuid=text_dict.get("uuid", str(uuid.uuid4())),
|
|
104
|
+
position=pos,
|
|
105
|
+
text=text_dict.get("text", ""),
|
|
106
|
+
rotation=text_dict.get("rotation", 0.0),
|
|
107
|
+
size=text_dict.get("size", 1.27),
|
|
108
|
+
exclude_from_sim=text_dict.get("exclude_from_sim", False),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def create_label(label_dict: Dict[str, Any]) -> Label:
|
|
113
|
+
"""
|
|
114
|
+
Create Label object from dictionary.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
label_dict: Dictionary containing label data
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Label object
|
|
121
|
+
"""
|
|
122
|
+
position = label_dict.get("position", {"x": 0, "y": 0})
|
|
123
|
+
pos = point_from_dict_or_tuple(position)
|
|
124
|
+
|
|
125
|
+
return Label(
|
|
126
|
+
uuid=label_dict.get("uuid", str(uuid.uuid4())),
|
|
127
|
+
position=pos,
|
|
128
|
+
text=label_dict.get("text", ""),
|
|
129
|
+
label_type=LabelType(label_dict.get("label_type", "local")),
|
|
130
|
+
rotation=label_dict.get("rotation", 0.0),
|
|
131
|
+
size=label_dict.get("size", 1.27),
|
|
132
|
+
shape=(
|
|
133
|
+
HierarchicalLabelShape(label_dict.get("shape"))
|
|
134
|
+
if label_dict.get("shape")
|
|
135
|
+
else None
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def create_no_connect(no_connect_dict: Dict[str, Any]) -> NoConnect:
|
|
141
|
+
"""
|
|
142
|
+
Create NoConnect object from dictionary.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
no_connect_dict: Dictionary containing no-connect data
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
NoConnect object
|
|
149
|
+
"""
|
|
150
|
+
position = no_connect_dict.get("position", {"x": 0, "y": 0})
|
|
151
|
+
pos = point_from_dict_or_tuple(position)
|
|
152
|
+
|
|
153
|
+
return NoConnect(
|
|
154
|
+
uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
|
|
155
|
+
position=pos,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def create_net(net_dict: Dict[str, Any]) -> Net:
|
|
160
|
+
"""
|
|
161
|
+
Create Net object from dictionary.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
net_dict: Dictionary containing net data
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Net object
|
|
168
|
+
"""
|
|
169
|
+
return Net(
|
|
170
|
+
name=net_dict.get("name", ""),
|
|
171
|
+
components=net_dict.get("components", []),
|
|
172
|
+
wires=net_dict.get("wires", []),
|
|
173
|
+
labels=net_dict.get("labels", []),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def create_wires_from_list(wire_data: List[Any]) -> List[Wire]:
|
|
178
|
+
"""
|
|
179
|
+
Create list of Wire objects from list of dictionaries.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
wire_data: List of wire dictionaries
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of Wire objects
|
|
186
|
+
"""
|
|
187
|
+
wires = []
|
|
188
|
+
for wire_dict in wire_data:
|
|
189
|
+
if isinstance(wire_dict, dict):
|
|
190
|
+
wires.append(ElementFactory.create_wire(wire_dict))
|
|
191
|
+
return wires
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def create_junctions_from_list(junction_data: List[Any]) -> List[Junction]:
|
|
195
|
+
"""
|
|
196
|
+
Create list of Junction objects from list of dictionaries.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
junction_data: List of junction dictionaries
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of Junction objects
|
|
203
|
+
"""
|
|
204
|
+
junctions = []
|
|
205
|
+
for junction_dict in junction_data:
|
|
206
|
+
if isinstance(junction_dict, dict):
|
|
207
|
+
junctions.append(ElementFactory.create_junction(junction_dict))
|
|
208
|
+
return junctions
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def create_texts_from_list(text_data: List[Any]) -> List[Text]:
|
|
212
|
+
"""
|
|
213
|
+
Create list of Text objects from list of dictionaries.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
text_data: List of text dictionaries
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of Text objects
|
|
220
|
+
"""
|
|
221
|
+
texts = []
|
|
222
|
+
for text_dict in text_data:
|
|
223
|
+
if isinstance(text_dict, dict):
|
|
224
|
+
texts.append(ElementFactory.create_text(text_dict))
|
|
225
|
+
return texts
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def create_labels_from_list(label_data: List[Any]) -> List[Label]:
|
|
229
|
+
"""
|
|
230
|
+
Create list of Label objects from list of dictionaries.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
label_data: List of label dictionaries
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of Label objects
|
|
237
|
+
"""
|
|
238
|
+
labels = []
|
|
239
|
+
for label_dict in label_data:
|
|
240
|
+
if isinstance(label_dict, dict):
|
|
241
|
+
labels.append(ElementFactory.create_label(label_dict))
|
|
242
|
+
return labels
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def create_no_connects_from_list(no_connect_data: List[Any]) -> List[NoConnect]:
|
|
246
|
+
"""
|
|
247
|
+
Create list of NoConnect objects from list of dictionaries.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
no_connect_data: List of no-connect dictionaries
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of NoConnect objects
|
|
254
|
+
"""
|
|
255
|
+
no_connects = []
|
|
256
|
+
for no_connect_dict in no_connect_data:
|
|
257
|
+
if isinstance(no_connect_dict, dict):
|
|
258
|
+
no_connects.append(ElementFactory.create_no_connect(no_connect_dict))
|
|
259
|
+
return no_connects
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def create_nets_from_list(net_data: List[Any]) -> List[Net]:
|
|
263
|
+
"""
|
|
264
|
+
Create list of Net objects from list of dictionaries.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
net_data: List of net dictionaries
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of Net objects
|
|
271
|
+
"""
|
|
272
|
+
nets = []
|
|
273
|
+
for net_dict in net_data:
|
|
274
|
+
if isinstance(net_dict, dict):
|
|
275
|
+
nets.append(ElementFactory.create_net(net_dict))
|
|
276
|
+
return nets
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -55,7 +55,9 @@ class ExactFormatter:
|
|
|
55
55
|
self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
|
|
56
56
|
self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
|
|
57
57
|
self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
|
|
58
|
-
self.rules["paper"] = FormatRule(
|
|
58
|
+
self.rules["paper"] = FormatRule(
|
|
59
|
+
inline=True, quote_indices={1}
|
|
60
|
+
) # Paper size should be quoted per KiCad format
|
|
59
61
|
|
|
60
62
|
# Title block
|
|
61
63
|
self.rules["title_block"] = FormatRule(inline=False)
|