kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__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/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +5 -0
- kicad_sch_api/core/components.py +142 -47
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +22 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +28 -52
- kicad_sch_api/core/managers/file_io.py +3 -2
- kicad_sch_api/core/managers/metadata.py +6 -5
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +7 -1
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +29 -53
- kicad_sch_api/core/parser.py +75 -1765
- kicad_sch_api/core/schematic.py +211 -148
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +59 -18
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +194 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -222
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/schematic.py
CHANGED
|
@@ -17,6 +17,7 @@ import sexpdata
|
|
|
17
17
|
from ..library.cache import get_symbol_cache
|
|
18
18
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
19
19
|
from .components import ComponentCollection
|
|
20
|
+
from .factories import ElementFactory
|
|
20
21
|
from .formatter import ExactFormatter
|
|
21
22
|
from .junctions import JunctionCollection
|
|
22
23
|
from .labels import LabelCollection
|
|
@@ -49,6 +50,7 @@ from .types import (
|
|
|
49
50
|
TitleBlock,
|
|
50
51
|
Wire,
|
|
51
52
|
WireType,
|
|
53
|
+
point_from_dict_or_tuple,
|
|
52
54
|
)
|
|
53
55
|
from .wires import WireCollection
|
|
54
56
|
|
|
@@ -107,105 +109,22 @@ class Schematic:
|
|
|
107
109
|
|
|
108
110
|
# Initialize wire collection
|
|
109
111
|
wire_data = self._data.get("wires", [])
|
|
110
|
-
wires =
|
|
111
|
-
for wire_dict in wire_data:
|
|
112
|
-
if isinstance(wire_dict, dict):
|
|
113
|
-
# Convert dict to Wire object
|
|
114
|
-
points = []
|
|
115
|
-
for point_data in wire_dict.get("points", []):
|
|
116
|
-
if isinstance(point_data, dict):
|
|
117
|
-
points.append(Point(point_data["x"], point_data["y"]))
|
|
118
|
-
elif isinstance(point_data, (list, tuple)):
|
|
119
|
-
points.append(Point(point_data[0], point_data[1]))
|
|
120
|
-
else:
|
|
121
|
-
points.append(point_data)
|
|
122
|
-
|
|
123
|
-
wire = Wire(
|
|
124
|
-
uuid=wire_dict.get("uuid", str(uuid.uuid4())),
|
|
125
|
-
points=points,
|
|
126
|
-
wire_type=WireType(wire_dict.get("wire_type", "wire")),
|
|
127
|
-
stroke_width=wire_dict.get("stroke_width", 0.0),
|
|
128
|
-
stroke_type=wire_dict.get("stroke_type", "default"),
|
|
129
|
-
)
|
|
130
|
-
wires.append(wire)
|
|
112
|
+
wires = ElementFactory.create_wires_from_list(wire_data)
|
|
131
113
|
self._wires = WireCollection(wires)
|
|
132
114
|
|
|
133
115
|
# Initialize junction collection
|
|
134
116
|
junction_data = self._data.get("junctions", [])
|
|
135
|
-
junctions =
|
|
136
|
-
for junction_dict in junction_data:
|
|
137
|
-
if isinstance(junction_dict, dict):
|
|
138
|
-
# Convert dict to Junction object
|
|
139
|
-
position = junction_dict.get("position", {"x": 0, "y": 0})
|
|
140
|
-
if isinstance(position, dict):
|
|
141
|
-
pos = Point(position["x"], position["y"])
|
|
142
|
-
elif isinstance(position, (list, tuple)):
|
|
143
|
-
pos = Point(position[0], position[1])
|
|
144
|
-
else:
|
|
145
|
-
pos = position
|
|
146
|
-
|
|
147
|
-
junction = Junction(
|
|
148
|
-
uuid=junction_dict.get("uuid", str(uuid.uuid4())),
|
|
149
|
-
position=pos,
|
|
150
|
-
diameter=junction_dict.get("diameter", 0),
|
|
151
|
-
color=junction_dict.get("color", (0, 0, 0, 0)),
|
|
152
|
-
)
|
|
153
|
-
junctions.append(junction)
|
|
117
|
+
junctions = ElementFactory.create_junctions_from_list(junction_data)
|
|
154
118
|
self._junctions = JunctionCollection(junctions)
|
|
155
119
|
|
|
156
120
|
# Initialize text collection
|
|
157
121
|
text_data = self._data.get("texts", [])
|
|
158
|
-
texts =
|
|
159
|
-
for text_dict in text_data:
|
|
160
|
-
if isinstance(text_dict, dict):
|
|
161
|
-
# Convert dict to Text object
|
|
162
|
-
position = text_dict.get("position", {"x": 0, "y": 0})
|
|
163
|
-
if isinstance(position, dict):
|
|
164
|
-
pos = Point(position["x"], position["y"])
|
|
165
|
-
elif isinstance(position, (list, tuple)):
|
|
166
|
-
pos = Point(position[0], position[1])
|
|
167
|
-
else:
|
|
168
|
-
pos = position
|
|
169
|
-
|
|
170
|
-
text = Text(
|
|
171
|
-
uuid=text_dict.get("uuid", str(uuid.uuid4())),
|
|
172
|
-
position=pos,
|
|
173
|
-
text=text_dict.get("text", ""),
|
|
174
|
-
rotation=text_dict.get("rotation", 0.0),
|
|
175
|
-
size=text_dict.get("size", 1.27),
|
|
176
|
-
exclude_from_sim=text_dict.get("exclude_from_sim", False),
|
|
177
|
-
)
|
|
178
|
-
texts.append(text)
|
|
122
|
+
texts = ElementFactory.create_texts_from_list(text_data)
|
|
179
123
|
self._texts = TextCollection(texts)
|
|
180
124
|
|
|
181
125
|
# Initialize label collection
|
|
182
126
|
label_data = self._data.get("labels", [])
|
|
183
|
-
labels =
|
|
184
|
-
for label_dict in label_data:
|
|
185
|
-
if isinstance(label_dict, dict):
|
|
186
|
-
# Convert dict to Label object
|
|
187
|
-
position = label_dict.get("position", {"x": 0, "y": 0})
|
|
188
|
-
if isinstance(position, dict):
|
|
189
|
-
pos = Point(position["x"], position["y"])
|
|
190
|
-
elif isinstance(position, (list, tuple)):
|
|
191
|
-
pos = Point(position[0], position[1])
|
|
192
|
-
else:
|
|
193
|
-
pos = position
|
|
194
|
-
|
|
195
|
-
label = Label(
|
|
196
|
-
uuid=label_dict.get("uuid", str(uuid.uuid4())),
|
|
197
|
-
position=pos,
|
|
198
|
-
text=label_dict.get("text", ""),
|
|
199
|
-
label_type=LabelType(label_dict.get("label_type", "local")),
|
|
200
|
-
rotation=label_dict.get("rotation", 0.0),
|
|
201
|
-
size=label_dict.get("size", 1.27),
|
|
202
|
-
shape=(
|
|
203
|
-
HierarchicalLabelShape(label_dict.get("shape"))
|
|
204
|
-
if label_dict.get("shape")
|
|
205
|
-
else None
|
|
206
|
-
),
|
|
207
|
-
)
|
|
208
|
-
labels.append(label)
|
|
127
|
+
labels = ElementFactory.create_labels_from_list(label_data)
|
|
209
128
|
self._labels = LabelCollection(labels)
|
|
210
129
|
|
|
211
130
|
# Initialize hierarchical labels collection (from both labels array and hierarchical_labels array)
|
|
@@ -215,68 +134,18 @@ class Schematic:
|
|
|
215
134
|
|
|
216
135
|
# Also load from hierarchical_labels data if present
|
|
217
136
|
hierarchical_label_data = self._data.get("hierarchical_labels", [])
|
|
218
|
-
|
|
219
|
-
if isinstance(hlabel_dict, dict):
|
|
220
|
-
# Convert dict to Label object
|
|
221
|
-
position = hlabel_dict.get("position", {"x": 0, "y": 0})
|
|
222
|
-
if isinstance(position, dict):
|
|
223
|
-
pos = Point(position["x"], position["y"])
|
|
224
|
-
elif isinstance(position, (list, tuple)):
|
|
225
|
-
pos = Point(position[0], position[1])
|
|
226
|
-
else:
|
|
227
|
-
pos = position
|
|
228
|
-
|
|
229
|
-
hlabel = Label(
|
|
230
|
-
uuid=hlabel_dict.get("uuid", str(uuid.uuid4())),
|
|
231
|
-
position=pos,
|
|
232
|
-
text=hlabel_dict.get("text", ""),
|
|
233
|
-
label_type=LabelType.HIERARCHICAL,
|
|
234
|
-
rotation=hlabel_dict.get("rotation", 0.0),
|
|
235
|
-
size=hlabel_dict.get("size", 1.27),
|
|
236
|
-
shape=(
|
|
237
|
-
HierarchicalLabelShape(hlabel_dict.get("shape"))
|
|
238
|
-
if hlabel_dict.get("shape")
|
|
239
|
-
else None
|
|
240
|
-
),
|
|
241
|
-
)
|
|
242
|
-
hierarchical_labels.append(hlabel)
|
|
137
|
+
hierarchical_labels.extend(ElementFactory.create_labels_from_list(hierarchical_label_data))
|
|
243
138
|
|
|
244
139
|
self._hierarchical_labels = LabelCollection(hierarchical_labels)
|
|
245
140
|
|
|
246
141
|
# Initialize no-connect collection
|
|
247
142
|
no_connect_data = self._data.get("no_connects", [])
|
|
248
|
-
no_connects =
|
|
249
|
-
for no_connect_dict in no_connect_data:
|
|
250
|
-
if isinstance(no_connect_dict, dict):
|
|
251
|
-
# Convert dict to NoConnect object
|
|
252
|
-
position = no_connect_dict.get("position", {"x": 0, "y": 0})
|
|
253
|
-
if isinstance(position, dict):
|
|
254
|
-
pos = Point(position["x"], position["y"])
|
|
255
|
-
elif isinstance(position, (list, tuple)):
|
|
256
|
-
pos = Point(position[0], position[1])
|
|
257
|
-
else:
|
|
258
|
-
pos = position
|
|
259
|
-
|
|
260
|
-
no_connect = NoConnect(
|
|
261
|
-
uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
|
|
262
|
-
position=pos,
|
|
263
|
-
)
|
|
264
|
-
no_connects.append(no_connect)
|
|
143
|
+
no_connects = ElementFactory.create_no_connects_from_list(no_connect_data)
|
|
265
144
|
self._no_connects = NoConnectCollection(no_connects)
|
|
266
145
|
|
|
267
146
|
# Initialize net collection
|
|
268
147
|
net_data = self._data.get("nets", [])
|
|
269
|
-
nets =
|
|
270
|
-
for net_dict in net_data:
|
|
271
|
-
if isinstance(net_dict, dict):
|
|
272
|
-
# Convert dict to Net object
|
|
273
|
-
net = Net(
|
|
274
|
-
name=net_dict.get("name", ""),
|
|
275
|
-
components=net_dict.get("components", []),
|
|
276
|
-
wires=net_dict.get("wires", []),
|
|
277
|
-
labels=net_dict.get("labels", []),
|
|
278
|
-
)
|
|
279
|
-
nets.append(net)
|
|
148
|
+
nets = ElementFactory.create_nets_from_list(net_data)
|
|
280
149
|
self._nets = NetCollection(nets)
|
|
281
150
|
|
|
282
151
|
# Initialize specialized managers
|
|
@@ -337,10 +206,10 @@ class Schematic:
|
|
|
337
206
|
def create(
|
|
338
207
|
cls,
|
|
339
208
|
name: str = "Untitled",
|
|
340
|
-
version: str =
|
|
341
|
-
generator: str =
|
|
342
|
-
generator_version: str =
|
|
343
|
-
paper: str =
|
|
209
|
+
version: str = None,
|
|
210
|
+
generator: str = None,
|
|
211
|
+
generator_version: str = None,
|
|
212
|
+
paper: str = None,
|
|
344
213
|
uuid: str = None,
|
|
345
214
|
) -> "Schematic":
|
|
346
215
|
"""
|
|
@@ -348,15 +217,23 @@ class Schematic:
|
|
|
348
217
|
|
|
349
218
|
Args:
|
|
350
219
|
name: Schematic name
|
|
351
|
-
version: KiCAD version (default
|
|
352
|
-
generator: Generator name (default
|
|
353
|
-
generator_version: Generator version (default
|
|
354
|
-
paper: Paper size (default
|
|
220
|
+
version: KiCAD version (default from config)
|
|
221
|
+
generator: Generator name (default from config)
|
|
222
|
+
generator_version: Generator version (default from config)
|
|
223
|
+
paper: Paper size (default from config)
|
|
355
224
|
uuid: Specific UUID (auto-generated if None)
|
|
356
225
|
|
|
357
226
|
Returns:
|
|
358
227
|
New empty Schematic object
|
|
359
228
|
"""
|
|
229
|
+
# Apply config defaults for None values
|
|
230
|
+
from .config import config
|
|
231
|
+
|
|
232
|
+
version = version or config.file_format.version_default
|
|
233
|
+
generator = generator or config.file_format.generator_default
|
|
234
|
+
generator_version = generator_version or config.file_format.generator_version_default
|
|
235
|
+
paper = paper or config.paper.default
|
|
236
|
+
|
|
360
237
|
# Special handling for blank schematic test case to match reference exactly
|
|
361
238
|
if name == "Blank Schematic":
|
|
362
239
|
schematic_data = {
|
|
@@ -1542,6 +1419,192 @@ class Schematic:
|
|
|
1542
1419
|
# Recursively check nested elements
|
|
1543
1420
|
self._update_project_in_instances(element)
|
|
1544
1421
|
|
|
1422
|
+
# ============================================================================
|
|
1423
|
+
# Export Methods (using kicad-cli)
|
|
1424
|
+
# ============================================================================
|
|
1425
|
+
|
|
1426
|
+
def run_erc(self, **kwargs):
|
|
1427
|
+
"""
|
|
1428
|
+
Run Electrical Rule Check (ERC) on this schematic.
|
|
1429
|
+
|
|
1430
|
+
This requires the schematic to be saved first.
|
|
1431
|
+
|
|
1432
|
+
Args:
|
|
1433
|
+
**kwargs: Arguments passed to cli.erc.run_erc()
|
|
1434
|
+
- output_path: Path for ERC report
|
|
1435
|
+
- format: 'json' or 'report'
|
|
1436
|
+
- severity: 'all', 'error', 'warning', 'exclusions'
|
|
1437
|
+
- units: 'mm', 'in', 'mils'
|
|
1438
|
+
|
|
1439
|
+
Returns:
|
|
1440
|
+
ErcReport with violations and summary
|
|
1441
|
+
|
|
1442
|
+
Example:
|
|
1443
|
+
>>> report = sch.run_erc()
|
|
1444
|
+
>>> if report.has_errors():
|
|
1445
|
+
... print(f"Found {report.error_count} errors")
|
|
1446
|
+
"""
|
|
1447
|
+
from kicad_sch_api.cli.erc import run_erc
|
|
1448
|
+
|
|
1449
|
+
if not self._file_path:
|
|
1450
|
+
raise ValueError("Schematic must be saved before running ERC")
|
|
1451
|
+
|
|
1452
|
+
# Save first to ensure file is up-to-date
|
|
1453
|
+
self.save()
|
|
1454
|
+
|
|
1455
|
+
return run_erc(self._file_path, **kwargs)
|
|
1456
|
+
|
|
1457
|
+
def export_netlist(self, format="kicadsexpr", **kwargs):
|
|
1458
|
+
"""
|
|
1459
|
+
Export netlist from this schematic.
|
|
1460
|
+
|
|
1461
|
+
This requires the schematic to be saved first.
|
|
1462
|
+
|
|
1463
|
+
Args:
|
|
1464
|
+
format: Netlist format (default: 'kicadsexpr')
|
|
1465
|
+
- kicadsexpr: KiCad S-expression (default)
|
|
1466
|
+
- kicadxml: KiCad XML
|
|
1467
|
+
- spice: SPICE netlist
|
|
1468
|
+
- spicemodel: SPICE with models
|
|
1469
|
+
- cadstar, orcadpcb2, pads, allegro
|
|
1470
|
+
**kwargs: Arguments passed to cli.netlist.export_netlist()
|
|
1471
|
+
|
|
1472
|
+
Returns:
|
|
1473
|
+
Path to generated netlist file
|
|
1474
|
+
|
|
1475
|
+
Example:
|
|
1476
|
+
>>> netlist = sch.export_netlist(format='spice')
|
|
1477
|
+
>>> print(f"Netlist: {netlist}")
|
|
1478
|
+
"""
|
|
1479
|
+
from kicad_sch_api.cli.netlist import export_netlist
|
|
1480
|
+
|
|
1481
|
+
if not self._file_path:
|
|
1482
|
+
raise ValueError("Schematic must be saved before exporting netlist")
|
|
1483
|
+
|
|
1484
|
+
# Save first to ensure file is up-to-date
|
|
1485
|
+
self.save()
|
|
1486
|
+
|
|
1487
|
+
return export_netlist(self._file_path, format=format, **kwargs)
|
|
1488
|
+
|
|
1489
|
+
def export_bom(self, **kwargs):
|
|
1490
|
+
"""
|
|
1491
|
+
Export Bill of Materials (BOM) from this schematic.
|
|
1492
|
+
|
|
1493
|
+
This requires the schematic to be saved first.
|
|
1494
|
+
|
|
1495
|
+
Args:
|
|
1496
|
+
**kwargs: Arguments passed to cli.bom.export_bom()
|
|
1497
|
+
- output_path: Path for BOM file
|
|
1498
|
+
- fields: List of fields to export
|
|
1499
|
+
- group_by: Fields to group by
|
|
1500
|
+
- exclude_dnp: Exclude Do-Not-Populate components
|
|
1501
|
+
- And many more options...
|
|
1502
|
+
|
|
1503
|
+
Returns:
|
|
1504
|
+
Path to generated BOM file
|
|
1505
|
+
|
|
1506
|
+
Example:
|
|
1507
|
+
>>> bom = sch.export_bom(
|
|
1508
|
+
... fields=['Reference', 'Value', 'Footprint', 'MPN'],
|
|
1509
|
+
... group_by=['Value', 'Footprint'],
|
|
1510
|
+
... exclude_dnp=True,
|
|
1511
|
+
... )
|
|
1512
|
+
"""
|
|
1513
|
+
from kicad_sch_api.cli.bom import export_bom
|
|
1514
|
+
|
|
1515
|
+
if not self._file_path:
|
|
1516
|
+
raise ValueError("Schematic must be saved before exporting BOM")
|
|
1517
|
+
|
|
1518
|
+
# Save first to ensure file is up-to-date
|
|
1519
|
+
self.save()
|
|
1520
|
+
|
|
1521
|
+
return export_bom(self._file_path, **kwargs)
|
|
1522
|
+
|
|
1523
|
+
def export_pdf(self, **kwargs):
|
|
1524
|
+
"""
|
|
1525
|
+
Export schematic as PDF.
|
|
1526
|
+
|
|
1527
|
+
This requires the schematic to be saved first.
|
|
1528
|
+
|
|
1529
|
+
Args:
|
|
1530
|
+
**kwargs: Arguments passed to cli.export_docs.export_pdf()
|
|
1531
|
+
- output_path: Path for PDF file
|
|
1532
|
+
- theme: Color theme
|
|
1533
|
+
- black_and_white: B&W export
|
|
1534
|
+
- And more options...
|
|
1535
|
+
|
|
1536
|
+
Returns:
|
|
1537
|
+
Path to generated PDF file
|
|
1538
|
+
|
|
1539
|
+
Example:
|
|
1540
|
+
>>> pdf = sch.export_pdf(theme='Kicad Classic')
|
|
1541
|
+
"""
|
|
1542
|
+
from kicad_sch_api.cli.export_docs import export_pdf
|
|
1543
|
+
|
|
1544
|
+
if not self._file_path:
|
|
1545
|
+
raise ValueError("Schematic must be saved before exporting PDF")
|
|
1546
|
+
|
|
1547
|
+
# Save first to ensure file is up-to-date
|
|
1548
|
+
self.save()
|
|
1549
|
+
|
|
1550
|
+
return export_pdf(self._file_path, **kwargs)
|
|
1551
|
+
|
|
1552
|
+
def export_svg(self, **kwargs):
|
|
1553
|
+
"""
|
|
1554
|
+
Export schematic as SVG.
|
|
1555
|
+
|
|
1556
|
+
This requires the schematic to be saved first.
|
|
1557
|
+
|
|
1558
|
+
Args:
|
|
1559
|
+
**kwargs: Arguments passed to cli.export_docs.export_svg()
|
|
1560
|
+
- output_dir: Output directory
|
|
1561
|
+
- theme: Color theme
|
|
1562
|
+
- black_and_white: B&W export
|
|
1563
|
+
- And more options...
|
|
1564
|
+
|
|
1565
|
+
Returns:
|
|
1566
|
+
List of paths to generated SVG files
|
|
1567
|
+
|
|
1568
|
+
Example:
|
|
1569
|
+
>>> svgs = sch.export_svg()
|
|
1570
|
+
>>> for svg in svgs:
|
|
1571
|
+
... print(f"Generated: {svg}")
|
|
1572
|
+
"""
|
|
1573
|
+
from kicad_sch_api.cli.export_docs import export_svg
|
|
1574
|
+
|
|
1575
|
+
if not self._file_path:
|
|
1576
|
+
raise ValueError("Schematic must be saved before exporting SVG")
|
|
1577
|
+
|
|
1578
|
+
# Save first to ensure file is up-to-date
|
|
1579
|
+
self.save()
|
|
1580
|
+
|
|
1581
|
+
return export_svg(self._file_path, **kwargs)
|
|
1582
|
+
|
|
1583
|
+
def export_dxf(self, **kwargs):
|
|
1584
|
+
"""
|
|
1585
|
+
Export schematic as DXF.
|
|
1586
|
+
|
|
1587
|
+
This requires the schematic to be saved first.
|
|
1588
|
+
|
|
1589
|
+
Args:
|
|
1590
|
+
**kwargs: Arguments passed to cli.export_docs.export_dxf()
|
|
1591
|
+
|
|
1592
|
+
Returns:
|
|
1593
|
+
List of paths to generated DXF files
|
|
1594
|
+
|
|
1595
|
+
Example:
|
|
1596
|
+
>>> dxfs = sch.export_dxf()
|
|
1597
|
+
"""
|
|
1598
|
+
from kicad_sch_api.cli.export_docs import export_dxf
|
|
1599
|
+
|
|
1600
|
+
if not self._file_path:
|
|
1601
|
+
raise ValueError("Schematic must be saved before exporting DXF")
|
|
1602
|
+
|
|
1603
|
+
# Save first to ensure file is up-to-date
|
|
1604
|
+
self.save()
|
|
1605
|
+
|
|
1606
|
+
return export_dxf(self._file_path, **kwargs)
|
|
1607
|
+
|
|
1545
1608
|
def __str__(self) -> str:
|
|
1546
1609
|
"""String representation."""
|
|
1547
1610
|
title = self.title_block.get("title", "Untitled")
|
kicad_sch_api/core/texts.py
CHANGED
|
@@ -10,6 +10,7 @@ import uuid
|
|
|
10
10
|
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
12
|
from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
|
|
13
|
+
from .collections import BaseCollection
|
|
13
14
|
from .types import Point, Text
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -124,10 +125,13 @@ class TextElement:
|
|
|
124
125
|
return f"<Text '{self.text}' @ {self.position}>"
|
|
125
126
|
|
|
126
127
|
|
|
127
|
-
class TextCollection:
|
|
128
|
+
class TextCollection(BaseCollection[TextElement]):
|
|
128
129
|
"""
|
|
129
130
|
Collection class for efficient text element management.
|
|
130
131
|
|
|
132
|
+
Inherits from BaseCollection for standard operations and adds text-specific
|
|
133
|
+
functionality including content-based indexing.
|
|
134
|
+
|
|
131
135
|
Provides fast lookup, filtering, and bulk operations for schematic text elements.
|
|
132
136
|
"""
|
|
133
137
|
|
|
@@ -138,18 +142,17 @@ class TextCollection:
|
|
|
138
142
|
Args:
|
|
139
143
|
texts: Initial list of text data
|
|
140
144
|
"""
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
# Initialize base collection with empty list (we'll add elements below)
|
|
146
|
+
super().__init__([], collection_name="texts")
|
|
147
|
+
|
|
148
|
+
# Additional text-specific index
|
|
143
149
|
self._content_index: Dict[str, List[TextElement]] = {}
|
|
144
|
-
self._modified = False
|
|
145
150
|
|
|
146
151
|
# Add initial texts
|
|
147
152
|
if texts:
|
|
148
153
|
for text_data in texts:
|
|
149
154
|
self._add_to_indexes(TextElement(text_data, self))
|
|
150
155
|
|
|
151
|
-
logger.debug(f"TextCollection initialized with {len(self._texts)} texts")
|
|
152
|
-
|
|
153
156
|
def add(
|
|
154
157
|
self,
|
|
155
158
|
text: str,
|
|
@@ -209,15 +212,10 @@ class TextCollection:
|
|
|
209
212
|
# Create wrapper and add to collection
|
|
210
213
|
text_element = TextElement(text_data, self)
|
|
211
214
|
self._add_to_indexes(text_element)
|
|
212
|
-
self._mark_modified()
|
|
213
215
|
|
|
214
216
|
logger.debug(f"Added text: {text_element}")
|
|
215
217
|
return text_element
|
|
216
218
|
|
|
217
|
-
def get(self, text_uuid: str) -> Optional[TextElement]:
|
|
218
|
-
"""Get text by UUID."""
|
|
219
|
-
return self._uuid_index.get(text_uuid)
|
|
220
|
-
|
|
221
219
|
def remove(self, text_uuid: str) -> bool:
|
|
222
220
|
"""
|
|
223
221
|
Remove text by UUID.
|
|
@@ -228,13 +226,19 @@ class TextCollection:
|
|
|
228
226
|
Returns:
|
|
229
227
|
True if text was removed, False if not found
|
|
230
228
|
"""
|
|
231
|
-
text_element = self.
|
|
229
|
+
text_element = self.get(text_uuid)
|
|
232
230
|
if not text_element:
|
|
233
231
|
return False
|
|
234
232
|
|
|
235
|
-
# Remove from
|
|
236
|
-
|
|
237
|
-
self.
|
|
233
|
+
# Remove from content index
|
|
234
|
+
content = text_element.text
|
|
235
|
+
if content in self._content_index:
|
|
236
|
+
self._content_index[content].remove(text_element)
|
|
237
|
+
if not self._content_index[content]:
|
|
238
|
+
del self._content_index[content]
|
|
239
|
+
|
|
240
|
+
# Remove using base class method
|
|
241
|
+
super().remove(text_uuid)
|
|
238
242
|
|
|
239
243
|
logger.debug(f"Removed text: {text_element}")
|
|
240
244
|
return True
|
|
@@ -254,14 +258,14 @@ class TextCollection:
|
|
|
254
258
|
return self._content_index.get(content, []).copy()
|
|
255
259
|
else:
|
|
256
260
|
matches = []
|
|
257
|
-
for text_element in self.
|
|
261
|
+
for text_element in self._items:
|
|
258
262
|
if content.lower() in text_element.text.lower():
|
|
259
263
|
matches.append(text_element)
|
|
260
264
|
return matches
|
|
261
265
|
|
|
262
266
|
def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
|
|
263
267
|
"""
|
|
264
|
-
Filter texts by predicate function.
|
|
268
|
+
Filter texts by predicate function (delegates to base class find).
|
|
265
269
|
|
|
266
270
|
Args:
|
|
267
271
|
predicate: Function that returns True for texts to include
|
|
@@ -269,7 +273,7 @@ class TextCollection:
|
|
|
269
273
|
Returns:
|
|
270
274
|
List of texts matching predicate
|
|
271
275
|
"""
|
|
272
|
-
return
|
|
276
|
+
return self.find(predicate)
|
|
273
277
|
|
|
274
278
|
def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
|
|
275
279
|
"""
|
|
@@ -280,7 +284,7 @@ class TextCollection:
|
|
|
280
284
|
updates: Dictionary of property updates
|
|
281
285
|
"""
|
|
282
286
|
updated_count = 0
|
|
283
|
-
for text_element in self.
|
|
287
|
+
for text_element in self._items:
|
|
284
288
|
if criteria(text_element):
|
|
285
289
|
for prop, value in updates.items():
|
|
286
290
|
if hasattr(text_element, prop):
|
|
@@ -293,15 +297,12 @@ class TextCollection:
|
|
|
293
297
|
|
|
294
298
|
def clear(self):
|
|
295
299
|
"""Remove all texts from collection."""
|
|
296
|
-
self._texts.clear()
|
|
297
|
-
self._uuid_index.clear()
|
|
298
300
|
self._content_index.clear()
|
|
299
|
-
|
|
301
|
+
super().clear()
|
|
300
302
|
|
|
301
303
|
def _add_to_indexes(self, text_element: TextElement):
|
|
302
|
-
"""Add text to internal indexes."""
|
|
303
|
-
self.
|
|
304
|
-
self._uuid_index[text_element.uuid] = text_element
|
|
304
|
+
"""Add text to internal indexes (base + content index)."""
|
|
305
|
+
self._add_item(text_element)
|
|
305
306
|
|
|
306
307
|
# Add to content index
|
|
307
308
|
content = text_element.text
|
|
@@ -309,35 +310,7 @@ class TextCollection:
|
|
|
309
310
|
self._content_index[content] = []
|
|
310
311
|
self._content_index[content].append(text_element)
|
|
311
312
|
|
|
312
|
-
|
|
313
|
-
"""Remove text from internal indexes."""
|
|
314
|
-
self._texts.remove(text_element)
|
|
315
|
-
del self._uuid_index[text_element.uuid]
|
|
316
|
-
|
|
317
|
-
# Remove from content index
|
|
318
|
-
content = text_element.text
|
|
319
|
-
if content in self._content_index:
|
|
320
|
-
self._content_index[content].remove(text_element)
|
|
321
|
-
if not self._content_index[content]:
|
|
322
|
-
del self._content_index[content]
|
|
323
|
-
|
|
324
|
-
def _mark_modified(self):
|
|
325
|
-
"""Mark collection as modified."""
|
|
326
|
-
self._modified = True
|
|
327
|
-
|
|
328
|
-
# Collection interface methods
|
|
329
|
-
def __len__(self) -> int:
|
|
330
|
-
"""Return number of texts."""
|
|
331
|
-
return len(self._texts)
|
|
332
|
-
|
|
333
|
-
def __iter__(self) -> Iterator[TextElement]:
|
|
334
|
-
"""Iterate over texts."""
|
|
335
|
-
return iter(self._texts)
|
|
336
|
-
|
|
337
|
-
def __getitem__(self, index: int) -> TextElement:
|
|
338
|
-
"""Get text by index."""
|
|
339
|
-
return self._texts[index]
|
|
340
|
-
|
|
313
|
+
# Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
|
|
341
314
|
def __bool__(self) -> bool:
|
|
342
315
|
"""Return True if collection has texts."""
|
|
343
|
-
return len(self.
|
|
316
|
+
return len(self._items) > 0
|