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,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component symbol elements parser for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
Handles parsing and serialization of Component symbol elements.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import sexpdata
|
|
12
|
+
|
|
13
|
+
from ...core.parsing_utils import parse_bool_property
|
|
14
|
+
from ...core.types import Point
|
|
15
|
+
from ..base import BaseElementParser
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SymbolParser(BaseElementParser):
|
|
21
|
+
"""Parser for Component symbol elements."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize symbol parser."""
|
|
25
|
+
super().__init__("symbol")
|
|
26
|
+
|
|
27
|
+
def _parse_symbol(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
28
|
+
"""Parse a symbol (component) definition."""
|
|
29
|
+
try:
|
|
30
|
+
symbol_data = {
|
|
31
|
+
"lib_id": None,
|
|
32
|
+
"position": Point(0, 0),
|
|
33
|
+
"rotation": 0,
|
|
34
|
+
"uuid": None,
|
|
35
|
+
"reference": None,
|
|
36
|
+
"value": None,
|
|
37
|
+
"footprint": None,
|
|
38
|
+
"properties": {},
|
|
39
|
+
"pins": [],
|
|
40
|
+
"in_bom": True,
|
|
41
|
+
"on_board": True,
|
|
42
|
+
"instances": [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for sub_item in item[1:]:
|
|
46
|
+
if not isinstance(sub_item, list) or len(sub_item) == 0:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
element_type = (
|
|
50
|
+
str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if element_type == "lib_id":
|
|
54
|
+
symbol_data["lib_id"] = sub_item[1] if len(sub_item) > 1 else None
|
|
55
|
+
elif element_type == "at":
|
|
56
|
+
if len(sub_item) >= 3:
|
|
57
|
+
symbol_data["position"] = Point(float(sub_item[1]), float(sub_item[2]))
|
|
58
|
+
if len(sub_item) > 3:
|
|
59
|
+
symbol_data["rotation"] = float(sub_item[3])
|
|
60
|
+
elif element_type == "uuid":
|
|
61
|
+
symbol_data["uuid"] = sub_item[1] if len(sub_item) > 1 else None
|
|
62
|
+
elif element_type == "property":
|
|
63
|
+
prop_data = self._parse_property(sub_item)
|
|
64
|
+
if prop_data:
|
|
65
|
+
prop_name = prop_data.get("name")
|
|
66
|
+
|
|
67
|
+
# Store original S-expression for format preservation
|
|
68
|
+
sexp_key = f"__sexp_{prop_name}"
|
|
69
|
+
symbol_data["properties"][sexp_key] = sub_item
|
|
70
|
+
|
|
71
|
+
if prop_name == "Reference":
|
|
72
|
+
symbol_data["reference"] = prop_data.get("value")
|
|
73
|
+
elif prop_name == "Value":
|
|
74
|
+
symbol_data["value"] = prop_data.get("value")
|
|
75
|
+
elif prop_name == "Footprint":
|
|
76
|
+
symbol_data["footprint"] = prop_data.get("value")
|
|
77
|
+
else:
|
|
78
|
+
# Unescape quotes in property values when loading
|
|
79
|
+
prop_value = prop_data.get("value")
|
|
80
|
+
if prop_value:
|
|
81
|
+
prop_value = str(prop_value).replace('\\"', '"')
|
|
82
|
+
symbol_data["properties"][prop_name] = prop_value
|
|
83
|
+
elif element_type == "in_bom":
|
|
84
|
+
symbol_data["in_bom"] = parse_bool_property(
|
|
85
|
+
sub_item[1] if len(sub_item) > 1 else None,
|
|
86
|
+
default=True
|
|
87
|
+
)
|
|
88
|
+
elif element_type == "on_board":
|
|
89
|
+
symbol_data["on_board"] = parse_bool_property(
|
|
90
|
+
sub_item[1] if len(sub_item) > 1 else None,
|
|
91
|
+
default=True
|
|
92
|
+
)
|
|
93
|
+
elif element_type == "instances":
|
|
94
|
+
# Parse instances section
|
|
95
|
+
instances = self._parse_instances(sub_item)
|
|
96
|
+
if instances:
|
|
97
|
+
symbol_data["instances"] = instances
|
|
98
|
+
|
|
99
|
+
return symbol_data
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.warning(f"Error parsing symbol: {e}")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _parse_property(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
107
|
+
"""Parse a property definition."""
|
|
108
|
+
if len(item) < 3:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"name": item[1] if len(item) > 1 else None,
|
|
113
|
+
"value": item[2] if len(item) > 2 else None,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def _parse_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
|
|
117
|
+
"""
|
|
118
|
+
Parse instances section from S-expression.
|
|
119
|
+
|
|
120
|
+
Format:
|
|
121
|
+
(instances
|
|
122
|
+
(project "project_name"
|
|
123
|
+
(path "/root_uuid/sheet_uuid"
|
|
124
|
+
(reference "R1")
|
|
125
|
+
(unit 1))))
|
|
126
|
+
"""
|
|
127
|
+
from ...core.types import SymbolInstance
|
|
128
|
+
|
|
129
|
+
instances = []
|
|
130
|
+
|
|
131
|
+
for sub_item in item[1:]:
|
|
132
|
+
if not isinstance(sub_item, list) or len(sub_item) == 0:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
element_type = str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
|
|
136
|
+
|
|
137
|
+
if element_type == "project":
|
|
138
|
+
# Parse project instance
|
|
139
|
+
project = sub_item[1] if len(sub_item) > 1 else None
|
|
140
|
+
|
|
141
|
+
# Find path section within project
|
|
142
|
+
for project_sub in sub_item[2:]:
|
|
143
|
+
if not isinstance(project_sub, list) or len(project_sub) == 0:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
path_type = str(project_sub[0]) if isinstance(project_sub[0], sexpdata.Symbol) else None
|
|
147
|
+
|
|
148
|
+
if path_type == "path":
|
|
149
|
+
# Extract path value
|
|
150
|
+
path = project_sub[1] if len(project_sub) > 1 else "/"
|
|
151
|
+
reference = None
|
|
152
|
+
unit = 1
|
|
153
|
+
|
|
154
|
+
# Parse reference and unit from path subsections
|
|
155
|
+
for path_sub in project_sub[2:]:
|
|
156
|
+
if not isinstance(path_sub, list) or len(path_sub) == 0:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
path_sub_type = str(path_sub[0]) if isinstance(path_sub[0], sexpdata.Symbol) else None
|
|
160
|
+
|
|
161
|
+
if path_sub_type == "reference":
|
|
162
|
+
reference = path_sub[1] if len(path_sub) > 1 else None
|
|
163
|
+
elif path_sub_type == "unit":
|
|
164
|
+
unit = int(path_sub[1]) if len(path_sub) > 1 else 1
|
|
165
|
+
|
|
166
|
+
# Create instance
|
|
167
|
+
if path and reference:
|
|
168
|
+
instance = SymbolInstance(
|
|
169
|
+
path=path,
|
|
170
|
+
reference=reference,
|
|
171
|
+
unit=unit
|
|
172
|
+
)
|
|
173
|
+
instances.append(instance)
|
|
174
|
+
|
|
175
|
+
return instances
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
|
|
179
|
+
"""Convert symbol to S-expression."""
|
|
180
|
+
sexp = [sexpdata.Symbol("symbol")]
|
|
181
|
+
|
|
182
|
+
if symbol_data.get("lib_id"):
|
|
183
|
+
sexp.append([sexpdata.Symbol("lib_id"), symbol_data["lib_id"]])
|
|
184
|
+
|
|
185
|
+
# Add position and rotation (preserve original format)
|
|
186
|
+
pos = symbol_data.get("position", Point(0, 0))
|
|
187
|
+
rotation = symbol_data.get("rotation", 0)
|
|
188
|
+
# Format numbers as integers if they are whole numbers
|
|
189
|
+
x = int(pos.x) if pos.x == int(pos.x) else pos.x
|
|
190
|
+
y = int(pos.y) if pos.y == int(pos.y) else pos.y
|
|
191
|
+
r = int(rotation) if rotation == int(rotation) else rotation
|
|
192
|
+
# Always include rotation for format consistency with KiCAD
|
|
193
|
+
sexp.append([sexpdata.Symbol("at"), x, y, r])
|
|
194
|
+
|
|
195
|
+
# Add unit (required by KiCAD)
|
|
196
|
+
unit = symbol_data.get("unit", 1)
|
|
197
|
+
sexp.append([sexpdata.Symbol("unit"), unit])
|
|
198
|
+
|
|
199
|
+
# Add simulation and board settings (required by KiCAD)
|
|
200
|
+
sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
|
|
201
|
+
sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
|
|
202
|
+
sexp.append(
|
|
203
|
+
[sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
|
|
204
|
+
)
|
|
205
|
+
sexp.append([sexpdata.Symbol("dnp"), "no"])
|
|
206
|
+
sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
|
|
207
|
+
|
|
208
|
+
if symbol_data.get("uuid"):
|
|
209
|
+
sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
|
|
210
|
+
|
|
211
|
+
# Add properties with proper positioning and effects
|
|
212
|
+
lib_id = symbol_data.get("lib_id", "")
|
|
213
|
+
is_power_symbol = "power:" in lib_id
|
|
214
|
+
rotation = symbol_data.get("rotation", 0)
|
|
215
|
+
|
|
216
|
+
if symbol_data.get("reference"):
|
|
217
|
+
# Check for preserved S-expression
|
|
218
|
+
preserved_ref = symbol_data.get("properties", {}).get("__sexp_Reference")
|
|
219
|
+
if preserved_ref:
|
|
220
|
+
# Use preserved format but update the value
|
|
221
|
+
ref_prop = list(preserved_ref)
|
|
222
|
+
if len(ref_prop) >= 3:
|
|
223
|
+
ref_prop[2] = symbol_data["reference"]
|
|
224
|
+
sexp.append(ref_prop)
|
|
225
|
+
else:
|
|
226
|
+
# No preserved format - create new (for newly added components)
|
|
227
|
+
ref_hide = is_power_symbol
|
|
228
|
+
ref_prop = self._create_property_with_positioning(
|
|
229
|
+
"Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide, rotation=rotation
|
|
230
|
+
)
|
|
231
|
+
sexp.append(ref_prop)
|
|
232
|
+
|
|
233
|
+
if symbol_data.get("value"):
|
|
234
|
+
# Check for preserved S-expression
|
|
235
|
+
preserved_val = symbol_data.get("properties", {}).get("__sexp_Value")
|
|
236
|
+
if preserved_val:
|
|
237
|
+
# Use preserved format but update the value
|
|
238
|
+
val_prop = list(preserved_val)
|
|
239
|
+
if len(val_prop) >= 3:
|
|
240
|
+
val_prop[2] = symbol_data["value"]
|
|
241
|
+
sexp.append(val_prop)
|
|
242
|
+
else:
|
|
243
|
+
# No preserved format - create new (for newly added components)
|
|
244
|
+
if is_power_symbol:
|
|
245
|
+
val_prop = self._create_power_symbol_value_property(
|
|
246
|
+
symbol_data["value"], pos, lib_id, rotation
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
val_prop = self._create_property_with_positioning(
|
|
250
|
+
"Value", symbol_data["value"], pos, 1, "left", rotation=rotation
|
|
251
|
+
)
|
|
252
|
+
sexp.append(val_prop)
|
|
253
|
+
|
|
254
|
+
footprint = symbol_data.get("footprint")
|
|
255
|
+
if footprint is not None: # Include empty strings but not None
|
|
256
|
+
# Check for preserved S-expression
|
|
257
|
+
preserved_fp = symbol_data.get("properties", {}).get("__sexp_Footprint")
|
|
258
|
+
if preserved_fp:
|
|
259
|
+
# Use preserved format but update the value
|
|
260
|
+
fp_prop = list(preserved_fp)
|
|
261
|
+
if len(fp_prop) >= 3:
|
|
262
|
+
fp_prop[2] = footprint
|
|
263
|
+
sexp.append(fp_prop)
|
|
264
|
+
else:
|
|
265
|
+
# No preserved format - create new (for newly added components)
|
|
266
|
+
fp_prop = self._create_property_with_positioning(
|
|
267
|
+
"Footprint", footprint, pos, 2, "left", hide=True
|
|
268
|
+
)
|
|
269
|
+
sexp.append(fp_prop)
|
|
270
|
+
|
|
271
|
+
for prop_name, prop_value in symbol_data.get("properties", {}).items():
|
|
272
|
+
# Skip internal preservation keys
|
|
273
|
+
if prop_name.startswith("__sexp_"):
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Check if we have a preserved S-expression for this custom property
|
|
277
|
+
preserved_prop = symbol_data.get("properties", {}).get(f"__sexp_{prop_name}")
|
|
278
|
+
if preserved_prop:
|
|
279
|
+
# Use preserved format but update the value
|
|
280
|
+
prop = list(preserved_prop)
|
|
281
|
+
if len(prop) >= 3:
|
|
282
|
+
# Re-escape quotes when saving
|
|
283
|
+
escaped_value = str(prop_value).replace('"', '\\"')
|
|
284
|
+
prop[2] = escaped_value
|
|
285
|
+
sexp.append(prop)
|
|
286
|
+
else:
|
|
287
|
+
# No preserved format - create new (for newly added properties)
|
|
288
|
+
escaped_value = str(prop_value).replace('"', '\\"')
|
|
289
|
+
prop = self._create_property_with_positioning(
|
|
290
|
+
prop_name, escaped_value, pos, 3, "left", hide=True
|
|
291
|
+
)
|
|
292
|
+
sexp.append(prop)
|
|
293
|
+
|
|
294
|
+
# Add pin UUID assignments (required by KiCAD)
|
|
295
|
+
for pin in symbol_data.get("pins", []):
|
|
296
|
+
pin_uuid = str(uuid.uuid4())
|
|
297
|
+
# Ensure pin number is a string for proper quoting
|
|
298
|
+
pin_number = str(pin.number)
|
|
299
|
+
sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
|
|
300
|
+
|
|
301
|
+
# Add instances section (required by KiCAD)
|
|
302
|
+
from ...core.config import config
|
|
303
|
+
|
|
304
|
+
# HIERARCHICAL FIX: Check if user explicitly set instances
|
|
305
|
+
# If so, preserve them exactly as-is (don't generate!)
|
|
306
|
+
user_instances = symbol_data.get("instances")
|
|
307
|
+
if user_instances:
|
|
308
|
+
logger.debug(f"🔍 HIERARCHICAL FIX: Component {symbol_data.get('reference')} has {len(user_instances)} user-set instance(s)")
|
|
309
|
+
# Build instances sexp from user data
|
|
310
|
+
instances_sexp = [sexpdata.Symbol("instances")]
|
|
311
|
+
for inst in user_instances:
|
|
312
|
+
project = inst.get('project', getattr(self, 'project_name', 'circuit'))
|
|
313
|
+
path = inst.get('path', '/')
|
|
314
|
+
reference = inst.get('reference', symbol_data.get('reference', 'U?'))
|
|
315
|
+
unit = inst.get('unit', 1)
|
|
316
|
+
|
|
317
|
+
logger.debug(f" Instance: project={project}, path={path}, ref={reference}, unit={unit}")
|
|
318
|
+
|
|
319
|
+
instances_sexp.append([
|
|
320
|
+
sexpdata.Symbol("project"),
|
|
321
|
+
project,
|
|
322
|
+
[
|
|
323
|
+
sexpdata.Symbol("path"),
|
|
324
|
+
path, # PRESERVE user-set hierarchical path!
|
|
325
|
+
[sexpdata.Symbol("reference"), reference],
|
|
326
|
+
[sexpdata.Symbol("unit"), unit],
|
|
327
|
+
],
|
|
328
|
+
])
|
|
329
|
+
sexp.append(instances_sexp)
|
|
330
|
+
else:
|
|
331
|
+
# No user-set instances - generate default (backward compatibility)
|
|
332
|
+
logger.debug(f"🔍 HIERARCHICAL FIX: Component {symbol_data.get('reference')} has NO user instances, generating default")
|
|
333
|
+
|
|
334
|
+
# Get project name from config or properties
|
|
335
|
+
project_name = symbol_data.get("properties", {}).get("project_name")
|
|
336
|
+
if not project_name:
|
|
337
|
+
project_name = getattr(self, "project_name", config.defaults.project_name)
|
|
338
|
+
|
|
339
|
+
# CRITICAL FIX: Use the FULL hierarchy_path from properties if available
|
|
340
|
+
# For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
|
|
341
|
+
# This ensures KiCad can properly annotate components in sub-sheets
|
|
342
|
+
hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
|
|
343
|
+
if hierarchy_path:
|
|
344
|
+
# Use the full hierarchical path (includes root + all sheet symbols)
|
|
345
|
+
instance_path = hierarchy_path
|
|
346
|
+
logger.debug(
|
|
347
|
+
f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
# Fallback: use root_uuid or schematic_uuid for flat designs
|
|
351
|
+
root_uuid = (
|
|
352
|
+
symbol_data.get("properties", {}).get("root_uuid")
|
|
353
|
+
or schematic_uuid
|
|
354
|
+
or str(uuid.uuid4())
|
|
355
|
+
)
|
|
356
|
+
instance_path = f"/{root_uuid}"
|
|
357
|
+
logger.debug(
|
|
358
|
+
f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
logger.debug(
|
|
362
|
+
f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
|
|
363
|
+
)
|
|
364
|
+
logger.debug(f"🔧 Using project name: '{project_name}'")
|
|
365
|
+
|
|
366
|
+
sexp.append(
|
|
367
|
+
[
|
|
368
|
+
sexpdata.Symbol("instances"),
|
|
369
|
+
[
|
|
370
|
+
sexpdata.Symbol("project"),
|
|
371
|
+
project_name,
|
|
372
|
+
[
|
|
373
|
+
sexpdata.Symbol("path"),
|
|
374
|
+
instance_path,
|
|
375
|
+
[sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
|
|
376
|
+
[sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
|
|
377
|
+
],
|
|
378
|
+
],
|
|
379
|
+
]
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return sexp
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _create_property_with_positioning(
|
|
386
|
+
self,
|
|
387
|
+
prop_name: str,
|
|
388
|
+
prop_value: str,
|
|
389
|
+
component_pos: Point,
|
|
390
|
+
offset_index: int,
|
|
391
|
+
justify: str = "left",
|
|
392
|
+
hide: bool = False,
|
|
393
|
+
rotation: float = 0,
|
|
394
|
+
) -> List[Any]:
|
|
395
|
+
"""Create a property with proper positioning and effects like KiCAD."""
|
|
396
|
+
from ...core.config import config
|
|
397
|
+
|
|
398
|
+
# Calculate property position using configuration
|
|
399
|
+
prop_x, prop_y, text_rotation = config.get_property_position(
|
|
400
|
+
prop_name, (component_pos.x, component_pos.y), offset_index, rotation
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Build effects section based on hide status
|
|
404
|
+
effects = [
|
|
405
|
+
sexpdata.Symbol("effects"),
|
|
406
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
# Only add justify for visible properties or Reference/Value
|
|
410
|
+
if not hide or prop_name in ["Reference", "Value"]:
|
|
411
|
+
effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
|
|
412
|
+
|
|
413
|
+
if hide:
|
|
414
|
+
effects.append([sexpdata.Symbol("hide"), sexpdata.Symbol("yes")])
|
|
415
|
+
|
|
416
|
+
prop_sexp = [
|
|
417
|
+
sexpdata.Symbol("property"),
|
|
418
|
+
prop_name,
|
|
419
|
+
prop_value,
|
|
420
|
+
[
|
|
421
|
+
sexpdata.Symbol("at"),
|
|
422
|
+
round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
|
|
423
|
+
round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
|
|
424
|
+
text_rotation,
|
|
425
|
+
],
|
|
426
|
+
effects,
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
return prop_sexp
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _create_power_symbol_value_property(
|
|
433
|
+
self, value: str, component_pos: Point, lib_id: str, rotation: float = 0
|
|
434
|
+
) -> List[Any]:
|
|
435
|
+
"""Create Value property for power symbols with correct positioning.
|
|
436
|
+
|
|
437
|
+
Matches circuit-synth power_symbol_positioning.py logic exactly.
|
|
438
|
+
"""
|
|
439
|
+
offset = 5.08 # KiCad standard offset
|
|
440
|
+
is_gnd_type = "GND" in lib_id.upper() or "VSS" in lib_id.upper()
|
|
441
|
+
|
|
442
|
+
# Rotation-aware positioning (matching circuit-synth logic)
|
|
443
|
+
if rotation == 0:
|
|
444
|
+
if is_gnd_type:
|
|
445
|
+
prop_x, prop_y = component_pos.x, component_pos.y + offset # GND points down, text below
|
|
446
|
+
else:
|
|
447
|
+
prop_x, prop_y = component_pos.x, component_pos.y - offset # VCC points up, text above
|
|
448
|
+
elif rotation == 90:
|
|
449
|
+
if is_gnd_type:
|
|
450
|
+
prop_x, prop_y = component_pos.x - offset, component_pos.y # GND left, text left
|
|
451
|
+
else:
|
|
452
|
+
prop_x, prop_y = component_pos.x + offset, component_pos.y # VCC right, text right
|
|
453
|
+
elif rotation == 180:
|
|
454
|
+
if is_gnd_type:
|
|
455
|
+
prop_x, prop_y = component_pos.x, component_pos.y - offset # GND inverted up, text above
|
|
456
|
+
else:
|
|
457
|
+
prop_x, prop_y = component_pos.x, component_pos.y + offset # VCC inverted down, text below
|
|
458
|
+
elif rotation == 270:
|
|
459
|
+
if is_gnd_type:
|
|
460
|
+
prop_x, prop_y = component_pos.x + offset, component_pos.y # GND right, text right
|
|
461
|
+
else:
|
|
462
|
+
prop_x, prop_y = component_pos.x - offset, component_pos.y # VCC left, text left
|
|
463
|
+
else:
|
|
464
|
+
# Fallback for non-standard rotations
|
|
465
|
+
prop_x, prop_y = component_pos.x, component_pos.y - offset if not is_gnd_type else component_pos.y + offset
|
|
466
|
+
|
|
467
|
+
prop_sexp = [
|
|
468
|
+
sexpdata.Symbol("property"),
|
|
469
|
+
"Value",
|
|
470
|
+
value,
|
|
471
|
+
[
|
|
472
|
+
sexpdata.Symbol("at"),
|
|
473
|
+
round(prop_x, 4) if prop_x != int(prop_x) else int(prop_x),
|
|
474
|
+
round(prop_y, 4) if prop_y != int(prop_y) else int(prop_y),
|
|
475
|
+
0,
|
|
476
|
+
],
|
|
477
|
+
[
|
|
478
|
+
sexpdata.Symbol("effects"),
|
|
479
|
+
[sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
|
|
480
|
+
],
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
return prop_sexp
|
|
484
|
+
|
|
485
|
+
|