一、信号列表「组名悬浮」平滑化、且不再遮住下一个组名
* 悬浮位置改为 min(列表上边界, 本组最后一行下边界 − 组名高度):
- 本组还有行在列表里时钉在列表上边界;
- 本组最后一行开始滑出上边界时,组名跟着一起上移、随滚动条自然离界
(不再像旧实现那样突然消失);
- 「本组最后一行的下边界」恰好就是下一个分组行的自然位置,因此悬浮组名的下边界
永远不超过下一组名 —— 旧实现固定钉在列表上边界会把下一组名盖住,让别人以为
后面的信号属于上一组。
* 折叠的分组不再悬浮(折叠时本组“最后一行”就是它自己,悬浮位置 = 自然位置,
于是随滚动自然划过,不会停在最上方)。
* 底部边界线只在真的“浮”在其它行之上时才画,避免与正常绘制出现像素差。
二、单点信号的初始视图
* 新增 PlotView::FitSinglePointSignal():当前没有任何信号被勾选、而勾选的第一个信号
只有一个有效数据点时,把该数据点居中放置,时间跨度 = 1s、Y 轴跨度 = 1
(各为一个大格),并关闭该网格的整段自适应以免被覆盖。
Application::SynchronizeView 在首次勾选时先尝试它,失败才走 FitAll()。
* AutoRangeLaneToView 的退化区间(可见区间内只有一个取值)改用与 PadRange 一致的留白
max(1, |v|×0.1),不再产生“几乎为 0”的区间(那会让幅值网格线挤成一团、看不出数据点)。
* 需求作废:上一轮“2…16 个有效采样点默认改为点线 + 2px”已取消(放大自动点线已覆盖该场景);
只保留“只有一个有效采样点 → 点线 + 最大线宽”。
* SignalData::singlePointTime 升级为 sparseTimes/sparseValues(有效点 ≤ 1024 时在加载/插入
时缓存全部有效点),同时服务单点初始视图与光标吸附。
三、光标吸附的统一判定
* N = GridMinorCellCount():网格底衬一横排的小格数,与网格绘制用同一套
NiceStep + 五等分算出(因此不随缩放等级大幅变化)。
* SignalSnapEnabled():当且仅当某信号在当前一屏内的数据点个数 < N×4 时为它开启吸附;
判据随视界实时重算 ⇒ 疏密变化的信号在稀疏区自动吸附、移回稠密区自动自由移动。
额外要求视界内至少有一个可见数据点(否则“0 < N×4”会把数据全在视界之外的信号
误判为稀疏)。统计只在可见下标区间内进行且攒够阈值即早退,代价有界。
* SnapTimeToSample():判定只针对鼠标所在网格(同一个时间轴上不同网格结果可以不同);
同一网格内所有符合判定的信号的数据点都参与吸附比较;
吸附半径 = 一个网格小格的宽度,且不超过半个局部采样间距 ⇒ 数据点很稀疏
(一屏只有一两个点)时光标在点之间仍可自由移动(吸附是提示,不是卡住)。
* 删除旧的 SampleSpacingPixels(6px 下门限 + 半屏宽上门限)。
四、放大自动切点线改为与吸附判定完全同源
* UpdateAutoScatterStyles() 改用 SignalSnapEnabled:进入判定自动切「点线」,
不再满足(适应视图、或视界从稀疏区移到稠密区)自动恢复原样式;
自动期间用户手动改过则不恢复(尊重用户选择,判定放在恢复之前)。
* 删除旧的 AxisSparseInView(“采样间距 > 半屏宽”)。
五、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
55/55 通过(连跑 3 次稳定):[A] 组名悬浮(钉住/平滑上移/整组滚过后换下一组/折叠组不悬浮/
悬浮下边界不超过下一组名)、[B] 单点初始视图(跨度 1s 与 1、居中、点线 + 最大线宽、
两信号时不再走单点路径)、[C] 吸附判定(满视图都关闭/稀疏窗口开启/无可见点不开启/
同一时间轴不同网格结果不同/同网格多信号的点都参与/稠密↔稀疏双向动态切换/
单点靠近吸附远离自由)、[D] 自动点线(同源判定/恢复/用户手动改过不覆盖)。
A/B:临时禁用「悬浮位置公式 / 单点初始视图 / 吸附判定」后重跑 ⇒ 14 项如实失败。
make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。
六、规则同步
Documents/agent_mem.md 新增 §9.30、§11.2 第 58–61 条硬约束与变更日志。
2622 lines
83 KiB
C++
2622 lines
83 KiB
C++
#include "DataModel.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <atomic>
|
||
#include <cmath>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
#include <cwctype>
|
||
#include <deque>
|
||
#include <limits>
|
||
#include <new>
|
||
|
||
#include <windows.h> // Sleep(低内存设备的“少量多次”节流)
|
||
|
||
#include "Theme.hpp"
|
||
#include "Utf8Util.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace
|
||
{
|
||
const double kMissing = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
|
||
const double kDefaultLineWidth = 1.6; /**< 默认线宽。 */
|
||
|
||
/**
|
||
* @brief 为网格范围补充留白,避免波形紧贴边界。
|
||
* @param [IN, OUT] yMin 范围下限。
|
||
* @param [IN, OUT] yMax 范围上限。
|
||
*/
|
||
void PadRange(double& yMin, double& yMax)
|
||
{
|
||
if (!std::isfinite(yMin) || !std::isfinite(yMax))
|
||
{
|
||
yMin = 0.0;
|
||
yMax = 1.0;
|
||
return;
|
||
}
|
||
|
||
const double span = yMax - yMin;
|
||
if (span <= 0.0)
|
||
{
|
||
const double magnitude = std::max(1.0, std::fabs(yMin) * 0.1);
|
||
yMin -= magnitude;
|
||
yMax += magnitude;
|
||
return;
|
||
}
|
||
|
||
const double pad = span * 0.06;
|
||
yMin -= pad;
|
||
yMax += pad;
|
||
}
|
||
|
||
/**
|
||
* @brief 依据全部可见信号重新生成网格划分。
|
||
* @param [IN] visible 各信号的可见性(按 [文件][信号] 索引)。
|
||
* @param [IN] lanes 网格集合(原地更新)。
|
||
* @param [IN] mode 网格划分策略。
|
||
*/
|
||
void RebuildLanes(const std::vector<std::vector<bool>>& visible, std::vector<Lane>& lanes,
|
||
LaneMode mode)
|
||
{
|
||
// 随网格携带的“网格自身状态”(按信号继承):重建网格时不能丢。
|
||
struct LaneCarry
|
||
{
|
||
bool matched = false; /**< 是否找到该信号原本所在的网格(false = 新出现的信号)。 */
|
||
bool fitYToSelection = false;
|
||
bool edgeCount = false;
|
||
double edgeValue = 0.0;
|
||
bool edgeValueValid = false;
|
||
bool edgePanelOpen = true;
|
||
double edgePanelX = -1.0;
|
||
double edgePanelY = -1.0;
|
||
bool edgeCenter = false;
|
||
std::vector<Lane::EdgeCenterItem> edgeCenterItems;
|
||
bool edgeCenterPanelOpen = true;
|
||
double edgeCenterPanelX = -1.0;
|
||
double edgeCenterPanelY = -1.0;
|
||
bool meanFilter = false;
|
||
std::vector<Lane::MeanFilterItem> meanFilterItems;
|
||
bool meanFilterPanelOpen = true;
|
||
double meanFilterPanelX = -1.0;
|
||
double meanFilterPanelY = -1.0;
|
||
bool weakPeak = false;
|
||
std::vector<Lane::WeakPeakItem> weakPeakItems;
|
||
bool weakPeakPanelOpen = true;
|
||
double weakPeakPanelX = -1.0;
|
||
double weakPeakPanelY = -1.0;
|
||
};
|
||
|
||
// 先记下每个信号原来所在网格的网格自身状态:
|
||
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
|
||
//
|
||
// ⚠️ 同时记下「重建之前是否**每个**网格都开着 `fitYToSelection`」:
|
||
// 那就是「全局 Y 轴自适应」的正式定义。新勾选信号产生的**新网格**必须继承它,
|
||
// 否则全局开关看着是开的、新网格却不按当前视界自适应(用户报告的缺陷)。
|
||
bool previousAllAutoFit = !lanes.empty();
|
||
for (const Lane& lane : lanes)
|
||
{
|
||
if (!lane.fitYToSelection)
|
||
{
|
||
previousAllAutoFit = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
std::vector<std::pair<SignalRef, LaneCarry>> previousCarry;
|
||
for (const Lane& lane : lanes)
|
||
{
|
||
LaneCarry carry;
|
||
carry.matched = true;
|
||
carry.fitYToSelection = lane.fitYToSelection;
|
||
carry.edgeCount = lane.edgeCount;
|
||
carry.edgeValue = lane.edgeValue;
|
||
carry.edgeValueValid = lane.edgeValueValid;
|
||
carry.edgePanelOpen = lane.edgePanelOpen;
|
||
carry.edgePanelX = lane.edgePanelX;
|
||
carry.edgePanelY = lane.edgePanelY;
|
||
carry.edgeCenter = lane.edgeCenter;
|
||
carry.edgeCenterItems = lane.edgeCenterItems;
|
||
carry.edgeCenterPanelOpen = lane.edgeCenterPanelOpen;
|
||
carry.edgeCenterPanelX = lane.edgeCenterPanelX;
|
||
carry.edgeCenterPanelY = lane.edgeCenterPanelY;
|
||
carry.meanFilter = lane.meanFilter;
|
||
carry.meanFilterItems = lane.meanFilterItems;
|
||
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
|
||
carry.meanFilterPanelX = lane.meanFilterPanelX;
|
||
carry.meanFilterPanelY = lane.meanFilterPanelY;
|
||
carry.weakPeak = lane.weakPeak;
|
||
carry.weakPeakItems = lane.weakPeakItems;
|
||
carry.weakPeakPanelOpen = lane.weakPeakPanelOpen;
|
||
carry.weakPeakPanelX = lane.weakPeakPanelX;
|
||
carry.weakPeakPanelY = lane.weakPeakPanelY;
|
||
for (const SignalRef& ref : lane.signals)
|
||
{
|
||
previousCarry.emplace_back(ref, carry);
|
||
}
|
||
}
|
||
const auto inheritedCarry = [&previousCarry](const SignalRef& ref)
|
||
{
|
||
for (const std::pair<SignalRef, LaneCarry>& entry : previousCarry)
|
||
{
|
||
if (entry.first == ref)
|
||
{
|
||
return entry.second;
|
||
}
|
||
}
|
||
return LaneCarry();
|
||
};
|
||
const auto applyCarry = [&inheritedCarry, previousAllAutoFit](Lane& lane, const SignalRef& ref)
|
||
{
|
||
const LaneCarry carry = inheritedCarry(ref);
|
||
// 新出现的信号(之前没有网格):继承「全局 Y 轴自适应」是否开启。
|
||
lane.fitYToSelection = carry.matched ? carry.fitYToSelection : previousAllAutoFit;
|
||
lane.edgeCount = carry.edgeCount;
|
||
lane.edgeValue = carry.edgeValue;
|
||
lane.edgeValueValid = carry.edgeValueValid;
|
||
lane.edgePanelOpen = carry.edgePanelOpen;
|
||
lane.edgePanelX = carry.edgePanelX;
|
||
lane.edgePanelY = carry.edgePanelY;
|
||
lane.edgeCenter = carry.edgeCenter;
|
||
lane.edgeCenterItems = carry.edgeCenterItems;
|
||
lane.edgeCenterPanelOpen = carry.edgeCenterPanelOpen;
|
||
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
|
||
lane.edgeCenterPanelY = carry.edgeCenterPanelY;
|
||
lane.meanFilter = carry.meanFilter;
|
||
lane.meanFilterItems = carry.meanFilterItems;
|
||
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
|
||
lane.meanFilterPanelX = carry.meanFilterPanelX;
|
||
lane.meanFilterPanelY = carry.meanFilterPanelY;
|
||
lane.weakPeak = carry.weakPeak;
|
||
lane.weakPeakItems = carry.weakPeakItems;
|
||
lane.weakPeakPanelOpen = carry.weakPeakPanelOpen;
|
||
lane.weakPeakPanelX = carry.weakPeakPanelX;
|
||
lane.weakPeakPanelY = carry.weakPeakPanelY;
|
||
};
|
||
|
||
// 收集全部可见信号引用。
|
||
std::vector<SignalRef> visibleRefs;
|
||
for (std::size_t groupIndex = 0; groupIndex < visible.size(); ++groupIndex)
|
||
{
|
||
for (std::size_t signalIndex = 0; signalIndex < visible[groupIndex].size(); ++signalIndex)
|
||
{
|
||
if (visible[groupIndex][signalIndex])
|
||
{
|
||
SignalRef ref;
|
||
ref.groupIndex = static_cast<int>(groupIndex);
|
||
ref.signalIndex = static_cast<int>(signalIndex);
|
||
visibleRefs.push_back(ref);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 判断某引用对应的信号是否可见(越界视为不可见)。
|
||
const auto isVisibleRef = [&visible](const SignalRef& ref)
|
||
{
|
||
if (ref.groupIndex < 0 || static_cast<std::size_t>(ref.groupIndex) >= visible.size())
|
||
{
|
||
return false;
|
||
}
|
||
const std::vector<bool>& fileVisible = visible[static_cast<std::size_t>(ref.groupIndex)];
|
||
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
|
||
{
|
||
return false;
|
||
}
|
||
return fileVisible[static_cast<std::size_t>(ref.signalIndex)];
|
||
};
|
||
|
||
// 某信号当前所属的网格下标(-1 表示尚未分配网格)。
|
||
const auto laneIndexOf = [&lanes](const SignalRef& ref)
|
||
{
|
||
for (std::size_t index = 0; index < lanes.size(); ++index)
|
||
{
|
||
const std::vector<SignalRef>& refs = lanes[index].signals;
|
||
if (std::find(refs.begin(), refs.end(), ref) != refs.end())
|
||
{
|
||
return static_cast<int>(index);
|
||
}
|
||
}
|
||
return -1;
|
||
};
|
||
|
||
if (mode == LaneMode::PER_SIGNAL)
|
||
{
|
||
// 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。
|
||
// **顺序按"原有网格位置"优先**:已经分配过网格的信号保持其相对次序,
|
||
// 新勾选(此前没有网格)的信号统一追加到最下方。
|
||
// 若直接按 visibleRefs(文件/列序)重建,新勾选的信号会按其列序
|
||
// 插到中间,位置看起来"不确定"。
|
||
std::vector<SignalRef> ordered;
|
||
ordered.reserve(visibleRefs.size());
|
||
for (const SignalRef& ref : visibleRefs)
|
||
{
|
||
if (laneIndexOf(ref) >= 0)
|
||
{
|
||
ordered.push_back(ref);
|
||
}
|
||
}
|
||
std::stable_sort(ordered.begin(), ordered.end(),
|
||
[&laneIndexOf](const SignalRef& a, const SignalRef& b)
|
||
{ return laneIndexOf(a) < laneIndexOf(b); });
|
||
for (const SignalRef& ref : visibleRefs)
|
||
{
|
||
if (laneIndexOf(ref) < 0)
|
||
{
|
||
ordered.push_back(ref);
|
||
}
|
||
}
|
||
|
||
lanes.clear();
|
||
lanes.reserve(ordered.size());
|
||
for (const SignalRef& ref : ordered)
|
||
{
|
||
Lane lane;
|
||
lane.autoRange = true;
|
||
applyCarry(lane, ref);
|
||
lane.signals.push_back(ref);
|
||
lanes.push_back(std::move(lane));
|
||
}
|
||
}
|
||
else if (mode == LaneMode::MERGED)
|
||
{
|
||
// 全部可见信号合并到同一网格;无可见信号时不保留空网格。
|
||
bool previousAutoRange = true;
|
||
double previousYMin = -1.0;
|
||
double previousYMax = 1.0;
|
||
if (lanes.size() == 1)
|
||
{
|
||
previousAutoRange = lanes[0].autoRange;
|
||
previousYMin = lanes[0].yMin;
|
||
previousYMax = lanes[0].yMax;
|
||
}
|
||
|
||
lanes.clear();
|
||
if (!visibleRefs.empty())
|
||
{
|
||
Lane lane;
|
||
lane.autoRange = previousAutoRange;
|
||
lane.yMin = previousYMin;
|
||
lane.yMax = previousYMax;
|
||
applyCarry(lane, visibleRefs.front());
|
||
lane.signals = visibleRefs;
|
||
lanes.push_back(std::move(lane));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 自定义划分:仅移除不可见信号与由此产生的空网格,保持用户排布。
|
||
for (Lane& lane : lanes)
|
||
{
|
||
std::vector<SignalRef>& refs = lane.signals;
|
||
refs.erase(std::remove_if(refs.begin(), refs.end(),
|
||
[&isVisibleRef](const SignalRef& ref)
|
||
{ return !isVisibleRef(ref); }),
|
||
refs.end());
|
||
}
|
||
|
||
lanes.erase(std::remove_if(lanes.begin(), lanes.end(),
|
||
[](const Lane& lane) { return lane.signals.empty(); }),
|
||
lanes.end());
|
||
|
||
// 未分配网格的可见信号各自独占一个新网格。
|
||
// 注意:不能把它们塞进已有的第一个网格——那会导致“拖拽合并后新勾选的信号
|
||
// 被悄悄并入合并网格”,用户看不到新增的独立泳道。
|
||
for (const SignalRef& ref : visibleRefs)
|
||
{
|
||
bool placed = false;
|
||
for (const Lane& lane : lanes)
|
||
{
|
||
if (std::find(lane.signals.begin(), lane.signals.end(), ref) != lane.signals.end())
|
||
{
|
||
placed = true;
|
||
break;
|
||
}
|
||
}
|
||
if (placed)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
Lane lane;
|
||
lane.autoRange = true;
|
||
applyCarry(lane, ref);
|
||
lane.signals.push_back(ref);
|
||
lanes.push_back(std::move(lane));
|
||
}
|
||
}
|
||
|
||
// 统一重命名,保证编号连续。
|
||
for (std::size_t index = 0; index < lanes.size(); ++index)
|
||
{
|
||
lanes[index].name = L"网格 " + std::to_wstring(index + 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
const wchar_t* DrawModeName(DrawMode mode)
|
||
{
|
||
switch (mode)
|
||
{
|
||
case DrawMode::LINE:
|
||
return L"线段";
|
||
case DrawMode::SCATTER:
|
||
return L"散点";
|
||
case DrawMode::LINE_SCATTER:
|
||
return L"点线";
|
||
default:
|
||
return L"线段";
|
||
}
|
||
}
|
||
|
||
// ============================ SignalGroup 构建 ============================
|
||
|
||
/**
|
||
* @brief 粗粒度包络层每个分组覆盖的采样数。
|
||
*
|
||
* 16KB 的双精度分组可覆盖 1024 个采样(内存开销 = 采样数的 1.6%),
|
||
* 但会让"每列采样数略多于分组跨度"的场景退化为逐点扫描。
|
||
* 取 256 时:额外内存约为采样数的 0.8%,最坏情况的扫描量也能压低到
|
||
* 每列 ≤256 个采样(2400 列 ≈ 0.6M 次读取,约 1ms)。
|
||
*/
|
||
const std::size_t kCoarseStride = 256;
|
||
|
||
/**
|
||
* @brief 单条信号的取值范围统计结果。
|
||
*/
|
||
struct SignalStats
|
||
{
|
||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
||
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 为**只有一个有效采样点**的信号设置初始绘制样式。
|
||
*
|
||
* 单个数据点在「线段」模式下完全画不出来,因此初始就用「点线」并取**最大线宽**
|
||
* (点线模式下数据点是一个圆点,半径与线宽成正比)。
|
||
*
|
||
* ⚠️ 曾经还对「有效采样点 2…16」的信号也改用「点线 + 2px」—— 该需求已**作废**
|
||
*(用户 2026-10-15 明确取消:放大后自动切点线的机制已覆盖那个场景)。
|
||
*
|
||
* 只在**新建信号**时调用(用户改过样式后不再覆盖)。
|
||
*
|
||
* @param [IN] finiteCount 有效采样点数量。
|
||
* @param [IN,OUT] style 待初始化的显示外观。
|
||
*/
|
||
void ApplySinglePointDefaultStyle(std::size_t finiteCount, SignalStyle& style)
|
||
{
|
||
if (finiteCount == 1)
|
||
{
|
||
style.mode = DrawMode::LINE_SCATTER;
|
||
style.lineWidth = kSinglePointSignalLineWidth;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 缓存「有效采样点很少」的信号的**全部有效点**(时刻 + 取值,按时间升序)。
|
||
*
|
||
* 供光标吸附(二分查找最近数据点、O(log n) 判定稀疏)与单点信号的初始视图使用。
|
||
* 有效点很多时(> `kSparseTimesLimit`)不做任何缓存(用不到,也没必要占内存)。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN,OUT] signal 目标信号。
|
||
*/
|
||
void CacheSparseSamples(const TimeAxis& axis, SignalData& signal)
|
||
{
|
||
signal.sparseTimes.clear();
|
||
signal.sparseValues.clear();
|
||
if (signal.finiteCount == 0 || signal.finiteCount > kSparseTimesLimit)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const std::size_t count = std::min(axis.time.size(), signal.values.size());
|
||
signal.sparseTimes.reserve(signal.finiteCount);
|
||
signal.sparseValues.reserve(signal.finiteCount);
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
const double value = signal.values[index];
|
||
const double time = axis.time[index];
|
||
if (!std::isfinite(value) || !std::isfinite(time))
|
||
{
|
||
continue;
|
||
}
|
||
signal.sparseTimes.push_back(time);
|
||
signal.sparseValues.push_back(value);
|
||
}
|
||
|
||
// 非单调时间轴:按时间排序,保证后续二分查找合法
|
||
//(单调时间轴的采样本身就是升序,无需排序)。
|
||
if (!axis.monotonic && signal.sparseTimes.size() > 1)
|
||
{
|
||
std::vector<std::size_t> order(signal.sparseTimes.size());
|
||
for (std::size_t index = 0; index < order.size(); ++index)
|
||
{
|
||
order[index] = index;
|
||
}
|
||
std::stable_sort(order.begin(), order.end(), [&signal](std::size_t a, std::size_t b)
|
||
{ return signal.sparseTimes[a] < signal.sparseTimes[b]; });
|
||
std::vector<double> times;
|
||
std::vector<double> values;
|
||
times.reserve(order.size());
|
||
values.reserve(order.size());
|
||
for (const std::size_t index : order)
|
||
{
|
||
times.push_back(signal.sparseTimes[index]);
|
||
values.push_back(signal.sparseValues[index]);
|
||
}
|
||
signal.sparseTimes = std::move(times);
|
||
signal.sparseValues = std::move(values);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 统计一列采样的最小/最大值与有效点数量,并顺带构建粗粒度包络层。
|
||
*
|
||
* 两者在同一个循环内完成,避免为粗粒度层多扫一遍数据(大文件下那是一整趟内存遍历)。
|
||
*
|
||
* @param [IN] values 采样列。
|
||
* @param [OUT] coarseMin 粗粒度分组最小值输出。
|
||
* @param [OUT] coarseMax 粗粒度分组最大值输出。
|
||
* @return 统计结果。
|
||
*/
|
||
SignalStats ComputeStats(const std::vector<double>& values, std::vector<double>& coarseMin,
|
||
std::vector<double>& coarseMax)
|
||
{
|
||
SignalStats stats;
|
||
double low = std::numeric_limits<double>::infinity();
|
||
double high = -std::numeric_limits<double>::infinity();
|
||
|
||
const std::size_t count = values.size();
|
||
const std::size_t buckets = (count + kCoarseStride - 1) / kCoarseStride;
|
||
coarseMin.assign(buckets, std::numeric_limits<double>::infinity());
|
||
coarseMax.assign(buckets, -std::numeric_limits<double>::infinity());
|
||
|
||
std::size_t bucket = 0;
|
||
std::size_t bucketEnd = kCoarseStride;
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
if (index >= bucketEnd)
|
||
{
|
||
++bucket;
|
||
bucketEnd += kCoarseStride;
|
||
}
|
||
const double value = values[index];
|
||
if (std::isfinite(value))
|
||
{
|
||
++stats.finiteCount;
|
||
low = std::min(low, value);
|
||
high = std::max(high, value);
|
||
if (value < coarseMin[bucket])
|
||
{
|
||
coarseMin[bucket] = value;
|
||
}
|
||
if (value > coarseMax[bucket])
|
||
{
|
||
coarseMax[bucket] = value;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (stats.finiteCount == 0)
|
||
{
|
||
stats.minValue = 0.0;
|
||
stats.maxValue = 0.0;
|
||
}
|
||
else
|
||
{
|
||
stats.minValue = low;
|
||
stats.maxValue = high;
|
||
}
|
||
return stats;
|
||
}
|
||
|
||
bool BuildSignalGroup(LoadedSource&& source, std::size_t paletteBase, SignalGroup& group,
|
||
std::wstring& error, const LoadProgress& progress)
|
||
{
|
||
error.clear();
|
||
group = SignalGroup();
|
||
|
||
if (source.blocks.empty() || !source.HasData())
|
||
{
|
||
error = L"数据源为空。";
|
||
return false;
|
||
}
|
||
|
||
// 分组默认名 = 源文件名(含扩展名),用户可重命名。
|
||
group.name = source.sourceName;
|
||
|
||
int paletteCount = 0;
|
||
const std::uint32_t* palette = SignalPalette(paletteCount);
|
||
|
||
// 时间轴去重表:内容完全相同(长度与全部数值逐位相同)的时间轴只保留一条。
|
||
// 这样 CSV 里多个同刻度的 `time` 列、MF4 里多个同基线的通道组不会重复占内存。
|
||
std::vector<std::size_t> axisHashToIndex;
|
||
std::vector<std::uint64_t> axisHashValues;
|
||
|
||
const auto timeHash = [](const std::vector<double>& time) -> std::uint64_t
|
||
{
|
||
std::uint64_t hash = 1469598103934665603ull;
|
||
for (const double value : time)
|
||
{
|
||
std::uint64_t bits = 0;
|
||
std::memcpy(&bits, &value, sizeof(bits));
|
||
hash ^= bits;
|
||
hash *= 1099511628211ull;
|
||
}
|
||
hash ^= static_cast<std::uint64_t>(time.size());
|
||
hash *= 1099511628211ull;
|
||
return hash;
|
||
};
|
||
const auto sameTime = [](const std::vector<double>& a, const std::vector<double>& b) -> bool
|
||
{
|
||
if (a.size() != b.size())
|
||
{
|
||
return false;
|
||
}
|
||
for (std::size_t i = 0; i < a.size(); ++i)
|
||
{
|
||
const double x = a[i];
|
||
const double y = b[i];
|
||
if (x != y)
|
||
{
|
||
// NaN 视为“相同”(缺失位置一致),其余必须完全相等。
|
||
if (!(std::isnan(x) && std::isnan(y)))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
|
||
std::size_t blockIndex = 0;
|
||
for (TableBlock& block : source.blocks)
|
||
{
|
||
++blockIndex;
|
||
DataTable& table = block.table;
|
||
if (!table.HasData())
|
||
{
|
||
continue;
|
||
}
|
||
const std::size_t columnCount = table.headers.size();
|
||
if (table.columns.size() < columnCount || columnCount < 2)
|
||
{
|
||
continue; // 只有时间列:没有可绘制信号
|
||
}
|
||
|
||
// 时间列固定为块的第 0 列(加载器已保证)。
|
||
std::vector<double>& timeColumn = table.columns[0];
|
||
|
||
// 统计时间轴的取值范围与单调性。
|
||
TimeAxis axis;
|
||
axis.name = table.headers[0];
|
||
{
|
||
bool monotonic = true;
|
||
bool hasPrevious = false;
|
||
double previousTime = 0.0;
|
||
double low = std::numeric_limits<double>::infinity();
|
||
double high = -std::numeric_limits<double>::infinity();
|
||
std::size_t finiteCount = 0;
|
||
for (const double value : timeColumn)
|
||
{
|
||
if (!std::isfinite(value))
|
||
{
|
||
continue;
|
||
}
|
||
++finiteCount;
|
||
low = std::min(low, value);
|
||
high = std::max(high, value);
|
||
if (hasPrevious && value < previousTime)
|
||
{
|
||
monotonic = false;
|
||
}
|
||
previousTime = value;
|
||
hasPrevious = true;
|
||
}
|
||
if (finiteCount < 1)
|
||
{
|
||
continue; // 该块的时间轴完全无效
|
||
}
|
||
axis.monotonic = monotonic;
|
||
axis.finiteCount = finiteCount;
|
||
axis.minTime = low;
|
||
axis.maxTime = high;
|
||
}
|
||
axis.time = std::move(timeColumn);
|
||
|
||
// 去重:命中已有时间轴则复用它,否则新增一条。
|
||
std::size_t axisIndex = group.axes.size();
|
||
{
|
||
const std::uint64_t hash = timeHash(axis.time);
|
||
for (std::size_t candidate = 0; candidate < axisHashValues.size(); ++candidate)
|
||
{
|
||
if (axisHashValues[candidate] == hash &&
|
||
sameTime(group.axes[axisHashToIndex[candidate]].time, axis.time))
|
||
{
|
||
axisIndex = axisHashToIndex[candidate];
|
||
break;
|
||
}
|
||
}
|
||
if (axisIndex == group.axes.size())
|
||
{
|
||
group.axes.push_back(std::move(axis));
|
||
axisHashValues.push_back(hash);
|
||
axisHashToIndex.push_back(axisIndex);
|
||
}
|
||
}
|
||
|
||
// 构建信号列:每列整块移动,随后就地统计取值范围。
|
||
group.signals.reserve(group.signals.size() + columnCount - 1);
|
||
for (std::size_t column = 1; column < columnCount; ++column)
|
||
{
|
||
SignalData signal;
|
||
signal.name = Trim(table.headers[column]);
|
||
if (signal.name.empty())
|
||
{
|
||
signal.name = L"Column" + std::to_wstring(column + 1);
|
||
}
|
||
signal.axisIndex = axisIndex;
|
||
|
||
signal.values = std::move(table.columns[column]);
|
||
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;
|
||
// 有效点很少时缓存全部有效点(光标吸附 / 单点信号初始视图用)。
|
||
CacheSparseSamples(group.AxisAt(signal.axisIndex), signal);
|
||
|
||
// 显示外观随信号一起初始化:调色板按「全局信号序号」取色,
|
||
// 使同一信号在任意标签页中都保持同一默认颜色。
|
||
{
|
||
const std::size_t globalIndex = paletteBase + group.signals.size();
|
||
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];
|
||
}
|
||
else
|
||
{
|
||
signal.style.color = 0xFF268BD2u;
|
||
signal.style.customColor = 0xFF859900u;
|
||
}
|
||
signal.style.opacity = 1.0f;
|
||
signal.style.lineWidth = 1.0f;
|
||
signal.style.mode = DrawMode::LINE;
|
||
// 只有一个有效采样点的信号改用「点线」+ 最大线宽,否则根本看不见。
|
||
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
|
||
}
|
||
|
||
group.signals.push_back(std::move(signal));
|
||
}
|
||
|
||
if (progress)
|
||
{
|
||
LoadProgressInfo info;
|
||
const double ratio = static_cast<double>(blockIndex) /
|
||
static_cast<double>(std::max<std::size_t>(1, source.blocks.size()));
|
||
info.ratio = 0.75 + 0.25 * ratio;
|
||
info.hasBytes = false;
|
||
if (!progress(info))
|
||
{
|
||
error = L"已取消加载。";
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (group.signals.empty() || group.axes.empty())
|
||
{
|
||
error = L"除时间轴外未发现任何数据列。";
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ============================ DataSet ============================
|
||
|
||
bool DataSet::AddGroup(SignalGroup&& group, int& groupIndex)
|
||
{
|
||
groupIndex = -1;
|
||
if (group.signals.empty() || group.axes.empty())
|
||
{
|
||
return false;
|
||
}
|
||
m_groups.push_back(std::move(group));
|
||
groupIndex = static_cast<int>(m_groups.size()) - 1;
|
||
return true;
|
||
}
|
||
|
||
bool DataSet::InsertDerivedSignal(int groupIndex, int afterSignalIndex, SignalData&& signal,
|
||
int& newSignalIndex)
|
||
{
|
||
newSignalIndex = -1;
|
||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||
if (afterSignalIndex < -1 || afterSignalIndex >= static_cast<int>(group.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;
|
||
// 有效点很少时缓存全部有效点;派生信号沿用源信号的时间轴。
|
||
CacheSparseSamples(group.AxisAt(signal.axisIndex), signal);
|
||
|
||
// 2. 显示外观按「全局信号序号」取调色板颜色(与普通新信号完全一致的规则)。
|
||
const int insertPos = afterSignalIndex + 1;
|
||
{
|
||
// 新信号的全局序号 = 本组之前的信号总数 + 它在组内的下标。
|
||
std::size_t globalIndex = static_cast<std::size_t>(insertPos);
|
||
for (int index = 0; index < groupIndex; ++index)
|
||
{
|
||
globalIndex += m_groups[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];
|
||
}
|
||
|
||
// 采样点极少的派生信号同样改用「点线」并加粗(与普通加载路径同一规则)。
|
||
ApplySinglePointDefaultStyle(stats.finiteCount, signal.style);
|
||
}
|
||
|
||
group.signals.insert(group.signals.begin() + insertPos, std::move(signal));
|
||
|
||
// 3. 全局选中引用:同一组内位于插入点之后的信号下标整体后移。
|
||
if (m_selected.groupIndex == groupIndex && m_selected.signalIndex >= insertPos)
|
||
{
|
||
++m_selected.signalIndex;
|
||
}
|
||
|
||
newSignalIndex = insertPos;
|
||
return true;
|
||
}
|
||
|
||
bool DataSet::RemoveGroup(int groupIndex)
|
||
{
|
||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 用移动到末位后弹出的方式移除,使被移除文件的时间轴与全部信号缓冲区在本调用内
|
||
// 立即析构归还操作系统,不再等待后续插入覆盖;最后收缩容量,避免残留空闲块。
|
||
const std::size_t position = static_cast<std::size_t>(groupIndex);
|
||
for (std::size_t index = position; index + 1 < m_groups.size(); ++index)
|
||
{
|
||
m_groups[index] = std::move(m_groups[index + 1]);
|
||
}
|
||
m_groups.pop_back();
|
||
m_groups.shrink_to_fit();
|
||
return true;
|
||
}
|
||
|
||
bool DataSet::RemoveSignal(int groupIndex, int signalIndex)
|
||
{
|
||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||
{
|
||
return false;
|
||
}
|
||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||
if (signalIndex < 0 || signalIndex >= static_cast<int>(group.signals.size()))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 与 RemoveGroup 同一思路:先把待移除项移到末位(move 赋值时其原有缓冲当场析构),
|
||
// 再 pop_back + shrink_to_fit,使该信号的全部采样内存在本调用内立即归还给系统,
|
||
// 而不是留在容量里等后续插入覆盖。
|
||
std::vector<SignalData>& signals = group.signals;
|
||
const std::size_t position = static_cast<std::size_t>(signalIndex);
|
||
for (std::size_t index = position; index + 1 < signals.size(); ++index)
|
||
{
|
||
signals[index] = std::move(signals[index + 1]);
|
||
}
|
||
signals.pop_back();
|
||
signals.shrink_to_fit();
|
||
return true;
|
||
}
|
||
|
||
void DataSet::RemapSelectionAfterSignalRemoval(int groupIndex, int signalIndex)
|
||
{
|
||
if (groupIndex < 0 || signalIndex < 0 || m_selected.groupIndex != groupIndex)
|
||
{
|
||
return;
|
||
}
|
||
if (m_selected.signalIndex == signalIndex)
|
||
{
|
||
// 选中的信号本身被移除:取消选中。
|
||
m_selected = SignalRef();
|
||
}
|
||
else if (m_selected.signalIndex > signalIndex)
|
||
{
|
||
// 其后信号整体前移一位。
|
||
--m_selected.signalIndex;
|
||
}
|
||
}
|
||
|
||
std::size_t DataSet::SignalCount() const
|
||
{
|
||
std::size_t count = 0;
|
||
for (const SignalGroup& group : m_groups)
|
||
{
|
||
count += group.signals.size();
|
||
}
|
||
return count;
|
||
}
|
||
|
||
const SignalGroup* DataSet::GroupAt(int groupIndex) const
|
||
{
|
||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||
{
|
||
return nullptr;
|
||
}
|
||
return &m_groups[static_cast<std::size_t>(groupIndex)];
|
||
}
|
||
|
||
bool DataSet::SetSignalDisplayName(int groupIndex, int signalIndex, const std::wstring& name)
|
||
{
|
||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||
if (signalIndex < 0 || signalIndex >= static_cast<int>(group.signals.size()))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 统一去除首尾空白;结果为空则视为恢复源名称。
|
||
std::wstring trimmed = Trim(name);
|
||
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
|
||
if (trimmed == signal.displayName)
|
||
{
|
||
return false;
|
||
}
|
||
signal.displayName = std::move(trimmed);
|
||
return true;
|
||
}
|
||
|
||
bool DataSet::SetGroupDisplayName(int groupIndex, const std::wstring& name)
|
||
{
|
||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
std::wstring trimmed = Trim(name);
|
||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||
if (trimmed == group.displayName)
|
||
{
|
||
return false;
|
||
}
|
||
group.displayName = std::move(trimmed);
|
||
return true;
|
||
}
|
||
|
||
const SignalData* DataSet::Resolve(const SignalRef& ref) const
|
||
{
|
||
const SignalGroup* group = GroupAt(ref.groupIndex);
|
||
if (group == nullptr)
|
||
{
|
||
return nullptr;
|
||
}
|
||
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(group->signals.size()))
|
||
{
|
||
return nullptr;
|
||
}
|
||
return &group->signals[static_cast<std::size_t>(ref.signalIndex)];
|
||
}
|
||
|
||
const TimeAxis* DataSet::Axis(const SignalRef& ref) const
|
||
{
|
||
const SignalGroup* group = GroupAt(ref.groupIndex);
|
||
if (group == nullptr)
|
||
{
|
||
return nullptr;
|
||
}
|
||
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(group->signals.size()) ||
|
||
group->axes.empty())
|
||
{
|
||
return nullptr;
|
||
}
|
||
return &group->AxisOf(group->signals[static_cast<std::size_t>(ref.signalIndex)]);
|
||
}
|
||
|
||
const SignalStyle* DataSet::Style(const SignalRef& ref) const
|
||
{
|
||
const SignalData* signal = Resolve(ref);
|
||
return (signal != nullptr) ? &signal->style : nullptr;
|
||
}
|
||
|
||
SignalStyle* DataSet::Style(const SignalRef& ref)
|
||
{
|
||
const SignalGroup* group = GroupAt(ref.groupIndex);
|
||
if (group == nullptr)
|
||
{
|
||
return nullptr;
|
||
}
|
||
if (ref.signalIndex < 0 || ref.signalIndex >= static_cast<int>(group->signals.size()))
|
||
{
|
||
return nullptr;
|
||
}
|
||
|
||
// 这里需要写入,因此借助非 const 路径重新定位信号。
|
||
SignalGroup& mutableGroup = m_groups[static_cast<std::size_t>(ref.groupIndex)];
|
||
return &mutableGroup.signals[static_cast<std::size_t>(ref.signalIndex)].style;
|
||
}
|
||
|
||
double DataSet::MinTime() const
|
||
{
|
||
double value = std::numeric_limits<double>::infinity();
|
||
for (const SignalGroup& group : m_groups)
|
||
{
|
||
double low = 0.0;
|
||
double high = 0.0;
|
||
if (group.TimeRange(low, high))
|
||
{
|
||
value = std::min(value, low);
|
||
}
|
||
}
|
||
return std::isfinite(value) ? value : 0.0;
|
||
}
|
||
|
||
double DataSet::MaxTime() const
|
||
{
|
||
double value = -std::numeric_limits<double>::infinity();
|
||
for (const SignalGroup& group : m_groups)
|
||
{
|
||
double low = 0.0;
|
||
double high = 0.0;
|
||
if (group.TimeRange(low, high))
|
||
{
|
||
value = std::max(value, high);
|
||
}
|
||
}
|
||
return std::isfinite(value) ? value : 1.0;
|
||
}
|
||
|
||
void DataSet::RemapSelectionAfterGroupRemoval(int groupIndex)
|
||
{
|
||
if (groupIndex < 0)
|
||
{
|
||
return;
|
||
}
|
||
if (m_selected.groupIndex == groupIndex)
|
||
{
|
||
m_selected = SignalRef();
|
||
}
|
||
else if (m_selected.groupIndex > groupIndex)
|
||
{
|
||
--m_selected.groupIndex;
|
||
}
|
||
}
|
||
|
||
bool DataSet::ValidateSelection()
|
||
{
|
||
if (!m_selected.IsValid())
|
||
{
|
||
return false;
|
||
}
|
||
if (Resolve(m_selected) != nullptr)
|
||
{
|
||
return false;
|
||
}
|
||
m_selected = SignalRef();
|
||
return true;
|
||
}
|
||
|
||
// ============================ 采样辅助 ============================
|
||
|
||
namespace
|
||
{
|
||
/**
|
||
* @brief 滑动平均的共用实现(输入只要可转成 double)。
|
||
*
|
||
* 窗口 = [max(0,i-N), min(count-1,i+N)],每前进一步只增减边界处的两个采样点,
|
||
* 整体 O(n),不额外占用前缀和内存。
|
||
*/
|
||
template <typename T>
|
||
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out);
|
||
} // namespace
|
||
|
||
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
|
||
{
|
||
MovingAverageImpl(values, radius, out);
|
||
}
|
||
|
||
namespace
|
||
{
|
||
template <typename T>
|
||
void MovingAverageImpl(const std::vector<T>& 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)
|
||
{
|
||
const double value = static_cast<double>(values[index]);
|
||
if (std::isfinite(value))
|
||
{
|
||
out[index] = static_cast<float>(value);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 滑动窗口求和:每前进一步只增减边界处的两个采样点,整体 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(static_cast<double>(values[i]));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
while (windowLow < low)
|
||
{
|
||
removeValue(static_cast<double>(values[windowLow]));
|
||
++windowLow;
|
||
}
|
||
while (windowHigh < high)
|
||
{
|
||
++windowHigh;
|
||
addValue(static_cast<double>(values[windowHigh]));
|
||
}
|
||
}
|
||
|
||
// 中心点本身无效(NaN)时该处不产生有效结果。
|
||
const double center = static_cast<double>(values[index]);
|
||
if (finiteCount > 0 && std::isfinite(center))
|
||
{
|
||
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
|
||
}
|
||
}
|
||
}
|
||
} // namespace
|
||
|
||
namespace
|
||
{
|
||
/**
|
||
* @brief `thread_local` 复用缓冲的**保留容量上限**(元素个数,约 4M)。
|
||
*
|
||
* 这些缓冲用 `thread_local` 复用,是为了省掉"每次调用都分配 + 零初始化上千万元素"
|
||
* 的开销(多阶弱峰调整实测 991ms → 577ms)。但**保留容量必须封顶**:
|
||
* 后台派生曲线线程池有 4 条线程,每条各自持有一份 `thread_local` 缓冲;
|
||
* 一条"整段信号"(VERY_BIG_DATA ≈ 2580 万采样)的弱峰调整需要
|
||
* `current` / `next`(float,各 ≈103MB)、极值表与位移表(≈数百 MB),
|
||
* 于是**每条线程常驻数百 MB、4 条线程就是 2GB 以上**,而 `thread_local`
|
||
* 只在**线程退出**时才析构 → 用户报告"启用预览后内存 1.42GB→4.21GB,
|
||
* 关掉预览也不下降,直到关闭整个文件分组才释放"。
|
||
*
|
||
* 因此只对"中/小规模"(≤ 上限)保留容量;**超大计算**改为一次性分配、用完即释放 ——
|
||
* 这类整段计算本身是秒级的一次性操作(结果会被曲线缓存复用),多一次分配完全可接受,
|
||
* 换来的是内存不再随线程数放大。
|
||
*/
|
||
const std::size_t kReuseBufferMaxElements = 4u * 1024u * 1024u;
|
||
|
||
/** @brief 容量超过上限就释放缓冲内存(`std::vector` 的容量不会随 `clear()` 归还)。 */
|
||
template <typename T>
|
||
void TrimReuseBuffer(std::vector<T>& buffer)
|
||
{
|
||
if (buffer.capacity() > kReuseBufferMaxElements)
|
||
{
|
||
std::vector<T> empty;
|
||
buffer.swap(empty);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 作用域守卫:离开作用域时对 `thread_local` 复用缓冲做一次容量裁剪。
|
||
*
|
||
* 用它(而不是在每个 `return` 前手写一遍)可以保证**所有**退出路径都会裁剪,
|
||
* 不会因为新增提前返回而漏掉、导致内存再次失控。
|
||
*/
|
||
template <typename T>
|
||
class ReuseBufferScope
|
||
{
|
||
public:
|
||
explicit ReuseBufferScope(std::vector<T>& buffer) : m_buffer(buffer) {}
|
||
~ReuseBufferScope() { TrimReuseBuffer(m_buffer); }
|
||
ReuseBufferScope(const ReuseBufferScope&) = delete;
|
||
ReuseBufferScope& operator=(const ReuseBufferScope&) = delete;
|
||
|
||
private:
|
||
std::vector<T>& m_buffer;
|
||
};
|
||
} // namespace
|
||
|
||
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int iterations,
|
||
std::vector<float>& out,
|
||
const std::atomic<bool>* cancel, bool throttle)
|
||
{
|
||
const int stages = std::max(1, iterations);
|
||
// 第 1 阶:从原始采样值出发。
|
||
ComputeMovingAverage(values, radius, out);
|
||
if (stages == 1)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。
|
||
// 中转缓冲复用容量(线程本地),避免每阶重新分配上千万元素的向量。
|
||
// ⚠️ 用 `ReuseBufferScope` 保证离开作用域时对超大容量做裁剪(见其说明)。
|
||
thread_local std::vector<double> bufferBuffer;
|
||
ReuseBufferScope<double> reuseScope(bufferBuffer);
|
||
std::vector<double>& buffer = bufferBuffer;
|
||
buffer.assign(values.size(), std::numeric_limits<double>::quiet_NaN());
|
||
std::vector<float> stage;
|
||
for (int iteration = 1; iteration < stages; ++iteration)
|
||
{
|
||
if (cancel != nullptr && cancel->load(std::memory_order_relaxed))
|
||
{
|
||
return; // 过时任务:立即退出(调用方会丢弃结果)
|
||
}
|
||
for (std::size_t index = 0; index < out.size(); ++index)
|
||
{
|
||
buffer[index] = std::isfinite(out[index]) ? static_cast<double>(out[index])
|
||
: std::numeric_limits<double>::quiet_NaN();
|
||
}
|
||
ComputeMovingAverage(buffer, radius, stage);
|
||
out.swap(stage);
|
||
if (throttle)
|
||
{
|
||
// 低内存设备的“少量多次”:每阶之间小睡一下,把内存带宽与 CPU 让给前台。
|
||
Sleep(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
namespace
|
||
{
|
||
/**
|
||
* @brief 「弱峰调整」内部使用的极值点(波峰 / 波谷)。
|
||
*
|
||
* ⚠️ **字段顺序是刻意的**:`double value` 放在最前、`index` 用 `uint32`、`type` 用 `int8`,
|
||
* 使结构体只占 **16 字节**(原来的 `size_t+double+int` 是 24 字节,含 7 字节填充)。
|
||
* 一个千万级方波信号的极值数量与采样数同量级(编码器信号几乎每个跳变都是极值),
|
||
* 这张表就是「弱峰调整」工作缓冲的主导项(24 B/极值 ≈ 24 B/采样)——
|
||
* 瘦身后同样规模的整段计算可省下约 1/3 的峰值内存。
|
||
* `uint32` 下标对本项目安全:4.29e9 个 `double` 需要 34 GB 采样内存,不可能达到。
|
||
*/
|
||
struct PeakExtremum
|
||
{
|
||
double value = 0.0; /**< 采样值(保持 double:改成 float 会让结果与历史版本不一致)。 */
|
||
std::uint32_t index = 0; /**< 采样下标。 */
|
||
std::int8_t type = 0; /**< +1 = 波峰,-1 = 波谷。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 三组识别规则各自的调整幅度(每一组都在 -2 – +2,即面板上的 -200% – +200%)。
|
||
*
|
||
* 滑动条的 ±400% 会按"梯度"同时驱动三组规则(见 `ComputeWeakPeakAdjust`):
|
||
* 越宽松的规则参与得越晚、在同一滑动条位置上的幅度越小。
|
||
*/
|
||
struct WeakPeakAmounts
|
||
{
|
||
double first = 0.0; /**< 第一类(严格:两侧均值都要满足)。 */
|
||
double second = 0.0; /**< 第二类(其中一侧满足即可)。 */
|
||
double third = 0.0; /**< 第三类(更宽松:只与左右同类极值比较)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 「弱峰调整」的单阶处理:把偏弱的波峰上拉 / 偏弱的波谷下拉,
|
||
* 或(负幅度)把弱峰本身削弱、压平。
|
||
*
|
||
* **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号):
|
||
* 极值点直接在输入采样上通过单调段的方向翻转识别。三组识别规则(见 `WeakPeakAmounts`)
|
||
* 各自有自己的幅度;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组
|
||
*(= 最严格的那一组),这样"越严格越先被推到目标值"。
|
||
*
|
||
* @param [IN] input 本阶的输入采样值。
|
||
* @param [IN] amounts 三组规则的调整幅度(各 -2 – +2,已夹取;0 = 该组不动)。
|
||
* @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。
|
||
* @param [OUT] extremes 复用的极值表(容量保留,避免每阶重新分配)。
|
||
* @param [IN] cancel 可选的取消标志(为 true 时**立即返回 false**)。
|
||
* @return 确实有采样点被改动返回 true;没有任何点需要调整时返回 false。
|
||
*/
|
||
bool ApplyWeakPeakStage(const std::vector<float>& input, const WeakPeakAmounts& amounts,
|
||
std::vector<float>& output, std::vector<PeakExtremum>& extremes,
|
||
const std::atomic<bool>* cancel)
|
||
{
|
||
const std::size_t count = input.size();
|
||
output.assign(count, std::numeric_limits<float>::quiet_NaN());
|
||
if (count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 先把有效采样原样拷入输出,之后按“极值点之间的平滑偏移”逐点叠加。
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
const float value = input[index];
|
||
if (std::isfinite(value))
|
||
{
|
||
output[index] = value;
|
||
}
|
||
}
|
||
|
||
// 1) 识别波峰 / 波谷:扫描单调段,方向翻转处的上一个采样点即为极值点。
|
||
// 波峰(先升后降)与波谷(先降后升)必然交替出现,
|
||
// 因此同类极值就是极值序列里下标 ±2 的两个。
|
||
extremes.clear();
|
||
// 适度预留:编码器方波约每 8 个采样一个极值,预留 1/8 能让最常见的密集信号
|
||
// 一次到位(避免 push_back 反复扩容带来的 1.5x 峰值内存);宁少勿多,
|
||
// 因为 unreserve 的代价(一次 realloc)远小于无谓地占住内存。
|
||
if (extremes.capacity() < count / 8)
|
||
{
|
||
try
|
||
{
|
||
extremes.reserve(count / 8);
|
||
}
|
||
catch (const std::bad_alloc&)
|
||
{
|
||
// 预留失败不影响正确性:让 push_back 自行增长(低内存时不报错)。
|
||
}
|
||
}
|
||
{
|
||
int direction = 0; // 当前单调段方向(+1 上升、-1 下降)。
|
||
bool hasPrevious = false;
|
||
std::size_t previousIndex = 0;
|
||
double previousValue = 0.0;
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
// 取消检查的粒度:每 1M 个采样一次(单次检查开销可忽略,
|
||
// 但能让“过时任务”在几十毫秒内退出)。
|
||
if (cancel != nullptr && (index & 0xFFFFFu) == 0 && cancel->load(std::memory_order_relaxed))
|
||
{
|
||
return false;
|
||
}
|
||
const double value = static_cast<double>(input[index]);
|
||
if (!std::isfinite(value))
|
||
{
|
||
// 无效采样作为断点:不跨越它判定极值。
|
||
hasPrevious = false;
|
||
continue;
|
||
}
|
||
if (!hasPrevious)
|
||
{
|
||
hasPrevious = true;
|
||
previousIndex = index;
|
||
previousValue = value;
|
||
continue;
|
||
}
|
||
const int step = (value > previousValue) ? 1 : ((value < previousValue) ? -1 : 0);
|
||
if (step != 0)
|
||
{
|
||
if (direction == 0)
|
||
{
|
||
direction = step;
|
||
}
|
||
else if (step != direction)
|
||
{
|
||
// 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。
|
||
PeakExtremum extreme;
|
||
extreme.index = static_cast<std::uint32_t>(previousIndex);
|
||
extreme.value = previousValue;
|
||
extreme.type = static_cast<std::int8_t>(direction);
|
||
extremes.push_back(extreme);
|
||
direction = step;
|
||
}
|
||
}
|
||
previousIndex = index;
|
||
previousValue = value;
|
||
}
|
||
}
|
||
|
||
// 识别不到足够多的极值(常量 / 单调 / 采样点太少):本阶无需任何改动。
|
||
if (extremes.size() < 5)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。
|
||
const std::size_t extremeCount = extremes.size();
|
||
thread_local std::vector<double> shiftBuffer;
|
||
ReuseBufferScope<double> reuseScope(shiftBuffer);
|
||
std::vector<double>& shift = shiftBuffer;
|
||
shift.assign(extremeCount, 0.0);
|
||
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
|
||
{
|
||
const PeakExtremum& current = extremes[k];
|
||
const PeakExtremum& left = extremes[k - 2];
|
||
const PeakExtremum& right = extremes[k + 2];
|
||
if (left.type != current.type || right.type != current.type)
|
||
{
|
||
continue;
|
||
}
|
||
if (!std::isfinite(current.value) || !std::isfinite(left.value) ||
|
||
!std::isfinite(right.value))
|
||
{
|
||
continue;
|
||
}
|
||
// 相邻的**异类**极值(波峰旁边的波谷 / 波谷旁边的波峰)。
|
||
const PeakExtremum& nearLeft = extremes[k - 1];
|
||
const PeakExtremum& nearRight = extremes[k + 1];
|
||
if (nearLeft.type == current.type || nearRight.type == current.type)
|
||
{
|
||
continue; // 极值序列本应交替;异常时不动这一点
|
||
}
|
||
if (!std::isfinite(nearLeft.value) || !std::isfinite(nearRight.value))
|
||
{
|
||
continue;
|
||
}
|
||
const bool isPeak = (current.type > 0);
|
||
// 「左侧/右侧相邻的一个同类极值 + 一个异类极值」的均值。
|
||
const double leftMean = (left.value + nearLeft.value) * 0.5;
|
||
const double rightMean = (nearRight.value + right.value) * 0.5;
|
||
// 同类极值里"较温和"的一侧:波峰取较低者、波谷取较高者。
|
||
const double milderSame = isPeak ? std::min(left.value, right.value)
|
||
: std::max(left.value, right.value);
|
||
const double meanSame = (left.value + right.value) * 0.5;
|
||
// 异类极值里"较近"的一个:波峰取较高的波谷、波谷取较低的波峰。
|
||
const double nearerOpposite = isPeak ? std::max(nearLeft.value, nearRight.value)
|
||
: std::min(nearLeft.value, nearRight.value);
|
||
const double meanOpposite = (nearLeft.value + nearRight.value) * 0.5;
|
||
|
||
// 三组识别规则(以弱峰为例,弱谷把"低于"换成"高于"、max/min 互换):
|
||
// 第一类:**同时**低于左右两侧的(相邻同类极值 + 相邻异类极值)均值;
|
||
// 第二类:低于其中**任意一侧**的该均值(从"且"放宽为"或");
|
||
// 第三类:只与左右**同类**极值比较 —— 低于左右两个同类极值即算弱峰。
|
||
const bool firstClass = isPeak ? ((current.value < leftMean) && (current.value < rightMean))
|
||
: ((current.value > leftMean) && (current.value > rightMean));
|
||
const bool secondClass = isPeak ? ((current.value < leftMean) || (current.value < rightMean))
|
||
: ((current.value > leftMean) || (current.value > rightMean));
|
||
const bool thirdClass = isPeak ? ((current.value < left.value) && (current.value < right.value))
|
||
: ((current.value > left.value) && (current.value > right.value));
|
||
|
||
// 同时满足多组时取**幅度绝对值最大**的那一组(= 最严格的那一组先被推到目标值)。
|
||
double amount = 0.0;
|
||
if (firstClass)
|
||
{
|
||
amount = amounts.first;
|
||
}
|
||
if (secondClass && std::fabs(amounts.second) > std::fabs(amount))
|
||
{
|
||
amount = amounts.second;
|
||
}
|
||
if (thirdClass && std::fabs(amounts.third) > std::fabs(amount))
|
||
{
|
||
amount = amounts.third;
|
||
}
|
||
if (amount == 0.0)
|
||
{
|
||
continue; // 不需要调整
|
||
}
|
||
|
||
double destination = current.value;
|
||
if (amount > 0.0)
|
||
{
|
||
// ---- 正幅度(增强)----
|
||
// 0 – 100%:当前值 → 左右**同类**极值中较温和的一侧(波峰取较低者、波谷取较高者);
|
||
// 100 – 200%:该侧 → 左右**同类**极值的**均值**。
|
||
// ⚠️ 波峰从 `milderSame` 走向 `meanSame`(向上)、波谷同样从 `milderSame`
|
||
// 走向 `meanSame`(此时 `meanSame` 可能比 `milderSame` **低**),
|
||
// 即两者的插值方向一致(都是"milderSame → meanSame"),无需分别取符号。
|
||
destination = (amount <= 1.0)
|
||
? (current.value + amount * (milderSame - current.value))
|
||
: (milderSame + (amount - 1.0) * (meanSame - milderSame));
|
||
}
|
||
else
|
||
{
|
||
// ---- 负幅度(削弱)----
|
||
// 0 – -100%:当前值 → 相邻**异类**极值中较近的一个
|
||
// (波峰取较高的波谷、波谷取较低的波峰);
|
||
// -100 – -200%:该侧 → 相邻异类极值的**均值**。
|
||
const double magnitude = std::min(2.0, -amount);
|
||
destination = (magnitude <= 1.0)
|
||
? (current.value + magnitude * (nearerOpposite - current.value))
|
||
: (nearerOpposite +
|
||
(magnitude - 1.0) * (meanOpposite - nearerOpposite));
|
||
// ⚠️ 削弱**不得反向增强**:目标被夹在"不越过当前值"的一侧。
|
||
if (isPeak)
|
||
{
|
||
destination = std::min(destination, current.value);
|
||
}
|
||
else
|
||
{
|
||
destination = std::max(destination, current.value);
|
||
}
|
||
}
|
||
shift[k] = destination - current.value;
|
||
}
|
||
|
||
// 3) 把偏移量平滑地摊到极值点之间的数据点上:
|
||
// 相邻两个极值点之间用平滑步进过渡(两端导数为 0,波形不会出现折角),
|
||
// 数据首尾则从 0 渐变到最近的极值点偏移。
|
||
// 注意:极值序列的首尾各两个元素的偏移量恒为 0(缺少同类的左右邻居),
|
||
// 因此这里只需要填充 shift 非零的区间。
|
||
bool changed = false;
|
||
const auto applySpan = [&input, &output, count](std::size_t begin, std::size_t end,
|
||
double beginShift, double endShift)
|
||
{
|
||
if (end <= begin)
|
||
{
|
||
if (begin < count && std::isfinite(input[begin]))
|
||
{
|
||
output[begin] = static_cast<float>(input[begin] + beginShift);
|
||
}
|
||
return;
|
||
}
|
||
const double span = static_cast<double>(end - begin);
|
||
for (std::size_t index = begin; index <= end; ++index)
|
||
{
|
||
const float value = input[index];
|
||
if (!std::isfinite(value))
|
||
{
|
||
continue;
|
||
}
|
||
const double t = static_cast<double>(index - begin) / span;
|
||
const double weight = t * t * (3.0 - 2.0 * t);
|
||
output[index] = static_cast<float>(value + beginShift * (1.0 - weight) +
|
||
endShift * weight);
|
||
}
|
||
};
|
||
|
||
for (std::size_t k = 0; k + 1 < extremeCount; ++k)
|
||
{
|
||
if (shift[k] == 0.0 && shift[k + 1] == 0.0)
|
||
{
|
||
continue;
|
||
}
|
||
applySpan(extremes[k].index, extremes[k + 1].index, shift[k], shift[k + 1]);
|
||
changed = true;
|
||
}
|
||
return changed;
|
||
}
|
||
} // namespace
|
||
|
||
void ComputeWeakPeakAdjust(const std::vector<double>& values, double amount, int iterations,
|
||
std::vector<float>& out,
|
||
const std::atomic<bool>* cancel, bool throttle)
|
||
{
|
||
const std::size_t count = values.size();
|
||
out.assign(count, std::numeric_limits<float>::quiet_NaN());
|
||
if (count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 第 1 阶:从原始采样值出发(非有限值保持 NaN)。
|
||
// 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配
|
||
// 并零初始化,单条「弱峰调整」的耗时会被明显放大(内存分配 + page fault)。
|
||
// ⚠️ 用 `ReuseBufferScope` 保证**离开作用域时裁剪超大容量**:否则 4 条后台线程
|
||
// 会各自常驻数百 MB(thread_local 只在线程退出时才析构),内存随线程数放大。
|
||
thread_local std::vector<float> currentBuffer;
|
||
thread_local std::vector<float> nextBuffer;
|
||
thread_local std::vector<PeakExtremum> extremesBuffer;
|
||
ReuseBufferScope<float> currentScope(currentBuffer);
|
||
ReuseBufferScope<float> nextScope(nextBuffer);
|
||
ReuseBufferScope<PeakExtremum> extremesScope(extremesBuffer);
|
||
std::vector<float>& current = currentBuffer;
|
||
current.assign(count, std::numeric_limits<float>::quiet_NaN());
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
const double value = values[index];
|
||
if (std::isfinite(value))
|
||
{
|
||
current[index] = static_cast<float>(value);
|
||
}
|
||
}
|
||
|
||
// 「迭代次数 N」与「均值滤波」的迭代次数语义一致:把同一处理**重复应用 N 次**,
|
||
// 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「幅度」对应的目标值;
|
||
// 多次迭代时相邻的弱峰会互相抬高彼此的基准,从而把较宽的弱峰区逐步恢复(或削弱)。
|
||
const int stages = std::max(1, std::min(iterations, 1000));
|
||
// 滑动条量程 ±400%(`amount` = 百分比 / 100,即 -4 – +4),按"梯度"同时驱动三组规则:
|
||
// 第一类:始终参与,幅度 = clamp(|a|, 0, 2);
|
||
// 第二类:|a| > 100% 起参与,幅度 = clamp(|a| - 1, 0, 2);
|
||
// 第三类:|a| > 200% 起参与,幅度 = clamp(|a| - 2, 0, 2)。
|
||
// 于是滑动条 0–100% 只有第一类在动;100–200% 第一类走完 100–200%、第二类开始走 0–100%;
|
||
// 200–300% 第一类已到上限、第二类走 100–200%、第三类开始走 0–100%;
|
||
// 300–400% 前两类都已到上限,只有第三类在走 100–200%。负值完全对称。
|
||
const double clamped = std::max(-4.0, std::min(4.0, amount));
|
||
const double magnitude = std::fabs(clamped);
|
||
const double sign = (clamped < 0.0) ? -1.0 : 1.0;
|
||
WeakPeakAmounts amounts;
|
||
amounts.first = sign * std::min(magnitude, 2.0);
|
||
amounts.second = sign * std::max(0.0, std::min(2.0, magnitude - 1.0));
|
||
amounts.third = sign * std::max(0.0, std::min(2.0, magnitude - 2.0));
|
||
std::vector<float>& next = nextBuffer;
|
||
for (int stage = 0; stage < stages; ++stage)
|
||
{
|
||
if (cancel != nullptr && cancel->load(std::memory_order_relaxed))
|
||
{
|
||
return; // 过时任务:立即退出(`out` 保留为 NaN,调用方会丢弃它)
|
||
}
|
||
if (!ApplyWeakPeakStage(current, amounts, next, extremesBuffer, cancel))
|
||
{
|
||
if (cancel != nullptr && cancel->load(std::memory_order_relaxed))
|
||
{
|
||
return; // 被取消(而非“本阶无改动”)
|
||
}
|
||
// 这一阶没有任何采样点需要调整:后续迭代次数也不会再有变化,提前结束。
|
||
break;
|
||
}
|
||
current.swap(next);
|
||
if (throttle)
|
||
{
|
||
// 低内存设备的“少量多次”:每阶之间小睡一下,把内存带宽与 CPU 让给前台。
|
||
Sleep(1);
|
||
}
|
||
}
|
||
out.swap(current);
|
||
}
|
||
|
||
const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const
|
||
{
|
||
for (const MeanFilterItem& item : meanFilterItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
return &item;
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
Lane::MeanFilterItem& Lane::EnsureMeanFilterItem(const SignalRef& ref)
|
||
{
|
||
for (MeanFilterItem& item : meanFilterItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
return item;
|
||
}
|
||
}
|
||
MeanFilterItem item;
|
||
item.ref = ref;
|
||
meanFilterItems.push_back(item);
|
||
return meanFilterItems.back();
|
||
}
|
||
|
||
void Lane::RemoveMeanFilterItem(const SignalRef& ref)
|
||
{
|
||
meanFilterItems.erase(
|
||
std::remove_if(meanFilterItems.begin(), meanFilterItems.end(),
|
||
[&ref](const MeanFilterItem& item) { return item.ref == ref; }),
|
||
meanFilterItems.end());
|
||
}
|
||
|
||
const Lane::EdgeCenterItem* Lane::FindEdgeCenterItem(const SignalRef& ref) const
|
||
{
|
||
for (const EdgeCenterItem& item : edgeCenterItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
return &item;
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
Lane::EdgeCenterItem& Lane::EnsureEdgeCenterItem(const SignalRef& ref)
|
||
{
|
||
for (EdgeCenterItem& item : edgeCenterItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
return item;
|
||
}
|
||
}
|
||
EdgeCenterItem item;
|
||
item.ref = ref;
|
||
edgeCenterItems.push_back(item);
|
||
return edgeCenterItems.back();
|
||
}
|
||
|
||
void Lane::RemoveEdgeCenterItem(const SignalRef& ref)
|
||
{
|
||
edgeCenterItems.erase(
|
||
std::remove_if(edgeCenterItems.begin(), edgeCenterItems.end(),
|
||
[&ref](const EdgeCenterItem& item) { return item.ref == ref; }),
|
||
edgeCenterItems.end());
|
||
}
|
||
|
||
const Lane::WeakPeakItem* Lane::FindWeakPeakItem(const SignalRef& ref) const
|
||
{
|
||
for (const WeakPeakItem& item : weakPeakItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
return &item;
|
||
}
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
Lane::WeakPeakItem& Lane::EnsureWeakPeakItem(const SignalRef& ref)
|
||
{
|
||
for (WeakPeakItem& item : weakPeakItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
return item;
|
||
}
|
||
}
|
||
WeakPeakItem item;
|
||
item.ref = ref;
|
||
weakPeakItems.push_back(item);
|
||
return weakPeakItems.back();
|
||
}
|
||
|
||
void Lane::RemoveWeakPeakItem(const SignalRef& ref)
|
||
{
|
||
weakPeakItems.erase(
|
||
std::remove_if(weakPeakItems.begin(), weakPeakItems.end(),
|
||
[&ref](const WeakPeakItem& item) { return item.ref == ref; }),
|
||
weakPeakItems.end());
|
||
}
|
||
|
||
int FindNearestSample(const TimeAxis& axis, double time)
|
||
{
|
||
const std::size_t count = axis.time.size();
|
||
if (count == 0)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
if (axis.monotonic)
|
||
{
|
||
const std::vector<double>::const_iterator lower =
|
||
std::lower_bound(axis.time.begin(), axis.time.end(), time);
|
||
if (lower == axis.time.begin())
|
||
{
|
||
return 0;
|
||
}
|
||
if (lower == axis.time.end())
|
||
{
|
||
return static_cast<int>(count - 1);
|
||
}
|
||
const std::size_t rightIndex = static_cast<std::size_t>(lower - axis.time.begin());
|
||
const std::size_t leftIndex = rightIndex - 1;
|
||
const double leftDistance = std::fabs(time - axis.time[leftIndex]);
|
||
const double rightDistance = std::fabs(axis.time[rightIndex] - time);
|
||
return (leftDistance <= rightDistance) ? static_cast<int>(leftIndex) : static_cast<int>(rightIndex);
|
||
}
|
||
|
||
int best = 0;
|
||
double bestDistance = std::numeric_limits<double>::infinity();
|
||
for (std::size_t index = 0; index < count; ++index)
|
||
{
|
||
if (!std::isfinite(axis.time[index]))
|
||
{
|
||
continue;
|
||
}
|
||
const double distance = std::fabs(axis.time[index] - time);
|
||
if (distance < bestDistance)
|
||
{
|
||
bestDistance = distance;
|
||
best = static_cast<int>(index);
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
bool SampleSignalAt(const TimeAxis& axis, const SignalData& signal, double time, bool interpolate,
|
||
double& value)
|
||
{
|
||
value = kMissing;
|
||
|
||
if (axis.time.empty() || signal.values.empty())
|
||
{
|
||
return false;
|
||
}
|
||
if (time < axis.minTime || time > axis.maxTime)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const std::vector<double>& values = signal.values;
|
||
|
||
if (!axis.monotonic)
|
||
{
|
||
const int nearest = FindNearestSample(axis, time);
|
||
if (nearest < 0 || static_cast<std::size_t>(nearest) >= values.size())
|
||
{
|
||
return false;
|
||
}
|
||
value = values[static_cast<std::size_t>(nearest)];
|
||
return std::isfinite(value);
|
||
}
|
||
|
||
const std::vector<double>::const_iterator lower = std::lower_bound(axis.time.begin(), axis.time.end(), time);
|
||
const std::size_t rightIndex = static_cast<std::size_t>(lower - axis.time.begin());
|
||
if (rightIndex == 0)
|
||
{
|
||
value = values[0];
|
||
return std::isfinite(value);
|
||
}
|
||
if (rightIndex >= axis.time.size())
|
||
{
|
||
value = values.back();
|
||
return std::isfinite(value);
|
||
}
|
||
|
||
const std::size_t leftIndex = rightIndex - 1;
|
||
if (rightIndex >= values.size() || leftIndex >= values.size())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const double t0 = axis.time[leftIndex];
|
||
const double t1 = axis.time[rightIndex];
|
||
const double v0 = values[leftIndex];
|
||
const double v1 = values[rightIndex];
|
||
|
||
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
|
||
{
|
||
const double leftDistance = std::fabs(time - t0);
|
||
const double rightDistance = std::fabs(t1 - time);
|
||
value = (leftDistance <= rightDistance) ? v0 : v1;
|
||
return std::isfinite(value);
|
||
}
|
||
|
||
const double ratio = (time - t0) / (t1 - t0);
|
||
value = v0 + (v1 - v0) * ratio;
|
||
return true;
|
||
}
|
||
|
||
bool SampleDisplayedSignalAt(const TimeAxis& axis, const SignalData& signal,
|
||
const SignalStyle& style, double time, bool interpolate, double& value)
|
||
{
|
||
// 未启用水平镜像 / 时间偏移时与 SampleSignalAt 完全等价
|
||
//(避免两套实现产生细微差异)。
|
||
if (!style.mirrorHorizontal && style.offsetTime == 0.0)
|
||
{
|
||
return SampleSignalAt(axis, signal, time, interpolate, value);
|
||
}
|
||
|
||
value = kMissing;
|
||
if (axis.time.empty() || signal.values.empty())
|
||
{
|
||
return false;
|
||
}
|
||
// 绘制位置 = 源时刻 + offsetTime,因此查询源数据要用「绘制时刻 − offsetTime」。
|
||
const double query = time - style.offsetTime;
|
||
if (query < axis.minTime || query > axis.maxTime)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const std::vector<double>& values = signal.values;
|
||
const std::size_t count = std::min(axis.time.size(), values.size());
|
||
if (count == 0)
|
||
{
|
||
return false;
|
||
}
|
||
// 画面上第 i 个位置用源下标 srcIndex(i) 的值(水平镜像 = 数据反序)。
|
||
const auto sample = [&](std::size_t index) -> double
|
||
{
|
||
return values[SourceIndexFor(style, count, index)];
|
||
};
|
||
|
||
if (!axis.monotonic)
|
||
{
|
||
const int nearest = FindNearestSample(axis, query);
|
||
if (nearest < 0 || static_cast<std::size_t>(nearest) >= count)
|
||
{
|
||
return false;
|
||
}
|
||
value = sample(static_cast<std::size_t>(nearest));
|
||
return std::isfinite(value);
|
||
}
|
||
|
||
const std::vector<double>::const_iterator lower =
|
||
std::lower_bound(axis.time.begin(), axis.time.end(), query);
|
||
const std::size_t rightIndex = static_cast<std::size_t>(lower - axis.time.begin());
|
||
if (rightIndex == 0)
|
||
{
|
||
value = sample(0);
|
||
return std::isfinite(value);
|
||
}
|
||
if (rightIndex >= count)
|
||
{
|
||
value = sample(count - 1);
|
||
return std::isfinite(value);
|
||
}
|
||
|
||
const std::size_t leftIndex = rightIndex - 1;
|
||
const double t0 = axis.time[leftIndex];
|
||
const double t1 = axis.time[rightIndex];
|
||
const double v0 = sample(leftIndex);
|
||
const double v1 = sample(rightIndex);
|
||
|
||
if (!interpolate || !std::isfinite(v0) || !std::isfinite(v1) || !(t1 > t0))
|
||
{
|
||
const double leftDistance = std::fabs(query - t0);
|
||
const double rightDistance = std::fabs(t1 - query);
|
||
value = (leftDistance <= rightDistance) ? v0 : v1;
|
||
return std::isfinite(value);
|
||
}
|
||
|
||
const double ratio = (query - t0) / (t1 - t0);
|
||
value = v0 + (v1 - v0) * ratio;
|
||
return true;
|
||
}
|
||
|
||
TabConfig::TabConfig()
|
||
: m_name(L"视图 1")
|
||
{
|
||
}
|
||
|
||
void TabConfig::SyncToDataSet(const DataSet& dataSet)
|
||
{
|
||
const std::size_t groupCount = dataSet.GroupCount();
|
||
m_visible.resize(groupCount);
|
||
// ⚠️ 只对**新出现的**分组应用默认折叠状态:已存在的分组保持用户当前的展开/折叠选择。
|
||
m_collapsed.resize(groupCount, m_groupsCollapsedByDefault);
|
||
|
||
for (std::size_t groupIndex = 0; groupIndex < groupCount; ++groupIndex)
|
||
{
|
||
const SignalGroup* group = dataSet.GroupAt(static_cast<int>(groupIndex));
|
||
if (group == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 新增信号默认为未勾选(由用户自行决定查看哪些信号),
|
||
// 其显示外观已在 DataSet 中初始化完毕。
|
||
m_visible[groupIndex].resize(group->signals.size(), false);
|
||
}
|
||
|
||
// 清理已失效的网格引用(选中状态由 DataSet 统一维护)。
|
||
for (Lane& lane : m_lanes)
|
||
{
|
||
std::vector<SignalRef>& refs = lane.signals;
|
||
refs.erase(std::remove_if(refs.begin(), refs.end(),
|
||
[&dataSet](const SignalRef& ref) { return dataSet.Resolve(ref) == nullptr; }),
|
||
refs.end());
|
||
}
|
||
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
|
||
[](const Lane& lane) { return lane.signals.empty(); }),
|
||
m_lanes.end());
|
||
|
||
SyncLanes();
|
||
}
|
||
|
||
void TabConfig::RemapAfterGroupRemoval(int groupIndex)
|
||
{
|
||
if (groupIndex < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const std::size_t slot = static_cast<std::size_t>(groupIndex);
|
||
if (slot < m_visible.size())
|
||
{
|
||
m_visible.erase(m_visible.begin() + groupIndex);
|
||
}
|
||
if (slot < m_collapsed.size())
|
||
{
|
||
m_collapsed.erase(m_collapsed.begin() + groupIndex);
|
||
}
|
||
|
||
for (Lane& lane : m_lanes)
|
||
{
|
||
std::vector<SignalRef>& refs = lane.signals;
|
||
refs.erase(std::remove_if(refs.begin(), refs.end(),
|
||
[groupIndex](const SignalRef& ref)
|
||
{ return ref.groupIndex == groupIndex; }),
|
||
refs.end());
|
||
for (SignalRef& ref : refs)
|
||
{
|
||
if (ref.groupIndex > groupIndex)
|
||
{
|
||
--ref.groupIndex;
|
||
}
|
||
}
|
||
}
|
||
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
|
||
[](const Lane& lane) { return lane.signals.empty(); }),
|
||
m_lanes.end());
|
||
}
|
||
|
||
void TabConfig::RemapAfterSignalRemoval(int groupIndex, int signalIndex)
|
||
{
|
||
if (groupIndex < 0 || signalIndex < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 可见性表:删掉该信号对应的那一列。
|
||
const std::size_t slot = static_cast<std::size_t>(groupIndex);
|
||
if (slot < m_visible.size())
|
||
{
|
||
std::vector<bool>& fileVisible = m_visible[slot];
|
||
if (static_cast<std::size_t>(signalIndex) < fileVisible.size())
|
||
{
|
||
fileVisible.erase(fileVisible.begin() + signalIndex);
|
||
}
|
||
}
|
||
|
||
// 网格归属:删掉该信号本身,并把同一文件内位于其后的引用下标前移一位。
|
||
for (Lane& lane : m_lanes)
|
||
{
|
||
std::vector<SignalRef>& refs = lane.signals;
|
||
refs.erase(std::remove_if(refs.begin(), refs.end(),
|
||
[groupIndex, signalIndex](const SignalRef& ref)
|
||
{
|
||
return ref.groupIndex == groupIndex &&
|
||
ref.signalIndex == signalIndex;
|
||
}),
|
||
refs.end());
|
||
for (SignalRef& ref : refs)
|
||
{
|
||
if (ref.groupIndex == groupIndex && ref.signalIndex > signalIndex)
|
||
{
|
||
--ref.signalIndex;
|
||
}
|
||
}
|
||
|
||
// 均值滤波 / 中心轴边沿计数 / 弱峰调整的条目同样持有信号引用,必须一起删除/前移。
|
||
// 三类条目都是「带 `ref` 的结构体」,因此用泛型 lambda 统一处理
|
||
//(弱峰调整的参数与均值滤波不同,不能再共用同一个具体类型)。
|
||
const auto purgeRefs = [groupIndex, signalIndex](auto& items)
|
||
{
|
||
using ItemType = typename std::decay_t<decltype(items)>::value_type;
|
||
items.erase(std::remove_if(items.begin(), items.end(),
|
||
[groupIndex, signalIndex](const ItemType& item)
|
||
{
|
||
return item.ref.groupIndex == groupIndex &&
|
||
item.ref.signalIndex == signalIndex;
|
||
}),
|
||
items.end());
|
||
for (ItemType& item : items)
|
||
{
|
||
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex > signalIndex)
|
||
{
|
||
--item.ref.signalIndex;
|
||
}
|
||
}
|
||
};
|
||
purgeRefs(lane.meanFilterItems);
|
||
purgeRefs(lane.edgeCenterItems);
|
||
purgeRefs(lane.weakPeakItems);
|
||
}
|
||
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
|
||
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
|
||
[](const Lane& lane) { return lane.signals.empty(); }),
|
||
m_lanes.end());
|
||
}
|
||
|
||
bool TabConfig::IsVisible(const SignalRef& ref) const
|
||
{
|
||
if (ref.groupIndex < 0 || static_cast<std::size_t>(ref.groupIndex) >= m_visible.size())
|
||
{
|
||
return false;
|
||
}
|
||
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(ref.groupIndex)];
|
||
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
|
||
{
|
||
return false;
|
||
}
|
||
return fileVisible[static_cast<std::size_t>(ref.signalIndex)];
|
||
}
|
||
|
||
void TabConfig::SetVisible(const SignalRef& ref, bool visible)
|
||
{
|
||
if (ref.groupIndex < 0 || static_cast<std::size_t>(ref.groupIndex) >= m_visible.size())
|
||
{
|
||
return;
|
||
}
|
||
std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(ref.groupIndex)];
|
||
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
|
||
{
|
||
return;
|
||
}
|
||
if (fileVisible[static_cast<std::size_t>(ref.signalIndex)] == visible)
|
||
{
|
||
return;
|
||
}
|
||
fileVisible[static_cast<std::size_t>(ref.signalIndex)] = visible;
|
||
|
||
// 合并模式下新勾选的信号独占一个新网格:切到自定义划分后,
|
||
// RebuildLanes 会把尚未分配网格的可见信号各自放入新网格。
|
||
if (visible && m_laneMode == LaneMode::MERGED)
|
||
{
|
||
m_laneMode = LaneMode::CUSTOM;
|
||
}
|
||
|
||
SyncLanes();
|
||
}
|
||
|
||
void TabConfig::ToggleVisible(const SignalRef& ref)
|
||
{
|
||
SetVisible(ref, !IsVisible(ref));
|
||
}
|
||
|
||
void TabConfig::SetGroupVisible(int groupIndex, bool visible)
|
||
{
|
||
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_visible.size())
|
||
{
|
||
return;
|
||
}
|
||
std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(groupIndex)];
|
||
// 合并模式下“新增”的信号同样应各自独占网格(与 SetVisible 一致):
|
||
// 先切到自定义划分,RebuildLanes 会把尚未分配网格的可见信号追加到最下方。
|
||
// 只有确实存在“原本不可见”的信号时才切模式,避免重复点击默默改掉划分策略。
|
||
const bool anyHidden =
|
||
std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end();
|
||
if (visible && anyHidden && m_laneMode == LaneMode::MERGED)
|
||
{
|
||
m_laneMode = LaneMode::CUSTOM;
|
||
}
|
||
std::fill(fileVisible.begin(), fileVisible.end(), visible);
|
||
SyncLanes();
|
||
}
|
||
|
||
void TabConfig::SetAllVisible(bool visible)
|
||
{
|
||
bool anyHidden = false;
|
||
for (const std::vector<bool>& fileVisible : m_visible)
|
||
{
|
||
if (std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end())
|
||
{
|
||
anyHidden = true;
|
||
break;
|
||
}
|
||
}
|
||
// 同上:合并模式下新增的信号各自独占网格,且排列在最下方。
|
||
if (visible && anyHidden && m_laneMode == LaneMode::MERGED)
|
||
{
|
||
m_laneMode = LaneMode::CUSTOM;
|
||
}
|
||
for (std::vector<bool>& fileVisible : m_visible)
|
||
{
|
||
std::fill(fileVisible.begin(), fileVisible.end(), visible);
|
||
}
|
||
SyncLanes();
|
||
}
|
||
|
||
void TabConfig::SoloSignal(const SignalRef& ref)
|
||
{
|
||
for (std::vector<bool>& fileVisible : m_visible)
|
||
{
|
||
std::fill(fileVisible.begin(), fileVisible.end(), false);
|
||
}
|
||
SetVisible(ref, true);
|
||
}
|
||
|
||
bool TabConfig::GroupHasVisible(int groupIndex) const
|
||
{
|
||
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_visible.size())
|
||
{
|
||
return false;
|
||
}
|
||
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(groupIndex)];
|
||
return std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end();
|
||
}
|
||
|
||
bool TabConfig::GroupAllVisible(int groupIndex) const
|
||
{
|
||
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_visible.size())
|
||
{
|
||
return false;
|
||
}
|
||
const std::vector<bool>& fileVisible = m_visible[static_cast<std::size_t>(groupIndex)];
|
||
if (fileVisible.empty())
|
||
{
|
||
return false;
|
||
}
|
||
return std::find(fileVisible.begin(), fileVisible.end(), false) == fileVisible.end();
|
||
}
|
||
|
||
bool TabConfig::AnyVisible() const
|
||
{
|
||
for (const std::vector<bool>& fileVisible : m_visible)
|
||
{
|
||
if (std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end())
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
int TabConfig::VisibleCount() const
|
||
{
|
||
int count = 0;
|
||
for (const std::vector<bool>& fileVisible : m_visible)
|
||
{
|
||
count += static_cast<int>(std::count(fileVisible.begin(), fileVisible.end(), true));
|
||
}
|
||
return count;
|
||
}
|
||
|
||
void TabConfig::SetMode(LaneMode mode)
|
||
{
|
||
m_laneMode = mode;
|
||
SyncLanes();
|
||
}
|
||
|
||
void TabConfig::SyncLanes()
|
||
{
|
||
RebuildLanes(m_visible, m_lanes, m_laneMode);
|
||
}
|
||
|
||
int TabConfig::LaneOfSignal(const SignalRef& ref) const
|
||
{
|
||
for (std::size_t lane = 0; lane < m_lanes.size(); ++lane)
|
||
{
|
||
const std::vector<SignalRef>& refs = m_lanes[lane].signals;
|
||
if (std::find(refs.begin(), refs.end(), ref) != refs.end())
|
||
{
|
||
return static_cast<int>(lane);
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
int TabConfig::MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex)
|
||
{
|
||
if (dataSet.Resolve(ref) == nullptr)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
const int sourceLane = LaneOfSignal(ref);
|
||
if (laneIndex >= 0 && laneIndex == sourceLane)
|
||
{
|
||
// 拖回原网格:无需变动,避免因网格回收导致顺序抖动。
|
||
return sourceLane;
|
||
}
|
||
|
||
// 先把目标网格的“锚信号”固定下来:移除信号后空网格会被回收,
|
||
// 下标随之变化,必须用不依赖下标的引用找回真正的目标网格
|
||
// (否则会变成“与原网格交换位置”而不是合并进去)。
|
||
SignalRef targetAnchor;
|
||
bool hasTarget = (laneIndex >= 0 && laneIndex < static_cast<int>(m_lanes.size()));
|
||
if (hasTarget)
|
||
{
|
||
const std::vector<SignalRef>& targetSignals = m_lanes[static_cast<std::size_t>(laneIndex)].signals;
|
||
for (const SignalRef& candidate : targetSignals)
|
||
{
|
||
if (!(candidate == ref))
|
||
{
|
||
targetAnchor = candidate;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 手动指定归属意味着切换到自定义划分策略。
|
||
m_laneMode = LaneMode::CUSTOM;
|
||
|
||
for (Lane& lane : m_lanes)
|
||
{
|
||
std::vector<SignalRef>& refs = lane.signals;
|
||
refs.erase(std::remove(refs.begin(), refs.end(), ref), refs.end());
|
||
}
|
||
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
|
||
[](const Lane& lane) { return lane.signals.empty(); }),
|
||
m_lanes.end());
|
||
|
||
int destination = -1;
|
||
if (hasTarget && targetAnchor.IsValid())
|
||
{
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
const std::vector<SignalRef>& refs = m_lanes[index].signals;
|
||
if (std::find(refs.begin(), refs.end(), targetAnchor) != refs.end())
|
||
{
|
||
destination = static_cast<int>(index);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (destination >= 0)
|
||
{
|
||
m_lanes[static_cast<std::size_t>(destination)].signals.push_back(ref);
|
||
}
|
||
else
|
||
{
|
||
Lane lane;
|
||
lane.autoRange = true;
|
||
lane.signals.push_back(ref);
|
||
m_lanes.push_back(std::move(lane));
|
||
}
|
||
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
|
||
}
|
||
return LaneOfSignal(ref);
|
||
}
|
||
|
||
int TabConfig::SplitLane(int laneIndex)
|
||
{
|
||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||
{
|
||
return -1;
|
||
}
|
||
const std::vector<SignalRef> signals = m_lanes[static_cast<std::size_t>(laneIndex)].signals;
|
||
if (signals.size() <= 1)
|
||
{
|
||
// 只有一条信号时无需拆分(本就独占一个网格)。
|
||
return -1;
|
||
}
|
||
|
||
// 手动排布 → 切换到自定义划分策略。
|
||
m_laneMode = LaneMode::CUSTOM;
|
||
|
||
// 用原网格的“网格自身状态”(Y 轴自适应/边沿计数等)作为每一份的初始状态,
|
||
// 这样拆分出来的网格保持原网格的显示设置。
|
||
const Lane source = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||
|
||
std::vector<Lane> pieces;
|
||
pieces.reserve(signals.size());
|
||
for (const SignalRef& ref : signals)
|
||
{
|
||
Lane lane;
|
||
lane.autoRange = source.autoRange;
|
||
lane.yMin = source.yMin;
|
||
lane.yMax = source.yMax;
|
||
lane.fitYToSelection = source.fitYToSelection;
|
||
lane.edgeCount = source.edgeCount;
|
||
lane.edgeValue = source.edgeValue;
|
||
lane.edgeValueValid = source.edgeValueValid;
|
||
lane.edgePanelOpen = source.edgePanelOpen;
|
||
lane.edgePanelX = source.edgePanelX;
|
||
lane.edgePanelY = source.edgePanelY;
|
||
lane.edgeCenter = source.edgeCenter;
|
||
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
|
||
lane.edgeCenterPanelX = source.edgeCenterPanelX;
|
||
lane.edgeCenterPanelY = source.edgeCenterPanelY;
|
||
lane.weakPeak = source.weakPeak;
|
||
lane.weakPeakPanelOpen = source.weakPeakPanelOpen;
|
||
lane.weakPeakPanelX = source.weakPeakPanelX;
|
||
lane.weakPeakPanelY = source.weakPeakPanelY;
|
||
lane.meanFilter = source.meanFilter;
|
||
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
|
||
lane.meanFilterPanelX = source.meanFilterPanelX;
|
||
lane.meanFilterPanelY = source.meanFilterPanelY;
|
||
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整条目。
|
||
lane.meanFilterItems.clear();
|
||
lane.edgeCenterItems.clear();
|
||
lane.weakPeakItems.clear();
|
||
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
lane.meanFilterItems.push_back(item);
|
||
}
|
||
}
|
||
for (const Lane::EdgeCenterItem& item : source.edgeCenterItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
lane.edgeCenterItems.push_back(item);
|
||
}
|
||
}
|
||
for (const Lane::WeakPeakItem& item : source.weakPeakItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
lane.weakPeakItems.push_back(item);
|
||
}
|
||
}
|
||
lane.signals.push_back(ref);
|
||
pieces.push_back(std::move(lane));
|
||
}
|
||
// 用 pieces 替换原网格(插入到原位置,因此其后方的网格整体顺次后移)。
|
||
m_lanes.erase(m_lanes.begin() + laneIndex);
|
||
m_lanes.insert(m_lanes.begin() + laneIndex, pieces.begin(), pieces.end());
|
||
|
||
// 统一重命名,保证编号连续(GX 编号在各显示位置均由此位置推导)。
|
||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||
{
|
||
m_lanes[index].name = L"网格 " + std::to_wstring(index + 1);
|
||
}
|
||
return laneIndex;
|
||
}
|
||
|
||
void TabConfig::RemapAfterSignalInsert(int groupIndex, int insertPos)
|
||
{
|
||
if (groupIndex < 0 || insertPos < 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 可见性表:在插入点补一列(新信号默认不可见,由调用方按需勾选)。
|
||
const std::size_t slot = static_cast<std::size_t>(groupIndex);
|
||
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.groupIndex == groupIndex && ref.signalIndex >= insertPos)
|
||
{
|
||
++ref.signalIndex;
|
||
}
|
||
}
|
||
// 均值滤波 / 中心轴条目同样持有信号引用,必须一起后移。
|
||
for (Lane::MeanFilterItem& item : lane.meanFilterItems)
|
||
{
|
||
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
|
||
{
|
||
++item.ref.signalIndex;
|
||
}
|
||
}
|
||
for (Lane::EdgeCenterItem& item : lane.edgeCenterItems)
|
||
{
|
||
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
|
||
{
|
||
++item.ref.signalIndex;
|
||
}
|
||
}
|
||
for (Lane::WeakPeakItem& item : lane.weakPeakItems)
|
||
{
|
||
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
|
||
{
|
||
++item.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;
|
||
}
|
||
|
||
bool TabConfig::MoveLane(int from, int target)
|
||
{
|
||
const int count = static_cast<int>(m_lanes.size());
|
||
if (from < 0 || from >= count || target < 0 || target > count)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// `target` 语义 = 「插入到该下标之前」;向后拖时扣除自身占位后再比较,
|
||
// 保证"拖到自己原来的位置"不会产生无意义的移动。
|
||
int destination = target;
|
||
if (destination > from)
|
||
{
|
||
--destination;
|
||
}
|
||
destination = std::max(0, std::min(destination, count - 1));
|
||
if (destination == from)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
Lane moved = std::move(m_lanes[static_cast<std::size_t>(from)]);
|
||
m_lanes.erase(m_lanes.begin() + from);
|
||
m_lanes.insert(m_lanes.begin() + destination, std::move(moved));
|
||
|
||
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
|
||
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 groupIndex) const
|
||
{
|
||
if (groupIndex < 0 || static_cast<std::size_t>(groupIndex) >= m_collapsed.size())
|
||
{
|
||
return false;
|
||
}
|
||
return m_collapsed[static_cast<std::size_t>(groupIndex)];
|
||
}
|
||
|
||
void TabConfig::ToggleCollapsed(int groupIndex)
|
||
{
|
||
if (groupIndex < 0)
|
||
{
|
||
return;
|
||
}
|
||
if (static_cast<std::size_t>(groupIndex) >= m_collapsed.size())
|
||
{
|
||
m_collapsed.resize(static_cast<std::size_t>(groupIndex) + 1, m_groupsCollapsedByDefault);
|
||
}
|
||
m_collapsed[static_cast<std::size_t>(groupIndex)] = !m_collapsed[static_cast<std::size_t>(groupIndex)];
|
||
}
|
||
|
||
void TabConfig::ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const
|
||
{
|
||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||
{
|
||
yMin = 0.0;
|
||
yMax = 1.0;
|
||
return;
|
||
}
|
||
|
||
const Lane& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||
if (!lane.autoRange)
|
||
{
|
||
yMin = lane.yMin;
|
||
yMax = lane.yMax;
|
||
if (!(yMax > yMin))
|
||
{
|
||
yMin = 0.0;
|
||
yMax = 1.0;
|
||
}
|
||
return;
|
||
}
|
||
|
||
double low = std::numeric_limits<double>::infinity();
|
||
double high = -std::numeric_limits<double>::infinity();
|
||
for (const SignalRef& ref : lane.signals)
|
||
{
|
||
if (!IsVisible(ref))
|
||
{
|
||
continue;
|
||
}
|
||
const SignalData* signal = dataSet.Resolve(ref);
|
||
if (signal == nullptr || signal->finiteCount == 0)
|
||
{
|
||
continue;
|
||
}
|
||
// ⚠️ 自适应必须基于**画出来的曲线**:先按绘制变换(系数 → 绝对值 → 镜像)把该信号的
|
||
// 原始值域映射成显示值域,否则调整系数/镜像后自适应范围仍按原始数据算(范围错误)。
|
||
const SignalStyle* style = dataSet.Style(ref);
|
||
double signalLow = signal->minValue;
|
||
double signalHigh = signal->maxValue;
|
||
if (style != nullptr)
|
||
{
|
||
TransformValueRange(*style, signalLow, signalHigh);
|
||
}
|
||
low = std::min(low, signalLow);
|
||
high = std::max(high, signalHigh);
|
||
}
|
||
|
||
if (!std::isfinite(low) || !std::isfinite(high))
|
||
{
|
||
low = 0.0;
|
||
high = 1.0;
|
||
}
|
||
PadRange(low, high);
|
||
yMin = low;
|
||
yMax = high;
|
||
}
|
||
|
||
bool TabConfig::ComputeVisibleTimeBounds(const DataSet& dataSet, double& minTime, double& maxTime) const
|
||
{
|
||
double low = std::numeric_limits<double>::infinity();
|
||
double high = -std::numeric_limits<double>::infinity();
|
||
|
||
for (std::size_t groupIndex = 0; groupIndex < m_visible.size(); ++groupIndex)
|
||
{
|
||
const std::vector<bool>& groupVisible = m_visible[groupIndex];
|
||
if (std::find(groupVisible.begin(), groupVisible.end(), true) == groupVisible.end())
|
||
{
|
||
continue;
|
||
}
|
||
|
||
const SignalGroup* group = dataSet.GroupAt(static_cast<int>(groupIndex));
|
||
if (group == nullptr)
|
||
{
|
||
continue;
|
||
}
|
||
// 只统计**可见信号**真正用到的时间轴:一个分组可能有多条时间轴
|
||
//(多 `time` 列 CSV / MF4 多通道组),把没显示的那些也算进来会让“适应视图”
|
||
// 拉出一个过宽的时间范围。
|
||
for (std::size_t signalIndex = 0; signalIndex < groupVisible.size() &&
|
||
signalIndex < group->signals.size();
|
||
++signalIndex)
|
||
{
|
||
if (!groupVisible[signalIndex])
|
||
{
|
||
continue;
|
||
}
|
||
const TimeAxis& axis = group->AxisOf(group->signals[signalIndex]);
|
||
if (axis.finiteCount == 0)
|
||
{
|
||
continue;
|
||
}
|
||
low = std::min(low, axis.minTime);
|
||
high = std::max(high, axis.maxTime);
|
||
}
|
||
}
|
||
|
||
if (!std::isfinite(low) || !std::isfinite(high) || !(high > low))
|
||
{
|
||
return false;
|
||
}
|
||
minTime = low;
|
||
maxTime = high;
|
||
return true;
|
||
}
|
||
|
||
} // namespace jdi
|