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
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Simple wire routing and connectivity detection for KiCAD schematics.
|
|
3
|
-
|
|
4
|
-
Provides basic wire routing between component pins and pin connectivity detection.
|
|
5
|
-
All positioning follows KiCAD's 1.27mm grid alignment rules.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import List, Optional, Tuple, Union
|
|
9
|
-
|
|
10
|
-
from .types import Point
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def snap_to_kicad_grid(
|
|
14
|
-
position: Union[Point, Tuple[float, float]], grid_size: float = 1.27
|
|
15
|
-
) -> Point:
|
|
16
|
-
"""
|
|
17
|
-
Snap position to KiCAD's standard 1.27mm grid.
|
|
18
|
-
|
|
19
|
-
KiCAD uses a 1.27mm (0.05 inch) grid for precise electrical connections.
|
|
20
|
-
ALL components, wires, and labels must be exactly on grid points.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
position: Point or (x, y) tuple to snap
|
|
24
|
-
grid_size: Grid size in mm (default 1.27mm)
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
Point snapped to grid
|
|
28
|
-
"""
|
|
29
|
-
if isinstance(position, Point):
|
|
30
|
-
x, y = position.x, position.y
|
|
31
|
-
else:
|
|
32
|
-
x, y = position
|
|
33
|
-
|
|
34
|
-
# Round to nearest grid point
|
|
35
|
-
snapped_x = round(x / grid_size) * grid_size
|
|
36
|
-
snapped_y = round(y / grid_size) * grid_size
|
|
37
|
-
|
|
38
|
-
return Point(snapped_x, snapped_y)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def calculate_component_position_for_grid_pins(
|
|
42
|
-
pin_offsets: List[Tuple[float, float]], target_pin_grid_pos: Point, target_pin_index: int = 0
|
|
43
|
-
) -> Point:
|
|
44
|
-
"""
|
|
45
|
-
Calculate component position so that pins land exactly on grid points.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
pin_offsets: List of (x_offset, y_offset) tuples for each pin relative to component center
|
|
49
|
-
target_pin_grid_pos: Desired grid position for the target pin
|
|
50
|
-
target_pin_index: Index of pin to align to grid (default: pin 0)
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
Component center position that places target pin on grid
|
|
54
|
-
"""
|
|
55
|
-
target_offset_x, target_offset_y = pin_offsets[target_pin_index]
|
|
56
|
-
|
|
57
|
-
# Component center = target pin position - pin offset
|
|
58
|
-
comp_center_x = target_pin_grid_pos.x - target_offset_x
|
|
59
|
-
comp_center_y = target_pin_grid_pos.y - target_offset_y
|
|
60
|
-
|
|
61
|
-
return Point(comp_center_x, comp_center_y)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def get_resistor_grid_position(target_pin1_grid: Point) -> Point:
|
|
65
|
-
"""
|
|
66
|
-
Get component position for Device:R resistor so pin 1 is at target grid position.
|
|
67
|
-
|
|
68
|
-
Device:R resistor pin offsets: Pin 1 = (0, +3.81), Pin 2 = (0, -3.81)
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
target_pin1_grid: Desired grid position for pin 1
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
Component center position
|
|
75
|
-
"""
|
|
76
|
-
# Device:R pin offsets (standard KiCAD library values)
|
|
77
|
-
pin_offsets = [(0.0, 3.81), (0.0, -3.81)] # Pin 1 and Pin 2 offsets
|
|
78
|
-
return calculate_component_position_for_grid_pins(pin_offsets, target_pin1_grid, 0)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def route_pins_direct(pin1_position: Point, pin2_position: Point) -> Point:
|
|
82
|
-
"""
|
|
83
|
-
Simple direct routing between two pins.
|
|
84
|
-
Just draws a straight wire - ok to go through other components.
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
pin1_position: Position of first pin
|
|
88
|
-
pin2_position: Position of second pin
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
End point (pin2_position) for wire creation
|
|
92
|
-
"""
|
|
93
|
-
return pin2_position
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def are_pins_connected(
|
|
97
|
-
schematic, comp1_ref: str, pin1_num: str, comp2_ref: str, pin2_num: str
|
|
98
|
-
) -> bool:
|
|
99
|
-
"""
|
|
100
|
-
Detect if two component pins are connected via wires or labels (local or hierarchical).
|
|
101
|
-
|
|
102
|
-
Checks for electrical connectivity through:
|
|
103
|
-
1. Direct wire connections
|
|
104
|
-
2. Indirect wire network connections
|
|
105
|
-
3. Local labels on connected nets
|
|
106
|
-
4. Hierarchical labels for inter-sheet connections
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
schematic: The schematic object to analyze
|
|
110
|
-
comp1_ref: Reference of first component (e.g., 'R1')
|
|
111
|
-
pin1_num: Pin number on first component (e.g., '1')
|
|
112
|
-
comp2_ref: Reference of second component (e.g., 'R2')
|
|
113
|
-
pin2_num: Pin number on second component (e.g., '2')
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
True if pins are electrically connected, False otherwise
|
|
117
|
-
"""
|
|
118
|
-
# Get pin positions
|
|
119
|
-
pin1_pos = schematic.get_component_pin_position(comp1_ref, pin1_num)
|
|
120
|
-
pin2_pos = schematic.get_component_pin_position(comp2_ref, pin2_num)
|
|
121
|
-
|
|
122
|
-
if not pin1_pos or not pin2_pos:
|
|
123
|
-
return False
|
|
124
|
-
|
|
125
|
-
# 1. Check for direct wire connection between the pins
|
|
126
|
-
for wire in schematic.wires:
|
|
127
|
-
wire_start = wire.points[0]
|
|
128
|
-
wire_end = wire.points[-1] # Last point for multi-segment wires
|
|
129
|
-
|
|
130
|
-
# Check if wire directly connects the two pins
|
|
131
|
-
if (
|
|
132
|
-
wire_start.x == pin1_pos.x
|
|
133
|
-
and wire_start.y == pin1_pos.y
|
|
134
|
-
and wire_end.x == pin2_pos.x
|
|
135
|
-
and wire_end.y == pin2_pos.y
|
|
136
|
-
) or (
|
|
137
|
-
wire_start.x == pin2_pos.x
|
|
138
|
-
and wire_start.y == pin2_pos.y
|
|
139
|
-
and wire_end.x == pin1_pos.x
|
|
140
|
-
and wire_end.y == pin1_pos.y
|
|
141
|
-
):
|
|
142
|
-
return True
|
|
143
|
-
|
|
144
|
-
# 2. Check for indirect connection through wire network
|
|
145
|
-
visited_wires = set()
|
|
146
|
-
if _pins_connected_via_wire_network(schematic, pin1_pos, pin2_pos, visited_wires):
|
|
147
|
-
return True
|
|
148
|
-
|
|
149
|
-
# 3. Check for connection via local labels
|
|
150
|
-
if _pins_connected_via_labels(schematic, pin1_pos, pin2_pos):
|
|
151
|
-
return True
|
|
152
|
-
|
|
153
|
-
# 4. Check for connection via hierarchical labels
|
|
154
|
-
if _pins_connected_via_hierarchical_labels(schematic, pin1_pos, pin2_pos):
|
|
155
|
-
return True
|
|
156
|
-
|
|
157
|
-
return False
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _pins_connected_via_wire_network(
|
|
161
|
-
schematic, pin1_pos: Point, pin2_pos: Point, visited_wires: set
|
|
162
|
-
) -> bool:
|
|
163
|
-
"""
|
|
164
|
-
Check if pins are connected through wire network tracing.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
schematic: The schematic object
|
|
168
|
-
pin1_pos: First pin position
|
|
169
|
-
pin2_pos: Second pin position
|
|
170
|
-
visited_wires: Set to track visited wires
|
|
171
|
-
|
|
172
|
-
Returns:
|
|
173
|
-
True if pins connected via wire network
|
|
174
|
-
"""
|
|
175
|
-
# Find all wires connected to pin1
|
|
176
|
-
pin1_wires = []
|
|
177
|
-
for wire in schematic.wires:
|
|
178
|
-
for point in wire.points:
|
|
179
|
-
if point.x == pin1_pos.x and point.y == pin1_pos.y:
|
|
180
|
-
pin1_wires.append(wire)
|
|
181
|
-
break
|
|
182
|
-
|
|
183
|
-
# For each wire connected to pin1, trace the network to see if it reaches pin2
|
|
184
|
-
visited_wire_uuids = set() # Use UUIDs instead of Wire objects
|
|
185
|
-
for start_wire in pin1_wires:
|
|
186
|
-
if _trace_wire_network(schematic, start_wire, pin2_pos, visited_wire_uuids):
|
|
187
|
-
return True
|
|
188
|
-
|
|
189
|
-
return False
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _pins_connected_via_labels(schematic, pin1_pos: Point, pin2_pos: Point) -> bool:
|
|
193
|
-
"""
|
|
194
|
-
Check if pins are connected via local labels (net names).
|
|
195
|
-
|
|
196
|
-
Two pins are connected if they're on nets with the same label name.
|
|
197
|
-
|
|
198
|
-
Args:
|
|
199
|
-
schematic: The schematic object
|
|
200
|
-
pin1_pos: First pin position
|
|
201
|
-
pin2_pos: Second pin position
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
True if pins connected via same local label
|
|
205
|
-
"""
|
|
206
|
-
# Get labels connected to pin1's net
|
|
207
|
-
pin1_labels = _get_net_labels_at_position(schematic, pin1_pos)
|
|
208
|
-
if not pin1_labels:
|
|
209
|
-
return False
|
|
210
|
-
|
|
211
|
-
# Get labels connected to pin2's net
|
|
212
|
-
pin2_labels = _get_net_labels_at_position(schematic, pin2_pos)
|
|
213
|
-
if not pin2_labels:
|
|
214
|
-
return False
|
|
215
|
-
|
|
216
|
-
# Check if any label names match (case-insensitive)
|
|
217
|
-
pin1_names = {label.text.upper() for label in pin1_labels}
|
|
218
|
-
pin2_names = {label.text.upper() for label in pin2_labels}
|
|
219
|
-
|
|
220
|
-
return bool(pin1_names.intersection(pin2_names))
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def _pins_connected_via_hierarchical_labels(schematic, pin1_pos: Point, pin2_pos: Point) -> bool:
|
|
224
|
-
"""
|
|
225
|
-
Check if pins are connected via hierarchical labels.
|
|
226
|
-
|
|
227
|
-
Hierarchical labels create connections between different sheets in a hierarchy.
|
|
228
|
-
|
|
229
|
-
Args:
|
|
230
|
-
schematic: The schematic object
|
|
231
|
-
pin1_pos: First pin position
|
|
232
|
-
pin2_pos: Second pin position
|
|
233
|
-
|
|
234
|
-
Returns:
|
|
235
|
-
True if pins connected via hierarchical labels
|
|
236
|
-
"""
|
|
237
|
-
# Get hierarchical labels connected to pin1's net
|
|
238
|
-
pin1_hier_labels = _get_hierarchical_labels_at_position(schematic, pin1_pos)
|
|
239
|
-
if not pin1_hier_labels:
|
|
240
|
-
return False
|
|
241
|
-
|
|
242
|
-
# Get hierarchical labels connected to pin2's net
|
|
243
|
-
pin2_hier_labels = _get_hierarchical_labels_at_position(schematic, pin2_pos)
|
|
244
|
-
if not pin2_hier_labels:
|
|
245
|
-
return False
|
|
246
|
-
|
|
247
|
-
# Check if any hierarchical label names match (case-insensitive)
|
|
248
|
-
pin1_names = {label.text.upper() for label in pin1_hier_labels}
|
|
249
|
-
pin2_names = {label.text.upper() for label in pin2_hier_labels}
|
|
250
|
-
|
|
251
|
-
return bool(pin1_names.intersection(pin2_names))
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def _get_net_labels_at_position(schematic, position: Point) -> List:
|
|
255
|
-
"""
|
|
256
|
-
Get all local labels connected to the wire network at the given position.
|
|
257
|
-
|
|
258
|
-
Uses coordinate proximity matching like kicad-skip (0.6mm tolerance).
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
schematic: The schematic object
|
|
262
|
-
position: Pin position to check
|
|
263
|
-
|
|
264
|
-
Returns:
|
|
265
|
-
List of labels connected to this position's net
|
|
266
|
-
"""
|
|
267
|
-
connected_labels = []
|
|
268
|
-
tolerance = 0.0 # Zero tolerance - KiCAD requires exact coordinate matching
|
|
269
|
-
|
|
270
|
-
# Find all wires connected to this position
|
|
271
|
-
connected_wires = []
|
|
272
|
-
for wire in schematic.wires:
|
|
273
|
-
for point in wire.points:
|
|
274
|
-
if point.x == position.x and point.y == position.y:
|
|
275
|
-
connected_wires.append(wire)
|
|
276
|
-
break
|
|
277
|
-
|
|
278
|
-
# Find labels near any connected wire points
|
|
279
|
-
labels_data = schematic._data.get("labels", [])
|
|
280
|
-
for label_dict in labels_data:
|
|
281
|
-
label_pos_dict = label_dict.get("position", {})
|
|
282
|
-
label_pos = Point(label_pos_dict.get("x", 0), label_pos_dict.get("y", 0))
|
|
283
|
-
|
|
284
|
-
# Check if label is near any connected wire
|
|
285
|
-
for wire in connected_wires:
|
|
286
|
-
for wire_point in wire.points:
|
|
287
|
-
if label_pos.x == wire_point.x and label_pos.y == wire_point.y:
|
|
288
|
-
# Create a simple object with text attribute
|
|
289
|
-
class SimpleLabel:
|
|
290
|
-
def __init__(self, text):
|
|
291
|
-
self.text = text
|
|
292
|
-
|
|
293
|
-
connected_labels.append(SimpleLabel(label_dict.get("text", "")))
|
|
294
|
-
break
|
|
295
|
-
|
|
296
|
-
return connected_labels
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def _get_hierarchical_labels_at_position(schematic, position: Point) -> List:
|
|
300
|
-
"""
|
|
301
|
-
Get all hierarchical labels connected to the wire network at the given position.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
schematic: The schematic object
|
|
305
|
-
position: Pin position to check
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
List of hierarchical labels connected to this position's net
|
|
309
|
-
"""
|
|
310
|
-
connected_labels = []
|
|
311
|
-
tolerance = 0.0 # Zero tolerance - KiCAD requires exact coordinate matching
|
|
312
|
-
|
|
313
|
-
# Find all wires connected to this position
|
|
314
|
-
connected_wires = []
|
|
315
|
-
for wire in schematic.wires:
|
|
316
|
-
for point in wire.points:
|
|
317
|
-
if point.x == position.x and point.y == position.y:
|
|
318
|
-
connected_wires.append(wire)
|
|
319
|
-
break
|
|
320
|
-
|
|
321
|
-
# Find hierarchical labels near any connected wire points
|
|
322
|
-
hier_labels_data = schematic._data.get("hierarchical_labels", [])
|
|
323
|
-
for label_dict in hier_labels_data:
|
|
324
|
-
label_pos_dict = label_dict.get("position", {})
|
|
325
|
-
label_pos = Point(label_pos_dict.get("x", 0), label_pos_dict.get("y", 0))
|
|
326
|
-
|
|
327
|
-
# Check if hierarchical label is near any connected wire
|
|
328
|
-
for wire in connected_wires:
|
|
329
|
-
for wire_point in wire.points:
|
|
330
|
-
if label_pos.x == wire_point.x and label_pos.y == wire_point.y:
|
|
331
|
-
# Create a simple object with text attribute
|
|
332
|
-
class SimpleLabel:
|
|
333
|
-
def __init__(self, text):
|
|
334
|
-
self.text = text
|
|
335
|
-
|
|
336
|
-
connected_labels.append(SimpleLabel(label_dict.get("text", "")))
|
|
337
|
-
break
|
|
338
|
-
|
|
339
|
-
return connected_labels
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def _trace_wire_network(
|
|
343
|
-
schematic, current_wire, target_position: Point, visited_uuids: set
|
|
344
|
-
) -> bool:
|
|
345
|
-
"""
|
|
346
|
-
Recursively trace wire network to find if it reaches target position.
|
|
347
|
-
|
|
348
|
-
Args:
|
|
349
|
-
schematic: The schematic object
|
|
350
|
-
current_wire: Current wire being traced
|
|
351
|
-
target_position: Target pin position we're looking for
|
|
352
|
-
visited_uuids: Set of already visited wire UUIDs to prevent infinite loops
|
|
353
|
-
|
|
354
|
-
Returns:
|
|
355
|
-
True if network reaches target position
|
|
356
|
-
"""
|
|
357
|
-
wire_uuid = current_wire.uuid
|
|
358
|
-
if wire_uuid in visited_uuids:
|
|
359
|
-
return False
|
|
360
|
-
|
|
361
|
-
visited_uuids.add(wire_uuid)
|
|
362
|
-
|
|
363
|
-
# Check if current wire reaches target
|
|
364
|
-
for point in current_wire.points:
|
|
365
|
-
if point.x == target_position.x and point.y == target_position.y:
|
|
366
|
-
return True
|
|
367
|
-
|
|
368
|
-
# Find other wires connected to this wire's endpoints and trace them
|
|
369
|
-
for wire_point in current_wire.points:
|
|
370
|
-
for other_wire in schematic.wires:
|
|
371
|
-
if other_wire.uuid == wire_uuid or other_wire.uuid in visited_uuids:
|
|
372
|
-
continue
|
|
373
|
-
|
|
374
|
-
# Check if other wire shares an endpoint with current wire
|
|
375
|
-
for other_point in other_wire.points:
|
|
376
|
-
if wire_point.x == other_point.x and wire_point.y == other_point.y:
|
|
377
|
-
if _trace_wire_network(schematic, other_wire, target_position, visited_uuids):
|
|
378
|
-
return True
|
|
379
|
-
|
|
380
|
-
return False
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Label element parser for S-expression label definitions.
|
|
3
|
-
|
|
4
|
-
Handles parsing of text labels, hierarchical labels, and other text elements.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
|
9
|
-
|
|
10
|
-
import sexpdata
|
|
11
|
-
|
|
12
|
-
from .base import BaseElementParser
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class LabelParser(BaseElementParser):
|
|
18
|
-
"""Parser for label S-expression elements."""
|
|
19
|
-
|
|
20
|
-
def __init__(self):
|
|
21
|
-
"""Initialize label parser."""
|
|
22
|
-
super().__init__("label")
|
|
23
|
-
|
|
24
|
-
def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
25
|
-
"""
|
|
26
|
-
Parse a label S-expression element.
|
|
27
|
-
|
|
28
|
-
Expected format:
|
|
29
|
-
(label "text" (at x y angle) (effects (font (size s s))))
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
element: Label S-expression element
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
Parsed label data with text, position, and formatting
|
|
36
|
-
"""
|
|
37
|
-
if len(element) < 2:
|
|
38
|
-
return None
|
|
39
|
-
|
|
40
|
-
label_data = {
|
|
41
|
-
"text": str(element[1]),
|
|
42
|
-
"position": {"x": 0, "y": 0, "angle": 0},
|
|
43
|
-
"effects": {
|
|
44
|
-
"font_size": 1.27,
|
|
45
|
-
"font_thickness": 0.15,
|
|
46
|
-
"bold": False,
|
|
47
|
-
"italic": False,
|
|
48
|
-
"hide": False,
|
|
49
|
-
"justify": [],
|
|
50
|
-
},
|
|
51
|
-
"uuid": None,
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
for elem in element[2:]:
|
|
55
|
-
if not isinstance(elem, list):
|
|
56
|
-
continue
|
|
57
|
-
|
|
58
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
59
|
-
|
|
60
|
-
if elem_type == "at":
|
|
61
|
-
self._parse_position(elem, label_data)
|
|
62
|
-
elif elem_type == "effects":
|
|
63
|
-
label_data["effects"] = self._parse_effects(elem)
|
|
64
|
-
elif elem_type == "uuid":
|
|
65
|
-
label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
66
|
-
|
|
67
|
-
return label_data
|
|
68
|
-
|
|
69
|
-
def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
|
|
70
|
-
"""
|
|
71
|
-
Parse position from at element.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
at_element: (at x y [angle])
|
|
75
|
-
label_data: Label data dictionary to update
|
|
76
|
-
"""
|
|
77
|
-
try:
|
|
78
|
-
if len(at_element) >= 3:
|
|
79
|
-
label_data["position"]["x"] = float(at_element[1])
|
|
80
|
-
label_data["position"]["y"] = float(at_element[2])
|
|
81
|
-
if len(at_element) >= 4:
|
|
82
|
-
label_data["position"]["angle"] = float(at_element[3])
|
|
83
|
-
except (ValueError, IndexError) as e:
|
|
84
|
-
self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
|
|
85
|
-
|
|
86
|
-
def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
|
|
87
|
-
"""
|
|
88
|
-
Parse effects element for text formatting.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
effects_element: (effects (font ...) (justify ...) ...)
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
Parsed effects data
|
|
95
|
-
"""
|
|
96
|
-
effects = {
|
|
97
|
-
"font_size": 1.27,
|
|
98
|
-
"font_thickness": 0.15,
|
|
99
|
-
"bold": False,
|
|
100
|
-
"italic": False,
|
|
101
|
-
"hide": False,
|
|
102
|
-
"justify": [],
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
for elem in effects_element[1:]:
|
|
106
|
-
if isinstance(elem, list) and len(elem) > 0:
|
|
107
|
-
elem_type = str(elem[0])
|
|
108
|
-
if elem_type == "font":
|
|
109
|
-
self._parse_font(elem, effects)
|
|
110
|
-
elif elem_type == "justify":
|
|
111
|
-
effects["justify"] = [str(j) for j in elem[1:]]
|
|
112
|
-
elif elem_type == "hide":
|
|
113
|
-
effects["hide"] = True
|
|
114
|
-
|
|
115
|
-
return effects
|
|
116
|
-
|
|
117
|
-
def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
|
|
118
|
-
"""
|
|
119
|
-
Parse font element within effects.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
font_element: (font (size w h) (thickness t) ...)
|
|
123
|
-
effects: Effects dictionary to update
|
|
124
|
-
"""
|
|
125
|
-
for elem in font_element[1:]:
|
|
126
|
-
if isinstance(elem, list) and len(elem) > 0:
|
|
127
|
-
elem_type = str(elem[0])
|
|
128
|
-
if elem_type == "size":
|
|
129
|
-
try:
|
|
130
|
-
if len(elem) >= 3:
|
|
131
|
-
# Usually (size width height), use width for font_size
|
|
132
|
-
effects["font_size"] = float(elem[1])
|
|
133
|
-
except (ValueError, IndexError):
|
|
134
|
-
pass
|
|
135
|
-
elif elem_type == "thickness":
|
|
136
|
-
try:
|
|
137
|
-
effects["font_thickness"] = float(elem[1])
|
|
138
|
-
except (ValueError, IndexError):
|
|
139
|
-
pass
|
|
140
|
-
elif elem_type == "bold":
|
|
141
|
-
effects["bold"] = True
|
|
142
|
-
elif elem_type == "italic":
|
|
143
|
-
effects["italic"] = True
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class HierarchicalLabelParser(BaseElementParser):
|
|
147
|
-
"""Parser for hierarchical label S-expression elements."""
|
|
148
|
-
|
|
149
|
-
def __init__(self):
|
|
150
|
-
"""Initialize hierarchical label parser."""
|
|
151
|
-
super().__init__("hierarchical_label")
|
|
152
|
-
|
|
153
|
-
def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
|
|
154
|
-
"""
|
|
155
|
-
Parse a hierarchical label S-expression element.
|
|
156
|
-
|
|
157
|
-
Expected format:
|
|
158
|
-
(hierarchical_label "text" (shape input) (at x y angle) ...)
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
element: Hierarchical label S-expression element
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
Parsed hierarchical label data
|
|
165
|
-
"""
|
|
166
|
-
if len(element) < 2:
|
|
167
|
-
return None
|
|
168
|
-
|
|
169
|
-
label_data = {
|
|
170
|
-
"text": str(element[1]),
|
|
171
|
-
"shape": "input", # Default shape
|
|
172
|
-
"position": {"x": 0, "y": 0, "angle": 0},
|
|
173
|
-
"effects": {
|
|
174
|
-
"font_size": 1.27,
|
|
175
|
-
"font_thickness": 0.15,
|
|
176
|
-
"bold": False,
|
|
177
|
-
"italic": False,
|
|
178
|
-
"hide": False,
|
|
179
|
-
"justify": [],
|
|
180
|
-
},
|
|
181
|
-
"uuid": None,
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
for elem in element[2:]:
|
|
185
|
-
if not isinstance(elem, list):
|
|
186
|
-
continue
|
|
187
|
-
|
|
188
|
-
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
189
|
-
|
|
190
|
-
if elem_type == "shape":
|
|
191
|
-
label_data["shape"] = str(elem[1]) if len(elem) > 1 else "input"
|
|
192
|
-
elif elem_type == "at":
|
|
193
|
-
self._parse_position(elem, label_data)
|
|
194
|
-
elif elem_type == "effects":
|
|
195
|
-
label_data["effects"] = self._parse_effects(elem)
|
|
196
|
-
elif elem_type == "uuid":
|
|
197
|
-
label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
198
|
-
|
|
199
|
-
return label_data
|
|
200
|
-
|
|
201
|
-
def _parse_position(self, at_element: List[Any], label_data: Dict[str, Any]) -> None:
|
|
202
|
-
"""Parse position from at element."""
|
|
203
|
-
try:
|
|
204
|
-
if len(at_element) >= 3:
|
|
205
|
-
label_data["position"]["x"] = float(at_element[1])
|
|
206
|
-
label_data["position"]["y"] = float(at_element[2])
|
|
207
|
-
if len(at_element) >= 4:
|
|
208
|
-
label_data["position"]["angle"] = float(at_element[3])
|
|
209
|
-
except (ValueError, IndexError) as e:
|
|
210
|
-
self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
|
|
211
|
-
|
|
212
|
-
def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
|
|
213
|
-
"""Parse effects element for text formatting."""
|
|
214
|
-
effects = {
|
|
215
|
-
"font_size": 1.27,
|
|
216
|
-
"font_thickness": 0.15,
|
|
217
|
-
"bold": False,
|
|
218
|
-
"italic": False,
|
|
219
|
-
"hide": False,
|
|
220
|
-
"justify": [],
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
for elem in effects_element[1:]:
|
|
224
|
-
if isinstance(elem, list) and len(elem) > 0:
|
|
225
|
-
elem_type = str(elem[0])
|
|
226
|
-
if elem_type == "font":
|
|
227
|
-
self._parse_font(elem, effects)
|
|
228
|
-
elif elem_type == "justify":
|
|
229
|
-
effects["justify"] = [str(j) for j in elem[1:]]
|
|
230
|
-
elif elem_type == "hide":
|
|
231
|
-
effects["hide"] = True
|
|
232
|
-
|
|
233
|
-
return effects
|
|
234
|
-
|
|
235
|
-
def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
|
|
236
|
-
"""Parse font element within effects."""
|
|
237
|
-
for elem in font_element[1:]:
|
|
238
|
-
if isinstance(elem, list) and len(elem) > 0:
|
|
239
|
-
elem_type = str(elem[0])
|
|
240
|
-
if elem_type == "size":
|
|
241
|
-
try:
|
|
242
|
-
if len(elem) >= 3:
|
|
243
|
-
effects["font_size"] = float(elem[1])
|
|
244
|
-
except (ValueError, IndexError):
|
|
245
|
-
pass
|
|
246
|
-
elif elem_type == "thickness":
|
|
247
|
-
try:
|
|
248
|
-
effects["font_thickness"] = float(elem[1])
|
|
249
|
-
except (ValueError, IndexError):
|
|
250
|
-
pass
|
|
251
|
-
elif elem_type == "bold":
|
|
252
|
-
effects["bold"] = True
|
|
253
|
-
elif elem_type == "italic":
|
|
254
|
-
effects["italic"] = True
|