样式区「调整」行改为 系数/镜像/绝对值:系数二级面板(还原图标 + 整行输入框)、镜像二级面板(水平/垂直复选框);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

+55 -26
View File
@@ -205,13 +205,15 @@ namespace jdi
* @param [IN] time 时间轴。
* @param [IN] values 采样值。
* @param [IN] count 可用采样点数量。
* @param [IN] style 显示外观(水平镜像时按**源下标反序**取值)。
* @param [IN] firstIndex 第一个时间不小于查询时刻的采样下标(相交线段为 firstIndex-1 → firstIndex)。
* @param [IN] t 查询时刻。
* @param [OUT] out 输出取值。
* @return 折线覆盖该时刻且取到有效值返回 true。
*/
bool PolylineValueAt(const std::vector<double>& time, const std::vector<double>& values,
std::size_t count, std::size_t firstIndex, double t, double& out)
std::size_t count, const SignalStyle& style, std::size_t firstIndex,
double t, double& out)
{
if (firstIndex == 0 || firstIndex >= count)
{
@@ -220,8 +222,9 @@ namespace jdi
}
const double t0 = time[firstIndex - 1];
const double t1 = time[firstIndex];
const double v0 = values[firstIndex - 1];
const double v1 = values[firstIndex];
// 水平镜像时画面上的相邻两点取源数组中头尾对调后的两个采样。
const double v0 = values[SourceIndexFor(style, count, firstIndex - 1)];
const double v1 = values[SourceIndexFor(style, count, firstIndex)];
if (!std::isfinite(t0) || !std::isfinite(t1) || !std::isfinite(v0) || !std::isfinite(v1) ||
!(t1 > t0))
{
@@ -3228,7 +3231,17 @@ namespace jdi
continue;
}
}
const double value = signal->values[index];
// ⚠️ 自适应必须基于**画出来的曲线**:套用与绘制完全相同的变换
//(系数 → 绝对值 → 镜像),否则调整系数/镜像后自适应范围会按原始数据算错
//(用户报告"自适应区域错误,看上去还是按照原始数据来的")。
// 水平镜像时画面上的第 index 个位置取源下标 count-1-index 的值。
const std::size_t sourceIndex = SourceIndexFor(*m_dataSet->Style(ref), count, index);
const double raw = signal->values[sourceIndex];
if (!std::isfinite(raw))
{
continue;
}
const double value = ApplyDrawTransform(*m_dataSet->Style(ref), raw);
if (std::isfinite(value))
{
low = std::min(low, value);
@@ -5342,10 +5355,11 @@ namespace jdi
const std::size_t sampleCount = std::min(resolved.file->time.size(), resolved.data->values.size());
const double viewMin = Tab().ViewMin();
const double viewMax = Tab().ViewMax();
// 绘制前的数值变换(增益/绝对值/中心反转)会改变顶点,必须进缓存键。
// 绘制前的数值变换(系数/绝对值/镜像)会改变顶点,必须进缓存键。
const float gain = (resolved.style != nullptr) ? resolved.style->gain : 1.0f;
const bool absolute = (resolved.style != nullptr) && resolved.style->absolute;
const bool inverted = (resolved.style != nullptr) && resolved.style->inverted;
const bool mirrorVertical = (resolved.style != nullptr) && resolved.style->mirrorVertical;
const bool mirrorHorizontal = (resolved.style != nullptr) && resolved.style->mirrorHorizontal;
for (const PolylineCacheEntry& entry : m_polylineCache)
{
@@ -5354,7 +5368,7 @@ namespace jdi
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
entry.laneYMax == lane.yMax && entry.gain == gain && entry.absolute == absolute &&
entry.inverted == inverted)
entry.mirrorVertical == mirrorVertical && entry.mirrorHorizontal == mirrorHorizontal)
{
return &entry;
}
@@ -5381,7 +5395,8 @@ namespace jdi
entry.laneYMax = lane.yMax;
entry.gain = gain;
entry.absolute = absolute;
entry.inverted = inverted;
entry.mirrorVertical = mirrorVertical;
entry.mirrorHorizontal = mirrorHorizontal;
BuildPolyline(resolved, lane, entry.points, entry.breaks, entry.band, entry.bandBreaks,
entry.bandMode, entry.bandSpiky);
@@ -5584,11 +5599,16 @@ namespace jdi
bool broken = false;
// (一)落在本列区间内的采样点。
// 水平镜像时取的是**源下标**(区间同样反转),因此先把列区间映射到源下标区间。
const std::size_t srcA = std::min(SourceIndexFor(*resolved.style, count, i0),
SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0));
const std::size_t srcB = std::max(SourceIndexFor(*resolved.style, count, i0),
SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0));
if (i1 > i0 && hasCoarse && (i1 - i0) >= 2 * coarseStride)
{
const std::size_t firstBucket = i0 / coarseStride;
const std::size_t firstBucket = srcA / coarseStride;
const std::size_t lastBucket =
std::min((i1 - 1) / coarseStride, coarseMin.size() - 1);
std::min(srcB / coarseStride, coarseMin.size() - 1);
for (std::size_t bucket = firstBucket; bucket <= lastBucket; ++bucket)
{
const double bucketLow = coarseMin[bucket] * coarseScale;
@@ -5613,7 +5633,7 @@ namespace jdi
}
else if (i1 > i0)
{
for (std::size_t index = i0; index < i1; ++index)
for (std::size_t index = srcA; index <= srcB; ++index)
{
const double value = ApplyDrawTransform(*resolved.style, values[index]);
if (!std::isfinite(value))
@@ -5647,32 +5667,33 @@ namespace jdi
// 其极值必然出现在采样点或列边界上,因此「列内采样点 + 两侧边界取值」就是
// 该列包络的精确解,且相邻两列在边界处取到同一个值,包络带严丝合缝。
double boundary = 0.0;
if (PolylineValueAt(time, values, count, i0, t0, boundary))
if (PolylineValueAt(time, values, count, *resolved.style, i0, t0, boundary))
{
boundary = ApplyDrawTransform(*resolved.style, boundary);
if (boundary < low)
{
low = boundary;
lowOrder = (i0 > 0) ? (i0 - 1) : 0;
lowOrder = (i0 > 0) ? SourceIndexFor(*resolved.style, count, i0 - 1) : 0;
}
if (boundary > high)
{
high = boundary;
highOrder = (i0 > 0) ? (i0 - 1) : 0;
highOrder = (i0 > 0) ? SourceIndexFor(*resolved.style, count, i0 - 1) : 0;
}
}
if (PolylineValueAt(time, values, count, i1, t1, boundary))
if (PolylineValueAt(time, values, count, *resolved.style, i1, t1, boundary))
{
boundary = ApplyDrawTransform(*resolved.style, boundary);
const std::size_t order = SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0);
if (boundary < low)
{
low = boundary;
lowOrder = i1;
lowOrder = order;
}
if (boundary > high)
{
high = boundary;
highOrder = i1;
highOrder = order;
}
}
@@ -5749,7 +5770,9 @@ namespace jdi
for (std::size_t index = begin; index < end; index += stride)
{
const double t = time[index];
const double value = ApplyDrawTransform(*resolved.style, values[index]);
// 水平镜像 = 数据反序:画面上第 index 个位置取源下标 count-1-index 的值。
const double value = ApplyDrawTransform(
*resolved.style, values[SourceIndexFor(*resolved.style, count, index)]);
if (!std::isfinite(t) || !std::isfinite(value))
{
inSegment = false;
@@ -6413,10 +6436,11 @@ namespace jdi
mixFloat(resolved.style->opacity);
mixFloat(resolved.style->lineWidth);
mix(static_cast<std::uint64_t>(resolved.style->mode));
// 绘制前的数值变换(增益 / 取绝对值 / 中心反转)会改变波形形状。
// 绘制前的数值变换(系数 / 绝对值 / 镜像)会改变波形形状。
mixFloat(resolved.style->gain);
mix(resolved.style->absolute ? 1ull : 0ull);
mix(resolved.style->inverted ? 1ull : 0ull);
mix(resolved.style->mirrorVertical ? 1ull : 0ull);
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
}
// 图例文案参与绘制,重命名必须让缓存失效。
mixText(resolved.data->Title());
@@ -6464,7 +6488,8 @@ namespace jdi
entry.mode = static_cast<int>(resolved.style->mode);
entry.gain = resolved.style->gain;
entry.absolute = resolved.style->absolute;
entry.inverted = resolved.style->inverted;
entry.mirrorVertical = resolved.style->mirrorVertical;
entry.mirrorHorizontal = resolved.style->mirrorHorizontal;
}
lane.push_back(entry);
}
@@ -9397,11 +9422,12 @@ namespace jdi
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
double value = 0.0;
if (!SampleSignalAt(*resolved.file, *resolved.data, time, interpolate, value))
if (!SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, time,
interpolate, value))
{
continue;
}
// 取值读数必须与画出来的波形一致:同样套用增益/取绝对值/中心反转。
// 取值读数必须与画出来的波形一致:同样套用系数/绝对值/镜像。
value = ApplyDrawTransform(*resolved.style, value);
const float rawY = ValueToY(value, lane);
@@ -9423,7 +9449,8 @@ namespace jdi
if (showDelta)
{
double otherValue = 0.0;
if (SampleSignalAt(*resolved.file, *resolved.data, otherTime, interpolate, otherValue))
if (SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, otherTime,
interpolate, otherValue))
{
otherValue = ApplyDrawTransform(*resolved.style, otherValue);
const double delta = value - otherValue;
@@ -10345,12 +10372,14 @@ namespace jdi
for (std::size_t index = begin; index < end; index += stride)
{
if (index < curveBegin || (index - curveBegin) >= curve.size())
// 水平镜像 = 数据反序:遍历下标映射到源下标后再取曲线值。
const std::size_t sourceIndex = SourceIndexFor(*resolved.style, count, index);
if (sourceIndex < curveBegin || (sourceIndex - curveBegin) >= curve.size())
{
inSegment = false;
continue;
}
const float sampled = curve[index - curveBegin];
const float sampled = curve[sourceIndex - curveBegin];
if (!std::isfinite(sampled))
{
inSegment = false;