修复上下拖动后的绘制错误(图层条带 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 零警告。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 18:25:31 +08:00
1 parent ff3319b842
commit 14b60f5d9e
6 files changed
+125 -40

No files matched your search

+41 -12
View File
@@ -56,7 +56,24 @@ namespace jdi
const float kPlotSidePadding = 8.0f; /**< 绘图区左右留白(逻辑像素)。 */
const float kLaneDividerWidth = 2.0f; /**< 网格之间分界线的宽度。 */
const float kSignalHitTolerance = 6.0f; /**< 信号曲线命中容差(逻辑像素)。 */
const float kCursorSnapMinPixels = 6.0f; /**< 光标吸附采样点的最小采样间隔(逻辑像素)。 */
/**
* @brief 两个矩形的交集(空交集返回宽高为 0 的矩形)。
*
* 用于把 GDI 绘制(不受 GDI+ 裁剪约束)限制在"本次真正要重画的区域"内,
* 见 `PaintLane` 里的 `laneClip`。
*/
gd::RectF IntersectRect(const gd::RectF& first, const gd::RectF& second)
{
const float left = std::max(first.X, second.X);
const float top = std::max(first.Y, second.Y);
const float right = std::min(first.X + first.Width, second.X + second.Width);
const float bottom = std::min(first.Y + first.Height, second.Y + second.Height);
if (right <= left || bottom <= top)
{
return gd::RectF(left, top, 0.0f, 0.0f);
}
return gd::RectF(left, top, right - left, bottom - top);
} const float kCursorSnapMinPixels = 6.0f; /**< 光标吸附采样点的最小采样间隔(逻辑像素)。 */
const float kLaneLabelPad = 6.0f; /**< 网格内侧标尺带的内边距。 */
const float kLegendHeight = 19.0f; /**< 图例条目高度(逻辑像素)。 */
const float kLegendPad = 5.0f; /**< 图例相对网格左上角的内缩。 */
@@ -145,12 +162,13 @@ namespace jdi
/**
* @brief 「弱峰调整」滑块行右侧**数值列**的宽度(布局与面板宽度共用同一算式)。
*
* 取「-200%」与「10000」两者较宽者:前者是幅度滑块的最小显示文本(带负号),
* 取「+200%」与「10000」两者较宽者:前者是幅度滑块的最大显示文本
*(**正负号始终显示**,因此要按带符号的最宽形式留位),
* 后者为迭代次数上限留出足够位宽(上限可被用户改大)。
*/
float WeakPeakValueColumnWidth(HFONT monoSmall)
{
return std::max(UiMeasureTextWidth(monoSmall, L"-200%"),
return std::max(UiMeasureTextWidth(monoSmall, L"+200%"),
UiMeasureTextWidth(monoSmall, L"10000"));
}
@@ -4818,7 +4836,7 @@ namespace jdi
if (!m_pendingWeakPeakItems.empty())
{
const WeakPeakBuild& first = m_pendingWeakPeakItems.front();
SetHint(L"正在构建弱峰调整信号(等级 " + std::to_wstring(first.amountPercent) +
SetHint(L"正在构建弱峰调整信号(幅度 " + std::to_wstring(first.amountPercent) +
L"% / " + std::to_wstring(first.iterations) + L" 次迭代,共 " +
std::to_wstring(m_pendingWeakPeakItems.size()) + L" 条)…");
m_pendingWeakPeakLane = index;
@@ -7876,8 +7894,17 @@ namespace jdi
// 与当前裁剪区求交(而不是替换):局部重画(RepaintSceneCachePatch)时
// 外层已经按“需要重画的网格范围”设过裁剪区,这里必须保留,否则会重画到
// 其他网格上。整幅重绘时外层没有裁剪区,等同于原来的行为。
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop),
gd::CombineModeIntersect);
//
// ⚠️ **本次调用只允许改动 `m_area`** —— 它既是"整幅渲染"的绘图区,也是
// "信号图层"的**条带**(横向补条 = 窄 x 区间 × 全高;纵向补条 = 全宽 × 窄 y 区间)。
// GDI 绘制(网格线 / 分界线)**不受 GDI+ 裁剪约束**,所以它们的裁剪矩形必须
// **同时**与 `m_area` 求交:否则纵向补条时横向网格线会被画到条带之外的已正确像素上、
// 覆盖在波形之上("网格线盖住波形"),并**永久烘进图层**、随之后的每次平移一起移动
// —— 正是用户报告的"上下拖动之后,左右/上下/斜向拖动都出现绘制错误"。
const gd::RectF laneClip = IntersectRect(
gd::RectF(m_area.X, m_area.Y, m_area.Width, m_area.Height),
gd::RectF(lane.rect.X, clipTop, lane.rect.Width, clipBottom - clipTop));
g.SetClip(laneClip, gd::CombineModeIntersect);
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
@@ -7888,7 +7915,7 @@ namespace jdi
// 而一张网格有几十条网格线(5 个网格就是数百条),逐条调用会让单帧多花十几毫秒
// —— 这正是"放大后拖动网格视界很卡"的主因。GDI 绘制不受 GDI+ 裁剪约束,
// 因此显式传入裁剪矩形;半透明辅助线先与网格底色混合成不透明色。
const gd::RectF gridClip = gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop);
const gd::RectF gridClip = laneClip;
/** 标尺数值文本的裁剪矩形(GDI 文本需要显式裁剪,否则会越过绘图区)。 */
const gd::RectF clipRectForLabels = gridClip;
const std::uint32_t minorLineColor = UiBlendOver(m_theme->cardBg, WithAlpha(m_theme->gridWeak, kMinorLineAlpha));
@@ -8357,7 +8384,7 @@ namespace jdi
}
if (weakPeakOn)
{
// 每个信号用自己的「增强等级 + 迭代次数」(与面板输入框、构建新信号完全一致)。
// 每个信号用自己的「增强等级 + 迭代次数」(与面板滑块、构建新信号完全一致)。
const Lane::WeakPeakItem* item = tabLane.FindWeakPeakItem(ref);
if (item != nullptr)
{
@@ -9387,7 +9414,7 @@ namespace jdi
// 逐条目面板:每个条目 = 第一排(信号名 + 复选框)
// + 参数部分(**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化):
// * MEAN / CENTER:一行「均值点数 X + 迭代次数 N」输入框(中心轴再多一行统计结果);
// * PEAK:两行滑块(增强等级 % / 迭代次数 N),每个信号各有一套自己的参数。
// * PEAK:两行滑块(调整幅度 % / 迭代次数 N),每个信号各有一套自己的参数。
const bool itemHasParams = (centerAxis || meanFilter || weakPeak);
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
float meanRowHeight = 0.0f;
@@ -10242,7 +10269,7 @@ namespace jdi
const Lane::EdgeCenterItem* centerItem = lane.FindEdgeCenterItem(ref);
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->points + 2 : 0));
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->iterations + 2 : 0));
// 弱峰调整的逐信号参数(增强等级 % + 迭代次数 + 该信号的滑块上限)直接显示在面板上:
// 弱峰调整的逐信号参数(调整幅度 % + 迭代次数 + 该信号的滑块上限)直接显示在面板上:
// 上限决定迭代次数滑块的圆钮位置,因此也必须参与缓存键。
const Lane::WeakPeakItem* peakItem = lane.FindWeakPeakItem(ref);
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->amountPercent + 2 : 0));
@@ -10573,7 +10600,7 @@ namespace jdi
: nullptr;
// 「弱峰调整」:每个被勾选的信号各有两行**滑块**
//(增强等级 0–200% / 迭代次数 1 … 该信号自定义上限)。
//(调整幅度 -200% – +200% / 迭代次数 1 … 该信号自定义上限)。
// 迭代次数行的右侧数值可点击 → 整行变为输入框以修改滑块上限。
if (weakPeak)
{
@@ -10624,7 +10651,9 @@ namespace jdi
1.0) *
0.5;
paintSliderBody(levelTrack, levelRatio,
std::to_wstring(peakItem->amountPercent) + L"%", levelActive);
(peakItem->amountPercent > 0 ? L"+" : L"") +
std::to_wstring(peakItem->amountPercent) + L"%",
levelActive);
// 迭代次数滑块:量程 = 1 … 该信号自己的上限。
const int peakMax = std::max(kWeakPeakIterationsMin,