From 30f853a3614cf5b63bf7cd1f85ba0434b10a932a Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Tue, 20 Oct 2026 14:29:49 +0800 Subject: [PATCH] =?UTF-8?q?MF4=20##CA=20=E5=85=A8=E5=AD=97=E6=AE=B5/?= =?UTF-8?q?=E4=B8=89=E7=BB=B4=20+=20cc=5Ftype=3D8=20=E5=8C=BA=E9=97=B4?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=20+=20=E7=A8=A0=E5=AF=86=E5=8C=85=E7=BB=9C?= =?UTF-8?q?=E5=B8=A6=E7=BA=BF=E5=AE=BD=E5=8F=8C=E5=90=91=E5=8A=A0=E7=B2=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、##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 位有效数字」的口径说明)。 --- Components/PlotView/Src/PlotViewInternal.hpp | 55 ++++- Components/PlotView/Src/PlotViewPaint.cpp | 15 +- Documents/agent_mem.md | 116 +++++++++ Utilities/Inc/DataTable.hpp | 81 +++++-- Utilities/Src/Mf4Loader.cpp | 238 +++++++++++++++++-- 5 files changed, 460 insertions(+), 45 deletions(-) diff --git a/Components/PlotView/Src/PlotViewInternal.hpp b/Components/PlotView/Src/PlotViewInternal.hpp index 30f7e4a..9c85c6b 100644 --- a/Components/PlotView/Src/PlotViewInternal.hpp +++ b/Components/PlotView/Src/PlotViewInternal.hpp @@ -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& columns, std::vector& band, - std::vector& bandBreaks, bool& spiky, float laneHeight) + std::vector& 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 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::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& 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)); } } diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index 07e93c6..c7f7b84 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -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 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(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) diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 3e04a27..c352b4f 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -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` 的 `` 文本里(如 `(单位:MV )`、`(单位:0.1*N-700℃)`)与 CANape `` 区间串里,故本轮不读单位。验证:新增离屏 `.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)。 + + diff --git a/Utilities/Inc/DataTable.hpp b/Utilities/Inc/DataTable.hpp index 774b29e..d3b2b41 100644 --- a/Utilities/Inc/DataTable.hpp +++ b/Utilities/Inc/DataTable.hpp @@ -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 texts; + /** + * @brief 区间枚举的下限(`cc_type = 8`,与 `rangeMax`/`rangeTexts` 一一对应)。 + * + * ⚠️ 三个数组必须同长度;长度不一致时整张区间表视为无效(`Empty()` 为真), + * 绝不能把错位的区间套到取值上。 + */ + std::vector rangeMin; + + /** @brief 区间枚举的上限(闭区间)。 */ + std::vector rangeMax; + + /** @brief 与 `rangeMin`/`rangeMax` 一一对应的显示文本。 */ + std::vector 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); + } }; /** diff --git a/Utilities/Src/Mf4Loader.cpp b/Utilities/Src/Mf4Loader.cpp index 1d99b90..9b22fe9 100644 --- a/Utilities/Src/Mf4Loader.cpp +++ b/Utilities/Src/Mf4Loader.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include 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 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 arrayDims; + /** + * @brief `##CA` 各维的周期数(`ca_cycle_count`);空 = 文件没写这个字段。 + * + * 规范里 `ca_cycle_count[i] > 1` 表示该维的元素被**拆到多条记录**里, + * 这时单个记录内并不含全部元素 ⇒ 不能按「记录内连续」展开(见 `ExpandArrayChannel`)。 + * ⚠️ 实测文件里这个字段**不存在**(`##CA` 块长度恰好到 `dim_size[]` 就结束), + * 因此读取前必须先按块长度做边界检查,不能盲读(盲读会拿到下一个块头当作数字)。 + */ + std::vector 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(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(file.U8(caData + 0)) : 0; + spec.arrayStorage = (caUsable >= 2) ? static_cast(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(axis) + 1u) > caUsable) + { + dimsOk = false; // 维度表被截断:不做任何猜测 + break; + } const std::size_t dim = static_cast(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(rank) + 4u * static_cast(axis); + if (cycleOffset + 4u > caUsable) + { + cyclesPresent = false; + break; + } + spec.arrayCycles.push_back(static_cast(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& out) + void ExpandArrayChannel(ChannelSpec&& spec, std::uint64_t cycles, std::size_t recordBytes, + std::vector& 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(cycles)) + { + expandable = false; + } + if (expandable && recordBytes > 0) + { + const std::uint64_t needBits = static_cast(spec.bitOffset) + + static_cast(count) * + static_cast(spec.bitCount); + if (needBits > static_cast(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 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 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 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(cgDataBytes), + channels); } return true; });