From b960b03cb5e52b8740db3635082f3d8e8396b572 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Sat, 10 Oct 2026 20:13:21 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=8C=E9=98=B6=E6=95=B0=20N=E3=80=8D?= =?UTF-8?q?=E6=94=B9=E7=A7=B0=E3=80=8C=E8=BF=AD=E4=BB=A3=E6=AC=A1=E6=95=B0?= =?UTF-8?q?=20N=E3=80=8D=EF=BC=9B=E4=BF=AE=E5=A4=8D=E5=A4=A7=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=94=BE=E5=A4=A7=E5=90=8E=E7=9A=84=E5=B9=B3=E7=A7=BB?= =?UTF-8?q?=E5=8D=A1=E9=A1=BF=E4=B8=8E=E8=B6=8A=E7=95=8C=E4=B8=89=E8=A7=92?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、命名(用户要求) - 实验性功能里的「阶数 N」全部改为「迭代次数 N」(面板滑块标签、均值滤波/中心轴 的数值框标签、构建提示语)。 - 相关标识符与函数参数统一改名:order → iterations(Lane::MeanFilterItem::iterations、 Lane::weakBoostIterations、ComputeWeakWaveBoost/ComputeMovingAverageIterated 的形参、 MeanFilterCurveFor/WeakBoostCurveFor/WeakBoostValueAt/EdgeCenterCountsFor、 各曲线缓存项的字段、EdgePanelLayout::rowIterations/sliderIterations、 PlotEditTarget::MEAN_FILTER_ITERATIONS/EDGE_CENTER_ITERATIONS、 PlotDragMode::BOOST_ITERATIONS、kWeakBoostIterationsMin/Max/Label、 DerivedSignalRequest::iterations 等)。网格拖拽重排里的局部 order 为避免歧义 改名为 laneOrder;`std::memory_order_*`、`rowOrder`→`rowIterations` 一并核对无误。 - 导出信号尾缀统一为「N次迭代」: `<原信号> (N点均值 M次迭代)`、`<原信号> (弱波增强 X% M次迭代)`; 弱波增强此前缺失该信息,现已补上(两种派生信号都恒定带迭代次数)。 状态提示语同步为「… M 次迭代信号。」。 二、越界三角与光标读数的样式统一(用户要求) - 新增 `kMarkerTriangleGap`:网格内数据点越界时,方向三角与数值标注之间**留出间隙** (此前三角与胶囊贴在一起,看起来像胶囊上多出的尖角);中心轴/均值滤波/弱波增强的 派生取值标注同样处理。 - 光标时间读数(以及双光标 Δt 读数)改用与网格内数据点**完全一致**的越界样式: 三角画在胶囊**外侧**、尖端朝外、与胶囊之间留出同样的间隙;为此把读数胶囊在越界一侧 向内让出「三角深度 + 间隙」。旧的"三角压在胶囊上沿"的画法以及为它额外留出的叠放 间距(kCursorTipStackGap)一并删除。 三、大文件放大后"平移 / 改配色非常卡"的根因与修复(用户要求,流畅性优先) 用离屏探针(`.preflight/perf3`)在 VERY_BIG_DATA(1.77GB、2580 万采样 × 5 信号、 1224×796 绘图区)上先量化,实测到的三个根因: 1. **包络带"梳状化"**:当信号的振荡周期与像素列宽度相当时(编码器方波在 2ms/大格下 约 3 个采样点一列),逐列包络会在"全高"与"极窄"之间交替——实测 1224 列里有 988 处 全高跳变。这种多边形让 GDI+ 的扫描线填充耗时暴涨:同一条 2448 顶点的路径 **27.6ms → 1.1ms(±1 列极值扩展后快 25 倍)**。这正是"某些放大等级下平移特别卡" (实测 2ms/大格平移 87ms,其余等级 17~20ms)的原因。 → `EmitEnvelopeBand` 在拼装包络带时对列窗口做 ±1 列的极值扩展(纵向最多各多 1 像素, 肉眼不可辨),一次性消除该病理。 2. **派生预览曲线按折线逐段描边**:均值滤波/弱波增强曲线在可见采样数超过像素列数时 仍按"每 2 像素一个顶点"的抗锯齿折线绘制,而这类曲线常叠在方波上(大量近垂直跳变), 实测单条 12~42ms,5 个网格合计上百毫秒。 → 稠密时改用**逐像素列的包络带**(复用 `EmitEnvelopeBand`)一次 FillPath,实测 0.56ms / 1.53ms(分别快 22 倍 / 28 倍),视觉与细折线一致(列宽就是 1 像素)。 3. **悬停光晕用宽笔描整条折线**:实测悬停一帧多花 35ms。 → 包络带模式下改为"把带整体外扩后填充一次"(`PaintBandHalo`),悬停帧恢复正常。 另外修掉两个相关的固定开销: - `UiMeasureTextWidth` 增加 (字体, 文本) 记忆化缓存:它是布局代码的热点(浮动面板几何 按「网格数 × 面板种类」重复计算,每帧重复测量同一批字符串成百上千次),实测单个面板 的绘制因此从 3.2ms 降到 1ms 量级。字体重建(DPI 变化)时由 `UiClearMeasureCache` 作废。 实测(VERY_BIG_DATA 全选 5 信号,绘图区全重绘 = 平移一帧): - 20ms/大格(用户报告的场景):平移 20.1ms → **11.3ms**; - 2ms/大格(此前最糟):平移 86.7ms → **12.3ms**; - 网格 1 开弱波增强:63.9ms → **17.6ms**;5 个网格全开:136.6ms → **31.9ms**; - 悬停信号:整帧 49.6ms → **15.5ms**(与不悬停持平,光晕几乎免费)。 仍存的已知热点:0.5ms/大格附近(可见约 1000 个采样点、走**逐点描边**路径)平移约 28~39ms —— 该区间采样比像素还稀疏、无法用包络带替代,且用户明确要求保留抗锯齿, 本次未动。 四、验证 - 离屏 harness(窗口 SW_HIDE、不占用鼠标/键盘/屏幕)**355 项检查全部通过**, 新增 `[AD]`:越界读数胶囊向内让位、三角在外侧、三角与胶囊间有真实间隙、 胶囊上方不再有内容;取值标注与三角之间保留 `kMarkerTriangleGap` 的间隙(用 散点模式排除波形干扰后逐像素断言)。`[X]/[Y]/[AA]/[AB]/[Z]` 等既有用例同步通过。 - 性能结论来自 `.preflight/perf3`(离屏探针 + 两套微基准),已随任务清理。 --- Components/Application/Inc/Application.hpp | 12 +- Components/Application/Src/Application.cpp | 40 +- Components/PlotView/Inc/PlotView.hpp | 105 ++-- Components/PlotView/Src/PlotView.cpp | 676 +++++++++++++++------ Utilities/Inc/DataModel.hpp | 30 +- Utilities/Inc/PaintUtil.hpp | 9 + Utilities/Src/DataModel.cpp | 22 +- Utilities/Src/PaintUtil.cpp | 73 ++- 8 files changed, 666 insertions(+), 301 deletions(-) diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index f987829..69cc3f0 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -241,7 +241,7 @@ namespace jdi /** * @brief 应用绘图区提交的「实验性功能 → 构建为新信号」请求。 * - * 为指定网格内的每条被勾选信号各构建一条派生信号(多阶均值滤波或弱波增强), + * 为指定网格内的每条被勾选信号各构建一条派生信号(多次迭代均值滤波或弱波增强), * 命名为 `<原信号名称> (N点均值…)` / `<原信号名称> (弱波增强 M%)`, * 插入到原信号所在组的原信号位置**下方**,并把新信号所在的网格移动到原信号网格的下方。 * @@ -250,20 +250,20 @@ namespace jdi bool ApplyPendingDerivedBuild(); /** - * @brief 派生信号构建请求(多阶均值滤波 / 弱波增强共用同一套插入流程)。 + * @brief 派生信号构建请求(多次迭代均值滤波 / 弱波增强共用同一套插入流程)。 */ struct DerivedSignalRequest { int fileIndex = -1; /**< 原信号所属文件下标。 */ int signalIndex = -1; /**< 原信号下标。 */ - int points = 5; /**< 多阶均值滤波:均值点数 X(滑平半径)。 */ - int order = 1; /**< 多阶均值滤波:阶数 N(迭代次数)。 */ + int points = 5; /**< 多次迭代均值滤波:均值点数 X(滑平半径)。 */ + int iterations = 1; /**< 多次迭代均值滤波:迭代次数 N(至少 1)。 */ int levelPercent = 50; /**< 弱波增强:增强等级(0–100)。 */ - bool weakBoost = false; /**< true = 弱波增强,false = 多阶均值滤波。 */ + bool weakBoost = false; /**< true = 弱波增强,false = 多次迭代均值滤波。 */ }; /** - * @brief 把一条派生信号(多阶均值滤波 / 弱波增强)插入数据层并同步各视图。 + * @brief 把一条派生信号(多次迭代均值滤波 / 弱波增强)插入数据层并同步各视图。 * @param [IN] request 构建请求。 * @param [IN] makeVisible 是否在当前标签页中勾选新信号。 * @return 构建并插入成功返回 true。 diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 78f7b7c..91a955a 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -1473,30 +1473,22 @@ namespace jdi const std::wstring baseName = source.Title(); // 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。 - // 多阶均值滤波形如「<原信号名称> (N点均值×M阶)」, - // 弱波增强形如「<原信号名称> (弱波增强 M%)」。 + // 多次迭代均值滤波形如「<原信号名称> (N点均值 M次迭代)」, + // 弱波增强形如「<原信号名称> (弱波增强 M% N次迭代)」。 std::vector derived_values; SignalData derived; if (request.weakBoost) { ComputeWeakWaveBoost(source.values, static_cast(request.levelPercent) / 100.0, - request.order, derived_values); - derived.name = baseName + L" (弱波增强 " + std::to_wstring(request.levelPercent) + L"%"; - if (request.order > 1) - { - derived.name += L"×" + std::to_wstring(request.order) + L"阶"; - } - derived.name += L")"; + request.iterations, derived_values); + derived.name = baseName + L" (弱波增强 " + std::to_wstring(request.levelPercent) + L"% " + + std::to_wstring(request.iterations) + L"次迭代)"; } else { - ComputeMovingAverageIterated(source.values, request.points, request.order, derived_values); - derived.name = baseName + L" (" + std::to_wstring(request.points) + L"点均值"; - if (request.order > 1) - { - derived.name += L"×" + std::to_wstring(request.order) + L"阶"; - } - derived.name += L")"; + ComputeMovingAverageIterated(source.values, request.points, request.iterations, derived_values); + derived.name = baseName + L" (" + std::to_wstring(request.points) + L"点均值 " + + std::to_wstring(request.iterations) + L"次迭代)"; } // 2. 组装新信号。 @@ -1551,7 +1543,7 @@ namespace jdi request.fileIndex = item.fileIndex; request.signalIndex = item.signalIndex; request.points = item.points; - request.order = item.order; + request.iterations = item.iterations; request.weakBoost = false; requests.push_back(request); } @@ -1568,7 +1560,7 @@ namespace jdi request.fileIndex = item.fileIndex; request.signalIndex = item.signalIndex; request.levelPercent = item.levelPercent; - request.order = item.order; + request.iterations = item.iterations; request.weakBoost = true; requests.push_back(request); } @@ -1611,17 +1603,13 @@ namespace jdi std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 "; if (last.weakBoost) { - text += L"弱波增强 " + std::to_wstring(last.levelPercent) + L"%" + - std::to_wstring(last.order) + L"阶信号。"; + text += L"弱波增强 " + std::to_wstring(last.levelPercent) + L"% " + + std::to_wstring(last.iterations) + L"次迭代信号。"; } else { - text += std::to_wstring(last.points) + L"点均值"; - if (last.order > 1) - { - text += L"×" + std::to_wstring(last.order) + L"阶"; - } - text += L"滤波信号。"; + text += std::to_wstring(last.points) + L"点均值 " + + std::to_wstring(last.iterations) + L"次迭代滤波信号。"; } SetStatusMessage(text, false); } diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 97d214e..dea21b1 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -31,7 +31,7 @@ namespace jdi EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */ EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */ BOOST_LEVEL, /**< 拖动「弱波增强」面板中的等级滑块。 */ - BOOST_ORDER, /**< 拖动「弱波增强」面板中的阶数 N 滑块。 */ + BOOST_ITERATIONS, /**< 拖动「弱波增强」面板中的迭代次数 N 滑块。 */ LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */ }; @@ -57,9 +57,9 @@ namespace jdi LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */ SIGNAL_NAME, /**< 编辑图例中某信号的显示名称。 */ EDGE_CENTER_POINTS, /**< 编辑「中心轴边沿计数」的均值点数 X。 */ - EDGE_CENTER_ORDER, /**< 编辑「中心轴边沿计数」的阶数 N。 */ + EDGE_CENTER_ITERATIONS, /**< 编辑「中心轴边沿计数」的迭代次数 N。 */ MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */ - MEAN_FILTER_ORDER /**< 编辑「均值滤波」的阶数 N。 */ + MEAN_FILTER_ITERATIONS /**< 编辑「均值滤波」的迭代次数 N。 */ }; /** @@ -507,7 +507,7 @@ namespace jdi int fileIndex = -1; /**< 信号所属文件下标。 */ int signalIndex = -1; /**< 文件内信号下标。 */ int points = 5; /**< 均值点数 X(滑平半径)。 */ - int order = 1; /**< 阶数 N(迭代次数)。 */ + int iterations = 1; /**< 迭代次数 N(至少 1)。 */ }; /** @@ -531,7 +531,7 @@ namespace jdi int fileIndex = -1; /**< 信号所属文件下标。 */ int signalIndex = -1; /**< 文件内信号下标。 */ int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ - int order = 1; /**< 阶数 N(1–10)。 */ + int iterations = 1; /**< 迭代次数 N(1–10)。 */ }; /** @@ -572,19 +572,19 @@ namespace jdi double LaneWeakBoostLevel(int laneIndex) const; /** - * @brief 读取某网格的弱波增强阶数 N(1–10)。 + * @brief 读取某网格的弱波增强迭代次数 N(1–10)。 * @param [IN] laneIndex 网格下标。 - * @return 阶数;网格不存在时返回 1。 + * @return 迭代次数;网格不存在时返回 1。 */ - int LaneWeakBoostOrder(int laneIndex) const; + int LaneWeakBoostIterations(int laneIndex) const; /** - * @brief 设置某网格的弱波增强阶数 N(1–10,超出范围会被夹取)。 + * @brief 设置某网格的弱波增强迭代次数 N(1–10,超出范围会被夹取)。 * @param [IN] laneIndex 网格下标。 - * @param [IN] order 阶数。 - * @return 阶数发生变化返回 true。 + * @param [IN] iterations 迭代次数。 + * @return 迭代次数发生变化返回 true。 */ - bool SetLaneWeakBoostOrder(int laneIndex, int order); + bool SetLaneWeakBoostIterations(int laneIndex, int iterations); /** * @brief 设置某网格的弱波增强等级(0–1,超出范围会被夹取)。 @@ -595,26 +595,26 @@ namespace jdi bool SetLaneWeakBoostLevel(int laneIndex, double level); /** - * @brief 获取弱波增强曲线(按「信号 + 增强等级 + 阶数」缓存)。 + * @brief 获取弱波增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。 * @param [IN] signal 目标信号。 * @param [IN] levelPercent 弱波增强等级(0–100,整数百分比)。 - * @param [IN] order 阶数 N(1–10)。 + * @param [IN] iterations 迭代次数 N(1–10)。 * @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。 */ const std::vector& WeakBoostCurveFor(const SignalData& signal, int levelPercent, - int order) const; + int iterations) const; /** * @brief 取某个时刻的弱波增强曲线取值(用于纵向光标的取值标注)。 * @param [IN] file 信号所属文件。 * @param [IN] signal 目标信号。 * @param [IN] levelPercent 弱波增强等级(0–100)。 - * @param [IN] order 阶数 N(1–10)。 + * @param [IN] iterations 迭代次数 N(1–10)。 * @param [IN] time 采样时刻。 * @param [OUT] value 输出取值。 * @return 取到有效值返回 true。 */ - bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int order, + bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int iterations, double time, double& value) const; /** @@ -1084,19 +1084,19 @@ namespace jdi gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */ gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */ gd::RectF slider; /**< 「弱波增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */ - gd::RectF sliderOrder; /**< 「弱波增强」阶数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */ + gd::RectF sliderIterations; /**< 「弱波增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */ int rows = 0; /**< 统计行数(每条可见信号一行)。 */ /** * @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。 * * 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序): - * `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowOrder` 为第二排的两个输入框; + * `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为第二排的两个输入框; * `rowStats` 为第三排的统计结果(中心轴边沿计数专用)。 * 未勾选的条目对应的矩形均为空。 */ std::vector rowChecks; std::vector rowPoints; - std::vector rowOrder; + std::vector rowIterations; std::vector rowStats; std::vector rowRefs; }; @@ -1112,28 +1112,28 @@ namespace jdi /** * @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。 * - * 语义:以「均值点数 X」作滑平半径,对输入重复做 `order` 次滑动平均。 + * 语义:以「均值点数 X」作滑平半径,对输入重复做 `iterations` 次滑动平均。 * 「实验性功能 → 均值滤波」与「中心轴边沿计数」共用本函数,二者曲线因此逐位一致。 * * @param [IN] signal 目标信号。 * @param [IN] points 均值点数 X(滑平半径)。 - * @param [IN] order 阶数 N(迭代次数,至少 1)。 + * @param [IN] iterations 迭代次数 N(至少 1)。 * @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。 */ const std::vector& MeanFilterCurveFor(const SignalData& signal, int points, - int order) const; + int iterations) const; /** - * @brief 取某个时刻的多阶均值滤波曲线取值(用于纵向光标的取值标注)。 + * @brief 取某个时刻的多次迭代均值滤波曲线取值(用于纵向光标的取值标注)。 * @param [IN] file 信号所属文件。 * @param [IN] signal 目标信号。 * @param [IN] points 均值点数 X。 - * @param [IN] order 阶数 N。 + * @param [IN] iterations 迭代次数 N。 * @param [IN] time 采样时刻。 * @param [OUT] value 输出取值。 * @return 取到有效值返回 true。 */ - bool MeanFilterValueAt(const DataFile& file, const SignalData& signal, int points, int order, + bool MeanFilterValueAt(const DataFile& file, const SignalData& signal, int points, int iterations, double time, double& value) const; /** @@ -1155,19 +1155,19 @@ namespace jdi /** * @brief 统计信号穿越“均值滤波曲线”的上/下边沿数量,结果带缓存。 * - * 曲线由「均值点数 X + 阶数 N」确定,与「实验性功能 → 均值滤波」的预览曲线逐位一致。 + * 曲线由「均值点数 X + 迭代次数 N」确定,与「实验性功能 → 均值滤波」的预览曲线逐位一致。 * * @param [IN] file 信号所属文件。 * @param [IN] signal 目标信号。 * @param [IN] points 均值点数 X。 - * @param [IN] order 阶数 N。 + * @param [IN] iterations 迭代次数 N。 * @param [IN] hasRange 是否只统计某个时间区间(双光标模式下为光标区间)。 * @param [IN] rangeMin 区间左端点时间。 * @param [IN] rangeMax 区间右端点时间。 * @param [OUT] rising 上升沿数量(信号从下往上越过均值滤波曲线)。 * @param [OUT] falling 下降沿数量(信号从上往下穿过均值滤波曲线)。 */ - void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points, int order, + void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points, int iterations, bool hasRange, double rangeMin, double rangeMax, std::size_t& rising, std::size_t& falling) const; @@ -1188,7 +1188,7 @@ namespace jdi bool LaneEdgeCenterSignalEnabled(int laneIndex, const SignalRef& ref) const; /** - * @brief 绘制一段派生曲线(中心轴均值滤波 / 多阶均值滤波共用同一实现)。 + * @brief 绘制一段派生曲线(中心轴均值滤波 / 多次迭代均值滤波共用同一实现)。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] resolved 信号视图。 * @param [IN] lane 网格绘制信息。 @@ -1198,15 +1198,15 @@ namespace jdi const std::vector& curve) const; /** - * @brief 绘制某条信号的多阶均值滤波曲线(与中心平均值曲线同一配色派生规则)。 + * @brief 绘制某条信号的多次迭代均值滤波曲线(与中心平均值曲线同一配色派生规则)。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] resolved 信号视图。 * @param [IN] lane 网格绘制信息。 * @param [IN] points 均值点数 X(滑平半径)。 - * @param [IN] order 阶数 N(迭代次数)。 + * @param [IN] iterations 迭代次数 N(至少 1)。 */ void PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, - int points, int order) const; + int points, int iterations) const; /** * @brief 绘制某条信号的弱波增强曲线(与均值滤波曲线同一配色派生规则)。 @@ -1214,10 +1214,10 @@ namespace jdi * @param [IN] resolved 信号视图。 * @param [IN] lane 网格绘制信息。 * @param [IN] levelPercent 弱波增强等级(0–100)。 - * @param [IN] order 阶数 N(1–10)。 + * @param [IN] iterations 迭代次数 N(1–10)。 */ void PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, - int levelPercent, int order) const; + int levelPercent, int iterations) const; /** * @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。 @@ -1483,22 +1483,22 @@ namespace jdi }; mutable std::vector m_edgeCountCache; /**< 边沿统计缓存(纯缓存,逻辑上不影响对象状态)。 */ - /** 多阶均值滤波曲线缓存项(按「信号指针 + 均值点数 + 阶数」标识)。 */ + /** 多次迭代均值滤波曲线缓存项(按「信号指针 + 均值点数 + 迭代次数」标识)。 */ struct MeanCurveEntry { const SignalData* signal = nullptr; int points = 5; - int order = 1; + int iterations = 1; std::vector values; /**< 与 signal->values 等长;缺失处为 NaN。 */ }; mutable std::deque m_meanCurveCache; - /** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比 + 阶数」标识)。 */ + /** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */ struct WeakBoostCurveEntry { const SignalData* signal = nullptr; int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ - int order = 1; /**< 阶数 N(1–10)。 */ + int iterations = 1; /**< 迭代次数 N(1–10)。 */ std::vector values; /**< 与 signal->values 等长;缺失处为 NaN。 */ }; mutable std::deque m_weakBoostCurveCache; @@ -1509,7 +1509,7 @@ namespace jdi const DataFile* file = nullptr; /**< 所属数据分组。 */ const SignalData* signal = nullptr; /**< 目标信号。 */ int points = 5; /**< 均值点数 X。 */ - int order = 1; /**< 阶数 N。 */ + int iterations = 1; /**< 迭代次数 N。 */ bool hasRange = false; /**< 是否限定在时间区间内统计。 */ bool reusable = false; /**< 计数是否完全由 [begin, end) 决定。 */ std::size_t begin = 0; /**< 已统计的采样区间起点(含)。 */ @@ -1713,7 +1713,16 @@ namespace jdi void PaintLegendHighlight(gd::Graphics& g); /** - * @brief 绘制某条信号的悬停/拖拽光晕。 + * @brief 绘制某条信号的悬停/拖拽光晕(按缓存的折线模式选择最省的画法)。 + * @param [IN,OUT] g 绘图上下文。 + * @param [IN] resolved 已解析的信号。 + * @param [IN] cached 该信号的折线缓存项(包络带模式下会走带外扩填充)。 + */ + void PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved, + const PolylineCacheEntry& cached); + + /** + * @brief 绘制某条信号的悬停/拖拽光晕(折线描边版本)。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] resolved 已解析的信号。 * @param [IN] points 折线顶点。 @@ -1722,6 +1731,20 @@ namespace jdi void PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved, const std::vector& points, const std::vector& breaks); + /** + * @brief 稠密包络带的光晕:把带整体外扩 `extra` 像素后填充一次。 + * + * 比用宽画笔描整条折线快一个数量级(含大量近垂直跳变的信号上尤其明显), + * 观感也更均匀。 + * + * @param [IN,OUT] g 绘图上下文。 + * @param [IN] band 包络带顶点(上包络正向 + 下包络反向)。 + * @param [IN] bandBreaks 各段起点。 + * @param [IN] extra 纵向额外外扩量(像素)。 + */ + void PaintBandHalo(gd::Graphics& g, const std::vector& band, + const std::vector& bandBreaks, float extra); + /** * @brief 计算某网格图例中 GX 编号徽标的矩形(绘制与命中测试共用,保证一致)。 * @param [IN] laneIndex 网格下标。 diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index a5ac082..9c573c3 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -35,12 +35,12 @@ namespace jdi /** 越界方向三角沿朝向的深度(与光标把手同比例)。 */ const float kMarkerTriangleDepth = kMarkerTriangleArm * 1.4f; /** - * 视界之外时,堆叠的光标读数之间要留出的间距(逻辑像素)。 + * 越界方向三角与数值/时间标注之间的间隙(逻辑像素)。 * - * 每个读数胶囊上方都要画一个方向三角(深度 = arm * 1.4 = 7), - * 间距若只有 2 像素,三角就会压在相邻胶囊上 —— 因此单独给一个更大的间距。 + * 三角与胶囊之间**必须留出这段空白**:贴在一起时三角看起来像胶囊上多出来的 + * 一个尖角(用户报告的"三角形与数值显示部分没有间隔")。 */ - const float kCursorTipStackGap = kMarkerTriangleArm * 1.4f + 4.0f; + const float kMarkerTriangleGap = 4.0f; const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */ const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */ const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */ @@ -73,10 +73,10 @@ namespace jdi const float kWeakBoostSliderRowHeight = 26.0f; /**< 弱波增强面板中每个滑块行的高度。 */ const float kWeakBoostTrackHeight = 6.0f; /**< 弱波增强滑块的轨道高度。 */ const float kWeakBoostTrackMinWidth = 120.0f; /**< 弱波增强滑块轨道的最小宽度。 */ - const int kWeakBoostOrderMin = 1; /**< 阶数 N 的下限。 */ - const int kWeakBoostOrderMax = 10; /**< 阶数 N 的上限。 */ + const int kWeakBoostIterationsMin = 1; /**< 迭代次数 N 的下限。 */ + const int kWeakBoostIterationsMax = 10; /**< 迭代次数 N 的上限。 */ const wchar_t* const kWeakBoostLevelLabel = L"弱波增强等级"; /**< 等级滑块行左侧的说明文字。 */ - const wchar_t* const kWeakBoostOrderLabel = L"阶数 N"; /**< 阶数滑块行左侧的说明文字。 */ + const wchar_t* const kWeakBoostIterationsLabel = L"迭代次数 N"; /**< 迭代次数滑块行左侧的说明文字。 */ const wchar_t* const kWeakBoostTitle = L"弱波增强"; /**< 弱波增强面板标题。 */ const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱波增强共用)。 */ const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */ @@ -267,6 +267,83 @@ namespace jdi /** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */ const int kEdgePanelKindCount = static_cast(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0])); + /** + * @brief 包络带的单个像素列(已映射到屏幕坐标)。 + */ + struct BandColumn + { + float x = 0.0f; /**< 列中心横坐标。 */ + float top = 0.0f; /**< 本列的**上**包络(较小的纵坐标)。 */ + float bottom = 0.0f; /**< 本列的**下**包络(较大的纵坐标)。 */ + }; + + /** + * @brief 把逐列极值拼成闭合的包络带多边形,并做 ±1 列的极值扩展。 + * + * 输出格式为「上包络正向 + 下包络反向」,调用方(`PaintLane`)依赖这个顺序把 + * 它拆成上下两条折线画成一个闭合环。 + * + * **±1 列的极值扩展是性能关键,不是美观偏好**: + * 当信号的振荡周期与像素列宽度相当时(例如 VERY_BIG_DATA 的编码器方波在 + * 2ms/大格下约 3 个采样点一列),同一列可能只落在高电平或只落在低电平, + * 于是逐列包络会在"全高"与"极窄"之间交替,形成大量 1px 宽的全高尖刺 + * (实测 1224 列里有 988 处全高跳变)。这种"梳状"多边形会让 GDI+ 的 + * 扫描线填充耗时暴涨(实测同一条 2448 顶点的路径:27.6ms → 1.1ms,快 25 倍)。 + * 扩展窗口只让包络在**纵向上最多各多出 1 像素**,肉眼看不出差别, + * 但它把尖刺合并成连续块,使填充耗时回到正常水平。 + * + * @param [IN] columns 逐列极值(同一段内按列顺序排列)。 + * @param [OUT] band 追加写入的包络顶点(上包络正向 + 下包络反向)。 + * @param [OUT] bandBreaks 追加写入的各段起点(相对 band 的下标)。 + */ + void EmitEnvelopeBand(const std::vector& columns, std::vector& band, + std::vector& bandBreaks) + { + if (columns.size() < 2) + { + return; + } + const std::size_t count = columns.size(); + bandBreaks.push_back(band.size()); + + // 上包络:窗口内取**较小**的纵坐标(更靠上);下包络:取较大的纵坐标。 + const auto widenedTop = [&columns, count](std::size_t index) + { + float value = columns[index].top; + if (index > 0) + { + value = std::min(value, columns[index - 1].top); + } + if (index + 1 < count) + { + value = std::min(value, columns[index + 1].top); + } + return value; + }; + const auto widenedBottom = [&columns, count](std::size_t index) + { + float value = columns[index].bottom; + if (index > 0) + { + value = std::max(value, columns[index - 1].bottom); + } + if (index + 1 < count) + { + value = std::max(value, columns[index + 1].bottom); + } + return value; + }; + + for (std::size_t index = 0; index < count; ++index) + { + band.push_back(gd::PointF(columns[index].x, widenedTop(index))); + } + for (std::size_t index = count; index-- > 0;) + { + band.push_back(gd::PointF(columns[index].x, widenedBottom(index))); + } + } + /** * @brief 时间间隔的显示单位。 */ @@ -1391,6 +1468,18 @@ namespace jdi // 时间读数改到绘图区**底部**:顶部留给网格图例与幅值标尺,避免互相遮挡。 float left = CursorDisplayX(index) - width * 0.5f; left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width)); + + // 光标滞留在视界之外时,胶囊外侧还要放一个方向三角(与网格内数据点越界同一套画法): + // 把胶囊整体**向内**让出三角 + 间隙的宽度,否则贴边时三角没有落脚之处、 + // 只能压在胶囊上(用户报告的"没有间隔、样式不统一")。 + bool offRight = false; + if (CursorOffViewSide(index, offRight)) + { + const float reserved = (kMarkerTriangleDepth + kMarkerTriangleGap) * m_scale; + left = offRight ? (left - reserved) : (left + reserved); + left = std::max(m_plotRect.X, std::min(left, m_plotRect.X + m_plotRect.Width - width)); + } + const float top = m_plotRect.Y + m_plotRect.Height - height - 2.0f * m_scale; return gd::RectF(left, top, width, height); @@ -1405,12 +1494,8 @@ namespace jdi { const gd::RectF first = rawRect(1); const gd::RectF second = rawRect(2); - // 光标滞留在视界之外时,读数胶囊上方还要放一个方向三角; - // 叠放间距必须容得下它,否则三角会压在相邻胶囊上(看起来很脏)。 - bool offRightA = false; - bool offRightB = false; - const bool offViewAny = CursorOffViewSide(1, offRightA) || CursorOffViewSide(2, offRightB); - const float gap = offViewAny ? kCursorTipStackGap * m_scale : 2.0f * m_scale; + // 方向三角已移到胶囊**外侧**,胶囊之间不再需要额外留白。 + const float gap = 2.0f * m_scale; const bool overlapX = (first.X < second.X + second.Width + gap) && (second.X < first.X + first.Width + gap); if (overlapX) @@ -1755,29 +1840,29 @@ namespace jdi return m_tab->Lanes()[static_cast(laneIndex)].weakBoostLevel; } - int PlotView::LaneWeakBoostOrder(int laneIndex) const + int PlotView::LaneWeakBoostIterations(int laneIndex) const { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { - return kWeakBoostOrderMin; + return kWeakBoostIterationsMin; } - return m_tab->Lanes()[static_cast(laneIndex)].weakBoostOrder; + return m_tab->Lanes()[static_cast(laneIndex)].weakBoostIterations; } - bool PlotView::SetLaneWeakBoostOrder(int laneIndex, int order) + bool PlotView::SetLaneWeakBoostIterations(int laneIndex, int iterations) { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return false; } Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; - const int clamped = std::max(kWeakBoostOrderMin, std::min(kWeakBoostOrderMax, order)); - if (lane.weakBoostOrder == clamped) + const int clamped = std::max(kWeakBoostIterationsMin, std::min(kWeakBoostIterationsMax, iterations)); + if (lane.weakBoostIterations == clamped) { return false; } - lane.weakBoostOrder = clamped; - // 阶数变化会改变预览曲线:清掉曲线缓存并让场景缓存失效。 + lane.weakBoostIterations = clamped; + // 迭代次数变化会改变预览曲线:清掉曲线缓存并让场景缓存失效。 m_weakBoostCurveCache.clear(); InvalidateSceneCache(); return true; @@ -1964,7 +2049,7 @@ namespace jdi return true; } - bool PlotView::MeanFilterValueAt(const DataFile& file, const SignalData& signal, int points, int order, + bool PlotView::MeanFilterValueAt(const DataFile& file, const SignalData& signal, int points, int iterations, double time, double& value) const { const int index = FindNearestSample(file, time); @@ -1972,7 +2057,7 @@ namespace jdi { return false; } - const std::vector& curve = MeanFilterCurveFor(signal, points, order); + const std::vector& curve = MeanFilterCurveFor(signal, points, iterations); if (static_cast(index) >= curve.size()) { return false; @@ -1987,13 +2072,13 @@ namespace jdi } const std::vector& PlotView::MeanFilterCurveFor(const SignalData& signal, int points, - int order) const + int iterations) const { const int radius = std::max(0, points); - const int stages = std::max(1, order); + const int stages = std::max(1, iterations); for (const MeanCurveEntry& entry : m_meanCurveCache) { - if (entry.signal == &signal && entry.points == radius && entry.order == stages) + if (entry.signal == &signal && entry.points == radius && entry.iterations == stages) { return entry.values; } @@ -2002,7 +2087,7 @@ namespace jdi MeanCurveEntry entry; entry.signal = &signal; entry.points = radius; - entry.order = stages; + entry.iterations = stages; // 与「构建为新信号」共用同一实现(Utilities 内的自由函数), // 因此预览曲线与构建出的采样值逐位一致。 ComputeMovingAverageIterated(signal.values, radius, stages, entry.values); @@ -2012,13 +2097,13 @@ namespace jdi } const std::vector& PlotView::WeakBoostCurveFor(const SignalData& signal, - int levelPercent, int order) const + int levelPercent, int iterations) const { const int level = std::max(0, std::min(100, levelPercent)); - const int stages = std::max(kWeakBoostOrderMin, std::min(kWeakBoostOrderMax, order)); + const int stages = std::max(kWeakBoostIterationsMin, std::min(kWeakBoostIterationsMax, iterations)); for (const WeakBoostCurveEntry& entry : m_weakBoostCurveCache) { - if (entry.signal == &signal && entry.levelPercent == level && entry.order == stages) + if (entry.signal == &signal && entry.levelPercent == level && entry.iterations == stages) { return entry.values; } @@ -2027,7 +2112,7 @@ namespace jdi WeakBoostCurveEntry entry; entry.signal = &signal; entry.levelPercent = level; - entry.order = stages; + entry.iterations = stages; // 与「构建为新信号」共用同一实现(Utilities 内的自由函数), // 因此预览曲线与构建出的采样值逐位一致。 ComputeWeakWaveBoost(signal.values, static_cast(level) / 100.0, stages, entry.values); @@ -2037,14 +2122,14 @@ namespace jdi } bool PlotView::WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, - int order, double time, double& value) const + int iterations, double time, double& value) const { const int index = FindNearestSample(file, time); if (index < 0) { return false; } - const std::vector& curve = WeakBoostCurveFor(signal, levelPercent, order); + const std::vector& curve = WeakBoostCurveFor(signal, levelPercent, iterations); if (static_cast(index) >= curve.size()) { return false; @@ -2059,14 +2144,14 @@ namespace jdi } void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points, - int order, bool hasRange, double rangeMin, double rangeMax, + int iterations, bool hasRange, double rangeMin, double rangeMax, std::size_t& rising, std::size_t& falling) const { rising = 0; falling = 0; const int radius = std::max(0, points); - const int stages = std::max(1, order); + const int stages = std::max(1, iterations); // 与「实验性功能 → 均值滤波」共用同一条曲线(含缓存)。 const std::vector& curve = MeanFilterCurveFor(signal, radius, stages); const std::size_t count = std::min(curve.size(), signal.values.size()); @@ -2099,14 +2184,14 @@ namespace jdi return value - static_cast(center); }; - // 按「文件 + 信号 + 均值点数 X + 阶数 N + 是否限定区间」定位缓存项 + // 按「文件 + 信号 + 均值点数 X + 迭代次数 N + 是否限定区间」定位缓存项 //(**时间区间不进缓存键**)。 EdgeCenterCountEntry* entry = nullptr; for (EdgeCenterCountEntry& candidate : m_edgeCenterCountCache) { if (candidate.file == &file && candidate.signal == &signal && candidate.hasRange == hasRange && candidate.points == radius && - candidate.order == stages) + candidate.iterations == stages) { entry = &candidate; break; @@ -2122,7 +2207,7 @@ namespace jdi created.file = &file; created.signal = &signal; created.points = radius; - created.order = stages; + created.iterations = stages; created.hasRange = hasRange; m_edgeCenterCountCache.push_back(created); entry = &m_edgeCenterCountCache.back(); @@ -2340,7 +2425,7 @@ namespace jdi { continue; } - EdgeCenterCountsFor(*resolved.file, *resolved.data, item->points, item->order, + EdgeCenterCountsFor(*resolved.file, *resolved.data, item->points, item->iterations, EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling); } else @@ -2358,9 +2443,9 @@ namespace jdi if (centerAxis || meanFilter) { // 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。 - // (「均值点数 X」/「阶数 N」的布局对两个逐条目面板通用:标签与数值框各占半行。) + // (「均值点数 X」/「迭代次数 N」的布局对两个逐条目面板通用:标签与数值框各占半行。) const float labelX = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X"); - const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"阶数 N"); + const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"迭代次数 N"); const float fieldWidth = std::max(kEdgePanelEditFieldWidth * m_scale * 0.75f, UiMeasureTextWidth(m_fonts->monoSmall, L"1000000") + 18.0f * m_scale); @@ -2371,9 +2456,9 @@ namespace jdi if (weakBoost) { - // 两个滑块行(等级 / 阶数 N)= 「说明文字 + 轨道 + 数值」。 + // 两个滑块行(等级 / 迭代次数 N)= 「说明文字 + 轨道 + 数值」。 const float labelWidth = std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel), - UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostOrderLabel)); + UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostIterationsLabel)); const float valueWidth = std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100%"), UiMeasureTextWidth(m_fonts->monoSmall, L"10")); const float rowWidth = labelWidth + 6.0f * m_scale + kWeakBoostTrackMinWidth * m_scale + @@ -2906,11 +2991,11 @@ namespace jdi } if (m_editTarget == PlotEditTarget::EDGE_CENTER_POINTS || - m_editTarget == PlotEditTarget::EDGE_CENTER_ORDER || + m_editTarget == PlotEditTarget::EDGE_CENTER_ITERATIONS || m_editTarget == PlotEditTarget::MEAN_FILTER_POINTS || - m_editTarget == PlotEditTarget::MEAN_FILTER_ORDER) + m_editTarget == PlotEditTarget::MEAN_FILTER_ITERATIONS) { - // 「均值点数 X」/「阶数 N」:输入框就落在对应的字段矩形上 + // 「均值点数 X」/「迭代次数 N」:输入框就落在对应的字段矩形上 //(中心轴与均值滤波面板的字段矩形含义相同)。 if (m_editFieldRect.Width <= 0.0f) { @@ -3077,11 +3162,11 @@ namespace jdi return true; } - // 「均值点数 X」/「阶数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。 + // 「均值点数 X」/「迭代次数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。 // 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用于自己的条目列表。 const bool centerTarget = - (target == PlotEditTarget::EDGE_CENTER_POINTS || target == PlotEditTarget::EDGE_CENTER_ORDER); - if (target == PlotEditTarget::MEAN_FILTER_POINTS || target == PlotEditTarget::MEAN_FILTER_ORDER || + (target == PlotEditTarget::EDGE_CENTER_POINTS || target == PlotEditTarget::EDGE_CENTER_ITERATIONS); + if (target == PlotEditTarget::MEAN_FILTER_POINTS || target == PlotEditTarget::MEAN_FILTER_ITERATIONS || centerTarget) { Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; @@ -3095,7 +3180,7 @@ namespace jdi } const int maximum = isPoints ? 1000000 : 1000; int value = static_cast(std::lround(parsed)); - // 阶数至少为 1(1 阶等于不做滤波);均值点数可以为 0。 + // 迭代次数至少为 1(1 次迭代等于不做滤波);均值点数可以为 0。 value = isPoints ? std::max(0, std::min(value, maximum)) : std::max(1, std::min(value, maximum)); @@ -3107,7 +3192,7 @@ namespace jdi } else { - item.order = value; + item.iterations = value; } // 参数变化会改变曲线:清掉派生缓存后重绘。 m_meanCurveCache.clear(); @@ -3312,8 +3397,8 @@ namespace jdi case PlotDragMode::BOOST_LEVEL: // 「弱波增强」等级滑块拖拽 → 左右箭头。 return LoadCursorW(nullptr, IDC_SIZEWE); - case PlotDragMode::BOOST_ORDER: - // 「弱波增强」阶数 N 滑块拖拽 → 左右箭头。 + case PlotDragMode::BOOST_ITERATIONS: + // 「弱波增强」迭代次数 N 滑块拖拽 → 左右箭头。 return LoadCursorW(nullptr, IDC_SIZEWE); case PlotDragMode::MOVE_SIGNAL: // 拖动信号(参考 Photoshop 平移元素的抓握光标)。 @@ -3363,10 +3448,10 @@ namespace jdi if (kind == EdgePanelKind::BOOST && (WeakBoostSliderHit(layout.slider, m_scale, static_cast(x), static_cast(y)) || - WeakBoostSliderHit(layout.sliderOrder, m_scale, static_cast(x), + WeakBoostSliderHit(layout.sliderIterations, m_scale, static_cast(x), static_cast(y)))) { - // 等级 / 阶数滑块:左右箭头表示可左右拖动。 + // 等级 / 迭代次数滑块:左右箭头表示可左右拖动。 return LoadCursorW(nullptr, IDC_SIZEWE); } if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::BOOST) && @@ -3636,23 +3721,23 @@ namespace jdi m_dragMoved = true; return true; } - case PlotDragMode::BOOST_ORDER: + case PlotDragMode::BOOST_ITERATIONS: { - // 「弱波增强」阶数 N 滑块:按滑轨上的横向位置换算 1–10(整数)。 + // 「弱波增强」迭代次数 N 滑块:按滑轨上的横向位置换算 1–10(整数)。 if (m_tab == nullptr || m_boostDragLane < 0 || m_boostDragLane >= static_cast(m_lanes.size())) { break; } const EdgePanelLayout layout = EdgePanelLayoutFor(m_boostDragLane, EdgePanelKind::BOOST); - if (layout.sliderOrder.Width <= 0.0f) + if (layout.sliderIterations.Width <= 0.0f) { break; } - const double ratio = UiSliderRatioFromX(layout.sliderOrder, fx); - const int order = kWeakBoostOrderMin + static_cast(std::lround( - ratio * (kWeakBoostOrderMax - kWeakBoostOrderMin))); - SetLaneWeakBoostOrder(m_boostDragLane, order); + const double ratio = UiSliderRatioFromX(layout.sliderIterations, fx); + const int iterations = kWeakBoostIterationsMin + static_cast(std::lround( + ratio * (kWeakBoostIterationsMax - kWeakBoostIterationsMin))); + SetLaneWeakBoostIterations(m_boostDragLane, iterations); m_dragMoved = true; return true; } @@ -3781,7 +3866,7 @@ namespace jdi changed = true; } - // ✕ 关闭按钮 / 等级与阶数滑块 / 底部按钮的悬停提示都直接依赖鼠标坐标, + // ✕ 关闭按钮 / 等级与迭代次数滑块 / 底部按钮的悬停提示都直接依赖鼠标坐标, // 但它们在面板内部移动时上面这些状态可能毫无变化(例如从同一网格的波形移到面板上), // 于是返回值恒为 false → 宿主不重绘 → **悬停提示不会出现**。 // 因此在“鼠标进入 / 离开 / 在面板内移动”时强制标记需要重绘。 @@ -3996,15 +4081,15 @@ namespace jdi SetLaneWeakBoostLevel(index, UiSliderRatioFromX(layout.slider, fx)); return true; } - if (boostMode && WeakBoostSliderHit(layout.sliderOrder, m_scale, fx, fy)) + if (boostMode && WeakBoostSliderHit(layout.sliderIterations, m_scale, fx, fy)) { - m_dragMode = PlotDragMode::BOOST_ORDER; + m_dragMode = PlotDragMode::BOOST_ITERATIONS; m_boostDragLane = index; - const double ratio = UiSliderRatioFromX(layout.sliderOrder, fx); - SetLaneWeakBoostOrder(index, - kWeakBoostOrderMin + + const double ratio = UiSliderRatioFromX(layout.sliderIterations, fx); + SetLaneWeakBoostIterations(index, + kWeakBoostIterationsMin + static_cast(std::lround( - ratio * (kWeakBoostOrderMax - kWeakBoostOrderMin)))); + ratio * (kWeakBoostIterationsMax - kWeakBoostIterationsMin)))); return true; } @@ -4040,7 +4125,7 @@ namespace jdi m_dragMode = PlotDragMode::NONE; return true; } - // 第二排的两个输入框 → 就地编辑该信号的均值点数 X / 阶数 N + // 第二排的两个输入框 → 就地编辑该信号的均值点数 X / 迭代次数 N //(弱波增强面板没有参数输入框,等级由滑块统一控制)。 if (layout.rowPoints[rowIndex].Width > 0.0f && UiPointInRect(layout.rowPoints[rowIndex], fx, fy)) @@ -4053,15 +4138,15 @@ namespace jdi layout.rowPoints[rowIndex]); return true; } - if (layout.rowOrder[rowIndex].Width > 0.0f && - UiPointInRect(layout.rowOrder[rowIndex], fx, fy)) + if (layout.rowIterations[rowIndex].Width > 0.0f && + UiPointInRect(layout.rowIterations[rowIndex], fx, fy)) { const Lane::MeanFilterItem* item = findItem(); m_editMeanRef = ref; - BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ORDER - : PlotEditTarget::EDGE_CENTER_ORDER, - index, std::to_wstring(item != nullptr ? item->order : 1), - layout.rowOrder[rowIndex]); + BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ITERATIONS + : PlotEditTarget::EDGE_CENTER_ITERATIONS, + index, std::to_wstring(item != nullptr ? item->iterations : 1), + layout.rowIterations[rowIndex]); return true; } } @@ -4069,12 +4154,12 @@ namespace jdi if (boostMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy)) { // 「弱波增强 → 构建为新信号」:与均值滤波同一套请求机制 - //(等级与阶数属于整个网格,网格内各被勾选信号共用同一组参数)。 + //(等级与迭代次数属于整个网格,网格内各被勾选信号共用同一组参数)。 const Lane& lane = m_tab->Lanes()[static_cast(index)]; const int levelPercent = static_cast( std::lround(std::max(0.0, std::min(1.0, lane.weakBoostLevel)) * 100.0)); - const int order = std::max(kWeakBoostOrderMin, - std::min(kWeakBoostOrderMax, lane.weakBoostOrder)); + const int iterations = std::max(kWeakBoostIterationsMin, + std::min(kWeakBoostIterationsMax, lane.weakBoostIterations)); m_pendingWeakBoostItems.clear(); for (const SignalRef& ref : lane.signals) { @@ -4091,13 +4176,13 @@ namespace jdi build.fileIndex = ref.fileIndex; build.signalIndex = ref.signalIndex; build.levelPercent = levelPercent; - build.order = order; + build.iterations = iterations; m_pendingWeakBoostItems.push_back(build); } if (!m_pendingWeakBoostItems.empty()) { SetHint(L"正在构建弱波增强信号(等级 " + std::to_wstring(levelPercent) + - L"% / " + std::to_wstring(order) + L" 阶,共 " + + L"% / " + std::to_wstring(iterations) + L" 次迭代,共 " + std::to_wstring(m_pendingWeakBoostItems.size()) + L" 条)…"); m_pendingWeakBoostLane = index; } @@ -4127,14 +4212,14 @@ namespace jdi build.fileIndex = ref.fileIndex; build.signalIndex = ref.signalIndex; build.points = item->points; - build.order = item->order; + build.iterations = item->iterations; m_pendingMeanFilterItems.push_back(build); } if (!m_pendingMeanFilterItems.empty()) { const MeanFilterBuild& first = m_pendingMeanFilterItems.front(); SetHint(L"正在构建均值滤波信号(" + std::to_wstring(first.points) + - L" 点 " + std::to_wstring(first.order) + L" 阶,共 " + + L" 点 " + std::to_wstring(first.iterations) + L" 次迭代,共 " + std::to_wstring(m_pendingMeanFilterItems.size()) + L" 条)…"); m_pendingMeanFilterLane = index; } @@ -4650,28 +4735,11 @@ namespace jdi bandMode = true; const float strokeHalf = std::max(1.0f, resolved.style->lineWidth) * m_scale * 0.5f; - std::vector segmentTop; - std::vector segmentBottom; - const auto flushBandSegment = [&band, &bandBreaks, &segmentTop, &segmentBottom]() + std::vector segmentColumns; + const auto flushBandSegment = [&band, &bandBreaks, &segmentColumns]() { - if (segmentTop.size() < 2) - { - segmentTop.clear(); - segmentBottom.clear(); - return; - } - bandBreaks.push_back(band.size()); - for (const gd::PointF& point : segmentTop) - { - band.push_back(point); - } - // 下包络反向,形成闭合环。 - for (std::size_t index = segmentBottom.size(); index-- > 0;) - { - band.push_back(segmentBottom[index]); - } - segmentTop.clear(); - segmentBottom.clear(); + EmitEnvelopeBand(segmentColumns, band, bandBreaks); + segmentColumns.clear(); }; std::size_t searchFrom = begin; @@ -4806,8 +4874,11 @@ namespace jdi static_cast(ClampDouble(ValueToY(low, lane), limitTop, limitBottom)); // 包络带每列都记录(上下各外扩半个线宽,保证细波动也有可见粗细)。 - segmentTop.push_back(gd::PointF(x, yTop - strokeHalf)); - segmentBottom.push_back(gd::PointF(x, yBottom + strokeHalf)); + BandColumn bandColumn; + bandColumn.x = x; + bandColumn.top = yTop - strokeHalf; + bandColumn.bottom = yBottom + strokeHalf; + segmentColumns.push_back(bandColumn); if (yBottom - yTop < kEnvelopeMinPixelSpan) { @@ -5013,6 +5084,22 @@ namespace jdi paintOverlays(); } + void PlotView::PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved, + const PolylineCacheEntry& cached) + { + if (resolved.style == nullptr) + { + return; + } + if (cached.bandMode && !cached.bandBreaks.empty()) + { + // 稠密包络带:直接用带的外扩填充(比宽笔描折线快一个数量级)。 + PaintBandHalo(g, cached.band, cached.bandBreaks, 2.0f * m_scale); + return; + } + PaintSignalHalo(g, resolved, cached.points, cached.breaks); + } + void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved, const std::vector& points, const std::vector& breaks) @@ -5041,6 +5128,53 @@ namespace jdi } } + void PlotView::PaintBandHalo(gd::Graphics& g, const std::vector& band, + const std::vector& bandBreaks, float extra) + { + if (bandBreaks.empty() || extra <= 0.0f) + { + return; + } + // 稠密波形(包络带)的光晕:把带**整体外扩** `extra` 像素再填充一次, + // 而不是用宽画笔描整条折线 —— 方波 / 编码器这类含大量近垂直跳变的信号上, + // GDI+ 抗锯齿宽折线的耗时与降级后的顶点数不成比例(实测单条光晕 35ms), + // 而一次 FillPath 只要 1ms 上下,观感也更均匀。 + gd::GraphicsPath path; + path.SetFillMode(gd::FillModeWinding); + for (std::size_t k = 0; k < bandBreaks.size(); ++k) + { + const std::size_t start = bandBreaks[k]; + const std::size_t stop = (k + 1 < bandBreaks.size()) ? bandBreaks[k + 1] : band.size(); + const std::size_t total = stop - start; + if (total < 4 || (total % 2) != 0) + { + continue; + } + const std::size_t half = total / 2; + std::vector expanded; + expanded.reserve(total); + for (std::size_t index = 0; index < half; ++index) + { + const gd::PointF& point = band[start + index]; + expanded.push_back(gd::PointF(point.X, point.Y - extra)); + } + for (std::size_t index = half; index < total; ++index) + { + const gd::PointF& point = band[start + index]; + expanded.push_back(gd::PointF(point.X, point.Y + extra)); + } + path.StartFigure(); + path.AddLines(expanded.data(), static_cast(expanded.size())); + path.CloseFigure(); + } + if (path.GetPointCount() == 0) + { + return; + } + gd::SolidBrush brush(ToGdiColor(WithAlpha(m_theme->accent, 0x66))); + g.FillPath(&brush, &path); + } + void PlotView::PaintHoverHighlight(gd::Graphics& g) { if (m_tab == nullptr || m_dragMode != PlotDragMode::NONE) @@ -5080,7 +5214,7 @@ namespace jdi const PolylineCacheEntry* cached = AcquirePolyline(m_hoverSignal, resolved, lane); if (cached != nullptr) { - PaintSignalHalo(g, resolved, cached->points, cached->breaks); + PaintSignalHaloFor(g, resolved, *cached); } } } @@ -5121,7 +5255,7 @@ namespace jdi const LaneVisual& lane = m_lanes[static_cast(laneIndex)]; if (const PolylineCacheEntry* cached = AcquirePolyline(m_hoverLegend, resolved, lane)) { - PaintSignalHalo(g, resolved, cached->points, cached->breaks); + PaintSignalHaloFor(g, resolved, *cached); } } @@ -5236,7 +5370,7 @@ namespace jdi mixFloat(static_cast(visual.yMax)); const std::vector& signals = Tab().Lanes()[laneIndex].signals; - // 「边沿计数(中心轴)」的开关与每个被勾选信号各自的均值点数 X / 阶数 N + // 「边沿计数(中心轴)」的开关与每个被勾选信号各自的均值点数 X / 迭代次数 N // 会改变网格内的曲线绘制 → 参与指纹。 mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull); mix(static_cast(Tab().Lanes()[laneIndex].edgeCenterItems.size())); @@ -5245,9 +5379,9 @@ namespace jdi mix(static_cast(item.ref.fileIndex)); mix(static_cast(item.ref.signalIndex)); mix(static_cast(item.points)); - mix(static_cast(item.order)); + mix(static_cast(item.iterations)); } - // 「均值滤波」的开关、每个被勾选信号各自的均值点数 X 与阶数 N 同样会改变 + // 「均值滤波」的开关、每个被勾选信号各自的均值点数 X 与迭代次数 N 同样会改变 // 网格内的曲线绘制 → 参与指纹(面板高度也随勾选数量变化)。 mix(Tab().Lanes()[laneIndex].meanFilter ? 1ull : 0ull); mix(static_cast(Tab().Lanes()[laneIndex].meanFilterItems.size())); @@ -5256,12 +5390,12 @@ namespace jdi mix(static_cast(item.ref.fileIndex)); mix(static_cast(item.ref.signalIndex)); mix(static_cast(item.points)); - mix(static_cast(item.order)); + mix(static_cast(item.iterations)); } - // 「弱波增强」的开关、增强等级、阶数 N 与每个被勾选信号的条目同理。 + // 「弱波增强」的开关、增强等级、迭代次数 N 与每个被勾选信号的条目同理。 mix(Tab().Lanes()[laneIndex].weakBoost ? 1ull : 0ull); mixFloat(static_cast(Tab().Lanes()[laneIndex].weakBoostLevel)); - mix(static_cast(Tab().Lanes()[laneIndex].weakBoostOrder)); + mix(static_cast(Tab().Lanes()[laneIndex].weakBoostIterations)); mix(static_cast(Tab().Lanes()[laneIndex].weakBoostItems.size())); for (const Lane::WeakBoostItem& item : Tab().Lanes()[laneIndex].weakBoostItems) { @@ -5632,20 +5766,20 @@ namespace jdi // 逐网格把缓存中该网格的横向条带按位移贴到屏幕上(非常廉价,整幅波形不必重绘)。 // 被拖拽的网格最后贴,因此始终压在最上层,形成"被拎起来"的观感。 - std::vector order; + std::vector laneOrder; for (int index = 0; index < static_cast(m_lanes.size()); ++index) { if (index != m_laneDragFrom) { - order.push_back(index); + laneOrder.push_back(index); } } if (m_laneDragFrom < static_cast(m_lanes.size())) { - order.push_back(m_laneDragFrom); + laneOrder.push_back(m_laneDragFrom); } - for (const int index : order) + for (const int index : laneOrder) { const gd::RectF& rect = m_lanes[static_cast(index)].rect; const float offset = LaneDragOffset(index); @@ -6089,10 +6223,10 @@ namespace jdi // 只有拖拽中的信号光晕可以进入静态场景缓存(悬停光晕见 PaintHoverHighlight)。 const SignalRef haloRef = SceneHaloRef(); // 「边沿计数(中心轴)」开启时,为**被勾选**的信号各叠加一条均值滤波曲线 - //(参数 = 该信号自己的条目:均值点数 X + 阶数 N,与「均值滤波」完全同一套语义)。 + //(参数 = 该信号自己的条目:均值点数 X + 迭代次数 N,与「均值滤波」完全同一套语义)。 const bool centerOn = Tab().Lanes()[static_cast(laneIndex)].edgeCenter; - // 「均值滤波」开启时,为**被勾选**的信号各叠加一条多阶滑动平均曲线 - //(均值点数 X、阶数 N 由该信号自己的条目给出)。 + // 「均值滤波」开启时,为**被勾选**的信号各叠加一条多次迭代滑动平均曲线 + //(均值点数 X、迭代次数 N 由该信号自己的条目给出)。 const bool meanFilterOn = Tab().Lanes()[static_cast(laneIndex)].meanFilter; // 「弱波增强」开启时同样只为**被勾选**的信号叠加曲线 //(增强等级取整个网格共用的 `weakBoostLevel`)。 @@ -6125,7 +6259,7 @@ namespace jdi const bool highlighted = (ref == haloRef); if (highlighted) { - PaintSignalHalo(g, resolved, points, breaks); + PaintSignalHaloFor(g, resolved, *cached); } if (drawLine && !breaks.empty()) @@ -6219,7 +6353,7 @@ namespace jdi Tab().Lanes()[static_cast(laneIndex)].FindEdgeCenterItem(ref); if (centerItem != nullptr) { - PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->order); + PaintMeanFilterCurve(g, resolved, lane, centerItem->points, centerItem->iterations); } } // 均值滤波曲线(「实验性功能 → 均值滤波」):同样是仅用于预览的叠加曲线, @@ -6230,7 +6364,7 @@ namespace jdi Tab().Lanes()[static_cast(laneIndex)].FindMeanFilterItem(ref); if (item != nullptr) { - PaintMeanFilterCurve(g, resolved, lane, item->points, item->order); + PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations); } } // 弱波增强曲线:同样是仅用于预览的叠加曲线,只在被勾选时绘制 @@ -6246,11 +6380,11 @@ namespace jdi Tab().Lanes()[static_cast(laneIndex)] .weakBoostLevel)) * 100.0)); - const int order = std::max( - kWeakBoostOrderMin, - std::min(kWeakBoostOrderMax, - Tab().Lanes()[static_cast(laneIndex)].weakBoostOrder)); - PaintWeakBoostCurve(g, resolved, lane, levelPercent, order); + const int iterations = std::max( + kWeakBoostIterationsMin, + std::min(kWeakBoostIterationsMax, + Tab().Lanes()[static_cast(laneIndex)].weakBoostIterations)); + PaintWeakBoostCurve(g, resolved, lane, levelPercent, iterations); } } } @@ -6728,11 +6862,9 @@ namespace jdi if (bothOffSameSide) { - // 与两条时间读数同一列、叠在它们下方(更靠下的一行之下), - // 间距同样要让开差值读数自己的方向三角。 + // 与两条时间读数同一列、叠在它们下方(更靠下的一行之下)。 const float columnLeft = std::max(m_plotRect.X, first.X); - const float stackBottom = - std::max(first.Y, second.Y) + first.Height + kCursorTipStackGap * m_scale; + const float stackBottom = std::max(first.Y, second.Y) + first.Height + 2.0f * m_scale; insideAxis = true; const float axisHeight = std::max(14.0f * m_scale, m_timeAxisRect.Height - 4.0f * m_scale); return gd::RectF(columnLeft, stackBottom, width, std::min(height, axisHeight)); @@ -6774,23 +6906,29 @@ namespace jdi void PlotView::PaintCursorOffViewTip(gd::Graphics& g, const gd::RectF& labelRect, bool offRight, std::uint32_t color) { - // 三角画在读数胶囊的**上方**、与胶囊水平居中对齐,尖端指向光标所在的那一侧。 - const float arm = 5.0f * m_scale; - const float depth = arm * 1.4f; - const float centerX = labelRect.X + labelRect.Width * 0.5f; - const float anchorY = std::max(m_plotRect.Y + depth * 0.5f, labelRect.Y - depth * 0.5f - 1.0f * m_scale); + // 三角画在读数胶囊的**外侧**(尖端朝外、指向光标实际所在的那一侧), + // 与胶囊之间留出 `kMarkerTriangleGap` 的空白。 + // + // 旧实现把三角压在胶囊**上沿**且水平居中:既没有间隙(看起来像胶囊长了个尖角), + // 又与网格内"数据点越界"的画法(左右三角)不统一。现在两处共用同一套画法。 + const float arm = kMarkerTriangleArm * m_scale; + const float depth = kMarkerTriangleDepth * m_scale; + const float gap = kMarkerTriangleGap * m_scale; + const float centerY = labelRect.Y + labelRect.Height * 0.5f; gd::PointF outer[3]; if (offRight) { - outer[0] = gd::PointF(centerX - arm, anchorY - depth * 0.5f); - outer[1] = gd::PointF(centerX - arm, anchorY + depth * 0.5f); - outer[2] = gd::PointF(centerX + arm, anchorY); + const float baseX = labelRect.X + labelRect.Width + gap; + outer[0] = gd::PointF(baseX, centerY - arm); + outer[1] = gd::PointF(baseX, centerY + arm); + outer[2] = gd::PointF(baseX + depth, centerY); } else { - outer[0] = gd::PointF(centerX + arm, anchorY - depth * 0.5f); - outer[1] = gd::PointF(centerX + arm, anchorY + depth * 0.5f); - outer[2] = gd::PointF(centerX - arm, anchorY); + const float baseX = labelRect.X - gap; + outer[0] = gd::PointF(baseX, centerY - arm); + outer[1] = gd::PointF(baseX, centerY + arm); + outer[2] = gd::PointF(baseX - depth, centerY); } gd::SolidBrush brush(ToGdiColor(color)); g.FillPolygon(&brush, outer, 3); @@ -6829,7 +6967,7 @@ namespace jdi } // 重叠时把"最近操作过的那条光标"的内容画在上层,因此先画另一条。 - const int order[2] = { (m_activeCursor == 2) ? 1 : 2, (m_activeCursor == 2) ? 2 : 1 }; + const int iterations[2] = { (m_activeCursor == 2) ? 1 : 2, (m_activeCursor == 2) ? 2 : 1 }; // 增量差值**只**显示在"右侧光标"上。右侧 = 时间较大的那条,随拖拽实时变化: // 把右光标拖到左边后,增量会自动改由新的右侧光标显示。 @@ -6838,7 +6976,7 @@ namespace jdi // ---- 光标竖线 + 顶部把手 ---- for (int pass = 0; pass < cursorCount; ++pass) { - const int which = doubleMode ? order[pass] : 1; + const int which = doubleMode ? iterations[pass] : 1; const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime(); const float x = TimeToX(time); if (x < m_plotRect.X - 2.0f || x > m_plotRect.X + m_plotRect.Width + 2.0f) @@ -6866,7 +7004,7 @@ namespace jdi // ---- 数据点取值标注 ---- for (int pass = 0; pass < cursorCount; ++pass) { - const int which = doubleMode ? order[pass] : 1; + const int which = doubleMode ? iterations[pass] : 1; const double time = (which == 2) ? Tab().CursorTimeAlt() : Tab().CursorTime(); const double otherTime = (which == 2) ? Tab().CursorTime() : Tab().CursorTimeAlt(); // 只有右侧光标显示增量差值(左光标只显示自身取值)。 @@ -6936,10 +7074,25 @@ namespace jdi const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale; const float textHeight = 17.0f * m_scale; - float labelLeft = pinX + radius + 4.0f * m_scale; - if (labelLeft + textWidth > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale) + // 左右越界时三角贴在视界内侧、尖端朝外;数值标注必须放在三角的**内侧** + // 并留出 `kMarkerTriangleGap` 的空白,否则三角会像"多出来的一个尖角" + // (用户报告的"三角形与数值显示部分没有间隔")。 + float labelLeft; + if (marker.offLeft) { - labelLeft = pinX - radius - 4.0f * m_scale - textWidth; + labelLeft = marker.x + kMarkerTriangleGap * m_scale; + } + else if (marker.offRight) + { + labelLeft = marker.x - kMarkerTriangleGap * m_scale - textWidth; + } + else + { + labelLeft = pinX + radius + 4.0f * m_scale; + if (labelLeft + textWidth > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale) + { + labelLeft = pinX - radius - 4.0f * m_scale - textWidth; + } } labelLeft = std::max( m_plotRect.X + 2.0f * m_scale, @@ -7017,11 +7170,24 @@ namespace jdi const float centerTextWidth = UiMeasureTextWidth(m_fonts->monoSmall, centerText) + 10.0f * m_scale; - float centerLeft = pinX + radius + 4.0f * m_scale; - if (centerLeft + centerTextWidth > - m_plotRect.X + m_plotRect.Width - 2.0f * m_scale) + // 与常规取值标注同一规则:左右越界时标注要让开外侧的方向三角。 + float centerLeft; + if (centerMarker.offLeft) { - centerLeft = pinX - radius - 4.0f * m_scale - centerTextWidth; + centerLeft = centerMarker.x + kMarkerTriangleGap * m_scale; + } + else if (centerMarker.offRight) + { + centerLeft = centerMarker.x - kMarkerTriangleGap * m_scale - centerTextWidth; + } + else + { + centerLeft = pinX + radius + 4.0f * m_scale; + if (centerLeft + centerTextWidth > + m_plotRect.X + m_plotRect.Width - 2.0f * m_scale) + { + centerLeft = pinX - radius - 4.0f * m_scale - centerTextWidth; + } } centerLeft = std::max( m_plotRect.X + 2.0f * m_scale, @@ -7055,7 +7221,7 @@ namespace jdi if (centerItem != nullptr) { paintDerivedCapsule( - MeanFilterCurveFor(*resolved.data, centerItem->points, centerItem->order), + MeanFilterCurveFor(*resolved.data, centerItem->points, centerItem->iterations), L"中心轴"); } } @@ -7067,7 +7233,7 @@ namespace jdi if (meanItem != nullptr) { paintDerivedCapsule( - MeanFilterCurveFor(*resolved.data, meanItem->points, meanItem->order), + MeanFilterCurveFor(*resolved.data, meanItem->points, meanItem->iterations), L"均值滤波"); } } @@ -7081,10 +7247,10 @@ namespace jdi const int levelPercent = static_cast(std::lround( std::max(0.0, std::min(1.0, m_tab->Lanes()[laneIndex].weakBoostLevel)) * 100.0)); - const int order = std::max( - kWeakBoostOrderMin, - std::min(kWeakBoostOrderMax, m_tab->Lanes()[laneIndex].weakBoostOrder)); - paintDerivedCapsule(WeakBoostCurveFor(*resolved.data, levelPercent, order), + const int iterations = std::max( + kWeakBoostIterationsMin, + std::min(kWeakBoostIterationsMax, m_tab->Lanes()[laneIndex].weakBoostIterations)); + paintDerivedCapsule(WeakBoostCurveFor(*resolved.data, levelPercent, iterations), L"弱波增强"); } } @@ -7102,7 +7268,7 @@ namespace jdi for (int pass = 0; pass < cursorCount; ++pass) { - const int which = doubleMode ? order[pass] : 1; + const int which = doubleMode ? iterations[pass] : 1; const gd::RectF labelRect = CursorLabelRect(which); if (labelRect.Width <= 0.0f) { @@ -7206,12 +7372,12 @@ namespace jdi // 因此不再有独立的底部编辑行;只有「边沿计数」保留两行文本式排版。 const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f; const float rowsPerSignal = meanFilter ? 1.0f : 2.0f; - // 「弱波增强」在条目与底部按钮之间额外有两行滑块(等级 / 阶数 N),其余逐条目面板没有。 + // 「弱波增强」在条目与底部按钮之间额外有两行滑块(等级 / 迭代次数 N),其余逐条目面板没有。 const float sliderRowHeight = weakBoost ? (kWeakBoostSliderRowHeight * m_scale) : 0.0f; const float sliderRowsHeight = weakBoost ? (sliderRowHeight * 2.0f) : 0.0f; // 逐条目面板:每个条目 = 第一排(信号名 + 复选框) - // + 第二排(均值点数 X / 阶数 N)+(中心轴)第三排(统计结果); + // + 第二排(均值点数 X / 迭代次数 N)+(中心轴)第三排(统计结果); // 后两排**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化。 float meanRowHeight = 0.0f; float itemsHeight = 0.0f; @@ -7287,12 +7453,12 @@ namespace jdi std::max(0.0f, width - pad * 2.0f), buttonRowHeight); } - // 弱波增强的两个滑块行(等级 / 阶数 N,位于信号条目与底部按钮之间)。 + // 弱波增强的两个滑块行(等级 / 迭代次数 N,位于信号条目与底部按钮之间)。 if (weakBoost) { const float labelWidth = std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel), - UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostOrderLabel)); + UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostIterationsLabel)); const float valueWidth = std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100%"), UiMeasureTextWidth(m_fonts->monoSmall, L"10")); const float trackHeight = kWeakBoostTrackHeight * m_scale; @@ -7303,13 +7469,13 @@ namespace jdi layout.slider = gd::RectF(trackLeft, firstRowTop + (sliderRowHeight - trackHeight) * 0.5f, trackWidth, trackHeight); - layout.sliderOrder = + layout.sliderIterations = gd::RectF(trackLeft, firstRowTop + sliderRowHeight + (sliderRowHeight - trackHeight) * 0.5f, trackWidth, trackHeight); } // 逐信号条目:第一排 = 名称 + 复选框(复选框靠右); - // 第二排 = 「均值点数 X」与「阶数 N」两个输入框(仅勾选时存在); + // 第二排 = 「均值点数 X」与「迭代次数 N」两个输入框(仅勾选时存在); // 第三排(中心轴)= 上升沿/下降沿统计结果(仅勾选时存在)。 const float checkSize = kEdgePanelMeanCheckSize * m_scale; const float fieldHeight = kEdgePanelMeanEditHeight * m_scale; @@ -7335,7 +7501,7 @@ namespace jdi const float editTop = rowY + meanRowHeight; layout.rowPoints.push_back( gd::RectF(left + pad, editTop, halfWidth, fieldHeight)); - layout.rowOrder.push_back( + layout.rowIterations.push_back( gd::RectF(left + pad + halfWidth + 8.0f * m_scale, editTop, halfWidth, fieldHeight)); if (centerAxis) { @@ -7351,7 +7517,7 @@ namespace jdi else { layout.rowPoints.push_back(gd::RectF()); - layout.rowOrder.push_back(gd::RectF()); + layout.rowIterations.push_back(gd::RectF()); layout.rowStats.push_back(gd::RectF()); } @@ -7620,12 +7786,130 @@ namespace jdi return; } - // 平滑曲线本身没有尖峰,等间隔抽稀即可(不需要包络层),顶点预算与波形一致。 + const std::size_t visibleCount = end - begin; + const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f; + const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f; + + // 派生曲线的颜色:与原信号**同色相**但大幅拉开明度,使其与原始波形形成高对比 + // (原信号改颜色时曲线随之改变,因为它是从信号色派生出来的);透明度与原信号一致。 + const unsigned int baseAlpha = static_cast( + std::max(0.0, std::min(1.0, static_cast(resolved.style->opacity))) * 255.0 + 0.5); + const std::uint32_t derived = WithAlpha(HighContrastDerived(StyleColor(*resolved.style)), + baseAlpha); + + // 稠密(可见采样数超过像素列数)时改用**逐像素列的包络带**绘制。 + // + // ⚠️ 这不是"降级",而是性能与观感的双重需要:派生曲线往往叠加在方波 / 编码器 + // 这类**含大量近垂直跳变**的信号上,而 GDI+ 的抗锯齿折线在这种形状下耗时 + // 与顶点数不成比例 —— 实测 20ms/大格、2448 个顶点时单条曲线要 12~29ms, + // 5 个网格合计上百毫秒(这就是"启用弱波增强后平移格外卡"的直接原因)。 + // 按像素列取上下包络后,每个像素列只保留两个顶点,且包络带做 ±1 列扩展 + // (见 EmitEnvelopeBand 的说明),一次 FillPath 即可完成,实测 1ms 上下, + // 视觉上与细折线完全一致(列宽就是 1 像素)。 + if (visibleCount > static_cast(std::max(1.0f, m_plotRect.Width)) && + resolved.file->timeMonotonic) + { + const double viewSpan = viewMax - viewMin; + const std::size_t columns = static_cast(std::max(1.0f, m_plotRect.Width)); + const float plotLeft = m_plotRect.X; + const float columnWidth = m_plotRect.Width / static_cast(columns); + const float strokeHalf = std::max(1.0f, resolved.style->lineWidth) * m_scale * 0.5f; + + std::vector band; + std::vector bandBreaks; + std::vector segmentColumns; + // 每个像素列的顺序与时间单调一致:从头到尾扫一遍,按列号分桶即可。 + long long currentColumn = -1; + float columnTop = 0.0f; + float columnBottom = 0.0f; + const auto flushColumn = [&]() + { + if (currentColumn < 0) + { + return; + } + BandColumn entry; + entry.x = plotLeft + (static_cast(currentColumn) + 0.5f) * columnWidth; + entry.top = columnTop; + entry.bottom = columnBottom; + segmentColumns.push_back(entry); + }; + const auto flushSegment = [&]() + { + EmitEnvelopeBand(segmentColumns, band, bandBreaks); + segmentColumns.clear(); + }; + + for (std::size_t index = begin; index < end; ++index) + { + const float sampled = curve[index]; + if (!std::isfinite(sampled) || !std::isfinite(time[index])) + { + // 无效采样作为断点:先收尾当前列与当前段,再另起一段。 + flushColumn(); + currentColumn = -1; + flushSegment(); + continue; + } + double ratio = (time[index] - viewMin) / viewSpan; + ratio = std::max(0.0, std::min(1.0, ratio)); + const long long column = std::min( + static_cast(columns) - 1, + static_cast(ratio * static_cast(columns))); + float y = ValueToY(static_cast(sampled), lane); + y = static_cast(ClampDouble(y, limitTop, limitBottom)); + if (column != currentColumn) + { + flushColumn(); + currentColumn = column; + columnTop = y - strokeHalf; + columnBottom = y + strokeHalf; + } + else + { + columnTop = std::min(columnTop, y - strokeHalf); + columnBottom = std::max(columnBottom, y + strokeHalf); + } + } + flushColumn(); + flushSegment(); + + if (!bandBreaks.empty()) + { + gd::GraphicsPath bandPath; + // **必须显式设为缠绕填充规则**:包络带自交(上下包络交叉)时, + // 交替(奇偶)规则会在交叠处挖出空洞。 + bandPath.SetFillMode(gd::FillModeWinding); + for (std::size_t k = 0; k < bandBreaks.size(); ++k) + { + const std::size_t start = bandBreaks[k]; + const std::size_t stop = + (k + 1 < bandBreaks.size()) ? bandBreaks[k + 1] : band.size(); + const std::size_t total = stop - start; + if (total < 4 || (total % 2) != 0) + { + continue; + } + const std::size_t half = total / 2; + bandPath.StartFigure(); + bandPath.AddLines(band.data() + start, static_cast(half)); + bandPath.AddLines(band.data() + start + half, static_cast(half)); + bandPath.CloseFigure(); + } + if (bandPath.GetPointCount() > 0) + { + gd::SolidBrush brush(ToGdiColor(derived)); + g.FillPath(&brush, &bandPath); + } + } + return; + } + + // 稀疏时按折线绘制(顶点预算与波形一致)。 std::size_t budget = static_cast(std::max(1.0f, m_plotRect.Width)) * kPolylinePointsPerPixel; budget = std::max(budget, kMinPolylinePoints); budget = std::min(budget, kMaxPolylinePoints); - const std::size_t visibleCount = end - begin; const std::size_t stride = std::max(1, visibleCount / std::max(1, budget)); std::vector points; @@ -7633,9 +7917,6 @@ namespace jdi points.reserve(std::min(visibleCount, budget) + 1); bool inSegment = false; - const float limitTop = lane.rect.Y - lane.rect.Height * 4.0f; - const float limitBottom = lane.rect.Y + lane.rect.Height * 5.0f; - for (std::size_t index = begin; index < end; index += stride) { const float sampled = curve[index]; @@ -7664,13 +7945,6 @@ namespace jdi return; } - // 中心平均值曲线:与原信号**同色相**但大幅拉开明度,使其与原始波形形成高对比 - // (原信号改颜色时曲线随之改变,因为它是从信号色派生出来的); - // 透明度与原信号保持一致。 - const unsigned int baseAlpha = static_cast( - std::max(0.0, std::min(1.0, static_cast(resolved.style->opacity))) * 255.0 + 0.5); - const std::uint32_t derived = WithAlpha(HighContrastDerived(StyleColor(*resolved.style)), - baseAlpha); gd::Pen pen(ToGdiColor(derived), std::max(1.0f, resolved.style->lineWidth) * m_scale); pen.SetLineJoin(gd::LineJoinRound); pen.SetStartCap(gd::LineCapRound); @@ -7688,26 +7962,26 @@ namespace jdi } void PlotView::PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, - const LaneVisual& lane, int points, int order) const + const LaneVisual& lane, int points, int iterations) const { if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr) { return; } // 与「边沿计数(中心轴)」的中心平均值曲线共用同一段绘制逻辑 - // (同色相高对比度的派生配色 + 同透明度),只是曲线本身按多阶滤波计算。 - PaintDerivedCurve(g, resolved, lane, MeanFilterCurveFor(*resolved.data, points, order)); + // (同色相高对比度的派生配色 + 同透明度),只是曲线本身按多次迭代滤波计算。 + PaintDerivedCurve(g, resolved, lane, MeanFilterCurveFor(*resolved.data, points, iterations)); } void PlotView::PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, - const LaneVisual& lane, int levelPercent, int order) const + const LaneVisual& lane, int levelPercent, int iterations) const { if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr) { return; } // 派生曲线一律使用同一段绘制逻辑与配色规则(弱波增强曲线同样如此)。 - PaintDerivedCurve(g, resolved, lane, WeakBoostCurveFor(*resolved.data, levelPercent, order)); + PaintDerivedCurve(g, resolved, lane, WeakBoostCurveFor(*resolved.data, levelPercent, iterations)); } void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector& lanes) @@ -7923,15 +8197,15 @@ namespace jdi continue; } - // 第二排:两个输入框(「均值点数 X / 阶数 N」;弱波增强没有参数输入框)。 + // 第二排:两个输入框(「均值点数 X / 迭代次数 N」;弱波增强没有参数输入框)。 const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref) : tabLane.FindEdgeCenterItem(ref); const int shownPoints = (item != nullptr) ? item->points : 5; - const int shownOrder = (item != nullptr) ? item->order : 1; + const int shownOrder = (item != nullptr) ? item->iterations : 1; const PlotEditTarget pointsTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS : PlotEditTarget::EDGE_CENTER_POINTS; - const PlotEditTarget orderTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_ORDER - : PlotEditTarget::EDGE_CENTER_ORDER; + const PlotEditTarget orderTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_ITERATIONS + : PlotEditTarget::EDGE_CENTER_ITERATIONS; const auto paintField = [&](const gd::RectF& rect, const wchar_t* label, int value, PlotEditTarget target) { @@ -7969,7 +8243,7 @@ namespace jdi } }; paintField(layout.rowPoints[rowIndex], L"均值点数 X", shownPoints, pointsTarget); - paintField(layout.rowOrder[rowIndex], L"阶数 N", shownOrder, orderTarget); + paintField(layout.rowIterations[rowIndex], L"迭代次数 N", shownOrder, orderTarget); // 第三排(中心轴):该信号在当前参数下的上升沿/下降沿统计结果。 if (centerAxis && rowIndex < layout.rowStats.size() && @@ -7977,7 +8251,7 @@ namespace jdi { const gd::RectF& statsRect = layout.rowStats[rowIndex]; const int statsPoints = (item != nullptr) ? item->points : 5; - const int statsOrder = (item != nullptr) ? item->order : 1; + const int statsOrder = (item != nullptr) ? item->iterations : 1; std::size_t rising = 0; std::size_t falling = 0; EdgeCenterCountsFor(*resolved.file, *resolved.data, statsPoints, statsOrder, @@ -7990,7 +8264,7 @@ namespace jdi } } - // 「弱波增强」的两个滑块(等级 0–100% / 阶数 N 1–10): + // 「弱波增强」的两个滑块(等级 0–100% / 迭代次数 N 1–10): // 左侧说明文字 + 轨道 + 右侧数值。 if (weakBoost && layout.slider.Width > 0.0f) { @@ -8030,15 +8304,15 @@ namespace jdi L"%", levelActive); - const int order = std::max(kWeakBoostOrderMin, - std::min(kWeakBoostOrderMax, tabLane.weakBoostOrder)); - const double orderRatio = static_cast(order - kWeakBoostOrderMin) / - static_cast(kWeakBoostOrderMax - - kWeakBoostOrderMin); - const bool orderActive = (m_dragMode == PlotDragMode::BOOST_ORDER) && + const int iterations = std::max(kWeakBoostIterationsMin, + std::min(kWeakBoostIterationsMax, tabLane.weakBoostIterations)); + const double orderRatio = static_cast(iterations - kWeakBoostIterationsMin) / + static_cast(kWeakBoostIterationsMax - + kWeakBoostIterationsMin); + const bool orderActive = (m_dragMode == PlotDragMode::BOOST_ITERATIONS) && (m_boostDragLane == static_cast(index)); - paintSliderRow(layout.sliderOrder, kWeakBoostOrderLabel, orderRatio, - std::to_wstring(order), orderActive); + paintSliderRow(layout.sliderIterations, kWeakBoostIterationsLabel, orderRatio, + std::to_wstring(iterations), orderActive); } } diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index cc7f943..933434e 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -359,7 +359,7 @@ namespace jdi * @brief 「均值滤波」中某个信号自己的参数。 * * 仅参与列表中的信号才会被勾选预览;参数按信号保存, - * 因此同一条网格内不同信号可以使用不同的均值点数与阶数。 + * 因此同一条网格内不同信号可以使用不同的均值点数与迭代次数。 * 「边沿计数(中心轴)」的平滑参数与它**完全同构**(同一套命名与作用), * 见 `EdgeCenterItem`。 */ @@ -367,7 +367,7 @@ namespace jdi { SignalRef ref; /**< 目标信号。 */ int points = 5; /**< 均值点数 X(滑平半径,窗口 = 2X+1)。 */ - int order = 1; /**< 阶数 N(重复滤波次数,至少 1)。 */ + int iterations = 1; /**< 迭代次数 N(重复滤波次数,至少 1)。 */ }; /** 中心轴边沿计数的平滑参数:与均值滤波共用同一结构与同一套语义。 */ @@ -423,7 +423,7 @@ namespace jdi * @brief 「实验性功能 → 边沿计数(中心轴)」开关。 * * 开启后为本网格内**被勾选**的信号额外绘制一条均值滤波曲线(与「实验性功能 → 均值滤波」 - * 用的是同一套参数:均值点数 X + 阶数 N),并按“信号穿越该滤波曲线”统计上/下边沿。 + * 用的是同一套参数:均值点数 X + 迭代次数 N),并按“信号穿越该滤波曲线”统计上/下边沿。 * 这条曲线只是叠加显示,不会作为信号加入左侧列表。 */ bool edgeCenter = false; @@ -432,7 +432,7 @@ namespace jdi * @brief 参与中心轴边沿计数的信号及其平滑参数(只包含被勾选的信号)。 * * 参数语义与 `meanFilterItems` **完全一致**(`points` = 均值点数 X, - * `order` = 阶数 N),因此两条曲线的数值逐位相同。 + * `iterations` = 迭代次数 N),因此两条曲线的数值逐位相同。 */ std::vector edgeCenterItems; @@ -463,7 +463,7 @@ namespace jdi /** * @brief 「实验性功能 → 均值滤波」开关。 * - * 开启后为本网格内**被勾选**的信号额外绘制一条多阶滑动平均曲线(按各自的均值点数 X + * 开启后为本网格内**被勾选**的信号额外绘制一条多次迭代滑动平均曲线(按各自的均值点数 X * 重复 N 阶),并在浮动面板中提供每个信号自己的参数与「构建为新信号」按钮。 * 与其它实验性功能一样属于网格自身状态,重建网格时按信号继承。 */ @@ -514,13 +514,13 @@ namespace jdi double weakBoostLevel = 0.5; /** - * @brief 弱波增强的阶数 N(1–10)。 + * @brief 弱波增强的迭代次数 N(1–10)。 * - * 与「均值滤波」的阶数 N 一样表示“把同一处理重复应用 N 次”(每一阶以上一阶为输入): + * 与「均值滤波」的迭代次数 N 一样表示“把同一处理重复应用 N 次”(每一阶以上一阶为输入): * 一阶时改定量就已严格等于「等级」对应的目标值;多阶会把相邻弱波互相抬高 * 彼此的基准,从而把较宽的弱波区逐步恢复。 */ - int weakBoostOrder = 1; + int weakBoostIterations = 1; /** 参与弱波增强预览的信号条目(只包含被勾选的信号)。 */ std::vector weakBoostItems; @@ -578,17 +578,17 @@ namespace jdi void ComputeMovingAverage(const std::vector& values, int radius, std::vector& out); /** - * @brief 计算多阶(迭代)滑动平均:把 N 点滑动平均重复应用 `order` 次。 + * @brief 计算多次迭代滑动平均:把 N 点滑动平均重复应用 `iterations` 次。 * - * 每次迭代以上一次的结果为输入(NaN 不参与平均),因此阶数越高曲线越平滑。 - * `order <= 1` 时与 `ComputeMovingAverage` 完全一致。 + * 每次迭代以上一次的结果为输入(NaN 不参与平均),因此迭代次数越高曲线越平滑。 + * `iterations <= 1` 时与 `ComputeMovingAverage` 完全一致。 * * @param [IN] values 原始采样值。 * @param [IN] radius 滑平半径 N(0 表示原样返回)。 - * @param [IN] order 阶数(迭代次数,至少为 1)。 + * @param [IN] iterations 迭代次数(至少为 1)。 * @param [OUT] out 输出数组(长度与 values 相同)。 */ - void ComputeMovingAverageIterated(const std::vector& values, int radius, int order, + void ComputeMovingAverageIterated(const std::vector& values, int radius, int iterations, std::vector& out); /** @@ -614,10 +614,10 @@ namespace jdi * * @param [IN] values 原始采样值。 * @param [IN] level 弱波增强等级(0–1,超出范围会被夹取)。 - * @param [IN] order 阶数 N(重复应用次数,至少为 1;与均值滤波的阶数同一语义)。 + * @param [IN] iterations 迭代次数 N(重复应用次数,至少为 1)。 * @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。 */ - void ComputeWeakWaveBoost(const std::vector& values, double level, int order, + void ComputeWeakWaveBoost(const std::vector& values, double level, int iterations, std::vector& out); /** diff --git a/Utilities/Inc/PaintUtil.hpp b/Utilities/Inc/PaintUtil.hpp index af84764..ae79872 100644 --- a/Utilities/Inc/PaintUtil.hpp +++ b/Utilities/Inc/PaintUtil.hpp @@ -256,6 +256,15 @@ namespace jdi */ std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb); + /** + * @brief 作废「文本宽度测量」的记忆化缓存。 + * + * `UiMeasureTextWidth` 内部按 (字体句柄, 文本) 缓存测量结果以摊薄 GDI 调用开销; + * 字体句柄被重建(DPI 变化等)时必须调用本函数,否则系统复用的同一句柄值会命中 + * 旧宽度,造成布局错位。 + */ + void UiClearMeasureCache(); + /** * @brief 一次性填充多个圆角矩形(同色同圆角)。 * diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index 6e8cbd1..61596eb 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -137,7 +137,7 @@ namespace jdi bool weakBoost = false; std::vector weakBoostItems; double weakBoostLevel = 0.5; - int weakBoostOrder = 1; + int weakBoostIterations = 1; bool weakBoostPanelOpen = true; double weakBoostPanelX = -1.0; double weakBoostPanelY = -1.0; @@ -169,7 +169,7 @@ namespace jdi carry.weakBoost = lane.weakBoost; carry.weakBoostItems = lane.weakBoostItems; carry.weakBoostLevel = lane.weakBoostLevel; - carry.weakBoostOrder = lane.weakBoostOrder; + carry.weakBoostIterations = lane.weakBoostIterations; carry.weakBoostPanelOpen = lane.weakBoostPanelOpen; carry.weakBoostPanelX = lane.weakBoostPanelX; carry.weakBoostPanelY = lane.weakBoostPanelY; @@ -212,7 +212,7 @@ namespace jdi lane.weakBoost = carry.weakBoost; lane.weakBoostItems = carry.weakBoostItems; lane.weakBoostLevel = carry.weakBoostLevel; - lane.weakBoostOrder = carry.weakBoostOrder; + lane.weakBoostIterations = carry.weakBoostIterations; lane.weakBoostPanelOpen = carry.weakBoostPanelOpen; lane.weakBoostPanelX = carry.weakBoostPanelX; lane.weakBoostPanelY = carry.weakBoostPanelY; @@ -1020,10 +1020,10 @@ namespace jdi } } // namespace - void ComputeMovingAverageIterated(const std::vector& values, int radius, int order, + void ComputeMovingAverageIterated(const std::vector& values, int radius, int iterations, std::vector& out) { - const int stages = std::max(1, order); + const int stages = std::max(1, iterations); // 第 1 阶:从原始采样值出发。 ComputeMovingAverage(values, radius, out); if (stages == 1) @@ -1230,7 +1230,7 @@ namespace jdi } } // namespace - void ComputeWeakWaveBoost(const std::vector& values, double level, int order, + void ComputeWeakWaveBoost(const std::vector& values, double level, int iterations, std::vector& out) { const std::size_t count = values.size(); @@ -1251,17 +1251,17 @@ namespace jdi } } - // 「阶数 N」与「均值滤波」的阶数语义一致:把同一处理**重复应用 N 次**, + // 「迭代次数 N」与「均值滤波」的迭代次数语义一致:把同一处理**重复应用 N 次**, // 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「等级」对应的目标值; - // 多阶时相邻的弱波会互相抬高彼此的基准,从而把较宽的弱波区逐步恢复。 - const int stages = std::max(1, std::min(order, 1000)); + // 多次迭代时相邻的弱波会互相抬高彼此的基准,从而把较宽的弱波区逐步恢复。 + const int stages = std::max(1, std::min(iterations, 1000)); const double strength = std::max(0.0, std::min(1.0, level)); std::vector next; for (int stage = 0; stage < stages; ++stage) { if (!ApplyWeakBoostStage(current, strength, next)) { - // 这一阶没有任何采样点需要调整:后续阶数也不会再有变化,提前结束。 + // 这一阶没有任何采样点需要调整:后续迭代次数也不会再有变化,提前结束。 break; } current.swap(next); @@ -1912,7 +1912,7 @@ namespace jdi lane.edgeCenterPanelY = source.edgeCenterPanelY; lane.weakBoost = source.weakBoost; lane.weakBoostLevel = source.weakBoostLevel; - lane.weakBoostOrder = source.weakBoostOrder; + lane.weakBoostIterations = source.weakBoostIterations; lane.weakBoostPanelOpen = source.weakBoostPanelOpen; lane.weakBoostPanelX = source.weakBoostPanelX; lane.weakBoostPanelY = source.weakBoostPanelY; diff --git a/Utilities/Src/PaintUtil.cpp b/Utilities/Src/PaintUtil.cpp index b8d5712..44779a1 100644 --- a/Utilities/Src/PaintUtil.cpp +++ b/Utilities/Src/PaintUtil.cpp @@ -2,6 +2,12 @@ #include #include +#include +#include +#include +#include +#include +#include namespace jdi { @@ -89,11 +95,52 @@ namespace jdi } return font; } + } // namespace + + namespace + { + /** + * @brief 文本宽度测量的缓存键(字体句柄 + 文本)。 + */ + struct MeasureKey + { + HFONT font = nullptr; + std::wstring text; + bool operator==(const MeasureKey& other) const + { + return font == other.font && text == other.text; + } + }; + struct MeasureKeyHash + { + std::size_t operator()(const MeasureKey& key) const + { + std::size_t hash = std::hash()(key.font); + for (const wchar_t character : key.text) + { + hash = hash * 131u + static_cast(character); + } + return hash; + } + }; + /** 上限保护:超过时整体清空(缓存只是加速手段,清空后重新填充即可)。 */ + constexpr std::size_t kMeasureCacheLimit = 4096; + std::unordered_map g_measureCache; + std::mutex g_measureMutex; + } // namespace + + void UiClearMeasureCache() + { + std::lock_guard lock(g_measureMutex); + g_measureCache.clear(); } bool UiFonts::Create(UINT targetDpi) { Destroy(); + // 字体句柄会被重新创建,旧句柄可能被系统复用:必须作废按句柄缓存的文本宽度, + // 否则复用同一句柄值的新字体会命中旧宽度(布局错位)。 + UiClearMeasureCache(); dpi = (targetDpi == 0) ? 96u : targetDpi; const double scale = static_cast(dpi) / 96.0; @@ -359,6 +406,21 @@ namespace jdi return 0.0f; } + // 记忆化:文本宽度只能走 GDI(GetDC + GetTextExtentPoint32W + ReleaseDC), + // 单次约 10µs;而布局代码每帧会把同一批字符串**重复测量成百上千次**—— + // 例如浮动面板的几何是按「网格数 × 面板种类」反复重算的(5 网格 × 4 类 = 20 次, + // 每次十来个字符串),实测光这一项就占 3ms 以上。字体与文本都相同的测量结果 + // 必然一致,因此按 (字体, 文本) 做一层缓存;字体被重建时由 `UiClearMeasureCache` 作废。 + const MeasureKey key{font, text}; + { + std::lock_guard lock(g_measureMutex); + const auto found = g_measureCache.find(key); + if (found != g_measureCache.end()) + { + return found->second; + } + } + HDC hdc = GetDC(nullptr); if (hdc == nullptr) { @@ -371,7 +433,16 @@ namespace jdi SelectObject(hdc, oldFont); ReleaseDC(nullptr, hdc); - return static_cast(size.cx); + const float width = static_cast(size.cx); + { + std::lock_guard lock(g_measureMutex); + if (g_measureCache.size() >= kMeasureCacheLimit) + { + g_measureCache.clear(); + } + g_measureCache.emplace(key, width); + } + return width; } std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth)