-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnode_graphics_view.py
More file actions
681 lines (541 loc) · 28.3 KB
/
Copy pathnode_graphics_view.py
File metadata and controls
681 lines (541 loc) · 28.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# -*- coding: utf-8 -*-
"""
A module containing `Graphics View` for NodeEditor
"""
from qtpy.QtWidgets import QGraphicsView, QApplication
from qtpy.QtCore import Signal, QPoint, Qt, QEvent, QPointF, QRectF
from qtpy.QtGui import QPainter, QDragEnterEvent, QDropEvent, QMouseEvent, QKeyEvent, QWheelEvent
from nodeeditor import _QT_API_NAME as QT_API
from nodeeditor.node_graphics_socket import QDMGraphicsSocket
from nodeeditor.node_graphics_edge import QDMGraphicsEdge
from nodeeditor.node_edge_dragging import EdgeDragging
from nodeeditor.node_edge_rerouting import EdgeRerouting
from nodeeditor.node_edge_intersect import EdgeIntersect
from nodeeditor.node_edge_snapping import EdgeSnapping
from nodeeditor.node_graphics_cutline import QDMCutLine
from nodeeditor.utils import dumpException, pp, isCTRLPressed, isSHIFTPressed, isALTPressed
MODE_NOOP = 1 #: Mode representing ready state
MODE_EDGE_DRAG = 2 #: Mode representing when we drag edge state
MODE_EDGE_CUT = 3 #: Mode representing when we draw a cutting edge
MODE_EDGES_REROUTING = 4 #: Mode representing when we re-route existing edges
MODE_NODE_DRAG = 5 #: Mode representing when we drag a node to calculate dropping on intersecting edge
STATE_STRING = ['', 'Noop', 'Edge Drag', 'Edge Cut', 'Edge Rerouting', 'Node Drag']
#: Distance when click on socket to enable `Drag Edge`
EDGE_DRAG_START_THRESHOLD = 50
#: Enable UnrealEngine style rerouting
EDGE_REROUTING_UE = True
#: Socket snapping distance
EDGE_SNAPPING_RADIUS = 24
#: Enable socket snapping feature
EDGE_SNAPPING = True
DEBUG = False
DEBUG_MMB_SCENE_ITEMS = False
DEBUG_MMB_LAST_SELECTIONS = False
DEBUG_EDGE_INTERSECT = False
DEBUG_STATE = False
class QDMGraphicsView(QGraphicsView):
"""Class representing NodeEditor's `Graphics View`"""
#: pyqtSignal emitted when cursor position on the `Scene` has changed
scenePosChanged = Signal(int, int)
def __init__(self, grScene: 'QDMGraphicsScene', parent: 'QWidget'=None):
"""
:param grScene: reference to the :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
:type grScene: :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
:param parent: parent widget
:type parent: ``QWidget``
:Instance Attributes:
- **grScene** - reference to the :class:`~nodeeditor.node_graphics_scene.QDMGraphicsScene`
- **mode** - state of the `Graphics View`
- **zoomInFactor**- ``float`` - zoom step scaling, default 1.25
- **zoomClamp** - ``bool`` - do we clamp zooming or is it infinite?
- **zoom** - current zoom step
- **zoomStep** - ``int`` - the relative zoom step when zooming in/out
- **zoomRange** - ``[min, max]``
"""
super().__init__(parent)
self.grScene = grScene
self.initUI()
self.setScene(self.grScene)
self.mode = MODE_NOOP
self.editingFlag = False
self.rubberBandDraggingRectangle = False
# edge dragging
self.dragging = EdgeDragging(self)
# edges re-routing
self.rerouting = EdgeRerouting(self)
# drop a node on an existing edge
self.edgeIntersect = EdgeIntersect(self)
# edge snapping
self.snapping = EdgeSnapping(self, snapping_radius=EDGE_SNAPPING_RADIUS)
# cutline
self.cutline = QDMCutLine()
self.grScene.addItem(self.cutline)
self.last_scene_mouse_position = QPoint(0,0)
self.zoomInFactor = 1.25
self.zoomClamp = True
self.zoom = 10
self.zoomStep = 1
self.zoomRange = [0, 10]
# listeners
self._drag_enter_listeners = []
self._drop_listeners = []
def initUI(self):
"""Set up this ``QGraphicsView``"""
# Start with basic render hints for performance
self.setRenderHints(QPainter.TextAntialiasing)
self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setDragMode(QGraphicsView.RubberBandDrag)
# enable dropping
self.setAcceptDrops(True)
# Store render hint levels for dynamic switching
self._render_hints_low = QPainter.TextAntialiasing
self._render_hints_high = QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform
def isSnappingEnabled(self, event: 'QInputEvent' = None) -> bool:
"""Returns ``True`` if snapping is currently enabled"""
return EDGE_SNAPPING and isCTRLPressed(event) if event else True
def resetMode(self):
"""Helper function to re-set the grView's State Machine state to the default"""
self.mode = MODE_NOOP
def dragEnterEvent(self, event: QDragEnterEvent):
"""Trigger our registered `Drag Enter` events"""
for callback in self._drag_enter_listeners: callback(event)
def dropEvent(self, event: QDropEvent):
"""Trigger our registered `Drop` events"""
for callback in self._drop_listeners: callback(event)
def addDragEnterListener(self, callback: 'function'):
"""
Register callback for `Drag Enter` event
:param callback: callback function
"""
self._drag_enter_listeners.append(callback)
def addDropListener(self, callback: 'function'):
"""
Register callback for `Drop` event
:param callback: callback function
"""
self._drop_listeners.append(callback)
def mousePressEvent(self, event: QMouseEvent):
"""Dispatch Qt's mousePress event to corresponding function below"""
if event.button() == Qt.MiddleButton:
self.middleMouseButtonPress(event)
elif event.button() == Qt.LeftButton:
self.leftMouseButtonPress(event)
elif event.button() == Qt.RightButton:
self.rightMouseButtonPress(event)
else:
super().mousePressEvent(event)
def mouseReleaseEvent(self, event: QMouseEvent):
"""Dispatch Qt's mouseRelease event to corresponding function below"""
if event.button() == Qt.MiddleButton:
self.middleMouseButtonRelease(event)
elif event.button() == Qt.LeftButton:
self.leftMouseButtonRelease(event)
elif event.button() == Qt.RightButton:
self.rightMouseButtonRelease(event)
else:
super().mouseReleaseEvent(event)
def middleMouseButtonPress(self, event: QMouseEvent):
"""When Middle mouse button was pressed"""
item = self.getItemAtClick(event)
# debug printout
if DEBUG_MMB_SCENE_ITEMS:
if isinstance(item, QDMGraphicsEdge):
print("MMB DEBUG:", item.edge, "\n\t", item.edge.grEdge if item.edge.grEdge is not None else None)
return
if isinstance(item, QDMGraphicsSocket):
print("MMB DEBUG:", item.socket, "socket_type:", item.socket.socket_type,
"has edges:", "no" if item.socket.edges == [] else "")
if item.socket.edges:
for edge in item.socket.edges: print("\t", edge)
return
if DEBUG_MMB_SCENE_ITEMS and (item is None or self.mode == MODE_EDGES_REROUTING):
print("SCENE:")
print(" Nodes:")
for node in self.grScene.scene.nodes: print("\t", node)
print(" Edges:")
for edge in self.grScene.scene.edges: print("\t", edge, "\n\t\tgrEdge:", edge.grEdge if edge.grEdge is not None else None)
if isCTRLPressed(event):
print(" Graphic Items in GraphicScene:")
for item in self.grScene.items():
print(' ', item)
if DEBUG_MMB_LAST_SELECTIONS and isSHIFTPressed(event):
print("scene _last_selected_items:", self.grScene.scene._last_selected_items)
return
# faking events for enable MMB dragging the scene
if QT_API in ("pyqt5", "pyside2"):
releaseEvent = QMouseEvent(QEvent.MouseButtonRelease, event.localPos(), event.screenPos(),
Qt.LeftButton, Qt.NoButton, event.modifiers())
elif QT_API in ("pyqt6", "pyside6"):
releaseEvent = QMouseEvent(QEvent.MouseButtonRelease, event.localPos(),
Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, event.modifiers())
super().mouseReleaseEvent(releaseEvent)
self.setDragMode(QGraphicsView.ScrollHandDrag)
if QT_API in ("pyqt5", "pyside2"):
fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
Qt.LeftButton, event.buttons() | Qt.LeftButton, event.modifiers())
elif QT_API in ("pyqt6", "pyside6"):
fakeEvent = QMouseEvent(event.type(), event.localPos(),
Qt.MouseButton.LeftButton, event.buttons() | Qt.MouseButton.LeftButton, event.modifiers())
super().mousePressEvent(fakeEvent)
def middleMouseButtonRelease(self, event: QMouseEvent):
"""When Middle mouse button was released"""
if QT_API in ("pyqt5", "pyside2"):
fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
Qt.LeftButton, event.buttons() & ~Qt.LeftButton, event.modifiers())
elif QT_API in ("pyqt6", "pyside6"):
fakeEvent = QMouseEvent(event.type(), event.localPos(),
Qt.MouseButton.LeftButton, event.buttons() & ~Qt.MouseButton.LeftButton, event.modifiers())
super().mouseReleaseEvent(fakeEvent)
self.setDragMode(QGraphicsView.RubberBandDrag)
def leftMouseButtonPress(self, event: QMouseEvent):
"""When Left mouse button was pressed"""
# get the item we clicked on
item = self.getItemAtClick(event)
# we store the position of last LMB click
self.last_lmb_click_scene_pos = self.mapToScene(event.pos())
# if DEBUG: print("LMB Click on", item, self.debug_modifiers(event))
# logic - Shift + LMB Node
if hasattr(item, "node") or isinstance(item, QDMGraphicsEdge) or item is None:
if isSHIFTPressed(event):
event.ignore()
if QT_API in ("pyqt5", "pyside2"):
fakeEvent = QMouseEvent(QEvent.MouseButtonPress, event.localPos(), event.screenPos(),
Qt.LeftButton, event.buttons() | Qt.LeftButton,
event.modifiers() | Qt.ControlModifier)
elif QT_API in ("pyqt6", "pyside6"):
fakeEvent = QMouseEvent(QEvent.MouseButtonPress, event.localPos(),
Qt.MouseButton.LeftButton, event.buttons() | Qt.MouseButton.LeftButton,
event.modifiers() | Qt.KeyboardModifier.ControlModifier)
super().mousePressEvent(fakeEvent)
return
if hasattr(item, "node"):
if DEBUG_EDGE_INTERSECT: print('View::leftMouseButtonPress - Start dragging a node')
if self.mode == MODE_NOOP:
self.mode = MODE_NODE_DRAG
self.edgeIntersect.enterState(item.node)
if DEBUG_EDGE_INTERSECT: print(">> edgeIntersect start:", self.edgeIntersect.draggedNode)
# support for snapping
if self.isSnappingEnabled(event):
item = self.snapping.getSnappedSocketItem(event)
if isinstance(item, QDMGraphicsSocket):
if self.mode == MODE_NOOP and isCTRLPressed(event):
socket = item.socket
if socket.hasAnyEdge():
self.mode = MODE_EDGES_REROUTING
self.rerouting.startRerouting(socket)
return
if self.mode == MODE_NOOP:
self.mode = MODE_EDGE_DRAG
self.dragging.edgeDragStart(item)
return
if self.mode == MODE_EDGE_DRAG:
res = self.dragging.edgeDragEnd(item)
if res: return
if item is None:
if isCTRLPressed(event):
self.mode = MODE_EDGE_CUT
if QT_API in ("pyqt5", "pyside2"):
fakeEvent = QMouseEvent(QEvent.MouseButtonRelease, event.localPos(), event.screenPos(),
Qt.LeftButton, Qt.NoButton, event.modifiers())
elif QT_API in ("pyqt6", "pyside6"):
fakeEvent = QMouseEvent(QEvent.MouseButtonRelease, event.localPos(),
Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, event.modifiers())
super().mouseReleaseEvent(fakeEvent)
QApplication.setOverrideCursor(Qt.CrossCursor)
return
else:
self.rubberBandDraggingRectangle = True
super().mousePressEvent(event)
def leftMouseButtonRelease(self, event: QMouseEvent):
"""When Left mouse button was released"""
# get the item on which we release the mouse button on
item = self.getItemAtClick(event)
try:
# logic - Shift + LMB release (add selection)
if hasattr(item, "node") or isinstance(item, QDMGraphicsEdge) or item is None:
if isSHIFTPressed(event):
event.ignore()
if QT_API in ("pyqt5", "pyside2"):
fakeEvent = QMouseEvent(event.type(), event.localPos(), event.screenPos(),
Qt.LeftButton, Qt.NoButton,
event.modifiers() | Qt.ControlModifier)
elif QT_API in ("pyqt6", "pyside6"):
fakeEvent = QMouseEvent(event.type(), event.localPos(),
Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton,
event.modifiers() | Qt.KeyboardModifier.ControlModifier)
super().mouseReleaseEvent(fakeEvent)
return
if self.mode == MODE_EDGE_DRAG:
if self.distanceBetweenClickAndReleaseIsOff(event):
if self.isSnappingEnabled(event):
item = self.snapping.getSnappedSocketItem(event)
res = self.dragging.edgeDragEnd(item)
if res: return
if self.mode == MODE_EDGES_REROUTING:
if self.isSnappingEnabled(event):
item = self.snapping.getSnappedSocketItem(event)
if not EDGE_REROUTING_UE:
# version 2 -- more consistent with the nodeeditor?
if not self.rerouting.first_mb_release:
# for confirmation of first MB release
self.rerouting.first_mb_release = True
# skip any re-routing until first MB was released
return
self.rerouting.stopRerouting(item.socket if isinstance(item, QDMGraphicsSocket) else None)
# don't forget to end the REROUTING MODE
self.mode = MODE_NOOP
if self.mode == MODE_EDGE_CUT:
self.cutIntersectingEdges()
self.cutline.line_points = []
self.cutline.update()
QApplication.setOverrideCursor(Qt.ArrowCursor)
self.mode = MODE_NOOP
return
if self.mode == MODE_NODE_DRAG:
scenepos = self.mapToScene(event.pos())
self.edgeIntersect.leaveState(scenepos.x(), scenepos.y())
self.mode = MODE_NOOP
self.update()
if self.rubberBandDraggingRectangle:
self.rubberBandDraggingRectangle = False
current_selected_items = self.grScene.selectedItems()
if current_selected_items != self.grScene.scene._last_selected_items:
if current_selected_items == []:
self.grScene.itemsDeselected.emit()
else:
self.grScene.itemSelected.emit()
self.grScene.scene._last_selected_items = current_selected_items
# the rubber band rectangle doesn't disappear without handling the event
super().mouseReleaseEvent(event)
return
# otherwise deselect everything
if item is None:
self.grScene.itemsDeselected.emit()
except: dumpException()
super().mouseReleaseEvent(event)
def rightMouseButtonPress(self, event: QMouseEvent):
"""When Right mouse button was pressed"""
super().mousePressEvent(event)
def rightMouseButtonRelease(self, event: QMouseEvent):
"""When Right mouse button was release"""
## cannot be because with dragging RMB we spawn Create New Node Context Menu
## However, you could use this if you want to cancel with RMB
# if self.mode == MODE_EDGE_DRAG:
# self.dragging.edgeDragEnd(None)
# return
super().mouseReleaseEvent(event)
def mouseMoveEvent(self, event: QMouseEvent):
"""Overriden Qt's ``mouseMoveEvent`` handling Scene/View logic"""
scenepos = self.mapToScene(event.pos())
try:
modified = self.setSocketHighlights(scenepos, highlighted=False, radius=EDGE_SNAPPING_RADIUS+100)
if self.isSnappingEnabled(event):
_, scenepos = self.snapping.getSnappedToSocketPosition(scenepos)
if modified: self.update()
if self.mode == MODE_EDGE_DRAG:
self.dragging.updateDestination(scenepos.x(), scenepos.y())
if self.mode == MODE_NODE_DRAG:
self.edgeIntersect.update(scenepos.x(), scenepos.y())
if self.mode == MODE_EDGES_REROUTING:
self.rerouting.updateScenePos(scenepos.x(), scenepos.y())
if self.mode == MODE_EDGE_CUT and self.cutline is not None:
self.cutline.line_points.append(scenepos)
self.cutline.update()
except Exception as e:
dumpException()
self.last_scene_mouse_position = scenepos
self.scenePosChanged.emit( int(scenepos.x()), int(scenepos.y()) )
super().mouseMoveEvent(event)
def keyPressEvent(self, event: QKeyEvent):
"""
.. note::
This overridden Qt's method was used for handling key shortcuts, before we implemented proper
``QWindow`` with Actions and Menu. Still the commented code serves as an example on how to handle
key presses without Qt's framework for Actions and shortcuts. There is also an example on
how to solve the problem when a Node contains Text/LineEdit and we press the `Delete`
key (also serving to delete `Node`)
:param event: Qt's Key event
:type event: ``QKeyEvent``
:return:
"""
# Use this code below if you wanna have shortcuts in this widget.
# You want to use this, when you don't have a window which handles these shortcuts for you
# if event.key() == Qt.Key_Delete:
# if not self.editingFlag:
# self.deleteSelected()
# else:
# super().keyPressEvent(event)
# elif event.key() == Qt.Key_S and event.modifiers() & Qt.ControlModifier:
# self.grScene.scene.saveToFile("graph.json")
# elif event.key() == Qt.Key_L and event.modifiers() & Qt.ControlModifier:
# self.grScene.scene.loadFromFile("graph.json")
# elif event.key() == Qt.Key_Z and isCTRLPressed(event) and not isSHIFTPressed(event):
# self.grScene.scene.history.undo()
# elif event.key() == Qt.Key_Z and isCTRLPressed(event) and isSHIFTPressed(event):
# self.grScene.scene.history.redo()
# elif event.key() == Qt.Key_H:
# print("HISTORY: len(%d)" % len(self.grScene.scene.history.history_stack),
# " -- current_step", self.grScene.scene.history.history_current_step)
# ix = 0
# for item in self.grScene.scene.history.history_stack:
# print("#", ix, "--", item['desc'])
# ix += 1
# else:
super().keyPressEvent(event)
def cutIntersectingEdges(self):
"""Compare which `Edges` intersect with current `Cut line` and delete them safely"""
for ix in range(len(self.cutline.line_points) - 1):
p1 = self.cutline.line_points[ix]
p2 = self.cutline.line_points[ix + 1]
# @TODO: we could collect all touched nodes, and notify them once after all edges removed
# we could cut 3 edges leading to a single nodeeditor this will notify it 3x
# maybe we could use some Notifier class with methods collect() and dispatch()
for edge in self.grScene.scene.edges.copy():
if edge.grEdge.intersectsWith(p1, p2):
edge.remove()
self.grScene.scene.history.storeHistory("Delete cutted edges", setModified=True)
def setSocketHighlights(self, scenepos: QPointF, highlighted: bool = True, radius: float = 50):
"""Set/disable socket highlights in Scene area defined by `scenepos` and `radius`"""
scanrect = QRectF(scenepos.x() - radius, scenepos.y() - radius, radius * 2, radius * 2)
items = self.grScene.items(scanrect)
items = list(filter(lambda x: isinstance(x, QDMGraphicsSocket), items))
for grSocket in items: grSocket.isHighlighted = highlighted
return items
def deleteSelected(self):
"""Shortcut for safe deleting every object selected in the `Scene`."""
from src.nodes.output import BaseNode_Output
for item in self.grScene.selectedItems():
if isinstance(item, QDMGraphicsEdge):
item.edge.remove()
elif hasattr(item, 'node'):
node = item.node
if isinstance(node, BaseNode_Output):
# Optionally show a warning here
print("Warning: Output node cannot be deleted.")
continue
node.remove()
self.grScene.scene.history.storeHistory("Delete selected", setModified=True)
def debug_modifiers(self, event):
"""Helper function get string if we hold Ctrl, Shift or Alt modifier keys"""
out = "MODS: "
if isSHIFTPressed(event): out += "SHIFT "
if isCTRLPressed(event): out += "CTRL "
if isALTPressed(event): out += "ALT "
return out
def getItemAtClick(self, event: QEvent) -> 'QGraphicsItem':
"""Return the object on which we've clicked/release mouse button
:param event: Qt's mouse or key event
:type event: ``QEvent``
:return: ``QGraphicsItem`` which the mouse event happened or ``None``
"""
pos = event.pos()
obj = self.itemAt(pos)
return obj
def distanceBetweenClickAndReleaseIsOff(self, event:QMouseEvent) -> bool:
""" Measures if we are too far from the last Mouse button click scene position.
This is used for detection if we release too far after we clicked on a `Socket`
:param event: Qt's mouse event
:type event: ``QMouseEvent``
:return: ``True`` if we released too far from where we clicked before
"""
new_lmb_release_scene_pos = self.mapToScene(event.pos())
dist_scene = new_lmb_release_scene_pos - self.last_lmb_click_scene_pos
edge_drag_threshold_sq = EDGE_DRAG_START_THRESHOLD*EDGE_DRAG_START_THRESHOLD
return (dist_scene.x()*dist_scene.x() + dist_scene.y()*dist_scene.y()) > edge_drag_threshold_sq
def wheelEvent(self, event: QWheelEvent):
"""overridden Qt's ``wheelEvent``. This handles zooming"""
# calculate our zoom Factor
zoomOutFactor = 1 / self.zoomInFactor
# calculate zoom
if event.angleDelta().y() > 0:
zoomFactor = self.zoomInFactor
self.zoom += self.zoomStep
else:
zoomFactor = zoomOutFactor
self.zoom -= self.zoomStep
clamped = False
if self.zoom < self.zoomRange[0]: self.zoom, clamped = self.zoomRange[0], True
if self.zoom > self.zoomRange[1]: self.zoom, clamped = self.zoomRange[1], True
# set scene scale
if not clamped or self.zoomClamp is False:
self.scale(zoomFactor, zoomFactor)
# Update render hints based on zoom level for performance
self.updateRenderHints()
# Update LOD for all nodes when zoom changes
self.updateNodeLODs()
def updateRenderHints(self):
"""Update render hints based on zoom level for performance"""
if hasattr(self, 'zoom'):
if self.zoom <= 6:
# Use low quality render hints for better performance at low zoom
self.setRenderHints(self._render_hints_low)
else:
# Use high quality render hints at high zoom
self.setRenderHints(self._render_hints_high)
def updateNodeLODs(self):
"""Update Level of Detail for all nodes based on current zoom - optimized version with proper ignore list"""
if not hasattr(self.grScene, 'scene') or not hasattr(self.grScene.scene, 'nodes'):
return
# Get visible rect to only update visible items
visible_rect = self.mapToScene(self.viewport().rect()).boundingRect()
# Cache the current LOD to avoid recalculating for each item
current_lod = None
if hasattr(self, 'zoom'):
if self.zoom <= 3:
current_lod = 0
elif self.zoom <= 6:
current_lod = 1
else:
current_lod = 2
else:
current_lod = 2
# Only update visible nodes
for node in self.grScene.scene.nodes:
if not hasattr(node, 'grNode'):
continue
# Skip comment and frame nodes - they should not be affected by LOD system
# Check by op_code (more reliable than class name)
if hasattr(node, 'op_code'):
# Import constants to check node types
try:
from src.conf import OP_NODE_COMMENT, OP_NODE_FRAME
if node.op_code in [OP_NODE_COMMENT, OP_NODE_FRAME]:
continue # Skip LOD processing for these node types
except ImportError:
# Fallback to class name check if import fails
node_class_name = node.__class__.__name__
if node_class_name in ['CommentNode', 'FrameNode']:
continue
else:
# Fallback to class name check if no op_code
node_class_name = node.__class__.__name__
if node_class_name in ['CommentNode', 'FrameNode']:
continue
# Check if node is visible in current viewport
node_rect = node.grNode.boundingRect()
node_rect.translate(node.grNode.pos())
if not visible_rect.intersects(node_rect):
continue # Skip invisible nodes
# Update title visibility
if hasattr(node.grNode, 'title_item') and node.grNode.title_item:
should_show_title = current_lod >= 1
if node.grNode.title_item.isVisible() != should_show_title:
node.grNode.title_item.setVisible(should_show_title)
# Update content visibility
if hasattr(node.grNode, 'grContent') and node.grNode.grContent:
should_show_content = current_lod >= 2
if node.grNode.grContent.isVisible() != should_show_content:
node.grNode.grContent.setVisible(should_show_content)
# Only update if something changed
node.grNode.update()
# Update visible edges only
for edge in self.grScene.scene.edges:
if not hasattr(edge, 'grEdge') or not edge.grEdge:
continue
# Simple visibility check for edges
edge_rect = edge.grEdge.boundingRect()
if edge_rect.isValid() and visible_rect.intersects(edge_rect):
edge.grEdge.update()