Files
Just-DataInspector/Utilities/Inc/DataTable.hpp
T
DeepSeek V4.1 Flash 30f853a361 MF4 ##CA 全字段/三维 + cc_type=8 区间枚举 + 稠密包络带线宽双向加粗
一、##CA 数组通道:从「按魔数猜」改成「按记录容量校验」
- 头字段全部按块长度做边界检查后读取:+0 ca_type / +1 ca_storage /
  +2 ndim(U16) / +4 flags(U32) / +8 ca_byte_offset_base(U16) /
  +10 ca_inval_bit_pos(U16) / +16 dim_size[](U64,步长 8),
  其后才是**可选**的 cycle_count[](U32)。
  ⚠️ 实测 CANape 写出的文件没有 cycle_count(块长度恰好到 dim_size 就结束),
  盲读会把下一个块头当数字(实测得到 1481909027)。
- 展开条件(任何一条不满足就原样保留,只显示元素 0,绝不猜):
  ca_storage == 0;ca_type <= 2(0 数组 / 1 缩放轴 / 2 区间轴,三者物理布局相同);
  cn_bit_count ∈ [1, 64];cycle_count 每维 <= 1(元素不被拆到多条记录);
  bit_offset + 元素数 × 位宽 <= cg_data_bytes × 8(真正的完整性校验)。
  规模护栏:元素数 <= 16384 且 元素数 × 记录数 <= 32M 单元格
  (取代原来的 count > 1024 魔数)。
- 元素名按 dim_size 从末维向前取余,1/2/3 维分别得到 [i] / [i][j] / [i][j][k]。
- MF4-Record.MF4 仍是 283 个信号(25 个 ##CA 全部通过新的容量校验,最大 7x9=63)。

二、cc_type = 8(值 -> 文本,区间表)补齐
- cc_val[] 交错存放 (下限, 上限),cc_ref[4 + i] 是第 i 段文本(最后一个引用是默认文本,不使用);
  cc_val_count 必须是偶数(奇数说明表残缺,整表丢弃);下限 > 上限时交换。
- SignalEnumText 新增 rangeMin / rangeMax / rangeTexts,并拆出 HasExact() / HasRanges();
  Find() 先查精确表(cc_type = 7)再查区间表(cc_type = 8,闭区间)。
- 界面没有改动:两种枚举共用同一出口,只在**光标取值标注**追加 " = <文本>",
  常规状态零变化。

三、修复:稠密包络带下改变线宽时竖直沿不变粗
- 根因:包络带每列只有 1 个 x 坐标,旧实现只在 top/bottom 各外扩半个线宽
  => 水平段会变粗,但方波竖直沿(上升/下降段)的横向宽度恒为 1 个像素列。
- 修法:EmitEnvelopeBand 新增 halfWidth 形参,先对上下包络做横向窗口膨胀
  (top 取窗口最小、bottom 取窗口最大,半径 ceil(halfWidth / 列距)),再纵向各外扩;
  膨胀只在同一段内生效,不会跨 NaN 断点连线。

验证(全离屏,不占用鼠标/键盘/屏幕)
- .preflight/convcheck 18/18:新增 3 维命名与取值、元素放不进记录时必须拒绝展开、
  cc_type = 8 的命中/未命中。
- 新增 .preflight/round5check 5/5:合成 20 万采样数字信号(必然走包络带),
  在网格中间行测量竖直沿横向像素宽度:线宽 1px -> 6px,10px -> 30px。
- A/B:临时把横向膨胀半径置 0(等价旧行为)=> 1px -> 2px、10px -> 2px 且如实 FAIL。
- 回归:check 137/137、editcheck 20/20、round3check 31/31、round4check 34/34;
  真实 MF4 信号数 283/8/18/24 不变;make release / make debug 零警告;
  TargetCheck.exe 808,448 字节(789.5 KiB)。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 165-170 条硬约束与文末本轮小节
(含「6 位有效数字」的口径说明)。
2026-10-20 14:29:49 +08:00

214 lines
7.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef __DATA_TABLE_HPP__
#define __DATA_TABLE_HPP__
#include <cmath>
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
namespace jdi
{
/**
* @brief 一列数值的「值 → 文本」枚举表(如 ASAM MDF v4 的 `cc_type = 7` / `8` 换算)。
*
* 它描述的是**信号本身的取值含义**(例如 `0 = Off`、`1 = On`),
* 与单位、注释、物理量纲无关,因此这里刻意命名为 `enumText` 而不是 `unit` / `label`。
*
* 支持两种表(可同时存在,界面只用来给取值追加文本,不改变数值):
* * **精确表** `values[] ↔ texts[]`(`cc_type = 7`,值 → 文本);
* * **区间表** `[rangeMin[i], rangeMax[i]] ↔ rangeTexts[i]`(`cc_type = 8`,值区间 → 文本)。
*
* 空表(`Empty()`)表示该列没有枚举信息,界面必须保持与以前完全一致的显示。
*/
struct SignalEnumText
{
/** @brief 枚举取值(与采样列同一量纲,通常是整数)。 */
std::vector<double> values;
/** @brief 与 `values` 一一对应的显示文本。 */
std::vector<std::wstring> texts;
/**
* @brief 区间枚举的下限(`cc_type = 8`,与 `rangeMax`/`rangeTexts` 一一对应)。
*
* ⚠️ 三个数组必须同长度;长度不一致时整张区间表视为无效(`Empty()` 为真),
* 绝不能把错位的区间套到取值上。
*/
std::vector<double> rangeMin;
/** @brief 区间枚举的上限(闭区间)。 */
std::vector<double> rangeMax;
/** @brief 与 `rangeMin`/`rangeMax` 一一对应的显示文本。 */
std::vector<std::wstring> rangeTexts;
/** @brief 精确表是否有效。 */
bool HasExact() const { return !values.empty() && texts.size() == values.size(); }
/** @brief 区间表是否有效(三个数组同长度且非空)。 */
bool HasRanges() const
{
return !rangeMin.empty() && rangeMax.size() == rangeMin.size() &&
rangeTexts.size() == rangeMin.size();
}
/** @brief 是否没有任何枚举信息。 */
bool Empty() const { return !HasExact() && !HasRanges(); }
/**
* @brief 按取值查找枚举文本。
*
* 先查**精确表**(枚举值优先),未命中再查**区间表**(闭区间)。
*
* @param [IN] value 取值(物理值,未叠加界面「调整」变换)。
* @return 命中返回对应文本指针,未命中返回 nullptr。
*/
const std::wstring* Find(double value) const
{
if (Empty() || !(value == value)) // NaN 永远不命中
{
return nullptr;
}
if (HasExact())
{
for (std::size_t index = 0; index < values.size(); ++index)
{
if (values[index] == value)
{
return &texts[index];
}
}
// 采样值按 double 保存,整数枚举在任意位宽下都是精确可表示的,
// 但个别工具会写 `1.0000001` 这类带噪声的换算表:再做一次四舍五入匹配。
const double rounded = std::nearbyint(value);
if (rounded != value)
{
for (std::size_t index = 0; index < values.size(); ++index)
{
if (std::nearbyint(values[index]) == rounded)
{
return &texts[index];
}
}
}
}
if (HasRanges())
{
for (std::size_t index = 0; index < rangeMin.size(); ++index)
{
if (value >= rangeMin[index] && value <= rangeMax[index])
{
return &rangeTexts[index];
}
}
}
return nullptr;
}
/** @brief 枚举项总数(精确表 + 区间表,供诊断/日志使用)。 */
std::size_t Count() const
{
return (HasExact() ? values.size() : 0u) + (HasRanges() ? rangeMin.size() : 0u);
}
};
/**
* @brief 内存中的**列优先**数据表(与具体文件格式无关)。
*
* 这是「从任意格式读出来的原始格子」:一行是表头,其余是各列的数值。
* 它**不是**应用的数据模型(不含时间轴语义、不含信号名规范化),
* 只是各格式加载器(CSV / MF4 / 以后更多格式)与数据模型之间的中转结构。
*
* 采用列优先(每列一整块连续内存)而不是行优先的二维数组,原因有二:
* 1. 大型文件(百万行级)若按行存储会产生上百万次独立堆分配,
* 每行还要额外承担 vector 的控制块与容量对齐开销,实测内存放大接近 1.5 倍;
* 2. 构建信号时可以直接把整列**移动**进 `SignalData`,不再逐元素拷贝。
*/
struct DataTable
{
std::vector<std::wstring> headers; /**< 列名(已去除引号与首尾空白)。 */
std::vector<std::vector<double>> columns; /**< 列优先数值矩阵:columns[列][行],缺失处为 NaN。 */
/**
* @brief 与 `columns` 同下标的「值 → 文本」枚举表(没有枚举信息的列为空)。
*
* 加载器只在源格式自带枚举文本(如 MDF4 `cc_type = 7`)时填充;
* 它不是单位、不是注释,界面只在光标读数处以尾缀形式展示。
*/
std::vector<SignalEnumText> columnEnumText;
std::size_t rowCount = 0; /**< 有效数据行数。 */
wchar_t delimiter = L','; /**< 实际使用的分隔符(仅文本格式有意义)。 */
unsigned long long sourceBytes = 0; /**< 源文件字节数(用于按比例预留容量)。 */
/**
* @brief 判断解析结果是否包含可用的表格数据。
* @return 存在列名且存在数据行时返回 true。
*/
bool HasData() const { return !headers.empty() && (rowCount > 0) && (columns.size() >= headers.size()); }
};
/**
* @brief 加载进度信息。
*
* 解析阶段会携带真实的"已读 / 总字节数";列重组与统计阶段没有字节可计,
* 此时 `hasBytes` 为 false,界面应只展示百分比。
*/
struct LoadProgressInfo
{
double ratio = 0.0; /**< 0-1 总体完成比例。 */
unsigned long long bytesRead = 0; /**< 已读取字节数。 */
unsigned long long totalBytes = 0; /**< 文件总字节数。 */
bool hasBytes = false; /**< 上述字节数字段是否有效。 */
};
/**
* @brief 加载进度回调。
*
* 在解析过程中按块回调;返回 false 表示请求取消,解析会尽快停止并返回失败。
*/
using LoadProgress = std::function<bool(const LoadProgressInfo& info)>;
/**
* @brief 一组**共享同一时间轴**的数据列(一个「时间轴块」)。
*
* 一个源文件可以包含多个这样的块:CSV 里每个 `time` 列都会开启一个新块,
* MF4 里每个测量通道组(CG)是一个块。块内的各列长度一致(都等于本块的行数)。
*/
struct TableBlock
{
std::wstring timeHeader; /**< 本块时间列的列名(原样保留,如 `time`)。 */
DataTable table; /**< 本块的列集合(**含**时间列本身)。 */
};
/**
* @brief 一个源文件加载后的全部内容(可能包含多个时间轴块)。
*
* ⚠️ 这是「加载器 → 数据模型」的唯一接口:加载器只负责把文件读成
* `LoadedSource`(纯格子,无应用语义),`BuildSignalGroup` 再把它规范化为
* `SignalGroup`(含时间轴去重、信号命名、调色板等应用语义)。
* 新增文件格式时**只需新增一个加载器**,数据模型与界面代码完全不用改。
*/
struct LoadedSource
{
std::wstring sourceName; /**< 源文件名(不含目录,含扩展名)。 */
std::vector<TableBlock> blocks; /**< 时间轴块集合(至少一个)。 */
/** @brief 是否包含可用的数据。 */
bool HasData() const
{
for (const TableBlock& block : blocks)
{
if (block.table.HasData())
{
return true;
}
}
return false;
}
};
} // namespace jdi
#endif // __DATA_TABLE_HPP__