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
@@ -0,0 +1,175 @@
1
+ """
2
+ Exception hierarchy for kicad-sch-api.
3
+
4
+ Provides a structured exception hierarchy for better error handling and debugging.
5
+ All exceptions inherit from the base KiCadSchError class.
6
+ """
7
+
8
+ from typing import Any, List, Optional, TYPE_CHECKING
9
+
10
+ # Import validation types for type hints
11
+ # ValidationLevel is imported at runtime in methods that need it
12
+ if TYPE_CHECKING:
13
+ from ..utils.validation import ValidationIssue
14
+
15
+
16
+ class KiCadSchError(Exception):
17
+ """Base exception for all kicad-sch-api errors."""
18
+
19
+ pass
20
+
21
+
22
+ class ValidationError(KiCadSchError):
23
+ """
24
+ Raised when validation fails.
25
+
26
+ Supports rich error context with field/value information and can collect
27
+ multiple validation issues.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ issues: Optional[List["ValidationIssue"]] = None,
34
+ field: str = "",
35
+ value: Any = None,
36
+ ):
37
+ """
38
+ Initialize validation error with context.
39
+
40
+ Args:
41
+ message: Error message describing the validation failure
42
+ issues: List of validation issues (for collecting multiple errors)
43
+ field: The field name that failed validation
44
+ value: The invalid value that was provided
45
+ """
46
+ self.issues = issues or []
47
+ self.field = field
48
+ self.value = value
49
+ super().__init__(message)
50
+
51
+ def add_issue(self, issue: "ValidationIssue") -> None:
52
+ """Add a validation issue to this error."""
53
+ self.issues.append(issue)
54
+
55
+ def get_errors(self) -> List["ValidationIssue"]:
56
+ """Get only error-level issues."""
57
+ # Import here to avoid circular dependency
58
+ from ..utils.validation import ValidationLevel
59
+
60
+ return [
61
+ issue
62
+ for issue in self.issues
63
+ if hasattr(issue, 'level') and issue.level in (ValidationLevel.ERROR, ValidationLevel.CRITICAL)
64
+ ]
65
+
66
+ def get_warnings(self) -> List["ValidationIssue"]:
67
+ """Get only warning-level issues."""
68
+ # Import here to avoid circular dependency
69
+ from ..utils.validation import ValidationLevel
70
+
71
+ return [issue for issue in self.issues if hasattr(issue, 'level') and issue.level == ValidationLevel.WARNING]
72
+
73
+
74
+ class ReferenceError(ValidationError):
75
+ """Raised when a component reference is invalid."""
76
+
77
+ pass
78
+
79
+
80
+ class LibraryError(ValidationError):
81
+ """Raised when a library or symbol reference is invalid."""
82
+
83
+ pass
84
+
85
+
86
+ class GeometryError(ValidationError):
87
+ """Raised when geometry validation fails (positions, shapes, dimensions)."""
88
+
89
+ pass
90
+
91
+
92
+ class NetError(ValidationError):
93
+ """Raised when a net specification or operation is invalid."""
94
+
95
+ pass
96
+
97
+
98
+ class ParseError(KiCadSchError):
99
+ """Raised when parsing a schematic file fails."""
100
+
101
+ pass
102
+
103
+
104
+ class FormatError(KiCadSchError):
105
+ """Raised when formatting a schematic file fails."""
106
+
107
+ pass
108
+
109
+
110
+ class CollectionError(KiCadSchError):
111
+ """Raised when a collection operation fails."""
112
+
113
+ pass
114
+
115
+
116
+ class ElementNotFoundError(CollectionError):
117
+ """Raised when an element is not found in a collection."""
118
+
119
+ def __init__(self, message: str, element_type: str = "", identifier: str = ""):
120
+ """
121
+ Initialize element not found error.
122
+
123
+ Args:
124
+ message: Error message
125
+ element_type: Type of element (e.g., 'component', 'wire', 'junction')
126
+ identifier: The identifier used to search (e.g., 'R1', UUID)
127
+ """
128
+ self.element_type = element_type
129
+ self.identifier = identifier
130
+ super().__init__(message)
131
+
132
+
133
+ class DuplicateElementError(CollectionError):
134
+ """Raised when attempting to add a duplicate element."""
135
+
136
+ def __init__(self, message: str, element_type: str = "", identifier: str = ""):
137
+ """
138
+ Initialize duplicate element error.
139
+
140
+ Args:
141
+ message: Error message
142
+ element_type: Type of element (e.g., 'component', 'wire', 'junction')
143
+ identifier: The duplicate identifier (e.g., 'R1', UUID)
144
+ """
145
+ self.element_type = element_type
146
+ self.identifier = identifier
147
+ super().__init__(message)
148
+
149
+
150
+ class CollectionOperationError(CollectionError):
151
+ """Raised when a collection operation fails for reasons other than not found/duplicate."""
152
+
153
+ pass
154
+
155
+
156
+ class FileOperationError(KiCadSchError):
157
+ """Raised when a file I/O operation fails."""
158
+
159
+ pass
160
+
161
+
162
+ class CLIError(KiCadSchError):
163
+ """Raised when KiCad CLI execution fails."""
164
+
165
+ pass
166
+
167
+
168
+ class SchematicStateError(KiCadSchError):
169
+ """
170
+ Raised when an operation requires specific schematic state.
171
+
172
+ Examples: schematic must be saved before export, etc.
173
+ """
174
+
175
+ pass
@@ -0,0 +1,5 @@
1
+ """Factory classes for creating schematic elements."""
2
+
3
+ from .element_factory import ElementFactory
4
+
5
+ __all__ = ["ElementFactory"]
@@ -0,0 +1,278 @@
1
+ """
2
+ Element Factory for creating schematic elements from dictionaries.
3
+
4
+ Centralizes object creation logic that was previously duplicated in Schematic.__init__.
5
+ """
6
+
7
+ import uuid
8
+ from typing import Any, Dict, List
9
+
10
+ from ..types import (
11
+ HierarchicalLabelShape,
12
+ Junction,
13
+ Label,
14
+ LabelType,
15
+ Net,
16
+ NoConnect,
17
+ Point,
18
+ Text,
19
+ Wire,
20
+ WireType,
21
+ )
22
+
23
+
24
+ def point_from_dict_or_tuple(position: Any) -> Point:
25
+ """Convert position data (dict or tuple) to Point object."""
26
+ if isinstance(position, dict):
27
+ return Point(position.get("x", 0), position.get("y", 0))
28
+ elif isinstance(position, (list, tuple)):
29
+ return Point(position[0], position[1])
30
+ elif isinstance(position, Point):
31
+ return position
32
+ else:
33
+ return Point(0, 0)
34
+
35
+
36
+ class ElementFactory:
37
+ """Factory for creating schematic elements from dictionary data."""
38
+
39
+ @staticmethod
40
+ def create_wire(wire_dict: Dict[str, Any]) -> Wire:
41
+ """
42
+ Create Wire object from dictionary.
43
+
44
+ Args:
45
+ wire_dict: Dictionary containing wire data
46
+
47
+ Returns:
48
+ Wire object
49
+ """
50
+ points = []
51
+ for point_data in wire_dict.get("points", []):
52
+ if isinstance(point_data, dict):
53
+ points.append(Point(point_data["x"], point_data["y"]))
54
+ elif isinstance(point_data, (list, tuple)):
55
+ points.append(Point(point_data[0], point_data[1]))
56
+ else:
57
+ points.append(point_data)
58
+
59
+ return Wire(
60
+ uuid=wire_dict.get("uuid", str(uuid.uuid4())),
61
+ points=points,
62
+ wire_type=WireType(wire_dict.get("wire_type", "wire")),
63
+ stroke_width=wire_dict.get("stroke_width", 0.0),
64
+ stroke_type=wire_dict.get("stroke_type", "default"),
65
+ )
66
+
67
+ @staticmethod
68
+ def create_junction(junction_dict: Dict[str, Any]) -> Junction:
69
+ """
70
+ Create Junction object from dictionary.
71
+
72
+ Args:
73
+ junction_dict: Dictionary containing junction data
74
+
75
+ Returns:
76
+ Junction object
77
+ """
78
+ position = junction_dict.get("position", {"x": 0, "y": 0})
79
+ pos = point_from_dict_or_tuple(position)
80
+
81
+ return Junction(
82
+ uuid=junction_dict.get("uuid", str(uuid.uuid4())),
83
+ position=pos,
84
+ diameter=junction_dict.get("diameter", 0),
85
+ color=junction_dict.get("color", (0, 0, 0, 0)),
86
+ )
87
+
88
+ @staticmethod
89
+ def create_text(text_dict: Dict[str, Any]) -> Text:
90
+ """
91
+ Create Text object from dictionary.
92
+
93
+ Args:
94
+ text_dict: Dictionary containing text data
95
+
96
+ Returns:
97
+ Text object
98
+ """
99
+ position = text_dict.get("position", {"x": 0, "y": 0})
100
+ pos = point_from_dict_or_tuple(position)
101
+
102
+ return Text(
103
+ uuid=text_dict.get("uuid", str(uuid.uuid4())),
104
+ position=pos,
105
+ text=text_dict.get("text", ""),
106
+ rotation=text_dict.get("rotation", 0.0),
107
+ size=text_dict.get("size", 1.27),
108
+ exclude_from_sim=text_dict.get("exclude_from_sim", False),
109
+ )
110
+
111
+ @staticmethod
112
+ def create_label(label_dict: Dict[str, Any]) -> Label:
113
+ """
114
+ Create Label object from dictionary.
115
+
116
+ Args:
117
+ label_dict: Dictionary containing label data
118
+
119
+ Returns:
120
+ Label object
121
+ """
122
+ position = label_dict.get("position", {"x": 0, "y": 0})
123
+ pos = point_from_dict_or_tuple(position)
124
+
125
+ return Label(
126
+ uuid=label_dict.get("uuid", str(uuid.uuid4())),
127
+ position=pos,
128
+ text=label_dict.get("text", ""),
129
+ label_type=LabelType(label_dict.get("label_type", "label")),
130
+ rotation=label_dict.get("rotation", 0.0),
131
+ size=label_dict.get("size", 1.27),
132
+ shape=(
133
+ HierarchicalLabelShape(label_dict.get("shape"))
134
+ if label_dict.get("shape")
135
+ else None
136
+ ),
137
+ justify_h=label_dict.get("justify_h", "left"),
138
+ justify_v=label_dict.get("justify_v", "bottom"),
139
+ )
140
+
141
+ @staticmethod
142
+ def create_no_connect(no_connect_dict: Dict[str, Any]) -> NoConnect:
143
+ """
144
+ Create NoConnect object from dictionary.
145
+
146
+ Args:
147
+ no_connect_dict: Dictionary containing no-connect data
148
+
149
+ Returns:
150
+ NoConnect object
151
+ """
152
+ position = no_connect_dict.get("position", {"x": 0, "y": 0})
153
+ pos = point_from_dict_or_tuple(position)
154
+
155
+ return NoConnect(
156
+ uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
157
+ position=pos,
158
+ )
159
+
160
+ @staticmethod
161
+ def create_net(net_dict: Dict[str, Any]) -> Net:
162
+ """
163
+ Create Net object from dictionary.
164
+
165
+ Args:
166
+ net_dict: Dictionary containing net data
167
+
168
+ Returns:
169
+ Net object
170
+ """
171
+ return Net(
172
+ name=net_dict.get("name", ""),
173
+ components=net_dict.get("components", []),
174
+ wires=net_dict.get("wires", []),
175
+ labels=net_dict.get("labels", []),
176
+ )
177
+
178
+ @staticmethod
179
+ def create_wires_from_list(wire_data: List[Any]) -> List[Wire]:
180
+ """
181
+ Create list of Wire objects from list of dictionaries.
182
+
183
+ Args:
184
+ wire_data: List of wire dictionaries
185
+
186
+ Returns:
187
+ List of Wire objects
188
+ """
189
+ wires = []
190
+ for wire_dict in wire_data:
191
+ if isinstance(wire_dict, dict):
192
+ wires.append(ElementFactory.create_wire(wire_dict))
193
+ return wires
194
+
195
+ @staticmethod
196
+ def create_junctions_from_list(junction_data: List[Any]) -> List[Junction]:
197
+ """
198
+ Create list of Junction objects from list of dictionaries.
199
+
200
+ Args:
201
+ junction_data: List of junction dictionaries
202
+
203
+ Returns:
204
+ List of Junction objects
205
+ """
206
+ junctions = []
207
+ for junction_dict in junction_data:
208
+ if isinstance(junction_dict, dict):
209
+ junctions.append(ElementFactory.create_junction(junction_dict))
210
+ return junctions
211
+
212
+ @staticmethod
213
+ def create_texts_from_list(text_data: List[Any]) -> List[Text]:
214
+ """
215
+ Create list of Text objects from list of dictionaries.
216
+
217
+ Args:
218
+ text_data: List of text dictionaries
219
+
220
+ Returns:
221
+ List of Text objects
222
+ """
223
+ texts = []
224
+ for text_dict in text_data:
225
+ if isinstance(text_dict, dict):
226
+ texts.append(ElementFactory.create_text(text_dict))
227
+ return texts
228
+
229
+ @staticmethod
230
+ def create_labels_from_list(label_data: List[Any]) -> List[Label]:
231
+ """
232
+ Create list of Label objects from list of dictionaries.
233
+
234
+ Args:
235
+ label_data: List of label dictionaries
236
+
237
+ Returns:
238
+ List of Label objects
239
+ """
240
+ labels = []
241
+ for label_dict in label_data:
242
+ if isinstance(label_dict, dict):
243
+ labels.append(ElementFactory.create_label(label_dict))
244
+ return labels
245
+
246
+ @staticmethod
247
+ def create_no_connects_from_list(no_connect_data: List[Any]) -> List[NoConnect]:
248
+ """
249
+ Create list of NoConnect objects from list of dictionaries.
250
+
251
+ Args:
252
+ no_connect_data: List of no-connect dictionaries
253
+
254
+ Returns:
255
+ List of NoConnect objects
256
+ """
257
+ no_connects = []
258
+ for no_connect_dict in no_connect_data:
259
+ if isinstance(no_connect_dict, dict):
260
+ no_connects.append(ElementFactory.create_no_connect(no_connect_dict))
261
+ return no_connects
262
+
263
+ @staticmethod
264
+ def create_nets_from_list(net_data: List[Any]) -> List[Net]:
265
+ """
266
+ Create list of Net objects from list of dictionaries.
267
+
268
+ Args:
269
+ net_data: List of net dictionaries
270
+
271
+ Returns:
272
+ List of Net objects
273
+ """
274
+ nets = []
275
+ for net_dict in net_data:
276
+ if isinstance(net_dict, dict):
277
+ nets.append(ElementFactory.create_net(net_dict))
278
+ return nets
@@ -55,7 +55,9 @@ class ExactFormatter:
55
55
  self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
56
56
  self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
57
57
  self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
58
- self.rules["paper"] = FormatRule(inline=True) # No quotes for paper size (A4, A3, etc.)
58
+ self.rules["paper"] = FormatRule(
59
+ inline=True, quote_indices={1}
60
+ ) # Paper size should be quoted per KiCad format
59
61
 
60
62
  # Title block
61
63
  self.rules["title_block"] = FormatRule(inline=False)
@@ -123,6 +125,10 @@ class ExactFormatter:
123
125
  self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
124
126
  self.rules["hierarchical_label"] = FormatRule(inline=False, quote_indices={1})
125
127
 
128
+ # Text elements
129
+ self.rules["text"] = FormatRule(inline=False, quote_indices={1})
130
+ self.rules["text_box"] = FormatRule(inline=False, quote_indices={1})
131
+
126
132
  # Effects and text formatting
127
133
  self.rules["effects"] = FormatRule(inline=False)
128
134
  self.rules["font"] = FormatRule(inline=False)
@@ -146,6 +152,9 @@ class ExactFormatter:
146
152
  self.rules["embedded_fonts"] = FormatRule(inline=True)
147
153
  self.rules["page"] = FormatRule(inline=True, quote_indices={1})
148
154
 
155
+ # Image element
156
+ self.rules["image"] = FormatRule(inline=False, custom_handler=self._format_image)
157
+
149
158
  def format(self, data: Any) -> str:
150
159
  """
151
160
  Format S-expression data with exact KiCAD formatting.
@@ -189,7 +198,8 @@ class ExactFormatter:
189
198
  elif isinstance(element, str):
190
199
  # Quote strings that need quoting
191
200
  if self._needs_quoting(element):
192
- return f'"{element}"'
201
+ escaped = self._escape_string(element)
202
+ return f'"{escaped}"'
193
203
  return element
194
204
  elif isinstance(element, float):
195
205
  # Custom float formatting for KiCAD compatibility
@@ -273,6 +283,8 @@ class ExactFormatter:
273
283
  "junction",
274
284
  "label",
275
285
  "hierarchical_label",
286
+ "text",
287
+ "text_box",
276
288
  "polyline",
277
289
  "rectangle",
278
290
  ):
@@ -378,7 +390,8 @@ class ExactFormatter:
378
390
  result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
379
391
  else:
380
392
  if i in rule.quote_indices and isinstance(element, str):
381
- result += f' "{element}"'
393
+ escaped_element = self._escape_string(element)
394
+ result += f' "{escaped_element}"'
382
395
  else:
383
396
  result += f" {self._format_element(element, 0)}"
384
397
 
@@ -390,7 +403,8 @@ class ExactFormatter:
390
403
  indent = "\t" * indent_level
391
404
  next_indent = "\t" * (indent_level + 1)
392
405
 
393
- result = f"({lst[0]}"
406
+ tag = str(lst[0])
407
+ result = f"({tag}"
394
408
 
395
409
  for i, element in enumerate(lst[1:], 1):
396
410
  if isinstance(element, list):
@@ -419,9 +433,18 @@ class ExactFormatter:
419
433
  return True
420
434
 
421
435
  def _escape_string(self, text: str) -> str:
422
- """Escape quotes in string for S-expression formatting."""
423
- # Replace double quotes with escaped quotes
424
- return text.replace('"', '\\"')
436
+ """Escape special characters in string for S-expression formatting."""
437
+ # Escape backslashes first (must be done before other replacements)
438
+ text = text.replace('\\', '\\\\')
439
+ # Escape double quotes
440
+ text = text.replace('"', '\\"')
441
+ # Escape newlines (convert actual newlines to escaped representation)
442
+ text = text.replace('\n', '\\n')
443
+ # Escape carriage returns
444
+ text = text.replace('\r', '\\r')
445
+ # Escape tabs
446
+ text = text.replace('\t', '\\t')
447
+ return text
425
448
 
426
449
  def _needs_quoting(self, text: str) -> bool:
427
450
  """Check if string needs to be quoted."""
@@ -460,8 +483,8 @@ class ExactFormatter:
460
483
  for item in lst[1:]:
461
484
  if isinstance(item, list) and len(item) >= 1:
462
485
  tag = str(item[0])
463
- if tag in ["version", "generator", "generator_version"] and len(item) >= 2:
464
- if tag in ["generator", "generator_version"]:
486
+ if tag in ["version", "generator", "generator_version", "uuid"] and len(item) >= 2:
487
+ if tag in ["generator", "generator_version", "uuid"]:
465
488
  header_parts.append(f'({tag} "{item[1]}")')
466
489
  else:
467
490
  header_parts.append(f"({tag} {item[1]})")
@@ -510,6 +533,34 @@ class ExactFormatter:
510
533
  result += f"\n{indent})"
511
534
  return result
512
535
 
536
+ def _format_image(self, lst: List[Any], indent_level: int) -> str:
537
+ """Format image elements with base64 data split across lines."""
538
+ indent = "\t" * indent_level
539
+ next_indent = "\t" * (indent_level + 1)
540
+
541
+ result = f"({lst[0]}"
542
+
543
+ # Process each element
544
+ for element in lst[1:]:
545
+ if isinstance(element, list):
546
+ tag = str(element[0]) if element else ""
547
+ if tag == "data":
548
+ # Special handling for data element
549
+ # First chunk on same line as (data, rest on subsequent lines
550
+ if len(element) > 1:
551
+ result += f'\n{next_indent}({element[0]} "{element[1]}"'
552
+ for chunk in element[2:]:
553
+ result += f'\n{next_indent}\t"{chunk}"'
554
+ result += f"\n{next_indent})"
555
+ else:
556
+ result += f"\n{next_indent}({element[0]})"
557
+ else:
558
+ # Regular element formatting
559
+ result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
560
+
561
+ result += f"\n{indent})"
562
+ return result
563
+
513
564
 
514
565
  class CompactFormatter(ExactFormatter):
515
566
  """Compact formatter for minimal output size."""