按钮与面板: - 「系数」按钮改名为「系数/偏移」;点击展开的二级面板在原「系数调整」输入框下方新增「偏移调整」: 与「系数调整」同一套绘制(标题 + 标题栏右侧的还原图标),下面两行分别是「Y轴」「时间轴」, 每行左侧说明文字、右侧内联输入框(整行数值框)。 - 「Y轴」默认 0.00,非编辑态**始终带正负号**(+0.00 / -12.50),小数不足两位时补两位、更高精度保留更多位。 - 「时间轴」默认 0.00,非编辑态显示与双光标差值同一套复合单位文本(`Δt:500ms`、`Δt:1h 1min 2s`); 进入编辑态时显示以**秒**为单位的数值,并在输入框最右侧提示单位 `s`。 - 「偏移调整」的还原图标把两个偏移恢复为默认值 0;「系数调整」的还原图标仍只重置系数。 - 面板/输入框的编辑初值与显示文本分离(`MenuItem::editValue` / `editSuffix`), 非编辑态(`Δt:…`)与编辑态(`3662`)不一致时也能正确回显。 语义与结算顺序: - 结算顺序变为 **系数 → 绝对值 → 镜像 → 偏移**(前三步不变)。 - Y 偏移:给绘制值加常数(波形上下移动);时间偏移:采样绘制在 `t + offsetTime` 处 (例如 t=1s、偏移 -0.5s → 画在 0.5s)。 - ⚠️ Y 偏移以**未镜像空间**为准:垂直镜像开关变化时同步取反偏移量, 使绘制等价于「先加上偏移再镜像」(开关来回切换可逆)。 - 绘制链路全部跟随偏移:`BuildPolyline`(逐点描边 / 像素列包络 / 跨列边界插值 / 粗粒度分组层, 保序的 Y 偏移直接加在分组极值上)、派生预览曲线、光标取值读数(含 Δt)、 可见区间 Y 轴自适应、整段自适应的区间映射;折线缓存键、场景指纹、样式快照与图层指纹都纳入两个偏移。 派生信号(实验性功能「构建为新信号」): - 修复「预览是调整后的波形、构建出来却按原始状态绘制」:新信号**继承源信号的调整变换** (系数/绝对值/两个镜像/两个偏移)——网格预览绘制的是 `变换(滤波结果)`, 而构建出的信号只存了滤波结果,因此必须把变换一并继承; 时间轴偏移**只能**靠继承(派生信号与源信号共用同一文件时间轴,无法把时间平移烘进数据)。 离屏验证(不干扰用户): - ⚠️ harness 现在在每次注入鼠标消息后**立即释放本窗口的鼠标捕获** (宿主 WM_LBUTTONDOWN 会 SetCapture,隐藏窗口同样会把用户的真实鼠标输入吸走); 后续消息本来就是直接投递,无需捕获。 - 新增 [AP]:按钮标签不裁切、面板 5 行顺序与「偏移调整」位于系数输入框下方、 Y轴/时间轴行的标签与输入框同行且默认值/单位提示正确、时间轴编辑态显示秒值并在最右侧提示 `s`、 Y 偏移提交/显示正负号/波形整体平移量、时间偏移提交/Δt 复合单位(含 3662 → Δt:1h 1min 2s)/ 编辑态为秒、**按"偏移若干个采样点"构造的精确几何验证**(采样被画到移位后的像素位置、且不再画在原处)、 读数跟随、两个还原图标、垂直镜像对 Y 偏移取反(可逆)、 以及派生信号继承变换;[AL] 补充「系数→绝对值→镜像→偏移」的顺序与区间映射(含 Y 偏移)。 - 合计 645/645 通过;make clean && make all 零警告。
1118 lines
45 KiB
C++
1118 lines
45 KiB
C++
#ifndef __DATA_MODEL_HPP__
|
||
#define __DATA_MODEL_HPP__
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "CsvTable.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
/**
|
||
* @brief 波形绘制方式。
|
||
*/
|
||
enum class DrawMode : int
|
||
{
|
||
LINE = 0, /**< 相邻采样点之间以插值直线连接。 */
|
||
SCATTER = 1, /**< 仅绘制采样点散点。 */
|
||
LINE_SCATTER = 2 /**< 同时绘制插值直线与采样点散点。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 绘图区拖拽缩放模式(由工具栏按钮切换)。
|
||
*/
|
||
enum class PlotZoomMode : int
|
||
{
|
||
NONE = 0, /**< 常规模式:拖拽平移时间轴,光标为箭头。 */
|
||
SELECT = 1 /**< 选区放大:横向拖拽放大时间轴,纵向拖拽放大当前网格的幅值范围。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 时间光标模式(由工具栏「光标」按钮三态循环切换)。
|
||
*/
|
||
enum class PlotCursorMode : int
|
||
{
|
||
NONE = 0, /**< 不显示光标。 */
|
||
SINGLE = 1, /**< 单光标。 */
|
||
DOUBLE = 2 /**< 双光标(第二条用于与第一条对比取值)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 将绘制方式转换为中文显示名称。
|
||
* @param [IN] mode 绘制方式。
|
||
* @return 显示名称。
|
||
*/
|
||
const wchar_t* DrawModeName(DrawMode mode);
|
||
|
||
/**
|
||
* @brief 网格划分策略:决定信号可见性与网格数量的联动方式。
|
||
*/
|
||
enum class LaneMode : int
|
||
{
|
||
PER_SIGNAL = 0, /**< 每条可见信号独占一个网格(随可见性自动增删)。 */
|
||
MERGED = 1, /**< 全部可见信号叠加在同一网格。 */
|
||
CUSTOM = 2 /**< 由用户手动指定归属;可见性变化时仅清理空网格。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 全局信号引用:文件下标 + 文件内信号下标。
|
||
*/
|
||
struct SignalRef
|
||
{
|
||
int fileIndex = -1; /**< 所属文件下标。 */
|
||
int signalIndex = -1; /**< 文件内信号下标。 */
|
||
|
||
/**
|
||
* @brief 判断引用是否指向有效位置。
|
||
* @return 两个下标均非负返回 true。
|
||
*/
|
||
bool IsValid() const { return fileIndex >= 0 && signalIndex >= 0; }
|
||
|
||
/**
|
||
* @brief 相等比较。
|
||
* @param [IN] other 另一引用。
|
||
* @return 指向同一信号返回 true。
|
||
*/
|
||
bool operator==(const SignalRef& other) const
|
||
{
|
||
return fileIndex == other.fileIndex && signalIndex == other.signalIndex;
|
||
}
|
||
|
||
/**
|
||
* @brief 不等比较。
|
||
* @param [IN] other 另一引用。
|
||
* @return 指向不同信号返回 true。
|
||
*/
|
||
bool operator!=(const SignalRef& other) const { return !(*this == other); }
|
||
};
|
||
|
||
/**
|
||
* @brief 单个信号的显示外观(颜色/透明度/线宽/绘制方式/数值变换)。
|
||
*
|
||
* 外观属于信号自身而非某个标签页——同一信号在所有标签页中必须显示效果一致,
|
||
* 因此在任一视图里修改颜色/线宽/透明度/样式,其它视图会自动同步。
|
||
* 信号是否可见则属于各标签页自己的配置(见 `TabConfig`)。
|
||
*/
|
||
struct SignalStyle
|
||
{
|
||
std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */
|
||
std::uint32_t customColor = 0xFF6C71C4; /**< 自定义颜色记忆值(切换预设色后仍保留)。 */
|
||
float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */
|
||
float lineWidth = 1.0f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */
|
||
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
|
||
|
||
/**
|
||
* 绘制前的变换(依次作用,见 `ApplyDrawTransform`):
|
||
* 1. `gain` 系数:采样值乘以该系数(默认 1,即不改变);
|
||
* 2. `absolute` 绝对值:|value|;
|
||
* 3. 镜像:`mirrorVertical` 按 Y = 0 上下镜像(value → −value),
|
||
* `mirrorHorizontal` 为**数据反序**(把采样序列头尾对调后绘制,即水平翻转);
|
||
* 4. 偏移:`offsetY` 给绘制值加上一个常数(波形整体上下移动),
|
||
* `offsetTime` 把采样沿时间轴平移(绘制在 `t + offsetTime` 处)。
|
||
*
|
||
* ⚠️ 结算顺序固定为「系数 → 绝对值 → 镜像 → 偏移」(用户明确要求)。
|
||
* 其中镜像的两个方向互相正交(垂直改**值**、水平改**取值下标**),偏移同理
|
||
*(`offsetY` 改值、`offsetTime` 改**绘制时刻**)。
|
||
* ⚠️ `offsetY` 以**未镜像空间**为准:启用/取消垂直镜像时会同步取反(见
|
||
* `SignalStyle::offsetY` 的使用处),使绘制等价于「先加上偏移再镜像」。
|
||
*/
|
||
float gain = 1.0f; /**< 系数,默认 1.0(不改变)。 */
|
||
bool absolute = false; /**< 是否对绘制值取绝对值,默认关闭。 */
|
||
bool mirrorVertical = false; /**< 是否按 Y=0 上下镜像(值取反),默认关闭。 */
|
||
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序绘制),默认关闭。 */
|
||
float offsetY = 0.0f; /**< Y 轴偏移(叠加到绘制值上),默认 0。 */
|
||
double offsetTime = 0.0; /**< 时间轴偏移(秒,绘制在 t + offsetTime 处),默认 0。 */
|
||
|
||
/** @brief 镜像功能的任一分量是否启用(决定「镜像」按钮的激活态)。 */
|
||
bool Mirrored() const { return mirrorVertical || mirrorHorizontal; }
|
||
|
||
/**
|
||
* @brief 是否存在任何**偏移**(决定「系数/偏移」按钮的激活态之一)。
|
||
* @return Y 或时间偏移不为 0 返回 true。
|
||
*/
|
||
bool Offset() const
|
||
{
|
||
return std::fabs(offsetY) > 1.0e-9f || std::fabs(offsetTime) > 1.0e-15;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @brief 把原始采样值变换为**绘制用值**。
|
||
*
|
||
* 顺序固定为「系数 → 绝对值 → 垂直镜像 → Y 偏移」,例如系数 2、原始值 1.5 →
|
||
* 2×1.5 = 3;取绝对值仍为 3;垂直镜像则 −3;Y 偏移 +1 得 −2。
|
||
* NaN 经乘/取绝对值/取反/加偏移后仍为 NaN(保持断线语义)。
|
||
*
|
||
* ⚠️ 水平镜像与时间偏移**不在这里处理**:它们分别改变“取值下标”与“绘制时刻”,
|
||
* 必须在遍历采样时处理(见 `SourceIndexFor` 与 `offsetTime` 的使用处)。
|
||
*
|
||
* @param [IN] style 信号外观。
|
||
* @param [IN] value 原始采样值。
|
||
* @return 绘制用值。
|
||
*/
|
||
inline double ApplyDrawTransform(const SignalStyle& style, double value)
|
||
{
|
||
if (style.gain != 1.0f)
|
||
{
|
||
value *= static_cast<double>(style.gain);
|
||
}
|
||
if (style.absolute)
|
||
{
|
||
value = (value < 0.0) ? -value : value;
|
||
}
|
||
if (style.mirrorVertical)
|
||
{
|
||
value = -value;
|
||
}
|
||
if (style.offsetY != 0.0f)
|
||
{
|
||
value += static_cast<double>(style.offsetY);
|
||
}
|
||
return value;
|
||
}
|
||
|
||
/**
|
||
* @brief 把遍历下标映射为**源采样下标**(处理水平镜像的"数据反序")。
|
||
*
|
||
* 水平镜像 = 头尾对调:遍历下标 `index` 取源下标 `count - 1 - index`。
|
||
* 关闭时原样返回,因此该函数在有/无镜像时都是 O(1)、无分支代价可忽略。
|
||
*
|
||
* @param [IN] style 信号外观。
|
||
* @param [IN] count 采样点数。
|
||
* @param [IN] index 遍历下标。
|
||
* @return 源采样下标。
|
||
*/
|
||
inline std::size_t SourceIndexFor(const SignalStyle& style, std::size_t count, std::size_t index)
|
||
{
|
||
if (!style.mirrorHorizontal || count == 0)
|
||
{
|
||
return index;
|
||
}
|
||
return (count - 1) - index;
|
||
}
|
||
|
||
/**
|
||
* @brief 判断数值变换是否**保持大小顺序**(正系数、不取绝对值、不垂直镜像)。
|
||
*
|
||
* 只有保序时,粗粒度包络层(每组的 min/max)才能直接按系数缩放后继续使用;
|
||
* 取绝对值 / 垂直镜像会把最小值与最大值互换,此时必须回退到逐采样点变换。
|
||
*
|
||
* @param [IN] style 信号外观。
|
||
* @return 变换单调递增返回 true。
|
||
*/
|
||
inline bool DrawTransformOrderPreserving(const SignalStyle& style)
|
||
{
|
||
return (style.gain > 0.0f) && !style.absolute && !style.mirrorVertical;
|
||
}
|
||
|
||
/**
|
||
* @brief 把**数值区间**按绘制变换映射为变换后的区间(用于整段 Y 轴自适应)。
|
||
*
|
||
* 各步作用在区间端点上是精确的(系数为线性、绝对值为 |x|、镜像与偏移为线性),
|
||
* 因此 `[minValue, maxValue]` 映射后就是真实的数据范围,无需扫描采样点。
|
||
* ⚠️ 水平镜像与时间偏移都不改变值的范围,因此不影响本函数。
|
||
*
|
||
* @param [IN] style 信号外观。
|
||
* @param [IN,OUT] low 区间下限,返回变换后的下限。
|
||
* @param [IN,OUT] high 区间上限,返回变换后的上限。
|
||
*/
|
||
inline void TransformValueRange(const SignalStyle& style, double& low, double& high)
|
||
{
|
||
if (low > high)
|
||
{
|
||
std::swap(low, high);
|
||
}
|
||
const double gain = static_cast<double>(style.gain);
|
||
if (gain != 1.0)
|
||
{
|
||
low *= gain;
|
||
high *= gain;
|
||
if (low > high)
|
||
{
|
||
std::swap(low, high);
|
||
}
|
||
}
|
||
if (style.absolute)
|
||
{
|
||
// |x| 在 [low, high] 上的取值范围:跳 0 时从 0 起,否则从较近的一端起。
|
||
const double first = std::fabs(low);
|
||
const double second = std::fabs(high);
|
||
const bool spansZero = (low <= 0.0) && (high >= 0.0);
|
||
low = spansZero ? 0.0 : std::min(first, second);
|
||
high = std::max(first, second);
|
||
}
|
||
if (style.mirrorVertical)
|
||
{
|
||
const double flipped = -low;
|
||
low = -high;
|
||
high = flipped;
|
||
}
|
||
if (style.offsetY != 0.0f)
|
||
{
|
||
const double offset = static_cast<double>(style.offsetY);
|
||
low += offset;
|
||
high += offset;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 单条信号的元数据与采样值(纯数据,不含可见性配置)。
|
||
*/
|
||
struct SignalData
|
||
{
|
||
std::wstring name; /**< 名称(取自文件列名,完全保留原样)。 */
|
||
std::wstring displayName; /**< 用户重命名的显示名;为空时使用 name。 */
|
||
std::wstring rawHeader; /**< 原始列名。 */
|
||
std::wstring unit; /**< 从列名括号中提取的单位,可能为空。 */
|
||
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
||
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
||
|
||
/**
|
||
* @brief 粗粒度包络层:每 coarseStride 个采样一组的 min/max。
|
||
*
|
||
* 大文件(千万级采样)的折线包络构建是 O(可见采样数) 的,每次视图变化都要重算,
|
||
* 实测 1770 万行 × 5 信号需要约 200ms/帧,拖拽时严重掉帧。
|
||
* 有了这一层(额外内存 ≈ 采样数的 0.8%),构建包络时只需读取“每列覆盖到的分组”,
|
||
* 读取量降到约 1/5000,且**绘制结果与全量扫描完全一致**(分组内已存真实极值)。
|
||
* 分组内没有有效值时存 ±inf,读取时跳过。
|
||
*/
|
||
std::vector<double> coarseMin; /**< 分组最小值。 */
|
||
std::vector<double> coarseMax; /**< 分组最大值。 */
|
||
std::size_t coarseStride = 0; /**< 分组的采样跨度;0 表示未构建。 */
|
||
|
||
SignalStyle style; /**< 显示外观(全局共享,跨标签页同步)。 */
|
||
|
||
/**
|
||
* @brief 获取用于界面展示的名称。
|
||
* @return 用户重命名后的显示名,未重命名时返回文件中的原始名称。
|
||
*/
|
||
const std::wstring& Title() const { return displayName.empty() ? name : displayName; }
|
||
};
|
||
|
||
/**
|
||
* @brief 一个已加载的数据文件及其自有时间轴。
|
||
*/
|
||
struct DataFile
|
||
{
|
||
std::wstring filePath; /**< 文件完整路径(仅用于状态提示)。 */
|
||
std::wstring fileName; /**< 文件名称。 */
|
||
std::wstring displayName; /**< 用户重命名的分组名;为空时使用 fileName。 */
|
||
std::wstring timeColumnName; /**< 时间列列名。 */
|
||
std::vector<double> time; /**< 时间轴数值。 */
|
||
std::vector<SignalData> signals; /**< 该文件包含的信号。 */
|
||
double minTime = 0.0; /**< 时间轴最小值。 */
|
||
double maxTime = 0.0; /**< 时间轴最大值。 */
|
||
bool timeMonotonic = true; /**< 时间轴是否单调不减。 */
|
||
|
||
/**
|
||
* @brief 获取用于界面展示的分组名。
|
||
* @return 用户重命名后的显示名,未重命名时返回文件名。
|
||
*/
|
||
const std::wstring& Title() const { return displayName.empty() ? fileName : displayName; }
|
||
};
|
||
|
||
/**
|
||
* @brief 由 CSV 解析结果构建可入库的数据文件。
|
||
*
|
||
* 该函数**不访问 DataSet**(只依赖传入的 CsvTable 与调色板基准序号),
|
||
* 因此可以在工作线程上执行:大文件的解析与列重组占整个加载耗时的大头,
|
||
* 放到工作线程后主线程只需做一次 O(1) 的移动入队,加载过程中的界面不会卡住。
|
||
*
|
||
* table 按右值传入:每列会被**移动**进对应信号的 values(零拷贝)。
|
||
*
|
||
* @param [IN,OUT] table CSV 解析结果(列会被移走)。
|
||
* @param [IN] filePath 源文件路径。
|
||
* @param [IN] paletteBase 调色板起始序号(通常传入当前已有信号数)。
|
||
* @param [OUT] file 构建结果。
|
||
* @param [OUT] error 失败原因描述(成功时清空)。
|
||
* @param [IN] progress 进度回调(0-1),返回 false 表示取消,可为空。
|
||
* @return 构建成功返回 true。
|
||
*/
|
||
bool BuildDataFile(CsvTable&& table, const std::wstring& filePath, std::size_t paletteBase, DataFile& file,
|
||
std::wstring& error, const CsvProgress& progress = CsvProgress());
|
||
|
||
/**
|
||
* @brief 数据集合:统一管理全部已加载文件,与视图配置无关。
|
||
*/
|
||
class DataSet
|
||
{
|
||
public:
|
||
/**
|
||
* @brief 追加一个已构建好的数据文件(仅做一次移动,不拷贝采样数据)。
|
||
* @param [IN,OUT] file 数据文件(内部缓冲会被移走)。
|
||
* @param [OUT] fileIndex 新文件下标。
|
||
* @return 追加成功返回 true。
|
||
*/
|
||
bool AddDataFile(DataFile&& file, int& fileIndex);
|
||
|
||
/**
|
||
* @brief 在指定文件的指定信号**下方**插入一条新信号(派生信号)。
|
||
*
|
||
* 新信号会占据 `afterSignalIndex + 1` 的位置,该位置及其后的信号下标整体后移一位;
|
||
* 调用方必须紧接着对每个标签页调用 `TabConfig::RemapAfterSignalInsert()` 修正引用。
|
||
* 采样值会被就地统计(min/max/finiteCount 与粗粒度包络层),显示外观按「全局信号序号」
|
||
* 取调色板颜色(与普通新信号一致)。
|
||
*
|
||
* @param [IN] fileIndex 目标文件下标。
|
||
* @param [IN] afterSignalIndex 插入位置的上一条信号下标。
|
||
* @param [IN,OUT] signal 待插入的信号(values 会被统计,其余字段原样使用)。
|
||
* @param [OUT] newSignalIndex 新信号在本文件内的下标。
|
||
* @return 插入成功返回 true。
|
||
*/
|
||
bool InsertDerivedSignal(int fileIndex, int afterSignalIndex, SignalData&& signal, int& newSignalIndex);
|
||
|
||
/**
|
||
* @brief 移除指定下标的文件,并立即释放其占用的全部采样内存。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @return 移除成功返回 true。
|
||
*/
|
||
bool RemoveFile(int fileIndex);
|
||
|
||
/**
|
||
* @brief 从某个分组中移除一条信号,并立即释放它的采样内存。
|
||
*
|
||
* 该信号之后的所有信号下标整体前移一位,因此调用方必须紧接着对每个标签页调用
|
||
* `TabConfig::RemapAfterSignalRemoval()`,并用 `RemapSelectionAfterSignalRemoval()`
|
||
* 修正共享选中项。使用者一旦确认不再需要该信号即可调用,本调用后其采样缓冲立即析构。
|
||
*
|
||
* @param [IN] fileIndex 信号所属文件下标。
|
||
* @param [IN] signalIndex 文件内信号下标。
|
||
* @return 移除成功返回 true。
|
||
*/
|
||
bool RemoveSignal(int fileIndex, int signalIndex);
|
||
|
||
/**
|
||
* @brief 在数据层移除某条信号之前,按旧下标修正选中引用。
|
||
* @param [IN] fileIndex 信号所属文件下标。
|
||
* @param [IN] signalIndex 即将被移除的信号下标。
|
||
*/
|
||
void RemapSelectionAfterSignalRemoval(int fileIndex, int signalIndex);
|
||
|
||
/**
|
||
* @brief 判断数据集合是否为空。
|
||
* @return 未加载任何文件返回 true。
|
||
*/
|
||
bool Empty() const { return m_files.empty(); }
|
||
|
||
/**
|
||
* @brief 获取文件数量。
|
||
* @return 文件数量。
|
||
*/
|
||
std::size_t FileCount() const { return m_files.size(); }
|
||
|
||
/**
|
||
* @brief 获取信号总数。
|
||
* @return 信号总数。
|
||
*/
|
||
std::size_t SignalCount() const;
|
||
|
||
/**
|
||
* @brief 获取全部文件(只读)。
|
||
* @return 文件集合。
|
||
*/
|
||
const std::vector<DataFile>& Files() const { return m_files; }
|
||
|
||
/**
|
||
* @brief 按下标获取文件。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @return 文件指针,下标非法时返回 nullptr。
|
||
*/
|
||
const DataFile* FileAt(int fileIndex) const;
|
||
|
||
/**
|
||
* @brief 修改某个信号在界面上的显示名称。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @param [IN] signalIndex 文件内信号下标。
|
||
* @param [IN] name 新的显示名;传入空串表示恢复使用原始列名。
|
||
* @return 修改成功返回 true。
|
||
*/
|
||
bool SetSignalDisplayName(int fileIndex, int signalIndex, const std::wstring& name);
|
||
|
||
/**
|
||
* @brief 修改某个文件分组的显示名称。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @param [IN] name 新的分组名;传入空串表示恢复使用文件名。
|
||
* @return 修改成功返回 true。
|
||
*/
|
||
bool SetFileDisplayName(int fileIndex, const std::wstring& name);
|
||
|
||
/**
|
||
* @brief 解析信号引用。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 信号数据指针,引用无效时返回 nullptr。
|
||
*/
|
||
const SignalData* Resolve(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 获取指定信号的显示外观(只读)。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 外观指针,引用无效时返回 nullptr。
|
||
*/
|
||
const SignalStyle* Style(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 获取指定信号的显示外观(可写)。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 外观指针,引用无效时返回 nullptr。
|
||
*/
|
||
SignalStyle* Style(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 计算全部文件时间范围的并集下限。
|
||
* @return 时间下限。
|
||
*/
|
||
double MinTime() const;
|
||
|
||
/**
|
||
* @brief 计算全部文件时间范围的并集上限。
|
||
* @return 时间上限。
|
||
*/
|
||
double MaxTime() const;
|
||
|
||
/**
|
||
* @brief 获取当前选中的信号(全标签页共享)。
|
||
* @return 信号引用。
|
||
*/
|
||
const SignalRef& Selected() const { return m_selected; }
|
||
|
||
/**
|
||
* @brief 设置当前选中的信号(全标签页共享)。
|
||
* @param [IN] ref 信号引用;传入无效值表示取消选择。
|
||
*/
|
||
void SetSelected(const SignalRef& ref) { m_selected = ref; }
|
||
|
||
/**
|
||
* @brief 在数据层移除某文件之前,按旧下标修正选中引用。
|
||
* @param [IN] fileIndex 即将被移除的文件下标。
|
||
*/
|
||
void RemapSelectionAfterFileRemoval(int fileIndex);
|
||
|
||
/**
|
||
* @brief 校验选中引用是否仍指向存在的信号,失效则取消选中。
|
||
* @return 选中状态发生变化返回 true。
|
||
*/
|
||
bool ValidateSelection();
|
||
|
||
private:
|
||
std::vector<DataFile> m_files; /**< 已加载文件集合。 */
|
||
SignalRef m_selected; /**< 当前选中信号(跨标签页共享)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 一个波形网格(纵向泳道),可容纳多条信号叠加显示。
|
||
*/
|
||
struct Lane
|
||
{
|
||
/**
|
||
* @brief 「均值滤波」中某个信号自己的参数。
|
||
*
|
||
* 仅参与列表中的信号才会被勾选预览;参数按信号保存,
|
||
* 因此同一条网格内不同信号可以使用不同的均值点数与迭代次数。
|
||
* 「边沿计数(中心轴)」的平滑参数与它**完全同构**(同一套命名与作用),
|
||
* 见 `EdgeCenterItem`。
|
||
*/
|
||
struct MeanFilterItem
|
||
{
|
||
SignalRef ref; /**< 目标信号。 */
|
||
int points = 5; /**< 均值点数 X(滑平半径,窗口 = 2X+1)。 */
|
||
int iterations = 1; /**< 迭代次数 N(重复滤波次数,至少 1)。 */
|
||
};
|
||
|
||
/** 中心轴边沿计数的平滑参数:与均值滤波共用同一结构与同一套语义。 */
|
||
using EdgeCenterItem = MeanFilterItem;
|
||
|
||
/**
|
||
* @brief 「弱峰调整」中某个信号自己的参数条目。
|
||
*
|
||
* 与「均值滤波」完全一致:**每个被勾选的信号各自持有参数**
|
||
*(调整幅度 -400% – +400% + 迭代次数 N),因此同一网格内不同信号可以使用不同强度。
|
||
*
|
||
* ⚠️ `iterations` 的**上限由用户自己设定**(`maxIterations`):面板上「迭代次数 N」
|
||
* 滑块的量程就是 `1 … maxIterations`,点击滑块右侧的数值可以把该上限改成其它正整数。
|
||
*/
|
||
struct WeakPeakItem
|
||
{
|
||
SignalRef ref; /**< 目标信号。 */
|
||
int amountPercent = 0; /**< 调整幅度(-400–400,默认 0 = 不改动)。 */
|
||
int iterations = 1; /**< 迭代次数 N(1 … `maxIterations`)。 */
|
||
int maxIterations = 10; /**< 「迭代次数 N」滑块的上限(用户可在面板上修改,恒 ≥ 1)。 */
|
||
};
|
||
|
||
std::wstring name; /**< 网格名称。 */
|
||
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
|
||
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
|
||
double yMin = -1.0; /**< 手动 Y 轴下限。 */
|
||
double yMax = 1.0; /**< 手动 Y 轴上限。 */
|
||
|
||
/**
|
||
* @brief 「选区 Y 轴自适应」开关。
|
||
*
|
||
* 开启后每次对该网格做时间轴选区放大,都会自动把本网格的 Y 轴匹配到新的可见时间区间;
|
||
* 用户手动调整幅值(拖拽平移、Y 轴选区放大、直接输入上下限)后会自动关闭。
|
||
*/
|
||
bool fitYToSelection = false;
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 边沿计数」开关。
|
||
*
|
||
* 开启后本网格显示一条横向光标(可上下拖动),并在浮动面板中列出该网格各信号的
|
||
* 上升沿 / 下降沿数量。与 `fitYToSelection` 一样属于网格自身状态,重建网格时按信号继承。
|
||
*/
|
||
bool edgeCount = false;
|
||
|
||
/** 横向光标所在的幅值(数据单位)。 */
|
||
double edgeValue = 0.0;
|
||
|
||
/** `edgeValue` 是否已经初始化(首次开启时取可见幅值范围的中点)。 */
|
||
bool edgeValueValid = false;
|
||
|
||
/** 边沿统计浮动面板是否展开(点击面板上的 ✕ 后置 false)。 */
|
||
bool edgePanelOpen = true;
|
||
|
||
/** 统计面板左上角位置(客户区像素;<0 表示自动定位到绘图区右侧)。 */
|
||
double edgePanelX = -1.0;
|
||
|
||
/** 统计面板左上角纵坐标(<0 表示自动定位)。 */
|
||
double edgePanelY = -1.0;
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 边沿计数(中心轴)」开关。
|
||
*
|
||
* 开启后为本网格内**被勾选**的信号额外绘制一条均值滤波曲线(与「实验性功能 → 均值滤波」
|
||
* 用的是同一套参数:均值点数 X + 迭代次数 N),并按“信号穿越该滤波曲线”统计上/下边沿。
|
||
* 这条曲线只是叠加显示,不会作为信号加入左侧列表。
|
||
*/
|
||
bool edgeCenter = false;
|
||
|
||
/**
|
||
* @brief 参与中心轴边沿计数的信号及其平滑参数(只包含被勾选的信号)。
|
||
*
|
||
* 参数语义与 `meanFilterItems` **完全一致**(`points` = 均值点数 X,
|
||
* `iterations` = 迭代次数 N),因此两条曲线的数值逐位相同。
|
||
*/
|
||
std::vector<EdgeCenterItem> edgeCenterItems;
|
||
|
||
/** 取某个信号的中心轴平滑参数(未勾选时返回空指针)。 */
|
||
const EdgeCenterItem* FindEdgeCenterItem(const SignalRef& ref) const;
|
||
|
||
/** 取得或创建某信号的中心轴平滑参数(用于勾选)。 */
|
||
EdgeCenterItem& EnsureEdgeCenterItem(const SignalRef& ref);
|
||
|
||
/** 取消某信号的中心轴边沿计数勾选。 */
|
||
void RemoveEdgeCenterItem(const SignalRef& ref);
|
||
|
||
/** 取某信号的中心轴边沿计数勾选状态。 */
|
||
bool EdgeCenterEnabled(const SignalRef& ref) const
|
||
{
|
||
return FindEdgeCenterItem(ref) != nullptr;
|
||
}
|
||
|
||
/** 中心轴统计浮动面板是否展开。 */
|
||
bool edgeCenterPanelOpen = true;
|
||
|
||
/** 中心轴统计面板左上角位置(客户区像素;<0 表示自动定位)。 */
|
||
double edgeCenterPanelX = -1.0;
|
||
|
||
/** 中心轴统计面板左上角纵坐标(<0 表示自动定位)。 */
|
||
double edgeCenterPanelY = -1.0;
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 均值滤波」开关。
|
||
*
|
||
* 开启后为本网格内**被勾选**的信号额外绘制一条多次迭代滑动平均曲线(按各自的均值点数 X
|
||
* 重复 N 阶),并在浮动面板中提供每个信号自己的参数与「构建为新信号」按钮。
|
||
* 与其它实验性功能一样属于网格自身状态,重建网格时按信号继承。
|
||
*/
|
||
bool meanFilter = false;
|
||
|
||
/**
|
||
* @brief 参与均值滤波预览的信号及其参数(只包含被勾选的信号)。
|
||
*
|
||
* 顺序与信号在网格内的顺序不一定一致,绘制时按网格内信号顺序派发。
|
||
*/
|
||
std::vector<MeanFilterItem> meanFilterItems;
|
||
|
||
/** 取某个信号的均值滤波参数(未勾选时返回空指针)。 */
|
||
const MeanFilterItem* FindMeanFilterItem(const SignalRef& ref) const;
|
||
|
||
/** 取得或创建某信号的均值滤波参数(用于勾选)。 */
|
||
MeanFilterItem& EnsureMeanFilterItem(const SignalRef& ref);
|
||
|
||
/** 取消某信号的均值滤波勾选。 */
|
||
void RemoveMeanFilterItem(const SignalRef& ref);
|
||
|
||
/** 取某信号的均值滤波勾选状态。 */
|
||
bool MeanFilterEnabled(const SignalRef& ref) const
|
||
{
|
||
return FindMeanFilterItem(ref) != nullptr;
|
||
}
|
||
|
||
/** 均值滤波浮动面板是否展开。 */
|
||
bool meanFilterPanelOpen = true;
|
||
|
||
/** 均值滤波面板左上角位置(客户区像素;<0 表示自动定位)。 */
|
||
double meanFilterPanelX = -1.0;
|
||
|
||
/** 均值滤波面板左上角纵坐标(<0 表示自动定位)。 */
|
||
double meanFilterPanelY = -1.0;
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 弱峰调整」开关。
|
||
*
|
||
* 开启后为本网格内**被勾选**的信号额外绘制一条「弱峰调整」曲线:
|
||
* 识别纹波的波峰/波谷,把“比左右同类极值更弱”的波峰上拉、波谷下拉,
|
||
* 并按各信号自己的「增强等级」在左右两个同类极值之间取目标值,使其恢复与周边振幅。
|
||
* 这条曲线只是叠加显示,不会作为信号加入左侧列表。
|
||
*/
|
||
bool weakPeak = false;
|
||
|
||
/**
|
||
* @brief 参与弱峰调整预览的信号及其**各自的参数**(只包含被勾选的信号)。
|
||
*
|
||
* 参数(调整幅度 -400% – +400% + 迭代次数 N)按信号保存,与「均值滤波」的实现方式一致:
|
||
* 同一网格内不同信号可以使用不同的增强强度,也可以分别「构建为新信号」。
|
||
*/
|
||
std::vector<WeakPeakItem> weakPeakItems;
|
||
|
||
/** 取某个信号的弱峰调整参数(未勾选时返回空指针)。 */
|
||
const WeakPeakItem* FindWeakPeakItem(const SignalRef& ref) const;
|
||
|
||
/** 取得或创建某信号的弱峰调整参数(用于勾选)。 */
|
||
WeakPeakItem& EnsureWeakPeakItem(const SignalRef& ref);
|
||
|
||
/** 取消某信号的弱峰调整勾选。 */
|
||
void RemoveWeakPeakItem(const SignalRef& ref);
|
||
|
||
/** 取某信号的弱峰调整勾选状态。 */
|
||
bool WeakPeakEnabled(const SignalRef& ref) const
|
||
{
|
||
return FindWeakPeakItem(ref) != nullptr;
|
||
}
|
||
|
||
/** 弱峰调整浮动面板是否展开。 */
|
||
bool weakPeakPanelOpen = true;
|
||
|
||
/** 弱峰调整面板左上角位置(客户区像素;<0 表示自动定位)。 */
|
||
double weakPeakPanelX = -1.0;
|
||
|
||
/** 弱峰调整面板左上角纵坐标(<0 表示自动定位)。 */
|
||
double weakPeakPanelY = -1.0;
|
||
};
|
||
|
||
/**
|
||
* @brief 从内存中的信号采样点取某一时刻的取值。
|
||
*
|
||
* 加载完成后全部采样值已在内存中,本函数只查询内存数据,与源文件无关。
|
||
*
|
||
* @param [IN] file 信号所属的已加载数据分组(提供时间轴)。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] time 采样时刻。
|
||
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
|
||
* @param [OUT] value 输出采样值,缺失时为 NaN。
|
||
* @return 时间点落在数据范围内且取到有效值返回 true。
|
||
*/
|
||
bool SampleSignalAt(const DataFile& file, const SignalData& signal, double time, bool interpolate,
|
||
double& value);
|
||
|
||
/**
|
||
* @brief 取某一时刻**画面上显示**的取值(含水平镜像的"数据反序"与时间偏移)。
|
||
*
|
||
* 与 `SampleSignalAt` 的区别:
|
||
* * 水平镜像时用 `SourceIndexFor` 把下标映射到源采样(头尾对调);
|
||
* * 时间偏移时先按 `time - style.offsetTime` 求出源采样时刻(因为绘制在 `t + offsetTime`)。
|
||
* 因此返回的正是当前绘制出来的那条曲线在该时刻的取值 —— 光标读数、数据点标注都必须用它,
|
||
* 否则读数会与看到的波形不一致。
|
||
* ⚠️ 系数 / 绝对值 / 垂直镜像 / `offsetY` 不在这里处理,由 `ApplyDrawTransform` 负责。
|
||
*
|
||
* @param [IN] file 信号所属的已加载数据分组(提供时间轴)。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] style 显示外观(只用 `mirrorHorizontal`)。
|
||
* @param [IN] time 采样时刻。
|
||
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
|
||
* @param [OUT] value 输出采样值,缺失时为 NaN。
|
||
* @return 时间点落在数据范围内且取到有效值返回 true。
|
||
*/
|
||
bool SampleDisplayedSignalAt(const DataFile& file, const SignalData& signal,
|
||
const SignalStyle& style, double time, bool interpolate, double& value);
|
||
|
||
/**
|
||
* @brief 计算 N 点滑动平均(窗口 = [i-N, i+N],边界按实际窗口长度)。
|
||
*
|
||
* 与「边沿计数(中心轴)」的中心平均值曲线**完全同一算法**:
|
||
* NaN 不参与平均;中心点本身为 NaN 时结果也为 NaN;窗口内无有效值时为 NaN。
|
||
*
|
||
* @param [IN] values 原始采样值。
|
||
* @param [IN] radius 平滑半径 N(0 表示原样返回)。
|
||
* @param [OUT] out 输出数组(长度与 values 相同)。
|
||
*/
|
||
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out);
|
||
|
||
/**
|
||
* @brief 计算多次迭代滑动平均:把 N 点滑动平均重复应用 `iterations` 次。
|
||
*
|
||
* 每次迭代以上一次的结果为输入(NaN 不参与平均),因此迭代次数越高曲线越平滑。
|
||
* `iterations <= 1` 时与 `ComputeMovingAverage` 完全一致。
|
||
*
|
||
* @param [IN] values 原始采样值。
|
||
* @param [IN] radius 滑平半径 N(0 表示原样返回)。
|
||
* @param [IN] iterations 迭代次数(至少为 1)。
|
||
* @param [OUT] out 输出数组(长度与 values 相同)。
|
||
*/
|
||
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int iterations,
|
||
std::vector<float>& out);
|
||
|
||
/**
|
||
* @brief 「弱峰调整」:把比周边更弱的纹波波峰/波谷恢复,或(负幅度时)把弱峰削弱。
|
||
*
|
||
* 本算法**不做任何前置滤波**:需要平滑时用户可以先用「均值滤波」构建一条
|
||
* 滤波后的信号,再对它做弱峰调整。极值点直接在输入采样上识别。
|
||
*
|
||
* 「调整幅度」滑动条的量程是 **-400% – +400%**,它按"梯度"同时驱动**三组识别规则**:
|
||
* 越宽松的规则参与得越晚,同一滑动条位置上的幅度越小:
|
||
* | 滑动条 | 第一类(严格) | 第二类(或) | 第三类(宽松) |
|
||
* | --- | --- | --- | --- |
|
||
* | 0 – 100% | 0 – 100% | — | — |
|
||
* | 100 – 200% | 100 – 200% | 0 – 100% | — |
|
||
* | 200 – 300% | 200%(上限) | 100 – 200% | 0 – 100% |
|
||
* | 300 – 400% | 200%(上限) | 200%(上限) | 100 – 200% |
|
||
* (负值完全对称;某个极值若同时满足多组规则,取**幅度绝对值最大**的那一组。)
|
||
*
|
||
* 单阶处理(全 O(n)):
|
||
* 1. **识别趋势与极值**:扫描单调段,方向发生翻转处的上一个采样点即为
|
||
* 波峰(先升后降)或波谷(先降后升);波峰与波谷交替出现。
|
||
* 2. **三组识别规则**(以波峰 k 为例;`L` = 左侧相邻第一个波峰、`l` = 左侧相邻第一个波谷,
|
||
* `r` = 右侧相邻第一个波谷、`R` = 右侧相邻第一个波峰):
|
||
* * 第一类:`v < (L+l)/2` **且** `v < (r+R)/2`(两侧均值都要满足);
|
||
* * 第二类:`v < (L+l)/2` **或** `v < (r+R)/2`(放宽为"或");
|
||
* * 第三类:`v < L` **且** `v < R`(只与左右同类极值比较)。
|
||
* 波谷把"低于"换成"高于"、把"且/或"的上下界互换即可。
|
||
* 3. **目标值**(每一组的幅度 `a` 都在 -2 – +2,对应其自身的 -200% – +200%):
|
||
* * `0 – +100%`:当前值 → 左右**同类**极值中**较温和**的一侧
|
||
* (波峰取较低的波峰、波谷取较高的波谷);
|
||
* * `+100 – +200%`:该侧 → 左右同类极值的**均值**;
|
||
* * `0 – -100%`:当前值 → 相邻**异类**极值中**较近**的一个
|
||
* (波峰取较高的波谷、波谷取较低的波峰);
|
||
* * `-100 – -200%`:该侧 → 相邻异类极值的**均值**。
|
||
* 削弱**不会反向增强**(目标被夹在"不越过当前值"的一侧)。
|
||
* 4. **平滑偏移**:极值点本身取其目标值,两侧的数据点按相邻两个极值点的偏移量
|
||
* 平滑过渡(平滑步进,两端导数为 0),使波形整体趋势不变、只在弱峰附近被推回。
|
||
*
|
||
* 不属于任何一类规则点的极值偏移量恒为 0,因此输出与输入逐位相同。
|
||
*
|
||
* @param [IN] values 原始采样值。
|
||
* @param [IN] amount 调整幅度(-4 – +4,对应 -400% – +400%,超出范围会被夹取;0 = 完全不动)。
|
||
* @param [IN] iterations 迭代次数 N(重复应用次数,至少为 1)。
|
||
* @param [OUT] out 输出数组(长度与 values 相同;原值为 NaN 处保持 NaN)。
|
||
*/
|
||
void ComputeWeakPeakAdjust(const std::vector<double>& values, double amount, int iterations,
|
||
std::vector<float>& out);
|
||
|
||
/**
|
||
* @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。
|
||
* @param [IN] file 目标文件。
|
||
* @param [IN] time 采样时刻。
|
||
* @return 采样点下标,文件为空时返回 -1。
|
||
*/
|
||
int FindNearestSample(const DataFile& file, double time);
|
||
|
||
/**
|
||
* @brief 单个标签页的独立视图配置(信号可见性、网格划分与视图范围)。
|
||
*
|
||
* 仅保存「本页看到哪些信号」与视图状态;信号的显示外观(颜色/线宽/透明度/样式)
|
||
* 保存在 `DataSet` 中并被所有标签页共享,因此同一个信号在各视图中的显示效果一致。
|
||
*/
|
||
class TabConfig
|
||
{
|
||
public:
|
||
TabConfig();
|
||
|
||
/**
|
||
* @brief 获取标签页名称。
|
||
* @return 名称。
|
||
*/
|
||
const std::wstring& Name() const { return m_name; }
|
||
|
||
/**
|
||
* @brief 设置标签页名称。
|
||
* @param [IN] name 新名称。
|
||
*/
|
||
void SetName(const std::wstring& name) { m_name = name; }
|
||
|
||
/**
|
||
* @brief 依据数据集合调整配置容量(新增文件补默认配置,移除文件清理配置)。
|
||
* @param [IN] dataSet 数据集合。
|
||
*/
|
||
void SyncToDataSet(const DataSet& dataSet);
|
||
|
||
/**
|
||
* @brief 在数据层移除某文件之前,按旧下标修正本配置(不重建网格)。
|
||
* @param [IN] fileIndex 即将被移除的文件下标。
|
||
* @note 调用顺序必须为:各标签页 RemapAfterFileRemoval → 数据层 RemoveFile → 各标签页 SyncToDataSet。
|
||
*/
|
||
void RemapAfterFileRemoval(int fileIndex);
|
||
|
||
/**
|
||
* @brief 在数据层移除某条信号之前,按旧下标修正本配置(不重建网格)。
|
||
*
|
||
* 会删除该信号的可见性记录与网格归属,并把同一文件内位于其后的引用下标前移一位。
|
||
* 若某个网格因此变空则一并移除(与“取消勾选后空网格消失”的策略一致)。
|
||
*
|
||
* @param [IN] fileIndex 信号所属文件下标。
|
||
* @param [IN] signalIndex 即将被移除的信号下标。
|
||
* @note 调用顺序必须为:各标签页 RemapAfterSignalRemoval → 数据层修正选中 →
|
||
* 数据层 RemoveSignal → 各标签页 SyncToDataSet。
|
||
*/
|
||
void RemapAfterSignalRemoval(int fileIndex, int signalIndex);
|
||
|
||
/**
|
||
* @brief 判断指定信号是否可见。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 可见返回 true。
|
||
*/
|
||
bool IsVisible(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 设置指定信号的可见性,并同步网格划分。
|
||
* @param [IN] ref 信号引用。
|
||
* @param [IN] visible 目标可见状态。
|
||
*/
|
||
void SetVisible(const SignalRef& ref, bool visible);
|
||
|
||
/**
|
||
* @brief 切换指定信号的可见性,并同步网格划分。
|
||
* @param [IN] ref 信号引用。
|
||
*/
|
||
void ToggleVisible(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 设置某个文件全部信号的可见性,并同步网格划分。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @param [IN] visible 目标可见状态。
|
||
*/
|
||
void SetFileVisible(int fileIndex, bool visible);
|
||
|
||
/**
|
||
* @brief 设置全部信号可见性,并同步网格划分。
|
||
* @param [IN] visible 目标可见状态。
|
||
*/
|
||
void SetAllVisible(bool visible);
|
||
|
||
/**
|
||
* @brief 仅显示指定信号,其余全部隐藏。
|
||
* @param [IN] ref 需要保持可见的信号。
|
||
*/
|
||
void SoloSignal(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 判断某文件是否存在可见信号。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @return 存在可见信号返回 true。
|
||
*/
|
||
bool FileHasVisible(int fileIndex) const;
|
||
|
||
/**
|
||
* @brief 判断某文件是否全部信号可见。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @return 全部可见且至少有一条信号返回 true。
|
||
*/
|
||
bool FileAllVisible(int fileIndex) const;
|
||
|
||
/**
|
||
* @brief 判断是否存在任何可见信号。
|
||
* @return 存在返回 true。
|
||
*/
|
||
bool AnyVisible() const;
|
||
|
||
/**
|
||
* @brief 统计可见信号数量。
|
||
* @return 可见信号数量。
|
||
*/
|
||
int VisibleCount() const;
|
||
|
||
/**
|
||
* @brief 获取网格划分策略。
|
||
* @return 策略枚举。
|
||
*/
|
||
LaneMode Mode() const { return m_laneMode; }
|
||
|
||
/**
|
||
* @brief 设置网格划分策略并同步网格。
|
||
* @param [IN] mode 目标策略。
|
||
*/
|
||
void SetMode(LaneMode mode);
|
||
|
||
/**
|
||
* @brief 依据当前可见性与策略重新生成网格划分。
|
||
*/
|
||
void SyncLanes();
|
||
|
||
/**
|
||
* @brief 获取网格集合(只读)。
|
||
* @return 网格集合。
|
||
*/
|
||
const std::vector<Lane>& Lanes() const { return m_lanes; }
|
||
|
||
/**
|
||
* @brief 获取网格集合(可写)。
|
||
* @return 网格集合。
|
||
*/
|
||
std::vector<Lane>& Lanes() { return m_lanes; }
|
||
|
||
/**
|
||
* @brief 查询某信号所属网格下标。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 网格下标,未找到返回 -1。
|
||
*/
|
||
int LaneOfSignal(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 将信号移动到指定网格,并切换为自定义划分策略。
|
||
* @param [IN] dataSet 数据集合。
|
||
* @param [IN] ref 信号引用。
|
||
* @param [IN] laneIndex 目标网格下标,传入负值表示新建网格。
|
||
* @return 移动后该信号所在的网格下标。
|
||
*/
|
||
int MoveSignalToLane(const DataSet& dataSet, const SignalRef& ref, int laneIndex);
|
||
|
||
/**
|
||
* @brief 「拆分当前网格」:把指定网格内的信号按个拆分成独立网格。
|
||
*
|
||
* 新网格插入在原网格的**下方依次排列**(原位置 + 1 起),原本在其下方的网格
|
||
* 顺次后移。由于网格编号(GX)完全由位置推导,拆分后所有引用该编号的地方
|
||
* (左侧列表徽标、图例徽标、面板标题、右键菜单标题)会自动保持一致。
|
||
*
|
||
* @param [IN] laneIndex 待拆分的网格下标。
|
||
* @return 拆分后该网格内第一条信号所在的网格下标;无法拆分(不存在或只有 1 条信号)返回 -1。
|
||
*/
|
||
int SplitLane(int laneIndex);
|
||
|
||
/** * @brief 在数据层插入新信号之后,修正本页引用该文件的信号下标。
|
||
*
|
||
* 数据层在下标 `insertPos` 处插入了一条信号,因此本页可见性表需插入一列,
|
||
* 且所有指向该文件、下标不小于 `insertPos` 的信号引用都要后移一位。
|
||
*
|
||
* @param [IN] fileIndex 发生插入的文件下标。
|
||
* @param [IN] insertPos 新信号的下标。
|
||
*/
|
||
void RemapAfterSignalInsert(int fileIndex, int insertPos);
|
||
|
||
/**
|
||
* @brief 把某信号所在的网格移动到另一信号所在网格的下方(紧邻其后)。
|
||
*
|
||
* 用于「均值滤波 → 构建为新信号」:新信号所在的网格应紧跟在原信号网格的下方。
|
||
* 移动成功后会切换为自定义划分策略,避免后续重建网格时顺序被复原。
|
||
*
|
||
* @param [IN] moved 被移动网格内的任一信号。
|
||
* @param [IN] anchor 锚点信号(其所在网格之后即为目标位置)。
|
||
* @return 发生移动返回 true。
|
||
*/
|
||
bool MoveLaneOfSignalBelow(const SignalRef& moved, const SignalRef& anchor);
|
||
|
||
/**
|
||
* @brief 把某个网格整体移动到新的位置(用于拖拽 GX 编号调整网格次序)。
|
||
*
|
||
* `target` 是**插入位置**(0..laneCount):0 表示移到最上方,laneCount 表示移到最下方。
|
||
* 移动后会切换到自定义划分策略并按新位置重新编号全部网格,
|
||
* 因此所有以 GX 编号关联的界面(信号列表徽标、图例徽标、菜单、浮动面板)自动同步。
|
||
*
|
||
* @param [IN] from 被移动的网格下标。
|
||
* @param [IN] target 插入位置(0..网格数)。
|
||
* @return 实际发生了移动返回 true。
|
||
*/
|
||
bool MoveLane(int from, int target);
|
||
|
||
/**
|
||
* @brief 按当前位置统一重命名全部网格(GX 编号由位置推导,须保持一致)。
|
||
*/
|
||
void RenameLanes();
|
||
|
||
/** * @brief 查询某文件的分组是否处于折叠状态。
|
||
* @param [IN] fileIndex 文件下标。
|
||
* @return 折叠状态。
|
||
*/
|
||
bool IsCollapsed(int fileIndex) const;
|
||
|
||
/**
|
||
* @brief 切换某文件分组的折叠状态。
|
||
* @param [IN] fileIndex 文件下标。
|
||
*/
|
||
void ToggleCollapsed(int fileIndex);
|
||
|
||
/**
|
||
* @brief 计算某网格的 Y 轴显示范围。
|
||
* @param [IN] dataSet 数据集合。
|
||
* @param [IN] laneIndex 网格下标。
|
||
* @param [OUT] yMin 输出下限。
|
||
* @param [OUT] yMax 输出上限。
|
||
*/
|
||
void ComputeLaneRange(const DataSet& dataSet, int laneIndex, double& yMin, double& yMax) const;
|
||
|
||
/**
|
||
* @brief 计算全部可见信号覆盖的时间范围。
|
||
* @param [IN] dataSet 数据集合。
|
||
* @param [OUT] minTime 输出下限。
|
||
* @param [OUT] maxTime 输出上限。
|
||
* @return 存在可见信号返回 true。
|
||
*/
|
||
bool ComputeVisibleTimeBounds(const DataSet& dataSet, double& minTime, double& maxTime) const;
|
||
|
||
/// @brief 视图左边界时间(可读写)。
|
||
double& ViewMin() { return m_viewMin; }
|
||
/// @brief 视图左边界时间(只读)。
|
||
double ViewMin() const { return m_viewMin; }
|
||
/// @brief 视图右边界时间(可读写)。
|
||
double& ViewMax() { return m_viewMax; }
|
||
/// @brief 视图右边界时间(只读)。
|
||
double ViewMax() const { return m_viewMax; }
|
||
/// @brief 视图范围是否已初始化(可读写)。
|
||
bool& ViewInitialized() { return m_viewInitialized; }
|
||
/// @brief 网格纵向滚动偏移量(可读写)。
|
||
float& LaneScroll() { return m_laneScroll; }
|
||
/// @brief 网格纵向滚动偏移量(只读)。
|
||
float LaneScroll() const { return m_laneScroll; }
|
||
/// @brief 时间光标模式(可读写)。
|
||
PlotCursorMode& CursorMode() { return m_cursorMode; }
|
||
/// @brief 时间光标模式(只读)。
|
||
PlotCursorMode CursorMode() const { return m_cursorMode; }
|
||
/// @brief 是否启用了时间光标(单/双光标均为启用,只读)。
|
||
bool CursorEnabled() const { return m_cursorMode != PlotCursorMode::NONE; }
|
||
/// @brief 光标 1(左侧)的时间位置(可读写)。
|
||
double& CursorTime() { return m_cursorTime; }
|
||
/// @brief 光标 1(左侧)的时间位置(只读)。
|
||
double CursorTime() const { return m_cursorTime; }
|
||
/// @brief 光标 2 的时间位置(可读写,仅双光标模式使用)。
|
||
double& CursorTimeAlt() { return m_cursorTimeAlt; }
|
||
/// @brief 光标 2 的时间位置(只读)。
|
||
double CursorTimeAlt() const { return m_cursorTimeAlt; }
|
||
/// @brief 绘图区拖拽缩放模式(可读写)。
|
||
PlotZoomMode& ZoomMode() { return m_zoomMode; }
|
||
/// @brief 绘图区拖拽缩放模式(只读)。
|
||
PlotZoomMode ZoomMode() const { return m_zoomMode; }
|
||
|
||
private:
|
||
std::wstring m_name; /**< 标签页名称。 */
|
||
std::vector<std::vector<bool>> m_visible; /**< 信号可见性,按 [文件][信号] 索引(本页独立)。 */
|
||
std::vector<bool> m_collapsed; /**< 文件分组折叠状态,按文件索引。 */
|
||
std::vector<Lane> m_lanes; /**< 网格划分。 */
|
||
LaneMode m_laneMode = LaneMode::PER_SIGNAL; /**< 网格划分策略。 */
|
||
double m_viewMin = 0.0; /**< 视图左边界时间。 */
|
||
double m_viewMax = 1.0; /**< 视图右边界时间。 */
|
||
bool m_viewInitialized = false; /**< 视图范围是否已初始化。 */
|
||
float m_laneScroll = 0.0f; /**< 网格纵向滚动偏移量。 */
|
||
PlotCursorMode m_cursorMode = PlotCursorMode::NONE; /**< 时间光标模式(无/单/双)。 */
|
||
double m_cursorTime = 0.0; /**< 光标 1(左侧)的时间位置。 */
|
||
double m_cursorTimeAlt = 0.0; /**< 光标 2 的时间位置(仅双光标模式有效)。 */
|
||
PlotZoomMode m_zoomMode = PlotZoomMode::NONE; /**< 绘图区拖拽缩放模式。 */
|
||
};
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __DATA_MODEL_HPP__
|