悬停高亮恢复旧版外延观感 + 支持 MATLAB .mat(v7.3 / HDF5,嵌套分组)
一、悬停高亮「几乎看不到、只是曲线换了颜色」(对比提交f845616的旧版观感) 根因:调色板第 1 色就是主题强调色(0xFF268BD2),而上一轮为提速把稠密信号的光晕改成 「按包络带原始几何 FillPath」——与曲线**逐像素同形**,于是曲线上的那层半透明强调色 完全看不出变化;旧版观感来自「lineWidth+4 的描边笔向两侧各外扩 2~3px」。 修法(PlotViewPaint.cpp::PaintSignalHaloFor): 1) 逐列成对填四边形,上下左右都外扩 grow = (lineWidth + 4) * scale / 2 (竖向跃变处旧版 5px 笔涂出的正是那条整高色带); 2) 不能把整条包络拼成一个多边形:满幅方波相邻列区间互相交叉,缠绕填充规则会在交叠处 相互抵消 ⇒ 填充变成一条条竖缝(实测只覆盖约一半带宽); 3) alpha 从 0x55 调回 0x66(与旧版描边笔一致)。 代价仍只在「换信号/换视界/改样式」时付一次,此后每帧只 AlphaBlend。 三维对照(新增 haloshot 探针 +f845616工作树构建):变化像素 旧 73726 / 上一轮 13529 / 本轮 74916,目视与旧版一致(跃变处的整高色带恢复)。 二、MATLAB .mat(v7.3 = HDF5)支持,含嵌套分组 新增只读 HDF5 子集(不引第三方库):Utilities/Inc/MatH5.hpp + Utilities/Src/MatH5.cpp - 超级块 v0/v2;对象头 v1(含续块,主块长度以 headerSize 为准);符号表(B-tree v1 + 本地堆)与 link 消息;数据空间 v1/v2;数据类型 v1;数据布局 v3(紧凑/连续/分块); 分块数据集支持 deflate(zlib);地址一律相对基地址解析。 新增 MATLAB 语义层:Utilities/Inc/MatLoader.hpp + Utilities/Src/MatLoader.cpp 1) Simulink SDI / SimulationData.Dataset(识别 #refs# + #sigstream#): · #refs# 元素有 ElementType(signal/dataset)、Name、Values; · timeseries 的 Values/DataR2、Values/TimeR2 是 #sigstream#/<n> 的流编号; #data# 是 uint8 字节流,按 8 字节一组解释为 double(与 #length#/#sampleSize# 一致); · 子数据集的 Values 是对象引用数组(子元素)⇒ 还原嵌套树; · 分组名 = 从根到信号的各级数据集名用 - 连接(如 A/A1/XYZ → 组 A-A1、信号 XYZ), 信号名取元素 Name(空则退回 timeseries 的 Name,再退回 信号N)。 2) 普通 v7.3 变量:顶层数值矩阵按「列 = 信号、行 = 采样」读取(时间轴 = 采样序号), 结构体按字段递归。 加载管线扩成多分组:新增 LoadSignalSourceGroups()(DataLoader)→ AsyncLoadState::groups → Application::LoadFile 逐个 AddGroup;其它格式仍只有一个分组。 文件对话框与 IsSupportedSignalFile 增加 .mat。 三、验证(全部离屏,不占用鼠标/键盘/屏幕) - 新增 haloshot(截图对照 f845616)、matcheck(15/15:4 组 8 信号、时间轴单调、采样数正确)、 matui(11/11:Application::LoadFile 端到端把 4 个分组加进数据层与信号列表); - 回归 verify 49/49、panelanim 158/158、gridanim 93/93、hoverdrag 20/20、lanescroll 23/23、 datacheck 69/69、edgecheck 46/46、errcheck 19/19、halocheck 16/16、panflash 19/19、 panelstutter 8/8、stress 6/6; - make release / make debug 零警告;Makefile 与探针构建脚本补 -lz。 文档:Documents/agent_mem.md 追加本轮小节(含 4 条新规则:叠加层必须自带外扩、 缠绕填充不可用于自交包络带、HDF5 主消息块长度看 headerSize、 MATLAB 分块维度可能比数据空间多一维、按右侧补 1 对齐)。
This commit is contained in:
1 parent
24f0bf849d
commit
5b24dacee0
12 files changed
+1831
-47
No files matched your search
@@ -361,11 +361,11 @@ namespace jdi
|
||||
* 直到后台完成后再把结果移动进 DataSet(O(1),不会引起卡顿)。
|
||||
*
|
||||
* @param [IN] path 文件路径。
|
||||
* @param [OUT] group 构建好的信号分组。
|
||||
* @param [OUT] groups 构建好的信号分组(多数文件只有 1 个;`.mat` 的嵌套数据集会有多个)。
|
||||
* @param [OUT] error 失败原因描述。
|
||||
* @return 加载成功返回 true;用户取消或失败返回 false。
|
||||
*/
|
||||
bool LoadFileAsync(const std::wstring& path, SignalGroup& group, std::wstring& error);
|
||||
bool LoadFileAsync(const std::wstring& path, std::vector<SignalGroup>& groups, std::wstring& error);
|
||||
|
||||
/**
|
||||
* @brief 绘制加载过渡动画遮罩(在 Render 的最后一步调用,覆盖整个客户区)。
|
||||
|
||||
@@ -212,7 +212,7 @@ namespace jdi
|
||||
std::atomic<unsigned long long> totalBytes{0}; /**< 文件总字节数(0 表示未知)。 */
|
||||
bool ok = false; /**< 加载是否成功。 */
|
||||
std::wstring error; /**< 失败原因。 */
|
||||
SignalGroup group; /**< 构建结果。 */
|
||||
std::vector<SignalGroup> groups; /**< 构建结果(多分组格式可有多个)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -235,31 +235,42 @@ namespace jdi
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
LoadedSource source;
|
||||
std::vector<LoadedSourceGroup> sources;
|
||||
std::wstring error;
|
||||
if (!LoadSignalSource(path, source, error, parseProgress))
|
||||
if (!LoadSignalSourceGroups(path, sources, error, parseProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
LoadProgress buildProgress = [state](const LoadProgressInfo& info)
|
||||
const std::size_t count = std::max<std::size_t>(sources.size(), 1);
|
||||
for (std::size_t index = 0; index < sources.size(); ++index)
|
||||
{
|
||||
state->progress.store(static_cast<float>(0.75 + info.ratio * 0.25),
|
||||
std::memory_order_relaxed);
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
SignalGroup group;
|
||||
if (!BuildSignalGroup(std::move(source), paletteBase, group, error, buildProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
LoadProgress buildProgress = [state, index, count](const LoadProgressInfo& info)
|
||||
{
|
||||
const double base = 0.75 + 0.25 * (static_cast<double>(index) / static_cast<double>(count));
|
||||
state->progress.store(
|
||||
static_cast<float>(base + 0.25 * info.ratio / static_cast<double>(count)),
|
||||
std::memory_order_relaxed);
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
SignalGroup group;
|
||||
if (!BuildSignalGroup(std::move(sources[index].source), paletteBase, group, error,
|
||||
buildProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
// 分组名:`.mat` 的嵌套路径由加载器给出(如 `A-A1`);其它格式为空 ⇒ 用文件名。
|
||||
if (!sources[index].name.empty())
|
||||
{
|
||||
group.name = sources[index].name;
|
||||
}
|
||||
state->groups.push_back(std::move(group));
|
||||
}
|
||||
|
||||
state->group = std::move(group);
|
||||
state->ok = true;
|
||||
state->progress.store(1.0f, std::memory_order_relaxed);
|
||||
state->done.store(true, std::memory_order_release);
|
||||
|
||||
@@ -15,9 +15,9 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
|
||||
SignalGroup group;
|
||||
std::vector<SignalGroup> loaded;
|
||||
std::wstring error;
|
||||
if (!LoadFileAsync(path, group, error))
|
||||
if (!LoadFileAsync(path, loaded, error))
|
||||
{
|
||||
if (!error.empty())
|
||||
{
|
||||
@@ -26,13 +26,28 @@ namespace jdi
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
if (loaded.empty())
|
||||
{
|
||||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
int groupIndex = -1;
|
||||
// 插入新分组会移动 SignalGroup/SignalData 的存储位置,必须先暂停后台派生曲线计算,
|
||||
// 否则后台线程可能正在读取即将被移动的采样数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||||
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
|
||||
int lastGroupIndex = -1;
|
||||
for (SignalGroup& group : loaded)
|
||||
{
|
||||
int groupIndex = -1;
|
||||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||||
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
lastGroupIndex = groupIndex;
|
||||
}
|
||||
if (lastGroupIndex < 0)
|
||||
{
|
||||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
@@ -43,14 +58,14 @@ namespace jdi
|
||||
SyncTabsToDataSet();
|
||||
UpdateWindowTitle();
|
||||
|
||||
const SignalGroup* loaded = m_dataSet.GroupAt(groupIndex);
|
||||
if (loaded != nullptr)
|
||||
const SignalGroup* loadedGroup = m_dataSet.GroupAt(lastGroupIndex);
|
||||
if (loadedGroup != nullptr)
|
||||
{
|
||||
// 行数取各时间轴的**最大值**(多时间轴文件的各轴长度可能不同)。
|
||||
std::size_t rowCount = 0;
|
||||
bool anyNonMonotonic = false;
|
||||
std::wstring timeNames;
|
||||
for (const TimeAxis& axis : loaded->axes)
|
||||
for (const TimeAxis& axis : loadedGroup->axes)
|
||||
{
|
||||
rowCount = std::max(rowCount, axis.Count());
|
||||
anyNonMonotonic = anyNonMonotonic || !axis.monotonic;
|
||||
@@ -60,11 +75,11 @@ namespace jdi
|
||||
}
|
||||
timeNames += axis.name;
|
||||
}
|
||||
std::wstring message = L"已加载 组" + std::to_wstring(groupIndex + 1) + L"|" + loaded->name +
|
||||
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) +
|
||||
L" 行采样," + std::to_wstring(loaded->signals.size()) +
|
||||
L" 条信号," + std::to_wstring(loaded->axes.size()) + L" 条时间轴:" +
|
||||
timeNames + L")";
|
||||
std::wstring message =
|
||||
L"已加载 组" + std::to_wstring(lastGroupIndex + 1) + L"|" + loadedGroup->name +
|
||||
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) + L" 行采样," +
|
||||
std::to_wstring(loadedGroup->signals.size()) + L" 条信号," +
|
||||
std::to_wstring(loadedGroup->axes.size()) + L" 条时间轴:" + timeNames + L")";
|
||||
if (anyNonMonotonic)
|
||||
{
|
||||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||||
@@ -74,7 +89,8 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::LoadFileAsync(const std::wstring& path, SignalGroup& group, std::wstring& error)
|
||||
bool Application::LoadFileAsync(const std::wstring& path, std::vector<SignalGroup>& groups,
|
||||
std::wstring& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
@@ -147,7 +163,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
group = std::move(state.group);
|
||||
groups = std::move(state.groups);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1050,22 +1050,29 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
// ------------------------------------------------------------------
|
||||
// 稠密信号(走"像素列包络带"):光晕 = **填充与曲线本身同一几何**的包络带。
|
||||
// 稠密信号(走"像素列包络带"):光晕 = **沿包络带向外扩一圈的填充**。
|
||||
//
|
||||
// ⚠️ 绝不能用「加粗折线描边」画稠密信号的光晕:包络带的顶点序列是
|
||||
// 「每列上/下包络交替」的满幅竖锯齿,GDI+ 描边的代价 = 线段数 × 覆盖行数,
|
||||
// 实测 2503 个顶点要 **210ms**。而 `FillPath` 同一条几何只要 ~14ms,
|
||||
// 且观感与原实现(沿折线描粗)一致:整条曲线一起"发光"。
|
||||
// ⚠️ 光晕**必须覆盖整条曲线**:曾经只画鼠标附近 ±72px 的一段,在满幅方波上
|
||||
// 看起来就是一个**长方形色块**(像选区框),用户 2026-10-18 第二轮明确报告。
|
||||
// 代价问题改由 `PaintSignalHaloCached`(渲染一次 + 每帧贴图)解决。
|
||||
// 实测 2503 个顶点要 **210ms**。而 `FillPath` 同一条几何只要 ~14ms。
|
||||
// ⚠️⚠️ **必须向外扩 `grow`(= 描边笔宽的一半)**:折线描边的笔宽是
|
||||
// `lineWidth + 4`,即沿曲线向两侧各扩 2~3px —— 那正是用户看到的"外延高亮描边"。
|
||||
// 曾经直接按包络带原始几何填充,结果光晕与曲线**逐像素同形**:当信号颜色恰好等于
|
||||
// 主题强调色时(调色板第 1 色就是强调色),曲线上的那层半透明强调色完全看不出变化,
|
||||
// 只剩下带宽内部的一点点底色变化 ⇒ 用户报告"高亮几乎看不到、只有曲线换了颜色"
|
||||
//(2026-10-18 第三轮)。扩一圈后与旧版描边的观感一致。
|
||||
// ⚠️ 光晕**必须覆盖整条曲线**:只画鼠标附近一段时,在满幅方波上就是一个长方形色块。
|
||||
// 代价问题由 `PaintSignalHaloCached`(渲染一次 + 每帧贴图)解决。
|
||||
// ------------------------------------------------------------------
|
||||
if (resolved.style->mode != DrawMode::SCATTER && cached.bandMode && !cached.bandBreaks.empty())
|
||||
{
|
||||
// 与旧版 `PaintSignalHalo` 的笔宽同源:`(lineWidth + 4) * scale`,向外取一半。
|
||||
const float grow = (std::max(1.0f, resolved.style->lineWidth) + 4.0f) * m_scale * 0.5f;
|
||||
const int savedSmoothing = g.GetSmoothingMode();
|
||||
g.SetSmoothingMode(gd::SmoothingModeNone); // 半透明填充:抗锯齿没有观感收益
|
||||
gd::GraphicsPath bandPath;
|
||||
bandPath.SetFillMode(gd::FillModeWinding);
|
||||
gd::PointF quad[4];
|
||||
for (std::size_t k = 0; k < cached.bandBreaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = cached.bandBreaks[k];
|
||||
@@ -1076,16 +1083,37 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 段内布局 = 「上包络正向(`half` 个)+ 下包络反向(`half` 个)」。
|
||||
// 段内布局 = 「上包络正向(`half` 个)+ 下包络反向(`half` 个)」,因此
|
||||
// 列 `i` 的上/下包络分别是 `start+i` 与 `start+half+(half-1-i)`。
|
||||
//
|
||||
// ⚠️⚠️ **逐列成对填四边形,而不是把整条包络拼成一个多边形**:满幅方波的
|
||||
// 包络相邻列区间**互相交叉**,拼成单个多边形后缠绕填充规则会在交叠处
|
||||
// 互相抵消 ⇒ 填充结果变成一条条竖缝(实测光晕只覆盖约一半带宽,用户看到
|
||||
// "只有曲线换了个颜色")。逐列四边形永远是简单多边形,不会被挖空。
|
||||
// ⚠️ 横向/纵向都外扩 `grow`:竖向跃变处旧版 5px 笔会涂出一条整高色带,
|
||||
// 只填 1px 宽的列区间根本看不出来。相邻四边形同向、且在同一次 `FillPath`
|
||||
// 内,重叠区域只混合一次(不会叠出更深的竖条)。
|
||||
const std::size_t half = total / 2;
|
||||
bandPath.StartFigure();
|
||||
bandPath.AddLines(cached.band.data() + start, static_cast<INT>(half));
|
||||
bandPath.AddLines(cached.band.data() + start + half, static_cast<INT>(half));
|
||||
bandPath.CloseFigure();
|
||||
for (std::size_t column = 0; column + 1 < half; ++column)
|
||||
{
|
||||
const gd::PointF& topA = cached.band[start + column];
|
||||
const gd::PointF& bottomA = cached.band[start + half + (half - 1 - column)];
|
||||
const gd::PointF& topB = cached.band[start + column + 1];
|
||||
const gd::PointF& bottomB =
|
||||
cached.band[start + half + (half - 1 - (column + 1))];
|
||||
const float highY = std::min(std::min(topA.Y, bottomA.Y), std::min(topB.Y, bottomB.Y));
|
||||
const float lowY = std::max(std::max(topA.Y, bottomA.Y), std::max(topB.Y, bottomB.Y));
|
||||
quad[0] = gd::PointF(topA.X - grow, highY - grow);
|
||||
quad[1] = gd::PointF(topB.X + grow, highY - grow);
|
||||
quad[2] = gd::PointF(topB.X + grow, lowY + grow);
|
||||
quad[3] = gd::PointF(topA.X - grow, lowY + grow);
|
||||
bandPath.AddPolygon(quad, 4);
|
||||
}
|
||||
}
|
||||
if (bandPath.GetPointCount() > 0)
|
||||
{
|
||||
gd::SolidBrush brush(ToGdiColor(WithAlpha(m_theme->accent, 0x55)));
|
||||
// alpha 与旧版描边笔一致(0x66),保证任何信号颜色下观感相同。
|
||||
gd::SolidBrush brush(ToGdiColor(WithAlpha(m_theme->accent, 0x66)));
|
||||
g.FillPath(&brush, &bandPath);
|
||||
}
|
||||
g.SetSmoothingMode(static_cast<gd::SmoothingMode>(savedSmoothing));
|
||||
|
||||
Reference in new issue
Block a user