修复信号面板滚动条跟随、工具栏连点丢状态、图层竖向网格线步长、多网格单点自适应与单点信号拖拽
用户 2026-10-15 报告的五个问题,逐一给出根因与修法(详见 Documents/agent_mem.md §9.32):
1. 拖动信号面板滚动条、鼠标移到绘图区后“滑块卡在原地不动”
根因:Application::WM_MOUSEMOVE 的 plotOnlyMove 只看 inPlot && m_mouseOverPlot,
拖滚动条进入绘图区后判定“本帧只影响绘图区” ⇒ 面板滚动偏移已变却从不重绘。
修法:新增 draggingNonPlot(正在拖非绘图区组件时一律走完整失效)。
2. 工具栏快速连点 / 双击丢状态
根因:(a) 窗口类带 CS_DBLCLKS,快速第二次按下被合成 WM_LBUTTONDBLCLK 而工具栏未处理
(那一次按下完全丢失);
(b) HandleMouseMove 只要鼠标不在工具栏上就调 OnMouseLeave(),清掉 m_pressedIndex
⇒ 按住期间从按钮边缘划出再回到按钮松手不会触发任何动作。
修法:WM_LBUTTONDBLCLK 命中工具栏时按“按下”处理;m_activeTarget == TOOLBAR 期间不调 OnMouseLeave()。
3. 网格叠底:拖拽期间竖向网格线错位 / 丢失,操作信号样式时“闪烁”
根因:PaintLane 的竖向主刻步长由 Tab().ViewMin/ViewMax 推出,而渲染信号图层时该区间已被
RenderLaneLayerRegion 改成“宽窗口”(≈2× 前台)⇒ 图层辅助线步长 2×、位置与前台不同、
“与主刻重合则跳过”的判据也随之错位 ⇒ 贴图后网格线错位 / 整条丢失,
并在“图层帧 ↔ 结算帧”之间来回切换时闪烁。
修法:新增 m_renderTimeStep,由 RenderLaneLayerRegion 用前台视界跨度算好传入
(与纵向 m_wideTickSpan/m_wideTickHeight 同一思路),PaintLane 优先使用它。
4. 多个网格、每个网格只有一个数据点时「适应视图」把数据点甩飞
修法:新增 FitSinglePointLanes(),按用户给出的规则:
* 左右边界 = 所有网格中最左 / 最右数据点的时间,再向外各偏“一个网格小格”的时间跨度;
* 逐网格 Y 自适应,并在纵向上下各留“一个 Y 小格”的高度;
* 全部数据点在同一时刻时 ⇒ 时间跨度 1s 且以该时刻居中(与首个单点信号的初始视图对齐);
* 某网格只有一个取值 ⇒ 该网格 Y 跨度 1 且居中。
仅在全部可见信号都是单点(finiteCount == 1)时启用,否则交回常规 FitAll。
5. 单点信号的悬停高亮与拖拽换网格
修法:HitTestSignal 新增单点分支(SinglePointScreenPos + 容差 = max(常规容差, 该点绘制半径));
悬停用环状光晕且不改鼠标形状(QueryCursor 不查 HitTestSignal),按下拖拽后才改 IDC_SIZEALL;
拖拽虚影对单点信号回退为实心点 + 环。
验证(全部离屏,不占用鼠标 / 键盘 / 屏幕):
- .preflight/check(SW_HIDE + 只注入合成消息 + 进程/线程 BELOW_NORMAL)**67/67 通过**,连跑 3 次稳定。
其中 [C] 直接量“宽基底图层位图自身”的网格线间距(图层主刻 123px vs 前台 122px、辅助线 25px vs 25px),
并断言拖拽帧确实来自宽基底贴图路径(m_wideLastValid)。
- A/B 判别:回退上述六处修复后 **17 项如实失败**([C] 辅助线 49px vs 25px、20 vs 40 条、最大偏移 539px)。
- make release:三个改动文件重新编译 **零警告**;链接用临时 `make release TARGET_FILE_NAME=Target_check` 通过。
同步记忆:Documents/agent_mem.md 新增 §9.32、§11.2 第 64–68 条硬约束、变更日志末行。
This commit is contained in:
1 parent
68d8b7ede6
commit
aee3a388b5
5 files changed
+541
-40
No files matched your search
@@ -1324,11 +1324,10 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 「仅有一个网格、且网格内只显示了一个**单点**信号」时,适应效果必须与
|
||||
// “首次加载并勾选该单点信号”时完全一致(数据点居中、时间跨度 1s、Y 跨度 1)。
|
||||
// 否则整段自适应会让时间轴覆盖整条时间列 —— 数据点被挤到网格边缘、看起来“飞了”
|
||||
//(用户 2026-10-15 报告)。
|
||||
if (FitSinglePointSignal())
|
||||
// 「网格内只显示单点信号」时的自适应(含“多个网格、每个网格只有一个数据点”):
|
||||
// 必须保证所有数据点都在屏幕内(用户 2026-10-15 报告“多网格单点时自适应把点甩飞”)。
|
||||
// 该函数也覆盖“只有一个网格 + 单点信号”的情形(时间跨度 1s、居中,与原行为一致)。
|
||||
if (FitSinglePointLanes())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1420,29 +1419,9 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 取出那个唯一数据点的时刻与取值(加载时已缓存为稀疏点)。
|
||||
double pointTime = std::numeric_limits<double>::quiet_NaN();
|
||||
double pointValue = std::numeric_limits<double>::quiet_NaN();
|
||||
if (!signal->sparseTimes.empty() && signal->sparseTimes.size() == signal->sparseValues.size())
|
||||
{
|
||||
pointTime = signal->sparseTimes.front();
|
||||
pointValue = signal->sparseValues.front();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 兜底:逐个采样找那个有效点(单点信号的采样数组必然很短)。
|
||||
const std::size_t count = std::min(axis->time.size(), signal->values.size());
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
if (std::isfinite(signal->values[index]) && std::isfinite(axis->time[index]))
|
||||
{
|
||||
pointTime = axis->time[index];
|
||||
pointValue = signal->values[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!std::isfinite(pointTime) || !std::isfinite(pointValue))
|
||||
double pointTime = 0.0;
|
||||
double pointValue = 0.0;
|
||||
if (!SinglePointOfSignal(*signal, *axis, pointTime, pointValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1479,6 +1458,201 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::SinglePointOfSignal(const SignalData& signal, const TimeAxis& axis, double& time,
|
||||
double& value) const
|
||||
{
|
||||
time = std::numeric_limits<double>::quiet_NaN();
|
||||
value = std::numeric_limits<double>::quiet_NaN();
|
||||
|
||||
// 正常路径:加载/插入信号时已把“有效点很少”的信号的全部有效点缓存下来。
|
||||
if (!signal.sparseTimes.empty() && signal.sparseTimes.size() == signal.sparseValues.size())
|
||||
{
|
||||
time = signal.sparseTimes.front();
|
||||
value = signal.sparseValues.front();
|
||||
return std::isfinite(time) && std::isfinite(value);
|
||||
}
|
||||
|
||||
// 兜底:逐个采样找那个有效点(单点信号的采样数组必然很短)。
|
||||
const std::size_t count = std::min(axis.time.size(), signal.values.size());
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
if (std::isfinite(signal.values[index]) && std::isfinite(axis.time[index]))
|
||||
{
|
||||
time = axis.time[index];
|
||||
value = signal.values[index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double PlotView::TimeGridCellSeconds(double span) const
|
||||
{
|
||||
if (!(span > 0.0) || !std::isfinite(span))
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
// 与网格绘制完全同源:主刻 = `NiceStep(跨度, kTimeTickTarget)`,一小格 = 主刻 / 五等分。
|
||||
return NiceStep(span, kTimeTickTarget) / static_cast<double>(kMinorDivisions);
|
||||
}
|
||||
|
||||
double PlotView::LaneValueGridCell(double span, float laneHeight) const
|
||||
{
|
||||
if (!(span > 0.0) || !std::isfinite(span) || laneHeight <= 1.0f || m_scale <= 0.0f)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
int tickTarget = static_cast<int>(std::lround(laneHeight / (kYTickSpacing * m_scale)));
|
||||
tickTarget = std::max(kYTickTargetMin, std::min(tickTarget, kYTickTargetMax));
|
||||
return NiceStep(span, tickTarget) / static_cast<double>(kMinorDivisions);
|
||||
}
|
||||
|
||||
bool PlotView::FitSinglePointLanes()
|
||||
{
|
||||
if (m_tab == nullptr || m_dataSet == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::vector<Lane>& lanes = m_tab->Lanes();
|
||||
if (lanes.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 逐网格收集“可见信号”的唯一数据点;只要任何一个网格里还存在**非单点**的可见信号,
|
||||
// 就不属于本函数的适用范围(交回常规的整段自适应)。
|
||||
struct LanePoints
|
||||
{
|
||||
std::vector<double> values;
|
||||
double minTime = std::numeric_limits<double>::infinity();
|
||||
double maxTime = -std::numeric_limits<double>::infinity();
|
||||
};
|
||||
std::vector<LanePoints> perLane(lanes.size());
|
||||
bool anyPoint = false;
|
||||
|
||||
for (std::size_t laneIndex = 0; laneIndex < lanes.size(); ++laneIndex)
|
||||
{
|
||||
for (const SignalRef& ref : lanes[laneIndex].signals)
|
||||
{
|
||||
if (!m_tab->IsVisible(ref))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const SignalData* data = m_dataSet->Resolve(ref);
|
||||
const TimeAxis* axis = m_dataSet->Axis(ref);
|
||||
if (data == nullptr || axis == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (data->finiteCount != 1)
|
||||
{
|
||||
return false; // 该网格里还有常规信号
|
||||
}
|
||||
double pointTime = 0.0;
|
||||
double pointValue = 0.0;
|
||||
if (!SinglePointOfSignal(*data, *axis, pointTime, pointValue))
|
||||
{
|
||||
continue; // 没有有效点(正常数据不会出现):跳过
|
||||
}
|
||||
LanePoints& points = perLane[laneIndex];
|
||||
points.values.push_back(pointValue);
|
||||
points.minTime = std::min(points.minTime, pointTime);
|
||||
points.maxTime = std::max(points.maxTime, pointTime);
|
||||
anyPoint = true;
|
||||
}
|
||||
}
|
||||
if (!anyPoint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 全局时间范围(“最左边/最右边的那个数据点”)。
|
||||
double minTime = std::numeric_limits<double>::infinity();
|
||||
double maxTime = -std::numeric_limits<double>::infinity();
|
||||
for (const LanePoints& points : perLane)
|
||||
{
|
||||
if (points.values.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
minTime = std::min(minTime, points.minTime);
|
||||
maxTime = std::max(maxTime, points.maxTime);
|
||||
}
|
||||
|
||||
// 时间轴:左右各留出**一小格**(一小格 = 主刻 / 五等分,与网格叠底同源)。
|
||||
// ⚠️ 留白会轻微改变“一小格”的值,因此迭代两遍让最终区间与最终网格自洽。
|
||||
if (maxTime > minTime)
|
||||
{
|
||||
const double dataSpan = maxTime - minTime;
|
||||
double cell = TimeGridCellSeconds(dataSpan);
|
||||
for (int pass = 0; pass < 2; ++pass)
|
||||
{
|
||||
cell = TimeGridCellSeconds(dataSpan + 2.0 * cell);
|
||||
}
|
||||
SetTimeView(minTime - cell, maxTime + cell);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 最极端情况:**所有数据点都在同一时刻** ⇒ 按“首个网格加载为单点信号”的方式对齐
|
||||
//(时间跨度 1s、数据点居中)。
|
||||
SetTimeView(minTime - 0.5, minTime + 0.5);
|
||||
}
|
||||
|
||||
Tab().ViewInitialized() = true;
|
||||
Tab().LaneScroll() = 0.0f;
|
||||
Tab().ZoomMode() = PlotZoomMode::NONE;
|
||||
m_rubberRect = gd::RectF();
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
m_dragSignal = SignalRef();
|
||||
m_dragTargetLane = -1;
|
||||
m_hoverSignal = SignalRef();
|
||||
m_rubberAxis = PlotRubberAxis::UNDECIDED;
|
||||
SetHint(std::wstring());
|
||||
|
||||
// 逐网格的 Y 轴:
|
||||
// * 网格内有**多个取值不同**的单点信号 ⇒ 纵向也留出“一小格”的余量(与横向同规则);
|
||||
// * 只有一个取值(含只有一条信号) ⇒ 跨度 1、数据点居中(与单点初始视图一致)。
|
||||
for (std::size_t laneIndex = 0; laneIndex < lanes.size() && laneIndex < m_lanes.size(); ++laneIndex)
|
||||
{
|
||||
const LanePoints& points = perLane[laneIndex];
|
||||
if (points.values.empty())
|
||||
{
|
||||
continue; // 该网格没有可见单点信号:保持原样
|
||||
}
|
||||
const double valueMin = *std::min_element(points.values.begin(), points.values.end());
|
||||
const double valueMax = *std::max_element(points.values.begin(), points.values.end());
|
||||
|
||||
Lane& lane = m_tab->Lanes()[laneIndex];
|
||||
lane.autoRange = false;
|
||||
lane.fitYToSelection = false;
|
||||
if (valueMax > valueMin)
|
||||
{
|
||||
const double valueSpan = valueMax - valueMin;
|
||||
const float laneHeight = m_lanes[laneIndex].rect.Height;
|
||||
double cell = LaneValueGridCell(valueSpan, laneHeight);
|
||||
for (int pass = 0; pass < 2; ++pass)
|
||||
{
|
||||
cell = LaneValueGridCell(valueSpan + 2.0 * cell, laneHeight);
|
||||
}
|
||||
if (!(cell > 0.0))
|
||||
{
|
||||
cell = valueSpan * kLaneRangePadRatio;
|
||||
}
|
||||
lane.yMin = valueMin - cell;
|
||||
lane.yMax = valueMax + cell;
|
||||
}
|
||||
else
|
||||
{
|
||||
lane.yMin = valueMax - 0.5;
|
||||
lane.yMax = valueMax + 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
InvalidateSceneCache();
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::DisableCursor()
|
||||
{
|
||||
Tab().CursorMode() = PlotCursorMode::NONE;
|
||||
@@ -4609,6 +4783,49 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
float PlotView::SinglePointDotRadius(const ResolvedSignal& resolved) const
|
||||
{
|
||||
if (resolved.style == nullptr)
|
||||
{
|
||||
return kMinScatterRadius * m_scale;
|
||||
}
|
||||
// 与 `PaintLane` / 拖拽虚影画那个点时的半径**完全同源**(散点半径随线宽线性增长),
|
||||
// 命中判定与光晕半径都复用它,三者不会出现“看着点中了却没反应”。
|
||||
const double radius = ClampDouble(static_cast<double>(resolved.style->lineWidth) * kScatterRadiusScale,
|
||||
static_cast<double>(kMinScatterRadius),
|
||||
static_cast<double>(kMaxScatterRadius));
|
||||
return static_cast<float>(radius) * m_scale;
|
||||
}
|
||||
|
||||
bool PlotView::SinglePointScreenPos(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
gd::PointF& out) const
|
||||
{
|
||||
if (resolved.data == nullptr || resolved.axis == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (resolved.data->finiteCount != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
double time = 0.0;
|
||||
double value = 0.0;
|
||||
if (!SinglePointOfSignal(*resolved.data, *resolved.axis, time, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 与绘制一致:采样画在 `t + offsetTime` 处,取值先过「调整」变换。
|
||||
const double displayTime = time + resolved.style->offsetTime;
|
||||
const double drawValue = ApplyDrawTransform(*resolved.style, value);
|
||||
if (!std::isfinite(displayTime) || !std::isfinite(drawValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out.X = TimeToX(displayTime);
|
||||
out.Y = ValueToY(drawValue, lane);
|
||||
return true;
|
||||
}
|
||||
|
||||
SignalRef PlotView::HitTestSignal(int x, int y) const
|
||||
{
|
||||
SignalRef empty;
|
||||
@@ -4644,6 +4861,30 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
// ⚠️ **只有单个有效数据点**的信号没有“折线”可测距(`AcquirePolyline` 给不出两点的
|
||||
// 线段),必须直接用数据点的屏幕坐标判定。否则用户永远无法悬停/拖动单点信号
|
||||
//(用户 2026-10-15 报告:“光标瞄到数据点没有高亮、也不能拖拽换网格”)。
|
||||
if (resolved.data->finiteCount == 1)
|
||||
{
|
||||
gd::PointF dot;
|
||||
if (!SinglePointScreenPos(resolved, lane, dot))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float dx = dot.X - fx;
|
||||
const float dy = dot.Y - fy;
|
||||
const float distance = std::sqrt(dx * dx + dy * dy);
|
||||
// 容差取“常规命中容差”与“该点绘制半径”的较大者:点本身很粗时更好点中。
|
||||
const float dotRadius = SinglePointDotRadius(resolved);
|
||||
const float limit = std::max(tolerance, dotRadius);
|
||||
if (distance <= limit && distance <= bestDistance + dotRadius)
|
||||
{
|
||||
bestDistance = distance;
|
||||
best = ref;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 复用绘制阶段的折线顶点:悬停检测通常与上一帧几何完全相同,
|
||||
// 不再为“鼠标移动但命中结果未变”重复扫描全量采样点。
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane);
|
||||
@@ -7442,7 +7683,7 @@ namespace jdi
|
||||
paintOverlays();
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||
void PlotView::PaintSignalHaloFor(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
const PolylineCacheEntry& cached)
|
||||
{
|
||||
if (resolved.style == nullptr)
|
||||
@@ -7451,13 +7692,28 @@ namespace jdi
|
||||
}
|
||||
// 光晕一律用折线描边(与"之前"的画法一致)。曾经在包络带模式下改成"带外扩填充",
|
||||
// 虽然更快但观感变了(用户反馈显示效果下降),因此恢复。
|
||||
PaintSignalHalo(g, resolved, cached.points, cached.breaks);
|
||||
PaintSignalHalo(g, resolved, lane, cached.points, cached.breaks);
|
||||
}
|
||||
|
||||
void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved,
|
||||
void PlotView::PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
const std::vector<gd::PointF>& points,
|
||||
const std::vector<std::size_t>& breaks)
|
||||
{
|
||||
// ⚠️ **只有单个数据点**的信号(默认样式是「点线」,那个点本身是一个粗圆点):
|
||||
// 折线光晕无从下手(不足两个顶点),改为在数据点外包一圈强调色圆环。
|
||||
// 否则悬停时完全没有“可拖拽”的视觉反馈(用户 2026-10-15 报告)。
|
||||
if (resolved.data != nullptr && resolved.data->finiteCount == 1)
|
||||
{
|
||||
gd::PointF dot;
|
||||
if (SinglePointScreenPos(resolved, lane, dot))
|
||||
{
|
||||
const float radius = SinglePointDotRadius(resolved) + 3.0f * m_scale;
|
||||
gd::Pen ring(ToGdiColor(WithAlpha(m_theme->accent, 0x88)), 2.0f * m_scale);
|
||||
g.DrawEllipse(&ring, dot.X - radius, dot.Y - radius, radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool drawLine = (resolved.style->mode != DrawMode::SCATTER);
|
||||
if (!drawLine || breaks.empty())
|
||||
{
|
||||
@@ -7521,7 +7777,7 @@ namespace jdi
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(m_hoverSignal, resolved, lane);
|
||||
if (cached != nullptr)
|
||||
{
|
||||
PaintSignalHaloFor(g, resolved, *cached);
|
||||
PaintSignalHaloFor(g, resolved, lane, *cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7594,7 +7850,7 @@ namespace jdi
|
||||
const LaneVisual& lane = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
if (const PolylineCacheEntry* cached = AcquirePolyline(m_hoverLegend, resolved, lane))
|
||||
{
|
||||
PaintSignalHaloFor(g, resolved, *cached);
|
||||
PaintSignalHaloFor(g, resolved, lane, *cached);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9246,6 +9502,7 @@ namespace jdi
|
||||
const float savedScroll = Tab().LaneScroll();
|
||||
const double savedWideTickSpan = m_wideTickSpan;
|
||||
const float savedWideTickHeight = m_wideTickHeight;
|
||||
const double savedRenderTimeStep = m_renderTimeStep;
|
||||
|
||||
m_plotRect = gd::RectF(0.0f, 0.0f, static_cast<float>(bitmapWidth),
|
||||
static_cast<float>(bitmapHeight));
|
||||
@@ -9313,16 +9570,21 @@ namespace jdi
|
||||
UiFillRect(g, m_area, m_theme->cardBg);
|
||||
|
||||
// 竖向网格刻度沿用**前台视界跨度**(而非宽窗口的跨度),否则竖网格线会只有一半。
|
||||
// ⚠️ 同时把它作为竖向网格的**主刻步长**传给 `PaintLane`(见 `m_renderTimeStep`):
|
||||
// 不传就会让图层用宽窗口跨度重算步长 → 竖向网格线与前台错位、部分整条丢失。
|
||||
const double fgSpan = savedViewMax - savedViewMin;
|
||||
const double timeStep = NiceStep((fgSpan > 0.0) ? fgSpan : 1.0, kTimeTickTarget);
|
||||
m_renderTimeStep = timeStep;
|
||||
std::vector<double> timeTicks;
|
||||
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeStep, timeTicks);
|
||||
PaintLane(g, laneIndex, m_lanes[static_cast<std::size_t>(laneIndex)], timeTicks);
|
||||
m_renderTimeStep = 0.0;
|
||||
}
|
||||
m_fastFrame = savedFastFrame;
|
||||
m_renderWide = savedRenderWide;
|
||||
m_wideTickSpan = savedWideTickSpan;
|
||||
m_wideTickHeight = savedWideTickHeight;
|
||||
m_renderTimeStep = savedRenderTimeStep;
|
||||
m_stripRenderActive = savedStripActive;
|
||||
m_stripX0 = savedStripX0;
|
||||
m_stripX1 = savedStripX1;
|
||||
@@ -9978,8 +10240,13 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 时间网格线(同样补充五等分估读线)。
|
||||
// ⚠️ 主刻步长优先取 `m_renderTimeStep`(渲染信号图层时由前台跨度给出,见该成员的说明):
|
||||
// 图层渲染的 `Tab().ViewMin/ViewMax` 是**宽窗口**区间,直接拿它算 `NiceStep` 会得到比前台
|
||||
// 更大的步长(往往正好 2 倍),使图层里的竖向网格线与前台错位、部分整条丢失。
|
||||
const double timeSpan = Tab().ViewMax() - Tab().ViewMin();
|
||||
const double timeMajorStep = NiceStep((timeSpan > 0.0) ? timeSpan : 1.0, kTimeTickTarget);
|
||||
const double timeMajorStep =
|
||||
(m_renderTimeStep > 0.0) ? m_renderTimeStep
|
||||
: NiceStep((timeSpan > 0.0) ? timeSpan : 1.0, kTimeTickTarget);
|
||||
const double timeMinorStep = timeMajorStep / static_cast<double>(kMinorDivisions);
|
||||
std::vector<double> timeMinorTicks;
|
||||
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeMinorStep, timeMinorTicks);
|
||||
@@ -10074,7 +10341,7 @@ namespace jdi
|
||||
const bool highlighted = (ref == haloRef);
|
||||
if (highlighted)
|
||||
{
|
||||
PaintSignalHaloFor(g, resolved, *cached);
|
||||
PaintSignalHaloFor(g, resolved, lane, *cached);
|
||||
}
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
@@ -13172,6 +13439,19 @@ namespace jdi
|
||||
g.FillPath(&brush, &ghostPath);
|
||||
}
|
||||
}
|
||||
else if (drawScatter && resolved.data->finiteCount == 1)
|
||||
{
|
||||
// ⚠️ **只有单个有效数据点**的信号:折线缓存给不出顶点(`points` 可能为空),
|
||||
// 因此直接按数据点的屏幕坐标画那个圆点,否则拖动时虚影会**整个消失**
|
||||
//(用户 2026-10-15 要求单点信号也能拖拽换网格)。
|
||||
gd::PointF dot;
|
||||
if (SinglePointScreenPos(resolved, sourceVisual, dot))
|
||||
{
|
||||
const float radius = SinglePointDotRadius(resolved);
|
||||
gd::SolidBrush brush(ToGdiColor(ghostColor));
|
||||
g.FillEllipse(&brush, dot.X - radius, dot.Y - radius, radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user