Files
Just-DataInspector/Utilities/Src/DataModel.cpp
T
DeepSeek V4.1 Flash 97b361dedd fix(ui): 采样统计显示一致化、GX 拖拽在布局边界下可用、信号「移动到…」与拖拽排序
一、采样间隔/频率显示不一致(用户报告 1ms vs 1000us、100us 0ns)
* FormatDurationCompound 改为「先量化到整数皮秒、再整数分解」,
  彻底消除同一数值因浮点噪声落到不同单位/留下零尾数的问题;
  秒级分支与亚秒分支共用 totalPs 方案,次级进位/单位升级集中在一处。
* FormatFrequency 的单位选择改为按「舍入后的显示值」判定(999.9999999Hz
  与 1000Hz 均为 1.000kHz),并做向下校正避免 0.001kHz。

二、选区截取面板
* 提示文本改为用户指定原文「启用双光标模式以进行选区截取」。
* EdgePanelWidth 计入该提示的实际文本宽度;绘制改用 UiTextClipped 不再省略。
* viewHeight 补回漏减的 bottomPad,修掉条目区与按钮之间的空白。

三、未展开的信号条目栏悬停不再变成手型光标。

四、开启「显示布局边界」后 GX 标签可正常拖拽排序
* 抽出 PaintLaneReorderOverlay(吸附槽位 + 被拖轮廓),贴图与整幅重绘共用。
* 整幅重绘在 LANE_REORDER 时按 LaneDragOffset 逐格平移重画(被拖网格最后画),
  修掉调试模式下不跟随鼠标、无吸附提示、只能盲拖的问题。

五、信号右键新增「移动到…」二级菜单(位于「仅显示该信号」之上)
* 条目为「分组 N(组名)」,当前组打勾;无标题行;底部「新建分组」。
* MenuItem::submenuMaxRows + PopupMenu 独立子菜单滚动(滚动条、滚轮、视口命中过滤),
  IsScrollable 计入子菜单可滚状态。
* 数据层 DataSet::MoveSignalToGroup/MoveSignal 让时间轴随信号搬运
  (独占则移动、共享则复制);TabConfig::RemapAfterSignalMove 改写引用而非先删后插。
* Application::MoveSignalCore 统一入口,保证派生计算暂停、引用修正、选中迁移与同步一致。

六、信号列表支持按住拖拽排序(组内重排 / 跨组移动)
* 4px 阈值进入拖拽;条目脱离原位跟随鼠标(阴影 + 强调色描边 + 插入指示线)。
* 其它信号行/分组行按插入位置做避让位移动画(RowRect 统一叠加插值位移,
  宿主 kScrollTimerId 驱动 AdvanceSignalDragAnimation)。
* 数据层 DataSet::MoveSignal 支持任意位置;TabConfig::RemapAfterSignalReorder 线性重映射。
* 拖拽中禁用组名悬浮;双击改名与 InvalidateRows 均正确终止拖拽态。

验证:删 .o 全量重编译零 error / 零 warning;产物 873,984 B(< 1MB 预警阈值)。
本轮未使用鼠标/键盘/屏幕,未运行任何 GUI 探针,仅编译验证 + 源码级核对。
2026-11-03 19:27:43 +08:00

2050 lines
66 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.
// DataModel 实现分片 —— 数据层核心:信号/分组容器、采样访问、派生曲线计算。
//
// 共用的常量与工具函数在 DataModelInternal.hpp(命名空间 `dmdetail`,全部 inline)。
// ⚠️ 本文件以 `-fno-exceptions` 构建:任何"按数据规模预留内存"的地方都必须用
// `MemReserveIfRoom()` 预检验,不能用 try/catch(见 `MemProbe.hpp`)。
#include "DataModelInternal.hpp"
#include "MemProbe.hpp"
namespace jdi
{
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 为**只有一个有效采样点**的信号设置初始绘制样式。
*
* 单个数据点在「线段」模式下完全画不出来,因此初始就用「散点」并取**最大线宽**
* (散点半径随线宽线性增长,取最大值才能看清那个点)。
*
* ⚠️ 用户 2026-10-16 要求:初始绘制方式由「点线」改为「**散点**」,并且信号面板
* 「样式」行里的「线段」/「点线」两个按钮对该信号**置灰**
* (见 `SignalPanel::ModeOptionEnabled`)。
*
* ⚠️ 曾经还对「有效采样点 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::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;
// 值 → 文本枚举表(MDF4 `cc_type = 7` 等):与列同下标,缺省为空。
if (column < table.columnEnumText.size())
{
signal.enumText = std::move(table.columnEnumText[column]);
}
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 = kDefaultSignalLineWidth;
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::AppendGroupAxis(int groupIndex, TimeAxis&& axis, std::size_t& axisIndex)
{
axisIndex = 0;
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
if (axis.time.empty())
{
return false;
}
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
group.axes.push_back(std::move(axis));
axisIndex = group.axes.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::ForkAxisForSignal(int groupIndex, int signalIndex, std::size_t& axisIndex)
{
axisIndex = 0;
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;
}
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
if (signal.axisIndex >= group.axes.size())
{
return false;
}
// 已经独占 ⇒ 不需要复制(避免每次截取都新增一条轴)。
bool shared = false;
for (std::size_t index = 0; index < group.signals.size(); ++index)
{
if (index == static_cast<std::size_t>(signalIndex))
{
continue;
}
if (group.signals[index].axisIndex == signal.axisIndex)
{
shared = true;
break;
}
}
axisIndex = signal.axisIndex;
if (!shared)
{
return true;
}
// 复制一条新轴(时间数组是只读共享,复制一份即可),并把信号指向它。
TimeAxis copy = group.axes[signal.axisIndex];
group.axes.push_back(std::move(copy));
signal.axisIndex = group.axes.size() - 1;
axisIndex = signal.axisIndex;
return true;
}
bool DataSet::ClipSignalRange(int groupIndex, int signalIndex, double low, double high)
{
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;
}
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
if (signal.axisIndex >= group.axes.size())
{
return false;
}
TimeAxis& axis = group.axes[signal.axisIndex];
const std::size_t count = std::min(axis.time.size(), signal.values.size());
// 1. 逐点筛选:保留 [low, high] 内的采样(时间与值按下标一一对应)。
std::vector<double> keptTime;
std::vector<double> keptValue;
keptTime.reserve(count);
keptValue.reserve(count);
for (std::size_t index = 0; index < count; ++index)
{
const double time = axis.time[index];
if (!std::isfinite(time) || time < low || time > high)
{
continue;
}
keptTime.push_back(time);
keptValue.push_back(signal.values[index]);
}
if (keptValue.size() < 2)
{
return false; // 不足两个有效采样:不改动任何数据
}
// 2. 用「新建 + 交换 + shrink_to_fit」释放多余容量(`erase` 不会归还内存)。
{
std::vector<double> trimmed;
trimmed.swap(keptValue);
signal.values.swap(trimmed);
signal.values.shrink_to_fit();
}
// ⚠️⚠️ **时间轴可能被同组的多条信号共享**(合并网格里常见):
// 只有在"本组没有任何**其它**信号使用同一条轴"时才可以裁剪它;
// 否则裁剪会把其它信号的时间基准一起改短 ⇒ 它们的数据错位
// (用户 2026-11-03 报告:"多个信号同时截取时截取出来的信号并不对")。
// 共享轴的情况由调用方先把该信号改成使用**自己的一条新轴**(见 `Application::ClipSignalInPlace`)。
bool axisShared = false;
for (std::size_t index = 0; index < group.signals.size(); ++index)
{
if (index == static_cast<std::size_t>(signalIndex))
{
continue;
}
if (group.signals[index].axisIndex == signal.axisIndex)
{
axisShared = true;
break;
}
}
if (axisShared)
{
// 共享轴:只裁剪采样值,时间基准保持原样(采样仍按原下标对齐,
// 因此需要把「区间内」的采样**原样**放回原位置?不——那样会让值与时间错位)。
// ⇒ 正确做法:为该信号派生一条**独立时间轴**,把它挂到新轴上。
// 这里直接返回失败,由调用方(宿主)先 `ForkAxisForSignal` 再重试。
return false;
}
{
std::vector<double> trimmedTime;
trimmedTime.swap(keptTime);
axis.time.swap(trimmedTime);
axis.time.shrink_to_fit();
}
// 3. 重算统计与粗粒度包络层(与加载/派生路径同一套实现)。
signal.coarseMin.clear();
signal.coarseMax.clear();
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;
signal.sparseTimes.clear();
signal.sparseValues.clear();
// 4. 时间轴元数据:截取后区间收窄,必须同步(否则"适应视图"会按旧范围取景)。
axis.minTime = axis.time.front();
axis.maxTime = axis.time.back();
axis.finiteCount = axis.time.size();
axis.monotonic = true;
bool monotonic = true;
for (std::size_t index = 1; index < axis.time.size(); ++index)
{
if (!std::isfinite(axis.time[index]) || axis.time[index] < axis.time[index - 1])
{
monotonic = false;
break;
}
}
axis.monotonic = monotonic;
// 有效采样很少时重新缓存全部有效点(与加载路径一致)。
CacheSparseSamples(axis, signal);
return true;
}
bool DataSet::MoveSignal(int fromGroup, int fromSignal, int toGroup, int toSignalIndex,
int& newSignalIndex)
{
newSignalIndex = -1;
if (fromGroup < 0 || toGroup < 0)
{
return false;
}
if (fromGroup >= static_cast<int>(m_groups.size()) ||
toGroup >= static_cast<int>(m_groups.size()))
{
return false;
}
SignalGroup& source = m_groups[static_cast<std::size_t>(fromGroup)];
if (fromSignal < 0 || fromSignal >= static_cast<int>(source.signals.size()))
{
return false;
}
// ---- 组内重排:只改顺序,时间轴与 axisIndex 完全不动 ----
if (fromGroup == toGroup)
{
std::vector<SignalData>& signals = source.signals;
SignalData moved = std::move(signals[static_cast<std::size_t>(fromSignal)]);
signals.erase(signals.begin() + fromSignal);
int position = toSignalIndex;
if (position < 0 || position > static_cast<int>(signals.size()))
{
position = static_cast<int>(signals.size());
}
signals.insert(signals.begin() + position, std::move(moved));
newSignalIndex = position;
return true;
}
// ---- 跨组移动 ----
SignalGroup& target = m_groups[static_cast<std::size_t>(toGroup)];
// 1) 时间轴随信号一起走(详见 `MoveSignalToGroup` 的说明)。
const std::size_t axisIndex = source.signals[static_cast<std::size_t>(fromSignal)].axisIndex;
const std::size_t safeAxis = (axisIndex < source.axes.size()) ? axisIndex : 0;
bool sharedWithOthers = false;
for (std::size_t index = 0; index < source.signals.size(); ++index)
{
if (static_cast<int>(index) != fromSignal && source.signals[index].axisIndex == axisIndex)
{
sharedWithOthers = true;
break;
}
}
std::size_t newAxisIndex = 0;
if (source.axes.empty())
{
target.axes.push_back(TimeAxis());
newAxisIndex = target.axes.size() - 1;
}
else if (sharedWithOthers)
{
target.axes.push_back(source.axes[safeAxis]);
newAxisIndex = target.axes.size() - 1;
}
else
{
target.axes.push_back(std::move(source.axes[safeAxis]));
newAxisIndex = target.axes.size() - 1;
source.axes.erase(source.axes.begin() + safeAxis);
for (SignalData& other : source.signals)
{
if (other.axisIndex > safeAxis)
{
--other.axisIndex;
}
}
}
// 2) 取出信号并插入目标位置的指定下标。
SignalData moved = std::move(source.signals[static_cast<std::size_t>(fromSignal)]);
moved.axisIndex = newAxisIndex;
source.signals.erase(source.signals.begin() + fromSignal);
int position = toSignalIndex;
if (position < 0 || position > static_cast<int>(target.signals.size()))
{
position = static_cast<int>(target.signals.size());
}
target.signals.insert(target.signals.begin() + position, std::move(moved));
newSignalIndex = position;
return true;
}
bool DataSet::MoveSignalToGroup(int fromGroup, int fromSignal, int toGroup, int& newSignalIndex)
{
if (fromGroup == toGroup)
{
newSignalIndex = -1;
return false;
}
const SignalGroup* target = GroupAt(toGroup);
if (target == nullptr)
{
newSignalIndex = -1;
return false;
}
return MoveSignal(fromGroup, fromSignal, toGroup, static_cast<int>(target->signals.size()),
newSignalIndex);
}
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::RenameSignal(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);
if (trimmed.empty())
{
return false;
}
SignalData& signal = group.signals[static_cast<std::size_t>(signalIndex)];
if (trimmed == signal.name)
{
return false;
}
signal.name = std::move(trimmed);
return true;
}
bool DataSet::RenameGroup(int groupIndex, const std::wstring& name)
{
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
{
return false;
}
std::wstring trimmed = Trim(name);
if (trimmed.empty())
{
return false;
}
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
if (trimmed == group.name)
{
return false;
}
group.name = 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)远小于无谓地占住内存。
// ⚠️ 用 `MemReserveIfRoom` 而**不是** try/catch:本项目以 `-fno-exceptions` 构建,
// 分配失败不再抛 `std::bad_alloc` 而是 terminate。预检验在"内存不够"时
// 直接跳过预留 —— 语义等到"少了一次性优化",`push_back` 会自行为增长。
MemReserveIfRoom(extremes, count / 8);
{
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());
}
// ---------------------------------------------------------------- 重采样
const Lane::ResampleItem* Lane::FindResampleItem(const SignalRef& ref) const
{
for (const ResampleItem& item : resampleItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::ResampleItem& Lane::EnsureResampleItem(const SignalRef& ref)
{
for (ResampleItem& item : resampleItems)
{
if (item.ref == ref)
{
return item;
}
}
ResampleItem item;
item.ref = ref;
resampleItems.push_back(item);
return resampleItems.back();
}
void Lane::RemoveResampleItem(const SignalRef& ref)
{
resampleItems.erase(
std::remove_if(resampleItems.begin(), resampleItems.end(),
[&ref](const ResampleItem& item) { return item.ref == ref; }),
resampleItems.end());
}
// ---------------------------------------------------------------- 选区截取
const Lane::ClipItem* Lane::FindClipItem(const SignalRef& ref) const
{
for (const ClipItem& item : clipItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::ClipItem& Lane::EnsureClipItem(const SignalRef& ref)
{
for (ClipItem& item : clipItems)
{
if (item.ref == ref)
{
return item;
}
}
ClipItem item;
item.ref = ref;
clipItems.push_back(item);
return clipItems.back();
}
void Lane::RemoveClipItem(const SignalRef& ref)
{
clipItems.erase(std::remove_if(clipItems.begin(), clipItems.end(),
[&ref](const ClipItem& item) { return item.ref == ref; }),
clipItems.end());
}
// ---------------------------------------------------------------- 自由标记
namespace
{
/**
* @brief 判断两个显示时刻是否算「同一个固定点」。
*
* 固定点是用户用鼠标点出来的,同一位置重复点击不应产生两条;但两次点击之间
* 鼠标可能相差 1 位采样,因此用一个**极小**的相对容差判定而不是精确 `==`。
*/
bool SameMarkerTime(double a, double b)
{
if (a == b)
{
return true;
}
const double scale = std::max(1.0, std::max(std::fabs(a), std::fabs(b)));
return std::fabs(a - b) <= scale * 1.0e-12;
}
} // namespace
int Lane::FindFreeMarker(const SignalRef& ref, double time) const
{
for (std::size_t index = 0; index < freeMarkerItems.size(); ++index)
{
const FreeMarkerItem& item = freeMarkerItems[index];
if (item.ref == ref && SameMarkerTime(item.time, time))
{
return static_cast<int>(index);
}
}
return -1;
}
bool Lane::AddFreeMarker(const SignalRef& ref, double time)
{
if (!ref.IsValid() || !std::isfinite(time))
{
return false;
}
if (FindFreeMarker(ref, time) >= 0)
{
return false; // 同一信号同一位置只保留一个
}
FreeMarkerItem item;
item.ref = ref;
item.time = time;
freeMarkerItems.push_back(item);
return true;
}
void Lane::RemoveFreeMarker(const SignalRef& ref, double time)
{
const int index = FindFreeMarker(ref, time);
if (index >= 0)
{
RemoveFreeMarkerAt(static_cast<std::size_t>(index));
}
}
void Lane::RemoveFreeMarkerAt(std::size_t index)
{
if (index >= freeMarkerItems.size())
{
return;
}
// 用移动 + `pop_back` 而不是 `erase`:固定点不带需要保持地址顺序的引用,
// 但这样能让尾部缓冲立即析构,与其它移除路径保持一致的写法。
for (std::size_t current = index; current + 1 < freeMarkerItems.size(); ++current)
{
freeMarkerItems[current] = freeMarkerItems[current + 1];
}
freeMarkerItems.pop_back();
// 选中下标跟随:删的就是它 ⇒ 取消选中;删在它前面 ⇒ 下标前移。
if (freeMarkerSelected == static_cast<int>(index))
{
freeMarkerSelected = -1;
}
else if (freeMarkerSelected > static_cast<int>(index))
{
--freeMarkerSelected;
}
if (freeMarkerSelected >= static_cast<int>(freeMarkerItems.size()))
{
freeMarkerSelected = -1;
}
}
void Lane::RemoveFreeMarkersOf(const SignalRef& ref)
{
// 选中项的引用先取下来:过滤会打乱下标,事后重新定位才不会错位到别的信号。
SignalRef selectedRef;
double selectedTime = 0.0;
const bool hadSelection = (freeMarkerSelected >= 0 &&
static_cast<std::size_t>(freeMarkerSelected) < freeMarkerItems.size());
if (hadSelection)
{
selectedRef = freeMarkerItems[static_cast<std::size_t>(freeMarkerSelected)].ref;
selectedTime = freeMarkerItems[static_cast<std::size_t>(freeMarkerSelected)].time;
}
freeMarkerItems.erase(
std::remove_if(freeMarkerItems.begin(), freeMarkerItems.end(),
[&ref](const FreeMarkerItem& item) { return item.ref == ref; }),
freeMarkerItems.end());
freeMarkerSelected = -1;
if (hadSelection && !(selectedRef == ref))
{
freeMarkerSelected = FindFreeMarker(selectedRef, selectedTime);
}
}
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;
}
} // namespace jdi