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

需求(用户):网格右键菜单「实验性功能」里新增带 √ 的「自由标记」。开启后网格内跟随鼠标
出现一条仅限本网格的纵向光标(上下各一个把手、不显示时间值),穿过信号时只标注离鼠标
最近的数据点;左键单击把该点固定(带 ✕ 可移除,鼠标移开仍显示、超出视界直接不显示),
后续固定点显示为相对前一个的增量;面板按信号分组列出「值(±增量) = 枚举」+「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

+207 -4
View File
@@ -70,7 +70,7 @@ namespace jdi
};
/**
* @brief 绘图区内的内联数值输入目标。
* @brief 绘图区内联数值输入的目标。
*/
enum class PlotEditTarget : int
{
@@ -84,7 +84,9 @@ namespace jdi
EDGE_CENTER_ITERATIONS, /**< 编辑「中心轴边沿计数」的迭代次数 N。 */
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
MEAN_FILTER_ITERATIONS, /**< 编辑「均值滤波」的迭代次数 N。 */
PEAK_ITERATIONS_MAX /**< 编辑「弱峰调整」某信号「迭代次数 N」滑块的**上限**(正整数)。 */
PEAK_ITERATIONS_MAX, /**< 编辑「弱峰调整」某信号「迭代次数 N」滑块的**上限**(正整数)。 */
FREE_MARKER_TIME, /**< 编辑「自由标记」某固定点的**自身时间**(秒)。 */
FREE_MARKER_DELTA /**< 编辑「自由标记」某固定点相对前一固定点的**时间间隔**(秒)。 */
};
/**
@@ -102,7 +104,8 @@ namespace jdi
PLOT_CMD_LANE_EDGE_CENTER = 8, /**< 切换本网格的「实验性功能 → 边沿计数(中心轴)」开关。 */
PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
PLOT_CMD_LANE_WEAK_PEAK = 11 /**< 切换本网格的「实验性功能 → 弱峰调整」开关。 */
PLOT_CMD_LANE_WEAK_PEAK = 11, /**< 切换本网格的「实验性功能 → 弱峰调整」开关。 */
PLOT_CMD_LANE_FREE_MARKER = 12 /**< 切换本网格的「实验性功能 → 自由标记」开关。 */
};
/**
@@ -113,7 +116,8 @@ namespace jdi
COUNT = 0, /**< 边沿计数。 */
CENTER = 1, /**< 边沿计数(中心轴)。 */
MEAN = 2, /**< 均值滤波。 */
PEAK = 3 /**< 弱峰调整。 */
PEAK = 3, /**< 弱峰调整。 */
MARK = 4 /**< 自由标记。 */
};
/**
@@ -909,6 +913,34 @@ namespace jdi
*/
bool LaneWeakPeakSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 切换某网格的「自由标记」开关。
*
* 开启后该网格内跟随鼠标显示一条仅限本网格的纵向光标(上下各一个三角把手、
* 不显示时间读数),光标穿过的信号中**离鼠标最近**的那个会被标注取值;
* 左键原地单击即把该数据点固定下来。关闭时**保留**已固定的点(用户可再次开启
* 继续查看),需要清空请用面板上的 ✕。
*
* @param [IN] laneIndex 网格下标。
* @param [IN] enabled 是否开启。
* @return 状态发生变化返回 true。
*/
bool SetLaneFreeMarker(int laneIndex, bool enabled);
/**
* @brief 查询某网格的「自由标记」开关。
* @param [IN] laneIndex 网格下标。
* @return 已开启返回 true。
*/
bool LaneFreeMarker(int laneIndex) const;
/**
* @brief 切换某网格的「自由标记」开关。
* @param [IN] laneIndex 网格下标。
* @return 切换后的状态。
*/
bool ToggleLaneFreeMarker(int laneIndex);
/**
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
* @param [IN] laneIndex 网格下标。
@@ -1579,6 +1611,40 @@ namespace jdi
std::vector<gd::RectF> rowLevelResets;
std::vector<gd::RectF> rowIterResets;
std::vector<SignalRef> rowRefs;
/**
* @brief 「自由标记」面板的一行(一个固定点)。
*
* `itemIndex` 是 `Lane::freeMarkerItems` 的下标(绘制文本、增删都通过它定位,
* 不依赖面板里的显示顺序);`rect` 是整行矩形(悬停/双击编辑时间用),
* `close` 是该行末尾的 ✕ 按钮。
*/
struct MarkerRow
{
std::size_t itemIndex = 0;
gd::RectF rect;
gd::RectF close;
};
/** 「自由标记」:每个固定点一行(按信号分组、组内按时间升序展开后的顺序)。 */
std::vector<MarkerRow> markerRows;
/**
* @brief 「自由标记」:每个信号名右侧的「清除该信号全部固定点」✕。
*
* 与 `rowRefs` 一一对应;该信号没有任何固定点时为空矩形。
*/
std::vector<gd::RectF> rowNameCloses;
/**
* @brief 「自由标记」:每个信号**名称行**的矩形(与 `rowRefs` 一一对应)。
*
* 绘制只读这个矩形(与 `rowNameCloses` 同一套横坐标),人为主观反推容易与布局漂移。
*/
std::vector<gd::RectF> rowNameRects;
/**
* @brief 「自由标记」时间列的两条右边界(`t:` 列与 `Δt:` 列)。
*
* 两列各自右对齐 ⇒ 所有条目上下对齐;首项没有 `Δt` 时该列留白。
*/
float markerTimeRight = 0.0f;
float markerDeltaRight = 0.0f;
};
/**
@@ -1589,6 +1655,108 @@ namespace jdi
*/
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
/**
* @brief 「自由标记」跟随光标的解析结果(每次鼠标移动/每帧重算,绘制与命中共用)。
*
* 它描述「鼠标此刻所在的、开启了自由标记的网格」里应当标注的那个数据点:
* 该网格内所有可见信号中**离鼠标最近**的那一个。
*/
struct FreeMarkerProbe
{
bool valid = false; /**< 鼠标是否落在某个开启了自由标记的网格内。 */
int laneIndex = -1; /**< 该网格下标。 */
double time = 0.0; /**< 标注时刻(显示时刻;吸附时 = 采样时刻)。 */
float x = 0.0f; /**< 该时刻的屏幕横坐标。 */
SignalRef ref; /**< 就近信号(网格内没有可用信号时无效)。 */
double value = 0.0; /**< 已叠加「调整」变换的取值(用于标注与增量)。 */
double enumValue = 0.0; /**< 采样原值(用于查枚举表)。 */
float y = 0.0f; /**< 数据点的屏幕纵坐标。 */
bool snapped = false; /**< 是否吸附到了采样点(需要给出吸附提示)。 */
};
/**
* @brief 解析「自由标记」跟随光标应当标注的数据点。
* @param [IN] mouseX 鼠标客户区横坐标。
* @param [IN] mouseY 鼠标客户区纵坐标。
* @return 解析结果;鼠标不在任何开启该功能的网格内时 `valid == false`。
*/
FreeMarkerProbe ProbeFreeMarker(int mouseX, int mouseY) const;
/**
* @brief 绘制「自由标记」:跟随光标 + 全部已固定点(含各自的 ✕)。
*
* 同时重建 `m_freeMarkerHits`(固定点 ✕ 的命中矩形),供鼠标悬停与点击使用。
*/
void PaintFreeMarker(gd::Graphics& g, const std::vector<LaneVisual>& lanes);
/**
* @brief 在鼠标附近画一个小提示气泡(与面板 GX 徽标的提示同一套样式)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] text 提示文本(空串则不绘制)。
*/
void PaintFreeMarkerTooltip(gd::Graphics& g, const std::wstring& text);
/** @brief 一个固定点 ✕ 的命中矩形(绘制时重建,命中测试时读取)。 */
struct FreeMarkerHit
{
int laneIndex = -1;
std::size_t itemIndex = 0;
gd::RectF close;
};
/**
* @brief 计算某个网格里「第 index 个固定点」的标注几何(绘制与命中共源)。
*
* @param [IN] laneIndex 网格下标。
* @param [IN] itemIndex `Lane::freeMarkerItems` 下标。
* @param [OUT] label 取值胶囊矩形(**不含** ✕)。
* @param [OUT] close ✕ 按钮矩形。
* @param [OUT] text 取值文本(`值(±增量) = 枚举`,绘制侧直接复用,避免重复求解)。
* @return 该固定点可显示(在视界内、取值有效)返回 true。
*/
bool FreeMarkerLabelRects(int laneIndex, std::size_t itemIndex, gd::RectF& label,
gd::RectF& close, std::wstring& text) const;
/**
* @brief 画一个「取值胶囊」(信号色底 + 高对比度文字 + 可选 ✕ 按钮)。
*
* 网格内的固定点与跟随鼠标的预览点共用这一段(两者外观规则完全相同)。
*
* @param [IN,OUT] g 绘图上下文。
* @param [IN] label 胶囊矩形。
* @param [IN] text 文本。
* @param [IN] color 信号色。
* @param [IN] close ✕ 按钮矩形(宽度为 0 表示不画)。
* @param [IN] closeHot ✕ 是否处于悬停态。
*/
void PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label, const std::wstring& text,
std::uint32_t color, const gd::RectF& close, bool closeHot);
/**
* @brief 计算某个固定点相对**同信号**前一个固定点的增量与其时间间隔。
*
* @param [IN] lane 目标网格。
* @param [IN] itemIndex `Lane::freeMarkerItems` 下标。
* @param [IN] value 当前点的取值(已叠加「调整」变换)。
* @param [OUT] hasPrev 是否存在前一个同信号固定点。
* @param [OUT] delta 取值增量(当前 − 前一个)。
* @param [OUT] timeDelta 时间间隔(当前 − 前一个,恒为正)。
* @return 当前点取值有效返回 true。
*/
bool FreeMarkerDelta(const Lane& lane, std::size_t itemIndex, double value, bool& hasPrev,
double& delta, double& timeDelta) const;
/**
* @brief 计算某个固定点的取值(已叠加「调整」变换)。
* @param [IN] lane 目标网格。
* @param [IN] item 固定点。
* @param [OUT] value 输出取值(已含界面变换)。
* @param [OUT] enumValue 输出采样原值(用于查枚举表)。
* @return 取到有效值返回 true。
*/
bool FreeMarkerValue(const Lane& lane, const Lane::FreeMarkerItem& item, double& value,
double& enumValue) const;
/**
* @brief 在面板布局里找出某个信号对应的「逐条目」行下标。
* @param [IN] layout 面板布局。
@@ -2325,6 +2493,41 @@ namespace jdi
int m_hoverPanelBadgeKind = -1;
/** 「弱峰调整」每行滑块右侧的复位控件是否处于悬停态(用于“进入/离开”时只提示一次)。 */
bool m_peakResetHover = false;
// ---------------------------------------------------------------- 自由标记
/** 最近一次解析出的「自由标记」跟随数据点(绘制与命中共用)。 */
FreeMarkerProbe m_freeMarker;
/**
* @brief 上一次绘制时登记的固定点 ✕ 命中矩形。
*
* 固定点 ✕ 的位置由「取值文本宽度」决定(文本又取决于数值),因此无法在命中测试里
* 重新推算 —— 改为绘制时登记、命中时读取(与 `m_legendHits` 同一套路)。
*/
mutable std::vector<FreeMarkerHit> m_freeMarkerHits;
/** 网格内固定点 ✕ 的悬停态:`m_freeMarkerHoverItem` 为 `SIZE_MAX` 表示没有悬停。 */
int m_freeMarkerHoverLane = -1;
std::size_t m_freeMarkerHoverItem = static_cast<std::size_t>(-1);
/** 面板内 ✕ 的悬停态:信号名 ✕(`itemIndex == SIZE_MAX`)或某一固定点行 ✕。 */
int m_freeMarkerPanelHoverLane = -1;
std::size_t m_freeMarkerPanelHoverItem = static_cast<std::size_t>(-1);
SignalRef m_freeMarkerPanelHoverSignal;
/**
* @brief 本次按下是否**可能**固定一个数据点(抬起时若"原地未拖动"就真正固定)。
*
* 自由标记网格 + 选区放大模式共存时,单击 = 固定数据点、按下后拖动 = 框选,
* 因此按下时不能立即固定,必须等抬起时判断是否发生过位移。
*/
bool m_freeMarkerPendingFix = false;
int m_freeMarkerPendingLane = -1;
/** 正在编辑的固定点(`m_editLane` 网格内的 `freeMarkerItems` 下标)。 */
std::size_t m_editMarkerIndex = static_cast<std::size_t>(-1);
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */