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