kicad-sch-api 0.3.5__py3-none-any.whl → 0.4.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 (81) 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/collections/__init__.py +2 -2
  10. kicad_sch_api/collections/base.py +5 -7
  11. kicad_sch_api/collections/components.py +24 -12
  12. kicad_sch_api/collections/junctions.py +31 -43
  13. kicad_sch_api/collections/labels.py +19 -27
  14. kicad_sch_api/collections/wires.py +17 -18
  15. kicad_sch_api/core/collections/__init__.py +5 -0
  16. kicad_sch_api/core/collections/base.py +248 -0
  17. kicad_sch_api/core/component_bounds.py +5 -0
  18. kicad_sch_api/core/components.py +67 -45
  19. kicad_sch_api/core/config.py +85 -3
  20. kicad_sch_api/core/factories/__init__.py +5 -0
  21. kicad_sch_api/core/factories/element_factory.py +276 -0
  22. kicad_sch_api/core/formatter.py +3 -1
  23. kicad_sch_api/core/junctions.py +26 -75
  24. kicad_sch_api/core/labels.py +29 -53
  25. kicad_sch_api/core/managers/__init__.py +26 -0
  26. kicad_sch_api/core/managers/file_io.py +244 -0
  27. kicad_sch_api/core/managers/format_sync.py +501 -0
  28. kicad_sch_api/core/managers/graphics.py +579 -0
  29. kicad_sch_api/core/managers/metadata.py +269 -0
  30. kicad_sch_api/core/managers/sheet.py +454 -0
  31. kicad_sch_api/core/managers/text_elements.py +536 -0
  32. kicad_sch_api/core/managers/validation.py +475 -0
  33. kicad_sch_api/core/managers/wire.py +352 -0
  34. kicad_sch_api/core/nets.py +38 -43
  35. kicad_sch_api/core/no_connects.py +33 -55
  36. kicad_sch_api/core/parser.py +75 -1731
  37. kicad_sch_api/core/schematic.py +951 -1192
  38. kicad_sch_api/core/texts.py +28 -55
  39. kicad_sch_api/core/types.py +60 -22
  40. kicad_sch_api/core/wires.py +27 -75
  41. kicad_sch_api/geometry/font_metrics.py +3 -1
  42. kicad_sch_api/geometry/symbol_bbox.py +40 -21
  43. kicad_sch_api/interfaces/__init__.py +1 -1
  44. kicad_sch_api/interfaces/parser.py +1 -1
  45. kicad_sch_api/interfaces/repository.py +1 -1
  46. kicad_sch_api/interfaces/resolver.py +1 -1
  47. kicad_sch_api/parsers/__init__.py +2 -2
  48. kicad_sch_api/parsers/base.py +7 -10
  49. kicad_sch_api/parsers/elements/__init__.py +22 -0
  50. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  51. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  52. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  53. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  54. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  55. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  56. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  57. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  58. kicad_sch_api/parsers/registry.py +4 -2
  59. kicad_sch_api/parsers/utils.py +80 -0
  60. kicad_sch_api/symbols/__init__.py +1 -1
  61. kicad_sch_api/symbols/cache.py +9 -12
  62. kicad_sch_api/symbols/resolver.py +20 -26
  63. kicad_sch_api/symbols/validators.py +188 -137
  64. kicad_sch_api/validation/__init__.py +25 -0
  65. kicad_sch_api/validation/erc.py +171 -0
  66. kicad_sch_api/validation/erc_models.py +203 -0
  67. kicad_sch_api/validation/pin_matrix.py +243 -0
  68. kicad_sch_api/validation/validators.py +391 -0
  69. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
  70. kicad_sch_api-0.4.1.dist-info/RECORD +87 -0
  71. kicad_sch_api/core/manhattan_routing.py +0 -430
  72. kicad_sch_api/core/simple_manhattan.py +0 -228
  73. kicad_sch_api/core/wire_routing.py +0 -380
  74. kicad_sch_api/parsers/label_parser.py +0 -254
  75. kicad_sch_api/parsers/symbol_parser.py +0 -227
  76. kicad_sch_api/parsers/wire_parser.py +0 -99
  77. kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
  78. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
  79. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
  80. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
  81. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.1.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
+
@@ -99,7 +99,9 @@ class ElementParserRegistry:
99
99
 
100
100
  # Try fallback parser
101
101
  if self._fallback_parser:
102
- self._logger.debug(f"Using fallback parser for unknown element type: {element_type_str}")
102
+ self._logger.debug(
103
+ f"Using fallback parser for unknown element type: {element_type_str}"
104
+ )
103
105
  return self._fallback_parser.parse(element)
104
106
 
105
107
  # No parser available
@@ -150,4 +152,4 @@ class ElementParserRegistry:
150
152
  """Clear all registered parsers."""
151
153
  self._parsers.clear()
152
154
  self._fallback_parser = None
153
- self._logger.debug("Cleared all registered parsers")
155
+ self._logger.debug("Cleared all registered parsers")
@@ -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])
@@ -15,4 +15,4 @@ __all__ = [
15
15
  "SymbolCache",
16
16
  "SymbolResolver",
17
17
  "SymbolValidator",
18
- ]
18
+ ]
@@ -17,7 +17,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
17
17
 
18
18
  import sexpdata
19
19
 
20
- from ..library.cache import SymbolDefinition, LibraryStats
20
+ from ..library.cache import LibraryStats, SymbolDefinition
21
21
  from ..utils.validation import ValidationError
22
22
 
23
23
  logger = logging.getLogger(__name__)
@@ -199,7 +199,7 @@ class SymbolCache(ISymbolCache):
199
199
  library_path=library_path,
200
200
  file_size=stat.st_size,
201
201
  last_modified=stat.st_mtime,
202
- symbol_count=0 # Will be updated when library is loaded
202
+ symbol_count=0, # Will be updated when library is loaded
203
203
  )
204
204
 
205
205
  logger.info(f"Added library: {library_name} ({library_path})")
@@ -257,10 +257,10 @@ class SymbolCache(ISymbolCache):
257
257
  name: {
258
258
  "file_size": stats.file_size,
259
259
  "symbols_count": stats.symbols_count,
260
- "last_loaded": stats.last_loaded
260
+ "last_loaded": stats.last_loaded,
261
261
  }
262
262
  for name, stats in self._lib_stats.items()
263
- }
263
+ },
264
264
  }
265
265
 
266
266
  # Private methods for implementation details
@@ -316,10 +316,7 @@ class SymbolCache(ISymbolCache):
316
316
  return None
317
317
 
318
318
  def _create_symbol_definition(
319
- self,
320
- symbol_data: List,
321
- lib_id: str,
322
- library_name: str
319
+ self, symbol_data: List, lib_id: str, library_name: str
323
320
  ) -> SymbolDefinition:
324
321
  """Create SymbolDefinition from parsed symbol data."""
325
322
  symbol_name = str(symbol_data[1]).strip('"')
@@ -345,7 +342,7 @@ class SymbolCache(ISymbolCache):
345
342
  power_symbol=properties.get("power_symbol", False),
346
343
  graphic_elements=graphic_elements,
347
344
  raw_kicad_data=symbol_data,
348
- extends=extends # Store extends information for resolver
345
+ extends=extends, # Store extends information for resolver
349
346
  )
350
347
 
351
348
  def _extract_symbol_properties(self, symbol_data: List) -> Dict[str, Any]:
@@ -356,7 +353,7 @@ class SymbolCache(ISymbolCache):
356
353
  "keywords": "",
357
354
  "datasheet": "",
358
355
  "units": 1,
359
- "power_symbol": False
356
+ "power_symbol": False,
360
357
  }
361
358
 
362
359
  for item in symbol_data[1:]:
@@ -446,7 +443,7 @@ class SymbolCache(ISymbolCache):
446
443
  index_data = {
447
444
  "symbol_index": self._symbol_index,
448
445
  "library_paths": [str(path) for path in self._library_paths],
449
- "created": time.time()
446
+ "created": time.time(),
450
447
  }
451
448
 
452
449
  with open(self._index_file, "w") as f:
@@ -467,4 +464,4 @@ class SymbolCache(ISymbolCache):
467
464
  if str(item[0]) == "symbol" and str(item[1]) == symbol_name:
468
465
  return item
469
466
 
470
- return None
467
+ return None