kicad-sch-api 0.3.4__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 +21 -0
- kicad_sch_api/collections/base.py +294 -0
- kicad_sch_api/collections/components.py +434 -0
- kicad_sch_api/collections/junctions.py +366 -0
- kicad_sch_api/collections/labels.py +404 -0
- kicad_sch_api/collections/wires.py +406 -0
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +348 -0
- 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 +310 -0
- kicad_sch_api/core/no_connects.py +276 -0
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +904 -1074
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +13 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +56 -43
- 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/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/symbol_parser.py +222 -0
- kicad_sch_api/parsers/wire_parser.py +99 -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-0.3.4.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.4.dist-info/RECORD +0 -34
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Text element management for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
This module provides collection classes for managing text elements,
|
|
5
|
+
featuring fast lookup, bulk operations, and validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
13
|
+
from .types import Point, Text
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextElement:
|
|
19
|
+
"""
|
|
20
|
+
Enhanced wrapper for schematic text elements with modern API.
|
|
21
|
+
|
|
22
|
+
Provides intuitive access to text properties and operations
|
|
23
|
+
while maintaining exact format preservation.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, text_data: Text, parent_collection: "TextCollection"):
|
|
27
|
+
"""
|
|
28
|
+
Initialize text element wrapper.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
text_data: Underlying text data
|
|
32
|
+
parent_collection: Parent collection for updates
|
|
33
|
+
"""
|
|
34
|
+
self._data = text_data
|
|
35
|
+
self._collection = parent_collection
|
|
36
|
+
self._validator = SchematicValidator()
|
|
37
|
+
|
|
38
|
+
# Core properties with validation
|
|
39
|
+
@property
|
|
40
|
+
def uuid(self) -> str:
|
|
41
|
+
"""Text element UUID."""
|
|
42
|
+
return self._data.uuid
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def text(self) -> str:
|
|
46
|
+
"""Text content."""
|
|
47
|
+
return self._data.text
|
|
48
|
+
|
|
49
|
+
@text.setter
|
|
50
|
+
def text(self, value: str):
|
|
51
|
+
"""Set text content with validation."""
|
|
52
|
+
if not isinstance(value, str):
|
|
53
|
+
raise ValidationError(f"Text content must be string, got {type(value)}")
|
|
54
|
+
self._data.text = value
|
|
55
|
+
self._collection._mark_modified()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def position(self) -> Point:
|
|
59
|
+
"""Text position."""
|
|
60
|
+
return self._data.position
|
|
61
|
+
|
|
62
|
+
@position.setter
|
|
63
|
+
def position(self, value: Union[Point, Tuple[float, float]]):
|
|
64
|
+
"""Set text position."""
|
|
65
|
+
if isinstance(value, tuple):
|
|
66
|
+
value = Point(value[0], value[1])
|
|
67
|
+
elif not isinstance(value, Point):
|
|
68
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
|
|
69
|
+
self._data.position = value
|
|
70
|
+
self._collection._mark_modified()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def rotation(self) -> float:
|
|
74
|
+
"""Text rotation in degrees."""
|
|
75
|
+
return self._data.rotation
|
|
76
|
+
|
|
77
|
+
@rotation.setter
|
|
78
|
+
def rotation(self, value: float):
|
|
79
|
+
"""Set text rotation."""
|
|
80
|
+
self._data.rotation = float(value)
|
|
81
|
+
self._collection._mark_modified()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def size(self) -> float:
|
|
85
|
+
"""Text size."""
|
|
86
|
+
return self._data.size
|
|
87
|
+
|
|
88
|
+
@size.setter
|
|
89
|
+
def size(self, value: float):
|
|
90
|
+
"""Set text size with validation."""
|
|
91
|
+
if value <= 0:
|
|
92
|
+
raise ValidationError(f"Text size must be positive, got {value}")
|
|
93
|
+
self._data.size = float(value)
|
|
94
|
+
self._collection._mark_modified()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def exclude_from_sim(self) -> bool:
|
|
98
|
+
"""Whether text is excluded from simulation."""
|
|
99
|
+
return self._data.exclude_from_sim
|
|
100
|
+
|
|
101
|
+
@exclude_from_sim.setter
|
|
102
|
+
def exclude_from_sim(self, value: bool):
|
|
103
|
+
"""Set exclude from simulation flag."""
|
|
104
|
+
self._data.exclude_from_sim = bool(value)
|
|
105
|
+
self._collection._mark_modified()
|
|
106
|
+
|
|
107
|
+
def validate(self) -> List[ValidationIssue]:
|
|
108
|
+
"""Validate this text element."""
|
|
109
|
+
return self._validator.validate_text(self._data.__dict__)
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
112
|
+
"""Convert text element to dictionary representation."""
|
|
113
|
+
return {
|
|
114
|
+
"uuid": self.uuid,
|
|
115
|
+
"text": self.text,
|
|
116
|
+
"position": {"x": self.position.x, "y": self.position.y},
|
|
117
|
+
"rotation": self.rotation,
|
|
118
|
+
"size": self.size,
|
|
119
|
+
"exclude_from_sim": self.exclude_from_sim,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
"""String representation."""
|
|
124
|
+
return f"<Text '{self.text}' @ {self.position}>"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TextCollection:
|
|
128
|
+
"""
|
|
129
|
+
Collection class for efficient text element management.
|
|
130
|
+
|
|
131
|
+
Provides fast lookup, filtering, and bulk operations for schematic text elements.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, texts: List[Text] = None):
|
|
135
|
+
"""
|
|
136
|
+
Initialize text collection.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
texts: Initial list of text data
|
|
140
|
+
"""
|
|
141
|
+
self._texts: List[TextElement] = []
|
|
142
|
+
self._uuid_index: Dict[str, TextElement] = {}
|
|
143
|
+
self._content_index: Dict[str, List[TextElement]] = {}
|
|
144
|
+
self._modified = False
|
|
145
|
+
|
|
146
|
+
# Add initial texts
|
|
147
|
+
if texts:
|
|
148
|
+
for text_data in texts:
|
|
149
|
+
self._add_to_indexes(TextElement(text_data, self))
|
|
150
|
+
|
|
151
|
+
logger.debug(f"TextCollection initialized with {len(self._texts)} texts")
|
|
152
|
+
|
|
153
|
+
def add(
|
|
154
|
+
self,
|
|
155
|
+
text: str,
|
|
156
|
+
position: Union[Point, Tuple[float, float]],
|
|
157
|
+
rotation: float = 0.0,
|
|
158
|
+
size: float = 1.27,
|
|
159
|
+
exclude_from_sim: bool = False,
|
|
160
|
+
text_uuid: Optional[str] = None,
|
|
161
|
+
) -> TextElement:
|
|
162
|
+
"""
|
|
163
|
+
Add a new text element to the schematic.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
text: Text content
|
|
167
|
+
position: Text position
|
|
168
|
+
rotation: Text rotation in degrees
|
|
169
|
+
size: Text size
|
|
170
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
171
|
+
text_uuid: Specific UUID for text (auto-generated if None)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Newly created TextElement
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValidationError: If text data is invalid
|
|
178
|
+
"""
|
|
179
|
+
# Validate inputs
|
|
180
|
+
if not isinstance(text, str) or not text.strip():
|
|
181
|
+
raise ValidationError("Text content cannot be empty")
|
|
182
|
+
|
|
183
|
+
if isinstance(position, tuple):
|
|
184
|
+
position = Point(position[0], position[1])
|
|
185
|
+
elif not isinstance(position, Point):
|
|
186
|
+
raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
|
|
187
|
+
|
|
188
|
+
if size <= 0:
|
|
189
|
+
raise ValidationError(f"Text size must be positive, got {size}")
|
|
190
|
+
|
|
191
|
+
# Generate UUID if not provided
|
|
192
|
+
if not text_uuid:
|
|
193
|
+
text_uuid = str(uuid.uuid4())
|
|
194
|
+
|
|
195
|
+
# Check for duplicate UUID
|
|
196
|
+
if text_uuid in self._uuid_index:
|
|
197
|
+
raise ValidationError(f"Text UUID {text_uuid} already exists")
|
|
198
|
+
|
|
199
|
+
# Create text data
|
|
200
|
+
text_data = Text(
|
|
201
|
+
uuid=text_uuid,
|
|
202
|
+
position=position,
|
|
203
|
+
text=text,
|
|
204
|
+
rotation=rotation,
|
|
205
|
+
size=size,
|
|
206
|
+
exclude_from_sim=exclude_from_sim,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Create wrapper and add to collection
|
|
210
|
+
text_element = TextElement(text_data, self)
|
|
211
|
+
self._add_to_indexes(text_element)
|
|
212
|
+
self._mark_modified()
|
|
213
|
+
|
|
214
|
+
logger.debug(f"Added text: {text_element}")
|
|
215
|
+
return text_element
|
|
216
|
+
|
|
217
|
+
def get(self, text_uuid: str) -> Optional[TextElement]:
|
|
218
|
+
"""Get text by UUID."""
|
|
219
|
+
return self._uuid_index.get(text_uuid)
|
|
220
|
+
|
|
221
|
+
def remove(self, text_uuid: str) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Remove text by UUID.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
text_uuid: UUID of text to remove
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
True if text was removed, False if not found
|
|
230
|
+
"""
|
|
231
|
+
text_element = self._uuid_index.get(text_uuid)
|
|
232
|
+
if not text_element:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
# Remove from indexes
|
|
236
|
+
self._remove_from_indexes(text_element)
|
|
237
|
+
self._mark_modified()
|
|
238
|
+
|
|
239
|
+
logger.debug(f"Removed text: {text_element}")
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
def find_by_content(self, content: str, exact: bool = True) -> List[TextElement]:
|
|
243
|
+
"""
|
|
244
|
+
Find texts by content.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
content: Content to search for
|
|
248
|
+
exact: If True, exact match; if False, substring match
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of matching text elements
|
|
252
|
+
"""
|
|
253
|
+
if exact:
|
|
254
|
+
return self._content_index.get(content, []).copy()
|
|
255
|
+
else:
|
|
256
|
+
matches = []
|
|
257
|
+
for text_element in self._texts:
|
|
258
|
+
if content.lower() in text_element.text.lower():
|
|
259
|
+
matches.append(text_element)
|
|
260
|
+
return matches
|
|
261
|
+
|
|
262
|
+
def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
|
|
263
|
+
"""
|
|
264
|
+
Filter texts by predicate function.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
predicate: Function that returns True for texts to include
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of texts matching predicate
|
|
271
|
+
"""
|
|
272
|
+
return [text for text in self._texts if predicate(text)]
|
|
273
|
+
|
|
274
|
+
def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
|
|
275
|
+
"""
|
|
276
|
+
Update multiple texts matching criteria.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
criteria: Function to select texts to update
|
|
280
|
+
updates: Dictionary of property updates
|
|
281
|
+
"""
|
|
282
|
+
updated_count = 0
|
|
283
|
+
for text_element in self._texts:
|
|
284
|
+
if criteria(text_element):
|
|
285
|
+
for prop, value in updates.items():
|
|
286
|
+
if hasattr(text_element, prop):
|
|
287
|
+
setattr(text_element, prop, value)
|
|
288
|
+
updated_count += 1
|
|
289
|
+
|
|
290
|
+
if updated_count > 0:
|
|
291
|
+
self._mark_modified()
|
|
292
|
+
logger.debug(f"Bulk updated {updated_count} text properties")
|
|
293
|
+
|
|
294
|
+
def clear(self):
|
|
295
|
+
"""Remove all texts from collection."""
|
|
296
|
+
self._texts.clear()
|
|
297
|
+
self._uuid_index.clear()
|
|
298
|
+
self._content_index.clear()
|
|
299
|
+
self._mark_modified()
|
|
300
|
+
|
|
301
|
+
def _add_to_indexes(self, text_element: TextElement):
|
|
302
|
+
"""Add text to internal indexes."""
|
|
303
|
+
self._texts.append(text_element)
|
|
304
|
+
self._uuid_index[text_element.uuid] = text_element
|
|
305
|
+
|
|
306
|
+
# Add to content index
|
|
307
|
+
content = text_element.text
|
|
308
|
+
if content not in self._content_index:
|
|
309
|
+
self._content_index[content] = []
|
|
310
|
+
self._content_index[content].append(text_element)
|
|
311
|
+
|
|
312
|
+
def _remove_from_indexes(self, text_element: TextElement):
|
|
313
|
+
"""Remove text from internal indexes."""
|
|
314
|
+
self._texts.remove(text_element)
|
|
315
|
+
del self._uuid_index[text_element.uuid]
|
|
316
|
+
|
|
317
|
+
# Remove from content index
|
|
318
|
+
content = text_element.text
|
|
319
|
+
if content in self._content_index:
|
|
320
|
+
self._content_index[content].remove(text_element)
|
|
321
|
+
if not self._content_index[content]:
|
|
322
|
+
del self._content_index[content]
|
|
323
|
+
|
|
324
|
+
def _mark_modified(self):
|
|
325
|
+
"""Mark collection as modified."""
|
|
326
|
+
self._modified = True
|
|
327
|
+
|
|
328
|
+
# Collection interface methods
|
|
329
|
+
def __len__(self) -> int:
|
|
330
|
+
"""Return number of texts."""
|
|
331
|
+
return len(self._texts)
|
|
332
|
+
|
|
333
|
+
def __iter__(self) -> Iterator[TextElement]:
|
|
334
|
+
"""Iterate over texts."""
|
|
335
|
+
return iter(self._texts)
|
|
336
|
+
|
|
337
|
+
def __getitem__(self, index: int) -> TextElement:
|
|
338
|
+
"""Get text by index."""
|
|
339
|
+
return self._texts[index]
|
|
340
|
+
|
|
341
|
+
def __bool__(self) -> bool:
|
|
342
|
+
"""Return True if collection has texts."""
|
|
343
|
+
return len(self._texts) > 0
|
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
|
|
@@ -386,6 +383,18 @@ class Image:
|
|
|
386
383
|
self.uuid = str(uuid4())
|
|
387
384
|
|
|
388
385
|
|
|
386
|
+
@dataclass
|
|
387
|
+
class NoConnect:
|
|
388
|
+
"""No-connect symbol in schematic."""
|
|
389
|
+
|
|
390
|
+
uuid: str
|
|
391
|
+
position: Point
|
|
392
|
+
|
|
393
|
+
def __post_init__(self):
|
|
394
|
+
if not self.uuid:
|
|
395
|
+
self.uuid = str(uuid4())
|
|
396
|
+
|
|
397
|
+
|
|
389
398
|
@dataclass
|
|
390
399
|
class Net:
|
|
391
400
|
"""Electrical net connecting components."""
|
|
@@ -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
|
+
)
|
|
@@ -59,12 +59,9 @@ class SymbolBoundingBoxCalculator:
|
|
|
59
59
|
if not symbol_data:
|
|
60
60
|
raise ValueError("Symbol data is None or empty")
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if debug_enabled:
|
|
66
|
-
print(f"\n=== CALCULATING BOUNDING BOX ===", file=sys.stderr, flush=True)
|
|
67
|
-
print(f"include_properties={include_properties}", file=sys.stderr, flush=True)
|
|
62
|
+
# Use proper logging instead of print statements
|
|
63
|
+
logger.debug("=== CALCULATING BOUNDING BOX ===")
|
|
64
|
+
logger.debug(f"include_properties={include_properties}")
|
|
68
65
|
|
|
69
66
|
min_x = float("inf")
|
|
70
67
|
min_y = float("inf")
|
|
@@ -73,7 +70,7 @@ class SymbolBoundingBoxCalculator:
|
|
|
73
70
|
|
|
74
71
|
# Process main symbol shapes (handle both 'shapes' and 'graphics' keys)
|
|
75
72
|
shapes = symbol_data.get("shapes", []) or symbol_data.get("graphics", [])
|
|
76
|
-
|
|
73
|
+
logger.debug(f"Processing {len(shapes)} main shapes")
|
|
77
74
|
for shape in shapes:
|
|
78
75
|
shape_bounds = cls._get_shape_bounds(shape)
|
|
79
76
|
if shape_bounds:
|
|
@@ -85,7 +82,7 @@ class SymbolBoundingBoxCalculator:
|
|
|
85
82
|
|
|
86
83
|
# Process pins (including their labels)
|
|
87
84
|
pins = symbol_data.get("pins", [])
|
|
88
|
-
|
|
85
|
+
logger.debug(f"Processing {len(pins)} main pins")
|
|
89
86
|
for pin in pins:
|
|
90
87
|
pin_bounds = cls._get_pin_bounds(pin, pin_net_map)
|
|
91
88
|
if pin_bounds:
|
|
@@ -123,8 +120,10 @@ class SymbolBoundingBoxCalculator:
|
|
|
123
120
|
if min_x == float("inf") or max_x == float("-inf"):
|
|
124
121
|
raise ValueError(f"No valid geometry found in symbol data")
|
|
125
122
|
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
logger.debug(
|
|
124
|
+
f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
|
|
125
|
+
)
|
|
126
|
+
logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
|
|
128
127
|
|
|
129
128
|
# Add small margin for text that might extend beyond shapes
|
|
130
129
|
margin = 0.254 # 10 mils
|
|
@@ -162,9 +161,9 @@ class SymbolBoundingBoxCalculator:
|
|
|
162
161
|
f"Calculated bounding box: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
|
|
163
162
|
)
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
logger.debug(f"FINAL BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
|
|
165
|
+
logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
|
|
166
|
+
logger.debug("=" * 50)
|
|
168
167
|
|
|
169
168
|
return (min_x, min_y, max_x, max_y)
|
|
170
169
|
|
|
@@ -196,8 +195,7 @@ class SymbolBoundingBoxCalculator:
|
|
|
196
195
|
if not symbol_data:
|
|
197
196
|
raise ValueError("Symbol data is None or empty")
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
print(f"\n=== CALCULATING PLACEMENT BOUNDING BOX (NO PIN LABELS) ===", file=sys.stderr, flush=True)
|
|
198
|
+
logger.debug("=== CALCULATING PLACEMENT BOUNDING BOX (NO PIN LABELS) ===")
|
|
201
199
|
|
|
202
200
|
min_x = float("inf")
|
|
203
201
|
min_y = float("inf")
|
|
@@ -206,7 +204,7 @@ class SymbolBoundingBoxCalculator:
|
|
|
206
204
|
|
|
207
205
|
# Process main symbol shapes (handle both 'shapes' and 'graphics' keys)
|
|
208
206
|
shapes = symbol_data.get("shapes", []) or symbol_data.get("graphics", [])
|
|
209
|
-
|
|
207
|
+
logger.debug(f"Processing {len(shapes)} main shapes")
|
|
210
208
|
for shape in shapes:
|
|
211
209
|
shape_bounds = cls._get_shape_bounds(shape)
|
|
212
210
|
if shape_bounds:
|
|
@@ -218,7 +216,7 @@ class SymbolBoundingBoxCalculator:
|
|
|
218
216
|
|
|
219
217
|
# Process pins WITHOUT labels (just pin endpoints)
|
|
220
218
|
pins = symbol_data.get("pins", [])
|
|
221
|
-
|
|
219
|
+
logger.debug(f"Processing {len(pins)} main pins (NO LABELS)")
|
|
222
220
|
for pin in pins:
|
|
223
221
|
pin_bounds = cls._get_pin_bounds_no_labels(pin)
|
|
224
222
|
if pin_bounds:
|
|
@@ -256,8 +254,10 @@ class SymbolBoundingBoxCalculator:
|
|
|
256
254
|
if min_x == float("inf") or max_x == float("-inf"):
|
|
257
255
|
raise ValueError(f"No valid geometry found in symbol data")
|
|
258
256
|
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
logger.debug(
|
|
258
|
+
f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
|
|
259
|
+
)
|
|
260
|
+
logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
|
|
261
261
|
|
|
262
262
|
# Add small margin for visual spacing
|
|
263
263
|
margin = 0.635 # 25mil margin (reduced from 50mil)
|
|
@@ -269,14 +269,18 @@ class SymbolBoundingBoxCalculator:
|
|
|
269
269
|
# Add minimal space for component properties (Reference above, Value below)
|
|
270
270
|
# Use adaptive spacing based on component height for better visual hierarchy
|
|
271
271
|
component_height = max_y - min_y
|
|
272
|
-
property_spacing = max(
|
|
272
|
+
property_spacing = max(
|
|
273
|
+
3.0, component_height * 0.15
|
|
274
|
+
) # Adaptive: minimum 3mm or 15% of height
|
|
273
275
|
property_height = 1.27 # Reduced from 2.54mm
|
|
274
276
|
min_y -= property_spacing + property_height # Reference above
|
|
275
277
|
max_y += property_spacing + property_height # Value below
|
|
276
278
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
logger.debug(
|
|
280
|
+
f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
|
|
281
|
+
)
|
|
282
|
+
logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
|
|
283
|
+
logger.debug("=" * 50)
|
|
280
284
|
|
|
281
285
|
return (min_x, min_y, max_x, max_y)
|
|
282
286
|
|
|
@@ -363,7 +367,10 @@ class SymbolBoundingBoxCalculator:
|
|
|
363
367
|
|
|
364
368
|
@classmethod
|
|
365
369
|
def get_symbol_dimensions(
|
|
366
|
-
cls,
|
|
370
|
+
cls,
|
|
371
|
+
symbol_data: Dict[str, Any],
|
|
372
|
+
include_properties: bool = True,
|
|
373
|
+
pin_net_map: Optional[Dict[str, str]] = None,
|
|
367
374
|
) -> Tuple[float, float]:
|
|
368
375
|
"""
|
|
369
376
|
Get the width and height of a symbol.
|
|
@@ -459,7 +466,6 @@ class SymbolBoundingBoxCalculator:
|
|
|
459
466
|
cls, pin: Dict[str, Any], pin_net_map: Optional[Dict[str, str]] = None
|
|
460
467
|
) -> Optional[Tuple[float, float, float, float]]:
|
|
461
468
|
"""Get bounding box for a pin including its labels."""
|
|
462
|
-
import sys
|
|
463
469
|
|
|
464
470
|
# Handle both formats: 'at' array or separate x/y/orientation
|
|
465
471
|
if "at" in pin:
|
|
@@ -493,24 +499,28 @@ class SymbolBoundingBoxCalculator:
|
|
|
493
499
|
# If no net name match, use minimal fallback to avoid oversized bounding boxes
|
|
494
500
|
if pin_net_map and pin_number in pin_net_map:
|
|
495
501
|
label_text = pin_net_map[pin_number]
|
|
496
|
-
|
|
502
|
+
logger.debug(
|
|
503
|
+
f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}"
|
|
504
|
+
)
|
|
497
505
|
else:
|
|
498
506
|
# No net match - use minimal size (3 chars) instead of potentially long pin name
|
|
499
507
|
label_text = "XXX" # 3-character placeholder for unmatched pins
|
|
500
|
-
|
|
508
|
+
logger.debug(
|
|
509
|
+
f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})"
|
|
510
|
+
)
|
|
501
511
|
|
|
502
512
|
if label_text and label_text != "~": # ~ means no name
|
|
503
513
|
# Calculate text dimensions
|
|
504
514
|
# For horizontal text: width = char_count * char_width
|
|
505
515
|
name_width = (
|
|
506
|
-
len(label_text)
|
|
507
|
-
* cls.DEFAULT_TEXT_HEIGHT
|
|
508
|
-
* cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
516
|
+
len(label_text) * cls.DEFAULT_TEXT_HEIGHT * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
509
517
|
)
|
|
510
518
|
# For vertical text: height = char_count * char_height (characters stack vertically)
|
|
511
519
|
name_height = len(label_text) * cls.DEFAULT_TEXT_HEIGHT
|
|
512
520
|
|
|
513
|
-
|
|
521
|
+
logger.debug(
|
|
522
|
+
f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})"
|
|
523
|
+
)
|
|
514
524
|
|
|
515
525
|
# Adjust bounds based on pin orientation
|
|
516
526
|
# Labels are placed at PIN ENDPOINT with offset, extending AWAY from the component
|
|
@@ -520,38 +530,42 @@ class SymbolBoundingBoxCalculator:
|
|
|
520
530
|
|
|
521
531
|
if angle == 0: # Pin points right - label extends LEFT from endpoint
|
|
522
532
|
label_x = end_x - offset - name_width
|
|
523
|
-
|
|
533
|
+
logger.debug(
|
|
534
|
+
f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
|
|
535
|
+
)
|
|
524
536
|
min_x = min(min_x, label_x)
|
|
525
537
|
elif angle == 180: # Pin points left - label extends RIGHT from endpoint
|
|
526
538
|
label_x = end_x + offset + name_width
|
|
527
|
-
|
|
539
|
+
logger.debug(
|
|
540
|
+
f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
|
|
541
|
+
)
|
|
528
542
|
max_x = max(max_x, label_x)
|
|
529
543
|
elif angle == 90: # Pin points up - label extends DOWN from endpoint
|
|
530
544
|
label_y = end_y - offset - name_height
|
|
531
|
-
|
|
545
|
+
logger.debug(
|
|
546
|
+
f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
|
|
547
|
+
)
|
|
532
548
|
min_y = min(min_y, label_y)
|
|
533
549
|
elif angle == 270: # Pin points down - label extends UP from endpoint
|
|
534
550
|
label_y = end_y + offset + name_height
|
|
535
|
-
|
|
551
|
+
logger.debug(
|
|
552
|
+
f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
|
|
553
|
+
)
|
|
536
554
|
max_y = max(max_y, label_y)
|
|
537
555
|
|
|
538
556
|
# Pin numbers are typically placed near the component body
|
|
539
557
|
if pin_number:
|
|
540
558
|
num_width = (
|
|
541
|
-
len(pin_number)
|
|
542
|
-
* cls.DEFAULT_PIN_NUMBER_SIZE
|
|
543
|
-
* cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
559
|
+
len(pin_number) * cls.DEFAULT_PIN_NUMBER_SIZE * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
|
|
544
560
|
)
|
|
545
561
|
# Add some space for the pin number
|
|
546
|
-
margin =
|
|
547
|
-
cls.DEFAULT_PIN_NUMBER_SIZE * 1.5
|
|
548
|
-
) # Increase margin for better spacing
|
|
562
|
+
margin = cls.DEFAULT_PIN_NUMBER_SIZE * 1.5 # Increase margin for better spacing
|
|
549
563
|
min_x -= margin
|
|
550
564
|
min_y -= margin
|
|
551
565
|
max_x += margin
|
|
552
566
|
max_y += margin
|
|
553
567
|
|
|
554
|
-
|
|
568
|
+
logger.debug(f" Pin bounds: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
|
|
555
569
|
return (min_x, min_y, max_x, max_y)
|
|
556
570
|
|
|
557
571
|
@classmethod
|
|
@@ -559,7 +573,6 @@ class SymbolBoundingBoxCalculator:
|
|
|
559
573
|
cls, pin: Dict[str, Any]
|
|
560
574
|
) -> Optional[Tuple[float, float, float, float]]:
|
|
561
575
|
"""Get bounding box for a pin WITHOUT labels - for placement calculations only."""
|
|
562
|
-
import sys
|
|
563
576
|
|
|
564
577
|
# Handle both formats: 'at' array or separate x/y/orientation
|
|
565
578
|
if "at" in pin:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core interfaces for KiCAD schematic API.
|
|
3
|
+
|
|
4
|
+
This module provides abstract interfaces for the main components of the system,
|
|
5
|
+
enabling better separation of concerns and testability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .parser import IElementParser, ISchematicParser
|
|
9
|
+
from .repository import ISchematicRepository
|
|
10
|
+
from .resolver import ISymbolResolver
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"IElementParser",
|
|
14
|
+
"ISchematicParser",
|
|
15
|
+
"ISchematicRepository",
|
|
16
|
+
"ISymbolResolver",
|
|
17
|
+
]
|