refactor(application): 拆分 2165 行的 ApplicationCommands.cpp 为 4 个职责分片
原文件混杂了标签页、派生信号、菜单、工具栏、键盘鼠标、布局调试绘制六类职责,
是本项目最大的“大杂烩”单文件之一。按职责纯搬移(零逻辑改动):
- ApplicationTabs.cpp (355 行): 标签页新建/关闭/切换/重排、视图同步、窗口标题、
标签栏结果回调、标签页右键菜单、待提交改名。
- ApplicationDerive.cpp (786 行): 派生烘焙、重采样、选区截取(新建/就地)、克隆(含带调整)。
- ApplicationMenus.cpp (409 行): 绘图区右键菜单、菜单数值回写、主题切换、设置菜单、
菜单结果派发。
- ApplicationCommands.cpp (642 行): 分组/信号增删、移动与重排、文件对话框、工具栏动作、
键盘鼠标分发、布局边界绘制。
Makefile 按目录通配收集,无需登记新文件。
验证:release 零 error 零 warning;产物 793,088 -> 794,112 B(+1,024,LTO 内联边界变化,
< 0.13%,属结构性拆分不可避免的噪声)。
This commit is contained in:
1 parent
7a3fefb9c2
commit
8437100586
5 files changed
+1587
-1524
No files matched your search
@@ -0,0 +1,789 @@
|
||||
// Application 实现分片 —— **派生信号**:派生烘焙(均值滤波/中心轴/弱峰调整)、
|
||||
// 重采样、选区截取(新建 / 就地破坏性截取)、克隆(普通 / 带调整)。
|
||||
//
|
||||
// 共用的常量与工具函数在 ApplicationInternal.hpp(命名空间 `appdetail`,全部 inline)。
|
||||
// 本文件由 2026-11-04 代码审查从 `ApplicationCommands.cpp`(原 2,165 行)按职责拆出,
|
||||
// **纯搬移、零逻辑改动**。分片划分见 `Documents/Code_Review_Plan.md` 的 P11。
|
||||
#include "ApplicationInternal.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
bool Application::BuildDerivedSignal(const DerivedSignalRequest& request, bool makeVisible)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(request.groupIndex);
|
||||
if (group == nullptr || request.signalIndex < 0 ||
|
||||
request.signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(request.signalIndex)];
|
||||
const std::wstring baseName = source.name;
|
||||
|
||||
// 0. 先把源信号的「调整」变换快照下来(下面 `InsertDerivedSignal` 会移动信号存储,
|
||||
// 之后再取 `source` 会悬垂)。
|
||||
//
|
||||
// ⚠️ **变换要"烘进数据",而不是让新信号继承这套指纹**(用户明确要求):
|
||||
// 网格里的预览曲线画的是 `变换(滤波结果)`。若新信号只存 `滤波结果` 再**继承变换**,
|
||||
// 它的「调整」面板里就会莫名其妙带着一整套系数 / 镜像 / 偏移 —— 用户看到的是
|
||||
// "新信号只是继承了原信号的指纹,数据本身没变"。现在改为:
|
||||
// * 数值域变换(系数 → 绝对值 → 垂直镜像 → Y 偏移)**与**水平镜像(数据反序)
|
||||
// 全部作用在派生采样值上;
|
||||
// * 于是新信号**用默认外观**绘制出来就与预览曲线逐位一致。
|
||||
// * ⚠️ **只有「数值域」的变换烘进采样值;时间轴偏移留在样式上**(见 3b)。
|
||||
// 曾经把 `offsetTime` 也按 `time[i] - offsetTime` 重采样烘进采样值("新信号
|
||||
// 不启用任何调整"),但那是**有损**的:平移出时间轴范围的那一段只能写成 NaN,
|
||||
// 偏移稍大(≥ 数据跨度)就连一个有效采样都不剩 —— 用户点「构建为新信号」
|
||||
// 得到的是一条**空信号**(2026-10-29 报告)。派生信号与源信号共用同一条文件
|
||||
// 时间轴,无法用采样值表达超出轴范围的平移,因此时间偏移只能由样式表达。
|
||||
// 预览曲线本来也是用源样式的 `offsetTime` 绘制的,两条路径**显示完全一致**。
|
||||
SignalStyle sourceTransform;
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
SignalRef sourceRef;
|
||||
sourceRef.groupIndex = request.groupIndex;
|
||||
sourceRef.signalIndex = request.signalIndex;
|
||||
if (const SignalStyle* sourceStyle = m_dataSet.Style(sourceRef))
|
||||
{
|
||||
sourceTransform = *sourceStyle;
|
||||
hasSourceStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 计算派生采样值(与网格内的预览曲线共用同一实现,因此逐位一致)。
|
||||
// 多次迭代均值滤波形如「<原信号名称> (N点均值 M次迭代)」,
|
||||
// 弱峰调整形如「<原信号名称> (弱峰调整 M% N次迭代)」。
|
||||
std::vector<float> derived_values;
|
||||
SignalData derived;
|
||||
if (request.weakPeak)
|
||||
{
|
||||
ComputeWeakPeakAdjust(source.values, static_cast<double>(request.amountPercent) / 100.0,
|
||||
request.iterations, derived_values);
|
||||
derived.name = baseName + L" (弱峰调整 " + std::to_wstring(request.amountPercent) + L"% " +
|
||||
std::to_wstring(request.iterations) + L"次迭代)";
|
||||
}
|
||||
else
|
||||
{
|
||||
ComputeMovingAverageIterated(source.values, request.points, request.iterations, derived_values);
|
||||
derived.name = baseName + L" (" + std::to_wstring(request.points) + L"点均值 " +
|
||||
std::to_wstring(request.iterations) + L"次迭代)";
|
||||
}
|
||||
|
||||
// 2. 组装新信号(外观一律用默认值:变换已经烘进采样数据,不再是"继承的指纹")。
|
||||
// ⚠️ 新信号**沿用源信号的时间轴**(`axisIndex`):派生只改采样值,不改时间基准。
|
||||
derived.axisIndex = source.axisIndex;
|
||||
derived.values.assign(derived_values.begin(), derived_values.end());
|
||||
|
||||
// 2b. 把源信号的「调整」变换**烘进派生采样值**:
|
||||
// * 数值域(**公式映射** → 系数 → 绝对值 → 垂直镜像 → Y 偏移)逐点套用
|
||||
// `ApplyDrawTransform`(它本来就是“公式最先结算”,与绘制路径同源);
|
||||
// * 水平镜像 = 数据反序,等价于把采样数组整体反转。
|
||||
// ⚠️ `ApplyDrawTransform` 是逐点函数,因此"先变换再反转"与"先反转再变换"等价。
|
||||
// ⚠️⚠️ **公式映射必须算作“有变换”**(用户 2026-11-03):旧判定只列了系数/绝对值/
|
||||
// 垂直镜像/Y 偏移,于是带公式的信号派生出来的新信号**丢掉了公式**(不烘也不继承)
|
||||
// ⇒ 新信号曲线与预览曲线不一致。
|
||||
if (hasSourceStyle)
|
||||
{
|
||||
const bool valueTransform =
|
||||
sourceTransform.HasFormula() || sourceTransform.gain != 1.0f ||
|
||||
sourceTransform.absolute || sourceTransform.mirrorVertical ||
|
||||
sourceTransform.offsetY != 0.0f;
|
||||
if (valueTransform)
|
||||
{
|
||||
for (double& value : derived.values)
|
||||
{
|
||||
value = ApplyDrawTransform(sourceTransform, value);
|
||||
}
|
||||
}
|
||||
if (sourceTransform.mirrorHorizontal)
|
||||
{
|
||||
std::reverse(derived.values.begin(), derived.values.end());
|
||||
}
|
||||
}
|
||||
|
||||
// 2c.(已删除)时间轴偏移**不再**按 `time[i] - offsetTime` 重采样烘进采样值。
|
||||
//
|
||||
// 旧实现为了让"新信号的「调整」面板保持默认"而把平移烘进数据,代价是**有损**:
|
||||
// 平移出时间轴范围的那一段只能写成 NaN,偏移 ≥ 数据跨度时**整条信号全变 NaN**
|
||||
//(用户 2026-10-29 报告的"派生出来是一条空信号");即便偏移很小,也会在两端
|
||||
// 留下与实际曲线不符的空洞。
|
||||
// 派生信号与源信号共用同一条文件时间轴,无法用采样值表达超出轴范围的平移,
|
||||
// 因此时间偏移改由**新信号样式**表达(见 3b)——预览曲线本来就用源样式的
|
||||
// `offsetTime` 绘制,所以两条路径的**显示完全一致**,且数据一点都不会丢。
|
||||
|
||||
// 3. 插入到原信号的下方(数据层 + 各标签页引用重映射)。
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(request.groupIndex, request.signalIndex, std::move(derived),
|
||||
makeVisible, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3b. 时间轴偏移:**只**写进新信号的样式(不再烘进采样值,见 2c 的说明)。
|
||||
// 绘制路径本来就把采样画在 `t + offsetTime` 处,因此新信号的曲线与预览曲线
|
||||
// 逐点重合,同时采样数据保持完整(不会因为平移越界而变成 NaN)。
|
||||
if (hasSourceStyle && sourceTransform.offsetTime != 0.0)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{request.groupIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->offsetTime = sourceTransform.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 数据层结构变化:刷新列表、作废折线/派生缓存并重绘。
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::BuildResampledSignal(int groupIndex, int signalIndex, double interval)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||||
if (group == nullptr || signalIndex < 0 ||
|
||||
signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(interval) || interval <= 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(signalIndex)];
|
||||
const TimeAxis& axis = group->AxisOf(source);
|
||||
const std::wstring baseName = source.name;
|
||||
|
||||
// 0. 快照源信号的「调整」变换(插入会移动信号存储,之后取 `source` 会悬垂)。
|
||||
SignalStyle sourceStyle;
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex});
|
||||
if (style != nullptr)
|
||||
{
|
||||
sourceStyle = *style;
|
||||
hasSourceStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 收集「变换后」的采样点(升序)。变换与绘制一致:
|
||||
// 公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移(时间轴偏移不烘值)。
|
||||
// ⚠️ 判定统一走 `HasValueTransform()`(含公式映射),否则带公式的信号
|
||||
// 重采样时会**丢掉公式**(用户 2026-11-03 报告)。
|
||||
const std::size_t count = std::min(axis.time.size(), source.values.size());
|
||||
std::vector<double> sourceTimes;
|
||||
std::vector<double> sourceValues;
|
||||
sourceTimes.reserve(count);
|
||||
sourceValues.reserve(count);
|
||||
bool monotonic = true;
|
||||
double previous = 0.0;
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double time = axis.time[index];
|
||||
const double value = source.values[index];
|
||||
if (!std::isfinite(time) || !std::isfinite(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!sourceTimes.empty())
|
||||
{
|
||||
if (time < previous)
|
||||
{
|
||||
monotonic = false;
|
||||
break;
|
||||
}
|
||||
if (time == previous)
|
||||
{
|
||||
continue; // 重复时刻只保留第一个
|
||||
}
|
||||
}
|
||||
previous = time;
|
||||
sourceTimes.push_back(time);
|
||||
sourceValues.push_back(hasSourceStyle ? ApplyDrawTransform(sourceStyle, value) : value);
|
||||
}
|
||||
if (!monotonic || sourceTimes.size() < 2)
|
||||
{
|
||||
m_plot.ShowPersistentHint(!monotonic ? L"重采样失败:该信号的时间轴不是单调递增的。"
|
||||
: L"重采样失败:有效采样点不足两个。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 生成目标时间刻度(等间隔)。
|
||||
const double t0 = sourceTimes.front();
|
||||
const double t1 = sourceTimes.back();
|
||||
const double span = t1 - t0;
|
||||
if (!(span > 0.0))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:该信号的时间跨度为 0。");
|
||||
return false;
|
||||
}
|
||||
// 采样点数上限:防止用户把间隔输得太小导致内存爆炸。
|
||||
constexpr double kMaxResamplePoints = 20000000.0;
|
||||
const double stepsExact = std::floor(span / interval + 1.0e-9) + 1.0;
|
||||
if (!(stepsExact >= 2.0))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:重采样间隔大于信号时间跨度。");
|
||||
return false;
|
||||
}
|
||||
if (stepsExact > kMaxResamplePoints)
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"重采样失败:目标采样点过多(超过 2000 万),请增大重采样间隔。");
|
||||
return false;
|
||||
}
|
||||
const std::size_t targetCount = static_cast<std::size_t>(stepsExact);
|
||||
|
||||
TimeAxis newAxis;
|
||||
newAxis.name = axis.name;
|
||||
newAxis.time.resize(targetCount);
|
||||
newAxis.monotonic = true;
|
||||
std::vector<double> resampled(targetCount);
|
||||
std::size_t cursor = 0;
|
||||
const std::size_t last = sourceTimes.size() - 1;
|
||||
for (std::size_t index = 0; index < targetCount; ++index)
|
||||
{
|
||||
const double time = std::min(t1, t0 + static_cast<double>(index) * interval);
|
||||
newAxis.time[index] = time;
|
||||
// 单调推进游标(O(n) 总代价)。
|
||||
while (cursor < last && sourceTimes[cursor + 1] < time)
|
||||
{
|
||||
++cursor;
|
||||
}
|
||||
if (cursor >= last)
|
||||
{
|
||||
resampled[index] = sourceValues[last];
|
||||
continue;
|
||||
}
|
||||
const double ta = sourceTimes[cursor];
|
||||
const double tb = sourceTimes[cursor + 1];
|
||||
if (time <= ta)
|
||||
{
|
||||
resampled[index] = sourceValues[cursor];
|
||||
continue;
|
||||
}
|
||||
if (tb <= ta)
|
||||
{
|
||||
resampled[index] = sourceValues[cursor];
|
||||
continue;
|
||||
}
|
||||
const double ratio = (time - ta) / (tb - ta);
|
||||
const double a = sourceValues[cursor];
|
||||
const double b = sourceValues[cursor + 1];
|
||||
resampled[index] = a + (b - a) * ratio;
|
||||
}
|
||||
newAxis.minTime = t0;
|
||||
newAxis.maxTime = newAxis.time.back();
|
||||
newAxis.finiteCount = targetCount;
|
||||
|
||||
// 3. 追加新时间轴(重采样后的采样刻度无法复用源信号的时间轴)。
|
||||
std::size_t axisIndex = 0;
|
||||
if (!m_dataSet.AppendGroupAxis(groupIndex, std::move(newAxis), axisIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 组装新信号并插入到源信号下方。
|
||||
SignalData derived;
|
||||
derived.name = baseName + L" (重采样 " + FormatFrequency(1.0 / interval) + L")";
|
||||
derived.axisIndex = axisIndex;
|
||||
derived.values = std::move(resampled);
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(groupIndex, signalIndex, std::move(derived), true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 时间轴偏移**不烘值**:留在新信号的样式上(与其它派生信号同一规则)。
|
||||
if (hasSourceStyle && sourceStyle.offsetTime != 0.0)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{groupIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->offsetTime = sourceStyle.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 选区截取
|
||||
|
||||
bool Application::BuildClippedSignal(int groupIndex, int signalIndex, double begin, double end)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(groupIndex);
|
||||
if (group == nullptr || signalIndex < 0 ||
|
||||
signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!std::isfinite(begin) || !std::isfinite(end))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double low = std::min(begin, end);
|
||||
const double high = std::max(begin, end);
|
||||
if (!(high > low))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:双光标区间宽度为 0。");
|
||||
return false;
|
||||
}
|
||||
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(signalIndex)];
|
||||
const TimeAxis& axis = group->AxisOf(source);
|
||||
const std::wstring baseName = source.name;
|
||||
|
||||
// 源信号的整套「调整」快照(插入会移动信号存储,之后不能再解引用 `source`)。
|
||||
SignalStyle sourceStyle;
|
||||
bool hasSourceStyle = false;
|
||||
{
|
||||
const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex});
|
||||
if (style != nullptr)
|
||||
{
|
||||
sourceStyle = *style;
|
||||
hasSourceStyle = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 逐点收集**区间内**的原始采样(不烘焙任何调整)。
|
||||
// ⚠️⚠️ 比较必须用**显示时刻**(`time + offsetTime`):双光标给出的是屏幕上看到的时刻,
|
||||
// 而采样画在 `time + offsetTime` 处(`offsetTime` 不进采样值,只平移绘制位置)。
|
||||
// 若直接拿 `time` 与选区比较,带时间轴偏移的信号会截错区间(用户 2026-11-03)。
|
||||
const double timeShift = hasSourceStyle ? sourceStyle.offsetTime : 0.0;
|
||||
const std::size_t count = std::min(axis.time.size(), source.values.size());
|
||||
std::vector<double> keptTimes;
|
||||
std::vector<double> keptValues;
|
||||
keptTimes.reserve(count);
|
||||
keptValues.reserve(count);
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const double time = axis.time[index];
|
||||
if (!std::isfinite(time))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double shown = time + timeShift;
|
||||
if (shown < low || shown > high)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
keptTimes.push_back(time);
|
||||
keptValues.push_back(source.values[index]);
|
||||
}
|
||||
if (keptValues.size() < 2)
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:区间内的有效采样点不足两个。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 名称尾缀:`(AAAA - BBBB)`,秒、保留三位小数(用户 2026-11-03 明确要求)。
|
||||
// 数值取**显示时刻**(与双光标读数一致),因此加上 `timeShift`。
|
||||
const std::wstring suffix =
|
||||
L" (" + FormatDecimalFixed(keptTimes.front() + timeShift, 3) + L"s - " +
|
||||
FormatDecimalFixed(keptTimes.back() + timeShift, 3) + L"s)";
|
||||
|
||||
// 3. 新建时间轴(截取后的采样刻度无法复用源信号的时间轴)。
|
||||
TimeAxis newAxis;
|
||||
newAxis.name = axis.name;
|
||||
newAxis.time = std::move(keptTimes);
|
||||
newAxis.monotonic = true;
|
||||
newAxis.finiteCount = newAxis.time.size();
|
||||
newAxis.minTime = newAxis.time.front();
|
||||
newAxis.maxTime = newAxis.time.back();
|
||||
std::size_t axisIndex = 0;
|
||||
if (!m_dataSet.AppendGroupAxis(groupIndex, std::move(newAxis), axisIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 组装新信号并插入到源信号下方。
|
||||
SignalData derived;
|
||||
derived.name = baseName + suffix;
|
||||
derived.axisIndex = axisIndex;
|
||||
derived.values = std::move(keptValues);
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(groupIndex, signalIndex, std::move(derived), true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 继承源信号的**整套调整**(不烘值 ⇒ 新曲线与源信号在区间内逐像素重合)。
|
||||
if (hasSourceStyle)
|
||||
{
|
||||
if (SignalStyle* derivedStyle = m_dataSet.Style(SignalRef{groupIndex, newIndex}))
|
||||
{
|
||||
derivedStyle->color = sourceStyle.color;
|
||||
derivedStyle->customColor = sourceStyle.customColor;
|
||||
derivedStyle->opacity = sourceStyle.opacity;
|
||||
derivedStyle->lineWidth = sourceStyle.lineWidth;
|
||||
derivedStyle->mode = sourceStyle.mode;
|
||||
derivedStyle->formula = sourceStyle.formula;
|
||||
derivedStyle->gain = sourceStyle.gain;
|
||||
derivedStyle->absolute = sourceStyle.absolute;
|
||||
derivedStyle->mirrorVertical = sourceStyle.mirrorVertical;
|
||||
derivedStyle->mirrorHorizontal = sourceStyle.mirrorHorizontal;
|
||||
derivedStyle->offsetY = sourceStyle.offsetY;
|
||||
derivedStyle->offsetTime = sourceStyle.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::ClipSignalInPlace(int groupIndex, int signalIndex, double begin, double end)
|
||||
{
|
||||
if (!std::isfinite(begin) || !std::isfinite(end))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const double low = std::min(begin, end);
|
||||
const double high = std::max(begin, end);
|
||||
if (!(high > low))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:双光标区间宽度为 0。");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 破坏性截取会移动/释放采样 ⇒ 先暂停后台派生计算,避免它读到已释放的数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
// ⚠️ 双光标给的是**显示时刻**,而采样时刻是 `time`;这里换算回采样时刻区间
|
||||
// (`offsetTime` 不烘进采样值,只平移绘制位置)。
|
||||
double timeShift = 0.0;
|
||||
if (const SignalStyle* style = m_dataSet.Style(SignalRef{groupIndex, signalIndex}))
|
||||
{
|
||||
timeShift = style->offsetTime;
|
||||
}
|
||||
// ⚠️⚠️ **必须先让该信号独占时间轴**再裁剪:同组信号常常共享同一条时间轴,
|
||||
// 直接裁剪会把其它信号的时间基准一起改短 ⇒ 它们的采样与时间错位
|
||||
//(用户 2026-11-03 报告:"多个信号同时截取时截取出来的信号并不对")。
|
||||
std::size_t ownAxis = 0;
|
||||
if (!m_dataSet.ForkAxisForSignal(groupIndex, signalIndex, ownAxis))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:无法为该信号创建独立时间轴。");
|
||||
return false;
|
||||
}
|
||||
if (!m_dataSet.ClipSignalRange(groupIndex, signalIndex, low - timeShift, high - timeShift))
|
||||
{
|
||||
m_plot.ShowPersistentHint(L"选区截取失败:区间内的有效采样点不足两个。");
|
||||
return false;
|
||||
}
|
||||
SyncTabsToDataSet();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
|
||||
bool makeVisible, int& newIndex)
|
||||
{
|
||||
newIndex = -1;
|
||||
// 插入派生信号会移动该分组的信号存储:先暂停后台计算,避免其读取已移动的数据。
|
||||
m_plot.PauseDerivedAsync();
|
||||
if (!m_dataSet.InsertDerivedSignal(groupIndex, sourceIndex, std::move(signal), newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (TabConfig& tab : m_tabs)
|
||||
{
|
||||
tab.RemapAfterSignalInsert(groupIndex, newIndex);
|
||||
}
|
||||
|
||||
// 在当前标签页中勾选新信号,并把它所在的网格移到原信号网格的下方。
|
||||
if (makeVisible && !m_tabs.empty())
|
||||
{
|
||||
const SignalRef insertedRef{groupIndex, newIndex};
|
||||
ActiveTab().SetVisible(insertedRef, true);
|
||||
const SignalRef sourceRef{groupIndex, sourceIndex};
|
||||
ActiveTab().MoveLaneOfSignalBelow(insertedRef, sourceRef);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::CloneSignal(const SignalRef& sourceRef)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(sourceRef.groupIndex);
|
||||
if (group == nullptr || sourceRef.signalIndex < 0 ||
|
||||
sourceRef.signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(sourceRef.signalIndex)];
|
||||
|
||||
// 源信号的样式快照(插入会移动信号存储,之后不能再解引用 `source`)。
|
||||
SignalStyle transform;
|
||||
bool hasStyle = false;
|
||||
if (const SignalStyle* style = m_dataSet.Style(sourceRef))
|
||||
{
|
||||
transform = *style;
|
||||
hasStyle = true;
|
||||
}
|
||||
|
||||
// ⚠️ 用户 2026-11-03 明确要求:「克隆该信号」**不做任何烘焙**。
|
||||
// 新信号直接使用源信号的**原始采样数据**,配方上只沿用:
|
||||
// * 外观(颜色 / 自定义色 / 透明度 / 线宽 / 绘制方式);
|
||||
// * **时间轴偏移**(`offsetTime` 只能留在样式上 —— 共用同一条时间轴,
|
||||
// 烘焙进采样值会在越界段写 NaN,见 `BuildDerivedSignal` 2c 的说明)。
|
||||
// 公式映射 / 系数 / 绝对值 / 镜像 / Y 偏移一律**保持默认**(不烘也不应用)。
|
||||
// 需要“连调整一起复制”请用「克隆该信号(带调整)」(`CloneSignalAdjusted`)。
|
||||
SignalData clone;
|
||||
clone.name = L"*" + source.name;
|
||||
clone.axisIndex = source.axisIndex;
|
||||
clone.values = source.values;
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(sourceRef.groupIndex, sourceRef.signalIndex, std::move(clone),
|
||||
true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 外观:克隆要**看起来与原信号一样**,但所有数值调整保持默认值。
|
||||
if (SignalStyle* cloned = m_dataSet.Style(SignalRef{sourceRef.groupIndex, newIndex}))
|
||||
{
|
||||
if (hasStyle)
|
||||
{
|
||||
cloned->color = transform.color;
|
||||
cloned->customColor = transform.customColor;
|
||||
cloned->opacity = transform.opacity;
|
||||
cloned->lineWidth = transform.lineWidth;
|
||||
cloned->mode = transform.mode;
|
||||
// ⚠️ 只保留时间轴偏移,其余调整保持默认(用户 2026-11-03 明确要求)。
|
||||
cloned->offsetTime = transform.offsetTime;
|
||||
}
|
||||
// ⚠️ 单点信号默认会改成「散点 + 最大线宽」(`InsertDerivedSignal` 里的规则),
|
||||
// 克隆应该用**原信号的样式**,因此上面几行把它覆盖回去了。
|
||||
}
|
||||
|
||||
SyncTabsToDataSet();
|
||||
SetStatusMessage(L"已克隆信号:" + m_dataSet.GroupAt(sourceRef.groupIndex)
|
||||
->signals[static_cast<std::size_t>(newIndex)]
|
||||
.name,
|
||||
false);
|
||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::CloneSignalAdjusted(const SignalRef& sourceRef)
|
||||
{
|
||||
const SignalGroup* group = m_dataSet.GroupAt(sourceRef.groupIndex);
|
||||
if (group == nullptr || sourceRef.signalIndex < 0 ||
|
||||
sourceRef.signalIndex >= static_cast<int>(group->signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SignalData& source = group->signals[static_cast<std::size_t>(sourceRef.signalIndex)];
|
||||
|
||||
SignalStyle transform;
|
||||
bool hasStyle = false;
|
||||
if (const SignalStyle* style = m_dataSet.Style(sourceRef))
|
||||
{
|
||||
transform = *style;
|
||||
hasStyle = true;
|
||||
}
|
||||
|
||||
// 「克隆该信号(带调整)」:同样是**原始采样数据**(不做烘焙),
|
||||
// 区别只在样式——完整沿用源信号的整套调整参数(公式映射 / 系数 / Y 偏移 /
|
||||
// 时间轴偏移 / 水平与垂直镜像 / 绝对值),因此两条曲线**逐像素重合**,
|
||||
// 且新信号可以独立继续调参而不影响源信号(用户 2026-11-03 明确要求)。
|
||||
SignalData clone;
|
||||
clone.name = L"*" + source.name;
|
||||
clone.axisIndex = source.axisIndex;
|
||||
clone.values = source.values;
|
||||
|
||||
int newIndex = -1;
|
||||
if (!InsertSignalBelowSource(sourceRef.groupIndex, sourceRef.signalIndex, std::move(clone),
|
||||
true, newIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SignalStyle* cloned = m_dataSet.Style(SignalRef{sourceRef.groupIndex, newIndex}))
|
||||
{
|
||||
if (hasStyle)
|
||||
{
|
||||
// 外观。
|
||||
cloned->color = transform.color;
|
||||
cloned->customColor = transform.customColor;
|
||||
cloned->opacity = transform.opacity;
|
||||
cloned->lineWidth = transform.lineWidth;
|
||||
cloned->mode = transform.mode;
|
||||
// 整套「调整」参数(与源信号完全一致)。
|
||||
cloned->formula = transform.formula;
|
||||
cloned->gain = transform.gain;
|
||||
cloned->absolute = transform.absolute;
|
||||
cloned->mirrorVertical = transform.mirrorVertical;
|
||||
cloned->mirrorHorizontal = transform.mirrorHorizontal;
|
||||
cloned->offsetY = transform.offsetY;
|
||||
cloned->offsetTime = transform.offsetTime;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTabsToDataSet();
|
||||
SetStatusMessage(L"已克隆信号(带调整):" + m_dataSet.GroupAt(sourceRef.groupIndex)
|
||||
->signals[static_cast<std::size_t>(newIndex)]
|
||||
.name,
|
||||
false);
|
||||
InvalidateRect(m_hwnd, nullptr, TRUE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::ApplyPendingDerivedBuild()
|
||||
{
|
||||
// 「重采样」的构建流程与均值/弱峰不同(要新建时间轴),单独处理。
|
||||
{
|
||||
int laneIndex = -1;
|
||||
std::vector<PlotView::ResampleBuild> items;
|
||||
if (m_plot.TakePendingResampleBuild(laneIndex, items) && !items.empty())
|
||||
{
|
||||
// **同一文件内必须按下标降序处理**(每次插入只让插入点之后的信号下标 +1)。
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const PlotView::ResampleBuild& a, const PlotView::ResampleBuild& b)
|
||||
{
|
||||
if (a.groupIndex != b.groupIndex)
|
||||
{
|
||||
return a.groupIndex < b.groupIndex;
|
||||
}
|
||||
return a.signalIndex > b.signalIndex;
|
||||
});
|
||||
int built = 0;
|
||||
double lastInterval = 0.0;
|
||||
for (const PlotView::ResampleBuild& item : items)
|
||||
{
|
||||
if (BuildResampledSignal(item.groupIndex, item.signalIndex, item.interval))
|
||||
{
|
||||
++built;
|
||||
lastInterval = item.interval;
|
||||
}
|
||||
}
|
||||
if (built > 0)
|
||||
{
|
||||
SetStatusMessage(L"已构建 " + std::to_wstring(built) + L" 条重采样信号(频率 " +
|
||||
FormatFrequency(lastInterval > 0.0 ? 1.0 / lastInterval : 0.0) +
|
||||
L")。",
|
||||
false);
|
||||
}
|
||||
return built > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 「选区截取」:两个按钮(截取为新信号 / 原始信号截取)共用同一份区间。
|
||||
{
|
||||
int laneIndex = -1;
|
||||
std::vector<PlotView::ClipBuild> items;
|
||||
if (m_plot.TakePendingClipBuild(laneIndex, items) && !items.empty())
|
||||
{
|
||||
// 同一文件内必须按下标降序处理(插入会让后续下标 +1)。
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const PlotView::ClipBuild& a, const PlotView::ClipBuild& b)
|
||||
{
|
||||
if (a.groupIndex != b.groupIndex)
|
||||
{
|
||||
return a.groupIndex < b.groupIndex;
|
||||
}
|
||||
return a.signalIndex > b.signalIndex;
|
||||
});
|
||||
int built = 0;
|
||||
for (const PlotView::ClipBuild& item : items)
|
||||
{
|
||||
if (item.destructive
|
||||
? ClipSignalInPlace(item.groupIndex, item.signalIndex, item.begin, item.end)
|
||||
: BuildClippedSignal(item.groupIndex, item.signalIndex, item.begin, item.end))
|
||||
{
|
||||
++built;
|
||||
}
|
||||
}
|
||||
if (built > 0)
|
||||
{
|
||||
SetStatusMessage((items.front().destructive ? L"已截取 " : L"已截取为新信号 ") +
|
||||
std::to_wstring(built) + L" 条。",
|
||||
false);
|
||||
}
|
||||
return built > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
|
||||
// 统一整理成同一份构建清单后按下标降序处理。
|
||||
std::vector<DerivedSignalRequest> requests;
|
||||
{
|
||||
int laneIndex = -1;
|
||||
std::vector<PlotView::MeanFilterBuild> items;
|
||||
if (m_plot.TakePendingMeanFilterBuild(laneIndex, items))
|
||||
{
|
||||
for (const PlotView::MeanFilterBuild& item : items)
|
||||
{
|
||||
DerivedSignalRequest request;
|
||||
request.groupIndex = item.groupIndex;
|
||||
request.signalIndex = item.signalIndex;
|
||||
request.points = item.points;
|
||||
request.iterations = item.iterations;
|
||||
request.weakPeak = false;
|
||||
requests.push_back(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
int laneIndex = -1;
|
||||
std::vector<PlotView::WeakPeakBuild> items;
|
||||
if (m_plot.TakePendingWeakPeakBuild(laneIndex, items))
|
||||
{
|
||||
for (const PlotView::WeakPeakBuild& item : items)
|
||||
{
|
||||
DerivedSignalRequest request;
|
||||
request.groupIndex = item.groupIndex;
|
||||
request.signalIndex = item.signalIndex;
|
||||
request.amountPercent = item.amountPercent;
|
||||
request.iterations = item.iterations;
|
||||
request.weakPeak = true;
|
||||
requests.push_back(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (requests.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_tabs.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// **同一文件内必须按下标降序处理**:每次插入只让插入点之后的信号下标 +1,
|
||||
// 降序处理可保证尚未处理的信号下标始终有效(升序会把新插入的信号当成原信号)。
|
||||
std::sort(requests.begin(), requests.end(),
|
||||
[](const DerivedSignalRequest& a, const DerivedSignalRequest& b)
|
||||
{
|
||||
if (a.groupIndex != b.groupIndex)
|
||||
{
|
||||
return a.groupIndex < b.groupIndex;
|
||||
}
|
||||
return a.signalIndex > b.signalIndex;
|
||||
});
|
||||
|
||||
int built = 0;
|
||||
DerivedSignalRequest last;
|
||||
for (const DerivedSignalRequest& request : requests)
|
||||
{
|
||||
if (BuildDerivedSignal(request, true))
|
||||
{
|
||||
++built;
|
||||
last = request;
|
||||
}
|
||||
}
|
||||
|
||||
if (built > 0)
|
||||
{
|
||||
std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 ";
|
||||
if (last.weakPeak)
|
||||
{
|
||||
text += L"弱峰调整 " + std::to_wstring(last.amountPercent) + L"% " +
|
||||
std::to_wstring(last.iterations) + L"次迭代信号。";
|
||||
}
|
||||
else
|
||||
{
|
||||
text += std::to_wstring(last.points) + L"点均值 " +
|
||||
std::to_wstring(last.iterations) + L"次迭代滤波信号。";
|
||||
}
|
||||
SetStatusMessage(text, false);
|
||||
}
|
||||
return built > 0;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user