Compare commits

...
10 Commits
Author SHA1 Message Date
DeepSeek V4.1 Flash 7344bbe9c4 修复长距离中键拖拽的绘制错误与抽搐("距离太远"不再退化成排队重画)
症状(用户报告 + TestData/PROBLEM_IMG):按住中键把视界纵向移动好几个网格高(或横向拖很远),
不松开再挪回来时出现大量绘制错误与持续抽搐。

根因:RollWideBuffer 对"位移超出图层余量"只做"排队等后续帧重画"(QueueLaneRebuild + continue):
  * 该帧 BlitWideBuffer 的 srcY = marginY + vOffset 已落在画布之外 → 只能贴出/贴不到像素
    → 网格里出现空白条带(截图里的白色块);
  * 下一帧位移依旧超限 → 每帧都重新入队,而 RenderPendingLaneRebuilds(1) 一帧只补 1 个网格
    → 5 个网格永远补不完 = 持续抽搐;
  * 纵向余量只有 ±marginY(半个网格高 ≈80px),中键纵向拖拽每帧很容易移动 20~150px
    → 几乎必现;横向同理(余量 ±半屏,快速拖拽会超过)。

修法:只有"内容真的变了"(contentStale:外观/网格几何/幅值跨度/可见性…)才排队(那时画面本身
仍是旧内容、不缺口);距离类问题一律本帧就地解决:
  * |shift| < bitmapWidth 且 |vOffset| < 画布高 → 滚动:BitBlt 整块平移 + 只补画暴露的细条
    (横向条 / 纵向条),并同步锚点记账(wideMin / anchorYMax);
  * 任一超出画布(内容整体滚出)→ 本帧立刻整块重画并重新居中
    (RenderLaneLayerRegion(i, layer, 0, bitmapWidth, 0, 0, LayerDefaultWideMin()),实测 0.4ms/网格)。

探针(.preflight/dragcheck,按用户要求"全程按住中键不放"):
  * 图层条带 vs 同锚点新渲染:逐像素(滚动是纯整数平移,必须相同);
  * 覆盖率:(ViewMin - layer.wideMin) * scale == marginX(±1px)且 marginY + vOffset 起的一整个
    网格高都落在画布内 —— 任一条不成立,BlitWideBuffer 就会留下空白条带;
  * 画面结构:网格内"无墨迹行数"与"墨迹占比"对比整幅重绘参考帧(空白条带 / 内容错位探测器)。
场景:上拖半个网格高;按住拖左右/上下/斜向并拖出窗口;纵向拖 4 个网格高再拖回来并小幅摆动;
快速大位移对角拖拽(每帧 150×110px)。A/B 验证探针有效性:临时把纵向滚动关掉
(kLayerRollQuantum = 100000)→ 立刻报 worst layer band diff 76221 / drawing errors 11;
恢复后 0 错误。

两点说明(避免误判):
1. dragcheck 的"画面像素 diff"会稳定出现 ~2.5 万像素(一个网格的 ~11%)差异 —— 那是图层与整幅
   重绘的列相位差(图层画布 2449px 按 1px 列聚合,整幅重绘 1224px 列,相位差 ≤0.5px),对稠密脉冲
   信号(256kHz 编码器)会放大成大片边缘差异;这是长期存在、由"结算帧"收敛的现象,不是本轮缺陷,
   因此画面检查用结构化指标而不是像素 diff。
2. 截图里"整条网格被信号色填满"是正确渲染:把幅值窗口平移到与稠密信号相交的区间时,每列的
   min/max 包络本来就覆盖整个网格(示波器上同样如此)。

验证(全部离屏,不占用鼠标/键盘/屏幕):dragcheck PASS(0 绘制错误;长拖拽 80 帧 + 快速对角 24 帧);
uitest 463/463;panperf PASS(竖直中位 4.4ms、水平 7-10ms);layercheck clobbers=0/24、
pan frames wrong=0/4;cursorcheck 30/30;make clean && make all 零警告。
2026-10-12 20:44:51 +08:00
DeepSeek V4.1 Flash 59a84ba126 「弱峰调整」升级:三组识别规则 + 滑动条 ±400% 梯度驱动
一、三组识别规则(以波峰 k 为例;L/l = 左相邻波峰/波谷,r/R = 右相邻波谷/波峰;
   leftMean = (L+l)/2,rightMean = (r+R)/2)
- 第一类:v < leftMean 且 v < rightMean(两侧的"相邻峰+相邻谷"均值都要低于)
- 第二类:v < leftMean 或 v < rightMean(放宽为"或")
- 第三类:v < L 且 v < R(只与左右同类极值比较,最宽松)
波谷把"低于"换成"高于"、min/max 互换。三组不是包含关系(第三类不蕴含第二类),
因此确实存在"只满足第三类"的点。

二、梯度驱动(滑动条 |a| ∈ [0,400%];每组自身仍是 ±200%)
  0–100%   :只有第一类在动(0–100%)
  100–200% :第一类 100–200%,第二类开始 0–100%
  200–300% :第一类到上限,第二类 100–200%,第三类开始 0–100%
  300–400% :前两类到上限,第三类 100–200%
负值完全对称。实现:first = sign*min(|a|,2)、second = sign*clamp(|a|-1,0,2)、
third = sign*clamp(|a|-2,0,2);某点同时满足多组时取幅度绝对值最大的一组(= 最严格的那组)。

三、每组自身的目标值映射(±200%)
  0 – +100%  :当前值 → 左右同类极值中较温和的一侧(波峰取较低者、波谷取较高者)
  +100 – +200%:该侧 → 左右同类极值的均值(不再是"较大的那个")
  0 – -100%  :当前值 → 相邻异类极值中较近的一个(波峰取较高的波谷、波谷取较低的波峰)
  -100 – -200%:该侧 → 相邻异类极值的均值(不再是"最小的那个")
负幅度的"不得反向增强"夹取保持不变;相邻极值之间的 smoothstep 平滑偏移逐字未改。

四、界面
滑动条量程 -400% – +400%(0 在正中间,kWeakPeakSliderScale = 4.0);数值列按 "+400%" 取宽;
右侧数值始终带符号。

五、验证(全部离屏,不占用鼠标/键盘/屏幕)
- uitest 463/463:第一类点(弱峰 520、相邻峰 620/640)0/50/100/150/200% → 520/570/620/625/630;
  弱谷 580 → 580/540/500/500/500,负值 590/600/605/610;
  只满足第二类的峰 130:100% 不动、150/200/250/300/400% → 140/150/162.5/175/175;
  负值 -100% 不动、-150/-200/-250/-300% → 115/100/97.5/95;
  只满足第三类的峰 195:200% 不动、250/300/400% → 197.5/200/200;
  第一类点 200% 已达上限(400% 不再变化);±400% 夹取;默认 0%;滑块中间 = 0%、
  两端 = ±400%、轨道 3/4 处 ≈ +200%。
- [Y] 端到端:合成 ripple 弱峰区间 20 → 70(50% 幅度,健康 120)。
- panperf PASS;layercheck clobbers=0/24、pan frames wrong=0/4;cursorcheck 30/30;
  dragcheck 0 绘制错误;make clean && make all 零警告。
2026-10-12 19:38:46 +08:00
DeepSeek V4.1 Flash 14b60f5d9e 修复上下拖动后的绘制错误(图层条带 GDI 裁剪)+「弱峰调整」定义/默认值/符号显示
一、绘制错误真因:图层条带渲染时 GDI 网格线越界作画并烘进图层
上一轮把图层条带从"只有横向条"扩展到"横向条 + 纵向条"(纵向滚动补画)。但 PaintLane 里 GDI
(网格线/分界线)的裁剪矩形 RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop) 只把 x
限制在条带内、y 仍是整个网格可见范围。纵向条带(全宽 × 窄 y)渲染时:
  * GDI 网格线被画到条带之外(那儿的像素由滚动而来、本来是正确的);
  * 那些行原本是"波形盖在网格线之上",变成"网格线盖在波形之上",与正确渲染不一致;
  * 错误像素就在图层位图里,会被后续每次平移带着移动 → "拖到哪儿都有绘制错误"。
修法:把"本次允许改动的区域"算成 laneClip = IntersectRect(m_area, 网格可见矩形),同时用于
Graphics::SetClip 与 GDI 的 gridClip(横向补条本来就没问题,所以此前只有纵向/斜向暴露)。

二、新增探针 .preflight/dragcheck(复现用户手势)
注入真实中键消息:先把最上方网格向上拖半个网格高(触发纵向滚动),再保持按住拖左右/上下/斜向、
最后拖出窗口;每帧把图层画布的有效条带(marginY + vOffset 起、网格高行)与"用同一 wideMin 新渲染
的画布"逐像素比较(滚动是纯整数平移,必须逐像素相同;用 FromHBITMAP + LockBits 直接读像素)。
修复前:被拖网格整行不一致(2410 px ≈ 2 行全宽,错误行固定在 canvas 行 138 = 某条横向网格线,
直接指向 GDI 网格线);修复后:最差 ≤171 px(仅补条边界的抗锯齿边缘),0 处绘制错误。

三、「弱峰调整」定义
"是否偏弱"改为(正/负幅度一致):波峰必须同时低于左右两侧各自的「相邻波峰 + 相邻波谷」均值;
波谷必须同时高于上述两个均值。与旧实现(只与左右两个同类极值比较)不同:整段均匀偏弱的纹波
不再被判为弱峰,因此合成测试波形重设计(分段线性、谷恒 500、弱峰 520、健康峰 620/640)。

四、默认幅度与符号显示
默认幅度改为 0%(面板提出即不改动波形);滑块右侧数值始终显示符号(+150% / -150% / 0%),
数值列按 "+200%" 取宽避免跳动。

五、验证(全部离屏,不占用鼠标/键盘/屏幕)
- uitest 441/441:[X] 正幅度 0/50/100/150/200% → 峰 520/570/620/630/640、弱谷 580/540/500/500/500;
  负幅度 -50/-100/-200% → 峰 510/500/500、谷 590/600/620;超范围夹取;"未反向增强"(极值点);
  健康段逐位不变;30% 幅度下 N=1/2/3/10 → 550/571/571/571(收敛);默认 0%;滑块中间 = 0%、
  两端 = ±200%;数值带符号(+150% 与 -150% 墨迹等宽、都比 0% 宽 ≥8px)。
- [Y] 端到端:合成 ripple(谷恒 500、一个弱峰 520)弱峰区间 20 → 70(健康 120)。
- dragcheck PASS(0 绘制错误);panperf 竖直中位 5.1ms、水平 7.1ms、20 面板竖直 25.0ms / 水平 41.3ms;
  layercheck clobbers=0/24、pan frames wrong=0/4;cursorcheck 30/30;make clean && make all 零警告。
2026-10-12 18:25:31 +08:00
DeepSeek V4.1 Flash ff3319b842 修复上下拖动时的绘制异常/抽搐(纵向预渲染补齐)+「弱峰增强」升级为「弱峰调整」(-200%–+200%)
一、纵向绘制异常/抽搐的根因
上一轮给图层加了「上下各半个网格高」的纵向预渲染余量,但"偏移超出余量"时仍走**排队整块重画**
(一帧只补一个网格):重画生效前 BlitWideBuffer 只能贴出有像素的那一部分,其余行留在底色上
= 网格内出现空白条带("绘制错误");且重画后的锚点与"贴图偏移"不同源 = 波形"没按预期移动"。
鼠标按在最上方网格一路拖到窗口最下方(离开网格很远)时,每帧的幅值位移都远超余量 → 每帧触发整块
重画 = 抽搐;左右拖动时若鼠标同时有纵向位移,同样触发。

二、修法:与横向完全同构的纵向滚动
- RenderLaneLayerRegion 增加 stripY/stripH(纵向条带),可只补画"新暴露的一整条"。
- RollWideBuffer 新增纵向滚动:BitBlt 整块上/下移 roll 行 → 补画暴露的那一条 →
  layer.anchorYMax -= roll / pxPerValue(与横向 wideMin 同一套记账)。
  必须在横向补条之前(横向补条会用当前 yMax 重置图层锚点,顺序反了细条会带上错的纵向偏移)。
- 滚动量量化(kLayerRollQuantum = 16 像素):不足量子的偏移不滚,交给贴图的 srcY 偏移处理,
  使画布映射在拖动中保持稳定、折线缓存(键含幅值映射)得以命中。
  未量化时"每帧滚 1~30 行"会让折线每帧重算(实测滚动 3~4ms + 整帧 10~14ms)。

三、「弱峰调整」(原「弱峰增强」)
- 术语统一:ComputeWeakPeakAdjust、WeakPeakItem.amountPercent(-200–200,默认 150)、面板标题
  「弱峰调整」、标签「调整幅度 %」、光标胶囊尾缀、构建信号名「<原信号> (弱峰调整 X% M次迭代)」。
- 滑块量程 -200% – +200%,0 在正中间(ratio = (amount/200 + 1) / 2);数值列加宽以容纳 "-200%"。
- 目标值(识别规则与旧实现完全相同;amount = 幅度/100):
  *  0 ≤ a ≤ 1:当前值 → 同类极值中较温和的一侧;
  *  1 < a ≤ 2:该侧 → 另一侧(与旧实现逐位一致);
  * -1 ≤ a < 0:当前值 → 相邻异类极值中较近的一个(波峰看较高的波谷、波谷看较低的波峰);
  * -2 ≤ a < -1:较近的异类极值 → 较远的那个(弱峰被压平甚至抹除,左右平滑过渡)。
- 负幅度不得反向增强:目标夹在"不越过当前值"的一侧。平滑偏移的逐点过渡逻辑未改动。

四、验证(全部离屏,不占用鼠标/键盘/屏幕)
- uitest 443/443:[X] 正幅度 0/50/100/150/200% → 峰 550/555/560/580/600(旧值逐位保持)、
  谷 450/445/440/420/400;新增负幅度 -50/-100/-200% → 峰 500/450/400、谷 500/550/560;
  超范围夹取、"未反向增强"(极值点)、健康峰不动;滑块中间 = 0%、拖出两端 = ±200%;
  构建信号名含「弱峰调整」;[AH] 纯竖直拖拽 16 帧仅 2 次图层重画。
- panperf:拖出网格 900px + 对角平移 31 帧,被拖网格的空白行数与整幅重绘逐帧一致(64 vs 64
  → 无空白条带),帧时 10~13ms,末帧差异与水平基线同量级(已知的 ≤0.5px 整数像素残差)。
- layercheck:clobbers=0/24、pan frames wrong=0/4、居中图层 vs 整幅重绘逐像素 0 差异;
  cursorcheck 30/30;make clean && make all 零警告。
2026-10-12 17:10:51 +08:00
DeepSeek V4.1 Flash cb030573fa 上下/斜向平移卡顿(图层与面板都缺纵向预渲染)+ 深放大拖拽光标跑飞
用户报告:Y 轴自适应 + 选区放大到约 1~2ms 一大格后,中键拖拽移动视界"左右很流畅,上下或
斜向会卡顿",并猜测"上下移动时没有像左右那样的预渲染缓存";另有"小缩放区间拖拽光标跑飞"。

一、先验证猜测(离屏量化,不凭感觉)
panperf 新增:纯竖直拖拽(只改被拖网格幅值、视界不动)+ 逐帧统计图层重画次数 + 5 网格全开
4 类悬浮面板(20 个)后的水平/竖直对比。结果:
  * 无面板:水平 6.29ms/帧,竖直 5.26ms/帧,但竖直**每帧都整块重画那个网格的图层**(40/40)。
  * 20 个面板:水平 37.7ms/帧,竖直 **166.6ms/帧** —— 这就是用户说的"上下卡顿"。
结论:猜测部分成立(纵向确实没有预渲染),但主因是另一处:浮动面板像素图层的**全局键**里含
visual.yMin/yMax,平移的纵向分量每帧小改幅值 → 整层 20 个面板每帧全部重画。水平平移不改幅值,
所以只有竖直/斜向才触发 —— 正好解释"左右流畅、上下卡顿"的不对称。

二、图层改为二维预渲染(横向 ±半屏 / 纵向 ±半个网格高)
- LaneLayer 增加 marginY / anchorYMax / pxPerValue;位图高度 = ceil(可见网格高) + 2*marginY。
- 幅值范围按**像素行**精确扩展(上扩 marginY/pxPerValue,下扩 (bitmapHeight - marginY - laneHeight)
  /pxPerValue):只有这样"扩展跨度 / 画布行数"才严格等于前台的 px/数值,可见网格在画布里的映射
  才与整幅渲染逐像素同源(按固定数值量扩展会留下 <1px 的纵向缩放误差,水平残差会从 77k 涨到 105k)。
- 图层里的幅值刻度改以**前台可见网格**的跨度/高度取 NiceStep(m_wideTickSpan / m_wideTickHeight):
  画布更高更宽,若按画布值取步长,网格线密度与前台不一致(贴图后拖动时网格线跳变)。
- 贴图:srcY = round(top - lane.Y) + marginY + round((anchorYMax - 当前 yMax) * pxPerValue);
  超出画布上下端时只贴有像素的那一段;偏移超出余量才排队重画(重画即重新居中)。
- LaneLayerHash 只计幅值**跨度**、不计绝对范围(绝对范围是贴图偏移的来源)。
- 网格分界线移出图层(m_renderWide 时跳过,改由 PaintLaneDividers() 在宽基底路径补画一次):
  它锚定在网格的屏幕位置,纵向偏移贴图会把它带着一起跑。

三、面板像素图层不再被幅值范围击穿
PanelGlobalKey() 去掉 visual.yMin/yMax(面板内容与幅值区间无关)。代价仅是 95.7% 不透明底背后
那 4.3% 透出的底层像素可能旧一帧(色差有界 ≤ 0.043*255 ≈ 11 级),与既有的"旧一帧"取舍一致。

四、深放大下拖拽光标跑飞
历史:388d3b6/67f659a 的"视图变化后夹光标"被 eaf5b9c 刻意删除(改为光标可滞留在视界外 + 贴边读数
+ 方向三角);但深放大下一屏平移就是几十倍可视跨度,光标瞬间被甩到视界外很远 = 用户看到的"跑飞"。
修法(区分连续手势与离散视图变化):
- 新增 ClampCursorsAfterPan(),在**平移**路径调用:PAN 拖拽(中键/左键)、Shift+滚轮、方向键。
- 离散视图变化(选区放大 / 适应视图 / 按网格时间轴展开)仍然不移动光标(先量好再自由探索,
  贴边读数可读、可拖回来)——不要把夹取加回这几处。
- 抓取被钉在边缘的光标读数时不再沿用抓取偏移(CursorOffViewSide → m_cursorGrabOffset = 0),
  否则首次移动会把目标送到视界外再被夹回边缘("一拖就跳很远 / 光标钉在边缘不动")。

五、验证(全部离屏,不占用鼠标/键盘/屏幕)
- uitest 432/432:[AH] 改为"幅值平移不再让图层失效(只改贴图偏移)、幅值跨度变化仍失效"并新增
  "纯竖直拖拽 8 帧 0 次图层重画";新增 [AI]"贴边读数抓取后光标落在鼠标处(误差 0.0px)、
  中键平移 1.5 屏 0 帧出界且跟手"。
- panperf(VERY_BIG_DATA、5 网格、1~2ms 一大格):竖直中位 3.8~5.0ms、40 帧仅 1 次图层重画
  (修复前 40/40);水平 5.7~6.5ms(与修复前 6.29ms 持平);20 个面板时竖直 166.6 → 24.8ms/帧,
  水平 37.7 → 38.8ms/帧(不对称消失)。整幅质量残差与修复前同量级(水平 77k~81k ↔ 77k~81k)。
- layercheck:clobbers=0/24、pan frames wrong=0/4、居中图层 vs 整幅重绘逐像素 0 差异。
- 新增探针 .preflight/cursorcheck(真文件 + 真缩放)30/30。
- make clean && make all 零警告。
2026-10-12 16:09:41 +08:00
DeepSeek V4.1 Flash 0afa6360ae 修复斜向自由拖拽视界时的卡顿/质量下降/错误绘制;预览曲线恢复抗锯齿;预览曲线不再闪烁
一、斜向拖拽退化的根因(纯水平拖拽测不出来)
平移的纵向分量 ShiftLaneRange 每帧都会小改被拖网格的 yMin/yMax,而网格幅值范围同时参与
「宽基底可用性」的几何指纹(SceneGeometryHash 原本含 yMin/yMax)与每个网格图层键的基
(LaneLayerKey 原本用全局 SceneContentHash):
  * 前者 → 每帧都判定宽基底不可用 → 退化成整幅重绘,且拖拽置 m_fastFrame=true → 关抗锯齿
    (绘制质量下降 + 卡顿);
  * 后者 → 一个网格的幅值变化让全部 5 个网格的图层作废并全部入队,而
    RenderPendingLaneRebuilds(1) 一帧只补 1 个 → 长期贴内容不一致的旧图层 = 错误绘制/断线。

二、修法
- SceneContentHash(includeStyles, includeDerived, includeLaneRange) 新增 includeLaneRange;
  SceneGeometryHash() = SceneContentHash(false, true, false) → 幅值范围不再参与
  「能不能走宽基底快路径」的判定。
- 新增逐网格 LaneLayerHash(laneIndex)(位图高度 + 该网格 yMin/yMax + 该网格各信号的
  visible/color/opacity/lineWidth/mode);LaneLayerKey(laneIndex) 的基由全局内容指纹改为它
  → 幅值变化只让那一个网格失效,其余网格保持 Ready。
- Paint 宽基底分支:RenderPendingLaneRebuilds(1) 从 BlitWideBuffer 之后移到
  RollWideBuffer() 之后、贴图之前 → 本帧预算内补好的网格本帧就可见(消除「比鼠标慢半拍」与断线)。

三、预览曲线抗锯齿
- PaintDerivedCurve 不再因 m_fastFrame 关闭抗锯齿(仅保留「陡峭段占比 > 1/8」的逃生口)。
  波形基底来自图层、永远是全质量抗锯齿;预览曲线是叠加层,交互期间关 AA 就会「比普通信号粗糙」。
  交互期间仍把顶点预算减半(低分辨率 → 松手恢复高分辨率)。

四、预览曲线闪烁
- AcquireNewestCurve(key, begin, end, spanBegin) 改为只按「信号 + 曲线种类」匹配(参数无关),
  并优先挑窗口覆盖 [begin,end] 的条目;拖参数滑块时先用上一帧的曲线顶替,后台按新参数算好后无缝替换,
  不再出现「消失一会儿又画出来」。

五、验证(全部离屏,不占用鼠标/键盘/屏幕)
- uitest 新增 [AH]:逐网格图层失效 / 斜向帧走宽基底快路径 / 参数连改 10 次 0 空白帧 /
  隔离抗锯齿断言(快帧混淆边缘像素 8149 vs 8404,旧实现该值为 2)→ 420/420 通过;
  make clean && make all 零警告。
- panperf:水平平移中位 6.17ms;斜向拖拽中位 9.16ms、p90 10.67ms、max 11.80ms,
  走宽基底快路径 40/40(修复前 0/40),像素差与水平基线同量级(仅已知的 ≤0.5px 整数像素残差)。
- layercheck:clobbers=0/24、pan frames wrong=0/4、居中图层 vs 整幅重绘逐像素 0 差异。
2026-10-12 14:22:14 +08:00
DeepSeek V4.1 Flash a30a6fcf4d 弱峰增强改回逐信号滑块;迭代次数上限可就地编辑
一、参数输入方式(回到滑块形态,但每个信号各有一套)
- 面板布局:每个被勾选的信号 = 名称行(+复选框)+ 两行滑块:
  「增强等级 %」(0~200%)与「迭代次数 N」(1 ~ 该信号自己的上限)。
  EdgePanelLayout 新增逐信号矩形 rowLevelTracks/rowLevelValues/rowIterTracks/rowIterValues;
  PEAK 的 rowPoints/rowIterations 恒为空(MEAN/CENTER 仍用文本输入框)。
- 恢复 kWeakPeakSliderRowHeight/kWeakPeakTrackHeight/kWeakPeakTrackMinWidth 与
  WeakPeakSliderHit();恢复 PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS,
  并新增 m_peakDragRef(拖动时必须知道是哪个信号)。
- 列宽算式集中在 WeakPeakLabelColumnWidth()/WeakPeakValueColumnWidth(),
  与 EdgePanelWidth 共用,避免布局与面板宽度计算漂移。

二、迭代次数滑块上限可就地编辑(用户要求的行为)
- 点击「迭代次数 N」滑块右侧的数值文本 → 整条滑块行(轨道 + 数值)变成一个输入框;
  左侧标签保留可见。
- 输入框以空内容开始;空内容时以灰色提示当前上限值(m_peakEditCurrentMax,EndEdit 清零)。
- 过滤器为 EditFilter::BYTE(纯数字):负号、小数点、字母无法键入,粘贴同样被清洗,
  从源头满足"只允许输入正整数"。
- 提交(回车 / 点击别处):仅当解析成功且 1 <= 值 <= 1000 时才作为新上限;
  空内容、0、超范围一律保持原上限不变;生效后若当前迭代次数超过新上限则自动替换为新上限。
- 输入框在 paintOverlays() 中与 Y 轴上下限输入框一起绘制(覆盖在面板之上)。
- 新增 PlotEditTarget::PEAK_ITERATIONS_MAX;删除 PEAK_LEVEL_PERCENT/PEAK_ITERATIONS
  (等级只能拖滑块,迭代次数只能拖滑块)。

三、数据与缓存
- Lane::WeakPeakItem 新增 maxIterations(默认 10)。
- maxIterations 参与 PanelSlotKey(决定圆钮位置,面板必须重画),
  但不参与 SceneContentHash(不影响网格内曲线,不应触发波形重画)。

四、测试
- uitest [X] 改为:滑块存在性/层序/列宽、等级滑块拖动(中间=100%、右端=200%)、
  迭代滑块拖动(左端=1、右端=当前上限)、点击数值进入上限编辑(空内容 + 提示值 + 输入框
  横跨整行、空内容回车不改、'-'/字母被拒、输入 40 生效且右端变 40、下调到 4 时当前值被夹到 4、
  输入 0 被拒、点击别处提交),并保存编辑态截图供目视核对。
- make all 零警告;uitest 409/409 通过;panperf PASS;layercheck 保持像素一致。
2026-10-12 13:35:50 +08:00
DeepSeek V4.1 Flash 1ddbc37b78 平移不再阻塞前台刷新;弱峰增强逐信号参数;浮动面板加入层序
一、平移:前台刷新线程永不阻塞(解决"按住键拖拽时质量下降/断线/周期性卡顿")
- RollWideBuffer 不再整块重画:逐网格整数像素平移 + 只补细条;内容指纹过期或锚点离视界
  过远的网格只登记到 m_layerRebuildPending 并仍按现有像素平移(位置照样跟随视界)。
- 新增 RenderPendingLaneRebuilds(budget):Paint 每帧最多重画 1 个网格,其余留到后续帧;
  队列非空时 PostMessage(kMessagePlotDerivedReady) 请宿主再送一帧,直到补齐。
- BlitWideBuffer 改为局部贴图:把 BitBlt 夹到图层可用区间,图层没覆盖到的部分留空由底色
  填充(旧实现直接跳过整块,导致网格瞬间变空 = "断线")。
- "结算"(整幅全质量精确帧)触发条件收紧为三者同时成立:内容与视界同上一条快路径帧、
  m_dragMode == NONE、全部图层就绪。拖拽期间不再结算,消除"宽基底快路径 / 整幅重绘(关AA)"
  每两帧振荡造成的质量下降与周期性卡顿。
- 新增 LaneLayerReady(i) / LanesAllReadyForView();原 LayersReadyForView() 更名
  LayersStructurallyReady()(只看结构,用于"跨度/布局变化必须整批重建一次")。
- 实测(.preflight/panperf,VERY_BIG_DATA 5 网格深放大,每帧平移 60px):平移 40 帧
  中位 6.2ms / p90 7.2ms / max 7.6ms;按住键且视界停在边界 12 帧中位 0.29ms(无整幅重绘)。

二、弱峰增强:改为逐信号参数(对齐均值滤波)
- Lane::WeakPeakItem 由 using = MeanFilterItem 改为独立结构体
  { ref, levelPercent(0-200, 默认 150), iterations(1-10, 默认 1) };删除网格级
  Lane::weakPeakLevel / weakPeakIterations。
- 面板改为与均值滤波完全同构:每个被勾选信号一行"名称 + 复选框",第二排两个输入框
  (增强等级 % / 迭代次数 N),底部"构建为新信号";删除网格级两个滑块及其常量、
  WeakPeakSliderHit、PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS、m_peakDragLane、
  EdgePanelLayout::slider/sliderIterations。
- 新增 PlotEditTarget::PEAK_LEVEL_PERCENT / PEAK_ITERATIONS,EndEdit 中分别夹到 0-200 / 1-10。
- 指纹/快照/预览曲线/光标胶囊/构建新信号全部改为逐信号参数(同一网格内不同信号可用不同强度,
  且"预览"与"构建"逐位一致);WeakPeakItem 不再是 MeanFilterItem,purgeRefs 改为泛型 lambda。

三、浮动面板:层序(z-order)与命中修复
- 新增 m_panelOrder(队尾 = 最上层):SyncPanelOrder 保留已有序、新面板追加到最上层;
  TopPanelAtPoint 从最上层往下取第一个命中;BringPanelToFront 在点击面板时把它置顶。
- 绘制改为按 m_panelOrder 自下而上;PanelGlobalKey 混合层序(层序变化即整层重画);
  QueryCursor / OnMouseMove 的"是否在面板上"也走 TopPanelAtPoint。
- 修复"上层面板的 ✕ / 标题栏被下层参数输入框抢走,导致上层面板关不掉/拖不动"。

四、测试
- uitest 新增 [AG]:层预算(每帧只重画 1 个网格、剩余排队、最终补齐并全部就绪)、
  局部贴图(只覆盖视界一半时左侧被贴、右侧保持哨兵色)、面板层序(上层 ✕ 命中上层、
  下层不受影响、点击下层置顶、TopPanelAtPoint 随之改变、新开面板在最上层)。
- [X] 改为逐信号参数(含两个输入框就地编辑:写入该信号自己的条目、越界夹取),
  移除已不存在的滑块断言。
- 新增离屏探针 .preflight/panperf(平移帧耗时 + 按住键静止帧耗时)。
- make all 零警告;uitest 388/388 通过;panperf PASS;layercheck 保持像素一致。
2026-10-12 12:06:42 +08:00
DeepSeek V4.1 Flash da83dc2987 修复缩放后平移的绘制错误/抖动与前台卡顿;弱波增强改名弱峰增强并支持 0~200%
渲染(根因与修法)
- 根因:后台图层线程渲染时会临时改写共享渲染状态(m_area/m_plotRect/视界/网格矩形),
  与 UI 线程无锁的平移(OnMouseMove)和命中测试相冲 → 后台的"保存→改写→恢复"会把
  用户刚做的平移弹回、命中测试读到错位网格;同时 RollWideBuffer 用单一全局锚点给所有
  图层算同一位移,而各图层可能是在不同视界下渲染的(后台逐网格)→ 网格之间时间错位、
  贴回像素互相矛盾;补细条又误用"当前视界"的映射(与旧像素不同源)→ ≤0.5px 接缝与拖动抖动。
- 修法:
  * 删除后台图层线程(m_layerWorker 及任务队列、EnsureLayerWorker、StopLayerWorker、
    LayerWorkerLoop、ScheduleStaleLayerRenders、m_layerBack、m_layerAnchorMin/Valid、
    m_lastPaintTick、m_layerRescheduleStreak)。图层渲染一律在 UI 线程内同步完成。
  * 逐网格独立锚点与滚动:以 LaneLayer::wideMin 为准,补细条沿用该图层自己的映射。
  * RenderLaneLayerRegion 新增 wideMin 形参;新增 LayerDefaultWideMin()。
  * RollWideBuffer 逐网格:内容指纹不一致或锚点离视界太远 → 该网格整块重画;
    否则整数像素自贴图平移 + 只补新暴露的细条。
  * Paint 宽基底分支:!LayersReadyForView() 时先同步 RebuildWideBuffer() 再滚动。
  * BackgroundLayerRenderCount() 保留为"同步图层区域渲染次数"的诊断计数。
- 效果:平移不再出现网格时间错位、视界被弹回或波形抖动;居中图层与整幅重绘逐像素一致。

弱峰增强(原「弱波增强」)
- 功能改名:界面文案与标识符统一为 WeakPeak/weakPeak(ComputeWeakPeakBoost、
  SetLaneWeakPeak*、EdgePanelKind::PEAK、PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS、
  PLOT_CMD_LANE_WEAK_PEAK、PeakExtremum 等),导出信号名 "<原信号> (弱峰增强 M% N次迭代)"。
- 等级范围改为 0~200%,目标值分段插值:
  * 0~100%:在"目标极值自身的值"与"左右同类极值中较温和的一侧"之间插值
    (0% 目标就是当前值 → 完全不动,允许用户做极小幅度调整);
  * 100~200%:与原先 0~100% 的插值方式一致(200% 完全拉平到另一侧)。
  两段在 100% 处连续。新 1.5 == 旧 0.5,新 2.0 == 旧 1.0。
- Lane::weakPeakLevel 默认 1.5;levelPercent(构建请求/曲线缓存键)范围 0~200、默认 150;
  新增 kWeakPeakLevelMax = 2.0 与 kWeakPeakPercentMax = 200;滑块轨道比例 = level / 2。

测试
- 同步更新离屏测试 .preflight/uitest:等级期望改为 0/50/100/150/200 五档;阶数扫描与
  "完全恢复"块改用 150%/200% 以保持旧期望;派生信号名校验改用新名;
  [J] 改为"补丁帧标记降级 → 渲染到缓存重新有效后用修复帧与整幅重绘比对(0 差异)";
  [AF] 面板像素容差 32→96(面板 96% 不透明,底层 ≤0.5px 残留会透出,实测最多 6 像素/72 级);
  [X] 预览曲线像素计数的颜色容差 6→40。
- 新增离屏检查 .preflight/layercheck:居中图层与整幅重绘逐像素对比、平移逐网格对齐位移、
  视界覆盖检测、跨度抖动下的视界保持检测。
- make release 零警告;uitest 380/380 通过(exit=0)。
2026-10-12 10:10:06 +08:00
DeepSeek V4.1 Flash 800c8b835b 同步起源设备详细记忆入库(并入同一记忆文件)
- 远端 gitea/main 已快进合并到本机(7608e85 → f97c4cf,16 个提交):
  每网格信号图层双缓冲 + 后台绘制线程、网格纵向滚动量纳入内容指纹、
  装饰层缓存不变量修复、样式补丁强制快速帧、二级菜单误关闭与
  菜单打开后悬停残留修复等。本机 MinGW-w64 GCC 15.2.0 (UCRT) 下 make release 通过。
- 按用户要求「使用同一个记忆文件,不要生成其它记忆文件」:把起源设备(本机)的
  详细工程记忆整份并入 Documents/agent_mem.md,不再另建 agent_mem_origin.md。
  - 新增 §11 跨设备协作须知:两端工具链差异、高风险硬约束摘要(13 条)、离屏验证方法学。
  - 新增 §12 起源设备详细工程记忆(原文附录 = 本机仓库记忆原文)。
  - §10 变更日志补充本轮合流记录;修正「宽 ×3」为实际「×2」、远端提交数 21→16。
- 代码未改动,仅文档;本次提交仅在本地,未推送。
2026-10-12 09:16:43 +08:00
7 changed files with 4358 additions and 1110 deletions

No files matched your search

+6 -6
View File
@@ -241,8 +241,8 @@ namespace jdi
/**
* @brief 应用绘图区提交的「实验性功能 → 构建为新信号」请求。
*
* 为指定网格内的每条被勾选信号各构建一条派生信号(多次迭代均值滤波或弱波增强),
* 命名为 `<原信号名称> (N点均值…)` / `<原信号名称> (弱波增强 M%)`,
* 为指定网格内的每条被勾选信号各构建一条派生信号(多次迭代均值滤波或弱峰调整),
* 命名为 `<原信号名称> (N点均值…)` / `<原信号名称> (弱峰调整 M%)`,
* 插入到原信号所在组的原信号位置**下方**,并把新信号所在的网格移动到原信号网格的下方。
*
* @return 实际构建了新信号返回 true。
@@ -250,7 +250,7 @@ namespace jdi
bool ApplyPendingDerivedBuild();
/**
* @brief 派生信号构建请求(多次迭代均值滤波 / 弱波增强共用同一套插入流程)。
* @brief 派生信号构建请求(多次迭代均值滤波 / 弱峰调整共用同一套插入流程)。
*/
struct DerivedSignalRequest
{
@@ -258,12 +258,12 @@ namespace jdi
int signalIndex = -1; /**< 原信号下标。 */
int points = 5; /**< 多次迭代均值滤波:均值点数 X(滑平半径)。 */
int iterations = 1; /**< 多次迭代均值滤波:迭代次数 N(至少 1)。 */
int levelPercent = 50; /**< 弱波增强:增强等级(0–100)。 */
bool weakBoost = false; /**< true = 弱波增强,false = 多次迭代均值滤波。 */
int amountPercent = 0; /**< 弱峰调整:调整幅度(-400–400,默认 0)。 */
bool weakPeak = false; /**< true = 弱峰调整,false = 多次迭代均值滤波。 */
};
/**
* @brief 把一条派生信号(多次迭代均值滤波 / 弱波增强)插入数据层并同步各视图。
* @brief 把一条派生信号(多次迭代均值滤波 / 弱峰调整)插入数据层并同步各视图。
* @param [IN] request 构建请求。
* @param [IN] makeVisible 是否在当前标签页中勾选新信号。
* @return 构建并插入成功返回 true。
+22 -22
View File
@@ -1481,14 +1481,14 @@ namespace jdi
// 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。
// 多次迭代均值滤波形如「<原信号名称> (N点均值 M次迭代)」,
// 弱波增强形如「<原信号名称> (弱波增强 M% N次迭代)」。
// 弱峰调整形如「<原信号名称> (弱峰调整 M% N次迭代)」。
std::vector<float> derived_values;
SignalData derived;
if (request.weakBoost)
if (request.weakPeak)
{
ComputeWeakWaveBoost(source.values, static_cast<double>(request.levelPercent) / 100.0,
ComputeWeakPeakAdjust(source.values, static_cast<double>(request.amountPercent) / 100.0,
request.iterations, derived_values);
derived.name = baseName + L" (弱波增强 " + std::to_wstring(request.levelPercent) + L"% " +
derived.name = baseName + L" (弱峰调整 " + std::to_wstring(request.amountPercent) + L"% " +
std::to_wstring(request.iterations) + L"次迭代)";
}
else
@@ -1538,7 +1538,7 @@ namespace jdi
bool Application::ApplyPendingDerivedBuild()
{
// 两类「构建为新信号」请求(均值滤波 / 弱波增强)机制完全一致,
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
// 统一整理成同一份构建清单后按下标降序处理。
std::vector<DerivedSignalRequest> requests;
{
@@ -1553,24 +1553,24 @@ namespace jdi
request.signalIndex = item.signalIndex;
request.points = item.points;
request.iterations = item.iterations;
request.weakBoost = false;
request.weakPeak = false;
requests.push_back(request);
}
}
}
{
int laneIndex = -1;
std::vector<PlotView::WeakBoostBuild> items;
if (m_plot.TakePendingWeakBoostBuild(laneIndex, items))
std::vector<PlotView::WeakPeakBuild> items;
if (m_plot.TakePendingWeakPeakBuild(laneIndex, items))
{
for (const PlotView::WeakBoostBuild& item : items)
for (const PlotView::WeakPeakBuild& item : items)
{
DerivedSignalRequest request;
request.fileIndex = item.fileIndex;
request.signalIndex = item.signalIndex;
request.levelPercent = item.levelPercent;
request.amountPercent = item.amountPercent;
request.iterations = item.iterations;
request.weakBoost = true;
request.weakPeak = true;
requests.push_back(request);
}
}
@@ -1610,9 +1610,9 @@ namespace jdi
if (built > 0)
{
std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 ";
if (last.weakBoost)
if (last.weakPeak)
{
text += L"弱波增强 " + std::to_wstring(last.levelPercent) + L"% " +
text += L"弱峰调整 " + std::to_wstring(last.amountPercent) + L"% " +
std::to_wstring(last.iterations) + L"次迭代信号。";
}
else
@@ -1753,11 +1753,11 @@ namespace jdi
meanFilter.checked = m_plot.LaneMeanFilter(laneIndex);
experimental.submenu.push_back(meanFilter);
MenuItem weakBoost;
weakBoost.label = L"弱波增强";
weakBoost.id = PLOT_CMD_LANE_WEAK_BOOST;
weakBoost.checked = m_plot.LaneWeakBoost(laneIndex);
experimental.submenu.push_back(weakBoost);
MenuItem weakPeak;
weakPeak.label = L"弱峰调整";
weakPeak.id = PLOT_CMD_LANE_WEAK_PEAK;
weakPeak.checked = m_plot.LaneWeakPeak(laneIndex);
experimental.submenu.push_back(weakPeak);
}
items.push_back(experimental);
@@ -1872,9 +1872,9 @@ namespace jdi
// 「实验性功能 → 均值滤波」:切换均值滤波曲线与参数面板。
m_plot.ToggleLaneMeanFilter(laneIndex);
break;
case PLOT_CMD_LANE_WEAK_BOOST:
// 「实验性功能 → 弱波增强」:切换弱波增强曲线与参数面板。
m_plot.ToggleLaneWeakBoost(laneIndex);
case PLOT_CMD_LANE_WEAK_PEAK:
// 「实验性功能 → 弱峰调整」:切换弱峰调整曲线与参数面板。
m_plot.ToggleLaneWeakPeak(laneIndex);
break;
case PLOT_CMD_LANE_SPLIT:
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
@@ -2378,7 +2378,7 @@ namespace jdi
{
// 绘图区内的点击也可能提交图例改名,需要立刻落到数据层。
ApplyPendingRename();
// 「构建为新信号」按钮(均值滤波 / 弱波增强):立即构建新信号并同步各视图。
// 「构建为新信号」按钮(均值滤波 / 弱峰调整):立即构建新信号并同步各视图。
ApplyPendingDerivedBuild();
InvalidateRect(hwnd, nullptr, FALSE);
}
+279 -164
View File
@@ -43,8 +43,8 @@ namespace jdi
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
BOOST_LEVEL, /**< 拖动「弱波增强」面板中的等级滑块。 */
BOOST_ITERATIONS, /**< 拖动「弱波增强」面板中的迭代次数 N 滑块。 */
PEAK_LEVEL, /**< 拖动「弱峰调整」某信号的幅度滑块(-400% – +400%)。 */
PEAK_ITERATIONS, /**< 拖动「弱峰调整」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
};
@@ -72,7 +72,8 @@ namespace jdi
EDGE_CENTER_POINTS, /**< 编辑「中心轴边沿计数」的均值点数 X。 */
EDGE_CENTER_ITERATIONS, /**< 编辑「中心轴边沿计数」的迭代次数 N。 */
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
MEAN_FILTER_ITERATIONS /**< 编辑「均值滤波」的迭代次数 N。 */
MEAN_FILTER_ITERATIONS, /**< 编辑「均值滤波」的迭代次数 N。 */
PEAK_ITERATIONS_MAX /**< 编辑「弱峰调整」某信号「迭代次数 N」滑块的**上限**(正整数)。 */
};
/**
@@ -90,7 +91,7 @@ namespace jdi
PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */
PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
PLOT_CMD_LANE_WEAK_BOOST = 11 /**< 切换本网格的「实验性功能 → 弱波增强」开关。 */
PLOT_CMD_LANE_WEAK_PEAK = 11 /**< 切换本网格的「实验性功能 → 弱峰调整」开关。 */
};
/**
@@ -101,7 +102,7 @@ namespace jdi
COUNT = 0, /**< 边沿计数。 */
CENTER = 1, /**< 边沿计数(中心轴)。 */
MEAN = 2, /**< 均值滤波。 */
BOOST = 3 /**< 弱波增强。 */
PEAK = 3 /**< 弱峰调整。 */
};
/**
@@ -112,7 +113,7 @@ namespace jdi
enum class DerivedCurveKind : int
{
MEAN_FILTER = 0, /**< 多次迭代均值滤波。 */
WEAK_BOOST = 1 /**< 弱波增强。 */
WEAK_PEAK = 1 /**< 弱峰调整。 */
};
/**
@@ -138,11 +139,11 @@ namespace jdi
void SetHostWindow(HWND host) { m_host = host; }
/**
* @brief 后台「网格图层」绘制线程已完成的图层数量(仅供验证 / 诊断)。
* @brief 已渲染的「信号图层」区域数量(仅供验证 / 诊断)。
*
* 计数 > 0 即说明单网格基底的绘制确实发生在**后台线程**,
* 前台只负责位图搬运(贴图)与叠加层。
* @return 累计完成的图层数量。
* 每次把一个网格的基底(整块或滚动细条)画进图层都会 +1。计数长期不增长即说明
* 平移期间确实在复用已有像素(只做坐标偏移),没有重新绘制波形。
* @return 累计完成的图层区域数量。
*/
std::uint64_t BackgroundLayerRenderCount() const
{
@@ -569,115 +570,85 @@ namespace jdi
bool TakePendingMeanFilterBuild(int& laneIndex, std::vector<MeanFilterBuild>& items);
/**
* @brief 「实验性功能 → 弱波增强」的「构建为新信号」单个请求
* (一个被勾选的信号 + 整个网格共用的增强等级)。
* @brief 「实验性功能 → 弱峰调整」的「构建为新信号」单个请求
* (一个被勾选的信号 + **它自己的**增强等级与迭代次数,与均值滤波同构)。
*/
struct WeakBoostBuild
struct WeakPeakBuild
{
int fileIndex = -1; /**< 信号所属文件下标。 */
int signalIndex = -1; /**< 文件内信号下标。 */
int levelPercent = 50; /**< 弱波增强等级(0–100)。 */
int amountPercent = 0; /**< 弱峰调整幅度(-400–400,默认 0 = 不改动)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
};
/**
* @brief 取出「弱波增强 → 构建为新信号」请求(由面板底部按钮触发)。
* @brief 取出「弱峰调整 → 构建为新信号」请求(由面板底部按钮触发)。
* @param [OUT] laneIndex 触发该请求的网格下标。
* @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。
* @return 存在待处理请求返回 true(取出后清空)。
*/
bool TakePendingWeakBoostBuild(int& laneIndex, std::vector<WeakBoostBuild>& items);
bool TakePendingWeakPeakBuild(int& laneIndex, std::vector<WeakPeakBuild>& items);
/**
* @brief 切换某网格的「弱波增强」开关。
* @brief 切换某网格的「弱峰调整」开关。
* @param [IN] laneIndex 网格下标。
* @param [IN] enabled 是否开启。
* @return 状态发生变化返回 true。
*/
bool SetLaneWeakBoost(int laneIndex, bool enabled);
bool SetLaneWeakPeak(int laneIndex, bool enabled);
/**
* @brief 查询某网格的「弱波增强」开关。
* @brief 查询某网格的「弱峰调整」开关。
* @param [IN] laneIndex 网格下标。
* @return 已开启返回 true。
*/
bool LaneWeakBoost(int laneIndex) const;
bool LaneWeakPeak(int laneIndex) const;
/**
* @brief 切换某网格的「弱波增强」开关。
* @brief 切换某网格的「弱峰调整」开关。
* @param [IN] laneIndex 网格下标。
* @return 切换后的状态。
*/
bool ToggleLaneWeakBoost(int laneIndex);
bool ToggleLaneWeakPeak(int laneIndex);
/**
* @brief 读取某网格的弱波增强等级(0–1)。
* @param [IN] laneIndex 网格下标。
* @return 增强等级;网格不存在时返回 0.5。
*/
double LaneWeakBoostLevel(int laneIndex) const;
/**
* @brief 读取某网格的弱波增强迭代次数 N(1–10)。
* @param [IN] laneIndex 网格下标。
* @return 迭代次数;网格不存在时返回 1。
*/
int LaneWeakBoostIterations(int laneIndex) const;
/**
* @brief 设置某网格的弱波增强迭代次数 N(1–10,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] iterations 迭代次数。
* @return 迭代次数发生变化返回 true。
*/
bool SetLaneWeakBoostIterations(int laneIndex, int iterations);
/**
* @brief 设置某网格的弱波增强等级(0–1,超出范围会被夹取)。
* @param [IN] laneIndex 网格下标。
* @param [IN] level 增强等级。
* @return 等级发生变化返回 true。
*/
bool SetLaneWeakBoostLevel(int laneIndex, double level);
/**
* @brief 获取弱波增强曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
* @brief 获取弱峰调整曲线(按「信号 + 增强等级 + 迭代次数」缓存)。
* @param [IN] signal 目标信号。
* @param [IN] levelPercent 弱波增强等级(0–100,整数百分比)。
* @param [IN] amountPercent 弱峰调整幅度(-400–400,整数百分比)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
*/
const std::vector<float>& WeakBoostCurveFor(const SignalData& signal, int levelPercent,
const std::vector<float>& WeakPeakCurveFor(const SignalData& signal, int amountPercent,
int iterations) const;
/**
* @brief 取某个时刻的弱波增强曲线取值(用于纵向光标的取值标注)。
* @brief 取某个时刻的弱峰调整曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] levelPercent 弱波增强等级(0–100)。
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
* @param [IN] iterations 迭代次数 N(1–10)。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool WeakBoostValueAt(const DataFile& file, const SignalData& signal, int levelPercent, int iterations,
bool WeakPeakValueAt(const DataFile& file, const SignalData& signal, int amountPercent, int iterations,
double time, double& value) const;
/**
* @brief 切换某个信号在「弱波增强」面板中的预览勾选状态。
* @brief 切换某个信号在「弱峰调整」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 切换后的勾选状态。
*/
bool ToggleLaneWeakBoostSignal(int laneIndex, const SignalRef& ref);
bool ToggleLaneWeakPeakSignal(int laneIndex, const SignalRef& ref);
/**
* @brief 查询某个信号在「弱波增强」面板中的预览勾选状态。
* @brief 查询某个信号在「弱峰调整」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 已勾选返回 true。
*/
bool LaneWeakBoostSignalEnabled(int laneIndex, const SignalRef& ref) const;
bool LaneWeakPeakSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
@@ -946,13 +917,25 @@ namespace jdi
/**
* @brief 将两条光标的时间夹取到当前可见时间范围内。
*
* ⚠️ 现在**只在拖拽光标时**使用。视图变化(平移/放大/自适应)必须**不**移动光标:
* ⚠️ 拖拽光标时使用;**平移**视界后也要用(见 `ClampCursorsAfterPan`)。
* 而**离散**的视图变化(选区放大 / 适应视图 / 按网格时间轴展开)必须**不**移动光标:
* 光标允许滞留在网格可视区域之外,此时仅把它的取值/时间读数贴在网格边缘显示。
*
* @param [IN] which 要夹取的光标序号(1 或 2)。
*/
void ClampCursorToView(int which);
/**
* @brief **平移视界**之后把时间光标夹回可见区间(单/双光标都处理)。
*
* ⚠️ 与「离散视图变化不移动光标」并不矛盾:选区放大 / 适应视图 / 按网格时间轴展开
* 仍然不动光标(用户可以先量好再自由探索,光标读数贴在边缘可读、可拖回来);
* 但**平移**是连续手势 —— 尤其在深放大区间,光标一旦被落在屏外就会立刻"跑飞"
* 很远(一屏的平移就是几十甚至上百倍可视跨度),用户会以为光标丢失。
* 因此在平移(中键/左键拖拽、Shift+滚轮、方向键)之后夹一次。
*/
void ClampCursorsAfterPan();
/**
* @brief 计算双光标之间的时间差(绝对值)。
* @return 时间差;非双光标模式返回 0。
@@ -1089,7 +1072,7 @@ namespace jdi
const std::vector<double>& timeTicks);
/**
* @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱波增强),叠加在原信号之上。
* @brief 绘制某网格的全部**派生预览曲线**(均值滤波 / 中心轴 / 弱峰调整),叠加在原信号之上。
*
* 从 `PaintLane` 抽出,供「宽缓冲区」前台路径复用:宽缓冲只存"基底"(网格/波形),
* 派生曲线每帧作为叠加层单独绘制,因此拖派生参数滑块时无需重画波形。
@@ -1158,21 +1141,31 @@ namespace jdi
gd::RectF close; /**< ✕ 关闭按钮。 */
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
gd::RectF slider; /**< 「弱波增强」等级滑块的轨道矩形(无滑块时宽度为 0)。 */
gd::RectF sliderIterations; /**< 「弱波增强」迭代次数 N 滑块的轨道矩形(无滑块时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
/**
* @brief 「逐条目」面板的矩形(MEAN 与 CENTER 面板共用同一套排布)。
* @brief 「逐条目」面板的矩形(MEAN / CENTER / PEAK 面板共用同一套排布)。
*
* 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序):
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为第二排的两个输入框;
* `rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为**输入框**
*(MEAN / CENTER 使用);`rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
* 未勾选的条目对应的矩形均为空。
*/
std::vector<gd::RectF> rowChecks;
std::vector<gd::RectF> rowPoints;
std::vector<gd::RectF> rowIterations;
std::vector<gd::RectF> rowStats;
/**
* @brief 「弱峰调整」每个被勾选信号自己的两行**滑块**(与 `rowRefs` 同序)。
*
* `rowLevelTracks/rowLevelValues` = 增强等级滑块与它右侧的数值文本;
* `rowIterTracks/rowIterValues` = 迭代次数滑块与它右侧的数值文本
*(**点值数值文本可以把滑块上限改为别的正整数**)。
* 未勾选或非 PEAK 面板时对应元素为空矩形(数组也可能为空)。
*/
std::vector<gd::RectF> rowLevelTracks;
std::vector<gd::RectF> rowLevelValues;
std::vector<gd::RectF> rowIterTracks;
std::vector<gd::RectF> rowIterValues;
std::vector<SignalRef> rowRefs;
};
@@ -1184,6 +1177,14 @@ namespace jdi
*/
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
/**
* @brief 在面板布局里找出某个信号对应的「逐条目」行下标。
* @param [IN] layout 面板布局。
* @param [IN] ref 目标信号。
* @return 行下标(与 `rowRefs` 同序);未找到返回 -1。
*/
int PeakRowIndexOf(const EdgePanelLayout& layout, const SignalRef& ref) const;
/**
* @brief 计算单个面板的缓存键(全局键之外,面板自身的内容)。
* @param [IN] laneIndex 面板所属网格。
@@ -1294,20 +1295,20 @@ namespace jdi
* @brief 取得某个采样区间内的派生曲线(带缓存)。
*
* ⚠️ **性能关键**:派生曲线只用于“看得见的那一段”,而整条信号可能有数千万个采样点
* (VERY_BIG_DATA 为 2576 万)。若每次都重算整条曲线(实测弱波增强 ~800ms/条),
* (VERY_BIG_DATA 为 2576 万)。若每次都重算整条曲线(实测弱峰调整 ~800ms/条),
* 拖动等级/迭代次数滑块时每帧要花几秒钟,交互完全卡死。
* 本函数只计算“所需区间 ± 保护带”,并按(信号 + 参数 + 对齐后的区间)缓存:
* 实测单条降到 1~3ms,滑块拖动与平移因此恢复流畅。
*
* 保护带保证可见区间内的取值与“整条曲线”一致:均值滤波的核是有限宽度,
* 弱波增强的修正沿相邻极值向内传播(每迭代一次传一个极值),
* 弱峰调整的修正沿相邻极值向内传播(每迭代一次传一个极值),
* 因此只要求保护带足够宽(见 `kDerivedCurveGuard` 系列常量)。
*
* @param [IN] kind 曲线种类。
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X(仅 `MEAN_FILTER` 使用)。
* @param [IN] iterations 迭代次数 N。
* @param [IN] levelPercent 弱波增强等级(仅 `WEAK_BOOST` 使用,0–100)。
* @param [IN] amountPercent 弱峰调整幅度(仅 `WEAK_PEAK` 使用,-400–400)。
* @param [IN] begin 需要的采样区间起点(含)。
* @param [IN] end 需要的采样区间终点(不含)。
* @param [OUT] spanBegin 返回数组对应的首个采样下标。
@@ -1318,7 +1319,7 @@ namespace jdi
* @return 曲线数组指针(覆盖 `[spanBegin, spanBegin + size())`);暂无可显示曲线时返回 nullptr。
*/
const std::vector<float>* CurveSpanFor(DerivedCurveKind kind, const SignalData& signal,
int points, int iterations, int levelPercent,
int points, int iterations, int amountPercent,
std::size_t begin, std::size_t end,
std::size_t& spanBegin,
bool allowAsyncCompute = false) const;
@@ -1336,12 +1337,12 @@ namespace jdi
int kind = 0; /**< `DerivedCurveKind`。 */
int points = 5; /**< 均值点数 X。 */
int iterations = 1; /**< 迭代次数 N。 */
int levelPercent = 0; /**< 弱波增强等级(0–100)。 */
int amountPercent = 0; /**< 弱峰调整幅度(-400–400)。 */
bool operator==(const CurveKey& other) const
{
return signal == other.signal && kind == other.kind && points == other.points &&
iterations == other.iterations && levelPercent == other.levelPercent;
iterations == other.iterations && amountPercent == other.amountPercent;
}
};
@@ -1399,11 +1400,19 @@ namespace jdi
/**
* @brief 取得该曲线键下最新的已算好条目并钉住(双缓冲的「旧缓冲」)。
* @param [IN] key 曲线键。
*
* ⚠️ **只按「信号 + 曲线种类」匹配,忽略参数**:拖参数滑块时每帧参数都在变,
* 若要求键完全相同就永远找不到上一帧的曲线,本帧便无曲线可画 → 预览曲线**闪烁**。
* 旧曲线只是参数略旧,按采样下标映射位置依然正确,先顶上再等后台算好新曲线。
*
* @param [IN] key 曲线键(只用其 `signal` 与 `kind`)。
* @param [IN] begin 需要区间起点(用于优先挑「覆盖该区间」的条目)。
* @param [IN] end 需要区间终点(不含)。
* @param [OUT] spanBegin 命中条目的窗口起点。
* @return 命中条目,无则返回 nullptr。
*/
const CurveEntry* AcquireNewestCurve(const CurveKey& key, std::size_t& spanBegin) const;
const CurveEntry* AcquireNewestCurve(const CurveKey& key, std::size_t begin, std::size_t end,
std::size_t& spanBegin) const;
/**
* @brief 发布一条新算好的曲线窗口并钉住。
@@ -1461,15 +1470,15 @@ namespace jdi
int points, int iterations) const;
/**
* @brief 绘制某条信号的弱波增强曲线(与均值滤波曲线同一配色派生规则)。
* @brief 绘制某条信号的弱峰调整曲线(与均值滤波曲线同一配色派生规则)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] levelPercent 弱波增强等级(0–100)。
* @param [IN] amountPercent 弱峰调整幅度(-400–400)。
* @param [IN] iterations 迭代次数 N(1–10)。
*/
void PaintWeakBoostCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int levelPercent, int iterations) const;
void PaintWeakPeakCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int amountPercent, int iterations) const;
/**
* @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
@@ -1685,11 +1694,18 @@ namespace jdi
bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */
int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */
int m_pendingWeakBoostLane = -1; /**< 待构建的弱波增强信号所属网格下标(-1 表示无请求)。 */
int m_pendingWeakPeakLane = -1; /**< 待构建的弱峰调整信号所属网格下标(-1 表示无请求)。 */
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
std::vector<WeakBoostBuild> m_pendingWeakBoostItems; /**< 待构建的弱波增强信号及其参数。 */
int m_boostDragLane = -1; /**< 正在拖动滑块的面板所属网格。 */
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰调整信号及其参数。 */
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
/**
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
*
* 输入框以空内容开始,空内容时显示这个值作为提示;进入编辑时记录,提交后清零。
*/
int m_peakEditCurrentMax = 0;
/**
* @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。
*
@@ -1753,18 +1769,18 @@ namespace jdi
};
mutable std::deque<MeanCurveEntry> m_meanCurveCache;
/** 弱波增强曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */
struct WeakBoostCurveEntry
/** 弱峰调整曲线缓存项(按「信号指针 + 调整幅度百分比 + 迭代次数」标识)。 */
struct WeakPeakCurveEntry
{
const SignalData* signal = nullptr;
int levelPercent = 50; /**< 弱波增强等级(0–100)。 */
int amountPercent = 0; /**< 弱峰调整幅度(-400–400,默认 0 = 不改动)。 */
int iterations = 1; /**< 迭代次数 N(1–10)。 */
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
};
mutable std::deque<WeakBoostCurveEntry> m_weakBoostCurveCache;
mutable std::deque<WeakPeakCurveEntry> m_weakPeakCurveCache;
// ---- 异步派生曲线计算(后台线程 + 双缓冲)----
// 设计:预览曲线(均值滤波 / 弱波增强)在窗口很大时**不在绘制线程里计算**,而是交给后台
// 设计:预览曲线(均值滤波 / 弱峰调整)在窗口很大时**不在绘制线程里计算**,而是交给后台
// 线程;绘制线程本帧先用上一条已算好的曲线顶替(双缓冲),后台算完后通过
// `kMessagePlotDerivedReady` 通知宿主重绘,从而切换为最新曲线。这样滑块 / 光标 / 视界 /
// 缩放等交互的反馈始终跟手,不会被计算拖住,同时把重计算放到后台充分利用多核。
@@ -1911,6 +1927,22 @@ namespace jdi
*/
bool SceneCacheMatches() const;
/**
* @brief **单个网格**的内容指纹(用于该网格自己的「信号图层」)。
*
* 图层位图里只含该网格的内容(底色 + 网格线 + 波形),位图高度 = 该网格高度、
* 内部以 `y=0` 为网格顶边,因此该指纹只需要「网格高度 + 幅值范围 + 本网格各信号的
* 可见性与外观 + 图例文案」。
*
* ⚠️ 这**必须**是逐网格的:早期版本用全局内容指纹,于是一个网格的幅值变化
* 会同时作废**全部**网格的图层(拖拽时纵向分量让 5 个网格排队重画 → 图层内容
* 相互不一致 = 错误绘制;且每帧都入队 → 永远补不完)。
*
* @param [IN] laneIndex 网格下标。
* @return 指纹值。
*/
std::uint64_t LaneLayerHash(int laneIndex) const;
/**
* @brief 计算场景内容指纹(信号集合、显示样式、网格幅值、图例文案)。
*
@@ -1919,16 +1951,22 @@ namespace jdi
*
* @param [IN] includeStyles 是否把逐信号外观样式(颜色/不透明度/线宽/模式)计入指纹。
* @param [IN] includeDerived 是否把派生预览曲线的开关与参数计入指纹。
* @param [IN] includeLaneRange 是否把各网格的**幅值范围**(`yMin/yMax`)计入指纹。
* ⚠️ **拖拽平移时的纵向分量**会每帧小幅修改被拖网格的幅值范围;若它参与
* “宽基底可用性”判据,每一帧都会退化成整幅重绘(质量下降 + 卡顿)。
* 因此宽基底判据用 `includeLaneRange = false` 的几何指纹,
* 幅值变化改由**逐网格图层自己的指纹**处理(只有该网格需要重画)。
* @return 指纹值。
*/
std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true) const;
std::uint64_t SceneContentHash(bool includeStyles = true, bool includeDerived = true,
bool includeLaneRange = true) const;
/**
* @brief 场景内容中**不含逐信号外观样式**的指纹。
*
* 只包含网格结构、视图几何、网格幅值、信号集合与可见性、图例文案、选中项等
* 只包含网格结构、视图几何、信号集合与可见性、图例文案、选中项等
* "变化后必须整幅重建"的内容;逐信号的 `color / opacity / lineWidth / mode`
* 一律不参与。
* 一律不参与,**各网格的幅值范围(`yMin/yMax`)也不参与**(由逐网格图层指纹负责)。
*
* 用途:外观样式变化时(典型场景是拖动自定义颜色的滑块)波形几何完全不变,
* 只要该指纹一致,就可以只重画"颜色变了的那个网格"再写回缓存,
@@ -1941,7 +1979,7 @@ namespace jdi
/**
* @brief 装饰层(图例 + Y 轴刻度)的指纹:含外观样式,但**不含派生曲线参数**。
*
* 拖弱波增强等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化;
* 拖弱峰调整等级/迭代次数滑块时,图例与 Y 轴刻度不变,仅网格内预览曲线变化;
* 因此装饰层缓存不应因参数变化而失效(否则拖滑块每帧都要重绘图例与刻度)。
*
* @return 指纹值。
@@ -1971,20 +2009,18 @@ namespace jdi
*
* 它**只影响网格内容的绘制**(不影响网格结构/视图几何),因此不进入
* `SceneGeometryHash`,而是与外观样式一起作为“局部重画”的判据:
* 拖动弱波增强的等级/迭代次数滑块时,只需重画受影响的网格,
* 拖动弱峰调整的等级/迭代次数滑块时,只需重画受影响的网格,
* 不必整幅重绘(也不会把静态场景缓存整体作废)。
*/
struct SceneDerivedEntry
{
int edgeCenter = 0; /**< 中心轴边沿计数开关。 */
int meanFilter = 0; /**< 均值滤波开关。 */
int weakBoost = 0; /**< 弱波增强开关。 */
int weakBoostLevelPercent = 0; /**< 弱波增强等级(0–100)。 */
int weakBoostIterations = 1; /**< 弱波增强迭代次数 N。 */
/** 各条目展开为 [文件, 信号, 均值点数 X, 迭代次数 N],按原顺序(稳定)。 */
int weakPeak = 0; /**< 弱峰调整开关。 */
/** 各条目展开为 [文件, 信号, 参数…],按原顺序(稳定)。 */
std::vector<int> edgeCenterItems;
std::vector<int> meanFilterItems;
std::vector<int> weakBoostItems;
std::vector<int> weakPeakItems;
};
/**
@@ -2222,20 +2258,25 @@ namespace jdi
mutable int m_decorOriginX = 0;
mutable int m_decorOriginY = 0;
// ---- 每网格「信号图层」:双缓冲 + 后台绘制(前台只做贴图 / 坐标偏移) ----
// ---- 每网格「信号图层」(前台同步维护;交互帧只做贴图 / 坐标偏移) ----
//
// 平移只改变绘制坐标、不改变画面内容。因此**每个网格各自维护一块离屏位图**
// (宽度 = 视界宽 ×3:左右各多渲染半个视界),称为该网格的「信号图层」。
// 图层只含基底(底色 + 网格线 + 波形 + 派生曲线),**不含装饰**(Y 轴刻度 / 图例)——
// (宽度 = 视界宽 + 左右各半个视界的余量),称为该网格的「信号图层」。
// 图层只含基底(底色 + 网格线 + 波形),**不含装饰**(Y 轴刻度 / 图例)——
// 装饰锚定在网格左缘、不随时间视界移动,若烘进图层会被平移带走(重影),
// 因此装饰由前台单独绘制(见 `m_decorCapturedRects` 那套缓存)。
//
// **双缓冲**:每个网格有两块图层(`m_layerFront` 前台读取 / `m_layerBack` 后台填充),
// 后台绘制完成后再整体交换。前台**永远只读 `m_layerFront`**,因此后台正在绘制时
// 前台仍能拿到上一份可用像素,绘制过程不影响交互响应。
// ⚠️ **图层一律在 UI 线程内同步维护**(`Paint` 全程持有 `m_renderMutex`),
// 因为渲染图层会临时改写 `m_area` / `m_plotRect` / `Tab().ViewMin()/ViewMax()` /
// 各网格矩形 —— 这些状态**同时被 UI 线程的消息处理无锁读取**
// (`OnMouseMove` 平移读视界、命中测试读网格矩形)。若把图层渲染搬到后台线程,
// 后台的"保存 → 改写 → 恢复"会**覆盖用户正在进行的平移**(视界被弹回原位),
// 命中测试也会读到错位的网格 → 表现为「拖动时视界跳回 / 波形抖动 / 绘制错乱」。
// 这正是先前"后台图层线程"被移除的原因:**不要把图层渲染移回后台**。
//
// **后台绘制**:跨度 / 几何 / 外观变化时,后台线程池把各网格的新图层画进后台缓冲;
// 若前台已经要刷新而后台还没画好,就继续用旧的前台缓冲「应付」交互,不解切换。
// 每个图层记录自己的时间映射(`LaneLayer::wideMin`):不同时刻渲染出的图层锚点
// 并不相同,滚动与贴图一律**逐网格**按各自的 `wideMin` 计算,**绝不共用一个全局锚点**
// (共用全局锚点会让各网格被平移不同的真实距离 → 网格之间时间错位)。
/**
* @brief 单个网格的「信号图层」(一块只含该网格基底的离屏位图)。
*/
@@ -2245,9 +2286,18 @@ namespace jdi
HBITMAP bitmap = nullptr; /**< 离屏位图。 */
HGDIOBJ oldBitmap = nullptr; /**< DC 原选位图。 */
int width = 0; /**< 位图宽度(含左右各半屏余量)。 */
int height = 0; /**< 位图高度(= 网格高)。 */
int height = 0; /**< 位图高度(含上下各半个网格高的余量)。 */
int marginY = 0; /**< 位图在**可见网格上方**预留的像素行数(纵向预渲染余量)。 */
double wideMin = 0.0; /**< 位图 x=0 对应的时间。 */
double span = 0.0; /**< 渲染时的视界跨度(决定像素/时间)。 */
/**
* @brief 渲染时的**纵向锚点**:该网格的 `yMax`(可见窗口顶行对应的数值)。
*
* 与 `wideMin` 同理,平移(纵向是幅值平移)时只移动**贴图偏移**、不重画像素:
* 需要贴到屏幕顶行的位图行 = `marginY + round((anchorYMax - 当前 yMax) * pxPerValue)`。
*/
double anchorYMax = 0.0;
double pxPerValue = 0.0; /**< 位图纵向像素 / 数值单位(= 可见网格高 / 幅值跨度)。 */
std::uint64_t key = 0; /**< 内容指纹(含跨度、网格几何与外观)。 */
bool valid = false; /**< 是否有可显示的像素。 */
};
@@ -2263,88 +2313,117 @@ namespace jdi
* @param [IN] laneIndex 网格下标。
* @param [IN,OUT] layer 目标图层(会被写入尺寸 / 时间映射 / 指纹)。
* @param [IN] stripX 只渲染位图的这一段 x 区间(滚动时补画细条用)。
* @param [IN] stripW 该区间的宽度;传 0 表示渲染整块。
* @param [IN] stripW 该区间的宽度;传 0 表示横向整块。
* @param [IN] stripY 只渲染位图的这一段 y 区间(纵向滚动补条用)。
* @param [IN] stripH 该区间的高度;传 0 表示纵向整块。
* @param [IN] wideMin 位图 `x=0` 对应的时间。
* **必须与调用方的意图一致**:整块重画传 `LayerDefaultWideMin()`(以当前视界居中);
* 滚动补细条则传 `layer.wideMin`(与该图层已有像素同一映射,否则会留下接缝)。
* @return 渲染成功返回 true。
*/
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW);
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW, int stripY,
int stripH, double wideMin);
/**
* @brief 前台补画各网格的**分界线**(网格顶部那条较重的横线)。
*
* ⚠️ 分界线锚定在网格的**屏幕位置**(`lane.rect.Y`),幅值平移时它**不跟着移动**,
* 因此不能烘进"纵向可平移"的图层里(否则纵向拖动时它会跟着波形跑)。
* 整幅重绘由 `PaintLane` 直接绘制;宽基底快路径(贴图层像素)必须在这里补画一次。
*/
void PaintLaneDividers(gd::Graphics& g) const;
/** @brief 「以当前视界居中」的默认图层映射(位图 `x=0` 对应的时间)。 */
double LayerDefaultWideMin() const;
/** @brief 图层位图的横向余量(左右各半屏 → 可横向平移 ±半屏)。 */
int LayerMarginX() const;
/**
* @brief 图层位图的纵向余量(上下各半个网格高 → 可纵向平移 ±半个网格高)。
*
* ⚠️ 纵向余量让「拖拽平移的纵向分量」(幅值平移)与横向平移一样**只改贴图偏移**,
* 不必每帧重画整块图层 —— 这正是「左右平移流畅、上下平移卡顿」的根因所在。
* 余量越大可连续拖拽的范围越大,但每个横向补条 / 整块重画的代价也按余量线性增长
*(因此取「半个网格高」:一整屏的额外内容已足够,且不拖慢已经流畅的横向平移)。
*/
int LayerMarginY(float laneHeight) const;
/**
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧时使用)。
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧 / 图层缺失时使用)。
* @return 全部成功返回 true。
*/
bool RebuildWideBuffer();
/**
* @brief 让全部图层"滚动"以跟随视界:整数像素自贴图平移 + 只补画新暴露的细条。
* @return 图层仍可用返回 true。
* @brief 让各图层"滚动"以跟随视界:逐网格整数像素自贴图平移 + 只补画新暴露的细条。
*
* ⚠️ **本函数绝不做"整块重画"**:内容指纹过期 / 锚点离视界太远的网格只被排进
* `m_layerRebuildPending`(随后由 `RenderPendingLaneRebuilds` 按预算补画),本帧仍按
* 现有像素做整数像素平移,因此**前台线程永远不会因为"图层没准备好"而卡顿**。
*/
bool RollWideBuffer();
void RollWideBuffer();
/**
* @brief 逐个网格截取图层子矩形贴图到后备缓冲区(O(1) 位图搬运)。
*
* 图层只覆盖"视界 ± 半屏":若当前视界有一部分落在图层之外,这里**只贴有像素的那一段**
* (其余留由底色填充,下一帧补齐),而不是跳过整块 —— 这正是"未绘制的部分先暂时不管"。
* @param [IN,OUT] g 绘图上下文。
* @return 至少贴了若干网格返回 true。
* @return 至少贴了若干像素返回 true。
*/
bool BlitWideBuffer(gd::Graphics& g) const;
/**
* @brief 全部图层是否已就绪且跨度与当前视界一致(**不要求内容也一致**)。
* @brief 全部图层是否**结构可用**(数量 / 位图尺寸 / 视界跨度都与当前状态一致)。
*
* 内容一致与否由后台线程负责跟踪:只要跨度一致、图层能覆盖当前视界,
* 前台就直接用现成像素“应付”交互(内容稍旧的一两帧由后台画好后交换),
* 这样样式/参数变化也不会退化成整幅重绘。
* @return 可以直接贴图层像素返回 true。
* 结构不可用意味着"像素的时间映射已经不对",只能整批重建(跨度变化 / 布局变化 / 首帧);
* 内容指纹是否最新由 `LaneLayerReady()` 逐个判断。
* @return 可以直接滚动 + 贴图层像素返回 true。
*/
bool LayersReadyForView() const;
bool LayersStructurallyReady() const;
/**
* @brief 某个网格的图层是否**完全就绪**(结构可用且内容指纹与当前状态一致)。
* @param [IN] laneIndex 网格下标。
* @return 就绪返回 true。
*/
bool LaneLayerReady(int laneIndex) const;
/** @brief 全部网格图层是否都完全就绪(用于判断能否做"结算"整幅精确帧)。 */
bool LanesAllReadyForView() const;
/** @brief 把某网格排进"待重画"队列(去重;视界/跨度后续变化时会在渲染时再次校验)。 */
void QueueLaneRebuild(int laneIndex);
/**
* @brief 按预算补画"待重画"的网格图层(**本帧最多 `budget` 个**)。
*
* 这是"前台不做整批重绘"的关键:即使 5 个网格都需要重画,也一帧只画一个,
* 其余留到后续帧(缺的部分留空 / 用旧像素)。调用方若发现队列非空应请求再次重绘。
* @param [IN] budget 本次最多重画的网格数量。
* @return 队列中仍有待重画网格返回 true。
*/
bool RenderPendingLaneRebuilds(int budget);
/** @brief 使全部图层失效。 */
void InvalidateWideBuffer();
/** @brief 释放全部图层位图(前后台都释放)。 */
/** @brief 释放全部图层位图。 */
void ReleaseWideBuffer();
std::vector<LaneLayer> m_layerFront; /**< 各网格的「信号图层」(与 `m_lanes` 同序)。 */
/** 待重画的网格下标(去重;`RollWideBuffer` 登记,`RenderPendingLaneRebuilds` 消费)。 */
std::vector<int> m_layerRebuildPending;
/**
* @brief 为"内容与前台图层不一致"的网格排一次后台渲染任务(去重)。
*
* 后台渲染完成后会与前台缓冲交换并通知宿主重绘,前台因此始终有立即可用的像素。
* @brief 图层元数据的互斥(当前图层只在 UI 线程维护,故它实际不承担跨线程保护;
* 保留是为了给 `BlitWideBuffer()` 这类 `const` 读取路径一个明确的一致性边界)。
*/
void ScheduleStaleLayerRenders();
/** @brief 确保后台图层绘制线程已启动。 */
void EnsureLayerWorker();
/** @brief 停止后台图层绘制线程。 */
void StopLayerWorker();
/** @brief 后台图层绘制线程主循环。 */
void LayerWorkerLoop();
/** 单个网格图层任务:只需网格下标(参数在渲染时按最新状态取)。 */
struct LayerJob
{
int laneIndex = -1;
std::uint64_t key = 0;
};
std::vector<LaneLayer> m_layerFront; /**< 前台读取(后台不会就地改写)。 */
std::vector<LaneLayer> m_layerBack; /**< 后台填充。 */
mutable std::mutex m_layerMutex; /**< 保护前后台切换 / 元数据读取 / 锚点(const 方法也要读,故 mutable)。 */
std::recursive_mutex m_renderMutex; /**< 串行化“改写渲染状态”的绘制(前台整帧 vs 后台单网格);可重入(整帧内部会再调图层渲染)。 */
double m_layerAnchorMin = 0.0; /**< 图层滚动锚点:视界左端对应的时间。 */
bool m_layerAnchorValid = false; /**< 锚点是否有效。 */
std::thread m_layerWorker; /**< 后台图层绘制线程。 */
std::mutex m_layerJobMutex; /**< 任务队列互斥。 */
std::condition_variable m_layerJobCv; /**< 任务到来 / 退出通知。 */
std::vector<LayerJob> m_layerJobs; /**< 待渲染的网格任务。 */
bool m_layerWorkerStop = false; /**< 请求退出后台线程。 */
/**
* @brief 连续排了多少次"图层重建"却始终没与前台一致。
*
* 正常情况下一两次就会收敛(后台画完→交换→指纹一致)。若持续增长,说明后台渲染
* 算出的指纹与前台不一致(例如某个参与指纹的状态没被正确还原),此时改为**同步重建**
* 一次强行收敛,避免"前台永远不等后台、后台永远画不好"的死循环。
*/
int m_layerRescheduleStreak = 0;
/** 前台最近一次 `Paint` 的时刻(毫秒,`GetTickCount64`)。后台据此"让路",不与前台抢渲染锁。 */
mutable std::atomic<unsigned long long> m_lastPaintTick{0};
/** 后台线程已完成绘制的图层数量(原子,仅用于验证/诊断)。 */
mutable std::mutex m_layerMutex;
std::recursive_mutex m_renderMutex; /**< 串行化"改写渲染状态"的绘制(整帧 / 单网格图层);可重入。 */
/** 已渲染的图层区域数量(原子,仅用于验证 / 诊断)。 */
mutable std::atomic<std::uint64_t> m_layerRenderCount{0};
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */
/**
* @brief 渲染信号图层时,横向网格线(幅值刻度)的「参考跨度 / 参考高度」。
*
* ⚠️ **必须是前台可见网格的值**,而不是图层画布的(含纵向余量的)值:
* 图层的幅值刻度必须以该跨度为基准取「整齐步长」,否则画布里的网格线密度与
* 整幅渲染不一致 —— 贴图后拖动时网格线会跳变。步长一致时刻度落在同一个
* 绝对值格点上,于是纵向平移就是纯粹的像素平移(逐像素同源)。
*/
double m_wideTickSpan = 0.0;
float m_wideTickHeight = 0.0f;
/**
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
*
@@ -2385,6 +2464,42 @@ namespace jdi
std::uint64_t m_panelGlobalKey = 0; /**< 上一层对应的全局键。 */
std::vector<PanelSlot> m_panelSlots; /**< 上一层缓存的面板(本帧比对用)。 */
/**
* @brief 浮动面板的**层序**(从下到上)。
*
* 面板允许互相重叠,因此必须有明确的层序:
* * **绘制**按此顺序从下往上;**命中测试**从后往前(最上层优先)。
* * 点击某个面板会把它移到队尾(置顶),因此重叠时总能操作到"用户想要"的那一个
* (否则上层面板的 ✕ / 标题栏会被下层控件的命中判定抢走)。
* * 新出现的面板追加到队尾(在最上层),不会被已有面板盖住。
*/
struct PanelRef
{
int laneIndex = -1; /**< 所属网格。 */
int kind = 0; /**< `EdgePanelKind` 的整型值。 */
bool operator==(const PanelRef& other) const
{
return laneIndex == other.laneIndex && kind == other.kind;
}
};
/** 同步 `m_panelOrder` 与当前"可见的面板集合"(新增的追加到最上层;消失的移除)。 */
void SyncPanelOrder() const;
/** @brief 把某个面板移到最上层(点击面板时调用)。 */
void BringPanelToFront(int laneIndex, int kind);
/**
* @brief 从最上层往下找出包含该点的面板。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @param [OUT] laneIndex 命中面板所属网格。
* @param [OUT] kind 命中面板的种类(`EdgePanelKind`)。
* @return 命中返回 true。
*/
bool TopPanelAtPoint(float x, float y, int& laneIndex, int& kind) const;
mutable std::vector<PanelRef> m_panelOrder; /**< 浮动面板层序(队尾 = 最上层;mutable 供 const 光标查询同步层序)。 */
/**
* @brief 查询坐标命中的图例条目。
* @param [IN] x 客户区横坐标。
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+67 -49
View File
@@ -374,13 +374,21 @@ namespace jdi
using EdgeCenterItem = MeanFilterItem;
/**
* @brief 「弱波增强」中某个信号自己的条目。
* @brief 「弱峰调整」中某个信号自己的参数条目。
*
* 目前只用到 `ref`(增强等级属于整个网格,统一保存在 `weakBoostLevel` 中);
* 复用 `MeanFilterItem` 结构是为了与均值滤波/中心轴共享
* 「条目查找 + 信号插入/删除时的引用重映射」等既有实现。
* 与「均值滤波」完全一致:**每个被勾选的信号各自持有参数**
*(调整幅度 -400% – +400% + 迭代次数 N),因此同一网格内不同信号可以使用不同强度。
*
* ⚠️ `iterations` 的**上限由用户自己设定**(`maxIterations`):面板上「迭代次数 N」
* 滑块的量程就是 `1 … maxIterations`,点击滑块右侧的数值可以把该上限改成其它正整数。
*/
using WeakBoostItem = MeanFilterItem;
struct WeakPeakItem
{
SignalRef ref; /**< 目标信号。 */
int amountPercent = 0; /**< 调整幅度(-400–400,默认 0 = 不改动)。 */
int iterations = 1; /**< 迭代次数 N(1 … `maxIterations`)。 */
int maxIterations = 10; /**< 「迭代次数 N」滑块的上限(用户可在面板上修改,恒 ≥ 1)。 */
};
std::wstring name; /**< 网格名称。 */
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
@@ -501,53 +509,46 @@ namespace jdi
double meanFilterPanelY = -1.0;
/**
* @brief 「实验性功能 → 弱波增强」开关。
* @brief 「实验性功能 → 弱峰调整」开关。
*
* 开启后为本网格内**被勾选**的信号额外绘制一条「弱波增强」曲线:
* 开启后为本网格内**被勾选**的信号额外绘制一条「弱峰调整」曲线:
* 识别纹波的波峰/波谷,把“比左右同类极值更弱”的波峰上拉、波谷下拉,
* 并按「弱波增强等级」在左右两个同类极值之间取目标值,使其恢复与周边一致的振幅。
* 并按各信号自己的「增强等级」在左右两个同类极值之间取目标值,使其恢复与周边振幅。
* 这条曲线只是叠加显示,不会作为信号加入左侧列表。
*/
bool weakBoost = false;
/** 弱波增强等级(0–1),0% = 目标取左右极值中较小/较大的那一个,100% = 取另一个。 */
double weakBoostLevel = 0.5;
bool weakPeak = false;
/**
* @brief 弱波增强的迭代次数 N(1–10)。
* @brief 参与弱峰调整预览的信号及其**各自的参数**(只包含被勾选的信号)。
*
* 与「均值滤波」的迭代次数 N 一样表示“把同一处理重复应用 N 次”(每一阶以上一阶为输入):
* 一阶时改定量就已严格等于「等级」对应的目标值;多阶会把相邻弱波互相抬高
* 彼此的基准,从而把较宽的弱波区逐步恢复。
* 参数(调整幅度 -400% – +400% + 迭代次数 N)按信号保存,与「均值滤波」的实现方式一致:
* 同一网格内不同信号可以使用不同的增强强度,也可以分别「构建为新信号」。
*/
int weakBoostIterations = 1;
std::vector<WeakPeakItem> weakPeakItems;
/** 参与弱波增强预览的信号条目(只包含被勾选的信号)。 */
std::vector<WeakBoostItem> weakBoostItems;
/** 取某个信号的弱峰调整参数(未勾选时返回空指针)。 */
const WeakPeakItem* FindWeakPeakItem(const SignalRef& ref) const;
/** 取某个信号的弱波增强条目(未勾选时返回空指针)。 */
const WeakBoostItem* FindWeakBoostItem(const SignalRef& ref) const;
/** 取得或创建某信号的弱峰调整参数(用于勾选)。 */
WeakPeakItem& EnsureWeakPeakItem(const SignalRef& ref);
/** 取得或创建某信号的弱波增强条目(用于勾选)。 */
WeakBoostItem& EnsureWeakBoostItem(const SignalRef& ref);
/** 取消某信号的弱峰调整勾选。 */
void RemoveWeakPeakItem(const SignalRef& ref);
/** 取消某信号的弱波增强勾选。 */
void RemoveWeakBoostItem(const SignalRef& ref);
/** 取某信号的弱波增强勾选状态。 */
bool WeakBoostEnabled(const SignalRef& ref) const
/** 取某信号的弱峰调整勾选状态。 */
bool WeakPeakEnabled(const SignalRef& ref) const
{
return FindWeakBoostItem(ref) != nullptr;
return FindWeakPeakItem(ref) != nullptr;
}
/** 弱波增强浮动面板是否展开。 */
bool weakBoostPanelOpen = true;
/** 弱峰调整浮动面板是否展开。 */
bool weakPeakPanelOpen = true;
/** 弱波增强面板左上角位置(客户区像素;<0 表示自动定位)。 */
double weakBoostPanelX = -1.0;
/** 弱峰调整面板左上角位置(客户区像素;<0 表示自动定位)。 */
double weakPeakPanelX = -1.0;
/** 弱波增强面板左上角纵坐标(<0 表示自动定位)。 */
double weakBoostPanelY = -1.0;
/** 弱峰调整面板左上角纵坐标(<0 表示自动定位)。 */
double weakPeakPanelY = -1.0;
};
/**
@@ -592,33 +593,50 @@ namespace jdi
std::vector<float>& out);
/**
* @brief 「弱波增强」:把比周边更弱的纹波波峰/波谷恢复(自适应弱波增强)。
* @brief 「弱峰调整」:把比周边更弱的纹波波峰/波谷恢复,或(负幅度时)把弱峰削弱。
*
* 本算法**不做任何前置滤波**:需要平滑时用户可以先用「均值滤波」构建一条
* 滤波后的信号,再对它做弱波增强。极值点直接在输入采样上识别。
* 滤波后的信号,再对它做弱峰调整。极值点直接在输入采样上识别。
*
* 「调整幅度」滑动条的量程是 **-400% – +400%**,它按"梯度"同时驱动**三组识别规则**:
* 越宽松的规则参与得越晚,同一滑动条位置上的幅度越小:
* | 滑动条 | 第一类(严格) | 第二类(或) | 第三类(宽松) |
* | --- | --- | --- | --- |
* | 0 – 100% | 0 – 100% | — | — |
* | 100 – 200% | 100 – 200% | 0 – 100% | — |
* | 200 – 300% | 200%(上限) | 100 – 200% | 0 – 100% |
* | 300 – 400% | 200%(上限) | 200%(上限) | 100 – 200% |
* (负值完全对称;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组。)
*
* 单阶处理(全 O(n)):
* 1. **识别趋势与极值**:扫描单调段,方向发生翻转处的上一个采样点即为
* 波峰(先升后降)或波谷(先降后升);波峰与波谷交替出现。
* 2. **判定“弱”**:对第 k 个波峰,取同类的左右相邻波峰(k−2、k+2)。
* 若本波峰低于两者的均值,说明它是被削弱的那个;波谷反之(高于两者均值)。
* 3. **目标值**(`level` = 弱波增强等级,0–1):
* * 波峰:左右两个波峰中较小者为 0%、较大者为 100%,
* 目标 = min + level × (max − min);
* * 波谷:左右两个波谷中较大者为 0%、较小者为 100%,
* 目标 = max − level × (max − min)。
* 2. **三组识别规则**(以波峰 k 为例;`L` = 左侧相邻第一个波峰、`l` = 左侧相邻第一个波谷,
* `r` = 右侧相邻第一个波谷、`R` = 右侧相邻第一个波峰):
* * 第一类:`v < (L+l)/2` **且** `v < (r+R)/2`(两侧均值都要满足);
* * 第二类:`v < (L+l)/2` **或** `v < (r+R)/2`(放宽为"或");
* * 第三类:`v < L` **且** `v < R`(只与左右同类极值比较)。
* 波谷把"低于"换成"高于"、把"且/或"的上下界互换即可。
* 3. **目标值**(每一组的幅度 `a` 都在 -2 – +2,对应其自身的 -200% – +200%):
* * `0 – +100%`:当前值 → 左右**同类**极值中**较温和**的一侧
* (波峰取较低的波峰、波谷取较高的波谷);
* * `+100 – +200%`:该侧 → 左右同类极值的**均值**;
* * `0 – -100%`:当前值 → 相邻**异类**极值中**较近**的一个
* (波峰取较高的波谷、波谷取较低的波峰);
* * `-100 – -200%`:该侧 → 相邻异类极值的**均值**。
* 削弱**不会反向增强**(目标被夹在"不越过当前值"的一侧)。
* 4. **平滑偏移**:极值点本身取其目标值,两侧的数据点按相邻两个极值点的偏移量
* 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱波附近被推回。
* 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱峰附近被推回。
*
* 正常周期的极值不满足条件、偏移量恒为 0,因此输出与输入逐位相同。
* 不属于任何一类规则点的极值偏移量恒为 0,因此输出与输入逐位相同。
*
* @param [IN] values 原始采样值。
* @param [IN] level 弱波增强等级(0–1,超出范围会被夹取)。
* @param [IN] amount 调整幅度(-4 – +4,对应 -400% – +400%,超出范围会被夹取;0 = 完全不动)。
* @param [IN] iterations 迭代次数 N(重复应用次数,至少为 1)。
* @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。
*/
void ComputeWeakWaveBoost(const std::vector<double>& values, double level, int iterations,
std::vector<float>& out);
void ComputeWeakPeakAdjust(const std::vector<double>& values, double amount, int iterations,
std::vector<float>& out);
/**
* @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。
+175 -85
View File
@@ -134,13 +134,11 @@ namespace jdi
bool meanFilterPanelOpen = true;
double meanFilterPanelX = -1.0;
double meanFilterPanelY = -1.0;
bool weakBoost = false;
std::vector<Lane::WeakBoostItem> weakBoostItems;
double weakBoostLevel = 0.5;
int weakBoostIterations = 1;
bool weakBoostPanelOpen = true;
double weakBoostPanelX = -1.0;
double weakBoostPanelY = -1.0;
bool weakPeak = false;
std::vector<Lane::WeakPeakItem> weakPeakItems;
bool weakPeakPanelOpen = true;
double weakPeakPanelX = -1.0;
double weakPeakPanelY = -1.0;
};
// 先记下每个信号原来所在网格的网格自身状态:
@@ -166,13 +164,11 @@ namespace jdi
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
carry.meanFilterPanelX = lane.meanFilterPanelX;
carry.meanFilterPanelY = lane.meanFilterPanelY;
carry.weakBoost = lane.weakBoost;
carry.weakBoostItems = lane.weakBoostItems;
carry.weakBoostLevel = lane.weakBoostLevel;
carry.weakBoostIterations = lane.weakBoostIterations;
carry.weakBoostPanelOpen = lane.weakBoostPanelOpen;
carry.weakBoostPanelX = lane.weakBoostPanelX;
carry.weakBoostPanelY = lane.weakBoostPanelY;
carry.weakPeak = lane.weakPeak;
carry.weakPeakItems = lane.weakPeakItems;
carry.weakPeakPanelOpen = lane.weakPeakPanelOpen;
carry.weakPeakPanelX = lane.weakPeakPanelX;
carry.weakPeakPanelY = lane.weakPeakPanelY;
for (const SignalRef& ref : lane.signals)
{
previousCarry.emplace_back(ref, carry);
@@ -209,13 +205,11 @@ namespace jdi
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
lane.meanFilterPanelX = carry.meanFilterPanelX;
lane.meanFilterPanelY = carry.meanFilterPanelY;
lane.weakBoost = carry.weakBoost;
lane.weakBoostItems = carry.weakBoostItems;
lane.weakBoostLevel = carry.weakBoostLevel;
lane.weakBoostIterations = carry.weakBoostIterations;
lane.weakBoostPanelOpen = carry.weakBoostPanelOpen;
lane.weakBoostPanelX = carry.weakBoostPanelX;
lane.weakBoostPanelY = carry.weakBoostPanelY;
lane.weakPeak = carry.weakPeak;
lane.weakPeakItems = carry.weakPeakItems;
lane.weakPeakPanelOpen = carry.weakPeakPanelOpen;
lane.weakPeakPanelX = carry.weakPeakPanelX;
lane.weakPeakPanelY = carry.weakPeakPanelY;
};
// 收集全部可见信号引用。
@@ -1052,9 +1046,9 @@ namespace jdi
namespace
{
/**
* @brief 「弱波增强」内部使用的极值点(波峰 / 波谷)。
* @brief 「弱峰调整」内部使用的极值点(波峰 / 波谷)。
*/
struct BoostExtremum
struct PeakExtremum
{
std::size_t index = 0; /**< 采样下标。 */
double value = 0.0; /**< 采样值。 */
@@ -1062,18 +1056,34 @@ namespace jdi
};
/**
* @brief 「弱波增强」的单阶处理:把偏弱的波峰上拉、偏弱的波谷下拉。
* @brief 三组识别规则各自的调整幅度(每一组都在 -2 – +2,即面板上的 -200% – +200%)。
*
* 滑动条的 ±400% 会按"梯度"同时驱动三组规则(见 `ComputeWeakPeakAdjust`):
* 越宽松的规则参与得越晚、在同一滑动条位置上的幅度越小。
*/
struct WeakPeakAmounts
{
double first = 0.0; /**< 第一类(严格:两侧均值都要满足)。 */
double second = 0.0; /**< 第二类(其中一侧满足即可)。 */
double third = 0.0; /**< 第三类(更宽松:只与左右同类极值比较)。 */
};
/**
* @brief 「弱峰调整」的单阶处理:把偏弱的波峰上拉 / 偏弱的波谷下拉,
* 或(负幅度)把弱峰本身削弱、压平。
*
* **不做任何前置滤波**(需要平滑时用户可以先用「均值滤波」构建一条滤波后的信号):
* 极值点直接在输入采样上通过单调段的方向翻转识别。
* 极值点直接在输入采样上通过单调段的方向翻转识别。三组识别规则(见 `WeakPeakAmounts`)
* 各自有自己的幅度;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组
*(= 最严格的那一组),这样"越严格越先被推到目标值"。
*
* @param [IN] input 本阶的输入采样值。
* @param [IN] strength 弱波增强等级(0–1,已夹取)。
* @param [IN] amounts 三组规则的调整幅度(各 -2 – +2,已夹取;0 = 该组不动)。
* @param [OUT] output 本阶的输出(长度与 input 相同;原值为 NaN 处保持 NaN)。
* @return 确实有采样点被改动返回 true;完全无弱波时返回 false。
* @return 确实有采样点被改动返回 true;没有任何点需要调整时返回 false。
*/
bool ApplyWeakBoostStage(const std::vector<float>& input, double strength,
std::vector<float>& output, std::vector<BoostExtremum>& extremes)
bool ApplyWeakPeakStage(const std::vector<float>& input, const WeakPeakAmounts& amounts,
std::vector<float>& output, std::vector<PeakExtremum>& extremes)
{
const std::size_t count = input.size();
output.assign(count, std::numeric_limits<float>::quiet_NaN());
@@ -1127,7 +1137,7 @@ namespace jdi
else if (step != direction)
{
// 方向在此翻转:上一个采样点就是极值(原方向向上则为波峰)。
BoostExtremum extreme;
PeakExtremum extreme;
extreme.index = previousIndex;
extreme.value = previousValue;
extreme.type = direction;
@@ -1146,16 +1156,16 @@ namespace jdi
return false;
}
// 2) 逐个极值点判定“是否偏弱”,并算出它自己的目标偏移量。
// 2) 逐个极值点判定它属于哪几类规则点,并算出它自己的目标偏移量。
const std::size_t extremeCount = extremes.size();
thread_local std::vector<double> shiftBuffer;
std::vector<double>& shift = shiftBuffer;
shift.assign(extremeCount, 0.0);
for (std::size_t k = 2; k + 2 < extremeCount; ++k)
{
const BoostExtremum& current = extremes[k];
const BoostExtremum& left = extremes[k - 2];
const BoostExtremum& right = extremes[k + 2];
const PeakExtremum& current = extremes[k];
const PeakExtremum& left = extremes[k - 2];
const PeakExtremum& right = extremes[k + 2];
if (left.type != current.type || right.type != current.type)
{
continue;
@@ -1165,27 +1175,93 @@ namespace jdi
{
continue;
}
const double mean = (left.value + right.value) * 0.5;
const double lower = std::min(left.value, right.value);
const double upper = std::max(left.value, right.value);
double destination = 0.0;
if (current.type > 0)
// 相邻的**异类**极值(波峰旁边的波谷 / 波谷旁边的波峰)。
const PeakExtremum& nearLeft = extremes[k - 1];
const PeakExtremum& nearRight = extremes[k + 1];
if (nearLeft.type == current.type || nearRight.type == current.type)
{
// 波峰:低于左右两个波峰的均值 → 判定为弱波,上拉到等级对应的目标值。
if (!(current.value < mean))
{
continue;
}
destination = lower + strength * (upper - lower);
continue; // 极值序列本应交替;异常时不动这一点
}
if (!std::isfinite(nearLeft.value) || !std::isfinite(nearRight.value))
{
continue;
}
const bool isPeak = (current.type > 0);
// 「左侧/右侧相邻的一个同类极值 + 一个异类极值」的均值。
const double leftMean = (left.value + nearLeft.value) * 0.5;
const double rightMean = (nearRight.value + right.value) * 0.5;
// 同类极值里"较温和"的一侧:波峰取较低者、波谷取较高者。
const double milderSame = isPeak ? std::min(left.value, right.value)
: std::max(left.value, right.value);
const double meanSame = (left.value + right.value) * 0.5;
// 异类极值里"较近"的一个:波峰取较高的波谷、波谷取较低的波峰。
const double nearerOpposite = isPeak ? std::max(nearLeft.value, nearRight.value)
: std::min(nearLeft.value, nearRight.value);
const double meanOpposite = (nearLeft.value + nearRight.value) * 0.5;
// 三组识别规则(以弱峰为例,弱谷把"低于"换成"高于"、max/min 互换):
// 第一类:**同时**低于左右两侧的(相邻同类极值 + 相邻异类极值)均值;
// 第二类:低于其中**任意一侧**的该均值(从"且"放宽为"或");
// 第三类:只与左右**同类**极值比较 —— 低于左右两个同类极值即算弱峰。
const bool firstClass = isPeak ? ((current.value < leftMean) && (current.value < rightMean))
: ((current.value > leftMean) && (current.value > rightMean));
const bool secondClass = isPeak ? ((current.value < leftMean) || (current.value < rightMean))
: ((current.value > leftMean) || (current.value > rightMean));
const bool thirdClass = isPeak ? ((current.value < left.value) && (current.value < right.value))
: ((current.value > left.value) && (current.value > right.value));
// 同时满足多组时取**幅度绝对值最大**的那一组(= 最严格的那一组先被推到目标值)。
double amount = 0.0;
if (firstClass)
{
amount = amounts.first;
}
if (secondClass && std::fabs(amounts.second) > std::fabs(amount))
{
amount = amounts.second;
}
if (thirdClass && std::fabs(amounts.third) > std::fabs(amount))
{
amount = amounts.third;
}
if (amount == 0.0)
{
continue; // 不需要调整
}
double destination = current.value;
if (amount > 0.0)
{
// ---- 正幅度(增强)----
// 0 – 100%:当前值 → 左右**同类**极值中较温和的一侧(波峰取较低者、波谷取较高者);
// 100 – 200%:该侧 → 左右**同类**极值的**均值**。
// ⚠️ 波峰从 `milderSame` 走向 `meanSame`(向上)、波谷同样从 `milderSame`
// 走向 `meanSame`(此时 `meanSame` 可能比 `milderSame` **低**),
// 即两者的插值方向一致(都是"milderSame → meanSame"),无需分别取符号。
destination = (amount <= 1.0)
? (current.value + amount * (milderSame - current.value))
: (milderSame + (amount - 1.0) * (meanSame - milderSame));
}
else
{
// 波谷:高于左右两个波谷的均值 → 判定为弱波,下拉到等级对应的目标值。
if (!(current.value > mean))
// ---- 负幅度(削弱)----
// 0 – -100%:当前值 → 相邻**异类**极值中较近的一个
// (波峰取较高的波谷、波谷取较低的波峰);
// -100 – -200%:该侧 → 相邻异类极值的**均值**。
const double magnitude = std::min(2.0, -amount);
destination = (magnitude <= 1.0)
? (current.value + magnitude * (nearerOpposite - current.value))
: (nearerOpposite +
(magnitude - 1.0) * (meanOpposite - nearerOpposite));
// ⚠️ 削弱**不得反向增强**:目标被夹在"不越过当前值"的一侧。
if (isPeak)
{
continue;
destination = std::min(destination, current.value);
}
else
{
destination = std::max(destination, current.value);
}
destination = upper - strength * (upper - lower);
}
shift[k] = destination - current.value;
}
@@ -1235,8 +1311,8 @@ namespace jdi
}
} // namespace
void ComputeWeakWaveBoost(const std::vector<double>& values, double level, int iterations,
std::vector<float>& out)
void ComputeWeakPeakAdjust(const std::vector<double>& values, double amount, int iterations,
std::vector<float>& out)
{
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
@@ -1247,10 +1323,10 @@ namespace jdi
// 第 1 阶:从原始采样值出发(非有限值保持 NaN)。
// 工作缓冲用线程本地变量复用容量:单条曲线动辄上千万采样,若每次调用都重新分配
// 并零初始化,单条「弱波增强」的耗时会被明显放大(内存分配 + page fault)。
// 并零初始化,单条「弱峰调整」的耗时会被明显放大(内存分配 + page fault)。
thread_local std::vector<float> currentBuffer;
thread_local std::vector<float> nextBuffer;
thread_local std::vector<BoostExtremum> extremesBuffer;
thread_local std::vector<PeakExtremum> extremesBuffer;
std::vector<float>& current = currentBuffer;
current.assign(count, std::numeric_limits<float>::quiet_NaN());
for (std::size_t index = 0; index < count; ++index)
@@ -1263,14 +1339,27 @@ namespace jdi
}
// 「迭代次数 N」与「均值滤波」的迭代次数语义一致:把同一处理**重复应用 N 次**,
// 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「等级」对应的目标值;
// 多次迭代时相邻的弱波会互相抬高彼此的基准,从而把较宽的弱波区逐步恢复。
// 每一阶以上一阶的结果为输入。一阶时改定量已严格等于「幅度」对应的目标值;
// 多次迭代时相邻的弱峰会互相抬高彼此的基准,从而把较宽的弱峰区逐步恢复(或削弱)。
const int stages = std::max(1, std::min(iterations, 1000));
const double strength = std::max(0.0, std::min(1.0, level));
// 滑动条量程 ±400%(`amount` = 百分比 / 100,即 -4 – +4),按"梯度"同时驱动三组规则:
// 第一类:始终参与,幅度 = clamp(|a|, 0, 2);
// 第二类:|a| > 100% 起参与,幅度 = clamp(|a| - 1, 0, 2);
// 第三类:|a| > 200% 起参与,幅度 = clamp(|a| - 2, 0, 2)。
// 于是滑动条 0–100% 只有第一类在动;100–200% 第一类走完 100–200%、第二类开始走 0–100%;
// 200–300% 第一类已到上限、第二类走 100–200%、第三类开始走 0–100%;
// 300–400% 前两类都已到上限,只有第三类在走 100–200%。负值完全对称。
const double clamped = std::max(-4.0, std::min(4.0, amount));
const double magnitude = std::fabs(clamped);
const double sign = (clamped < 0.0) ? -1.0 : 1.0;
WeakPeakAmounts amounts;
amounts.first = sign * std::min(magnitude, 2.0);
amounts.second = sign * std::max(0.0, std::min(2.0, magnitude - 1.0));
amounts.third = sign * std::max(0.0, std::min(2.0, magnitude - 2.0));
std::vector<float>& next = nextBuffer;
for (int stage = 0; stage < stages; ++stage)
{
if (!ApplyWeakBoostStage(current, strength, next, extremesBuffer))
if (!ApplyWeakPeakStage(current, amounts, next, extremesBuffer))
{
// 这一阶没有任何采样点需要调整:后续迭代次数也不会再有变化,提前结束。
break;
@@ -1350,9 +1439,9 @@ namespace jdi
edgeCenterItems.end());
}
const Lane::WeakBoostItem* Lane::FindWeakBoostItem(const SignalRef& ref) const
const Lane::WeakPeakItem* Lane::FindWeakPeakItem(const SignalRef& ref) const
{
for (const WeakBoostItem& item : weakBoostItems)
for (const WeakPeakItem& item : weakPeakItems)
{
if (item.ref == ref)
{
@@ -1362,27 +1451,27 @@ namespace jdi
return nullptr;
}
Lane::WeakBoostItem& Lane::EnsureWeakBoostItem(const SignalRef& ref)
Lane::WeakPeakItem& Lane::EnsureWeakPeakItem(const SignalRef& ref)
{
for (WeakBoostItem& item : weakBoostItems)
for (WeakPeakItem& item : weakPeakItems)
{
if (item.ref == ref)
{
return item;
}
}
WeakBoostItem item;
WeakPeakItem item;
item.ref = ref;
weakBoostItems.push_back(item);
return weakBoostItems.back();
weakPeakItems.push_back(item);
return weakPeakItems.back();
}
void Lane::RemoveWeakBoostItem(const SignalRef& ref)
void Lane::RemoveWeakPeakItem(const SignalRef& ref)
{
weakBoostItems.erase(
std::remove_if(weakBoostItems.begin(), weakBoostItems.end(),
[&ref](const WeakBoostItem& item) { return item.ref == ref; }),
weakBoostItems.end());
weakPeakItems.erase(
std::remove_if(weakPeakItems.begin(), weakPeakItems.end(),
[&ref](const WeakPeakItem& item) { return item.ref == ref; }),
weakPeakItems.end());
}
int FindNearestSample(const DataFile& file, double time)
@@ -1609,17 +1698,20 @@ namespace jdi
}
}
// 均值滤波 / 中心轴边沿计数的条目同样持有信号引用,必须一起删除/前移。
const auto purgeRefs = [fileIndex, signalIndex](std::vector<Lane::MeanFilterItem>& items)
// 均值滤波 / 中心轴边沿计数 / 弱峰调整的条目同样持有信号引用,必须一起删除/前移。
// 三类条目都是「带 `ref` 的结构体」,因此用泛型 lambda 统一处理
//(弱峰调整的参数与均值滤波不同,不能再共用同一个具体类型)。
const auto purgeRefs = [fileIndex, signalIndex](auto& items)
{
using ItemType = typename std::decay_t<decltype(items)>::value_type;
items.erase(std::remove_if(items.begin(), items.end(),
[fileIndex, signalIndex](const Lane::MeanFilterItem& item)
[fileIndex, signalIndex](const ItemType& item)
{
return item.ref.fileIndex == fileIndex &&
item.ref.signalIndex == signalIndex;
}),
items.end());
for (Lane::MeanFilterItem& item : items)
for (ItemType& item : items)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex > signalIndex)
{
@@ -1629,7 +1721,7 @@ namespace jdi
};
purgeRefs(lane.meanFilterItems);
purgeRefs(lane.edgeCenterItems);
purgeRefs(lane.weakBoostItems);
purgeRefs(lane.weakPeakItems);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
@@ -1921,20 +2013,18 @@ namespace jdi
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.weakBoost = source.weakBoost;
lane.weakBoostLevel = source.weakBoostLevel;
lane.weakBoostIterations = source.weakBoostIterations;
lane.weakBoostPanelOpen = source.weakBoostPanelOpen;
lane.weakBoostPanelX = source.weakBoostPanelX;
lane.weakBoostPanelY = source.weakBoostPanelY;
lane.weakPeak = source.weakPeak;
lane.weakPeakPanelOpen = source.weakPeakPanelOpen;
lane.weakPeakPanelX = source.weakPeakPanelX;
lane.weakPeakPanelY = source.weakPeakPanelY;
lane.meanFilter = source.meanFilter;
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱波增强条目。
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整条目。
lane.meanFilterItems.clear();
lane.edgeCenterItems.clear();
lane.weakBoostItems.clear();
lane.weakPeakItems.clear();
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
{
if (item.ref == ref)
@@ -1949,11 +2039,11 @@ namespace jdi
lane.edgeCenterItems.push_back(item);
}
}
for (const Lane::WeakBoostItem& item : source.weakBoostItems)
for (const Lane::WeakPeakItem& item : source.weakPeakItems)
{
if (item.ref == ref)
{
lane.weakBoostItems.push_back(item);
lane.weakPeakItems.push_back(item);
}
}
lane.signals.push_back(ref);
@@ -2010,7 +2100,7 @@ namespace jdi
++item.ref.signalIndex;
}
}
for (Lane::WeakBoostItem& item : lane.weakBoostItems)
for (Lane::WeakPeakItem& item : lane.weakPeakItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{