Files
Just-DataInspector/Utilities/Src/DataModel.cpp
T
DeepSeek V4.1 Flash 68d8b7ede6 自适应范围/时机澄清 + 适应视图与 Y 轴自适应互不干扰 + 吸附跟随时间偏移
一、澄清「自适应范围与时机」(并修正记忆里不完善的表述)
 * 准确说法:「额外要求视界内至少有一个可见数据点」这句不完善,完整规则是:
   - 首个网格显示内容时对它做完整自适应 —— **Y 轴与时间轴都**匹配自己的数据范围;
   - 只有一个数据点的信号必须显示在网格**中间**,保证其可见
     (FitSinglePointSignal:数据点居中、时间跨度 1s、Y 轴跨度 1);
   - **后续新增网格只做 Y 轴自适应,不再调整时间轴**。
 * Documents/agent_mem.md 相应章节(§9.30 正文、§11.2 第 58/60 条)已按此改写。

二、修复「适应视图」与「Y 轴自适应」互相干扰
 * 根因:两条自适应路径的留白不同 —— 整段自适应(ComputeLaneRange → PadRange)用 6%,
   「选区 Y 轴自适应」(AutoRangeLaneToView)用 5%。视界未变时两者本应完全相等,
   于是「点过适应视图 → 打开 Y 轴自适应」会算出不同区间、网格被无意义重绘
   (实测切一次 143513 像素),反向操作同样(137326 像素)。
 * 修法:新增 DataModel.hpp 的 PadValueRange() 作为**唯一**留白实现(含退化区间规则
   max(1, |值|×0.1)),PadRange 与 AutoRangeLaneToView 共用它。
 * 结果:「适应视图 → 开自适应 → 关自适应 → 再适应视图」四个步骤上生效区间逐位相同,
   绘图区逐像素 0 差异;而真正改变视界(缩放)时仍然正常重绘。

三、修复「适应视图」在单点信号上让数据点“飞了”
 * FitAll() 开头先尝试 FitSinglePointSignal():仅一个网格、且唯一可见信号只有一个有效
   数据点时,走与首次加载完全相同的单点初始视图(数据点居中、时间跨度 1s、Y 跨度 1),
   并保持幂等(重复点击像素不变)。

四、修复光标吸附未考虑「调整 → 时间轴偏移」
 * 根因:采样点 k 画在 time[k] + offsetTime 处,而光标存的也是显示时间,
   但吸附一直在源时间空间查找 —— 设过时间轴偏移后吸附会落到历史位置。
 * 修法:SignalSnapEnabled / HasVisibleSample / NearestSnapSample / LocalSampleSpacing
   全部增加 timeShift 形参:可见性与点数统计比较 time[k] + timeShift;
   NearestSnapSample 内部用 time - timeShift 回源空间查找、返回时再加回去;
   UpdateAutoScatterStyles(自动点线)同步传入 offsetTime,保持与吸附判定同源。

五、验证(离屏 .preflight/check:SW_HIDE 窗口、不注入真实鼠标键盘、进程/线程 BELOW_NORMAL)
 39/39 通过(连跑 3 次稳定):
 [A] 四个步骤的生效区间逐位相同 + 绘图区逐像素 0 差异(对照组:真缩放重绘 79581 像素);
 [B] 单点信号的适应视图与首次加载一致(1s/1、居中、幂等);
 [C] 首个网格自适应时间轴、后续新增/隐藏信号都不再改动时间轴;
 [D] 偏移 +0.3 时吸到“画面上的点”,偏移 0 行为不变,偏移把点移出视界时不吸附;
 [E] 回归:单点默认样式(点线 + 最大线宽)与“2…16 点不再改点线”均保持。
 A/B:回退三项修复 ⇒ 7 项如实失败(含 143513 / 137326 像素的重绘症状)。
 make release 零警告(用户正运行 Target.exe,链接用临时 -o 名字验证通过)。

六、规则同步
 Documents/agent_mem.md 新增 §9.31、§11.2 第 58–63 条(含修正后的自适应范围/时机规则)
 与变更日志。
2026-10-15 15:46:14 +08:00

2608 lines
83 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 为网格范围补充留白,避免波形紧贴边界。
*
* ⚠️ 实际规则实现在 `DataModel.hpp` 的 `PadValueRange()`:它同时被
* 「选区 Y 轴自适应」使用,两处必须逐位一致(否则两种自适应会算出不同区间)。
*
* @param [IN, OUT] yMin 范围下限。
* @param [IN, OUT] yMax 范围上限。
*/
void PadRange(double& yMin, double& yMax)
{
PadValueRange(yMin, yMax);
}
/**
* @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