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.

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