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.

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  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 +348 -0
  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 +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.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.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,454 @@
1
+ """
2
+ Sheet Manager for KiCAD hierarchical sheet operations.
3
+
4
+ Handles hierarchical sheet management, sheet pin connections, and
5
+ multi-sheet project coordination while maintaining sheet instance tracking.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from pathlib import Path
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 SheetManager:
19
+ """
20
+ Manages hierarchical sheets and multi-sheet project coordination.
21
+
22
+ Responsible for:
23
+ - Sheet creation and management
24
+ - Sheet pin connections
25
+ - Sheet instance tracking
26
+ - Hierarchical navigation
27
+ - Sheet file references
28
+ """
29
+
30
+ def __init__(self, schematic_data: Dict[str, Any]):
31
+ """
32
+ Initialize SheetManager.
33
+
34
+ Args:
35
+ schematic_data: Reference to schematic data
36
+ """
37
+ self._data = schematic_data
38
+
39
+ def add_sheet(
40
+ self,
41
+ name: str,
42
+ filename: str,
43
+ position: Union[Point, Tuple[float, float]],
44
+ size: Union[Point, Tuple[float, float]],
45
+ uuid_str: Optional[str] = None,
46
+ sheet_pins: Optional[List[Dict[str, Any]]] = None,
47
+ stroke_width: Optional[float] = None,
48
+ stroke_type: str = "solid",
49
+ project_name: Optional[str] = None,
50
+ page_number: Optional[str] = None,
51
+ ) -> str:
52
+ """
53
+ Add a hierarchical sheet to the schematic.
54
+
55
+ Args:
56
+ name: Sheet name/title
57
+ filename: Referenced schematic filename
58
+ position: Sheet position (top-left corner)
59
+ size: Sheet size (width, height)
60
+ uuid_str: Optional UUID
61
+ sheet_pins: Optional list of sheet pins
62
+ stroke_width: Border stroke width
63
+ stroke_type: Border stroke type (solid, dashed, etc.)
64
+ project_name: Project name for this sheet
65
+ page_number: Page number for this sheet
66
+
67
+ Returns:
68
+ UUID of created sheet
69
+ """
70
+ if isinstance(position, tuple):
71
+ position = Point(position[0], position[1])
72
+ if isinstance(size, tuple):
73
+ size = Point(size[0], size[1])
74
+
75
+ if uuid_str is None:
76
+ uuid_str = str(uuid.uuid4())
77
+
78
+ if sheet_pins is None:
79
+ sheet_pins = []
80
+
81
+ # Validate filename
82
+ if not filename.endswith(".kicad_sch"):
83
+ filename = f"{filename}.kicad_sch"
84
+
85
+ sheet_data = {
86
+ "uuid": uuid_str,
87
+ "position": {"x": position.x, "y": position.y},
88
+ "size": {"width": size.x, "height": size.y},
89
+ "stroke_width": stroke_width if stroke_width is not None else 0.1524,
90
+ "stroke_type": stroke_type,
91
+ "fill_color": (0, 0, 0, 0.0),
92
+ "name": name,
93
+ "filename": filename,
94
+ "exclude_from_sim": False,
95
+ "in_bom": True,
96
+ "on_board": True,
97
+ "dnp": False,
98
+ "fields_autoplaced": True,
99
+ "pins": [],
100
+ "project_name": project_name,
101
+ "page_number": page_number if page_number else "2",
102
+ "instances": [
103
+ {"project": project_name, "path": f"/{uuid_str}", "reference": name, "unit": 1}
104
+ ],
105
+ }
106
+
107
+ # Add sheet pins if provided (though usually added separately)
108
+ if sheet_pins:
109
+ sheet_data["pins"] = sheet_pins
110
+
111
+ # Add to schematic data
112
+ if "sheets" not in self._data:
113
+ self._data["sheets"] = []
114
+ self._data["sheets"].append(sheet_data)
115
+
116
+ logger.debug(f"Added sheet '{name}' ({filename}) at {position}")
117
+ return uuid_str
118
+
119
+ def add_sheet_pin(
120
+ self,
121
+ sheet_uuid: str,
122
+ name: str,
123
+ pin_type: str,
124
+ position: Union[Point, Tuple[float, float]],
125
+ rotation: float = 0,
126
+ justify: str = "left",
127
+ uuid_str: Optional[str] = None,
128
+ ) -> Optional[str]:
129
+ """
130
+ Add a pin to an existing sheet.
131
+
132
+ Args:
133
+ sheet_uuid: UUID of target sheet
134
+ name: Pin name
135
+ pin_type: Pin type (input, output, bidirectional, tri_state, passive)
136
+ position: Pin position relative to sheet
137
+ rotation: Pin rotation in degrees
138
+ justify: Text justification (left, right, center)
139
+ uuid_str: Optional pin UUID
140
+
141
+ Returns:
142
+ UUID of created pin, or None if sheet not found
143
+ """
144
+ if isinstance(position, tuple):
145
+ position = Point(position[0], position[1])
146
+
147
+ if uuid_str is None:
148
+ uuid_str = str(uuid.uuid4())
149
+
150
+ valid_pin_types = ["input", "output", "bidirectional", "tri_state", "passive"]
151
+ if pin_type not in valid_pin_types:
152
+ logger.warning(f"Invalid sheet pin type: {pin_type}. Using 'input'")
153
+ pin_type = "input"
154
+
155
+ # Find the sheet
156
+ sheets = self._data.get("sheets", [])
157
+ for sheet in sheets:
158
+ if sheet.get("uuid") == sheet_uuid:
159
+ pin_data = {
160
+ "uuid": uuid_str,
161
+ "name": name,
162
+ "pin_type": pin_type,
163
+ "position": {"x": position.x, "y": position.y},
164
+ "rotation": rotation,
165
+ "size": 1.27,
166
+ "justify": justify,
167
+ }
168
+
169
+ # Add to sheet's pins array (already initialized in add_sheet)
170
+ sheet["pins"].append(pin_data)
171
+
172
+ logger.debug(f"Added pin '{name}' to sheet {sheet_uuid}")
173
+ return uuid_str
174
+
175
+ logger.warning(f"Sheet not found: {sheet_uuid}")
176
+ return None
177
+
178
+ def remove_sheet(self, sheet_uuid: str) -> bool:
179
+ """
180
+ Remove a sheet by UUID.
181
+
182
+ Args:
183
+ sheet_uuid: UUID of sheet to remove
184
+
185
+ Returns:
186
+ True if sheet was removed, False if not found
187
+ """
188
+ sheets = self._data.get("sheets", [])
189
+ for i, sheet in enumerate(sheets):
190
+ if sheet.get("uuid") == sheet_uuid:
191
+ # Also remove from sheet instances
192
+ self._remove_sheet_from_instances(sheet_uuid)
193
+ del sheets[i]
194
+ logger.debug(f"Removed sheet: {sheet_uuid}")
195
+ return True
196
+
197
+ logger.warning(f"Sheet not found for removal: {sheet_uuid}")
198
+ return False
199
+
200
+ def remove_sheet_pin(self, sheet_uuid: str, pin_uuid: str) -> bool:
201
+ """
202
+ Remove a pin from a sheet.
203
+
204
+ Args:
205
+ sheet_uuid: UUID of parent sheet
206
+ pin_uuid: UUID of pin to remove
207
+
208
+ Returns:
209
+ True if pin was removed, False if not found
210
+ """
211
+ sheets = self._data.get("sheets", [])
212
+ for sheet in sheets:
213
+ if sheet.get("uuid") == sheet_uuid:
214
+ pins = sheet.get("pins", [])
215
+ for i, pin in enumerate(pins):
216
+ if pin.get("uuid") == pin_uuid:
217
+ del pins[i]
218
+ logger.debug(f"Removed pin {pin_uuid} from sheet {sheet_uuid}")
219
+ return True
220
+
221
+ logger.warning(f"Sheet pin not found: {pin_uuid} in sheet {sheet_uuid}")
222
+ return False
223
+
224
+ def get_sheet_by_name(self, name: str) -> Optional[Dict[str, Any]]:
225
+ """
226
+ Find sheet by name.
227
+
228
+ Args:
229
+ name: Sheet name to find
230
+
231
+ Returns:
232
+ Sheet data or None if not found
233
+ """
234
+ sheets = self._data.get("sheets", [])
235
+ for sheet in sheets:
236
+ if sheet.get("name") == name:
237
+ return sheet
238
+ return None
239
+
240
+ def get_sheet_by_filename(self, filename: str) -> Optional[Dict[str, Any]]:
241
+ """
242
+ Find sheet by filename.
243
+
244
+ Args:
245
+ filename: Filename to find
246
+
247
+ Returns:
248
+ Sheet data or None if not found
249
+ """
250
+ # Normalize filename
251
+ if not filename.endswith(".kicad_sch"):
252
+ filename = f"{filename}.kicad_sch"
253
+
254
+ sheets = self._data.get("sheets", [])
255
+ for sheet in sheets:
256
+ if sheet.get("filename") == filename:
257
+ return sheet
258
+ return None
259
+
260
+ def list_sheet_pins(self, sheet_uuid: str) -> List[Dict[str, Any]]:
261
+ """
262
+ Get all pins for a sheet.
263
+
264
+ Args:
265
+ sheet_uuid: UUID of sheet
266
+
267
+ Returns:
268
+ List of pin data
269
+ """
270
+ sheets = self._data.get("sheets", [])
271
+ for sheet in sheets:
272
+ if sheet.get("uuid") == sheet_uuid:
273
+ pins = sheet.get("pins", [])
274
+ return [
275
+ {
276
+ "uuid": pin.get("uuid"),
277
+ "name": pin.get("name"),
278
+ "pin_type": pin.get("pin_type"),
279
+ "position": (
280
+ Point(pin["position"]["x"], pin["position"]["y"])
281
+ if "position" in pin
282
+ else None
283
+ ),
284
+ "data": pin,
285
+ }
286
+ for pin in pins
287
+ ]
288
+ return []
289
+
290
+ def update_sheet_size(self, sheet_uuid: str, size: Union[Point, Tuple[float, float]]) -> bool:
291
+ """
292
+ Update sheet size.
293
+
294
+ Args:
295
+ sheet_uuid: UUID of sheet
296
+ size: New size (width, height)
297
+
298
+ Returns:
299
+ True if updated, False if not found
300
+ """
301
+ if isinstance(size, tuple):
302
+ size = Point(size[0], size[1])
303
+
304
+ sheets = self._data.get("sheets", [])
305
+ for sheet in sheets:
306
+ if sheet.get("uuid") == sheet_uuid:
307
+ sheet["size"] = {"width": size.x, "height": size.y}
308
+ logger.debug(f"Updated sheet size: {sheet_uuid}")
309
+ return True
310
+
311
+ logger.warning(f"Sheet not found for size update: {sheet_uuid}")
312
+ return False
313
+
314
+ def update_sheet_position(
315
+ self, sheet_uuid: str, position: Union[Point, Tuple[float, float]]
316
+ ) -> bool:
317
+ """
318
+ Update sheet position.
319
+
320
+ Args:
321
+ sheet_uuid: UUID of sheet
322
+ position: New position
323
+
324
+ Returns:
325
+ True if updated, False if not found
326
+ """
327
+ if isinstance(position, tuple):
328
+ position = Point(position[0], position[1])
329
+
330
+ sheets = self._data.get("sheets", [])
331
+ for sheet in sheets:
332
+ if sheet.get("uuid") == sheet_uuid:
333
+ sheet["position"] = {"x": position.x, "y": position.y}
334
+ logger.debug(f"Updated sheet position: {sheet_uuid}")
335
+ return True
336
+
337
+ logger.warning(f"Sheet not found for position update: {sheet_uuid}")
338
+ return False
339
+
340
+ def get_sheet_hierarchy(self) -> Dict[str, Any]:
341
+ """
342
+ Get hierarchical structure of all sheets.
343
+
344
+ Returns:
345
+ Dictionary representing sheet hierarchy
346
+ """
347
+ sheets = self._data.get("sheets", [])
348
+
349
+ hierarchy = {"root": {"uuid": self._data.get("uuid"), "name": "Root Sheet", "children": []}}
350
+
351
+ # Build sheet tree
352
+ for sheet in sheets:
353
+ sheet_info = {
354
+ "uuid": sheet.get("uuid"),
355
+ "name": sheet.get("name"),
356
+ "filename": sheet.get("filename"),
357
+ "pin_count": len(sheet.get("pins", [])),
358
+ "position": (
359
+ Point(sheet["position"]["x"], sheet["position"]["y"])
360
+ if "position" in sheet
361
+ else None
362
+ ),
363
+ "size": (
364
+ Point(sheet["size"]["width"], sheet["size"]["height"])
365
+ if "size" in sheet
366
+ else None
367
+ ),
368
+ }
369
+ hierarchy["root"]["children"].append(sheet_info)
370
+
371
+ return hierarchy
372
+
373
+ def validate_sheet_references(self) -> List[str]:
374
+ """
375
+ Validate sheet file references and connections.
376
+
377
+ Returns:
378
+ List of validation warnings
379
+ """
380
+ warnings = []
381
+ sheets = self._data.get("sheets", [])
382
+
383
+ for sheet in sheets:
384
+ sheet_name = sheet.get("name", "Unknown")
385
+ filename = sheet.get("filename")
386
+
387
+ # Check filename format
388
+ if filename and not filename.endswith(".kicad_sch"):
389
+ warnings.append(f"Sheet '{sheet_name}' has invalid filename: {filename}")
390
+
391
+ # Check for duplicate filenames
392
+ filename_count = sum(1 for s in sheets if s.get("filename") == filename)
393
+ if filename_count > 1:
394
+ warnings.append(f"Duplicate sheet filename: {filename}")
395
+
396
+ # Check sheet pins
397
+ pins = sheet.get("pins", [])
398
+ pin_names = [pin.get("name") for pin in pins]
399
+ duplicate_pins = set([name for name in pin_names if pin_names.count(name) > 1])
400
+ if duplicate_pins:
401
+ warnings.append(f"Sheet '{sheet_name}' has duplicate pin names: {duplicate_pins}")
402
+
403
+ return warnings
404
+
405
+ def get_sheet_statistics(self) -> Dict[str, Any]:
406
+ """
407
+ Get statistics about sheets in the schematic.
408
+
409
+ Returns:
410
+ Dictionary with sheet statistics
411
+ """
412
+ sheets = self._data.get("sheets", [])
413
+
414
+ total_pins = sum(len(sheet.get("pins", [])) for sheet in sheets)
415
+ sheet_instances = self._data.get("sheet_instances", [])
416
+
417
+ return {
418
+ "total_sheets": len(sheets),
419
+ "total_sheet_pins": total_pins,
420
+ "average_pins_per_sheet": total_pins / len(sheets) if sheets else 0,
421
+ "sheet_instances": len(sheet_instances),
422
+ "filenames": [sheet.get("filename") for sheet in sheets if sheet.get("filename")],
423
+ }
424
+
425
+ def _remove_sheet_from_instances(self, sheet_uuid: str) -> None:
426
+ """Remove sheet from sheet instances tracking."""
427
+ sheet_instances = self._data.get("sheet_instances", [])
428
+ for i, instance in enumerate(sheet_instances):
429
+ if instance.get("uuid") == sheet_uuid:
430
+ del sheet_instances[i]
431
+ break
432
+
433
+ def add_sheet_instance(self, sheet_uuid: str, project: str, path: str, reference: str) -> None:
434
+ """
435
+ Add sheet instance tracking.
436
+
437
+ Args:
438
+ sheet_uuid: UUID of sheet
439
+ project: Project identifier
440
+ path: Hierarchical path
441
+ reference: Sheet reference
442
+ """
443
+ if "sheet_instances" not in self._data:
444
+ self._data["sheet_instances"] = []
445
+
446
+ instance_data = {
447
+ "uuid": sheet_uuid,
448
+ "project": project,
449
+ "path": path,
450
+ "reference": reference,
451
+ }
452
+
453
+ self._data["sheet_instances"].append(instance_data)
454
+ logger.debug(f"Added sheet instance: {reference} at {path}")