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/config.py
CHANGED
|
@@ -6,8 +6,8 @@ This module centralizes all magic numbers and configuration values
|
|
|
6
6
|
to make them easily configurable and maintainable.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
from typing import Any, Dict, Tuple
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Dict, List, Tuple
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
@dataclass
|
|
@@ -57,20 +57,102 @@ class DefaultValues:
|
|
|
57
57
|
|
|
58
58
|
project_name: str = "untitled"
|
|
59
59
|
stroke_width: float = 0.0
|
|
60
|
+
stroke_type: str = "default"
|
|
61
|
+
fill_type: str = "none"
|
|
60
62
|
font_size: float = 1.27
|
|
61
63
|
pin_name_size: float = 1.27
|
|
62
64
|
pin_number_size: float = 1.27
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
@dataclass
|
|
68
|
+
class FileFormatConstants:
|
|
69
|
+
"""KiCAD file format identifiers and version strings."""
|
|
70
|
+
|
|
71
|
+
file_type: str = "kicad_sch"
|
|
72
|
+
generator_default: str = "eeschema"
|
|
73
|
+
version_default: str = "20250114"
|
|
74
|
+
generator_version_default: str = "9.0"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class PaperSizeConstants:
|
|
79
|
+
"""Standard paper size definitions."""
|
|
80
|
+
|
|
81
|
+
default: str = "A4"
|
|
82
|
+
valid_sizes: List[str] = field(
|
|
83
|
+
default_factory=lambda: ["A4", "A3", "A2", "A1", "A0", "Letter", "Legal", "Tabloid"]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class FieldNames:
|
|
89
|
+
"""Common S-expression field names to avoid typos."""
|
|
90
|
+
|
|
91
|
+
# File structure
|
|
92
|
+
version: str = "version"
|
|
93
|
+
generator: str = "generator"
|
|
94
|
+
generator_version: str = "generator_version"
|
|
95
|
+
uuid: str = "uuid"
|
|
96
|
+
paper: str = "paper"
|
|
97
|
+
|
|
98
|
+
# Positioning
|
|
99
|
+
at: str = "at"
|
|
100
|
+
xy: str = "xy"
|
|
101
|
+
pts: str = "pts"
|
|
102
|
+
start: str = "start"
|
|
103
|
+
end: str = "end"
|
|
104
|
+
mid: str = "mid"
|
|
105
|
+
center: str = "center"
|
|
106
|
+
radius: str = "radius"
|
|
107
|
+
|
|
108
|
+
# Styling
|
|
109
|
+
stroke: str = "stroke"
|
|
110
|
+
fill: str = "fill"
|
|
111
|
+
width: str = "width"
|
|
112
|
+
type: str = "type"
|
|
113
|
+
color: str = "color"
|
|
114
|
+
|
|
115
|
+
# Text/Font
|
|
116
|
+
font: str = "font"
|
|
117
|
+
size: str = "size"
|
|
118
|
+
effects: str = "effects"
|
|
119
|
+
|
|
120
|
+
# Components
|
|
121
|
+
pin: str = "pin"
|
|
122
|
+
property: str = "property"
|
|
123
|
+
symbol: str = "symbol"
|
|
124
|
+
lib_id: str = "lib_id"
|
|
125
|
+
|
|
126
|
+
# Graphics
|
|
127
|
+
polyline: str = "polyline"
|
|
128
|
+
arc: str = "arc"
|
|
129
|
+
circle: str = "circle"
|
|
130
|
+
rectangle: str = "rectangle"
|
|
131
|
+
bezier: str = "bezier"
|
|
132
|
+
|
|
133
|
+
# Connection elements
|
|
134
|
+
wire: str = "wire"
|
|
135
|
+
junction: str = "junction"
|
|
136
|
+
no_connect: str = "no_connect"
|
|
137
|
+
label: str = "label"
|
|
138
|
+
|
|
139
|
+
# Hierarchical
|
|
140
|
+
sheet: str = "sheet"
|
|
141
|
+
sheet_instances: str = "sheet_instances"
|
|
142
|
+
|
|
143
|
+
|
|
65
144
|
class KiCADConfig:
|
|
66
145
|
"""Central configuration class for KiCAD schematic API."""
|
|
67
146
|
|
|
68
|
-
def __init__(self):
|
|
147
|
+
def __init__(self) -> None:
|
|
69
148
|
self.properties = PropertyOffsets()
|
|
70
149
|
self.grid = GridSettings()
|
|
71
150
|
self.sheet = SheetSettings()
|
|
72
151
|
self.tolerance = ToleranceSettings()
|
|
73
152
|
self.defaults = DefaultValues()
|
|
153
|
+
self.file_format = FileFormatConstants()
|
|
154
|
+
self.paper = PaperSizeConstants()
|
|
155
|
+
self.fields = FieldNames()
|
|
74
156
|
|
|
75
157
|
# Names that should not generate title_block (for backward compatibility)
|
|
76
158
|
# Include test schematic names to maintain reference compatibility
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Element Factory for creating schematic elements from dictionaries.
|
|
3
|
+
|
|
4
|
+
Centralizes object creation logic that was previously duplicated in Schematic.__init__.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
from ..types import (
|
|
11
|
+
HierarchicalLabelShape,
|
|
12
|
+
Junction,
|
|
13
|
+
Label,
|
|
14
|
+
LabelType,
|
|
15
|
+
Net,
|
|
16
|
+
NoConnect,
|
|
17
|
+
Point,
|
|
18
|
+
Text,
|
|
19
|
+
Wire,
|
|
20
|
+
WireType,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def point_from_dict_or_tuple(position: Any) -> Point:
|
|
25
|
+
"""Convert position data (dict or tuple) to Point object."""
|
|
26
|
+
if isinstance(position, dict):
|
|
27
|
+
return Point(position.get("x", 0), position.get("y", 0))
|
|
28
|
+
elif isinstance(position, (list, tuple)):
|
|
29
|
+
return Point(position[0], position[1])
|
|
30
|
+
elif isinstance(position, Point):
|
|
31
|
+
return position
|
|
32
|
+
else:
|
|
33
|
+
return Point(0, 0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ElementFactory:
|
|
37
|
+
"""Factory for creating schematic elements from dictionary data."""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def create_wire(wire_dict: Dict[str, Any]) -> Wire:
|
|
41
|
+
"""
|
|
42
|
+
Create Wire object from dictionary.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
wire_dict: Dictionary containing wire data
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Wire object
|
|
49
|
+
"""
|
|
50
|
+
points = []
|
|
51
|
+
for point_data in wire_dict.get("points", []):
|
|
52
|
+
if isinstance(point_data, dict):
|
|
53
|
+
points.append(Point(point_data["x"], point_data["y"]))
|
|
54
|
+
elif isinstance(point_data, (list, tuple)):
|
|
55
|
+
points.append(Point(point_data[0], point_data[1]))
|
|
56
|
+
else:
|
|
57
|
+
points.append(point_data)
|
|
58
|
+
|
|
59
|
+
return Wire(
|
|
60
|
+
uuid=wire_dict.get("uuid", str(uuid.uuid4())),
|
|
61
|
+
points=points,
|
|
62
|
+
wire_type=WireType(wire_dict.get("wire_type", "wire")),
|
|
63
|
+
stroke_width=wire_dict.get("stroke_width", 0.0),
|
|
64
|
+
stroke_type=wire_dict.get("stroke_type", "default"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def create_junction(junction_dict: Dict[str, Any]) -> Junction:
|
|
69
|
+
"""
|
|
70
|
+
Create Junction object from dictionary.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
junction_dict: Dictionary containing junction data
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Junction object
|
|
77
|
+
"""
|
|
78
|
+
position = junction_dict.get("position", {"x": 0, "y": 0})
|
|
79
|
+
pos = point_from_dict_or_tuple(position)
|
|
80
|
+
|
|
81
|
+
return Junction(
|
|
82
|
+
uuid=junction_dict.get("uuid", str(uuid.uuid4())),
|
|
83
|
+
position=pos,
|
|
84
|
+
diameter=junction_dict.get("diameter", 0),
|
|
85
|
+
color=junction_dict.get("color", (0, 0, 0, 0)),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def create_text(text_dict: Dict[str, Any]) -> Text:
|
|
90
|
+
"""
|
|
91
|
+
Create Text object from dictionary.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
text_dict: Dictionary containing text data
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Text object
|
|
98
|
+
"""
|
|
99
|
+
position = text_dict.get("position", {"x": 0, "y": 0})
|
|
100
|
+
pos = point_from_dict_or_tuple(position)
|
|
101
|
+
|
|
102
|
+
return Text(
|
|
103
|
+
uuid=text_dict.get("uuid", str(uuid.uuid4())),
|
|
104
|
+
position=pos,
|
|
105
|
+
text=text_dict.get("text", ""),
|
|
106
|
+
rotation=text_dict.get("rotation", 0.0),
|
|
107
|
+
size=text_dict.get("size", 1.27),
|
|
108
|
+
exclude_from_sim=text_dict.get("exclude_from_sim", False),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def create_label(label_dict: Dict[str, Any]) -> Label:
|
|
113
|
+
"""
|
|
114
|
+
Create Label object from dictionary.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
label_dict: Dictionary containing label data
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Label object
|
|
121
|
+
"""
|
|
122
|
+
position = label_dict.get("position", {"x": 0, "y": 0})
|
|
123
|
+
pos = point_from_dict_or_tuple(position)
|
|
124
|
+
|
|
125
|
+
return Label(
|
|
126
|
+
uuid=label_dict.get("uuid", str(uuid.uuid4())),
|
|
127
|
+
position=pos,
|
|
128
|
+
text=label_dict.get("text", ""),
|
|
129
|
+
label_type=LabelType(label_dict.get("label_type", "local")),
|
|
130
|
+
rotation=label_dict.get("rotation", 0.0),
|
|
131
|
+
size=label_dict.get("size", 1.27),
|
|
132
|
+
shape=(
|
|
133
|
+
HierarchicalLabelShape(label_dict.get("shape"))
|
|
134
|
+
if label_dict.get("shape")
|
|
135
|
+
else None
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def create_no_connect(no_connect_dict: Dict[str, Any]) -> NoConnect:
|
|
141
|
+
"""
|
|
142
|
+
Create NoConnect object from dictionary.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
no_connect_dict: Dictionary containing no-connect data
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
NoConnect object
|
|
149
|
+
"""
|
|
150
|
+
position = no_connect_dict.get("position", {"x": 0, "y": 0})
|
|
151
|
+
pos = point_from_dict_or_tuple(position)
|
|
152
|
+
|
|
153
|
+
return NoConnect(
|
|
154
|
+
uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
|
|
155
|
+
position=pos,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def create_net(net_dict: Dict[str, Any]) -> Net:
|
|
160
|
+
"""
|
|
161
|
+
Create Net object from dictionary.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
net_dict: Dictionary containing net data
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Net object
|
|
168
|
+
"""
|
|
169
|
+
return Net(
|
|
170
|
+
name=net_dict.get("name", ""),
|
|
171
|
+
components=net_dict.get("components", []),
|
|
172
|
+
wires=net_dict.get("wires", []),
|
|
173
|
+
labels=net_dict.get("labels", []),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def create_wires_from_list(wire_data: List[Any]) -> List[Wire]:
|
|
178
|
+
"""
|
|
179
|
+
Create list of Wire objects from list of dictionaries.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
wire_data: List of wire dictionaries
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of Wire objects
|
|
186
|
+
"""
|
|
187
|
+
wires = []
|
|
188
|
+
for wire_dict in wire_data:
|
|
189
|
+
if isinstance(wire_dict, dict):
|
|
190
|
+
wires.append(ElementFactory.create_wire(wire_dict))
|
|
191
|
+
return wires
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def create_junctions_from_list(junction_data: List[Any]) -> List[Junction]:
|
|
195
|
+
"""
|
|
196
|
+
Create list of Junction objects from list of dictionaries.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
junction_data: List of junction dictionaries
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of Junction objects
|
|
203
|
+
"""
|
|
204
|
+
junctions = []
|
|
205
|
+
for junction_dict in junction_data:
|
|
206
|
+
if isinstance(junction_dict, dict):
|
|
207
|
+
junctions.append(ElementFactory.create_junction(junction_dict))
|
|
208
|
+
return junctions
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def create_texts_from_list(text_data: List[Any]) -> List[Text]:
|
|
212
|
+
"""
|
|
213
|
+
Create list of Text objects from list of dictionaries.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
text_data: List of text dictionaries
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of Text objects
|
|
220
|
+
"""
|
|
221
|
+
texts = []
|
|
222
|
+
for text_dict in text_data:
|
|
223
|
+
if isinstance(text_dict, dict):
|
|
224
|
+
texts.append(ElementFactory.create_text(text_dict))
|
|
225
|
+
return texts
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def create_labels_from_list(label_data: List[Any]) -> List[Label]:
|
|
229
|
+
"""
|
|
230
|
+
Create list of Label objects from list of dictionaries.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
label_data: List of label dictionaries
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of Label objects
|
|
237
|
+
"""
|
|
238
|
+
labels = []
|
|
239
|
+
for label_dict in label_data:
|
|
240
|
+
if isinstance(label_dict, dict):
|
|
241
|
+
labels.append(ElementFactory.create_label(label_dict))
|
|
242
|
+
return labels
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def create_no_connects_from_list(no_connect_data: List[Any]) -> List[NoConnect]:
|
|
246
|
+
"""
|
|
247
|
+
Create list of NoConnect objects from list of dictionaries.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
no_connect_data: List of no-connect dictionaries
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of NoConnect objects
|
|
254
|
+
"""
|
|
255
|
+
no_connects = []
|
|
256
|
+
for no_connect_dict in no_connect_data:
|
|
257
|
+
if isinstance(no_connect_dict, dict):
|
|
258
|
+
no_connects.append(ElementFactory.create_no_connect(no_connect_dict))
|
|
259
|
+
return no_connects
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def create_nets_from_list(net_data: List[Any]) -> List[Net]:
|
|
263
|
+
"""
|
|
264
|
+
Create list of Net objects from list of dictionaries.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
net_data: List of net dictionaries
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
List of Net objects
|
|
271
|
+
"""
|
|
272
|
+
nets = []
|
|
273
|
+
for net_dict in net_data:
|
|
274
|
+
if isinstance(net_dict, dict):
|
|
275
|
+
nets.append(ElementFactory.create_net(net_dict))
|
|
276
|
+
return nets
|
kicad_sch_api/core/formatter.py
CHANGED
|
@@ -125,6 +125,10 @@ class ExactFormatter:
|
|
|
125
125
|
self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
|
|
126
126
|
self.rules["hierarchical_label"] = FormatRule(inline=False, quote_indices={1})
|
|
127
127
|
|
|
128
|
+
# Text elements
|
|
129
|
+
self.rules["text"] = FormatRule(inline=False, quote_indices={1})
|
|
130
|
+
self.rules["text_box"] = FormatRule(inline=False, quote_indices={1})
|
|
131
|
+
|
|
128
132
|
# Effects and text formatting
|
|
129
133
|
self.rules["effects"] = FormatRule(inline=False)
|
|
130
134
|
self.rules["font"] = FormatRule(inline=False)
|
|
@@ -279,6 +283,8 @@ class ExactFormatter:
|
|
|
279
283
|
"junction",
|
|
280
284
|
"label",
|
|
281
285
|
"hierarchical_label",
|
|
286
|
+
"text",
|
|
287
|
+
"text_box",
|
|
282
288
|
"polyline",
|
|
283
289
|
"rectangle",
|
|
284
290
|
):
|
|
@@ -384,7 +390,8 @@ class ExactFormatter:
|
|
|
384
390
|
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
385
391
|
else:
|
|
386
392
|
if i in rule.quote_indices and isinstance(element, str):
|
|
387
|
-
|
|
393
|
+
escaped_element = self._escape_string(element)
|
|
394
|
+
result += f' "{escaped_element}"'
|
|
388
395
|
else:
|
|
389
396
|
result += f" {self._format_element(element, 0)}"
|
|
390
397
|
|
|
@@ -396,7 +403,8 @@ class ExactFormatter:
|
|
|
396
403
|
indent = "\t" * indent_level
|
|
397
404
|
next_indent = "\t" * (indent_level + 1)
|
|
398
405
|
|
|
399
|
-
|
|
406
|
+
tag = str(lst[0])
|
|
407
|
+
result = f"({tag}"
|
|
400
408
|
|
|
401
409
|
for i, element in enumerate(lst[1:], 1):
|
|
402
410
|
if isinstance(element, list):
|
|
@@ -425,9 +433,18 @@ class ExactFormatter:
|
|
|
425
433
|
return True
|
|
426
434
|
|
|
427
435
|
def _escape_string(self, text: str) -> str:
|
|
428
|
-
"""Escape
|
|
429
|
-
#
|
|
430
|
-
|
|
436
|
+
"""Escape special characters in string for S-expression formatting."""
|
|
437
|
+
# Escape backslashes first (must be done before other replacements)
|
|
438
|
+
text = text.replace('\\', '\\\\')
|
|
439
|
+
# Escape double quotes
|
|
440
|
+
text = text.replace('"', '\\"')
|
|
441
|
+
# Escape newlines (convert actual newlines to escaped representation)
|
|
442
|
+
text = text.replace('\n', '\\n')
|
|
443
|
+
# Escape carriage returns
|
|
444
|
+
text = text.replace('\r', '\\r')
|
|
445
|
+
# Escape tabs
|
|
446
|
+
text = text.replace('\t', '\\t')
|
|
447
|
+
return text
|
|
431
448
|
|
|
432
449
|
def _needs_quoting(self, text: str) -> bool:
|
|
433
450
|
"""Check if string needs to be quoted."""
|
kicad_sch_api/core/junctions.py
CHANGED
|
@@ -9,55 +9,34 @@ import logging
|
|
|
9
9
|
import uuid as uuid_module
|
|
10
10
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
|
+
from .collections import BaseCollection
|
|
12
13
|
from .types import Junction, Point
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class JunctionCollection:
|
|
18
|
+
class JunctionCollection(BaseCollection[Junction]):
|
|
18
19
|
"""
|
|
19
20
|
Professional junction collection with enhanced management features.
|
|
20
21
|
|
|
22
|
+
Inherits from BaseCollection for standard operations and adds junction-specific
|
|
23
|
+
functionality.
|
|
24
|
+
|
|
21
25
|
Features:
|
|
22
|
-
- Fast UUID-based lookup and indexing
|
|
26
|
+
- Fast UUID-based lookup and indexing (inherited)
|
|
23
27
|
- Position-based junction queries
|
|
24
|
-
- Bulk operations for performance
|
|
28
|
+
- Bulk operations for performance (inherited)
|
|
25
29
|
- Validation and conflict detection
|
|
26
30
|
"""
|
|
27
31
|
|
|
28
|
-
def __init__(self, junctions: Optional[List[Junction]] = None):
|
|
32
|
+
def __init__(self, junctions: Optional[List[Junction]] = None) -> None:
|
|
29
33
|
"""
|
|
30
34
|
Initialize junction collection.
|
|
31
35
|
|
|
32
36
|
Args:
|
|
33
37
|
junctions: Initial list of junctions
|
|
34
38
|
"""
|
|
35
|
-
|
|
36
|
-
self._uuid_index: Dict[str, int] = {}
|
|
37
|
-
self._modified = False
|
|
38
|
-
|
|
39
|
-
# Build UUID index
|
|
40
|
-
self._rebuild_index()
|
|
41
|
-
|
|
42
|
-
logger.debug(f"JunctionCollection initialized with {len(self._junctions)} junctions")
|
|
43
|
-
|
|
44
|
-
def _rebuild_index(self):
|
|
45
|
-
"""Rebuild UUID index for fast lookups."""
|
|
46
|
-
self._uuid_index = {junction.uuid: i for i, junction in enumerate(self._junctions)}
|
|
47
|
-
|
|
48
|
-
def __len__(self) -> int:
|
|
49
|
-
"""Number of junctions in collection."""
|
|
50
|
-
return len(self._junctions)
|
|
51
|
-
|
|
52
|
-
def __iter__(self):
|
|
53
|
-
"""Iterate over junctions."""
|
|
54
|
-
return iter(self._junctions)
|
|
55
|
-
|
|
56
|
-
def __getitem__(self, uuid: str) -> Junction:
|
|
57
|
-
"""Get junction by UUID."""
|
|
58
|
-
if uuid not in self._uuid_index:
|
|
59
|
-
raise KeyError(f"Junction with UUID '{uuid}' not found")
|
|
60
|
-
return self._junctions[self._uuid_index[uuid]]
|
|
39
|
+
super().__init__(junctions, collection_name="junctions")
|
|
61
40
|
|
|
62
41
|
def add(
|
|
63
42
|
self,
|
|
@@ -94,35 +73,12 @@ class JunctionCollection:
|
|
|
94
73
|
# Create junction
|
|
95
74
|
junction = Junction(uuid=uuid, position=position, diameter=diameter, color=color)
|
|
96
75
|
|
|
97
|
-
# Add to collection
|
|
98
|
-
self.
|
|
99
|
-
self._uuid_index[uuid] = len(self._junctions) - 1
|
|
100
|
-
self._modified = True
|
|
76
|
+
# Add to collection using base class method
|
|
77
|
+
self._add_item(junction)
|
|
101
78
|
|
|
102
79
|
logger.debug(f"Added junction at {position}, UUID={uuid}")
|
|
103
80
|
return uuid
|
|
104
81
|
|
|
105
|
-
def remove(self, uuid: str) -> bool:
|
|
106
|
-
"""
|
|
107
|
-
Remove junction by UUID.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
uuid: Junction UUID to remove
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
True if junction was removed, False if not found
|
|
114
|
-
"""
|
|
115
|
-
if uuid not in self._uuid_index:
|
|
116
|
-
return False
|
|
117
|
-
|
|
118
|
-
index = self._uuid_index[uuid]
|
|
119
|
-
del self._junctions[index]
|
|
120
|
-
self._rebuild_index()
|
|
121
|
-
self._modified = True
|
|
122
|
-
|
|
123
|
-
logger.debug(f"Removed junction: {uuid}")
|
|
124
|
-
return True
|
|
125
|
-
|
|
126
82
|
def get_at_position(
|
|
127
83
|
self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.01
|
|
128
84
|
) -> Optional[Junction]:
|
|
@@ -139,7 +95,7 @@ class JunctionCollection:
|
|
|
139
95
|
if isinstance(position, tuple):
|
|
140
96
|
position = Point(position[0], position[1])
|
|
141
97
|
|
|
142
|
-
for junction in self.
|
|
98
|
+
for junction in self._items:
|
|
143
99
|
if junction.position.distance_to(position) <= tolerance:
|
|
144
100
|
return junction
|
|
145
101
|
|
|
@@ -162,40 +118,35 @@ class JunctionCollection:
|
|
|
162
118
|
point = Point(point[0], point[1])
|
|
163
119
|
|
|
164
120
|
matching_junctions = []
|
|
165
|
-
for junction in self.
|
|
121
|
+
for junction in self._items:
|
|
166
122
|
if junction.position.distance_to(point) <= tolerance:
|
|
167
123
|
matching_junctions.append(junction)
|
|
168
124
|
|
|
169
125
|
return matching_junctions
|
|
170
126
|
|
|
171
127
|
def get_statistics(self) -> Dict[str, Any]:
|
|
172
|
-
"""Get junction collection statistics."""
|
|
173
|
-
|
|
174
|
-
|
|
128
|
+
"""Get junction collection statistics (extends base statistics)."""
|
|
129
|
+
base_stats = super().get_statistics()
|
|
130
|
+
if not self._items:
|
|
131
|
+
return {**base_stats, "total_junctions": 0, "avg_diameter": 0, "positions": []}
|
|
175
132
|
|
|
176
|
-
avg_diameter = sum(j.diameter for j in self.
|
|
177
|
-
positions = [(j.position.x, j.position.y) for j in self.
|
|
133
|
+
avg_diameter = sum(j.diameter for j in self._items) / len(self._items)
|
|
134
|
+
positions = [(j.position.x, j.position.y) for j in self._items]
|
|
178
135
|
|
|
179
136
|
return {
|
|
180
|
-
|
|
137
|
+
**base_stats,
|
|
138
|
+
"total_junctions": len(self._items),
|
|
181
139
|
"avg_diameter": avg_diameter,
|
|
182
140
|
"positions": positions,
|
|
183
|
-
"unique_diameters": len(set(j.diameter for j in self.
|
|
184
|
-
"unique_colors": len(set(j.color for j in self.
|
|
141
|
+
"unique_diameters": len(set(j.diameter for j in self._items)),
|
|
142
|
+
"unique_colors": len(set(j.color for j in self._items)),
|
|
185
143
|
}
|
|
186
144
|
|
|
187
|
-
def clear(self):
|
|
188
|
-
"""Remove all junctions from collection."""
|
|
189
|
-
self._junctions.clear()
|
|
190
|
-
self._uuid_index.clear()
|
|
191
|
-
self._modified = True
|
|
192
|
-
logger.debug("Cleared all junctions")
|
|
193
|
-
|
|
194
145
|
@property
|
|
195
146
|
def modified(self) -> bool:
|
|
196
147
|
"""Check if collection has been modified."""
|
|
197
|
-
return self.
|
|
148
|
+
return self.is_modified()
|
|
198
149
|
|
|
199
|
-
def mark_saved(self):
|
|
150
|
+
def mark_saved(self) -> None:
|
|
200
151
|
"""Mark collection as saved (reset modified flag)."""
|
|
201
|
-
self.
|
|
152
|
+
self.reset_modified_flag()
|