数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + 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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user