kicad-sch-api 0.3.2__py3-none-any.whl → 0.3.5__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 (39) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/collections/__init__.py +21 -0
  3. kicad_sch_api/collections/base.py +296 -0
  4. kicad_sch_api/collections/components.py +422 -0
  5. kicad_sch_api/collections/junctions.py +378 -0
  6. kicad_sch_api/collections/labels.py +412 -0
  7. kicad_sch_api/collections/wires.py +407 -0
  8. kicad_sch_api/core/formatter.py +31 -0
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/nets.py +310 -0
  11. kicad_sch_api/core/no_connects.py +274 -0
  12. kicad_sch_api/core/parser.py +72 -0
  13. kicad_sch_api/core/schematic.py +185 -9
  14. kicad_sch_api/core/texts.py +343 -0
  15. kicad_sch_api/core/types.py +26 -0
  16. kicad_sch_api/geometry/__init__.py +26 -0
  17. kicad_sch_api/geometry/font_metrics.py +20 -0
  18. kicad_sch_api/geometry/symbol_bbox.py +589 -0
  19. kicad_sch_api/interfaces/__init__.py +17 -0
  20. kicad_sch_api/interfaces/parser.py +76 -0
  21. kicad_sch_api/interfaces/repository.py +70 -0
  22. kicad_sch_api/interfaces/resolver.py +117 -0
  23. kicad_sch_api/parsers/__init__.py +14 -0
  24. kicad_sch_api/parsers/base.py +148 -0
  25. kicad_sch_api/parsers/label_parser.py +254 -0
  26. kicad_sch_api/parsers/registry.py +153 -0
  27. kicad_sch_api/parsers/symbol_parser.py +227 -0
  28. kicad_sch_api/parsers/wire_parser.py +99 -0
  29. kicad_sch_api/symbols/__init__.py +18 -0
  30. kicad_sch_api/symbols/cache.py +470 -0
  31. kicad_sch_api/symbols/resolver.py +367 -0
  32. kicad_sch_api/symbols/validators.py +453 -0
  33. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  34. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  35. kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
  36. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  37. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  38. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  39. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,274 @@
1
+ """
2
+ No-connect element management for KiCAD schematics.
3
+
4
+ This module provides collection classes for managing no-connect elements,
5
+ featuring fast lookup, bulk operations, and validation.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
+
12
+ from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
+ from .types import Point, NoConnect
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class NoConnectElement:
19
+ """
20
+ Enhanced wrapper for schematic no-connect elements with modern API.
21
+
22
+ Provides intuitive access to no-connect properties and operations
23
+ while maintaining exact format preservation.
24
+ """
25
+
26
+ def __init__(self, no_connect_data: NoConnect, parent_collection: "NoConnectCollection"):
27
+ """
28
+ Initialize no-connect element wrapper.
29
+
30
+ Args:
31
+ no_connect_data: Underlying no-connect data
32
+ parent_collection: Parent collection for updates
33
+ """
34
+ self._data = no_connect_data
35
+ self._collection = parent_collection
36
+ self._validator = SchematicValidator()
37
+
38
+ # Core properties with validation
39
+ @property
40
+ def uuid(self) -> str:
41
+ """No-connect element UUID."""
42
+ return self._data.uuid
43
+
44
+ @property
45
+ def position(self) -> Point:
46
+ """No-connect position."""
47
+ return self._data.position
48
+
49
+ @position.setter
50
+ def position(self, value: Union[Point, Tuple[float, float]]):
51
+ """Set no-connect position."""
52
+ if isinstance(value, tuple):
53
+ value = Point(value[0], value[1])
54
+ elif not isinstance(value, Point):
55
+ raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
56
+ self._data.position = value
57
+ self._collection._mark_modified()
58
+
59
+ def validate(self) -> List[ValidationIssue]:
60
+ """Validate this no-connect element."""
61
+ return self._validator.validate_no_connect(self._data.__dict__)
62
+
63
+ def to_dict(self) -> Dict[str, Any]:
64
+ """Convert no-connect element to dictionary representation."""
65
+ return {
66
+ "uuid": self.uuid,
67
+ "position": {"x": self.position.x, "y": self.position.y},
68
+ }
69
+
70
+ def __str__(self) -> str:
71
+ """String representation."""
72
+ return f"<NoConnect @ {self.position}>"
73
+
74
+
75
+ class NoConnectCollection:
76
+ """
77
+ Collection class for efficient no-connect element management.
78
+
79
+ Provides fast lookup, filtering, and bulk operations for schematic no-connect elements.
80
+ """
81
+
82
+ def __init__(self, no_connects: List[NoConnect] = None):
83
+ """
84
+ Initialize no-connect collection.
85
+
86
+ Args:
87
+ no_connects: Initial list of no-connect data
88
+ """
89
+ self._no_connects: List[NoConnectElement] = []
90
+ self._uuid_index: Dict[str, NoConnectElement] = {}
91
+ self._position_index: Dict[Tuple[float, float], List[NoConnectElement]] = {}
92
+ self._modified = False
93
+
94
+ # Add initial no-connects
95
+ if no_connects:
96
+ for no_connect_data in no_connects:
97
+ self._add_to_indexes(NoConnectElement(no_connect_data, self))
98
+
99
+ logger.debug(f"NoConnectCollection initialized with {len(self._no_connects)} no-connects")
100
+
101
+ def add(
102
+ self,
103
+ position: Union[Point, Tuple[float, float]],
104
+ no_connect_uuid: Optional[str] = None,
105
+ ) -> NoConnectElement:
106
+ """
107
+ Add a new no-connect element to the schematic.
108
+
109
+ Args:
110
+ position: No-connect position
111
+ no_connect_uuid: Specific UUID for no-connect (auto-generated if None)
112
+
113
+ Returns:
114
+ Newly created NoConnectElement
115
+
116
+ Raises:
117
+ ValidationError: If no-connect data is invalid
118
+ """
119
+ # Validate inputs
120
+ if isinstance(position, tuple):
121
+ position = Point(position[0], position[1])
122
+ elif not isinstance(position, Point):
123
+ raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
124
+
125
+ # Generate UUID if not provided
126
+ if not no_connect_uuid:
127
+ no_connect_uuid = str(uuid.uuid4())
128
+
129
+ # Check for duplicate UUID
130
+ if no_connect_uuid in self._uuid_index:
131
+ raise ValidationError(f"NoConnect UUID {no_connect_uuid} already exists")
132
+
133
+ # Create no-connect data
134
+ no_connect_data = NoConnect(
135
+ uuid=no_connect_uuid,
136
+ position=position,
137
+ )
138
+
139
+ # Create wrapper and add to collection
140
+ no_connect_element = NoConnectElement(no_connect_data, self)
141
+ self._add_to_indexes(no_connect_element)
142
+ self._mark_modified()
143
+
144
+ logger.debug(f"Added no-connect: {no_connect_element}")
145
+ return no_connect_element
146
+
147
+ def get(self, no_connect_uuid: str) -> Optional[NoConnectElement]:
148
+ """Get no-connect by UUID."""
149
+ return self._uuid_index.get(no_connect_uuid)
150
+
151
+ def remove(self, no_connect_uuid: str) -> bool:
152
+ """
153
+ Remove no-connect by UUID.
154
+
155
+ Args:
156
+ no_connect_uuid: UUID of no-connect to remove
157
+
158
+ Returns:
159
+ True if no-connect was removed, False if not found
160
+ """
161
+ no_connect_element = self._uuid_index.get(no_connect_uuid)
162
+ if not no_connect_element:
163
+ return False
164
+
165
+ # Remove from indexes
166
+ self._remove_from_indexes(no_connect_element)
167
+ self._mark_modified()
168
+
169
+ logger.debug(f"Removed no-connect: {no_connect_element}")
170
+ return True
171
+
172
+ def find_at_position(self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.1) -> List[NoConnectElement]:
173
+ """
174
+ Find no-connects at or near a position.
175
+
176
+ Args:
177
+ position: Position to search around
178
+ tolerance: Search tolerance in mm
179
+
180
+ Returns:
181
+ List of matching no-connect elements
182
+ """
183
+ if isinstance(position, tuple):
184
+ position = Point(position[0], position[1])
185
+
186
+ matches = []
187
+ for no_connect_element in self._no_connects:
188
+ distance = no_connect_element.position.distance_to(position)
189
+ if distance <= tolerance:
190
+ matches.append(no_connect_element)
191
+ return matches
192
+
193
+ def filter(self, predicate: Callable[[NoConnectElement], bool]) -> List[NoConnectElement]:
194
+ """
195
+ Filter no-connects by predicate function.
196
+
197
+ Args:
198
+ predicate: Function that returns True for no-connects to include
199
+
200
+ Returns:
201
+ List of no-connects matching predicate
202
+ """
203
+ return [no_connect for no_connect in self._no_connects if predicate(no_connect)]
204
+
205
+ def bulk_update(self, criteria: Callable[[NoConnectElement], bool], updates: Dict[str, Any]):
206
+ """
207
+ Update multiple no-connects matching criteria.
208
+
209
+ Args:
210
+ criteria: Function to select no-connects to update
211
+ updates: Dictionary of property updates
212
+ """
213
+ updated_count = 0
214
+ for no_connect_element in self._no_connects:
215
+ if criteria(no_connect_element):
216
+ for prop, value in updates.items():
217
+ if hasattr(no_connect_element, prop):
218
+ setattr(no_connect_element, prop, value)
219
+ updated_count += 1
220
+
221
+ if updated_count > 0:
222
+ self._mark_modified()
223
+ logger.debug(f"Bulk updated {updated_count} no-connect properties")
224
+
225
+ def clear(self):
226
+ """Remove all no-connects from collection."""
227
+ self._no_connects.clear()
228
+ self._uuid_index.clear()
229
+ self._position_index.clear()
230
+ self._mark_modified()
231
+
232
+ def _add_to_indexes(self, no_connect_element: NoConnectElement):
233
+ """Add no-connect to internal indexes."""
234
+ self._no_connects.append(no_connect_element)
235
+ self._uuid_index[no_connect_element.uuid] = no_connect_element
236
+
237
+ # Add to position index
238
+ pos_key = (no_connect_element.position.x, no_connect_element.position.y)
239
+ if pos_key not in self._position_index:
240
+ self._position_index[pos_key] = []
241
+ self._position_index[pos_key].append(no_connect_element)
242
+
243
+ def _remove_from_indexes(self, no_connect_element: NoConnectElement):
244
+ """Remove no-connect from internal indexes."""
245
+ self._no_connects.remove(no_connect_element)
246
+ del self._uuid_index[no_connect_element.uuid]
247
+
248
+ # Remove from position index
249
+ pos_key = (no_connect_element.position.x, no_connect_element.position.y)
250
+ if pos_key in self._position_index:
251
+ self._position_index[pos_key].remove(no_connect_element)
252
+ if not self._position_index[pos_key]:
253
+ del self._position_index[pos_key]
254
+
255
+ def _mark_modified(self):
256
+ """Mark collection as modified."""
257
+ self._modified = True
258
+
259
+ # Collection interface methods
260
+ def __len__(self) -> int:
261
+ """Return number of no-connects."""
262
+ return len(self._no_connects)
263
+
264
+ def __iter__(self) -> Iterator[NoConnectElement]:
265
+ """Iterate over no-connects."""
266
+ return iter(self._no_connects)
267
+
268
+ def __getitem__(self, index: int) -> NoConnectElement:
269
+ """Get no-connect by index."""
270
+ return self._no_connects[index]
271
+
272
+ def __bool__(self) -> bool:
273
+ """Return True if collection has no-connects."""
274
+ return len(self._no_connects) > 0
@@ -197,6 +197,7 @@ class SExpressionParser:
197
197
  "circles": [],
198
198
  "beziers": [],
199
199
  "rectangles": [],
200
+ "images": [],
200
201
  "nets": [],
201
202
  "lib_symbols": {},
202
203
  "sheet_instances": [],
@@ -282,6 +283,10 @@ class SExpressionParser:
282
283
  rectangle = self._parse_rectangle(item)
283
284
  if rectangle:
284
285
  schematic_data["rectangles"].append(rectangle)
286
+ elif element_type == "image":
287
+ image = self._parse_image(item)
288
+ if image:
289
+ schematic_data["images"].append(image)
285
290
  elif element_type == "lib_symbols":
286
291
  schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
287
292
  elif element_type == "sheet_instances":
@@ -357,6 +362,10 @@ class SExpressionParser:
357
362
  for graphic in schematic_data.get("graphics", []):
358
363
  sexp_data.append(self._graphic_to_sexp(graphic))
359
364
 
365
+ # Images
366
+ for image in schematic_data.get("images", []):
367
+ sexp_data.append(self._image_to_sexp(image))
368
+
360
369
  # Circles
361
370
  for circle in schematic_data.get("circles", []):
362
371
  sexp_data.append(self._circle_to_sexp(circle))
@@ -1108,6 +1117,37 @@ class SExpressionParser:
1108
1117
 
1109
1118
  return rectangle if rectangle else None
1110
1119
 
1120
+ def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1121
+ """Parse an image element."""
1122
+ # Format: (image (at x y) (uuid "...") (data "base64..."))
1123
+ image = {
1124
+ "position": {"x": 0, "y": 0},
1125
+ "data": "",
1126
+ "scale": 1.0,
1127
+ "uuid": None
1128
+ }
1129
+
1130
+ for elem in item[1:]:
1131
+ if not isinstance(elem, list):
1132
+ continue
1133
+
1134
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1135
+
1136
+ if elem_type == "at" and len(elem) >= 3:
1137
+ image["position"] = {"x": float(elem[1]), "y": float(elem[2])}
1138
+ elif elem_type == "scale" and len(elem) >= 2:
1139
+ image["scale"] = float(elem[1])
1140
+ elif elem_type == "data" and len(elem) >= 2:
1141
+ # The data can be spread across multiple string elements
1142
+ data_parts = []
1143
+ for data_elem in elem[1:]:
1144
+ data_parts.append(str(data_elem).strip('"'))
1145
+ image["data"] = "".join(data_parts)
1146
+ elif elem_type == "uuid" and len(elem) >= 2:
1147
+ image["uuid"] = str(elem[1]).strip('"')
1148
+
1149
+ return image if image.get("uuid") and image.get("data") else None
1150
+
1111
1151
  def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
1112
1152
  """Parse lib_symbols section."""
1113
1153
  # Implementation for lib_symbols parsing
@@ -1933,6 +1973,38 @@ class SExpressionParser:
1933
1973
 
1934
1974
  return sexp
1935
1975
 
1976
+ def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
1977
+ """Convert image element to S-expression."""
1978
+ sexp = [sexpdata.Symbol("image")]
1979
+
1980
+ # Add position
1981
+ position = image_data.get("position", {"x": 0, "y": 0})
1982
+ pos_x, pos_y = position["x"], position["y"]
1983
+ sexp.append([sexpdata.Symbol("at"), pos_x, pos_y])
1984
+
1985
+ # Add UUID
1986
+ if "uuid" in image_data:
1987
+ sexp.append([sexpdata.Symbol("uuid"), image_data["uuid"]])
1988
+
1989
+ # Add scale if not default
1990
+ scale = image_data.get("scale", 1.0)
1991
+ if scale != 1.0:
1992
+ sexp.append([sexpdata.Symbol("scale"), scale])
1993
+
1994
+ # Add image data
1995
+ # KiCad splits base64 data into multiple lines for readability
1996
+ # Each line is roughly 76 characters (standard base64 line length)
1997
+ data = image_data.get("data", "")
1998
+ if data:
1999
+ data_sexp = [sexpdata.Symbol("data")]
2000
+ # Split the data into 76-character chunks
2001
+ chunk_size = 76
2002
+ for i in range(0, len(data), chunk_size):
2003
+ data_sexp.append(data[i:i+chunk_size])
2004
+ sexp.append(data_sexp)
2005
+
2006
+ return sexp
2007
+
1936
2008
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
1937
2009
  """Convert lib_symbols to S-expression."""
1938
2010
  sexp = [sexpdata.Symbol("lib_symbols")]
@@ -18,13 +18,18 @@ from ..utils.validation import SchematicValidator, ValidationError, ValidationIs
18
18
  from .components import ComponentCollection
19
19
  from .formatter import ExactFormatter
20
20
  from .junctions import JunctionCollection
21
+ from .labels import LabelCollection
22
+ from .nets import NetCollection
23
+ from .no_connects import NoConnectCollection
21
24
  from .parser import SExpressionParser
25
+ from .texts import TextCollection
22
26
  from .types import (
23
27
  HierarchicalLabelShape,
24
28
  Junction,
25
29
  Label,
26
30
  LabelType,
27
31
  Net,
32
+ NoConnect,
28
33
  Point,
29
34
  SchematicSymbol,
30
35
  Sheet,
@@ -136,6 +141,97 @@ class Schematic:
136
141
  junctions.append(junction)
137
142
  self._junctions = JunctionCollection(junctions)
138
143
 
144
+ # Initialize text collection
145
+ text_data = self._data.get("texts", [])
146
+ texts = []
147
+ for text_dict in text_data:
148
+ if isinstance(text_dict, dict):
149
+ # Convert dict to Text object
150
+ position = text_dict.get("position", {"x": 0, "y": 0})
151
+ if isinstance(position, dict):
152
+ pos = Point(position["x"], position["y"])
153
+ elif isinstance(position, (list, tuple)):
154
+ pos = Point(position[0], position[1])
155
+ else:
156
+ pos = position
157
+
158
+ text = Text(
159
+ uuid=text_dict.get("uuid", str(uuid.uuid4())),
160
+ position=pos,
161
+ text=text_dict.get("text", ""),
162
+ rotation=text_dict.get("rotation", 0.0),
163
+ size=text_dict.get("size", 1.27),
164
+ exclude_from_sim=text_dict.get("exclude_from_sim", False),
165
+ )
166
+ texts.append(text)
167
+ self._texts = TextCollection(texts)
168
+
169
+ # Initialize label collection
170
+ label_data = self._data.get("labels", [])
171
+ labels = []
172
+ for label_dict in label_data:
173
+ if isinstance(label_dict, dict):
174
+ # Convert dict to Label object
175
+ position = label_dict.get("position", {"x": 0, "y": 0})
176
+ if isinstance(position, dict):
177
+ pos = Point(position["x"], position["y"])
178
+ elif isinstance(position, (list, tuple)):
179
+ pos = Point(position[0], position[1])
180
+ else:
181
+ pos = position
182
+
183
+ label = Label(
184
+ uuid=label_dict.get("uuid", str(uuid.uuid4())),
185
+ position=pos,
186
+ text=label_dict.get("text", ""),
187
+ label_type=LabelType(label_dict.get("label_type", "local")),
188
+ rotation=label_dict.get("rotation", 0.0),
189
+ size=label_dict.get("size", 1.27),
190
+ shape=HierarchicalLabelShape(label_dict.get("shape")) if label_dict.get("shape") else None,
191
+ )
192
+ labels.append(label)
193
+ self._labels = LabelCollection(labels)
194
+
195
+ # Initialize hierarchical labels collection (filter from labels)
196
+ hierarchical_labels = [label for label in labels if label.label_type == LabelType.HIERARCHICAL]
197
+ self._hierarchical_labels = LabelCollection(hierarchical_labels)
198
+
199
+ # Initialize no-connect collection
200
+ no_connect_data = self._data.get("no_connects", [])
201
+ no_connects = []
202
+ for no_connect_dict in no_connect_data:
203
+ if isinstance(no_connect_dict, dict):
204
+ # Convert dict to NoConnect object
205
+ position = no_connect_dict.get("position", {"x": 0, "y": 0})
206
+ if isinstance(position, dict):
207
+ pos = Point(position["x"], position["y"])
208
+ elif isinstance(position, (list, tuple)):
209
+ pos = Point(position[0], position[1])
210
+ else:
211
+ pos = position
212
+
213
+ no_connect = NoConnect(
214
+ uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
215
+ position=pos,
216
+ )
217
+ no_connects.append(no_connect)
218
+ self._no_connects = NoConnectCollection(no_connects)
219
+
220
+ # Initialize net collection
221
+ net_data = self._data.get("nets", [])
222
+ nets = []
223
+ for net_dict in net_data:
224
+ if isinstance(net_dict, dict):
225
+ # Convert dict to Net object
226
+ net = Net(
227
+ name=net_dict.get("name", ""),
228
+ components=net_dict.get("components", []),
229
+ wires=net_dict.get("wires", []),
230
+ labels=net_dict.get("labels", []),
231
+ )
232
+ nets.append(net)
233
+ self._nets = NetCollection(nets)
234
+
139
235
  # Track modifications for save optimization
140
236
  self._modified = False
141
237
  self._last_save_time = None
@@ -145,7 +241,10 @@ class Schematic:
145
241
  self._total_operation_time = 0.0
146
242
 
147
243
  logger.debug(
148
- f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, and {len(self._junctions)} junctions"
244
+ f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, "
245
+ f"{len(self._junctions)} junctions, {len(self._texts)} texts, {len(self._labels)} labels, "
246
+ f"{len(self._hierarchical_labels)} hierarchical labels, {len(self._no_connects)} no-connects, "
247
+ f"and {len(self._nets)} nets"
149
248
  )
150
249
 
151
250
  @classmethod
@@ -276,7 +375,42 @@ class Schematic:
276
375
  @property
277
376
  def modified(self) -> bool:
278
377
  """Whether schematic has been modified since last save."""
279
- return self._modified or self._components._modified
378
+ return (
379
+ self._modified
380
+ or self._components._modified
381
+ or self._wires._modified
382
+ or self._junctions._modified
383
+ or self._texts._modified
384
+ or self._labels._modified
385
+ or self._hierarchical_labels._modified
386
+ or self._no_connects._modified
387
+ or self._nets._modified
388
+ )
389
+
390
+ @property
391
+ def texts(self) -> TextCollection:
392
+ """Collection of all text elements in the schematic."""
393
+ return self._texts
394
+
395
+ @property
396
+ def labels(self) -> LabelCollection:
397
+ """Collection of all label elements in the schematic."""
398
+ return self._labels
399
+
400
+ @property
401
+ def hierarchical_labels(self) -> LabelCollection:
402
+ """Collection of all hierarchical label elements in the schematic."""
403
+ return self._hierarchical_labels
404
+
405
+ @property
406
+ def no_connects(self) -> NoConnectCollection:
407
+ """Collection of all no-connect elements in the schematic."""
408
+ return self._no_connects
409
+
410
+ @property
411
+ def nets(self) -> NetCollection:
412
+ """Collection of all electrical nets in the schematic."""
413
+ return self._nets
280
414
 
281
415
  # Pin positioning methods (migrated from circuit-synth)
282
416
  def get_component_pin_position(self, reference: str, pin_number: str) -> Optional[Point]:
@@ -1138,6 +1272,55 @@ class Schematic:
1138
1272
  logger.debug(f"Added text box: '{text}' at {position} size {size}")
1139
1273
  return text_box.uuid
1140
1274
 
1275
+ def add_image(
1276
+ self,
1277
+ position: Union[Point, Tuple[float, float]],
1278
+ data: str,
1279
+ scale: float = 1.0,
1280
+ uuid: Optional[str] = None,
1281
+ ) -> str:
1282
+ """
1283
+ Add an image element.
1284
+
1285
+ Args:
1286
+ position: Image position
1287
+ data: Base64-encoded image data
1288
+ scale: Image scale factor (default 1.0)
1289
+ uuid: Optional UUID (auto-generated if None)
1290
+
1291
+ Returns:
1292
+ UUID of created image element
1293
+ """
1294
+ if isinstance(position, tuple):
1295
+ position = Point(position[0], position[1])
1296
+
1297
+ from .types import Image
1298
+
1299
+ import uuid as uuid_module
1300
+
1301
+ image = Image(
1302
+ uuid=uuid if uuid else str(uuid_module.uuid4()),
1303
+ position=position,
1304
+ data=data,
1305
+ scale=scale,
1306
+ )
1307
+
1308
+ if "images" not in self._data:
1309
+ self._data["images"] = []
1310
+
1311
+ self._data["images"].append(
1312
+ {
1313
+ "uuid": image.uuid,
1314
+ "position": {"x": image.position.x, "y": image.position.y},
1315
+ "data": image.data,
1316
+ "scale": image.scale,
1317
+ }
1318
+ )
1319
+ self._modified = True
1320
+
1321
+ logger.debug(f"Added image at {position} with {len(data)} bytes of data")
1322
+ return image.uuid
1323
+
1141
1324
  def add_rectangle(
1142
1325
  self,
1143
1326
  start: Union[Point, Tuple[float, float]],
@@ -1592,9 +1775,7 @@ class Schematic:
1592
1775
  break
1593
1776
 
1594
1777
  # Fix string/symbol conversion issues in pin definitions
1595
- print(f"🔧 DEBUG: Before fix - checking for pin definitions...")
1596
1778
  self._fix_symbol_strings_recursively(modified_data)
1597
- print(f"🔧 DEBUG: After fix - symbol strings fixed")
1598
1779
 
1599
1780
  return modified_data
1600
1781
 
@@ -1607,15 +1788,10 @@ class Schematic:
1607
1788
  if isinstance(item, list):
1608
1789
  # Check for pin definitions that need fixing
1609
1790
  if len(item) >= 3 and item[0] == sexpdata.Symbol("pin"):
1610
- print(
1611
- f"🔧 DEBUG: Found pin definition: {item[:3]} - types: {[type(x) for x in item[:3]]}"
1612
- )
1613
1791
  # Fix pin type and shape - ensure they are symbols not strings
1614
1792
  if isinstance(item[1], str):
1615
- print(f"🔧 DEBUG: Converting pin type '{item[1]}' to symbol")
1616
1793
  item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
1617
1794
  if len(item) >= 3 and isinstance(item[2], str):
1618
- print(f"🔧 DEBUG: Converting pin shape '{item[2]}' to symbol")
1619
1795
  item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
1620
1796
 
1621
1797
  # Recursively process nested lists