kicad-sch-api 0.0.2__py3-none-any.whl → 0.1.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.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

@@ -179,7 +179,9 @@ class SExpressionParser:
179
179
  schematic_data = {
180
180
  "version": None,
181
181
  "generator": None,
182
+ "generator_version": None,
182
183
  "uuid": None,
184
+ "paper": None,
183
185
  "title_block": {},
184
186
  "components": [],
185
187
  "wires": [],
@@ -187,6 +189,9 @@ class SExpressionParser:
187
189
  "labels": [],
188
190
  "nets": [],
189
191
  "lib_symbols": {},
192
+ "sheet_instances": [],
193
+ "symbol_instances": [],
194
+ "embedded_fonts": None,
190
195
  }
191
196
 
192
197
  # Process top-level elements
@@ -203,6 +208,10 @@ class SExpressionParser:
203
208
  schematic_data["version"] = str(item[1]) if len(item) > 1 else None
204
209
  elif element_type == "generator":
205
210
  schematic_data["generator"] = item[1] if len(item) > 1 else None
211
+ elif element_type == "generator_version":
212
+ schematic_data["generator_version"] = item[1] if len(item) > 1 else None
213
+ elif element_type == "paper":
214
+ schematic_data["paper"] = item[1] if len(item) > 1 else None
206
215
  elif element_type == "uuid":
207
216
  schematic_data["uuid"] = item[1] if len(item) > 1 else None
208
217
  elif element_type == "title_block":
@@ -225,6 +234,12 @@ class SExpressionParser:
225
234
  schematic_data["labels"].append(label)
226
235
  elif element_type == "lib_symbols":
227
236
  schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
237
+ elif element_type == "sheet_instances":
238
+ schematic_data["sheet_instances"] = self._parse_sheet_instances(item)
239
+ elif element_type == "symbol_instances":
240
+ schematic_data["symbol_instances"] = self._parse_symbol_instances(item)
241
+ elif element_type == "embedded_fonts":
242
+ schematic_data["embedded_fonts"] = item[1] if len(item) > 1 else None
228
243
 
229
244
  return schematic_data
230
245
 
@@ -232,17 +247,22 @@ class SExpressionParser:
232
247
  """Convert internal schematic format to S-expression data."""
233
248
  sexp_data = [sexpdata.Symbol("kicad_sch")]
234
249
 
235
- # Add version and generator
250
+ # Add version and generator info
236
251
  if schematic_data.get("version"):
237
- sexp_data.append([sexpdata.Symbol("version"), schematic_data["version"]])
252
+ sexp_data.append([sexpdata.Symbol("version"), int(schematic_data["version"])])
238
253
  if schematic_data.get("generator"):
239
254
  sexp_data.append([sexpdata.Symbol("generator"), schematic_data["generator"]])
255
+ if schematic_data.get("generator_version"):
256
+ sexp_data.append([sexpdata.Symbol("generator_version"), schematic_data["generator_version"]])
240
257
  if schematic_data.get("uuid"):
241
258
  sexp_data.append([sexpdata.Symbol("uuid"), schematic_data["uuid"]])
259
+ if schematic_data.get("paper"):
260
+ sexp_data.append([sexpdata.Symbol("paper"), schematic_data["paper"]])
242
261
 
243
- # Add title block
244
- if schematic_data.get("title_block"):
245
- sexp_data.append(self._title_block_to_sexp(schematic_data["title_block"]))
262
+ # Add title block only if it has non-default content
263
+ 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"]):
265
+ sexp_data.append(self._title_block_to_sexp(title_block))
246
266
 
247
267
  # Add lib_symbols (always include for KiCAD compatibility)
248
268
  lib_symbols = schematic_data.get("lib_symbols", {})
@@ -250,7 +270,7 @@ class SExpressionParser:
250
270
 
251
271
  # Add components
252
272
  for component in schematic_data.get("components", []):
253
- sexp_data.append(self._symbol_to_sexp(component))
273
+ sexp_data.append(self._symbol_to_sexp(component, schematic_data.get("uuid")))
254
274
 
255
275
  # Add wires
256
276
  for wire in schematic_data.get("wires", []):
@@ -264,11 +284,36 @@ class SExpressionParser:
264
284
  for label in schematic_data.get("labels", []):
265
285
  sexp_data.append(self._label_to_sexp(label))
266
286
 
287
+ # Add hierarchical labels
288
+ for hlabel in schematic_data.get("hierarchical_labels", []):
289
+ sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
290
+
291
+ # Add hierarchical sheets
292
+ for sheet in schematic_data.get("sheets", []):
293
+ sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
294
+
295
+ # Add text elements
296
+ for text in schematic_data.get("texts", []):
297
+ sexp_data.append(self._text_to_sexp(text))
298
+
299
+ # Add text boxes
300
+ for text_box in schematic_data.get("text_boxes", []):
301
+ sexp_data.append(self._text_box_to_sexp(text_box))
302
+
303
+ # Add sheet_instances (required by KiCAD)
304
+ sheet_instances = schematic_data.get("sheet_instances", [])
305
+ if sheet_instances:
306
+ sexp_data.append(self._sheet_instances_to_sexp(sheet_instances))
307
+
267
308
  # Add symbol_instances (required by KiCAD)
268
309
  symbol_instances = schematic_data.get("symbol_instances", [])
269
310
  if symbol_instances or schematic_data.get("components"):
270
311
  sexp_data.append([sexpdata.Symbol("symbol_instances")])
271
312
 
313
+ # Add embedded_fonts (required by KiCAD)
314
+ if schematic_data.get("embedded_fonts") is not None:
315
+ sexp_data.append([sexpdata.Symbol("embedded_fonts"), schematic_data["embedded_fonts"]])
316
+
272
317
  return sexp_data
273
318
 
274
319
  def _parse_title_block(self, item: List[Any]) -> Dict[str, Any]:
@@ -377,11 +422,21 @@ class SExpressionParser:
377
422
  def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
378
423
  """Convert title block to S-expression."""
379
424
  sexp = [sexpdata.Symbol("title_block")]
380
- for key, value in title_block.items():
381
- sexp.append([sexpdata.Symbol(key), value])
425
+
426
+ # Add standard fields
427
+ for key in ["title", "date", "rev", "company"]:
428
+ if key in title_block and title_block[key]:
429
+ sexp.append([sexpdata.Symbol(key), title_block[key]])
430
+
431
+ # Add comments with special formatting
432
+ comments = title_block.get("comments", {})
433
+ if isinstance(comments, dict):
434
+ for comment_num, comment_text in comments.items():
435
+ sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
436
+
382
437
  return sexp
383
438
 
384
- def _symbol_to_sexp(self, symbol_data: Dict[str, Any]) -> List[Any]:
439
+ def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
385
440
  """Convert symbol to S-expression."""
386
441
  sexp = [sexpdata.Symbol("symbol")]
387
442
 
@@ -398,45 +453,494 @@ class SExpressionParser:
398
453
  # Always include rotation for format consistency with KiCAD
399
454
  sexp.append([sexpdata.Symbol("at"), x, y, r])
400
455
 
456
+ # Add unit (required by KiCAD)
457
+ unit = symbol_data.get("unit", 1)
458
+ sexp.append([sexpdata.Symbol("unit"), unit])
459
+
460
+ # Add simulation and board settings (required by KiCAD)
461
+ sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
462
+ 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"])
464
+ sexp.append([sexpdata.Symbol("dnp"), "no"])
465
+ sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
466
+
401
467
  if symbol_data.get("uuid"):
402
468
  sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
403
469
 
404
- # Add properties
470
+ # Add properties with proper positioning and effects
471
+ lib_id = symbol_data.get("lib_id", "")
472
+ is_power_symbol = "power:" in lib_id
473
+
405
474
  if symbol_data.get("reference"):
406
- sexp.append([sexpdata.Symbol("property"), "Reference", symbol_data["reference"]])
475
+ # Power symbol references should be hidden by default
476
+ ref_hide = is_power_symbol
477
+ ref_prop = self._create_property_with_positioning(
478
+ "Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
479
+ )
480
+ sexp.append(ref_prop)
481
+
407
482
  if symbol_data.get("value"):
408
- sexp.append([sexpdata.Symbol("property"), "Value", symbol_data["value"]])
483
+ # Power symbol values need different positioning
484
+ if is_power_symbol:
485
+ val_prop = self._create_power_symbol_value_property(
486
+ symbol_data["value"], pos, lib_id
487
+ )
488
+ else:
489
+ val_prop = self._create_property_with_positioning(
490
+ "Value", symbol_data["value"], pos, 1, "left"
491
+ )
492
+ sexp.append(val_prop)
493
+
409
494
  footprint = symbol_data.get("footprint")
410
495
  if footprint is not None: # Include empty strings but not None
411
- sexp.append([sexpdata.Symbol("property"), "Footprint", footprint])
496
+ fp_prop = self._create_property_with_positioning(
497
+ "Footprint", footprint, pos, 2, "left", hide=True
498
+ )
499
+ sexp.append(fp_prop)
412
500
 
413
501
  for prop_name, prop_value in symbol_data.get("properties", {}).items():
414
- # Escape quotes in property values for proper S-expression format
415
502
  escaped_value = str(prop_value).replace('"', '\\"')
416
- sexp.append([sexpdata.Symbol("property"), prop_name, escaped_value])
417
-
418
- # Add BOM and board settings
419
- sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
420
- sexp.append(
421
- [sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
422
- )
503
+ prop = self._create_property_with_positioning(
504
+ prop_name, escaped_value, pos, 3, "left", hide=True
505
+ )
506
+ sexp.append(prop)
507
+
508
+ # Add pin UUID assignments (required by KiCAD)
509
+ for pin in symbol_data.get("pins", []):
510
+ pin_uuid = str(uuid.uuid4())
511
+ # Ensure pin number is a string for proper quoting
512
+ pin_number = str(pin.number)
513
+ sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
514
+
515
+ # 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')}")
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
+ ])
423
527
 
424
528
  return sexp
425
529
 
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]:
533
+ """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
+
545
+ prop_sexp = [
546
+ sexpdata.Symbol("property"),
547
+ prop_name,
548
+ 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)]]
556
+ ]
557
+
558
+ if hide:
559
+ prop_sexp[4].append([sexpdata.Symbol("hide"), sexpdata.Symbol("yes")])
560
+
561
+ return prop_sexp
562
+
563
+ def _create_power_symbol_value_property(self, value: str, component_pos: Point, lib_id: str) -> List[Any]:
564
+ """Create Value property for power symbols with correct positioning."""
565
+ # Power symbols have different value positioning based on type
566
+ if "GND" in lib_id:
567
+ # GND value goes below the symbol
568
+ prop_x = component_pos.x
569
+ prop_y = component_pos.y + 5.08 # Below GND symbol
570
+ elif "+3.3V" in lib_id or "VDD" in lib_id:
571
+ # Positive voltage values go below the symbol
572
+ prop_x = component_pos.x
573
+ prop_y = component_pos.y - 5.08 # Above symbol (negative offset)
574
+ else:
575
+ # Default power symbol positioning
576
+ prop_x = component_pos.x
577
+ prop_y = component_pos.y + 3.556
578
+
579
+ prop_sexp = [
580
+ sexpdata.Symbol("property"),
581
+ "Value",
582
+ 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]]]
589
+ ]
590
+
591
+ return prop_sexp
592
+
426
593
  def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
427
594
  """Convert wire to S-expression."""
428
- # Implementation for wire conversion
429
- return [sexpdata.Symbol("wire")]
595
+ sexp = [sexpdata.Symbol("wire")]
596
+
597
+ # Add points (pts section)
598
+ points = wire_data.get("points", [])
599
+ if len(points) >= 2:
600
+ pts_sexp = [sexpdata.Symbol("pts")]
601
+ for point in points:
602
+ if isinstance(point, dict):
603
+ x, y = point["x"], point["y"]
604
+ elif isinstance(point, (list, tuple)) and len(point) >= 2:
605
+ x, y = point[0], point[1]
606
+ else:
607
+ # Assume it's a Point object
608
+ x, y = point.x, point.y
609
+
610
+ # Format coordinates properly (avoid unnecessary .0 for integers)
611
+ if isinstance(x, float) and x.is_integer():
612
+ x = int(x)
613
+ if isinstance(y, float) and y.is_integer():
614
+ y = int(y)
615
+
616
+ pts_sexp.append([sexpdata.Symbol("xy"), x, y])
617
+ sexp.append(pts_sexp)
618
+
619
+ # Add stroke information
620
+ stroke_width = wire_data.get("stroke_width", 0)
621
+ stroke_type = wire_data.get("stroke_type", "default")
622
+ stroke_sexp = [sexpdata.Symbol("stroke")]
623
+
624
+ # Format stroke width (use int for 0, preserve float for others)
625
+ if isinstance(stroke_width, float) and stroke_width == 0.0:
626
+ stroke_width = 0
627
+
628
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
629
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
630
+ sexp.append(stroke_sexp)
631
+
632
+ # Add UUID
633
+ if "uuid" in wire_data:
634
+ sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
635
+
636
+ return sexp
430
637
 
431
638
  def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
432
639
  """Convert junction to S-expression."""
433
- # Implementation for junction conversion
434
- return [sexpdata.Symbol("junction")]
640
+ sexp = [sexpdata.Symbol("junction")]
641
+
642
+ # Add position
643
+ pos = junction_data["position"]
644
+ if isinstance(pos, dict):
645
+ x, y = pos["x"], pos["y"]
646
+ elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
647
+ x, y = pos[0], pos[1]
648
+ else:
649
+ # Assume it's a Point object
650
+ x, y = pos.x, pos.y
651
+
652
+ # Format coordinates properly
653
+ if isinstance(x, float) and x.is_integer():
654
+ x = int(x)
655
+ if isinstance(y, float) and y.is_integer():
656
+ y = int(y)
657
+
658
+ sexp.append([sexpdata.Symbol("at"), x, y])
659
+
660
+ # Add diameter
661
+ diameter = junction_data.get("diameter", 0)
662
+ sexp.append([sexpdata.Symbol("diameter"), diameter])
663
+
664
+ # Add color (RGBA)
665
+ color = junction_data.get("color", (0, 0, 0, 0))
666
+ if isinstance(color, (list, tuple)) and len(color) >= 4:
667
+ sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
668
+ else:
669
+ sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
670
+
671
+ # Add UUID
672
+ if "uuid" in junction_data:
673
+ sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
674
+
675
+ return sexp
435
676
 
436
677
  def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
437
- """Convert label to S-expression."""
438
- # Implementation for label conversion
439
- return [sexpdata.Symbol("label")]
678
+ """Convert local label to S-expression."""
679
+ sexp = [sexpdata.Symbol("label"), label_data["text"]]
680
+
681
+ # Add position
682
+ pos = label_data["position"]
683
+ x, y = pos["x"], pos["y"]
684
+ rotation = label_data.get("rotation", 0)
685
+
686
+ # Format coordinates properly
687
+ if isinstance(x, float) and x.is_integer():
688
+ x = int(x)
689
+ if isinstance(y, float) and y.is_integer():
690
+ y = int(y)
691
+
692
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
693
+
694
+ # Add effects (font properties)
695
+ size = label_data.get("size", 1.27)
696
+ effects = [sexpdata.Symbol("effects")]
697
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
698
+ effects.append(font)
699
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")])
700
+ sexp.append(effects)
701
+
702
+ # Add UUID
703
+ if "uuid" in label_data:
704
+ sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
705
+
706
+ return sexp
707
+
708
+ def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
709
+ """Convert hierarchical label to S-expression."""
710
+ sexp = [sexpdata.Symbol("hierarchical_label"), hlabel_data["text"]]
711
+
712
+ # Add shape
713
+ shape = hlabel_data.get("shape", "input")
714
+ sexp.append([sexpdata.Symbol("shape"), sexpdata.Symbol(shape)])
715
+
716
+ # Add position
717
+ pos = hlabel_data["position"]
718
+ x, y = pos["x"], pos["y"]
719
+ rotation = hlabel_data.get("rotation", 0)
720
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
721
+
722
+ # Add effects (font properties)
723
+ size = hlabel_data.get("size", 1.27)
724
+ effects = [sexpdata.Symbol("effects")]
725
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
726
+ effects.append(font)
727
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol("left")])
728
+ sexp.append(effects)
729
+
730
+ # Add UUID
731
+ if "uuid" in hlabel_data:
732
+ sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
733
+
734
+ return sexp
735
+
736
+ def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
737
+ """Convert hierarchical sheet to S-expression."""
738
+ sexp = [sexpdata.Symbol("sheet")]
739
+
740
+ # Add position
741
+ pos = sheet_data["position"]
742
+ x, y = pos["x"], pos["y"]
743
+ if isinstance(x, float) and x.is_integer():
744
+ x = int(x)
745
+ if isinstance(y, float) and y.is_integer():
746
+ y = int(y)
747
+ sexp.append([sexpdata.Symbol("at"), x, y])
748
+
749
+ # Add size
750
+ size = sheet_data["size"]
751
+ w, h = size["width"], size["height"]
752
+ sexp.append([sexpdata.Symbol("size"), w, h])
753
+
754
+ # 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
+
766
+ # Add stroke
767
+ stroke_width = sheet_data.get("stroke_width", 0.1524)
768
+ stroke_type = sheet_data.get("stroke_type", "solid")
769
+ stroke_sexp = [sexpdata.Symbol("stroke")]
770
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
771
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
772
+ sexp.append(stroke_sexp)
773
+
774
+ # Add fill
775
+ fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
776
+ fill_sexp = [sexpdata.Symbol("fill")]
777
+ fill_sexp.append([sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]])
778
+ sexp.append(fill_sexp)
779
+
780
+ # Add UUID
781
+ if "uuid" in sheet_data:
782
+ sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
783
+
784
+ # Add sheet properties (name and filename)
785
+ name = sheet_data.get("name", "Sheet")
786
+ filename = sheet_data.get("filename", "sheet.kicad_sch")
787
+
788
+ # Sheetname property
789
+ 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")]])
794
+ sexp.append(name_prop)
795
+
796
+ # Sheetfile property
797
+ 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")]])
802
+ sexp.append(file_prop)
803
+
804
+ # Add sheet pins if any
805
+ for pin in sheet_data.get("pins", []):
806
+ pin_sexp = self._sheet_pin_to_sexp(pin)
807
+ sexp.append(pin_sexp)
808
+
809
+ # Add instances
810
+ if schematic_uuid:
811
+ instances_sexp = [sexpdata.Symbol("instances")]
812
+ project_name = sheet_data.get("project_name", "")
813
+ page_number = sheet_data.get("page_number", "2")
814
+ project_sexp = [sexpdata.Symbol("project"), project_name]
815
+ path_sexp = [sexpdata.Symbol("path"), f"/{schematic_uuid}"]
816
+ path_sexp.append([sexpdata.Symbol("page"), page_number])
817
+ project_sexp.append(path_sexp)
818
+ instances_sexp.append(project_sexp)
819
+ sexp.append(instances_sexp)
820
+
821
+ return sexp
822
+
823
+ def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
824
+ """Convert sheet pin to S-expression."""
825
+ pin_sexp = [sexpdata.Symbol("pin"), pin_data["name"], sexpdata.Symbol(pin_data.get("pin_type", "input"))]
826
+
827
+ # Add position
828
+ pos = pin_data["position"]
829
+ x, y = pos["x"], pos["y"]
830
+ rotation = pin_data.get("rotation", 0)
831
+ pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
832
+
833
+ # Add UUID
834
+ if "uuid" in pin_data:
835
+ pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
836
+
837
+ # Add effects
838
+ size = pin_data.get("size", 1.27)
839
+ effects = [sexpdata.Symbol("effects")]
840
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
841
+ effects.append(font)
842
+ justify = pin_data.get("justify", "right")
843
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
844
+ pin_sexp.append(effects)
845
+
846
+ return pin_sexp
847
+
848
+ def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
849
+ """Convert text element to S-expression."""
850
+ sexp = [sexpdata.Symbol("text"), text_data["text"]]
851
+
852
+ # Add exclude_from_sim
853
+ 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
+
857
+ # Add position
858
+ pos = text_data["position"]
859
+ x, y = pos["x"], pos["y"]
860
+ rotation = text_data.get("rotation", 0)
861
+
862
+ # Format coordinates properly
863
+ if isinstance(x, float) and x.is_integer():
864
+ x = int(x)
865
+ if isinstance(y, float) and y.is_integer():
866
+ y = int(y)
867
+
868
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
869
+
870
+ # Add effects (font properties)
871
+ size = text_data.get("size", 1.27)
872
+ effects = [sexpdata.Symbol("effects")]
873
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
874
+ effects.append(font)
875
+ sexp.append(effects)
876
+
877
+ # Add UUID
878
+ if "uuid" in text_data:
879
+ sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
880
+
881
+ return sexp
882
+
883
+ def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
884
+ """Convert text box element to S-expression."""
885
+ sexp = [sexpdata.Symbol("text_box"), text_box_data["text"]]
886
+
887
+ # Add exclude_from_sim
888
+ 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
+
892
+ # Add position
893
+ pos = text_box_data["position"]
894
+ x, y = pos["x"], pos["y"]
895
+ rotation = text_box_data.get("rotation", 0)
896
+
897
+ # Format coordinates properly
898
+ if isinstance(x, float) and x.is_integer():
899
+ x = int(x)
900
+ if isinstance(y, float) and y.is_integer():
901
+ y = int(y)
902
+
903
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
904
+
905
+ # Add size
906
+ size = text_box_data["size"]
907
+ w, h = size["width"], size["height"]
908
+ sexp.append([sexpdata.Symbol("size"), w, h])
909
+
910
+ # Add margins
911
+ margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
912
+ sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
913
+
914
+ # Add stroke
915
+ stroke_width = text_box_data.get("stroke_width", 0)
916
+ stroke_type = text_box_data.get("stroke_type", "solid")
917
+ stroke_sexp = [sexpdata.Symbol("stroke")]
918
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
919
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
920
+ sexp.append(stroke_sexp)
921
+
922
+ # Add fill
923
+ fill_type = text_box_data.get("fill_type", "none")
924
+ fill_sexp = [sexpdata.Symbol("fill")]
925
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
926
+ sexp.append(fill_sexp)
927
+
928
+ # Add effects (font properties and justification)
929
+ font_size = text_box_data.get("font_size", 1.27)
930
+ justify_h = text_box_data.get("justify_horizontal", "left")
931
+ justify_v = text_box_data.get("justify_vertical", "top")
932
+
933
+ effects = [sexpdata.Symbol("effects")]
934
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
935
+ effects.append(font)
936
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)])
937
+ sexp.append(effects)
938
+
939
+ # Add UUID
940
+ if "uuid" in text_box_data:
941
+ sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
942
+
943
+ return sexp
440
944
 
441
945
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
442
946
  """Convert lib_symbols to S-expression."""
@@ -444,12 +948,99 @@ class SExpressionParser:
444
948
 
445
949
  # Add each symbol definition
446
950
  for symbol_name, symbol_def in lib_symbols.items():
447
- if isinstance(symbol_def, dict):
448
- symbol_sexp = [sexpdata.Symbol("symbol"), symbol_name]
449
- # Add symbol definition details (for now, basic structure)
951
+ if isinstance(symbol_def, list):
952
+ # Raw S-expression data from parsed library file - use directly
953
+ sexp.append(symbol_def)
954
+ elif isinstance(symbol_def, dict):
955
+ # Dictionary format - convert to S-expression
956
+ symbol_sexp = self._create_basic_symbol_definition(symbol_name)
450
957
  sexp.append(symbol_sexp)
451
958
 
452
959
  return sexp
960
+
961
+ def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
962
+ """Create a basic symbol definition for KiCAD compatibility."""
963
+ symbol_sexp = [sexpdata.Symbol("symbol"), lib_id]
964
+
965
+ # 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
+
974
+ # Add basic properties for the symbol
975
+ 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
+
991
+ 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
+
1007
+ # Add basic graphics and pins (minimal for now)
1008
+ symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
1009
+
1010
+ return symbol_sexp
1011
+
1012
+ def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
1013
+ """Parse sheet_instances section."""
1014
+ sheet_instances = []
1015
+ for sheet_item in item[1:]: # Skip 'sheet_instances' header
1016
+ if isinstance(sheet_item, list) and len(sheet_item) > 0:
1017
+ sheet_data = {"path": "/", "page": "1"}
1018
+ for element in sheet_item[1:]: # Skip element header
1019
+ if isinstance(element, list) and len(element) >= 2:
1020
+ key = str(element[0]) if isinstance(element[0], sexpdata.Symbol) else str(element[0])
1021
+ if key == "path":
1022
+ sheet_data["path"] = element[1]
1023
+ elif key == "page":
1024
+ sheet_data["page"] = element[1]
1025
+ sheet_instances.append(sheet_data)
1026
+ return sheet_instances
1027
+
1028
+ def _parse_symbol_instances(self, item: List[Any]) -> List[Any]:
1029
+ """Parse symbol_instances section."""
1030
+ # For now, just return the raw structure minus the header
1031
+ return item[1:] if len(item) > 1 else []
1032
+
1033
+ def _sheet_instances_to_sexp(self, sheet_instances: List[Dict[str, Any]]) -> List[Any]:
1034
+ """Convert sheet_instances to S-expression."""
1035
+ sexp = [sexpdata.Symbol("sheet_instances")]
1036
+ for sheet in sheet_instances:
1037
+ # Create: (path "/" (page "1"))
1038
+ sheet_sexp = [
1039
+ sexpdata.Symbol("path"), sheet.get("path", "/"),
1040
+ [sexpdata.Symbol("page"), str(sheet.get("page", "1"))]
1041
+ ]
1042
+ sexp.append(sheet_sexp)
1043
+ return sexp
453
1044
 
454
1045
  def get_validation_issues(self) -> List[ValidationIssue]:
455
1046
  """Get list of validation issues from last parse operation."""