kicad-sch-api 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -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
@@ -200,9 +205,13 @@ class SExpressionParser:
200
205
  element_type = str(item[0]) if isinstance(item[0], sexpdata.Symbol) else None
201
206
 
202
207
  if element_type == "version":
203
- schematic_data["version"] = item[1] if len(item) > 1 else None
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,25 +247,29 @@ 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
262
  # Add title block
244
263
  if schematic_data.get("title_block"):
245
264
  sexp_data.append(self._title_block_to_sexp(schematic_data["title_block"]))
246
265
 
247
- # Add lib_symbols
248
- if schematic_data.get("lib_symbols"):
249
- sexp_data.append(self._lib_symbols_to_sexp(schematic_data["lib_symbols"]))
266
+ # Add lib_symbols (always include for KiCAD compatibility)
267
+ lib_symbols = schematic_data.get("lib_symbols", {})
268
+ sexp_data.append(self._lib_symbols_to_sexp(lib_symbols))
250
269
 
251
270
  # Add components
252
271
  for component in schematic_data.get("components", []):
253
- sexp_data.append(self._symbol_to_sexp(component))
272
+ sexp_data.append(self._symbol_to_sexp(component, schematic_data.get("uuid")))
254
273
 
255
274
  # Add wires
256
275
  for wire in schematic_data.get("wires", []):
@@ -264,6 +283,36 @@ class SExpressionParser:
264
283
  for label in schematic_data.get("labels", []):
265
284
  sexp_data.append(self._label_to_sexp(label))
266
285
 
286
+ # Add hierarchical labels
287
+ for hlabel in schematic_data.get("hierarchical_labels", []):
288
+ sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
289
+
290
+ # Add hierarchical sheets
291
+ for sheet in schematic_data.get("sheets", []):
292
+ sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
293
+
294
+ # Add text elements
295
+ for text in schematic_data.get("texts", []):
296
+ sexp_data.append(self._text_to_sexp(text))
297
+
298
+ # Add text boxes
299
+ for text_box in schematic_data.get("text_boxes", []):
300
+ sexp_data.append(self._text_box_to_sexp(text_box))
301
+
302
+ # Add sheet_instances (required by KiCAD)
303
+ sheet_instances = schematic_data.get("sheet_instances", [])
304
+ if sheet_instances:
305
+ sexp_data.append(self._sheet_instances_to_sexp(sheet_instances))
306
+
307
+ # Add symbol_instances (required by KiCAD)
308
+ symbol_instances = schematic_data.get("symbol_instances", [])
309
+ if symbol_instances or schematic_data.get("components"):
310
+ sexp_data.append([sexpdata.Symbol("symbol_instances")])
311
+
312
+ # Add embedded_fonts (required by KiCAD)
313
+ if schematic_data.get("embedded_fonts") is not None:
314
+ sexp_data.append([sexpdata.Symbol("embedded_fonts"), schematic_data["embedded_fonts"]])
315
+
267
316
  return sexp_data
268
317
 
269
318
  def _parse_title_block(self, item: List[Any]) -> Dict[str, Any]:
@@ -321,7 +370,11 @@ class SExpressionParser:
321
370
  elif prop_name == "Footprint":
322
371
  symbol_data["footprint"] = prop_data.get("value")
323
372
  else:
324
- symbol_data["properties"][prop_name] = prop_data.get("value")
373
+ # Unescape quotes in property values when loading
374
+ prop_value = prop_data.get("value")
375
+ if prop_value:
376
+ prop_value = str(prop_value).replace('\\"', '"')
377
+ symbol_data["properties"][prop_name] = prop_value
325
378
  elif element_type == "in_bom":
326
379
  symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
327
380
  elif element_type == "on_board":
@@ -372,62 +425,611 @@ class SExpressionParser:
372
425
  sexp.append([sexpdata.Symbol(key), value])
373
426
  return sexp
374
427
 
375
- def _symbol_to_sexp(self, symbol_data: Dict[str, Any]) -> List[Any]:
428
+ def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
376
429
  """Convert symbol to S-expression."""
377
430
  sexp = [sexpdata.Symbol("symbol")]
378
431
 
379
432
  if symbol_data.get("lib_id"):
380
433
  sexp.append([sexpdata.Symbol("lib_id"), symbol_data["lib_id"]])
381
434
 
382
- # Add position and rotation
435
+ # Add position and rotation (preserve original format)
383
436
  pos = symbol_data.get("position", Point(0, 0))
384
437
  rotation = symbol_data.get("rotation", 0)
385
- if rotation != 0:
386
- sexp.append([sexpdata.Symbol("at"), pos.x, pos.y, rotation])
387
- else:
388
- sexp.append([sexpdata.Symbol("at"), pos.x, pos.y])
438
+ # Format numbers as integers if they are whole numbers
439
+ x = int(pos.x) if pos.x == int(pos.x) else pos.x
440
+ y = int(pos.y) if pos.y == int(pos.y) else pos.y
441
+ r = int(rotation) if rotation == int(rotation) else rotation
442
+ # Always include rotation for format consistency with KiCAD
443
+ sexp.append([sexpdata.Symbol("at"), x, y, r])
444
+
445
+ # Add unit (required by KiCAD)
446
+ unit = symbol_data.get("unit", 1)
447
+ sexp.append([sexpdata.Symbol("unit"), unit])
448
+
449
+ # Add simulation and board settings (required by KiCAD)
450
+ sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
451
+ sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
452
+ sexp.append([sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"])
453
+ sexp.append([sexpdata.Symbol("dnp"), "no"])
454
+ sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
389
455
 
390
456
  if symbol_data.get("uuid"):
391
457
  sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
392
458
 
393
- # Add properties
459
+ # Add properties with proper positioning and effects
460
+ lib_id = symbol_data.get("lib_id", "")
461
+ is_power_symbol = "power:" in lib_id
462
+
394
463
  if symbol_data.get("reference"):
395
- sexp.append([sexpdata.Symbol("property"), "Reference", symbol_data["reference"]])
464
+ # Power symbol references should be hidden by default
465
+ ref_hide = is_power_symbol
466
+ ref_prop = self._create_property_with_positioning(
467
+ "Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
468
+ )
469
+ sexp.append(ref_prop)
470
+
396
471
  if symbol_data.get("value"):
397
- sexp.append([sexpdata.Symbol("property"), "Value", symbol_data["value"]])
398
- if symbol_data.get("footprint"):
399
- sexp.append([sexpdata.Symbol("property"), "Footprint", symbol_data["footprint"]])
472
+ # Power symbol values need different positioning
473
+ if is_power_symbol:
474
+ val_prop = self._create_power_symbol_value_property(
475
+ symbol_data["value"], pos, lib_id
476
+ )
477
+ else:
478
+ val_prop = self._create_property_with_positioning(
479
+ "Value", symbol_data["value"], pos, 1, "left"
480
+ )
481
+ sexp.append(val_prop)
482
+
483
+ footprint = symbol_data.get("footprint")
484
+ if footprint is not None: # Include empty strings but not None
485
+ fp_prop = self._create_property_with_positioning(
486
+ "Footprint", footprint, pos, 2, "left", hide=True
487
+ )
488
+ sexp.append(fp_prop)
400
489
 
401
490
  for prop_name, prop_value in symbol_data.get("properties", {}).items():
402
- sexp.append([sexpdata.Symbol("property"), prop_name, prop_value])
403
-
404
- # Add BOM and board settings
405
- sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
406
- sexp.append(
407
- [sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
408
- )
491
+ escaped_value = str(prop_value).replace('"', '\\"')
492
+ prop = self._create_property_with_positioning(
493
+ prop_name, escaped_value, pos, 3, "left", hide=True
494
+ )
495
+ sexp.append(prop)
496
+
497
+ # Add pin UUID assignments (required by KiCAD)
498
+ for pin in symbol_data.get("pins", []):
499
+ pin_uuid = str(uuid.uuid4())
500
+ # Ensure pin number is a string for proper quoting
501
+ pin_number = str(pin.number)
502
+ sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
503
+
504
+ # Add instances section (required by KiCAD)
505
+ project_name = "simple_circuit" # TODO: Get from schematic context
506
+ root_uuid = schematic_uuid or symbol_data.get("root_uuid", str(uuid.uuid4()))
507
+ logger.debug(f"🔧 Using UUID {root_uuid} for component {symbol_data.get('reference', 'unknown')}")
508
+ logger.debug(f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}")
509
+ sexp.append([
510
+ sexpdata.Symbol("instances"),
511
+ [sexpdata.Symbol("project"), project_name,
512
+ [sexpdata.Symbol("path"), f"/{root_uuid}",
513
+ [sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
514
+ [sexpdata.Symbol("unit"), symbol_data.get("unit", 1)]]]
515
+ ])
409
516
 
410
517
  return sexp
411
518
 
519
+ def _create_property_with_positioning(self, prop_name: str, prop_value: str,
520
+ component_pos: Point, offset_index: int,
521
+ justify: str = "left", hide: bool = False) -> List[Any]:
522
+ """Create a property with proper positioning and effects like KiCAD."""
523
+ # Calculate property position relative to component
524
+ # Based on KiCAD's positioning: Reference above component, Value below
525
+ prop_x = component_pos.x + 2.54 # Standard offset to the right
526
+
527
+ if prop_name == "Reference":
528
+ prop_y = component_pos.y - 1.27 # Reference above component
529
+ elif prop_name == "Value":
530
+ prop_y = component_pos.y + 1.27 # Value below component
531
+ else:
532
+ prop_y = component_pos.y + (1.27 * (offset_index + 1)) # Other properties below
533
+
534
+ prop_sexp = [
535
+ sexpdata.Symbol("property"),
536
+ prop_name,
537
+ prop_value,
538
+ [sexpdata.Symbol("at"),
539
+ round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
540
+ round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
541
+ 0],
542
+ [sexpdata.Symbol("effects"),
543
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
544
+ [sexpdata.Symbol("justify"), sexpdata.Symbol(justify)]]
545
+ ]
546
+
547
+ if hide:
548
+ prop_sexp[4].append([sexpdata.Symbol("hide"), sexpdata.Symbol("yes")])
549
+
550
+ return prop_sexp
551
+
552
+ def _create_power_symbol_value_property(self, value: str, component_pos: Point, lib_id: str) -> List[Any]:
553
+ """Create Value property for power symbols with correct positioning."""
554
+ # Power symbols have different value positioning based on type
555
+ if "GND" in lib_id:
556
+ # GND value goes below the symbol
557
+ prop_x = component_pos.x
558
+ prop_y = component_pos.y + 5.08 # Below GND symbol
559
+ elif "+3.3V" in lib_id or "VDD" in lib_id:
560
+ # Positive voltage values go below the symbol
561
+ prop_x = component_pos.x
562
+ prop_y = component_pos.y - 5.08 # Above symbol (negative offset)
563
+ else:
564
+ # Default power symbol positioning
565
+ prop_x = component_pos.x
566
+ prop_y = component_pos.y + 3.556
567
+
568
+ prop_sexp = [
569
+ sexpdata.Symbol("property"),
570
+ "Value",
571
+ value,
572
+ [sexpdata.Symbol("at"),
573
+ round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
574
+ round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
575
+ 0],
576
+ [sexpdata.Symbol("effects"),
577
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]
578
+ ]
579
+
580
+ return prop_sexp
581
+
412
582
  def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
413
583
  """Convert wire to S-expression."""
414
- # Implementation for wire conversion
415
- return [sexpdata.Symbol("wire")]
584
+ sexp = [sexpdata.Symbol("wire")]
585
+
586
+ # Add points (pts section)
587
+ points = wire_data.get("points", [])
588
+ if len(points) >= 2:
589
+ pts_sexp = [sexpdata.Symbol("pts")]
590
+ for point in points:
591
+ if isinstance(point, dict):
592
+ x, y = point["x"], point["y"]
593
+ elif isinstance(point, (list, tuple)) and len(point) >= 2:
594
+ x, y = point[0], point[1]
595
+ else:
596
+ # Assume it's a Point object
597
+ x, y = point.x, point.y
598
+
599
+ # Format coordinates properly (avoid unnecessary .0 for integers)
600
+ if isinstance(x, float) and x.is_integer():
601
+ x = int(x)
602
+ if isinstance(y, float) and y.is_integer():
603
+ y = int(y)
604
+
605
+ pts_sexp.append([sexpdata.Symbol("xy"), x, y])
606
+ sexp.append(pts_sexp)
607
+
608
+ # Add stroke information
609
+ stroke_width = wire_data.get("stroke_width", 0)
610
+ stroke_type = wire_data.get("stroke_type", "default")
611
+ stroke_sexp = [sexpdata.Symbol("stroke")]
612
+
613
+ # Format stroke width (use int for 0, preserve float for others)
614
+ if isinstance(stroke_width, float) and stroke_width == 0.0:
615
+ stroke_width = 0
616
+
617
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
618
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
619
+ sexp.append(stroke_sexp)
620
+
621
+ # Add UUID
622
+ if "uuid" in wire_data:
623
+ sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
624
+
625
+ return sexp
416
626
 
417
627
  def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
418
628
  """Convert junction to S-expression."""
419
- # Implementation for junction conversion
420
- return [sexpdata.Symbol("junction")]
629
+ sexp = [sexpdata.Symbol("junction")]
630
+
631
+ # Add position
632
+ pos = junction_data["position"]
633
+ if isinstance(pos, dict):
634
+ x, y = pos["x"], pos["y"]
635
+ elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
636
+ x, y = pos[0], pos[1]
637
+ else:
638
+ # Assume it's a Point object
639
+ x, y = pos.x, pos.y
640
+
641
+ # Format coordinates properly
642
+ if isinstance(x, float) and x.is_integer():
643
+ x = int(x)
644
+ if isinstance(y, float) and y.is_integer():
645
+ y = int(y)
646
+
647
+ sexp.append([sexpdata.Symbol("at"), x, y])
648
+
649
+ # Add diameter
650
+ diameter = junction_data.get("diameter", 0)
651
+ sexp.append([sexpdata.Symbol("diameter"), diameter])
652
+
653
+ # Add color (RGBA)
654
+ color = junction_data.get("color", (0, 0, 0, 0))
655
+ if isinstance(color, (list, tuple)) and len(color) >= 4:
656
+ sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
657
+ else:
658
+ sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
659
+
660
+ # Add UUID
661
+ if "uuid" in junction_data:
662
+ sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
663
+
664
+ return sexp
421
665
 
422
666
  def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
423
- """Convert label to S-expression."""
424
- # Implementation for label conversion
425
- return [sexpdata.Symbol("label")]
667
+ """Convert local label to S-expression."""
668
+ sexp = [sexpdata.Symbol("label"), label_data["text"]]
669
+
670
+ # Add position
671
+ pos = label_data["position"]
672
+ x, y = pos["x"], pos["y"]
673
+ rotation = label_data.get("rotation", 0)
674
+
675
+ # Format coordinates properly
676
+ if isinstance(x, float) and x.is_integer():
677
+ x = int(x)
678
+ if isinstance(y, float) and y.is_integer():
679
+ y = int(y)
680
+
681
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
682
+
683
+ # Add effects (font properties)
684
+ size = label_data.get("size", 1.27)
685
+ effects = [sexpdata.Symbol("effects")]
686
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
687
+ effects.append(font)
688
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")])
689
+ sexp.append(effects)
690
+
691
+ # Add UUID
692
+ if "uuid" in label_data:
693
+ sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
694
+
695
+ return sexp
696
+
697
+ def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
698
+ """Convert hierarchical label to S-expression."""
699
+ sexp = [sexpdata.Symbol("hierarchical_label"), hlabel_data["text"]]
700
+
701
+ # Add shape
702
+ shape = hlabel_data.get("shape", "input")
703
+ sexp.append([sexpdata.Symbol("shape"), sexpdata.Symbol(shape)])
704
+
705
+ # Add position
706
+ pos = hlabel_data["position"]
707
+ x, y = pos["x"], pos["y"]
708
+ rotation = hlabel_data.get("rotation", 0)
709
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
710
+
711
+ # Add effects (font properties)
712
+ size = hlabel_data.get("size", 1.27)
713
+ effects = [sexpdata.Symbol("effects")]
714
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
715
+ effects.append(font)
716
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol("left")])
717
+ sexp.append(effects)
718
+
719
+ # Add UUID
720
+ if "uuid" in hlabel_data:
721
+ sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
722
+
723
+ return sexp
724
+
725
+ def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
726
+ """Convert hierarchical sheet to S-expression."""
727
+ sexp = [sexpdata.Symbol("sheet")]
728
+
729
+ # Add position
730
+ pos = sheet_data["position"]
731
+ x, y = pos["x"], pos["y"]
732
+ if isinstance(x, float) and x.is_integer():
733
+ x = int(x)
734
+ if isinstance(y, float) and y.is_integer():
735
+ y = int(y)
736
+ sexp.append([sexpdata.Symbol("at"), x, y])
737
+
738
+ # Add size
739
+ size = sheet_data["size"]
740
+ w, h = size["width"], size["height"]
741
+ sexp.append([sexpdata.Symbol("size"), w, h])
742
+
743
+ # Add basic properties
744
+ sexp.append([sexpdata.Symbol("exclude_from_sim"),
745
+ sexpdata.Symbol("yes" if sheet_data.get("exclude_from_sim", False) else "no")])
746
+ sexp.append([sexpdata.Symbol("in_bom"),
747
+ sexpdata.Symbol("yes" if sheet_data.get("in_bom", True) else "no")])
748
+ sexp.append([sexpdata.Symbol("on_board"),
749
+ sexpdata.Symbol("yes" if sheet_data.get("on_board", True) else "no")])
750
+ sexp.append([sexpdata.Symbol("dnp"),
751
+ sexpdata.Symbol("yes" if sheet_data.get("dnp", False) else "no")])
752
+ sexp.append([sexpdata.Symbol("fields_autoplaced"),
753
+ sexpdata.Symbol("yes" if sheet_data.get("fields_autoplaced", True) else "no")])
754
+
755
+ # Add stroke
756
+ stroke_width = sheet_data.get("stroke_width", 0.1524)
757
+ stroke_type = sheet_data.get("stroke_type", "solid")
758
+ stroke_sexp = [sexpdata.Symbol("stroke")]
759
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
760
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
761
+ sexp.append(stroke_sexp)
762
+
763
+ # Add fill
764
+ fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
765
+ fill_sexp = [sexpdata.Symbol("fill")]
766
+ fill_sexp.append([sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]])
767
+ sexp.append(fill_sexp)
768
+
769
+ # Add UUID
770
+ if "uuid" in sheet_data:
771
+ sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
772
+
773
+ # Add sheet properties (name and filename)
774
+ name = sheet_data.get("name", "Sheet")
775
+ filename = sheet_data.get("filename", "sheet.kicad_sch")
776
+
777
+ # Sheetname property
778
+ name_prop = [sexpdata.Symbol("property"), "Sheetname", name]
779
+ name_prop.append([sexpdata.Symbol("at"), x, y - 0.7116, 0]) # Above sheet
780
+ name_prop.append([sexpdata.Symbol("effects"),
781
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
782
+ [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]])
783
+ sexp.append(name_prop)
784
+
785
+ # Sheetfile property
786
+ file_prop = [sexpdata.Symbol("property"), "Sheetfile", filename]
787
+ file_prop.append([sexpdata.Symbol("at"), x, y + h + 0.5754, 0]) # Below sheet
788
+ file_prop.append([sexpdata.Symbol("effects"),
789
+ [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
790
+ [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("top")]])
791
+ sexp.append(file_prop)
792
+
793
+ # Add sheet pins if any
794
+ for pin in sheet_data.get("pins", []):
795
+ pin_sexp = self._sheet_pin_to_sexp(pin)
796
+ sexp.append(pin_sexp)
797
+
798
+ # Add instances
799
+ if schematic_uuid:
800
+ instances_sexp = [sexpdata.Symbol("instances")]
801
+ project_name = sheet_data.get("project_name", "")
802
+ page_number = sheet_data.get("page_number", "2")
803
+ project_sexp = [sexpdata.Symbol("project"), project_name]
804
+ path_sexp = [sexpdata.Symbol("path"), f"/{schematic_uuid}"]
805
+ path_sexp.append([sexpdata.Symbol("page"), page_number])
806
+ project_sexp.append(path_sexp)
807
+ instances_sexp.append(project_sexp)
808
+ sexp.append(instances_sexp)
809
+
810
+ return sexp
811
+
812
+ def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
813
+ """Convert sheet pin to S-expression."""
814
+ pin_sexp = [sexpdata.Symbol("pin"), pin_data["name"], sexpdata.Symbol(pin_data.get("pin_type", "input"))]
815
+
816
+ # Add position
817
+ pos = pin_data["position"]
818
+ x, y = pos["x"], pos["y"]
819
+ rotation = pin_data.get("rotation", 0)
820
+ pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
821
+
822
+ # Add UUID
823
+ if "uuid" in pin_data:
824
+ pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
825
+
826
+ # Add effects
827
+ size = pin_data.get("size", 1.27)
828
+ effects = [sexpdata.Symbol("effects")]
829
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
830
+ effects.append(font)
831
+ justify = pin_data.get("justify", "right")
832
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
833
+ pin_sexp.append(effects)
834
+
835
+ return pin_sexp
836
+
837
+ def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
838
+ """Convert text element to S-expression."""
839
+ sexp = [sexpdata.Symbol("text"), text_data["text"]]
840
+
841
+ # Add exclude_from_sim
842
+ exclude_sim = text_data.get("exclude_from_sim", False)
843
+ sexp.append([sexpdata.Symbol("exclude_from_sim"),
844
+ sexpdata.Symbol("yes" if exclude_sim else "no")])
845
+
846
+ # Add position
847
+ pos = text_data["position"]
848
+ x, y = pos["x"], pos["y"]
849
+ rotation = text_data.get("rotation", 0)
850
+
851
+ # Format coordinates properly
852
+ if isinstance(x, float) and x.is_integer():
853
+ x = int(x)
854
+ if isinstance(y, float) and y.is_integer():
855
+ y = int(y)
856
+
857
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
858
+
859
+ # Add effects (font properties)
860
+ size = text_data.get("size", 1.27)
861
+ effects = [sexpdata.Symbol("effects")]
862
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
863
+ effects.append(font)
864
+ sexp.append(effects)
865
+
866
+ # Add UUID
867
+ if "uuid" in text_data:
868
+ sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
869
+
870
+ return sexp
871
+
872
+ def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
873
+ """Convert text box element to S-expression."""
874
+ sexp = [sexpdata.Symbol("text_box"), text_box_data["text"]]
875
+
876
+ # Add exclude_from_sim
877
+ exclude_sim = text_box_data.get("exclude_from_sim", False)
878
+ sexp.append([sexpdata.Symbol("exclude_from_sim"),
879
+ sexpdata.Symbol("yes" if exclude_sim else "no")])
880
+
881
+ # Add position
882
+ pos = text_box_data["position"]
883
+ x, y = pos["x"], pos["y"]
884
+ rotation = text_box_data.get("rotation", 0)
885
+
886
+ # Format coordinates properly
887
+ if isinstance(x, float) and x.is_integer():
888
+ x = int(x)
889
+ if isinstance(y, float) and y.is_integer():
890
+ y = int(y)
891
+
892
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
893
+
894
+ # Add size
895
+ size = text_box_data["size"]
896
+ w, h = size["width"], size["height"]
897
+ sexp.append([sexpdata.Symbol("size"), w, h])
898
+
899
+ # Add margins
900
+ margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
901
+ sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
902
+
903
+ # Add stroke
904
+ stroke_width = text_box_data.get("stroke_width", 0)
905
+ stroke_type = text_box_data.get("stroke_type", "solid")
906
+ stroke_sexp = [sexpdata.Symbol("stroke")]
907
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
908
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
909
+ sexp.append(stroke_sexp)
910
+
911
+ # Add fill
912
+ fill_type = text_box_data.get("fill_type", "none")
913
+ fill_sexp = [sexpdata.Symbol("fill")]
914
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
915
+ sexp.append(fill_sexp)
916
+
917
+ # Add effects (font properties and justification)
918
+ font_size = text_box_data.get("font_size", 1.27)
919
+ justify_h = text_box_data.get("justify_horizontal", "left")
920
+ justify_v = text_box_data.get("justify_vertical", "top")
921
+
922
+ effects = [sexpdata.Symbol("effects")]
923
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
924
+ effects.append(font)
925
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)])
926
+ sexp.append(effects)
927
+
928
+ # Add UUID
929
+ if "uuid" in text_box_data:
930
+ sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
931
+
932
+ return sexp
426
933
 
427
934
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
428
935
  """Convert lib_symbols to S-expression."""
429
- # Implementation for lib_symbols conversion
430
- return [sexpdata.Symbol("lib_symbols")]
936
+ sexp = [sexpdata.Symbol("lib_symbols")]
937
+
938
+ # Add each symbol definition
939
+ for symbol_name, symbol_def in lib_symbols.items():
940
+ if isinstance(symbol_def, list):
941
+ # Raw S-expression data from parsed library file - use directly
942
+ sexp.append(symbol_def)
943
+ elif isinstance(symbol_def, dict):
944
+ # Dictionary format - convert to S-expression
945
+ symbol_sexp = self._create_basic_symbol_definition(symbol_name)
946
+ sexp.append(symbol_sexp)
947
+
948
+ return sexp
949
+
950
+ def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
951
+ """Create a basic symbol definition for KiCAD compatibility."""
952
+ symbol_sexp = [sexpdata.Symbol("symbol"), lib_id]
953
+
954
+ # Add basic symbol properties
955
+ symbol_sexp.extend([
956
+ [sexpdata.Symbol("pin_numbers"), [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]],
957
+ [sexpdata.Symbol("pin_names"), [sexpdata.Symbol("offset"), 0]],
958
+ [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("no")],
959
+ [sexpdata.Symbol("in_bom"), sexpdata.Symbol("yes")],
960
+ [sexpdata.Symbol("on_board"), sexpdata.Symbol("yes")]
961
+ ])
962
+
963
+ # Add basic properties for the symbol
964
+ if "R" in lib_id: # Resistor
965
+ symbol_sexp.extend([
966
+ [sexpdata.Symbol("property"), "Reference", "R",
967
+ [sexpdata.Symbol("at"), 2.032, 0, 90],
968
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
969
+ [sexpdata.Symbol("property"), "Value", "R",
970
+ [sexpdata.Symbol("at"), 0, 0, 90],
971
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
972
+ [sexpdata.Symbol("property"), "Footprint", "",
973
+ [sexpdata.Symbol("at"), -1.778, 0, 90],
974
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]],
975
+ [sexpdata.Symbol("property"), "Datasheet", "~",
976
+ [sexpdata.Symbol("at"), 0, 0, 0],
977
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]]
978
+ ])
979
+
980
+ elif "C" in lib_id: # Capacitor
981
+ symbol_sexp.extend([
982
+ [sexpdata.Symbol("property"), "Reference", "C",
983
+ [sexpdata.Symbol("at"), 0.635, 2.54, 0],
984
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
985
+ [sexpdata.Symbol("property"), "Value", "C",
986
+ [sexpdata.Symbol("at"), 0.635, -2.54, 0],
987
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]]]],
988
+ [sexpdata.Symbol("property"), "Footprint", "",
989
+ [sexpdata.Symbol("at"), 0, -1.27, 0],
990
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]],
991
+ [sexpdata.Symbol("property"), "Datasheet", "~",
992
+ [sexpdata.Symbol("at"), 0, 0, 0],
993
+ [sexpdata.Symbol("effects"), [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]], [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]]]
994
+ ])
995
+
996
+ # Add basic graphics and pins (minimal for now)
997
+ symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
998
+
999
+ return symbol_sexp
1000
+
1001
+ def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
1002
+ """Parse sheet_instances section."""
1003
+ sheet_instances = []
1004
+ for sheet_item in item[1:]: # Skip 'sheet_instances' header
1005
+ if isinstance(sheet_item, list) and len(sheet_item) > 0:
1006
+ sheet_data = {"path": "/", "page": "1"}
1007
+ for element in sheet_item[1:]: # Skip element header
1008
+ if isinstance(element, list) and len(element) >= 2:
1009
+ key = str(element[0]) if isinstance(element[0], sexpdata.Symbol) else str(element[0])
1010
+ if key == "path":
1011
+ sheet_data["path"] = element[1]
1012
+ elif key == "page":
1013
+ sheet_data["page"] = element[1]
1014
+ sheet_instances.append(sheet_data)
1015
+ return sheet_instances
1016
+
1017
+ def _parse_symbol_instances(self, item: List[Any]) -> List[Any]:
1018
+ """Parse symbol_instances section."""
1019
+ # For now, just return the raw structure minus the header
1020
+ return item[1:] if len(item) > 1 else []
1021
+
1022
+ def _sheet_instances_to_sexp(self, sheet_instances: List[Dict[str, Any]]) -> List[Any]:
1023
+ """Convert sheet_instances to S-expression."""
1024
+ sexp = [sexpdata.Symbol("sheet_instances")]
1025
+ for sheet in sheet_instances:
1026
+ # Create: (path "/" (page "1"))
1027
+ sheet_sexp = [
1028
+ sexpdata.Symbol("path"), sheet.get("path", "/"),
1029
+ [sexpdata.Symbol("page"), str(sheet.get("page", "1"))]
1030
+ ]
1031
+ sexp.append(sheet_sexp)
1032
+ return sexp
431
1033
 
432
1034
  def get_validation_issues(self) -> List[ValidationIssue]:
433
1035
  """Get list of validation issues from last parse operation."""