feat(resample): 实验性功能新增「重采样」(位于「自由标记」上方)

- Lane::ResampleItem + resample/resampleItems/resamplePanel/resampleScroll,
  并在 LaneCarry / SplitLane / PruneLaneSignalItems / RemapAfterSignalInsert|Removal 同步
- 面板:可滚动分组列表(信号色条目栏 + 复选框 + 展开三角),展开后 4 行:
  最大/最小数据间隔(只读,勾选时一次性缓存,绘制不扫描采样)+ 重采样间隔/频率(输入)
- 输入框联动:改间隔同步频率,改频率同步间隔(输入单位分别为秒 / Hz)
- 「构建为新信号」:DataSet::AppendGroupAxis 追加新时间轴 + InsertDerivedSignal,
  采样值先按公式/系数/绝对值/镜像/Y偏移变换再等间隔线性插值,时间轴偏移不烘值
- 新信号名 <原信号> (重采样 <自适应单位频率>);Utf8Util::FormatFrequency 为唯一实现
- 非单调轴 / 点太少 / 间隔过大 / 点数超 2000 万 ⇒ 拒绝构建并给出自绘提示
- 顺带加入 FormulaEval(公式编译/求值,供 ApplyDrawTransform 与后续 UI 使用)
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-02 20:41:26 +08:00
1 parent 30e9d65c30
commit 1e1c2432ae
19 files changed
+1979 -56

No files matched your search

@@ -775,6 +775,173 @@ namespace jdi
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 偏移(时间轴偏移不烘值)。
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::InsertSignalBelowSource(int groupIndex, int sourceIndex, SignalData&& signal,
bool makeVisible, int& newIndex)
{
@@ -902,6 +1069,43 @@ namespace jdi
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;
}
}
// 两类「构建为新信号」请求(均值滤波 / 弱峰调整)机制完全一致,
// 统一整理成同一份构建清单后按下标降序处理。
std::vector<DerivedSignalRequest> requests;
@@ -1127,6 +1331,13 @@ namespace jdi
freeMarker.label = L"自由标记";
freeMarker.id = PLOT_CMD_LANE_FREE_MARKER;
freeMarker.checked = m_plot.LaneFreeMarker(laneIndex);
// 「重采样」位于「自由标记」**上方**(用户 2026-11-02 要求)。
MenuItem resample;
resample.label = L"重采样";
resample.id = PLOT_CMD_LANE_RESAMPLE;
resample.checked = m_plot.LaneResample(laneIndex);
experimental.submenu.push_back(resample);
experimental.submenu.push_back(freeMarker);
}
items.push_back(experimental);
@@ -1355,6 +1566,15 @@ namespace jdi
case MenuScope::SIGNAL_PANEL:
{
const bool needRelayout = m_panel.OnMenuCommand(result);
// 「公式映射」等就地输入可能产生**非模态**警告(不使用 Windows 消息框),
// 由绘图区底部的自绘提示浮层显示(与加载失败提示同一套)。
{
std::wstring hint;
if (m_panel.TakePendingHint(hint))
{
m_plot.ShowPersistentHint(hint);
}
}
if (needRelayout)
{
Relayout();
@@ -1418,6 +1638,10 @@ namespace jdi
// 「实验性功能 → 自由标记」:切换网格内跟随鼠标的纵向光标与固定数据点面板。
m_plot.ToggleLaneFreeMarker(laneIndex);
break;
case PLOT_CMD_LANE_RESAMPLE:
// 「实验性功能 → 重采样」:切换重采样参数面板。
m_plot.ToggleLaneResample(laneIndex);
break;
case PLOT_CMD_LANE_SPLIT:
// 「拆分当前网格」:信号按个拆成独立网格,新网格顺次插入其下方。
m_plot.SplitLane(laneIndex);