实验性功能新增「均值滤波」+ 构建为新信号
- 「实验性功能」二级菜单新增带勾选的「均值滤波」:开启后为本网格每条信号叠加绘制
一条 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:
1 parent
8ae8d7af4f
commit
f5028b1e18
6 files changed
+885
-210
No files matched your search
@@ -227,6 +227,27 @@ namespace jdi
|
||||
*/
|
||||
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 弹出文件选择对话框并加载所选文件。
|
||||
*/
|
||||
|
||||
@@ -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,需在按下后立即刷新一次,
|
||||
|
||||
@@ -53,7 +53,8 @@ namespace jdi
|
||||
LANE_Y_MAX, /**< 编辑某网格的 Y 轴最大值。 */
|
||||
LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */
|
||||
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_EDGE_COUNT = 7, /**< 切换本网格的「实验性功能 → 边沿计数」开关。 */
|
||||
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);
|
||||
|
||||
/**
|
||||
* @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 轴上限/下限的内联输入状态。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
@@ -913,7 +959,7 @@ namespace jdi
|
||||
bool IsOnEdgeCursorLine(int x, int y, int& laneIndex) const;
|
||||
|
||||
/**
|
||||
* @brief 实验性功能浮动面板的几何布局(边沿计数 / 中心轴边沿计数共用同一套样式)。
|
||||
* @brief 实验性功能浮动面板的几何布局(三类面板共用同一套样式)。
|
||||
*/
|
||||
struct EdgePanelLayout
|
||||
{
|
||||
@@ -921,16 +967,17 @@ namespace jdi
|
||||
gd::RectF title; /**< 标题栏(**唯一可拖拽区域**)。 */
|
||||
gd::RectF close; /**< ✕ 关闭按钮。 */
|
||||
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
|
||||
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
|
||||
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 计算某个实验性功能面板的布局。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] centerAxis 为 true 时取「边沿计数(中心轴)」面板,否则取「边沿计数」。
|
||||
* @param [IN] kind 面板种类。
|
||||
* @return 布局;面板未展开时各项均为空矩形。
|
||||
*/
|
||||
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, bool centerAxis) const;
|
||||
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
|
||||
|
||||
/**
|
||||
* @brief 获取「边沿计数(中心轴)」浮动面板的矩形。
|
||||
@@ -984,6 +1031,28 @@ namespace jdi
|
||||
void PaintCenterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
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 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
|
||||
*/
|
||||
@@ -1066,10 +1135,10 @@ namespace jdi
|
||||
/**
|
||||
* @brief 计算实验性功能面板所需的宽度(排版与绘制共用,保证文字完整不被省略)。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] centerAxis 为 true 时取「边沿计数(中心轴)」面板。
|
||||
* @param [IN] kind 面板种类。
|
||||
* @return 面板宽度(像素)。
|
||||
*/
|
||||
float EdgePanelWidth(int laneIndex, bool centerAxis) const;
|
||||
float EdgePanelWidth(int laneIndex, EdgePanelKind kind) const;
|
||||
|
||||
/**
|
||||
* @brief 判断边沿统计是否限定在双光标区间内(双光标模式)。
|
||||
@@ -1205,6 +1274,9 @@ namespace jdi
|
||||
std::wstring m_pendingRenameName; /**< 待宿主窗口应用的信号新名称。 */
|
||||
bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */
|
||||
|
||||
int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */
|
||||
int m_pendingMeanFilterRadius = 0; /**< 待构建的均值滤波半径 N。 */
|
||||
|
||||
int m_hoverLane = -1; /**< 当前悬停网格下标。 */
|
||||
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
|
||||
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
|
||||
@@ -1257,7 +1329,7 @@ namespace jdi
|
||||
// 「边沿计数」横向光标 / 面板的拖拽状态。
|
||||
int m_edgeDragLane = -1; /**< 正在拖动的横向光标所属网格。 */
|
||||
int m_edgePanelDragLane = -1; /**< 正在拖动的统计面板所属网格。 */
|
||||
bool m_edgePanelDragCenter = false; /**< 正在拖动的面板是否为「中心轴边沿计数」面板。 */
|
||||
EdgePanelKind m_edgePanelDragKind = EdgePanelKind::COUNT; /**< 正在拖动的面板种类。 */
|
||||
gd::PointF m_edgePanelDragOffset; /**< 抓取面板时的相对偏移(保持不跳变)。 */
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,22 @@ namespace jdi
|
||||
*/
|
||||
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 移除指定下标的文件,并立即释放其占用的全部采样内存。
|
||||
* @param [IN] fileIndex 文件下标。
|
||||
@@ -391,6 +407,27 @@ namespace jdi
|
||||
|
||||
/** 中心轴统计面板左上角纵坐标(<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);
|
||||
|
||||
/**
|
||||
* @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 在指定文件的时间轴上查找与给定时刻最接近的采样点。
|
||||
* @param [IN] file 目标文件。
|
||||
@@ -579,8 +628,34 @@ namespace jdi
|
||||
*/
|
||||
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 文件下标。
|
||||
* @return 折叠状态。
|
||||
*/
|
||||
|
||||
+221
-1
@@ -126,6 +126,11 @@ namespace jdi
|
||||
bool edgeCenterPanelOpen = true;
|
||||
double edgeCenterPanelX = -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.edgeCenterPanelX = lane.edgeCenterPanelX;
|
||||
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)
|
||||
{
|
||||
previousCarry.emplace_back(ref, carry);
|
||||
@@ -177,6 +187,11 @@ namespace jdi
|
||||
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
|
||||
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
const std::size_t count = file.time.size();
|
||||
@@ -1247,6 +1398,11 @@ namespace jdi
|
||||
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
|
||||
lane.edgeCenterPanelX = source.edgeCenterPanelX;
|
||||
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);
|
||||
pieces.push_back(std::move(lane));
|
||||
}
|
||||
@@ -1263,8 +1419,72 @@ namespace jdi
|
||||
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
|
||||
{ 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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user