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