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.
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", "&#160;"}:
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", "&#160;"}:
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))