kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.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.

Files changed (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  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/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,250 @@
1
+ """
2
+ Text and text box elements parser for KiCAD schematics.
3
+
4
+ Handles parsing and serialization of Text and text box elements.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import sexpdata
11
+
12
+ from ...core.config import config
13
+ from ..base import BaseElementParser
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class TextParser(BaseElementParser):
19
+ """Parser for Text and text box elements."""
20
+
21
+ def __init__(self):
22
+ """Initialize text parser."""
23
+ super().__init__("text")
24
+
25
+ def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
26
+ """Parse a text element."""
27
+ # Format: (text "text" (exclude_from_sim no) (at x y rotation) (effects ...) (uuid ...))
28
+ if len(item) < 2:
29
+ return None
30
+
31
+ text_data = {
32
+ "text": str(item[1]),
33
+ "exclude_from_sim": False,
34
+ "position": {"x": 0, "y": 0},
35
+ "rotation": 0,
36
+ "size": config.defaults.font_size,
37
+ "uuid": None,
38
+ }
39
+
40
+ for elem in item[2:]:
41
+ if not isinstance(elem, list):
42
+ continue
43
+
44
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
45
+
46
+ if elem_type == "exclude_from_sim":
47
+ if len(elem) >= 2:
48
+ text_data["exclude_from_sim"] = str(elem[1]) == "yes"
49
+ elif elem_type == "at":
50
+ if len(elem) >= 3:
51
+ text_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
52
+ if len(elem) >= 4:
53
+ text_data["rotation"] = float(elem[3])
54
+ elif elem_type == "effects":
55
+ for effect_elem in elem[1:]:
56
+ if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
57
+ for font_elem in effect_elem[1:]:
58
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
59
+ if len(font_elem) >= 2:
60
+ text_data["size"] = float(font_elem[1])
61
+ elif elem_type == "uuid":
62
+ text_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
63
+
64
+ return text_data
65
+
66
+
67
+ def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
68
+ """Parse a text_box element."""
69
+ # Format: (text_box "text" (exclude_from_sim no) (at x y rotation) (size w h) (margins ...) (stroke ...) (fill ...) (effects ...) (uuid ...))
70
+ if len(item) < 2:
71
+ return None
72
+
73
+ text_box_data = {
74
+ "text": str(item[1]),
75
+ "exclude_from_sim": False,
76
+ "position": {"x": 0, "y": 0},
77
+ "rotation": 0,
78
+ "size": {"width": 0, "height": 0},
79
+ "margins": (0.9525, 0.9525, 0.9525, 0.9525),
80
+ "stroke_width": 0,
81
+ "stroke_type": "solid",
82
+ "fill_type": "none",
83
+ "font_size": config.defaults.font_size,
84
+ "justify_horizontal": "left",
85
+ "justify_vertical": "top",
86
+ "uuid": None,
87
+ }
88
+
89
+ for elem in item[2:]:
90
+ if not isinstance(elem, list):
91
+ continue
92
+
93
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
94
+
95
+ if elem_type == "exclude_from_sim":
96
+ if len(elem) >= 2:
97
+ text_box_data["exclude_from_sim"] = str(elem[1]) == "yes"
98
+ elif elem_type == "at":
99
+ if len(elem) >= 3:
100
+ text_box_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
101
+ if len(elem) >= 4:
102
+ text_box_data["rotation"] = float(elem[3])
103
+ elif elem_type == "size":
104
+ if len(elem) >= 3:
105
+ text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
106
+ elif elem_type == "margins":
107
+ if len(elem) >= 5:
108
+ text_box_data["margins"] = (
109
+ float(elem[1]),
110
+ float(elem[2]),
111
+ float(elem[3]),
112
+ float(elem[4]),
113
+ )
114
+ elif elem_type == "stroke":
115
+ for stroke_elem in elem[1:]:
116
+ if isinstance(stroke_elem, list):
117
+ stroke_type = str(stroke_elem[0])
118
+ if stroke_type == "width" and len(stroke_elem) >= 2:
119
+ text_box_data["stroke_width"] = float(stroke_elem[1])
120
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
121
+ text_box_data["stroke_type"] = str(stroke_elem[1])
122
+ elif elem_type == "fill":
123
+ for fill_elem in elem[1:]:
124
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
125
+ text_box_data["fill_type"] = (
126
+ str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
127
+ )
128
+ elif elem_type == "effects":
129
+ for effect_elem in elem[1:]:
130
+ if isinstance(effect_elem, list):
131
+ effect_type = str(effect_elem[0])
132
+ if effect_type == "font":
133
+ for font_elem in effect_elem[1:]:
134
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
135
+ if len(font_elem) >= 2:
136
+ text_box_data["font_size"] = float(font_elem[1])
137
+ elif effect_type == "justify":
138
+ if len(effect_elem) >= 2:
139
+ text_box_data["justify_horizontal"] = str(effect_elem[1])
140
+ if len(effect_elem) >= 3:
141
+ text_box_data["justify_vertical"] = str(effect_elem[2])
142
+ elif elem_type == "uuid":
143
+ text_box_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
144
+
145
+ return text_box_data
146
+
147
+
148
+ def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
149
+ """Convert text element to S-expression."""
150
+ sexp = [sexpdata.Symbol("text"), text_data["text"]]
151
+
152
+ # Add exclude_from_sim
153
+ exclude_sim = text_data.get("exclude_from_sim", False)
154
+ sexp.append(
155
+ [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
156
+ )
157
+
158
+ # Add position
159
+ pos = text_data["position"]
160
+ x, y = pos["x"], pos["y"]
161
+ rotation = text_data.get("rotation", 0)
162
+
163
+ # Format coordinates properly
164
+ if isinstance(x, float) and x.is_integer():
165
+ x = int(x)
166
+ if isinstance(y, float) and y.is_integer():
167
+ y = int(y)
168
+
169
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
170
+
171
+ # Add effects (font properties)
172
+ size = text_data.get("size", config.defaults.font_size)
173
+ effects = [sexpdata.Symbol("effects")]
174
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
175
+ effects.append(font)
176
+ sexp.append(effects)
177
+
178
+ # Add UUID
179
+ if "uuid" in text_data:
180
+ sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
181
+
182
+ return sexp
183
+
184
+
185
+ def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
186
+ """Convert text box element to S-expression."""
187
+ sexp = [sexpdata.Symbol("text_box"), text_box_data["text"]]
188
+
189
+ # Add exclude_from_sim
190
+ exclude_sim = text_box_data.get("exclude_from_sim", False)
191
+ sexp.append(
192
+ [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
193
+ )
194
+
195
+ # Add position
196
+ pos = text_box_data["position"]
197
+ x, y = pos["x"], pos["y"]
198
+ rotation = text_box_data.get("rotation", 0)
199
+
200
+ # Format coordinates properly
201
+ if isinstance(x, float) and x.is_integer():
202
+ x = int(x)
203
+ if isinstance(y, float) and y.is_integer():
204
+ y = int(y)
205
+
206
+ sexp.append([sexpdata.Symbol("at"), x, y, rotation])
207
+
208
+ # Add size
209
+ size = text_box_data["size"]
210
+ w, h = size["width"], size["height"]
211
+ sexp.append([sexpdata.Symbol("size"), w, h])
212
+
213
+ # Add margins
214
+ margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
215
+ sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
216
+
217
+ # Add stroke
218
+ stroke_width = text_box_data.get("stroke_width", 0)
219
+ stroke_type = text_box_data.get("stroke_type", "solid")
220
+ stroke_sexp = [sexpdata.Symbol("stroke")]
221
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
222
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
223
+ sexp.append(stroke_sexp)
224
+
225
+ # Add fill
226
+ fill_type = text_box_data.get("fill_type", "none")
227
+ fill_sexp = [sexpdata.Symbol("fill")]
228
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
229
+ sexp.append(fill_sexp)
230
+
231
+ # Add effects (font properties and justification)
232
+ font_size = text_box_data.get("font_size", config.defaults.font_size)
233
+ justify_h = text_box_data.get("justify_horizontal", "left")
234
+ justify_v = text_box_data.get("justify_vertical", "top")
235
+
236
+ effects = [sexpdata.Symbol("effects")]
237
+ font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
238
+ effects.append(font)
239
+ effects.append(
240
+ [sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
241
+ )
242
+ sexp.append(effects)
243
+
244
+ # Add UUID
245
+ if "uuid" in text_box_data:
246
+ sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
247
+
248
+ return sexp
249
+
250
+
@@ -0,0 +1,242 @@
1
+ """
2
+ Wire and connection element parsers for KiCAD schematics.
3
+
4
+ Handles parsing and serialization of connection elements:
5
+ - Wire
6
+ - Junction
7
+ - No-connect
8
+ """
9
+
10
+ import logging
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ import sexpdata
14
+
15
+ from ...core.config import config
16
+ from ..base import BaseElementParser
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class WireParser(BaseElementParser):
22
+ """Parser for wire and connection elements."""
23
+
24
+ def __init__(self):
25
+ """Initialize wire parser."""
26
+ super().__init__("wire")
27
+
28
+ def _parse_wire(self, item: List[Any]) -> Optional[Dict[str, Any]]:
29
+ """Parse a wire definition."""
30
+ wire_data = {
31
+ "points": [],
32
+ "stroke_width": 0.0,
33
+ "stroke_type": config.defaults.stroke_type,
34
+ "uuid": None,
35
+ "wire_type": "wire", # Default to wire (vs bus)
36
+ }
37
+
38
+ for elem in item[1:]:
39
+ if not isinstance(elem, list):
40
+ continue
41
+
42
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
43
+
44
+ if elem_type == "pts":
45
+ # Parse points: (pts (xy x1 y1) (xy x2 y2) ...)
46
+ for pt in elem[1:]:
47
+ if isinstance(pt, list) and len(pt) >= 3:
48
+ if str(pt[0]) == "xy":
49
+ x, y = float(pt[1]), float(pt[2])
50
+ wire_data["points"].append({"x": x, "y": y})
51
+
52
+ elif elem_type == "stroke":
53
+ # Parse stroke: (stroke (width 0) (type default))
54
+ for stroke_elem in elem[1:]:
55
+ if isinstance(stroke_elem, list) and len(stroke_elem) >= 2:
56
+ stroke_type = str(stroke_elem[0])
57
+ if stroke_type == "width":
58
+ wire_data["stroke_width"] = float(stroke_elem[1])
59
+ elif stroke_type == "type":
60
+ wire_data["stroke_type"] = str(stroke_elem[1])
61
+
62
+ elif elem_type == "uuid":
63
+ wire_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
64
+
65
+ # Only return wire if it has at least 2 points
66
+ if len(wire_data["points"]) >= 2:
67
+ return wire_data
68
+ else:
69
+ logger.warning(f"Wire has insufficient points: {len(wire_data['points'])}")
70
+ return None
71
+
72
+
73
+ def _parse_junction(self, item: List[Any]) -> Optional[Dict[str, Any]]:
74
+ """Parse a junction definition."""
75
+ junction_data = {
76
+ "position": {"x": 0, "y": 0},
77
+ "diameter": 0,
78
+ "color": (0, 0, 0, 0),
79
+ "uuid": None,
80
+ }
81
+
82
+ for elem in item[1:]:
83
+ if not isinstance(elem, list):
84
+ continue
85
+
86
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
87
+
88
+ if elem_type == "at":
89
+ # Parse position: (at x y)
90
+ if len(elem) >= 3:
91
+ junction_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
92
+
93
+ elif elem_type == "diameter":
94
+ # Parse diameter: (diameter value)
95
+ if len(elem) >= 2:
96
+ junction_data["diameter"] = float(elem[1])
97
+
98
+ elif elem_type == "color":
99
+ # Parse color: (color r g b a)
100
+ if len(elem) >= 5:
101
+ junction_data["color"] = (
102
+ int(elem[1]),
103
+ int(elem[2]),
104
+ int(elem[3]),
105
+ int(elem[4]),
106
+ )
107
+
108
+ elif elem_type == "uuid":
109
+ junction_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
110
+
111
+ return junction_data
112
+
113
+
114
+ def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
115
+ """Parse a no_connect symbol."""
116
+ # Format: (no_connect (at x y) (uuid ...))
117
+ no_connect_data = {"position": {"x": 0, "y": 0}, "uuid": None}
118
+
119
+ for elem in item[1:]:
120
+ if not isinstance(elem, list):
121
+ continue
122
+
123
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
124
+
125
+ if elem_type == "at":
126
+ if len(elem) >= 3:
127
+ no_connect_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
128
+ elif elem_type == "uuid":
129
+ no_connect_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
130
+
131
+ return no_connect_data
132
+
133
+
134
+ def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
135
+ """Convert wire to S-expression."""
136
+ sexp = [sexpdata.Symbol("wire")]
137
+
138
+ # Add points (pts section)
139
+ points = wire_data.get("points", [])
140
+ if len(points) >= 2:
141
+ pts_sexp = [sexpdata.Symbol("pts")]
142
+ for point in points:
143
+ if isinstance(point, dict):
144
+ x, y = point["x"], point["y"]
145
+ elif isinstance(point, (list, tuple)) and len(point) >= 2:
146
+ x, y = point[0], point[1]
147
+ else:
148
+ # Assume it's a Point object
149
+ x, y = point.x, point.y
150
+
151
+ # Format coordinates properly (avoid unnecessary .0 for integers)
152
+ if isinstance(x, float) and x.is_integer():
153
+ x = int(x)
154
+ if isinstance(y, float) and y.is_integer():
155
+ y = int(y)
156
+
157
+ pts_sexp.append([sexpdata.Symbol("xy"), x, y])
158
+ sexp.append(pts_sexp)
159
+
160
+ # Add stroke information
161
+ stroke_width = wire_data.get("stroke_width", config.defaults.stroke_width)
162
+ stroke_type = wire_data.get("stroke_type", config.defaults.stroke_type)
163
+ stroke_sexp = [sexpdata.Symbol("stroke")]
164
+
165
+ # Format stroke width (use int for 0, preserve float for others)
166
+ if isinstance(stroke_width, float) and stroke_width == 0.0:
167
+ stroke_width = 0
168
+
169
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
170
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
171
+ sexp.append(stroke_sexp)
172
+
173
+ # Add UUID
174
+ if "uuid" in wire_data:
175
+ sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
176
+
177
+ return sexp
178
+
179
+
180
+ def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
181
+ """Convert junction to S-expression."""
182
+ sexp = [sexpdata.Symbol("junction")]
183
+
184
+ # Add position
185
+ pos = junction_data["position"]
186
+ if isinstance(pos, dict):
187
+ x, y = pos["x"], pos["y"]
188
+ elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
189
+ x, y = pos[0], pos[1]
190
+ else:
191
+ # Assume it's a Point object
192
+ x, y = pos.x, pos.y
193
+
194
+ # Format coordinates properly
195
+ if isinstance(x, float) and x.is_integer():
196
+ x = int(x)
197
+ if isinstance(y, float) and y.is_integer():
198
+ y = int(y)
199
+
200
+ sexp.append([sexpdata.Symbol("at"), x, y])
201
+
202
+ # Add diameter
203
+ diameter = junction_data.get("diameter", 0)
204
+ sexp.append([sexpdata.Symbol("diameter"), diameter])
205
+
206
+ # Add color (RGBA)
207
+ color = junction_data.get("color", (0, 0, 0, 0))
208
+ if isinstance(color, (list, tuple)) and len(color) >= 4:
209
+ sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
210
+ else:
211
+ sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
212
+
213
+ # Add UUID
214
+ if "uuid" in junction_data:
215
+ sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
216
+
217
+ return sexp
218
+
219
+
220
+ def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
221
+ """Convert no_connect to S-expression."""
222
+ sexp = [sexpdata.Symbol("no_connect")]
223
+
224
+ # Add position
225
+ pos = no_connect_data["position"]
226
+ x, y = pos["x"], pos["y"]
227
+
228
+ # Format coordinates properly
229
+ if isinstance(x, float) and x.is_integer():
230
+ x = int(x)
231
+ if isinstance(y, float) and y.is_integer():
232
+ y = int(y)
233
+
234
+ sexp.append([sexpdata.Symbol("at"), x, y])
235
+
236
+ # Add UUID
237
+ if "uuid" in no_connect_data:
238
+ sexp.append([sexpdata.Symbol("uuid"), no_connect_data["uuid"]])
239
+
240
+ return sexp
241
+
242
+
@@ -0,0 +1,80 @@
1
+ """
2
+ Utility functions for S-expression parsing.
3
+
4
+ Common helper functions used across multiple element parsers,
5
+ extracted from monolithic parser.py for reusability.
6
+ """
7
+
8
+ from typing import List
9
+
10
+
11
+ def color_to_rgba(color_name: str) -> List[float]:
12
+ """
13
+ Convert color name to RGBA values (0.0-1.0) for KiCAD compatibility.
14
+
15
+ Args:
16
+ color_name: Color name (e.g., "red", "blue", "green")
17
+
18
+ Returns:
19
+ List of 4 floats [R, G, B, A] in range 0.0-1.0
20
+
21
+ Example:
22
+ >>> color_to_rgba("red")
23
+ [1.0, 0.0, 0.0, 1.0]
24
+ >>> color_to_rgba("unknown")
25
+ [0.0, 0.0, 0.0, 1.0] # defaults to black
26
+ """
27
+ # Basic color mapping for common colors (0.0-1.0 range)
28
+ color_map = {
29
+ "red": [1.0, 0.0, 0.0, 1.0],
30
+ "blue": [0.0, 0.0, 1.0, 1.0],
31
+ "green": [0.0, 1.0, 0.0, 1.0],
32
+ "yellow": [1.0, 1.0, 0.0, 1.0],
33
+ "magenta": [1.0, 0.0, 1.0, 1.0],
34
+ "cyan": [0.0, 1.0, 1.0, 1.0],
35
+ "black": [0.0, 0.0, 0.0, 1.0],
36
+ "white": [1.0, 1.0, 1.0, 1.0],
37
+ "gray": [0.5, 0.5, 0.5, 1.0],
38
+ "grey": [0.5, 0.5, 0.5, 1.0],
39
+ "orange": [1.0, 0.5, 0.0, 1.0],
40
+ "purple": [0.5, 0.0, 0.5, 1.0],
41
+ }
42
+
43
+ # Return RGBA values, default to black if color not found
44
+ return color_map.get(color_name.lower(), [0.0, 0.0, 0.0, 1.0])
45
+
46
+
47
+ def color_to_rgb255(color_name: str) -> List[int]:
48
+ """
49
+ Convert color name to RGB values (0-255) for KiCAD rectangle graphics.
50
+
51
+ Args:
52
+ color_name: Color name (e.g., "red", "blue", "green")
53
+
54
+ Returns:
55
+ List of 3 integers [R, G, B] in range 0-255
56
+
57
+ Example:
58
+ >>> color_to_rgb255("red")
59
+ [255, 0, 0]
60
+ >>> color_to_rgb255("unknown")
61
+ [0, 0, 0] # defaults to black
62
+ """
63
+ # Basic color mapping for common colors (0-255 range)
64
+ color_map = {
65
+ "red": [255, 0, 0],
66
+ "blue": [0, 0, 255],
67
+ "green": [0, 255, 0],
68
+ "yellow": [255, 255, 0],
69
+ "magenta": [255, 0, 255],
70
+ "cyan": [0, 255, 255],
71
+ "black": [0, 0, 0],
72
+ "white": [255, 255, 255],
73
+ "gray": [128, 128, 128],
74
+ "grey": [128, 128, 128],
75
+ "orange": [255, 128, 0],
76
+ "purple": [128, 0, 128],
77
+ }
78
+
79
+ # Return RGB values, default to black if color not found
80
+ return color_map.get(color_name.lower(), [0, 0, 0])
@@ -0,0 +1,25 @@
1
+ """
2
+ Electrical Rules Check (ERC) validation module.
3
+
4
+ Provides comprehensive electrical validation for KiCAD schematics.
5
+ """
6
+
7
+ from kicad_sch_api.validation.erc import ElectricalRulesChecker
8
+ from kicad_sch_api.validation.erc_models import (
9
+ ERCConfig,
10
+ ERCResult,
11
+ ERCViolation,
12
+ )
13
+ from kicad_sch_api.validation.pin_matrix import (
14
+ PinConflictMatrix,
15
+ PinSeverity,
16
+ )
17
+
18
+ __all__ = [
19
+ "ERCViolation",
20
+ "ERCResult",
21
+ "ERCConfig",
22
+ "PinConflictMatrix",
23
+ "PinSeverity",
24
+ "ElectricalRulesChecker",
25
+ ]