chartflow 0.1.2__tar.gz → 0.1.5__tar.gz
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.
- {chartflow-0.1.2 → chartflow-0.1.5}/PKG-INFO +1 -1
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/chart.py +2 -1
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/__init__.py +9 -0
- chartflow-0.1.5/chartflow/node/_connection_interface.py +196 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/_node_base.py +29 -2
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/_node_interface.py +3 -1
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/_node_ports.py +99 -30
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/connection_item.py +1877 -1413
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/enums.py +1 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/example.py +45 -6
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/node_canvas.py +118 -11
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/node_item.py +59 -3
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/popmenu.py +53 -3
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/port_item.py +24 -3
- chartflow-0.1.5/chartflow/node/qconnection_editor.py +280 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/test_qnode.py +516 -4
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow.egg-info/PKG-INFO +1 -1
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow.egg-info/SOURCES.txt +2 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/setup.py +1 -1
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/_verify_importexport.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/code_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/code_folder.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/completer.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/context_analyzer.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/line_number_area.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/minimap.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/python_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/qcompletion.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/syntax_highlighter.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/edit/test_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/_demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/flow.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/qchart.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/test/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/test/test_chart.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/flow/test_decorator_sync.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/decorators.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/demo_pyqt6.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/demo_tree.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/logic.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/meta.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/scheduler.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/stage.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/stdio.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/__init__.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_decorators.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_events.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_integration.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_logic.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_meta.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_runtime_param.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_scheduler.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test/test_stage.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test_behavior_events.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test_nested_parallel.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/test_stage_spec.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/fsm/utils.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/_node_interaction.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/_node_markers.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/_port_interface.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/arrow_item.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/color_utils.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/demo.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/demo_arrow_drag.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/demo_decorator.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/demo_ports.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/layout_utils.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/main_window.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/menu_bar.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/qnode_editor.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/selection_rect.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/style_panel.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/node/styles.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/showcase.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow/theme.py +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow.egg-info/dependency_links.txt +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow.egg-info/requires.txt +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/chartflow.egg-info/top_level.txt +0 -0
- {chartflow-0.1.2 → chartflow-0.1.5}/setup.cfg +0 -0
|
@@ -19,12 +19,16 @@ from chartflow.node.node_item import QNodeItem
|
|
|
19
19
|
from chartflow.node.port_item import QNodePort, QNodePortSpec
|
|
20
20
|
from chartflow.node._port_interface import IQNodePort
|
|
21
21
|
from chartflow.node.connection_item import ConnectionItem
|
|
22
|
+
from chartflow.node._connection_interface import IConnectionItem, IQConnectionEditor
|
|
22
23
|
from chartflow.node.arrow_item import ArrowItem
|
|
23
24
|
from chartflow.node.selection_rect import SelectionRect
|
|
24
25
|
from chartflow.node.style_panel import StylePanel
|
|
25
26
|
from chartflow.node.node_canvas import QNodeCanvas
|
|
26
27
|
from chartflow.node.main_window import NodeCanvasWindow
|
|
27
28
|
from chartflow.node.qnode_editor import QNodeEditor
|
|
29
|
+
from chartflow.node.qconnection_editor import (
|
|
30
|
+
QConnectionEditor, QConnectionNameEditor, openConnectionEditor,
|
|
31
|
+
)
|
|
28
32
|
from chartflow.node.popmenu import (
|
|
29
33
|
ICanvasPopMenu, QCanvasPopMenu,
|
|
30
34
|
INodePopMenu, QNodePopMenu,
|
|
@@ -50,6 +54,7 @@ __all__ = [
|
|
|
50
54
|
"QNodePortSpec",
|
|
51
55
|
"IQNodePort",
|
|
52
56
|
"ConnectionItem",
|
|
57
|
+
"IConnectionItem",
|
|
53
58
|
"ArrowItem",
|
|
54
59
|
"SelectionRect",
|
|
55
60
|
# UI Components
|
|
@@ -59,6 +64,10 @@ __all__ = [
|
|
|
59
64
|
"NodeCanvasWindow",
|
|
60
65
|
# Editor
|
|
61
66
|
"QNodeEditor",
|
|
67
|
+
"QConnectionEditor",
|
|
68
|
+
"QConnectionNameEditor",
|
|
69
|
+
"IQConnectionEditor",
|
|
70
|
+
"openConnectionEditor",
|
|
62
71
|
# Pop Menus
|
|
63
72
|
"ICanvasPopMenu", "QCanvasPopMenu",
|
|
64
73
|
"INodePopMenu", "QNodePopMenu",
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
连接项接口契约模块
|
|
3
|
+
|
|
4
|
+
定义 IConnectionItem(Protocol) 与 IQConnectionEditor(Protocol) ——
|
|
5
|
+
连接项(连线)及其编辑器的对外类型契约。
|
|
6
|
+
|
|
7
|
+
Protocol 仅用于静态类型检查与文档, 不参与运行时继承(不进入 MRO),
|
|
8
|
+
因此可安全地与 QGraphicsPathItem / QWidget 继承体系并存。
|
|
9
|
+
|
|
10
|
+
IConnectionItem 声明了 ConnectionItem 对外暴露的名称/数据/编辑器/
|
|
11
|
+
信号/序列化接口; IQConnectionEditor 声明了连线编辑器(见
|
|
12
|
+
qconnection_editor.QConnectionEditor)的对外接口, 与节点编辑器
|
|
13
|
+
QNodeEditor 的接口形态保持一致。
|
|
14
|
+
|
|
15
|
+
约定: 本模块只含类型声明, 不得有运行时依赖, 以避免循环导入。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
from PyQt6.QtCore import QPointF, pyqtSignal
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class IConnectionItem(Protocol):
|
|
25
|
+
"""连接项接口契约
|
|
26
|
+
|
|
27
|
+
声明一条连线(ConnectionItem)在 名称 / 编辑器 / 序列化 方面应具备的
|
|
28
|
+
对外属性与方法。几何/样式/箭头/端口锚定等既有接口不在此重复声明。
|
|
29
|
+
|
|
30
|
+
契约分组:
|
|
31
|
+
|
|
32
|
+
- **名称与数据**: ``name`` / ``data``
|
|
33
|
+
- **编辑器**: ``editorClass`` / ``editorEmbedded`` / ``isEditWindowOpen`` /
|
|
34
|
+
:meth:`setEditWindowOpen` / :meth:`closeEditor`
|
|
35
|
+
- **几何**: ``center``(连线中点, 编辑器/名称 label 的锚点)
|
|
36
|
+
- **信号**: ``editRequested`` / ``nameChanged``
|
|
37
|
+
- **序列化**: :meth:`toDict` / :meth:`fromDict`
|
|
38
|
+
|
|
39
|
+
所有坐标默认为 **场景坐标(scene coordinate)**, 几何单位为像素。
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# ---- 名称与数据 ----
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
"""连线名称(可读写)。
|
|
46
|
+
|
|
47
|
+
读取返回当前名称(可为空串); 赋值会更新连线中点的名称 label
|
|
48
|
+
(空串时隐藏)并发出 :attr:`nameChanged` 信号。名称会被
|
|
49
|
+
:meth:`toDict` / :meth:`fromDict` 序列化。
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@name.setter
|
|
54
|
+
def name(self, value: str) -> None: ...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def data(self) -> Dict[str, Any]:
|
|
58
|
+
"""连线自定义数据字典(只读引用, 内容可改)。
|
|
59
|
+
|
|
60
|
+
返回内部 ``_data`` 字典本身而非副本, 调用方可直接增删键值,
|
|
61
|
+
供自定义编辑器(如保存备注/参数)持久化内容使用。
|
|
62
|
+
会被 :meth:`toDict` / :meth:`fromDict` 序列化。
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
# ---- 编辑器 ----
|
|
67
|
+
@property
|
|
68
|
+
def editorClass(self) -> Optional[type]:
|
|
69
|
+
"""连线编辑器类(可读写)。
|
|
70
|
+
|
|
71
|
+
设为某个 ``QWidget`` 子类 ``type`` 时, 双击连线(或右键菜单
|
|
72
|
+
Edit Connection)会通过 :attr:`editRequested` 信号请求由该类创建
|
|
73
|
+
编辑面板; ``None`` 表示使用内置默认名称编辑器
|
|
74
|
+
(:class:`~chartflow.node.qconnection_editor.QConnectionNameEditor`)。
|
|
75
|
+
编辑器类本身不可序列化(与节点 editorClass 一致)。
|
|
76
|
+
"""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
@editorClass.setter
|
|
80
|
+
def editorClass(self, value: Optional[type]) -> None: ...
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def editorEmbedded(self) -> bool:
|
|
84
|
+
"""编辑器是否嵌入连线中点(可读写, 默认 ``True``)。
|
|
85
|
+
|
|
86
|
+
- ``True``: 编辑器经 ``QGraphicsProxyWidget`` 嵌入连线中点, 随连线
|
|
87
|
+
路径更新自动跟随;
|
|
88
|
+
- ``False``: 编辑器作为独立浮动窗口打开(与节点编辑器一致)。
|
|
89
|
+
|
|
90
|
+
该状态会被 :meth:`toDict` / :meth:`fromDict` 序列化(仅在
|
|
91
|
+
``False`` 时落盘, 避免污染默认导出)。
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@editorEmbedded.setter
|
|
96
|
+
def editorEmbedded(self, value: bool) -> None: ...
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def isEditWindowOpen(self) -> bool:
|
|
100
|
+
"""当前是否有编辑器窗口/嵌入面板打开(只读)。"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def setEditWindowOpen(self, open: bool) -> None:
|
|
104
|
+
"""切换编辑窗口打开状态, 进而控制中点名称 label 的高亮提示。
|
|
105
|
+
|
|
106
|
+
由编辑器(如 :class:`QConnectionEditor`)在 show/close 时自动驱动;
|
|
107
|
+
打开时名称 label 显示高亮底衬(类比节点的 📖 悬浮标记)。
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
open: ``True`` 表示编辑器已打开, ``False`` 已关闭。
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
def closeEditor(self) -> None:
|
|
115
|
+
"""关闭当前打开的编辑器并清理嵌入代理(无打开编辑器时为空操作)。
|
|
116
|
+
|
|
117
|
+
画布在删除连线(:meth:`NodeCanvas.removeConnection`)时调用,
|
|
118
|
+
防止编辑器窗口/嵌入代理悬挂。
|
|
119
|
+
"""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
# ---- 几何 ----
|
|
123
|
+
@property
|
|
124
|
+
def center(self) -> QPointF:
|
|
125
|
+
"""连线中点的 **场景坐标**(只读), 取自 ``path().pointAtPercent(0.5)``。
|
|
126
|
+
|
|
127
|
+
作为名称 label 与嵌入编辑器的锚点; 自环连线同样按路径 50% 处取值。
|
|
128
|
+
"""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
# ---- 信号(只读属性, 返回 pyqtSignal) ----
|
|
132
|
+
@property
|
|
133
|
+
def editRequested(self) -> pyqtSignal:
|
|
134
|
+
"""编辑请求信号(只读)。
|
|
135
|
+
|
|
136
|
+
双击连线或点击右键菜单 Edit Connection 时发射, 载荷为本连线实例;
|
|
137
|
+
画布默认连接到打开编辑器的处理器(无 editorClass 时打开内置名称
|
|
138
|
+
编辑器), 宿主可自行断开/接管。
|
|
139
|
+
"""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def nameChanged(self) -> pyqtSignal:
|
|
144
|
+
"""名称变更信号(只读)。名称被修改时发射, 载荷为 (连线, 新名称)。"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
# ---- 序列化 ----
|
|
148
|
+
def toDict(self) -> dict:
|
|
149
|
+
"""将连线的 名称/数据/编辑器布局 序列化为字典。
|
|
150
|
+
|
|
151
|
+
仅包含非默认键: ``name``(非空) / ``data``(非空) /
|
|
152
|
+
``editor_embedded``(仅 ``False``)。拓扑(source/target/port)与样式
|
|
153
|
+
由画布 export 与 styleToDict 负责, 不在此重复。
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
连线数据字典(深拷贝, 修改返回值不影响连线)。
|
|
157
|
+
"""
|
|
158
|
+
...
|
|
159
|
+
|
|
160
|
+
def fromDict(self, data: dict) -> None:
|
|
161
|
+
"""从字典恢复连线的 名称/数据/编辑器布局(增量合并, 忽略未知键)。
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
data: 由 :meth:`toDict` 产生(或同构)的字典。
|
|
165
|
+
"""
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@runtime_checkable
|
|
170
|
+
class IQConnectionEditor(Protocol):
|
|
171
|
+
"""连线编辑器接口契约
|
|
172
|
+
|
|
173
|
+
声明连线编辑器应具备的对外接口, 与节点编辑器
|
|
174
|
+
:class:`~chartflow.node.qnode_editor.QNodeEditor` 形态保持一致:
|
|
175
|
+
构造时传入被编辑对象, 提供 ``getTitle`` / ``save`` / ``load``。
|
|
176
|
+
|
|
177
|
+
具体实现由 :class:`~chartflow.node.qconnection_editor.QConnectionEditor`
|
|
178
|
+
承担; 用户自定义编辑器继承该类(或满足本契约)即可。
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def connection(self) -> IConnectionItem:
|
|
183
|
+
"""获取当前编辑的连线(只读)。"""
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
def getTitle(self) -> str:
|
|
187
|
+
"""获取编辑器窗口标题(嵌入模式下一般不可见)。"""
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
def save(self) -> bool:
|
|
191
|
+
"""保存编辑器内容到连线。返回是否成功。"""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
def load(self) -> bool:
|
|
195
|
+
"""从连线加载内容到编辑器。返回是否成功。"""
|
|
196
|
+
...
|
|
@@ -398,7 +398,10 @@ class _NodeCore(QGraphicsEllipseItem):
|
|
|
398
398
|
|
|
399
399
|
- 名称需容纳在内部区域(INNER_RATIO 以内); 有 hit 时额外预留
|
|
400
400
|
(dot 深度 + 间距 + hit 文本宽度), 避免 hit 与名称重叠。
|
|
401
|
-
- 矩形且有端口时: 宽度由名称(+hit)决定, 高度由最多端口的一侧决定
|
|
401
|
+
- 矩形且有端口时: 宽度由名称(+hit)决定, 高度由最多端口的一侧决定
|
|
402
|
+
(可长宽不等, 但宽不短于高——顶/底边不短于左右边)。
|
|
403
|
+
- 六边形: 平顶/平底边长(=rw)须不短于斜边长 hypot(rw/2, rh),
|
|
404
|
+
即 rw >= rh * 2/√3, 按此加宽(名称短时正方形不满足该约束)。
|
|
402
405
|
- 其余(椭圆/菱形/无端口矩形): 保持方形。
|
|
403
406
|
"""
|
|
404
407
|
tw, th = self._nameTextSize()
|
|
@@ -411,6 +414,8 @@ class _NodeCore(QGraphicsEllipseItem):
|
|
|
411
414
|
port_extent = self._portInwardExtent()
|
|
412
415
|
rw = max(rw_name, name_half + port_extent + NAME_INNER_PAD, self._min_radius)
|
|
413
416
|
rh = max(rh_name, self._halfHeightForPorts(rw), self._min_radius)
|
|
417
|
+
# 矩形: 顶/底边(宽)不短于左右边(高); 名称短/端口多导致超高时加宽
|
|
418
|
+
rw = max(rw, rh)
|
|
414
419
|
return rw, rh
|
|
415
420
|
|
|
416
421
|
# 方形
|
|
@@ -421,6 +426,10 @@ class _NodeCore(QGraphicsEllipseItem):
|
|
|
421
426
|
R = max(R, self._circlePortRadius())
|
|
422
427
|
else:
|
|
423
428
|
R = max(R, self._halfHeightForPorts(R))
|
|
429
|
+
if self._shape == NodeShape.HEXAGON:
|
|
430
|
+
# 六边形: 顶/底边 >= 斜边 -> rw >= rh * 2/√3; 加宽而非压扁,
|
|
431
|
+
# 以保留端口/名称的纵向空间
|
|
432
|
+
return R * 2.0 / math.sqrt(3.0), R
|
|
424
433
|
return R, R
|
|
425
434
|
|
|
426
435
|
def _setSize(self, rw: float, rh: float) -> None:
|
|
@@ -429,6 +438,12 @@ class _NodeCore(QGraphicsEllipseItem):
|
|
|
429
438
|
# 圆形节点固定为标准圆(宽高相等), 保证端口沿弧线布局正确
|
|
430
439
|
side = max(rw, rh)
|
|
431
440
|
rw = rh = side
|
|
441
|
+
elif self._shape == NodeShape.HEXAGON:
|
|
442
|
+
# 六边形: 顶/底边 >= 斜边 -> rw >= rh * 2/√3(兜底, 含自定义半径路径)
|
|
443
|
+
rw = max(rw, rh * 2.0 / math.sqrt(3.0))
|
|
444
|
+
elif self._shape == NodeShape.RECTANGLE:
|
|
445
|
+
# 矩形: 顶/底边(宽)不短于左右边(高)(兜底)
|
|
446
|
+
rw = max(rw, rh)
|
|
432
447
|
self._rw = rw
|
|
433
448
|
self._rh = rh
|
|
434
449
|
self._radius = rw # 兼容椭圆/菱形及旧代码
|
|
@@ -475,6 +490,7 @@ class _NodeCore(QGraphicsEllipseItem):
|
|
|
475
490
|
- ELLIPSE: 圆(半径 + 外环)
|
|
476
491
|
- RECTANGLE: 圆角矩形(外扩外环)
|
|
477
492
|
- DIAMOND: 菱形(四顶点外扩外环)
|
|
493
|
+
- HEXAGON: 六边形(六顶点外扩外环)
|
|
478
494
|
"""
|
|
479
495
|
rect = self.rect()
|
|
480
496
|
m = self._hitShapeMargin()
|
|
@@ -488,7 +504,7 @@ class _NodeCore(QGraphicsEllipseItem):
|
|
|
488
504
|
# 圆角半径与 paint() 一致(基于原始 rect, 否则圆角位置错位)
|
|
489
505
|
corner = min(rect.width(), rect.height()) * 0.2
|
|
490
506
|
path.addRoundedRect(outer, corner, corner)
|
|
491
|
-
|
|
507
|
+
elif self._shape == NodeShape.DIAMOND:
|
|
492
508
|
c = rect.center()
|
|
493
509
|
hw = rect.width() / 2.0 + m
|
|
494
510
|
hh = rect.height() / 2.0 + m
|
|
@@ -497,6 +513,17 @@ class _NodeCore(QGraphicsEllipseItem):
|
|
|
497
513
|
path.lineTo(c.x(), c.y() + hh)
|
|
498
514
|
path.lineTo(c.x() - hw, c.y())
|
|
499
515
|
path.closeSubpath()
|
|
516
|
+
else: # HEXAGON
|
|
517
|
+
c = rect.center()
|
|
518
|
+
hw = rect.width() / 2.0 + m
|
|
519
|
+
hh = rect.height() / 2.0 + m
|
|
520
|
+
path.moveTo(c.x() - hw / 2.0, c.y() - hh)
|
|
521
|
+
path.lineTo(c.x() + hw / 2.0, c.y() - hh)
|
|
522
|
+
path.lineTo(c.x() + hw, c.y())
|
|
523
|
+
path.lineTo(c.x() + hw / 2.0, c.y() + hh)
|
|
524
|
+
path.lineTo(c.x() - hw / 2.0, c.y() + hh)
|
|
525
|
+
path.lineTo(c.x() - hw, c.y())
|
|
526
|
+
path.closeSubpath()
|
|
500
527
|
return path
|
|
501
528
|
|
|
502
529
|
def _centerText(self) -> None:
|
|
@@ -78,6 +78,7 @@ class INodeItem(Protocol):
|
|
|
78
78
|
- ``ELLIPSE`` 椭圆/圆形(强制为宽高相等的标准圆)
|
|
79
79
|
- ``RECTANGLE`` 圆角矩形
|
|
80
80
|
- ``DIAMOND`` 菱形
|
|
81
|
+
- ``HEXAGON`` 六边形(平顶/平底, 左右尖角)
|
|
81
82
|
|
|
82
83
|
赋值后若未设置自定义半径, 会按新形状重算尺寸; 切换到圆形时
|
|
83
84
|
端口会从轴向排布切换为沿侧弧径向排布。
|
|
@@ -396,7 +397,8 @@ class INodeItem(Protocol):
|
|
|
396
397
|
"""计算从节点中心指向 ``target_pos`` 方向、落在节点外轮廓上的交点。
|
|
397
398
|
|
|
398
399
|
依据当前 :attr:`nodeShape` 采用不同算法: 圆形用三角函数、
|
|
399
|
-
圆角矩形先求外接矩形交点再修正圆角、菱形用 L1
|
|
400
|
+
圆角矩形先求外接矩形交点再修正圆角、菱形用 L1 范数缩放、
|
|
401
|
+
六边形取平顶边与斜边两组约束的较小射线交点。
|
|
400
402
|
|
|
401
403
|
Args:
|
|
402
404
|
target_pos: 目标位置(场景坐标)。
|
|
@@ -56,6 +56,11 @@ class _NodePortMixin:
|
|
|
56
56
|
self._port_label_timer.setSingleShot(True)
|
|
57
57
|
self._port_label_timer.setInterval(PORT_LABEL_HOVER_DELAY_MS)
|
|
58
58
|
self._port_label_timer.timeout.connect(self._showAllPortLabels)
|
|
59
|
+
# label 钉住态(始终可见)与闪现计时器(flashPortLabels 用)
|
|
60
|
+
self._port_labels_pinned: bool = False
|
|
61
|
+
self._port_flash_timer: QTimer = QTimer()
|
|
62
|
+
self._port_flash_timer.setSingleShot(True)
|
|
63
|
+
self._port_flash_timer.timeout.connect(self._unflashPortLabels)
|
|
59
64
|
|
|
60
65
|
# ------------------------------------------------------------------ #
|
|
61
66
|
# ports 属性
|
|
@@ -126,6 +131,50 @@ class _NodePortMixin:
|
|
|
126
131
|
result[key] = ports
|
|
127
132
|
return result
|
|
128
133
|
|
|
134
|
+
# ------------------------------------------------------------------ #
|
|
135
|
+
# 端口 label 显隐(钉住 / 闪现)
|
|
136
|
+
# ------------------------------------------------------------------ #
|
|
137
|
+
@property
|
|
138
|
+
def portLabelsVisible(self) -> bool:
|
|
139
|
+
"""端口 label 是否始终可见(读写)。
|
|
140
|
+
|
|
141
|
+
True 时全部端口 label 钉住常显(优先于 hover 规则); False 时交还
|
|
142
|
+
hover 规则(节点 hover>0.5s 显示全部 / hover 单个 dot 显示该条)。
|
|
143
|
+
对重建端口(ports 重赋值)后新建的端口项同样生效。
|
|
144
|
+
"""
|
|
145
|
+
return self._port_labels_pinned
|
|
146
|
+
|
|
147
|
+
@portLabelsVisible.setter
|
|
148
|
+
def portLabelsVisible(self, on: bool) -> None:
|
|
149
|
+
self._port_labels_pinned = bool(on)
|
|
150
|
+
self._setAllPortsLabelPinned(on)
|
|
151
|
+
|
|
152
|
+
def _setAllPortsLabelPinned(self, on: bool) -> None:
|
|
153
|
+
for side in ("left", "right"):
|
|
154
|
+
for item in self._port_items[side]:
|
|
155
|
+
item.setLabelPinned(on)
|
|
156
|
+
|
|
157
|
+
def flashPortLabels(self, continue_time: int = 1500) -> None:
|
|
158
|
+
"""模拟弹出全部端口 label 并持续 ``continue_time`` 毫秒, 到期自动收回。
|
|
159
|
+
|
|
160
|
+
重复调用会重新计时。与 hover 规则正交: 闪现收回后 label 回到
|
|
161
|
+
hover/钉住所决定的可见态。continue_time <= 0 时立即收回。
|
|
162
|
+
"""
|
|
163
|
+
for side in ("left", "right"):
|
|
164
|
+
for item in self._port_items[side]:
|
|
165
|
+
item.setLabelFlashed(True)
|
|
166
|
+
if continue_time <= 0:
|
|
167
|
+
self._unflashPortLabels()
|
|
168
|
+
return
|
|
169
|
+
self._port_flash_timer.start(max(0, int(continue_time)))
|
|
170
|
+
|
|
171
|
+
def _unflashPortLabels(self) -> None:
|
|
172
|
+
"""闪现到期: 解除全部端口的闪现态(label 回到 hover/钉住规则)。"""
|
|
173
|
+
self._port_flash_timer.stop()
|
|
174
|
+
for side in ("left", "right"):
|
|
175
|
+
for item in self._port_items[side]:
|
|
176
|
+
item.setLabelFlashed(False)
|
|
177
|
+
|
|
129
178
|
# ------------------------------------------------------------------ #
|
|
130
179
|
# 端口渲染项构建/移除
|
|
131
180
|
# ------------------------------------------------------------------ #
|
|
@@ -142,6 +191,7 @@ class _NodePortMixin:
|
|
|
142
191
|
for side in ("left", "right"):
|
|
143
192
|
for spec in self._ports_spec.get(side, []):
|
|
144
193
|
item = QNodePort(spec, side, self)
|
|
194
|
+
item.setLabelPinned(self._port_labels_pinned) # 继承钉住态
|
|
145
195
|
self._port_items[side].append(item)
|
|
146
196
|
self._layoutPorts()
|
|
147
197
|
|
|
@@ -163,21 +213,22 @@ class _NodePortMixin:
|
|
|
163
213
|
# 端口布局
|
|
164
214
|
# ------------------------------------------------------------------ #
|
|
165
215
|
def _layoutPorts(self) -> None:
|
|
166
|
-
"""分布端口: 圆形沿侧弧径向;
|
|
216
|
+
"""分布端口: 圆形沿侧弧径向; 菱形/六边形沿左/右斜边贴合排布; 矩形沿边缘轴向。"""
|
|
167
217
|
is_circle = self._shape == NodeShape.ELLIPSE
|
|
168
|
-
|
|
218
|
+
# 菱形与六边形(左右均为两条斜边)共用斜边贴合布局
|
|
219
|
+
is_slant = self._shape in (NodeShape.DIAMOND, NodeShape.HEXAGON)
|
|
169
220
|
for side in ("left", "right"):
|
|
170
221
|
for it in self._port_items[side]:
|
|
171
|
-
# 互斥的排布模式: _radial(圆形) / _diamond(
|
|
222
|
+
# 互斥的排布模式: _radial(圆形) / _diamond(斜边) / 皆 False(矩形)
|
|
172
223
|
if it._radial != is_circle:
|
|
173
224
|
it._radial = is_circle
|
|
174
225
|
it.update()
|
|
175
|
-
if it._diamond !=
|
|
176
|
-
it._diamond =
|
|
226
|
+
if it._diamond != is_slant:
|
|
227
|
+
it._diamond = is_slant
|
|
177
228
|
it.update()
|
|
178
229
|
if is_circle:
|
|
179
230
|
self._layoutPortsOnCircle()
|
|
180
|
-
elif
|
|
231
|
+
elif is_slant:
|
|
181
232
|
self._layoutPortsOnDiamond()
|
|
182
233
|
else:
|
|
183
234
|
self._layoutPortsAxisAligned()
|
|
@@ -227,16 +278,29 @@ class _NodePortMixin:
|
|
|
227
278
|
it.setPos(QPointF(x, y))
|
|
228
279
|
it.update()
|
|
229
280
|
|
|
281
|
+
def _slantInsetX(self) -> float:
|
|
282
|
+
"""斜边上端点(顶/底侧)相对中心 x 的内缩量。
|
|
283
|
+
|
|
284
|
+
菱形: 0(斜边直达中线上的顶/底尖角);
|
|
285
|
+
六边形: half_w / 2(斜边从侧尖角连到平顶/平底边的端点)。
|
|
286
|
+
"""
|
|
287
|
+
if self._shape == NodeShape.HEXAGON:
|
|
288
|
+
return self.rect().width() / 4.0
|
|
289
|
+
return 0.0
|
|
290
|
+
|
|
230
291
|
def _layoutPortsOnDiamond(self) -> None:
|
|
231
|
-
"""
|
|
292
|
+
"""菱形/六边形: 端口沿左/右两条斜边贴合排布, dot 旋转使底边贴合斜边、
|
|
232
293
|
apex 沿斜边内法线(指向节点内部), 出射方向沿斜边外法线。
|
|
233
294
|
|
|
234
295
|
每侧(right/left)对应两条斜边(上、下), 总弧长 2L(L=单条斜边长)。
|
|
235
296
|
端口沿弧长以 PORT_MIN_GAP 紧凑排布, 整组围绕侧顶点(右顶点/左顶点,
|
|
236
297
|
即赤道)居中。**所有四个顶点(top/side/bottom 及对侧顶点)附近都预留
|
|
237
|
-
顶点间隙**, 故端口自然落在斜边中段,
|
|
298
|
+
顶点间隙**, 故端口自然落在斜边中段, 不压在任何尖角上。
|
|
299
|
+
|
|
300
|
+
斜边端点由 _slantInsetX() 参数化: 菱形斜边连到中线的顶/底尖角,
|
|
301
|
+
六边形斜边连到平顶/平底边的端点(x=±hw/2), 布局算法完全一致。
|
|
238
302
|
|
|
239
|
-
注意:
|
|
303
|
+
注意: 斜边的内法线是**该斜边的常量法线**(垂直于斜边), 而非"指向
|
|
240
304
|
圆心"的方向 —— 二者仅在斜边中点重合。dot 底边须平行于斜边, 故
|
|
241
305
|
_orient_angle 必须取斜边内法线方向(使旋转后底边贴合斜边)。
|
|
242
306
|
"""
|
|
@@ -246,20 +310,22 @@ class _NodePortMixin:
|
|
|
246
310
|
hh = rect.height() / 2.0
|
|
247
311
|
if hw <= 0 or hh <= 0:
|
|
248
312
|
return
|
|
249
|
-
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
313
|
+
kx = self._slantInsetX()
|
|
314
|
+
sw = hw - kx # 斜边的水平跨度(菱形=hw, 六边形=hw/2)
|
|
315
|
+
L = math.hypot(sw, hh) # 单条斜边长
|
|
316
|
+
# 斜边内法线(指向节点内部, 垂直于斜边)。四条斜边, 内法线方向固定。
|
|
317
|
+
# 右上(TR: top->right) 斜边方向 (sw, hh), 内法线 (-hh, sw)
|
|
318
|
+
# 右下(RB: right->bottom) 方向 (-sw, hh), 内法线 (-hh, -sw)
|
|
319
|
+
# 左下(BL: bottom->left) 方向 (-sw, -hh), 内法线 (hh, -sw)
|
|
320
|
+
# 左上(LT: left->top) 方向 (sw, -hh), 内法线 (hh, sw)
|
|
255
321
|
def inward_normal(ix, iy):
|
|
256
322
|
nl = math.hypot(ix, iy)
|
|
257
323
|
return (ix / nl, iy / nl) if nl > 1e-6 else (0.0, 0.0)
|
|
258
324
|
|
|
259
|
-
NORM_TR = inward_normal(-hh,
|
|
260
|
-
NORM_RB = inward_normal(-hh, -
|
|
261
|
-
NORM_BL = inward_normal(hh, -
|
|
262
|
-
NORM_LT = inward_normal(hh,
|
|
325
|
+
NORM_TR = inward_normal(-hh, sw) # 右上斜边内法线
|
|
326
|
+
NORM_RB = inward_normal(-hh, -sw) # 右下斜边内法线
|
|
327
|
+
NORM_BL = inward_normal(hh, -sw) # 左下斜边内法线
|
|
328
|
+
NORM_LT = inward_normal(hh, sw) # 左上斜边内法线
|
|
263
329
|
|
|
264
330
|
# 四个顶点处的弧长预留(像素, 沿弧长方向): 避免端口压在任何菱角上。
|
|
265
331
|
# top/bottom 顶点处一条斜边交汇, side 顶点处两条斜边交汇, 均需避让。
|
|
@@ -316,30 +382,31 @@ class _NodePortMixin:
|
|
|
316
382
|
s_values.append(L + vertex_gap + off_c) # 下半中段内
|
|
317
383
|
for it, s in zip(items, s_values):
|
|
318
384
|
# s ∈ [0, 2L]; 上半斜边 s∈[0,L](top→侧顶点), 下半 s∈[L,2L](侧顶点→bottom)
|
|
385
|
+
# 斜边端点: 侧尖角(±hw,0) <-> 顶/底端点(±kx,∓hh); kx=0 即菱形
|
|
319
386
|
if side == "right":
|
|
320
387
|
if s <= L:
|
|
321
|
-
# 上半(TR):
|
|
388
|
+
# 上半(TR): (kx,-hh) -> right(+hw,0), t=s/L
|
|
322
389
|
t = max(0.0, min(1.0, s / L))
|
|
323
|
-
px = cx +
|
|
390
|
+
px = cx + kx + sw * t
|
|
324
391
|
py = cy + (-hh) + hh * t
|
|
325
392
|
nx, ny = NORM_TR
|
|
326
393
|
else:
|
|
327
|
-
# 下半(RB): right(+hw,0) ->
|
|
394
|
+
# 下半(RB): right(+hw,0) -> (kx,+hh), t=(s-L)/L
|
|
328
395
|
t = max(0.0, min(1.0, (s - L) / L))
|
|
329
|
-
px = cx + hw
|
|
396
|
+
px = cx + hw - sw * t
|
|
330
397
|
py = cy + hh * t
|
|
331
398
|
nx, ny = NORM_RB
|
|
332
399
|
else: # left
|
|
333
400
|
if s <= L:
|
|
334
|
-
# 上半(LT):
|
|
401
|
+
# 上半(LT): (-kx,-hh) -> left(-hw,0), t=s/L
|
|
335
402
|
t = max(0.0, min(1.0, s / L))
|
|
336
|
-
px = cx
|
|
403
|
+
px = cx - kx - sw * t
|
|
337
404
|
py = cy + (-hh) + hh * t
|
|
338
405
|
nx, ny = NORM_LT
|
|
339
406
|
else:
|
|
340
|
-
# 下半(BL): left(-hw,0) ->
|
|
407
|
+
# 下半(BL): left(-hw,0) -> (-kx,+hh), t=(s-L)/L
|
|
341
408
|
t = max(0.0, min(1.0, (s - L) / L))
|
|
342
|
-
px = cx
|
|
409
|
+
px = cx - hw + sw * t
|
|
343
410
|
py = cy + hh * t
|
|
344
411
|
nx, ny = NORM_BL
|
|
345
412
|
it._orient_angle = math.degrees(math.atan2(ny, nx))
|
|
@@ -347,15 +414,15 @@ class _NodePortMixin:
|
|
|
347
414
|
it.update()
|
|
348
415
|
|
|
349
416
|
def _diamondSideBias(self) -> float:
|
|
350
|
-
"""
|
|
417
|
+
"""菱形/六边形每条斜边上端口组的"逻辑中点"参数(0=远端顶点, 1=侧顶点)。
|
|
351
418
|
|
|
352
419
|
取 0.7: 端口组偏向侧顶点(左右两角), 体现"左右两侧"的视觉聚类,
|
|
353
|
-
同时仍处于斜边中段(避开顶点预留区),
|
|
420
|
+
同时仍处于斜边中段(避开顶点预留区), 不会压在任何尖角上。
|
|
354
421
|
"""
|
|
355
422
|
return 0.7
|
|
356
423
|
|
|
357
424
|
def _diamondVertexGap(self) -> float:
|
|
358
|
-
"""
|
|
425
|
+
"""菱形/六边形每个顶点处沿弧长预留的间隙(像素)。
|
|
359
426
|
|
|
360
427
|
四个顶点(top/底/左/右)处斜边交汇, 法线方向不连续, 端口压在顶点上
|
|
361
428
|
视觉突兀。预留固定像素间隙使端口落在斜边中段。取 10px: 对常见尺寸
|
|
@@ -372,6 +439,8 @@ class _NodePortMixin:
|
|
|
372
439
|
return corner + 4.0
|
|
373
440
|
if self._shape == NodeShape.DIAMOND:
|
|
374
441
|
return half_h * 0.30
|
|
442
|
+
if self._shape == NodeShape.HEXAGON:
|
|
443
|
+
return half_h * 0.25
|
|
375
444
|
# ELLIPSE
|
|
376
445
|
return half_h * 0.22
|
|
377
446
|
|