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,580 @@
1
+ """
2
+ Graphics Manager for KiCAD schematic graphic elements.
3
+
4
+ Handles geometric shapes, drawing elements, and visual annotations including
5
+ rectangles, circles, polylines, arcs, and images while managing styling
6
+ and positioning.
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 GraphicsManager(BaseManager):
20
+ """
21
+ Manages graphic elements and drawing shapes in KiCAD schematics.
22
+
23
+ Responsible for:
24
+ - Geometric shape creation (rectangles, circles, arcs)
25
+ - Polyline and path management
26
+ - Image placement and scaling
27
+ - Stroke and fill styling
28
+ - Layer management for graphics
29
+ """
30
+
31
+ def __init__(self, schematic_data: Dict[str, Any]):
32
+ """
33
+ Initialize GraphicsManager.
34
+
35
+ Args:
36
+ schematic_data: Reference to schematic data
37
+ """
38
+ super().__init__(schematic_data)
39
+
40
+ def add_rectangle(
41
+ self,
42
+ start: Union[Point, Tuple[float, float]],
43
+ end: Union[Point, Tuple[float, float]],
44
+ stroke: Optional[Dict[str, Any]] = None,
45
+ fill: Optional[Dict[str, Any]] = None,
46
+ uuid_str: Optional[str] = None,
47
+ ) -> str:
48
+ """
49
+ Add a rectangle to the schematic.
50
+
51
+ Args:
52
+ start: Top-left corner position
53
+ end: Bottom-right corner position
54
+ stroke: Stroke properties (width, type, color)
55
+ fill: Fill properties (color, type)
56
+ uuid_str: Optional UUID
57
+
58
+ Returns:
59
+ UUID of created rectangle
60
+ """
61
+ if isinstance(start, tuple):
62
+ start = Point(start[0], start[1])
63
+ if isinstance(end, tuple):
64
+ end = Point(end[0], end[1])
65
+
66
+ if uuid_str is None:
67
+ uuid_str = str(uuid.uuid4())
68
+
69
+ if stroke is None:
70
+ stroke = self._get_default_stroke()
71
+
72
+ # Convert to parser format (flat keys, dict positions)
73
+ rectangle_data = {
74
+ "uuid": uuid_str,
75
+ "start": {"x": start.x, "y": start.y},
76
+ "end": {"x": end.x, "y": end.y},
77
+ "stroke_width": stroke.get("width", 0.127),
78
+ "stroke_type": stroke.get("type", "solid"),
79
+ }
80
+
81
+ # Add stroke color if provided
82
+ if "color" in stroke:
83
+ rectangle_data["stroke_color"] = stroke["color"]
84
+
85
+ # Add fill type if provided
86
+ if fill is not None:
87
+ rectangle_data["fill_type"] = fill.get("type", "none")
88
+ # Add fill color if provided
89
+ if "color" in fill:
90
+ rectangle_data["fill_color"] = fill["color"]
91
+ else:
92
+ rectangle_data["fill_type"] = "none"
93
+
94
+ # Add to schematic data
95
+ if "rectangles" not in self._data:
96
+ self._data["rectangles"] = []
97
+ self._data["rectangles"].append(rectangle_data)
98
+
99
+ logger.debug(f"Added rectangle from {start} to {end}")
100
+ return uuid_str
101
+
102
+ def add_circle(
103
+ self,
104
+ center: Union[Point, Tuple[float, float]],
105
+ radius: float,
106
+ stroke: Optional[Dict[str, Any]] = None,
107
+ fill: Optional[Dict[str, Any]] = None,
108
+ uuid_str: Optional[str] = None,
109
+ ) -> str:
110
+ """
111
+ Add a circle to the schematic.
112
+
113
+ Args:
114
+ center: Center position
115
+ radius: Circle radius
116
+ stroke: Stroke properties
117
+ fill: Fill properties
118
+ uuid_str: Optional UUID
119
+
120
+ Returns:
121
+ UUID of created circle
122
+ """
123
+ if isinstance(center, tuple):
124
+ center = Point(center[0], center[1])
125
+
126
+ if uuid_str is None:
127
+ uuid_str = str(uuid.uuid4())
128
+
129
+ if stroke is None:
130
+ stroke = self._get_default_stroke()
131
+
132
+ circle_data = {
133
+ "uuid": uuid_str,
134
+ "center": [center.x, center.y],
135
+ "radius": radius,
136
+ "stroke": stroke,
137
+ }
138
+
139
+ if fill is not None:
140
+ circle_data["fill"] = fill
141
+
142
+ # Add to schematic data
143
+ if "circle" not in self._data:
144
+ self._data["circle"] = []
145
+ self._data["circle"].append(circle_data)
146
+
147
+ logger.debug(f"Added circle at {center} with radius {radius}")
148
+ return uuid_str
149
+
150
+ def add_arc(
151
+ self,
152
+ start: Union[Point, Tuple[float, float]],
153
+ mid: Union[Point, Tuple[float, float]],
154
+ end: Union[Point, Tuple[float, float]],
155
+ stroke: Optional[Dict[str, Any]] = None,
156
+ uuid_str: Optional[str] = None,
157
+ ) -> str:
158
+ """
159
+ Add an arc to the schematic (defined by three points).
160
+
161
+ Args:
162
+ start: Arc start point
163
+ mid: Arc midpoint
164
+ end: Arc end point
165
+ stroke: Stroke properties
166
+ uuid_str: Optional UUID
167
+
168
+ Returns:
169
+ UUID of created arc
170
+ """
171
+ if isinstance(start, tuple):
172
+ start = Point(start[0], start[1])
173
+ if isinstance(mid, tuple):
174
+ mid = Point(mid[0], mid[1])
175
+ if isinstance(end, tuple):
176
+ end = Point(end[0], end[1])
177
+
178
+ if uuid_str is None:
179
+ uuid_str = str(uuid.uuid4())
180
+
181
+ if stroke is None:
182
+ stroke = self._get_default_stroke()
183
+
184
+ arc_data = {
185
+ "uuid": uuid_str,
186
+ "start": [start.x, start.y],
187
+ "mid": [mid.x, mid.y],
188
+ "end": [end.x, end.y],
189
+ "stroke": stroke,
190
+ }
191
+
192
+ # Add to schematic data
193
+ if "arc" not in self._data:
194
+ self._data["arc"] = []
195
+ self._data["arc"].append(arc_data)
196
+
197
+ logger.debug(f"Added arc from {start} through {mid} to {end}")
198
+ return uuid_str
199
+
200
+ def add_polyline(
201
+ self,
202
+ points: List[Union[Point, Tuple[float, float]]],
203
+ stroke: Optional[Dict[str, Any]] = None,
204
+ fill: Optional[Dict[str, Any]] = None,
205
+ uuid_str: Optional[str] = None,
206
+ ) -> str:
207
+ """
208
+ Add a polyline (multi-segment line) to the schematic.
209
+
210
+ Args:
211
+ points: List of points defining the polyline
212
+ stroke: Stroke properties
213
+ fill: Fill properties (for closed polylines)
214
+ uuid_str: Optional UUID
215
+
216
+ Returns:
217
+ UUID of created polyline
218
+ """
219
+ if len(points) < 2:
220
+ raise ValueError("Polyline must have at least 2 points")
221
+
222
+ # Convert tuples to Points
223
+ converted_points = []
224
+ for point in points:
225
+ if isinstance(point, tuple):
226
+ converted_points.append(Point(point[0], point[1]))
227
+ else:
228
+ converted_points.append(point)
229
+
230
+ if uuid_str is None:
231
+ uuid_str = str(uuid.uuid4())
232
+
233
+ if stroke is None:
234
+ stroke = self._get_default_stroke()
235
+
236
+ polyline_data = {
237
+ "uuid": uuid_str,
238
+ "pts": [[pt.x, pt.y] for pt in converted_points],
239
+ "stroke": stroke,
240
+ }
241
+
242
+ if fill is not None:
243
+ polyline_data["fill"] = fill
244
+
245
+ # Add to schematic data
246
+ if "polyline" not in self._data:
247
+ self._data["polyline"] = []
248
+ self._data["polyline"].append(polyline_data)
249
+
250
+ logger.debug(f"Added polyline with {len(points)} points")
251
+ return uuid_str
252
+
253
+ def add_image(
254
+ self,
255
+ position: Union[Point, Tuple[float, float]],
256
+ scale: float = 1.0,
257
+ image_data: Optional[str] = None,
258
+ uuid_str: Optional[str] = None,
259
+ ) -> str:
260
+ """
261
+ Add an image to the schematic.
262
+
263
+ Args:
264
+ position: Image position
265
+ scale: Image scale factor
266
+ image_data: Base64 encoded image data (optional)
267
+ uuid_str: Optional UUID
268
+
269
+ Returns:
270
+ UUID of created image
271
+ """
272
+ if isinstance(position, tuple):
273
+ position = Point(position[0], position[1])
274
+
275
+ if uuid_str is None:
276
+ uuid_str = str(uuid.uuid4())
277
+
278
+ # Store in parser format (position as dict)
279
+ image_element = {
280
+ "uuid": uuid_str,
281
+ "position": {"x": position.x, "y": position.y},
282
+ "scale": scale,
283
+ }
284
+
285
+ if image_data is not None:
286
+ image_element["data"] = image_data
287
+
288
+ # Add to schematic data
289
+ if "images" not in self._data:
290
+ self._data["images"] = []
291
+ self._data["images"].append(image_element)
292
+
293
+ logger.debug(f"Added image at {position} with scale {scale}")
294
+ return uuid_str
295
+
296
+ def remove_rectangle(self, uuid_str: str) -> bool:
297
+ """
298
+ Remove a rectangle by UUID.
299
+
300
+ Args:
301
+ uuid_str: UUID of rectangle to remove
302
+
303
+ Returns:
304
+ True if removed, False if not found
305
+ """
306
+ return self._remove_graphic_element("rectangle", uuid_str)
307
+
308
+ def remove_circle(self, uuid_str: str) -> bool:
309
+ """
310
+ Remove a circle by UUID.
311
+
312
+ Args:
313
+ uuid_str: UUID of circle to remove
314
+
315
+ Returns:
316
+ True if removed, False if not found
317
+ """
318
+ return self._remove_graphic_element("circle", uuid_str)
319
+
320
+ def remove_arc(self, uuid_str: str) -> bool:
321
+ """
322
+ Remove an arc by UUID.
323
+
324
+ Args:
325
+ uuid_str: UUID of arc to remove
326
+
327
+ Returns:
328
+ True if removed, False if not found
329
+ """
330
+ return self._remove_graphic_element("arc", uuid_str)
331
+
332
+ def remove_polyline(self, uuid_str: str) -> bool:
333
+ """
334
+ Remove a polyline by UUID.
335
+
336
+ Args:
337
+ uuid_str: UUID of polyline to remove
338
+
339
+ Returns:
340
+ True if removed, False if not found
341
+ """
342
+ return self._remove_graphic_element("polyline", uuid_str)
343
+
344
+ def remove_image(self, uuid_str: str) -> bool:
345
+ """
346
+ Remove an image by UUID.
347
+
348
+ Args:
349
+ uuid_str: UUID of image to remove
350
+
351
+ Returns:
352
+ True if removed, False if not found
353
+ """
354
+ return self._remove_graphic_element("image", uuid_str)
355
+
356
+ def update_stroke(self, uuid_str: str, stroke: Dict[str, Any]) -> bool:
357
+ """
358
+ Update stroke properties for a graphic element.
359
+
360
+ Args:
361
+ uuid_str: UUID of element
362
+ stroke: New stroke properties
363
+
364
+ Returns:
365
+ True if updated, False if not found
366
+ """
367
+ for element_type in ["rectangle", "circle", "arc", "polyline"]:
368
+ elements = self._data.get(element_type, [])
369
+ for element in elements:
370
+ if element.get("uuid") == uuid_str:
371
+ element["stroke"] = stroke
372
+ logger.debug(f"Updated stroke for {element_type} {uuid_str}")
373
+ return True
374
+
375
+ logger.warning(f"Graphic element not found for stroke update: {uuid_str}")
376
+ return False
377
+
378
+ def update_fill(self, uuid_str: str, fill: Dict[str, Any]) -> bool:
379
+ """
380
+ Update fill properties for a graphic element.
381
+
382
+ Args:
383
+ uuid_str: UUID of element
384
+ fill: New fill properties
385
+
386
+ Returns:
387
+ True if updated, False if not found
388
+ """
389
+ for element_type in ["rectangle", "circle", "polyline"]:
390
+ elements = self._data.get(element_type, [])
391
+ for element in elements:
392
+ if element.get("uuid") == uuid_str:
393
+ element["fill"] = fill
394
+ logger.debug(f"Updated fill for {element_type} {uuid_str}")
395
+ return True
396
+
397
+ logger.warning(f"Graphic element not found for fill update: {uuid_str}")
398
+ return False
399
+
400
+ def get_graphics_in_area(
401
+ self,
402
+ area_start: Union[Point, Tuple[float, float]],
403
+ area_end: Union[Point, Tuple[float, float]],
404
+ ) -> List[Dict[str, Any]]:
405
+ """
406
+ Get all graphic elements within a specified area.
407
+
408
+ Args:
409
+ area_start: Area top-left corner
410
+ area_end: Area bottom-right corner
411
+
412
+ Returns:
413
+ List of graphic elements in the area
414
+ """
415
+ if isinstance(area_start, tuple):
416
+ area_start = Point(area_start[0], area_start[1])
417
+ if isinstance(area_end, tuple):
418
+ area_end = Point(area_end[0], area_end[1])
419
+
420
+ result = []
421
+
422
+ # Check rectangles
423
+ rectangles = self._data.get("rectangle", [])
424
+ for rect in rectangles:
425
+ rect_start = Point(rect["start"][0], rect["start"][1])
426
+ rect_end = Point(rect["end"][0], rect["end"][1])
427
+ if self._rectangles_overlap(area_start, area_end, rect_start, rect_end):
428
+ result.append({"type": "rectangle", "data": rect})
429
+
430
+ # Check circles
431
+ circles = self._data.get("circle", [])
432
+ for circle in circles:
433
+ center = Point(circle["center"][0], circle["center"][1])
434
+ radius = circle["radius"]
435
+ if self._circle_in_area(center, radius, area_start, area_end):
436
+ result.append({"type": "circle", "data": circle})
437
+
438
+ # Check polylines
439
+ polylines = self._data.get("polyline", [])
440
+ for polyline in polylines:
441
+ if self._polyline_in_area(polyline["pts"], area_start, area_end):
442
+ result.append({"type": "polyline", "data": polyline})
443
+
444
+ # Check arcs
445
+ arcs = self._data.get("arc", [])
446
+ for arc in arcs:
447
+ arc_start = Point(arc["start"][0], arc["start"][1])
448
+ arc_end = Point(arc["end"][0], arc["end"][1])
449
+ if self._point_in_area(arc_start, area_start, area_end) or self._point_in_area(
450
+ arc_end, area_start, area_end
451
+ ):
452
+ result.append({"type": "arc", "data": arc})
453
+
454
+ # Check images
455
+ images = self._data.get("image", [])
456
+ for image in images:
457
+ pos = Point(image["at"][0], image["at"][1])
458
+ if self._point_in_area(pos, area_start, area_end):
459
+ result.append({"type": "image", "data": image})
460
+
461
+ return result
462
+
463
+ def list_all_graphics(self) -> Dict[str, List[Dict[str, Any]]]:
464
+ """
465
+ Get all graphic elements in the schematic.
466
+
467
+ Returns:
468
+ Dictionary with graphic element types and their data
469
+ """
470
+ result = {}
471
+ for element_type in ["rectangle", "circle", "arc", "polyline", "image"]:
472
+ elements = self._data.get(element_type, [])
473
+ result[element_type] = [{"uuid": elem.get("uuid"), "data": elem} for elem in elements]
474
+
475
+ return result
476
+
477
+ def get_graphics_statistics(self) -> Dict[str, Any]:
478
+ """
479
+ Get statistics about graphic elements.
480
+
481
+ Returns:
482
+ Dictionary with graphics statistics
483
+ """
484
+ stats = {}
485
+ total_elements = 0
486
+
487
+ for element_type in ["rectangle", "circle", "arc", "polyline", "image"]:
488
+ count = len(self._data.get(element_type, []))
489
+ stats[element_type] = count
490
+ total_elements += count
491
+
492
+ stats["total_graphics"] = total_elements
493
+ return stats
494
+
495
+ def _remove_graphic_element(self, element_type: str, uuid_str: str) -> bool:
496
+ """Remove graphic element by UUID from specified type."""
497
+ elements = self._data.get(element_type, [])
498
+ for i, element in enumerate(elements):
499
+ if element.get("uuid") == uuid_str:
500
+ del elements[i]
501
+ logger.debug(f"Removed {element_type}: {uuid_str}")
502
+ return True
503
+ return False
504
+
505
+ def _get_default_stroke(self) -> Dict[str, Any]:
506
+ """Get default stroke properties."""
507
+ return {"width": 0.254, "type": "default"}
508
+
509
+ def _rectangles_overlap(
510
+ self, area_start: Point, area_end: Point, rect_start: Point, rect_end: Point
511
+ ) -> bool:
512
+ """Check if two rectangles overlap."""
513
+ return not (
514
+ area_end.x < rect_start.x
515
+ or area_start.x > rect_end.x
516
+ or area_end.y < rect_start.y
517
+ or area_start.y > rect_end.y
518
+ )
519
+
520
+ def _circle_in_area(
521
+ self, center: Point, radius: float, area_start: Point, area_end: Point
522
+ ) -> bool:
523
+ """Check if circle intersects with area."""
524
+ # Check if circle center is in area or if circle overlaps area bounds
525
+ if self._point_in_area(center, area_start, area_end):
526
+ return True
527
+
528
+ # Check if circle intersects with area edges
529
+ closest_x = max(area_start.x, min(center.x, area_end.x))
530
+ closest_y = max(area_start.y, min(center.y, area_end.y))
531
+ distance = Point(closest_x, closest_y).distance_to(center)
532
+
533
+ return distance <= radius
534
+
535
+ def _polyline_in_area(
536
+ self, points: List[List[float]], area_start: Point, area_end: Point
537
+ ) -> bool:
538
+ """Check if any part of polyline is in area."""
539
+ for point_coords in points:
540
+ point = Point(point_coords[0], point_coords[1])
541
+ if self._point_in_area(point, area_start, area_end):
542
+ return True
543
+ return False
544
+
545
+ def _point_in_area(self, point: Point, area_start: Point, area_end: Point) -> bool:
546
+ """Check if point is within area."""
547
+ return area_start.x <= point.x <= area_end.x and area_start.y <= point.y <= area_end.y
548
+
549
+ def validate_graphics(self) -> List[str]:
550
+ """
551
+ Validate graphic elements for consistency and correctness.
552
+
553
+ Returns:
554
+ List of validation warnings
555
+ """
556
+ warnings = []
557
+
558
+ # Check rectangles
559
+ rectangles = self._data.get("rectangle", [])
560
+ for rect in rectangles:
561
+ start = Point(rect["start"][0], rect["start"][1])
562
+ end = Point(rect["end"][0], rect["end"][1])
563
+ if start.x >= end.x or start.y >= end.y:
564
+ warnings.append(f"Rectangle {rect.get('uuid')} has invalid dimensions")
565
+
566
+ # Check circles
567
+ circles = self._data.get("circle", [])
568
+ for circle in circles:
569
+ radius = circle.get("radius", 0)
570
+ if radius <= 0:
571
+ warnings.append(f"Circle {circle.get('uuid')} has invalid radius: {radius}")
572
+
573
+ # Check polylines
574
+ polylines = self._data.get("polyline", [])
575
+ for polyline in polylines:
576
+ points = polyline.get("pts", [])
577
+ if len(points) < 2:
578
+ warnings.append(f"Polyline {polyline.get('uuid')} has too few points")
579
+
580
+ return warnings