drawsvg-ui 0.4.0__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.
- app_info.py +61 -0
- canvas_view.py +2506 -0
- constants.py +49 -0
- drawsvg_ui-0.4.0.dist-info/METADATA +86 -0
- drawsvg_ui-0.4.0.dist-info/RECORD +28 -0
- drawsvg_ui-0.4.0.dist-info/WHEEL +5 -0
- drawsvg_ui-0.4.0.dist-info/entry_points.txt +2 -0
- drawsvg_ui-0.4.0.dist-info/top_level.txt +11 -0
- export_drawsvg.py +1700 -0
- import_drawsvg.py +807 -0
- items/__init__.py +66 -0
- items/base.py +606 -0
- items/labels.py +247 -0
- items/shapes/__init__.py +20 -0
- items/shapes/curves.py +139 -0
- items/shapes/lines.py +439 -0
- items/shapes/polygons.py +359 -0
- items/shapes/rects.py +310 -0
- items/text.py +331 -0
- items/widgets/__init__.py +5 -0
- items/widgets/folder_tree.py +415 -0
- main.py +23 -0
- main_window.py +254 -0
- palette.py +556 -0
- properties_panel.py +1406 -0
- ui/__init__.py +1 -0
- ui/main_window.ui +157 -0
- ui/properties_panel.ui +996 -0
import_drawsvg.py
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import math
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from PySide6 import QtCore, QtGui, QtWidgets
|
|
11
|
+
|
|
12
|
+
from items import (
|
|
13
|
+
BlockArrowItem,
|
|
14
|
+
CurvyBracketItem,
|
|
15
|
+
DiamondItem,
|
|
16
|
+
EllipseItem,
|
|
17
|
+
FolderTreeItem,
|
|
18
|
+
LineItem,
|
|
19
|
+
RectItem,
|
|
20
|
+
ShapeLabelMixin,
|
|
21
|
+
SplitRoundedRectItem,
|
|
22
|
+
TextItem,
|
|
23
|
+
TriangleItem,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from constants import DEFAULTS, PEN_STYLE_DASH_ARRAYS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_ROT_RE = re.compile(r"rotate\(([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\)")
|
|
30
|
+
_DASH_ARRAY_TO_STYLE = {
|
|
31
|
+
tuple(round(val, 2) for val in pattern): style
|
|
32
|
+
for style, pattern in PEN_STYLE_DASH_ARRAYS.items()
|
|
33
|
+
if pattern
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_call(line: str) -> tuple[list[Any], dict[str, Any]]:
|
|
38
|
+
"""Parse a drawsvg call line and return args and kwargs."""
|
|
39
|
+
call_src = line.split("=", 1)[1].strip()
|
|
40
|
+
node = ast.parse(call_src, mode="eval").body
|
|
41
|
+
args = []
|
|
42
|
+
for a in node.args:
|
|
43
|
+
if isinstance(a, ast.Name):
|
|
44
|
+
args.append(a.id)
|
|
45
|
+
else:
|
|
46
|
+
args.append(ast.literal_eval(a))
|
|
47
|
+
kwargs: dict[str, Any] = {}
|
|
48
|
+
for kw in node.keywords:
|
|
49
|
+
v = kw.value
|
|
50
|
+
if isinstance(v, ast.Name):
|
|
51
|
+
kwargs[kw.arg] = v.id
|
|
52
|
+
else:
|
|
53
|
+
kwargs[kw.arg] = ast.literal_eval(v)
|
|
54
|
+
return args, kwargs
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _apply_style(item: QtWidgets.QGraphicsItem, kwargs: dict[str, Any]) -> None:
|
|
58
|
+
if isinstance(item, (QtWidgets.QGraphicsRectItem, QtWidgets.QGraphicsEllipseItem, LineItem, TriangleItem, DiamondItem, BlockArrowItem)):
|
|
59
|
+
if kwargs.get("fill") == "none":
|
|
60
|
+
item.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
|
61
|
+
elif "fill" in kwargs:
|
|
62
|
+
color = QtGui.QColor(kwargs["fill"])
|
|
63
|
+
if "fill_opacity" in kwargs:
|
|
64
|
+
color.setAlphaF(float(kwargs["fill_opacity"]))
|
|
65
|
+
item.setBrush(color)
|
|
66
|
+
pen = item.pen()
|
|
67
|
+
if "stroke" in kwargs:
|
|
68
|
+
pen.setColor(QtGui.QColor(kwargs["stroke"]))
|
|
69
|
+
if "stroke_width" in kwargs:
|
|
70
|
+
pen.setWidthF(float(kwargs["stroke_width"]))
|
|
71
|
+
dash_pattern = None
|
|
72
|
+
dash_value = kwargs.get("stroke_dasharray")
|
|
73
|
+
if dash_value is not None:
|
|
74
|
+
if isinstance(dash_value, (list, tuple)):
|
|
75
|
+
dash_pattern = [float(v) for v in dash_value]
|
|
76
|
+
else:
|
|
77
|
+
parts = str(dash_value).replace(",", " ").split()
|
|
78
|
+
try:
|
|
79
|
+
dash_pattern = [float(part) for part in parts]
|
|
80
|
+
except ValueError:
|
|
81
|
+
dash_pattern = []
|
|
82
|
+
style_to_apply: QtCore.Qt.PenStyle | None = None
|
|
83
|
+
if dash_pattern is not None:
|
|
84
|
+
if not dash_pattern:
|
|
85
|
+
style_to_apply = QtCore.Qt.PenStyle.SolidLine
|
|
86
|
+
else:
|
|
87
|
+
rounded = tuple(round(val, 2) for val in dash_pattern)
|
|
88
|
+
style_to_apply = _DASH_ARRAY_TO_STYLE.get(rounded)
|
|
89
|
+
if style_to_apply is not None:
|
|
90
|
+
pen.setStyle(style_to_apply)
|
|
91
|
+
elif dash_pattern:
|
|
92
|
+
pen.setStyle(QtCore.Qt.PenStyle.CustomDashLine)
|
|
93
|
+
pen.setDashPattern(dash_pattern)
|
|
94
|
+
item.setPen(pen)
|
|
95
|
+
if isinstance(item, LineItem) and style_to_apply is not None:
|
|
96
|
+
item.set_pen_style(style_to_apply)
|
|
97
|
+
elif isinstance(item, TextItem):
|
|
98
|
+
if "fill" in kwargs:
|
|
99
|
+
color = QtGui.QColor(kwargs["fill"])
|
|
100
|
+
if "fill_opacity" in kwargs:
|
|
101
|
+
color.setAlphaF(float(kwargs["fill_opacity"]))
|
|
102
|
+
item.setDefaultTextColor(color)
|
|
103
|
+
if "font_family" in kwargs:
|
|
104
|
+
font = item.font()
|
|
105
|
+
font.setFamily(str(kwargs["font_family"]))
|
|
106
|
+
item.setFont(font)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_rotate(val: str) -> float:
|
|
110
|
+
m = _ROT_RE.match(val)
|
|
111
|
+
if m:
|
|
112
|
+
return float(m.group(1))
|
|
113
|
+
return 0.0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def import_drawsvg_py(scene: QtWidgets.QGraphicsScene, parent: QtWidgets.QWidget | None = None) -> None:
|
|
117
|
+
path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
|
118
|
+
parent, "Load drawsvg-.py…", "", "Python (*.py)"
|
|
119
|
+
)
|
|
120
|
+
if not path:
|
|
121
|
+
return
|
|
122
|
+
try:
|
|
123
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
124
|
+
lines = f.readlines()
|
|
125
|
+
|
|
126
|
+
cleared = False
|
|
127
|
+
view = scene.parent()
|
|
128
|
+
if view is not None:
|
|
129
|
+
clear_method = getattr(view, "clear_canvas", None)
|
|
130
|
+
if callable(clear_method):
|
|
131
|
+
clear_method()
|
|
132
|
+
cleared = True
|
|
133
|
+
if not cleared:
|
|
134
|
+
scene.clear()
|
|
135
|
+
|
|
136
|
+
pending_split: dict[str, Any] | None = None
|
|
137
|
+
pending_block: dict[str, Any] | None = None
|
|
138
|
+
pending_bracket: dict[str, Any] | None = None
|
|
139
|
+
pending_line: LineItem | None = None
|
|
140
|
+
|
|
141
|
+
# Neu: Mapping von Label-ID -> Ziel-Shape sowie Sammelcontainer für mehrzeilige Label
|
|
142
|
+
shape_label_targets: dict[str, ShapeLabelMixin] = {}
|
|
143
|
+
shape_label_pending: dict[str, dict[str, Any]] = {}
|
|
144
|
+
|
|
145
|
+
def _apply_shape_label(target: ShapeLabelMixin, data: dict[str, Any]) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Wendet gesammelte Label-Daten auf ein Shape an.
|
|
148
|
+
Unterstützt sowohl 'text' (alt) als auch 'lines' (neu, mehrere Zeilen).
|
|
149
|
+
NBSP (U+00A0) wird als leere Zeile interpretiert.
|
|
150
|
+
"""
|
|
151
|
+
if "lines" in data and isinstance(data["lines"], list):
|
|
152
|
+
norm = [("" if (s == "\u00A0" or s == " ") else str(s)) for s in data["lines"]]
|
|
153
|
+
text_value = "\n".join(norm)
|
|
154
|
+
else:
|
|
155
|
+
text_value = str(data.get("text", ""))
|
|
156
|
+
|
|
157
|
+
target.set_label_text(text_value)
|
|
158
|
+
target.set_label_alignment(
|
|
159
|
+
horizontal=str(data.get("h")) if data.get("h") else None,
|
|
160
|
+
vertical=str(data.get("v")) if data.get("v") else None,
|
|
161
|
+
)
|
|
162
|
+
font_px = data.get("font_px")
|
|
163
|
+
if font_px is not None:
|
|
164
|
+
try:
|
|
165
|
+
target.set_label_font_pixel_size(float(font_px))
|
|
166
|
+
except (TypeError, ValueError):
|
|
167
|
+
pass
|
|
168
|
+
color_value = data.get("color")
|
|
169
|
+
if color_value is not None:
|
|
170
|
+
color = QtGui.QColor(str(color_value))
|
|
171
|
+
if color.isValid():
|
|
172
|
+
override_flag = data.get("color_override")
|
|
173
|
+
override = False
|
|
174
|
+
if isinstance(override_flag, str):
|
|
175
|
+
override = override_flag.strip().lower() in {"1", "true", "yes", "on"}
|
|
176
|
+
elif isinstance(override_flag, (int, float)):
|
|
177
|
+
override = bool(override_flag)
|
|
178
|
+
elif isinstance(override_flag, bool):
|
|
179
|
+
override = override_flag
|
|
180
|
+
if override:
|
|
181
|
+
target.set_label_color(color)
|
|
182
|
+
else:
|
|
183
|
+
target.reset_label_color(update=True, base_color=color)
|
|
184
|
+
|
|
185
|
+
for raw in lines:
|
|
186
|
+
line = raw.strip()
|
|
187
|
+
if not line:
|
|
188
|
+
pending_split = None
|
|
189
|
+
pending_block = None
|
|
190
|
+
pending_bracket = None
|
|
191
|
+
pending_line = None
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
if line.startswith("#"):
|
|
195
|
+
if line.startswith("# SplitRoundedRect"):
|
|
196
|
+
info: dict[str, Any] = {}
|
|
197
|
+
for part in line.split()[2:]:
|
|
198
|
+
if "=" not in part:
|
|
199
|
+
continue
|
|
200
|
+
key, value = part.split("=", 1)
|
|
201
|
+
value = value.rstrip(",")
|
|
202
|
+
if value.startswith("'") and value.endswith("'"):
|
|
203
|
+
value = value[1:-1]
|
|
204
|
+
info[key] = value
|
|
205
|
+
pending_split = info
|
|
206
|
+
elif line.startswith("# BlockArrow"):
|
|
207
|
+
info = {}
|
|
208
|
+
for part in line.split()[2:]:
|
|
209
|
+
if "=" not in part:
|
|
210
|
+
continue
|
|
211
|
+
key, value = part.split("=", 1)
|
|
212
|
+
value = value.rstrip(",")
|
|
213
|
+
if value.startswith("'") and value.endswith("'"):
|
|
214
|
+
value = value[1:-1]
|
|
215
|
+
info[key] = value
|
|
216
|
+
pending_block = info
|
|
217
|
+
elif line.startswith("# CurvyBracket"):
|
|
218
|
+
info = {}
|
|
219
|
+
for part in line.split()[2:]:
|
|
220
|
+
if "=" not in part:
|
|
221
|
+
continue
|
|
222
|
+
key, value = part.split("=", 1)
|
|
223
|
+
value = value.rstrip(",")
|
|
224
|
+
if value.startswith("'") and value.endswith("'"):
|
|
225
|
+
value = value[1:-1]
|
|
226
|
+
info[key] = value
|
|
227
|
+
pending_bracket = info
|
|
228
|
+
elif line.startswith("# FolderTree"):
|
|
229
|
+
info: dict[str, Any] = {}
|
|
230
|
+
pos_match = re.search(r"pos=\(([-0-9.]+),\s*([-0-9.]+)\)", line)
|
|
231
|
+
if pos_match:
|
|
232
|
+
info["x"] = float(pos_match.group(1))
|
|
233
|
+
info["y"] = float(pos_match.group(2))
|
|
234
|
+
rot_match = re.search(r"rotation=([-0-9.]+)", line)
|
|
235
|
+
if rot_match:
|
|
236
|
+
info["rotation"] = float(rot_match.group(1))
|
|
237
|
+
size_match = re.search(r"size=\(([-0-9.]+),\s*([-0-9.]+)\)", line)
|
|
238
|
+
if size_match:
|
|
239
|
+
info["w"] = float(size_match.group(1))
|
|
240
|
+
info["h"] = float(size_match.group(2))
|
|
241
|
+
struct_index = line.find("structure=")
|
|
242
|
+
structure_data: Any = None
|
|
243
|
+
if struct_index != -1:
|
|
244
|
+
struct_text = line[struct_index + len("structure=") :].strip()
|
|
245
|
+
try:
|
|
246
|
+
structure_data = json.loads(struct_text)
|
|
247
|
+
except json.JSONDecodeError:
|
|
248
|
+
structure_data = None
|
|
249
|
+
info["structure"] = structure_data
|
|
250
|
+
x = float(info.get("x", 0.0))
|
|
251
|
+
y = float(info.get("y", 0.0))
|
|
252
|
+
w = float(info.get("w", DEFAULTS["Folder Tree"][0]))
|
|
253
|
+
h = float(info.get("h", DEFAULTS["Folder Tree"][1]))
|
|
254
|
+
structure = structure_data if isinstance(structure_data, Mapping) else None
|
|
255
|
+
item = FolderTreeItem(x, y, w, h, structure)
|
|
256
|
+
rotation = float(info.get("rotation", 0.0))
|
|
257
|
+
if rotation:
|
|
258
|
+
item.setRotation(rotation)
|
|
259
|
+
item.setData(0, "Folder Tree")
|
|
260
|
+
scene.addItem(item)
|
|
261
|
+
elif line.startswith("# Arrowheads:") and pending_line is not None:
|
|
262
|
+
comment = line.split(":", 1)[1]
|
|
263
|
+
start_flag = False
|
|
264
|
+
end_flag = False
|
|
265
|
+
length_value: float | None = None
|
|
266
|
+
width_value: float | None = None
|
|
267
|
+
for part in comment.split(","):
|
|
268
|
+
if "=" not in part:
|
|
269
|
+
continue
|
|
270
|
+
key, value = part.split("=", 1)
|
|
271
|
+
key = key.strip().lower()
|
|
272
|
+
raw_value = value.strip()
|
|
273
|
+
lower_value = raw_value.lower()
|
|
274
|
+
if key == "start":
|
|
275
|
+
start_flag = lower_value in {"true", "1", "yes"}
|
|
276
|
+
elif key == "end":
|
|
277
|
+
end_flag = lower_value in {"true", "1", "yes"}
|
|
278
|
+
elif key == "length":
|
|
279
|
+
try:
|
|
280
|
+
length_value = float(raw_value)
|
|
281
|
+
except (TypeError, ValueError):
|
|
282
|
+
length_value = None
|
|
283
|
+
elif key == "width":
|
|
284
|
+
try:
|
|
285
|
+
width_value = float(raw_value)
|
|
286
|
+
except (TypeError, ValueError):
|
|
287
|
+
width_value = None
|
|
288
|
+
if start_flag:
|
|
289
|
+
pending_line.set_arrow_start(True)
|
|
290
|
+
if end_flag:
|
|
291
|
+
pending_line.set_arrow_end(True)
|
|
292
|
+
if length_value is not None:
|
|
293
|
+
setter = getattr(pending_line, "set_arrow_head_length", None)
|
|
294
|
+
if callable(setter):
|
|
295
|
+
setter(length_value)
|
|
296
|
+
if width_value is not None:
|
|
297
|
+
setter = getattr(pending_line, "set_arrow_head_width", None)
|
|
298
|
+
if callable(setter):
|
|
299
|
+
setter(width_value)
|
|
300
|
+
pending_line.setData(
|
|
301
|
+
0, "Arrow" if (start_flag or end_flag) else "Line"
|
|
302
|
+
)
|
|
303
|
+
pending_line = None
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
if line.startswith("d = draw.Drawing("):
|
|
307
|
+
args, kwargs = _parse_call(line)
|
|
308
|
+
if len(args) >= 2:
|
|
309
|
+
ox = oy = 0.0
|
|
310
|
+
if "origin" in kwargs and isinstance(kwargs["origin"], (tuple, list)):
|
|
311
|
+
ox, oy = map(float, kwargs["origin"][:2])
|
|
312
|
+
scene.setSceneRect(float(ox), float(oy), float(args[0]), float(args[1]))
|
|
313
|
+
|
|
314
|
+
elif line.startswith("_folder_tree"):
|
|
315
|
+
continue
|
|
316
|
+
|
|
317
|
+
elif line.startswith("_split_rect = draw.Rectangle("):
|
|
318
|
+
args, kwargs = _parse_call(line)
|
|
319
|
+
x, y, w, h = map(float, args[:4])
|
|
320
|
+
rx = min(float(kwargs.get("rx", 0.0)), 50.0)
|
|
321
|
+
ry = min(float(kwargs.get("ry", rx)), 50.0)
|
|
322
|
+
if "rx" in kwargs and "ry" not in kwargs:
|
|
323
|
+
ry = rx
|
|
324
|
+
if "ry" in kwargs and "rx" not in kwargs:
|
|
325
|
+
rx = ry
|
|
326
|
+
item = SplitRoundedRectItem(x, y, w, h, rx, ry)
|
|
327
|
+
_apply_style(item, kwargs)
|
|
328
|
+
if pending_split is not None:
|
|
329
|
+
ratio_val = pending_split.get("ratio")
|
|
330
|
+
if ratio_val is not None:
|
|
331
|
+
try:
|
|
332
|
+
item.set_divider_ratio(float(ratio_val))
|
|
333
|
+
except (TypeError, ValueError):
|
|
334
|
+
pass
|
|
335
|
+
top_fill = pending_split.get("top_fill")
|
|
336
|
+
if top_fill == "none":
|
|
337
|
+
item.setTopBrush(QtGui.QBrush(QtCore.Qt.BrushStyle.NoBrush))
|
|
338
|
+
elif top_fill:
|
|
339
|
+
color = QtGui.QColor(str(top_fill))
|
|
340
|
+
opacity = pending_split.get("top_opacity")
|
|
341
|
+
if opacity is not None:
|
|
342
|
+
try:
|
|
343
|
+
color.setAlphaF(float(opacity))
|
|
344
|
+
except (TypeError, ValueError):
|
|
345
|
+
pass
|
|
346
|
+
item.setTopBrush(color)
|
|
347
|
+
if "transform" in kwargs:
|
|
348
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
349
|
+
item.setData(0, "Split Rounded Rectangle")
|
|
350
|
+
scene.addItem(item)
|
|
351
|
+
pending_split = None
|
|
352
|
+
|
|
353
|
+
elif line.startswith("_rect = draw.Rectangle("):
|
|
354
|
+
args, kwargs = _parse_call(line)
|
|
355
|
+
x, y, w, h = map(float, args[:4])
|
|
356
|
+
rx = min(float(kwargs.get("rx", 0.0)), 50.0)
|
|
357
|
+
ry = min(float(kwargs.get("ry", 0.0)), 50.0)
|
|
358
|
+
if "rx" in kwargs and "ry" not in kwargs:
|
|
359
|
+
ry = rx
|
|
360
|
+
if "ry" in kwargs and "rx" not in kwargs:
|
|
361
|
+
rx = ry
|
|
362
|
+
item = RectItem(x, y, w, h, rx, ry)
|
|
363
|
+
_apply_style(item, kwargs)
|
|
364
|
+
if "transform" in kwargs:
|
|
365
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
366
|
+
shape_name = "Rounded Rectangle" if (rx or ry) else "Rectangle"
|
|
367
|
+
item.setData(0, shape_name)
|
|
368
|
+
label_id = kwargs.get("data_label_id")
|
|
369
|
+
if label_id:
|
|
370
|
+
key = str(label_id)
|
|
371
|
+
shape_label_targets[key] = item
|
|
372
|
+
pending = shape_label_pending.pop(key, None)
|
|
373
|
+
if pending:
|
|
374
|
+
_apply_shape_label(item, pending)
|
|
375
|
+
scene.addItem(item)
|
|
376
|
+
|
|
377
|
+
elif line.startswith("_ell = draw.Ellipse("):
|
|
378
|
+
args, kwargs = _parse_call(line)
|
|
379
|
+
cx, cy, rx, ry = map(float, args[:4])
|
|
380
|
+
x = cx - rx
|
|
381
|
+
y = cy - ry
|
|
382
|
+
w = 2 * rx
|
|
383
|
+
h = 2 * ry
|
|
384
|
+
item = EllipseItem(x, y, w, h)
|
|
385
|
+
_apply_style(item, kwargs)
|
|
386
|
+
if "transform" in kwargs:
|
|
387
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
388
|
+
item.setData(0, "Ellipse")
|
|
389
|
+
label_id = kwargs.get("data_label_id")
|
|
390
|
+
if label_id:
|
|
391
|
+
key = str(label_id)
|
|
392
|
+
shape_label_targets[key] = item
|
|
393
|
+
pending = shape_label_pending.pop(key, None)
|
|
394
|
+
if pending:
|
|
395
|
+
_apply_shape_label(item, pending)
|
|
396
|
+
scene.addItem(item)
|
|
397
|
+
|
|
398
|
+
elif line.startswith("_circ = draw.Circle("):
|
|
399
|
+
args, kwargs = _parse_call(line)
|
|
400
|
+
cx, cy, r = map(float, args[:3])
|
|
401
|
+
x = cx - r
|
|
402
|
+
y = cy - r
|
|
403
|
+
w = h = 2 * r
|
|
404
|
+
item = EllipseItem(x, y, w, h)
|
|
405
|
+
_apply_style(item, kwargs)
|
|
406
|
+
if "transform" in kwargs:
|
|
407
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
408
|
+
item.setData(0, "Circle")
|
|
409
|
+
label_id = kwargs.get("data_label_id")
|
|
410
|
+
if label_id:
|
|
411
|
+
key = str(label_id)
|
|
412
|
+
shape_label_targets[key] = item
|
|
413
|
+
pending = shape_label_pending.pop(key, None)
|
|
414
|
+
if pending:
|
|
415
|
+
_apply_shape_label(item, pending)
|
|
416
|
+
scene.addItem(item)
|
|
417
|
+
|
|
418
|
+
elif line.startswith("_tri = draw.Lines("):
|
|
419
|
+
args, kwargs = _parse_call(line)
|
|
420
|
+
coords = [float(a) for a in args]
|
|
421
|
+
xs = coords[0::2]
|
|
422
|
+
ys = coords[1::2]
|
|
423
|
+
x = min(xs)
|
|
424
|
+
y = min(ys)
|
|
425
|
+
w = max(xs) - x
|
|
426
|
+
h = max(ys) - y
|
|
427
|
+
item = TriangleItem(x, y, w, h)
|
|
428
|
+
_apply_style(item, kwargs)
|
|
429
|
+
if "transform" in kwargs:
|
|
430
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
431
|
+
item.setData(0, "Triangle")
|
|
432
|
+
scene.addItem(item)
|
|
433
|
+
|
|
434
|
+
elif line.startswith("_diamond = draw.Lines("):
|
|
435
|
+
args, kwargs = _parse_call(line)
|
|
436
|
+
coords = [float(a) for a in args]
|
|
437
|
+
xs = coords[0::2]
|
|
438
|
+
ys = coords[1::2]
|
|
439
|
+
x = min(xs)
|
|
440
|
+
y = min(ys)
|
|
441
|
+
w = max(xs) - x
|
|
442
|
+
h = max(ys) - y
|
|
443
|
+
item = DiamondItem(x, y, w, h)
|
|
444
|
+
_apply_style(item, kwargs)
|
|
445
|
+
if "transform" in kwargs:
|
|
446
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
447
|
+
item.setData(0, "Diamond")
|
|
448
|
+
label_id = kwargs.get("data_label_id")
|
|
449
|
+
if label_id:
|
|
450
|
+
key = str(label_id)
|
|
451
|
+
shape_label_targets[key] = item
|
|
452
|
+
pending = shape_label_pending.pop(key, None)
|
|
453
|
+
if pending:
|
|
454
|
+
_apply_shape_label(item, pending)
|
|
455
|
+
scene.addItem(item)
|
|
456
|
+
|
|
457
|
+
elif line.startswith("_block_arrow = draw.Lines("):
|
|
458
|
+
args, kwargs = _parse_call(line)
|
|
459
|
+
coords = [float(a) for a in args]
|
|
460
|
+
xs = coords[0::2]
|
|
461
|
+
ys = coords[1::2]
|
|
462
|
+
x = min(xs)
|
|
463
|
+
y = min(ys)
|
|
464
|
+
w = max(xs) - x
|
|
465
|
+
h = max(ys) - y
|
|
466
|
+
item = BlockArrowItem(x, y, w, h)
|
|
467
|
+
_apply_style(item, kwargs)
|
|
468
|
+
if pending_block is not None:
|
|
469
|
+
head_ratio = pending_block.get("head_ratio")
|
|
470
|
+
shaft_ratio = pending_block.get("shaft_ratio")
|
|
471
|
+
if head_ratio is not None:
|
|
472
|
+
try:
|
|
473
|
+
item.set_head_ratio(float(head_ratio), update_handles=False)
|
|
474
|
+
except (TypeError, ValueError):
|
|
475
|
+
pass
|
|
476
|
+
if shaft_ratio is not None:
|
|
477
|
+
try:
|
|
478
|
+
item.set_shaft_ratio(float(shaft_ratio))
|
|
479
|
+
except (TypeError, ValueError):
|
|
480
|
+
item.update_handles()
|
|
481
|
+
else:
|
|
482
|
+
item.update_handles()
|
|
483
|
+
if "transform" in kwargs:
|
|
484
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
485
|
+
item.setData(0, "Block Arrow")
|
|
486
|
+
scene.addItem(item)
|
|
487
|
+
pending_block = None
|
|
488
|
+
|
|
489
|
+
elif line.startswith("_path = draw.Path("):
|
|
490
|
+
args, kwargs = _parse_call(line)
|
|
491
|
+
if pending_bracket is not None:
|
|
492
|
+
x = float(pending_bracket.get("x", 0.0))
|
|
493
|
+
y = float(pending_bracket.get("y", 0.0))
|
|
494
|
+
w = float(pending_bracket.get("w", DEFAULTS["Curvy Right Bracket"][0]))
|
|
495
|
+
h = float(pending_bracket.get("h", DEFAULTS["Curvy Right Bracket"][1]))
|
|
496
|
+
ratio = pending_bracket.get("hook_ratio")
|
|
497
|
+
hook_ratio = CurvyBracketItem.DEFAULT_HOOK_RATIO
|
|
498
|
+
if ratio is not None:
|
|
499
|
+
try:
|
|
500
|
+
hook_ratio = float(ratio)
|
|
501
|
+
except (TypeError, ValueError):
|
|
502
|
+
hook_ratio = CurvyBracketItem.DEFAULT_HOOK_RATIO
|
|
503
|
+
item = CurvyBracketItem(x, y, w, h, hook_ratio)
|
|
504
|
+
_apply_style(item, kwargs)
|
|
505
|
+
if "transform" in kwargs:
|
|
506
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
507
|
+
item.setData(0, "Curvy Right Bracket")
|
|
508
|
+
scene.addItem(item)
|
|
509
|
+
pending_bracket = None
|
|
510
|
+
continue
|
|
511
|
+
if args:
|
|
512
|
+
cmd = args[0]
|
|
513
|
+
parts = cmd.split()
|
|
514
|
+
if len(parts) >= 6 and parts[0] == "M":
|
|
515
|
+
coords: list[float] = []
|
|
516
|
+
i = 1
|
|
517
|
+
while i < len(parts):
|
|
518
|
+
coords.append(float(parts[i]))
|
|
519
|
+
coords.append(float(parts[i + 1]))
|
|
520
|
+
i += 2
|
|
521
|
+
if i < len(parts) and parts[i] == "L":
|
|
522
|
+
i += 1
|
|
523
|
+
else:
|
|
524
|
+
break
|
|
525
|
+
if len(coords) >= 4:
|
|
526
|
+
pts = [
|
|
527
|
+
QtCore.QPointF(coords[i], coords[i + 1])
|
|
528
|
+
for i in range(0, len(coords), 2)
|
|
529
|
+
]
|
|
530
|
+
arrow_start = "marker_start" in kwargs
|
|
531
|
+
arrow_end = "marker_end" in kwargs
|
|
532
|
+
arrow_start_attr = kwargs.get("data_arrow_start")
|
|
533
|
+
arrow_end_attr = kwargs.get("data_arrow_end")
|
|
534
|
+
if arrow_start_attr is not None:
|
|
535
|
+
arrow_start = (
|
|
536
|
+
arrow_start_attr
|
|
537
|
+
if isinstance(arrow_start_attr, bool)
|
|
538
|
+
else str(arrow_start_attr).strip().lower()
|
|
539
|
+
in {"true", "1", "yes"}
|
|
540
|
+
)
|
|
541
|
+
if arrow_end_attr is not None:
|
|
542
|
+
arrow_end = (
|
|
543
|
+
arrow_end_attr
|
|
544
|
+
if isinstance(arrow_end_attr, bool)
|
|
545
|
+
else str(arrow_end_attr).strip().lower()
|
|
546
|
+
in {"true", "1", "yes"}
|
|
547
|
+
)
|
|
548
|
+
arrow_head_length = kwargs.get("data_arrow_head_length")
|
|
549
|
+
arrow_head_width = kwargs.get("data_arrow_head_width")
|
|
550
|
+
try:
|
|
551
|
+
arrow_head_length_value = (
|
|
552
|
+
float(arrow_head_length)
|
|
553
|
+
if arrow_head_length is not None
|
|
554
|
+
else None
|
|
555
|
+
)
|
|
556
|
+
except (TypeError, ValueError):
|
|
557
|
+
arrow_head_length_value = None
|
|
558
|
+
try:
|
|
559
|
+
arrow_head_width_value = (
|
|
560
|
+
float(arrow_head_width)
|
|
561
|
+
if arrow_head_width is not None
|
|
562
|
+
else None
|
|
563
|
+
)
|
|
564
|
+
except (TypeError, ValueError):
|
|
565
|
+
arrow_head_width_value = None
|
|
566
|
+
angle = 0.0
|
|
567
|
+
if "transform" in kwargs:
|
|
568
|
+
angle = _parse_rotate(kwargs["transform"])
|
|
569
|
+
item = LineItem(
|
|
570
|
+
0.0,
|
|
571
|
+
0.0,
|
|
572
|
+
points=pts,
|
|
573
|
+
arrow_start=arrow_start,
|
|
574
|
+
arrow_end=arrow_end,
|
|
575
|
+
arrow_head_length=arrow_head_length_value,
|
|
576
|
+
arrow_head_width=arrow_head_width_value,
|
|
577
|
+
)
|
|
578
|
+
_apply_style(item, kwargs)
|
|
579
|
+
item.setRotation(angle)
|
|
580
|
+
item.setData(0, "Arrow" if arrow_start or arrow_end else "Line")
|
|
581
|
+
scene.addItem(item)
|
|
582
|
+
pending_line = item
|
|
583
|
+
else:
|
|
584
|
+
pending_line = None
|
|
585
|
+
|
|
586
|
+
elif line.startswith("_line = draw.Lines("):
|
|
587
|
+
args, kwargs = _parse_call(line)
|
|
588
|
+
coords = list(map(float, args))
|
|
589
|
+
pts = [
|
|
590
|
+
QtCore.QPointF(coords[i], coords[i + 1])
|
|
591
|
+
for i in range(0, len(coords), 2)
|
|
592
|
+
]
|
|
593
|
+
angle = 0.0
|
|
594
|
+
if "transform" in kwargs:
|
|
595
|
+
angle = _parse_rotate(kwargs["transform"])
|
|
596
|
+
item = LineItem(0.0, 0.0, points=pts)
|
|
597
|
+
_apply_style(item, kwargs)
|
|
598
|
+
item.setRotation(angle)
|
|
599
|
+
item.setData(0, "Line")
|
|
600
|
+
scene.addItem(item)
|
|
601
|
+
pending_line = item
|
|
602
|
+
|
|
603
|
+
elif line.startswith("_line = draw.Line("):
|
|
604
|
+
args, kwargs = _parse_call(line)
|
|
605
|
+
x1, y1, x2, y2 = map(float, args[:4])
|
|
606
|
+
dx, dy = x2 - x1, y2 - y1
|
|
607
|
+
length = math.hypot(dx, dy)
|
|
608
|
+
angle = math.degrees(math.atan2(dy, dx))
|
|
609
|
+
if "transform" in kwargs:
|
|
610
|
+
angle = _parse_rotate(kwargs["transform"])
|
|
611
|
+
cx = (x1 + x2) / 2.0
|
|
612
|
+
cy = (y1 + y2) / 2.0
|
|
613
|
+
item = LineItem(cx - length / 2.0, cy, length)
|
|
614
|
+
_apply_style(item, kwargs)
|
|
615
|
+
item.setRotation(angle)
|
|
616
|
+
item.setData(0, "Line")
|
|
617
|
+
scene.addItem(item)
|
|
618
|
+
pending_line = item
|
|
619
|
+
|
|
620
|
+
# --- NEU: _label = draw.Text(...) mehrzeilig zusammenführen ---
|
|
621
|
+
elif "_label = draw.Text(" in line:
|
|
622
|
+
args, kwargs = _parse_call(line)
|
|
623
|
+
text_arg = args[0]
|
|
624
|
+
|
|
625
|
+
flag_value = kwargs.get("data_shape_label")
|
|
626
|
+
if flag_value is None:
|
|
627
|
+
flag_value = kwargs.get("data_rect_label")
|
|
628
|
+
|
|
629
|
+
if str(flag_value).lower() == "true":
|
|
630
|
+
label_id = kwargs.get("data_label_id")
|
|
631
|
+
key = str(label_id) if label_id is not None else None
|
|
632
|
+
if key:
|
|
633
|
+
data = shape_label_pending.setdefault(
|
|
634
|
+
key,
|
|
635
|
+
{
|
|
636
|
+
"lines": [],
|
|
637
|
+
"h": None,
|
|
638
|
+
"v": None,
|
|
639
|
+
"font_px": None,
|
|
640
|
+
"color": None,
|
|
641
|
+
"color_override": None,
|
|
642
|
+
},
|
|
643
|
+
)
|
|
644
|
+
if "data_label_h" in kwargs:
|
|
645
|
+
data["h"] = kwargs.get("data_label_h")
|
|
646
|
+
if "data_label_v" in kwargs:
|
|
647
|
+
data["v"] = kwargs.get("data_label_v")
|
|
648
|
+
if "data_font_px" in kwargs:
|
|
649
|
+
data["font_px"] = kwargs.get("data_font_px")
|
|
650
|
+
if "fill" in kwargs and kwargs["fill"] is not None:
|
|
651
|
+
data["color"] = kwargs.get("fill")
|
|
652
|
+
if "data_label_color_override" in kwargs:
|
|
653
|
+
data["color_override"] = kwargs.get("data_label_color_override")
|
|
654
|
+
|
|
655
|
+
def _normalize_entry(value: object) -> str:
|
|
656
|
+
if isinstance(value, str):
|
|
657
|
+
if value in {"\u00A0", " "}:
|
|
658
|
+
return ""
|
|
659
|
+
return value
|
|
660
|
+
return str(value)
|
|
661
|
+
|
|
662
|
+
if isinstance(text_arg, (list, tuple)):
|
|
663
|
+
data["lines"] = [_normalize_entry(entry) for entry in text_arg]
|
|
664
|
+
else:
|
|
665
|
+
normalized = _normalize_entry(text_arg)
|
|
666
|
+
data.setdefault("lines", [])
|
|
667
|
+
data["lines"].append(normalized)
|
|
668
|
+
|
|
669
|
+
target = shape_label_targets.get(key)
|
|
670
|
+
if target is not None:
|
|
671
|
+
_apply_shape_label(target, data)
|
|
672
|
+
continue
|
|
673
|
+
|
|
674
|
+
elif line.startswith("_text = draw.Text("):
|
|
675
|
+
args, kwargs = _parse_call(line)
|
|
676
|
+
raw_text_arg = args[0]
|
|
677
|
+
if isinstance(raw_text_arg, (list, tuple)):
|
|
678
|
+
parts = []
|
|
679
|
+
for entry in raw_text_arg:
|
|
680
|
+
if isinstance(entry, str):
|
|
681
|
+
if entry in {"\u00A0", " "}:
|
|
682
|
+
parts.append("")
|
|
683
|
+
else:
|
|
684
|
+
parts.append(entry)
|
|
685
|
+
else:
|
|
686
|
+
parts.append(str(entry))
|
|
687
|
+
text = "\n".join(parts)
|
|
688
|
+
else:
|
|
689
|
+
text = str(raw_text_arg)
|
|
690
|
+
size = float(args[1])
|
|
691
|
+
text_x = float(args[2])
|
|
692
|
+
text_y = float(args[3])
|
|
693
|
+
|
|
694
|
+
item = TextItem(0, 0, 0, 0)
|
|
695
|
+
item.setPlainText(text)
|
|
696
|
+
|
|
697
|
+
data_font_px = kwargs.get("data_font_px")
|
|
698
|
+
data_scale = kwargs.get("data_scale")
|
|
699
|
+
data_doc_margin = kwargs.get("data_doc_margin")
|
|
700
|
+
data_box_w = kwargs.get("data_box_w")
|
|
701
|
+
data_box_h = kwargs.get("data_box_h")
|
|
702
|
+
data_text_h = kwargs.get("data_text_h")
|
|
703
|
+
data_text_v = kwargs.get("data_text_v")
|
|
704
|
+
data_text_dir = kwargs.get("data_text_dir")
|
|
705
|
+
|
|
706
|
+
# Export speichert Schriftgröße (Pixel) und Item-Skalierung separat
|
|
707
|
+
font = item.font()
|
|
708
|
+
base_px = None
|
|
709
|
+
if data_font_px is not None:
|
|
710
|
+
try:
|
|
711
|
+
base_px = float(data_font_px)
|
|
712
|
+
except (TypeError, ValueError):
|
|
713
|
+
base_px = None
|
|
714
|
+
if base_px is None or base_px <= 0.0:
|
|
715
|
+
base_px = float(font.pixelSize())
|
|
716
|
+
if base_px <= 0.0:
|
|
717
|
+
point_size = font.pointSizeF()
|
|
718
|
+
if point_size > 0.0:
|
|
719
|
+
screen = QtGui.QGuiApplication.primaryScreen()
|
|
720
|
+
dpi = screen.logicalDotsPerInch() if screen else 96.0
|
|
721
|
+
base_px = point_size * dpi / 72.0
|
|
722
|
+
if base_px <= 0.0:
|
|
723
|
+
fm = QtGui.QFontMetricsF(font)
|
|
724
|
+
base_px = fm.height()
|
|
725
|
+
if base_px <= 0.0:
|
|
726
|
+
base_px = size if size > 0.0 else 1.0
|
|
727
|
+
font.setPixelSize(max(1, int(round(base_px))))
|
|
728
|
+
item.setFont(font)
|
|
729
|
+
_apply_style(item, kwargs)
|
|
730
|
+
|
|
731
|
+
box_w = box_h = None
|
|
732
|
+
if data_box_w is not None:
|
|
733
|
+
try:
|
|
734
|
+
box_w = float(data_box_w)
|
|
735
|
+
except (TypeError, ValueError):
|
|
736
|
+
box_w = None
|
|
737
|
+
if data_box_h is not None:
|
|
738
|
+
try:
|
|
739
|
+
box_h = float(data_box_h)
|
|
740
|
+
except (TypeError, ValueError):
|
|
741
|
+
box_h = None
|
|
742
|
+
if box_w is not None or box_h is not None:
|
|
743
|
+
current = item.boundingRect()
|
|
744
|
+
width = box_w if box_w is not None else current.width()
|
|
745
|
+
height = box_h if box_h is not None else current.height()
|
|
746
|
+
item.set_size(width, height, adjust_origin=False)
|
|
747
|
+
|
|
748
|
+
if data_doc_margin is not None and item.document():
|
|
749
|
+
try:
|
|
750
|
+
base_doc_margin = float(data_doc_margin)
|
|
751
|
+
except (TypeError, ValueError):
|
|
752
|
+
base_doc_margin = item.document().documentMargin()
|
|
753
|
+
item.set_document_margin(base_doc_margin)
|
|
754
|
+
|
|
755
|
+
doc_margin = item.document().documentMargin() if item.document() else 0.0
|
|
756
|
+
|
|
757
|
+
if data_scale is not None:
|
|
758
|
+
try:
|
|
759
|
+
scale_factor = float(data_scale)
|
|
760
|
+
except (TypeError, ValueError):
|
|
761
|
+
scale_factor = size / base_px if base_px > 0.0 else 1.0
|
|
762
|
+
else:
|
|
763
|
+
scale_factor = size / base_px if base_px > 0.0 else 1.0
|
|
764
|
+
if not math.isfinite(scale_factor) or scale_factor <= 0.0:
|
|
765
|
+
scale_factor = 1.0
|
|
766
|
+
item.setScale(scale_factor)
|
|
767
|
+
|
|
768
|
+
if data_text_h is not None or data_text_v is not None:
|
|
769
|
+
try:
|
|
770
|
+
h_align = str(data_text_h) if data_text_h is not None else None
|
|
771
|
+
v_align = str(data_text_v) if data_text_v is not None else None
|
|
772
|
+
item.set_text_alignment(horizontal=h_align, vertical=v_align)
|
|
773
|
+
except Exception:
|
|
774
|
+
pass
|
|
775
|
+
if data_text_dir is not None:
|
|
776
|
+
try:
|
|
777
|
+
item.set_text_direction(str(data_text_dir))
|
|
778
|
+
except Exception:
|
|
779
|
+
pass
|
|
780
|
+
|
|
781
|
+
doc_margin_scene = doc_margin * scale_factor
|
|
782
|
+
x_pos = text_x - doc_margin_scene
|
|
783
|
+
y_pos = text_y - doc_margin_scene
|
|
784
|
+
item.setPos(x_pos, y_pos)
|
|
785
|
+
br = item.boundingRect()
|
|
786
|
+
item.setTransformOriginPoint(br.width() / 2.0, br.height() / 2.0)
|
|
787
|
+
if "transform" in kwargs:
|
|
788
|
+
item.setRotation(_parse_rotate(kwargs["transform"]))
|
|
789
|
+
item.setData(0, "Text")
|
|
790
|
+
scene.addItem(item)
|
|
791
|
+
|
|
792
|
+
# --- NEU: am Ende verbleibende pending Labels anwenden ---
|
|
793
|
+
for key, data in list(shape_label_pending.items()):
|
|
794
|
+
target = shape_label_targets.get(key)
|
|
795
|
+
if target is not None:
|
|
796
|
+
_apply_shape_label(target, data)
|
|
797
|
+
shape_label_pending.pop(key, None)
|
|
798
|
+
|
|
799
|
+
if view is not None:
|
|
800
|
+
ensure_pages = getattr(view, "ensure_pages_for_scene_items", None)
|
|
801
|
+
if callable(ensure_pages):
|
|
802
|
+
ensure_pages()
|
|
803
|
+
|
|
804
|
+
if parent is not None:
|
|
805
|
+
parent.statusBar().showMessage(f"Loaded: {path}", 5000)
|
|
806
|
+
except Exception as e:
|
|
807
|
+
QtWidgets.QMessageBox.critical(parent, "Error loading file", str(e))
|