// 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(group->signals.size())) { return false; } const SignalData& source = group->signals[static_cast(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 derived_values; SignalData derived; if (request.weakPeak) { ComputeWeakPeakAdjust(source.values, static_cast(request.amountPercent) / 100.0, request.iterations, derived_values); derived.name = baseName + L" (弱峰调整 " + FormatInteger(request.amountPercent) + L"% " + FormatInteger(request.iterations) + L"次迭代)"; } else { ComputeMovingAverageIterated(source.values, request.points, request.iterations, derived_values); derived.name = baseName + L" (" + FormatInteger(request.points) + L"点均值 " + FormatInteger(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(group->signals.size())) { return false; } if (!std::isfinite(interval) || interval <= 0.0) { return false; } const SignalData& source = group->signals[static_cast(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 sourceTimes; std::vector 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(stepsExact); TimeAxis newAxis; newAxis.name = axis.name; newAxis.time.resize(targetCount); newAxis.monotonic = true; std::vector 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(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(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(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 keptTimes; std::vector 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(group->signals.size())) { return false; } const SignalData& source = group->signals[static_cast(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(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(group->signals.size())) { return false; } const SignalData& source = group->signals[static_cast(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(newIndex)] .name, false); InvalidateRect(m_hwnd, nullptr, TRUE); return true; } bool Application::ApplyPendingDerivedBuild() { // 「重采样」的构建流程与均值/弱峰不同(要新建时间轴),单独处理。 { int laneIndex = -1; std::vector 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"已构建 " + FormatInteger(built) + L" 条重采样信号(频率 " + FormatFrequency(lastInterval > 0.0 ? 1.0 / lastInterval : 0.0) + L")。", false); } return built > 0; } } // 「选区截取」:两个按钮(截取为新信号 / 原始信号截取)共用同一份区间。 { int laneIndex = -1; std::vector 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"已截取为新信号 ") + FormatInteger(built) + L" 条。", false); } return built > 0; } } // 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致, // 统一整理成同一份构建清单后按下标降序处理。 std::vector requests; { int laneIndex = -1; std::vector 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 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"已构建 " + FormatInteger(built) + L" 条 "; if (last.weakPeak) { text += L"弱峰调整 " + FormatInteger(last.amountPercent) + L"% " + FormatInteger(last.iterations) + L"次迭代信号。"; } else { text += FormatInteger(last.points) + L"点均值 " + FormatInteger(last.iterations) + L"次迭代滤波信号。"; } SetStatusMessage(text, false); } return built > 0; } } // namespace jdi