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.
Files changed (112) hide show
  1. kicad_sch_api/__init__.py +68 -3
  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/kicad_to_python.py +169 -0
  8. kicad_sch_api/cli/netlist.py +94 -0
  9. kicad_sch_api/cli/types.py +43 -0
  10. kicad_sch_api/collections/__init__.py +36 -0
  11. kicad_sch_api/collections/base.py +604 -0
  12. kicad_sch_api/collections/components.py +1623 -0
  13. kicad_sch_api/collections/junctions.py +206 -0
  14. kicad_sch_api/collections/labels.py +508 -0
  15. kicad_sch_api/collections/wires.py +292 -0
  16. kicad_sch_api/core/__init__.py +37 -2
  17. kicad_sch_api/core/collections/__init__.py +5 -0
  18. kicad_sch_api/core/collections/base.py +248 -0
  19. kicad_sch_api/core/component_bounds.py +34 -7
  20. kicad_sch_api/core/components.py +213 -52
  21. kicad_sch_api/core/config.py +110 -15
  22. kicad_sch_api/core/connectivity.py +692 -0
  23. kicad_sch_api/core/exceptions.py +175 -0
  24. kicad_sch_api/core/factories/__init__.py +5 -0
  25. kicad_sch_api/core/factories/element_factory.py +278 -0
  26. kicad_sch_api/core/formatter.py +60 -9
  27. kicad_sch_api/core/geometry.py +94 -5
  28. kicad_sch_api/core/junctions.py +26 -75
  29. kicad_sch_api/core/labels.py +324 -0
  30. kicad_sch_api/core/managers/__init__.py +30 -0
  31. kicad_sch_api/core/managers/base.py +76 -0
  32. kicad_sch_api/core/managers/file_io.py +246 -0
  33. kicad_sch_api/core/managers/format_sync.py +502 -0
  34. kicad_sch_api/core/managers/graphics.py +580 -0
  35. kicad_sch_api/core/managers/hierarchy.py +661 -0
  36. kicad_sch_api/core/managers/metadata.py +271 -0
  37. kicad_sch_api/core/managers/sheet.py +492 -0
  38. kicad_sch_api/core/managers/text_elements.py +537 -0
  39. kicad_sch_api/core/managers/validation.py +476 -0
  40. kicad_sch_api/core/managers/wire.py +410 -0
  41. kicad_sch_api/core/nets.py +305 -0
  42. kicad_sch_api/core/no_connects.py +252 -0
  43. kicad_sch_api/core/parser.py +194 -970
  44. kicad_sch_api/core/parsing_utils.py +63 -0
  45. kicad_sch_api/core/pin_utils.py +103 -9
  46. kicad_sch_api/core/schematic.py +1328 -1079
  47. kicad_sch_api/core/texts.py +316 -0
  48. kicad_sch_api/core/types.py +159 -23
  49. kicad_sch_api/core/wires.py +27 -75
  50. kicad_sch_api/exporters/__init__.py +10 -0
  51. kicad_sch_api/exporters/python_generator.py +610 -0
  52. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  53. kicad_sch_api/geometry/__init__.py +38 -0
  54. kicad_sch_api/geometry/font_metrics.py +22 -0
  55. kicad_sch_api/geometry/routing.py +211 -0
  56. kicad_sch_api/geometry/symbol_bbox.py +608 -0
  57. kicad_sch_api/interfaces/__init__.py +17 -0
  58. kicad_sch_api/interfaces/parser.py +76 -0
  59. kicad_sch_api/interfaces/repository.py +70 -0
  60. kicad_sch_api/interfaces/resolver.py +117 -0
  61. kicad_sch_api/parsers/__init__.py +14 -0
  62. kicad_sch_api/parsers/base.py +145 -0
  63. kicad_sch_api/parsers/elements/__init__.py +22 -0
  64. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  65. kicad_sch_api/parsers/elements/label_parser.py +216 -0
  66. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  67. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  68. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  69. kicad_sch_api/parsers/elements/symbol_parser.py +485 -0
  70. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  71. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  72. kicad_sch_api/parsers/registry.py +155 -0
  73. kicad_sch_api/parsers/utils.py +80 -0
  74. kicad_sch_api/symbols/__init__.py +18 -0
  75. kicad_sch_api/symbols/cache.py +467 -0
  76. kicad_sch_api/symbols/resolver.py +361 -0
  77. kicad_sch_api/symbols/validators.py +504 -0
  78. kicad_sch_api/utils/logging.py +555 -0
  79. kicad_sch_api/utils/logging_decorators.py +587 -0
  80. kicad_sch_api/utils/validation.py +16 -22
  81. kicad_sch_api/validation/__init__.py +25 -0
  82. kicad_sch_api/validation/erc.py +171 -0
  83. kicad_sch_api/validation/erc_models.py +203 -0
  84. kicad_sch_api/validation/pin_matrix.py +243 -0
  85. kicad_sch_api/validation/validators.py +391 -0
  86. kicad_sch_api/wrappers/__init__.py +14 -0
  87. kicad_sch_api/wrappers/base.py +89 -0
  88. kicad_sch_api/wrappers/wire.py +198 -0
  89. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  90. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  91. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  92. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  93. mcp_server/__init__.py +34 -0
  94. mcp_server/example_logging_integration.py +506 -0
  95. mcp_server/models.py +252 -0
  96. mcp_server/server.py +357 -0
  97. mcp_server/tools/__init__.py +32 -0
  98. mcp_server/tools/component_tools.py +516 -0
  99. mcp_server/tools/connectivity_tools.py +532 -0
  100. mcp_server/tools/consolidated_tools.py +1216 -0
  101. mcp_server/tools/pin_discovery.py +333 -0
  102. mcp_server/utils/__init__.py +38 -0
  103. mcp_server/utils/logging.py +127 -0
  104. mcp_server/utils.py +36 -0
  105. kicad_sch_api/core/manhattan_routing.py +0 -430
  106. kicad_sch_api/core/simple_manhattan.py +0 -228
  107. kicad_sch_api/core/wire_routing.py +0 -380
  108. kicad_sch_api-0.3.0.dist-info/METADATA +0 -483
  109. kicad_sch_api-0.3.0.dist-info/RECORD +0 -31
  110. kicad_sch_api-0.3.0.dist-info/entry_points.txt +0 -2
  111. {kicad_sch_api-0.3.0.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  112. {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
+