弱波增强:去掉前置滤波并新增「阶数 N」滑块;修复选区放大越界绘制与浮动面板 ✕ 交互

- 弱波增强不再做任何前置滤波(删除内部的 5 点滑动平均 BoostShapeAt):
  极值点直接在输入采样上识别。需要平滑时用户可以先用「均值滤波」构建一条
  滤波后的信号,再对它做弱波增强。
- 新增「阶数 N」参数(1~10,滑块输入),语义与均值滤波的阶数 N 一致 ——
  把同一处理重复应用 N 次,每阶以上一阶为输入:
  一阶时改定量严格等于「等级」对应的目标值;多阶会让相邻弱波互相抬高彼此的
  基准,从而把较宽的弱波区逐步恢复(实测三连弱波的中间峰 550→575→598.4)。
  某一阶若不再产生改动会提前结束迭代。
  `ComputeWeakWaveBoost(values, level, order, out)`;
  `Lane::weakBoostOrder`(默认 1)随 LaneCarry / SplitLane / 引用重映射同步,
  并进入 SceneContentHash;曲线缓存键变为「信号 + 等级 + 阶数」。
- 浮动面板同步新增一行「阶数 N」滑块(新拖拽模式 BOOST_ORDER、
  EdgePanelLayout::sliderOrder);「构建为新信号」命名带上阶数
  (形如 `<原信号> (弱波增强 60%×2阶)`)。
- 修复:选区放大时鼠标被捕获到绘图区之外(工具栏 / 信号面板 / 时间轴)会把
  选区块画到绘图区之外,而拖拽期间只失效绘图区 → 留下无法擦除的墨迹。
  现在拖拽时把选区夹在绘图区内,PaintRubberBand 再对 m_plotRect 求交一次。
- 修复:浮动面板的 ✕ 位于标题栏矩形内部,按下后会继续落到「标题栏 → 拖动面板」
  分支,表现为「按住 ✕ 却把面板拖走了」;现在按下 ✕ 直接消费该次点击(抬起时
  仍按原语义确认关闭),并把 ✕ 的光标改为手型,与其它关闭按钮一致。
- 修复:✕ / 滑块 / 底部按钮的悬停高亮依赖鼠标坐标,但从同一网格的波形移到面板上
  时悬停状态毫无变化 → OnMouseMove 返回 false → 宿主不重绘 → 高亮不出现。
  现在「进入 / 离开 / 在面板内移动」都会请求重绘。
- 离屏验证(窗口隐藏,不占用鼠标/键盘/屏幕):345 项检查全部通过,含
  阶数扫描(N=1 只改外侧弱峰、N=2 传导到中间弱峰、高 N 逼近健康幅值且不越界、
  健康峰在任何阶数下逐位不变)、两个滑块的按压/拖拽/钳制、选区越界零墨迹
  (绘图区外像素差 0)、面板 ✕ 悬停高亮与按下不拖动、光标拖拽始终在视界内,
  以及真实 20K 电机纹波录波(信号范围保持不变、20%+ 采样被调整)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 19:20:09 +08:00
1 parent 67f659ad23
commit 8c8508e5f8
5 files changed
+457 -240

No files matched your search

+224 -70
View File
@@ -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<std::size_t>(laneIndex)].weakBoostLevel;
}
int PlotView::LaneWeakBoostOrder(int laneIndex) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return kWeakBoostOrderMin;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].weakBoostOrder;
}
bool PlotView::SetLaneWeakBoostOrder(int laneIndex, int order)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(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<int>(m_tab->Lanes().size()))
@@ -1981,12 +2012,13 @@ namespace jdi
}
const std::vector<float>& 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<double>(level) / 100.0, entry.values);
ComputeWeakWaveBoost(signal.values, static_cast<double>(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<float>& curve = WeakBoostCurveFor(signal, levelPercent);
const std::vector<float>& curve = WeakBoostCurveFor(signal, levelPercent, order);
if (static_cast<std::size_t>(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<float>(x), static_cast<float>(y)))
{
// ✕ 按钮:保持原始箭头(仅做悬停高亮提示)。
return nullptr;
// ✕ 按钮:与面板/标签栏的其它 ✕ 一致,用手型光标作为可点击提示。
return LoadCursorW(nullptr, IDC_HAND);
}
if (UiPointInRect(layout.title, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEALL);
}
if (kind == EdgePanelKind::BOOST &&
WeakBoostSliderHit(layout.slider, m_scale, static_cast<float>(x),
static_cast<float>(y)))
(WeakBoostSliderHit(layout.slider, m_scale, static_cast<float>(x),
static_cast<float>(y)) ||
WeakBoostSliderHit(layout.sliderOrder, m_scale, static_cast<float>(x),
static_cast<float>(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<float>(std::min(x, m_dragStartX));
const float width = static_cast<float>(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<double>(m_dragStartX),
static_cast<double>(plotLeft),
static_cast<double>(plotRight));
const float to = ClampDouble(static_cast<double>(x), static_cast<double>(plotLeft),
static_cast<double>(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<int>(m_lanes.size()))
{
return false;
}
const gd::RectF& laneRect = m_lanes[static_cast<std::size_t>(m_dragLane)].rect;
const float top = static_cast<float>(std::min(y, m_dragStartY));
const float bottom = static_cast<float>(std::max(y, m_dragStartY));
const float from = ClampDouble(static_cast<double>(m_dragStartY),
static_cast<double>(laneRect.Y),
static_cast<double>(laneRect.Y + laneRect.Height));
const float to = ClampDouble(static_cast<double>(y), static_cast<double>(laneRect.Y),
static_cast<double>(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<int>(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<int>(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<std::size_t>(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<int>(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<std::size_t>(index)];
const int levelPercent = static_cast<int>(
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<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.order));
}
// 「弱波增强」的开关、增强等级与每个被勾选信号的条目同理。
// 「弱波增强」的开关、增强等级、阶数 N 与每个被勾选信号的条目同理。
mix(Tab().Lanes()[laneIndex].weakBoost ? 1ull : 0ull);
mixFloat(static_cast<float>(Tab().Lanes()[laneIndex].weakBoostLevel));
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].weakBoostOrder));
mix(static_cast<std::uint64_t>(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<std::size_t>(laneIndex)]
.weakBoostLevel)) *
100.0));
PaintWeakBoostCurve(g, resolved, lane, levelPercent);
const int order = std::max(
kWeakBoostOrderMin,
std::min(kWeakBoostOrderMax,
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].weakBoostOrder));
PaintWeakBoostCurve(g, resolved, lane, levelPercent, order);
}
}
}
@@ -6960,7 +7081,10 @@ namespace jdi
const int levelPercent = static_cast<int>(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<LaneVisual>& 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<int>(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<int>(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<int>(index));
paintSliderRow(layout.slider, kWeakBoostLevelLabel, level,
std::to_wstring(static_cast<int>(std::lround(level * 100.0))) +
L"%",
levelActive);
const int order = std::max(kWeakBoostOrderMin,
std::min(kWeakBoostOrderMax, tabLane.weakBoostOrder));
const double orderRatio = static_cast<double>(order - kWeakBoostOrderMin) /
static_cast<double>(kWeakBoostOrderMax -
kWeakBoostOrderMin);
const bool orderActive = (m_dragMode == PlotDragMode::BOOST_ORDER) &&
(m_boostDragLane == static_cast<int>(index));
paintSliderRow(layout.sliderOrder, kWeakBoostOrderLabel, orderRatio,
std::to_wstring(order), orderActive);
}
}