batoolset-gui 0.2.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.
Files changed (74) hide show
  1. batoolset/GUI/__init__.py +0 -0
  2. batoolset/GUI/customdialog.py +136 -0
  3. batoolset/GUI/exception_handler.py +74 -0
  4. batoolset/GUI/list_gui.py +514 -0
  5. batoolset/GUI/multi_image_display.py +1038 -0
  6. batoolset/GUI/open.py +325 -0
  7. batoolset/GUI/overlay_hints.py +45 -0
  8. batoolset/GUI/paint2.py +1369 -0
  9. batoolset/GUI/scrollablepaint.py +1165 -0
  10. batoolset/dialogs/__init__.py +0 -0
  11. batoolset/dialogs/opensave.py +228 -0
  12. batoolset/drawings/__init__.py +0 -0
  13. batoolset/drawings/math_text.py +294 -0
  14. batoolset/drawings/pdf_svg_renderer.py +376 -0
  15. batoolset/drawings/shapes/Position.py +183 -0
  16. batoolset/drawings/shapes/__init__.py +0 -0
  17. batoolset/drawings/shapes/automated_alignment_drawing_refactored.py +346 -0
  18. batoolset/drawings/shapes/circle2d.py +99 -0
  19. batoolset/drawings/shapes/compute_optimal_size_all_merged.py +328 -0
  20. batoolset/drawings/shapes/ellipse2d.py +336 -0
  21. batoolset/drawings/shapes/freehand2d.py +235 -0
  22. batoolset/drawings/shapes/group.py +1985 -0
  23. batoolset/drawings/shapes/image2d.py +3157 -0
  24. batoolset/drawings/shapes/line2d.py +579 -0
  25. batoolset/drawings/shapes/point2d.py +257 -0
  26. batoolset/drawings/shapes/polygon2d.py +357 -0
  27. batoolset/drawings/shapes/polyline2d.py +235 -0
  28. batoolset/drawings/shapes/rect2d.py +388 -0
  29. batoolset/drawings/shapes/rectangle2d.py +601 -0
  30. batoolset/drawings/shapes/scalebar.py +310 -0
  31. batoolset/drawings/shapes/serializablefont.py +133 -0
  32. batoolset/drawings/shapes/serializableguiparams.py +14 -0
  33. batoolset/drawings/shapes/square2d.py +102 -0
  34. batoolset/drawings/shapes/test_publication_style.py +220 -0
  35. batoolset/drawings/shapes/textorientation.py +45 -0
  36. batoolset/drawings/shapes/tools.py +467 -0
  37. batoolset/drawings/shapes/transform2D.py +350 -0
  38. batoolset/drawings/shapes/txt2d.py +615 -0
  39. batoolset/drawings/svg_clip_renderer.py +247 -0
  40. batoolset/drawings/svg_embed.py +165 -0
  41. batoolset/drawings/svg_text_renderer.py +183 -0
  42. batoolset/drawings/widgets/__init__.py +0 -0
  43. batoolset/drawings/widgets/col.py +343 -0
  44. batoolset/drawings/widgets/image_group.py +248 -0
  45. batoolset/drawings/widgets/image_organizer.py +96 -0
  46. batoolset/drawings/widgets/paint.py +184 -0
  47. batoolset/drawings/widgets/vectorial.py +256 -0
  48. batoolset/figure/__init__.py +0 -0
  49. batoolset/figure/alignment.py +822 -0
  50. batoolset/figure/column.py +1117 -0
  51. batoolset/figure/fig_tools.py +285 -0
  52. batoolset/figure/minimal_debugger_scale_etc.py +139 -0
  53. batoolset/figure/panel.py +697 -0
  54. batoolset/figure/row.py +852 -0
  55. batoolset/figure/user_selection.py +287 -0
  56. batoolset/icons/__init__.py +0 -0
  57. batoolset/icons/tools.py +248 -0
  58. batoolset/luts/__init__.py +0 -0
  59. batoolset/luts/lut_minimal_test.py +1308 -0
  60. batoolset/luts/lutcreatorfromformula.py +209 -0
  61. batoolset/luts/luteditor.py +243 -0
  62. batoolset/pyqts/__init__.py +0 -0
  63. batoolset/pyqts/pleasewaitdialog.py +14 -0
  64. batoolset/pyqts/tools.py +760 -0
  65. batoolset/serializations/__init__.py +0 -0
  66. batoolset/serializations/tools.py +1338 -0
  67. batoolset/theme.py +647 -0
  68. batoolset/uis/__init__.py +0 -0
  69. batoolset/uis/blinker.py +56 -0
  70. batoolset_gui-0.2.0.dist-info/METADATA +54 -0
  71. batoolset_gui-0.2.0.dist-info/RECORD +74 -0
  72. batoolset_gui-0.2.0.dist-info/WHEEL +5 -0
  73. batoolset_gui-0.2.0.dist-info/licenses/LICENSE +24 -0
  74. batoolset_gui-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,376 @@
1
+ """Draws the SVG that PyMuPDF writes for a PDF page, clipping included.
2
+
3
+ A PDF placed as a figure panel is converted to SVG (PyMuPDF
4
+ ``page.get_svg_image``) and drawn as vectors. Qt 6.4's SVG renderer ignores
5
+ ``clipPath``, so a plot curve leaving its axes would be drawn over the whole
6
+ figure, and Qt 6.4's SVG *writer* drops painter clipping as well.
7
+
8
+ ``PdfSvgRenderer`` is a QSvgRenderer (Image2D and the exports only need
9
+ ``render``, ``defaultSize`` and the aspect-ratio mode) whose ``render`` walks
10
+ the small SVG subset PyMuPDF produces itself: groups, paths, glyph ``use``
11
+ references, images and clip paths, with matrix transforms. Clipping is done by
12
+ the painter for PDF and raster output, and geometrically (path intersection,
13
+ strokes turned into outlines) when the painter writes SVG. Anything outside
14
+ that subset (gradients, masks, patterns...) falls back to QSvgRenderer.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import base64
19
+ import re
20
+ import xml.etree.ElementTree as ET
21
+
22
+ from qtpy.QtCore import QByteArray, QPointF, QRectF, Qt
23
+ from qtpy.QtGui import (QBrush, QColor, QImage, QPaintEngine, QPainter, QPainterPath,
24
+ QPainterPathStroker, QPen, QTransform)
25
+ from qtpy.QtSvg import QSvgRenderer
26
+
27
+ _SVG = '{http://www.w3.org/2000/svg}'
28
+ _XLINK = '{http://www.w3.org/1999/xlink}href'
29
+ _SUPPORTED = {'svg', 'defs', 'g', 'path', 'use', 'clipPath', 'image', 'title', 'desc', 'metadata'}
30
+ _INHERITED = ('fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-opacity',
31
+ 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-dasharray')
32
+ _NUM = re.compile(r'[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?')
33
+ _CMD = re.compile(r'[MmLlHhVvCcSsQqTtAaZz]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?')
34
+
35
+
36
+ def _tag(el):
37
+ return el.tag.split('}', 1)[-1]
38
+
39
+
40
+ def parse_transform(text):
41
+ """SVG transform list -> QTransform (matrix, translate, scale, rotate)."""
42
+ t = QTransform()
43
+ if not text:
44
+ return t
45
+ for name, args in re.findall(r'(\w+)\s*\(([^)]*)\)', text):
46
+ v = [float(x) for x in _NUM.findall(args)]
47
+ if name == 'matrix' and len(v) == 6:
48
+ m = QTransform(v[0], v[1], v[2], v[3], v[4], v[5])
49
+ elif name == 'translate' and v:
50
+ m = QTransform.fromTranslate(v[0], v[1] if len(v) > 1 else 0.0)
51
+ elif name == 'scale' and v:
52
+ m = QTransform.fromScale(v[0], v[1] if len(v) > 1 else v[0])
53
+ elif name == 'rotate' and v:
54
+ m = QTransform()
55
+ if len(v) == 3:
56
+ m.translate(v[1], v[2])
57
+ m.rotate(v[0])
58
+ if len(v) == 3:
59
+ m.translate(-v[1], -v[2])
60
+ else:
61
+ continue
62
+ # in SVG the leftmost transform is applied last
63
+ t = m * t
64
+ return t
65
+
66
+
67
+ def parse_path(d):
68
+ """SVG path data -> QPainterPath (arcs are drawn as straight segments)."""
69
+ path = QPainterPath()
70
+ tokens = _CMD.findall(d or '')
71
+ i = 0
72
+ cmd = None
73
+ cur = QPointF(0, 0)
74
+ start = QPointF(0, 0)
75
+ last_ctrl = None
76
+ last_cmd = ''
77
+
78
+ def num():
79
+ nonlocal i
80
+ v = float(tokens[i])
81
+ i += 1
82
+ return v
83
+
84
+ while i < len(tokens):
85
+ if tokens[i].isalpha():
86
+ cmd = tokens[i]
87
+ i += 1
88
+ if cmd in 'Zz':
89
+ path.closeSubpath()
90
+ cur = QPointF(start)
91
+ last_cmd = cmd
92
+ continue
93
+ if cmd is None:
94
+ break
95
+ rel = cmd.islower()
96
+ c = cmd.upper()
97
+ base = cur if rel else QPointF(0, 0)
98
+ try:
99
+ if c == 'M':
100
+ cur = QPointF(base.x() + num(), base.y() + num())
101
+ path.moveTo(cur)
102
+ start = QPointF(cur)
103
+ cmd = 'l' if rel else 'L' # further pairs are line-tos
104
+ elif c == 'L':
105
+ cur = QPointF(base.x() + num(), base.y() + num())
106
+ path.lineTo(cur)
107
+ elif c == 'H':
108
+ cur = QPointF((cur.x() if rel else 0.0) + num(), cur.y())
109
+ path.lineTo(cur)
110
+ elif c == 'V':
111
+ cur = QPointF(cur.x(), (cur.y() if rel else 0.0) + num())
112
+ path.lineTo(cur)
113
+ elif c == 'C':
114
+ c1 = QPointF(base.x() + num(), base.y() + num())
115
+ c2 = QPointF(base.x() + num(), base.y() + num())
116
+ cur = QPointF(base.x() + num(), base.y() + num())
117
+ path.cubicTo(c1, c2, cur)
118
+ last_ctrl = c2
119
+ elif c == 'S':
120
+ c1 = (cur * 2 - last_ctrl) if last_ctrl is not None and last_cmd in 'CcSs' else QPointF(cur)
121
+ c2 = QPointF(base.x() + num(), base.y() + num())
122
+ cur = QPointF(base.x() + num(), base.y() + num())
123
+ path.cubicTo(c1, c2, cur)
124
+ last_ctrl = c2
125
+ elif c == 'Q':
126
+ c1 = QPointF(base.x() + num(), base.y() + num())
127
+ cur = QPointF(base.x() + num(), base.y() + num())
128
+ path.quadTo(c1, cur)
129
+ last_ctrl = c1
130
+ elif c == 'T':
131
+ c1 = (cur * 2 - last_ctrl) if last_ctrl is not None and last_cmd in 'QqTt' else QPointF(cur)
132
+ cur = QPointF(base.x() + num(), base.y() + num())
133
+ path.quadTo(c1, cur)
134
+ last_ctrl = c1
135
+ elif c == 'A':
136
+ for _ in range(5):
137
+ num()
138
+ cur = QPointF(base.x() + num(), base.y() + num())
139
+ path.lineTo(cur)
140
+ else:
141
+ break
142
+ except (IndexError, ValueError):
143
+ break
144
+ last_cmd = cmd
145
+ return path
146
+
147
+
148
+ def _color(value, opacity):
149
+ if value is None or value == 'none':
150
+ return None
151
+ col = QColor(value)
152
+ if not col.isValid():
153
+ return None
154
+ col.setAlphaF(max(0.0, min(1.0, opacity)))
155
+ return col
156
+
157
+
158
+ def _float(value, default):
159
+ try:
160
+ return float(value)
161
+ except (TypeError, ValueError):
162
+ return default
163
+
164
+
165
+ class PdfSvgRenderer(QSvgRenderer):
166
+ """QSvgRenderer for PyMuPDF's SVG output, with working clip paths."""
167
+
168
+ def __init__(self, svg_bytes):
169
+ super().__init__(QByteArray(svg_bytes))
170
+ root = ET.fromstring(svg_bytes)
171
+ self._root = root
172
+ self._ids = {el.get('id'): el for el in root.iter() if el.get('id')}
173
+ vb = [float(x) for x in _NUM.findall(root.get('viewBox') or '')]
174
+ if len(vb) == 4 and vb[2] > 0 and vb[3] > 0:
175
+ self._view_box = QRectF(*vb)
176
+ else:
177
+ self._view_box = QRectF(0, 0, _float(root.get('width'), 100.0), _float(root.get('height'), 100.0))
178
+ self._path_cache = {}
179
+ self._image_cache = {}
180
+
181
+ @staticmethod
182
+ def supports(svg_bytes):
183
+ """True if every element of the SVG is in the subset drawn here."""
184
+ try:
185
+ root = ET.fromstring(svg_bytes)
186
+ except ET.ParseError:
187
+ return False
188
+ return all(_tag(el) in _SUPPORTED for el in root.iter())
189
+
190
+ # -- QSvgRenderer API used by PyFigures ---------------------------------
191
+ def render(self, painter, *args):
192
+ if len(args) == 1 and isinstance(args[0], str): # render(painter, elementId)
193
+ return super().render(painter, *args)
194
+ if len(args) >= 2: # render(painter, elementId, bounds)
195
+ return super().render(painter, *args)
196
+ bounds = QRectF(args[0]) if args else QRectF(QPointF(0, 0), QSizeF_from(self.defaultSize()))
197
+ vb = self._view_box
198
+ sx = bounds.width() / vb.width()
199
+ sy = bounds.height() / vb.height()
200
+ dx, dy = bounds.left(), bounds.top()
201
+ if self.aspectRatioMode() == Qt.AspectRatioMode.KeepAspectRatio:
202
+ s = min(sx, sy)
203
+ dx += (bounds.width() - vb.width() * s) / 2.0
204
+ dy += (bounds.height() - vb.height() * s) / 2.0
205
+ sx = sy = s
206
+ to_bounds = QTransform(sx, 0, 0, sy, dx - vb.left() * sx, dy - vb.top() * sy)
207
+ engine = painter.paintEngine()
208
+ geometric = engine is not None and engine.type() == QPaintEngine.Type.SVG
209
+ painter.save()
210
+ try:
211
+ base = painter.worldTransform()
212
+ self._geometric = geometric
213
+ self._base = base
214
+ for child in self._root:
215
+ self._draw(painter, child, to_bounds, {}, None, 1.0)
216
+ finally:
217
+ painter.restore()
218
+
219
+ # -- drawing ---------------------------------------------------------------
220
+ def _clip_path(self, ref, user_to_base):
221
+ m = re.match(r'url\(#([^)]+)\)', ref or '')
222
+ el = self._ids.get(m.group(1)) if m else None
223
+ if el is None:
224
+ return None
225
+ out = QPainterPath()
226
+ out.setFillRule(Qt.FillRule.WindingFill)
227
+ for child in el:
228
+ if _tag(child) != 'path':
229
+ continue
230
+ p = parse_path(child.get('d'))
231
+ if child.get('clip-rule') == 'evenodd' or child.get('fill-rule') == 'evenodd':
232
+ p.setFillRule(Qt.FillRule.OddEvenFill)
233
+ t = parse_transform(child.get('transform')) * user_to_base
234
+ out = out.united(t.map(p)) if not out.isEmpty() else t.map(p)
235
+ nested = el.get('clip-path')
236
+ if nested:
237
+ inner = self._clip_path(nested, user_to_base)
238
+ if inner is not None:
239
+ out = out.intersected(inner)
240
+ return out
241
+
242
+ def _draw(self, painter, el, user_to_base, style, clip, opacity):
243
+ tag = _tag(el)
244
+ if tag in ('defs', 'clipPath', 'title', 'desc', 'metadata'):
245
+ return
246
+ style = dict(style)
247
+ for key in _INHERITED:
248
+ if el.get(key) is not None:
249
+ style[key] = el.get(key)
250
+ opacity *= _float(el.get('opacity'), 1.0)
251
+ t = parse_transform(el.get('transform')) * user_to_base
252
+ if el.get('clip-path'):
253
+ new_clip = self._clip_path(el.get('clip-path'), t)
254
+ if new_clip is not None:
255
+ clip = new_clip if clip is None else clip.intersected(new_clip)
256
+ if tag in ('g', 'svg'):
257
+ for child in el:
258
+ self._draw(painter, child, t, style, clip, opacity)
259
+ elif tag == 'use':
260
+ ref = (el.get(_XLINK) or el.get('href') or '').lstrip('#')
261
+ target = self._ids.get(ref)
262
+ if target is not None:
263
+ shift = QTransform.fromTranslate(_float(el.get('x'), 0.0), _float(el.get('y'), 0.0))
264
+ self._draw(painter, target, shift * t, style, clip, opacity)
265
+ elif tag == 'path':
266
+ d = el.get('d')
267
+ path = self._path_cache.get(d)
268
+ if path is None:
269
+ path = parse_path(d)
270
+ self._path_cache[d] = path
271
+ if style.get('fill-rule') == 'evenodd':
272
+ path = QPainterPath(path)
273
+ path.setFillRule(Qt.FillRule.OddEvenFill)
274
+ self._paint_path(painter, path, t, style, clip, opacity)
275
+ elif tag == 'image':
276
+ self._paint_image(painter, el, t, clip, opacity)
277
+
278
+ def _pen(self, style, opacity):
279
+ col = _color(style.get('stroke'), _float(style.get('stroke-opacity'), 1.0) * opacity)
280
+ if col is None:
281
+ return None
282
+ pen = QPen(col)
283
+ pen.setWidthF(_float(style.get('stroke-width'), 1.0))
284
+ pen.setCapStyle({'round': Qt.PenCapStyle.RoundCap, 'square': Qt.PenCapStyle.SquareCap}
285
+ .get(style.get('stroke-linecap'), Qt.PenCapStyle.FlatCap))
286
+ pen.setJoinStyle({'round': Qt.PenJoinStyle.RoundJoin, 'bevel': Qt.PenJoinStyle.BevelJoin}
287
+ .get(style.get('stroke-linejoin'), Qt.PenJoinStyle.SvgMiterJoin))
288
+ pen.setMiterLimit(_float(style.get('stroke-miterlimit'), 4.0))
289
+ dashes = [x for x in (_float(v, None) for v in _NUM.findall(style.get('stroke-dasharray') or ''))
290
+ if x is not None]
291
+ if dashes and any(dashes) and pen.widthF() > 0:
292
+ if len(dashes) % 2:
293
+ dashes = dashes * 2
294
+ pen.setDashPattern([max(1e-3, x / pen.widthF()) for x in dashes])
295
+ return pen
296
+
297
+ def _paint_path(self, painter, path, t, style, clip, opacity):
298
+ fill = _color(style.get('fill', '#000000'), _float(style.get('fill-opacity'), 1.0) * opacity)
299
+ pen = self._pen(style, opacity)
300
+ if fill is None and pen is None:
301
+ return
302
+ if not self._geometric:
303
+ painter.save()
304
+ painter.setWorldTransform(self._base)
305
+ if clip is not None:
306
+ painter.setClipPath(clip, Qt.ClipOperation.IntersectClip)
307
+ painter.setWorldTransform(t * self._base)
308
+ painter.setBrush(QBrush(fill) if fill is not None else Qt.BrushStyle.NoBrush)
309
+ painter.setPen(pen if pen is not None else Qt.PenStyle.NoPen)
310
+ painter.drawPath(path)
311
+ painter.restore()
312
+ return
313
+ # SVG output: Qt 6.4's SVG writer drops clipping, so clip the geometry
314
+ painter.save()
315
+ painter.setWorldTransform(self._base)
316
+ painter.setPen(Qt.PenStyle.NoPen)
317
+ if fill is not None:
318
+ area = t.map(path)
319
+ if clip is not None and not clip.contains(area.boundingRect()):
320
+ area = area.intersected(clip)
321
+ painter.setBrush(QBrush(fill))
322
+ painter.drawPath(area)
323
+ if pen is not None:
324
+ if clip is None or clip.contains(t.map(path).boundingRect().adjusted(
325
+ -pen.widthF(), -pen.widthF(), pen.widthF(), pen.widthF())):
326
+ painter.setWorldTransform(t * self._base)
327
+ painter.setBrush(Qt.BrushStyle.NoBrush)
328
+ painter.setPen(pen)
329
+ painter.drawPath(path)
330
+ else:
331
+ stroker = QPainterPathStroker(pen)
332
+ outline = t.map(stroker.createStroke(path)).intersected(clip)
333
+ painter.setBrush(QBrush(pen.color()))
334
+ painter.drawPath(outline)
335
+ painter.restore()
336
+
337
+ def _paint_image(self, painter, el, t, clip, opacity):
338
+ href = el.get(_XLINK) or el.get('href') or ''
339
+ img = self._image_cache.get(href)
340
+ if img is None:
341
+ m = re.match(r'data:image/[^;]+;base64,(.*)', href, re.S)
342
+ if not m:
343
+ return
344
+ img = QImage.fromData(base64.b64decode(re.sub(r'\s', '', m.group(1))))
345
+ self._image_cache[href] = img
346
+ if img.isNull():
347
+ return
348
+ rect = QRectF(_float(el.get('x'), 0.0), _float(el.get('y'), 0.0),
349
+ _float(el.get('width'), img.width()), _float(el.get('height'), img.height()))
350
+ painter.save()
351
+ painter.setWorldTransform(self._base)
352
+ if clip is not None:
353
+ if self._geometric:
354
+ # an SVG writer cannot clip an image: keep the clip's bounding box
355
+ visible = t.mapRect(rect).intersected(clip.boundingRect())
356
+ if visible.isEmpty():
357
+ painter.restore()
358
+ return
359
+ else:
360
+ painter.setClipPath(clip, Qt.ClipOperation.IntersectClip)
361
+ painter.setWorldTransform(t * self._base)
362
+ painter.setOpacity(painter.opacity() * opacity)
363
+ painter.drawImage(rect, img, QRectF(img.rect()))
364
+ painter.restore()
365
+
366
+
367
+ def QSizeF_from(size):
368
+ from qtpy.QtCore import QSizeF
369
+ return QSizeF(size.width(), size.height())
370
+
371
+
372
+ def make_pdf_svg_renderer(svg_bytes):
373
+ """PdfSvgRenderer if the SVG is in the subset it draws, else QSvgRenderer."""
374
+ if PdfSvgRenderer.supports(svg_bytes):
375
+ return PdfSvgRenderer(svg_bytes)
376
+ return QSvgRenderer(QByteArray(svg_bytes))
@@ -0,0 +1,183 @@
1
+ # TODO make these things combined with pack and align commands and make loops
2
+ # this way I can make any component at any specific position !!!
3
+ # this is quite good I think
4
+ import copy
5
+ import traceback
6
+ from itertools import groupby
7
+
8
+
9
+ from batoolset.figure.alignment import packing_modes
10
+
11
+
12
+ # Define a sorting key function to sort objects based on their positions
13
+ def sorting_key(obj):
14
+ try:
15
+ return tuple(sorted(obj.items()))
16
+ except Exception:
17
+ try:
18
+ return tuple(sorted(obj.placement.get_position()))
19
+ except Exception:
20
+ # pass
21
+ return ('free','relative') # if the object does not have a position assume it obeys free/relative positioning
22
+
23
+ def groupby_position(objects):
24
+ from batoolset.drawings.shapes.txt2d import TAText2D
25
+ if isinstance(objects, TAText2D):
26
+ objects = [objects]
27
+ # Sort the objects based on their positions
28
+ sorted_objects = sorted(objects, key=sorting_key)
29
+ # Group the sorted objects based on their positions
30
+ grouped_objects = {key: list(group) for key, group in groupby(sorted_objects, key=sorting_key)}
31
+ return grouped_objects
32
+
33
+
34
+ class Position:
35
+ positions = {
36
+ 'top': False,
37
+ 'bottom': False,
38
+ 'left': False,
39
+ 'right': False,
40
+ 'center_h': False,
41
+ 'center_v': False
42
+ }
43
+
44
+ def __init__(self, position=None):
45
+ self.positions = copy.deepcopy(Position.positions)
46
+ if position is not None:
47
+ self.set_position(position)
48
+
49
+ def set_position(self, position):
50
+ for key in self.positions.keys(): # reset all positions
51
+ self.positions[key] = False
52
+
53
+ if not position:
54
+ return
55
+
56
+ position = position.lower()
57
+ # except:
58
+ # traceback.print_exc()
59
+ # print('position', position)
60
+
61
+ if 'top' in position:
62
+ self.positions['top'] = True
63
+ if 'bottom' in position:
64
+ self.positions['bottom'] = True
65
+ if 'left' in position:
66
+ self.positions['left'] = True
67
+ if 'right' in position:
68
+ self.positions['right'] = True
69
+ if 'center_h' in position:
70
+ self.positions['center_h'] = True
71
+ if 'center_v' in position:
72
+ self.positions['center_v'] = True
73
+
74
+ def get_position(self):
75
+ return {key: value for key, value in self.positions.items() if value}
76
+
77
+ def check_position(self, position):
78
+ if not position in self.positions:
79
+ return False
80
+ return self.positions[position]
81
+
82
+ def position_to_string(self):
83
+ out = ''
84
+ if self.positions['top']:
85
+ out += 'top '
86
+ if self.positions['bottom']:
87
+ out += 'bottom '
88
+ if self.positions['left']:
89
+ out += 'left '
90
+ if self.positions['right']:
91
+ out += 'right '
92
+ if self.positions['center_h']:
93
+ out += 'center_h '
94
+ if self.positions['center_v']:
95
+ out += 'center_v '
96
+
97
+ return out.strip()
98
+
99
+ def get_packing_orientation(self):
100
+ # try rescue packing mode from position
101
+ # if reference.position.get_position():
102
+ if self.check_position('top'):
103
+ return packing_modes[1]
104
+ elif self.check_position('bottom'):
105
+ return packing_modes[3]
106
+ # elif mode.check_position('left'):
107
+ # mode = packing_modes[0]
108
+ # elif mode.check_position('right'):
109
+ # mode = packing_modes[2]
110
+ # else:
111
+ # # logger.error('unknown positioning --> ignoring')
112
+ # print('unknown position ignoring')
113
+
114
+ def isEmpty(self):
115
+ for key, value in self.positions.items():
116
+ if value:
117
+ return False
118
+ return True
119
+
120
+ def __str__(self):
121
+ return self.position_to_string()
122
+
123
+ # very good --> maybe just slightly improve it and fix it --> it will be placed with repsect to the parent stuff
124
+ if __name__ == '__main__':
125
+ # Create a new Position object
126
+ p = Position()
127
+ print(p.get_position()) # --> empty
128
+
129
+ # Set the position
130
+ p.set_position('top left')
131
+ # Get the current position
132
+ print(p.get_position()) # Output: {'top': True, 'left': True}
133
+
134
+ p.set_position('TOp Left')
135
+ # Get the current position
136
+ print(p.get_position()) # Output: {'top': True, 'left': True}
137
+
138
+ p.set_position('Bottom Right')
139
+ # Get the current position
140
+ print(p.get_position()) # Output: {'top': True, 'left': True}
141
+
142
+
143
+ print('pos to string', p.position_to_string())
144
+
145
+ print(p.check_position('top'))
146
+ print(p.check_position('right'))
147
+ print(p.check_position('bottom'))
148
+
149
+ p2 = Position()
150
+
151
+
152
+
153
+ print(p2.get_position())
154
+
155
+ print(p2.isEmpty())
156
+ print(p.isEmpty())
157
+
158
+
159
+
160
+ if False:
161
+
162
+ # List of objects with positions
163
+ objects = [
164
+ {'top': True, 'left': True},
165
+ {'bottom': True, 'right': True},
166
+ {'top': True, 'left': True},
167
+ {'bottom': True, 'left': True}
168
+ ]
169
+
170
+ grouped_objects = groupby_position(objects)
171
+
172
+
173
+
174
+
175
+ print(grouped_objects)
176
+
177
+ # Print the grouped objects
178
+ for position, objects_in_group in grouped_objects.items():
179
+ print(f"Position: {position}")
180
+ for obj in objects_in_group:
181
+ print(obj)
182
+ print()
183
+
File without changes