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:
1 parent
30e9d65c30
commit
1e1c2432ae
19 files changed
+1979
-56
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user