kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.2__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.

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