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:
DeepSeek V4.1 Flash committed 2026-10-20 14:29:49 +08:00
1 parent b315201598
commit 30f853a361
5 files changed
+460 -45

No files matched your search

+52 -3
View File
@@ -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));
}
}
+10 -5
View File
@@ -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)