数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + 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:
|
||||
|
||||
@@ -322,7 +322,7 @@ namespace jdi
|
||||
* @param [OUT] pixels 间隔换算到屏幕上的像素宽度。
|
||||
* @return 计算成功返回 true。
|
||||
*/
|
||||
bool SampleSpacingPixels(const DataFile& file, double time, double& pixels) const;
|
||||
bool SampleSpacingPixels(const TimeAxis& axis, double time, double& pixels) const;
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在绘图区范围内。
|
||||
@@ -408,7 +408,7 @@ namespace jdi
|
||||
* @brief 暂停后台派生曲线计算并作废其结果。
|
||||
*
|
||||
* 后台线程会读取采样数据,因此**任何会改变数据层结构(增删文件/信号、插入派生信号,
|
||||
* 进而移动 `DataFile`/`SignalData` 存储)的操作之前**都必须先调用本函数:它会等待正在
|
||||
* 进而移动 `SignalGroup`/`SignalData` 存储)的操作之前**都必须先调用本函数:它会等待正在
|
||||
* 进行的计算结束、清空待算队列与缓存,从而杜绝后台线程读取已释放内存。
|
||||
* 之后的绘制会按需重新启动后台线程(惰性)。
|
||||
*/
|
||||
@@ -587,7 +587,7 @@ namespace jdi
|
||||
*/
|
||||
struct MeanFilterBuild
|
||||
{
|
||||
int fileIndex = -1; /**< 信号所属文件下标。 */
|
||||
int groupIndex = -1; /**< 信号所属分组下标。 */
|
||||
int signalIndex = -1; /**< 文件内信号下标。 */
|
||||
int points = 5; /**< 均值点数 X(滑平半径)。 */
|
||||
int iterations = 1; /**< 迭代次数 N(至少 1)。 */
|
||||
@@ -611,7 +611,7 @@ namespace jdi
|
||||
*/
|
||||
struct WeakPeakBuild
|
||||
{
|
||||
int fileIndex = -1; /**< 信号所属文件下标。 */
|
||||
int groupIndex = -1; /**< 信号所属分组下标。 */
|
||||
int signalIndex = -1; /**< 文件内信号下标。 */
|
||||
int amountPercent = 0; /**< 弱峰调整幅度(-400–400,默认 0 = 不改动)。 */
|
||||
int iterations = 1; /**< 迭代次数 N(1–10)。 */
|
||||
@@ -659,7 +659,7 @@ namespace jdi
|
||||
|
||||
/**
|
||||
* @brief 取某个时刻的弱峰调整曲线取值(用于纵向光标的取值标注)。
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] axis 该信号使用的时间轴。
|
||||
* @param [IN] signal 目标信号。
|
||||
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
|
||||
* @param [IN] iterations 迭代次数 N(1–10)。
|
||||
@@ -667,7 +667,7 @@ namespace jdi
|
||||
* @param [OUT] value 输出取值。
|
||||
* @return 取到有效值返回 true。
|
||||
*/
|
||||
bool WeakPeakValueAt(const DataFile& file, const SignalData& signal, int amountPercent, int iterations,
|
||||
bool WeakPeakValueAt(const TimeAxis& axis, const SignalData& signal, int amountPercent, int iterations,
|
||||
double time, double& value) const;
|
||||
|
||||
/**
|
||||
@@ -783,7 +783,8 @@ namespace jdi
|
||||
{
|
||||
const SignalData* data = nullptr; /**< 信号数据(含全局共享的显示外观)。 */
|
||||
const SignalStyle* style = nullptr; /**< 显示外观。 */
|
||||
const DataFile* file = nullptr; /**< 所属文件。 */
|
||||
const SignalGroup* group = nullptr; /**< 信号所属分组(仅用于分组级信息)。 */
|
||||
const TimeAxis* axis = nullptr; /**< 该信号使用的时间轴(采样值 ↔ 时刻的唯一依据)。 */
|
||||
bool visible = false; /**< 在当前标签页中是否可见。 */
|
||||
};
|
||||
|
||||
@@ -1294,7 +1295,7 @@ namespace jdi
|
||||
|
||||
/**
|
||||
* @brief 取某个时刻的多次迭代均值滤波曲线取值(用于纵向光标的取值标注)。
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] axis 该信号使用的时间轴。
|
||||
* @param [IN] signal 目标信号。
|
||||
* @param [IN] points 均值点数 X。
|
||||
* @param [IN] iterations 迭代次数 N。
|
||||
@@ -1302,7 +1303,7 @@ namespace jdi
|
||||
* @param [OUT] value 输出取值。
|
||||
* @return 取到有效值返回 true。
|
||||
*/
|
||||
bool MeanFilterValueAt(const DataFile& file, const SignalData& signal, int points, int iterations,
|
||||
bool MeanFilterValueAt(const TimeAxis& axis, const SignalData& signal, int points, int iterations,
|
||||
double time, double& value) const;
|
||||
|
||||
/**
|
||||
@@ -1329,7 +1330,7 @@ namespace jdi
|
||||
* ⚠️ 判定对象是**画出来的两条曲线**:信号与中心曲线都要先按 `style` 做水平镜像映射、
|
||||
* 再套用 `ApplyDrawTransform`(系数 → 绝对值 → 垂直镜像 → Y 偏移)。
|
||||
*
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] axis 该信号使用的时间轴。
|
||||
* @param [IN] signal 目标信号。
|
||||
* @param [IN] style 该信号的外观(含水平镜像与「调整」变换)。
|
||||
* @param [IN] points 均值点数 X。
|
||||
@@ -1341,7 +1342,7 @@ namespace jdi
|
||||
* @param [OUT] falling 下降沿数量(信号从上往下穿过均值滤波曲线)。
|
||||
* @param [OUT] complete 是否为完整精确值(false = 仍在推进,界面显示“…”)。
|
||||
*/
|
||||
void EdgeCenterCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
void EdgeCenterCountsFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||||
int points, int iterations, bool hasRange, double rangeMin,
|
||||
double rangeMax, std::size_t& rising, std::size_t& falling,
|
||||
bool& complete) const;
|
||||
@@ -1583,7 +1584,7 @@ namespace jdi
|
||||
* @param [IN] timeShift 信号的时间偏移:采样画在 `t + timeShift` 处,因此可见采样对应的
|
||||
* 源时刻区间为 `[ViewMin - timeShift, ViewMax - timeShift]`(偏移 0 时同视界)。
|
||||
*/
|
||||
void VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
|
||||
void VisibleIndexRange(const TimeAxis& axis, std::size_t count, std::size_t& begin,
|
||||
std::size_t& end, double timeShift = 0.0) const;
|
||||
|
||||
/**
|
||||
@@ -1677,7 +1678,7 @@ namespace jdi
|
||||
* 阈值变化(横向光标拖动中)就地把累积状态重置(不新建条目),且当帧不展示部分值
|
||||
*(`justReset`),避免“错误且不更新的数字”。
|
||||
*
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] axis 该信号使用的时间轴。
|
||||
* @param [IN] signal 目标信号。
|
||||
* @param [IN] style 该信号的外观(含水平镜像与「调整」变换)。
|
||||
* @param [IN] edgeValue 横向光标所在的幅值。
|
||||
@@ -1688,7 +1689,7 @@ namespace jdi
|
||||
* @param [OUT] falling 下降沿数量(从上往下穿越)。
|
||||
* @param [OUT] complete 结果是否为该阈值的**完整**精确值(false = 仍在推进,当前是临时值)。
|
||||
*/
|
||||
void EdgeCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
void EdgeCountsFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||||
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling, bool& complete) const;
|
||||
|
||||
@@ -1905,12 +1906,12 @@ namespace jdi
|
||||
* * 未扫完时**主动请宿主再送一帧**(`RequestEdgeScanFrame`),因此**不需要移动鼠标**
|
||||
* 也会持续推进(修掉"面板一直显示统计中,把鼠标移到面板上才更新")。
|
||||
*
|
||||
* 键 = 「文件 + 信号 + 种类 + 样式指纹 + 参数 +(仅非单调时间轴)区间」;
|
||||
* 键 = 「时间轴 + 信号 + 种类 + 样式指纹 + 参数 +(仅非单调时间轴)区间」;
|
||||
* 阈值/参数变化时就地重新开始(不新建条目,避免拖动时状态爆炸)。
|
||||
*/
|
||||
struct EdgeScanState
|
||||
{
|
||||
const DataFile* file = nullptr; /**< 所属数据分组。 */
|
||||
const TimeAxis* axis = nullptr; /**< 该信号使用的时间轴(统计身份 = 时间轴 + 信号)。 */
|
||||
const SignalData* signal = nullptr; /**< 目标信号。 */
|
||||
bool centre = false; /**< false = 穿越横向光标阈值;true = 穿越中心平均值曲线。 */
|
||||
std::uint64_t styleKey = 0; /**< 「调整」变换 + 水平镜像的指纹。 */
|
||||
@@ -1972,7 +1973,7 @@ namespace jdi
|
||||
* 中心轴时两侧(信号与中心曲线)都套用同一套变换。
|
||||
* 时间区间**之外**的采样一律不参与计数(只有两个端点都在区间内的采样对才计入)。
|
||||
*
|
||||
* @param [IN] file 信号所属文件。 @param [IN] signal 目标信号。
|
||||
* @param [IN] axis 该信号使用的时间轴。 @param [IN] signal 目标信号。
|
||||
* @param [IN] style 信号外观。 @param [IN] centre 是否为中心轴判定。
|
||||
* @param [IN] threshold 阈值(`centre = false` 时使用)。
|
||||
* @param [IN] points 均值点数 X(`centre = true` 时使用)。
|
||||
@@ -1984,7 +1985,7 @@ namespace jdi
|
||||
* @param [OUT] rising 上升沿数量。 @param [OUT] falling 下降沿数量。
|
||||
* @param [OUT] complete 是否为完整精确值(false = 仍在推进)。
|
||||
*/
|
||||
void EdgeScanFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
void EdgeScanFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||||
bool centre, double threshold, int points, int iterations, bool hasRange,
|
||||
double rangeMin, double rangeMax, std::size_t& rising, std::size_t& falling,
|
||||
bool& complete) const;
|
||||
@@ -2102,7 +2103,7 @@ namespace jdi
|
||||
struct PolylineCacheEntry
|
||||
{
|
||||
SignalRef ref; /**< 信号引用。 */
|
||||
const DataFile* file = nullptr; /**< 所属文件(内容不可变)。 */
|
||||
const SignalGroup* group = nullptr; /**< 所属分组(缓存键的一部分)。 */
|
||||
std::size_t sampleCount = 0; /**< 采样点数(数据变化时改变)。 */
|
||||
double viewMin = 0.0; /**< 时间轴显示下界。 */
|
||||
double viewMax = 0.0; /**< 时间轴显示上界。 */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "PlotView.hpp"
|
||||
#include "PlotView.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -504,7 +504,7 @@ namespace jdi
|
||||
* @param [OUT] end 输出区间终点(不含)。
|
||||
* @return 计数是否完全由该下标区间决定(时间轴单调时为真)。
|
||||
*/
|
||||
bool EdgeIndexRange(const DataFile& file, std::size_t count, bool hasRange, double rangeMin,
|
||||
bool EdgeIndexRange(const TimeAxis& axis, std::size_t count, bool hasRange, double rangeMin,
|
||||
double rangeMax, std::size_t& begin, std::size_t& end)
|
||||
{
|
||||
begin = 0;
|
||||
@@ -513,13 +513,13 @@ namespace jdi
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!file.timeMonotonic)
|
||||
if (!axis.monotonic)
|
||||
{
|
||||
// 时间轴非单调时无法用下标区间表达统计范围(需逐点按时间过滤)。
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<double>& times = file.time;
|
||||
const std::vector<double>& times = axis.time;
|
||||
const std::size_t timeCount = std::min(times.size(), count);
|
||||
begin = static_cast<std::size_t>(
|
||||
std::lower_bound(times.begin(), times.begin() + static_cast<std::ptrdiff_t>(timeCount),
|
||||
@@ -920,7 +920,8 @@ namespace jdi
|
||||
return result;
|
||||
}
|
||||
result.data = m_dataSet->Resolve(ref);
|
||||
result.file = m_dataSet->FileAt(ref.fileIndex);
|
||||
result.group = m_dataSet->GroupAt(ref.groupIndex);
|
||||
result.axis = m_dataSet->Axis(ref);
|
||||
result.style = m_dataSet->Style(ref);
|
||||
result.visible = (m_tab != nullptr) && m_tab->IsVisible(ref);
|
||||
return result;
|
||||
@@ -1011,9 +1012,9 @@ namespace jdi
|
||||
}
|
||||
if (firstFile < 0)
|
||||
{
|
||||
firstFile = ref.fileIndex;
|
||||
firstFile = ref.groupIndex;
|
||||
}
|
||||
else if (ref.fileIndex != firstFile)
|
||||
else if (ref.groupIndex != firstFile)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1582,13 +1583,13 @@ namespace jdi
|
||||
double high = -std::numeric_limits<double>::infinity();
|
||||
for (const SignalRef& ref : lanes[static_cast<std::size_t>(laneIndex)].signals)
|
||||
{
|
||||
const DataFile* file = m_dataSet->FileAt(ref.fileIndex);
|
||||
if (file == nullptr)
|
||||
const TimeAxis* axis = m_dataSet->Axis(ref);
|
||||
if (axis == nullptr || axis->finiteCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
low = std::min(low, file->minTime);
|
||||
high = std::max(high, file->maxTime);
|
||||
low = std::min(low, axis->minTime);
|
||||
high = std::max(high, axis->maxTime);
|
||||
}
|
||||
|
||||
if (!std::isfinite(low) || !std::isfinite(high) || !(high > low))
|
||||
@@ -1644,10 +1645,10 @@ namespace jdi
|
||||
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals.size();
|
||||
}
|
||||
|
||||
bool PlotView::SampleSpacingPixels(const DataFile& file, double time, double& pixels) const
|
||||
bool PlotView::SampleSpacingPixels(const TimeAxis& axis, double time, double& pixels) const
|
||||
{ pixels = 0.0;
|
||||
|
||||
const std::vector<double>& samples = file.time;
|
||||
const std::vector<double>& samples = axis.time;
|
||||
if (samples.size() < 2 || m_plotRect.Width <= 1.0f)
|
||||
{
|
||||
return false;
|
||||
@@ -1659,7 +1660,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
const int index = FindNearestSample(file, time);
|
||||
const int index = FindNearestSample(axis, time);
|
||||
if (index < 0)
|
||||
{
|
||||
return false;
|
||||
@@ -1703,37 +1704,42 @@ namespace jdi
|
||||
double bestDistance = std::numeric_limits<double>::infinity();
|
||||
double bestTime = 0.0;
|
||||
|
||||
for (const DataFile& file : m_dataSet->Files())
|
||||
for (const SignalGroup& group : m_dataSet->Groups())
|
||||
{
|
||||
// 只有采样点在屏幕上足够稀疏时才吸附,否则缩小视图后光标会“跳格”。
|
||||
double pixels = 0.0;
|
||||
if (!SampleSpacingPixels(file, time, pixels) || pixels < minPixels || pixels > maxPixels)
|
||||
// 一个分组可能有多条时间轴(多 `time` 列 CSV / MF4 多通道组),逐条尝试。
|
||||
for (const TimeAxis& groupAxis : group.axes)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double pixels = 0.0;
|
||||
if (!SampleSpacingPixels(groupAxis, time, pixels) || pixels < minPixels ||
|
||||
pixels > maxPixels)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const int index = FindNearestSample(file, time);
|
||||
if (index < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double candidate = file.time[static_cast<std::size_t>(index)];
|
||||
if (!std::isfinite(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 只吸附**当前可见区间内**的采样点:视界之外的采样点不是合法的落点。
|
||||
if (candidate < viewMin || candidate > viewMax)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const int index = FindNearestSample(groupAxis, time);
|
||||
if (index < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double candidate = groupAxis.time[static_cast<std::size_t>(index)];
|
||||
if (!std::isfinite(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 只吸附**当前可见区间内**的采样点:视界之外的采样点不是合法的落点。
|
||||
if (candidate < viewMin || candidate > viewMax)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const double distance = std::fabs(candidate - time);
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
bestTime = candidate;
|
||||
found = true;
|
||||
const double distance = std::fabs(candidate - time);
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
bestDistance = distance;
|
||||
bestTime = candidate;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2372,10 +2378,10 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::MeanFilterValueAt(const DataFile& file, const SignalData& signal, int points, int iterations,
|
||||
bool PlotView::MeanFilterValueAt(const TimeAxis& axis, const SignalData& signal, int points, int iterations,
|
||||
double time, double& value) const
|
||||
{
|
||||
const int index = FindNearestSample(file, time);
|
||||
const int index = FindNearestSample(axis, time);
|
||||
if (index < 0)
|
||||
{
|
||||
return false;
|
||||
@@ -2402,17 +2408,17 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::VisibleIndexRange(const DataFile& file, std::size_t count, std::size_t& begin,
|
||||
void PlotView::VisibleIndexRange(const TimeAxis& axis, std::size_t count, std::size_t& begin,
|
||||
std::size_t& end, double timeShift) const
|
||||
{
|
||||
begin = 0;
|
||||
end = count;
|
||||
if (!file.timeMonotonic || count == 0)
|
||||
if (!axis.monotonic || count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 时间偏移:采样画在 `t + timeShift` 处,因此落在当前视界内的采样其源时刻区间要整体左移。
|
||||
const std::vector<double>& time = file.time;
|
||||
const std::vector<double>& time = axis.time;
|
||||
begin = static_cast<std::size_t>(
|
||||
std::lower_bound(time.begin(), time.end(), Tab().ViewMin() - timeShift) - time.begin());
|
||||
if (begin > 0)
|
||||
@@ -3053,10 +3059,10 @@ namespace jdi
|
||||
return m_weakPeakCurveCache.back().values;
|
||||
}
|
||||
|
||||
bool PlotView::WeakPeakValueAt(const DataFile& file, const SignalData& signal, int amountPercent,
|
||||
bool PlotView::WeakPeakValueAt(const TimeAxis& axis, const SignalData& signal, int amountPercent,
|
||||
int iterations, double time, double& value) const
|
||||
{
|
||||
const int index = FindNearestSample(file, time);
|
||||
const int index = FindNearestSample(axis, time);
|
||||
if (index < 0)
|
||||
{
|
||||
return false;
|
||||
@@ -3115,7 +3121,7 @@ namespace jdi
|
||||
m_edgeScans.end());
|
||||
}
|
||||
|
||||
void PlotView::EdgeScanFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
void PlotView::EdgeScanFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||||
bool centre, double threshold, int points, int iterations, bool hasRange,
|
||||
double rangeMin, double rangeMax, std::size_t& rising, std::size_t& falling,
|
||||
bool& complete) const
|
||||
@@ -3130,7 +3136,7 @@ namespace jdi
|
||||
complete = true;
|
||||
return;
|
||||
}
|
||||
const bool monotonic = file.timeMonotonic;
|
||||
const bool monotonic = axis.monotonic;
|
||||
// 非单调时间轴无法用「采样对下标区间」表达统计范围,只能逐点按时间过滤 →
|
||||
// 此时区间必须进键(否则会混用别的区间结果)。
|
||||
const bool rangeInKey = hasRange && !monotonic;
|
||||
@@ -3149,7 +3155,7 @@ namespace jdi
|
||||
{
|
||||
std::size_t indexBegin = 0;
|
||||
std::size_t indexEnd = 0;
|
||||
EdgeIndexRange(file, count, hasRange, rangeMin, rangeMax, indexBegin, indexEnd);
|
||||
EdgeIndexRange(axis, count, hasRange, rangeMin, rangeMax, indexBegin, indexEnd);
|
||||
pairFirst = (indexBegin + 1 < count) ? (indexBegin + 1) : count;
|
||||
pairLast = (indexEnd > 0) ? (indexEnd - 1) : 0;
|
||||
if (pairLast < pairFirst)
|
||||
@@ -3163,7 +3169,7 @@ namespace jdi
|
||||
EdgeScanState* state = nullptr;
|
||||
for (EdgeScanState& candidate : m_edgeScans)
|
||||
{
|
||||
if (candidate.file == &file && candidate.signal == &signal && candidate.centre == centre &&
|
||||
if (candidate.axis == &axis && candidate.signal == &signal && candidate.centre == centre &&
|
||||
candidate.styleKey == styleKey && candidate.points == safePoints &&
|
||||
candidate.iterations == safeIterations && candidate.hasRange == rangeInKey &&
|
||||
(!rangeInKey || (candidate.rangeMin == rangeMin && candidate.rangeMax == rangeMax)))
|
||||
@@ -3197,7 +3203,7 @@ namespace jdi
|
||||
m_edgeScans.erase(m_edgeScans.begin() + static_cast<std::ptrdiff_t>(victim));
|
||||
}
|
||||
EdgeScanState created;
|
||||
created.file = &file;
|
||||
created.axis = &axis;
|
||||
created.signal = &signal;
|
||||
created.centre = centre;
|
||||
created.styleKey = styleKey;
|
||||
@@ -3314,9 +3320,9 @@ namespace jdi
|
||||
{
|
||||
return nan;
|
||||
}
|
||||
if (filterByTime && index < file.time.size())
|
||||
if (filterByTime && index < axis.time.size())
|
||||
{
|
||||
const double time = file.time[index];
|
||||
const double time = axis.time[index];
|
||||
if (time < rangeMin || time > rangeMax)
|
||||
{
|
||||
return nan;
|
||||
@@ -3539,22 +3545,22 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::EdgeCountsFor(const DataFile& file, const SignalData& signal, const SignalStyle& style,
|
||||
void PlotView::EdgeCountsFor(const TimeAxis& axis, const SignalData& signal, const SignalStyle& style,
|
||||
double edgeValue, bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling, bool& complete) const
|
||||
{
|
||||
EdgeScanFor(file, signal, style, false, edgeValue, 0, 1, hasRange, rangeMin, rangeMax, rising,
|
||||
EdgeScanFor(axis, signal, style, false, edgeValue, 0, 1, hasRange, rangeMin, rangeMax, rising,
|
||||
falling, complete);
|
||||
}
|
||||
|
||||
void PlotView::EdgeCenterCountsFor(const DataFile& file, const SignalData& signal,
|
||||
void PlotView::EdgeCenterCountsFor(const TimeAxis& axis, const SignalData& signal,
|
||||
const SignalStyle& style, int points, int iterations,
|
||||
bool hasRange, double rangeMin, double rangeMax,
|
||||
std::size_t& rising, std::size_t& falling, bool& complete) const
|
||||
{
|
||||
// 与普通边沿计数完全共用同一套「逐块累积 + 每 tick 预算推进」机制;
|
||||
// 区别只在判定基准(信号 − 中心平均值曲线)与所需曲线(均值滤波,需整条)。
|
||||
EdgeScanFor(file, signal, style, true, 0.0, points, iterations, hasRange, rangeMin, rangeMax,
|
||||
EdgeScanFor(axis, signal, style, true, 0.0, points, iterations, hasRange, rangeMin, rangeMax,
|
||||
rising, falling, complete);
|
||||
}
|
||||
|
||||
@@ -3621,13 +3627,13 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EdgeCenterCountsFor(*resolved.file, *resolved.data, *resolved.style, item->points,
|
||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
|
||||
item->iterations, EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(),
|
||||
rising, falling, complete);
|
||||
}
|
||||
else
|
||||
{
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, lane.edgeValue,
|
||||
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style, lane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
|
||||
}
|
||||
// 宽度按实际要显示的文本测量(含未算完时的“…”),否则推进过程中数字变长会被截断。
|
||||
@@ -3812,13 +3818,13 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||||
const DataFile* file = m_dataSet->FileAt(ref.fileIndex);
|
||||
if (signal == nullptr || file == nullptr)
|
||||
const TimeAxis* axis = m_dataSet->Axis(ref);
|
||||
if (signal == nullptr || axis == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t count = std::min(file->time.size(), signal->values.size());
|
||||
const std::size_t count = std::min(axis->time.size(), signal->values.size());
|
||||
const SignalStyle* style = m_dataSet->Style(ref);
|
||||
// 时间偏移:可见采样对应的源时刻区间要整体左移(画在 t + shift 处)。
|
||||
const double timeShift = (style != nullptr) ? style->offsetTime : 0.0;
|
||||
@@ -3826,17 +3832,17 @@ namespace jdi
|
||||
const double sourceMax = viewMax - timeShift;
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = count;
|
||||
if (file->timeMonotonic)
|
||||
if (axis->monotonic)
|
||||
{
|
||||
// 时间单调时直接二分定位区间,百万级采样也只需扫过可见段。
|
||||
begin = static_cast<std::size_t>(
|
||||
std::lower_bound(file->time.begin(), file->time.end(), sourceMin) - file->time.begin());
|
||||
std::lower_bound(axis->time.begin(), axis->time.end(), sourceMin) - axis->time.begin());
|
||||
if (begin > 0)
|
||||
{
|
||||
--begin;
|
||||
}
|
||||
end = static_cast<std::size_t>(
|
||||
std::lower_bound(file->time.begin(), file->time.end(), sourceMax) - file->time.begin());
|
||||
std::lower_bound(axis->time.begin(), axis->time.end(), sourceMax) - axis->time.begin());
|
||||
if (end < count)
|
||||
{
|
||||
++end;
|
||||
@@ -3857,7 +3863,7 @@ namespace jdi
|
||||
// `ComputeStats` 预存的 min/max 完全一致。工具栏「Y 轴自适应」开关在全视图下
|
||||
// 会一次性对全部网格做这件事 —— 逐点扫描 2580 万 × 5 是秒级卡顿(实测 1200ms),
|
||||
// 这里改为 O(1)。
|
||||
if (file->timeMonotonic && begin == 0 && end == count && !mirrored && !transform &&
|
||||
if (axis->monotonic && begin == 0 && end == count && !mirrored && !transform &&
|
||||
signal->finiteCount > 0)
|
||||
{
|
||||
low = std::min(low, signal->minValue);
|
||||
@@ -3870,9 +3876,9 @@ namespace jdi
|
||||
// 无变换:与既有实现完全一致的紧凑循环(无逐点分支,便于向量化)。
|
||||
for (std::size_t index = begin; index < end; ++index)
|
||||
{
|
||||
if (!file->timeMonotonic)
|
||||
if (!axis->monotonic)
|
||||
{
|
||||
const double t = file->time[index];
|
||||
const double t = axis->time[index];
|
||||
if (!std::isfinite(t) || t < sourceMin || t > sourceMax)
|
||||
{
|
||||
continue;
|
||||
@@ -3895,9 +3901,9 @@ namespace jdi
|
||||
// 水平镜像时画面上的第 index 个位置取源下标 count-1-index 的值。
|
||||
for (std::size_t index = begin; index < end; ++index)
|
||||
{
|
||||
if (!file->timeMonotonic)
|
||||
if (!axis->monotonic)
|
||||
{
|
||||
const double t = file->time[index];
|
||||
const double t = axis->time[index];
|
||||
if (!std::isfinite(t) || t < sourceMin || t > sourceMax)
|
||||
{
|
||||
continue;
|
||||
@@ -4145,7 +4151,7 @@ namespace jdi
|
||||
// 否则每次鼠标移动都要遍历上万个顶点(悬停卡顿的来源之一)。
|
||||
std::size_t windowFirst = 0;
|
||||
std::size_t windowLast = points.size();
|
||||
if (resolved.file->timeMonotonic)
|
||||
if (resolved.axis->monotonic)
|
||||
{
|
||||
const auto byX = [](const gd::PointF& point, float limit) { return point.X < limit; };
|
||||
windowFirst = static_cast<std::size_t>(
|
||||
@@ -5674,7 +5680,7 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
WeakPeakBuild build;
|
||||
build.fileIndex = ref.fileIndex;
|
||||
build.groupIndex = ref.groupIndex;
|
||||
build.signalIndex = ref.signalIndex;
|
||||
const int peakAmount =
|
||||
std::max(kWeakPeakAmountMin, std::min(kWeakPeakAmountMax, item->amountPercent));
|
||||
@@ -5718,7 +5724,7 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
MeanFilterBuild build;
|
||||
build.fileIndex = ref.fileIndex;
|
||||
build.groupIndex = ref.groupIndex;
|
||||
build.signalIndex = ref.signalIndex;
|
||||
build.points = item->points;
|
||||
build.iterations = item->iterations;
|
||||
@@ -6094,7 +6100,7 @@ namespace jdi
|
||||
const ResolvedSignal& resolved,
|
||||
const LaneVisual& lane) const
|
||||
{
|
||||
if (resolved.data == nullptr || resolved.file == nullptr)
|
||||
if (resolved.data == nullptr || resolved.group == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
@@ -6113,7 +6119,7 @@ namespace jdi
|
||||
// 键 = 构建折线时实际读取的全部输入:
|
||||
// 信号(引用 + 文件内容 + 采样点数)、时间轴显示范围、绘图区横向几何、网格纵向几何。
|
||||
// 这些值全部相同时,BuildPolyline 的输出必然完全一致,可直接复用。
|
||||
const std::size_t sampleCount = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||
const std::size_t sampleCount = std::min(resolved.axis->time.size(), resolved.data->values.size());
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
// 绘制前的数值变换(系数/绝对值/镜像/偏移)会改变顶点,必须进缓存键。
|
||||
@@ -6126,7 +6132,7 @@ namespace jdi
|
||||
|
||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||
{
|
||||
if (entry.ref == ref && entry.file == resolved.file && entry.sampleCount == sampleCount &&
|
||||
if (entry.ref == ref && entry.group == resolved.group && entry.sampleCount == sampleCount &&
|
||||
entry.viewMin == viewMin && entry.viewMax == viewMax && entry.plotX == m_plotRect.X &&
|
||||
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
||||
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
||||
@@ -6147,7 +6153,7 @@ namespace jdi
|
||||
m_polylineCache.push_back(PolylineCacheEntry());
|
||||
PolylineCacheEntry& entry = m_polylineCache.back();
|
||||
entry.ref = ref;
|
||||
entry.file = resolved.file;
|
||||
entry.group = resolved.group;
|
||||
entry.sampleCount = sampleCount;
|
||||
entry.viewMin = viewMin;
|
||||
entry.viewMax = viewMax;
|
||||
@@ -6182,12 +6188,12 @@ namespace jdi
|
||||
bandMode = false;
|
||||
bandSpiky = false;
|
||||
|
||||
if (resolved.data == nullptr || resolved.file == nullptr)
|
||||
if (resolved.data == nullptr || resolved.group == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<double>& time = resolved.file->time;
|
||||
const std::vector<double>& time = resolved.axis->time;
|
||||
const std::vector<double>& values = resolved.data->values;
|
||||
const std::size_t count = std::min(time.size(), values.size());
|
||||
if (count == 0 || m_plotRect.Width <= 1.0f)
|
||||
@@ -6207,7 +6213,7 @@ namespace jdi
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = count;
|
||||
|
||||
if (resolved.file->timeMonotonic)
|
||||
if (resolved.axis->monotonic)
|
||||
{
|
||||
const std::vector<double>::const_iterator first = std::lower_bound(time.begin(), time.end(), sourceMin);
|
||||
begin = static_cast<std::size_t>(first - time.begin());
|
||||
@@ -6259,12 +6265,12 @@ namespace jdi
|
||||
// 把阈值降到 1 采样/像素后,1~2 采样/像素区间走包络带(逐列 min/max 与折线在该密度下
|
||||
// 几乎等价,且**保留抗锯齿**),彻底消除这一区间的病态卡顿。
|
||||
const bool useEnvelopeBand = (visibleCount > static_cast<std::size_t>(m_plotRect.Width)) &&
|
||||
resolved.file->timeMonotonic && (m_plotRect.Width > 1.0f);
|
||||
resolved.axis->monotonic && (m_plotRect.Width > 1.0f);
|
||||
|
||||
// 逐点描边路径若处于"横向补条"渲染:把采样区间裁剪到条带对应的 x 范围
|
||||
//(两侧各留少量像素余量以避免接缝与抗锯齿溢出)。逐点描边路径必然 `visibleCount <= budget`
|
||||
//(否则走包络),此时 `stride == 1`,裁剪后条带内的顶点序列与整块渲染完全一致 → 像素不变。
|
||||
if (!useEnvelopeBand && m_stripRenderActive && resolved.file->timeMonotonic &&
|
||||
if (!useEnvelopeBand && m_stripRenderActive && resolved.axis->monotonic &&
|
||||
m_stripX1 > m_stripX0)
|
||||
{
|
||||
const float marginPx =
|
||||
@@ -6581,11 +6587,11 @@ namespace jdi
|
||||
std::wstring message;
|
||||
if (m_dataSet == nullptr || m_dataSet->Empty())
|
||||
{
|
||||
message = L"打开 CSV 文件以查看仿真数据波形";
|
||||
message = L"打开数据文件以查看波形";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = L"请在左侧勾选需要查看的信号(可同时加载多个文件进行对比)";
|
||||
message = L"请在左侧勾选需要查看的信号(可同时加载多个文件,各自成为一个分组)";
|
||||
}
|
||||
UiText(g, message, m_fonts->ui, m_area, m_theme->textMuted, TextAlign::CENTER);
|
||||
}
|
||||
@@ -7111,7 +7117,7 @@ namespace jdi
|
||||
key.scale = m_scale;
|
||||
|
||||
const SignalRef halo = SceneHaloRef();
|
||||
key.haloFile = halo.fileIndex;
|
||||
key.haloFile = halo.groupIndex;
|
||||
key.haloSignal = halo.signalIndex;
|
||||
key.contentHash = SceneContentHash();
|
||||
return key;
|
||||
@@ -7195,7 +7201,7 @@ namespace jdi
|
||||
{
|
||||
const SignalRef selected = SelectedRef();
|
||||
mix(selected.IsValid() ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(selected.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(selected.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(selected.signalIndex));
|
||||
}
|
||||
|
||||
@@ -7225,7 +7231,7 @@ namespace jdi
|
||||
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].edgeCenterItems.size()));
|
||||
for (const Lane::EdgeCenterItem& item : Tab().Lanes()[laneIndex].edgeCenterItems)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(item.ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
|
||||
mix(static_cast<std::uint64_t>(item.points));
|
||||
mix(static_cast<std::uint64_t>(item.iterations));
|
||||
@@ -7234,7 +7240,7 @@ namespace jdi
|
||||
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].meanFilterItems.size()));
|
||||
for (const Lane::MeanFilterItem& item : Tab().Lanes()[laneIndex].meanFilterItems)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(item.ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
|
||||
mix(static_cast<std::uint64_t>(item.points));
|
||||
mix(static_cast<std::uint64_t>(item.iterations));
|
||||
@@ -7244,7 +7250,7 @@ namespace jdi
|
||||
for (const Lane::WeakPeakItem& item : Tab().Lanes()[laneIndex].weakPeakItems)
|
||||
{
|
||||
// 逐信号参数(等级 + 迭代次数)都参与指纹:改任一条的参数都会重画。
|
||||
mix(static_cast<std::uint64_t>(item.ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
|
||||
mix(static_cast<std::uint64_t>(item.amountPercent));
|
||||
mix(static_cast<std::uint64_t>(item.iterations));
|
||||
@@ -7254,7 +7260,7 @@ namespace jdi
|
||||
for (const SignalRef& ref : signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
mix(static_cast<std::uint64_t>(ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||||
mix(resolved.visible ? 1ull : 0ull);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
@@ -7281,9 +7287,9 @@ namespace jdi
|
||||
}
|
||||
// 图例文案参与绘制,重命名必须让缓存失效。
|
||||
mixText(resolved.data->Title());
|
||||
if (resolved.file != nullptr)
|
||||
if (resolved.group != nullptr)
|
||||
{
|
||||
mixText(resolved.file->Title());
|
||||
mixText(resolved.group->Title());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7425,7 +7431,7 @@ namespace jdi
|
||||
target.reserve(items.size() * 4);
|
||||
for (const auto& item : items)
|
||||
{
|
||||
target.push_back(item.ref.fileIndex);
|
||||
target.push_back(item.ref.groupIndex);
|
||||
target.push_back(item.ref.signalIndex);
|
||||
// 均值滤波/中心轴取 points,弱峰调整取 amountPercent(由 `valueOf` 决定)。
|
||||
target.push_back(static_cast<int>(valueOf(item)));
|
||||
@@ -8290,7 +8296,7 @@ namespace jdi
|
||||
for (const SignalRef& ref : signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
mix(static_cast<std::uint64_t>(ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||||
mix(resolved.visible ? 1ull : 0ull);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
@@ -9945,9 +9951,9 @@ namespace jdi
|
||||
}
|
||||
++visibleCount;
|
||||
std::wstring label = BuildLegendText(*resolved.data);
|
||||
if (multiFile && resolved.file != nullptr)
|
||||
if (multiFile && resolved.group != nullptr)
|
||||
{
|
||||
label += L" · " + resolved.file->Title();
|
||||
label += L" · " + resolved.group->Title();
|
||||
}
|
||||
const float maxTextWidth = kLegendMaxTextWidth * m_scale;
|
||||
const float fullTextWidth = UiMeasureTextWidth(m_fonts->uiTiny, label);
|
||||
@@ -10130,20 +10136,20 @@ namespace jdi
|
||||
UiDrawSolidLines(g, axisClip, axisSegment, 1, axisColor);
|
||||
}
|
||||
|
||||
// 时间轴列名(取自首个可见信号所属文件),贴左侧绘制。
|
||||
// 时间轴列名(取自首个可见信号所属分组的第一条时间轴),贴左侧绘制。
|
||||
std::wstring axisName = L"时间";
|
||||
if (m_dataSet != nullptr && m_tab != nullptr)
|
||||
{
|
||||
for (std::size_t fileIndex = 0; fileIndex < m_dataSet->FileCount(); ++fileIndex)
|
||||
for (std::size_t groupIndex = 0; groupIndex < m_dataSet->GroupCount(); ++groupIndex)
|
||||
{
|
||||
if (!m_tab->FileHasVisible(static_cast<int>(fileIndex)))
|
||||
if (!m_tab->GroupHasVisible(static_cast<int>(groupIndex)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const DataFile* file = m_dataSet->FileAt(static_cast<int>(fileIndex));
|
||||
if (file != nullptr && !file->timeColumnName.empty())
|
||||
const SignalGroup* group = m_dataSet->GroupAt(static_cast<int>(groupIndex));
|
||||
if (group != nullptr && !group->axes.empty() && !group->axes.front().name.empty())
|
||||
{
|
||||
axisName = file->timeColumnName;
|
||||
axisName = group->axes.front().name;
|
||||
// CSV 中该列通常直接叫 "time",这里补上时间单位秒便于阅读;
|
||||
// 若列名自带单位描述(以右括号结尾)则不重复追加。
|
||||
if (axisName.back() != L')')
|
||||
@@ -10457,14 +10463,14 @@ namespace jdi
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr ||
|
||||
resolved.file == nullptr || !resolved.visible)
|
||||
resolved.axis == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
|
||||
double value = 0.0;
|
||||
if (!SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, time,
|
||||
if (!SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style, time,
|
||||
interpolate, value))
|
||||
{
|
||||
continue;
|
||||
@@ -10491,7 +10497,7 @@ namespace jdi
|
||||
if (showDelta)
|
||||
{
|
||||
double otherValue = 0.0;
|
||||
if (SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, otherTime,
|
||||
if (SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style, otherTime,
|
||||
interpolate, otherValue))
|
||||
{
|
||||
otherValue = ApplyDrawTransform(*resolved.style, otherValue);
|
||||
@@ -10561,13 +10567,13 @@ namespace jdi
|
||||
int iterations, int amountPercent,
|
||||
const std::wstring& suffix)
|
||||
{
|
||||
const int sampleIndex = FindNearestSample(*resolved.file, time);
|
||||
const int sampleIndex = FindNearestSample(*resolved.axis, time);
|
||||
if (sampleIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const int otherIndex =
|
||||
showDelta ? FindNearestSample(*resolved.file, otherTime) : -1;
|
||||
showDelta ? FindNearestSample(*resolved.axis, otherTime) : -1;
|
||||
// 本函数**只需要光标所在采样点处的派生取值**(以及双光标另一端的取值),
|
||||
// 因此把请求窗口收窄到「需要的那几个下标」即可,均值滤波半径造成的取值
|
||||
// 依赖由 `CurveSpanFor` 自身的保护带负责覆盖。
|
||||
@@ -11374,12 +11380,12 @@ namespace jdi
|
||||
const LaneVisual& lane, const std::vector<float>& curve,
|
||||
std::size_t curveBegin) const
|
||||
{
|
||||
if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr)
|
||||
if (resolved.data == nullptr || resolved.axis == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<double>& time = resolved.file->time;
|
||||
const std::vector<double>& time = resolved.axis->time;
|
||||
const std::size_t count = std::min(time.size(), resolved.data->values.size());
|
||||
if (count == 0 || m_plotRect.Width <= 1.0f || curve.empty())
|
||||
{
|
||||
@@ -11388,7 +11394,7 @@ namespace jdi
|
||||
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = 0;
|
||||
VisibleIndexRange(*resolved.file, count, begin, end, resolved.style->offsetTime);
|
||||
VisibleIndexRange(*resolved.axis, count, begin, end, resolved.style->offsetTime);
|
||||
if (end <= begin)
|
||||
{
|
||||
return;
|
||||
@@ -11501,14 +11507,14 @@ namespace jdi
|
||||
void PlotView::PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||
const LaneVisual& lane, int points, int iterations) const
|
||||
{
|
||||
if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr)
|
||||
if (resolved.data == nullptr || resolved.axis == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 只取「可见区间 ± 保护带」的窗口(见 `CurveSpanFor`):拖参数滑块不会重算整条曲线。
|
||||
// 与「边沿计数(中心轴)」共用同一段绘制逻辑
|
||||
//(同色相高对比度的派生配色 + 同透明度),只是曲线本身按多次迭代滤波计算。
|
||||
const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||
const std::size_t count = std::min(resolved.axis->time.size(), resolved.data->values.size());
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
@@ -11531,11 +11537,11 @@ namespace jdi
|
||||
void PlotView::PaintWeakPeakCurve(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||
const LaneVisual& lane, int amountPercent, int iterations) const
|
||||
{
|
||||
if (resolved.data == nullptr || resolved.file == nullptr || resolved.style == nullptr)
|
||||
if (resolved.data == nullptr || resolved.axis == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::size_t count = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||
const std::size_t count = std::min(resolved.axis->time.size(), resolved.data->values.size());
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
@@ -11661,7 +11667,7 @@ namespace jdi
|
||||
const auto mixRef = [&mix](const SignalRef& ref)
|
||||
{
|
||||
mix(ref.IsValid() ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||||
};
|
||||
|
||||
@@ -11798,7 +11804,7 @@ namespace jdi
|
||||
for (const SignalRef& ref : layout.rowRefs)
|
||||
{
|
||||
mix(ref.IsValid() ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(ref.fileIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(ref.signalIndex));
|
||||
// 勾选状态与参数直接决定行文本与行高。
|
||||
mix(lane.MeanFilterEnabled(ref) ? 1ull : 0ull);
|
||||
@@ -12339,7 +12345,7 @@ namespace jdi
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool statsComplete = false;
|
||||
EdgeCenterCountsFor(*resolved.file, *resolved.data, *resolved.style,
|
||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style,
|
||||
statsPoints, statsOrder, EdgeRangeActive(),
|
||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling,
|
||||
statsComplete);
|
||||
@@ -12374,7 +12380,7 @@ namespace jdi
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
bool complete = true;
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, tabLane.edgeValue,
|
||||
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style, tabLane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling,
|
||||
complete);
|
||||
paintCounts(gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight), rising,
|
||||
|
||||
@@ -28,9 +28,9 @@ namespace jdi
|
||||
CMD_SIGNAL_SOLO = 2, /**< 仅显示该信号。 */
|
||||
CMD_SIGNAL_SHOW_ALL = 3, /**< 显示全部信号。 */
|
||||
CMD_SIGNAL_HIDE_ALL = 4, /**< 隐藏全部信号。 */
|
||||
CMD_FILE_SHOW_ALL = 10, /**< 选中该文件全部信号。 */
|
||||
CMD_FILE_HIDE_ALL = 11, /**< 取消该文件全部信号。 */
|
||||
CMD_FILE_REMOVE = 12, /**< 从工作区移除该文件。 */
|
||||
CMD_GROUP_SHOW_ALL = 10, /**< 选中该文件全部信号。 */
|
||||
CMD_GROUP_HIDE_ALL = 11, /**< 取消该文件全部信号。 */
|
||||
CMD_GROUP_REMOVE = 12, /**< 从工作区移除该文件。 */
|
||||
CMD_COLOR_BASE = 100, /**< 预设颜色基准(配合色块下标)。 */
|
||||
CMD_MODE_BASE = 200, /**< 绘制样式基准(配合下标)。 */
|
||||
CMD_LANE_BASE = 300, /**< 网格归属基准(配合下标)。 */
|
||||
@@ -315,7 +315,7 @@ namespace jdi
|
||||
*/
|
||||
enum class RowKind : int
|
||||
{
|
||||
FILE = 0, /**< 文件分组标题行。 */
|
||||
GROUP = 0, /**< 文件分组标题行。 */
|
||||
SIGNAL = 1 /**< 信号行。 */
|
||||
};
|
||||
|
||||
@@ -359,7 +359,7 @@ namespace jdi
|
||||
struct ListRow
|
||||
{
|
||||
RowKind kind = RowKind::SIGNAL; /**< 行类型。 */
|
||||
int fileIndex = -1; /**< 所属文件下标。 */
|
||||
int groupIndex = -1; /**< 所属文件下标。 */
|
||||
int signalIndex = -1; /**< 信号下标(仅信号行有效)。 */
|
||||
float height = 0.0f; /**< 行高(像素)。 */
|
||||
};
|
||||
@@ -478,11 +478,11 @@ namespace jdi
|
||||
|
||||
/**
|
||||
* @brief 弹出文件分组右键菜单。
|
||||
* @param [IN] fileIndex 目标文件下标。
|
||||
* @param [IN] groupIndex 目标文件下标。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
*/
|
||||
void OpenFileMenu(int fileIndex, int x, int y);
|
||||
void OpenFileMenu(int groupIndex, int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 弹出网格归属菜单(由信号行的网格徽标触发)。
|
||||
|
||||
@@ -448,17 +448,17 @@ namespace jdi
|
||||
const float signalHeight = kSignalRowHeight * m_scale;
|
||||
const bool filtering = !m_filterText.empty();
|
||||
|
||||
const std::vector<DataFile>& files = m_dataSet->Files();
|
||||
for (std::size_t fileIndex = 0; fileIndex < files.size(); ++fileIndex)
|
||||
const std::vector<SignalGroup>& files = m_dataSet->Groups();
|
||||
for (std::size_t groupIndex = 0; groupIndex < files.size(); ++groupIndex)
|
||||
{
|
||||
// 筛选时先挑出本组内匹配的信号;整组都没有匹配则**不显示该组**。
|
||||
std::vector<int> matched;
|
||||
if (filtering)
|
||||
{
|
||||
const std::size_t signalCount = files[fileIndex].signals.size();
|
||||
const std::size_t signalCount = files[groupIndex].signals.size();
|
||||
for (std::size_t signalIndex = 0; signalIndex < signalCount; ++signalIndex)
|
||||
{
|
||||
if (FilterMatchesSignal(files[fileIndex].signals[signalIndex]))
|
||||
if (FilterMatchesSignal(files[groupIndex].signals[signalIndex]))
|
||||
{
|
||||
matched.push_back(static_cast<int>(signalIndex));
|
||||
}
|
||||
@@ -470,13 +470,13 @@ namespace jdi
|
||||
}
|
||||
|
||||
ListRow fileRow;
|
||||
fileRow.kind = RowKind::FILE;
|
||||
fileRow.fileIndex = static_cast<int>(fileIndex);
|
||||
fileRow.kind = RowKind::GROUP;
|
||||
fileRow.groupIndex = static_cast<int>(groupIndex);
|
||||
fileRow.height = fileHeight;
|
||||
m_rows.push_back(fileRow);
|
||||
|
||||
// 筛选结果**默认展开**(忽略折叠状态),便于直接看到匹配项。
|
||||
if (!filtering && m_tab->IsCollapsed(static_cast<int>(fileIndex)))
|
||||
if (!filtering && m_tab->IsCollapsed(static_cast<int>(groupIndex)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -487,7 +487,7 @@ namespace jdi
|
||||
{
|
||||
ListRow signalRow;
|
||||
signalRow.kind = RowKind::SIGNAL;
|
||||
signalRow.fileIndex = static_cast<int>(fileIndex);
|
||||
signalRow.groupIndex = static_cast<int>(groupIndex);
|
||||
signalRow.signalIndex = signalIndex;
|
||||
signalRow.height = signalHeight;
|
||||
m_rows.push_back(signalRow);
|
||||
@@ -495,12 +495,12 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t signalCount = files[fileIndex].signals.size();
|
||||
const std::size_t signalCount = files[groupIndex].signals.size();
|
||||
for (std::size_t signalIndex = 0; signalIndex < signalCount; ++signalIndex)
|
||||
{
|
||||
ListRow signalRow;
|
||||
signalRow.kind = RowKind::SIGNAL;
|
||||
signalRow.fileIndex = static_cast<int>(fileIndex);
|
||||
signalRow.groupIndex = static_cast<int>(groupIndex);
|
||||
signalRow.signalIndex = static_cast<int>(signalIndex);
|
||||
signalRow.height = signalHeight;
|
||||
m_rows.push_back(signalRow);
|
||||
@@ -639,7 +639,7 @@ namespace jdi
|
||||
{
|
||||
const int rowIndex = static_cast<int>(index);
|
||||
const gd::RectF rect = RowRect(rowIndex);
|
||||
const bool isFile = (row.kind == RowKind::FILE);
|
||||
const bool isFile = (row.kind == RowKind::GROUP);
|
||||
const float baseX = rect.X + kPadding * m_scale + (isFile ? 0.0f : kIndent * m_scale);
|
||||
|
||||
const gd::RectF disclosureRect(baseX, rect.Y, kDisclosureWidth * m_scale, rect.Height);
|
||||
@@ -668,7 +668,7 @@ namespace jdi
|
||||
}
|
||||
else
|
||||
{
|
||||
const SignalRef ref{row.fileIndex, row.signalIndex};
|
||||
const SignalRef ref{row.groupIndex, row.signalIndex};
|
||||
const bool badgeVisible = (m_tab != nullptr) && m_tab->IsVisible(ref);
|
||||
|
||||
// ✕ 命中优先于复选框:两者紧邻,顺序保证不会互相抢命中。
|
||||
@@ -708,7 +708,7 @@ namespace jdi
|
||||
for (std::size_t index = 0; index < m_rows.size(); ++index)
|
||||
{
|
||||
const ListRow& row = m_rows[index];
|
||||
if (row.kind == RowKind::SIGNAL && row.fileIndex == ref.fileIndex &&
|
||||
if (row.kind == RowKind::SIGNAL && row.groupIndex == ref.groupIndex &&
|
||||
row.signalIndex == ref.signalIndex)
|
||||
{
|
||||
if (top < m_scrollOffset)
|
||||
@@ -968,7 +968,7 @@ namespace jdi
|
||||
|
||||
if (m_dataSet == nullptr || m_dataSet->Empty())
|
||||
{
|
||||
UiText(g, L"尚未加载数据,请打开 CSV 文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||||
UiText(g, L"尚未加载数据,请打开数据文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
else if (m_rows.empty() && !m_filterText.empty())
|
||||
@@ -987,7 +987,7 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::FILE)
|
||||
if (m_rows[static_cast<std::size_t>(row)].kind == RowKind::GROUP)
|
||||
{
|
||||
PaintFileRow(g, row);
|
||||
}
|
||||
@@ -1025,8 +1025,8 @@ namespace jdi
|
||||
}
|
||||
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
const DataFile* file = m_dataSet->FileAt(item.fileIndex);
|
||||
if (file == nullptr)
|
||||
const SignalGroup* group = m_dataSet->GroupAt(item.groupIndex);
|
||||
if (group == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1042,7 +1042,7 @@ namespace jdi
|
||||
|
||||
const float baseX = rect.X + kPadding * m_scale;
|
||||
const float centerY = rect.Y + rect.Height * 0.5f;
|
||||
const bool collapsed = m_tab->IsCollapsed(item.fileIndex);
|
||||
const bool collapsed = m_tab->IsCollapsed(item.groupIndex);
|
||||
|
||||
// 折叠三角。
|
||||
{
|
||||
@@ -1057,15 +1057,15 @@ namespace jdi
|
||||
// 分组复选框(三态:全选 / 部分 / 未选)。
|
||||
const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f,
|
||||
kCheckSize * m_scale, kCheckSize * m_scale);
|
||||
const bool allVisible = m_tab->FileAllVisible(item.fileIndex);
|
||||
const bool anyVisible = m_tab->FileHasVisible(item.fileIndex);
|
||||
const bool allVisible = m_tab->GroupAllVisible(item.groupIndex);
|
||||
const bool anyVisible = m_tab->GroupHasVisible(item.groupIndex);
|
||||
UiDrawCheckBox(g, checkRect, allVisible ? 1 : (anyVisible ? 2 : 0), 3.0f * m_scale);
|
||||
|
||||
const gd::RectF closeRect = FileCloseRect(row);
|
||||
|
||||
// 分组标题:左侧为“组X”标签,右侧以绘制的竖线分隔分组名(不使用字符),再显示可重命名的名称。
|
||||
const std::wstring groupLabel = L"组" + std::to_wstring(item.fileIndex + 1);
|
||||
const std::wstring stats = std::to_wstring(file->signals.size()) + L" 项";
|
||||
const std::wstring groupLabel = L"组" + std::to_wstring(item.groupIndex + 1);
|
||||
const std::wstring stats = std::to_wstring(group->signals.size()) + L" 项";
|
||||
const float groupWidth = FileGroupLabelWidth(row);
|
||||
const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats);
|
||||
const float statsRight = closeRect.X - 6.0f * m_scale;
|
||||
@@ -1091,7 +1091,7 @@ namespace jdi
|
||||
}
|
||||
else
|
||||
{
|
||||
UiText(g, UiEllipsize(file->Title(), m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
||||
UiText(g, UiEllipsize(group->Title(), m_fonts->uiBold, nameWidth), m_fonts->uiBold,
|
||||
gd::RectF(nameAreaLeft, rect.Y, nameWidth, rect.Height), m_theme->textStrong,
|
||||
TextAlign::LEFT);
|
||||
}
|
||||
@@ -1116,7 +1116,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||||
const SignalStyle* style = StyleOf(ref);
|
||||
if (signal == nullptr || style == nullptr)
|
||||
@@ -1451,7 +1451,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
const gd::RectF rect = RowRect(row);
|
||||
const bool isFile = (m_rows[static_cast<std::size_t>(row)].kind == RowKind::FILE);
|
||||
const bool isFile = (m_rows[static_cast<std::size_t>(row)].kind == RowKind::GROUP);
|
||||
const float baseX = rect.X + kPadding * m_scale + (isFile ? 0.0f : kIndent * m_scale);
|
||||
const float checkRight = baseX + kDisclosureWidth * m_scale + kCheckSize * m_scale;
|
||||
|
||||
@@ -1468,7 +1468,7 @@ namespace jdi
|
||||
{
|
||||
left = checkRight + 6.0f * m_scale + kSwatchSize * m_scale + 6.0f * m_scale;
|
||||
// 行尾的可用右界:有网格徽标时让位给徽标,否则可直接延伸到行尾 ✕ 之前。
|
||||
const SignalRef ref{m_rows[static_cast<std::size_t>(row)].fileIndex,
|
||||
const SignalRef ref{m_rows[static_cast<std::size_t>(row)].groupIndex,
|
||||
m_rows[static_cast<std::size_t>(row)].signalIndex};
|
||||
const bool badgeVisible = (m_tab != nullptr) && m_tab->IsVisible(ref) &&
|
||||
(m_tab->LaneOfSignal(ref) >= 0);
|
||||
@@ -1489,7 +1489,7 @@ namespace jdi
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
const std::wstring label = L"组" + std::to_wstring(m_rows[static_cast<std::size_t>(row)].fileIndex + 1);
|
||||
const std::wstring label = L"组" + std::to_wstring(m_rows[static_cast<std::size_t>(row)].groupIndex + 1);
|
||||
return UiMeasureTextWidth(m_fonts->uiBold, label);
|
||||
}
|
||||
|
||||
@@ -1534,7 +1534,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
const SignalData* signal = m_dataSet->Resolve(SignalRef{item.fileIndex, item.signalIndex});
|
||||
const SignalData* signal = m_dataSet->Resolve(SignalRef{item.groupIndex, item.signalIndex});
|
||||
if (signal == nullptr)
|
||||
{
|
||||
return false;
|
||||
@@ -1548,12 +1548,12 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const DataFile* file = m_dataSet->FileAt(m_rows[static_cast<std::size_t>(row)].fileIndex);
|
||||
if (file == nullptr)
|
||||
const SignalGroup* group = m_dataSet->GroupAt(m_rows[static_cast<std::size_t>(row)].groupIndex);
|
||||
if (group == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
initial = file->Title();
|
||||
initial = group->Title();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -1638,7 +1638,7 @@ namespace jdi
|
||||
break;
|
||||
}
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
m_dataSetWritable->SetSignalDisplayName(item.fileIndex, item.signalIndex, trimmed);
|
||||
m_dataSetWritable->SetSignalDisplayName(item.groupIndex, item.signalIndex, trimmed);
|
||||
break;
|
||||
}
|
||||
case StyleEditField::FILE_NAME:
|
||||
@@ -1647,7 +1647,7 @@ namespace jdi
|
||||
{
|
||||
break;
|
||||
}
|
||||
m_dataSetWritable->SetFileDisplayName(m_rows[static_cast<std::size_t>(row)].fileIndex, trimmed);
|
||||
m_dataSetWritable->SetGroupDisplayName(m_rows[static_cast<std::size_t>(row)].groupIndex, trimmed);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -2458,12 +2458,12 @@ namespace jdi
|
||||
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
|
||||
if (item.kind == RowKind::FILE)
|
||||
if (item.kind == RowKind::GROUP)
|
||||
{
|
||||
if (part == RowPart::CHECK)
|
||||
{
|
||||
const bool allVisible = m_tab->FileAllVisible(item.fileIndex);
|
||||
m_tab->SetFileVisible(item.fileIndex, !allVisible);
|
||||
const bool allVisible = m_tab->GroupAllVisible(item.groupIndex);
|
||||
m_tab->SetGroupVisible(item.groupIndex, !allVisible);
|
||||
m_rowsDirty = true;
|
||||
return true;
|
||||
}
|
||||
@@ -2475,12 +2475,12 @@ namespace jdi
|
||||
m_pendingCloseRow = row;
|
||||
return true;
|
||||
}
|
||||
m_tab->ToggleCollapsed(item.fileIndex);
|
||||
m_tab->ToggleCollapsed(item.groupIndex);
|
||||
m_rowsDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
|
||||
// ✕:用户明确表示不再需要该信号 → 交由宿主从数据层移除并释放其采样内存。
|
||||
// 必须在 SelectSignal 之前处理,避免选中一条即将被删除的信号;
|
||||
@@ -2542,12 +2542,12 @@ namespace jdi
|
||||
{
|
||||
if (pendingPart == RowPart::CLOSE)
|
||||
{
|
||||
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(item.fileIndex), 0);
|
||||
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(item.groupIndex), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// wParam = 文件下标,lParam = 文件内信号下标(见 Application 的消息处理)。
|
||||
PostMessageW(m_host, WM_APP + 2, static_cast<WPARAM>(item.fileIndex),
|
||||
PostMessageW(m_host, WM_APP + 2, static_cast<WPARAM>(item.groupIndex),
|
||||
static_cast<LPARAM>(item.signalIndex));
|
||||
}
|
||||
}
|
||||
@@ -2616,7 +2616,7 @@ namespace jdi
|
||||
if (row >= 0 && part == RowPart::LANE_BADGE)
|
||||
{
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
if (ref == m_pendingLaneRef)
|
||||
{
|
||||
const gd::RectF badge = SignalBadgeRect(row);
|
||||
@@ -2660,11 +2660,11 @@ namespace jdi
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
|
||||
// 分组行:双击折叠三角用于折叠,双击名称区域进入重命名。
|
||||
if (item.kind == RowKind::FILE)
|
||||
if (item.kind == RowKind::GROUP)
|
||||
{
|
||||
if (part == RowPart::DISCLOSURE)
|
||||
{
|
||||
m_tab->ToggleCollapsed(item.fileIndex);
|
||||
m_tab->ToggleCollapsed(item.groupIndex);
|
||||
m_rowsDirty = true;
|
||||
return true;
|
||||
}
|
||||
@@ -2683,7 +2683,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
SelectSignal(ref);
|
||||
m_rowsDirty = true;
|
||||
return BeginEdit(StyleEditField::SIGNAL_NAME, row);
|
||||
@@ -2704,13 +2704,13 @@ namespace jdi
|
||||
}
|
||||
|
||||
const ListRow& item = m_rows[static_cast<std::size_t>(row)];
|
||||
if (item.kind == RowKind::FILE)
|
||||
if (item.kind == RowKind::GROUP)
|
||||
{
|
||||
OpenFileMenu(item.fileIndex, x, y);
|
||||
OpenFileMenu(item.groupIndex, x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
const SignalRef ref{item.fileIndex, item.signalIndex};
|
||||
const SignalRef ref{item.groupIndex, item.signalIndex};
|
||||
SelectSignal(ref);
|
||||
OpenSignalMenu(ref, x, y);
|
||||
return true;
|
||||
@@ -2825,37 +2825,37 @@ namespace jdi
|
||||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, x, y, items);
|
||||
}
|
||||
|
||||
void SignalPanel::OpenFileMenu(int fileIndex, int x, int y)
|
||||
void SignalPanel::OpenFileMenu(int groupIndex, int x, int y)
|
||||
{
|
||||
if (m_menu == nullptr || m_tab == nullptr || m_dataSet == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const DataFile* file = m_dataSet->FileAt(fileIndex);
|
||||
if (file == nullptr)
|
||||
const SignalGroup* group = m_dataSet->GroupAt(groupIndex);
|
||||
if (group == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_menuTargetFile = fileIndex;
|
||||
m_menuTargetFile = groupIndex;
|
||||
m_menuTargetSignal = SignalRef();
|
||||
|
||||
std::vector<MenuItem> items;
|
||||
|
||||
MenuItem header;
|
||||
header.kind = MenuItemKind::HEADER;
|
||||
header.label = file->Title();
|
||||
header.label = group->Title();
|
||||
items.push_back(header);
|
||||
|
||||
MenuItem showAll;
|
||||
showAll.label = L"选中该文件全部信号";
|
||||
showAll.id = CMD_FILE_SHOW_ALL;
|
||||
showAll.label = L"选中该分组全部信号";
|
||||
showAll.id = CMD_GROUP_SHOW_ALL;
|
||||
items.push_back(showAll);
|
||||
|
||||
MenuItem hideAll;
|
||||
hideAll.label = L"取消该文件全部信号";
|
||||
hideAll.id = CMD_FILE_HIDE_ALL;
|
||||
hideAll.label = L"取消该分组全部信号";
|
||||
hideAll.id = CMD_GROUP_HIDE_ALL;
|
||||
items.push_back(hideAll);
|
||||
|
||||
MenuItem separator;
|
||||
@@ -2863,8 +2863,8 @@ namespace jdi
|
||||
items.push_back(separator);
|
||||
|
||||
MenuItem remove;
|
||||
remove.label = L"关闭该文件(从工作区移除)";
|
||||
remove.id = CMD_FILE_REMOVE;
|
||||
remove.label = L"关闭该分组(从工作区移除)";
|
||||
remove.id = CMD_GROUP_REMOVE;
|
||||
items.push_back(remove);
|
||||
|
||||
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||||
@@ -3086,23 +3086,23 @@ namespace jdi
|
||||
m_rowsDirty = true;
|
||||
needRelayout = true;
|
||||
break;
|
||||
case CMD_FILE_SHOW_ALL:
|
||||
case CMD_GROUP_SHOW_ALL:
|
||||
if (m_menuTargetFile >= 0)
|
||||
{
|
||||
m_tab->SetFileVisible(m_menuTargetFile, true);
|
||||
m_tab->SetGroupVisible(m_menuTargetFile, true);
|
||||
m_rowsDirty = true;
|
||||
needRelayout = true;
|
||||
}
|
||||
break;
|
||||
case CMD_FILE_HIDE_ALL:
|
||||
case CMD_GROUP_HIDE_ALL:
|
||||
if (m_menuTargetFile >= 0)
|
||||
{
|
||||
m_tab->SetFileVisible(m_menuTargetFile, false);
|
||||
m_tab->SetGroupVisible(m_menuTargetFile, false);
|
||||
m_rowsDirty = true;
|
||||
needRelayout = true;
|
||||
}
|
||||
break;
|
||||
case CMD_FILE_REMOVE:
|
||||
case CMD_GROUP_REMOVE:
|
||||
if (m_menuTargetFile >= 0 && m_host != nullptr)
|
||||
{
|
||||
PostMessageW(m_host, WM_APP + 1, static_cast<WPARAM>(m_menuTargetFile), 0);
|
||||
|
||||
Reference in new issue
Block a user