kicad-sch-api 0.3.1__py3-none-any.whl → 0.3.4__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.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/core/formatter.py +33 -1
- kicad_sch_api/core/parser.py +788 -12
- kicad_sch_api/core/schematic.py +49 -7
- kicad_sch_api/core/types.py +14 -0
- kicad_sch_api/geometry/__init__.py +26 -0
- kicad_sch_api/geometry/font_metrics.py +20 -0
- kicad_sch_api/geometry/symbol_bbox.py +595 -0
- {kicad_sch_api-0.3.1.dist-info → kicad_sch_api-0.3.4.dist-info}/METADATA +1 -1
- {kicad_sch_api-0.3.1.dist-info → kicad_sch_api-0.3.4.dist-info}/RECORD +14 -11
- {kicad_sch_api-0.3.1.dist-info → kicad_sch_api-0.3.4.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.1.dist-info → kicad_sch_api-0.3.4.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.1.dist-info → kicad_sch_api-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.1.dist-info → kicad_sch_api-0.3.4.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/parser.py
CHANGED
|
@@ -187,7 +187,17 @@ class SExpressionParser:
|
|
|
187
187
|
"wires": [],
|
|
188
188
|
"junctions": [],
|
|
189
189
|
"labels": [],
|
|
190
|
+
"hierarchical_labels": [],
|
|
191
|
+
"no_connects": [],
|
|
192
|
+
"texts": [],
|
|
193
|
+
"text_boxes": [],
|
|
194
|
+
"sheets": [],
|
|
195
|
+
"polylines": [],
|
|
196
|
+
"arcs": [],
|
|
197
|
+
"circles": [],
|
|
198
|
+
"beziers": [],
|
|
190
199
|
"rectangles": [],
|
|
200
|
+
"images": [],
|
|
191
201
|
"nets": [],
|
|
192
202
|
"lib_symbols": {},
|
|
193
203
|
"sheet_instances": [],
|
|
@@ -233,10 +243,50 @@ class SExpressionParser:
|
|
|
233
243
|
label = self._parse_label(item)
|
|
234
244
|
if label:
|
|
235
245
|
schematic_data["labels"].append(label)
|
|
246
|
+
elif element_type == "hierarchical_label":
|
|
247
|
+
hlabel = self._parse_hierarchical_label(item)
|
|
248
|
+
if hlabel:
|
|
249
|
+
schematic_data["hierarchical_labels"].append(hlabel)
|
|
250
|
+
elif element_type == "no_connect":
|
|
251
|
+
no_connect = self._parse_no_connect(item)
|
|
252
|
+
if no_connect:
|
|
253
|
+
schematic_data["no_connects"].append(no_connect)
|
|
254
|
+
elif element_type == "text":
|
|
255
|
+
text = self._parse_text(item)
|
|
256
|
+
if text:
|
|
257
|
+
schematic_data["texts"].append(text)
|
|
258
|
+
elif element_type == "text_box":
|
|
259
|
+
text_box = self._parse_text_box(item)
|
|
260
|
+
if text_box:
|
|
261
|
+
schematic_data["text_boxes"].append(text_box)
|
|
262
|
+
elif element_type == "sheet":
|
|
263
|
+
sheet = self._parse_sheet(item)
|
|
264
|
+
if sheet:
|
|
265
|
+
schematic_data["sheets"].append(sheet)
|
|
266
|
+
elif element_type == "polyline":
|
|
267
|
+
polyline = self._parse_polyline(item)
|
|
268
|
+
if polyline:
|
|
269
|
+
schematic_data["polylines"].append(polyline)
|
|
270
|
+
elif element_type == "arc":
|
|
271
|
+
arc = self._parse_arc(item)
|
|
272
|
+
if arc:
|
|
273
|
+
schematic_data["arcs"].append(arc)
|
|
274
|
+
elif element_type == "circle":
|
|
275
|
+
circle = self._parse_circle(item)
|
|
276
|
+
if circle:
|
|
277
|
+
schematic_data["circles"].append(circle)
|
|
278
|
+
elif element_type == "bezier":
|
|
279
|
+
bezier = self._parse_bezier(item)
|
|
280
|
+
if bezier:
|
|
281
|
+
schematic_data["beziers"].append(bezier)
|
|
236
282
|
elif element_type == "rectangle":
|
|
237
283
|
rectangle = self._parse_rectangle(item)
|
|
238
284
|
if rectangle:
|
|
239
285
|
schematic_data["rectangles"].append(rectangle)
|
|
286
|
+
elif element_type == "image":
|
|
287
|
+
image = self._parse_image(item)
|
|
288
|
+
if image:
|
|
289
|
+
schematic_data["images"].append(image)
|
|
240
290
|
elif element_type == "lib_symbols":
|
|
241
291
|
schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
|
|
242
292
|
elif element_type == "sheet_instances":
|
|
@@ -297,25 +347,48 @@ class SExpressionParser:
|
|
|
297
347
|
for hlabel in schematic_data.get("hierarchical_labels", []):
|
|
298
348
|
sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
|
|
299
349
|
|
|
300
|
-
# Add
|
|
301
|
-
for
|
|
302
|
-
sexp_data.append(self.
|
|
350
|
+
# Add no_connects
|
|
351
|
+
for no_connect in schematic_data.get("no_connects", []):
|
|
352
|
+
sexp_data.append(self._no_connect_to_sexp(no_connect))
|
|
353
|
+
|
|
354
|
+
# Add graphical elements (in KiCad element order)
|
|
355
|
+
# Beziers
|
|
356
|
+
for bezier in schematic_data.get("beziers", []):
|
|
357
|
+
sexp_data.append(self._bezier_to_sexp(bezier))
|
|
358
|
+
|
|
359
|
+
# Rectangles (both from API and graphics)
|
|
360
|
+
for rectangle in schematic_data.get("rectangles", []):
|
|
361
|
+
sexp_data.append(self._rectangle_to_sexp(rectangle))
|
|
362
|
+
for graphic in schematic_data.get("graphics", []):
|
|
363
|
+
sexp_data.append(self._graphic_to_sexp(graphic))
|
|
364
|
+
|
|
365
|
+
# Images
|
|
366
|
+
for image in schematic_data.get("images", []):
|
|
367
|
+
sexp_data.append(self._image_to_sexp(image))
|
|
368
|
+
|
|
369
|
+
# Circles
|
|
370
|
+
for circle in schematic_data.get("circles", []):
|
|
371
|
+
sexp_data.append(self._circle_to_sexp(circle))
|
|
372
|
+
|
|
373
|
+
# Arcs
|
|
374
|
+
for arc in schematic_data.get("arcs", []):
|
|
375
|
+
sexp_data.append(self._arc_to_sexp(arc))
|
|
376
|
+
|
|
377
|
+
# Polylines
|
|
378
|
+
for polyline in schematic_data.get("polylines", []):
|
|
379
|
+
sexp_data.append(self._polyline_to_sexp(polyline))
|
|
303
380
|
|
|
304
|
-
#
|
|
381
|
+
# Text elements
|
|
305
382
|
for text in schematic_data.get("texts", []):
|
|
306
383
|
sexp_data.append(self._text_to_sexp(text))
|
|
307
384
|
|
|
308
|
-
#
|
|
385
|
+
# Text boxes
|
|
309
386
|
for text_box in schematic_data.get("text_boxes", []):
|
|
310
387
|
sexp_data.append(self._text_box_to_sexp(text_box))
|
|
311
388
|
|
|
312
|
-
#
|
|
313
|
-
for
|
|
314
|
-
sexp_data.append(self.
|
|
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))
|
|
389
|
+
# Hierarchical sheets
|
|
390
|
+
for sheet in schematic_data.get("sheets", []):
|
|
391
|
+
sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
|
|
319
392
|
|
|
320
393
|
# Add sheet_instances (required by KiCAD)
|
|
321
394
|
sheet_instances = schematic_data.get("sheet_instances", [])
|
|
@@ -538,6 +611,481 @@ class SExpressionParser:
|
|
|
538
611
|
|
|
539
612
|
return label_data
|
|
540
613
|
|
|
614
|
+
def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
615
|
+
"""Parse a hierarchical label definition."""
|
|
616
|
+
# Format: (hierarchical_label "text" (shape input) (at x y rotation) (effects ...) (uuid ...))
|
|
617
|
+
if len(item) < 2:
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
hlabel_data = {
|
|
621
|
+
"text": str(item[1]), # Hierarchical label text is second element
|
|
622
|
+
"shape": "input", # input/output/bidirectional/tri_state/passive
|
|
623
|
+
"position": {"x": 0, "y": 0},
|
|
624
|
+
"rotation": 0,
|
|
625
|
+
"size": 1.27,
|
|
626
|
+
"justify": "left",
|
|
627
|
+
"uuid": None
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
for elem in item[2:]: # Skip hierarchical_label keyword and text
|
|
631
|
+
if not isinstance(elem, list):
|
|
632
|
+
continue
|
|
633
|
+
|
|
634
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
635
|
+
|
|
636
|
+
if elem_type == "shape":
|
|
637
|
+
# Parse shape: (shape input)
|
|
638
|
+
if len(elem) >= 2:
|
|
639
|
+
hlabel_data["shape"] = str(elem[1])
|
|
640
|
+
|
|
641
|
+
elif elem_type == "at":
|
|
642
|
+
# Parse position: (at x y rotation)
|
|
643
|
+
if len(elem) >= 3:
|
|
644
|
+
hlabel_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
645
|
+
if len(elem) >= 4:
|
|
646
|
+
hlabel_data["rotation"] = float(elem[3])
|
|
647
|
+
|
|
648
|
+
elif elem_type == "effects":
|
|
649
|
+
# Parse effects for font size and justification: (effects (font (size x y)) (justify left))
|
|
650
|
+
for effect_elem in elem[1:]:
|
|
651
|
+
if isinstance(effect_elem, list):
|
|
652
|
+
effect_type = str(effect_elem[0]) if isinstance(effect_elem[0], sexpdata.Symbol) else None
|
|
653
|
+
|
|
654
|
+
if effect_type == "font":
|
|
655
|
+
# Parse font size
|
|
656
|
+
for font_elem in effect_elem[1:]:
|
|
657
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
658
|
+
if len(font_elem) >= 2:
|
|
659
|
+
hlabel_data["size"] = float(font_elem[1])
|
|
660
|
+
|
|
661
|
+
elif effect_type == "justify":
|
|
662
|
+
# Parse justification (e.g., "left", "right")
|
|
663
|
+
if len(effect_elem) >= 2:
|
|
664
|
+
hlabel_data["justify"] = str(effect_elem[1])
|
|
665
|
+
|
|
666
|
+
elif elem_type == "uuid":
|
|
667
|
+
hlabel_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
668
|
+
|
|
669
|
+
return hlabel_data
|
|
670
|
+
|
|
671
|
+
def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
672
|
+
"""Parse a no_connect symbol."""
|
|
673
|
+
# Format: (no_connect (at x y) (uuid ...))
|
|
674
|
+
no_connect_data = {
|
|
675
|
+
"position": {"x": 0, "y": 0},
|
|
676
|
+
"uuid": None
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for elem in item[1:]:
|
|
680
|
+
if not isinstance(elem, list):
|
|
681
|
+
continue
|
|
682
|
+
|
|
683
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
684
|
+
|
|
685
|
+
if elem_type == "at":
|
|
686
|
+
if len(elem) >= 3:
|
|
687
|
+
no_connect_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
688
|
+
elif elem_type == "uuid":
|
|
689
|
+
no_connect_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
690
|
+
|
|
691
|
+
return no_connect_data
|
|
692
|
+
|
|
693
|
+
def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
694
|
+
"""Parse a text element."""
|
|
695
|
+
# Format: (text "text" (exclude_from_sim no) (at x y rotation) (effects ...) (uuid ...))
|
|
696
|
+
if len(item) < 2:
|
|
697
|
+
return None
|
|
698
|
+
|
|
699
|
+
text_data = {
|
|
700
|
+
"text": str(item[1]),
|
|
701
|
+
"exclude_from_sim": False,
|
|
702
|
+
"position": {"x": 0, "y": 0},
|
|
703
|
+
"rotation": 0,
|
|
704
|
+
"size": 1.27,
|
|
705
|
+
"uuid": None
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
for elem in item[2:]:
|
|
709
|
+
if not isinstance(elem, list):
|
|
710
|
+
continue
|
|
711
|
+
|
|
712
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
713
|
+
|
|
714
|
+
if elem_type == "exclude_from_sim":
|
|
715
|
+
if len(elem) >= 2:
|
|
716
|
+
text_data["exclude_from_sim"] = str(elem[1]) == "yes"
|
|
717
|
+
elif elem_type == "at":
|
|
718
|
+
if len(elem) >= 3:
|
|
719
|
+
text_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
720
|
+
if len(elem) >= 4:
|
|
721
|
+
text_data["rotation"] = float(elem[3])
|
|
722
|
+
elif elem_type == "effects":
|
|
723
|
+
for effect_elem in elem[1:]:
|
|
724
|
+
if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
|
|
725
|
+
for font_elem in effect_elem[1:]:
|
|
726
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
727
|
+
if len(font_elem) >= 2:
|
|
728
|
+
text_data["size"] = float(font_elem[1])
|
|
729
|
+
elif elem_type == "uuid":
|
|
730
|
+
text_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
731
|
+
|
|
732
|
+
return text_data
|
|
733
|
+
|
|
734
|
+
def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
735
|
+
"""Parse a text_box element."""
|
|
736
|
+
# Format: (text_box "text" (exclude_from_sim no) (at x y rotation) (size w h) (margins ...) (stroke ...) (fill ...) (effects ...) (uuid ...))
|
|
737
|
+
if len(item) < 2:
|
|
738
|
+
return None
|
|
739
|
+
|
|
740
|
+
text_box_data = {
|
|
741
|
+
"text": str(item[1]),
|
|
742
|
+
"exclude_from_sim": False,
|
|
743
|
+
"position": {"x": 0, "y": 0},
|
|
744
|
+
"rotation": 0,
|
|
745
|
+
"size": {"width": 0, "height": 0},
|
|
746
|
+
"margins": (0.9525, 0.9525, 0.9525, 0.9525),
|
|
747
|
+
"stroke_width": 0,
|
|
748
|
+
"stroke_type": "solid",
|
|
749
|
+
"fill_type": "none",
|
|
750
|
+
"font_size": 1.27,
|
|
751
|
+
"justify_horizontal": "left",
|
|
752
|
+
"justify_vertical": "top",
|
|
753
|
+
"uuid": None
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
for elem in item[2:]:
|
|
757
|
+
if not isinstance(elem, list):
|
|
758
|
+
continue
|
|
759
|
+
|
|
760
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
761
|
+
|
|
762
|
+
if elem_type == "exclude_from_sim":
|
|
763
|
+
if len(elem) >= 2:
|
|
764
|
+
text_box_data["exclude_from_sim"] = str(elem[1]) == "yes"
|
|
765
|
+
elif elem_type == "at":
|
|
766
|
+
if len(elem) >= 3:
|
|
767
|
+
text_box_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
768
|
+
if len(elem) >= 4:
|
|
769
|
+
text_box_data["rotation"] = float(elem[3])
|
|
770
|
+
elif elem_type == "size":
|
|
771
|
+
if len(elem) >= 3:
|
|
772
|
+
text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
|
|
773
|
+
elif elem_type == "margins":
|
|
774
|
+
if len(elem) >= 5:
|
|
775
|
+
text_box_data["margins"] = (float(elem[1]), float(elem[2]), float(elem[3]), float(elem[4]))
|
|
776
|
+
elif elem_type == "stroke":
|
|
777
|
+
for stroke_elem in elem[1:]:
|
|
778
|
+
if isinstance(stroke_elem, list):
|
|
779
|
+
stroke_type = str(stroke_elem[0])
|
|
780
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
781
|
+
text_box_data["stroke_width"] = float(stroke_elem[1])
|
|
782
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
783
|
+
text_box_data["stroke_type"] = str(stroke_elem[1])
|
|
784
|
+
elif elem_type == "fill":
|
|
785
|
+
for fill_elem in elem[1:]:
|
|
786
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
787
|
+
text_box_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
788
|
+
elif elem_type == "effects":
|
|
789
|
+
for effect_elem in elem[1:]:
|
|
790
|
+
if isinstance(effect_elem, list):
|
|
791
|
+
effect_type = str(effect_elem[0])
|
|
792
|
+
if effect_type == "font":
|
|
793
|
+
for font_elem in effect_elem[1:]:
|
|
794
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
795
|
+
if len(font_elem) >= 2:
|
|
796
|
+
text_box_data["font_size"] = float(font_elem[1])
|
|
797
|
+
elif effect_type == "justify":
|
|
798
|
+
if len(effect_elem) >= 2:
|
|
799
|
+
text_box_data["justify_horizontal"] = str(effect_elem[1])
|
|
800
|
+
if len(effect_elem) >= 3:
|
|
801
|
+
text_box_data["justify_vertical"] = str(effect_elem[2])
|
|
802
|
+
elif elem_type == "uuid":
|
|
803
|
+
text_box_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
804
|
+
|
|
805
|
+
return text_box_data
|
|
806
|
+
|
|
807
|
+
def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
808
|
+
"""Parse a hierarchical sheet."""
|
|
809
|
+
# Complex format with position, size, properties, pins, instances
|
|
810
|
+
sheet_data = {
|
|
811
|
+
"position": {"x": 0, "y": 0},
|
|
812
|
+
"size": {"width": 0, "height": 0},
|
|
813
|
+
"exclude_from_sim": False,
|
|
814
|
+
"in_bom": True,
|
|
815
|
+
"on_board": True,
|
|
816
|
+
"dnp": False,
|
|
817
|
+
"fields_autoplaced": True,
|
|
818
|
+
"stroke_width": 0.1524,
|
|
819
|
+
"stroke_type": "solid",
|
|
820
|
+
"fill_color": (0, 0, 0, 0.0),
|
|
821
|
+
"uuid": None,
|
|
822
|
+
"name": "Sheet",
|
|
823
|
+
"filename": "sheet.kicad_sch",
|
|
824
|
+
"pins": [],
|
|
825
|
+
"project_name": "",
|
|
826
|
+
"page_number": "2"
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
for elem in item[1:]:
|
|
830
|
+
if not isinstance(elem, list):
|
|
831
|
+
continue
|
|
832
|
+
|
|
833
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
834
|
+
|
|
835
|
+
if elem_type == "at":
|
|
836
|
+
if len(elem) >= 3:
|
|
837
|
+
sheet_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
838
|
+
elif elem_type == "size":
|
|
839
|
+
if len(elem) >= 3:
|
|
840
|
+
sheet_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
|
|
841
|
+
elif elem_type == "exclude_from_sim":
|
|
842
|
+
sheet_data["exclude_from_sim"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
843
|
+
elif elem_type == "in_bom":
|
|
844
|
+
sheet_data["in_bom"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
845
|
+
elif elem_type == "on_board":
|
|
846
|
+
sheet_data["on_board"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
847
|
+
elif elem_type == "dnp":
|
|
848
|
+
sheet_data["dnp"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
849
|
+
elif elem_type == "fields_autoplaced":
|
|
850
|
+
sheet_data["fields_autoplaced"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
851
|
+
elif elem_type == "stroke":
|
|
852
|
+
for stroke_elem in elem[1:]:
|
|
853
|
+
if isinstance(stroke_elem, list):
|
|
854
|
+
stroke_type = str(stroke_elem[0])
|
|
855
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
856
|
+
sheet_data["stroke_width"] = float(stroke_elem[1])
|
|
857
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
858
|
+
sheet_data["stroke_type"] = str(stroke_elem[1])
|
|
859
|
+
elif elem_type == "fill":
|
|
860
|
+
for fill_elem in elem[1:]:
|
|
861
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
|
|
862
|
+
if len(fill_elem) >= 5:
|
|
863
|
+
sheet_data["fill_color"] = (int(fill_elem[1]), int(fill_elem[2]), int(fill_elem[3]), float(fill_elem[4]))
|
|
864
|
+
elif elem_type == "uuid":
|
|
865
|
+
sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
866
|
+
elif elem_type == "property":
|
|
867
|
+
if len(elem) >= 3:
|
|
868
|
+
prop_name = str(elem[1])
|
|
869
|
+
prop_value = str(elem[2])
|
|
870
|
+
if prop_name == "Sheetname":
|
|
871
|
+
sheet_data["name"] = prop_value
|
|
872
|
+
elif prop_name == "Sheetfile":
|
|
873
|
+
sheet_data["filename"] = prop_value
|
|
874
|
+
elif elem_type == "pin":
|
|
875
|
+
# Parse sheet pin - reuse existing _parse_sheet_pin helper
|
|
876
|
+
pin_data = self._parse_sheet_pin_for_read(elem)
|
|
877
|
+
if pin_data:
|
|
878
|
+
sheet_data["pins"].append(pin_data)
|
|
879
|
+
elif elem_type == "instances":
|
|
880
|
+
# Parse instances for project name and page number
|
|
881
|
+
for inst_elem in elem[1:]:
|
|
882
|
+
if isinstance(inst_elem, list) and str(inst_elem[0]) == "project":
|
|
883
|
+
if len(inst_elem) >= 2:
|
|
884
|
+
sheet_data["project_name"] = str(inst_elem[1])
|
|
885
|
+
for path_elem in inst_elem[2:]:
|
|
886
|
+
if isinstance(path_elem, list) and str(path_elem[0]) == "path":
|
|
887
|
+
for page_elem in path_elem[1:]:
|
|
888
|
+
if isinstance(page_elem, list) and str(page_elem[0]) == "page":
|
|
889
|
+
sheet_data["page_number"] = str(page_elem[1]) if len(page_elem) > 1 else "2"
|
|
890
|
+
|
|
891
|
+
return sheet_data
|
|
892
|
+
|
|
893
|
+
def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
894
|
+
"""Parse a sheet pin (for reading during sheet parsing)."""
|
|
895
|
+
# Format: (pin "name" type (at x y rotation) (uuid ...) (effects ...))
|
|
896
|
+
if len(item) < 3:
|
|
897
|
+
return None
|
|
898
|
+
|
|
899
|
+
pin_data = {
|
|
900
|
+
"name": str(item[1]),
|
|
901
|
+
"pin_type": str(item[2]) if len(item) > 2 else "input",
|
|
902
|
+
"position": {"x": 0, "y": 0},
|
|
903
|
+
"rotation": 0,
|
|
904
|
+
"size": 1.27,
|
|
905
|
+
"justify": "right",
|
|
906
|
+
"uuid": None
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
for elem in item[3:]:
|
|
910
|
+
if not isinstance(elem, list):
|
|
911
|
+
continue
|
|
912
|
+
|
|
913
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
914
|
+
|
|
915
|
+
if elem_type == "at":
|
|
916
|
+
if len(elem) >= 3:
|
|
917
|
+
pin_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
918
|
+
if len(elem) >= 4:
|
|
919
|
+
pin_data["rotation"] = float(elem[3])
|
|
920
|
+
elif elem_type == "uuid":
|
|
921
|
+
pin_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
922
|
+
elif elem_type == "effects":
|
|
923
|
+
for effect_elem in elem[1:]:
|
|
924
|
+
if isinstance(effect_elem, list):
|
|
925
|
+
effect_type = str(effect_elem[0])
|
|
926
|
+
if effect_type == "font":
|
|
927
|
+
for font_elem in effect_elem[1:]:
|
|
928
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
929
|
+
if len(font_elem) >= 2:
|
|
930
|
+
pin_data["size"] = float(font_elem[1])
|
|
931
|
+
elif effect_type == "justify":
|
|
932
|
+
if len(effect_elem) >= 2:
|
|
933
|
+
pin_data["justify"] = str(effect_elem[1])
|
|
934
|
+
|
|
935
|
+
return pin_data
|
|
936
|
+
|
|
937
|
+
def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
938
|
+
"""Parse a polyline graphical element."""
|
|
939
|
+
# Format: (polyline (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (uuid ...))
|
|
940
|
+
polyline_data = {
|
|
941
|
+
"points": [],
|
|
942
|
+
"stroke_width": 0,
|
|
943
|
+
"stroke_type": "default",
|
|
944
|
+
"uuid": None
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
for elem in item[1:]:
|
|
948
|
+
if not isinstance(elem, list):
|
|
949
|
+
continue
|
|
950
|
+
|
|
951
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
952
|
+
|
|
953
|
+
if elem_type == "pts":
|
|
954
|
+
for pt in elem[1:]:
|
|
955
|
+
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
956
|
+
polyline_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
957
|
+
elif elem_type == "stroke":
|
|
958
|
+
for stroke_elem in elem[1:]:
|
|
959
|
+
if isinstance(stroke_elem, list):
|
|
960
|
+
stroke_type = str(stroke_elem[0])
|
|
961
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
962
|
+
polyline_data["stroke_width"] = float(stroke_elem[1])
|
|
963
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
964
|
+
polyline_data["stroke_type"] = str(stroke_elem[1])
|
|
965
|
+
elif elem_type == "uuid":
|
|
966
|
+
polyline_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
967
|
+
|
|
968
|
+
return polyline_data if polyline_data["points"] else None
|
|
969
|
+
|
|
970
|
+
def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
971
|
+
"""Parse an arc graphical element."""
|
|
972
|
+
# Format: (arc (start x y) (mid x y) (end x y) (stroke ...) (fill ...) (uuid ...))
|
|
973
|
+
arc_data = {
|
|
974
|
+
"start": {"x": 0, "y": 0},
|
|
975
|
+
"mid": {"x": 0, "y": 0},
|
|
976
|
+
"end": {"x": 0, "y": 0},
|
|
977
|
+
"stroke_width": 0,
|
|
978
|
+
"stroke_type": "default",
|
|
979
|
+
"fill_type": "none",
|
|
980
|
+
"uuid": None
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
for elem in item[1:]:
|
|
984
|
+
if not isinstance(elem, list):
|
|
985
|
+
continue
|
|
986
|
+
|
|
987
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
988
|
+
|
|
989
|
+
if elem_type == "start" and len(elem) >= 3:
|
|
990
|
+
arc_data["start"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
991
|
+
elif elem_type == "mid" and len(elem) >= 3:
|
|
992
|
+
arc_data["mid"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
993
|
+
elif elem_type == "end" and len(elem) >= 3:
|
|
994
|
+
arc_data["end"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
995
|
+
elif elem_type == "stroke":
|
|
996
|
+
for stroke_elem in elem[1:]:
|
|
997
|
+
if isinstance(stroke_elem, list):
|
|
998
|
+
stroke_type = str(stroke_elem[0])
|
|
999
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1000
|
+
arc_data["stroke_width"] = float(stroke_elem[1])
|
|
1001
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1002
|
+
arc_data["stroke_type"] = str(stroke_elem[1])
|
|
1003
|
+
elif elem_type == "fill":
|
|
1004
|
+
for fill_elem in elem[1:]:
|
|
1005
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1006
|
+
arc_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1007
|
+
elif elem_type == "uuid":
|
|
1008
|
+
arc_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1009
|
+
|
|
1010
|
+
return arc_data
|
|
1011
|
+
|
|
1012
|
+
def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1013
|
+
"""Parse a circle graphical element."""
|
|
1014
|
+
# Format: (circle (center x y) (radius r) (stroke ...) (fill ...) (uuid ...))
|
|
1015
|
+
circle_data = {
|
|
1016
|
+
"center": {"x": 0, "y": 0},
|
|
1017
|
+
"radius": 0,
|
|
1018
|
+
"stroke_width": 0,
|
|
1019
|
+
"stroke_type": "default",
|
|
1020
|
+
"fill_type": "none",
|
|
1021
|
+
"uuid": None
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for elem in item[1:]:
|
|
1025
|
+
if not isinstance(elem, list):
|
|
1026
|
+
continue
|
|
1027
|
+
|
|
1028
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1029
|
+
|
|
1030
|
+
if elem_type == "center" and len(elem) >= 3:
|
|
1031
|
+
circle_data["center"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1032
|
+
elif elem_type == "radius" and len(elem) >= 2:
|
|
1033
|
+
circle_data["radius"] = float(elem[1])
|
|
1034
|
+
elif elem_type == "stroke":
|
|
1035
|
+
for stroke_elem in elem[1:]:
|
|
1036
|
+
if isinstance(stroke_elem, list):
|
|
1037
|
+
stroke_type = str(stroke_elem[0])
|
|
1038
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1039
|
+
circle_data["stroke_width"] = float(stroke_elem[1])
|
|
1040
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1041
|
+
circle_data["stroke_type"] = str(stroke_elem[1])
|
|
1042
|
+
elif elem_type == "fill":
|
|
1043
|
+
for fill_elem in elem[1:]:
|
|
1044
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1045
|
+
circle_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1046
|
+
elif elem_type == "uuid":
|
|
1047
|
+
circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1048
|
+
|
|
1049
|
+
return circle_data
|
|
1050
|
+
|
|
1051
|
+
def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1052
|
+
"""Parse a bezier curve graphical element."""
|
|
1053
|
+
# Format: (bezier (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (fill ...) (uuid ...))
|
|
1054
|
+
bezier_data = {
|
|
1055
|
+
"points": [],
|
|
1056
|
+
"stroke_width": 0,
|
|
1057
|
+
"stroke_type": "default",
|
|
1058
|
+
"fill_type": "none",
|
|
1059
|
+
"uuid": None
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
for elem in item[1:]:
|
|
1063
|
+
if not isinstance(elem, list):
|
|
1064
|
+
continue
|
|
1065
|
+
|
|
1066
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1067
|
+
|
|
1068
|
+
if elem_type == "pts":
|
|
1069
|
+
for pt in elem[1:]:
|
|
1070
|
+
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
1071
|
+
bezier_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
1072
|
+
elif elem_type == "stroke":
|
|
1073
|
+
for stroke_elem in elem[1:]:
|
|
1074
|
+
if isinstance(stroke_elem, list):
|
|
1075
|
+
stroke_type = str(stroke_elem[0])
|
|
1076
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1077
|
+
bezier_data["stroke_width"] = float(stroke_elem[1])
|
|
1078
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1079
|
+
bezier_data["stroke_type"] = str(stroke_elem[1])
|
|
1080
|
+
elif elem_type == "fill":
|
|
1081
|
+
for fill_elem in elem[1:]:
|
|
1082
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1083
|
+
bezier_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1084
|
+
elif elem_type == "uuid":
|
|
1085
|
+
bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1086
|
+
|
|
1087
|
+
return bezier_data if bezier_data["points"] else None
|
|
1088
|
+
|
|
541
1089
|
def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
542
1090
|
"""Parse a rectangle graphical element."""
|
|
543
1091
|
rectangle = {}
|
|
@@ -569,6 +1117,37 @@ class SExpressionParser:
|
|
|
569
1117
|
|
|
570
1118
|
return rectangle if rectangle else None
|
|
571
1119
|
|
|
1120
|
+
def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1121
|
+
"""Parse an image element."""
|
|
1122
|
+
# Format: (image (at x y) (uuid "...") (data "base64..."))
|
|
1123
|
+
image = {
|
|
1124
|
+
"position": {"x": 0, "y": 0},
|
|
1125
|
+
"data": "",
|
|
1126
|
+
"scale": 1.0,
|
|
1127
|
+
"uuid": None
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
for elem in item[1:]:
|
|
1131
|
+
if not isinstance(elem, list):
|
|
1132
|
+
continue
|
|
1133
|
+
|
|
1134
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1135
|
+
|
|
1136
|
+
if elem_type == "at" and len(elem) >= 3:
|
|
1137
|
+
image["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1138
|
+
elif elem_type == "scale" and len(elem) >= 2:
|
|
1139
|
+
image["scale"] = float(elem[1])
|
|
1140
|
+
elif elem_type == "data" and len(elem) >= 2:
|
|
1141
|
+
# The data can be spread across multiple string elements
|
|
1142
|
+
data_parts = []
|
|
1143
|
+
for data_elem in elem[1:]:
|
|
1144
|
+
data_parts.append(str(data_elem).strip('"'))
|
|
1145
|
+
image["data"] = "".join(data_parts)
|
|
1146
|
+
elif elem_type == "uuid" and len(elem) >= 2:
|
|
1147
|
+
image["uuid"] = str(elem[1]).strip('"')
|
|
1148
|
+
|
|
1149
|
+
return image if image.get("uuid") and image.get("data") else None
|
|
1150
|
+
|
|
572
1151
|
def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
|
|
573
1152
|
"""Parse lib_symbols section."""
|
|
574
1153
|
# Implementation for lib_symbols parsing
|
|
@@ -942,6 +1521,171 @@ class SExpressionParser:
|
|
|
942
1521
|
|
|
943
1522
|
return sexp
|
|
944
1523
|
|
|
1524
|
+
def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
|
|
1525
|
+
"""Convert no_connect to S-expression."""
|
|
1526
|
+
sexp = [sexpdata.Symbol("no_connect")]
|
|
1527
|
+
|
|
1528
|
+
# Add position
|
|
1529
|
+
pos = no_connect_data["position"]
|
|
1530
|
+
x, y = pos["x"], pos["y"]
|
|
1531
|
+
|
|
1532
|
+
# Format coordinates properly
|
|
1533
|
+
if isinstance(x, float) and x.is_integer():
|
|
1534
|
+
x = int(x)
|
|
1535
|
+
if isinstance(y, float) and y.is_integer():
|
|
1536
|
+
y = int(y)
|
|
1537
|
+
|
|
1538
|
+
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
1539
|
+
|
|
1540
|
+
# Add UUID
|
|
1541
|
+
if "uuid" in no_connect_data:
|
|
1542
|
+
sexp.append([sexpdata.Symbol("uuid"), no_connect_data["uuid"]])
|
|
1543
|
+
|
|
1544
|
+
return sexp
|
|
1545
|
+
|
|
1546
|
+
def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
|
|
1547
|
+
"""Convert polyline to S-expression."""
|
|
1548
|
+
sexp = [sexpdata.Symbol("polyline")]
|
|
1549
|
+
|
|
1550
|
+
# Add points
|
|
1551
|
+
points = polyline_data.get("points", [])
|
|
1552
|
+
if points:
|
|
1553
|
+
pts_sexp = [sexpdata.Symbol("pts")]
|
|
1554
|
+
for point in points:
|
|
1555
|
+
x, y = point["x"], point["y"]
|
|
1556
|
+
# Format coordinates properly
|
|
1557
|
+
if isinstance(x, float) and x.is_integer():
|
|
1558
|
+
x = int(x)
|
|
1559
|
+
if isinstance(y, float) and y.is_integer():
|
|
1560
|
+
y = int(y)
|
|
1561
|
+
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
1562
|
+
sexp.append(pts_sexp)
|
|
1563
|
+
|
|
1564
|
+
# Add stroke
|
|
1565
|
+
stroke_width = polyline_data.get("stroke_width", 0)
|
|
1566
|
+
stroke_type = polyline_data.get("stroke_type", "default")
|
|
1567
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1568
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1569
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1570
|
+
sexp.append(stroke_sexp)
|
|
1571
|
+
|
|
1572
|
+
# Add UUID
|
|
1573
|
+
if "uuid" in polyline_data:
|
|
1574
|
+
sexp.append([sexpdata.Symbol("uuid"), polyline_data["uuid"]])
|
|
1575
|
+
|
|
1576
|
+
return sexp
|
|
1577
|
+
|
|
1578
|
+
def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
|
|
1579
|
+
"""Convert arc to S-expression."""
|
|
1580
|
+
sexp = [sexpdata.Symbol("arc")]
|
|
1581
|
+
|
|
1582
|
+
# Add start, mid, end points
|
|
1583
|
+
for point_name in ["start", "mid", "end"]:
|
|
1584
|
+
point = arc_data.get(point_name, {"x": 0, "y": 0})
|
|
1585
|
+
x, y = point["x"], point["y"]
|
|
1586
|
+
# Format coordinates properly
|
|
1587
|
+
if isinstance(x, float) and x.is_integer():
|
|
1588
|
+
x = int(x)
|
|
1589
|
+
if isinstance(y, float) and y.is_integer():
|
|
1590
|
+
y = int(y)
|
|
1591
|
+
sexp.append([sexpdata.Symbol(point_name), x, y])
|
|
1592
|
+
|
|
1593
|
+
# Add stroke
|
|
1594
|
+
stroke_width = arc_data.get("stroke_width", 0)
|
|
1595
|
+
stroke_type = arc_data.get("stroke_type", "default")
|
|
1596
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1597
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1598
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1599
|
+
sexp.append(stroke_sexp)
|
|
1600
|
+
|
|
1601
|
+
# Add fill
|
|
1602
|
+
fill_type = arc_data.get("fill_type", "none")
|
|
1603
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1604
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1605
|
+
sexp.append(fill_sexp)
|
|
1606
|
+
|
|
1607
|
+
# Add UUID
|
|
1608
|
+
if "uuid" in arc_data:
|
|
1609
|
+
sexp.append([sexpdata.Symbol("uuid"), arc_data["uuid"]])
|
|
1610
|
+
|
|
1611
|
+
return sexp
|
|
1612
|
+
|
|
1613
|
+
def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
|
|
1614
|
+
"""Convert circle to S-expression."""
|
|
1615
|
+
sexp = [sexpdata.Symbol("circle")]
|
|
1616
|
+
|
|
1617
|
+
# Add center
|
|
1618
|
+
center = circle_data.get("center", {"x": 0, "y": 0})
|
|
1619
|
+
x, y = center["x"], center["y"]
|
|
1620
|
+
# Format coordinates properly
|
|
1621
|
+
if isinstance(x, float) and x.is_integer():
|
|
1622
|
+
x = int(x)
|
|
1623
|
+
if isinstance(y, float) and y.is_integer():
|
|
1624
|
+
y = int(y)
|
|
1625
|
+
sexp.append([sexpdata.Symbol("center"), x, y])
|
|
1626
|
+
|
|
1627
|
+
# Add radius
|
|
1628
|
+
radius = circle_data.get("radius", 0)
|
|
1629
|
+
sexp.append([sexpdata.Symbol("radius"), radius])
|
|
1630
|
+
|
|
1631
|
+
# Add stroke
|
|
1632
|
+
stroke_width = circle_data.get("stroke_width", 0)
|
|
1633
|
+
stroke_type = circle_data.get("stroke_type", "default")
|
|
1634
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1635
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1636
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1637
|
+
sexp.append(stroke_sexp)
|
|
1638
|
+
|
|
1639
|
+
# Add fill
|
|
1640
|
+
fill_type = circle_data.get("fill_type", "none")
|
|
1641
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1642
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1643
|
+
sexp.append(fill_sexp)
|
|
1644
|
+
|
|
1645
|
+
# Add UUID
|
|
1646
|
+
if "uuid" in circle_data:
|
|
1647
|
+
sexp.append([sexpdata.Symbol("uuid"), circle_data["uuid"]])
|
|
1648
|
+
|
|
1649
|
+
return sexp
|
|
1650
|
+
|
|
1651
|
+
def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
|
|
1652
|
+
"""Convert bezier curve to S-expression."""
|
|
1653
|
+
sexp = [sexpdata.Symbol("bezier")]
|
|
1654
|
+
|
|
1655
|
+
# Add points
|
|
1656
|
+
points = bezier_data.get("points", [])
|
|
1657
|
+
if points:
|
|
1658
|
+
pts_sexp = [sexpdata.Symbol("pts")]
|
|
1659
|
+
for point in points:
|
|
1660
|
+
x, y = point["x"], point["y"]
|
|
1661
|
+
# Format coordinates properly
|
|
1662
|
+
if isinstance(x, float) and x.is_integer():
|
|
1663
|
+
x = int(x)
|
|
1664
|
+
if isinstance(y, float) and y.is_integer():
|
|
1665
|
+
y = int(y)
|
|
1666
|
+
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
1667
|
+
sexp.append(pts_sexp)
|
|
1668
|
+
|
|
1669
|
+
# Add stroke
|
|
1670
|
+
stroke_width = bezier_data.get("stroke_width", 0)
|
|
1671
|
+
stroke_type = bezier_data.get("stroke_type", "default")
|
|
1672
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1673
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1674
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1675
|
+
sexp.append(stroke_sexp)
|
|
1676
|
+
|
|
1677
|
+
# Add fill
|
|
1678
|
+
fill_type = bezier_data.get("fill_type", "none")
|
|
1679
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1680
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1681
|
+
sexp.append(fill_sexp)
|
|
1682
|
+
|
|
1683
|
+
# Add UUID
|
|
1684
|
+
if "uuid" in bezier_data:
|
|
1685
|
+
sexp.append([sexpdata.Symbol("uuid"), bezier_data["uuid"]])
|
|
1686
|
+
|
|
1687
|
+
return sexp
|
|
1688
|
+
|
|
945
1689
|
def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
|
|
946
1690
|
"""Convert hierarchical sheet to S-expression."""
|
|
947
1691
|
sexp = [sexpdata.Symbol("sheet")]
|
|
@@ -1229,6 +1973,38 @@ class SExpressionParser:
|
|
|
1229
1973
|
|
|
1230
1974
|
return sexp
|
|
1231
1975
|
|
|
1976
|
+
def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
|
|
1977
|
+
"""Convert image element to S-expression."""
|
|
1978
|
+
sexp = [sexpdata.Symbol("image")]
|
|
1979
|
+
|
|
1980
|
+
# Add position
|
|
1981
|
+
position = image_data.get("position", {"x": 0, "y": 0})
|
|
1982
|
+
pos_x, pos_y = position["x"], position["y"]
|
|
1983
|
+
sexp.append([sexpdata.Symbol("at"), pos_x, pos_y])
|
|
1984
|
+
|
|
1985
|
+
# Add UUID
|
|
1986
|
+
if "uuid" in image_data:
|
|
1987
|
+
sexp.append([sexpdata.Symbol("uuid"), image_data["uuid"]])
|
|
1988
|
+
|
|
1989
|
+
# Add scale if not default
|
|
1990
|
+
scale = image_data.get("scale", 1.0)
|
|
1991
|
+
if scale != 1.0:
|
|
1992
|
+
sexp.append([sexpdata.Symbol("scale"), scale])
|
|
1993
|
+
|
|
1994
|
+
# Add image data
|
|
1995
|
+
# KiCad splits base64 data into multiple lines for readability
|
|
1996
|
+
# Each line is roughly 76 characters (standard base64 line length)
|
|
1997
|
+
data = image_data.get("data", "")
|
|
1998
|
+
if data:
|
|
1999
|
+
data_sexp = [sexpdata.Symbol("data")]
|
|
2000
|
+
# Split the data into 76-character chunks
|
|
2001
|
+
chunk_size = 76
|
|
2002
|
+
for i in range(0, len(data), chunk_size):
|
|
2003
|
+
data_sexp.append(data[i:i+chunk_size])
|
|
2004
|
+
sexp.append(data_sexp)
|
|
2005
|
+
|
|
2006
|
+
return sexp
|
|
2007
|
+
|
|
1232
2008
|
def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
|
|
1233
2009
|
"""Convert lib_symbols to S-expression."""
|
|
1234
2010
|
sexp = [sexpdata.Symbol("lib_symbols")]
|