数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + 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:
DeepSeek V4.1 Flash committed 2026-10-15 03:48:58 +08:00
1 parent fba40ee1a2
commit 6eedbe3c5d
18 files changed
+2545 -817

No files matched your search

+125 -119
View File
@@ -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,