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

+84 -4
View File
@@ -65,7 +65,8 @@ namespace jdi
MARKER_SCROLL, /**< 拖动「自由标记」面板内容的滚动条滑块。 */
MEAN_POINTS, /**< 拖动「均值滤波」某信号的「均值点数」滑块(0 … 该信号自定义上限)。 */
MEAN_ITERATIONS,/**< 拖动「均值滤波」某信号的「迭代次数」滑块(1 … 该信号自定义上限)。 */
MEAN_SCROLL /**< 拖动「均值滤波」面板内容的滚动条滑块。 */
MEAN_SCROLL,/**< 拖动「均值滤波」面板内容的滚动条滑块。 */
RESAMPLE_SCROLL /**< 拖动「重采样」面板内容的滚动条滑块。 */
};
/**
@@ -103,6 +104,8 @@ namespace jdi
MEAN_POINTS_MAX,
/** @brief 编辑「均值滤波」某信号「迭代次数」滑块的**上限**(正整数)。 */
MEAN_ITERATIONS_MAX,
RESAMPLE_INTERVAL, /**< 编辑「重采样」某信号的**重采样间隔**(秒)。 */
RESAMPLE_FREQUENCY, /**< 编辑「重采样」某信号的**重采样频率**(Hz,与间隔联动)。 */
FREE_MARKER_TIME, /**< 编辑「自由标记」某固定点的**自身时间**(秒)。 */
FREE_MARKER_DELTA /**< 编辑「自由标记」某固定点相对前一固定点的**时间间隔**(秒)。 */
};
@@ -123,7 +126,8 @@ namespace jdi
PLOT_CMD_LANE_SPLIT = 9, /**< 拆分当前网格:把网格内信号按个拆成独立网格并顺次下移编号。 */
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
PLOT_CMD_LANE_WEAK_PEAK = 11, /**< 切换本网格的「实验性功能 → 弱峰调整」开关。 */
PLOT_CMD_LANE_FREE_MARKER = 12 /**< 切换本网格的「实验性功能 → 自由标记」开关。 */
PLOT_CMD_LANE_FREE_MARKER = 12, /**< 切换本网格的「实验性功能 → 自由标记」开关。 */
PLOT_CMD_LANE_RESAMPLE = 13 /**< 切换本网格的「实验性功能 → 重采样」开关。 */
};
/**
@@ -135,7 +139,8 @@ namespace jdi
CENTER = 1, /**< 边沿计数(中心轴)。 */
MEAN = 2, /**< 均值滤波。 */
PEAK = 3, /**< 弱峰调整。 */
MARK = 4 /**< 自由标记。 */
MARK = 4, /**< 自由标记。 */
RESAMPLE = 5 /**< 重采样(把非固定频率采样规整为固定频率)。 */
};
/**
@@ -1022,6 +1027,66 @@ namespace jdi
*/
bool ToggleLaneFreeMarker(int laneIndex);
/**
* @brief 「重采样 → 构建为新信号」的单个请求(一个被勾选的信号 + 自己的间隔)。
*/
struct ResampleBuild
{
int groupIndex = -1; /**< 信号所属分组下标。 */
int signalIndex = -1; /**< 组内信号下标。 */
double interval = 0.0; /**< 重采样间隔(秒,> 0)。 */
};
/**
* @brief 取出「重采样 → 构建为新信号」请求(由面板底部按钮触发)。
* @param [OUT] laneIndex 触发该请求的网格下标。
* @param [OUT] items 需要构建的信号及其间隔(按信号在网格内的顺序)。
* @return 存在待处理请求返回 true(取出后清空)。
*/
bool TakePendingResampleBuild(int& laneIndex, std::vector<ResampleBuild>& items);
/**
* @brief 开启 / 关闭某网格的「重采样」功能。
* @param [IN] laneIndex 网格下标。
* @param [IN] enabled 是否开启。
* @return 状态发生变化返回 true。
*/
bool SetLaneResample(int laneIndex, bool enabled);
/** @brief 查询某网格的「重采样」开关。 */
bool LaneResample(int laneIndex) const;
/**
* @brief 切换某网格的「重采样」开关。
* @param [IN] laneIndex 网格下标。
* @return 切换后的状态。
*/
bool ToggleLaneResample(int laneIndex);
/** @brief 切换某信号是否参与重采样(面板条目栏右侧复选框)。 */
bool ToggleLaneResampleSignal(int laneIndex, const SignalRef& ref);
/** @brief 查询某信号是否参与重采样。 */
bool LaneResampleSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 设置「重采样」面板内容的滚动偏移(自动夹到合法范围)。
* @param [IN] laneIndex 网格下标。 @param [IN] scroll 目标偏移(像素)。
*/
bool SetLaneResampleScroll(int laneIndex, float scroll);
/**
* @brief 查询某信号的最大 / 最小数据间隔(相邻有效采样点的时间差极值)。
*
* 只统计**时间轴与采样值都有效**的相邻采样对;不足两个有效采样时返回 false。
*
* @param [IN] ref 目标信号。
* @param [OUT] maxInterval 最大间隔(秒)。
* @param [OUT] minInterval 最小间隔(秒)。
* @return 至少存在一个有效间隔返回 true。
*/
bool SignalDataInterval(const SignalRef& ref, double& maxInterval, double& minInterval) const;
/**
* @brief 设置某网格是否处于「自由标记点选取」(浮动面板底部的切换按钮)。
*
@@ -1902,6 +1967,17 @@ namespace jdi
*/
std::vector<gd::RectF> rowLevelResets;
std::vector<gd::RectF> rowIterResets;
/**
* @brief 「重采样」面板每个被勾选信号的四行内容(与 `rowRefs` 同序)。
*
* `rowInfoMaxInterval` / `rowInfoMinInterval` = 只读统计(最大 / 最小数据间隔);
* `rowIntervalField` / `rowFrequencyField` = 两行**联动输入框**(间隔秒 / 频率 Hz)。
* 未勾选或非 RESAMPLE 面板时对应元素为空矩形(数组也可能为空)。
*/
std::vector<gd::RectF> rowInfoMaxInterval;
std::vector<gd::RectF> rowInfoMinInterval;
std::vector<gd::RectF> rowIntervalField;
std::vector<gd::RectF> rowFrequencyField;
/**
* @brief 「均值滤波」面板每个信号的**信号色叠底标题栏**矩形(与 `rowRefs` 同序)。
*
@@ -2896,6 +2972,8 @@ namespace jdi
int m_pendingWeakPeakLane = -1; /**< 待构建的弱峰调整信号所属网格下标(-1 表示无请求)。 */
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰调整信号及其参数。 */
int m_pendingResampleLane = -1; /**< 待构建的重采样信号所属网格下标(-1 表示无请求)。 */
std::vector<ResampleBuild> m_pendingResampleItems; /**< 待构建的重采样信号及其间隔。 */
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
@@ -3585,6 +3663,7 @@ namespace jdi
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序)。 */
float offsetY = 0.0f; /**< Y 轴偏移。 */
double offsetTime = 0.0; /**< 时间轴偏移(秒)。 */
std::wstring formula; /**< 「公式映射」表达式(空 = 不映射)。 */
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
bool operator==(const SceneStyleEntry& other) const
@@ -3594,7 +3673,7 @@ namespace jdi
gain == other.gain && absolute == other.absolute &&
mirrorVertical == other.mirrorVertical &&
mirrorHorizontal == other.mirrorHorizontal && offsetY == other.offsetY &&
offsetTime == other.offsetTime;
offsetTime == other.offsetTime && formula == other.formula;
}
};
@@ -3729,6 +3808,7 @@ namespace jdi
bool mirrorHorizontal = false; /**< 是否水平镜像。 */
float offsetY = 0.0f; /**< Y 偏移。 */
double offsetTime = 0.0; /**< 时间偏移。 */
std::wstring formula; /**< 「公式映射」表达式(空 = 不映射)。 */
int originX = 0; /**< 位图左上角(屏幕 x)。 */
int originY = 0; /**< 位图左上角(屏幕 y)。 */
std::unique_ptr<gd::Bitmap> bitmap; /**< 32bpp ARGB 光晕图层(直通 alpha)。 */
+51 -1
View File
@@ -587,6 +587,26 @@ namespace jdi
inline const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
inline const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
// ---- 「重采样」面板 ----
/** 「重采样」面板标题。 */
inline const wchar_t* const kResampleTitle = L"重采样";
/** 只读统计行标签:最大数据间隔。 */
inline const wchar_t* const kResampleMaxIntervalLabel = L"最大数据间隔";
/** 只读统计行标签:最小数据间隔。 */
inline const wchar_t* const kResampleMinIntervalLabel = L"最小数据间隔";
/** 输入行标签:重采样间隔(秒)。 */
inline const wchar_t* const kResampleIntervalLabel = L"重采样间隔";
/** 输入行标签:重采样频率(Hz)。 */
inline const wchar_t* const kResampleFrequencyLabel = L"重采样频率";
/**
* @brief 重采样间隔的合法区间(秒)。
*
* 下限取 1ns(再小已经没有实际意义的采样率,且会让新信号的采样数爆炸);
* 上限取 1e6 秒。
*/
inline const double kResampleIntervalMin = 1.0e-9;
inline const double kResampleIntervalMax = 1.0e6;
// ---- 「均值滤波」面板的两行参数滑块(用户 2026-10-30 要求)----
/**
* 标签一律**不带 `X` / `N`**:滑块的量程本身已经说明是数值,符号只会让标签变长。
@@ -1034,7 +1054,7 @@ namespace jdi
*/
inline const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK};
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE};
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
@@ -1274,6 +1294,36 @@ namespace jdi
return FormatValue(seconds) + L"s";
}
/**
* @brief 时间间隔的显示文本(**不带** "Δt:" 前缀)。
*
* 「重采样」面板的「最大 / 最小数据间隔」两行用它:单位随间隔长短自适应
*(`FormatDurationCompound`:1h 1min 2s / 5s 60ms / 120us …)。
*
* @param [IN] seconds 时间间隔(秒)。
* @return 文本。
*/
inline std::wstring FormatIntervalText(double seconds)
{
if (!std::isfinite(seconds) || seconds <= 0.0)
{
return L"—";
}
return FormatDurationCompound(seconds);
}
/**
* @brief 频率的显示文本(单位在 Hz / kHz / MHz / GHz 之间自适应,保留小数)。
*
* @param [IN] hertz 频率(Hz)。
* @return 形如 "1.2 kHz" 的文本;非法值返回 "—"。
*/
inline std::wstring FormatFrequencyText(double hertz)
{
// 唯一实现在 `Utf8Util::FormatFrequency`(宿主构建派生信号名时也用它)。
return FormatFrequency(hertz);
}
/**
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
* @param [IN] argb 原始 0xAARRGGBB 颜色。
@@ -208,6 +208,8 @@ namespace jdi
// 偏移(Y 平移 + 时间平移)同样会改变波形。
mixFloat(resolved.style->offsetY);
mixFloat(static_cast<float>(resolved.style->offsetTime));
// 「公式映射」逐点改变取值 ⇒ 必须进指纹(否则改公式后波形不变)。
mixText(resolved.style->formula);
}
// 图例文案参与绘制,重命名必须让缓存失效。
mixText(resolved.data->name);
@@ -263,6 +265,7 @@ namespace jdi
entry.mirrorHorizontal = resolved.style->mirrorHorizontal;
entry.offsetY = resolved.style->offsetY;
entry.offsetTime = resolved.style->offsetTime;
entry.formula = resolved.style->formula;
}
lane.push_back(entry);
}
+187 -2
View File
@@ -1811,8 +1811,187 @@ namespace jdi
return true;
}
bool PlotView::SetLaneEdgeCenterScroll(int laneIndex, float scroll)
// ============================ 「重采样」 ============================
bool PlotView::SignalDataInterval(const SignalRef& ref, double& maxInterval,
double& minInterval) const
{
maxInterval = 0.0;
minInterval = 0.0;
if (m_dataSet == nullptr)
{
return false;
}
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.axis == nullptr)
{
return false;
}
const TimeAxis& axis = *resolved.axis;
const SignalData& signal = *resolved.data;
const std::size_t count = std::min(axis.time.size(), signal.values.size());
bool havePrevious = false;
double previousTime = 0.0;
bool any = false;
for (std::size_t index = 0; index < count; ++index)
{
const double time = axis.time[index];
if (!std::isfinite(time) || !std::isfinite(signal.values[index]))
{
continue;
}
if (havePrevious)
{
const double gap = time - previousTime;
if (gap > 0.0)
{
if (!any)
{
maxInterval = gap;
minInterval = gap;
any = true;
}
else
{
maxInterval = std::max(maxInterval, gap);
minInterval = std::min(minInterval, gap);
}
}
}
previousTime = time;
havePrevious = true;
}
return any;
}
bool PlotView::SetLaneResample(int laneIndex, bool enabled)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
if (lane.resample == enabled)
{
return false;
}
lane.resample = enabled;
if (enabled)
{
// 默认只勾选网格内**第一个可见信号**(与其它实验性功能一致)。
if (lane.resampleItems.empty())
{
for (const SignalRef& ref : lane.signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data != nullptr && resolved.visible)
{
Lane::ResampleItem& item = lane.EnsureResampleItem(ref);
double maxInterval = 0.0;
double minInterval = 0.0;
if (SignalDataInterval(ref, maxInterval, minInterval))
{
item.maxInterval = maxInterval;
item.minInterval = minInterval;
item.interval = minInterval;
}
item.initialized = true;
break;
}
}
}
lane.resamplePanel.open = true;
}
else
{
lane.resamplePanel.open = false;
}
EnsureLaneLayersPrefetched();
return true;
}
bool PlotView::LaneResample(int laneIndex) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].resample;
}
bool PlotView::ToggleLaneResample(int laneIndex)
{
const bool enabled = !LaneResample(laneIndex);
SetLaneResample(laneIndex, enabled);
return enabled;
}
bool PlotView::ToggleLaneResampleSignal(int laneIndex, const SignalRef& ref)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
if (lane.ResampleEnabled(ref))
{
lane.RemoveResampleItem(ref);
return false;
}
// 勾选时一次性算好最大 / 最小数据间隔(面板绘制只读缓存值)。
Lane::ResampleItem& item = lane.EnsureResampleItem(ref);
double maxInterval = 0.0;
double minInterval = 0.0;
if (SignalDataInterval(ref, maxInterval, minInterval))
{
item.maxInterval = maxInterval;
item.minInterval = minInterval;
item.interval = minInterval;
}
item.initialized = true;
return true;
}
bool PlotView::LaneResampleSignalEnabled(int laneIndex, const SignalRef& ref) const
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].ResampleEnabled(ref);
}
bool PlotView::SetLaneResampleScroll(int laneIndex, float scroll)
{
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
}
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const float clamped = std::max(0.0f, scroll);
if (std::fabs(lane.resampleScroll - clamped) < 0.5f)
{
return false;
}
lane.resampleScroll = clamped;
return true;
}
bool PlotView::TakePendingResampleBuild(int& laneIndex, std::vector<ResampleBuild>& items)
{
if (m_pendingResampleLane < 0)
{
return false;
}
laneIndex = m_pendingResampleLane;
items = m_pendingResampleItems;
m_pendingResampleLane = -1;
m_pendingResampleItems.clear();
return true;
}
bool PlotView::SetLaneEdgeCenterScroll(int laneIndex, float scroll) {
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
{
return false;
@@ -1916,6 +2095,9 @@ namespace jdi
case EdgePanelKind::MARK:
state = &lane.freeMarkerPanel;
break;
case EdgePanelKind::RESAMPLE:
state = &lane.resamplePanel;
break;
default:
return false;
}
@@ -1946,6 +2128,8 @@ namespace jdi
return lane.weakPeakPanel.collapsed;
case EdgePanelKind::MARK:
return lane.freeMarkerPanel.collapsed;
case EdgePanelKind::RESAMPLE:
return lane.resamplePanel.collapsed;
default:
return false;
}
@@ -3393,7 +3577,8 @@ namespace jdi
const bool mirrored = (resolvedStyle != nullptr) && resolvedStyle->mirrorHorizontal;
const bool transform = (resolvedStyle != nullptr) &&
(resolvedStyle->gain != 1.0f || resolvedStyle->absolute ||
resolvedStyle->mirrorVertical || resolvedStyle->offsetY != 0.0f);
resolvedStyle->mirrorVertical || resolvedStyle->offsetY != 0.0f ||
resolvedStyle->HasFormula());
// ⚠️ **整段自适应直接取信号已缓存的 min/max**(O(1)):
// 当视界覆盖整条信号、且该信号没有任何数值变换时,逐点扫描的结果与
+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;
+165 -8
View File
@@ -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)
+128 -28
View File
@@ -28,12 +28,13 @@ namespace jdi
const bool centerAxis = (kind == EdgePanelKind::CENTER);
const bool meanFilter = (kind == EdgePanelKind::MEAN);
const bool weakPeak = (kind == EdgePanelKind::PEAK);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」= 可滚动的分组列表
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」= 可滚动的分组列表
//(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行);
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数)。
// ⚠️ 旧式「逐条目」排版(无滚动的名称+参数行)已彻底删除:上百个信号时它会算出
// 上万像素高的面板,超出绘图区后**面板像素缓存一次都存不进去** ⇒ 每帧整块重画。
const bool listStyle = (centerAxis || meanFilter || weakPeak);
const bool listStyle = (centerAxis || meanFilter || weakPeak || resamplePanel);
const bool countTable = (kind == EdgePanelKind::COUNT);
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
@@ -41,7 +42,10 @@ namespace jdi
const std::wstring title =
meanFilter ? std::wstring(L"均值滤波")
: (weakPeak ? std::wstring(kWeakPeakTitle)
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") + EdgeRangeSuffix()));
: (resamplePanel
? std::wstring(kResampleTitle)
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
EdgeRangeSuffix())));
float width = kEdgePanelPad * 2.0f * m_scale + kLaneBadgeWidth * m_scale +
8.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, title) +
kEdgePanelTitleHeight * m_scale;
@@ -162,6 +166,22 @@ namespace jdi
kWeakPeakResetGap * m_scale + kWeakPeakResetButtonSize * m_scale;
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + sliderRowWidth +
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
if (resamplePanel)
{
// 「重采样」四行 = 「标签列 + 数值列」(两行统计 + 两行输入框),
// 标签取四者中最宽的一个,数值列按间隔 / 频率的典型文本宽度取。
const float resampleLabelColumn =
std::max(std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMinIntervalLabel)),
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleFrequencyLabel)));
const float resampleValueColumn =
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"1.23456 GHz"),
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s"));
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + resampleLabelColumn +
8.0f * m_scale + resampleValueColumn +
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
}
}
if (weakPeak)
@@ -174,9 +194,9 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
}
if (meanFilter || weakPeak)
if (meanFilter || weakPeak || resamplePanel)
{
// 底部按钮行 = 「构建为新信号」(仅均值滤波 / 弱峰调整有),按按钮文字实测宽度确定下限。
// 底部按钮行 = 「构建为新信号」(均值滤波 / 弱峰调整 / 重采样有),按按钮文字实测宽度确定下限。
const float buttonWidth =
UiMeasureTextWidth(m_fonts->uiSmall, kBuildSignalLabel) + 28.0f * m_scale;
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
@@ -276,21 +296,26 @@ 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);
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」共用同一套**可滚动分组列表**排布
//(信号色叠底条目栏 + 展开后的参数行);「边沿计数」自 2026-11-02 起走**表格**分支。
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);
const bool enabled = meanFilter ? lane.meanFilter
: (weakPeak ? lane.weakPeak
: (freeMarker ? lane.freeMarker
: (centerAxis ? lane.edgeCenter
: lane.edgeCount)));
: (resamplePanel ? lane.resample
: (centerAxis ? lane.edgeCenter
: lane.edgeCount))));
const bool open = meanFilter ? lane.meanFilterPanel.open
: (weakPeak ? lane.weakPeakPanel.open
: (freeMarker ? lane.freeMarkerPanel.open
: (centerAxis ? lane.edgeCenterPanel.open
: lane.edgePanel.open)));
: (resamplePanel
? lane.resamplePanel.open
: (centerAxis
? lane.edgeCenterPanel.open
: lane.edgePanel.open))));
if (!enabled || !open)
{
return layout;
@@ -299,9 +324,11 @@ namespace jdi
const bool collapsed = meanFilter ? lane.meanFilterPanel.collapsed
: (weakPeak ? lane.weakPeakPanel.collapsed
: (freeMarker ? lane.freeMarkerPanel.collapsed
: (centerAxis
? lane.edgeCenterPanel.collapsed
: lane.edgePanel.collapsed)));
: (resamplePanel
? lane.resamplePanel.collapsed
: (centerAxis
? lane.edgeCenterPanel.collapsed
: lane.edgePanel.collapsed))));
layout.collapsed = collapsed;
int rowCount = 0;
@@ -368,7 +395,12 @@ namespace jdi
}
content += groupRow;
bool expanded = false;
if (weakPeak)
if (resamplePanel)
{
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
expanded = (item != nullptr && !item->collapsed);
}
else if (weakPeak)
{
const Lane::WeakPeakItem* item = lane.FindWeakPeakItem(ref);
expanded = (item != nullptr && !item->collapsed);
@@ -385,10 +417,18 @@ namespace jdi
}
if (expanded)
{
content += sliderRow * 2.0f;
if (centerAxis)
if (resamplePanel)
{
content += groupRow; // 统计行(上升沿 / 下降沿)
// 四行:最大间隔 / 最小间隔 / 重采样间隔 / 重采样频率。
content += sliderRow * 4.0f;
}
else
{
content += sliderRow * 2.0f;
if (centerAxis)
{
content += groupRow; // 统计行(上升沿 / 下降沿)
}
}
}
}
@@ -489,13 +529,19 @@ namespace jdi
const double storedX = meanFilter ? lane.meanFilterPanel.x
: (weakPeak ? lane.weakPeakPanel.x
: (freeMarker ? lane.freeMarkerPanel.x
: (centerAxis ? lane.edgeCenterPanel.x
: lane.edgePanel.x)));
: (resamplePanel
? lane.resamplePanel.x
: (centerAxis
? lane.edgeCenterPanel.x
: lane.edgePanel.x))));
const double storedY = meanFilter ? lane.meanFilterPanel.y
: (weakPeak ? lane.weakPeakPanel.y
: (freeMarker ? lane.freeMarkerPanel.y
: (centerAxis ? lane.edgeCenterPanel.y
: lane.edgePanel.y)));
: (resamplePanel
? lane.resamplePanel.y
: (centerAxis
? lane.edgeCenterPanel.y
: lane.edgePanel.y))));
int openPanelOrdinal = 0;
{
// ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」:
@@ -519,12 +565,14 @@ namespace jdi
return lane.weakPeak && lane.weakPeakPanel.open;
case EdgePanelKind::MARK:
return lane.freeMarker && lane.freeMarkerPanel.open;
case EdgePanelKind::RESAMPLE:
return lane.resample && lane.resamplePanel.open;
}
return false;
};
static const EdgePanelKind kOrder[5] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK};
static const EdgePanelKind kOrder[6] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE};
int openSeen = 0;
bool found = false;
for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane)
@@ -701,7 +749,9 @@ namespace jdi
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
const float storedScroll = meanFilter ? lane.meanFilterScroll
: (weakPeak ? lane.weakPeakScroll : lane.edgeCenterScroll);
: (weakPeak ? lane.weakPeakScroll
: (resamplePanel ? lane.resampleScroll
: lane.edgeCenterScroll));
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
const float innerWidth =
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
@@ -734,6 +784,14 @@ namespace jdi
const float resetGap = kWeakPeakResetGap * m_scale;
const float trackHeight = kWeakPeakTrackHeight * m_scale;
const float closeSize = kEdgePanelMeanCheckSize * m_scale;
// 「重采样」四行的标签列(「最大数据间隔」比均值/弱峰的标签更宽):
// 与 `EdgePanelWidth` 用同一组实测宽度,保证字段不会压到标签上。
const float resampleLabelColumn =
std::max(std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMinIntervalLabel)),
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleIntervalLabel),
UiMeasureTextWidth(m_fonts->uiSmall, kResampleFrequencyLabel)));
const float rowLabelColumn = resamplePanel ? resampleLabelColumn : labelColumn;
const float contentLeft = layout.content.X;
const float trackLeft = contentLeft + labelColumn + 6.0f * m_scale;
const float resetLeft = contentLeft + innerWidth - resetSize;
@@ -751,7 +809,13 @@ namespace jdi
}
bool checked = false;
bool itemCollapsed = false;
if (weakPeak)
if (resamplePanel)
{
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
checked = (item != nullptr);
itemCollapsed = (item != nullptr && item->collapsed);
}
else if (weakPeak)
{
const Lane::WeakPeakItem* item = lane.FindWeakPeakItem(ref);
checked = (item != nullptr);
@@ -797,6 +861,42 @@ namespace jdi
layout.rowIterTracks.push_back(gd::RectF());
layout.rowIterValues.push_back(gd::RectF());
layout.rowIterResets.push_back(gd::RectF());
if (resamplePanel)
{
layout.rowInfoMaxInterval.push_back(gd::RectF());
layout.rowInfoMinInterval.push_back(gd::RectF());
layout.rowIntervalField.push_back(gd::RectF());
layout.rowFrequencyField.push_back(gd::RectF());
}
continue;
}
if (resamplePanel)
{
// 「重采样」四行:① 最大间隔(只读)② 最小间隔(只读)
// ③ 重采样间隔(输入)④ 重采样频率(输入,与③联动)。
// 每行 = 左侧标签 + 右侧数值/输入框区域。
layout.rowLevelTracks.push_back(gd::RectF());
layout.rowLevelValues.push_back(gd::RectF());
layout.rowLevelResets.push_back(gd::RectF());
layout.rowIterTracks.push_back(gd::RectF());
layout.rowIterValues.push_back(gd::RectF());
layout.rowIterResets.push_back(gd::RectF());
layout.rowInfoMaxInterval.push_back(
gd::RectF(contentLeft, rowY, innerWidth, sliderRow));
rowY += sliderRow;
layout.rowInfoMinInterval.push_back(
gd::RectF(contentLeft, rowY, innerWidth, sliderRow));
rowY += sliderRow;
// 输入行的**数值矩形**靠右对齐,宽度取剩余可用宽度(绘制框与编辑框同源)。
const float fieldLeft = contentLeft + rowLabelColumn + 8.0f * m_scale;
const float fieldWidth =
std::max(24.0f * m_scale, innerWidth - rowLabelColumn - 8.0f * m_scale);
layout.rowIntervalField.push_back(
gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow));
rowY += sliderRow;
layout.rowFrequencyField.push_back(
gd::RectF(fieldLeft, rowY, fieldWidth, sliderRow));
rowY += sliderRow;
continue;
}
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
+13 -1
View File
@@ -142,7 +142,8 @@ namespace jdi
cache.mirrorVertical == resolved.style->mirrorVertical &&
cache.mirrorHorizontal == resolved.style->mirrorHorizontal &&
cache.offsetY == resolved.style->offsetY &&
cache.offsetTime == resolved.style->offsetTime;
cache.offsetTime == resolved.style->offsetTime &&
cache.formula == resolved.style->formula;
if (!sameKey)
{
@@ -185,6 +186,7 @@ namespace jdi
cache.mirrorHorizontal = resolved.style->mirrorHorizontal;
cache.offsetY = resolved.style->offsetY;
cache.offsetTime = resolved.style->offsetTime;
cache.formula = resolved.style->formula;
cache.originX = targetX;
cache.originY = targetY;
cache.valid = true;
@@ -1185,6 +1187,16 @@ namespace jdi
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
mixFloat(resolved.style->offsetY);
mixFloat(static_cast<float>(resolved.style->offsetTime));
// 「公式映射」逐点改变图层里的波形 ⇒ 必须让图层失效。
{
std::uint64_t textHash = 1469598103934665603ull;
for (const wchar_t character : resolved.style->formula)
{
textHash ^= static_cast<std::uint64_t>(static_cast<std::uint32_t>(character));
textHash *= 1099511628211ull;
}
mix(textHash);
}
}
return hash;
}