Files
Just-DataInspector/Utilities/Src/MatLoader.cpp
T
DeepSeek-V4.1-Flash 5b24dacee0 悬停高亮恢复旧版外延观感 + 支持 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 对齐)。
2026-10-19 00:52:41 +08:00

470 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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