上下/斜向平移卡顿(图层与面板都缺纵向预渲染)+ 深放大拖拽光标跑飞
用户报告: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 将两条光标的时间夹取到当前可见时间范围内。
|
||||
*
|
||||
* ⚠️ 现在**只在拖拽光标时**使用。视图变化(平移/放大/自适应)必须**不**移动光标:
|
||||
* ⚠️ 拖拽光标时使用;**平移**视界后也要用(见 `ClampCursorsAfterPan`)。
|
||||
* 而**离散**的视图变化(选区放大 / 适应视图 / 按网格时间轴展开)必须**不**移动光标:
|
||||
* 光标允许滞留在网格可视区域之外,此时仅把它的取值/时间读数贴在网格边缘显示。
|
||||
*
|
||||
* @param [IN] which 要夹取的光标序号(1 或 2)。
|
||||
*/
|
||||
void ClampCursorToView(int which);
|
||||
|
||||
/**
|
||||
* @brief **平移视界**之后把时间光标夹回可见区间(单/双光标都处理)。
|
||||
*
|
||||
* ⚠️ 与「离散视图变化不移动光标」并不矛盾:选区放大 / 适应视图 / 按网格时间轴展开
|
||||
* 仍然不动光标(用户可以先量好再自由探索,光标读数贴在边缘可读、可拖回来);
|
||||
* 但**平移**是连续手势 —— 尤其在深放大区间,光标一旦被落在屏外就会立刻"跑飞"
|
||||
* 很远(一屏的平移就是几十甚至上百倍可视跨度),用户会以为光标丢失。
|
||||
* 因此在平移(中键/左键拖拽、Shift+滚轮、方向键)之后夹一次。
|
||||
*/
|
||||
void ClampCursorsAfterPan();
|
||||
|
||||
/**
|
||||
* @brief 计算双光标之间的时间差(绝对值)。
|
||||
* @return 时间差;非双光标模式返回 0。
|
||||
@@ -2274,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; /**< 是否有可显示的像素。 */
|
||||
};
|
||||
@@ -2300,8 +2321,27 @@ namespace jdi
|
||||
*/
|
||||
bool RenderLaneLayerRegion(int laneIndex, LaneLayer& layer, int stripX, int stripW,
|
||||
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 以当前视界为中心重建全部网格图层(同步;跨度变化 / 首帧 / 图层缺失时使用)。
|
||||
@@ -2371,6 +2411,17 @@ namespace jdi
|
||||
|
||||
bool m_renderWide = false; /**< 正在渲染信号图层(重入 `PaintLane` 时跳过装饰)。 */
|
||||
|
||||
/**
|
||||
* @brief 渲染信号图层时,横向网格线(幅值刻度)的「参考跨度 / 参考高度」。
|
||||
*
|
||||
* ⚠️ **必须是前台可见网格的值**,而不是图层画布的(含纵向余量的)值:
|
||||
* 图层的幅值刻度必须以该跨度为基准取「整齐步长」,否则画布里的网格线密度与
|
||||
* 整幅渲染不一致 —— 贴图后拖动时网格线会跳变。步长一致时刻度落在同一个
|
||||
* 绝对值格点上,于是纵向平移就是纯粹的像素平移(逐像素同源)。
|
||||
*/
|
||||
double m_wideTickSpan = 0.0;
|
||||
float m_wideTickHeight = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief 本次 `PaintLane` 只绘制「装饰」(Y 轴刻度胶囊 + 图例 + 网格分界线)。
|
||||
*
|
||||
|
||||
@@ -1055,6 +1055,8 @@ namespace jdi
|
||||
Tab().ViewMin() += delta;
|
||||
Tab().ViewMax() += delta;
|
||||
ClampTimeView();
|
||||
// 方向键平移同样是平移:把光标留在可见区间内(与拖拽平移一致)。
|
||||
ClampCursorsAfterPan();
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
// 两条光标统一使用同一样式与颜色,不按序号区分配色。
|
||||
@@ -4067,6 +4082,9 @@ namespace jdi
|
||||
Tab().ViewMin() += delta;
|
||||
Tab().ViewMax() += delta;
|
||||
ClampTimeView();
|
||||
// 平移是连续手势:必须把光标留在可见区间内,否则深放大下一屏平移就能把光标
|
||||
// 甩到视界外很远(用户会认为光标“跑飞”、找不到)。
|
||||
ClampCursorsAfterPan();
|
||||
}
|
||||
|
||||
const int laneIndex = m_dragLane;
|
||||
@@ -4532,7 +4550,16 @@ namespace jdi
|
||||
m_hoverCursor = labelWhich;
|
||||
m_activeCursor = labelWhich;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -5061,6 +5088,8 @@ namespace jdi
|
||||
Tab().ViewMin() += sweep;
|
||||
Tab().ViewMax() += sweep;
|
||||
ClampTimeView();
|
||||
// Shift+滚轮也是平移:同样不能把光标落在视界之外(与拖拽平移一致)。
|
||||
ClampCursorsAfterPan();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5760,6 +5789,9 @@ namespace jdi
|
||||
std::vector<double> wideTimeTicks;
|
||||
BuildTicks(wideViewMin, wideViewMax, wideTimeStep, wideTimeTicks);
|
||||
PaintTimeAxis(g, wideTimeTicks, wideTimeStep);
|
||||
// 网格分界线:图层里**不含**它(它锚定在网格的屏幕位置,会被纵向偏移贴图带走),
|
||||
// 因此在这里补画一次(与整幅重绘同一套颜色与线宽)。
|
||||
PaintLaneDividers(g);
|
||||
// 派生预览曲线:基底不含它们(拖参数滑块时基底不变、可直接复用像素)。
|
||||
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
|
||||
{
|
||||
// 单个网格图层的内容 = 位图高度(= 网格高度)+ 幅值范围 + 本网格各信号的
|
||||
// 单个网格图层的内容 = 位图高度(= 网格高度)+ 幅值**跨度** + 本网格各信号的
|
||||
// 可见性与外观 + 图例文案(图例不在图层里,但一起计入无妨且更安全)。
|
||||
//
|
||||
// ⚠️ **只计跨度、不计幅值的绝对范围**:图层的纵向锚点(`anchorYMax`)与 `wideMin`
|
||||
// 一样是"贴图偏移"的来源,幅值整体平移不改变图层内容(只是贴图时纵向偏移若干像素),
|
||||
// 因此纵向拖动不会让图层失效。真正改变内容的是**跨度**(决定像素/数值与刻度步长)。
|
||||
std::uint64_t hash = 1469598103934665603ull;
|
||||
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)];
|
||||
mixFloat(visual.rect.Height);
|
||||
mixFloat(static_cast<float>(visual.yMin));
|
||||
mixFloat(static_cast<float>(visual.yMax));
|
||||
mixFloat(static_cast<float>(visual.yMax - visual.yMin));
|
||||
const std::vector<SignalRef>& signals = Tab().Lanes()[static_cast<std::size_t>(laneIndex)].signals;
|
||||
mix(signals.size());
|
||||
for (const SignalRef& ref : signals)
|
||||
@@ -7085,6 +7120,20 @@ namespace jdi
|
||||
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
|
||||
{
|
||||
// 以当前视界为中心:位图左右各留半个视界宽,因此可向两侧平移 ±半屏。
|
||||
@@ -7095,8 +7144,7 @@ namespace jdi
|
||||
return Tab().ViewMin();
|
||||
}
|
||||
const double scale = static_cast<double>(width) / span;
|
||||
const int margin = std::max(1, width / 2);
|
||||
return Tab().ViewMin() - static_cast<double>(margin) / scale;
|
||||
return Tab().ViewMin() - static_cast<double>(LayerMarginX()) / scale;
|
||||
}
|
||||
|
||||
bool PlotView::LayersStructurallyReady() const
|
||||
@@ -7113,8 +7161,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
const int width = static_cast<int>(std::lround(plotWidth));
|
||||
const int margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const int bitmapWidth = width + 2 * LayerMarginX();
|
||||
for (std::size_t index = 0; index < m_layerFront.size(); ++index)
|
||||
{
|
||||
const LaneLayer& layer = m_layerFront[index];
|
||||
@@ -7127,9 +7174,10 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 高度也要一致:网格高度变化会让贴图纵向错位。
|
||||
// 高度也要一致:网格高度变化会让贴图纵向错位(含上下预渲染余量)。
|
||||
const float laneHeight = m_lanes[index].rect.Height;
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
@@ -7157,9 +7205,9 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
const int width = static_cast<int>(std::lround(plotWidth));
|
||||
const int margin = std::max(1, width / 2);
|
||||
const int bitmapWidth = width + 2 * margin;
|
||||
const int bitmapHeight = static_cast<int>(std::ceil(m_lanes[static_cast<std::size_t>(laneIndex)].rect.Height));
|
||||
const int bitmapWidth = width + 2 * LayerMarginX();
|
||||
const float laneHeight = 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 ||
|
||||
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 margin = std::max(1, width / 2); // 左右各半屏 → 可平移 ±半屏
|
||||
const int margin = 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(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))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -7285,17 +7338,25 @@ namespace jdi
|
||||
const bool savedFastFrame = m_fastFrame;
|
||||
const bool savedRenderWide = m_renderWide;
|
||||
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+ 的裁剪只落在它上面;
|
||||
// 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().ViewMax() = wideMin + static_cast<double>(bitmapWidth) / scale;
|
||||
|
||||
m_renderWide = true; // 图层只含基底:跳过派生曲线与装饰
|
||||
// 图层必须与"同一视界的整幅渲染"**逐像素同源**:强制全质量抗锯齿(不受拖拽快速帧影响)。
|
||||
m_fastFrame = false;
|
||||
// 幅值刻度的参考跨度 / 参考高度取**前台可见网格**的值(而不是含上下余量的画布值):
|
||||
// 这样图层里的横向网格线步长与前台一致,纵向平移才是纯粹的像素平移。
|
||||
m_wideTickSpan = laneSpan;
|
||||
m_wideTickHeight = laneHeight;
|
||||
BuildLaneVisuals();
|
||||
// 沿用**当前视界**的 Y 轴范围,否则幅值映射与前台不一致。
|
||||
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].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 =
|
||||
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);
|
||||
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
|
||||
@@ -7324,6 +7399,8 @@ namespace jdi
|
||||
}
|
||||
m_fastFrame = savedFastFrame;
|
||||
m_renderWide = savedRenderWide;
|
||||
m_wideTickSpan = savedWideTickSpan;
|
||||
m_wideTickHeight = savedWideTickHeight;
|
||||
|
||||
// 恢复布局 / 视图 / 网格。
|
||||
// ⚠️ **必须调用 `BuildLaneVisuals()`**(而不是只把 `m_lanes` 拷回去):它会按正常布局
|
||||
@@ -7350,6 +7427,11 @@ namespace jdi
|
||||
|
||||
layer.wideMin = wideMin;
|
||||
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.valid = true;
|
||||
m_layerRenderCount.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -7394,7 +7476,7 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
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 double scale = static_cast<double>(width) / span;
|
||||
|
||||
@@ -7419,7 +7501,9 @@ namespace jdi
|
||||
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 ||
|
||||
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 int shift = static_cast<int>(std::llround(desired - static_cast<double>(margin)));
|
||||
const bool tooFar = (std::abs(shift) >= bitmapWidth - 4);
|
||||
// 内容(外观 / 网格几何 / 幅值 / 可见性…)与当前状态不一致。
|
||||
// 内容(外观 / 网格几何 / 幅值**跨度** / 可见性…)与当前状态不一致。
|
||||
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);
|
||||
if (!tooFar && shift != 0)
|
||||
{
|
||||
@@ -7491,7 +7586,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
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 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 destY = static_cast<int>(std::lround(top));
|
||||
const int srcY = static_cast<int>(std::lround(top - lane.Y));
|
||||
const int height = static_cast<int>(std::lround(bottom - top));
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
@@ -7554,7 +7678,8 @@ namespace jdi
|
||||
{
|
||||
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;
|
||||
}
|
||||
g.ReleaseHDC(target);
|
||||
@@ -7705,7 +7830,21 @@ namespace jdi
|
||||
|
||||
int yTickTarget = static_cast<int>(std::lround(lane.rect.Height / (kYTickSpacing * m_scale)));
|
||||
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;
|
||||
BuildTicks(lane.yMin, lane.yMax, yStep, yTicks);
|
||||
|
||||
@@ -7956,8 +8095,11 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 网格分界线:网格之间不再留间隔,改用较重的横线明确分隔相邻网格。
|
||||
// ⚠️ **渲染信号图层时跳过**:分界线锚定在网格的**屏幕位置**(`lane.rect.Y`),
|
||||
// 而图层会被纵向偏移贴图(幅值平移)—— 若烘进图层,纵向拖动时它会跟着波形一起跑。
|
||||
// 因此它在整幅重绘里由这里绘制,宽基底快路径则由 `PaintLaneDividers` 补画一次。
|
||||
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)
|
||||
{
|
||||
const float dividerSegments[4] = {lane.rect.X, dividerY, lane.rect.X + lane.rect.Width, dividerY};
|
||||
@@ -8068,6 +8210,30 @@ namespace jdi
|
||||
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
|
||||
{
|
||||
const float fx = static_cast<float>(x);
|
||||
@@ -9910,8 +10076,12 @@ namespace jdi
|
||||
for (const LaneVisual& visual : m_lanes)
|
||||
{
|
||||
mixRect(visual.rect);
|
||||
mixFloat(static_cast<float>(visual.yMin));
|
||||
mixFloat(static_cast<float>(visual.yMax));
|
||||
// ⚠️ **幅值范围(yMin/yMax)刻意不入面板指纹**:面板内容(标题、条目、滑块、
|
||||
// 参数与边沿统计)都不依赖网格的幅值区间;而「拖拽平移的纵向分量」会每帧小幅
|
||||
// 改动它 —— 一旦计入,拖拽期间**全部**悬浮面板都会每帧重画一遍
|
||||
//(实测 20 个面板:竖向拖拽 167ms/帧 vs 横向 38ms/帧,正是用户报告的
|
||||
// "左右流畅、上下/斜向卡顿")。代价只是面板不透明底(95.7%)背后那 4%
|
||||
// 透出的底层像素可能旧一帧,与既有取舍一致(实测最大色差 ≤ 29 级)。
|
||||
}
|
||||
|
||||
if (m_tab != nullptr)
|
||||
|
||||
Reference in new issue
Block a user