// 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 加载进度上报器(`.mat` 解析拆成“读文件”与“解码每个样本流”两段)。 */ struct ProgressReporter { const LoadProgress* sink = nullptr; /**< 目标回调(可空)。 */ double low = 0.0; /**< 本阶段占的进度区间下界。 */ double high = 1.0; /**< 本阶段占的进度区间上界。 */ std::uint64_t read = 0; /**< 已读字节。 */ std::uint64_t total = 0; /**< 总字节。 */ /** @brief 上报一个 0-1 的阶段内进度(向宿主传递字节数与百分比)。 */ bool Report(double fraction) const { if (sink == nullptr || !(*sink)) { return true; } if (fraction < 0.0) { fraction = 0.0; } if (fraction > 1.0) { fraction = 1.0; } LoadProgressInfo info; info.ratio = low + (high - low) * fraction; // 总字节已知时一并上报(遮罩层据此显示“已加载 / 总大小”)。 info.hasBytes = (total > 0); info.bytesRead = (total > 0) ? read : 0; info.totalBytes = total; return (*sink)(info); } }; /** @brief SDI(`SimulationData.Dataset`)结构的分组读取。 */ bool LoadSdi(const MatHdf5& file, std::uint64_t refs, std::uint64_t sigstream, const std::wstring& stem, std::vector& groups, std::wstring& error, const ProgressReporter& reporter) { // 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; // 进度:`#sigstream#` 的总流数,用来把"解码样本流"这段映射成阶段内 0-1。 std::size_t streamTotal = 0; for (const auto& member : file.Members(sigstream)) { if (!member.first.empty() && member.first[0] != '#') { ++streamTotal; } } std::size_t streamDone = 0; 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; // ⚠️ 必须在这里回报进度:读一个 42MB 的 `.mat` 时,"解码样本流"才是真正的 // 耗时大头(每个流都要解压 + 拷贝);不报进度的话界面会停在原地等 //(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新)。 ++streamDone; reporter.Report(streamTotal > 0 ? static_cast(streamDone) / static_cast(streamTotal) : 1.0); } 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; // ------------------------------------------------------------------ // 进度拆成三段(用户 2026-10-19 报告:加载 `.mat` 时已加载大小与百分比都不更新, // 界面只是原地等待): // 0.00 – 0.55 读文件字节(遮罩层显示"已加载 / 总大小") // 0.55 – 0.98 解码样本流(每个流解码后推进,含解压与拷贝) // 0.98 – 1.00 收尾 // 读文件阶段把 `hasBytes` 置真;之后字节数已满,只推进百分比。 // ------------------------------------------------------------------ ProgressReporter readReporter; readReporter.sink = &progress; readReporter.low = 0.0; readReporter.high = 0.55; const MatHdf5::OpenProgress openProgress = [&readReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool { readReporter.read = bytesRead; readReporter.total = totalBytes; return readReporter.Report(totalBytes > 0 ? static_cast(bytesRead) / static_cast(totalBytes) : 0.0); }; if (!file.Open(filePath, error, openProgress)) { return false; // 用户取消时 `error` 为空 } ProgressReporter parseReporter; parseReporter.sink = &progress; parseReporter.low = 0.55; parseReporter.high = 0.98; parseReporter.read = readReporter.total; parseReporter.total = 0; // 字节已读完:本阶段只报百分比 if (!parseReporter.Report(0.0)) { error.clear(); return false; } const std::wstring stem = StemOf(filePath); const std::uint64_t refs = file.Member(file.Root(), "#refs#"); const std::uint64_t sigstream = file.Member(file.Root(), "#sigstream#"); if (refs != 0 && sigstream != 0) { if (!LoadSdi(file, refs, sigstream, stem, groups, error, parseReporter)) { return false; } } else { LoadedSourceGroup group; group.source.sourceName = stem; for (const auto& member : file.Members(file.Root())) { if (member.first.empty() || member.first[0] == '#') { continue; } std::wstring name(member.first.begin(), member.first.end()); LoadPlainObject(file, member.second, name, group.source); } if (!group.source.HasData()) { error = L".mat 文件中没有可用的数值数据。"; return false; } groups.push_back(std::move(group)); } if (progress) { LoadProgressInfo info; info.ratio = 1.0; info.hasBytes = (readReporter.total > 0); info.bytesRead = readReporter.total; info.totalBytes = readReporter.total; progress(info); } return true; } } // namespace jdi