修复上下拖动后的绘制错误(图层条带 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:
1 parent
ff3319b842
commit
14b60f5d9e
6 files changed
+125
-40
No files matched your search
@@ -258,7 +258,7 @@ namespace jdi
|
||||
int signalIndex = -1; /**< 原信号下标。 */
|
||||
int points = 5; /**< 多次迭代均值滤波:均值点数 X(滑平半径)。 */
|
||||
int iterations = 1; /**< 多次迭代均值滤波:迭代次数 N(至少 1)。 */
|
||||
int amountPercent = 150; /**< 弱峰调整:增强等级(0–200)。 */
|
||||
int amountPercent = 0; /**< 弱峰调整:调整幅度(-200–200,默认 0)。 */
|
||||
bool weakPeak = false; /**< true = 弱峰调整,false = 多次迭代均值滤波。 */
|
||||
};
|
||||
|
||||
|
||||
@@ -577,7 +577,7 @@ namespace jdi
|
||||
{
|
||||
int fileIndex = -1; /**< 信号所属文件下标。 */
|
||||
int signalIndex = -1; /**< 文件内信号下标。 */
|
||||
int amountPercent = 150; /**< 弱峰调整等级(0–200)。 */
|
||||
int amountPercent = 0; /**< 弱峰调整幅度(-200–200,默认 0 = 不改动)。 */
|
||||
int iterations = 1; /**< 迭代次数 N(1–10)。 */
|
||||
};
|
||||
|
||||
@@ -1769,11 +1769,11 @@ namespace jdi
|
||||
};
|
||||
mutable std::deque<MeanCurveEntry> m_meanCurveCache;
|
||||
|
||||
/** 弱峰调整曲线缓存项(按「信号指针 + 增强等级百分比 + 迭代次数」标识)。 */
|
||||
/** 弱峰调整曲线缓存项(按「信号指针 + 调整幅度百分比 + 迭代次数」标识)。 */
|
||||
struct WeakPeakCurveEntry
|
||||
{
|
||||
const SignalData* signal = nullptr;
|
||||
int amountPercent = 150; /**< 弱峰调整等级(0–200)。 */
|
||||
int amountPercent = 0; /**< 弱峰调整幅度(-200–200,默认 0 = 不改动)。 */
|
||||
int iterations = 1; /**< 迭代次数 N(1–10)。 */
|
||||
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -515,6 +515,7 @@ Documents/ 项目文档(含本文件)
|
||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"斜向/不规则拖拽自由移动视界时的卡顿、掉帧与波形错误绘制"、"预览曲线不如普通信号细腻(无抗锯齿)"、"拖动参数滑块时预览曲线跳帧闪烁"**。① 根因:拖拽平移的**纵向分量**会逐帧小改被拖网格的 `yMin/yMax`,而**网格幅值范围同时参与「宽基底可用性」几何指纹与每个网格的图层键**,于是每帧都退化成整幅重绘(`m_fastFrame` 关抗锯齿)+ 5 个网格全部入队(一帧只补 1 个)→ 质量下降、卡顿、图层相互不一致(断线)。修法:`SceneContentHash` 新增 `includeLaneRange`(`SceneGeometryHash()` = `false,true,false`,幅值不进"能不能走快路径"的判定),幅值改由**逐网格** `LaneLayerHash(laneIndex)`(`LaneLayerKey` 的基)承担 → 只让真正变化的那个网格失效;并把 `RenderPendingLaneRebuilds(1)` **移到 `BlitWideBuffer` 之前**(本帧补好的网格本帧就可见)。② 预览曲线抗锯齿不再因交互(`m_fastFrame`)关闭(仅保留"陡峭段过多"这一条形同无声的逃生口)。③ `AcquireNewestCurve` 改为**只按「信号 + 曲线种类」匹配**(参数无关,优先挑覆盖所需区间的条目)→ 拖参数滑块时用上一帧的曲线顶替,后台算好再无缝替换,**不再闪烁/消失**。离屏验证:斜向拖拽 40/40 帧走宽基底快路径(中位 9.2ms,无尖峰),参数连改 10 次 **0 个空白帧**,抗锯齿回归断言(快帧混淆边缘像素 8149/8404);`uitest` **420/420 通过**、`panperf` PASS、`layercheck` 保持"0 次视界覆盖 / 平移对齐 ≤1px"。 |
|
||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"深放大下左右平移流畅、上下/斜向平移卡顿"(用户猜测:上下没有预渲染机制——已证实)与"深放大时拖拽光标跑飞"**。① 量化复现:`panperf` 新增纯竖直拖拽 + 逐帧统计**图层重画次数**,并在 5 网格全开 4 类悬浮面板(20 个)后对比 —— 竖直拖拽中位 **166.6ms/帧**、水平 **37.7ms/帧**(4.4 倍差距,用户报告的"上下卡顿"正是它)。② 主因是**面板像素图层被幅值范围击穿**:`PanelGlobalKey()` 计入 `visual.yMin/yMax`,平移的纵向分量每帧小改幅值 → 整层 20 个面板每帧重画(去掉后 166.6→**24.8ms**);次因是**图层没有纵向预渲染余量**(幅值一变就整块重画,实测 40 帧里 40 次 → 修复后 **1 次**)。③ 修法:`LaneLayer` 增加 `marginY/anchorYMax/pxPerValue`,幅值范围按**像素行**精确扩展(上下各半个网格高)、贴图时按 `(anchorYMax - 当前 yMax) * pxPerValue` 取纵向偏移,图层指纹只计幅值**跨度**不计绝对范围;幅值刻度改以**前台可见网格**的跨度/高度取 `NiceStep`(保证与整幅渲染逐像素同源);网格分界线移出图层(改由宽基底路径的 `PaintLaneDividers()` 补画,否则纵向偏移会把它带走)。④ 光标:深放大下平移会把时间光标甩到视界外很远(像"跑飞/丢失")—— 新增 `ClampCursorsAfterPan()`,在**平移**(中键/左键拖拽、Shift+滚轮、方向键)之后夹取;**离散**的视图变化(选区放大/适应视图/按网格时间轴展开)仍然不移动光标;抓取被钉在边缘的光标读数时不再沿用抓取偏移(否则首帧会跳到视界外再被夹回边缘)。验证:`uitest` **432/432**(新增 `[AI]`:贴边读数抓取后光标落在鼠标处误差 0.0px、中键平移 1.5 屏 0 帧出界且跟手)、`panperf` 竖直 3.8~5.0ms(40 帧仅 1 次图层重画)、水平 6.5ms 持平、`layercheck` 0 视界覆盖、新增探针 `.preflight/cursorcheck` 30/30。 |
|
||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"纵向预渲染带出的绘制异常/抽搐"(中键上下拖动、尤其拖到网格之外时),并把「弱峰增强」升级为「弱峰调整」(幅度 -200% – +200%,负值削弱)**。① 绘制异常根因:上一轮给图层加了纵向余量,但"偏移超出余量"仍走**排队整块重画**(一帧只补 1 个网格)→ 重画完成前 `BlitWideBuffer` 只能贴出部分行、**其余留空**(网格内出现空白条带),且重画与贴图的锚点不同源 = "波形没按预期移动";鼠标拖到网格外很远时每帧都触发 → 抽搐。修法:与横向**同构的纵向滚动**(BitBlt 整块滚动 + 只补画暴露的一整条,`anchorYMax -= roll/pxPerValue`),并且**必须先于横向补条**;滚动量**量化**(`kLayerRollQuantum = 16` 像素,不足量子交给贴图 `srcY` 偏移)以保持画布映射稳定、让折线缓存命中(未量化时每帧滚 1~30 行 → 折线每帧重算)。验证:`panperf` 拖出网格 900px 时被拖网格的**空白行数与整幅重绘逐帧一致**(64 vs 64,即无空白条带),竖直拖拽 16 帧仅 2 次图层重画。② 弱峰调整:`ComputeWeakPeakAdjust`(原 `ComputeWeakPeakBoost`)+ `WeakPeakItem.amountPercent`(-200–200,默认 150),面板「增强等级 %」→「调整幅度 %」、滑块量程 -200%–+200%(**0 在正中间**)、数值列加宽以容纳 "-200%";识别规则与旧实现完全一致,正幅度段与旧实现**逐位相同**,负幅度段改为以相邻**异类**极值(波峰看相邻波谷)为参照:-100% 拉到**较近**的那个、-200% 拉到较远的那个(压平/抹除,左右平滑过渡),且**不得反向增强**。离屏验证:`uitest` **443/443**(正幅度 0/50/100/150/200% → 550/555/560/580/600 逐位保持;负幅度 -50/-100/-200% → 峰 500/450/400、谷 500/550/560;超范围夹取;健康峰不动)、`panperf`/`layercheck`/`cursorcheck` 全过。 |
|
||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"上下拖动之后,左右/上下/斜向拖动都出现绘制错误"(真因:图层条带渲染时 GDI 网格线没被限制在条带内),并调整「弱峰调整」的弱峰/弱谷定义、默认幅度与数值符号显示**。① 根因:`PaintLane` 的 GDI 网格线裁剪矩形只限制了横向(`RectF(m_area.X, clipTop, m_area.Width, …)`),而**纵向补条是全宽 × 窄 y** —— GDI 绘制不受 GDI+ 裁剪约束,于是横向网格线被画到条带之外的已正确像素上、**覆盖在波形之上**,并**永久烘进图层**,随后每次平移都带着它一起漂移(用户看到"到处都有绘制错误")。修法:`laneClip = IntersectRect(m_area, 网格可见矩形)` 同时用于 `SetClip` 与 GDI 裁剪。② 新增离屏探针 `.preflight/dragcheck`(真文件 + 真中键拖拽消息:先上拖半个网格高,再左右/上下/斜向、最后拖出窗口;每帧把图层画布的有效条带与"同一 `wideMin` 的新渲染画布"逐像素比较):修复前被拖网格整行不一致(2410px ≈ 2 行全宽),修复后 ≤171px(仅补条边界的抗锯齿边缘)。③ 弱峰调整:**「是否偏弱」改为**"波峰必须同时低于左右两侧各自的(相邻波峰+相邻波谷)均值;波谷反之";默认幅度 **0%**(不改动);滑块右侧数值**始终带符号**(`+150%` / `-150%` / `0%`,验证 +150% 与 -150% 墨迹宽度一致且都比 `0%` 宽 ≥8px)。验证:`uitest` **441/441**、`dragcheck`/`panperf`/`layercheck`/`cursorcheck` 全过、`make clean && make all` 零警告。 |
|
||||
|
||||
---
|
||||
|
||||
@@ -2719,3 +2720,46 @@ Documents/ 项目文档(含本文件)
|
||||
- `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**; 超范围(+7 / -7)夹取;"未反向增强"(极值点)与"健康峰不动"; 滑块:中间 = 0%、右端(拖出)= +200%、左端(拖出)= -200%;构建信号名含「弱峰调整」; `[AH]` 纯竖直拖拽 16 帧仅 **2** 次图层重画(量化滚动)。
|
||||
- `panperf`:拖出网格 900px(鼠标远在窗口外)+ 对角平移 31 帧,**被拖网格的空白行数与整幅重绘逐帧一致** (64 vs 64 → 无空白条带)、帧时 10~13ms(滚动 2~4ms)、末帧与整幅重绘的差异与水平基线同量级 (best integer shift (-1,0),即已知的 ≤0.5px 整数像素残差)。
|
||||
- `layercheck`:`clobbers=0/24`、`pan frames wrong=0/4`、居中图层 vs 整幅重绘逐像素 0 差异; `cursorcheck` 30/30;`make clean && make all` 零警告。
|
||||
|
||||
---
|
||||
|
||||
## 本轮(2026-10-12,起源设备):图层条带 GDI 裁剪(绘制错误真因)+「弱峰调整」定义与默认值
|
||||
|
||||
用户报告:① 上下拖动变流畅了,但**绘制错误仍在**——对某个网格中键上拖半个网格的纵向视界后,按住中键不放再左右/上下/斜向拖拽**都会**出现绘制错误;② 调整「弱峰调整」的判定定义与默认值。
|
||||
|
||||
### A. 绘制错误真因:图层条带渲染时 GDI 网格线"越界作画"并烘进图层
|
||||
|
||||
上一轮把图层条带渲染从"只有横向条"扩展到"横向条 + **纵向条**"(纵向滚动补画)。但 `PaintLane` 里 **GDI**(网格线 / 分界线)用的裁剪矩形是
|
||||
`RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop)` —— 它只把 x 限制在条带内,**y 仍是整个网格可见范围**。于是纵向条带(全宽 × 窄 y)渲染时:
|
||||
|
||||
1. GDI 网格线被画到**条带之外**(该处像素由滚动而来、本来是正确的);
|
||||
2. 那些行原本是"波形盖在网格线之上",现在变成"网格线盖在波形之上" → 与正确渲染不一致;
|
||||
3. 更糟的是这些错误像素**就在图层位图里**,会被后续每次平移带着一起移动 → 用户看到"拖到哪儿都有绘制错误"(错误行随内容漂移,无法通过重绘恢复)。
|
||||
|
||||
修法:把"本次允许改动的区域"算成 `laneClip = IntersectRect(m_area, 网格可见矩形)`,**同时**用于 `Graphics::SetClip` 与 GDI 的 `gridClip`。(横向补条本来就没问题,这也是为什么此前只有纵向/斜向才暴露。)
|
||||
|
||||
### B. 复现与验证(`.preflight/dragcheck`)
|
||||
|
||||
完全照用户手势:注入**真实中键消息**(`WM_MBUTTONDOWN` + `WM_MOUSEMOVE`),先把最上方网格向上拖半个网格高(触发纵向滚动),再保持按住拖左右/上下/斜向、最后拖出窗口。
|
||||
判定方式(去掉亚像素噪声):把图层画布的**有效条带**(`marginY + vOffset` 起、网格高行)与"用同一 `wideMin` 新渲染的画布"逐像素比较 —— 滚动是纯整数平移,二者必须逐像素相同。(用 `Bitmap::FromHBITMAP` + `LockBits` 直接读像素,`GetPixel` 太慢。)
|
||||
|
||||
- 修复前:被拖网格出现**整行不一致**(2410 px ≈ 2 行 × 全宽),其他网格只有 ~20 px;
|
||||
并且**错误行固定在 canvas 行 138**(即某条横向网格线的行号)——直接指向 GDI 网格线。
|
||||
- 修复后:最差 ≤171 px(仅补条边界处的抗锯齿边缘,占 0.09%),0 处绘制错误。
|
||||
|
||||
### C. 「弱峰调整」的定义、默认幅度与符号显示
|
||||
|
||||
- **「是否偏弱」的新定义**(正/负幅度一致,用户明确要求):
|
||||
* 波峰 k:必须**同时**低于
|
||||
左侧「相邻第一个波峰 k-2 与相邻第一个波谷 k-1 的均值」**且**
|
||||
右侧「相邻第一个波谷 k+1 与相邻第一个波峰 k+2 的均值」;
|
||||
* 波谷 k:必须**同时**高于上述两个均值。
|
||||
⚠️ 与旧实现(只与左右两个**同类**极值比较)不同:**整段均匀偏弱的纹波不再被判为弱峰**(它两侧没有"更健康"的组合可比),因此测试数据必须重设计(新的合成波形为分段线性、谷恒 500、弱峰 520、健康峰 620/640)。
|
||||
- **默认幅度改为 0%**(面板打开即不改动波形;`WeakPeakItem`/构建请求/曲线缓存项三处默认值)。
|
||||
- **滑块右侧数值始终显示符号**:`+150%` / `-150%` / `0%`(0 无符号);数值列按 `"+200%"` 取宽,避免文字跳动。离屏验证:`+150%` 与 `-150%` 的墨迹宽度一致(40 / 38 px),且都比 `0%`(16 px)宽 ≥8px。
|
||||
|
||||
### D. 验证(全部离屏,不占用鼠标/键盘/屏幕)
|
||||
|
||||
- `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%、数值带符号。
|
||||
- `[Y]` 端到端:合成 ripple(谷恒 500、一个弱峰 520)弱峰区间 **20 → 70**(健康 120)。
|
||||
- `dragcheck` PASS(0 绘制错误);`panperf` 竖直中位 5.1ms、水平 7.1ms、面板全开竖直 25.0ms / 水平 41.3ms;`layercheck` `clobbers=0/24`、`pan frames wrong=0/4`;`cursorcheck` 30/30;`make clean && make all` 零警告。
|
||||
@@ -385,7 +385,7 @@ namespace jdi
|
||||
struct WeakPeakItem
|
||||
{
|
||||
SignalRef ref; /**< 目标信号。 */
|
||||
int amountPercent = 150; /**< 调整幅度(-200–200;0 = 不动,100% = 到达较温和一侧,200% = 完全拉平;负值为削弱)。 */
|
||||
int amountPercent = 0; /**< 调整幅度(-200–200,默认 0 = 不改动)。 */
|
||||
int iterations = 1; /**< 迭代次数 N(1 … `maxIterations`)。 */
|
||||
int maxIterations = 10; /**< 「迭代次数 N」滑块的上限(用户可在面板上修改,恒 ≥ 1)。 */
|
||||
};
|
||||
@@ -601,18 +601,20 @@ namespace jdi
|
||||
* 单阶处理(全 O(n)):
|
||||
* 1. **识别趋势与极值**:扫描单调段,方向发生翻转处的上一个采样点即为
|
||||
* 波峰(先升后降)或波谷(先降后升);波峰与波谷交替出现。
|
||||
* 2. **判定“弱”**:对第 k 个波峰,取同类的左右相邻波峰(k−2、k+2)。
|
||||
* 若本波峰低于两者的均值,说明它是被削弱的那个;波谷反之(高于两者均值)。
|
||||
* 这一步在正、负幅度下**完全一致**(用户要求「弱峰的识别定义与之前相同」)。
|
||||
* 2. **判定“弱”**(用户明确定义,正/负幅度一致;与旧实现不同):
|
||||
* * 波峰 k 必须**同时**低于「左侧相邻的第一个波谷(k-1) 与第一个波峰(k-2) 的均值」
|
||||
* 与「右侧相邻的第一个波谷(k+1) 与第一个波峰(k+2) 的均值」;
|
||||
* * 波谷 k 必须**同时**高于上述两个均值。
|
||||
* 即与“两侧各自相邻的一个波峰 + 一个波谷”的平均水平比较。
|
||||
* 3. **目标值**(`amount` = 调整幅度,-2 – +2,对应面板上的 -200% – +200%):
|
||||
* * **正幅度(增强,与原实现逐位一致)**:以**同类**极值为参照。
|
||||
* * **正幅度(增强)**:以**同类**极值(k±2)为参照。
|
||||
* 波峰:较小者记为 `lower`、较大者记为 `upper`;
|
||||
* 0 ≤ a ≤ 1 → 目标 = 当前值 + a × (`lower` − 当前值);
|
||||
* 1 < a ≤ 2 → 目标 = `lower` + (a − 1) × (`upper` − `lower`)。
|
||||
* 波谷:较大者记为 `upper`、较小者记为 `lower`;
|
||||
* 0 ≤ a ≤ 1 → 目标 = 当前值 + a × (`upper` − 当前值);
|
||||
* 1 < a ≤ 2 → 目标 = `upper` − (a − 1) × (`upper` − `lower`)。
|
||||
* * **负幅度(削弱)**:以相邻的**异类**极值(波峰的相邻波谷 / 波谷的相邻波峰)为参照。
|
||||
* * **负幅度(削弱)**:以相邻的**异类**极值(k±1)为参照。
|
||||
* 令 `near` = 较靠近当前值的那个(波峰 → 较高的波谷;波谷 → 较低的波峰)、
|
||||
* `far` = 另一个:
|
||||
* -1 ≤ a < 0 → 目标 = 当前值 + |a| × (`near` − 当前值);
|
||||
@@ -625,7 +627,7 @@ namespace jdi
|
||||
* 正常周期的极值不满足条件、偏移量恒为 0,因此输出与输入逐位相同。
|
||||
*
|
||||
* @param [IN] values 原始采样值。
|
||||
* @param [IN] amount 调整幅度(-2 – +2,对应 -200% – +200%,超出范围会被夹取)。
|
||||
* @param [IN] amount 调整幅度(-2 – +2,对应 -200% – +200%,超出范围会被夹取;0 = 完全不动)。
|
||||
* @param [IN] iterations 迭代次数 N(重复应用次数,至少为 1)。
|
||||
* @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。
|
||||
*/
|
||||
|
||||
+27
-17
@@ -1160,10 +1160,30 @@ 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);
|
||||
if (!(current.type > 0 ? (current.value < mean) : (current.value > mean)))
|
||||
// 「是否偏弱」的判定(用户明确定义,正/负幅度**完全一致**):
|
||||
// 波峰:必须**同时**低于
|
||||
// 「左侧相邻的第一个波谷与第一个波峰的均值」(= k-1 与 k-2 的均值) 与
|
||||
// 「右侧相邻的第一个波谷与第一个波峰的均值」(= k+1 与 k+2 的均值);
|
||||
// 波谷:必须**同时**高于上述两个均值。
|
||||
// 即:与"两侧各自相邻的一个波峰+一个波谷"的平均水平比较,
|
||||
// 而不是旧实现只与左右两个**同类**极值比较。
|
||||
const PeakExtremum& leftNear = extremes[k - 1];
|
||||
const PeakExtremum& rightNear = extremes[k + 1];
|
||||
if (leftNear.type == current.type || rightNear.type == current.type)
|
||||
{
|
||||
continue; // 极值序列本应交替;异常时不动这一点
|
||||
}
|
||||
if (!std::isfinite(leftNear.value) || !std::isfinite(rightNear.value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double leftMean = (left.value + leftNear.value) * 0.5;
|
||||
const double rightMean = (rightNear.value + right.value) * 0.5;
|
||||
const bool weak = (current.type > 0) ? ((current.value < leftMean) && (current.value < rightMean))
|
||||
: ((current.value > leftMean) && (current.value > rightMean));
|
||||
if (!weak)
|
||||
{
|
||||
// 不是弱峰:本极值点不动。
|
||||
continue;
|
||||
@@ -1194,22 +1214,12 @@ namespace jdi
|
||||
else
|
||||
{
|
||||
// ---- 负幅度(削弱)----
|
||||
// 参照物改为相邻的**异类**极值(波峰看相邻波谷、波谷看相邻波峰),
|
||||
// 参照物为相邻的**异类**极值(波峰看相邻波谷、波谷看相邻波峰),
|
||||
// 并且"较近的那个"对波峰是较高的波谷、对波谷是较低的波峰。
|
||||
const PeakExtremum& nearLeft = extremes[k - 1];
|
||||
const PeakExtremum& nearRight = extremes[k + 1];
|
||||
if (nearLeft.type == current.type || nearRight.type == current.type)
|
||||
{
|
||||
continue; // 极值序列本应交替;异常时不动这一点
|
||||
}
|
||||
if (!std::isfinite(nearLeft.value) || !std::isfinite(nearRight.value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double nearValue = (current.type > 0) ? std::max(nearLeft.value, nearRight.value)
|
||||
: std::min(nearLeft.value, nearRight.value);
|
||||
const double farValue = (current.type > 0) ? std::min(nearLeft.value, nearRight.value)
|
||||
: std::max(nearLeft.value, nearRight.value);
|
||||
const double nearValue = (current.type > 0) ? std::max(leftNear.value, rightNear.value)
|
||||
: std::min(leftNear.value, rightNear.value);
|
||||
const double farValue = (current.type > 0) ? std::min(leftNear.value, rightNear.value)
|
||||
: std::max(leftNear.value, rightNear.value);
|
||||
const double magnitude = std::min(2.0, -amount);
|
||||
destination = (magnitude <= 1.0)
|
||||
? (current.value + magnitude * (nearValue - current.value))
|
||||
|
||||
Reference in new issue
Block a user