chartflow 0.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.
- chartflow/__init__.py +28 -0
- chartflow/_verify_importexport.py +169 -0
- chartflow/edit/__init__.py +119 -0
- chartflow/edit/code_editor.py +1007 -0
- chartflow/edit/code_folder.py +523 -0
- chartflow/edit/completer.py +2652 -0
- chartflow/edit/context_analyzer.py +521 -0
- chartflow/edit/demo.py +469 -0
- chartflow/edit/line_number_area.py +548 -0
- chartflow/edit/minimap.py +581 -0
- chartflow/edit/python_editor.py +509 -0
- chartflow/edit/syntax_highlighter.py +291 -0
- chartflow/edit/test_editor.py +190 -0
- chartflow/flow/__init__.py +31 -0
- chartflow/flow/_demo.py +98 -0
- chartflow/flow/chart.py +1101 -0
- chartflow/flow/editor.py +375 -0
- chartflow/flow/flow.py +6 -0
- chartflow/flow/qchart.py +1250 -0
- chartflow/flow/test/__init__.py +0 -0
- chartflow/flow/test/test_chart.py +537 -0
- chartflow/flow/test_decorator_sync.py +102 -0
- chartflow/fsm/__init__.py +84 -0
- chartflow/fsm/decorators.py +395 -0
- chartflow/fsm/demo.py +381 -0
- chartflow/fsm/demo_pyqt6.py +881 -0
- chartflow/fsm/demo_tree.py +46 -0
- chartflow/fsm/logic.py +447 -0
- chartflow/fsm/meta.py +569 -0
- chartflow/fsm/scheduler.py +427 -0
- chartflow/fsm/stage.py +2079 -0
- chartflow/fsm/stdio.py +66 -0
- chartflow/fsm/test/__init__.py +7 -0
- chartflow/fsm/test/test_decorators.py +210 -0
- chartflow/fsm/test/test_events.py +202 -0
- chartflow/fsm/test/test_integration.py +596 -0
- chartflow/fsm/test/test_logic.py +371 -0
- chartflow/fsm/test/test_meta.py +192 -0
- chartflow/fsm/test/test_runtime_param.py +336 -0
- chartflow/fsm/test/test_scheduler.py +280 -0
- chartflow/fsm/test/test_stage.py +314 -0
- chartflow/fsm/test_behavior_events.py +263 -0
- chartflow/fsm/test_nested_parallel.py +80 -0
- chartflow/fsm/test_stage_spec.py +448 -0
- chartflow/fsm/utils.py +34 -0
- chartflow/node/__init__.py +66 -0
- chartflow/node/_node_base.py +727 -0
- chartflow/node/_node_interaction.py +488 -0
- chartflow/node/_node_interface.py +426 -0
- chartflow/node/_node_markers.py +648 -0
- chartflow/node/_node_ports.py +536 -0
- chartflow/node/_port_interface.py +111 -0
- chartflow/node/arrow_item.py +287 -0
- chartflow/node/color_utils.py +113 -0
- chartflow/node/connection_item.py +939 -0
- chartflow/node/demo.py +258 -0
- chartflow/node/demo_arrow_drag.py +46 -0
- chartflow/node/demo_decorator.py +63 -0
- chartflow/node/demo_ports.py +69 -0
- chartflow/node/enums.py +35 -0
- chartflow/node/example.py +84 -0
- chartflow/node/layout_utils.py +307 -0
- chartflow/node/main_window.py +196 -0
- chartflow/node/menu_bar.py +240 -0
- chartflow/node/node_canvas.py +1730 -0
- chartflow/node/node_item.py +754 -0
- chartflow/node/popmenu.py +472 -0
- chartflow/node/port_item.py +591 -0
- chartflow/node/qnode_editor.py +73 -0
- chartflow/node/selection_rect.py +53 -0
- chartflow/node/style_panel.py +615 -0
- chartflow/node/styles.py +63 -0
- chartflow/node/test_qnode.py +2336 -0
- chartflow/showcase.py +528 -0
- chartflow/theme.py +508 -0
- chartflow-0.1.dist-info/METADATA +23 -0
- chartflow-0.1.dist-info/RECORD +79 -0
- chartflow-0.1.dist-info/WHEEL +5 -0
- chartflow-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
箭头项模块 - 表示连接线上的独立箭头
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
from typing import Optional, TYPE_CHECKING, Callable
|
|
9
|
+
|
|
10
|
+
from PyQt6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsSceneMouseEvent
|
|
11
|
+
from PyQt6.QtCore import Qt, QPointF, QRectF
|
|
12
|
+
from PyQt6.QtGui import QPainterPath, QPen, QColor, QBrush, QPainterPathStroker
|
|
13
|
+
|
|
14
|
+
from chartflow.node.styles import ArrowStyle
|
|
15
|
+
from chartflow.node.enums import ArrowState
|
|
16
|
+
|
|
17
|
+
class ArrowItem(QGraphicsPathItem):
|
|
18
|
+
"""
|
|
19
|
+
箭头项 - 表示连接线上的独立箭头,可单独选中和拖拽
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
connection: 'ConnectionItem',
|
|
25
|
+
is_source_arrow: bool = False,
|
|
26
|
+
parent: Optional[QGraphicsItem] = None
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
初始化箭头项
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
connection: 所属的连接线
|
|
33
|
+
is_source_arrow: 是否是源端箭头(反向箭头)
|
|
34
|
+
parent: 父项
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(parent)
|
|
37
|
+
|
|
38
|
+
self._connection: 'ConnectionItem' = connection
|
|
39
|
+
self._is_source_arrow: bool = is_source_arrow
|
|
40
|
+
self._style: ArrowStyle = ArrowStyle()
|
|
41
|
+
self._state: ArrowState = ArrowState.IDLE
|
|
42
|
+
self._arrow_tip: QPointF = QPointF(0, 0)
|
|
43
|
+
self._arrow_direction: QPointF = QPointF(1, 0) # 箭头指向的方向
|
|
44
|
+
|
|
45
|
+
# 回调函数(由NodeCanvas设置)
|
|
46
|
+
self._drag_started_callback: Optional[Callable[[QPointF], None]] = None
|
|
47
|
+
self._drag_moved_callback: Optional[Callable[[QPointF], None]] = None
|
|
48
|
+
self._drag_finished_callback: Optional[Callable[[QPointF, bool], None]] = None
|
|
49
|
+
|
|
50
|
+
# 设置可选项标志
|
|
51
|
+
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
|
|
52
|
+
self.setAcceptHoverEvents(True)
|
|
53
|
+
self.setZValue(10) # 确保箭头在连线上方
|
|
54
|
+
|
|
55
|
+
# 初始化样式
|
|
56
|
+
self._setupStyle()
|
|
57
|
+
|
|
58
|
+
# 拖拽状态
|
|
59
|
+
self._is_dragging: bool = False
|
|
60
|
+
self._drag_start_pos: Optional[QPointF] = None
|
|
61
|
+
|
|
62
|
+
def _setupStyle(self) -> None:
|
|
63
|
+
"""设置箭头样式"""
|
|
64
|
+
self._normal_pen = QPen(self._style.normal_color, self._style.stroke_width)
|
|
65
|
+
self._normal_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
66
|
+
self._normal_pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
67
|
+
|
|
68
|
+
self._selected_pen = QPen(self._style.selected_color, self._style.stroke_width + 1)
|
|
69
|
+
self._selected_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
70
|
+
self._selected_pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
71
|
+
|
|
72
|
+
self._hover_pen = QPen(self._style.hover_color, self._style.stroke_width + 1)
|
|
73
|
+
self._hover_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
74
|
+
self._hover_pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
|
75
|
+
|
|
76
|
+
self.setPen(self._normal_pen)
|
|
77
|
+
self.setBrush(QBrush(self._style.normal_color))
|
|
78
|
+
|
|
79
|
+
def setArrowSize(self, size: float) -> None:
|
|
80
|
+
"""设置箭头大小"""
|
|
81
|
+
self._style.arrow_size = size
|
|
82
|
+
|
|
83
|
+
def setDragCallbacks(
|
|
84
|
+
self,
|
|
85
|
+
started: Optional[Callable[[QPointF], None]] = None,
|
|
86
|
+
moved: Optional[Callable[[QPointF], None]] = None,
|
|
87
|
+
finished: Optional[Callable[[QPointF, bool], None]] = None
|
|
88
|
+
) -> None:
|
|
89
|
+
"""设置拖拽回调函数"""
|
|
90
|
+
self._drag_started_callback = started
|
|
91
|
+
self._drag_moved_callback = moved
|
|
92
|
+
self._drag_finished_callback = finished
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def connection(self) -> 'ConnectionItem':
|
|
96
|
+
"""获取所属的连接线"""
|
|
97
|
+
return self._connection
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def isSourceArrow(self) -> bool:
|
|
101
|
+
"""是否是源端箭头"""
|
|
102
|
+
return self._is_source_arrow
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def isDragging(self) -> bool:
|
|
106
|
+
"""是否正在拖拽"""
|
|
107
|
+
return self._is_dragging
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def arrowTip(self) -> QPointF:
|
|
111
|
+
"""获取箭头尖端位置"""
|
|
112
|
+
return self._arrow_tip
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def arrowDirection(self) -> QPointF:
|
|
116
|
+
"""获取箭头指向的方向"""
|
|
117
|
+
return self._arrow_direction
|
|
118
|
+
|
|
119
|
+
def updateArrow(
|
|
120
|
+
self,
|
|
121
|
+
tip_pos: QPointF,
|
|
122
|
+
direction: QPointF,
|
|
123
|
+
from_pos: QPointF
|
|
124
|
+
) -> None:
|
|
125
|
+
"""
|
|
126
|
+
更新箭头形状和位置
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
tip_pos: 箭头尖端位置
|
|
130
|
+
direction: 箭头指向的方向(单位向量)
|
|
131
|
+
from_pos: 箭头的起始位置(用于计算角度)
|
|
132
|
+
"""
|
|
133
|
+
self._arrow_tip = tip_pos
|
|
134
|
+
self._arrow_direction = direction
|
|
135
|
+
|
|
136
|
+
# 计算箭头两个底点
|
|
137
|
+
arrow_angle = math.pi / 6 # 30度
|
|
138
|
+
arrow_size = self._style.arrow_size
|
|
139
|
+
|
|
140
|
+
# 根据方向向量计算垂直向量
|
|
141
|
+
dx = direction.x()
|
|
142
|
+
dy = direction.y()
|
|
143
|
+
|
|
144
|
+
# 计算两个底点
|
|
145
|
+
base1 = QPointF(
|
|
146
|
+
tip_pos.x() - arrow_size * (dx * math.cos(arrow_angle) - dy * math.sin(arrow_angle)),
|
|
147
|
+
tip_pos.y() - arrow_size * (dx * math.sin(arrow_angle) + dy * math.cos(arrow_angle))
|
|
148
|
+
)
|
|
149
|
+
base2 = QPointF(
|
|
150
|
+
tip_pos.x() - arrow_size * (dx * math.cos(-arrow_angle) - dy * math.sin(-arrow_angle)),
|
|
151
|
+
tip_pos.y() - arrow_size * (dx * math.sin(-arrow_angle) + dy * math.cos(-arrow_angle))
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# 创建箭头路径(三角形)
|
|
155
|
+
path = QPainterPath()
|
|
156
|
+
path.moveTo(tip_pos)
|
|
157
|
+
path.lineTo(base1)
|
|
158
|
+
path.lineTo(base2)
|
|
159
|
+
path.closeSubpath()
|
|
160
|
+
|
|
161
|
+
self.setPath(path)
|
|
162
|
+
self._applyStyle()
|
|
163
|
+
|
|
164
|
+
def _applyStyle(self) -> None:
|
|
165
|
+
"""应用当前状态的样式"""
|
|
166
|
+
if self._state == ArrowState.HOVER:
|
|
167
|
+
self.setPen(self._hover_pen)
|
|
168
|
+
self.setBrush(QBrush(self._style.hover_color))
|
|
169
|
+
elif self.isSelected():
|
|
170
|
+
self.setPen(self._selected_pen)
|
|
171
|
+
self.setBrush(QBrush(self._style.selected_color))
|
|
172
|
+
else:
|
|
173
|
+
self.setPen(self._normal_pen)
|
|
174
|
+
self.setBrush(QBrush(self._style.normal_color))
|
|
175
|
+
|
|
176
|
+
def boundingRect(self) -> QRectF:
|
|
177
|
+
"""获取边界矩形,添加一些边距以便更容易选中"""
|
|
178
|
+
rect = super().boundingRect()
|
|
179
|
+
margin = 8.0
|
|
180
|
+
return rect.adjusted(-margin, -margin, margin, margin)
|
|
181
|
+
|
|
182
|
+
def shape(self) -> QPainterPath:
|
|
183
|
+
"""获取形状,用于命中测试"""
|
|
184
|
+
path = super().shape()
|
|
185
|
+
# 扩大形状以便更容易选中
|
|
186
|
+
stroke = QPainterPathStroker()
|
|
187
|
+
stroke.setWidth(10)
|
|
188
|
+
return stroke.createStroke(path)
|
|
189
|
+
|
|
190
|
+
def hoverEnterEvent(self, event) -> None:
|
|
191
|
+
"""鼠标进入事件"""
|
|
192
|
+
self._state = ArrowState.HOVER
|
|
193
|
+
self._applyStyle()
|
|
194
|
+
super().hoverEnterEvent(event)
|
|
195
|
+
|
|
196
|
+
def hoverLeaveEvent(self, event) -> None:
|
|
197
|
+
"""鼠标离开事件"""
|
|
198
|
+
self._state = ArrowState.IDLE
|
|
199
|
+
self._applyStyle()
|
|
200
|
+
super().hoverLeaveEvent(event)
|
|
201
|
+
|
|
202
|
+
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
|
203
|
+
"""鼠标按下事件 - 开始拖拽"""
|
|
204
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
205
|
+
self._is_dragging = True
|
|
206
|
+
self._drag_start_pos = event.scenePos()
|
|
207
|
+
self.setSelected(True)
|
|
208
|
+
if self._drag_started_callback:
|
|
209
|
+
self._drag_started_callback(event.scenePos())
|
|
210
|
+
event.accept()
|
|
211
|
+
else:
|
|
212
|
+
super().mousePressEvent(event)
|
|
213
|
+
|
|
214
|
+
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
|
215
|
+
"""鼠标移动事件 - 拖拽中"""
|
|
216
|
+
if self._is_dragging:
|
|
217
|
+
if self._drag_moved_callback:
|
|
218
|
+
self._drag_moved_callback(event.scenePos())
|
|
219
|
+
event.accept()
|
|
220
|
+
else:
|
|
221
|
+
super().mouseMoveEvent(event)
|
|
222
|
+
|
|
223
|
+
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
|
224
|
+
"""鼠标释放事件 - 结束拖拽"""
|
|
225
|
+
if self._is_dragging and event.button() == Qt.MouseButton.LeftButton:
|
|
226
|
+
self._is_dragging = False
|
|
227
|
+
# 检查是否连接到了节点
|
|
228
|
+
scene = self.scene()
|
|
229
|
+
is_success = False
|
|
230
|
+
if scene:
|
|
231
|
+
# 获取该位置的所有项,查找节点(连接线和箭头可能覆盖在节点上)
|
|
232
|
+
items = scene.items(event.scenePos())
|
|
233
|
+
target_node = None
|
|
234
|
+
for item in items:
|
|
235
|
+
# 通过类名字符串避免循环导入(NodeItem/ConnectionItem)
|
|
236
|
+
_clsname = item.__class__.__name__
|
|
237
|
+
if _clsname == 'NodeItem':
|
|
238
|
+
target_node = item
|
|
239
|
+
break
|
|
240
|
+
# 如果是连接线,跳过继续查找
|
|
241
|
+
if _clsname == 'ConnectionItem':
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
is_success = target_node is not None
|
|
245
|
+
|
|
246
|
+
if self._drag_finished_callback:
|
|
247
|
+
self._drag_finished_callback(event.scenePos(), is_success)
|
|
248
|
+
|
|
249
|
+
event.accept()
|
|
250
|
+
else:
|
|
251
|
+
super().mouseReleaseEvent(event)
|
|
252
|
+
|
|
253
|
+
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value):
|
|
254
|
+
"""项状态改变事件"""
|
|
255
|
+
if change == QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
|
|
256
|
+
self._applyStyle()
|
|
257
|
+
return super().itemChange(change, value)
|
|
258
|
+
|
|
259
|
+
def setColor(self, color: QColor) -> None:
|
|
260
|
+
"""
|
|
261
|
+
设置箭头的主颜色(填充和描边)
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
color: 主颜色
|
|
265
|
+
"""
|
|
266
|
+
self._style.normal_color = color
|
|
267
|
+
self._style.selected_color = color
|
|
268
|
+
self._style.hover_color = color
|
|
269
|
+
self._setupStyle()
|
|
270
|
+
|
|
271
|
+
def setBorder(self, width: int = 1, color: QColor = None) -> None:
|
|
272
|
+
"""
|
|
273
|
+
设置箭头的边框宽度和颜色
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
width: 边框宽度,<=0 表示无边框
|
|
277
|
+
color: 边框颜色,None 表示使用默认颜色
|
|
278
|
+
"""
|
|
279
|
+
if width <= 0:
|
|
280
|
+
self.setPen(QPen(Qt.PenStyle.NoPen))
|
|
281
|
+
else:
|
|
282
|
+
self._style.stroke_width = width
|
|
283
|
+
if color is not None:
|
|
284
|
+
self._style.normal_color = color
|
|
285
|
+
self._style.selected_color = color
|
|
286
|
+
self._style.hover_color = color
|
|
287
|
+
self._setupStyle()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""颜色工具模块 - 连线渐变用的感知亮度调整
|
|
2
|
+
|
|
3
|
+
提供基于 WCAG 相对亮度(WCAG 2.x)的颜色调整辅助:
|
|
4
|
+
|
|
5
|
+
- :func:`relative_luminance`: 计算 QColor 的 WCAG 相对亮度(0~1)。
|
|
6
|
+
- :func:`color_at_relative_luminance`: 把一个颜色调整到指定相对亮度,
|
|
7
|
+
尽量保留色相与饱和度(均匀缩放 sRGB 线性通道, 再二分微调收敛)。
|
|
8
|
+
- :data:`PORT_GRADIENT_NEUTRAL_LUMINANCE`: 记录为常量的"当前连线灰度亮度",
|
|
9
|
+
取自默认连线灰 ``DEFAULT_CONNECTION_NORMAL = #8B93A1``。
|
|
10
|
+
|
|
11
|
+
连线渐变规则: 当某一端为 port 时, 该端颜色取 port.color(或所在侧默认色),
|
|
12
|
+
并用 :func:`color_at_relative_luminance` 调整到 ``PORT_GRADIENT_NEUTRAL_LUMINANCE``;
|
|
13
|
+
连线中点为同一亮度下的中性灰。两端点同亮度(中间 RGB 插值不严格保证)。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from PyQt6.QtGui import QColor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---- 常量: 当前连线灰度亮度(取自默认连线灰 #8B93A1 的 WCAG 相对亮度) ----
|
|
22
|
+
PORT_GRADIENT_NEUTRAL_LUMINANCE: float = 0.289296
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _srgb_to_linear(c: float) -> float:
|
|
26
|
+
"""sRGB 单通道(0~1) -> 线性光(0~1)。"""
|
|
27
|
+
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _linear_to_srgb(c: float) -> float:
|
|
31
|
+
"""线性光(0~1) -> sRGB 单通道(0~1)。"""
|
|
32
|
+
return c * 12.92 if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def relative_luminance(color: QColor) -> float:
|
|
36
|
+
"""计算 QColor 的 WCAG 相对亮度(0~1)。
|
|
37
|
+
|
|
38
|
+
公式: L = 0.2126*R_lin + 0.7152*G_lin + 0.0722*B_lin。
|
|
39
|
+
"""
|
|
40
|
+
r, g, b, _ = color.getRgb()
|
|
41
|
+
return (
|
|
42
|
+
0.2126 * _srgb_to_linear(r / 255.0)
|
|
43
|
+
+ 0.7152 * _srgb_to_linear(g / 255.0)
|
|
44
|
+
+ 0.0722 * _srgb_to_linear(b / 255.0)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def color_at_relative_luminance(color: QColor, target_lum: float) -> QColor:
|
|
49
|
+
"""把 ``color`` 调整到 WCAG 相对亮度 ``target_lum``, 尽量保留色相与饱和度。
|
|
50
|
+
|
|
51
|
+
做法:
|
|
52
|
+
1. 把 sRGB 三通道转到线性光;
|
|
53
|
+
2. 以 0 为中心均匀缩放三个线性通道(保持 RGB 比例 => 保留色相),
|
|
54
|
+
使相对亮度逼近目标;
|
|
55
|
+
3. 因 G 通道权重最大, 缩放后做少量二分微调(仅调 G 线性分量)收敛误差。
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
color: 原始颜色(QColor)。
|
|
59
|
+
target_lum: 目标相对亮度(0~1)。
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
调整后的 QColor(已 clamp 到合法范围)。输入为纯黑时回退到
|
|
63
|
+
与目标亮度等价的灰度。
|
|
64
|
+
"""
|
|
65
|
+
target_lum = max(0.0, min(1.0, float(target_lum)))
|
|
66
|
+
r, g, b, a = color.getRgb()
|
|
67
|
+
rl, gl, bl = _srgb_to_linear(r / 255.0), _srgb_to_linear(g / 255.0), _srgb_to_linear(b / 255.0)
|
|
68
|
+
|
|
69
|
+
cur_lum = 0.2126 * rl + 0.7152 * gl + 0.0722 * bl
|
|
70
|
+
if cur_lum <= 1e-9:
|
|
71
|
+
# 纯黑无方向信息: 回退为与目标亮度等价的灰度
|
|
72
|
+
grey_lin = target_lum # 灰度三通道相等 => rel_lum = (0.2126+0.7152+0.0722)*g = g
|
|
73
|
+
gs = _linear_to_srgb(grey_lin)
|
|
74
|
+
v = int(round(max(0.0, min(1.0, gs)) * 255.0))
|
|
75
|
+
return QColor(v, v, v, a)
|
|
76
|
+
|
|
77
|
+
# 1) 均匀缩放线性通道(保留色相)
|
|
78
|
+
scale = target_lum / cur_lum
|
|
79
|
+
rl2, gl2, bl2 = rl * scale, gl * scale, bl * scale
|
|
80
|
+
# clamp 线性分量到 [0,1] (sRGB 上限)
|
|
81
|
+
rl2, gl2, bl2 = (max(0.0, min(1.0, c)) for c in (rl2, gl2, bl2))
|
|
82
|
+
|
|
83
|
+
# 2) 二分微调: 仅调 G(权重最大), 收敛到 target_lum
|
|
84
|
+
def lum_of(rg, gg, bg):
|
|
85
|
+
return 0.2126 * rg + 0.7152 * gg + 0.0722 * bg
|
|
86
|
+
|
|
87
|
+
lo, hi = 0.0, 1.0
|
|
88
|
+
for _ in range(24):
|
|
89
|
+
mid = (lo + hi) / 2.0
|
|
90
|
+
if lum_of(rl2, mid, bl2) < target_lum:
|
|
91
|
+
lo = mid
|
|
92
|
+
else:
|
|
93
|
+
hi = mid
|
|
94
|
+
gl2 = (lo + hi) / 2.0
|
|
95
|
+
|
|
96
|
+
rs = _linear_to_srgb(rl2)
|
|
97
|
+
gs = _linear_to_srgb(gl2)
|
|
98
|
+
bs = _linear_to_srgb(bl2)
|
|
99
|
+
ri = int(round(max(0.0, min(1.0, rs)) * 255.0))
|
|
100
|
+
gi = int(round(max(0.0, min(1.0, gs)) * 255.0))
|
|
101
|
+
bi = int(round(max(0.0, min(1.0, bs)) * 255.0))
|
|
102
|
+
return QColor(ri, gi, bi, a)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def neutral_grey_at_relative_luminance(target_lum: float, alpha: int = 255) -> QColor:
|
|
106
|
+
"""返回与目标相对亮度等价的中性灰(R=G=B)。
|
|
107
|
+
|
|
108
|
+
灰度三通道相等时, 相对亮度 = (0.2126+0.7152+0.0722)*g = g, 故直接取 g=target_lum。
|
|
109
|
+
"""
|
|
110
|
+
g = max(0.0, min(1.0, float(target_lum)))
|
|
111
|
+
gs = _linear_to_srgb(g)
|
|
112
|
+
v = int(round(max(0.0, min(1.0, gs)) * 255.0))
|
|
113
|
+
return QColor(v, v, v, alpha)
|