自由标记:面板表头/吸顶/行选中与悬停联动 + 原位显示 + 探测容差与阶跃吸附

- 面板:每个展开分组下方新增一行列表头(标记值/枚举文本/增量/时间点/时间间隔),
  与数据行共用列边界;列宽同时容纳列标题;表头行计入内容总高与面板高度;
  全部无枚举文本时仍整列缺省
- 吸顶:分组栏与表头各自按 max(自然, min(吸顶, 组下边界-自身高)) 钉住,
  组末尾按"表头先走 -> 条目 -> 分组栏最后离场";绘制顺序改为条目->表头->吸顶分组栏,
  被盖住的条目不绘制也不可命中
- 行交互:Lane::freeMarkerSelected(单击选中/再点取消)+ 独立的整行悬停状态
  (整行悬停不会把该行 ✕ 变成 danger 色);悬停时网格内对应固定点整体高亮
  (数据点圆环 + 胶囊强制不透明 + accent 描边,仅视界内)
- 固定点恢复原位显示(删除纵向避让,允许遮挡,回到初版方案)
- 探测判据重写:鼠标到画出来的折线的垂直距离 <= kSignalHitTolerance(与曲线高亮同源),
  命中后吸附到最近采样时刻 => 修复阶跃竖直跳变线无法探测/固定的问题;
  删除已无用的 kFreeMarkerHoverTolerance
- 修复滚动时条目文字串到面板标题栏(GDI 文本不认 GDI+ 裁剪,改用 UiTextClipped)
- 顺带清理过期注释(四列->五列、折起指示器已无横线、探测判据说明),
  RemoveFreeMarker 改为复用 RemoveFreeMarkerAt(选中下标校正只有一处)
- 验证:新增离屏 .preflight/mark4check 38/38(三组 A/B 均如实失败);
  回归 markcheck 61/61、fixcheck 37/37、round3check 31/31、round4check 34/34、
  round5check 5/5、editcheck 20/20、convcheck 18/18;make release/debug 零警告
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-22 21:04:03 +08:00
1 parent 8f735d51be
commit 79eebc281e
10 files changed
+618 -116

No files matched your search

+15 -4
View File
@@ -930,16 +930,19 @@ namespace jdi
/**
* @brief 「实验性功能 → 自由标记」开关(功能总开关)。
*
* 开启后本网格参与自由标记:鼠标**贴近信号曲线**时标注离鼠标最近的数据点,
* 左键单击把该数据点固定下来(固定点带 ✕ 可单独移除,并在浮动面板按信号分组列出
* 「值 / 增量 / 时刻 / 间隔」四列)。默认**不**显示网格私有纵向光标。
* 开启后本网格参与自由标记:鼠标**贴近信号曲线**(判据 = 到**画出来的折线**的垂直距离 ≤
* `kSignalHitTolerance`,与常规触发曲线高亮同源)时标注离鼠标最近的数据点,
* 左键单击把该数据点固定下来(固定点带 ✕ 可单独移除;浮动面板按信号分组,
* 每组一行**表头** + 五列数据:标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔)。
* 默认**不**显示网格私有纵向光标。
*/
bool freeMarker = false;
/**
* @brief 「自由标记」的光标模式(浮动面板最下方的切换按钮)。
*
* `false`(默认):鼠标在信号曲线上(或稀疏判定下靠近数据点)时才显示取值预览;
* `false`(默认):鼠标在信号曲线上(判据同 `PlotView::ProbeFreeMarker`:
* 到画出来的折线的垂直距离 ≤ `kSignalHitTolerance`)时才显示取值预览;
* 鼠标下方没有曲线时按下就是普通的平移/框选,不会误固定数据点。
* `true`:与最初实现的语义相同 —— 网格内始终跟随鼠标画一条**仅限本网格**的纵向光标
*(上下各一个三角把手、不显示时间读数),光标穿过信号时标注离鼠标最近的数据点。
@@ -1014,6 +1017,14 @@ namespace jdi
*/
std::vector<SignalRef> freeMarkerCollapsedGroups;
/**
* @brief 「自由标记」面板里被**选中**的条目(`freeMarkerItems` 下标,-1 = 未选中)。
*
* 单击面板里的条目的数据区域即选中(该行高亮);它与网格内的固定点一一对应,
* 因此增删固定点后需要重新校正(见 `RemoveFreeMarker*`)。
*/
int freeMarkerSelected = -1;
/**
* @brief 「自由标记」面板五列的**实测内容宽度**(设备像素,0 = 还没量过)。
*
+29 -1
View File
@@ -1457,7 +1457,7 @@ namespace jdi
const int index = FindFreeMarker(ref, time);
if (index >= 0)
{
freeMarkerItems.erase(freeMarkerItems.begin() + index);
RemoveFreeMarkerAt(static_cast<std::size_t>(index));
}
}
@@ -1474,14 +1474,42 @@ namespace jdi
freeMarkerItems[current] = freeMarkerItems[current + 1];
}
freeMarkerItems.pop_back();
// 选中下标跟随:删的就是它 ⇒ 取消选中;删在它前面 ⇒ 下标前移。
if (freeMarkerSelected == static_cast<int>(index))
{
freeMarkerSelected = -1;
}
else if (freeMarkerSelected > static_cast<int>(index))
{
--freeMarkerSelected;
}
if (freeMarkerSelected >= static_cast<int>(freeMarkerItems.size()))
{
freeMarkerSelected = -1;
}
}
void Lane::RemoveFreeMarkersOf(const SignalRef& ref)
{
// 选中项的引用先取下来:过滤会打乱下标,事后重新定位才不会错位到别的信号。
SignalRef selectedRef;
double selectedTime = 0.0;
const bool hadSelection = (freeMarkerSelected >= 0 &&
static_cast<std::size_t>(freeMarkerSelected) < freeMarkerItems.size());
if (hadSelection)
{
selectedRef = freeMarkerItems[static_cast<std::size_t>(freeMarkerSelected)].ref;
selectedTime = freeMarkerItems[static_cast<std::size_t>(freeMarkerSelected)].time;
}
freeMarkerItems.erase(
std::remove_if(freeMarkerItems.begin(), freeMarkerItems.end(),
[&ref](const FreeMarkerItem& item) { return item.ref == ref; }),
freeMarkerItems.end());
freeMarkerSelected = -1;
if (hadSelection && !(selectedRef == ref))
{
freeMarkerSelected = FindFreeMarker(selectedRef, selectedTime);
}
}
int FindNearestSample(const TimeAxis& axis, double time)
+3
View File
@@ -136,6 +136,7 @@ namespace jdi
LanePanelState freeMarkerPanel;
float freeMarkerOpacity = 1.0f;
float freeMarkerScroll = 0.0f;
int freeMarkerSelected = -1;
std::vector<SignalRef> freeMarkerCollapsedGroups;
};
@@ -180,6 +181,7 @@ namespace jdi
carry.freeMarkerPanel = lane.freeMarkerPanel;
carry.freeMarkerOpacity = lane.freeMarkerOpacity;
carry.freeMarkerScroll = lane.freeMarkerScroll;
carry.freeMarkerSelected = lane.freeMarkerSelected;
carry.freeMarkerCollapsedGroups = lane.freeMarkerCollapsedGroups;
for (std::size_t signalIndex = 0; signalIndex < lane.signals.size(); ++signalIndex)
{
@@ -245,6 +247,7 @@ namespace jdi
lane.freeMarkerPanel.y = carry.freeMarkerPanel.y;
lane.freeMarkerOpacity = carry.freeMarkerOpacity;
lane.freeMarkerScroll = carry.freeMarkerScroll;
lane.freeMarkerSelected = carry.freeMarkerSelected;
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
};
+9
View File
@@ -473,6 +473,15 @@ namespace jdi
{
if (item.ref == ref)
{
// 选中下标按“第几个条目”跟随(过滤会改变下标)。
if (source.freeMarkerSelected >= 0 &&
static_cast<std::size_t>(source.freeMarkerSelected) <
source.freeMarkerItems.size() &&
&source.freeMarkerItems[static_cast<std::size_t>(source.freeMarkerSelected)] ==
&item)
{
lane.freeMarkerSelected = static_cast<int>(lane.freeMarkerItems.size());
}
lane.freeMarkerItems.push_back(item);
}
}