上下/斜向平移卡顿(图层与面板都缺纵向预渲染)+ 深放大拖拽光标跑飞
用户报告: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 零警告。
This commit is contained in:
1 parent
0afa6360ae
commit
cb030573fa
3 files changed
+308
-35
No files matched your search
@@ -917,13 +917,25 @@ namespace jdi
|
|||||||
/**
|
/**
|
||||||
* @brief 将两条光标的时间夹取到当前可见时间范围内。
|
* @brief 将两条光标的时间夹取到当前可见时间范围内。
|
||||||
*
|
*
|
||||||
* ⚠️ 现在**只在拖拽光标时**使用。视图变化(平移/放大/自适应)必须**不**移动光标:
|
* ⚠️ 拖拽光标时使用;**平移**视界后也要用(见 `ClampCursorsAfterPan`)。
|
||||||
|
* 而**离散**的视图变化(选区放大 / 适应视图 / 按网格时间轴展开)必须**不**移动光标:
|
||||||
* 光标允许滞留在网格可视区域之外,此时仅把它的取值/时间读数贴在网格边缘显示。
|
* 光标允许滞留在网格可视区域之外,此时仅把它的取值/时间读数贴在网格边缘显示。
|
||||||
*
|
*
|
||||||
* @param [IN] which 要夹取的光标序号(1 或 2)。
|
* @param [IN] which 要夹取的光标序号(1 或 2)。
|
||||||
*/
|
*/
|
||||||
void ClampCursorToView(int which);
|
void ClampCursorToView(int which);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief **平移视界**之后把时间光标夹回可见区间(单/双光标都处理)。
|
||||||
|
*
|
||||||
|
* ⚠️ 与「离散视图变化不移动光标」并不矛盾:选区放大 / 适应视图 / 按网格时间轴展开
|
||||||
|
* 仍然不动光标(用户可以先量好再自由探索,光标读数贴在边缘可读、可拖回来);
|
||||||
|
* 但**平移**是连续手势 —— 尤其在深放大区间,光标一旦被落在屏外就会立刻"跑飞"
|
||||||
|
* 很远(一屏的平移就是几十甚至上百倍可视跨度),用户会以为光标丢失。
|
||||||
|
* 因此在平移(中键/左键拖拽、Shift+滚轮、方向键)之后夹一次。
|
||||||
|
*/
|
||||||
|
void ClampCursorsAfterPan();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 计算双光标之间的时间差(绝对值)。
|
* @brief 计算双光标之间的时间差(绝对值)。
|
||||||
* @return 时间差;非双光标模式返回 0。
|
* @return 时间差;非双光标模式返回 0。
|
||||||
@@ -2274,9 +2286,18 @@ namespace jdi
|
|||||||
HBITMAP bitmap = nullptr; /**< 离屏位图。 */
|
HBITMAP bitmap = nullptr; /**< 离屏位图。 */
|
||||||
HGDIOBJ oldBitmap = nullptr; /**< DC 原选位图。 */
|
HGDIOBJ oldBitmap = nullptr; /**< DC 原选位图。 */
|
||||||
int width = 0; /**< 位图宽度(含左右各半屏余量)。 */
|
int width = 0; /**< 位图宽度(含左右各半屏余量)。 */
|
||||||
int height = 0; /**< 位图高度(= 网格高)。 */
|
int height = 0; /**< 位图高度(含上下各半个网格高的余量)。 */
|
||||||
|
int marginY = 0; /**< 位图在**可见网格上方**预留的像素行数(纵向预渲染余量)。 */
|
||||||
double wideMin = 0.0; /**< 位图 x=0 对应的时间。 */
|
double wideMin = 0.0; /**< 位图 x=0 对应的时间。 */
|
||||||
double span = 0.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; /**< 内容指纹(含跨度、网格几何与外观)。 */
|
std::uint64_t key = 0; /**< 内容指纹(含跨度、网格几何与外观)。 */
|
||||||
bool valid = false; /**< 是否有可显示的像素。 */
|
bool valid = false; /**< 是否有可显示的像素。 */
|
||||||
};
|
};
|
||||||
@@ -2300,8 +2321,27 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW,
|
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW,
|
||||||
double wideMin);
|
double wideMin);
|
||||||
|
/**
|
||||||
|
* @brief 前台补画各网格的**分界线**(网格顶部那条较重的横线)。
|
||||||
|
*
|
||||||
|
* ⚠️ 分界线锚定在网格的**屏幕位置**(`lane.rect.Y`),幅值平移时它**不跟着移动**,
|
||||||
|
* 因此不能烘进"纵向可平移"的图层里(否则纵向拖动时它会跟着波形跑)。
|
||||||
|
* 整幅重绘由 `PaintLane` 直接绘制;宽基底快路径(贴图层像素)必须在这里补画一次。
|
||||||
|
*/
|
||||||
|
void PaintLaneDividers(gd::Graphics& g) const;
|
||||||
/** @brief 「以当前视界居中」的默认图层映射(位图 `x=0` 对应的时间)。 */
|
/** @brief 「以当前视界居中」的默认图层映射(位图 `x=0` 对应的时间)。 */
|
||||||
double LayerDefaultWideMin() const;
|
double LayerDefaultWideMin() const;
|
||||||
|
/** @brief 图层位图的横向余量(左右各半屏 → 可横向平移 ±半屏)。 */
|
||||||
|
int LayerMarginX() const;
|
||||||
|
/**
|
||||||
|
* @brief 图层位图的纵向余量(上下各半个网格高 → 可纵向平移 ±半个网格高)。
|
||||||
|
*
|
||||||
|
* ⚠️ 纵向余量让「拖拽平移的纵向分量」(幅值平移)与横向平移一样**只改贴图偏移**,
|
||||||
|
* 不必每帧重画整块图层 —— 这正是「左右平移流畅、上下平移卡顿」的根因所在。
|
||||||
|
* 余量越大可连续拖拽的范围越大,但每个横向补条 / 整块重画的代价也按余量线性增长
|
||||||
|
*(因此取「半个网格高」:一整屏的额外内容已足够,且不拖慢已经流畅的横向平移)。
|
||||||
|
*/
|
||||||
|
int LayerMarginY(float laneHeight) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧 / 图层缺失时使用)。
|
* @brief 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧 / 图层缺失时使用)。
|
||||||
@@ -2371,6 +2411,17 @@ namespace jdi
|
|||||||
|
|
||||||
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */
|
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 渲染信号图层时,横向网格线(幅值刻度)的「参考跨度 / 参考高度」。
|
||||||
|
*
|
||||||
|
* ⚠️ **必须是前台可见网格的值**,而不是图层画布的(含纵向余量的)值:
|
||||||
|
* 图层的幅值刻度必须以该跨度为基准取「整齐步长」,否则画布里的网格线密度与
|
||||||
|
* 整幅渲染不一致 —— 贴图后拖动时网格线会跳变。步长一致时刻度落在同一个
|
||||||
|
* 绝对值格点上,于是纵向平移就是纯粹的像素平移(逐像素同源)。
|
||||||
|
*/
|
||||||
|
double m_wideTickSpan = 0.0;
|
||||||
|
float m_wideTickHeight = 0.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
|
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1055,6 +1055,8 @@ namespace jdi
|
|||||||
Tab().ViewMin() += delta;
|
Tab().ViewMin() += delta;
|
||||||
Tab().ViewMax() += delta;
|
Tab().ViewMax() += delta;
|
||||||
ClampTimeView();
|
ClampTimeView();
|
||||||
|
// 方向键平移同样是平移:把光标留在可见区间内(与拖拽平移一致)。
|
||||||
|
ClampCursorsAfterPan();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::ScrollLanesByFraction(double fraction)
|
void PlotView::ScrollLanesByFraction(double fraction)
|
||||||
@@ -1260,6 +1262,19 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlotView::ClampCursorsAfterPan()
|
||||||
|
{
|
||||||
|
if (m_tab == nullptr || !Tab().CursorEnabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ClampCursorToView(1);
|
||||||
|
if (Tab().CursorMode() == PlotCursorMode::DOUBLE)
|
||||||
|
{
|
||||||
|
ClampCursorToView(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::uint32_t PlotView::CursorColor() const
|
std::uint32_t PlotView::CursorColor() const
|
||||||
{
|
{
|
||||||
// 两条光标统一使用同一样式与颜色,不按序号区分配色。
|
// 两条光标统一使用同一样式与颜色,不按序号区分配色。
|
||||||
@@ -4067,6 +4082,9 @@ namespace jdi
|
|||||||
Tab().ViewMin() += delta;
|
Tab().ViewMin() += delta;
|
||||||
Tab().ViewMax() += delta;
|
Tab().ViewMax() += delta;
|
||||||
ClampTimeView();
|
ClampTimeView();
|
||||||
|
// 平移是连续手势:必须把光标留在可见区间内,否则深放大下一屏平移就能把光标
|
||||||
|
// 甩到视界外很远(用户会认为光标“跑飞”、找不到)。
|
||||||
|
ClampCursorsAfterPan();
|
||||||
}
|
}
|
||||||
|
|
||||||
const int laneIndex = m_dragLane;
|
const int laneIndex = m_dragLane;
|
||||||
@@ -4532,7 +4550,16 @@ namespace jdi
|
|||||||
m_hoverCursor = labelWhich;
|
m_hoverCursor = labelWhich;
|
||||||
m_activeCursor = labelWhich;
|
m_activeCursor = labelWhich;
|
||||||
m_cursorHover = true;
|
m_cursorHover = true;
|
||||||
m_cursorGrabOffset = x - static_cast<int>(std::lround(CursorDisplayX(labelWhich)));
|
// 抓取偏移 = “手指按在读数标签的哪个位置”。
|
||||||
|
// ⚠️ 但光标**滞留在视界之外**时读数被钉在网格边缘,此时抓取点与光标真实位置
|
||||||
|
// 之间没有任何有意义的对应关系:仍然沿用偏移会让首次移动把目标送到视界之外
|
||||||
|
//(再被夹回边缘),表现为“一拖就跳很远 / 光标钉在边缘不动”。
|
||||||
|
// 因此这种情形下不带偏移:首次移动就把光标放在鼠标处(自然地拖回来)。
|
||||||
|
bool offRight = false;
|
||||||
|
const bool offView = CursorOffViewSide(labelWhich, offRight);
|
||||||
|
m_cursorGrabOffset =
|
||||||
|
offView ? 0
|
||||||
|
: (x - static_cast<int>(std::lround(CursorDisplayX(labelWhich))));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5061,6 +5088,8 @@ namespace jdi
|
|||||||
Tab().ViewMin() += sweep;
|
Tab().ViewMin() += sweep;
|
||||||
Tab().ViewMax() += sweep;
|
Tab().ViewMax() += sweep;
|
||||||
ClampTimeView();
|
ClampTimeView();
|
||||||
|
// Shift+滚轮也是平移:同样不能把光标落在视界之外(与拖拽平移一致)。
|
||||||
|
ClampCursorsAfterPan();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5760,6 +5789,9 @@ namespace jdi
|
|||||||
std::vector<double> wideTimeTicks;
|
std::vector<double> wideTimeTicks;
|
||||||
BuildTicks(wideViewMin, wideViewMax, wideTimeStep, wideTimeTicks);
|
BuildTicks(wideViewMin, wideViewMax, wideTimeStep, wideTimeTicks);
|
||||||
PaintTimeAxis(g, wideTimeTicks, wideTimeStep);
|
PaintTimeAxis(g, wideTimeTicks, wideTimeStep);
|
||||||
|
// 网格分界线:图层里**不含**它(它锚定在网格的屏幕位置,会被纵向偏移贴图带走),
|
||||||
|
// 因此在这里补画一次(与整幅重绘同一套颜色与线宽)。
|
||||||
|
PaintLaneDividers(g);
|
||||||
// 派生预览曲线:基底不含它们(拖参数滑块时基底不变、可直接复用像素)。
|
// 派生预览曲线:基底不含它们(拖参数滑块时基底不变、可直接复用像素)。
|
||||||
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
for (std::size_t i = 0; i < m_lanes.size(); ++i)
|
||||||
{
|
{
|
||||||
@@ -7043,8 +7075,12 @@ namespace jdi
|
|||||||
|
|
||||||
std::uint64_t PlotView::LaneLayerHash(int laneIndex) const
|
std::uint64_t PlotView::LaneLayerHash(int laneIndex) const
|
||||||
{
|
{
|
||||||
// 单个网格图层的内容 = 位图高度(= 网格高度)+ 幅值范围 + 本网格各信号的
|
// 单个网格图层的内容 = 位图高度(= 网格高度)+ 幅值**跨度** + 本网格各信号的
|
||||||
// 可见性与外观 + 图例文案(图例不在图层里,但一起计入无妨且更安全)。
|
// 可见性与外观 + 图例文案(图例不在图层里,但一起计入无妨且更安全)。
|
||||||
|
//
|
||||||
|
// ⚠️ **只计跨度、不计幅值的绝对范围**:图层的纵向锚点(`anchorYMax`)与 `wideMin`
|
||||||
|
// 一样是"贴图偏移"的来源,幅值整体平移不改变图层内容(只是贴图时纵向偏移若干像素),
|
||||||
|
// 因此纵向拖动不会让图层失效。真正改变内容的是**跨度**(决定像素/数值与刻度步长)。
|
||||||
std::uint64_t hash = 1469598103934665603ull;
|
std::uint64_t hash = 1469598103934665603ull;
|
||||||
const auto mix = [&hash](std::uint64_t value)
|
const auto mix = [&hash](std::uint64_t value)
|
||||||
{
|
{
|
||||||
@@ -7063,8 +7099,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||||
mixFloat(visual.rect.Height);
|
mixFloat(visual.rect.Height);
|
||||||
mixFloat(static_cast<float>(visual.yMin));
|
mixFloat(static_cast<float>(visual.yMax - visual.yMin));
|
||||||
mixFloat(static_cast<float>(visual.yMax));
|
|
||||||
const std::vector<SignalRef>& signals = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].signals;
|
const std::vector<SignalRef>& signals = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].signals;
|
||||||
mix(signals.size());
|
mix(signals.size());
|
||||||
for (const SignalRef& ref : signals)
|
for (const SignalRef& ref : signals)
|
||||||
@@ -7085,6 +7120,20 @@ namespace jdi
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int PlotView::LayerMarginX() const
|
||||||
|
{
|
||||||
|
const int width = static_cast<int>(std::lround(m_plotRect.Width));
|
||||||
|
return std::max(1, width / 2); // 左右各半屏
|
||||||
|
}
|
||||||
|
|
||||||
|
int PlotView::LayerMarginY(float laneHeight) const
|
||||||
|
{
|
||||||
|
// 上下各半个网格高:一整屏的额外内容已足够覆盖实际拖拽范围,
|
||||||
|
// 又不至于让每个横向补条 / 整块重画按三倍高度放大(横向平移因此不受影响)。
|
||||||
|
const int half = static_cast<int>(std::lround(static_cast<double>(laneHeight) * 0.5));
|
||||||
|
return std::max(1, half);
|
||||||
|
}
|
||||||
|
|
||||||
double PlotView::LayerDefaultWideMin() const
|
double PlotView::LayerDefaultWideMin() const
|
||||||
{
|
{
|
||||||
// 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。
|
// 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。
|
||||||
@@ -7095,8 +7144,7 @@ namespace jdi
|
|||||||
return Tab().ViewMin();
|
return Tab().ViewMin();
|
||||||
}
|
}
|
||||||
const double scale = static_cast<double>(width) / span;
|
const double scale = static_cast<double>(width) / span;
|
||||||
const int margin = std::max(1, width / 2);
|
return Tab().ViewMin() - static_cast<double>(LayerMarginX()) / scale;
|
||||||
return Tab().ViewMin() - static_cast<double>(margin) / scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PlotView::LayersStructurallyReady() const
|
bool PlotView::LayersStructurallyReady() const
|
||||||
@@ -7113,8 +7161,7 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int width = static_cast<int>(std::lround(plotWidth));
|
const int width = static_cast<int>(std::lround(plotWidth));
|
||||||
const int margin = std::max(1, width / 2);
|
const int bitmapWidth = width + 2 * LayerMarginX();
|
||||||
const int bitmapWidth = width + 2 * margin;
|
|
||||||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||||||
{
|
{
|
||||||
const LaneLayer& layer = m_layerFront[index];
|
const LaneLayer& layer = m_layerFront[index];
|
||||||
@@ -7127,9 +7174,10 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 高度也要一致:网格高度变化会让贴图纵向错位。
|
// 高度也要一致:网格高度变化会让贴图纵向错位(含上下预渲染余量)。
|
||||||
|
const float laneHeight = m_lanes[index].rect.Height;
|
||||||
const int bitmapHeight =
|
const int bitmapHeight =
|
||||||
static_cast<int>(std::ceil(m_lanes[index].rect.Height));
|
static_cast<int>(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight);
|
||||||
if (layer.height != bitmapHeight)
|
if (layer.height != bitmapHeight)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -7157,9 +7205,9 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int width = static_cast<int>(std::lround(plotWidth));
|
const int width = static_cast<int>(std::lround(plotWidth));
|
||||||
const int margin = std::max(1, width / 2);
|
const int bitmapWidth = width + 2 * LayerMarginX();
|
||||||
const int bitmapWidth = width + 2 * margin;
|
const float laneHeight = m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height;
|
||||||
const int bitmapHeight = static_cast<int>(std::ceil(m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height));
|
const int bitmapHeight = static_cast<int>(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight);
|
||||||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
||||||
layer.height != bitmapHeight || std::fabs(layer.span - span) > 1.0e-12)
|
layer.height != bitmapHeight || std::fabs(layer.span - span) > 1.0e-12)
|
||||||
{
|
{
|
||||||
@@ -7259,16 +7307,21 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int width = static_cast<int>(std::lround(plotWidth));
|
const int width = static_cast<int>(std::lround(plotWidth));
|
||||||
const int margin = std::max(1, width / 2); // 左右各半屏 → 可平移 ±半屏
|
const int margin = LayerMarginX(); // 左右各半屏 → 可平移 ±半屏
|
||||||
const int bitmapWidth = width + 2 * margin;
|
const int bitmapWidth = width + 2 * margin;
|
||||||
const float laneHeight = m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height;
|
const float laneHeight = m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height;
|
||||||
const int bitmapHeight = static_cast<int>(std::ceil(laneHeight));
|
const int lanePixelHeight = static_cast<int>(std::ceil(laneHeight));
|
||||||
|
const int marginY = LayerMarginY(laneHeight); // 上下各半个网格高 → 幅值可平移 ±半个网格高
|
||||||
|
const int bitmapHeight = lanePixelHeight + 2 * marginY;
|
||||||
if (bitmapHeight <= 0 || !EnsureLaneLayer(layer, bitmapWidth, bitmapHeight))
|
if (bitmapHeight <= 0 || !EnsureLaneLayer(layer, bitmapWidth, bitmapHeight))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const double scale = static_cast<double>(width) / span; // 像素 / 时间
|
const double scale = static_cast<double>(width) / span; // 像素 / 时间
|
||||||
|
const double laneSpan = m_lanes[static_cast<std::size_t>(laneIndex)].yMax -
|
||||||
|
m_lanes[static_cast<std::size_t>(laneIndex)].yMin;
|
||||||
|
const double pxPerValue = (laneSpan > 0.0) ? static_cast<double>(laneHeight) / laneSpan : 0.0;
|
||||||
|
|
||||||
if (stripW <= 0 || stripX < 0 || stripX + stripW > bitmapWidth)
|
if (stripW <= 0 || stripX < 0 || stripX + stripW > bitmapWidth)
|
||||||
{
|
{
|
||||||
@@ -7285,17 +7338,25 @@ namespace jdi
|
|||||||
const bool savedFastFrame = m_fastFrame;
|
const bool savedFastFrame = m_fastFrame;
|
||||||
const bool savedRenderWide = m_renderWide;
|
const bool savedRenderWide = m_renderWide;
|
||||||
const float savedScroll = Tab().LaneScroll();
|
const float savedScroll = Tab().LaneScroll();
|
||||||
|
const double savedWideTickSpan = m_wideTickSpan;
|
||||||
|
const float savedWideTickHeight = m_wideTickHeight;
|
||||||
|
|
||||||
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), laneHeight);
|
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth),
|
||||||
|
static_cast<float>(bitmapHeight));
|
||||||
// 只把 `m_area` 限为待渲染的那一段 x 区间,使 GDI/GDI+ 的裁剪只落在它上面;
|
// 只把 `m_area` 限为待渲染的那一段 x 区间,使 GDI/GDI+ 的裁剪只落在它上面;
|
||||||
// x 坐标仍用**整条宽窗口**的映射(`m_plotRect` 为完整宽图),因此网格线位置与整块图层一致。
|
// x 坐标仍用**整条宽窗口**的映射(`m_plotRect` 为完整宽图),因此网格线位置与整块图层一致。
|
||||||
m_area = gd::RectF(static_cast<float>(stripX), 0.0f, static_cast<float>(stripW), laneHeight);
|
m_area = gd::RectF(static_cast<float>(stripX), 0.0f, static_cast<float>(stripW),
|
||||||
|
static_cast<float>(bitmapHeight));
|
||||||
Tab().ViewMin() = wideMin;
|
Tab().ViewMin() = wideMin;
|
||||||
Tab().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
|
Tab().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
|
||||||
|
|
||||||
m_renderWide = true; // 图层只含基底:跳过派生曲线与装饰
|
m_renderWide = true; // 图层只含基底:跳过派生曲线与装饰
|
||||||
// 图层必须与"同一视界的整幅渲染"**逐像素同源**:强制全质量抗锯齿(不受拖拽快速帧影响)。
|
// 图层必须与"同一视界的整幅渲染"**逐像素同源**:强制全质量抗锯齿(不受拖拽快速帧影响)。
|
||||||
m_fastFrame = false;
|
m_fastFrame = false;
|
||||||
|
// 幅值刻度的参考跨度 / 参考高度取**前台可见网格**的值(而不是含上下余量的画布值):
|
||||||
|
// 这样图层里的横向网格线步长与前台一致,纵向平移才是纯粹的像素平移。
|
||||||
|
m_wideTickSpan = laneSpan;
|
||||||
|
m_wideTickHeight = laneHeight;
|
||||||
BuildLaneVisuals();
|
BuildLaneVisuals();
|
||||||
// 沿用**当前视界**的 Y 轴范围,否则幅值映射与前台不一致。
|
// 沿用**当前视界**的 Y 轴范围,否则幅值映射与前台不一致。
|
||||||
for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i)
|
for (std::size_t i = 0; i < m_lanes.size() && i < savedLanes.size(); ++i)
|
||||||
@@ -7303,9 +7364,23 @@ namespace jdi
|
|||||||
m_lanes[i].yMin = savedLanes[i].yMin;
|
m_lanes[i].yMin = savedLanes[i].yMin;
|
||||||
m_lanes[i].yMax = savedLanes[i].yMax;
|
m_lanes[i].yMax = savedLanes[i].yMax;
|
||||||
}
|
}
|
||||||
// 该网格在本图层中的位置:横向铺满整条宽图,纵向从 0 开始。
|
// 该网格在本图层中的位置:横向铺满整条宽图,纵向铺满整张画布。
|
||||||
|
// 画布 = 「可见网格 + 上下各 `marginY` 行余量」。**上下扩展量按"像素行"精确换算**
|
||||||
|
//(而不是上下各扩展固定的数值量):位图高度是整数(`ceil(laneHeight) + 2*marginY`),
|
||||||
|
// 只有当「扩展后总跨度 / 画布行数」严格等于前台的 px/数值 时,可见网格在画布里的映射
|
||||||
|
// 才与前台整幅渲染**逐像素同源**(否则会差一个 <1px 的纵向缩放误差,
|
||||||
|
// 让拖拽期间的像素残差明显变大)。
|
||||||
|
if (pxPerValue > 0.0)
|
||||||
|
{
|
||||||
|
const double topExtend = static_cast<double>(marginY) / pxPerValue;
|
||||||
|
const double bottomRows =
|
||||||
|
static_cast<double>(bitmapHeight - marginY) - static_cast<double>(laneHeight);
|
||||||
|
const double bottomExtend = std::max(0.0, bottomRows) / pxPerValue;
|
||||||
|
m_lanes[static_cast<std::size_t>(laneIndex)].yMin -= bottomExtend;
|
||||||
|
m_lanes[static_cast<std::size_t>(laneIndex)].yMax += topExtend;
|
||||||
|
}
|
||||||
m_lanes[static_cast<std::size_t>(laneIndex)].rect =
|
m_lanes[static_cast<std::size_t>(laneIndex)].rect =
|
||||||
gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), laneHeight);
|
gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth), static_cast<float>(bitmapHeight));
|
||||||
{
|
{
|
||||||
gd::Graphics g(layer.dc);
|
gd::Graphics g(layer.dc);
|
||||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||||
@@ -7324,6 +7399,8 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
m_fastFrame = savedFastFrame;
|
m_fastFrame = savedFastFrame;
|
||||||
m_renderWide = savedRenderWide;
|
m_renderWide = savedRenderWide;
|
||||||
|
m_wideTickSpan = savedWideTickSpan;
|
||||||
|
m_wideTickHeight = savedWideTickHeight;
|
||||||
|
|
||||||
// 恢复布局 / 视图 / 网格。
|
// 恢复布局 / 视图 / 网格。
|
||||||
// ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局
|
// ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局
|
||||||
@@ -7350,6 +7427,11 @@ namespace jdi
|
|||||||
|
|
||||||
layer.wideMin = wideMin;
|
layer.wideMin = wideMin;
|
||||||
layer.span = span;
|
layer.span = span;
|
||||||
|
layer.marginY = marginY;
|
||||||
|
// 纵向锚点 = 渲染时该网格的 `yMax`(可见窗口顶行对应的数值)。
|
||||||
|
// 贴图时按 `(anchorYMax - 当前 yMax) * pxPerValue` 得到纵向偏移像素数。
|
||||||
|
layer.anchorYMax = savedLanes[static_cast<std::size_t>(laneIndex)].yMax;
|
||||||
|
layer.pxPerValue = pxPerValue;
|
||||||
layer.key = LaneLayerKey(laneIndex);
|
layer.key = LaneLayerKey(laneIndex);
|
||||||
layer.valid = true;
|
layer.valid = true;
|
||||||
m_layerRenderCount.fetch_add(1, std::memory_order_relaxed);
|
m_layerRenderCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
@@ -7394,7 +7476,7 @@ namespace jdi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const int width = static_cast<int>(std::lround(plotWidth));
|
const int width = static_cast<int>(std::lround(plotWidth));
|
||||||
const int margin = std::max(1, width / 2);
|
const int margin = LayerMarginX();
|
||||||
const int bitmapWidth = width + 2 * margin;
|
const int bitmapWidth = width + 2 * margin;
|
||||||
const double scale = static_cast<double>(width) / span;
|
const double scale = static_cast<double>(width) / span;
|
||||||
|
|
||||||
@@ -7419,7 +7501,9 @@ namespace jdi
|
|||||||
const int laneIndex = static_cast<int>(index);
|
const int laneIndex = static_cast<int>(index);
|
||||||
|
|
||||||
// 结构不可用(尺寸 / 跨度不符):像素的时间映射已经不对,既不能贴也不能滚。
|
// 结构不可用(尺寸 / 跨度不符):像素的时间映射已经不对,既不能贴也不能滚。
|
||||||
const int bitmapHeight = static_cast<int>(std::ceil(m_lanes[index].rect.Height));
|
const float laneHeight = m_lanes[index].rect.Height;
|
||||||
|
const int bitmapHeight =
|
||||||
|
static_cast<int>(std::ceil(laneHeight)) + 2 * LayerMarginY(laneHeight);
|
||||||
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
if (!layer.valid || layer.dc == nullptr || layer.width != bitmapWidth ||
|
||||||
layer.height != bitmapHeight || std::fabs(layer.span - span) > 1.0e-12)
|
layer.height != bitmapHeight || std::fabs(layer.span - span) > 1.0e-12)
|
||||||
{
|
{
|
||||||
@@ -7431,12 +7515,23 @@ namespace jdi
|
|||||||
const double desired = (Tab().ViewMin() - layer.wideMin) * scale;
|
const double desired = (Tab().ViewMin() - layer.wideMin) * scale;
|
||||||
const int shift = static_cast<int>(std::llround(desired - static_cast<double>(margin)));
|
const int shift = static_cast<int>(std::llround(desired - static_cast<double>(margin)));
|
||||||
const bool tooFar = (std::abs(shift) >= bitmapWidth - 4);
|
const bool tooFar = (std::abs(shift) >= bitmapWidth - 4);
|
||||||
// 内容(外观 / 网格几何 / 幅值 / 可见性…)与当前状态不一致。
|
// 内容(外观 / 网格几何 / 幅值**跨度** / 可见性…)与当前状态不一致。
|
||||||
const bool contentStale = (layer.key != LaneLayerKey(laneIndex));
|
const bool contentStale = (layer.key != LaneLayerKey(laneIndex));
|
||||||
|
// 纵向:幅值**平移**不改内容,只改贴图偏移;但偏移超出画布余量时必须重画(重新居中)。
|
||||||
if (contentStale || tooFar)
|
int vOffset = 0;
|
||||||
|
if (layer.pxPerValue > 0.0)
|
||||||
{
|
{
|
||||||
// 登记待重画(本帧不重画),但仍按现有像素平移,避免网格"卡在原地"。
|
vOffset = static_cast<int>(
|
||||||
|
std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue));
|
||||||
|
}
|
||||||
|
const int topRow = layer.marginY + vOffset;
|
||||||
|
const bool verticalTooFar = (topRow < 0) ||
|
||||||
|
(topRow + static_cast<int>(std::ceil(laneHeight)) > layer.height);
|
||||||
|
|
||||||
|
if (contentStale || tooFar || verticalTooFar)
|
||||||
|
{
|
||||||
|
// 登记待重画(本帧不重画,改由 `RenderPendingLaneRebuilds` 在**贴图之前**补画),
|
||||||
|
// 但仍按现有像素平移,避免网格"卡在原地"。
|
||||||
QueueLaneRebuild(laneIndex);
|
QueueLaneRebuild(laneIndex);
|
||||||
if (!tooFar && shift != 0)
|
if (!tooFar && shift != 0)
|
||||||
{
|
{
|
||||||
@@ -7491,7 +7586,7 @@ namespace jdi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int width = static_cast<int>(std::lround(plotWidth));
|
const int width = static_cast<int>(std::lround(plotWidth));
|
||||||
const int margin = std::max(1, width / 2);
|
const int margin = LayerMarginX();
|
||||||
const int bitmapWidth = width + 2 * margin;
|
const int bitmapWidth = width + 2 * margin;
|
||||||
const double scale = static_cast<double>(width) / span;
|
const double scale = static_cast<double>(width) / span;
|
||||||
|
|
||||||
@@ -7526,8 +7621,37 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
const int destX = static_cast<int>(std::lround(m_plotRect.X));
|
const int destX = static_cast<int>(std::lround(m_plotRect.X));
|
||||||
const int destY = static_cast<int>(std::lround(top));
|
const int destY = static_cast<int>(std::lround(top));
|
||||||
const int srcY = static_cast<int>(std::lround(top - lane.Y));
|
int height = static_cast<int>(std::lround(bottom - top));
|
||||||
const int height = static_cast<int>(std::lround(bottom - top));
|
if (height <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ **纵向偏移**:图层带「上下各半个网格高」的预渲染余量,幅值平移只改这里的
|
||||||
|
// 源行偏移(`anchorYMax` 与当前 `yMax` 之差换算成像素),不必重画图层 ——
|
||||||
|
// 这就是"纵向拖动与横向拖动同样是 O(1) 贴图"的关键。
|
||||||
|
int srcY = 0;
|
||||||
|
{
|
||||||
|
int vOffset = 0;
|
||||||
|
if (layer.pxPerValue > 0.0)
|
||||||
|
{
|
||||||
|
vOffset = static_cast<int>(
|
||||||
|
std::llround((layer.anchorYMax - m_lanes[index].yMax) * layer.pxPerValue));
|
||||||
|
}
|
||||||
|
srcY = static_cast<int>(std::lround(top - lane.Y)) + layer.marginY + vOffset;
|
||||||
|
}
|
||||||
|
// 超出画布的上下两端只贴**有像素的那一段**(其余由底色填充,本帧的预算内重画会补齐)。
|
||||||
|
int destTopOffset = 0;
|
||||||
|
if (srcY < 0)
|
||||||
|
{
|
||||||
|
destTopOffset = -srcY;
|
||||||
|
height -= destTopOffset;
|
||||||
|
srcY = 0;
|
||||||
|
}
|
||||||
|
if (srcY + height > layer.height)
|
||||||
|
{
|
||||||
|
height = layer.height - srcY;
|
||||||
|
}
|
||||||
if (height <= 0)
|
if (height <= 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -7554,7 +7678,8 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue; // 完全没有可用像素(该网格待重画)
|
continue; // 完全没有可用像素(该网格待重画)
|
||||||
}
|
}
|
||||||
BitBlt(target, destX + destOffset, destY, blitWidth, height, layer.dc, srcX, srcY, SRCCOPY);
|
BitBlt(target, destX + destOffset, destY + destTopOffset, blitWidth, height, layer.dc, srcX,
|
||||||
|
srcY, SRCCOPY);
|
||||||
any = true;
|
any = true;
|
||||||
}
|
}
|
||||||
g.ReleaseHDC(target);
|
g.ReleaseHDC(target);
|
||||||
@@ -7705,7 +7830,21 @@ namespace jdi
|
|||||||
|
|
||||||
int yTickTarget = static_cast<int>(std::lround(lane.rect.Height / (kYTickSpacing * m_scale)));
|
int yTickTarget = static_cast<int>(std::lround(lane.rect.Height / (kYTickSpacing * m_scale)));
|
||||||
yTickTarget = std::max(kYTickTargetMin, std::min(yTickTarget, kYTickTargetMax));
|
yTickTarget = std::max(kYTickTargetMin, std::min(yTickTarget, kYTickTargetMax));
|
||||||
const double yStep = NiceStep(lane.yMax - lane.yMin, yTickTarget);
|
// ⚠️ 渲染「信号图层」时,幅值刻度必须以**前台可见网格**的跨度/高度为基准:
|
||||||
|
// 图层画布比可见网格高(上下各留预渲染余量),若按画布高度与扩展后的跨度取刻度,
|
||||||
|
// 图里的网格线密度就与整幅渲染不一致 —— 贴图后拖动时网格线会跳变。
|
||||||
|
// 用可见跨度取步长 + 按扩展范围铺满整张画布 ⇒ 刻度落在同一批绝对值格点上,
|
||||||
|
// 于是"纵向平移 = 纯粹的像素平移"(逐像素同源)。
|
||||||
|
const double tickSpan = (m_renderWide && m_wideTickSpan > 0.0)
|
||||||
|
? m_wideTickSpan
|
||||||
|
: (lane.yMax - lane.yMin);
|
||||||
|
if (m_renderWide && m_wideTickHeight > 0.0f)
|
||||||
|
{
|
||||||
|
yTickTarget = static_cast<int>(
|
||||||
|
std::lround(m_wideTickHeight / (kYTickSpacing * m_scale)));
|
||||||
|
yTickTarget = std::max(kYTickTargetMin, std::min(yTickTarget, kYTickTargetMax));
|
||||||
|
}
|
||||||
|
const double yStep = NiceStep(tickSpan, yTickTarget);
|
||||||
std::vector<double> yTicks;
|
std::vector<double> yTicks;
|
||||||
BuildTicks(lane.yMin, lane.yMax, yStep, yTicks);
|
BuildTicks(lane.yMin, lane.yMax, yStep, yTicks);
|
||||||
|
|
||||||
@@ -7956,8 +8095,11 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 网格分界线:网格之间不再留间隔,改用较重的横线明确分隔相邻网格。
|
// 网格分界线:网格之间不再留间隔,改用较重的横线明确分隔相邻网格。
|
||||||
|
// ⚠️ **渲染信号图层时跳过**:分界线锚定在网格的**屏幕位置**(`lane.rect.Y`),
|
||||||
|
// 而图层会被纵向偏移贴图(幅值平移)—— 若烘进图层,纵向拖动时它会跟着波形一起跑。
|
||||||
|
// 因此它在整幅重绘里由这里绘制,宽基底快路径则由 `PaintLaneDividers` 补画一次。
|
||||||
const float dividerY = lane.rect.Y;
|
const float dividerY = lane.rect.Y;
|
||||||
if (!m_renderDecorOnly && dividerY >= m_plotRect.Y - 1.0f &&
|
if (!m_renderDecorOnly && !m_renderWide && dividerY >= m_plotRect.Y - 1.0f &&
|
||||||
dividerY <= m_plotRect.Y + m_plotRect.Height)
|
dividerY <= m_plotRect.Y + m_plotRect.Height)
|
||||||
{
|
{
|
||||||
const float dividerSegments[4] = {lane.rect.X, dividerY, lane.rect.X + lane.rect.Width, dividerY};
|
const float dividerSegments[4] = {lane.rect.X, dividerY, lane.rect.X + lane.rect.Width, dividerY};
|
||||||
@@ -8068,6 +8210,30 @@ namespace jdi
|
|||||||
g.Restore(saveState);
|
g.Restore(saveState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlotView::PaintLaneDividers(gd::Graphics& g) const
|
||||||
|
{
|
||||||
|
if (m_theme == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||||
|
{
|
||||||
|
const gd::RectF& rect = m_lanes[index].rect;
|
||||||
|
const float dividerY = rect.Y;
|
||||||
|
if (dividerY < m_plotRect.Y - 1.0f || dividerY > m_plotRect.Y + m_plotRect.Height)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const float segments[4] = {rect.X, dividerY, rect.X + rect.Width, dividerY};
|
||||||
|
const std::uint32_t dividerColor =
|
||||||
|
((index == 0) ? m_theme->axisLine : m_theme->borderStrong) | 0xFF000000u;
|
||||||
|
// GDI 批绘不受 GDI+ 裁剪约束 → 显式给出只覆盖该分界线附近的裁剪矩形。
|
||||||
|
const gd::RectF clip(m_area.X, dividerY - 4.0f * m_scale, m_area.Width, 8.0f * m_scale);
|
||||||
|
UiDrawSolidLines(g, clip, segments, 1, dividerColor,
|
||||||
|
(index == 0) ? 2 : static_cast<int>(kLaneDividerWidth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SignalRef PlotView::LegendAtPoint(int x, int y) const
|
SignalRef PlotView::LegendAtPoint(int x, int y) const
|
||||||
{
|
{
|
||||||
const float fx = static_cast<float>(x);
|
const float fx = static_cast<float>(x);
|
||||||
@@ -9910,8 +10076,12 @@ namespace jdi
|
|||||||
for (const LaneVisual& visual : m_lanes)
|
for (const LaneVisual& visual : m_lanes)
|
||||||
{
|
{
|
||||||
mixRect(visual.rect);
|
mixRect(visual.rect);
|
||||||
mixFloat(static_cast<float>(visual.yMin));
|
// ⚠️ **幅值范围(yMin/yMax)刻意不入面板指纹**:面板内容(标题、条目、滑块、
|
||||||
mixFloat(static_cast<float>(visual.yMax));
|
// 参数与边沿统计)都不依赖网格的幅值区间;而「拖拽平移的纵向分量」会每帧小幅
|
||||||
|
// 改动它 —— 一旦计入,拖拽期间**全部**悬浮面板都会每帧重画一遍
|
||||||
|
//(实测 20 个面板:竖向拖拽 167ms/帧 vs 横向 38ms/帧,正是用户报告的
|
||||||
|
// "左右流畅、上下/斜向卡顿")。代价只是面板不透明底(95.7%)背后那 4%
|
||||||
|
// 透出的底层像素可能旧一帧,与既有取舍一致(实测最大色差 ≤ 29 级)。
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_tab != nullptr)
|
if (m_tab != nullptr)
|
||||||
|
|||||||
@@ -513,6 +513,7 @@ Documents/ 项目文档(含本文件)
|
|||||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"按住键拖拽平移时波形质量下降 / 断线 / 周期性卡顿"与"浮动面板互相覆盖时交互错位"**,并把弱峰增强改为**逐信号参数**。① 平移:`RollWideBuffer` 不再整块重画(只平移+补细条,内容过期/锚点过远只入队),新增 `RenderPendingLaneRebuilds(budget)`(**每帧最多重画 1 个网格**,其余下帧)+ `BlitWideBuffer` **局部贴图**(图层没覆盖的部分留空,不再整块跳过);"结算"整幅精确帧**仅限非拖拽且全部图层就绪**(消除拖拽中每两帧的整幅重绘/质量振荡)。实测 `VERY_BIG_DATA` 5 网格深放大平移:中位 **6.2ms**、max 7.6ms(无尖峰);按住键且视界停在边界时 **0.29ms/帧**。② 弱峰增强:`WeakPeakItem` 改为携带 `levelPercent(0–200)+iterations`,面板改为与均值滤波同构的**逐信号两个输入框**(删除网格级两滑块),哈希/快照/构建/预览全部改为逐信号。③ 浮动面板:新增 `m_panelOrder` 层序,**命中从最上层往下**、点击 `BringPanelToFront`、新面板置顶;修复"上层 ✕ / 标题栏被下层参数框抢走"。离屏测试 **388/388 通过**。 |
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | **修复"按住键拖拽平移时波形质量下降 / 断线 / 周期性卡顿"与"浮动面板互相覆盖时交互错位"**,并把弱峰增强改为**逐信号参数**。① 平移:`RollWideBuffer` 不再整块重画(只平移+补细条,内容过期/锚点过远只入队),新增 `RenderPendingLaneRebuilds(budget)`(**每帧最多重画 1 个网格**,其余下帧)+ `BlitWideBuffer` **局部贴图**(图层没覆盖的部分留空,不再整块跳过);"结算"整幅精确帧**仅限非拖拽且全部图层就绪**(消除拖拽中每两帧的整幅重绘/质量振荡)。实测 `VERY_BIG_DATA` 5 网格深放大平移:中位 **6.2ms**、max 7.6ms(无尖峰);按住键且视界停在边界时 **0.29ms/帧**。② 弱峰增强:`WeakPeakItem` 改为携带 `levelPercent(0–200)+iterations`,面板改为与均值滤波同构的**逐信号两个输入框**(删除网格级两滑块),哈希/快照/构建/预览全部改为逐信号。③ 浮动面板:新增 `m_panelOrder` 层序,**命中从最上层往下**、点击 `BringPanelToFront`、新面板置顶;修复"上层 ✕ / 标题栏被下层参数框抢走"。离屏测试 **388/388 通过**。 |
|
||||||
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | 弱峰增强的参数输入改回**逐信号滑块**(每信号两条轨道:增强等级 0–200% / 迭代次数 N),并让「迭代次数 N」**上限可就地编辑**:点击滑块右侧数值 → 整行变成输入框(`EditFilter::BYTE` 只收数字、空内容显示灰提示当前上限、`0`/超限/空 保持原值、下调后当前迭代次数自动被夹到新上限);上限进 `PanelSlotKey` 但不进 `SceneContentHash`(不影响波形重画)。离屏测试 **409/409 通过**。 |
|
| 2026-10-12 | DeepSeek-V4.1-Flash(**起源设备**) | 弱峰增强的参数输入改回**逐信号滑块**(每信号两条轨道:增强等级 0–200% / 迭代次数 N),并让「迭代次数 N」**上限可就地编辑**:点击滑块右侧数值 → 整行变成输入框(`EditFilter::BYTE` 只收数字、空内容显示灰提示当前上限、`0`/超限/空 保持原值、下调后当前迭代次数自动被夹到新上限);上限进 `PanelSlotKey` 但不进 `SceneContentHash`(不影响波形重画)。离屏测试 **409/409 通过**。 |
|
||||||
| 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(**起源设备**) | **修复"斜向/不规则拖拽自由移动视界时的卡顿、掉帧与波形错误绘制"、"预览曲线不如普通信号细腻(无抗锯齿)"、"拖动参数滑块时预览曲线跳帧闪烁"**。① 根因:拖拽平移的**纵向分量**会逐帧小改被拖网格的 `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。 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2627,3 +2628,54 @@ Documents/ 项目文档(含本文件)
|
|||||||
3. **待重画网格要在贴图之前补**,否则用户看到的是"比鼠标慢半拍"的旧像素。
|
3. **待重画网格要在贴图之前补**,否则用户看到的是"比鼠标慢半拍"的旧像素。
|
||||||
4. **异步发布的派生曲线必须有"参数无关"的兜底**(按信号 + 种类取最新),否则拖参数滑块必然闪烁。
|
4. **异步发布的派生曲线必须有"参数无关"的兜底**(按信号 + 种类取最新),否则拖参数滑块必然闪烁。
|
||||||
5. **交互优化只能降分辨率(顶点数),不能关抗锯齿**:基底图层永远是全质量抗锯齿,叠加层一旦关了就会"看着更粗糙"。
|
5. **交互优化只能降分辨率(顶点数),不能关抗锯齿**:基底图层永远是全质量抗锯齿,叠加层一旦关了就会"看着更粗糙"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本轮(2026-10-12,起源设备):上下/斜向平移卡顿(纵向预渲染)+ 深放大拖拽光标跑飞
|
||||||
|
|
||||||
|
用户报告:以 `VERY_BIG_DATA.CSV` 为例,**Y 轴自适应 + 选区放大**到主时间轴约 1~2ms 一大格后,中键拖拽移动视界时「左右很流畅,但上下或斜向会卡顿」,并猜测「上下移动时没有像左右那样的预渲染缓存」。另外「小缩放区间拖拽光标跑飞」的旧缺陷复现。
|
||||||
|
|
||||||
|
### A. 先验证猜测(量化,而不是凭感觉)
|
||||||
|
|
||||||
|
`.preflight/panperf` 新增:**纯竖直拖拽**(只改被拖网格幅值、视界不动)、逐帧统计 `BackgroundLayerRenderCount()` 增量,以及「5 网格全开 4 类悬浮面板(20 个)」后的水平/竖直对比。结果:
|
||||||
|
|
||||||
|
| 场景 | 水平拖拽 | 竖直拖拽 | 每次竖直帧的图层重画次数 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 无面板 | 6.29ms/帧 | 5.26ms/帧 | **1 次/帧**(水平:5 个横向补条/帧) |
|
||||||
|
| 20 个悬浮面板 | 37.7ms/帧 | **166.6ms/帧** | 同上 |
|
||||||
|
|
||||||
|
结论:猜测**部分成立**——竖直方向确实没有预渲染(每帧整块重画该网格图层),但**主因**是另一处:浮动面板的**像素图层全局键**里含 `visual.yMin/yMax`,平移的纵向分量每帧小改幅值 → 整层面板缓存失效 → 20 个面板每帧全部重画(≈130ms/帧)。水平平移不改幅值,所以只有竖直/斜向才触发 —— 这正好解释了用户观察到的「不对称」。
|
||||||
|
|
||||||
|
### B. 图层改为**二维预渲染**(横向 ±半屏 / 纵向 ±半个网格高)
|
||||||
|
|
||||||
|
- `LaneLayer` 新增 `marginY`(可见网格上方的余量行数)、`anchorYMax`(渲染时的 `yMax`)、 `pxPerValue`(像素/数值单位);位图高度 = `ceil(可见网格高) + 2*marginY`。
|
||||||
|
- 渲染时把该网格幅值范围按**像素行**精确扩展(上扩 `marginY/pxPerValue`,下扩
|
||||||
|
`(bitmapHeight - marginY - laneHeight)/pxPerValue`)。⚠️ 必须按行数反推数值扩展量:
|
||||||
|
只有这样「扩展跨度 / 画布行数」才严格等于前台的 px/数值,可见网格在画布中的映射才与整幅渲染**逐像素同源**。
|
||||||
|
早先按「上下各扩同样的数值量」时存在 <1px 的纵向缩放误差,水平平移的像素残差实测从 77k 涨到 105k,改对后回到 77k。
|
||||||
|
- 图层里的**幅值刻度必须以前台可见网格的跨度/高度为基准**(`m_wideTickSpan` / `m_wideTickHeight`):画布更高、范围更宽,若按画布值取 `NiceStep`,网格线密度就与前台不一致(贴图后拖动网格线跳变)。
|
||||||
|
步长一致 ⇒ 刻度落在同一批绝对值格点上 ⇒ 纵向平移就是纯像素平移。
|
||||||
|
- 贴图:`srcY = round(top - lane.Y) + marginY + round((anchorYMax - 当前 yMax) * pxPerValue)`;超出画布上下端时只贴**有像素的那一段**。`RollWideBuffer` 里只有 `|偏移|` 超出余量时才排队重画(重画即重新居中)。
|
||||||
|
- 图层指纹:`LaneLayerHash` 只计幅值**跨度**(`yMax - yMin`),**不计绝对范围**(绝对范围是贴图偏移的来源)。
|
||||||
|
- ⚠️ **网格分界线必须移出图层**:它锚定在网格的**屏幕位置**,而图层会被纵向偏移贴图;若烘进图层,竖直拖动时它会跟着波形跑。现在整幅重绘由 `PaintLane` 画,宽基底快路径由 `PaintLaneDividers()` 补画一次。
|
||||||
|
|
||||||
|
### C. 面板像素图层不再被幅值范围击穿
|
||||||
|
|
||||||
|
- `PanelGlobalKey()` 去掉 `visual.yMin/yMax`:面板内容(标题、条目、滑块、参数、边沿统计)都与幅值区间无关。
|
||||||
|
- 代价:面板底是不透明度 95.7% 的圆角矩形,缓存像素里"它下面的场景"可能旧一帧 → 透出 **4.3%**,色差**有上界**(≤ 0.043×255 ≈ 11 级),与既有的「旧一帧」取舍一致,[AF] 的容差 96 覆盖得住。
|
||||||
|
|
||||||
|
### D. 深放大下拖拽光标「跑飞」
|
||||||
|
|
||||||
|
- 历史:`388d3b6`/`67f659a` 里「视图变化后夹光标」被 `eaf5b9c` 刻意删除(改为「光标可滞留在视界外 + 贴边读数 + 方向三角」)。但深放大下一屏平移就是几十倍可视跨度,光标瞬间被甩到视界外很远 —— 用户看到的就是「拖拽(视界)时光标跑飞」。
|
||||||
|
- 修法(区分**连续手势**与**离散视图变化**):
|
||||||
|
* 新增 `ClampCursorsAfterPan()`,在**平移**路径调用:PAN 拖拽(中键/左键)、Shift+滚轮、方向键(`PanTimeByFraction`)。
|
||||||
|
* ⚠️ **离散**视图变化(选区放大 / 适应视图 / 按网格时间轴展开)**仍然不移动光标** —— 用户可以先量好再自由探索, 贴边读数可读、可拖回来。不要在这几处加夹取。
|
||||||
|
* 抓取**被钉在边缘**的光标读数时不带抓取偏移(`CursorOffViewSide` 为真 → `m_cursorGrabOffset = 0`): 否则首次移动会把目标送到视界外再被夹回边缘(表现为"一拖就跳很远 / 光标钉在边缘不动")。
|
||||||
|
|
||||||
|
### E. 验证(全部离屏,不占用鼠标/键盘/屏幕)
|
||||||
|
|
||||||
|
- `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** 次图层重画;水平 5.7~6.5ms(与修复前持平);对角 11~13ms(略增:补条要按 2 倍画布高渲染);整幅质量对比残差与修复前同量级(水平 77k~81k ↔ 修复后 77k~81k;对角仅被拖动的那一个网格增加,属同一类 ≤0.5px 整数像素残差)。
|
||||||
|
- 20 个悬浮面板时:竖直 **166.6 → 24.8ms/帧**,水平 37.7 → 38.8ms(持平)——"上下比左右卡"的不对称消失。
|
||||||
|
- `layercheck`:`clobbers=0/24`、`pan frames wrong=0/4`、居中图层 vs 整幅重绘逐像素 0 差异;新增探针 `.preflight/cursorcheck`(真文件 + 真缩放)**30/30**(单/双光标拖拽、贴边读数抓取、中键/键盘/滚轮平移)。
|
||||||
|
- `make clean && make all` 零警告。
|
||||||
Reference in new issue
Block a user