kicad-sch-api 0.3.5__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 +2 -2
- kicad_sch_api/collections/base.py +5 -7
- kicad_sch_api/collections/components.py +24 -12
- kicad_sch_api/collections/junctions.py +31 -43
- kicad_sch_api/collections/labels.py +19 -27
- kicad_sch_api/collections/wires.py +17 -18
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +2 -2
- 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 +1 -1
- kicad_sch_api/core/no_connects.py +5 -3
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +779 -1083
- kicad_sch_api/core/texts.py +1 -1
- kicad_sch_api/core/types.py +1 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +40 -21
- kicad_sch_api/interfaces/__init__.py +1 -1
- kicad_sch_api/interfaces/parser.py +1 -1
- kicad_sch_api/interfaces/repository.py +1 -1
- kicad_sch_api/interfaces/resolver.py +1 -1
- kicad_sch_api/parsers/__init__.py +2 -2
- kicad_sch_api/parsers/base.py +7 -10
- kicad_sch_api/parsers/label_parser.py +7 -7
- kicad_sch_api/parsers/registry.py +4 -2
- kicad_sch_api/parsers/symbol_parser.py +5 -10
- kicad_sch_api/parsers/wire_parser.py +2 -2
- kicad_sch_api/symbols/__init__.py +1 -1
- kicad_sch_api/symbols/cache.py +9 -12
- kicad_sch_api/symbols/resolver.py +20 -26
- kicad_sch_api/symbols/validators.py +188 -137
- {kicad_sch_api-0.3.5.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.5.dist-info/RECORD +0 -58
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.5.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
|
|
@@ -19,6 +20,16 @@ from .components import ComponentCollection
|
|
|
19
20
|
from .formatter import ExactFormatter
|
|
20
21
|
from .junctions import JunctionCollection
|
|
21
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
|
+
)
|
|
22
33
|
from .nets import NetCollection
|
|
23
34
|
from .no_connects import NoConnectCollection
|
|
24
35
|
from .parser import SExpressionParser
|
|
@@ -46,7 +57,7 @@ logger = logging.getLogger(__name__)
|
|
|
46
57
|
|
|
47
58
|
class Schematic:
|
|
48
59
|
"""
|
|
49
|
-
Professional KiCAD schematic manipulation class.
|
|
60
|
+
Professional KiCAD schematic manipulation class with manager-based architecture.
|
|
50
61
|
|
|
51
62
|
Features:
|
|
52
63
|
- Exact format preservation
|
|
@@ -55,9 +66,10 @@ class Schematic:
|
|
|
55
66
|
- Comprehensive validation
|
|
56
67
|
- Performance optimization for large schematics
|
|
57
68
|
- AI agent integration via MCP
|
|
69
|
+
- Modular architecture with specialized managers
|
|
58
70
|
|
|
59
71
|
This class provides a modern, intuitive API while maintaining exact compatibility
|
|
60
|
-
with KiCAD's native file format.
|
|
72
|
+
with KiCAD's native file format through specialized manager classes.
|
|
61
73
|
"""
|
|
62
74
|
|
|
63
75
|
def __init__(
|
|
@@ -67,7 +79,7 @@ class Schematic:
|
|
|
67
79
|
name: Optional[str] = None,
|
|
68
80
|
):
|
|
69
81
|
"""
|
|
70
|
-
Initialize schematic object.
|
|
82
|
+
Initialize schematic object with manager-based architecture.
|
|
71
83
|
|
|
72
84
|
Args:
|
|
73
85
|
schematic_data: Parsed schematic data
|
|
@@ -78,13 +90,13 @@ class Schematic:
|
|
|
78
90
|
self._data = schematic_data or self._create_empty_schematic_data()
|
|
79
91
|
self._file_path = Path(file_path) if file_path else None
|
|
80
92
|
self._original_content = self._data.get("_original_content", "")
|
|
81
|
-
self.name = name or "simple_circuit"
|
|
93
|
+
self.name = name or "simple_circuit"
|
|
82
94
|
|
|
83
95
|
# Initialize parser and formatter
|
|
84
96
|
self._parser = SExpressionParser(preserve_format=True)
|
|
85
|
-
self._parser.project_name = self.name
|
|
97
|
+
self._parser.project_name = self.name
|
|
86
98
|
self._formatter = ExactFormatter()
|
|
87
|
-
self.
|
|
99
|
+
self._legacy_validator = SchematicValidator() # Keep for compatibility
|
|
88
100
|
|
|
89
101
|
# Initialize component collection
|
|
90
102
|
component_symbols = [
|
|
@@ -187,13 +199,48 @@ class Schematic:
|
|
|
187
199
|
label_type=LabelType(label_dict.get("label_type", "local")),
|
|
188
200
|
rotation=label_dict.get("rotation", 0.0),
|
|
189
201
|
size=label_dict.get("size", 1.27),
|
|
190
|
-
shape=
|
|
202
|
+
shape=(
|
|
203
|
+
HierarchicalLabelShape(label_dict.get("shape"))
|
|
204
|
+
if label_dict.get("shape")
|
|
205
|
+
else None
|
|
206
|
+
),
|
|
191
207
|
)
|
|
192
208
|
labels.append(label)
|
|
193
209
|
self._labels = LabelCollection(labels)
|
|
194
210
|
|
|
195
|
-
# Initialize hierarchical labels collection (
|
|
196
|
-
hierarchical_labels = [
|
|
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
|
+
|
|
197
244
|
self._hierarchical_labels = LabelCollection(hierarchical_labels)
|
|
198
245
|
|
|
199
246
|
# Initialize no-connect collection
|
|
@@ -232,6 +279,16 @@ class Schematic:
|
|
|
232
279
|
nets.append(net)
|
|
233
280
|
self._nets = NetCollection(nets)
|
|
234
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
|
+
|
|
235
292
|
# Track modifications for save optimization
|
|
236
293
|
self._modified = False
|
|
237
294
|
self._last_save_time = None
|
|
@@ -244,7 +301,7 @@ class Schematic:
|
|
|
244
301
|
f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, "
|
|
245
302
|
f"{len(self._junctions)} junctions, {len(self._texts)} texts, {len(self._labels)} labels, "
|
|
246
303
|
f"{len(self._hierarchical_labels)} hierarchical labels, {len(self._no_connects)} no-connects, "
|
|
247
|
-
f"and {len(self._nets)} nets"
|
|
304
|
+
f"and {len(self._nets)} nets with managers initialized"
|
|
248
305
|
)
|
|
249
306
|
|
|
250
307
|
@classmethod
|
|
@@ -267,8 +324,9 @@ class Schematic:
|
|
|
267
324
|
|
|
268
325
|
logger.info(f"Loading schematic: {file_path}")
|
|
269
326
|
|
|
270
|
-
|
|
271
|
-
|
|
327
|
+
# Use FileIOManager for loading
|
|
328
|
+
file_io_manager = FileIOManager()
|
|
329
|
+
schematic_data = file_io_manager.load_schematic(file_path)
|
|
272
330
|
|
|
273
331
|
load_time = time.time() - start_time
|
|
274
332
|
logger.info(f"Loaded schematic in {load_time:.3f}s")
|
|
@@ -311,8 +369,10 @@ class Schematic:
|
|
|
311
369
|
"junctions": [],
|
|
312
370
|
"labels": [],
|
|
313
371
|
"nets": [],
|
|
314
|
-
"lib_symbols":
|
|
372
|
+
"lib_symbols": {}, # Empty dict for blank schematic
|
|
315
373
|
"symbol_instances": [],
|
|
374
|
+
"sheet_instances": [],
|
|
375
|
+
"embedded_fonts": "no",
|
|
316
376
|
}
|
|
317
377
|
else:
|
|
318
378
|
schematic_data = cls._create_empty_schematic_data()
|
|
@@ -385,6 +445,7 @@ class Schematic:
|
|
|
385
445
|
or self._hierarchical_labels._modified
|
|
386
446
|
or self._no_connects._modified
|
|
387
447
|
or self._nets._modified
|
|
448
|
+
or self._format_sync_manager.is_dirty()
|
|
388
449
|
)
|
|
389
450
|
|
|
390
451
|
@property
|
|
@@ -412,13 +473,11 @@ class Schematic:
|
|
|
412
473
|
"""Collection of all electrical nets in the schematic."""
|
|
413
474
|
return self._nets
|
|
414
475
|
|
|
415
|
-
# Pin positioning methods (
|
|
476
|
+
# Pin positioning methods (delegated to WireManager)
|
|
416
477
|
def get_component_pin_position(self, reference: str, pin_number: str) -> Optional[Point]:
|
|
417
478
|
"""
|
|
418
479
|
Get the absolute position of a component pin.
|
|
419
480
|
|
|
420
|
-
Migrated from circuit-synth with enhanced logging for verification.
|
|
421
|
-
|
|
422
481
|
Args:
|
|
423
482
|
reference: Component reference (e.g., "R1")
|
|
424
483
|
pin_number: Pin number to find (e.g., "1", "2")
|
|
@@ -426,20 +485,7 @@ class Schematic:
|
|
|
426
485
|
Returns:
|
|
427
486
|
Absolute position of the pin, or None if not found
|
|
428
487
|
"""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
# Find the component
|
|
432
|
-
component = None
|
|
433
|
-
for comp in self._components:
|
|
434
|
-
if comp.reference == reference:
|
|
435
|
-
component = comp
|
|
436
|
-
break
|
|
437
|
-
|
|
438
|
-
if not component:
|
|
439
|
-
logger.warning(f"Component {reference} not found")
|
|
440
|
-
return None
|
|
441
|
-
|
|
442
|
-
return get_component_pin_position(component, pin_number)
|
|
488
|
+
return self._wire_manager.get_component_pin_position(reference, pin_number)
|
|
443
489
|
|
|
444
490
|
def list_component_pins(self, reference: str) -> List[Tuple[str, Point]]:
|
|
445
491
|
"""
|
|
@@ -451,22 +497,9 @@ class Schematic:
|
|
|
451
497
|
Returns:
|
|
452
498
|
List of (pin_number, absolute_position) tuples
|
|
453
499
|
"""
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
# Find the component
|
|
457
|
-
component = None
|
|
458
|
-
for comp in self._components:
|
|
459
|
-
if comp.reference == reference:
|
|
460
|
-
component = comp
|
|
461
|
-
break
|
|
462
|
-
|
|
463
|
-
if not component:
|
|
464
|
-
logger.warning(f"Component {reference} not found")
|
|
465
|
-
return []
|
|
500
|
+
return self._wire_manager.list_component_pins(reference)
|
|
466
501
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
# File operations
|
|
502
|
+
# File operations (delegated to FileIOManager)
|
|
470
503
|
def save(self, file_path: Optional[Union[str, Path]] = None, preserve_format: bool = True):
|
|
471
504
|
"""
|
|
472
505
|
Save schematic to file.
|
|
@@ -495,31 +528,26 @@ class Schematic:
|
|
|
495
528
|
if errors:
|
|
496
529
|
raise ValidationError("Cannot save schematic with validation errors", errors)
|
|
497
530
|
|
|
498
|
-
#
|
|
531
|
+
# Sync collection state back to data structure (critical for save)
|
|
499
532
|
self._sync_components_to_data()
|
|
500
533
|
self._sync_wires_to_data()
|
|
501
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()
|
|
502
540
|
|
|
503
|
-
#
|
|
504
|
-
|
|
505
|
-
# Use format-preserving writer
|
|
506
|
-
sexp_data = self._parser._schematic_data_to_sexp(self._data)
|
|
507
|
-
content = self._formatter.format_preserving_write(sexp_data, self._original_content)
|
|
508
|
-
else:
|
|
509
|
-
# Standard formatting
|
|
510
|
-
sexp_data = self._parser._schematic_data_to_sexp(self._data)
|
|
511
|
-
content = self._formatter.format(sexp_data)
|
|
512
|
-
|
|
513
|
-
# Ensure directory exists
|
|
514
|
-
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
|
|
515
543
|
|
|
516
|
-
#
|
|
517
|
-
|
|
518
|
-
f.write(content)
|
|
544
|
+
# Use FileIOManager for saving
|
|
545
|
+
self._file_io_manager.save_schematic(self._data, file_path, preserve_format)
|
|
519
546
|
|
|
520
547
|
# Update state
|
|
521
548
|
self._modified = False
|
|
522
549
|
self._components._modified = False
|
|
550
|
+
self._format_sync_manager.clear_dirty_flags()
|
|
523
551
|
self._last_save_time = time.time()
|
|
524
552
|
|
|
525
553
|
save_time = time.time() - start_time
|
|
@@ -534,121 +562,22 @@ class Schematic:
|
|
|
534
562
|
Create a backup of the current schematic file.
|
|
535
563
|
|
|
536
564
|
Args:
|
|
537
|
-
suffix:
|
|
565
|
+
suffix: Backup file suffix
|
|
538
566
|
|
|
539
567
|
Returns:
|
|
540
568
|
Path to backup file
|
|
541
569
|
"""
|
|
542
|
-
if
|
|
543
|
-
raise ValidationError("Cannot backup
|
|
544
|
-
|
|
545
|
-
backup_path = self._file_path.with_suffix(self._file_path.suffix + suffix)
|
|
546
|
-
|
|
547
|
-
if self._file_path.exists():
|
|
548
|
-
import shutil
|
|
549
|
-
|
|
550
|
-
shutil.copy2(self._file_path, backup_path)
|
|
551
|
-
logger.info(f"Created backup: {backup_path}")
|
|
552
|
-
|
|
553
|
-
return backup_path
|
|
554
|
-
|
|
555
|
-
# Validation and analysis
|
|
556
|
-
def validate(self) -> List[ValidationIssue]:
|
|
557
|
-
"""
|
|
558
|
-
Validate the schematic for errors and issues.
|
|
559
|
-
|
|
560
|
-
Returns:
|
|
561
|
-
List of validation issues found
|
|
562
|
-
"""
|
|
563
|
-
# Sync current state to data for validation
|
|
564
|
-
self._sync_components_to_data()
|
|
565
|
-
|
|
566
|
-
# Use validator to check schematic
|
|
567
|
-
issues = self._validator.validate_schematic_data(self._data)
|
|
568
|
-
|
|
569
|
-
# Add component-level validation
|
|
570
|
-
component_issues = self._components.validate_all()
|
|
571
|
-
issues.extend(component_issues)
|
|
572
|
-
|
|
573
|
-
return issues
|
|
574
|
-
|
|
575
|
-
# Focused helper functions for specific KiCAD sections
|
|
576
|
-
def add_lib_symbols_section(self, lib_symbols: Dict[str, Any]):
|
|
577
|
-
"""Add or update lib_symbols section with specific symbol definitions."""
|
|
578
|
-
self._data["lib_symbols"] = lib_symbols
|
|
579
|
-
self._modified = True
|
|
580
|
-
|
|
581
|
-
def add_instances_section(self, instances: Dict[str, Any]):
|
|
582
|
-
"""Add instances section for component placement tracking."""
|
|
583
|
-
self._data["instances"] = instances
|
|
584
|
-
self._modified = True
|
|
585
|
-
|
|
586
|
-
def add_sheet_instances_section(self, sheet_instances: List[Dict]):
|
|
587
|
-
"""Add sheet_instances section for hierarchical design."""
|
|
588
|
-
self._data["sheet_instances"] = sheet_instances
|
|
589
|
-
self._modified = True
|
|
590
|
-
|
|
591
|
-
def set_paper_size(self, paper: str):
|
|
592
|
-
"""Set paper size (A4, A3, etc.)."""
|
|
593
|
-
self._data["paper"] = paper
|
|
594
|
-
self._modified = True
|
|
595
|
-
|
|
596
|
-
def set_version_info(
|
|
597
|
-
self, version: str, generator: str = "eeschema", generator_version: str = "9.0"
|
|
598
|
-
):
|
|
599
|
-
"""Set version and generator information."""
|
|
600
|
-
self._data["version"] = version
|
|
601
|
-
self._data["generator"] = generator
|
|
602
|
-
self._data["generator_version"] = generator_version
|
|
603
|
-
self._modified = True
|
|
570
|
+
if self._file_path is None:
|
|
571
|
+
raise ValidationError("Cannot backup schematic with no file path")
|
|
604
572
|
|
|
605
|
-
|
|
606
|
-
"""Copy all metadata from another schematic (version, generator, paper, etc.)."""
|
|
607
|
-
metadata_fields = [
|
|
608
|
-
"version",
|
|
609
|
-
"generator",
|
|
610
|
-
"generator_version",
|
|
611
|
-
"paper",
|
|
612
|
-
"uuid",
|
|
613
|
-
"title_block",
|
|
614
|
-
]
|
|
615
|
-
for field in metadata_fields:
|
|
616
|
-
if field in source_schematic._data:
|
|
617
|
-
self._data[field] = source_schematic._data[field]
|
|
618
|
-
self._modified = True
|
|
619
|
-
|
|
620
|
-
def get_summary(self) -> Dict[str, Any]:
|
|
621
|
-
"""Get summary information about the schematic."""
|
|
622
|
-
component_stats = self._components.get_statistics()
|
|
623
|
-
|
|
624
|
-
return {
|
|
625
|
-
"file_path": str(self._file_path) if self._file_path else None,
|
|
626
|
-
"version": self.version,
|
|
627
|
-
"uuid": self.uuid,
|
|
628
|
-
"title": self.title_block.get("title", ""),
|
|
629
|
-
"component_count": len(self._components),
|
|
630
|
-
"modified": self.modified,
|
|
631
|
-
"last_save": self._last_save_time,
|
|
632
|
-
"component_stats": component_stats,
|
|
633
|
-
"performance": {
|
|
634
|
-
"operation_count": self._operation_count,
|
|
635
|
-
"avg_operation_time_ms": round(
|
|
636
|
-
(
|
|
637
|
-
(self._total_operation_time / self._operation_count * 1000)
|
|
638
|
-
if self._operation_count > 0
|
|
639
|
-
else 0
|
|
640
|
-
),
|
|
641
|
-
2,
|
|
642
|
-
),
|
|
643
|
-
},
|
|
644
|
-
}
|
|
573
|
+
return self._file_io_manager.create_backup(self._file_path, suffix)
|
|
645
574
|
|
|
646
|
-
# Wire
|
|
575
|
+
# Wire operations (delegated to WireManager)
|
|
647
576
|
def add_wire(
|
|
648
577
|
self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
|
|
649
578
|
) -> str:
|
|
650
579
|
"""
|
|
651
|
-
Add a wire connection.
|
|
580
|
+
Add a wire connection between two points.
|
|
652
581
|
|
|
653
582
|
Args:
|
|
654
583
|
start: Start point
|
|
@@ -657,489 +586,149 @@ class Schematic:
|
|
|
657
586
|
Returns:
|
|
658
587
|
UUID of created wire
|
|
659
588
|
"""
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if isinstance(end, tuple):
|
|
663
|
-
end = Point(end[0], end[1])
|
|
664
|
-
|
|
665
|
-
# Use the wire collection to add the wire
|
|
666
|
-
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})
|
|
667
591
|
self._modified = True
|
|
668
|
-
|
|
669
|
-
logger.debug(f"Added wire: {start} -> {end}")
|
|
670
592
|
return wire_uuid
|
|
671
593
|
|
|
672
594
|
def remove_wire(self, wire_uuid: str) -> bool:
|
|
673
|
-
"""
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
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)
|
|
687
607
|
self._modified = True
|
|
688
|
-
|
|
689
|
-
return True
|
|
690
|
-
return False
|
|
608
|
+
return removed
|
|
691
609
|
|
|
692
|
-
|
|
693
|
-
def add_hierarchical_label(
|
|
610
|
+
def auto_route_pins(
|
|
694
611
|
self,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
) -> 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]:
|
|
701
618
|
"""
|
|
702
|
-
|
|
619
|
+
Auto-route between two component pins.
|
|
703
620
|
|
|
704
621
|
Args:
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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")
|
|
710
627
|
|
|
711
628
|
Returns:
|
|
712
|
-
|
|
629
|
+
List of wire UUIDs created
|
|
713
630
|
"""
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
label = Label(
|
|
718
|
-
uuid=str(uuid.uuid4()),
|
|
719
|
-
position=position,
|
|
720
|
-
text=text,
|
|
721
|
-
label_type=LabelType.HIERARCHICAL,
|
|
722
|
-
rotation=rotation,
|
|
723
|
-
size=size,
|
|
724
|
-
shape=shape,
|
|
725
|
-
)
|
|
726
|
-
|
|
727
|
-
if "hierarchical_labels" not in self._data:
|
|
728
|
-
self._data["hierarchical_labels"] = []
|
|
729
|
-
|
|
730
|
-
self._data["hierarchical_labels"].append(
|
|
731
|
-
{
|
|
732
|
-
"uuid": label.uuid,
|
|
733
|
-
"position": {"x": label.position.x, "y": label.position.y},
|
|
734
|
-
"text": label.text,
|
|
735
|
-
"shape": label.shape.value,
|
|
736
|
-
"rotation": label.rotation,
|
|
737
|
-
"size": label.size,
|
|
738
|
-
}
|
|
631
|
+
wire_uuids = self._wire_manager.auto_route_pins(
|
|
632
|
+
component1_ref, pin1_number, component2_ref, pin2_number, routing_strategy
|
|
739
633
|
)
|
|
634
|
+
for wire_uuid in wire_uuids:
|
|
635
|
+
self._format_sync_manager.mark_dirty("wire", "add", {"uuid": wire_uuid})
|
|
740
636
|
self._modified = True
|
|
741
|
-
|
|
742
|
-
logger.debug(f"Added hierarchical label: {text} at {position}")
|
|
743
|
-
return label.uuid
|
|
744
|
-
|
|
745
|
-
def remove_hierarchical_label(self, label_uuid: str) -> bool:
|
|
746
|
-
"""Remove hierarchical label by UUID."""
|
|
747
|
-
labels = self._data.get("hierarchical_labels", [])
|
|
748
|
-
for i, label in enumerate(labels):
|
|
749
|
-
if label.get("uuid") == label_uuid:
|
|
750
|
-
del labels[i]
|
|
751
|
-
self._modified = True
|
|
752
|
-
logger.debug(f"Removed hierarchical label: {label_uuid}")
|
|
753
|
-
return True
|
|
754
|
-
return False
|
|
637
|
+
return wire_uuids
|
|
755
638
|
|
|
756
639
|
def add_wire_to_pin(
|
|
757
|
-
self,
|
|
640
|
+
self, start: Union[Point, Tuple[float, float]], component_ref: str, pin_number: str
|
|
758
641
|
) -> Optional[str]:
|
|
759
642
|
"""
|
|
760
|
-
|
|
643
|
+
Add wire from arbitrary position to component pin.
|
|
761
644
|
|
|
762
645
|
Args:
|
|
763
|
-
|
|
764
|
-
component_ref:
|
|
765
|
-
pin_number: Pin number
|
|
646
|
+
start: Start position
|
|
647
|
+
component_ref: Component reference
|
|
648
|
+
pin_number: Pin number
|
|
766
649
|
|
|
767
650
|
Returns:
|
|
768
|
-
UUID
|
|
651
|
+
Wire UUID or None if pin not found
|
|
769
652
|
"""
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
# Find the component
|
|
773
|
-
component = self.components.get(component_ref)
|
|
774
|
-
if not component:
|
|
775
|
-
logger.warning(f"Component {component_ref} not found")
|
|
653
|
+
pin_pos = self.get_component_pin_position(component_ref, pin_number)
|
|
654
|
+
if pin_pos is None:
|
|
776
655
|
return None
|
|
777
656
|
|
|
778
|
-
|
|
779
|
-
pin_position = get_component_pin_position(component, pin_number)
|
|
780
|
-
if not pin_position:
|
|
781
|
-
logger.warning(f"Could not determine position of pin {pin_number} on {component_ref}")
|
|
782
|
-
return None
|
|
783
|
-
|
|
784
|
-
# Create the wire
|
|
785
|
-
return self.add_wire(start_point, pin_position)
|
|
657
|
+
return self.add_wire(start, pin_pos)
|
|
786
658
|
|
|
787
659
|
def add_wire_between_pins(
|
|
788
660
|
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
789
661
|
) -> Optional[str]:
|
|
790
662
|
"""
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
Args:
|
|
794
|
-
component1_ref: Reference of the first component (e.g., "R1")
|
|
795
|
-
pin1_number: Pin number on the first component (e.g., "1")
|
|
796
|
-
component2_ref: Reference of the second component (e.g., "R2")
|
|
797
|
-
pin2_number: Pin number on the second component (e.g., "2")
|
|
798
|
-
|
|
799
|
-
Returns:
|
|
800
|
-
UUID of created wire, or None if either pin position cannot be determined
|
|
801
|
-
"""
|
|
802
|
-
from .pin_utils import get_component_pin_position
|
|
803
|
-
|
|
804
|
-
# Find both components
|
|
805
|
-
component1 = self.components.get(component1_ref)
|
|
806
|
-
component2 = self.components.get(component2_ref)
|
|
807
|
-
|
|
808
|
-
if not component1:
|
|
809
|
-
logger.warning(f"Component {component1_ref} not found")
|
|
810
|
-
return None
|
|
811
|
-
if not component2:
|
|
812
|
-
logger.warning(f"Component {component2_ref} not found")
|
|
813
|
-
return None
|
|
814
|
-
|
|
815
|
-
# Get both pin positions
|
|
816
|
-
pin1_position = get_component_pin_position(component1, pin1_number)
|
|
817
|
-
pin2_position = get_component_pin_position(component2, pin2_number)
|
|
818
|
-
|
|
819
|
-
if not pin1_position:
|
|
820
|
-
logger.warning(f"Could not determine position of pin {pin1_number} on {component1_ref}")
|
|
821
|
-
return None
|
|
822
|
-
if not pin2_position:
|
|
823
|
-
logger.warning(f"Could not determine position of pin {pin2_number} on {component2_ref}")
|
|
824
|
-
return None
|
|
825
|
-
|
|
826
|
-
# Create the wire
|
|
827
|
-
return self.add_wire(pin1_position, pin2_position)
|
|
828
|
-
|
|
829
|
-
def get_component_pin_position(self, component_ref: str, pin_number: str) -> Optional[Point]:
|
|
830
|
-
"""
|
|
831
|
-
Get the absolute position of a component pin.
|
|
663
|
+
Add wire between two component pins.
|
|
832
664
|
|
|
833
665
|
Args:
|
|
834
|
-
|
|
835
|
-
|
|
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
|
|
836
670
|
|
|
837
671
|
Returns:
|
|
838
|
-
|
|
672
|
+
Wire UUID or None if either pin not found
|
|
839
673
|
"""
|
|
840
|
-
|
|
674
|
+
pin1_pos = self.get_component_pin_position(component1_ref, pin1_number)
|
|
675
|
+
pin2_pos = self.get_component_pin_position(component2_ref, pin2_number)
|
|
841
676
|
|
|
842
|
-
|
|
843
|
-
if not component:
|
|
677
|
+
if pin1_pos is None or pin2_pos is None:
|
|
844
678
|
return None
|
|
845
679
|
|
|
846
|
-
return
|
|
680
|
+
return self.add_wire(pin1_pos, pin2_pos)
|
|
847
681
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
self,
|
|
851
|
-
comp1_ref: str,
|
|
852
|
-
pin1_num: str,
|
|
853
|
-
comp2_ref: str,
|
|
854
|
-
pin2_num: str,
|
|
855
|
-
routing_mode: str = "direct",
|
|
856
|
-
clearance: float = 2.54,
|
|
682
|
+
def connect_pins_with_wire(
|
|
683
|
+
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
857
684
|
) -> Optional[str]:
|
|
858
685
|
"""
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
All positions are snapped to KiCAD's 1.27mm grid for exact electrical connections.
|
|
862
|
-
|
|
863
|
-
Args:
|
|
864
|
-
comp1_ref: First component reference (e.g., 'R1')
|
|
865
|
-
pin1_num: First component pin number (e.g., '1')
|
|
866
|
-
comp2_ref: Second component reference (e.g., 'R2')
|
|
867
|
-
pin2_num: Second component pin number (e.g., '2')
|
|
868
|
-
routing_mode: Routing strategy:
|
|
869
|
-
- "direct": Direct connection through components (default)
|
|
870
|
-
- "manhattan": Manhattan routing with obstacle avoidance
|
|
871
|
-
clearance: Clearance from obstacles in mm (for manhattan mode)
|
|
872
|
-
|
|
873
|
-
Returns:
|
|
874
|
-
UUID of created wire, or None if routing failed
|
|
875
|
-
"""
|
|
876
|
-
from .wire_routing import route_pins_direct, snap_to_kicad_grid
|
|
877
|
-
|
|
878
|
-
# Get pin positions
|
|
879
|
-
pin1_pos = self.get_component_pin_position(comp1_ref, pin1_num)
|
|
880
|
-
pin2_pos = self.get_component_pin_position(comp2_ref, pin2_num)
|
|
881
|
-
|
|
882
|
-
if not pin1_pos or not pin2_pos:
|
|
883
|
-
return None
|
|
884
|
-
|
|
885
|
-
# Ensure positions are grid-snapped
|
|
886
|
-
pin1_pos = snap_to_kicad_grid(pin1_pos)
|
|
887
|
-
pin2_pos = snap_to_kicad_grid(pin2_pos)
|
|
888
|
-
|
|
889
|
-
# Choose routing strategy
|
|
890
|
-
if routing_mode.lower() == "manhattan":
|
|
891
|
-
# Manhattan routing with obstacle avoidance
|
|
892
|
-
from .simple_manhattan import auto_route_with_manhattan
|
|
893
|
-
|
|
894
|
-
# Get component objects
|
|
895
|
-
comp1 = self.components.get(comp1_ref)
|
|
896
|
-
comp2 = self.components.get(comp2_ref)
|
|
897
|
-
|
|
898
|
-
if not comp1 or not comp2:
|
|
899
|
-
logger.warning(f"Component not found: {comp1_ref} or {comp2_ref}")
|
|
900
|
-
return None
|
|
901
|
-
|
|
902
|
-
return auto_route_with_manhattan(
|
|
903
|
-
self,
|
|
904
|
-
comp1,
|
|
905
|
-
pin1_num,
|
|
906
|
-
comp2,
|
|
907
|
-
pin2_num,
|
|
908
|
-
avoid_components=None, # Avoid all other components
|
|
909
|
-
clearance=clearance,
|
|
910
|
-
)
|
|
911
|
-
else:
|
|
912
|
-
# Default direct routing - just connect the pins
|
|
913
|
-
return self.add_wire(pin1_pos, pin2_pos)
|
|
914
|
-
|
|
915
|
-
def are_pins_connected(
|
|
916
|
-
self, comp1_ref: str, pin1_num: str, comp2_ref: str, pin2_num: str
|
|
917
|
-
) -> bool:
|
|
918
|
-
"""
|
|
919
|
-
Detect when two pins are connected via wire routing.
|
|
686
|
+
Connect two component pins with a wire (alias for add_wire_between_pins).
|
|
920
687
|
|
|
921
688
|
Args:
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
|
926
693
|
|
|
927
694
|
Returns:
|
|
928
|
-
|
|
695
|
+
Wire UUID or None if either pin not found
|
|
929
696
|
"""
|
|
930
|
-
from .wire_routing import are_pins_connected
|
|
931
|
-
|
|
932
|
-
return are_pins_connected(self, comp1_ref, pin1_num, comp2_ref, pin2_num)
|
|
933
|
-
|
|
934
|
-
# Legacy method names for compatibility
|
|
935
|
-
def connect_pins_with_wire(
|
|
936
|
-
self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
|
|
937
|
-
) -> Optional[str]:
|
|
938
|
-
"""Legacy alias for add_wire_between_pins."""
|
|
939
697
|
return self.add_wire_between_pins(component1_ref, pin1_number, component2_ref, pin2_number)
|
|
940
698
|
|
|
699
|
+
# Text and label operations (delegated to TextElementManager)
|
|
941
700
|
def add_label(
|
|
942
701
|
self,
|
|
943
702
|
text: str,
|
|
944
703
|
position: Union[Point, Tuple[float, float]],
|
|
945
|
-
|
|
946
|
-
|
|
704
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
705
|
+
rotation: float = 0,
|
|
706
|
+
size: Optional[float] = None,
|
|
947
707
|
uuid: Optional[str] = None,
|
|
948
708
|
) -> str:
|
|
949
709
|
"""
|
|
950
|
-
Add a
|
|
710
|
+
Add a text label to the schematic.
|
|
951
711
|
|
|
952
712
|
Args:
|
|
953
|
-
text: Label text
|
|
713
|
+
text: Label text content
|
|
954
714
|
position: Label position
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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)
|
|
958
719
|
|
|
959
720
|
Returns:
|
|
960
721
|
UUID of created label
|
|
961
722
|
"""
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
label
|
|
968
|
-
uuid=uuid if uuid else str(uuid_module.uuid4()),
|
|
969
|
-
position=position,
|
|
970
|
-
text=text,
|
|
971
|
-
label_type=LabelType.LOCAL,
|
|
972
|
-
rotation=rotation,
|
|
973
|
-
size=size,
|
|
974
|
-
)
|
|
975
|
-
|
|
976
|
-
if "labels" not in self._data:
|
|
977
|
-
self._data["labels"] = []
|
|
978
|
-
|
|
979
|
-
self._data["labels"].append(
|
|
980
|
-
{
|
|
981
|
-
"uuid": label.uuid,
|
|
982
|
-
"position": {"x": label.position.x, "y": label.position.y},
|
|
983
|
-
"text": label.text,
|
|
984
|
-
"rotation": label.rotation,
|
|
985
|
-
"size": label.size,
|
|
986
|
-
}
|
|
987
|
-
)
|
|
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})
|
|
988
729
|
self._modified = True
|
|
989
|
-
|
|
990
|
-
logger.debug(f"Added local label: {text} at {position}")
|
|
991
730
|
return label.uuid
|
|
992
731
|
|
|
993
|
-
def remove_label(self, label_uuid: str) -> bool:
|
|
994
|
-
"""Remove local label by UUID."""
|
|
995
|
-
labels = self._data.get("labels", [])
|
|
996
|
-
for i, label in enumerate(labels):
|
|
997
|
-
if label.get("uuid") == label_uuid:
|
|
998
|
-
del labels[i]
|
|
999
|
-
self._modified = True
|
|
1000
|
-
logger.debug(f"Removed local label: {label_uuid}")
|
|
1001
|
-
return True
|
|
1002
|
-
return False
|
|
1003
|
-
|
|
1004
|
-
def add_sheet(
|
|
1005
|
-
self,
|
|
1006
|
-
name: str,
|
|
1007
|
-
filename: str,
|
|
1008
|
-
position: Union[Point, Tuple[float, float]],
|
|
1009
|
-
size: Union[Point, Tuple[float, float]],
|
|
1010
|
-
stroke_width: float = 0.1524,
|
|
1011
|
-
stroke_type: str = "solid",
|
|
1012
|
-
exclude_from_sim: bool = False,
|
|
1013
|
-
in_bom: bool = True,
|
|
1014
|
-
on_board: bool = True,
|
|
1015
|
-
project_name: str = "",
|
|
1016
|
-
page_number: str = "2",
|
|
1017
|
-
uuid: Optional[str] = None,
|
|
1018
|
-
) -> str:
|
|
1019
|
-
"""
|
|
1020
|
-
Add a hierarchical sheet.
|
|
1021
|
-
|
|
1022
|
-
Args:
|
|
1023
|
-
name: Sheet name (displayed above sheet)
|
|
1024
|
-
filename: Sheet filename (.kicad_sch file)
|
|
1025
|
-
position: Sheet position (top-left corner)
|
|
1026
|
-
size: Sheet size (width, height)
|
|
1027
|
-
stroke_width: Border line width
|
|
1028
|
-
stroke_type: Border line type
|
|
1029
|
-
exclude_from_sim: Exclude from simulation
|
|
1030
|
-
in_bom: Include in BOM
|
|
1031
|
-
on_board: Include on board
|
|
1032
|
-
project_name: Project name for instances
|
|
1033
|
-
page_number: Page number for instances
|
|
1034
|
-
uuid: Optional UUID (auto-generated if None)
|
|
1035
|
-
|
|
1036
|
-
Returns:
|
|
1037
|
-
UUID of created sheet
|
|
1038
|
-
"""
|
|
1039
|
-
if isinstance(position, tuple):
|
|
1040
|
-
position = Point(position[0], position[1])
|
|
1041
|
-
if isinstance(size, tuple):
|
|
1042
|
-
size = Point(size[0], size[1])
|
|
1043
|
-
|
|
1044
|
-
import uuid as uuid_module
|
|
1045
|
-
|
|
1046
|
-
sheet = Sheet(
|
|
1047
|
-
uuid=uuid if uuid else str(uuid_module.uuid4()),
|
|
1048
|
-
position=position,
|
|
1049
|
-
size=size,
|
|
1050
|
-
name=name,
|
|
1051
|
-
filename=filename,
|
|
1052
|
-
exclude_from_sim=exclude_from_sim,
|
|
1053
|
-
in_bom=in_bom,
|
|
1054
|
-
on_board=on_board,
|
|
1055
|
-
stroke_width=stroke_width,
|
|
1056
|
-
stroke_type=stroke_type,
|
|
1057
|
-
)
|
|
1058
|
-
|
|
1059
|
-
if "sheets" not in self._data:
|
|
1060
|
-
self._data["sheets"] = []
|
|
1061
|
-
|
|
1062
|
-
self._data["sheets"].append(
|
|
1063
|
-
{
|
|
1064
|
-
"uuid": sheet.uuid,
|
|
1065
|
-
"position": {"x": sheet.position.x, "y": sheet.position.y},
|
|
1066
|
-
"size": {"width": sheet.size.x, "height": sheet.size.y},
|
|
1067
|
-
"name": sheet.name,
|
|
1068
|
-
"filename": sheet.filename,
|
|
1069
|
-
"exclude_from_sim": sheet.exclude_from_sim,
|
|
1070
|
-
"in_bom": sheet.in_bom,
|
|
1071
|
-
"on_board": sheet.on_board,
|
|
1072
|
-
"dnp": sheet.dnp,
|
|
1073
|
-
"fields_autoplaced": sheet.fields_autoplaced,
|
|
1074
|
-
"stroke_width": sheet.stroke_width,
|
|
1075
|
-
"stroke_type": sheet.stroke_type,
|
|
1076
|
-
"fill_color": sheet.fill_color,
|
|
1077
|
-
"pins": [], # Sheet pins added separately
|
|
1078
|
-
"project_name": project_name,
|
|
1079
|
-
"page_number": page_number,
|
|
1080
|
-
}
|
|
1081
|
-
)
|
|
1082
|
-
self._modified = True
|
|
1083
|
-
|
|
1084
|
-
logger.debug(f"Added hierarchical sheet: {name} ({filename}) at {position}")
|
|
1085
|
-
return sheet.uuid
|
|
1086
|
-
|
|
1087
|
-
def add_sheet_pin(
|
|
1088
|
-
self,
|
|
1089
|
-
sheet_uuid: str,
|
|
1090
|
-
name: str,
|
|
1091
|
-
pin_type: str = "input",
|
|
1092
|
-
position: Union[Point, Tuple[float, float]] = (0, 0),
|
|
1093
|
-
rotation: float = 0,
|
|
1094
|
-
size: float = 1.27,
|
|
1095
|
-
justify: str = "right",
|
|
1096
|
-
uuid: Optional[str] = None,
|
|
1097
|
-
) -> str:
|
|
1098
|
-
"""
|
|
1099
|
-
Add a pin to a hierarchical sheet.
|
|
1100
|
-
|
|
1101
|
-
Args:
|
|
1102
|
-
sheet_uuid: UUID of the sheet to add pin to
|
|
1103
|
-
name: Pin name (NET1, NET2, etc.)
|
|
1104
|
-
pin_type: Pin type (input, output, bidirectional, etc.)
|
|
1105
|
-
position: Pin position relative to sheet
|
|
1106
|
-
rotation: Pin rotation in degrees
|
|
1107
|
-
size: Font size for pin label
|
|
1108
|
-
justify: Text justification (left, right, center)
|
|
1109
|
-
uuid: Optional UUID (auto-generated if None)
|
|
1110
|
-
|
|
1111
|
-
Returns:
|
|
1112
|
-
UUID of created sheet pin
|
|
1113
|
-
"""
|
|
1114
|
-
if isinstance(position, tuple):
|
|
1115
|
-
position = Point(position[0], position[1])
|
|
1116
|
-
|
|
1117
|
-
import uuid as uuid_module
|
|
1118
|
-
|
|
1119
|
-
pin_uuid = uuid if uuid else str(uuid_module.uuid4())
|
|
1120
|
-
|
|
1121
|
-
# Find the sheet in the data
|
|
1122
|
-
sheets = self._data.get("sheets", [])
|
|
1123
|
-
for sheet in sheets:
|
|
1124
|
-
if sheet.get("uuid") == sheet_uuid:
|
|
1125
|
-
# Add pin to the sheet's pins list
|
|
1126
|
-
pin_data = {
|
|
1127
|
-
"uuid": pin_uuid,
|
|
1128
|
-
"name": name,
|
|
1129
|
-
"pin_type": pin_type,
|
|
1130
|
-
"position": {"x": position.x, "y": position.y},
|
|
1131
|
-
"rotation": rotation,
|
|
1132
|
-
"size": size,
|
|
1133
|
-
"justify": justify,
|
|
1134
|
-
}
|
|
1135
|
-
sheet["pins"].append(pin_data)
|
|
1136
|
-
self._modified = True
|
|
1137
|
-
|
|
1138
|
-
logger.debug(f"Added sheet pin: {name} ({pin_type}) to sheet {sheet_uuid}")
|
|
1139
|
-
return pin_uuid
|
|
1140
|
-
|
|
1141
|
-
raise ValueError(f"Sheet with UUID '{sheet_uuid}' not found")
|
|
1142
|
-
|
|
1143
732
|
def add_text(
|
|
1144
733
|
self,
|
|
1145
734
|
text: str,
|
|
@@ -1147,49 +736,30 @@ class Schematic:
|
|
|
1147
736
|
rotation: float = 0.0,
|
|
1148
737
|
size: float = 1.27,
|
|
1149
738
|
exclude_from_sim: bool = False,
|
|
739
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
1150
740
|
) -> str:
|
|
1151
741
|
"""
|
|
1152
|
-
Add
|
|
742
|
+
Add free text annotation to the schematic.
|
|
1153
743
|
|
|
1154
744
|
Args:
|
|
1155
745
|
text: Text content
|
|
1156
746
|
position: Text position
|
|
1157
747
|
rotation: Text rotation in degrees
|
|
1158
|
-
size:
|
|
1159
|
-
exclude_from_sim:
|
|
748
|
+
size: Text size
|
|
749
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
750
|
+
effects: Text effects
|
|
1160
751
|
|
|
1161
752
|
Returns:
|
|
1162
|
-
UUID of created text
|
|
753
|
+
UUID of created text
|
|
1163
754
|
"""
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
text_element = Text(
|
|
1168
|
-
uuid=str(uuid.uuid4()),
|
|
1169
|
-
position=position,
|
|
1170
|
-
text=text,
|
|
1171
|
-
rotation=rotation,
|
|
1172
|
-
size=size,
|
|
1173
|
-
exclude_from_sim=exclude_from_sim,
|
|
1174
|
-
)
|
|
1175
|
-
|
|
1176
|
-
if "texts" not in self._data:
|
|
1177
|
-
self._data["texts"] = []
|
|
1178
|
-
|
|
1179
|
-
self._data["texts"].append(
|
|
1180
|
-
{
|
|
1181
|
-
"uuid": text_element.uuid,
|
|
1182
|
-
"position": {"x": text_element.position.x, "y": text_element.position.y},
|
|
1183
|
-
"text": text_element.text,
|
|
1184
|
-
"rotation": text_element.rotation,
|
|
1185
|
-
"size": text_element.size,
|
|
1186
|
-
"exclude_from_sim": text_element.exclude_from_sim,
|
|
1187
|
-
}
|
|
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
|
|
1188
758
|
)
|
|
759
|
+
self._sync_texts_to_data() # Sync immediately
|
|
760
|
+
self._format_sync_manager.mark_dirty("text", "add", {"uuid": text_elem.uuid})
|
|
1189
761
|
self._modified = True
|
|
1190
|
-
|
|
1191
|
-
logger.debug(f"Added text: '{text}' at {position}")
|
|
1192
|
-
return text_element.uuid
|
|
762
|
+
return text_elem.uuid
|
|
1193
763
|
|
|
1194
764
|
def add_text_box(
|
|
1195
765
|
self,
|
|
@@ -1198,44 +768,42 @@ class Schematic:
|
|
|
1198
768
|
size: Union[Point, Tuple[float, float]],
|
|
1199
769
|
rotation: float = 0.0,
|
|
1200
770
|
font_size: float = 1.27,
|
|
1201
|
-
margins: Tuple[float, float, float, float] =
|
|
1202
|
-
stroke_width: float =
|
|
771
|
+
margins: Optional[Tuple[float, float, float, float]] = None,
|
|
772
|
+
stroke_width: Optional[float] = None,
|
|
1203
773
|
stroke_type: str = "solid",
|
|
1204
774
|
fill_type: str = "none",
|
|
1205
775
|
justify_horizontal: str = "left",
|
|
1206
776
|
justify_vertical: str = "top",
|
|
1207
777
|
exclude_from_sim: bool = False,
|
|
778
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
779
|
+
stroke: Optional[Dict[str, Any]] = None,
|
|
1208
780
|
) -> str:
|
|
1209
781
|
"""
|
|
1210
|
-
Add a text box
|
|
782
|
+
Add a text box with border to the schematic.
|
|
1211
783
|
|
|
1212
784
|
Args:
|
|
1213
785
|
text: Text content
|
|
1214
|
-
position:
|
|
1215
|
-
size:
|
|
786
|
+
position: Top-left position
|
|
787
|
+
size: Box size (width, height)
|
|
1216
788
|
rotation: Text rotation in degrees
|
|
1217
|
-
font_size:
|
|
1218
|
-
margins:
|
|
1219
|
-
stroke_width: Border
|
|
1220
|
-
stroke_type: Border
|
|
1221
|
-
fill_type: Fill type (none,
|
|
1222
|
-
justify_horizontal: Horizontal
|
|
1223
|
-
justify_vertical: Vertical
|
|
1224
|
-
exclude_from_sim:
|
|
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)
|
|
1225
799
|
|
|
1226
800
|
Returns:
|
|
1227
|
-
UUID of created text box
|
|
801
|
+
UUID of created text box
|
|
1228
802
|
"""
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
if isinstance(size, tuple):
|
|
1232
|
-
size = Point(size[0], size[1])
|
|
1233
|
-
|
|
1234
|
-
text_box = TextBox(
|
|
1235
|
-
uuid=str(uuid.uuid4()),
|
|
803
|
+
text_box_uuid = self._text_element_manager.add_text_box(
|
|
804
|
+
text=text,
|
|
1236
805
|
position=position,
|
|
1237
806
|
size=size,
|
|
1238
|
-
text=text,
|
|
1239
807
|
rotation=rotation,
|
|
1240
808
|
font_size=font_size,
|
|
1241
809
|
margins=margins,
|
|
@@ -1245,236 +813,366 @@ class Schematic:
|
|
|
1245
813
|
justify_horizontal=justify_horizontal,
|
|
1246
814
|
justify_vertical=justify_vertical,
|
|
1247
815
|
exclude_from_sim=exclude_from_sim,
|
|
816
|
+
effects=effects,
|
|
817
|
+
stroke=stroke,
|
|
1248
818
|
)
|
|
1249
|
-
|
|
1250
|
-
if "text_boxes" not in self._data:
|
|
1251
|
-
self._data["text_boxes"] = []
|
|
1252
|
-
|
|
1253
|
-
self._data["text_boxes"].append(
|
|
1254
|
-
{
|
|
1255
|
-
"uuid": text_box.uuid,
|
|
1256
|
-
"position": {"x": text_box.position.x, "y": text_box.position.y},
|
|
1257
|
-
"size": {"width": text_box.size.x, "height": text_box.size.y},
|
|
1258
|
-
"text": text_box.text,
|
|
1259
|
-
"rotation": text_box.rotation,
|
|
1260
|
-
"font_size": text_box.font_size,
|
|
1261
|
-
"margins": text_box.margins,
|
|
1262
|
-
"stroke_width": text_box.stroke_width,
|
|
1263
|
-
"stroke_type": text_box.stroke_type,
|
|
1264
|
-
"fill_type": text_box.fill_type,
|
|
1265
|
-
"justify_horizontal": text_box.justify_horizontal,
|
|
1266
|
-
"justify_vertical": text_box.justify_vertical,
|
|
1267
|
-
"exclude_from_sim": text_box.exclude_from_sim,
|
|
1268
|
-
}
|
|
1269
|
-
)
|
|
819
|
+
self._format_sync_manager.mark_dirty("text_box", "add", {"uuid": text_box_uuid})
|
|
1270
820
|
self._modified = True
|
|
821
|
+
return text_box_uuid
|
|
1271
822
|
|
|
1272
|
-
|
|
1273
|
-
return text_box.uuid
|
|
1274
|
-
|
|
1275
|
-
def add_image(
|
|
823
|
+
def add_hierarchical_label(
|
|
1276
824
|
self,
|
|
825
|
+
text: str,
|
|
1277
826
|
position: Union[Point, Tuple[float, float]],
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
827
|
+
shape: str = "input",
|
|
828
|
+
rotation: float = 0.0,
|
|
829
|
+
size: float = 1.27,
|
|
830
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
1281
831
|
) -> str:
|
|
1282
832
|
"""
|
|
1283
|
-
Add
|
|
833
|
+
Add a hierarchical label for sheet connections.
|
|
1284
834
|
|
|
1285
835
|
Args:
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
1290
842
|
|
|
1291
843
|
Returns:
|
|
1292
|
-
UUID of created
|
|
844
|
+
UUID of created hierarchical label
|
|
1293
845
|
"""
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
|
1298
852
|
|
|
1299
|
-
|
|
853
|
+
def add_global_label(
|
|
854
|
+
self,
|
|
855
|
+
text: str,
|
|
856
|
+
position: Union[Point, Tuple[float, float]],
|
|
857
|
+
shape: str = "input",
|
|
858
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
859
|
+
) -> str:
|
|
860
|
+
"""
|
|
861
|
+
Add a global label for project-wide connections.
|
|
1300
862
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
position
|
|
1304
|
-
|
|
1305
|
-
|
|
863
|
+
Args:
|
|
864
|
+
text: Label text
|
|
865
|
+
position: Label position
|
|
866
|
+
shape: Shape type
|
|
867
|
+
effects: Text effects
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
UUID of created global label
|
|
871
|
+
"""
|
|
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
|
|
876
|
+
|
|
877
|
+
def remove_label(self, label_uuid: str) -> bool:
|
|
878
|
+
"""
|
|
879
|
+
Remove a label by UUID.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
label_uuid: UUID of label to remove
|
|
883
|
+
|
|
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
|
|
893
|
+
|
|
894
|
+
def remove_hierarchical_label(self, label_uuid: str) -> bool:
|
|
895
|
+
"""
|
|
896
|
+
Remove a hierarchical label by UUID.
|
|
897
|
+
|
|
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(
|
|
915
|
+
self,
|
|
916
|
+
name: str,
|
|
917
|
+
filename: str,
|
|
918
|
+
position: Union[Point, Tuple[float, float]],
|
|
919
|
+
size: Union[Point, Tuple[float, float]],
|
|
920
|
+
stroke_width: Optional[float] = None,
|
|
921
|
+
stroke_type: str = "solid",
|
|
922
|
+
project_name: Optional[str] = None,
|
|
923
|
+
page_number: Optional[str] = None,
|
|
924
|
+
uuid: Optional[str] = None,
|
|
925
|
+
) -> str:
|
|
926
|
+
"""
|
|
927
|
+
Add a hierarchical sheet to the schematic.
|
|
928
|
+
|
|
929
|
+
Args:
|
|
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
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
UUID of created sheet
|
|
942
|
+
"""
|
|
943
|
+
sheet_uuid = self._sheet_manager.add_sheet(
|
|
944
|
+
name,
|
|
945
|
+
filename,
|
|
946
|
+
position,
|
|
947
|
+
size,
|
|
948
|
+
uuid_str=uuid,
|
|
949
|
+
stroke_width=stroke_width,
|
|
950
|
+
stroke_type=stroke_type,
|
|
951
|
+
project_name=project_name,
|
|
952
|
+
page_number=page_number,
|
|
1306
953
|
)
|
|
954
|
+
self._format_sync_manager.mark_dirty("sheet", "add", {"uuid": sheet_uuid})
|
|
955
|
+
self._modified = True
|
|
956
|
+
return sheet_uuid
|
|
1307
957
|
|
|
1308
|
-
|
|
1309
|
-
|
|
958
|
+
def add_sheet_pin(
|
|
959
|
+
self,
|
|
960
|
+
sheet_uuid: str,
|
|
961
|
+
name: str,
|
|
962
|
+
pin_type: str,
|
|
963
|
+
position: Union[Point, Tuple[float, float]],
|
|
964
|
+
rotation: float = 0,
|
|
965
|
+
justify: str = "left",
|
|
966
|
+
uuid: Optional[str] = None,
|
|
967
|
+
) -> str:
|
|
968
|
+
"""
|
|
969
|
+
Add a pin to a hierarchical sheet.
|
|
1310
970
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
971
|
+
Args:
|
|
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
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
UUID of created sheet pin
|
|
982
|
+
"""
|
|
983
|
+
pin_uuid = self._sheet_manager.add_sheet_pin(
|
|
984
|
+
sheet_uuid, name, pin_type, position, rotation, justify, uuid_str=uuid
|
|
1318
985
|
)
|
|
986
|
+
self._format_sync_manager.mark_dirty("sheet", "modify", {"uuid": sheet_uuid})
|
|
1319
987
|
self._modified = True
|
|
988
|
+
return pin_uuid
|
|
1320
989
|
|
|
1321
|
-
|
|
1322
|
-
|
|
990
|
+
def remove_sheet(self, sheet_uuid: str) -> bool:
|
|
991
|
+
"""
|
|
992
|
+
Remove a sheet by UUID.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
sheet_uuid: UUID of sheet to remove
|
|
996
|
+
|
|
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
|
|
1323
1005
|
|
|
1006
|
+
# Graphics operations (delegated to GraphicsManager)
|
|
1324
1007
|
def add_rectangle(
|
|
1325
1008
|
self,
|
|
1326
1009
|
start: Union[Point, Tuple[float, float]],
|
|
1327
1010
|
end: Union[Point, Tuple[float, float]],
|
|
1328
|
-
stroke_width: float = 0.
|
|
1329
|
-
stroke_type: str = "
|
|
1330
|
-
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,
|
|
1331
1016
|
) -> str:
|
|
1332
1017
|
"""
|
|
1333
|
-
Add a
|
|
1018
|
+
Add a rectangle to the schematic.
|
|
1334
1019
|
|
|
1335
1020
|
Args:
|
|
1336
|
-
start:
|
|
1337
|
-
end:
|
|
1338
|
-
stroke_width:
|
|
1339
|
-
stroke_type:
|
|
1340
|
-
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)
|
|
1341
1028
|
|
|
1342
1029
|
Returns:
|
|
1343
|
-
UUID of created rectangle
|
|
1030
|
+
UUID of created rectangle
|
|
1344
1031
|
"""
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
if
|
|
1348
|
-
|
|
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
|
|
1349
1036
|
|
|
1350
|
-
|
|
1037
|
+
fill = {"type": fill_type}
|
|
1038
|
+
if fill_color:
|
|
1039
|
+
fill["color"] = fill_color
|
|
1351
1040
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
start=start,
|
|
1355
|
-
end=end,
|
|
1356
|
-
stroke_width=stroke_width,
|
|
1357
|
-
stroke_type=stroke_type,
|
|
1358
|
-
fill_type=fill_type
|
|
1359
|
-
)
|
|
1360
|
-
|
|
1361
|
-
if "rectangles" not in self._data:
|
|
1362
|
-
self._data["rectangles"] = []
|
|
1363
|
-
|
|
1364
|
-
self._data["rectangles"].append({
|
|
1365
|
-
"uuid": rectangle.uuid,
|
|
1366
|
-
"start": {"x": rectangle.start.x, "y": rectangle.start.y},
|
|
1367
|
-
"end": {"x": rectangle.end.x, "y": rectangle.end.y},
|
|
1368
|
-
"stroke_width": rectangle.stroke_width,
|
|
1369
|
-
"stroke_type": rectangle.stroke_type,
|
|
1370
|
-
"fill_type": rectangle.fill_type
|
|
1371
|
-
})
|
|
1041
|
+
rect_uuid = self._graphics_manager.add_rectangle(start, end, stroke, fill)
|
|
1042
|
+
self._format_sync_manager.mark_dirty("rectangle", "add", {"uuid": rect_uuid})
|
|
1372
1043
|
self._modified = True
|
|
1044
|
+
return rect_uuid
|
|
1045
|
+
|
|
1046
|
+
def remove_rectangle(self, rect_uuid: str) -> bool:
|
|
1047
|
+
"""
|
|
1048
|
+
Remove a rectangle by UUID.
|
|
1373
1049
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1050
|
+
Args:
|
|
1051
|
+
rect_uuid: UUID of rectangle to remove
|
|
1376
1052
|
|
|
1377
|
-
|
|
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(
|
|
1378
1063
|
self,
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
comments: Optional[Dict[int, str]] = None,
|
|
1384
|
-
):
|
|
1064
|
+
position: Union[Point, Tuple[float, float]],
|
|
1065
|
+
scale: float = 1.0,
|
|
1066
|
+
data: Optional[str] = None,
|
|
1067
|
+
) -> str:
|
|
1385
1068
|
"""
|
|
1386
|
-
|
|
1069
|
+
Add an image to the schematic.
|
|
1387
1070
|
|
|
1388
1071
|
Args:
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
company: Company name
|
|
1393
|
-
comments: Numbered comments (1, 2, 3, etc.)
|
|
1394
|
-
"""
|
|
1395
|
-
if comments is None:
|
|
1396
|
-
comments = {}
|
|
1072
|
+
position: Image position
|
|
1073
|
+
scale: Image scale factor
|
|
1074
|
+
data: Base64 encoded image data
|
|
1397
1075
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
"comments": comments,
|
|
1404
|
-
}
|
|
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})
|
|
1405
1081
|
self._modified = True
|
|
1082
|
+
return image_uuid
|
|
1083
|
+
|
|
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)
|
|
1406
1106
|
|
|
1407
|
-
|
|
1107
|
+
return self.add_rectangle(start, end, stroke_width=stroke_width, stroke_type=stroke_type)
|
|
1408
1108
|
|
|
1409
1109
|
def draw_bounding_box(
|
|
1410
1110
|
self,
|
|
1411
1111
|
bbox: "BoundingBox",
|
|
1412
|
-
stroke_width: float = 0,
|
|
1413
|
-
stroke_color: str = None,
|
|
1414
|
-
stroke_type: str = "
|
|
1415
|
-
exclude_from_sim: bool = False,
|
|
1112
|
+
stroke_width: float = 0.127,
|
|
1113
|
+
stroke_color: Optional[str] = None,
|
|
1114
|
+
stroke_type: str = "solid",
|
|
1416
1115
|
) -> str:
|
|
1417
1116
|
"""
|
|
1418
|
-
Draw a
|
|
1117
|
+
Draw a single bounding box as a rectangle.
|
|
1419
1118
|
|
|
1420
1119
|
Args:
|
|
1421
1120
|
bbox: BoundingBox to draw
|
|
1422
|
-
stroke_width: Line width
|
|
1423
|
-
stroke_color:
|
|
1424
|
-
stroke_type:
|
|
1425
|
-
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.)
|
|
1426
1124
|
|
|
1427
1125
|
Returns:
|
|
1428
|
-
UUID of created rectangle
|
|
1126
|
+
UUID of created rectangle
|
|
1429
1127
|
"""
|
|
1430
|
-
# Import BoundingBox type
|
|
1431
1128
|
from .component_bounds import BoundingBox
|
|
1432
1129
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
# Create rectangle data structure in KiCAD dictionary format
|
|
1436
|
-
stroke_data = {"width": stroke_width, "type": stroke_type}
|
|
1437
|
-
|
|
1438
|
-
# Add color if specified
|
|
1130
|
+
# Convert color name to RGBA tuple if provided
|
|
1131
|
+
stroke_rgba = None
|
|
1439
1132
|
if stroke_color:
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
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))
|
|
1453
1145
|
|
|
1454
|
-
|
|
1455
|
-
|
|
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
|
+
)
|
|
1456
1154
|
|
|
1457
|
-
logger.debug(f"Drew bounding box
|
|
1155
|
+
logger.debug(f"Drew bounding box: {bbox}")
|
|
1458
1156
|
return rect_uuid
|
|
1459
1157
|
|
|
1460
1158
|
def draw_component_bounding_boxes(
|
|
1461
1159
|
self,
|
|
1462
1160
|
include_properties: bool = False,
|
|
1463
|
-
stroke_width: float = 0.
|
|
1464
|
-
stroke_color: str = "
|
|
1465
|
-
stroke_type: str = "
|
|
1161
|
+
stroke_width: float = 0.127,
|
|
1162
|
+
stroke_color: str = "green",
|
|
1163
|
+
stroke_type: str = "solid",
|
|
1466
1164
|
) -> List[str]:
|
|
1467
1165
|
"""
|
|
1468
|
-
Draw bounding boxes for all components
|
|
1166
|
+
Draw bounding boxes for all components.
|
|
1469
1167
|
|
|
1470
1168
|
Args:
|
|
1471
|
-
include_properties:
|
|
1472
|
-
stroke_width: Line width
|
|
1473
|
-
stroke_color:
|
|
1474
|
-
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
|
|
1475
1173
|
|
|
1476
1174
|
Returns:
|
|
1477
|
-
List of UUIDs
|
|
1175
|
+
List of rectangle UUIDs created
|
|
1478
1176
|
"""
|
|
1479
1177
|
from .component_bounds import get_component_bounding_box
|
|
1480
1178
|
|
|
@@ -1488,67 +1186,157 @@ class Schematic:
|
|
|
1488
1186
|
logger.info(f"Drew {len(uuids)} component bounding boxes")
|
|
1489
1187
|
return uuids
|
|
1490
1188
|
|
|
1491
|
-
#
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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")
|
|
1509
1210
|
self._modified = True
|
|
1510
|
-
logger.info("Cleared schematic")
|
|
1511
1211
|
|
|
1512
|
-
def
|
|
1513
|
-
"""
|
|
1514
|
-
|
|
1212
|
+
def set_paper_size(self, paper: str) -> None:
|
|
1213
|
+
"""
|
|
1214
|
+
Set paper size for the schematic.
|
|
1215
|
+
|
|
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
|
|
1222
|
+
|
|
1223
|
+
# Validation (enhanced with ValidationManager)
|
|
1224
|
+
def validate(self) -> List[ValidationIssue]:
|
|
1225
|
+
"""
|
|
1226
|
+
Perform comprehensive schematic validation.
|
|
1227
|
+
|
|
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()
|
|
1515
1233
|
|
|
1516
|
-
|
|
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 = []
|
|
1517
1240
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1241
|
+
# Combine issues (remove duplicates based on message)
|
|
1242
|
+
all_issues = manager_issues + legacy_issues
|
|
1243
|
+
unique_issues = []
|
|
1244
|
+
seen_messages = set()
|
|
1521
1245
|
|
|
1522
|
-
|
|
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)
|
|
1523
1250
|
|
|
1524
|
-
|
|
1525
|
-
def rebuild_indexes(self):
|
|
1526
|
-
"""Rebuild internal indexes for performance."""
|
|
1527
|
-
# This would rebuild component indexes, etc.
|
|
1528
|
-
logger.info("Rebuilt schematic indexes")
|
|
1251
|
+
return unique_issues
|
|
1529
1252
|
|
|
1530
|
-
def
|
|
1531
|
-
"""
|
|
1532
|
-
|
|
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)
|
|
1533
1262
|
|
|
1263
|
+
# Statistics and information
|
|
1264
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
1265
|
+
"""Get comprehensive schematic statistics."""
|
|
1534
1266
|
return {
|
|
1535
|
-
"
|
|
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": {
|
|
1536
1274
|
"operation_count": self._operation_count,
|
|
1537
|
-
"
|
|
1538
|
-
"
|
|
1539
|
-
|
|
1540
|
-
(self._total_operation_time / self._operation_count * 1000)
|
|
1541
|
-
if self._operation_count > 0
|
|
1542
|
-
else 0
|
|
1543
|
-
),
|
|
1544
|
-
2,
|
|
1545
|
-
),
|
|
1275
|
+
"total_operation_time": self._total_operation_time,
|
|
1276
|
+
"modified": self.modified,
|
|
1277
|
+
"last_save_time": self._last_save_time,
|
|
1546
1278
|
},
|
|
1547
|
-
"components": self._components.get_statistics(),
|
|
1548
|
-
"symbol_cache": cache_stats,
|
|
1549
1279
|
}
|
|
1550
1280
|
|
|
1551
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)
|
|
1552
1340
|
def _sync_components_to_data(self):
|
|
1553
1341
|
"""Sync component collection state back to data structure."""
|
|
1554
1342
|
self._data["components"] = [comp._data.__dict__ for comp in self._components]
|
|
@@ -1559,112 +1347,23 @@ class Schematic:
|
|
|
1559
1347
|
|
|
1560
1348
|
for comp in self._components:
|
|
1561
1349
|
if comp.lib_id and comp.lib_id not in lib_symbols:
|
|
1562
|
-
logger.debug(f"🔧 SCHEMATIC: Processing component {comp.lib_id}")
|
|
1563
|
-
|
|
1564
1350
|
# Get the actual symbol definition
|
|
1565
1351
|
symbol_def = cache.get_symbol(comp.lib_id)
|
|
1352
|
+
|
|
1566
1353
|
if symbol_def:
|
|
1567
|
-
|
|
1568
|
-
lib_symbols[comp.lib_id] =
|
|
1569
|
-
symbol_def, comp.lib_id
|
|
1570
|
-
)
|
|
1571
|
-
|
|
1572
|
-
# Check if this symbol extends another symbol using multiple methods
|
|
1573
|
-
extends_parent = None
|
|
1574
|
-
|
|
1575
|
-
# Method 1: Check raw_kicad_data
|
|
1576
|
-
if hasattr(symbol_def, "raw_kicad_data") and symbol_def.raw_kicad_data:
|
|
1577
|
-
extends_parent = self._check_symbol_extends(symbol_def.raw_kicad_data)
|
|
1578
|
-
logger.debug(
|
|
1579
|
-
f"🔧 SCHEMATIC: Checked raw_kicad_data for {comp.lib_id}, extends: {extends_parent}"
|
|
1580
|
-
)
|
|
1581
|
-
|
|
1582
|
-
# Method 2: Check raw_data attribute
|
|
1583
|
-
if not extends_parent and hasattr(symbol_def, "__dict__"):
|
|
1584
|
-
for attr_name, attr_value in symbol_def.__dict__.items():
|
|
1585
|
-
if attr_name == "raw_data":
|
|
1586
|
-
logger.debug(
|
|
1587
|
-
f"🔧 SCHEMATIC: Checking raw_data for extends: {type(attr_value)}"
|
|
1588
|
-
)
|
|
1589
|
-
extends_parent = self._check_symbol_extends(attr_value)
|
|
1590
|
-
if extends_parent:
|
|
1591
|
-
logger.debug(
|
|
1592
|
-
f"🔧 SCHEMATIC: Found extends in raw_data: {extends_parent}"
|
|
1593
|
-
)
|
|
1594
|
-
|
|
1595
|
-
# Method 3: Check the extends attribute directly
|
|
1596
|
-
if not extends_parent and hasattr(symbol_def, "extends"):
|
|
1597
|
-
extends_parent = symbol_def.extends
|
|
1598
|
-
logger.debug(f"🔧 SCHEMATIC: Found extends attribute: {extends_parent}")
|
|
1599
|
-
|
|
1600
|
-
if extends_parent:
|
|
1601
|
-
# Load the parent symbol too
|
|
1602
|
-
parent_lib_id = f"{comp.lib_id.split(':')[0]}:{extends_parent}"
|
|
1603
|
-
logger.debug(f"🔧 SCHEMATIC: Loading parent symbol: {parent_lib_id}")
|
|
1604
|
-
|
|
1605
|
-
if parent_lib_id not in lib_symbols:
|
|
1606
|
-
parent_symbol_def = cache.get_symbol(parent_lib_id)
|
|
1607
|
-
if parent_symbol_def:
|
|
1608
|
-
lib_symbols[parent_lib_id] = self._convert_symbol_to_kicad_format(
|
|
1609
|
-
parent_symbol_def, parent_lib_id
|
|
1610
|
-
)
|
|
1611
|
-
logger.debug(
|
|
1612
|
-
f"🔧 SCHEMATIC: Successfully loaded parent symbol: {parent_lib_id} for {comp.lib_id}"
|
|
1613
|
-
)
|
|
1614
|
-
else:
|
|
1615
|
-
logger.warning(
|
|
1616
|
-
f"🔧 SCHEMATIC: Failed to load parent symbol: {parent_lib_id}"
|
|
1617
|
-
)
|
|
1618
|
-
else:
|
|
1619
|
-
logger.debug(
|
|
1620
|
-
f"🔧 SCHEMATIC: Parent symbol {parent_lib_id} already loaded"
|
|
1621
|
-
)
|
|
1622
|
-
else:
|
|
1623
|
-
logger.debug(f"🔧 SCHEMATIC: No extends found for {comp.lib_id}")
|
|
1624
|
-
else:
|
|
1625
|
-
# Fallback for unknown symbols
|
|
1626
|
-
logger.warning(
|
|
1627
|
-
f"🔧 SCHEMATIC: Failed to load symbol {comp.lib_id}, using fallback"
|
|
1628
|
-
)
|
|
1629
|
-
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
|
|
1630
1356
|
|
|
1631
1357
|
self._data["lib_symbols"] = lib_symbols
|
|
1632
1358
|
|
|
1633
|
-
#
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
logger.debug(f"🔧 FINAL: - {sym_id}")
|
|
1637
|
-
# Check if this symbol has extends
|
|
1638
|
-
sym_data = lib_symbols[sym_id]
|
|
1639
|
-
if isinstance(sym_data, list) and len(sym_data) > 2:
|
|
1640
|
-
for item in sym_data[1:]:
|
|
1641
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1642
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1643
|
-
logger.debug(f"🔧 FINAL: - {sym_id} extends {item[1]}")
|
|
1644
|
-
break
|
|
1645
|
-
|
|
1646
|
-
def _check_symbol_extends(self, symbol_data: Any) -> Optional[str]:
|
|
1647
|
-
"""Check if symbol extends another symbol and return parent name."""
|
|
1648
|
-
logger.debug(f"🔧 EXTENDS: Checking symbol data type: {type(symbol_data)}")
|
|
1359
|
+
# Update sheet instances
|
|
1360
|
+
if not self._data["sheet_instances"]:
|
|
1361
|
+
self._data["sheet_instances"] = [{"path": "/", "page": "1"}]
|
|
1649
1362
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
logger.debug(f"🔧 EXTENDS: Checking {len(symbol_data)} items for extends directive")
|
|
1655
|
-
|
|
1656
|
-
for i, item in enumerate(symbol_data[1:], 1):
|
|
1657
|
-
logger.debug(
|
|
1658
|
-
f"🔧 EXTENDS: Item {i}: {type(item)} - {item if not isinstance(item, list) else f'list[{len(item)}]'}"
|
|
1659
|
-
)
|
|
1660
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1661
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1662
|
-
parent_name = str(item[1]).strip('"')
|
|
1663
|
-
logger.debug(f"🔧 EXTENDS: Found extends directive: {parent_name}")
|
|
1664
|
-
return parent_name
|
|
1665
|
-
|
|
1666
|
-
logger.debug(f"🔧 EXTENDS: No extends directive found")
|
|
1667
|
-
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"]
|
|
1668
1367
|
|
|
1669
1368
|
def _sync_wires_to_data(self):
|
|
1670
1369
|
"""Sync wire collection state back to data structure."""
|
|
@@ -1695,156 +1394,153 @@ class Schematic:
|
|
|
1695
1394
|
|
|
1696
1395
|
self._data["junctions"] = junction_data
|
|
1697
1396
|
|
|
1698
|
-
def
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
"
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
"
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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,
|
|
1742
1452
|
},
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
|
|
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)
|
|
1746
1469
|
|
|
1747
|
-
|
|
1748
|
-
"""Convert raw parsed KiCAD symbol data to dictionary format for S-expression generation."""
|
|
1749
|
-
import copy
|
|
1470
|
+
self._data["nets"] = net_data
|
|
1750
1471
|
|
|
1751
|
-
|
|
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
|
|
1752
1477
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1478
|
+
# Check if raw data already contains instances with project info
|
|
1479
|
+
project_refs_found = []
|
|
1755
1480
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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}]")
|
|
1759
1489
|
|
|
1760
|
-
|
|
1761
|
-
logger.debug(f"🔧 CONVERT: Processing {len(modified_data)} items for {lib_id}")
|
|
1762
|
-
for i, item in enumerate(modified_data[1:], 1):
|
|
1763
|
-
if isinstance(item, list) and len(item) >= 2:
|
|
1764
|
-
logger.debug(
|
|
1765
|
-
f"🔧 CONVERT: Item {i}: {item[0]} = {item[1] if len(item) > 1 else 'N/A'}"
|
|
1766
|
-
)
|
|
1767
|
-
if item[0] == sexpdata.Symbol("extends"):
|
|
1768
|
-
# Convert bare symbol name to full lib_id
|
|
1769
|
-
parent_name = str(item[1]).strip('"')
|
|
1770
|
-
parent_lib_id = f"{lib_id.split(':')[0]}:{parent_name}"
|
|
1771
|
-
modified_data[i][1] = parent_lib_id
|
|
1772
|
-
logger.debug(
|
|
1773
|
-
f"🔧 CONVERT: Fixed extends directive: {parent_name} -> {parent_lib_id}"
|
|
1774
|
-
)
|
|
1775
|
-
break
|
|
1776
|
-
|
|
1777
|
-
# Fix string/symbol conversion issues in pin definitions
|
|
1778
|
-
self._fix_symbol_strings_recursively(modified_data)
|
|
1779
|
-
|
|
1780
|
-
return modified_data
|
|
1781
|
-
|
|
1782
|
-
def _fix_symbol_strings_recursively(self, data):
|
|
1783
|
-
"""Recursively fix string/symbol issues in parsed S-expression data."""
|
|
1784
|
-
import sexpdata
|
|
1785
|
-
|
|
1786
|
-
if isinstance(data, list):
|
|
1787
|
-
for i, item in enumerate(data):
|
|
1788
|
-
if isinstance(item, list):
|
|
1789
|
-
# Check for pin definitions that need fixing
|
|
1790
|
-
if len(item) >= 3 and item[0] == sexpdata.Symbol("pin"):
|
|
1791
|
-
# Fix pin type and shape - ensure they are symbols not strings
|
|
1792
|
-
if isinstance(item[1], str):
|
|
1793
|
-
item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
|
|
1794
|
-
if len(item) >= 3 and isinstance(item[2], str):
|
|
1795
|
-
item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
|
|
1796
|
-
|
|
1797
|
-
# Recursively process nested lists
|
|
1798
|
-
self._fix_symbol_strings_recursively(item)
|
|
1799
|
-
elif isinstance(item, str):
|
|
1800
|
-
# Fix common KiCAD keywords that should be symbols
|
|
1801
|
-
if item in ["yes", "no", "default", "none", "left", "right", "center"]:
|
|
1802
|
-
data[i] = sexpdata.Symbol(item)
|
|
1803
|
-
|
|
1804
|
-
return data
|
|
1490
|
+
find_project_refs(raw_data)
|
|
1805
1491
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
|
1809
1505
|
return {
|
|
1810
|
-
"
|
|
1811
|
-
"
|
|
1812
|
-
"generator_version": "9.0",
|
|
1813
|
-
"uuid": str(uuid.uuid4()),
|
|
1814
|
-
"paper": "A4",
|
|
1815
|
-
"components": [],
|
|
1816
|
-
"wires": [],
|
|
1817
|
-
"junctions": [],
|
|
1818
|
-
"labels": [],
|
|
1819
|
-
"nets": [],
|
|
1820
|
-
"lib_symbols": {},
|
|
1821
|
-
"sheet_instances": [{"path": "/", "page": "1"}],
|
|
1822
|
-
"symbol_instances": [],
|
|
1823
|
-
"embedded_fonts": "no",
|
|
1506
|
+
"lib_id": lib_id,
|
|
1507
|
+
"symbol": symbol_def.name if hasattr(symbol_def, "name") else lib_id.split(":")[-1],
|
|
1824
1508
|
}
|
|
1825
1509
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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)
|
|
1848
1544
|
|
|
1849
1545
|
def __str__(self) -> str:
|
|
1850
1546
|
"""String representation."""
|