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

用户需求(逐条落地):
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

+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;
}