From a30a6fcf4d53f5a0a1e8ed428ad80dfeb8f5bd10 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Mon, 12 Oct 2026 13:35:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=B1=E5=B3=B0=E5=A2=9E=E5=BC=BA=E6=94=B9?= =?UTF-8?q?=E5=9B=9E=E9=80=90=E4=BF=A1=E5=8F=B7=E6=BB=91=E5=9D=97=EF=BC=9B?= =?UTF-8?q?=E8=BF=AD=E4=BB=A3=E6=AC=A1=E6=95=B0=E4=B8=8A=E9=99=90=E5=8F=AF?= =?UTF-8?q?=E5=B0=B1=E5=9C=B0=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、参数输入方式(回到滑块形态,但每个信号各有一套) - 面板布局:每个被勾选的信号 = 名称行(+复选框)+ 两行滑块: 「增强等级 %」(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 保持像素一致。 --- Components/PlotView/Inc/PlotView.hpp | 38 +- Components/PlotView/Src/PlotView.cpp | 602 ++++++++++++++++++++++----- Documents/agent_mem.md | 64 +++ Utilities/Inc/DataModel.hpp | 6 +- 4 files changed, 598 insertions(+), 112 deletions(-) diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 067f875..4d5ce27 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -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 rowChecks; std::vector rowPoints; std::vector rowIterations; std::vector rowStats; + /** + * @brief 「弱峰增强」每个被勾选信号自己的两行**滑块**(与 `rowRefs` 同序)。 + * + * `rowLevelTracks/rowLevelValues` = 增强等级滑块与它右侧的数值文本; + * `rowIterTracks/rowIterValues` = 迭代次数滑块与它右侧的数值文本 + *(**点值数值文本可以把滑块上限改为别的正整数**)。 + * 未勾选或非 PEAK 面板时对应元素为空矩形(数组也可能为空)。 + */ + std::vector rowLevelTracks; + std::vector rowLevelValues; + std::vector rowIterTracks; + std::vector rowIterValues; std::vector 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 m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */ std::vector m_pendingWeakPeakItems; /**< 待构建的弱峰增强信号及其参数。 */ bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */ + int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */ + SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */ + /** + * @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。 + * + * 输入框以空内容开始,空内容时显示这个值作为提示;进入编辑时记录,提交后清零。 + */ + int m_peakEditCurrentMax = 0; /** * @brief 本帧是否使用「快速绘制」(交互期间关闭波形与派生曲线的抗锯齿)。 * diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 0a723ef..4a16adc 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -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(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(std::lround(parsed)))); + const long long value = static_cast(std::llround(parsed)); + if (value >= kWeakPeakIterationsMin && value <= kWeakPeakIterationsMaxLimit) + { + Lane::WeakPeakItem& item = lane.EnsureWeakPeakItem(m_editMeanRef); + item.maxIterations = static_cast(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(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(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(x), + static_cast(y))) + { + return LoadCursorW(nullptr, IDC_HAND); + } + if (rowIndex < layout.rowIterTracks.size() && + WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale, + static_cast(x), static_cast(y))) + { + return LoadCursorW(nullptr, IDC_SIZEWE); + } + if (rowIndex < layout.rowLevelTracks.size() && + WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, + static_cast(x), static_cast(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(m_tab->Lanes().size())) + { + break; + } + const EdgePanelLayout layout = EdgePanelLayoutFor(m_peakDragLane, EdgePanelKind::PEAK); + Lane& lane = m_tab->Lanes()[static_cast(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(layout.rowLevelTracks.size()) || + layout.rowLevelTracks[static_cast(rowIndex)].Width <= 0.0f) + { + break; + } + const double ratio = + UiSliderRatioFromX(layout.rowLevelTracks[static_cast(rowIndex)], fx); + item.levelPercent = + std::max(0, std::min(kWeakPeakPercentMax, + static_cast(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(m_tab->Lanes().size())) + { + break; + } + const EdgePanelLayout layout = EdgePanelLayoutFor(m_peakDragLane, EdgePanelKind::PEAK); + Lane& lane = m_tab->Lanes()[static_cast(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(layout.rowIterTracks.size()) || + layout.rowIterTracks[static_cast(rowIndex)].Width <= 0.0f) + { + break; + } + const double ratio = + UiSliderRatioFromX(layout.rowIterTracks[static_cast(rowIndex)], fx); + const int peakMax = std::max(kWeakPeakIterationsMin, + std::min(kWeakPeakIterationsMaxLimit, item.maxIterations)); + item.iterations = std::max( + kWeakPeakIterationsMin, + std::min(peakMax, kWeakPeakIterationsMin + + static_cast(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(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(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(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(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(centerItem != nullptr ? centerItem->points + 2 : 0)); mix(static_cast(centerItem != nullptr ? centerItem->iterations + 2 : 0)); - // 弱峰增强的逐信号参数(增强等级 % + 迭代次数)直接显示在面板输入框里。 + // 弱峰增强的逐信号参数(增强等级 % + 迭代次数 + 该信号的滑块上限)直接显示在面板上: + // 上限决定迭代次数滑块的圆钮位置,因此也必须参与缓存键。 const Lane::WeakPeakItem* peakItem = lane.FindWeakPeakItem(ref); mix(static_cast(peakItem != nullptr ? peakItem->levelPercent + 2 : 0)); mix(static_cast(peakItem != nullptr ? peakItem->iterations + 2 : 0)); + mix(static_cast(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(index)) && + (m_peakDragRef == ref); + const double levelRatio = + static_cast(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(peakIter - kWeakPeakIterationsMin) / + static_cast(peakMax - kWeakPeakIterationsMin)) + : 0.0; + const bool iterActive = (m_dragMode == PlotDragMode::PEAK_ITERATIONS) && + (m_peakDragLane == static_cast(index)) && + (m_peakDragRef == ref); + const bool editingMax = + (m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX) && + (m_editLane == static_cast(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) diff --git a/Documents/agent_mem.md b/Documents/agent_mem.md index eb80ee6..daddefd 100644 --- a/Documents/agent_mem.md +++ b/Documents/agent_mem.md @@ -2484,3 +2484,67 @@ Documents/ 项目文档(含本文件) - 新增离屏探针 `.preflight/panperf`(平移帧耗时 + 按住键静止帧耗时)。 - **验证**:`make all` 零警告;`uitest` **388/388 通过**;`panperf` PASS; `.preflight/layercheck` 保持“居中图层 vs 整幅重绘逐像素 0 差异 / 平移对齐 ≤1px / 0 次视界覆盖”。 + +--- + +## 本轮(2026-10-12,起源设备):弱峰增强改回**逐信号滑块** + 「迭代次数 N」上限可编辑 + +用户要求:把上一轮改成“逐信号两个输入框”的参数输入方式,改回**上一次提交之前**的输入方式 +(滑块),但**每个信号各自一套滑块**;并且「迭代次数 N」滑块右侧的数值可以点击, +整行(滑块 + 数值)变成一个输入框来修改该滑块的**上限**。 + +### 数据模型 + +- `Lane::WeakPeakItem` 新增 `int maxIterations = 10;`(**该信号迭代次数滑块的上限**,用户可改)。 +- 上限参与 `PanelSlotKey`(决定圆钮位置 → 面板必须重画),但**不进 `SceneContentHash`** + (上限不影响网格内画出的曲线,因此不该让波形重画)。 + +### 面板输入方式(滑块的“上一次提交之前”形态,但逐信号) + +- `EdgePanelLayout` 新增四个**逐信号**矩形数组(`std::vector`,与 `rowRefs` 同序): + `rowLevelTracks` / `rowLevelValues`(增强等级滑块 + 右侧数值)、 + `rowIterTracks` / `rowIterValues`(迭代次数滑块 + 右侧数值)。 + PEAK 面板的 `rowPoints`/`rowIterations`(文本输入框)**恒为空矩形**(MEAN/CENTER 仍用它们)。 +- 行高:每个**被勾选**的信号 = 名称行(22)+ **两行滑块**(各 26)= 74;未勾选只占名称行。 +- 列宽算式集中在两个文件内辅助函数里,`EdgePanelWidth` 与 `EdgePanelLayoutFor` 共用, + 避免两处漂移:`WeakPeakLabelColumnWidth(uiSmall)`、`WeakPeakValueColumnWidth(monoSmall)` + (后者取 `"200%"` 与 `"10000"` 的较宽者,为可改大的上限留位宽)。 + 轨道最小宽度用恢复的 `kWeakPeakTrackMinWidth = 120`。 +- 恢复 `WeakPeakSliderHit()`(按轨道矩形上下各放宽 ≥8px 命中)与 + `PlotDragMode::PEAK_LEVEL / PEAK_ITERATIONS`、`m_peakDragLane` + 新增 **`m_peakDragRef`** + (每个信号一套参数,拖动时必须记住是**哪个信号**)。 +- 删除 `PlotEditTarget::PEAK_LEVEL_PERCENT` / `PEAK_ITERATIONS` + (等级只能拖滑块;迭代次数只能拖滑块),新增 **`PlotEditTarget::PEAK_ITERATIONS_MAX`**。 + +### 「迭代次数 N」上限的就地编辑(用户明确要求) + +- **触发**:点击**迭代次数滑块右侧的数值文本**(`rowIterValues[i]`)。 +- **输入框范围**:整条滑块行 —— 从轨道左端到数值文本右端 + (`rowRect = {track.X, value.Y, value.X + value.Width - track.X, value.Height}`), + 即“整个滑动条部分包括数字完整变为一个输入框”。左侧的「迭代次数 N」标签保留可见。 +- **初始内容为空**:`BeginEdit(PEAK_ITERATIONS_MAX, lane, L"", rowRect)` 之后 + 再用 `m_edit.Begin(L"", false, EditFilter::BYTE, 0u)` 覆盖过滤器 + (`BeginEdit` 默认 `NUMBER`,允许负号/小数点)。 +- **灰色提示**:`PaintEditField()` 里当 `m_editTarget == PEAK_ITERATIONS_MAX && m_edit.Text().empty()` + 时用 `UiTextAtClipped` 画当前上限(`m_peakEditCurrentMax`,进入编辑时记录、`EndEdit` 清零); + 提示文本比插入符右移 `11 * scale`,避免插入符压在数字上。 + 该输入框在 `paintOverlays()` 里与 Y 轴上下限输入框一起绘制(在面板之上)。 +- **提交**:回车 / 点击别处(沿用既有 `EndEdit` 路径)。规则: + * `EditFilter::BYTE` = **只允许数字**(负号、小数点、字母根本无法键入,粘贴同样被清洗); + * `parsedOk` 且 `1 ≤ value ≤ kWeakPeakIterationsMaxLimit(=1000)` → 生效为新上限; + 否则(空内容 / `0` / 超出范围)→ **保持原上限不变**; + * 生效后若 `iterations > maxIterations` → **自动替换为新上限**(并清 `m_weakPeakCurveCache`)。 +- **滑块量程**:`1 … maxIterations`;比例 = `(iterations-1)/(maxIterations-1)`(`max≤1` 时取 0)。 +- 「构建为新信号」按该信号自己的量程夹取迭代次数。 + +### 验证(全部离屏,不占用鼠标/键盘/屏幕) + +- `uitest` `[X]` 重写为:逐信号滑块存在性/层序/列宽/面板内、**等级滑块拖动**(中间=100%、 + 右端=200%)、**迭代滑块拖动**(左端=1、右端=当前上限 10)、 + **点击数值进入 `PEAK_ITERATIONS_MAX` 编辑**(空内容 + 提示值 10 + 输入框横跨整行、 + 空内容回车不改、`-`/字母被拒、输入 40 生效、右端变为 40、下调到 4 时当前值被夹到 4、 + 输入 0 被拒、点击别处也能提交),并保存编辑态截图 `X_peak_max_edit.png` 供目视核对。 +- ⚠️ 测试坐标坑:`UiPointInRect` 右边界为**开区间**,按压“轨道最右端”的取整坐标可能落在 + 命中区之外 → 测试里按 `lround(X+W) - 1`;而**拖拽**可以超出(`UiSliderRatioFromX` 会夹到 1)。 +- `make all` 零警告;`uitest` **409/409 通过**;`panperf` PASS; + `layercheck` 保持“居中图层逐像素 0 差异 / 平移对齐 ≤1px / 0 次视界覆盖”。 diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 48ef253..b5d58b2 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -378,12 +378,16 @@ namespace jdi * * 与「均值滤波」完全一致:**每个被勾选的信号各自持有参数** *(增强等级 0–200% + 迭代次数 N),因此同一网格内不同信号可以使用不同强度。 + * + * ⚠️ `iterations` 的**上限由用户自己设定**(`maxIterations`):面板上「迭代次数 N」 + * 滑块的量程就是 `1 … maxIterations`,点击滑块右侧的数值可以把该上限改成其它正整数。 */ struct WeakPeakItem { SignalRef ref; /**< 目标信号。 */ int levelPercent = 150; /**< 增强等级(0–200;100% = 到达较温和的一侧,200% = 完全拉平)。 */ - int iterations = 1; /**< 迭代次数 N(1–10)。 */ + int iterations = 1; /**< 迭代次数 N(1 … `maxIterations`)。 */ + int maxIterations = 10; /**< 「迭代次数 N」滑块的上限(用户可在面板上修改,恒 ≥ 1)。 */ }; std::wstring name; /**< 网格名称。 */