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

- 「实验性功能」二级菜单新增带勾选的「均值滤波」:开启后为本网格每条信号叠加绘制
  一条 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

+369 -200
View File
@@ -55,9 +55,19 @@ namespace jdi
const float kEdgePanelEditHeight = 30.0f; /**< 中心轴面板底部编辑行高度。 */
const float kEdgePanelEditFieldWidth = 64.0f; /**< 编辑行数值框的最小宽度。 */
const float kEdgePanelEditFieldGap = 8.0f; /**< 编辑行说明文字与数值框之间的间隔。 */
const float kEdgePanelButtonHeight = 30.0f; /**< 均值滤波面板底部按钮行高度。 */
const wchar_t* const kMeanFilterBuildLabel = L"构建为新信号"; /**< 均值滤波面板底部按钮文案。 */
const std::uint64_t kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */
const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */
/**
* @brief 实验性功能浮动面板的遍历顺序(命中测试与绘制共用,保证层序一致)。
*
* 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。
*/
const EdgePanelKind kEdgePanelKinds[] = {
EdgePanelKind::COUNT, EdgePanelKind::CENTER, EdgePanelKind::MEAN};
/**
* @brief 时间间隔的显示单位。
*/
@@ -949,6 +959,11 @@ namespace jdi
m_edgeCountCache.clear();
m_edgeCenterCountCache.clear();
m_centerCurveCache.clear();
// 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。
m_hoverSignal = SignalRef();
m_hoverLegend = SignalRef();
m_dragSignal = SignalRef();
m_dragTargetLane = -1;
InvalidateSceneCache();
}
@@ -1458,6 +1473,62 @@ namespace jdi
return enabled;
}
bool PlotView::LaneMeanFilter(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)].meanFilter;
}
bool PlotView::SetLaneMeanFilter(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.meanFilter == enabled)
{
return false;
}
lane.meanFilter = enabled;
if (enabled)
{
if (lane.meanFilterRadius < 0)
{
lane.meanFilterRadius = 0;
}
lane.meanFilterPanelOpen = true;
}
else
{
lane.meanFilterPanelOpen = false;
}
return true;
}
bool PlotView::ToggleLaneMeanFilter(int laneIndex)
{
const bool enabled = !LaneMeanFilter(laneIndex);
SetLaneMeanFilter(laneIndex, enabled);
return enabled;
}
bool PlotView::TakePendingMeanFilterBuild(int& laneIndex, int& radius)
{
if (m_pendingMeanFilterLane < 0)
{
return false;
}
laneIndex = m_pendingMeanFilterLane;
radius = m_pendingMeanFilterRadius;
m_pendingMeanFilterLane = -1;
m_pendingMeanFilterRadius = 0;
return true;
}
const std::vector<float>& PlotView::CenterCurveFor(const SignalData& signal, int smooth) const
{
const int radius = std::max(0, smooth);
@@ -1472,74 +1543,9 @@ namespace jdi
CenterCurveEntry entry;
entry.signal = &signal;
entry.smooth = radius;
entry.values.assign(signal.values.size(), std::numeric_limits<float>::quiet_NaN());
const std::size_t count = signal.values.size();
if (count > 0)
{
// 滑动窗口求和:窗口 = [max(0,i-N), min(count-1,i+N)],
// 每前进一步只增减边界处的两个采样点,整体 O(n),不额外占用前缀和内存。
double sum = 0.0;
std::size_t finiteCount = 0;
std::size_t windowLow = 0;
std::size_t windowHigh = 0; // 闭区间右端(尚未计入时为 0)。
const auto addValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum += value;
++finiteCount;
}
};
const auto removeValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum -= value;
--finiteCount;
}
};
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t low = (index > static_cast<std::size_t>(radius))
? (index - static_cast<std::size_t>(radius))
: 0;
const std::size_t high = std::min(count - 1, index + static_cast<std::size_t>(radius));
if (index == 0)
{
windowLow = low;
windowHigh = high;
for (std::size_t i = windowLow; i <= windowHigh; ++i)
{
addValue(signal.values[i]);
}
}
else
{
// 左端前移:移出旧左端(若已不在窗口内)。
while (windowLow < low)
{
removeValue(signal.values[windowLow]);
++windowLow;
}
// 右端前移:加入新右端。
while (windowHigh < high)
{
++windowHigh;
addValue(signal.values[windowHigh]);
}
}
// 中心点本身无效(NaN)时该处不绘制曲线。
if (finiteCount > 0 && std::isfinite(signal.values[index]))
{
entry.values[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
}
}
}
// 中心平均值曲线与「均值滤波」共用同一实现(DataSet 内的自由函数),
// 因此预览曲线与"构建为新信号"得到的采样值逐位一致。
ComputeMovingAverage(signal.values, radius, entry.values);
m_centerCurveCache.push_back(std::move(entry));
return m_centerCurveCache.back().values;
@@ -1567,6 +1573,13 @@ namespace jdi
return true;
}
bool PlotView::MeanFilterValueAt(const DataFile& file, const SignalData& signal, int radius,
double time, double& value) const
{
// 均值滤波曲线与中心平均值曲线是同一算法,取值路径完全一致。
return CenterValueAt(file, signal, radius, time, value);
}
void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int smooth,
bool hasRange, double rangeMin, double rangeMax,
std::size_t& rising, std::size_t& falling) const
@@ -1760,7 +1773,7 @@ namespace jdi
m_edgeCountCache.push_back(entry);
}
float PlotView::EdgePanelWidth(int laneIndex, bool centerAxis) const
float PlotView::EdgePanelWidth(int laneIndex, EdgePanelKind kind) const
{
if (m_tab == nullptr || m_fonts == nullptr || laneIndex < 0 ||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
@@ -1768,9 +1781,13 @@ namespace jdi
return kEdgePanelWidth * m_scale;
}
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” + 光标区间尾缀)。
const std::wstring title = (centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix();
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” + 光标区间尾缀)。
const std::wstring title =
meanFilter ? 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;
@@ -1785,6 +1802,11 @@ namespace jdi
continue;
}
const float nameWidth = UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->Title());
if (meanFilter)
{
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth);
continue;
}
std::size_t rising = 0;
std::size_t falling = 0;
if (centerAxis)
@@ -1804,17 +1826,26 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth));
}
if (centerAxis)
if (centerAxis || meanFilter)
{
// 底部编辑行 = “平滑点数”说明文字 + 数值框(与菜单内联编辑同一套排版习惯)。
// 底部编辑行 = “平滑点数 / 均值点数”说明文字 + 数值框(与菜单内联编辑同一套排版习惯)。
const int shown = meanFilter ? lane.meanFilterRadius : lane.edgeCenterSmooth;
const std::wstring label = meanFilter ? L"均值点数 N" : L"平滑点数 N";
const float editWidth =
UiMeasureTextWidth(m_fonts->uiSmall, L"平滑点数 N") + kEdgePanelEditFieldGap * m_scale +
UiMeasureTextWidth(m_fonts->uiSmall, label) + kEdgePanelEditFieldGap * m_scale +
std::max(kEdgePanelEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(lane.edgeCenterSmooth)) +
20.0f * m_scale);
UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(shown)) + 20.0f * m_scale);
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + editWidth);
}
if (meanFilter)
{
// 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。
const float buttonWidth =
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterBuildLabel) + 28.0f * m_scale;
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
}
// 夹在绘图区宽度之内(过窄的网格也不至于把面板挤到外面)。
return std::min(width, std::max(120.0f * m_scale, m_plotRect.Width - 8.0f * m_scale));
}
@@ -2383,9 +2414,10 @@ namespace jdi
return gd::RectF(m_editFieldRect.X, m_editFieldRect.Y, nameWidth, height);
}
if (m_editTarget == PlotEditTarget::EDGE_CENTER_SMOOTH)
if (m_editTarget == PlotEditTarget::EDGE_CENTER_SMOOTH ||
m_editTarget == PlotEditTarget::MEAN_FILTER_RADIUS)
{
// 「平滑点数 N」:输入框落在中心轴面板底部的编辑行内(靠右,与显示态对齐)。
// 「平滑点数 / 均值点数 N」:输入框落在面板底部的编辑行内(靠右,与显示态对齐)。
if (m_editFieldRect.Width <= 0.0f)
{
return gd::RectF();
@@ -2565,6 +2597,18 @@ namespace jdi
return true;
}
// 「均值点数 N」:只作用于均值滤波曲线的计算,夹在合理范围内(0–1000000)。
if (target == PlotEditTarget::MEAN_FILTER_RADIUS)
{
int radius = static_cast<int>(std::lround(parsed));
radius = std::max(0, std::min(radius, 1000000));
m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].meanFilterRadius = radius;
// 均值半径变化会改变曲线:清掉派生缓存后重绘(面板宽度也随之变化)。
m_centerCurveCache.clear();
InvalidateSceneCache();
return true;
}
// 直接输入上下限:先固化当前范围,再用新值替换对应端点(保证 max > min)。
if (laneIndex >= static_cast<int>(m_lanes.size()))
{
@@ -2781,12 +2825,11 @@ namespace jdi
// 面板**只有标题栏**可以拖拽(显示抓握光标);✕ 与面板主体一律保持原始箭头。
if (m_tab != nullptr)
{
for (int pass = 0; pass < 2; ++pass)
for (const EdgePanelKind kind : kEdgePanelKinds)
{
const bool centerAxis = (pass == 1);
for (int index = 0; index < static_cast<int>(m_lanes.size()); ++index)
{
const EdgePanelLayout layout = EdgePanelLayoutFor(index, centerAxis);
const EdgePanelLayout layout = EdgePanelLayoutFor(index, kind);
if (layout.panel.Width <= 0.0f ||
!UiPointInRect(layout.panel, static_cast<float>(x), static_cast<float>(y)))
{
@@ -2801,7 +2844,13 @@ namespace jdi
{
return LoadCursorW(nullptr, IDC_SIZEALL);
}
// 面板主体(含底部编辑行):保持原始箭头。
if (kind == EdgePanelKind::MEAN && layout.button.Width > 0.0f &&
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
{
// 「构建为新信号」按钮:手型光标作为可点击提示。
return LoadCursorW(nullptr, IDC_HAND);
}
// 面板主体(含底部编辑行与按钮):保持原始箭头。
return nullptr;
}
}
@@ -3029,7 +3078,7 @@ namespace jdi
break;
}
const gd::RectF panel =
EdgePanelLayoutFor(m_edgePanelDragLane, m_edgePanelDragCenter).panel;
EdgePanelLayoutFor(m_edgePanelDragLane, m_edgePanelDragKind).panel;
if (panel.Width <= 0.0f)
{
break;
@@ -3041,15 +3090,21 @@ namespace jdi
top = std::max(m_plotRect.Y + 2.0f * m_scale,
std::min(top, m_plotRect.Y + m_plotRect.Height - panel.Height - 2.0f * m_scale));
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_edgePanelDragLane)];
if (m_edgePanelDragCenter)
switch (m_edgePanelDragKind)
{
case EdgePanelKind::CENTER:
lane.edgeCenterPanelX = left;
lane.edgeCenterPanelY = top;
}
else
{
break;
case EdgePanelKind::MEAN:
lane.meanFilterPanelX = left;
lane.meanFilterPanelY = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanelX = left;
lane.edgePanelY = top;
break;
}
m_dragMoved = true;
return true;
@@ -3065,12 +3120,14 @@ namespace jdi
// 但该面板所属的网格仍然要高亮(与鼠标直接停在网格上一致)。
bool overEdgePanel = false;
int panelLane = -1;
for (int pass = 0; pass < 2 && !overEdgePanel; ++pass)
for (long kindIndex = 0;
kindIndex < static_cast<long>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0])) &&
!overEdgePanel;
++kindIndex)
{
const bool centerAxis = (pass == 1);
for (int index = 0; index < static_cast<int>(m_lanes.size()); ++index)
{
const gd::RectF panel = EdgePanelLayoutFor(index, centerAxis).panel;
const gd::RectF panel = EdgePanelLayoutFor(index, kEdgePanelKinds[kindIndex]).panel;
if (panel.Width > 0.0f &&
UiPointInRect(panel, static_cast<float>(x), static_cast<float>(y)))
{
@@ -3223,16 +3280,17 @@ namespace jdi
// 实验性功能浮动面板(面板压在波形之上,必须先于信号拖拽判定):
// * ✕ → 关闭该网格对应的功能;
// * 底部编辑行 → 进入「平滑点数 N」内联编辑;
// * 底部编辑行 → 进入「平滑点数 / 均值点数 N」内联编辑;
// * 底部按钮 → 触发「构建为新信号」请求;
// * 标题栏 → 拖动面板;
// * 面板主体 → 消费点击,但**不拖动也不穿透**(用户明确要求只有标题栏可拖拽)。
for (int pass = 0; pass < 2; ++pass)
for (int pass = 0; pass < 3; ++pass)
{
const bool centerAxis = (pass == 1);
const EdgePanelKind kind = kEdgePanelKinds[pass];
bool handledPanel = false;
for (int index = 0; index < static_cast<int>(m_lanes.size()); ++index)
{
const EdgePanelLayout layout = EdgePanelLayoutFor(index, centerAxis);
const EdgePanelLayout layout = EdgePanelLayoutFor(index, kind);
if (layout.panel.Width <= 0.0f ||
!UiPointInRect(layout.panel, fx, fy))
{
@@ -3243,25 +3301,52 @@ namespace jdi
if (UiPointInRect(layout.close, fx, fy))
{
// ✕ 关闭面板的同时关闭该网格对应的实验性功能。
if (centerAxis)
switch (kind)
{
case EdgePanelKind::CENTER:
SetLaneEdgeCenterCount(index, false);
}
else
{
break;
case EdgePanelKind::MEAN:
SetLaneMeanFilter(index, false);
break;
case EdgePanelKind::COUNT:
default:
SetLaneEdgeCount(index, false);
break;
}
m_dragMode = PlotDragMode::NONE;
return true;
}
if (centerAxis && layout.edit.Width > 0.0f && UiPointInRect(layout.edit, fx, fy) &&
m_tab != nullptr)
if ((kind == EdgePanelKind::CENTER || kind == EdgePanelKind::MEAN) &&
layout.edit.Width > 0.0f && UiPointInRect(layout.edit, fx, fy) && m_tab != nullptr)
{
// 底部编辑行 → 就地编辑平滑点数 N(输入框落在该行内)。
const int smooth = m_tab->Lanes()[static_cast<std::size_t>(index)].edgeCenterSmooth;
BeginEdit(PlotEditTarget::EDGE_CENTER_SMOOTH, index, std::to_wstring(smooth),
layout.edit);
// 底部编辑行 → 就地编辑参数 N(输入框落在该行内)。
const Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
if (kind == EdgePanelKind::MEAN)
{
BeginEdit(PlotEditTarget::MEAN_FILTER_RADIUS, index,
std::to_wstring(tabLane.meanFilterRadius), layout.edit);
}
else
{
BeginEdit(PlotEditTarget::EDGE_CENTER_SMOOTH, index,
std::to_wstring(tabLane.edgeCenterSmooth), layout.edit);
}
return true;
}
if (kind == EdgePanelKind::MEAN && layout.button.Width > 0.0f &&
UiPointInRect(layout.button, fx, fy) && m_tab != nullptr)
{
// 「构建为新信号」:只登记请求,真正的信号构建由宿主(Application)完成。
SetHint(L"正在构建均值滤波信号(" +
std::to_wstring(m_tab->Lanes()[static_cast<std::size_t>(index)].meanFilterRadius) +
L" 点均值)…");
m_pendingMeanFilterLane = index;
m_pendingMeanFilterRadius =
m_tab->Lanes()[static_cast<std::size_t>(index)].meanFilterRadius;
m_dragMode = PlotDragMode::NONE;
return true;
}
@@ -3269,7 +3354,7 @@ namespace jdi
{
m_dragMode = PlotDragMode::EDGE_PANEL_MOVE;
m_edgePanelDragLane = index;
m_edgePanelDragCenter = centerAxis;
m_edgePanelDragKind = kind;
m_edgePanelDragOffset.X = fx - layout.panel.X;
m_edgePanelDragOffset.Y = fy - layout.panel.Y;
return true;
@@ -3363,7 +3448,7 @@ namespace jdi
m_cursorGrabOffset = 0;
m_edgeDragLane = -1;
m_edgePanelDragLane = -1;
m_edgePanelDragCenter = false;
m_edgePanelDragKind = EdgePanelKind::COUNT;
SetHint(std::wstring());
m_dragLane = LaneAtY(y);
m_dragMode = PlotDragMode::PAN;
@@ -3440,7 +3525,7 @@ namespace jdi
m_cursorGrabOffset = 0;
m_edgeDragLane = -1;
m_edgePanelDragLane = -1;
m_edgePanelDragCenter = false;
m_edgePanelDragKind = EdgePanelKind::COUNT;
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
ReleaseHint();
(void)x;
@@ -4253,6 +4338,9 @@ namespace jdi
// 「边沿计数(中心轴)」的开关与平滑点数会改变网格内的曲线绘制 → 参与指纹。
mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterSmooth));
// 「均值滤波」的开关与均值点数同样会改变网格内的曲线绘制 → 参与指纹。
mix(Tab().Lanes()[laneIndex].meanFilter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].meanFilterRadius));
mix(signals.size());
for (const SignalRef& ref : signals)
{
@@ -4610,6 +4698,11 @@ namespace jdi
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].edgeCenter
? Tab().Lanes()[static_cast<std::size_t>(laneIndex)].edgeCenterSmooth
: -1;
// 「均值滤波」开启时同样叠加一条 N 点滑动平均曲线(与中心平均值曲线同一算法)。
const int meanFilterRadius =
Tab().Lanes()[static_cast<std::size_t>(laneIndex)].meanFilter
? Tab().Lanes()[static_cast<std::size_t>(laneIndex)].meanFilterRadius
: -1;
for (const SignalRef& ref : laneSignals)
{
@@ -4729,6 +4822,11 @@ namespace jdi
{
PaintCenterCurve(g, resolved, lane, centerSmooth);
}
// 均值滤波曲线(「实验性功能 → 均值滤波」):同样是仅用于预览的叠加曲线。
if (meanFilterRadius >= 0)
{
PaintMeanFilterCurve(g, resolved, lane, meanFilterRadius);
}
}
// g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。
@@ -5306,68 +5404,87 @@ namespace jdi
UiText(g, text, m_fonts->monoSmall, labelRect,
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
// 「边沿计数(中心轴)」开启时,纵向光标还要额外标注**中心平均值曲线**
// 「边沿计数(中心轴)」/「均值滤波」开启时,纵向光标还要额外标注**派生曲线**
// 在该时刻的取值(它不是信号,因此单独标注,不并入上面的取值文案)。
if (m_tab->Lanes()[laneIndex].edgeCenter)
// 两条曲线的标注规则完全一致,故共用同一段绘制逻辑。
const auto paintDerivedCapsule = [&](int smooth, const std::wstring& suffix,
bool meanFilter)
{
const int smooth = m_tab->Lanes()[laneIndex].edgeCenterSmooth;
double centerValue = 0.0;
if (CenterValueAt(*resolved.file, *resolved.data, smooth, time, centerValue))
double derivedValue = 0.0;
const bool ok = meanFilter
? MeanFilterValueAt(*resolved.file, *resolved.data, smooth,
time, derivedValue)
: CenterValueAt(*resolved.file, *resolved.data, smooth, time,
derivedValue);
if (!ok)
{
const float rawCenterY = ValueToY(centerValue, lane);
const DataMarker centerMarker = MakeDataMarker(
rawX, rawCenterY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
return;
}
const float rawCenterY = ValueToY(derivedValue, lane);
const DataMarker centerMarker = MakeDataMarker(
rawX, rawCenterY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
// 派生数据点的配色与原始信号一致(便于一一对应);
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
const std::uint32_t centerColor = StyleColor(*resolved.style);
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
// 取值文本:数值在前,尾缀「(中心轴)/(均值滤波)」;双光标的右侧光标
// 还要附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。
std::wstring centerText = FormatValue(derivedValue);
centerText += L"(";
if (showDelta)
{
double otherDerived = 0.0;
const bool otherOk =
meanFilter
? MeanFilterValueAt(*resolved.file, *resolved.data, smooth,
otherTime, otherDerived)
: CenterValueAt(*resolved.file, *resolved.data, smooth, otherTime,
otherDerived);
if (otherOk)
{
// 中心轴数据点的配色与原始信号一致(便于一一对应);
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
const std::uint32_t centerColor = StyleColor(*resolved.style);
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
// 取值文本:数值在前,尾缀「(中心轴)」;双光标的右侧光标还要
// 附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。
std::wstring centerText = FormatValue(centerValue);
centerText += L"(";
if (showDelta)
{
double otherCenter = 0.0;
if (CenterValueAt(*resolved.file, *resolved.data, smooth, otherTime,
otherCenter))
{
const double delta = centerValue - otherCenter;
centerText += (delta < 0.0) ? L"-" : L"+";
centerText += FormatValue(std::fabs(delta));
centerText += L" ";
}
}
centerText += L"中心轴)";
const float centerTextWidth =
UiTextWidth(g, centerText, m_fonts->monoSmall) + 10.0f * m_scale;
float centerLeft = pinX + radius + 4.0f * m_scale;
if (centerLeft + centerTextWidth >
m_plotRect.X + m_plotRect.Width - 2.0f * m_scale)
{
centerLeft = pinX - radius - 4.0f * m_scale - centerTextWidth;
}
centerLeft = std::max(
m_plotRect.X + 2.0f * m_scale,
std::min(centerLeft, m_plotRect.X + m_plotRect.Width -
centerTextWidth - 2.0f * m_scale));
float centerTop = centerMarker.y - textHeight * 0.5f;
centerTop = std::max(
clipTop + 1.0f * m_scale,
std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale));
gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight);
PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom);
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
const bool centerDark = IsDarkColor(centerColor);
UiFillRoundRect(g, centerRect, 3.0f * m_scale,
WithAlpha(centerColor, 0xE0));
DrawLabelOutline(g, centerRect, 3.0f * m_scale, centerDark);
UiText(g, centerText, m_fonts->monoSmall, centerRect,
centerDark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
const double delta = derivedValue - otherDerived;
centerText += (delta < 0.0) ? L"-" : L"+";
centerText += FormatValue(std::fabs(delta));
centerText += L" ";
}
}
centerText += suffix;
centerText += L")";
const float centerTextWidth =
UiTextWidth(g, centerText, m_fonts->monoSmall) + 10.0f * m_scale;
float centerLeft = pinX + radius + 4.0f * m_scale;
if (centerLeft + centerTextWidth >
m_plotRect.X + m_plotRect.Width - 2.0f * m_scale)
{
centerLeft = pinX - radius - 4.0f * m_scale - centerTextWidth;
}
centerLeft = std::max(
m_plotRect.X + 2.0f * m_scale,
std::min(centerLeft,
m_plotRect.X + m_plotRect.Width - centerTextWidth - 2.0f * m_scale));
float centerTop = centerMarker.y - textHeight * 0.5f;
centerTop = std::max(clipTop + 1.0f * m_scale,
std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale));
gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight);
PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom);
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
const bool centerDark = IsDarkColor(centerColor);
UiFillRoundRect(g, centerRect, 3.0f * m_scale, WithAlpha(centerColor, 0xE0));
DrawLabelOutline(g, centerRect, 3.0f * m_scale, centerDark);
UiText(g, centerText, m_fonts->monoSmall, centerRect,
centerDark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER);
};
if (m_tab->Lanes()[laneIndex].edgeCenter)
{
paintDerivedCapsule(m_tab->Lanes()[laneIndex].edgeCenterSmooth, L"中心轴", false);
}
if (m_tab->Lanes()[laneIndex].meanFilter)
{
paintDerivedCapsule(m_tab->Lanes()[laneIndex].meanFilterRadius, L"均值滤波", true);
}
}
}
@@ -5424,7 +5541,7 @@ namespace jdi
}
}
PlotView::EdgePanelLayout PlotView::EdgePanelLayoutFor(int laneIndex, bool centerAxis) const
PlotView::EdgePanelLayout PlotView::EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const
{
EdgePanelLayout layout;
if (m_tab == nullptr || m_fonts == nullptr || laneIndex < 0 ||
@@ -5434,8 +5551,11 @@ namespace jdi
return layout;
}
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool enabled = centerAxis ? lane.edgeCenter : lane.edgeCount;
const bool open = centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen;
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 open = meanFilter ? lane.meanFilterPanelOpen
: (centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen);
if (!enabled || !open)
{
return layout;
@@ -5456,18 +5576,23 @@ namespace jdi
}
layout.rows = rowCount;
const float width = EdgePanelWidth(laneIndex, centerAxis);
const float width = EdgePanelWidth(laneIndex, kind);
const float titleHeight = kEdgePanelTitleHeight * m_scale;
const float rowHeight = kEdgePanelRowHeight * m_scale;
const float pad = kEdgePanelPad * m_scale;
// 中心轴面板在底部固定多出一行「平滑点数」编辑行。
const float editHeight = centerAxis ? (kEdgePanelEditHeight * m_scale) : 0.0f;
const float height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * 2.0f + pad +
editHeight;
// 中心轴 / 均值滤波面板在底部固定多出一行编辑行;均值滤波再多一个按钮行。
const float editHeight =
(centerAxis || meanFilter) ? (kEdgePanelEditHeight * m_scale) : 0.0f;
const float buttonHeight = meanFilter ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
const float rowsPerSignal = meanFilter ? 1.0f : 2.0f;
const float height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal +
pad + editHeight + buttonHeight;
// 首次使用(位置为负)时自动放到绘图区右侧,并各网格依次错开,避免完全重叠。
const double storedX = centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX;
const double storedY = centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY;
const double storedX = meanFilter ? lane.meanFilterPanelX
: (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX);
const double storedY = meanFilter ? lane.meanFilterPanelY
: (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);
@@ -5487,28 +5612,35 @@ namespace jdi
layout.title = gd::RectF(left, top, width, titleHeight);
const float closeSize = titleHeight;
layout.close = gd::RectF(left + width - closeSize, top, closeSize, closeSize);
if (centerAxis)
if (centerAxis || meanFilter)
{
const float editRowHeight = kEdgePanelEditHeight * m_scale;
layout.edit = gd::RectF(left + pad, top + height - editRowHeight,
std::max(0.0f, width - pad * 2.0f), editRowHeight);
const float editTop = top + height - buttonHeight - editRowHeight;
layout.edit = gd::RectF(left + pad, editTop, std::max(0.0f, width - pad * 2.0f),
editRowHeight);
}
if (meanFilter)
{
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
layout.button = gd::RectF(left + pad, top + height - buttonRowHeight,
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
}
return layout;
}
gd::RectF PlotView::EdgePanelRect(int laneIndex) const
{
return EdgePanelLayoutFor(laneIndex, false).panel;
return EdgePanelLayoutFor(laneIndex, EdgePanelKind::COUNT).panel;
}
gd::RectF PlotView::EdgePanelCloseRect(int laneIndex) const
{
return EdgePanelLayoutFor(laneIndex, false).close;
return EdgePanelLayoutFor(laneIndex, EdgePanelKind::COUNT).close;
}
gd::RectF PlotView::EdgeCenterPanelRect(int laneIndex) const
{
return EdgePanelLayoutFor(laneIndex, true).panel;
return EdgePanelLayoutFor(laneIndex, EdgePanelKind::CENTER).panel;
}
bool PlotView::IsOnEdgeCursorLine(int x, int y, int& laneIndex) const
@@ -5785,6 +5917,14 @@ namespace jdi
}
}
void PlotView::PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved,
const LaneVisual& lane, int radius) const
{
// 均值滤波曲线与「边沿计数(中心轴)」的中心平均值曲线是**同一算法、同一视觉效果**
// (同色相高对比度的派生配色 + 同透明度),因此直接复用同一段绘制逻辑。
PaintCenterCurve(g, resolved, lane, radius);
}
void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
{
if (m_tab == nullptr || m_dataSet == nullptr)
@@ -5858,14 +5998,17 @@ namespace jdi
}
// ---- 统计浮动面板(最后绘制,覆盖在波形之上)----
// 「边沿计数」与「边沿计数(中心轴)」共用同一套样式,仅标题、统计口径与底部编辑行不同。
for (int pass = 0; pass < 2; ++pass)
// 「边沿计数」「边沿计数(中心轴)」「均值滤波」共用同一套样式,
// 仅标题、内容行与底部编辑行/按钮不同。
for (int pass = 0; pass < 3; ++pass)
{
const bool centerAxis = (pass == 1);
const EdgePanelKind kind = kEdgePanelKinds[pass];
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
for (std::size_t index = 0; index < count; ++index)
{
const Lane& tabLane = tabLanes[index];
const EdgePanelLayout layout = EdgePanelLayoutFor(static_cast<int>(index), centerAxis);
const EdgePanelLayout layout = EdgePanelLayoutFor(static_cast<int>(index), kind);
const gd::RectF& panel = layout.panel;
if (panel.Width <= 0.0f || panel.Height <= 0.0f)
{
@@ -5895,7 +6038,8 @@ namespace jdi
UiText(g, L"G" + std::to_wstring(index + 1), m_fonts->monoSmall, badgeRect,
m_theme->text, TextAlign::CENTER);
const std::wstring title =
(centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix();
meanFilter ? 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,
@@ -5923,7 +6067,8 @@ namespace jdi
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
1.6f * m_scale);
// 每个信号两行:名称(信号色)+ 上升沿/下降沿数量。
// 每个信号两行:名称(信号色)+ 上升沿/下降沿数量;
// 均值滤波面板只需列出被处理的信号名(一行)。
float rowY = panel.Y + titleHeight + pad;
for (const SignalRef& ref : tabLane.signals)
{
@@ -5938,6 +6083,11 @@ namespace jdi
UiText(g, resolved.data->Title(), m_fonts->uiSmall,
gd::RectF(panel.X + pad, rowY, textWidth, rowHeight), signalColor,
TextAlign::LEFT);
if (meanFilter)
{
rowY += rowHeight;
continue;
}
std::size_t rising = 0;
std::size_t falling = 0;
@@ -5960,32 +6110,33 @@ namespace jdi
rowY += rowHeight * 2.0f;
}
// 中心轴面板底部固定一行「平滑点数」编辑设置(始终位于面板最下方)。
if (centerAxis)
// 中心轴 / 均值滤波面板底部固定一行参数编辑设置(始终位于面板最下方)。
if (centerAxis || meanFilter)
{
const gd::RectF& editRect = layout.edit;
if (editRect.Width > 0.0f)
{
const float fieldGap = kEdgePanelEditFieldGap * m_scale;
const std::wstring label = L"平滑点数 N";
const int shownValue = meanFilter ? tabLane.meanFilterRadius
: tabLane.edgeCenterSmooth;
const std::wstring label = meanFilter ? L"均值点数 N" : L"平滑点数 N";
const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
UiText(g, label, m_fonts->uiSmall,
gd::RectF(editRect.X, editRect.Y, labelWidth, editRect.Height),
m_theme->text, TextAlign::LEFT);
const float fieldWidth = std::max(
kEdgePanelEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->monoSmall,
std::to_wstring(tabLane.edgeCenterSmooth)) +
20.0f * m_scale);
const float fieldWidth =
std::max(kEdgePanelEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(shownValue)) +
20.0f * m_scale);
const float fieldLeft = editRect.X + editRect.Width - fieldWidth;
const float fieldHeight = editRect.Height - 6.0f * m_scale;
const gd::RectF fieldRect(fieldLeft, editRect.Y + 3.0f * m_scale, fieldWidth,
fieldHeight);
const PlotEditTarget editTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_RADIUS
: PlotEditTarget::EDGE_CENTER_SMOOTH;
const bool editingThis =
(m_editTarget == PlotEditTarget::EDGE_CENTER_SMOOTH &&
m_editLane == static_cast<int>(index));
(m_editTarget == editTarget && m_editLane == static_cast<int>(index));
if (editingThis)
{
UiDrawEditField(g, fieldRect, m_edit, m_fonts->monoSmall, m_scale);
@@ -5994,11 +6145,29 @@ namespace jdi
{
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
UiText(g, std::to_wstring(tabLane.edgeCenterSmooth), m_fonts->monoSmall,
fieldRect, m_theme->textStrong, TextAlign::CENTER);
UiText(g, std::to_wstring(shownValue), m_fonts->monoSmall, fieldRect,
m_theme->textStrong, TextAlign::CENTER);
}
(void)labelWidth;
(void)fieldGap;
}
}
// 均值滤波面板底部按钮行:「构建为新信号」(悬停高亮)。
if (meanFilter)
{
const gd::RectF& buttonRect = layout.button;
if (buttonRect.Width > 0.0f)
{
const gd::RectF inner(buttonRect.X, buttonRect.Y + 2.0f * m_scale,
buttonRect.Width, buttonRect.Height - 4.0f * m_scale);
const bool hot =
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
UiPointInRect(inner, static_cast<float>(m_mouseX),
static_cast<float>(m_mouseY));
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,
TextAlign::CENTER);
}
}
}