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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception hierarchy for kicad-sch-api.
|
|
3
|
+
|
|
4
|
+
Provides a structured exception hierarchy for better error handling and debugging.
|
|
5
|
+
All exceptions inherit from the base KiCadSchError class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
# Import validation types for type hints
|
|
11
|
+
# ValidationLevel is imported at runtime in methods that need it
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..utils.validation import ValidationIssue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KiCadSchError(Exception):
|
|
17
|
+
"""Base exception for all kicad-sch-api errors."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidationError(KiCadSchError):
|
|
23
|
+
"""
|
|
24
|
+
Raised when validation fails.
|
|
25
|
+
|
|
26
|
+
Supports rich error context with field/value information and can collect
|
|
27
|
+
multiple validation issues.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
message: str,
|
|
33
|
+
issues: Optional[List["ValidationIssue"]] = None,
|
|
34
|
+
field: str = "",
|
|
35
|
+
value: Any = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize validation error with context.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
message: Error message describing the validation failure
|
|
42
|
+
issues: List of validation issues (for collecting multiple errors)
|
|
43
|
+
field: The field name that failed validation
|
|
44
|
+
value: The invalid value that was provided
|
|
45
|
+
"""
|
|
46
|
+
self.issues = issues or []
|
|
47
|
+
self.field = field
|
|
48
|
+
self.value = value
|
|
49
|
+
super().__init__(message)
|
|
50
|
+
|
|
51
|
+
def add_issue(self, issue: "ValidationIssue") -> None:
|
|
52
|
+
"""Add a validation issue to this error."""
|
|
53
|
+
self.issues.append(issue)
|
|
54
|
+
|
|
55
|
+
def get_errors(self) -> List["ValidationIssue"]:
|
|
56
|
+
"""Get only error-level issues."""
|
|
57
|
+
# Import here to avoid circular dependency
|
|
58
|
+
from ..utils.validation import ValidationLevel
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
issue
|
|
62
|
+
for issue in self.issues
|
|
63
|
+
if hasattr(issue, 'level') and issue.level in (ValidationLevel.ERROR, ValidationLevel.CRITICAL)
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def get_warnings(self) -> List["ValidationIssue"]:
|
|
67
|
+
"""Get only warning-level issues."""
|
|
68
|
+
# Import here to avoid circular dependency
|
|
69
|
+
from ..utils.validation import ValidationLevel
|
|
70
|
+
|
|
71
|
+
return [issue for issue in self.issues if hasattr(issue, 'level') and issue.level == ValidationLevel.WARNING]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ReferenceError(ValidationError):
|
|
75
|
+
"""Raised when a component reference is invalid."""
|
|
76
|
+
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LibraryError(ValidationError):
|
|
81
|
+
"""Raised when a library or symbol reference is invalid."""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class GeometryError(ValidationError):
|
|
87
|
+
"""Raised when geometry validation fails (positions, shapes, dimensions)."""
|
|
88
|
+
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class NetError(ValidationError):
|
|
93
|
+
"""Raised when a net specification or operation is invalid."""
|
|
94
|
+
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ParseError(KiCadSchError):
|
|
99
|
+
"""Raised when parsing a schematic file fails."""
|
|
100
|
+
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class FormatError(KiCadSchError):
|
|
105
|
+
"""Raised when formatting a schematic file fails."""
|
|
106
|
+
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class CollectionError(KiCadSchError):
|
|
111
|
+
"""Raised when a collection operation fails."""
|
|
112
|
+
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ElementNotFoundError(CollectionError):
|
|
117
|
+
"""Raised when an element is not found in a collection."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, message: str, element_type: str = "", identifier: str = ""):
|
|
120
|
+
"""
|
|
121
|
+
Initialize element not found error.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
message: Error message
|
|
125
|
+
element_type: Type of element (e.g., 'component', 'wire', 'junction')
|
|
126
|
+
identifier: The identifier used to search (e.g., 'R1', UUID)
|
|
127
|
+
"""
|
|
128
|
+
self.element_type = element_type
|
|
129
|
+
self.identifier = identifier
|
|
130
|
+
super().__init__(message)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DuplicateElementError(CollectionError):
|
|
134
|
+
"""Raised when attempting to add a duplicate element."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, message: str, element_type: str = "", identifier: str = ""):
|
|
137
|
+
"""
|
|
138
|
+
Initialize duplicate element error.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
message: Error message
|
|
142
|
+
element_type: Type of element (e.g., 'component', 'wire', 'junction')
|
|
143
|
+
identifier: The duplicate identifier (e.g., 'R1', UUID)
|
|
144
|
+
"""
|
|
145
|
+
self.element_type = element_type
|
|
146
|
+
self.identifier = identifier
|
|
147
|
+
super().__init__(message)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CollectionOperationError(CollectionError):
|
|
151
|
+
"""Raised when a collection operation fails for reasons other than not found/duplicate."""
|
|
152
|
+
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FileOperationError(KiCadSchError):
|
|
157
|
+
"""Raised when a file I/O operation fails."""
|
|
158
|
+
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CLIError(KiCadSchError):
|
|
163
|
+
"""Raised when KiCad CLI execution fails."""
|
|
164
|
+
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class SchematicStateError(KiCadSchError):
|
|
169
|
+
"""
|
|
170
|
+
Raised when an operation requires specific schematic state.
|
|
171
|
+
|
|
172
|
+
Examples: schematic must be saved before export, etc.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
pass
|
|
@@ -126,7 +126,7 @@ class ElementFactory:
|
|
|
126
126
|
uuid=label_dict.get("uuid", str(uuid.uuid4())),
|
|
127
127
|
position=pos,
|
|
128
128
|
text=label_dict.get("text", ""),
|
|
129
|
-
label_type=LabelType(label_dict.get("label_type", "
|
|
129
|
+
label_type=LabelType(label_dict.get("label_type", "label")),
|
|
130
130
|
rotation=label_dict.get("rotation", 0.0),
|
|
131
131
|
size=label_dict.get("size", 1.27),
|
|
132
132
|
shape=(
|
|
@@ -134,6 +134,8 @@ class ElementFactory:
|
|
|
134
134
|
if label_dict.get("shape")
|
|
135
135
|
else None
|
|
136
136
|
),
|
|
137
|
+
justify_h=label_dict.get("justify_h", "left"),
|
|
138
|
+
justify_v=label_dict.get("justify_v", "bottom"),
|
|
137
139
|
)
|
|
138
140
|
|
|
139
141
|
@staticmethod
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -125,6 +125,10 @@ class ExactFormatter:
|
|
|
125
125
|
self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
|
|
126
126
|
self.rules["hierarchical_label"] = FormatRule(inline=False, quote_indices={1})
|
|
127
127
|
|
|
128
|
+
# Text elements
|
|
129
|
+
self.rules["text"] = FormatRule(inline=False, quote_indices={1})
|
|
130
|
+
self.rules["text_box"] = FormatRule(inline=False, quote_indices={1})
|
|
131
|
+
|
|
128
132
|
# Effects and text formatting
|
|
129
133
|
self.rules["effects"] = FormatRule(inline=False)
|
|
130
134
|
self.rules["font"] = FormatRule(inline=False)
|
|
@@ -279,6 +283,8 @@ class ExactFormatter:
|
|
|
279
283
|
"junction",
|
|
280
284
|
"label",
|
|
281
285
|
"hierarchical_label",
|
|
286
|
+
"text",
|
|
287
|
+
"text_box",
|
|
282
288
|
"polyline",
|
|
283
289
|
"rectangle",
|
|
284
290
|
):
|
|
@@ -384,7 +390,8 @@ class ExactFormatter:
|
|
|
384
390
|
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
385
391
|
else:
|
|
386
392
|
if i in rule.quote_indices and isinstance(element, str):
|
|
387
|
-
|
|
393
|
+
escaped_element = self._escape_string(element)
|
|
394
|
+
result += f' "{escaped_element}"'
|
|
388
395
|
else:
|
|
389
396
|
result += f" {self._format_element(element, 0)}"
|
|
390
397
|
|
|
@@ -396,7 +403,8 @@ class ExactFormatter:
|
|
|
396
403
|
indent = "\t" * indent_level
|
|
397
404
|
next_indent = "\t" * (indent_level + 1)
|
|
398
405
|
|
|
399
|
-
|
|
406
|
+
tag = str(lst[0])
|
|
407
|
+
result = f"({tag}"
|
|
400
408
|
|
|
401
409
|
for i, element in enumerate(lst[1:], 1):
|
|
402
410
|
if isinstance(element, list):
|
|
@@ -425,9 +433,18 @@ class ExactFormatter:
|
|
|
425
433
|
return True
|
|
426
434
|
|
|
427
435
|
def _escape_string(self, text: str) -> str:
|
|
428
|
-
"""Escape
|
|
429
|
-
#
|
|
430
|
-
|
|
436
|
+
"""Escape special characters in string for S-expression formatting."""
|
|
437
|
+
# Escape backslashes first (must be done before other replacements)
|
|
438
|
+
text = text.replace('\\', '\\\\')
|
|
439
|
+
# Escape double quotes
|
|
440
|
+
text = text.replace('"', '\\"')
|
|
441
|
+
# Escape newlines (convert actual newlines to escaped representation)
|
|
442
|
+
text = text.replace('\n', '\\n')
|
|
443
|
+
# Escape carriage returns
|
|
444
|
+
text = text.replace('\r', '\\r')
|
|
445
|
+
# Escape tabs
|
|
446
|
+
text = text.replace('\t', '\\t')
|
|
447
|
+
return text
|
|
431
448
|
|
|
432
449
|
def _needs_quoting(self, text: str) -> bool:
|
|
433
450
|
"""Check if string needs to be quoted."""
|
|
@@ -466,8 +483,8 @@ class ExactFormatter:
|
|
|
466
483
|
for item in lst[1:]:
|
|
467
484
|
if isinstance(item, list) and len(item) >= 1:
|
|
468
485
|
tag = str(item[0])
|
|
469
|
-
if tag in ["version", "generator", "generator_version"] and len(item) >= 2:
|
|
470
|
-
if tag in ["generator", "generator_version"]:
|
|
486
|
+
if tag in ["version", "generator", "generator_version", "uuid"] and len(item) >= 2:
|
|
487
|
+
if tag in ["generator", "generator_version", "uuid"]:
|
|
471
488
|
header_parts.append(f'({tag} "{item[1]}")')
|
|
472
489
|
else:
|
|
473
490
|
header_parts.append(f"({tag} {item[1]})")
|
kicad_sch_api/core/geometry.py
CHANGED
|
@@ -7,7 +7,7 @@ migrated from circuit-synth for improved maintainability.
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
import math
|
|
10
|
-
from typing import Optional, Tuple
|
|
10
|
+
from typing import Optional, Tuple, Union
|
|
11
11
|
|
|
12
12
|
from .types import Point
|
|
13
13
|
|
|
@@ -71,20 +71,29 @@ def apply_transformation(
|
|
|
71
71
|
|
|
72
72
|
Migrated from circuit-synth for accurate pin position calculation.
|
|
73
73
|
|
|
74
|
+
CRITICAL: Symbol coordinates use normal Y-axis (+Y is up), but schematic
|
|
75
|
+
coordinates use inverted Y-axis (+Y is down). We must negate Y from symbol
|
|
76
|
+
space before applying transformations.
|
|
77
|
+
|
|
74
78
|
Args:
|
|
75
|
-
point: Point to transform (x, y) relative to origin
|
|
76
|
-
origin: Component origin point
|
|
79
|
+
point: Point to transform (x, y) relative to origin in SYMBOL space
|
|
80
|
+
origin: Component origin point in SCHEMATIC space
|
|
77
81
|
rotation: Rotation in degrees (0, 90, 180, 270)
|
|
78
82
|
mirror: Mirror axis ("x" or "y" or None)
|
|
79
83
|
|
|
80
84
|
Returns:
|
|
81
|
-
Transformed absolute position (x, y)
|
|
85
|
+
Transformed absolute position (x, y) in SCHEMATIC space
|
|
82
86
|
"""
|
|
83
87
|
x, y = point
|
|
84
88
|
|
|
85
89
|
logger.debug(f"Transforming point ({x}, {y}) with rotation={rotation}°, mirror={mirror}")
|
|
86
90
|
|
|
87
|
-
#
|
|
91
|
+
# CRITICAL: Negate Y to convert from symbol space (normal Y) to schematic space (inverted Y)
|
|
92
|
+
# This must happen BEFORE rotation/mirroring
|
|
93
|
+
y = -y
|
|
94
|
+
logger.debug(f"After Y-axis inversion (symbol→schematic): ({x}, {y})")
|
|
95
|
+
|
|
96
|
+
# Apply mirroring
|
|
88
97
|
if mirror == "x":
|
|
89
98
|
x = -x
|
|
90
99
|
logger.debug(f"After X mirror: ({x}, {y})")
|
|
@@ -109,3 +118,83 @@ def apply_transformation(
|
|
|
109
118
|
|
|
110
119
|
logger.debug(f"Final absolute position: ({final_x}, {final_y})")
|
|
111
120
|
return (final_x, final_y)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def calculate_position_for_pin(
|
|
124
|
+
pin_local_position: Union[Point, Tuple[float, float]],
|
|
125
|
+
desired_pin_position: Union[Point, Tuple[float, float]],
|
|
126
|
+
rotation: float = 0.0,
|
|
127
|
+
mirror: Optional[str] = None,
|
|
128
|
+
grid_size: float = 1.27,
|
|
129
|
+
) -> Point:
|
|
130
|
+
"""
|
|
131
|
+
Calculate component position needed to place a specific pin at a desired location.
|
|
132
|
+
|
|
133
|
+
This is the inverse of get_pin_position() - given where you want a pin to be,
|
|
134
|
+
it calculates where the component center needs to be placed.
|
|
135
|
+
|
|
136
|
+
Useful for aligning components by their pins rather than their centers, which
|
|
137
|
+
is essential for clean horizontal signal flows without unnecessary wire jogs.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
pin_local_position: Pin position in symbol space (from symbol definition)
|
|
141
|
+
desired_pin_position: Where you want the pin to be in schematic space
|
|
142
|
+
rotation: Component rotation in degrees (0, 90, 180, 270)
|
|
143
|
+
mirror: Mirror axis ("x" or "y" or None) - currently unused
|
|
144
|
+
grid_size: Grid size for snapping result (default 1.27mm = 50mil)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Component position that will place the pin at desired_pin_position
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
>>> # Place resistor so pin 2 is at (150, 100)
|
|
151
|
+
>>> pin_pos = Point(0, -3.81) # Pin 2 local position from symbol
|
|
152
|
+
>>> comp_pos = calculate_position_for_pin(pin_pos, (150, 100))
|
|
153
|
+
>>> # Now add component at comp_pos, and pin 2 will be at (150, 100)
|
|
154
|
+
|
|
155
|
+
Note:
|
|
156
|
+
The result is automatically snapped to the KiCAD grid for proper connectivity.
|
|
157
|
+
This function matches the behavior of SchematicSymbol.get_pin_position().
|
|
158
|
+
"""
|
|
159
|
+
# Convert inputs to proper types
|
|
160
|
+
if isinstance(pin_local_position, Point):
|
|
161
|
+
pin_x, pin_y = pin_local_position.x, pin_local_position.y
|
|
162
|
+
else:
|
|
163
|
+
pin_x, pin_y = pin_local_position
|
|
164
|
+
|
|
165
|
+
if isinstance(desired_pin_position, Point):
|
|
166
|
+
target_x, target_y = desired_pin_position.x, desired_pin_position.y
|
|
167
|
+
else:
|
|
168
|
+
target_x, target_y = desired_pin_position
|
|
169
|
+
|
|
170
|
+
logger.debug(
|
|
171
|
+
f"Calculating component position for pin at local ({pin_x}, {pin_y}) "
|
|
172
|
+
f"to reach target ({target_x}, {target_y}) with rotation={rotation}°"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Apply the same transformation that get_pin_position() uses
|
|
176
|
+
# This is a standard 2D rotation matrix (NO Y-axis inversion)
|
|
177
|
+
angle_rad = math.radians(rotation)
|
|
178
|
+
cos_a = math.cos(angle_rad)
|
|
179
|
+
sin_a = math.sin(angle_rad)
|
|
180
|
+
|
|
181
|
+
# Calculate rotated offset (same as get_pin_position)
|
|
182
|
+
rotated_x = pin_x * cos_a - pin_y * sin_a
|
|
183
|
+
rotated_y = pin_x * sin_a + pin_y * cos_a
|
|
184
|
+
|
|
185
|
+
logger.debug(f"Pin offset after rotation: ({rotated_x:.3f}, {rotated_y:.3f})")
|
|
186
|
+
|
|
187
|
+
# Calculate component origin
|
|
188
|
+
# Since: target = component + rotated_offset
|
|
189
|
+
# Therefore: component = target - rotated_offset
|
|
190
|
+
component_x = target_x - rotated_x
|
|
191
|
+
component_y = target_y - rotated_y
|
|
192
|
+
|
|
193
|
+
logger.debug(f"Calculated component position (before grid snap): ({component_x:.3f}, {component_y:.3f})")
|
|
194
|
+
|
|
195
|
+
# Snap to grid for proper KiCAD connectivity
|
|
196
|
+
snapped_x, snapped_y = snap_to_grid((component_x, component_y), grid_size=grid_size)
|
|
197
|
+
|
|
198
|
+
logger.debug(f"Final component position (after grid snap): ({snapped_x:.3f}, {snapped_y:.3f})")
|
|
199
|
+
|
|
200
|
+
return Point(snapped_x, snapped_y)
|
|
@@ -5,9 +5,11 @@ This package contains specialized managers for different aspects of schematic
|
|
|
5
5
|
manipulation, enabling clean separation of concerns and better maintainability.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from .base import BaseManager
|
|
8
9
|
from .file_io import FileIOManager
|
|
9
10
|
from .format_sync import FormatSyncManager
|
|
10
11
|
from .graphics import GraphicsManager
|
|
12
|
+
from .hierarchy import HierarchyManager
|
|
11
13
|
from .metadata import MetadataManager
|
|
12
14
|
from .sheet import SheetManager
|
|
13
15
|
from .text_elements import TextElementManager
|
|
@@ -15,9 +17,11 @@ from .validation import ValidationManager
|
|
|
15
17
|
from .wire import WireManager
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
|
20
|
+
"BaseManager",
|
|
18
21
|
"FileIOManager",
|
|
19
22
|
"FormatSyncManager",
|
|
20
23
|
"GraphicsManager",
|
|
24
|
+
"HierarchyManager",
|
|
21
25
|
"MetadataManager",
|
|
22
26
|
"SheetManager",
|
|
23
27
|
"TextElementManager",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Base manager class for schematic operations.
|
|
2
|
+
|
|
3
|
+
Provides a consistent interface for all manager classes and enforces
|
|
4
|
+
common patterns for validation and data access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC
|
|
8
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from ...utils.validation import ValidationIssue
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..schematic import Schematic
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseManager(ABC):
|
|
17
|
+
"""Base class for all schematic managers.
|
|
18
|
+
|
|
19
|
+
Managers encapsulate complex operations and keep Schematic focused.
|
|
20
|
+
This base class provides a consistent interface and common utilities
|
|
21
|
+
for all managers.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
_data: Reference to schematic data (optional, varies by manager)
|
|
25
|
+
_schematic: Reference to parent Schematic instance (optional)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, schematic_data: Optional[Dict[str, Any]] = None, **kwargs):
|
|
29
|
+
"""Initialize manager with schematic data reference.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
schematic_data: Reference to schematic data dictionary (optional)
|
|
33
|
+
**kwargs: Additional manager-specific parameters
|
|
34
|
+
"""
|
|
35
|
+
self._data = schematic_data
|
|
36
|
+
self._schematic: Optional["Schematic"] = None
|
|
37
|
+
|
|
38
|
+
def set_schematic(self, schematic: "Schematic") -> None:
|
|
39
|
+
"""Set reference to parent schematic.
|
|
40
|
+
|
|
41
|
+
This is called by Schematic after manager initialization to establish
|
|
42
|
+
bidirectional relationship.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
schematic: The parent Schematic instance
|
|
46
|
+
"""
|
|
47
|
+
self._schematic = schematic
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def schematic(self) -> Optional["Schematic"]:
|
|
51
|
+
"""Get the parent schematic instance.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The parent Schematic, or None if not set
|
|
55
|
+
"""
|
|
56
|
+
return self._schematic
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def data(self) -> Optional[Dict[str, Any]]:
|
|
60
|
+
"""Get the schematic data dictionary.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The schematic data, or None if not set
|
|
64
|
+
"""
|
|
65
|
+
return self._data
|
|
66
|
+
|
|
67
|
+
def validate(self) -> List[ValidationIssue]:
|
|
68
|
+
"""Validate managed elements.
|
|
69
|
+
|
|
70
|
+
This is an optional method that managers can override to provide
|
|
71
|
+
validation. Default implementation returns empty list (no issues).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of validation issues found
|
|
75
|
+
"""
|
|
76
|
+
return []
|
|
@@ -14,11 +14,12 @@ from ...utils.validation import ValidationError
|
|
|
14
14
|
from ..config import config
|
|
15
15
|
from ..formatter import ExactFormatter
|
|
16
16
|
from ..parser import SExpressionParser
|
|
17
|
+
from .base import BaseManager
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class FileIOManager:
|
|
22
|
+
class FileIOManager(BaseManager):
|
|
22
23
|
"""
|
|
23
24
|
Manages file I/O operations for KiCAD schematics.
|
|
24
25
|
|
|
@@ -31,6 +32,7 @@ class FileIOManager:
|
|
|
31
32
|
|
|
32
33
|
def __init__(self):
|
|
33
34
|
"""Initialize the FileIOManager."""
|
|
35
|
+
super().__init__()
|
|
34
36
|
self._parser = SExpressionParser(preserve_format=True)
|
|
35
37
|
self._formatter = ExactFormatter()
|
|
36
38
|
|
|
@@ -11,11 +11,12 @@ from typing import Any, Dict, List, Optional, Set, Union
|
|
|
11
11
|
|
|
12
12
|
from ..components import Component
|
|
13
13
|
from ..types import Point, Wire
|
|
14
|
+
from .base import BaseManager
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class FormatSyncManager:
|
|
19
|
+
class FormatSyncManager(BaseManager):
|
|
19
20
|
"""
|
|
20
21
|
Manages synchronization between object models and S-expression data.
|
|
21
22
|
|
|
@@ -34,7 +35,7 @@ class FormatSyncManager:
|
|
|
34
35
|
Args:
|
|
35
36
|
schematic_data: Reference to schematic data
|
|
36
37
|
"""
|
|
37
|
-
|
|
38
|
+
super().__init__(schematic_data)
|
|
38
39
|
self._dirty_flags: Set[str] = set()
|
|
39
40
|
self._change_log: List[Dict[str, Any]] = []
|
|
40
41
|
self._sync_lock = False
|
|
@@ -11,11 +11,12 @@ import uuid
|
|
|
11
11
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
12
12
|
|
|
13
13
|
from ..types import Point
|
|
14
|
+
from .base import BaseManager
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class GraphicsManager:
|
|
19
|
+
class GraphicsManager(BaseManager):
|
|
19
20
|
"""
|
|
20
21
|
Manages graphic elements and drawing shapes in KiCAD schematics.
|
|
21
22
|
|
|
@@ -34,7 +35,7 @@ class GraphicsManager:
|
|
|
34
35
|
Args:
|
|
35
36
|
schematic_data: Reference to schematic data
|
|
36
37
|
"""
|
|
37
|
-
|
|
38
|
+
super().__init__(schematic_data)
|
|
38
39
|
|
|
39
40
|
def add_rectangle(
|
|
40
41
|
self,
|