Files
Just-DataInspector/Components/SignalPanel/Src/SignalPanelInternal.hpp
T
DeepSeek-V4.1-Flash 436dcbfd52 滚动过渡动画根治 + 快速连点跟手 + 分组名单击展开 + 光标吸附同源 + 按钮淡出对齐 + 滚动位置复位
一、滚动过渡动画卡顿(用户报告:甚至不如手动拖滚动条)—— 三处独立根因
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 条与变更日志一行。
2026-10-17 03:17:42 +08:00

336 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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__