Files
Just-DataInspector/Utilities/Src/DataModel.cpp
T
DeepSeek V4.1 Flash 53b981a2a9 全项目大检查:修复数据层/UI 真 BUG、清理死代码与过期注释、并评估二进制体积
按「性能 / BUG / 错误 / 过期注释 / 命名与实际含义不符」四项对全仓做了一次审查,
四个只读子审查交叉核对后逐条回到源码确认再改。

一、数据层真 BUG
- MatH5:U8/U16/U32/U64 用逐字节 Cached() 拼装,而 Cached 只服务「完全落在同一个
  64KB 块内」的请求 => 跨块时静默读成 0(U32 整段为 0、U64 高 32 位为 0),分块 B-tree
  的 chunk 偏移/子节点地址被破坏且无任何报错。新增 ScalarLE():命中缓存用缓存,
  否则退回一次 ReadAt;U64 顺带从 8 次缓存查找降到 1 次。
- Mf4Loader::IsBlock 未校验链接表是否落在块内 => DataOffset 越界,随后
  block_len - (data - offset) 在 uint64 上下溢,得到一个约 2^64 的"可用字节数",
  下游 vector(usable+1) 直接 bad_alloc。同时把 offset + bytes > size 改成不产生回绕的写法。
- DecodeRawValue 的整数路径:bit_offset%8 != 0 且位宽 64 时需 9 个字节,先拼成 64 位
  再整体右移会把最高字节静默丢弃 => 改为逐字节带位偏移累加。
- cg_cycle_count 加「数据段总字节 / 记录字节」上限(压缩段按 deflate 最大膨胀比估算)。
- CsvLoader 表头分支同样跳过 #/% 注释行(否则整份文件按 1 列解析并被丢弃)。
- MatLoader:double -> int 的越界转换加了范围检查;普通对象递归加深度上限与成环检测。
- Utf8Util::BuildTicks:double -> long long 越界转换前先判商是否落在 ±9e18 内。

二、界面 BUG
- 工具栏按钮永远不显示悬停高亮:Paint 读的 Button::hover 全仓从未赋值,改为按 m_hoverIndex 判断。
- 菜单打开时"取消底层拖拽"对每个消息都执行,把菜单自己的 Target::EDIT_FIELD 也清掉
  => 菜单内联输入框无法拖拽框选。排除 EDIT_FIELD。
- GdiplusShutdown 之前未释放 PlotView 的 GDI+ 位图(Application 是进程单例,成员析构在
  WinMain 之后)=> 新增 PlotView::ReleaseGraphicsResources() 并在 Shutdown 里调用。
- WM_KILLFOCUS 现在会提交标签页/绘图区编辑并复位捕获与拖拽状态。
- WM_LBUTTONUP 不再抢走中键平移的鼠标捕获。
- TextEdit:被过滤器拒绝的字符返回 false;无选区时 Ctrl+X 只复制、不再清空整段文本。
- SignalPanel::InvalidateRows 作废编辑/待确认/改名计时等行下标状态。
- EndFilterEdit 改走 ApplyFilterText(否则输入纯空格后回车,列表仍停在筛选结果)。
- 新增 SignalBadgeVisible 作为唯一判据(原来命中只看"可见",绘制还要求"已分配网格")。
- 折起动画结束后夹取滚动量;双击分组名改名时清掉该组正在播放的折起动画。
- ScrollToSignal 复用 SignalScrollTarget(含悬浮组名避让)。
- RowNameRect 与绘制使用同一最小宽度(原来退化时返回空矩形)。
- 分隔条拖拽上限改用与 Relayout 同一公式;UpdateDragCursor 补 SPLITTER/EDIT_FIELD。
- SignalPanel 里几个守卫改为判断真正被解引用的指针;分组行 ✕ 也返回手型光标。

三、PlotView
- FindStyleOnlyLanes 改用完整的 SceneStyleEntry::operator==(原来只比 4 个字段,
  另一个网格同时改"调整"时带着旧像素被当成最新)。
- 宽基底图层不再烘焙拖拽光晕(LaneLayerHash 不含它),改由 PaintHoverHighlight 每帧叠加。
- PaintLaneLegend 提前返回前清掉本网格的命中矩形(否则能点中看不见的图例条目)。
- 面板主体改为完全不透明(消除平移后 4% 透出的旧波形,零额外代价)。
- PAN / Shift+滚轮改走 SetTimeView,使"自动点线"判据随平移重算。
- 内联输入框命中字体与绘制统一为 monoSmall。
- 派生曲线取值与窗口按 SourceIndexFor 映射(水平镜像时不再读到镜像另一端)。
- EdgePanelWidth 补 style/axis 空指针守卫;二分结果夹到 count。

四、清理
- 删除 Toolbar::ActionLabel、Theme::statusBg、PopupMenu 的 editResult 死机制、
  Toolbar::Layout 的重复 clear()、HitRow 的恒假分支、OnDoubleClick 的重复判定、
  PaintUtil 里孤立的注释块。
- 修正一批过期/错误注释(包络带"不做加宽"、LaneLayerHash 的图例说法、
  均值滤波曲线的窗口说法、样式区行数与「调整」按钮顺序、FIT_ALL 退出缩放模式、
  RemoveSignal 的 ✕ 位置、.mat/.dat 格式文档、MatH5 的"映射视图"、DataModel 的
  "点线/被移除文件"、SignalPanel 的"行高连续缩小"、TextEdit 的 PaintUtil 引用等)。

五、二进制体积评估(按要求只评估,未做压缩修改)
- release 809,472 字节;.text 652KB、.xdata+.pdata 约 75KB、.rdata 42KB、
  .rsrc 20KB、.idata 11.5KB。
- 去掉 -ld3d11 -ldwmapi -ld3dcompiler(仓内无任何调用)实测 0 字节收益
  (静态库只拉取被引用的对象)=> 删链接库不是压缩手段,只是配置整洁。
- 仓内已无 <iostream>/<sstream>/<filesystem>/<regex>/<locale> 等体积炸弹。
- 结论:已无明显的浪费可捡;要显著变小只能改变取舍(动态 CRT / UPX / 砍功能),
  需要用户决策,本轮未动。

验证
- editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、convcheck 18/18。
- check 136/137:唯一失败项经 git stash 回到改动前复现同一数值,属该探针自身的既有
  不稳定项(取决于前置用例留下的缓存/时序),与本轮改动无关。
- 真实数据:4 个 MF4 => 283/8/18/24 个信号;.mat 样例 8 信号、
  带无障碍对照-防夹力曲线数据 8 信号、BIG_MAT.mat 5 信号(私有内存 +741MB)。
- make release / make debug 零警告。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 171-180 条硬约束
与文末本轮小节(含体积评估数据)。
2026-10-20 16:10:29 +08:00

1594 lines
52 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 实现分片 —— 本文件只包含 DataModel 的**成员函数定义**(纯代码搬移,行为与拆分前完全一致)。
//
// 共用的常量与自由函数在 DataModelInternal.hpp(命名空间 `dmdetail`,全部 inline)。
// 分片按“职责”划分:核心(状态/布局/几何)、渲染、交互与编辑,见各文件名。
#include "DataModelInternal.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 = 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::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)远小于无谓地占住内存。
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;
}
} // namespace jdi