diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index 93fb5fb..f987829 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -239,28 +239,36 @@ namespace jdi bool ApplyPendingRename(); /** - * @brief 应用绘图区提交的「均值滤波 → 构建为新信号」请求。 + * @brief 应用绘图区提交的「实验性功能 → 构建为新信号」请求。 * - * 为指定网格内的每条可见信号各构建一条 N 点均值滤波信号,命名为 - * `<原信号名称> (N点均值)`,插入到原信号所在组的原信号位置**下方**, - * 并把新信号所在的网格移动到原信号网格的下方。 + * 为指定网格内的每条被勾选信号各构建一条派生信号(多阶均值滤波或弱波增强), + * 命名为 `<原信号名称> (N点均值…)` / `<原信号名称> (弱波增强 M%)`, + * 插入到原信号所在组的原信号位置**下方**,并把新信号所在的网格移动到原信号网格的下方。 * * @return 实际构建了新信号返回 true。 */ bool ApplyPendingDerivedBuild(); /** - * @brief 把一条多阶均值滤波派生信号插入数据层并同步各视图。 - * @param [IN] fileIndex 原信号所属文件下标。 - * @param [IN] signalIndex 原信号下标。 - * @param [IN] points 均值点数 X(滑平半径)。 - * @param [IN] order 阶数 N(迭代次数)。 + * @brief 派生信号构建请求(多阶均值滤波 / 弱波增强共用同一套插入流程)。 + */ + struct DerivedSignalRequest + { + int fileIndex = -1; /**< 原信号所属文件下标。 */ + int signalIndex = -1; /**< 原信号下标。 */ + int points = 5; /**< 多阶均值滤波:均值点数 X(滑平半径)。 */ + int order = 1; /**< 多阶均值滤波:阶数 N(迭代次数)。 */ + int levelPercent = 50; /**< 弱波增强:增强等级(0–100)。 */ + bool weakBoost = false; /**< true = 弱波增强,false = 多阶均值滤波。 */ + }; + + /** + * @brief 把一条派生信号(多阶均值滤波 / 弱波增强)插入数据层并同步各视图。 + * @param [IN] request 构建请求。 * @param [IN] makeVisible 是否在当前标签页中勾选新信号。 * @return 构建并插入成功返回 true。 */ - bool BuildDerivedSignal(int fileIndex, int signalIndex, int points, int order, - bool diffAmp, - bool makeVisible); + bool BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible); /** * @brief 弹出文件选择对话框并加载所选文件。 diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index ebc1db4..78fe772 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -1461,71 +1461,67 @@ namespace jdi return true; } - bool Application::BuildDerivedSignal(int fileIndex, int signalIndex, int points, int order, - bool diffAmp, bool makeVisible) + bool Application::BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible) { - const DataFile* file = m_dataSet.FileAt(fileIndex); - if (file == nullptr || signalIndex < 0 || signalIndex >= static_cast(file->signals.size())) + const DataFile* file = m_dataSet.FileAt(request.fileIndex); + if (file == nullptr || request.signalIndex < 0 || + request.signalIndex >= static_cast(file->signals.size())) { return false; } - const SignalData& source = file->signals[static_cast(signalIndex)]; + const SignalData& source = file->signals[static_cast(request.signalIndex)]; const std::wstring baseName = source.Title(); // 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。 + // 多阶均值滤波形如「<原信号名称> (N点均值×M阶)」, + // 弱波增强形如「<原信号名称> (弱波增强 M%)」。 std::vector derived_values; - if (diffAmp) - { - ComputeDiffAmplify(source.values, points, order, derived_values); - } - else - { - ComputeMovingAverageIterated(source.values, points, order, derived_values); - } - - // 2. 组装新信号:均值滤波形如「<原信号名称> (N点均值×M阶)」, - // 差异放大形如「<原信号名称> (X点差异放大×N倍)」。 SignalData derived; - if (diffAmp) + if (request.weakBoost) { - derived.name = baseName + L" (" + std::to_wstring(points) + L"点差异放大×" + - std::to_wstring(order) + L"倍)"; + ComputeWeakWaveBoost(source.values, static_cast(request.levelPercent) / 100.0, + derived_values); + derived.name = baseName + L" (弱波增强 " + std::to_wstring(request.levelPercent) + L"%)"; } else { - derived.name = baseName + L" (" + std::to_wstring(points) + L"点均值"; - if (order > 1) + ComputeMovingAverageIterated(source.values, request.points, request.order, derived_values); + derived.name = baseName + L" (" + std::to_wstring(request.points) + L"点均值"; + if (request.order > 1) { - derived.name += L"×" + std::to_wstring(order) + L"阶"; + derived.name += L"×" + std::to_wstring(request.order) + L"阶"; } derived.name += L")"; } + + // 2. 组装新信号。 derived.rawHeader = derived.name; derived.unit = source.unit; derived.values.assign(derived_values.begin(), derived_values.end()); // 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。 int newIndex = -1; - if (!m_dataSet.InsertDerivedSignal(fileIndex, signalIndex, std::move(derived), newIndex)) + if (!m_dataSet.InsertDerivedSignal(request.fileIndex, request.signalIndex, std::move(derived), + newIndex)) { return false; } for (TabConfig& tab : m_tabs) { - tab.RemapAfterSignalInsert(fileIndex, newIndex); + tab.RemapAfterSignalInsert(request.fileIndex, newIndex); } // 4. 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。 if (makeVisible && !m_tabs.empty()) { SignalRef derivedRef; - derivedRef.fileIndex = fileIndex; + derivedRef.fileIndex = request.fileIndex; derivedRef.signalIndex = newIndex; ActiveTab().SetVisible(derivedRef, true); SignalRef sourceRef; - sourceRef.fileIndex = fileIndex; - sourceRef.signalIndex = signalIndex; + sourceRef.fileIndex = request.fileIndex; + sourceRef.signalIndex = request.signalIndex; ActiveTab().MoveLaneOfSignalBelow(derivedRef, sourceRef); } @@ -1536,18 +1532,9 @@ namespace jdi bool Application::ApplyPendingDerivedBuild() { - // 两类「构建为新信号」请求(均值滤波 / 差异放大)机制完全一致, + // 两类「构建为新信号」请求(均值滤波 / 弱波增强)机制完全一致, // 统一整理成同一份构建清单后按下标降序处理。 - struct BuildRequest - { - int fileIndex = -1; - int signalIndex = -1; - int first = 0; - int second = 0; - bool diffAmp = false; - }; - - std::vector requests; + std::vector requests; { int laneIndex = -1; std::vector items; @@ -1555,29 +1542,28 @@ namespace jdi { for (const PlotView::MeanFilterBuild& item : items) { - BuildRequest request; + DerivedSignalRequest request; request.fileIndex = item.fileIndex; request.signalIndex = item.signalIndex; - request.first = item.points; - request.second = item.order; - request.diffAmp = false; + request.points = item.points; + request.order = item.order; + request.weakBoost = false; requests.push_back(request); } } } { int laneIndex = -1; - std::vector items; - if (m_plot.TakePendingDiffAmpBuild(laneIndex, items)) + std::vector items; + if (m_plot.TakePendingWeakBoostBuild(laneIndex, items)) { - for (const PlotView::DiffAmpBuild& item : items) + for (const PlotView::WeakBoostBuild& item : items) { - BuildRequest request; + DerivedSignalRequest request; request.fileIndex = item.fileIndex; request.signalIndex = item.signalIndex; - request.first = item.points; - request.second = item.gain; - request.diffAmp = true; + request.levelPercent = item.levelPercent; + request.weakBoost = true; requests.push_back(request); } } @@ -1594,7 +1580,7 @@ namespace jdi // **同一文件内必须按下标降序处理**:每次插入只让插入点之后的信号下标 +1, // 降序处理可保证尚未处理的信号下标始终有效(升序会把新插入的信号当成原信号)。 std::sort(requests.begin(), requests.end(), - [](const BuildRequest& a, const BuildRequest& b) + [](const DerivedSignalRequest& a, const DerivedSignalRequest& b) { if (a.fileIndex != b.fileIndex) { @@ -1604,35 +1590,29 @@ namespace jdi }); int built = 0; - int lastFirst = 0; - int lastSecond = 1; - bool lastDiffAmp = false; - for (const BuildRequest& request : requests) + DerivedSignalRequest last; + for (const DerivedSignalRequest& request : requests) { - if (BuildDerivedSignal(request.fileIndex, request.signalIndex, request.first, - request.second, request.diffAmp, true)) + if (BuildDerivedSignal(request, true)) { ++built; - lastFirst = request.first; - lastSecond = request.second; - lastDiffAmp = request.diffAmp; + last = request; } } if (built > 0) { std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 "; - if (lastDiffAmp) + if (last.weakBoost) { - text += std::to_wstring(lastFirst) + L"点差异放大×" + - std::to_wstring(lastSecond) + L"倍信号。"; + text += L"弱波增强 " + std::to_wstring(last.levelPercent) + L"% 信号。"; } else { - text += std::to_wstring(lastFirst) + L"点均值"; - if (lastSecond > 1) + text += std::to_wstring(last.points) + L"点均值"; + if (last.order > 1) { - text += L"×" + std::to_wstring(lastSecond) + L"阶"; + text += L"×" + std::to_wstring(last.order) + L"阶"; } text += L"滤波信号。"; } @@ -1769,11 +1749,11 @@ namespace jdi meanFilter.checked = m_plot.LaneMeanFilter(laneIndex); experimental.submenu.push_back(meanFilter); - MenuItem diffAmp; - diffAmp.label = L"差异放大"; - diffAmp.id = PLOT_CMD_LANE_DIFF_AMP; - diffAmp.checked = m_plot.LaneDiffAmp(laneIndex); - experimental.submenu.push_back(diffAmp); + MenuItem weakBoost; + weakBoost.label = L"弱波增强"; + weakBoost.id = PLOT_CMD_LANE_WEAK_BOOST; + weakBoost.checked = m_plot.LaneWeakBoost(laneIndex); + experimental.submenu.push_back(weakBoost); } items.push_back(experimental); @@ -1888,9 +1868,9 @@ namespace jdi // 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。 m_plot.ToggleLaneMeanFilter(laneIndex); break; - case PLOT_CMD_LANE_DIFF_AMP: - // 「实验性功能 → 差异放大」:切换差异放大曲线与参数面板。 - m_plot.ToggleLaneDiffAmp(laneIndex); + case PLOT_CMD_LANE_WEAK_BOOST: + // 「实验性功能 → 弱波增强」:切换弱波增强曲线与参数面板。 + m_plot.ToggleLaneWeakBoost(laneIndex); break; case PLOT_CMD_LANE_SPLIT: // 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。 @@ -2390,7 +2370,7 @@ namespace jdi { // 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。 ApplyPendingRename(); - // 「均值滤波 → 构建为新信号」按钮:立即构建新信号并同步各视图。 + // 「构建为新信号」按钮(均值滤波 / 弱波增强):立即构建新信号并同步各视图。 ApplyPendingDerivedBuild(); InvalidateRect(hwnd, nullptr, FALSE); } diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 1cc6475..a77acb1 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -30,6 +30,7 @@ namespace jdi MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */ EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */ EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */ + BOOST_LEVEL, /**< 拖动「弱波增强」面板中的等级滑块。 */ LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */ }; @@ -57,9 +58,7 @@ namespace jdi EDGE_CENTER_POINTS, /**< 编辑「中心轴边沿计数」的均值点数 X。 */ EDGE_CENTER_ORDER, /**< 编辑「中心轴边沿计数」的阶数 N。 */ MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */ - MEAN_FILTER_ORDER, /**< 编辑「均值滤波」的阶数 N。 */ - DIFF_AMP_POINTS, /**< 编辑「差异放大」的窗口点数 X。 */ - DIFF_AMP_ORDER /**< 编辑「差异放大」的放大倍数 N。 */ + MEAN_FILTER_ORDER /**< 编辑「均值滤波」的阶数 N。 */ }; /** @@ -77,7 +76,7 @@ namespace jdi PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */ PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */ PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */ - PLOT_CMD_LANE_DIFF_AMP = 11 /**< 切换本网格的「实验性功能 → 差异放大」开关。 */ + PLOT_CMD_LANE_WEAK_BOOST = 11 /**< 切换本网格的「实验性功能 → 弱波增强」开关。 */ }; /** @@ -88,7 +87,7 @@ namespace jdi COUNT = 0, /**< 边沿计数。 */ CENTER = 1, /**< 边沿计数(中心轴)。 */ MEAN = 2, /**< 均值滤波。 */ - DIFF_AMP = 3 /**< 差异放大。 */ + BOOST = 3 /**< 弱波增强。 */ }; /** @@ -523,84 +522,96 @@ namespace jdi bool TakePendingMeanFilterBuild(int& laneIndex, std::vector& items); /** - * @brief 「实验性功能 → 差异放大」的「构建为新信号」单个请求 - * (一个被勾选的信号 + 自己的参数)。 + * @brief 「实验性功能 → 弱波增强」的「构建为新信号」单个请求 + * (一个被勾选的信号 + 整个网格共用的增强等级)。 */ - struct DiffAmpBuild + struct WeakBoostBuild { int fileIndex = -1; /**< 信号所属文件下标。 */ int signalIndex = -1; /**< 文件内信号下标。 */ - int points = 80; /**< 窗口点数 X(略大于纹波周期的一半)。 */ - int gain = 2; /**< 放大倍数 N。 */ + int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ }; /** - * @brief 取出「差异放大 → 构建为新信号」请求(由面板底部按钮触发)。 + * @brief 取出「弱波增强 → 构建为新信号」请求(由面板底部按钮触发)。 * @param [OUT] laneIndex 触发该请求的网格下标。 * @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。 * @return 存在待处理请求返回 true(取出后清空)。 */ - bool TakePendingDiffAmpBuild(int& laneIndex, std::vector& items); + bool TakePendingWeakBoostBuild(int& laneIndex, std::vector& items); /** - * @brief 切换某网格的「差异放大」开关。 + * @brief 切换某网格的「弱波增强」开关。 * @param [IN] laneIndex 网格下标。 * @param [IN] enabled 是否开启。 * @return 状态发生变化返回 true。 */ - bool SetLaneDiffAmp(int laneIndex, bool enabled); + bool SetLaneWeakBoost(int laneIndex, bool enabled); /** - * @brief 查询某网格的「差异放大」开关。 + * @brief 查询某网格的「弱波增强」开关。 * @param [IN] laneIndex 网格下标。 * @return 已开启返回 true。 */ - bool LaneDiffAmp(int laneIndex) const; + bool LaneWeakBoost(int laneIndex) const; /** - * @brief 切换某网格的「差异放大」开关。 + * @brief 切换某网格的「弱波增强」开关。 * @param [IN] laneIndex 网格下标。 * @return 切换后的状态。 */ - bool ToggleLaneDiffAmp(int laneIndex); + bool ToggleLaneWeakBoost(int laneIndex); /** - * @brief 获取差异放大曲线(按「信号 + 窗口点数 + 放大倍数」缓存)。 + * @brief 读取某网格的弱波增强等级(0–1)。 + * @param [IN] laneIndex 网格下标。 + * @return 增强等级;网格不存在时返回 0.5。 + */ + double LaneWeakBoostLevel(int laneIndex) const; + + /** + * @brief 设置某网格的弱波增强等级(0–1,超出范围会被夹取)。 + * @param [IN] laneIndex 网格下标。 + * @param [IN] level 增强等级。 + * @return 等级发生变化返回 true。 + */ + bool SetLaneWeakBoostLevel(int laneIndex, double level); + + /** + * @brief 获取弱波增强曲线(按「信号 + 增强等级」缓存)。 * @param [IN] signal 目标信号。 - * @param [IN] points 窗口点数 X。 - * @param [IN] gain 放大倍数 N。 + * @param [IN] levelPercent 弱波增强等级(0–100,整数百分比)。 * @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。 */ - const std::vector& DiffAmpCurveFor(const SignalData& signal, int points, int gain) const; + const std::vector& WeakBoostCurveFor(const SignalData& signal, int levelPercent) const; /** - * @brief 取某个时刻的差异放大曲线取值(用于纵向光标的取值标注)。 + * @brief 取某个时刻的弱波增强曲线取值(用于纵向光标的取值标注)。 * @param [IN] file 信号所属文件。 * @param [IN] signal 目标信号。 - * @param [IN] points 窗口点数 X(略大于纹波周期的一半)。 - * @param [IN] gain 放大倍数 N。 + * @param [IN] levelPercent 弱波增强等级(0–100)。 * @param [IN] time 采样时刻。 * @param [OUT] value 输出取值。 * @return 取到有效值返回 true。 */ - bool DiffAmpValueAt(const DataFile& file, const SignalData& signal, int points, int gain, - double time, double& value) const; + bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, + double time, double& value) const; /** - * @brief 切换某个信号在「差异放大」面板中的预览勾选状态。 + * @brief 切换某个信号在「弱波增强」面板中的预览勾选状态。 * @param [IN] laneIndex 网格下标。 * @param [IN] ref 目标信号。 * @return 切换后的勾选状态。 */ - bool ToggleLaneDiffAmpSignal(int laneIndex, const SignalRef& ref); + bool ToggleLaneWeakBoostSignal(int laneIndex, const SignalRef& ref); /** - * @brief 查询某个信号在「差异放大」面板中的预览勾选状态。 + * @brief 查询某个信号在「弱波增强」面板中的预览勾选状态。 * @param [IN] laneIndex 网格下标。 * @param [IN] ref 目标信号。 * @return 已勾选返回 true。 */ - bool LaneDiffAmpSignalEnabled(int laneIndex, const SignalRef& ref) const; + bool LaneWeakBoostSignalEnabled(int laneIndex, const SignalRef& ref) const; /** * @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。 @@ -1052,6 +1063,7 @@ namespace jdi gd::RectF close; /**< ✕ 关闭按钮。 */ gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */ gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */ + gd::RectF slider; /**< 「弱波增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */ int rows = 0; /**< 统计行数(每条可见信号一行)。 */ /** * @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。 @@ -1176,15 +1188,14 @@ namespace jdi int points, int order) const; /** - * @brief 绘制某条信号的差异放大曲线(与均值滤波曲线同一配色派生规则)。 + * @brief 绘制某条信号的弱波增强曲线(与均值滤波曲线同一配色派生规则)。 * @param [IN,OUT] g 绘图上下文。 * @param [IN] resolved 信号视图。 * @param [IN] lane 网格绘制信息。 - * @param [IN] points 窗口点数 X(局部差异强度统计半径)。 - * @param [IN] gain 放大倍数 N(最大增益)。 + * @param [IN] levelPercent 弱波增强等级(0–100)。 */ - void PaintDiffAmpCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, - int points, int gain) const; + void PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, + int levelPercent) const; /** * @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。 @@ -1400,9 +1411,10 @@ namespace jdi bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */ int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */ - int m_pendingDiffAmpLane = -1; /**< 待构建的差异放大信号所属网格下标(-1 表示无请求)。 */ + int m_pendingWeakBoostLane = -1; /**< 待构建的弱波增强信号所属网格下标(-1 表示无请求)。 */ std::vector m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */ - std::vector m_pendingDiffAmpItems; /**< 待构建的差异放大信号及其参数。 */ + std::vector m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */ + int m_boostDragLane = -1; /**< 正在拖动等级滑块的面板所属网格。 */ /** * @brief 实验性功能浮动面板的 ✕ 处于"已按下、等待抬起确认"状态。 @@ -1458,15 +1470,14 @@ namespace jdi }; mutable std::deque m_meanCurveCache; - /** 多阶均值滤波曲线缓存项(按「信号指针 + 均值点数 + 阶数」标识)。 */ - struct DiffAmpCurveEntry + /** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比」标识)。 */ + struct WeakBoostCurveEntry { const SignalData* signal = nullptr; - int points = 80; /**< 窗口点数 X。 */ - int gain = 2; /**< 放大倍数 N。 */ + int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ std::vector values; /**< 与 signal->values 等长;缺失处为 NaN。 */ }; - mutable std::deque m_diffAmpCurveCache; + mutable std::deque m_weakBoostCurveCache; /** 中心轴边沿统计缓存项(增量统计机制与 `EdgeCountEntry` 完全一致)。 */ struct EdgeCenterCountEntry diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index c5dc3c3..7a88eab 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -70,9 +70,39 @@ namespace jdi const float kEdgePanelMeanEditHeight = 24.0f; /**< 逐条目面板中每条信号的参数行高度。 */ const float kEdgePanelMeanRowGap = 2.0f; /**< 名称行与参数行之间的间距。 */ const float kEdgePanelMeanCheckSize = 15.0f; /**< 逐条目面板中条目复选框尺寸。 */ - const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 差异放大共用)。 */ + const float kWeakBoostSliderRowHeight = 26.0f; /**< 弱波增强面板中等级滑块行的高度。 */ + const float kWeakBoostTrackHeight = 6.0f; /**< 弱波增强等级滑块轨道高度。 */ + const float kWeakBoostTrackMinWidth = 120.0f; /**< 弱波增强等级滑块轨道的最小宽度。 */ + const wchar_t* const kWeakBoostLevelLabel = L"弱波增强等级"; /**< 等级滑块行左侧的说明文字。 */ + const wchar_t* const kWeakBoostTitle = L"弱波增强"; /**< 弱波增强面板标题。 */ + const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱波增强共用)。 */ const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */ + /** + * @brief 判断坐标是否命中「弱波增强」的等级滑块。 + * + * 轨道本身只有几个像素高,直接按轨道矩形命中很难抓住,因此把命中区在纵向上下各放宽 + * (至少一个轨道高度),与样式区滑块的交互一致。 + * + * @param [IN] track 滑块轨道矩形(宽度为 0 表示当前没有滑块)。 + * @param [IN] scale DPI 缩放比例。 + * @param [IN] x 客户区横坐标。 + * @param [IN] y 客户区纵坐标。 + * @return 命中返回 true。 + */ + bool WeakBoostSliderHit(const gd::RectF& track, float scale, float x, float y) + { + if (track.Width <= 0.0f) + { + return false; + } + gd::RectF hit = track; + const float pad = std::max(8.0f * scale, track.Height); + hit.Y -= pad; + hit.Height += pad * 2.0f; + return UiPointInRect(hit, x, y); + } + /** * @brief 取折线(相邻采样点之间线性连接)在给定时刻的取值。 * @@ -229,7 +259,7 @@ namespace jdi * 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。 */ const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER, - EdgePanelKind::MEAN, EdgePanelKind::DIFF_AMP}; + EdgePanelKind::MEAN, EdgePanelKind::BOOST}; /** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */ const int kEdgePanelKindCount = static_cast(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0])); @@ -1129,7 +1159,7 @@ namespace jdi m_edgeCountCache.clear(); m_edgeCenterCountCache.clear(); m_meanCurveCache.clear(); - m_diffAmpCurveCache.clear(); + m_weakBoostCurveCache.clear(); // 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。 m_hoverSignal = SignalRef(); m_hoverLegend = SignalRef(); @@ -1704,93 +1734,121 @@ namespace jdi return m_tab->Lanes()[static_cast(laneIndex)].meanFilter; } - bool PlotView::LaneDiffAmp(int laneIndex) const + bool PlotView::LaneWeakBoost(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)].diffAmp; + return m_tab->Lanes()[static_cast(laneIndex)].weakBoost; } - bool PlotView::SetLaneDiffAmp(int laneIndex, bool enabled) + double PlotView::LaneWeakBoostLevel(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; + } + + bool PlotView::SetLaneWeakBoostLevel(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)]; - if (lane.diffAmp == enabled) + const double clamped = std::max(0.0, std::min(1.0, level)); + if (lane.weakBoostLevel == clamped) { return false; } - lane.diffAmp = enabled; + lane.weakBoostLevel = clamped; + // 等级变化会改变预览曲线:清掉曲线缓存并让场景缓存失效。 + m_weakBoostCurveCache.clear(); + InvalidateSceneCache(); + return true; + } + + bool PlotView::SetLaneWeakBoost(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) + { + return false; + } + lane.weakBoost = enabled; if (enabled) { // 与「均值滤波」完全一致:默认只勾选网格内**第一个可见信号**。 - if (lane.diffAmpItems.empty()) + if (lane.weakBoostItems.empty()) { for (const SignalRef& ref : lane.signals) { const ResolvedSignal resolved = ResolveSignal(ref); if (resolved.data != nullptr && resolved.visible) { - lane.EnsureDiffAmpItem(ref); + lane.EnsureWeakBoostItem(ref); break; } } } - lane.diffAmpPanelOpen = true; + lane.weakBoostPanelOpen = true; } else { - lane.diffAmpPanelOpen = false; + lane.weakBoostPanelOpen = false; } return true; } - bool PlotView::ToggleLaneDiffAmp(int laneIndex) + bool PlotView::ToggleLaneWeakBoost(int laneIndex) { - const bool enabled = !LaneDiffAmp(laneIndex); - SetLaneDiffAmp(laneIndex, enabled); + const bool enabled = !LaneWeakBoost(laneIndex); + SetLaneWeakBoost(laneIndex, enabled); return enabled; } - bool PlotView::ToggleLaneDiffAmpSignal(int laneIndex, const SignalRef& ref) + bool PlotView::ToggleLaneWeakBoostSignal(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.DiffAmpEnabled(ref)) + if (lane.WeakBoostEnabled(ref)) { - lane.RemoveDiffAmpItem(ref); + lane.RemoveWeakBoostItem(ref); return false; } - lane.EnsureDiffAmpItem(ref); + lane.EnsureWeakBoostItem(ref); return true; } - bool PlotView::LaneDiffAmpSignalEnabled(int laneIndex, const SignalRef& ref) const + bool PlotView::LaneWeakBoostSignalEnabled(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)].DiffAmpEnabled(ref); + return m_tab->Lanes()[static_cast(laneIndex)].WeakBoostEnabled(ref); } - bool PlotView::TakePendingDiffAmpBuild(int& laneIndex, std::vector& items) + bool PlotView::TakePendingWeakBoostBuild(int& laneIndex, std::vector& items) { - if (m_pendingDiffAmpLane < 0) + if (m_pendingWeakBoostLane < 0) { return false; } - laneIndex = m_pendingDiffAmpLane; - items = m_pendingDiffAmpItems; - m_pendingDiffAmpLane = -1; - m_pendingDiffAmpItems.clear(); + laneIndex = m_pendingWeakBoostLane; + items = m_pendingWeakBoostItems; + m_pendingWeakBoostLane = -1; + m_pendingWeakBoostItems.clear(); return true; } @@ -1922,40 +1980,38 @@ namespace jdi return m_meanCurveCache.back().values; } - const std::vector& PlotView::DiffAmpCurveFor(const SignalData& signal, int points, - int gain) const + const std::vector& PlotView::WeakBoostCurveFor(const SignalData& signal, + int levelPercent) const { - const int window = std::max(1, points); - const int multiplier = std::max(1, gain); - for (const DiffAmpCurveEntry& entry : m_diffAmpCurveCache) + const int level = std::max(0, std::min(100, levelPercent)); + for (const WeakBoostCurveEntry& entry : m_weakBoostCurveCache) { - if (entry.signal == &signal && entry.points == window && entry.gain == multiplier) + if (entry.signal == &signal && entry.levelPercent == level) { return entry.values; } } - DiffAmpCurveEntry entry; + WeakBoostCurveEntry entry; entry.signal = &signal; - entry.points = window; - entry.gain = multiplier; + entry.levelPercent = level; // 与「构建为新信号」共用同一实现(Utilities 内的自由函数), // 因此预览曲线与构建出的采样值逐位一致。 - ComputeDiffAmplify(signal.values, window, multiplier, entry.values); + ComputeWeakWaveBoost(signal.values, static_cast(level) / 100.0, entry.values); - m_diffAmpCurveCache.push_back(std::move(entry)); - return m_diffAmpCurveCache.back().values; + m_weakBoostCurveCache.push_back(std::move(entry)); + return m_weakBoostCurveCache.back().values; } - bool PlotView::DiffAmpValueAt(const DataFile& file, const SignalData& signal, int points, int gain, - double time, double& value) const + bool PlotView::WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, + double time, double& value) const { const int index = FindNearestSample(file, time); if (index < 0) { return false; } - const std::vector& curve = DiffAmpCurveFor(signal, points, gain); + const std::vector& curve = WeakBoostCurveFor(signal, levelPercent); if (static_cast(index) >= curve.size()) { return false; @@ -2208,16 +2264,16 @@ 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 diffAmp = (kind == EdgePanelKind::DIFF_AMP); - // 「均值滤波」「中心轴边沿计数」「差异放大」都是逐条目面板(名称 + 输入框 [+ 统计])。 - const bool itemized = (centerAxis || meanFilter || diffAmp); + const bool weakBoost = (kind == EdgePanelKind::BOOST); + // 「均值滤波」「中心轴边沿计数」「弱波增强」都是逐条目面板(名称 + 复选框)。 + const bool itemized = (centerAxis || meanFilter || weakBoost); - // 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “差异放大” + // 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱波增强” // + 光标区间尾缀)。 const std::wstring title = meanFilter ? std::wstring(L"均值滤波") - : (diffAmp ? std::wstring(L"差异放大") - : ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix())); + : (weakBoost ? std::wstring(kWeakBoostTitle) + : ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix())); float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale + 8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) + kEdgePanelTitleHeight * m_scale; @@ -2240,7 +2296,7 @@ namespace jdi } std::size_t rising = 0; std::size_t falling = 0; - if (meanFilter || diffAmp) + if (meanFilter || weakBoost) { continue; } @@ -2266,14 +2322,12 @@ namespace jdi width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth)); } - if (centerAxis || meanFilter || diffAmp) + if (centerAxis || meanFilter) { // 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。 - // (「均值点数 X」/「阶数 N」的布局对三个逐条目面板通用:标签与数值框各占半行, - // 标签取较宽者以保证“放大倍数 N”这类更长的标签不被压住。) + // (「均值点数 X」/「阶数 N」的布局对两个逐条目面板通用:标签与数值框各占半行。) const float labelX = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X"); - const float labelN = std::max(UiMeasureTextWidth(m_fonts->uiSmall, L"阶数 N"), - UiMeasureTextWidth(m_fonts->uiSmall, L"放大倍数 N")); + const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"阶数 N"); const float fieldWidth = std::max(kEdgePanelEditFieldWidth * m_scale * 0.75f, UiMeasureTextWidth(m_fonts->monoSmall, L"1000000") + 18.0f * m_scale); @@ -2282,7 +2336,17 @@ namespace jdi width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth); } - if (meanFilter || diffAmp) + if (weakBoost) + { + // 等级滑块行 = 「弱波增强等级」说明文字 + 轨道 + 百分比数值。 + const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel); + const float valueWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"100%"); + const float rowWidth = labelWidth + 6.0f * m_scale + kWeakBoostTrackMinWidth * m_scale + + 6.0f * m_scale + valueWidth; + width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth); + } + + if (meanFilter || weakBoost) { // 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。 const float buttonWidth = @@ -2809,12 +2873,10 @@ namespace jdi if (m_editTarget == PlotEditTarget::EDGE_CENTER_POINTS || m_editTarget == PlotEditTarget::EDGE_CENTER_ORDER || m_editTarget == PlotEditTarget::MEAN_FILTER_POINTS || - m_editTarget == PlotEditTarget::MEAN_FILTER_ORDER || - m_editTarget == PlotEditTarget::DIFF_AMP_POINTS || - m_editTarget == PlotEditTarget::DIFF_AMP_ORDER) + m_editTarget == PlotEditTarget::MEAN_FILTER_ORDER) { // 「均值点数 X」/「阶数 N」:输入框就落在对应的字段矩形上 - //(中心轴、均值滤波与差异放大面板的字段矩形含义相同)。 + //(中心轴与均值滤波面板的字段矩形含义相同)。 if (m_editFieldRect.Width <= 0.0f) { return gd::RectF(); @@ -2981,37 +3043,29 @@ namespace jdi } // 「均值点数 X」/「阶数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。 - // 「中心轴边沿计数」「差异放大」用**完全相同的参数与语义**, - // 只是各自作用于自己的条目列表。 + // 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用于自己的条目列表。 const bool centerTarget = (target == PlotEditTarget::EDGE_CENTER_POINTS || target == PlotEditTarget::EDGE_CENTER_ORDER); - const bool diffTarget = - (target == PlotEditTarget::DIFF_AMP_POINTS || target == PlotEditTarget::DIFF_AMP_ORDER); if (target == PlotEditTarget::MEAN_FILTER_POINTS || target == PlotEditTarget::MEAN_FILTER_ORDER || - centerTarget || diffTarget) + centerTarget) { Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; const bool isPoints = (target == PlotEditTarget::MEAN_FILTER_POINTS || - target == PlotEditTarget::EDGE_CENTER_POINTS || - target == PlotEditTarget::DIFF_AMP_POINTS); - const Lane::MeanFilterItem* existing = - centerTarget ? lane.FindEdgeCenterItem(m_editMeanRef) - : (diffTarget ? lane.FindDiffAmpItem(m_editMeanRef) - : lane.FindMeanFilterItem(m_editMeanRef)); + target == PlotEditTarget::EDGE_CENTER_POINTS); + const Lane::MeanFilterItem* existing = centerTarget ? lane.FindEdgeCenterItem(m_editMeanRef) + : lane.FindMeanFilterItem(m_editMeanRef); if (existing == nullptr) { return true; } const int maximum = isPoints ? 1000000 : 1000; int value = static_cast(std::lround(parsed)); - // 阶数/倍数至少为 1(1 阶等于不做滤波、1 倍等于不放大);均值点数可以为 0。 + // 阶数至少为 1(1 阶等于不做滤波);均值点数可以为 0。 value = isPoints ? std::max(0, std::min(value, maximum)) : std::max(1, std::min(value, maximum)); - Lane::MeanFilterItem& item = - centerTarget ? lane.EnsureEdgeCenterItem(m_editMeanRef) - : (diffTarget ? lane.EnsureDiffAmpItem(m_editMeanRef) - : lane.EnsureMeanFilterItem(m_editMeanRef)); + Lane::MeanFilterItem& item = centerTarget ? lane.EnsureEdgeCenterItem(m_editMeanRef) + : lane.EnsureMeanFilterItem(m_editMeanRef); if (isPoints) { item.points = value; @@ -3023,7 +3077,6 @@ namespace jdi // 参数变化会改变曲线:清掉派生缓存后重绘。 m_meanCurveCache.clear(); m_edgeCenterCountCache.clear(); - m_diffAmpCurveCache.clear(); InvalidateSceneCache(); return true; } @@ -3221,6 +3274,9 @@ namespace jdi case PlotDragMode::CURSOR_SPAN_MOVE: // 差值标签拖拽:两条光标整体沿时间轴平移 → 左右箭头。 return LoadCursorW(nullptr, IDC_SIZEWE); + case PlotDragMode::BOOST_LEVEL: + // 「弱波增强」等级滑块拖拽 → 左右箭头。 + return LoadCursorW(nullptr, IDC_SIZEWE); case PlotDragMode::MOVE_SIGNAL: // 拖动信号(参考 Photoshop 平移元素的抓握光标)。 return LoadCursorW(nullptr, IDC_SIZEALL); @@ -3266,7 +3322,14 @@ namespace jdi { return LoadCursorW(nullptr, IDC_SIZEALL); } - if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::DIFF_AMP) && + if (kind == EdgePanelKind::BOOST && + WeakBoostSliderHit(layout.slider, m_scale, static_cast(x), + static_cast(y))) + { + // 等级滑块:左右箭头表示可左右拖动。 + return LoadCursorW(nullptr, IDC_SIZEWE); + } + if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::BOOST) && layout.button.Width > 0.0f && UiPointInRect(layout.button, static_cast(x), static_cast(y))) { @@ -3441,7 +3504,9 @@ namespace jdi target = snapped; } - if (m_hoverCursor == 2 && Tab().CursorMode() == PlotCursorMode::DOUBLE) + const bool second = + (m_hoverCursor == 2 && Tab().CursorMode() == PlotCursorMode::DOUBLE); + if (second) { Tab().CursorTimeAlt() = target; } @@ -3449,6 +3514,10 @@ namespace jdi { Tab().CursorTime() = target; } + // 抓取读数标签时可能带有一个较大的横向抓取偏移(放大到一定程度后该偏移 + // 换算成的时间可能落到视界之外)——拖拽结束前必须把光标夹回当前可见区间, + // 否则用户会看到光标线"跑到视界外"而无法继续操作。 + ClampCursorToView(second ? 2 : 1); m_dragMoved = true; m_cursorHover = true; return true; @@ -3458,8 +3527,58 @@ namespace jdi // 拖拽差值标签:两条光标整体平移,间隔保持不变。 const double startTime = XToTime(static_cast(m_spanDragStartX)); const double delta = XToTime(fx) - startTime; - Tab().CursorTime() = m_spanDragTime1 + delta; - Tab().CursorTimeAlt() = m_spanDragTime2 + delta; + double first = m_spanDragTime1 + delta; + double second = m_spanDragTime2 + delta; + // 整体平移后同样不能越出可视区:先尝试保持间隔地整体夹取, + // 两光标间隔本身就大于可视跨度时退化为各自夹取。 + const double low = Tab().ViewMin(); + const double high = Tab().ViewMax(); + if (high > low) + { + const double inset = (high - low) * 0.002; + const double lo = low + inset; + const double hi = high - inset; + const double pairLow = std::min(first, second); + const double pairHigh = std::max(first, second); + if (pairHigh - pairLow <= hi - lo) + { + if (pairLow < lo) + { + first += lo - pairLow; + second += lo - pairLow; + } + else if (pairHigh > hi) + { + first -= pairHigh - hi; + second -= pairHigh - hi; + } + } + else + { + first = ClampDouble(first, lo, hi); + second = ClampDouble(second, lo, hi); + } + } + Tab().CursorTime() = first; + Tab().CursorTimeAlt() = second; + m_dragMoved = true; + return true; + } + case PlotDragMode::BOOST_LEVEL: + { + // 「弱波增强」等级滑块:按滑轨上的横向位置换算 0–100%。 + if (m_tab == nullptr || m_boostDragLane < 0 || + m_boostDragLane >= static_cast(m_lanes.size())) + { + break; + } + const EdgePanelLayout layout = EdgePanelLayoutFor(m_boostDragLane, EdgePanelKind::BOOST); + if (layout.slider.Width <= 0.0f) + { + break; + } + const double ratio = UiSliderRatioFromX(layout.slider, fx); + SetLaneWeakBoostLevel(m_boostDragLane, ratio); m_dragMoved = true; return true; } @@ -3538,9 +3657,9 @@ namespace jdi lane.meanFilterPanelX = left; lane.meanFilterPanelY = top; break; - case EdgePanelKind::DIFF_AMP: - lane.diffAmpPanelX = left; - lane.diffAmpPanelY = top; + case EdgePanelKind::BOOST: + lane.weakBoostPanelX = left; + lane.weakBoostPanelY = top; break; case EdgePanelKind::COUNT: default: @@ -3662,6 +3781,7 @@ namespace jdi m_rubberRect = gd::RectF(); m_rubberAxis = PlotRubberAxis::UNDECIDED; m_cursorGrabOffset = 0; + m_boostDragLane = -1; // 新的交互开始:清掉上一次留下的提示(拖动过程中会重新设置粘滞提示)。 SetHint(std::wstring()); // 上一轮尚未抬起确认的面板关闭请求一律作废。 @@ -3770,23 +3890,33 @@ namespace jdi m_pendingPanelCloseKind = kind; } - // 「中心轴边沿计数」/「均值滤波」/「差异放大」面板:逐信号条目 - // (同一套排布与交互,只是输入框绑定的编辑目标与条目列表不同)。 + // 「中心轴边沿计数」/「均值滤波」/「弱波增强」面板:逐信号条目 + // (同一套「名称 + 复选框」排布,只是复选框绑定的条目列表不同)。 if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER || - kind == EdgePanelKind::DIFF_AMP) && + kind == EdgePanelKind::BOOST) && m_tab != nullptr) { const bool meanMode = (kind == EdgePanelKind::MEAN); - const bool diffMode = (kind == EdgePanelKind::DIFF_AMP); + const bool boostMode = (kind == EdgePanelKind::BOOST); Lane& tabLane = m_tab->Lanes()[static_cast(index)]; + + // 弱波增强等级滑块:按下即开始拖动(拖动过程中实时更新等级)。 + if (boostMode && WeakBoostSliderHit(layout.slider, m_scale, fx, fy)) + { + m_dragMode = PlotDragMode::BOOST_LEVEL; + m_boostDragLane = index; + SetLaneWeakBoostLevel(index, UiSliderRatioFromX(layout.slider, fx)); + return true; + } + for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex) { const SignalRef& ref = layout.rowRefs[rowIndex]; const auto findItem = [&]() -> const Lane::MeanFilterItem* { return meanMode ? tabLane.FindMeanFilterItem(ref) - : (diffMode ? tabLane.FindDiffAmpItem(ref) - : tabLane.FindEdgeCenterItem(ref)); + : (boostMode ? tabLane.FindWeakBoostItem(ref) + : tabLane.FindEdgeCenterItem(ref)); }; // 第一排右侧的复选框 → 切换该信号的预览勾选状态。 if (layout.rowChecks[rowIndex].Width > 0.0f && @@ -3796,9 +3926,9 @@ namespace jdi { ToggleLaneMeanFilterSignal(index, ref); } - else if (diffMode) + else if (boostMode) { - ToggleLaneDiffAmpSignal(index, ref); + ToggleLaneWeakBoostSignal(index, ref); } else { @@ -3806,23 +3936,21 @@ namespace jdi } m_meanCurveCache.clear(); m_edgeCenterCountCache.clear(); - m_diffAmpCurveCache.clear(); + m_weakBoostCurveCache.clear(); InvalidateSceneCache(); m_dragMode = PlotDragMode::NONE; return true; } // 第二排的两个输入框 → 就地编辑该信号的均值点数 X / 阶数 N - //(差异放大面板是窗口点数 X / 放大倍数 N,交互完全一致)。 + //(弱波增强面板没有参数输入框,等级由滑块统一控制)。 if (layout.rowPoints[rowIndex].Width > 0.0f && UiPointInRect(layout.rowPoints[rowIndex], fx, fy)) { const Lane::MeanFilterItem* item = findItem(); m_editMeanRef = ref; BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_POINTS - : (diffMode ? PlotEditTarget::DIFF_AMP_POINTS - : PlotEditTarget::EDGE_CENTER_POINTS), - index, - std::to_wstring(item != nullptr ? item->points : (diffMode ? 64 : 5)), + : PlotEditTarget::EDGE_CENTER_POINTS, + index, std::to_wstring(item != nullptr ? item->points : 5), layout.rowPoints[rowIndex]); return true; } @@ -3832,20 +3960,21 @@ namespace jdi const Lane::MeanFilterItem* item = findItem(); m_editMeanRef = ref; BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ORDER - : (diffMode ? PlotEditTarget::DIFF_AMP_ORDER - : PlotEditTarget::EDGE_CENTER_ORDER), - index, - std::to_wstring(item != nullptr ? item->order : (diffMode ? 2 : 1)), + : PlotEditTarget::EDGE_CENTER_ORDER, + index, std::to_wstring(item != nullptr ? item->order : 1), layout.rowOrder[rowIndex]); return true; } } - if (diffMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy)) + if (boostMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy)) { - // 「差异放大 → 构建为新信号」:与均值滤波同一套请求机制。 + // 「弱波增强 → 构建为新信号」:与均值滤波同一套请求机制 + //(等级属于整个网格,网格内各被勾选信号共用同一个值)。 const Lane& lane = m_tab->Lanes()[static_cast(index)]; - m_pendingDiffAmpItems.clear(); + const int levelPercent = static_cast( + std::lround(std::max(0.0, std::min(1.0, lane.weakBoostLevel)) * 100.0)); + m_pendingWeakBoostItems.clear(); for (const SignalRef& ref : lane.signals) { const ResolvedSignal resolved = ResolveSignal(ref); @@ -3853,25 +3982,22 @@ namespace jdi { continue; } - const Lane::DiffAmpItem* item = lane.FindDiffAmpItem(ref); - if (item == nullptr) + if (lane.FindWeakBoostItem(ref) == nullptr) { continue; } - DiffAmpBuild build; + WeakBoostBuild build; build.fileIndex = ref.fileIndex; build.signalIndex = ref.signalIndex; - build.points = item->points; - build.gain = item->order; - m_pendingDiffAmpItems.push_back(build); + build.levelPercent = levelPercent; + m_pendingWeakBoostItems.push_back(build); } - if (!m_pendingDiffAmpItems.empty()) + if (!m_pendingWeakBoostItems.empty()) { - const DiffAmpBuild& first = m_pendingDiffAmpItems.front(); - SetHint(L"正在构建差异放大信号(" + std::to_wstring(first.points) + - L" 点 " + std::to_wstring(first.gain) + L" 倍,共 " + - std::to_wstring(m_pendingDiffAmpItems.size()) + L" 条)…"); - m_pendingDiffAmpLane = index; + SetHint(L"正在构建弱波增强信号(等级 " + std::to_wstring(levelPercent) + + L"%,共 " + std::to_wstring(m_pendingWeakBoostItems.size()) + + L" 条)…"); + m_pendingWeakBoostLane = index; } m_dragMode = PlotDragMode::NONE; return true; @@ -4014,6 +4140,7 @@ namespace jdi m_edgeDragLane = -1; m_edgePanelDragLane = -1; m_edgePanelDragKind = EdgePanelKind::COUNT; + m_boostDragLane = -1; SetHint(std::wstring()); m_dragLane = LaneAtY(y); m_dragMode = PlotDragMode::PAN; @@ -4059,8 +4186,8 @@ namespace jdi case EdgePanelKind::MEAN: SetLaneMeanFilter(lane, false); break; - case EdgePanelKind::DIFF_AMP: - SetLaneDiffAmp(lane, false); + case EdgePanelKind::BOOST: + SetLaneWeakBoost(lane, false); break; case EdgePanelKind::COUNT: default: @@ -4132,6 +4259,7 @@ namespace jdi m_edgeDragLane = -1; m_edgePanelDragLane = -1; m_edgePanelDragKind = EdgePanelKind::COUNT; + m_boostDragLane = -1; // 松开左键:粘滞提示结束计时,开始"停留后淡出"。 ReleaseHint(); return changed; @@ -5028,15 +5156,14 @@ namespace jdi mix(static_cast(item.points)); mix(static_cast(item.order)); } - // 「差异放大」的开关与每个被勾选信号各自的窗口点数 X / 放大倍数 N 同理。 - mix(Tab().Lanes()[laneIndex].diffAmp ? 1ull : 0ull); - mix(static_cast(Tab().Lanes()[laneIndex].diffAmpItems.size())); - for (const Lane::DiffAmpItem& item : Tab().Lanes()[laneIndex].diffAmpItems) + // 「弱波增强」的开关、增强等级与每个被勾选信号的条目同理。 + mix(Tab().Lanes()[laneIndex].weakBoost ? 1ull : 0ull); + mixFloat(static_cast(Tab().Lanes()[laneIndex].weakBoostLevel)); + mix(static_cast(Tab().Lanes()[laneIndex].weakBoostItems.size())); + for (const Lane::WeakBoostItem& item : Tab().Lanes()[laneIndex].weakBoostItems) { mix(static_cast(item.ref.fileIndex)); mix(static_cast(item.ref.signalIndex)); - mix(static_cast(item.points)); - mix(static_cast(item.order)); } mix(signals.size()); for (const SignalRef& ref : signals) @@ -5850,9 +5977,9 @@ namespace jdi // 「均值滤波」开启时,为**被勾选**的信号各叠加一条多阶滑动平均曲线 //(均值点数 X、阶数 N 由该信号自己的条目给出)。 const bool meanFilterOn = Tab().Lanes()[static_cast(laneIndex)].meanFilter; - // 「差异放大」开启时同样只为**被勾选**的信号叠加曲线 - //(窗口点数 X、放大倍数 N 由该信号自己的条目给出)。 - const bool diffAmpOn = Tab().Lanes()[static_cast(laneIndex)].diffAmp; + // 「弱波增强」开启时同样只为**被勾选**的信号叠加曲线 + //(增强等级取整个网格共用的 `weakBoostLevel`)。 + const bool weakBoostOn = Tab().Lanes()[static_cast(laneIndex)].weakBoost; for (const SignalRef& ref : laneSignals) { @@ -5989,14 +6116,20 @@ namespace jdi PaintMeanFilterCurve(g, resolved, lane, item->points, item->order); } } - // 差异放大曲线:同样是仅用于预览的叠加曲线,只在被勾选时绘制。 - if (diffAmpOn) + // 弱波增强曲线:同样是仅用于预览的叠加曲线,只在被勾选时绘制 + //(增强等级取自整个网格)。 + if (weakBoostOn) { - const Lane::DiffAmpItem* item = - Tab().Lanes()[static_cast(laneIndex)].FindDiffAmpItem(ref); + const Lane::WeakBoostItem* item = + Tab().Lanes()[static_cast(laneIndex)].FindWeakBoostItem(ref); if (item != nullptr) { - PaintDiffAmpCurve(g, resolved, lane, item->points, item->order); + const int levelPercent = static_cast(std::lround( + std::max(0.0, std::min(1.0, + Tab().Lanes()[static_cast(laneIndex)] + .weakBoostLevel)) * + 100.0)); + PaintWeakBoostCurve(g, resolved, lane, levelPercent); } } } @@ -6713,22 +6846,24 @@ namespace jdi DrawLabelOutline(g, labelRect, 3.0f * m_scale, ink.outline); UiText(g, text, m_fonts->monoSmall, labelRect, ink.text, TextAlign::CENTER); - // 「边沿计数(中心轴)」/「均值滤波」/「差异放大」开启时,纵向光标还要额外标注 + // 「边沿计数(中心轴)」/「均值滤波」/「弱波增强」开启时,纵向光标还要额外标注 // **派生曲线**在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。 - // 三条曲线的参数与标注规则完全一致,故共用同一段绘制逻辑。 - const auto paintDerivedCapsule = [&](int first, int second, const std::wstring& suffix, - bool diffAmp) + // 三条曲线只是曲线来源不同,标注规则完全一致,故共用同一段绘制逻辑。 + const auto paintDerivedCapsule = [&](const std::vector& curve, + const std::wstring& suffix) { - double derivedValue = 0.0; - const bool ok = - diffAmp ? DiffAmpValueAt(*resolved.file, *resolved.data, first, second, time, - derivedValue) - : MeanFilterValueAt(*resolved.file, *resolved.data, first, second, time, - derivedValue); - if (!ok) + const int sampleIndex = FindNearestSample(*resolved.file, time); + if (sampleIndex < 0 || + static_cast(sampleIndex) >= curve.size()) { return; } + const float sampledCurve = curve[static_cast(sampleIndex)]; + if (!std::isfinite(sampledCurve)) + { + return; + } + const double derivedValue = static_cast(sampledCurve); const float rawCenterY = ValueToY(derivedValue, lane); DataMarker centerMarker = MakeDataMarker( @@ -6743,14 +6878,13 @@ namespace jdi centerText += L"("; if (showDelta) { - double otherDerived = 0.0; - const bool otherOk = - diffAmp ? DiffAmpValueAt(*resolved.file, *resolved.data, first, second, - otherTime, otherDerived) - : MeanFilterValueAt(*resolved.file, *resolved.data, first, second, - otherTime, otherDerived); - if (otherOk) + const int otherIndex = FindNearestSample(*resolved.file, otherTime); + if (otherIndex >= 0 && + static_cast(otherIndex) < curve.size() && + std::isfinite(curve[static_cast(otherIndex)])) { + const double otherDerived = + static_cast(curve[static_cast(otherIndex)]); const double delta = derivedValue - otherDerived; centerText += (delta < 0.0) ? L"-" : L"+"; centerText += FormatValue(std::fabs(delta)); @@ -6799,7 +6933,9 @@ namespace jdi m_tab->Lanes()[laneIndex].FindEdgeCenterItem(ref); if (centerItem != nullptr) { - paintDerivedCapsule(centerItem->points, centerItem->order, L"中心轴", false); + paintDerivedCapsule( + MeanFilterCurveFor(*resolved.data, centerItem->points, centerItem->order), + L"中心轴"); } } if (m_tab->Lanes()[laneIndex].meanFilter) @@ -6809,17 +6945,23 @@ namespace jdi m_tab->Lanes()[laneIndex].FindMeanFilterItem(ref); if (meanItem != nullptr) { - paintDerivedCapsule(meanItem->points, meanItem->order, L"均值滤波", false); + paintDerivedCapsule( + MeanFilterCurveFor(*resolved.data, meanItem->points, meanItem->order), + L"均值滤波"); } } - if (m_tab->Lanes()[laneIndex].diffAmp) + if (m_tab->Lanes()[laneIndex].weakBoost) { - // 差异放大曲线:同样是只标注被勾选的信号,尾缀为「差异化」。 - const Lane::DiffAmpItem* diffItem = - m_tab->Lanes()[laneIndex].FindDiffAmpItem(ref); - if (diffItem != nullptr) + // 弱波增强曲线:同样是只标注被勾选的信号,尾缀为「弱波增强」。 + const Lane::WeakBoostItem* boostItem = + m_tab->Lanes()[laneIndex].FindWeakBoostItem(ref); + if (boostItem != nullptr) { - paintDerivedCapsule(diffItem->points, diffItem->order, L"差异化", true); + const int levelPercent = static_cast(std::lround( + std::max(0.0, std::min(1.0, m_tab->Lanes()[laneIndex].weakBoostLevel)) * + 100.0)); + paintDerivedCapsule(WeakBoostCurveFor(*resolved.data, levelPercent), + L"弱波增强"); } } } @@ -6902,15 +7044,16 @@ 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 diffAmp = (kind == EdgePanelKind::DIFF_AMP); - // 「均值滤波」与「差异放大」共用同一套「逐条目 + 底部按钮」排布。 - const bool itemized = (centerAxis || meanFilter || diffAmp); - const bool hasButton = (meanFilter || diffAmp); - const bool enabled = - meanFilter ? lane.meanFilter : (diffAmp ? lane.diffAmp : (centerAxis ? lane.edgeCenter : lane.edgeCount)); + const bool weakBoost = (kind == EdgePanelKind::BOOST); + // 「均值滤波」与「弱波增强」共用同一套「逐条目 + 底部按钮」排布。 + const bool itemized = (centerAxis || meanFilter || weakBoost); + const bool hasButton = (meanFilter || weakBoost); + const bool enabled = meanFilter ? lane.meanFilter + : (weakBoost ? lane.weakBoost + : (centerAxis ? lane.edgeCenter : lane.edgeCount)); const bool open = meanFilter ? lane.meanFilterPanelOpen - : (diffAmp ? lane.diffAmpPanelOpen - : (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen)); + : (weakBoost ? lane.weakBoostPanelOpen + : (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen)); if (!enabled || !open) { return layout; @@ -6935,20 +7078,22 @@ 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; + // 「弱波增强」在条目与底部按钮之间额外有一行等级滑块,其余逐条目面板没有。 + const float sliderRowHeight = weakBoost ? (kWeakBoostSliderRowHeight * m_scale) : 0.0f; - // 逐条目面板:每个条目 = 第一排(信号名 + 复选框)+ 第二排(均值点数 X / 阶数 N) - // + (中心轴)第三排(统计结果);后两排**只在该条目被勾选时才占位**, - // 因此面板高度随勾选数量动态变化。 + // 逐条目面板:每个条目 = 第一排(信号名 + 复选框) + // + 第二排(均值点数 X / 阶数 N)+(中心轴)第三排(统计结果); + // 后两排**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化。 float meanRowHeight = 0.0f; + float itemsHeight = 0.0f; float height = 0.0f; if (itemized) { meanRowHeight = kEdgePanelMeanRowHeight * m_scale; - float itemsHeight = 0.0f; for (const SignalRef& ref : lane.signals) { const ResolvedSignal resolved = ResolveSignal(ref); @@ -6958,9 +7103,9 @@ namespace jdi } itemsHeight += meanRowHeight; const bool checked = meanFilter ? lane.MeanFilterEnabled(ref) - : (diffAmp ? lane.DiffAmpEnabled(ref) - : lane.EdgeCenterEnabled(ref)); - if (checked) + : (weakBoost ? lane.WeakBoostEnabled(ref) + : lane.EdgeCenterEnabled(ref)); + if (checked && (centerAxis || meanFilter)) { itemsHeight += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale; if (centerAxis) @@ -6971,7 +7116,7 @@ namespace jdi } // 底部按钮行:上方与条目之间、下方与面板下边界之间各留一个 `pad`, // 与面板的左右内边距完全一致(原先按钮直接贴着下边框,看起来很不协调)。 - height = titleHeight + pad + itemsHeight + pad + buttonHeight + + height = titleHeight + pad + itemsHeight + sliderRowHeight + pad + buttonHeight + (hasButton ? pad : 0.0f); } else @@ -6981,11 +7126,11 @@ namespace jdi // 首次使用(位置为负)时自动放到绘图区右侧,并各网格依次错开,避免完全重叠。 const double storedX = meanFilter ? lane.meanFilterPanelX - : (diffAmp ? lane.diffAmpPanelX - : (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX)); + : (weakBoost ? lane.weakBoostPanelX + : (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX)); const double storedY = meanFilter ? lane.meanFilterPanelY - : (diffAmp ? lane.diffAmpPanelY - : (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY)); + : (weakBoost ? lane.weakBoostPanelY + : (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY)); float left = (storedX >= 0.0) ? static_cast(storedX) : (m_plotRect.X + m_plotRect.Width - width - 16.0f * m_scale); @@ -7017,6 +7162,20 @@ namespace jdi std::max(0.0f, width - pad * 2.0f), buttonRowHeight); } + // 弱波增强等级滑块行(位于信号条目与底部按钮之间)。 + if (weakBoost) + { + const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel); + const float valueWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"100%"); + const float trackHeight = kWeakBoostTrackHeight * m_scale; + const float rowTop = 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; + layout.slider = + gd::RectF(trackLeft, rowTop + (sliderRowHeight - trackHeight) * 0.5f, + std::max(10.0f * m_scale, trackRight - trackLeft), trackHeight); + } + // 逐信号条目:第一排 = 名称 + 复选框(复选框靠右); // 第二排 = 「均值点数 X」与「阶数 N」两个输入框(仅勾选时存在); // 第三排(中心轴)= 上升沿/下降沿统计结果(仅勾选时存在)。 @@ -7037,9 +7196,9 @@ namespace jdi left + width - pad - checkSize, rowY + (meanRowHeight - checkSize) * 0.5f, checkSize, checkSize)); const bool checked = meanFilter ? lane.MeanFilterEnabled(ref) - : (diffAmp ? lane.DiffAmpEnabled(ref) - : lane.EdgeCenterEnabled(ref)); - if (checked) + : (weakBoost ? lane.WeakBoostEnabled(ref) + : lane.EdgeCenterEnabled(ref)); + if (checked && (centerAxis || meanFilter)) { const float editTop = rowY + meanRowHeight; layout.rowPoints.push_back( @@ -7065,7 +7224,7 @@ namespace jdi } rowY += meanRowHeight; - if (checked) + if (checked && (centerAxis || meanFilter)) { rowY += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale; if (centerAxis) @@ -7408,15 +7567,15 @@ namespace jdi PaintDerivedCurve(g, resolved, lane, MeanFilterCurveFor(*resolved.data, points, order)); } - void PlotView::PaintDiffAmpCurve(gd::Graphics& g, const ResolvedSignal& resolved, - const LaneVisual& lane, int points, int gain) const + void PlotView::PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, + const LaneVisual& lane, int levelPercent) const { if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr) { return; } - // 派生曲线一律使用同一段绘制逻辑与配色规则(差异放大曲线同样如此)。 - PaintDerivedCurve(g, resolved, lane, DiffAmpCurveFor(*resolved.data, points, gain)); + // 派生曲线一律使用同一段绘制逻辑与配色规则(弱波增强曲线同样如此)。 + PaintDerivedCurve(g, resolved, lane, WeakBoostCurveFor(*resolved.data, levelPercent)); } void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector& lanes) @@ -7523,17 +7682,17 @@ namespace jdi } // ---- 统计浮动面板(最后绘制,覆盖在波形之上)---- - // 「边沿计数」「边沿计数(中心轴)」「均值滤波」「差异放大」共用同一套样式, + // 「边沿计数」「边沿计数(中心轴)」「均值滤波」「弱波增强」共用同一套样式, // 仅标题、内容行与底部按钮不同。 for (int pass = 0; pass < kEdgePanelKindCount; ++pass) { const EdgePanelKind kind = kEdgePanelKinds[pass]; const bool centerAxis = (kind == EdgePanelKind::CENTER); const bool meanFilter = (kind == EdgePanelKind::MEAN); - const bool diffAmp = (kind == EdgePanelKind::DIFF_AMP); - // 「均值滤波」与「差异放大」共用「逐条目 + 底部按钮」排布。 - const bool itemized = (centerAxis || meanFilter || diffAmp); - const bool hasButton = (meanFilter || diffAmp); + const bool weakBoost = (kind == EdgePanelKind::BOOST); + // 「均值滤波」与「弱波增强」共用「逐条目 + 底部按钮」排布。 + const bool itemized = (centerAxis || meanFilter || weakBoost); + const bool hasButton = (meanFilter || weakBoost); for (std::size_t index = 0; index < count; ++index) { const Lane& tabLane = tabLanes[index]; @@ -7568,9 +7727,9 @@ namespace jdi m_theme->text, TextAlign::CENTER); const std::wstring title = meanFilter ? std::wstring(L"均值滤波") - : (diffAmp ? std::wstring(L"差异放大") - : ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + - EdgeRangeSuffix())); + : (weakBoost ? std::wstring(kWeakBoostTitle) + : ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + + EdgeRangeSuffix())); UiText(g, title, m_fonts->uiSmall, gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y, panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight, @@ -7598,9 +7757,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) @@ -7614,8 +7773,8 @@ namespace jdi const std::uint32_t signalColor = StyleColor(*resolved.style); const gd::RectF& checkRect = layout.rowChecks[rowIndex]; const bool checked = meanFilter ? tabLane.MeanFilterEnabled(ref) - : (diffAmp ? tabLane.DiffAmpEnabled(ref) - : tabLane.EdgeCenterEnabled(ref)); + : (weakBoost ? tabLane.WeakBoostEnabled(ref) + : tabLane.EdgeCenterEnabled(ref)); // 名称:第一排左侧,避开右侧复选框。 const float nameWidth = @@ -7632,26 +7791,15 @@ namespace jdi continue; } - // 第二排:两个输入框。均值滤波是“均值点数 X / 阶数 N”, - // 差异放大是“窗口点数 X / 放大倍数 N”(同一套排版)。 - const Lane::MeanFilterItem* item = - meanFilter ? tabLane.FindMeanFilterItem(ref) - : (diffAmp ? tabLane.FindDiffAmpItem(ref) - : tabLane.FindEdgeCenterItem(ref)); - const int shownPoints = - (item != nullptr) ? item->points : (diffAmp ? 64 : 5); - const int shownOrder = - (item != nullptr) ? item->order : (diffAmp ? 2 : 1); - const wchar_t* pointsLabel = diffAmp ? L"窗口点数 X" : L"均值点数 X"; - const wchar_t* orderLabel = diffAmp ? L"放大倍数 N" : L"阶数 N"; - const PlotEditTarget pointsTarget = - meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS - : (diffAmp ? PlotEditTarget::DIFF_AMP_POINTS - : PlotEditTarget::EDGE_CENTER_POINTS); - const PlotEditTarget orderTarget = - meanFilter ? PlotEditTarget::MEAN_FILTER_ORDER - : (diffAmp ? PlotEditTarget::DIFF_AMP_ORDER - : PlotEditTarget::EDGE_CENTER_ORDER); + // 第二排:两个输入框(「均值点数 X / 阶数 N」;弱波增强没有参数输入框)。 + const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref) + : tabLane.FindEdgeCenterItem(ref); + const int shownPoints = (item != nullptr) ? item->points : 5; + const int shownOrder = (item != nullptr) ? item->order : 1; + const PlotEditTarget pointsTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS + : PlotEditTarget::EDGE_CENTER_POINTS; + const PlotEditTarget orderTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_ORDER + : PlotEditTarget::EDGE_CENTER_ORDER; const auto paintField = [&](const gd::RectF& rect, const wchar_t* label, int value, PlotEditTarget target) { @@ -7688,8 +7836,8 @@ namespace jdi m_theme->textStrong, TextAlign::CENTER); } }; - paintField(layout.rowPoints[rowIndex], pointsLabel, shownPoints, pointsTarget); - paintField(layout.rowOrder[rowIndex], orderLabel, shownOrder, orderTarget); + paintField(layout.rowPoints[rowIndex], L"均值点数 X", shownPoints, pointsTarget); + paintField(layout.rowOrder[rowIndex], L"阶数 N", shownOrder, orderTarget); // 第三排(中心轴):该信号在当前参数下的上升沿/下降沿统计结果。 if (centerAxis && rowIndex < layout.rowStats.size() && @@ -7709,6 +7857,35 @@ namespace jdi TextAlign::LEFT); } } + + // 「弱波增强」等级滑块:左侧说明文字 + 轨道 + 右侧百分比数值。 + if (weakBoost && layout.slider.Width > 0.0f) + { + const gd::RectF& track = layout.slider; + const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel); + const float rowHeight = std::max(kWeakBoostSliderRowHeight * m_scale, track.Height); + UiText(g, kWeakBoostLevelLabel, m_fonts->uiSmall, + gd::RectF(track.X - 6.0f * m_scale - labelWidth, + track.Y + track.Height * 0.5f - rowHeight * 0.5f, labelWidth, + rowHeight), + m_theme->textMuted, TextAlign::LEFT); + + const double level = + std::max(0.0, std::min(1.0, tabLane.weakBoostLevel)); + const bool active = (m_dragMode == PlotDragMode::BOOST_LEVEL) && + (m_boostDragLane == static_cast(index)); + // 与样式区/拾色器共用同一套滑块绘制(轨道 + 已填充部分 + 圆钮)。 + UiDrawSlider(g, track, level, m_theme->accent, active, m_scale); + + const std::wstring levelText = + std::to_wstring(static_cast(std::lround(level * 100.0))) + L"%"; + const float valueWidth = UiMeasureTextWidth(m_fonts->monoSmall, levelText); + UiText(g, levelText, m_fonts->monoSmall, + gd::RectF(track.X + track.Width + 6.0f * m_scale, + track.Y + track.Height * 0.5f - rowHeight * 0.5f, valueWidth, + rowHeight), + m_theme->textStrong, TextAlign::LEFT); + } } float rowY = panel.Y + titleHeight + pad; diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 1718f52..01bf858 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -374,13 +374,13 @@ namespace jdi using EdgeCenterItem = MeanFilterItem; /** - * @brief 「差异放大」中某个信号自己的参数。 + * @brief 「弱波增强」中某个信号自己的条目。 * - * 字段含义与均值滤波/中心轴不同(但同样是「一个信号 + 两个整数参数」, - * 因此复用同一结构以共享面板、缓存与引用重映射的实现): - * `points` = 窗口点数 X(趋势窗口半径),`order` = 放大倍数 N。 + * 目前只用到 `ref`(增强等级属于整个网格,统一保存在 `weakBoostLevel` 中); + * 复用 `MeanFilterItem` 结构是为了与均值滤波/中心轴共享 + * 「条目查找 + 信号插入/删除时的引用重映射」等既有实现。 */ - using DiffAmpItem = MeanFilterItem; + using WeakBoostItem = MeanFilterItem; std::wstring name; /**< 网格名称。 */ std::vector signals; /**< 该网格包含的信号(按加入顺序)。 */ @@ -501,46 +501,44 @@ namespace jdi double meanFilterPanelY = -1.0; /** - * @brief 「实验性功能 → 差异放大」开关。 + * @brief 「实验性功能 → 弱波增强」开关。 * - * 开启后为本网格内**被勾选**的信号额外绘制一条「差异放大」曲线: - * 把信号与局部趋势线的差值按“局部差异强度”归一化后放大, - * 使不明显的下凹(如纹波里的弱 M 形)变明显,而差异已经很大的区域**逐点不变**。 + * 开启后为本网格内**被勾选**的信号额外绘制一条「弱波增强」曲线: + * 识别纹波的波峰/波谷,把“比左右同类极值更弱”的波峰上拉、波谷下拉, + * 并按「弱波增强等级」在左右两个同类极值之间取目标值,使其恢复与周边一致的振幅。 * 这条曲线只是叠加显示,不会作为信号加入左侧列表。 */ - bool diffAmp = false; + bool weakBoost = false; - /** - * @brief 参与差异放大预览的信号及其参数(只包含被勾选的信号)。 - * - * `points` = 窗口点数 X(局部差异强度统计半径,约取纹波周期的一半), - * `order` = 放大倍数 N(最大放大倍数)。 - */ - std::vector diffAmpItems; + /** 弱波增强等级(0–1),0% = 目标取左右极值中较小/较大的那一个,100% = 取另一个。 */ + double weakBoostLevel = 0.5; - /** 取某个信号的差异放大参数(未勾选时返回空指针)。 */ - const DiffAmpItem* FindDiffAmpItem(const SignalRef& ref) const; + /** 参与弱波增强预览的信号条目(只包含被勾选的信号)。 */ + std::vector weakBoostItems; - /** 取得或创建某信号的差异放大参数(用于勾选)。 */ - DiffAmpItem& EnsureDiffAmpItem(const SignalRef& ref); + /** 取某个信号的弱波增强条目(未勾选时返回空指针)。 */ + const WeakBoostItem* FindWeakBoostItem(const SignalRef& ref) const; - /** 取消某信号的差异放大勾选。 */ - void RemoveDiffAmpItem(const SignalRef& ref); + /** 取得或创建某信号的弱波增强条目(用于勾选)。 */ + WeakBoostItem& EnsureWeakBoostItem(const SignalRef& ref); - /** 取某信号的差异放大勾选状态。 */ - bool DiffAmpEnabled(const SignalRef& ref) const + /** 取消某信号的弱波增强勾选。 */ + void RemoveWeakBoostItem(const SignalRef& ref); + + /** 取某信号的弱波增强勾选状态。 */ + bool WeakBoostEnabled(const SignalRef& ref) const { - return FindDiffAmpItem(ref) != nullptr; + return FindWeakBoostItem(ref) != nullptr; } - /** 差异放大浮动面板是否展开。 */ - bool diffAmpPanelOpen = true; + /** 弱波增强浮动面板是否展开。 */ + bool weakBoostPanelOpen = true; - /** 差异放大面板左上角位置(客户区像素;<0 表示自动定位)。 */ - double diffAmpPanelX = -1.0; + /** 弱波增强面板左上角位置(客户区像素;<0 表示自动定位)。 */ + double weakBoostPanelX = -1.0; - /** 差异放大面板左上角纵坐标(<0 表示自动定位)。 */ - double diffAmpPanelY = -1.0; + /** 弱波增强面板左上角纵坐标(<0 表示自动定位)。 */ + double weakBoostPanelY = -1.0; }; /** @@ -570,45 +568,6 @@ namespace jdi */ void ComputeMovingAverage(const std::vector& values, int radius, std::vector& out); - /** - * @brief 计算 N 点滑动平均(float 输入版本)。 - * - * 与 `ComputeMovingAverage(const std::vector&, ...)` 完全同一实现, - * 供「差异放大」在 float 缓冲上二次滑动平均使用:亿级采样下若用 double 中转 - * 会多占一倍内存,而曲线本身只要求 float 精度。 - * - * @param [IN] values 原始采样值。 - * @param [IN] radius 平滑半径 N(0 表示原样返回)。 - * @param [OUT] out 输出数组(长度与 values 相同)。 - */ - void ComputeMovingAverage(const std::vector& values, int radius, std::vector& out); - - /** - * @brief 「差异放大」:把不明显的局部差异放大,而差异已经很大的区域逐点不变。 - * - * 算法(三部分,全部 O(n)): - * 1. **趋势线** `trend` = 以 8×window 为半径的滑动平均 —— 把“整体”(低频成分)分离出来; - * 该半径必须远大于纹波周期,趋势线才不带着纹波。 - * 2. **局部差异强度** `strength` = |采样值 − 趋势线| 的滑动平均(半径 = window) - * —— 衡量该处“纹波有多明显”。窗口恰好覆盖一个纹波周期时该值与相位无关 - * (正弦为 2A/π),因此 `window` 应取**纹波周期的一半左右**; - * 3. **逐点增益** `gain = clamp(目标强度 / 该处强度, 1, 放大倍数)`, - * 目标强度取全信号差异强度的 75 分位(用直方图统计,O(1) 额外内存; - * 用均值会被个别尖峰抬高,用最大值则完全被最尖处主导)。 - * - * 输出 `out = trend + gain × (采样值 − trend)`。 - * **增益下界为 1**,所以差异强度不低于目标强度的采样点(即绝大多数“正常”数据点) - * 输出与原值逐位相同 —— 这正是“不影响其它数据点的振幅范围”的实现方式; - * 只有强度低于目标强度的区域(如被削弱的那个周期)才被放大,最多放大到 `gain` 倍。 - * - * @param [IN] values 原始采样值。 - * @param [IN] window 窗口点数 X(局部差异强度统计半径,应取纹波周期的一半左右)。 - * @param [IN] gain 放大倍数 N(最大增益,至少 1)。 - * @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。 - */ - void ComputeDiffAmplify(const std::vector& values, int window, int gain, - std::vector& out); - /** * @brief 计算多阶(迭代)滑动平均:把 N 点滑动平均重复应用 `order` 次。 * @@ -623,6 +582,32 @@ namespace jdi void ComputeMovingAverageIterated(const std::vector& values, int radius, int order, std::vector& out); + /** + * @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)。 + * 4. **平滑偏移**:极值点本身取其目标值,两侧的数据点按相邻两个极值点的偏移量 + * 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱波附近被推回。 + * + * 与“全局缩放/局部放大”不同,这里**只改动被判为弱波的极值及其两侧**, + * 正常周期的极值不满足条件、偏移量恒为 0,因此输出与输入逐位相同。 + * + * @param [IN] values 原始采样值。 + * @param [IN] level 弱波增强等级(0–1,超出范围会被夹取)。 + * @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。 + */ + void ComputeWeakWaveBoost(const std::vector& values, double level, + std::vector& out); + /** * @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。 * @param [IN] file 目标文件。 diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index a5899a1..e4bf3cc 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -134,11 +134,12 @@ namespace jdi bool meanFilterPanelOpen = true; double meanFilterPanelX = -1.0; double meanFilterPanelY = -1.0; - bool diffAmp = false; - std::vector diffAmpItems; - bool diffAmpPanelOpen = true; - double diffAmpPanelX = -1.0; - double diffAmpPanelY = -1.0; + bool weakBoost = false; + std::vector weakBoostItems; + double weakBoostLevel = 0.5; + bool weakBoostPanelOpen = true; + double weakBoostPanelX = -1.0; + double weakBoostPanelY = -1.0; }; // 先记下每个信号原来所在网格的网格自身状态: @@ -164,11 +165,12 @@ namespace jdi carry.meanFilterPanelOpen = lane.meanFilterPanelOpen; carry.meanFilterPanelX = lane.meanFilterPanelX; carry.meanFilterPanelY = lane.meanFilterPanelY; - carry.diffAmp = lane.diffAmp; - carry.diffAmpItems = lane.diffAmpItems; - carry.diffAmpPanelOpen = lane.diffAmpPanelOpen; - carry.diffAmpPanelX = lane.diffAmpPanelX; - carry.diffAmpPanelY = lane.diffAmpPanelY; + carry.weakBoost = lane.weakBoost; + carry.weakBoostItems = lane.weakBoostItems; + carry.weakBoostLevel = lane.weakBoostLevel; + carry.weakBoostPanelOpen = lane.weakBoostPanelOpen; + carry.weakBoostPanelX = lane.weakBoostPanelX; + carry.weakBoostPanelY = lane.weakBoostPanelY; for (const SignalRef& ref : lane.signals) { previousCarry.emplace_back(ref, carry); @@ -205,11 +207,12 @@ namespace jdi lane.meanFilterPanelOpen = carry.meanFilterPanelOpen; lane.meanFilterPanelX = carry.meanFilterPanelX; lane.meanFilterPanelY = carry.meanFilterPanelY; - lane.diffAmp = carry.diffAmp; - lane.diffAmpItems = carry.diffAmpItems; - lane.diffAmpPanelOpen = carry.diffAmpPanelOpen; - lane.diffAmpPanelX = carry.diffAmpPanelX; - lane.diffAmpPanelY = carry.diffAmpPanelY; + lane.weakBoost = carry.weakBoost; + lane.weakBoostItems = carry.weakBoostItems; + lane.weakBoostLevel = carry.weakBoostLevel; + lane.weakBoostPanelOpen = carry.weakBoostPanelOpen; + lane.weakBoostPanelX = carry.weakBoostPanelX; + lane.weakBoostPanelY = carry.weakBoostPanelY; }; // 收集全部可见信号引用。 @@ -917,60 +920,10 @@ namespace jdi * @brief 滑动平均的共用实现(输入只要可转成 double)。 * * 窗口 = [max(0,i-N), min(count-1,i+N)],每前进一步只增减边界处的两个采样点, - * 整体 O(n),不额外占用前缀和内存。“差异放大”需要在 float 缓冲上再做一次 - * 滑动平均,若先转成 double 会因为亿级采样多占一倍内存,因此做成模板。 + * 整体 O(n),不额外占用前缀和内存。 */ template void MovingAverageImpl(const std::vector& values, int radius, std::vector& out); - /** - * @brief 滑动窗口最大值(只要最大值)。 - * - * 与 `SlidingExtrema` 同一个单调队列思路,但**不额外分配最小值数组**: - * 亿级采样下一次全量计算会多占 100 MB 以上,而“差异放大”只需要最大值。 - * - * @param [IN] values 采样值。 - * @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。 - * @param [OUT] outMax 邻域最大值(整窗都是 NaN 时为 NaN)。 - */ - template - void SlidingMax(const std::vector& values, std::size_t radius, std::vector& outMax) - { - const std::size_t count = values.size(); - outMax.assign(count, std::numeric_limits::quiet_NaN()); - if (count == 0) - { - return; - } - - std::deque maxQueue; // 递减:队首最大。 - std::size_t filled = 0; - for (std::size_t index = 0; index < count; ++index) - { - const std::size_t wantHigh = std::min(count - 1, index + radius); - while (filled <= wantHigh) - { - const double incoming = values[filled]; - if (std::isfinite(incoming)) - { - while (!maxQueue.empty() && values[maxQueue.back()] <= incoming) - { - maxQueue.pop_back(); - } - maxQueue.push_back(filled); - } - ++filled; - } - const std::size_t low = (index > radius) ? (index - radius) : 0; - while (!maxQueue.empty() && maxQueue.front() < low) - { - maxQueue.pop_front(); - } - if (!maxQueue.empty()) - { - outMax[index] = static_cast(values[maxQueue.front()]); - } - } - } } // namespace void ComputeMovingAverage(const std::vector& values, int radius, std::vector& out) @@ -978,11 +931,6 @@ namespace jdi MovingAverageImpl(values, radius, out); } - void ComputeMovingAverage(const std::vector& values, int radius, std::vector& out) - { - MovingAverageImpl(values, radius, out); - } - namespace { template @@ -1067,73 +1015,6 @@ namespace jdi } } } - - /** - * @brief 滑动窗口极值:一次求出每个中心点邻域(±radius)内的最小值与最大值。 - * - * 两个单调队列(最小值队列递增、最大值队列递减)同步滑动,O(n) 时间, - * 内存只有窗口长度。NaN 不参与(整窗都是 NaN 时输出 NaN)。 - * - * @param [IN] values 采样值。 - * @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。 - * @param [OUT] outMin 邻域最小值。 - * @param [OUT] outMax 邻域最大值。 - */ - template - void SlidingExtrema(const std::vector& values, std::size_t radius, - std::vector& outMin, std::vector& outMax) - { - const std::size_t count = values.size(); - outMin.assign(count, std::numeric_limits::quiet_NaN()); - outMax.assign(count, std::numeric_limits::quiet_NaN()); - if (count == 0) - { - return; - } - - std::deque minQueue; // 递增:队首最小。 - std::deque maxQueue; // 递减:队首最大。 - std::size_t filled = 0; - for (std::size_t index = 0; index < count; ++index) - { - const std::size_t wantHigh = std::min(count - 1, index + radius); - while (filled <= wantHigh) - { - const double incoming = values[filled]; - if (std::isfinite(incoming)) - { - while (!minQueue.empty() && values[minQueue.back()] >= incoming) - { - minQueue.pop_back(); - } - minQueue.push_back(filled); - while (!maxQueue.empty() && values[maxQueue.back()] <= incoming) - { - maxQueue.pop_back(); - } - maxQueue.push_back(filled); - } - ++filled; - } - const std::size_t low = (index > radius) ? (index - radius) : 0; - while (!minQueue.empty() && minQueue.front() < low) - { - minQueue.pop_front(); - } - while (!maxQueue.empty() && maxQueue.front() < low) - { - maxQueue.pop_front(); - } - if (!minQueue.empty()) - { - outMin[index] = static_cast(values[minQueue.front()]); - } - if (!maxQueue.empty()) - { - outMax[index] = static_cast(values[maxQueue.front()]); - } - } - } } // namespace void ComputeMovingAverageIterated(const std::vector& values, int radius, int order, @@ -1162,8 +1043,54 @@ namespace jdi } } - void ComputeDiffAmplify(const std::vector& values, int window, int gain, - std::vector& out) + namespace + { + /** + * @brief 「弱波增强」内部使用的极值点(波峰 / 波谷)。 + */ + struct BoostExtremum + { + std::size_t index = 0; /**< 采样下标。 */ + double value = 0.0; /**< 采样值。 */ + int type = 0; /**< +1 = 波峰,-1 = 波谷。 */ + }; + + /** + * @brief 取用于“识别波峰/波谷”的轻度平滑值。 + * + * 直接对原始采样做方向判定时,采样噪声会被当成大量假波峰/波谷,使增强曲线抖动; + * 因此先看一下 5 点滑动平均(半径 2)之后的“形状”。这里按需就地计算, + * 不额外分配与采样数同量级的缓冲。 + * + * @param [IN] values 原始采样值。 + * @param [IN] index 采样下标。 + * @return 轻度平滑后的取值;邻域内没有有效值时返回 NaN。 + */ + double BoostShapeAt(const std::vector& values, std::size_t index) + { + const std::size_t count = values.size(); + const std::size_t low = (index > 2) ? (index - 2) : 0; + const std::size_t high = std::min(count - 1, index + 2); + double sum = 0.0; + int used = 0; + for (std::size_t i = low; i <= high; ++i) + { + const double value = values[i]; + if (std::isfinite(value)) + { + sum += value; + ++used; + } + } + if (used == 0) + { + return std::numeric_limits::quiet_NaN(); + } + return sum / static_cast(used); + } + } // namespace + + void ComputeWeakWaveBoost(const std::vector& values, double level, std::vector& out) { const std::size_t count = values.size(); out.assign(count, std::numeric_limits::quiet_NaN()); @@ -1172,192 +1099,155 @@ namespace jdi return; } - // 窗口点数 X:局部“差异强度”的统计**窗口长度**(点数),窗口 = [i−r, i+r], - // r = (X−1)/2。窗口长度**应略大于纹波周期的一半**: - // 只有当窗口跨越的相位达到 180° 时,窗口内才必定包含一个波峰或波谷, - // “局部 |交流分量| 的峰值”因而恒等于纹波振幅 A,与相位无关 - //(正常区域恒为 A、弱区恒为 A/2,比值恰为 2);窗口再小就会跟随相位起伏 - //(把正常区域的波峰也一起放大),远大于一个周期时又会把弱区与邻近正常区一起统计掉。 - // 窗口越短、混叠区(弱区边界各 ±r)越窄,对“只有一两个周期(甚至半周期)变弱”的 - // 情形越有效。 - const int span = std::max(3, std::min(window, 1000000)); - const int strengthRadius = std::max(1, (span - 1) / 2); - // 趋势窗口取局部窗口的 2 倍(≈ 一个完整纹波周期):只有按整周期取平均, - // 趋势线才恰好是纹波的中心线(交流分量均值为零),待放大的弱区也就**不会** - // 因为自己变“扁”而把趋势线一起拉偏。 - const int trendRadius = std::max(1, strengthRadius * 2); - // 包络窗口固定取局部窗口的 8 倍:用来给出“周边本来的振幅范围”, - // 既作为放大基准(中心值)也作为输出的夹取上下界。 - const std::size_t envelopeRadius = - static_cast(std::max(1, std::min(strengthRadius * 8, 16384))); - const double maxGain = static_cast(std::max(1, gain)); - - // 1) 周边包络:邻域内采样值的最小/最大值(单调队列,O(n))。 - // 邻域**必须明显大于一个纹波周期**,否则包络会跟着纹波起伏, - // 放大后会把相邻正常区域的相位也一并扭曲。 - std::vector envMin; - std::vector envMax; - SlidingExtrema(values, envelopeRadius, envMin, envMax); - - // 2) 局部差异强度 = 邻域内 |采样值 − 趋势线| 的**最大值**(即局部纹波振幅)。 - // - // 这里刻意**不用**另外两种看起来更自然的做法: - // * “|采样值 − 趋势线| 的平均”:趋势线是低频量,会被待放大的弱区本身拉偏, - // 于是弱区周边本来正常的采样点也会被判成“差异偏小”而被一起放大; - // * “原始采样值的峰峰值”:峰峰值要同时覆盖波峰与波谷才与相位无关, - // 这要求窗口跨越 360°,窗口会大到把弱区与邻近正常区平均掉 - // (实测一个周期宽的弱区只能恢复三分之一)。 - // |交流分量| 的最大值只需要窗口跨越 180°(窗口约半个周期)就与相位无关, - // 既能准确识别弱区,又因为窗口很短而具备足够好的定位精度。 - // - // 又因为增益下界恒为 1(见第 4 步),正常区域的增益恰好是 1, - // 输出与输入**逐位相同**,因此对其它数据点真正没有任何影响。 - // - // 极值窗口按**实际长度**(端点截断)统计,端点因此会低估振幅、 - // 导致增益被高估;这一端点效应由第 4 步的收敛系数消除。 - std::vector trend; - ComputeMovingAverage(values, trendRadius, trend); - std::vector strength; - { - // `ac` 只在求局部最大值时用到:单独放在一个作用域里, - // 亿级采样下这一份 100 MB 级的中转数组能尽早归还给系统。 - std::vector ac(count, std::numeric_limits::quiet_NaN()); - for (std::size_t index = 0; index < count; ++index) - { - const double value = values[index]; - if (std::isfinite(value) && std::isfinite(trend[index])) - { - ac[index] = static_cast(std::fabs(value - static_cast(trend[index]))); - } - } - SlidingMax(ac, static_cast(strengthRadius), strength); - } - - // 3) 目标强度:差异强度的 75 分位。用直方图统计 → O(1) 额外内存, - // 且不会被个别尖峰抬高(用均值会),也不会被最尖处完全主导(用最大值会)。 - // - // ⚠️ 统计时**排除首尾的端点收敛区**(见第 4 步):那里的趋势窗口被截断, - // “|交流分量| 的最大值”整体偏大,会把分位数抬高、导致目标强度偏大。 - // 待统计的采样点数不足时退回全段统计。 - const std::size_t edgeGuard = - static_cast(2) * static_cast(trendRadius); - // 剩下足够多的采样点时才有必要排除端点区(短信号就直接全段统计)。 - constexpr std::size_t kEdgeGuardMinimum = 256; - const bool useGuard = (count > edgeGuard * 2 + kEdgeGuardMinimum); - const auto inGuard = [count, edgeGuard, useGuard](std::size_t index) - { - if (!useGuard) - { - return true; - } - return (index >= edgeGuard) && ((count - 1 - index) >= edgeGuard); - }; - - float maxStrength = 0.0f; - for (std::size_t index = 0; index < count; ++index) - { - const float value = strength[index]; - if (inGuard(index) && std::isfinite(value) && value > maxStrength) - { - maxStrength = value; - } - } - if (!(maxStrength > 0.0f)) - { - // 完全平坦(或全为 NaN)的信号:无差异可放大,等价于原样输出。 - for (std::size_t index = 0; index < count; ++index) - { - const double value = values[index]; - if (std::isfinite(value)) - { - out[index] = static_cast(value); - } - } - return; - } - - constexpr int kBinCount = 2048; - std::vector histogram(static_cast(kBinCount), 0); - std::size_t sampleCount = 0; - for (std::size_t index = 0; index < count; ++index) - { - const float value = strength[index]; - if (!inGuard(index) || !std::isfinite(value)) - { - continue; - } - int bin = static_cast(static_cast(value) / - static_cast(maxStrength) * kBinCount); - bin = std::max(0, std::min(bin, kBinCount - 1)); - ++histogram[static_cast(bin)]; - ++sampleCount; - } - double target = static_cast(maxStrength); - if (sampleCount > 0) - { - const std::size_t wanted = (sampleCount * 75) / 100; - std::size_t accumulated = 0; - for (int bin = 0; bin < kBinCount; ++bin) - { - accumulated += static_cast(histogram[static_cast(bin)]); - if (accumulated > wanted) - { - // 取桶中心,避免目标强度贴到桶的下边界而整体偏小。 - target = (static_cast(bin) + 0.5) / kBinCount * - static_cast(maxStrength); - break; - } - } - } - - // 4) 逐点增益并合成输出。 - // * 基准 = 趋势线(纹波的中心线,与信号整体同步,纹波在其中上下摆动); - // * 增益下界为 1 → 局部振幅不比目标值小的采样点**输出与原值逐位相同** - // (基准被 1 倍增益完全消掉,因此对其它数据点没有任何影响); - // * 放大后的取值再夹到周边包络内 → **任何一点都不会超出原有的振幅范围** - // (尤其重要:被放大区域两侧自身的振幅本来就很大,不应被继续抬升)。 + // 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。 for (std::size_t index = 0; index < count; ++index) { const double value = values[index]; - if (!std::isfinite(value)) + if (std::isfinite(value)) + { + out[index] = static_cast(value); + } + } + + // 增强等级:波峰取左右两个波峰中较小者为 0%、较大者为 100%, + // 目标值 = 较小 + 等级 × (较大 − 较小)(波谷相反)。 + const double strength = std::max(0.0, std::min(1.0, level)); + + // 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。 + // 波峰(先升后降)与波谷(先降后升)必然交替出现,因此同类极值就是下标 ±2 的两个。 + std::vector extremes; + { + int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。 + bool hasPrevious = false; + std::size_t previousIndex = 0; + double previousShape = 0.0; + for (std::size_t index = 0; index < count; ++index) + { + if (!std::isfinite(values[index])) + { + // 无效采样作为断点:不跨越它判定极值。 + hasPrevious = false; + continue; + } + const double shape = BoostShapeAt(values, index); + if (!std::isfinite(shape)) + { + hasPrevious = false; + continue; + } + if (!hasPrevious) + { + hasPrevious = true; + previousIndex = index; + previousShape = shape; + continue; + } + const int step = (shape > previousShape) ? 1 : ((shape < previousShape) ? -1 : 0); + if (step != 0) + { + if (direction == 0) + { + direction = step; + } + else if (step != direction) + { + // 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。 + BoostExtremum extreme; + extreme.index = previousIndex; + extreme.value = values[previousIndex]; + extreme.type = direction; + extremes.push_back(extreme); + direction = step; + } + } + previousIndex = index; + previousShape = shape; + } + } + + if (extremes.size() < 3) + { + // 找不到足够的极值(例如常量或单调信号):无可增强之处。 + return; + } + + // 2) 逐个极值点判定“是否偏弱”,并算出它自己的目标偏移量。 + const std::size_t extremeCount = extremes.size(); + std::vector shift(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]; + if (left.type != current.type || right.type != current.type) { continue; } - const bool hasEnvelope = std::isfinite(envMin[index]) && std::isfinite(envMax[index]); - const double base = - std::isfinite(trend[index]) ? static_cast(trend[index]) : value; - - double pointGain = 1.0; - const float local = strength[index]; - if (std::isfinite(local) && local > 0.0f) + if (!std::isfinite(current.value) || !std::isfinite(left.value) || + !std::isfinite(right.value)) { - pointGain = target / static_cast(local); + continue; } - pointGain = std::max(1.0, std::min(pointGain, maxGain)); - - // 端点收敛:首尾各 2r 个采样点的**趋势窗口与极值窗口都被截断** - //(趋势窗口半径 2r、极值窗口半径 r),局部振幅与中心线都被系统性估偏。 - // 因此这些采样点一律保持原样(增益恰为 1),再在接下来的 2r 个点内 - // 平滑过渡到完整增益,避免记录首尾被整片抬升。 - // - // ⚠️ 不能靠“镜像延拓”解决:镜像后的窗口覆盖的真实下标集合与截断窗口相同 - //(例如 i=16、r=40 时两者都只覆盖真实下标 0..56),因此不会带来任何新信息。 - const double taperStart = static_cast(trendRadius); - const double taperSpan = std::max(1.0, static_cast(trendRadius)); - const double distanceToEdge = - static_cast(std::min(index, count - 1 - index)); - const double taper = - std::max(0.0, std::min(1.0, (distanceToEdge - taperStart) / taperSpan)); - pointGain = 1.0 + (pointGain - 1.0) * taper; - - double amplified = base + pointGain * (value - base); - if (hasEnvelope) + 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); + double destination = 0.0; + if (current.type > 0) { - amplified = std::max(static_cast(envMin[index]), - std::min(amplified, static_cast(envMax[index]))); + // 波峰:低于左右两个波峰的均值 → 判定为弱波,上拉到等级对应的目标值。 + if (!(current.value < mean)) + { + continue; + } + destination = lower + strength * (upper - lower); } - out[index] = static_cast(amplified); + else + { + // 波谷:高于左右两个波谷的均值 → 判定为弱波,下拉到等级对应的目标值。 + if (!(current.value > mean)) + { + continue; + } + destination = upper - strength * (upper - lower); + } + shift[k] = destination - current.value; } + + // 3) 把偏移量平滑地摊到极值点之间的数据点上: + // 相邻两个极值点之间用平滑步进过渡(两端导数为 0,波形不会出现折角), + // 数据首尾则从 0 渐变到最近的极值点偏移,保证端点原样不动。 + const auto applySpan = [&values, &out, count](std::size_t begin, std::size_t end, + double beginShift, double endShift) + { + if (end <= begin) + { + if (begin < count && std::isfinite(values[begin])) + { + out[begin] = static_cast(values[begin] + beginShift); + } + return; + } + const double span = static_cast(end - begin); + for (std::size_t index = begin; index <= end; ++index) + { + const double value = values[index]; + if (!std::isfinite(value)) + { + continue; + } + const double t = static_cast(index - begin) / span; + const double weight = t * t * (3.0 - 2.0 * t); + out[index] = static_cast(value + beginShift * (1.0 - weight) + + endShift * weight); + } + }; + + applySpan(0, extremes.front().index, 0.0, shift.front()); + for (std::size_t k = 0; k + 1 < extremeCount; ++k) + { + applySpan(extremes[k].index, extremes[k + 1].index, shift[k], shift[k + 1]); + } + applySpan(extremes.back().index, count - 1, shift.back(), 0.0); } const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const @@ -1430,9 +1320,9 @@ namespace jdi edgeCenterItems.end()); } - const Lane::DiffAmpItem* Lane::FindDiffAmpItem(const SignalRef& ref) const + const Lane::WeakBoostItem* Lane::FindWeakBoostItem(const SignalRef& ref) const { - for (const DiffAmpItem& item : diffAmpItems) + for (const WeakBoostItem& item : weakBoostItems) { if (item.ref == ref) { @@ -1442,32 +1332,27 @@ namespace jdi return nullptr; } - Lane::DiffAmpItem& Lane::EnsureDiffAmpItem(const SignalRef& ref) + Lane::WeakBoostItem& Lane::EnsureWeakBoostItem(const SignalRef& ref) { - for (DiffAmpItem& item : diffAmpItems) + for (WeakBoostItem& item : weakBoostItems) { if (item.ref == ref) { return item; } } - DiffAmpItem item; + WeakBoostItem item; item.ref = ref; - // 默认参数:窗口点数 X = 80(长度)、放大倍数 N = 2。 - // 窗口长度需略大于纹波周期的一半(见 ComputeDiffAmplify 的说明), - // 80 点可覆盖周期约 160 采样以内的纹波。 - item.points = 80; - item.order = 2; - diffAmpItems.push_back(item); - return diffAmpItems.back(); + weakBoostItems.push_back(item); + return weakBoostItems.back(); } - void Lane::RemoveDiffAmpItem(const SignalRef& ref) + void Lane::RemoveWeakBoostItem(const SignalRef& ref) { - diffAmpItems.erase( - std::remove_if(diffAmpItems.begin(), diffAmpItems.end(), - [&ref](const DiffAmpItem& item) { return item.ref == ref; }), - diffAmpItems.end()); + weakBoostItems.erase( + std::remove_if(weakBoostItems.begin(), weakBoostItems.end(), + [&ref](const WeakBoostItem& item) { return item.ref == ref; }), + weakBoostItems.end()); } int FindNearestSample(const DataFile& file, double time) @@ -1714,7 +1599,7 @@ namespace jdi }; purgeRefs(lane.meanFilterItems); purgeRefs(lane.edgeCenterItems); - purgeRefs(lane.diffAmpItems); + purgeRefs(lane.weakBoostItems); } // 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。 m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(), @@ -2006,18 +1891,19 @@ namespace jdi lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen; lane.edgeCenterPanelX = source.edgeCenterPanelX; lane.edgeCenterPanelY = source.edgeCenterPanelY; - lane.diffAmp = source.diffAmp; - lane.diffAmpPanelOpen = source.diffAmpPanelOpen; - lane.diffAmpPanelX = source.diffAmpPanelX; - lane.diffAmpPanelY = source.diffAmpPanelY; + lane.weakBoost = source.weakBoost; + lane.weakBoostLevel = source.weakBoostLevel; + lane.weakBoostPanelOpen = source.weakBoostPanelOpen; + lane.weakBoostPanelX = source.weakBoostPanelX; + lane.weakBoostPanelY = source.weakBoostPanelY; lane.meanFilter = source.meanFilter; lane.meanFilterPanelOpen = source.meanFilterPanelOpen; lane.meanFilterPanelX = source.meanFilterPanelX; lane.meanFilterPanelY = source.meanFilterPanelY; - // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 差异放大条目。 + // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱波增强条目。 lane.meanFilterItems.clear(); lane.edgeCenterItems.clear(); - lane.diffAmpItems.clear(); + lane.weakBoostItems.clear(); for (const Lane::MeanFilterItem& item : source.meanFilterItems) { if (item.ref == ref) @@ -2032,11 +1918,11 @@ namespace jdi lane.edgeCenterItems.push_back(item); } } - for (const Lane::DiffAmpItem& item : source.diffAmpItems) + for (const Lane::WeakBoostItem& item : source.weakBoostItems) { if (item.ref == ref) { - lane.diffAmpItems.push_back(item); + lane.weakBoostItems.push_back(item); } } lane.signals.push_back(ref); @@ -2093,7 +1979,7 @@ namespace jdi ++item.ref.signalIndex; } } - for (Lane::DiffAmpItem& item : lane.diffAmpItems) + for (Lane::WeakBoostItem& item : lane.weakBoostItems) { if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos) {