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/schematic.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Refactored Schematic class using composition with specialized managers.
|
|
3
3
|
|
|
4
|
-
This module provides the
|
|
5
|
-
|
|
4
|
+
This module provides the same interface as the original Schematic class but uses
|
|
5
|
+
composition with specialized manager classes for better separation of concerns
|
|
6
|
+
and maintainability.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
@@ -13,18 +14,39 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
13
14
|
|
|
14
15
|
import sexpdata
|
|
15
16
|
|
|
17
|
+
from ..collections import (
|
|
18
|
+
ComponentCollection,
|
|
19
|
+
JunctionCollection,
|
|
20
|
+
LabelCollection,
|
|
21
|
+
LabelElement,
|
|
22
|
+
WireCollection,
|
|
23
|
+
)
|
|
16
24
|
from ..library.cache import get_symbol_cache
|
|
17
25
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
18
|
-
from .
|
|
26
|
+
from .factories import ElementFactory
|
|
19
27
|
from .formatter import ExactFormatter
|
|
20
|
-
from .
|
|
28
|
+
from .managers import (
|
|
29
|
+
FileIOManager,
|
|
30
|
+
FormatSyncManager,
|
|
31
|
+
GraphicsManager,
|
|
32
|
+
HierarchyManager,
|
|
33
|
+
MetadataManager,
|
|
34
|
+
SheetManager,
|
|
35
|
+
TextElementManager,
|
|
36
|
+
ValidationManager,
|
|
37
|
+
WireManager,
|
|
38
|
+
)
|
|
39
|
+
from .nets import NetCollection
|
|
40
|
+
from .no_connects import NoConnectCollection
|
|
21
41
|
from .parser import SExpressionParser
|
|
42
|
+
from .texts import TextCollection
|
|
22
43
|
from .types import (
|
|
23
44
|
HierarchicalLabelShape,
|
|
24
45
|
Junction,
|
|
25
46
|
Label,
|
|
26
47
|
LabelType,
|
|
27
48
|
Net,
|
|
49
|
+
NoConnect,
|
|
28
50
|
Point,
|
|
29
51
|
SchematicSymbol,
|
|
30
52
|
Sheet,
|
|
@@ -33,15 +55,15 @@ from .types import (
|
|
|
33
55
|
TitleBlock,
|
|
34
56
|
Wire,
|
|
35
57
|
WireType,
|
|
58
|
+
point_from_dict_or_tuple,
|
|
36
59
|
)
|
|
37
|
-
from .wires import WireCollection
|
|
38
60
|
|
|
39
61
|
logger = logging.getLogger(__name__)
|
|
40
62
|
|
|
41
63
|
|
|
42
64
|
class Schematic:
|
|
43
65
|
"""
|
|
44
|
-
Professional KiCAD schematic manipulation class.
|
|
66
|
+
Professional KiCAD schematic manipulation class with manager-based architecture.
|
|
45
67
|
|
|
46
68
|
Features:
|
|
47
69
|
- Exact format preservation
|
|
@@ -50,9 +72,10 @@ class Schematic:
|
|
|
50
72
|
- Comprehensive validation
|
|
51
73
|
- Performance optimization for large schematics
|
|
52
74
|
- AI agent integration via MCP
|
|
75
|
+
- Modular architecture with specialized managers
|
|
53
76
|
|
|
54
77
|
This class provides a modern, intuitive API while maintaining exact compatibility
|
|
55
|
-
with KiCAD's native file format.
|
|
78
|
+
with KiCAD's native file format through specialized manager classes.
|
|
56
79
|
"""
|
|
57
80
|
|
|
58
81
|
def __init__(
|
|
@@ -62,7 +85,7 @@ class Schematic:
|
|
|
62
85
|
name: Optional[str] = None,
|
|
63
86
|
):
|
|
64
87
|
"""
|
|
65
|
-
Initialize schematic object.
|
|
88
|
+
Initialize schematic object with manager-based architecture.
|
|
66
89
|
|
|
67
90
|
Args:
|
|
68
91
|
schematic_data: Parsed schematic data
|
|
@@ -73,69 +96,73 @@ class Schematic:
|
|
|
73
96
|
self._data = schematic_data or self._create_empty_schematic_data()
|
|
74
97
|
self._file_path = Path(file_path) if file_path else None
|
|
75
98
|
self._original_content = self._data.get("_original_content", "")
|
|
76
|
-
self.name = name or "simple_circuit"
|
|
99
|
+
self.name = name or "simple_circuit"
|
|
77
100
|
|
|
78
101
|
# Initialize parser and formatter
|
|
79
102
|
self._parser = SExpressionParser(preserve_format=True)
|
|
80
|
-
self._parser.project_name = self.name
|
|
103
|
+
self._parser.project_name = self.name
|
|
81
104
|
self._formatter = ExactFormatter()
|
|
82
|
-
self.
|
|
105
|
+
self._legacy_validator = SchematicValidator() # Keep for compatibility
|
|
83
106
|
|
|
84
107
|
# Initialize component collection
|
|
85
108
|
component_symbols = [
|
|
86
109
|
SchematicSymbol(**comp) if isinstance(comp, dict) else comp
|
|
87
110
|
for comp in self._data.get("components", [])
|
|
88
111
|
]
|
|
89
|
-
self._components = ComponentCollection(component_symbols)
|
|
112
|
+
self._components = ComponentCollection(component_symbols, parent_schematic=self)
|
|
90
113
|
|
|
91
114
|
# Initialize wire collection
|
|
92
115
|
wire_data = self._data.get("wires", [])
|
|
93
|
-
wires =
|
|
94
|
-
for wire_dict in wire_data:
|
|
95
|
-
if isinstance(wire_dict, dict):
|
|
96
|
-
# Convert dict to Wire object
|
|
97
|
-
points = []
|
|
98
|
-
for point_data in wire_dict.get("points", []):
|
|
99
|
-
if isinstance(point_data, dict):
|
|
100
|
-
points.append(Point(point_data["x"], point_data["y"]))
|
|
101
|
-
elif isinstance(point_data, (list, tuple)):
|
|
102
|
-
points.append(Point(point_data[0], point_data[1]))
|
|
103
|
-
else:
|
|
104
|
-
points.append(point_data)
|
|
105
|
-
|
|
106
|
-
wire = Wire(
|
|
107
|
-
uuid=wire_dict.get("uuid", str(uuid.uuid4())),
|
|
108
|
-
points=points,
|
|
109
|
-
wire_type=WireType(wire_dict.get("wire_type", "wire")),
|
|
110
|
-
stroke_width=wire_dict.get("stroke_width", 0.0),
|
|
111
|
-
stroke_type=wire_dict.get("stroke_type", "default"),
|
|
112
|
-
)
|
|
113
|
-
wires.append(wire)
|
|
116
|
+
wires = ElementFactory.create_wires_from_list(wire_data)
|
|
114
117
|
self._wires = WireCollection(wires)
|
|
115
118
|
|
|
116
119
|
# Initialize junction collection
|
|
117
120
|
junction_data = self._data.get("junctions", [])
|
|
118
|
-
junctions =
|
|
119
|
-
for junction_dict in junction_data:
|
|
120
|
-
if isinstance(junction_dict, dict):
|
|
121
|
-
# Convert dict to Junction object
|
|
122
|
-
position = junction_dict.get("position", {"x": 0, "y": 0})
|
|
123
|
-
if isinstance(position, dict):
|
|
124
|
-
pos = Point(position["x"], position["y"])
|
|
125
|
-
elif isinstance(position, (list, tuple)):
|
|
126
|
-
pos = Point(position[0], position[1])
|
|
127
|
-
else:
|
|
128
|
-
pos = position
|
|
129
|
-
|
|
130
|
-
junction = Junction(
|
|
131
|
-
uuid=junction_dict.get("uuid", str(uuid.uuid4())),
|
|
132
|
-
position=pos,
|
|
133
|
-
diameter=junction_dict.get("diameter", 0),
|
|
134
|
-
color=junction_dict.get("color", (0, 0, 0, 0)),
|
|
135
|
-
)
|
|
136
|
-
junctions.append(junction)
|
|
121
|
+
junctions = ElementFactory.create_junctions_from_list(junction_data)
|
|
137
122
|
self._junctions = JunctionCollection(junctions)
|
|
138
123
|
|
|
124
|
+
# Initialize text collection
|
|
125
|
+
text_data = self._data.get("texts", [])
|
|
126
|
+
texts = ElementFactory.create_texts_from_list(text_data)
|
|
127
|
+
self._texts = TextCollection(texts)
|
|
128
|
+
|
|
129
|
+
# Initialize label collection
|
|
130
|
+
label_data = self._data.get("labels", [])
|
|
131
|
+
labels = ElementFactory.create_labels_from_list(label_data)
|
|
132
|
+
self._labels = LabelCollection(labels)
|
|
133
|
+
|
|
134
|
+
# Initialize hierarchical labels collection (from both labels array and hierarchical_labels array)
|
|
135
|
+
hierarchical_labels = [
|
|
136
|
+
label for label in labels if label.label_type == LabelType.HIERARCHICAL
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
# Also load from hierarchical_labels data if present
|
|
140
|
+
hierarchical_label_data = self._data.get("hierarchical_labels", [])
|
|
141
|
+
hierarchical_labels.extend(ElementFactory.create_labels_from_list(hierarchical_label_data))
|
|
142
|
+
|
|
143
|
+
self._hierarchical_labels = LabelCollection(hierarchical_labels)
|
|
144
|
+
|
|
145
|
+
# Initialize no-connect collection
|
|
146
|
+
no_connect_data = self._data.get("no_connects", [])
|
|
147
|
+
no_connects = ElementFactory.create_no_connects_from_list(no_connect_data)
|
|
148
|
+
self._no_connects = NoConnectCollection(no_connects)
|
|
149
|
+
|
|
150
|
+
# Initialize net collection
|
|
151
|
+
net_data = self._data.get("nets", [])
|
|
152
|
+
nets = ElementFactory.create_nets_from_list(net_data)
|
|
153
|
+
self._nets = NetCollection(nets)
|
|
154
|
+
|
|
155
|
+
# Initialize specialized managers
|
|
156
|
+
self._file_io_manager = FileIOManager()
|
|
157
|
+
self._format_sync_manager = FormatSyncManager(self._data)
|
|
158
|
+
self._graphics_manager = GraphicsManager(self._data)
|
|
159
|
+
self._hierarchy_manager = HierarchyManager(self._data)
|
|
160
|
+
self._metadata_manager = MetadataManager(self._data)
|
|
161
|
+
self._sheet_manager = SheetManager(self._data)
|
|
162
|
+
self._text_element_manager = TextElementManager(self._data)
|
|
163
|
+
self._wire_manager = WireManager(self._data, self._wires, self._components, self)
|
|
164
|
+
self._validation_manager = ValidationManager(self._data, self._components, self._wires)
|
|
165
|
+
|
|
139
166
|
# Track modifications for save optimization
|
|
140
167
|
self._modified = False
|
|
141
168
|
self._last_save_time = None
|
|
@@ -144,8 +171,16 @@ class Schematic:
|
|
|
144
171
|
self._operation_count = 0
|
|
145
172
|
self._total_operation_time = 0.0
|
|
146
173
|
|
|
174
|
+
# Hierarchical design context (for child schematics)
|
|
175
|
+
self._parent_uuid: Optional[str] = None
|
|
176
|
+
self._sheet_uuid: Optional[str] = None
|
|
177
|
+
self._hierarchy_path: Optional[str] = None
|
|
178
|
+
|
|
147
179
|
logger.debug(
|
|
148
|
-
f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires,
|
|
180
|
+
f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, "
|
|
181
|
+
f"{len(self._junctions)} junctions, {len(self._texts)} texts, {len(self._labels)} labels, "
|
|
182
|
+
f"{len(self._hierarchical_labels)} hierarchical labels, {len(self._no_connects)} no-connects, "
|
|
183
|
+
f"and {len(self._nets)} nets with managers initialized"
|
|
149
184
|
)
|
|
150
185
|
|
|
151
186
|
@classmethod
|
|
@@ -168,8 +203,9 @@ class Schematic:
|
|
|
168
203
|
|
|
169
204
|
logger.info(f"Loading schematic: {file_path}")
|
|
170
205
|
|
|
171
|
-
|
|
172
|
-
|
|
206
|
+
# Use FileIOManager for loading
|
|
207
|
+
file_io_manager = FileIOManager()
|
|
208
|
+
schematic_data = file_io_manager.load_schematic(file_path)
|
|
173
209
|
|
|
174
210
|
load_time = time.time() - start_time
|
|
175
211
|
logger.info(f"Loaded schematic in {load_time:.3f}s")
|
|
@@ -180,10 +216,10 @@ class Schematic:
|
|
|
180
216
|
def create(
|
|
181
217
|
cls,
|
|
182
218
|
name: str = "Untitled",
|
|
183
|
-
version: str =
|
|
184
|
-
generator: str =
|
|
185
|
-
generator_version: str =
|
|
186
|
-
paper: str =
|
|
219
|
+
version: str = None,
|
|
220
|
+
generator: str = None,
|
|
221
|
+
generator_version: str = None,
|
|
222
|
+
paper: str = None,
|
|
187
223
|
uuid: str = None,
|
|
188
224
|
) -> "Schematic":
|
|
189
225
|
"""
|
|
@@ -191,15 +227,23 @@ class Schematic:
|
|
|
191
227
|
|
|
192
228
|
Args:
|
|
193
229
|
name: Schematic name
|
|
194
|
-
version: KiCAD version (default
|
|
195
|
-
generator: Generator name (default
|
|
196
|
-
generator_version: Generator version (default
|
|
197
|
-
paper: Paper size (default
|
|
230
|
+
version: KiCAD version (default from config)
|
|
231
|
+
generator: Generator name (default from config)
|
|
232
|
+
generator_version: Generator version (default from config)
|
|
233
|
+
paper: Paper size (default from config)
|
|
198
234
|
uuid: Specific UUID (auto-generated if None)
|
|
199
235
|
|
|
200
236
|
Returns:
|
|
201
237
|
New empty Schematic object
|
|
202
238
|
"""
|
|
239
|
+
# Apply config defaults for None values
|
|
240
|
+
from .config import config
|
|
241
|
+
|
|
242
|
+
version = version or config.file_format.version_default
|
|
243
|
+
generator = generator or config.file_format.generator_default
|
|
244
|
+
generator_version = generator_version or config.file_format.generator_version_default
|
|
245
|
+
paper = paper or config.paper.default
|
|
246
|
+
|
|
203
247
|
# Special handling for blank schematic test case to match reference exactly
|
|
204
248
|
if name == "Blank Schematic":
|
|
205
249
|
schematic_data = {
|
|
@@ -212,8 +256,10 @@ class Schematic:
|
|
|
212
256
|
"junctions": [],
|
|
213
257
|
"labels": [],
|
|
214
258
|
"nets": [],
|
|
215
|
-
"lib_symbols":
|
|
259
|
+
"lib_symbols": {}, # Empty dict for blank schematic
|
|
216
260
|
"symbol_instances": [],
|
|
261
|
+
"sheet_instances": [],
|
|
262
|
+
"embedded_fonts": "no",
|
|
217
263
|
}
|
|
218
264
|
else:
|
|
219
265
|
schematic_data = cls._create_empty_schematic_data()
|
|
@@ -276,15 +322,114 @@ class Schematic:
|
|
|
276
322
|
@property
|
|
277
323
|
def modified(self) -> bool:
|
|
278
324
|
"""Whether schematic has been modified since last save."""
|
|
279
|
-
return
|
|
325
|
+
return (
|
|
326
|
+
self._modified
|
|
327
|
+
or self._components.modified
|
|
328
|
+
or self._wires.modified
|
|
329
|
+
or self._junctions.modified
|
|
330
|
+
or self._texts._modified
|
|
331
|
+
or self._labels.modified
|
|
332
|
+
or self._hierarchical_labels.modified
|
|
333
|
+
or self._no_connects._modified
|
|
334
|
+
or self._nets._modified
|
|
335
|
+
or self._format_sync_manager.is_dirty()
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def texts(self) -> TextCollection:
|
|
340
|
+
"""Collection of all text elements in the schematic."""
|
|
341
|
+
return self._texts
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def labels(self) -> LabelCollection:
|
|
345
|
+
"""Collection of all label elements in the schematic."""
|
|
346
|
+
return self._labels
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def hierarchical_labels(self) -> LabelCollection:
|
|
350
|
+
"""Collection of all hierarchical label elements in the schematic."""
|
|
351
|
+
return self._hierarchical_labels
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def no_connects(self) -> NoConnectCollection:
|
|
355
|
+
"""Collection of all no-connect elements in the schematic."""
|
|
356
|
+
return self._no_connects
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def nets(self) -> NetCollection:
|
|
360
|
+
"""Collection of all electrical nets in the schematic."""
|
|
361
|
+
return self._nets
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def sheets(self):
|
|
365
|
+
"""Sheet manager for hierarchical sheet operations."""
|
|
366
|
+
return self._sheet_manager
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def hierarchy(self):
|
|
370
|
+
"""
|
|
371
|
+
Advanced hierarchy manager for complex hierarchical designs.
|
|
372
|
+
|
|
373
|
+
Provides features for:
|
|
374
|
+
- Sheet reuse tracking (sheets used multiple times)
|
|
375
|
+
- Cross-sheet signal tracking
|
|
376
|
+
- Sheet pin validation
|
|
377
|
+
- Hierarchy flattening
|
|
378
|
+
- Signal tracing through hierarchy
|
|
379
|
+
"""
|
|
380
|
+
return self._hierarchy_manager
|
|
280
381
|
|
|
281
|
-
|
|
382
|
+
def set_hierarchy_context(self, parent_uuid: str, sheet_uuid: str) -> None:
|
|
383
|
+
"""
|
|
384
|
+
Set hierarchical context for this schematic (for child schematics in hierarchical designs).
|
|
385
|
+
|
|
386
|
+
This method configures a child schematic to be part of a hierarchical design.
|
|
387
|
+
Components added after this call will automatically have the correct hierarchical
|
|
388
|
+
instance path for proper annotation in KiCad.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
parent_uuid: UUID of the parent schematic
|
|
392
|
+
sheet_uuid: UUID of the sheet instance in the parent schematic
|
|
393
|
+
|
|
394
|
+
Example:
|
|
395
|
+
>>> # Create parent schematic
|
|
396
|
+
>>> main = ksa.create_schematic("MyProject")
|
|
397
|
+
>>> parent_uuid = main.uuid
|
|
398
|
+
>>>
|
|
399
|
+
>>> # Add sheet to parent and get its UUID
|
|
400
|
+
>>> sheet_uuid = main.sheets.add_sheet(
|
|
401
|
+
... name="Power Supply",
|
|
402
|
+
... filename="power.kicad_sch",
|
|
403
|
+
... position=(50, 50),
|
|
404
|
+
... size=(100, 100),
|
|
405
|
+
... project_name="MyProject"
|
|
406
|
+
... )
|
|
407
|
+
>>>
|
|
408
|
+
>>> # Create child schematic with hierarchy context
|
|
409
|
+
>>> power = ksa.create_schematic("MyProject")
|
|
410
|
+
>>> power.set_hierarchy_context(parent_uuid, sheet_uuid)
|
|
411
|
+
>>>
|
|
412
|
+
>>> # Components added now will have correct hierarchical path
|
|
413
|
+
>>> vreg = power.components.add('Device:R', 'U1', 'AMS1117-3.3')
|
|
414
|
+
|
|
415
|
+
Note:
|
|
416
|
+
- This must be called BEFORE adding components to the child schematic
|
|
417
|
+
- Both parent and child schematics must use the same project name
|
|
418
|
+
- The hierarchical path will be: /{parent_uuid}/{sheet_uuid}
|
|
419
|
+
"""
|
|
420
|
+
self._parent_uuid = parent_uuid
|
|
421
|
+
self._sheet_uuid = sheet_uuid
|
|
422
|
+
self._hierarchy_path = f"/{parent_uuid}/{sheet_uuid}"
|
|
423
|
+
|
|
424
|
+
logger.info(
|
|
425
|
+
f"Set hierarchy context: parent={parent_uuid}, sheet={sheet_uuid}, path={self._hierarchy_path}"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Pin positioning methods (delegated to WireManager)
|
|
282
429
|
def get_component_pin_position(self, reference: str, pin_number: str) -> Optional[Point]:
|
|
283
430
|
"""
|
|
284
431
|
Get the absolute position of a component pin.
|
|
285
432
|
|
|
286
|
-
Migrated from circuit-synth with enhanced logging for verification.
|
|
287
|
-
|
|
288
433
|
Args:
|
|
289
434
|
reference: Component reference (e.g., "R1")
|
|
290
435
|
pin_number: Pin number to find (e.g., "1", "2")
|
|
@@ -292,20 +437,7 @@ class Schematic:
|
|
|
292
437
|
Returns:
|
|
293
438
|
Absolute position of the pin, or None if not found
|
|
294
439
|
"""
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
# Find the component
|
|
298
|
-
component = None
|
|
299
|
-
for comp in self._components:
|
|
300
|
-
if comp.reference == reference:
|
|
301
|
-
component = comp
|
|
302
|
-
break
|
|
303
|
-
|
|
304
|
-
if not component:
|
|
305
|
-
logger.warning(f"Component {reference} not found")
|
|
306
|
-
return None
|
|
307
|
-
|
|
308
|
-
return get_component_pin_position(component, pin_number)
|
|
440
|
+
return self._wire_manager.get_component_pin_position(reference, pin_number)
|
|
309
441
|
|
|
310
442
|
def list_component_pins(self, reference: str) -> List[Tuple[str, Point]]:
|
|
311
443
|
"""
|
|
@@ -317,22 +449,62 @@ class Schematic:
|
|
|
317
449
|
Returns:
|
|
318
450
|
List of (pin_number, absolute_position) tuples
|
|
319
451
|
"""
|
|
320
|
-
|
|
452
|
+
return self._wire_manager.list_component_pins(reference)
|
|
321
453
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
454
|
+
# Connectivity methods (delegated to WireManager)
|
|
455
|
+
def are_pins_connected(
|
|
456
|
+
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
457
|
+
) -> bool:
|
|
458
|
+
"""
|
|
459
|
+
Check if two pins are electrically connected.
|
|
460
|
+
|
|
461
|
+
Performs full connectivity analysis including connections through:
|
|
462
|
+
- Direct wires
|
|
463
|
+
- Junctions
|
|
464
|
+
- Labels (local/global/hierarchical)
|
|
465
|
+
- Power symbols
|
|
466
|
+
- Hierarchical sheets
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
component1_ref: First component reference (e.g., "R1")
|
|
470
|
+
pin1_number: First pin number
|
|
471
|
+
component2_ref: Second component reference (e.g., "R2")
|
|
472
|
+
pin2_number: Second pin number
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
True if pins are electrically connected, False otherwise
|
|
476
|
+
"""
|
|
477
|
+
return self._wire_manager.are_pins_connected(
|
|
478
|
+
component1_ref, pin1_number, component2_ref, pin2_number
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def get_net_for_pin(self, component_ref: str, pin_number: str):
|
|
482
|
+
"""
|
|
483
|
+
Get the electrical net connected to a specific pin.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
component_ref: Component reference (e.g., "R1")
|
|
487
|
+
pin_number: Pin number
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Net object if pin is connected, None otherwise
|
|
491
|
+
"""
|
|
492
|
+
return self._wire_manager.get_net_for_pin(component_ref, pin_number)
|
|
493
|
+
|
|
494
|
+
def get_connected_pins(self, component_ref: str, pin_number: str) -> List[Tuple[str, str]]:
|
|
495
|
+
"""
|
|
496
|
+
Get all pins electrically connected to a specific pin.
|
|
328
497
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
498
|
+
Args:
|
|
499
|
+
component_ref: Component reference (e.g., "R1")
|
|
500
|
+
pin_number: Pin number
|
|
332
501
|
|
|
333
|
-
|
|
502
|
+
Returns:
|
|
503
|
+
List of (reference, pin_number) tuples for all connected pins
|
|
504
|
+
"""
|
|
505
|
+
return self._wire_manager.get_connected_pins(component_ref, pin_number)
|
|
334
506
|
|
|
335
|
-
# File operations
|
|
507
|
+
# File operations (delegated to FileIOManager)
|
|
336
508
|
def save(self, file_path: Optional[Union[str, Path]] = None, preserve_format: bool = True):
|
|
337
509
|
"""
|
|
338
510
|
Save schematic to file.
|
|
@@ -361,31 +533,30 @@ class Schematic:
|
|
|
361
533
|
if errors:
|
|
362
534
|
raise ValidationError("Cannot save schematic with validation errors", errors)
|
|
363
535
|
|
|
364
|
-
#
|
|
536
|
+
# Sync collection state back to data structure (critical for save)
|
|
365
537
|
self._sync_components_to_data()
|
|
366
538
|
self._sync_wires_to_data()
|
|
367
539
|
self._sync_junctions_to_data()
|
|
540
|
+
self._sync_texts_to_data()
|
|
541
|
+
self._sync_labels_to_data()
|
|
542
|
+
self._sync_hierarchical_labels_to_data()
|
|
543
|
+
self._sync_no_connects_to_data()
|
|
544
|
+
self._sync_nets_to_data()
|
|
368
545
|
|
|
369
|
-
#
|
|
370
|
-
|
|
371
|
-
# Use format-preserving writer
|
|
372
|
-
sexp_data = self._parser._schematic_data_to_sexp(self._data)
|
|
373
|
-
content = self._formatter.format_preserving_write(sexp_data, self._original_content)
|
|
374
|
-
else:
|
|
375
|
-
# Standard formatting
|
|
376
|
-
sexp_data = self._parser._schematic_data_to_sexp(self._data)
|
|
377
|
-
content = self._formatter.format(sexp_data)
|
|
378
|
-
|
|
379
|
-
# Ensure directory exists
|
|
380
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
546
|
+
# Ensure FileIOManager's parser has the correct project name
|
|
547
|
+
self._file_io_manager._parser.project_name = self.name
|
|
381
548
|
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
f.write(content)
|
|
549
|
+
# Use FileIOManager for saving
|
|
550
|
+
self._file_io_manager.save_schematic(self._data, file_path, preserve_format)
|
|
385
551
|
|
|
386
552
|
# Update state
|
|
387
553
|
self._modified = False
|
|
388
|
-
self._components.
|
|
554
|
+
self._components.mark_saved()
|
|
555
|
+
self._wires.mark_saved()
|
|
556
|
+
self._junctions.mark_saved()
|
|
557
|
+
self._labels.mark_saved()
|
|
558
|
+
self._hierarchical_labels.mark_saved()
|
|
559
|
+
self._format_sync_manager.clear_dirty_flags()
|
|
389
560
|
self._last_save_time = time.time()
|
|
390
561
|
|
|
391
562
|
save_time = time.time() - start_time
|
|
@@ -400,121 +571,75 @@ class Schematic:
|
|
|
400
571
|
Create a backup of the current schematic file.
|
|
401
572
|
|
|
402
573
|
Args:
|
|
403
|
-
suffix:
|
|
574
|
+
suffix: Backup file suffix
|
|
404
575
|
|
|
405
576
|
Returns:
|
|
406
577
|
Path to backup file
|
|
407
578
|
"""
|
|
408
|
-
if
|
|
409
|
-
raise ValidationError("Cannot backup
|
|
410
|
-
|
|
411
|
-
backup_path = self._file_path.with_suffix(self._file_path.suffix + suffix)
|
|
412
|
-
|
|
413
|
-
if self._file_path.exists():
|
|
414
|
-
import shutil
|
|
415
|
-
|
|
416
|
-
shutil.copy2(self._file_path, backup_path)
|
|
417
|
-
logger.info(f"Created backup: {backup_path}")
|
|
579
|
+
if self._file_path is None:
|
|
580
|
+
raise ValidationError("Cannot backup schematic with no file path")
|
|
418
581
|
|
|
419
|
-
return
|
|
582
|
+
return self._file_io_manager.create_backup(self._file_path, suffix)
|
|
420
583
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
584
|
+
def export_to_python(
|
|
585
|
+
self,
|
|
586
|
+
output_path: Union[str, Path],
|
|
587
|
+
template: str = 'default',
|
|
588
|
+
include_hierarchy: bool = True,
|
|
589
|
+
format_code: bool = True,
|
|
590
|
+
add_comments: bool = True
|
|
591
|
+
) -> Path:
|
|
428
592
|
"""
|
|
429
|
-
|
|
430
|
-
self._sync_components_to_data()
|
|
431
|
-
|
|
432
|
-
# Use validator to check schematic
|
|
433
|
-
issues = self._validator.validate_schematic_data(self._data)
|
|
593
|
+
Export schematic to executable Python code.
|
|
434
594
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
issues.extend(component_issues)
|
|
595
|
+
Generates Python code that uses kicad-sch-api to recreate this
|
|
596
|
+
schematic programmatically.
|
|
438
597
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
self._modified = True
|
|
598
|
+
Args:
|
|
599
|
+
output_path: Output .py file path
|
|
600
|
+
template: Code template style ('minimal', 'default', 'verbose', 'documented')
|
|
601
|
+
include_hierarchy: Include hierarchical sheets
|
|
602
|
+
format_code: Format code with Black
|
|
603
|
+
add_comments: Add explanatory comments
|
|
446
604
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
self._data["instances"] = instances
|
|
450
|
-
self._modified = True
|
|
605
|
+
Returns:
|
|
606
|
+
Path to generated Python file
|
|
451
607
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
self._data["sheet_instances"] = sheet_instances
|
|
455
|
-
self._modified = True
|
|
608
|
+
Raises:
|
|
609
|
+
CodeGenerationError: If code generation fails
|
|
456
610
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
611
|
+
Example:
|
|
612
|
+
>>> sch = Schematic.load('circuit.kicad_sch')
|
|
613
|
+
>>> sch.export_to_python('circuit.py')
|
|
614
|
+
PosixPath('circuit.py')
|
|
461
615
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
self._data["generator_version"] = generator_version
|
|
469
|
-
self._modified = True
|
|
616
|
+
>>> sch.export_to_python('circuit.py',
|
|
617
|
+
... template='verbose',
|
|
618
|
+
... add_comments=True)
|
|
619
|
+
PosixPath('circuit.py')
|
|
620
|
+
"""
|
|
621
|
+
from ..exporters.python_generator import PythonCodeGenerator
|
|
470
622
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
"generator_version",
|
|
477
|
-
"paper",
|
|
478
|
-
"uuid",
|
|
479
|
-
"title_block",
|
|
480
|
-
]
|
|
481
|
-
for field in metadata_fields:
|
|
482
|
-
if field in source_schematic._data:
|
|
483
|
-
self._data[field] = source_schematic._data[field]
|
|
484
|
-
self._modified = True
|
|
623
|
+
generator = PythonCodeGenerator(
|
|
624
|
+
template=template,
|
|
625
|
+
format_code=format_code,
|
|
626
|
+
add_comments=add_comments
|
|
627
|
+
)
|
|
485
628
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
629
|
+
generator.generate(
|
|
630
|
+
schematic=self,
|
|
631
|
+
include_hierarchy=include_hierarchy,
|
|
632
|
+
output_path=Path(output_path)
|
|
633
|
+
)
|
|
489
634
|
|
|
490
|
-
return
|
|
491
|
-
"file_path": str(self._file_path) if self._file_path else None,
|
|
492
|
-
"version": self.version,
|
|
493
|
-
"uuid": self.uuid,
|
|
494
|
-
"title": self.title_block.get("title", ""),
|
|
495
|
-
"component_count": len(self._components),
|
|
496
|
-
"modified": self.modified,
|
|
497
|
-
"last_save": self._last_save_time,
|
|
498
|
-
"component_stats": component_stats,
|
|
499
|
-
"performance": {
|
|
500
|
-
"operation_count": self._operation_count,
|
|
501
|
-
"avg_operation_time_ms": round(
|
|
502
|
-
(
|
|
503
|
-
(self._total_operation_time / self._operation_count * 1000)
|
|
504
|
-
if self._operation_count > 0
|
|
505
|
-
else 0
|
|
506
|
-
),
|
|
507
|
-
2,
|
|
508
|
-
),
|
|
509
|
-
},
|
|
510
|
-
}
|
|
635
|
+
return Path(output_path)
|
|
511
636
|
|
|
512
|
-
# Wire
|
|
637
|
+
# Wire operations (delegated to WireManager)
|
|
513
638
|
def add_wire(
|
|
514
639
|
self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
|
|
515
640
|
) -> str:
|
|
516
641
|
"""
|
|
517
|
-
Add a wire connection.
|
|
642
|
+
Add a wire connection between two points.
|
|
518
643
|
|
|
519
644
|
Args:
|
|
520
645
|
start: Start point
|
|
@@ -523,775 +648,681 @@ class Schematic:
|
|
|
523
648
|
Returns:
|
|
524
649
|
UUID of created wire
|
|
525
650
|
"""
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if isinstance(end, tuple):
|
|
529
|
-
end = Point(end[0], end[1])
|
|
530
|
-
|
|
531
|
-
# Use the wire collection to add the wire
|
|
532
|
-
wire_uuid = self._wires.add(start=start, end=end)
|
|
651
|
+
wire_uuid = self._wire_manager.add_wire(start, end)
|
|
652
|
+
self._format_sync_manager.mark_dirty("wire", "add", {"uuid": wire_uuid})
|
|
533
653
|
self._modified = True
|
|
534
|
-
|
|
535
|
-
logger.debug(f"Added wire: {start} -> {end}")
|
|
536
654
|
return wire_uuid
|
|
537
655
|
|
|
538
656
|
def remove_wire(self, wire_uuid: str) -> bool:
|
|
539
|
-
"""
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if removed_from_collection or removed_from_data:
|
|
657
|
+
"""
|
|
658
|
+
Remove a wire by UUID.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
wire_uuid: UUID of wire to remove
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
True if wire was removed, False if not found
|
|
665
|
+
"""
|
|
666
|
+
removed = self._wires.remove(wire_uuid)
|
|
667
|
+
if removed:
|
|
668
|
+
self._format_sync_manager.remove_wire_from_data(wire_uuid)
|
|
553
669
|
self._modified = True
|
|
554
|
-
|
|
555
|
-
return True
|
|
556
|
-
return False
|
|
670
|
+
return removed
|
|
557
671
|
|
|
558
|
-
|
|
559
|
-
def add_hierarchical_label(
|
|
672
|
+
def auto_route_pins(
|
|
560
673
|
self,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
) -> str:
|
|
674
|
+
component1_ref: str,
|
|
675
|
+
pin1_number: str,
|
|
676
|
+
component2_ref: str,
|
|
677
|
+
pin2_number: str,
|
|
678
|
+
routing_strategy: str = "direct",
|
|
679
|
+
) -> List[str]:
|
|
567
680
|
"""
|
|
568
|
-
|
|
681
|
+
Auto-route between two component pins.
|
|
569
682
|
|
|
570
683
|
Args:
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
684
|
+
component1_ref: First component reference
|
|
685
|
+
pin1_number: First component pin number
|
|
686
|
+
component2_ref: Second component reference
|
|
687
|
+
pin2_number: Second component pin number
|
|
688
|
+
routing_strategy: Routing strategy ("direct", "orthogonal", "manhattan")
|
|
576
689
|
|
|
577
690
|
Returns:
|
|
578
|
-
|
|
691
|
+
List of wire UUIDs created
|
|
579
692
|
"""
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
label = Label(
|
|
584
|
-
uuid=str(uuid.uuid4()),
|
|
585
|
-
position=position,
|
|
586
|
-
text=text,
|
|
587
|
-
label_type=LabelType.HIERARCHICAL,
|
|
588
|
-
rotation=rotation,
|
|
589
|
-
size=size,
|
|
590
|
-
shape=shape,
|
|
591
|
-
)
|
|
592
|
-
|
|
593
|
-
if "hierarchical_labels" not in self._data:
|
|
594
|
-
self._data["hierarchical_labels"] = []
|
|
595
|
-
|
|
596
|
-
self._data["hierarchical_labels"].append(
|
|
597
|
-
{
|
|
598
|
-
"uuid": label.uuid,
|
|
599
|
-
"position": {"x": label.position.x, "y": label.position.y},
|
|
600
|
-
"text": label.text,
|
|
601
|
-
"shape": label.shape.value,
|
|
602
|
-
"rotation": label.rotation,
|
|
603
|
-
"size": label.size,
|
|
604
|
-
}
|
|
693
|
+
wire_uuids = self._wire_manager.auto_route_pins(
|
|
694
|
+
component1_ref, pin1_number, component2_ref, pin2_number, routing_strategy
|
|
605
695
|
)
|
|
696
|
+
for wire_uuid in wire_uuids:
|
|
697
|
+
self._format_sync_manager.mark_dirty("wire", "add", {"uuid": wire_uuid})
|
|
606
698
|
self._modified = True
|
|
607
|
-
|
|
608
|
-
logger.debug(f"Added hierarchical label: {text} at {position}")
|
|
609
|
-
return label.uuid
|
|
610
|
-
|
|
611
|
-
def remove_hierarchical_label(self, label_uuid: str) -> bool:
|
|
612
|
-
"""Remove hierarchical label by UUID."""
|
|
613
|
-
labels = self._data.get("hierarchical_labels", [])
|
|
614
|
-
for i, label in enumerate(labels):
|
|
615
|
-
if label.get("uuid") == label_uuid:
|
|
616
|
-
del labels[i]
|
|
617
|
-
self._modified = True
|
|
618
|
-
logger.debug(f"Removed hierarchical label: {label_uuid}")
|
|
619
|
-
return True
|
|
620
|
-
return False
|
|
699
|
+
return wire_uuids
|
|
621
700
|
|
|
622
701
|
def add_wire_to_pin(
|
|
623
|
-
self,
|
|
702
|
+
self, start: Union[Point, Tuple[float, float]], component_ref: str, pin_number: str
|
|
624
703
|
) -> Optional[str]:
|
|
625
704
|
"""
|
|
626
|
-
|
|
705
|
+
Add wire from arbitrary position to component pin.
|
|
627
706
|
|
|
628
707
|
Args:
|
|
629
|
-
|
|
630
|
-
component_ref:
|
|
631
|
-
pin_number: Pin number
|
|
708
|
+
start: Start position
|
|
709
|
+
component_ref: Component reference
|
|
710
|
+
pin_number: Pin number
|
|
632
711
|
|
|
633
712
|
Returns:
|
|
634
|
-
UUID
|
|
713
|
+
Wire UUID or None if pin not found
|
|
635
714
|
"""
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
# Find the component
|
|
639
|
-
component = self.components.get(component_ref)
|
|
640
|
-
if not component:
|
|
641
|
-
logger.warning(f"Component {component_ref} not found")
|
|
642
|
-
return None
|
|
643
|
-
|
|
644
|
-
# Get the pin position
|
|
645
|
-
pin_position = get_component_pin_position(component, pin_number)
|
|
646
|
-
if not pin_position:
|
|
647
|
-
logger.warning(f"Could not determine position of pin {pin_number} on {component_ref}")
|
|
715
|
+
pin_pos = self.get_component_pin_position(component_ref, pin_number)
|
|
716
|
+
if pin_pos is None:
|
|
648
717
|
return None
|
|
649
718
|
|
|
650
|
-
|
|
651
|
-
return self.add_wire(start_point, pin_position)
|
|
719
|
+
return self.add_wire(start, pin_pos)
|
|
652
720
|
|
|
653
721
|
def add_wire_between_pins(
|
|
654
722
|
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
655
723
|
) -> Optional[str]:
|
|
656
724
|
"""
|
|
657
|
-
|
|
725
|
+
Add wire between two component pins.
|
|
658
726
|
|
|
659
727
|
Args:
|
|
660
|
-
component1_ref:
|
|
661
|
-
pin1_number:
|
|
662
|
-
component2_ref:
|
|
663
|
-
pin2_number:
|
|
728
|
+
component1_ref: First component reference
|
|
729
|
+
pin1_number: First component pin number
|
|
730
|
+
component2_ref: Second component reference
|
|
731
|
+
pin2_number: Second component pin number
|
|
664
732
|
|
|
665
733
|
Returns:
|
|
666
|
-
UUID
|
|
734
|
+
Wire UUID or None if either pin not found
|
|
667
735
|
"""
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
# Find both components
|
|
671
|
-
component1 = self.components.get(component1_ref)
|
|
672
|
-
component2 = self.components.get(component2_ref)
|
|
673
|
-
|
|
674
|
-
if not component1:
|
|
675
|
-
logger.warning(f"Component {component1_ref} not found")
|
|
676
|
-
return None
|
|
677
|
-
if not component2:
|
|
678
|
-
logger.warning(f"Component {component2_ref} not found")
|
|
679
|
-
return None
|
|
680
|
-
|
|
681
|
-
# Get both pin positions
|
|
682
|
-
pin1_position = get_component_pin_position(component1, pin1_number)
|
|
683
|
-
pin2_position = get_component_pin_position(component2, pin2_number)
|
|
736
|
+
pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
|
|
737
|
+
pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
|
|
684
738
|
|
|
685
|
-
if
|
|
686
|
-
logger.warning(f"Could not determine position of pin {pin1_number} on {component1_ref}")
|
|
687
|
-
return None
|
|
688
|
-
if not pin2_position:
|
|
689
|
-
logger.warning(f"Could not determine position of pin {pin2_number} on {component2_ref}")
|
|
739
|
+
if pin1_pos is None or pin2_pos is None:
|
|
690
740
|
return None
|
|
691
741
|
|
|
692
|
-
|
|
693
|
-
return self.add_wire(pin1_position, pin2_position)
|
|
742
|
+
return self.add_wire(pin1_pos, pin2_pos)
|
|
694
743
|
|
|
695
|
-
def
|
|
744
|
+
def connect_pins_with_wire(
|
|
745
|
+
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
746
|
+
) -> Optional[str]:
|
|
696
747
|
"""
|
|
697
|
-
|
|
748
|
+
Connect two component pins with a wire (alias for add_wire_between_pins).
|
|
698
749
|
|
|
699
750
|
Args:
|
|
700
|
-
|
|
701
|
-
|
|
751
|
+
component1_ref: First component reference
|
|
752
|
+
pin1_number: First component pin number
|
|
753
|
+
component2_ref: Second component reference
|
|
754
|
+
pin2_number: Second component pin number
|
|
702
755
|
|
|
703
756
|
Returns:
|
|
704
|
-
|
|
757
|
+
Wire UUID or None if either pin not found
|
|
705
758
|
"""
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
component = self.components.get(component_ref)
|
|
709
|
-
if not component:
|
|
710
|
-
return None
|
|
711
|
-
|
|
712
|
-
return get_component_pin_position(component, pin_number)
|
|
759
|
+
return self.add_wire_between_pins(component1_ref, pin1_number, component2_ref, pin2_number)
|
|
713
760
|
|
|
714
|
-
#
|
|
715
|
-
def
|
|
761
|
+
# Text and label operations (delegated to TextElementManager)
|
|
762
|
+
def add_label(
|
|
716
763
|
self,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
764
|
+
text: str,
|
|
765
|
+
position: Optional[Union[Point, Tuple[float, float]]] = None,
|
|
766
|
+
pin: Optional[Tuple[str, str]] = None,
|
|
767
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
768
|
+
rotation: Optional[float] = None,
|
|
769
|
+
size: Optional[float] = None,
|
|
770
|
+
uuid: Optional[str] = None,
|
|
771
|
+
) -> str:
|
|
724
772
|
"""
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
All positions are snapped to KiCAD's 1.27mm grid for exact electrical connections.
|
|
773
|
+
Add a text label to the schematic.
|
|
728
774
|
|
|
729
775
|
Args:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
clearance: Clearance from obstacles in mm (for manhattan mode)
|
|
776
|
+
text: Label text content
|
|
777
|
+
position: Label position (required if pin not provided)
|
|
778
|
+
pin: Pin to attach label to as (component_ref, pin_number) tuple (alternative to position)
|
|
779
|
+
effects: Text effects (size, font, etc.)
|
|
780
|
+
rotation: Label rotation in degrees (default 0, or auto-calculated if pin provided)
|
|
781
|
+
size: Text size override (default from effects)
|
|
782
|
+
uuid: Specific UUID for label (auto-generated if None)
|
|
738
783
|
|
|
739
784
|
Returns:
|
|
740
|
-
UUID of created
|
|
741
|
-
"""
|
|
742
|
-
from .wire_routing import route_pins_direct, snap_to_kicad_grid
|
|
743
|
-
|
|
744
|
-
# Get pin positions
|
|
745
|
-
pin1_pos = self.get_component_pin_position(comp1_ref, pin1_num)
|
|
746
|
-
pin2_pos = self.get_component_pin_position(comp2_ref, pin2_num)
|
|
747
|
-
|
|
748
|
-
if not pin1_pos or not pin2_pos:
|
|
749
|
-
return None
|
|
750
|
-
|
|
751
|
-
# Ensure positions are grid-snapped
|
|
752
|
-
pin1_pos = snap_to_kicad_grid(pin1_pos)
|
|
753
|
-
pin2_pos = snap_to_kicad_grid(pin2_pos)
|
|
754
|
-
|
|
755
|
-
# Choose routing strategy
|
|
756
|
-
if routing_mode.lower() == "manhattan":
|
|
757
|
-
# Manhattan routing with obstacle avoidance
|
|
758
|
-
from .simple_manhattan import auto_route_with_manhattan
|
|
759
|
-
|
|
760
|
-
# Get component objects
|
|
761
|
-
comp1 = self.components.get(comp1_ref)
|
|
762
|
-
comp2 = self.components.get(comp2_ref)
|
|
763
|
-
|
|
764
|
-
if not comp1 or not comp2:
|
|
765
|
-
logger.warning(f"Component not found: {comp1_ref} or {comp2_ref}")
|
|
766
|
-
return None
|
|
767
|
-
|
|
768
|
-
return auto_route_with_manhattan(
|
|
769
|
-
self,
|
|
770
|
-
comp1,
|
|
771
|
-
pin1_num,
|
|
772
|
-
comp2,
|
|
773
|
-
pin2_num,
|
|
774
|
-
avoid_components=None, # Avoid all other components
|
|
775
|
-
clearance=clearance,
|
|
776
|
-
)
|
|
777
|
-
else:
|
|
778
|
-
# Default direct routing - just connect the pins
|
|
779
|
-
return self.add_wire(pin1_pos, pin2_pos)
|
|
780
|
-
|
|
781
|
-
def are_pins_connected(
|
|
782
|
-
self, comp1_ref: str, pin1_num: str, comp2_ref: str, pin2_num: str
|
|
783
|
-
) -> bool:
|
|
784
|
-
"""
|
|
785
|
-
Detect when two pins are connected via wire routing.
|
|
786
|
-
|
|
787
|
-
Args:
|
|
788
|
-
comp1_ref: First component reference (e.g., 'R1')
|
|
789
|
-
pin1_num: First component pin number (e.g., '1')
|
|
790
|
-
comp2_ref: Second component reference (e.g., 'R2')
|
|
791
|
-
pin2_num: Second component pin number (e.g., '2')
|
|
785
|
+
UUID of created label
|
|
792
786
|
|
|
793
|
-
|
|
794
|
-
|
|
787
|
+
Raises:
|
|
788
|
+
ValueError: If neither position nor pin is provided, or if pin is not found
|
|
795
789
|
"""
|
|
796
|
-
from .
|
|
797
|
-
|
|
798
|
-
|
|
790
|
+
from .pin_utils import get_component_pin_info
|
|
791
|
+
|
|
792
|
+
# Validate arguments
|
|
793
|
+
if position is None and pin is None:
|
|
794
|
+
raise ValueError("Either position or pin must be provided")
|
|
795
|
+
if position is not None and pin is not None:
|
|
796
|
+
raise ValueError("Cannot provide both position and pin")
|
|
797
|
+
|
|
798
|
+
# Handle pin-based placement
|
|
799
|
+
justify_h = "left"
|
|
800
|
+
justify_v = "bottom"
|
|
801
|
+
|
|
802
|
+
if pin is not None:
|
|
803
|
+
component_ref, pin_number = pin
|
|
804
|
+
|
|
805
|
+
# Get component
|
|
806
|
+
component = self._components.get(component_ref)
|
|
807
|
+
if component is None:
|
|
808
|
+
raise ValueError(f"Component {component_ref} not found")
|
|
809
|
+
|
|
810
|
+
# Get pin position and rotation
|
|
811
|
+
pin_info = get_component_pin_info(component, pin_number)
|
|
812
|
+
if pin_info is None:
|
|
813
|
+
raise ValueError(f"Pin {pin_number} not found on component {component_ref}")
|
|
814
|
+
|
|
815
|
+
pin_position, pin_rotation = pin_info
|
|
816
|
+
position = pin_position
|
|
817
|
+
|
|
818
|
+
# Calculate label rotation if not explicitly provided
|
|
819
|
+
if rotation is None:
|
|
820
|
+
# Label should face away from component:
|
|
821
|
+
# Pin rotation indicates where pin points INTO the component
|
|
822
|
+
# Label should face OPPOSITE direction
|
|
823
|
+
rotation = (pin_rotation + 180) % 360
|
|
824
|
+
logger.info(
|
|
825
|
+
f"Auto-calculated label rotation: {rotation}° (pin rotation: {pin_rotation}°)"
|
|
826
|
+
)
|
|
799
827
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
828
|
+
# Calculate justification based on pin angle
|
|
829
|
+
# This determines which corner of the text is anchored to the pin position
|
|
830
|
+
if pin_rotation == 0: # Pin points right into component
|
|
831
|
+
justify_h = "left"
|
|
832
|
+
justify_v = "bottom"
|
|
833
|
+
elif pin_rotation == 90: # Pin points up into component
|
|
834
|
+
justify_h = "right"
|
|
835
|
+
justify_v = "bottom"
|
|
836
|
+
elif pin_rotation == 180: # Pin points left into component
|
|
837
|
+
justify_h = "right"
|
|
838
|
+
justify_v = "bottom"
|
|
839
|
+
elif pin_rotation == 270: # Pin points down into component
|
|
840
|
+
justify_h = "left"
|
|
841
|
+
justify_v = "bottom"
|
|
842
|
+
logger.info(f"Auto-calculated justification: {justify_h} {justify_v} (pin angle: {pin_rotation}°)")
|
|
843
|
+
|
|
844
|
+
# Use default rotation if still not set
|
|
845
|
+
if rotation is None:
|
|
846
|
+
rotation = 0
|
|
847
|
+
|
|
848
|
+
# Use the new labels collection instead of manager
|
|
849
|
+
if size is None:
|
|
850
|
+
size = 1.27 # Default size
|
|
851
|
+
label = self._labels.add(
|
|
852
|
+
text, position, rotation=rotation, size=size,
|
|
853
|
+
justify_h=justify_h, justify_v=justify_v, uuid=uuid
|
|
854
|
+
)
|
|
855
|
+
self._sync_labels_to_data() # Sync immediately
|
|
856
|
+
self._format_sync_manager.mark_dirty("label", "add", {"uuid": label.uuid})
|
|
857
|
+
self._modified = True
|
|
858
|
+
return label.uuid
|
|
806
859
|
|
|
807
|
-
def
|
|
860
|
+
def add_text(
|
|
808
861
|
self,
|
|
809
862
|
text: str,
|
|
810
863
|
position: Union[Point, Tuple[float, float]],
|
|
811
864
|
rotation: float = 0.0,
|
|
812
865
|
size: float = 1.27,
|
|
813
|
-
|
|
866
|
+
exclude_from_sim: bool = False,
|
|
867
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
814
868
|
) -> str:
|
|
815
869
|
"""
|
|
816
|
-
Add
|
|
870
|
+
Add free text annotation to the schematic.
|
|
817
871
|
|
|
818
872
|
Args:
|
|
819
|
-
text:
|
|
820
|
-
position:
|
|
873
|
+
text: Text content
|
|
874
|
+
position: Text position
|
|
821
875
|
rotation: Text rotation in degrees
|
|
822
|
-
size:
|
|
823
|
-
|
|
876
|
+
size: Text size
|
|
877
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
878
|
+
effects: Text effects
|
|
824
879
|
|
|
825
880
|
Returns:
|
|
826
|
-
UUID of created
|
|
881
|
+
UUID of created text
|
|
827
882
|
"""
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
import uuid as uuid_module
|
|
832
|
-
|
|
833
|
-
label = Label(
|
|
834
|
-
uuid=uuid if uuid else str(uuid_module.uuid4()),
|
|
835
|
-
position=position,
|
|
836
|
-
text=text,
|
|
837
|
-
label_type=LabelType.LOCAL,
|
|
838
|
-
rotation=rotation,
|
|
839
|
-
size=size,
|
|
840
|
-
)
|
|
841
|
-
|
|
842
|
-
if "labels" not in self._data:
|
|
843
|
-
self._data["labels"] = []
|
|
844
|
-
|
|
845
|
-
self._data["labels"].append(
|
|
846
|
-
{
|
|
847
|
-
"uuid": label.uuid,
|
|
848
|
-
"position": {"x": label.position.x, "y": label.position.y},
|
|
849
|
-
"text": label.text,
|
|
850
|
-
"rotation": label.rotation,
|
|
851
|
-
"size": label.size,
|
|
852
|
-
}
|
|
883
|
+
# Use the new texts collection instead of manager
|
|
884
|
+
text_elem = self._texts.add(
|
|
885
|
+
text, position, rotation=rotation, size=size, exclude_from_sim=exclude_from_sim
|
|
853
886
|
)
|
|
887
|
+
self._sync_texts_to_data() # Sync immediately
|
|
888
|
+
self._format_sync_manager.mark_dirty("text", "add", {"uuid": text_elem.uuid})
|
|
854
889
|
self._modified = True
|
|
890
|
+
return text_elem.uuid
|
|
855
891
|
|
|
856
|
-
|
|
857
|
-
return label.uuid
|
|
858
|
-
|
|
859
|
-
def remove_label(self, label_uuid: str) -> bool:
|
|
860
|
-
"""Remove local label by UUID."""
|
|
861
|
-
labels = self._data.get("labels", [])
|
|
862
|
-
for i, label in enumerate(labels):
|
|
863
|
-
if label.get("uuid") == label_uuid:
|
|
864
|
-
del labels[i]
|
|
865
|
-
self._modified = True
|
|
866
|
-
logger.debug(f"Removed local label: {label_uuid}")
|
|
867
|
-
return True
|
|
868
|
-
return False
|
|
869
|
-
|
|
870
|
-
def add_sheet(
|
|
892
|
+
def add_text_box(
|
|
871
893
|
self,
|
|
872
|
-
|
|
873
|
-
filename: str,
|
|
894
|
+
text: str,
|
|
874
895
|
position: Union[Point, Tuple[float, float]],
|
|
875
896
|
size: Union[Point, Tuple[float, float]],
|
|
876
|
-
|
|
897
|
+
rotation: float = 0.0,
|
|
898
|
+
font_size: float = 1.27,
|
|
899
|
+
margins: Optional[Tuple[float, float, float, float]] = None,
|
|
900
|
+
stroke_width: Optional[float] = None,
|
|
877
901
|
stroke_type: str = "solid",
|
|
902
|
+
fill_type: str = "none",
|
|
903
|
+
justify_horizontal: str = "left",
|
|
904
|
+
justify_vertical: str = "top",
|
|
878
905
|
exclude_from_sim: bool = False,
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
project_name: str = "",
|
|
882
|
-
page_number: str = "2",
|
|
883
|
-
uuid: Optional[str] = None,
|
|
906
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
907
|
+
stroke: Optional[Dict[str, Any]] = None,
|
|
884
908
|
) -> str:
|
|
885
909
|
"""
|
|
886
|
-
Add a
|
|
910
|
+
Add a text box with border to the schematic.
|
|
887
911
|
|
|
888
912
|
Args:
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
913
|
+
text: Text content
|
|
914
|
+
position: Top-left position
|
|
915
|
+
size: Box size (width, height)
|
|
916
|
+
rotation: Text rotation in degrees
|
|
917
|
+
font_size: Text font size
|
|
918
|
+
margins: Box margins (top, bottom, left, right)
|
|
919
|
+
stroke_width: Border stroke width
|
|
920
|
+
stroke_type: Border stroke type (solid, dash, etc.)
|
|
921
|
+
fill_type: Fill type (none, outline, background)
|
|
922
|
+
justify_horizontal: Horizontal justification
|
|
923
|
+
justify_vertical: Vertical justification
|
|
924
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
925
|
+
effects: Text effects (legacy)
|
|
926
|
+
stroke: Border stroke settings (legacy)
|
|
901
927
|
|
|
902
928
|
Returns:
|
|
903
|
-
UUID of created
|
|
929
|
+
UUID of created text box
|
|
904
930
|
"""
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if isinstance(size, tuple):
|
|
908
|
-
size = Point(size[0], size[1])
|
|
909
|
-
|
|
910
|
-
import uuid as uuid_module
|
|
911
|
-
|
|
912
|
-
sheet = Sheet(
|
|
913
|
-
uuid=uuid if uuid else str(uuid_module.uuid4()),
|
|
931
|
+
text_box_uuid = self._text_element_manager.add_text_box(
|
|
932
|
+
text=text,
|
|
914
933
|
position=position,
|
|
915
934
|
size=size,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
in_bom=in_bom,
|
|
920
|
-
on_board=on_board,
|
|
935
|
+
rotation=rotation,
|
|
936
|
+
font_size=font_size,
|
|
937
|
+
margins=margins,
|
|
921
938
|
stroke_width=stroke_width,
|
|
922
939
|
stroke_type=stroke_type,
|
|
940
|
+
fill_type=fill_type,
|
|
941
|
+
justify_horizontal=justify_horizontal,
|
|
942
|
+
justify_vertical=justify_vertical,
|
|
943
|
+
exclude_from_sim=exclude_from_sim,
|
|
944
|
+
effects=effects,
|
|
945
|
+
stroke=stroke,
|
|
923
946
|
)
|
|
924
|
-
|
|
925
|
-
if "sheets" not in self._data:
|
|
926
|
-
self._data["sheets"] = []
|
|
927
|
-
|
|
928
|
-
self._data["sheets"].append(
|
|
929
|
-
{
|
|
930
|
-
"uuid": sheet.uuid,
|
|
931
|
-
"position": {"x": sheet.position.x, "y": sheet.position.y},
|
|
932
|
-
"size": {"width": sheet.size.x, "height": sheet.size.y},
|
|
933
|
-
"name": sheet.name,
|
|
934
|
-
"filename": sheet.filename,
|
|
935
|
-
"exclude_from_sim": sheet.exclude_from_sim,
|
|
936
|
-
"in_bom": sheet.in_bom,
|
|
937
|
-
"on_board": sheet.on_board,
|
|
938
|
-
"dnp": sheet.dnp,
|
|
939
|
-
"fields_autoplaced": sheet.fields_autoplaced,
|
|
940
|
-
"stroke_width": sheet.stroke_width,
|
|
941
|
-
"stroke_type": sheet.stroke_type,
|
|
942
|
-
"fill_color": sheet.fill_color,
|
|
943
|
-
"pins": [], # Sheet pins added separately
|
|
944
|
-
"project_name": project_name,
|
|
945
|
-
"page_number": page_number,
|
|
946
|
-
}
|
|
947
|
-
)
|
|
947
|
+
self._format_sync_manager.mark_dirty("text_box", "add", {"uuid": text_box_uuid})
|
|
948
948
|
self._modified = True
|
|
949
|
+
return text_box_uuid
|
|
949
950
|
|
|
950
|
-
|
|
951
|
-
return sheet.uuid
|
|
952
|
-
|
|
953
|
-
def add_sheet_pin(
|
|
951
|
+
def add_hierarchical_label(
|
|
954
952
|
self,
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
rotation: float = 0,
|
|
953
|
+
text: str,
|
|
954
|
+
position: Union[Point, Tuple[float, float]],
|
|
955
|
+
shape: str = "input",
|
|
956
|
+
rotation: float = 0.0,
|
|
960
957
|
size: float = 1.27,
|
|
961
|
-
|
|
962
|
-
uuid: Optional[str] = None,
|
|
958
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
963
959
|
) -> str:
|
|
964
960
|
"""
|
|
965
|
-
Add a
|
|
961
|
+
Add a hierarchical label for sheet connections.
|
|
966
962
|
|
|
967
963
|
Args:
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
justify: Text justification (left, right, center)
|
|
975
|
-
uuid: Optional UUID (auto-generated if None)
|
|
964
|
+
text: Label text
|
|
965
|
+
position: Label position
|
|
966
|
+
shape: Shape type (input, output, bidirectional, tri_state, passive)
|
|
967
|
+
rotation: Label rotation in degrees (default 0)
|
|
968
|
+
size: Label text size (default 1.27)
|
|
969
|
+
effects: Text effects
|
|
976
970
|
|
|
977
971
|
Returns:
|
|
978
|
-
UUID of created
|
|
972
|
+
UUID of created hierarchical label
|
|
979
973
|
"""
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
# Find the sheet in the data
|
|
988
|
-
sheets = self._data.get("sheets", [])
|
|
989
|
-
for sheet in sheets:
|
|
990
|
-
if sheet.get("uuid") == sheet_uuid:
|
|
991
|
-
# Add pin to the sheet's pins list
|
|
992
|
-
pin_data = {
|
|
993
|
-
"uuid": pin_uuid,
|
|
994
|
-
"name": name,
|
|
995
|
-
"pin_type": pin_type,
|
|
996
|
-
"position": {"x": position.x, "y": position.y},
|
|
997
|
-
"rotation": rotation,
|
|
998
|
-
"size": size,
|
|
999
|
-
"justify": justify,
|
|
1000
|
-
}
|
|
1001
|
-
sheet["pins"].append(pin_data)
|
|
1002
|
-
self._modified = True
|
|
1003
|
-
|
|
1004
|
-
logger.debug(f"Added sheet pin: {name} ({pin_type}) to sheet {sheet_uuid}")
|
|
1005
|
-
return pin_uuid
|
|
1006
|
-
|
|
1007
|
-
raise ValueError(f"Sheet with UUID '{sheet_uuid}' not found")
|
|
974
|
+
# Use the hierarchical_labels collection
|
|
975
|
+
hlabel = self._hierarchical_labels.add(text, position, rotation=rotation, size=size)
|
|
976
|
+
self._sync_hierarchical_labels_to_data() # Sync immediately
|
|
977
|
+
self._format_sync_manager.mark_dirty("hierarchical_label", "add", {"uuid": hlabel.uuid})
|
|
978
|
+
self._modified = True
|
|
979
|
+
return hlabel.uuid
|
|
1008
980
|
|
|
1009
|
-
def
|
|
981
|
+
def add_global_label(
|
|
1010
982
|
self,
|
|
1011
983
|
text: str,
|
|
1012
984
|
position: Union[Point, Tuple[float, float]],
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
exclude_from_sim: bool = False,
|
|
985
|
+
shape: str = "input",
|
|
986
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
1016
987
|
) -> str:
|
|
1017
988
|
"""
|
|
1018
|
-
Add a
|
|
989
|
+
Add a global label for project-wide connections.
|
|
1019
990
|
|
|
1020
991
|
Args:
|
|
1021
|
-
text:
|
|
1022
|
-
position:
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
exclude_from_sim: Exclude from simulation
|
|
992
|
+
text: Label text
|
|
993
|
+
position: Label position
|
|
994
|
+
shape: Shape type
|
|
995
|
+
effects: Text effects
|
|
1026
996
|
|
|
1027
997
|
Returns:
|
|
1028
|
-
UUID of created
|
|
998
|
+
UUID of created global label
|
|
1029
999
|
"""
|
|
1030
|
-
|
|
1031
|
-
|
|
1000
|
+
label_uuid = self._text_element_manager.add_global_label(text, position, shape, effects)
|
|
1001
|
+
self._format_sync_manager.mark_dirty("global_label", "add", {"uuid": label_uuid})
|
|
1002
|
+
self._modified = True
|
|
1003
|
+
return label_uuid
|
|
1032
1004
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
text=text,
|
|
1037
|
-
rotation=rotation,
|
|
1038
|
-
size=size,
|
|
1039
|
-
exclude_from_sim=exclude_from_sim,
|
|
1040
|
-
)
|
|
1005
|
+
def remove_label(self, label_uuid: str) -> bool:
|
|
1006
|
+
"""
|
|
1007
|
+
Remove a label by UUID.
|
|
1041
1008
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1009
|
+
Args:
|
|
1010
|
+
label_uuid: UUID of label to remove
|
|
1044
1011
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1012
|
+
Returns:
|
|
1013
|
+
True if label was removed, False if not found
|
|
1014
|
+
"""
|
|
1015
|
+
removed = self._labels.remove(label_uuid)
|
|
1016
|
+
if removed:
|
|
1017
|
+
self._sync_labels_to_data() # Sync immediately
|
|
1018
|
+
self._format_sync_manager.mark_dirty("label", "remove", {"uuid": label_uuid})
|
|
1019
|
+
self._modified = True
|
|
1020
|
+
return removed
|
|
1021
|
+
|
|
1022
|
+
def remove_hierarchical_label(self, label_uuid: str) -> bool:
|
|
1023
|
+
"""
|
|
1024
|
+
Remove a hierarchical label by UUID.
|
|
1056
1025
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1026
|
+
Args:
|
|
1027
|
+
label_uuid: UUID of hierarchical label to remove
|
|
1059
1028
|
|
|
1060
|
-
|
|
1029
|
+
Returns:
|
|
1030
|
+
True if hierarchical label was removed, False if not found
|
|
1031
|
+
"""
|
|
1032
|
+
removed = self._hierarchical_labels.remove(label_uuid)
|
|
1033
|
+
if removed:
|
|
1034
|
+
self._sync_hierarchical_labels_to_data() # Sync immediately
|
|
1035
|
+
self._format_sync_manager.mark_dirty(
|
|
1036
|
+
"hierarchical_label", "remove", {"uuid": label_uuid}
|
|
1037
|
+
)
|
|
1038
|
+
self._modified = True
|
|
1039
|
+
return removed
|
|
1040
|
+
|
|
1041
|
+
# Sheet operations (delegated to SheetManager)
|
|
1042
|
+
def add_sheet(
|
|
1061
1043
|
self,
|
|
1062
|
-
|
|
1044
|
+
name: str,
|
|
1045
|
+
filename: str,
|
|
1063
1046
|
position: Union[Point, Tuple[float, float]],
|
|
1064
1047
|
size: Union[Point, Tuple[float, float]],
|
|
1065
|
-
|
|
1066
|
-
font_size: float = 1.27,
|
|
1067
|
-
margins: Tuple[float, float, float, float] = (0.9525, 0.9525, 0.9525, 0.9525),
|
|
1068
|
-
stroke_width: float = 0.0,
|
|
1048
|
+
stroke_width: Optional[float] = None,
|
|
1069
1049
|
stroke_type: str = "solid",
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
exclude_from_sim: bool = False,
|
|
1050
|
+
project_name: Optional[str] = None,
|
|
1051
|
+
page_number: Optional[str] = None,
|
|
1052
|
+
uuid: Optional[str] = None,
|
|
1074
1053
|
) -> str:
|
|
1075
1054
|
"""
|
|
1076
|
-
Add a
|
|
1055
|
+
Add a hierarchical sheet to the schematic.
|
|
1077
1056
|
|
|
1078
1057
|
Args:
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
justify_horizontal: Horizontal text alignment
|
|
1089
|
-
justify_vertical: Vertical text alignment
|
|
1090
|
-
exclude_from_sim: Exclude from simulation
|
|
1058
|
+
name: Sheet name/title
|
|
1059
|
+
filename: Referenced schematic filename
|
|
1060
|
+
position: Sheet position (top-left corner)
|
|
1061
|
+
size: Sheet size (width, height)
|
|
1062
|
+
stroke_width: Border stroke width
|
|
1063
|
+
stroke_type: Border stroke type (solid, dashed, etc.)
|
|
1064
|
+
project_name: Project name for this sheet
|
|
1065
|
+
page_number: Page number for this sheet
|
|
1066
|
+
uuid: Optional UUID for the sheet
|
|
1091
1067
|
|
|
1092
1068
|
Returns:
|
|
1093
|
-
UUID of created
|
|
1069
|
+
UUID of created sheet
|
|
1094
1070
|
"""
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
uuid=str(uuid.uuid4()),
|
|
1102
|
-
position=position,
|
|
1103
|
-
size=size,
|
|
1104
|
-
text=text,
|
|
1105
|
-
rotation=rotation,
|
|
1106
|
-
font_size=font_size,
|
|
1107
|
-
margins=margins,
|
|
1071
|
+
sheet_uuid = self._sheet_manager.add_sheet(
|
|
1072
|
+
name,
|
|
1073
|
+
filename,
|
|
1074
|
+
position,
|
|
1075
|
+
size,
|
|
1076
|
+
uuid_str=uuid,
|
|
1108
1077
|
stroke_width=stroke_width,
|
|
1109
1078
|
stroke_type=stroke_type,
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
justify_vertical=justify_vertical,
|
|
1113
|
-
exclude_from_sim=exclude_from_sim,
|
|
1079
|
+
project_name=project_name,
|
|
1080
|
+
page_number=page_number,
|
|
1114
1081
|
)
|
|
1082
|
+
self._format_sync_manager.mark_dirty("sheet", "add", {"uuid": sheet_uuid})
|
|
1083
|
+
self._modified = True
|
|
1084
|
+
return sheet_uuid
|
|
1115
1085
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1086
|
+
def add_sheet_pin(
|
|
1087
|
+
self,
|
|
1088
|
+
sheet_uuid: str,
|
|
1089
|
+
name: str,
|
|
1090
|
+
pin_type: str,
|
|
1091
|
+
edge: str,
|
|
1092
|
+
position_along_edge: float,
|
|
1093
|
+
uuid: Optional[str] = None,
|
|
1094
|
+
) -> str:
|
|
1095
|
+
"""
|
|
1096
|
+
Add a pin to a hierarchical sheet using edge-based positioning.
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
sheet_uuid: UUID of the sheet to add pin to
|
|
1100
|
+
name: Pin name
|
|
1101
|
+
pin_type: Pin type (input, output, bidirectional, tri_state, passive)
|
|
1102
|
+
edge: Edge to place pin on ("right", "bottom", "left", "top")
|
|
1103
|
+
position_along_edge: Distance along edge from reference corner (mm)
|
|
1104
|
+
uuid: Optional UUID for the pin
|
|
1105
|
+
|
|
1106
|
+
Returns:
|
|
1107
|
+
UUID of created sheet pin
|
|
1108
|
+
|
|
1109
|
+
Edge positioning (clockwise from right):
|
|
1110
|
+
- "right": Pins face right (0°), position measured from top edge
|
|
1111
|
+
- "bottom": Pins face down (270°), position measured from left edge
|
|
1112
|
+
- "left": Pins face left (180°), position measured from bottom edge
|
|
1113
|
+
- "top": Pins face up (90°), position measured from left edge
|
|
1114
|
+
|
|
1115
|
+
Example:
|
|
1116
|
+
>>> # Sheet at (100, 100) with size (50, 40)
|
|
1117
|
+
>>> sch.add_sheet_pin(
|
|
1118
|
+
... sheet_uuid=sheet_id,
|
|
1119
|
+
... name="DATA_IN",
|
|
1120
|
+
... pin_type="input",
|
|
1121
|
+
... edge="left",
|
|
1122
|
+
... position_along_edge=20 # 20mm from top on left edge
|
|
1123
|
+
... )
|
|
1124
|
+
"""
|
|
1125
|
+
pin_uuid = self._sheet_manager.add_sheet_pin(
|
|
1126
|
+
sheet_uuid, name, pin_type, edge, position_along_edge, uuid_str=uuid
|
|
1135
1127
|
)
|
|
1128
|
+
self._format_sync_manager.mark_dirty("sheet", "modify", {"uuid": sheet_uuid})
|
|
1136
1129
|
self._modified = True
|
|
1130
|
+
return pin_uuid
|
|
1131
|
+
|
|
1132
|
+
def remove_sheet(self, sheet_uuid: str) -> bool:
|
|
1133
|
+
"""
|
|
1134
|
+
Remove a sheet by UUID.
|
|
1135
|
+
|
|
1136
|
+
Args:
|
|
1137
|
+
sheet_uuid: UUID of sheet to remove
|
|
1137
1138
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1139
|
+
Returns:
|
|
1140
|
+
True if sheet was removed, False if not found
|
|
1141
|
+
"""
|
|
1142
|
+
removed = self._sheet_manager.remove_sheet(sheet_uuid)
|
|
1143
|
+
if removed:
|
|
1144
|
+
self._format_sync_manager.mark_dirty("sheet", "remove", {"uuid": sheet_uuid})
|
|
1145
|
+
self._modified = True
|
|
1146
|
+
return removed
|
|
1140
1147
|
|
|
1148
|
+
# Graphics operations (delegated to GraphicsManager)
|
|
1141
1149
|
def add_rectangle(
|
|
1142
1150
|
self,
|
|
1143
1151
|
start: Union[Point, Tuple[float, float]],
|
|
1144
1152
|
end: Union[Point, Tuple[float, float]],
|
|
1145
|
-
stroke_width: float = 0.
|
|
1146
|
-
stroke_type: str = "
|
|
1147
|
-
fill_type: str = "none"
|
|
1153
|
+
stroke_width: float = 0.127,
|
|
1154
|
+
stroke_type: str = "solid",
|
|
1155
|
+
fill_type: str = "none",
|
|
1156
|
+
stroke_color: Optional[Tuple[int, int, int, float]] = None,
|
|
1157
|
+
fill_color: Optional[Tuple[int, int, int, float]] = None,
|
|
1148
1158
|
) -> str:
|
|
1149
1159
|
"""
|
|
1150
|
-
Add a
|
|
1160
|
+
Add a rectangle to the schematic.
|
|
1151
1161
|
|
|
1152
1162
|
Args:
|
|
1153
|
-
start:
|
|
1154
|
-
end:
|
|
1155
|
-
stroke_width:
|
|
1156
|
-
stroke_type:
|
|
1157
|
-
fill_type: Fill type (none,
|
|
1163
|
+
start: Top-left corner position
|
|
1164
|
+
end: Bottom-right corner position
|
|
1165
|
+
stroke_width: Line width
|
|
1166
|
+
stroke_type: Line type (solid, dash, dash_dot, dash_dot_dot, dot, or default)
|
|
1167
|
+
fill_type: Fill type (none, background, etc.)
|
|
1168
|
+
stroke_color: Stroke color as (r, g, b, a)
|
|
1169
|
+
fill_color: Fill color as (r, g, b, a)
|
|
1158
1170
|
|
|
1159
1171
|
Returns:
|
|
1160
|
-
UUID of created rectangle
|
|
1172
|
+
UUID of created rectangle
|
|
1161
1173
|
"""
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if
|
|
1165
|
-
|
|
1174
|
+
# Validate stroke_type
|
|
1175
|
+
valid_stroke_types = ["solid", "dash", "dash_dot", "dash_dot_dot", "dot", "default"]
|
|
1176
|
+
if stroke_type not in valid_stroke_types:
|
|
1177
|
+
raise ValueError(
|
|
1178
|
+
f"Invalid stroke_type '{stroke_type}'. "
|
|
1179
|
+
f"Must be one of: {', '.join(valid_stroke_types)}"
|
|
1180
|
+
)
|
|
1166
1181
|
|
|
1167
|
-
|
|
1182
|
+
# Convert individual parameters to stroke/fill dicts
|
|
1183
|
+
stroke = {"width": stroke_width, "type": stroke_type}
|
|
1184
|
+
if stroke_color:
|
|
1185
|
+
stroke["color"] = stroke_color
|
|
1168
1186
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
end=end,
|
|
1173
|
-
stroke_width=stroke_width,
|
|
1174
|
-
stroke_type=stroke_type,
|
|
1175
|
-
fill_type=fill_type
|
|
1176
|
-
)
|
|
1187
|
+
fill = {"type": fill_type}
|
|
1188
|
+
if fill_color:
|
|
1189
|
+
fill["color"] = fill_color
|
|
1177
1190
|
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
self._data["rectangles"].append({
|
|
1182
|
-
"uuid": rectangle.uuid,
|
|
1183
|
-
"start": {"x": rectangle.start.x, "y": rectangle.start.y},
|
|
1184
|
-
"end": {"x": rectangle.end.x, "y": rectangle.end.y},
|
|
1185
|
-
"stroke_width": rectangle.stroke_width,
|
|
1186
|
-
"stroke_type": rectangle.stroke_type,
|
|
1187
|
-
"fill_type": rectangle.fill_type
|
|
1188
|
-
})
|
|
1191
|
+
rect_uuid = self._graphics_manager.add_rectangle(start, end, stroke, fill)
|
|
1192
|
+
self._format_sync_manager.mark_dirty("rectangle", "add", {"uuid": rect_uuid})
|
|
1189
1193
|
self._modified = True
|
|
1194
|
+
return rect_uuid
|
|
1195
|
+
|
|
1196
|
+
def remove_rectangle(self, rect_uuid: str) -> bool:
|
|
1197
|
+
"""
|
|
1198
|
+
Remove a rectangle by UUID.
|
|
1190
1199
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1200
|
+
Args:
|
|
1201
|
+
rect_uuid: UUID of rectangle to remove
|
|
1193
1202
|
|
|
1194
|
-
|
|
1203
|
+
Returns:
|
|
1204
|
+
True if removed, False if not found
|
|
1205
|
+
"""
|
|
1206
|
+
removed = self._graphics_manager.remove_rectangle(rect_uuid)
|
|
1207
|
+
if removed:
|
|
1208
|
+
self._format_sync_manager.mark_dirty("rectangle", "remove", {"uuid": rect_uuid})
|
|
1209
|
+
self._modified = True
|
|
1210
|
+
return removed
|
|
1211
|
+
|
|
1212
|
+
def add_image(
|
|
1195
1213
|
self,
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
comments: Optional[Dict[int, str]] = None,
|
|
1201
|
-
):
|
|
1214
|
+
position: Union[Point, Tuple[float, float]],
|
|
1215
|
+
scale: float = 1.0,
|
|
1216
|
+
data: Optional[str] = None,
|
|
1217
|
+
) -> str:
|
|
1202
1218
|
"""
|
|
1203
|
-
|
|
1219
|
+
Add an image to the schematic.
|
|
1204
1220
|
|
|
1205
1221
|
Args:
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
company: Company name
|
|
1210
|
-
comments: Numbered comments (1, 2, 3, etc.)
|
|
1211
|
-
"""
|
|
1212
|
-
if comments is None:
|
|
1213
|
-
comments = {}
|
|
1222
|
+
position: Image position
|
|
1223
|
+
scale: Image scale factor
|
|
1224
|
+
data: Base64 encoded image data
|
|
1214
1225
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
"comments": comments,
|
|
1221
|
-
}
|
|
1226
|
+
Returns:
|
|
1227
|
+
UUID of created image
|
|
1228
|
+
"""
|
|
1229
|
+
image_uuid = self._graphics_manager.add_image(position, scale, data)
|
|
1230
|
+
self._format_sync_manager.mark_dirty("image", "add", {"uuid": image_uuid})
|
|
1222
1231
|
self._modified = True
|
|
1232
|
+
return image_uuid
|
|
1233
|
+
|
|
1234
|
+
def draw_bounding_box(
|
|
1235
|
+
self,
|
|
1236
|
+
bbox,
|
|
1237
|
+
stroke_width: float = 0.127,
|
|
1238
|
+
stroke_color: str = "black",
|
|
1239
|
+
stroke_type: str = "solid",
|
|
1240
|
+
) -> str:
|
|
1241
|
+
"""
|
|
1242
|
+
Draw a bounding box rectangle around the given bounding box.
|
|
1243
|
+
|
|
1244
|
+
Args:
|
|
1245
|
+
bbox: BoundingBox object with min_x, min_y, max_x, max_y
|
|
1246
|
+
stroke_width: Line width
|
|
1247
|
+
stroke_color: Line color
|
|
1248
|
+
stroke_type: Line type
|
|
1249
|
+
|
|
1250
|
+
Returns:
|
|
1251
|
+
UUID of created rectangle
|
|
1252
|
+
"""
|
|
1253
|
+
# Convert bounding box to rectangle coordinates
|
|
1254
|
+
start = (bbox.min_x, bbox.min_y)
|
|
1255
|
+
end = (bbox.max_x, bbox.max_y)
|
|
1223
1256
|
|
|
1224
|
-
|
|
1257
|
+
return self.add_rectangle(start, end, stroke_width=stroke_width, stroke_type=stroke_type)
|
|
1225
1258
|
|
|
1226
1259
|
def draw_bounding_box(
|
|
1227
1260
|
self,
|
|
1228
1261
|
bbox: "BoundingBox",
|
|
1229
|
-
stroke_width: float = 0,
|
|
1230
|
-
stroke_color: str = None,
|
|
1231
|
-
stroke_type: str = "
|
|
1232
|
-
exclude_from_sim: bool = False,
|
|
1262
|
+
stroke_width: float = 0.127,
|
|
1263
|
+
stroke_color: Optional[str] = None,
|
|
1264
|
+
stroke_type: str = "solid",
|
|
1233
1265
|
) -> str:
|
|
1234
1266
|
"""
|
|
1235
|
-
Draw a
|
|
1267
|
+
Draw a single bounding box as a rectangle.
|
|
1236
1268
|
|
|
1237
1269
|
Args:
|
|
1238
1270
|
bbox: BoundingBox to draw
|
|
1239
|
-
stroke_width: Line width
|
|
1240
|
-
stroke_color:
|
|
1241
|
-
stroke_type:
|
|
1242
|
-
exclude_from_sim: Exclude from simulation
|
|
1271
|
+
stroke_width: Line width
|
|
1272
|
+
stroke_color: Line color name (red, green, blue, etc.) or None
|
|
1273
|
+
stroke_type: Line type (solid, dashed, etc.)
|
|
1243
1274
|
|
|
1244
1275
|
Returns:
|
|
1245
|
-
UUID of created rectangle
|
|
1276
|
+
UUID of created rectangle
|
|
1246
1277
|
"""
|
|
1247
|
-
# Import BoundingBox type
|
|
1248
1278
|
from .component_bounds import BoundingBox
|
|
1249
1279
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
# Create rectangle data structure in KiCAD dictionary format
|
|
1253
|
-
stroke_data = {"width": stroke_width, "type": stroke_type}
|
|
1254
|
-
|
|
1255
|
-
# Add color if specified
|
|
1280
|
+
# Convert color name to RGBA tuple if provided
|
|
1281
|
+
stroke_rgba = None
|
|
1256
1282
|
if stroke_color:
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
self._data["graphics"] = []
|
|
1283
|
+
# Simple color name to RGB mapping
|
|
1284
|
+
color_map = {
|
|
1285
|
+
"red": (255, 0, 0, 1.0),
|
|
1286
|
+
"green": (0, 255, 0, 1.0),
|
|
1287
|
+
"blue": (0, 0, 255, 1.0),
|
|
1288
|
+
"yellow": (255, 255, 0, 1.0),
|
|
1289
|
+
"cyan": (0, 255, 255, 1.0),
|
|
1290
|
+
"magenta": (255, 0, 255, 1.0),
|
|
1291
|
+
"black": (0, 0, 0, 1.0),
|
|
1292
|
+
"white": (255, 255, 255, 1.0),
|
|
1293
|
+
}
|
|
1294
|
+
stroke_rgba = color_map.get(stroke_color.lower(), (0, 255, 0, 1.0))
|
|
1270
1295
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1296
|
+
# Add rectangle using the manager
|
|
1297
|
+
rect_uuid = self.add_rectangle(
|
|
1298
|
+
start=(bbox.min_x, bbox.min_y),
|
|
1299
|
+
end=(bbox.max_x, bbox.max_y),
|
|
1300
|
+
stroke_width=stroke_width,
|
|
1301
|
+
stroke_type=stroke_type,
|
|
1302
|
+
stroke_color=stroke_rgba,
|
|
1303
|
+
)
|
|
1273
1304
|
|
|
1274
|
-
logger.debug(f"Drew bounding box
|
|
1305
|
+
logger.debug(f"Drew bounding box: {bbox}")
|
|
1275
1306
|
return rect_uuid
|
|
1276
1307
|
|
|
1277
1308
|
def draw_component_bounding_boxes(
|
|
1278
1309
|
self,
|
|
1279
1310
|
include_properties: bool = False,
|
|
1280
|
-
stroke_width: float = 0.
|
|
1281
|
-
stroke_color: str = "
|
|
1282
|
-
stroke_type: str = "
|
|
1311
|
+
stroke_width: float = 0.127,
|
|
1312
|
+
stroke_color: str = "green",
|
|
1313
|
+
stroke_type: str = "solid",
|
|
1283
1314
|
) -> List[str]:
|
|
1284
1315
|
"""
|
|
1285
|
-
Draw bounding boxes for all components
|
|
1316
|
+
Draw bounding boxes for all components.
|
|
1286
1317
|
|
|
1287
1318
|
Args:
|
|
1288
|
-
include_properties:
|
|
1289
|
-
stroke_width: Line width
|
|
1290
|
-
stroke_color:
|
|
1291
|
-
stroke_type:
|
|
1319
|
+
include_properties: Whether to include properties in bounding box
|
|
1320
|
+
stroke_width: Line width
|
|
1321
|
+
stroke_color: Line color
|
|
1322
|
+
stroke_type: Line type
|
|
1292
1323
|
|
|
1293
1324
|
Returns:
|
|
1294
|
-
List of UUIDs
|
|
1325
|
+
List of rectangle UUIDs created
|
|
1295
1326
|
"""
|
|
1296
1327
|
from .component_bounds import get_component_bounding_box
|
|
1297
1328
|
|
|
@@ -1305,70 +1336,199 @@ class Schematic:
|
|
|
1305
1336
|
logger.info(f"Drew {len(uuids)} component bounding boxes")
|
|
1306
1337
|
return uuids
|
|
1307
1338
|
|
|
1308
|
-
#
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1339
|
+
# Metadata operations (delegated to MetadataManager)
|
|
1340
|
+
def set_title_block(
|
|
1341
|
+
self,
|
|
1342
|
+
title: str = "",
|
|
1343
|
+
date: str = "",
|
|
1344
|
+
rev: str = "",
|
|
1345
|
+
company: str = "",
|
|
1346
|
+
comments: Optional[Dict[int, str]] = None,
|
|
1347
|
+
) -> None:
|
|
1348
|
+
"""
|
|
1349
|
+
Set title block information.
|
|
1350
|
+
|
|
1351
|
+
Args:
|
|
1352
|
+
title: Schematic title
|
|
1353
|
+
date: Date
|
|
1354
|
+
rev: Revision
|
|
1355
|
+
company: Company name
|
|
1356
|
+
comments: Comment fields (1-9)
|
|
1357
|
+
"""
|
|
1358
|
+
self._metadata_manager.set_title_block(title, date, rev, company, comments)
|
|
1359
|
+
self._format_sync_manager.mark_dirty("title_block", "update")
|
|
1326
1360
|
self._modified = True
|
|
1327
|
-
logger.info("Cleared schematic")
|
|
1328
1361
|
|
|
1329
|
-
def
|
|
1330
|
-
"""
|
|
1331
|
-
|
|
1362
|
+
def set_paper_size(self, paper: str) -> None:
|
|
1363
|
+
"""
|
|
1364
|
+
Set paper size for the schematic.
|
|
1332
1365
|
|
|
1333
|
-
|
|
1366
|
+
Args:
|
|
1367
|
+
paper: Paper size (A4, A3, etc.)
|
|
1368
|
+
"""
|
|
1369
|
+
self._metadata_manager.set_paper_size(paper)
|
|
1370
|
+
self._format_sync_manager.mark_dirty("paper", "update")
|
|
1371
|
+
self._modified = True
|
|
1372
|
+
|
|
1373
|
+
# Validation (enhanced with ValidationManager)
|
|
1374
|
+
def validate(self) -> List[ValidationIssue]:
|
|
1375
|
+
"""
|
|
1376
|
+
Perform comprehensive schematic validation.
|
|
1377
|
+
|
|
1378
|
+
Returns:
|
|
1379
|
+
List of validation issues found
|
|
1380
|
+
"""
|
|
1381
|
+
# Use the new ValidationManager for comprehensive validation
|
|
1382
|
+
manager_issues = self._validation_manager.validate_schematic()
|
|
1383
|
+
|
|
1384
|
+
# Also run legacy validator for compatibility
|
|
1385
|
+
try:
|
|
1386
|
+
legacy_issues = self._legacy_validator.validate_schematic_data(self._data)
|
|
1387
|
+
except Exception as e:
|
|
1388
|
+
logger.warning(f"Legacy validator failed: {e}")
|
|
1389
|
+
legacy_issues = []
|
|
1334
1390
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1391
|
+
# Combine issues (remove duplicates based on message)
|
|
1392
|
+
all_issues = manager_issues + legacy_issues
|
|
1393
|
+
unique_issues = []
|
|
1394
|
+
seen_messages = set()
|
|
1338
1395
|
|
|
1339
|
-
|
|
1396
|
+
for issue in all_issues:
|
|
1397
|
+
if issue.message not in seen_messages:
|
|
1398
|
+
unique_issues.append(issue)
|
|
1399
|
+
seen_messages.add(issue.message)
|
|
1340
1400
|
|
|
1341
|
-
|
|
1342
|
-
def rebuild_indexes(self):
|
|
1343
|
-
"""Rebuild internal indexes for performance."""
|
|
1344
|
-
# This would rebuild component indexes, etc.
|
|
1345
|
-
logger.info("Rebuilt schematic indexes")
|
|
1401
|
+
return unique_issues
|
|
1346
1402
|
|
|
1347
|
-
def
|
|
1348
|
-
"""
|
|
1349
|
-
|
|
1403
|
+
def get_validation_summary(self) -> Dict[str, Any]:
|
|
1404
|
+
"""
|
|
1405
|
+
Get validation summary statistics.
|
|
1350
1406
|
|
|
1407
|
+
Returns:
|
|
1408
|
+
Summary dictionary with counts and severity
|
|
1409
|
+
"""
|
|
1410
|
+
issues = self.validate()
|
|
1411
|
+
return self._validation_manager.get_validation_summary(issues)
|
|
1412
|
+
|
|
1413
|
+
# Statistics and information
|
|
1414
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
1415
|
+
"""Get comprehensive schematic statistics."""
|
|
1351
1416
|
return {
|
|
1352
|
-
"
|
|
1417
|
+
"components": len(self._components),
|
|
1418
|
+
"wires": len(self._wires),
|
|
1419
|
+
"junctions": len(self._junctions),
|
|
1420
|
+
"text_elements": self._text_element_manager.get_text_statistics(),
|
|
1421
|
+
"graphics": self._graphics_manager.get_graphics_statistics(),
|
|
1422
|
+
"sheets": self._sheet_manager.get_sheet_statistics(),
|
|
1423
|
+
"performance": {
|
|
1353
1424
|
"operation_count": self._operation_count,
|
|
1354
|
-
"
|
|
1355
|
-
"
|
|
1356
|
-
|
|
1357
|
-
(self._total_operation_time / self._operation_count * 1000)
|
|
1358
|
-
if self._operation_count > 0
|
|
1359
|
-
else 0
|
|
1360
|
-
),
|
|
1361
|
-
2,
|
|
1362
|
-
),
|
|
1425
|
+
"total_operation_time": self._total_operation_time,
|
|
1426
|
+
"modified": self.modified,
|
|
1427
|
+
"last_save_time": self._last_save_time,
|
|
1363
1428
|
},
|
|
1364
|
-
"components": self._components.get_statistics(),
|
|
1365
|
-
"symbol_cache": cache_stats,
|
|
1366
1429
|
}
|
|
1367
1430
|
|
|
1368
1431
|
# Internal methods
|
|
1432
|
+
@staticmethod
|
|
1433
|
+
def _create_empty_schematic_data() -> Dict[str, Any]:
|
|
1434
|
+
"""Create empty schematic data structure."""
|
|
1435
|
+
from uuid import uuid4
|
|
1436
|
+
|
|
1437
|
+
return {
|
|
1438
|
+
"version": "20250114",
|
|
1439
|
+
"generator": "eeschema",
|
|
1440
|
+
"generator_version": "9.0",
|
|
1441
|
+
"uuid": str(uuid4()),
|
|
1442
|
+
"paper": "A4",
|
|
1443
|
+
"lib_symbols": {},
|
|
1444
|
+
"symbol": [],
|
|
1445
|
+
"wire": [],
|
|
1446
|
+
"junction": [],
|
|
1447
|
+
"label": [],
|
|
1448
|
+
"hierarchical_label": [],
|
|
1449
|
+
"global_label": [],
|
|
1450
|
+
"text": [],
|
|
1451
|
+
"sheet": [],
|
|
1452
|
+
"rectangle": [],
|
|
1453
|
+
"circle": [],
|
|
1454
|
+
"arc": [],
|
|
1455
|
+
"polyline": [],
|
|
1456
|
+
"image": [],
|
|
1457
|
+
"symbol_instances": [],
|
|
1458
|
+
"sheet_instances": [],
|
|
1459
|
+
"embedded_fonts": "no",
|
|
1460
|
+
"components": [],
|
|
1461
|
+
"wires": [],
|
|
1462
|
+
"junctions": [],
|
|
1463
|
+
"labels": [],
|
|
1464
|
+
"nets": [],
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
# Context manager support for atomic operations
|
|
1468
|
+
def __enter__(self):
|
|
1469
|
+
"""Enter atomic operation context."""
|
|
1470
|
+
# Create backup for rollback
|
|
1471
|
+
if self._file_path and self._file_path.exists():
|
|
1472
|
+
self._backup_path = self._file_io_manager.create_backup(
|
|
1473
|
+
self._file_path, ".atomic_backup"
|
|
1474
|
+
)
|
|
1475
|
+
return self
|
|
1476
|
+
|
|
1477
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1478
|
+
"""Exit atomic operation context."""
|
|
1479
|
+
if exc_type is not None:
|
|
1480
|
+
# Exception occurred - rollback if possible
|
|
1481
|
+
if hasattr(self, "_backup_path") and self._backup_path.exists():
|
|
1482
|
+
logger.warning("Exception in atomic operation - rolling back")
|
|
1483
|
+
# Restore from backup
|
|
1484
|
+
restored_data = self._file_io_manager.load_schematic(self._backup_path)
|
|
1485
|
+
self._data = restored_data
|
|
1486
|
+
self._modified = True
|
|
1487
|
+
else:
|
|
1488
|
+
# Success - clean up backup
|
|
1489
|
+
if hasattr(self, "_backup_path") and self._backup_path.exists():
|
|
1490
|
+
self._backup_path.unlink()
|
|
1491
|
+
|
|
1492
|
+
# Internal sync methods (migrated from original implementation)
|
|
1369
1493
|
def _sync_components_to_data(self):
|
|
1370
1494
|
"""Sync component collection state back to data structure."""
|
|
1371
|
-
|
|
1495
|
+
logger.debug("🔍 _sync_components_to_data: Syncing components to _data")
|
|
1496
|
+
|
|
1497
|
+
components_data = []
|
|
1498
|
+
for comp in self._components:
|
|
1499
|
+
# Start with base component data
|
|
1500
|
+
comp_dict = {k: v for k, v in comp._data.__dict__.items() if not k.startswith("_")}
|
|
1501
|
+
|
|
1502
|
+
# CRITICAL FIX: Explicitly preserve instances if user set them
|
|
1503
|
+
if hasattr(comp._data, "instances") and comp._data.instances:
|
|
1504
|
+
logger.debug(
|
|
1505
|
+
f" Component {comp._data.reference} has {len(comp._data.instances)} instance(s)"
|
|
1506
|
+
)
|
|
1507
|
+
comp_dict["instances"] = [
|
|
1508
|
+
{
|
|
1509
|
+
"project": (
|
|
1510
|
+
getattr(inst, "project", self.name)
|
|
1511
|
+
if hasattr(inst, "project")
|
|
1512
|
+
else self.name
|
|
1513
|
+
),
|
|
1514
|
+
"path": inst.path, # PRESERVE exact path user set!
|
|
1515
|
+
"reference": inst.reference,
|
|
1516
|
+
"unit": inst.unit,
|
|
1517
|
+
}
|
|
1518
|
+
for inst in comp._data.instances
|
|
1519
|
+
]
|
|
1520
|
+
logger.debug(
|
|
1521
|
+
f" Instance paths: {[inst.path for inst in comp._data.instances]}"
|
|
1522
|
+
)
|
|
1523
|
+
else:
|
|
1524
|
+
logger.debug(
|
|
1525
|
+
f" Component {comp._data.reference} has NO instances (will be generated by parser)"
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
components_data.append(comp_dict)
|
|
1529
|
+
|
|
1530
|
+
self._data["components"] = components_data
|
|
1531
|
+
logger.debug(f" Synced {len(components_data)} components to _data")
|
|
1372
1532
|
|
|
1373
1533
|
# Populate lib_symbols with actual symbol definitions used by components
|
|
1374
1534
|
lib_symbols = {}
|
|
@@ -1376,112 +1536,23 @@ class Schematic:
|
|
|
1376
1536
|
|
|
1377
1537
|
for comp in self._components:
|
|
1378
1538
|
if comp.lib_id and comp.lib_id not in lib_symbols:
|
|
1379
|
-
logger.debug(f"🔧 SCHEMATIC: Processing component {comp.lib_id}")
|
|
1380
|
-
|
|
1381
1539
|
# Get the actual symbol definition
|
|
1382
1540
|
symbol_def = cache.get_symbol(comp.lib_id)
|
|
1541
|
+
|
|
1383
1542
|
if symbol_def:
|
|
1384
|
-
|
|
1385
|
-
lib_symbols[comp.lib_id] =
|
|
1386
|
-
symbol_def, comp.lib_id
|
|
1387
|
-
)
|
|
1388
|
-
|
|
1389
|
-
# Check if this symbol extends another symbol using multiple methods
|
|
1390
|
-
extends_parent = None
|
|
1391
|
-
|
|
1392
|
-
# Method 1: Check raw_kicad_data
|
|
1393
|
-
if hasattr(symbol_def, "raw_kicad_data") and symbol_def.raw_kicad_data:
|
|
1394
|
-
extends_parent = self._check_symbol_extends(symbol_def.raw_kicad_data)
|
|
1395
|
-
logger.debug(
|
|
1396
|
-
f"🔧 SCHEMATIC: Checked raw_kicad_data for {comp.lib_id}, extends: {extends_parent}"
|
|
1397
|
-
)
|
|
1398
|
-
|
|
1399
|
-
# Method 2: Check raw_data attribute
|
|
1400
|
-
if not extends_parent and hasattr(symbol_def, "__dict__"):
|
|
1401
|
-
for attr_name, attr_value in symbol_def.__dict__.items():
|
|
1402
|
-
if attr_name == "raw_data":
|
|
1403
|
-
logger.debug(
|
|
1404
|
-
f"🔧 SCHEMATIC: Checking raw_data for extends: {type(attr_value)}"
|
|
1405
|
-
)
|
|
1406
|
-
extends_parent = self._check_symbol_extends(attr_value)
|
|
1407
|
-
if extends_parent:
|
|
1408
|
-
logger.debug(
|
|
1409
|
-
f"🔧 SCHEMATIC: Found extends in raw_data: {extends_parent}"
|
|
1410
|
-
)
|
|
1411
|
-
|
|
1412
|
-
# Method 3: Check the extends attribute directly
|
|
1413
|
-
if not extends_parent and hasattr(symbol_def, "extends"):
|
|
1414
|
-
extends_parent = symbol_def.extends
|
|
1415
|
-
logger.debug(f"🔧 SCHEMATIC: Found extends attribute: {extends_parent}")
|
|
1416
|
-
|
|
1417
|
-
if extends_parent:
|
|
1418
|
-
# Load the parent symbol too
|
|
1419
|
-
parent_lib_id = f"{comp.lib_id.split(':')[0]}:{extends_parent}"
|
|
1420
|
-
logger.debug(f"🔧 SCHEMATIC: Loading parent symbol: {parent_lib_id}")
|
|
1421
|
-
|
|
1422
|
-
if parent_lib_id not in lib_symbols:
|
|
1423
|
-
parent_symbol_def = cache.get_symbol(parent_lib_id)
|
|
1424
|
-
if parent_symbol_def:
|
|
1425
|
-
lib_symbols[parent_lib_id] = self._convert_symbol_to_kicad_format(
|
|
1426
|
-
parent_symbol_def, parent_lib_id
|
|
1427
|
-
)
|
|
1428
|
-
logger.debug(
|
|
1429
|
-
f"🔧 SCHEMATIC: Successfully loaded parent symbol: {parent_lib_id} for {comp.lib_id}"
|
|
1430
|
-
)
|
|
1431
|
-
else:
|
|
1432
|
-
logger.warning(
|
|
1433
|
-
f"🔧 SCHEMATIC: Failed to load parent symbol: {parent_lib_id}"
|
|
1434
|
-
)
|
|
1435
|
-
else:
|
|
1436
|
-
logger.debug(
|
|
1437
|
-
f"🔧 SCHEMATIC: Parent symbol {parent_lib_id} already loaded"
|
|
1438
|
-
)
|
|
1439
|
-
else:
|
|
1440
|
-
logger.debug(f"🔧 SCHEMATIC: No extends found for {comp.lib_id}")
|
|
1441
|
-
else:
|
|
1442
|
-
# Fallback for unknown symbols
|
|
1443
|
-
logger.warning(
|
|
1444
|
-
f"🔧 SCHEMATIC: Failed to load symbol {comp.lib_id}, using fallback"
|
|
1445
|
-
)
|
|
1446
|
-
lib_symbols[comp.lib_id] = {"definition": "basic"}
|
|
1543
|
+
converted_symbol = self._convert_symbol_to_kicad_format(symbol_def, comp.lib_id)
|
|
1544
|
+
lib_symbols[comp.lib_id] = converted_symbol
|
|
1447
1545
|
|
|
1448
1546
|
self._data["lib_symbols"] = lib_symbols
|
|
1449
1547
|
|
|
1450
|
-
#
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
logger.debug(f"🔧 FINAL: - {sym_id}")
|
|
1454
|
-
# Check if this symbol has extends
|
|
1455
|
-
sym_data = lib_symbols[sym_id]
|
|
1456
|
-
if isinstance(sym_data, list) and len(sym_data) > 2:
|
|
1457
|
-
for item in sym_data[1:]:
|
|
1458
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1459
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1460
|
-
logger.debug(f"🔧 FINAL: - {sym_id} extends {item[1]}")
|
|
1461
|
-
break
|
|
1462
|
-
|
|
1463
|
-
def _check_symbol_extends(self, symbol_data: Any) -> Optional[str]:
|
|
1464
|
-
"""Check if symbol extends another symbol and return parent name."""
|
|
1465
|
-
logger.debug(f"🔧 EXTENDS: Checking symbol data type: {type(symbol_data)}")
|
|
1548
|
+
# Update sheet instances
|
|
1549
|
+
if not self._data["sheet_instances"]:
|
|
1550
|
+
self._data["sheet_instances"] = [{"path": "/", "page": "1"}]
|
|
1466
1551
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
logger.debug(f"🔧 EXTENDS: Checking {len(symbol_data)} items for extends directive")
|
|
1472
|
-
|
|
1473
|
-
for i, item in enumerate(symbol_data[1:], 1):
|
|
1474
|
-
logger.debug(
|
|
1475
|
-
f"🔧 EXTENDS: Item {i}: {type(item)} - {item if not isinstance(item, list) else f'list[{len(item)}]'}"
|
|
1476
|
-
)
|
|
1477
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1478
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1479
|
-
parent_name = str(item[1]).strip('"')
|
|
1480
|
-
logger.debug(f"🔧 EXTENDS: Found extends directive: {parent_name}")
|
|
1481
|
-
return parent_name
|
|
1482
|
-
|
|
1483
|
-
logger.debug(f"🔧 EXTENDS: No extends directive found")
|
|
1484
|
-
return None
|
|
1552
|
+
# Remove symbol_instances section - instances are stored within each symbol in lib_symbols
|
|
1553
|
+
# This matches KiCAD's format where instances are part of the symbol definition
|
|
1554
|
+
if "symbol_instances" in self._data:
|
|
1555
|
+
del self._data["symbol_instances"]
|
|
1485
1556
|
|
|
1486
1557
|
def _sync_wires_to_data(self):
|
|
1487
1558
|
"""Sync wire collection state back to data structure."""
|
|
@@ -1512,163 +1583,341 @@ class Schematic:
|
|
|
1512
1583
|
|
|
1513
1584
|
self._data["junctions"] = junction_data
|
|
1514
1585
|
|
|
1515
|
-
def
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1586
|
+
def _sync_texts_to_data(self):
|
|
1587
|
+
"""Sync text collection state back to data structure."""
|
|
1588
|
+
text_data = []
|
|
1589
|
+
for text_element in self._texts:
|
|
1590
|
+
text_dict = {
|
|
1591
|
+
"uuid": text_element.uuid,
|
|
1592
|
+
"text": text_element.text,
|
|
1593
|
+
"position": {"x": text_element.position.x, "y": text_element.position.y},
|
|
1594
|
+
"rotation": text_element.rotation,
|
|
1595
|
+
"size": text_element.size,
|
|
1596
|
+
"exclude_from_sim": text_element.exclude_from_sim,
|
|
1597
|
+
}
|
|
1598
|
+
text_data.append(text_dict)
|
|
1599
|
+
|
|
1600
|
+
self._data["texts"] = text_data
|
|
1601
|
+
|
|
1602
|
+
def _sync_labels_to_data(self):
|
|
1603
|
+
"""Sync label collection state back to data structure."""
|
|
1604
|
+
label_data = []
|
|
1605
|
+
for label_element in self._labels:
|
|
1606
|
+
label_dict = {
|
|
1607
|
+
"uuid": label_element.uuid,
|
|
1608
|
+
"text": label_element.text,
|
|
1609
|
+
"position": {"x": label_element.position.x, "y": label_element.position.y},
|
|
1610
|
+
"rotation": label_element.rotation,
|
|
1611
|
+
"size": label_element.size,
|
|
1612
|
+
"justify_h": label_element._data.justify_h,
|
|
1613
|
+
"justify_v": label_element._data.justify_v,
|
|
1614
|
+
}
|
|
1615
|
+
label_data.append(label_dict)
|
|
1616
|
+
|
|
1617
|
+
self._data["labels"] = label_data
|
|
1618
|
+
|
|
1619
|
+
def _sync_hierarchical_labels_to_data(self):
|
|
1620
|
+
"""Sync hierarchical label collection state back to data structure."""
|
|
1621
|
+
hierarchical_label_data = []
|
|
1622
|
+
for hlabel_element in self._hierarchical_labels:
|
|
1623
|
+
hlabel_dict = {
|
|
1624
|
+
"uuid": hlabel_element.uuid,
|
|
1625
|
+
"text": hlabel_element.text,
|
|
1626
|
+
"position": {"x": hlabel_element.position.x, "y": hlabel_element.position.y},
|
|
1627
|
+
"rotation": hlabel_element.rotation,
|
|
1628
|
+
"size": hlabel_element.size,
|
|
1629
|
+
}
|
|
1630
|
+
hierarchical_label_data.append(hlabel_dict)
|
|
1631
|
+
|
|
1632
|
+
self._data["hierarchical_labels"] = hierarchical_label_data
|
|
1633
|
+
|
|
1634
|
+
def _sync_no_connects_to_data(self):
|
|
1635
|
+
"""Sync no-connect collection state back to data structure."""
|
|
1636
|
+
no_connect_data = []
|
|
1637
|
+
for no_connect_element in self._no_connects:
|
|
1638
|
+
no_connect_dict = {
|
|
1639
|
+
"uuid": no_connect_element.uuid,
|
|
1640
|
+
"position": {
|
|
1641
|
+
"x": no_connect_element.position.x,
|
|
1642
|
+
"y": no_connect_element.position.y,
|
|
1643
|
+
},
|
|
1644
|
+
}
|
|
1645
|
+
no_connect_data.append(no_connect_dict)
|
|
1646
|
+
|
|
1647
|
+
self._data["no_connects"] = no_connect_data
|
|
1648
|
+
|
|
1649
|
+
def _sync_nets_to_data(self):
|
|
1650
|
+
"""Sync net collection state back to data structure."""
|
|
1651
|
+
net_data = []
|
|
1652
|
+
for net_element in self._nets:
|
|
1653
|
+
net_dict = {
|
|
1654
|
+
"name": net_element.name,
|
|
1655
|
+
"components": net_element.components,
|
|
1656
|
+
"wires": net_element.wires,
|
|
1657
|
+
"labels": net_element.labels,
|
|
1658
|
+
}
|
|
1659
|
+
net_data.append(net_dict)
|
|
1660
|
+
|
|
1661
|
+
self._data["nets"] = net_data
|
|
1662
|
+
|
|
1663
|
+
def _convert_symbol_to_kicad_format(self, symbol_def, lib_id: str):
|
|
1664
|
+
"""Convert symbol definition to KiCAD format."""
|
|
1665
|
+
# Use raw data if available, but fix the symbol name to use full lib_id
|
|
1666
|
+
if hasattr(symbol_def, "raw_kicad_data") and symbol_def.raw_kicad_data:
|
|
1667
|
+
raw_data = symbol_def.raw_kicad_data
|
|
1668
|
+
|
|
1669
|
+
# Check if raw data already contains instances with project info
|
|
1670
|
+
project_refs_found = []
|
|
1671
|
+
|
|
1672
|
+
def find_project_refs(data, path="root"):
|
|
1673
|
+
if isinstance(data, list):
|
|
1674
|
+
for i, item in enumerate(data):
|
|
1675
|
+
if hasattr(item, "__str__") and str(item) == "project":
|
|
1676
|
+
if i < len(data) - 1:
|
|
1677
|
+
project_refs_found.append(f"{path}[{i}] = '{data[i+1]}'")
|
|
1678
|
+
elif isinstance(item, list):
|
|
1679
|
+
find_project_refs(item, f"{path}[{i}]")
|
|
1680
|
+
|
|
1681
|
+
find_project_refs(raw_data)
|
|
1682
|
+
|
|
1683
|
+
# Make a copy and fix the symbol name (index 1) to use full lib_id
|
|
1684
|
+
if isinstance(raw_data, list) and len(raw_data) > 1:
|
|
1685
|
+
fixed_data = raw_data.copy()
|
|
1686
|
+
fixed_data[1] = lib_id # Replace short name with full lib_id
|
|
1687
|
+
|
|
1688
|
+
# Also fix any project references in instances to use current project name
|
|
1689
|
+
self._fix_symbol_project_references(fixed_data)
|
|
1690
|
+
|
|
1691
|
+
return fixed_data
|
|
1692
|
+
else:
|
|
1693
|
+
return raw_data
|
|
1522
1694
|
|
|
1523
1695
|
# Fallback: create basic symbol structure
|
|
1524
1696
|
return {
|
|
1525
|
-
"
|
|
1526
|
-
"
|
|
1527
|
-
"exclude_from_sim": "no",
|
|
1528
|
-
"in_bom": "yes",
|
|
1529
|
-
"on_board": "yes",
|
|
1530
|
-
"properties": {
|
|
1531
|
-
"Reference": {
|
|
1532
|
-
"value": symbol.reference_prefix,
|
|
1533
|
-
"at": [2.032, 0, 90],
|
|
1534
|
-
"effects": {"font": {"size": [1.27, 1.27]}},
|
|
1535
|
-
},
|
|
1536
|
-
"Value": {
|
|
1537
|
-
"value": symbol.reference_prefix,
|
|
1538
|
-
"at": [0, 0, 90],
|
|
1539
|
-
"effects": {"font": {"size": [1.27, 1.27]}},
|
|
1540
|
-
},
|
|
1541
|
-
"Footprint": {
|
|
1542
|
-
"value": "",
|
|
1543
|
-
"at": [-1.778, 0, 90],
|
|
1544
|
-
"effects": {"font": {"size": [1.27, 1.27]}, "hide": "yes"},
|
|
1545
|
-
},
|
|
1546
|
-
"Datasheet": {
|
|
1547
|
-
"value": getattr(symbol, "Datasheet", None)
|
|
1548
|
-
or getattr(symbol, "datasheet", None)
|
|
1549
|
-
or "~",
|
|
1550
|
-
"at": [0, 0, 0],
|
|
1551
|
-
"effects": {"font": {"size": [1.27, 1.27]}, "hide": "yes"},
|
|
1552
|
-
},
|
|
1553
|
-
"Description": {
|
|
1554
|
-
"value": getattr(symbol, "Description", None)
|
|
1555
|
-
or getattr(symbol, "description", None)
|
|
1556
|
-
or "Resistor",
|
|
1557
|
-
"at": [0, 0, 0],
|
|
1558
|
-
"effects": {"font": {"size": [1.27, 1.27]}, "hide": "yes"},
|
|
1559
|
-
},
|
|
1560
|
-
},
|
|
1561
|
-
"embedded_fonts": "no",
|
|
1697
|
+
"lib_id": lib_id,
|
|
1698
|
+
"symbol": symbol_def.name if hasattr(symbol_def, "name") else lib_id.split(":")[-1],
|
|
1562
1699
|
}
|
|
1563
1700
|
|
|
1564
|
-
def
|
|
1565
|
-
"""
|
|
1566
|
-
|
|
1701
|
+
def _fix_symbol_project_references(self, symbol_data):
|
|
1702
|
+
"""Fix project references in symbol instances to use current project name."""
|
|
1703
|
+
if not isinstance(symbol_data, list):
|
|
1704
|
+
return
|
|
1705
|
+
|
|
1706
|
+
# Recursively search for instances sections and update project names
|
|
1707
|
+
for i, element in enumerate(symbol_data):
|
|
1708
|
+
if isinstance(element, list):
|
|
1709
|
+
# Check if this is an instances section
|
|
1710
|
+
if (
|
|
1711
|
+
len(element) > 0
|
|
1712
|
+
and hasattr(element[0], "__str__")
|
|
1713
|
+
and str(element[0]) == "instances"
|
|
1714
|
+
):
|
|
1715
|
+
# Look for project references within instances
|
|
1716
|
+
self._update_project_in_instances(element)
|
|
1717
|
+
else:
|
|
1718
|
+
# Recursively check nested lists
|
|
1719
|
+
self._fix_symbol_project_references(element)
|
|
1720
|
+
|
|
1721
|
+
def _update_project_in_instances(self, instances_element):
|
|
1722
|
+
"""Update project name in instances element."""
|
|
1723
|
+
if not isinstance(instances_element, list):
|
|
1724
|
+
return
|
|
1725
|
+
|
|
1726
|
+
for i, element in enumerate(instances_element):
|
|
1727
|
+
if isinstance(element, list) and len(element) >= 2:
|
|
1728
|
+
# Check if this is a project element: ['project', 'old_name', ...]
|
|
1729
|
+
if hasattr(element[0], "__str__") and str(element[0]) == "project":
|
|
1730
|
+
old_name = element[1]
|
|
1731
|
+
element[1] = self.name # Replace with current schematic name
|
|
1732
|
+
else:
|
|
1733
|
+
# Recursively check nested elements
|
|
1734
|
+
self._update_project_in_instances(element)
|
|
1567
1735
|
|
|
1568
|
-
|
|
1736
|
+
# ============================================================================
|
|
1737
|
+
# Export Methods (using kicad-cli)
|
|
1738
|
+
# ============================================================================
|
|
1569
1739
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1740
|
+
def run_erc(self, **kwargs):
|
|
1741
|
+
"""
|
|
1742
|
+
Run Electrical Rule Check (ERC) on this schematic.
|
|
1572
1743
|
|
|
1573
|
-
|
|
1574
|
-
if len(modified_data) >= 2:
|
|
1575
|
-
modified_data[1] = lib_id # Change 'R' to 'Device:R'
|
|
1744
|
+
This requires the schematic to be saved first.
|
|
1576
1745
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
)
|
|
1584
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1585
|
-
# Convert bare symbol name to full lib_id
|
|
1586
|
-
parent_name = str(item[1]).strip('"')
|
|
1587
|
-
parent_lib_id = f"{lib_id.split(':')[0]}:{parent_name}"
|
|
1588
|
-
modified_data[i][1] = parent_lib_id
|
|
1589
|
-
logger.debug(
|
|
1590
|
-
f"🔧 CONVERT: Fixed extends directive: {parent_name} -> {parent_lib_id}"
|
|
1591
|
-
)
|
|
1592
|
-
break
|
|
1593
|
-
|
|
1594
|
-
# Fix string/symbol conversion issues in pin definitions
|
|
1595
|
-
print(f"🔧 DEBUG: Before fix - checking for pin definitions...")
|
|
1596
|
-
self._fix_symbol_strings_recursively(modified_data)
|
|
1597
|
-
print(f"🔧 DEBUG: After fix - symbol strings fixed")
|
|
1598
|
-
|
|
1599
|
-
return modified_data
|
|
1600
|
-
|
|
1601
|
-
def _fix_symbol_strings_recursively(self, data):
|
|
1602
|
-
"""Recursively fix string/symbol issues in parsed S-expression data."""
|
|
1603
|
-
import sexpdata
|
|
1604
|
-
|
|
1605
|
-
if isinstance(data, list):
|
|
1606
|
-
for i, item in enumerate(data):
|
|
1607
|
-
if isinstance(item, list):
|
|
1608
|
-
# Check for pin definitions that need fixing
|
|
1609
|
-
if len(item) >= 3 and item[0] == sexpdata.Symbol("pin"):
|
|
1610
|
-
print(
|
|
1611
|
-
f"🔧 DEBUG: Found pin definition: {item[:3]} - types: {[type(x) for x in item[:3]]}"
|
|
1612
|
-
)
|
|
1613
|
-
# Fix pin type and shape - ensure they are symbols not strings
|
|
1614
|
-
if isinstance(item[1], str):
|
|
1615
|
-
print(f"🔧 DEBUG: Converting pin type '{item[1]}' to symbol")
|
|
1616
|
-
item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
|
|
1617
|
-
if len(item) >= 3 and isinstance(item[2], str):
|
|
1618
|
-
print(f"🔧 DEBUG: Converting pin shape '{item[2]}' to symbol")
|
|
1619
|
-
item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
|
|
1620
|
-
|
|
1621
|
-
# Recursively process nested lists
|
|
1622
|
-
self._fix_symbol_strings_recursively(item)
|
|
1623
|
-
elif isinstance(item, str):
|
|
1624
|
-
# Fix common KiCAD keywords that should be symbols
|
|
1625
|
-
if item in ["yes", "no", "default", "none", "left", "right", "center"]:
|
|
1626
|
-
data[i] = sexpdata.Symbol(item)
|
|
1627
|
-
|
|
1628
|
-
return data
|
|
1746
|
+
Args:
|
|
1747
|
+
**kwargs: Arguments passed to cli.erc.run_erc()
|
|
1748
|
+
- output_path: Path for ERC report
|
|
1749
|
+
- format: 'json' or 'report'
|
|
1750
|
+
- severity: 'all', 'error', 'warning', 'exclusions'
|
|
1751
|
+
- units: 'mm', 'in', 'mils'
|
|
1629
1752
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
"""Create empty schematic data structure."""
|
|
1633
|
-
return {
|
|
1634
|
-
"version": "20250114",
|
|
1635
|
-
"generator": "eeschema",
|
|
1636
|
-
"generator_version": "9.0",
|
|
1637
|
-
"uuid": str(uuid.uuid4()),
|
|
1638
|
-
"paper": "A4",
|
|
1639
|
-
"components": [],
|
|
1640
|
-
"wires": [],
|
|
1641
|
-
"junctions": [],
|
|
1642
|
-
"labels": [],
|
|
1643
|
-
"nets": [],
|
|
1644
|
-
"lib_symbols": {},
|
|
1645
|
-
"sheet_instances": [{"path": "/", "page": "1"}],
|
|
1646
|
-
"symbol_instances": [],
|
|
1647
|
-
"embedded_fonts": "no",
|
|
1648
|
-
}
|
|
1753
|
+
Returns:
|
|
1754
|
+
ErcReport with violations and summary
|
|
1649
1755
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
return self
|
|
1756
|
+
Example:
|
|
1757
|
+
>>> report = sch.run_erc()
|
|
1758
|
+
>>> if report.has_errors():
|
|
1759
|
+
... print(f"Found {report.error_count} errors")
|
|
1760
|
+
"""
|
|
1761
|
+
from kicad_sch_api.cli.erc import run_erc
|
|
1657
1762
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1763
|
+
if not self._file_path:
|
|
1764
|
+
raise ValueError("Schematic must be saved before running ERC")
|
|
1765
|
+
|
|
1766
|
+
# Save first to ensure file is up-to-date
|
|
1767
|
+
self.save()
|
|
1768
|
+
|
|
1769
|
+
return run_erc(self._file_path, **kwargs)
|
|
1770
|
+
|
|
1771
|
+
def export_netlist(self, format="kicadsexpr", **kwargs):
|
|
1772
|
+
"""
|
|
1773
|
+
Export netlist from this schematic.
|
|
1774
|
+
|
|
1775
|
+
This requires the schematic to be saved first.
|
|
1776
|
+
|
|
1777
|
+
Args:
|
|
1778
|
+
format: Netlist format (default: 'kicadsexpr')
|
|
1779
|
+
- kicadsexpr: KiCad S-expression (default)
|
|
1780
|
+
- kicadxml: KiCad XML
|
|
1781
|
+
- spice: SPICE netlist
|
|
1782
|
+
- spicemodel: SPICE with models
|
|
1783
|
+
- cadstar, orcadpcb2, pads, allegro
|
|
1784
|
+
**kwargs: Arguments passed to cli.netlist.export_netlist()
|
|
1785
|
+
|
|
1786
|
+
Returns:
|
|
1787
|
+
Path to generated netlist file
|
|
1788
|
+
|
|
1789
|
+
Example:
|
|
1790
|
+
>>> netlist = sch.export_netlist(format='spice')
|
|
1791
|
+
>>> print(f"Netlist: {netlist}")
|
|
1792
|
+
"""
|
|
1793
|
+
from kicad_sch_api.cli.netlist import export_netlist
|
|
1794
|
+
|
|
1795
|
+
if not self._file_path:
|
|
1796
|
+
raise ValueError("Schematic must be saved before exporting netlist")
|
|
1797
|
+
|
|
1798
|
+
# Save first to ensure file is up-to-date
|
|
1799
|
+
self.save()
|
|
1800
|
+
|
|
1801
|
+
return export_netlist(self._file_path, format=format, **kwargs)
|
|
1802
|
+
|
|
1803
|
+
def export_bom(self, **kwargs):
|
|
1804
|
+
"""
|
|
1805
|
+
Export Bill of Materials (BOM) from this schematic.
|
|
1806
|
+
|
|
1807
|
+
This requires the schematic to be saved first.
|
|
1808
|
+
|
|
1809
|
+
Args:
|
|
1810
|
+
**kwargs: Arguments passed to cli.bom.export_bom()
|
|
1811
|
+
- output_path: Path for BOM file
|
|
1812
|
+
- fields: List of fields to export
|
|
1813
|
+
- group_by: Fields to group by
|
|
1814
|
+
- exclude_dnp: Exclude Do-Not-Populate components
|
|
1815
|
+
- And many more options...
|
|
1816
|
+
|
|
1817
|
+
Returns:
|
|
1818
|
+
Path to generated BOM file
|
|
1819
|
+
|
|
1820
|
+
Example:
|
|
1821
|
+
>>> bom = sch.export_bom(
|
|
1822
|
+
... fields=['Reference', 'Value', 'Footprint', 'MPN'],
|
|
1823
|
+
... group_by=['Value', 'Footprint'],
|
|
1824
|
+
... exclude_dnp=True,
|
|
1825
|
+
... )
|
|
1826
|
+
"""
|
|
1827
|
+
from kicad_sch_api.cli.bom import export_bom
|
|
1828
|
+
|
|
1829
|
+
if not self._file_path:
|
|
1830
|
+
raise ValueError("Schematic must be saved before exporting BOM")
|
|
1831
|
+
|
|
1832
|
+
# Save first to ensure file is up-to-date
|
|
1833
|
+
self.save()
|
|
1834
|
+
|
|
1835
|
+
return export_bom(self._file_path, **kwargs)
|
|
1836
|
+
|
|
1837
|
+
def export_pdf(self, **kwargs):
|
|
1838
|
+
"""
|
|
1839
|
+
Export schematic as PDF.
|
|
1840
|
+
|
|
1841
|
+
This requires the schematic to be saved first.
|
|
1842
|
+
|
|
1843
|
+
Args:
|
|
1844
|
+
**kwargs: Arguments passed to cli.export_docs.export_pdf()
|
|
1845
|
+
- output_path: Path for PDF file
|
|
1846
|
+
- theme: Color theme
|
|
1847
|
+
- black_and_white: B&W export
|
|
1848
|
+
- And more options...
|
|
1849
|
+
|
|
1850
|
+
Returns:
|
|
1851
|
+
Path to generated PDF file
|
|
1852
|
+
|
|
1853
|
+
Example:
|
|
1854
|
+
>>> pdf = sch.export_pdf(theme='Kicad Classic')
|
|
1855
|
+
"""
|
|
1856
|
+
from kicad_sch_api.cli.export_docs import export_pdf
|
|
1857
|
+
|
|
1858
|
+
if not self._file_path:
|
|
1859
|
+
raise ValueError("Schematic must be saved before exporting PDF")
|
|
1860
|
+
|
|
1861
|
+
# Save first to ensure file is up-to-date
|
|
1862
|
+
self.save()
|
|
1863
|
+
|
|
1864
|
+
return export_pdf(self._file_path, **kwargs)
|
|
1865
|
+
|
|
1866
|
+
def export_svg(self, **kwargs):
|
|
1867
|
+
"""
|
|
1868
|
+
Export schematic as SVG.
|
|
1869
|
+
|
|
1870
|
+
This requires the schematic to be saved first.
|
|
1871
|
+
|
|
1872
|
+
Args:
|
|
1873
|
+
**kwargs: Arguments passed to cli.export_docs.export_svg()
|
|
1874
|
+
- output_dir: Output directory
|
|
1875
|
+
- theme: Color theme
|
|
1876
|
+
- black_and_white: B&W export
|
|
1877
|
+
- And more options...
|
|
1878
|
+
|
|
1879
|
+
Returns:
|
|
1880
|
+
List of paths to generated SVG files
|
|
1881
|
+
|
|
1882
|
+
Example:
|
|
1883
|
+
>>> svgs = sch.export_svg()
|
|
1884
|
+
>>> for svg in svgs:
|
|
1885
|
+
... print(f"Generated: {svg}")
|
|
1886
|
+
"""
|
|
1887
|
+
from kicad_sch_api.cli.export_docs import export_svg
|
|
1888
|
+
|
|
1889
|
+
if not self._file_path:
|
|
1890
|
+
raise ValueError("Schematic must be saved before exporting SVG")
|
|
1891
|
+
|
|
1892
|
+
# Save first to ensure file is up-to-date
|
|
1893
|
+
self.save()
|
|
1894
|
+
|
|
1895
|
+
return export_svg(self._file_path, **kwargs)
|
|
1896
|
+
|
|
1897
|
+
def export_dxf(self, **kwargs):
|
|
1898
|
+
"""
|
|
1899
|
+
Export schematic as DXF.
|
|
1900
|
+
|
|
1901
|
+
This requires the schematic to be saved first.
|
|
1902
|
+
|
|
1903
|
+
Args:
|
|
1904
|
+
**kwargs: Arguments passed to cli.export_docs.export_dxf()
|
|
1905
|
+
|
|
1906
|
+
Returns:
|
|
1907
|
+
List of paths to generated DXF files
|
|
1908
|
+
|
|
1909
|
+
Example:
|
|
1910
|
+
>>> dxfs = sch.export_dxf()
|
|
1911
|
+
"""
|
|
1912
|
+
from kicad_sch_api.cli.export_docs import export_dxf
|
|
1913
|
+
|
|
1914
|
+
if not self._file_path:
|
|
1915
|
+
raise ValueError("Schematic must be saved before exporting DXF")
|
|
1916
|
+
|
|
1917
|
+
# Save first to ensure file is up-to-date
|
|
1918
|
+
self.save()
|
|
1919
|
+
|
|
1920
|
+
return export_dxf(self._file_path, **kwargs)
|
|
1672
1921
|
|
|
1673
1922
|
def __str__(self) -> str:
|
|
1674
1923
|
"""String representation."""
|