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
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Text Element Manager for KiCAD schematic text operations.
|
|
3
|
+
|
|
4
|
+
Handles all text-related elements including labels, hierarchical labels,
|
|
5
|
+
global labels, text annotations, and text boxes while managing positioning
|
|
6
|
+
and validation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
12
|
+
|
|
13
|
+
from ..types import Point
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TextElementManager:
|
|
19
|
+
"""
|
|
20
|
+
Manages text elements and labeling in KiCAD schematics.
|
|
21
|
+
|
|
22
|
+
Responsible for:
|
|
23
|
+
- Label creation and management (normal, hierarchical, global)
|
|
24
|
+
- Text annotation placement
|
|
25
|
+
- Text box management
|
|
26
|
+
- Text effect and styling
|
|
27
|
+
- Position validation and adjustment
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, schematic_data: Dict[str, Any]):
|
|
31
|
+
"""
|
|
32
|
+
Initialize TextElementManager.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
schematic_data: Reference to schematic data
|
|
36
|
+
"""
|
|
37
|
+
self._data = schematic_data
|
|
38
|
+
|
|
39
|
+
def add_label(
|
|
40
|
+
self,
|
|
41
|
+
text: str,
|
|
42
|
+
position: Union[Point, Tuple[float, float]],
|
|
43
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
44
|
+
uuid_str: Optional[str] = None,
|
|
45
|
+
rotation: float = 0,
|
|
46
|
+
size: Optional[float] = None,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Add a text label to the schematic.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
text: Label text content
|
|
53
|
+
position: Label position
|
|
54
|
+
effects: Text effects (size, font, etc.)
|
|
55
|
+
uuid_str: Optional UUID, generated if not provided
|
|
56
|
+
rotation: Label rotation in degrees (default 0)
|
|
57
|
+
size: Text size override (default from effects)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
UUID of created label
|
|
61
|
+
"""
|
|
62
|
+
if isinstance(position, tuple):
|
|
63
|
+
position = Point(position[0], position[1])
|
|
64
|
+
|
|
65
|
+
if uuid_str is None:
|
|
66
|
+
uuid_str = str(uuid.uuid4())
|
|
67
|
+
|
|
68
|
+
if effects is None:
|
|
69
|
+
effects = self._get_default_text_effects()
|
|
70
|
+
|
|
71
|
+
# Override size if provided
|
|
72
|
+
if size is not None:
|
|
73
|
+
effects = dict(effects) # Make a copy
|
|
74
|
+
if "font" not in effects:
|
|
75
|
+
effects["font"] = {}
|
|
76
|
+
effects["font"]["size"] = [size, size]
|
|
77
|
+
|
|
78
|
+
label_data = {
|
|
79
|
+
"uuid": uuid_str,
|
|
80
|
+
"text": text,
|
|
81
|
+
"at": [position.x, position.y, rotation], # KiCAD format: [x, y, rotation]
|
|
82
|
+
"effects": effects,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Add to schematic data
|
|
86
|
+
if "label" not in self._data:
|
|
87
|
+
self._data["label"] = []
|
|
88
|
+
self._data["label"].append(label_data)
|
|
89
|
+
|
|
90
|
+
logger.debug(f"Added label '{text}' at {position}")
|
|
91
|
+
return uuid_str
|
|
92
|
+
|
|
93
|
+
def add_hierarchical_label(
|
|
94
|
+
self,
|
|
95
|
+
text: str,
|
|
96
|
+
position: Union[Point, Tuple[float, float]],
|
|
97
|
+
shape: str = "input",
|
|
98
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
99
|
+
uuid_str: Optional[str] = None,
|
|
100
|
+
) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Add a hierarchical label (for sheet connections).
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
text: Label text
|
|
106
|
+
position: Label position
|
|
107
|
+
shape: Shape type (input, output, bidirectional, tri_state, passive)
|
|
108
|
+
effects: Text effects
|
|
109
|
+
uuid_str: Optional UUID
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
UUID of created hierarchical label
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(position, tuple):
|
|
115
|
+
position = Point(position[0], position[1])
|
|
116
|
+
|
|
117
|
+
if uuid_str is None:
|
|
118
|
+
uuid_str = str(uuid.uuid4())
|
|
119
|
+
|
|
120
|
+
if effects is None:
|
|
121
|
+
effects = self._get_default_text_effects()
|
|
122
|
+
|
|
123
|
+
valid_shapes = ["input", "output", "bidirectional", "tri_state", "passive"]
|
|
124
|
+
if shape not in valid_shapes:
|
|
125
|
+
logger.warning(f"Invalid hierarchical label shape: {shape}. Using 'input'")
|
|
126
|
+
shape = "input"
|
|
127
|
+
|
|
128
|
+
label_data = {
|
|
129
|
+
"uuid": uuid_str,
|
|
130
|
+
"text": text,
|
|
131
|
+
"shape": shape,
|
|
132
|
+
"at": [position.x, position.y, 0],
|
|
133
|
+
"effects": effects,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Add to schematic data
|
|
137
|
+
if "hierarchical_label" not in self._data:
|
|
138
|
+
self._data["hierarchical_label"] = []
|
|
139
|
+
self._data["hierarchical_label"].append(label_data)
|
|
140
|
+
|
|
141
|
+
logger.debug(f"Added hierarchical label '{text}' ({shape}) at {position}")
|
|
142
|
+
return uuid_str
|
|
143
|
+
|
|
144
|
+
def add_global_label(
|
|
145
|
+
self,
|
|
146
|
+
text: str,
|
|
147
|
+
position: Union[Point, Tuple[float, float]],
|
|
148
|
+
shape: str = "input",
|
|
149
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
150
|
+
uuid_str: Optional[str] = None,
|
|
151
|
+
) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Add a global label (for project-wide connections).
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
text: Label text
|
|
157
|
+
position: Label position
|
|
158
|
+
shape: Shape type
|
|
159
|
+
effects: Text effects
|
|
160
|
+
uuid_str: Optional UUID
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
UUID of created global label
|
|
164
|
+
"""
|
|
165
|
+
if isinstance(position, tuple):
|
|
166
|
+
position = Point(position[0], position[1])
|
|
167
|
+
|
|
168
|
+
if uuid_str is None:
|
|
169
|
+
uuid_str = str(uuid.uuid4())
|
|
170
|
+
|
|
171
|
+
if effects is None:
|
|
172
|
+
effects = self._get_default_text_effects()
|
|
173
|
+
|
|
174
|
+
valid_shapes = ["input", "output", "bidirectional", "tri_state", "passive"]
|
|
175
|
+
if shape not in valid_shapes:
|
|
176
|
+
logger.warning(f"Invalid global label shape: {shape}. Using 'input'")
|
|
177
|
+
shape = "input"
|
|
178
|
+
|
|
179
|
+
label_data = {
|
|
180
|
+
"uuid": uuid_str,
|
|
181
|
+
"text": text,
|
|
182
|
+
"shape": shape,
|
|
183
|
+
"at": [position.x, position.y, 0],
|
|
184
|
+
"effects": effects,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Add to schematic data
|
|
188
|
+
if "global_label" not in self._data:
|
|
189
|
+
self._data["global_label"] = []
|
|
190
|
+
self._data["global_label"].append(label_data)
|
|
191
|
+
|
|
192
|
+
logger.debug(f"Added global label '{text}' ({shape}) at {position}")
|
|
193
|
+
return uuid_str
|
|
194
|
+
|
|
195
|
+
def add_text(
|
|
196
|
+
self,
|
|
197
|
+
text: str,
|
|
198
|
+
position: Union[Point, Tuple[float, float]],
|
|
199
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
200
|
+
uuid_str: Optional[str] = None,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Add free text annotation.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
text: Text content
|
|
207
|
+
position: Text position
|
|
208
|
+
effects: Text effects
|
|
209
|
+
uuid_str: Optional UUID
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
UUID of created text
|
|
213
|
+
"""
|
|
214
|
+
if isinstance(position, tuple):
|
|
215
|
+
position = Point(position[0], position[1])
|
|
216
|
+
|
|
217
|
+
if uuid_str is None:
|
|
218
|
+
uuid_str = str(uuid.uuid4())
|
|
219
|
+
|
|
220
|
+
if effects is None:
|
|
221
|
+
effects = self._get_default_text_effects()
|
|
222
|
+
|
|
223
|
+
text_data = {
|
|
224
|
+
"uuid": uuid_str,
|
|
225
|
+
"text": text,
|
|
226
|
+
"at": [position.x, position.y, 0],
|
|
227
|
+
"effects": effects,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Add to schematic data
|
|
231
|
+
if "text" not in self._data:
|
|
232
|
+
self._data["text"] = []
|
|
233
|
+
self._data["text"].append(text_data)
|
|
234
|
+
|
|
235
|
+
logger.debug(f"Added text '{text}' at {position}")
|
|
236
|
+
return uuid_str
|
|
237
|
+
|
|
238
|
+
def add_text_box(
|
|
239
|
+
self,
|
|
240
|
+
text: str,
|
|
241
|
+
position: Union[Point, Tuple[float, float]],
|
|
242
|
+
size: Union[Point, Tuple[float, float]],
|
|
243
|
+
rotation: float = 0.0,
|
|
244
|
+
font_size: float = 1.27,
|
|
245
|
+
margins: Optional[Tuple[float, float, float, float]] = None,
|
|
246
|
+
stroke_width: Optional[float] = None,
|
|
247
|
+
stroke_type: str = "solid",
|
|
248
|
+
fill_type: str = "none",
|
|
249
|
+
justify_horizontal: str = "left",
|
|
250
|
+
justify_vertical: str = "top",
|
|
251
|
+
exclude_from_sim: bool = False,
|
|
252
|
+
effects: Optional[Dict[str, Any]] = None,
|
|
253
|
+
stroke: Optional[Dict[str, Any]] = None,
|
|
254
|
+
uuid_str: Optional[str] = None,
|
|
255
|
+
) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Add a text box with border.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
text: Text content
|
|
261
|
+
position: Top-left position
|
|
262
|
+
size: Box size (width, height)
|
|
263
|
+
rotation: Text rotation in degrees
|
|
264
|
+
font_size: Text font size
|
|
265
|
+
margins: Box margins (top, bottom, left, right)
|
|
266
|
+
stroke_width: Border stroke width
|
|
267
|
+
stroke_type: Border stroke type (solid, dash, etc.)
|
|
268
|
+
fill_type: Fill type (none, outline, background)
|
|
269
|
+
justify_horizontal: Horizontal justification
|
|
270
|
+
justify_vertical: Vertical justification
|
|
271
|
+
exclude_from_sim: Whether to exclude from simulation
|
|
272
|
+
effects: Text effects (legacy, overrides font_size and justify if provided)
|
|
273
|
+
stroke: Border stroke settings (legacy, overrides stroke_width/type if provided)
|
|
274
|
+
uuid_str: Optional UUID
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
UUID of created text box
|
|
278
|
+
"""
|
|
279
|
+
if isinstance(position, tuple):
|
|
280
|
+
position = Point(position[0], position[1])
|
|
281
|
+
if isinstance(size, tuple):
|
|
282
|
+
size = Point(size[0], size[1])
|
|
283
|
+
|
|
284
|
+
if uuid_str is None:
|
|
285
|
+
uuid_str = str(uuid.uuid4())
|
|
286
|
+
|
|
287
|
+
if margins is None:
|
|
288
|
+
margins = (0.9525, 0.9525, 0.9525, 0.9525)
|
|
289
|
+
|
|
290
|
+
if stroke_width is None:
|
|
291
|
+
stroke_width = 0
|
|
292
|
+
|
|
293
|
+
# Build text_box_data matching parser format
|
|
294
|
+
text_box_data = {
|
|
295
|
+
"uuid": uuid_str,
|
|
296
|
+
"text": text,
|
|
297
|
+
"exclude_from_sim": exclude_from_sim,
|
|
298
|
+
"position": {"x": position.x, "y": position.y},
|
|
299
|
+
"rotation": rotation,
|
|
300
|
+
"size": {"width": size.x, "height": size.y},
|
|
301
|
+
"margins": margins,
|
|
302
|
+
"stroke_width": stroke_width,
|
|
303
|
+
"stroke_type": stroke_type,
|
|
304
|
+
"fill_type": fill_type,
|
|
305
|
+
"font_size": font_size,
|
|
306
|
+
"justify_horizontal": justify_horizontal,
|
|
307
|
+
"justify_vertical": justify_vertical,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Add to schematic data (note: plural "text_boxes")
|
|
311
|
+
if "text_boxes" not in self._data:
|
|
312
|
+
self._data["text_boxes"] = []
|
|
313
|
+
self._data["text_boxes"].append(text_box_data)
|
|
314
|
+
|
|
315
|
+
logger.debug(f"Added text box '{text}' at {position}, size {size}")
|
|
316
|
+
return uuid_str
|
|
317
|
+
|
|
318
|
+
def remove_label(self, uuid_str: str) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Remove a label by UUID.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
uuid_str: UUID of label to remove
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if label was removed, False if not found
|
|
327
|
+
"""
|
|
328
|
+
return self._remove_text_element_by_uuid("label", uuid_str)
|
|
329
|
+
|
|
330
|
+
def remove_hierarchical_label(self, uuid_str: str) -> bool:
|
|
331
|
+
"""
|
|
332
|
+
Remove a hierarchical label by UUID.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
uuid_str: UUID of label to remove
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
True if label was removed, False if not found
|
|
339
|
+
"""
|
|
340
|
+
return self._remove_text_element_by_uuid("hierarchical_label", uuid_str)
|
|
341
|
+
|
|
342
|
+
def remove_global_label(self, uuid_str: str) -> bool:
|
|
343
|
+
"""
|
|
344
|
+
Remove a global label by UUID.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
uuid_str: UUID of label to remove
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
True if label was removed, False if not found
|
|
351
|
+
"""
|
|
352
|
+
return self._remove_text_element_by_uuid("global_label", uuid_str)
|
|
353
|
+
|
|
354
|
+
def remove_text(self, uuid_str: str) -> bool:
|
|
355
|
+
"""
|
|
356
|
+
Remove a text element by UUID.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
uuid_str: UUID of text to remove
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
True if text was removed, False if not found
|
|
363
|
+
"""
|
|
364
|
+
return self._remove_text_element_by_uuid("text", uuid_str)
|
|
365
|
+
|
|
366
|
+
def remove_text_box(self, uuid_str: str) -> bool:
|
|
367
|
+
"""
|
|
368
|
+
Remove a text box by UUID.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
uuid_str: UUID of text box to remove
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
True if text box was removed, False if not found
|
|
375
|
+
"""
|
|
376
|
+
return self._remove_text_element_by_uuid("text_boxes", uuid_str)
|
|
377
|
+
|
|
378
|
+
def get_labels_at_position(
|
|
379
|
+
self, position: Union[Point, Tuple[float, float]], tolerance: float = 1.0
|
|
380
|
+
) -> List[Dict[str, Any]]:
|
|
381
|
+
"""
|
|
382
|
+
Get all labels near a position.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
position: Search position
|
|
386
|
+
tolerance: Position tolerance
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
List of matching label data
|
|
390
|
+
"""
|
|
391
|
+
if isinstance(position, tuple):
|
|
392
|
+
position = Point(position[0], position[1])
|
|
393
|
+
|
|
394
|
+
matches = []
|
|
395
|
+
for label_type in ["label", "hierarchical_label", "global_label"]:
|
|
396
|
+
labels = self._data.get(label_type, [])
|
|
397
|
+
for label in labels:
|
|
398
|
+
label_pos = Point(label["at"][0], label["at"][1])
|
|
399
|
+
if label_pos.distance_to(position) <= tolerance:
|
|
400
|
+
matches.append(
|
|
401
|
+
{
|
|
402
|
+
"type": label_type,
|
|
403
|
+
"data": label,
|
|
404
|
+
"uuid": label.get("uuid"),
|
|
405
|
+
"text": label.get("text"),
|
|
406
|
+
"position": label_pos,
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
return matches
|
|
411
|
+
|
|
412
|
+
def update_text_effects(self, uuid_str: str, effects: Dict[str, Any]) -> bool:
|
|
413
|
+
"""
|
|
414
|
+
Update text effects for any text element.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
uuid_str: UUID of text element
|
|
418
|
+
effects: New text effects
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
True if updated, False if not found
|
|
422
|
+
"""
|
|
423
|
+
for text_type in ["label", "hierarchical_label", "global_label", "text", "text_boxes"]:
|
|
424
|
+
elements = self._data.get(text_type, [])
|
|
425
|
+
for element in elements:
|
|
426
|
+
if element.get("uuid") == uuid_str:
|
|
427
|
+
element["effects"] = effects
|
|
428
|
+
logger.debug(f"Updated text effects for {text_type} {uuid_str}")
|
|
429
|
+
return True
|
|
430
|
+
|
|
431
|
+
logger.warning(f"Text element not found for UUID: {uuid_str}")
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
def list_all_text_elements(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
435
|
+
"""
|
|
436
|
+
Get all text elements in the schematic.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Dictionary with text element types and their data
|
|
440
|
+
"""
|
|
441
|
+
result = {}
|
|
442
|
+
for text_type in ["label", "hierarchical_label", "global_label", "text", "text_boxes"]:
|
|
443
|
+
elements = self._data.get(text_type, [])
|
|
444
|
+
result[text_type] = [
|
|
445
|
+
{
|
|
446
|
+
"uuid": elem.get("uuid"),
|
|
447
|
+
"text": elem.get("text"),
|
|
448
|
+
"position": (
|
|
449
|
+
Point(elem["at"][0], elem["at"][1])
|
|
450
|
+
if "at" in elem
|
|
451
|
+
else (
|
|
452
|
+
Point(elem["position"]["x"], elem["position"]["y"])
|
|
453
|
+
if "position" in elem
|
|
454
|
+
else None
|
|
455
|
+
)
|
|
456
|
+
),
|
|
457
|
+
"data": elem,
|
|
458
|
+
}
|
|
459
|
+
for elem in elements
|
|
460
|
+
]
|
|
461
|
+
|
|
462
|
+
return result
|
|
463
|
+
|
|
464
|
+
def get_text_statistics(self) -> Dict[str, Any]:
|
|
465
|
+
"""
|
|
466
|
+
Get statistics about text elements.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Dictionary with text element statistics
|
|
470
|
+
"""
|
|
471
|
+
stats = {}
|
|
472
|
+
total_elements = 0
|
|
473
|
+
|
|
474
|
+
for text_type in ["label", "hierarchical_label", "global_label", "text", "text_boxes"]:
|
|
475
|
+
count = len(self._data.get(text_type, []))
|
|
476
|
+
stats[text_type] = count
|
|
477
|
+
total_elements += count
|
|
478
|
+
|
|
479
|
+
stats["total_text_elements"] = total_elements
|
|
480
|
+
return stats
|
|
481
|
+
|
|
482
|
+
def _remove_text_element_by_uuid(self, element_type: str, uuid_str: str) -> bool:
|
|
483
|
+
"""Remove text element by UUID from specified type."""
|
|
484
|
+
elements = self._data.get(element_type, [])
|
|
485
|
+
for i, element in enumerate(elements):
|
|
486
|
+
if element.get("uuid") == uuid_str:
|
|
487
|
+
del elements[i]
|
|
488
|
+
logger.debug(f"Removed {element_type}: {uuid_str}")
|
|
489
|
+
return True
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
def _get_default_text_effects(self) -> Dict[str, Any]:
|
|
493
|
+
"""Get default text effects configuration."""
|
|
494
|
+
return {"font": {"size": [1.27, 1.27], "thickness": 0.254}, "justify": ["left"]}
|
|
495
|
+
|
|
496
|
+
def validate_text_positions(self) -> List[str]:
|
|
497
|
+
"""
|
|
498
|
+
Validate text element positions for overlaps and readability.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
List of validation warnings
|
|
502
|
+
"""
|
|
503
|
+
warnings = []
|
|
504
|
+
all_elements = []
|
|
505
|
+
|
|
506
|
+
# Collect all text elements with positions
|
|
507
|
+
for text_type in ["label", "hierarchical_label", "global_label", "text", "text_boxes"]:
|
|
508
|
+
elements = self._data.get(text_type, [])
|
|
509
|
+
for element in elements:
|
|
510
|
+
if "at" in element:
|
|
511
|
+
position = Point(element["at"][0], element["at"][1])
|
|
512
|
+
elif "position" in element:
|
|
513
|
+
position = Point(element["position"]["x"], element["position"]["y"])
|
|
514
|
+
else:
|
|
515
|
+
continue
|
|
516
|
+
all_elements.append(
|
|
517
|
+
{
|
|
518
|
+
"type": text_type,
|
|
519
|
+
"position": position,
|
|
520
|
+
"text": element.get("text", ""),
|
|
521
|
+
"uuid": element.get("uuid"),
|
|
522
|
+
}
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Check for overlapping elements
|
|
526
|
+
overlap_threshold = 2.0 # Minimum distance
|
|
527
|
+
for i, elem1 in enumerate(all_elements):
|
|
528
|
+
for elem2 in all_elements[i + 1 :]:
|
|
529
|
+
distance = elem1["position"].distance_to(elem2["position"])
|
|
530
|
+
if distance < overlap_threshold:
|
|
531
|
+
warnings.append(
|
|
532
|
+
f"Text elements '{elem1['text']}' and '{elem2['text']}' "
|
|
533
|
+
f"are very close ({distance:.2f} units apart)"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return warnings
|