实验性功能新增「均值滤波」+ 构建为新信号

- 「实验性功能」二级菜单新增带勾选的「均值滤波」:开启后为本网格每条信号叠加绘制
  一条 N 点滑动平均曲线,算法与「边沿计数(中心轴)」的中心平均值曲线**完全一致**
  (同一实现 ComputeMovingAverage),配色同样由信号色派生为高对比度颜色、透明度一致。
- 新增均值滤波浮动面板:标题栏可拖拽、✕ 关闭,面板内列出被处理的信号名、
  「均值点数 N」内联编辑行,以及底部的「构建为新信号」按钮(悬停高亮 + 手型光标)。
- 「构建为新信号」:把滤波后的曲线作为新信号插入,命名为「<原信号名称> (N点均值)」,
  归属到原信号所属组的原信号位置**下方**,成为独立的新信号;其所在网格也移动到
  原信号网格的下方。合并网格时一次按钮为该网格内每条可见信号各构建一条。
  为此新增 DataSet::InsertDerivedSignal / TabConfig::RemapAfterSignalInsert /
  TabConfig::MoveLaneOfSignalBelow / TabConfig::RenameLanes(插入位置之后的信号引用
  与可见性列同步重映射)。
- 光标交互与「边沿计数(中心轴)」同构:光标处标注均值滤波曲线的取值
  与双光标增量,文案为「值(均值滤波)」/「值(+增量 均值滤波)」;
  两条派生曲线的取值胶囊已抽成同一段绘制逻辑。
- 浮动面板种类由 bool centerAxis 改为 EdgePanelKind{COUNT,CENTER,MEAN},
  布局/宽度/命中测试/绘制统一走同一套几何。
- 以 VERY_BIG_DATA.CSV 验证:曲线预览、N 编辑、双光标取值标注、
  单信号与合并网格两种情况下的新信号命名与插入位置均正确。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-09 21:00:38 +08:00
1 parent 8ae8d7af4f
commit f5028b1e18
6 files changed
+885 -210

No files matched your search

+118
View File
@@ -1398,6 +1398,112 @@ namespace jdi
return true;
}
bool Application::BuildMeanFilterSignal(int fileIndex, int signalIndex, int radius, bool makeVisible)
{
const DataFile* file = m_dataSet.FileAt(fileIndex);
if (file == nullptr || signalIndex < 0 || signalIndex >= static_cast<int>(file->signals.size()))
{
return false;
}
const SignalData& source = file->signals[static_cast<std::size_t>(signalIndex)];
const std::wstring baseName = source.Title();
// 1. 计算均值滤波采样值(与预览曲线共用同一实现)。
std::vector<float> filtered;
ComputeMovingAverage(source.values, radius, filtered);
// 2. 组装新信号:名称形如「<原信号名称> (N点均值)」。
SignalData derived;
derived.name = baseName + L" (" + std::to_wstring(radius) + L"点均值)";
derived.rawHeader = derived.name;
derived.unit = source.unit;
derived.values.assign(filtered.begin(), filtered.end());
// 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。
int newIndex = -1;
if (!m_dataSet.InsertDerivedSignal(fileIndex, signalIndex, std::move(derived), newIndex))
{
return false;
}
for (TabConfig& tab : m_tabs)
{
tab.RemapAfterSignalInsert(fileIndex, newIndex);
}
// 4. 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
if (makeVisible && !m_tabs.empty())
{
SignalRef derivedRef;
derivedRef.fileIndex = fileIndex;
derivedRef.signalIndex = newIndex;
ActiveTab().SetVisible(m_dataSet, derivedRef, true);
SignalRef sourceRef;
sourceRef.fileIndex = fileIndex;
sourceRef.signalIndex = signalIndex;
ActiveTab().MoveLaneOfSignalBelow(derivedRef, sourceRef);
}
// 5. 数据层结构变化:刷新列表、作废折线/派生缓存并重绘。
SyncTabsToDataSet();
return true;
}
bool Application::ApplyPendingMeanFilterBuild()
{
int laneIndex = -1;
int radius = 0;
if (!m_plot.TakePendingMeanFilterBuild(laneIndex, radius))
{
return false;
}
if (m_tabs.empty() || laneIndex < 0 ||
laneIndex >= static_cast<int>(ActiveTab().Lanes().size()))
{
return false;
}
// 先收集该网格内的全部可见信号(插入过程中信号下标会变化,因此先取快照)。
std::vector<SignalRef> targets;
for (const SignalRef& ref : ActiveTab().Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{
if (m_dataSet.Resolve(ref) == nullptr || !ActiveTab().IsVisible(ref))
{
continue;
}
targets.push_back(ref);
}
// **同一文件内必须按下标降序处理**:每次插入只让插入点之后的信号下标 +1,
// 降序处理可保证尚未处理的信号下标始终有效(升序会把新插入的信号当成原信号)。
std::sort(targets.begin(), targets.end(),
[](const SignalRef& a, const SignalRef& b)
{
if (a.fileIndex != b.fileIndex)
{
return a.fileIndex < b.fileIndex;
}
return a.signalIndex > b.signalIndex;
});
int built = 0;
for (const SignalRef& ref : targets)
{
if (BuildMeanFilterSignal(ref.fileIndex, ref.signalIndex, radius, true))
{
++built;
}
}
if (built > 0)
{
SetStatusMessage(L"已构建 " + std::to_wstring(built) + L" 条 " + std::to_wstring(radius) +
L"点均值滤波信号。",
false);
}
return built > 0;
}
void Application::ShowTabContextMenu(const TabBarResult& result)
{
if (result.index < 0 || result.index >= static_cast<int>(m_tabs.size()))
@@ -1519,6 +1625,12 @@ namespace jdi
edgeCenter.id = PLOT_CMD_LANE_EDGE_CENTER;
edgeCenter.checked = m_plot.LaneEdgeCenterCount(laneIndex);
experimental.submenu.push_back(edgeCenter);
MenuItem meanFilter;
meanFilter.label = L"均值滤波";
meanFilter.id = PLOT_CMD_LANE_MEAN_FILTER;
meanFilter.checked = m_plot.LaneMeanFilter(laneIndex);
experimental.submenu.push_back(meanFilter);
}
items.push_back(experimental);
@@ -1629,6 +1741,10 @@ namespace jdi
// 「实验性功能 → 边沿计数(中心轴)」:切换中心平均值曲线与统计面板。
m_plot.ToggleLaneEdgeCenterCount(laneIndex);
break;
case PLOT_CMD_LANE_MEAN_FILTER:
// 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。
m_plot.ToggleLaneMeanFilter(laneIndex);
break;
case PLOT_CMD_LANE_SPLIT:
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
m_plot.SplitLane(laneIndex);
@@ -2116,6 +2232,8 @@ namespace jdi
{
// 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。
ApplyPendingRename();
// 「均值滤波 → 构建为新信号」按钮:立即构建新信号并同步各视图。
ApplyPendingMeanFilterBuild();
InvalidateRect(hwnd, nullptr, FALSE);
}
// 拖动期间 Windows 不再派发 WM_SETCURSOR,需在按下后立即刷新一次,