nodeeditor-qtpy 0.8.1__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.
- nodeeditor/__init__.py +93 -0
- nodeeditor/node_colors_config.py +343 -0
- nodeeditor/node_content_widget.py +129 -0
- nodeeditor/node_decorators.py +65 -0
- nodeeditor/node_edge.py +518 -0
- nodeeditor/node_edge_dragging.py +185 -0
- nodeeditor/node_edge_intersect.py +219 -0
- nodeeditor/node_edge_rerouting.py +216 -0
- nodeeditor/node_edge_snapping.py +95 -0
- nodeeditor/node_edge_validators.py +82 -0
- nodeeditor/node_editor_widget.py +257 -0
- nodeeditor/node_editor_window.py +448 -0
- nodeeditor/node_graph.py +583 -0
- nodeeditor/node_graphics_cutline.py +57 -0
- nodeeditor/node_graphics_edge.py +375 -0
- nodeeditor/node_graphics_edge_path.py +226 -0
- nodeeditor/node_graphics_node.py +693 -0
- nodeeditor/node_graphics_scene.py +369 -0
- nodeeditor/node_graphics_socket.py +262 -0
- nodeeditor/node_graphics_view.py +907 -0
- nodeeditor/node_group.py +1286 -0
- nodeeditor/node_group_node.py +5 -0
- nodeeditor/node_icon_content_widget.py +176 -0
- nodeeditor/node_icon_graphics_node.py +524 -0
- nodeeditor/node_multi_input_node.py +450 -0
- nodeeditor/node_node.py +848 -0
- nodeeditor/node_scene.py +1106 -0
- nodeeditor/node_scene_clipboard.py +314 -0
- nodeeditor/node_scene_history.py +556 -0
- nodeeditor/node_serializable.py +43 -0
- nodeeditor/node_socket.py +445 -0
- nodeeditor/node_socket_shapes.py +261 -0
- nodeeditor/py.typed +0 -0
- nodeeditor/qss/nodestyle.qss +3 -0
- nodeeditor/utils.py +108 -0
- nodeeditor/utils_no_qt.py +21 -0
- nodeeditor_qtpy-0.8.1.dist-info/METADATA +165 -0
- nodeeditor_qtpy-0.8.1.dist-info/RECORD +40 -0
- nodeeditor_qtpy-0.8.1.dist-info/WHEEL +4 -0
- nodeeditor_qtpy-0.8.1.dist-info/licenses/LICENCE +23 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
A module containing the Graphics representation of an Edge
|
|
4
|
+
"""
|
|
5
|
+
from qtpy.QtWidgets import QGraphicsPathItem, QWidget, QGraphicsItem, QGraphicsSceneHoverEvent, QGraphicsSimpleTextItem
|
|
6
|
+
from qtpy.QtGui import QColor, QPen, QBrush, QPainterPath, QFont
|
|
7
|
+
from qtpy.QtCore import Qt, QRectF, QPointF
|
|
8
|
+
|
|
9
|
+
from nodeeditor.node_graphics_edge_path import GraphicsEdgePathBezier, GraphicsEdgePathDirect, GraphicsEdgePathSquare, GraphicsEdgePathImprovedSharp, GraphicsEdgePathImprovedBezier
|
|
10
|
+
from nodeeditor.node_colors_config import get_color_scheme
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
17
|
+
from nodeeditor.node_edge import Edge
|
|
18
|
+
from nodeeditor.node_socket import Socket
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class QDMGraphicsEdge(QGraphicsPathItem):
|
|
22
|
+
"""Base class for Graphics Edge"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, edge: 'Edge', parent: QGraphicsPathItem = None) -> None:
|
|
25
|
+
"""
|
|
26
|
+
:param edge: reference to :class:`~nodeeditor.node_edge.Edge`
|
|
27
|
+
:type edge: :class:`~nodeeditor.node_edge.Edge`
|
|
28
|
+
:param parent: parent widget
|
|
29
|
+
:type parent: ``QWidget``
|
|
30
|
+
|
|
31
|
+
:Instance attributes:
|
|
32
|
+
|
|
33
|
+
- **edge** - reference to :class:`~nodeeditor.node_edge.Edge`
|
|
34
|
+
- **posSource** - ``[x, y]`` source position in the `Scene`
|
|
35
|
+
- **posDestination** - ``[x, y]`` destination position in the `Scene`
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(parent)
|
|
38
|
+
|
|
39
|
+
self.edge = edge
|
|
40
|
+
|
|
41
|
+
# create instance of our path class
|
|
42
|
+
self.pathCalculator = self.determineEdgePathClass()(self)
|
|
43
|
+
|
|
44
|
+
# init our flags
|
|
45
|
+
self._last_selected_state = False
|
|
46
|
+
self.hovered = False
|
|
47
|
+
|
|
48
|
+
# init our variables
|
|
49
|
+
self.posSource: List[float] = [0, 0]
|
|
50
|
+
self.posDestination: List[float] = [200, 100]
|
|
51
|
+
|
|
52
|
+
# code-settable, user-read-only label drawn on top of the edge path.
|
|
53
|
+
# Never editable/selectable/movable: users cannot change it, code can
|
|
54
|
+
# via QDMGraphicsEdge.setLabel() / Edge.label.
|
|
55
|
+
self._label_offset = QPointF(0, -18)
|
|
56
|
+
self.labelItem = QGraphicsSimpleTextItem(self)
|
|
57
|
+
self.labelItem.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
|
58
|
+
self.labelItem.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
|
59
|
+
self.labelItem.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, False)
|
|
60
|
+
self.labelItem.setAcceptedMouseButtons(Qt.MouseButton.NoButton)
|
|
61
|
+
self.labelItem.setAcceptHoverEvents(False)
|
|
62
|
+
# QGraphicsSimpleTextItem has no text-interaction/editing API at all,
|
|
63
|
+
# which is exactly what we want: code-only, never user-editable.
|
|
64
|
+
self.labelItem.setBrush(QBrush(QColor("#ffffff")))
|
|
65
|
+
self.labelItem.setZValue(1)
|
|
66
|
+
self.labelItem.setVisible(False)
|
|
67
|
+
|
|
68
|
+
self.initAssets()
|
|
69
|
+
self.initUI()
|
|
70
|
+
|
|
71
|
+
def initUI(self) -> None:
|
|
72
|
+
"""Set up this ``QGraphicsPathItem``"""
|
|
73
|
+
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
|
74
|
+
self.setAcceptHoverEvents(True)
|
|
75
|
+
self.setZValue(-1)
|
|
76
|
+
|
|
77
|
+
def initAssets(self) -> None:
|
|
78
|
+
"""Initialize ``QObjects`` like ``QColor``, ``QPen`` and ``QBrush``"""
|
|
79
|
+
# Get colors from global color scheme
|
|
80
|
+
scheme = get_color_scheme()
|
|
81
|
+
self._color = self._default_color = scheme.edges.default
|
|
82
|
+
self._color_selected = scheme.edges.selected
|
|
83
|
+
self._color_hovered = scheme.edges.hovered
|
|
84
|
+
self._pen = QPen(self._color)
|
|
85
|
+
self._pen_selected = QPen(self._color_selected)
|
|
86
|
+
self._pen_dragging = QPen(scheme.edges.dragging)
|
|
87
|
+
self._pen_hovered = QPen(self._color_hovered)
|
|
88
|
+
self._pen_dragging.setStyle(Qt.PenStyle.DashLine)
|
|
89
|
+
self._pen.setWidthF(3.0)
|
|
90
|
+
self._pen_selected.setWidthF(3.0)
|
|
91
|
+
self._pen_dragging.setWidthF(3.0)
|
|
92
|
+
self._pen_hovered.setWidthF(5.0)
|
|
93
|
+
|
|
94
|
+
def createEdgePathCalculator(self):
|
|
95
|
+
"""Create instance of :class:`~nodeeditor.node_graphics_edge_path.GraphicsEdgePathBase`"""
|
|
96
|
+
self.pathCalculator = self.determineEdgePathClass()(self)
|
|
97
|
+
return self.pathCalculator
|
|
98
|
+
|
|
99
|
+
def determineEdgePathClass(self):
|
|
100
|
+
"""Decide which GraphicsEdgePath class should be used to calculate path according to edge.edge_type value"""
|
|
101
|
+
from nodeeditor.node_edge import EDGE_TYPE_BEZIER, EDGE_TYPE_DIRECT, EDGE_TYPE_SQUARE, EDGE_TYPE_IMPROVED_SHARP, EDGE_TYPE_IMPROVED_BEZIER
|
|
102
|
+
if self.edge.edge_type == EDGE_TYPE_BEZIER:
|
|
103
|
+
return GraphicsEdgePathBezier
|
|
104
|
+
if self.edge.edge_type == EDGE_TYPE_DIRECT:
|
|
105
|
+
return GraphicsEdgePathDirect
|
|
106
|
+
if self.edge.edge_type == EDGE_TYPE_SQUARE:
|
|
107
|
+
return GraphicsEdgePathSquare
|
|
108
|
+
if self.edge.edge_type == EDGE_TYPE_IMPROVED_SHARP:
|
|
109
|
+
return GraphicsEdgePathImprovedSharp
|
|
110
|
+
if self.edge.edge_type == EDGE_TYPE_IMPROVED_BEZIER:
|
|
111
|
+
return GraphicsEdgePathImprovedBezier
|
|
112
|
+
|
|
113
|
+
else:
|
|
114
|
+
return GraphicsEdgePathImprovedBezier
|
|
115
|
+
|
|
116
|
+
def makeUnselectable(self) -> None:
|
|
117
|
+
"""Used for drag edge to disable click detection over this graphics item"""
|
|
118
|
+
self.setFlag(QGraphicsItem.ItemIsSelectable, False)
|
|
119
|
+
self.setAcceptHoverEvents(False)
|
|
120
|
+
|
|
121
|
+
def changeColor(self, color) -> None:
|
|
122
|
+
"""Change color of the edge from string hex value '#00ff00'"""
|
|
123
|
+
# print("^Called change color to:", color.red(), color.green(), color.blue(), "on edge:", self.edge)
|
|
124
|
+
self._color = QColor(color) if type(color) == str else color
|
|
125
|
+
self._pen = QPen(self._color)
|
|
126
|
+
self._pen.setWidthF(3.0)
|
|
127
|
+
|
|
128
|
+
def setColorFromSockets(self) -> bool:
|
|
129
|
+
"""Change color according to connected sockets. Returns ``True`` if color can be determined"""
|
|
130
|
+
socket_type_start = self.edge.start_socket.socket_type
|
|
131
|
+
socket_type_end = self.edge.end_socket.socket_type
|
|
132
|
+
if socket_type_start != socket_type_end:
|
|
133
|
+
return False
|
|
134
|
+
self.changeColor(
|
|
135
|
+
self.edge.start_socket.grSocket.getSocketColor(socket_type_start))
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
def label(self) -> str:
|
|
140
|
+
"""Return the current code-set edge label (``""`` when unset)."""
|
|
141
|
+
return self.labelItem.text()
|
|
142
|
+
|
|
143
|
+
def setLabel(self, text: str) -> None:
|
|
144
|
+
"""Set the text drawn on top of the edge.
|
|
145
|
+
|
|
146
|
+
Code-only API: the label item is not selectable, movable, focusable
|
|
147
|
+
and accepts no mouse buttons, so users cannot edit it directly.
|
|
148
|
+
|
|
149
|
+
:param text: label text, ``""``/``None`` hides the label
|
|
150
|
+
"""
|
|
151
|
+
self.labelItem.setText(text or "")
|
|
152
|
+
self.labelItem.setVisible(bool(text))
|
|
153
|
+
self.updateLabelPosition()
|
|
154
|
+
self.update()
|
|
155
|
+
|
|
156
|
+
def setLabelColor(self, color) -> None:
|
|
157
|
+
"""Set label text color from ``QColor`` or hex string."""
|
|
158
|
+
self.labelItem.setBrush(QBrush(QColor(color) if isinstance(color, str) else color))
|
|
159
|
+
self.update()
|
|
160
|
+
|
|
161
|
+
def setLabelFont(self, font: QFont) -> None:
|
|
162
|
+
"""Set label font, then recenter it on the edge path."""
|
|
163
|
+
self.labelItem.setFont(font)
|
|
164
|
+
self.updateLabelPosition()
|
|
165
|
+
self.update()
|
|
166
|
+
|
|
167
|
+
def setLabelOffset(self, x: float, y: float) -> None:
|
|
168
|
+
"""Offset (in px) applied to the centered label position."""
|
|
169
|
+
self._label_offset = QPointF(x, y)
|
|
170
|
+
self.updateLabelPosition()
|
|
171
|
+
self.update()
|
|
172
|
+
|
|
173
|
+
def updateLabelPosition(self) -> None:
|
|
174
|
+
"""Center the label on top of the current edge path."""
|
|
175
|
+
if not self.labelItem.isVisible():
|
|
176
|
+
return
|
|
177
|
+
try:
|
|
178
|
+
path = self.path()
|
|
179
|
+
mid = path.pointAtPercent(0.5) if path.length() > 0 else QPointF(
|
|
180
|
+
(self.posSource[0] + self.posDestination[0]) / 2.0,
|
|
181
|
+
(self.posSource[1] + self.posDestination[1]) / 2.0,
|
|
182
|
+
)
|
|
183
|
+
except Exception:
|
|
184
|
+
mid = QPointF(
|
|
185
|
+
(self.posSource[0] + self.posDestination[0]) / 2.0,
|
|
186
|
+
(self.posSource[1] + self.posDestination[1]) / 2.0,
|
|
187
|
+
)
|
|
188
|
+
rect = self.labelItem.boundingRect()
|
|
189
|
+
self.labelItem.setPos(mid + self._label_offset - QPointF(rect.width() / 2.0, rect.height() / 2.0))
|
|
190
|
+
|
|
191
|
+
def onSelected(self) -> None:
|
|
192
|
+
"""Our event handling when the edge was selected"""
|
|
193
|
+
self.edge.scene.grScene.itemSelected.emit()
|
|
194
|
+
|
|
195
|
+
def doSelect(self, new_state: bool = True) -> None:
|
|
196
|
+
"""Safe version of selecting the `Graphics Node`. Takes care about the selection state flag used internally
|
|
197
|
+
|
|
198
|
+
:param new_state: ``True`` to select, ``False`` to deselect
|
|
199
|
+
:type new_state: ``bool``
|
|
200
|
+
"""
|
|
201
|
+
self.setSelected(new_state)
|
|
202
|
+
self._last_selected_state = new_state
|
|
203
|
+
if new_state:
|
|
204
|
+
self.onSelected()
|
|
205
|
+
|
|
206
|
+
def mouseReleaseEvent(self, event) -> None:
|
|
207
|
+
"""Overridden Qt's method to handle selecting and deselecting this `Graphics Edge`"""
|
|
208
|
+
super().mouseReleaseEvent(event)
|
|
209
|
+
if self._last_selected_state != self.isSelected():
|
|
210
|
+
self.edge.scene.resetLastSelectedStates()
|
|
211
|
+
self._last_selected_state = self.isSelected()
|
|
212
|
+
self.onSelected()
|
|
213
|
+
|
|
214
|
+
def hoverEnterEvent(self, event: Optional['QGraphicsSceneHoverEvent']) -> None:
|
|
215
|
+
"""Handle hover effect
|
|
216
|
+
|
|
217
|
+
:param event: The hover event
|
|
218
|
+
:type event: Optional[QGraphicsSceneHoverEvent]
|
|
219
|
+
"""
|
|
220
|
+
self.hovered = True
|
|
221
|
+
self.update()
|
|
222
|
+
|
|
223
|
+
def hoverLeaveEvent(self, event: Optional['QGraphicsSceneHoverEvent']) -> None:
|
|
224
|
+
"""Handle hover effect
|
|
225
|
+
|
|
226
|
+
:param event: The hover event
|
|
227
|
+
:type event: Optional[QGraphicsSceneHoverEvent]
|
|
228
|
+
"""
|
|
229
|
+
self.hovered = False
|
|
230
|
+
self.update()
|
|
231
|
+
|
|
232
|
+
def setSource(self, x: float, y: float) -> None:
|
|
233
|
+
""" Set source point
|
|
234
|
+
|
|
235
|
+
:param x: x position
|
|
236
|
+
:type x: ``float``
|
|
237
|
+
:param y: y position
|
|
238
|
+
:type y: ``float``
|
|
239
|
+
"""
|
|
240
|
+
self.setPositions((x, y), self.posDestination)
|
|
241
|
+
|
|
242
|
+
def setDestination(self, x: float, y: float) -> None:
|
|
243
|
+
""" Set destination point
|
|
244
|
+
|
|
245
|
+
:param x: x position
|
|
246
|
+
:type x: ``float``
|
|
247
|
+
:param y: y position
|
|
248
|
+
:type y: ``float``
|
|
249
|
+
"""
|
|
250
|
+
self.setPositions(self.posSource, (x, y))
|
|
251
|
+
|
|
252
|
+
def setPositions(self, source, destination) -> None:
|
|
253
|
+
"""Set both ends and adopt the resulting path once.
|
|
254
|
+
|
|
255
|
+
A node move changes both ends. Adopting inside each setter would run the
|
|
256
|
+
route calculation twice per edge per move, and that calculation is the
|
|
257
|
+
expensive part of moving a node.
|
|
258
|
+
|
|
259
|
+
:param source: new source point
|
|
260
|
+
:param destination: new destination point
|
|
261
|
+
"""
|
|
262
|
+
self.posSource = [source[0], source[1]]
|
|
263
|
+
self.posDestination = [destination[0], destination[1]]
|
|
264
|
+
self.adoptGeometry()
|
|
265
|
+
|
|
266
|
+
def adoptGeometry(self) -> None:
|
|
267
|
+
"""Recompute the drawn path right away, so Qt invalidates the true rect.
|
|
268
|
+
|
|
269
|
+
The setters run before the caller repaints, and a path picked up only
|
|
270
|
+
while painting invalidates the *previous* geometry (see
|
|
271
|
+
:meth:`refreshPath`).
|
|
272
|
+
"""
|
|
273
|
+
if getattr(self, "pathCalculator", None) is None:
|
|
274
|
+
return
|
|
275
|
+
if getattr(self, "labelItem", None) is None:
|
|
276
|
+
return
|
|
277
|
+
self.refreshPath()
|
|
278
|
+
|
|
279
|
+
def strokeMargin(self) -> float:
|
|
280
|
+
"""Half the widest pen this edge can paint with, plus an antialias pixel.
|
|
281
|
+
|
|
282
|
+
``paint`` strokes the path, and a stroke reaches half its width past the
|
|
283
|
+
path on both sides. A ``boundingRect`` of the bare path is therefore
|
|
284
|
+
smaller than what the item draws, and every repaint leaves the outer band
|
|
285
|
+
of the *previous* stroke on screen. That is not a one-pixel cosmetic
|
|
286
|
+
miss: while a node is dragged, each frame adds a band along the path's
|
|
287
|
+
old position, and dozens of frames accumulate into the smeared arcs and
|
|
288
|
+
loops the canvas was showing.
|
|
289
|
+
"""
|
|
290
|
+
widest = 0.0
|
|
291
|
+
for name in ("_pen", "_pen_selected", "_pen_dragging", "_pen_hovered"):
|
|
292
|
+
pen = getattr(self, name, None)
|
|
293
|
+
if pen is not None:
|
|
294
|
+
widest = max(widest, pen.widthF())
|
|
295
|
+
return widest / 2.0 + 1.0
|
|
296
|
+
|
|
297
|
+
def boundingRect(self) -> QRectF:
|
|
298
|
+
"""Defining Qt' bounding rectangle (path united with label rect)."""
|
|
299
|
+
margin = self.strokeMargin()
|
|
300
|
+
base = self.shape().boundingRect().adjusted(-margin, -margin, margin, margin)
|
|
301
|
+
try:
|
|
302
|
+
if self.labelItem.isVisible():
|
|
303
|
+
label_rect = self.labelItem.boundingRect().translated(self.labelItem.pos())
|
|
304
|
+
return base.united(label_rect)
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
return base
|
|
308
|
+
|
|
309
|
+
def refreshPath(self) -> None:
|
|
310
|
+
"""Adopt the current path and label position now.
|
|
311
|
+
|
|
312
|
+
Qt invalidates an item by its *declared* geometry, so an item that only
|
|
313
|
+
adopts a new path while painting invalidates the previous one and draws
|
|
314
|
+
the new one: whatever the old path covered outside both rectangles stays
|
|
315
|
+
on screen. Callers that know the edge moved (``updatePositions``) call
|
|
316
|
+
this first, which makes ``setPath`` repaint the union of old and new.
|
|
317
|
+
"""
|
|
318
|
+
self.setPath(self.calcPath())
|
|
319
|
+
self.updateLabelPosition()
|
|
320
|
+
|
|
321
|
+
def shape(self) -> QPainterPath:
|
|
322
|
+
"""Returns ``QPainterPath`` representation of this `Edge`
|
|
323
|
+
|
|
324
|
+
:return: path representation
|
|
325
|
+
:rtype: ``QPainterPath``
|
|
326
|
+
"""
|
|
327
|
+
return self.calcPath()
|
|
328
|
+
|
|
329
|
+
def paint(self, painter, QStyleOptionGraphicsItem, widget=None) -> None:
|
|
330
|
+
"""Qt's overridden method to paint this Graphics Edge. Path calculated
|
|
331
|
+
in :func:`~nodeeditor.node_graphics_edge.QDMGraphicsEdge.calcPath` method"""
|
|
332
|
+
# A no-op once refreshPath() has run for this geometry (Qt's setPath
|
|
333
|
+
# returns early on an unchanged path); kept for consumers that only ever
|
|
334
|
+
# paint.
|
|
335
|
+
self.setPath(self.calcPath())
|
|
336
|
+
self.updateLabelPosition()
|
|
337
|
+
|
|
338
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
339
|
+
|
|
340
|
+
if self.hovered and self.edge.end_socket is not None:
|
|
341
|
+
painter.setPen(self._pen_hovered)
|
|
342
|
+
painter.drawPath(self.path())
|
|
343
|
+
|
|
344
|
+
if self.edge.end_socket is None:
|
|
345
|
+
painter.setPen(self._pen_dragging)
|
|
346
|
+
else:
|
|
347
|
+
painter.setPen(self._pen if not self.isSelected()
|
|
348
|
+
else self._pen_selected)
|
|
349
|
+
|
|
350
|
+
painter.drawPath(self.path())
|
|
351
|
+
|
|
352
|
+
def intersectsWith(self, p1: QPointF, p2: QPointF) -> bool:
|
|
353
|
+
"""Does this Graphics Edge intersect with the line between point A and point B ?
|
|
354
|
+
|
|
355
|
+
:param p1: point A
|
|
356
|
+
:type p1: ``QPointF``
|
|
357
|
+
:param p2: point B
|
|
358
|
+
:type p2: ``QPointF``
|
|
359
|
+
:return: ``True`` if this `Graphics Edge` intersects
|
|
360
|
+
:rtype: ``bool``
|
|
361
|
+
"""
|
|
362
|
+
cutpath = QPainterPath(p1)
|
|
363
|
+
cutpath.lineTo(p2)
|
|
364
|
+
path = self.calcPath()
|
|
365
|
+
return cutpath.intersects(path)
|
|
366
|
+
|
|
367
|
+
def calcPath(self) -> QPainterPath:
|
|
368
|
+
"""Will handle drawing QPainterPath from Point A to B. Internally there exist self.pathCalculator which
|
|
369
|
+
is an instance of derived :class:`~nodeeditor.node_graphics_edge_path.GraphicsEdgePathBase` class
|
|
370
|
+
containing the actual `calcPath()` function - computing how the edge should look like.
|
|
371
|
+
|
|
372
|
+
:returns: ``QPainterPath`` of the edge connecting `source` and `destination`
|
|
373
|
+
:rtype: ``QPainterPath``
|
|
374
|
+
"""
|
|
375
|
+
return self.pathCalculator.calcPath()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module containing the classes that calculate the graphics path drawn for an Edge.
|
|
3
|
+
|
|
4
|
+
Every class derives from :class:`GraphicsEdgePathBase` and implements
|
|
5
|
+
:meth:`GraphicsEdgePathBase.calcPath`, which turns the positions of the edge's
|
|
6
|
+
two sockets into the :class:`~qtpy.QtGui.QPainterPath` that gets painted.
|
|
7
|
+
"""
|
|
8
|
+
import math
|
|
9
|
+
from qtpy.QtCore import QPointF
|
|
10
|
+
from qtpy.QtGui import QPainterPath
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from nodeeditor.node_graphics_view import QDMGraphicsView
|
|
17
|
+
from nodeeditor.node_edge import Edge
|
|
18
|
+
from nodeeditor.node_socket import Socket
|
|
19
|
+
from nodeeditor.node_graphics_edge import QDMGraphicsEdge
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
EDGE_CP_ROUNDNESS = 100 #: Bezier control point distance on the line
|
|
23
|
+
#: factor for square edge to change the midpoint between start and end socket
|
|
24
|
+
WEIGHT_SOURCE = 0.2
|
|
25
|
+
|
|
26
|
+
#: Scale EDGE_CURVATURE with distance of the edge endpoints
|
|
27
|
+
EDGE_IBCP_ROUNDNESS = 75
|
|
28
|
+
NODE_DISTANCE = 12
|
|
29
|
+
EDGE_CURVATURE = 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GraphicsEdgePathBase:
|
|
33
|
+
"""Base Class for calculating the graphics path to draw for an graphics Edge"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, owner: 'QDMGraphicsEdge') -> None:
|
|
36
|
+
"""
|
|
37
|
+
:param owner: the :class:`~nodeeditor.node_graphics_edge.QDMGraphicsEdge` this path belongs to
|
|
38
|
+
:type owner: :class:`~nodeeditor.node_graphics_edge.QDMGraphicsEdge`
|
|
39
|
+
"""
|
|
40
|
+
# keep the reference to owner GraphicsEdge class
|
|
41
|
+
self.owner = owner
|
|
42
|
+
|
|
43
|
+
def calcPath(self):
|
|
44
|
+
"""Calculate the Direct line connection
|
|
45
|
+
|
|
46
|
+
:returns: ``QPainterPath`` of the graphics path to draw
|
|
47
|
+
:rtype: ``QPainterPath`` or ``None``
|
|
48
|
+
"""
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GraphicsEdgePathDirect(GraphicsEdgePathBase):
|
|
53
|
+
"""Direct line connection Graphics Edge"""
|
|
54
|
+
|
|
55
|
+
def calcPath(self) -> QPainterPath:
|
|
56
|
+
"""Calculate the Direct line connection
|
|
57
|
+
|
|
58
|
+
:returns: ``QPainterPath`` of the direct line
|
|
59
|
+
:rtype: ``QPainterPath``
|
|
60
|
+
"""
|
|
61
|
+
path = QPainterPath(
|
|
62
|
+
QPointF(self.owner.posSource[0], self.owner.posSource[1]))
|
|
63
|
+
path.lineTo(self.owner.posDestination[0], self.owner.posDestination[1])
|
|
64
|
+
return path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GraphicsEdgePathBezier(GraphicsEdgePathBase):
|
|
68
|
+
"""Cubic line connection Graphics Edge"""
|
|
69
|
+
|
|
70
|
+
def calcPath(self) -> QPainterPath:
|
|
71
|
+
"""Calculate the cubic Bezier line connection with 2 control points
|
|
72
|
+
|
|
73
|
+
:returns: ``QPainterPath`` of the cubic Bezier line
|
|
74
|
+
:rtype: ``QPainterPath``
|
|
75
|
+
"""
|
|
76
|
+
s = self.owner.posSource
|
|
77
|
+
d = self.owner.posDestination
|
|
78
|
+
dist = (d[0] - s[0]) * 0.5
|
|
79
|
+
|
|
80
|
+
cpx_s: float = +dist
|
|
81
|
+
cpx_d: float = -dist
|
|
82
|
+
cpy_s: float = 0.0
|
|
83
|
+
cpy_d: float = 0.0
|
|
84
|
+
|
|
85
|
+
if self.owner.edge.start_socket is not None:
|
|
86
|
+
ssin = self.owner.edge.start_socket.is_input
|
|
87
|
+
ssout = self.owner.edge.start_socket.is_output
|
|
88
|
+
|
|
89
|
+
if (s[0] > d[0] and ssout) or (s[0] < d[0] and ssin):
|
|
90
|
+
cpx_d *= -1
|
|
91
|
+
cpx_s *= -1
|
|
92
|
+
|
|
93
|
+
cpy_d = (
|
|
94
|
+
(s[1] - d[1]) / math.fabs(
|
|
95
|
+
(s[1] - d[1]) if (s[1] - d[1]) != 0 else 0.00001
|
|
96
|
+
)
|
|
97
|
+
) * EDGE_CP_ROUNDNESS
|
|
98
|
+
cpy_s = (
|
|
99
|
+
(d[1] - s[1]) / math.fabs(
|
|
100
|
+
(d[1] - s[1]) if (d[1] - s[1]) != 0 else 0.00001
|
|
101
|
+
)
|
|
102
|
+
) * EDGE_CP_ROUNDNESS
|
|
103
|
+
|
|
104
|
+
path = QPainterPath(
|
|
105
|
+
QPointF(self.owner.posSource[0], self.owner.posSource[1]))
|
|
106
|
+
path.cubicTo(s[0] + cpx_s, s[1] + cpy_s, d[0] + cpx_d, d[1] + cpy_d,
|
|
107
|
+
self.owner.posDestination[0], self.owner.posDestination[1])
|
|
108
|
+
|
|
109
|
+
return path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class GraphicsEdgePathSquare(GraphicsEdgePathBase):
|
|
113
|
+
"""Square line connection Graphics Edge"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, *args, handle_weight: float=0.5, **kwargs) -> None:
|
|
116
|
+
"""
|
|
117
|
+
:param args: positional arguments passed on to the base class
|
|
118
|
+
:param handle_weight: where the vertical part of the square sits, ``0.5`` being the middle
|
|
119
|
+
:param kwargs: keyword arguments passed on to the base class
|
|
120
|
+
"""
|
|
121
|
+
super().__init__(*args, **kwargs)
|
|
122
|
+
self.rand = None
|
|
123
|
+
self.handle_weight = handle_weight
|
|
124
|
+
|
|
125
|
+
def calcPath(self):
|
|
126
|
+
"""Calculate the square edge line connection
|
|
127
|
+
|
|
128
|
+
:returns: ``QPainterPath`` of the edge square line
|
|
129
|
+
:rtype: ``QPainterPath``
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
s = self.owner.posSource
|
|
133
|
+
d = self.owner.posDestination
|
|
134
|
+
|
|
135
|
+
mid_x = s[0] + ((d[0] - s[0]) * self.handle_weight)
|
|
136
|
+
|
|
137
|
+
path = QPainterPath(QPointF(s[0], s[1]))
|
|
138
|
+
path.lineTo(mid_x, s[1])
|
|
139
|
+
path.lineTo(mid_x, d[1])
|
|
140
|
+
path.lineTo(d[0], d[1])
|
|
141
|
+
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class GraphicsEdgePathImprovedSharp(GraphicsEdgePathBase):
|
|
146
|
+
"""Better Cubic line connection Graphics Edge"""
|
|
147
|
+
|
|
148
|
+
def calcPath(self) -> QPainterPath:
|
|
149
|
+
"""Calculate the Direct line connection
|
|
150
|
+
|
|
151
|
+
:returns: ``QPainterPath`` of the painting line
|
|
152
|
+
:rtype: ``QPainterPath``
|
|
153
|
+
"""
|
|
154
|
+
sx, sy = self.owner.posSource
|
|
155
|
+
dx, dy = self.owner.posDestination
|
|
156
|
+
distx, disty = dx-sx, dy-sy
|
|
157
|
+
dist = math.sqrt(distx*distx + disty*disty)
|
|
158
|
+
|
|
159
|
+
# is start / end socket on left side?
|
|
160
|
+
sleft = self.owner.edge.start_socket.position <= 3
|
|
161
|
+
|
|
162
|
+
# if the drag edge started from input socket, we should connect to output socket...
|
|
163
|
+
eleft = self.owner.edge.start_socket.position > 3
|
|
164
|
+
|
|
165
|
+
if self.owner.edge.end_socket is not None:
|
|
166
|
+
eleft = self.owner.edge.end_socket.position <= 3
|
|
167
|
+
|
|
168
|
+
node_sdist = (-NODE_DISTANCE) if sleft else NODE_DISTANCE
|
|
169
|
+
node_edist = (-NODE_DISTANCE) if eleft else NODE_DISTANCE
|
|
170
|
+
|
|
171
|
+
path = QPainterPath(QPointF(sx, sy))
|
|
172
|
+
|
|
173
|
+
if abs(dist) > NODE_DISTANCE:
|
|
174
|
+
path.lineTo(sx + node_sdist, sy)
|
|
175
|
+
path.lineTo(dx + node_edist, dy)
|
|
176
|
+
|
|
177
|
+
path.lineTo(dx, dy)
|
|
178
|
+
|
|
179
|
+
return path
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class GraphicsEdgePathImprovedBezier(GraphicsEdgePathBase):
|
|
183
|
+
"""Better Cubic line connection Graphics Edge"""
|
|
184
|
+
|
|
185
|
+
def calcPath(self) -> QPainterPath:
|
|
186
|
+
"""Calculate the Direct line connection
|
|
187
|
+
|
|
188
|
+
:returns: ``QPainterPath`` of the painting line
|
|
189
|
+
:rtype: ``QPainterPath``
|
|
190
|
+
"""
|
|
191
|
+
sx, sy = self.owner.posSource
|
|
192
|
+
dx, dy = self.owner.posDestination
|
|
193
|
+
distx, disty = dx-sx, dy-sy
|
|
194
|
+
dist = math.sqrt(distx*distx + disty*disty)
|
|
195
|
+
|
|
196
|
+
# is start / end socket on left side?
|
|
197
|
+
sleft = self.owner.edge.start_socket.position <= 3
|
|
198
|
+
|
|
199
|
+
# if the drag edge started from input socket, we should connect to output socket...
|
|
200
|
+
eleft = self.owner.edge.start_socket.position > 3
|
|
201
|
+
|
|
202
|
+
if self.owner.edge.end_socket is not None:
|
|
203
|
+
eleft = self.owner.edge.end_socket.position <= 3
|
|
204
|
+
|
|
205
|
+
path = QPainterPath(QPointF(sx, sy))
|
|
206
|
+
|
|
207
|
+
if abs(dist) > NODE_DISTANCE:
|
|
208
|
+
curvature = max(EDGE_CURVATURE, (EDGE_CURVATURE *
|
|
209
|
+
abs(dist)) / EDGE_IBCP_ROUNDNESS)
|
|
210
|
+
|
|
211
|
+
node_sdist = (-NODE_DISTANCE) if sleft else NODE_DISTANCE
|
|
212
|
+
node_edist = (-NODE_DISTANCE) if eleft else NODE_DISTANCE
|
|
213
|
+
|
|
214
|
+
path.lineTo(sx + node_sdist, sy)
|
|
215
|
+
|
|
216
|
+
path.cubicTo(
|
|
217
|
+
QPointF(sx + node_sdist * curvature, sy),
|
|
218
|
+
QPointF(dx + node_edist * curvature, dy),
|
|
219
|
+
QPointF(dx + node_edist, dy)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
path.lineTo(dx + node_edist, dy)
|
|
223
|
+
|
|
224
|
+
path.lineTo(dx, dy)
|
|
225
|
+
|
|
226
|
+
return path
|