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.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +5 -0
- kicad_sch_api/core/components.py +142 -47
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +22 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +28 -52
- kicad_sch_api/core/managers/file_io.py +3 -2
- kicad_sch_api/core/managers/metadata.py +6 -5
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +7 -1
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +29 -53
- kicad_sch_api/core/parser.py +75 -1765
- kicad_sch_api/core/schematic.py +211 -148
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +59 -18
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +194 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -222
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
]
|