悬停高亮恢复旧版外延观感 + 支持 MATLAB .mat(v7.3 / HDF5,嵌套分组)
一、悬停高亮「几乎看不到、只是曲线换了颜色」(对比提交f845616的旧版观感) 根因:调色板第 1 色就是主题强调色(0xFF268BD2),而上一轮为提速把稠密信号的光晕改成 「按包络带原始几何 FillPath」——与曲线**逐像素同形**,于是曲线上的那层半透明强调色 完全看不出变化;旧版观感来自「lineWidth+4 的描边笔向两侧各外扩 2~3px」。 修法(PlotViewPaint.cpp::PaintSignalHaloFor): 1) 逐列成对填四边形,上下左右都外扩 grow = (lineWidth + 4) * scale / 2 (竖向跃变处旧版 5px 笔涂出的正是那条整高色带); 2) 不能把整条包络拼成一个多边形:满幅方波相邻列区间互相交叉,缠绕填充规则会在交叠处 相互抵消 ⇒ 填充变成一条条竖缝(实测只覆盖约一半带宽); 3) alpha 从 0x55 调回 0x66(与旧版描边笔一致)。 代价仍只在「换信号/换视界/改样式」时付一次,此后每帧只 AlphaBlend。 三维对照(新增 haloshot 探针 +f845616工作树构建):变化像素 旧 73726 / 上一轮 13529 / 本轮 74916,目视与旧版一致(跃变处的整高色带恢复)。 二、MATLAB .mat(v7.3 = HDF5)支持,含嵌套分组 新增只读 HDF5 子集(不引第三方库):Utilities/Inc/MatH5.hpp + Utilities/Src/MatH5.cpp - 超级块 v0/v2;对象头 v1(含续块,主块长度以 headerSize 为准);符号表(B-tree v1 + 本地堆)与 link 消息;数据空间 v1/v2;数据类型 v1;数据布局 v3(紧凑/连续/分块); 分块数据集支持 deflate(zlib);地址一律相对基地址解析。 新增 MATLAB 语义层:Utilities/Inc/MatLoader.hpp + Utilities/Src/MatLoader.cpp 1) Simulink SDI / SimulationData.Dataset(识别 #refs# + #sigstream#): · #refs# 元素有 ElementType(signal/dataset)、Name、Values; · timeseries 的 Values/DataR2、Values/TimeR2 是 #sigstream#/<n> 的流编号; #data# 是 uint8 字节流,按 8 字节一组解释为 double(与 #length#/#sampleSize# 一致); · 子数据集的 Values 是对象引用数组(子元素)⇒ 还原嵌套树; · 分组名 = 从根到信号的各级数据集名用 - 连接(如 A/A1/XYZ → 组 A-A1、信号 XYZ), 信号名取元素 Name(空则退回 timeseries 的 Name,再退回 信号N)。 2) 普通 v7.3 变量:顶层数值矩阵按「列 = 信号、行 = 采样」读取(时间轴 = 采样序号), 结构体按字段递归。 加载管线扩成多分组:新增 LoadSignalSourceGroups()(DataLoader)→ AsyncLoadState::groups → Application::LoadFile 逐个 AddGroup;其它格式仍只有一个分组。 文件对话框与 IsSupportedSignalFile 增加 .mat。 三、验证(全部离屏,不占用鼠标/键盘/屏幕) - 新增 haloshot(截图对照 f845616)、matcheck(15/15:4 组 8 信号、时间轴单调、采样数正确)、 matui(11/11:Application::LoadFile 端到端把 4 个分组加进数据层与信号列表); - 回归 verify 49/49、panelanim 158/158、gridanim 93/93、hoverdrag 20/20、lanescroll 23/23、 datacheck 69/69、edgecheck 46/46、errcheck 19/19、halocheck 16/16、panflash 19/19、 panelstutter 8/8、stress 6/6; - make release / make debug 零警告;Makefile 与探针构建脚本补 -lz。 文档:Documents/agent_mem.md 追加本轮小节(含 4 条新规则:叠加层必须自带外扩、 缠绕填充不可用于自交包络带、HDF5 主消息块长度看 headerSize、 MATLAB 分块维度可能比数据空间多一维、按右侧补 1 对齐)。
This commit is contained in:
1 parent
24f0bf849d
commit
5b24dacee0
12 files changed
+1831
-47
No files matched your search
@@ -361,11 +361,11 @@ namespace jdi
|
||||
* 直到后台完成后再把结果移动进 DataSet(O(1),不会引起卡顿)。
|
||||
*
|
||||
* @param [IN] path 文件路径。
|
||||
* @param [OUT] group 构建好的信号分组。
|
||||
* @param [OUT] groups 构建好的信号分组(多数文件只有 1 个;`.mat` 的嵌套数据集会有多个)。
|
||||
* @param [OUT] error 失败原因描述。
|
||||
* @return 加载成功返回 true;用户取消或失败返回 false。
|
||||
*/
|
||||
bool LoadFileAsync(const std::wstring& path, SignalGroup& group, std::wstring& error);
|
||||
bool LoadFileAsync(const std::wstring& path, std::vector<SignalGroup>& groups, std::wstring& error);
|
||||
|
||||
/**
|
||||
* @brief 绘制加载过渡动画遮罩(在 Render 的最后一步调用,覆盖整个客户区)。
|
||||
|
||||
@@ -212,7 +212,7 @@ namespace jdi
|
||||
std::atomic<unsigned long long> totalBytes{0}; /**< 文件总字节数(0 表示未知)。 */
|
||||
bool ok = false; /**< 加载是否成功。 */
|
||||
std::wstring error; /**< 失败原因。 */
|
||||
SignalGroup group; /**< 构建结果。 */
|
||||
std::vector<SignalGroup> groups; /**< 构建结果(多分组格式可有多个)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -235,31 +235,42 @@ namespace jdi
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
LoadedSource source;
|
||||
std::vector<LoadedSourceGroup> sources;
|
||||
std::wstring error;
|
||||
if (!LoadSignalSource(path, source, error, parseProgress))
|
||||
if (!LoadSignalSourceGroups(path, sources, error, parseProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
LoadProgress buildProgress = [state](const LoadProgressInfo& info)
|
||||
const std::size_t count = std::max<std::size_t>(sources.size(), 1);
|
||||
for (std::size_t index = 0; index < sources.size(); ++index)
|
||||
{
|
||||
state->progress.store(static_cast<float>(0.75 + info.ratio * 0.25),
|
||||
std::memory_order_relaxed);
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
SignalGroup group;
|
||||
if (!BuildSignalGroup(std::move(source), paletteBase, group, error, buildProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
LoadProgress buildProgress = [state, index, count](const LoadProgressInfo& info)
|
||||
{
|
||||
const double base = 0.75 + 0.25 * (static_cast<double>(index) / static_cast<double>(count));
|
||||
state->progress.store(
|
||||
static_cast<float>(base + 0.25 * info.ratio / static_cast<double>(count)),
|
||||
std::memory_order_relaxed);
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
SignalGroup group;
|
||||
if (!BuildSignalGroup(std::move(sources[index].source), paletteBase, group, error,
|
||||
buildProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
// 分组名:`.mat` 的嵌套路径由加载器给出(如 `A-A1`);其它格式为空 ⇒ 用文件名。
|
||||
if (!sources[index].name.empty())
|
||||
{
|
||||
group.name = sources[index].name;
|
||||
}
|
||||
state->groups.push_back(std::move(group));
|
||||
}
|
||||
|
||||
state->group = std::move(group);
|
||||
state->ok = true;
|
||||
state->progress.store(1.0f, std::memory_order_relaxed);
|
||||
state->done.store(true, std::memory_order_release);
|
||||
|
||||
@@ -15,9 +15,9 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
|
||||
SignalGroup group;
|
||||
std::vector<SignalGroup> loaded;
|
||||
std::wstring error;
|
||||
if (!LoadFileAsync(path, group, error))
|
||||
if (!LoadFileAsync(path, loaded, error))
|
||||
{
|
||||
if (!error.empty())
|
||||
{
|
||||
@@ -26,13 +26,28 @@ namespace jdi
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
if (loaded.empty())
|
||||
{
|
||||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
int groupIndex = -1;
|
||||
// 插入新分组会移动 SignalGroup/SignalData 的存储位置,必须先暂停后台派生曲线计算,
|
||||
// 否则后台线程可能正在读取即将被移动的采样数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||||
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
|
||||
int lastGroupIndex = -1;
|
||||
for (SignalGroup& group : loaded)
|
||||
{
|
||||
int groupIndex = -1;
|
||||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||||
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
lastGroupIndex = groupIndex;
|
||||
}
|
||||
if (lastGroupIndex < 0)
|
||||
{
|
||||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
@@ -43,14 +58,14 @@ namespace jdi
|
||||
SyncTabsToDataSet();
|
||||
UpdateWindowTitle();
|
||||
|
||||
const SignalGroup* loaded = m_dataSet.GroupAt(groupIndex);
|
||||
if (loaded != nullptr)
|
||||
const SignalGroup* loadedGroup = m_dataSet.GroupAt(lastGroupIndex);
|
||||
if (loadedGroup != nullptr)
|
||||
{
|
||||
// 行数取各时间轴的**最大值**(多时间轴文件的各轴长度可能不同)。
|
||||
std::size_t rowCount = 0;
|
||||
bool anyNonMonotonic = false;
|
||||
std::wstring timeNames;
|
||||
for (const TimeAxis& axis : loaded->axes)
|
||||
for (const TimeAxis& axis : loadedGroup->axes)
|
||||
{
|
||||
rowCount = std::max(rowCount, axis.Count());
|
||||
anyNonMonotonic = anyNonMonotonic || !axis.monotonic;
|
||||
@@ -60,11 +75,11 @@ namespace jdi
|
||||
}
|
||||
timeNames += axis.name;
|
||||
}
|
||||
std::wstring message = L"已加载 组" + std::to_wstring(groupIndex + 1) + L"|" + loaded->name +
|
||||
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) +
|
||||
L" 行采样," + std::to_wstring(loaded->signals.size()) +
|
||||
L" 条信号," + std::to_wstring(loaded->axes.size()) + L" 条时间轴:" +
|
||||
timeNames + L")";
|
||||
std::wstring message =
|
||||
L"已加载 组" + std::to_wstring(lastGroupIndex + 1) + L"|" + loadedGroup->name +
|
||||
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) + L" 行采样," +
|
||||
std::to_wstring(loadedGroup->signals.size()) + L" 条信号," +
|
||||
std::to_wstring(loadedGroup->axes.size()) + L" 条时间轴:" + timeNames + L")";
|
||||
if (anyNonMonotonic)
|
||||
{
|
||||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||||
@@ -74,7 +89,8 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::LoadFileAsync(const std::wstring& path, SignalGroup& group, std::wstring& error)
|
||||
bool Application::LoadFileAsync(const std::wstring& path, std::vector<SignalGroup>& groups,
|
||||
std::wstring& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
@@ -147,7 +163,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
group = std::move(state.group);
|
||||
groups = std::move(state.groups);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1050,22 +1050,29 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// 稠密信号(走"像素列包络带"):光晕 = **填充与曲线本身同一几何**的包络带。
|
||||
// 稠密信号(走"像素列包络带"):光晕 = **沿包络带向外扩一圈的填充**。
|
||||
//
|
||||
// ⚠️ 绝不能用「加粗折线描边」画稠密信号的光晕:包络带的顶点序列是
|
||||
// 「每列上/下包络交替」的满幅竖锯齿,GDI+ 描边的代价 = 线段数 × 覆盖行数,
|
||||
// 实测 2503 个顶点要 **210ms**。而 `FillPath` 同一条几何只要 ~14ms,
|
||||
// 且观感与原实现(沿折线描粗)一致:整条曲线一起"发光"。
|
||||
// ⚠️ 光晕**必须覆盖整条曲线**:曾经只画鼠标附近 ±72px 的一段,在满幅方波上
|
||||
// 看起来就是一个**长方形色块**(像选区框),用户 2026-10-18 第二轮明确报告。
|
||||
// 代价问题改由 `PaintSignalHaloCached`(渲染一次 + 每帧贴图)解决。
|
||||
// 实测 2503 个顶点要 **210ms**。而 `FillPath` 同一条几何只要 ~14ms。
|
||||
// ⚠️⚠️ **必须向外扩 `grow`(= 描边笔宽的一半)**:折线描边的笔宽是
|
||||
// `lineWidth + 4`,即沿曲线向两侧各扩 2~3px —— 那正是用户看到的"外延高亮描边"。
|
||||
// 曾经直接按包络带原始几何填充,结果光晕与曲线**逐像素同形**:当信号颜色恰好等于
|
||||
// 主题强调色时(调色板第 1 色就是强调色),曲线上的那层半透明强调色完全看不出变化,
|
||||
// 只剩下带宽内部的一点点底色变化 ⇒ 用户报告"高亮几乎看不到、只有曲线换了颜色"
|
||||
//(2026-10-18 第三轮)。扩一圈后与旧版描边的观感一致。
|
||||
// ⚠️ 光晕**必须覆盖整条曲线**:只画鼠标附近一段时,在满幅方波上就是一个长方形色块。
|
||||
// 代价问题由 `PaintSignalHaloCached`(渲染一次 + 每帧贴图)解决。
|
||||
// ------------------------------------------------------------------
|
||||
if (resolved.style->mode != DrawMode::SCATTER && cached.bandMode && !cached.bandBreaks.empty())
|
||||
{
|
||||
// 与旧版 `PaintSignalHalo` 的笔宽同源:`(lineWidth + 4) * scale`,向外取一半。
|
||||
const float grow = (std::max(1.0f, resolved.style->lineWidth) + 4.0f) * m_scale * 0.5f;
|
||||
const int savedSmoothing = g.GetSmoothingMode();
|
||||
g.SetSmoothingMode(gd::SmoothingModeNone); // 半透明填充:抗锯齿没有观感收益
|
||||
gd::GraphicsPath bandPath;
|
||||
bandPath.SetFillMode(gd::FillModeWinding);
|
||||
gd::PointF quad[4];
|
||||
for (std::size_t k = 0; k < cached.bandBreaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = cached.bandBreaks[k];
|
||||
@@ -1076,16 +1083,37 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 段内布局 = 「上包络正向(`half` 个)+ 下包络反向(`half` 个)」。
|
||||
// 段内布局 = 「上包络正向(`half` 个)+ 下包络反向(`half` 个)」,因此
|
||||
// 列 `i` 的上/下包络分别是 `start+i` 与 `start+half+(half-1-i)`。
|
||||
//
|
||||
// ⚠️⚠️ **逐列成对填四边形,而不是把整条包络拼成一个多边形**:满幅方波的
|
||||
// 包络相邻列区间**互相交叉**,拼成单个多边形后缠绕填充规则会在交叠处
|
||||
// 互相抵消 ⇒ 填充结果变成一条条竖缝(实测光晕只覆盖约一半带宽,用户看到
|
||||
// "只有曲线换了个颜色")。逐列四边形永远是简单多边形,不会被挖空。
|
||||
// ⚠️ 横向/纵向都外扩 `grow`:竖向跃变处旧版 5px 笔会涂出一条整高色带,
|
||||
// 只填 1px 宽的列区间根本看不出来。相邻四边形同向、且在同一次 `FillPath`
|
||||
// 内,重叠区域只混合一次(不会叠出更深的竖条)。
|
||||
const std::size_t half = total / 2;
|
||||
bandPath.StartFigure();
|
||||
bandPath.AddLines(cached.band.data() + start, static_cast<INT>(half));
|
||||
bandPath.AddLines(cached.band.data() + start + half, static_cast<INT>(half));
|
||||
bandPath.CloseFigure();
|
||||
for (std::size_t column = 0; column + 1 < half; ++column)
|
||||
{
|
||||
const gd::PointF& topA = cached.band[start + column];
|
||||
const gd::PointF& bottomA = cached.band[start + half + (half - 1 - column)];
|
||||
const gd::PointF& topB = cached.band[start + column + 1];
|
||||
const gd::PointF& bottomB =
|
||||
cached.band[start + half + (half - 1 - (column + 1))];
|
||||
const float highY = std::min(std::min(topA.Y, bottomA.Y), std::min(topB.Y, bottomB.Y));
|
||||
const float lowY = std::max(std::max(topA.Y, bottomA.Y), std::max(topB.Y, bottomB.Y));
|
||||
quad[0] = gd::PointF(topA.X - grow, highY - grow);
|
||||
quad[1] = gd::PointF(topB.X + grow, highY - grow);
|
||||
quad[2] = gd::PointF(topB.X + grow, lowY + grow);
|
||||
quad[3] = gd::PointF(topA.X - grow, lowY + grow);
|
||||
bandPath.AddPolygon(quad, 4);
|
||||
}
|
||||
}
|
||||
if (bandPath.GetPointCount() > 0)
|
||||
{
|
||||
gd::SolidBrush brush(ToGdiColor(WithAlpha(m_theme->accent, 0x55)));
|
||||
// alpha 与旧版描边笔一致(0x66),保证任何信号颜色下观感相同。
|
||||
gd::SolidBrush brush(ToGdiColor(WithAlpha(m_theme->accent, 0x66)));
|
||||
g.FillPath(&brush, &bandPath);
|
||||
}
|
||||
g.SetSmoothingMode(static_cast<gd::SmoothingMode>(savedSmoothing));
|
||||
|
||||
@@ -6194,3 +6194,81 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
||||
3. **"面板只有文字却卡顿"多半不是文字本身的锅**:先量"每行成本 × 可见行数",
|
||||
再量"这一帧是否顺带在做绘图区的重活"(图层预热 / 队列排空)。
|
||||
4. **动画帧里绝不做可延迟的整帧级工作**(图层重建、队列排空);这类工作一律推迟到动画之后。
|
||||
|
||||
---
|
||||
|
||||
## 本轮(2026-10-18 第三轮):悬停高亮恢复旧版观感 + 支持 MATLAB `.mat`(v7.3 / HDF5)
|
||||
|
||||
用户报告:① 全部信号分网格显示时,鼠标悬停的高亮「几乎看不到、就像曲线换了个颜色」,
|
||||
只有放大到一定程度(走折线描边路径)才是旧版效果 —— 要参考提交 `f845616` 的观感;
|
||||
② 需要支持 `TestData/data-sample.mat`(`Simulink.sdi.view` 导出的 `timeseries`),
|
||||
并且**嵌套分组**要变成多个组(`A/A1/XYZ` → 组名 `A-A1`、信号 `XYZ`)。
|
||||
|
||||
### A. ① 的根因:包络带光晕与曲线**逐像素同形**
|
||||
|
||||
调色板第 1 色**就是主题强调色**(`0xFF268BD2`),所以「用强调色给曲线上一层半透明」在
|
||||
曲线像素上完全看不出变化。旧版之所以有「外延描边」,是因为它用 `lineWidth + 4` 的笔
|
||||
沿折线描粗 —— **笔画向两侧各外扩 2~3px**,而那圈外扩落在背景上才显眼。
|
||||
|
||||
上一轮为提速把光晕改成「按包络带原始几何 `FillPath`」,正好丢掉了这圈外扩 ⇒ 用户看到
|
||||
「只是曲线换了个颜色」。本轮(`PlotViewPaint.cpp::PaintSignalHaloFor`):
|
||||
1. **逐列成对填四边形**,上下各外扩 `grow = (lineWidth + 4) * scale / 2`,左右也外扩 `grow`
|
||||
(竖向跃变处旧版 5px 笔涂出的正是那条整高色带,只填 1px 列区间看不出来);
|
||||
2. **不能把整条包络拼成一个多边形**:满幅方波的相邻列区间互相交叉,缠绕填充规则会在
|
||||
交叠处相互抵消 ⇒ 填充变成一条条竖缝(实测只覆盖约一半带宽);
|
||||
3. alpha 从 `0x55` 调回 **`0x66`**(与旧版描边笔一致)。
|
||||
|
||||
代价:光晕仍然只在「换信号 / 换视界 / 改样式」时重建一次(~40ms),此后每帧只 `AlphaBlend`
|
||||
(~2ms)。三维校验(新增 `haloshot` 探针 + `f845616` 工作树对照构建):
|
||||
|
||||
| 版本 | 变化像素 | 覆盖外延 |
|
||||
| --- | --- | --- |
|
||||
| f845616(旧版) | 73726 | 有(跃变处整高色带) |
|
||||
| 上一轮 | 13529 | 无(只有带宽内部) |
|
||||
| 本轮 | 74916 | 有(与旧版逐像素目视一致) |
|
||||
|
||||
### B. ② MATLAB 7.3 `.mat`(HDF5)支持
|
||||
|
||||
`MATLAB 7.3` 的 `.mat` 文件头是 `HDF5`(user block 512 字节)。项目不引入第三方库,
|
||||
新增两个文件实现**只读 HDF5 子集**:
|
||||
- `Utilities/Inc/MatH5.hpp` + `Utilities/Src/MatH5.cpp`:超级块 v0/v2、对象头 v1(含续块)、
|
||||
符号表(B-tree v1 + 本地堆)与 link 消息、数据空间 v1/v2、数据类型 v1、数据布局 v3
|
||||
(紧凑 / 连续 / 分块),分块数据集支持 **deflate**(zlib)。
|
||||
- `Utilities/Inc/MatLoader.hpp` + `Utilities/Src/MatLoader.cpp`:MATLAB 语义层。
|
||||
|
||||
两类内容:
|
||||
1. **Simulink SDI / `SimulationData.Dataset`**(识别 `#refs#` + `#sigstream#`):
|
||||
- `#refs#` 里的每个元素有 `ElementType`(`signal` / `dataset`)、`Name`、`Values`;
|
||||
- 信号(`timeseries`)的 `Values/DataR2`、`Values/TimeR2` 是 **`#sigstream#/<n>` 的流编号**
|
||||
(数据流与时间流成对出现);`#data#` 是 `uint8` 字节流,按 8 字节一组解释为 double,
|
||||
长度与 `#length#`/`#sampleSize#` 一致;
|
||||
- 子数据集元素的 `Values` 是**对象引用数组**(子元素),据此还原嵌套树;
|
||||
- **分组名 = 从根到信号的各级数据集名用 `-` 连接**(用户 2026-10-18 要求),
|
||||
信号名取元素的 `Name`(空则退回 timeseries 的 `Name`,再退回 `信号N`)。
|
||||
2. **普通 v7.3 变量**:顶层数值矩阵按「列 = 信号、行 = 采样」读取(时间轴 = 采样序号),
|
||||
结构体按字段递归。
|
||||
|
||||
`BuildSignalGroup` 只能产出一个分组,因此把加载入口扩成**多分组**:
|
||||
`LoadSignalSourceGroups()`(`DataLoader`)→ `AsyncLoadState::groups` → `Application::LoadFile`
|
||||
逐个 `AddGroup`;其它格式仍然只有一个分组(名字取文件名)。
|
||||
|
||||
### C. 验证(全部离屏,不占用鼠标/键盘/屏幕)
|
||||
|
||||
- 新增 `haloshot`(截图探针,与 `f845616` 工作树对照:旧/新变化像素 73726 / 74916);
|
||||
- 新增 `matcheck`(15/15):4 个分组、8 条信号、时间轴单调、采样数正确;
|
||||
- 新增 `matui`(11/11):`Application::LoadFile` 端到端把 4 个分组加进数据层与信号列表;
|
||||
- 回归:`verify` 49/49、`panelanim` 158/158、`gridanim` 93/93、`hoverdrag` 20/20、
|
||||
`lanescroll` 23/23、`datacheck` 69/69、`edgecheck` 46/46、`errcheck` 19/19、
|
||||
`halocheck` 16/16、`panflash` 19/19、`panelstutter` 8/8、`stress` 6/6;
|
||||
- `make release` / `make debug` 零警告;Makefile 与探针构建脚本均补 `-lz`。
|
||||
|
||||
### D. 教训(新增规则)
|
||||
|
||||
1. **「高亮/选中」这类叠加层必须自己带"外扩"**:若叠加色恰好等于元素本身的颜色,
|
||||
逐像素同形的填充等于没有效果;旧实现的观感来自**描边笔宽**,等效做法是几何外扩半个笔宽。
|
||||
2. **缠绕填充规则( winding )不能用于自交的包络带**:相邻列区间交叉时正负缠绕会抵消,
|
||||
必须逐列成对填充(每段都是简单多边形)。
|
||||
3. 读 HDF5 时**对象头里的 `headerSize` 才是主消息块的长度**(消息数只是总数),
|
||||
否则消息多、需要续块的对象会读到块外垃圾,字段全部解析失败。
|
||||
4. MATLAB 写出的**分块维度可能与数据空间维度维数不同**(1 维数据集的分块是 `[N,1]`),
|
||||
按"右侧补 1"对齐即可解码。
|
||||
@@ -57,6 +57,7 @@ LIB_LINK = \
|
||||
-ld3d11 \
|
||||
-ldwmapi \
|
||||
-ld3dcompiler \
|
||||
-lz \
|
||||
|
||||
# C定义
|
||||
C_DEFS = \
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
#define __DATA_LOADER_HPP__
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "DataTable.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 一个文件加载后的**命名分组**(多数格式只有一个,名字取文件名)。
|
||||
*/
|
||||
struct LoadedSourceGroup
|
||||
{
|
||||
std::wstring name; /**< 分组名(空表示"用源文件名")。 */
|
||||
LoadedSource source; /**< 该分组的数据。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 判断路径是否为应用当前支持的测量文件(按扩展名判断,不访问文件内容)。
|
||||
* @param [IN] filePath 文件路径。
|
||||
@@ -40,6 +50,23 @@ namespace jdi
|
||||
bool LoadSignalSource(const std::wstring& filePath, LoadedSource& source, std::wstring& error,
|
||||
const LoadProgress& progress = LoadProgress());
|
||||
|
||||
/**
|
||||
* @brief 与 `LoadSignalSource` 相同,但允许一个文件产出**多个命名分组**。
|
||||
*
|
||||
* 绝大多数格式只有一个分组(`groups.size() == 1`,`name` 为空 ⇒ 用文件名)。
|
||||
* `.mat`(MATLAB 7.3)里的嵌套数据集会产生多个分组:分组名由"从顶层数据集到信号"
|
||||
* 的路径用 `-` 连接而成(用户 2026-10-18 要求)。
|
||||
*
|
||||
* @param [IN] filePath 文件完整路径。
|
||||
* @param [OUT] groups 读出的分组(至少一个)。
|
||||
* @param [OUT] error 失败原因描述(成功时清空)。
|
||||
* @param [IN] progress 进度回调,可为空。
|
||||
* @return 加载成功返回 true。
|
||||
*/
|
||||
bool LoadSignalSourceGroups(const std::wstring& filePath, std::vector<LoadedSourceGroup>& groups,
|
||||
std::wstring& error,
|
||||
const LoadProgress& progress = LoadProgress());
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __DATA_LOADER_HPP__
|
||||
@@ -0,0 +1,145 @@
|
||||
#ifndef __MAT_H5_HPP__
|
||||
#define __MAT_H5_HPP__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 只读的 HDF5 子集解析器(用于 MATLAB 7.3 的 `.mat` 文件)。
|
||||
*
|
||||
* MATLAB 7.3(`-v7.3`)的 `.mat` 其实是 **HDF5** 文件:数值矩阵是数据集,
|
||||
* 结构体是分组(字段 = 成员),元信息放在属性里(如 `MATLAB_class`)。
|
||||
* 本项目不引入 HDF5 库,因此这里实现一个**刚好够用**的只读子集:
|
||||
*
|
||||
* * 超级块 v0/v1/v2/v3;
|
||||
* * 对象头 v1(含续块消息),v2 仅识别并跳过;
|
||||
* * 分组成员:符号表(B-tree v1 + 本地堆)与 link 消息两种方式;
|
||||
* * 数据空间 v1/v2、数据类型 v1(整数 / 浮点 / 定长字符串 / 引用 / 复合 / vlen);
|
||||
* * 数据布局 v3(紧凑 / 连续 / 分块),分块数据集支持 deflate 压缩(zlib)。
|
||||
*
|
||||
* ⚠️ 不支持:外部数据文件、共享消息表、非 deflate 过滤器、极老的 v1 布局。
|
||||
* 这些在 MATLAB 保存的文件里都不出现。
|
||||
*/
|
||||
class MatHdf5
|
||||
{
|
||||
public:
|
||||
/** @brief 数据类型类别(对应 HDF5 的 class 编号)。 */
|
||||
enum class TypeClass : int
|
||||
{
|
||||
Integer = 0,
|
||||
Float = 1,
|
||||
Time = 2,
|
||||
String = 3,
|
||||
Bitfield = 4,
|
||||
Opaque = 5,
|
||||
Compound = 6,
|
||||
Reference = 7,
|
||||
Enum = 8,
|
||||
VariableLength = 9,
|
||||
Array = 10
|
||||
};
|
||||
|
||||
/** @brief 数据类型(只保留读取 MATLAB 数据所需的字段)。 */
|
||||
struct Type
|
||||
{
|
||||
TypeClass cls = TypeClass::Opaque;
|
||||
int version = 1;
|
||||
std::size_t size = 0; /**< 单个元素占用的字节数。 */
|
||||
bool isSigned = false; /**< 整数是否有符号。 */
|
||||
bool bigEndian = false; /**< 字节序(MATLAB 一律小端)。 */
|
||||
bool isVariableString = false; /**< vlen 字符串。 */
|
||||
std::vector<std::pair<std::string, Type>> members; /**< 复合类型成员。 */
|
||||
};
|
||||
|
||||
/** @brief 数据布局。 */
|
||||
enum class LayoutClass
|
||||
{
|
||||
Compact = 0,
|
||||
Contiguous = 1,
|
||||
Chunked = 2
|
||||
};
|
||||
|
||||
/** @brief 一个数据集的定义与数据。 */
|
||||
struct Dataset
|
||||
{
|
||||
std::vector<std::size_t> dims; /**< 各维长度。 */
|
||||
Type type; /**< 元素类型。 */
|
||||
LayoutClass layout = LayoutClass::Compact;
|
||||
std::vector<std::uint8_t> data; /**< 解码后的原始字节(行优先,按 `dims`)。 */
|
||||
std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */
|
||||
std::vector<std::size_t> chunkDims;
|
||||
bool deflate = false; /**< 是否使用了 deflate(zlib)过滤器。 */
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
/** @brief 打开文件(一次性读入内存;`.mat` 通常几十 MB,可接受)。 */
|
||||
bool Open(const std::wstring& path, std::wstring& error);
|
||||
void Close();
|
||||
|
||||
/** @brief 根分组地址。 */
|
||||
std::uint64_t Root() const { return m_root; }
|
||||
|
||||
/** @brief 该对象是否为数据集(有数据空间消息)。 */
|
||||
bool IsDataset(std::uint64_t address) const;
|
||||
|
||||
/** @brief 分组(或结构体)的成员:`名字 → 对象头地址`。 */
|
||||
std::vector<std::pair<std::string, std::uint64_t>> Members(std::uint64_t address) const;
|
||||
|
||||
/** @brief 按名字取成员(不存在返回 0)。 */
|
||||
std::uint64_t Member(std::uint64_t address, const std::string& name) const;
|
||||
|
||||
/** @brief 读取数据集定义(含解码后的原始数据)。 */
|
||||
bool ReadDataset(std::uint64_t address, Dataset& out) const;
|
||||
|
||||
/** @brief 读取字符串属性(如 `MATLAB_class`、`MATLAB_fields`)。 */
|
||||
std::string AttributeString(std::uint64_t address, const char* name) const;
|
||||
|
||||
/** @brief 读取 `char` 数据集为宽字符串(MATLAB 的 char 是 uint16)。 */
|
||||
bool ReadText(std::uint64_t address, std::wstring& out) const;
|
||||
|
||||
/** @brief 读取数值数据集为 double 序列。 */
|
||||
bool ReadNumbers(std::uint64_t address, std::vector<double>& out) const;
|
||||
|
||||
/** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */
|
||||
bool ReadReferences(std::uint64_t address, std::vector<std::uint64_t>& out) const;
|
||||
|
||||
private:
|
||||
struct Message
|
||||
{
|
||||
std::uint16_t type = 0;
|
||||
std::vector<std::uint8_t> data;
|
||||
};
|
||||
|
||||
std::uint8_t U8(std::uint64_t offset) const;
|
||||
std::uint16_t U16(std::uint64_t offset) const;
|
||||
std::uint32_t U32(std::uint64_t offset) const;
|
||||
std::uint64_t U64(std::uint64_t offset) const;
|
||||
std::uint64_t Addr(std::uint64_t offset) const;
|
||||
std::uint64_t Resolve(std::uint64_t address) const;
|
||||
|
||||
void ReadMessages(std::uint64_t address, std::vector<Message>& messages) const;
|
||||
bool ParseType(const std::uint8_t* data, std::size_t size, std::size_t offset, Type& out,
|
||||
std::size_t& consumed) const;
|
||||
bool DecodeChunked(const Dataset& dataset, std::size_t elementSize,
|
||||
std::vector<std::uint8_t>& out) const;
|
||||
void WalkBtree(std::uint64_t node, std::uint64_t heapData,
|
||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
||||
void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
|
||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const;
|
||||
|
||||
std::vector<std::uint8_t> m_data;
|
||||
std::uint64_t m_base = 0;
|
||||
std::uint64_t m_root = 0;
|
||||
int m_offsetSize = 8;
|
||||
int m_lengthSize = 8;
|
||||
mutable std::vector<std::pair<std::uint64_t, std::uint64_t>> m_heaps; /**< 堆地址 → 数据地址。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __MAT_H5_HPP__
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef __MAT_LOADER_HPP__
|
||||
#define __MAT_LOADER_HPP__
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "DataLoader.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 读取 MATLAB `.mat`(**v7.3 / HDF5**)文件,可能产出**多个分组**。
|
||||
*
|
||||
* 支持两种内容:
|
||||
* 1. **Simulink SDI / `SimulationData.Dataset` 导出**(`#refs#` + `#sigstream#`):
|
||||
* 元素可以是「信号」(timeseries,每个有自己的时间流与数据流)或「子数据集」。
|
||||
* 子数据集就是用户要求的**嵌套分组**:本函数把「从根数据集到信号」的路径上
|
||||
* 每一层数据集的名字用 `-` 连接起来作为分组名(例如 `A/A1/XYZ` → 分组 `A-A1`,
|
||||
* 信号 `XYZ`)。
|
||||
* 2. **普通 v7.3 变量**:顶层数值矩阵按「列 = 信号、行 = 采样」读取,
|
||||
* 时间轴取采样序号;顶层结构体按字段拆成同名分组。
|
||||
*
|
||||
* @param [IN] filePath 文件路径。
|
||||
* @param [OUT] groups 读取结果(每组一个 `LoadedSource`,`name` 为该组名)。
|
||||
* @param [OUT] error 失败原因。
|
||||
* @param [IN] progress 进度回调(可为空)。
|
||||
* @return 至少读出一组数据返回 true。
|
||||
*/
|
||||
bool LoadMatSourceGroups(const std::wstring& filePath, std::vector<LoadedSourceGroup>& groups,
|
||||
std::wstring& error, const LoadProgress& progress);
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __MAT_LOADER_HPP__
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "DataLoader.hpp"
|
||||
|
||||
#include "CsvLoader.hpp"
|
||||
#include "MatLoader.hpp"
|
||||
#include "Mf4Loader.hpp"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
@@ -27,18 +28,38 @@ namespace jdi
|
||||
{
|
||||
const std::wstring extension = ExtensionOf(filePath);
|
||||
return extension == L".csv" || extension == L".txt" || extension == L".mf4" ||
|
||||
extension == L".mdf" || extension == L".dat";
|
||||
extension == L".mdf" || extension == L".dat" || extension == L".mat";
|
||||
}
|
||||
|
||||
const wchar_t* SignalFileDialogFilter()
|
||||
{
|
||||
// GetOpenFileNameW 要求:每个过滤器一对「说明\0通配符\0」,最后以空字符串结束。
|
||||
return L"测量数据文件 (*.csv;*.txt;*.mf4;*.mdf;*.dat)\0*.csv;*.txt;*.mf4;*.mdf;*.dat\0"
|
||||
return L"测量数据文件 (*.csv;*.txt;*.mf4;*.mdf;*.dat;*.mat)\0*.csv;*.txt;*.mf4;*.mdf;*.dat;*.mat\0"
|
||||
L"CSV 文本 (*.csv;*.txt)\0*.csv;*.txt\0"
|
||||
L"ASAM MDF v4 (*.mf4;*.mdf)\0*.mf4;*.mdf\0"
|
||||
L"MATLAB 数据 (*.mat)\0*.mat\0"
|
||||
L"全部文件 (*.*)\0*.*\0";
|
||||
}
|
||||
|
||||
bool LoadSignalSourceGroups(const std::wstring& filePath, std::vector<LoadedSourceGroup>& groups,
|
||||
std::wstring& error, const LoadProgress& progress)
|
||||
{
|
||||
error.clear();
|
||||
groups.clear();
|
||||
const std::wstring extension = ExtensionOf(filePath);
|
||||
if (extension == L".mat")
|
||||
{
|
||||
return LoadMatSourceGroups(filePath, groups, error, progress);
|
||||
}
|
||||
LoadedSourceGroup group;
|
||||
if (!LoadSignalSource(filePath, group.source, error, progress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
groups.push_back(std::move(group));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadSignalSource(const std::wstring& filePath, LoadedSource& source, std::wstring& error,
|
||||
const LoadProgress& progress)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,953 @@
|
||||
// HDF5 只读子集:MATLAB 7.3 `.mat` 解析用(见 MatH5.hpp 的说明)。
|
||||
#include "MatH5.hpp"
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::uint16_t kMessageDataspace = 0x0001;
|
||||
constexpr std::uint16_t kMessageLink = 0x0006;
|
||||
constexpr std::uint16_t kMessageDatatype = 0x0003;
|
||||
constexpr std::uint16_t kMessageLayout = 0x0008;
|
||||
constexpr std::uint16_t kMessageFilter = 0x000B;
|
||||
constexpr std::uint16_t kMessageAttribute = 0x000C;
|
||||
constexpr std::uint16_t kMessageContinuation = 0x0010;
|
||||
constexpr std::uint16_t kMessageSymbolTable = 0x0011;
|
||||
|
||||
std::uint64_t ReadLE(const std::uint8_t* data, std::size_t size)
|
||||
{
|
||||
std::uint64_t value = 0;
|
||||
for (std::size_t index = 0; index < size; ++index)
|
||||
{
|
||||
value |= static_cast<std::uint64_t>(data[index]) << (8 * index);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::uint8_t MatHdf5::U8(std::uint64_t offset) const
|
||||
{
|
||||
return (offset < m_data.size()) ? m_data[static_cast<std::size_t>(offset)] : 0;
|
||||
}
|
||||
|
||||
std::uint16_t MatHdf5::U16(std::uint64_t offset) const
|
||||
{
|
||||
return static_cast<std::uint16_t>(U8(offset)) | (static_cast<std::uint16_t>(U8(offset + 1)) << 8);
|
||||
}
|
||||
|
||||
std::uint32_t MatHdf5::U32(std::uint64_t offset) const
|
||||
{
|
||||
return static_cast<std::uint32_t>(U16(offset)) | (static_cast<std::uint32_t>(U16(offset + 2)) << 16);
|
||||
}
|
||||
|
||||
std::uint64_t MatHdf5::U64(std::uint64_t offset) const
|
||||
{
|
||||
return static_cast<std::uint64_t>(U32(offset)) | (static_cast<std::uint64_t>(U32(offset + 4)) << 32);
|
||||
}
|
||||
|
||||
std::uint64_t MatHdf5::Addr(std::uint64_t offset) const
|
||||
{
|
||||
return (m_offsetSize == 8) ? U64(offset) : U32(offset);
|
||||
}
|
||||
|
||||
std::uint64_t MatHdf5::Resolve(std::uint64_t address) const
|
||||
{
|
||||
if (address == 0 || address == 0xFFFFFFFFFFFFFFFFull)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return address + m_base;
|
||||
}
|
||||
|
||||
bool MatHdf5::Open(const std::wstring& path, std::wstring& error)
|
||||
{
|
||||
error.clear();
|
||||
Close();
|
||||
std::error_code code;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, code);
|
||||
if (code || size == 0)
|
||||
{
|
||||
error = L"无法读取文件大小。";
|
||||
return false;
|
||||
}
|
||||
m_data.resize(static_cast<std::size_t>(size));
|
||||
FILE* file = nullptr;
|
||||
if (_wfopen_s(&file, path.c_str(), L"rb") != 0 || file == nullptr)
|
||||
{
|
||||
error = L"无法打开文件。";
|
||||
return false;
|
||||
}
|
||||
const std::size_t read = std::fread(m_data.data(), 1, m_data.size(), file);
|
||||
std::fclose(file);
|
||||
if (read != m_data.size())
|
||||
{
|
||||
error = L"读取文件失败。";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 超级块:签名可能出现在 0、512、1024…(MATLAB 用 512 字节 user block)。
|
||||
std::size_t signature = m_data.size();
|
||||
for (std::size_t index = 0; index + 8 <= m_data.size() && index <= 4096; index += 512)
|
||||
{
|
||||
if (m_data[index] == 0x89 && m_data[index + 1] == 'H' && m_data[index + 2] == 'D' &&
|
||||
m_data[index + 3] == 'F')
|
||||
{
|
||||
signature = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (signature == m_data.size())
|
||||
{
|
||||
error = L"不是 HDF5(MATLAB 7.3)格式。";
|
||||
return false;
|
||||
}
|
||||
const std::uint8_t version = U8(signature + 8);
|
||||
if (version == 0 || version == 1)
|
||||
{
|
||||
m_offsetSize = U8(signature + 13);
|
||||
m_lengthSize = U8(signature + 14);
|
||||
m_base = U64(signature + 24);
|
||||
const std::uint64_t rootEntry = signature + 24 + 32;
|
||||
m_root = Resolve(Addr(rootEntry + 8));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_offsetSize = U8(signature + 9);
|
||||
m_lengthSize = U8(signature + 10);
|
||||
m_base = U64(signature + 12);
|
||||
m_root = Resolve(U64(signature + 36));
|
||||
}
|
||||
if (m_root == 0 || m_root >= m_data.size())
|
||||
{
|
||||
error = L"HDF5 超级块损坏。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MatHdf5::Close()
|
||||
{
|
||||
m_data.clear();
|
||||
m_base = 0;
|
||||
m_root = 0;
|
||||
m_heaps.clear();
|
||||
}
|
||||
|
||||
void MatHdf5::ReadMessages(std::uint64_t address, std::vector<Message>& messages) const
|
||||
{
|
||||
messages.clear();
|
||||
if (address + 8 > m_data.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (U8(address) == 1)
|
||||
{
|
||||
const std::uint16_t count = U16(address + 2);
|
||||
const std::uint32_t headerSize = U32(address + 8);
|
||||
// 先收集消息块:主块(长度 = 对象头里的 headerSize)+ 若干续块(续块里没有对象头前缀)。
|
||||
// ⚠️ **主块长度必须用 headerSize 限定**:消息数超过主块容量时,其余消息在续块里,
|
||||
// 若在主块里继续按"消息数"往下读,就会读到块外的其它数据(字段全部解析失败)。
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> blocks;
|
||||
blocks.emplace_back(address + 16, headerSize);
|
||||
std::size_t read = 0;
|
||||
for (std::size_t blockIndex = 0; blockIndex < blocks.size(); ++blockIndex)
|
||||
{
|
||||
std::uint64_t offset = blocks[blockIndex].first;
|
||||
const std::uint64_t end =
|
||||
(blocks[blockIndex].second > 0) ? (offset + blocks[blockIndex].second) : 0;
|
||||
while (read < count && offset + 8 <= m_data.size() && (end == 0 || offset + 8 <= end))
|
||||
{
|
||||
Message message;
|
||||
message.type = U16(offset);
|
||||
const std::uint16_t messageSize = U16(offset + 2);
|
||||
offset += 8;
|
||||
if (offset + messageSize > m_data.size())
|
||||
{
|
||||
break;
|
||||
}
|
||||
message.data.assign(m_data.begin() + static_cast<std::ptrdiff_t>(offset),
|
||||
m_data.begin() + static_cast<std::ptrdiff_t>(offset + messageSize));
|
||||
offset += (messageSize + 7u) & ~static_cast<std::uint16_t>(7u);
|
||||
++read;
|
||||
if (message.type == kMessageContinuation && message.data.size() >= 16)
|
||||
{
|
||||
const std::uint64_t blockAddress = Resolve(ReadLE(message.data.data(), 8));
|
||||
const std::uint64_t blockSize = ReadLE(message.data.data() + 8, 8);
|
||||
if (blockAddress != 0)
|
||||
{
|
||||
blocks.emplace_back(blockAddress, blockSize);
|
||||
}
|
||||
}
|
||||
messages.push_back(std::move(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MatHdf5::WalkBtree(std::uint64_t node, std::uint64_t heapData,
|
||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const
|
||||
{
|
||||
int guard = 0;
|
||||
while (node != 0 && node + 24 < m_data.size() && guard++ < 100000)
|
||||
{
|
||||
if (std::memcmp(&m_data[static_cast<std::size_t>(node)], "TREE", 4) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::uint8_t level = U8(node + 5);
|
||||
const std::uint16_t entries = U16(node + 6);
|
||||
std::uint64_t offset = node + 8 + 8 + 8;
|
||||
std::vector<std::uint64_t> children;
|
||||
for (std::uint16_t index = 0; index < static_cast<std::uint16_t>(entries + 1u); ++index)
|
||||
{
|
||||
offset += 8; // key(分组 B-tree 的 key 是堆偏移)
|
||||
if (index < entries)
|
||||
{
|
||||
children.push_back(Resolve(U64(offset)));
|
||||
offset += 8;
|
||||
}
|
||||
}
|
||||
for (const std::uint64_t child : children)
|
||||
{
|
||||
if (level > 0)
|
||||
{
|
||||
WalkBtree(child, heapData, out);
|
||||
}
|
||||
else
|
||||
{
|
||||
WalkSymbolNode(child, heapData, out);
|
||||
}
|
||||
}
|
||||
node = Resolve(U64(node + 8)); // 右兄弟
|
||||
}
|
||||
}
|
||||
|
||||
void MatHdf5::WalkSymbolNode(std::uint64_t node, std::uint64_t heapData,
|
||||
std::vector<std::pair<std::string, std::uint64_t>>& out) const
|
||||
{
|
||||
if (node == 0 || node + 8 > m_data.size() ||
|
||||
std::memcmp(&m_data[static_cast<std::size_t>(node)], "SNOD", 4) != 0 || heapData == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::uint16_t count = U16(node + 6);
|
||||
std::uint64_t offset = node + 8;
|
||||
for (std::uint16_t index = 0; index < count; ++index)
|
||||
{
|
||||
if (offset + 40 > m_data.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::uint64_t nameOffset = U64(offset);
|
||||
const std::uint64_t objectAddress = Resolve(U64(offset + 8));
|
||||
offset += 40;
|
||||
if (objectAddress == 0 || heapData + nameOffset >= m_data.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const char* name = reinterpret_cast<const char*>(m_data.data() + heapData + nameOffset);
|
||||
out.emplace_back(std::string(name), objectAddress);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::uint64_t>> MatHdf5::Members(std::uint64_t address) const
|
||||
{
|
||||
std::vector<std::pair<std::string, std::uint64_t>> out;
|
||||
std::vector<Message> messages;
|
||||
ReadMessages(address, messages);
|
||||
for (const Message& message : messages)
|
||||
{
|
||||
if (message.type == kMessageSymbolTable && message.data.size() >= 16)
|
||||
{
|
||||
const std::uint64_t btree = Resolve(ReadLE(message.data.data(), 8));
|
||||
const std::uint64_t heap = Resolve(ReadLE(message.data.data() + 8, 8));
|
||||
std::uint64_t heapData = 0;
|
||||
if (heap + 32 <= m_data.size() &&
|
||||
std::memcmp(&m_data[static_cast<std::size_t>(heap)], "HEAP", 4) == 0)
|
||||
{
|
||||
heapData = Resolve(U64(heap + 24));
|
||||
bool known = false;
|
||||
for (const auto& entry : m_heaps)
|
||||
{
|
||||
if (entry.first == heap)
|
||||
{
|
||||
known = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!known)
|
||||
{
|
||||
m_heaps.emplace_back(heap, heapData);
|
||||
}
|
||||
}
|
||||
WalkBtree(btree, heapData, out);
|
||||
}
|
||||
else if (message.type == kMessageLink && message.data.size() >= 8)
|
||||
{
|
||||
std::size_t offset = 2;
|
||||
const std::uint8_t flags = message.data[1];
|
||||
if (flags & 0x08)
|
||||
{
|
||||
offset += 8;
|
||||
}
|
||||
if (flags & 0x04)
|
||||
{
|
||||
offset += 8;
|
||||
}
|
||||
if (flags & 0x10)
|
||||
{
|
||||
++offset;
|
||||
}
|
||||
if (offset >= message.data.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint8_t lengthSize = message.data[offset++];
|
||||
std::uint64_t nameLength = 0;
|
||||
for (std::uint8_t byte = 0; byte < lengthSize && offset < message.data.size(); ++byte)
|
||||
{
|
||||
nameLength |= static_cast<std::uint64_t>(message.data[offset + byte]) << (8 * byte);
|
||||
}
|
||||
offset += lengthSize;
|
||||
if (offset + nameLength + 8 > message.data.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string name(reinterpret_cast<const char*>(message.data.data() + offset),
|
||||
static_cast<std::size_t>(nameLength));
|
||||
offset += static_cast<std::size_t>(nameLength);
|
||||
const std::uint64_t target = Resolve(ReadLE(message.data.data() + offset, 8));
|
||||
out.emplace_back(std::move(name), target);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::uint64_t MatHdf5::Member(std::uint64_t address, const std::string& name) const
|
||||
{
|
||||
for (const auto& entry : Members(address))
|
||||
{
|
||||
if (entry.first == name)
|
||||
{
|
||||
return entry.second;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool MatHdf5::IsDataset(std::uint64_t address) const
|
||||
{
|
||||
std::vector<Message> messages;
|
||||
ReadMessages(address, messages);
|
||||
for (const Message& message : messages)
|
||||
{
|
||||
if (message.type == kMessageDataspace)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MatHdf5::ParseType(const std::uint8_t* data, std::size_t size, std::size_t offset, Type& out,
|
||||
std::size_t& consumed) const
|
||||
{
|
||||
if (offset + 8 > size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out.version = data[offset] >> 4;
|
||||
out.cls = static_cast<TypeClass>(data[offset] & 0x0F);
|
||||
const std::uint8_t bits0 = data[offset + 1];
|
||||
out.size = ReadLE(data + offset + 4, 4);
|
||||
std::size_t cursor = offset + 8;
|
||||
if (out.cls == TypeClass::Integer)
|
||||
{
|
||||
out.bigEndian = (bits0 & 0x01) != 0;
|
||||
out.isSigned = (bits0 & 0x08) != 0;
|
||||
if (out.version >= 3)
|
||||
{
|
||||
cursor += 2; // bit offset
|
||||
cursor += 2; // precision
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor += 4;
|
||||
}
|
||||
}
|
||||
else if (out.cls == TypeClass::Float)
|
||||
{
|
||||
out.bigEndian = (bits0 & 0x01) != 0;
|
||||
cursor += 12;
|
||||
}
|
||||
else if (out.cls == TypeClass::String)
|
||||
{
|
||||
// 无需额外属性
|
||||
}
|
||||
else if (out.cls == TypeClass::Reference)
|
||||
{
|
||||
// 无需额外属性
|
||||
}
|
||||
else if (out.cls == TypeClass::VariableLength)
|
||||
{
|
||||
out.isVariableString = (bits0 & 0x0F) == 1;
|
||||
std::size_t childConsumed = 0;
|
||||
Type child;
|
||||
if (ParseType(data, size, cursor, child, childConsumed))
|
||||
{
|
||||
cursor += childConsumed;
|
||||
}
|
||||
}
|
||||
else if (out.cls == TypeClass::Compound)
|
||||
{
|
||||
const unsigned members = static_cast<unsigned>(bits0) | (static_cast<unsigned>(data[offset + 2]) << 8);
|
||||
for (unsigned index = 0; index < members; ++index)
|
||||
{
|
||||
if (cursor >= size)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const std::size_t nameStart = cursor;
|
||||
while (cursor < size && data[cursor] != 0)
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
std::string name(reinterpret_cast<const char*>(data + nameStart), cursor - nameStart);
|
||||
++cursor; // 结束符
|
||||
cursor = (cursor + 7) & ~static_cast<std::size_t>(7);
|
||||
if (out.version == 3)
|
||||
{
|
||||
// v3:先维度(1 字节 rank + 4 字节保留),再字节偏移(可变)
|
||||
}
|
||||
cursor += 4; // 字节偏移(v1/v2)
|
||||
std::size_t childConsumed = 0;
|
||||
Type child;
|
||||
if (ParseType(data, size, cursor, child, childConsumed))
|
||||
{
|
||||
cursor += childConsumed;
|
||||
out.members.emplace_back(std::move(name), std::move(child));
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (out.cls == TypeClass::Array)
|
||||
{
|
||||
if (cursor + 4 <= size)
|
||||
{
|
||||
const std::uint8_t rank = data[cursor];
|
||||
cursor += 4;
|
||||
cursor += static_cast<std::size_t>(rank) * 4; // 维度
|
||||
cursor += static_cast<std::size_t>(rank) * 4; // 排列
|
||||
std::size_t childConsumed = 0;
|
||||
Type child;
|
||||
if (ParseType(data, size, cursor, child, childConsumed))
|
||||
{
|
||||
cursor += childConsumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
consumed = cursor - offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatHdf5::DecodeChunked(const Dataset& dataset, std::size_t elementSize,
|
||||
std::vector<std::uint8_t>& out) const
|
||||
{
|
||||
if (dataset.chunkAddress == 0 || dataset.chunkDims.empty() || elementSize == 0 ||
|
||||
dataset.dims.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// ⚠️ MATLAB 写出的分块维度可能与数据空间维度**维数不一致**(实测:1 维数据集的
|
||||
// 分块维度是 `[N, 1]`)。把维度数组在**右侧补 1**(行优先的最快轴)对齐即可,
|
||||
// 二者的元素总数与遍历顺序都不变。
|
||||
std::vector<std::size_t> dims = dataset.dims;
|
||||
std::vector<std::size_t> chunkDims = dataset.chunkDims;
|
||||
while (dims.size() < chunkDims.size())
|
||||
{
|
||||
dims.push_back(1);
|
||||
}
|
||||
while (chunkDims.size() < dims.size())
|
||||
{
|
||||
chunkDims.push_back(1);
|
||||
}
|
||||
const std::size_t rank = dims.size();
|
||||
const std::uint64_t total = [&]() -> std::uint64_t
|
||||
{
|
||||
std::uint64_t value = 1;
|
||||
for (const std::size_t dim : dims)
|
||||
{
|
||||
value *= dim;
|
||||
}
|
||||
return value;
|
||||
}();
|
||||
out.assign(static_cast<std::size_t>(total) * elementSize, 0);
|
||||
std::vector<std::uint8_t> chunk;
|
||||
std::vector<std::uint8_t> inflated;
|
||||
// 遍历分块 B-tree:key = 分块大小 + 过滤器掩码 + 各维偏移。
|
||||
std::vector<std::uint64_t> stack{dataset.chunkAddress};
|
||||
int guard = 0;
|
||||
while (!stack.empty() && guard++ < 1000000)
|
||||
{
|
||||
const std::uint64_t node = stack.back();
|
||||
stack.pop_back();
|
||||
if (node == 0 || node + 24 > m_data.size() ||
|
||||
std::memcmp(&m_data[static_cast<std::size_t>(node)], "TREE", 4) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint8_t level = U8(node + 5);
|
||||
const std::uint16_t entries = U16(node + 6);
|
||||
const std::size_t keySize = 8 + 8 * rank;
|
||||
std::uint64_t offset = node + 8 + 8 + 8;
|
||||
for (std::uint16_t index = 0; index < static_cast<std::uint16_t>(entries + 1u); ++index)
|
||||
{
|
||||
const std::uint32_t chunkSize = (offset + 4 <= m_data.size()) ? U32(offset) : 0;
|
||||
std::vector<std::uint64_t> chunkOffset(rank, 0);
|
||||
for (std::size_t axis = 0; axis < rank; ++axis)
|
||||
{
|
||||
chunkOffset[axis] = U64(offset + 8 + axis * 8);
|
||||
}
|
||||
offset += keySize;
|
||||
if (index < entries)
|
||||
{
|
||||
const std::uint64_t child = Resolve(U64(offset));
|
||||
offset += 8;
|
||||
if (level > 0)
|
||||
{
|
||||
stack.push_back(child);
|
||||
}
|
||||
else if (child != 0 && chunkSize > 0)
|
||||
{
|
||||
chunk.assign(m_data.begin() + static_cast<std::ptrdiff_t>(child),
|
||||
m_data.begin() + static_cast<std::ptrdiff_t>(child + chunkSize));
|
||||
const std::uint8_t* source = chunk.data();
|
||||
std::size_t sourceSize = chunk.size();
|
||||
if (dataset.deflate)
|
||||
{
|
||||
// 分块未压缩时大小 = 分块元素数 × 元素大小:此时不需要解压。
|
||||
std::size_t expected = elementSize;
|
||||
for (const std::size_t dim : dataset.chunkDims)
|
||||
{
|
||||
expected *= dim;
|
||||
}
|
||||
if (chunk.size() != expected)
|
||||
{
|
||||
inflated.assign(expected, 0);
|
||||
uLongf destLength = static_cast<uLongf>(expected);
|
||||
if (uncompress(inflated.data(), &destLength, chunk.data(),
|
||||
static_cast<uLong>(chunk.size())) != Z_OK)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
source = inflated.data();
|
||||
sourceSize = expected;
|
||||
}
|
||||
}
|
||||
// 把该块拷进输出缓冲区(按行优先展开)。
|
||||
const std::size_t rowBytes = chunkDims[rank - 1] * elementSize;
|
||||
std::size_t rows = 1;
|
||||
for (std::size_t axis = 0; axis + 1 < rank; ++axis)
|
||||
{
|
||||
rows *= chunkDims[axis];
|
||||
}
|
||||
for (std::size_t row = 0; row < rows; ++row)
|
||||
{
|
||||
std::vector<std::size_t> coordinate(rank - 1, 0);
|
||||
std::size_t remainder = row;
|
||||
for (std::size_t axis = rank - 1; axis-- > 0;)
|
||||
{
|
||||
coordinate[axis] = remainder % chunkDims[axis];
|
||||
remainder /= chunkDims[axis];
|
||||
}
|
||||
std::uint64_t targetRow = 0;
|
||||
bool inside = true;
|
||||
std::uint64_t stride = 1;
|
||||
for (std::size_t axis = rank - 1; axis-- > 0;)
|
||||
{
|
||||
const std::uint64_t index = chunkOffset[axis] + coordinate[axis];
|
||||
if (index >= dims[axis])
|
||||
{
|
||||
inside = false;
|
||||
break;
|
||||
}
|
||||
targetRow += index * stride;
|
||||
stride *= dims[axis];
|
||||
}
|
||||
if (!inside)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t sourceOffset = row * rowBytes;
|
||||
const std::size_t targetOffset =
|
||||
static_cast<std::size_t>(targetRow) * dims[rank - 1] * elementSize;
|
||||
if (sourceOffset + rowBytes > sourceSize ||
|
||||
targetOffset + rowBytes > out.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::memcpy(out.data() + targetOffset, source + sourceOffset, rowBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatHdf5::ReadDataset(std::uint64_t address, Dataset& out) const
|
||||
{
|
||||
out = Dataset();
|
||||
std::vector<Message> messages;
|
||||
ReadMessages(address, messages);
|
||||
bool haveSpace = false;
|
||||
bool haveType = false;
|
||||
std::size_t compactSize = 0;
|
||||
std::uint64_t dataAddress = 0;
|
||||
std::uint64_t dataSize = 0;
|
||||
for (const Message& message : messages)
|
||||
{
|
||||
if (message.type == kMessageDataspace && message.data.size() >= 8)
|
||||
{
|
||||
const std::uint8_t version = message.data[0];
|
||||
const std::uint8_t rank = message.data[1];
|
||||
std::size_t offset = (version == 1) ? 8 : 4;
|
||||
out.dims.assign(rank, 0);
|
||||
for (std::uint8_t axis = 0; axis < rank && offset + 8 <= message.data.size(); ++axis)
|
||||
{
|
||||
out.dims[axis] = static_cast<std::size_t>(ReadLE(&message.data[offset], 8));
|
||||
offset += 8;
|
||||
}
|
||||
haveSpace = true;
|
||||
}
|
||||
else if (message.type == kMessageDatatype)
|
||||
{
|
||||
std::size_t consumed = 0;
|
||||
haveType = ParseType(message.data.data(), message.data.size(), 0, out.type, consumed);
|
||||
}
|
||||
else if (message.type == kMessageLayout && message.data.size() >= 8)
|
||||
{
|
||||
const std::uint8_t version = message.data[0];
|
||||
const std::uint8_t klass = message.data[1];
|
||||
out.layout = static_cast<LayoutClass>(klass);
|
||||
if (klass == 0)
|
||||
{
|
||||
// 紧凑:数据就放在消息里
|
||||
if (version == 3)
|
||||
{
|
||||
compactSize = static_cast<std::size_t>(message.data[2]) |
|
||||
(static_cast<std::size_t>(message.data[3]) << 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
compactSize = static_cast<std::size_t>(ReadLE(&message.data[2], 4));
|
||||
}
|
||||
}
|
||||
else if (klass == 1)
|
||||
{
|
||||
dataAddress = Resolve(ReadLE(&message.data[2], 8));
|
||||
dataSize = ReadLE(&message.data[10], 8);
|
||||
}
|
||||
else if (klass == 2)
|
||||
{
|
||||
if (version == 3)
|
||||
{
|
||||
const std::uint8_t rank = message.data[2];
|
||||
out.chunkAddress = Resolve(ReadLE(&message.data[3], 8));
|
||||
out.chunkDims.assign(rank, 0);
|
||||
for (std::uint8_t axis = 0; axis < rank; ++axis)
|
||||
{
|
||||
out.chunkDims[axis] = ReadLE(&message.data[11 + axis * 4], 4);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::uint8_t rank = message.data[2];
|
||||
out.chunkAddress = Resolve(ReadLE(&message.data[3], 8));
|
||||
out.chunkDims.assign(rank, 0);
|
||||
for (std::uint8_t axis = 0; axis < rank; ++axis)
|
||||
{
|
||||
out.chunkDims[axis] = ReadLE(&message.data[11 + axis * 4], 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message.type == kMessageFilter && message.data.size() >= 2)
|
||||
{
|
||||
const int count = message.data[1];
|
||||
std::size_t offset = 8;
|
||||
for (int index = 0; index < count && offset + 8 <= message.data.size(); ++index)
|
||||
{
|
||||
const std::uint16_t id = static_cast<std::uint16_t>(message.data[offset]) |
|
||||
(static_cast<std::uint16_t>(message.data[offset + 1]) << 8);
|
||||
const std::uint16_t nameLength = static_cast<std::uint16_t>(message.data[offset + 2]) |
|
||||
(static_cast<std::uint16_t>(message.data[offset + 3]) << 8);
|
||||
const std::uint16_t values =
|
||||
static_cast<std::uint16_t>(message.data[offset + 6]) |
|
||||
(static_cast<std::uint16_t>(message.data[offset + 7]) << 8);
|
||||
offset += 8;
|
||||
offset += (nameLength + 7) & ~static_cast<std::size_t>(7);
|
||||
offset += static_cast<std::size_t>(values) * 4;
|
||||
if (id == 1)
|
||||
{
|
||||
out.deflate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!haveSpace || !haveType || out.type.size == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t elementSize = out.type.size;
|
||||
if (out.layout == LayoutClass::Compact)
|
||||
{
|
||||
// 紧凑数据在消息里,重新扫描一次取出字节。
|
||||
for (const Message& message : messages)
|
||||
{
|
||||
if (message.type == kMessageLayout)
|
||||
{
|
||||
const std::size_t offset = (message.data[0] == 3) ? 4 : 8;
|
||||
if (offset + compactSize <= message.data.size())
|
||||
{
|
||||
out.data.assign(message.data.begin() + static_cast<std::ptrdiff_t>(offset),
|
||||
message.data.begin() +
|
||||
static_cast<std::ptrdiff_t>(offset + compactSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (out.layout == LayoutClass::Contiguous)
|
||||
{
|
||||
if (dataAddress != 0 && dataAddress + dataSize <= m_data.size())
|
||||
{
|
||||
out.data.assign(m_data.begin() + static_cast<std::ptrdiff_t>(dataAddress),
|
||||
m_data.begin() + static_cast<std::ptrdiff_t>(dataAddress + dataSize));
|
||||
}
|
||||
}
|
||||
else if (out.layout == LayoutClass::Chunked)
|
||||
{
|
||||
if (!DecodeChunked(out, elementSize, out.data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
out.ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MatHdf5::AttributeString(std::uint64_t address, const char* name) const
|
||||
{
|
||||
std::vector<Message> messages;
|
||||
ReadMessages(address, messages);
|
||||
for (const Message& message : messages)
|
||||
{
|
||||
if (message.type != kMessageAttribute || message.data.size() < 8)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint8_t version = message.data[0];
|
||||
const std::size_t headerSize = (version == 1) ? 8 : (version == 2 ? 8 : 9);
|
||||
const std::uint16_t nameSize = static_cast<std::uint16_t>(message.data[2]) |
|
||||
(static_cast<std::uint16_t>(message.data[3]) << 8);
|
||||
const std::uint16_t typeSize = static_cast<std::uint16_t>(message.data[4]) |
|
||||
(static_cast<std::uint16_t>(message.data[5]) << 8);
|
||||
const std::uint16_t spaceSize = static_cast<std::uint16_t>(message.data[6]) |
|
||||
(static_cast<std::uint16_t>(message.data[7]) << 8);
|
||||
std::size_t offset = headerSize;
|
||||
if (offset + nameSize > message.data.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string attributeName(reinterpret_cast<const char*>(message.data.data() + offset),
|
||||
nameSize);
|
||||
while (!attributeName.empty() && attributeName.back() == '\0')
|
||||
{
|
||||
attributeName.pop_back();
|
||||
}
|
||||
offset += (nameSize + 7) & ~static_cast<std::size_t>(7);
|
||||
const std::size_t typeOffset = offset;
|
||||
offset += (typeSize + 7) & ~static_cast<std::size_t>(7);
|
||||
const std::size_t spaceOffset = offset;
|
||||
offset += (spaceSize + 7) & ~static_cast<std::size_t>(7);
|
||||
if (attributeName != name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (offset > message.data.size())
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
Type type;
|
||||
std::size_t consumed = 0;
|
||||
if (!ParseType(message.data.data(), message.data.size(), typeOffset, type, consumed))
|
||||
{
|
||||
return std::string();
|
||||
}
|
||||
std::size_t count = 1;
|
||||
if (spaceOffset + 4 <= message.data.size())
|
||||
{
|
||||
const std::uint8_t spaceVersion = message.data[spaceOffset];
|
||||
const std::uint8_t rank = message.data[spaceOffset + 1];
|
||||
std::size_t dimOffset = spaceOffset + ((spaceVersion == 1) ? 8 : 4);
|
||||
count = 1;
|
||||
for (std::uint8_t axis = 0; axis < rank && dimOffset + 8 <= message.data.size(); ++axis)
|
||||
{
|
||||
count *= static_cast<std::size_t>(ReadLE(&message.data[dimOffset], 8));
|
||||
dimOffset += 8;
|
||||
}
|
||||
}
|
||||
std::string text;
|
||||
if (type.cls == TypeClass::String || type.cls == TypeClass::Integer)
|
||||
{
|
||||
text.assign(reinterpret_cast<const char*>(message.data.data() + offset),
|
||||
std::min(message.data.size() - offset, std::max<std::size_t>(count, 1)));
|
||||
while (!text.empty() && text.back() == '\0')
|
||||
{
|
||||
text.pop_back();
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
bool MatHdf5::ReadText(std::uint64_t address, std::wstring& out) const
|
||||
{
|
||||
out.clear();
|
||||
Dataset dataset;
|
||||
if (!ReadDataset(address, dataset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (dataset.type.cls == TypeClass::String)
|
||||
{
|
||||
out.assign(dataset.data.begin(), dataset.data.end());
|
||||
while (!out.empty() && out.back() == L'\0')
|
||||
{
|
||||
out.pop_back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (dataset.type.size == 2)
|
||||
{
|
||||
for (std::size_t index = 0; index + 1 < dataset.data.size(); index += 2)
|
||||
{
|
||||
const wchar_t character = static_cast<wchar_t>(
|
||||
static_cast<std::uint16_t>(dataset.data[index]) |
|
||||
(static_cast<std::uint16_t>(dataset.data[index + 1]) << 8));
|
||||
if (character != 0)
|
||||
{
|
||||
out.push_back(character);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (dataset.type.size == 1)
|
||||
{
|
||||
for (const std::uint8_t byte : dataset.data)
|
||||
{
|
||||
if (byte != 0)
|
||||
{
|
||||
out.push_back(static_cast<wchar_t>(byte));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MatHdf5::ReadNumbers(std::uint64_t address, std::vector<double>& out) const
|
||||
{
|
||||
out.clear();
|
||||
Dataset dataset;
|
||||
if (!ReadDataset(address, dataset) || !dataset.ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t elementSize = dataset.type.size;
|
||||
if (elementSize == 0 || dataset.data.size() % elementSize != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t count = dataset.data.size() / elementSize;
|
||||
out.resize(count);
|
||||
const bool integer = (dataset.type.cls == TypeClass::Integer);
|
||||
const bool floating = (dataset.type.cls == TypeClass::Float);
|
||||
if (!integer && !floating)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const std::uint8_t* source = dataset.data.data() + index * elementSize;
|
||||
if (floating && elementSize == 8)
|
||||
{
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, source, sizeof(value));
|
||||
out[index] = value;
|
||||
}
|
||||
else if (floating && elementSize == 4)
|
||||
{
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, source, sizeof(value));
|
||||
out[index] = static_cast<double>(value);
|
||||
}
|
||||
else if (integer && dataset.type.isSigned)
|
||||
{
|
||||
std::int64_t value = 0;
|
||||
for (std::size_t byte = 0; byte < elementSize; ++byte)
|
||||
{
|
||||
value |= static_cast<std::int64_t>(source[byte]) << (8 * byte);
|
||||
}
|
||||
// 符号扩展
|
||||
const int bits = static_cast<int>(elementSize) * 8;
|
||||
if (bits < 64 && (value & (1ll << (bits - 1))) != 0)
|
||||
{
|
||||
value |= ~((1ll << bits) - 1);
|
||||
}
|
||||
out[index] = static_cast<double>(value);
|
||||
}
|
||||
else if (integer)
|
||||
{
|
||||
std::uint64_t value = 0;
|
||||
for (std::size_t byte = 0; byte < elementSize; ++byte)
|
||||
{
|
||||
value |= static_cast<std::uint64_t>(source[byte]) << (8 * byte);
|
||||
}
|
||||
out[index] = static_cast<double>(value);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MatHdf5::ReadReferences(std::uint64_t address, std::vector<std::uint64_t>& out) const
|
||||
{
|
||||
out.clear();
|
||||
Dataset dataset; if (!ReadDataset(address, dataset) || !dataset.ok ||
|
||||
dataset.type.cls != TypeClass::Reference)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t count = dataset.data.size() / 8;
|
||||
out.reserve(count);
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const std::uint64_t raw = ReadLE(dataset.data.data() + index * 8, 8);
|
||||
out.push_back(Resolve(raw));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
@@ -0,0 +1,469 @@
|
||||
// MATLAB `.mat`(v7.3 / HDF5)加载器。
|
||||
//
|
||||
// 支持两种内容(详见 MatLoader.hpp 的说明):
|
||||
// * Simulink SDI / `SimulationData.Dataset` 导出:`#refs#` 里的元素图 + `#sigstream#` 的样本流;
|
||||
// * 普通 v7.3 变量:数值矩阵按"列 = 信号、行 = 采样"读取(时间轴 = 采样序号)。
|
||||
#include "MatLoader.hpp"
|
||||
|
||||
#include "MatH5.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#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 去掉文件名中的扩展名。 */
|
||||
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;
|
||||
}
|
||||
MatHdf5::Dataset dataset;
|
||||
if (!file.ReadDataset(data, dataset) || !dataset.ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
|
||||
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double(`#sampleSize#` 可印证)。
|
||||
if (dataset.data.size() % sizeof(double) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t count = dataset.data.size() / sizeof(double);
|
||||
out.resize(count);
|
||||
std::memcpy(out.data(), dataset.data.data(), count * sizeof(double));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @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 = static_cast<int>(numbers.front());
|
||||
}
|
||||
numbers.clear();
|
||||
if (file.ReadNumbers(file.Member(values, "TimeR2"), numbers) && !numbers.empty())
|
||||
{
|
||||
element.timeStream = static_cast<int>(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 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)
|
||||
{
|
||||
// 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));
|
||||
}
|
||||
|
||||
std::map<std::wstring, std::size_t> groupIndex;
|
||||
std::vector<std::vector<double>> streams; // 流编号 → 样本
|
||||
std::vector<bool> streamCached;
|
||||
const auto streamFor = [&](int index) -> const std::vector<double>*
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (streams.size() <= static_cast<std::size_t>(index))
|
||||
{
|
||||
streams.resize(static_cast<std::size_t>(index) + 1);
|
||||
streamCached.resize(static_cast<std::size_t>(index) + 1, false);
|
||||
}
|
||||
if (!streamCached[static_cast<std::size_t>(index)])
|
||||
{
|
||||
const std::uint64_t streamObject =
|
||||
file.Member(sigstream, std::to_string(index));
|
||||
if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast<std::size_t>(index)]))
|
||||
{
|
||||
streams[static_cast<std::size_t>(index)].clear();
|
||||
}
|
||||
streamCached[static_cast<std::size_t>(index)] = true;
|
||||
}
|
||||
return streams[static_cast<std::size_t>(index)].empty()
|
||||
? nullptr
|
||||
: &streams[static_cast<std::size_t>(index)];
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::vector<double>* time = streamFor(element.timeStream);
|
||||
const std::vector<double>* values = streamFor(element.dataStream);
|
||||
if (values == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::vector<double> timeCopy;
|
||||
if (time != nullptr && time->size() == values->size())
|
||||
{
|
||||
timeCopy = *time;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 时间流缺失/长度不符:退化为采样序号(秒),保证信号仍可显示。
|
||||
timeCopy.resize(values->size());
|
||||
for (std::size_t index = 0; index < values->size(); ++index)
|
||||
{
|
||||
timeCopy[index] = static_cast<double>(index);
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring groupName;
|
||||
for (const std::wstring& piece : pending.path)
|
||||
{
|
||||
if (!groupName.empty())
|
||||
{
|
||||
groupName += L"-";
|
||||
}
|
||||
groupName += piece;
|
||||
}
|
||||
if (groupName.empty())
|
||||
{
|
||||
groupName = stem;
|
||||
}
|
||||
std::wstring signalName = element.name;
|
||||
if (signalName.empty())
|
||||
{
|
||||
signalName = L"信号" + std::to_wstring(++unnamedSignals);
|
||||
}
|
||||
|
||||
std::size_t index = 0;
|
||||
const auto existing = groupIndex.find(groupName);
|
||||
if (existing == groupIndex.end())
|
||||
{
|
||||
LoadedSourceGroup group;
|
||||
group.name = groupName;
|
||||
group.source.sourceName = groupName;
|
||||
groups.push_back(std::move(group));
|
||||
index = groups.size() - 1;
|
||||
groupIndex.emplace(groupName, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = existing->second;
|
||||
}
|
||||
AppendSignal(groups[index].source, signalName, std::move(timeCopy),
|
||||
std::vector<double>(*values));
|
||||
}
|
||||
|
||||
if (groups.empty())
|
||||
{
|
||||
error = L"文件中没有可用的信号数据。";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief 普通 v7.3 变量:数值矩阵 / 结构体。 */
|
||||
void LoadPlainObject(const MatHdf5& file, std::uint64_t address, const std::wstring& name,
|
||||
LoadedSource& source)
|
||||
{
|
||||
if (file.IsDataset(address))
|
||||
{
|
||||
std::vector<double> numbers;
|
||||
if (!file.ReadNumbers(address, numbers) || numbers.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
MatHdf5::Dataset dataset;
|
||||
file.ReadDataset(address, dataset);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool LoadMatSourceGroups(const std::wstring& filePath, std::vector<LoadedSourceGroup>& groups,
|
||||
std::wstring& error, const LoadProgress& progress)
|
||||
{
|
||||
error.clear();
|
||||
groups.clear();
|
||||
MatHdf5 file;
|
||||
if (!file.Open(filePath, error))
|
||||
{
|
||||
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))
|
||||
{
|
||||
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;
|
||||
progress(info);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user