目录规范化(Inc/Src)+「自由标记」第六轮交互调整

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 字节
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-23 15:50:55 +08:00
1 parent 79eebc281e
commit affb9cc340
12 files changed
+561 -187

No files matched your search

+28 -12
View File
@@ -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<float, 5> freeMarkerColumnWidths{};
std::array<float, 6> freeMarkerColumnWidths{};
/** 某信号分组在「自由标记」面板里是否被收起。 */
bool FreeMarkerGroupCollapsed(const SignalRef& ref) const
+426
View File
@@ -0,0 +1,426 @@
#ifndef __DATAMODEL_INTERNAL_HPP__
#define __DATAMODEL_INTERNAL_HPP__
// DataModel 组件内部共享的常量与工具函数(私有头,仅本组件的 .cpp 使用)。
//
// 本组件的实现按功能拆成多个协作文件,它们共用这一份内部工具集。
//
// ⚠️ 为让多个翻译单元都能使用,原匿名命名空间改为**具名**命名空间 `dmdetail`,
// 并把其中每个函数与常量标为 `inline` —— 单一定义、单一副本,不会产生代码重复。
// 各实现文件在包含本头之后用 `using namespace dmdetail;` 引入这些工具,
// 因此函数体里原有的非限定调用一行都不用改。
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <cwctype>
#include <deque>
#include <limits>
#include <new>
#include <string>
#include <utility>
#include <vector>
#include "DataModel.hpp"
#include "Theme.hpp"
#include "Utf8Util.hpp"
namespace jdi
{
namespace dmdetail
{
/**
* @brief DataModel 内部工具集(常量与自由函数)。
*
* 全部符号都是 `inline` ⇒ 多个翻译单元共用同一个定义:既不重复符号,也不产生多份代码。
* 不要把它们移回匿名命名空间(那样每个 .cpp 都会各自复制一份)。
*/
inline const double kMissing = std::numeric_limits<double>::quiet_NaN(); /**< 缺失值占位。 */
inline const double kDefaultLineWidth = 1.6; /**< 默认线宽。 */
/**
* @brief 为网格范围补充留白,避免波形紧贴边界。
*
* ⚠️ 实际规则实现在 `DataModel.hpp` 的 `PadValueRange()`:它同时被
* 「选区 Y 轴自适应」使用,两处必须逐位一致(否则两种自适应会算出不同区间)。
*
* @param [IN, OUT] yMin 范围下限。
* @param [IN, OUT] yMax 范围上限。
*/
inline void PadRange(double& yMin, double& yMax)
{
PadValueRange(yMin, yMax);
}
/**
* @brief 清掉网格里**不属于本网格信号**的逐信号条目(均值滤波 / 中心轴 / 弱峰调整 / 自由标记)。
*
* 这些条目按信号保存,但只有“该信号就在本网格里”时才有意义。
* 网格重建(合并 → 拆分)时上一帧的条目是按网格整体携带的,若不过滤就会出现:
* 拆分出来的新网格里仍然画着**其它信号**的固定点(用户 2026-10-21 报告:
* 第二个网格上出现“野生的”自由标记点,而浮动面板里又没有任何条目)。
*
* @param [IN,OUT] lane 目标网格(`signals` 必须已经就位)。
*/
inline void PruneLaneSignalItems(Lane& lane)
{
const auto inLane = [&lane](const SignalRef& ref)
{
return std::find(lane.signals.begin(), lane.signals.end(), ref) != lane.signals.end();
};
const auto prune = [&inLane](auto& items)
{
items.erase(std::remove_if(items.begin(), items.end(),
[&inLane](const auto& item) { return !inLane(item.ref); }),
items.end());
};
prune(lane.meanFilterItems);
prune(lane.edgeCenterItems);
prune(lane.weakPeakItems);
prune(lane.freeMarkerItems);
lane.freeMarkerCollapsedGroups.erase(
std::remove_if(lane.freeMarkerCollapsedGroups.begin(), lane.freeMarkerCollapsedGroups.end(),
[&inLane](const SignalRef& ref) { return !inLane(ref); }),
lane.freeMarkerCollapsedGroups.end());
}
/**
* @brief 依据全部可见信号重新生成网格划分。
* @param [IN] visible 各信号的可见性(按 [分组][信号] 索引)。
* @param [IN] lanes 网格集合(原地更新)。
* @param [IN] mode 网格划分策略。
*/
inline void RebuildLanes(const std::vector<std::vector<bool>>& visible, std::vector<Lane>& lanes,
LaneMode mode)
{
// 随网格携带的“网格自身状态”(按信号继承):重建网格时不能丢。
struct LaneCarry
{
bool matched = false; /**< 是否找到该信号原本所在的网格(false = 新出现的信号)。 */
/**
* @brief 该信号是否是原网格的**首个信号**(= 这个网格的“实验性功能”持有者)。
*
* ⚠️ 实验性功能(边沿计数 / 中心轴 / 均值滤波 / 弱峰调整 / 自由标记)是**网格级**开关,
* 但一个网格可能与其它信号共用(合并网格)。若把“开着”的状态按信号逐个继承,
* 那么「合并网格 → 拆分网格」之后**每个**拆出来的网格都会带着这些功能
* (用户 2026-10-21 报告:第二个网格也自动启用了实验性功能、并出现了
* 其它信号的固定点镜像)。因此只有**首个信号**继承功能状态,其余信号视为默认(关闭)。
*/
bool owner = false;
bool fitYToSelection = false;
bool edgeCount = false;
double edgeValue = 0.0;
bool edgeValueValid = false;
LanePanelState edgePanel;
bool edgeCenter = false;
std::vector<Lane::EdgeCenterItem> edgeCenterItems;
LanePanelState edgeCenterPanel;
bool meanFilter = false;
std::vector<Lane::MeanFilterItem> meanFilterItems;
LanePanelState meanFilterPanel;
bool weakPeak = false;
std::vector<Lane::WeakPeakItem> weakPeakItems;
LanePanelState weakPeakPanel;
bool freeMarker = false;
bool freeMarkerCursor = false;
std::vector<Lane::FreeMarkerItem> freeMarkerItems;
LanePanelState freeMarkerPanel;
float freeMarkerOpacity = 1.0f;
float freeMarkerScroll = 0.0f;
int freeMarkerSelected = -1;
bool freeMarkerShowDelta = false;
bool freeMarkerShowEnum = false;
std::vector<SignalRef> freeMarkerCollapsedGroups;
};
// 先记下每个信号原来所在网格的网格自身状态:
// 勾选可见性会重建网格,若不携带,用户刚打开的开关会被悄悄清掉。
//
// ⚠️ 同时记下「重建之前是否**每个**网格都开着 `fitYToSelection`」:
// 那就是「全局 Y 轴自适应」的正式定义。新勾选信号产生的**新网格**必须继承它,
// 否则全局开关看着是开的、新网格却不按当前视界自适应(用户报告的缺陷)。
bool previousAllAutoFit = !lanes.empty();
for (const Lane& lane : lanes)
{
if (!lane.fitYToSelection)
{
previousAllAutoFit = false;
break;
}
}
std::vector<std::pair<SignalRef, LaneCarry>> previousCarry;
for (const Lane& lane : lanes)
{
LaneCarry carry;
carry.matched = true;
carry.fitYToSelection = lane.fitYToSelection;
carry.edgeCount = lane.edgeCount;
carry.edgeValue = lane.edgeValue;
carry.edgeValueValid = lane.edgeValueValid;
carry.edgePanel = lane.edgePanel;
carry.edgeCenter = lane.edgeCenter;
carry.edgeCenterItems = lane.edgeCenterItems;
carry.edgeCenterPanel = lane.edgeCenterPanel;
carry.meanFilter = lane.meanFilter;
carry.meanFilterItems = lane.meanFilterItems;
carry.meanFilterPanel = lane.meanFilterPanel;
carry.weakPeak = lane.weakPeak;
carry.weakPeakItems = lane.weakPeakItems;
carry.weakPeakPanel = lane.weakPeakPanel;
carry.freeMarker = lane.freeMarker;
carry.freeMarkerCursor = lane.freeMarkerCursor;
carry.freeMarkerItems = lane.freeMarkerItems;
carry.freeMarkerPanel = lane.freeMarkerPanel;
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)
{
LaneCarry entry = carry;
// 只有首个信号是这些网格级开关的持有者(见 `LaneCarry::owner` 说明)。
entry.owner = (signalIndex == 0);
previousCarry.emplace_back(lane.signals[signalIndex], entry);
}
}
const auto inheritedCarry = [&previousCarry](const SignalRef& ref)
{
for (const std::pair<SignalRef, LaneCarry>& entry : previousCarry)
{
if (entry.first == ref)
{
return entry.second;
}
}
return LaneCarry();
};
const auto applyCarry = [&inheritedCarry, previousAllAutoFit](Lane& lane, const SignalRef& ref)
{
const LaneCarry carry = inheritedCarry(ref);
// 新出现的信号(之前没有网格):继承「全局 Y 轴自适应」是否开启。
lane.fitYToSelection = carry.matched ? carry.fitYToSelection : previousAllAutoFit;
// ⚠️ 实验性功能的状态**只由原网格的首个信号继承**(见 `LaneCarry::owner`):
// 否则「合并网格 → 拆分网格」会把一个网格的功能复制到全部拆分结果上。
if (!carry.matched || !carry.owner)
{
return; // 其它信号:保持 `Lane` 的默认值(全部功能关闭、条目为空)。
}
lane.edgeCount = carry.edgeCount;
lane.edgeValue = carry.edgeValue;
lane.edgeValueValid = carry.edgeValueValid;
lane.edgePanel.open = carry.edgePanel.open;
lane.edgePanel.collapsed = carry.edgePanel.collapsed;
lane.edgePanel.x = carry.edgePanel.x;
lane.edgePanel.y = carry.edgePanel.y;
lane.edgeCenter = carry.edgeCenter;
lane.edgeCenterItems = carry.edgeCenterItems;
lane.edgeCenterPanel.open = carry.edgeCenterPanel.open;
lane.edgeCenterPanel.collapsed = carry.edgeCenterPanel.collapsed;
lane.edgeCenterPanel.x = carry.edgeCenterPanel.x;
lane.edgeCenterPanel.y = carry.edgeCenterPanel.y;
lane.meanFilter = carry.meanFilter;
lane.meanFilterItems = carry.meanFilterItems;
lane.meanFilterPanel.open = carry.meanFilterPanel.open;
lane.meanFilterPanel.collapsed = carry.meanFilterPanel.collapsed;
lane.meanFilterPanel.x = carry.meanFilterPanel.x;
lane.meanFilterPanel.y = carry.meanFilterPanel.y;
lane.weakPeak = carry.weakPeak;
lane.weakPeakItems = carry.weakPeakItems;
lane.weakPeakPanel.open = carry.weakPeakPanel.open;
lane.weakPeakPanel.collapsed = carry.weakPeakPanel.collapsed;
lane.weakPeakPanel.x = carry.weakPeakPanel.x;
lane.weakPeakPanel.y = carry.weakPeakPanel.y;
lane.freeMarker = carry.freeMarker;
lane.freeMarkerCursor = carry.freeMarkerCursor;
lane.freeMarkerItems = carry.freeMarkerItems;
lane.freeMarkerPanel.open = carry.freeMarkerPanel.open;
lane.freeMarkerPanel.collapsed = carry.freeMarkerPanel.collapsed;
lane.freeMarkerPanel.x = carry.freeMarkerPanel.x;
lane.freeMarkerPanel.y = carry.freeMarkerPanel.y;
lane.freeMarkerOpacity = carry.freeMarkerOpacity;
lane.freeMarkerScroll = carry.freeMarkerScroll;
lane.freeMarkerSelected = carry.freeMarkerSelected;
lane.freeMarkerShowDelta = carry.freeMarkerShowDelta;
lane.freeMarkerShowEnum = carry.freeMarkerShowEnum;
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
};
// 收集全部可见信号引用。
std::vector<SignalRef> visibleRefs;
for (std::size_t groupIndex = 0; groupIndex < visible.size(); ++groupIndex)
{
for (std::size_t signalIndex = 0; signalIndex < visible[groupIndex].size(); ++signalIndex)
{
if (visible[groupIndex][signalIndex])
{
SignalRef ref;
ref.groupIndex = static_cast<int>(groupIndex);
ref.signalIndex = static_cast<int>(signalIndex);
visibleRefs.push_back(ref);
}
}
}
// 判断某引用对应的信号是否可见(越界视为不可见)。
const auto isVisibleRef = [&visible](const SignalRef& ref)
{
if (ref.groupIndex < 0 || static_cast<std::size_t>(ref.groupIndex) >= visible.size())
{
return false;
}
const std::vector<bool>& fileVisible = visible[static_cast<std::size_t>(ref.groupIndex)];
if (ref.signalIndex < 0 || static_cast<std::size_t>(ref.signalIndex) >= fileVisible.size())
{
return false;
}
return fileVisible[static_cast<std::size_t>(ref.signalIndex)];
};
// 某信号当前所属的网格下标(-1 表示尚未分配网格)。
const auto laneIndexOf = [&lanes](const SignalRef& ref)
{
for (std::size_t index = 0; index < lanes.size(); ++index)
{
const std::vector<SignalRef>& refs = lanes[index].signals;
if (std::find(refs.begin(), refs.end(), ref) != refs.end())
{
return static_cast<int>(index);
}
}
return -1;
};
if (mode == LaneMode::PER_SIGNAL)
{
// 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。
// **顺序按"原有网格位置"优先**:已经分配过网格的信号保持其相对次序,
// 新勾选(此前没有网格)的信号统一追加到最下方。
// 若直接按 visibleRefs(文件/列序)重建,新勾选的信号会按其列序
// 插到中间,位置看起来"不确定"。
std::vector<SignalRef> ordered;
ordered.reserve(visibleRefs.size());
for (const SignalRef& ref : visibleRefs)
{
if (laneIndexOf(ref) >= 0)
{
ordered.push_back(ref);
}
}
std::stable_sort(ordered.begin(), ordered.end(),
[&laneIndexOf](const SignalRef& a, const SignalRef& b)
{ return laneIndexOf(a) < laneIndexOf(b); });
for (const SignalRef& ref : visibleRefs)
{
if (laneIndexOf(ref) < 0)
{
ordered.push_back(ref);
}
}
lanes.clear();
lanes.reserve(ordered.size());
for (const SignalRef& ref : ordered)
{
Lane lane;
lane.autoRange = true;
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
}
else if (mode == LaneMode::MERGED)
{
// 全部可见信号合并到同一网格;无可见信号时不保留空网格。
bool previousAutoRange = true;
double previousYMin = -1.0;
double previousYMax = 1.0;
if (lanes.size() == 1)
{
previousAutoRange = lanes[0].autoRange;
previousYMin = lanes[0].yMin;
previousYMax = lanes[0].yMax;
}
lanes.clear();
if (!visibleRefs.empty())
{
Lane lane;
lane.autoRange = previousAutoRange;
lane.yMin = previousYMin;
lane.yMax = previousYMax;
applyCarry(lane, visibleRefs.front());
lane.signals = visibleRefs;
lanes.push_back(std::move(lane));
}
}
else
{
// 自定义划分:仅移除不可见信号与由此产生的空网格,保持用户排布。
for (Lane& lane : lanes)
{
std::vector<SignalRef>& refs = lane.signals;
refs.erase(std::remove_if(refs.begin(), refs.end(),
[&isVisibleRef](const SignalRef& ref)
{ return !isVisibleRef(ref); }),
refs.end());
}
lanes.erase(std::remove_if(lanes.begin(), lanes.end(),
[](const Lane& lane) { return lane.signals.empty(); }),
lanes.end());
// 未分配网格的可见信号各自独占一个新网格。
// 注意:不能把它们塞进已有的第一个网格——那会导致“拖拽合并后新勾选的信号
// 被悄悄并入合并网格”,用户看不到新增的独立泳道。
for (const SignalRef& ref : visibleRefs)
{
bool placed = false;
for (const Lane& lane : lanes)
{
if (std::find(lane.signals.begin(), lane.signals.end(), ref) != lane.signals.end())
{
placed = true;
break;
}
}
if (placed)
{
continue;
}
Lane lane;
lane.autoRange = true;
applyCarry(lane, ref);
lane.signals.push_back(ref);
lanes.push_back(std::move(lane));
}
}
// 清掉“不属于本网格信号”的逐信号条目(合并网格时它们会被整块携带过来)。
// 统一重命名,保证编号连续。
for (std::size_t index = 0; index < lanes.size(); ++index)
{
PruneLaneSignalItems(lanes[index]);
lanes[index].name = L"网格 " + std::to_wstring(index + 1);
}
}
} // namespace dmdetail
using namespace dmdetail;
} // namespace jdi
#endif // __DATAMODEL_INTERNAL_HPP__