弱峰增强改回逐信号滑块;迭代次数上限可就地编辑

一、参数输入方式(回到滑块形态,但每个信号各有一套)
- 面板布局:每个被勾选的信号 = 名称行(+复选框)+ 两行滑块:
  「增强等级 %」(0~200%)与「迭代次数 N」(1 ~ 该信号自己的上限)。
  EdgePanelLayout 新增逐信号矩形 rowLevelTracks/rowLevelValues/rowIterTracks/rowIterValues;
  PEAK 的 rowPoints/rowIterations 恒为空(MEAN/CENTER 仍用文本输入框)。
- 恢复 kWeakPeakSliderRowHeight/kWeakPeakTrackHeight/kWeakPeakTrackMinWidth 与
  WeakPeakSliderHit();恢复 PlotDragMode::PEAK_LEVEL/PEAK_ITERATIONS,
  并新增 m_peakDragRef(拖动时必须知道是哪个信号)。
- 列宽算式集中在 WeakPeakLabelColumnWidth()/WeakPeakValueColumnWidth(),
  与 EdgePanelWidth 共用,避免布局与面板宽度计算漂移。

二、迭代次数滑块上限可就地编辑(用户要求的行为)
- 点击「迭代次数 N」滑块右侧的数值文本 → 整条滑块行(轨道 + 数值)变成一个输入框;
  左侧标签保留可见。
- 输入框以空内容开始;空内容时以灰色提示当前上限值(m_peakEditCurrentMax,EndEdit 清零)。
- 过滤器为 EditFilter::BYTE(纯数字):负号、小数点、字母无法键入,粘贴同样被清洗,
  从源头满足"只允许输入正整数"。
- 提交(回车 / 点击别处):仅当解析成功且 1 <= 值 <= 1000 时才作为新上限;
  空内容、0、超范围一律保持原上限不变;生效后若当前迭代次数超过新上限则自动替换为新上限。
- 输入框在 paintOverlays() 中与 Y 轴上下限输入框一起绘制(覆盖在面板之上)。
- 新增 PlotEditTarget::PEAK_ITERATIONS_MAX;删除 PEAK_LEVEL_PERCENT/PEAK_ITERATIONS
  (等级只能拖滑块,迭代次数只能拖滑块)。

三、数据与缓存
- Lane::WeakPeakItem 新增 maxIterations(默认 10)。
- maxIterations 参与 PanelSlotKey(决定圆钮位置,面板必须重画),
  但不参与 SceneContentHash(不影响网格内曲线,不应触发波形重画)。

四、测试
- uitest [X] 改为:滑块存在性/层序/列宽、等级滑块拖动(中间=100%、右端=200%)、
  迭代滑块拖动(左端=1、右端=当前上限)、点击数值进入上限编辑(空内容 + 提示值 + 输入框
  横跨整行、空内容回车不改、'-'/字母被拒、输入 40 生效且右端变 40、下调到 4 时当前值被夹到 4、
  输入 0 被拒、点击别处提交),并保存编辑态截图供目视核对。
- make all 零警告;uitest 409/409 通过;panperf PASS;layercheck 保持像素一致。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-12 13:35:50 +08:00
1 parent 1ddbc37b78
commit a30a6fcf4d
4 files changed
+598 -112

No files matched your search

+33 -5
View File
@@ -43,6 +43,8 @@ namespace jdi
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
PEAK_LEVEL, /**< 拖动「弱峰增强」某信号的等级滑块(0–200%)。 */
PEAK_ITERATIONS, /**< 拖动「弱峰增强」某信号的迭代次数滑块(1 … 该信号自定义上限)。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
};
@@ -71,8 +73,7 @@ namespace jdi
EDGE_CENTER_ITERATIONS, /**< 编辑「中心轴边沿计数」的迭代次数 N。 */
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
MEAN_FILTER_ITERATIONS, /**< 编辑「均值滤波」的迭代次数 N。 */
PEAK_LEVEL_PERCENT, /**< 编辑「弱峰增强」某信号的增强等级(0–200%)。 */
PEAK_ITERATIONS /**< 编辑「弱峰增强」某信号的迭代次数 N。 */
PEAK_ITERATIONS_MAX /**< 编辑「弱峰增强」某信号「迭代次数 N」滑块的**上限**(正整数)。 */
};
/**
@@ -1133,15 +1134,26 @@ namespace jdi
* @brief 「逐条目」面板的矩形(MEAN / CENTER / PEAK 面板共用同一套排布)。
*
* 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序):
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为第二排的两个输入框
*(PEEK 信号分别为「增强等级 %」与「迭代次数 N」);
* `rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowIterations` 为**输入框**
*(MEAN / CENTER 使用);`rowStats` 为第三排的统计结果(中心轴边沿计数专用)。
* 未勾选的条目对应的矩形均为空。
*/
std::vector<gd::RectF> rowChecks;
std::vector<gd::RectF> rowPoints;
std::vector<gd::RectF> rowIterations;
std::vector<gd::RectF> rowStats;
/**
* @brief 「弱峰增强」每个被勾选信号自己的两行**滑块**(与 `rowRefs` 同序)。
*
* `rowLevelTracks/rowLevelValues` = 增强等级滑块与它右侧的数值文本;
* `rowIterTracks/rowIterValues` = 迭代次数滑块与它右侧的数值文本
*(**点值数值文本可以把滑块上限改为别的正整数**)。
* 未勾选或非 PEAK 面板时对应元素为空矩形(数组也可能为空)。
*/
std::vector<gd::RectF> rowLevelTracks;
std::vector<gd::RectF> rowLevelValues;
std::vector<gd::RectF> rowIterTracks;
std::vector<gd::RectF> rowIterValues;
std::vector<SignalRef> rowRefs;
};
@@ -1153,6 +1165,14 @@ namespace jdi
*/
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
/**
* @brief 在面板布局里找出某个信号对应的「逐条目」行下标。
* @param [IN] layout 面板布局。
* @param [IN] ref 目标信号。
* @return 行下标(与 `rowRefs` 同序);未找到返回 -1。
*/
int PeakRowIndexOf(const EdgePanelLayout& layout, const SignalRef& ref) const;
/**
* @brief 计算单个面板的缓存键(全局键之外,面板自身的内容)。
* @param [IN] laneIndex 面板所属网格。
@@ -1658,6 +1678,14 @@ namespace jdi
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰增强信号及其参数。 */
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
/**
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
*
* 输入框以空内容开始,空内容时显示这个值作为提示;进入编辑时记录,提交后清零。
*/
int m_peakEditCurrentMax = 0;
/**
* @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。
*
+496 -106
View File
@@ -68,8 +68,14 @@ namespace jdi
const float kEdgePanelMeanEditHeight = 24.0f; /**< 逐条目面板中每条信号的参数行高度。 */
const float kEdgePanelMeanRowGap = 2.0f; /**< 名称行与参数行之间的间距。 */
const float kEdgePanelMeanCheckSize = 15.0f; /**< 逐条目面板中条目复选框尺寸。 */
const float kWeakPeakSliderRowHeight = 26.0f; /**< 弱峰增强面板中每个滑块行的高度。 */
const float kWeakPeakTrackHeight = 6.0f; /**< 弱峰增强滑块的轨道高度。 */
const float kWeakPeakTrackMinWidth = 120.0f; /**< 弱峰增强滑块轨道的最小宽度。 */
const int kWeakPeakIterationsMin = 1; /**< 迭代次数 N 的下限。 */
const int kWeakPeakIterationsMax = 10; /**< 迭代次数 N 的上限。 */
/** 迭代次数滑块「自定义上限」的允许范围(与算法内部的阶数夹取保持一致)。 */
const int kWeakPeakIterationsMaxLimit = 1000;
/** 迭代次数滑块上限的默认值(与 `Lane::WeakPeakItem::maxIterations` 一致)。 */
const int kWeakPeakIterationsDefaultMax = 10;
/** 弱峰增强等级上限(面板上显示为 200%)。 */
const double kWeakPeakLevelMax = 2.0;
/** 弱峰增强等级百分比上限(用于构建新信号 / 曲线缓存键)。 */
@@ -95,12 +101,56 @@ namespace jdi
* 此时改为交后台线程计算、本帧先用旧曲线顶替(双缓冲),保证控件反馈始终跟手。
*/
const std::size_t kDerivedAsyncWindowThreshold = 65536;
const wchar_t* const kWeakPeakLevelLabel = L"增强等级 %"; /**< 逐条目面板里「等级」输入框的标签。 */
const wchar_t* const kWeakPeakIterationsLabel = L"迭代次数 N"; /**< 逐条目面板里「迭代次数」输入框的标签。 */
const wchar_t* const kWeakPeakLevelLabel = L"增强等级 %"; /**< 逐条目面板里「等级」滑块行的标签。 */
const wchar_t* const kWeakPeakIterationsLabel = L"迭代次数 N"; /**< 逐条目面板里「迭代次数」滑块行的标签。 */
const wchar_t* const kWeakPeakTitle = L"弱峰增强"; /**< 弱峰增强面板标题。 */
const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰增强共用)。 */
const std::size_t kEdgeCountCacheLimit = 48; /**< 边沿统计缓存条目上限(超出时淘汰最早的一条)。 */
/**
* @brief 判断坐标是否命中「弱峰增强」的某条滑块轨道。
*
* 轨道本身只有几个像素高,直接按轨道矩形命中很难抓住,因此把命中区在纵向上下各放宽
* (至少一个轨道高度),与样式区滑块的交互一致。
*
* @param [IN] track 滑块轨道矩形(宽度为 0 表示当前没有滑块)。
* @param [IN] scale DPI 缩放比例。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中返回 true。
*/
bool WeakPeakSliderHit(const gd::RectF& track, float scale, float x, float y)
{
if (track.Width <= 0.0f)
{
return false;
}
gd::RectF hit = track;
const float pad = std::max(8.0f * scale, track.Height);
hit.Y -= pad;
hit.Height += pad * 2.0f;
return UiPointInRect(hit, x, y);
}
/**
* @brief 「弱峰增强」滑块行右侧**数值列**的宽度(布局与面板宽度共用同一算式)。
*
* 取「200%」与「10000」两者较宽者:前者是等级滑块的最大显示文本,
* 后者为迭代次数上限留出足够位宽(上限可被用户改大)。
*/
float WeakPeakValueColumnWidth(HFONT monoSmall)
{
return std::max(UiMeasureTextWidth(monoSmall, L"200%"),
UiMeasureTextWidth(monoSmall, L"10000"));
}
/** @brief 「弱峰增强」滑块行左侧**标签列**的宽度(两条标签取较宽者,保证轨道左对齐)。 */
float WeakPeakLabelColumnWidth(HFONT uiSmall)
{
return std::max(UiMeasureTextWidth(uiSmall, kWeakPeakLevelLabel),
UiMeasureTextWidth(uiSmall, kWeakPeakIterationsLabel));
}
/**
* @brief 取折线(相邻采样点之间线性连接)在给定时刻的取值。
*
@@ -2468,7 +2518,7 @@ namespace jdi
int levelPercent, int iterations) const
{
const int level = std::max(0, std::min(kWeakPeakPercentMax, levelPercent));
const int stages = std::max(kWeakPeakIterationsMin, std::min(kWeakPeakIterationsMax, iterations));
const int stages = std::max(kWeakPeakIterationsMin, std::min(kWeakPeakIterationsMaxLimit, iterations));
for (const WeakPeakCurveEntry& entry : m_weakPeakCurveCache)
{
if (entry.signal == &signal && entry.levelPercent == level && entry.iterations == stages)
@@ -2861,13 +2911,11 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth));
}
if (centerAxis || meanFilter || weakPeak)
if (centerAxis || meanFilter)
{
// 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。
// (MEAN / CENTER:「均值点数 X」+「迭代次数 N」;PEAK:「增强等级 %」+「迭代次数 N」。
// 标签与数值框各占半行。)
const wchar_t* const labelX = weakPeak ? kWeakPeakLevelLabel : L"均值点数 X";
const float labelXWidth = UiMeasureTextWidth(m_fonts->uiSmall, labelX);
// (MEAN / CENTER:「均值点数 X」+「迭代次数 N」。标签与数值框各占半行。)
const float labelXWidth = UiMeasureTextWidth(m_fonts->uiSmall, L"均值点数 X");
const float labelN = UiMeasureTextWidth(m_fonts->uiSmall, L"迭代次数 N");
const float fieldWidth =
std::max(kEdgePanelEditFieldWidth * m_scale * 0.75f,
@@ -2877,6 +2925,15 @@ namespace jdi
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
}
if (weakPeak)
{
// 滑块行 = 「标签列 + 轨道(最小宽度)+ 数值列」(等级与迭代次数两行共用同一列宽)。
const float rowWidth = WeakPeakLabelColumnWidth(m_fonts->uiSmall) + 6.0f * m_scale +
kWeakPeakTrackMinWidth * m_scale + 6.0f * m_scale +
WeakPeakValueColumnWidth(m_fonts->monoSmall);
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
}
if (meanFilter || weakPeak)
{
// 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。
@@ -3405,11 +3462,10 @@ namespace jdi
m_editTarget == PlotEditTarget::EDGE_CENTER_ITERATIONS ||
m_editTarget == PlotEditTarget::MEAN_FILTER_POINTS ||
m_editTarget == PlotEditTarget::MEAN_FILTER_ITERATIONS ||
m_editTarget == PlotEditTarget::PEAK_LEVEL_PERCENT ||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS)
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX)
{
// 「均值点数 X」/「迭代次数 N」(均值滤波、中心轴)与「增强等级 %」/「迭代次数 N」(弱峰增强):
// 输入框都落在对应的字段矩形上(三类面板的字段矩形含义相同)。
// 「均值点数 X」/「迭代次数 N」(均值滤波、中心轴)与「迭代次数 N 上限」(弱峰增强):
// 输入框都落在对应的字段/滑块行矩形上。
if (m_editFieldRect.Width <= 0.0f)
{
return gd::RectF();
@@ -3464,7 +3520,6 @@ namespace jdi
}
m_editTarget = target;
m_editLane = laneIndex;
// 名称与数值均不限长度(数值在提交时会做范围夹取)。
const bool nameField = (target == PlotEditTarget::SIGNAL_NAME);
m_edit.Begin(initialText, true, nameField ? EditFilter::ANY : EditFilter::NUMBER, 0u);
@@ -3501,6 +3556,7 @@ namespace jdi
m_editLane = -1;
m_editSignal = SignalRef();
m_editFieldRect = gd::RectF();
m_peakEditCurrentMax = 0; // 退出编辑:清除“迭代上限”输入框的灰色提示值
m_edit.End();
// 信号改名:交给宿主窗口落到数据层,绘图区只持有只读数据。
@@ -3575,12 +3631,12 @@ namespace jdi
return true;
}
// 「均值点数 X」/「迭代次数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。
// 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用于自己的条目列表。
const bool centerTarget =
(target == PlotEditTarget::EDGE_CENTER_POINTS || target == PlotEditTarget::EDGE_CENTER_ITERATIONS);
// 「弱峰增强」也是逐信号参数(增强等级 % + 迭代次数 N),只是作用于自己的条目列表。
if (target == PlotEditTarget::PEAK_LEVEL_PERCENT || target == PlotEditTarget::PEAK_ITERATIONS)
// 「弱峰增强」迭代次数滑块的**上限**:用户可直接输入一个正整数作为新的量程上限。
// 规则(用户明确要求):
// * 输入框只允许输入正整数(过滤策略为 BYTE = 纯数字,粘贴同样会被清洗);
// * 空内容 / 非法值(0、超出允许范围)→ 保持原上限不变;
// * 生效后若当前迭代次数超过新上限,则自动夹到新上限。
if (target == PlotEditTarget::PEAK_ITERATIONS_MAX)
{
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
const Lane::WeakPeakItem* existing = lane.FindWeakPeakItem(m_editMeanRef);
@@ -3588,21 +3644,27 @@ namespace jdi
{
return true;
}
Lane::WeakPeakItem& item = lane.EnsureWeakPeakItem(m_editMeanRef);
if (target == PlotEditTarget::PEAK_LEVEL_PERCENT)
if (parsedOk)
{
// 增强等级 0–200%。
item.levelPercent = std::max(0, std::min(kWeakPeakPercentMax, static_cast<int>(std::lround(parsed))));
const long long value = static_cast<long long>(std::llround(parsed));
if (value >= kWeakPeakIterationsMin && value <= kWeakPeakIterationsMaxLimit)
{
Lane::WeakPeakItem& item = lane.EnsureWeakPeakItem(m_editMeanRef);
item.maxIterations = static_cast<int>(value);
// 当前值超出新上限 → 自动替换为上界。
item.iterations = std::max(kWeakPeakIterationsMin,
std::min(item.maxIterations, item.iterations));
// 曲线可能因迭代次数被夹取而变化,缓存整体作废最稳妥。
m_weakPeakCurveCache.clear();
}
}
else
{
item.iterations = std::max(kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMax, static_cast<int>(std::lround(parsed))));
}
// 参数变化会改变曲线:清掉曲线缓存即可(局部重画由 `m_sceneDerived` 快照驱动)。
m_weakPeakCurveCache.clear();
return true;
}
// 「均值点数 X」/「迭代次数 N」:作用于被勾选信号的均值滤波曲线,夹在合理范围内。
// 「中心轴边沿计数」用**完全相同的参数与语义**,只是作用于自己的条目列表。
const bool centerTarget =
(target == PlotEditTarget::EDGE_CENTER_POINTS || target == PlotEditTarget::EDGE_CENTER_ITERATIONS);
if (target == PlotEditTarget::MEAN_FILTER_POINTS || target == PlotEditTarget::MEAN_FILTER_ITERATIONS ||
centerTarget)
{
@@ -3830,6 +3892,10 @@ namespace jdi
case PlotDragMode::CURSOR_SPAN_MOVE:
// 差值标签拖拽:两条光标整体沿时间轴平移 → 左右箭头。
return LoadCursorW(nullptr, IDC_SIZEWE);
case PlotDragMode::PEAK_LEVEL:
case PlotDragMode::PEAK_ITERATIONS:
// 「弱峰增强」滑块拖拽 → 左右箭头。
return LoadCursorW(nullptr, IDC_SIZEWE);
case PlotDragMode::MOVE_SIGNAL:
// 拖动信号(参考 Photoshop 平移元素的抓握光标)。
return LoadCursorW(nullptr, IDC_SIZEALL);
@@ -3880,6 +3946,31 @@ namespace jdi
// 「构建为新信号」按钮:手型光标作为可点击提示。
return LoadCursorW(nullptr, IDC_HAND);
}
// 「弱峰增强」的逐信号滑块:轨道 → 左右箭头;迭代次数右侧数值 → 手型(可点击改上限)。
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowIterValues.size(); ++rowIndex)
{
if (layout.rowIterValues[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterValues[rowIndex], static_cast<float>(x),
static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_HAND);
}
if (rowIndex < layout.rowIterTracks.size() &&
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale,
static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEWE);
}
if (rowIndex < layout.rowLevelTracks.size() &&
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale,
static_cast<float>(x), static_cast<float>(y)))
{
return LoadCursorW(nullptr, IDC_SIZEWE);
}
}
}
// 面板主体(含底部编辑行与参数输入框):保持原始箭头。
return nullptr;
}
@@ -4121,6 +4212,70 @@ namespace jdi
m_dragMoved = true;
return true;
}
case PlotDragMode::PEAK_LEVEL:
{
// 「弱峰增强」某信号的等级滑块:按滑轨横向位置换算 0–200%。
if (m_tab == nullptr || m_peakDragLane < 0 ||
m_peakDragLane >= static_cast<int>(m_tab->Lanes().size()))
{
break;
}
const EdgePanelLayout layout = EdgePanelLayoutFor(m_peakDragLane, EdgePanelKind::PEAK);
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_peakDragLane)];
if (lane.FindWeakPeakItem(m_peakDragRef) == nullptr)
{
break;
}
Lane::WeakPeakItem& item = lane.EnsureWeakPeakItem(m_peakDragRef);
const int rowIndex = PeakRowIndexOf(layout, m_peakDragRef);
if (rowIndex < 0 || rowIndex >= static_cast<int>(layout.rowLevelTracks.size()) ||
layout.rowLevelTracks[static_cast<std::size_t>(rowIndex)].Width <= 0.0f)
{
break;
}
const double ratio =
UiSliderRatioFromX(layout.rowLevelTracks[static_cast<std::size_t>(rowIndex)], fx);
item.levelPercent =
std::max(0, std::min(kWeakPeakPercentMax,
static_cast<int>(std::lround(ratio * 100.0 * kWeakPeakLevelMax))));
m_weakPeakCurveCache.clear();
m_dragMoved = true;
return true;
}
case PlotDragMode::PEAK_ITERATIONS:
{
// 「弱峰增强」某信号的迭代次数滑块:量程为 1 … 该信号自己的上限。
if (m_tab == nullptr || m_peakDragLane < 0 ||
m_peakDragLane >= static_cast<int>(m_tab->Lanes().size()))
{
break;
}
const EdgePanelLayout layout = EdgePanelLayoutFor(m_peakDragLane, EdgePanelKind::PEAK);
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_peakDragLane)];
if (lane.FindWeakPeakItem(m_peakDragRef) == nullptr)
{
break;
}
Lane::WeakPeakItem& item = lane.EnsureWeakPeakItem(m_peakDragRef);
const int rowIndex = PeakRowIndexOf(layout, m_peakDragRef);
if (rowIndex < 0 || rowIndex >= static_cast<int>(layout.rowIterTracks.size()) ||
layout.rowIterTracks[static_cast<std::size_t>(rowIndex)].Width <= 0.0f)
{
break;
}
const double ratio =
UiSliderRatioFromX(layout.rowIterTracks[static_cast<std::size_t>(rowIndex)], fx);
const int peakMax = std::max(kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit, item.maxIterations));
item.iterations = std::max(
kWeakPeakIterationsMin,
std::min(peakMax, kWeakPeakIterationsMin +
static_cast<int>(std::lround(
ratio * (peakMax - kWeakPeakIterationsMin)))));
m_weakPeakCurveCache.clear();
m_dragMoved = true;
return true;
}
case PlotDragMode::MOVE_SIGNAL:
{
const int target = LaneAtY(y);
@@ -4433,7 +4588,7 @@ namespace jdi
}
// 「中心轴边沿计数」/「均值滤波」/「弱峰增强」面板:逐信号条目
// (同一套「名称 + 复选框 + 两个参数输入框」排布,只是绑定的条目列表不同)。
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰增强是「名称 + 复选框 + 两行滑块」)。
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
kind == EdgePanelKind::PEAK) &&
m_tab != nullptr)
@@ -4442,6 +4597,78 @@ namespace jdi
const bool boostMode = (kind == EdgePanelKind::PEAK);
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
// 「弱峰增强」:先判两行滑块(数值文本 → 改上限;轨道 → 拖动)。
if (boostMode)
{
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
const bool hasLevel = rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f;
if (!hasLevel)
{
continue;
}
if (tabLane.FindWeakPeakItem(ref) == nullptr)
{
continue;
}
Lane::WeakPeakItem& item = tabLane.EnsureWeakPeakItem(ref);
// 迭代次数行右侧的**数值文本**:点击后整行变成输入框以修改滑块上限。
// ⚠️ 必须放在轨道命中之前(数值区在轨道右侧,命中区不重叠,但顺序更安全)。
if (layout.rowIterValues[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterValues[rowIndex], fx, fy))
{
m_editMeanRef = ref;
m_peakEditCurrentMax = item.maxIterations;
// 输入框以**空内容**开始:空内容时显示当前上限作为灰色提示。
// 输入框覆盖"整条滑块行"(从轨道左端到数值文本右端)。
const gd::RectF& track = layout.rowIterTracks[rowIndex];
const gd::RectF& value = layout.rowIterValues[rowIndex];
const gd::RectF rowRect(track.X, value.Y, value.X + value.Width - track.X,
value.Height);
BeginEdit(PlotEditTarget::PEAK_ITERATIONS_MAX, index, L"", rowRect);
// `BeginEdit` 默认用 NUMBER 过滤(允许负号/小数点);这里改为**纯数字**,
// 从源头保证"只允许输入正整数"(负数、小数根本无法键入,粘贴同样被清洗)。
m_edit.Begin(L"", false, EditFilter::BYTE, 0u);
m_dragMode = PlotDragMode::NONE;
return true;
}
if (layout.rowIterTracks[rowIndex].Width > 0.0f &&
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale, fx, fy))
{
m_dragMode = PlotDragMode::PEAK_ITERATIONS;
m_peakDragLane = index;
m_peakDragRef = ref;
const double ratio = UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
const int peakMax = std::max(kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit,
item.maxIterations));
item.iterations = std::max(
kWeakPeakIterationsMin,
std::min(peakMax, kWeakPeakIterationsMin +
static_cast<int>(std::lround(
ratio * (peakMax - kWeakPeakIterationsMin)))));
m_weakPeakCurveCache.clear();
return true;
}
if (layout.rowLevelTracks[rowIndex].Width > 0.0f &&
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
{
m_dragMode = PlotDragMode::PEAK_LEVEL;
m_peakDragLane = index;
m_peakDragRef = ref;
item.levelPercent = static_cast<int>(std::lround(
UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx) * 100.0 *
kWeakPeakLevelMax));
item.levelPercent =
std::max(0, std::min(kWeakPeakPercentMax, item.levelPercent));
m_weakPeakCurveCache.clear();
return true;
}
}
}
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
{
const SignalRef& ref = layout.rowRefs[rowIndex];
@@ -4469,52 +4696,32 @@ namespace jdi
m_dragMode = PlotDragMode::NONE;
return true;
}
// 第二排的两个输入框 → 就地编辑该信号的参数
//(MEAN/CENTER:均值点数 X / 迭代次数 N;PEAK:增强等级 % / 迭代次数 N)。
// 第二排的两个输入框 → 就地编辑该信号的参数(仅 MEAN / CENTER 面板;
// 弱峰增强用的是滑块,已在上面处理)。
if (layout.rowPoints[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowPoints[rowIndex], fx, fy))
{
m_editMeanRef = ref;
if (boostMode)
{
const Lane::WeakPeakItem* item = tabLane.FindWeakPeakItem(ref);
BeginEdit(PlotEditTarget::PEAK_LEVEL_PERCENT, index,
std::to_wstring(item != nullptr ? item->levelPercent : 150),
layout.rowPoints[rowIndex]);
}
else
{
const Lane::MeanFilterItem* item =
meanMode ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS,
index, std::to_wstring(item != nullptr ? item->points : 5),
layout.rowPoints[rowIndex]);
}
const Lane::MeanFilterItem* item =
meanMode ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS,
index, std::to_wstring(item != nullptr ? item->points : 5),
layout.rowPoints[rowIndex]);
return true;
}
if (layout.rowIterations[rowIndex].Width > 0.0f &&
UiPointInRect(layout.rowIterations[rowIndex], fx, fy))
{
m_editMeanRef = ref;
if (boostMode)
{
const Lane::WeakPeakItem* item = tabLane.FindWeakPeakItem(ref);
BeginEdit(PlotEditTarget::PEAK_ITERATIONS, index,
std::to_wstring(item != nullptr ? item->iterations : 1),
layout.rowIterations[rowIndex]);
}
else
{
const Lane::MeanFilterItem* item =
meanMode ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ITERATIONS
: PlotEditTarget::EDGE_CENTER_ITERATIONS,
index, std::to_wstring(item != nullptr ? item->iterations : 1),
layout.rowIterations[rowIndex]);
}
const Lane::MeanFilterItem* item =
meanMode ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ITERATIONS
: PlotEditTarget::EDGE_CENTER_ITERATIONS,
index, std::to_wstring(item != nullptr ? item->iterations : 1),
layout.rowIterations[rowIndex]);
return true;
}
}
@@ -4542,9 +4749,12 @@ namespace jdi
build.signalIndex = ref.signalIndex;
build.levelPercent =
std::max(0, std::min(kWeakPeakPercentMax, item->levelPercent));
build.iterations = std::max(
// 迭代次数按**该信号自己的滑块量程**(1 … `maxIterations`)夹取。
const int peakMax = std::max(
kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMax, item->iterations));
std::min(kWeakPeakIterationsMaxLimit, item->maxIterations));
build.iterations =
std::max(kWeakPeakIterationsMin, std::min(peakMax, item->iterations));
m_pendingWeakPeakItems.push_back(build);
}
if (!m_pendingWeakPeakItems.empty())
@@ -4810,6 +5020,9 @@ namespace jdi
m_edgeDragLane = -1;
m_edgePanelDragLane = -1;
m_edgePanelDragKind = EdgePanelKind::COUNT;
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
m_peakDragLane = -1;
m_peakDragRef = SignalRef();
// 松开左键:粘滞提示结束计时,开始"停留后淡出"。
ReleaseHint();
return changed;
@@ -5411,9 +5624,11 @@ namespace jdi
PaintEdgeCursor(g, m_lanes);
PaintSignalInteraction(g);
PaintHint(g);
// 网格 Y 轴上下限的输入框绘制在最上层。
// 网格 Y 轴上下限的输入框、以及「弱峰增强 → 迭代次数上限」的输入框都绘制在最上层
//(后者覆盖在浮动面板的滑块行之上)。
if (m_editTarget == PlotEditTarget::LANE_Y_MAX ||
m_editTarget == PlotEditTarget::LANE_Y_MIN)
m_editTarget == PlotEditTarget::LANE_Y_MIN ||
m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX)
{
PaintEditField(g);
}
@@ -7847,7 +8062,8 @@ namespace jdi
const int levelPercent =
std::max(0, std::min(kWeakPeakPercentMax, item->levelPercent));
const int boostIterations = std::max(
kWeakPeakIterationsMin, std::min(kWeakPeakIterationsMax, item->iterations));
kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit, item->iterations));
PaintWeakPeakCurve(g, resolved, lane, levelPercent, boostIterations);
}
}
@@ -8643,7 +8859,7 @@ namespace jdi
std::max(0, std::min(kWeakPeakPercentMax, boostItem->levelPercent));
const int iterations = std::max(
kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMax, boostItem->iterations));
std::min(kWeakPeakIterationsMaxLimit, boostItem->iterations));
paintDerivedCapsule(DerivedCurveKind::WEAK_PEAK, 0, iterations, levelPercent,
L"弱峰增强");
}
@@ -8867,11 +9083,11 @@ namespace jdi
const float rowsPerSignal = meanFilter ? 1.0f : 2.0f;
// 逐条目面板:每个条目 = 第一排(信号名 + 复选框)
// + 第二排(均值点数 X / 迭代次数 N,或弱峰增强的增强等级 % / 迭代次数 N)
// +(中心轴)第三排(统计结果);
// 后两排**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化。
// 逐条目参数行:MEAN / CENTER / PEAK 三种面板共用(PEEK 与 MEAN 完全同构)。
// + 参数部分(**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化):
// * MEAN / CENTER:一行「均值点数 X + 迭代次数 N」输入框(中心轴再多一行统计结果);
// * PEAK:两行滑块(增强等级 % / 迭代次数 N),每个信号各有一套自己的参数。
const bool itemHasParams = (centerAxis || meanFilter || weakPeak);
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
float meanRowHeight = 0.0f;
float itemsHeight = 0.0f;
float height = 0.0f;
@@ -8891,10 +9107,17 @@ namespace jdi
: lane.EdgeCenterEnabled(ref));
if (checked && itemHasParams)
{
itemsHeight += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale;
if (centerAxis)
if (weakPeak)
{
itemsHeight += kEdgePanelMeanRowHeight * m_scale;
itemsHeight += weakPeakSliderRow * 2.0f;
}
else
{
itemsHeight += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale;
if (centerAxis)
{
itemsHeight += kEdgePanelMeanRowHeight * m_scale;
}
}
}
}
@@ -8946,12 +9169,19 @@ namespace jdi
}
// 逐信号条目:第一排 = 名称 + 复选框(复选框靠右);
// 第二排 = 两个参数输入框(MEAN:均值点数 X / 迭代次数 N;PEAK:增强等级 % / 迭代次数 N,
// 仅勾选时存在;
// 第三排(中心轴)= 上升沿/下降沿统计结果(仅勾选时存在)。
// 参数部分:MEAN / CENTER = 一行两个输入框(中心轴再多一行统计结果);
// PEAK = 两行滑块(每个信号各有自己的等级与迭代次数)。
const float checkSize = kEdgePanelMeanCheckSize * m_scale;
const float fieldHeight = kEdgePanelMeanEditHeight * m_scale;
const float halfWidth = std::max(40.0f, (width - pad * 2.0f - 8.0f * m_scale) * 0.5f);
// 滑块行列宽(与 `EdgePanelWidth` 使用同一组算式,避免布局与宽度计算漂移)。
const float weakPeakLabelWidth = WeakPeakLabelColumnWidth(m_fonts->uiSmall);
const float weakPeakValueWidth = WeakPeakValueColumnWidth(m_fonts->monoSmall);
const float weakPeakTrackHeight = kWeakPeakTrackHeight * m_scale;
const float weakPeakTrackLeft = left + pad + weakPeakLabelWidth + 6.0f * m_scale;
const float weakPeakTrackWidth =
std::max(10.0f * m_scale, (left + width - pad - weakPeakValueWidth - 6.0f * m_scale) -
weakPeakTrackLeft);
float rowY = top + titleHeight + pad;
for (const SignalRef& ref : lane.signals)
{
@@ -8968,6 +9198,48 @@ namespace jdi
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref)
: (weakPeak ? lane.WeakPeakEnabled(ref)
: lane.EdgeCenterEnabled(ref));
if (weakPeak)
{
// 每个被勾选的信号各有两行滑块:等级(0–200%)与迭代次数(1 … 上限)。
if (checked)
{
const float levelRowTop = rowY + meanRowHeight;
const float iterRowTop = levelRowTop + weakPeakSliderRow;
const gd::RectF levelTrack(
weakPeakTrackLeft,
levelRowTop + (weakPeakSliderRow - weakPeakTrackHeight) * 0.5f,
weakPeakTrackWidth, weakPeakTrackHeight);
const gd::RectF iterTrack(
weakPeakTrackLeft,
iterRowTop + (weakPeakSliderRow - weakPeakTrackHeight) * 0.5f,
weakPeakTrackWidth, weakPeakTrackHeight);
layout.rowLevelTracks.push_back(levelTrack);
layout.rowLevelValues.push_back(gd::RectF(
levelTrack.X + levelTrack.Width + 6.0f * m_scale, levelRowTop,
weakPeakValueWidth, weakPeakSliderRow));
layout.rowIterTracks.push_back(iterTrack);
layout.rowIterValues.push_back(gd::RectF(
iterTrack.X + iterTrack.Width + 6.0f * m_scale, iterRowTop,
weakPeakValueWidth, weakPeakSliderRow));
}
else
{
layout.rowLevelTracks.push_back(gd::RectF());
layout.rowLevelValues.push_back(gd::RectF());
layout.rowIterTracks.push_back(gd::RectF());
layout.rowIterValues.push_back(gd::RectF());
}
layout.rowPoints.push_back(gd::RectF());
layout.rowIterations.push_back(gd::RectF());
layout.rowStats.push_back(gd::RectF());
rowY += meanRowHeight;
if (checked)
{
rowY += weakPeakSliderRow * 2.0f;
}
continue;
}
if (checked && itemHasParams)
{
const float editTop = rowY + meanRowHeight;
@@ -9007,6 +9279,18 @@ namespace jdi
return layout;
}
int PlotView::PeakRowIndexOf(const EdgePanelLayout& layout, const SignalRef& ref) const
{
for (std::size_t index = 0; index < layout.rowRefs.size(); ++index)
{
if (layout.rowRefs[index] == ref)
{
return static_cast<int>(index);
}
}
return -1;
}
bool PlotView::IsOnEdgeCursorLine(int x, int y, int& laneIndex) const
{
laneIndex = -1;
@@ -9650,10 +9934,25 @@ namespace jdi
const Lane::EdgeCenterItem* centerItem = lane.FindEdgeCenterItem(ref);
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->points + 2 : 0));
mix(static_cast<std::uint64_t>(centerItem != nullptr ? centerItem->iterations + 2 : 0));
// 弱峰增强的逐信号参数(增强等级 % + 迭代次数)直接显示在面板输入框里。
// 弱峰增强的逐信号参数(增强等级 % + 迭代次数 + 该信号的滑块上限)直接显示在面板上:
// 上限决定迭代次数滑块的圆钮位置,因此也必须参与缓存键。
const Lane::WeakPeakItem* peakItem = lane.FindWeakPeakItem(ref);
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->levelPercent + 2 : 0));
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->iterations + 2 : 0));
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
}
// 弱峰增强的滑块行矩形(每信号两行)。
for (const gd::RectF& rect : layout.rowLevelTracks)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowIterTracks)
{
mixRect(rect);
}
for (const gd::RectF& rect : layout.rowIterValues)
{
mixRect(rect);
}
// 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。
@@ -9958,28 +10257,101 @@ namespace jdi
continue;
}
// 第二排:两个参数输入框。
// MEAN/CENTER = 「均值点数 X / 迭代次数 N」;PEAK = 「增强等级 % / 迭代次数 N」。
const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref);
const Lane::WeakPeakItem* peakItem =
weakPeak ? tabLane.FindWeakPeakItem(ref) : nullptr;
const int shownPoints =
weakPeak ? (peakItem != nullptr ? peakItem->levelPercent : 150)
: ((item != nullptr) ? item->points : 5);
const int shownOrder = weakPeak
? (peakItem != nullptr ? peakItem->iterations : 1)
: ((item != nullptr) ? item->iterations : 1);
// MEAN / CENTER 的平滑参数条目(弱峰增强用自己的 `WeakPeakItem`)。
const Lane::MeanFilterItem* item =
(meanFilter || centerAxis)
? (meanFilter ? tabLane.FindMeanFilterItem(ref)
: tabLane.FindEdgeCenterItem(ref))
: nullptr;
// 「弱峰增强」:每个被勾选的信号各有两行**滑块**
//(增强等级 0–200% / 迭代次数 1 … 该信号自定义上限)。
// 迭代次数行的右侧数值可点击 → 整行变为输入框以修改滑块上限。
if (weakPeak)
{
const Lane::WeakPeakItem* peakItem = tabLane.FindWeakPeakItem(ref);
if (peakItem != nullptr && rowIndex < layout.rowLevelTracks.size() &&
layout.rowLevelTracks[rowIndex].Width > 0.0f)
{
// 两行滑块的「标签」始终绘制(编辑上限时也要能看出在改哪一项);
// 「轨道 + 数值」在编辑上限时改由 `PaintEditField` 画成输入框。
const auto paintSliderLabel = [&](const gd::RectF& track,
const wchar_t* label)
{
const float labelColumn =
WeakPeakLabelColumnWidth(m_fonts->uiSmall);
const float labelLeft =
track.X - 6.0f * m_scale - labelColumn;
UiText(g, label, m_fonts->uiSmall,
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
labelColumn, track.Height + 16.0f * m_scale),
m_theme->textMuted, TextAlign::LEFT);
};
const auto paintSliderBody = [&](const gd::RectF& track, double ratio,
const std::wstring& valueText,
bool active)
{
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
UiText(g, valueText, m_fonts->monoSmall,
gd::RectF(track.X + track.Width + 6.0f * m_scale,
track.Y - 8.0f * m_scale,
WeakPeakValueColumnWidth(m_fonts->monoSmall),
track.Height + 16.0f * m_scale),
m_theme->textStrong, TextAlign::LEFT);
};
const gd::RectF& levelTrack = layout.rowLevelTracks[rowIndex];
const gd::RectF& iterTrack = layout.rowIterTracks[rowIndex];
paintSliderLabel(levelTrack, kWeakPeakLevelLabel);
paintSliderLabel(iterTrack, kWeakPeakIterationsLabel);
const bool levelActive = (m_dragMode == PlotDragMode::PEAK_LEVEL) &&
(m_peakDragLane == static_cast<int>(index)) &&
(m_peakDragRef == ref);
const double levelRatio =
static_cast<double>(std::max(0, std::min(kWeakPeakPercentMax,
peakItem->levelPercent))) /
(100.0 * kWeakPeakLevelMax);
paintSliderBody(levelTrack, levelRatio,
std::to_wstring(peakItem->levelPercent) + L"%",
levelActive);
// 迭代次数滑块:量程 = 1 … 该信号自己的上限。
const int peakMax = std::max(kWeakPeakIterationsMin,
std::min(kWeakPeakIterationsMaxLimit,
peakItem->maxIterations));
const int peakIter = std::max(
kWeakPeakIterationsMin, std::min(peakMax, peakItem->iterations));
const double iterRatio =
(peakMax > kWeakPeakIterationsMin)
? (static_cast<double>(peakIter - kWeakPeakIterationsMin) /
static_cast<double>(peakMax - kWeakPeakIterationsMin))
: 0.0;
const bool iterActive = (m_dragMode == PlotDragMode::PEAK_ITERATIONS) &&
(m_peakDragLane == static_cast<int>(index)) &&
(m_peakDragRef == ref);
const bool editingMax =
(m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX) &&
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
if (!editingMax)
{
paintSliderBody(iterTrack, iterRatio, std::to_wstring(peakIter),
iterActive);
}
}
}
// MEAN / CENTER:第二排的两个参数输入框。
if (!weakPeak)
{
const int shownPoints = (item != nullptr) ? item->points : 5;
const int shownOrder = (item != nullptr) ? item->iterations : 1;
const PlotEditTarget pointsTarget =
weakPeak ? PlotEditTarget::PEAK_LEVEL_PERCENT
: (meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS);
meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS
: PlotEditTarget::EDGE_CENTER_POINTS;
const PlotEditTarget orderTarget =
weakPeak ? PlotEditTarget::PEAK_ITERATIONS
: (meanFilter ? PlotEditTarget::MEAN_FILTER_ITERATIONS
: PlotEditTarget::EDGE_CENTER_ITERATIONS);
const wchar_t* const pointsLabel =
weakPeak ? kWeakPeakLevelLabel : L"均值点数 X";
meanFilter ? PlotEditTarget::MEAN_FILTER_ITERATIONS
: PlotEditTarget::EDGE_CENTER_ITERATIONS;
const auto paintField = [&](const gd::RectF& rect, const wchar_t* label,
int value, PlotEditTarget target)
{
@@ -10016,9 +10388,10 @@ namespace jdi
m_theme->textStrong, TextAlign::CENTER);
}
};
paintField(layout.rowPoints[rowIndex], pointsLabel, shownPoints, pointsTarget);
paintField(layout.rowPoints[rowIndex], L"均值点数 X", shownPoints, pointsTarget);
paintField(layout.rowIterations[rowIndex], kWeakPeakIterationsLabel, shownOrder,
orderTarget);
}
// 第三排(中心轴):该信号在当前参数下的上升沿/下降沿统计结果。
if (centerAxis && rowIndex < layout.rowStats.size() &&
@@ -10143,6 +10516,23 @@ namespace jdi
// 编辑态:强调色外框 + 选区高亮 + 插入符,首次键入即整体替换(与常规输入框一致)。
UiDrawEditField(g, fieldRect, m_edit, m_fonts->monoSmall, m_scale);
// 「迭代次数 N 上限」:输入框**以空内容开始**,未输入任何内容时以灰色提示当前上限值
//(用户明确要求:"在未输入任何内容时,输入框内提示当前上限值")。
// 提示文本比插入符稍微右移几像素,避免插入符压在上面看不清数字。
if (m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX && m_edit.Text().empty())
{
const int currentMax = m_peakEditCurrentMax > 0 ? m_peakEditCurrentMax
: kWeakPeakIterationsDefaultMax;
const std::wstring hint = std::to_wstring(currentMax);
const float hintLeft = fieldRect.X + 11.0f * m_scale;
UiTextAtClipped(g, hint, m_fonts->monoSmall, hintLeft,
gd::RectF(hintLeft, fieldRect.Y,
std::max(0.0f, fieldRect.X + fieldRect.Width -
hintLeft - 4.0f * m_scale),
fieldRect.Height),
m_theme->textMuted, fieldRect);
}
}
void PlotView::PaintSignalInteraction(gd::Graphics& g)