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
@@ -150,6 +150,19 @@ namespace jdi
|
||||
mixHashFloat(offsetY);
|
||||
mixHashDouble(offsetTime);
|
||||
mixHashFloat(lineWidth);
|
||||
// 「公式映射」会逐点改变取值 ⇒ 必须进折线缓存键(否则改公式后仍贴旧顶点)。
|
||||
{
|
||||
std::uint64_t textHash = 1469598103934665603ull;
|
||||
if (resolved.style != nullptr)
|
||||
{
|
||||
for (const wchar_t character : resolved.style->formula)
|
||||
{
|
||||
textHash ^= static_cast<std::uint64_t>(static_cast<std::uint32_t>(character));
|
||||
textHash *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
mixHash(textHash);
|
||||
}
|
||||
|
||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||
{
|
||||
@@ -4071,6 +4084,16 @@ namespace jdi
|
||||
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
|
||||
// 「弱峰调整」的条目栏三角(展开 / 收起)直接改变面板像素 ⇒ 必须进键。
|
||||
mix((peakItem != nullptr && peakItem->collapsed) ? 1ull : 0ull);
|
||||
// 「重采样」:勾选状态、两个输入框的数值(间隔 / 频率)与收起状态都直接显示在面板上。
|
||||
const Lane::ResampleItem* resampleItem = lane.FindResampleItem(ref);
|
||||
mix(resampleItem != nullptr ? 1ull : 0ull);
|
||||
if (resampleItem != nullptr)
|
||||
{
|
||||
std::uint64_t intervalBits = 0;
|
||||
std::memcpy(&intervalBits, &resampleItem->interval, sizeof(intervalBits));
|
||||
mix(intervalBits);
|
||||
mix(resampleItem->collapsed ? 1ull : 0ull);
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,数字变化必须能让面板重绘**:
|
||||
@@ -4124,6 +4147,22 @@ namespace jdi
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowInfoMaxInterval)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowInfoMinInterval)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowIntervalField)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowFrequencyField)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
mixFloat(layout.scrollOffset);
|
||||
|
||||
// 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。
|
||||
@@ -4742,12 +4781,14 @@ namespace jdi
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」= 可滚动的分组列表(条目栏 + 展开后的参数行);
|
||||
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」= 可滚动的分组列表
|
||||
//(条目栏 + 展开后的参数行);
|
||||
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
|
||||
// 它们都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
const bool hasButton = (meanFilter || weakPeak || resamplePanel);
|
||||
{
|
||||
if (index >= count)
|
||||
{
|
||||
@@ -4944,8 +4985,10 @@ namespace jdi
|
||||
meanFilter ? std::wstring(L"均值滤波")
|
||||
: (weakPeak ? std::wstring(kWeakPeakTitle)
|
||||
: (freeMarker ? std::wstring(L"自由标记")
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix())));
|
||||
: (resamplePanel
|
||||
? std::wstring(kResampleTitle)
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix()))));
|
||||
UiText(g, title, m_fonts->uiSmall,
|
||||
gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y,
|
||||
panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight,
|
||||
@@ -5170,6 +5213,22 @@ namespace jdi
|
||||
{
|
||||
extendRow(layout.rowStats[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowInfoMaxInterval.size())
|
||||
{
|
||||
extendRow(layout.rowInfoMaxInterval[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowInfoMinInterval.size())
|
||||
{
|
||||
extendRow(layout.rowInfoMinInterval[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowIntervalField.size())
|
||||
{
|
||||
extendRow(layout.rowIntervalField[rowIndex]);
|
||||
}
|
||||
if (rowIndex < layout.rowFrequencyField.size())
|
||||
{
|
||||
extendRow(layout.rowFrequencyField[rowIndex]);
|
||||
}
|
||||
if (rowBottom <= layout.content.Y ||
|
||||
rowTop >= layout.content.Y + layout.content.Height)
|
||||
{
|
||||
@@ -5185,10 +5244,15 @@ namespace jdi
|
||||
weakPeak ? nullptr
|
||||
: (centerAxis ? tabLane.FindEdgeCenterItem(ref)
|
||||
: tabLane.FindMeanFilterItem(ref));
|
||||
const bool checked = weakPeak ? (peakItem != nullptr) : (item != nullptr);
|
||||
const Lane::ResampleItem* resampleItem =
|
||||
resamplePanel ? tabLane.FindResampleItem(ref) : nullptr;
|
||||
const bool checked = resamplePanel ? (resampleItem != nullptr)
|
||||
: (weakPeak ? (peakItem != nullptr)
|
||||
: (item != nullptr));
|
||||
const bool rowCollapsed =
|
||||
weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
: (item != nullptr && item->collapsed);
|
||||
resamplePanel ? (resampleItem != nullptr && resampleItem->collapsed)
|
||||
: (weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
: (item != nullptr && item->collapsed));
|
||||
|
||||
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
|
||||
// 已经完整滚出可视范围的行在上面就 `continue` 了,这里不再额外门控。
|
||||
@@ -5232,6 +5296,99 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
// 「重采样」:四行 = 最大间隔(只读)/ 最小间隔(只读)/
|
||||
// 重采样间隔(输入)/ 重采样频率(输入)。标签在左、数值在右。
|
||||
if (resamplePanel)
|
||||
{
|
||||
const float labelWidth =
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel);
|
||||
const auto paintInfoRow = [&](const gd::RectF& row, const wchar_t* label,
|
||||
const std::wstring& text)
|
||||
{
|
||||
if (row.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UiTextClipped(g, label, m_fonts->uiSmall,
|
||||
gd::RectF(row.X, row.Y, std::max(10.0f, labelWidth),
|
||||
row.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT, layout.content);
|
||||
UiTextClipped(g, text, m_fonts->monoSmall,
|
||||
gd::RectF(row.X + labelWidth + 8.0f * m_scale, row.Y,
|
||||
std::max(10.0f, row.Width - labelWidth -
|
||||
8.0f * m_scale),
|
||||
row.Height),
|
||||
m_theme->textStrong, TextAlign::RIGHT,
|
||||
layout.content);
|
||||
};
|
||||
const auto paintInputRow = [&](const gd::RectF& field, const wchar_t* label,
|
||||
const std::wstring& text, bool editing)
|
||||
{
|
||||
if (field.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 标签与数值同高:标签从内容区左缘画到字段左缘。
|
||||
UiTextClipped(
|
||||
g, label, m_fonts->uiSmall,
|
||||
gd::RectF(layout.content.X, field.Y,
|
||||
std::max(10.0f, field.X - 8.0f * m_scale - layout.content.X),
|
||||
field.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT, layout.content);
|
||||
if (editing)
|
||||
{
|
||||
return; // 正在编辑:内容由 `PaintEditField` 画
|
||||
}
|
||||
const gd::RectF box(field.X, field.Y + 2.0f * m_scale, field.Width,
|
||||
std::max(10.0f, field.Height - 4.0f * m_scale));
|
||||
UiFillRoundRect(g, box, 3.0f * m_scale, m_theme->pressed);
|
||||
UiDrawRoundRect(g, box, 3.0f * m_scale, m_theme->border, 1.0f);
|
||||
UiTextClipped(g, text, m_fonts->monoSmall,
|
||||
gd::RectF(box.X + 4.0f * m_scale, box.Y,
|
||||
std::max(6.0f, box.Width - 8.0f * m_scale),
|
||||
box.Height),
|
||||
m_theme->textStrong, TextAlign::LEFT, layout.content);
|
||||
};
|
||||
double maxInterval = 0.0;
|
||||
double minInterval = 0.0;
|
||||
const bool hasInterval =
|
||||
(resampleItem->maxInterval > 0.0) && (resampleItem->minInterval > 0.0);
|
||||
maxInterval = resampleItem->maxInterval;
|
||||
minInterval = resampleItem->minInterval;
|
||||
if (rowIndex < layout.rowInfoMaxInterval.size())
|
||||
{
|
||||
paintInfoRow(layout.rowInfoMaxInterval[rowIndex], kResampleMaxIntervalLabel,
|
||||
hasInterval ? FormatIntervalText(maxInterval) : L"—");
|
||||
}
|
||||
if (rowIndex < layout.rowInfoMinInterval.size())
|
||||
{
|
||||
paintInfoRow(layout.rowInfoMinInterval[rowIndex], kResampleMinIntervalLabel,
|
||||
hasInterval ? FormatIntervalText(minInterval) : L"—");
|
||||
}
|
||||
const double interval = (resampleItem->interval > 0.0)
|
||||
? resampleItem->interval
|
||||
: minInterval;
|
||||
const bool editingInterval =
|
||||
(m_editTarget == PlotEditTarget::RESAMPLE_INTERVAL) &&
|
||||
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
|
||||
const bool editingFrequency =
|
||||
(m_editTarget == PlotEditTarget::RESAMPLE_FREQUENCY) &&
|
||||
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
|
||||
if (rowIndex < layout.rowIntervalField.size())
|
||||
{
|
||||
paintInputRow(layout.rowIntervalField[rowIndex], kResampleIntervalLabel,
|
||||
FormatValue(interval), editingInterval);
|
||||
}
|
||||
if (rowIndex < layout.rowFrequencyField.size())
|
||||
{
|
||||
const double hertz =
|
||||
(interval > 0.0) ? (1.0 / interval) : 0.0;
|
||||
paintInputRow(layout.rowFrequencyField[rowIndex], kResampleFrequencyLabel,
|
||||
FormatFrequencyText(hertz), editingFrequency);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
|
||||
if (centerAxis && rowIndex < layout.rowStats.size() &&
|
||||
layout.rowStats[rowIndex].Width > 0.0f)
|
||||
|
||||
Reference in new issue
Block a user