新增实验性功能「自由标记」:网格内跟随鼠标的纵向光标 + 单击固定数据点 + 浮动面板

需求(用户):网格右键菜单「实验性功能」里新增带 √ 的「自由标记」。开启后网格内跟随鼠标
出现一条仅限本网格的纵向光标(上下各一个把手、不显示时间值),穿过信号时只标注离鼠标
最近的数据点;左键单击把该点固定(带 ✕ 可移除,鼠标移开仍显示、超出视界直接不显示),
后续固定点显示为相对前一个的增量;面板按信号分组列出「值(±增量) = 枚举」+「t:… Δt:…」
(两列右对齐、可双击按秒编辑并自动换位);命中自动点线判据的信号要有吸附提示;
两级 ✕ 都要悬停提示;与选区放大共存时原地单击 = 固定、按下拖动 = 框选。

一、数据层
- Lane 新增 freeMarker 开关、freeMarkerItems(FreeMarkerItem{ref, time})、freeMarkerPanel。
  time 是「显示时刻」(画在 TimeToX(time) 处),与光标同一口径;取值必须走
  SampleDisplayedSignalAt(内部处理时间偏移与水平镜像)。
- 新增 AddFreeMarker / RemoveFreeMarker / RemoveFreeMarkerAt / RemoveFreeMarkersOf /
  FindFreeMarker / HasFreeMarkers。同一信号同一时刻不重复加入(相对容差 1e-12);
  freeMarkerItems 保持插入顺序,显示顺序与「前一个固定点」一律由 SortedFreeMarkerIndices
  按时间排序得出(删除某项不打乱其它项下标)。
- LaneCarry 同步携带,勾选/取消勾选信号导致网格重建时不会丢掉已固定的点。

二、网格内交互
- ProbeFreeMarker:仅当鼠标所在网格开启该功能时有效;在网格内全部可见信号里挑屏幕纵向
  距离鼠标最近的数据点。MinorCellSparse 成立时经 SnapTimeToSample 吸附到真实采样时刻,
  绘制侧画一圈圆环作为吸附提示。吸附只看判据,不看当前绘制方式。
- PaintFreeMarker(接在 paintOverlays 里、浮动面板之前):纵向线只在本网格内,上把手尖端
  朝下(与常规光标顶部把手一致)、下把手尖端朝上,不画时间读数;已固定点画低透明度细线 +
  圆点 + 「值(±增量) = 枚举」胶囊 + 行尾 ✕(悬停高亮 + 提示气泡);
  超出视界直接不显示(不复用常规数据点的贴边定格 + 方向三角)。
- 网格内固定点 ✕ 的命中矩形由绘制时登记(m_freeMarkerHits),因为它的位置取决于取值文本宽度。
- OnMouseLeave 清跟随光标与三处悬停态(已固定的点保留)。

三、单击 vs 框选
- OnLButtonDown 在开启该功能的网格上只登记 m_freeMarkerPendingFix,并把拖拽模式设为
  「选区放大 ? RUBBER_SELECT : PAN」;OnLButtonUp 里只有「整段操作没有拖动且选区轴向未定」
  才 AddFreeMarker,否则照旧框选/平移。

四、浮动面板(EdgePanelKind::MARK / PLOT_CMD_LANE_FREE_MARKER = 12)
- 每个可见信号一行名称(右侧带清除该信号全部固定点的 ✕),名称行下方按时间升序列出该信号的
  固定点;没有任何固定点时第一行给灰色提示。
- 行 = 左侧「值(±增量) = 枚举文本」(无尖头括号、首项无增量)+ 右侧两列各自右对齐的
  「t:<时刻>s」与「Δt:+<复合单位>」(自动单位匹配)+ 行尾 ✕。
  两列右边界存进 EdgePanelLayout(markerTimeRight / markerDeltaRight),保证所有条目纵向对齐。
- 双击行:左半编辑自身时刻(FREE_MARKER_TIME),右半编辑相对前一固定点的间隔
  (FREE_MARKER_DELTA,最左边的点没有前一项所以不进入编辑)。提交后只改 time,
  顺序由排序函数自动重排(越过后续点即交换条目位置)。
- PanelSlotKey 把每个固定点的信号 + 时刻、全部 marker 行矩形与两列边界一起 mix。

五、一并修掉的两个自伤
1. EdgePanelWidth 的 MARK 分支原本逐行取采样值算文本宽度 —— 它被 EdgePanelLayoutFor 调用,
   而后者在 TopPanelAtPoint / SyncPanelOrder 里每次鼠标移动调用数次,等于「鼠标一动就做几十次
   采样查询」。改为固定宽度 kFreeMarkerPanelWidth(340 逻辑像素)+ 绘制侧 UiEllipsize。
2. PaintFreeMarker 与跟随光标的胶囊绘制原本是两份重复代码,抽 PaintFreeMarkerCapsule 共用;
   FreeMarkerLabelRects 增加 text 出参,避免绘制侧重复求解一次取值。

验证(全离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/markcheck 31/31:探测有效与就近选择、原地单击恰好新增 1 个固定点、
  选区放大共存(单击固定不缩放 / 拖动缩放不固定)、面板行数与时间升序、首项无增量、
  后续项增量与 Δt 正确、两列右对齐、行尾 ✕ 只删 1 项、信号名 ✕ 清空该信号、
  编辑 Δt 后移动并重排、深放大时吸附且时刻精确等于采样时刻、视界外固定点不绘制。
- 回归:editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、
  convcheck 18/18;check 136/137(唯一失败项是本轮之前就存在的不稳定项,上一轮已用
  git stash 在基线复现同一数值);真实 MF4 仍 283/8/18/24 个信号。
- make release / make debug 零警告。
- TargetCheck.exe 833,536 字节(814.0 KiB)—— 比上一轮 +24 KB,首次超出旧的 800 KiB
  红线约 14 KB。已做的瘦身见上;剩余为新功能的真实代码量,若要回到 800 KiB 以内需要
  改变取舍(动态 CRT / UPX / 砍其它功能),本轮按「实现功能优先」未动。

文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 181-188 条硬约束
与文末本轮小节。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-20 19:47:55 +08:00
1 parent 53b981a2a9
commit dd8f63aa0e
10 files changed
+1735 -16

No files matched your search

+124 -1
View File
@@ -328,6 +328,128 @@ namespace jdi
inline const float kWeakPeakResetButtonSize = 16.0f;
/** 复位控件与其左侧数值列之间的间距(逻辑像素)。 */
inline const float kWeakPeakResetGap = 4.0f;
// ---------------------------------------------------------------- 自由标记
/** 「自由标记」面板里信号名行的高度(比普通条目略高:右侧多一个 ✕)。 */
inline const float kFreeMarkerNameRowHeight = 22.0f;
/** 「自由标记」面板里每个固定点行的高度。 */
inline const float kFreeMarkerItemRowHeight = 18.0f;
/** 「自由标记」面板与网格内固定点 ✕ 按钮的边长。 */
inline const float kFreeMarkerCloseSize = 13.0f;
/**
* @brief 「自由标记」面板的宽度(逻辑像素)。
*
* 取固定值而**不做逐行文本测量**:`EdgePanelWidth` 会被 `EdgePanelLayoutFor` 调用,
* 而后者在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用(每次鼠标移动数次);
* 逐行取采样值算宽度会变成"鼠标一动就做几十次采样查询"。
*/
inline const float kFreeMarkerPanelWidth = 340.0f;
/** 固定点取值胶囊与 ✕ 之间的间距。 */
inline const float kFreeMarkerCloseGap = 3.0f;
/** 跟随光标纵向线与网格左右边界之间的留白(避免线与边框重叠)。 */
inline const float kFreeMarkerLineInset = 1.0f;
/**
* @brief 把数值格式化为**始终带符号**的文本(`+1.5` / `-0.25` / `+0`)。
*
* 「自由标记」的取值增量与时间增量都要求显式正负号(即使是 0 也带 `+`),
* 与双光标模式的增量显示规则一致。
*/
inline std::wstring FormatSignedValue(double value)
{
std::wstring text = (value < 0.0) ? L"-" : L"+";
text += FormatValue(std::fabs(value));
return text;
}
/**
* @brief 「自由标记」面板/胶囊里的取值部分:`值(+增量) = 枚举文本`。
*
* @param value 当前取值(已叠加「调整」变换)。
* @param hasPrev 是否存在前一个同信号固定点(存在才显示增量)。
* @param delta 相对前一个固定点的取值增量。
* @param enumText 枚举文本(可为空指针)。
*/
inline std::wstring FreeMarkerValueText(double value, bool hasPrev, double delta,
const std::wstring* enumText)
{
std::wstring text = FormatValue(value);
if (hasPrev)
{
text += L"(";
text += FormatSignedValue(delta);
text += L")";
}
if (enumText != nullptr && !enumText->empty())
{
text += L" = ";
text += *enumText;
}
return text;
}
/**
* @brief 「自由标记」的时间列文本:`t:<时刻>`。
*
* 数值格式与光标时间读数**完全相同**(`FormatValue` + `s` 后缀,
* 即 `FormatCursorTime` 的口径);这里不调用 `FormatCursorTime` 是因为它在本头里
* 定义在本段**之后**,直接展开可以避免前向声明。
*/
inline std::wstring FreeMarkerTimeText(double time)
{
return L"t:" + FormatValue(time) + L"s";
}
/**
* @brief 「自由标记」的时间间隔列文本:`Δt:<间隔>`。
*
* 平时用复合单位(自动匹配 ms / s / min,与双光标差值读数一致),
* 双击后按**秒**编辑。首个固定点没有前一项 ⇒ 返回空串(该列留白)。
*/
inline std::wstring FreeMarkerDeltaText(bool hasPrev, double timeDelta)
{
if (!hasPrev)
{
return std::wstring();
}
// 时间间隔恒为正(按时间排序后当前点总在后方);显示时显式带 `+`。
return L"Δt:+" + FormatDurationCompound(std::fabs(timeDelta));
}
/**
* @brief 收集某信号的全部固定点下标,按**时间升序**输出。
*
* 面板里同一信号下的条目按「从左到右」从上到下排列,「前一个固定点」也按这个顺序
* 计算,因此绘制、命中、增量计算必须共用本函数(否则三者会各自排一次序而错位)。
*
* @param [IN] lane 目标网格。
* @param [IN] ref 目标信号。
* @param [OUT] out 追加写入的下标(`Lane::freeMarkerItems` 的下标)。
*/
inline void SortedFreeMarkerIndices(const Lane& lane, const SignalRef& ref,
std::vector<std::size_t>& out)
{
out.clear();
for (std::size_t index = 0; index < lane.freeMarkerItems.size(); ++index)
{
if (lane.freeMarkerItems[index].ref == ref)
{
out.push_back(index);
}
}
std::stable_sort(out.begin(), out.end(),
[&lane](std::size_t a, std::size_t b)
{
return lane.freeMarkerItems[a].time <
lane.freeMarkerItems[b].time;
});
}
inline const int kWeakPeakIterationsMin = 1; /**< 迭代次数 N 的下限。 */
/** 迭代次数滑块「自定义上限」的允许范围(与算法内部的阶数夹取保持一致)。 */
inline const int kWeakPeakIterationsMaxLimit = 1000;
@@ -727,7 +849,8 @@ namespace jdi
* 顺序即绘制层序:先绘制的在下方,最后绘制的在最上方。
*/
inline const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK};
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK};
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));