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.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/collections/__init__.py +21 -0
- kicad_sch_api/collections/base.py +296 -0
- kicad_sch_api/collections/components.py +422 -0
- kicad_sch_api/collections/junctions.py +378 -0
- kicad_sch_api/collections/labels.py +412 -0
- kicad_sch_api/collections/wires.py +407 -0
- kicad_sch_api/core/formatter.py +31 -0
- kicad_sch_api/core/labels.py +348 -0
- kicad_sch_api/core/nets.py +310 -0
- kicad_sch_api/core/no_connects.py +274 -0
- kicad_sch_api/core/parser.py +72 -0
- kicad_sch_api/core/schematic.py +185 -9
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +26 -0
- kicad_sch_api/geometry/__init__.py +26 -0
- kicad_sch_api/geometry/font_metrics.py +20 -0
- kicad_sch_api/geometry/symbol_bbox.py +589 -0
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +148 -0
- kicad_sch_api/parsers/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +153 -0
- kicad_sch_api/parsers/symbol_parser.py +227 -0
- kicad_sch_api/parsers/wire_parser.py +99 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +470 -0
- kicad_sch_api/symbols/resolver.py +367 -0
- kicad_sch_api/symbols/validators.py +453 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
- kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
- kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {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
|
kicad_sch_api/core/parser.py
CHANGED
|
@@ -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")]
|
kicad_sch_api/core/schematic.py
CHANGED
|
@@ -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,
|
|
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
|
|
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
|