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