kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kicad_sch_api/__init__.py +6 -2
- kicad_sch_api/cli.py +67 -62
- kicad_sch_api/core/component_bounds.py +477 -0
- kicad_sch_api/core/components.py +22 -10
- kicad_sch_api/core/config.py +127 -0
- kicad_sch_api/core/formatter.py +183 -23
- kicad_sch_api/core/geometry.py +111 -0
- kicad_sch_api/core/ic_manager.py +43 -37
- kicad_sch_api/core/junctions.py +17 -22
- kicad_sch_api/core/manhattan_routing.py +430 -0
- kicad_sch_api/core/parser.py +495 -196
- kicad_sch_api/core/pin_utils.py +149 -0
- kicad_sch_api/core/schematic.py +630 -207
- kicad_sch_api/core/simple_manhattan.py +228 -0
- kicad_sch_api/core/types.py +9 -4
- kicad_sch_api/core/wire_routing.py +380 -0
- kicad_sch_api/core/wires.py +29 -25
- kicad_sch_api/discovery/__init__.py +1 -1
- kicad_sch_api/discovery/search_index.py +142 -107
- kicad_sch_api/library/cache.py +70 -62
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/METADATA +212 -40
- kicad_sch_api-0.2.1.dist-info/RECORD +31 -0
- kicad_sch_api-0.2.0.dist-info/RECORD +0 -24
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/parser.py
CHANGED
|
@@ -253,7 +253,9 @@ class SExpressionParser:
|
|
|
253
253
|
if schematic_data.get("generator"):
|
|
254
254
|
sexp_data.append([sexpdata.Symbol("generator"), schematic_data["generator"]])
|
|
255
255
|
if schematic_data.get("generator_version"):
|
|
256
|
-
sexp_data.append(
|
|
256
|
+
sexp_data.append(
|
|
257
|
+
[sexpdata.Symbol("generator_version"), schematic_data["generator_version"]]
|
|
258
|
+
)
|
|
257
259
|
if schematic_data.get("uuid"):
|
|
258
260
|
sexp_data.append([sexpdata.Symbol("uuid"), schematic_data["uuid"]])
|
|
259
261
|
if schematic_data.get("paper"):
|
|
@@ -261,7 +263,9 @@ class SExpressionParser:
|
|
|
261
263
|
|
|
262
264
|
# Add title block only if it has non-default content
|
|
263
265
|
title_block = schematic_data.get("title_block")
|
|
264
|
-
if title_block and any(
|
|
266
|
+
if title_block and any(
|
|
267
|
+
title_block.get(key) for key in ["title", "company", "revision", "date", "comments"]
|
|
268
|
+
):
|
|
265
269
|
sexp_data.append(self._title_block_to_sexp(title_block))
|
|
266
270
|
|
|
267
271
|
# Add lib_symbols (always include for KiCAD compatibility)
|
|
@@ -300,14 +304,22 @@ class SExpressionParser:
|
|
|
300
304
|
for text_box in schematic_data.get("text_boxes", []):
|
|
301
305
|
sexp_data.append(self._text_box_to_sexp(text_box))
|
|
302
306
|
|
|
307
|
+
# Add graphics (rectangles, etc.)
|
|
308
|
+
for graphic in schematic_data.get("graphics", []):
|
|
309
|
+
sexp_data.append(self._graphic_to_sexp(graphic))
|
|
310
|
+
|
|
303
311
|
# Add sheet_instances (required by KiCAD)
|
|
304
312
|
sheet_instances = schematic_data.get("sheet_instances", [])
|
|
305
313
|
if sheet_instances:
|
|
306
314
|
sexp_data.append(self._sheet_instances_to_sexp(sheet_instances))
|
|
307
315
|
|
|
308
|
-
# Add symbol_instances (
|
|
316
|
+
# Add symbol_instances (only if non-empty or for blank schematics)
|
|
309
317
|
symbol_instances = schematic_data.get("symbol_instances", [])
|
|
310
|
-
|
|
318
|
+
# Always include for blank schematics (no UUID, no embedded_fonts)
|
|
319
|
+
is_blank_schematic = (
|
|
320
|
+
not schematic_data.get("uuid") and schematic_data.get("embedded_fonts") is None
|
|
321
|
+
)
|
|
322
|
+
if symbol_instances or is_blank_schematic:
|
|
311
323
|
sexp_data.append([sexpdata.Symbol("symbol_instances")])
|
|
312
324
|
|
|
313
325
|
# Add embedded_fonts (required by KiCAD)
|
|
@@ -422,18 +434,18 @@ class SExpressionParser:
|
|
|
422
434
|
def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
|
|
423
435
|
"""Convert title block to S-expression."""
|
|
424
436
|
sexp = [sexpdata.Symbol("title_block")]
|
|
425
|
-
|
|
437
|
+
|
|
426
438
|
# Add standard fields
|
|
427
439
|
for key in ["title", "date", "rev", "company"]:
|
|
428
440
|
if key in title_block and title_block[key]:
|
|
429
441
|
sexp.append([sexpdata.Symbol(key), title_block[key]])
|
|
430
|
-
|
|
442
|
+
|
|
431
443
|
# Add comments with special formatting
|
|
432
444
|
comments = title_block.get("comments", {})
|
|
433
445
|
if isinstance(comments, dict):
|
|
434
446
|
for comment_num, comment_text in comments.items():
|
|
435
447
|
sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
|
|
436
|
-
|
|
448
|
+
|
|
437
449
|
return sexp
|
|
438
450
|
|
|
439
451
|
def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
|
|
@@ -460,7 +472,9 @@ class SExpressionParser:
|
|
|
460
472
|
# Add simulation and board settings (required by KiCAD)
|
|
461
473
|
sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
|
|
462
474
|
sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
|
|
463
|
-
sexp.append(
|
|
475
|
+
sexp.append(
|
|
476
|
+
[sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
|
|
477
|
+
)
|
|
464
478
|
sexp.append([sexpdata.Symbol("dnp"), "no"])
|
|
465
479
|
sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
|
|
466
480
|
|
|
@@ -470,7 +484,7 @@ class SExpressionParser:
|
|
|
470
484
|
# Add properties with proper positioning and effects
|
|
471
485
|
lib_id = symbol_data.get("lib_id", "")
|
|
472
486
|
is_power_symbol = "power:" in lib_id
|
|
473
|
-
|
|
487
|
+
|
|
474
488
|
if symbol_data.get("reference"):
|
|
475
489
|
# Power symbol references should be hidden by default
|
|
476
490
|
ref_hide = is_power_symbol
|
|
@@ -478,9 +492,9 @@ class SExpressionParser:
|
|
|
478
492
|
"Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
|
|
479
493
|
)
|
|
480
494
|
sexp.append(ref_prop)
|
|
481
|
-
|
|
495
|
+
|
|
482
496
|
if symbol_data.get("value"):
|
|
483
|
-
# Power symbol values need different positioning
|
|
497
|
+
# Power symbol values need different positioning
|
|
484
498
|
if is_power_symbol:
|
|
485
499
|
val_prop = self._create_power_symbol_value_property(
|
|
486
500
|
symbol_data["value"], pos, lib_id
|
|
@@ -490,7 +504,7 @@ class SExpressionParser:
|
|
|
490
504
|
"Value", symbol_data["value"], pos, 1, "left"
|
|
491
505
|
)
|
|
492
506
|
sexp.append(val_prop)
|
|
493
|
-
|
|
507
|
+
|
|
494
508
|
footprint = symbol_data.get("footprint")
|
|
495
509
|
if footprint is not None: # Include empty strings but not None
|
|
496
510
|
fp_prop = self._create_property_with_positioning(
|
|
@@ -505,7 +519,7 @@ class SExpressionParser:
|
|
|
505
519
|
)
|
|
506
520
|
sexp.append(prop)
|
|
507
521
|
|
|
508
|
-
# Add pin UUID assignments (required by KiCAD)
|
|
522
|
+
# Add pin UUID assignments (required by KiCAD)
|
|
509
523
|
for pin in symbol_data.get("pins", []):
|
|
510
524
|
pin_uuid = str(uuid.uuid4())
|
|
511
525
|
# Ensure pin number is a string for proper quoting
|
|
@@ -513,54 +527,82 @@ class SExpressionParser:
|
|
|
513
527
|
sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
|
|
514
528
|
|
|
515
529
|
# Add instances section (required by KiCAD)
|
|
516
|
-
|
|
530
|
+
from .config import config
|
|
531
|
+
|
|
532
|
+
project_name = getattr(self, "project_name", config.defaults.project_name)
|
|
517
533
|
root_uuid = schematic_uuid or symbol_data.get("root_uuid", str(uuid.uuid4()))
|
|
518
|
-
logger.debug(
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
534
|
+
logger.debug(
|
|
535
|
+
f"🔧 Using UUID {root_uuid} for component {symbol_data.get('reference', 'unknown')}"
|
|
536
|
+
)
|
|
537
|
+
logger.debug(
|
|
538
|
+
f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
|
|
539
|
+
)
|
|
540
|
+
sexp.append(
|
|
541
|
+
[
|
|
542
|
+
sexpdata.Symbol("instances"),
|
|
543
|
+
[
|
|
544
|
+
sexpdata.Symbol("project"),
|
|
545
|
+
project_name,
|
|
546
|
+
[
|
|
547
|
+
sexpdata.Symbol("path"),
|
|
548
|
+
f"/{root_uuid}",
|
|
549
|
+
[sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
|
|
550
|
+
[sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
|
|
551
|
+
],
|
|
552
|
+
],
|
|
553
|
+
]
|
|
554
|
+
)
|
|
527
555
|
|
|
528
556
|
return sexp
|
|
529
557
|
|
|
530
|
-
def _create_property_with_positioning(
|
|
531
|
-
|
|
532
|
-
|
|
558
|
+
def _create_property_with_positioning(
|
|
559
|
+
self,
|
|
560
|
+
prop_name: str,
|
|
561
|
+
prop_value: str,
|
|
562
|
+
component_pos: Point,
|
|
563
|
+
offset_index: int,
|
|
564
|
+
justify: str = "left",
|
|
565
|
+
hide: bool = False,
|
|
566
|
+
) -> List[Any]:
|
|
533
567
|
"""Create a property with proper positioning and effects like KiCAD."""
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
568
|
+
from .config import config
|
|
569
|
+
|
|
570
|
+
# Calculate property position using configuration
|
|
571
|
+
prop_x, prop_y, rotation = config.get_property_position(
|
|
572
|
+
prop_name, (component_pos.x, component_pos.y), offset_index
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Build effects section based on hide status
|
|
576
|
+
effects = [
|
|
577
|
+
sexpdata.Symbol("effects"),
|
|
578
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
# Only add justify for visible properties or Reference/Value
|
|
582
|
+
if not hide or prop_name in ["Reference", "Value"]:
|
|
583
|
+
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
584
|
+
|
|
585
|
+
if hide:
|
|
586
|
+
effects.append([sexpdata.Symbol("hide"), sexpdata.Symbol("yes")])
|
|
587
|
+
|
|
545
588
|
prop_sexp = [
|
|
546
|
-
sexpdata.Symbol("property"),
|
|
547
|
-
prop_name,
|
|
589
|
+
sexpdata.Symbol("property"),
|
|
590
|
+
prop_name,
|
|
548
591
|
prop_value,
|
|
549
|
-
[
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
592
|
+
[
|
|
593
|
+
sexpdata.Symbol("at"),
|
|
594
|
+
round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
|
|
595
|
+
round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
|
|
596
|
+
rotation,
|
|
597
|
+
],
|
|
598
|
+
effects,
|
|
556
599
|
]
|
|
557
|
-
|
|
558
|
-
if hide:
|
|
559
|
-
prop_sexp[4].append([sexpdata.Symbol("hide"), sexpdata.Symbol("yes")])
|
|
560
|
-
|
|
600
|
+
|
|
561
601
|
return prop_sexp
|
|
562
602
|
|
|
563
|
-
def _create_power_symbol_value_property(
|
|
603
|
+
def _create_power_symbol_value_property(
|
|
604
|
+
self, value: str, component_pos: Point, lib_id: str
|
|
605
|
+
) -> List[Any]:
|
|
564
606
|
"""Create Value property for power symbols with correct positioning."""
|
|
565
607
|
# Power symbols have different value positioning based on type
|
|
566
608
|
if "GND" in lib_id:
|
|
@@ -568,32 +610,36 @@ class SExpressionParser:
|
|
|
568
610
|
prop_x = component_pos.x
|
|
569
611
|
prop_y = component_pos.y + 5.08 # Below GND symbol
|
|
570
612
|
elif "+3.3V" in lib_id or "VDD" in lib_id:
|
|
571
|
-
# Positive voltage values go below the symbol
|
|
613
|
+
# Positive voltage values go below the symbol
|
|
572
614
|
prop_x = component_pos.x
|
|
573
615
|
prop_y = component_pos.y - 5.08 # Above symbol (negative offset)
|
|
574
616
|
else:
|
|
575
617
|
# Default power symbol positioning
|
|
576
618
|
prop_x = component_pos.x
|
|
577
619
|
prop_y = component_pos.y + 3.556
|
|
578
|
-
|
|
620
|
+
|
|
579
621
|
prop_sexp = [
|
|
580
|
-
sexpdata.Symbol("property"),
|
|
581
|
-
"Value",
|
|
622
|
+
sexpdata.Symbol("property"),
|
|
623
|
+
"Value",
|
|
582
624
|
value,
|
|
583
|
-
[
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
625
|
+
[
|
|
626
|
+
sexpdata.Symbol("at"),
|
|
627
|
+
round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
|
|
628
|
+
round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
|
|
629
|
+
0,
|
|
630
|
+
],
|
|
631
|
+
[
|
|
632
|
+
sexpdata.Symbol("effects"),
|
|
633
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
634
|
+
],
|
|
589
635
|
]
|
|
590
|
-
|
|
636
|
+
|
|
591
637
|
return prop_sexp
|
|
592
638
|
|
|
593
639
|
def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
|
|
594
640
|
"""Convert wire to S-expression."""
|
|
595
641
|
sexp = [sexpdata.Symbol("wire")]
|
|
596
|
-
|
|
642
|
+
|
|
597
643
|
# Add points (pts section)
|
|
598
644
|
points = wire_data.get("points", [])
|
|
599
645
|
if len(points) >= 2:
|
|
@@ -606,39 +652,39 @@ class SExpressionParser:
|
|
|
606
652
|
else:
|
|
607
653
|
# Assume it's a Point object
|
|
608
654
|
x, y = point.x, point.y
|
|
609
|
-
|
|
655
|
+
|
|
610
656
|
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
611
657
|
if isinstance(x, float) and x.is_integer():
|
|
612
658
|
x = int(x)
|
|
613
659
|
if isinstance(y, float) and y.is_integer():
|
|
614
660
|
y = int(y)
|
|
615
|
-
|
|
661
|
+
|
|
616
662
|
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
617
663
|
sexp.append(pts_sexp)
|
|
618
|
-
|
|
664
|
+
|
|
619
665
|
# Add stroke information
|
|
620
666
|
stroke_width = wire_data.get("stroke_width", 0)
|
|
621
667
|
stroke_type = wire_data.get("stroke_type", "default")
|
|
622
668
|
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
623
|
-
|
|
669
|
+
|
|
624
670
|
# Format stroke width (use int for 0, preserve float for others)
|
|
625
671
|
if isinstance(stroke_width, float) and stroke_width == 0.0:
|
|
626
672
|
stroke_width = 0
|
|
627
|
-
|
|
673
|
+
|
|
628
674
|
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
629
675
|
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
630
676
|
sexp.append(stroke_sexp)
|
|
631
|
-
|
|
677
|
+
|
|
632
678
|
# Add UUID
|
|
633
679
|
if "uuid" in wire_data:
|
|
634
680
|
sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
|
|
635
|
-
|
|
681
|
+
|
|
636
682
|
return sexp
|
|
637
683
|
|
|
638
684
|
def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
|
|
639
685
|
"""Convert junction to S-expression."""
|
|
640
686
|
sexp = [sexpdata.Symbol("junction")]
|
|
641
|
-
|
|
687
|
+
|
|
642
688
|
# Add position
|
|
643
689
|
pos = junction_data["position"]
|
|
644
690
|
if isinstance(pos, dict):
|
|
@@ -648,77 +694,79 @@ class SExpressionParser:
|
|
|
648
694
|
else:
|
|
649
695
|
# Assume it's a Point object
|
|
650
696
|
x, y = pos.x, pos.y
|
|
651
|
-
|
|
697
|
+
|
|
652
698
|
# Format coordinates properly
|
|
653
699
|
if isinstance(x, float) and x.is_integer():
|
|
654
700
|
x = int(x)
|
|
655
701
|
if isinstance(y, float) and y.is_integer():
|
|
656
702
|
y = int(y)
|
|
657
|
-
|
|
703
|
+
|
|
658
704
|
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
659
|
-
|
|
705
|
+
|
|
660
706
|
# Add diameter
|
|
661
707
|
diameter = junction_data.get("diameter", 0)
|
|
662
708
|
sexp.append([sexpdata.Symbol("diameter"), diameter])
|
|
663
|
-
|
|
709
|
+
|
|
664
710
|
# Add color (RGBA)
|
|
665
711
|
color = junction_data.get("color", (0, 0, 0, 0))
|
|
666
712
|
if isinstance(color, (list, tuple)) and len(color) >= 4:
|
|
667
713
|
sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
|
|
668
714
|
else:
|
|
669
715
|
sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
|
|
670
|
-
|
|
716
|
+
|
|
671
717
|
# Add UUID
|
|
672
718
|
if "uuid" in junction_data:
|
|
673
719
|
sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
|
|
674
|
-
|
|
720
|
+
|
|
675
721
|
return sexp
|
|
676
722
|
|
|
677
723
|
def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
|
|
678
724
|
"""Convert local label to S-expression."""
|
|
679
725
|
sexp = [sexpdata.Symbol("label"), label_data["text"]]
|
|
680
|
-
|
|
726
|
+
|
|
681
727
|
# Add position
|
|
682
728
|
pos = label_data["position"]
|
|
683
729
|
x, y = pos["x"], pos["y"]
|
|
684
730
|
rotation = label_data.get("rotation", 0)
|
|
685
|
-
|
|
731
|
+
|
|
686
732
|
# Format coordinates properly
|
|
687
733
|
if isinstance(x, float) and x.is_integer():
|
|
688
734
|
x = int(x)
|
|
689
735
|
if isinstance(y, float) and y.is_integer():
|
|
690
736
|
y = int(y)
|
|
691
|
-
|
|
737
|
+
|
|
692
738
|
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
693
|
-
|
|
739
|
+
|
|
694
740
|
# Add effects (font properties)
|
|
695
741
|
size = label_data.get("size", 1.27)
|
|
696
742
|
effects = [sexpdata.Symbol("effects")]
|
|
697
743
|
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
698
744
|
effects.append(font)
|
|
699
|
-
effects.append(
|
|
745
|
+
effects.append(
|
|
746
|
+
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]
|
|
747
|
+
)
|
|
700
748
|
sexp.append(effects)
|
|
701
|
-
|
|
749
|
+
|
|
702
750
|
# Add UUID
|
|
703
751
|
if "uuid" in label_data:
|
|
704
752
|
sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
|
|
705
|
-
|
|
753
|
+
|
|
706
754
|
return sexp
|
|
707
755
|
|
|
708
756
|
def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
|
|
709
757
|
"""Convert hierarchical label to S-expression."""
|
|
710
758
|
sexp = [sexpdata.Symbol("hierarchical_label"), hlabel_data["text"]]
|
|
711
|
-
|
|
759
|
+
|
|
712
760
|
# Add shape
|
|
713
761
|
shape = hlabel_data.get("shape", "input")
|
|
714
762
|
sexp.append([sexpdata.Symbol("shape"), sexpdata.Symbol(shape)])
|
|
715
|
-
|
|
763
|
+
|
|
716
764
|
# Add position
|
|
717
765
|
pos = hlabel_data["position"]
|
|
718
766
|
x, y = pos["x"], pos["y"]
|
|
719
767
|
rotation = hlabel_data.get("rotation", 0)
|
|
720
768
|
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
721
|
-
|
|
769
|
+
|
|
722
770
|
# Add effects (font properties)
|
|
723
771
|
size = hlabel_data.get("size", 1.27)
|
|
724
772
|
effects = [sexpdata.Symbol("effects")]
|
|
@@ -726,17 +774,17 @@ class SExpressionParser:
|
|
|
726
774
|
effects.append(font)
|
|
727
775
|
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol("left")])
|
|
728
776
|
sexp.append(effects)
|
|
729
|
-
|
|
777
|
+
|
|
730
778
|
# Add UUID
|
|
731
779
|
if "uuid" in hlabel_data:
|
|
732
780
|
sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
|
|
733
|
-
|
|
781
|
+
|
|
734
782
|
return sexp
|
|
735
783
|
|
|
736
784
|
def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
|
|
737
785
|
"""Convert hierarchical sheet to S-expression."""
|
|
738
786
|
sexp = [sexpdata.Symbol("sheet")]
|
|
739
|
-
|
|
787
|
+
|
|
740
788
|
# Add position
|
|
741
789
|
pos = sheet_data["position"]
|
|
742
790
|
x, y = pos["x"], pos["y"]
|
|
@@ -745,24 +793,44 @@ class SExpressionParser:
|
|
|
745
793
|
if isinstance(y, float) and y.is_integer():
|
|
746
794
|
y = int(y)
|
|
747
795
|
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
748
|
-
|
|
796
|
+
|
|
749
797
|
# Add size
|
|
750
798
|
size = sheet_data["size"]
|
|
751
799
|
w, h = size["width"], size["height"]
|
|
752
800
|
sexp.append([sexpdata.Symbol("size"), w, h])
|
|
753
|
-
|
|
801
|
+
|
|
754
802
|
# Add basic properties
|
|
755
|
-
sexp.append(
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
sexp.append(
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
803
|
+
sexp.append(
|
|
804
|
+
[
|
|
805
|
+
sexpdata.Symbol("exclude_from_sim"),
|
|
806
|
+
sexpdata.Symbol("yes" if sheet_data.get("exclude_from_sim", False) else "no"),
|
|
807
|
+
]
|
|
808
|
+
)
|
|
809
|
+
sexp.append(
|
|
810
|
+
[
|
|
811
|
+
sexpdata.Symbol("in_bom"),
|
|
812
|
+
sexpdata.Symbol("yes" if sheet_data.get("in_bom", True) else "no"),
|
|
813
|
+
]
|
|
814
|
+
)
|
|
815
|
+
sexp.append(
|
|
816
|
+
[
|
|
817
|
+
sexpdata.Symbol("on_board"),
|
|
818
|
+
sexpdata.Symbol("yes" if sheet_data.get("on_board", True) else "no"),
|
|
819
|
+
]
|
|
820
|
+
)
|
|
821
|
+
sexp.append(
|
|
822
|
+
[
|
|
823
|
+
sexpdata.Symbol("dnp"),
|
|
824
|
+
sexpdata.Symbol("yes" if sheet_data.get("dnp", False) else "no"),
|
|
825
|
+
]
|
|
826
|
+
)
|
|
827
|
+
sexp.append(
|
|
828
|
+
[
|
|
829
|
+
sexpdata.Symbol("fields_autoplaced"),
|
|
830
|
+
sexpdata.Symbol("yes" if sheet_data.get("fields_autoplaced", True) else "no"),
|
|
831
|
+
]
|
|
832
|
+
)
|
|
833
|
+
|
|
766
834
|
# Add stroke
|
|
767
835
|
stroke_width = sheet_data.get("stroke_width", 0.1524)
|
|
768
836
|
stroke_type = sheet_data.get("stroke_type", "solid")
|
|
@@ -770,42 +838,58 @@ class SExpressionParser:
|
|
|
770
838
|
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
771
839
|
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
772
840
|
sexp.append(stroke_sexp)
|
|
773
|
-
|
|
841
|
+
|
|
774
842
|
# Add fill
|
|
775
843
|
fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
|
|
776
844
|
fill_sexp = [sexpdata.Symbol("fill")]
|
|
777
|
-
fill_sexp.append(
|
|
845
|
+
fill_sexp.append(
|
|
846
|
+
[sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]]
|
|
847
|
+
)
|
|
778
848
|
sexp.append(fill_sexp)
|
|
779
|
-
|
|
849
|
+
|
|
780
850
|
# Add UUID
|
|
781
851
|
if "uuid" in sheet_data:
|
|
782
852
|
sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
|
|
783
|
-
|
|
853
|
+
|
|
784
854
|
# Add sheet properties (name and filename)
|
|
785
855
|
name = sheet_data.get("name", "Sheet")
|
|
786
856
|
filename = sheet_data.get("filename", "sheet.kicad_sch")
|
|
787
|
-
|
|
857
|
+
|
|
788
858
|
# Sheetname property
|
|
859
|
+
from .config import config
|
|
860
|
+
|
|
789
861
|
name_prop = [sexpdata.Symbol("property"), "Sheetname", name]
|
|
790
|
-
name_prop.append(
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
862
|
+
name_prop.append(
|
|
863
|
+
[sexpdata.Symbol("at"), x, round(y + config.sheet.name_offset_y, 4), 0]
|
|
864
|
+
) # Above sheet
|
|
865
|
+
name_prop.append(
|
|
866
|
+
[
|
|
867
|
+
sexpdata.Symbol("effects"),
|
|
868
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
869
|
+
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")],
|
|
870
|
+
]
|
|
871
|
+
)
|
|
794
872
|
sexp.append(name_prop)
|
|
795
|
-
|
|
796
|
-
# Sheetfile property
|
|
873
|
+
|
|
874
|
+
# Sheetfile property
|
|
797
875
|
file_prop = [sexpdata.Symbol("property"), "Sheetfile", filename]
|
|
798
|
-
file_prop.append(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
876
|
+
file_prop.append(
|
|
877
|
+
[sexpdata.Symbol("at"), x, round(y + h + config.sheet.file_offset_y, 4), 0]
|
|
878
|
+
) # Below sheet
|
|
879
|
+
file_prop.append(
|
|
880
|
+
[
|
|
881
|
+
sexpdata.Symbol("effects"),
|
|
882
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
883
|
+
[sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("top")],
|
|
884
|
+
]
|
|
885
|
+
)
|
|
802
886
|
sexp.append(file_prop)
|
|
803
|
-
|
|
887
|
+
|
|
804
888
|
# Add sheet pins if any
|
|
805
889
|
for pin in sheet_data.get("pins", []):
|
|
806
890
|
pin_sexp = self._sheet_pin_to_sexp(pin)
|
|
807
891
|
sexp.append(pin_sexp)
|
|
808
|
-
|
|
892
|
+
|
|
809
893
|
# Add instances
|
|
810
894
|
if schematic_uuid:
|
|
811
895
|
instances_sexp = [sexpdata.Symbol("instances")]
|
|
@@ -817,23 +901,27 @@ class SExpressionParser:
|
|
|
817
901
|
project_sexp.append(path_sexp)
|
|
818
902
|
instances_sexp.append(project_sexp)
|
|
819
903
|
sexp.append(instances_sexp)
|
|
820
|
-
|
|
904
|
+
|
|
821
905
|
return sexp
|
|
822
906
|
|
|
823
907
|
def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
|
|
824
908
|
"""Convert sheet pin to S-expression."""
|
|
825
|
-
pin_sexp = [
|
|
826
|
-
|
|
909
|
+
pin_sexp = [
|
|
910
|
+
sexpdata.Symbol("pin"),
|
|
911
|
+
pin_data["name"],
|
|
912
|
+
sexpdata.Symbol(pin_data.get("pin_type", "input")),
|
|
913
|
+
]
|
|
914
|
+
|
|
827
915
|
# Add position
|
|
828
916
|
pos = pin_data["position"]
|
|
829
917
|
x, y = pos["x"], pos["y"]
|
|
830
918
|
rotation = pin_data.get("rotation", 0)
|
|
831
919
|
pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
832
|
-
|
|
920
|
+
|
|
833
921
|
# Add UUID
|
|
834
922
|
if "uuid" in pin_data:
|
|
835
923
|
pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
|
|
836
|
-
|
|
924
|
+
|
|
837
925
|
# Add effects
|
|
838
926
|
size = pin_data.get("size", 1.27)
|
|
839
927
|
effects = [sexpdata.Symbol("effects")]
|
|
@@ -842,75 +930,77 @@ class SExpressionParser:
|
|
|
842
930
|
justify = pin_data.get("justify", "right")
|
|
843
931
|
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
844
932
|
pin_sexp.append(effects)
|
|
845
|
-
|
|
933
|
+
|
|
846
934
|
return pin_sexp
|
|
847
935
|
|
|
848
936
|
def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
|
|
849
937
|
"""Convert text element to S-expression."""
|
|
850
938
|
sexp = [sexpdata.Symbol("text"), text_data["text"]]
|
|
851
|
-
|
|
939
|
+
|
|
852
940
|
# Add exclude_from_sim
|
|
853
941
|
exclude_sim = text_data.get("exclude_from_sim", False)
|
|
854
|
-
sexp.append(
|
|
855
|
-
|
|
856
|
-
|
|
942
|
+
sexp.append(
|
|
943
|
+
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
|
|
944
|
+
)
|
|
945
|
+
|
|
857
946
|
# Add position
|
|
858
947
|
pos = text_data["position"]
|
|
859
948
|
x, y = pos["x"], pos["y"]
|
|
860
949
|
rotation = text_data.get("rotation", 0)
|
|
861
|
-
|
|
950
|
+
|
|
862
951
|
# Format coordinates properly
|
|
863
952
|
if isinstance(x, float) and x.is_integer():
|
|
864
953
|
x = int(x)
|
|
865
954
|
if isinstance(y, float) and y.is_integer():
|
|
866
955
|
y = int(y)
|
|
867
|
-
|
|
956
|
+
|
|
868
957
|
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
869
|
-
|
|
958
|
+
|
|
870
959
|
# Add effects (font properties)
|
|
871
960
|
size = text_data.get("size", 1.27)
|
|
872
961
|
effects = [sexpdata.Symbol("effects")]
|
|
873
962
|
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
|
|
874
963
|
effects.append(font)
|
|
875
964
|
sexp.append(effects)
|
|
876
|
-
|
|
965
|
+
|
|
877
966
|
# Add UUID
|
|
878
967
|
if "uuid" in text_data:
|
|
879
968
|
sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
|
|
880
|
-
|
|
969
|
+
|
|
881
970
|
return sexp
|
|
882
971
|
|
|
883
972
|
def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
|
|
884
973
|
"""Convert text box element to S-expression."""
|
|
885
974
|
sexp = [sexpdata.Symbol("text_box"), text_box_data["text"]]
|
|
886
|
-
|
|
975
|
+
|
|
887
976
|
# Add exclude_from_sim
|
|
888
977
|
exclude_sim = text_box_data.get("exclude_from_sim", False)
|
|
889
|
-
sexp.append(
|
|
890
|
-
|
|
891
|
-
|
|
978
|
+
sexp.append(
|
|
979
|
+
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
|
|
980
|
+
)
|
|
981
|
+
|
|
892
982
|
# Add position
|
|
893
983
|
pos = text_box_data["position"]
|
|
894
984
|
x, y = pos["x"], pos["y"]
|
|
895
985
|
rotation = text_box_data.get("rotation", 0)
|
|
896
|
-
|
|
986
|
+
|
|
897
987
|
# Format coordinates properly
|
|
898
988
|
if isinstance(x, float) and x.is_integer():
|
|
899
989
|
x = int(x)
|
|
900
990
|
if isinstance(y, float) and y.is_integer():
|
|
901
991
|
y = int(y)
|
|
902
|
-
|
|
992
|
+
|
|
903
993
|
sexp.append([sexpdata.Symbol("at"), x, y, rotation])
|
|
904
|
-
|
|
994
|
+
|
|
905
995
|
# Add size
|
|
906
996
|
size = text_box_data["size"]
|
|
907
997
|
w, h = size["width"], size["height"]
|
|
908
998
|
sexp.append([sexpdata.Symbol("size"), w, h])
|
|
909
|
-
|
|
999
|
+
|
|
910
1000
|
# Add margins
|
|
911
1001
|
margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
|
|
912
1002
|
sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
|
|
913
|
-
|
|
1003
|
+
|
|
914
1004
|
# Add stroke
|
|
915
1005
|
stroke_width = text_box_data.get("stroke_width", 0)
|
|
916
1006
|
stroke_type = text_box_data.get("stroke_type", "solid")
|
|
@@ -918,34 +1008,36 @@ class SExpressionParser:
|
|
|
918
1008
|
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
919
1009
|
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
920
1010
|
sexp.append(stroke_sexp)
|
|
921
|
-
|
|
1011
|
+
|
|
922
1012
|
# Add fill
|
|
923
1013
|
fill_type = text_box_data.get("fill_type", "none")
|
|
924
1014
|
fill_sexp = [sexpdata.Symbol("fill")]
|
|
925
1015
|
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
926
1016
|
sexp.append(fill_sexp)
|
|
927
|
-
|
|
1017
|
+
|
|
928
1018
|
# Add effects (font properties and justification)
|
|
929
1019
|
font_size = text_box_data.get("font_size", 1.27)
|
|
930
1020
|
justify_h = text_box_data.get("justify_horizontal", "left")
|
|
931
1021
|
justify_v = text_box_data.get("justify_vertical", "top")
|
|
932
|
-
|
|
1022
|
+
|
|
933
1023
|
effects = [sexpdata.Symbol("effects")]
|
|
934
1024
|
font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
|
|
935
1025
|
effects.append(font)
|
|
936
|
-
effects.append(
|
|
1026
|
+
effects.append(
|
|
1027
|
+
[sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
|
|
1028
|
+
)
|
|
937
1029
|
sexp.append(effects)
|
|
938
|
-
|
|
1030
|
+
|
|
939
1031
|
# Add UUID
|
|
940
1032
|
if "uuid" in text_box_data:
|
|
941
1033
|
sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
|
|
942
|
-
|
|
1034
|
+
|
|
943
1035
|
return sexp
|
|
944
1036
|
|
|
945
1037
|
def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
|
|
946
1038
|
"""Convert lib_symbols to S-expression."""
|
|
947
1039
|
sexp = [sexpdata.Symbol("lib_symbols")]
|
|
948
|
-
|
|
1040
|
+
|
|
949
1041
|
# Add each symbol definition
|
|
950
1042
|
for symbol_name, symbol_def in lib_symbols.items():
|
|
951
1043
|
if isinstance(symbol_def, list):
|
|
@@ -955,58 +1047,124 @@ class SExpressionParser:
|
|
|
955
1047
|
# Dictionary format - convert to S-expression
|
|
956
1048
|
symbol_sexp = self._create_basic_symbol_definition(symbol_name)
|
|
957
1049
|
sexp.append(symbol_sexp)
|
|
958
|
-
|
|
1050
|
+
|
|
959
1051
|
return sexp
|
|
960
|
-
|
|
1052
|
+
|
|
961
1053
|
def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
|
|
962
1054
|
"""Create a basic symbol definition for KiCAD compatibility."""
|
|
963
1055
|
symbol_sexp = [sexpdata.Symbol("symbol"), lib_id]
|
|
964
|
-
|
|
1056
|
+
|
|
965
1057
|
# Add basic symbol properties
|
|
966
|
-
symbol_sexp.extend(
|
|
967
|
-
[
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1058
|
+
symbol_sexp.extend(
|
|
1059
|
+
[
|
|
1060
|
+
[sexpdata.Symbol("pin_numbers"), [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]],
|
|
1061
|
+
[sexpdata.Symbol("pin_names"), [sexpdata.Symbol("offset"), 0]],
|
|
1062
|
+
[sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("no")],
|
|
1063
|
+
[sexpdata.Symbol("in_bom"), sexpdata.Symbol("yes")],
|
|
1064
|
+
[sexpdata.Symbol("on_board"), sexpdata.Symbol("yes")],
|
|
1065
|
+
]
|
|
1066
|
+
)
|
|
1067
|
+
|
|
974
1068
|
# Add basic properties for the symbol
|
|
975
1069
|
if "R" in lib_id: # Resistor
|
|
976
|
-
symbol_sexp.extend(
|
|
977
|
-
[
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1070
|
+
symbol_sexp.extend(
|
|
1071
|
+
[
|
|
1072
|
+
[
|
|
1073
|
+
sexpdata.Symbol("property"),
|
|
1074
|
+
"Reference",
|
|
1075
|
+
"R",
|
|
1076
|
+
[sexpdata.Symbol("at"), 2.032, 0, 90],
|
|
1077
|
+
[
|
|
1078
|
+
sexpdata.Symbol("effects"),
|
|
1079
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1080
|
+
],
|
|
1081
|
+
],
|
|
1082
|
+
[
|
|
1083
|
+
sexpdata.Symbol("property"),
|
|
1084
|
+
"Value",
|
|
1085
|
+
"R",
|
|
1086
|
+
[sexpdata.Symbol("at"), 0, 0, 90],
|
|
1087
|
+
[
|
|
1088
|
+
sexpdata.Symbol("effects"),
|
|
1089
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1090
|
+
],
|
|
1091
|
+
],
|
|
1092
|
+
[
|
|
1093
|
+
sexpdata.Symbol("property"),
|
|
1094
|
+
"Footprint",
|
|
1095
|
+
"",
|
|
1096
|
+
[sexpdata.Symbol("at"), -1.778, 0, 90],
|
|
1097
|
+
[
|
|
1098
|
+
sexpdata.Symbol("effects"),
|
|
1099
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1100
|
+
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1101
|
+
],
|
|
1102
|
+
],
|
|
1103
|
+
[
|
|
1104
|
+
sexpdata.Symbol("property"),
|
|
1105
|
+
"Datasheet",
|
|
1106
|
+
"~",
|
|
1107
|
+
[sexpdata.Symbol("at"), 0, 0, 0],
|
|
1108
|
+
[
|
|
1109
|
+
sexpdata.Symbol("effects"),
|
|
1110
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1111
|
+
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1112
|
+
],
|
|
1113
|
+
],
|
|
1114
|
+
]
|
|
1115
|
+
)
|
|
1116
|
+
|
|
991
1117
|
elif "C" in lib_id: # Capacitor
|
|
992
|
-
symbol_sexp.extend(
|
|
993
|
-
[
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1118
|
+
symbol_sexp.extend(
|
|
1119
|
+
[
|
|
1120
|
+
[
|
|
1121
|
+
sexpdata.Symbol("property"),
|
|
1122
|
+
"Reference",
|
|
1123
|
+
"C",
|
|
1124
|
+
[sexpdata.Symbol("at"), 0.635, 2.54, 0],
|
|
1125
|
+
[
|
|
1126
|
+
sexpdata.Symbol("effects"),
|
|
1127
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1128
|
+
],
|
|
1129
|
+
],
|
|
1130
|
+
[
|
|
1131
|
+
sexpdata.Symbol("property"),
|
|
1132
|
+
"Value",
|
|
1133
|
+
"C",
|
|
1134
|
+
[sexpdata.Symbol("at"), 0.635, -2.54, 0],
|
|
1135
|
+
[
|
|
1136
|
+
sexpdata.Symbol("effects"),
|
|
1137
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1138
|
+
],
|
|
1139
|
+
],
|
|
1140
|
+
[
|
|
1141
|
+
sexpdata.Symbol("property"),
|
|
1142
|
+
"Footprint",
|
|
1143
|
+
"",
|
|
1144
|
+
[sexpdata.Symbol("at"), 0, -1.27, 0],
|
|
1145
|
+
[
|
|
1146
|
+
sexpdata.Symbol("effects"),
|
|
1147
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1148
|
+
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1149
|
+
],
|
|
1150
|
+
],
|
|
1151
|
+
[
|
|
1152
|
+
sexpdata.Symbol("property"),
|
|
1153
|
+
"Datasheet",
|
|
1154
|
+
"~",
|
|
1155
|
+
[sexpdata.Symbol("at"), 0, 0, 0],
|
|
1156
|
+
[
|
|
1157
|
+
sexpdata.Symbol("effects"),
|
|
1158
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
1159
|
+
[sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
|
|
1160
|
+
],
|
|
1161
|
+
],
|
|
1162
|
+
]
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1007
1165
|
# Add basic graphics and pins (minimal for now)
|
|
1008
1166
|
symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
|
|
1009
|
-
|
|
1167
|
+
|
|
1010
1168
|
return symbol_sexp
|
|
1011
1169
|
|
|
1012
1170
|
def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
|
|
@@ -1017,7 +1175,11 @@ class SExpressionParser:
|
|
|
1017
1175
|
sheet_data = {"path": "/", "page": "1"}
|
|
1018
1176
|
for element in sheet_item[1:]: # Skip element header
|
|
1019
1177
|
if isinstance(element, list) and len(element) >= 2:
|
|
1020
|
-
key =
|
|
1178
|
+
key = (
|
|
1179
|
+
str(element[0])
|
|
1180
|
+
if isinstance(element[0], sexpdata.Symbol)
|
|
1181
|
+
else str(element[0])
|
|
1182
|
+
)
|
|
1021
1183
|
if key == "path":
|
|
1022
1184
|
sheet_data["path"] = element[1]
|
|
1023
1185
|
elif key == "page":
|
|
@@ -1036,12 +1198,149 @@ class SExpressionParser:
|
|
|
1036
1198
|
for sheet in sheet_instances:
|
|
1037
1199
|
# Create: (path "/" (page "1"))
|
|
1038
1200
|
sheet_sexp = [
|
|
1039
|
-
sexpdata.Symbol("path"),
|
|
1040
|
-
|
|
1201
|
+
sexpdata.Symbol("path"),
|
|
1202
|
+
sheet.get("path", "/"),
|
|
1203
|
+
[sexpdata.Symbol("page"), str(sheet.get("page", "1"))],
|
|
1041
1204
|
]
|
|
1042
1205
|
sexp.append(sheet_sexp)
|
|
1043
1206
|
return sexp
|
|
1044
1207
|
|
|
1208
|
+
def _graphic_to_sexp(self, graphic_data: Dict[str, Any]) -> List[Any]:
|
|
1209
|
+
"""Convert graphics (rectangles, etc.) to S-expression."""
|
|
1210
|
+
# For now, we only support rectangles - this is the main graphics element we create
|
|
1211
|
+
sexp = [sexpdata.Symbol("rectangle")]
|
|
1212
|
+
|
|
1213
|
+
# Add start position
|
|
1214
|
+
start = graphic_data.get("start", {})
|
|
1215
|
+
start_x = start.get("x", 0)
|
|
1216
|
+
start_y = start.get("y", 0)
|
|
1217
|
+
|
|
1218
|
+
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
1219
|
+
if isinstance(start_x, float) and start_x.is_integer():
|
|
1220
|
+
start_x = int(start_x)
|
|
1221
|
+
if isinstance(start_y, float) and start_y.is_integer():
|
|
1222
|
+
start_y = int(start_y)
|
|
1223
|
+
|
|
1224
|
+
sexp.append([sexpdata.Symbol("start"), start_x, start_y])
|
|
1225
|
+
|
|
1226
|
+
# Add end position
|
|
1227
|
+
end = graphic_data.get("end", {})
|
|
1228
|
+
end_x = end.get("x", 0)
|
|
1229
|
+
end_y = end.get("y", 0)
|
|
1230
|
+
|
|
1231
|
+
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
1232
|
+
if isinstance(end_x, float) and end_x.is_integer():
|
|
1233
|
+
end_x = int(end_x)
|
|
1234
|
+
if isinstance(end_y, float) and end_y.is_integer():
|
|
1235
|
+
end_y = int(end_y)
|
|
1236
|
+
|
|
1237
|
+
sexp.append([sexpdata.Symbol("end"), end_x, end_y])
|
|
1238
|
+
|
|
1239
|
+
# Add stroke information (KiCAD format: width, type, and optionally color)
|
|
1240
|
+
stroke = graphic_data.get("stroke", {})
|
|
1241
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1242
|
+
|
|
1243
|
+
# Stroke width - default to 0 to match KiCAD behavior
|
|
1244
|
+
stroke_width = stroke.get("width", 0)
|
|
1245
|
+
if isinstance(stroke_width, float) and stroke_width == 0.0:
|
|
1246
|
+
stroke_width = 0
|
|
1247
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1248
|
+
|
|
1249
|
+
# Stroke type - normalize to KiCAD format and validate
|
|
1250
|
+
stroke_type = stroke.get("type", "default")
|
|
1251
|
+
|
|
1252
|
+
# KiCAD only supports these exact stroke types
|
|
1253
|
+
valid_kicad_types = {"solid", "dash", "dash_dot", "dash_dot_dot", "dot", "default"}
|
|
1254
|
+
|
|
1255
|
+
# Map common variations to KiCAD format
|
|
1256
|
+
stroke_type_map = {
|
|
1257
|
+
"dashdot": "dash_dot",
|
|
1258
|
+
"dash-dot": "dash_dot",
|
|
1259
|
+
"dashdotdot": "dash_dot_dot",
|
|
1260
|
+
"dash-dot-dot": "dash_dot_dot",
|
|
1261
|
+
"solid": "solid",
|
|
1262
|
+
"dash": "dash",
|
|
1263
|
+
"dot": "dot",
|
|
1264
|
+
"default": "default",
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
# Normalize and validate
|
|
1268
|
+
normalized_stroke_type = stroke_type_map.get(stroke_type.lower(), stroke_type)
|
|
1269
|
+
if normalized_stroke_type not in valid_kicad_types:
|
|
1270
|
+
normalized_stroke_type = "default" # Fallback to default for invalid types
|
|
1271
|
+
|
|
1272
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(normalized_stroke_type)])
|
|
1273
|
+
|
|
1274
|
+
# Stroke color (if specified) - KiCAD format uses RGB 0-255 values plus alpha
|
|
1275
|
+
stroke_color = stroke.get("color")
|
|
1276
|
+
if stroke_color:
|
|
1277
|
+
if isinstance(stroke_color, str):
|
|
1278
|
+
# Convert string color names to RGB 0-255 values
|
|
1279
|
+
color_rgb = self._color_to_rgb255(stroke_color)
|
|
1280
|
+
stroke_sexp.append([sexpdata.Symbol("color")] + color_rgb + [1]) # Add alpha=1
|
|
1281
|
+
elif isinstance(stroke_color, (list, tuple)) and len(stroke_color) >= 3:
|
|
1282
|
+
# Use provided RGB values directly
|
|
1283
|
+
stroke_sexp.append([sexpdata.Symbol("color")] + list(stroke_color))
|
|
1284
|
+
|
|
1285
|
+
sexp.append(stroke_sexp)
|
|
1286
|
+
|
|
1287
|
+
# Add fill information
|
|
1288
|
+
fill = graphic_data.get("fill", {"type": "none"})
|
|
1289
|
+
fill_type = fill.get("type", "none")
|
|
1290
|
+
fill_sexp = [sexpdata.Symbol("fill"), [sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)]]
|
|
1291
|
+
sexp.append(fill_sexp)
|
|
1292
|
+
|
|
1293
|
+
# Add UUID (no quotes around UUID in KiCAD format)
|
|
1294
|
+
if "uuid" in graphic_data:
|
|
1295
|
+
uuid_str = graphic_data["uuid"]
|
|
1296
|
+
# Remove quotes and convert to Symbol to match KiCAD format
|
|
1297
|
+
uuid_clean = uuid_str.replace('"', "")
|
|
1298
|
+
sexp.append([sexpdata.Symbol("uuid"), sexpdata.Symbol(uuid_clean)])
|
|
1299
|
+
|
|
1300
|
+
return sexp
|
|
1301
|
+
|
|
1302
|
+
def _color_to_rgba(self, color_name: str) -> List[float]:
|
|
1303
|
+
"""Convert color name to RGBA values (0.0-1.0) for KiCAD compatibility."""
|
|
1304
|
+
# Basic color mapping for common colors (0.0-1.0 range)
|
|
1305
|
+
color_map = {
|
|
1306
|
+
"red": [1.0, 0.0, 0.0, 1.0],
|
|
1307
|
+
"blue": [0.0, 0.0, 1.0, 1.0],
|
|
1308
|
+
"green": [0.0, 1.0, 0.0, 1.0],
|
|
1309
|
+
"yellow": [1.0, 1.0, 0.0, 1.0],
|
|
1310
|
+
"magenta": [1.0, 0.0, 1.0, 1.0],
|
|
1311
|
+
"cyan": [0.0, 1.0, 1.0, 1.0],
|
|
1312
|
+
"black": [0.0, 0.0, 0.0, 1.0],
|
|
1313
|
+
"white": [1.0, 1.0, 1.0, 1.0],
|
|
1314
|
+
"gray": [0.5, 0.5, 0.5, 1.0],
|
|
1315
|
+
"grey": [0.5, 0.5, 0.5, 1.0],
|
|
1316
|
+
"orange": [1.0, 0.5, 0.0, 1.0],
|
|
1317
|
+
"purple": [0.5, 0.0, 0.5, 1.0],
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
# Return RGBA values, default to black if color not found
|
|
1321
|
+
return color_map.get(color_name.lower(), [0.0, 0.0, 0.0, 1.0])
|
|
1322
|
+
|
|
1323
|
+
def _color_to_rgb255(self, color_name: str) -> List[int]:
|
|
1324
|
+
"""Convert color name to RGB values (0-255) for KiCAD rectangle graphics."""
|
|
1325
|
+
# Basic color mapping for common colors (0-255 range)
|
|
1326
|
+
color_map = {
|
|
1327
|
+
"red": [255, 0, 0],
|
|
1328
|
+
"blue": [0, 0, 255],
|
|
1329
|
+
"green": [0, 255, 0],
|
|
1330
|
+
"yellow": [255, 255, 0],
|
|
1331
|
+
"magenta": [255, 0, 255],
|
|
1332
|
+
"cyan": [0, 255, 255],
|
|
1333
|
+
"black": [0, 0, 0],
|
|
1334
|
+
"white": [255, 255, 255],
|
|
1335
|
+
"gray": [128, 128, 128],
|
|
1336
|
+
"grey": [128, 128, 128],
|
|
1337
|
+
"orange": [255, 128, 0],
|
|
1338
|
+
"purple": [128, 0, 128],
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
# Return RGB values, default to black if color not found
|
|
1342
|
+
return color_map.get(color_name.lower(), [0, 0, 0])
|
|
1343
|
+
|
|
1045
1344
|
def get_validation_issues(self) -> List[ValidationIssue]:
|
|
1046
1345
|
"""Get list of validation issues from last parse operation."""
|
|
1047
1346
|
return self._validation_issues.copy()
|