自由标记改进:默认不启用光标模式 + 面板四列对齐 + 光标模式切换按钮

用户需求(逐条落地):
1) 默认不启用网格私有光标,只有鼠标处于信号上方时才显示数据点数值;命中
   「自动点线」稀疏判据的信号,只有靠近数据点时才有数值显示;
2) 有数值预览时左键单击固定该数据点(固定点样式不变:半透明纵向细线);鼠标
   下方没有信号曲线时,按下/拖动就是普通的平移视界(选区放大模式下照旧框选);
3) 浮动面板最下方新增切换按钮「启用光标模式」↔「关闭光标模式」;启用后与最初
   实现一致(跟随鼠标的纵向自由光标),并修掉「光标模式下拖不动浮动面板」;
   鼠标处于浮动面板上时网格不画自由光标,面板上的操作不穿透到网格;
4) 面板只列出确实有固定点的信号(否则该信号名条目不出现);
5) 每个信息元素纵向对齐:取值 / 增量 / 时刻 / 间隔各自一列。

数据层
- Lane 新增 bool freeMarkerCursor(默认 false,功能总开关仍是 freeMarker);
  LaneCarry 同步携带。
- PlotView 新增 SetLaneFreeMarkerCursor / LaneFreeMarkerCursor /
  ToggleLaneFreeMarkerCursor,切换时立即作废属于该网格的探针。

探针与绘制
- FreeMarkerProbe 新增 cursorMode。
- 非光标模式:稀疏判据成立但吸附失败 ⇒ 跳过该信号;其余要求鼠标与曲线纵向距离
  在 kFreeMarkerHoverTolerance(8 逻辑像素)以内;都不满足 ⇒ 探针无效 ⇒ 按下即
  普通平移/框选,不会误固定数据点。
- 光标模式:与之前实现一致(网格内始终有效)。
- PaintFreeMarker:纵向线 + 上下把手只在光标模式绘制;取值胶囊/数据点/吸附圆环
  两种模式都画。

浮动面板
- 底部新增「启用光标模式 / 关闭光标模式」切换按钮(layout.button),手型光标 +
  悬停高亮,PanelSlotKey 混入 freeMarkerCursor。
- 只给有固定点的信号发名称行(高度分支与矩形分支同步跳过空信号)。
- 四列固定横坐标(EdgePanelLayout::markerColumns:取值 / 增量 / 时刻 / 间隔),
  面板宽度 340 → 415(= 4 列 + 间隙 + 行 ✕ + 两侧内边距);绘制一律左对齐 +
  省略号截断,双击分行改按列边界判断(不再用 xxxRight - 40*scale 反推)。

修复的缺陷(用户报告)
- 「光标模式下拖不动浮动面板」的真因不是命中判定,而是 OnMouseMove 的
  EDGE_PANEL_MOVE 分支里 switch (m_edgePanelDragKind) 漏了 MARK:拖动坐标被写进
  lane.edgePanel,而布局从 lane.freeMarkerPanel 读 ⇒ 面板纹丝不动。已补 case。
- 鼠标压在浮动面板上时:OnMouseMove 先 TopPanelAtPoint,命中即清空探针并跳过固定
  点 ✕ 的悬停判定;OnLButtonDown 的自由标记分支增加 freeOverPanel 守卫,
  确保面板操作不穿透到网格(也不再绘制穿过面板的自由光标)。

验证(全程离屏,不占用鼠标/键盘/屏幕)
- .preflight/markcheck 重写并扩到 61/61:默认无光标、贴近曲线才有效、远离曲线单击
  不固定且拖动仍是平移、四列几何对齐、按钮切换光标模式、光标模式下拖动面板确实
  移动、面板上不画光标、选区放大共存、Δt 编辑重排、两处 ✕、空信号不出行、深度
  放大吸附、视界外固定点不绘制。
- 回归:editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、
  convcheck 18/18、check 136/137(唯一失败项是此前就存在的不稳定断言)、真实
  MF4-Record.MF4 仍 283 条信号。
- make release / make debug 零警告;Build/Release/Target.exe 834,560 字节。
- 同步更新 Documents/agent_mem.md(§11.2 第 189–193 条 + 文末本轮章节)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-20 20:39:42 +08:00
1 parent dd8f63aa0e
commit 067128f46c
8 files changed
+412 -86

No files matched your search

@@ -1655,6 +1655,44 @@ namespace jdi
return enabled;
}
bool PlotView::SetLaneFreeMarkerCursor(int laneIndex, bool enabled)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
if (!lane.freeMarker || lane.freeMarkerCursor == enabled)
{
return false;
}
lane.freeMarkerCursor = enabled;
// 光标模式的可见结果依赖鼠标位置,模式一变必须让已缓存的探针立刻作废,
// 否则关闭后仍会残留一条纵向光标(或开启后要等下一次移动才出现)。
if (m_freeMarker.laneIndex == laneIndex)
{
m_freeMarker = FreeMarkerProbe();
}
return true;
}
bool PlotView::LaneFreeMarkerCursor(int laneIndex) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
return lane.freeMarker && lane.freeMarkerCursor;
}
bool PlotView::ToggleLaneFreeMarkerCursor(int laneIndex)
{
const bool enabled = !LaneFreeMarkerCursor(laneIndex);
SetLaneFreeMarkerCursor(laneIndex, enabled);
return enabled;
}
bool PlotView::MeanFilterValueAt(const TimeAxis& axis, const SignalData& signal, int points, int iterations,
double time, double& value) const
{
+70 -18
View File
@@ -956,6 +956,12 @@ namespace jdi
// 「自由标记」面板:行尾 ✕ 与信号名右侧 ✕ 都是可点击控件(手型)。
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MARK)
{
// 底部「启用光标模式 / 关闭光标模式」切换按钮。
if (layout.button.Width > 0.0f &&
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{
if (row.close.Width > 0.0f &&
@@ -1418,6 +1424,12 @@ namespace jdi
lane.weakPeakPanel.x = left;
lane.weakPeakPanel.y = top;
break;
case EdgePanelKind::MARK:
// ⚠️ 漏掉这一支会让「自由标记」面板**拖不动**(位置写进了 edgePanel,
// 下一帧又从 freeMarkerPanel 读到旧值 ⇒ 面板纹丝不动)。
lane.freeMarkerPanel.x = left;
lane.freeMarkerPanel.y = top;
break;
case EdgePanelKind::COUNT:
default:
lane.edgePanel.x = left;
@@ -1436,9 +1448,22 @@ namespace jdi
// ---- 「自由标记」:跟随光标解析 + 两处 ✕ 的悬停态 ----
// 跟随光标是**每帧随鼠标变化**的叠加层(不进任何缓存),因此只要鼠标位置变了就得重绘。
//
// ⚠️ 鼠标压在浮动面板上时:**不画**网格内的自由光标、也**不**做任何自由标记交互
//(用户 2026-10-20 明确要求:"当鼠标处于浮动面板上时网格不绘制自由光标,
// 用户操作浮动面板时的操作不要穿透到网格上")。
bool freeMarkerOverPanel = false;
{
const FreeMarkerProbe probe = ProbeFreeMarker(x, y);
int freePanelLane = -1;
int freePanelKind = 0;
freeMarkerOverPanel = TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y),
freePanelLane, freePanelKind);
}
{
const FreeMarkerProbe probe =
freeMarkerOverPanel ? FreeMarkerProbe() : ProbeFreeMarker(x, y);
const bool probeChanged = (probe.valid != m_freeMarker.valid) ||
(probe.cursorMode != m_freeMarker.cursorMode) ||
(probe.laneIndex != m_freeMarker.laneIndex) ||
(probe.time != m_freeMarker.time) ||
(probe.snapped != m_freeMarker.snapped) ||
@@ -1454,14 +1479,17 @@ namespace jdi
{
int hoverLane = -1;
std::size_t hoverItem = static_cast<std::size_t>(-1);
for (const FreeMarkerHit& hit : m_freeMarkerHits)
if (!freeMarkerOverPanel)
{
if (hit.close.Width > 0.0f &&
UiPointInRect(hit.close, static_cast<float>(x), static_cast<float>(y)))
for (const FreeMarkerHit& hit : m_freeMarkerHits)
{
hoverLane = hit.laneIndex;
hoverItem = hit.itemIndex;
break;
if (hit.close.Width > 0.0f &&
UiPointInRect(hit.close, static_cast<float>(x), static_cast<float>(y)))
{
hoverLane = hit.laneIndex;
hoverItem = hit.itemIndex;
break;
}
}
}
if (hoverLane != m_freeMarkerHoverLane || hoverItem != m_freeMarkerHoverItem)
@@ -2131,6 +2159,13 @@ namespace jdi
if (kind == EdgePanelKind::MARK && m_tab != nullptr)
{
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
// 底部切换按钮「启用光标模式 / 关闭光标模式」(默认关闭)。
if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{
ToggleLaneFreeMarkerCursor(index);
m_dragMode = PlotDragMode::NONE;
return true;
}
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{
if (row.close.Width > 0.0f && UiPointInRect(row.close, fx, fy))
@@ -2192,11 +2227,16 @@ namespace jdi
}
// ---- 「自由标记」----
// ⚠️ 鼠标压在浮动面板上时**一律不参与**自由标记的命中与固定
//(面板自身的交互已在上面消费,这里再兜一层,确保不穿透到网格)。
const bool freeOverPanel = TopPanelAtPoint(fx, fy, guardLane, guardKind);
// (1) 网格内固定点的 ✕:按下即移除该固定点(矩形由绘制时登记)。
for (const FreeMarkerHit& hit : m_freeMarkerHits)
{
if (hit.close.Width > 0.0f && UiPointInRect(hit.close, fx, fy) && m_tab != nullptr &&
hit.laneIndex >= 0 && hit.laneIndex < static_cast<int>(m_tab->Lanes().size()))
if (!freeOverPanel && hit.close.Width > 0.0f && UiPointInRect(hit.close, fx, fy) &&
m_tab != nullptr && hit.laneIndex >= 0 &&
hit.laneIndex < static_cast<int>(m_tab->Lanes().size()))
{
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(hit.laneIndex)];
tabLane.RemoveFreeMarkerAt(hit.itemIndex);
@@ -2209,6 +2249,9 @@ namespace jdi
// (常规模式 = 平移;选区放大模式 = 框选)。
// ⚠️ 用户明确要求:「仅单击时」才算固定数据点 —— 因此这里**只登记意图**,
// 真正固定发生在 `OnLButtonUp` 且整段操作没有发生过拖动。
// ⚠️ 默认(非光标模式)下 `ProbeFreeMarker` 只在鼠标贴近曲线时有效 ⇒
// 鼠标下方没有信号时按下就是普通的平移/框选(用户明确要求)。
if (!freeOverPanel)
{
const int freeLane = LaneAtY(y);
if (freeLane >= 0 && freeLane < static_cast<int>(m_tab ? m_tab->Lanes().size() : 0) &&
@@ -2516,11 +2559,14 @@ namespace jdi
continue;
}
const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[row.itemIndex];
// 分隔点:时间列左边界(`markerTimeRight` 减去最宽文本的近似值)。
// 右侧(Δt 列)→ 编辑间隔;左侧 → 编辑自身时刻。
const float splitX = layout.markerTimeRight - 40.0f * m_scale;
// 分隔点:直接用布局里的列边界,不再"反推"列位置。
// 右侧(③ 时刻列 或 ④ 间隔列)→ 编辑间隔;① 取值 / ② 增量列 → 编辑自身时刻。
const float deltaColumnLeft =
layout.markerColumns[EdgePanelLayout::MARKER_COL_DT].left;
const float timeColumnLeft =
layout.markerColumns[EdgePanelLayout::MARKER_COL_TIME].left;
m_editMarkerIndex = row.itemIndex;
if (static_cast<float>(x) >= splitX)
if (static_cast<float>(x) >= deltaColumnLeft)
{
// 间隔编辑:初始值 = 当前间隔(秒)。最左边的点没有前一项 ⇒ 不允许编辑。
std::vector<std::size_t> order;
@@ -2538,17 +2584,23 @@ namespace jdi
break;
}
}
const gd::RectF anchor(layout.markerTimeRight,
row.rect.Y,
std::max(10.0f, layout.markerDeltaRight - layout.markerTimeRight),
row.rect.Height);
const gd::RectF anchor(
deltaColumnLeft, row.rect.Y,
std::max(10.0f, layout.markerColumns[EdgePanelLayout::MARKER_COL_DT].width),
row.rect.Height);
BeginEdit(PlotEditTarget::FREE_MARKER_DELTA, panelLane,
FormatValue(item.time - previousTime), anchor);
m_editFieldRect = row.rect;
return true;
}
// 时刻编辑:输入框覆盖「③ 时刻」列(列起点由布局给出)。
const gd::RectF timeAnchor(
timeColumnLeft, row.rect.Y,
std::max(10.0f, layout.markerColumns[EdgePanelLayout::MARKER_COL_TIME].width +
layout.markerColumns[EdgePanelLayout::MARKER_COL_DT].width),
row.rect.Height);
BeginEdit(PlotEditTarget::FREE_MARKER_TIME, panelLane, FormatValue(item.time),
row.rect);
timeAnchor);
m_editFieldRect = row.rect;
return true;
}
+26 -1
View File
@@ -346,8 +346,29 @@ namespace jdi
* 取固定值而**不做逐行文本测量**:`EdgePanelWidth` 会被 `EdgePanelLayoutFor` 调用,
* 而后者在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用(每次鼠标移动数次);
* 逐行取采样值算宽度会变成"鼠标一动就做几十次采样查询"。
*
* 宽度 = 左右内边距 + 四列(取值/增量/时刻/间隔)+ 行 ✕ ⇒ 保证四列在纵向上对齐。
*/
inline const float kFreeMarkerPanelWidth = 340.0f;
inline const float kFreeMarkerPanelWidth = 415.0f;
/**
* @brief 「自由标记」面板四列的逻辑宽度(**按此顺序**:取值 / 增量 / 时刻 / 间隔)。
*
* 固定列宽(不按文本测量,见上),绘制时用 `UiEllipsize` 截断超长文本。
* 四列左对齐且右边界由列宽推得 ⇒ 同一信息在所有条目上纵向上严格对齐。
*/
inline const float kFreeMarkerColumnWidths[4] = {124.0f, 86.0f, 78.0f, 90.0f};
/** 「自由标记」面板行内相邻两列之间的间距。 */
inline const float kFreeMarkerColumnGap = 2.0f;
/**
* @brief 非光标模式下判定"鼠标在信号曲线上"的纵向容差(逻辑像素)。
*
* 默认(不启用光标模式)时只有鼠标贴近曲线才显示取值预览;这个容差决定"贴近"的量级。
* 取得太小会很难命中细线,太大则会在空白处误判成"有信号"从而吃掉平移操作。
*/
inline const float kFreeMarkerHoverTolerance = 8.0f;
/** 固定点取值胶囊与 ✕ 之间的间距。 */
inline const float kFreeMarkerCloseGap = 3.0f;
@@ -504,6 +525,10 @@ namespace jdi
inline const wchar_t* const kWeakPeakIterationsSliderLabel = L"迭代次数";
inline const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
inline const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
/** 「自由标记」面板底部切换按钮:光标模式**未启用**时的文案(点击后启用)。 */
inline const wchar_t* const kFreeMarkerCursorOnLabel = L"启用光标模式";
/** 「自由标记」面板底部切换按钮:光标模式**已启用**时的文案(点击后关闭)。 */
inline const wchar_t* const kFreeMarkerCursorOffLabel = L"关闭光标模式";
/**
* @brief 边沿统计累积状态上限。
*
+114 -57
View File
@@ -5415,8 +5415,8 @@ namespace jdi
height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal + pad;
}
// 「自由标记」面板:每个可见信号一行名称,下面跟着它的全部固定点;
// 没有任何固定点时在名称行下方给一行灰色提示。
// 「自由标记」面板:逐信号一行名称(**只列出确实有固定点的信号**),下面跟着它的全部固定点;
// 没有任何固定点时给一行灰色提示,最后始终带一行底部切换按钮。
bool markerAny = false;
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
{
@@ -5430,12 +5430,9 @@ namespace jdi
{
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
float content = 0.0f;
std::vector<std::size_t> order;
if (!markerAny)
{
content += nameRow;
}
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
@@ -5443,11 +5440,22 @@ namespace jdi
{
continue;
}
content += nameRow;
// ⚠️ 用户明确要求:某个信号没有任何固定点时,它的名称条目**不出现**在面板上。
SortedFreeMarkerIndices(lane, ref, order);
if (order.empty())
{
continue;
}
content += nameRow;
content += static_cast<float>(order.size()) * itemRow;
}
height = titleHeight + pad + content + pad;
if (!markerAny)
{
// 还没有任何固定点:第一行留作提示文案(绘制时画「在网格内单击以固定数据点」)。
content += nameRow;
}
// 底部「启用光标模式 / 关闭光标模式」按钮:上下各留一个 pad(与左右内边距一致)。
height = titleHeight + pad + content + pad + markerButtonHeight + pad;
}
// 首次使用(位置为负)时自动放到绘图区**右上角**,并按网格在**当前可见网格中的序数**
@@ -5680,28 +5688,27 @@ namespace jdi
if (freeMarker)
{
// 「自由标记」面板:每个可见信号一行名称(右侧带「清除本信号全部固定点」✕),
// 下面逐行列出它的固定点(组内按时间升序 == 从左到右),行尾再带一个 ✕。
// 「自由标记」面板:**只列出确实有固定点的信号**,每个信号一行名称
//(右侧带「清除本信号全部固定点」✕),下面逐行列出它的固定点
//(组内按时间升序 == 从左到右),行尾再带一个 ✕。
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
const float closeSize = kFreeMarkerCloseSize * m_scale;
const float gap = 4.0f * m_scale;
// 时间两列右对齐:Δt 列贴面板右内边界(✕ 左侧),t 列在它左边。
layout.markerDeltaRight = left + width - pad - closeSize - gap;
layout.markerTimeRight = layout.markerDeltaRight - 74.0f * m_scale;
const float markerButtonHeight = kEdgePanelButtonHeight * m_scale;
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)从左往右**固定列宽**排布:
// 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。
float columnX = left + pad;
for (int column = 0; column < 4; ++column)
{
layout.markerColumns[column].left = columnX;
layout.markerColumns[column].width = kFreeMarkerColumnWidths[column] * m_scale;
columnX += layout.markerColumns[column].width + kFreeMarkerColumnGap * m_scale;
}
float rowY = top + titleHeight + pad;
std::vector<std::size_t> order;
bool markerAny = false;
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
{
markerAny = markerAny || item.ref.IsValid();
}
if (!markerAny)
{
// 还没有任何固定点:第一行留作提示文案(绘制时画「在网格内单击以固定数据点」)。
rowY += nameRow;
}
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
@@ -5709,16 +5716,19 @@ namespace jdi
{
continue;
}
layout.rowRefs.push_back(ref);
// 该信号没有任何固定点 ⇒ 名称条目与数据行都**不出现**(用户明确要求)。
SortedFreeMarkerIndices(lane, ref, order);
const bool hasMarkers = !order.empty();
if (order.empty())
{
continue;
}
layout.rowRefs.push_back(ref);
layout.rowNameRects.push_back(gd::RectF(left + pad, rowY,
std::max(0.0f, width - pad * 2.0f - closeSize - gap),
nameRow));
layout.rowNameCloses.push_back(
hasMarkers ? gd::RectF(left + width - pad - closeSize,
rowY + (nameRow - closeSize) * 0.5f, closeSize, closeSize)
: gd::RectF());
gd::RectF(left + width - pad - closeSize, rowY + (nameRow - closeSize) * 0.5f,
closeSize, closeSize));
rowY += nameRow;
for (const std::size_t itemIndex : order)
{
@@ -5731,6 +5741,10 @@ namespace jdi
rowY += itemRow;
}
}
// 底部切换按钮:整行宽度、与面板下边界留一个 pad。
layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight,
std::max(0.0f, width - pad * 2.0f), markerButtonHeight);
}
return layout;
}
@@ -6213,7 +6227,7 @@ namespace jdi
{
mixRect(rect);
}
// 「自由标记」:固定点行/名称 ✕ 的矩形、时间列边界与每个固定点的身份 + 时刻都要入键。
// 「自由标记」:固定点行/名称 ✕ 的矩形、四列边界与每个固定点的身份 + 时刻都要入键。
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
{
mixRect(row.rect);
@@ -6224,12 +6238,21 @@ namespace jdi
{
mixRect(rect);
}
mixFloat(layout.markerTimeRight);
mixFloat(layout.markerDeltaRight);
for (const gd::RectF& rect : layout.rowNameRects)
{
mixRect(rect);
}
for (const EdgePanelLayout::MarkerColumn& column : layout.markerColumns)
{
mixFloat(column.left);
mixFloat(column.width);
}
const Lane& lane = Tab().Lanes()[static_cast<std::size_t>(laneIndex)];
if (kind == EdgePanelKind::MARK)
{
// 底部切换按钮的文案取决于光标模式 ⇒ 必须进键,否则开关后仍贴回旧像素。
mix(lane.freeMarkerCursor ? 1ull : 0ull);
// 固定点的**内容**(哪个信号、什么时刻)直接决定面板文字与行数,必须进键;
// 否则新增/删除一个固定点后可能贴回旧像素。
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
@@ -6593,7 +6616,7 @@ namespace jdi
}
}
// ---- (2) 跟随鼠标的纵向光标(仅限本网格,上下各一个三角把手)----
// ---- (2) 跟随鼠标的纵向光标(**仅「光标模式」**;默认不画,只给贴近曲线的取值预览)----
const FreeMarkerProbe& probe = m_freeMarker;
if (probe.valid && probe.laneIndex >= 0 &&
probe.laneIndex < static_cast<int>(lanes.size()))
@@ -6605,7 +6628,8 @@ namespace jdi
float x = probe.x;
x = std::max(visual.rect.X + kFreeMarkerLineInset * m_scale,
std::min(x, visual.rect.X + visual.rect.Width - kFreeMarkerLineInset * m_scale));
if (clipBottom > clipTop)
// ⚠️ 用户明确要求:默认**不启用**网格私有光标 —— 只有「光标模式」才画这条线。
if (probe.cursorMode && clipBottom > clipTop)
{
UiDrawLine(g, x, clipTop, x, clipBottom, lineColor, 1.4f * m_scale);
// 上把手:尖端朝下(与常规光标的顶部把手一致);下把手:尖端朝上(用户明确要求)。
@@ -6719,11 +6743,16 @@ namespace jdi
}
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
probe.valid = true;
// 光标模式(默认关闭):决定"是否在网格内始终显示纵向光标"以及
// "鼠标离曲线多远还算有信号"。关闭时只有贴近曲线(或稀疏判定下贴近数据点)才有结果。
const bool cursorMode = lane.freeMarkerCursor;
probe.cursorMode = cursorMode;
probe.laneIndex = laneIndex;
probe.time = XToTime(static_cast<float>(mouseX));
probe.x = static_cast<float>(mouseX);
const double hoverTolerance = static_cast<double>(kFreeMarkerHoverTolerance * m_scale);
// 在网格内所有可见信号中找**离鼠标最近**的那个数据点。
double bestDistance = std::numeric_limits<double>::infinity();
double bestTime = probe.time;
@@ -6753,6 +6782,12 @@ namespace jdi
snapped = true;
}
}
// 非光标模式且该信号命中「自动点线」判据:**只有靠近数据点时**才给数值
//(吸附失败 = 附近没有采样点 ⇒ 直接跳过,避免在两点之间也冒出预览)。
if (!cursorMode && sparse && !snapped)
{
continue;
}
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
double sampled = 0.0;
if (!SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style, time,
@@ -6768,6 +6803,11 @@ namespace jdi
}
const float y = ValueToY(value, visual);
const double distance = std::fabs(static_cast<double>(y) - static_cast<double>(mouseY));
// 非光标模式:鼠标必须**在信号上方**(纵向贴近曲线)才算命中该信号。
if (!cursorMode && distance > hoverTolerance)
{
continue;
}
if (distance >= bestDistance)
{
continue;
@@ -6783,8 +6823,12 @@ namespace jdi
if (!probe.ref.IsValid())
{
return probe; // 网格内没有任何可用数据点:只画纵向线
// 网格内没有可标注的数据点:光标模式下仍要画纵向线(`valid` 为真),
// 非光标模式下则**什么都不显示**,鼠标按下即普通的平移/框选。
probe.valid = cursorMode;
return probe;
}
probe.valid = true;
probe.time = bestTime;
probe.x = TimeToX(bestTime);
probe.value = bestValue;
@@ -7389,28 +7433,37 @@ namespace jdi
const std::wstring timeText = FreeMarkerTimeText(item.time);
const std::wstring deltaText = FreeMarkerDeltaText(hasPrev, timeDelta);
const float timeRight = layout.markerTimeRight;
const float deltaRight = layout.markerDeltaRight;
const float valueWidth =
std::max(0.0f, timeRight - gap * 2.0f - (panel.X + pad));
UiText(g, UiEllipsize(valueText, m_fonts->monoSmall, valueWidth),
m_fonts->monoSmall,
gd::RectF(panel.X + pad, markerRow.rect.Y, valueWidth,
markerRow.rect.Height),
// 四列(① 取值 ② 增量 ③ 时刻 ④ 间隔)各列**左对齐在固定横坐标**上,
// 于是同一信息在所有条目上纵向上严格对齐(用户明确要求);
// 列宽固定,超长文本用 `UiEllipsize` 截断(布局期不做文本测量)。
const auto columnRect = [&](EdgePanelLayout::MarkerColumnIndex column) {
return gd::RectF(layout.markerColumns[column].left, markerRow.rect.Y,
layout.markerColumns[column].width,
markerRow.rect.Height);
};
UiText(g, UiEllipsize(valueText, m_fonts->monoSmall,
layout.markerColumns[EdgePanelLayout::MARKER_COL_VALUE].width),
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_VALUE),
m_theme->text, TextAlign::LEFT);
// 时间列各自右对齐 ⇒ 所有条目上下对齐(用户明确要求)。
const float timeWidth = UiMeasureTextWidth(m_fonts->monoSmall, timeText);
UiText(g, timeText, m_fonts->monoSmall,
gd::RectF(timeRight - timeWidth, markerRow.rect.Y, timeWidth,
markerRow.rect.Height),
if (hasPrev)
{
UiText(g,
UiEllipsize(FormatSignedValue(delta), m_fonts->monoSmall,
layout.markerColumns[EdgePanelLayout::MARKER_COL_DELTA].width),
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DELTA),
m_theme->textMuted, TextAlign::LEFT);
}
UiText(g,
UiEllipsize(timeText, m_fonts->monoSmall,
layout.markerColumns[EdgePanelLayout::MARKER_COL_TIME].width),
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_TIME),
m_theme->textMuted, TextAlign::LEFT);
if (!deltaText.empty())
{
const float deltaWidth = UiMeasureTextWidth(m_fonts->monoSmall, deltaText);
UiText(g, deltaText, m_fonts->monoSmall,
gd::RectF(deltaRight - deltaWidth, markerRow.rect.Y, deltaWidth,
markerRow.rect.Height),
UiText(g,
UiEllipsize(deltaText, m_fonts->monoSmall,
layout.markerColumns[EdgePanelLayout::MARKER_COL_DT].width),
m_fonts->monoSmall, columnRect(EdgePanelLayout::MARKER_COL_DT),
m_theme->textMuted, TextAlign::LEFT);
}
@@ -7466,8 +7519,9 @@ namespace jdi
rowY += rowHeight * 2.0f;
}
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮)。
if (hasButton)
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
// 「自由标记」面板的按钮是「启用光标模式 / 关闭光标模式」的切换按钮。
if (hasButton || freeMarker)
{
const gd::RectF& buttonRect = layout.button;
if (buttonRect.Width > 0.0f)
@@ -7481,8 +7535,11 @@ namespace jdi
UiFillRoundRect(g, inner, 4.0f * m_scale,
hot ? WithAlpha(m_theme->accent, 0x3A) : m_theme->cardBg);
UiDrawRoundRect(g, inner, 4.0f * m_scale, m_theme->accent, 1.2f);
UiText(g, kBuildSignalLabel, m_fonts->uiSmall, inner, m_theme->accent,
TextAlign::CENTER);
const std::wstring label =
freeMarker ? (tabLane.freeMarkerCursor ? std::wstring(kFreeMarkerCursorOffLabel)
: std::wstring(kFreeMarkerCursorOnLabel))
: std::wstring(kBuildSignalLabel);
UiText(g, label, m_fonts->uiSmall, inner, m_theme->accent, TextAlign::CENTER);
}
}