适应视图:撤回旧策略并重做边界规则(单值散点让出半径 + 单点簇视图)

用户 2026-10-15 第五轮报告的两个问题与要求(详见 Documents/agent_mem.md §9.33):

1. 加载文件后不勾选任何信号、连点几次「适应视图」,再勾选一个单值信号时,
   初次自适应没有发生、数据点既不居中也不在网格内。
   根因:FitAll() 在没有任何可见信号时也把 Tab().ViewInitialized() 置为 true,
        随后勾选第一个信号时 Application::SynchronizeView 认为"已经初始化",
        于是跳过自适应,视界停在"无信号"那一次的整段范围上(实测 MF4 为 42.386s,
        数据点被推到绘图区右边缘之外)。
   修法:FitAll() 只有在存在可见信号时才置位 ViewInitialized();
        并让"初次自适应"与工具栏「适应视图」共用 FitAll() 这一条实现
        (不再先试 FitSinglePointSignal)。

2. 仅勾选 WIN_Motor_StallCurrentVolBrk(单值信号)与 m_MOT_Current_DCom[0](连续信号)
   时点「适应视图」,单值信号的点被挤到显示区域外只剩半个;上一轮"为左右空出一小格"
   的需求效果不佳。
   修法(按用户要求先撤回旧策略、再重做规则):
   * 撤回并删除 FitSinglePointLanes()(左右各留一小格)、TimeGridCellSeconds()、
     LaneValueGridCell()、FitSinglePointSignal()(首个单值信号的时间跨度 1s 视图)。
   * 新规则:常规全局适应后,任何一条边界(时间轴左/右、每个网格幅值上/下)
     若被一个单值信号的数据点顶住,就把该边界向外让出该散点的半径(+1 像素),
     使其完整显示在网格内;只在散点会被裁切时才扩展(内部散点使视界一点不动);
     边界不是单值信号的数据点时就落在该信号点上(时间轴不留额外余量)。
     判断依据用信号自身的数据点,而不是时间轴范围(MF4 里单值信号与连续信号
     常共用同一条时间轴,用轴的范围会误判)。
   * 特例:全部可见数据点都是单值信号且时刻完全相同时,时间轴以该时刻居中、
     缩放到一大格 = 1s(视界 = kTimeTickTarget × 1s = 10s,此时 NiceStep(10,10) 恰为 1.0),
     每个网格的幅值区间以"能完整画出该网格内的全部数据点"为准
     (退化成单值时以跨度 1 居中)。
   * 新增 FitContribution / CollectFitContributions / FitLaneHeight /
     ApplySinglePointValueMargins / FitSinglePointCluster 五个成员承载上述规则。

验证(全部离屏,不占用鼠标 / 键盘 / 屏幕):
- .preflight/check(SW_HIDE + 只注入合成消息 + 进程/线程 BELOW_NORMAL)**62/62 通过**,
  连跑 3 次稳定:[A] 初次自适应、[B] 时间边界(外扩量恰为半径 + 1px)、
  [C] 幅值边界、[D] 单点簇特例(跨度 10、一大格 1s、每个点都在网格内)、
  [E] 回归(普通信号视界 = 数据范围、网格保持自动自适应、§9.31 的
  "适应视图 ↔ Y 轴自适应互不干扰"仍逐位成立)。
- [F] 真实文件 TestData/MF4-Record.MF4:WIN_Motor_StallCurrentVolBrk 确实只有 1 个有效点
  (t = 0.005492),与 m_MOT_Current_DCom[0](8475 样点)复现用户场景 —— 点「适应视图」后
  散点圆心离绘图区左边界 16.5px、半径 15px,完整可见(修复前它正好压在边界上、只剩半个)。
- A/B 判别:回退四处规则后 **20 项如实失败**,失败信息即用户症状
  ("the dot is horizontally centred (got 1697, want 1084.75)"、
   "[F] initial view = one big cell per second (got 42.386313, want 10.000000)")。
- make release:全部源文件重新编译 **零警告**;链接用临时 `make release TARGET_FILE_NAME=Target_check` 通过。

同步记忆:Documents/agent_mem.md 新增 §9.33、§11.2 第 69–71 条硬约束(并把第 68 条与
§9.32 的第四小节显式标注为"已作废/被取代")、变更日志新行。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-15 18:35:11 +08:00
1 parent aee3a388b5
commit 73389f424c
4 files changed
+470 -282

No files matched your search

+317 -233
View File
@@ -156,6 +156,31 @@ namespace jdi
*/
const std::size_t kSnapScanWindowFactor = 64;
/**
* @brief 「所有可见数据点都是单值信号、且时刻完全相同」时,时间轴**一个大格**对应的秒数。
*
* 用户 2026-10-15 明确要求:这种最极端情况下时间轴以该时刻居中、并缩放到
* **一大格 = 1s** 的级别。视界跨度取 `kTimeTickTarget` 个大格 ⇒
* 跨度 = 10s、`NiceStep(10, kTimeTickTarget)` = **1.0 秒**,正好是"一大格 1s"。
*/
const double kSinglePointClusterSecondsPerCell = 1.0;
/**
* @brief 让边界处的单值散点"完整显示"时,在半径之外额外多留的像素(设备像素)。
*
* 只让出**正好等于半径**的量会让圆点边缘贴着网格边框(抗锯齿边缘看起来被切了一刀),
* 因此再多留 1 个像素。
*/
const float kSinglePointBoundaryPadPixels = 1.0f;
/**
* @brief 判定"多个单值数据点处于同一时刻"的相对误差。
*
* 同一列里读出的时刻是完全相同的 double;但不同来源(例如 MF4 的整数刻度 ×
* 通道组的标定系数)可能差 1ulp,因此用相对误差而不是精确相等。
*/
const double kSinglePointSameTimeEpsilon = 1.0e-9;
const float kLaneLabelPad = 6.0f; /**< 网格内侧标尺带的内边距。 */
const float kLegendHeight = 19.0f; /**< 图例条目高度(逻辑像素)。 */
const float kLegendPad = 5.0f; /**< 图例相对网格左上角的内缩。 */
@@ -1315,23 +1340,69 @@ namespace jdi
BuildLaneVisuals();
}
/**
* @brief 「适应视图」:时间轴与幅值轴同时自适应到可见信号范围。
*
* 边界规则(用户 2026-10-15 第五轮**重新定义**,取代此前"左右各留一小格"的做法):
* * 先做一次常规的全局适应(时间 = 可见信号的整段时间范围;幅值走
* `ComputeLaneRange`,含 `PadValueRange` 的统一留白);
* * 若某条边界(时间轴的左 / 右、每个网格幅值的上 / 下)被一个**单值信号的
* 数据点**顶住,则把该边界再向外让出**该散点的半径**(+1 像素),使它
* **完整地显示在网格内**;边界若不是单值信号的数据点,则边界就落在这个信号点上
* (时间轴不留额外余量);
* * 唯一特例:**所有可见信号都是单值数据点、且时刻完全相同**时,时间轴以该时刻
* 居中并缩放到"一大格 = 1s"(`kSinglePointClusterSecondsPerCell`),
* 每个网格的幅值区间保证该网格内的数据点都能完整画出。
*/
void PlotView::FitAll()
{
if (m_dataSet == nullptr || m_dataSet->Empty())
{
SetTimeView(0.0, 1.0);
Tab().ViewInitialized() = true;
return;
}
// 「网格内只显示单点信号」时的自适应(含“多个网格、每个网格只有一个数据点”):
// 必须保证所有数据点都在屏幕内(用户 2026-10-15 报告“多网格单点时自适应把点甩飞”)。
// 该函数也覆盖“只有一个网格 + 单点信号”的情形(时间跨度 1s、居中,与原行为一致)。
if (FitSinglePointLanes())
if (m_tab == nullptr)
{
return;
}
// 全部可见信号的"边界贡献"(时刻范围 / 绘制取值 / 是否单点 / 散点半径)。
std::vector<std::vector<FitContribution>> perLane;
const std::size_t visibleCount = CollectFitContributions(perLane);
// ---- 特例:所有可见数据点都是单值信号、且时刻完全相同 ----
bool allSingle = true;
bool sameTime = true;
double sharedTime = 0.0;
bool haveTime = false;
for (const std::vector<FitContribution>& items : perLane)
{
for (const FitContribution& item : items)
{
if (!item.singlePoint)
{
allSingle = false;
continue;
}
if (!haveTime)
{
sharedTime = item.pointTime;
haveTime = true;
}
else if (std::fabs(item.pointTime - sharedTime) >
kSinglePointSameTimeEpsilon * std::max(1.0, std::fabs(sharedTime)))
{
sameTime = false;
}
}
}
if (visibleCount > 0 && allSingle && sameTime)
{
FitSinglePointCluster(perLane, sharedTime);
return;
}
// ---- 常规的整段适应 ----
double low = 0.0;
double high = 1.0;
TimeBounds(low, high);
@@ -1342,8 +1413,48 @@ namespace jdi
high += padding;
}
// 时间边界处的单值散点不能被裁切:**被裁切的那一侧**向外让出散点半径。
// ⚠️ 迭代两遍:让出余量会改变跨度,而"半径换算成多少时间"又依赖跨度。
const double plotWidth = std::max(1.0, static_cast<double>(m_plotRect.Width));
for (int pass = 0; pass < 2; ++pass)
{
const double span = high - low;
if (!(span > 0.0))
{
break;
}
for (const std::vector<FitContribution>& items : perLane)
{
for (const FitContribution& item : items)
{
if (!item.singlePoint || !(item.dotRadius > 0.0f))
{
continue;
}
const double radius =
(static_cast<double>(item.dotRadius) + kSinglePointBoundaryPadPixels) * span /
plotWidth;
if (item.pointTime - radius < low)
{
low = item.pointTime - radius;
}
if (item.pointTime + radius > high)
{
high = item.pointTime + radius;
}
}
}
}
SetTimeView(low, high);
Tab().ViewInitialized() = true;
// ⚠️ **只有真的存在可见信号时才置位 `ViewInitialized()`**:没有任何信号被勾选时
//(用户先连点几次「适应视图」)必须保持"未初始化",否则随后勾选第一个信号时
// `Application::SynchronizeView` 会**跳过初次自适应** —— 数据点既不居中,
// 还可能有一半被裁在网格外(用户 2026-10-15 报告)。
if (visibleCount > 0)
{
Tab().ViewInitialized() = true;
}
Tab().LaneScroll() = 0.0f;
// 只放下进行中的选区与拖拽,**不改变「选区放大」等缩放模式的启用状态**
// (用户明确要求:点适应视图后仍保持已启用的缩放模式)。
@@ -1356,106 +1467,246 @@ namespace jdi
? std::wstring()
: L"横向或纵向拖动以放大选择区域,ESC退出选区模式");
if (m_tab != nullptr)
// ---- 逐网格的幅值轴 ----
// 保留既有语义:开启了「选区 Y 轴自适应」的网格按新的(整段)时间视图重新匹配,
// 其余网格回到整段自适应;随后再为"被单值散点顶住的边界"让出余量。
const float laneHeight = FitLaneHeight();
for (std::size_t index = 0; index < m_tab->Lanes().size(); ++index)
{
// 适应视图 = 回到初始状态:恢复整段数据的自动自适应。
// 但**开启了「选区 Y 轴自适应」的网格必须继续跟随时间视图**:
// 直接置 autoRange 会让 ComputeLaneRange 忽略已经算好的时间段幅值,
// 表现为"点一下适应视图就把 Y 轴自适应关掉了"。这里改为按新的
// (整段)时间视图重新匹配一次,开关状态与视觉效果都保持。
const int laneCount = static_cast<int>(m_tab->Lanes().size());
for (int index = 0; index < laneCount; ++index)
const int laneIndex = static_cast<int>(index);
if (m_tab->Lanes()[index].fitYToSelection)
{
if (m_tab->Lanes()[static_cast<std::size_t>(index)].fitYToSelection)
{
AutoRangeLaneToView(index);
}
else
{
m_tab->Lanes()[static_cast<std::size_t>(index)].autoRange = true;
}
AutoRangeLaneToView(laneIndex);
}
else
{
m_tab->Lanes()[index].autoRange = true;
}
ApplySinglePointValueMargins(laneIndex, perLane[index], laneHeight);
}
// 适应视图已回到初始状态,这里不再移动光标(光标允许停留在可视区外)。
BuildLaneVisuals();
}
bool PlotView::FitSinglePointSignal()
std::size_t PlotView::CollectFitContributions(std::vector<std::vector<FitContribution>>& perLane) const
{
if (m_tab == nullptr || m_dataSet == nullptr || m_dataSet->Empty())
perLane.clear();
if (m_tab == nullptr || m_dataSet == nullptr)
{
return false;
return 0;
}
perLane.assign(m_tab->Lanes().size(), std::vector<FitContribution>());
// 只在「当前没有任何信号被勾选,勾选的第一个信号只有一个有效数据点」时生效
// (用户 2026-10-15 明确要求):此时整段自适应会让时间轴覆盖整条时间列、
// Y 轴退化成几乎为 0 的区间,数据点既不在中心也看不出来。
SignalRef only;
int visibleCount = 0;
for (const Lane& lane : m_tab->Lanes())
std::size_t total = 0;
for (std::size_t laneIndex = 0; laneIndex < m_tab->Lanes().size(); ++laneIndex)
{
for (const SignalRef& ref : lane.signals)
for (const SignalRef& ref : m_tab->Lanes()[laneIndex].signals)
{
if (!m_tab->IsVisible(ref))
{
continue;
}
only = ref;
if (++visibleCount > 1)
const SignalData* data = m_dataSet->Resolve(ref);
const TimeAxis* axis = m_dataSet->Axis(ref);
if (data == nullptr || axis == nullptr || data->finiteCount == 0)
{
return false; // 多个可见信号:按常规的整段自适应处理
continue;
}
FitContribution item;
item.ref = ref;
// 与 `ComputeVisibleTimeBounds` 同源:时间边界用**时间轴**的范围
//(一个分组的多条信号可能共用一条轴,轴范围才是"这一段数据"的边界)。
item.minTime = axis->minTime;
item.maxTime = axis->maxTime;
item.singlePoint = (data->finiteCount == 1);
if (item.singlePoint)
{
double time = 0.0;
double value = 0.0;
if (!SinglePointOfSignal(*data, *axis, time, value))
{
item.singlePoint = false;
}
else
{
const SignalStyle* style = m_dataSet->Style(ref);
item.pointTime = time;
item.pointValue =
(style != nullptr) ? ApplyDrawTransform(*style, value) : value;
item.dotRadius = SinglePointDotRadius(ResolveSignal(ref));
}
}
perLane[laneIndex].push_back(item);
++total;
}
}
return total;
}
float PlotView::FitLaneHeight() const
{
// 与 `BuildLaneVisuals()` **完全同源**:网格高度 = 绘图区高 / 网格数(不低于最小高度)。
// 直接复用它(而不是读 `m_lanes[i].rect.Height`):`FitAll` 可能在网格数量刚变化、
// 布局尚未重建时被调用,用公式能拿到**将要生效**的高度。
const std::size_t count = (m_tab != nullptr) ? m_tab->Lanes().size() : 0;
if (count == 0 || !(m_plotRect.Height > 0.0f))
{
return 0.0f;
}
float height = m_plotRect.Height / static_cast<float>(count);
if (height < m_minLaneHeight * m_scale)
{
height = m_minLaneHeight * m_scale;
}
return height;
}
void PlotView::ApplySinglePointValueMargins(int laneIndex, const std::vector<FitContribution>& items,
float laneHeight)
{
if (m_tab == nullptr || m_dataSet == nullptr || laneHeight <= 1.0f)
{
return;
}
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return;
}
bool anySingle = false;
for (const FitContribution& item : items)
{
if (item.singlePoint && item.dotRadius > 0.0f)
{
anySingle = true;
break;
}
}
if (!anySingle)
{
return; // 该网格的边界不是单值散点顶住的:保持现有(自动)区间
}
// 该网格当前**生效**的幅值区间(自动自适应时即 `ComputeLaneRange` 的结果)。
double low = 0.0;
double high = 0.0;
m_tab->ComputeLaneRange(*m_dataSet, laneIndex, low, high);
if (!(high > low))
{
return;
}
double newLow = low;
double newHigh = high;
// 迭代两遍:让出余量会改变跨度,而"半径换算成多少数值"又依赖跨度。
for (int pass = 0; pass < 2; ++pass)
{
const double span = newHigh - newLow;
if (!(span > 0.0))
{
break;
}
for (const FitContribution& item : items)
{
if (!item.singlePoint || !(item.dotRadius > 0.0f))
{
continue;
}
const double radius =
(static_cast<double>(item.dotRadius) + kSinglePointBoundaryPadPixels) * span /
static_cast<double>(laneHeight);
if (item.pointValue - radius < newLow)
{
newLow = item.pointValue - radius;
}
if (item.pointValue + radius > newHigh)
{
newHigh = item.pointValue + radius;
}
}
}
if (visibleCount != 1)
if (newLow == low && newHigh == high)
{
return false;
return; // 散点本来就完整可见:不改动区间(保持自动自适应的语义)
}
const SignalData* signal = m_dataSet->Resolve(only);
const TimeAxis* axis = m_dataSet->Axis(only);
if (signal == nullptr || axis == nullptr || signal->finiteCount != 1)
{
return false;
}
// ⚠️ 必须**同时**关掉 `autoRange`:否则 `ComputeLaneRange` 会忽略下面写入的区间。
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
lane.autoRange = false;
lane.yMin = newLow;
lane.yMax = newHigh;
}
double pointTime = 0.0;
double pointValue = 0.0;
if (!SinglePointOfSignal(*signal, *axis, pointTime, pointValue))
{
return false;
}
// 时间轴:跨度 = **一个大格(1s)**,数据点居中。
const double halfTimeSpan = 0.5;
SetTimeView(pointTime - halfTimeSpan, pointTime + halfTimeSpan);
void PlotView::FitSinglePointCluster(const std::vector<std::vector<FitContribution>>& perLane,
double sharedTime)
{
// 时间轴:该时刻居中、**一大格 = 1s**(视界 = 大格数 × 1s)。
const double span =
static_cast<double>(kTimeTickTarget) * kSinglePointClusterSecondsPerCell;
SetTimeView(sharedTime - span * 0.5, sharedTime + span * 0.5);
Tab().ViewInitialized() = true;
Tab().LaneScroll() = 0.0f;
Tab().ZoomMode() = PlotZoomMode::NONE;
// ⚠️ **不要**在这里复位 `ZoomMode()`:用户明确要求"点适应视图后仍保持已启用的
// 缩放模式"(`FitAll` 的常规路径同样只更新提示、不动缩放模式)。
m_rubberRect = gd::RectF();
m_dragMode = PlotDragMode::NONE;
m_dragSignal = SignalRef();
m_dragTargetLane = -1;
m_hoverSignal = SignalRef();
m_rubberAxis = PlotRubberAxis::UNDECIDED;
SetHint(std::wstring());
SetHint(Tab().ZoomMode() == PlotZoomMode::NONE
? std::wstring()
: L"横向或纵向拖动以放大选择区域,ESC退出选区模式");
// Y 轴:跨度 = **一个大格(1)**,数据点居中。
// ⚠️ 必须同时关掉 `autoRange`(否则会被整段自适应覆盖)与 `fitYToSelection`
//(选区自适应会按可见区间算出退化区间;全局 Y 轴自适应开启时那条路径也会生效)。
const double halfValueSpan = 0.5;
for (Lane& lane : m_tab->Lanes())
const float laneHeight = FitLaneHeight();
for (std::size_t index = 0; index < perLane.size() && index < m_tab->Lanes().size(); ++index)
{
if (lane.signals.size() != 1 || !(lane.signals.front() == only))
const std::vector<FitContribution>& items = perLane[index];
double valueLow = 0.0;
double valueHigh = 0.0;
float radius = 0.0f;
bool any = false;
for (const FitContribution& item : items)
{
continue;
if (!item.singlePoint || !std::isfinite(item.pointValue))
{
continue;
}
valueLow = any ? std::min(valueLow, item.pointValue) : item.pointValue;
valueHigh = any ? std::max(valueHigh, item.pointValue) : item.pointValue;
radius = std::max(radius, item.dotRadius);
any = true;
}
if (!any)
{
continue; // 该网格没有可见单值信号:保持原样
}
// 幅值跨度:以容纳"网格内的全部数据点"为下限(退化成单值时取 1,
// 与"单值信号居中显示"的观感一致),并保证**每个散点都完整在网格内**。
double range = std::max(1.0, valueHigh - valueLow);
if (laneHeight > 1.0f && radius > 0.0f)
{
for (int pass = 0; pass < 2; ++pass)
{
const double margin =
(static_cast<double>(radius) + kSinglePointBoundaryPadPixels) * range /
static_cast<double>(laneHeight);
range = std::max(1.0, valueHigh - valueLow + 2.0 * margin);
}
}
const double center = (valueLow + valueHigh) * 0.5;
Lane& lane = m_tab->Lanes()[index];
lane.autoRange = false;
lane.fitYToSelection = false;
lane.yMin = pointValue - halfValueSpan;
lane.yMax = pointValue + halfValueSpan;
lane.yMin = center - range * 0.5;
lane.yMax = center + range * 0.5;
}
InvalidateSceneCache();
BuildLaneVisuals();
return true;
}
bool PlotView::SinglePointOfSignal(const SignalData& signal, const TimeAxis& axis, double& time,
@@ -1486,173 +1737,6 @@ namespace jdi
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;