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 位有效数字」的口径说明)。
This commit is contained in:
1 parent
b315201598
commit
30f853a361
5 files changed
+460
-45
No files matched your search
@@ -807,14 +807,26 @@ namespace jdi
|
||||
* (用户反馈"显示效果没有之前好了")。性能问题改由**绘制侧**解决:把梳状带
|
||||
* 交给无抗锯齿填充(见 `spiky` 输出与 `PlotView::m_fastFrame`)。
|
||||
*
|
||||
* ⚠️⚠️ **但线宽必须同时在两个方向加粗**(用户 2026-10-20 报告:把线宽从 1px
|
||||
* 拖到 10px,图像上**竖直方向的沿**(方波的上升/下降段)宽度毫无变化):
|
||||
* 包络带每列只有 1 个 x,靠 `top/bottom` 各外扩只能让**水平段**变粗;竖直沿的
|
||||
* **横向**宽度始终是「1 个像素列」。正确做法是对上下包络做**横向膨胀**
|
||||
* (窗口内 `top` 取最小、`bottom` 取最大,等价于用一个边长 `2*halfWidth` 的方块
|
||||
* 做闵可夫斯基膨胀)——这样竖直沿也会被撑成 `2*halfWidth + 列宽` 宽。
|
||||
* ⚠️ 膨胀窗口**只在同一段内**生效(`segmentColumns` 按段清空),因此 NaN 断点处
|
||||
* 不会被"跨断点连线"填满。
|
||||
*
|
||||
* @param [IN] columns 逐列极值(同一段内按列顺序排列)。
|
||||
* @param [OUT] band 追加写入的包络顶点(上包络正向 + 下包络反向)。
|
||||
* @param [OUT] bandBreaks 追加写入的各段起点(相对 band 的下标)。
|
||||
* @param [OUT] spiky 该段是否为「梳状 / 密集满幅」(建议关闭抗锯齿)。
|
||||
* @param [IN] laneHeight 网格的可见像素高度(用于「覆盖率」判据;≤0 时跳过)。
|
||||
* @param [IN] halfWidth 线宽的一半(**设备像素**):上下包络各外扩这么多,
|
||||
* 并作为横向膨胀的半径;≤0 表示不做线宽补偿。
|
||||
*/
|
||||
inline void EmitEnvelopeBand(const std::vector<BandColumn>& columns, std::vector<gd::PointF>& band,
|
||||
std::vector<std::size_t>& bandBreaks, bool& spiky, float laneHeight)
|
||||
std::vector<std::size_t>& bandBreaks, bool& spiky, float laneHeight,
|
||||
float halfWidth = 0.0f)
|
||||
{
|
||||
if (columns.size() < 2)
|
||||
{
|
||||
@@ -877,13 +889,50 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ 线宽补偿(见函数头注释):
|
||||
// 1. 纵向:上下包络各外扩 `halfWidth`(水平段因此有厚度);
|
||||
// 2. 横向:对上下包络做窗口膨胀(`top` 取最小、`bottom` 取最大),
|
||||
// 竖直沿因此从「1 个像素列」变成「2*halfWidth + 列宽」。
|
||||
// 只在 `halfWidth` 有效时才做膨胀,避免给默认线宽引入额外的横向糊化。
|
||||
std::vector<BandColumn> widened;
|
||||
if (halfWidth > 0.0f && count > 1)
|
||||
{
|
||||
// 列间距由前两列推断(同一段内等宽等距);退化时按 1px 处理。
|
||||
const float spacing = columns[1].x - columns[0].x;
|
||||
const float step = (spacing > 0.01f) ? spacing : 1.0f;
|
||||
std::size_t radius = static_cast<std::size_t>(std::ceil(halfWidth / step));
|
||||
if (radius > 0 && radius * 2 + 1 < count)
|
||||
{
|
||||
widened.resize(count);
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const std::size_t first = (index > radius) ? (index - radius) : 0;
|
||||
const std::size_t last = std::min(count - 1, index + radius);
|
||||
float top = columns[index].top;
|
||||
float bottom = columns[index].bottom;
|
||||
for (std::size_t probe = first; probe <= last; ++probe)
|
||||
{
|
||||
top = std::min(top, columns[probe].top);
|
||||
bottom = std::max(bottom, columns[probe].bottom);
|
||||
}
|
||||
widened[index].x = columns[index].x;
|
||||
widened[index].top = top;
|
||||
widened[index].bottom = bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::vector<BandColumn>& shape = widened.empty() ? columns : widened;
|
||||
const bool expand = (halfWidth > 0.0f);
|
||||
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
band.push_back(gd::PointF(columns[index].x, columns[index].top));
|
||||
const float top = shape[index].top - (expand ? halfWidth : 0.0f);
|
||||
band.push_back(gd::PointF(shape[index].x, top));
|
||||
}
|
||||
for (std::size_t index = count; index-- > 0;)
|
||||
{
|
||||
band.push_back(gd::PointF(columns[index].x, columns[index].bottom));
|
||||
const float bottom = shape[index].bottom + (expand ? halfWidth : 0.0f);
|
||||
band.push_back(gd::PointF(shape[index].x, bottom));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -394,13 +394,17 @@ namespace jdi
|
||||
// (1224 列:14.3ms → 1.7ms),且**保留抗锯齿**,
|
||||
// 因此静态帧与拖拽帧的观感完全一致(不是"交互降级")。
|
||||
bandMode = true;
|
||||
// 线宽(设备像素)的一半:包络带在**纵/横两个方向**各外扩这么多。
|
||||
// ⚠️ 旧实现只在每列上下各外扩 —— 水平段会变粗,但方波的**竖直沿**横向宽度始终是
|
||||
// 1 个像素列,于是用户把线宽从 1px 拖到 10px 时"竖直方向的沿没有变粗"(2026-10-20 报告)。
|
||||
// 现在把线宽交给 `EmitEnvelopeBand`,由它对上下包络做横向膨胀。
|
||||
const float strokeHalf =
|
||||
std::max(1.0f, resolved.style->lineWidth) * m_scale * 0.5f;
|
||||
std::vector<BandColumn> segmentColumns;
|
||||
const auto flushBandSegment = [&band, &bandBreaks, &segmentColumns, &bandSpiky, &lane]()
|
||||
const auto flushBandSegment = [&band, &bandBreaks, &segmentColumns, &bandSpiky, &lane, strokeHalf]()
|
||||
{
|
||||
bool spiky = false;
|
||||
EmitEnvelopeBand(segmentColumns, band, bandBreaks, spiky, lane.rect.Height);
|
||||
EmitEnvelopeBand(segmentColumns, band, bandBreaks, spiky, lane.rect.Height, strokeHalf);
|
||||
bandSpiky = bandSpiky || spiky;
|
||||
segmentColumns.clear();
|
||||
};
|
||||
@@ -575,11 +579,12 @@ namespace jdi
|
||||
const float yBottom =
|
||||
static_cast<float>(ClampDouble(ValueToY(low, lane), limitTop, limitBottom));
|
||||
|
||||
// 包络带每列都记录(上下各外扩半个线宽,保证细波动也有可见粗细)。
|
||||
// 包络带每列只记录**精确**的上下包络(线宽在 `EmitEnvelopeBand` 里统一补偿:
|
||||
// 纵向各外扩半个线宽 + 横向窗口膨胀,两个方向都随线宽变化)。
|
||||
BandColumn bandColumn;
|
||||
bandColumn.x = x;
|
||||
bandColumn.top = yTop - strokeHalf;
|
||||
bandColumn.bottom = yBottom + strokeHalf;
|
||||
bandColumn.top = yTop;
|
||||
bandColumn.bottom = yBottom;
|
||||
segmentColumns.push_back(bandColumn);
|
||||
|
||||
if (yBottom - yTop < kEnvelopeMinPixelSpan)
|
||||
|
||||
@@ -3005,6 +3005,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
||||
|
||||
| 2026-10-21 | DeepSeek-V4.1-Flash(**本机**) | **MF4 数值换算类型 2/3/4/5/6 + `##CA` 数组通道完整支持 + 数值型 VLSD(详见 §12 末尾「2026-10-21」与 §11.2 第 160–164 条)**。① **换算**:`ChannelSpec` 把 `linearConversion/offset/factor` 换成 `conversion`(枚举 0–8)+ `conversionValues[]`,新增 `ApplyConversion`:`1` = `v0+v1*raw`、`2` = `(v0+v1*raw)/(v2+v3*raw)`、`3` = `(v0*raw²+v1*raw+v2)/(v3*raw²+v4*raw+v5)`、`4` = 交错 `(键,值)` 对线性插值(端点夹紧)、`5` = 同表阶梯取值、`6` = `(下限,上限,物理值)` 三元组区间映射;**参数个数不够或格式不符一律原样返回原始值,绝不猜**。② **`##CA` 数组**:CN 链接[1] → `ca_type(+0)/ca_storage(+1)/ndim(U16,+2)/flags(U32,+4)/dim_size[](U64,**+16 起、步长 8**,实测 Simulink 与 CANape 一致)`;`ExpandArrayChannel` 只在 `ca_storage == 0`(元素在记录内首尾相连)、`1 < 元素数 ≤ 1024`、`bit_count != 0` 时展开成多列,名字 `name[i]` / `name[i][j]`,复用同一换算与枚举。`MF4-Record.MF4` 信号数 **124 → 283**(9 点特性曲线 → 6000…14000;二维 map → 175/120/180…)。③ **VLSD**:数值型不再要求 `bit_count > 0`(新增 `InferVlsdElementBits` 按长度整除推 64/32/16/8),`DecodeVlsdValue` 只按一个元素做越界检查并取样本首个元素;主通道候选排除 VLSD。实测 4 个 MF4 文件的 VLSD 通道只有 `Comment`(字符串、`cycles = 0`),**数值型 VLSD 数量为 0** ⇒ 本次不改变现有输出。④ **单位**:4 个真实文件的 `cn_md_unit` **全部为空链接**;单位只出现在 `cn_md_comment` 的 `<TX>` 文本里(如 `(单位:MV )`、`(单位:0.1*N-700℃)`)与 CANape `<display>` 区间串里,故本轮不读单位。验证:新增离屏 `.preflight/convcheck`(**现场合成**一个 1 DG / 2 记录的 MDF4,覆盖 cc_type 1–6、`##CA`、数值 VLSD)**11/11**;**A/B 禁用新换算 + 数组展开 ⇒ 8 项如实失败**;回归 `check` 137/137、`editcheck` 20/20、`round3check` 31/31、`round4check` 34/34,真实文件信号数 283 / 8 / 18 / 24 不变;`make release`/`make debug` 零警告;`TargetCheck.exe` **804,352 字节**(785.5 KiB)。 |
|
||||
|
||||
| 2026-10-20 | DeepSeek-V4.1-Flash(**本机**) | **MF4 `##CA` 全字段(`ca_type`/`cycle_count`/`byte_offset_base`)+ 3 维命名 + 记录容量校验;`cc_type = 8` 区间枚举;稠密包络带的线宽改为「纵横双向加粗」(详见 §12 末尾「2026-10-20 第五轮」与 §11.2 第 165–170 条)**。① **`##CA`**:`ce_type`/`ca_storage`/`ndim`/`flags`/`ca_byte_offset_base`/`ca_inval_bit_pos`/`dim_size[]`/`cycle_count[]` 全部按**块长度做边界检查**后读取(`cycle_count[]` 实测缺失,盲读会拿到下一个块头 1481909027);展开条件从「数一个魔数上限」改为**记录容量校验** `bit_offset + 元素数×位宽 ≤ cg_data_bytes×8` + `cycle_count 每维 ≤ 1` + `ca_type ≤ 2`;规模护栏改为「元素数 ≤ 16384 且 元素数×记录数 ≤ 32M 单元格」;名字按 `dim_size[]` 逐维取余 ⇒ 1/2/3 维分别得到 `[i]`/`[i][j]`/`[i][j][k]`。② **`cc_type = 8`**(值区间→文本):`cc_val` 交错 `(下限,上限)`、`cc_ref[4+i]` 为第 i 段文本,`SignalEnumText` 新增 `rangeMin/rangeMax/rangeTexts` + `HasRanges()/Count()`,`Find` 先精确表后区间表;**仍然只在光标取值标注里追加 ` = <文本>`**。③ **线宽**:包络带旧实现只在每列**纵向**外扩 `strokeHalf` ⇒ 方波的竖直沿横向宽度始终是 1 个像素列(用户把线宽 1px 拖到 10px 时竖直沿完全不变);`EmitEnvelopeBand` 新增 `halfWidth` 形参,对上下包络做**横向窗口膨胀**(`top` 取窗口最小、`bottom` 取窗口最大)后再纵向各外扩,两个方向都随线宽变化。验证:`convcheck` **18/18**(新增 3 维命名、放不进记录的数组必须拒绝展开、区间枚举命中/未命中);新增 `.preflight/round5check` **5/5**(合成 20 万采样数字信号 ⇒ 包络带,中间行竖直沿墨迹 1px→6px、10px→30px);**A/B 关闭横向膨胀 ⇒ 1px→2px、10px→2px 并如实失败**;回归 `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)。 |
|
||||
|
||||
## 11. 跨设备协作须知(起源设备视角)
|
||||
|
||||
> **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。
|
||||
@@ -3796,6 +3798,39 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ }
|
||||
判定数值型时只要求 `cn_data_type` 是数值类型,不能再要求位宽(旧实现会让整列被丢弃)。
|
||||
164. ⚠️ **VLSD 通道不得作为主通道(时间轴)候选**:其记录里存的是 `##SD` 偏移,不是物理量。
|
||||
`SelectMasterChannel` 的顺序是「`cn_sync_type` 命中**且**数值**且**非 VLSD」→ 名字命中 → 首个数值通道。
|
||||
165. ⚠️⚠️ **`##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 × ndim)`,
|
||||
随后才是 **可选**的 `cycle_count[](U32 × ndim)`。
|
||||
⚠️ 实测 CANape 写出的文件**没有** `cycle_count[]`(块长度恰好到 `dim_size[]` 结束)⇒
|
||||
盲读会读到下一个块头(实测得到 1481909027),必须用 `BlockLength - (data - block)` 判断。
|
||||
166. ⚠️⚠️ **`##CA` 能否展开由「记录容量」决定,不再用元素数魔数**:
|
||||
`bit_offset + 元素数 × 位宽 ≤ cg_data_bytes × 8` 是真正的完整性校验(元素是否真的都在本记录内);
|
||||
再加上 `ca_storage == 0`、`ca_type ∈ {0,1,2}`、`cn_bit_count ∈ [1,64]`、`cycle_count 每维 ≤ 1`。
|
||||
规模护栏:元素数 ≤ **16384** 且 `元素数 × 记录数 ≤ 32M` 单元格(防止畸形巨型数组把列表/网格撑爆)。
|
||||
⚠️ `ca_type` 0/1/2 的**物理布局相同**(实测 25 个块全部是记录内连续),差别只在语义
|
||||
(0 数组 / 1 缩放轴 / 2 区间轴)——所以三者都展开;`ca_type > 2` 视为未知,不展开。
|
||||
167. ⚠️ **`##CA` 元素名必须按 `dim_size[]` 逐维取余**:从末维向前 `index % dim_i` 再 `/= dim_i`,
|
||||
因此 1/2/3 维分别得到 `name[i]` / `name[i][j]` / `name[i][j][k]`。
|
||||
⚠️ 旧实现只对「2 维」特判,3 维会退化成平面下标 `name[k]`(丢掉维度信息)。
|
||||
168. ⚠️⚠️ **MF4 `cc_type = 8`(值区间 → 文本)**:`cc_val[]` **交错**存放 `(下限, 上限)`,
|
||||
`cc_ref[4 + i]` 是第 i 段的文本,**最后一个引用是默认文本**(不使用)。
|
||||
`cc_val_count` 必须是偶数(奇数说明表残缺,整表丢弃);`下限 > 上限` 时交换。
|
||||
解析结果进 `SignalEnumText::rangeMin/rangeMax/rangeTexts`,
|
||||
**只在光标取值标注**里追加 ` = <文本>`(与 `cc_type = 7` 同一出口,常规状态零变化)。
|
||||
169. ⚠️⚠️ **稠密包络带的线宽必须「纵横双向」加粗**(用户 2026-10-20 报告:线宽 1px→10px 时
|
||||
方波的**竖直沿**宽度完全不变):包络带每列只有 1 个 x,`top/bottom` 各外扩只能让**水平段**变粗,
|
||||
竖直沿的横向宽度永远是「1 个像素列」。`EmitEnvelopeBand` 必须再做**横向窗口膨胀**
|
||||
(`top` 取窗口内最小、`bottom` 取窗口内最大,窗口半径 = `ceil(halfWidth / 列距)`)——
|
||||
等价于用边长 `2*halfWidth` 的方块做闵可夫斯基膨胀,竖直沿因此变成 `2*halfWidth + 列宽` 宽。
|
||||
⚠️ 膨胀**只在同一段内**(`segmentColumns` 按段清空),跨 NaN 断点不得连线。
|
||||
⚠️ 线宽本来就在 `PolylineCacheEntry` 的键里(2026-10-16 修过),所以这里不是缓存问题。
|
||||
⚠️ 像素级验证时**不能用「隐藏信号的参考帧」做差**:隐掉唯一信号会改变网格幅值范围,
|
||||
网格线与 Y 轴刻度跟着变,差分会把网格也算成波形(实测虚高到 57px)。直接匹配信号色。
|
||||
170. ℹ️ **数值显示精度是 6 位有效数字**(`FormatValue` = `%.6g`),**不是**「小数点后 6 位」:
|
||||
`123.456789 → 123.457`,`123.456789 ± 5e-7` 的两个值格式化后完全相同(波动在第 9 位有效数字上),
|
||||
且指数 < -4 或 ≥ 6 时切成科学计数法(`0.0000123456 → 1.23456e-05`)。
|
||||
想看到微小波动必须改显示格式(本轮未改,属显示策略而非数据丢失)。
|
||||
|
||||
### 11.3 离屏验证方法学(本机偏好,强烈建议沿用)
|
||||
|
||||
@@ -6822,3 +6857,84 @@ DG3 data=0 cycle=0 (空组,一个字符串通道)
|
||||
- `make release` / `make debug` **零警告**;`TargetCheck.exe` **804,352 字节**(785.5 KiB,仍在 800KiB 以内)。
|
||||
- ⚠️ 复测探针时**不要覆盖正在运行的 `Target.exe`**:统一用 `make release TARGET_FILE_NAME=TargetCheck`。
|
||||
|
||||
---
|
||||
|
||||
## 本轮(2026-10-20 第五轮,本机):`##CA` 全字段 + 3 维命名 + `cc_type = 8` 区间枚举 + 包络带线宽双向加粗
|
||||
|
||||
> 用户要求:① 处理前面报告里的「护栏改成按单元格数」「读 `ca_type`」以及「真正的待办」三条
|
||||
> (`ca_type` / `ca_cycle_count` / `ca_byte_offset_base`)和「ndim 支持三维」;② 若范围枚举文本映射
|
||||
> (`cc_type = 8`)没实现就补上,且同样只在光标模式下显示;③ 修「改变线宽但图像上纵向的绘制线宽不变」;
|
||||
> ④ 先回答「6 位有效数字」到底指什么;⑤ 完成后列举 MF4 特性与支持程度并提交。
|
||||
> 全程离屏(合成文件 + 数据层断言 + 离屏像素探针),不占用用户的鼠标 / 键盘 / 屏幕。
|
||||
|
||||
### A. `##CA`:从「按魔数猜」改成「按记录容量校验」
|
||||
|
||||
- **三个待办字段的结论**(都用 `.preflight/mf4catype.ps1` 扫过 4 个真实文件):
|
||||
- `ca_type`:**实测 0 / 1 / 2 都存在**,但三者**物理布局完全相同**(元素在记录内按 `cn_bit_count`
|
||||
连续排布,`cg_data_bytes` 恰好等于 `头 + 元素数 × 位宽`)⇒ 语义不同、存储同构,
|
||||
因此 0/1/2 **都展开**,`> 2`(未知)才不展开。
|
||||
- `ca_cycle_count`:**实测不存在**(`##CA` 块长度到 `dim_size[]` 就结束)。盲读会拿到
|
||||
下一个块头当数字(实测 1481909027)⇒ 必须先按块长度判断「读得到」才读。
|
||||
真读到时只要任一维 `> 1`(元素被拆到多条记录),就**不展开**。
|
||||
- `ca_byte_offset_base`:实测值 2(= uint16 的字节宽度,巧合),**不用于定位**;
|
||||
只记录进 `ChannelSpec` 供诊断(定位仍用 `cn_byte_offset`,已被真实数据验证)。
|
||||
- **展开条件重写**(`ExpandArrayChannel(spec, cycles, recordBytes, out)`):
|
||||
`ca_storage == 0`、`ca_type ≤ 2`、`bit_count ∈ [1,64]`、`cycle_count 每维 ≤ 1`、
|
||||
**`bit_offset + 元素数 × 位宽 ≤ recordBytes × 8`**(真正的完整性校验)、
|
||||
元素数 ≤ 16384、`元素数 × 记录数 ≤ 32M`。
|
||||
旧实现的 `count > 1024` 魔数与「`bitCount == 0` 就放弃」被上面这些**有依据**的判据取代。
|
||||
- **3 维命名**:按 `dim_size[]` 从末维向前取余,1/2/3 维分别得到 `[i]` / `[i][j]` / `[i][j][k]`。
|
||||
- 真实文件回归:`MF4-Record.MF4` 仍是 **283** 个信号(25 个 `##CA` 全部通过新的容量校验,
|
||||
最大 7×9=63),其余三个文件 8 / 18 / 24 不变。
|
||||
|
||||
### B. `cc_type = 8`(值区间 → 文本):补齐
|
||||
|
||||
- `SignalEnumText` 新增 `rangeMin / rangeMax / rangeTexts`,并拆出 `HasExact()` / `HasRanges()`:
|
||||
`Empty()` = 两者都无效;`Find(value)` **先精确表、后区间表**(闭区间);`Count()` 供诊断。
|
||||
- `ParseChannel` 新增 `VALUE_RANGE_TO_TEXT` 分支:`cc_val[]` 交错 `(下限, 上限)`,
|
||||
`cc_ref[4 + i]` 是第 i 段文本(**最后一个引用是默认文本,不使用**);
|
||||
`cc_val_count` 必须为偶数(奇数 = 表残缺 ⇒ 整表丢弃);`下限 > 上限` 时交换;非有限值跳过。
|
||||
- 界面**没有改动**:`cc_type = 7` 与 `8` 共用同一个出口(光标取值标注追加 ` = <文本>`),
|
||||
因此「只在光标模式下显示」这条要求自动成立,常规状态零变化。
|
||||
|
||||
### C. 线宽:包络带必须纵横双向加粗(用户报告的缺陷)
|
||||
|
||||
- **根因(代码级确认)**:`bandColumn.top = yTop - strokeHalf; bottom = yBottom + strokeHalf;`
|
||||
只在**纵向**外扩。包络带每列只有 1 个 x 坐标,填充多边形的横向跨度就是「1 个像素列」⇒
|
||||
**方波的竖直沿(上升/下降段)横向宽度恒为 1px**,与线宽无关。用户看到的现象正是
|
||||
「水平段变粗了,竖直方向的沿一点没变」。
|
||||
⚠️ 不是缓存问题:`lineWidth` 早在 2026-10-16 就进了 `PolylineCacheEntry` 的键。
|
||||
- **修法**:`EmitEnvelopeBand(..., float halfWidth)`:
|
||||
1. **横向**:对上下包络做**窗口膨胀**(`top` 取窗口最小、`bottom` 取窗口最大,
|
||||
半径 `ceil(halfWidth / 列距)`)——等价于用边长 `2*halfWidth` 的方块做闵可夫斯基膨胀,
|
||||
竖直沿因此变成 `2*halfWidth + 列宽` 宽;
|
||||
2. **纵向**:再各外扩 `halfWidth`(水平段厚度);
|
||||
3. 膨胀**只在同一段内**(`segmentColumns` 按段清空),不会跨 NaN 断点连线。
|
||||
调用方不再自己给列加 `strokeHalf`(列里存**精确**包络)。
|
||||
|
||||
### D. 关于「6 位有效数字」
|
||||
|
||||
- `FormatValue`(`Utilities/Src/Utf8Util.cpp`)用 `%.6g`:是**总共 6 位有效数字**,
|
||||
**不是**小数点后 6 位。`123.456789 → 123.457`;`123.456789 ± 5e-7` 的两个值格式化后**完全相同**
|
||||
(波动在第 9 位有效数字上);`%.6g` 在指数 < -4 或 ≥ 6 时切成科学计数法。
|
||||
- 这只是**显示格式**,数据本身是完整 double(绘图、边沿统计、派生信号都用原值)。
|
||||
若要观察微小波动需要改显示策略(本轮按用户要求**未改**)。
|
||||
|
||||
### E. 验证
|
||||
|
||||
- `.preflight/convcheck`(合成 MDF4,recordBytes 72 → 128):**18/18**
|
||||
- 新增:`##CA` 3 维 → 8 个 `caCube[i][j][k]` 且 `[1][1][1]` 取值 8/18;
|
||||
故意放到 `byteOffset = 124` 的 4 元素数组(需要 1056 位 > 1024 位)**必须不展开**;
|
||||
`cc_type = 8` 两段区间 → `5.0 → LOW`、`15.0 → MID`、`25.0 → 无文本`。
|
||||
- 新增 `.preflight/round5check`(离屏像素探针):合成 20 万采样「平坦 + 短脉冲」数字信号
|
||||
⇒ 必然走稠密包络带;在网格中间行统计**接近信号色**的像素数(= 竖直沿横向宽度):
|
||||
**1px → 6px、10px → 30px**,**5/5 通过**。
|
||||
- ⚠️ 像素统计**不能用「隐藏信号的参考帧」做差**:隐掉唯一信号会改变网格幅值范围,
|
||||
网格线与 Y 轴刻度跟着变,差分会把网格也算成波形(实测虚高到 57px)。
|
||||
- **A/B**:临时令膨胀半径 `radius = 0`(只保留纵向外扩 = 旧行为)⇒ `1px → 2px、10px → 2px`
|
||||
且「thickens the vertical stroke」如实 FAIL;恢复后 5/5。
|
||||
- 回归:`check` 137/137、`editcheck` 20/20、`round3check` 31/31、`round4check` 34/34;
|
||||
真实 MF4 信号数 283 / 8 / 18 / 24;枚举表仍解析出 32 / 12 / 5 / 32 项。
|
||||
- `make release` / `make debug` 零警告;`TargetCheck.exe` **808,448 字节**(789.5 KiB,仍 < 800KB)。
|
||||
|
||||
|
||||
+65
-16
@@ -11,12 +11,16 @@ namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 一列数值的「值 → 文本」枚举表(如 ASAM MDF v4 的 `cc_type = 7` 换算)。
|
||||
* @brief 一列数值的「值 → 文本」枚举表(如 ASAM MDF v4 的 `cc_type = 7` / `8` 换算)。
|
||||
*
|
||||
* 它描述的是**信号本身的取值含义**(例如 `0 = Off`、`1 = On`),
|
||||
* 与单位、注释、物理量纲无关,因此这里刻意命名为 `enumText` 而不是 `unit` / `label`。
|
||||
*
|
||||
* 空表(`texts.empty()`)表示该列没有枚举信息,界面必须保持与以前完全一致的显示。
|
||||
* 支持两种表(可同时存在,界面只用来给取值追加文本,不改变数值):
|
||||
* * **精确表** `values[] ↔ texts[]`(`cc_type = 7`,值 → 文本);
|
||||
* * **区间表** `[rangeMin[i], rangeMax[i]] ↔ rangeTexts[i]`(`cc_type = 8`,值区间 → 文本)。
|
||||
*
|
||||
* 空表(`Empty()`)表示该列没有枚举信息,界面必须保持与以前完全一致的显示。
|
||||
*/
|
||||
struct SignalEnumText
|
||||
{
|
||||
@@ -26,11 +30,38 @@ namespace jdi
|
||||
/** @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 values.empty() || texts.size() != values.size(); }
|
||||
bool Empty() const { return !HasExact() && !HasRanges(); }
|
||||
|
||||
/**
|
||||
* @brief 按取值查找枚举文本。
|
||||
*
|
||||
* 先查**精确表**(枚举值优先),未命中再查**区间表**(闭区间)。
|
||||
*
|
||||
* @param [IN] value 取值(物理值,未叠加界面「调整」变换)。
|
||||
* @return 命中返回对应文本指针,未命中返回 nullptr。
|
||||
*/
|
||||
@@ -40,29 +71,47 @@ namespace jdi
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (std::size_t index = 0; index < values.size(); ++index)
|
||||
if (HasExact())
|
||||
{
|
||||
if (values[index] == value)
|
||||
for (std::size_t index = 0; index < values.size(); ++index)
|
||||
{
|
||||
return &texts[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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 采样值按 double 保存,整数枚举在任意位宽下都是精确可表示的,
|
||||
// 但个别工具会写 `1.0000001` 这类带噪声的换算表:再做一次四舍五入匹配。
|
||||
const double rounded = std::nearbyint(value);
|
||||
if (rounded == value)
|
||||
if (HasRanges())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
for (std::size_t index = 0; index < values.size(); ++index)
|
||||
{
|
||||
if (std::nearbyint(values[index]) == rounded)
|
||||
for (std::size_t index = 0; index < rangeMin.size(); ++index)
|
||||
{
|
||||
return &texts[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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+217
-21
@@ -12,6 +12,7 @@
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace jdi
|
||||
@@ -61,7 +62,21 @@ namespace jdi
|
||||
TAB = 5, /**< 值→值查表(**不插值**,取阶梯值)。 */
|
||||
TAB_RANGE = 6, /**< 值区间→值(`[min,max] → phys`,区间外不换算)。 */
|
||||
VALUE_TO_TEXT = 7, /**< 值 → 枚举文本(`cc_val[i]` ↔ `cc_ref[i]` 文本)。 */
|
||||
VALUE_RANGE_TO_TEXT = 8 /**< 值区间 → 文本(未支持,按恒等处理)。 */
|
||||
VALUE_RANGE_TO_TEXT = 8 /**< 值区间 → 文本(`cc_val` 交错 `(下限,上限)` ↔ `cc_ref[i]` 文本)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief `##CA` 的数组类型(`ca_type`)。
|
||||
*
|
||||
* 三者**物理布局相同**(`ca_storage == 0` 时元素在本记录内按位宽连续排布,
|
||||
* 实测 CANape 写出的 25 个 `##CA` 块全部如此),差别只在语义:
|
||||
* `0` 普通数组 / `1` 缩放轴(特性曲线的断点) / `2` 区间轴(map 的一维断点)。
|
||||
*/
|
||||
enum class ArrayKind : int
|
||||
{
|
||||
ARRAY = 0, /**< 普通数值数组。 */
|
||||
SCALING_AXIS = 1, /**< 缩放轴(断点表)。 */
|
||||
INTERVAL_AXIS = 2 /**< 区间轴(每维断点成对给出)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -381,13 +396,29 @@ namespace jdi
|
||||
* 查表类(4/5)为交错的 `(raw, phys)` 点对,区间表(6)为 `(min, max, phys)` 三元组。
|
||||
*/
|
||||
std::vector<double> conversionValues;
|
||||
SignalEnumText enumText; /**< 值 → 文本枚举表(`cc_type == 7`,可能为空)。 */
|
||||
/** 值 → 文本枚举表(`cc_type == 7` 精确值 / `cc_type == 8` 值区间,可能为空)。 */
|
||||
SignalEnumText enumText;
|
||||
/** @brief `##CA` 数组元素总数(0 = 普通标量通道)。 */
|
||||
std::size_t arrayElements = 0;
|
||||
/** @brief `##CA` 存储方式(0 = 元素连续存放在本记录内)。 */
|
||||
int arrayStorage = 0;
|
||||
/** @brief `##CA` 数组类型(`ca_type`,见 `ArrayKind`)。 */
|
||||
int arrayType = 0;
|
||||
/** @brief `##CA` 各维长度(仅用于生成 `name[i][j]` 这样的名字)。 */
|
||||
std::vector<std::size_t> arrayDims;
|
||||
/**
|
||||
* @brief `##CA` 各维的周期数(`ca_cycle_count`);空 = 文件没写这个字段。
|
||||
*
|
||||
* 规范里 `ca_cycle_count[i] > 1` 表示该维的元素被**拆到多条记录**里,
|
||||
* 这时单个记录内并不含全部元素 ⇒ 不能按「记录内连续」展开(见 `ExpandArrayChannel`)。
|
||||
* ⚠️ 实测文件里这个字段**不存在**(`##CA` 块长度恰好到 `dim_size[]` 就结束),
|
||||
* 因此读取前必须先按块长度做边界检查,不能盲读(盲读会拿到下一个块头当作数字)。
|
||||
*/
|
||||
std::vector<std::size_t> arrayCycles;
|
||||
/** @brief `##CA` 的元素起始字节基址(`ca_byte_offset_base`,仅用于一致性校验与诊断)。 */
|
||||
std::size_t arrayByteOffsetBase = 0;
|
||||
/** @brief `##CA` 的无效位位置(`ca_inval_bit_pos`,仅记录用于诊断)。 */
|
||||
std::size_t arrayInvalidBitPos = 0;
|
||||
/** 数值有效性:只有整数与浮点通道可以绘制。 */
|
||||
bool numeric = false;
|
||||
};
|
||||
@@ -989,35 +1020,125 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ConversionType::VALUE_RANGE_TO_TEXT:
|
||||
// 值区间 → 文本:`cc_val[]` 交错存放 `(下限, 上限)`,`cc_ref[4 + i]` 是第 i 段的文本
|
||||
//(最后一个引用是「默认文本」,这里不使用)。
|
||||
// ⚠️ 区间**成对**出现在一个 1-D 表里:周期描述的是**一维断点表**的相邻输出值
|
||||
//(例如 0–10 → 'A'、10–20 → 'B'),故取相交的第一个区间。
|
||||
// ⚠️ 段数必须是 `cc_val_count / 2`(奇数个参数说明表是残缺的,整张表丢弃)。
|
||||
if (valCount >= 2 && (valCount % 2) == 0)
|
||||
{
|
||||
const std::uint64_t ccLinkCount = file.LinkCount(cc);
|
||||
const std::size_t ranges = static_cast<std::size_t>(valCount / 2);
|
||||
for (std::size_t index = 0; index < ranges; ++index)
|
||||
{
|
||||
const std::uint64_t textLink = 4u + index;
|
||||
if (textLink >= ccLinkCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
std::wstring text = file.TextBlock(file.Link(cc, textLink));
|
||||
if (text.empty())
|
||||
{
|
||||
continue; // 没有文本的区间不进入枚举表
|
||||
}
|
||||
double lower = file.F64(ccData + 24u + 16u * index);
|
||||
double upper = file.F64(ccData + 24u + 16u * index + 8u);
|
||||
if (!std::isfinite(lower) || !std::isfinite(upper))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (lower > upper)
|
||||
{
|
||||
std::swap(lower, upper);
|
||||
}
|
||||
spec.enumText.rangeMin.push_back(lower);
|
||||
spec.enumText.rangeMax.push_back(upper);
|
||||
spec.enumText.rangeTexts.push_back(std::move(text));
|
||||
}
|
||||
if (!spec.enumText.HasRanges())
|
||||
{
|
||||
spec.enumText = SignalEnumText();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// `##CA`(通道数组):一个通道装一整条曲线/矩阵(标定的特性曲线、map)。
|
||||
// 数据段布局(实测 CANape 写出的文件):type(1) + storage(1) + ndim(U16) + flags(U32)
|
||||
// + [8..15 保留] + dim_size[](U64 × ndim)。
|
||||
// 数据段布局(实测 CANape 写出的文件):
|
||||
// +0 ca_type(U8) / +1 ca_storage(U8) / +2 ndim(U16) / +4 flags(U32)
|
||||
// +8 ca_byte_offset_base(U16) / +10 ca_inval_bit_pos(U16)
|
||||
// +16 dim_size[](U64 × ndim) 然后紧接 cycle_count[](U32 × ndim,**可缺省**)
|
||||
// ⚠️ `cycle_count` 在实测文件里**不存在**(块长度到 `dim_size[]` 就结束)。
|
||||
// 必须按块长度做边界检查,否则会把下一个块头当成数字读进来(实测得到 1481909027)。
|
||||
const std::uint64_t ca = (linkCount > 1) ? file.Link(cn, 1) : 0;
|
||||
if (ca != 0 && file.IsBlock(ca, "##CA"))
|
||||
{
|
||||
const std::uint64_t caData = file.DataOffset(ca);
|
||||
spec.arrayStorage = file.U8(caData + 1);
|
||||
const std::uint16_t rank = file.U16(caData + 2);
|
||||
const std::uint64_t caBlockLength = file.BlockLength(ca);
|
||||
const std::uint64_t caUsable =
|
||||
(caBlockLength > (caData - ca)) ? (caBlockLength - (caData - ca)) : 0;
|
||||
spec.arrayType = (caUsable >= 1) ? static_cast<int>(file.U8(caData + 0)) : 0;
|
||||
spec.arrayStorage = (caUsable >= 2) ? static_cast<int>(file.U8(caData + 1)) : 0;
|
||||
const std::uint16_t rank = (caUsable >= 4) ? file.U16(caData + 2) : 0;
|
||||
if (caUsable >= 12)
|
||||
{
|
||||
spec.arrayByteOffsetBase = file.U16(caData + 8);
|
||||
spec.arrayInvalidBitPos = file.U16(caData + 10);
|
||||
}
|
||||
|
||||
std::size_t total = (rank > 0) ? 1 : 0;
|
||||
bool dimsOk = (rank > 0);
|
||||
spec.arrayDims.clear();
|
||||
spec.arrayCycles.clear();
|
||||
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
|
||||
{
|
||||
if (16u + 8u * (static_cast<std::uint64_t>(axis) + 1u) > caUsable)
|
||||
{
|
||||
dimsOk = false; // 维度表被截断:不做任何猜测
|
||||
break;
|
||||
}
|
||||
const std::size_t dim =
|
||||
static_cast<std::size_t>(file.U64(caData + 16u + 8u * axis));
|
||||
if (dim == 0)
|
||||
{
|
||||
total = 0;
|
||||
dimsOk = false;
|
||||
break;
|
||||
}
|
||||
spec.arrayDims.push_back(dim);
|
||||
total *= dim;
|
||||
// 元素总数太大时不再累乘(只用它判断「是否可展开」)。
|
||||
if (total <= (1u << 24))
|
||||
{
|
||||
total *= dim;
|
||||
if (total > (1u << 24))
|
||||
{
|
||||
total = (1u << 24) + 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
spec.arrayElements = total;
|
||||
|
||||
// `ca_cycle_count[]`(可缺省):只有真的读得到才记录。
|
||||
bool cyclesPresent = (rank > 0);
|
||||
for (std::uint16_t axis = 0; axis < rank && axis < 8; ++axis)
|
||||
{
|
||||
const std::uint64_t cycleOffset =
|
||||
16u + 8u * static_cast<std::uint64_t>(rank) + 4u * static_cast<std::uint64_t>(axis);
|
||||
if (cycleOffset + 4u > caUsable)
|
||||
{
|
||||
cyclesPresent = false;
|
||||
break;
|
||||
}
|
||||
spec.arrayCycles.push_back(static_cast<std::size_t>(file.U32(caData + cycleOffset)));
|
||||
}
|
||||
if (!cyclesPresent || spec.arrayCycles.size() != spec.arrayDims.size())
|
||||
{
|
||||
spec.arrayCycles.clear(); // 缺省 = 每维 1 个周期(整个数组在本记录内)
|
||||
}
|
||||
|
||||
spec.arrayElements = dimsOk ? total : 0;
|
||||
}
|
||||
|
||||
// 通道类型 2 = 虚拟主通道(CANape 的时间轴),同样可以取值。
|
||||
@@ -1042,20 +1163,81 @@ namespace jdi
|
||||
*(实测:`WIN_Motor_StallCurrentVolBrk` 的 9 个 `uint16` 就是从记录 +8 起连续 18 字节)。
|
||||
* 展开后每个元素都是普通标量信号,因此自动获得光标取值、枚举、图例等全部能力。
|
||||
*
|
||||
* ⚠️ 仅处理 `storage == 0`(记录内连续);`storage != 0`(元素分散在不同记录/
|
||||
* 不同通道组/不同数据组)保持原样(只显示元素 0),避免解析出错值。
|
||||
* ⚠️ 元素总数超过 `kMaxArrayElements` 时同样保持原样(防止巨型数组把信号列表撑爆)。
|
||||
* 展开的**必要条件**(任何一条不满足就原样保留,只显示元素 0,绝不猜):
|
||||
* 1. `ca_storage == 0`:元素在本记录内连续存放;
|
||||
* 2. `ca_type` ∈ {0 数组, 1 缩放轴, 2 区间轴}:三者物理布局相同(实测 25 个 `##CA` 块全部如此),
|
||||
* 未知类型(> 2)不展开;
|
||||
* 3. `cn_bit_count` ∈ [1, 64]:位宽是算元素步进的**唯一依据**,为 0 时不知道元素之间隔多少位
|
||||
* (VLSD 通道的 `bit_count` 常写 0,那类数据在 `##SD` 里,与"记录内连续"完全不是一套逻辑);
|
||||
* 4. `ca_cycle_count` 缺失或每一维都为 1:否则元素被拆到**多条记录**里,单个记录内不含全部元素;
|
||||
* 5. 元素能放进本记录:`bit_offset + 元素数 × 位宽 ≤ 记录位数`(这是真正的完整性校验,
|
||||
* 替代了「猜一个元素数上限」);
|
||||
* 6. 规模护栏(防止一个巨型数组把信号列表/网格撑爆):
|
||||
* 元素数 ≤ `kMaxArrayElements`,且 `元素数 × 记录数` ≤ `kMaxArrayCells`。
|
||||
*
|
||||
* @param [IN,OUT] spec 已解析的通道方案(不展开时原样平移进 `out`)。
|
||||
* @param [IN] cycles 该通道组的记录条数(`cg_cycle_count`,用于规模护栏)。
|
||||
* @param [IN] recordBytes 单条记录字节数(`cg_data_bytes`,0 = 未知则跳过第 5 条校验)。
|
||||
* @param [OUT] out 展开后的通道列表。
|
||||
*/
|
||||
void ExpandArrayChannel(ChannelSpec&& spec, std::vector<ChannelSpec>& out)
|
||||
void ExpandArrayChannel(ChannelSpec&& spec, std::uint64_t cycles, std::size_t recordBytes,
|
||||
std::vector<ChannelSpec>& out)
|
||||
{
|
||||
constexpr std::size_t kMaxArrayElements = 1024;
|
||||
/** 单个通道最多展开成多少条信号。 */
|
||||
constexpr std::size_t kMaxArrayElements = 16384;
|
||||
/** 元素总数 × 记录数的上限(≈ 生成的数据点数 × 8 字节的内存/绘制代价)。 */
|
||||
constexpr std::size_t kMaxArrayCells = 32u << 20;
|
||||
|
||||
const std::size_t count = spec.arrayElements;
|
||||
if (count <= 1 || spec.arrayStorage != 0 || count > kMaxArrayElements || spec.bitCount == 0)
|
||||
bool expandable = (count > 1);
|
||||
if (expandable && spec.arrayStorage != 0)
|
||||
{
|
||||
expandable = false; // 元素不在本记录内(跨通道/跨通道组)
|
||||
}
|
||||
if (expandable && (spec.arrayType < 0 || spec.arrayType > 2))
|
||||
{
|
||||
expandable = false; // 未知 `ca_type`
|
||||
}
|
||||
if (expandable && (spec.bitCount == 0 || spec.bitCount > 64))
|
||||
{
|
||||
expandable = false; // 位宽未知
|
||||
}
|
||||
if (expandable)
|
||||
{
|
||||
for (const std::size_t perAxis : spec.arrayCycles)
|
||||
{
|
||||
if (perAxis > 1)
|
||||
{
|
||||
expandable = false; // 元素被拆到多条记录
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expandable && count > kMaxArrayElements)
|
||||
{
|
||||
expandable = false;
|
||||
}
|
||||
if (expandable && cycles > 1 && count > kMaxArrayCells / static_cast<std::size_t>(cycles))
|
||||
{
|
||||
expandable = false;
|
||||
}
|
||||
if (expandable && recordBytes > 0)
|
||||
{
|
||||
const std::uint64_t needBits = static_cast<std::uint64_t>(spec.bitOffset) +
|
||||
static_cast<std::uint64_t>(count) *
|
||||
static_cast<std::uint64_t>(spec.bitCount);
|
||||
if (needBits > static_cast<std::uint64_t>(recordBytes) * 8u)
|
||||
{
|
||||
expandable = false; // 放不进本记录 ⇒ 元素位置不符合「记录内连续」
|
||||
}
|
||||
}
|
||||
if (!expandable)
|
||||
{
|
||||
spec.arrayElements = 0;
|
||||
out.push_back(std::move(spec));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t strideBits = spec.bitCount;
|
||||
const std::vector<std::size_t> dims = spec.arrayDims;
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
@@ -1063,11 +1245,24 @@ namespace jdi
|
||||
ChannelSpec element = spec; // 复制:换算表 / 枚举表 / 数据类型全部沿用
|
||||
element.arrayElements = 0;
|
||||
element.bitOffset = spec.bitOffset + index * strideBits;
|
||||
// 维度后缀:按 `dim_size[]` 从末维向前取余,因此 1/2/3/… 维都得到 `[i][j][k]`。
|
||||
std::wstring suffix;
|
||||
if (dims.size() == 2 && dims[1] > 0)
|
||||
if (dims.size() >= 2)
|
||||
{
|
||||
suffix = L"[" + std::to_wstring(index / dims[1]) + L"][" +
|
||||
std::to_wstring(index % dims[1]) + L"]";
|
||||
std::vector<std::size_t> axisIndex(dims.size(), 0);
|
||||
std::size_t remainder = index;
|
||||
for (std::size_t axis = dims.size(); axis-- > 0;)
|
||||
{
|
||||
const std::size_t size = dims[axis];
|
||||
axisIndex[axis] = (size > 0) ? (remainder % size) : 0;
|
||||
remainder = (size > 0) ? (remainder / size) : 0;
|
||||
}
|
||||
for (const std::size_t value : axisIndex)
|
||||
{
|
||||
suffix += L"[";
|
||||
suffix += std::to_wstring(value);
|
||||
suffix += L"]";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1646,6 +1841,7 @@ namespace jdi
|
||||
const std::uint32_t cgDataBytes = file.U32(cgData + 24);
|
||||
|
||||
// 解析通道链。
|
||||
// `##CA` 数组通道就地展开:需要 `cg_data_bytes`(单个记录容量)与 `cycles`(规模护栏)。
|
||||
std::vector<ChannelSpec> channels;
|
||||
WalkChain(file, file.Link(cg, 1), "##CN", kMaxChannelsPerGroup,
|
||||
[&](std::uint64_t cn)
|
||||
@@ -1653,9 +1849,9 @@ namespace jdi
|
||||
ChannelSpec spec;
|
||||
if (ParseChannel(file, cn, spec))
|
||||
{
|
||||
// `##CA` 数组通道在这里就地展开成多个标量通道
|
||||
//(一个通道装整条曲线时,旧实现只能看到元素 0)。
|
||||
ExpandArrayChannel(std::move(spec), channels);
|
||||
// 一个通道装整条曲线时,旧实现只能看到元素 0。
|
||||
ExpandArrayChannel(std::move(spec), cycles, static_cast<std::size_t>(cgDataBytes),
|
||||
channels);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user