kicad-sch-api 0.3.5__py3-none-any.whl → 0.4.0__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/collections/__init__.py +2 -2
- kicad_sch_api/collections/base.py +5 -7
- kicad_sch_api/collections/components.py +24 -12
- kicad_sch_api/collections/junctions.py +31 -43
- kicad_sch_api/collections/labels.py +19 -27
- kicad_sch_api/collections/wires.py +17 -18
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +2 -2
- kicad_sch_api/core/managers/__init__.py +26 -0
- kicad_sch_api/core/managers/file_io.py +243 -0
- kicad_sch_api/core/managers/format_sync.py +501 -0
- kicad_sch_api/core/managers/graphics.py +579 -0
- kicad_sch_api/core/managers/metadata.py +268 -0
- kicad_sch_api/core/managers/sheet.py +454 -0
- kicad_sch_api/core/managers/text_elements.py +536 -0
- kicad_sch_api/core/managers/validation.py +474 -0
- kicad_sch_api/core/managers/wire.py +346 -0
- kicad_sch_api/core/nets.py +1 -1
- kicad_sch_api/core/no_connects.py +5 -3
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +779 -1083
- kicad_sch_api/core/texts.py +1 -1
- kicad_sch_api/core/types.py +1 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +40 -21
- kicad_sch_api/interfaces/__init__.py +1 -1
- kicad_sch_api/interfaces/parser.py +1 -1
- kicad_sch_api/interfaces/repository.py +1 -1
- kicad_sch_api/interfaces/resolver.py +1 -1
- kicad_sch_api/parsers/__init__.py +2 -2
- kicad_sch_api/parsers/base.py +7 -10
- kicad_sch_api/parsers/label_parser.py +7 -7
- kicad_sch_api/parsers/registry.py +4 -2
- kicad_sch_api/parsers/symbol_parser.py +5 -10
- kicad_sch_api/parsers/wire_parser.py +2 -2
- kicad_sch_api/symbols/__init__.py +1 -1
- kicad_sch_api/symbols/cache.py +9 -12
- kicad_sch_api/symbols/resolver.py +20 -26
- kicad_sch_api/symbols/validators.py +188 -137
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
- kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
- kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/texts.py
CHANGED
kicad_sch_api/core/types.py
CHANGED
|
@@ -366,10 +366,7 @@ class SchematicRectangle:
|
|
|
366
366
|
@property
|
|
367
367
|
def center(self) -> Point:
|
|
368
368
|
"""Rectangle center point."""
|
|
369
|
-
return Point(
|
|
370
|
-
(self.start.x + self.end.x) / 2,
|
|
371
|
-
(self.start.y + self.end.y) / 2
|
|
372
|
-
)
|
|
369
|
+
return Point((self.start.x + self.end.x) / 2, (self.start.y + self.end.y) / 2)
|
|
373
370
|
|
|
374
371
|
|
|
375
372
|
@dataclass
|
|
@@ -17,4 +17,6 @@ DEFAULT_PIN_NUMBER_SIZE = 1.27 # 50 mils
|
|
|
17
17
|
# Text width ratio for proportional font rendering
|
|
18
18
|
# KiCad uses proportional fonts where average character width is ~0.65x height
|
|
19
19
|
# This prevents label text from extending beyond calculated bounding boxes
|
|
20
|
-
DEFAULT_PIN_TEXT_WIDTH_RATIO =
|
|
20
|
+
DEFAULT_PIN_TEXT_WIDTH_RATIO = (
|
|
21
|
+
0.65 # Width to height ratio for pin text (proportional font average)
|
|
22
|
+
)
|
|
@@ -120,7 +120,9 @@ class SymbolBoundingBoxCalculator:
|
|
|
120
120
|
if min_x == float("inf") or max_x == float("-inf"):
|
|
121
121
|
raise ValueError(f"No valid geometry found in symbol data")
|
|
122
122
|
|
|
123
|
-
logger.debug(
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
|
|
125
|
+
)
|
|
124
126
|
logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
|
|
125
127
|
|
|
126
128
|
# Add small margin for text that might extend beyond shapes
|
|
@@ -252,7 +254,9 @@ class SymbolBoundingBoxCalculator:
|
|
|
252
254
|
if min_x == float("inf") or max_x == float("-inf"):
|
|
253
255
|
raise ValueError(f"No valid geometry found in symbol data")
|
|
254
256
|
|
|
255
|
-
logger.debug(
|
|
257
|
+
logger.debug(
|
|
258
|
+
f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
|
|
259
|
+
)
|
|
256
260
|
logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
|
|
257
261
|
|
|
258
262
|
# Add small margin for visual spacing
|
|
@@ -265,12 +269,16 @@ class SymbolBoundingBoxCalculator:
|
|
|
265
269
|
# Add minimal space for component properties (Reference above, Value below)
|
|
266
270
|
# Use adaptive spacing based on component height for better visual hierarchy
|
|
267
271
|
component_height = max_y - min_y
|
|
268
|
-
property_spacing = max(
|
|
272
|
+
property_spacing = max(
|
|
273
|
+
3.0, component_height * 0.15
|
|
274
|
+
) # Adaptive: minimum 3mm or 15% of height
|
|
269
275
|
property_height = 1.27 # Reduced from 2.54mm
|
|
270
276
|
min_y -= property_spacing + property_height # Reference above
|
|
271
277
|
max_y += property_spacing + property_height # Value below
|
|
272
278
|
|
|
273
|
-
logger.debug(
|
|
279
|
+
logger.debug(
|
|
280
|
+
f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
|
|
281
|
+
)
|
|
274
282
|
logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
|
|
275
283
|
logger.debug("=" * 50)
|
|
276
284
|
|
|
@@ -359,7 +367,10 @@ class SymbolBoundingBoxCalculator:
|
|
|
359
367
|
|
|
360
368
|
@classmethod
|
|
361
369
|
def get_symbol_dimensions(
|
|
362
|
-
cls,
|
|
370
|
+
cls,
|
|
371
|
+
symbol_data: Dict[str, Any],
|
|
372
|
+
include_properties: bool = True,
|
|
373
|
+
pin_net_map: Optional[Dict[str, str]] = None,
|
|
363
374
|
) -> Tuple[float, float]:
|
|
364
375
|
"""
|
|
365
376
|
Get the width and height of a symbol.
|
|
@@ -488,24 +499,28 @@ class SymbolBoundingBoxCalculator:
|
|
|
488
499
|
# If no net name match, use minimal fallback to avoid oversized bounding boxes
|
|
489
500
|
if pin_net_map and pin_number in pin_net_map:
|
|
490
501
|
label_text = pin_net_map[pin_number]
|
|
491
|
-
logger.debug(
|
|
502
|
+
logger.debug(
|
|
503
|
+
f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}"
|
|
504
|
+
)
|
|
492
505
|
else:
|
|
493
506
|
# No net match - use minimal size (3 chars) instead of potentially long pin name
|
|
494
507
|
label_text = "XXX" # 3-character placeholder for unmatched pins
|
|
495
|
-
logger.debug(
|
|
508
|
+
logger.debug(
|
|
509
|
+
f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})"
|
|
510
|
+
)
|
|
496
511
|
|
|
497
512
|
if label_text and label_text != "~": # ~ means no name
|
|
498
513
|
# Calculate text dimensions
|
|
499
514
|
# For horizontal text: width = char_count * char_width
|
|
500
515
|
name_width = (
|
|
501
|
-
len(label_text)
|
|
502
|
-
* cls.DEFAULT_TEXT_HEIGHT
|
|
503
|
-
* cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
516
|
+
len(label_text) * cls.DEFAULT_TEXT_HEIGHT * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
504
517
|
)
|
|
505
518
|
# For vertical text: height = char_count * char_height (characters stack vertically)
|
|
506
519
|
name_height = len(label_text) * cls.DEFAULT_TEXT_HEIGHT
|
|
507
520
|
|
|
508
|
-
logger.debug(
|
|
521
|
+
logger.debug(
|
|
522
|
+
f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})"
|
|
523
|
+
)
|
|
509
524
|
|
|
510
525
|
# Adjust bounds based on pin orientation
|
|
511
526
|
# Labels are placed at PIN ENDPOINT with offset, extending AWAY from the component
|
|
@@ -515,32 +530,36 @@ class SymbolBoundingBoxCalculator:
|
|
|
515
530
|
|
|
516
531
|
if angle == 0: # Pin points right - label extends LEFT from endpoint
|
|
517
532
|
label_x = end_x - offset - name_width
|
|
518
|
-
logger.debug(
|
|
533
|
+
logger.debug(
|
|
534
|
+
f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
|
|
535
|
+
)
|
|
519
536
|
min_x = min(min_x, label_x)
|
|
520
537
|
elif angle == 180: # Pin points left - label extends RIGHT from endpoint
|
|
521
538
|
label_x = end_x + offset + name_width
|
|
522
|
-
logger.debug(
|
|
539
|
+
logger.debug(
|
|
540
|
+
f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
|
|
541
|
+
)
|
|
523
542
|
max_x = max(max_x, label_x)
|
|
524
543
|
elif angle == 90: # Pin points up - label extends DOWN from endpoint
|
|
525
544
|
label_y = end_y - offset - name_height
|
|
526
|
-
logger.debug(
|
|
545
|
+
logger.debug(
|
|
546
|
+
f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
|
|
547
|
+
)
|
|
527
548
|
min_y = min(min_y, label_y)
|
|
528
549
|
elif angle == 270: # Pin points down - label extends UP from endpoint
|
|
529
550
|
label_y = end_y + offset + name_height
|
|
530
|
-
logger.debug(
|
|
551
|
+
logger.debug(
|
|
552
|
+
f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
|
|
553
|
+
)
|
|
531
554
|
max_y = max(max_y, label_y)
|
|
532
555
|
|
|
533
556
|
# Pin numbers are typically placed near the component body
|
|
534
557
|
if pin_number:
|
|
535
558
|
num_width = (
|
|
536
|
-
len(pin_number)
|
|
537
|
-
* cls.DEFAULT_PIN_NUMBER_SIZE
|
|
538
|
-
* cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
559
|
+
len(pin_number) * cls.DEFAULT_PIN_NUMBER_SIZE * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
539
560
|
)
|
|
540
561
|
# Add some space for the pin number
|
|
541
|
-
margin =
|
|
542
|
-
cls.DEFAULT_PIN_NUMBER_SIZE * 1.5
|
|
543
|
-
) # Increase margin for better spacing
|
|
562
|
+
margin = cls.DEFAULT_PIN_NUMBER_SIZE * 1.5 # Increase margin for better spacing
|
|
544
563
|
min_x -= margin
|
|
545
564
|
min_y -= margin
|
|
546
565
|
max_x += margin
|
|
@@ -5,10 +5,10 @@ This package provides specialized parsers for different types of KiCAD
|
|
|
5
5
|
S-expression elements, organized by responsibility and testable in isolation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .registry import ElementParserRegistry
|
|
9
8
|
from .base import BaseElementParser
|
|
9
|
+
from .registry import ElementParserRegistry
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
12
12
|
"ElementParserRegistry",
|
|
13
13
|
"BaseElementParser",
|
|
14
|
-
]
|
|
14
|
+
]
|
kicad_sch_api/parsers/base.py
CHANGED
|
@@ -92,10 +92,7 @@ class BaseElementParser(IElementParser):
|
|
|
92
92
|
return None
|
|
93
93
|
|
|
94
94
|
try:
|
|
95
|
-
result = {
|
|
96
|
-
"x": float(element[1]),
|
|
97
|
-
"y": float(element[2])
|
|
98
|
-
}
|
|
95
|
+
result = {"x": float(element[1]), "y": float(element[2])}
|
|
99
96
|
|
|
100
97
|
# Optional angle parameter
|
|
101
98
|
if len(element) > 3:
|
|
@@ -105,7 +102,9 @@ class BaseElementParser(IElementParser):
|
|
|
105
102
|
except (ValueError, IndexError):
|
|
106
103
|
return None
|
|
107
104
|
|
|
108
|
-
def _extract_property_list(
|
|
105
|
+
def _extract_property_list(
|
|
106
|
+
self, elements: List[Any], property_name: str
|
|
107
|
+
) -> List[Dict[str, Any]]:
|
|
109
108
|
"""
|
|
110
109
|
Extract all instances of a property from a list of elements.
|
|
111
110
|
|
|
@@ -118,9 +117,7 @@ class BaseElementParser(IElementParser):
|
|
|
118
117
|
"""
|
|
119
118
|
properties = []
|
|
120
119
|
for element in elements:
|
|
121
|
-
if
|
|
122
|
-
len(element) > 0 and
|
|
123
|
-
element[0] == property_name):
|
|
120
|
+
if isinstance(element, list) and len(element) > 0 and element[0] == property_name:
|
|
124
121
|
prop = self._parse_property_element(element)
|
|
125
122
|
if prop:
|
|
126
123
|
properties.append(prop)
|
|
@@ -144,5 +141,5 @@ class BaseElementParser(IElementParser):
|
|
|
144
141
|
return {
|
|
145
142
|
"type": element[0],
|
|
146
143
|
"value": element[1] if len(element) > 1 else None,
|
|
147
|
-
"raw": element
|
|
148
|
-
}
|
|
144
|
+
"raw": element,
|
|
145
|
+
}
|
|
@@ -46,9 +46,9 @@ class LabelParser(BaseElementParser):
|
|
|
46
46
|
"bold": False,
|
|
47
47
|
"italic": False,
|
|
48
48
|
"hide": False,
|
|
49
|
-
"justify": []
|
|
49
|
+
"justify": [],
|
|
50
50
|
},
|
|
51
|
-
"uuid": None
|
|
51
|
+
"uuid": None,
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
for elem in element[2:]:
|
|
@@ -99,7 +99,7 @@ class LabelParser(BaseElementParser):
|
|
|
99
99
|
"bold": False,
|
|
100
100
|
"italic": False,
|
|
101
101
|
"hide": False,
|
|
102
|
-
"justify": []
|
|
102
|
+
"justify": [],
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
for elem in effects_element[1:]:
|
|
@@ -176,9 +176,9 @@ class HierarchicalLabelParser(BaseElementParser):
|
|
|
176
176
|
"bold": False,
|
|
177
177
|
"italic": False,
|
|
178
178
|
"hide": False,
|
|
179
|
-
"justify": []
|
|
179
|
+
"justify": [],
|
|
180
180
|
},
|
|
181
|
-
"uuid": None
|
|
181
|
+
"uuid": None,
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
for elem in element[2:]:
|
|
@@ -217,7 +217,7 @@ class HierarchicalLabelParser(BaseElementParser):
|
|
|
217
217
|
"bold": False,
|
|
218
218
|
"italic": False,
|
|
219
219
|
"hide": False,
|
|
220
|
-
"justify": []
|
|
220
|
+
"justify": [],
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
for elem in effects_element[1:]:
|
|
@@ -251,4 +251,4 @@ class HierarchicalLabelParser(BaseElementParser):
|
|
|
251
251
|
elif elem_type == "bold":
|
|
252
252
|
effects["bold"] = True
|
|
253
253
|
elif elem_type == "italic":
|
|
254
|
-
effects["italic"] = True
|
|
254
|
+
effects["italic"] = True
|
|
@@ -99,7 +99,9 @@ class ElementParserRegistry:
|
|
|
99
99
|
|
|
100
100
|
# Try fallback parser
|
|
101
101
|
if self._fallback_parser:
|
|
102
|
-
self._logger.debug(
|
|
102
|
+
self._logger.debug(
|
|
103
|
+
f"Using fallback parser for unknown element type: {element_type_str}"
|
|
104
|
+
)
|
|
103
105
|
return self._fallback_parser.parse(element)
|
|
104
106
|
|
|
105
107
|
# No parser available
|
|
@@ -150,4 +152,4 @@ class ElementParserRegistry:
|
|
|
150
152
|
"""Clear all registered parsers."""
|
|
151
153
|
self._parsers.clear()
|
|
152
154
|
self._fallback_parser = None
|
|
153
|
-
self._logger.debug("Cleared all registered parsers")
|
|
155
|
+
self._logger.debug("Cleared all registered parsers")
|
|
@@ -44,7 +44,7 @@ class SymbolParser(BaseElementParser):
|
|
|
44
44
|
"fields_autoplaced": False,
|
|
45
45
|
"uuid": None,
|
|
46
46
|
"properties": [],
|
|
47
|
-
"instances": []
|
|
47
|
+
"instances": [],
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
for elem in element[1:]:
|
|
@@ -112,7 +112,7 @@ class SymbolParser(BaseElementParser):
|
|
|
112
112
|
"name": str(prop_element[1]),
|
|
113
113
|
"value": str(prop_element[2]),
|
|
114
114
|
"position": {"x": 0, "y": 0, "angle": 0},
|
|
115
|
-
"effects": {}
|
|
115
|
+
"effects": {},
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
# Parse additional property elements
|
|
@@ -145,7 +145,7 @@ class SymbolParser(BaseElementParser):
|
|
|
145
145
|
"bold": False,
|
|
146
146
|
"italic": False,
|
|
147
147
|
"hide": False,
|
|
148
|
-
"justify": []
|
|
148
|
+
"justify": [],
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
for elem in effects_element[1:]:
|
|
@@ -193,12 +193,7 @@ class SymbolParser(BaseElementParser):
|
|
|
193
193
|
|
|
194
194
|
def _parse_instance(self, instance_element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
195
195
|
"""Parse a single instance element."""
|
|
196
|
-
instance = {
|
|
197
|
-
"project": "",
|
|
198
|
-
"path": "",
|
|
199
|
-
"reference": "",
|
|
200
|
-
"unit": 1
|
|
201
|
-
}
|
|
196
|
+
instance = {"project": "", "path": "", "reference": "", "unit": 1}
|
|
202
197
|
|
|
203
198
|
for elem in instance_element[1:]:
|
|
204
199
|
if isinstance(elem, list) and len(elem) >= 2:
|
|
@@ -224,4 +219,4 @@ class SymbolParser(BaseElementParser):
|
|
|
224
219
|
elif isinstance(value, sexpdata.Symbol):
|
|
225
220
|
return str(value).lower() in ("yes", "true", "1")
|
|
226
221
|
else:
|
|
227
|
-
return bool(value)
|
|
222
|
+
return bool(value)
|
|
@@ -39,7 +39,7 @@ class WireParser(BaseElementParser):
|
|
|
39
39
|
"stroke_width": 0.0,
|
|
40
40
|
"stroke_type": "default",
|
|
41
41
|
"uuid": None,
|
|
42
|
-
"wire_type": "wire" # Default to wire (vs bus)
|
|
42
|
+
"wire_type": "wire", # Default to wire (vs bus)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
for elem in element[1:]:
|
|
@@ -96,4 +96,4 @@ class WireParser(BaseElementParser):
|
|
|
96
96
|
elif stroke_type == "type":
|
|
97
97
|
wire_data["stroke_type"] = str(stroke_elem[1])
|
|
98
98
|
except (ValueError, IndexError) as e:
|
|
99
|
-
self._logger.warning(f"Invalid stroke property: {stroke_elem}, error: {e}")
|
|
99
|
+
self._logger.warning(f"Invalid stroke property: {stroke_elem}, error: {e}")
|
kicad_sch_api/symbols/cache.py
CHANGED
|
@@ -17,7 +17,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
|
17
17
|
|
|
18
18
|
import sexpdata
|
|
19
19
|
|
|
20
|
-
from ..library.cache import
|
|
20
|
+
from ..library.cache import LibraryStats, SymbolDefinition
|
|
21
21
|
from ..utils.validation import ValidationError
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
@@ -199,7 +199,7 @@ class SymbolCache(ISymbolCache):
|
|
|
199
199
|
library_path=library_path,
|
|
200
200
|
file_size=stat.st_size,
|
|
201
201
|
last_modified=stat.st_mtime,
|
|
202
|
-
symbol_count=0 # Will be updated when library is loaded
|
|
202
|
+
symbol_count=0, # Will be updated when library is loaded
|
|
203
203
|
)
|
|
204
204
|
|
|
205
205
|
logger.info(f"Added library: {library_name} ({library_path})")
|
|
@@ -257,10 +257,10 @@ class SymbolCache(ISymbolCache):
|
|
|
257
257
|
name: {
|
|
258
258
|
"file_size": stats.file_size,
|
|
259
259
|
"symbols_count": stats.symbols_count,
|
|
260
|
-
"last_loaded": stats.last_loaded
|
|
260
|
+
"last_loaded": stats.last_loaded,
|
|
261
261
|
}
|
|
262
262
|
for name, stats in self._lib_stats.items()
|
|
263
|
-
}
|
|
263
|
+
},
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
# Private methods for implementation details
|
|
@@ -316,10 +316,7 @@ class SymbolCache(ISymbolCache):
|
|
|
316
316
|
return None
|
|
317
317
|
|
|
318
318
|
def _create_symbol_definition(
|
|
319
|
-
self,
|
|
320
|
-
symbol_data: List,
|
|
321
|
-
lib_id: str,
|
|
322
|
-
library_name: str
|
|
319
|
+
self, symbol_data: List, lib_id: str, library_name: str
|
|
323
320
|
) -> SymbolDefinition:
|
|
324
321
|
"""Create SymbolDefinition from parsed symbol data."""
|
|
325
322
|
symbol_name = str(symbol_data[1]).strip('"')
|
|
@@ -345,7 +342,7 @@ class SymbolCache(ISymbolCache):
|
|
|
345
342
|
power_symbol=properties.get("power_symbol", False),
|
|
346
343
|
graphic_elements=graphic_elements,
|
|
347
344
|
raw_kicad_data=symbol_data,
|
|
348
|
-
extends=extends # Store extends information for resolver
|
|
345
|
+
extends=extends, # Store extends information for resolver
|
|
349
346
|
)
|
|
350
347
|
|
|
351
348
|
def _extract_symbol_properties(self, symbol_data: List) -> Dict[str, Any]:
|
|
@@ -356,7 +353,7 @@ class SymbolCache(ISymbolCache):
|
|
|
356
353
|
"keywords": "",
|
|
357
354
|
"datasheet": "",
|
|
358
355
|
"units": 1,
|
|
359
|
-
"power_symbol": False
|
|
356
|
+
"power_symbol": False,
|
|
360
357
|
}
|
|
361
358
|
|
|
362
359
|
for item in symbol_data[1:]:
|
|
@@ -446,7 +443,7 @@ class SymbolCache(ISymbolCache):
|
|
|
446
443
|
index_data = {
|
|
447
444
|
"symbol_index": self._symbol_index,
|
|
448
445
|
"library_paths": [str(path) for path in self._library_paths],
|
|
449
|
-
"created": time.time()
|
|
446
|
+
"created": time.time(),
|
|
450
447
|
}
|
|
451
448
|
|
|
452
449
|
with open(self._index_file, "w") as f:
|
|
@@ -467,4 +464,4 @@ class SymbolCache(ISymbolCache):
|
|
|
467
464
|
if str(item[0]) == "symbol" and str(item[1]) == symbol_name:
|
|
468
465
|
return item
|
|
469
466
|
|
|
470
|
-
return None
|
|
467
|
+
return None
|
|
@@ -86,7 +86,7 @@ class SymbolResolver:
|
|
|
86
86
|
max_chain_length = 0
|
|
87
87
|
|
|
88
88
|
for symbol in self._inheritance_cache.values():
|
|
89
|
-
if hasattr(symbol,
|
|
89
|
+
if hasattr(symbol, "_inheritance_depth"):
|
|
90
90
|
inheritance_chains += 1
|
|
91
91
|
max_chain_length = max(max_chain_length, symbol._inheritance_depth)
|
|
92
92
|
|
|
@@ -94,7 +94,7 @@ class SymbolResolver:
|
|
|
94
94
|
"resolved_symbols": len(self._inheritance_cache),
|
|
95
95
|
"inheritance_chains": inheritance_chains,
|
|
96
96
|
"max_chain_length": max_chain_length,
|
|
97
|
-
"cache_size": len(self._inheritance_cache)
|
|
97
|
+
"cache_size": len(self._inheritance_cache),
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
def _resolve_with_inheritance(self, symbol: SymbolDefinition) -> Optional[SymbolDefinition]:
|
|
@@ -112,7 +112,9 @@ class SymbolResolver:
|
|
|
112
112
|
|
|
113
113
|
# Check for circular inheritance
|
|
114
114
|
if symbol.lib_id in self._resolution_stack:
|
|
115
|
-
logger.error(
|
|
115
|
+
logger.error(
|
|
116
|
+
f"Circular inheritance detected: {' -> '.join(self._resolution_stack + [symbol.lib_id])}"
|
|
117
|
+
)
|
|
116
118
|
return None
|
|
117
119
|
|
|
118
120
|
self._resolution_stack.append(symbol.lib_id)
|
|
@@ -136,7 +138,7 @@ class SymbolResolver:
|
|
|
136
138
|
resolved_symbol = self._merge_parent_into_child(symbol, resolved_parent)
|
|
137
139
|
|
|
138
140
|
# Track inheritance depth for statistics
|
|
139
|
-
parent_depth = getattr(resolved_parent,
|
|
141
|
+
parent_depth = getattr(resolved_parent, "_inheritance_depth", 0)
|
|
140
142
|
resolved_symbol._inheritance_depth = parent_depth + 1
|
|
141
143
|
|
|
142
144
|
logger.debug(f"Resolved inheritance: {symbol.lib_id} extends {parent_lib_id}")
|
|
@@ -168,9 +170,7 @@ class SymbolResolver:
|
|
|
168
170
|
return f"{current_library}:{parent_name}"
|
|
169
171
|
|
|
170
172
|
def _merge_parent_into_child(
|
|
171
|
-
self,
|
|
172
|
-
child: SymbolDefinition,
|
|
173
|
-
parent: SymbolDefinition
|
|
173
|
+
self, child: SymbolDefinition, parent: SymbolDefinition
|
|
174
174
|
) -> SymbolDefinition:
|
|
175
175
|
"""
|
|
176
176
|
Merge parent symbol into child symbol.
|
|
@@ -188,10 +188,7 @@ class SymbolResolver:
|
|
|
188
188
|
# Merge raw KiCAD data for exact format preservation
|
|
189
189
|
if child.raw_kicad_data and parent.raw_kicad_data:
|
|
190
190
|
merged.raw_kicad_data = self._merge_kicad_data(
|
|
191
|
-
child.raw_kicad_data,
|
|
192
|
-
parent.raw_kicad_data,
|
|
193
|
-
child.name,
|
|
194
|
-
parent.name
|
|
191
|
+
child.raw_kicad_data, parent.raw_kicad_data, child.name, parent.name
|
|
195
192
|
)
|
|
196
193
|
|
|
197
194
|
# Merge other properties
|
|
@@ -204,11 +201,7 @@ class SymbolResolver:
|
|
|
204
201
|
return merged
|
|
205
202
|
|
|
206
203
|
def _merge_kicad_data(
|
|
207
|
-
self,
|
|
208
|
-
child_data: List,
|
|
209
|
-
parent_data: List,
|
|
210
|
-
child_name: str,
|
|
211
|
-
parent_name: str
|
|
204
|
+
self, child_data: List, parent_data: List, child_name: str, parent_name: str
|
|
212
205
|
) -> List:
|
|
213
206
|
"""
|
|
214
207
|
Merge parent KiCAD data into child KiCAD data.
|
|
@@ -227,11 +220,10 @@ class SymbolResolver:
|
|
|
227
220
|
|
|
228
221
|
# Remove extends directive from child
|
|
229
222
|
merged = [
|
|
230
|
-
item
|
|
223
|
+
item
|
|
224
|
+
for item in merged
|
|
231
225
|
if not (
|
|
232
|
-
isinstance(item, list) and
|
|
233
|
-
len(item) >= 2 and
|
|
234
|
-
item[0] == sexpdata.Symbol("extends")
|
|
226
|
+
isinstance(item, list) and len(item) >= 2 and item[0] == sexpdata.Symbol("extends")
|
|
235
227
|
)
|
|
236
228
|
]
|
|
237
229
|
|
|
@@ -255,9 +247,7 @@ class SymbolResolver:
|
|
|
255
247
|
return merged
|
|
256
248
|
|
|
257
249
|
def _merge_symbol_properties(
|
|
258
|
-
self,
|
|
259
|
-
child: SymbolDefinition,
|
|
260
|
-
parent: SymbolDefinition
|
|
250
|
+
self, child: SymbolDefinition, parent: SymbolDefinition
|
|
261
251
|
) -> SymbolDefinition:
|
|
262
252
|
"""
|
|
263
253
|
Merge symbol properties, with child properties taking precedence.
|
|
@@ -315,7 +305,9 @@ class SymbolResolver:
|
|
|
315
305
|
|
|
316
306
|
def check_symbol(current_lib_id: str) -> None:
|
|
317
307
|
if current_lib_id in visited:
|
|
318
|
-
issues.append(
|
|
308
|
+
issues.append(
|
|
309
|
+
f"Circular inheritance detected: {' -> '.join(chain + [current_lib_id])}"
|
|
310
|
+
)
|
|
319
311
|
return
|
|
320
312
|
|
|
321
313
|
visited.add(current_lib_id)
|
|
@@ -329,7 +321,9 @@ class SymbolResolver:
|
|
|
329
321
|
if symbol.extends:
|
|
330
322
|
parent_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
|
|
331
323
|
if not self._cache.has_symbol(parent_lib_id):
|
|
332
|
-
issues.append(
|
|
324
|
+
issues.append(
|
|
325
|
+
f"Parent symbol not found: {parent_lib_id} (extended by {current_lib_id})"
|
|
326
|
+
)
|
|
333
327
|
else:
|
|
334
328
|
check_symbol(parent_lib_id)
|
|
335
329
|
|
|
@@ -364,4 +358,4 @@ class SymbolResolver:
|
|
|
364
358
|
|
|
365
359
|
current_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
|
|
366
360
|
|
|
367
|
-
return chain
|
|
361
|
+
return chain
|