实验性功能新增「差异放大」,并修正「构建为新信号」按钮的间距

1. 新增「实验性功能 → 差异放大」(与其它实验功能一样是带勾选的开关)
   - 用途:纹波类信号某个周期(甚至半个周期)的 M 形下凹明显偏弱时,
     把这段下凹按比例拉回周边振幅,恢复正常周期的对比度,便于看出波形异常。
   - 算法(全 O(n),不额外占用与信号长度成正比以外的内存):
     以半径约半个纹波周期的滑动平均作为纹波中心线,用邻域内「交流分量的最大值」
     作为局部纹波振幅,取全信号振幅的 75 分位作为目标振幅,
     逐点增益 = 目标振幅 / 局部振幅(夹在 1 到放大倍数之间),
     输出 = 中心线 + 增益 × 交流分量,再夹在周边包络内。
   - 关键性质(均已用合成正弦逐点验证):
     * 增益下界恒为 1,因此**只有被判定为"偏弱"的采样点会改变**,
       其余采样点输出与原值逐位相同 —— 实测逐周期表格中只有一个周期发生变化;
     * 放大结果夹在周边包络内,因此**任何一点都不超出原有的振幅范围**,
       整体值域与其它周期的峰峰值都不变;
     * 端点自动收敛到不放大,避免记录首尾被整片抬升。
   - 参数「窗口点数 X」(窗口长度,建议略大于纹波周期的一半)与
     「放大倍数 N」(增益上限)都可在浮动面板内按信号编辑。

2. 浮动面板
   - 与「均值滤波」完全同一套排版:自上而下逐信号条目,
     第一排信号名与复选框、第二排两个参数输入框;取消勾选即不显示该信号的参数与预览曲线;
     启用时默认只勾选网格内第一个可见信号。
   - 新增「构建为新信号」按钮,按各信号自己的参数把结果插入到源信号下方,
     与「均值滤波」的构建机制完全一致(统一走同一套请求与插入流程)。

3. 光标交互
   - 纵向光标额外标注差异放大曲线在该时刻的取值,配色与原始信号一致,
     数值尾部括号内容为「差异化」,双光标时同样带增量。

4. 修复「构建为新信号」按钮与面板下边界的间距
   - 原按钮行直接贴着面板下边框,与面板左右内边距及内容间距都不协调;
     现在按钮上方与下方各保留一个与左右一致的内边距(均值滤波与差异放大同时生效)。

测试:离屏验证套件新增两组检查(合成正弦上算法只改动一个周期、放大不越界、
参数上限生效、面板排版与按钮间距;再写一份合成纹波文件真正加载渲染并核对曲线数值),
合计 289 项检查全部通过;验证过程不显示窗口、不移动鼠标键盘、不抓屏。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 17:54:27 +08:00
1 parent 8801c8f019
commit d4206a8d67
6 files changed
+1188 -185

No files matched your search

+491 -71
View File
@@ -2,7 +2,10 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cwctype>
#include <deque>
#include <limits>
#include "Theme.hpp"
@@ -131,6 +134,11 @@ namespace jdi
bool meanFilterPanelOpen = true;
double meanFilterPanelX = -1.0;
double meanFilterPanelY = -1.0;
bool diffAmp = false;
std::vector<Lane::DiffAmpItem> diffAmpItems;
bool diffAmpPanelOpen = true;
double diffAmpPanelX = -1.0;
double diffAmpPanelY = -1.0;
};
// 先记下每个信号原来所在网格的网格自身状态:
@@ -156,6 +164,11 @@ namespace jdi
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
carry.meanFilterPanelX = lane.meanFilterPanelX;
carry.meanFilterPanelY = lane.meanFilterPanelY;
carry.diffAmp = lane.diffAmp;
carry.diffAmpItems = lane.diffAmpItems;
carry.diffAmpPanelOpen = lane.diffAmpPanelOpen;
carry.diffAmpPanelX = lane.diffAmpPanelX;
carry.diffAmpPanelY = lane.diffAmpPanelY;
for (const SignalRef& ref : lane.signals)
{
previousCarry.emplace_back(ref, carry);
@@ -192,6 +205,11 @@ namespace jdi
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
lane.meanFilterPanelX = carry.meanFilterPanelX;
lane.meanFilterPanelY = carry.meanFilterPanelY;
lane.diffAmp = carry.diffAmp;
lane.diffAmpItems = carry.diffAmpItems;
lane.diffAmpPanelOpen = carry.diffAmpPanelOpen;
lane.diffAmpPanelX = carry.diffAmpPanelX;
lane.diffAmpPanelY = carry.diffAmpPanelY;
};
// 收集全部可见信号引用。
@@ -893,86 +911,231 @@ namespace jdi
// ============================ 采样辅助 ============================
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
namespace
{
const int window = std::max(0, radius);
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0 || window == 0)
/**
* @brief 滑动平均的共用实现(输入只要可转成 double)。
*
* 窗口 = [max(0,i-N), min(count-1,i+N)],每前进一步只增减边界处的两个采样点,
* 整体 O(n),不额外占用前缀和内存。“差异放大”需要在 float 缓冲上再做一次
* 滑动平均,若先转成 double 会因为亿级采样多占一倍内存,因此做成模板。
*/
template <typename T>
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out);
/**
* @brief 滑动窗口最大值(只要最大值)。
*
* 与 `SlidingExtrema` 同一个单调队列思路,但**不额外分配最小值数组**:
* 亿级采样下一次全量计算会多占 100 MB 以上,而“差异放大”只需要最大值。
*
* @param [IN] values 采样值。
* @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。
* @param [OUT] outMax 邻域最大值(整窗都是 NaN 时为 NaN)。
*/
template <typename T>
void SlidingMax(const std::vector<T>& values, std::size_t radius, std::vector<float>& outMax)
{
// 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。
const std::size_t count = values.size();
outMax.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
std::deque<std::size_t> maxQueue; // 递减:队首最大。
std::size_t filled = 0;
for (std::size_t index = 0; index < count; ++index)
{
if (std::isfinite(values[index]))
const std::size_t wantHigh = std::min(count - 1, index + radius);
while (filled <= wantHigh)
{
out[index] = static_cast<float>(values[index]);
const double incoming = values[filled];
if (std::isfinite(incoming))
{
while (!maxQueue.empty() && values[maxQueue.back()] <= incoming)
{
maxQueue.pop_back();
}
maxQueue.push_back(filled);
}
++filled;
}
}
return;
}
// 滑动窗口求和:窗口 = [max(0,i-N), min(count-1,i+N)],
// 每前进一步只增减边界处的两个采样点,整体 O(n),不额外占用前缀和内存。
double sum = 0.0;
std::size_t finiteCount = 0;
std::size_t windowLow = 0;
std::size_t windowHigh = 0; // 闭区间右端。
const auto addValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum += value;
++finiteCount;
}
};
const auto removeValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum -= value;
--finiteCount;
}
};
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t low = (index > static_cast<std::size_t>(window))
? (index - static_cast<std::size_t>(window))
: 0;
const std::size_t high = std::min(count - 1, index + static_cast<std::size_t>(window));
if (index == 0)
{
windowLow = low;
windowHigh = high;
for (std::size_t i = windowLow; i <= windowHigh; ++i)
const std::size_t low = (index > radius) ? (index - radius) : 0;
while (!maxQueue.empty() && maxQueue.front() < low)
{
addValue(values[i]);
maxQueue.pop_front();
}
}
else
{
while (windowLow < low)
if (!maxQueue.empty())
{
removeValue(values[windowLow]);
++windowLow;
outMax[index] = static_cast<float>(values[maxQueue.front()]);
}
while (windowHigh < high)
{
++windowHigh;
addValue(values[windowHigh]);
}
}
// 中心点本身无效(NaN)时该处不产生有效结果。
if (finiteCount > 0 && std::isfinite(values[index]))
{
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
}
}
} // namespace
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out)
{
MovingAverageImpl(values, radius, out);
}
void ComputeMovingAverage(const std::vector<float>& values, int radius, std::vector<float>& out)
{
MovingAverageImpl(values, radius, out);
}
namespace
{
template <typename T>
void MovingAverageImpl(const std::vector<T>& values, int radius, std::vector<float>& out)
{
const int window = std::max(0, radius);
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0 || window == 0)
{
// 半径 0 表示不平滑:原样复制(中心点无效处仍为 NaN)。
for (std::size_t index = 0; index < count; ++index)
{
const double value = static_cast<double>(values[index]);
if (std::isfinite(value))
{
out[index] = static_cast<float>(value);
}
}
return;
}
// 滑动窗口求和:每前进一步只增减边界处的两个采样点,整体 O(n)。
double sum = 0.0;
std::size_t finiteCount = 0;
std::size_t windowLow = 0;
std::size_t windowHigh = 0; // 闭区间右端。
const auto addValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum += value;
++finiteCount;
}
};
const auto removeValue = [&sum, &finiteCount](double value)
{
if (std::isfinite(value))
{
sum -= value;
--finiteCount;
}
};
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t low = (index > static_cast<std::size_t>(window))
? (index - static_cast<std::size_t>(window))
: 0;
const std::size_t high =
std::min(count - 1, index + static_cast<std::size_t>(window));
if (index == 0)
{
windowLow = low;
windowHigh = high;
for (std::size_t i = windowLow; i <= windowHigh; ++i)
{
addValue(static_cast<double>(values[i]));
}
}
else
{
while (windowLow < low)
{
removeValue(static_cast<double>(values[windowLow]));
++windowLow;
}
while (windowHigh < high)
{
++windowHigh;
addValue(static_cast<double>(values[windowHigh]));
}
}
// 中心点本身无效(NaN)时该处不产生有效结果。
const double center = static_cast<double>(values[index]);
if (finiteCount > 0 && std::isfinite(center))
{
out[index] = static_cast<float>(sum / static_cast<double>(finiteCount));
}
}
}
/**
* @brief 滑动窗口极值:一次求出每个中心点邻域(±radius)内的最小值与最大值。
*
* 两个单调队列(最小值队列递增、最大值队列递减)同步滑动,O(n) 时间,
* 内存只有窗口长度。NaN 不参与(整窗都是 NaN 时输出 NaN)。
*
* @param [IN] values 采样值。
* @param [IN] radius 邻域半径(窗口 = [i-radius, i+radius],两端按实际长度)。
* @param [OUT] outMin 邻域最小值。
* @param [OUT] outMax 邻域最大值。
*/
template <typename T>
void SlidingExtrema(const std::vector<T>& values, std::size_t radius,
std::vector<float>& outMin, std::vector<float>& outMax)
{
const std::size_t count = values.size();
outMin.assign(count, std::numeric_limits<float>::quiet_NaN());
outMax.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
std::deque<std::size_t> minQueue; // 递增:队首最小。
std::deque<std::size_t> maxQueue; // 递减:队首最大。
std::size_t filled = 0;
for (std::size_t index = 0; index < count; ++index)
{
const std::size_t wantHigh = std::min(count - 1, index + radius);
while (filled <= wantHigh)
{
const double incoming = values[filled];
if (std::isfinite(incoming))
{
while (!minQueue.empty() && values[minQueue.back()] >= incoming)
{
minQueue.pop_back();
}
minQueue.push_back(filled);
while (!maxQueue.empty() && values[maxQueue.back()] <= incoming)
{
maxQueue.pop_back();
}
maxQueue.push_back(filled);
}
++filled;
}
const std::size_t low = (index > radius) ? (index - radius) : 0;
while (!minQueue.empty() && minQueue.front() < low)
{
minQueue.pop_front();
}
while (!maxQueue.empty() && maxQueue.front() < low)
{
maxQueue.pop_front();
}
if (!minQueue.empty())
{
outMin[index] = static_cast<float>(values[minQueue.front()]);
}
if (!maxQueue.empty())
{
outMax[index] = static_cast<float>(values[maxQueue.front()]);
}
}
}
} // namespace
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int order,
std::vector<float>& out)
{
@@ -999,6 +1162,204 @@ namespace jdi
}
}
void ComputeDiffAmplify(const std::vector<double>& values, int window, int gain,
std::vector<float>& out)
{
const std::size_t count = values.size();
out.assign(count, std::numeric_limits<float>::quiet_NaN());
if (count == 0)
{
return;
}
// 窗口点数 X:局部“差异强度”的统计**窗口长度**(点数),窗口 = [i−r, i+r],
// r = (X−1)/2。窗口长度**应略大于纹波周期的一半**:
// 只有当窗口跨越的相位达到 180° 时,窗口内才必定包含一个波峰或波谷,
// “局部 |交流分量| 的峰值”因而恒等于纹波振幅 A,与相位无关
//(正常区域恒为 A、弱区恒为 A/2,比值恰为 2);窗口再小就会跟随相位起伏
//(把正常区域的波峰也一起放大),远大于一个周期时又会把弱区与邻近正常区一起统计掉。
// 窗口越短、混叠区(弱区边界各 ±r)越窄,对“只有一两个周期(甚至半周期)变弱”的
// 情形越有效。
const int span = std::max(3, std::min(window, 1000000));
const int strengthRadius = std::max(1, (span - 1) / 2);
// 趋势窗口取局部窗口的 2 倍(≈ 一个完整纹波周期):只有按整周期取平均,
// 趋势线才恰好是纹波的中心线(交流分量均值为零),待放大的弱区也就**不会**
// 因为自己变“扁”而把趋势线一起拉偏。
const int trendRadius = std::max(1, strengthRadius * 2);
// 包络窗口固定取局部窗口的 8 倍:用来给出“周边本来的振幅范围”,
// 既作为放大基准(中心值)也作为输出的夹取上下界。
const std::size_t envelopeRadius =
static_cast<std::size_t>(std::max(1, std::min(strengthRadius * 8, 16384)));
const double maxGain = static_cast<double>(std::max(1, gain));
// 1) 周边包络:邻域内采样值的最小/最大值(单调队列,O(n))。
// 邻域**必须明显大于一个纹波周期**,否则包络会跟着纹波起伏,
// 放大后会把相邻正常区域的相位也一并扭曲。
std::vector<float> envMin;
std::vector<float> envMax;
SlidingExtrema(values, envelopeRadius, envMin, envMax);
// 2) 局部差异强度 = 邻域内 |采样值 − 趋势线| 的**最大值**(即局部纹波振幅)。
//
// 这里刻意**不用**另外两种看起来更自然的做法:
// * “|采样值 − 趋势线| 的平均”:趋势线是低频量,会被待放大的弱区本身拉偏,
// 于是弱区周边本来正常的采样点也会被判成“差异偏小”而被一起放大;
// * “原始采样值的峰峰值”:峰峰值要同时覆盖波峰与波谷才与相位无关,
// 这要求窗口跨越 360°,窗口会大到把弱区与邻近正常区平均掉
// (实测一个周期宽的弱区只能恢复三分之一)。
// |交流分量| 的最大值只需要窗口跨越 180°(窗口约半个周期)就与相位无关,
// 既能准确识别弱区,又因为窗口很短而具备足够好的定位精度。
//
// 又因为增益下界恒为 1(见第 4 步),正常区域的增益恰好是 1,
// 输出与输入**逐位相同**,因此对其它数据点真正没有任何影响。
//
// 极值窗口按**实际长度**(端点截断)统计,端点因此会低估振幅、
// 导致增益被高估;这一端点效应由第 4 步的收敛系数消除。
std::vector<float> trend;
ComputeMovingAverage(values, trendRadius, trend);
std::vector<float> strength;
{
// `ac` 只在求局部最大值时用到:单独放在一个作用域里,
// 亿级采样下这一份 100 MB 级的中转数组能尽早归还给系统。
std::vector<float> ac(count, std::numeric_limits<float>::quiet_NaN());
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (std::isfinite(value) && std::isfinite(trend[index]))
{
ac[index] = static_cast<float>(std::fabs(value - static_cast<double>(trend[index])));
}
}
SlidingMax(ac, static_cast<std::size_t>(strengthRadius), strength);
}
// 3) 目标强度:差异强度的 75 分位。用直方图统计 → O(1) 额外内存,
// 且不会被个别尖峰抬高(用均值会),也不会被最尖处完全主导(用最大值会)。
//
// ⚠️ 统计时**排除首尾的端点收敛区**(见第 4 步):那里的趋势窗口被截断,
// “|交流分量| 的最大值”整体偏大,会把分位数抬高、导致目标强度偏大。
// 待统计的采样点数不足时退回全段统计。
const std::size_t edgeGuard =
static_cast<std::size_t>(2) * static_cast<std::size_t>(trendRadius);
// 剩下足够多的采样点时才有必要排除端点区(短信号就直接全段统计)。
constexpr std::size_t kEdgeGuardMinimum = 256;
const bool useGuard = (count > edgeGuard * 2 + kEdgeGuardMinimum);
const auto inGuard = [count, edgeGuard, useGuard](std::size_t index)
{
if (!useGuard)
{
return true;
}
return (index >= edgeGuard) && ((count - 1 - index) >= edgeGuard);
};
float maxStrength = 0.0f;
for (std::size_t index = 0; index < count; ++index)
{
const float value = strength[index];
if (inGuard(index) && std::isfinite(value) && value > maxStrength)
{
maxStrength = value;
}
}
if (!(maxStrength > 0.0f))
{
// 完全平坦(或全为 NaN)的信号:无差异可放大,等价于原样输出。
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (std::isfinite(value))
{
out[index] = static_cast<float>(value);
}
}
return;
}
constexpr int kBinCount = 2048;
std::vector<int> histogram(static_cast<std::size_t>(kBinCount), 0);
std::size_t sampleCount = 0;
for (std::size_t index = 0; index < count; ++index)
{
const float value = strength[index];
if (!inGuard(index) || !std::isfinite(value))
{
continue;
}
int bin = static_cast<int>(static_cast<double>(value) /
static_cast<double>(maxStrength) * kBinCount);
bin = std::max(0, std::min(bin, kBinCount - 1));
++histogram[static_cast<std::size_t>(bin)];
++sampleCount;
}
double target = static_cast<double>(maxStrength);
if (sampleCount > 0)
{
const std::size_t wanted = (sampleCount * 75) / 100;
std::size_t accumulated = 0;
for (int bin = 0; bin < kBinCount; ++bin)
{
accumulated += static_cast<std::size_t>(histogram[static_cast<std::size_t>(bin)]);
if (accumulated > wanted)
{
// 取桶中心,避免目标强度贴到桶的下边界而整体偏小。
target = (static_cast<double>(bin) + 0.5) / kBinCount *
static_cast<double>(maxStrength);
break;
}
}
}
// 4) 逐点增益并合成输出。
// * 基准 = 趋势线(纹波的中心线,与信号整体同步,纹波在其中上下摆动);
// * 增益下界为 1 → 局部振幅不比目标值小的采样点**输出与原值逐位相同**
// (基准被 1 倍增益完全消掉,因此对其它数据点没有任何影响);
// * 放大后的取值再夹到周边包络内 → **任何一点都不会超出原有的振幅范围**
// (尤其重要:被放大区域两侧自身的振幅本来就很大,不应被继续抬升)。
for (std::size_t index = 0; index < count; ++index)
{
const double value = values[index];
if (!std::isfinite(value))
{
continue;
}
const bool hasEnvelope = std::isfinite(envMin[index]) && std::isfinite(envMax[index]);
const double base =
std::isfinite(trend[index]) ? static_cast<double>(trend[index]) : value;
double pointGain = 1.0;
const float local = strength[index];
if (std::isfinite(local) && local > 0.0f)
{
pointGain = target / static_cast<double>(local);
}
pointGain = std::max(1.0, std::min(pointGain, maxGain));
// 端点收敛:首尾各 2r 个采样点的**趋势窗口与极值窗口都被截断**
//(趋势窗口半径 2r、极值窗口半径 r),局部振幅与中心线都被系统性估偏。
// 因此这些采样点一律保持原样(增益恰为 1),再在接下来的 2r 个点内
// 平滑过渡到完整增益,避免记录首尾被整片抬升。
//
// ⚠️ 不能靠“镜像延拓”解决:镜像后的窗口覆盖的真实下标集合与截断窗口相同
//(例如 i=16、r=40 时两者都只覆盖真实下标 0..56),因此不会带来任何新信息。
const double taperStart = static_cast<double>(trendRadius);
const double taperSpan = std::max(1.0, static_cast<double>(trendRadius));
const double distanceToEdge =
static_cast<double>(std::min(index, count - 1 - index));
const double taper =
std::max(0.0, std::min(1.0, (distanceToEdge - taperStart) / taperSpan));
pointGain = 1.0 + (pointGain - 1.0) * taper;
double amplified = base + pointGain * (value - base);
if (hasEnvelope)
{
amplified = std::max(static_cast<double>(envMin[index]),
std::min(amplified, static_cast<double>(envMax[index])));
}
out[index] = static_cast<float>(amplified);
}
}
const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const
{
for (const MeanFilterItem& item : meanFilterItems)
@@ -1069,6 +1430,46 @@ namespace jdi
edgeCenterItems.end());
}
const Lane::DiffAmpItem* Lane::FindDiffAmpItem(const SignalRef& ref) const
{
for (const DiffAmpItem& item : diffAmpItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::DiffAmpItem& Lane::EnsureDiffAmpItem(const SignalRef& ref)
{
for (DiffAmpItem& item : diffAmpItems)
{
if (item.ref == ref)
{
return item;
}
}
DiffAmpItem item;
item.ref = ref;
// 默认参数:窗口点数 X = 80(长度)、放大倍数 N = 2。
// 窗口长度需略大于纹波周期的一半(见 ComputeDiffAmplify 的说明),
// 80 点可覆盖周期约 160 采样以内的纹波。
item.points = 80;
item.order = 2;
diffAmpItems.push_back(item);
return diffAmpItems.back();
}
void Lane::RemoveDiffAmpItem(const SignalRef& ref)
{
diffAmpItems.erase(
std::remove_if(diffAmpItems.begin(), diffAmpItems.end(),
[&ref](const DiffAmpItem& item) { return item.ref == ref; }),
diffAmpItems.end());
}
int FindNearestSample(const DataFile& file, double time)
{
const std::size_t count = file.time.size();
@@ -1313,6 +1714,7 @@ namespace jdi
};
purgeRefs(lane.meanFilterItems);
purgeRefs(lane.edgeCenterItems);
purgeRefs(lane.diffAmpItems);
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
@@ -1604,10 +2006,18 @@ namespace jdi
lane.edgeCenterPanelOpen = source.edgeCenterPanelOpen;
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.diffAmp = source.diffAmp;
lane.diffAmpPanelOpen = source.diffAmpPanelOpen;
lane.diffAmpPanelX = source.diffAmpPanelX;
lane.diffAmpPanelY = source.diffAmpPanelY;
lane.meanFilter = source.meanFilter;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴条目。
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 差异放大条目。
lane.meanFilterItems.clear();
lane.edgeCenterItems.clear();
lane.diffAmpItems.clear();
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
{
if (item.ref == ref)
@@ -1622,13 +2032,16 @@ namespace jdi
lane.edgeCenterItems.push_back(item);
}
}
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
for (const Lane::DiffAmpItem& item : source.diffAmpItems)
{
if (item.ref == ref)
{
lane.diffAmpItems.push_back(item);
}
}
lane.signals.push_back(ref);
pieces.push_back(std::move(lane));
}
// 用 pieces 替换原网格(插入到原位置,因此其后方的网格整体顺次后移)。
m_lanes.erase(m_lanes.begin() + laneIndex);
m_lanes.insert(m_lanes.begin() + laneIndex, pieces.begin(), pieces.end());
@@ -1680,6 +2093,13 @@ namespace jdi
++item.ref.signalIndex;
}
}
for (Lane::DiffAmpItem& item : lane.diffAmpItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
}
}