generated from hulk/gd32f4xx_template_cmake_vscode
GUI v0.2.13: chart Y-axis range control (right-edge dual-handle slider)
- TemperatureChart gains a right-edge slider on a FIXED -20..120C rail (user-specified): top handle = upper bound, bottom handle = lower bound, drag middle band = pan keeping span, click free rail = snap that bound. First drag engages manual mode (yManualEngaged -> sync button state + config); release quantizes to 0.1C via yRangeChanged, min span 0.5C. Bound values during drag must come from live display range, not the stale paint cache (two drags without repaint made pan jump the range). - Auto/manual toggle moved into the card header (was a wrapped second row); pause/clear merged into header too; legend moved to top-left. - Value labels next to handles only visible on hover/drag (constant labels covered the curve per user feedback). - Out-of-range curves clipped back to the plot area (setClipRect); current-value marker suppressed outside the manual range. - Draft textbox rows from v0.2.10 superseded; versions v0.2.10..v0.2.13. - Fixed "no data yet" hint never disappearing: set_source cached a stale _has_data bool once at build time while data flows through the same list references; now evaluated live at paint time. - config: y_axis key persists mode/bounds; invalid stored values fall back to auto. selftest settings isolated to a temp dir (user json with stored manual state broke the fresh-default assertions). - selftest_gui: slider hit/drag/clamp/pan/clipping regression + hint gating; 83 checks all pass.
This commit is contained in:
@@ -8,6 +8,7 @@ import math
|
||||
import os
|
||||
from collections import deque
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
@@ -15,9 +16,16 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import filmforge_rs485 as ff
|
||||
import config
|
||||
from PySide6.QtCore import QRectF
|
||||
from PySide6.QtGui import QImage, QPainter
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
# 设置文件指向临时目录: 隔离用户真实 filmforge_gui.json(用户可能展存着手动
|
||||
# Y 轴等状态), 测试一律从"首次启动默认"出发, 残留写入不留真实配置盘
|
||||
_tmpdir = tempfile.mkdtemp(prefix='filmforge_selftest_')
|
||||
config.SETTINGS_FILE = os.path.join(_tmpdir, 'settings.json')
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
@@ -73,6 +81,18 @@ def count_pixels(img, rgb, tol=30):
|
||||
return n
|
||||
|
||||
|
||||
def count_in(img, x0, x1, y0, y1, rgb, tol=30):
|
||||
"""区域内逐像素计数(stride 1): 用于条带级断言(如越界裁剪)与空态提示"""
|
||||
n = 0
|
||||
for y in range(y0, y1):
|
||||
for x in range(x0, x1):
|
||||
c = img.pixelColor(x, y)
|
||||
if (abs(c.red() - rgb[0]) < tol and abs(c.green() - rgb[1]) < tol
|
||||
and abs(c.blue() - rgb[2]) < tol):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
measured = (0, 200, 215) # Dark 主题实测曲线色 #00c8d7
|
||||
target = (232, 131, 58) # 目标线 #E8833A
|
||||
fault = (217, 83, 79) # 故障 #D9534F
|
||||
@@ -279,5 +299,126 @@ ts, _vs = c._visible(0, 99)
|
||||
check('实时切片完整', ts, [1.0, 2.0, 3.0, 4.0])
|
||||
c._samples_t, c._samples_v = window.samples_t, window.samples_v
|
||||
|
||||
# 8) v0.2.11 回归: 右缘双把手滑条拖拽量程 + 空态提示实时消失。
|
||||
# (提示不消失根因: 旧版 set_source 构建期缓存 _has_data 布尔, 数据经引用追加
|
||||
# 后永不刷新 → 改绘制时实时判定; 本段同时覆盖滑条命中/拖拽/钳位/平移/裁剪)
|
||||
c = window.chart
|
||||
check('Y 轴初始自动', c._y_mode, 'auto')
|
||||
check('Y 轴按钮默认自动', window.btn_ymode.isChecked()
|
||||
and window.btn_ymode.text() == 'Y轴自动', True)
|
||||
|
||||
engaged, ranges = [], []
|
||||
c.yManualEngaged.connect(lambda: engaged.append(1))
|
||||
c.yRangeChanged.connect(lambda lo, hi: ranges.append((lo, hi)))
|
||||
|
||||
c.update()
|
||||
app.processEvents() # 同步一次绘制: 命中缓存与当前数据一致
|
||||
area, _t0, _t1, _vt, _vv, _tgt, ymin0, ymax0 = c._layout()
|
||||
gx0, gx1 = c._gutter()
|
||||
ref0 = c._y_ref()
|
||||
y_hi_px = c._rail_px(area, ref0, ymax0)
|
||||
y_lo_px = c._rail_px(area, ref0, ymin0)
|
||||
check('把手命中(上把手)', c._gutter_hit(gx0 + 6, y_hi_px), 'max')
|
||||
check('把手命中(下把手)', c._gutter_hit(gx0 + 6, y_lo_px), 'min')
|
||||
check('轨道空白亦视为边界', c._gutter_hit(gx0 + 6, area.bottom() - 3), 'min')
|
||||
check('栏外不命中', c._gutter_hit(10, 100) is None, True)
|
||||
|
||||
c.start_y_drag('min', y_lo_px)
|
||||
check('拖拽即切手动', c._y_mode, 'manual')
|
||||
check('手动信号已发', engaged, [1])
|
||||
# 滑条域固定 -20~120 后拖拽映射恒定(0.53C/px 量级), 断言用目标值反算像素,
|
||||
# 不再依赖域宽: 拖到 ymin0+30 对应位置 → 下限抬升 30℃
|
||||
c.update_y_drag(c._rail_px(area, ref0, ymin0 + 30.0))
|
||||
lo1, hi1 = c._y_manual
|
||||
check('下限随拖拽抬升', lo1 > ymin0 + 1e-9, True)
|
||||
check('上限不受牵动', abs(hi1 - ymax0) < 1e-9, True)
|
||||
check('松手生效', c.end_y_drag(), True)
|
||||
lo_e, hi_e = ranges[-1]
|
||||
check('范围量化 0.1', abs(lo_e * 10 - round(lo_e * 10)) < 1e-6
|
||||
and abs(hi_e * 10 - round(hi_e * 10)) < 1e-6, True)
|
||||
|
||||
c.start_y_drag('min', y_lo_px) # 极端上拉 → 钳到 上限-最小间距
|
||||
c.update_y_drag(area.top() - 500)
|
||||
check('最小间距约束',
|
||||
c._y_manual[1] - c._y_manual[0] >= c.MIN_Y_SPAN - 1e-9, True)
|
||||
c.end_y_drag()
|
||||
|
||||
lo_prev, hi_prev = c._y_manual[0], c._y_manual[1] # 中带平移: 整段同移
|
||||
c.start_y_drag('win', (y_hi_px + y_lo_px) / 2)
|
||||
c.update_y_drag(area.bottom() + 30)
|
||||
c.end_y_drag()
|
||||
lo_pan, hi_pan = c._y_manual
|
||||
# 松手量化 0.1℃ 各自取整, 跨度允许 ±0.05 偏差(两段半界)
|
||||
check('平移不改跨度(量化容差)', abs((hi_pan - lo_pan) - (hi_prev - lo_prev)) < 0.12, True)
|
||||
check('平移已生效', lo_pan < lo_prev - 1e-9, True)
|
||||
check('上下限同步移动', abs((lo_prev - lo_pan) - (hi_prev - hi_pan)) < 0.12, True)
|
||||
check('范围通知已发 3 次', len(ranges), 3)
|
||||
|
||||
window.btn_ymode.setChecked(True) # 按钮勾回自动
|
||||
check('切回自动', c._y_mode, 'auto')
|
||||
check('按钮文本回自动', window.btn_ymode.text(), 'Y轴自动')
|
||||
|
||||
# 确定形状的像素断言: 手动 [28,80], 数据(~19~60℃)出界部分必被裁剪回绘图区
|
||||
c.set_y_manual(28.0, 80.0)
|
||||
_hx = c._colors['measured'].lstrip('#')
|
||||
measured_now = (int(_hx[0:2], 16), int(_hx[2:4], 16), int(_hx[4:6], 16))
|
||||
window.chart.update()
|
||||
img = QImage(window.chart.size(), QImage.Format_ARGB32)
|
||||
window.chart.render(img)
|
||||
area = QRectF(window.chart.rect()).adjusted(
|
||||
52, 10, -(app_fluent.RIGHT_MARGIN + app_fluent.GUTTER_W), -26)
|
||||
x0, x1 = int(area.left()) + 2, int(area.right()) - 2
|
||||
check('手动范围下曲线仍绘制', count_pixels(img, measured_now) > 0, True)
|
||||
check('裁剪: 上条带无曲线像素',
|
||||
count_in(img, x0, x1, 0, int(area.top()) - 1, measured_now), 0)
|
||||
check('裁剪: 下条带无曲线像素',
|
||||
count_in(img, x0, x1, int(area.bottom()), window.chart.height(), measured_now), 0)
|
||||
c.set_y_mode('auto')
|
||||
|
||||
hint = (201, 201, 201) # 提示文字色 #c9c9c9
|
||||
c2 = app_fluent.TemperatureChart()
|
||||
c2.resize(640, 320)
|
||||
lst_t, lst_v = [], []
|
||||
c2.set_source(lst_t, lst_v, [], []) # 模拟构建期一次性注入(空)
|
||||
img2 = QImage(c2.size(), QImage.Format_ARGB32)
|
||||
c2.render(img2)
|
||||
ax0, ax1 = 60, c2.width() - 20
|
||||
ax_top, ax_bot = 30, c2.height() - 30
|
||||
n_hint0 = count_in(img2, ax0, ax1, ax_top, ax_bot, hint)
|
||||
lst_t.append(time.time()) # 经同一引用追加数据(复现旧 bug 路径)
|
||||
lst_v.append(50.0)
|
||||
c2.update()
|
||||
img3 = QImage(c2.size(), QImage.Format_ARGB32)
|
||||
c2.render(img3)
|
||||
n_hint1 = count_in(img3, ax0, ax1, ax_top, ax_bot, hint)
|
||||
print(f'提示像素: 空={n_hint0} 有数据={n_hint1}')
|
||||
check('空态提示已绘出', n_hint0 > 50, True)
|
||||
check('数据到位后提示消失(实时判定)', n_hint1, 0)
|
||||
|
||||
# 8b) 值标签悬停门控: 非悬停不画(不常驻遮挡曲线, 用户标注), 悬停/拖拽才显示
|
||||
lt2, lv2 = [time.time() - 100], [35.0]
|
||||
c2.set_source(lt2, lv2, [], [])
|
||||
c2.set_y_manual(20.0, 60.0)
|
||||
c2.set_y_mode('manual')
|
||||
c2.update()
|
||||
img4 = QImage(c2.size(), QImage.Format_ARGB32)
|
||||
c2.render(img4)
|
||||
zx0, zx1 = c2.width() - 71, c2.width() - 31 # 把手值标签区(绘图区右缘)
|
||||
zy0, zy1 = 110, 230 # 覆盖上下两个把手 y 位
|
||||
n_pill0 = count_in(img4, zx0, zx1, zy0, zy1, (0, 200, 215))
|
||||
c2._set_y_hover(True)
|
||||
c2.update()
|
||||
img5 = QImage(c2.size(), QImage.Format_ARGB32)
|
||||
c2.render(img5)
|
||||
n_pill1 = count_in(img5, zx0, zx1, zy0, zy1, (0, 200, 215))
|
||||
print(f'值标签像素: 未悬停={n_pill0} 悬停={n_pill1}')
|
||||
check('未悬停时值标签隐藏(不遮曲线)', n_pill0, 0)
|
||||
check('悬停/拖拽时值标签显示', n_pill1 > 20, True)
|
||||
c2._set_y_hover(False)
|
||||
|
||||
# 还原模块配置, 避免测试残留写入用户 filmforge_gui.json
|
||||
config.Y_MODE, config.Y_MIN, config.Y_MAX = 'auto', None, None
|
||||
config.save_settings()
|
||||
|
||||
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
|
||||
sys.exit(1 if fails else 0)
|
||||
|
||||
Reference in New Issue
Block a user