kicad-sch-api 0.3.4__py3-none-any.whl → 0.4.0__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.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/collections/__init__.py +21 -0
- kicad_sch_api/collections/base.py +294 -0
- kicad_sch_api/collections/components.py +434 -0
- kicad_sch_api/collections/junctions.py +366 -0
- kicad_sch_api/collections/labels.py +404 -0
- kicad_sch_api/collections/wires.py +406 -0
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +348 -0
- kicad_sch_api/core/managers/__init__.py +26 -0
- kicad_sch_api/core/managers/file_io.py +243 -0
- kicad_sch_api/core/managers/format_sync.py +501 -0
- kicad_sch_api/core/managers/graphics.py +579 -0
- kicad_sch_api/core/managers/metadata.py +268 -0
- kicad_sch_api/core/managers/sheet.py +454 -0
- kicad_sch_api/core/managers/text_elements.py +536 -0
- kicad_sch_api/core/managers/validation.py +474 -0
- kicad_sch_api/core/managers/wire.py +346 -0
- kicad_sch_api/core/nets.py +310 -0
- kicad_sch_api/core/no_connects.py +276 -0
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +904 -1074
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +13 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +56 -43
- 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/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/symbol_parser.py +222 -0
- kicad_sch_api/parsers/wire_parser.py +99 -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-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
- kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
- kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +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
|
|
@@ -18,13 +19,28 @@ from ..utils.validation import SchematicValidator, ValidationError, ValidationIs
|
|
|
18
19
|
from .components import ComponentCollection
|
|
19
20
|
from .formatter import ExactFormatter
|
|
20
21
|
from .junctions import JunctionCollection
|
|
22
|
+
from .labels import LabelCollection
|
|
23
|
+
from .managers import (
|
|
24
|
+
FileIOManager,
|
|
25
|
+
FormatSyncManager,
|
|
26
|
+
GraphicsManager,
|
|
27
|
+
MetadataManager,
|
|
28
|
+
SheetManager,
|
|
29
|
+
TextElementManager,
|
|
30
|
+
ValidationManager,
|
|
31
|
+
WireManager,
|
|
32
|
+
)
|
|
33
|
+
from .nets import NetCollection
|
|
34
|
+
from .no_connects import NoConnectCollection
|
|
21
35
|
from .parser import SExpressionParser
|
|
36
|
+
from .texts import TextCollection
|
|
22
37
|
from .types import (
|
|
23
38
|
HierarchicalLabelShape,
|
|
24
39
|
Junction,
|
|
25
40
|
Label,
|
|
26
41
|
LabelType,
|
|
27
42
|
Net,
|
|
43
|
+
NoConnect,
|
|
28
44
|
Point,
|
|
29
45
|
SchematicSymbol,
|
|
30
46
|
Sheet,
|
|
@@ -41,7 +57,7 @@ logger = logging.getLogger(__name__)
|
|
|
41
57
|
|
|
42
58
|
class Schematic:
|
|
43
59
|
"""
|
|
44
|
-
Professional KiCAD schematic manipulation class.
|
|
60
|
+
Professional KiCAD schematic manipulation class with manager-based architecture.
|
|
45
61
|
|
|
46
62
|
Features:
|
|
47
63
|
- Exact format preservation
|
|
@@ -50,9 +66,10 @@ class Schematic:
|
|
|
50
66
|
- Comprehensive validation
|
|
51
67
|
- Performance optimization for large schematics
|
|
52
68
|
- AI agent integration via MCP
|
|
69
|
+
- Modular architecture with specialized managers
|
|
53
70
|
|
|
54
71
|
This class provides a modern, intuitive API while maintaining exact compatibility
|
|
55
|
-
with KiCAD's native file format.
|
|
72
|
+
with KiCAD's native file format through specialized manager classes.
|
|
56
73
|
"""
|
|
57
74
|
|
|
58
75
|
def __init__(
|
|
@@ -62,7 +79,7 @@ class Schematic:
|
|
|
62
79
|
name: Optional[str] = None,
|
|
63
80
|
):
|
|
64
81
|
"""
|
|
65
|
-
Initialize schematic object.
|
|
82
|
+
Initialize schematic object with manager-based architecture.
|
|
66
83
|
|
|
67
84
|
Args:
|
|
68
85
|
schematic_data: Parsed schematic data
|
|
@@ -73,13 +90,13 @@ class Schematic:
|
|
|
73
90
|
self._data = schematic_data or self._create_empty_schematic_data()
|
|
74
91
|
self._file_path = Path(file_path) if file_path else None
|
|
75
92
|
self._original_content = self._data.get("_original_content", "")
|
|
76
|
-
self.name = name or "simple_circuit"
|
|
93
|
+
self.name = name or "simple_circuit"
|
|
77
94
|
|
|
78
95
|
# Initialize parser and formatter
|
|
79
96
|
self._parser = SExpressionParser(preserve_format=True)
|
|
80
|
-
self._parser.project_name = self.name
|
|
97
|
+
self._parser.project_name = self.name
|
|
81
98
|
self._formatter = ExactFormatter()
|
|
82
|
-
self.
|
|
99
|
+
self._legacy_validator = SchematicValidator() # Keep for compatibility
|
|
83
100
|
|
|
84
101
|
# Initialize component collection
|
|
85
102
|
component_symbols = [
|
|
@@ -136,6 +153,142 @@ class Schematic:
|
|
|
136
153
|
junctions.append(junction)
|
|
137
154
|
self._junctions = JunctionCollection(junctions)
|
|
138
155
|
|
|
156
|
+
# Initialize text collection
|
|
157
|
+
text_data = self._data.get("texts", [])
|
|
158
|
+
texts = []
|
|
159
|
+
for text_dict in text_data:
|
|
160
|
+
if isinstance(text_dict, dict):
|
|
161
|
+
# Convert dict to Text object
|
|
162
|
+
position = text_dict.get("position", {"x": 0, "y": 0})
|
|
163
|
+
if isinstance(position, dict):
|
|
164
|
+
pos = Point(position["x"], position["y"])
|
|
165
|
+
elif isinstance(position, (list, tuple)):
|
|
166
|
+
pos = Point(position[0], position[1])
|
|
167
|
+
else:
|
|
168
|
+
pos = position
|
|
169
|
+
|
|
170
|
+
text = Text(
|
|
171
|
+
uuid=text_dict.get("uuid", str(uuid.uuid4())),
|
|
172
|
+
position=pos,
|
|
173
|
+
text=text_dict.get("text", ""),
|
|
174
|
+
rotation=text_dict.get("rotation", 0.0),
|
|
175
|
+
size=text_dict.get("size", 1.27),
|
|
176
|
+
exclude_from_sim=text_dict.get("exclude_from_sim", False),
|
|
177
|
+
)
|
|
178
|
+
texts.append(text)
|
|
179
|
+
self._texts = TextCollection(texts)
|
|
180
|
+
|
|
181
|
+
# Initialize label collection
|
|
182
|
+
label_data = self._data.get("labels", [])
|
|
183
|
+
labels = []
|
|
184
|
+
for label_dict in label_data:
|
|
185
|
+
if isinstance(label_dict, dict):
|
|
186
|
+
# Convert dict to Label object
|
|
187
|
+
position = label_dict.get("position", {"x": 0, "y": 0})
|
|
188
|
+
if isinstance(position, dict):
|
|
189
|
+
pos = Point(position["x"], position["y"])
|
|
190
|
+
elif isinstance(position, (list, tuple)):
|
|
191
|
+
pos = Point(position[0], position[1])
|
|
192
|
+
else:
|
|
193
|
+
pos = position
|
|
194
|
+
|
|
195
|
+
label = Label(
|
|
196
|
+
uuid=label_dict.get("uuid", str(uuid.uuid4())),
|
|
197
|
+
position=pos,
|
|
198
|
+
text=label_dict.get("text", ""),
|
|
199
|
+
label_type=LabelType(label_dict.get("label_type", "local")),
|
|
200
|
+
rotation=label_dict.get("rotation", 0.0),
|
|
201
|
+
size=label_dict.get("size", 1.27),
|
|
202
|
+
shape=(
|
|
203
|
+
HierarchicalLabelShape(label_dict.get("shape"))
|
|
204
|
+
if label_dict.get("shape")
|
|
205
|
+
else None
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
labels.append(label)
|
|
209
|
+
self._labels = LabelCollection(labels)
|
|
210
|
+
|
|
211
|
+
# Initialize hierarchical labels collection (from both labels array and hierarchical_labels array)
|
|
212
|
+
hierarchical_labels = [
|
|
213
|
+
label for label in labels if label.label_type == LabelType.HIERARCHICAL
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
# Also load from hierarchical_labels data if present
|
|
217
|
+
hierarchical_label_data = self._data.get("hierarchical_labels", [])
|
|
218
|
+
for hlabel_dict in hierarchical_label_data:
|
|
219
|
+
if isinstance(hlabel_dict, dict):
|
|
220
|
+
# Convert dict to Label object
|
|
221
|
+
position = hlabel_dict.get("position", {"x": 0, "y": 0})
|
|
222
|
+
if isinstance(position, dict):
|
|
223
|
+
pos = Point(position["x"], position["y"])
|
|
224
|
+
elif isinstance(position, (list, tuple)):
|
|
225
|
+
pos = Point(position[0], position[1])
|
|
226
|
+
else:
|
|
227
|
+
pos = position
|
|
228
|
+
|
|
229
|
+
hlabel = Label(
|
|
230
|
+
uuid=hlabel_dict.get("uuid", str(uuid.uuid4())),
|
|
231
|
+
position=pos,
|
|
232
|
+
text=hlabel_dict.get("text", ""),
|
|
233
|
+
label_type=LabelType.HIERARCHICAL,
|
|
234
|
+
rotation=hlabel_dict.get("rotation", 0.0),
|
|
235
|
+
size=hlabel_dict.get("size", 1.27),
|
|
236
|
+
shape=(
|
|
237
|
+
HierarchicalLabelShape(hlabel_dict.get("shape"))
|
|
238
|
+
if hlabel_dict.get("shape")
|
|
239
|
+
else None
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
hierarchical_labels.append(hlabel)
|
|
243
|
+
|
|
244
|
+
self._hierarchical_labels = LabelCollection(hierarchical_labels)
|
|
245
|
+
|
|
246
|
+
# Initialize no-connect collection
|
|
247
|
+
no_connect_data = self._data.get("no_connects", [])
|
|
248
|
+
no_connects = []
|
|
249
|
+
for no_connect_dict in no_connect_data:
|
|
250
|
+
if isinstance(no_connect_dict, dict):
|
|
251
|
+
# Convert dict to NoConnect object
|
|
252
|
+
position = no_connect_dict.get("position", {"x": 0, "y": 0})
|
|
253
|
+
if isinstance(position, dict):
|
|
254
|
+
pos = Point(position["x"], position["y"])
|
|
255
|
+
elif isinstance(position, (list, tuple)):
|
|
256
|
+
pos = Point(position[0], position[1])
|
|
257
|
+
else:
|
|
258
|
+
pos = position
|
|
259
|
+
|
|
260
|
+
no_connect = NoConnect(
|
|
261
|
+
uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
|
|
262
|
+
position=pos,
|
|
263
|
+
)
|
|
264
|
+
no_connects.append(no_connect)
|
|
265
|
+
self._no_connects = NoConnectCollection(no_connects)
|
|
266
|
+
|
|
267
|
+
# Initialize net collection
|
|
268
|
+
net_data = self._data.get("nets", [])
|
|
269
|
+
nets = []
|
|
270
|
+
for net_dict in net_data:
|
|
271
|
+
if isinstance(net_dict, dict):
|
|
272
|
+
# Convert dict to Net object
|
|
273
|
+
net = Net(
|
|
274
|
+
name=net_dict.get("name", ""),
|
|
275
|
+
components=net_dict.get("components", []),
|
|
276
|
+
wires=net_dict.get("wires", []),
|
|
277
|
+
labels=net_dict.get("labels", []),
|
|
278
|
+
)
|
|
279
|
+
nets.append(net)
|
|
280
|
+
self._nets = NetCollection(nets)
|
|
281
|
+
|
|
282
|
+
# Initialize specialized managers
|
|
283
|
+
self._file_io_manager = FileIOManager()
|
|
284
|
+
self._format_sync_manager = FormatSyncManager(self._data)
|
|
285
|
+
self._graphics_manager = GraphicsManager(self._data)
|
|
286
|
+
self._metadata_manager = MetadataManager(self._data)
|
|
287
|
+
self._sheet_manager = SheetManager(self._data)
|
|
288
|
+
self._text_element_manager = TextElementManager(self._data)
|
|
289
|
+
self._wire_manager = WireManager(self._data, self._wires, self._components)
|
|
290
|
+
self._validation_manager = ValidationManager(self._data, self._components, self._wires)
|
|
291
|
+
|
|
139
292
|
# Track modifications for save optimization
|
|
140
293
|
self._modified = False
|
|
141
294
|
self._last_save_time = None
|
|
@@ -145,7 +298,10 @@ class Schematic:
|
|
|
145
298
|
self._total_operation_time = 0.0
|
|
146
299
|
|
|
147
300
|
logger.debug(
|
|
148
|
-
f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires,
|
|
301
|
+
f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, "
|
|
302
|
+
f"{len(self._junctions)} junctions, {len(self._texts)} texts, {len(self._labels)} labels, "
|
|
303
|
+
f"{len(self._hierarchical_labels)} hierarchical labels, {len(self._no_connects)} no-connects, "
|
|
304
|
+
f"and {len(self._nets)} nets with managers initialized"
|
|
149
305
|
)
|
|
150
306
|
|
|
151
307
|
@classmethod
|
|
@@ -168,8 +324,9 @@ class Schematic:
|
|
|
168
324
|
|
|
169
325
|
logger.info(f"Loading schematic: {file_path}")
|
|
170
326
|
|
|
171
|
-
|
|
172
|
-
|
|
327
|
+
# Use FileIOManager for loading
|
|
328
|
+
file_io_manager = FileIOManager()
|
|
329
|
+
schematic_data = file_io_manager.load_schematic(file_path)
|
|
173
330
|
|
|
174
331
|
load_time = time.time() - start_time
|
|
175
332
|
logger.info(f"Loaded schematic in {load_time:.3f}s")
|
|
@@ -212,8 +369,10 @@ class Schematic:
|
|
|
212
369
|
"junctions": [],
|
|
213
370
|
"labels": [],
|
|
214
371
|
"nets": [],
|
|
215
|
-
"lib_symbols":
|
|
372
|
+
"lib_symbols": {}, # Empty dict for blank schematic
|
|
216
373
|
"symbol_instances": [],
|
|
374
|
+
"sheet_instances": [],
|
|
375
|
+
"embedded_fonts": "no",
|
|
217
376
|
}
|
|
218
377
|
else:
|
|
219
378
|
schematic_data = cls._create_empty_schematic_data()
|
|
@@ -276,15 +435,49 @@ class Schematic:
|
|
|
276
435
|
@property
|
|
277
436
|
def modified(self) -> bool:
|
|
278
437
|
"""Whether schematic has been modified since last save."""
|
|
279
|
-
return
|
|
438
|
+
return (
|
|
439
|
+
self._modified
|
|
440
|
+
or self._components._modified
|
|
441
|
+
or self._wires._modified
|
|
442
|
+
or self._junctions._modified
|
|
443
|
+
or self._texts._modified
|
|
444
|
+
or self._labels._modified
|
|
445
|
+
or self._hierarchical_labels._modified
|
|
446
|
+
or self._no_connects._modified
|
|
447
|
+
or self._nets._modified
|
|
448
|
+
or self._format_sync_manager.is_dirty()
|
|
449
|
+
)
|
|
280
450
|
|
|
281
|
-
|
|
451
|
+
@property
|
|
452
|
+
def texts(self) -> TextCollection:
|
|
453
|
+
"""Collection of all text elements in the schematic."""
|
|
454
|
+
return self._texts
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def labels(self) -> LabelCollection:
|
|
458
|
+
"""Collection of all label elements in the schematic."""
|
|
459
|
+
return self._labels
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def hierarchical_labels(self) -> LabelCollection:
|
|
463
|
+
"""Collection of all hierarchical label elements in the schematic."""
|
|
464
|
+
return self._hierarchical_labels
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def no_connects(self) -> NoConnectCollection:
|
|
468
|
+
"""Collection of all no-connect elements in the schematic."""
|
|
469
|
+
return self._no_connects
|
|
470
|
+
|
|
471
|
+
@property
|
|
472
|
+
def nets(self) -> NetCollection:
|
|
473
|
+
"""Collection of all electrical nets in the schematic."""
|
|
474
|
+
return self._nets
|
|
475
|
+
|
|
476
|
+
# Pin positioning methods (delegated to WireManager)
|
|
282
477
|
def get_component_pin_position(self, reference: str, pin_number: str) -> Optional[Point]:
|
|
283
478
|
"""
|
|
284
479
|
Get the absolute position of a component pin.
|
|
285
480
|
|
|
286
|
-
Migrated from circuit-synth with enhanced logging for verification.
|
|
287
|
-
|
|
288
481
|
Args:
|
|
289
482
|
reference: Component reference (e.g., "R1")
|
|
290
483
|
pin_number: Pin number to find (e.g., "1", "2")
|
|
@@ -292,20 +485,7 @@ class Schematic:
|
|
|
292
485
|
Returns:
|
|
293
486
|
Absolute position of the pin, or None if not found
|
|
294
487
|
"""
|
|
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)
|
|
488
|
+
return self._wire_manager.get_component_pin_position(reference, pin_number)
|
|
309
489
|
|
|
310
490
|
def list_component_pins(self, reference: str) -> List[Tuple[str, Point]]:
|
|
311
491
|
"""
|
|
@@ -317,22 +497,9 @@ class Schematic:
|
|
|
317
497
|
Returns:
|
|
318
498
|
List of (pin_number, absolute_position) tuples
|
|
319
499
|
"""
|
|
320
|
-
|
|
500
|
+
return self._wire_manager.list_component_pins(reference)
|
|
321
501
|
|
|
322
|
-
|
|
323
|
-
component = None
|
|
324
|
-
for comp in self._components:
|
|
325
|
-
if comp.reference == reference:
|
|
326
|
-
component = comp
|
|
327
|
-
break
|
|
328
|
-
|
|
329
|
-
if not component:
|
|
330
|
-
logger.warning(f"Component {reference} not found")
|
|
331
|
-
return []
|
|
332
|
-
|
|
333
|
-
return list_component_pins(component)
|
|
334
|
-
|
|
335
|
-
# File operations
|
|
502
|
+
# File operations (delegated to FileIOManager)
|
|
336
503
|
def save(self, file_path: Optional[Union[str, Path]] = None, preserve_format: bool = True):
|
|
337
504
|
"""
|
|
338
505
|
Save schematic to file.
|
|
@@ -361,31 +528,26 @@ class Schematic:
|
|
|
361
528
|
if errors:
|
|
362
529
|
raise ValidationError("Cannot save schematic with validation errors", errors)
|
|
363
530
|
|
|
364
|
-
#
|
|
531
|
+
# Sync collection state back to data structure (critical for save)
|
|
365
532
|
self._sync_components_to_data()
|
|
366
533
|
self._sync_wires_to_data()
|
|
367
534
|
self._sync_junctions_to_data()
|
|
535
|
+
self._sync_texts_to_data()
|
|
536
|
+
self._sync_labels_to_data()
|
|
537
|
+
self._sync_hierarchical_labels_to_data()
|
|
538
|
+
self._sync_no_connects_to_data()
|
|
539
|
+
self._sync_nets_to_data()
|
|
368
540
|
|
|
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)
|
|
541
|
+
# Ensure FileIOManager's parser has the correct project name
|
|
542
|
+
self._file_io_manager._parser.project_name = self.name
|
|
381
543
|
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
f.write(content)
|
|
544
|
+
# Use FileIOManager for saving
|
|
545
|
+
self._file_io_manager.save_schematic(self._data, file_path, preserve_format)
|
|
385
546
|
|
|
386
547
|
# Update state
|
|
387
548
|
self._modified = False
|
|
388
549
|
self._components._modified = False
|
|
550
|
+
self._format_sync_manager.clear_dirty_flags()
|
|
389
551
|
self._last_save_time = time.time()
|
|
390
552
|
|
|
391
553
|
save_time = time.time() - start_time
|
|
@@ -400,121 +562,22 @@ class Schematic:
|
|
|
400
562
|
Create a backup of the current schematic file.
|
|
401
563
|
|
|
402
564
|
Args:
|
|
403
|
-
suffix:
|
|
565
|
+
suffix: Backup file suffix
|
|
404
566
|
|
|
405
567
|
Returns:
|
|
406
568
|
Path to backup file
|
|
407
569
|
"""
|
|
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}")
|
|
418
|
-
|
|
419
|
-
return backup_path
|
|
420
|
-
|
|
421
|
-
# Validation and analysis
|
|
422
|
-
def validate(self) -> List[ValidationIssue]:
|
|
423
|
-
"""
|
|
424
|
-
Validate the schematic for errors and issues.
|
|
425
|
-
|
|
426
|
-
Returns:
|
|
427
|
-
List of validation issues found
|
|
428
|
-
"""
|
|
429
|
-
# Sync current state to data for validation
|
|
430
|
-
self._sync_components_to_data()
|
|
431
|
-
|
|
432
|
-
# Use validator to check schematic
|
|
433
|
-
issues = self._validator.validate_schematic_data(self._data)
|
|
434
|
-
|
|
435
|
-
# Add component-level validation
|
|
436
|
-
component_issues = self._components.validate_all()
|
|
437
|
-
issues.extend(component_issues)
|
|
438
|
-
|
|
439
|
-
return issues
|
|
440
|
-
|
|
441
|
-
# Focused helper functions for specific KiCAD sections
|
|
442
|
-
def add_lib_symbols_section(self, lib_symbols: Dict[str, Any]):
|
|
443
|
-
"""Add or update lib_symbols section with specific symbol definitions."""
|
|
444
|
-
self._data["lib_symbols"] = lib_symbols
|
|
445
|
-
self._modified = True
|
|
446
|
-
|
|
447
|
-
def add_instances_section(self, instances: Dict[str, Any]):
|
|
448
|
-
"""Add instances section for component placement tracking."""
|
|
449
|
-
self._data["instances"] = instances
|
|
450
|
-
self._modified = True
|
|
451
|
-
|
|
452
|
-
def add_sheet_instances_section(self, sheet_instances: List[Dict]):
|
|
453
|
-
"""Add sheet_instances section for hierarchical design."""
|
|
454
|
-
self._data["sheet_instances"] = sheet_instances
|
|
455
|
-
self._modified = True
|
|
456
|
-
|
|
457
|
-
def set_paper_size(self, paper: str):
|
|
458
|
-
"""Set paper size (A4, A3, etc.)."""
|
|
459
|
-
self._data["paper"] = paper
|
|
460
|
-
self._modified = True
|
|
461
|
-
|
|
462
|
-
def set_version_info(
|
|
463
|
-
self, version: str, generator: str = "eeschema", generator_version: str = "9.0"
|
|
464
|
-
):
|
|
465
|
-
"""Set version and generator information."""
|
|
466
|
-
self._data["version"] = version
|
|
467
|
-
self._data["generator"] = generator
|
|
468
|
-
self._data["generator_version"] = generator_version
|
|
469
|
-
self._modified = True
|
|
470
|
-
|
|
471
|
-
def copy_metadata_from(self, source_schematic: "Schematic"):
|
|
472
|
-
"""Copy all metadata from another schematic (version, generator, paper, etc.)."""
|
|
473
|
-
metadata_fields = [
|
|
474
|
-
"version",
|
|
475
|
-
"generator",
|
|
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
|
|
570
|
+
if self._file_path is None:
|
|
571
|
+
raise ValidationError("Cannot backup schematic with no file path")
|
|
485
572
|
|
|
486
|
-
|
|
487
|
-
"""Get summary information about the schematic."""
|
|
488
|
-
component_stats = self._components.get_statistics()
|
|
489
|
-
|
|
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
|
-
}
|
|
573
|
+
return self._file_io_manager.create_backup(self._file_path, suffix)
|
|
511
574
|
|
|
512
|
-
# Wire
|
|
575
|
+
# Wire operations (delegated to WireManager)
|
|
513
576
|
def add_wire(
|
|
514
577
|
self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
|
|
515
578
|
) -> str:
|
|
516
579
|
"""
|
|
517
|
-
Add a wire connection.
|
|
580
|
+
Add a wire connection between two points.
|
|
518
581
|
|
|
519
582
|
Args:
|
|
520
583
|
start: Start point
|
|
@@ -523,824 +586,593 @@ class Schematic:
|
|
|
523
586
|
Returns:
|
|
524
587
|
UUID of created wire
|
|
525
588
|
"""
|
|
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)
|
|
589
|
+
wire_uuid = self._wire_manager.add_wire(start, end)
|
|
590
|
+
self._format_sync_manager.mark_dirty("wire", "add", {"uuid": wire_uuid})
|
|
533
591
|
self._modified = True
|
|
534
|
-
|
|
535
|
-
logger.debug(f"Added wire: {start} -> {end}")
|
|
536
592
|
return wire_uuid
|
|
537
593
|
|
|
538
594
|
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:
|
|
595
|
+
"""
|
|
596
|
+
Remove a wire by UUID.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
wire_uuid: UUID of wire to remove
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
True if wire was removed, False if not found
|
|
603
|
+
"""
|
|
604
|
+
removed = self._wires.remove(wire_uuid)
|
|
605
|
+
if removed:
|
|
606
|
+
self._format_sync_manager.remove_wire_from_data(wire_uuid)
|
|
553
607
|
self._modified = True
|
|
554
|
-
|
|
555
|
-
return True
|
|
556
|
-
return False
|
|
608
|
+
return removed
|
|
557
609
|
|
|
558
|
-
|
|
559
|
-
def add_hierarchical_label(
|
|
610
|
+
def auto_route_pins(
|
|
560
611
|
self,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
) -> str:
|
|
612
|
+
component1_ref: str,
|
|
613
|
+
pin1_number: str,
|
|
614
|
+
component2_ref: str,
|
|
615
|
+
pin2_number: str,
|
|
616
|
+
routing_strategy: str = "direct",
|
|
617
|
+
) -> List[str]:
|
|
567
618
|
"""
|
|
568
|
-
|
|
619
|
+
Auto-route between two component pins.
|
|
569
620
|
|
|
570
621
|
Args:
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
622
|
+
component1_ref: First component reference
|
|
623
|
+
pin1_number: First component pin number
|
|
624
|
+
component2_ref: Second component reference
|
|
625
|
+
pin2_number: Second component pin number
|
|
626
|
+
routing_strategy: Routing strategy ("direct", "orthogonal", "manhattan")
|
|
576
627
|
|
|
577
628
|
Returns:
|
|
578
|
-
|
|
629
|
+
List of wire UUIDs created
|
|
579
630
|
"""
|
|
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
|
-
}
|
|
631
|
+
wire_uuids = self._wire_manager.auto_route_pins(
|
|
632
|
+
component1_ref, pin1_number, component2_ref, pin2_number, routing_strategy
|
|
605
633
|
)
|
|
634
|
+
for wire_uuid in wire_uuids:
|
|
635
|
+
self._format_sync_manager.mark_dirty("wire", "add", {"uuid": wire_uuid})
|
|
606
636
|
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
|
|
637
|
+
return wire_uuids
|
|
621
638
|
|
|
622
639
|
def add_wire_to_pin(
|
|
623
|
-
self,
|
|
640
|
+
self, start: Union[Point, Tuple[float, float]], component_ref: str, pin_number: str
|
|
624
641
|
) -> Optional[str]:
|
|
625
642
|
"""
|
|
626
|
-
|
|
643
|
+
Add wire from arbitrary position to component pin.
|
|
627
644
|
|
|
628
645
|
Args:
|
|
629
|
-
|
|
630
|
-
component_ref:
|
|
631
|
-
pin_number: Pin number
|
|
646
|
+
start: Start position
|
|
647
|
+
component_ref: Component reference
|
|
648
|
+
pin_number: Pin number
|
|
632
649
|
|
|
633
650
|
Returns:
|
|
634
|
-
UUID
|
|
651
|
+
Wire UUID or None if pin not found
|
|
635
652
|
"""
|
|
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}")
|
|
653
|
+
pin_pos = self.get_component_pin_position(component_ref, pin_number)
|
|
654
|
+
if pin_pos is None:
|
|
648
655
|
return None
|
|
649
656
|
|
|
650
|
-
|
|
651
|
-
return self.add_wire(start_point, pin_position)
|
|
657
|
+
return self.add_wire(start, pin_pos)
|
|
652
658
|
|
|
653
659
|
def add_wire_between_pins(
|
|
654
660
|
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
655
661
|
) -> Optional[str]:
|
|
656
662
|
"""
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
Args:
|
|
660
|
-
component1_ref: Reference of the first component (e.g., "R1")
|
|
661
|
-
pin1_number: Pin number on the first component (e.g., "1")
|
|
662
|
-
component2_ref: Reference of the second component (e.g., "R2")
|
|
663
|
-
pin2_number: Pin number on the second component (e.g., "2")
|
|
664
|
-
|
|
665
|
-
Returns:
|
|
666
|
-
UUID of created wire, or None if either pin position cannot be determined
|
|
667
|
-
"""
|
|
668
|
-
from .pin_utils import get_component_pin_position
|
|
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)
|
|
684
|
-
|
|
685
|
-
if not pin1_position:
|
|
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}")
|
|
690
|
-
return None
|
|
691
|
-
|
|
692
|
-
# Create the wire
|
|
693
|
-
return self.add_wire(pin1_position, pin2_position)
|
|
694
|
-
|
|
695
|
-
def get_component_pin_position(self, component_ref: str, pin_number: str) -> Optional[Point]:
|
|
696
|
-
"""
|
|
697
|
-
Get the absolute position of a component pin.
|
|
663
|
+
Add wire between two component pins.
|
|
698
664
|
|
|
699
665
|
Args:
|
|
700
|
-
|
|
701
|
-
|
|
666
|
+
component1_ref: First component reference
|
|
667
|
+
pin1_number: First component pin number
|
|
668
|
+
component2_ref: Second component reference
|
|
669
|
+
pin2_number: Second component pin number
|
|
702
670
|
|
|
703
671
|
Returns:
|
|
704
|
-
|
|
672
|
+
Wire UUID or None if either pin not found
|
|
705
673
|
"""
|
|
706
|
-
|
|
674
|
+
pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
|
|
675
|
+
pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
|
|
707
676
|
|
|
708
|
-
|
|
709
|
-
if not component:
|
|
677
|
+
if pin1_pos is None or pin2_pos is None:
|
|
710
678
|
return None
|
|
711
679
|
|
|
712
|
-
return
|
|
680
|
+
return self.add_wire(pin1_pos, pin2_pos)
|
|
713
681
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
self,
|
|
717
|
-
comp1_ref: str,
|
|
718
|
-
pin1_num: str,
|
|
719
|
-
comp2_ref: str,
|
|
720
|
-
pin2_num: str,
|
|
721
|
-
routing_mode: str = "direct",
|
|
722
|
-
clearance: float = 2.54,
|
|
682
|
+
def connect_pins_with_wire(
|
|
683
|
+
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
723
684
|
) -> Optional[str]:
|
|
724
685
|
"""
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
All positions are snapped to KiCAD's 1.27mm grid for exact electrical connections.
|
|
686
|
+
Connect two component pins with a wire (alias for add_wire_between_pins).
|
|
728
687
|
|
|
729
688
|
Args:
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
routing_mode: Routing strategy:
|
|
735
|
-
- "direct": Direct connection through components (default)
|
|
736
|
-
- "manhattan": Manhattan routing with obstacle avoidance
|
|
737
|
-
clearance: Clearance from obstacles in mm (for manhattan mode)
|
|
689
|
+
component1_ref: First component reference
|
|
690
|
+
pin1_number: First component pin number
|
|
691
|
+
component2_ref: Second component reference
|
|
692
|
+
pin2_number: Second component pin number
|
|
738
693
|
|
|
739
694
|
Returns:
|
|
740
|
-
UUID
|
|
695
|
+
Wire UUID or None if either pin not found
|
|
741
696
|
"""
|
|
742
|
-
|
|
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)
|
|
697
|
+
return self.add_wire_between_pins(component1_ref, pin1_number, component2_ref, pin2_number)
|
|
780
698
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
699
|
+
# Text and label operations (delegated to TextElementManager)
|
|
700
|
+
def add_label(
|
|
701
|
+
self,
|
|
702
|
+
text: str,
|
|
703
|
+
position: Union[Point, Tuple[float, float]],
|
|
704
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
705
|
+
rotation: float = 0,
|
|
706
|
+
size: Optional[float] = None,
|
|
707
|
+
uuid: Optional[str] = None,
|
|
708
|
+
) -> str:
|
|
784
709
|
"""
|
|
785
|
-
|
|
710
|
+
Add a text label to the schematic.
|
|
786
711
|
|
|
787
712
|
Args:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
713
|
+
text: Label text content
|
|
714
|
+
position: Label position
|
|
715
|
+
effects: Text effects (size, font, etc.)
|
|
716
|
+
rotation: Label rotation in degrees (default 0)
|
|
717
|
+
size: Text size override (default from effects)
|
|
718
|
+
uuid: Specific UUID for label (auto-generated if None)
|
|
792
719
|
|
|
793
720
|
Returns:
|
|
794
|
-
|
|
721
|
+
UUID of created label
|
|
795
722
|
"""
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
self
|
|
803
|
-
|
|
804
|
-
"""Legacy alias for add_wire_between_pins."""
|
|
805
|
-
return self.add_wire_between_pins(component1_ref, pin1_number, component2_ref, pin2_number)
|
|
723
|
+
# Use the new labels collection instead of manager
|
|
724
|
+
if size is None:
|
|
725
|
+
size = 1.27 # Default size
|
|
726
|
+
label = self._labels.add(text, position, rotation=rotation, size=size, label_uuid=uuid)
|
|
727
|
+
self._sync_labels_to_data() # Sync immediately
|
|
728
|
+
self._format_sync_manager.mark_dirty("label", "add", {"uuid": label.uuid})
|
|
729
|
+
self._modified = True
|
|
730
|
+
return label.uuid
|
|
806
731
|
|
|
807
|
-
def
|
|
732
|
+
def add_text(
|
|
808
733
|
self,
|
|
809
734
|
text: str,
|
|
810
735
|
position: Union[Point, Tuple[float, float]],
|
|
811
736
|
rotation: float = 0.0,
|
|
812
737
|
size: float = 1.27,
|
|
813
|
-
|
|
738
|
+
exclude_from_sim: bool = False,
|
|
739
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
814
740
|
) -> str:
|
|
815
741
|
"""
|
|
816
|
-
Add
|
|
742
|
+
Add free text annotation to the schematic.
|
|
817
743
|
|
|
818
744
|
Args:
|
|
819
|
-
text:
|
|
820
|
-
position:
|
|
745
|
+
text: Text content
|
|
746
|
+
position: Text position
|
|
821
747
|
rotation: Text rotation in degrees
|
|
822
|
-
size:
|
|
823
|
-
|
|
748
|
+
size: Text size
|
|
749
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
750
|
+
effects: Text effects
|
|
824
751
|
|
|
825
752
|
Returns:
|
|
826
|
-
UUID of created
|
|
753
|
+
UUID of created text
|
|
827
754
|
"""
|
|
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
|
-
}
|
|
755
|
+
# Use the new texts collection instead of manager
|
|
756
|
+
text_elem = self._texts.add(
|
|
757
|
+
text, position, rotation=rotation, size=size, exclude_from_sim=exclude_from_sim
|
|
853
758
|
)
|
|
759
|
+
self._sync_texts_to_data() # Sync immediately
|
|
760
|
+
self._format_sync_manager.mark_dirty("text", "add", {"uuid": text_elem.uuid})
|
|
854
761
|
self._modified = True
|
|
762
|
+
return text_elem.uuid
|
|
855
763
|
|
|
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(
|
|
764
|
+
def add_text_box(
|
|
871
765
|
self,
|
|
872
|
-
|
|
873
|
-
filename: str,
|
|
766
|
+
text: str,
|
|
874
767
|
position: Union[Point, Tuple[float, float]],
|
|
875
768
|
size: Union[Point, Tuple[float, float]],
|
|
876
|
-
|
|
769
|
+
rotation: float = 0.0,
|
|
770
|
+
font_size: float = 1.27,
|
|
771
|
+
margins: Optional[Tuple[float, float, float, float]] = None,
|
|
772
|
+
stroke_width: Optional[float] = None,
|
|
877
773
|
stroke_type: str = "solid",
|
|
774
|
+
fill_type: str = "none",
|
|
775
|
+
justify_horizontal: str = "left",
|
|
776
|
+
justify_vertical: str = "top",
|
|
878
777
|
exclude_from_sim: bool = False,
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
project_name: str = "",
|
|
882
|
-
page_number: str = "2",
|
|
883
|
-
uuid: Optional[str] = None,
|
|
778
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
779
|
+
stroke: Optional[Dict[str, Any]] = None,
|
|
884
780
|
) -> str:
|
|
885
781
|
"""
|
|
886
|
-
Add a
|
|
782
|
+
Add a text box with border to the schematic.
|
|
887
783
|
|
|
888
784
|
Args:
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
785
|
+
text: Text content
|
|
786
|
+
position: Top-left position
|
|
787
|
+
size: Box size (width, height)
|
|
788
|
+
rotation: Text rotation in degrees
|
|
789
|
+
font_size: Text font size
|
|
790
|
+
margins: Box margins (top, bottom, left, right)
|
|
791
|
+
stroke_width: Border stroke width
|
|
792
|
+
stroke_type: Border stroke type (solid, dash, etc.)
|
|
793
|
+
fill_type: Fill type (none, outline, background)
|
|
794
|
+
justify_horizontal: Horizontal justification
|
|
795
|
+
justify_vertical: Vertical justification
|
|
796
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
797
|
+
effects: Text effects (legacy)
|
|
798
|
+
stroke: Border stroke settings (legacy)
|
|
901
799
|
|
|
902
800
|
Returns:
|
|
903
|
-
UUID of created
|
|
801
|
+
UUID of created text box
|
|
904
802
|
"""
|
|
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()),
|
|
803
|
+
text_box_uuid = self._text_element_manager.add_text_box(
|
|
804
|
+
text=text,
|
|
914
805
|
position=position,
|
|
915
806
|
size=size,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
in_bom=in_bom,
|
|
920
|
-
on_board=on_board,
|
|
807
|
+
rotation=rotation,
|
|
808
|
+
font_size=font_size,
|
|
809
|
+
margins=margins,
|
|
921
810
|
stroke_width=stroke_width,
|
|
922
811
|
stroke_type=stroke_type,
|
|
812
|
+
fill_type=fill_type,
|
|
813
|
+
justify_horizontal=justify_horizontal,
|
|
814
|
+
justify_vertical=justify_vertical,
|
|
815
|
+
exclude_from_sim=exclude_from_sim,
|
|
816
|
+
effects=effects,
|
|
817
|
+
stroke=stroke,
|
|
923
818
|
)
|
|
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
|
-
)
|
|
819
|
+
self._format_sync_manager.mark_dirty("text_box", "add", {"uuid": text_box_uuid})
|
|
948
820
|
self._modified = True
|
|
821
|
+
return text_box_uuid
|
|
949
822
|
|
|
950
|
-
|
|
951
|
-
return sheet.uuid
|
|
952
|
-
|
|
953
|
-
def add_sheet_pin(
|
|
823
|
+
def add_hierarchical_label(
|
|
954
824
|
self,
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
rotation: float = 0,
|
|
825
|
+
text: str,
|
|
826
|
+
position: Union[Point, Tuple[float, float]],
|
|
827
|
+
shape: str = "input",
|
|
828
|
+
rotation: float = 0.0,
|
|
960
829
|
size: float = 1.27,
|
|
961
|
-
|
|
962
|
-
uuid: Optional[str] = None,
|
|
830
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
963
831
|
) -> str:
|
|
964
832
|
"""
|
|
965
|
-
Add a
|
|
833
|
+
Add a hierarchical label for sheet connections.
|
|
966
834
|
|
|
967
835
|
Args:
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
justify: Text justification (left, right, center)
|
|
975
|
-
uuid: Optional UUID (auto-generated if None)
|
|
836
|
+
text: Label text
|
|
837
|
+
position: Label position
|
|
838
|
+
shape: Shape type (input, output, bidirectional, tri_state, passive)
|
|
839
|
+
rotation: Label rotation in degrees (default 0)
|
|
840
|
+
size: Label text size (default 1.27)
|
|
841
|
+
effects: Text effects
|
|
976
842
|
|
|
977
843
|
Returns:
|
|
978
|
-
UUID of created
|
|
844
|
+
UUID of created hierarchical label
|
|
979
845
|
"""
|
|
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")
|
|
846
|
+
# Use the hierarchical_labels collection
|
|
847
|
+
hlabel = self._hierarchical_labels.add(text, position, rotation=rotation, size=size)
|
|
848
|
+
self._sync_hierarchical_labels_to_data() # Sync immediately
|
|
849
|
+
self._format_sync_manager.mark_dirty("hierarchical_label", "add", {"uuid": hlabel.uuid})
|
|
850
|
+
self._modified = True
|
|
851
|
+
return hlabel.uuid
|
|
1008
852
|
|
|
1009
|
-
def
|
|
853
|
+
def add_global_label(
|
|
1010
854
|
self,
|
|
1011
855
|
text: str,
|
|
1012
856
|
position: Union[Point, Tuple[float, float]],
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
exclude_from_sim: bool = False,
|
|
857
|
+
shape: str = "input",
|
|
858
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
1016
859
|
) -> str:
|
|
1017
860
|
"""
|
|
1018
|
-
Add a
|
|
861
|
+
Add a global label for project-wide connections.
|
|
1019
862
|
|
|
1020
863
|
Args:
|
|
1021
|
-
text:
|
|
1022
|
-
position:
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
exclude_from_sim: Exclude from simulation
|
|
864
|
+
text: Label text
|
|
865
|
+
position: Label position
|
|
866
|
+
shape: Shape type
|
|
867
|
+
effects: Text effects
|
|
1026
868
|
|
|
1027
869
|
Returns:
|
|
1028
|
-
UUID of created
|
|
870
|
+
UUID of created global label
|
|
1029
871
|
"""
|
|
1030
|
-
|
|
1031
|
-
|
|
872
|
+
label_uuid = self._text_element_manager.add_global_label(text, position, shape, effects)
|
|
873
|
+
self._format_sync_manager.mark_dirty("global_label", "add", {"uuid": label_uuid})
|
|
874
|
+
self._modified = True
|
|
875
|
+
return label_uuid
|
|
1032
876
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
text=text,
|
|
1037
|
-
rotation=rotation,
|
|
1038
|
-
size=size,
|
|
1039
|
-
exclude_from_sim=exclude_from_sim,
|
|
1040
|
-
)
|
|
877
|
+
def remove_label(self, label_uuid: str) -> bool:
|
|
878
|
+
"""
|
|
879
|
+
Remove a label by UUID.
|
|
1041
880
|
|
|
1042
|
-
|
|
1043
|
-
|
|
881
|
+
Args:
|
|
882
|
+
label_uuid: UUID of label to remove
|
|
1044
883
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
)
|
|
1055
|
-
self._modified = True
|
|
884
|
+
Returns:
|
|
885
|
+
True if label was removed, False if not found
|
|
886
|
+
"""
|
|
887
|
+
removed = self._labels.remove(label_uuid)
|
|
888
|
+
if removed:
|
|
889
|
+
self._sync_labels_to_data() # Sync immediately
|
|
890
|
+
self._format_sync_manager.mark_dirty("label", "remove", {"uuid": label_uuid})
|
|
891
|
+
self._modified = True
|
|
892
|
+
return removed
|
|
1056
893
|
|
|
1057
|
-
|
|
1058
|
-
|
|
894
|
+
def remove_hierarchical_label(self, label_uuid: str) -> bool:
|
|
895
|
+
"""
|
|
896
|
+
Remove a hierarchical label by UUID.
|
|
1059
897
|
|
|
1060
|
-
|
|
898
|
+
Args:
|
|
899
|
+
label_uuid: UUID of hierarchical label to remove
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
True if hierarchical label was removed, False if not found
|
|
903
|
+
"""
|
|
904
|
+
removed = self._hierarchical_labels.remove(label_uuid)
|
|
905
|
+
if removed:
|
|
906
|
+
self._sync_hierarchical_labels_to_data() # Sync immediately
|
|
907
|
+
self._format_sync_manager.mark_dirty(
|
|
908
|
+
"hierarchical_label", "remove", {"uuid": label_uuid}
|
|
909
|
+
)
|
|
910
|
+
self._modified = True
|
|
911
|
+
return removed
|
|
912
|
+
|
|
913
|
+
# Sheet operations (delegated to SheetManager)
|
|
914
|
+
def add_sheet(
|
|
1061
915
|
self,
|
|
1062
|
-
|
|
916
|
+
name: str,
|
|
917
|
+
filename: str,
|
|
1063
918
|
position: Union[Point, Tuple[float, float]],
|
|
1064
919
|
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,
|
|
920
|
+
stroke_width: Optional[float] = None,
|
|
1069
921
|
stroke_type: str = "solid",
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
exclude_from_sim: bool = False,
|
|
922
|
+
project_name: Optional[str] = None,
|
|
923
|
+
page_number: Optional[str] = None,
|
|
924
|
+
uuid: Optional[str] = None,
|
|
1074
925
|
) -> str:
|
|
1075
926
|
"""
|
|
1076
|
-
Add a
|
|
927
|
+
Add a hierarchical sheet to the schematic.
|
|
1077
928
|
|
|
1078
929
|
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
|
|
930
|
+
name: Sheet name/title
|
|
931
|
+
filename: Referenced schematic filename
|
|
932
|
+
position: Sheet position (top-left corner)
|
|
933
|
+
size: Sheet size (width, height)
|
|
934
|
+
stroke_width: Border stroke width
|
|
935
|
+
stroke_type: Border stroke type (solid, dashed, etc.)
|
|
936
|
+
project_name: Project name for this sheet
|
|
937
|
+
page_number: Page number for this sheet
|
|
938
|
+
uuid: Optional UUID for the sheet
|
|
1091
939
|
|
|
1092
940
|
Returns:
|
|
1093
|
-
UUID of created
|
|
941
|
+
UUID of created sheet
|
|
1094
942
|
"""
|
|
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,
|
|
943
|
+
sheet_uuid = self._sheet_manager.add_sheet(
|
|
944
|
+
name,
|
|
945
|
+
filename,
|
|
946
|
+
position,
|
|
947
|
+
size,
|
|
948
|
+
uuid_str=uuid,
|
|
1108
949
|
stroke_width=stroke_width,
|
|
1109
950
|
stroke_type=stroke_type,
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
justify_vertical=justify_vertical,
|
|
1113
|
-
exclude_from_sim=exclude_from_sim,
|
|
1114
|
-
)
|
|
1115
|
-
|
|
1116
|
-
if "text_boxes" not in self._data:
|
|
1117
|
-
self._data["text_boxes"] = []
|
|
1118
|
-
|
|
1119
|
-
self._data["text_boxes"].append(
|
|
1120
|
-
{
|
|
1121
|
-
"uuid": text_box.uuid,
|
|
1122
|
-
"position": {"x": text_box.position.x, "y": text_box.position.y},
|
|
1123
|
-
"size": {"width": text_box.size.x, "height": text_box.size.y},
|
|
1124
|
-
"text": text_box.text,
|
|
1125
|
-
"rotation": text_box.rotation,
|
|
1126
|
-
"font_size": text_box.font_size,
|
|
1127
|
-
"margins": text_box.margins,
|
|
1128
|
-
"stroke_width": text_box.stroke_width,
|
|
1129
|
-
"stroke_type": text_box.stroke_type,
|
|
1130
|
-
"fill_type": text_box.fill_type,
|
|
1131
|
-
"justify_horizontal": text_box.justify_horizontal,
|
|
1132
|
-
"justify_vertical": text_box.justify_vertical,
|
|
1133
|
-
"exclude_from_sim": text_box.exclude_from_sim,
|
|
1134
|
-
}
|
|
951
|
+
project_name=project_name,
|
|
952
|
+
page_number=page_number,
|
|
1135
953
|
)
|
|
954
|
+
self._format_sync_manager.mark_dirty("sheet", "add", {"uuid": sheet_uuid})
|
|
1136
955
|
self._modified = True
|
|
956
|
+
return sheet_uuid
|
|
1137
957
|
|
|
1138
|
-
|
|
1139
|
-
return text_box.uuid
|
|
1140
|
-
|
|
1141
|
-
def add_image(
|
|
958
|
+
def add_sheet_pin(
|
|
1142
959
|
self,
|
|
960
|
+
sheet_uuid: str,
|
|
961
|
+
name: str,
|
|
962
|
+
pin_type: str,
|
|
1143
963
|
position: Union[Point, Tuple[float, float]],
|
|
1144
|
-
|
|
1145
|
-
|
|
964
|
+
rotation: float = 0,
|
|
965
|
+
justify: str = "left",
|
|
1146
966
|
uuid: Optional[str] = None,
|
|
1147
967
|
) -> str:
|
|
1148
968
|
"""
|
|
1149
|
-
Add
|
|
969
|
+
Add a pin to a hierarchical sheet.
|
|
1150
970
|
|
|
1151
971
|
Args:
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
972
|
+
sheet_uuid: UUID of the sheet to add pin to
|
|
973
|
+
name: Pin name
|
|
974
|
+
pin_type: Pin type (input, output, bidirectional, etc.)
|
|
975
|
+
position: Pin position
|
|
976
|
+
rotation: Pin rotation in degrees
|
|
977
|
+
justify: Text justification
|
|
978
|
+
uuid: Optional UUID for the pin
|
|
1156
979
|
|
|
1157
980
|
Returns:
|
|
1158
|
-
UUID of created
|
|
981
|
+
UUID of created sheet pin
|
|
1159
982
|
"""
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
from .types import Image
|
|
1164
|
-
|
|
1165
|
-
import uuid as uuid_module
|
|
1166
|
-
|
|
1167
|
-
image = Image(
|
|
1168
|
-
uuid=uuid if uuid else str(uuid_module.uuid4()),
|
|
1169
|
-
position=position,
|
|
1170
|
-
data=data,
|
|
1171
|
-
scale=scale,
|
|
983
|
+
pin_uuid = self._sheet_manager.add_sheet_pin(
|
|
984
|
+
sheet_uuid, name, pin_type, position, rotation, justify, uuid_str=uuid
|
|
1172
985
|
)
|
|
986
|
+
self._format_sync_manager.mark_dirty("sheet", "modify", {"uuid": sheet_uuid})
|
|
987
|
+
self._modified = True
|
|
988
|
+
return pin_uuid
|
|
1173
989
|
|
|
1174
|
-
|
|
1175
|
-
|
|
990
|
+
def remove_sheet(self, sheet_uuid: str) -> bool:
|
|
991
|
+
"""
|
|
992
|
+
Remove a sheet by UUID.
|
|
1176
993
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
"uuid": image.uuid,
|
|
1180
|
-
"position": {"x": image.position.x, "y": image.position.y},
|
|
1181
|
-
"data": image.data,
|
|
1182
|
-
"scale": image.scale,
|
|
1183
|
-
}
|
|
1184
|
-
)
|
|
1185
|
-
self._modified = True
|
|
994
|
+
Args:
|
|
995
|
+
sheet_uuid: UUID of sheet to remove
|
|
1186
996
|
|
|
1187
|
-
|
|
1188
|
-
|
|
997
|
+
Returns:
|
|
998
|
+
True if sheet was removed, False if not found
|
|
999
|
+
"""
|
|
1000
|
+
removed = self._sheet_manager.remove_sheet(sheet_uuid)
|
|
1001
|
+
if removed:
|
|
1002
|
+
self._format_sync_manager.mark_dirty("sheet", "remove", {"uuid": sheet_uuid})
|
|
1003
|
+
self._modified = True
|
|
1004
|
+
return removed
|
|
1189
1005
|
|
|
1006
|
+
# Graphics operations (delegated to GraphicsManager)
|
|
1190
1007
|
def add_rectangle(
|
|
1191
1008
|
self,
|
|
1192
1009
|
start: Union[Point, Tuple[float, float]],
|
|
1193
1010
|
end: Union[Point, Tuple[float, float]],
|
|
1194
|
-
stroke_width: float = 0.
|
|
1195
|
-
stroke_type: str = "
|
|
1196
|
-
fill_type: str = "none"
|
|
1011
|
+
stroke_width: float = 0.127,
|
|
1012
|
+
stroke_type: str = "solid",
|
|
1013
|
+
fill_type: str = "none",
|
|
1014
|
+
stroke_color: Optional[Tuple[int, int, int, float]] = None,
|
|
1015
|
+
fill_color: Optional[Tuple[int, int, int, float]] = None,
|
|
1197
1016
|
) -> str:
|
|
1198
1017
|
"""
|
|
1199
|
-
Add a
|
|
1018
|
+
Add a rectangle to the schematic.
|
|
1200
1019
|
|
|
1201
1020
|
Args:
|
|
1202
|
-
start:
|
|
1203
|
-
end:
|
|
1204
|
-
stroke_width:
|
|
1205
|
-
stroke_type:
|
|
1206
|
-
fill_type: Fill type (none,
|
|
1021
|
+
start: Top-left corner position
|
|
1022
|
+
end: Bottom-right corner position
|
|
1023
|
+
stroke_width: Line width
|
|
1024
|
+
stroke_type: Line type (solid, dashed, etc.)
|
|
1025
|
+
fill_type: Fill type (none, background, etc.)
|
|
1026
|
+
stroke_color: Stroke color as (r, g, b, a)
|
|
1027
|
+
fill_color: Fill color as (r, g, b, a)
|
|
1207
1028
|
|
|
1208
1029
|
Returns:
|
|
1209
|
-
UUID of created rectangle
|
|
1030
|
+
UUID of created rectangle
|
|
1210
1031
|
"""
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
if
|
|
1214
|
-
|
|
1032
|
+
# Convert individual parameters to stroke/fill dicts
|
|
1033
|
+
stroke = {"width": stroke_width, "type": stroke_type}
|
|
1034
|
+
if stroke_color:
|
|
1035
|
+
stroke["color"] = stroke_color
|
|
1215
1036
|
|
|
1216
|
-
|
|
1037
|
+
fill = {"type": fill_type}
|
|
1038
|
+
if fill_color:
|
|
1039
|
+
fill["color"] = fill_color
|
|
1217
1040
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
start=start,
|
|
1221
|
-
end=end,
|
|
1222
|
-
stroke_width=stroke_width,
|
|
1223
|
-
stroke_type=stroke_type,
|
|
1224
|
-
fill_type=fill_type
|
|
1225
|
-
)
|
|
1226
|
-
|
|
1227
|
-
if "rectangles" not in self._data:
|
|
1228
|
-
self._data["rectangles"] = []
|
|
1229
|
-
|
|
1230
|
-
self._data["rectangles"].append({
|
|
1231
|
-
"uuid": rectangle.uuid,
|
|
1232
|
-
"start": {"x": rectangle.start.x, "y": rectangle.start.y},
|
|
1233
|
-
"end": {"x": rectangle.end.x, "y": rectangle.end.y},
|
|
1234
|
-
"stroke_width": rectangle.stroke_width,
|
|
1235
|
-
"stroke_type": rectangle.stroke_type,
|
|
1236
|
-
"fill_type": rectangle.fill_type
|
|
1237
|
-
})
|
|
1041
|
+
rect_uuid = self._graphics_manager.add_rectangle(start, end, stroke, fill)
|
|
1042
|
+
self._format_sync_manager.mark_dirty("rectangle", "add", {"uuid": rect_uuid})
|
|
1238
1043
|
self._modified = True
|
|
1044
|
+
return rect_uuid
|
|
1239
1045
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1046
|
+
def remove_rectangle(self, rect_uuid: str) -> bool:
|
|
1047
|
+
"""
|
|
1048
|
+
Remove a rectangle by UUID.
|
|
1242
1049
|
|
|
1243
|
-
|
|
1050
|
+
Args:
|
|
1051
|
+
rect_uuid: UUID of rectangle to remove
|
|
1052
|
+
|
|
1053
|
+
Returns:
|
|
1054
|
+
True if removed, False if not found
|
|
1055
|
+
"""
|
|
1056
|
+
removed = self._graphics_manager.remove_rectangle(rect_uuid)
|
|
1057
|
+
if removed:
|
|
1058
|
+
self._format_sync_manager.mark_dirty("rectangle", "remove", {"uuid": rect_uuid})
|
|
1059
|
+
self._modified = True
|
|
1060
|
+
return removed
|
|
1061
|
+
|
|
1062
|
+
def add_image(
|
|
1244
1063
|
self,
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
comments: Optional[Dict[int, str]] = None,
|
|
1250
|
-
):
|
|
1064
|
+
position: Union[Point, Tuple[float, float]],
|
|
1065
|
+
scale: float = 1.0,
|
|
1066
|
+
data: Optional[str] = None,
|
|
1067
|
+
) -> str:
|
|
1251
1068
|
"""
|
|
1252
|
-
|
|
1069
|
+
Add an image to the schematic.
|
|
1253
1070
|
|
|
1254
1071
|
Args:
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
company: Company name
|
|
1259
|
-
comments: Numbered comments (1, 2, 3, etc.)
|
|
1260
|
-
"""
|
|
1261
|
-
if comments is None:
|
|
1262
|
-
comments = {}
|
|
1072
|
+
position: Image position
|
|
1073
|
+
scale: Image scale factor
|
|
1074
|
+
data: Base64 encoded image data
|
|
1263
1075
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
"comments": comments,
|
|
1270
|
-
}
|
|
1076
|
+
Returns:
|
|
1077
|
+
UUID of created image
|
|
1078
|
+
"""
|
|
1079
|
+
image_uuid = self._graphics_manager.add_image(position, scale, data)
|
|
1080
|
+
self._format_sync_manager.mark_dirty("image", "add", {"uuid": image_uuid})
|
|
1271
1081
|
self._modified = True
|
|
1082
|
+
return image_uuid
|
|
1272
1083
|
|
|
1273
|
-
|
|
1084
|
+
def draw_bounding_box(
|
|
1085
|
+
self,
|
|
1086
|
+
bbox,
|
|
1087
|
+
stroke_width: float = 0.127,
|
|
1088
|
+
stroke_color: str = "black",
|
|
1089
|
+
stroke_type: str = "solid",
|
|
1090
|
+
) -> str:
|
|
1091
|
+
"""
|
|
1092
|
+
Draw a bounding box rectangle around the given bounding box.
|
|
1093
|
+
|
|
1094
|
+
Args:
|
|
1095
|
+
bbox: BoundingBox object with min_x, min_y, max_x, max_y
|
|
1096
|
+
stroke_width: Line width
|
|
1097
|
+
stroke_color: Line color
|
|
1098
|
+
stroke_type: Line type
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
UUID of created rectangle
|
|
1102
|
+
"""
|
|
1103
|
+
# Convert bounding box to rectangle coordinates
|
|
1104
|
+
start = (bbox.min_x, bbox.min_y)
|
|
1105
|
+
end = (bbox.max_x, bbox.max_y)
|
|
1106
|
+
|
|
1107
|
+
return self.add_rectangle(start, end, stroke_width=stroke_width, stroke_type=stroke_type)
|
|
1274
1108
|
|
|
1275
1109
|
def draw_bounding_box(
|
|
1276
1110
|
self,
|
|
1277
1111
|
bbox: "BoundingBox",
|
|
1278
|
-
stroke_width: float = 0,
|
|
1279
|
-
stroke_color: str = None,
|
|
1280
|
-
stroke_type: str = "
|
|
1281
|
-
exclude_from_sim: bool = False,
|
|
1112
|
+
stroke_width: float = 0.127,
|
|
1113
|
+
stroke_color: Optional[str] = None,
|
|
1114
|
+
stroke_type: str = "solid",
|
|
1282
1115
|
) -> str:
|
|
1283
1116
|
"""
|
|
1284
|
-
Draw a
|
|
1117
|
+
Draw a single bounding box as a rectangle.
|
|
1285
1118
|
|
|
1286
1119
|
Args:
|
|
1287
1120
|
bbox: BoundingBox to draw
|
|
1288
|
-
stroke_width: Line width
|
|
1289
|
-
stroke_color:
|
|
1290
|
-
stroke_type:
|
|
1291
|
-
exclude_from_sim: Exclude from simulation
|
|
1121
|
+
stroke_width: Line width
|
|
1122
|
+
stroke_color: Line color name (red, green, blue, etc.) or None
|
|
1123
|
+
stroke_type: Line type (solid, dashed, etc.)
|
|
1292
1124
|
|
|
1293
1125
|
Returns:
|
|
1294
|
-
UUID of created rectangle
|
|
1126
|
+
UUID of created rectangle
|
|
1295
1127
|
"""
|
|
1296
|
-
# Import BoundingBox type
|
|
1297
1128
|
from .component_bounds import BoundingBox
|
|
1298
1129
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
# Create rectangle data structure in KiCAD dictionary format
|
|
1302
|
-
stroke_data = {"width": stroke_width, "type": stroke_type}
|
|
1303
|
-
|
|
1304
|
-
# Add color if specified
|
|
1130
|
+
# Convert color name to RGBA tuple if provided
|
|
1131
|
+
stroke_rgba = None
|
|
1305
1132
|
if stroke_color:
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
self._data["graphics"] = []
|
|
1133
|
+
# Simple color name to RGB mapping
|
|
1134
|
+
color_map = {
|
|
1135
|
+
"red": (255, 0, 0, 1.0),
|
|
1136
|
+
"green": (0, 255, 0, 1.0),
|
|
1137
|
+
"blue": (0, 0, 255, 1.0),
|
|
1138
|
+
"yellow": (255, 255, 0, 1.0),
|
|
1139
|
+
"cyan": (0, 255, 255, 1.0),
|
|
1140
|
+
"magenta": (255, 0, 255, 1.0),
|
|
1141
|
+
"black": (0, 0, 0, 1.0),
|
|
1142
|
+
"white": (255, 255, 255, 1.0),
|
|
1143
|
+
}
|
|
1144
|
+
stroke_rgba = color_map.get(stroke_color.lower(), (0, 255, 0, 1.0))
|
|
1319
1145
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1146
|
+
# Add rectangle using the manager
|
|
1147
|
+
rect_uuid = self.add_rectangle(
|
|
1148
|
+
start=(bbox.min_x, bbox.min_y),
|
|
1149
|
+
end=(bbox.max_x, bbox.max_y),
|
|
1150
|
+
stroke_width=stroke_width,
|
|
1151
|
+
stroke_type=stroke_type,
|
|
1152
|
+
stroke_color=stroke_rgba,
|
|
1153
|
+
)
|
|
1322
1154
|
|
|
1323
|
-
logger.debug(f"Drew bounding box
|
|
1155
|
+
logger.debug(f"Drew bounding box: {bbox}")
|
|
1324
1156
|
return rect_uuid
|
|
1325
1157
|
|
|
1326
1158
|
def draw_component_bounding_boxes(
|
|
1327
1159
|
self,
|
|
1328
1160
|
include_properties: bool = False,
|
|
1329
|
-
stroke_width: float = 0.
|
|
1330
|
-
stroke_color: str = "
|
|
1331
|
-
stroke_type: str = "
|
|
1161
|
+
stroke_width: float = 0.127,
|
|
1162
|
+
stroke_color: str = "green",
|
|
1163
|
+
stroke_type: str = "solid",
|
|
1332
1164
|
) -> List[str]:
|
|
1333
1165
|
"""
|
|
1334
|
-
Draw bounding boxes for all components
|
|
1166
|
+
Draw bounding boxes for all components.
|
|
1335
1167
|
|
|
1336
1168
|
Args:
|
|
1337
|
-
include_properties:
|
|
1338
|
-
stroke_width: Line width
|
|
1339
|
-
stroke_color:
|
|
1340
|
-
stroke_type:
|
|
1169
|
+
include_properties: Whether to include properties in bounding box
|
|
1170
|
+
stroke_width: Line width
|
|
1171
|
+
stroke_color: Line color
|
|
1172
|
+
stroke_type: Line type
|
|
1341
1173
|
|
|
1342
1174
|
Returns:
|
|
1343
|
-
List of UUIDs
|
|
1175
|
+
List of rectangle UUIDs created
|
|
1344
1176
|
"""
|
|
1345
1177
|
from .component_bounds import get_component_bounding_box
|
|
1346
1178
|
|
|
@@ -1354,67 +1186,157 @@ class Schematic:
|
|
|
1354
1186
|
logger.info(f"Drew {len(uuids)} component bounding boxes")
|
|
1355
1187
|
return uuids
|
|
1356
1188
|
|
|
1357
|
-
#
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1189
|
+
# Metadata operations (delegated to MetadataManager)
|
|
1190
|
+
def set_title_block(
|
|
1191
|
+
self,
|
|
1192
|
+
title: str = "",
|
|
1193
|
+
date: str = "",
|
|
1194
|
+
rev: str = "",
|
|
1195
|
+
company: str = "",
|
|
1196
|
+
comments: Optional[Dict[int, str]] = None,
|
|
1197
|
+
) -> None:
|
|
1198
|
+
"""
|
|
1199
|
+
Set title block information.
|
|
1200
|
+
|
|
1201
|
+
Args:
|
|
1202
|
+
title: Schematic title
|
|
1203
|
+
date: Date
|
|
1204
|
+
rev: Revision
|
|
1205
|
+
company: Company name
|
|
1206
|
+
comments: Comment fields (1-9)
|
|
1207
|
+
"""
|
|
1208
|
+
self._metadata_manager.set_title_block(title, date, rev, company, comments)
|
|
1209
|
+
self._format_sync_manager.mark_dirty("title_block", "update")
|
|
1375
1210
|
self._modified = True
|
|
1376
|
-
logger.info("Cleared schematic")
|
|
1377
1211
|
|
|
1378
|
-
def
|
|
1379
|
-
"""
|
|
1380
|
-
|
|
1212
|
+
def set_paper_size(self, paper: str) -> None:
|
|
1213
|
+
"""
|
|
1214
|
+
Set paper size for the schematic.
|
|
1381
1215
|
|
|
1382
|
-
|
|
1216
|
+
Args:
|
|
1217
|
+
paper: Paper size (A4, A3, etc.)
|
|
1218
|
+
"""
|
|
1219
|
+
self._metadata_manager.set_paper_size(paper)
|
|
1220
|
+
self._format_sync_manager.mark_dirty("paper", "update")
|
|
1221
|
+
self._modified = True
|
|
1383
1222
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1223
|
+
# Validation (enhanced with ValidationManager)
|
|
1224
|
+
def validate(self) -> List[ValidationIssue]:
|
|
1225
|
+
"""
|
|
1226
|
+
Perform comprehensive schematic validation.
|
|
1387
1227
|
|
|
1388
|
-
|
|
1228
|
+
Returns:
|
|
1229
|
+
List of validation issues found
|
|
1230
|
+
"""
|
|
1231
|
+
# Use the new ValidationManager for comprehensive validation
|
|
1232
|
+
manager_issues = self._validation_manager.validate_schematic()
|
|
1233
|
+
|
|
1234
|
+
# Also run legacy validator for compatibility
|
|
1235
|
+
try:
|
|
1236
|
+
legacy_issues = self._legacy_validator.validate_schematic_data(self._data)
|
|
1237
|
+
except Exception as e:
|
|
1238
|
+
logger.warning(f"Legacy validator failed: {e}")
|
|
1239
|
+
legacy_issues = []
|
|
1240
|
+
|
|
1241
|
+
# Combine issues (remove duplicates based on message)
|
|
1242
|
+
all_issues = manager_issues + legacy_issues
|
|
1243
|
+
unique_issues = []
|
|
1244
|
+
seen_messages = set()
|
|
1389
1245
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
logger.info("Rebuilt schematic indexes")
|
|
1246
|
+
for issue in all_issues:
|
|
1247
|
+
if issue.message not in seen_messages:
|
|
1248
|
+
unique_issues.append(issue)
|
|
1249
|
+
seen_messages.add(issue.message)
|
|
1395
1250
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1251
|
+
return unique_issues
|
|
1252
|
+
|
|
1253
|
+
def get_validation_summary(self) -> Dict[str, Any]:
|
|
1254
|
+
"""
|
|
1255
|
+
Get validation summary statistics.
|
|
1256
|
+
|
|
1257
|
+
Returns:
|
|
1258
|
+
Summary dictionary with counts and severity
|
|
1259
|
+
"""
|
|
1260
|
+
issues = self.validate()
|
|
1261
|
+
return self._validation_manager.get_validation_summary(issues)
|
|
1399
1262
|
|
|
1263
|
+
# Statistics and information
|
|
1264
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
1265
|
+
"""Get comprehensive schematic statistics."""
|
|
1400
1266
|
return {
|
|
1401
|
-
"
|
|
1267
|
+
"components": len(self._components),
|
|
1268
|
+
"wires": len(self._wires),
|
|
1269
|
+
"junctions": len(self._junctions),
|
|
1270
|
+
"text_elements": self._text_element_manager.get_text_statistics(),
|
|
1271
|
+
"graphics": self._graphics_manager.get_graphics_statistics(),
|
|
1272
|
+
"sheets": self._sheet_manager.get_sheet_statistics(),
|
|
1273
|
+
"performance": {
|
|
1402
1274
|
"operation_count": self._operation_count,
|
|
1403
|
-
"
|
|
1404
|
-
"
|
|
1405
|
-
|
|
1406
|
-
(self._total_operation_time / self._operation_count * 1000)
|
|
1407
|
-
if self._operation_count > 0
|
|
1408
|
-
else 0
|
|
1409
|
-
),
|
|
1410
|
-
2,
|
|
1411
|
-
),
|
|
1275
|
+
"total_operation_time": self._total_operation_time,
|
|
1276
|
+
"modified": self.modified,
|
|
1277
|
+
"last_save_time": self._last_save_time,
|
|
1412
1278
|
},
|
|
1413
|
-
"components": self._components.get_statistics(),
|
|
1414
|
-
"symbol_cache": cache_stats,
|
|
1415
1279
|
}
|
|
1416
1280
|
|
|
1417
1281
|
# Internal methods
|
|
1282
|
+
@staticmethod
|
|
1283
|
+
def _create_empty_schematic_data() -> Dict[str, Any]:
|
|
1284
|
+
"""Create empty schematic data structure."""
|
|
1285
|
+
return {
|
|
1286
|
+
"version": "20250114",
|
|
1287
|
+
"generator": "eeschema",
|
|
1288
|
+
"generator_version": "9.0",
|
|
1289
|
+
"paper": "A4",
|
|
1290
|
+
"lib_symbols": {},
|
|
1291
|
+
"symbol": [],
|
|
1292
|
+
"wire": [],
|
|
1293
|
+
"junction": [],
|
|
1294
|
+
"label": [],
|
|
1295
|
+
"hierarchical_label": [],
|
|
1296
|
+
"global_label": [],
|
|
1297
|
+
"text": [],
|
|
1298
|
+
"sheet": [],
|
|
1299
|
+
"rectangle": [],
|
|
1300
|
+
"circle": [],
|
|
1301
|
+
"arc": [],
|
|
1302
|
+
"polyline": [],
|
|
1303
|
+
"image": [],
|
|
1304
|
+
"symbol_instances": [],
|
|
1305
|
+
"sheet_instances": [],
|
|
1306
|
+
"embedded_fonts": "no",
|
|
1307
|
+
"components": [],
|
|
1308
|
+
"wires": [],
|
|
1309
|
+
"junctions": [],
|
|
1310
|
+
"labels": [],
|
|
1311
|
+
"nets": [],
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
# Context manager support for atomic operations
|
|
1315
|
+
def __enter__(self):
|
|
1316
|
+
"""Enter atomic operation context."""
|
|
1317
|
+
# Create backup for rollback
|
|
1318
|
+
if self._file_path and self._file_path.exists():
|
|
1319
|
+
self._backup_path = self._file_io_manager.create_backup(
|
|
1320
|
+
self._file_path, ".atomic_backup"
|
|
1321
|
+
)
|
|
1322
|
+
return self
|
|
1323
|
+
|
|
1324
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1325
|
+
"""Exit atomic operation context."""
|
|
1326
|
+
if exc_type is not None:
|
|
1327
|
+
# Exception occurred - rollback if possible
|
|
1328
|
+
if hasattr(self, "_backup_path") and self._backup_path.exists():
|
|
1329
|
+
logger.warning("Exception in atomic operation - rolling back")
|
|
1330
|
+
# Restore from backup
|
|
1331
|
+
restored_data = self._file_io_manager.load_schematic(self._backup_path)
|
|
1332
|
+
self._data = restored_data
|
|
1333
|
+
self._modified = True
|
|
1334
|
+
else:
|
|
1335
|
+
# Success - clean up backup
|
|
1336
|
+
if hasattr(self, "_backup_path") and self._backup_path.exists():
|
|
1337
|
+
self._backup_path.unlink()
|
|
1338
|
+
|
|
1339
|
+
# Internal sync methods (migrated from original implementation)
|
|
1418
1340
|
def _sync_components_to_data(self):
|
|
1419
1341
|
"""Sync component collection state back to data structure."""
|
|
1420
1342
|
self._data["components"] = [comp._data.__dict__ for comp in self._components]
|
|
@@ -1425,112 +1347,23 @@ class Schematic:
|
|
|
1425
1347
|
|
|
1426
1348
|
for comp in self._components:
|
|
1427
1349
|
if comp.lib_id and comp.lib_id not in lib_symbols:
|
|
1428
|
-
logger.debug(f"🔧 SCHEMATIC: Processing component {comp.lib_id}")
|
|
1429
|
-
|
|
1430
1350
|
# Get the actual symbol definition
|
|
1431
1351
|
symbol_def = cache.get_symbol(comp.lib_id)
|
|
1352
|
+
|
|
1432
1353
|
if symbol_def:
|
|
1433
|
-
|
|
1434
|
-
lib_symbols[comp.lib_id] =
|
|
1435
|
-
symbol_def, comp.lib_id
|
|
1436
|
-
)
|
|
1437
|
-
|
|
1438
|
-
# Check if this symbol extends another symbol using multiple methods
|
|
1439
|
-
extends_parent = None
|
|
1440
|
-
|
|
1441
|
-
# Method 1: Check raw_kicad_data
|
|
1442
|
-
if hasattr(symbol_def, "raw_kicad_data") and symbol_def.raw_kicad_data:
|
|
1443
|
-
extends_parent = self._check_symbol_extends(symbol_def.raw_kicad_data)
|
|
1444
|
-
logger.debug(
|
|
1445
|
-
f"🔧 SCHEMATIC: Checked raw_kicad_data for {comp.lib_id}, extends: {extends_parent}"
|
|
1446
|
-
)
|
|
1447
|
-
|
|
1448
|
-
# Method 2: Check raw_data attribute
|
|
1449
|
-
if not extends_parent and hasattr(symbol_def, "__dict__"):
|
|
1450
|
-
for attr_name, attr_value in symbol_def.__dict__.items():
|
|
1451
|
-
if attr_name == "raw_data":
|
|
1452
|
-
logger.debug(
|
|
1453
|
-
f"🔧 SCHEMATIC: Checking raw_data for extends: {type(attr_value)}"
|
|
1454
|
-
)
|
|
1455
|
-
extends_parent = self._check_symbol_extends(attr_value)
|
|
1456
|
-
if extends_parent:
|
|
1457
|
-
logger.debug(
|
|
1458
|
-
f"🔧 SCHEMATIC: Found extends in raw_data: {extends_parent}"
|
|
1459
|
-
)
|
|
1460
|
-
|
|
1461
|
-
# Method 3: Check the extends attribute directly
|
|
1462
|
-
if not extends_parent and hasattr(symbol_def, "extends"):
|
|
1463
|
-
extends_parent = symbol_def.extends
|
|
1464
|
-
logger.debug(f"🔧 SCHEMATIC: Found extends attribute: {extends_parent}")
|
|
1465
|
-
|
|
1466
|
-
if extends_parent:
|
|
1467
|
-
# Load the parent symbol too
|
|
1468
|
-
parent_lib_id = f"{comp.lib_id.split(':')[0]}:{extends_parent}"
|
|
1469
|
-
logger.debug(f"🔧 SCHEMATIC: Loading parent symbol: {parent_lib_id}")
|
|
1470
|
-
|
|
1471
|
-
if parent_lib_id not in lib_symbols:
|
|
1472
|
-
parent_symbol_def = cache.get_symbol(parent_lib_id)
|
|
1473
|
-
if parent_symbol_def:
|
|
1474
|
-
lib_symbols[parent_lib_id] = self._convert_symbol_to_kicad_format(
|
|
1475
|
-
parent_symbol_def, parent_lib_id
|
|
1476
|
-
)
|
|
1477
|
-
logger.debug(
|
|
1478
|
-
f"🔧 SCHEMATIC: Successfully loaded parent symbol: {parent_lib_id} for {comp.lib_id}"
|
|
1479
|
-
)
|
|
1480
|
-
else:
|
|
1481
|
-
logger.warning(
|
|
1482
|
-
f"🔧 SCHEMATIC: Failed to load parent symbol: {parent_lib_id}"
|
|
1483
|
-
)
|
|
1484
|
-
else:
|
|
1485
|
-
logger.debug(
|
|
1486
|
-
f"🔧 SCHEMATIC: Parent symbol {parent_lib_id} already loaded"
|
|
1487
|
-
)
|
|
1488
|
-
else:
|
|
1489
|
-
logger.debug(f"🔧 SCHEMATIC: No extends found for {comp.lib_id}")
|
|
1490
|
-
else:
|
|
1491
|
-
# Fallback for unknown symbols
|
|
1492
|
-
logger.warning(
|
|
1493
|
-
f"🔧 SCHEMATIC: Failed to load symbol {comp.lib_id}, using fallback"
|
|
1494
|
-
)
|
|
1495
|
-
lib_symbols[comp.lib_id] = {"definition": "basic"}
|
|
1354
|
+
converted_symbol = self._convert_symbol_to_kicad_format(symbol_def, comp.lib_id)
|
|
1355
|
+
lib_symbols[comp.lib_id] = converted_symbol
|
|
1496
1356
|
|
|
1497
1357
|
self._data["lib_symbols"] = lib_symbols
|
|
1498
1358
|
|
|
1499
|
-
#
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
logger.debug(f"🔧 FINAL: - {sym_id}")
|
|
1503
|
-
# Check if this symbol has extends
|
|
1504
|
-
sym_data = lib_symbols[sym_id]
|
|
1505
|
-
if isinstance(sym_data, list) and len(sym_data) > 2:
|
|
1506
|
-
for item in sym_data[1:]:
|
|
1507
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1508
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1509
|
-
logger.debug(f"🔧 FINAL: - {sym_id} extends {item[1]}")
|
|
1510
|
-
break
|
|
1511
|
-
|
|
1512
|
-
def _check_symbol_extends(self, symbol_data: Any) -> Optional[str]:
|
|
1513
|
-
"""Check if symbol extends another symbol and return parent name."""
|
|
1514
|
-
logger.debug(f"🔧 EXTENDS: Checking symbol data type: {type(symbol_data)}")
|
|
1515
|
-
|
|
1516
|
-
if not isinstance(symbol_data, list):
|
|
1517
|
-
logger.debug(f"🔧 EXTENDS: Not a list, returning None")
|
|
1518
|
-
return None
|
|
1519
|
-
|
|
1520
|
-
logger.debug(f"🔧 EXTENDS: Checking {len(symbol_data)} items for extends directive")
|
|
1359
|
+
# Update sheet instances
|
|
1360
|
+
if not self._data["sheet_instances"]:
|
|
1361
|
+
self._data["sheet_instances"] = [{"path": "/", "page": "1"}]
|
|
1521
1362
|
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1527
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1528
|
-
parent_name = str(item[1]).strip('"')
|
|
1529
|
-
logger.debug(f"🔧 EXTENDS: Found extends directive: {parent_name}")
|
|
1530
|
-
return parent_name
|
|
1531
|
-
|
|
1532
|
-
logger.debug(f"🔧 EXTENDS: No extends directive found")
|
|
1533
|
-
return None
|
|
1363
|
+
# Remove symbol_instances section - instances are stored within each symbol in lib_symbols
|
|
1364
|
+
# This matches KiCAD's format where instances are part of the symbol definition
|
|
1365
|
+
if "symbol_instances" in self._data:
|
|
1366
|
+
del self._data["symbol_instances"]
|
|
1534
1367
|
|
|
1535
1368
|
def _sync_wires_to_data(self):
|
|
1536
1369
|
"""Sync wire collection state back to data structure."""
|
|
@@ -1561,156 +1394,153 @@ class Schematic:
|
|
|
1561
1394
|
|
|
1562
1395
|
self._data["junctions"] = junction_data
|
|
1563
1396
|
|
|
1564
|
-
def
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
"
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
"
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1397
|
+
def _sync_texts_to_data(self):
|
|
1398
|
+
"""Sync text collection state back to data structure."""
|
|
1399
|
+
text_data = []
|
|
1400
|
+
for text_element in self._texts:
|
|
1401
|
+
text_dict = {
|
|
1402
|
+
"uuid": text_element.uuid,
|
|
1403
|
+
"text": text_element.text,
|
|
1404
|
+
"position": {"x": text_element.position.x, "y": text_element.position.y},
|
|
1405
|
+
"rotation": text_element.rotation,
|
|
1406
|
+
"size": text_element.size,
|
|
1407
|
+
"exclude_from_sim": text_element.exclude_from_sim,
|
|
1408
|
+
}
|
|
1409
|
+
text_data.append(text_dict)
|
|
1410
|
+
|
|
1411
|
+
self._data["texts"] = text_data
|
|
1412
|
+
|
|
1413
|
+
def _sync_labels_to_data(self):
|
|
1414
|
+
"""Sync label collection state back to data structure."""
|
|
1415
|
+
label_data = []
|
|
1416
|
+
for label_element in self._labels:
|
|
1417
|
+
label_dict = {
|
|
1418
|
+
"uuid": label_element.uuid,
|
|
1419
|
+
"text": label_element.text,
|
|
1420
|
+
"position": {"x": label_element.position.x, "y": label_element.position.y},
|
|
1421
|
+
"rotation": label_element.rotation,
|
|
1422
|
+
"size": label_element.size,
|
|
1423
|
+
}
|
|
1424
|
+
label_data.append(label_dict)
|
|
1425
|
+
|
|
1426
|
+
self._data["labels"] = label_data
|
|
1427
|
+
|
|
1428
|
+
def _sync_hierarchical_labels_to_data(self):
|
|
1429
|
+
"""Sync hierarchical label collection state back to data structure."""
|
|
1430
|
+
hierarchical_label_data = []
|
|
1431
|
+
for hlabel_element in self._hierarchical_labels:
|
|
1432
|
+
hlabel_dict = {
|
|
1433
|
+
"uuid": hlabel_element.uuid,
|
|
1434
|
+
"text": hlabel_element.text,
|
|
1435
|
+
"position": {"x": hlabel_element.position.x, "y": hlabel_element.position.y},
|
|
1436
|
+
"rotation": hlabel_element.rotation,
|
|
1437
|
+
"size": hlabel_element.size,
|
|
1438
|
+
}
|
|
1439
|
+
hierarchical_label_data.append(hlabel_dict)
|
|
1440
|
+
|
|
1441
|
+
self._data["hierarchical_labels"] = hierarchical_label_data
|
|
1442
|
+
|
|
1443
|
+
def _sync_no_connects_to_data(self):
|
|
1444
|
+
"""Sync no-connect collection state back to data structure."""
|
|
1445
|
+
no_connect_data = []
|
|
1446
|
+
for no_connect_element in self._no_connects:
|
|
1447
|
+
no_connect_dict = {
|
|
1448
|
+
"uuid": no_connect_element.uuid,
|
|
1449
|
+
"position": {
|
|
1450
|
+
"x": no_connect_element.position.x,
|
|
1451
|
+
"y": no_connect_element.position.y,
|
|
1608
1452
|
},
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1453
|
+
}
|
|
1454
|
+
no_connect_data.append(no_connect_dict)
|
|
1455
|
+
|
|
1456
|
+
self._data["no_connects"] = no_connect_data
|
|
1457
|
+
|
|
1458
|
+
def _sync_nets_to_data(self):
|
|
1459
|
+
"""Sync net collection state back to data structure."""
|
|
1460
|
+
net_data = []
|
|
1461
|
+
for net_element in self._nets:
|
|
1462
|
+
net_dict = {
|
|
1463
|
+
"name": net_element.name,
|
|
1464
|
+
"components": net_element.components,
|
|
1465
|
+
"wires": net_element.wires,
|
|
1466
|
+
"labels": net_element.labels,
|
|
1467
|
+
}
|
|
1468
|
+
net_data.append(net_dict)
|
|
1612
1469
|
|
|
1613
|
-
|
|
1614
|
-
"""Convert raw parsed KiCAD symbol data to dictionary format for S-expression generation."""
|
|
1615
|
-
import copy
|
|
1470
|
+
self._data["nets"] = net_data
|
|
1616
1471
|
|
|
1617
|
-
|
|
1472
|
+
def _convert_symbol_to_kicad_format(self, symbol_def, lib_id: str):
|
|
1473
|
+
"""Convert symbol definition to KiCAD format."""
|
|
1474
|
+
# Use raw data if available, but fix the symbol name to use full lib_id
|
|
1475
|
+
if hasattr(symbol_def, "raw_kicad_data") and symbol_def.raw_kicad_data:
|
|
1476
|
+
raw_data = symbol_def.raw_kicad_data
|
|
1618
1477
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1478
|
+
# Check if raw data already contains instances with project info
|
|
1479
|
+
project_refs_found = []
|
|
1621
1480
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1481
|
+
def find_project_refs(data, path="root"):
|
|
1482
|
+
if isinstance(data, list):
|
|
1483
|
+
for i, item in enumerate(data):
|
|
1484
|
+
if hasattr(item, "__str__") and str(item) == "project":
|
|
1485
|
+
if i < len(data) - 1:
|
|
1486
|
+
project_refs_found.append(f"{path}[{i}] = '{data[i+1]}'")
|
|
1487
|
+
elif isinstance(item, list):
|
|
1488
|
+
find_project_refs(item, f"{path}[{i}]")
|
|
1625
1489
|
|
|
1626
|
-
|
|
1627
|
-
logger.debug(f"🔧 CONVERT: Processing {len(modified_data)} items for {lib_id}")
|
|
1628
|
-
for i, item in enumerate(modified_data[1:], 1):
|
|
1629
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1630
|
-
logger.debug(
|
|
1631
|
-
f"🔧 CONVERT: Item {i}: {item[0]} = {item[1] if len(item) > 1 else 'N/A'}"
|
|
1632
|
-
)
|
|
1633
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1634
|
-
# Convert bare symbol name to full lib_id
|
|
1635
|
-
parent_name = str(item[1]).strip('"')
|
|
1636
|
-
parent_lib_id = f"{lib_id.split(':')[0]}:{parent_name}"
|
|
1637
|
-
modified_data[i][1] = parent_lib_id
|
|
1638
|
-
logger.debug(
|
|
1639
|
-
f"🔧 CONVERT: Fixed extends directive: {parent_name} -> {parent_lib_id}"
|
|
1640
|
-
)
|
|
1641
|
-
break
|
|
1642
|
-
|
|
1643
|
-
# Fix string/symbol conversion issues in pin definitions
|
|
1644
|
-
self._fix_symbol_strings_recursively(modified_data)
|
|
1645
|
-
|
|
1646
|
-
return modified_data
|
|
1647
|
-
|
|
1648
|
-
def _fix_symbol_strings_recursively(self, data):
|
|
1649
|
-
"""Recursively fix string/symbol issues in parsed S-expression data."""
|
|
1650
|
-
import sexpdata
|
|
1651
|
-
|
|
1652
|
-
if isinstance(data, list):
|
|
1653
|
-
for i, item in enumerate(data):
|
|
1654
|
-
if isinstance(item, list):
|
|
1655
|
-
# Check for pin definitions that need fixing
|
|
1656
|
-
if len(item) >= 3 and item[0] == sexpdata.Symbol("pin"):
|
|
1657
|
-
# Fix pin type and shape - ensure they are symbols not strings
|
|
1658
|
-
if isinstance(item[1], str):
|
|
1659
|
-
item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
|
|
1660
|
-
if len(item) >= 3 and isinstance(item[2], str):
|
|
1661
|
-
item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
|
|
1662
|
-
|
|
1663
|
-
# Recursively process nested lists
|
|
1664
|
-
self._fix_symbol_strings_recursively(item)
|
|
1665
|
-
elif isinstance(item, str):
|
|
1666
|
-
# Fix common KiCAD keywords that should be symbols
|
|
1667
|
-
if item in ["yes", "no", "default", "none", "left", "right", "center"]:
|
|
1668
|
-
data[i] = sexpdata.Symbol(item)
|
|
1669
|
-
|
|
1670
|
-
return data
|
|
1490
|
+
find_project_refs(raw_data)
|
|
1671
1491
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1492
|
+
# Make a copy and fix the symbol name (index 1) to use full lib_id
|
|
1493
|
+
if isinstance(raw_data, list) and len(raw_data) > 1:
|
|
1494
|
+
fixed_data = raw_data.copy()
|
|
1495
|
+
fixed_data[1] = lib_id # Replace short name with full lib_id
|
|
1496
|
+
|
|
1497
|
+
# Also fix any project references in instances to use current project name
|
|
1498
|
+
self._fix_symbol_project_references(fixed_data)
|
|
1499
|
+
|
|
1500
|
+
return fixed_data
|
|
1501
|
+
else:
|
|
1502
|
+
return raw_data
|
|
1503
|
+
|
|
1504
|
+
# Fallback: create basic symbol structure
|
|
1675
1505
|
return {
|
|
1676
|
-
"
|
|
1677
|
-
"
|
|
1678
|
-
"generator_version": "9.0",
|
|
1679
|
-
"uuid": str(uuid.uuid4()),
|
|
1680
|
-
"paper": "A4",
|
|
1681
|
-
"components": [],
|
|
1682
|
-
"wires": [],
|
|
1683
|
-
"junctions": [],
|
|
1684
|
-
"labels": [],
|
|
1685
|
-
"nets": [],
|
|
1686
|
-
"lib_symbols": {},
|
|
1687
|
-
"sheet_instances": [{"path": "/", "page": "1"}],
|
|
1688
|
-
"symbol_instances": [],
|
|
1689
|
-
"embedded_fonts": "no",
|
|
1506
|
+
"lib_id": lib_id,
|
|
1507
|
+
"symbol": symbol_def.name if hasattr(symbol_def, "name") else lib_id.split(":")[-1],
|
|
1690
1508
|
}
|
|
1691
1509
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1510
|
+
def _fix_symbol_project_references(self, symbol_data):
|
|
1511
|
+
"""Fix project references in symbol instances to use current project name."""
|
|
1512
|
+
if not isinstance(symbol_data, list):
|
|
1513
|
+
return
|
|
1514
|
+
|
|
1515
|
+
# Recursively search for instances sections and update project names
|
|
1516
|
+
for i, element in enumerate(symbol_data):
|
|
1517
|
+
if isinstance(element, list):
|
|
1518
|
+
# Check if this is an instances section
|
|
1519
|
+
if (
|
|
1520
|
+
len(element) > 0
|
|
1521
|
+
and hasattr(element[0], "__str__")
|
|
1522
|
+
and str(element[0]) == "instances"
|
|
1523
|
+
):
|
|
1524
|
+
# Look for project references within instances
|
|
1525
|
+
self._update_project_in_instances(element)
|
|
1526
|
+
else:
|
|
1527
|
+
# Recursively check nested lists
|
|
1528
|
+
self._fix_symbol_project_references(element)
|
|
1529
|
+
|
|
1530
|
+
def _update_project_in_instances(self, instances_element):
|
|
1531
|
+
"""Update project name in instances element."""
|
|
1532
|
+
if not isinstance(instances_element, list):
|
|
1533
|
+
return
|
|
1534
|
+
|
|
1535
|
+
for i, element in enumerate(instances_element):
|
|
1536
|
+
if isinstance(element, list) and len(element) >= 2:
|
|
1537
|
+
# Check if this is a project element: ['project', 'old_name', ...]
|
|
1538
|
+
if hasattr(element[0], "__str__") and str(element[0]) == "project":
|
|
1539
|
+
old_name = element[1]
|
|
1540
|
+
element[1] = self.name # Replace with current schematic name
|
|
1541
|
+
else:
|
|
1542
|
+
# Recursively check nested elements
|
|
1543
|
+
self._update_project_in_instances(element)
|
|
1714
1544
|
|
|
1715
1545
|
def __str__(self) -> str:
|
|
1716
1546
|
"""String representation."""
|