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

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

@@ -227,6 +227,27 @@ namespace jdi
*/ */
bool ApplyPendingRename(); bool ApplyPendingRename();
/**
* @brief 应用绘图区提交的「均值滤波 → 构建为新信号」请求。
*
* 为指定网格内的每条可见信号各构建一条 N 点均值滤波信号,命名为
* `<原信号名称> (N点均值)`,插入到原信号所在组的原信号位置**下方**,
* 并把新信号所在的网格移动到原信号网格的下方。
*
* @return 实际构建了新信号返回 true。
*/
bool ApplyPendingMeanFilterBuild();
/**
* @brief 把一条均值滤波派生信号插入数据层并同步各视图。
* @param [IN] fileIndex 原信号所属文件下标。
* @param [IN] signalIndex 原信号下标。
* @param [IN] radius 均值滤波半径 N。
* @param [IN] makeVisible 是否在当前标签页中勾选新信号。
* @return 构建并插入成功返回 true。
*/
bool BuildMeanFilterSignal(int fileIndex, int signalIndex, int radius, bool makeVisible);
/** /**
* @brief 弹出文件选择对话框并加载所选文件。 * @brief 弹出文件选择对话框并加载所选文件。
*/ */
+118
View File
@@ -1398,6 +1398,112 @@ namespace jdi
return true; 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) void Application::ShowTabContextMenu(const TabBarResult& result)
{ {
if (result.index < 0 || result.index >= static_cast<int>(m_tabs.size())) 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.id = PLOT_CMD_LANE_EDGE_CENTER;
edgeCenter.checked = m_plot.LaneEdgeCenterCount(laneIndex); edgeCenter.checked = m_plot.LaneEdgeCenterCount(laneIndex);
experimental.submenu.push_back(edgeCenter); 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); items.push_back(experimental);
@@ -1629,6 +1741,10 @@ namespace jdi
// 「实验性功能 → 边沿计数(中心轴)」:切换中心平均值曲线与统计面板。 // 「实验性功能 → 边沿计数(中心轴)」:切换中心平均值曲线与统计面板。
m_plot.ToggleLaneEdgeCenterCount(laneIndex); m_plot.ToggleLaneEdgeCenterCount(laneIndex);
break; break;
case PLOT_CMD_LANE_MEAN_FILTER:
// 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。
m_plot.ToggleLaneMeanFilter(laneIndex);
break;
case PLOT_CMD_LANE_SPLIT: case PLOT_CMD_LANE_SPLIT:
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。 // 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
m_plot.SplitLane(laneIndex); m_plot.SplitLane(laneIndex);
@@ -2116,6 +2232,8 @@ namespace jdi
{ {
// 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。 // 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。
ApplyPendingRename(); ApplyPendingRename();
// 「均值滤波 → 构建为新信号」按钮:立即构建新信号并同步各视图。
ApplyPendingMeanFilterBuild();
InvalidateRect(hwnd, nullptr, FALSE); InvalidateRect(hwnd, nullptr, FALSE);
} }
// 拖动期间 Windows 不再派发 WM_SETCURSOR,需在按下后立即刷新一次, // 拖动期间 Windows 不再派发 WM_SETCURSOR,需在按下后立即刷新一次,
+80 -8
View File
@@ -53,7 +53,8 @@ namespace jdi
LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */ LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */
LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */ LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */
SIGNAL_NAME, /**< 编辑图例中某信号的显示名称。 */ SIGNAL_NAME, /**< 编辑图例中某信号的显示名称。 */
EDGE_CENTER_SMOOTH /**< 编辑「中心轴边沿计数」的平滑点数 N。 */ EDGE_CENTER_SMOOTH, /**< 编辑「中心轴边沿计数」的平滑点数 N。 */
MEAN_FILTER_RADIUS /**< 编辑「均值滤波」的均值点数 N。 */
}; };
/** /**
@@ -69,7 +70,18 @@ namespace jdi
PLOT_CMD_LANE_AUTO_VIEW = 6, /**< 切换本网格的「选区 Y 轴自适应」开关。 */ PLOT_CMD_LANE_AUTO_VIEW = 6, /**< 切换本网格的「选区 Y 轴自适应」开关。 */
PLOT_CMD_LANE_EDGE_COUNT = 7, /**< 切换本网格的「实验性功能 → 边沿计数」开关。 */ PLOT_CMD_LANE_EDGE_COUNT = 7, /**< 切换本网格的「实验性功能 → 边沿计数」开关。 */
PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */ PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */
PLOT_CMD_LANE_SPLIT = 9 /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */ PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
PLOT_CMD_LANE_MEAN_FILTER = 10 /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
};
/**
* @brief 实验性功能浮动面板的种类。
*/
enum class EdgePanelKind : int
{
COUNT = 0, /**< 边沿计数。 */
CENTER = 1, /**< 边沿计数(中心轴)。 */
MEAN = 2 /**< 均值滤波。 */
}; };
/** /**
@@ -462,6 +474,40 @@ namespace jdi
*/ */
bool ToggleLaneEdgeCenterCount(int laneIndex); bool ToggleLaneEdgeCenterCount(int laneIndex);
/**
* @brief 判断某网格是否开启了「实验性功能 → 均值滤波」。
* @param [IN] laneIndex 网格下标。
* @return 开启返回 true。
*/
bool LaneMeanFilter(int laneIndex) const;
/**
* @brief 设置某网格的「均值滤波」开关。
* @param [IN] laneIndex 网格下标。
* @param [IN] enabled 目标状态。
* @return 状态发生变化返回 true。
*/
bool SetLaneMeanFilter(int laneIndex, bool enabled);
/**
* @brief 切换某网格的「均值滤波」开关。
* @param [IN] laneIndex 网格下标。
* @return 切换后的状态。
*/
bool ToggleLaneMeanFilter(int laneIndex);
/**
* @brief 取出「构建为新信号」请求(由面板底部按钮触发)。
*
* 请求只描述「哪一网格、用多大的 N」,真正的信号构建与插入由宿主(Application)
* 完成——数据层属于 DataSet,绘图区不能直接修改它。
*
* @param [OUT] laneIndex 触发该请求的网格下标。
* @param [OUT] radius 均值滤波半径 N。
* @return 存在待处理请求返回 true(取出后清空)。
*/
bool TakePendingMeanFilterBuild(int& laneIndex, int& radius);
/** /**
* @brief 进入某网格 Y 轴上限/下限的内联输入状态。 * @brief 进入某网格 Y 轴上限/下限的内联输入状态。
* @param [IN] laneIndex 网格下标。 * @param [IN] laneIndex 网格下标。
@@ -913,7 +959,7 @@ namespace jdi
bool IsOnEdgeCursorLine(int x, int y, int& laneIndex) const; bool IsOnEdgeCursorLine(int x, int y, int& laneIndex) const;
/** /**
* @brief 实验性功能浮动面板的几何布局(边沿计数 / 中心轴边沿计数共用同一套样式)。 * @brief 实验性功能浮动面板的几何布局(三类面板共用同一套样式)。
*/ */
struct EdgePanelLayout struct EdgePanelLayout
{ {
@@ -921,16 +967,17 @@ namespace jdi
gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */ gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */
gd::RectF close; /**< ✕ 关闭按钮。 */ gd::RectF close; /**< ✕ 关闭按钮。 */
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */ gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */ int rows = 0; /**< 统计行数(每条可见信号一行)。 */
}; };
/** /**
* @brief 计算某个实验性功能面板的布局。 * @brief 计算某个实验性功能面板的布局。
* @param [IN] laneIndex 网格下标。 * @param [IN] laneIndex 网格下标。
* @param [IN] centerAxis 为 true 时取「边沿计数(中心轴)」面板,否则取「边沿计数」。 * @param [IN] kind 面板种类。
* @return 布局;面板未展开时各项均为空矩形。 * @return 布局;面板未展开时各项均为空矩形。
*/ */
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, bool centerAxis) const; EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
/** /**
* @brief 获取「边沿计数(中心轴)」浮动面板的矩形。 * @brief 获取「边沿计数(中心轴)」浮动面板的矩形。
@@ -984,6 +1031,28 @@ namespace jdi
void PaintCenterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane, void PaintCenterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int smooth) const; int smooth) const;
/**
* @brief 绘制某条信号的均值滤波曲线(与中心平均值曲线同一算法与同一配色派生规则)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] radius 均值滤波半径 N。
*/
void PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int radius) const;
/**
* @brief 取某个时刻的均值滤波曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] radius 均值滤波半径 N。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool MeanFilterValueAt(const DataFile& file, const SignalData& signal, int radius, double time,
double& value) const;
/** /**
* @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。 * @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
*/ */
@@ -1066,10 +1135,10 @@ namespace jdi
/** /**
* @brief 计算实验性功能面板所需的宽度(排版与绘制共用,保证文字完整不被省略)。 * @brief 计算实验性功能面板所需的宽度(排版与绘制共用,保证文字完整不被省略)。
* @param [IN] laneIndex 网格下标。 * @param [IN] laneIndex 网格下标。
* @param [IN] centerAxis 为 true 时取「边沿计数(中心轴)」面板。 * @param [IN] kind 面板种类。
* @return 面板宽度(像素)。 * @return 面板宽度(像素)。
*/ */
float EdgePanelWidth(int laneIndex, bool centerAxis) const; float EdgePanelWidth(int laneIndex, EdgePanelKind kind) const;
/** /**
* @brief 判断边沿统计是否限定在双光标区间内(双光标模式)。 * @brief 判断边沿统计是否限定在双光标区间内(双光标模式)。
@@ -1205,6 +1274,9 @@ namespace jdi
std::wstring m_pendingRenameName; /**< 待宿主窗口应用的信号新名称。 */ std::wstring m_pendingRenameName; /**< 待宿主窗口应用的信号新名称。 */
bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */ bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */
int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */
int m_pendingMeanFilterRadius = 0; /**< 待构建的均值滤波半径 N。 */
int m_hoverLane = -1; /**< 当前悬停网格下标。 */ int m_hoverLane = -1; /**< 当前悬停网格下标。 */
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */ int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */ int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
@@ -1257,7 +1329,7 @@ namespace jdi
// 「边沿计数」横向光标 / 面板的拖拽状态。 // 「边沿计数」横向光标 / 面板的拖拽状态。
int m_edgeDragLane = -1; /**< 正在拖动的横向光标所属网格。 */ int m_edgeDragLane = -1; /**< 正在拖动的横向光标所属网格。 */
int m_edgePanelDragLane = -1; /**< 正在拖动的统计面板所属网格。 */ int m_edgePanelDragLane = -1; /**< 正在拖动的统计面板所属网格。 */
bool m_edgePanelDragCenter = false; /**< 正在拖动的面板是否为「中心轴边沿计数」面板。 */ EdgePanelKind m_edgePanelDragKind = EdgePanelKind::COUNT; /**< 正在拖动的面板种类。 */
gd::PointF m_edgePanelDragOffset; /**< 抓取面板时的相对偏移(保持不跳变)。 */ gd::PointF m_edgePanelDragOffset; /**< 抓取面板时的相对偏移(保持不跳变)。 */
/** /**
+369 -200
View File
@@ -55,9 +55,19 @@ namespace jdi
const float kEdgePanelEditHeight = 30.0f; /**< 中心轴面板底部编辑行高度。 */ const float kEdgePanelEditHeight = 30.0f; /**< 中心轴面板底部编辑行高度。 */
const float kEdgePanelEditFieldWidth = 64.0f; /**< 编辑行数值框的最小宽度。 */ const float kEdgePanelEditFieldWidth = 64.0f; /**< 编辑行数值框的最小宽度。 */
const float kEdgePanelEditFieldGap = 8.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 kHintHoldMs = 1300; /**< 提示浮层完全显示的停留时长(毫秒)。 */
const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */ const std::uint64_t kHintFadeMs = 500; /**< 提示浮层的淡出时长(毫秒)。 */
/**
* @brief 实验性功能浮动面板的遍历顺序(命中测试与绘制共用,保证层序一致)。
*
* 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。
*/
const EdgePanelKind kEdgePanelKinds[] = {
EdgePanelKind::COUNT, EdgePanelKind::CENTER, EdgePanelKind::MEAN};
/** /**
* @brief 时间间隔的显示单位。 * @brief 时间间隔的显示单位。
*/ */
@@ -949,6 +959,11 @@ namespace jdi
m_edgeCountCache.clear(); m_edgeCountCache.clear();
m_edgeCenterCountCache.clear(); m_edgeCenterCountCache.clear();
m_centerCurveCache.clear(); m_centerCurveCache.clear();
// 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。
m_hoverSignal = SignalRef();
m_hoverLegend = SignalRef();
m_dragSignal = SignalRef();
m_dragTargetLane = -1;
InvalidateSceneCache(); InvalidateSceneCache();
} }
@@ -1458,6 +1473,62 @@ namespace jdi
return enabled; 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 std::vector<float>& PlotView::CenterCurveFor(const SignalData& signal, int smooth) const
{ {
const int radius = std::max(0, smooth); const int radius = std::max(0, smooth);
@@ -1472,74 +1543,9 @@ namespace jdi
CenterCurveEntry entry; CenterCurveEntry entry;
entry.signal = &signal; entry.signal = &signal;
entry.smooth = radius; entry.smooth = radius;
entry.values.assign(signal.values.size(), std::numeric_limits<float>::quiet_NaN()); // 中心平均值曲线与「均值滤波」共用同一实现(DataSet 内的自由函数),
// 因此预览曲线与"构建为新信号"得到的采样值逐位一致。
const std::size_t count = signal.values.size(); ComputeMovingAverage(signal.values, radius, entry.values);
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));
}
}
}
m_centerCurveCache.push_back(std::move(entry)); m_centerCurveCache.push_back(std::move(entry));
return m_centerCurveCache.back().values; return m_centerCurveCache.back().values;
@@ -1567,6 +1573,13 @@ namespace jdi
return true; 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, void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, int smooth,
bool hasRange, double rangeMin, double rangeMax, bool hasRange, double rangeMin, double rangeMax,
std::size_t& rising, std::size_t& falling) const std::size_t& rising, std::size_t& falling) const
@@ -1760,7 +1773,7 @@ namespace jdi
m_edgeCountCache.push_back(entry); 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 || if (m_tab == nullptr || m_fonts == nullptr || laneIndex < 0 ||
laneIndex >= static_cast<int>(m_tab->Lanes().size())) laneIndex >= static_cast<int>(m_tab->Lanes().size()))
@@ -1768,9 +1781,13 @@ namespace jdi
return kEdgePanelWidth * m_scale; return kEdgePanelWidth * m_scale;
} }
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)]; 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” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” + 光标区间尾缀)。 // 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” + 光标区间尾缀)。
const std::wstring title = (centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix(); const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix());
float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale + float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale +
8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) + 8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) +
kEdgePanelTitleHeight * m_scale; kEdgePanelTitleHeight * m_scale;
@@ -1785,6 +1802,11 @@ namespace jdi
continue; continue;
} }
const float nameWidth = UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->Title()); 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 rising = 0;
std::size_t falling = 0; std::size_t falling = 0;
if (centerAxis) if (centerAxis)
@@ -1804,17 +1826,26 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth)); 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 = 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, std::max(kEdgePanelEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(lane.edgeCenterSmooth)) + UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(shown)) + 20.0f * m_scale);
20.0f * m_scale);
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + editWidth); 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)); 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); 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) if (m_editFieldRect.Width <= 0.0f)
{ {
return gd::RectF(); return gd::RectF();
@@ -2565,6 +2597,18 @@ namespace jdi
return true; 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)。 // 直接输入上下限:先固化当前范围,再用新值替换对应端点(保证 max > min)。
if (laneIndex >= static_cast<int>(m_lanes.size())) if (laneIndex >= static_cast<int>(m_lanes.size()))
{ {
@@ -2781,12 +2825,11 @@ namespace jdi
// 面板**只有标题栏**可以拖拽(显示抓握光标);✕ 与面板主体一律保持原始箭头。 // 面板**只有标题栏**可以拖拽(显示抓握光标);✕ 与面板主体一律保持原始箭头。
if (m_tab != nullptr) 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) 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 || if (layout.panel.Width <= 0.0f ||
!UiPointInRect(layout.panel, static_cast<float>(x), static_cast<float>(y))) !UiPointInRect(layout.panel, static_cast<float>(x), static_cast<float>(y)))
{ {
@@ -2801,7 +2844,13 @@ namespace jdi
{ {
return LoadCursorW(nullptr, IDC_SIZEALL); 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; return nullptr;
} }
} }
@@ -3029,7 +3078,7 @@ namespace jdi
break; break;
} }
const gd::RectF panel = const gd::RectF panel =
EdgePanelLayoutFor(m_edgePanelDragLane, m_edgePanelDragCenter).panel; EdgePanelLayoutFor(m_edgePanelDragLane, m_edgePanelDragKind).panel;
if (panel.Width <= 0.0f) if (panel.Width <= 0.0f)
{ {
break; break;
@@ -3041,15 +3090,21 @@ namespace jdi
top = std::max(m_plotRect.Y + 2.0f * m_scale, 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)); 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)]; 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.edgeCenterPanelX = left;
lane.edgeCenterPanelY = top; lane.edgeCenterPanelY = top;
} break;
else case EdgePanelKind::MEAN:
{ lane.meanFilterPanelX = left;
lane.meanFilterPanelY = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanelX = left; lane.edgePanelX = left;
lane.edgePanelY = top; lane.edgePanelY = top;
break;
} }
m_dragMoved = true; m_dragMoved = true;
return true; return true;
@@ -3065,12 +3120,14 @@ namespace jdi
// 但该面板所属的网格仍然要高亮(与鼠标直接停在网格上一致)。 // 但该面板所属的网格仍然要高亮(与鼠标直接停在网格上一致)。
bool overEdgePanel = false; bool overEdgePanel = false;
int panelLane = -1; 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) 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 && if (panel.Width > 0.0f &&
UiPointInRect(panel, static_cast<float>(x), static_cast<float>(y))) 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; bool handledPanel = false;
for (int index = 0; index < static_cast<int>(m_lanes.size()); ++index) 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 || if (layout.panel.Width <= 0.0f ||
!UiPointInRect(layout.panel, fx, fy)) !UiPointInRect(layout.panel, fx, fy))
{ {
@@ -3243,25 +3301,52 @@ namespace jdi
if (UiPointInRect(layout.close, fx, fy)) if (UiPointInRect(layout.close, fx, fy))
{ {
// ✕ 关闭面板的同时关闭该网格对应的实验性功能。 // ✕ 关闭面板的同时关闭该网格对应的实验性功能。
if (centerAxis) switch (kind)
{ {
case EdgePanelKind::CENTER:
SetLaneEdgeCenterCount(index, false); SetLaneEdgeCenterCount(index, false);
} break;
else case EdgePanelKind::MEAN:
{ SetLaneMeanFilter(index, false);
break;
case EdgePanelKind::COUNT:
default:
SetLaneEdgeCount(index, false); SetLaneEdgeCount(index, false);
break;
} }
m_dragMode = PlotDragMode::NONE; m_dragMode = PlotDragMode::NONE;
return true; return true;
} }
if (centerAxis && layout.edit.Width > 0.0f && UiPointInRect(layout.edit, fx, fy) && if ((kind == EdgePanelKind::CENTER || kind == EdgePanelKind::MEAN) &&
m_tab != nullptr) layout.edit.Width > 0.0f && UiPointInRect(layout.edit, fx, fy) && m_tab != nullptr)
{ {
// 底部编辑行 → 就地编辑平滑点数 N(输入框落在该行内)。 // 底部编辑行 → 就地编辑参数 N(输入框落在该行内)。
const int smooth = m_tab->Lanes()[static_cast<std::size_t>(index)].edgeCenterSmooth; const Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
BeginEdit(PlotEditTarget::EDGE_CENTER_SMOOTH, index, std::to_wstring(smooth), if (kind == EdgePanelKind::MEAN)
layout.edit); {
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; return true;
} }
@@ -3269,7 +3354,7 @@ namespace jdi
{ {
m_dragMode = PlotDragMode::EDGE_PANEL_MOVE; m_dragMode = PlotDragMode::EDGE_PANEL_MOVE;
m_edgePanelDragLane = index; m_edgePanelDragLane = index;
m_edgePanelDragCenter = centerAxis; m_edgePanelDragKind = kind;
m_edgePanelDragOffset.X = fx - layout.panel.X; m_edgePanelDragOffset.X = fx - layout.panel.X;
m_edgePanelDragOffset.Y = fy - layout.panel.Y; m_edgePanelDragOffset.Y = fy - layout.panel.Y;
return true; return true;
@@ -3363,7 +3448,7 @@ namespace jdi
m_cursorGrabOffset = 0; m_cursorGrabOffset = 0;
m_edgeDragLane = -1; m_edgeDragLane = -1;
m_edgePanelDragLane = -1; m_edgePanelDragLane = -1;
m_edgePanelDragCenter = false; m_edgePanelDragKind = EdgePanelKind::COUNT;
SetHint(std::wstring()); SetHint(std::wstring());
m_dragLane = LaneAtY(y); m_dragLane = LaneAtY(y);
m_dragMode = PlotDragMode::PAN; m_dragMode = PlotDragMode::PAN;
@@ -3440,7 +3525,7 @@ namespace jdi
m_cursorGrabOffset = 0; m_cursorGrabOffset = 0;
m_edgeDragLane = -1; m_edgeDragLane = -1;
m_edgePanelDragLane = -1; m_edgePanelDragLane = -1;
m_edgePanelDragCenter = false; m_edgePanelDragKind = EdgePanelKind::COUNT;
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。 // 松开左键:粘滞提示结束计时,开始"停留后淡出"。
ReleaseHint(); ReleaseHint();
(void)x; (void)x;
@@ -4253,6 +4338,9 @@ namespace jdi
// 「边沿计数(中心轴)」的开关与平滑点数会改变网格内的曲线绘制 → 参与指纹。 // 「边沿计数(中心轴)」的开关与平滑点数会改变网格内的曲线绘制 → 参与指纹。
mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull); mix(Tab().Lanes()[laneIndex].edgeCenter ? 1ull : 0ull);
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterSmooth)); 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()); mix(signals.size());
for (const SignalRef& ref : signals) 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)].edgeCenter
? Tab().Lanes()[static_cast<std::size_t>(laneIndex)].edgeCenterSmooth ? Tab().Lanes()[static_cast<std::size_t>(laneIndex)].edgeCenterSmooth
: -1; : -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) for (const SignalRef& ref : laneSignals)
{ {
@@ -4729,6 +4822,11 @@ namespace jdi
{ {
PaintCenterCurve(g, resolved, lane, centerSmooth); PaintCenterCurve(g, resolved, lane, centerSmooth);
} }
// 均值滤波曲线(「实验性功能 → 均值滤波」):同样是仅用于预览的叠加曲线。
if (meanFilterRadius >= 0)
{
PaintMeanFilterCurve(g, resolved, lane, meanFilterRadius);
}
} }
// g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。 // g.Restore 也会还原绘制状态,此处直接恢复图形状态即可。
@@ -5306,68 +5404,87 @@ namespace jdi
UiText(g, text, m_fonts->monoSmall, labelRect, UiText(g, text, m_fonts->monoSmall, labelRect,
dark ? 0xFFFDF6E3u : 0xFF073642u, TextAlign::CENTER); 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 derivedValue = 0.0;
double centerValue = 0.0; const bool ok = meanFilter
if (CenterValueAt(*resolved.file, *resolved.data, smooth, time, centerValue)) ? MeanFilterValueAt(*resolved.file, *resolved.data, smooth,
time, derivedValue)
: CenterValueAt(*resolved.file, *resolved.data, smooth, time,
derivedValue);
if (!ok)
{ {
const float rawCenterY = ValueToY(centerValue, lane); return;
const DataMarker centerMarker = MakeDataMarker( }
rawX, rawCenterY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
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 double delta = derivedValue - otherDerived;
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。 centerText += (delta < 0.0) ? L"-" : L"+";
const std::uint32_t centerColor = StyleColor(*resolved.style); centerText += FormatValue(std::fabs(delta));
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg); centerText += L" ";
// 取值文本:数值在前,尾缀「(中心轴)」;双光标的右侧光标还要
// 附上增量,形如「(+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);
} }
} }
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; EdgePanelLayout layout;
if (m_tab == nullptr || m_fonts == nullptr || laneIndex < 0 || if (m_tab == nullptr || m_fonts == nullptr || laneIndex < 0 ||
@@ -5434,8 +5551,11 @@ namespace jdi
return layout; return layout;
} }
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)]; const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const bool enabled = centerAxis ? lane.edgeCenter : lane.edgeCount; const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool open = centerAxis ? lane.edgeCenterPanelOpen : lane.edgePanelOpen; 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) if (!enabled || !open)
{ {
return layout; return layout;
@@ -5456,18 +5576,23 @@ namespace jdi
} }
layout.rows = rowCount; layout.rows = rowCount;
const float width = EdgePanelWidth(laneIndex, centerAxis); const float width = EdgePanelWidth(laneIndex, kind);
const float titleHeight = kEdgePanelTitleHeight * m_scale; const float titleHeight = kEdgePanelTitleHeight * m_scale;
const float rowHeight = kEdgePanelRowHeight * m_scale; const float rowHeight = kEdgePanelRowHeight * m_scale;
const float pad = kEdgePanelPad * m_scale; const float pad = kEdgePanelPad * m_scale;
// 中心轴面板在底部固定多出一行「平滑点数」编辑行。 // 中心轴 / 均值滤波面板在底部固定多出一行编辑行;均值滤波再多一个按钮行。
const float editHeight = centerAxis ? (kEdgePanelEditHeight * m_scale) : 0.0f; const float editHeight =
const float height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * 2.0f + pad + (centerAxis || meanFilter) ? (kEdgePanelEditHeight * m_scale) : 0.0f;
editHeight; 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 storedX = meanFilter ? lane.meanFilterPanelX
const double storedY = centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY; : (centerAxis ? lane.edgeCenterPanelX : lane.edgePanelX);
const double storedY = meanFilter ? lane.meanFilterPanelY
: (centerAxis ? lane.edgeCenterPanelY : lane.edgePanelY);
float left = (storedX >= 0.0) float left = (storedX >= 0.0)
? static_cast<float>(storedX) ? static_cast<float>(storedX)
: (m_plotRect.X + m_plotRect.Width - width - 16.0f * m_scale); : (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); layout.title = gd::RectF(left, top, width, titleHeight);
const float closeSize = titleHeight; const float closeSize = titleHeight;
layout.close = gd::RectF(left + width - closeSize, top, closeSize, closeSize); layout.close = gd::RectF(left + width - closeSize, top, closeSize, closeSize);
if (centerAxis) if (centerAxis || meanFilter)
{ {
const float editRowHeight = kEdgePanelEditHeight * m_scale; const float editRowHeight = kEdgePanelEditHeight * m_scale;
layout.edit = gd::RectF(left + pad, top + height - editRowHeight, const float editTop = top + height - buttonHeight - editRowHeight;
std::max(0.0f, width - pad * 2.0f), 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; return layout;
} }
gd::RectF PlotView::EdgePanelRect(int laneIndex) const 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 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 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 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) void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
{ {
if (m_tab == nullptr || m_dataSet == nullptr) 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) for (std::size_t index = 0; index < count; ++index)
{ {
const Lane& tabLane = tabLanes[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; const gd::RectF& panel = layout.panel;
if (panel.Width <= 0.0f || panel.Height <= 0.0f) 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, UiText(g, L"G" + std::to_wstring(index + 1), m_fonts->monoSmall, badgeRect,
m_theme->text, TextAlign::CENTER); m_theme->text, TextAlign::CENTER);
const std::wstring title = const std::wstring title =
(centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix(); meanFilter ? std::wstring(L"均值滤波")
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix());
UiText(g, title, m_fonts->uiSmall, UiText(g, title, m_fonts->uiSmall,
gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y, gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y,
panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight, panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight,
@@ -5923,7 +6067,8 @@ namespace jdi
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor, closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad, crossColor,
1.6f * m_scale); 1.6f * m_scale);
// 每个信号两行:名称(信号色)+ 上升沿/下降沿数量。 // 每个信号两行:名称(信号色)+ 上升沿/下降沿数量;
// 均值滤波面板只需列出被处理的信号名(一行)。
float rowY = panel.Y + titleHeight + pad; float rowY = panel.Y + titleHeight + pad;
for (const SignalRef& ref : tabLane.signals) for (const SignalRef& ref : tabLane.signals)
{ {
@@ -5938,6 +6083,11 @@ namespace jdi
UiText(g, resolved.data->Title(), m_fonts->uiSmall, UiText(g, resolved.data->Title(), m_fonts->uiSmall,
gd::RectF(panel.X + pad, rowY, textWidth, rowHeight), signalColor, gd::RectF(panel.X + pad, rowY, textWidth, rowHeight), signalColor,
TextAlign::LEFT); TextAlign::LEFT);
if (meanFilter)
{
rowY += rowHeight;
continue;
}
std::size_t rising = 0; std::size_t rising = 0;
std::size_t falling = 0; std::size_t falling = 0;
@@ -5960,32 +6110,33 @@ namespace jdi
rowY += rowHeight * 2.0f; rowY += rowHeight * 2.0f;
} }
// 中心轴面板底部固定一行「平滑点数」编辑设置(始终位于面板最下方)。 // 中心轴 / 均值滤波面板底部固定一行参数编辑设置(始终位于面板最下方)。
if (centerAxis) if (centerAxis || meanFilter)
{ {
const gd::RectF& editRect = layout.edit; const gd::RectF& editRect = layout.edit;
if (editRect.Width > 0.0f) if (editRect.Width > 0.0f)
{ {
const float fieldGap = kEdgePanelEditFieldGap * m_scale; const int shownValue = meanFilter ? tabLane.meanFilterRadius
const std::wstring label = L"平滑点数 N"; : tabLane.edgeCenterSmooth;
const std::wstring label = meanFilter ? L"均值点数 N" : L"平滑点数 N";
const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, label); const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
UiText(g, label, m_fonts->uiSmall, UiText(g, label, m_fonts->uiSmall,
gd::RectF(editRect.X, editRect.Y, labelWidth, editRect.Height), gd::RectF(editRect.X, editRect.Y, labelWidth, editRect.Height),
m_theme->text, TextAlign::LEFT); m_theme->text, TextAlign::LEFT);
const float fieldWidth = std::max( const float fieldWidth =
kEdgePanelEditFieldWidth * m_scale, std::max(kEdgePanelEditFieldWidth * m_scale,
UiMeasureTextWidth(m_fonts->monoSmall, UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(shownValue)) +
std::to_wstring(tabLane.edgeCenterSmooth)) + 20.0f * m_scale);
20.0f * m_scale);
const float fieldLeft = editRect.X + editRect.Width - fieldWidth; const float fieldLeft = editRect.X + editRect.Width - fieldWidth;
const float fieldHeight = editRect.Height - 6.0f * m_scale; const float fieldHeight = editRect.Height - 6.0f * m_scale;
const gd::RectF fieldRect(fieldLeft, editRect.Y + 3.0f * m_scale, fieldWidth, const gd::RectF fieldRect(fieldLeft, editRect.Y + 3.0f * m_scale, fieldWidth,
fieldHeight); fieldHeight);
const PlotEditTarget editTarget = meanFilter ? PlotEditTarget::MEAN_FILTER_RADIUS
: PlotEditTarget::EDGE_CENTER_SMOOTH;
const bool editingThis = const bool editingThis =
(m_editTarget == PlotEditTarget::EDGE_CENTER_SMOOTH && (m_editTarget == editTarget && m_editLane == static_cast<int>(index));
m_editLane == static_cast<int>(index));
if (editingThis) if (editingThis)
{ {
UiDrawEditField(g, fieldRect, m_edit, m_fonts->monoSmall, m_scale); 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); UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f); UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
UiText(g, std::to_wstring(tabLane.edgeCenterSmooth), m_fonts->monoSmall, UiText(g, std::to_wstring(shownValue), m_fonts->monoSmall, fieldRect,
fieldRect, m_theme->textStrong, TextAlign::CENTER); 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);
} }
} }
} }
+76 -1
View File
@@ -212,6 +212,22 @@ namespace jdi
*/ */
bool AddPreparedFile(DataFile&& file, int& fileIndex); bool AddPreparedFile(DataFile&& file, int& fileIndex);
/**
* @brief 在指定文件的指定信号**下方**插入一条新信号(派生信号)。
*
* 新信号会占据 `afterSignalIndex + 1` 的位置,该位置及其后的信号下标整体后移一位;
* 调用方必须紧接着对每个标签页调用 `TabConfig::RemapAfterSignalInsert()` 修正引用。
* 采样值会被就地统计(min/max/finiteCount 与粗粒度包络层),显示外观按「全局信号序号」
* 取调色板颜色(与普通新信号一致)。
*
* @param [IN] fileIndex 目标文件下标。
* @param [IN] afterSignalIndex 插入位置的上一条信号下标。
* @param [IN,OUT] signal 待插入的信号(values 会被统计,其余字段原样使用)。
* @param [OUT] newSignalIndex 新信号在本文件内的下标。
* @return 插入成功返回 true。
*/
bool InsertDerivedSignal(int fileIndex, int afterSignalIndex, SignalData&& signal, int& newSignalIndex);
/** /**
* @brief 移除指定下标的文件,并立即释放其占用的全部采样内存。 * @brief 移除指定下标的文件,并立即释放其占用的全部采样内存。
* @param [IN] fileIndex 文件下标。 * @param [IN] fileIndex 文件下标。
@@ -391,6 +407,27 @@ namespace jdi
/** 中心轴统计面板左上角纵坐标(<0 表示自动定位)。 */ /** 中心轴统计面板左上角纵坐标(<0 表示自动定位)。 */
double edgeCenterPanelY = -1.0; double edgeCenterPanelY = -1.0;
/**
* @brief 「实验性功能 → 均值滤波」开关。
*
* 开启后为本网格内每条信号额外绘制一条 N 点滑动平均曲线(算法与「边沿计数(中心轴)」
* 的中心平均值曲线完全一致),并在浮动面板中提供参数 N 与「构建为新信号」按钮。
* 与其它实验性功能一样属于网格自身状态,重建网格时按信号继承。
*/
bool meanFilter = false;
/** 均值滤波半径 N(左右各取 N 个数据点,窗口大小 2N+1)。 */
int meanFilterRadius = 5;
/** 均值滤波浮动面板是否展开。 */
bool meanFilterPanelOpen = true;
/** 均值滤波面板左上角位置(客户区像素;<0 表示自动定位)。 */
double meanFilterPanelX = -1.0;
/** 均值滤波面板左上角纵坐标(<0 表示自动定位)。 */
double meanFilterPanelY = -1.0;
}; };
/** /**
@@ -404,6 +441,18 @@ namespace jdi
*/ */
bool SampleFileAt(const DataFile& file, const SignalData& signal, double time, bool interpolate, double& value); bool SampleFileAt(const DataFile& file, const SignalData& signal, double time, bool interpolate, double& value);
/**
* @brief 计算 N 点滑动平均(窗口 = [i-N, i+N],边界按实际窗口长度)。
*
* 与「边沿计数(中心轴)」的中心平均值曲线**完全同一算法**:
* NaN 不参与平均;中心点本身为 NaN 时结果也为 NaN;窗口内无有效值时为 NaN。
*
* @param [IN] values 原始采样值。
* @param [IN] radius 平滑半径 N(0 表示原样返回)。
* @param [OUT] out 输出数组(长度与 values 相同)。
*/
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out);
/** /**
* @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。 * @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。
* @param [IN] file 目标文件。 * @param [IN] file 目标文件。
@@ -579,8 +628,34 @@ namespace jdi
*/ */
int SplitLane(int laneIndex); int SplitLane(int laneIndex);
/** * @brief 在数据层插入新信号之后,修正本页引用该文件的信号下标。
*
* 数据层在下标 `insertPos` 处插入了一条信号,因此本页可见性表需插入一列,
* 且所有指向该文件、下标不小于 `insertPos` 的信号引用都要后移一位。
*
* @param [IN] fileIndex 发生插入的文件下标。
* @param [IN] insertPos 新信号的下标。
*/
void RemapAfterSignalInsert(int fileIndex, int insertPos);
/** /**
* @brief 查询某文件的分组是否处于折叠状态。 * @brief 把某信号所在的网格移动到另一信号所在网格的下方(紧邻其后)。
*
* 用于「均值滤波 → 构建为新信号」:新信号所在的网格应紧跟在原信号网格的下方。
* 移动成功后会切换为自定义划分策略,避免后续重建网格时顺序被复原。
*
* @param [IN] moved 被移动网格内的任一信号。
* @param [IN] anchor 锚点信号(其所在网格之后即为目标位置)。
* @return 发生移动返回 true。
*/
bool MoveLaneOfSignalBelow(const SignalRef& moved, const SignalRef& anchor);
/**
* @brief 按当前位置统一重命名全部网格(GX 编号由位置推导,须保持一致)。
*/
void RenameLanes();
/** * @brief 查询某文件的分组是否处于折叠状态。
* @param [IN] fileIndex 文件下标。 * @param [IN] fileIndex 文件下标。
* @return 折叠状态。 * @return 折叠状态。
*/ */
+221 -1
View File
@@ -126,6 +126,11 @@ namespace jdi
bool edgeCenterPanelOpen = true; bool edgeCenterPanelOpen = true;
double edgeCenterPanelX = -1.0; double edgeCenterPanelX = -1.0;
double edgeCenterPanelY = -1.0; double edgeCenterPanelY = -1.0;
bool meanFilter = false;
int meanFilterRadius = 5;
bool meanFilterPanelOpen = true;
double meanFilterPanelX = -1.0;
double meanFilterPanelY = -1.0;
}; };
// 先记下每个信号原来所在网格的网格自身状态: // 先记下每个信号原来所在网格的网格自身状态:
@@ -146,6 +151,11 @@ namespace jdi
carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen; carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen;
carry.edgeCenterPanelX = lane.edgeCenterPanelX; carry.edgeCenterPanelX = lane.edgeCenterPanelX;
carry.edgeCenterPanelY = lane.edgeCenterPanelY; carry.edgeCenterPanelY = lane.edgeCenterPanelY;
carry.meanFilter = lane.meanFilter;
carry.meanFilterRadius = lane.meanFilterRadius;
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
carry.meanFilterPanelX = lane.meanFilterPanelX;
carry.meanFilterPanelY = lane.meanFilterPanelY;
for (const SignalRef& ref : lane.signals) for (const SignalRef& ref : lane.signals)
{ {
previousCarry.emplace_back(ref, carry); previousCarry.emplace_back(ref, carry);
@@ -177,6 +187,11 @@ namespace jdi
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen; lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
lane.edgeCenterPanelX = carry.edgeCenterPanelX; lane.edgeCenterPanelX = carry.edgeCenterPanelX;
lane.edgeCenterPanelY = carry.edgeCenterPanelY; lane.edgeCenterPanelY = carry.edgeCenterPanelY;
lane.meanFilter = carry.meanFilter;
lane.meanFilterRadius = carry.meanFilterRadius;
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
lane.meanFilterPanelX = carry.meanFilterPanelX;
lane.meanFilterPanelY = carry.meanFilterPanelY;
}; };
// 收集全部可见信号引用。 // 收集全部可见信号引用。
@@ -622,6 +637,62 @@ namespace jdi
return AddPreparedFile(std::move(file), fileIndex); return AddPreparedFile(std::move(file), fileIndex);
} }
bool DataSet::InsertDerivedSignal(int fileIndex, int afterSignalIndex, SignalData&& signal,
int& newSignalIndex)
{
newSignalIndex = -1;
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
{
return false;
}
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
if (afterSignalIndex < -1 || afterSignalIndex >= static_cast<int>(file.signals.size()))
{
return false;
}
// 1. 采样值统计与粗粒度包络层:与正常加载路径使用同一套统计逻辑,
// 否则派生信号在大文件下会退化成逐点扫描(包络构建变慢)。
const SignalStats stats = ComputeStats(signal.values, signal.coarseMin, signal.coarseMax);
signal.coarseStride = kCoarseStride;
signal.minValue = stats.minValue;
signal.maxValue = stats.maxValue;
signal.finiteCount = stats.finiteCount;
// 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。
const int insertPos = afterSignalIndex + 1;
{
// 新信号的全局序号 = 本文件之前的信号总数 + 它在文件内的下标。
std::size_t globalIndex = static_cast<std::size_t>(insertPos);
for (int index = 0; index < fileIndex; ++index)
{
globalIndex += m_files[static_cast<std::size_t>(index)].signals.size();
}
int paletteCount = 0;
const std::uint32_t* palette = SignalPalette(paletteCount);
if (palette != nullptr && paletteCount > 0)
{
const std::size_t count = static_cast<std::size_t>(paletteCount);
const std::size_t slot = globalIndex % count;
signal.style.color = palette[slot];
signal.style.customColor = palette[(slot + 1) % count];
}
}
file.signals.insert(file.signals.begin() + insertPos, std::move(signal));
// 3. 全局选中引用:同一文件内位于插入点之后的信号下标整体后移。
if (m_selected.fileIndex == fileIndex && m_selected.signalIndex >= insertPos)
{
++m_selected.signalIndex;
}
newSignalIndex = insertPos;
return true;
}
bool DataSet::RemoveFile(int fileIndex) bool DataSet::RemoveFile(int fileIndex)
{ {
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size())) if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
@@ -796,6 +867,86 @@ namespace jdi
// ============================ 采样辅助 ============================ // ============================ 采样辅助 ============================
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
{
const int window = std::max(0, radius);
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0 || window == 0)
{
// 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。
for (std::size_t index = 0; index < count; ++index)
{
if (std::isfinite(values[index]))
{
out[index] = static_cast<float>(values[index]);
}
}
return;
}
// 滑动窗口求和:窗口 = [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; // 闭区间右端。
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>(window))
? (index - static_cast<std::size_t>(window))
: 0;
const std::size_t high = std::min(count - 1, index + static_cast<std::size_t>(window));
if (index == 0)
{
windowLow = low;
windowHigh = high;
for (std::size_t i = windowLow; i <= windowHigh; ++i)
{
addValue(values[i]);
}
}
else
{
while (windowLow < low)
{
removeValue(values[windowLow]);
++windowLow;
}
while (windowHigh < high)
{
++windowHigh;
addValue(values[windowHigh]);
}
}
// 中心点本身无效(NaN)时该处不产生有效结果。
if (finiteCount > 0 && std::isfinite(values[index]))
{
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
}
}
}
int FindNearestSample(const DataFile& file, double time) int FindNearestSample(const DataFile& file, double time)
{ {
const std::size_t count = file.time.size(); const std::size_t count = file.time.size();
@@ -1247,6 +1398,11 @@ namespace jdi
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen; lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
lane.edgeCenterPanelX = source.edgeCenterPanelX; lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY; lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.meanFilter = source.meanFilter;
lane.meanFilterRadius = source.meanFilterRadius;
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
lane.signals.push_back(ref); lane.signals.push_back(ref);
pieces.push_back(std::move(lane)); pieces.push_back(std::move(lane));
} }
@@ -1263,8 +1419,72 @@ namespace jdi
return laneIndex; return laneIndex;
} }
void TabConfig::RemapAfterSignalInsert(int fileIndex, int insertPos)
{
if (fileIndex < 0 || insertPos < 0)
{
return;
}
// 可见性表:在插入点补一列(新信号默认不可见,由调用方按需勾选)。
const std::size_t slot = static_cast<std::size_t>(fileIndex);
if (slot < m_visible.size() && static_cast<std::size_t>(insertPos) <= m_visible[slot].size())
{
m_visible[slot].insert(m_visible[slot].begin() + insertPos, false);
}
// 网格内的信号引用:同一文件中位于插入点之后的引用下标整体后移。
for (Lane& lane : m_lanes)
{
for (SignalRef& ref : lane.signals)
{
if (ref.fileIndex == fileIndex && ref.signalIndex >= insertPos)
{
++ref.signalIndex;
}
}
}
}
bool TabConfig::MoveLaneOfSignalBelow(const SignalRef& moved, const SignalRef& anchor)
{
const int from = LaneOfSignal(moved);
if (from < 0 || LaneOfSignal(anchor) < 0)
{
return false;
}
Lane lane = std::move(m_lanes[static_cast<std::size_t>(from)]);
m_lanes.erase(m_lanes.begin() + from);
// erase 之后锚点网格的下标可能变化,必须用信号引用重新定位。
const int target = LaneOfSignal(anchor);
if (target < 0)
{
m_lanes.push_back(std::move(lane));
}
else
{
m_lanes.insert(m_lanes.begin() + target + 1, std::move(lane));
}
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
m_laneMode = LaneMode::CUSTOM;
RenameLanes();
return true;
}
void TabConfig::RenameLanes()
{
for (std::size_t index = 0; index < m_lanes.size(); ++index)
{
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
bool TabConfig::IsCollapsed(int fileIndex) const bool TabConfig::IsCollapsed(int fileIndex) const
{ if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_collapsed.size()) {
if (fileIndex < 0 || static_cast<std::size_t>(fileIndex) >= m_collapsed.size())
{ {
return false; return false;
} }