一、滚动过渡动画卡顿(用户报告:甚至不如手动拖滚动条)—— 三处独立根因
1) WM_TIMER(kScrollTimerId) 无条件整窗失效 ⇒ WM_PAINT 看到更新区域不在绘图区内 ⇒ 把
m_chromeDirty 置真 ⇒ 每一帧都重绘标签栏/工具栏/面板。
修法:用「推进之前」的面板动画状态决定失效范围 —— 面板动画(滚动到目标信号 / 分组折起展开)
确实要改面板像素 ⇒ 整窗失效;只有绘图区动画(网格滚动到可视区 / 回到最上方)⇒ 只失效绘图区。
实测:绘图区动画帧 13.6ms → 3.5ms;手动拖滚动条参照 4.2ms。
⚠️ 未改变 §9.41 的回送结构(仍是 if (fullFrame) {整窗} else {绘图区})。
2) 图层预热不足:上百个离屏网格长期不建图层,长距离滚动动画扫进去时每一帧都有十几个网格
没有图层像素,只能靠「网格底衬兜底」重画(约 0.5ms/网格 ⇒ +8ms/帧)。
修法:LaneVisuallyRelevant(laneIndex, prefetchScreens) 增加 ±1 屏预取带(AnyVisibleLanePending 用),
并新增 PrefetchStepAllowed()(kPrefetchMinIntervalMs=32 ⇒ 约 31 次/秒)限速地继续请求重绘,
让整批图层在静置期间逐步建好。
实测:lane-only + VERY_BIG_DATA 帧 13.9ms → 5.3ms;signal-click + VERY_BIG_DATA 18.3ms → 10.3ms。
3) 网格滚动量原本参与「宽基底可用性」判据(上一轮已修:SceneContentHash(..., includeLaneScroll=false))。
二、光标吸附改用「放大自动点线」的同一判定(用户 2026-10-17 修订)
AutoPointLineNeeded 改名 MinorCellSparse(中性名,同一判定两处用途):任一小格内有效数据点 < 5 即为稀疏。
- 「放大自动切点线」与「光标吸附」共用它;判定完全不看绘制方式 ⇒ 即使用户手动改过显示样式也照样吸附。
- 鼠标下方网格内既有符合又有不符合的信号时,只有符合的那条的数据点参与吸附;没有符合的则自由移动。
- 删除旧的整屏判据 SignalSnapEnabled 与 kSnapVisibleCellsPerPoint(死代码)。
三、「回到最上方」按钮淡出与「浮动提示」一致
kBackToTopFadeOutMs = kHintFadeMs(500ms)+ 线性 alpha,到达顶部立即开始淡出(不停留)。
淡入保留 170ms ease-out。实测 50% 时刻 alpha = 0.500(线性),旧 ease-out 会是 0.875。
四、快速连点跟手(根因:CS_DBLCLKS 合成 WM_LBUTTONDBLCLK 导致按下丢失)
组件各自实现 DoubleClickIsPlainPress(x, y)(该落点有没有双击语义),宿主 WM_LBUTTONDBLCLK 据此
选择走 OnLButtonDown(当作按下)还是 OnDoubleClick:
- PlotView:浮动面板(含复选框/滑块/复位/✕/标题栏)、「回到最上方」按钮、绘图区滚动条、GX 徽标 ⇒ 按下;
时间光标线/读数标签/图例条目名/绘图区空白 ⇒ 保留双击。
- SignalPanel:样式区数值框、「目标」行名称与 GX 标签、行名称区域 ⇒ 保留双击;其余全部 ⇒ 按下。
- 弹出菜单 / 拾色器打开时,WM_LBUTTONDBLCLK 也按各自 OnLButtonDown 走。
五、分组行:名称恢复单击展开收起 + 只有快速双击进入改名
- OnLButtonDown 分组分支不再排除 RowPart::BODY:三角 /「组X」/ 名称本体统一折叠展开。
- OnDoubleClick 名称区域:只有快速双击(kGroupRenameDoubleClickMs=300,比系统 500ms 紧)才进入改名;
超阈值的「慢双击」只当又一次单击;快速双击前先撤销按下那一次的折叠切换,保证进编辑时状态与点击前一致。
六、信号重现时网格滚动回到最上方
“有过需滚动条的网格 → 没有任何信号显示 → 又出现需滚动条数量的网格”这一类情形必须从最上方开始看。
修法:Application::SynchronizeView 的「首次出现可见信号」分支里,在 FitAll() 之后 tab.LaneScroll() = 0。
验证(全部离屏,不占用鼠标/键盘/屏幕)
- grabdanim(.preflight/perf/gridanim_main.cpp)93/93:真实消息循环帧距 + 逐帧渲染耗时(与手动拖滚动条
对比)、光标吸附新判定、按钮淡出线性、快速连点路由、分组名单击/双击/慢双击、滚动位置复位。
- 既有回归:verify 49/49、panelanim 158/158(按新分组名规则更新 8 条断言)、datacheck 69/69、
edgecheck 46/46、errcheck 19/19。
- make release / make debug 零错误零警告(用 TARGET_FILE_NAME=TargetCheck/TargetCheckD 避免与正在运行的实例冲突)。
文档:Documents/agent_mem.md 新增 §9.43、§11.2 第 125–133 条与变更日志一行。
336 lines
14 KiB
C++
336 lines
14 KiB
C++
#ifndef __SIGNALPANEL_INTERNAL_HPP__
|
||
#define __SIGNALPANEL_INTERNAL_HPP__
|
||
|
||
// SignalPanel 组件内部共享的常量与工具函数(私有头,仅本组件的 .cpp 使用)。
|
||
//
|
||
// 本组件的实现按功能拆成多个协作文件,它们共用这一份内部工具集。
|
||
//
|
||
// ⚠️ 为让多个翻译单元都能使用,原匿名命名空间改为**具名**命名空间 `spdetail`,
|
||
// 并把其中每个函数与常量标为 `inline` —— 单一定义、单一副本,不会产生代码重复。
|
||
// 各实现文件在包含本头之后用 `using namespace spdetail;` 引入这些工具,
|
||
// 因此函数体里原有的非限定调用一行都不用改。
|
||
|
||
#include <windows.h>
|
||
#include <objidl.h>
|
||
#include <gdiplus.h>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <cstdlib>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include "SignalPanel.hpp"
|
||
#include "UiControls.hpp"
|
||
#include "Utf8Util.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace spdetail
|
||
{
|
||
/**
|
||
* @brief SignalPanel 内部工具集(常量与自由函数)。
|
||
*
|
||
* 全部符号都是 `inline` ⇒ 多个翻译单元共用同一个定义:既不重复符号,也不产生多份代码。
|
||
* 不要把它们移回匿名命名空间(那样每个 .cpp 都会各自复制一份)。
|
||
*/
|
||
inline const float kHeaderHeight = 38.0f; /**< 标题区高度(逻辑像素)。 */
|
||
inline const float kGroupRowHeight = 30.0f; /**< 分组行高。 */
|
||
inline const float kSignalRowHeight = 27.0f; /**< 信号行高。 */
|
||
inline const float kPadding = 12.0f; /**< 通用内边距。 */
|
||
inline const float kCheckSize = 15.0f; /**< 复选框尺寸。 */
|
||
inline const float kSwatchSize = 12.0f; /**< 信号行颜色小方块尺寸。 */
|
||
inline const float kBadgeWidth = 30.0f; /**< 网格徽标宽度。 */
|
||
inline const float kBadgeHeight = 17.0f; /**< 网格徽标高度。 */
|
||
inline const float kCorner = 4.0f; /**< 通用圆角半径。 */
|
||
inline const float kDisclosureWidth = 16.0f; /**< 折叠三角占位宽度。 */
|
||
inline const float kIndent = 14.0f; /**< 信号行相对分组行的缩进。 */
|
||
inline const float kScrollBarThickness = 8.0f; /**< 滚动条宽度。 */
|
||
inline const float kCloseButtonSize = 18.0f; /**< 行尾关闭(✕)按钮尺寸(分组行与信号行共用)。 */
|
||
inline const float kGroupSeparatorGap = 7.0f; /**< 分组标签与分隔线/名称的间距。 */
|
||
inline const float kGroupSeparatorWidth = 1.0f; /**< 分组标签右侧分隔线宽度。 */
|
||
inline const float kFilterHeight = 26.0f; /**< 「筛选信号」输入框高度。 */
|
||
inline const float kFilterPadV = 5.0f; /**< 筛选框上下留白。 */
|
||
inline const float kFilterTextPad = 4.0f; /**< 筛选框内文字/输入框相对外框的内缩(四边一致)。 */
|
||
inline const float kRowTailGap = 6.0f; /**< 网格徽标与行尾 ✕ 之间的间距。 */
|
||
|
||
/**
|
||
* @brief 「所属网格」菜单里可滚动的网格条目**最多显示的行数**(用户 2026-10-16 要求)。
|
||
*
|
||
* 信号很多时(例如把 MF4 的上百条信号各自放进独立网格)该菜单会有上百个条目;
|
||
* 条目太多既超出屏幕、又让人难以选择,因此只显示 10 行,其余用滚动条 / 滚轮翻动。
|
||
*/
|
||
inline const int kLaneMenuVisibleRows = 10;
|
||
|
||
/**
|
||
* @brief 大小写无关的通配符匹配(`*` 匹配任意长度的任意字符序列)。
|
||
* @param [IN] text 待匹配文本。
|
||
* @param [IN] pattern 模式(可含多个 `*`)。
|
||
* @return 匹配返回 true。
|
||
*/
|
||
inline bool WildcardMatchNoCase(const std::wstring& text, const std::wstring& pattern)
|
||
{
|
||
const auto fold = [](wchar_t character) -> wchar_t
|
||
{
|
||
// 仅对基本拉丁字母做大小写折叠(中文等其它字符原样比较)。
|
||
if (character >= L'A' && character <= L'Z')
|
||
{
|
||
return static_cast<wchar_t>(character - L'A' + L'a');
|
||
}
|
||
return character;
|
||
};
|
||
|
||
// 经典双指针回溯:`star` 记录最近一个 `*` 的位置,`resume` 记录当时尝试的文本位置。
|
||
std::size_t t = 0;
|
||
std::size_t p = 0;
|
||
std::size_t star = std::wstring::npos;
|
||
std::size_t resume = 0;
|
||
|
||
while (t < text.size())
|
||
{
|
||
if (p < pattern.size() && pattern[p] == L'*')
|
||
{
|
||
star = p;
|
||
resume = t;
|
||
++p;
|
||
}
|
||
else if (p < pattern.size() && fold(pattern[p]) == fold(text[t]))
|
||
{
|
||
++p;
|
||
++t;
|
||
}
|
||
else if (star != std::wstring::npos)
|
||
{
|
||
p = star + 1;
|
||
++resume;
|
||
t = resume;
|
||
}
|
||
else
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 文本已耗尽:模式剩余部分必须全是 `*`。
|
||
while (p < pattern.size() && pattern[p] == L'*')
|
||
{
|
||
++p;
|
||
}
|
||
return p == pattern.size();
|
||
}
|
||
|
||
// 样式编辑区:固定五行网格(颜色 / 样式 / 调整 / 线宽 / 透明度),
|
||
// 行高、间距与内容列宽度统一,保证面板宽度变化时各行控件始终对齐,且不会被无限制拉伸。
|
||
inline const float kStylePadH = 12.0f; /**< 样式区左右内边距。 */
|
||
inline const float kStylePadV = 12.0f; /**< 样式区上下内边距。 */
|
||
inline const float kStyleRowHeight = 26.0f; /**< 样式区单行高度。 */
|
||
inline const float kStyleRowGap = 10.0f; /**< 样式区行间距。 */
|
||
inline const float kStyleLabelWidth = 44.0f; /**< 样式行标签列宽度。 */
|
||
inline const float kStyleLabelGap = 8.0f; /**< 标签列与内容列间距。 */
|
||
inline const float kStyleRowCount = 5.0f; /**< 样式区固定行数(不含顶部「目标」行)。 */
|
||
inline const float kStyleTargetRowHeight = 26.0f; /**< 样式区顶部「目标」行高度(与其它行一致)。 */
|
||
/**
|
||
* @brief GX 网格标签与信号名之间的间距(逻辑像素)。
|
||
*
|
||
* 用户要求:目标信号已绘制到网格时,在信号名**后方**显示它所在的 GX 标签。
|
||
*/
|
||
inline const float kTargetBadgeGap = 8.0f;
|
||
/**
|
||
* @brief 平滑滚动的动画时长(毫秒)。
|
||
*
|
||
* 距离越远时长越长(但不会无限增长),靠近时由 ease-out 自动变慢 —— 用户要求
|
||
* “越远时越快、靠近时变慢”且“整个动画过程不要太久”。
|
||
*/
|
||
inline const float kScrollAnimMinMs = 130.0f; /**< 最短时长(短距离)。 */
|
||
inline const float kScrollAnimMaxMs = 300.0f; /**< 最长时长(长距离)。 */
|
||
inline const float kScrollAnimMsPerPixel = 0.35f; /**< 每像素距离增加的时长。 */
|
||
|
||
// ---- 分组折起/展开动画(用户 2026-10-16 要求)----
|
||
// 整组信号作为一个**刚性整体**向上平移(收起)/ 向下平移(展开),
|
||
// 越过组名下边界的部分被裁掉 —— 即“抽拉卷尺”观感:元素相对位置不变。
|
||
// 总时长 = clamp(基础 + 行数×每行, 下限, 上限),保证“整个动画过程不会太久”。
|
||
inline const float kGroupAnimBaseMs = 115.0f; /**< 动画基础时长。 */
|
||
inline const float kGroupAnimPerRowMs = 6.0f; /**< 每多一行增加的时长。 */
|
||
inline const float kGroupAnimMinMs = 150.0f; /**< 最短时长。 */
|
||
inline const float kGroupAnimMaxMs = 320.0f; /**< 最长时长。 */
|
||
|
||
/**
|
||
* @brief 分组**名称区域**进入改名编辑所需的「快速双击」时间上限(毫秒)。
|
||
*
|
||
* 用户 2026-10-17 要求:名称区域单击 = 展开/收起(恢复),**只有快速双击**才进入编辑框;
|
||
* “较慢但明显不是双击的点击频率”必须只当两次单击。
|
||
* Windows 的双击判定上限通常是 500ms(`GetDoubleClickTime()`),太宽松 ——
|
||
* 用户快速连点两次想“展开看一眼再收起”时会误入编辑态。这里收紧到 300ms:
|
||
* 真双击(一般 <250ms)照常进入编辑,稍慢的两次单击则各自触发展开/收起。
|
||
*/
|
||
inline const std::uint64_t kGroupRenameDoubleClickMs = 300;
|
||
inline const float kStyleContentMax = 260.0f; /**< 内容列最大宽度(再拉宽面板也不再延伸)。 */
|
||
inline const float kSwatchEdge = 18.0f; /**< 颜色色块边长上限(含自定义颜色入口)。 */
|
||
inline const float kSwatchMin = 11.0f; /**< 颜色色块边长下限。 */
|
||
inline const float kSwatchGap = 4.0f; /**< 颜色色块最小间距。 */
|
||
inline const float kSwatchMaxGap = 10.0f; /**< 颜色色块最大间距(用于铺满内容列)。 */
|
||
inline const float kSliderValueWidth = 52.0f; /**< 滑块行数值文本宽度(透明度与线宽一致)。 */
|
||
inline const float kSliderValueGap = 8.0f; /**< 滑块轨道与数值文本间距。 */
|
||
inline const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
|
||
inline const float kAdjustGap = 6.0f; /**< 「调整」行三个按钮之间的间距。 */
|
||
inline const float kAdjustMenuGap = 4.0f; /**< 「系数/偏移」「镜像」二级面板与按钮左上角的额外间距。 */
|
||
|
||
/** 「调整」行的按钮下标(与 `m_adjustRects` 同序)。 */
|
||
inline const int kAdjustGain = 0; /**< 系数(点击展开系数二级面板)。 */
|
||
inline const int kAdjustMirror = 1; /**< 镜像(点击展开镜像二级面板)。 */
|
||
inline const int kAdjustAbsolute = 2; /**< 绝对值。 */
|
||
inline const int kAdjustCount = 3; /**< 按钮数量。 */
|
||
|
||
// 线宽范围与「稀疏信号默认线宽」共用 `DataModel.hpp` 的常量,避免两处漂移
|
||
//(稀疏信号初始化时用的最大线宽必须与这里滑块的上限完全一致)。
|
||
inline const float kWidthMin = kSignalLineWidthMin; /**< 允许的最小线宽(px)。 */
|
||
inline const float kWidthMax = kSignalLineWidthMax; /**< 允许的最大线宽(px)。 */
|
||
|
||
/** @brief 系数调整允许的绝对值上限(防止输入极端值把波形完全挤出可视区)。 */
|
||
inline const double kGainLimit = 10000.0;
|
||
|
||
/** @brief 系数为默认值 1(不做任何缩放)时按钮显示为未启用样式。 */
|
||
inline bool GainIsDefault(float gain)
|
||
{
|
||
return std::fabs(gain - 1.0f) < 1.0e-6f;
|
||
}
|
||
|
||
/** @brief 可选绘制样式顺序(不含阶梯方式)。 */
|
||
inline const DrawMode kModeOptions[] = {DrawMode::LINE, DrawMode::SCATTER, DrawMode::LINE_SCATTER};
|
||
inline const int kModeOptionCount = static_cast<int>(sizeof(kModeOptions) / sizeof(kModeOptions[0]));
|
||
|
||
/**
|
||
* @brief 计算滑块的便捷命中矩形(比轨道略大,便于拖拽)。
|
||
* @param [IN] track 轨道矩形。
|
||
* @param [IN] scale 缩放比例。
|
||
* @return 命中矩形。
|
||
*/
|
||
inline gd::RectF SliderHitRect(const gd::RectF& track, float scale)
|
||
{
|
||
return gd::RectF(track.X - 6.0f * scale, track.Y - 9.0f * scale, track.Width + 12.0f * scale,
|
||
track.Height + 18.0f * scale);
|
||
}
|
||
|
||
/**
|
||
* @brief 判断颜色是否偏暗(用于在色块之上选择可读的图标颜色)。
|
||
* @param [IN] color 0xAARRGGBB 颜色值。
|
||
* @return 相对亮度低于阈值返回 true。
|
||
*/
|
||
inline bool IsDarkColor(std::uint32_t color)
|
||
{
|
||
const double r = static_cast<double>((color >> 16) & 0xFFu);
|
||
const double g = static_cast<double>((color >> 8) & 0xFFu);
|
||
const double b = static_cast<double>(color & 0xFFu);
|
||
return (0.299 * r + 0.587 * g + 0.114 * b) < 140.0;
|
||
}
|
||
|
||
/**
|
||
* @brief 将数值限制到合法区间。
|
||
* @param [IN] value 输入值。
|
||
* @param [IN] low 下限。
|
||
* @param [IN] high 上限。
|
||
* @return 限制后的数值。
|
||
*/
|
||
inline float ClampFloat(float value, float low, float high)
|
||
{
|
||
return (value < low) ? low : ((value > high) ? high : value);
|
||
}
|
||
|
||
/**
|
||
* @brief 数值文本:**至少两位小数**,精度更高时按实际精度显示。
|
||
*
|
||
* 例:`1` → `1.00`、`2.5` → `2.50`、`0.125` → `0.125`、`1.2345678` → `1.2345678`。
|
||
* 实现:从 2 位小数开始递增位数,直到文本能精确表示该数值
|
||
*(容差 1e-7 相对量,与 float 精度匹配)。
|
||
*
|
||
* @param [IN] value 数值。
|
||
* @return 显示文本。
|
||
*/
|
||
inline std::wstring FormatNumberText(double value)
|
||
{
|
||
const double tolerance = std::max(1.0e-12, std::fabs(value) * 1.0e-7);
|
||
for (int decimals = 2; decimals <= 9; ++decimals)
|
||
{
|
||
const std::wstring text = jdi::FormatNumber(value, decimals);
|
||
const double parsed = std::wcstod(text.c_str(), nullptr);
|
||
if (std::fabs(parsed - value) <= tolerance)
|
||
{
|
||
return text;
|
||
}
|
||
}
|
||
return jdi::FormatNumber(value, 9);
|
||
}
|
||
|
||
/**
|
||
* @brief 带正负符号的数值文本(非编辑态显示偏移量时使用)。
|
||
*
|
||
* 规则(用户明确要求):**正数带 `+`、负数带 `-`、0 不带任何符号**。
|
||
* (旧实现 0 也会显示 `+0.00`,用户报告“偏移调整的 Y 轴输入框默认值为 0 时也显示了正号,这不正确”。)
|
||
* @param [IN] value 数值。
|
||
* @return 形如 `+1.50` / `-0.25` / `0.00` 的文本。
|
||
*/
|
||
inline std::wstring FormatSignedNumber(double value)
|
||
{
|
||
const std::wstring text = FormatNumberText(std::fabs(value));
|
||
if (value > 0.0)
|
||
{
|
||
return L"+" + text;
|
||
}
|
||
if (value < 0.0)
|
||
{
|
||
return L"-" + text;
|
||
}
|
||
return text; // 0:不带符号
|
||
}
|
||
|
||
/**
|
||
* @brief 把系数格式化为界面文本(与偏移量同一套两位小数规则 + 显式正号)。
|
||
*
|
||
* 规则(用户明确要求):“系数调整”输入框**非编辑态**显示时,非 0 正值前面加 `+`,
|
||
* 负值按原样显示负号,0 不带符号。编辑态仍用不带符号的纯数值(见 `FormatNumberText`)。
|
||
* @param [IN] gain 系数。
|
||
* @return 显示文本。
|
||
*/
|
||
inline std::wstring FormatGainText(float gain)
|
||
{
|
||
const double value = static_cast<double>(gain);
|
||
if (value > 0.0)
|
||
{
|
||
return L"+" + FormatNumberText(value);
|
||
}
|
||
return FormatNumberText(value);
|
||
}
|
||
|
||
/**
|
||
* @brief 「时间轴」偏移的非编辑态显示文本(`Δt:` + 带正负号的复合单位间隔)。
|
||
*
|
||
* 规则(用户明确要求):在 `Δt:` 冒号后的时间显示值**前面加上正负号**,
|
||
* 只有值为 0 时不显示符号。示例:`Δt:+1h 1min 2s` / `Δt:-5s 60ms` / `Δt:0s`。
|
||
*(`FormatDurationCompound` 内部取绝对值,因此符号由这里补上。)
|
||
* @param [IN] seconds 时间偏移(秒,可为负)。
|
||
* @return 显示文本。
|
||
*/
|
||
inline std::wstring FormatOffsetTimeText(double seconds)
|
||
{
|
||
if (seconds > 0.0)
|
||
{
|
||
return L"Δt:+" + FormatDurationCompound(seconds);
|
||
}
|
||
if (seconds < 0.0)
|
||
{
|
||
return L"Δt:-" + FormatDurationCompound(seconds);
|
||
}
|
||
return L"Δt:" + FormatDurationCompound(0.0);
|
||
}
|
||
} // namespace spdetail
|
||
|
||
using namespace spdetail;
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __SIGNALPANEL_INTERNAL_HPP__
|
||
|