悬停高亮恢复旧版外延观感 + 支持 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
@@ -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