实验性功能新增「差异放大」,并修正「构建为新信号」按钮的间距

1. 新增「实验性功能 → 差异放大」(与其它实验功能一样是带勾选的开关)
   - 用途:纹波类信号某个周期(甚至半个周期)的 M 形下凹明显偏弱时,
     把这段下凹按比例拉回周边振幅,恢复正常周期的对比度,便于看出波形异常。
   - 算法(全 O(n),不额外占用与信号长度成正比以外的内存):
     以半径约半个纹波周期的滑动平均作为纹波中心线,用邻域内「交流分量的最大值」
     作为局部纹波振幅,取全信号振幅的 75 分位作为目标振幅,
     逐点增益 = 目标振幅 / 局部振幅(夹在 1 到放大倍数之间),
     输出 = 中心线 + 增益 × 交流分量,再夹在周边包络内。
   - 关键性质(均已用合成正弦逐点验证):
     * 增益下界恒为 1,因此**只有被判定为"偏弱"的采样点会改变**,
       其余采样点输出与原值逐位相同 —— 实测逐周期表格中只有一个周期发生变化;
     * 放大结果夹在周边包络内,因此**任何一点都不超出原有的振幅范围**,
       整体值域与其它周期的峰峰值都不变;
     * 端点自动收敛到不放大,避免记录首尾被整片抬升。
   - 参数「窗口点数 X」(窗口长度,建议略大于纹波周期的一半)与
     「放大倍数 N」(增益上限)都可在浮动面板内按信号编辑。

2. 浮动面板
   - 与「均值滤波」完全同一套排版:自上而下逐信号条目,
     第一排信号名与复选框、第二排两个参数输入框;取消勾选即不显示该信号的参数与预览曲线;
     启用时默认只勾选网格内第一个可见信号。
   - 新增「构建为新信号」按钮,按各信号自己的参数把结果插入到源信号下方,
     与「均值滤波」的构建机制完全一致(统一走同一套请求与插入流程)。

3. 光标交互
   - 纵向光标额外标注差异放大曲线在该时刻的取值,配色与原始信号一致,
     数值尾部括号内容为「差异化」,双光标时同样带增量。

4. 修复「构建为新信号」按钮与面板下边界的间距
   - 原按钮行直接贴着面板下边框,与面板左右内边距及内容间距都不协调;
     现在按钮上方与下方各保留一个与左右一致的内边距(均值滤波与差异放大同时生效)。

测试:离屏验证套件新增两组检查(合成正弦上算法只改动一个周期、放大不越界、
参数上限生效、面板排版与按钮间距;再写一份合成纹波文件真正加载渲染并核对曲线数值),
合计 289 项检查全部通过;验证过程不显示窗口、不移动鼠标键盘、不抓屏。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 17:54:27 +08:00
1 parent 8801c8f019
commit d4206a8d67
6 files changed
+1188 -185

No files matched your search

+380 -75
View File
@@ -70,7 +70,7 @@ namespace jdi
const float kEdgePanelMeanEditHeight = 24.0f; /**< 逐条目面板中每条信号的参数行高度。 */
const float kEdgePanelMeanRowGap = 2.0f; /**< 名称行与参数行之间的间距。 */
const float kEdgePanelMeanCheckSize = 15.0f; /**< 逐条目面板中条目复选框尺寸。 */
const wchar_t* const kMeanFilterBuildLabel = L"构建为新信号"; /**< 均值滤波面板底部按钮文案。 */
const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 差异放大共用)。 */
const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */
/**
@@ -228,8 +228,11 @@ namespace jdi
*
* 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。
*/
const EdgePanelKind kEdgePanelKinds[] = {
EdgePanelKind::COUNT, EdgePanelKind::CENTER, EdgePanelKind::MEAN};
const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::DIFF_AMP};
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
/**
* @brief 时间间隔的显示单位。
@@ -1126,6 +1129,7 @@ namespace jdi
m_edgeCountCache.clear();
m_edgeCenterCountCache.clear();
m_meanCurveCache.clear();
m_diffAmpCurveCache.clear();
// 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。
m_hoverSignal = SignalRef();
m_hoverLegend = SignalRef();
@@ -1700,6 +1704,96 @@ namespace jdi
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].meanFilter;
}
bool PlotView::LaneDiffAmp(int laneIndex) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].diffAmp;
}
bool PlotView::SetLaneDiffAmp(int laneIndex, bool enabled)
{
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)];
if (lane.diffAmp == enabled)
{
return false;
}
lane.diffAmp = enabled;
if (enabled)
{
// 与「均值滤波」完全一致:默认只勾选网格内**第一个可见信号**。
if (lane.diffAmpItems.empty())
{
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data != nullptr && resolved.visible)
{
lane.EnsureDiffAmpItem(ref);
break;
}
}
}
lane.diffAmpPanelOpen = true;
}
else
{
lane.diffAmpPanelOpen = false;
}
return true;
}
bool PlotView::ToggleLaneDiffAmp(int laneIndex)
{
const bool enabled = !LaneDiffAmp(laneIndex);
SetLaneDiffAmp(laneIndex, enabled);
return enabled;
}
bool PlotView::ToggleLaneDiffAmpSignal(int laneIndex, const SignalRef& ref)
{
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)];
if (lane.DiffAmpEnabled(ref))
{
lane.RemoveDiffAmpItem(ref);
return false;
}
lane.EnsureDiffAmpItem(ref);
return true;
}
bool PlotView::LaneDiffAmpSignalEnabled(int laneIndex, const SignalRef& ref) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].DiffAmpEnabled(ref);
}
bool PlotView::TakePendingDiffAmpBuild(int& laneIndex, std::vector<DiffAmpBuild>& items)
{
if (m_pendingDiffAmpLane < 0)
{
return false;
}
laneIndex = m_pendingDiffAmpLane;
items = m_pendingDiffAmpItems;
m_pendingDiffAmpLane = -1;
m_pendingDiffAmpItems.clear();
return true;
}
bool PlotView::SetLaneMeanFilter(int laneIndex, bool enabled)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
@@ -1828,6 +1922,53 @@ namespace jdi
return m_meanCurveCache.back().values;
}
const std::vector<float>& PlotView::DiffAmpCurveFor(const SignalData& signal, int points,
int gain) const
{
const int window = std::max(1, points);
const int multiplier = std::max(1, gain);
for (const DiffAmpCurveEntry& entry : m_diffAmpCurveCache)
{
if (entry.signal == &signal && entry.points == window && entry.gain == multiplier)
{
return entry.values;
}
}
DiffAmpCurveEntry entry;
entry.signal = &signal;
entry.points = window;
entry.gain = multiplier;
// 与「构建为新信号」共用同一实现(Utilities 内的自由函数),
// 因此预览曲线与构建出的采样值逐位一致。
ComputeDiffAmplify(signal.values, window, multiplier, entry.values);
m_diffAmpCurveCache.push_back(std::move(entry));
return m_diffAmpCurveCache.back().values;
}
bool PlotView::DiffAmpValueAt(const DataFile& file, const SignalData& signal, int points, int gain,
double time, double& value) const
{
const int index = FindNearestSample(file, time);
if (index < 0)
{
return false;
}
const std::vector<float>& curve = DiffAmpCurveFor(signal, points, gain);
if (static_cast<std::size_t>(index) >= curve.size())
{
return false;
}
const float sampled = curve[static_cast<std::size_t>(index)];
if (!std::isfinite(sampled))
{
return false;
}
value = static_cast<double>(sampled);
return true;
}
void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int points,
int order, bool hasRange, double rangeMin, double rangeMax,
std::size_t& rising, std::size_t& falling) const
@@ -2067,11 +2208,16 @@ namespace jdi
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(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);
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” + 光标区间尾缀)。
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “差异放大”
// + 光标区间尾缀)。
const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix());
: (diffAmp ? std::wstring(L"差异放大")
: ((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;
@@ -2086,7 +2232,7 @@ namespace jdi
continue;
}
const float nameWidth = UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->Title());
if (meanFilter || centerAxis)
if (itemized)
{
// 逐条目面板:第一排 = 名称 + 复选框;第三排(中心轴)= 统计结果。
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
@@ -2094,7 +2240,7 @@ namespace jdi
}
std::size_t rising = 0;
std::size_t falling = 0;
if (meanFilter)
if (meanFilter || diffAmp)
{
continue;
}
@@ -2120,12 +2266,14 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth));
}
if (centerAxis || meanFilter)
if (centerAxis || meanFilter || diffAmp)
{
// 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。
// (「均值点数 X」/「阶数 N」的中心轴面板与均值滤波面板排布完全一致。)
// (「均值点数 X」/「阶数 N」的布局对三个逐条目面板通用:标签与数值框各占半行,
// 标签取较宽者以保证“放大倍数 N”这类更长的标签不被压住。)
const float labelX = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X");
const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"阶数 N");
const float labelN = std::max(UiMeasureTextWidth(m_fonts->uiSmall, L"阶数 N"),
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);
@@ -2134,11 +2282,11 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
}
if (meanFilter)
if (meanFilter || diffAmp)
{
// 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。
const float buttonWidth =
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterBuildLabel) + 28.0f * m_scale;
UiMeasureTextWidth(m_fonts->uiSmall, kBuildSignalLabel) + 28.0f * m_scale;
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
}
@@ -2661,10 +2809,12 @@ 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::MEAN_FILTER_ORDER ||
m_editTarget == PlotEditTarget::DIFF_AMP_POINTS ||
m_editTarget == PlotEditTarget::DIFF_AMP_ORDER)
{
// 「均值点数 X」/「阶数 N」:输入框就落在对应的字段矩形上
//(中心轴面板与均值滤波面板的字段矩形含义相同)。
//(中心轴、均值滤波与差异放大面板的字段矩形含义相同)。
if (m_editFieldRect.Width <= 0.0f)
{
return gd::RectF();
@@ -2831,30 +2981,37 @@ 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)
centerTarget || diffTarget)
{
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool isPoints = (target == PlotEditTarget::MEAN_FILTER_POINTS ||
target == PlotEditTarget::EDGE_CENTER_POINTS);
target == PlotEditTarget::EDGE_CENTER_POINTS ||
target == PlotEditTarget::DIFF_AMP_POINTS);
const Lane::MeanFilterItem* existing =
centerTarget ? lane.FindEdgeCenterItem(m_editMeanRef) : lane.FindMeanFilterItem(m_editMeanRef);
centerTarget ? lane.FindEdgeCenterItem(m_editMeanRef)
: (diffTarget ? lane.FindDiffAmpItem(m_editMeanRef)
: lane.FindMeanFilterItem(m_editMeanRef));
if (existing == nullptr)
{
return true;
}
const int maximum = isPoints ? 1000000 : 1000;
int value = static_cast<int>(std::lround(parsed));
// 阶数至少为 1(0 阶等于不做滤波,没有意义);均值点数可以为 0(不平滑)。
// 阶数/倍数至少为 1(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)
: lane.EnsureMeanFilterItem(m_editMeanRef);
: (diffTarget ? lane.EnsureDiffAmpItem(m_editMeanRef)
: lane.EnsureMeanFilterItem(m_editMeanRef));
if (isPoints)
{
item.points = value;
@@ -2866,6 +3023,7 @@ namespace jdi
// 参数变化会改变曲线:清掉派生缓存后重绘。
m_meanCurveCache.clear();
m_edgeCenterCountCache.clear();
m_diffAmpCurveCache.clear();
InvalidateSceneCache();
return true;
}
@@ -3108,7 +3266,8 @@ namespace jdi
{
return LoadCursorW(nullptr, IDC_SIZEALL);
}
if (kind == EdgePanelKind::MEAN && layout.button.Width > 0.0f &&
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::DIFF_AMP) &&
layout.button.Width > 0.0f &&
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
{
// 「构建为新信号」按钮:手型光标作为可点击提示。
@@ -3379,6 +3538,10 @@ namespace jdi
lane.meanFilterPanelX = left;
lane.meanFilterPanelY = top;
break;
case EdgePanelKind::DIFF_AMP:
lane.diffAmpPanelX = left;
lane.diffAmpPanelY = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanelX = left;
@@ -3584,7 +3747,7 @@ namespace jdi
// * 底部按钮 → 触发「构建为新信号」请求;
// * 标题栏 → 拖动面板;
// * 面板主体 → 消费点击,但**不拖动也不穿透**(用户明确要求只有标题栏可拖拽)。
for (int pass = 0; pass < 3; ++pass)
for (int pass = 0; pass < kEdgePanelKindCount; ++pass)
{
const EdgePanelKind kind = kEdgePanelKinds[pass];
bool handledPanel = false;
@@ -3607,17 +3770,23 @@ namespace jdi
m_pendingPanelCloseKind = kind;
}
// 「中心轴边沿计数」/「均值滤波」面板:逐信号条目(同一套排布与交互)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER) && m_tab != nullptr)
// 「中心轴边沿计数」/「均值滤波」/「差异放大」面板:逐信号条目
// (同一套排布与交互,只是输入框绑定的编辑目标与条目列表不同)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
kind == EdgePanelKind::DIFF_AMP) &&
m_tab != nullptr)
{
const bool meanMode = (kind == EdgePanelKind::MEAN);
const bool diffMode = (kind == EdgePanelKind::DIFF_AMP);
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
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) : tabLane.FindEdgeCenterItem(ref);
return meanMode ? tabLane.FindMeanFilterItem(ref)
: (diffMode ? tabLane.FindDiffAmpItem(ref)
: tabLane.FindEdgeCenterItem(ref));
};
// 第一排右侧的复选框 → 切换该信号的预览勾选状态。
if (layout.rowChecks[rowIndex].Width > 0.0f &&
@@ -3627,25 +3796,33 @@ namespace jdi
{
ToggleLaneMeanFilterSignal(index, ref);
}
else if (diffMode)
{
ToggleLaneDiffAmpSignal(index, ref);
}
else
{
ToggleLaneEdgeCenterSignal(index, ref);
}
m_meanCurveCache.clear();
m_edgeCenterCountCache.clear();
m_diffAmpCurveCache.clear();
InvalidateSceneCache();
m_dragMode = PlotDragMode::NONE;
return true;
}
// 第二排的两个输入框 → 就地编辑该信号的均值点数 X / 阶数 N。
// 第二排的两个输入框 → 就地编辑该信号的均值点数 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
: PlotEditTarget::EDGE_CENTER_POINTS,
index, std::to_wstring(item != nullptr ? item->points : 5),
: (diffMode ? PlotEditTarget::DIFF_AMP_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS),
index,
std::to_wstring(item != nullptr ? item->points : (diffMode ? 64 : 5)),
layout.rowPoints[rowIndex]);
return true;
}
@@ -3655,13 +3832,51 @@ namespace jdi
const Lane::MeanFilterItem* item = findItem();
m_editMeanRef = ref;
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ORDER
: PlotEditTarget::EDGE_CENTER_ORDER,
index, std::to_wstring(item != nullptr ? item->order : 1),
: (diffMode ? PlotEditTarget::DIFF_AMP_ORDER
: PlotEditTarget::EDGE_CENTER_ORDER),
index,
std::to_wstring(item != nullptr ? item->order : (diffMode ? 2 : 1)),
layout.rowOrder[rowIndex]);
return true;
}
}
if (diffMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{
// 「差异放大 → 构建为新信号」:与均值滤波同一套请求机制。
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(index)];
m_pendingDiffAmpItems.clear();
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || !resolved.visible)
{
continue;
}
const Lane::DiffAmpItem* item = lane.FindDiffAmpItem(ref);
if (item == nullptr)
{
continue;
}
DiffAmpBuild build;
build.fileIndex = ref.fileIndex;
build.signalIndex = ref.signalIndex;
build.points = item->points;
build.gain = item->order;
m_pendingDiffAmpItems.push_back(build);
}
if (!m_pendingDiffAmpItems.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;
}
m_dragMode = PlotDragMode::NONE;
return true;
}
if (meanMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{
// 「构建为新信号」:只登记请求(含每个信号自己的 X/N),
@@ -3844,6 +4059,9 @@ namespace jdi
case EdgePanelKind::MEAN:
SetLaneMeanFilter(lane, false);
break;
case EdgePanelKind::DIFF_AMP:
SetLaneDiffAmp(lane, false);
break;
case EdgePanelKind::COUNT:
default:
SetLaneEdgeCount(lane, false);
@@ -4810,6 +5028,16 @@ namespace jdi
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.order));
}
// 「差异放大」的开关与每个被勾选信号各自的窗口点数 X / 放大倍数 N 同理。
mix(Tab().Lanes()[laneIndex].diffAmp ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].diffAmpItems.size()));
for (const Lane::DiffAmpItem& item : Tab().Lanes()[laneIndex].diffAmpItems)
{
mix(static_cast<std::uint64_t>(item.ref.fileIndex));
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
mix(static_cast<std::uint64_t>(item.points));
mix(static_cast<std::uint64_t>(item.order));
}
mix(signals.size());
for (const SignalRef& ref : signals)
{
@@ -5622,6 +5850,9 @@ namespace jdi
// 「均值滤波」开启时,为**被勾选**的信号各叠加一条多阶滑动平均曲线
//(均值点数 X、阶数 N 由该信号自己的条目给出)。
const bool meanFilterOn = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].meanFilter;
// 「差异放大」开启时同样只为**被勾选**的信号叠加曲线
//(窗口点数 X、放大倍数 N 由该信号自己的条目给出)。
const bool diffAmpOn = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].diffAmp;
for (const SignalRef& ref : laneSignals)
{
@@ -5758,6 +5989,16 @@ namespace jdi
PaintMeanFilterCurve(g, resolved, lane, item->points, item->order);
}
}
// 差异放大曲线:同样是仅用于预览的叠加曲线,只在被勾选时绘制。
if (diffAmpOn)
{
const Lane::DiffAmpItem* item =
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].FindDiffAmpItem(ref);
if (item != nullptr)
{
PaintDiffAmpCurve(g, resolved, lane, item->points, item->order);
}
}
}
// g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。
@@ -6472,14 +6713,19 @@ 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)
// 「边沿计数(中心轴)」/「均值滤波」/「差异放大」开启时,纵向光标还要额外标注
// **派生曲线**在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。
// 三条曲线的参数与标注规则完全一致,故共用同一段绘制逻辑。
const auto paintDerivedCapsule = [&](int first, int second, const std::wstring& suffix,
bool diffAmp)
{
double derivedValue = 0.0;
if (!MeanFilterValueAt(*resolved.file, *resolved.data, first, second, time,
derivedValue))
const bool ok =
diffAmp ? DiffAmpValueAt(*resolved.file, *resolved.data, first, second, time,
derivedValue)
: MeanFilterValueAt(*resolved.file, *resolved.data, first, second, time,
derivedValue);
if (!ok)
{
return;
}
@@ -6498,8 +6744,12 @@ namespace jdi
if (showDelta)
{
double otherDerived = 0.0;
if (MeanFilterValueAt(*resolved.file, *resolved.data, first, second,
otherTime, otherDerived))
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 double delta = derivedValue - otherDerived;
centerText += (delta < 0.0) ? L"-" : L"+";
@@ -6549,7 +6799,7 @@ namespace jdi
m_tab->Lanes()[laneIndex].FindEdgeCenterItem(ref);
if (centerItem != nullptr)
{
paintDerivedCapsule(centerItem->points, centerItem->order, L"中心轴");
paintDerivedCapsule(centerItem->points, centerItem->order, L"中心轴", false);
}
}
if (m_tab->Lanes()[laneIndex].meanFilter)
@@ -6559,7 +6809,17 @@ namespace jdi
m_tab->Lanes()[laneIndex].FindMeanFilterItem(ref);
if (meanItem != nullptr)
{
paintDerivedCapsule(meanItem->points, meanItem->order, L"均值滤波");
paintDerivedCapsule(meanItem->points, meanItem->order, L"均值滤波", false);
}
}
if (m_tab->Lanes()[laneIndex].diffAmp)
{
// 差异放大曲线:同样是只标注被勾选的信号,尾缀为「差异化」。
const Lane::DiffAmpItem* diffItem =
m_tab->Lanes()[laneIndex].FindDiffAmpItem(ref);
if (diffItem != nullptr)
{
paintDerivedCapsule(diffItem->points, diffItem->order, L"差异化", true);
}
}
}
@@ -6642,9 +6902,15 @@ namespace jdi
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool enabled = meanFilter ? lane.meanFilter : (centerAxis ? lane.edgeCenter : lane.edgeCount);
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 open = meanFilter ? lane.meanFilterPanelOpen
: (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen);
: (diffAmp ? lane.diffAmpPanelOpen
: (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen));
if (!enabled || !open)
{
return layout;
@@ -6669,11 +6935,10 @@ namespace jdi
const float titleHeight = kEdgePanelTitleHeight * m_scale;
const float rowHeight = kEdgePanelRowHeight * m_scale;
const float pad = kEdgePanelPad * m_scale;
// 「中心轴边沿计数」与「均值滤波」共用同一套「逐条目」排布(三排/两排),
// 「中心轴边沿计数」「均值滤波」「差异放大」共用同一套「逐条目」排布(三排/两排),
// 因此不再有独立的底部编辑行;只有「边沿计数」保留两行文本式排版。
const float buttonHeight = meanFilter ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
const float rowsPerSignal = meanFilter ? 1.0f : 2.0f;
const bool itemized = meanFilter || centerAxis;
// 逐条目面板:每个条目 = 第一排(信号名 + 复选框)+ 第二排(均值点数 X / 阶数 N)
// + (中心轴)第三排(统计结果);后两排**只在该条目被勾选时才占位**,
@@ -6692,7 +6957,9 @@ namespace jdi
continue;
}
itemsHeight += meanRowHeight;
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref) : lane.EdgeCenterEnabled(ref);
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref)
: (diffAmp ? lane.DiffAmpEnabled(ref)
: lane.EdgeCenterEnabled(ref));
if (checked)
{
itemsHeight += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale;
@@ -6702,7 +6969,10 @@ namespace jdi
}
}
}
height = titleHeight + pad + itemsHeight + pad + buttonHeight;
// 底部按钮行:上方与条目之间、下方与面板下边界之间各留一个 `pad`,
// 与面板的左右内边距完全一致(原先按钮直接贴着下边框,看起来很不协调)。
height = titleHeight + pad + itemsHeight + pad + buttonHeight +
(hasButton ? pad : 0.0f);
}
else
{
@@ -6711,9 +6981,11 @@ namespace jdi
// 首次使用(位置为负)时自动放到绘图区右侧,并各网格依次错开,避免完全重叠。
const double storedX = meanFilter ? lane.meanFilterPanelX
: (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX);
: (diffAmp ? lane.diffAmpPanelX
: (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX));
const double storedY = meanFilter ? lane.meanFilterPanelY
: (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY);
: (diffAmp ? lane.diffAmpPanelY
: (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY));
float left = (storedX >= 0.0)
? static_cast<float>(storedX)
: (m_plotRect.X + m_plotRect.Width - width - 16.0f * m_scale);
@@ -6736,9 +7008,12 @@ namespace jdi
if (itemized)
{
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
if (meanFilter)
if (hasButton)
{
layout.button = gd::RectF(left + pad, top + height - buttonRowHeight,
// 底部按钮与面板下边界之间留出与左右、与内容相同的 `pad` 间距
//(原先按钮行直接贴着下边框,视觉上与其它元素不协调);
// 面板高度已含这份 pad,因此这里只上移按钮行即可。
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
}
@@ -6762,7 +7037,8 @@ namespace jdi
left + width - pad - checkSize, rowY + (meanRowHeight - checkSize) * 0.5f, checkSize,
checkSize));
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref)
: lane.EdgeCenterEnabled(ref);
: (diffAmp ? lane.DiffAmpEnabled(ref)
: lane.EdgeCenterEnabled(ref));
if (checked)
{
const float editTop = rowY + meanRowHeight;
@@ -7132,6 +7408,17 @@ 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
{
if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr)
{
return;
}
// 派生曲线一律使用同一段绘制逻辑与配色规则(差异放大曲线同样如此)。
PaintDerivedCurve(g, resolved, lane, DiffAmpCurveFor(*resolved.data, points, gain));
}
void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
{
if (m_tab == nullptr || m_dataSet == nullptr)
@@ -7236,13 +7523,17 @@ namespace jdi
}
// ---- 统计浮动面板(最后绘制,覆盖在波形之上)----
// 「边沿计数」「边沿计数(中心轴)」「均值滤波」共用同一套样式,
// 仅标题、内容行与底部编辑行/按钮不同。
for (int pass = 0; pass < 3; ++pass)
// 「边沿计数」「边沿计数(中心轴)」「均值滤波」「差异放大」共用同一套样式,
// 仅标题、内容行与底部按钮不同。
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);
for (std::size_t index = 0; index < count; ++index)
{
const Lane& tabLane = tabLanes[index];
@@ -7277,7 +7568,9 @@ namespace jdi
m_theme->text, TextAlign::CENTER);
const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix());
: (diffAmp ? std::wstring(L"差异放大")
: ((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,
@@ -7305,10 +7598,10 @@ namespace jdi
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
1.6f * m_scale);
// 「中心轴边沿计数」/「均值滤波」面板都采用「逐条目」排布:
// 第一排 = 名称 + 复选框,第二排 = 均值点数 X / 阶数 N(仅勾选的条目),
// 「中心轴边沿计数」/「均值滤波」/「差异放大」面板都采用「逐条目」排布:
// 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目),
// 中心轴再多第三排 = 对应的统计结果(同样仅勾选的条目)。
if (meanFilter || centerAxis)
if (itemized)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
@@ -7321,7 +7614,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)
: tabLane.EdgeCenterEnabled(ref);
: (diffAmp ? tabLane.DiffAmpEnabled(ref)
: tabLane.EdgeCenterEnabled(ref));
// 名称:第一排左侧,避开右侧复选框。
const float nameWidth =
@@ -7338,15 +7632,26 @@ namespace jdi
continue;
}
// 第二排:两个输入框,左侧标“均值点数 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;
// 第二排:两个输入框。均值滤波是“均值点数 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);
const auto paintField = [&](const gd::RectF& rect, const wchar_t* label,
int value, PlotEditTarget target)
{
@@ -7383,8 +7688,8 @@ namespace jdi
m_theme->textStrong, TextAlign::CENTER);
}
};
paintField(layout.rowPoints[rowIndex], L"均值点数 X", shownPoints, pointsTarget);
paintField(layout.rowOrder[rowIndex], L"阶数 N", shownOrder, orderTarget);
paintField(layout.rowPoints[rowIndex], pointsLabel, shownPoints, pointsTarget);
paintField(layout.rowOrder[rowIndex], orderLabel, shownOrder, orderTarget);
// 第三排(中心轴):该信号在当前参数下的上升沿/下降沿统计结果。
if (centerAxis && rowIndex < layout.rowStats.size() &&
@@ -7414,7 +7719,7 @@ namespace jdi
{
continue;
}
if (meanFilter || centerAxis)
if (itemized)
{
// 逐条目绘制已在上面完成。
continue;
@@ -7438,8 +7743,8 @@ namespace jdi
rowY += rowHeight * 2.0f;
}
// 均值滤波面板底部按钮行:「构建为新信号」(悬停高亮)。
if (meanFilter)
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮)。
if (hasButton)
{
const gd::RectF& buttonRect = layout.button;
if (buttonRect.Width > 0.0f)
@@ -7453,7 +7758,7 @@ namespace jdi
UiFillRoundRect(g, inner, 4.0f * m_scale,
hot ? WithAlpha(m_theme->accent, 0x3A) : m_theme->cardBg);
UiDrawRoundRect(g, inner, 4.0f * m_scale, m_theme->accent, 1.2f);
UiText(g, kMeanFilterBuildLabel, m_fonts->uiSmall, inner, m_theme->accent,
UiText(g, kBuildSignalLabel, m_fonts->uiSmall, inner, m_theme->accent,
TextAlign::CENTER);
}
}