数据层大重构:文件→分组 / 时间轴一等公民 / 加载器抽象 + 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

+20 -19
View File
@@ -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; /**< 时间轴显示上界。 */