背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。
一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
`FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
`LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。
二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
**最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
`PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
已经包含)。
三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
`lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
`#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
(注释:libstdc++ depends on POSIX-definitions of those functions)。
`g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。
四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
+ 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
(整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
790 lines
29 KiB
C++
790 lines
29 KiB
C++
// 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" (弱峰调整 " + 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<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"已构建 " + FormatInteger(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"已截取为新信号 ") +
|
||
FormatInteger(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"已构建 " + 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
|