diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index 78fe772..78f7b7c 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -1480,8 +1480,13 @@ namespace jdi if (request.weakBoost) { ComputeWeakWaveBoost(source.values, static_cast(request.levelPercent) / 100.0, - derived_values); - derived.name = baseName + L" (弱波增强 " + std::to_wstring(request.levelPercent) + L"%)"; + request.order, derived_values); + derived.name = baseName + L" (弱波增强 " + std::to_wstring(request.levelPercent) + L"%"; + if (request.order > 1) + { + derived.name += L"×" + std::to_wstring(request.order) + L"阶"; + } + derived.name += L")"; } else { @@ -1563,6 +1568,7 @@ namespace jdi request.fileIndex = item.fileIndex; request.signalIndex = item.signalIndex; request.levelPercent = item.levelPercent; + request.order = item.order; request.weakBoost = true; requests.push_back(request); } @@ -1605,7 +1611,8 @@ namespace jdi std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 "; if (last.weakBoost) { - text += L"弱波增强 " + std::to_wstring(last.levelPercent) + L"% 信号。"; + text += L"弱波增强 " + std::to_wstring(last.levelPercent) + L"%" + + std::to_wstring(last.order) + L"阶信号。"; } else { diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index a77acb1..97d214e 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -31,6 +31,7 @@ namespace jdi EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */ EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */ BOOST_LEVEL, /**< 拖动「弱波增强」面板中的等级滑块。 */ + BOOST_ORDER, /**< 拖动「弱波增强」面板中的阶数 N 滑块。 */ LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */ }; @@ -530,6 +531,7 @@ namespace jdi int fileIndex = -1; /**< 信号所属文件下标。 */ int signalIndex = -1; /**< 文件内信号下标。 */ int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ + int order = 1; /**< 阶数 N(1–10)。 */ }; /** @@ -569,6 +571,21 @@ namespace jdi */ double LaneWeakBoostLevel(int laneIndex) const; + /** + * @brief 读取某网格的弱波增强阶数 N(1–10)。 + * @param [IN] laneIndex 网格下标。 + * @return 阶数;网格不存在时返回 1。 + */ + int LaneWeakBoostOrder(int laneIndex) const; + + /** + * @brief 设置某网格的弱波增强阶数 N(1–10,超出范围会被夹取)。 + * @param [IN] laneIndex 网格下标。 + * @param [IN] order 阶数。 + * @return 阶数发生变化返回 true。 + */ + bool SetLaneWeakBoostOrder(int laneIndex, int order); + /** * @brief 设置某网格的弱波增强等级(0–1,超出范围会被夹取)。 * @param [IN] laneIndex 网格下标。 @@ -578,23 +595,26 @@ namespace jdi bool SetLaneWeakBoostLevel(int laneIndex, double level); /** - * @brief 获取弱波增强曲线(按「信号 + 增强等级」缓存)。 + * @brief 获取弱波增强曲线(按「信号 + 增强等级 + 阶数」缓存)。 * @param [IN] signal 目标信号。 * @param [IN] levelPercent 弱波增强等级(0–100,整数百分比)。 + * @param [IN] order 阶数 N(1–10)。 * @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。 */ - const std::vector& WeakBoostCurveFor(const SignalData& signal, int levelPercent) const; + const std::vector& WeakBoostCurveFor(const SignalData& signal, int levelPercent, + int order) const; /** * @brief 取某个时刻的弱波增强曲线取值(用于纵向光标的取值标注)。 * @param [IN] file 信号所属文件。 * @param [IN] signal 目标信号。 * @param [IN] levelPercent 弱波增强等级(0–100)。 + * @param [IN] order 阶数 N(1–10)。 * @param [IN] time 采样时刻。 * @param [OUT] value 输出取值。 * @return 取到有效值返回 true。 */ - bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, + bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int order, double time, double& value) const; /** @@ -1064,6 +1084,7 @@ namespace jdi gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */ gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */ gd::RectF slider; /**< 「弱波增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */ + gd::RectF sliderOrder; /**< 「弱波增强」阶数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */ int rows = 0; /**< 统计行数(每条可见信号一行)。 */ /** * @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。 @@ -1193,9 +1214,10 @@ namespace jdi * @param [IN] resolved 信号视图。 * @param [IN] lane 网格绘制信息。 * @param [IN] levelPercent 弱波增强等级(0–100)。 + * @param [IN] order 阶数 N(1–10)。 */ void PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, - int levelPercent) const; + int levelPercent, int order) const; /** * @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。 @@ -1414,7 +1436,8 @@ namespace jdi int m_pendingWeakBoostLane = -1; /**< 待构建的弱波增强信号所属网格下标(-1 表示无请求)。 */ std::vector m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */ std::vector m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */ - int m_boostDragLane = -1; /**< 正在拖动等级滑块的面板所属网格。 */ + int m_boostDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */ + bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */ /** * @brief 实验性功能浮动面板的 ✕ 处于"已按下、等待抬起确认"状态。 @@ -1470,11 +1493,12 @@ namespace jdi }; mutable std::deque m_meanCurveCache; - /** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比」标识)。 */ + /** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比 + 阶数」标识)。 */ struct WeakBoostCurveEntry { const SignalData* signal = nullptr; int levelPercent = 50; /**< 弱波增强等级(0–100)。 */ + int order = 1; /**< 阶数 N(1–10)。 */ std::vector values; /**< 与 signal->values 等长;缺失处为 NaN。 */ }; mutable std::deque m_weakBoostCurveCache; diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 7a88eab..a5ac082 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -70,10 +70,13 @@ 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 float kWeakBoostSliderRowHeight = 26.0f; /**< 弱波增强面板中每个滑块行的高度。 */ + const float kWeakBoostTrackHeight = 6.0f; /**< 弱波增强滑块的轨道高度。 */ + const float kWeakBoostTrackMinWidth = 120.0f; /**< 弱波增强滑块轨道的最小宽度。 */ + const int kWeakBoostOrderMin = 1; /**< 阶数 N 的下限。 */ + const int kWeakBoostOrderMax = 10; /**< 阶数 N 的上限。 */ const wchar_t* const kWeakBoostLevelLabel = L"弱波增强等级"; /**< 等级滑块行左侧的说明文字。 */ + const wchar_t* const kWeakBoostOrderLabel = L"阶数 N"; /**< 阶数滑块行左侧的说明文字。 */ const wchar_t* const kWeakBoostTitle = L"弱波增强"; /**< 弱波增强面板标题。 */ const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱波增强共用)。 */ const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */ @@ -1752,6 +1755,34 @@ namespace jdi return m_tab->Lanes()[static_cast(laneIndex)].weakBoostLevel; } + int PlotView::LaneWeakBoostOrder(int laneIndex) const + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return kWeakBoostOrderMin; + } + return m_tab->Lanes()[static_cast(laneIndex)].weakBoostOrder; + } + + bool PlotView::SetLaneWeakBoostOrder(int laneIndex, int order) + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return false; + } + Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + const int clamped = std::max(kWeakBoostOrderMin, std::min(kWeakBoostOrderMax, order)); + if (lane.weakBoostOrder == clamped) + { + return false; + } + lane.weakBoostOrder = clamped; + // 阶数变化会改变预览曲线:清掉曲线缓存并让场景缓存失效。 + m_weakBoostCurveCache.clear(); + InvalidateSceneCache(); + return true; + } + bool PlotView::SetLaneWeakBoostLevel(int laneIndex, double level) { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) @@ -1981,12 +2012,13 @@ namespace jdi } const std::vector& PlotView::WeakBoostCurveFor(const SignalData& signal, - int levelPercent) const + int levelPercent, int order) const { const int level = std::max(0, std::min(100, levelPercent)); + const int stages = std::max(kWeakBoostOrderMin, std::min(kWeakBoostOrderMax, order)); for (const WeakBoostCurveEntry& entry : m_weakBoostCurveCache) { - if (entry.signal == &signal && entry.levelPercent == level) + if (entry.signal == &signal && entry.levelPercent == level && entry.order == stages) { return entry.values; } @@ -1995,23 +2027,24 @@ namespace jdi WeakBoostCurveEntry entry; entry.signal = &signal; entry.levelPercent = level; + entry.order = stages; // 与「构建为新信号」共用同一实现(Utilities 内的自由函数), // 因此预览曲线与构建出的采样值逐位一致。 - ComputeWeakWaveBoost(signal.values, static_cast(level) / 100.0, entry.values); + ComputeWeakWaveBoost(signal.values, static_cast(level) / 100.0, stages, entry.values); m_weakBoostCurveCache.push_back(std::move(entry)); return m_weakBoostCurveCache.back().values; } bool PlotView::WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, - double time, double& value) const + int order, double time, double& value) const { const int index = FindNearestSample(file, time); if (index < 0) { return false; } - const std::vector& curve = WeakBoostCurveFor(signal, levelPercent); + const std::vector& curve = WeakBoostCurveFor(signal, levelPercent, order); if (static_cast(index) >= curve.size()) { return false; @@ -2338,9 +2371,11 @@ namespace jdi if (weakBoost) { - // 等级滑块行 = 「弱波增强等级」说明文字 + 轨道 + 百分比数值。 - const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel); - const float valueWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"100%"); + // 两个滑块行(等级 / 阶数 N)= 「说明文字 + 轨道 + 数值」。 + const float labelWidth = std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel), + UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostOrderLabel)); + 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 + 6.0f * m_scale + valueWidth; width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth); @@ -3277,6 +3312,9 @@ namespace jdi case PlotDragMode::BOOST_LEVEL: // 「弱波增强」等级滑块拖拽 → 左右箭头。 return LoadCursorW(nullptr, IDC_SIZEWE); + case PlotDragMode::BOOST_ORDER: + // 「弱波增强」阶数 N 滑块拖拽 → 左右箭头。 + return LoadCursorW(nullptr, IDC_SIZEWE); case PlotDragMode::MOVE_SIGNAL: // 拖动信号(参考 Photoshop 平移元素的抓握光标)。 return LoadCursorW(nullptr, IDC_SIZEALL); @@ -3315,18 +3353,20 @@ namespace jdi } if (UiPointInRect(layout.close, static_cast(x), static_cast(y))) { - // ✕ 按钮:保持原始箭头(仅做悬停高亮提示)。 - return nullptr; + // ✕ 按钮:与面板/标签栏的其它 ✕ 一致,用手型光标作为可点击提示。 + return LoadCursorW(nullptr, IDC_HAND); } if (UiPointInRect(layout.title, static_cast(x), static_cast(y))) { return LoadCursorW(nullptr, IDC_SIZEALL); } if (kind == EdgePanelKind::BOOST && - WeakBoostSliderHit(layout.slider, m_scale, static_cast(x), - static_cast(y))) + (WeakBoostSliderHit(layout.slider, m_scale, static_cast(x), + static_cast(y)) || + WeakBoostSliderHit(layout.sliderOrder, m_scale, static_cast(x), + static_cast(y)))) { - // 等级滑块:左右箭头表示可左右拖动。 + // 等级 / 阶数滑块:左右箭头表示可左右拖动。 return LoadCursorW(nullptr, IDC_SIZEWE); } if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::BOOST) && @@ -3459,20 +3499,34 @@ namespace jdi if (m_rubberAxis == PlotRubberAxis::TIME) { // 竖向选区贯穿整个绘图区高度,只取横向区间用于时间轴缩放。 - const float left = static_cast(std::min(x, m_dragStartX)); - const float width = static_cast(std::abs(x - m_dragStartX)); + // **横向必须夹在绘图区内**:鼠标被捕获时可能移动到工具栏 / 信号面板等处, + // 若不夹取,选区会画到绘图区之外(那些区域只会被局部失效,于是残留成"绘制错误")。 + const float plotLeft = m_plotRect.X; + const float plotRight = m_plotRect.X + m_plotRect.Width; + const float from = ClampDouble(static_cast(m_dragStartX), + static_cast(plotLeft), + static_cast(plotRight)); + const float to = ClampDouble(static_cast(x), static_cast(plotLeft), + static_cast(plotRight)); + const float left = std::min(from, to); + const float width = std::fabs(to - from); m_rubberRect = gd::RectF(left, m_plotRect.Y, width, m_plotRect.Height); } else { - // 横向选区限定在按下时的网格内,只取纵向区间用于幅值轴缩放。 + // 横向选区限定在按下时的网格内(纵向同样夹在该网格内),只取纵向区间用于幅值轴缩放。 if (m_dragLane < 0 || m_dragLane >= static_cast(m_lanes.size())) { return false; } const gd::RectF& laneRect = m_lanes[static_cast(m_dragLane)].rect; - const float top = static_cast(std::min(y, m_dragStartY)); - const float bottom = static_cast(std::max(y, m_dragStartY)); + const float from = ClampDouble(static_cast(m_dragStartY), + static_cast(laneRect.Y), + static_cast(laneRect.Y + laneRect.Height)); + const float to = ClampDouble(static_cast(y), static_cast(laneRect.Y), + static_cast(laneRect.Y + laneRect.Height)); + const float top = std::min(from, to); + const float bottom = std::max(from, to); m_rubberRect = gd::RectF(laneRect.X, top, laneRect.Width, bottom - top); } m_dragMoved = true; @@ -3582,6 +3636,26 @@ namespace jdi m_dragMoved = true; return true; } + case PlotDragMode::BOOST_ORDER: + { + // 「弱波增强」阶数 N 滑块:按滑轨上的横向位置换算 1–10(整数)。 + if (m_tab == nullptr || m_boostDragLane < 0 || + m_boostDragLane >= static_cast(m_lanes.size())) + { + break; + } + const EdgePanelLayout layout = EdgePanelLayoutFor(m_boostDragLane, EdgePanelKind::BOOST); + if (layout.sliderOrder.Width <= 0.0f) + { + break; + } + const double ratio = UiSliderRatioFromX(layout.sliderOrder, fx); + const int order = kWeakBoostOrderMin + static_cast(std::lround( + ratio * (kWeakBoostOrderMax - kWeakBoostOrderMin))); + SetLaneWeakBoostOrder(m_boostDragLane, order); + m_dragMoved = true; + return true; + } case PlotDragMode::MOVE_SIGNAL: { const int target = LaneAtY(y); @@ -3707,6 +3781,16 @@ namespace jdi changed = true; } + // ✕ 关闭按钮 / 等级与阶数滑块 / 底部按钮的悬停提示都直接依赖鼠标坐标, + // 但它们在面板内部移动时上面这些状态可能毫无变化(例如从同一网格的波形移到面板上), + // 于是返回值恒为 false → 宿主不重绘 → **悬停提示不会出现**。 + // 因此在“鼠标进入 / 离开 / 在面板内移动”时强制标记需要重绘。 + if ((overEdgePanel || m_mouseOverPanel) && mouseMoved) + { + changed = true; + } + m_mouseOverPanel = overEdgePanel; + // 时间光标不再跟随鼠标移动,仅在鼠标覆盖光标时才显示具体时间读数。 int hoverWhich = 0; const bool hover = Tab().CursorEnabled() && IsOnCursorLine(x, hoverWhich); @@ -3883,11 +3967,15 @@ namespace jdi if (UiPointInRect(layout.close, fx, fy)) { - // ✕:**抬起**时才关闭该网格对应的实验性功能,这里只记录目标; - // 按住后移开再松开视为放弃(与面板/标签栏的关闭 ✕ 语义一致)。 - m_pendingPanelClose = true; - m_pendingPanelCloseLane = index; - m_pendingPanelCloseKind = kind; + // ✕:**抬起**时才关闭该网格对应的实验性功能,这里只记录目标; + // 按住后移开再松开视为放弃(与面板/标签栏的关闭 ✕ 语义一致)。 + // ⚠️ 必须在此**直接消费**该次按下:✕ 位于标题栏矩形内部, + // 若继续往下走会被“标题栏 → 拖动面板”分支命中,表现为“按下 ✕ 却拖走了面板”。 + m_pendingPanelClose = true; + m_pendingPanelCloseLane = index; + m_pendingPanelCloseKind = kind; + m_dragMode = PlotDragMode::NONE; + return true; } // 「中心轴边沿计数」/「均值滤波」/「弱波增强」面板:逐信号条目 @@ -3900,7 +3988,7 @@ namespace jdi 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; @@ -3908,6 +3996,17 @@ namespace jdi SetLaneWeakBoostLevel(index, UiSliderRatioFromX(layout.slider, fx)); return true; } + if (boostMode && WeakBoostSliderHit(layout.sliderOrder, m_scale, fx, fy)) + { + m_dragMode = PlotDragMode::BOOST_ORDER; + m_boostDragLane = index; + const double ratio = UiSliderRatioFromX(layout.sliderOrder, fx); + SetLaneWeakBoostOrder(index, + kWeakBoostOrderMin + + static_cast(std::lround( + ratio * (kWeakBoostOrderMax - kWeakBoostOrderMin)))); + return true; + } for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex) { @@ -3970,10 +4069,12 @@ namespace jdi if (boostMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy)) { // 「弱波增强 → 构建为新信号」:与均值滤波同一套请求机制 - //(等级属于整个网格,网格内各被勾选信号共用同一个值)。 + //(等级与阶数属于整个网格,网格内各被勾选信号共用同一组参数)。 const Lane& lane = m_tab->Lanes()[static_cast(index)]; const int levelPercent = static_cast( std::lround(std::max(0.0, std::min(1.0, lane.weakBoostLevel)) * 100.0)); + const int order = std::max(kWeakBoostOrderMin, + std::min(kWeakBoostOrderMax, lane.weakBoostOrder)); m_pendingWeakBoostItems.clear(); for (const SignalRef& ref : lane.signals) { @@ -3990,13 +4091,14 @@ namespace jdi build.fileIndex = ref.fileIndex; build.signalIndex = ref.signalIndex; build.levelPercent = levelPercent; + build.order = order; m_pendingWeakBoostItems.push_back(build); } if (!m_pendingWeakBoostItems.empty()) { SetHint(L"正在构建弱波增强信号(等级 " + std::to_wstring(levelPercent) + - L"%,共 " + std::to_wstring(m_pendingWeakBoostItems.size()) + - L" 条)…"); + L"% / " + std::to_wstring(order) + L" 阶,共 " + + std::to_wstring(m_pendingWeakBoostItems.size()) + L" 条)…"); m_pendingWeakBoostLane = index; } m_dragMode = PlotDragMode::NONE; @@ -5156,9 +5258,10 @@ namespace jdi mix(static_cast(item.points)); mix(static_cast(item.order)); } - // 「弱波增强」的开关、增强等级与每个被勾选信号的条目同理。 + // 「弱波增强」的开关、增强等级、阶数 N 与每个被勾选信号的条目同理。 mix(Tab().Lanes()[laneIndex].weakBoost ? 1ull : 0ull); mixFloat(static_cast(Tab().Lanes()[laneIndex].weakBoostLevel)); + mix(static_cast(Tab().Lanes()[laneIndex].weakBoostOrder)); mix(static_cast(Tab().Lanes()[laneIndex].weakBoostItems.size())); for (const Lane::WeakBoostItem& item : Tab().Lanes()[laneIndex].weakBoostItems) { @@ -5756,10 +5859,24 @@ namespace jdi return; } + // 选区只应出现在绘图区内:即使内部矩形意外越界(例如鼠标被捕获到工具栏 / + // 信号面板上),也在这里再夹一次,绝不把选区块画到绘图区之外。 + const float clipLeft = std::max(m_rubberRect.X, m_plotRect.X); + const float clipTop = std::max(m_rubberRect.Y, m_plotRect.Y); + const float clipRight = + std::min(m_rubberRect.X + m_rubberRect.Width, m_plotRect.X + m_plotRect.Width); + const float clipBottom = + std::min(m_rubberRect.Y + m_rubberRect.Height, m_plotRect.Y + m_plotRect.Height); + if (clipRight <= clipLeft || clipBottom <= clipTop) + { + return; + } + const gd::RectF bandRect(clipLeft, clipTop, clipRight - clipLeft, clipBottom - clipTop); + // 两种方向的选区使用**同一种强调色**,保持视觉一致。 const std::uint32_t band = m_theme->accent; - UiFillRect(g, m_rubberRect, WithAlpha(band, 0x28)); - UiDrawRoundRect(g, m_rubberRect, 2.0f * m_scale, band, 1.4f); + UiFillRect(g, bandRect, WithAlpha(band, 0x28)); + UiDrawRoundRect(g, bandRect, 2.0f * m_scale, band, 1.4f); // 选区跨度提示(时间用秒/数值用幅度量纲),放在选区顶部。 std::wstring text; @@ -5781,9 +5898,9 @@ namespace jdi const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 12.0f * m_scale; const float textHeight = 18.0f * m_scale; - float labelX = m_rubberRect.X + m_rubberRect.Width * 0.5f - textWidth * 0.5f; + float labelX = bandRect.X + bandRect.Width * 0.5f - textWidth * 0.5f; labelX = std::max(m_plotRect.X, std::min(labelX, m_plotRect.X + m_plotRect.Width - textWidth)); - const gd::RectF labelRect(labelX, m_rubberRect.Y + 3.0f * m_scale, textWidth, textHeight); + const gd::RectF labelRect(labelX, bandRect.Y + 3.0f * m_scale, textWidth, textHeight); UiFillRoundRect(g, labelRect, 3.0f * m_scale, band); UiText(g, text, m_fonts->monoSmall, labelRect, m_theme->textOnAccent, TextAlign::CENTER); @@ -6129,7 +6246,11 @@ namespace jdi Tab().Lanes()[static_cast(laneIndex)] .weakBoostLevel)) * 100.0)); - PaintWeakBoostCurve(g, resolved, lane, levelPercent); + const int order = std::max( + kWeakBoostOrderMin, + std::min(kWeakBoostOrderMax, + Tab().Lanes()[static_cast(laneIndex)].weakBoostOrder)); + PaintWeakBoostCurve(g, resolved, lane, levelPercent, order); } } } @@ -6960,7 +7081,10 @@ namespace jdi const int levelPercent = static_cast(std::lround( std::max(0.0, std::min(1.0, m_tab->Lanes()[laneIndex].weakBoostLevel)) * 100.0)); - paintDerivedCapsule(WeakBoostCurveFor(*resolved.data, levelPercent), + const int order = std::max( + kWeakBoostOrderMin, + std::min(kWeakBoostOrderMax, m_tab->Lanes()[laneIndex].weakBoostOrder)); + paintDerivedCapsule(WeakBoostCurveFor(*resolved.data, levelPercent, order), L"弱波增强"); } } @@ -7082,8 +7206,9 @@ namespace jdi // 因此不再有独立的底部编辑行;只有「边沿计数」保留两行文本式排版。 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; // 逐条目面板:每个条目 = 第一排(信号名 + 复选框) // + 第二排(均值点数 X / 阶数 N)+(中心轴)第三排(统计结果); @@ -7116,7 +7241,7 @@ namespace jdi } // 底部按钮行:上方与条目之间、下方与面板下边界之间各留一个 `pad`, // 与面板的左右内边距完全一致(原先按钮直接贴着下边框,看起来很不协调)。 - height = titleHeight + pad + itemsHeight + sliderRowHeight + pad + buttonHeight + + height = titleHeight + pad + itemsHeight + sliderRowsHeight + pad + buttonHeight + (hasButton ? pad : 0.0f); } else @@ -7162,18 +7287,25 @@ namespace jdi std::max(0.0f, width - pad * 2.0f), buttonRowHeight); } - // 弱波增强等级滑块行(位于信号条目与底部按钮之间)。 + // 弱波增强的两个滑块行(等级 / 阶数 N,位于信号条目与底部按钮之间)。 if (weakBoost) { - const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel); - const float valueWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"100%"); + const float labelWidth = + std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostLevelLabel), + UiMeasureTextWidth(m_fonts->uiSmall, kWeakBoostOrderLabel)); + 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 rowTop = top + titleHeight + pad + itemsHeight; + 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; + const float trackWidth = std::max(10.0f * m_scale, trackRight - trackLeft); layout.slider = - gd::RectF(trackLeft, rowTop + (sliderRowHeight - trackHeight) * 0.5f, - std::max(10.0f * m_scale, trackRight - trackLeft), trackHeight); + gd::RectF(trackLeft, firstRowTop + (sliderRowHeight - trackHeight) * 0.5f, + trackWidth, trackHeight); + layout.sliderOrder = + gd::RectF(trackLeft, firstRowTop + sliderRowHeight + (sliderRowHeight - trackHeight) * 0.5f, + trackWidth, trackHeight); } // 逐信号条目:第一排 = 名称 + 复选框(复选框靠右); @@ -7568,14 +7700,14 @@ namespace jdi } void PlotView::PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, - const LaneVisual& lane, int levelPercent) const + const LaneVisual& lane, int levelPercent, int order) const { if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr) { return; } // 派生曲线一律使用同一段绘制逻辑与配色规则(弱波增强曲线同样如此)。 - PaintDerivedCurve(g, resolved, lane, WeakBoostCurveFor(*resolved.data, levelPercent)); + PaintDerivedCurve(g, resolved, lane, WeakBoostCurveFor(*resolved.data, levelPercent, order)); } void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector& lanes) @@ -7858,33 +7990,55 @@ namespace jdi } } - // 「弱波增强」等级滑块:左侧说明文字 + 轨道 + 右侧百分比数值。 + // 「弱波增强」的两个滑块(等级 0–100% / 阶数 N 1–10): + // 左侧说明文字 + 轨道 + 右侧数值。 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 float rowHeight = + std::max(kWeakBoostSliderRowHeight * m_scale, layout.slider.Height); + const auto paintSliderRow = [&](const gd::RectF& track, const wchar_t* label, + double ratio, const std::wstring& valueText, + bool active) + { + if (track.Width <= 0.0f) + { + return; + } + const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, label); + UiText(g, label, 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); + // 与样式区/拾色器共用同一套滑块绘制(轨道 + 已填充部分 + 圆钮)。 + UiDrawSlider(g, track, ratio, 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); + const float valueWidth = UiMeasureTextWidth(m_fonts->monoSmall, valueText); + UiText(g, valueText, 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); + }; + + 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, + std::to_wstring(static_cast(std::lround(level * 100.0))) + + L"%", + levelActive); + + const int order = std::max(kWeakBoostOrderMin, + std::min(kWeakBoostOrderMax, tabLane.weakBoostOrder)); + const double orderRatio = static_cast(order - kWeakBoostOrderMin) / + static_cast(kWeakBoostOrderMax - + kWeakBoostOrderMin); + const bool orderActive = (m_dragMode == PlotDragMode::BOOST_ORDER) && + (m_boostDragLane == static_cast(index)); + paintSliderRow(layout.sliderOrder, kWeakBoostOrderLabel, orderRatio, + std::to_wstring(order), orderActive); } } diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 01bf858..cc7f943 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -513,6 +513,15 @@ namespace jdi /** 弱波增强等级(0–1),0% = 目标取左右极值中较小/较大的那一个,100% = 取另一个。 */ double weakBoostLevel = 0.5; + /** + * @brief 弱波增强的阶数 N(1–10)。 + * + * 与「均值滤波」的阶数 N 一样表示“把同一处理重复应用 N 次”(每一阶以上一阶为输入): + * 一阶时改定量就已严格等于「等级」对应的目标值;多阶会把相邻弱波互相抬高 + * 彼此的基准,从而把较宽的弱波区逐步恢复。 + */ + int weakBoostOrder = 1; + /** 参与弱波增强预览的信号条目(只包含被勾选的信号)。 */ std::vector weakBoostItems; @@ -585,8 +594,11 @@ namespace jdi /** * @brief 「弱波增强」:把比周边更弱的纹波波峰/波谷恢复(自适应弱波增强)。 * - * 思路(全部 O(n)): - * 1. **识别趋势与极值**:对轻度平滑后的信号扫描单调段,方向发生翻转处即为 + * 本算法**不做任何前置滤波**:需要平滑时用户可以先用「均值滤波」构建一条 + * 滤波后的信号,再对它做弱波增强。极值点直接在输入采样上识别。 + * + * 单阶处理(全 O(n)): + * 1. **识别趋势与极值**:扫描单调段,方向发生翻转处的上一个采样点即为 * 波峰(先升后降)或波谷(先降后升);波峰与波谷交替出现。 * 2. **判定“弱”**:对第 k 个波峰,取同类的左右相邻波峰(k−2、k+2)。 * 若本波峰低于两者的均值,说明它是被削弱的那个;波谷反之(高于两者均值)。 @@ -598,14 +610,14 @@ namespace jdi * 4. **平滑偏移**:极值点本身取其目标值,两侧的数据点按相邻两个极值点的偏移量 * 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱波附近被推回。 * - * 与“全局缩放/局部放大”不同,这里**只改动被判为弱波的极值及其两侧**, * 正常周期的极值不满足条件、偏移量恒为 0,因此输出与输入逐位相同。 * * @param [IN] values 原始采样值。 * @param [IN] level 弱波增强等级(0–1,超出范围会被夹取)。 + * @param [IN] order 阶数 N(重复应用次数,至少为 1;与均值滤波的阶数同一语义)。 * @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。 */ - void ComputeWeakWaveBoost(const std::vector& values, double level, + void ComputeWeakWaveBoost(const std::vector& values, double level, int order, std::vector& out); /** diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index e4bf3cc..6e8cbd1 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -137,6 +137,7 @@ namespace jdi bool weakBoost = false; std::vector weakBoostItems; double weakBoostLevel = 0.5; + int weakBoostOrder = 1; bool weakBoostPanelOpen = true; double weakBoostPanelX = -1.0; double weakBoostPanelY = -1.0; @@ -168,6 +169,7 @@ namespace jdi carry.weakBoost = lane.weakBoost; carry.weakBoostItems = lane.weakBoostItems; carry.weakBoostLevel = lane.weakBoostLevel; + carry.weakBoostOrder = lane.weakBoostOrder; carry.weakBoostPanelOpen = lane.weakBoostPanelOpen; carry.weakBoostPanelX = lane.weakBoostPanelX; carry.weakBoostPanelY = lane.weakBoostPanelY; @@ -210,6 +212,7 @@ namespace jdi lane.weakBoost = carry.weakBoost; lane.weakBoostItems = carry.weakBoostItems; lane.weakBoostLevel = carry.weakBoostLevel; + lane.weakBoostOrder = carry.weakBoostOrder; lane.weakBoostPanelOpen = carry.weakBoostPanelOpen; lane.weakBoostPanelX = carry.weakBoostPanelX; lane.weakBoostPanelY = carry.weakBoostPanelY; @@ -1056,41 +1059,179 @@ namespace jdi }; /** - * @brief 取用于“识别波峰/波谷”的轻度平滑值。 + * @brief 「弱波增强」的单阶处理:把偏弱的波峰上拉、偏弱的波谷下拉。 * - * 直接对原始采样做方向判定时,采样噪声会被当成大量假波峰/波谷,使增强曲线抖动; - * 因此先看一下 5 点滑动平均(半径 2)之后的“形状”。这里按需就地计算, - * 不额外分配与采样数同量级的缓冲。 + * **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号): + * 极值点直接在输入采样上通过单调段的方向翻转识别。 * - * @param [IN] values 原始采样值。 - * @param [IN] index 采样下标。 - * @return 轻度平滑后的取值;邻域内没有有效值时返回 NaN。 + * @param [IN] input 本阶的输入采样值。 + * @param [IN] strength 弱波增强等级(0–1,已夹取)。 + * @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。 + * @return 确实有采样点被改动返回 true;完全无弱波时返回 false。 */ - double BoostShapeAt(const std::vector& values, std::size_t index) + bool ApplyWeakBoostStage(const std::vector& input, double strength, + std::vector& output) { - 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 std::size_t count = input.size(); + output.assign(count, std::numeric_limits::quiet_NaN()); + if (count == 0) { - const double value = values[i]; + return false; + } + + // 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。 + for (std::size_t index = 0; index < count; ++index) + { + const float value = input[index]; if (std::isfinite(value)) { - sum += value; - ++used; + output[index] = value; } } - if (used == 0) + + // 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。 + // 波峰(先升后降)与波谷(先降后升)必然交替出现, + // 因此同类极值就是极值序列里下标 ±2 的两个。 + std::vector extremes; { - return std::numeric_limits::quiet_NaN(); + int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。 + bool hasPrevious = false; + std::size_t previousIndex = 0; + double previousValue = 0.0; + for (std::size_t index = 0; index < count; ++index) + { + const double value = static_cast(input[index]); + if (!std::isfinite(value)) + { + // 无效采样作为断点:不跨越它判定极值。 + hasPrevious = false; + continue; + } + if (!hasPrevious) + { + hasPrevious = true; + previousIndex = index; + previousValue = value; + continue; + } + const int step = (value > previousValue) ? 1 : ((value < previousValue) ? -1 : 0); + if (step != 0) + { + if (direction == 0) + { + direction = step; + } + else if (step != direction) + { + // 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。 + BoostExtremum extreme; + extreme.index = previousIndex; + extreme.value = previousValue; + extreme.type = direction; + extremes.push_back(extreme); + direction = step; + } + } + previousIndex = index; + previousValue = value; + } } - return sum / static_cast(used); + + // 识别不到足够多的极值(常量 / 单调 / 采样点太少):本阶无需任何改动。 + if (extremes.size() < 5) + { + return false; + } + + // 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; + } + if (!std::isfinite(current.value) || !std::isfinite(left.value) || + !std::isfinite(right.value)) + { + continue; + } + 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) + { + // 波峰:低于左右两个波峰的均值 → 判定为弱波,上拉到等级对应的目标值。 + if (!(current.value < mean)) + { + continue; + } + destination = lower + strength * (upper - lower); + } + else + { + // 波谷:高于左右两个波谷的均值 → 判定为弱波,下拉到等级对应的目标值。 + if (!(current.value > mean)) + { + continue; + } + destination = upper - strength * (upper - lower); + } + shift[k] = destination - current.value; + } + + // 3) 把偏移量平滑地摊到极值点之间的数据点上: + // 相邻两个极值点之间用平滑步进过渡(两端导数为 0,波形不会出现折角), + // 数据首尾则从 0 渐变到最近的极值点偏移。 + // 注意:极值序列的首尾各两个元素的偏移量恒为 0(缺少同类的左右邻居), + // 因此这里只需要填充 shift 非零的区间。 + bool changed = false; + const auto applySpan = [&input, &output, count](std::size_t begin, std::size_t end, + double beginShift, double endShift) + { + if (end <= begin) + { + if (begin < count && std::isfinite(input[begin])) + { + output[begin] = static_cast(input[begin] + beginShift); + } + return; + } + const double span = static_cast(end - begin); + for (std::size_t index = begin; index <= end; ++index) + { + const float value = input[index]; + if (!std::isfinite(value)) + { + continue; + } + const double t = static_cast(index - begin) / span; + const double weight = t * t * (3.0 - 2.0 * t); + output[index] = static_cast(value + beginShift * (1.0 - weight) + + endShift * weight); + } + }; + + for (std::size_t k = 0; k + 1 < extremeCount; ++k) + { + if (shift[k] == 0.0 && shift[k + 1] == 0.0) + { + continue; + } + applySpan(extremes[k].index, extremes[k + 1].index, shift[k], shift[k + 1]); + changed = true; + } + return changed; } } // namespace - void ComputeWeakWaveBoost(const std::vector& values, double level, std::vector& out) + void ComputeWeakWaveBoost(const std::vector& values, double level, int order, + std::vector& out) { const std::size_t count = values.size(); out.assign(count, std::numeric_limits::quiet_NaN()); @@ -1099,155 +1240,33 @@ namespace jdi return; } - // 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。 + // 第 1 阶:从原始采样值出发(非有限值保持 NaN)。 + std::vector current(count, std::numeric_limits::quiet_NaN()); for (std::size_t index = 0; index < count; ++index) { const double value = values[index]; if (std::isfinite(value)) { - out[index] = static_cast(value); + current[index] = static_cast(value); } } - // 增强等级:波峰取左右两个波峰中较小者为 0%、较大者为 100%, - // 目标值 = 较小 + 等级 × (较大 − 较小)(波谷相反)。 + // 「阶数 N」与「均值滤波」的阶数语义一致:把同一处理**重复应用 N 次**, + // 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「等级」对应的目标值; + // 多阶时相邻的弱波会互相抬高彼此的基准,从而把较宽的弱波区逐步恢复。 + const int stages = std::max(1, std::min(order, 1000)); const double strength = std::max(0.0, std::min(1.0, level)); - - // 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。 - // 波峰(先升后降)与波谷(先降后升)必然交替出现,因此同类极值就是下标 ±2 的两个。 - std::vector extremes; + std::vector next; + for (int stage = 0; stage < stages; ++stage) { - 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 (!ApplyWeakBoostStage(current, strength, next)) { - 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; + // 这一阶没有任何采样点需要调整:后续阶数也不会再有变化,提前结束。 + break; } + current.swap(next); } - - 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; - } - if (!std::isfinite(current.value) || !std::isfinite(left.value) || - !std::isfinite(right.value)) - { - continue; - } - 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) - { - // 波峰:低于左右两个波峰的均值 → 判定为弱波,上拉到等级对应的目标值。 - if (!(current.value < mean)) - { - continue; - } - destination = lower + strength * (upper - lower); - } - 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); + out.swap(current); } const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const @@ -1893,6 +1912,7 @@ namespace jdi lane.edgeCenterPanelY = source.edgeCenterPanelY; lane.weakBoost = source.weakBoost; lane.weakBoostLevel = source.weakBoostLevel; + lane.weakBoostOrder = source.weakBoostOrder; lane.weakBoostPanelOpen = source.weakBoostPanelOpen; lane.weakBoostPanelX = source.weakBoostPanelX; lane.weakBoostPanelY = source.weakBoostPanelY;