四次卡顿是**同一个根因**:公式信号(不保序)的幅值范围与包络带各写了一份 「逐点扫描整条信号」,且都没有缓存。2580 万采样时实测 1589ms + 1481ms, 而幅值范围指纹包含公式文本 / 信号顺序 / 可见性 / lane.autoRange ⇒ 确认编辑、适应视图、拖拽排序、组复选框每一个都会让它失效、每次都重付 3.1 秒。 修法: - 新增 DataSet::FormulaTransformCache / FormulaTransformFor:一次扫描同时算出 绘制值域与逐分组包络,缓存到数据层(std::deque,指针稳定)。 - TabConfig::ComputeLaneRange 的公式分支改为查该缓存(保留非法公式的逐点回退)。 - PlotView::AcquireFormulaEnvelope 改为直接复用数据层同一条目, 删除 PlotView 自有的 FormulaEnvelopeEntry / m_formulaEnvelopes 与两个重复常量。 - FormulaEval 新增 FormulaProgram(编译一次、批量求值):always_inline、 常量折叠(2/1.25 => 1.6)、幂次改写(x^2 => x*x、x^3、x^0.5 => sqrt)、 按分组两级循环(去掉每点一次整数除法)。 - EvaluateFormula 复用 thread_local FormulaProgram(按内容缓存),两条路径语义一致。 实测(2580 万采样,纯控制台探针):确认公式 3070ms => 379ms;改 x^2 1.9s => 0.19s; 适应视图 / 拖拽排序 / 组复选框 1589ms => 0.0015ms(命中缓存)。 回归:缓存命中/失效/值域、14 组公式语义与逐位一致性、非法/空公式回退全部 PASS; release/debug 零警告;Release TargetCheck.exe 890,880 B(< 1MB)。 未做:跨组移动共享时间轴仍深拷贝 197MB(55-66ms,值语义决定,风险大于收益); 首次 379ms 仍同步阻塞(后台化的复杂度暂不划算)。
2030 lines
88 KiB
C++
2030 lines
88 KiB
C++
#ifndef __DATA_MODEL_HPP__
|
||
#define __DATA_MODEL_HPP__
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstddef>
|
||
#include <cstdint>
|
||
#include <array>
|
||
#include <deque>
|
||
#include <limits>
|
||
#include <string>
|
||
#include <atomic>
|
||
#include <vector>
|
||
|
||
#include "DataLoader.hpp"
|
||
#include "DataTable.hpp"
|
||
#include "FormulaEval.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
/**
|
||
* @brief 波形绘制方式。
|
||
*/
|
||
enum class DrawMode : int
|
||
{
|
||
LINE = 0, /**< 相邻采样点之间以插值直线连接。 */
|
||
SCATTER = 1, /**< 仅绘制采样点散点。 */
|
||
LINE_SCATTER = 2 /**< 同时绘制插值直线与采样点散点(界面文案「点线」)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 样式允许的线宽范围(逻辑像素)。
|
||
*
|
||
* 信号面板的线宽滑块与所有线宽写入路径都以此为唯一依据;
|
||
* 稀疏信号(见 `kSparseSignalSampleLimit`)的初始线宽取其中的较大值。
|
||
*/
|
||
constexpr float kSignalLineWidthMin = 0.5f;
|
||
constexpr float kSignalLineWidthMax = 10.0f;
|
||
|
||
/**
|
||
* @brief **只含一个有效采样点**的信号的初始线宽(取样式允许的最大线宽)。
|
||
*
|
||
* 单个数据点的初始绘制方式是「散点」,半径随线宽线性增长;取最大值才能看清那个点。
|
||
* ⚠️ 用户 2026-10-16 要求:初始绘制方式由「点线」改为「**散点**」,
|
||
* 同时信号面板「样式」行的「线段」/「点线」两个按钮对该信号置灰。
|
||
* ⚠️ 曾经还对「有效采样点 2…16」的信号也设了「点线 + 2px」的默认样式,
|
||
* 该需求已**作废**(用户 2026-10-15 明确取消):放大后自动切「点线」的机制
|
||
* 已经覆盖了那个场景,无需再改默认样式。
|
||
*/
|
||
constexpr float kSinglePointSignalLineWidth = kSignalLineWidthMax;
|
||
|
||
/**
|
||
* @brief 普通信号的**默认线宽**(逻辑像素):新建信号时写入,也是样式区「复位」按钮要恢复的值。
|
||
*
|
||
* ⚠️ 只有这一个来源:`BuildSignalGroup` 的初始化与信号面板线宽复位都用它,
|
||
* 否则改了一处就会与"新建信号"的观感不一致。
|
||
*/
|
||
constexpr float kDefaultSignalLineWidth = 1.0f;
|
||
|
||
/**
|
||
* @brief 按有效采样点数取该信号**默认的线宽**(复位按钮与新建信号共用同一判定)。
|
||
* @param [IN] finiteCount 有效采样点数。
|
||
* @return 默认线宽(单数据点信号 = 最大线宽,其余 = `kDefaultSignalLineWidth`)。
|
||
*/
|
||
inline float DefaultSignalLineWidth(std::size_t finiteCount)
|
||
{
|
||
return (finiteCount == 1) ? kSinglePointSignalLineWidth : kDefaultSignalLineWidth;
|
||
}
|
||
|
||
/**
|
||
* @brief 「稀疏信号」缓存有效采样点时刻/取值的上限(有效点不超过该值才缓存)。
|
||
*
|
||
* 光标吸附需要“当前视界内有多少个数据点”以及“离光标最近的那个数据点”。
|
||
* 对有效点很少的信号(典型:单点信号、只有零星采样的列)事先把它们的时刻与取值缓存下来,
|
||
* 判定与查找都退化成二分 → 不必在鼠标移动时扫描整条采样数组(千万级信号无法承受)。
|
||
* 1024 个有效点 × 16 字节 ≈ 16 KB/条,可忽略。
|
||
*/
|
||
constexpr std::size_t kSparseTimesLimit = 1024;
|
||
|
||
/** @brief 网格 Y 轴自适应时为幅值区间补充的留白比例(相对区间跨度)。 */
|
||
constexpr double kLaneRangePadRatio = 0.06;
|
||
|
||
/**
|
||
* @brief 为幅值区间补充留白 —— **网格 Y 轴自适应的唯一留白规则**。
|
||
*
|
||
* ⚠️ 必须只有这一份实现:整段自适应(`TabConfig::ComputeLaneRange`)与
|
||
* 「选区 Y 轴自适应」(`PlotView::AutoRangeLaneToView`)都要调用它。
|
||
* 过去两处各自硬编码(6% / 5%),于是「点过适应视图后再打开 Y 轴自适应」会因为
|
||
* 留白不同而算出**不一样的区间** → 网格被无意义地重绘一次;反向(关掉自适应再点适应视图)
|
||
* 同样会变。统一留白后这两种操作在视界未变时都是**像素级无变化**。
|
||
*
|
||
* 区间退化(恒定值 / 单点信号)时给一个可读的跨度 `max(1, |值|×0.1)`,
|
||
* 而不是“几乎为 0”的区间(那会让幅值网格线挤成一团、数据点也看不出位置)。
|
||
*
|
||
* @param [IN,OUT] low 区间下限。 @param [IN,OUT] high 区间上限。
|
||
*/
|
||
inline void PadValueRange(double& low, double& high)
|
||
{
|
||
if (!std::isfinite(low) || !std::isfinite(high))
|
||
{
|
||
low = 0.0;
|
||
high = 1.0;
|
||
return;
|
||
}
|
||
if (low > high)
|
||
{
|
||
std::swap(low, high);
|
||
}
|
||
|
||
const double span = high - low;
|
||
if (span <= 0.0)
|
||
{
|
||
const double magnitude = std::max(1.0, std::fabs(low) * 0.1);
|
||
low -= magnitude;
|
||
high += magnitude;
|
||
return;
|
||
}
|
||
|
||
const double pad = span * kLaneRangePadRatio;
|
||
low -= pad;
|
||
high += pad;
|
||
}
|
||
|
||
/**
|
||
* @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 groupIndex = -1; /**< 所属分组下标。 */
|
||
int signalIndex = -1; /**< 组内信号下标。 */
|
||
|
||
/**
|
||
* @brief 判断引用是否指向有效位置。
|
||
* @return 两个下标均非负返回 true。
|
||
*/
|
||
bool IsValid() const { return groupIndex >= 0 && signalIndex >= 0; }
|
||
|
||
/**
|
||
* @brief 相等比较。
|
||
* @param [IN] other 另一引用。
|
||
* @return 指向同一信号返回 true。
|
||
*/
|
||
bool operator==(const SignalRef& other) const
|
||
{
|
||
return groupIndex == other.groupIndex && signalIndex == other.signalIndex;
|
||
}
|
||
|
||
/**
|
||
* @brief 不等比较。
|
||
* @param [IN] other 另一引用。
|
||
* @return 指向不同信号返回 true。
|
||
*/
|
||
bool operator!=(const SignalRef& other) const { return !(*this == other); }
|
||
};
|
||
|
||
/**
|
||
* @brief 「公式映射」的**恒等**写法:`f(t)`(= 原样输出采样值)。
|
||
*
|
||
* 空串与它语义完全相同(都表示“不做映射”)。
|
||
*/
|
||
inline const wchar_t* const kIdentityFormulaText = L"f(t)";
|
||
|
||
/**
|
||
* @brief 判断公式文本是否**等价于不映射**(空串或 `f(t)`)。
|
||
*
|
||
* ⚠️ 恒等公式会被当作“未启用”:它逐点恒等,但若被当成“有公式”,
|
||
* `DrawTransformOrderPreserving()` 就会返回 false(丢掉粗粒度包络层),
|
||
* 且自适应幅值范围会退化成**逐点扫描整条信号** —— 千万级采样时每帧几百毫秒
|
||
* (用户 2026-11-03 报告的“只是点进过公式输入框就卡死”的直接原因)。
|
||
*
|
||
* @param [IN] formula 公式文本。
|
||
* @return 恒等(不做映射)返回 true。
|
||
*/
|
||
inline bool FormulaIsIdentity(const std::wstring& formula)
|
||
{
|
||
return formula.empty() || formula == kIdentityFormulaText;
|
||
}
|
||
|
||
/**
|
||
* @brief 公式输入框的**非编辑态**文本(内含 `Y=` 前缀,用户 2026-11-03 要求)。
|
||
* @param [IN] formula 公式文本(空 = 未启用)。
|
||
* @return 形如 `Y=f(t)` / `Y=f(t)^2 + 1` 的文本。
|
||
*/
|
||
inline std::wstring FormulaDisplayText(const std::wstring& formula)
|
||
{
|
||
return std::wstring(L"Y=") +
|
||
(FormulaIsIdentity(formula) ? std::wstring(kIdentityFormulaText) : formula);
|
||
}
|
||
|
||
/**
|
||
* @brief 公式输入框的**编辑态**文本(只有公式本身,不带 `Y=` 前缀)。
|
||
* @param [IN] formula 公式文本(空 = 未启用)。
|
||
* @return 形如 `f(t)` / `f(t)^2 + 1` 的文本。
|
||
*/
|
||
inline std::wstring FormulaEditText(const std::wstring& formula)
|
||
{
|
||
return FormulaIsIdentity(formula) ? std::wstring(kIdentityFormulaText) : formula;
|
||
}
|
||
|
||
/**
|
||
* @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 「公式映射」表达式(`Y = <formula>`,自变量写作 `f(t)`;空 = 不映射)。
|
||
*
|
||
* 结算顺序为 **公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移 → 时间轴偏移**,
|
||
* 见 `ApplyDrawTransform`。表达式由 `Utilities/FormulaEval` 编译(带缓存),
|
||
* 非法表达式的取值原样返回采样值(不会让波形消失)。
|
||
*
|
||
* ⚠️ **`f(t)`(恒等映射)与空串等价**:`HasFormula()` 两者都返回 false。
|
||
* 否则用户只是“点进输入框又点出去”就会让样式带上一个恒等公式,
|
||
* 而公式会关闭粗粒度包络层、逼得自适应量程每帧逐点扫描整条信号
|
||
* (千万级采样时界面直接卡死,用户 2026-11-03 报告)。
|
||
*/
|
||
std::wstring formula;
|
||
|
||
/** @brief 是否**真的**启用了「公式映射」(`f(t)` 恒等映射不算)。 */
|
||
bool HasFormula() const { return !FormulaIsIdentity(formula); }
|
||
|
||
/** @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 「放大到采样点稀疏时自动启用点线」的运行时状态。
|
||
*
|
||
* 视界放大到采样点间距超过半个绘图区宽度时(此时光标也会吸附数据点),
|
||
* 线段模式会退化成「看不出数据点在哪」,因此自动切到「点线」给用户提示;
|
||
* 视界回退到阈值之外(如适应视图)时自动恢复**原来的**绘制方式。
|
||
*
|
||
* * `autoScatterActive` —— 当前绘制方式是否由该机制自动设置的;
|
||
* * `autoScatterSavedMode` —— 自动启用**之前**的绘制方式(退出时恢复它);
|
||
* * `autoScatterUserOverride` —— 自动启用期间用户**手动**改过绘制方式:
|
||
* 此时退出不再恢复(用户的选择优先,用户明确要求)。
|
||
*/
|
||
bool autoScatterActive = false;
|
||
DrawMode autoScatterSavedMode = DrawMode::LINE;
|
||
bool autoScatterUserOverride = false;
|
||
};
|
||
|
||
/**
|
||
* @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)
|
||
{
|
||
// 「公式映射」最先结算(用户 2026-11-02 明确要求:公式 → 系数 → 偏移)。
|
||
// ⚠️ 表达式编译结果按样式对象缓存,**不会**逐采样点重新解析。
|
||
if (!style.formula.empty())
|
||
{
|
||
value = EvaluateFormula(style.formula, 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)
|
||
{
|
||
// ⚠️ 「公式映射」不保证保序(`x * x` 在负半轴递减),因此一律按非保序处理,
|
||
// 让包络绘制回退到逐采样点变换(粗粒度分组层只存 min/max,非保序会算错)。
|
||
return (style.gain > 0.0f) && !style.absolute && !style.mirrorVertical && !style.HasFormula();
|
||
}
|
||
|
||
/**
|
||
* @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 一条时间轴(时间向量 + 它的取值范围与单调性)。
|
||
*
|
||
* 时间轴**独立于信号**:同一分组内多条信号可以共用同一条时间轴(最常见的 CSV 单时间轴文件、
|
||
* MF4 里同一通道组内的全部通道),也可以各自使用不同的时间轴(多 `time` 列的 CSV、
|
||
* MF4 的不同通道组)。
|
||
*/
|
||
struct TimeAxis
|
||
{
|
||
std::wstring name; /**< 时间列名(源文件中的原样名称,如 `time`)。 */
|
||
std::vector<double> time; /**< 时间数值,缺失处为 NaN。 */
|
||
double minTime = 0.0; /**< 有限时间最小值。 */
|
||
double maxTime = 0.0; /**< 有限时间最大值。 */
|
||
bool monotonic = true; /**< 时间是否单调不减(决定能否用下标区间做二分)。 */
|
||
std::size_t finiteCount = 0; /**< 有效时间点数量。 */
|
||
|
||
/** @brief 时间点数量。 */
|
||
std::size_t Count() const { return time.size(); }
|
||
};
|
||
|
||
/**
|
||
* @brief 单条信号的元数据与采样值(纯数据,不含可见性配置)。
|
||
*
|
||
* ⚠️ 信号**只有一个名称字段** `name` —— 它就是界面展示用的名字,也是用户重命名后写入的地方
|
||
*(加载时取列名 / 通道名,之后不再保留“原始名称”的第二份副本:应用从不靠名称定位信号,
|
||
* 定位一律走 `SignalRef` 下标)。
|
||
*
|
||
* ⚠️ 信号的时间轴由 `axisIndex` 指向所属分组的时间轴列表,
|
||
* 因此采样值本身不需要、也不应该携带时间信息。
|
||
*/
|
||
struct SignalData
|
||
{
|
||
std::wstring name; /**< 信号名称(加载时取列名 / 通道名;用户可在界面上重命名)。 */
|
||
std::size_t axisIndex = 0; /**< 所属时间轴在分组内的下标。 */
|
||
/**
|
||
* @brief 取值 → 文本的枚举表(源格式自带时才有,例如 MDF4 `cc_type = 7`)。
|
||
*
|
||
* ⚠️ 这**不是**单位、也不是注释:它只回答「这个数值代表什么状态」。
|
||
* 空表示该信号没有枚举信息,界面必须与以前完全一致(不加任何尾缀)。
|
||
* 启用光标时,取值标注才在数值之后追加 ` = <枚举文本>`。
|
||
*/
|
||
SignalEnumText enumText;
|
||
std::vector<double> values; /**< 采样值,缺失处为 NaN。 */
|
||
double minValue = 0.0; /**< 有限采样值最小值。 */
|
||
double maxValue = 0.0; /**< 有限采样值最大值。 */
|
||
std::size_t finiteCount = 0; /**< 有限采样点数量。 */
|
||
|
||
/**
|
||
* @brief 有效采样点很少时的**全部有效点**(按时间升序;有效点很多时为空)。
|
||
*
|
||
* 仅当 `finiteCount <= kSparseTimesLimit` 时在加载/插入信号时就地填充,用于:
|
||
* * 光标吸附:直接二分得到“视界内有几个数据点”“离光标最近的数据点”;
|
||
* * 单点信号的初始视图:Centering 需要那个点的时刻与取值。
|
||
* ⚠️ 不要在鼠标移动时改成扫描整条采样数组来找有效点(千万级信号承受不住)。
|
||
*
|
||
* 说明:`sparseTimes.size() == 1` 即“只有一个数据点的信号”。
|
||
*/
|
||
std::vector<double> sparseTimes;
|
||
|
||
/** @brief `sparseTimes` 对应的取值(同序)。 */
|
||
std::vector<double> sparseValues;
|
||
|
||
/**
|
||
* @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 一个**信号分组**:一次加载产生的全部信号,以及它们使用的若干条时间轴。
|
||
*
|
||
* ⚠️ 「分组」是应用内的逻辑容器,**不是文件**:加载完成后二者已无关系,
|
||
* 因此这里**不保存文件路径**(路径只在加载那一刻用于错误提示,之后毫无用处)。
|
||
* 分组的默认名称取源文件名(便于用户辨认来源),用户可以重命名。
|
||
*
|
||
* 一个分组可以包含**多条时间轴**(CSV 的多个 `time` 列、MF4 的多个通道组),
|
||
* 每条信号通过 `SignalData::axisIndex` 指向自己那条。
|
||
*/
|
||
struct SignalGroup
|
||
{
|
||
std::wstring name; /**< 分组名(加载时取源文件名、含扩展名;用户可重命名)。 */
|
||
std::vector<TimeAxis> axes; /**< 该分组使用的时间轴(至少一条)。 */
|
||
std::vector<SignalData> signals; /**< 该分组包含的信号。 */
|
||
|
||
/**
|
||
* @brief 取某条信号所属的时间轴(下标越界时回退到第一条,保证永不返回悬空引用)。
|
||
* @param [IN] signal 目标信号。
|
||
* @return 时间轴引用;分组没有任何时间轴时返回内部静态空时间轴。
|
||
*/
|
||
const TimeAxis& AxisOf(const SignalData& signal) const
|
||
{
|
||
return AxisAt(signal.axisIndex);
|
||
}
|
||
|
||
/**
|
||
* @brief 按下标取时间轴(越界时回退到第一条)。
|
||
* @param [IN] index 时间轴下标。
|
||
* @return 时间轴引用。
|
||
*/
|
||
const TimeAxis& AxisAt(std::size_t index) const
|
||
{
|
||
static const TimeAxis kEmptyAxis;
|
||
if (axes.empty())
|
||
{
|
||
return kEmptyAxis;
|
||
}
|
||
return axes[(index < axes.size()) ? index : 0];
|
||
}
|
||
|
||
/**
|
||
* @brief 计算本分组全部时间轴的时间范围并集。
|
||
* @param [OUT] low 下限。 @param [OUT] high 上限。
|
||
* @return 至少有一条时间轴有有效时间返回 true。
|
||
*/
|
||
bool TimeRange(double& low, double& high) const
|
||
{
|
||
low = 0.0;
|
||
high = 0.0;
|
||
bool valid = false;
|
||
for (const TimeAxis& axis : axes)
|
||
{
|
||
if (axis.finiteCount == 0)
|
||
{
|
||
continue;
|
||
}
|
||
if (!valid)
|
||
{
|
||
low = axis.minTime;
|
||
high = axis.maxTime;
|
||
valid = true;
|
||
}
|
||
else
|
||
{
|
||
low = std::min(low, axis.minTime);
|
||
high = std::max(high, axis.maxTime);
|
||
}
|
||
}
|
||
return valid;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @brief 由一个**格式无关**的加载结果构建应用内的信号分组。
|
||
*
|
||
* 职责划分(新增文件格式时只需要写一个加载器,本函数完全不用改):
|
||
* * 加载器:文件 → `LoadedSource`(若干「时间轴块」,纯格子,无应用语义);
|
||
* * 本函数:`LoadedSource` → `SignalGroup`(时间轴**去重合并**、信号命名、
|
||
* 调色板取色、统计与粗粒度包络层)。
|
||
*
|
||
* 时间轴去重:内容完全相同(长度与全部数值逐位相同)的时间轴只保留一条,
|
||
* 于是 CSV 里多个同刻度的 `time` 列、MF4 里多个同基线的通道组不会重复占内存,
|
||
* 也不会让信号各自持有实质上相同的轴。
|
||
*
|
||
* 该函数**不访问 DataSet**(只依赖传入数据与调色板基准序号),因此可以在工作线程上执行:
|
||
* 大文件的解析与列重组占整个加载耗时的大头,放到工作线程后主线程只需做一次 O(1) 的
|
||
* 移动入队,加载过程中的界面不会卡住。
|
||
*
|
||
* @param [IN,OUT] source 加载结果(列会被**移动**进信号,零拷贝)。
|
||
* @param [IN] paletteBase 调色板起始序号(通常传入当前已有信号数)。
|
||
* @param [OUT] group 构建结果。
|
||
* @param [OUT] error 失败原因描述(成功时清空)。
|
||
* @param [IN] progress 进度回调(0-1),返回 false 表示取消,可为空。
|
||
* @return 构建成功返回 true。
|
||
*/
|
||
bool BuildSignalGroup(LoadedSource&& source, std::size_t paletteBase, SignalGroup& group,
|
||
std::wstring& error, const LoadProgress& progress = LoadProgress());
|
||
|
||
/**
|
||
* @brief 数据集合:统一管理全部已加载的**信号分组**,与视图配置无关。
|
||
*/
|
||
class DataSet
|
||
{
|
||
public:
|
||
/**
|
||
* @brief 追加一个已构建好的信号分组(仅做一次移动,不拷贝采样数据)。
|
||
* @param [IN,OUT] group 信号分组(内部缓冲会被移走)。
|
||
* @param [OUT] groupIndex 新分组下标。
|
||
* @return 追加成功返回 true。
|
||
*/
|
||
bool AddGroup(SignalGroup&& group, int& groupIndex);
|
||
|
||
/**
|
||
* @brief 在某个分组末尾追加一条**新时间轴**(「重采样」派生的信号使用它)。
|
||
*
|
||
* 重采样产生的是**新的时间刻度**,无法复用源信号的时间轴;追加后该分组的
|
||
* `axes` 多一条,返回它的下标,供随后 `InsertDerivedSignal` 的 `axisIndex` 使用。
|
||
*
|
||
* @param [IN] groupIndex 目标分组下标。
|
||
* @param [IN,OUT] axis 时间轴(内部缓冲会被移走)。
|
||
* @param [OUT] axisIndex 新时间轴在分组内的下标。
|
||
* @return 追加成功返回 true(轴为空/分组越界时返回 false)。
|
||
*/
|
||
bool AppendGroupAxis(int groupIndex, TimeAxis&& axis, std::size_t& axisIndex);
|
||
|
||
/**
|
||
* @brief 在指定分组的指定信号**下方**插入一条新信号(派生信号)。
|
||
*
|
||
* 新信号会占据 `afterSignalIndex + 1` 的位置,该位置及其后的信号下标整体后移一位;
|
||
* 调用方必须紧接着对每个标签页调用 `TabConfig::RemapAfterSignalInsert()` 修正引用。
|
||
* 采样值会被就地统计(min/max/finiteCount 与粗粒度包络层),显示外观按「全局信号序号」
|
||
* 取调色板颜色(与普通新信号一致)。
|
||
*
|
||
* @param [IN] groupIndex 目标分组下标。
|
||
* @param [IN] afterSignalIndex 插入位置的上一条信号下标。
|
||
* @param [IN,OUT] signal 待插入的信号(values 会被统计,其余字段原样使用)。
|
||
* @param [OUT] newSignalIndex 新信号在本组内的下标。
|
||
* @return 插入成功返回 true。
|
||
*/
|
||
bool InsertDerivedSignal(int groupIndex, int afterSignalIndex, SignalData&& signal,
|
||
int& newSignalIndex);
|
||
|
||
/**
|
||
* @brief **破坏性**截取一条信号:只保留 `[low, high]` 内的采样,其余立即释放。
|
||
*
|
||
* 用于「实验性功能 → 选区截取 → 原始信号截取」(用户 2026-11-03 明确要求):
|
||
* * 同时裁剪该信号所属的**时间轴**(`axes[signal.axisIndex].time`)与采样值数组;
|
||
* * 两者按下标一一对应,因此截取后仍保持对齐;
|
||
* * 用「新建 + 交换 + `shrink_to_fit`」释放多余容量(`erase` 不会归还内存);
|
||
* * 重算统计(min/max/finiteCount 与粗粒度包络层),并重建该轴的
|
||
* `minTime/maxTime/finiteCount/monotonic`。
|
||
*
|
||
* ⚠️ 该操作**不可撤销**;区间内有效采样不足两个时返回 false 且不改动任何数据。
|
||
*
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] signalIndex 信号下标。
|
||
* @param [IN] low 保留区间左端(时间,秒)。
|
||
* @param [IN] high 保留区间右端(时间,秒)。
|
||
* @return 截取成功返回 true。
|
||
*/
|
||
bool ClipSignalRange(int groupIndex, int signalIndex, double low, double high);
|
||
|
||
/**
|
||
* @brief 让某条信号**独占一条时间轴**(复制一条新轴并把信号指向它)。
|
||
*
|
||
* 同组的信号常常共享同一条时间轴(合并网格、同文件多通道等)。
|
||
* 破坏性截取若要裁剪时间轴,就必须先让这条信号独占自己的轴,
|
||
* 否则会把其它信号的时间基准一起改短、导致它们数据错位
|
||
* (用户 2026-11-03 报告:"多个信号同时截取时截取出来的信号并不对")。
|
||
*
|
||
* 若该信号的时间轴**已经**被它独占,本函数不做任何事并返回 true。
|
||
*
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] signalIndex 信号下标。
|
||
* @param [OUT] axisIndex 该信号(新)轴的下标。
|
||
* @return 成功返回 true。
|
||
*/
|
||
bool ForkAxisForSignal(int groupIndex, int signalIndex, std::size_t& axisIndex);
|
||
|
||
/**
|
||
* @brief 把一条信号移动到**另一个分组**(追加到目标分组末尾)。
|
||
*
|
||
* 用于信号右键菜单的「移动到…」(用户 2026-11-03 要求):把信号从原分组摘出来、
|
||
* 插入另一个分组(含「新建分组」)。
|
||
*
|
||
* ⚠️ **时间轴必须随信号一起走**:`SignalData::axisIndex` 指向**所属分组**的
|
||
* `axes`,跨组移动后原下标毫无意义。这里的处理是:
|
||
* * 该信号的轴**只被它自己使用** → 把轴 move 到目标分组(零拷贝,并从源分组删掉,
|
||
* 源分组内其余信号的 `axisIndex` 若大于被删下标则整体前移一位);
|
||
* * 与源分组内其它信号**共享** → 复制一条到目标分组(不能抽走共享轴)。
|
||
*
|
||
* ⚠️ 调用方必须紧接着对每个标签页调用
|
||
* `TabConfig::RemapAfterSignalMove(from, to, visible)` 修正引用,
|
||
* 并在之后 `SyncToDataSet`。源分组即便被移空也**不自动删除**(删除会让分组下标整体左移,
|
||
* 牵动所有引用);需要的话由调用方另行处理。
|
||
*
|
||
* @param [IN] fromGroup 源分组下标。
|
||
* @param [IN] fromSignal 源分组内的信号下标。
|
||
* @param [IN] toGroup 目标分组下标(必须已存在,且与源分组不同)。
|
||
* @param [OUT] newSignalIndex 移动后该信号在目标分组内的下标(= 移动前目标组信号数)。
|
||
* @return 移动成功返回 true。
|
||
*/
|
||
bool MoveSignalToGroup(int fromGroup, int fromSignal, int toGroup, int& newSignalIndex);
|
||
|
||
/**
|
||
* @brief 把一条信号移动到**任意分组内的任意位置**(组内重排 / 跨组移动的统一入口)。
|
||
*
|
||
* 组内重排(`fromGroup == toGroup`)只调整顺序,时间轴完全不动;
|
||
* 跨组移动按 `MoveSignalToGroup` 的规则搬运时间轴。
|
||
*
|
||
* @param [IN] fromGroup 源分组下标。
|
||
* @param [IN] fromSignal 源分组内的信号下标。
|
||
* @param [IN] toGroup 目标分组下标。
|
||
* @param [IN] toSignalIndex 移动后该信号在目标分组内的下标(负数或越界表示追加到末尾)。
|
||
* @param [OUT] newSignalIndex 移动后的实际下标。
|
||
* @return 移动成功返回 true。
|
||
*/
|
||
bool MoveSignal(int fromGroup, int fromSignal, int toGroup, int toSignalIndex,
|
||
int& newSignalIndex);
|
||
|
||
/**
|
||
* @brief 新建一个分组,并把一条信号(连同它的时间轴)搬进去。
|
||
*
|
||
* ⚠️ **不能写成「先 `AddGroup` 再 `MoveSignal`」**:`AddGroup` 拒绝“既没有信号、
|
||
* 也没有时间轴”的空分组(它用于加载器/派生信号,需要完整内容)⇒ 先建空组必然
|
||
* 失败返回,表现出来就是“点了「新建分组」毫无反应”(用户 2026-11-03 报告,
|
||
* 其推测——空分组被当作不需要而拒收——完全正确)。
|
||
* 本函数把“建组 + 搬运”做成一次原子操作:先推入空组,再搬信号进来,失败则回滚。
|
||
*
|
||
* @param [IN] fromGroup 源分组下标。
|
||
* @param [IN] fromSignal 源分组内的信号下标。
|
||
* @param [IN] groupName 新分组名称(调用方负责去重)。
|
||
* @param [OUT] newGroupIndex 新分组下标(恒为原分组数的位置)。
|
||
* @param [OUT] newSignalIndex 该信号在新分组内的下标(恒为 0)。
|
||
* @return 成功返回 true。
|
||
*/
|
||
bool MoveSignalToNewGroup(int fromGroup, int fromSignal, const std::wstring& groupName,
|
||
int& newGroupIndex, int& newSignalIndex);
|
||
|
||
/**
|
||
* @brief 移除指定下标的分组,并立即释放其占用的全部采样内存。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 移除成功返回 true。
|
||
*/
|
||
bool RemoveGroup(int groupIndex);
|
||
|
||
/**
|
||
* @brief 从某个分组中移除一条信号,并立即释放它的采样内存。
|
||
*
|
||
* 该信号之后的所有信号下标整体前移一位,因此调用方必须紧接着对每个标签页调用
|
||
* `TabConfig::RemapAfterSignalRemoval()`,并用 `RemapSelectionAfterSignalRemoval()`
|
||
* 修正共享选中项。使用者一旦确认不再需要该信号即可调用,本调用后其采样缓冲立即析构。
|
||
*
|
||
* @param [IN] groupIndex 信号所属分组下标。
|
||
* @param [IN] signalIndex 组内信号下标。
|
||
* @return 移除成功返回 true。
|
||
*/
|
||
bool RemoveSignal(int groupIndex, int signalIndex);
|
||
|
||
/**
|
||
* @brief 在数据层移除某条信号之前,按旧下标修正选中引用。
|
||
* @param [IN] groupIndex 信号所属分组下标。
|
||
* @param [IN] signalIndex 即将被移除的信号下标。
|
||
*/
|
||
void RemapSelectionAfterSignalRemoval(int groupIndex, int signalIndex);
|
||
|
||
/**
|
||
* @brief 判断数据集合是否为空。
|
||
* @return 未加载任何分组返回 true。
|
||
*/
|
||
bool Empty() const { return m_groups.empty(); }
|
||
|
||
/**
|
||
* @brief 获取分组数量。
|
||
* @return 分组数量。
|
||
*/
|
||
std::size_t GroupCount() const { return m_groups.size(); }
|
||
|
||
/**
|
||
* @brief 获取信号总数。
|
||
* @return 信号总数。
|
||
*/
|
||
std::size_t SignalCount() const;
|
||
|
||
/**
|
||
* @brief 获取全部分组(只读)。
|
||
* @return 分组集合。
|
||
*/
|
||
const std::vector<SignalGroup>& Groups() const { return m_groups; }
|
||
|
||
/**
|
||
* @brief 按下标获取分组。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 分组指针,下标非法时返回 nullptr。
|
||
*/
|
||
const SignalGroup* GroupAt(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 重命名某条信号(写入它的**唯一**名称字段 `SignalData::name`)。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] signalIndex 组内信号下标。
|
||
* @param [IN] name 新名称;空串(或仅空白)忽略,避免把名称抹空。
|
||
* @return 名称确实发生变化返回 true。
|
||
*/
|
||
bool RenameSignal(int groupIndex, int signalIndex, const std::wstring& name);
|
||
|
||
/**
|
||
* @brief 重命名某个分组(写入它的**唯一**名称字段 `SignalGroup::name`)。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] name 新名称;空串(或仅空白)忽略,避免把名称抹空。
|
||
* @return 名称确实发生变化返回 true。
|
||
*/
|
||
bool RenameGroup(int groupIndex, const std::wstring& name);
|
||
|
||
/**
|
||
* @brief 解析信号引用。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 信号数据指针,引用无效时返回 nullptr。
|
||
*/
|
||
const SignalData* Resolve(const SignalRef& ref) const;
|
||
|
||
/**
|
||
* @brief 取某条信号所属的时间轴。
|
||
* @param [IN] ref 信号引用。
|
||
* @return 时间轴指针,引用无效时返回 nullptr。
|
||
*/
|
||
const TimeAxis* Axis(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 「公式映射」信号的**绘制值**缓存(值域 + 逐分组包络)。
|
||
*
|
||
* ⚠️⚠️ **这是「启用公式后界面卡死」的唯一根治点**(用户 2026-11-04 报告):
|
||
* 公式不保序(`x*x`、`1/x`),幅值自适应与包络带都不能由原始 `min/max` 推导,
|
||
* 旧实现每条路径各写一次「逐点扫描整条信号」:
|
||
* * `TabConfig::ComputeLaneRange` —— 每次幅值范围重算都扫一遍;
|
||
* * `PlotView::AcquireFormulaEnvelope` —— 首次绘制包络带再扫一遍。
|
||
* 2580 万采样 × 两条路径 ≈ **3.1 秒**(实测 1.6 s + 1.5 s),而
|
||
* 「确认公式编辑 / 适应视图 / 拖拽排序 / 组复选框」都会让幅值范围指纹变化
|
||
* ⇒ 每次都重新付一遍,表现为「整个应用卡住完全无法交互」。
|
||
*
|
||
* 现在把「值域」与「分组包络」合并成**一次全量扫描**并缓存:
|
||
* * 键 = 信号对象 + 采样数组 + 采样数 + 公式文本 + 影响数值的样式字段;
|
||
* * 命中后两条路径都退化成 O(1) / O(分组数)。
|
||
*
|
||
* ⚠️ 只在 **UI 线程**读写(`mutable` + `const` 方法);后台派生线程只读
|
||
* `SignalStyle` 并走 `ApplyDrawTransform`,不会碰到这个缓存。
|
||
*/
|
||
struct FormulaTransformCache
|
||
{
|
||
const SignalData* signal = nullptr; /**< 信号对象(区分不同信号)。 */
|
||
const double* values = nullptr; /**< `values.data()`(区分重新加载/截取)。 */
|
||
std::size_t count = 0; /**< 参与统计的采样数。 */
|
||
std::wstring formula; /**< 公式文本。 */
|
||
float gain = 1.0f; /**< 公式之后的系数。 */
|
||
bool absolute = false; /**< 公式之后的绝对值。 */
|
||
bool mirrorVertical = false; /**< 公式之后的垂直镜像。 */
|
||
float offsetY = 0.0f; /**< 公式之后的 Y 偏移。 */
|
||
|
||
bool ready = false; /**< 是否已成功建立。 */
|
||
double low = 0.0; /**< 绘制值下限(= 幅值自适应的下限)。 */
|
||
double high = 0.0; /**< 绘制值上限。 */
|
||
std::size_t stride = 0; /**< 每个分组覆盖的采样数。 */
|
||
std::vector<double> minimums; /**< 每组的绘制值下限(无有效值 = NaN)。 */
|
||
std::vector<double> maximums; /**< 每组的绘制值上限。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 取得(必要时**建立一次**)某信号的公式变换缓存。
|
||
*
|
||
* 只在 `style.HasFormula()`(排除恒等 `f(t)`)且采样非空时返回非空。
|
||
* 未命中时立即做**一次**全量扫描:同时算出绘制值域与逐分组极值。
|
||
*
|
||
* ⚠️ 返回的指针在下一次 `FormulaTransformFor()` 建立**新条目**后可能失效
|
||
* (`std::vector` 扩容);调用方只能在本表达式内即时使用,不要长期保存。
|
||
*
|
||
* @param [IN] signal 信号数据(提供采样)。
|
||
* @param [IN] style 该信号的外观(提供公式与后续数值变换)。
|
||
* @return 缓存条目指针;公式为空/非法或没有采样时返回 nullptr。
|
||
*/
|
||
const FormulaTransformCache* FormulaTransformFor(const SignalData& signal,
|
||
const SignalStyle& style) const;
|
||
|
||
/**
|
||
* @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] groupIndex 即将被移除的分组下标。
|
||
*/
|
||
void RemapSelectionAfterGroupRemoval(int groupIndex);
|
||
|
||
/**
|
||
* @brief 校验选中引用是否仍指向存在的信号,失效则取消选中。
|
||
* @return 选中状态发生变化返回 true。
|
||
*/
|
||
bool ValidateSelection();
|
||
|
||
private:
|
||
std::vector<SignalGroup> m_groups; /**< 已加载信号分组集合。 */
|
||
SignalRef m_selected; /**< 当前选中信号(跨标签页共享)。 */
|
||
/**
|
||
* @brief 「公式映射」信号的绘制值缓存(见 `FormulaTransformFor`)。
|
||
*
|
||
* `mutable`:查询缓存是逻辑上的只读操作;只在 UI 线程写。
|
||
* ⚠️ 用 `std::deque`(而不是 `vector`):`emplace_back` **不会**移动已有元素,
|
||
* 因此 `FormulaTransformFor` 返回的指针在后续新建其它信号的条目时依然有效。
|
||
*/
|
||
mutable std::deque<FormulaTransformCache> m_formulaTransforms;
|
||
};
|
||
|
||
/**
|
||
* @brief 一个实验性功能浮动面板的 UI 状态(展开开关 + 左上角位置)。
|
||
*
|
||
* 四类实验性功能(边沿计数 / 边沿计数(中心轴)/ 均值滤波 / 弱峰调整)各自持有一个,
|
||
* 因此 `Lane` 里不必再重复 12 个(4×3)同形字段 —— 这属于「同一含义被拆成多份」的冗余。
|
||
* 面板的**每信号参数**放在 `Lane::*Items` 里,与这里的位置/展开状态分开。
|
||
*/
|
||
struct LanePanelState
|
||
{
|
||
bool open = true; /**< 功能面板是否显示(功能关闭时置 false)。 */
|
||
/**
|
||
* @brief 面板是否被**折起**(只显示标题栏)。
|
||
*
|
||
* 与 `open` 的区别:`open == false` 表示该功能本身没有开启(面板完全不存在);
|
||
* `collapsed == true` 表示功能仍开启、但内容被收起,只留标题栏(点击标题栏左侧的
|
||
* 三角指示器切换,双击标题栏也可展开)。
|
||
*/
|
||
bool collapsed = false;
|
||
double x = -1.0; /**< 面板左上角横坐标(客户区像素;<0 表示自动定位)。 */
|
||
double y = -1.0; /**< 面板左上角纵坐标(<0 表示自动定位)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 一个波形网格(纵向泳道),可容纳多条信号叠加显示。
|
||
*/
|
||
struct Lane
|
||
{
|
||
/**
|
||
* @brief 「均值滤波」中某个信号自己的参数。
|
||
*
|
||
* 仅参与列表中的信号才会被勾选预览;参数按信号保存,
|
||
* 因此同一条网格内不同信号可以使用不同的均值点数与迭代次数。
|
||
* 「边沿计数(中心轴)」的平滑参数与它**完全同构**(同一套命名与作用),
|
||
* 见 `EdgeCenterItem`。
|
||
*/
|
||
struct MeanFilterItem
|
||
{
|
||
SignalRef ref; /**< 目标信号。 */
|
||
int points = 5; /**< 均值点数 X(滑平半径,窗口 = 2X+1)。 */
|
||
int iterations = 1; /**< 迭代次数 N(重复滤波次数,至少 1)。 */
|
||
/**
|
||
* @brief 「均值点数」滑块的**上限**(用户可点击滑块右侧数值改成其它正整数)。
|
||
*
|
||
* 滑块量程 = `1 … maxPoints`,`points` 恒被夹在该区间内。
|
||
*/
|
||
int maxPoints = 50;
|
||
/** @brief 「迭代次数」滑块的**上限**(同上,量程 = `1 … maxIterations`)。 */
|
||
int maxIterations = 10;
|
||
/**
|
||
* @brief 参数行是否被**收起**(只保留信号色叠底的标题栏)。
|
||
*
|
||
* 仅「均值滤波」面板使用:勾选后标题栏左侧出现展开三角,单击它切换收起/展开。
|
||
* 未勾选时没有参数行,本字段无意义。
|
||
*/
|
||
bool collapsed = false;
|
||
};
|
||
|
||
/** 中心轴边沿计数的平滑参数:与均值滤波共用同一结构与同一套语义。 */
|
||
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)。 */
|
||
/**
|
||
* @brief 参数行是否被**收起**(只保留信号色叠底的条目栏)。
|
||
*
|
||
* 「弱峰调整」面板自 2026-11-02 起与「均值滤波 / 中心轴边沿计数」同一套可滚动列表:
|
||
* 勾选后条目栏左侧出现展开三角,单击它切换收起 / 展开。未勾选时本字段无意义。
|
||
*/
|
||
bool collapsed = false;
|
||
};
|
||
|
||
/**
|
||
* @brief 「自由标记」中一个**固定**的数据点。
|
||
*
|
||
* 用户开启「自由标记」后,网格内会跟随鼠标出现一条纵向光标;左键单击一次就把当时
|
||
* 就近的那个数据点固定下来。固定点按信号分组显示在浮动面板里,
|
||
* 同一信号内的条目按**时间从左到右**排列(面板里从上到下)。
|
||
*
|
||
* ⚠️ `time` 存的是**显示时刻**(= 画在 `TimeToX(time)` 处),
|
||
* 与 `TabConfig::CursorTime()` 同一口径;采样查询要用
|
||
* `SampleDisplayedSignalAt`(它内部会减掉 `style.offsetTime`)。
|
||
*/
|
||
struct FreeMarkerItem
|
||
{
|
||
SignalRef ref; /**< 目标信号。 */
|
||
double time = 0.0; /**< 显示时刻(秒)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 「重采样」中某个信号自己的参数。
|
||
*
|
||
* 该功能把**非固定频率**采样的信号规整成固定间隔采样(超分 / 降采样),
|
||
* 再把结果派生为一条新信号。参数按信号保存,同一网格内不同信号可以用不同间隔。
|
||
*
|
||
* ⚠️ `interval` 只在**首次勾选**(`initialized == false`)时初始化为该信号的最小
|
||
* 数据间隔;之后用户手动输入的间隔不会被重新初始化覆盖。
|
||
*/
|
||
struct ResampleItem
|
||
{
|
||
SignalRef ref; /**< 目标信号。 */
|
||
double interval = 0.0; /**< 重采样间隔(秒,> 0)。 */
|
||
/** @brief `interval` 是否已经按「该信号最小数据间隔」初始化过。 */
|
||
bool initialized = false;
|
||
/**
|
||
* @brief 该信号的**最大 / 最小**数据间隔(相邻有效采样点时间差的极值)。
|
||
*
|
||
* ⚠️ 在勾选该信号时由 `PlotView` 一次性算好并缓存(千万级信号逐帧重算会卡死),
|
||
* 面板绘制只读这两个字段。数据加载后不再变化,因此无需失效。
|
||
*/
|
||
double maxInterval = 0.0;
|
||
double minInterval = 0.0;
|
||
/** @brief 参数行是否被**收起**(只保留信号色叠底的条目栏)。 */
|
||
bool collapsed = false;
|
||
};
|
||
|
||
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;
|
||
|
||
/** 边沿计数浮动面板的 UI 状态(展开 + 位置)。 */
|
||
LanePanelState edgePanel;
|
||
|
||
/**
|
||
* @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;
|
||
}
|
||
|
||
/** 中心轴边沿计数浮动面板的 UI 状态(展开 + 位置)。 */
|
||
LanePanelState edgeCenterPanel;
|
||
|
||
/**
|
||
* @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;
|
||
}
|
||
|
||
/** 均值滤波浮动面板的 UI 状态(展开 + 位置)。 */
|
||
LanePanelState meanFilterPanel;
|
||
|
||
/**
|
||
* @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;
|
||
}
|
||
|
||
/** 弱峰调整浮动面板的 UI 状态(展开 + 位置)。 */
|
||
LanePanelState weakPeakPanel;
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 自由标记」开关(功能总开关)。
|
||
*
|
||
* 开启后本网格参与自由标记:鼠标**贴近信号曲线**(判据 = 到**画出来的折线**的垂直距离 ≤
|
||
* `kSignalHitTolerance`,与常规触发曲线高亮同源)时标注离鼠标最近的数据点,
|
||
* 左键单击把该数据点固定下来(固定点带 ✕ 可单独移除;浮动面板按信号分组,
|
||
* 每组一行**表头** + 五列数据:标记值 / 枚举文本 / 增量 / 时间点 / 时间间隔)。
|
||
* 默认**不**显示网格私有纵向光标。
|
||
*/
|
||
bool freeMarker = false;
|
||
|
||
/**
|
||
* @brief 「自由标记」的**光标模式**(浮动面板底部「自由标记点选取」切换按钮)。
|
||
*
|
||
* `false`(默认):鼠标就是**普通鼠标** —— 不显示任何取值预览、不画纵向光标,
|
||
* 按下/拖动完全走常规语义(平移 / 框选 / 拖信号),**不会**固定数据点。
|
||
* `true`:进入「自由标记点选取」—— 网格内始终跟随鼠标画一条**仅限本网格**的纵向光标
|
||
*(上下各一个三角把手、不显示时间读数),鼠标接触到信号曲线时标注离鼠标最近的数据点,
|
||
* 左键单击把该数据点固定下来(此时左键**不再**用于平移/框选/拖信号)。
|
||
*/
|
||
bool freeMarkerCursor = false;
|
||
|
||
/**
|
||
* @brief 固定点的数值显示是否附带**括号内增量**(浮动面板的「显示增量」开关)。
|
||
*
|
||
* 默认 `false`:胶囊只显示当前值。⚠️ 只影响**已固定点**的显示,
|
||
* 不影响「自由标记点选取」模式下跟随鼠标的预览胶囊,也不影响面板统计列。
|
||
*/
|
||
bool freeMarkerShowDelta = false;
|
||
|
||
/**
|
||
* @brief 固定点的数值显示是否附带**枚举文本**(浮动面板的「显示枚举文本」开关)。
|
||
*
|
||
* 默认 `false`:胶囊只显示当前值。⚠️ 只影响**已固定点**的显示,
|
||
* 不影响跟随鼠标的预览胶囊,也不影响面板统计列(那一列只看有没有枚举表)。
|
||
*/
|
||
bool freeMarkerShowEnum = false;
|
||
|
||
/**
|
||
* @brief 已固定的自由标记点(**未排序**,按加入顺序)。
|
||
*
|
||
* 面板内的显示顺序是按「网格内信号顺序 → 该信号内按时间升序」临时计算的,
|
||
* 这样删除某一项不会打乱其它项的下标。同一信号同一时刻不会被重复加入。
|
||
*/
|
||
std::vector<FreeMarkerItem> freeMarkerItems;
|
||
|
||
/** 取某信号在某时刻的固定点下标(未找到返回 -1)。 */
|
||
int FindFreeMarker(const SignalRef& ref, double time) const;
|
||
|
||
/**
|
||
* @brief 加入一个固定点(同一信号同一时刻已存在时不重复加入)。
|
||
* @return 新加入返回 true。
|
||
*/
|
||
bool AddFreeMarker(const SignalRef& ref, double time);
|
||
|
||
/** 移除某信号在某时刻的固定点(按时刻容差匹配)。 */
|
||
void RemoveFreeMarker(const SignalRef& ref, double time);
|
||
|
||
/** 移除某个下标对应的固定点。 */
|
||
void RemoveFreeMarkerAt(std::size_t index);
|
||
|
||
/** 移除某信号的**全部**固定点。 */
|
||
void RemoveFreeMarkersOf(const SignalRef& ref);
|
||
|
||
/** 某信号是否已有固定点。 */
|
||
bool HasFreeMarkers(const SignalRef& ref) const
|
||
{
|
||
for (const FreeMarkerItem& item : freeMarkerItems)
|
||
{
|
||
if (item.ref == ref)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** 自由标记浮动面板的 UI 状态(展开/折起 + 位置)。 */
|
||
LanePanelState freeMarkerPanel;
|
||
|
||
/**
|
||
* @brief 网格内固定数据点(纵向细线 / 数据点 / 取值胶囊 / 行尾 ✕)的显示不透明度。
|
||
*
|
||
* 0 = 完全透明(看不见),1 = 完全不透明(默认)。由浮动面板底部**总是显示**的
|
||
* 「标记透明度」滑动条驱动(与「自由标记点选取」是否开启无关);只影响**本网格**的
|
||
* 固定点显示内容,不影响面板自身的像素与鼠标交互。
|
||
*/
|
||
float freeMarkerOpacity = 1.0f;
|
||
|
||
/**
|
||
* @brief 「自由标记」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
*
|
||
* 面板内容(信号分组栏 + 固定点条目)超出可视高度时出现滚动条;
|
||
* 保持在这个范围之外的内容会被裁剪。面板底部的按钮与「标记透明度」
|
||
* 滑动条**不参与**滚动(它们固定在面板底部)。
|
||
*/
|
||
float freeMarkerScroll = 0.0f;
|
||
|
||
/**
|
||
* @brief 「均值滤波」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
*
|
||
* 面板内容(每个信号的"信号色叠底标题栏 + 参数滑块行")超出可视高度时右侧出现滚动条;
|
||
* 底部的「构建为新信号」按钮**不参与**滚动(固定在面板底部)。
|
||
* 上百个信号合并到同一网格时它是必备的(否则面板会被顶出绘图区)。
|
||
*/
|
||
float meanFilterScroll = 0.0f;
|
||
|
||
/**
|
||
* @brief 「中心轴边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
*
|
||
* 与 `meanFilterScroll` 完全同一套语义:面板自 2026-11-02 起改为**可滚动的分组列表**
|
||
*(信号色叠底条目栏 + 右侧复选框 + 展开后的统计行与两行参数滑块),
|
||
* 内容超出可视高度时右侧出现滚动条(否则上百个信号会把面板顶出绘图区)。
|
||
*/
|
||
float edgeCenterScroll = 0.0f;
|
||
|
||
/**
|
||
* @brief 「弱峰调整」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
*
|
||
* 与 `meanFilterScroll` / `edgeCenterScroll` 完全同一套语义:面板自 2026-11-02 起
|
||
* 改为**可滚动的分组列表**(信号色叠底条目栏 + 右侧复选框 + 展开后的两行滑块),
|
||
* 底部「构建为新信号」按钮固定不滚动。
|
||
*/
|
||
float weakPeakScroll = 0.0f;
|
||
|
||
/**
|
||
* @brief 「边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||
*
|
||
* 该面板仍是「每信号两行(名称 / 上升沿·下降沿)」的旧式排版,但**内容可以滚动**:
|
||
* 上百个信号合并到同一网格时内容高达上万像素,若不限高,面板会远超绘图区
|
||
* ⇒ 面板像素缓存一次都存不进去 ⇒ 每一帧都要整块重画(用户 2026-11-02 报告
|
||
* “开启该功能后所有交互都非常卡顿”)。
|
||
*/
|
||
float edgeCountScroll = 0.0f;
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 重采样」开关(功能总开关)。
|
||
*
|
||
* 开启后本网格出现浮动面板:以网格内每个信号为条目(信号色叠底 + 复选框 +
|
||
* 展开/收起),展开后显示「最大数据间隔 / 最小数据间隔」两行统计与
|
||
* 「重采样间隔 / 重采样频率」两行**联动输入框**;底部「构建为新信号」按当前参数
|
||
* 把勾选的信号重采样成固定频率并插到源信号下方。
|
||
*
|
||
* 与其它实验性功能一样属于**网格级**状态,重建网格时按信号继承(只由原网格首个信号持有)。
|
||
*/
|
||
bool resample = false;
|
||
|
||
/** @brief 参与重采样的信号及其参数(只包含被勾选的信号)。 */
|
||
std::vector<ResampleItem> resampleItems;
|
||
|
||
/** 取某个信号的重采样参数(未勾选时返回空指针)。 */
|
||
const ResampleItem* FindResampleItem(const SignalRef& ref) const;
|
||
|
||
/** 取得或创建某信号的重采样参数(用于勾选)。 */
|
||
ResampleItem& EnsureResampleItem(const SignalRef& ref);
|
||
|
||
/** 取消某信号的重采样勾选。 */
|
||
void RemoveResampleItem(const SignalRef& ref);
|
||
|
||
/** 取某信号的重采样勾选状态。 */
|
||
bool ResampleEnabled(const SignalRef& ref) const
|
||
{
|
||
return FindResampleItem(ref) != nullptr;
|
||
}
|
||
|
||
/** 重采样浮动面板的 UI 状态(展开/折起 + 位置)。 */
|
||
LanePanelState resamplePanel;
|
||
|
||
/** @brief 「重采样」面板内容的滚动偏移(客户区像素,0 = 顶部)。 */
|
||
float resampleScroll = 0.0f;
|
||
|
||
/**
|
||
* @brief 「选区截取」中某个信号是否为**处理目标**(面板条目栏右侧复选框)。
|
||
*
|
||
* 与其它实验性功能不同,这里不需要参数:截取区间固定来自**双光标**(`CursorTime` /
|
||
* `CursorTimeAlt`),因此每个信号只记一个布尔标记。条目按 `Lane::signals` 顺序排列,
|
||
* **不可展开/收起**(用户 2026-11-03 明确要求)。
|
||
*/
|
||
struct ClipItem
|
||
{
|
||
SignalRef ref; /**< 目标信号。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 「实验性功能 → 选区截取」开关。
|
||
*
|
||
* 开启后本网格出现浮动面板:逐信号一行(信号色叠底 + 右侧复选框),
|
||
* 底部两个固定按钮「截取为新信号」与「原始信号截取」。
|
||
* ⚠️ 该功能**必须配合双光标**使用,未开启双光标时面板给出提示、按钮不可用。
|
||
*/
|
||
bool clip = false;
|
||
|
||
/** @brief 参与选区截取的信号(只包含被勾选的信号)。 */
|
||
std::vector<ClipItem> clipItems;
|
||
|
||
/** 取某个信号的选区截取标记(未勾选时返回空指针)。 */
|
||
const ClipItem* FindClipItem(const SignalRef& ref) const;
|
||
|
||
/** 取得或创建某信号的选区截取标记(用于勾选)。 */
|
||
ClipItem& EnsureClipItem(const SignalRef& ref);
|
||
|
||
/** 取消某信号的选区截取勾选。 */
|
||
void RemoveClipItem(const SignalRef& ref);
|
||
|
||
/** 取某信号的选区截取勾选状态。 */
|
||
bool ClipEnabled(const SignalRef& ref) const
|
||
{
|
||
return FindClipItem(ref) != nullptr;
|
||
}
|
||
|
||
/** 选区截取浮动面板的 UI 状态(展开/折起 + 位置)。 */
|
||
LanePanelState clipPanel;
|
||
|
||
/** @brief 「选区截取」面板内容的滚动偏移(客户区像素,0 = 顶部)。 */
|
||
float clipScroll = 0.0f;
|
||
|
||
/**
|
||
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
||
*
|
||
* 只保存被收起的信号引用:不在列表里 = 展开(默认)。收起后该分组只显示
|
||
* 分组栏(三角朝右),下属的固定点条目不显示。
|
||
*/
|
||
std::vector<SignalRef> freeMarkerCollapsedGroups;
|
||
|
||
/**
|
||
* @brief 「自由标记」面板里被**选中**的条目(`freeMarkerItems` 下标,-1 = 未选中)。
|
||
*
|
||
* 单击面板里的条目的数据区域即选中(该行高亮);它与网格内的固定点一一对应,
|
||
* 因此增删固定点后需要重新校正(见 `RemoveFreeMarker*`)。
|
||
*/
|
||
int freeMarkerSelected = -1;
|
||
|
||
/**
|
||
* @brief 「自由标记」面板六列的**实测内容宽度**(设备像素,0 = 还没量过)。
|
||
*
|
||
* 用户 2026-10-21 明确要求:面板里的序号 / 取值 / 枚举文本 / 增量 / 时刻 / 间隔
|
||
* **不得出现省略号(…)**。因此列宽不能再用固定值:绘制该面板时顺便把每格
|
||
* 真实文本量一遍,取最大值存到这里;布局(`EdgePanelWidth` / `EdgePanelLayoutFor`)
|
||
* 用它与缺省列宽 `kFreeMarkerColumnWidths` 的较大者,于是列宽自然贴合内容。
|
||
* ⚠️ 存的是**设备像素**(已经乘过 DPI 缩放),与字体直接对应。
|
||
*/
|
||
std::array<float, 6> freeMarkerColumnWidths{};
|
||
|
||
/** 某信号分组在「自由标记」面板里是否被收起。 */
|
||
bool FreeMarkerGroupCollapsed(const SignalRef& ref) const
|
||
{
|
||
for (const SignalRef& entry : freeMarkerCollapsedGroups)
|
||
{
|
||
if (entry == ref)
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** 切换某信号分组在「自由标记」面板里的收起状态,返回切换后的状态。 */
|
||
bool ToggleFreeMarkerGroupCollapsed(const SignalRef& ref)
|
||
{
|
||
for (std::size_t index = 0; index < freeMarkerCollapsedGroups.size(); ++index)
|
||
{
|
||
if (freeMarkerCollapsedGroups[index] == ref)
|
||
{
|
||
freeMarkerCollapsedGroups.erase(
|
||
freeMarkerCollapsedGroups.begin() + static_cast<std::ptrdiff_t>(index));
|
||
return false;
|
||
}
|
||
}
|
||
freeMarkerCollapsedGroups.push_back(ref);
|
||
return true;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @brief 从内存中的信号采样点取某一时刻的取值。
|
||
*
|
||
* 加载完成后全部采样值已在内存中,本函数只查询内存数据,与源文件无关。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] time 采样时刻。
|
||
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
|
||
* @param [OUT] value 输出采样值,缺失时为 NaN。
|
||
* @return 时间点落在数据范围内且取到有效值返回 true。
|
||
*/
|
||
bool SampleSignalAt(const TimeAxis& axis, const SignalData& signal, double time, bool interpolate,
|
||
double& value);
|
||
|
||
/**
|
||
* @brief 取某一时刻**画面上显示**的取值(含水平镜像的"数据反序"与时间偏移)。
|
||
*
|
||
* 与 `SampleSignalAt` 的区别:
|
||
* * 水平镜像时用 `SourceIndexFor` 把下标映射到源采样(头尾对调);
|
||
* * 时间偏移时先按 `time - style.offsetTime` 求出源采样时刻(因为绘制在 `t + offsetTime`)。
|
||
* 因此返回的正是当前绘制出来的那条曲线在该时刻的取值 —— 光标读数、数据点标注都必须用它,
|
||
* 否则读数会与看到的波形不一致。
|
||
* ⚠️ 系数 / 绝对值 / 垂直镜像 / `offsetY` 不在这里处理,由 `ApplyDrawTransform` 负责。
|
||
*
|
||
* @param [IN] axis 该信号使用的时间轴。
|
||
* @param [IN] signal 目标信号。
|
||
* @param [IN] style 显示外观(只用 `mirrorHorizontal` 与 `offsetTime`)。
|
||
* @param [IN] time 采样时刻。
|
||
* @param [IN] interpolate 是否在相邻采样点之间做线性插值。
|
||
* @param [OUT] value 输出采样值,缺失时为 NaN。
|
||
* @return 时间点落在数据范围内且取到有效值返回 true。
|
||
*/
|
||
bool SampleDisplayedSignalAt(const TimeAxis& axis, 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 相同)。
|
||
* @param [IN] cancel 可选的取消标志(非 nullptr 且为 true 时**尽快退出**)。
|
||
* 用于「过时任务立即清退」:拖动参数滑块时旧参数的计算不再需要,
|
||
* 越早退出就能越早把工作缓冲(千万级采样时可达数百 MB)归还。
|
||
* @param [IN] throttle 是否在阶与阶之间让出 CPU(低内存设备上的“少量多次”模式)。
|
||
*/
|
||
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int iterations,
|
||
std::vector<float>& out,
|
||
const std::atomic<bool>* cancel = nullptr,
|
||
bool throttle = false);
|
||
|
||
/**
|
||
* @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)。
|
||
* @param [IN] cancel 可选的取消标志(非 nullptr 且为 true 时**尽快退出**)。
|
||
* 拖动滑块时旧参数的计算会被新参数取代,越早退出越早释放工作缓冲。
|
||
* @param [IN] throttle 是否在阶与阶之间让出 CPU(低内存设备上的“少量多次”模式)。
|
||
*/
|
||
void ComputeWeakPeakAdjust(const std::vector<double>& values, double amount, int iterations,
|
||
std::vector<float>& out,
|
||
const std::atomic<bool>* cancel = nullptr,
|
||
bool throttle = false);
|
||
|
||
/**
|
||
* @brief 在给定时间轴上查找与给定时刻最接近的采样点。
|
||
* @param [IN] axis 目标时间轴。
|
||
* @param [IN] time 采样时刻。
|
||
* @return 采样点下标,时间轴为空时返回 -1。
|
||
*/
|
||
int FindNearestSample(const TimeAxis& axis, 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] groupIndex 即将被移除的分组下标。
|
||
* @note 调用顺序必须为:各标签页 RemapAfterGroupRemoval → 数据层 RemoveGroup → 各标签页 SyncToDataSet。
|
||
*/
|
||
void RemapAfterGroupRemoval(int groupIndex);
|
||
|
||
/**
|
||
* @brief 在数据层移除某条信号之前,按旧下标修正本配置(不重建网格)。
|
||
*
|
||
* 会删除该信号的可见性记录与网格归属,并把同一分组内位于其后的引用下标前移一位。
|
||
* 若某个网格因此变空则一并移除(与“取消勾选后空网格消失”的策略一致)。
|
||
*
|
||
* @param [IN] groupIndex 信号所属分组下标。
|
||
* @param [IN] signalIndex 即将被移除的信号下标。
|
||
* @note 调用顺序必须为:各标签页 RemapAfterSignalRemoval → 数据层修正选中 →
|
||
* 数据层 RemoveSignal → 各标签页 SyncToDataSet。
|
||
*/
|
||
void RemapAfterSignalRemoval(int groupIndex, 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] groupIndex 分组下标。
|
||
* @param [IN] visible 目标可见状态。
|
||
*/
|
||
void SetGroupVisible(int groupIndex, bool visible);
|
||
|
||
/**
|
||
* @brief 设置某分组内**子集**信号的可见性,并同步网格划分。
|
||
*
|
||
* 用于「筛选状态」下的组复选框(用户 2026-11-03 明确要求):
|
||
* 筛选时组名前的复选框只对应**组内被筛选出来的那些信号**,
|
||
* 因此全选/部分选择/未选都只作用于 `signalMask` 为 true 的元素,
|
||
* 不会把未通过筛选的信号一起勾上或一起取消。
|
||
*
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] signalMask 逐信号的目标状态(长度不足的部分视为"不参与",保持不变)。
|
||
*/
|
||
void SetGroupVisibleSubset(int groupIndex, const std::vector<bool>& signalMask);
|
||
|
||
/**
|
||
* @brief 判断某分组的**子集**是否全部可见(用于筛选状态下的复选框显示)。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] signalMask 参与判定的信号(长度不足的部分不参与)。
|
||
* @return 参与判定的信号**全部**可见且至少有一条返回 true。
|
||
*/
|
||
bool GroupAllVisibleSubset(int groupIndex, const std::vector<bool>& signalMask) const;
|
||
|
||
/**
|
||
* @brief 判断某分组的**子集**是否存在可见信号(用于筛选状态下的部分选择态)。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @param [IN] signalMask 参与判定的信号(长度不足的部分不参与)。
|
||
* @return 参与判定的信号中至少有一条可见返回 true。
|
||
*/
|
||
bool GroupHasVisibleSubset(int groupIndex, const std::vector<bool>& signalMask) const;
|
||
|
||
/**
|
||
* @brief 设置全部信号可见性,并同步网格划分。
|
||
* @param [IN] visible 目标可见状态。
|
||
*/
|
||
void SetAllVisible(bool visible);
|
||
|
||
/**
|
||
* @brief 仅显示指定信号,其余全部隐藏。
|
||
* @param [IN] ref 需要保持可见的信号。
|
||
*/
|
||
void SoloSignal(const SignalRef& ref);
|
||
|
||
/**
|
||
* @brief 判断某分组是否存在可见信号。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 存在可见信号返回 true。
|
||
*/
|
||
bool GroupHasVisible(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 判断某分组是否全部信号可见。
|
||
* @param [IN] groupIndex 分组下标。
|
||
* @return 全部可见且至少有一条信号返回 true。
|
||
*/
|
||
bool GroupAllVisible(int groupIndex) 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] groupIndex 发生插入的分组下标。
|
||
* @param [IN] insertPos 新信号的下标。
|
||
*/
|
||
void RemapAfterSignalInsert(int groupIndex, int insertPos);
|
||
|
||
/**
|
||
* @brief 数据层把一条信号**跨分组移动**之后,修正本页的全部引用。
|
||
*
|
||
* 与 `RemapAfterSignalRemoval` + `RemapAfterSignalInsert` 组合不同:移动**不应**
|
||
* 把信号从网格里删掉(用户期望曲线原地继续显示,只是归属分组变了),
|
||
* 因此这里把「恰好等于源引用」的条目**改写**为目标引用,而不是先删后插。
|
||
*
|
||
* 处理顺序(不可调换):
|
||
* 1. 精确等于 `from` 的引用 → 改写为 `to`(其下标已是移动后的最终值);
|
||
* 2. 其余引用按「目标分组在 `to.signalIndex` 处插入」/「源分组删除 `from.signalIndex`」
|
||
* 做 ±1 位移。
|
||
* ⚠️ 若先位移再匹配,源分组里 `from.signalIndex + 1` 的引用会位移成 `from.signalIndex`
|
||
* 而被误判成被移动的信号(曾经的 BUG)。
|
||
*
|
||
* 可见性表:目标分组在 `to.signalIndex` 处插入一列(取 `visible`),源分组删除一列。
|
||
*
|
||
* @param [IN] from 源引用(移动前)。
|
||
* @param [IN] to 目标引用(移动后)。
|
||
* @param [IN] visible 该信号在源分组里的可见性(迁移到目标分组后沿用)。
|
||
*/
|
||
void RemapAfterSignalMove(const SignalRef& from, const SignalRef& to, bool visible);
|
||
|
||
/**
|
||
* @brief 数据层**组内重排**一条信号之后,修正本页在该分组内的全部引用下标。
|
||
*
|
||
* 只做线性下标映射(可见性表也按同一规则移动),不改变信号的分组归属:
|
||
* `from` → `to`;`from < to` 时 `(from, to]` 区间内的下标各前移一位,
|
||
* `from > to` 时 `[to, from)` 区间内的下标各后移一位。
|
||
*
|
||
* @param [IN] groupIndex 发生重排的分组下标。
|
||
* @param [IN] from 移动前的下标。
|
||
* @param [IN] to 移动后的下标。
|
||
*/
|
||
void RemapAfterSignalReorder(int groupIndex, int from, int to);
|
||
|
||
/**
|
||
* @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] groupIndex 分组下标。
|
||
* @return 折叠状态。
|
||
*/
|
||
bool IsCollapsed(int groupIndex) const;
|
||
|
||
/**
|
||
* @brief 切换某分组的折叠状态。
|
||
* @param [IN] groupIndex 分组下标。
|
||
*/
|
||
void ToggleCollapsed(int groupIndex);
|
||
|
||
/**
|
||
* @brief 设置「新出现的分组是否默认折起」。
|
||
*
|
||
* 只影响**之后**新出现的分组(例如新加载的文件);已存在的分组保持用户当前的选择。
|
||
* 用户新建的标签页会把它设为 true,使信号列表默认紧凑。
|
||
*
|
||
* @param [IN] collapsed 是否默认折起。
|
||
*/
|
||
void SetGroupsCollapsedByDefault(bool collapsed) { m_groupsCollapsedByDefault = collapsed; }
|
||
|
||
/**
|
||
* @brief 把当前所有分组设为折叠或展开。
|
||
* @param [IN] collapsed true = 全部折起,false = 全部展开。
|
||
*/
|
||
void SetAllCollapsed(bool collapsed)
|
||
{
|
||
std::fill(m_collapsed.begin(), m_collapsed.end(), collapsed);
|
||
}
|
||
|
||
/**
|
||
* @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 计算全部可见信号覆盖的**显示时间**范围。
|
||
*
|
||
* ⚠️ 返回的是"画出来的位置",即每条信号的 `axis.minTime/maxTime` 已叠加它自己的
|
||
* `SignalStyle::offsetTime`(采样画在 `t + offsetTime` 处)。视界要覆盖的正是这个范围,
|
||
* 少了偏移就会出现"设置时间轴偏移后点适应视图,曲线跑到视界之外"。
|
||
*
|
||
* @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; /**< 分组折叠状态,按分组下标。 */
|
||
/**
|
||
* @brief 新出现的(首次见到的)分组是否默认折起。
|
||
*
|
||
* 用户新建的标签页把它设为 true(信号列表默认紧凑);启动时的首个标签页保持 false,
|
||
* 这样加载完文件就能直接勾选信号。
|
||
*/
|
||
bool m_groupsCollapsedByDefault = false;
|
||
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__
|