kicad-sch-api 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/core/formatter.py +3 -2
- kicad_sch_api/core/parser.py +827 -19
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.3.2.dist-info}/METADATA +1 -1
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.3.2.dist-info}/RECORD +9 -9
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.3.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.3.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.3.2.dist-info}/top_level.txt +0 -0
kicad_sch_api/__init__.py
CHANGED
|
@@ -42,7 +42,7 @@ Advanced Usage:
|
|
|
42
42
|
print(f"Found {len(issues)} validation issues")
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
-
__version__ = "0.3.
|
|
45
|
+
__version__ = "0.3.2"
|
|
46
46
|
__author__ = "Circuit-Synth"
|
|
47
47
|
__email__ = "info@circuit-synth.com"
|
|
48
48
|
|
|
@@ -55,7 +55,7 @@ from .library.cache import SymbolLibraryCache, get_symbol_cache
|
|
|
55
55
|
from .utils.validation import ValidationError, ValidationIssue
|
|
56
56
|
|
|
57
57
|
# Version info
|
|
58
|
-
VERSION_INFO = (0, 3,
|
|
58
|
+
VERSION_INFO = (0, 3, 2)
|
|
59
59
|
|
|
60
60
|
# Public API
|
|
61
61
|
__all__ = [
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -55,7 +55,7 @@ class ExactFormatter:
|
|
|
55
55
|
self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
|
|
56
56
|
self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
|
|
57
57
|
self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
|
|
58
|
-
self.rules["paper"] = FormatRule(inline=True) #
|
|
58
|
+
self.rules["paper"] = FormatRule(inline=True, quote_indices={1}) # Paper size should be quoted per KiCad format
|
|
59
59
|
|
|
60
60
|
# Title block
|
|
61
61
|
self.rules["title_block"] = FormatRule(inline=False)
|
|
@@ -189,7 +189,8 @@ class ExactFormatter:
|
|
|
189
189
|
elif isinstance(element, str):
|
|
190
190
|
# Quote strings that need quoting
|
|
191
191
|
if self._needs_quoting(element):
|
|
192
|
-
|
|
192
|
+
escaped = self._escape_string(element)
|
|
193
|
+
return f'"{escaped}"'
|
|
193
194
|
return element
|
|
194
195
|
elif isinstance(element, float):
|
|
195
196
|
# Custom float formatting for KiCAD compatibility
|
kicad_sch_api/core/parser.py
CHANGED
|
@@ -187,6 +187,15 @@ 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": [],
|
|
191
200
|
"nets": [],
|
|
192
201
|
"lib_symbols": {},
|
|
@@ -233,6 +242,42 @@ class SExpressionParser:
|
|
|
233
242
|
label = self._parse_label(item)
|
|
234
243
|
if label:
|
|
235
244
|
schematic_data["labels"].append(label)
|
|
245
|
+
elif element_type == "hierarchical_label":
|
|
246
|
+
hlabel = self._parse_hierarchical_label(item)
|
|
247
|
+
if hlabel:
|
|
248
|
+
schematic_data["hierarchical_labels"].append(hlabel)
|
|
249
|
+
elif element_type == "no_connect":
|
|
250
|
+
no_connect = self._parse_no_connect(item)
|
|
251
|
+
if no_connect:
|
|
252
|
+
schematic_data["no_connects"].append(no_connect)
|
|
253
|
+
elif element_type == "text":
|
|
254
|
+
text = self._parse_text(item)
|
|
255
|
+
if text:
|
|
256
|
+
schematic_data["texts"].append(text)
|
|
257
|
+
elif element_type == "text_box":
|
|
258
|
+
text_box = self._parse_text_box(item)
|
|
259
|
+
if text_box:
|
|
260
|
+
schematic_data["text_boxes"].append(text_box)
|
|
261
|
+
elif element_type == "sheet":
|
|
262
|
+
sheet = self._parse_sheet(item)
|
|
263
|
+
if sheet:
|
|
264
|
+
schematic_data["sheets"].append(sheet)
|
|
265
|
+
elif element_type == "polyline":
|
|
266
|
+
polyline = self._parse_polyline(item)
|
|
267
|
+
if polyline:
|
|
268
|
+
schematic_data["polylines"].append(polyline)
|
|
269
|
+
elif element_type == "arc":
|
|
270
|
+
arc = self._parse_arc(item)
|
|
271
|
+
if arc:
|
|
272
|
+
schematic_data["arcs"].append(arc)
|
|
273
|
+
elif element_type == "circle":
|
|
274
|
+
circle = self._parse_circle(item)
|
|
275
|
+
if circle:
|
|
276
|
+
schematic_data["circles"].append(circle)
|
|
277
|
+
elif element_type == "bezier":
|
|
278
|
+
bezier = self._parse_bezier(item)
|
|
279
|
+
if bezier:
|
|
280
|
+
schematic_data["beziers"].append(bezier)
|
|
236
281
|
elif element_type == "rectangle":
|
|
237
282
|
rectangle = self._parse_rectangle(item)
|
|
238
283
|
if rectangle:
|
|
@@ -297,25 +342,44 @@ class SExpressionParser:
|
|
|
297
342
|
for hlabel in schematic_data.get("hierarchical_labels", []):
|
|
298
343
|
sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
|
|
299
344
|
|
|
300
|
-
# Add
|
|
301
|
-
for
|
|
302
|
-
sexp_data.append(self.
|
|
345
|
+
# Add no_connects
|
|
346
|
+
for no_connect in schematic_data.get("no_connects", []):
|
|
347
|
+
sexp_data.append(self._no_connect_to_sexp(no_connect))
|
|
348
|
+
|
|
349
|
+
# Add graphical elements (in KiCad element order)
|
|
350
|
+
# Beziers
|
|
351
|
+
for bezier in schematic_data.get("beziers", []):
|
|
352
|
+
sexp_data.append(self._bezier_to_sexp(bezier))
|
|
353
|
+
|
|
354
|
+
# Rectangles (both from API and graphics)
|
|
355
|
+
for rectangle in schematic_data.get("rectangles", []):
|
|
356
|
+
sexp_data.append(self._rectangle_to_sexp(rectangle))
|
|
357
|
+
for graphic in schematic_data.get("graphics", []):
|
|
358
|
+
sexp_data.append(self._graphic_to_sexp(graphic))
|
|
359
|
+
|
|
360
|
+
# Circles
|
|
361
|
+
for circle in schematic_data.get("circles", []):
|
|
362
|
+
sexp_data.append(self._circle_to_sexp(circle))
|
|
303
363
|
|
|
304
|
-
#
|
|
364
|
+
# Arcs
|
|
365
|
+
for arc in schematic_data.get("arcs", []):
|
|
366
|
+
sexp_data.append(self._arc_to_sexp(arc))
|
|
367
|
+
|
|
368
|
+
# Polylines
|
|
369
|
+
for polyline in schematic_data.get("polylines", []):
|
|
370
|
+
sexp_data.append(self._polyline_to_sexp(polyline))
|
|
371
|
+
|
|
372
|
+
# Text elements
|
|
305
373
|
for text in schematic_data.get("texts", []):
|
|
306
374
|
sexp_data.append(self._text_to_sexp(text))
|
|
307
375
|
|
|
308
|
-
#
|
|
376
|
+
# Text boxes
|
|
309
377
|
for text_box in schematic_data.get("text_boxes", []):
|
|
310
378
|
sexp_data.append(self._text_box_to_sexp(text_box))
|
|
311
379
|
|
|
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))
|
|
380
|
+
# Hierarchical sheets
|
|
381
|
+
for sheet in schematic_data.get("sheets", []):
|
|
382
|
+
sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
|
|
319
383
|
|
|
320
384
|
# Add sheet_instances (required by KiCAD)
|
|
321
385
|
sheet_instances = schematic_data.get("sheet_instances", [])
|
|
@@ -420,19 +484,598 @@ class SExpressionParser:
|
|
|
420
484
|
|
|
421
485
|
def _parse_wire(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
422
486
|
"""Parse a wire definition."""
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
487
|
+
wire_data = {
|
|
488
|
+
"points": [],
|
|
489
|
+
"stroke_width": 0.0,
|
|
490
|
+
"stroke_type": "default",
|
|
491
|
+
"uuid": None,
|
|
492
|
+
"wire_type": "wire" # Default to wire (vs bus)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for elem in item[1:]:
|
|
496
|
+
if not isinstance(elem, list):
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
500
|
+
|
|
501
|
+
if elem_type == "pts":
|
|
502
|
+
# Parse points: (pts (xy x1 y1) (xy x2 y2) ...)
|
|
503
|
+
for pt in elem[1:]:
|
|
504
|
+
if isinstance(pt, list) and len(pt) >= 3:
|
|
505
|
+
if str(pt[0]) == "xy":
|
|
506
|
+
x, y = float(pt[1]), float(pt[2])
|
|
507
|
+
wire_data["points"].append({"x": x, "y": y})
|
|
508
|
+
|
|
509
|
+
elif elem_type == "stroke":
|
|
510
|
+
# Parse stroke: (stroke (width 0) (type default))
|
|
511
|
+
for stroke_elem in elem[1:]:
|
|
512
|
+
if isinstance(stroke_elem, list) and len(stroke_elem) >= 2:
|
|
513
|
+
stroke_type = str(stroke_elem[0])
|
|
514
|
+
if stroke_type == "width":
|
|
515
|
+
wire_data["stroke_width"] = float(stroke_elem[1])
|
|
516
|
+
elif stroke_type == "type":
|
|
517
|
+
wire_data["stroke_type"] = str(stroke_elem[1])
|
|
518
|
+
|
|
519
|
+
elif elem_type == "uuid":
|
|
520
|
+
wire_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
521
|
+
|
|
522
|
+
# Only return wire if it has at least 2 points
|
|
523
|
+
if len(wire_data["points"]) >= 2:
|
|
524
|
+
return wire_data
|
|
525
|
+
else:
|
|
526
|
+
logger.warning(f"Wire has insufficient points: {len(wire_data['points'])}")
|
|
527
|
+
return None
|
|
426
528
|
|
|
427
529
|
def _parse_junction(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
428
530
|
"""Parse a junction definition."""
|
|
429
|
-
|
|
430
|
-
|
|
531
|
+
junction_data = {
|
|
532
|
+
"position": {"x": 0, "y": 0},
|
|
533
|
+
"diameter": 0,
|
|
534
|
+
"color": (0, 0, 0, 0),
|
|
535
|
+
"uuid": None
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
for elem in item[1:]:
|
|
539
|
+
if not isinstance(elem, list):
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
543
|
+
|
|
544
|
+
if elem_type == "at":
|
|
545
|
+
# Parse position: (at x y)
|
|
546
|
+
if len(elem) >= 3:
|
|
547
|
+
junction_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
548
|
+
|
|
549
|
+
elif elem_type == "diameter":
|
|
550
|
+
# Parse diameter: (diameter value)
|
|
551
|
+
if len(elem) >= 2:
|
|
552
|
+
junction_data["diameter"] = float(elem[1])
|
|
553
|
+
|
|
554
|
+
elif elem_type == "color":
|
|
555
|
+
# Parse color: (color r g b a)
|
|
556
|
+
if len(elem) >= 5:
|
|
557
|
+
junction_data["color"] = (int(elem[1]), int(elem[2]), int(elem[3]), int(elem[4]))
|
|
558
|
+
|
|
559
|
+
elif elem_type == "uuid":
|
|
560
|
+
junction_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
561
|
+
|
|
562
|
+
return junction_data
|
|
431
563
|
|
|
432
564
|
def _parse_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
433
565
|
"""Parse a label definition."""
|
|
434
|
-
#
|
|
435
|
-
|
|
566
|
+
# Label format: (label "text" (at x y rotation) (effects ...) (uuid ...))
|
|
567
|
+
if len(item) < 2:
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
label_data = {
|
|
571
|
+
"text": str(item[1]), # Label text is second element
|
|
572
|
+
"position": {"x": 0, "y": 0},
|
|
573
|
+
"rotation": 0,
|
|
574
|
+
"size": 1.27,
|
|
575
|
+
"uuid": None
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for elem in item[2:]: # Skip label keyword and text
|
|
579
|
+
if not isinstance(elem, list):
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
583
|
+
|
|
584
|
+
if elem_type == "at":
|
|
585
|
+
# Parse position: (at x y rotation)
|
|
586
|
+
if len(elem) >= 3:
|
|
587
|
+
label_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
588
|
+
if len(elem) >= 4:
|
|
589
|
+
label_data["rotation"] = float(elem[3])
|
|
590
|
+
|
|
591
|
+
elif elem_type == "effects":
|
|
592
|
+
# Parse effects for font size: (effects (font (size x y)) ...)
|
|
593
|
+
for effect_elem in elem[1:]:
|
|
594
|
+
if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
|
|
595
|
+
for font_elem in effect_elem[1:]:
|
|
596
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
597
|
+
if len(font_elem) >= 2:
|
|
598
|
+
label_data["size"] = float(font_elem[1])
|
|
599
|
+
|
|
600
|
+
elif elem_type == "uuid":
|
|
601
|
+
label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
602
|
+
|
|
603
|
+
return label_data
|
|
604
|
+
|
|
605
|
+
def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
606
|
+
"""Parse a hierarchical label definition."""
|
|
607
|
+
# Format: (hierarchical_label "text" (shape input) (at x y rotation) (effects ...) (uuid ...))
|
|
608
|
+
if len(item) < 2:
|
|
609
|
+
return None
|
|
610
|
+
|
|
611
|
+
hlabel_data = {
|
|
612
|
+
"text": str(item[1]), # Hierarchical label text is second element
|
|
613
|
+
"shape": "input", # input/output/bidirectional/tri_state/passive
|
|
614
|
+
"position": {"x": 0, "y": 0},
|
|
615
|
+
"rotation": 0,
|
|
616
|
+
"size": 1.27,
|
|
617
|
+
"justify": "left",
|
|
618
|
+
"uuid": None
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
for elem in item[2:]: # Skip hierarchical_label keyword and text
|
|
622
|
+
if not isinstance(elem, list):
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
626
|
+
|
|
627
|
+
if elem_type == "shape":
|
|
628
|
+
# Parse shape: (shape input)
|
|
629
|
+
if len(elem) >= 2:
|
|
630
|
+
hlabel_data["shape"] = str(elem[1])
|
|
631
|
+
|
|
632
|
+
elif elem_type == "at":
|
|
633
|
+
# Parse position: (at x y rotation)
|
|
634
|
+
if len(elem) >= 3:
|
|
635
|
+
hlabel_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
636
|
+
if len(elem) >= 4:
|
|
637
|
+
hlabel_data["rotation"] = float(elem[3])
|
|
638
|
+
|
|
639
|
+
elif elem_type == "effects":
|
|
640
|
+
# Parse effects for font size and justification: (effects (font (size x y)) (justify left))
|
|
641
|
+
for effect_elem in elem[1:]:
|
|
642
|
+
if isinstance(effect_elem, list):
|
|
643
|
+
effect_type = str(effect_elem[0]) if isinstance(effect_elem[0], sexpdata.Symbol) else None
|
|
644
|
+
|
|
645
|
+
if effect_type == "font":
|
|
646
|
+
# Parse font size
|
|
647
|
+
for font_elem in effect_elem[1:]:
|
|
648
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
649
|
+
if len(font_elem) >= 2:
|
|
650
|
+
hlabel_data["size"] = float(font_elem[1])
|
|
651
|
+
|
|
652
|
+
elif effect_type == "justify":
|
|
653
|
+
# Parse justification (e.g., "left", "right")
|
|
654
|
+
if len(effect_elem) >= 2:
|
|
655
|
+
hlabel_data["justify"] = str(effect_elem[1])
|
|
656
|
+
|
|
657
|
+
elif elem_type == "uuid":
|
|
658
|
+
hlabel_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
659
|
+
|
|
660
|
+
return hlabel_data
|
|
661
|
+
|
|
662
|
+
def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
663
|
+
"""Parse a no_connect symbol."""
|
|
664
|
+
# Format: (no_connect (at x y) (uuid ...))
|
|
665
|
+
no_connect_data = {
|
|
666
|
+
"position": {"x": 0, "y": 0},
|
|
667
|
+
"uuid": None
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
for elem in item[1:]:
|
|
671
|
+
if not isinstance(elem, list):
|
|
672
|
+
continue
|
|
673
|
+
|
|
674
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
675
|
+
|
|
676
|
+
if elem_type == "at":
|
|
677
|
+
if len(elem) >= 3:
|
|
678
|
+
no_connect_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
679
|
+
elif elem_type == "uuid":
|
|
680
|
+
no_connect_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
681
|
+
|
|
682
|
+
return no_connect_data
|
|
683
|
+
|
|
684
|
+
def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
685
|
+
"""Parse a text element."""
|
|
686
|
+
# Format: (text "text" (exclude_from_sim no) (at x y rotation) (effects ...) (uuid ...))
|
|
687
|
+
if len(item) < 2:
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
text_data = {
|
|
691
|
+
"text": str(item[1]),
|
|
692
|
+
"exclude_from_sim": False,
|
|
693
|
+
"position": {"x": 0, "y": 0},
|
|
694
|
+
"rotation": 0,
|
|
695
|
+
"size": 1.27,
|
|
696
|
+
"uuid": None
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
for elem in item[2:]:
|
|
700
|
+
if not isinstance(elem, list):
|
|
701
|
+
continue
|
|
702
|
+
|
|
703
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
704
|
+
|
|
705
|
+
if elem_type == "exclude_from_sim":
|
|
706
|
+
if len(elem) >= 2:
|
|
707
|
+
text_data["exclude_from_sim"] = str(elem[1]) == "yes"
|
|
708
|
+
elif elem_type == "at":
|
|
709
|
+
if len(elem) >= 3:
|
|
710
|
+
text_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
711
|
+
if len(elem) >= 4:
|
|
712
|
+
text_data["rotation"] = float(elem[3])
|
|
713
|
+
elif elem_type == "effects":
|
|
714
|
+
for effect_elem in elem[1:]:
|
|
715
|
+
if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
|
|
716
|
+
for font_elem in effect_elem[1:]:
|
|
717
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
718
|
+
if len(font_elem) >= 2:
|
|
719
|
+
text_data["size"] = float(font_elem[1])
|
|
720
|
+
elif elem_type == "uuid":
|
|
721
|
+
text_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
722
|
+
|
|
723
|
+
return text_data
|
|
724
|
+
|
|
725
|
+
def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
726
|
+
"""Parse a text_box element."""
|
|
727
|
+
# Format: (text_box "text" (exclude_from_sim no) (at x y rotation) (size w h) (margins ...) (stroke ...) (fill ...) (effects ...) (uuid ...))
|
|
728
|
+
if len(item) < 2:
|
|
729
|
+
return None
|
|
730
|
+
|
|
731
|
+
text_box_data = {
|
|
732
|
+
"text": str(item[1]),
|
|
733
|
+
"exclude_from_sim": False,
|
|
734
|
+
"position": {"x": 0, "y": 0},
|
|
735
|
+
"rotation": 0,
|
|
736
|
+
"size": {"width": 0, "height": 0},
|
|
737
|
+
"margins": (0.9525, 0.9525, 0.9525, 0.9525),
|
|
738
|
+
"stroke_width": 0,
|
|
739
|
+
"stroke_type": "solid",
|
|
740
|
+
"fill_type": "none",
|
|
741
|
+
"font_size": 1.27,
|
|
742
|
+
"justify_horizontal": "left",
|
|
743
|
+
"justify_vertical": "top",
|
|
744
|
+
"uuid": None
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
for elem in item[2:]:
|
|
748
|
+
if not isinstance(elem, list):
|
|
749
|
+
continue
|
|
750
|
+
|
|
751
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
752
|
+
|
|
753
|
+
if elem_type == "exclude_from_sim":
|
|
754
|
+
if len(elem) >= 2:
|
|
755
|
+
text_box_data["exclude_from_sim"] = str(elem[1]) == "yes"
|
|
756
|
+
elif elem_type == "at":
|
|
757
|
+
if len(elem) >= 3:
|
|
758
|
+
text_box_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
759
|
+
if len(elem) >= 4:
|
|
760
|
+
text_box_data["rotation"] = float(elem[3])
|
|
761
|
+
elif elem_type == "size":
|
|
762
|
+
if len(elem) >= 3:
|
|
763
|
+
text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
|
|
764
|
+
elif elem_type == "margins":
|
|
765
|
+
if len(elem) >= 5:
|
|
766
|
+
text_box_data["margins"] = (float(elem[1]), float(elem[2]), float(elem[3]), float(elem[4]))
|
|
767
|
+
elif elem_type == "stroke":
|
|
768
|
+
for stroke_elem in elem[1:]:
|
|
769
|
+
if isinstance(stroke_elem, list):
|
|
770
|
+
stroke_type = str(stroke_elem[0])
|
|
771
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
772
|
+
text_box_data["stroke_width"] = float(stroke_elem[1])
|
|
773
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
774
|
+
text_box_data["stroke_type"] = str(stroke_elem[1])
|
|
775
|
+
elif elem_type == "fill":
|
|
776
|
+
for fill_elem in elem[1:]:
|
|
777
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
778
|
+
text_box_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
779
|
+
elif elem_type == "effects":
|
|
780
|
+
for effect_elem in elem[1:]:
|
|
781
|
+
if isinstance(effect_elem, list):
|
|
782
|
+
effect_type = str(effect_elem[0])
|
|
783
|
+
if effect_type == "font":
|
|
784
|
+
for font_elem in effect_elem[1:]:
|
|
785
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
786
|
+
if len(font_elem) >= 2:
|
|
787
|
+
text_box_data["font_size"] = float(font_elem[1])
|
|
788
|
+
elif effect_type == "justify":
|
|
789
|
+
if len(effect_elem) >= 2:
|
|
790
|
+
text_box_data["justify_horizontal"] = str(effect_elem[1])
|
|
791
|
+
if len(effect_elem) >= 3:
|
|
792
|
+
text_box_data["justify_vertical"] = str(effect_elem[2])
|
|
793
|
+
elif elem_type == "uuid":
|
|
794
|
+
text_box_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
795
|
+
|
|
796
|
+
return text_box_data
|
|
797
|
+
|
|
798
|
+
def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
799
|
+
"""Parse a hierarchical sheet."""
|
|
800
|
+
# Complex format with position, size, properties, pins, instances
|
|
801
|
+
sheet_data = {
|
|
802
|
+
"position": {"x": 0, "y": 0},
|
|
803
|
+
"size": {"width": 0, "height": 0},
|
|
804
|
+
"exclude_from_sim": False,
|
|
805
|
+
"in_bom": True,
|
|
806
|
+
"on_board": True,
|
|
807
|
+
"dnp": False,
|
|
808
|
+
"fields_autoplaced": True,
|
|
809
|
+
"stroke_width": 0.1524,
|
|
810
|
+
"stroke_type": "solid",
|
|
811
|
+
"fill_color": (0, 0, 0, 0.0),
|
|
812
|
+
"uuid": None,
|
|
813
|
+
"name": "Sheet",
|
|
814
|
+
"filename": "sheet.kicad_sch",
|
|
815
|
+
"pins": [],
|
|
816
|
+
"project_name": "",
|
|
817
|
+
"page_number": "2"
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
for elem in item[1:]:
|
|
821
|
+
if not isinstance(elem, list):
|
|
822
|
+
continue
|
|
823
|
+
|
|
824
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
825
|
+
|
|
826
|
+
if elem_type == "at":
|
|
827
|
+
if len(elem) >= 3:
|
|
828
|
+
sheet_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
829
|
+
elif elem_type == "size":
|
|
830
|
+
if len(elem) >= 3:
|
|
831
|
+
sheet_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
|
|
832
|
+
elif elem_type == "exclude_from_sim":
|
|
833
|
+
sheet_data["exclude_from_sim"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
834
|
+
elif elem_type == "in_bom":
|
|
835
|
+
sheet_data["in_bom"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
836
|
+
elif elem_type == "on_board":
|
|
837
|
+
sheet_data["on_board"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
838
|
+
elif elem_type == "dnp":
|
|
839
|
+
sheet_data["dnp"] = str(elem[1]) == "yes" if len(elem) > 1 else False
|
|
840
|
+
elif elem_type == "fields_autoplaced":
|
|
841
|
+
sheet_data["fields_autoplaced"] = str(elem[1]) == "yes" if len(elem) > 1 else True
|
|
842
|
+
elif elem_type == "stroke":
|
|
843
|
+
for stroke_elem in elem[1:]:
|
|
844
|
+
if isinstance(stroke_elem, list):
|
|
845
|
+
stroke_type = str(stroke_elem[0])
|
|
846
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
847
|
+
sheet_data["stroke_width"] = float(stroke_elem[1])
|
|
848
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
849
|
+
sheet_data["stroke_type"] = str(stroke_elem[1])
|
|
850
|
+
elif elem_type == "fill":
|
|
851
|
+
for fill_elem in elem[1:]:
|
|
852
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
|
|
853
|
+
if len(fill_elem) >= 5:
|
|
854
|
+
sheet_data["fill_color"] = (int(fill_elem[1]), int(fill_elem[2]), int(fill_elem[3]), float(fill_elem[4]))
|
|
855
|
+
elif elem_type == "uuid":
|
|
856
|
+
sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
857
|
+
elif elem_type == "property":
|
|
858
|
+
if len(elem) >= 3:
|
|
859
|
+
prop_name = str(elem[1])
|
|
860
|
+
prop_value = str(elem[2])
|
|
861
|
+
if prop_name == "Sheetname":
|
|
862
|
+
sheet_data["name"] = prop_value
|
|
863
|
+
elif prop_name == "Sheetfile":
|
|
864
|
+
sheet_data["filename"] = prop_value
|
|
865
|
+
elif elem_type == "pin":
|
|
866
|
+
# Parse sheet pin - reuse existing _parse_sheet_pin helper
|
|
867
|
+
pin_data = self._parse_sheet_pin_for_read(elem)
|
|
868
|
+
if pin_data:
|
|
869
|
+
sheet_data["pins"].append(pin_data)
|
|
870
|
+
elif elem_type == "instances":
|
|
871
|
+
# Parse instances for project name and page number
|
|
872
|
+
for inst_elem in elem[1:]:
|
|
873
|
+
if isinstance(inst_elem, list) and str(inst_elem[0]) == "project":
|
|
874
|
+
if len(inst_elem) >= 2:
|
|
875
|
+
sheet_data["project_name"] = str(inst_elem[1])
|
|
876
|
+
for path_elem in inst_elem[2:]:
|
|
877
|
+
if isinstance(path_elem, list) and str(path_elem[0]) == "path":
|
|
878
|
+
for page_elem in path_elem[1:]:
|
|
879
|
+
if isinstance(page_elem, list) and str(page_elem[0]) == "page":
|
|
880
|
+
sheet_data["page_number"] = str(page_elem[1]) if len(page_elem) > 1 else "2"
|
|
881
|
+
|
|
882
|
+
return sheet_data
|
|
883
|
+
|
|
884
|
+
def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
885
|
+
"""Parse a sheet pin (for reading during sheet parsing)."""
|
|
886
|
+
# Format: (pin "name" type (at x y rotation) (uuid ...) (effects ...))
|
|
887
|
+
if len(item) < 3:
|
|
888
|
+
return None
|
|
889
|
+
|
|
890
|
+
pin_data = {
|
|
891
|
+
"name": str(item[1]),
|
|
892
|
+
"pin_type": str(item[2]) if len(item) > 2 else "input",
|
|
893
|
+
"position": {"x": 0, "y": 0},
|
|
894
|
+
"rotation": 0,
|
|
895
|
+
"size": 1.27,
|
|
896
|
+
"justify": "right",
|
|
897
|
+
"uuid": None
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
for elem in item[3:]:
|
|
901
|
+
if not isinstance(elem, list):
|
|
902
|
+
continue
|
|
903
|
+
|
|
904
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
905
|
+
|
|
906
|
+
if elem_type == "at":
|
|
907
|
+
if len(elem) >= 3:
|
|
908
|
+
pin_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
909
|
+
if len(elem) >= 4:
|
|
910
|
+
pin_data["rotation"] = float(elem[3])
|
|
911
|
+
elif elem_type == "uuid":
|
|
912
|
+
pin_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
913
|
+
elif elem_type == "effects":
|
|
914
|
+
for effect_elem in elem[1:]:
|
|
915
|
+
if isinstance(effect_elem, list):
|
|
916
|
+
effect_type = str(effect_elem[0])
|
|
917
|
+
if effect_type == "font":
|
|
918
|
+
for font_elem in effect_elem[1:]:
|
|
919
|
+
if isinstance(font_elem, list) and str(font_elem[0]) == "size":
|
|
920
|
+
if len(font_elem) >= 2:
|
|
921
|
+
pin_data["size"] = float(font_elem[1])
|
|
922
|
+
elif effect_type == "justify":
|
|
923
|
+
if len(effect_elem) >= 2:
|
|
924
|
+
pin_data["justify"] = str(effect_elem[1])
|
|
925
|
+
|
|
926
|
+
return pin_data
|
|
927
|
+
|
|
928
|
+
def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
929
|
+
"""Parse a polyline graphical element."""
|
|
930
|
+
# Format: (polyline (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (uuid ...))
|
|
931
|
+
polyline_data = {
|
|
932
|
+
"points": [],
|
|
933
|
+
"stroke_width": 0,
|
|
934
|
+
"stroke_type": "default",
|
|
935
|
+
"uuid": None
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
for elem in item[1:]:
|
|
939
|
+
if not isinstance(elem, list):
|
|
940
|
+
continue
|
|
941
|
+
|
|
942
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
943
|
+
|
|
944
|
+
if elem_type == "pts":
|
|
945
|
+
for pt in elem[1:]:
|
|
946
|
+
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
947
|
+
polyline_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
948
|
+
elif elem_type == "stroke":
|
|
949
|
+
for stroke_elem in elem[1:]:
|
|
950
|
+
if isinstance(stroke_elem, list):
|
|
951
|
+
stroke_type = str(stroke_elem[0])
|
|
952
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
953
|
+
polyline_data["stroke_width"] = float(stroke_elem[1])
|
|
954
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
955
|
+
polyline_data["stroke_type"] = str(stroke_elem[1])
|
|
956
|
+
elif elem_type == "uuid":
|
|
957
|
+
polyline_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
958
|
+
|
|
959
|
+
return polyline_data if polyline_data["points"] else None
|
|
960
|
+
|
|
961
|
+
def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
962
|
+
"""Parse an arc graphical element."""
|
|
963
|
+
# Format: (arc (start x y) (mid x y) (end x y) (stroke ...) (fill ...) (uuid ...))
|
|
964
|
+
arc_data = {
|
|
965
|
+
"start": {"x": 0, "y": 0},
|
|
966
|
+
"mid": {"x": 0, "y": 0},
|
|
967
|
+
"end": {"x": 0, "y": 0},
|
|
968
|
+
"stroke_width": 0,
|
|
969
|
+
"stroke_type": "default",
|
|
970
|
+
"fill_type": "none",
|
|
971
|
+
"uuid": None
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
for elem in item[1:]:
|
|
975
|
+
if not isinstance(elem, list):
|
|
976
|
+
continue
|
|
977
|
+
|
|
978
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
979
|
+
|
|
980
|
+
if elem_type == "start" and len(elem) >= 3:
|
|
981
|
+
arc_data["start"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
982
|
+
elif elem_type == "mid" and len(elem) >= 3:
|
|
983
|
+
arc_data["mid"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
984
|
+
elif elem_type == "end" and len(elem) >= 3:
|
|
985
|
+
arc_data["end"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
986
|
+
elif elem_type == "stroke":
|
|
987
|
+
for stroke_elem in elem[1:]:
|
|
988
|
+
if isinstance(stroke_elem, list):
|
|
989
|
+
stroke_type = str(stroke_elem[0])
|
|
990
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
991
|
+
arc_data["stroke_width"] = float(stroke_elem[1])
|
|
992
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
993
|
+
arc_data["stroke_type"] = str(stroke_elem[1])
|
|
994
|
+
elif elem_type == "fill":
|
|
995
|
+
for fill_elem in elem[1:]:
|
|
996
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
997
|
+
arc_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
998
|
+
elif elem_type == "uuid":
|
|
999
|
+
arc_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1000
|
+
|
|
1001
|
+
return arc_data
|
|
1002
|
+
|
|
1003
|
+
def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1004
|
+
"""Parse a circle graphical element."""
|
|
1005
|
+
# Format: (circle (center x y) (radius r) (stroke ...) (fill ...) (uuid ...))
|
|
1006
|
+
circle_data = {
|
|
1007
|
+
"center": {"x": 0, "y": 0},
|
|
1008
|
+
"radius": 0,
|
|
1009
|
+
"stroke_width": 0,
|
|
1010
|
+
"stroke_type": "default",
|
|
1011
|
+
"fill_type": "none",
|
|
1012
|
+
"uuid": None
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
for elem in item[1:]:
|
|
1016
|
+
if not isinstance(elem, list):
|
|
1017
|
+
continue
|
|
1018
|
+
|
|
1019
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1020
|
+
|
|
1021
|
+
if elem_type == "center" and len(elem) >= 3:
|
|
1022
|
+
circle_data["center"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
1023
|
+
elif elem_type == "radius" and len(elem) >= 2:
|
|
1024
|
+
circle_data["radius"] = float(elem[1])
|
|
1025
|
+
elif elem_type == "stroke":
|
|
1026
|
+
for stroke_elem in elem[1:]:
|
|
1027
|
+
if isinstance(stroke_elem, list):
|
|
1028
|
+
stroke_type = str(stroke_elem[0])
|
|
1029
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1030
|
+
circle_data["stroke_width"] = float(stroke_elem[1])
|
|
1031
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1032
|
+
circle_data["stroke_type"] = str(stroke_elem[1])
|
|
1033
|
+
elif elem_type == "fill":
|
|
1034
|
+
for fill_elem in elem[1:]:
|
|
1035
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1036
|
+
circle_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1037
|
+
elif elem_type == "uuid":
|
|
1038
|
+
circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1039
|
+
|
|
1040
|
+
return circle_data
|
|
1041
|
+
|
|
1042
|
+
def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
1043
|
+
"""Parse a bezier curve graphical element."""
|
|
1044
|
+
# Format: (bezier (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (fill ...) (uuid ...))
|
|
1045
|
+
bezier_data = {
|
|
1046
|
+
"points": [],
|
|
1047
|
+
"stroke_width": 0,
|
|
1048
|
+
"stroke_type": "default",
|
|
1049
|
+
"fill_type": "none",
|
|
1050
|
+
"uuid": None
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
for elem in item[1:]:
|
|
1054
|
+
if not isinstance(elem, list):
|
|
1055
|
+
continue
|
|
1056
|
+
|
|
1057
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
1058
|
+
|
|
1059
|
+
if elem_type == "pts":
|
|
1060
|
+
for pt in elem[1:]:
|
|
1061
|
+
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
1062
|
+
bezier_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
1063
|
+
elif elem_type == "stroke":
|
|
1064
|
+
for stroke_elem in elem[1:]:
|
|
1065
|
+
if isinstance(stroke_elem, list):
|
|
1066
|
+
stroke_type = str(stroke_elem[0])
|
|
1067
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
1068
|
+
bezier_data["stroke_width"] = float(stroke_elem[1])
|
|
1069
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
1070
|
+
bezier_data["stroke_type"] = str(stroke_elem[1])
|
|
1071
|
+
elif elem_type == "fill":
|
|
1072
|
+
for fill_elem in elem[1:]:
|
|
1073
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
1074
|
+
bezier_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
1075
|
+
elif elem_type == "uuid":
|
|
1076
|
+
bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
1077
|
+
|
|
1078
|
+
return bezier_data if bezier_data["points"] else None
|
|
436
1079
|
|
|
437
1080
|
def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
438
1081
|
"""Parse a rectangle graphical element."""
|
|
@@ -838,6 +1481,171 @@ class SExpressionParser:
|
|
|
838
1481
|
|
|
839
1482
|
return sexp
|
|
840
1483
|
|
|
1484
|
+
def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
|
|
1485
|
+
"""Convert no_connect to S-expression."""
|
|
1486
|
+
sexp = [sexpdata.Symbol("no_connect")]
|
|
1487
|
+
|
|
1488
|
+
# Add position
|
|
1489
|
+
pos = no_connect_data["position"]
|
|
1490
|
+
x, y = pos["x"], pos["y"]
|
|
1491
|
+
|
|
1492
|
+
# Format coordinates properly
|
|
1493
|
+
if isinstance(x, float) and x.is_integer():
|
|
1494
|
+
x = int(x)
|
|
1495
|
+
if isinstance(y, float) and y.is_integer():
|
|
1496
|
+
y = int(y)
|
|
1497
|
+
|
|
1498
|
+
sexp.append([sexpdata.Symbol("at"), x, y])
|
|
1499
|
+
|
|
1500
|
+
# Add UUID
|
|
1501
|
+
if "uuid" in no_connect_data:
|
|
1502
|
+
sexp.append([sexpdata.Symbol("uuid"), no_connect_data["uuid"]])
|
|
1503
|
+
|
|
1504
|
+
return sexp
|
|
1505
|
+
|
|
1506
|
+
def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
|
|
1507
|
+
"""Convert polyline to S-expression."""
|
|
1508
|
+
sexp = [sexpdata.Symbol("polyline")]
|
|
1509
|
+
|
|
1510
|
+
# Add points
|
|
1511
|
+
points = polyline_data.get("points", [])
|
|
1512
|
+
if points:
|
|
1513
|
+
pts_sexp = [sexpdata.Symbol("pts")]
|
|
1514
|
+
for point in points:
|
|
1515
|
+
x, y = point["x"], point["y"]
|
|
1516
|
+
# Format coordinates properly
|
|
1517
|
+
if isinstance(x, float) and x.is_integer():
|
|
1518
|
+
x = int(x)
|
|
1519
|
+
if isinstance(y, float) and y.is_integer():
|
|
1520
|
+
y = int(y)
|
|
1521
|
+
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
1522
|
+
sexp.append(pts_sexp)
|
|
1523
|
+
|
|
1524
|
+
# Add stroke
|
|
1525
|
+
stroke_width = polyline_data.get("stroke_width", 0)
|
|
1526
|
+
stroke_type = polyline_data.get("stroke_type", "default")
|
|
1527
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1528
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1529
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1530
|
+
sexp.append(stroke_sexp)
|
|
1531
|
+
|
|
1532
|
+
# Add UUID
|
|
1533
|
+
if "uuid" in polyline_data:
|
|
1534
|
+
sexp.append([sexpdata.Symbol("uuid"), polyline_data["uuid"]])
|
|
1535
|
+
|
|
1536
|
+
return sexp
|
|
1537
|
+
|
|
1538
|
+
def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
|
|
1539
|
+
"""Convert arc to S-expression."""
|
|
1540
|
+
sexp = [sexpdata.Symbol("arc")]
|
|
1541
|
+
|
|
1542
|
+
# Add start, mid, end points
|
|
1543
|
+
for point_name in ["start", "mid", "end"]:
|
|
1544
|
+
point = arc_data.get(point_name, {"x": 0, "y": 0})
|
|
1545
|
+
x, y = point["x"], point["y"]
|
|
1546
|
+
# Format coordinates properly
|
|
1547
|
+
if isinstance(x, float) and x.is_integer():
|
|
1548
|
+
x = int(x)
|
|
1549
|
+
if isinstance(y, float) and y.is_integer():
|
|
1550
|
+
y = int(y)
|
|
1551
|
+
sexp.append([sexpdata.Symbol(point_name), x, y])
|
|
1552
|
+
|
|
1553
|
+
# Add stroke
|
|
1554
|
+
stroke_width = arc_data.get("stroke_width", 0)
|
|
1555
|
+
stroke_type = arc_data.get("stroke_type", "default")
|
|
1556
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1557
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1558
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1559
|
+
sexp.append(stroke_sexp)
|
|
1560
|
+
|
|
1561
|
+
# Add fill
|
|
1562
|
+
fill_type = arc_data.get("fill_type", "none")
|
|
1563
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1564
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1565
|
+
sexp.append(fill_sexp)
|
|
1566
|
+
|
|
1567
|
+
# Add UUID
|
|
1568
|
+
if "uuid" in arc_data:
|
|
1569
|
+
sexp.append([sexpdata.Symbol("uuid"), arc_data["uuid"]])
|
|
1570
|
+
|
|
1571
|
+
return sexp
|
|
1572
|
+
|
|
1573
|
+
def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
|
|
1574
|
+
"""Convert circle to S-expression."""
|
|
1575
|
+
sexp = [sexpdata.Symbol("circle")]
|
|
1576
|
+
|
|
1577
|
+
# Add center
|
|
1578
|
+
center = circle_data.get("center", {"x": 0, "y": 0})
|
|
1579
|
+
x, y = center["x"], center["y"]
|
|
1580
|
+
# Format coordinates properly
|
|
1581
|
+
if isinstance(x, float) and x.is_integer():
|
|
1582
|
+
x = int(x)
|
|
1583
|
+
if isinstance(y, float) and y.is_integer():
|
|
1584
|
+
y = int(y)
|
|
1585
|
+
sexp.append([sexpdata.Symbol("center"), x, y])
|
|
1586
|
+
|
|
1587
|
+
# Add radius
|
|
1588
|
+
radius = circle_data.get("radius", 0)
|
|
1589
|
+
sexp.append([sexpdata.Symbol("radius"), radius])
|
|
1590
|
+
|
|
1591
|
+
# Add stroke
|
|
1592
|
+
stroke_width = circle_data.get("stroke_width", 0)
|
|
1593
|
+
stroke_type = circle_data.get("stroke_type", "default")
|
|
1594
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1595
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1596
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1597
|
+
sexp.append(stroke_sexp)
|
|
1598
|
+
|
|
1599
|
+
# Add fill
|
|
1600
|
+
fill_type = circle_data.get("fill_type", "none")
|
|
1601
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1602
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1603
|
+
sexp.append(fill_sexp)
|
|
1604
|
+
|
|
1605
|
+
# Add UUID
|
|
1606
|
+
if "uuid" in circle_data:
|
|
1607
|
+
sexp.append([sexpdata.Symbol("uuid"), circle_data["uuid"]])
|
|
1608
|
+
|
|
1609
|
+
return sexp
|
|
1610
|
+
|
|
1611
|
+
def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
|
|
1612
|
+
"""Convert bezier curve to S-expression."""
|
|
1613
|
+
sexp = [sexpdata.Symbol("bezier")]
|
|
1614
|
+
|
|
1615
|
+
# Add points
|
|
1616
|
+
points = bezier_data.get("points", [])
|
|
1617
|
+
if points:
|
|
1618
|
+
pts_sexp = [sexpdata.Symbol("pts")]
|
|
1619
|
+
for point in points:
|
|
1620
|
+
x, y = point["x"], point["y"]
|
|
1621
|
+
# Format coordinates properly
|
|
1622
|
+
if isinstance(x, float) and x.is_integer():
|
|
1623
|
+
x = int(x)
|
|
1624
|
+
if isinstance(y, float) and y.is_integer():
|
|
1625
|
+
y = int(y)
|
|
1626
|
+
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
1627
|
+
sexp.append(pts_sexp)
|
|
1628
|
+
|
|
1629
|
+
# Add stroke
|
|
1630
|
+
stroke_width = bezier_data.get("stroke_width", 0)
|
|
1631
|
+
stroke_type = bezier_data.get("stroke_type", "default")
|
|
1632
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
1633
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
1634
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
1635
|
+
sexp.append(stroke_sexp)
|
|
1636
|
+
|
|
1637
|
+
# Add fill
|
|
1638
|
+
fill_type = bezier_data.get("fill_type", "none")
|
|
1639
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
1640
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
1641
|
+
sexp.append(fill_sexp)
|
|
1642
|
+
|
|
1643
|
+
# Add UUID
|
|
1644
|
+
if "uuid" in bezier_data:
|
|
1645
|
+
sexp.append([sexpdata.Symbol("uuid"), bezier_data["uuid"]])
|
|
1646
|
+
|
|
1647
|
+
return sexp
|
|
1648
|
+
|
|
841
1649
|
def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
|
|
842
1650
|
"""Convert hierarchical sheet to S-expression."""
|
|
843
1651
|
sexp = [sexpdata.Symbol("sheet")]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kicad-sch-api
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Professional KiCAD schematic manipulation library with exact format preservation
|
|
5
5
|
Author-email: Circuit-Synth <shane@circuit-synth.com>
|
|
6
6
|
Maintainer-email: Circuit-Synth <shane@circuit-synth.com>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
kicad_sch_api/__init__.py,sha256=
|
|
1
|
+
kicad_sch_api/__init__.py,sha256=6yDjQeaoOdZatCgDaIULVxwXqT1fjLSNjYU0yKRh9f4,2919
|
|
2
2
|
kicad_sch_api/cli.py,sha256=ZzmwzfHEvPgGfCiQBU4G2LBAyRtMNiBRoY21pivJSYc,7621
|
|
3
3
|
kicad_sch_api/py.typed,sha256=e4ldqxwpY7pNDG1olbvj4HSKr8sZ9vxgA_2ek8xXn-Q,70
|
|
4
4
|
kicad_sch_api/core/__init__.py,sha256=ur_KeYBlGKl-e1hLpLdxAhGV2A-PCCGkcqd0r6KSeBA,566
|
|
5
5
|
kicad_sch_api/core/component_bounds.py,sha256=BFYJYULyzs5it2hN7bHTimyS9Vet4dxsMklRStob-F4,17509
|
|
6
6
|
kicad_sch_api/core/components.py,sha256=tXRL18GObl2u94wl5jP-1ID56s_UD9F1gQ_iRIyZ_Kw,25290
|
|
7
7
|
kicad_sch_api/core/config.py,sha256=itw0j3DeIEHaFVf8p3mfAS1SP6jclBwvMv7NPdkThE4,4309
|
|
8
|
-
kicad_sch_api/core/formatter.py,sha256=
|
|
8
|
+
kicad_sch_api/core/formatter.py,sha256=7kwM7WdbVjT8biirWSSFO44OMjeByvzYYr_-mpGiEA4,20862
|
|
9
9
|
kicad_sch_api/core/geometry.py,sha256=27SgN0padLbQuTi8MV6UUCp6Pyaiv8V9gmYDOhfwny8,2947
|
|
10
10
|
kicad_sch_api/core/ic_manager.py,sha256=Kg0HIOMU-TGXiIkrnwcHFQ1Kfv_3rW2U1cwBKJsKopc,7219
|
|
11
11
|
kicad_sch_api/core/junctions.py,sha256=Ay6BsWX_DLs-wB0eMA2CytKKq0N8Ja41ZubJWpAqNgM,6122
|
|
12
12
|
kicad_sch_api/core/manhattan_routing.py,sha256=t_T2u0zsQB-a8dTijFmY-qFq-oDt2qDebYyXzD_pBWI,15989
|
|
13
|
-
kicad_sch_api/core/parser.py,sha256=
|
|
13
|
+
kicad_sch_api/core/parser.py,sha256=UMTpwrxtyFM5cjAkgLWts6vBJ254gc4Hvx-puZydkVQ,92209
|
|
14
14
|
kicad_sch_api/core/pin_utils.py,sha256=XGEow3HzBTyT8a0B_ZC8foMvwzYaENSaqTUwDW1rz24,5417
|
|
15
15
|
kicad_sch_api/core/schematic.py,sha256=U9-wrhuGtgRqZJfc76Dj-g1_ZTjrT8R9LmfX-BIBH8w,61201
|
|
16
16
|
kicad_sch_api/core/simple_manhattan.py,sha256=CvIHvwmfABPF-COzhblYxEgRoR_R_eD-lmBFHHjDuMI,7241
|
|
@@ -23,9 +23,9 @@ kicad_sch_api/library/__init__.py,sha256=NG9UTdcpn25Bl9tPsYs9ED7bvpaVPVdtLMbnxkQ
|
|
|
23
23
|
kicad_sch_api/library/cache.py,sha256=7na88grl465WHwUOGuOzYrrWwjsMBXhXVtxhnaJ9GBY,33208
|
|
24
24
|
kicad_sch_api/utils/__init__.py,sha256=1V_yGgI7jro6MUc4Pviux_WIeJ1wmiYFID186SZwWLQ,277
|
|
25
25
|
kicad_sch_api/utils/validation.py,sha256=XlWGRZJb3cOPYpU9sLQQgC_NASwbi6W-LCN7PzUmaPY,15626
|
|
26
|
-
kicad_sch_api-0.3.
|
|
27
|
-
kicad_sch_api-0.3.
|
|
28
|
-
kicad_sch_api-0.3.
|
|
29
|
-
kicad_sch_api-0.3.
|
|
30
|
-
kicad_sch_api-0.3.
|
|
31
|
-
kicad_sch_api-0.3.
|
|
26
|
+
kicad_sch_api-0.3.2.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
|
|
27
|
+
kicad_sch_api-0.3.2.dist-info/METADATA,sha256=m8NfvoWy9m4AfShTSlHkn2AMUzy1cZkxiQXPnF3e0a0,17183
|
|
28
|
+
kicad_sch_api-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
29
|
+
kicad_sch_api-0.3.2.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
|
|
30
|
+
kicad_sch_api-0.3.2.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
|
|
31
|
+
kicad_sch_api-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|