数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + CSV 多时间轴 + MF4 支持
一、数据模型解耦(详见 Documents/refactor_report_2026-10-15.md 与 agent_mem §9.27)
1. DataFile → SignalGroup:删除 filePath / fileName / time / timeColumnName / timeMonotonic /
minTime / maxTime —— 「分组」是应用内的逻辑容器,加载完成后与磁盘文件再无关系。
2. 新增 TimeAxis(name / time / minTime / maxTime / monotonic / finiteCount),一个分组可含 1..N 条;
SignalData 新增 axisIndex 指向所属时间轴,采样值与时间的对应关系自包含。
3. SignalData 删除冗余的 rawHeader(name 的副本)与 unit(由列名猜出、全仓无人使用);
名字只保留 name(源名称)+ displayName(用户重命名),Title() 统一展示。
4. SignalRef::fileIndex → groupIndex;DataSet:AddGroup/RemoveGroup/GroupAt/GroupCount/Groups()/
SetGroupDisplayName/RemapSelectionAfterGroupRemoval + 新增 Axis(ref);
TabConfig:SetGroupVisible/GroupHasVisible/GroupAllVisible/RemapAfterGroupRemoval +
RowKind::GROUP / CMD_GROUP_*。
5. 采样与统计 API 首参由 const DataFile& 改为 const TimeAxis&:
SampleSignalAt / SampleDisplayedSignalAt / FindNearestSample(Utilities)与
SampleSpacingPixels / VisibleIndexRange / EdgeIndexRange / MeanFilterValueAt / WeakPeakValueAt /
EdgeCountsFor / EdgeCenterCountsFor / EdgeScanFor(PlotView)。
6. 视图层:ResolvedSignal = { data, style, group, axis, visible };PolylineCacheEntry::file→group;
EdgeScanState::file→axis(统计身份 = 时间轴 + 信号);
ComputeVisibleTimeBounds 只统计可见信号真正用到的时间轴。
二、加载层抽象(格式与模型彻底解耦)
- 新增 Utilities/Inc/DataTable.hpp:DataTable(通用列优先表格)+ LoadProgress/Info +
TableBlock(一个时间轴块)+ LoadedSource(源文件全部块)。
- 新增 Utilities/Inc|Src/DataLoader.*:唯一入口 LoadSignalSource 按扩展名分发 +
IsSupportedSignalFile + SignalFileDialogFilter。
- 新增 Utilities/Inc|Src/CsvLoader.*:CSV 解析 + 多时间轴切分 + 各轴按自己的时间列裁剪。
- 新增 Utilities/Inc|Src/Mf4Loader.*:ASAM MDF v4 解析。
- 删除 Utilities/Inc/CsvTable.hpp 与 Utilities/Src/CsvTable.cpp。
- 唯一装配点 BuildSignalGroup(LoadedSource&&, paletteBase, group, error, progress):
时间轴去重合并 + 信号命名 + 调色板取色 + 统计与粗粒度包络;新增格式只需写一个加载器。
三、CSV 多时间轴(修正用户报告的缺陷)
- 只有名为 t/time/timestamp/simulation time/时间/时间轴 的列是时间轴(判据收紧,
不再用「包含 time」以免把 timeout 误判成时间轴)。
- 每个时间列开启一个新块,其后直到下一个时间列之前的列都属于该块;各块按自己的时间列裁剪
⇒ 支持各轴长度不同(Multi_Timeline.CSV 实测切出 3 条轴,长度 7/4/7);
**不再把其它 time 列当成信号显示**。
- 完全没有时间列时保留旧行为(第一列当时间轴,单轴)。
四、MF4 支持(新功能)
- 加载后新建一个分组、组名 = 文件名,文件内全部信号归入该分组。
- 每个通道组(CG)是一条时间轴,内容相同的时间轴自动合并(实测 27 个 CG → 3 条轴 / 124 条信号)。
- 实现 MDF v4 块解析(HD/DG/CG/CN/CC/DT)、1–64 位整数(任意位偏移、含符号扩展)与
32/64 位 IEEE 浮点解码、无效位(inval bit)处理、线性换算 phys = cc_val[0] + cc_val[1]*raw。
- TX 文本块同时兼容规范写法(U32 长度 + UTF-16/UTF-8)与 CANape 的「无长度前缀 NUL 结尾裸文本」。
- 未实现(跳过该组):压缩块 ##DZ/##HL/##SD、##DL 数据列表、字符串/MIME 通道。
五、顺带修复
- Application::Initialize:窗口类已注册(ERROR_CLASS_ALREADY_EXISTS)时复用而不是弹模态错误框
(旧实现让同一进程第二次构造 Application 永久阻塞);Shutdown 注销窗口类并销毁窗口。
- UI 文案统一到「分组」:选中该分组全部信号 / 取消该分组全部信号 / 关闭该分组(从工作区移除)/
请关闭部分分组或信号后重试。/ 尚未加载数据,请打开数据文件 / 打开数据文件以查看波形 /
(可同时加载多个文件,各自成为一个分组);对话框过滤器增加 *.mf4/*.mdf。
六、验证(全部离屏:BELOW_NORMAL 优先级 + SW_HIDE 窗口 + 只注入合成消息)
- 新增 .preflight/perf/datacheck_main.cpp:69/69 通过(单轴 CSV 回归、多时间轴 CSV、MF4 组名/信号数/
时间轴/主时间轴 ns→s/setSpeed 首样本与原始字节人工核对、DataSet 不变量、派生信号沿用源轴、
MF4 集成 124 信号并一格 60~68ms/帧)。
- 新增 .preflight/perf/edgecheck_main.cpp(ASCII-only,替代被编码破坏的 panelcheck):46/46 通过
(「调整」符号规则走真实菜单路径、边沿统计按帧推进/不饿死/逐位一致/无鼠标输入也完成、
面板两列等宽与信号名限宽、弱峰复位控件)。
- 既有 .preflight/perf/verify_main.cpp(已适配新 API):47/47,另有 2 条本机未同步的旧规则断言
(derived offsetTime kept / mouse in lane1 lower half),与本轮无关。
- make clean && make all 零警告。
This commit is contained in:
1 parent
fba40ee1a2
commit
6eedbe3c5d
18 files changed
+2545
-817
No files matched your search
@@ -183,9 +183,9 @@ namespace jdi
|
||||
|
||||
/**
|
||||
* @brief 从工作区移除指定文件并修正各标签页配置。
|
||||
* @param [IN] fileIndex 文件下标。
|
||||
* @param [IN] groupIndex 文件下标。
|
||||
*/
|
||||
void RemoveFile(int fileIndex);
|
||||
void RemoveGroup(int groupIndex);
|
||||
|
||||
/**
|
||||
* @brief 从工作区移除某分组中的一条信号,并立即释放其采样内存。
|
||||
@@ -193,10 +193,10 @@ namespace jdi
|
||||
* 用户点击信号行左侧的 ✕ 时调用:该信号一旦被确认不再需要,就应从数据层删除
|
||||
* 并释放内存(而非只是隐藏)。若它是所属分组中最后一条信号,则连同分组一起移除。
|
||||
*
|
||||
* @param [IN] fileIndex 信号所属文件下标。
|
||||
* @param [IN] groupIndex 信号所属文件下标。
|
||||
* @param [IN] signalIndex 文件内信号下标。
|
||||
*/
|
||||
void RemoveSignal(int fileIndex, int signalIndex);
|
||||
void RemoveSignal(int groupIndex, int signalIndex);
|
||||
|
||||
/**
|
||||
* @brief 处理标签栏交互结果。
|
||||
@@ -259,7 +259,7 @@ namespace jdi
|
||||
*/
|
||||
struct DerivedSignalRequest
|
||||
{
|
||||
int fileIndex = -1; /**< 原信号所属文件下标。 */
|
||||
int groupIndex = -1; /**< 原信号所属文件下标。 */
|
||||
int signalIndex = -1; /**< 原信号下标。 */
|
||||
int points = 5; /**< 多次迭代均值滤波:均值点数 X(滑平半径)。 */
|
||||
int iterations = 1; /**< 多次迭代均值滤波:迭代次数 N(至少 1)。 */
|
||||
@@ -330,7 +330,7 @@ namespace jdi
|
||||
* @param [OUT] error 失败原因描述。
|
||||
* @return 加载成功返回 true;用户取消或失败返回 false。
|
||||
*/
|
||||
bool LoadFileAsync(const std::wstring& path, DataFile& file, std::wstring& error);
|
||||
bool LoadFileAsync(const std::wstring& path, SignalGroup& file, std::wstring& error);
|
||||
|
||||
/**
|
||||
* @brief 绘制加载过渡动画遮罩(在 Render 的最后一步调用,覆盖整个客户区)。
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include <shellapi.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include "CsvTable.hpp"
|
||||
#include "DataLoader.hpp"
|
||||
#include "ResourceIds.h"
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace jdi
|
||||
const float kMaxPanelWidth = 400.0f; /**< 面板最大宽度(逻辑像素)。 */
|
||||
const int kMinClientWidth = 860; /**< 窗口最小客户区宽度。 */
|
||||
const int kMinClientHeight = 540; /**< 窗口最小客户区高度。 */
|
||||
const UINT kMessageRemoveFile = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */
|
||||
const UINT kMessageRemoveGroup = WM_APP + 1; /**< 面板请求移除文件的自定义消息。 */
|
||||
const UINT kMessageRemoveSignal = WM_APP + 2; /**< 面板请求移除单个信号的自定义消息。 */
|
||||
const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
|
||||
const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
|
||||
@@ -177,11 +177,11 @@ namespace jdi
|
||||
std::atomic<unsigned long long> totalBytes{0}; /**< 文件总字节数(0 表示未知)。 */
|
||||
bool ok = false; /**< 加载是否成功。 */
|
||||
std::wstring error; /**< 失败原因。 */
|
||||
DataFile file; /**< 构建结果。 */
|
||||
SignalGroup group; /**< 构建结果。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 工作线程主体:解析 CSV 并构建数据文件(不接触 DataSet)。
|
||||
* @brief 工作线程主体:按扩展名选择加载器读入文件并构建信号分组(不接触 DataSet)。
|
||||
* @param [IN,OUT] state 共享状态。
|
||||
* @param [IN] path 文件路径。
|
||||
* @param [IN] paletteBase 调色板起始序号。
|
||||
@@ -189,7 +189,7 @@ namespace jdi
|
||||
void RunAsyncLoad(AsyncLoadState* state, std::wstring path, std::size_t paletteBase)
|
||||
{
|
||||
// 解析阶段占总进度的 0-75%,列重组与统计占 75-100%。
|
||||
CsvProgress parseProgress = [state](const CsvProgressInfo& info)
|
||||
LoadProgress parseProgress = [state](const LoadProgressInfo& info)
|
||||
{
|
||||
state->progress.store(static_cast<float>(info.ratio * 0.75), std::memory_order_relaxed);
|
||||
if (info.hasBytes)
|
||||
@@ -200,31 +200,31 @@ namespace jdi
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
CsvTable table;
|
||||
LoadedSource source;
|
||||
std::wstring error;
|
||||
if (!LoadCsvTable(path, table, error, parseProgress))
|
||||
if (!LoadSignalSource(path, source, error, parseProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
CsvProgress buildProgress = [state](const CsvProgressInfo& info)
|
||||
LoadProgress buildProgress = [state](const LoadProgressInfo& info)
|
||||
{
|
||||
state->progress.store(static_cast<float>(0.75 + info.ratio * 0.25),
|
||||
std::memory_order_relaxed);
|
||||
return !state->cancelled.load(std::memory_order_relaxed);
|
||||
};
|
||||
|
||||
DataFile file;
|
||||
if (!BuildDataFile(std::move(table), path, paletteBase, file, error, buildProgress))
|
||||
SignalGroup group;
|
||||
if (!BuildSignalGroup(std::move(source), paletteBase, group, error, buildProgress))
|
||||
{
|
||||
state->error = error;
|
||||
state->done.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
state->file = std::move(file);
|
||||
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);
|
||||
@@ -357,7 +357,10 @@ namespace jdi
|
||||
windowClass.hIcon = LoadAppIcon(GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON));
|
||||
windowClass.hIconSm = LoadAppIcon(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
|
||||
|
||||
if (RegisterClassExW(&windowClass) == 0)
|
||||
// ⚠️ 同一个进程里再次创建 `Application`(离屏测试、单进程多次启动)时窗口类已经注册,
|
||||
// 这时**不是错误**:直接复用已注册的类即可。旧实现把它当致命错误弹模态框,
|
||||
// 会让调用方永远阻塞在对话框上。
|
||||
if (RegisterClassExW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||
{
|
||||
MessageBoxW(nullptr, L"窗口类注册失败。", kWindowTitle, MB_ICONERROR | MB_OK);
|
||||
return false;
|
||||
@@ -439,6 +442,15 @@ namespace jdi
|
||||
gd::GdiplusShutdown(m_gdiplusToken);
|
||||
m_gdiplusReady = false;
|
||||
}
|
||||
|
||||
// 注销窗口类:否则同一进程里再次创建 `Application` 时会命中
|
||||
// `ERROR_CLASS_ALREADY_EXISTS`(离屏测试需要多次构造/销毁)。
|
||||
if (m_hwnd != nullptr)
|
||||
{
|
||||
DestroyWindow(m_hwnd);
|
||||
m_hwnd = nullptr;
|
||||
}
|
||||
UnregisterClassW(kWindowClassName, m_instance);
|
||||
}
|
||||
|
||||
void Application::ApplyDpi(UINT dpi)
|
||||
@@ -655,48 +667,48 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void Application::RemoveFile(int fileIndex)
|
||||
void Application::RemoveGroup(int groupIndex)
|
||||
{
|
||||
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
||||
if (file == nullptr)
|
||||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||||
if (group == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::wstring name = file->fileName;
|
||||
const std::wstring name = group->name;
|
||||
|
||||
// 移除文件会释放大量采样内存:先暂停后台派生曲线计算,避免其读取已释放内存。
|
||||
m_plot.PauseDerivedAsync();
|
||||
// 先按旧下标修正各标签页配置与共享选中项,再从数据层移除,最后统一对齐。
|
||||
for (TabConfig& tab : m_tabs)
|
||||
{
|
||||
tab.RemapAfterFileRemoval(fileIndex);
|
||||
tab.RemapAfterGroupRemoval(groupIndex);
|
||||
}
|
||||
m_dataSet.RemapSelectionAfterFileRemoval(fileIndex);
|
||||
m_dataSet.RemoveFile(fileIndex);
|
||||
m_dataSet.RemapSelectionAfterGroupRemoval(groupIndex);
|
||||
m_dataSet.RemoveGroup(groupIndex);
|
||||
SyncTabsToDataSet();
|
||||
|
||||
SetStatusMessage(L"已从工作区移除:" + name, false);
|
||||
UpdateWindowTitle();
|
||||
}
|
||||
|
||||
void Application::RemoveSignal(int fileIndex, int signalIndex)
|
||||
void Application::RemoveSignal(int groupIndex, int signalIndex)
|
||||
{
|
||||
SignalRef target;
|
||||
target.fileIndex = fileIndex;
|
||||
target.groupIndex = groupIndex;
|
||||
target.signalIndex = signalIndex;
|
||||
|
||||
const SignalData* signal = m_dataSet.Resolve(target);
|
||||
const DataFile* file = m_dataSet.FileAt(fileIndex);
|
||||
if (signal == nullptr || file == nullptr)
|
||||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||||
if (signal == nullptr || group == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::wstring name = signal->Title();
|
||||
|
||||
if (file->signals.size() <= 1)
|
||||
if (group->signals.size() <= 1)
|
||||
{
|
||||
// 组内只剩这一条信号:直接移除整个分组,避免在列表里留下一个空分组。
|
||||
RemoveFile(fileIndex);
|
||||
RemoveGroup(groupIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -705,10 +717,10 @@ namespace jdi
|
||||
// 先按旧下标修正各标签页配置与共享选中项,再从数据层移除(释放采样内存),最后对齐。
|
||||
for (TabConfig& tab : m_tabs)
|
||||
{
|
||||
tab.RemapAfterSignalRemoval(fileIndex, signalIndex);
|
||||
tab.RemapAfterSignalRemoval(groupIndex, signalIndex);
|
||||
}
|
||||
m_dataSet.RemapSelectionAfterSignalRemoval(fileIndex, signalIndex);
|
||||
m_dataSet.RemoveSignal(fileIndex, signalIndex);
|
||||
m_dataSet.RemapSelectionAfterSignalRemoval(groupIndex, signalIndex);
|
||||
m_dataSet.RemoveSignal(groupIndex, signalIndex);
|
||||
SyncTabsToDataSet();
|
||||
|
||||
SetStatusMessage(L"已从工作区移除信号:" + name, false);
|
||||
@@ -967,10 +979,10 @@ namespace jdi
|
||||
OPENFILENAMEW dialog = {};
|
||||
dialog.lStructSize = sizeof(dialog);
|
||||
dialog.hwndOwner = m_hwnd;
|
||||
dialog.lpstrFilter = L"CSV 数据文件 (*.csv)\0*.csv\0文本数据文件 (*.txt)\0*.txt\0所有文件 (*.*)\0*.*\0\0";
|
||||
dialog.lpstrFilter = SignalFileDialogFilter();
|
||||
dialog.lpstrFile = buffer.data();
|
||||
dialog.nMaxFile = static_cast<DWORD>(buffer.size());
|
||||
dialog.lpstrTitle = L"打开仿真数据文件(可多选)";
|
||||
dialog.lpstrTitle = L"打开测量数据文件(可多选)";
|
||||
dialog.lpstrDefExt = L"csv";
|
||||
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_EXPLORER | OFN_NOCHANGEDIR |
|
||||
OFN_ALLOWMULTISELECT;
|
||||
@@ -1021,9 +1033,9 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 大文件在工作线程解析并构建,主线程播放加载过渡动画,界面不再整块卡住。
|
||||
DataFile file;
|
||||
SignalGroup group;
|
||||
std::wstring error;
|
||||
if (!LoadFileAsync(path, file, error))
|
||||
if (!LoadFileAsync(path, group, error))
|
||||
{
|
||||
if (!error.empty())
|
||||
{
|
||||
@@ -1033,30 +1045,45 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
int fileIndex = -1;
|
||||
// 插入新文件会移动 DataFile/SignalData 的存储位置,必须先暂停后台派生曲线计算,
|
||||
int groupIndex = -1;
|
||||
// 插入新分组会移动 SignalGroup/SignalData 的存储位置,必须先暂停后台派生曲线计算,
|
||||
// 否则后台线程可能正在读取即将被移动的采样数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
// 此处只做一次指针/缓冲移动,不拷贝任何采样数据,因此不会造成可见卡顿。
|
||||
if (!m_dataSet.AddDataFile(std::move(file), fileIndex))
|
||||
if (!m_dataSet.AddGroup(std::move(group), groupIndex))
|
||||
{
|
||||
SetStatusMessage(L"解析失败:数据为空。", true);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 新文件的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
|
||||
// 新分组的信号在全部标签页中默认均为未勾选状态,由用户自行决定查看内容。
|
||||
SyncTabsToDataSet();
|
||||
UpdateWindowTitle();
|
||||
|
||||
const DataFile* loaded = m_dataSet.FileAt(fileIndex);
|
||||
const SignalGroup* loaded = m_dataSet.GroupAt(groupIndex);
|
||||
if (loaded != nullptr)
|
||||
{
|
||||
std::wstring message = L"已加载 组" + std::to_wstring(fileIndex + 1) + L"|" + loaded->Title() +
|
||||
L"(" + FormatCount(static_cast<unsigned long long>(loaded->time.size())) +
|
||||
// 行数取各时间轴的**最大值**(多时间轴文件的各轴长度可能不同)。
|
||||
std::size_t rowCount = 0;
|
||||
bool anyNonMonotonic = false;
|
||||
std::wstring timeNames;
|
||||
for (const TimeAxis& axis : loaded->axes)
|
||||
{
|
||||
rowCount = std::max(rowCount, axis.Count());
|
||||
anyNonMonotonic = anyNonMonotonic || !axis.monotonic;
|
||||
if (!timeNames.empty())
|
||||
{
|
||||
timeNames += L"、";
|
||||
}
|
||||
timeNames += axis.name;
|
||||
}
|
||||
std::wstring message = L"已加载 组" + std::to_wstring(groupIndex + 1) + L"|" + loaded->Title() +
|
||||
L"(" + FormatCount(static_cast<unsigned long long>(rowCount)) +
|
||||
L" 行采样," + std::to_wstring(loaded->signals.size()) +
|
||||
L" 条信号,时间列:" + loaded->timeColumnName + L")";
|
||||
if (!loaded->timeMonotonic)
|
||||
L" 条信号," + std::to_wstring(loaded->axes.size()) + L" 条时间轴:" +
|
||||
timeNames + L")";
|
||||
if (anyNonMonotonic)
|
||||
{
|
||||
message += L" 提示:时间列非单调递增,光标取值使用最近邻策略。";
|
||||
}
|
||||
@@ -1065,7 +1092,7 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::LoadFileAsync(const std::wstring& path, DataFile& file, std::wstring& error)
|
||||
bool Application::LoadFileAsync(const std::wstring& path, SignalGroup& group, std::wstring& error)
|
||||
{
|
||||
error.clear();
|
||||
|
||||
@@ -1138,7 +1165,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
file = std::move(state.file);
|
||||
group = std::move(state.group);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1549,7 +1576,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_dataSet.SetSignalDisplayName(ref.fileIndex, ref.signalIndex, name))
|
||||
if (!m_dataSet.SetSignalDisplayName(ref.groupIndex, ref.signalIndex, name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1563,13 +1590,14 @@ namespace jdi
|
||||
|
||||
bool Application::BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible)
|
||||
{
|
||||
const DataFile* file = m_dataSet.FileAt(request.fileIndex);
|
||||
if (file == nullptr || request.signalIndex < 0 ||
|
||||
request.signalIndex >= static_cast<int>(file->signals.size()))
|
||||
const SignalGroup* group = m_dataSet.GroupAt(request.groupIndex);
|
||||
if (group == nullptr || request.signalIndex < 0 ||
|
||||
request.signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SignalData& source = file->signals[static_cast<std::size_t>(request.signalIndex)];
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(request.signalIndex)];
|
||||
const TimeAxis& axis = group->AxisOf(source);
|
||||
const std::wstring baseName = source.Title();
|
||||
|
||||
// 0. 先把源信号的「调整」变换快照下来(下面 `InsertDerivedSignal` 会移动信号存储,
|
||||
@@ -1590,7 +1618,7 @@ namespace jdi
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
SignalRef sourceRef;
|
||||
sourceRef.fileIndex = request.fileIndex;
|
||||
sourceRef.groupIndex = request.groupIndex;
|
||||
sourceRef.signalIndex = request.signalIndex;
|
||||
if (const SignalStyle* sourceStyle = m_dataSet.Style(sourceRef))
|
||||
{
|
||||
@@ -1619,8 +1647,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 2. 组装新信号(外观一律用默认值:变换已经烘进采样数据,不再是"继承的指纹")。
|
||||
derived.rawHeader = derived.name;
|
||||
derived.unit = source.unit;
|
||||
// ⚠️ 新信号**沿用源信号的时间轴**(`axisIndex`):派生只改采样值,不改时间基准。
|
||||
derived.axisIndex = source.axisIndex;
|
||||
derived.values.assign(derived_values.begin(), derived_values.end());
|
||||
|
||||
// 2b. 把源信号的「调整」变换**烘进派生采样值**:
|
||||
@@ -1660,9 +1688,9 @@ namespace jdi
|
||||
// ⚠️ 时间轴非单调时无法按下标单调推进取样(只能整表扫描),此时退回"继承
|
||||
// `offsetTime`"(见 3b):两条路径的**显示效果相同**,只是偏移留在样式里。
|
||||
bool timeShiftBaked = false;
|
||||
if (hasSourceStyle && sourceTransform.offsetTime != 0.0 && file->timeMonotonic)
|
||||
if (hasSourceStyle && sourceTransform.offsetTime != 0.0 && axis.monotonic)
|
||||
{
|
||||
const std::vector<double>& times = file->time;
|
||||
const std::vector<double>& times = axis.time;
|
||||
const std::size_t total = derived.values.size();
|
||||
if (total > 1 && times.size() >= total)
|
||||
{
|
||||
@@ -1698,14 +1726,14 @@ namespace jdi
|
||||
int newIndex = -1;
|
||||
// 插入派生信号会移动该文件的信号存储:先暂停后台计算,避免其读取已移动的数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
if (!m_dataSet.InsertDerivedSignal(request.fileIndex, request.signalIndex, std::move(derived),
|
||||
if (!m_dataSet.InsertDerivedSignal(request.groupIndex, request.signalIndex, std::move(derived),
|
||||
newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (TabConfig& tab : m_tabs)
|
||||
{
|
||||
tab.RemapAfterSignalInsert(request.fileIndex, newIndex);
|
||||
tab.RemapAfterSignalInsert(request.groupIndex, newIndex);
|
||||
}
|
||||
|
||||
// 3b. 时间轴偏移正常情况下已烘进采样值(见 2c),新信号的「调整」全部保持默认。
|
||||
@@ -1713,7 +1741,7 @@ namespace jdi
|
||||
// `offsetTime` 带在新信号样式上 —— 无论如何都要让**显示**与预览一致。
|
||||
if (hasSourceStyle && sourceTransform.offsetTime != 0.0 && !timeShiftBaked)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{request.fileIndex, newIndex}))
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{request.groupIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->offsetTime = sourceTransform.offsetTime;
|
||||
}
|
||||
@@ -1723,12 +1751,12 @@ namespace jdi
|
||||
if (makeVisible && !m_tabs.empty())
|
||||
{
|
||||
SignalRef derivedRef;
|
||||
derivedRef.fileIndex = request.fileIndex;
|
||||
derivedRef.groupIndex = request.groupIndex;
|
||||
derivedRef.signalIndex = newIndex;
|
||||
ActiveTab().SetVisible(derivedRef, true);
|
||||
|
||||
SignalRef sourceRef;
|
||||
sourceRef.fileIndex = request.fileIndex;
|
||||
sourceRef.groupIndex = request.groupIndex;
|
||||
sourceRef.signalIndex = request.signalIndex;
|
||||
ActiveTab().MoveLaneOfSignalBelow(derivedRef, sourceRef);
|
||||
}
|
||||
@@ -1751,7 +1779,7 @@ namespace jdi
|
||||
for (const PlotView::MeanFilterBuild& item : items)
|
||||
{
|
||||
DerivedSignalRequest request;
|
||||
request.fileIndex = item.fileIndex;
|
||||
request.groupIndex = item.groupIndex;
|
||||
request.signalIndex = item.signalIndex;
|
||||
request.points = item.points;
|
||||
request.iterations = item.iterations;
|
||||
@@ -1768,7 +1796,7 @@ namespace jdi
|
||||
for (const PlotView::WeakPeakBuild& item : items)
|
||||
{
|
||||
DerivedSignalRequest request;
|
||||
request.fileIndex = item.fileIndex;
|
||||
request.groupIndex = item.groupIndex;
|
||||
request.signalIndex = item.signalIndex;
|
||||
request.amountPercent = item.amountPercent;
|
||||
request.iterations = item.iterations;
|
||||
@@ -1791,9 +1819,9 @@ namespace jdi
|
||||
std::sort(requests.begin(), requests.end(),
|
||||
[](const DerivedSignalRequest& a, const DerivedSignalRequest& b)
|
||||
{
|
||||
if (a.fileIndex != b.fileIndex)
|
||||
if (a.groupIndex != b.groupIndex)
|
||||
{
|
||||
return a.fileIndex < b.fileIndex;
|
||||
return a.groupIndex < b.groupIndex;
|
||||
}
|
||||
return a.signalIndex > b.signalIndex;
|
||||
});
|
||||
@@ -2364,7 +2392,7 @@ namespace jdi
|
||||
// 可用内存**严重不足**(连一条预览曲线本身都装不下):绘图区已放弃本次派生计算,
|
||||
// 这里给出一次明确的告警。界面保持可用,绝不会因为内存不足而崩溃。
|
||||
SetStatusMessage(L"可用内存不足,已暂停派生预览曲线与相关统计的计算。\n"
|
||||
L"请关闭部分文件分组或信号后重试。",
|
||||
L"请关闭部分分组或信号后重试。",
|
||||
true);
|
||||
InvalidatePlotRect(hwnd, m_plotRect);
|
||||
return 0;
|
||||
@@ -2945,9 +2973,9 @@ namespace jdi
|
||||
m_menu.Close();
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
case kMessageRemoveFile:
|
||||
case kMessageRemoveGroup:
|
||||
{
|
||||
RemoveFile(static_cast<int>(wParam));
|
||||
RemoveGroup(static_cast<int>(wParam));
|
||||
return 0;
|
||||
}
|
||||
case kMessageRemoveSignal:
|
||||
|
||||
Reference in new issue
Block a user