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.
@@ -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([sexpdata.Symbol("generator_version"), schematic_data["generator_version"]])
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(title_block.get(key) for key in ["title", "company", "revision", "date", "comments"]):
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 (required by KiCAD)
316
+ # Add symbol_instances (only if non-empty or for blank schematics)
309
317
  symbol_instances = schematic_data.get("symbol_instances", [])
310
- if symbol_instances or schematic_data.get("components"):
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([sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"])
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
- project_name = "simple_circuit" # TODO: Get from schematic context
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(f"🔧 Using UUID {root_uuid} for component {symbol_data.get('reference', 'unknown')}")
519
- logger.debug(f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}")
520
- sexp.append([
521
- sexpdata.Symbol("instances"),
522
- [sexpdata.Symbol("project"), project_name,
523
- [sexpdata.Symbol("path"), f"/{root_uuid}",
524
- [sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
525
- [sexpdata.Symbol("unit"), symbol_data.get("unit", 1)]]]
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(self, prop_name: str, prop_value: str,
531
- component_pos: Point, offset_index: int,
532
- justify: str = "left", hide: bool = False) -> List[Any]:
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
- # Calculate property position relative to component
535
- # Based on KiCAD's positioning: Reference above component, Value below
536
- prop_x = component_pos.x + 2.54 # Standard offset to the right
537
-
538
- if prop_name == "Reference":
539
- prop_y = component_pos.y - 1.27 # Reference above component
540
- elif prop_name == "Value":
541
- prop_y = component_pos.y + 1.27 # Value below component
542
- else:
543
- prop_y = component_pos.y + (1.27 * (offset_index + 1)) # Other properties below
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
- [sexpdata.Symbol("at"),
550
- round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
551
- round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
552
- 0],
553
- [sexpdata.Symbol("effects"),
554
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
555
- [sexpdata.Symbol("justify"), sexpdata.Symbol(justify)]]
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(self, value: str, component_pos: Point, lib_id: str) -> List[Any]:
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
- [sexpdata.Symbol("at"),
584
- round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
585
- round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
586
- 0],
587
- [sexpdata.Symbol("effects"),
588
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]
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([sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")])
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([sexpdata.Symbol("exclude_from_sim"),
756
- sexpdata.Symbol("yes" if sheet_data.get("exclude_from_sim", False) else "no")])
757
- sexp.append([sexpdata.Symbol("in_bom"),
758
- sexpdata.Symbol("yes" if sheet_data.get("in_bom", True) else "no")])
759
- sexp.append([sexpdata.Symbol("on_board"),
760
- sexpdata.Symbol("yes" if sheet_data.get("on_board", True) else "no")])
761
- sexp.append([sexpdata.Symbol("dnp"),
762
- sexpdata.Symbol("yes" if sheet_data.get("dnp", False) else "no")])
763
- sexp.append([sexpdata.Symbol("fields_autoplaced"),
764
- sexpdata.Symbol("yes" if sheet_data.get("fields_autoplaced", True) else "no")])
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([sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]])
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([sexpdata.Symbol("at"), x, y - 0.7116, 0]) # Above sheet
791
- name_prop.append([sexpdata.Symbol("effects"),
792
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
793
- [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]])
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([sexpdata.Symbol("at"), x, y + h + 0.5754, 0]) # Below sheet
799
- file_prop.append([sexpdata.Symbol("effects"),
800
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
801
- [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("top")]])
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 = [sexpdata.Symbol("pin"), pin_data["name"], sexpdata.Symbol(pin_data.get("pin_type", "input"))]
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([sexpdata.Symbol("exclude_from_sim"),
855
- sexpdata.Symbol("yes" if exclude_sim else "no")])
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([sexpdata.Symbol("exclude_from_sim"),
890
- sexpdata.Symbol("yes" if exclude_sim else "no")])
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([sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)])
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
- [sexpdata.Symbol("pin_numbers"), [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]],
968
- [sexpdata.Symbol("pin_names"), [sexpdata.Symbol("offset"), 0]],
969
- [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("no")],
970
- [sexpdata.Symbol("in_bom"), sexpdata.Symbol("yes")],
971
- [sexpdata.Symbol("on_board"), sexpdata.Symbol("yes")]
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
- [sexpdata.Symbol("property"), "Reference", "R",
978
- [sexpdata.Symbol("at"), 2.032, 0, 90],
979
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
980
- [sexpdata.Symbol("property"), "Value", "R",
981
- [sexpdata.Symbol("at"), 0, 0, 90],
982
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
983
- [sexpdata.Symbol("property"), "Footprint", "",
984
- [sexpdata.Symbol("at"), -1.778, 0, 90],
985
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]],
986
- [sexpdata.Symbol("property"), "Datasheet", "~",
987
- [sexpdata.Symbol("at"), 0, 0, 0],
988
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]]
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
- [sexpdata.Symbol("property"), "Reference", "C",
994
- [sexpdata.Symbol("at"), 0.635, 2.54, 0],
995
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
996
- [sexpdata.Symbol("property"), "Value", "C",
997
- [sexpdata.Symbol("at"), 0.635, -2.54, 0],
998
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
999
- [sexpdata.Symbol("property"), "Footprint", "",
1000
- [sexpdata.Symbol("at"), 0, -1.27, 0],
1001
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]],
1002
- [sexpdata.Symbol("property"), "Datasheet", "~",
1003
- [sexpdata.Symbol("at"), 0, 0, 0],
1004
- [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]]
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 = str(element[0]) if isinstance(element[0], sexpdata.Symbol) else str(element[0])
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"), sheet.get("path", "/"),
1040
- [sexpdata.Symbol("page"), str(sheet.get("page", "1"))]
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()