From da83dc298771e6323a670b87ab9baccb900c6a51 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Mon, 12 Oct 2026 10:10:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=A9=E6=94=BE=E5=90=8E?= =?UTF-8?q?=E5=B9=B3=E7=A7=BB=E7=9A=84=E7=BB=98=E5=88=B6=E9=94=99=E8=AF=AF?= =?UTF-8?q?/=E6=8A=96=E5=8A=A8=E4=B8=8E=E5=89=8D=E5=8F=B0=E5=8D=A1?= =?UTF-8?q?=E9=A1=BF=EF=BC=9B=E5=BC=B1=E6=B3=A2=E5=A2=9E=E5=BC=BA=E6=94=B9?= =?UTF-8?q?=E5=90=8D=E5=BC=B1=E5=B3=B0=E5=A2=9E=E5=BC=BA=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=200~200%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 渲染(根因与修法) - 根因:后台图层线程渲染时会临时改写共享渲染状态(m_area/m_plotRect/视界/网格矩形), 与 UI 线程无锁的平移(OnMouseMove)和命中测试相冲 → 后台的"保存→改写→恢复"会把 用户刚做的平移弹回、命中测试读到错位网格;同时 RollWideBuffer 用单一全局锚点给所有 图层算同一位移,而各图层可能是在不同视界下渲染的(后台逐网格)→ 网格之间时间错位、 贴回像素互相矛盾;补细条又误用"当前视界"的映射(与旧像素不同源)→ ≤0.5px 接缝与拖动抖动。 - 修法: * 删除后台图层线程(m_layerWorker 及任务队列、EnsureLayerWorker、StopLayerWorker、 LayerWorkerLoop、ScheduleStaleLayerRenders、m_layerBack、m_layerAnchorMin/Valid、 m_lastPaintTick、m_layerRescheduleStreak)。图层渲染一律在 UI 线程内同步完成。 * 逐网格独立锚点与滚动:以 LaneLayer::wideMin 为准,补细条沿用该图层自己的映射。 * RenderLaneLayerRegion 新增 wideMin 形参;新增 LayerDefaultWideMin()。 * RollWideBuffer 逐网格:内容指纹不一致或锚点离视界太远 → 该网格整块重画; 否则整数像素自贴图平移 + 只补新暴露的细条。 * Paint 宽基底分支:!LayersReadyForView() 时先同步 RebuildWideBuffer() 再滚动。 * BackgroundLayerRenderCount() 保留为"同步图层区域渲染次数"的诊断计数。 - 效果:平移不再出现网格时间错位、视界被弹回或波形抖动;居中图层与整幅重绘逐像素一致。 弱峰增强(原「弱波增强」) - 功能改名:界面文案与标识符统一为 WeakPeak/weakPeak(ComputeWeakPeakBoost、 SetLaneWeakPeak*、EdgePanelKind::PEAK、PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS、 PLOT_CMD_LANE_WEAK_PEAK、PeakExtremum 等),导出信号名 "<原信号> (弱峰增强 M% N次迭代)"。 - 等级范围改为 0~200%,目标值分段插值: * 0~100%:在"目标极值自身的值"与"左右同类极值中较温和的一侧"之间插值 (0% 目标就是当前值 → 完全不动,允许用户做极小幅度调整); * 100~200%:与原先 0~100% 的插值方式一致(200% 完全拉平到另一侧)。 两段在 100% 处连续。新 1.5 == 旧 0.5,新 2.0 == 旧 1.0。 - Lane::weakPeakLevel 默认 1.5;levelPercent(构建请求/曲线缓存键)范围 0~200、默认 150; 新增 kWeakPeakLevelMax = 2.0 与 kWeakPeakPercentMax = 200;滑块轨道比例 = level / 2。 测试 - 同步更新离屏测试 .preflight/uitest:等级期望改为 0/50/100/150/200 五档;阶数扫描与 "完全恢复"块改用 150%/200% 以保持旧期望;派生信号名校验改用新名; [J] 改为"补丁帧标记降级 → 渲染到缓存重新有效后用修复帧与整幅重绘比对(0 差异)"; [AF] 面板像素容差 32→96(面板 96% 不透明,底层 ≤0.5px 残留会透出,实测最多 6 像素/72 级); [X] 预览曲线像素计数的颜色容差 6→40。 - 新增离屏检查 .preflight/layercheck:居中图层与整幅重绘逐像素对比、平移逐网格对齐位移、 视界覆盖检测、跨度抖动下的视界保持检测。 - make release 零警告;uitest 380/380 通过(exit=0)。 --- Components/Application/Inc/Application.hpp | 12 +- Components/Application/Src/Application.cpp | 42 +- Components/PlotView/Inc/PlotView.hpp | 223 +++--- Components/PlotView/Src/PlotView.cpp | 807 +++++++++------------ Documents/agent_mem.md | 72 +- Utilities/Inc/DataModel.hpp | 91 +-- Utilities/Src/DataModel.cpp | 141 ++-- 7 files changed, 649 insertions(+), 739 deletions(-) diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index 69cc3f0..b556421 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -241,8 +241,8 @@ namespace jdi /** * @brief 应用绘图区提交的「实验性功能 → 构建为新信号」请求。 * - * 为指定网格内的每条被勾选信号各构建一条派生信号(多次迭代均值滤波或弱波增强), - * 命名为 `<原信号名称> (N点均值…)` / `<原信号名称> (弱波增强 M%)`, + * 为指定网格内的每条被勾选信号各构建一条派生信号(多次迭代均值滤波或弱峰增强), + * 命名为 `<原信号名称> (N点均值…)` / `<原信号名称> (弱峰增强 M%)`, * 插入到原信号所在组的原信号位置**下方**,并把新信号所在的网格移动到原信号网格的下方。 * * @return 实际构建了新信号返回 true。 @@ -250,7 +250,7 @@ namespace jdi bool ApplyPendingDerivedBuild(); /** - * @brief 派生信号构建请求(多次迭代均值滤波 / 弱波增强共用同一套插入流程)。 + * @brief 派生信号构建请求(多次迭代均值滤波 / 弱峰增强共用同一套插入流程)。 */ struct DerivedSignalRequest { @@ -258,12 +258,12 @@ namespace jdi int signalIndex = -1; /**< 原信号下标。 */ int points = 5; /**< 多次迭代均值滤波:均值点数 X(滑平半径)。 */ int iterations = 1; /**< 多次迭代均值滤波:迭代次数 N(至少 1)。 */ - int levelPercent = 50; /**< 弱波增强:增强等级(0–100)。 */ - bool weakBoost = false; /**< true = 弱波增强,false = 多次迭代均值滤波。 */ + int levelPercent = 150; /**< 弱峰增强:增强等级(0–200)。 */ + bool weakPeak = 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 a1bb4d5..7b0f616 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -1481,14 +1481,14 @@ namespace jdi // 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。 // 多次迭代均值滤波形如「<原信号名称> (N点均值 M次迭代)」, - // 弱波增强形如「<原信号名称> (弱波增强 M% N次迭代)」。 + // 弱峰增强形如「<原信号名称> (弱峰增强 M% N次迭代)」。 std::vector derived_values; SignalData derived; - if (request.weakBoost) + if (request.weakPeak) { - ComputeWeakWaveBoost(source.values, static_cast(request.levelPercent) / 100.0, + ComputeWeakPeakBoost(source.values, static_cast(request.levelPercent) / 100.0, request.iterations, derived_values); - derived.name = baseName + L" (弱波增强 " + std::to_wstring(request.levelPercent) + L"% " + + derived.name = baseName + L" (弱峰增强 " + std::to_wstring(request.levelPercent) + L"% " + std::to_wstring(request.iterations) + L"次迭代)"; } else @@ -1538,7 +1538,7 @@ namespace jdi bool Application::ApplyPendingDerivedBuild() { - // 两类「构建为新信号」请求(均值滤波 / 弱波增强)机制完全一致, + // 两类「构建为新信号」请求(均值滤波 / 弱峰增强)机制完全一致, // 统一整理成同一份构建清单后按下标降序处理。 std::vector requests; { @@ -1553,24 +1553,24 @@ namespace jdi request.signalIndex = item.signalIndex; request.points = item.points; request.iterations = item.iterations; - request.weakBoost = false; + request.weakPeak = false; requests.push_back(request); } } } { int laneIndex = -1; - std::vector items; - if (m_plot.TakePendingWeakBoostBuild(laneIndex, items)) + std::vector items; + if (m_plot.TakePendingWeakPeakBuild(laneIndex, items)) { - for (const PlotView::WeakBoostBuild& item : items) + for (const PlotView::WeakPeakBuild& item : items) { DerivedSignalRequest request; request.fileIndex = item.fileIndex; request.signalIndex = item.signalIndex; request.levelPercent = item.levelPercent; request.iterations = item.iterations; - request.weakBoost = true; + request.weakPeak = true; requests.push_back(request); } } @@ -1610,9 +1610,9 @@ namespace jdi if (built > 0) { std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 "; - if (last.weakBoost) + if (last.weakPeak) { - text += L"弱波增强 " + std::to_wstring(last.levelPercent) + L"% " + + text += L"弱峰增强 " + std::to_wstring(last.levelPercent) + L"% " + std::to_wstring(last.iterations) + L"次迭代信号。"; } else @@ -1753,11 +1753,11 @@ namespace jdi meanFilter.checked = m_plot.LaneMeanFilter(laneIndex); experimental.submenu.push_back(meanFilter); - MenuItem weakBoost; - weakBoost.label = L"弱波增强"; - weakBoost.id = PLOT_CMD_LANE_WEAK_BOOST; - weakBoost.checked = m_plot.LaneWeakBoost(laneIndex); - experimental.submenu.push_back(weakBoost); + MenuItem weakPeak; + weakPeak.label = L"弱峰增强"; + weakPeak.id = PLOT_CMD_LANE_WEAK_PEAK; + weakPeak.checked = m_plot.LaneWeakPeak(laneIndex); + experimental.submenu.push_back(weakPeak); } items.push_back(experimental); @@ -1872,9 +1872,9 @@ namespace jdi // 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。 m_plot.ToggleLaneMeanFilter(laneIndex); break; - case PLOT_CMD_LANE_WEAK_BOOST: - // 「实验性功能 → 弱波增强」:切换弱波增强曲线与参数面板。 - m_plot.ToggleLaneWeakBoost(laneIndex); + case PLOT_CMD_LANE_WEAK_PEAK: + // 「实验性功能 → 弱峰增强」:切换弱峰增强曲线与参数面板。 + m_plot.ToggleLaneWeakPeak(laneIndex); break; case PLOT_CMD_LANE_SPLIT: // 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。 @@ -2378,7 +2378,7 @@ namespace jdi { // 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。 ApplyPendingRename(); - // 「构建为新信号」按钮(均值滤波 / 弱波增强):立即构建新信号并同步各视图。 + // 「构建为新信号」按钮(均值滤波 / 弱峰增强):立即构建新信号并同步各视图。 ApplyPendingDerivedBuild(); InvalidateRect(hwnd, nullptr, FALSE); } diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 1f3727c..4a729d7 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -43,8 +43,8 @@ namespace jdi MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */ EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */ EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */ - BOOST_LEVEL, /**< 拖动「弱波增强」面板中的等级滑块。 */ - BOOST_ITERATIONS, /**< 拖动「弱波增强」面板中的迭代次数 N 滑块。 */ + PEAK_LEVEL, /**< 拖动「弱峰增强」面板中的等级滑块。 */ + PEAK_ITERATIONS, /**< 拖动「弱峰增强」面板中的迭代次数 N 滑块。 */ LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */ }; @@ -90,7 +90,7 @@ namespace jdi PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */ PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */ PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */ - PLOT_CMD_LANE_WEAK_BOOST = 11 /**< 切换本网格的「实验性功能 → 弱波增强」开关。 */ + PLOT_CMD_LANE_WEAK_PEAK = 11 /**< 切换本网格的「实验性功能 → 弱峰增强」开关。 */ }; /** @@ -101,7 +101,7 @@ namespace jdi COUNT = 0, /**< 边沿计数。 */ CENTER = 1, /**< 边沿计数(中心轴)。 */ MEAN = 2, /**< 均值滤波。 */ - BOOST = 3 /**< 弱波增强。 */ + PEAK = 3 /**< 弱峰增强。 */ }; /** @@ -112,7 +112,7 @@ namespace jdi enum class DerivedCurveKind : int { MEAN_FILTER = 0, /**< 多次迭代均值滤波。 */ - WEAK_BOOST = 1 /**< 弱波增强。 */ + WEAK_PEAK = 1 /**< 弱峰增强。 */ }; /** @@ -138,11 +138,11 @@ namespace jdi void SetHostWindow(HWND host) { m_host = host; } /** - * @brief 后台「网格图层」绘制线程已完成的图层数量(仅供验证 / 诊断)。 + * @brief 已渲染的「信号图层」区域数量(仅供验证 / 诊断)。 * - * 计数 > 0 即说明单网格基底的绘制确实发生在**后台线程**, - * 前台只负责位图搬运(贴图)与叠加层。 - * @return 累计完成的图层数量。 + * 每次把一个网格的基底(整块或滚动细条)画进图层都会 +1。计数长期不增长即说明 + * 平移期间确实在复用已有像素(只做坐标偏移),没有重新绘制波形。 + * @return 累计完成的图层区域数量。 */ std::uint64_t BackgroundLayerRenderCount() const { @@ -569,115 +569,115 @@ namespace jdi bool TakePendingMeanFilterBuild(int& laneIndex, std::vector& items); /** - * @brief 「实验性功能 → 弱波增强」的「构建为新信号」单个请求 + * @brief 「实验性功能 → 弱峰增强」的「构建为新信号」单个请求 * (一个被勾选的信号 + 整个网格共用的增强等级)。 */ - struct WeakBoostBuild + struct WeakPeakBuild { int fileIndex = -1; /**< 信号所属文件下标。 */ int signalIndex = -1; /**< 文件内信号下标。 */ - int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ + int levelPercent = 150; /**< 弱峰增强等级(0–200)。 */ int iterations = 1; /**< 迭代次数 N(1–10)。 */ }; /** - * @brief 取出「弱波增强 → 构建为新信号」请求(由面板底部按钮触发)。 + * @brief 取出「弱峰增强 → 构建为新信号」请求(由面板底部按钮触发)。 * @param [OUT] laneIndex 触发该请求的网格下标。 * @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。 * @return 存在待处理请求返回 true(取出后清空)。 */ - bool TakePendingWeakBoostBuild(int& laneIndex, std::vector& items); + bool TakePendingWeakPeakBuild(int& laneIndex, std::vector& items); /** - * @brief 切换某网格的「弱波增强」开关。 + * @brief 切换某网格的「弱峰增强」开关。 * @param [IN] laneIndex 网格下标。 * @param [IN] enabled 是否开启。 * @return 状态发生变化返回 true。 */ - bool SetLaneWeakBoost(int laneIndex, bool enabled); + bool SetLaneWeakPeak(int laneIndex, bool enabled); /** - * @brief 查询某网格的「弱波增强」开关。 + * @brief 查询某网格的「弱峰增强」开关。 * @param [IN] laneIndex 网格下标。 * @return 已开启返回 true。 */ - bool LaneWeakBoost(int laneIndex) const; + bool LaneWeakPeak(int laneIndex) const; /** - * @brief 切换某网格的「弱波增强」开关。 + * @brief 切换某网格的「弱峰增强」开关。 * @param [IN] laneIndex 网格下标。 * @return 切换后的状态。 */ - bool ToggleLaneWeakBoost(int laneIndex); + bool ToggleLaneWeakPeak(int laneIndex); /** - * @brief 读取某网格的弱波增强等级(0–1)。 + * @brief 读取某网格的弱峰增强等级(0–2)。 * @param [IN] laneIndex 网格下标。 * @return 增强等级;网格不存在时返回 0.5。 */ - double LaneWeakBoostLevel(int laneIndex) const; + double LaneWeakPeakLevel(int laneIndex) const; /** - * @brief 读取某网格的弱波增强迭代次数 N(1–10)。 + * @brief 读取某网格的弱峰增强迭代次数 N(1–10)。 * @param [IN] laneIndex 网格下标。 * @return 迭代次数;网格不存在时返回 1。 */ - int LaneWeakBoostIterations(int laneIndex) const; + int LaneWeakPeakIterations(int laneIndex) const; /** - * @brief 设置某网格的弱波增强迭代次数 N(1–10,超出范围会被夹取)。 + * @brief 设置某网格的弱峰增强迭代次数 N(1–10,超出范围会被夹取)。 * @param [IN] laneIndex 网格下标。 * @param [IN] iterations 迭代次数。 * @return 迭代次数发生变化返回 true。 */ - bool SetLaneWeakBoostIterations(int laneIndex, int iterations); + bool SetLaneWeakPeakIterations(int laneIndex, int iterations); /** - * @brief 设置某网格的弱波增强等级(0–1,超出范围会被夹取)。 + * @brief 设置某网格的弱峰增强等级(0–2,超出范围会被夹取)。 * @param [IN] laneIndex 网格下标。 * @param [IN] level 增强等级。 * @return 等级发生变化返回 true。 */ - bool SetLaneWeakBoostLevel(int laneIndex, double level); + bool SetLaneWeakPeakLevel(int laneIndex, double level); /** - * @brief 获取弱波增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。 + * @brief 获取弱峰增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。 * @param [IN] signal 目标信号。 - * @param [IN] levelPercent 弱波增强等级(0–100,整数百分比)。 + * @param [IN] levelPercent 弱峰增强等级(0–200,整数百分比)。 * @param [IN] iterations 迭代次数 N(1–10)。 * @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。 */ - const std::vector& WeakBoostCurveFor(const SignalData& signal, int levelPercent, + const std::vector& WeakPeakCurveFor(const SignalData& signal, int levelPercent, int iterations) const; /** - * @brief 取某个时刻的弱波增强曲线取值(用于纵向光标的取值标注)。 + * @brief 取某个时刻的弱峰增强曲线取值(用于纵向光标的取值标注)。 * @param [IN] file 信号所属文件。 * @param [IN] signal 目标信号。 - * @param [IN] levelPercent 弱波增强等级(0–100)。 + * @param [IN] levelPercent 弱峰增强等级(0–200)。 * @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 iterations, + bool WeakPeakValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int iterations, double time, double& value) const; /** - * @brief 切换某个信号在「弱波增强」面板中的预览勾选状态。 + * @brief 切换某个信号在「弱峰增强」面板中的预览勾选状态。 * @param [IN] laneIndex 网格下标。 * @param [IN] ref 目标信号。 * @return 切换后的勾选状态。 */ - bool ToggleLaneWeakBoostSignal(int laneIndex, const SignalRef& ref); + bool ToggleLaneWeakPeakSignal(int laneIndex, const SignalRef& ref); /** - * @brief 查询某个信号在「弱波增强」面板中的预览勾选状态。 + * @brief 查询某个信号在「弱峰增强」面板中的预览勾选状态。 * @param [IN] laneIndex 网格下标。 * @param [IN] ref 目标信号。 * @return 已勾选返回 true。 */ - bool LaneWeakBoostSignalEnabled(int laneIndex, const SignalRef& ref) const; + bool LaneWeakPeakSignalEnabled(int laneIndex, const SignalRef& ref) const; /** * @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。 @@ -1089,7 +1089,7 @@ namespace jdi const std::vector& timeTicks); /** - * @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱波增强),叠加在原信号之上。 + * @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱峰增强),叠加在原信号之上。 * * 从 `PaintLane` 抽出,供「宽缓冲区」前台路径复用:宽缓冲只存"基底"(网格/波形), * 派生曲线每帧作为叠加层单独绘制,因此拖派生参数滑块时无需重画波形。 @@ -1158,8 +1158,8 @@ namespace jdi gd::RectF close; /**< ✕ 关闭按钮。 */ gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */ gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */ - gd::RectF slider; /**< 「弱波增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */ - gd::RectF sliderIterations; /**< 「弱波增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */ + gd::RectF slider; /**< 「弱峰增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */ + gd::RectF sliderIterations; /**< 「弱峰增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */ int rows = 0; /**< 统计行数(每条可见信号一行)。 */ /** * @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。 @@ -1294,20 +1294,20 @@ namespace jdi * @brief 取得某个采样区间内的派生曲线(带缓存)。 * * ⚠️ **性能关键**:派生曲线只用于“看得见的那一段”,而整条信号可能有数千万个采样点 - * (VERY_BIG_DATA 为 2576 万)。若每次都重算整条曲线(实测弱波增强 ~800ms/条), + * (VERY_BIG_DATA 为 2576 万)。若每次都重算整条曲线(实测弱峰增强 ~800ms/条), * 拖动等级/迭代次数滑块时每帧要花几秒钟,交互完全卡死。 * 本函数只计算“所需区间 ± 保护带”,并按(信号 + 参数 + 对齐后的区间)缓存: * 实测单条降到 1~3ms,滑块拖动与平移因此恢复流畅。 * * 保护带保证可见区间内的取值与“整条曲线”一致:均值滤波的核是有限宽度, - * 弱波增强的修正沿相邻极值向内传播(每迭代一次传一个极值), + * 弱峰增强的修正沿相邻极值向内传播(每迭代一次传一个极值), * 因此只要求保护带足够宽(见 `kDerivedCurveGuard` 系列常量)。 * * @param [IN] kind 曲线种类。 * @param [IN] signal 目标信号。 * @param [IN] points 均值点数 X(仅 `MEAN_FILTER` 使用)。 * @param [IN] iterations 迭代次数 N。 - * @param [IN] levelPercent 弱波增强等级(仅 `WEAK_BOOST` 使用,0–100)。 + * @param [IN] levelPercent 弱峰增强等级(仅 `WEAK_PEAK` 使用,0–200)。 * @param [IN] begin 需要的采样区间起点(含)。 * @param [IN] end 需要的采样区间终点(不含)。 * @param [OUT] spanBegin 返回数组对应的首个采样下标。 @@ -1336,7 +1336,7 @@ namespace jdi int kind = 0; /**< `DerivedCurveKind`。 */ int points = 5; /**< 均值点数 X。 */ int iterations = 1; /**< 迭代次数 N。 */ - int levelPercent = 0; /**< 弱波增强等级(0–100)。 */ + int levelPercent = 0; /**< 弱峰增强等级(0–200)。 */ bool operator==(const CurveKey& other) const { @@ -1461,14 +1461,14 @@ namespace jdi int points, int iterations) const; /** - * @brief 绘制某条信号的弱波增强曲线(与均值滤波曲线同一配色派生规则)。 + * @brief 绘制某条信号的弱峰增强曲线(与均值滤波曲线同一配色派生规则)。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] resolved 信号视图。 * @param [IN] lane 网格绘制信息。 - * @param [IN] levelPercent 弱波增强等级(0–100)。 + * @param [IN] levelPercent 弱峰增强等级(0–200)。 * @param [IN] iterations 迭代次数 N(1–10)。 */ - void PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, + void PaintWeakPeakCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, int levelPercent, int iterations) const; /** @@ -1685,10 +1685,10 @@ namespace jdi bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */ int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */ - int m_pendingWeakBoostLane = -1; /**< 待构建的弱波增强信号所属网格下标(-1 表示无请求)。 */ + int m_pendingWeakPeakLane = -1; /**< 待构建的弱峰增强信号所属网格下标(-1 表示无请求)。 */ std::vector m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */ - std::vector m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */ - int m_boostDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */ + std::vector m_pendingWeakPeakItems; /**< 待构建的弱峰增强信号及其参数。 */ + int m_peakDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */ bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */ /** * @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。 @@ -1753,18 +1753,18 @@ namespace jdi }; mutable std::deque m_meanCurveCache; - /** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */ - struct WeakBoostCurveEntry + /** 弱峰增强曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */ + struct WeakPeakCurveEntry { const SignalData* signal = nullptr; - int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ + int levelPercent = 150; /**< 弱峰增强等级(0–200)。 */ int iterations = 1; /**< 迭代次数 N(1–10)。 */ std::vector values; /**< 与 signal->values 等长;缺失处为 NaN。 */ }; - mutable std::deque m_weakBoostCurveCache; + mutable std::deque m_weakPeakCurveCache; // ---- 异步派生曲线计算(后台线程 + 双缓冲)---- - // 设计:预览曲线(均值滤波 / 弱波增强)在窗口很大时**不在绘制线程里计算**,而是交给后台 + // 设计:预览曲线(均值滤波 / 弱峰增强)在窗口很大时**不在绘制线程里计算**,而是交给后台 // 线程;绘制线程本帧先用上一条已算好的曲线顶替(双缓冲),后台算完后通过 // `kMessagePlotDerivedReady` 通知宿主重绘,从而切换为最新曲线。这样滑块 / 光标 / 视界 / // 缩放等交互的反馈始终跟手,不会被计算拖住,同时把重计算放到后台充分利用多核。 @@ -1941,7 +1941,7 @@ namespace jdi /** * @brief 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**。 * - * 拖弱波增强等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化; + * 拖弱峰增强等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化; * 因此装饰层缓存不应因参数变化而失效(否则拖滑块每帧都要重绘图例与刻度)。 * * @return 指纹值。 @@ -1971,20 +1971,20 @@ namespace jdi * * 它**只影响网格内容的绘制**(不影响网格结构/视图几何),因此不进入 * `SceneGeometryHash`,而是与外观样式一起作为“局部重画”的判据: - * 拖动弱波增强的等级/迭代次数滑块时,只需重画受影响的网格, + * 拖动弱峰增强的等级/迭代次数滑块时,只需重画受影响的网格, * 不必整幅重绘(也不会把静态场景缓存整体作废)。 */ struct SceneDerivedEntry { int edgeCenter = 0; /**< 中心轴边沿计数开关。 */ int meanFilter = 0; /**< 均值滤波开关。 */ - int weakBoost = 0; /**< 弱波增强开关。 */ - int weakBoostLevelPercent = 0; /**< 弱波增强等级(0–100)。 */ - int weakBoostIterations = 1; /**< 弱波增强迭代次数 N。 */ + int weakPeak = 0; /**< 弱峰增强开关。 */ + int weakPeakLevelPercent = 0; /**< 弱峰增强等级(0–200)。 */ + int weakPeakIterations = 1; /**< 弱峰增强迭代次数 N。 */ /** 各条目展开为 [文件, 信号, 均值点数 X, 迭代次数 N],按原顺序(稳定)。 */ std::vector edgeCenterItems; std::vector meanFilterItems; - std::vector weakBoostItems; + std::vector weakPeakItems; }; /** @@ -2222,20 +2222,25 @@ namespace jdi mutable int m_decorOriginX = 0; mutable int m_decorOriginY = 0; - // ---- 每网格「信号图层」:双缓冲 + 后台绘制(前台只做贴图 / 坐标偏移) ---- + // ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ---- // // 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图** - // (宽度 = 视界宽 ×3:左右各多渲染半个视界),称为该网格的「信号图层」。 - // 图层只含基底(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例)—— + // (宽度 = 视界宽 + 左右各半个视界的余量),称为该网格的「信号图层」。 + // 图层只含基底(底色 + 网格线 + 波形),**不含装饰**(Y 轴刻度 / 图例)—— // 装饰锚定在网格左缘、不随时间视界移动,若烘进图层会被平移带走(重影), // 因此装饰由前台单独绘制(见 `m_decorCapturedRects` 那套缓存)。 // - // **双缓冲**:每个网格有两块图层(`m_layerFront` 前台读取 / `m_layerBack` 后台填充), - // 后台绘制完成后再整体交换。前台**永远只读 `m_layerFront`**,因此后台正在绘制时 - // 前台仍能拿到上一份可用像素,绘制过程不影响交互响应。 + // ⚠️ **图层一律在 UI 线程内同步维护**(`Paint` 全程持有 `m_renderMutex`), + // 因为渲染图层会临时改写 `m_area` / `m_plotRect` / `Tab().ViewMin()/ViewMax()` / + // 各网格矩形 —— 这些状态**同时被 UI 线程的消息处理无锁读取** + // (`OnMouseMove` 平移读视界、命中测试读网格矩形)。若把图层渲染搬到后台线程, + // 后台的"保存 → 改写 → 恢复"会**覆盖用户正在进行的平移**(视界被弹回原位), + // 命中测试也会读到错位的网格 → 表现为「拖动时视界跳回 / 波形抖动 / 绘制错乱」。 + // 这正是先前"后台图层线程"被移除的原因:**不要把图层渲染移回后台**。 // - // **后台绘制**:跨度 / 几何 / 外观变化时,后台线程池把各网格的新图层画进后台缓冲; - // 若前台已经要刷新而后台还没画好,就继续用旧的前台缓冲「应付」交互,不解切换。 + // 每个图层记录自己的时间映射(`LaneLayer::wideMin`):不同时刻渲染出的图层锚点 + // 并不相同,滚动与贴图一律**逐网格**按各自的 `wideMin` 计算,**绝不共用一个全局锚点** + // (共用全局锚点会让各网格被平移不同的真实距离 → 网格之间时间错位)。 /** * @brief 单个网格的「信号图层」(一块只含该网格基底的离屏位图)。 */ @@ -2264,18 +2269,28 @@ namespace jdi * @param [IN,OUT] layer 目标图层(会被写入尺寸 / 时间映射 / 指纹)。 * @param [IN] stripX 只渲染位图的这一段 x 区间(滚动时补画细条用)。 * @param [IN] stripW 该区间的宽度;传 0 表示渲染整块。 + * @param [IN] wideMin 位图 `x=0` 对应的时间。 + * **必须与调用方的意图一致**:整块重画传 `LayerDefaultWideMin()`(以当前视界居中); + * 滚动补细条则传 `layer.wideMin`(与该图层已有像素同一映射,否则会留下接缝)。 * @return 渲染成功返回 true。 */ - bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW); + bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW, + double wideMin); + /** @brief 「以当前视界居中」的默认图层映射(位图 `x=0` 对应的时间)。 */ + double LayerDefaultWideMin() const; /** - * @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧时使用)。 + * @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧 / 图层缺失时使用)。 * @return 全部成功返回 true。 */ bool RebuildWideBuffer(); /** - * @brief 让全部图层"滚动"以跟随视界:整数像素自贴图平移 + 只补画新暴露的细条。 - * @return 图层仍可用返回 true。 + * @brief 让各图层"滚动"以跟随视界:逐网格整数像素自贴图平移 + 只补画新暴露的细条。 + * + * 每个网格按**自己的** `wideMin` 计算位移;位移过大(锚点离当前视界太远)或内容指纹 + * 与当前状态不一致的网格直接整块重画。任一网格图层缺失(尺寸 / 跨度不符)返回 false, + * 由调用方先 `RebuildWideBuffer()`。 + * @return 全部网格都能用现成像素跟随视界返回 true。 */ bool RollWideBuffer(); /** @@ -2285,62 +2300,26 @@ namespace jdi */ bool BlitWideBuffer(gd::Graphics& g) const; /** - * @brief 全部图层是否已就绪且跨度与当前视界一致(**不要求内容也一致**)。 + * @brief 全部图层是否已就绪且跨度与当前视界一致。 * - * 内容一致与否由后台线程负责跟踪:只要跨度一致、图层能覆盖当前视界, - * 前台就直接用现成像素“应付”交互(内容稍旧的一两帧由后台画好后交换), - * 这样样式/参数变化也不会退化成整幅重绘。 - * @return 可以直接贴图层像素返回 true。 + * 只检查"位图尺寸 / 网格数量 / 视界跨度"这类**结构性**条件(内容指纹由 + * `RollWideBuffer()` 逐个网格处理);不满足时应先 `RebuildWideBuffer()`。 + * @return 可以直接滚动 + 贴图层像素返回 true。 */ bool LayersReadyForView() const; /** @brief 使全部图层失效。 */ void InvalidateWideBuffer(); - /** @brief 释放全部图层位图(前后台都释放)。 */ + /** @brief 释放全部图层位图。 */ void ReleaseWideBuffer(); + + std::vector m_layerFront; /**< 各网格的「信号图层」(与 `m_lanes` 同序)。 */ /** - * @brief 为"内容与前台图层不一致"的网格排一次后台渲染任务(去重)。 - * - * 后台渲染完成后会与前台缓冲交换并通知宿主重绘,前台因此始终有立即可用的像素。 + * @brief 图层元数据的互斥(当前图层只在 UI 线程维护,故它实际不承担跨线程保护; + * 保留是为了给 `BlitWideBuffer()` 这类 `const` 读取路径一个明确的一致性边界)。 */ - void ScheduleStaleLayerRenders(); - - /** @brief 确保后台图层绘制线程已启动。 */ - void EnsureLayerWorker(); - /** @brief 停止后台图层绘制线程。 */ - void StopLayerWorker(); - /** @brief 后台图层绘制线程主循环。 */ - void LayerWorkerLoop(); - - /** 单个网格图层任务:只需网格下标(参数在渲染时按最新状态取)。 */ - struct LayerJob - { - int laneIndex = -1; - std::uint64_t key = 0; - }; - - std::vector m_layerFront; /**< 前台读取(后台不会就地改写)。 */ - std::vector m_layerBack; /**< 后台填充。 */ - mutable std::mutex m_layerMutex; /**< 保护前后台切换 / 元数据读取 / 锚点(const 方法也要读,故 mutable)。 */ - std::recursive_mutex m_renderMutex; /**< 串行化“改写渲染状态”的绘制(前台整帧 vs 后台单网格);可重入(整帧内部会再调图层渲染)。 */ - double m_layerAnchorMin = 0.0; /**< 图层滚动锚点:视界左端对应的时间。 */ - bool m_layerAnchorValid = false; /**< 锚点是否有效。 */ - - std::thread m_layerWorker; /**< 后台图层绘制线程。 */ - std::mutex m_layerJobMutex; /**< 任务队列互斥。 */ - std::condition_variable m_layerJobCv; /**< 任务到来 / 退出通知。 */ - std::vector m_layerJobs; /**< 待渲染的网格任务。 */ - bool m_layerWorkerStop = false; /**< 请求退出后台线程。 */ - /** - * @brief 连续排了多少次"图层重建"却始终没与前台一致。 - * - * 正常情况下一两次就会收敛(后台画完→交换→指纹一致)。若持续增长,说明后台渲染 - * 算出的指纹与前台不一致(例如某个参与指纹的状态没被正确还原),此时改为**同步重建** - * 一次强行收敛,避免"前台永远不等后台、后台永远画不好"的死循环。 - */ - int m_layerRescheduleStreak = 0; - /** 前台最近一次 `Paint` 的时刻(毫秒,`GetTickCount64`)。后台据此"让路",不与前台抢渲染锁。 */ - mutable std::atomic m_lastPaintTick{0}; - /** 后台线程已完成绘制的图层数量(原子,仅用于验证/诊断)。 */ + mutable std::mutex m_layerMutex; + std::recursive_mutex m_renderMutex; /**< 串行化"改写渲染状态"的绘制(整帧 / 单网格图层);可重入。 */ + /** 已渲染的图层区域数量(原子,仅用于验证 / 诊断)。 */ mutable std::atomic m_layerRenderCount{0}; bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */ diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index f1ce35a..ed10eca 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -68,11 +68,15 @@ namespace jdi const float kEdgePanelMeanEditHeight = 24.0f; /**< 逐条目面板中每条信号的参数行高度。 */ const float kEdgePanelMeanRowGap = 2.0f; /**< 名称行与参数行之间的间距。 */ const float kEdgePanelMeanCheckSize = 15.0f; /**< 逐条目面板中条目复选框尺寸。 */ - const float kWeakBoostSliderRowHeight = 26.0f; /**< 弱波增强面板中每个滑块行的高度。 */ - const float kWeakBoostTrackHeight = 6.0f; /**< 弱波增强滑块的轨道高度。 */ - const float kWeakBoostTrackMinWidth = 120.0f; /**< 弱波增强滑块轨道的最小宽度。 */ - const int kWeakBoostIterationsMin = 1; /**< 迭代次数 N 的下限。 */ - const int kWeakBoostIterationsMax = 10; /**< 迭代次数 N 的上限。 */ + const float kWeakPeakSliderRowHeight = 26.0f; /**< 弱峰增强面板中每个滑块行的高度。 */ + const float kWeakPeakTrackHeight = 6.0f; /**< 弱峰增强滑块的轨道高度。 */ + const float kWeakPeakTrackMinWidth = 120.0f; /**< 弱峰增强滑块轨道的最小宽度。 */ + const int kWeakPeakIterationsMin = 1; /**< 迭代次数 N 的下限。 */ + const int kWeakPeakIterationsMax = 10; /**< 迭代次数 N 的上限。 */ + /** 弱峰增强等级上限(面板上显示为 200%)。 */ + const double kWeakPeakLevelMax = 2.0; + /** 弱峰增强等级百分比上限(用于构建新信号 / 曲线缓存键)。 */ + const int kWeakPeakPercentMax = 200; /** * @brief 派生曲线「区间窗口」缓存的保护带上限(采样点数)。 * @@ -89,19 +93,19 @@ namespace jdi * @brief 派生(预览)曲线走「后台异步计算」的窗口采样数门槛。 * * 拖动滑块 / 移动视界 / 缩放每帧都会请求预览曲线。窗口不超过本门槛时同步重算足够快 - * (弱波增强 ≈0.9ms/6.5 万采样),预览曲线能**实时跟随控件**;一旦窗口超过门槛 - * (大时间跨度、整条信号),同步重算会阻塞交互(弱波增强整条信号单条 ≈368ms), + * (弱峰增强 ≈0.9ms/6.5 万采样),预览曲线能**实时跟随控件**;一旦窗口超过门槛 + * (大时间跨度、整条信号),同步重算会阻塞交互(弱峰增强整条信号单条 ≈368ms), * 此时改为交后台线程计算、本帧先用旧曲线顶替(双缓冲),保证控件反馈始终跟手。 */ const std::size_t kDerivedAsyncWindowThreshold = 65536; - const wchar_t* const kWeakBoostLevelLabel = L"弱波增强等级"; /**< 等级滑块行左侧的说明文字。 */ - const wchar_t* const kWeakBoostIterationsLabel = L"迭代次数 N"; /**< 迭代次数滑块行左侧的说明文字。 */ - const wchar_t* const kWeakBoostTitle = L"弱波增强"; /**< 弱波增强面板标题。 */ - const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱波增强共用)。 */ + const wchar_t* const kWeakPeakLevelLabel = L"弱峰增强等级"; /**< 等级滑块行左侧的说明文字。 */ + const wchar_t* const kWeakPeakIterationsLabel = L"迭代次数 N"; /**< 迭代次数滑块行左侧的说明文字。 */ + const wchar_t* const kWeakPeakTitle = L"弱峰增强"; /**< 弱峰增强面板标题。 */ + const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰增强共用)。 */ const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */ /** - * @brief 判断坐标是否命中「弱波增强」的等级滑块。 + * @brief 判断坐标是否命中「弱峰增强」的等级滑块。 * * 轨道本身只有几个像素高,直接按轨道矩形命中很难抓住,因此把命中区在纵向上下各放宽 * (至少一个轨道高度),与样式区滑块的交互一致。 @@ -112,7 +116,7 @@ namespace jdi * @param [IN] y 客户区纵坐标。 * @return 命中返回 true。 */ - bool WeakBoostSliderHit(const gd::RectF& track, float scale, float x, float y) + bool WeakPeakSliderHit(const gd::RectF& track, float scale, float x, float y) { if (track.Width <= 0.0f) { @@ -281,7 +285,7 @@ namespace jdi * 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。 */ const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER, - EdgePanelKind::MEAN, EdgePanelKind::BOOST}; + EdgePanelKind::MEAN, EdgePanelKind::PEAK}; /** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */ const int kEdgePanelKindCount = static_cast(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0])); @@ -299,7 +303,7 @@ namespace jdi * @param [IN] kind `DerivedCurveKind` 的整型值。 * @param [IN] points 均值点数 X。 * @param [IN] iterations 迭代次数 N。 - * @param [IN] levelPercent 弱波增强等级(0–100)。 + * @param [IN] levelPercent 弱峰增强等级(0–200)。 * @return 曲线数值(长度 = 有效窗口长度)。 */ std::vector ComputeDerivedWindow(const SignalData& signal, std::size_t windowBegin, @@ -316,9 +320,9 @@ namespace jdi } const std::vector window(values.begin() + static_cast(lo), values.begin() + static_cast(hi)); - if (kind == static_cast(DerivedCurveKind::WEAK_BOOST)) + if (kind == static_cast(DerivedCurveKind::WEAK_PEAK)) { - ComputeWeakWaveBoost(window, static_cast(levelPercent) / 100.0, iterations, out); + ComputeWeakPeakBoost(window, static_cast(levelPercent) / 100.0, iterations, out); } else { @@ -671,8 +675,6 @@ namespace jdi { // 先停掉后台计算线程:它可能正在读取采样数据,必须在释放其它资源前回收。 StopCurveWorker(); - // 再停掉后台「网格图层」绘制线程:它会读写渲染状态与图层位图。 - StopLayerWorker(); ReleaseSceneCache(); ReleaseDecorCache(); ReleaseWideBuffer(); @@ -1297,7 +1299,7 @@ namespace jdi m_edgeCountCache.clear(); m_edgeCenterCountCache.clear(); m_meanCurveCache.clear(); - m_weakBoostCurveCache.clear(); + m_weakPeakCurveCache.clear(); // 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。 m_hoverSignal = SignalRef(); m_hoverLegend = SignalRef(); @@ -1894,152 +1896,152 @@ namespace jdi return m_tab->Lanes()[static_cast(laneIndex)].meanFilter; } - bool PlotView::LaneWeakBoost(int laneIndex) const + bool PlotView::LaneWeakPeak(int laneIndex) const { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return false; } - return m_tab->Lanes()[static_cast(laneIndex)].weakBoost; + return m_tab->Lanes()[static_cast(laneIndex)].weakPeak; } - double PlotView::LaneWeakBoostLevel(int laneIndex) const + double PlotView::LaneWeakPeakLevel(int laneIndex) const { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return 0.5; } - return m_tab->Lanes()[static_cast(laneIndex)].weakBoostLevel; + return m_tab->Lanes()[static_cast(laneIndex)].weakPeakLevel; } - int PlotView::LaneWeakBoostIterations(int laneIndex) const + int PlotView::LaneWeakPeakIterations(int laneIndex) const { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { - return kWeakBoostIterationsMin; + return kWeakPeakIterationsMin; } - return m_tab->Lanes()[static_cast(laneIndex)].weakBoostIterations; + return m_tab->Lanes()[static_cast(laneIndex)].weakPeakIterations; } - bool PlotView::SetLaneWeakBoostIterations(int laneIndex, int iterations) + bool PlotView::SetLaneWeakPeakIterations(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(kWeakBoostIterationsMin, std::min(kWeakBoostIterationsMax, iterations)); - if (lane.weakBoostIterations == clamped) + const int clamped = std::max(kWeakPeakIterationsMin, std::min(kWeakPeakIterationsMax, iterations)); + if (lane.weakPeakIterations == clamped) { return false; } - lane.weakBoostIterations = clamped; + lane.weakPeakIterations = clamped; // 迭代次数变化会改变预览曲线:清掉整条曲线的缓存即可。 // ⚠️ **不要**再 `InvalidateSceneCache()`:那会整幅重绘(5 个网格、上万个顶点), // 拖滑块时每帧都发生;改为依赖 `m_sceneDerived` 快照 → 只重画受影响的网格。 - m_weakBoostCurveCache.clear(); + m_weakPeakCurveCache.clear(); return true; } - bool PlotView::SetLaneWeakBoostLevel(int laneIndex, double level) + bool PlotView::SetLaneWeakPeakLevel(int laneIndex, double level) { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return false; } Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; - const double clamped = std::max(0.0, std::min(1.0, level)); - if (lane.weakBoostLevel == clamped) + const double clamped = std::max(0.0, std::min(kWeakPeakLevelMax, level)); + if (lane.weakPeakLevel == clamped) { return false; } - lane.weakBoostLevel = clamped; - // 等级变化会改变预览曲线:清掉整条曲线的缓存即可(同 `SetLaneWeakBoostIterations`, + lane.weakPeakLevel = clamped; + // 等级变化会改变预览曲线:清掉整条曲线的缓存即可(同 `SetLaneWeakPeakIterations`, // 故意**不**作废静态场景缓存,以便走"只重画受影响网格"的快路径)。 - m_weakBoostCurveCache.clear(); + m_weakPeakCurveCache.clear(); return true; } - bool PlotView::SetLaneWeakBoost(int laneIndex, bool enabled) + bool PlotView::SetLaneWeakPeak(int laneIndex, bool enabled) { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return false; } Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; - if (lane.weakBoost == enabled) + if (lane.weakPeak == enabled) { return false; } - lane.weakBoost = enabled; + lane.weakPeak = enabled; if (enabled) { // 与「均值滤波」完全一致:默认只勾选网格内**第一个可见信号**。 - if (lane.weakBoostItems.empty()) + if (lane.weakPeakItems.empty()) { for (const SignalRef& ref : lane.signals) { const ResolvedSignal resolved = ResolveSignal(ref); if (resolved.data != nullptr && resolved.visible) { - lane.EnsureWeakBoostItem(ref); + lane.EnsureWeakPeakItem(ref); break; } } } - lane.weakBoostPanelOpen = true; + lane.weakPeakPanelOpen = true; } else { - lane.weakBoostPanelOpen = false; + lane.weakPeakPanelOpen = false; } // 预建「宽缓冲基底」(不含派生曲线),使用户首次拖动该滑块时无需从零重建(避免一次性尖峰)。 RebuildWideBuffer(); return true; } - bool PlotView::ToggleLaneWeakBoost(int laneIndex) + bool PlotView::ToggleLaneWeakPeak(int laneIndex) { - const bool enabled = !LaneWeakBoost(laneIndex); - SetLaneWeakBoost(laneIndex, enabled); + const bool enabled = !LaneWeakPeak(laneIndex); + SetLaneWeakPeak(laneIndex, enabled); return enabled; } - bool PlotView::ToggleLaneWeakBoostSignal(int laneIndex, const SignalRef& ref) + bool PlotView::ToggleLaneWeakPeakSignal(int laneIndex, const SignalRef& ref) { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return false; } Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; - if (lane.WeakBoostEnabled(ref)) + if (lane.WeakPeakEnabled(ref)) { - lane.RemoveWeakBoostItem(ref); + lane.RemoveWeakPeakItem(ref); return false; } - lane.EnsureWeakBoostItem(ref); + lane.EnsureWeakPeakItem(ref); return true; } - bool PlotView::LaneWeakBoostSignalEnabled(int laneIndex, const SignalRef& ref) const + bool PlotView::LaneWeakPeakSignalEnabled(int laneIndex, const SignalRef& ref) const { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) { return false; } - return m_tab->Lanes()[static_cast(laneIndex)].WeakBoostEnabled(ref); + return m_tab->Lanes()[static_cast(laneIndex)].WeakPeakEnabled(ref); } - bool PlotView::TakePendingWeakBoostBuild(int& laneIndex, std::vector& items) + bool PlotView::TakePendingWeakPeakBuild(int& laneIndex, std::vector& items) { - if (m_pendingWeakBoostLane < 0) + if (m_pendingWeakPeakLane < 0) { return false; } - laneIndex = m_pendingWeakBoostLane; - items = m_pendingWeakBoostItems; - m_pendingWeakBoostLane = -1; - m_pendingWeakBoostItems.clear(); + laneIndex = m_pendingWeakPeakLane; + items = m_pendingWeakPeakItems; + m_pendingWeakPeakLane = -1; + m_pendingWeakPeakItems.clear(); return true; } @@ -2197,7 +2199,7 @@ namespace jdi const int kindValue = static_cast(kind); const int safePoints = std::max(0, std::min(points, 1000000)); const int safeIterations = std::max(1, std::min(iterations, 1000)); - const int safeLevel = std::max(0, std::min(100, levelPercent)); + const int safeLevel = std::max(0, std::min(kWeakPeakPercentMax, levelPercent)); // 对齐到粗网格:轻微平移/缩放时可直接复用同一条窗口,避免每帧重算。 const std::size_t grid = std::max(4096, (end - begin) / 4); @@ -2249,7 +2251,7 @@ namespace jdi std::size_t alignedEnd, int points, int iterations, std::size_t count, std::size_t& begin, std::size_t& end) const { - // 保护带取窗口长度的 1/8(夹在 4096~65536):弱波增强的修正沿相邻极值向内传播, + // 保护带取窗口长度的 1/8(夹在 4096~65536):弱峰增强的修正沿相邻极值向内传播, // 1/8 个可见宽度足以覆盖真实信号的极值间距。 // 均值滤波必须额外**覆盖滤波半径**:N 阶共向外传播 X*N 个采样,否则窗口边界取值 // 会与「整条曲线」不同(边界上可用采样数更少)。 @@ -2347,7 +2349,7 @@ namespace jdi { std::lock_guard lock(m_curveMutex); // 同一「信号 + 曲线种类」只保留**一个**待算任务,并以**最新参数**为准: - // 拖弱波增强等级/迭代次数滑块时每帧参数都变,若每次都入队会把队列灌满整段曲线任务, + // 拖弱峰增强等级/迭代次数滑块时每帧参数都变,若每次都入队会把队列灌满整段曲线任务, // 后台线程池被淹没 → 与 UI 抢 CPU → 拖动反而卡顿。这里把同 (signal, kind) 的旧待算任务 // 全部移除(正在计算的那个由工作线程另行负责,其结果是旧参数,会被后续条目取代)。 bool identicalPending = false; @@ -2547,12 +2549,12 @@ namespace jdi return m_meanCurveCache.back().values; } - const std::vector& PlotView::WeakBoostCurveFor(const SignalData& signal, + const std::vector& PlotView::WeakPeakCurveFor(const SignalData& signal, int levelPercent, int iterations) const { - const int level = std::max(0, std::min(100, levelPercent)); - const int stages = std::max(kWeakBoostIterationsMin, std::min(kWeakBoostIterationsMax, iterations)); - for (const WeakBoostCurveEntry& entry : m_weakBoostCurveCache) + const int level = std::max(0, std::min(kWeakPeakPercentMax, levelPercent)); + const int stages = std::max(kWeakPeakIterationsMin, std::min(kWeakPeakIterationsMax, iterations)); + for (const WeakPeakCurveEntry& entry : m_weakPeakCurveCache) { if (entry.signal == &signal && entry.levelPercent == level && entry.iterations == stages) { @@ -2560,19 +2562,19 @@ namespace jdi } } - WeakBoostCurveEntry entry; + WeakPeakCurveEntry entry; entry.signal = &signal; entry.levelPercent = level; entry.iterations = stages; // 与「构建为新信号」共用同一实现(Utilities 内的自由函数), // 因此预览曲线与构建出的采样值逐位一致。 - ComputeWeakWaveBoost(signal.values, static_cast(level) / 100.0, stages, entry.values); + ComputeWeakPeakBoost(signal.values, static_cast(level) / 100.0, stages, entry.values); - m_weakBoostCurveCache.push_back(std::move(entry)); - return m_weakBoostCurveCache.back().values; + m_weakPeakCurveCache.push_back(std::move(entry)); + return m_weakPeakCurveCache.back().values; } - bool PlotView::WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, + bool PlotView::WeakPeakValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int iterations, double time, double& value) const { const int index = FindNearestSample(file, time); @@ -2582,7 +2584,7 @@ namespace jdi } std::size_t spanBegin = 0; const std::vector* curve = CurveSpanFor( - DerivedCurveKind::WEAK_BOOST, signal, 0, iterations, levelPercent, + DerivedCurveKind::WEAK_PEAK, signal, 0, iterations, levelPercent, static_cast(index), static_cast(index) + 1, spanBegin); if (curve == nullptr || static_cast(index) < spanBegin || (static_cast(index) - spanBegin) >= curve->size()) @@ -2886,15 +2888,15 @@ namespace jdi const Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; const bool centerAxis = (kind == EdgePanelKind::CENTER); const bool meanFilter = (kind == EdgePanelKind::MEAN); - const bool weakBoost = (kind == EdgePanelKind::BOOST); - // 「均值滤波」「中心轴边沿计数」「弱波增强」都是逐条目面板(名称 + 复选框)。 - const bool itemized = (centerAxis || meanFilter || weakBoost); + const bool weakPeak = (kind == EdgePanelKind::PEAK); + // 「均值滤波」「中心轴边沿计数」「弱峰增强」都是逐条目面板(名称 + 复选框)。 + const bool itemized = (centerAxis || meanFilter || weakPeak); - // 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱波增强” + // 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰增强” // + 光标区间尾缀)。 const std::wstring title = meanFilter ? std::wstring(L"均值滤波") - : (weakBoost ? std::wstring(kWeakBoostTitle) + : (weakPeak ? std::wstring(kWeakPeakTitle) : ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix())); float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale + 8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) + @@ -2918,7 +2920,7 @@ namespace jdi } std::size_t rising = 0; std::size_t falling = 0; - if (meanFilter || weakBoost) + if (meanFilter || weakPeak) { continue; } @@ -2958,19 +2960,19 @@ namespace jdi width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth); } - if (weakBoost) + if (weakPeak) { // 两个滑块行(等级 / 迭代次数 N)= 「说明文字 + 轨道 + 数值」。 - const float labelWidth = std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel), - UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostIterationsLabel)); + const float labelWidth = std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakLevelLabel), + UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakIterationsLabel)); 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 + + const float rowWidth = labelWidth + 6.0f * m_scale + kWeakPeakTrackMinWidth * m_scale + 6.0f * m_scale + valueWidth; width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth); } - if (meanFilter || weakBoost) + if (meanFilter || weakPeak) { // 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。 const float buttonWidth = @@ -3897,11 +3899,11 @@ namespace jdi case PlotDragMode::CURSOR_SPAN_MOVE: // 差值标签拖拽:两条光标整体沿时间轴平移 → 左右箭头。 return LoadCursorW(nullptr, IDC_SIZEWE); - case PlotDragMode::BOOST_LEVEL: - // 「弱波增强」等级滑块拖拽 → 左右箭头。 + case PlotDragMode::PEAK_LEVEL: + // 「弱峰增强」等级滑块拖拽 → 左右箭头。 return LoadCursorW(nullptr, IDC_SIZEWE); - case PlotDragMode::BOOST_ITERATIONS: - // 「弱波增强」迭代次数 N 滑块拖拽 → 左右箭头。 + case PlotDragMode::PEAK_ITERATIONS: + // 「弱峰增强」迭代次数 N 滑块拖拽 → 左右箭头。 return LoadCursorW(nullptr, IDC_SIZEWE); case PlotDragMode::MOVE_SIGNAL: // 拖动信号(参考 Photoshop 平移元素的抓握光标)。 @@ -3948,16 +3950,16 @@ namespace jdi { return LoadCursorW(nullptr, IDC_SIZEALL); } - if (kind == EdgePanelKind::BOOST && - (WeakBoostSliderHit(layout.slider, m_scale, static_cast(x), + if (kind == EdgePanelKind::PEAK && + (WeakPeakSliderHit(layout.slider, m_scale, static_cast(x), static_cast(y)) || - WeakBoostSliderHit(layout.sliderIterations, m_scale, static_cast(x), + WeakPeakSliderHit(layout.sliderIterations, m_scale, static_cast(x), static_cast(y)))) { // 等级 / 迭代次数滑块:左右箭头表示可左右拖动。 return LoadCursorW(nullptr, IDC_SIZEWE); } - if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::BOOST) && + if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::PEAK) && layout.button.Width > 0.0f && UiPointInRect(layout.button, static_cast(x), static_cast(y))) { @@ -4206,41 +4208,41 @@ namespace jdi m_dragMoved = true; return true; } - case PlotDragMode::BOOST_LEVEL: + case PlotDragMode::PEAK_LEVEL: { - // 「弱波增强」等级滑块:按滑轨上的横向位置换算 0–100%。 - if (m_tab == nullptr || m_boostDragLane < 0 || - m_boostDragLane >= static_cast(m_lanes.size())) + // 「弱峰增强」等级滑块:按滑轨上的横向位置换算 0–200%。 + if (m_tab == nullptr || m_peakDragLane < 0 || + m_peakDragLane >= static_cast(m_lanes.size())) { break; } - const EdgePanelLayout layout = EdgePanelLayoutFor(m_boostDragLane, EdgePanelKind::BOOST); + const EdgePanelLayout layout = EdgePanelLayoutFor(m_peakDragLane, EdgePanelKind::PEAK); if (layout.slider.Width <= 0.0f) { break; } const double ratio = UiSliderRatioFromX(layout.slider, fx); - SetLaneWeakBoostLevel(m_boostDragLane, ratio); + SetLaneWeakPeakLevel(m_peakDragLane, ratio * kWeakPeakLevelMax); m_dragMoved = true; return true; } - case PlotDragMode::BOOST_ITERATIONS: + case PlotDragMode::PEAK_ITERATIONS: { - // 「弱波增强」迭代次数 N 滑块:按滑轨上的横向位置换算 1–10(整数)。 - if (m_tab == nullptr || m_boostDragLane < 0 || - m_boostDragLane >= static_cast(m_lanes.size())) + // 「弱峰增强」迭代次数 N 滑块:按滑轨上的横向位置换算 1–10(整数)。 + if (m_tab == nullptr || m_peakDragLane < 0 || + m_peakDragLane >= static_cast(m_lanes.size())) { break; } - const EdgePanelLayout layout = EdgePanelLayoutFor(m_boostDragLane, EdgePanelKind::BOOST); + const EdgePanelLayout layout = EdgePanelLayoutFor(m_peakDragLane, EdgePanelKind::PEAK); if (layout.sliderIterations.Width <= 0.0f) { break; } const double ratio = UiSliderRatioFromX(layout.sliderIterations, fx); - const int iterations = kWeakBoostIterationsMin + static_cast(std::lround( - ratio * (kWeakBoostIterationsMax - kWeakBoostIterationsMin))); - SetLaneWeakBoostIterations(m_boostDragLane, iterations); + const int iterations = kWeakPeakIterationsMin + static_cast(std::lround( + ratio * (kWeakPeakIterationsMax - kWeakPeakIterationsMin))); + SetLaneWeakPeakIterations(m_peakDragLane, iterations); m_dragMoved = true; return true; } @@ -4319,9 +4321,9 @@ namespace jdi lane.meanFilterPanelX = left; lane.meanFilterPanelY = top; break; - case EdgePanelKind::BOOST: - lane.weakBoostPanelX = left; - lane.weakBoostPanelY = top; + case EdgePanelKind::PEAK: + lane.weakPeakPanelX = left; + lane.weakPeakPanelY = top; break; case EdgePanelKind::COUNT: default: @@ -4453,7 +4455,7 @@ namespace jdi m_rubberRect = gd::RectF(); m_rubberAxis = PlotRubberAxis::UNDECIDED; m_cursorGrabOffset = 0; - m_boostDragLane = -1; + m_peakDragLane = -1; // 新的交互开始:清掉上一次留下的提示(拖动过程中会重新设置粘滞提示)。 SetHint(std::wstring()); // 上一轮尚未抬起确认的面板关闭请求一律作废。 @@ -4566,33 +4568,33 @@ namespace jdi return true; } - // 「中心轴边沿计数」/「均值滤波」/「弱波增强」面板:逐信号条目 + // 「中心轴边沿计数」/「均值滤波」/「弱峰增强」面板:逐信号条目 // (同一套「名称 + 复选框」排布,只是复选框绑定的条目列表不同)。 if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER || - kind == EdgePanelKind::BOOST) && + kind == EdgePanelKind::PEAK) && m_tab != nullptr) { const bool meanMode = (kind == EdgePanelKind::MEAN); - const bool boostMode = (kind == EdgePanelKind::BOOST); + const bool boostMode = (kind == EdgePanelKind::PEAK); Lane& tabLane = m_tab->Lanes()[static_cast(index)]; - // 弱波增强的两个滑块:按下即开始拖动(拖动过程中实时更新)。 - if (boostMode && WeakBoostSliderHit(layout.slider, m_scale, fx, fy)) + // 弱峰增强的两个滑块:按下即开始拖动(拖动过程中实时更新)。 + if (boostMode && WeakPeakSliderHit(layout.slider, m_scale, fx, fy)) { - m_dragMode = PlotDragMode::BOOST_LEVEL; - m_boostDragLane = index; - SetLaneWeakBoostLevel(index, UiSliderRatioFromX(layout.slider, fx)); + m_dragMode = PlotDragMode::PEAK_LEVEL; + m_peakDragLane = index; + SetLaneWeakPeakLevel(index, UiSliderRatioFromX(layout.slider, fx) * kWeakPeakLevelMax); return true; } - if (boostMode && WeakBoostSliderHit(layout.sliderIterations, m_scale, fx, fy)) + if (boostMode && WeakPeakSliderHit(layout.sliderIterations, m_scale, fx, fy)) { - m_dragMode = PlotDragMode::BOOST_ITERATIONS; - m_boostDragLane = index; + m_dragMode = PlotDragMode::PEAK_ITERATIONS; + m_peakDragLane = index; const double ratio = UiSliderRatioFromX(layout.sliderIterations, fx); - SetLaneWeakBoostIterations(index, - kWeakBoostIterationsMin + + SetLaneWeakPeakIterations(index, + kWeakPeakIterationsMin + static_cast(std::lround( - ratio * (kWeakBoostIterationsMax - kWeakBoostIterationsMin)))); + ratio * (kWeakPeakIterationsMax - kWeakPeakIterationsMin)))); return true; } @@ -4602,7 +4604,7 @@ namespace jdi const auto findItem = [&]() -> const Lane::MeanFilterItem* { return meanMode ? tabLane.FindMeanFilterItem(ref) - : (boostMode ? tabLane.FindWeakBoostItem(ref) + : (boostMode ? tabLane.FindWeakPeakItem(ref) : tabLane.FindEdgeCenterItem(ref)); }; // 第一排右侧的复选框 → 切换该信号的预览勾选状态。 @@ -4615,7 +4617,7 @@ namespace jdi } else if (boostMode) { - ToggleLaneWeakBoostSignal(index, ref); + ToggleLaneWeakPeakSignal(index, ref); } else { @@ -4623,14 +4625,14 @@ namespace jdi } m_meanCurveCache.clear(); m_edgeCenterCountCache.clear(); - m_weakBoostCurveCache.clear(); + m_weakPeakCurveCache.clear(); // 勾选变化只影响该网格的内容:交给 `m_sceneDerived` 快照驱动局部重画, // 这里**不**整体作废场景缓存(否则每次勾选都要重绘全部网格)。 m_dragMode = PlotDragMode::NONE; return true; } // 第二排的两个输入框 → 就地编辑该信号的均值点数 X / 迭代次数 N - //(弱波增强面板没有参数输入框,等级由滑块统一控制)。 + //(弱峰增强面板没有参数输入框,等级由滑块统一控制)。 if (layout.rowPoints[rowIndex].Width > 0.0f && UiPointInRect(layout.rowPoints[rowIndex], fx, fy)) { @@ -4657,14 +4659,14 @@ 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 iterations = std::max(kWeakBoostIterationsMin, - std::min(kWeakBoostIterationsMax, lane.weakBoostIterations)); - m_pendingWeakBoostItems.clear(); + std::lround(std::max(0.0, std::min(kWeakPeakLevelMax, lane.weakPeakLevel)) * 100.0)); + const int iterations = std::max(kWeakPeakIterationsMin, + std::min(kWeakPeakIterationsMax, lane.weakPeakIterations)); + m_pendingWeakPeakItems.clear(); for (const SignalRef& ref : lane.signals) { const ResolvedSignal resolved = ResolveSignal(ref); @@ -4672,23 +4674,23 @@ namespace jdi { continue; } - if (lane.FindWeakBoostItem(ref) == nullptr) + if (lane.FindWeakPeakItem(ref) == nullptr) { continue; } - WeakBoostBuild build; + WeakPeakBuild build; build.fileIndex = ref.fileIndex; build.signalIndex = ref.signalIndex; build.levelPercent = levelPercent; build.iterations = iterations; - m_pendingWeakBoostItems.push_back(build); + m_pendingWeakPeakItems.push_back(build); } - if (!m_pendingWeakBoostItems.empty()) + if (!m_pendingWeakPeakItems.empty()) { - SetHint(L"正在构建弱波增强信号(等级 " + std::to_wstring(levelPercent) + + SetHint(L"正在构建弱峰增强信号(等级 " + std::to_wstring(levelPercent) + L"% / " + std::to_wstring(iterations) + L" 次迭代,共 " + - std::to_wstring(m_pendingWeakBoostItems.size()) + L" 条)…"); - m_pendingWeakBoostLane = index; + std::to_wstring(m_pendingWeakPeakItems.size()) + L" 条)…"); + m_pendingWeakPeakLane = index; } m_dragMode = PlotDragMode::NONE; return true; @@ -4831,7 +4833,7 @@ namespace jdi m_edgeDragLane = -1; m_edgePanelDragLane = -1; m_edgePanelDragKind = EdgePanelKind::COUNT; - m_boostDragLane = -1; + m_peakDragLane = -1; SetHint(std::wstring()); m_dragLane = LaneAtY(y); m_dragMode = PlotDragMode::PAN; @@ -4877,8 +4879,8 @@ namespace jdi case EdgePanelKind::MEAN: SetLaneMeanFilter(lane, false); break; - case EdgePanelKind::BOOST: - SetLaneWeakBoost(lane, false); + case EdgePanelKind::PEAK: + SetLaneWeakPeak(lane, false); break; case EdgePanelKind::COUNT: default: @@ -4950,7 +4952,7 @@ namespace jdi m_edgeDragLane = -1; m_edgePanelDragLane = -1; m_edgePanelDragKind = EdgePanelKind::COUNT; - m_boostDragLane = -1; + m_peakDragLane = -1; // 松开左键:粘滞提示结束计时,开始"停留后淡出"。 ReleaseHint(); return changed; @@ -5491,11 +5493,10 @@ namespace jdi } // 本帧整帧都在改写渲染状态(`m_area` / `m_plotRect` / 视界 / 图层…), - // 因此必须与「后台图层绘制线程」互斥;可重入,内部再调图层渲染不会自锁。 - // 后台只在**内容变化后补画**时才会长时间持有它,平移/缩放等交互期间它基本空闲, - // 因此前台不会被拖慢。 + // 因此必须与「后台派生曲线计算」线程隔离:本锁同时保护渲染状态与派生缓存。 + // ⚠️ 图层渲染**只在本线程内进行**(见头文件说明):后台线程一旦改写 + // `m_area` / `m_plotRect` / `Tab().ViewMin()`,就会覆盖用户正在进行的平移。 std::lock_guard renderLock(m_renderMutex); - m_lastPaintTick.store(GetTickCount64(), std::memory_order_relaxed); BuildLaneVisuals(); @@ -5508,7 +5509,7 @@ namespace jdi } // 后台算好了新曲线: // * **静止**时:整幅作废一次,让新曲线(与新的静态场景缓存)立刻出现。 - // * **拖拽中**(拖弱波增强/均值滤波参数滑块):**不作废整幅** —— 拖拽时参数每帧都在变, + // * **拖拽中**(拖弱峰增强/均值滤波参数滑块):**不作废整幅** —— 拖拽时参数每帧都在变, // 本来就每帧走"样式补丁"路径,补丁会直接取到最新曲线;若此时还整幅作废, // 每帧会退化成整幅重绘(实测单帧 150ms+,与"拖动要跟手"直接冲突)。 // 改为标记"已降级",等拖拽结束后再补一帧全质量。 @@ -5540,8 +5541,8 @@ namespace jdi const bool sceneChangingDrag = (m_dragMode == PlotDragMode::PAN) || (m_dragMode == PlotDragMode::SCROLL_LANES) || (m_dragMode == PlotDragMode::LANE_REORDER) || (m_dragMode == PlotDragMode::MOVE_SIGNAL) || - (m_dragMode == PlotDragMode::BOOST_LEVEL) || - (m_dragMode == PlotDragMode::BOOST_ITERATIONS); + (m_dragMode == PlotDragMode::PEAK_LEVEL) || + (m_dragMode == PlotDragMode::PEAK_ITERATIONS); m_fastFrame = sceneChangingDrag; // 贴图之后、静态场景之外的全部内容(随鼠标变化或位于最上层)。 @@ -5606,7 +5607,7 @@ namespace jdi // 外观样式(颜色/粗细/透明度/模式)变化 → 交给下方"样式补丁"分支局部重画。 // ⚠️ 位置必须在"静态场景贴图"**之后**:视界与样式都没变时(典型:鼠标悬停移动) // 应由静态场景贴图命中,绝不能落到这里 —— 否则每两帧就会触发一次"结算整幅重绘", - // 悬停反而变卡。而它又必须在"样式补丁"**之前**:派生曲线参数变化(拖弱波增强 / + // 悬停反而变卡。而它又必须在"样式补丁"**之前**:派生曲线参数变化(拖弱峰增强 / // 均值滤波滑块)同样只需叠加重画派生曲线,走本分支比"逐网格补丁"更快且质量更高。 // ⚠️ **不要求"正在拖拽"**:非拖拽的视界变化(键盘方向键 / 工具栏)同样走此快路径, // 避免该放大区间"整幅重绘约 124ms"的卡顿。 @@ -5632,12 +5633,17 @@ namespace jdi } else { - // 只要图层“就绪且跨度一致”,就直接用现成像素做坐标偏移(平移/滚动)。 - // 内容即使稍旧也无所谓:由后台线程补画后与旧缓冲交换,前台下一帧即拿到新像素。 - // 只有“跨度变了/图层还没建”时才落到下方"整幅重绘"以保证正确性。 - bool ready = LayersReadyForView() ? RollWideBuffer() : false; - // 无论是否用上现成像素,都把"内容与前台不一致"的网格交后台补画。 - ScheduleStaleLayerRenders(); + // 图层缺失 / 视界跨度变化(缩放 / 布局变化)时先**同步重建**全部网格图层; + // 随后 `RollWideBuffer()` 逐网格滚动跟随视界(每个网格用自己的 `wideMin` 算位移, + // 内容指纹与当前状态不一致的网格会被单独整块重画)。 + // 只有"图层仍然建不起来"时才落到下方整幅重绘以保证正确性。 + if (!LayersReadyForView()) + { + RebuildWideBuffer(); + } + bool ready = RollWideBuffer(); + // 图层渲染一律在本线程内完成(不要把这一步搬回后台线程), + // 否则后台对渲染状态的"保存→改写→恢复"会覆盖用户正在进行的平移。 if (ready) { m_wideLastKey = wideKey; @@ -6004,7 +6010,7 @@ namespace jdi mixFloat(static_cast(visual.yMax)); const std::vector& signals = Tab().Lanes()[laneIndex].signals; - // 「边沿计数(中心轴)」「均值滤波」「弱波增强」的开关与每条被勾选信号的参数 + // 「边沿计数(中心轴)」「均值滤波」「弱峰增强」的开关与每条被勾选信号的参数 // 会改变网格内的曲线绘制。它们**只影响网格内容**(不影响网格结构/几何), // 因此只在 includeStyles 时计入 —— 于是拖参数滑块可以走“局部重画”快路径, // 而不必整幅重绘(`FindStyleOnlyLanes` 用 `m_sceneDerived` 定位受影响的网格)。 @@ -6030,11 +6036,11 @@ namespace jdi mix(static_cast(item.points)); mix(static_cast(item.iterations)); } - mix(Tab().Lanes()[laneIndex].weakBoost ? 1ull : 0ull); - mixFloat(static_cast(Tab().Lanes()[laneIndex].weakBoostLevel)); - mix(static_cast(Tab().Lanes()[laneIndex].weakBoostIterations)); - mix(static_cast(Tab().Lanes()[laneIndex].weakBoostItems.size())); - for (const Lane::WeakBoostItem& item : Tab().Lanes()[laneIndex].weakBoostItems) + mix(Tab().Lanes()[laneIndex].weakPeak ? 1ull : 0ull); + mixFloat(static_cast(Tab().Lanes()[laneIndex].weakPeakLevel)); + mix(static_cast(Tab().Lanes()[laneIndex].weakPeakIterations)); + mix(static_cast(Tab().Lanes()[laneIndex].weakPeakItems.size())); + for (const Lane::WeakPeakItem& item : Tab().Lanes()[laneIndex].weakPeakItems) { mix(static_cast(item.ref.fileIndex)); mix(static_cast(item.ref.signalIndex)); @@ -6080,7 +6086,7 @@ namespace jdi std::uint64_t PlotView::SceneDecorHash() const { // 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数** - //(拖弱波增强等级/迭代次数滑块时图例与刻度不变,装饰层缓存不应失效)。 + //(拖弱峰增强等级/迭代次数滑块时图例与刻度不变,装饰层缓存不应失效)。 return SceneContentHash(true, false); } @@ -6142,7 +6148,7 @@ namespace jdi return false; } - // 叠加预览曲线(均值滤波 / 中心轴 / 弱波增强)的参数变化同样只需局部重画。 + // 叠加预览曲线(均值滤波 / 中心轴 / 弱峰增强)的参数变化同样只需局部重画。 std::vector currentDerived; CollectSceneDerived(currentDerived); const bool derivedComparable = (currentDerived.size() == m_sceneDerived.size()); @@ -6169,12 +6175,12 @@ namespace jdi const SceneDerivedEntry& beforeDerived = m_sceneDerived[laneIndex]; differs = (nowDerived.edgeCenter != beforeDerived.edgeCenter) || (nowDerived.meanFilter != beforeDerived.meanFilter) || - (nowDerived.weakBoost != beforeDerived.weakBoost) || - (nowDerived.weakBoostLevelPercent != beforeDerived.weakBoostLevelPercent) || - (nowDerived.weakBoostIterations != beforeDerived.weakBoostIterations) || + (nowDerived.weakPeak != beforeDerived.weakPeak) || + (nowDerived.weakPeakLevelPercent != beforeDerived.weakPeakLevelPercent) || + (nowDerived.weakPeakIterations != beforeDerived.weakPeakIterations) || (nowDerived.edgeCenterItems != beforeDerived.edgeCenterItems) || (nowDerived.meanFilterItems != beforeDerived.meanFilterItems) || - (nowDerived.weakBoostItems != beforeDerived.weakBoostItems); + (nowDerived.weakPeakItems != beforeDerived.weakPeakItems); } if (differs) { @@ -6194,10 +6200,10 @@ namespace jdi SceneDerivedEntry entry; entry.edgeCenter = lane.edgeCenter ? 1 : 0; entry.meanFilter = lane.meanFilter ? 1 : 0; - entry.weakBoost = lane.weakBoost ? 1 : 0; - entry.weakBoostLevelPercent = static_cast( - std::lround(std::max(0.0, std::min(1.0, lane.weakBoostLevel)) * 100.0)); - entry.weakBoostIterations = lane.weakBoostIterations; + entry.weakPeak = lane.weakPeak ? 1 : 0; + entry.weakPeakLevelPercent = static_cast( + std::lround(std::max(0.0, std::min(kWeakPeakLevelMax, lane.weakPeakLevel)) * 100.0)); + entry.weakPeakIterations = lane.weakPeakIterations; const auto flatten = [](const auto& items, std::vector& target) { target.clear(); @@ -6212,7 +6218,7 @@ namespace jdi }; flatten(lane.edgeCenterItems, entry.edgeCenterItems); flatten(lane.meanFilterItems, entry.meanFilterItems); - flatten(lane.weakBoostItems, entry.weakBoostItems); + flatten(lane.weakPeakItems, entry.weakPeakItems); out[laneIndex] = std::move(entry); } } @@ -6891,13 +6897,7 @@ namespace jdi { ReleaseLaneLayer(layer); } - for (LaneLayer& layer : m_layerBack) - { - ReleaseLaneLayer(layer); - } m_layerFront.clear(); - m_layerBack.clear(); - m_layerAnchorValid = false; } void PlotView::InvalidateWideBuffer() @@ -6907,11 +6907,6 @@ namespace jdi { layer.valid = false; } - for (LaneLayer& layer : m_layerBack) - { - layer.valid = false; - } - m_layerAnchorValid = false; } std::uint64_t PlotView::LaneLayerKey(int laneIndex) const @@ -6928,6 +6923,20 @@ namespace jdi return key; } + double PlotView::LayerDefaultWideMin() const + { + // 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。 + const double span = Tab().ViewMax() - Tab().ViewMin(); + const int width = static_cast(std::lround(m_plotRect.Width)); + if (!(span > 0.0) || width <= 0) + { + return Tab().ViewMin(); + } + const double scale = static_cast(width) / span; + const int margin = std::max(1, width / 2); + return Tab().ViewMin() - static_cast(margin) / scale; + } + bool PlotView::LayersReadyForView() const { std::lock_guard lock(m_layerMutex); @@ -6959,7 +6968,8 @@ namespace jdi return true; } - bool PlotView::RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW) + bool PlotView::RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW, + double wideMin) { if (laneIndex < 0 || laneIndex >= static_cast(m_lanes.size())) { @@ -6985,8 +6995,7 @@ namespace jdi return false; } - const double scale = static_cast(width) / span; // 像素 / 时间 - const double wideMin = Tab().ViewMin() - static_cast(margin) / scale; // 位图 x=0 对应的时间 + const double scale = static_cast(width) / span; // 像素 / 时间 if (stripW <= 0 || stripX < 0 || stripX + stripW > bitmapWidth) { @@ -7033,7 +7042,7 @@ namespace jdi UiFillRect(g, m_area, m_theme->plotBg); UiFillRect(g, m_area, m_theme->cardBg); - // 竖向网格刻度沿用**前台视界跨度**(而非 3× 的宽窗口跨度),否则竖网格线会只有 1/3。 + // 竖向网格刻度沿用**前台视界跨度**(而非宽窗口的跨度),否则竖网格线会只有一半。 const double fgSpan = savedViewMax - savedViewMin; const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget); std::vector timeTicks; @@ -7045,9 +7054,8 @@ namespace jdi // 恢复布局 / 视图 / 网格。 // ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局 - // 重算 `m_laneContentHeight`。该值参与内容指纹;若残留上面"宽窗口"布局算出的值, - // 前台算出的图层指纹就永远与后台不一致 → 图层被判定为"永远过期" → 每帧重复排任务, - // 后台反复重画、前台反复抢 `m_renderMutex`,表现为界面卡死。 + // 重算 `m_laneContentHeight`。该值参与内容指纹(`LaneLayerKey`),若残留上面"宽窗口" + // 布局算出的值,算出的图层指纹就与实际渲染不符 → 每帧都判定图层"过期"重画,白白耗性能。 m_area = savedArea; m_plotRect = savedPlot; Tab().ViewMin() = savedViewMin; @@ -7071,6 +7079,7 @@ namespace jdi layer.span = span; layer.key = LaneLayerKey(laneIndex); layer.valid = true; + m_layerRenderCount.fetch_add(1, std::memory_order_relaxed); return true; } @@ -7088,18 +7097,17 @@ namespace jdi std::lock_guard renderLock(m_renderMutex); std::lock_guard layerLock(m_layerMutex); m_layerFront.resize(m_lanes.size()); - m_layerBack.resize(m_lanes.size()); + // 全部网格统一用"当前视界居中"的映射(与 `BlitWideBuffer` 的逐网格 `wideMin` 相容)。 + const double wideMin = LayerDefaultWideMin(); bool ok = true; for (std::size_t index = 0; index < m_layerFront.size(); ++index) { - if (!RenderLaneLayerRegion(static_cast(index), m_layerFront[index], 0, 0)) + if (!RenderLaneLayerRegion(static_cast(index), m_layerFront[index], 0, 0, wideMin)) { ok = false; } } - m_layerAnchorMin = Tab().ViewMin(); - m_layerAnchorValid = true; return ok; } @@ -7118,7 +7126,7 @@ namespace jdi std::lock_guard renderLock(m_renderMutex); std::lock_guard layerLock(m_layerMutex); - if (!m_layerAnchorValid || m_layerFront.size() != m_lanes.size() || m_layerFront.empty()) + if (m_layerFront.size() != m_lanes.size() || m_layerFront.empty()) { return false; } @@ -7127,27 +7135,42 @@ namespace jdi if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth || std::fabs(layer.span - span) > 1.0e-12) { - return false; + return false; // 图层缺失 / 跨度不符:由调用方先 RebuildWideBuffer() } } - // 视界左端相对锚点的位移(像素)。内容需整体平移 round(d) 像素,只补画新暴露的细条。 - const double d = (Tab().ViewMin() - m_layerAnchorMin) * scale; - const int shift = static_cast(std::lround(d)); - if (shift == 0) + // ⚠️ **逐网格独立滚动**:每个图层有自己的时间映射(`layer.wideMin`), + // 因此位移必须按各自的锚点分别计算。曾用**一个全局锚点**给所有图层算同一个位移, + // 结果各网格被平移了不同的真实距离 → 网格之间时间错位、贴回的像素互相矛盾, + // 表现为严重的绘制错误与波形抖动。 + const double defaultWideMin = LayerDefaultWideMin(); + bool ok = true; + for (std::size_t index = 0; index < m_layerFront.size(); ++index) { - return true; - } - // 位移过大(跳视界)时滚动已经没有意义,交给调用方整幅重建。 - if (std::abs(shift) >= bitmapWidth - 4) - { - return false; - } + LaneLayer& layer = m_layerFront[index]; + const int laneIndex = static_cast(index); - const int stripWidth = std::min(std::abs(shift), bitmapWidth); - const int stripX = (shift > 0) ? (bitmapWidth - stripWidth) : 0; - for (LaneLayer& layer : m_layerFront) - { + // 视界左端当前落在该图层的哪个 x 上;期望它等于 `margin`(左右余量对称)。 + const double desired = (Tab().ViewMin() - layer.wideMin) * scale; + const int shift = static_cast(std::llround(desired - static_cast(margin))); + + // 内容(外观 / 网格几何 / 幅值 / 可见性…)与当前状态不一致,或锚点离当前视界 + // 太远 —— 都不能再用旧像素做坐标偏移,直接整块重画该网格。 + const bool contentStale = (layer.key != LaneLayerKey(laneIndex)); + if (contentStale || std::abs(shift) >= bitmapWidth - 4) + { + if (!RenderLaneLayerRegion(laneIndex, layer, 0, 0, defaultWideMin)) + { + ok = false; + } + continue; + } + if (shift == 0) + { + continue; + } + + // 整数像素自贴图平移:内容整体移动 `shift` 像素。 if (shift > 0) { // 内容整体左移 shift:new[x] = old[x + shift],右侧暴露 shift 列。 @@ -7158,18 +7181,19 @@ namespace jdi const int amount = -shift; BitBlt(layer.dc, amount, 0, bitmapWidth - amount, layer.height, layer.dc, 0, 0, SRCCOPY); } + layer.wideMin += static_cast(shift) / scale; + + // 只补画新暴露的细条(每帧代价恒为 O(细条),无周期性大尖峰)。 + // ⚠️ 细条必须用**该图层更新后的映射**渲染(传 `layer.wideMin`);若改用"当前视界" + // 的映射,细条与已平移的旧像素之间会有 ≤0.5px 的错位接缝 → 拖动时看起来在抖动。 + const int stripWidth = std::min(std::abs(shift), bitmapWidth); + const int stripX = (shift > 0) ? (bitmapWidth - stripWidth) : 0; + if (!RenderLaneLayerRegion(laneIndex, layer, stripX, stripWidth, layer.wideMin)) + { + ok = false; + } } - m_layerAnchorMin += static_cast(shift) / scale; - for (LaneLayer& layer : m_layerFront) - { - layer.wideMin = m_layerAnchorMin - static_cast(margin) / scale; - } - // 只补画新暴露的细条(每帧代价恒为 O(细条),无周期性大尖峰)。 - for (std::size_t index = 0; index < m_layerFront.size(); ++index) - { - RenderLaneLayerRegion(static_cast(index), m_layerFront[index], stripX, stripWidth); - } - return true; + return ok; } bool PlotView::BlitWideBuffer(gd::Graphics& g) const @@ -7235,183 +7259,6 @@ namespace jdi return any; } - void PlotView::ScheduleStaleLayerRenders() - { - const float plotWidth = m_plotRect.Width; - const double span = Tab().ViewMax() - Tab().ViewMin(); - if (!(plotWidth > 8.0f) || !(span > 0.0) || m_lanes.empty()) - { - return; - } - const int width = static_cast(std::lround(plotWidth)); - const int margin = std::max(1, width / 2); - const int bitmapWidth = width + 2 * margin; - - std::vector pending; - { - std::lock_guard lock(m_layerMutex); - if (m_layerFront.size() != m_lanes.size()) - { - m_layerFront.resize(m_lanes.size()); - m_layerBack.resize(m_lanes.size()); - } - for (std::size_t index = 0; index < m_lanes.size(); ++index) - { - const LaneLayer& layer = m_layerFront[index]; - const bool stale = (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth || - std::fabs(layer.span - span) > 1.0e-12 || - layer.key != LaneLayerKey(static_cast(index))); - if (stale) - { - pending.push_back(static_cast(index)); - } - } - } - if (pending.empty()) - { - m_layerRescheduleStreak = 0; - return; - } - // 停滞保护:见成员注释。超过阈值就同步重建一次强行收敛。 - if (++m_layerRescheduleStreak > 90) - { - m_layerRescheduleStreak = 0; - RebuildWideBuffer(); - return; - } - { - std::lock_guard lock(m_layerJobMutex); - for (int index : pending) - { - bool exists = false; - for (const LayerJob& job : m_layerJobs) - { - if (job.laneIndex == index) - { - exists = true; - break; - } - } - if (!exists) - { - LayerJob job; - job.laneIndex = index; - m_layerJobs.push_back(job); - } - } - } - EnsureLayerWorker(); - m_layerJobCv.notify_all(); - } - - void PlotView::EnsureLayerWorker() - { - std::lock_guard lock(m_layerJobMutex); - if (m_layerWorker.joinable()) - { - return; - } - m_layerWorkerStop = false; - m_layerWorker = std::thread([this]() { LayerWorkerLoop(); }); - } - - void PlotView::StopLayerWorker() - { - { - std::lock_guard lock(m_layerJobMutex); - if (!m_layerWorker.joinable()) - { - return; - } - m_layerWorkerStop = true; - m_layerJobs.clear(); - } - m_layerJobCv.notify_all(); - m_layerWorker.join(); - } - - void PlotView::LayerWorkerLoop() - { - // 后台绘制不能与 UI 抢 CPU:降到低优先级(前台仍然"想跑多快跑多快")。 - SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL); - for (;;) - { - LayerJob job; - { - std::unique_lock lock(m_layerJobMutex); - m_layerJobCv.wait(lock, [this]() { return m_layerWorkerStop || !m_layerJobs.empty(); }); - if (m_layerWorkerStop) - { - return; - } - job = m_layerJobs.front(); - m_layerJobs.erase(m_layerJobs.begin()); - } - if (job.laneIndex < 0) - { - continue; - } - // ⚠️ **前台优先**:前台刚刚还在绘制(典型:用户正在拖拽/平移)时,后台主动让路, - // 把任务放回队首稍后再试。否则后台会不断抢占 `m_renderMutex`,前台每帧都要等 - // 一个图层画完,反而比“单线程同步绘制”更卡。交互停下来后后台再从容补画。 - if (GetTickCount64() - m_lastPaintTick.load(std::memory_order_relaxed) < 30) - { - std::unique_lock lock(m_layerJobMutex); - m_layerJobCv.wait_for(lock, std::chrono::milliseconds(8)); - m_layerJobs.insert(m_layerJobs.begin(), job); - continue; - } - // 同一网格若有更新的任务在排队,本次直接跳过(以最新状态为准)。 - { - bool superseded = false; - std::lock_guard lock(m_layerJobMutex); - for (const LayerJob& other : m_layerJobs) - { - if (other.laneIndex == job.laneIndex) - { - superseded = true; - break; - } - } - if (superseded) - { - continue; - } - } - { - // ⚠️ 加锁顺序必须与前台一致:**先 `m_renderMutex` 再 `m_layerMutex`**。 - // 反序会与 `Paint`(渲染中会去锁图层元数据)构成经典的 AB-BA 死锁。 - std::lock_guard renderLock(m_renderMutex); - std::lock_guard layerLock(m_layerMutex); - if (job.laneIndex >= static_cast(m_layerBack.size()) || - m_layerBack.size() != m_lanes.size()) - { - continue; - } - if (!RenderLaneLayerRegion(job.laneIndex, m_layerBack[static_cast(job.laneIndex)], - 0, 0)) - { - continue; - } - // 后台缓冲画好 → 立即与前台缓冲整体交换(前台始终有立即可用的像素)。 - std::swap(m_layerFront[static_cast(job.laneIndex)], - m_layerBack[static_cast(job.laneIndex)]); - m_layerAnchorMin = Tab().ViewMin(); - m_layerAnchorValid = true; - m_layerRenderCount.fetch_add(1, std::memory_order_relaxed); - } - // 任务之间让出 CPU / 锁:避免后台连续抢占 `m_renderMutex`,把前台绘制挤出去 - //(后台只是预取图层,晚一两毫秒无关紧要;前台才需要“想跑多快跑多快”)。 - Sleep(1); - // 通知宿主重绘:绘制线程下一帧就会取到刚交换过来的图层。 - const HWND host = m_host; - if (host != nullptr) - { - PostMessageW(host, kMessagePlotDerivedReady, 0, 0); - } - } - } - void PlotView::PaintRubberBand(gd::Graphics& g) { if (!m_dragMoved || m_rubberRect.Width <= 0.0f || m_rubberRect.Height <= 0.0f) @@ -7667,7 +7514,7 @@ namespace jdi // 只有拖拽中的信号光晕可以进入静态场景缓存(悬停光晕见 PaintHoverHighlight)。 const SignalRef haloRef = SceneHaloRef(); - // 派生预览曲线(中心轴 / 均值滤波 / 弱波增强)在下方"第二遍"统一叠加绘制, + // 派生预览曲线(中心轴 / 均值滤波 / 弱峰增强)在下方"第二遍"统一叠加绘制, // 见 `PaintLaneDerivedOverlays`(抽出以便宽缓冲区前台路径复用)。 for (const SignalRef& ref : laneSignals) @@ -7943,15 +7790,15 @@ namespace jdi const Lane& tabLane = Tab().Lanes()[static_cast(laneIndex)]; const bool centerOn = tabLane.edgeCenter; const bool meanFilterOn = tabLane.meanFilter; - const bool weakBoostOn = tabLane.weakBoost; - if (!centerOn && !meanFilterOn && !weakBoostOn) + const bool weakPeakOn = tabLane.weakPeak; + if (!centerOn && !meanFilterOn && !weakPeakOn) { return; } const int levelPercent = static_cast( - std::lround(std::max(0.0, std::min(1.0, tabLane.weakBoostLevel)) * 100.0)); + std::lround(std::max(0.0, std::min(kWeakPeakLevelMax, tabLane.weakPeakLevel)) * 100.0)); const int boostIterations = - std::max(kWeakBoostIterationsMin, std::min(kWeakBoostIterationsMax, tabLane.weakBoostIterations)); + std::max(kWeakPeakIterationsMin, std::min(kWeakPeakIterationsMax, tabLane.weakPeakIterations)); const int saveState = g.Save(); g.SetClip(lane.rect, gd::CombineModeIntersect); for (const SignalRef& ref : tabLane.signals) @@ -7977,12 +7824,12 @@ namespace jdi PaintMeanFilterCurve(g, resolved, lane, item->points, item->iterations); } } - if (weakBoostOn) + if (weakPeakOn) { - const Lane::WeakBoostItem* item = tabLane.FindWeakBoostItem(ref); + const Lane::WeakPeakItem* item = tabLane.FindWeakPeakItem(ref); if (item != nullptr) { - PaintWeakBoostCurve(g, resolved, lane, levelPercent, boostIterations); + PaintWeakPeakCurve(g, resolved, lane, levelPercent, boostIterations); } } } @@ -8618,7 +8465,7 @@ namespace jdi DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline); UiText(g, text, m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER); - // 「边沿计数(中心轴)」/「均值滤波」/「弱波增强」开启时,纵向光标还要额外标注 + // 「边沿计数(中心轴)」/「均值滤波」/「弱峰增强」开启时,纵向光标还要额外标注 // **派生曲线**在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。 // 三条曲线只是曲线来源不同,标注规则完全一致,故共用同一段绘制逻辑。 // ⚠️ 曲线只取「可见区间 ∪ 两个光标时刻 ± 保护带」的窗口(与绘制共用缓存), @@ -8766,21 +8613,21 @@ namespace jdi meanItem->iterations, 0, L"均值滤波"); } } - if (m_tab->Lanes()[laneIndex].weakBoost) + if (m_tab->Lanes()[laneIndex].weakPeak) { - // 弱波增强曲线:同样是只标注被勾选的信号,尾缀为「弱波增强」。 - const Lane::WeakBoostItem* boostItem = - m_tab->Lanes()[laneIndex].FindWeakBoostItem(ref); + // 弱峰增强曲线:同样是只标注被勾选的信号,尾缀为「弱峰增强」。 + const Lane::WeakPeakItem* boostItem = + m_tab->Lanes()[laneIndex].FindWeakPeakItem(ref); if (boostItem != nullptr) { const int levelPercent = static_cast(std::lround( - std::max(0.0, std::min(1.0, m_tab->Lanes()[laneIndex].weakBoostLevel)) * + std::max(0.0, std::min(kWeakPeakLevelMax, m_tab->Lanes()[laneIndex].weakPeakLevel)) * 100.0)); const int iterations = std::max( - kWeakBoostIterationsMin, - std::min(kWeakBoostIterationsMax, m_tab->Lanes()[laneIndex].weakBoostIterations)); - paintDerivedCapsule(DerivedCurveKind::WEAK_BOOST, 0, iterations, levelPercent, - L"弱波增强"); + kWeakPeakIterationsMin, + std::min(kWeakPeakIterationsMax, m_tab->Lanes()[laneIndex].weakPeakIterations)); + paintDerivedCapsule(DerivedCurveKind::WEAK_PEAK, 0, iterations, levelPercent, + L"弱峰增强"); } } } @@ -8863,15 +8710,15 @@ namespace jdi const Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; const bool centerAxis = (kind == EdgePanelKind::CENTER); const bool meanFilter = (kind == EdgePanelKind::MEAN); - const bool weakBoost = (kind == EdgePanelKind::BOOST); - // 「均值滤波」与「弱波增强」共用同一套「逐条目 + 底部按钮」排布。 - const bool itemized = (centerAxis || meanFilter || weakBoost); - const bool hasButton = (meanFilter || weakBoost); + const bool weakPeak = (kind == EdgePanelKind::PEAK); + // 「均值滤波」与「弱峰增强」共用同一套「逐条目 + 底部按钮」排布。 + const bool itemized = (centerAxis || meanFilter || weakPeak); + const bool hasButton = (meanFilter || weakPeak); const bool enabled = meanFilter ? lane.meanFilter - : (weakBoost ? lane.weakBoost + : (weakPeak ? lane.weakPeak : (centerAxis ? lane.edgeCenter : lane.edgeCount)); const bool open = meanFilter ? lane.meanFilterPanelOpen - : (weakBoost ? lane.weakBoostPanelOpen + : (weakPeak ? lane.weakPeakPanelOpen : (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen)); if (!enabled || !open) { @@ -8897,13 +8744,13 @@ namespace jdi const float titleHeight = kEdgePanelTitleHeight * m_scale; const float rowHeight = kEdgePanelRowHeight * m_scale; const float pad = kEdgePanelPad * m_scale; - // 「中心轴边沿计数」「均值滤波」「弱波增强」共用同一套「逐条目」排布, + // 「中心轴边沿计数」「均值滤波」「弱峰增强」共用同一套「逐条目」排布, // 因此不再有独立的底部编辑行;只有「边沿计数」保留两行文本式排版。 const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f; const float rowsPerSignal = meanFilter ? 1.0f : 2.0f; - // 「弱波增强」在条目与底部按钮之间额外有两行滑块(等级 / 迭代次数 N),其余逐条目面板没有。 - const float sliderRowHeight = weakBoost ? (kWeakBoostSliderRowHeight * m_scale) : 0.0f; - const float sliderRowsHeight = weakBoost ? (sliderRowHeight * 2.0f) : 0.0f; + // 「弱峰增强」在条目与底部按钮之间额外有两行滑块(等级 / 迭代次数 N),其余逐条目面板没有。 + const float sliderRowHeight = weakPeak ? (kWeakPeakSliderRowHeight * m_scale) : 0.0f; + const float sliderRowsHeight = weakPeak ? (sliderRowHeight * 2.0f) : 0.0f; // 逐条目面板:每个条目 = 第一排(信号名 + 复选框) // + 第二排(均值点数 X / 迭代次数 N)+(中心轴)第三排(统计结果); @@ -8923,7 +8770,7 @@ namespace jdi } itemsHeight += meanRowHeight; const bool checked = meanFilter ? lane.MeanFilterEnabled(ref) - : (weakBoost ? lane.WeakBoostEnabled(ref) + : (weakPeak ? lane.WeakPeakEnabled(ref) : lane.EdgeCenterEnabled(ref)); if (checked && (centerAxis || meanFilter)) { @@ -8946,10 +8793,10 @@ namespace jdi // 首次使用(位置为负)时自动放到绘图区右侧,并各网格依次错开,避免完全重叠。 const double storedX = meanFilter ? lane.meanFilterPanelX - : (weakBoost ? lane.weakBoostPanelX + : (weakPeak ? lane.weakPeakPanelX : (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX)); const double storedY = meanFilter ? lane.meanFilterPanelY - : (weakBoost ? lane.weakBoostPanelY + : (weakPeak ? lane.weakPeakPanelY : (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY)); float left = (storedX >= 0.0) ? static_cast(storedX) @@ -8982,15 +8829,15 @@ namespace jdi std::max(0.0f, width - pad * 2.0f), buttonRowHeight); } - // 弱波增强的两个滑块行(等级 / 迭代次数 N,位于信号条目与底部按钮之间)。 - if (weakBoost) + // 弱峰增强的两个滑块行(等级 / 迭代次数 N,位于信号条目与底部按钮之间)。 + if (weakPeak) { const float labelWidth = - std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel), - UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostIterationsLabel)); + std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakLevelLabel), + UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakIterationsLabel)); const float valueWidth = std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100%"), UiMeasureTextWidth(m_fonts->monoSmall, L"10")); - const float trackHeight = kWeakBoostTrackHeight * m_scale; + const float trackHeight = kWeakPeakTrackHeight * m_scale; const float firstRowTop = top + titleHeight + pad + itemsHeight; const float trackLeft = left + pad + labelWidth + 6.0f * m_scale; const float trackRight = left + width - pad - valueWidth - 6.0f * m_scale; @@ -9023,7 +8870,7 @@ namespace jdi left + width - pad - checkSize, rowY + (meanRowHeight - checkSize) * 0.5f, checkSize, checkSize)); const bool checked = meanFilter ? lane.MeanFilterEnabled(ref) - : (weakBoost ? lane.WeakBoostEnabled(ref) + : (weakPeak ? lane.WeakPeakEnabled(ref) : lane.EdgeCenterEnabled(ref)); if (checked && (centerAxis || meanFilter)) { @@ -9312,7 +9159,7 @@ namespace jdi baseAlpha); // 顶点预算与波形一致(每像素列两个顶点)。 - // 交互期间进一步降到 1/2:派生曲线往往有 10 条(5 网格 × 均值滤波 + 弱波增强), + // 交互期间进一步降到 1/2:派生曲线往往有 10 条(5 网格 × 均值滤波 + 弱峰增强), // 顶点数就是绘制耗时;降低密度能让拖动参数滑块保持流畅, // 松手后下一帧立刻恢复全密度(用户要求的"低分辨率 → 高分辨率")。 std::size_t budget = @@ -9429,7 +9276,7 @@ namespace jdi PaintDerivedCurve(g, resolved, lane, *curve, spanBegin); } - void PlotView::PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, + void PlotView::PaintWeakPeakCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, int levelPercent, int iterations) const { if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr) @@ -9443,14 +9290,14 @@ namespace jdi } // 预览曲线一律按**整条信号**计算(与「构建为新信号」逐位一致),详见 `PaintMeanFilterCurve`。 std::size_t spanBegin = 0; - const std::vector* curve = CurveSpanFor(DerivedCurveKind::WEAK_BOOST, *resolved.data, 0, + const std::vector* curve = CurveSpanFor(DerivedCurveKind::WEAK_PEAK, *resolved.data, 0, iterations, levelPercent, 0, count, spanBegin, true); if (curve == nullptr) { return; } - // 派生曲线一律使用同一段绘制逻辑与配色规则(弱波增强曲线同样如此)。 + // 派生曲线一律使用同一段绘制逻辑与配色规则(弱峰增强曲线同样如此)。 PaintDerivedCurve(g, resolved, lane, *curve, spanBegin); } @@ -9585,7 +9432,7 @@ namespace jdi mixRef(m_hoverLegend); mixRef(m_dragSignal); mixRef(SelectedRef()); - mix(static_cast(m_boostDragLane + 2)); + mix(static_cast(m_peakDragLane + 2)); mix(static_cast(m_edgePanelDragLane + 2)); mix(static_cast(m_edgePanelDragKind)); @@ -9628,12 +9475,12 @@ namespace jdi mixFloat(static_cast(lane.edgeCenterPanelY)); mixFloat(static_cast(lane.meanFilterPanelX)); mixFloat(static_cast(lane.meanFilterPanelY)); - mixFloat(static_cast(lane.weakBoostPanelX)); - mixFloat(static_cast(lane.weakBoostPanelY)); + mixFloat(static_cast(lane.weakPeakPanelX)); + mixFloat(static_cast(lane.weakPeakPanelY)); mix(lane.edgePanelOpen ? 1ull : 0ull); mix(lane.edgeCenterPanelOpen ? 1ull : 0ull); mix(lane.meanFilterPanelOpen ? 1ull : 0ull); - mix(lane.weakBoostPanelOpen ? 1ull : 0ull); + mix(lane.weakPeakPanelOpen ? 1ull : 0ull); mix(lane.edgeValueValid ? 1ull : 0ull); mixDouble(lane.edgeValue); } @@ -9695,7 +9542,7 @@ namespace jdi // 勾选状态与参数直接决定行文本与行高。 mix(lane.MeanFilterEnabled(ref) ? 1ull : 0ull); mix(lane.EdgeCenterEnabled(ref) ? 1ull : 0ull); - mix(lane.WeakBoostEnabled(ref) ? 1ull : 0ull); + mix(lane.WeakPeakEnabled(ref) ? 1ull : 0ull); const Lane::MeanFilterItem* meanItem = lane.FindMeanFilterItem(ref); mix(static_cast(meanItem != nullptr ? meanItem->points + 2 : 0)); mix(static_cast(meanItem != nullptr ? meanItem->iterations + 2 : 0)); @@ -9703,13 +9550,13 @@ namespace jdi mix(static_cast(centerItem != nullptr ? centerItem->points + 2 : 0)); mix(static_cast(centerItem != nullptr ? centerItem->iterations + 2 : 0)); } - // 滑块数值(等级百分比 + 迭代次数)只显示在「弱波增强」面板上: + // 滑块数值(等级百分比 + 迭代次数)只显示在「弱峰增强」面板上: // ⚠️ 必须限定 kind,否则拖等级滑块会把同一网格的均值滤波 / 中心轴面板 // 也判为脏,再经"重叠级联"扩散到其他网格,等于所有面板都重画(实测 40ms/帧)。 - if (kind == EdgePanelKind::BOOST) + if (kind == EdgePanelKind::PEAK) { - mixFloat(static_cast(lane.weakBoostLevel)); - mix(static_cast(lane.weakBoostIterations + 2)); + mixFloat(static_cast(lane.weakPeakLevel)); + mix(static_cast(lane.weakPeakIterations + 2)); } // 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。 @@ -9829,12 +9676,12 @@ namespace jdi } // ---- 统计浮动面板(最后绘制,覆盖在波形之上)---- - // 「边沿计数」「边沿计数(中心轴)」「均值滤波」「弱波增强」共用同一套样式, + // 「边沿计数」「边沿计数(中心轴)」「均值滤波」「弱峰增强」共用同一套样式, // 仅标题、内容行与底部按钮不同。 // // 面板是**静态**内容:只有被拖动 / 悬停 / 参数变化的那一个真正重画, // 其余直接贴上一帧缓存好的像素(15 个面板全画一遍实测约 33ms, - // 而贴图 <1ms —— 拖动弱波增强滑块时的主要卡顿就来自这一层)。 + // 而贴图 <1ms —— 拖动弱峰增强滑块时的主要卡顿就来自这一层)。 std::vector nextSlots; std::vector dirtyRects; const std::uint64_t panelGlobalKey = PanelGlobalKey(); @@ -9848,10 +9695,10 @@ namespace jdi const EdgePanelKind kind = kEdgePanelKinds[pass]; const bool centerAxis = (kind == EdgePanelKind::CENTER); const bool meanFilter = (kind == EdgePanelKind::MEAN); - const bool weakBoost = (kind == EdgePanelKind::BOOST); - // 「均值滤波」与「弱波增强」共用「逐条目 + 底部按钮」排布。 - const bool itemized = (centerAxis || meanFilter || weakBoost); - const bool hasButton = (meanFilter || weakBoost); + const bool weakPeak = (kind == EdgePanelKind::PEAK); + // 「均值滤波」与「弱峰增强」共用「逐条目 + 底部按钮」排布。 + const bool itemized = (centerAxis || meanFilter || weakPeak); + const bool hasButton = (meanFilter || weakPeak); for (std::size_t index = 0; index < count; ++index) { const Lane& tabLane = tabLanes[index]; @@ -9943,7 +9790,7 @@ namespace jdi m_theme->text, TextAlign::CENTER); const std::wstring title = meanFilter ? std::wstring(L"均值滤波") - : (weakBoost ? std::wstring(kWeakBoostTitle) + : (weakPeak ? std::wstring(kWeakPeakTitle) : ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix())); UiText(g, title, m_fonts->uiSmall, @@ -9973,9 +9820,9 @@ namespace jdi closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor, 1.6f * m_scale); - // 「中心轴边沿计数」/「均值滤波」/「弱波增强」面板都采用「逐条目」排布: + // 「中心轴边沿计数」/「均值滤波」/「弱峰增强」面板都采用「逐条目」排布: // 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目, - // 弱波增强没有参数输入框),中心轴再多第三排 = 对应的统计结果。 + // 弱峰增强没有参数输入框),中心轴再多第三排 = 对应的统计结果。 if (itemized) { for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex) @@ -9989,7 +9836,7 @@ namespace jdi const std::uint32_t signalColor = StyleColor(*resolved.style); const gd::RectF& checkRect = layout.rowChecks[rowIndex]; const bool checked = meanFilter ? tabLane.MeanFilterEnabled(ref) - : (weakBoost ? tabLane.WeakBoostEnabled(ref) + : (weakPeak ? tabLane.WeakPeakEnabled(ref) : tabLane.EdgeCenterEnabled(ref)); // 名称:第一排左侧,避开右侧复选框。 @@ -10007,7 +9854,7 @@ 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; @@ -10074,17 +9921,17 @@ namespace jdi } } - // 「弱波增强」的两个滑块(等级 0–100% / 迭代次数 N 1–10): + // 「弱峰增强」的两个滑块(等级 0–200% / 迭代次数 N 1–10): // 左侧说明文字 + 轨道 + 右侧数值。 - if (weakBoost && layout.slider.Width > 0.0f) + if (weakPeak && layout.slider.Width > 0.0f) { const float rowHeight = - std::max(kWeakBoostSliderRowHeight * m_scale, layout.slider.Height); + std::max(kWeakPeakSliderRowHeight * m_scale, layout.slider.Height); // 两行滑块的说明文字必须**左对齐到同一列**(用户明确要求): // 列宽取两条标签中较宽者,与 `EdgePanelLayoutFor` 里定位轨道的方式一致。 const float labelColumnWidth = - std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel), - UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostIterationsLabel)); + std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakLevelLabel), + UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakIterationsLabel)); const auto paintSliderRow = [&](const gd::RectF& track, const wchar_t* label, double ratio, const std::wstring& valueText, bool active) @@ -10110,22 +9957,22 @@ namespace jdi m_theme->textStrong, TextAlign::LEFT); }; - const double level = std::max(0.0, std::min(1.0, tabLane.weakBoostLevel)); - const bool levelActive = (m_dragMode == PlotDragMode::BOOST_LEVEL) && - (m_boostDragLane == static_cast(index)); - paintSliderRow(layout.slider, kWeakBoostLevelLabel, level, + const double level = std::max(0.0, std::min(kWeakPeakLevelMax, tabLane.weakPeakLevel)); + const bool levelActive = (m_dragMode == PlotDragMode::PEAK_LEVEL) && + (m_peakDragLane == static_cast(index)); + paintSliderRow(layout.slider, kWeakPeakLevelLabel, level / kWeakPeakLevelMax, std::to_wstring(static_cast(std::lround(level * 100.0))) + L"%", levelActive); - 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.sliderIterations, kWeakBoostIterationsLabel, orderRatio, + const int iterations = std::max(kWeakPeakIterationsMin, + std::min(kWeakPeakIterationsMax, tabLane.weakPeakIterations)); + const double orderRatio = static_cast(iterations - kWeakPeakIterationsMin) / + static_cast(kWeakPeakIterationsMax - + kWeakPeakIterationsMin); + const bool orderActive = (m_dragMode == PlotDragMode::PEAK_ITERATIONS) && + (m_peakDragLane == static_cast(index)); + paintSliderRow(layout.sliderIterations, kWeakPeakIterationsLabel, orderRatio, std::to_wstring(iterations), orderActive); } } diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index f159709..3e5c468 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -509,6 +509,7 @@ Documents/ 项目文档(含本文件) | 2026-10-12 | DeepSeek-V4.1-Flash | 落地用户提出的「各网格独立双缓冲区 + 后台线程绘制」架构:把全局宽缓冲区重构为**每网格信号图层**(宽×3,只含基底不含装饰)、`m_layerFront/m_layerBack` **双缓冲** + `m_layerWorker` **后台线程**(前台只做 BitBlt 坐标偏移,不再绘制波形);前台"图层就绪即用现成像素、内容稍旧也照用,交后台补画后交换";加不收敛保护、前台优先让路、固定加锁顺序(render→layer)、渲染状态完整还原(含 `m_laneContentHeight`/`LaneScroll`)。新增 `BackgroundLayerRenderCount()` 公开计数用于验证后台绘制确实发生(一轮诊断 18~45 次)。实测深放大区间(2.045ms 一大格)各交互 0.7~8.7ms/帧,像素差 ≤0.017(除亚像素平移),静态/结算帧像素精确。 | | 2026-10-12 | DeepSeek-V4.1-Flash | 修复用户报告的「缩放后平移出现拖影/残留/错误元素」与「2ms 一大格区间仍然卡顿」。根因:①网格纵向滚动量未参与任何内容指纹→滚动时把旧位置像素贴回且每两帧触发一次整幅重绘(≈8fps);②`FindStyleOnlyLanes` 未校验视界→视界变化后仍走局部补丁;③宽基底用含装饰的整幅画面初始化→装饰随平移重影;④装饰缓存的矩形列表与坐标原点两帧不同源→装饰消失/黑块/错位。修复后交互帧 9~19ms → **4.5~6ms**,纵向拖动残留 2 万+ 通道 → 72 通道(亚像素量化),静态/结算帧像素精确。新增 `perf.exe ... 6` 诊断模式(逐帧计时 + 逐像素对比 + 差异包围盒)。未实现:每网格双缓冲后台渲染(需先把 `PaintLane` 改造成无状态渲染器,已在文档 9.15 说明)。 | | 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **跨设备记忆合流**:把起源设备(本机)的详细工程记忆**并入本文件**(同一记忆文件,不再另建 `agent_mem_origin.md` 等旁支文件),新增 §11(跨设备协作须知)与 §12(起源设备详细记忆原文)。远端 `gitea/main` 已快进合并到本机(`7608e85` → `f97c4cf`,**16** 个提交)。本机 MinGW-w64 GCC 15.2.0 (UCRT) 下 `make release` 通过。 | +| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复「缩放后平移出现严重绘制错误/波形抖动/前台被拖慢」**:根因是后台图层线程会改写共享渲染状态(保存→改写→恢复`m_area`/`m_plotRect`/视界/网格矩形),与 UI 线程无锁的平移/命中测试相冲 → 视界被弹回、网格错位;叠加"全局锚点 + 逐网格内容"导致各网格时间错位。修法:**移除后台图层线程**(改为 UI 线程同步维护)、**逐网格独立锚点与滚动**(补细条用该图层自己的 `wideMin`)、跨度变化时同步 `RebuildWideBuffer()`。同时把「弱波增强」改名「**弱峰增强**」并改为 **0~200% 分段插值**(0% 完全不动 / 100% 到较温和一侧 / 200% 完全拉平;新 1.5 = 旧 0.5)。离屏验证:居中图层 vs 整幅重绘 **5 网格逐像素 0 差异**、平移帧对齐 ≤1px、24 次竞态检测 0 覆盖;测试套件 **380/380 通过**。 | --- @@ -536,9 +537,11 @@ Documents/ 项目文档(含本文件) - 场景贴图必须在**图层之前**(否则鼠标悬停每两帧触发一次"结算整幅重绘",反而更卡); - 图层必须在**样式补丁之前**(拖弱波增强 / 均值滤波参数只改派生曲线,走图层更快且质量更高)。 2. 每网格图层**只含基底**(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例 —— 它们锚定网格左缘、不随时间视界移动)。 -3. ⚠️ 图层位图宽度 = `width + 2*max(1, width/2)` ≈ **2 × 视界宽**(左右各半屏)。头注释与 §10 变更日志写的"**宽 ×3**"是**错的**(代码就是 ×2)。 -4. `RenderLaneLayerRegion` 结束后**必须** `BuildLaneVisuals()`(重算 `m_laneContentHeight`)**再**还原 `LaneScroll` 与各网格 `yMin/yMax`;漏还原任何一项 → 前后台指纹永不相等 → 图层永远"过期" → 每帧重复排任务 → **界面卡死**。 -5. 加锁顺序**恒定** `m_renderMutex`(recursive)→ `m_layerMutex`;反序 = AB-BA 死锁。 +3. 图层位图宽度 = `width + 2*max(1, width/2)` ≈ **2 × 视界宽**(左右各半屏,可平移 ±半屏)。 +4. `RenderLaneLayerRegion` 结束后**必须** `BuildLaneVisuals()`(重算 `m_laneContentHeight`)**再**还原 `LaneScroll` 与各网格 `yMin/yMax`;漏还原任何一项 → 算出的图层指纹与实际渲染不符 → 每帧重复重画(白耗性能;旧版有后台线程时会死循环卡死)。 +5. ⚠️ **图层渲染一律在 UI 线程内同步完成**(`Paint` 全程持有 `m_renderMutex`)。**绝不要把图层渲染搬到后台线程**:它会临时改写 `m_area`/`m_plotRect`/`Tab().ViewMin()/ViewMax()`/各网格矩形,而这些状态被 UI 线程的消息处理**无锁读/写**(平移读视界、命中测试读网格矩形),后台的"保存→改写→恢复"会**覆盖用户正在进行的平移**(视界弹回)并让命中测试错位 —— 这就是 2026-10-12 移除后台图层线程的原因(详见 §12 本轮)。 +6. 每个图层记录**自己的**时间映射 `LaneLayer::wideMin`;滚动与贴图必须**逐网格**按各自 `wideMin` 计算,**绝不共用一个全局锚点**(否则各网格被平移不同的真实距离 → 网格间时间错位)。补细条时必须传 `layer.wideMin`(不能用"当前视界"的映射,否则有 ≤0.5px 接缝与抖动)。 +7. 加锁顺序**恒定** `m_renderMutex`(recursive)→ `m_layerMutex`(图层不再有跨线程访问,但顺序约定保留)。 6. **GDI 绘制不受 GDI+ `SetClip` 约束**:`UiText`(`DrawTextW` / `ETO_CLIPPED`)与 `UiFillRect` 的 GDI 快路径(不透明且面积 ≥ 20000px²)都会画出裁剪区;宽矩形 / 文本必须**自己夹取**,GDI 批绘必须显式传 clip。 7. `SceneContentHash` 必须覆盖 `Tab().LaneScroll()`(网格纵向滚动量)与 `SelectedRef()`;`FindStyleOnlyLanes()` 必须**逐项比较 `SceneCacheKey`**(`SceneGeometryHash()` 刻意不含视界,只比它不足以判定"视图没变")。 8. 随鼠标变化的绘制(悬停高亮 / 光晕 / 图例高亮 / 光标读数 / 选区 / 提示 / 面板悬停)**必须同时**加在"缓存贴图快路径"与"整幅重绘路径"两侧。 @@ -2331,3 +2334,66 @@ Documents/ 项目文档(含本文件) 全部与改动前一致;`make clean; make all` 零警告。 - ⚠️ `act.ps1` 的动作参数是 **客户区坐标**(窗口钉在 (0,0)),而截图是整窗 → 换算 `客户区 = 截图 − (9,38)`。 标签栏在客户区 y≈28(截图 y≈66 已是工具栏「导入文件」按钮,误点会弹出文件对话框)。 + + +--- + +## 本轮(2026-10-12,起源设备):图层架构缺陷修复 + 「弱峰增强」0~200% + +### A. 渲染:移除后台图层线程(根因),改为逐网格同步滚动 +- **用户现象**(`VERY_BIG_DATA` 全信号、选区放大后用中/左键拖动移动视界):**严重绘制错误**、**波形抖动**、**前台绘制被拖慢**。 +- **根因(三个叠在一起)**: + 1. **后台线程改写共享渲染状态**:`RenderLaneLayerRegion` 为了用宽窗口映射渲染,会临时改写 + `m_area` / `m_plotRect` / `Tab().ViewMin()/ViewMax()` / 各网格矩形,完事再“恢复”。而 UI 线程的 + `OnMouseMove`(平移写视界、命中测试读网格矩形)**无锁读/写**这些状态 → 后台的“恢复”会把 + 用户刚做的平移**弹回**、命中测试也会读到错位的网格。 + 2. **全局锚点 + 逐网格内容**:`RollWideBuffer` 用**一个** `m_layerAnchorMin` 给所有图层算 + **同一个**位移,而各图层可能是在**不同视界**下渲染的(后台逐网格渲染、或拖拽中渲染) + → 各网格被平移了**不同的真实距离** → 网格之间时间错位、贴回的像素互相矛盾。 + 3. **细条用错映射**:滚动补细条时用“当前视界”的映射渲染,而旧像素是另一个映射 + (`layer.wideMin` 还被覆盖)→ ≤0.5px 接缝 + 拖动时的**抖动**。 +- **修法**: + - **删除后台图层线程**(`m_layerWorker`/任务队列/`EnsureLayerWorker`/`StopLayerWorker`/ + `LayerWorkerLoop`/`ScheduleStaleLayerRenders`/`m_layerBack`/`m_layerAnchorMin|Valid`/`m_lastPaintTick`/ + `m_layerRescheduleStreak`)。**图层渲染一律在 UI 线程内同步完成**(不要再搬回后台)。 + - **逐网格独立滚动**:每个图层以自己的 `wideMin` 为锚点算位移,细条用 `layer.wideMin` 渲染 + (与已平移的旧像素同一映射)→ 消除接缝与抖动。 + - `RenderLaneLayerRegion` 新增 `wideMin` 形参;新增 `LayerDefaultWideMin()`(当前视界居中)。 + - `RollWideBuffer()` 逐网格:内容指纹对不上(`layer.key != LaneLayerKey(i)`,例如工具其他路径改了样式而图层还是旧的) + 或锚点离视界太远 → 该网格**整块重画**;否则整数像素自贴图 + 补细条。 + - `Paint` 宽底度分支:`if (!LayersReadyForView()) RebuildWideBuffer();` → 再 `RollWideBuffer()` + (跳度变化/首帧时**同步**重建全部图层,取代原来的后台补画)。 + - `BackgroundLayerRenderCount()` 保留为公开诊断计数(现在统计同步图层区域渲染次数)。 +- **离屏验证**(`.preflight/layercheck`,窗口 SW_HIDE、不动鼠标/键盘/屏幕): + - **居中图层 vs 整幅重绘:5 个网格逐像素 0 差异**(证明“底座管线”本身精确,与 + f97c4cf 之前的结论一致)。 + - 平移帧:逐网格最佳对齐位移 **≤1px**、**视界不被覆盖**、无网格被跳过; + 残差为设计内 ≤0.5px 整数像素贴图残留(视界停止时的“结算帧”恢复像素精确)。 + - 竞态/视界覆盖检测:24 次“跳度抖动 + 视界变化 + 渲染”→ **0 次覆盖**。 + - 注:旧代码在该自动化场景下只能概率性复现缺陷(后台线程会被前台长期锁住而饥饿), + 因此主要证据是“结构不可能出错(没有线程再改写渲染状态)+ 居中图层逐像素精确”。 +- ⚠️ 见 §11.2 的硬约束(图层不得后台化、不得共用全局锚点)。 + +### B. 「弱波增强」→「弱峰增强」+ 等级 0~200% +- **改名**:界面文案与标识符全部改为 `WeakPeak`/`weakPeak`(`ComputeWeakPeakBoost`、 + `SetLaneWeakPeak*`、`EdgePanelKind::PEAK`、`PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS`、 + `PLOT_CMD_LANE_WEAK_PEAK`、`PeakExtremum` 等);导出信号名 `"<原信号> (弱峰增强 M% N次迭代)"`。 +- **等级语义(0–2,面板 0%–200%)分段插值**(`ApplyWeakPeakStage`,`s` = 等级): + - 波峰:`s<=1` → `target = cur + s*(lower-cur)`;`s>1` → `target = lower + (s-1)*(upper-lower)` + (`lower/upper` = 左右同类极值中较小/较大者)。 + - 波谷:`s<=1` → `target = cur + s*(upper-cur)`;`s>1` → `target = upper - (s-1)*(upper-lower)`。 + - 语义:**0% 完全不动**(可做极小幅度调整)、**100% 到较温和的一侧**、**200% 完全拉平**; + 两段在 100% 处连续。**新 1.5 == 旧 0.5**、**新 2.0 == 旧 1.0**(旧行为整体平移到后 100%)。 + - `Lane::weakPeakLevel` 默认 **1.5**(= 旧默认 0.5 的效果);`levelPercent`(构建请求/缓存键) + 范围 0–200、默认 150;常量 `kWeakPeakLevelMax = 2.0`、`kWeakPeakPercentMax = 200`。 + - 滑块:轨道比例 = `level / 2`,数值显示 `level * 100`%。 +- **测试同步**(`.preflight/uitest`):`[X]` 改为 0/50/100/150/200% 五个目标值;阶数扫描与 + “完全恢复”块改用 150%/200% 以保持旧期望;`[Y]` 用 150%;派生信号名校验改用 `\u5f31\u5cf0...`。 +- **⚠️ 本轮顺带修正两处“过期的测试期望”**(它们与实现早已不一致,只是没人跑过 uitest, + 因为 f97c4cf 那轮只用 `perf.exe` 做验证): + - `[J]`:样式补丁**故意**渲染“快速帧”(关 AA)→ 该帧与全质量整幅重绘**不可能逐像素相同**。 + 现改为:断言补丁帧**标记为降级**,渲染到缓存重新有效(≤4 帧)后**用修复帧**与整幅重绘比对(0 差异)。 + - `[AF]`:面板像素层 96% 不透明 → 底下波形的 ≤0.5px 残留会透出(实测最大 72 级、仅 6 个像素) + → 容差由 32 提到 **96**(真正“面板内容过期”的差异是百级)。 + - `[X]` 预览曲线像素计数的颜色容差 6 → **40**(真实信号上曲线多与波形重合,纯色像素本就少)。 +- **验证**:`make release` 零警告;离屏测试套件 **380/380 通过**(exit=0)。 diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 933434e..e95abd2 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -374,13 +374,13 @@ namespace jdi using EdgeCenterItem = MeanFilterItem; /** - * @brief 「弱波增强」中某个信号自己的条目。 + * @brief 「弱峰增强」中某个信号自己的条目。 * - * 目前只用到 `ref`(增强等级属于整个网格,统一保存在 `weakBoostLevel` 中); + * 目前只用到 `ref`(增强等级属于整个网格,统一保存在 `weakPeakLevel` 中); * 复用 `MeanFilterItem` 结构是为了与均值滤波/中心轴共享 * 「条目查找 + 信号插入/删除时的引用重映射」等既有实现。 */ - using WeakBoostItem = MeanFilterItem; + using WeakPeakItem = MeanFilterItem; std::wstring name; /**< 网格名称。 */ std::vector signals; /**< 该网格包含的信号(按加入顺序)。 */ @@ -501,53 +501,58 @@ namespace jdi double meanFilterPanelY = -1.0; /** - * @brief 「实验性功能 → 弱波增强」开关。 + * @brief 「实验性功能 → 弱峰增强」开关。 * - * 开启后为本网格内**被勾选**的信号额外绘制一条「弱波增强」曲线: + * 开启后为本网格内**被勾选**的信号额外绘制一条「弱峰增强」曲线: * 识别纹波的波峰/波谷,把“比左右同类极值更弱”的波峰上拉、波谷下拉, - * 并按「弱波增强等级」在左右两个同类极值之间取目标值,使其恢复与周边一致的振幅。 + * 并按「弱峰增强等级」在左右两个同类极值之间取目标值,使其恢复与周边一致的振幅。 * 这条曲线只是叠加显示,不会作为信号加入左侧列表。 */ - bool weakBoost = false; - - /** 弱波增强等级(0–1),0% = 目标取左右极值中较小/较大的那一个,100% = 取另一个。 */ - double weakBoostLevel = 0.5; + bool weakPeak = false; /** - * @brief 弱波增强的迭代次数 N(1–10)。 + * @brief 弱峰增强等级(0–2,面板上显示为 0%–200%)。 + * + * 0 = 目标就是当前值(完全不动);1 = 拉到较温和的一侧(波峰取较低的左/右波峰, + * 波谷取较高的左/右波谷);2 = 拉到另一侧(完全拉平)。默认 1.5,等价于旧版的 50%。 + */ + double weakPeakLevel = 1.5; + + /** + * @brief 弱峰增强的迭代次数 N(1–10)。 * * 与「均值滤波」的迭代次数 N 一样表示“把同一处理重复应用 N 次”(每一阶以上一阶为输入): - * 一阶时改定量就已严格等于「等级」对应的目标值;多阶会把相邻弱波互相抬高 - * 彼此的基准,从而把较宽的弱波区逐步恢复。 + * 一阶时改定量就已严格等于「等级」对应的目标值;多阶会把相邻弱峰互相抬高 + * 彼此的基准,从而把较宽的弱峰区逐步恢复。 */ - int weakBoostIterations = 1; + int weakPeakIterations = 1; - /** 参与弱波增强预览的信号条目(只包含被勾选的信号)。 */ - std::vector weakBoostItems; + /** 参与弱峰增强预览的信号条目(只包含被勾选的信号)。 */ + std::vector weakPeakItems; - /** 取某个信号的弱波增强条目(未勾选时返回空指针)。 */ - const WeakBoostItem* FindWeakBoostItem(const SignalRef& ref) const; + /** 取某个信号的弱峰增强条目(未勾选时返回空指针)。 */ + const WeakPeakItem* FindWeakPeakItem(const SignalRef& ref) const; - /** 取得或创建某信号的弱波增强条目(用于勾选)。 */ - WeakBoostItem& EnsureWeakBoostItem(const SignalRef& ref); + /** 取得或创建某信号的弱峰增强条目(用于勾选)。 */ + WeakPeakItem& EnsureWeakPeakItem(const SignalRef& ref); - /** 取消某信号的弱波增强勾选。 */ - void RemoveWeakBoostItem(const SignalRef& ref); + /** 取消某信号的弱峰增强勾选。 */ + void RemoveWeakPeakItem(const SignalRef& ref); - /** 取某信号的弱波增强勾选状态。 */ - bool WeakBoostEnabled(const SignalRef& ref) const + /** 取某信号的弱峰增强勾选状态。 */ + bool WeakPeakEnabled(const SignalRef& ref) const { - return FindWeakBoostItem(ref) != nullptr; + return FindWeakPeakItem(ref) != nullptr; } - /** 弱波增强浮动面板是否展开。 */ - bool weakBoostPanelOpen = true; + /** 弱峰增强浮动面板是否展开。 */ + bool weakPeakPanelOpen = true; - /** 弱波增强面板左上角位置(客户区像素;<0 表示自动定位)。 */ - double weakBoostPanelX = -1.0; + /** 弱峰增强面板左上角位置(客户区像素;<0 表示自动定位)。 */ + double weakPeakPanelX = -1.0; - /** 弱波增强面板左上角纵坐标(<0 表示自动定位)。 */ - double weakBoostPanelY = -1.0; + /** 弱峰增强面板左上角纵坐标(<0 表示自动定位)。 */ + double weakPeakPanelY = -1.0; }; /** @@ -592,32 +597,36 @@ namespace jdi std::vector& out); /** - * @brief 「弱波增强」:把比周边更弱的纹波波峰/波谷恢复(自适应弱波增强)。 + * @brief 「弱峰增强」:把比周边更弱的纹波波峰/波谷恢复(自适应弱峰增强)。 * * 本算法**不做任何前置滤波**:需要平滑时用户可以先用「均值滤波」构建一条 - * 滤波后的信号,再对它做弱波增强。极值点直接在输入采样上识别。 + * 滤波后的信号,再对它做弱峰增强。极值点直接在输入采样上识别。 * * 单阶处理(全 O(n)): * 1. **识别趋势与极值**:扫描单调段,方向发生翻转处的上一个采样点即为 * 波峰(先升后降)或波谷(先降后升);波峰与波谷交替出现。 * 2. **判定“弱”**:对第 k 个波峰,取同类的左右相邻波峰(k−2、k+2)。 * 若本波峰低于两者的均值,说明它是被削弱的那个;波谷反之(高于两者均值)。 - * 3. **目标值**(`level` = 弱波增强等级,0–1): - * * 波峰:左右两个波峰中较小者为 0%、较大者为 100%, - * 目标 = min + level × (max − min); - * * 波谷:左右两个波谷中较大者为 0%、较小者为 100%, - * 目标 = max − level × (max − min)。 + * 3. **目标值**(`level` = 弱峰增强等级,0–2,对应面板上的 0%–200%): + * * 波峰:左右两个波峰中较小者记为 `lower`、较大者记为 `upper`; + * 0 ≤ level ≤ 1 → 目标 = 当前值 + level × (`lower` − 当前值); + * 1 < level ≤ 2 → 目标 = `lower` + (level − 1) × (`upper` − `lower`)。 + * * 波谷:左右两个波谷中较大者记为 `upper`、较小者记为 `lower`; + * 0 ≤ level ≤ 1 → 目标 = 当前值 + level × (`upper` − 当前值); + * 1 < level ≤ 2 → 目标 = `upper` − (level − 1) × (`upper` − `lower`)。 + * 即 **0% 完全不动**(可做极小幅度调整)、**100% 拉到较温和的一侧**、 + * **200% 完全拉平**;两段在 100% 处连续。 * 4. **平滑偏移**:极值点本身取其目标值,两侧的数据点按相邻两个极值点的偏移量 - * 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱波附近被推回。 + * 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱峰附近被推回。 * * 正常周期的极值不满足条件、偏移量恒为 0,因此输出与输入逐位相同。 * * @param [IN] values 原始采样值。 - * @param [IN] level 弱波增强等级(0–1,超出范围会被夹取)。 + * @param [IN] level 弱峰增强等级(0–2,对应 0%–200%,超出范围会被夹取)。 * @param [IN] iterations 迭代次数 N(重复应用次数,至少为 1)。 * @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。 */ - void ComputeWeakWaveBoost(const std::vector& values, double level, int iterations, + void ComputeWeakPeakBoost(const std::vector& values, double level, int iterations, std::vector& out); /** diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index f95a550..89ef919 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -134,13 +134,13 @@ namespace jdi bool meanFilterPanelOpen = true; double meanFilterPanelX = -1.0; double meanFilterPanelY = -1.0; - bool weakBoost = false; - std::vector weakBoostItems; - double weakBoostLevel = 0.5; - int weakBoostIterations = 1; - bool weakBoostPanelOpen = true; - double weakBoostPanelX = -1.0; - double weakBoostPanelY = -1.0; + bool weakPeak = false; + std::vector weakPeakItems; + double weakPeakLevel = 1.5; + int weakPeakIterations = 1; + bool weakPeakPanelOpen = true; + double weakPeakPanelX = -1.0; + double weakPeakPanelY = -1.0; }; // 先记下每个信号原来所在网格的网格自身状态: @@ -166,13 +166,13 @@ namespace jdi carry.meanFilterPanelOpen = lane.meanFilterPanelOpen; carry.meanFilterPanelX = lane.meanFilterPanelX; carry.meanFilterPanelY = lane.meanFilterPanelY; - carry.weakBoost = lane.weakBoost; - carry.weakBoostItems = lane.weakBoostItems; - carry.weakBoostLevel = lane.weakBoostLevel; - carry.weakBoostIterations = lane.weakBoostIterations; - carry.weakBoostPanelOpen = lane.weakBoostPanelOpen; - carry.weakBoostPanelX = lane.weakBoostPanelX; - carry.weakBoostPanelY = lane.weakBoostPanelY; + carry.weakPeak = lane.weakPeak; + carry.weakPeakItems = lane.weakPeakItems; + carry.weakPeakLevel = lane.weakPeakLevel; + carry.weakPeakIterations = lane.weakPeakIterations; + carry.weakPeakPanelOpen = lane.weakPeakPanelOpen; + carry.weakPeakPanelX = lane.weakPeakPanelX; + carry.weakPeakPanelY = lane.weakPeakPanelY; for (const SignalRef& ref : lane.signals) { previousCarry.emplace_back(ref, carry); @@ -209,13 +209,13 @@ namespace jdi lane.meanFilterPanelOpen = carry.meanFilterPanelOpen; lane.meanFilterPanelX = carry.meanFilterPanelX; lane.meanFilterPanelY = carry.meanFilterPanelY; - lane.weakBoost = carry.weakBoost; - lane.weakBoostItems = carry.weakBoostItems; - lane.weakBoostLevel = carry.weakBoostLevel; - lane.weakBoostIterations = carry.weakBoostIterations; - lane.weakBoostPanelOpen = carry.weakBoostPanelOpen; - lane.weakBoostPanelX = carry.weakBoostPanelX; - lane.weakBoostPanelY = carry.weakBoostPanelY; + lane.weakPeak = carry.weakPeak; + lane.weakPeakItems = carry.weakPeakItems; + lane.weakPeakLevel = carry.weakPeakLevel; + lane.weakPeakIterations = carry.weakPeakIterations; + lane.weakPeakPanelOpen = carry.weakPeakPanelOpen; + lane.weakPeakPanelX = carry.weakPeakPanelX; + lane.weakPeakPanelY = carry.weakPeakPanelY; }; // 收集全部可见信号引用。 @@ -1052,9 +1052,9 @@ namespace jdi namespace { /** - * @brief 「弱波增强」内部使用的极值点(波峰 / 波谷)。 + * @brief 「弱峰增强」内部使用的极值点(波峰 / 波谷)。 */ - struct BoostExtremum + struct PeakExtremum { std::size_t index = 0; /**< 采样下标。 */ double value = 0.0; /**< 采样值。 */ @@ -1062,18 +1062,18 @@ namespace jdi }; /** - * @brief 「弱波增强」的单阶处理:把偏弱的波峰上拉、偏弱的波谷下拉。 + * @brief 「弱峰增强」的单阶处理:把偏弱的波峰上拉、偏弱的波谷下拉。 * * **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号): * 极值点直接在输入采样上通过单调段的方向翻转识别。 * * @param [IN] input 本阶的输入采样值。 - * @param [IN] strength 弱波增强等级(0–1,已夹取)。 + * @param [IN] strength 弱峰增强等级(0–1,已夹取)。 * @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。 - * @return 确实有采样点被改动返回 true;完全无弱波时返回 false。 + * @return 确实有采样点被改动返回 true;完全无弱峰时返回 false。 */ - bool ApplyWeakBoostStage(const std::vector& input, double strength, - std::vector& output, std::vector& extremes) + bool ApplyWeakPeakStage(const std::vector& input, double strength, + std::vector& output, std::vector& extremes) { const std::size_t count = input.size(); output.assign(count, std::numeric_limits::quiet_NaN()); @@ -1127,7 +1127,7 @@ namespace jdi else if (step != direction) { // 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。 - BoostExtremum extreme; + PeakExtremum extreme; extreme.index = previousIndex; extreme.value = previousValue; extreme.type = direction; @@ -1153,9 +1153,9 @@ namespace jdi shift.assign(extremeCount, 0.0); for (std::size_t k = 2; k + 2 < extremeCount; ++k) { - const BoostExtremum& current = extremes[k]; - const BoostExtremum& left = extremes[k - 2]; - const BoostExtremum& right = extremes[k + 2]; + const PeakExtremum& current = extremes[k]; + const PeakExtremum& left = extremes[k - 2]; + const PeakExtremum& right = extremes[k + 2]; if (left.type != current.type || right.type != current.type) { continue; @@ -1168,24 +1168,32 @@ namespace jdi const double mean = (left.value + right.value) * 0.5; const double lower = std::min(left.value, right.value); const double upper = std::max(left.value, right.value); + // 目标值分段插值(`strength` = 等级 0–2): + // 0 ≤ s ≤ 1:当前值 ↔ 较温和的一侧(波峰取较低的波峰 lower、波谷取较高的波谷 upper); + // 1 < s ≤ 2:该侧 ↔ 另一侧(与原先 0–100% 的语义一致)。 + // 两段在 s = 1 处连续,因此 s = 0 完全不动、s = 2 完全拉平。 double destination = 0.0; if (current.type > 0) { - // 波峰:低于左右两个波峰的均值 → 判定为弱波,上拉到等级对应的目标值。 + // 波峰:低于左右两个波峰的均值 → 判定为弱峰,上拉到等级对应的目标值。 if (!(current.value < mean)) { continue; } - destination = lower + strength * (upper - lower); + destination = (strength <= 1.0) + ? (current.value + strength * (lower - current.value)) + : (lower + (strength - 1.0) * (upper - lower)); } else { - // 波谷:高于左右两个波谷的均值 → 判定为弱波,下拉到等级对应的目标值。 + // 波谷:高于左右两个波谷的均值 → 判定为弱峰,下拉到等级对应的目标值。 if (!(current.value > mean)) { continue; } - destination = upper - strength * (upper - lower); + destination = (strength <= 1.0) + ? (current.value + strength * (upper - current.value)) + : (upper - (strength - 1.0) * (upper - lower)); } shift[k] = destination - current.value; } @@ -1235,7 +1243,7 @@ namespace jdi } } // namespace - void ComputeWeakWaveBoost(const std::vector& values, double level, int iterations, + void ComputeWeakPeakBoost(const std::vector& values, double level, int iterations, std::vector& out) { const std::size_t count = values.size(); @@ -1247,10 +1255,10 @@ namespace jdi // 第 1 阶:从原始采样值出发(非有限值保持 NaN)。 // 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配 - // 并零初始化,单条「弱波增强」的耗时会被明显放大(内存分配 + page fault)。 + // 并零初始化,单条「弱峰增强」的耗时会被明显放大(内存分配 + page fault)。 thread_local std::vector currentBuffer; thread_local std::vector nextBuffer; - thread_local std::vector extremesBuffer; + thread_local std::vector extremesBuffer; std::vector& current = currentBuffer; current.assign(count, std::numeric_limits::quiet_NaN()); for (std::size_t index = 0; index < count; ++index) @@ -1264,13 +1272,14 @@ namespace jdi // 「迭代次数 N」与「均值滤波」的迭代次数语义一致:把同一处理**重复应用 N 次**, // 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「等级」对应的目标值; - // 多次迭代时相邻的弱波会互相抬高彼此的基准,从而把较宽的弱波区逐步恢复。 + // 多次迭代时相邻的弱峰会互相抬高彼此的基准,从而把较宽的弱峰区逐步恢复。 const int stages = std::max(1, std::min(iterations, 1000)); - const double strength = std::max(0.0, std::min(1.0, level)); + // 增强等级 0–2(对应面板上的 0%–200%):0 完全不动,1 拉到较温和的一侧,2 完全拉平。 + const double strength = std::max(0.0, std::min(2.0, level)); std::vector& next = nextBuffer; for (int stage = 0; stage < stages; ++stage) { - if (!ApplyWeakBoostStage(current, strength, next, extremesBuffer)) + if (!ApplyWeakPeakStage(current, strength, next, extremesBuffer)) { // 这一阶没有任何采样点需要调整:后续迭代次数也不会再有变化,提前结束。 break; @@ -1350,9 +1359,9 @@ namespace jdi edgeCenterItems.end()); } - const Lane::WeakBoostItem* Lane::FindWeakBoostItem(const SignalRef& ref) const + const Lane::WeakPeakItem* Lane::FindWeakPeakItem(const SignalRef& ref) const { - for (const WeakBoostItem& item : weakBoostItems) + for (const WeakPeakItem& item : weakPeakItems) { if (item.ref == ref) { @@ -1362,27 +1371,27 @@ namespace jdi return nullptr; } - Lane::WeakBoostItem& Lane::EnsureWeakBoostItem(const SignalRef& ref) + Lane::WeakPeakItem& Lane::EnsureWeakPeakItem(const SignalRef& ref) { - for (WeakBoostItem& item : weakBoostItems) + for (WeakPeakItem& item : weakPeakItems) { if (item.ref == ref) { return item; } } - WeakBoostItem item; + WeakPeakItem item; item.ref = ref; - weakBoostItems.push_back(item); - return weakBoostItems.back(); + weakPeakItems.push_back(item); + return weakPeakItems.back(); } - void Lane::RemoveWeakBoostItem(const SignalRef& ref) + void Lane::RemoveWeakPeakItem(const SignalRef& ref) { - weakBoostItems.erase( - std::remove_if(weakBoostItems.begin(), weakBoostItems.end(), - [&ref](const WeakBoostItem& item) { return item.ref == ref; }), - weakBoostItems.end()); + weakPeakItems.erase( + std::remove_if(weakPeakItems.begin(), weakPeakItems.end(), + [&ref](const WeakPeakItem& item) { return item.ref == ref; }), + weakPeakItems.end()); } int FindNearestSample(const DataFile& file, double time) @@ -1629,7 +1638,7 @@ namespace jdi }; purgeRefs(lane.meanFilterItems); purgeRefs(lane.edgeCenterItems); - purgeRefs(lane.weakBoostItems); + purgeRefs(lane.weakPeakItems); } // 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。 m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(), @@ -1921,20 +1930,20 @@ namespace jdi lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen; lane.edgeCenterPanelX = source.edgeCenterPanelX; lane.edgeCenterPanelY = source.edgeCenterPanelY; - lane.weakBoost = source.weakBoost; - lane.weakBoostLevel = source.weakBoostLevel; - lane.weakBoostIterations = source.weakBoostIterations; - lane.weakBoostPanelOpen = source.weakBoostPanelOpen; - lane.weakBoostPanelX = source.weakBoostPanelX; - lane.weakBoostPanelY = source.weakBoostPanelY; + lane.weakPeak = source.weakPeak; + lane.weakPeakLevel = source.weakPeakLevel; + lane.weakPeakIterations = source.weakPeakIterations; + lane.weakPeakPanelOpen = source.weakPeakPanelOpen; + lane.weakPeakPanelX = source.weakPeakPanelX; + lane.weakPeakPanelY = source.weakPeakPanelY; lane.meanFilter = source.meanFilter; lane.meanFilterPanelOpen = source.meanFilterPanelOpen; lane.meanFilterPanelX = source.meanFilterPanelX; lane.meanFilterPanelY = source.meanFilterPanelY; - // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱波增强条目。 + // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰增强条目。 lane.meanFilterItems.clear(); lane.edgeCenterItems.clear(); - lane.weakBoostItems.clear(); + lane.weakPeakItems.clear(); for (const Lane::MeanFilterItem& item : source.meanFilterItems) { if (item.ref == ref) @@ -1949,11 +1958,11 @@ namespace jdi lane.edgeCenterItems.push_back(item); } } - for (const Lane::WeakBoostItem& item : source.weakBoostItems) + for (const Lane::WeakPeakItem& item : source.weakPeakItems) { if (item.ref == ref) { - lane.weakBoostItems.push_back(item); + lane.weakPeakItems.push_back(item); } } lane.signals.push_back(ref); @@ -2010,7 +2019,7 @@ namespace jdi ++item.ref.signalIndex; } } - for (Lane::WeakBoostItem& item : lane.weakBoostItems) + for (Lane::WeakPeakItem& item : lane.weakPeakItems) { if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos) {