From 5b24dacee0757c9efa154d75ebc01fe2bf8050bc Mon Sep 17 00:00:00 2001 From: "DeepSeek-V4.1-Flash" <> Date: Mon, 19 Oct 2026 00:52:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=82=AC=E5=81=9C=E9=AB=98=E4=BA=AE=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E6=97=A7=E7=89=88=E5=A4=96=E5=BB=B6=E8=A7=82=E6=84=9F?= =?UTF-8?q?=20+=20=E6=94=AF=E6=8C=81=20MATLAB=20.mat=EF=BC=88v7.3=20/=20HD?= =?UTF-8?q?F5=EF=BC=8C=E5=B5=8C=E5=A5=97=E5=88=86=E7=BB=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、悬停高亮「几乎看不到、只是曲线换了颜色」(对比提交 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#/ 的流编号; #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 对齐)。 --- Components/Application/Inc/Application.hpp | 4 +- .../Application/Src/ApplicationInternal.hpp | 43 +- .../Application/Src/ApplicationLoading.cpp | 46 +- Components/PlotView/Src/PlotViewPaint.cpp | 52 +- Documents/agent_mem.md | 78 ++ Makefile | 1 + Utilities/Inc/DataLoader.hpp | 27 + Utilities/Inc/MatH5.hpp | 145 +++ Utilities/Inc/MatLoader.hpp | 35 + Utilities/Src/DataLoader.cpp | 25 +- Utilities/Src/MatH5.cpp | 953 ++++++++++++++++++ Utilities/Src/MatLoader.cpp | 469 +++++++++ 12 files changed, 1831 insertions(+), 47 deletions(-) create mode 100644 Utilities/Inc/MatH5.hpp create mode 100644 Utilities/Inc/MatLoader.hpp create mode 100644 Utilities/Src/MatH5.cpp create mode 100644 Utilities/Src/MatLoader.cpp diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index e8626ca..c3e7a29 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -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& groups, std::wstring& error); /** * @brief 绘制加载过渡动画遮罩(在 Render 的最后一步调用,覆盖整个客户区)。 diff --git a/Components/Application/Src/ApplicationInternal.hpp b/Components/Application/Src/ApplicationInternal.hpp index e088992..c013472 100644 --- a/Components/Application/Src/ApplicationInternal.hpp +++ b/Components/Application/Src/ApplicationInternal.hpp @@ -212,7 +212,7 @@ namespace jdi std::atomic totalBytes{0}; /**< 文件总字节数(0 表示未知)。 */ bool ok = false; /**< 加载是否成功。 */ std::wstring error; /**< 失败原因。 */ - SignalGroup group; /**< 构建结果。 */ + std::vector groups; /**< 构建结果(多分组格式可有多个)。 */ }; /** @@ -235,31 +235,42 @@ namespace jdi return !state->cancelled.load(std::memory_order_relaxed); }; - LoadedSource source; + std::vector 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(sources.size(), 1); + for (std::size_t index = 0; index < sources.size(); ++index) { - state->progress.store(static_cast(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(index) / static_cast(count)); + state->progress.store( + static_cast(base + 0.25 * info.ratio / static_cast(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); diff --git a/Components/Application/Src/ApplicationLoading.cpp b/Components/Application/Src/ApplicationLoading.cpp index bc39071..4025c6e 100644 --- a/Components/Application/Src/ApplicationLoading.cpp +++ b/Components/Application/Src/ApplicationLoading.cpp @@ -15,9 +15,9 @@ namespace jdi } // 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。 - SignalGroup group; + std::vector 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(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(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& 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; } diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index 39efd54..9fe9679 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -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(half)); - bandPath.AddLines(cached.band.data() + start + half, static_cast(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(savedSmoothing)); diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 7e8e920..9b6ae8f 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -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#/` 的流编号** + (数据流与时间流成对出现);`#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"对齐即可解码。 diff --git a/Makefile b/Makefile index 859e1d1..b200728 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,7 @@ LIB_LINK = \ -ld3d11 \ -ldwmapi \ -ld3dcompiler \ +-lz \ # C定义 C_DEFS = \ diff --git a/Utilities/Inc/DataLoader.hpp b/Utilities/Inc/DataLoader.hpp index 2f3427f..4c5eccd 100644 --- a/Utilities/Inc/DataLoader.hpp +++ b/Utilities/Inc/DataLoader.hpp @@ -2,12 +2,22 @@ #define __DATA_LOADER_HPP__ #include +#include #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& groups, + std::wstring& error, + const LoadProgress& progress = LoadProgress()); + } // namespace jdi #endif // __DATA_LOADER_HPP__ diff --git a/Utilities/Inc/MatH5.hpp b/Utilities/Inc/MatH5.hpp new file mode 100644 index 0000000..6b79109 --- /dev/null +++ b/Utilities/Inc/MatH5.hpp @@ -0,0 +1,145 @@ +#ifndef __MAT_H5_HPP__ +#define __MAT_H5_HPP__ + +#include +#include +#include +#include + +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> members; /**< 复合类型成员。 */ + }; + + /** @brief 数据布局。 */ + enum class LayoutClass + { + Compact = 0, + Contiguous = 1, + Chunked = 2 + }; + + /** @brief 一个数据集的定义与数据。 */ + struct Dataset + { + std::vector dims; /**< 各维长度。 */ + Type type; /**< 元素类型。 */ + LayoutClass layout = LayoutClass::Compact; + std::vector data; /**< 解码后的原始字节(行优先,按 `dims`)。 */ + std::uint64_t chunkAddress = 0; /**< 分块数据集的 B-tree 地址。 */ + std::vector 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> 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& out) const; + + /** @brief 读取引用数据集(HDF5 对象引用 = 对象头地址,相对基地址)。 */ + bool ReadReferences(std::uint64_t address, std::vector& out) const; + + private: + struct Message + { + std::uint16_t type = 0; + std::vector 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& 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& out) const; + void WalkBtree(std::uint64_t node, std::uint64_t heapData, + std::vector>& out) const; + void WalkSymbolNode(std::uint64_t node, std::uint64_t heapData, + std::vector>& out) const; + + std::vector 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> m_heaps; /**< 堆地址 → 数据地址。 */ + }; + +} // namespace jdi + +#endif // __MAT_H5_HPP__ diff --git a/Utilities/Inc/MatLoader.hpp b/Utilities/Inc/MatLoader.hpp new file mode 100644 index 0000000..26f6ac7 --- /dev/null +++ b/Utilities/Inc/MatLoader.hpp @@ -0,0 +1,35 @@ +#ifndef __MAT_LOADER_HPP__ +#define __MAT_LOADER_HPP__ + +#include +#include + +#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& groups, + std::wstring& error, const LoadProgress& progress); + +} // namespace jdi + +#endif // __MAT_LOADER_HPP__ diff --git a/Utilities/Src/DataLoader.cpp b/Utilities/Src/DataLoader.cpp index 65d47a8..1b5e60c 100644 --- a/Utilities/Src/DataLoader.cpp +++ b/Utilities/Src/DataLoader.cpp @@ -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& 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) { diff --git a/Utilities/Src/MatH5.cpp b/Utilities/Src/MatH5.cpp new file mode 100644 index 0000000..87d5eb7 --- /dev/null +++ b/Utilities/Src/MatH5.cpp @@ -0,0 +1,953 @@ +// HDF5 只读子集:MATLAB 7.3 `.mat` 解析用(见 MatH5.hpp 的说明)。 +#include "MatH5.hpp" + +#include + +#include +#include +#include +#include + +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(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(offset)] : 0; + } + + std::uint16_t MatHdf5::U16(std::uint64_t offset) const + { + return static_cast(U8(offset)) | (static_cast(U8(offset + 1)) << 8); + } + + std::uint32_t MatHdf5::U32(std::uint64_t offset) const + { + return static_cast(U16(offset)) | (static_cast(U16(offset + 2)) << 16); + } + + std::uint64_t MatHdf5::U64(std::uint64_t offset) const + { + return static_cast(U32(offset)) | (static_cast(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(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& 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> 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(offset), + m_data.begin() + static_cast(offset + messageSize)); + offset += (messageSize + 7u) & ~static_cast(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>& out) const + { + int guard = 0; + while (node != 0 && node + 24 < m_data.size() && guard++ < 100000) + { + if (std::memcmp(&m_data[static_cast(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 children; + for (std::uint16_t index = 0; index < static_cast(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>& out) const + { + if (node == 0 || node + 8 > m_data.size() || + std::memcmp(&m_data[static_cast(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(m_data.data() + heapData + nameOffset); + out.emplace_back(std::string(name), objectAddress); + } + } + + std::vector> MatHdf5::Members(std::uint64_t address) const + { + std::vector> out; + std::vector 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(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(message.data[offset + byte]) << (8 * byte); + } + offset += lengthSize; + if (offset + nameLength + 8 > message.data.size()) + { + continue; + } + std::string name(reinterpret_cast(message.data.data() + offset), + static_cast(nameLength)); + offset += static_cast(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 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(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(bits0) | (static_cast(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(data + nameStart), cursor - nameStart); + ++cursor; // 结束符 + cursor = (cursor + 7) & ~static_cast(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(rank) * 4; // 维度 + cursor += static_cast(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& out) const + { + if (dataset.chunkAddress == 0 || dataset.chunkDims.empty() || elementSize == 0 || + dataset.dims.empty()) + { + return false; + } + // ⚠️ MATLAB 写出的分块维度可能与数据空间维度**维数不一致**(实测:1 维数据集的 + // 分块维度是 `[N, 1]`)。把维度数组在**右侧补 1**(行优先的最快轴)对齐即可, + // 二者的元素总数与遍历顺序都不变。 + std::vector dims = dataset.dims; + std::vector 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(total) * elementSize, 0); + std::vector chunk; + std::vector inflated; + // 遍历分块 B-tree:key = 分块大小 + 过滤器掩码 + 各维偏移。 + std::vector 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(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(entries + 1u); ++index) + { + const std::uint32_t chunkSize = (offset + 4 <= m_data.size()) ? U32(offset) : 0; + std::vector 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(child), + m_data.begin() + static_cast(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(expected); + if (uncompress(inflated.data(), &destLength, chunk.data(), + static_cast(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 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(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 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(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(klass); + if (klass == 0) + { + // 紧凑:数据就放在消息里 + if (version == 3) + { + compactSize = static_cast(message.data[2]) | + (static_cast(message.data[3]) << 8); + } + else + { + compactSize = static_cast(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(message.data[offset]) | + (static_cast(message.data[offset + 1]) << 8); + const std::uint16_t nameLength = static_cast(message.data[offset + 2]) | + (static_cast(message.data[offset + 3]) << 8); + const std::uint16_t values = + static_cast(message.data[offset + 6]) | + (static_cast(message.data[offset + 7]) << 8); + offset += 8; + offset += (nameLength + 7) & ~static_cast(7); + offset += static_cast(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(offset), + message.data.begin() + + static_cast(offset + compactSize)); + } + } + } + } + else if (out.layout == LayoutClass::Contiguous) + { + if (dataAddress != 0 && dataAddress + dataSize <= m_data.size()) + { + out.data.assign(m_data.begin() + static_cast(dataAddress), + m_data.begin() + static_cast(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 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(message.data[2]) | + (static_cast(message.data[3]) << 8); + const std::uint16_t typeSize = static_cast(message.data[4]) | + (static_cast(message.data[5]) << 8); + const std::uint16_t spaceSize = static_cast(message.data[6]) | + (static_cast(message.data[7]) << 8); + std::size_t offset = headerSize; + if (offset + nameSize > message.data.size()) + { + continue; + } + std::string attributeName(reinterpret_cast(message.data.data() + offset), + nameSize); + while (!attributeName.empty() && attributeName.back() == '\0') + { + attributeName.pop_back(); + } + offset += (nameSize + 7) & ~static_cast(7); + const std::size_t typeOffset = offset; + offset += (typeSize + 7) & ~static_cast(7); + const std::size_t spaceOffset = offset; + offset += (spaceSize + 7) & ~static_cast(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(ReadLE(&message.data[dimOffset], 8)); + dimOffset += 8; + } + } + std::string text; + if (type.cls == TypeClass::String || type.cls == TypeClass::Integer) + { + text.assign(reinterpret_cast(message.data.data() + offset), + std::min(message.data.size() - offset, std::max(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( + static_cast(dataset.data[index]) | + (static_cast(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(byte)); + } + } + return true; + } + return false; + } + + bool MatHdf5::ReadNumbers(std::uint64_t address, std::vector& 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(value); + } + else if (integer && dataset.type.isSigned) + { + std::int64_t value = 0; + for (std::size_t byte = 0; byte < elementSize; ++byte) + { + value |= static_cast(source[byte]) << (8 * byte); + } + // 符号扩展 + const int bits = static_cast(elementSize) * 8; + if (bits < 64 && (value & (1ll << (bits - 1))) != 0) + { + value |= ~((1ll << bits) - 1); + } + out[index] = static_cast(value); + } + else if (integer) + { + std::uint64_t value = 0; + for (std::size_t byte = 0; byte < elementSize; ++byte) + { + value |= static_cast(source[byte]) << (8 * byte); + } + out[index] = static_cast(value); + } + } + return true; + } + + bool MatHdf5::ReadReferences(std::uint64_t address, std::vector& 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 diff --git a/Utilities/Src/MatLoader.cpp b/Utilities/Src/MatLoader.cpp new file mode 100644 index 0000000..650d515 --- /dev/null +++ b/Utilities/Src/MatLoader.cpp @@ -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 +#include +#include +#include +#include +#include + +namespace jdi +{ + + namespace + { + /** @brief 一个 SDI 信号(或子数据集)的解析结果。 */ + struct SdiElement + { + bool isSignal = false; + bool isDataset = false; + std::wstring name; + std::vector children; /**< 子数据集元素的引用目标。 */ + int dataStream = -1; /**< 数据流下标。 */ + int timeStream = -1; /**< 时间流下标。 */ + }; + + std::wstring TrimNumber(double value) + { + wchar_t buffer[64] = {}; + if (value == static_cast(static_cast(value))) + { + std::swprintf(buffer, 64, L"%lld", static_cast(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#//#data#`)。 */ + bool ReadStream(const MatHdf5& file, std::uint64_t streamObject, std::vector& 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 numbers; + if (file.ReadNumbers(file.Member(values, "DataR2"), numbers) && !numbers.empty()) + { + element.dataStream = static_cast(numbers.front()); + } + numbers.clear(); + if (file.ReadNumbers(file.Member(values, "TimeR2"), numbers) && !numbers.empty()) + { + element.timeStream = static_cast(numbers.front()); + } + } + + /** @brief 追加一条信号(时间 + 数值)到某个分组的时间轴块集合。 */ + void AppendSignal(LoadedSource& source, const std::wstring& name, std::vector&& time, + std::vector&& 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& groups, + std::wstring& error) + { + // 1) 枚举 `#refs#` 里的全部元素。 + std::map 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 childAddresses; + for (const auto& entry : elements) + { + for (const std::uint64_t child : entry.second.children) + { + childAddresses.insert(child); + } + } + std::vector 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 path; + int depth = 0; + }; + std::vector 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 groupIndex; + std::vector> streams; // 流编号 → 样本 + std::vector streamCached; + const auto streamFor = [&](int index) -> const std::vector* + { + if (index < 0) + { + return nullptr; + } + if (streams.size() <= static_cast(index)) + { + streams.resize(static_cast(index) + 1); + streamCached.resize(static_cast(index) + 1, false); + } + if (!streamCached[static_cast(index)]) + { + const std::uint64_t streamObject = + file.Member(sigstream, std::to_string(index)); + if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast(index)])) + { + streams[static_cast(index)].clear(); + } + streamCached[static_cast(index)] = true; + } + return streams[static_cast(index)].empty() + ? nullptr + : &streams[static_cast(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* time = streamFor(element.timeStream); + const std::vector* values = streamFor(element.dataStream); + if (values == nullptr) + { + continue; + } + std::vector 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(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(*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 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 values(rows); + for (std::size_t row = 0; row < rows; ++row) + { + values[row] = numbers[column * rows + row]; + } + std::vector time(rows); + for (std::size_t row = 0; row < rows; ++row) + { + time[row] = static_cast(row); + } + const std::wstring signalName = + (columns == 1) ? name : (name + L"(" + TrimNumber(static_cast(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& 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