按组件目录结构把四个巨型实现文件拆分为多个协作文件(PlotView / SignalPanel / Application / DataModel)
承接上一条提交(0871eb0)—— 该提交只完成了 Components/Shared 与注释/命名清理,并**主动未做**
大文件拆分;本提交把大文件拆分补齐,至此“按功能拆分逻辑块到各组件自己的 Inc/Src”已落地。
## 一、拆分结果
| 原文件 | 拆分后 |
| --- | --- |
| Components/PlotView/Src/PlotView.cpp(13 695 行 / 544 KB) | PlotView.cpp(787) + PlotViewPaint.cpp(6 367) + PlotViewInteraction.cpp(2 715) + PlotViewFeatures.cpp(3 005) + PlotViewInternal.hpp(936) |
| Components/SignalPanel/Src/SignalPanel.cpp(4 184 行) | SignalPanel.cpp(1 141) + SignalPanelPaint.cpp(697) + SignalPanelInteraction.cpp(2 095) + SignalPanelInternal.hpp |
| Components/Application/Src/Application.cpp(3 135 行) | Application.cpp(1 427) + ApplicationCommands.cpp(1 184) + ApplicationLoading.cpp(270) + ApplicationInternal.hpp |
| Utilities/Src/DataModel.cpp(2 615 行) | DataModel.cpp(1 583,含自由函数区) + DataModelTab.cpp(732) + DataModelInternal.hpp |
分片职责(以 PlotView 为例):
core 视图核心:构造/析构、上下文、布局、坐标映射、视界与平移、网格结构与归属
paint 渲染与像素缓存:全部绘制、场景/装饰缓存、每网格图层、宽基底与滚动补条
interaction 交互与编辑:命中测试、鼠标/键盘、光标形状、内联输入框、浮动面板层序
features 视图特性与派生数据:光标读数/提示/缩放、自适应、吸附、单点、派生曲线线程池、
边沿统计、实验性功能参数
(SignalPanel 为 core/paint/interaction;Application 为 core/commands/loading;
DataModel 为 core/tab。)
## 二、关键手法(这是能安全拆开的前提)
原文件的辅助区都是**匿名命名空间**(PlotView 约 870 行常量与工具函数),被所有成员函数广泛引用。
直接拆文件会让各 .cpp 拿不到这些工具,因此:
1. 把该块整块搬进 `<组件>Internal.hpp`,从 `namespace { }` 改为**具名**命名空间
(pvdetail / spdetail / appdetail / dmdetail);
2. 块内**每个函数与常量都标 inline** ⇒ 多个翻译单元共用同一个定义:既不重复符号,
也不产生 N 份函数定义;
3. 头部末尾 `using namespace <ns>;` ⇒ 各 .cpp 函数体里原有的非限定调用(IntersectRect /
NiceStep / ClampDouble …)**一行都不用改**;
4. 成员定义按「方法名 → 分片」归类后**整块搬移** —— 全程**纯代码搬移**,没有改动任何一行函数体。
## 三、拆分过程中踩到并修正的四个坑(已固化到 .preflight/perf/split_component.ps1)
1. `[[maybe_unused]]` 等属性**必须排在 inline 之前**(`inline [[x]] T` 非法);
单独的 `{`/`}` 行、注释行、`struct`/`enum`/`using`/`template` 行不能加 inline。
2. 匿名命名空间的**结束行可能带注释**(`} // namespace`),只认 `^\t}$` 会一路匹配到后面
某个成员函数的结束括号,把大段成员定义误当成“辅助区”搬走。
3. **首个成员定义之前的所有行**必须跟着进核心文件:DataModel.cpp 在匿名命名空间之后、
首个成员定义之前还有约 390 行命名空间外的自由函数(SignalStats / ComputeStats /
CacheSparseSamples / ApplySinglePointDefaultStyle …),漏掉会被静默丢弃。
4. 内部头必须带上原文件真正用到的全部头:DataModel 需要 Theme.hpp / <cstring> / <cstdio> /
<deque> / <atomic> / <cwctype> / <new>(原文件靠这些头才编译得过)。
## 四、附带完成的命名审计
PlotView 中残留的 File 语义命名一并修正:LaneSpansMultipleFiles → LaneSpansMultipleGroups、
firstFile → firstGroup、multiFile → multiGroup、SceneCacheKey::haloFile → haloGroup。
## 五、验证(全部离屏:SW_HIDE + 不注入真实输入 + 进程/线程 BELOW_NORMAL)
- .preflight/perf/verify.exe(PlotView 渲染/交互,含逐像素断言)49/49
- .preflight/perf/panelanim.exe(信号面板:卷尺式折起/展开、筛选态、悬浮组名、目标行)156/156
- .preflight/perf/datacheck.exe(数据层:单轴/多时间轴 CSV、MF4、重命名语义)69/69
- .preflight/perf/edgecheck.exe(边沿统计与面板排版)46/46
- make release / make debug 零警告。
即:拆分后**行为与拆分前完全一致**(纯代码搬移,四套探针全绿)。
## 六、体积(诚实记录)
Target.exe: 754 176 → 764 416 B(+10 240 B,+1.4%)
因为 inline 工具函数会在每个引用它的翻译单元里各生成一份(PE/COFF 链接器没有 ICF 合并)。
这再次印证:拆分解决的是**可维护性**,不是体积(本轮的 −24.6% 全部来自 ICO 256×256 条目改 PNG)。
## 七、文档
Documents/agent_mem.md:§9.37 新增“七、多文件拆分(已完成)”与“八、仍未完成”(只剩
Lane → LanePanelState、模态提示解耦、MF4 压缩块三项);§11.2 新增第 91、92 条(多文件化的
唯一安全手法 + 体积代价)。Documents/refactor_report_2026-10-16.md 同步更新 §五/§七/§八。
This commit is contained in:
1 parent
0871eb0cba
commit
e6b7bf66ec
19 files changed
+19130
-18771
No files matched your search
@@ -0,0 +1,932 @@
|
||||
#ifndef __PLOT_VIEW_INTERNAL_HPP__
|
||||
#define __PLOT_VIEW_INTERNAL_HPP__
|
||||
|
||||
// Components/PlotView 组件内部共享的常量与工具函数(私有头,仅本组件的 .cpp 使用)。
|
||||
//
|
||||
// PlotView 的实现按功能拆成多个协作文件:
|
||||
// PlotView.cpp 视图核心(上下文 / 布局 / 坐标映射 / 视界与缩放 / 网格结构)
|
||||
// PlotViewPaint.cpp 渲染与像素缓存(绘制 / 场景与装饰缓存 / 图层 / 宽基底)
|
||||
// PlotViewInteraction.cpp 交互、命中测试与内联编辑
|
||||
// PlotViewFeatures.cpp 实验性功能、派生曲线、边沿统计、自适应与光标吸附
|
||||
// 它们共用这一份内部工具集。
|
||||
//
|
||||
// ⚠️ 为让多个翻译单元都能使用,原匿名命名空间改为**具名**命名空间 `pvdetail`,
|
||||
// 并把其中每个函数与常量标为 `inline` —— 单一定义、单一副本,不会产生代码重复。
|
||||
// 各实现文件在包含本头之后用 `using namespace pvdetail;` 引入这些工具,
|
||||
// 因此函数体里原有的非限定调用(如 `IntersectRect` / `NiceStep`)一行都不用改。
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "PlotView.hpp"
|
||||
#include "UiControls.hpp"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace pvdetail
|
||||
{
|
||||
/**
|
||||
* @brief PlotView 内部工具集(常量 / 几何与配色辅助 / 边沿统计辅助 / 绘制辅助)。
|
||||
*
|
||||
* 全部符号都是 `inline` ⇒ 多个翻译单元共用同一个定义:既不重复符号,也不产生多份代码。
|
||||
* 不要把它们移回匿名命名空间(那样每个 .cpp 都会各自复制一份)。
|
||||
*/
|
||||
inline const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */
|
||||
inline const int kYTickTarget = 6; /**< 幅值轴期望刻度数(网格较高时)。 */
|
||||
inline const int kYTickTargetMin = 3; /**< 幅值轴最少刻度数(网格很矮时也要保证可读数)。 */
|
||||
inline const int kYTickTargetMax = 8; /**< 幅值轴最多刻度数。 */
|
||||
inline const float kYTickSpacing = 28.0f; /**< 幅值刻度之间的理想间距(逻辑像素)。 */
|
||||
inline const float kYLabelMinGap = 15.0f; /**< 幅值标签的最小行距(逻辑像素)。 */
|
||||
inline const int kMinorDivisions = 5; /**< 相邻主刻度之间的等分数(用于估读)。 */
|
||||
inline const unsigned int kMinorLineAlpha = 0x9Au; /**< 辅助等分线的透明度(越高越清晰)。 */
|
||||
inline const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */
|
||||
inline const float kLanePadding = 8.0f; /**< 网格内边距。 */
|
||||
inline const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数(上限保护)。 */
|
||||
inline const size_t kPolylinePointsPerPixel = 2; /**< 每个像素列允许的折线顶点数(上/下包络两顶点)。 */
|
||||
inline const size_t kMinPolylinePoints = 512; /**< 折线最少顶点数(窗口很窄时仍保留细节)。 */
|
||||
/**
|
||||
* @brief 图层**纵向滚动**的量化阈值(位图像素)。
|
||||
*
|
||||
* 幅值平移不足该值时**不滚动**图层,交给贴图时的纵向偏移(`srcY`)处理;
|
||||
* 超过才滚动一次并补画新暴露的一整条。这样画布映射在拖动过程中保持稳定 →
|
||||
* 折线缓存(键含幅值映射)能命中,避免"每帧滚 1 行 → 每帧重算折线"的性能陷阱。
|
||||
*/
|
||||
inline const int kLayerRollQuantum = 16;
|
||||
inline const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
||||
inline const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */
|
||||
inline const float kMarkerEdgeInset = 7.0f; /**< 数据点越界贴边时距网格可见边界的间距。 */
|
||||
inline const float kMarkerInsideMargin = 2.0f; /**< 数据点在视界内时允许贴到边界的最小余量。 */
|
||||
inline const float kMarkerTriangleArm = 5.0f; /**< 越界方向三角"半宽/半高"(垂直于朝向的半边长)。 */
|
||||
/** 越界方向三角沿朝向的深度(与光标把手同比例)。 */
|
||||
inline const float kMarkerTriangleDepth = kMarkerTriangleArm * 1.4f;
|
||||
/**
|
||||
* 越界方向三角与数值/时间标注之间的间隙(逻辑像素)。
|
||||
*
|
||||
* 三角与胶囊之间**必须留出这段空白**:贴在一起时三角看起来像胶囊上多出来的
|
||||
* 一个尖角(用户报告的"三角形与数值显示部分没有间隔")。
|
||||
*/
|
||||
inline const float kMarkerTriangleGap = 4.0f;
|
||||
inline const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
||||
inline const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */
|
||||
inline const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
||||
inline const float kMaxScatterRadius = 12.0f; /**< 散点最大半径(逻辑像素)。 */
|
||||
inline const float kScatterRadiusScale = 1.5f; /**< 散点半径相对线宽的增长系数。 */
|
||||
inline const double kMinTimeSpan = 1.0e-12; /**< 允许的最小时间跨度。 */
|
||||
inline const float kPlotSidePadding = 8.0f; /**< 绘图区左右留白(逻辑像素)。 */
|
||||
inline const float kLaneDividerWidth = 2.0f; /**< 网格之间分界线的宽度。 */
|
||||
inline const float kSignalHitTolerance = 6.0f; /**< 信号曲线命中容差(逻辑像素)。 */
|
||||
/**
|
||||
* @brief 两个矩形的交集(空交集返回宽高为 0 的矩形)。
|
||||
*
|
||||
* 用于把 GDI 绘制(不受 GDI+ 裁剪约束)限制在"本次真正要重画的区域"内,
|
||||
* 见 `PaintLane` 里的 `laneClip`。
|
||||
*/
|
||||
inline gd::RectF IntersectRect(const gd::RectF& first, const gd::RectF& second)
|
||||
{
|
||||
const float left = std::max(first.X, second.X);
|
||||
const float top = std::max(first.Y, second.Y);
|
||||
const float right = std::min(first.X + first.Width, second.X + second.Width);
|
||||
const float bottom = std::min(first.Y + first.Height, second.Y + second.Height);
|
||||
if (right <= left || bottom <= top)
|
||||
{
|
||||
return gd::RectF(left, top, 0.0f, 0.0f);
|
||||
}
|
||||
return gd::RectF(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 绘制「还原/重置」图标(逆时针回环箭头)。
|
||||
*
|
||||
* 与 `PopupMenu` 里「系数 / 偏移」二级面板标题栏右侧的还原图标**同一套笔画**
|
||||
*(用户要求:“可以直接挪用信号『调整』功能中『系数/偏移』二级菜单内的复位控件”),
|
||||
* 因此弱峰调整面板的复位控件与它视觉完全一致。不依赖字体或资源,任意 DPI 都清晰。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 图标外接矩形(正方形)。
|
||||
* @param [IN] color 笔画颜色(0xAARRGGBB)。
|
||||
* @param [IN] scale DPI 缩放比例。
|
||||
*/
|
||||
inline void PaintPanelResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color,
|
||||
float scale)
|
||||
{
|
||||
const float inset = 2.5f * scale;
|
||||
gd::RectF box(rect.X + inset, rect.Y + inset, rect.Width - inset * 2.0f,
|
||||
rect.Height - inset * 2.0f);
|
||||
if (box.Width <= 1.0f || box.Height <= 1.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
gd::Pen pen(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu, (color >> 8) & 0xFFu,
|
||||
color & 0xFFu),
|
||||
std::max(1.0f, 1.5f * scale));
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
// 缺口留在右上角(从 300° 到 20° 处断开),与常见的"重置/刷新"图标一致。
|
||||
g.DrawArc(&pen, box.X, box.Y, box.Width, box.Height, 300.0f, 300.0f);
|
||||
|
||||
// 箭头:位于缺口末端(约 300° 方向),指向前进方向。
|
||||
const float cx = box.X + box.Width * 0.5f;
|
||||
const float cy = box.Y + box.Height * 0.5f;
|
||||
const float radius = box.Width * 0.5f;
|
||||
const float angle = 300.0f * 3.14159265f / 180.0f;
|
||||
const float tipX = cx + std::cos(angle) * radius;
|
||||
const float tipY = cy + std::sin(angle) * radius;
|
||||
const float size = std::max(2.5f, 3.2f * scale);
|
||||
gd::PointF arrow[3];
|
||||
arrow[0] = gd::PointF(tipX, tipY);
|
||||
arrow[1] = gd::PointF(tipX - size, tipY - size * 0.25f);
|
||||
arrow[2] = gd::PointF(tipX - size * 0.25f, tipY - size);
|
||||
gd::SolidBrush brush(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu,
|
||||
(color >> 8) & 0xFFu, color & 0xFFu));
|
||||
g.FillPolygon(&brush, arrow, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 光标吸附的**判定系数**:一屏内的数据点少于 `网格小格数 × 该系数` 时才开启吸附。
|
||||
*
|
||||
* 用户 2026-10-15 明确定义:网格底衬一横排有 N 个小格(见 `GridMinorCellCount`),
|
||||
* **当且仅当**某信号在当前视界内的数据点个数 `< N × 4` 时为它开启光标吸附。
|
||||
* 该判据随视界实时重算 ⇒ 疏密变化的信号在稀疏区自动吸附、稠密区自动自由移动。
|
||||
*/
|
||||
inline const int kSnapVisibleCellsPerPoint = 4;
|
||||
|
||||
/**
|
||||
* @brief 吸附半径(以**网格小格**为单位):一个数据点只用其**一个小格宽**内的移动来吸。
|
||||
*
|
||||
* 吸附半径 = 一个网格小格的宽度(与判定用同一套网格定义),且**不超过半个采样间距**
|
||||
* ——后者保证点很稀疏时(例如一屏只有一两个点)光标在点与点之间仍可自由移动,
|
||||
* 而不是被“死死吸住”(用户明确要求:吸附只是提示性响应)。
|
||||
*/
|
||||
inline const double kSnapRadiusCells = 1.0;
|
||||
|
||||
/**
|
||||
* @brief 一般信号查找“离光标最近的有效采样点”时向两侧探测的最大步数。
|
||||
*
|
||||
* 采样值里的 NaN 只出现在缺少数据的区段(例如多时间轴 CSV 的较短轴的尾部),
|
||||
* 因此从最近下标向外少量探测即可找到有效点;探测失败就不吸附(宁可自由移动)。
|
||||
*/
|
||||
inline const int kSnapSampleProbeSteps = 64;
|
||||
|
||||
/**
|
||||
* @brief 判定“可见数据点是否足够少”时允许扫描的最大样本数(相对阈值的倍数)。
|
||||
*
|
||||
* 判定只需知道“有效点是否少于阈值”,因此扫到阈值个有效点就早退;
|
||||
* 但连续 NaN 不会被计入,因此再给一个**有界**的扫描窗口防止极端情况下的长循环。
|
||||
*/
|
||||
inline const std::size_t kSnapScanWindowFactor = 64;
|
||||
|
||||
/**
|
||||
* @brief 「所有可见数据点都是单值信号、且时刻完全相同」时,时间轴**一个大格**对应的秒数。
|
||||
*
|
||||
* 用户 2026-10-15 明确要求:这种最极端情况下时间轴以该时刻居中、并缩放到
|
||||
* **一大格 = 1s** 的级别。视界跨度取 `kTimeTickTarget` 个大格 ⇒
|
||||
* 跨度 = 10s、`NiceStep(10, kTimeTickTarget)` = **1.0 秒**,正好是"一大格 1s"。
|
||||
*/
|
||||
inline const double kSinglePointClusterSecondsPerCell = 1.0;
|
||||
|
||||
/**
|
||||
* @brief 让边界处的单值散点"完整显示"时,在半径之外额外多留的像素(设备像素)。
|
||||
*
|
||||
* 只让出**正好等于半径**的量会让圆点边缘贴着网格边框(抗锯齿边缘看起来被切了一刀),
|
||||
* 因此再多留 1 个像素。
|
||||
*/
|
||||
inline const float kSinglePointBoundaryPadPixels = 1.0f;
|
||||
|
||||
/**
|
||||
* @brief 判定"多个单值数据点处于同一时刻"的相对误差。
|
||||
*
|
||||
* 同一列里读出的时刻是完全相同的 double;但不同来源(例如 MF4 的整数刻度 ×
|
||||
* 通道组的标定系数)可能差 1ulp,因此用相对误差而不是精确相等。
|
||||
*/
|
||||
inline const double kSinglePointSameTimeEpsilon = 1.0e-9;
|
||||
|
||||
inline const float kLaneLabelPad = 6.0f; /**< 网格内侧标尺带的内边距。 */
|
||||
inline const float kLegendHeight = 19.0f; /**< 图例条目高度(逻辑像素)。 */
|
||||
inline const float kLegendPad = 5.0f; /**< 图例相对网格左上角的内缩。 */
|
||||
inline const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
|
||||
inline const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
|
||||
inline const float kLegendMaxTextWidth = 200.0f; /**< 图例条目的文本宽度上限(超出则省略)。 */
|
||||
inline const float kLaneBadgeWidth = 30.0f; /**< 网格编号徽标宽度(与信号面板保持一致)。 */
|
||||
inline const float kLaneBadgeHeight = 17.0f; /**< 网格编号徽标高度。 */
|
||||
inline const float kLaneBadgeGap = 8.0f; /**< 编号徽标与图例条目之间的间距。 */
|
||||
inline const float kEdgePanelWidth = 230.0f; /**< 边沿统计浮动面板宽度。 */
|
||||
inline const float kEdgePanelTitleHeight = 26.0f; /**< 面板标题栏高度。 */
|
||||
inline const float kEdgePanelRowHeight = 18.0f; /**< 面板内单行文本高度。 */
|
||||
inline const float kEdgePanelPad = 7.0f; /**< 面板内边距。 */
|
||||
inline const float kEdgePanelNameMaxWidth = 150.0f; /**< 面板内信号名的宽度上限(超过则省略;避免超长信号名把面板顶得很长)。 */
|
||||
inline const float kEdgePanelStatColumnGap = 14.0f; /**< 「上升沿 / 下降沿」两列之间的间距(保证多信号的文字垂直对齐)。 */
|
||||
inline const float kEdgePanelEditFieldWidth = 64.0f; /**< 数值框的最小宽度。 */
|
||||
inline const float kEdgePanelButtonHeight = 30.0f; /**< 均值滤波面板底部按钮行高度。 */
|
||||
inline const float kEdgePanelMeanRowHeight = 22.0f; /**< 逐条目面板中每条信号的名称行高度。 */
|
||||
inline const float kEdgePanelMeanEditHeight = 24.0f; /**< 逐条目面板中每条信号的参数行高度。 */
|
||||
inline const float kEdgePanelMeanRowGap = 2.0f; /**< 名称行与参数行之间的间距。 */
|
||||
inline const float kEdgePanelMeanCheckSize = 15.0f; /**< 逐条目面板中条目复选框尺寸。 */
|
||||
inline const float kWeakPeakSliderRowHeight = 26.0f; /**< 弱峰调整面板中每个滑块行的高度。 */
|
||||
inline const float kWeakPeakTrackHeight = 6.0f; /**< 弱峰调整滑块的轨道高度。 */
|
||||
inline const float kWeakPeakTrackMinWidth = 120.0f; /**< 弱峰调整滑块轨道的最小宽度。 */
|
||||
/**
|
||||
* @brief 「弱峰调整」滑块行右侧**复位控件**的边长(逻辑像素)。
|
||||
*
|
||||
* 每行滑块都有一个复位按钮(与「系数/偏移」二级面板里的「还原」图标同一套绘制),
|
||||
* 点击把该行的参数恢复为默认值(幅度 0 / 迭代次数 1 且上限回到初始上限)。
|
||||
*/
|
||||
inline const float kWeakPeakResetButtonSize = 16.0f;
|
||||
/** 复位控件与其左侧数值列之间的间距(逻辑像素)。 */
|
||||
inline const float kWeakPeakResetGap = 4.0f;
|
||||
inline const int kWeakPeakIterationsMin = 1; /**< 迭代次数 N 的下限。 */
|
||||
/** 迭代次数滑块「自定义上限」的允许范围(与算法内部的阶数夹取保持一致)。 */
|
||||
inline const int kWeakPeakIterationsMaxLimit = 1000;
|
||||
/** 迭代次数滑块上限的默认值(与 `Lane::WeakPeakItem::maxIterations` 一致)。 */
|
||||
inline const int kWeakPeakIterationsDefaultMax = 10;
|
||||
/**
|
||||
* @brief 「弱峰调整」滑动条的量程(±400%)。
|
||||
*
|
||||
* 滑动条同时驱动三组识别规则(见 `ComputeWeakPeakAdjust`):0–100% 只有第一类规则在动,
|
||||
* 之后每 100% 加入下一类、而前一类继续走完自己的 0–200%。因此量程是算法内部
|
||||
* 「单组 ±200%」的两倍。
|
||||
*/
|
||||
inline const double kWeakPeakSliderScale = 4.0;
|
||||
/** 弱峰调整幅度百分比上限(滑动条右端,用于构建新信号 / 曲线缓存键)。 */
|
||||
inline const int kWeakPeakAmountMax = 400;
|
||||
/** 弱峰调整幅度百分比下限(滑动条左端,负值 = 削弱)。 */
|
||||
inline const int kWeakPeakAmountMin = -400;
|
||||
/**
|
||||
* @brief 派生曲线「区间窗口」缓存的保护带上限(采样点数)。
|
||||
*
|
||||
* 窗口只覆盖可见区间 ± 保护带,可见区间内的取值与整条曲线一致(见 `CurveSpanFor`)。
|
||||
* 上限避免在完全缩小时把保护带放大到整条信号(那就退化成重算整条曲线)。
|
||||
*/
|
||||
inline const std::size_t kDerivedCurveGuardMax = 65536;
|
||||
/** 派生曲线缓存条目上限(超出时按「最旧优先」淘汰;它只是加速与内存控制手段)。 */
|
||||
inline const std::size_t kCurveSpanCacheLimit = 32;
|
||||
/** 派生曲线缓存允许占用的总字节数(约 768MB):超出时按「最旧优先」淘汰。
|
||||
* 取足够大是为了让「完全放大时 5 个网格各缓存一条整段曲线」可以同时驻留。 */
|
||||
inline const std::size_t kCurveSpanCacheMaxBytes = 768ull * 1024ull * 1024ull;
|
||||
/**
|
||||
* @brief 派生(预览)曲线走「后台异步计算」的窗口采样数门槛。
|
||||
*
|
||||
* 拖动滑块 / 移动视界 / 缩放每帧都会请求预览曲线。窗口不超过本门槛时同步重算足够快
|
||||
* (弱峰调整 ≈0.9ms/6.5 万采样),预览曲线能**实时跟随控件**;一旦窗口超过门槛
|
||||
* (大时间跨度、整条信号),同步重算会阻塞交互(弱峰调整整条信号单条 ≈368ms),
|
||||
* 此时改为交后台线程计算、本帧先用旧曲线顶替(双缓冲),保证控件反馈始终跟手。
|
||||
*/
|
||||
inline const std::size_t kDerivedAsyncWindowThreshold = 65536;
|
||||
/**
|
||||
* @brief 「超大信号」门槛:信号采样数超过它时,**拖拽参数期间不再反复排队**"整条信号"的
|
||||
* 预览曲线重算(见 `CurveSpanFor`)。
|
||||
*
|
||||
* 预览曲线按整条信号计算(`[0, count]`,与「构建为新信号」逐位一致)。信号极大时每条
|
||||
* 曲线要扫描上亿个采样,后台线程被持续占满会**反过来拖慢前台绘制**(实测前台 14ms/帧,
|
||||
* 而绘制本身仅 0.4ms)。此时拖动期间沿用旧曲线、松手后再算一次最新的即可 ——
|
||||
* 整条信号的重算本来就追不上滑块速度,"冻结"与"每帧重排"观感相同,但后者拖慢前台。
|
||||
*/
|
||||
inline const std::size_t kHugeSignalPreviewSamples = 4000000ull;
|
||||
inline const wchar_t* const kWeakPeakLevelLabel = L"调整幅度"; /**< 逐条目面板里「幅度」滑块行的标签(用户要求:滑动条前不再单独显示 `%`)。 */
|
||||
inline const wchar_t* const kWeakPeakIterationsLabel = L"迭代次数 N"; /**< MEAN / CENTER 面板里「迭代次数」输入框的标签。 */
|
||||
/** 「弱峰调整」迭代次数滑块行的标签(用户要求:滑动条前不再单独显示 `N`)。 */
|
||||
inline const wchar_t* const kWeakPeakIterationsSliderLabel = L"迭代次数";
|
||||
inline const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
|
||||
inline const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
|
||||
/**
|
||||
* @brief 边沿统计累积状态上限。
|
||||
*
|
||||
* ⚠️ 必须足够大:`普通 + 中心轴` × 一个网格里的信号数(都勾选时)会被全部使用。
|
||||
* 上限太小会**淘汰其它信号的已扫完状态** → 它们从头重扫(表现为“改一个参数、
|
||||
* 其它几个信号也重新统计”)。一个状态仅占「块数组 ≈ (采样数/8192)×16B」+ 几十 KB
|
||||
* 本地曲线,64 个状态对内存无压力。
|
||||
* 真的超限时**优先淘汰「已扫完」的最旧状态**(它重新扫完很快),绝不淘汰正在推进的。
|
||||
*/
|
||||
inline const std::size_t kEdgeScanStateLimit = 64;
|
||||
/**
|
||||
* @brief 「边沿计数」累积统计的分块大小(采样点数)。
|
||||
*
|
||||
* 每块各存上升/下降沿数:查询某个时间区间时,内部整块用存储值相加、只有两端边界块
|
||||
* 需要就地精确扫描(≤ 2 块),因此拖动时间光标的单帧代价与区间长度无关。
|
||||
* 取小一点 ⇒ 边界块扫描更便宜;取大一点 ⇒ 块数组更省内存。
|
||||
*/
|
||||
inline const std::size_t kEdgeBlockSamples = 8192;
|
||||
/**
|
||||
* @brief 每一帧允许「边沿计数」扫描的采样对总数(跨全部累积状态共享)。
|
||||
*
|
||||
* 千万级信号整段扫描一次 ≈115ms(用户报告:5 条信号同一网格时单帧 ≈575ms,
|
||||
* 界面完全失去响应)。改成预算推进后,单帧最多只花 ≈5ms 在统计上,
|
||||
* 剩下的部分在后续帧里接着扫(未扫完时界面显示上一个完整结果 + “…”)。
|
||||
*
|
||||
* ⚠️ **必须按帧重置**(在 `Paint` 开头):旧实现按 `GetTickCount64()` 分桶,
|
||||
* 而系统时钟粒度约 15.6ms —— 当帧率高于它时(`PostMessage` 自持重绘会把帧率拉高),
|
||||
* 同一 tick 内的多帧**完全不推进**(用户报告:“按住横向光标不动时统计卡在统计中”,
|
||||
* “回车确认参数后不移动鼠标就一直停在部分值”)。
|
||||
*/
|
||||
inline const std::size_t kEdgeScanPairsPerFrame = 1200000;
|
||||
/**
|
||||
* @brief 每个累积状态每帧的扫描配额下限(防止多个状态时前面的把预算吃光)。
|
||||
*
|
||||
* 过去所有状态共享一个“本 tick 总预算”,而推进总是从列表中的第一个状态开始
|
||||
* —— 5 条信号时只有第一条有进展,其余 4 条长期显示“统计中…”(用户报告
|
||||
* “很多项都停在了统计中不更新”)。现在每个状态各自持有每帧配额,互不饿死。
|
||||
*/
|
||||
inline const std::size_t kEdgeScanPairsPerStateMin = 60000;
|
||||
/**
|
||||
* @brief 计算每状态配额时假定的“最小并发状态数”。
|
||||
*
|
||||
* ⚠️ 不能直接用 `m_edgeScans.size()`:状态是在**同一帧内逐个创建**的,第一个状态的
|
||||
* 那一刻 `size() == 1`,会拿走整额预算 → 同帧后面新建的状态全部被截断为 0 配额
|
||||
*(首帧只有第一条信号推进)。按一个典型并发量(普通 + 中心轴 × 网格内信号数)
|
||||
* 下限分摊,可保证同一帧里所有状态都得到推进。
|
||||
*/
|
||||
inline const std::size_t kEdgeScanFairShare = 6;
|
||||
/**
|
||||
* @brief 「边沿计数」结果里的**上升沿**部分文本。
|
||||
* @param [IN] rising 上升沿数量。 @param [IN] complete 是否为完整精确值。
|
||||
* @return 形如 "上升沿 123"(未算完时追加 " …")的文本。
|
||||
*/
|
||||
inline std::wstring EdgeRisingText(std::size_t rising, bool complete)
|
||||
{
|
||||
std::wstring text = L"上升沿 " + std::to_wstring(rising);
|
||||
if (!complete)
|
||||
{
|
||||
text += L" …";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「边沿计数」结果里的**下降沿**部分文本。
|
||||
* @param [IN] falling 下降沿数量。 @param [IN] complete 是否为完整精确值。
|
||||
* @return 形如 "下降沿 456"(未算完时追加 " …")的文本。
|
||||
*/
|
||||
inline std::wstring EdgeFallingText(std::size_t falling, bool complete)
|
||||
{
|
||||
std::wstring text = L"下降沿 " + std::to_wstring(falling);
|
||||
if (!complete)
|
||||
{
|
||||
text += L" …";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「边沿计数」是否处于“完全未算出”状态(用于显示“统计中…”)。
|
||||
*/
|
||||
inline bool EdgeCountsPending(std::size_t rising, std::size_t falling, bool complete)
|
||||
{
|
||||
return (!complete && rising == 0 && falling == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「边沿计数」结果的显示文本(合并成一行,用于宽度测量与兼容旧调用)。
|
||||
*
|
||||
* ⚠️ 未完成时**两个数字后面都要带 `…`**(旧实现只在整行末尾追加一个 `…`,
|
||||
* 视觉上只跟在“下降沿”的数字后面,用户明确要求“为上升沿也加上这个尾缀”)。
|
||||
* @param [IN] rising 上升沿数量。 @param [IN] falling 下降沿数量。
|
||||
* @param [IN] complete 是否为该阈值的完整精确值。
|
||||
* @return 面板/宽度测量共用的文本。
|
||||
*/
|
||||
[[maybe_unused]] inline std::wstring EdgeCountsText(std::size_t rising, std::size_t falling,
|
||||
bool complete)
|
||||
{
|
||||
if (EdgeCountsPending(rising, falling, complete))
|
||||
{
|
||||
return L"统计中…";
|
||||
}
|
||||
return EdgeRisingText(rising, complete) + L" " + EdgeFallingText(falling, complete);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否命中「弱峰调整」的某条滑块轨道。
|
||||
*
|
||||
* 轨道本身只有几个像素高,直接按轨道矩形命中很难抓住,因此把命中区在纵向上下各放宽
|
||||
* (至少一个轨道高度),与样式区滑块的交互一致。
|
||||
*
|
||||
* @param [IN] track 滑块轨道矩形(宽度为 0 表示当前没有滑块)。
|
||||
* @param [IN] scale DPI 缩放比例。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 命中返回 true。
|
||||
*/
|
||||
inline bool WeakPeakSliderHit(const gd::RectF& track, float scale, float x, float y)
|
||||
{
|
||||
if (track.Width <= 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
gd::RectF hit = track;
|
||||
const float pad = std::max(8.0f * scale, track.Height);
|
||||
hit.Y -= pad;
|
||||
hit.Height += pad * 2.0f;
|
||||
return UiPointInRect(hit, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「弱峰调整」滑块行右侧**数值列**的宽度(布局与面板宽度共用同一算式)。
|
||||
*
|
||||
* 取「+400%」与「10000」两者较宽者:前者是幅度滑块的最大显示文本
|
||||
*(**正负号始终显示**,且量程已扩到 ±400%),
|
||||
* 后者为迭代次数上限留出足够位宽(上限可被用户改大)。
|
||||
*/
|
||||
inline float WeakPeakValueColumnWidth(HFONT monoSmall)
|
||||
{
|
||||
return std::max(UiMeasureTextWidth(monoSmall, L"+400%"),
|
||||
UiMeasureTextWidth(monoSmall, L"10000"));
|
||||
}
|
||||
|
||||
/** @brief 「弱峰调整」滑块行左侧**标签列**的宽度(两条标签取较宽者,保证轨道左对齐)。 */
|
||||
inline float WeakPeakLabelColumnWidth(HFONT uiSmall)
|
||||
{
|
||||
return std::max(UiMeasureTextWidth(uiSmall, kWeakPeakLevelLabel),
|
||||
UiMeasureTextWidth(uiSmall, kWeakPeakIterationsSliderLabel));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 取折线(相邻采样点之间线性连接)在给定时刻的取值。
|
||||
*
|
||||
* 包络是按像素列聚合的,因此某一段折线可能**跨越列边界**:跨越边界的那一小段
|
||||
* 在两个列里都不属于"落在本列内的采样点",只看列内采样点就会把它整段丢掉。
|
||||
* 该函数用于在列边界处取回这段折线的取值,使相邻两列的包络严丝合缝。
|
||||
*
|
||||
* @param [IN] time 时间轴。
|
||||
* @param [IN] values 采样值。
|
||||
* @param [IN] count 可用采样点数量。
|
||||
* @param [IN] style 显示外观(水平镜像时按**源下标反序**取值)。
|
||||
* @param [IN] firstIndex 第一个时间不小于查询时刻的采样下标(相交线段为 firstIndex-1 → firstIndex)。
|
||||
* @param [IN] t 查询时刻。
|
||||
* @param [OUT] out 输出取值。
|
||||
* @return 折线覆盖该时刻且取到有效值返回 true。
|
||||
*/
|
||||
inline bool PolylineValueAt(const std::vector<double>& time, const std::vector<double>& values,
|
||||
std::size_t count, const SignalStyle& style, std::size_t firstIndex,
|
||||
double t, double& out)
|
||||
{
|
||||
if (firstIndex == 0 || firstIndex >= count)
|
||||
{
|
||||
// 该时刻之前(或之后)没有采样点,折线并未延伸到这里。
|
||||
return false;
|
||||
}
|
||||
const double t0 = time[firstIndex - 1];
|
||||
const double t1 = time[firstIndex];
|
||||
// 水平镜像时画面上的相邻两点取源数组中头尾对调后的两个采样。
|
||||
const double v0 = values[SourceIndexFor(style, count, firstIndex - 1)];
|
||||
const double v1 = values[SourceIndexFor(style, count, firstIndex)];
|
||||
if (!std::isfinite(t0) || !std::isfinite(t1) || !std::isfinite(v0) || !std::isfinite(v1) ||
|
||||
!(t1 > t0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
double ratio = (t - t0) / (t1 - t0);
|
||||
if (ratio < 0.0)
|
||||
{
|
||||
ratio = 0.0;
|
||||
}
|
||||
else if (ratio > 1.0)
|
||||
{
|
||||
ratio = 1.0;
|
||||
}
|
||||
out = v0 + (v1 - v0) * ratio;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 统计相邻采样对 (k-1, k) 中穿越判定基准的边沿数量。
|
||||
*
|
||||
* `deltaAt(k)` 返回第 k 个采样相对判定基准的偏差;返回非有限值(NaN)表示该采样
|
||||
* 不参与判定,并**打断连续性**(与前一点之间不计边沿)——这正好覆盖
|
||||
* 「采样缺失」与「采样落在统计时间区间之外」两种情况。
|
||||
*
|
||||
* 统计的是采样对 `k ∈ [pairFirst, pairLast]`(闭区间),每对的「前一点」为 `k-1`,
|
||||
* 因此起点对的连续性由 `deltaAt(pairFirst - 1)` 决定(它无效时从下一个有效点重新起算)。
|
||||
*
|
||||
* 该函数是增量统计的基本单位:只要把区间切成互不相交的若干段分别累加(或从总量中减去),
|
||||
* 结果与一次性扫描整段完全一致,因此拖动光标时可以只扫描两端差集。
|
||||
*
|
||||
* @param [IN] pairFirst 起始采样对的下标 k(含),必须 ≥ 1。
|
||||
* @param [IN] pairLast 结束采样对的下标 k(含);小于起点时不做任何事。
|
||||
* @param [IN] deltaAt 偏差取值仿函数。
|
||||
* @param [IN,OUT] rising 累加的上升沿数量。
|
||||
* @param [IN,OUT] falling 累加的下降沿数量。
|
||||
*/
|
||||
template <typename DeltaAt>
|
||||
inline void AccumulateEdgeCrossings(std::size_t pairFirst, std::size_t pairLast, DeltaAt deltaAt,
|
||||
std::size_t& rising, std::size_t& falling)
|
||||
{
|
||||
if (pairFirst == 0 || pairFirst > pairLast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
double previousDelta = deltaAt(pairFirst - 1);
|
||||
bool hasPrevious = std::isfinite(previousDelta);
|
||||
for (std::size_t index = pairFirst; index <= pairLast; ++index)
|
||||
{
|
||||
const double current = deltaAt(index);
|
||||
if (!std::isfinite(current))
|
||||
{
|
||||
hasPrevious = false;
|
||||
continue;
|
||||
}
|
||||
if (hasPrevious)
|
||||
{
|
||||
if (previousDelta < 0.0 && current >= 0.0)
|
||||
{
|
||||
++rising;
|
||||
}
|
||||
else if (previousDelta > 0.0 && current <= 0.0)
|
||||
{
|
||||
++falling;
|
||||
}
|
||||
}
|
||||
previousDelta = current;
|
||||
hasPrevious = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「调整」变换 + 水平镜像的指纹(用于边沿统计缓存键)。
|
||||
*
|
||||
* 边沿判定基于**画出来的曲线**,因此任何一个变换项改变都可能改变计数结果;
|
||||
* 把这些项折叠成一个指纹后,样式一变就能立刻失效缓存项(否则会沿用变换前的旧计数)。
|
||||
*
|
||||
* @param [IN] style 信号外观。
|
||||
* @return 指纹值。
|
||||
*/
|
||||
inline std::uint64_t EdgeStyleKey(const SignalStyle& style)
|
||||
{
|
||||
std::uint64_t key = 1469598103934665603ull;
|
||||
const auto mix = [&key](std::uint64_t value)
|
||||
{
|
||||
key ^= value;
|
||||
key *= 1099511628211ull;
|
||||
};
|
||||
mix(static_cast<std::uint64_t>(
|
||||
std::llround(static_cast<double>(style.gain) * 1.0e6)));
|
||||
mix(static_cast<std::uint64_t>(
|
||||
std::llround(static_cast<double>(style.offsetY) * 1.0e6)));
|
||||
mix(style.absolute ? 1u : 0u);
|
||||
mix(style.mirrorVertical ? 2u : 0u);
|
||||
mix(style.mirrorHorizontal ? 4u : 0u);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算统计区间对应的采样下标区间 [begin, end)。
|
||||
*
|
||||
* 时间轴单调时用二分定位。⚠️ 区间**之外**的采样一律不参与计数,
|
||||
* 因此这里**不做任何外扩**:只有两端都落在 `[begin, end)` 内的采样对才会被累计
|
||||
*(调用方从 `begin + 1` 开始枚举采样对)。
|
||||
*
|
||||
* @param [IN] group 所属数据分组。
|
||||
* @param [IN] count 采样点数量。
|
||||
* @param [IN] hasRange 是否限定时间区间。
|
||||
* @param [IN] rangeMin 区间左端点时间。
|
||||
* @param [IN] rangeMax 区间右端点时间。
|
||||
* @param [OUT] begin 输出区间起点(含)。
|
||||
* @param [OUT] end 输出区间终点(不含)。
|
||||
* @return 计数是否完全由该下标区间决定(时间轴单调时为真)。
|
||||
*/
|
||||
inline bool EdgeIndexRange(const TimeAxis& axis, std::size_t count, bool hasRange, double rangeMin,
|
||||
double rangeMax, std::size_t& begin, std::size_t& end)
|
||||
{
|
||||
begin = 0;
|
||||
end = count;
|
||||
if (!hasRange)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!axis.monotonic)
|
||||
{
|
||||
// 时间轴非单调时无法用下标区间表达统计范围(需逐点按时间过滤)。
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<double>& times = axis.time;
|
||||
const std::size_t timeCount = std::min(times.size(), count);
|
||||
begin = static_cast<std::size_t>(
|
||||
std::lower_bound(times.begin(), times.begin() + static_cast<std::ptrdiff_t>(timeCount),
|
||||
rangeMin) -
|
||||
times.begin());
|
||||
// ⚠️ **不要**在左端多取一个采样点:那会让“跨越区间边界的边沿”也算进来,
|
||||
// 而用户明确要求“在光标选区外的点不要算入计数规则”。
|
||||
// (例:光标恰好落在越过阈值的第一个采样点 B 上时,A→B 这一段处于选区之外,
|
||||
// 不计入;旧实现在左端多取一个点,于是 B 被错认成上升沿。)
|
||||
end = static_cast<std::size_t>(
|
||||
std::upper_bound(times.begin(), times.begin() + static_cast<std::ptrdiff_t>(timeCount),
|
||||
rangeMax) -
|
||||
times.begin());
|
||||
end = std::min(end, timeCount);
|
||||
begin = std::min(begin, end);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline const std::uint64_t kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */
|
||||
inline const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */
|
||||
|
||||
/**
|
||||
* @brief 实验性功能浮动面板的遍历顺序(命中测试与绘制共用,保证层序一致)。
|
||||
*
|
||||
* 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。
|
||||
*/
|
||||
inline const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK};
|
||||
|
||||
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
|
||||
inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
|
||||
|
||||
/**
|
||||
* @brief 计算一条派生曲线在给定采样窗口内的取值(同步路径与后台线程共用)。
|
||||
*
|
||||
* 只拷贝所需的窗口,再调用与「构建为新信号」完全相同的自由函数,因此
|
||||
* 预览曲线与构建出的采样值逐位一致。本函数**只读**入参、不接触任何成员状态,
|
||||
* 因此可以安全地在后台线程调用。
|
||||
*
|
||||
* @param [IN] signal 目标信号(数据只读)。
|
||||
* @param [IN] windowBegin 窗口起点(采样下标)。
|
||||
* @param [IN] windowEnd 窗口终点(不含)。
|
||||
* @param [IN] kind `DerivedCurveKind` 的整型值。
|
||||
* @param [IN] points 均值点数 X。
|
||||
* @param [IN] iterations 迭代次数 N。
|
||||
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
|
||||
* @param [IN] cancel 可选的取消标志(透传给算法,使过时任务尽快退出)。
|
||||
* @param [IN] throttle 是否在阶与阶之间让出 CPU(低内存设备的“少量多次”模式)。
|
||||
* @return 曲线数值(长度 = 有效窗口长度)。
|
||||
*/
|
||||
inline std::vector<float> ComputeDerivedWindow(const SignalData& signal, std::size_t windowBegin,
|
||||
std::size_t windowEnd, int kind, int points,
|
||||
int iterations, int amountPercent,
|
||||
const std::atomic<bool>* cancel, bool throttle)
|
||||
{
|
||||
std::vector<float> out;
|
||||
const std::vector<double>& values = signal.values;
|
||||
const std::size_t hi = std::min(windowEnd, values.size());
|
||||
const std::size_t lo = std::min(windowBegin, hi);
|
||||
if (hi <= lo)
|
||||
{
|
||||
return out;
|
||||
}
|
||||
// ⚠️ **整段窗口不要复制**:`VERY_BIG_DATA` 一条曲线要 8 B/采样 × 2580 万 ≈ 206 MB,
|
||||
// 而“预览曲线一律按整条信号计算”正是最常见的情形。
|
||||
// 采样在本函数执行期间不会被修改(结构变化前宿主会先 `PauseDerivedAsync`),
|
||||
// 因此整段时直接引用原向量;只有子窗口才切片复制。
|
||||
const bool wholeSignal = (lo == 0) && (hi == values.size());
|
||||
const std::vector<double> window = wholeSignal
|
||||
? std::vector<double>()
|
||||
: std::vector<double>(
|
||||
values.begin() + static_cast<std::ptrdiff_t>(lo),
|
||||
values.begin() + static_cast<std::ptrdiff_t>(hi));
|
||||
const std::vector<double>& input = wholeSignal ? values : window;
|
||||
if (kind == static_cast<int>(DerivedCurveKind::WEAK_PEAK))
|
||||
{
|
||||
ComputeWeakPeakAdjust(input, static_cast<double>(amountPercent) / 100.0, iterations, out,
|
||||
cancel, throttle);
|
||||
}
|
||||
else
|
||||
{
|
||||
ComputeMovingAverageIterated(input, points, iterations, out, cancel, throttle);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 包络带的单个像素列(已映射到屏幕坐标)。
|
||||
*/
|
||||
struct BandColumn
|
||||
{
|
||||
float x = 0.0f; /**< 列中心横坐标。 */
|
||||
float top = 0.0f; /**< 本列的**上**包络(较小的纵坐标)。 */
|
||||
float bottom = 0.0f; /**< 本列的**下**包络(较大的纵坐标)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 把逐列极值拼成闭合的包络带多边形。
|
||||
*
|
||||
* 输出格式为「上包络正向 + 下包络反向」,调用方(`PaintLane`)依赖这个顺序把
|
||||
* 它拆成上下两条折线画成一个闭合环。
|
||||
*
|
||||
* ⚠️ **几何必须是精确的**:这里**不做任何**列窗口加宽。曾经为了让填充更快而做过
|
||||
* ±1 列的极值扩展,结果把"梳状"振荡糊成了实心块,波形细节整体丢失
|
||||
* (用户反馈"显示效果没有之前好了")。性能问题改由**绘制侧**解决:把梳状带
|
||||
* 交给无抗锯齿填充(见 `spiky` 输出与 `PlotView::m_fastFrame`)。
|
||||
*
|
||||
* @param [IN] columns 逐列极值(同一段内按列顺序排列)。
|
||||
* @param [OUT] band 追加写入的包络顶点(上包络正向 + 下包络反向)。
|
||||
* @param [OUT] bandBreaks 追加写入的各段起点(相对 band 的下标)。
|
||||
* @param [OUT] spiky 该段是否为「梳状 / 密集满幅」(建议关闭抗锯齿)。
|
||||
* @param [IN] laneHeight 网格的可见像素高度(用于「覆盖率」判据;≤0 时跳过)。
|
||||
*/
|
||||
inline void EmitEnvelopeBand(const std::vector<BandColumn>& columns, std::vector<gd::PointF>& band,
|
||||
std::vector<std::size_t>& bandBreaks, bool& spiky, float laneHeight)
|
||||
{
|
||||
if (columns.size() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::size_t count = columns.size();
|
||||
bandBreaks.push_back(band.size());
|
||||
|
||||
// 「梳状」判定:相邻列的上下包络几乎不相交(振荡周期与像素列宽相当时出现)。
|
||||
// 这种形状下抗锯齿填充的扫描线代价暴涨,而抗锯齿对它的平滑作用几乎看不出来。
|
||||
std::size_t inverted = 0;
|
||||
const float slack = kEnvelopeMinPixelSpan;
|
||||
for (std::size_t index = 1; index < count; ++index)
|
||||
{
|
||||
if (columns[index].top > columns[index - 1].bottom + slack ||
|
||||
columns[index].bottom < columns[index - 1].top - slack)
|
||||
{
|
||||
++inverted;
|
||||
}
|
||||
}
|
||||
if (inverted * 8 > count)
|
||||
{
|
||||
spiky = true;
|
||||
}
|
||||
|
||||
// 「密集满幅」判定:包络带在**每一列内部**都横跨了网格的大部分高度
|
||||
//(编码器方波在 1~2 采样/像素时,多数像素列里都含一次整幅跳变)。
|
||||
// GDI+ **抗锯齿**的代价与这些近竖直、满幅的边成正比:实测同一 4832 顶点的几何
|
||||
// **抗锯齿 694ms、关抗锯齿 29ms(23 倍)**,而这么密的色块上抗锯齿的平滑作用肉眼不可辨。
|
||||
// ⚠️ 只影响「本段是否关抗锯齿」,几何(逐列 min/max)完全不变。
|
||||
if (laneHeight > 0.0f)
|
||||
{
|
||||
std::size_t tallColumns = 0;
|
||||
double covered = 0.0;
|
||||
for (const BandColumn& column : columns)
|
||||
{
|
||||
const float span = column.bottom - column.top;
|
||||
covered += static_cast<double>(span);
|
||||
if (span > 0.5f * laneHeight)
|
||||
{
|
||||
++tallColumns;
|
||||
}
|
||||
}
|
||||
// 「锯齿」判定(最能代表 GDI+ 抗锯齿代价):相邻列的上下包络**整幅跳变**。
|
||||
std::size_t jumps = 0;
|
||||
const float jumpThreshold = 0.25f * laneHeight;
|
||||
for (std::size_t index = 1; index < count; ++index)
|
||||
{
|
||||
const float dTop = std::fabs(columns[index].top - columns[index - 1].top);
|
||||
const float dBottom = std::fabs(columns[index].bottom - columns[index - 1].bottom);
|
||||
if (dTop > jumpThreshold || dBottom > jumpThreshold)
|
||||
{
|
||||
++jumps;
|
||||
}
|
||||
}
|
||||
if (tallColumns * 4 > count || jumps * 4 > count ||
|
||||
covered > 0.45 * static_cast<double>(count) * static_cast<double>(laneHeight))
|
||||
{
|
||||
spiky = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
band.push_back(gd::PointF(columns[index].x, columns[index].top));
|
||||
}
|
||||
for (std::size_t index = count; index-- > 0;)
|
||||
{
|
||||
band.push_back(gd::PointF(columns[index].x, columns[index].bottom));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 双光标差值的完整显示文本("Δt:" + 复合单位间隔)。
|
||||
* @param [IN] seconds 时间间隔(秒)。
|
||||
* @return 形如 "Δt:1h 1min 2s" 的文本。
|
||||
*/
|
||||
inline std::wstring FormatSpanText(double seconds)
|
||||
{
|
||||
return L"Δt:" + FormatDurationCompound(seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 时间光标的读数文本("数值 + s 后缀")。
|
||||
*
|
||||
* 时间轴本身就是秒,但与幅值读数放在一起时没有单位容易误读,
|
||||
* 因此读数恒带 "s" 后缀;内联编辑仍使用不带单位的纯数值(见 BeginEdit 调用处)。
|
||||
*
|
||||
* @param [IN] seconds 光标所在时刻(秒)。
|
||||
* @return 形如 "1.25s" 的文本。
|
||||
*/
|
||||
inline std::wstring FormatCursorTime(double seconds)
|
||||
{
|
||||
return FormatValue(seconds) + L"s";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
|
||||
* @param [IN] argb 原始 0xAARRGGBB 颜色。
|
||||
* @param [IN] alpha 新的 alpha(0-255)。
|
||||
* @return 缩放后的颜色。
|
||||
*/
|
||||
inline std::uint32_t ScaleAlpha(std::uint32_t argb, unsigned int alpha)
|
||||
{
|
||||
const unsigned int a = std::min(255u, alpha);
|
||||
return (argb & 0x00FFFFFFu) | (a << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将数值限制到合法区间。
|
||||
* @param [IN] value 输入值。
|
||||
* @param [IN] low 下限。
|
||||
* @param [IN] high 上限。
|
||||
* @return 限制后的数值。
|
||||
*/
|
||||
inline double ClampDouble(double value, double low, double high)
|
||||
{
|
||||
if (value < low)
|
||||
{
|
||||
return low;
|
||||
}
|
||||
if (value > high)
|
||||
{
|
||||
return high;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 组装图例文本:使用信号当前显示名(用户重命名后立即生效)。
|
||||
* @param [IN] data 信号数据。
|
||||
* @return 图例文本。
|
||||
*/
|
||||
inline std::wstring BuildLegendText(const SignalData& data)
|
||||
{
|
||||
return data.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将信号样式颜色按不透明度换算为实际绘制颜色。
|
||||
* @param [IN] style 信号样式。
|
||||
* @return 叠加不透明度后的 0xAARRGGBB 颜色值。
|
||||
*/
|
||||
inline std::uint32_t StyleColor(const SignalStyle& style)
|
||||
{
|
||||
double opacity = static_cast<double>(style.opacity);
|
||||
if (opacity < 0.0)
|
||||
{
|
||||
opacity = 0.0;
|
||||
}
|
||||
if (opacity > 1.0)
|
||||
{
|
||||
opacity = 1.0;
|
||||
}
|
||||
return WithAlpha(style.color, static_cast<unsigned int>(opacity * 255.0 + 0.5));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 由信号色派生一个「高对比度」颜色:保持色相、大幅拉开明度。
|
||||
*
|
||||
* 用途:「边沿计数(中心轴)」的平均值曲线叠加在原始波形之上,
|
||||
* 若直接用信号色会被波形淹没。这里把信号色向黑或白方向大幅偏移,
|
||||
* 并**同时考虑与原始波形色以及与绘图区底色的距离**,取对比更强的一侧,
|
||||
* 保证曲线既能与波形区分、又能压在底图上看得清。
|
||||
*
|
||||
* 结果是从信号色派生的,因此原信号改颜色时曲线颜色会自动跟着变。
|
||||
*
|
||||
* @param [IN] color 原始信号色(0xAARRGGBB)。
|
||||
* @param [IN] background 绘图区底色,用于评估可见度。
|
||||
* @return 派生出的高对比度颜色(alpha 与输入一致,由调用方再覆盖)。
|
||||
*/
|
||||
inline std::uint32_t HighContrastDerived(std::uint32_t color, std::uint32_t background = 0xFFFDF6E3u)
|
||||
{
|
||||
const auto squaredDistance = [](std::uint32_t a, std::uint32_t b)
|
||||
{
|
||||
const double dr =
|
||||
static_cast<double>((a >> 16) & 0xFFu) - static_cast<double>((b >> 16) & 0xFFu);
|
||||
const double dg =
|
||||
static_cast<double>((a >> 8) & 0xFFu) - static_cast<double>((b >> 8) & 0xFFu);
|
||||
const double db = static_cast<double>(a & 0xFFu) - static_cast<double>(b & 0xFFu);
|
||||
return dr * dr + dg * dg + db * db;
|
||||
};
|
||||
|
||||
const std::uint32_t darker = MixColor(color, 0xFF000000u, 0.72);
|
||||
const std::uint32_t lighter = MixColor(color, 0xFFFFFFFFu, 0.72);
|
||||
// 评分:与原始波形色的区分度为主(权重 1.0),与底色的可见度为辅(权重 0.6)。
|
||||
const double scoreDarker =
|
||||
squaredDistance(darker, color) + 0.6 * squaredDistance(darker, background);
|
||||
const double scoreLighter =
|
||||
squaredDistance(lighter, color) + 0.6 * squaredDistance(lighter, background);
|
||||
return (scoreDarker >= scoreLighter) ? darker : lighter;
|
||||
}
|
||||
/** @brief FNV-1a 混合(面板像素缓存键共用)。 */
|
||||
inline void MixPanelKey(std::uint64_t& hash, std::uint64_t value)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
} // namespace pvdetail
|
||||
|
||||
using namespace pvdetail;
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __PLOT_VIEW_INTERNAL_HPP__
|
||||
|
||||
Reference in new issue
Block a user