From affb9cc34007ab499ef22e531403e0abc3fcb55e Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Fri, 23 Oct 2026 15:50:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=AE=E5=BD=95=E8=A7=84=E8=8C=83=E5=8C=96?= =?UTF-8?q?=EF=BC=88Inc/Src=EF=BC=89+=E3=80=8C=E8=87=AA=E7=94=B1=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E3=80=8D=E7=AC=AC=E5=85=AD=E8=BD=AE=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) 目录规范化:所有 .hpp 只放 Inc,Src 只放 .cpp - 4 个 *Internal.hpp 从 Src 迁到 Inc(DataModel/Application/PlotView/SignalPanel) - Makefile 固化该约定(注释必须单独成行:写在同一行会把 CXX_INCLUDES_PATHS 整条注释掉) - include 全是扁平形式,搬家无需改任何 include 2) 常规模式 = 普通鼠标(未开启「自由标记点选取」) - ProbeFreeMarker 早退:不预览、不画纵向光标、单击不固定 - 补 if (!hitCurve) continue:否则整列光标下 probe.ref 恒有效,任意位置都能固定 - 固定点仍恒吸附到真实采样时刻;「已吸附」圆环只在稀疏判据成立时画 3) 两个显示开关:按钮改名「自由标记点选取」,其下新增「显示增量 | 显示枚举文本」 - 默认关闭,只影响已固定点的胶囊文本;预览胶囊恒为「值 = 枚举」 - 开关行计入面板高度与内容区高度 4) 面板六列:新增恒存在的「序号」列(组内 1 起) - 枚举列存在时,无枚举/对不上任何枚举项显示 N/A - 列与列之间画竖线;悬停某行时其它固定点封顶 50% 不透明度,移出恢复 - 删除固定点 ✕ 的悬停浮动提示(PaintFreeMarkerTooltip 实现与声明全部移除) - 开启选取模式的网格上,信号悬停提示只显示信号名 5) 缓存键补齐 - FreeMarkerContents 混入枚举表指纹(旧键会让加上枚举表后仍显示 N/A) - PanelSlotKey 混入两个新按钮矩形与开关状态 6) 注释/文档 - 修正 FreeMarkerProbe、SetLaneFreeMarkerCursor、标记透明度等处的过期说明 - 空态提示按是否开启选取模式区分文案 验证(全离屏,未占用鼠标/键盘/屏幕) - 新增 .preflight/mark5check 52/52 - 回归:markcheck 67/67、mark4check 40/40、fixcheck 37/37、round3check 31/31、 round4check 34/34、round5check 5/5、editcheck 20/20、convcheck 18/18、diag 19/19 - check 136/137(唯一失败项为既有不稳定项,drift=15818 与改动前逐位相同) - make release / make debug 零警告;Release 产物 TargetCheck.exe 857,600 字节 --- .../{Src => Inc}/ApplicationInternal.hpp | 0 Components/PlotView/Inc/PlotView.hpp | 114 ++++-- .../{Src => Inc}/PlotViewInternal.hpp | 61 ++-- Components/PlotView/Src/PlotViewFeatures.cpp | 48 +++ .../PlotView/Src/PlotViewInteraction.cpp | 37 +- Components/PlotView/Src/PlotViewPaint.cpp | 324 ++++++++++++------ .../{Src => Inc}/SignalPanelInternal.hpp | 0 Documents/agent_mem.md | 112 ++++++ Makefile | 4 + Utilities/Inc/DataModel.hpp | 40 ++- Utilities/{Src => Inc}/DataModelInternal.hpp | 6 + Utilities/Src/DataModelTab.cpp | 2 + 12 files changed, 561 insertions(+), 187 deletions(-) rename Components/Application/{Src => Inc}/ApplicationInternal.hpp (100%) rename Components/PlotView/{Src => Inc}/PlotViewInternal.hpp (95%) rename Components/SignalPanel/{Src => Inc}/SignalPanelInternal.hpp (100%) rename Utilities/{Src => Inc}/DataModelInternal.hpp (98%) diff --git a/Components/Application/Src/ApplicationInternal.hpp b/Components/Application/Inc/ApplicationInternal.hpp similarity index 100% rename from Components/Application/Src/ApplicationInternal.hpp rename to Components/Application/Inc/ApplicationInternal.hpp diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 8367e9a..60f66b4 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -945,32 +945,60 @@ namespace jdi bool ToggleLaneFreeMarker(int laneIndex); /** - * @brief 设置某网格「自由标记」的**光标模式**(浮动面板最下方的切换按钮)。 + * @brief 设置某网格是否处于「自由标记点选取」(浮动面板底部的切换按钮)。 * - * 关闭(默认)时鼠标必须贴近信号曲线(稀疏判定下需靠近数据点)才显示取值预览, - * 鼠标下方没有曲线时按下即普通的平移/框选;开启时与最初实现一致 —— - * 网格内始终跟随鼠标画一条纵向光标。 + * 关闭(默认)= **普通鼠标**:不预览数据点、不画纵向光标、单击不固定任何点, + * 按下/拖动/单击完全是常规语义(平移 / 框选 / 拖信号)。 + * 开启时网格内始终跟随鼠标画一条纵向光标,并在贴近曲线时预览/固定数据点。 * * @param [IN] laneIndex 网格下标。 - * @param [IN] enabled 是否启用光标模式。 + * @param [IN] enabled 是否启用「自由标记点选取」。 * @return 状态发生变化返回 true。 */ bool SetLaneFreeMarkerCursor(int laneIndex, bool enabled); /** - * @brief 查询某网格是否处于「自由标记」的光标模式。 + * @brief 查询某网格是否开启了「自由标记点选取」。 * @param [IN] laneIndex 网格下标。 * @return 启用返回 true(该网格未开启自由标记时恒为 false)。 */ bool LaneFreeMarkerCursor(int laneIndex) const; /** - * @brief 切换某网格「自由标记」的光标模式。 + * @brief 切换某网格的「自由标记点选取」。 * @param [IN] laneIndex 网格下标。 * @return 切换后的状态。 */ bool ToggleLaneFreeMarkerCursor(int laneIndex); + /** + * @brief 设置某个网格「自由标记」固定点显示是否附带**括号内增量**。 + * + * 只影响已固定点的取值胶囊(面板的「显示增量」开关), + * 不影响跟随鼠标的预览胶囊与统计列。 + * @param [IN] laneIndex 网格下标。 + * @param [IN] enabled 是否显示增量。 + * @return 状态发生变化返回 true。 + */ + bool SetLaneFreeMarkerShowDelta(int laneIndex, bool enabled); + + /** + * @brief 设置某个网格「自由标记」固定点显示是否附带**枚举文本**。 + * + * 只影响已固定点的取值胶囊(面板的「显示枚举文本」开关), + * 不影响跟随鼠标的预览胶囊与统计列。 + * @param [IN] laneIndex 网格下标。 + * @param [IN] enabled 是否显示枚举文本。 + * @return 状态发生变化返回 true。 + */ + bool SetLaneFreeMarkerShowEnum(int laneIndex, bool enabled); + + /** 查询某网格固定点是否显示增量。 */ + bool LaneFreeMarkerShowDelta(int laneIndex) const; + + /** 查询某网格固定点是否显示枚举文本。 */ + bool LaneFreeMarkerShowEnum(int laneIndex) const; + /** * @brief 设置某网格「自由标记」固定点的**显示不透明度**(0–1)。 * @@ -1710,6 +1738,12 @@ namespace jdi struct MarkerRow { std::size_t itemIndex = 0; + /** + * @brief 该固定点在**本分组内**的序号(从 1 开始,组内按时间升序)。 + * + * 面板最左侧「序号」列直接显示它;按分组重置(每组 = 一个信号)。 + */ + std::size_t serial = 0; gd::RectF rect; gd::RectF close; }; @@ -1758,8 +1792,17 @@ namespace jdi float markerContentHeight = 0.0f; gd::RectF scrollTrack; /**< 内容区右侧的滚动条轨道(不需要滚动时为空矩形)。 */ gd::RectF scrollThumb; /**< 滚动条滑块。 */ - gd::RectF opacityTrack; /**< 「标记透明度」滑动条轨道(仅光标模式激活时非空)。 */ + gd::RectF opacityTrack; /**< 「标记透明度」滑动条轨道(总是显示,与选取模式无关)。 */ gd::RectF opacityValue; /**< 「标记透明度」数值文本矩形。 */ + /** + * @brief 「显示增量」开关按钮(位于「自由标记点选取」下方那一行的左半)。 + * + * 它只控制**已固定点**的取值胶囊是否附带括号内增量, + * 不影响跟随鼠标的预览胶囊,也不影响下面的统计列。 + */ + gd::RectF showDelta; + /** @brief 「显示枚举文本」开关按钮(同一行的右半)。 */ + gd::RectF showEnum; /** * @brief 「自由标记」:每个信号名右侧的「清除该信号全部固定点」✕。 * @@ -1773,29 +1816,31 @@ namespace jdi */ std::vector rowNameRects; /** - * @brief 「自由标记」面板的五列文本区 - * (① 取值 ② 枚举文本 ③ 增量 ④ 时刻 ⑤ 间隔)。 + * @brief 「自由标记」面板的**六列**文本区 + * (① 序号 ② 标记值 ③ 枚举文本 ④ 增量 ⑤ 时间点 ⑥ 时间间隔)。 * - * 每行都在这五列的**同一横坐标**上左对齐输出,因此同一信息在纵向上严格对齐。 - * 列宽固定(不按文本测量),绘制时用 `UiEllipsize` 截断,避免布局期查询采样。 - * 当整个面板里没有任何枚举文本时,② 枚举列的宽度为 0(整列缺省)。 + * 每行都在这六列的**同一横坐标**上左对齐输出,因此同一信息在纵向上严格对齐; + * 相邻列之间画一条竖线作为分隔。 + * 列宽取「缺省下限」「实测内容宽度」「列标题宽度」三者的较大者; + * 当整个面板里没有任何枚举文本时,③ 枚举列的宽度为 0(整列缺省)。 */ struct MarkerColumn { float left = 0.0f; float width = 0.0f; }; - std::array markerColumns{}; - /** 面板里是否存在枚举文本(为 false 时 ② 列整列缺省)。 */ + std::array markerColumns{}; + /** 面板里是否存在枚举文本(为 false 时 ③ 列整列缺省)。 */ bool markerHasEnumColumn = false; - /** 五列的语义下标(`markerColumns` 的索引)。 */ + /** 六列的语义下标(`markerColumns` 的索引)。 */ enum MarkerColumnIndex { - MARKER_COL_VALUE = 0, - MARKER_COL_ENUM = 1, - MARKER_COL_DELTA = 2, - MARKER_COL_TIME = 3, - MARKER_COL_DT = 4 + MARKER_COL_SERIAL = 0, + MARKER_COL_VALUE = 1, + MARKER_COL_ENUM = 2, + MARKER_COL_DELTA = 3, + MARKER_COL_TIME = 4, + MARKER_COL_DT = 5 }; }; @@ -1813,19 +1858,19 @@ namespace jdi * 它描述「鼠标此刻所在的、开启了自由标记的网格」里应当标注的那个数据点: * 该网格内所有可见信号中**离鼠标最近**的那一个。 * - * 两种模式由 `Lane::freeMarkerCursor` 决定: - * * **默认(非光标模式)**:只有鼠标**贴近某条信号曲线**时才有结果(`valid == true`), - * 也**不画**纵向光标。⚠️ 判据 = 鼠标到**画出来的折线**的垂直距离 ≤ - * `kSignalHitTolerance`(与「常规状态下触发信号曲线高亮」**完全同一范围**; - * 因此阶跃的竖直跳变段也能探测到)。判定通过后,标注时刻会被吸附到最近的**采样时刻** - * (固定点必须落在真实数据点上); - * * **光标模式**:网格内始终 `valid == true`(画纵向光标),能取到数据点时再标注取值 - * (此时只有命中「自动点线」稀疏判据的信号才吸附)。 + * 两种情形由 `Lane::freeMarkerCursor` 决定: + * * **未开启「自由标记点选取」**:**没有结果**(`valid == false`)—— 鼠标就是普通鼠标, + * 不预览数据点、不画纵向光标、单击也不固定任何点(用户 2026-10-23 要求); + * * **已开启「自由标记点选取」**:网格内始终 `valid == true`(画纵向光标),但只有鼠标 + * **贴近某条信号曲线**时才有可固定的数据点(`ref` 有效)。⚠️ 判据 = 鼠标到**画出来的折线** + * 的垂直距离 ≤ `kSignalHitTolerance`(与「常规状态下触发信号曲线高亮」**完全同一范围**; + * 因此阶跃的竖直跳变段也能探测到)。命中后标注时刻会被吸附到最近的**采样时刻** + * (固定点必须落在真实数据点上),但“已吸附”圆环只在命中「自动点线」稀疏判据时画。 */ struct FreeMarkerProbe { - bool valid = false; /**< 是否有结果(`ref` 有效,或光标模式下恒真)。 */ - bool cursorMode = false; /**< 该网格是否处于「光标模式」(决定是否画纵向光标)。 */ + bool valid = false; /**< 是否有结果(开启选取模式后恒真;`ref` 才是可固定的数据点)。 */ + bool cursorMode = false; /**< 该网格是否处于「自由标记点选取」(决定是否画纵向光标)。 */ int laneIndex = -1; /**< 该网格下标。 */ double time = 0.0; /**< 标注时刻(显示时刻;吸附时 = 采样时刻)。 */ float x = 0.0f; /**< 该时刻的屏幕横坐标。 */ @@ -1851,13 +1896,6 @@ namespace jdi */ void PaintFreeMarker(gd::Graphics& g, const std::vector& lanes); - /** - * @brief 在鼠标附近画一个小提示气泡(与面板 GX 徽标的提示同一套样式)。 - * @param [IN,OUT] g 绘图上下文。 - * @param [IN] text 提示文本(空串则不绘制)。 - */ - void PaintFreeMarkerTooltip(gd::Graphics& g, const std::wstring& text); - /** @brief 一个固定点 ✕ 的命中矩形(绘制时重建,命中测试时读取)。 */ struct FreeMarkerHit { diff --git a/Components/PlotView/Src/PlotViewInternal.hpp b/Components/PlotView/Inc/PlotViewInternal.hpp similarity index 95% rename from Components/PlotView/Src/PlotViewInternal.hpp rename to Components/PlotView/Inc/PlotViewInternal.hpp index 25f6e88..8a39d2f 100644 --- a/Components/PlotView/Src/PlotViewInternal.hpp +++ b/Components/PlotView/Inc/PlotViewInternal.hpp @@ -340,7 +340,7 @@ namespace jdi /** * @brief 「自由标记」面板里每组分栏下方那一行**列表头**的高度(逻辑像素)。 * - * 表头 = 取值 / (枚举文本)/ 增量 / 时间点 / 时间间隔 五个列标题, + * 表头 = 序号 / 标记值 / (枚举文本)/ 增量 / 时间点 / 时间间隔 的列标题, * 与数据行共用同一组列边界 ⇒ 表头与数据严格对齐。 * ⚠️ 滚动时分组栏与表头会**各自吸顶**(见 `EdgePanelLayoutFor` 的 `MarkerGroup`), * 因此表头高度必须参与内容总高度与吸顶量计算。 @@ -348,35 +348,34 @@ namespace jdi inline const float kFreeMarkerHeaderRowHeight = 18.0f; /** - * @brief 「自由标记」面板表头的五个列标题(顺序与 `markerColumns` 一致: - * ① 取值 ② 枚举 ③ 增量 ④ 时刻 ⑤ 间隔)。 + * @brief 「自由标记」面板表头的列标题(顺序与 `markerColumns` 一致: + * ① 序号 ② 标记值 ③ 枚举文本 ④ 增量 ⑤ 时间点 ⑥ 时间间隔)。 */ - inline const wchar_t* const kFreeMarkerColumnLabels[5] = { - L"标记值", L"枚举文本", L"增量", L"时间点", L"时间间隔"}; + inline const wchar_t* const kFreeMarkerColumnLabels[6] = { + L"序号", L"标记值", L"枚举文本", L"增量", L"时间点", L"时间间隔"}; /** 「自由标记」面板与行尾 ✕ 按钮的边长(网格内取值胶囊后的 ✕ 另按胶囊高度取正方形)。 */ inline const float kFreeMarkerCloseSize = 13.0f; /** - * @brief 「自由标记」面板的宽度(逻辑像素)。 + * @brief 「自由标记」面板的宽度(逻辑像素,作为下限)。 * - * 取固定值而**不做逐行文本测量**:`EdgePanelWidth` 会被 `EdgePanelLayoutFor` 调用, - * 而后者在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用(每次鼠标移动数次); - * 逐行取采样值算宽度会变成"鼠标一动就做几十次采样查询"。 - * - * 宽度 = 左右内边距 + 五列(取值/枚举/增量/时刻/间隔)+ 行 ✕ ⇒ 保证五列在纵向上对齐。 + * 实际宽度 = `max(本值, 六列实测/缺省/列标题宽度之和 + 行 ✕ + 滚动条预留)`; + * 这里取固定下限而**不做逐行文本测量**(`EdgePanelWidth` 被 `EdgePanelLayoutFor` 调用, + * 而后者在 `TopPanelAtPoint` / `SyncPanelOrder` 里被高频调用)。 */ inline const float kFreeMarkerPanelWidth = 424.0f; /** - * @brief 「自由标记」面板五列的逻辑宽度 - * (**按此顺序**:取值 / 枚举文本 / 增量 / 时刻 / 间隔)。 + * @brief 「自由标记」面板六列的逻辑宽度下限 + * (**按此顺序**:序号 / 标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔)。 * - * 固定列宽(不按文本测量,见上),绘制时用 `UiEllipsize` 截断超长文本。 + * 实际列宽取「本下限」「实测内容宽度」「列标题宽度」三者的**较大者** + * ⇒ 面板里的数据与表头都不会出现省略号。 * 当整个面板里没有任何枚举文本时,枚举这一列会被**整列缺省**(列宽记 0, * 后面的列左移),其它列的宽度不变。 */ - inline const float kFreeMarkerColumnWidths[5] = {74.0f, 80.0f, 60.0f, 72.0f, 78.0f}; + inline const float kFreeMarkerColumnWidths[6] = {34.0f, 74.0f, 80.0f, 60.0f, 72.0f, 78.0f}; /** 「自由标记」面板行内相邻两列之间的间距。 */ inline const float kFreeMarkerColumnGap = 2.0f; @@ -395,6 +394,14 @@ namespace jdi /** 「自由标记」面板底部「标记透明度」滑动条行的高度(与信号样式区的行高同量级)。 */ inline const float kFreeMarkerSliderRowHeight = 24.0f; + /** + * @brief 「自由标记」面板底部相邻两行按钮之间的间隙(逻辑像素)。 + * + * 底部固定区自上而下:`标记透明度` 滑块行 → `自由标记点选取` → `显示增量 | 显示枚举文本`。 + * 后两行之间用本间隙分开,避免两个按钮看起来连成一块。 + */ + inline const float kFreeMarkerButtonGap = 4.0f; + /** * @brief 所有实验性功能浮动面板标题栏左侧「展开/折起」指示器的宽度。 * @@ -429,24 +436,30 @@ namespace jdi } /** - * @brief 「自由标记」面板/胶囊里的取值部分:`值(+增量) = 枚举文本`。 + * @brief 「自由标记」面板/胶囊里的取值部分:`值[(±增量)] [= 枚举文本]`。 + * + * 两个开关仅作用于**已固定点**的显示(「显示增量」/「显示枚举文本」): + * 跟随鼠标的预览胶囊恒传 `showDelta=false, showEnum=true`(保持原有观感), + * 面板统计列也不看这两个开关(它总是显示全部信息)。 * * @param value 当前取值(已叠加「调整」变换)。 + * @param showDelta 是否显示括号内增量。 * @param hasPrev 是否存在前一个同信号固定点(存在才显示增量)。 * @param delta 相对前一个固定点的取值增量。 + * @param showEnum 是否显示枚举文本。 * @param enumText 枚举文本(可为空指针)。 */ - inline std::wstring FreeMarkerValueText(double value, bool hasPrev, double delta, - const std::wstring* enumText) + inline std::wstring FreeMarkerValueText(double value, bool showDelta, bool hasPrev, double delta, + bool showEnum, const std::wstring* enumText) { std::wstring text = FormatValue(value); - if (hasPrev) + if (showDelta && hasPrev) { text += L"("; text += FormatSignedValue(delta); text += L")"; } - if (enumText != nullptr && !enumText->empty()) + if (showEnum && enumText != nullptr && !enumText->empty()) { text += L" = "; text += *enumText; @@ -565,8 +578,12 @@ 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 kFreeMarkerCursorModeLabel = L"光标选取模式"; + /** 「自由标记」面板底部「自由标记点选取」切换按钮文案(激活态 = 光标模式开启)。 */ + inline const wchar_t* const kFreeMarkerCursorModeLabel = L"自由标记点选取"; + /** 「自由标记」面板底部「显示增量」切换按钮文案(只影响已固定点的显示)。 */ + inline const wchar_t* const kFreeMarkerShowDeltaLabel = L"显示增量"; + /** 「自由标记」面板底部「显示枚举文本」切换按钮文案(只影响已固定点的显示)。 */ + inline const wchar_t* const kFreeMarkerShowEnumLabel = L"显示枚举文本"; /** 「自由标记」面板底部切换按钮激活时出现在其上方的滑动条标签。 */ inline const wchar_t* const kFreeMarkerOpacityLabel = L"标记透明度"; /** diff --git a/Components/PlotView/Src/PlotViewFeatures.cpp b/Components/PlotView/Src/PlotViewFeatures.cpp index da4fcb5..236ab36 100644 --- a/Components/PlotView/Src/PlotViewFeatures.cpp +++ b/Components/PlotView/Src/PlotViewFeatures.cpp @@ -1693,6 +1693,54 @@ namespace jdi return enabled; } + bool PlotView::SetLaneFreeMarkerShowDelta(int laneIndex, bool enabled) + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return false; + } + Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + if (lane.freeMarkerShowDelta == enabled) + { + return false; + } + lane.freeMarkerShowDelta = enabled; + return true; + } + + bool PlotView::SetLaneFreeMarkerShowEnum(int laneIndex, bool enabled) + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return false; + } + Lane& lane = m_tab->Lanes()[static_cast(laneIndex)]; + if (lane.freeMarkerShowEnum == enabled) + { + return false; + } + lane.freeMarkerShowEnum = enabled; + return true; + } + + bool PlotView::LaneFreeMarkerShowDelta(int laneIndex) const + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return false; + } + return m_tab->Lanes()[static_cast(laneIndex)].freeMarkerShowDelta; + } + + bool PlotView::LaneFreeMarkerShowEnum(int laneIndex) const + { + if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) + { + return false; + } + return m_tab->Lanes()[static_cast(laneIndex)].freeMarkerShowEnum; + } + bool PlotView::SetLaneFreeMarkerOpacity(int laneIndex, float opacity) { if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast(m_tab->Lanes().size())) diff --git a/Components/PlotView/Src/PlotViewInteraction.cpp b/Components/PlotView/Src/PlotViewInteraction.cpp index 5c79a8e..424e558 100644 --- a/Components/PlotView/Src/PlotViewInteraction.cpp +++ b/Components/PlotView/Src/PlotViewInteraction.cpp @@ -966,12 +966,22 @@ namespace jdi // 滚动条与「标记透明度」滑块则是拖动类控件(上下 / 左右箭头)。 if (static_cast(panelKind) == EdgePanelKind::MARK) { - // 底部「光标选取模式」切换按钮。 + // 底部「自由标记点选取」切换按钮与两个显示开关。 if (layout.button.Width > 0.0f && UiPointInRect(layout.button, static_cast(x), static_cast(y))) { return LoadCursorW(nullptr, IDC_HAND); } + if (layout.showDelta.Width > 0.0f && + UiPointInRect(layout.showDelta, static_cast(x), static_cast(y))) + { + return LoadCursorW(nullptr, IDC_HAND); + } + if (layout.showEnum.Width > 0.0f && + UiPointInRect(layout.showEnum, static_cast(x), static_cast(y))) + { + return LoadCursorW(nullptr, IDC_HAND); + } if (layout.opacityTrack.Width > 0.0f && WeakPeakSliderHit(layout.opacityTrack, m_scale, static_cast(x), static_cast(y))) @@ -2285,13 +2295,27 @@ namespace jdi if (kind == EdgePanelKind::MARK && m_tab != nullptr) { Lane& tabLane = m_tab->Lanes()[static_cast(index)]; - // 底部切换按钮「光标选取模式」(默认关闭)。 + // 底部切换按钮「自由标记点选取」(默认关闭)。 if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy)) { ToggleLaneFreeMarkerCursor(index); m_dragMode = PlotDragMode::NONE; return true; } + // 「显示增量」开关(只影响已固定点的显示;默认关闭)。 + if (layout.showDelta.Width > 0.0f && UiPointInRect(layout.showDelta, fx, fy)) + { + SetLaneFreeMarkerShowDelta(index, !tabLane.freeMarkerShowDelta); + m_dragMode = PlotDragMode::NONE; + return true; + } + // 「显示枚举文本」开关(只影响已固定点的显示;默认关闭)。 + if (layout.showEnum.Width > 0.0f && UiPointInRect(layout.showEnum, fx, fy)) + { + SetLaneFreeMarkerShowEnum(index, !tabLane.freeMarkerShowEnum); + m_dragMode = PlotDragMode::NONE; + return true; + } // 「标记透明度」滑块:按下即写入该处数值并进入拖动。 if (layout.opacityTrack.Width > 0.0f && WeakPeakSliderHit(layout.opacityTrack, m_scale, fx, fy)) @@ -2424,17 +2448,18 @@ namespace jdi } } - // (2) 在开启「自由标记」的网格内按下:单击固定数据点、拖动仍是原有语义 + // (2) 在开启「自由标记点选取」的网格内按下:单击固定数据点、拖动仍是原有语义 // (常规模式 = 平移;选区放大模式 = 框选)。 // ⚠️ 用户明确要求:「仅单击时」才算固定数据点 —— 因此这里**只登记意图**, // 真正固定发生在 `OnLButtonUp` 且整段操作没有发生过拖动。 - // ⚠️ 默认(非光标模式)下 `ProbeFreeMarker` 只在鼠标贴近曲线时有效 ⇒ - // 鼠标下方没有信号时按下就是普通的平移/框选(用户明确要求)。 + // ⚠️⚠️ 只有「自由标记点选取」已开启才参与(用户 2026-10-23 要求): + // 未开启时鼠标是普通鼠标,按下/拖动/单击完全是常规语义。 if (!freeOverPanel) { const int freeLane = LaneAtY(y); if (freeLane >= 0 && freeLane < static_cast(m_tab ? m_tab->Lanes().size() : 0) && - m_tab->Lanes()[static_cast(freeLane)].freeMarker) + m_tab->Lanes()[static_cast(freeLane)].freeMarker && + m_tab->Lanes()[static_cast(freeLane)].freeMarkerCursor) { const FreeMarkerProbe probe = ProbeFreeMarker(x, y); if (probe.valid && probe.ref.IsValid()) diff --git a/Components/PlotView/Src/PlotViewPaint.cpp b/Components/PlotView/Src/PlotViewPaint.cpp index a1d2497..62cd70d 100644 --- a/Components/PlotView/Src/PlotViewPaint.cpp +++ b/Components/PlotView/Src/PlotViewPaint.cpp @@ -144,8 +144,8 @@ namespace jdi if (kind == EdgePanelKind::MARK) { - // 「自由标记」面板:宽度 = 五列(取「缺省列宽」与**实测内容宽度**的较大者)+ 行 ✕ - // + 左右内边距 + **滚动条预留位**(内容超高时右侧要放滚动条)。 + // 「自由标记」面板:宽度 = 六列(取「缺省列宽」「实测内容宽度」「列标题宽度」三者 + // 的大者)+ 行 ✕ + 左右内边距 + **滚动条预留位**(内容超高时右侧要放滚动条)。 // ⚠️ 用户 2026-10-21 要求面板里的数据列**不得出现省略号**,因此不能再按固定列宽。 // 逐行取采样值算宽度会拖慢交互(本函数被 `TopPanelAtPoint` / `SyncPanelOrder` // 高频调用),所以走 `Lane::freeMarkerColumnWidths`:绘制该面板时顺便量好真实 @@ -163,7 +163,7 @@ namespace jdi return false; }(); float columnsWidth = 0.0f; - for (int column = 0; column < 5; ++column) + for (int column = 0; column < 6; ++column) { const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum; if (omitted) @@ -5460,8 +5460,7 @@ namespace jdi // 「自由标记」面板:按信号分组(**分组栏** + 下属固定点条目), // 只列出确实有固定点的信号;没有任何固定点时给一行灰色提示。 - // 内容区超高时出现滚动条;面板底部固定「标记透明度」滑块(仅光标模式激活时) - // 与「光标选取模式」切换按钮(二者**不参与**滚动)。 + // 底部固定区(自上而下):标记透明度滑块 → 「自由标记点选取」→ 两个显示开关。 bool markerAny = false; for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems) { @@ -5477,8 +5476,13 @@ namespace jdi const float headerRow = kFreeMarkerHeaderRowHeight * m_scale; const float itemRow = kFreeMarkerItemRowHeight * m_scale; const float markerButtonHeight = kEdgePanelButtonHeight * m_scale; - // 「标记透明度」滑块行**始终显示**(用户 2026-10-21 要求:不再依附于光标模式按钮)。 + // 「标记透明度」滑块行**始终显示**(用户 2026-10-21 要求:不再依附于「自由标记点选取」按钮)。 const float sliderRow = kFreeMarkerSliderRowHeight * m_scale; + // 「显示增量 / 显示枚举文本」两个开关按钮独占一行,位于「自由标记点选取」下方 + //(用户 2026-10-23 要求)。 + const float switchRow = kEdgePanelButtonHeight * m_scale; + const float buttonGap = kFreeMarkerButtonGap * m_scale; + const float footer = sliderRow + markerButtonHeight + buttonGap + switchRow; float content = 0.0f; std::vector order; for (const SignalRef& ref : lane.signals) @@ -5507,13 +5511,12 @@ namespace jdi content += groupRow; } // 内容区高度上限:太长的列表不能把面板顶出绘图区(超出部分靠滚动条翻页)。 - const float budget = - m_plotRect.Height - (titleHeight + pad * 3.0f + markerButtonHeight + sliderRow); + const float budget = m_plotRect.Height - (titleHeight + pad * 3.0f + footer); const float maxContent = std::max(2.0f * itemRow, std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(2.0f * itemRow, budget))); const float view = std::min(content, maxContent); - height = titleHeight + pad + view + pad + sliderRow + markerButtonHeight + pad; + height = titleHeight + pad + view + pad + footer + pad; } // 折起的面板:只剩标题栏(内容区完全不占高度)。 @@ -5773,17 +5776,29 @@ namespace jdi const float itemRow = kFreeMarkerItemRowHeight * m_scale; const float closeSize = kFreeMarkerCloseSize * m_scale; const float markerButtonHeight = kEdgePanelButtonHeight * m_scale; - // 「标记透明度」独立于光标模式按钮:**总是**显示在按钮上方。 + // 「标记透明度」独立于「自由标记点选取」:**总是**显示在按钮上方。 const float sliderRow = kFreeMarkerSliderRowHeight * m_scale; const float scrollW = kFreeMarkerScrollWidth * m_scale; + // 「显示增量 / 显示枚举文本」两个开关按钮:独占底部最后一行(左右各半个宽度)。 + const float switchRow = kEdgePanelButtonHeight * m_scale; + const float buttonGap = kFreeMarkerButtonGap * m_scale; - // 底部固定区:切换按钮(整行宽度、与面板下边界留一个 pad)。 - layout.button = gd::RectF(left + pad, top + height - pad - markerButtonHeight, - std::max(0.0f, width - pad * 2.0f), markerButtonHeight); + // 底部固定区(自上而下):标记透明度滑块 → 「自由标记点选取」→ 两个显示开关。 + { + const float switchTop = top + height - pad - switchRow; + const float halfWidth = + std::max(0.0f, (width - pad * 2.0f - buttonGap) * 0.5f); + layout.showDelta = gd::RectF(left + pad, switchTop, halfWidth, switchRow); + layout.showEnum = + gd::RectF(left + pad + halfWidth + buttonGap, switchTop, halfWidth, switchRow); + // 「自由标记点选取」切换按钮(整行宽度)。 + layout.button = gd::RectF(left + pad, switchTop - buttonGap - markerButtonHeight, + std::max(0.0f, width - pad * 2.0f), markerButtonHeight); + } { // 「标记透明度」行:与信号样式区的「透明度」行同一套排版 //(左侧标签 + 中间轨道 + 右侧百分比),位置固定在按钮上方。 - // ⚠️ 无论是否启用「光标选取模式」都显示(用户 2026-10-21 要求)。 + // ⚠️ 无论是否启用「自由标记点选取」都显示(用户 2026-10-21 要求)。 const float rowTop = layout.button.Y - sliderRow; const float valueWidth = std::max(36.0f * m_scale, @@ -5802,7 +5817,8 @@ namespace jdi // 内容区高度(面板高度算式里的 `view` 部分)与"内容总高"决定是否需要滚动条。 const float viewHeight = std::max( - 0.0f, height - titleHeight - pad * 3.0f - markerButtonHeight - sliderRow); + 0.0f, height - titleHeight - pad * 3.0f - markerButtonHeight - sliderRow - + buttonGap - switchRow); float totalContent = 0.0f; std::vector order; for (const SignalRef& ref : lane.signals) @@ -5848,9 +5864,9 @@ namespace jdi layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight); } - // 五列(① 取值 ② 枚举 ③ 增量 ④ 时刻 ⑤ 间隔)从左往右**固定列宽**排布: - // 每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户明确要求)。 - // ⚠️ 整个面板里没有任何枚举文本时,② 枚举列**整列缺省**(列宽记 0,后面的列左移)。 + // 六列(① 序号 ② 标记值 ③ 枚举文本 ④ 增量 ⑤ 时间点 ⑥ 时间间隔) + // 从左往右排布:每一行的同类信息都从同一横坐标开始 ⇒ 纵向上严格对齐(用户要求)。 + // ⚠️ 整个面板里没有任何枚举文本时,③ 枚举列**整列缺省**(列宽记 0,后面的列左移)。 // 这里只查"该信号是否带枚举表"(O(1)),**不**逐条采样(布局函数被高频调用)。 bool hasEnum = false; for (const SignalRef& ref : lane.signals) @@ -5865,7 +5881,7 @@ namespace jdi } layout.markerHasEnumColumn = hasEnum; float columnX = left + pad; - for (int column = 0; column < 5; ++column) + for (int column = 0; column < 6; ++column) { const bool omitted = (column == EdgePanelLayout::MARKER_COL_ENUM) && !hasEnum; // ⚠️ 列宽取「缺省列宽」「实测内容宽度」与**表头列标题宽度**三者的大者: @@ -5957,10 +5973,13 @@ namespace jdi continue; } rowY += headerRow; - for (const std::size_t itemIndex : order) + for (std::size_t orderPosition = 0; orderPosition < order.size(); ++orderPosition) { + const std::size_t itemIndex = order[orderPosition]; EdgePanelLayout::MarkerRow markerRow; markerRow.itemIndex = itemIndex; + // 「序号」列:组内按时间升序从 1 开始(每组 = 一个信号)。 + markerRow.serial = orderPosition + 1; markerRow.rect = gd::RectF(layout.content.X, rowY, innerWidth, itemRow); markerRow.close = gd::RectF(layout.content.X + innerWidth - closeSize, rowY + (itemRow - closeSize) * 0.5f, closeSize, @@ -6445,6 +6464,8 @@ namespace jdi mixRect(layout.badge); mixRect(layout.edit); mixRect(layout.button); + mixRect(layout.showDelta); + mixRect(layout.showEnum); mixRect(layout.content); mixRect(layout.scrollTrack); mixRect(layout.scrollThumb); @@ -6508,6 +6529,10 @@ namespace jdi // 底部切换按钮的激活态、透明度滑块与内容滚动偏移直接改变面板像素 ⇒ 必须进键, // 否则开关 / 拖滑块 / 滚动之后仍会贴回旧像素。 mix(lane.freeMarkerCursor ? 1ull : 0ull); + // 「显示增量 / 显示枚举文本」两个开关直接改变已固定点的胶囊文本 ⇒ 必须进键 + // (否则切换开关后仍会贴回旧像素)。 + mix(lane.freeMarkerShowDelta ? 1ull : 0ull); + mix(lane.freeMarkerShowEnum ? 1ull : 0ull); mixFloat(lane.freeMarkerOpacity); mixFloat(lane.freeMarkerScroll); // 选中行会改变整行底色 ⇒ 必须进键(否则单击行后贴回旧像素)。 @@ -6740,6 +6765,9 @@ namespace jdi mix(bits); }; mix(static_cast(lane.freeMarkerItems.size())); + // ⚠️ 两个显示开关只影响胶囊文本,但同样属于“与视界无关的输入”,必须进键。 + mix(lane.freeMarkerShowDelta ? 1ull : 0ull); + mix(lane.freeMarkerShowEnum ? 1ull : 0ull); for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems) { mix(static_cast(static_cast(item.ref.groupIndex))); @@ -6762,6 +6790,15 @@ namespace jdi mix(static_cast(static_cast(resolved.style->mode))); mix((resolved.style->absolute ? 1ull : 0ull) | (resolved.style->mirrorVertical ? 2ull : 0ull) | (resolved.style->mirrorHorizontal ? 4ull : 0ull)); + // ⚠️ 枚举表的内容决定「枚举文本」列与胶囊里的 `= <文本>`(以及缺省时的 `N/A`): + // 旧实现只混了信号指针与采样数量 ⇒ 重新加载/替换枚举表后仍贴回旧文本 + //(离屏探针实测:加上枚举表后条目里仍是 `N/A`)。这里把表的规模与文本数组 + // 的地址一并入键 —— 只比较 `size()` 不足以区分“换成另一张同样长度的表”。 + mix(static_cast(resolved.data->enumText.values.size())); + mix(static_cast(resolved.data->enumText.texts.size())); + mix(static_cast(resolved.data->enumText.rangeMin.size())); + mix(static_cast( + reinterpret_cast(resolved.data->enumText.texts.data()))); } if (cache.key != key) @@ -6790,9 +6827,12 @@ namespace jdi entry.valid = valueOk; entry.value = value; entry.color = (resolved.style != nullptr) ? StyleColor(*resolved.style) : 0u; - entry.capsuleText = FreeMarkerValueText(value, hasPrev, delta, enumPtr); + entry.capsuleText = FreeMarkerValueText(value, lane.freeMarkerShowDelta, hasPrev, delta, + lane.freeMarkerShowEnum, enumPtr); entry.valueText = valueOk ? FormatValue(value) : std::wstring(kFreeMarkerNotAvailable); - entry.enumText = (enumPtr != nullptr) ? *enumPtr : std::wstring(); + // ⚠️ 枚举列一旦存在(面板里有任一固定点带枚举表),**没有**枚举文本的点必须显示 + // `N/A` 而不是空着(用户 2026-10-23 要求);数值对不上任何枚举项也落在这一分支。 + entry.enumText = (enumPtr != nullptr) ? *enumPtr : std::wstring(kFreeMarkerNotAvailable); entry.deltaText = (valueOk && hasPrev) ? FormatSignedValue(delta) : std::wstring(kFreeMarkerNotAvailable); entry.timeText = FreeMarkerTimeText(item.time); @@ -6988,6 +7028,10 @@ namespace jdi continue; } const float markerAlpha = std::max(0.0f, std::min(1.0f, lane.freeMarkerOpacity)); + // 面板里正悬停着本网格的某一行 ⇒ 其余固定点“压暗”以突出目标 + //(用户 2026-10-23 要求:透明度高于 50% 时降到 50%,移出后恢复)。 + const bool rowHoverActive = (m_freeMarkerRowHoverLane == static_cast(laneIndex) && + m_freeMarkerRowHoverItem != static_cast(-1)); // 内容(取值 / 文本 / 信号色)来自与视界无关的缓存,不逐帧重新采样。 const std::vector& contents = FreeMarkerContents(static_cast(laneIndex)); for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex) @@ -7015,13 +7059,16 @@ namespace jdi // 鼠标悬停在面板里这一行时:网格内对应的固定点整体高亮(用户 2026-10-22 要求)。 const bool rowHot = (m_freeMarkerRowHoverLane == static_cast(laneIndex) && m_freeMarkerRowHoverItem == itemIndex); + // 本行实际使用的不透明度:目标行强制不透明;其它行在“悬停联动”期间封顶 50%。 + const float rowAlpha = + rowHot ? 1.0f + : ((rowHoverActive && markerAlpha > 0.5f) ? 0.5f : markerAlpha); - // 数据点:与光标的数据点同一画法(圆点),同样受「标记透明度」控制。 + // 数据点:与光标的数据点同一画法(圆点),同样受「标记透明度」控制; + // 悬停联动期间其它点封顶 50%(见 `rowAlpha`)。 const float x = TimeToX(item.time); const float radius = 3.4f * m_scale; - // 高亮(面板里悬停该行)时数据点必须完整可见 ⇒ 强制不透明。 - const float effectiveAlpha = rowHot ? 1.0f : markerAlpha; - const int dotAlpha = static_cast(std::lround(255.0f * effectiveAlpha)); + const int dotAlpha = static_cast(std::lround(255.0f * rowAlpha)); gd::SolidBrush dot(ToGdiColor(WithAlpha(signalColor, dotAlpha))); const float dotY = valueOk ? ValueToY(entry.value, visual) : (clipTop + clipBottom) * 0.5f; @@ -7043,10 +7090,10 @@ namespace jdi // ⚠️ 例外:如果此刻跟随探针正瞄着一个**被这个胶囊遮住的数据点** //(用户意图标记被遮住的那条曲线),则不恢复 —— 否则胶囊会重新遮住目标点、 // 无法看出自己到底瞄到了哪个采样点。 - float capsuleAlpha = markerAlpha; + float capsuleAlpha = rowAlpha; const float fx = static_cast(m_mouseX); const float fy = static_cast(m_mouseY); - if (markerAlpha < 0.999f && UiPointInRect(label, fx, fy)) + if (capsuleAlpha < 0.999f && UiPointInRect(label, fx, fy)) { const bool probeHiddenByLabel = m_freeMarker.valid && m_freeMarker.laneIndex == static_cast(laneIndex) && @@ -7071,7 +7118,7 @@ namespace jdi hit.laneIndex = static_cast(laneIndex); hit.itemIndex = itemIndex; hit.close = close; - if (markerAlpha > 0.02f) + if (rowAlpha > 0.02f) { // 完全透明时不可见 ⇒ 也不该被点中(否则会“隔着看不见的按钮”移除固定点)。 m_freeMarkerHits.push_back(hit); @@ -7079,7 +7126,7 @@ namespace jdi } } - // ---- (2) 跟随鼠标的纵向光标(**仅「光标模式」**;默认不画,只给贴近曲线的取值预览)---- + // ---- (2) 跟随鼠标的纵向光标(**仅「自由标记点选取」开启时**;`probe.valid` 恒真)---- const FreeMarkerProbe& probe = m_freeMarker; if (probe.valid && probe.laneIndex >= 0 && probe.laneIndex < static_cast(lanes.size())) @@ -7091,7 +7138,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)); - // ⚠️ 用户明确要求:默认**不启用**网格私有光标 —— 只有「光标模式」才画这条线。 + // ⚠️ 用户明确要求:未开启「自由标记点选取」时**连这条线都不画**(`ProbeFreeMarker` 直接早退, + // `probe.valid` 为假);`cursorMode` 只是“确实处于选取模式”的双重保险。 if (probe.cursorMode && clipBottom > clipTop) { UiDrawLine(g, x, clipTop, x, clipBottom, lineColor, 1.4f * m_scale); @@ -7118,7 +7166,10 @@ namespace jdi { const std::uint32_t signalColor = StyleColor(*resolved.style); const std::wstring* enumText = resolved.data->enumText.Find(probe.enumValue); - const std::wstring text = FreeMarkerValueText(probe.value, false, 0.0, enumText); + // 跟随鼠标的**预览**胶囊不受「显示增量 / 显示枚举文本」开关影响: + // 它总是显示「值 = 枚举」(与两个开关的默认关闭状态无关)。 + const std::wstring text = + FreeMarkerValueText(probe.value, false, false, 0.0, true, enumText); const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale; const float textHeight = 17.0f * m_scale; @@ -7152,38 +7203,8 @@ namespace jdi } g.Restore(saveState); - - // 悬停在固定点 ✕ 上时给出提示(与面板 ✕ 的提示风格一致)。 - if (m_freeMarkerHoverLane >= 0 && m_freeMarkerHoverItem != static_cast(-1)) - { - PaintFreeMarkerTooltip(g, L"点击移除此固定数据点"); - } } - void PlotView::PaintFreeMarkerTooltip(gd::Graphics& g, const std::wstring& text) - { - if (text.empty() || m_fonts == nullptr) - { - return; - } - const float pad = 5.0f * m_scale; - const float width = UiMeasureTextWidth(m_fonts->uiSmall, text) + pad * 2.0f; - const float height = 18.0f * m_scale; - float left = static_cast(m_mouseX) + 12.0f * m_scale; - float top = static_cast(m_mouseY) + 16.0f * m_scale; - if (left + width > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale) - { - left = m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale; - } - if (top + height > m_plotRect.Y + m_plotRect.Height - 2.0f * m_scale) - { - top = static_cast(m_mouseY) - height - 10.0f * m_scale; - } - const gd::RectF bubble(left, top, width, height); - UiFillRoundRect(g, bubble, 4.0f * m_scale, m_theme->readoutBg); - UiDrawRoundRect(g, bubble, 4.0f * m_scale, m_theme->borderStrong, 1.0f); - UiText(g, text, m_fonts->uiSmall, bubble, m_theme->textStrong, TextAlign::CENTER); - } PlotView::FreeMarkerProbe PlotView::ProbeFreeMarker(int mouseX, int mouseY) const { @@ -7204,11 +7225,17 @@ namespace jdi { return probe; } + // ⚠️⚠️ **未开启「自由标记点选取」时鼠标就是普通鼠标**(用户 2026-10-23 明确要求): + // 不预览数据点、不画纵向光标、按下/拖动完全走常规语义(平移 / 框选 / 拖信号), + // 也不会固定任何数据点。所有自由标记的探测都从这一句的“早退”开始。 + if (!lane.freeMarkerCursor) + { + return probe; + } const LaneVisual& visual = m_lanes[static_cast(laneIndex)]; - // 光标模式(默认关闭):决定"是否在网格内始终显示纵向光标"以及 - // "鼠标离曲线多远还算有信号"。关闭时只有贴近曲线(或稀疏判定下贴近数据点)才有结果。 - const bool cursorMode = lane.freeMarkerCursor; + // 「自由标记点选取」已开启:网格内始终跟随鼠标画纵向光标(见 `PaintFreeMarker`)。 + const bool cursorMode = true; probe.cursorMode = cursorMode; probe.laneIndex = laneIndex; probe.time = XToTime(static_cast(mouseX)); @@ -7362,16 +7389,21 @@ namespace jdi // ---- ② 决定标注时刻:先取曲线上的最近点,再吸附到采样点 ---- const bool hitCurve = (distance <= static_cast(tolerance)); - if (!hitCurve && !cursorMode) + // ⚠️ 只有**贴着曲线**的那根信号才是可标注目标:否则整列预览时会把“离鼠标很远 + // 的那根曲线的采样值”当成预览值(`probe.ref` 会一直有效 ⇒ 任意位置单击都能固定)。 + if (!hitCurve) { - continue; // 非光标模式:鼠标不在曲线附近 ⇒ 该信号不参与 + continue; } - double time = hitCurve ? XToTime(nearestX) : probe.time; + double time = XToTime(nearestX); + // ⚠️ **命中曲线时始终吸附到最近的采样点**:固定点必须落在真实采样时刻上 + //(第四轮的硬约束;否则面板里的「标记值」会变成插值出来的中间值)。 + // ⚠️ 但表示“已吸附”的那个圆环**只在「自动点线」判据成立(`sparse`)时**才画: + // 密采样信号里吸附量远小于 1 像素,圆环会一直挂在那里,反而像是画错了。 + // ⚠️ 鼠标**没有**命中任何曲线时(上面已 `continue`)不存在吸附问题:那时的纵向 + // 光标由 `probe.time`(鼠标横坐标)驱动,光标不能自己跳到采样点上去。 bool snapped = false; - if (!cursorMode || sparse) { - // 非光标模式恒吸附(固定点必须落在真实采样点上,阶跃两端才点得中); - // 光标模式只在「自动点线」判据成立时吸附(保持原有手感)。 double candidate = 0.0; if (snapToNearestSample(*resolved.axis, *resolved.data, *resolved.style, time, candidate)) { @@ -7408,9 +7440,8 @@ namespace jdi if (!probe.ref.IsValid()) { - // 网格内没有可标注的数据点:光标模式下仍要画纵向线(`valid` 为真), - // 非光标模式下则**什么都不显示**,鼠标按下即普通的平移/框选。 - probe.valid = cursorMode; + // 网格内没有可标注的数据点:「自由标记点选取」模式下仍要画纵向线(`valid` 为真)。 + probe.valid = true; return probe; } probe.valid = true; @@ -8027,7 +8058,7 @@ namespace jdi } // 「自由标记」面板:按信号分组的**分组栏**(信号色托底 + 三角 + 名称 + - // 「N 标记点」+ ✕),下属固定点条目五列对齐(取值 / 枚举 / 增量 / 时刻 / 间隔), + // 「N 标记点」+ ✕),下属固定点条目六列对齐(序号 / 取值 / 枚举 / 增量 / 时刻 / 间隔), // 行尾各带一个 ✕。内容区有滚动偏移时按内容区裁剪。 if (freeMarker) { @@ -8043,7 +8074,12 @@ namespace jdi if (!markerAny) { - UiTextClipped(g, L"在网格内单击以固定数据点", m_fonts->uiSmall, + // ⚠️ 未开启「自由标记点选取」时鼠标是普通鼠标,**单击不会固定**任何数据点 + //(用户 2026-10-23 要求)⇒ 提示语必须说清前提,否则用户会以为 bug。 + const wchar_t* emptyHint = tabLane.freeMarkerCursor + ? L"在网格内单击以固定数据点" + : L"开启「自由标记点选取」后单击网格可固定数据点"; + UiTextClipped(g, emptyHint, m_fonts->uiSmall, gd::RectF(layout.content.X, layout.content.Y, layout.content.Width, groupRowHeight), m_theme->textMuted, TextAlign::LEFT, layout.content); @@ -8132,23 +8168,35 @@ namespace jdi } } - // ---- 固定点条目(五列,列起点由布局给定 ⇒ 纵向严格对齐)---- + // ---- 固定点条目(六列,列起点由布局给定 ⇒ 纵向严格对齐)---- // ⚠️ 取值与各列文本来自**与视界无关的内容缓存**(`FreeMarkerContents`): // 大量固定点时逐帧重新采样会让"拖光标 / 平移视界"明显变卡。 const std::vector& contents = FreeMarkerContents(static_cast(index)); - // 五列的**实测宽度**:含滚出视界的条目,保证列宽稳定、文本不被省略。 + // 六列的**实测宽度**:含滚出视界的条目,保证列宽稳定、文本不被省略。 // 存回 `Lane` 供下一步的布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)使用。 { - float widest[5] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + float widest[6] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; const auto measure = [this, &widest](std::size_t column, const std::wstring& text) { - if (text.empty() || column >= 5) + if (text.empty() || column >= 6) { return; } widest[column] = std::max(widest[column], UiMeasureTextWidth(m_fonts->monoSmall, text)); }; + // 「序号」列:取本网格所有分组里最大的序号文本宽度。 + std::size_t maxSerial = 0; + std::vector serialOrder; + for (const SignalRef& ref : tabLane.signals) + { + SortedFreeMarkerIndices(tabLane, ref, serialOrder); + maxSerial = std::max(maxSerial, serialOrder.size()); + } + if (maxSerial > 0) + { + measure(EdgePanelLayout::MARKER_COL_SERIAL, std::to_wstring(maxSerial)); + } for (const FreeMarkerContentEntry& entry : contents) { measure(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText); @@ -8157,9 +8205,9 @@ namespace jdi measure(EdgePanelLayout::MARKER_COL_TIME, entry.timeText); measure(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText); } - std::array& stored = + std::array& stored = tabLanes[index].freeMarkerColumnWidths; - for (std::size_t column = 0; column < 5; ++column) + for (std::size_t column = 0; column < 6; ++column) { // +2px 余量:`DrawTextW` 的 `DT_END_ELLIPSIS` 对"刚好装满"的文本 // 有时仍会截成省略号(亚像素/字形伸部),留一点余量可彻底避免。 @@ -8171,6 +8219,31 @@ namespace jdi } } } + // 列分隔竖线:在每列右边界(加上半个列间隙)画一条淡竖线, + // 让「序号 / 标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔」有明显的列分界 + //(用户 2026-10-23 要求)。最后一列不画(它右边紧接行尾 ✕)。 + const auto paintColumnSeparators = [this, &g, &layout](float y, float height) { + int lastVisible = -1; + for (int column = 0; column < 6; ++column) + { + if (layout.markerColumns[column].width > 0.0f) + { + lastVisible = column; + } + } + for (int column = 0; column < lastVisible; ++column) + { + const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column]; + if (cell.width <= 0.0f) + { + continue; + } + const float lineX = + cell.left + cell.width + kFreeMarkerColumnGap * m_scale * 0.5f; + UiDrawLine(g, lineX, y + 2.0f * m_scale, lineX, y + height - 2.0f * m_scale, + WithAlpha(m_theme->borderStrong, 0x6C), 1.0f); + } + }; for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows) { if (markerRow.itemIndex >= contents.size()) @@ -8213,6 +8286,8 @@ namespace jdi UiTextClipped(g, text, m_fonts->monoSmall, columnRect(column), color, TextAlign::LEFT, layout.content); }; + paintCell(EdgePanelLayout::MARKER_COL_SERIAL, std::to_wstring(markerRow.serial), + m_theme->textMuted); paintCell(EdgePanelLayout::MARKER_COL_VALUE, entry.valueText, m_theme->textStrong); if (layout.markerHasEnumColumn) @@ -8225,6 +8300,9 @@ namespace jdi paintCell(EdgePanelLayout::MARKER_COL_DT, entry.deltaTimeText, m_theme->textMuted); + // 列分隔竖线(用户 2026-10-23 要求:各列之间要有明显分界)。 + paintColumnSeparators(markerRow.rect.Y, markerRow.rect.Height); + // 行尾 ✕:移除该固定点(与之前一致)。 const gd::RectF& closeRect = markerRow.close; const bool hot = (m_freeMarkerPanelHoverLane == static_cast(index) && @@ -8258,7 +8336,7 @@ namespace jdi group.tableHeader.X + group.tableHeader.Width, group.tableHeader.Y + group.tableHeader.Height - 1.0f, WithAlpha(m_theme->borderStrong, 0x99), 1.0f); - for (int column = 0; column < 5; ++column) + for (int column = 0; column < 6; ++column) { const EdgePanelLayout::MarkerColumn& cell = layout.markerColumns[column]; @@ -8271,6 +8349,8 @@ namespace jdi group.tableHeader.Height), m_theme->textMuted, TextAlign::LEFT, layout.content); } + // 表头的列分隔竖线(与数据行同一套列边界)。 + paintColumnSeparators(group.tableHeader.Y, group.tableHeader.Height); } // ---- 吸顶的分组栏(最后画:压住向上滚动的条目)---- @@ -8293,7 +8373,7 @@ namespace jdi m_theme->borderStrong); } - // ---- 「标记透明度」滑动条(仅「光标选取模式」激活时出现,固定在按钮上方)---- + // ---- 「标记透明度」滑动条(固定在「自由标记点选取」按钮上方)---- if (layout.opacityTrack.Width > 0.0f) { UiText(g, kFreeMarkerOpacityLabel, m_fonts->uiSmall, @@ -8350,32 +8430,51 @@ namespace jdi } // 逐条目面板底部按钮行:「构建为新信号」(悬停高亮); - // 「自由标记」面板的按钮是「光标选取模式」的**切换类按钮** - //(交互外观与工具栏的切换按钮一致:激活时浅强调色底 + 强调描边 + 强文本色)。 - if (hasButton || freeMarker) - { - const gd::RectF& buttonRect = layout.button; - if (buttonRect.Width > 0.0f) + // 「自由标记」面板的底部固定区有三行: + // ①「标记透明度」滑块(画在上面); + // ②「自由标记点选取」切换按钮; + // ③「显示增量 | 显示枚举文本」两个开关(用户 2026-10-23 要求)。 + // 三者都是切换类按钮外观,与工具栏的切换按钮一致 + //(激活时浅强调色底 + 强调描边 + 强文本色)。 + const auto paintToggleButton = [this, &g](const gd::RectF& buttonRect, + const wchar_t* label, bool checked) { + if (buttonRect.Width <= 0.0f) { - const gd::RectF inner(buttonRect.X, buttonRect.Y + 2.0f * m_scale, - buttonRect.Width, buttonRect.Height - 4.0f * m_scale); - const bool hot = - (m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) && - UiPointInRect(inner, static_cast(m_mouseX), - static_cast(m_mouseY)); - const bool checked = freeMarker && tabLane.freeMarkerCursor; - const std::uint32_t background = - checked ? (hot ? MixColor(m_theme->accentSoft, m_theme->accent, 0.16) - : m_theme->accentSoft) - : (hot ? m_theme->hover : m_theme->cardBg); - UiFillRoundRect(g, inner, 4.0f * m_scale, background); - UiDrawRoundRect(g, inner, 4.0f * m_scale, - checked ? m_theme->accent : m_theme->border, 1.2f); - const std::wstring label = freeMarker ? std::wstring(kFreeMarkerCursorModeLabel) - : std::wstring(kBuildSignalLabel); - UiText(g, label, m_fonts->uiSmall, inner, - checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER); + return; } + const gd::RectF inner(buttonRect.X, buttonRect.Y + 2.0f * m_scale, + buttonRect.Width, buttonRect.Height - 4.0f * m_scale); + const bool hot = (m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) && + UiPointInRect(inner, static_cast(m_mouseX), + static_cast(m_mouseY)); + const std::uint32_t background = + checked ? (hot ? MixColor(m_theme->accentSoft, m_theme->accent, 0.16) + : m_theme->accentSoft) + : (hot ? m_theme->hover : m_theme->cardBg); + UiFillRoundRect(g, inner, 4.0f * m_scale, background); + UiDrawRoundRect(g, inner, 4.0f * m_scale, checked ? m_theme->accent : m_theme->border, + 1.2f); + // 窄按钮(两个开关各占半行)里文本可能超宽 ⇒ 省略,不允许画到按钮外。 + const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, label); + const std::wstring text = + (labelWidth <= inner.Width - 6.0f * m_scale) + ? std::wstring(label) + : UiEllipsize(label, m_fonts->uiSmall, inner.Width - 6.0f * m_scale); + UiText(g, text, m_fonts->uiSmall, inner, + checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER); + }; + if (hasButton) + { + paintToggleButton(layout.button, kBuildSignalLabel, false); + } + if (freeMarker) + { + paintToggleButton(layout.button, kFreeMarkerCursorModeLabel, + tabLane.freeMarkerCursor); + paintToggleButton(layout.showDelta, kFreeMarkerShowDeltaLabel, + tabLane.freeMarkerShowDelta); + paintToggleButton(layout.showEnum, kFreeMarkerShowEnumLabel, + tabLane.freeMarkerShowEnum); } } // if (!layout.collapsed) @@ -8647,8 +8746,15 @@ namespace jdi const LaneVisual& lane = m_lanes[static_cast(laneIndex)]; // ⚠️ 「选区放大」模式下**仍保留悬停高亮**,但该模式下拖动是框选、不是拖信号 // ⇒ 提示文案去掉“按住拖拽可移入其它网格”的尾缀,只提示信号名(用户 2026-10-17 要求)。 + // ⚠️ 同理(用户 2026-10-23 要求):该网格开启「自由标记点选取」时,左键专用于固定 + // 标记点、拖动也不再是拖信号 ⇒ 提示只显示信号名,不再提示可以拖拽。 + const std::vector& tabLanes = m_tab->Lanes(); + const bool pickMode = + laneIndex < static_cast(tabLanes.size()) && + tabLanes[static_cast(laneIndex)].freeMarker && + tabLanes[static_cast(laneIndex)].freeMarkerCursor; const std::wstring text = - (Tab().ZoomMode() == PlotZoomMode::SELECT) + (Tab().ZoomMode() == PlotZoomMode::SELECT || pickMode) ? resolved.data->name : (resolved.data->name + L" · 按住拖拽可移入其它网格"); diff --git a/Components/SignalPanel/Src/SignalPanelInternal.hpp b/Components/SignalPanel/Inc/SignalPanelInternal.hpp similarity index 100% rename from Components/SignalPanel/Src/SignalPanelInternal.hpp rename to Components/SignalPanel/Inc/SignalPanelInternal.hpp diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index 7d510ce..484f481 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -3019,6 +3019,8 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ } | 2026-10-22 | DeepSeek-V4.1-Flash(**本机**) | **「自由标记」面板表头 / 吸顶 / 行选中与悬停联动 + 固定点原位显示 + 探测容差与阶跃吸附(详见 §12 末尾「2026-10-22 第四轮」与 §11.2 第 213–220 条)**。① **列表头**:每个展开分组下方新增一行列标题(标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔),与数据行共用列边界;列宽还需容纳列标题;内容总高与面板高度计入表头行;全部无枚举文本时整列缺省。② **吸顶**:分组栏与表头**各自**按 `y = max(自然, min(吸顶, 本组下边界 − 自身高度))` 钉住 ⇒ 组末尾按「表头先走 → 条目 → 分组栏最后离场」;绘制顺序改为「条目 → 表头 → 吸顶分组栏」,被盖住的条目不绘制也不可命中。③ **行交互**:`Lane::freeMarkerSelected`(单击选中/再点取消,`PanelSlotKey` 混入)+ 独立的整行悬停状态(整行悬停不会把 ✕ 变成 danger 色),悬停时网格内对应固定点整体高亮(数据点圆环 + 胶囊强制不透明 + accent 描边)。④ **固定点恢复原位显示**:删除 2026-10-21 的纵向避让,允许互相遮挡。⑤ **探测判据重写**:改为“鼠标到画出来的折线的垂直距离 ≤ `kSignalHitTolerance`”(与曲线高亮同源,复用 `AcquirePolyline` 顶点 + 点到线段距离),命中后吸附到最近采样时刻 ⇒ 修复 `SCATTER_TEST.csv` 阶跃竖直跳变线无法探测/固定的问题;删除 `kFreeMarkerHoverTolerance`。⑥ **滚动文字串到标题栏**:MARK 面板内全部文字改走 `UiTextClipped(..., content)`(GDI 文本不认 GDI+ 裁剪)。验证:新增离屏 `.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`/`make debug` 零警告;`TargetCheck.exe` **855,552 字节**。 | +| 2026-10-23 | DeepSeek-V4.1-Flash(**本机**) | **目录规范化(Inc/Src)+「自由标记」常规模式回归普通鼠标 + 两个显示开关 + 序号列/列分隔线 + 行悬停压暗(详见 §12 末尾「2026-10-23 第五轮」与 §11.2 第 221–229 条)**。① **目录规范化**:四个 `*Internal.hpp` 从 `Src` 迁到 `Inc`(`DataModelInternal` / `ApplicationInternal` / `PlotViewInternal` / `SignalPanelInternal`),并在 `Makefile` 固化「`Inc` 只放头文件、`Src` 只放 `.cpp`」;因 include 全是扁平形式,无需改任何 include。② **常规模式 = 普通鼠标**:`ProbeFreeMarker` 在未开启「自由标记点选取」时直接早退(不预览/不画纵向光标/单击不固定),并补上 `if (!hitCurve) continue;`(否则纵向光标整列都在时 `probe.ref` 恒有效 ⇒ 任意位置都能固定);固定点仍恒吸附到真实采样时刻,但“已吸附”圆环只在稀疏判据成立时画。③ **两个显示开关**:「自由标记点选取」下方新增「显示增量 | 显示枚举文本」(默认关闭,半宽同行),只影响已固定点胶囊,预览胶囊恒为“值 = 枚举”。④ **面板六列**:新增恒存在的「序号」列(组内 1 起),枚举列存在时无枚举/对不上的点显示 `N/A`,列间画竖线;悬停某行时其它固定点(数据点 + 胶囊)封顶 50% 不透明度、移出恢复。⑤ **移除固定点 ✕ 的悬停浮动提示**(`PaintFreeMarkerTooltip` 实现与声明全部删除);开启选取模式的网格上信号悬停提示只显示信号名。⑥ **两处缓存键补齐**:`FreeMarkerContents` 混入枚举表指纹(旧键会让加上枚举表后仍显示 `N/A`)、`PanelSlotKey` 混入两个新按钮矩形与开关状态。验证:新增离屏 `.preflight/mark5check` **52/52**(含“预览胶囊像素差 = 0”“行悬停压暗 848px → 移出恢复 0”“选取模式气泡变窄 1023→830”);回归 `markcheck` 67/67、`mark4check` 40/40、`fixcheck` 37/37、`round3check` 31/31、`round4check` 34/34、`round5check` 5/5、`editcheck` 20/20、`convcheck` 18/18、`diag` 19/19,`check` 136/137(唯一失败项为该探针既有的不稳定项,`drift=15818` 与改动前逐位相同);`make release`/`make debug` 零警告;`TargetCheck.exe` **857,600 字节(837.5 KiB)**。 | + ## 11. 跨设备协作须知(起源设备视角) > **本文件是唯一权威记忆**。所有设备 / Agent 共用它,**不要再新建 `agent_mem_origin.md` 等旁支记忆文件**。 @@ -4022,6 +4024,39 @@ else if (panelOnly) { /* 只回送“本帧重绘过的区域并集” */ } 220. ⚠️⚠️ **面板内的文字一律用 `UiTextClipped(..., layout.content)`**:GDI `DrawTextW` **不认 GDI+ 裁剪**,滚动时“部分可见”的条目会把文字画到标题栏上(用户 2026-10-22 报告)。 这条同时适用于分组栏名称/计数、表头列标题与空内容提示。 +221. ⚠️⚠️ **目录约定:`Inc` 只放头文件、`Src` 只放 `.cpp`**(含 `*Internal.hpp` —— 它也是头文件)。 + 历史上 `*Internal.hpp` 放在 `Src` 也能编译,只是因为 `#include "..."` 会先找源文件所在目录; + 一旦文件被搬走就无声失效。所有 include 都是扁平形式,靠 `-I.../Inc` 解析 ⇒ **搬家不要改 include**。 + ⚠️ `Makefile` 里 `CXX_INCLUDES_PATHS` 前的注释块必须单独成行(写成同一行会把整个变量注释掉, + 症状是 `fatal error: CsvLoader.hpp: No such file or directory`)。 +222. ⚠️⚠️ **未开启「自由标记点选取」时鼠标就是普通鼠标**(用户 2026-10-23 要求): + `ProbeFreeMarker` 必须 `if (!lane.freeMarkerCursor) return probe;` 早退 ⇒ 不预览数据点、 + 不画网格私有纵向光标、`OnLButtonDown` 不登记固定意图、单击也不固定。 + ⚠️ 开启后纵向光标**整列都在**(`probe.valid` 恒为真),因此“能否固定”只看 `probe.ref.IsValid()`: + `ProbeFreeMarker` 里必须有 `if (!hitCurve) continue;`(否则 `ref` 一直有效, + 鼠标在全网格任意位置单击都能固定出一个离曲线很远的点)。 +223. ⚠️ **「显示增量」「显示枚举文本」两个开关只影响已固定点的胶囊文本** + (`Lane::freeMarkerShowDelta/freeMarkerShowEnum`,默认 `false`,`LaneCarry` 双向携带): + 跟随鼠标的预览胶囊恒为 `FreeMarkerValueText(value,false,false,0,true,enumText)`(值 = 枚举); + 面板统计列也不看这两个开关。开关行独占底部最后一行、左右各半宽,与主按钮间距 `kFreeMarkerButtonGap`, + `switchRow` 必须计入面板高度**和**内容区高度。 +224. ⚠️ **面板列序固定为 `序号 / 标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔`**(六列): + 序号列**恒存在**(`MarkerRow::serial` 组内 1 起);枚举列只在存在枚举表时出现,出现后 + **无枚举/对不上任何枚举项一律显示 `N/A`**;列与列之间必须画竖线分隔。 + 任何“列数”都散落在 `markerColumns`、`kFreeMarkerColumnLabels/Widths`、`Lane::freeMarkerColumnWidths` + 与各处 `column < N` 循环里 —— **改列数必须一次性全改**。 +225. ⚠️ **悬停面板某行时,网格内其它固定点的不透明度封顶 50%**(原本 ≤ 50% 的不变;目标行强制 100%), + 移出立即恢复;命中也必须按压暗后的 alpha 判定(否则“看不见还能点”)。 +226. ⚠️ **固定点 ✕ 的悬停浮动提示已删除**(`PaintFreeMarkerTooltip` 实现与声明均已移除,不要加回来); + 该网格开启「自由标记点选取」时,信号悬停提示只显示信号名(不再提示“可按住拖拽”)。 +227. ⚠️⚠️ **`FreeMarkerContents` 的内容缓存键必须混入枚举表指纹** + (`values/texts/rangeMin` 规模 + `texts.data()` 地址):只混信号指针与采样数量时, + 给信号加上枚举表后条目里仍然是 `N/A`(离屏探针实测复现)。 +228. ⚠️ **`PanelSlotKey` 必须混入 `layout.showDelta/showEnum` 与两个开关布尔值**: + 否则点击开关 / 布局变化后会把旧像素贴回来。 +229. ⚠️ **固定点必须落在真实采样时刻上**:`ProbeFreeMarker` 命中曲线时**始终**吸附到最近采样点 + (`snapToNearestSample`);只有“已吸附”圆环(`probe.snapped`)仍只在稀疏判据 `sparse` 成立时才显示 + (密采样时吸附量 < 1 像素,圆环常驻会像是画错了)。 ### 11.3 离屏验证方法学(本机偏好,强烈建议沿用) @@ -7682,3 +7717,80 @@ DG3 data=0 cycle=0 (空组,一个字符串通道) - ⚠️ 探针坑:两条信号同网格时阶跃幅度更小(阈值取 40 逻辑像素); 吸顶“离场”必须在**两组**内容上验证(单组且内容超高时分组栏数学上会一直钉住); 滚动扫描步长按设备像素(`step*2`),再乘 `m_scale` 会扫不到组末尾。 + +## 本轮(2026-10-23 第五轮,本机):「自由标记」常规模式回归普通鼠标 + 两个显示开关 + 序号列/列分隔线 + 行悬停压暗 + 目录规范化 + +### A. 目录规范化(§11.2 第 221 条) + +- `Inc` 只放**头文件**(含 `*Internal.hpp`),`Src` 只放 `.cpp`。四个 `*Internal.hpp` 已 `git mv` 到 `Inc`: + `Utilities/Inc/DataModelInternal.hpp`、`Components/Application/Inc/ApplicationInternal.hpp`、 + `Components/PlotView/Inc/PlotViewInternal.hpp`、`Components/SignalPanel/Inc/SignalPanelInternal.hpp`。 +- 历史原因:它们原来放在 `Src` 也能编译,因为 `#include "XxxInternal.hpp"` 会先找**源文件所在目录** —— + 一旦文件被搬走就无声失效。所有 include 都是扁平形式,靠 `-I.../Inc` 解析,因此搬家**无需改任何 include**。 +- ⚠️ `Makefile` 里 `CXX_INCLUDES_PATHS` 上方新增的注释块**不能和该变量写在同一行**(写在同一行会把整个变量注释掉, + 症状是 `fatal error: CsvLoader.hpp: No such file or directory`)。本轮踩过一次,已修。 +- ⚠️ `Makefile` 是所有 `.o` 的前置依赖 ⇒ 改注释也会触发全量重编(`-MP` 已加 phony 目标,`.d` 不会把构建卡死)。 + +### B. 常规模式 = 普通鼠标(§11.2 第 222 条) + +- `ProbeFreeMarker` 开头新增早退:`if (!lane.freeMarkerCursor) return probe;`(未开启「自由标记点选取」时 + 不预览数据点、不画纵向光标、`OnLButtonUp` 也不会固定任何点)。`OnLButtonDown` 的固定意图登记同样加上该前提。 +- 面板空提示文案按状态区分:未开启 ⇒「开启「自由标记点选取」后单击网格可固定数据点」;已开启 ⇒「在网格内单击以固定数据点」。 +- ⚠️ 只有**贴着曲线**(点到折线距离 ≤ `kSignalHitTolerance`)的信号才是可标注目标:`ProbeFreeMarker` 里 + `if (!hitCurve) continue;`。少了这一句,纵向光标“整列都在”时 `probe.ref` 会一直有效 ⇒ 任意位置单击都能固定。 + +### C. 两个显示开关(§11.2 第 223 条) + +- 按钮文案「光标选取模式」→「自由标记点选取」(功能不变,`kFreeMarkerCursorModeLabel`)。 + 其下方新增一行两个半宽切换按钮「显示增量 | 显示枚举文本」,`Lane::freeMarkerShowDelta/freeMarkerShowEnum` + (默认 `false`,`LaneCarry` 双向携带,`SplitLane` 复制到 primary 分支)。 +- **只影响已固定点**的胶囊文本;跟随鼠标的预览胶囊恒为 `FreeMarkerValueText(value,false,false,0,true,enumText)` + (即“值 = 枚举”,不受开关影响)。面板统计列不看这两个开关。 +- 布局:`EdgePanelLayout` 新增 `showDelta/showEnum`;底部固定区自上而下 = 透明度滑块 → 主按钮 → 开关行, + 行间距 `kFreeMarkerButtonGap = 4`;`switchRow` 计入面板高度与内容区高度 + (`height = title + pad + view + pad + footer + pad`,`footer = sliderRow + buttonH + gap + switchRow`)。 + +### D. 六列 / 序号 / N/A / 列分隔线(§11.2 第 224 条) + +- 列序:`序号 / 标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔`(`kFreeMarkerColumnLabels[6]`、 + `kFreeMarkerColumnWidths[6] = {34,74,80,60,72,78}`、`Lane::freeMarkerColumnWidths` 由 5 → 6)。 +- `MarkerRow::serial`(组内 1 起);序号列**恒存在**,用 `m_theme->textMuted` 绘制。 +- 枚举列:只有当**某个有固定点的可见信号**带枚举表时才出现;出现时其它无枚举的点、以及数值对不上任何枚举项的点 + 一律显示 `N/A`(`kFreeMarkerNotAvailable`)。 +- 列与列之间画竖线:`cell.left + cell.width + gap*0.5`,颜色 `WithAlpha(borderStrong, 0x6C)`、1px, + 数据行与表头都画。 + +### E. 行悬停压暗 / ✕ 提示移除(§11.2 第 225–226 条) + +- 悬停面板某行 ⇒ 该网格其它固定点(数据点 + 胶囊)不透明度封顶 **50%**(原本就 ≤ 50% 的不变), + 目标行强制 100%(并保留 accent 圆环 + 2px 描边);移出立即恢复。 + ⚠️ 命中登记(`m_freeMarkerHits`)也要用压暗后的 alpha 判定,否则会出现“看不见却能点”。 +- 删除固定点 ✕ 的悬停浮动提示:`PaintFreeMarkerTooltip` 的**实现与声明都已删除**,不要加回来。 +- 信号悬停提示(`PaintSignalInteraction`):该网格开启「自由标记点选取」时文案**只显示信号名** + (与「选区放大」模式同理:左键已专用于固定标记点,拖动也不再是拖信号)。 + +### F. 缓存键两处补齐(§11.2 第 227–228 条) + +- `FreeMarkerContents` 的内容缓存键新增**枚举表的规模与 `texts.data()` 地址**: + 旧键只混信号指针与采样数量 ⇒ 给信号加枚举表之后条目里仍然是 `N/A`(离屏探针实测复现)。 +- `PanelSlotKey` 新增 `mixRect(layout.showDelta/showEnum)` 与两个开关布尔值: + 否则点开关 / 布局变化后会贴回旧像素。 + +### G. 验证(全离屏,未占用鼠标/键盘/屏幕) + +- 新增 `.preflight/mark5check` **52/52**:[A] 常规模式无探测/不固定;[B] 三按钮几何、 + 两个开关默认关闭且点击可双向切换、点击开关不穿透、不启动拖动;[C] 六列 + 序号 1 起 + 无枚举时缺省枚举列; + [D] 枚举命中 LOW/HIGH、对不上 ⇒ `N/A`;[E] 开关只改已固定点胶囊(默认 `1` / `+增量` `1(+2)` / + `+枚举` `1 = HIGH` / 全开 `1(+2) = HIGH`),**预览胶囊区域像素差 = 0**;[F] 每个列边界都检测到竖线; + [G] 行悬停时另一固定点胶囊像素变化 848px,移出恢复为 0;[H] 选取模式下悬停气泡变窄(1023 → 830,拖拽提示已去掉)。 +- 回归(探针已按新语义更新,全部重新链接当天代码):`markcheck` **67/67**(A 段改为“普通鼠标不探测/不固定/拖动仍平移”, + D 段改为“按钮双向切换”,C 段左边缘断言改为序号列)、`fixcheck` **37/37**(列宽断言扩到 6 列含序号)、 + `mark4check` **40/40**(E 段先开启选取模式,且“远离曲线”改判 `probe.ref` 无效)、 + `round3check` 31/31、`round4check` 34/34、`round5check` 5/5、`editcheck` 20/20、`convcheck` 18/18、 + `diag` 19/19;`check` **136/137**(唯一失败项 `the plot is not corrupted by a panel-animated frame` 的 + `drift=15818` 与**本轮改动前逐位相同**,是该探针既有的不稳定项)。 +- `make release` / `make debug` **零警告**;`TargetCheck.exe` **857,600 字节(837.5 KiB)**。 +- ⚠️ 探针坑:`PlotView::FreeMarkerProbe::valid` 在选取模式下**恒为真**(纵向光标整列都在), + 判断“是否命中曲线/是否有可固定的点”必须看 `probe.ref.IsValid()`; + 在固定点上做像素 A/B 时,固定点若落在悬浮面板下方(绘图区右侧)像素永远不会变 ⇒ 固定点的时间要放在左侧。 + diff --git a/Makefile b/Makefile index 34976f3..6d49780 100644 --- a/Makefile +++ b/Makefile @@ -80,6 +80,10 @@ _UNICODE \ UNICODE \ # C++ 头文件目录 +# ⚠️ 目录约定:**所有头文件(.hpp)只放在 `Inc`**,`Src` 只放实现(.cpp)。 +# `*Internal.hpp`(组件内部共享的常量/工具,如 `PlotViewInternal.hpp`)同样属于头文件, +# 历史上曾被放在 `Src` 下(那里也能编译,因为编译器会先找 `#include` 所在目录), +# 但那是靠“同目录优先”隐式生效的 —— 一旦文件被搬走就会无声失效。现已全部移入 `Inc`。 CXX_INCLUDES_PATHS = \ Utilities/Inc \ Resources/Inc \ diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 798896b..77ae8ff 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -939,16 +939,32 @@ namespace jdi bool freeMarker = false; /** - * @brief 「自由标记」的光标模式(浮动面板最下方的切换按钮)。 + * @brief 「自由标记」的**光标模式**(浮动面板底部「自由标记点选取」切换按钮)。 * - * `false`(默认):鼠标在信号曲线上(判据同 `PlotView::ProbeFreeMarker`: - * 到画出来的折线的垂直距离 ≤ `kSignalHitTolerance`)时才显示取值预览; - * 鼠标下方没有曲线时按下就是普通的平移/框选,不会误固定数据点。 - * `true`:与最初实现的语义相同 —— 网格内始终跟随鼠标画一条**仅限本网格**的纵向光标 - *(上下各一个三角把手、不显示时间读数),光标穿过信号时标注离鼠标最近的数据点。 + * `false`(默认):鼠标就是**普通鼠标** —— 不显示任何取值预览、不画纵向光标, + * 按下/拖动完全走常规语义(平移 / 框选 / 拖信号),**不会**固定数据点。 + * `true`:进入「自由标记点选取」—— 网格内始终跟随鼠标画一条**仅限本网格**的纵向光标 + *(上下各一个三角把手、不显示时间读数),鼠标接触到信号曲线时标注离鼠标最近的数据点, + * 左键单击把该数据点固定下来(此时左键**不再**用于平移/框选/拖信号)。 */ bool freeMarkerCursor = false; + /** + * @brief 固定点的数值显示是否附带**括号内增量**(浮动面板的「显示增量」开关)。 + * + * 默认 `false`:胶囊只显示当前值。⚠️ 只影响**已固定点**的显示, + * 不影响「自由标记点选取」模式下跟随鼠标的预览胶囊,也不影响面板统计列。 + */ + bool freeMarkerShowDelta = false; + + /** + * @brief 固定点的数值显示是否附带**枚举文本**(浮动面板的「显示枚举文本」开关)。 + * + * 默认 `false`:胶囊只显示当前值。⚠️ 只影响**已固定点**的显示, + * 不影响跟随鼠标的预览胶囊,也不影响面板统计列(那一列只看有没有枚举表)。 + */ + bool freeMarkerShowEnum = false; + /** * @brief 已固定的自由标记点(**未排序**,按加入顺序)。 * @@ -994,9 +1010,9 @@ namespace jdi /** * @brief 网格内固定数据点(纵向细线 / 数据点 / 取值胶囊 / 行尾 ✕)的显示不透明度。 * - * 0 = 完全透明(看不见),1 = 完全不透明(默认)。由浮动面板底部「光标选取模式」 - * 激活时出现的「标记透明度」滑动条驱动;只影响**本网格**的固定点显示内容, - * 不影响面板自身的像素与鼠标交互。 + * 0 = 完全透明(看不见),1 = 完全不透明(默认)。由浮动面板底部**总是显示**的 + * 「标记透明度」滑动条驱动(与「自由标记点选取」是否开启无关);只影响**本网格**的 + * 固定点显示内容,不影响面板自身的像素与鼠标交互。 */ float freeMarkerOpacity = 1.0f; @@ -1026,15 +1042,15 @@ namespace jdi int freeMarkerSelected = -1; /** - * @brief 「自由标记」面板五列的**实测内容宽度**(设备像素,0 = 还没量过)。 + * @brief 「自由标记」面板六列的**实测内容宽度**(设备像素,0 = 还没量过)。 * - * 用户 2026-10-21 明确要求:面板里的取值 / 枚举文本 / 增量 / 时刻 / 间隔 + * 用户 2026-10-21 明确要求:面板里的序号 / 取值 / 枚举文本 / 增量 / 时刻 / 间隔 * **不得出现省略号(…)**。因此列宽不能再用固定值:绘制该面板时顺便把每格 * 真实文本量一遍,取最大值存到这里;布局(`EdgePanelWidth` / `EdgePanelLayoutFor`) * 用它与缺省列宽 `kFreeMarkerColumnWidths` 的较大者,于是列宽自然贴合内容。 * ⚠️ 存的是**设备像素**(已经乘过 DPI 缩放),与字体直接对应。 */ - std::array freeMarkerColumnWidths{}; + std::array freeMarkerColumnWidths{}; /** 某信号分组在「自由标记」面板里是否被收起。 */ bool FreeMarkerGroupCollapsed(const SignalRef& ref) const diff --git a/Utilities/Src/DataModelInternal.hpp b/Utilities/Inc/DataModelInternal.hpp similarity index 98% rename from Utilities/Src/DataModelInternal.hpp rename to Utilities/Inc/DataModelInternal.hpp index 8cb1f9a..ba1dcf1 100644 --- a/Utilities/Src/DataModelInternal.hpp +++ b/Utilities/Inc/DataModelInternal.hpp @@ -137,6 +137,8 @@ namespace jdi float freeMarkerOpacity = 1.0f; float freeMarkerScroll = 0.0f; int freeMarkerSelected = -1; + bool freeMarkerShowDelta = false; + bool freeMarkerShowEnum = false; std::vector freeMarkerCollapsedGroups; }; @@ -182,6 +184,8 @@ namespace jdi carry.freeMarkerOpacity = lane.freeMarkerOpacity; carry.freeMarkerScroll = lane.freeMarkerScroll; carry.freeMarkerSelected = lane.freeMarkerSelected; + carry.freeMarkerShowDelta = lane.freeMarkerShowDelta; + carry.freeMarkerShowEnum = lane.freeMarkerShowEnum; carry.freeMarkerCollapsedGroups = lane.freeMarkerCollapsedGroups; for (std::size_t signalIndex = 0; signalIndex < lane.signals.size(); ++signalIndex) { @@ -248,6 +252,8 @@ namespace jdi lane.freeMarkerOpacity = carry.freeMarkerOpacity; lane.freeMarkerScroll = carry.freeMarkerScroll; lane.freeMarkerSelected = carry.freeMarkerSelected; + lane.freeMarkerShowDelta = carry.freeMarkerShowDelta; + lane.freeMarkerShowEnum = carry.freeMarkerShowEnum; lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups; }; diff --git a/Utilities/Src/DataModelTab.cpp b/Utilities/Src/DataModelTab.cpp index 844ef03..164212c 100644 --- a/Utilities/Src/DataModelTab.cpp +++ b/Utilities/Src/DataModelTab.cpp @@ -445,6 +445,8 @@ namespace jdi lane.meanFilterPanel = source.meanFilterPanel; lane.freeMarker = source.freeMarker; lane.freeMarkerCursor = source.freeMarkerCursor; + lane.freeMarkerShowDelta = source.freeMarkerShowDelta; + lane.freeMarkerShowEnum = source.freeMarkerShowEnum; lane.freeMarkerPanel = source.freeMarkerPanel; lane.freeMarkerOpacity = source.freeMarkerOpacity; // 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整 / 自由标记条目。