kicad-sch-api 0.3.0__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -12,6 +12,15 @@ from typing import Any, Dict, List, Optional, Tuple, Union
12
12
 
13
13
  import sexpdata
14
14
 
15
+ from ..parsers.elements.graphics_parser import GraphicsParser
16
+ from ..parsers.elements.label_parser import LabelParser
17
+ from ..parsers.elements.library_parser import LibraryParser
18
+ from ..parsers.elements.metadata_parser import MetadataParser
19
+ from ..parsers.elements.sheet_parser import SheetParser
20
+ from ..parsers.elements.symbol_parser import SymbolParser
21
+ from ..parsers.elements.text_parser import TextParser
22
+ from ..parsers.elements.wire_parser import WireParser
23
+ from ..parsers.utils import color_to_rgb255, color_to_rgba
15
24
  from ..utils.validation import ValidationError, ValidationIssue
16
25
  from .formatter import ExactFormatter
17
26
  from .types import Junction, Label, Net, Point, SchematicSymbol, Wire
@@ -40,8 +49,30 @@ class SExpressionParser:
40
49
  self.preserve_format = preserve_format
41
50
  self._formatter = ExactFormatter() if preserve_format else None
42
51
  self._validation_issues = []
52
+ self._graphics_parser = GraphicsParser()
53
+ self._wire_parser = WireParser()
54
+ self._label_parser = LabelParser()
55
+ self._text_parser = TextParser()
56
+ self._sheet_parser = SheetParser()
57
+ self._library_parser = LibraryParser()
58
+ self._symbol_parser = SymbolParser()
59
+ self._metadata_parser = MetadataParser()
60
+ self._project_name = None
43
61
  logger.info(f"S-expression parser initialized (format preservation: {preserve_format})")
44
62
 
63
+ @property
64
+ def project_name(self):
65
+ """Get project name."""
66
+ return self._project_name
67
+
68
+ @project_name.setter
69
+ def project_name(self, value):
70
+ """Set project name on parser and propagate to sub-parsers."""
71
+ self._project_name = value
72
+ # Propagate to symbol parser which needs it for instances
73
+ if hasattr(self, '_symbol_parser'):
74
+ self._symbol_parser.project_name = value
75
+
45
76
  def parse_file(self, filepath: Union[str, Path]) -> Dict[str, Any]:
46
77
  """
47
78
  Parse a KiCAD schematic file with comprehensive validation.
@@ -187,7 +218,17 @@ class SExpressionParser:
187
218
  "wires": [],
188
219
  "junctions": [],
189
220
  "labels": [],
221
+ "hierarchical_labels": [],
222
+ "no_connects": [],
223
+ "texts": [],
224
+ "text_boxes": [],
225
+ "sheets": [],
226
+ "polylines": [],
227
+ "arcs": [],
228
+ "circles": [],
229
+ "beziers": [],
190
230
  "rectangles": [],
231
+ "images": [],
191
232
  "nets": [],
192
233
  "lib_symbols": {},
193
234
  "sheet_instances": [],
@@ -233,10 +274,50 @@ class SExpressionParser:
233
274
  label = self._parse_label(item)
234
275
  if label:
235
276
  schematic_data["labels"].append(label)
277
+ elif element_type == "hierarchical_label":
278
+ hlabel = self._parse_hierarchical_label(item)
279
+ if hlabel:
280
+ schematic_data["hierarchical_labels"].append(hlabel)
281
+ elif element_type == "no_connect":
282
+ no_connect = self._parse_no_connect(item)
283
+ if no_connect:
284
+ schematic_data["no_connects"].append(no_connect)
285
+ elif element_type == "text":
286
+ text = self._parse_text(item)
287
+ if text:
288
+ schematic_data["texts"].append(text)
289
+ elif element_type == "text_box":
290
+ text_box = self._parse_text_box(item)
291
+ if text_box:
292
+ schematic_data["text_boxes"].append(text_box)
293
+ elif element_type == "sheet":
294
+ sheet = self._parse_sheet(item)
295
+ if sheet:
296
+ schematic_data["sheets"].append(sheet)
297
+ elif element_type == "polyline":
298
+ polyline = self._parse_polyline(item)
299
+ if polyline:
300
+ schematic_data["polylines"].append(polyline)
301
+ elif element_type == "arc":
302
+ arc = self._parse_arc(item)
303
+ if arc:
304
+ schematic_data["arcs"].append(arc)
305
+ elif element_type == "circle":
306
+ circle = self._parse_circle(item)
307
+ if circle:
308
+ schematic_data["circles"].append(circle)
309
+ elif element_type == "bezier":
310
+ bezier = self._parse_bezier(item)
311
+ if bezier:
312
+ schematic_data["beziers"].append(bezier)
236
313
  elif element_type == "rectangle":
237
314
  rectangle = self._parse_rectangle(item)
238
315
  if rectangle:
239
316
  schematic_data["rectangles"].append(rectangle)
317
+ elif element_type == "image":
318
+ image = self._parse_image(item)
319
+ if image:
320
+ schematic_data["images"].append(image)
240
321
  elif element_type == "lib_symbols":
241
322
  schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
242
323
  elif element_type == "sheet_instances":
@@ -297,25 +378,48 @@ class SExpressionParser:
297
378
  for hlabel in schematic_data.get("hierarchical_labels", []):
298
379
  sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
299
380
 
300
- # Add hierarchical sheets
301
- for sheet in schematic_data.get("sheets", []):
302
- sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
381
+ # Add no_connects
382
+ for no_connect in schematic_data.get("no_connects", []):
383
+ sexp_data.append(self._no_connect_to_sexp(no_connect))
384
+
385
+ # Add graphical elements (in KiCad element order)
386
+ # Beziers
387
+ for bezier in schematic_data.get("beziers", []):
388
+ sexp_data.append(self._bezier_to_sexp(bezier))
389
+
390
+ # Rectangles (both from API and graphics)
391
+ for rectangle in schematic_data.get("rectangles", []):
392
+ sexp_data.append(self._rectangle_to_sexp(rectangle))
393
+ for graphic in schematic_data.get("graphics", []):
394
+ sexp_data.append(self._graphic_to_sexp(graphic))
303
395
 
304
- # Add text elements
396
+ # Images
397
+ for image in schematic_data.get("images", []):
398
+ sexp_data.append(self._image_to_sexp(image))
399
+
400
+ # Circles
401
+ for circle in schematic_data.get("circles", []):
402
+ sexp_data.append(self._circle_to_sexp(circle))
403
+
404
+ # Arcs
405
+ for arc in schematic_data.get("arcs", []):
406
+ sexp_data.append(self._arc_to_sexp(arc))
407
+
408
+ # Polylines
409
+ for polyline in schematic_data.get("polylines", []):
410
+ sexp_data.append(self._polyline_to_sexp(polyline))
411
+
412
+ # Text elements
305
413
  for text in schematic_data.get("texts", []):
306
414
  sexp_data.append(self._text_to_sexp(text))
307
415
 
308
- # Add text boxes
416
+ # Text boxes
309
417
  for text_box in schematic_data.get("text_boxes", []):
310
418
  sexp_data.append(self._text_box_to_sexp(text_box))
311
419
 
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))
420
+ # Hierarchical sheets
421
+ for sheet in schematic_data.get("sheets", []):
422
+ sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
319
423
 
320
424
  # Add sheet_instances (required by KiCAD)
321
425
  sheet_instances = schematic_data.get("sheet_instances", [])
@@ -339,276 +443,67 @@ class SExpressionParser:
339
443
 
340
444
  def _parse_title_block(self, item: List[Any]) -> Dict[str, Any]:
341
445
  """Parse title block information."""
342
- title_block = {}
343
- for sub_item in item[1:]:
344
- if isinstance(sub_item, list) and len(sub_item) >= 2:
345
- key = str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
346
- if key:
347
- title_block[key] = sub_item[1] if len(sub_item) > 1 else None
348
- return title_block
349
-
446
+ return self._metadata_parser._parse_title_block(item)
350
447
  def _parse_symbol(self, item: List[Any]) -> Optional[Dict[str, Any]]:
351
448
  """Parse a symbol (component) definition."""
352
- try:
353
- symbol_data = {
354
- "lib_id": None,
355
- "position": Point(0, 0),
356
- "rotation": 0,
357
- "uuid": None,
358
- "reference": None,
359
- "value": None,
360
- "footprint": None,
361
- "properties": {},
362
- "pins": [],
363
- "in_bom": True,
364
- "on_board": True,
365
- }
366
-
367
- for sub_item in item[1:]:
368
- if not isinstance(sub_item, list) or len(sub_item) == 0:
369
- continue
370
-
371
- element_type = (
372
- str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
373
- )
374
-
375
- if element_type == "lib_id":
376
- symbol_data["lib_id"] = sub_item[1] if len(sub_item) > 1 else None
377
- elif element_type == "at":
378
- if len(sub_item) >= 3:
379
- symbol_data["position"] = Point(float(sub_item[1]), float(sub_item[2]))
380
- if len(sub_item) > 3:
381
- symbol_data["rotation"] = float(sub_item[3])
382
- elif element_type == "uuid":
383
- symbol_data["uuid"] = sub_item[1] if len(sub_item) > 1 else None
384
- elif element_type == "property":
385
- prop_data = self._parse_property(sub_item)
386
- if prop_data:
387
- prop_name = prop_data.get("name")
388
- if prop_name == "Reference":
389
- symbol_data["reference"] = prop_data.get("value")
390
- elif prop_name == "Value":
391
- symbol_data["value"] = prop_data.get("value")
392
- elif prop_name == "Footprint":
393
- symbol_data["footprint"] = prop_data.get("value")
394
- else:
395
- # Unescape quotes in property values when loading
396
- prop_value = prop_data.get("value")
397
- if prop_value:
398
- prop_value = str(prop_value).replace('\\"', '"')
399
- symbol_data["properties"][prop_name] = prop_value
400
- elif element_type == "in_bom":
401
- symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
402
- elif element_type == "on_board":
403
- symbol_data["on_board"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
404
-
405
- return symbol_data
406
-
407
- except Exception as e:
408
- logger.warning(f"Error parsing symbol: {e}")
409
- return None
410
-
449
+ return self._symbol_parser._parse_symbol(item)
411
450
  def _parse_property(self, item: List[Any]) -> Optional[Dict[str, Any]]:
412
451
  """Parse a property definition."""
413
- if len(item) < 3:
414
- return None
415
-
416
- return {
417
- "name": item[1] if len(item) > 1 else None,
418
- "value": item[2] if len(item) > 2 else None,
419
- }
420
-
452
+ return self._symbol_parser._parse_property(item)
421
453
  def _parse_wire(self, item: List[Any]) -> Optional[Dict[str, Any]]:
422
454
  """Parse a wire definition."""
423
- # Implementation for wire parsing
424
- # This would parse pts, stroke, uuid elements
425
- return {}
426
-
455
+ return self._wire_parser._parse_wire(item)
427
456
  def _parse_junction(self, item: List[Any]) -> Optional[Dict[str, Any]]:
428
457
  """Parse a junction definition."""
429
- # Implementation for junction parsing
430
- return {}
431
-
458
+ return self._wire_parser._parse_junction(item)
432
459
  def _parse_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
433
460
  """Parse a label definition."""
434
- # Implementation for label parsing
435
- return {}
436
-
461
+ return self._label_parser._parse_label(item)
462
+ def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
463
+ """Parse a hierarchical label definition."""
464
+ return self._label_parser._parse_hierarchical_label(item)
465
+ def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
466
+ """Parse a no_connect symbol."""
467
+ return self._wire_parser._parse_no_connect(item)
468
+ def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
469
+ """Parse a text element."""
470
+ return self._text_parser._parse_text(item)
471
+ def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
472
+ """Parse a text_box element."""
473
+ return self._text_parser._parse_text_box(item)
474
+ def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
475
+ """Parse a hierarchical sheet."""
476
+ return self._sheet_parser._parse_sheet(item)
477
+ def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
478
+ """Parse a sheet pin (for reading during sheet parsing)."""
479
+ return self._sheet_parser._parse_sheet_pin_for_read(item)
480
+ def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
481
+ """Parse a polyline graphical element."""
482
+ return self._graphics_parser._parse_polyline(item)
483
+ def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
484
+ """Parse an arc graphical element."""
485
+ return self._graphics_parser._parse_arc(item)
486
+ def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
487
+ """Parse a circle graphical element."""
488
+ return self._graphics_parser._parse_circle(item)
489
+ def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
490
+ """Parse a bezier curve graphical element."""
491
+ return self._graphics_parser._parse_bezier(item)
437
492
  def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
438
493
  """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
-
494
+ return self._graphics_parser._parse_rectangle(item)
495
+ def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
496
+ """Parse an image element."""
497
+ return self._graphics_parser._parse_image(item)
468
498
  def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
469
499
  """Parse lib_symbols section."""
470
- # Implementation for lib_symbols parsing
471
- return {}
472
-
473
- # Conversion methods from internal format to S-expression
500
+ return self._library_parser._parse_lib_symbols(item)
474
501
  def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
475
502
  """Convert title block to S-expression."""
476
- sexp = [sexpdata.Symbol("title_block")]
477
-
478
- # Add standard fields
479
- for key in ["title", "date", "rev", "company"]:
480
- if key in title_block and title_block[key]:
481
- sexp.append([sexpdata.Symbol(key), title_block[key]])
482
-
483
- # Add comments with special formatting
484
- comments = title_block.get("comments", {})
485
- if isinstance(comments, dict):
486
- for comment_num, comment_text in comments.items():
487
- sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
488
-
489
- return sexp
490
-
503
+ return self._metadata_parser._title_block_to_sexp(title_block)
491
504
  def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
492
505
  """Convert symbol to S-expression."""
493
- sexp = [sexpdata.Symbol("symbol")]
494
-
495
- if symbol_data.get("lib_id"):
496
- sexp.append([sexpdata.Symbol("lib_id"), symbol_data["lib_id"]])
497
-
498
- # Add position and rotation (preserve original format)
499
- pos = symbol_data.get("position", Point(0, 0))
500
- rotation = symbol_data.get("rotation", 0)
501
- # Format numbers as integers if they are whole numbers
502
- x = int(pos.x) if pos.x == int(pos.x) else pos.x
503
- y = int(pos.y) if pos.y == int(pos.y) else pos.y
504
- r = int(rotation) if rotation == int(rotation) else rotation
505
- # Always include rotation for format consistency with KiCAD
506
- sexp.append([sexpdata.Symbol("at"), x, y, r])
507
-
508
- # Add unit (required by KiCAD)
509
- unit = symbol_data.get("unit", 1)
510
- sexp.append([sexpdata.Symbol("unit"), unit])
511
-
512
- # Add simulation and board settings (required by KiCAD)
513
- sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
514
- sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
515
- sexp.append(
516
- [sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
517
- )
518
- sexp.append([sexpdata.Symbol("dnp"), "no"])
519
- sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
520
-
521
- if symbol_data.get("uuid"):
522
- sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
523
-
524
- # Add properties with proper positioning and effects
525
- lib_id = symbol_data.get("lib_id", "")
526
- is_power_symbol = "power:" in lib_id
527
-
528
- if symbol_data.get("reference"):
529
- # Power symbol references should be hidden by default
530
- ref_hide = is_power_symbol
531
- ref_prop = self._create_property_with_positioning(
532
- "Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
533
- )
534
- sexp.append(ref_prop)
535
-
536
- if symbol_data.get("value"):
537
- # Power symbol values need different positioning
538
- if is_power_symbol:
539
- val_prop = self._create_power_symbol_value_property(
540
- symbol_data["value"], pos, lib_id
541
- )
542
- else:
543
- val_prop = self._create_property_with_positioning(
544
- "Value", symbol_data["value"], pos, 1, "left"
545
- )
546
- sexp.append(val_prop)
547
-
548
- footprint = symbol_data.get("footprint")
549
- if footprint is not None: # Include empty strings but not None
550
- fp_prop = self._create_property_with_positioning(
551
- "Footprint", footprint, pos, 2, "left", hide=True
552
- )
553
- sexp.append(fp_prop)
554
-
555
- for prop_name, prop_value in symbol_data.get("properties", {}).items():
556
- escaped_value = str(prop_value).replace('"', '\\"')
557
- prop = self._create_property_with_positioning(
558
- prop_name, escaped_value, pos, 3, "left", hide=True
559
- )
560
- sexp.append(prop)
561
-
562
- # Add pin UUID assignments (required by KiCAD)
563
- for pin in symbol_data.get("pins", []):
564
- pin_uuid = str(uuid.uuid4())
565
- # Ensure pin number is a string for proper quoting
566
- pin_number = str(pin.number)
567
- sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
568
-
569
- # Add instances section (required by KiCAD)
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
-
591
- logger.debug(f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}")
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
- )
609
-
610
- return sexp
611
-
506
+ return self._symbol_parser._symbol_to_sexp(symbol_data, schematic_uuid)
612
507
  def _create_property_with_positioning(
613
508
  self,
614
509
  prop_name: str,
@@ -692,745 +587,74 @@ class SExpressionParser:
692
587
 
693
588
  def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
694
589
  """Convert wire to S-expression."""
695
- sexp = [sexpdata.Symbol("wire")]
696
-
697
- # Add points (pts section)
698
- points = wire_data.get("points", [])
699
- if len(points) >= 2:
700
- pts_sexp = [sexpdata.Symbol("pts")]
701
- for point in points:
702
- if isinstance(point, dict):
703
- x, y = point["x"], point["y"]
704
- elif isinstance(point, (list, tuple)) and len(point) >= 2:
705
- x, y = point[0], point[1]
706
- else:
707
- # Assume it's a Point object
708
- x, y = point.x, point.y
709
-
710
- # Format coordinates properly (avoid unnecessary .0 for integers)
711
- if isinstance(x, float) and x.is_integer():
712
- x = int(x)
713
- if isinstance(y, float) and y.is_integer():
714
- y = int(y)
715
-
716
- pts_sexp.append([sexpdata.Symbol("xy"), x, y])
717
- sexp.append(pts_sexp)
718
-
719
- # Add stroke information
720
- stroke_width = wire_data.get("stroke_width", 0)
721
- stroke_type = wire_data.get("stroke_type", "default")
722
- stroke_sexp = [sexpdata.Symbol("stroke")]
723
-
724
- # Format stroke width (use int for 0, preserve float for others)
725
- if isinstance(stroke_width, float) and stroke_width == 0.0:
726
- stroke_width = 0
727
-
728
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
729
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
730
- sexp.append(stroke_sexp)
731
-
732
- # Add UUID
733
- if "uuid" in wire_data:
734
- sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
735
-
736
- return sexp
737
-
590
+ return self._wire_parser._wire_to_sexp(wire_data)
738
591
  def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
739
592
  """Convert junction to S-expression."""
740
- sexp = [sexpdata.Symbol("junction")]
741
-
742
- # Add position
743
- pos = junction_data["position"]
744
- if isinstance(pos, dict):
745
- x, y = pos["x"], pos["y"]
746
- elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
747
- x, y = pos[0], pos[1]
748
- else:
749
- # Assume it's a Point object
750
- x, y = pos.x, pos.y
751
-
752
- # Format coordinates properly
753
- if isinstance(x, float) and x.is_integer():
754
- x = int(x)
755
- if isinstance(y, float) and y.is_integer():
756
- y = int(y)
757
-
758
- sexp.append([sexpdata.Symbol("at"), x, y])
759
-
760
- # Add diameter
761
- diameter = junction_data.get("diameter", 0)
762
- sexp.append([sexpdata.Symbol("diameter"), diameter])
763
-
764
- # Add color (RGBA)
765
- color = junction_data.get("color", (0, 0, 0, 0))
766
- if isinstance(color, (list, tuple)) and len(color) >= 4:
767
- sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
768
- else:
769
- sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
770
-
771
- # Add UUID
772
- if "uuid" in junction_data:
773
- sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
774
-
775
- return sexp
776
-
593
+ return self._wire_parser._junction_to_sexp(junction_data)
777
594
  def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
778
595
  """Convert local label to S-expression."""
779
- sexp = [sexpdata.Symbol("label"), label_data["text"]]
780
-
781
- # Add position
782
- pos = label_data["position"]
783
- x, y = pos["x"], pos["y"]
784
- rotation = label_data.get("rotation", 0)
785
-
786
- # Format coordinates properly
787
- if isinstance(x, float) and x.is_integer():
788
- x = int(x)
789
- if isinstance(y, float) and y.is_integer():
790
- y = int(y)
791
-
792
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
793
-
794
- # Add effects (font properties)
795
- size = label_data.get("size", 1.27)
796
- effects = [sexpdata.Symbol("effects")]
797
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
798
- effects.append(font)
799
- effects.append(
800
- [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]
801
- )
802
- sexp.append(effects)
803
-
804
- # Add UUID
805
- if "uuid" in label_data:
806
- sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
807
-
808
- return sexp
809
-
596
+ return self._label_parser._label_to_sexp(label_data)
810
597
  def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
811
598
  """Convert hierarchical label to S-expression."""
812
- sexp = [sexpdata.Symbol("hierarchical_label"), hlabel_data["text"]]
813
-
814
- # Add shape
815
- shape = hlabel_data.get("shape", "input")
816
- sexp.append([sexpdata.Symbol("shape"), sexpdata.Symbol(shape)])
817
-
818
- # Add position
819
- pos = hlabel_data["position"]
820
- x, y = pos["x"], pos["y"]
821
- rotation = hlabel_data.get("rotation", 0)
822
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
823
-
824
- # Add effects (font properties)
825
- size = hlabel_data.get("size", 1.27)
826
- effects = [sexpdata.Symbol("effects")]
827
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
828
- effects.append(font)
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)])
833
- sexp.append(effects)
834
-
835
- # Add UUID
836
- if "uuid" in hlabel_data:
837
- sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
838
-
839
- return sexp
840
-
599
+ return self._label_parser._hierarchical_label_to_sexp(hlabel_data)
600
+ def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
601
+ """Convert no_connect to S-expression."""
602
+ return self._wire_parser._no_connect_to_sexp(no_connect_data)
603
+ def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
604
+ """Convert polyline to S-expression."""
605
+ return self._graphics_parser._polyline_to_sexp(polyline_data)
606
+ def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
607
+ """Convert arc to S-expression."""
608
+ return self._graphics_parser._arc_to_sexp(arc_data)
609
+ def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
610
+ """Convert circle to S-expression."""
611
+ return self._graphics_parser._circle_to_sexp(circle_data)
612
+ def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
613
+ """Convert bezier curve to S-expression."""
614
+ return self._graphics_parser._bezier_to_sexp(bezier_data)
841
615
  def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
842
616
  """Convert hierarchical sheet to S-expression."""
843
- sexp = [sexpdata.Symbol("sheet")]
844
-
845
- # Add position
846
- pos = sheet_data["position"]
847
- x, y = pos["x"], pos["y"]
848
- if isinstance(x, float) and x.is_integer():
849
- x = int(x)
850
- if isinstance(y, float) and y.is_integer():
851
- y = int(y)
852
- sexp.append([sexpdata.Symbol("at"), x, y])
853
-
854
- # Add size
855
- size = sheet_data["size"]
856
- w, h = size["width"], size["height"]
857
- sexp.append([sexpdata.Symbol("size"), w, h])
858
-
859
- # Add basic properties
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
-
891
- # Add stroke
892
- stroke_width = sheet_data.get("stroke_width", 0.1524)
893
- stroke_type = sheet_data.get("stroke_type", "solid")
894
- stroke_sexp = [sexpdata.Symbol("stroke")]
895
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
896
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
897
- sexp.append(stroke_sexp)
898
-
899
- # Add fill
900
- fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
901
- fill_sexp = [sexpdata.Symbol("fill")]
902
- fill_sexp.append(
903
- [sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]]
904
- )
905
- sexp.append(fill_sexp)
906
-
907
- # Add UUID
908
- if "uuid" in sheet_data:
909
- sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
910
-
911
- # Add sheet properties (name and filename)
912
- name = sheet_data.get("name", "Sheet")
913
- filename = sheet_data.get("filename", "sheet.kicad_sch")
914
-
915
- # Sheetname property
916
- from .config import config
917
-
918
- name_prop = [sexpdata.Symbol("property"), "Sheetname", name]
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
- )
929
- sexp.append(name_prop)
930
-
931
- # Sheetfile property
932
- file_prop = [sexpdata.Symbol("property"), "Sheetfile", filename]
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
- )
943
- sexp.append(file_prop)
944
-
945
- # Add sheet pins if any
946
- for pin in sheet_data.get("pins", []):
947
- pin_sexp = self._sheet_pin_to_sexp(pin)
948
- sexp.append(pin_sexp)
949
-
950
- # Add instances
951
- if schematic_uuid:
952
- instances_sexp = [sexpdata.Symbol("instances")]
953
- project_name = sheet_data.get("project_name", "")
954
- page_number = sheet_data.get("page_number", "2")
955
- project_sexp = [sexpdata.Symbol("project"), project_name]
956
- path_sexp = [sexpdata.Symbol("path"), f"/{schematic_uuid}"]
957
- path_sexp.append([sexpdata.Symbol("page"), page_number])
958
- project_sexp.append(path_sexp)
959
- instances_sexp.append(project_sexp)
960
- sexp.append(instances_sexp)
961
-
962
- return sexp
963
-
617
+ return self._sheet_parser._sheet_to_sexp(sheet_data, schematic_uuid)
964
618
  def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
965
619
  """Convert sheet pin to S-expression."""
966
- pin_sexp = [
967
- sexpdata.Symbol("pin"),
968
- pin_data["name"],
969
- sexpdata.Symbol(pin_data.get("pin_type", "input")),
970
- ]
971
-
972
- # Add position
973
- pos = pin_data["position"]
974
- x, y = pos["x"], pos["y"]
975
- rotation = pin_data.get("rotation", 0)
976
- pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
977
-
978
- # Add UUID
979
- if "uuid" in pin_data:
980
- pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
981
-
982
- # Add effects
983
- size = pin_data.get("size", 1.27)
984
- effects = [sexpdata.Symbol("effects")]
985
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
986
- effects.append(font)
987
- justify = pin_data.get("justify", "right")
988
- effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
989
- pin_sexp.append(effects)
990
-
991
- return pin_sexp
992
-
620
+ return self._sheet_parser._sheet_pin_to_sexp(pin_data)
993
621
  def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
994
622
  """Convert text element to S-expression."""
995
- sexp = [sexpdata.Symbol("text"), text_data["text"]]
996
-
997
- # Add exclude_from_sim
998
- exclude_sim = text_data.get("exclude_from_sim", False)
999
- sexp.append(
1000
- [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
1001
- )
1002
-
1003
- # Add position
1004
- pos = text_data["position"]
1005
- x, y = pos["x"], pos["y"]
1006
- rotation = text_data.get("rotation", 0)
1007
-
1008
- # Format coordinates properly
1009
- if isinstance(x, float) and x.is_integer():
1010
- x = int(x)
1011
- if isinstance(y, float) and y.is_integer():
1012
- y = int(y)
1013
-
1014
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
1015
-
1016
- # Add effects (font properties)
1017
- size = text_data.get("size", 1.27)
1018
- effects = [sexpdata.Symbol("effects")]
1019
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
1020
- effects.append(font)
1021
- sexp.append(effects)
1022
-
1023
- # Add UUID
1024
- if "uuid" in text_data:
1025
- sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
1026
-
1027
- return sexp
1028
-
623
+ return self._text_parser._text_to_sexp(text_data)
1029
624
  def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
1030
625
  """Convert text box element to S-expression."""
1031
- sexp = [sexpdata.Symbol("text_box"), text_box_data["text"]]
1032
-
1033
- # Add exclude_from_sim
1034
- exclude_sim = text_box_data.get("exclude_from_sim", False)
1035
- sexp.append(
1036
- [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
1037
- )
1038
-
1039
- # Add position
1040
- pos = text_box_data["position"]
1041
- x, y = pos["x"], pos["y"]
1042
- rotation = text_box_data.get("rotation", 0)
1043
-
1044
- # Format coordinates properly
1045
- if isinstance(x, float) and x.is_integer():
1046
- x = int(x)
1047
- if isinstance(y, float) and y.is_integer():
1048
- y = int(y)
1049
-
1050
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
1051
-
1052
- # Add size
1053
- size = text_box_data["size"]
1054
- w, h = size["width"], size["height"]
1055
- sexp.append([sexpdata.Symbol("size"), w, h])
1056
-
1057
- # Add margins
1058
- margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
1059
- sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
1060
-
1061
- # Add stroke
1062
- stroke_width = text_box_data.get("stroke_width", 0)
1063
- stroke_type = text_box_data.get("stroke_type", "solid")
1064
- stroke_sexp = [sexpdata.Symbol("stroke")]
1065
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1066
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1067
- sexp.append(stroke_sexp)
1068
-
1069
- # Add fill
1070
- fill_type = text_box_data.get("fill_type", "none")
1071
- fill_sexp = [sexpdata.Symbol("fill")]
1072
- fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1073
- sexp.append(fill_sexp)
1074
-
1075
- # Add effects (font properties and justification)
1076
- font_size = text_box_data.get("font_size", 1.27)
1077
- justify_h = text_box_data.get("justify_horizontal", "left")
1078
- justify_v = text_box_data.get("justify_vertical", "top")
1079
-
1080
- effects = [sexpdata.Symbol("effects")]
1081
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
1082
- effects.append(font)
1083
- effects.append(
1084
- [sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
1085
- )
1086
- sexp.append(effects)
1087
-
1088
- # Add UUID
1089
- if "uuid" in text_box_data:
1090
- sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
1091
-
1092
- return sexp
1093
-
626
+ return self._text_parser._text_box_to_sexp(text_box_data)
1094
627
  def _rectangle_to_sexp(self, rectangle_data: Dict[str, Any]) -> List[Any]:
1095
628
  """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
-
1126
- return sexp
1127
-
629
+ return self._graphics_parser._rectangle_to_sexp(rectangle_data)
630
+ def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
631
+ """Convert image element to S-expression."""
632
+ return self._graphics_parser._image_to_sexp(image_data)
1128
633
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
1129
634
  """Convert lib_symbols to S-expression."""
1130
- sexp = [sexpdata.Symbol("lib_symbols")]
1131
-
1132
- # Add each symbol definition
1133
- for symbol_name, symbol_def in lib_symbols.items():
1134
- if isinstance(symbol_def, list):
1135
- # Raw S-expression data from parsed library file - use directly
1136
- sexp.append(symbol_def)
1137
- elif isinstance(symbol_def, dict):
1138
- # Dictionary format - convert to S-expression
1139
- symbol_sexp = self._create_basic_symbol_definition(symbol_name)
1140
- sexp.append(symbol_sexp)
1141
-
1142
- return sexp
1143
-
635
+ return self._library_parser._lib_symbols_to_sexp(lib_symbols)
1144
636
  def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
1145
637
  """Create a basic symbol definition for KiCAD compatibility."""
1146
- symbol_sexp = [sexpdata.Symbol("symbol"), lib_id]
1147
-
1148
- # Add basic symbol properties
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
-
1159
- # Add basic properties for the symbol
1160
- if "R" in lib_id: # Resistor
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
-
1208
- elif "C" in lib_id: # Capacitor
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
-
1256
- # Add basic graphics and pins (minimal for now)
1257
- symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
1258
-
1259
- return symbol_sexp
1260
-
638
+ return self._library_parser._create_basic_symbol_definition(lib_id)
1261
639
  def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
1262
640
  """Parse sheet_instances section."""
1263
- sheet_instances = []
1264
- for sheet_item in item[1:]: # Skip 'sheet_instances' header
1265
- if isinstance(sheet_item, list) and len(sheet_item) > 0:
1266
- sheet_data = {"path": "/", "page": "1"}
1267
- for element in sheet_item[1:]: # Skip element header
1268
- if isinstance(element, list) and len(element) >= 2:
1269
- key = (
1270
- str(element[0])
1271
- if isinstance(element[0], sexpdata.Symbol)
1272
- else str(element[0])
1273
- )
1274
- if key == "path":
1275
- sheet_data["path"] = element[1]
1276
- elif key == "page":
1277
- sheet_data["page"] = element[1]
1278
- sheet_instances.append(sheet_data)
1279
- return sheet_instances
1280
-
641
+ return self._sheet_parser._parse_sheet_instances(item)
1281
642
  def _parse_symbol_instances(self, item: List[Any]) -> List[Any]:
1282
643
  """Parse symbol_instances section."""
1283
- # For now, just return the raw structure minus the header
1284
- return item[1:] if len(item) > 1 else []
1285
-
644
+ return self._metadata_parser._parse_symbol_instances(item)
1286
645
  def _sheet_instances_to_sexp(self, sheet_instances: List[Dict[str, Any]]) -> List[Any]:
1287
646
  """Convert sheet_instances to S-expression."""
1288
- sexp = [sexpdata.Symbol("sheet_instances")]
1289
- for sheet in sheet_instances:
1290
- # Create: (path "/" (page "1"))
1291
- sheet_sexp = [
1292
- sexpdata.Symbol("path"),
1293
- sheet.get("path", "/"),
1294
- [sexpdata.Symbol("page"), str(sheet.get("page", "1"))],
1295
- ]
1296
- sexp.append(sheet_sexp)
1297
- return sexp
1298
-
647
+ return self._sheet_parser._sheet_instances_to_sexp(sheet_instances)
1299
648
  def _graphic_to_sexp(self, graphic_data: Dict[str, Any]) -> List[Any]:
1300
649
  """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
-
650
+ return self._graphics_parser._graphic_to_sexp(graphic_data)
1393
651
  def _color_to_rgba(self, color_name: str) -> List[float]:
1394
652
  """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])
653
+ return color_to_rgba(color_name)
1413
654
 
1414
655
  def _color_to_rgb255(self, color_name: str) -> List[int]:
1415
656
  """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])
657
+ return color_to_rgb255(color_name)
1434
658
 
1435
659
  def get_validation_issues(self) -> List[ValidationIssue]:
1436
660
  """Get list of validation issues from last parse operation."""