样式区「调整」行改为 系数/镜像/绝对值:系数二级面板(还原图标 + 整行输入框)、镜像二级面板(水平/垂直复选框);Y 轴自适应按变换后曲线

- 「调整」行三个按钮改为「系数」「镜像」「绝对值」(顺序即结算顺序:系数 → 绝对值 → 镜像)。
  「系数」「镜像」点击后在按钮下沿展开二级面板(按下只登记、抬起命中同一按钮才展开,
  与自定义颜色入口一致),「绝对值」仍是直接切换的按钮。
  打开面板时主动清除样式区悬停态 —— 面板会接管鼠标事件,否则该按钮会一直保持"鼠标按住"般的高亮。

- 数据层:`SignalStyle::inverted` 拆成 `mirrorVertical`(Y=0 上下翻转)与
  `mirrorHorizontal`(水平镜像 = 数据反序);新增 `Mirrored()`、
  `SourceIndexFor`(水平镜像的遍历下标 → 源下标映射)、`TransformValueRange`(区间映射)。
  `ApplyDrawTransform` 顺序固定为 系数 → 绝对值 → 垂直镜像;水平镜像在采样遍历层处理。

- 绘制链路:`BuildPolyline`(逐点描边与像素列包络两条路径、跨列边界插值、粗粒度分组层)、
  派生预览曲线、光标取值读数(含 Δt)全部按同一变换取值;水平镜像时按源下标区间反序读取,
  包络层仍复用粗粒度分组;折线缓存键、`SceneContentHash`、样式快照都带上两个镜像位。

- 系数二级面板:标题「系数调整」,标题栏最右端是一个可点击的「还原」自绘图标按钮
  (悬停有高亮反馈,点击后系数重置为 1,面板保持打开);下面是一整行输入框
  (不再显示「绘制系数」说明文字),数值至少显示两位小数、精度更高时按实际精度显示
  (1 → 1.00、2.5 → 2.50、1.2345678 原样)。

- 镜像二级面板:两行带复选框的切换项「水平镜像」「垂直镜像」,点击后**面板保持打开**
  (`MenuItem::keepOpen`),勾选状态就地回写;任一项勾选时「镜像」按钮显示为激活态。

- Y 轴自适应修复:整段自适应(`TabConfig::ComputeLaneRange`)按区间映射(`TransformValueRange`)
  计算,不再直接用原始 min/max;可见区间自适应(`AutoRangeLaneToView`)逐点套用绘制变换
  (含水平镜像的源下标映射)。用户报告的"调整系数/镜像后自适应区域错误、仍按原始数据"由此修复。

- `PopupMenu`:新增 `MenuItemKind::HEADER_ACTION`(标题 + 右侧可点击图标,图标有悬停高亮)、
  `MenuItem::keepOpen`(切换项点击后不关闭菜单)、`HeaderIconRect()`,
  以及无说明文字的整行输入框(占满内容区)。

验证(离屏 harness,SW_HIDE、不动鼠标键盘屏幕):
- [AL] 重写/扩充:变换顺序与区间映射(含水平镜像的源下标反序)、调整行三按钮的顺序/等宽/标签不裁切、
  「系数」按住不弹出 / 移开取消 / 单击展开(标题 HEADER_ACTION、无标题输入框占满整行、初始 1.00、
  两位小数与更高精度回显、提交后面板保持打开、折线缓存键携带系数)、
  「还原」图标位置/悬停像素变化/点击后系数回到 1.00 且面板保持打开、
  「镜像」面板两行复选框(初始未勾选、keepOpen、勾选后按钮激活、取消其一保留另一)、
  打开面板后按钮不再停留高亮、系数/镜像按钮的激活填充与其它启用态切换按钮逐像素同色;
  `BuildPolyline` 顶点范围与变换后数据范围逐项对齐(默认/系数 2/绝对值/垂直镜像/三者叠加),
  水平镜像用窄视窗逐顶点核对"取的是对面端点的采样"(8/8)+ 读数跟随;
  以及 Y 轴自适应:整段/可见区间自适应在系数、绝对值、垂直镜像下均跟随变换后的曲线。
- 合计 594/594 通过;make clean && make all 零警告。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-13 14:50:38 +08:00
1 parent 2f243dd557
commit 2b02518f85
8 files changed
+608 -110

No files matched your search

+111 -15
View File
@@ -1,6 +1,8 @@
#ifndef __DATA_MODEL_HPP__
#define __DATA_MODEL_HPP__
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <string>
@@ -105,24 +107,35 @@ namespace jdi
DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */
/**
* 绘制前的数值变换(三者**依次**作用,见 `ApplyDrawTransform`):
* 1. `gain` 系数增益:采样值乘以该系数(默认 1,即不改变);
* 2. `absolute` 取绝对值:|value|;
* 3. `inverted` 中心反转:按 Y = 0 上下反向(value → −value)。
* 绘制前的数值变换(依次作用,见 `ApplyDrawTransform`):
* 1. `gain` 系数:采样值乘以该系数(默认 1,即不改变);
* 2. `absolute` 绝对值:|value|;
* 3. 镜像:`mirrorVertical` 按 Y = 0 上下镜像(value → −value),
* `mirrorHorizontal` 为**数据反序**(把采样序列头尾对调后绘制,即水平翻转)。
*
* ⚠️ 结算顺序固定为「系数 → 绝对值 → 镜像」(用户明确要求),
* 其中镜像的两个方向互相正交:垂直改**值**、水平改**取值下标**。
*/
float gain = 1.0f; /**< 系数增益,默认 1.0。 */
bool absolute = false; /**< 是否对绘制值取绝对值,默认关闭。 */
bool inverted = false; /**< 是否按 Y=0 中心反转绘制值,默认关闭。 */
float gain = 1.0f; /**< 系数,默认 1.0(不改变)。 */
bool absolute = false; /**< 是否对绘制值取绝对值,默认关闭。 */
bool mirrorVertical = false; /**< 是否按 Y=0 上下镜像(值取反),默认关闭。 */
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序绘制),默认关闭。 */
/** @brief 镜像功能的任一分量是否启用(决定「镜像」按钮的激活态)。 */
bool Mirrored() const { return mirrorVertical || mirrorHorizontal; }
};
/**
* @brief 把原始采样值变换为**绘制用值**。
*
* 顺序固定为「系数增益 → 取绝对值 → 中心反转」(用户明确要求中心反转最后结算),
* 例如增益 2、原始值 1.5 → 2×1.5 = 3;再取绝对值仍为 3;再中心反转为 −3。
* 顺序固定为「系数 → 绝对值 → 镜像(垂直分量)」,例如系数 2、原始值 1.5 →
* 2×1.5 = 3;再取绝对值仍为 3;若开启垂直镜像则为 −3。
* NaN 经乘/取绝对值/取反后仍为 NaN(保持断线语义)。
*
* @param [IN] style 信号外观(提供增益与两个开关)。
* ⚠️ 水平镜像**不在这里处理**:它是"取值下标反序"(把采样序列头尾对调),
* 必须在遍历采样时按 `SourceIndexFor(style, count, index)` 取源下标。
*
* @param [IN] style 信号外观(提供系数与两个数值开关)。
* @param [IN] value 原始采样值。
* @return 绘制用值。
*/
@@ -136,7 +149,7 @@ namespace jdi
{
value = (value < 0.0) ? -value : value;
}
if (style.inverted)
if (style.mirrorVertical)
{
value = -value;
}
@@ -144,17 +157,81 @@ namespace jdi
}
/**
* @brief 判断数值变换是否**保持大小顺序**(正增益、且不取绝对值、不反转)。
* @brief 把遍历下标映射为**源采样下标**(处理水平镜像的"数据反序")。
*
* 只有保序时,粗粒度包络层(每组的 min/max)才能直接按增益缩放后继续使用;
* 取绝对值 / 反转会把最小值与最大值互换,此时必须回退到逐采样点变换。
* 水平镜像 = 头尾对调:遍历下标 `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.inverted;
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;
}
}
/**
@@ -618,6 +695,25 @@ namespace jdi
bool SampleSignalAt(const DataFile& file, const SignalData& signal, double time, bool interpolate,
double& value);
/**
* @brief 取某一时刻**画面上显示**的取值(含水平镜像的"数据反序")。
*
* 与 `SampleSignalAt` 的唯一区别:水平镜像启用时用 `SourceIndexFor` 把下标映射到源采样
*(头尾对调),因此返回的正是当前绘制出来的那条曲线在该时刻的取值 —— 光标读数、
* 数据点标注都必须用它,否则读数会与看到的波形不一致。
* ⚠️ 系数 / 绝对值 / 垂直镜像不在这里处理,由 `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],边界按实际窗口长度)。
*
+21 -1
View File
@@ -37,7 +37,8 @@ namespace jdi
HEADER = 2, /**< 分组标题(不可点击)。 */
SWATCHES = 3, /**< 颜色色块行。 */
EDIT = 4, /**< 带内联输入框的项:在菜单内部直接编辑数值(菜单不关闭)。 */
SUBMENU = 5 /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
SUBMENU = 5, /**< 带二级子菜单的项:鼠标悬停时向右展开。 */
HEADER_ACTION = 6 /**< 分组标题 + **右侧图标按钮**(如「还原」),图标可点击。 */
};
/**
@@ -56,6 +57,13 @@ namespace jdi
std::wstring value; /**< EDIT 项的当前数值文本(非编辑态显示)。 */
EditFilter filter = EditFilter::ANY; /**< EDIT 项的输入过滤规则。 */
std::vector<MenuItem> submenu; /**< 二级子菜单项(kind == SUBMENU 时有效)。 */
/**
* 普通项被点击后是否**保持菜单打开**。
*
* 用于「带复选框的切换项」(如镜像的水平/垂直):可以连续勾选多项,
* 勾选状态通过 `UpdateItemLabel` 回写。默认 false(点击即关):与既有行为一致。
*/
bool keepOpen = false;
};
/**
@@ -283,6 +291,17 @@ namespace jdi
*/
gd::RectF EditFieldRect(int index) const;
/**
* @brief 计算 `HEADER_ACTION` 项右侧**图标按钮**(如「还原」)的矩形。
*
* 图标紧贴该行右端(右边距与其它行一致、行内垂直居中),大小按缩放比例推导。
* 鼠标悬停时该图标会高亮,点击即下发该项的命令。
*
* @param [IN] index 项目下标。
* @return 图标矩形;非 `HEADER_ACTION` 项或非法下标返回空矩形。
*/
gd::RectF HeaderIconRect(int index) const;
/**
* @brief 计算 EDIT 项输入框的“自然宽度”(排版与绘制共用同一公式)。
* @param [IN] item 菜单项。
@@ -353,6 +372,7 @@ namespace jdi
std::vector<std::vector<gd::RectF>> m_swatchRects; /**< 各项内的色块矩形。 */
int m_hoverIndex = -1; /**< 当前悬停项。 */
int m_hoverIconIndex = -1; /**< 当前悬停的 `HEADER_ACTION` 图标所在项下标。 */
int m_hoverSwatch = -1; /**< 当前悬停色块。 */
int m_pressedIndex = -1; /**< 按下时的项目下标。 */
int m_pressedSwatch = -1; /**< 按下时的色块下标。 */