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.
- kicad_sch_api/__init__.py +68 -3
- 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/kicad_to_python.py +169 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/collections/__init__.py +36 -0
- kicad_sch_api/collections/base.py +604 -0
- kicad_sch_api/collections/components.py +1623 -0
- kicad_sch_api/collections/junctions.py +206 -0
- kicad_sch_api/collections/labels.py +508 -0
- kicad_sch_api/collections/wires.py +292 -0
- kicad_sch_api/core/__init__.py +37 -2
- 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 +34 -7
- kicad_sch_api/core/components.py +213 -52
- kicad_sch_api/core/config.py +110 -15
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +278 -0
- kicad_sch_api/core/formatter.py +60 -9
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +324 -0
- kicad_sch_api/core/managers/__init__.py +30 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +246 -0
- kicad_sch_api/core/managers/format_sync.py +502 -0
- kicad_sch_api/core/managers/graphics.py +580 -0
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +271 -0
- kicad_sch_api/core/managers/sheet.py +492 -0
- kicad_sch_api/core/managers/text_elements.py +537 -0
- kicad_sch_api/core/managers/validation.py +476 -0
- kicad_sch_api/core/managers/wire.py +410 -0
- kicad_sch_api/core/nets.py +305 -0
- kicad_sch_api/core/no_connects.py +252 -0
- kicad_sch_api/core/parser.py +194 -970
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +1328 -1079
- kicad_sch_api/core/texts.py +316 -0
- kicad_sch_api/core/types.py +159 -23
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +38 -0
- kicad_sch_api/geometry/font_metrics.py +22 -0
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/geometry/symbol_bbox.py +608 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- 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 +216 -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 +485 -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/registry.py +155 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- 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/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -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-0.3.0.dist-info/METADATA +0 -483
- kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
- kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +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,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser registry for managing S-expression element parsers.
|
|
3
|
+
|
|
4
|
+
Provides a central registry for all element parsers and handles
|
|
5
|
+
dispatching parsing requests to the appropriate parser.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..interfaces.parser import IElementParser
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ElementParserRegistry:
|
|
17
|
+
"""
|
|
18
|
+
Central registry for all S-expression element parsers.
|
|
19
|
+
|
|
20
|
+
This class manages the registration of element-specific parsers
|
|
21
|
+
and provides a unified interface for parsing any S-expression element.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
"""Initialize the parser registry."""
|
|
26
|
+
self._parsers: Dict[str, IElementParser] = {}
|
|
27
|
+
self._fallback_parser: Optional[IElementParser] = None
|
|
28
|
+
self._logger = logger.getChild(self.__class__.__name__)
|
|
29
|
+
|
|
30
|
+
def register(self, element_type: str, parser: IElementParser) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Register a parser for a specific element type.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
element_type: The S-expression element type (e.g., "symbol", "wire")
|
|
36
|
+
parser: Parser instance that handles this element type
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
ValueError: If element_type is already registered
|
|
40
|
+
"""
|
|
41
|
+
if element_type in self._parsers:
|
|
42
|
+
self._logger.warning(f"Overriding existing parser for element type: {element_type}")
|
|
43
|
+
|
|
44
|
+
self._parsers[element_type] = parser
|
|
45
|
+
self._logger.debug(f"Registered parser for element type: {element_type}")
|
|
46
|
+
|
|
47
|
+
def unregister(self, element_type: str) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Unregister a parser for a specific element type.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
element_type: The element type to unregister
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if parser was removed, False if not found
|
|
56
|
+
"""
|
|
57
|
+
if element_type in self._parsers:
|
|
58
|
+
del self._parsers[element_type]
|
|
59
|
+
self._logger.debug(f"Unregistered parser for element type: {element_type}")
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def set_fallback_parser(self, parser: IElementParser) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Set a fallback parser for unknown element types.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
parser: Parser to use when no specific parser is registered
|
|
69
|
+
"""
|
|
70
|
+
self._fallback_parser = parser
|
|
71
|
+
self._logger.debug("Set fallback parser")
|
|
72
|
+
|
|
73
|
+
def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
74
|
+
"""
|
|
75
|
+
Parse an S-expression element using the appropriate registered parser.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
element: S-expression element to parse
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Parsed element data or None if parsing failed
|
|
82
|
+
"""
|
|
83
|
+
if not element or not isinstance(element, list):
|
|
84
|
+
self._logger.debug("Invalid element: not a list or empty")
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
element_type = element[0] if element else None
|
|
88
|
+
# Convert sexpdata.Symbol to string for lookup
|
|
89
|
+
element_type_str = str(element_type) if element_type else None
|
|
90
|
+
if not element_type_str:
|
|
91
|
+
self._logger.debug(f"Invalid element type: {element_type}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Try specific parser first
|
|
95
|
+
parser = self._parsers.get(element_type_str)
|
|
96
|
+
if parser:
|
|
97
|
+
self._logger.debug(f"Using registered parser for element type: {element_type_str}")
|
|
98
|
+
return parser.parse(element)
|
|
99
|
+
|
|
100
|
+
# Try fallback parser
|
|
101
|
+
if self._fallback_parser:
|
|
102
|
+
self._logger.debug(
|
|
103
|
+
f"Using fallback parser for unknown element type: {element_type_str}"
|
|
104
|
+
)
|
|
105
|
+
return self._fallback_parser.parse(element)
|
|
106
|
+
|
|
107
|
+
# No parser available
|
|
108
|
+
self._logger.warning(f"No parser available for element type: {element_type_str}")
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def parse_elements(self, elements: List[List[Any]]) -> List[Dict[str, Any]]:
|
|
112
|
+
"""
|
|
113
|
+
Parse multiple S-expression elements.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
elements: List of S-expression elements to parse
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of parsed element data (excluding failed parses)
|
|
120
|
+
"""
|
|
121
|
+
results = []
|
|
122
|
+
for element in elements:
|
|
123
|
+
parsed = self.parse_element(element)
|
|
124
|
+
if parsed is not None:
|
|
125
|
+
results.append(parsed)
|
|
126
|
+
|
|
127
|
+
self._logger.debug(f"Parsed {len(results)} of {len(elements)} elements")
|
|
128
|
+
return results
|
|
129
|
+
|
|
130
|
+
def get_registered_types(self) -> List[str]:
|
|
131
|
+
"""
|
|
132
|
+
Get list of all registered element types.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of registered element type names
|
|
136
|
+
"""
|
|
137
|
+
return list(self._parsers.keys())
|
|
138
|
+
|
|
139
|
+
def has_parser(self, element_type: str) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Check if a parser is registered for the given element type.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
element_type: Element type to check
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if parser is registered
|
|
148
|
+
"""
|
|
149
|
+
return element_type in self._parsers
|
|
150
|
+
|
|
151
|
+
def clear(self) -> None:
|
|
152
|
+
"""Clear all registered parsers."""
|
|
153
|
+
self._parsers.clear()
|
|
154
|
+
self._fallback_parser = None
|
|
155
|
+
self._logger.debug("Cleared all registered parsers")
|