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

+185 -9
View File
@@ -391,7 +391,9 @@ namespace jdi
m_editTarget == PlotEditTarget::MEAN_FILTER_ITERATIONS ||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX ||
m_editTarget == PlotEditTarget::MEAN_POINTS_MAX ||
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX)
m_editTarget == PlotEditTarget::MEAN_ITERATIONS_MAX ||
m_editTarget == PlotEditTarget::RESAMPLE_INTERVAL ||
m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY)
{
// 「均值点数 X」/「迭代次数 N」(均值滤波、中心轴)与「滑块上限」(弱峰调整 / 均值滤波):
// 输入框都落在对应的字段/滑块行矩形上。
@@ -613,6 +615,31 @@ namespace jdi
return true;
}
// 「重采样」两行输入框:间隔(秒)与频率(Hz)**联动**,改任意一个都同时更新两者。
// ⚠️ 输入框内的数值单位:间隔 = 秒、频率 = Hz(显示态再按大小自适应单位)。
if (target == PlotEditTarget::RESAMPLE_INTERVAL ||
target == PlotEditTarget::RESAMPLE_FREQUENCY)
{
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
Lane::ResampleItem* existing = const_cast<Lane::ResampleItem*>(
lane.FindResampleItem(m_editMeanRef));
if (existing == nullptr)
{
return true;
}
// 频率 → 间隔(或反之),非法值(0 / 负数 / 非有限)不生效。
const double interval =
(target == PlotEditTarget::RESAMPLE_FREQUENCY) ? ((parsed > 0.0) ? (1.0 / parsed) : -1.0)
: parsed;
if (interval > 0.0 && std::isfinite(interval))
{
Lane::ResampleItem& item = lane.EnsureResampleItem(m_editMeanRef);
item.interval = std::max(kResampleIntervalMin, std::min(interval, kResampleIntervalMax));
item.initialized = true;
}
return true;
}
// 「弱峰调整」迭代次数滑块的**上限**:用户可直接输入一个正整数作为新的量程上限。
// 规则(用户明确要求):
// * 输入框只允许输入正整数(过滤策略为 BYTE = 纯数字,粘贴同样会被清洗);
@@ -1011,7 +1038,8 @@ namespace jdi
return nullptr;
}
if ((static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MEAN ||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK) &&
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK ||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::RESAMPLE) &&
layout.button.Width > 0.0f &&
UiPointInRect(layout.button, static_cast<float>(x), static_cast<float>(y)))
{
@@ -1080,7 +1108,8 @@ namespace jdi
const EdgePanelKind cursorKind = static_cast<EdgePanelKind>(panelKind);
const bool listCursor = (cursorKind == EdgePanelKind::MEAN ||
cursorKind == EdgePanelKind::CENTER ||
cursorKind == EdgePanelKind::PEAK);
cursorKind == EdgePanelKind::PEAK ||
cursorKind == EdgePanelKind::RESAMPLE);
const Lane& cursorLane =
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
if (listCursor)
@@ -1102,6 +1131,11 @@ namespace jdi
checked = cursorLane.FindEdgeCenterItem(
layout.rowRefs[rowIndex]) != nullptr;
}
else if (cursorKind == EdgePanelKind::RESAMPLE)
{
checked = cursorLane.FindResampleItem(
layout.rowRefs[rowIndex]) != nullptr;
}
else
{
checked = cursorLane.FindMeanFilterItem(
@@ -1109,6 +1143,23 @@ namespace jdi
}
return checked ? LoadCursorW(nullptr, IDC_HAND) : nullptr;
}
if (cursorKind == EdgePanelKind::RESAMPLE)
{
// 两行输入框是可点击控件(点击进入就地编辑)。
if (rowIndex < layout.rowIntervalField.size() &&
UiPointInRect(layout.rowIntervalField[rowIndex], static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (rowIndex < layout.rowFrequencyField.size() &&
UiPointInRect(layout.rowFrequencyField[rowIndex], static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
continue;
}
const bool hasParams =
rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f;
@@ -1772,6 +1823,9 @@ namespace jdi
case EdgePanelKind::PEAK:
scrolled = SetLaneWeakPeakScroll(m_markerScrollDragLane, target);
break;
case EdgePanelKind::RESAMPLE:
scrolled = SetLaneResampleScroll(m_markerScrollDragLane, target);
break;
default:
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
break;
@@ -1954,7 +2008,7 @@ namespace jdi
static_cast<EdgePanelKind>(hoverPanelKind);
if (overEdgePanel && m_tab != nullptr && panelLane >= 0 &&
(hoverKind == EdgePanelKind::MEAN || hoverKind == EdgePanelKind::PEAK ||
hoverKind == EdgePanelKind::CENTER))
hoverKind == EdgePanelKind::CENTER || hoverKind == EdgePanelKind::RESAMPLE))
{
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoverKind);
if (UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)))
@@ -2350,15 +2404,17 @@ namespace jdi
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
kind == EdgePanelKind::PEAK || kind == EdgePanelKind::COUNT) &&
kind == EdgePanelKind::PEAK || kind == EdgePanelKind::COUNT ||
kind == EdgePanelKind::RESAMPLE) &&
m_tab != nullptr)
{
const bool meanMode = (kind == EdgePanelKind::MEAN);
const bool centerMode = (kind == EdgePanelKind::CENTER);
const bool boostMode = (kind == EdgePanelKind::PEAK);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」自 2026-11-02 起共用同一套
//**可滚动列表**交互(信号色叠底条目栏 + 右侧复选框 + 展开后的两行参数滑块)。
const bool listMode = (meanMode || centerMode || boostMode);
const bool resampleMode = (kind == EdgePanelKind::RESAMPLE);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」自 2026-11-02 起共用同一套
//**可滚动列表**交互(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行)。
const bool listMode = (meanMode || centerMode || boostMode || resampleMode);
// 「边沿计数」= **表格**(表头 + 每信号一行),只有滚动条可拖。
if (kind == EdgePanelKind::COUNT)
{
@@ -2402,6 +2458,120 @@ namespace jdi
}
const bool insideContent =
UiPointInRect(layout.content, fx, fy);
// 「重采样」:四行内容 = 两行只读统计 + 两行**输入框**(间隔 / 频率)。
// 条目栏 = 复选框切换 + 已勾选时整行收起/展开(与其它列表式面板一致)。
if (resampleMode)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
if (!insideContent)
{
break;
}
const SignalRef& ref = layout.rowRefs[rowIndex];
const Lane::ResampleItem* item = meanLane.FindResampleItem(ref);
if (item == nullptr)
{
continue;
}
// 点击「重采样间隔」输入框 → 编辑秒数。
if (rowIndex < layout.rowIntervalField.size() &&
layout.rowIntervalField[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIntervalField[rowIndex], fx, fy))
{
m_editMeanRef = ref;
m_editMeanKind = kind;
const gd::RectF& field = layout.rowIntervalField[rowIndex];
BeginEdit(PlotEditTarget::RESAMPLE_INTERVAL, index,
FormatValue(item->interval), field);
m_edit.Begin(FormatValue(item->interval), true, EditFilter::NUMBER,
0u);
m_dragMode = PlotDragMode::NONE;
return true;
}
// 点击「重采样频率」输入框 → 编辑频率(Hz,与间隔联动)。
if (rowIndex < layout.rowFrequencyField.size() &&
layout.rowFrequencyField[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowFrequencyField[rowIndex], fx, fy))
{
m_editMeanRef = ref;
m_editMeanKind = kind;
const gd::RectF& field = layout.rowFrequencyField[rowIndex];
const double hz = (item->interval > 0.0) ? (1.0 / item->interval) : 0.0;
BeginEdit(PlotEditTarget::RESAMPLE_FREQUENCY, index, FormatValue(hz),
field);
m_edit.Begin(FormatValue(hz), true, EditFilter::NUMBER, 0u);
m_dragMode = PlotDragMode::NONE;
return true;
}
}
// 条目栏(信号色叠底):复选框切换;已勾选时整行切换参数行收起/展开。
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
if (!insideContent || rowIndex >= layout.rowHeaders.size() ||
layout.rowHeaders[rowIndex].Width <= 0.0f ||
!UiPointInRect(layout.rowHeaders[rowIndex], fx, fy))
{
continue;
}
const bool onCheckBox =
rowIndex < layout.rowChecks.size() &&
layout.rowChecks[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
const Lane::ResampleItem* existing = meanLane.FindResampleItem(ref);
if (onCheckBox)
{
ToggleLaneResampleSignal(index, ref);
m_meanRowHoverLane = -1;
m_meanRowHoverRef = SignalRef();
}
else if (existing != nullptr)
{
Lane::ResampleItem& editable =
meanLane.EnsureResampleItem(ref);
editable.collapsed = !editable.collapsed;
}
m_dragMode = PlotDragMode::NONE;
return true;
}
// 底部「构建为新信号」按钮。
if (layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
{
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(index)];
m_pendingResampleItems.clear();
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || !resolved.visible)
{
continue;
}
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
if (item == nullptr || !(item->interval > 0.0))
{
continue;
}
ResampleBuild build;
build.groupIndex = ref.groupIndex;
build.signalIndex = ref.signalIndex;
build.interval = item->interval;
m_pendingResampleItems.push_back(build);
}
if (!m_pendingResampleItems.empty())
{
const ResampleBuild& first = m_pendingResampleItems.front();
SetHint(L"正在构建重采样信号(间隔 " +
FormatIntervalText(first.interval) + L",共 " +
std::to_wstring(m_pendingResampleItems.size()) + L" 条)…");
m_pendingResampleLane = index;
}
m_dragMode = PlotDragMode::NONE;
return true;
}
m_dragMode = PlotDragMode::NONE;
return true; // 面板内的其它位置:消费点击,不穿透
}
// 「弱峰调整」:两行滑块 = 调整幅度(-400% … +400%)/ 迭代次数(1 … 自定上限)。
// 语义与该面板旧式排版完全一致,只是外观改成与均值滤波同款的可滚动列表。
if (boostMode)
@@ -3270,7 +3440,8 @@ namespace jdi
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整 / 边沿计数」面板:内容区超高时用滚轮翻页
//(上百个信号合并时必备)。
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER ||
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT)
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT ||
hoveredKind == EdgePanelKind::RESAMPLE)
{
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
if (layout.content.Width > 0.0f &&
@@ -3289,6 +3460,8 @@ namespace jdi
? wheelLane.weakPeakScroll
: (hoveredKind == EdgePanelKind::COUNT)
? wheelLane.edgeCountScroll
: (hoveredKind == EdgePanelKind::RESAMPLE)
? wheelLane.resampleScroll
: wheelLane.meanFilterScroll;
const float next = current - static_cast<float>(notches * 40.0 * m_scale);
const float target = std::max(0.0f, std::min(next, maxScroll));
@@ -3303,6 +3476,9 @@ namespace jdi
case EdgePanelKind::COUNT:
SetLaneEdgeCountScroll(panelLane, target);
break;
case EdgePanelKind::RESAMPLE:
SetLaneResampleScroll(panelLane, target);
break;
default:
SetLaneMeanFilterScroll(panelLane, target);
break;