按「性能 / 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 条硬约束 与文末本轮小节(含体积评估数据)。
712 lines
22 KiB
C++
712 lines
22 KiB
C++
// MATLAB `.mat`(v7.3 / HDF5)加载器。
|
||
//
|
||
// 支持两种内容(详见 MatLoader.hpp 的说明):
|
||
// * Simulink SDI / `SimulationData.Dataset` 导出:`#refs#` 里的元素图 + `#sigstream#` 的样本流;
|
||
// * 普通 v7.3 变量:数值矩阵按"列 = 信号、行 = 采样"读取(时间轴 = 采样序号)。
|
||
#include "MatLoader.hpp"
|
||
|
||
#include "MatH5.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <cstring>
|
||
#include <map>
|
||
#include <set>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace
|
||
{
|
||
/** @brief 一个 SDI 信号(或子数据集)的解析结果。 */
|
||
struct SdiElement
|
||
{
|
||
bool isSignal = false;
|
||
bool isDataset = false;
|
||
std::wstring name;
|
||
std::vector<std::uint64_t> children; /**< 子数据集元素的引用目标。 */
|
||
int dataStream = -1; /**< 数据流下标。 */
|
||
int timeStream = -1; /**< 时间流下标。 */
|
||
};
|
||
|
||
std::wstring TrimNumber(double value)
|
||
{
|
||
wchar_t buffer[64] = {};
|
||
if (value == static_cast<double>(static_cast<long long>(value)))
|
||
{
|
||
std::swprintf(buffer, 64, L"%lld", static_cast<long long>(value));
|
||
}
|
||
else
|
||
{
|
||
std::swprintf(buffer, 64, L"%g", value);
|
||
}
|
||
return buffer;
|
||
}
|
||
|
||
/**
|
||
* @brief 把 SDI 里读到的 `DataR2` / `TimeR2` 数值转成流下标。
|
||
*
|
||
* ⚠️ 不能直接 `static_cast<int>(double)`:文件里那个数可能是 NaN、无穷或超出 `int` 范围,
|
||
* 而浮点 → 整型的越界转换在 C++ 里是**未定义行为**(x86-64 上会得到 `INT_MIN` 之类的
|
||
* 垃圾下标,随后拿去拼数据集名)。非法值统一返回 -1(= 没有流)。
|
||
*/
|
||
int StreamIndexFromDouble(double value)
|
||
{
|
||
if (!std::isfinite(value) || value < 0.0 || value > 2147483647.0)
|
||
{
|
||
return -1;
|
||
}
|
||
return static_cast<int>(value);
|
||
}
|
||
|
||
/** @brief 去掉文件名中的扩展名。 */
|
||
std::wstring StemOf(const std::wstring& path)
|
||
{
|
||
const std::size_t slash = path.find_last_of(L"\\/");
|
||
std::wstring name = (slash == std::wstring::npos) ? path : path.substr(slash + 1);
|
||
const std::size_t dot = name.find_last_of(L'.');
|
||
if (dot != std::wstring::npos && dot > 0)
|
||
{
|
||
name.resize(dot);
|
||
}
|
||
return name;
|
||
}
|
||
|
||
/** @brief 读取一个「流」的全部 double 样本(`#sigstream#/<n>/#data#`)。 */
|
||
bool ReadStream(const MatHdf5& file, std::uint64_t streamObject, std::vector<double>& out)
|
||
{
|
||
out.clear();
|
||
const std::uint64_t data = file.Member(streamObject, "#data#");
|
||
if (data == 0)
|
||
{
|
||
return false;
|
||
}
|
||
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
|
||
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double。
|
||
// `ReadDoubleStream` 会逐 chunk 解压并**直接写入** `out`:
|
||
// 不生成“整个数据集的未压缩字节”,也不做第二次拷贝(GB 级文件的关键)。
|
||
return file.ReadDoubleStream(data, out) && !out.empty();
|
||
}
|
||
|
||
/** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */
|
||
void ParseElement(const MatHdf5& file, std::uint64_t address, SdiElement& element)
|
||
{
|
||
element = SdiElement();
|
||
std::wstring elementType;
|
||
file.ReadText(file.Member(address, "ElementType"), elementType);
|
||
file.ReadText(file.Member(address, "Name"), element.name);
|
||
const std::uint64_t values = file.Member(address, "Values");
|
||
if (elementType == L"dataset")
|
||
{
|
||
element.isDataset = true;
|
||
if (values != 0)
|
||
{
|
||
file.ReadReferences(values, element.children);
|
||
}
|
||
return;
|
||
}
|
||
element.isSignal = true;
|
||
if (values == 0)
|
||
{
|
||
return;
|
||
}
|
||
// 名字为空时退回 timeseries 自己的 Name。
|
||
if (element.name.empty())
|
||
{
|
||
file.ReadText(file.Member(values, "Name"), element.name);
|
||
}
|
||
std::vector<double> numbers;
|
||
if (file.ReadNumbers(file.Member(values, "DataR2"), numbers) && !numbers.empty())
|
||
{
|
||
element.dataStream = StreamIndexFromDouble(numbers.front());
|
||
}
|
||
numbers.clear();
|
||
if (file.ReadNumbers(file.Member(values, "TimeR2"), numbers) && !numbers.empty())
|
||
{
|
||
element.timeStream = StreamIndexFromDouble(numbers.front());
|
||
}
|
||
}
|
||
|
||
/** @brief 追加一条信号(时间 + 数值)到某个分组的时间轴块集合。 */
|
||
void AppendSignal(LoadedSource& source, const std::wstring& name, std::vector<double>&& time,
|
||
std::vector<double>&& values)
|
||
{
|
||
if (time.empty() || values.empty())
|
||
{
|
||
return;
|
||
}
|
||
TableBlock block;
|
||
block.timeHeader = L"time";
|
||
DataTable& table = block.table;
|
||
table.headers.push_back(L"time");
|
||
table.headers.push_back(name);
|
||
table.rowCount = std::min(time.size(), values.size());
|
||
time.resize(table.rowCount);
|
||
values.resize(table.rowCount);
|
||
table.columns.push_back(std::move(time));
|
||
table.columns.push_back(std::move(values));
|
||
source.blocks.push_back(std::move(block));
|
||
}
|
||
|
||
/** @brief 加载进度上报器(`.mat` 解析拆成“读文件”与“解码每个样本流”两段)。 */
|
||
struct ProgressReporter
|
||
{
|
||
const LoadProgress* sink = nullptr; /**< 目标回调(可空)。 */
|
||
double low = 0.0; /**< 本阶段占的进度区间下界。 */
|
||
double high = 1.0; /**< 本阶段占的进度区间上界。 */
|
||
std::uint64_t read = 0; /**< 已读字节。 */
|
||
std::uint64_t total = 0; /**< 总字节。 */
|
||
mutable double lastRatio = -1.0; /**< 已上报的最大比例(保证单调不回退)。 */
|
||
|
||
/** @brief 上报一个 0-1 的阶段内进度(向宿主传递字节数与百分比)。 */
|
||
bool Report(double fraction) const
|
||
{
|
||
if (sink == nullptr || !(*sink))
|
||
{
|
||
return true;
|
||
}
|
||
if (fraction < 0.0)
|
||
{
|
||
fraction = 0.0;
|
||
}
|
||
if (fraction > 1.0)
|
||
{
|
||
fraction = 1.0;
|
||
}
|
||
double ratio = low + (high - low) * fraction;
|
||
// 「已读字节」与「逐流解码」是两条独立进度来源:取较大值,避免进度条回退。
|
||
if (ratio < lastRatio)
|
||
{
|
||
ratio = lastRatio;
|
||
}
|
||
else
|
||
{
|
||
lastRatio = ratio;
|
||
}
|
||
LoadProgressInfo info;
|
||
info.ratio = ratio;
|
||
// 总字节已知时一并上报(遮罩层据此显示“已加载 / 总大小”)。
|
||
info.hasBytes = (total > 0);
|
||
info.bytesRead = (total > 0) ? std::min(read, total) : 0;
|
||
info.totalBytes = total;
|
||
return (*sink)(info);
|
||
}
|
||
};
|
||
|
||
/** @brief SDI(`SimulationData.Dataset`)结构的分组读取。 */
|
||
bool LoadSdi(const MatHdf5& file, std::uint64_t refs, std::uint64_t sigstream,
|
||
const std::wstring& stem, std::vector<LoadedSourceGroup>& groups,
|
||
std::wstring& error, const ProgressReporter& reporter)
|
||
{
|
||
// 1) 枚举 `#refs#` 里的全部元素。
|
||
std::map<std::uint64_t, SdiElement> elements;
|
||
for (const auto& member : file.Members(refs))
|
||
{
|
||
if (file.IsDataset(member.second))
|
||
{
|
||
continue;
|
||
}
|
||
SdiElement element;
|
||
ParseElement(file, member.second, element);
|
||
if (element.isSignal || element.isDataset)
|
||
{
|
||
elements[member.second] = std::move(element);
|
||
}
|
||
}
|
||
if (elements.empty())
|
||
{
|
||
error = L"文件中没有 Simulink 数据元素。";
|
||
return false;
|
||
}
|
||
|
||
// 2) 找出"根"数据集:没有被任何其它数据集当作子元素引用的那些。
|
||
std::set<std::uint64_t> childAddresses;
|
||
for (const auto& entry : elements)
|
||
{
|
||
for (const std::uint64_t child : entry.second.children)
|
||
{
|
||
childAddresses.insert(child);
|
||
}
|
||
}
|
||
std::vector<std::uint64_t> roots;
|
||
for (const auto& entry : elements)
|
||
{
|
||
if (!entry.second.isDataset)
|
||
{
|
||
continue;
|
||
}
|
||
if (childAddresses.count(entry.first) == 0)
|
||
{
|
||
roots.push_back(entry.first);
|
||
}
|
||
}
|
||
if (roots.empty())
|
||
{
|
||
// 没有任何子数据集关系:所有信号归入同一组。
|
||
for (const auto& entry : elements)
|
||
{
|
||
if (entry.second.isSignal)
|
||
{
|
||
roots.push_back(entry.first);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3) 深度优先遍历:路径上的数据集名字用 `-` 连接成分组名。
|
||
struct Pending
|
||
{
|
||
std::uint64_t address = 0;
|
||
std::vector<std::wstring> path;
|
||
int depth = 0;
|
||
};
|
||
std::vector<Pending> stack;
|
||
for (const std::uint64_t root : roots)
|
||
{
|
||
const auto found = elements.find(root);
|
||
if (found == elements.end())
|
||
{
|
||
continue;
|
||
}
|
||
Pending pending;
|
||
pending.address = root;
|
||
if (found->second.isDataset && !found->second.name.empty())
|
||
{
|
||
pending.path.push_back(found->second.name);
|
||
}
|
||
stack.push_back(std::move(pending));
|
||
}
|
||
|
||
// ⚠️ **先收集信号清单、再解码**(用户 2026-10-19 要求:边解析边填充):
|
||
// * 旧实现把**每条**信号的时间列和时间流各拷一份(`streams` 缓存 + `AppendSignal`
|
||
// 里的整份复制)⇒ GB 级文件会出现“文件 + 解压字节 + 每条信号两份 double”叠加;
|
||
// * 现在按「分组 + 时间流」合并成一个 `TableBlock`:时间列只存一份、信号列直接
|
||
// `std::move` 进数据模型,且每条数据流解码完立刻进入最终缓冲。
|
||
struct PendingSignal
|
||
{
|
||
std::wstring groupName;
|
||
std::wstring signalName;
|
||
int timeStream = -1;
|
||
int dataStream = -1;
|
||
};
|
||
std::vector<PendingSignal> signals;
|
||
int unnamedSignals = 0;
|
||
while (!stack.empty())
|
||
{
|
||
Pending pending = std::move(stack.back());
|
||
stack.pop_back();
|
||
const auto found = elements.find(pending.address);
|
||
if (found == elements.end())
|
||
{
|
||
continue;
|
||
}
|
||
const SdiElement& element = found->second;
|
||
if (element.isDataset)
|
||
{
|
||
for (const std::uint64_t child : element.children)
|
||
{
|
||
Pending next;
|
||
next.address = child;
|
||
next.path = pending.path;
|
||
const auto childElement = elements.find(child);
|
||
if (childElement != elements.end() && childElement->second.isDataset &&
|
||
!childElement->second.name.empty())
|
||
{
|
||
next.path.push_back(childElement->second.name);
|
||
}
|
||
stack.push_back(std::move(next));
|
||
}
|
||
continue;
|
||
}
|
||
if (!element.isSignal || element.dataStream < 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
PendingSignal signal;
|
||
for (const std::wstring& piece : pending.path)
|
||
{
|
||
if (!signal.groupName.empty())
|
||
{
|
||
signal.groupName += L"-";
|
||
}
|
||
signal.groupName += piece;
|
||
}
|
||
if (signal.groupName.empty())
|
||
{
|
||
signal.groupName = stem;
|
||
}
|
||
signal.signalName = element.name;
|
||
if (signal.signalName.empty())
|
||
{
|
||
signal.signalName = L"信号" + std::to_wstring(++unnamedSignals);
|
||
}
|
||
signal.timeStream = element.timeStream;
|
||
signal.dataStream = element.dataStream;
|
||
signals.push_back(std::move(signal));
|
||
}
|
||
|
||
// 2) 解码:每个(分组,时间流)一个时间轴块;每条数据流只解码一次并直接进列。
|
||
std::map<std::wstring, std::size_t> groupIndex;
|
||
std::map<std::pair<std::wstring, int>, std::size_t> blockIndex;
|
||
std::size_t decoded = 0;
|
||
|
||
const auto reportStream = [&]()
|
||
{
|
||
++decoded;
|
||
const double fraction = (signals.empty())
|
||
? 1.0
|
||
: static_cast<double>(decoded) /
|
||
static_cast<double>(signals.size());
|
||
return reporter.Report(std::min(1.0, fraction));
|
||
};
|
||
|
||
for (PendingSignal& signal : signals)
|
||
{
|
||
// 分组(每个分组名对应一个 `LoadedSourceGroup`)。
|
||
std::size_t groupSlot = 0;
|
||
const auto existing = groupIndex.find(signal.groupName);
|
||
if (existing == groupIndex.end())
|
||
{
|
||
LoadedSourceGroup group;
|
||
group.name = signal.groupName;
|
||
group.source.sourceName = signal.groupName;
|
||
groups.push_back(std::move(group));
|
||
groupSlot = groups.size() - 1;
|
||
groupIndex.emplace(signal.groupName, groupSlot);
|
||
}
|
||
else
|
||
{
|
||
groupSlot = existing->second;
|
||
}
|
||
LoadedSource& source = groups[groupSlot].source;
|
||
|
||
// 数据列:直接解码到最终 vector(不再经过 `Dataset.data` 的整块字节)。
|
||
std::vector<double> values;
|
||
const std::uint64_t dataObject = file.Member(sigstream, std::to_string(signal.dataStream));
|
||
if (dataObject == 0 || !ReadStream(file, dataObject, values) || values.empty())
|
||
{
|
||
if (!reportStream())
|
||
{
|
||
return false;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 找(或建)该信号所属的时间轴块。
|
||
const std::pair<std::wstring, int> key{signal.groupName, signal.timeStream};
|
||
std::size_t blockSlot = 0;
|
||
const auto blockFound = blockIndex.find(key);
|
||
if (blockFound == blockIndex.end())
|
||
{
|
||
TableBlock block;
|
||
block.timeHeader = L"time";
|
||
std::vector<double> time;
|
||
bool haveTime = false;
|
||
if (signal.timeStream >= 0)
|
||
{
|
||
const std::uint64_t timeObject =
|
||
file.Member(sigstream, std::to_string(signal.timeStream));
|
||
haveTime = timeObject != 0 && ReadStream(file, timeObject, time) &&
|
||
!time.empty();
|
||
}
|
||
if (!haveTime)
|
||
{
|
||
// 时间流缺失:退化为采样序号(秒),保证信号仍可显示(长度在下面裁剪)。
|
||
time.resize(values.size());
|
||
for (std::size_t index = 0; index < values.size(); ++index)
|
||
{
|
||
time[index] = static_cast<double>(index);
|
||
}
|
||
}
|
||
|
||
// ⚠️ **时间轴按内容去重**:SDI 里每条 timeseries 都有自己的时间流,
|
||
// 但内容通常完全一致。若同一分组里已经有一模一样的时间列,就直接复用它
|
||
//(否则每条信号各存一份时间轴:1.29GB / 5 信号的文件会多占 ~500MB)。
|
||
std::size_t reused = SIZE_MAX;
|
||
for (std::size_t index = 0; index < source.blocks.size(); ++index)
|
||
{
|
||
const std::vector<double>& existingTime = source.blocks[index].table.columns[0];
|
||
if (existingTime.size() != time.size())
|
||
{
|
||
continue;
|
||
}
|
||
if (time.empty() ||
|
||
std::memcmp(existingTime.data(), time.data(),
|
||
time.size() * sizeof(double)) == 0)
|
||
{
|
||
reused = index;
|
||
break;
|
||
}
|
||
}
|
||
if (reused != SIZE_MAX)
|
||
{
|
||
blockIndex.emplace(key, reused);
|
||
blockSlot = reused;
|
||
}
|
||
else
|
||
{
|
||
block.table.headers.push_back(block.timeHeader);
|
||
block.table.columns.push_back(std::move(time));
|
||
source.blocks.push_back(std::move(block));
|
||
blockSlot = source.blocks.size() - 1;
|
||
blockIndex.emplace(key, blockSlot);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
blockSlot = blockFound->second;
|
||
}
|
||
|
||
TableBlock& block = source.blocks[blockSlot];
|
||
// 时间列长度与信号不一致(损坏 / 异构)⇒ 单独成块,用采样序号当时间,
|
||
// 保持“每条信号都能显示”的既有语义,而不是把数据截掉或错位。
|
||
if (block.table.columns[0].size() != values.size())
|
||
{
|
||
TableBlock own;
|
||
own.timeHeader = L"time";
|
||
std::vector<double> time(values.size());
|
||
for (std::size_t index = 0; index < values.size(); ++index)
|
||
{
|
||
time[index] = static_cast<double>(index);
|
||
}
|
||
own.table.headers.push_back(own.timeHeader);
|
||
own.table.columns.push_back(std::move(time));
|
||
own.table.headers.push_back(signal.signalName);
|
||
own.table.columns.push_back(std::move(values));
|
||
source.blocks.push_back(std::move(own));
|
||
if (!reportStream())
|
||
{
|
||
return false;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
block.table.headers.push_back(signal.signalName);
|
||
block.table.columns.push_back(std::move(values));
|
||
if (!reportStream())
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 3) 统一行数:同一块内取各列最小长度(时间流与数据流长度不一致时按最小裁剪)。
|
||
for (LoadedSourceGroup& group : groups)
|
||
{
|
||
for (TableBlock& block : group.source.blocks)
|
||
{
|
||
std::size_t rows = SIZE_MAX;
|
||
for (const std::vector<double>& column : block.table.columns)
|
||
{
|
||
rows = std::min(rows, column.size());
|
||
}
|
||
if (rows == SIZE_MAX)
|
||
{
|
||
rows = 0;
|
||
}
|
||
for (std::vector<double>& column : block.table.columns)
|
||
{
|
||
if (column.size() > rows)
|
||
{
|
||
column.resize(rows);
|
||
}
|
||
}
|
||
block.table.rowCount = rows;
|
||
}
|
||
}
|
||
|
||
if (groups.empty())
|
||
{
|
||
error = L"文件中没有可用的信号数据。";
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 普通 v7.3 变量:数值矩阵 / 结构体。
|
||
*
|
||
* ⚠️ 必须带**深度上限**与**已访问集合**:HDF5 允许硬链接成环,损坏文件也可能构造出
|
||
* 极深嵌套,无保护的成员递归会直接栈溢出(崩溃,无错误提示)。
|
||
*/
|
||
void LoadPlainObject(const MatHdf5& file, std::uint64_t address, const std::wstring& name,
|
||
LoadedSource& source, int depth = 0,
|
||
std::set<std::uint64_t>* visited = nullptr)
|
||
{
|
||
constexpr int kMaxDepth = 32;
|
||
if (depth > kMaxDepth)
|
||
{
|
||
return;
|
||
}
|
||
std::set<std::uint64_t> localVisited;
|
||
std::set<std::uint64_t>& seen = (visited != nullptr) ? *visited : localVisited;
|
||
if (address != 0 && !seen.insert(address).second)
|
||
{
|
||
return; // 成环:已经展开过这个对象
|
||
}
|
||
|
||
if (file.IsDataset(address))
|
||
{
|
||
// ⚠️ 只解析元信息(维度),**不要**用 `ReadDataset`:那会把整块数据再解码一遍。
|
||
// 旧实现先 `ReadNumbers` 解码一次、再 `ReadDataset` 只为拿 `dims`,
|
||
// 大矩阵的时间与内存都翻倍。
|
||
MatHdf5::Dataset dataset;
|
||
if (!file.ReadDatasetInfo(address, dataset) || dataset.dims.empty())
|
||
{
|
||
return;
|
||
}
|
||
std::vector<double> numbers;
|
||
if (!file.ReadNumbers(address, numbers) || numbers.empty())
|
||
{
|
||
return;
|
||
}
|
||
std::size_t rows = 1;
|
||
std::size_t columns = 1;
|
||
if (dataset.dims.size() >= 2)
|
||
{
|
||
rows = dataset.dims[0];
|
||
columns = dataset.dims[1];
|
||
}
|
||
else if (dataset.dims.size() == 1)
|
||
{
|
||
rows = dataset.dims[0];
|
||
}
|
||
if (rows == 0 || columns == 0 || rows * columns != numbers.size())
|
||
{
|
||
return;
|
||
}
|
||
// 列 = 信号、行 = 采样(与 MATLAB 的列优先存储一致)。
|
||
for (std::size_t column = 0; column < columns; ++column)
|
||
{
|
||
std::vector<double> values(rows);
|
||
for (std::size_t row = 0; row < rows; ++row)
|
||
{
|
||
values[row] = numbers[column * rows + row];
|
||
}
|
||
std::vector<double> time(rows);
|
||
for (std::size_t row = 0; row < rows; ++row)
|
||
{
|
||
time[row] = static_cast<double>(row);
|
||
}
|
||
const std::wstring signalName =
|
||
(columns == 1) ? name : (name + L"(" + TrimNumber(static_cast<double>(column + 1)) + L")");
|
||
AppendSignal(source, signalName, std::move(time), std::move(values));
|
||
}
|
||
return;
|
||
}
|
||
// 结构体:字段各自作为信号(子结构体递归)。
|
||
for (const auto& member : file.Members(address))
|
||
{
|
||
if (member.first.empty())
|
||
{
|
||
continue;
|
||
}
|
||
std::wstring childName(member.first.begin(), member.first.end());
|
||
LoadPlainObject(file, member.second, childName, source, depth + 1, &seen);
|
||
}
|
||
}
|
||
} // namespace
|
||
|
||
bool LoadMatSourceGroups(const std::wstring& filePath, std::vector<LoadedSourceGroup>& groups,
|
||
std::wstring& error, const LoadProgress& progress)
|
||
{
|
||
error.clear();
|
||
groups.clear();
|
||
MatHdf5 file;
|
||
|
||
// ------------------------------------------------------------------
|
||
// 进度拆成三段:
|
||
// 0.00 – 0.02 打开文件(立刻知道总大小;遮罩层显示「0 / 总大小」)
|
||
// 0.02 – 0.98 随机读取 + 逐条解码样本流(进度按**实际已读字节**推进)
|
||
// 0.98 – 1.00 收尾
|
||
// ⚠️ 旧实现是内存映射:字节瞬间「全部可访问」⇒ 文件读取百分比立刻 100%,
|
||
// 而真正的解析还要跑很久(用户 2026-10-20 报告“这部分不太对劲”)。
|
||
// 现在每次读盘都累计到 `MatHdf5::BytesRead()`,百分比随真实 I/O 推进。
|
||
// ------------------------------------------------------------------
|
||
ProgressReporter parseReporter;
|
||
parseReporter.sink = &progress;
|
||
parseReporter.low = 0.02;
|
||
parseReporter.high = 0.98;
|
||
|
||
const MatHdf5::OpenProgress openProgress =
|
||
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
|
||
{
|
||
parseReporter.read = bytesRead;
|
||
parseReporter.total = totalBytes;
|
||
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
|
||
static_cast<double>(totalBytes)
|
||
: 0.0);
|
||
};
|
||
if (!file.Open(filePath, error, openProgress))
|
||
{
|
||
return false; // 用户取消时 `error` 为空
|
||
}
|
||
|
||
// 流式解码期间的字节进度(每读一块回调一次);返回 false 即取消。
|
||
file.SetReadProgress(
|
||
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
|
||
{
|
||
parseReporter.read = bytesRead;
|
||
parseReporter.total = totalBytes;
|
||
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
|
||
static_cast<double>(totalBytes)
|
||
: 0.0);
|
||
});
|
||
if (!parseReporter.Report(0.0))
|
||
{
|
||
error.clear();
|
||
return false;
|
||
}
|
||
|
||
const std::wstring stem = StemOf(filePath);
|
||
|
||
const std::uint64_t refs = file.Member(file.Root(), "#refs#");
|
||
const std::uint64_t sigstream = file.Member(file.Root(), "#sigstream#");
|
||
if (refs != 0 && sigstream != 0)
|
||
{
|
||
if (!LoadSdi(file, refs, sigstream, stem, groups, error, parseReporter))
|
||
{
|
||
if (file.Cancelled())
|
||
{
|
||
error.clear(); // 用户取消:不显示错误
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LoadedSourceGroup group;
|
||
group.source.sourceName = stem;
|
||
for (const auto& member : file.Members(file.Root()))
|
||
{
|
||
if (member.first.empty() || member.first[0] == '#')
|
||
{
|
||
continue;
|
||
}
|
||
std::wstring name(member.first.begin(), member.first.end());
|
||
LoadPlainObject(file, member.second, name, group.source);
|
||
}
|
||
if (!group.source.HasData())
|
||
{
|
||
error = L".mat 文件中没有可用的数值数据。";
|
||
return false;
|
||
}
|
||
groups.push_back(std::move(group));
|
||
}
|
||
|
||
if (progress)
|
||
{
|
||
LoadProgressInfo info;
|
||
info.ratio = 1.0;
|
||
info.hasBytes = (file.FileBytes() > 0);
|
||
info.bytesRead = file.FileBytes();
|
||
info.totalBytes = file.FileBytes();
|
||
progress(info);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
} // namespace jdi
|