「调整」数值符号规则 + 边沿统计按帧推进(含首帧公平分摊)+ 浮动面板排版与弱峰复位控件
一、「调整」类数值的符号(SignalPanel) 1. FormatSignedNumber:0 不再显示 `+`(旧实现恒带符号 → `+0.00`),Y 轴偏移默认值显示 0.00。 2. FormatGainText:非 0 正值带 `+`、负值带 `-`、0 不带符号;仅用于非编辑态显示, 进入编辑的初始文本仍为无符号纯数值。 3. 新增 FormatOffsetTimeText:`Δt:` 后按正负带符号(Δt:+5s 60ms / Δt:-5s 60ms / Δt:0s), 打开面板 / CMD_OFFSET_TIME 提交 / CMD_OFFSET_RESET 三处统一走它。 二、边沿统计:按帧预算 + 每状态公平配额 (用户报告:按住横向光标不动时统计停在“统计中…”不更新;改中心轴参数回车后不动鼠标时停在部分值) 根因:预算按 GetTickCount64() 分桶,而系统时钟粒度约 15.6ms;RequestEdgeScanFrame 的 PostMessage 自持重绘把帧率拉高 ⇒ 同一 tick 内连画多帧却完全不推进。 1. Paint 开头 ++m_edgeScanFrame; m_edgeScanBudget = kEdgeScanPairsPerFrame;(按帧重置), 删除 m_edgeScanTick。 2. 每个状态各自持有每帧配额(pumpedFrame/pumpedPairs):perState = max(60000, 1.2M/share), share = max(kEdgeScanFairShare=6, m_edgeScans.size())。 注意:不能直接用 size() 分摊 —— 状态在同一帧内逐个创建,第一个那一刻 size()==1 会独占整额, 同帧后面新建的状态全部被截断为 0 配额(即“只有第一条信号推进”)。 实测(合成 5 信号 × 200 万采样、同一网格):首帧 5 个状态全部推进;9 帧内全部扫完(期间不注入 任何鼠标消息);数值与独立参考实现逐位一致;阈值变化后就地重置、随后“停住不动”仍精确扫完。 三、浮动面板排版 1. 信号名宽度上限 kEdgePanelNameMaxWidth=150 逻辑像素:宽度计算与绘制都用 min(实测, 上限) + UiEllipsize(300 字符名字只比单字符宽 44px,旧实现要多 5100px)。 2. 统计信息两列等宽(新增 EdgeRisingText/EdgeFallingText/EdgeCountsPending):面板宽度按 2×max(两列文本宽)+kEdgePanelStatColumnGap 预留,绘制时两列各占 (可用宽−间距)/2 ⇒ 多条信号的 “上升沿/下降沿”列起点固定、严格垂直对齐。 3. 未完成时 rising 与 falling 两个数字后面都带 `…`(旧实现只在整行末尾追加一次, 视觉上只跟在“下降沿”的数字后面)。 四、弱峰调整面板 1. 每行滑块右侧新增复位控件(复用还原图标绘制 PaintPanelResetIcon):悬停高亮 + IDC_HAND + 一次性提示;点击恢复幅度 0 / 迭代 1 且 maxIterations 回到初始上限 10。 EdgePanelLayout 新增 rowLevelResets/rowIterResets(并进 PanelSlotKey),两行按钮共用同一 X。 2. 迭代次数上限输入框的空内容提示改为“输入上限(当前N)”。 3. 滑块标签去掉多余的 %/N(kWeakPeakLevelLabel=调整幅度;新增 kWeakPeakIterationsSliderLabel= 迭代次数;MEAN / CENTER 输入框的“迭代次数 N”保持不变)。 五、验证(全部离屏;进程/线程 BELOW_NORMAL、窗口 SW_HIDE、不注入真实鼠标键盘) - 新增 .preflight/perf/panelcheck_main.cpp:46/46 通过(符号规则走真实菜单路径;按帧推进 / 不饿死 / 精确性 / 自持驱动;两列等宽预留(含未完成时两处 …);名字上限; 弱峰复位控件位置 / 对齐 / 点击恢复默认值与上限)。 - 既有 verify_main.cpp:47/47(另 2 条为本机 .preflight 未随远端同步的旧规则断言)。 - make release 零警告。 规则同步:Documents/agent_mem.md 新增 §9.26,§11.2 新增第 43–48 条硬约束,变更日志补一行。
This commit is contained in:
1 parent
65f7625dce
commit
fba40ee1a2
4 files changed
+564
-80
No files matched your search
@@ -1234,6 +1234,14 @@ namespace jdi
|
||||
std::vector<gd::RectF> rowLevelValues;
|
||||
std::vector<gd::RectF> rowIterTracks;
|
||||
std::vector<gd::RectF> rowIterValues;
|
||||
/**
|
||||
* @brief 「弱峰调整」每行滑块右侧的**复位按钮**矩形(与 `rowLevelTracks` 同序)。
|
||||
*
|
||||
* 点击把该行参数恢复为默认值(幅度 0 / 迭代次数 1 且上限回到初始上限)。
|
||||
* 未勾选或非 PEAK 面板时对应元素为空矩形(数组也可能为空)。
|
||||
*/
|
||||
std::vector<gd::RectF> rowLevelResets;
|
||||
std::vector<gd::RectF> rowIterResets;
|
||||
std::vector<SignalRef> rowRefs;
|
||||
};
|
||||
|
||||
@@ -1663,9 +1671,11 @@ namespace jdi
|
||||
*
|
||||
* ⚠️ **本函数绝不长时间阻塞 UI 线程**(千万级信号整段扫描实测 ≈115ms/条,
|
||||
* 横向光标每一帧都会换阈值 → 拖动时每帧都要扫,5 条信号就是 ≈575ms/帧):
|
||||
* 扫描按「逐块累积」进行,每个 tick(约一帧)只消耗 `kEdgeScanPairsPerTick`
|
||||
* 个采样对的预算,未扫完时 `complete` 返回 false(界面显示上一个完整结果 + “…”)。
|
||||
* 拖动横向光标(`EDGE_MOVE`)期间阈值每帧都变,推进等于白做功 → 不推进。
|
||||
* 扫描按「逐块累积」进行,**每帧**只消耗 `kEdgeScanPairsPerFrame` 个采样对的预算
|
||||
*(且**每个状态各自持有配额**,多条信号不会互相饿死),未扫完时 `complete` 返回 false
|
||||
*(界面显示上一个完整结果 + “…”)。
|
||||
* 阈值变化(横向光标拖动中)就地把累积状态重置(不新建条目),且当帧不展示部分值
|
||||
*(`justReset`),避免“错误且不更新的数字”。
|
||||
*
|
||||
* @param [IN] file 信号所属文件。
|
||||
* @param [IN] signal 目标信号。
|
||||
@@ -1871,6 +1881,8 @@ namespace jdi
|
||||
* 因此必须单独记住这个状态才能触发重绘与取消高亮。
|
||||
*/
|
||||
int m_hoverBadge = -1;
|
||||
/** 「弱峰调整」每行滑块右侧的复位控件是否处于悬停态(用于“进入/离开”时只提示一次)。 */
|
||||
bool m_peakResetHover = false;
|
||||
int m_mouseX = 0; /**< 最近一次鼠标横坐标(用于悬停提示定位)。 */
|
||||
int m_mouseY = 0; /**< 最近一次鼠标纵坐标(用于悬停提示定位)。 */
|
||||
bool m_spanHover = false; /**< 鼠标是否悬停在双光标差值标签上。 */
|
||||
@@ -1915,6 +1927,15 @@ namespace jdi
|
||||
std::size_t falling = 0; /**< 已扫部分的下降沿总数。 */
|
||||
bool complete = false; /**< 整条信号是否已扫完。 */
|
||||
bool justReset = false; /**< 本 tick 刚因阈值/参数变化重新开始(不展示部分值)。 */
|
||||
/**
|
||||
* @brief 本状态**上一次推进所在的帧序号**与当时已消耗的配额。
|
||||
*
|
||||
* 每个状态**每帧各自持有配额**(而不是所有状态共享一个总额):旧实现下推进总是
|
||||
* 从列表第一个状态扫起,5 条信号同一网格时只有第一条有进展、其余长期显示“统计中…”
|
||||
*(用户报告“很多项都停在了统计中不更新”)。
|
||||
*/
|
||||
std::uint64_t pumpedFrame = 0; /**< 最近一次推进的帧序号(与 `m_edgeScanFrame` 比较)。 */
|
||||
std::size_t pumpedPairs = 0; /**< 本帧已消耗的采样对配额(新帧开始时重置)。 */
|
||||
/**
|
||||
* @brief 中心轴扫描**就地计算**的一小段均值滤波曲线(源下标空间)。
|
||||
*
|
||||
@@ -1932,8 +1953,15 @@ namespace jdi
|
||||
bool localValid = false; /**< 本地区间是否可用。 */
|
||||
};
|
||||
mutable std::vector<EdgeScanState> m_edgeScans; /**< 边沿统计累积状态(纯缓存)。 */
|
||||
mutable std::size_t m_edgeScanBudget = 0; /**< 本 tick 剩余可扫描的采样对数量(限制单帧总开销)。 */
|
||||
mutable std::uint64_t m_edgeScanTick = 0; /**< 最近一次重置扫描预算的 tick。 */
|
||||
mutable std::size_t m_edgeScanBudget = 0; /**< 本帧剩余可扫描的采样对数量(跨状态共享,限制单帧总开销)。 */
|
||||
/**
|
||||
* @brief 帧序号(`Paint` 每帧递增)。
|
||||
*
|
||||
* ⚠️ 预算必须**按帧**重置,不能按 `GetTickCount64()` 分桶:系统时钟粒度约 15.6ms,
|
||||
* 帧率一高(`PostMessage` 自持重绘会把它拉高)就会在同一 tick 内连画多帧而不推进,
|
||||
* 用户看到的就是“按住光标不动时统计卡住、要动鼠标才更新”。
|
||||
*/
|
||||
mutable std::uint64_t m_edgeScanFrame = 0;
|
||||
/** 诊断计数:中心轴本地曲线计算失败次数(内存不足等)。0 = 一切正常。 */
|
||||
mutable std::size_t m_edgeScanLocalCurveFails = 0;
|
||||
|
||||
|
||||
@@ -73,7 +73,56 @@ namespace jdi
|
||||
return gd::RectF(left, top, 0.0f, 0.0f);
|
||||
}
|
||||
return gd::RectF(left, top, right - left, bottom - top);
|
||||
} const float kCursorSnapMinPixels = 6.0f; /**< 光标吸附采样点的最小采样间隔(逻辑像素)。 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 绘制「还原/重置」图标(逆时针回环箭头)。
|
||||
*
|
||||
* 与 `PopupMenu` 里「系数 / 偏移」二级面板标题栏右侧的还原图标**同一套笔画**
|
||||
*(用户要求:“可以直接挪用信号『调整』功能中『系数/偏移』二级菜单内的复位控件”),
|
||||
* 因此弱峰调整面板的复位控件与它视觉完全一致。不依赖字体或资源,任意 DPI 都清晰。
|
||||
*
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] rect 图标外接矩形(正方形)。
|
||||
* @param [IN] color 笔画颜色(0xAARRGGBB)。
|
||||
* @param [IN] scale DPI 缩放比例。
|
||||
*/
|
||||
void PaintPanelResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color,
|
||||
float scale)
|
||||
{
|
||||
const float inset = 2.5f * scale;
|
||||
gd::RectF box(rect.X + inset, rect.Y + inset, rect.Width - inset * 2.0f,
|
||||
rect.Height - inset * 2.0f);
|
||||
if (box.Width <= 1.0f || box.Height <= 1.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
gd::Pen pen(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu, (color >> 8) & 0xFFu,
|
||||
color & 0xFFu),
|
||||
std::max(1.0f, 1.5f * scale));
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
// 缺口留在右上角(从 300° 到 20° 处断开),与常见的"重置/刷新"图标一致。
|
||||
g.DrawArc(&pen, box.X, box.Y, box.Width, box.Height, 300.0f, 300.0f);
|
||||
|
||||
// 箭头:位于缺口末端(约 300° 方向),指向前进方向。
|
||||
const float cx = box.X + box.Width * 0.5f;
|
||||
const float cy = box.Y + box.Height * 0.5f;
|
||||
const float radius = box.Width * 0.5f;
|
||||
const float angle = 300.0f * 3.14159265f / 180.0f;
|
||||
const float tipX = cx + std::cos(angle) * radius;
|
||||
const float tipY = cy + std::sin(angle) * radius;
|
||||
const float size = std::max(2.5f, 3.2f * scale);
|
||||
gd::PointF arrow[3];
|
||||
arrow[0] = gd::PointF(tipX, tipY);
|
||||
arrow[1] = gd::PointF(tipX - size, tipY - size * 0.25f);
|
||||
arrow[2] = gd::PointF(tipX - size * 0.25f, tipY - size);
|
||||
gd::SolidBrush brush(gd::Color((color >> 24) & 0xFFu, (color >> 16) & 0xFFu,
|
||||
(color >> 8) & 0xFFu, color & 0xFFu));
|
||||
g.FillPolygon(&brush, arrow, 3);
|
||||
}
|
||||
|
||||
const float kCursorSnapMinPixels = 6.0f; /**< 光标吸附采样点的最小采样间隔(逻辑像素)。 */
|
||||
const float kLaneLabelPad = 6.0f; /**< 网格内侧标尺带的内边距。 */
|
||||
const float kLegendHeight = 19.0f; /**< 图例条目高度(逻辑像素)。 */
|
||||
const float kLegendPad = 5.0f; /**< 图例相对网格左上角的内缩。 */
|
||||
@@ -87,6 +136,8 @@ namespace jdi
|
||||
const float kEdgePanelTitleHeight = 26.0f; /**< 面板标题栏高度。 */
|
||||
const float kEdgePanelRowHeight = 18.0f; /**< 面板内单行文本高度。 */
|
||||
const float kEdgePanelPad = 7.0f; /**< 面板内边距。 */
|
||||
const float kEdgePanelNameMaxWidth = 150.0f; /**< 面板内信号名的宽度上限(超过则省略;避免超长信号名把面板顶得很长)。 */
|
||||
const float kEdgePanelStatColumnGap = 14.0f; /**< 「上升沿 / 下降沿」两列之间的间距(保证多信号的文字垂直对齐)。 */
|
||||
const float kEdgePanelEditFieldWidth = 64.0f; /**< 数值框的最小宽度。 */
|
||||
const float kEdgePanelButtonHeight = 30.0f; /**< 均值滤波面板底部按钮行高度。 */
|
||||
const float kEdgePanelMeanRowHeight = 22.0f; /**< 逐条目面板中每条信号的名称行高度。 */
|
||||
@@ -96,6 +147,15 @@ namespace jdi
|
||||
const float kWeakPeakSliderRowHeight = 26.0f; /**< 弱峰调整面板中每个滑块行的高度。 */
|
||||
const float kWeakPeakTrackHeight = 6.0f; /**< 弱峰调整滑块的轨道高度。 */
|
||||
const float kWeakPeakTrackMinWidth = 120.0f; /**< 弱峰调整滑块轨道的最小宽度。 */
|
||||
/**
|
||||
* @brief 「弱峰调整」滑块行右侧**复位控件**的边长(逻辑像素)。
|
||||
*
|
||||
* 每行滑块都有一个复位按钮(与「系数/偏移」二级面板里的「还原」图标同一套绘制),
|
||||
* 点击把该行的参数恢复为默认值(幅度 0 / 迭代次数 1 且上限回到初始上限)。
|
||||
*/
|
||||
const float kWeakPeakResetButtonSize = 16.0f;
|
||||
/** 复位控件与其左侧数值列之间的间距(逻辑像素)。 */
|
||||
const float kWeakPeakResetGap = 4.0f;
|
||||
const int kWeakPeakIterationsMin = 1; /**< 迭代次数 N 的下限。 */
|
||||
/** 迭代次数滑块「自定义上限」的允许范围(与算法内部的阶数夹取保持一致)。 */
|
||||
const int kWeakPeakIterationsMaxLimit = 1000;
|
||||
@@ -144,8 +204,10 @@ namespace jdi
|
||||
* 整条信号的重算本来就追不上滑块速度,"冻结"与"每帧重排"观感相同,但后者拖慢前台。
|
||||
*/
|
||||
const std::size_t kHugeSignalPreviewSamples = 4000000ull;
|
||||
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"; /**< MEAN / CENTER 面板里「迭代次数」输入框的标签。 */
|
||||
/** 「弱峰调整」迭代次数滑块行的标签(用户要求:滑动条前不再单独显示 `N`)。 */
|
||||
const wchar_t* const kWeakPeakIterationsSliderLabel = L"迭代次数";
|
||||
const wchar_t* const kWeakPeakTitle = L"弱峰调整"; /**< 弱峰调整面板标题。 */
|
||||
const wchar_t* const kBuildSignalLabel = L"构建为新信号"; /**< 逐条目面板底部按钮文案(均值滤波 / 弱峰调整共用)。 */
|
||||
/**
|
||||
@@ -167,27 +229,43 @@ namespace jdi
|
||||
*/
|
||||
const std::size_t kEdgeBlockSamples = 8192;
|
||||
/**
|
||||
* @brief 每个 tick(约一帧)允许「边沿计数」扫描的采样对总数。
|
||||
* @brief 每一帧允许「边沿计数」扫描的采样对总数(跨全部累积状态共享)。
|
||||
*
|
||||
* 千万级信号整段扫描一次 ≈115ms(用户报告:5 条信号同一网格时单帧 ≈575ms,
|
||||
* 界面完全失去响应)。改成预算推进后,单帧最多只花 ≈5ms 在统计上,
|
||||
* 剩下的部分在后续帧里接着扫(未扫完时界面显示上一个完整结果 + “…”)。
|
||||
*
|
||||
* ⚠️ **必须按帧重置**(在 `Paint` 开头):旧实现按 `GetTickCount64()` 分桶,
|
||||
* 而系统时钟粒度约 15.6ms —— 当帧率高于它时(`PostMessage` 自持重绘会把帧率拉高),
|
||||
* 同一 tick 内的多帧**完全不推进**(用户报告:“按住横向光标不动时统计卡在统计中”,
|
||||
* “回车确认参数后不移动鼠标就一直停在部分值”)。
|
||||
*/
|
||||
const std::size_t kEdgeScanPairsPerTick = 1200000;
|
||||
const std::size_t kEdgeScanPairsPerFrame = 1200000;
|
||||
/**
|
||||
* @brief 「边沿计数」结果的显示文本。
|
||||
* @param [IN] rising 上升沿数量。 @param [IN] falling 下降沿数量。
|
||||
* @param [IN] complete 是否为该阈值的完整精确值。
|
||||
* @return 面板/宽度测量共用的文本(未算完时带省略号,完全未开始时显示“统计中…”)。
|
||||
* @brief 每个累积状态每帧的扫描配额下限(防止多个状态时前面的把预算吃光)。
|
||||
*
|
||||
* 过去所有状态共享一个“本 tick 总预算”,而推进总是从列表中的第一个状态开始
|
||||
* —— 5 条信号时只有第一条有进展,其余 4 条长期显示“统计中…”(用户报告
|
||||
* “很多项都停在了统计中不更新”)。现在每个状态各自持有每帧配额,互不饿死。
|
||||
*/
|
||||
std::wstring EdgeCountsText(std::size_t rising, std::size_t falling, bool complete)
|
||||
const std::size_t kEdgeScanPairsPerStateMin = 60000;
|
||||
/**
|
||||
* @brief 计算每状态配额时假定的“最小并发状态数”。
|
||||
*
|
||||
* ⚠️ 不能直接用 `m_edgeScans.size()`:状态是在**同一帧内逐个创建**的,第一个状态的
|
||||
* 那一刻 `size() == 1`,会拿走整额预算 → 同帧后面新建的状态全部被截断为 0 配额
|
||||
*(首帧只有第一条信号推进)。按一个典型并发量(普通 + 中心轴 × 网格内信号数)
|
||||
* 下限分摊,可保证同一帧里所有状态都得到推进。
|
||||
*/
|
||||
const std::size_t kEdgeScanFairShare = 6;
|
||||
/**
|
||||
* @brief 「边沿计数」结果里的**上升沿**部分文本。
|
||||
* @param [IN] rising 上升沿数量。 @param [IN] complete 是否为完整精确值。
|
||||
* @return 形如 "上升沿 123"(未算完时追加 " …")的文本。
|
||||
*/
|
||||
std::wstring EdgeRisingText(std::size_t rising, bool complete)
|
||||
{
|
||||
if (!complete && rising == 0 && falling == 0)
|
||||
{
|
||||
return L"统计中…";
|
||||
}
|
||||
std::wstring text = L"上升沿 " + std::to_wstring(rising) + L" 下降沿 " +
|
||||
std::to_wstring(falling);
|
||||
std::wstring text = L"上升沿 " + std::to_wstring(rising);
|
||||
if (!complete)
|
||||
{
|
||||
text += L" …";
|
||||
@@ -195,6 +273,48 @@ namespace jdi
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「边沿计数」结果里的**下降沿**部分文本。
|
||||
* @param [IN] falling 下降沿数量。 @param [IN] complete 是否为完整精确值。
|
||||
* @return 形如 "下降沿 456"(未算完时追加 " …")的文本。
|
||||
*/
|
||||
std::wstring EdgeFallingText(std::size_t falling, bool complete)
|
||||
{
|
||||
std::wstring text = L"下降沿 " + std::to_wstring(falling);
|
||||
if (!complete)
|
||||
{
|
||||
text += L" …";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「边沿计数」是否处于“完全未算出”状态(用于显示“统计中…”)。
|
||||
*/
|
||||
bool EdgeCountsPending(std::size_t rising, std::size_t falling, bool complete)
|
||||
{
|
||||
return (!complete && rising == 0 && falling == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「边沿计数」结果的显示文本(合并成一行,用于宽度测量与兼容旧调用)。
|
||||
*
|
||||
* ⚠️ 未完成时**两个数字后面都要带 `…`**(旧实现只在整行末尾追加一个 `…`,
|
||||
* 视觉上只跟在“下降沿”的数字后面,用户明确要求“为上升沿也加上这个尾缀”)。
|
||||
* @param [IN] rising 上升沿数量。 @param [IN] falling 下降沿数量。
|
||||
* @param [IN] complete 是否为该阈值的完整精确值。
|
||||
* @return 面板/宽度测量共用的文本。
|
||||
*/
|
||||
[[maybe_unused]] std::wstring EdgeCountsText(std::size_t rising, std::size_t falling,
|
||||
bool complete)
|
||||
{
|
||||
if (EdgeCountsPending(rising, falling, complete))
|
||||
{
|
||||
return L"统计中…";
|
||||
}
|
||||
return EdgeRisingText(rising, complete) + L" " + EdgeFallingText(falling, complete);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否命中「弱峰调整」的某条滑块轨道。
|
||||
*
|
||||
@@ -237,7 +357,7 @@ namespace jdi
|
||||
float WeakPeakLabelColumnWidth(HFONT uiSmall)
|
||||
{
|
||||
return std::max(UiMeasureTextWidth(uiSmall, kWeakPeakLevelLabel),
|
||||
UiMeasureTextWidth(uiSmall, kWeakPeakIterationsLabel));
|
||||
UiMeasureTextWidth(uiSmall, kWeakPeakIterationsSliderLabel));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1391,7 +1511,6 @@ namespace jdi
|
||||
// 数据层结构变化后边沿统计与均值滤波曲线的派生结果都会失效。
|
||||
m_edgeScans.clear();
|
||||
m_edgeScanBudget = 0;
|
||||
m_edgeScanTick = 0;
|
||||
m_meanCurveCache.clear();
|
||||
m_weakPeakCurveCache.clear();
|
||||
// 信号下标可能已经整体位移:清掉按引用缓存的悬停/拖拽目标,避免指向错误的信号。
|
||||
@@ -3238,19 +3357,28 @@ namespace jdi
|
||||
return std::isfinite(drawnCenter) ? (drawn - drawnCenter) : nan;
|
||||
};
|
||||
|
||||
// ---- 推进(受每 tick 预算限制)------------------------------------------
|
||||
// ---- 推进(受「每帧总量 + 每状态配额」双重限制)--------------------------
|
||||
// ⚠️ 预算在 `Paint` 开头**按帧**重置(不再按 `GetTickCount64()` 分桶):系统时钟
|
||||
// 粒度约 15.6ms,帧率一高就会在同一 tick 内连画多帧而完全不推进 —— 用户报告的
|
||||
// “按住横向光标不动时统计卡在统计中”“回车确认参数后不移动鼠标就一直停在部分值”
|
||||
// 就是这个原因。
|
||||
// ⚠️ 同时**每个状态各自持有配额**:否则推进总从列表第一个状态扫起,多条信号时
|
||||
// 只有第一条有进展、其余长期显示“统计中…”(用户报告“很多项都停在统计中不更新”)。
|
||||
const bool suppressPartial = state->justReset && !state->complete;
|
||||
if (!state->complete && !state->blockRising.empty())
|
||||
{
|
||||
// 扫描预算按 tick 共享:同一帧里本函数会被调用多次(面板宽度测量 + 绘制 + 多条信号),
|
||||
// 若每次都各扫一份,单帧总开销就会翻好几倍(实测整段 25.8M 采样 ≈115ms)。
|
||||
const std::uint64_t tick = GetTickCount64();
|
||||
if (tick != m_edgeScanTick)
|
||||
const std::size_t activeStates = std::max<std::size_t>(1, m_edgeScans.size());
|
||||
const std::size_t share = std::max<std::size_t>(kEdgeScanFairShare, activeStates);
|
||||
const std::size_t perState =
|
||||
std::max<std::size_t>(kEdgeScanPairsPerStateMin, kEdgeScanPairsPerFrame / share);
|
||||
if (state->pumpedFrame != m_edgeScanFrame)
|
||||
{
|
||||
m_edgeScanTick = tick;
|
||||
m_edgeScanBudget = kEdgeScanPairsPerTick;
|
||||
state->pumpedFrame = m_edgeScanFrame;
|
||||
state->pumpedPairs = 0;
|
||||
}
|
||||
if (m_edgeScanBudget > 0)
|
||||
std::size_t quota = (perState > state->pumpedPairs) ? (perState - state->pumpedPairs) : 0;
|
||||
quota = std::min(quota, m_edgeScanBudget);
|
||||
if (quota > 0)
|
||||
{
|
||||
state->justReset = false;
|
||||
// 快路径:时间单调、无水平镜像、无「调整」变换、非中心轴 ⇒ 直接比较采样值与阈值。
|
||||
@@ -3260,12 +3388,13 @@ namespace jdi
|
||||
std::size_t scanned = 0;
|
||||
std::size_t pair = state->scannedPair;
|
||||
std::size_t block = (pair > 0) ? ((pair - 1) / kEdgeBlockSamples) : 0;
|
||||
while (pair < count && scanned < m_edgeScanBudget)
|
||||
while (pair < count && scanned < quota && m_edgeScanBudget > 0)
|
||||
{
|
||||
const std::size_t blockFirst = block * kEdgeBlockSamples + 1;
|
||||
const std::size_t blockLast = std::min(count - 1, (block + 1) * kEdgeBlockSamples);
|
||||
const std::size_t from = std::max(pair, blockFirst);
|
||||
const std::size_t to = std::min(blockLast, from + (m_edgeScanBudget - scanned) - 1);
|
||||
const std::size_t avail = std::min(quota - scanned, m_edgeScanBudget);
|
||||
const std::size_t to = std::min(blockLast, from + avail - 1);
|
||||
if (to < from)
|
||||
{
|
||||
break;
|
||||
@@ -3340,6 +3469,7 @@ namespace jdi
|
||||
state->localEnd = 0;
|
||||
state->localValid = false;
|
||||
}
|
||||
state->pumpedPairs += scanned;
|
||||
m_edgeScanBudget -= std::min(m_edgeScanBudget, scanned);
|
||||
}
|
||||
}
|
||||
@@ -3454,7 +3584,12 @@ namespace jdi
|
||||
kEdgePanelTitleHeight * m_scale;
|
||||
|
||||
// 数据行 = 名称与“上升沿 N / 下降沿 M”两行,取较宽者。
|
||||
// 用 UiMeasureTextWidth(无省略号)测量,保证数字完整显示。
|
||||
// ⚠️ 名称宽度有上限(`kEdgePanelNameMaxWidth`):超长信号名不再把面板顶得很长,
|
||||
// 绘制时超出部分用省略号(用户明确要求“超过某个长度就省略名称显示”)。
|
||||
// ⚠️ 统计信息按「上升沿 / 下降沿」**两列**排版(列起点固定)→ 多条信号时文字垂直对齐。
|
||||
float nameColumnWidth = 0.0f;
|
||||
float statRisingWidth = 0.0f;
|
||||
float statFallingWidth = 0.0f;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
@@ -3462,7 +3597,10 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float nameWidth = UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->Title());
|
||||
const float nameWidth =
|
||||
std::min(UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->Title()),
|
||||
kEdgePanelNameMaxWidth * m_scale);
|
||||
nameColumnWidth = std::max(nameColumnWidth, nameWidth);
|
||||
if (itemized)
|
||||
{
|
||||
// 逐条目面板:第一排 = 名称 + 复选框;第三排(中心轴)= 统计结果。
|
||||
@@ -3492,11 +3630,25 @@ namespace jdi
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, lane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling, complete);
|
||||
}
|
||||
// 宽度按实际要显示的文本测量(含未算完时的“统计中…”/省略号),
|
||||
// 否则推进过程中数字变长会被截断(统计数字必须完整显示)。
|
||||
const float countWidth =
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, EdgeCountsText(rising, falling, complete));
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + std::max(nameWidth, countWidth));
|
||||
// 宽度按实际要显示的文本测量(含未算完时的“…”),否则推进过程中数字变长会被截断。
|
||||
// 完全未算出时整行显示“统计中…”,因此两列都要预留它的宽度。
|
||||
const float pendingWidth = UiMeasureTextWidth(m_fonts->monoSmall, L"统计中…");
|
||||
statRisingWidth = std::max(
|
||||
statRisingWidth,
|
||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, EdgeRisingText(rising, complete)),
|
||||
pendingWidth));
|
||||
statFallingWidth =
|
||||
std::max(statFallingWidth,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, EdgeFallingText(falling, complete)));
|
||||
}
|
||||
if (statRisingWidth > 0.0f || statFallingWidth > 0.0f)
|
||||
{
|
||||
// 两列**等宽**(取两列最大值中的较大者)→ 绘制时两列起点固定,
|
||||
// 多条信号的“上升沿 / 下降沿”文字严格垂直对齐(用户明确要求)。
|
||||
const float maxColumn = std::max(statRisingWidth, statFallingWidth);
|
||||
const float countsWidth = maxColumn * 2.0f + kEdgePanelStatColumnGap * m_scale;
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale +
|
||||
std::max(nameColumnWidth, countsWidth));
|
||||
}
|
||||
|
||||
if (centerAxis || meanFilter)
|
||||
@@ -3515,10 +3667,11 @@ namespace jdi
|
||||
|
||||
if (weakPeak)
|
||||
{
|
||||
// 滑块行 = 「标签列 + 轨道(最小宽度)+ 数值列」(等级与迭代次数两行共用同一列宽)。
|
||||
// 滑块行 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(等级与迭代次数两行共用同一列宽)。
|
||||
const float rowWidth = WeakPeakLabelColumnWidth(m_fonts->uiSmall) + 6.0f * m_scale +
|
||||
kWeakPeakTrackMinWidth * m_scale + 6.0f * m_scale +
|
||||
WeakPeakValueColumnWidth(m_fonts->monoSmall);
|
||||
WeakPeakValueColumnWidth(m_fonts->monoSmall) +
|
||||
kWeakPeakResetGap * m_scale + kWeakPeakResetButtonSize * m_scale;
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
||||
}
|
||||
|
||||
@@ -4616,6 +4769,21 @@ namespace jdi
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
// 两行滑块右侧的**复位控件**:与其它可点击图标一致,用手型光标提示可点击。
|
||||
if (rowIndex < layout.rowLevelResets.size() &&
|
||||
layout.rowLevelResets[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowLevelResets[rowIndex], static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (rowIndex < layout.rowIterResets.size() &&
|
||||
layout.rowIterResets[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowIterResets[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)))
|
||||
@@ -5029,10 +5197,43 @@ namespace jdi
|
||||
// 按**层序**取最上层的面板(与点击命中保持一致,重叠时以最上层为准)。
|
||||
bool overEdgePanel = false;
|
||||
int panelLane = -1;
|
||||
int hoverPanelKind = 0;
|
||||
{
|
||||
int hoverKind = 0;
|
||||
overEdgePanel = TopPanelAtPoint(static_cast<float>(x), static_cast<float>(y), panelLane,
|
||||
hoverKind);
|
||||
hoverPanelKind);
|
||||
}
|
||||
|
||||
// 「弱峰调整」每行滑块右侧的**复位控件**:悬停时给出一次性提示(进入时设置、离开时清除)。
|
||||
// 与「系数/偏移」二级面板标题栏的还原图标同一交互语义(悬停高亮 + 手型 + 提示)。
|
||||
bool overPeakReset = false;
|
||||
if (overEdgePanel && panelLane >= 0 && m_tab != nullptr &&
|
||||
static_cast<EdgePanelKind>(hoverPanelKind) == EdgePanelKind::PEAK)
|
||||
{
|
||||
const EdgePanelLayout resetLayout = EdgePanelLayoutFor(panelLane, EdgePanelKind::PEAK);
|
||||
for (std::size_t rowIndex = 0; rowIndex < resetLayout.rowLevelResets.size(); ++rowIndex)
|
||||
{
|
||||
const gd::RectF& level = resetLayout.rowLevelResets[rowIndex];
|
||||
if (level.Width > 0.0f && UiPointInRect(level, fx, static_cast<float>(y)))
|
||||
{
|
||||
overPeakReset = true;
|
||||
break;
|
||||
}
|
||||
if (rowIndex < resetLayout.rowIterResets.size())
|
||||
{
|
||||
const gd::RectF& iter = resetLayout.rowIterResets[rowIndex];
|
||||
if (iter.Width > 0.0f && UiPointInRect(iter, fx, static_cast<float>(y)))
|
||||
{
|
||||
overPeakReset = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (overPeakReset != m_peakResetHover)
|
||||
{
|
||||
m_peakResetHover = overPeakReset;
|
||||
SetHint(overPeakReset ? std::wstring(L"点击恢复该参数的默认值") : std::wstring());
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 悬停网格:鼠标在面板上时高亮面板所属网格(用户明确要求)。
|
||||
@@ -5320,6 +5521,28 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
Lane::WeakPeakItem& item = tabLane.EnsureWeakPeakItem(ref);
|
||||
// 两行滑块右侧的**复位控件**:点击恢复该行参数的默认值
|
||||
//(幅度 → 0;迭代次数 → 1 且上限回到初始上限 `kWeakPeakIterationsDefaultMax`)。
|
||||
// 与「系数/偏移」二级面板里的还原图标同一交互语义(面板保持打开、就地回显)。
|
||||
if (rowIndex < layout.rowLevelResets.size() &&
|
||||
layout.rowLevelResets[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowLevelResets[rowIndex], fx, fy))
|
||||
{
|
||||
item.amountPercent = 0;
|
||||
m_weakPeakCurveCache.clear();
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
if (rowIndex < layout.rowIterResets.size() &&
|
||||
layout.rowIterResets[rowIndex].Width > 0.0f &&
|
||||
UiPointInRect(layout.rowIterResets[rowIndex], fx, fy))
|
||||
{
|
||||
item.iterations = kWeakPeakIterationsMin;
|
||||
item.maxIterations = kWeakPeakIterationsDefaultMax;
|
||||
m_weakPeakCurveCache.clear();
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 迭代次数行右侧的**数值文本**:点击后整行变成输入框以修改滑块上限。
|
||||
// ⚠️ 必须放在轨道命中之前(数值区在轨道右侧,命中区不重叠,但顺序更安全)。
|
||||
if (layout.rowIterValues[rowIndex].Width > 0.0f &&
|
||||
@@ -6382,6 +6605,13 @@ namespace jdi
|
||||
|
||||
BuildLaneVisuals();
|
||||
|
||||
// 「边沿计数」的扫描预算**按帧重置**(见 `m_edgeScanFrame` 的说明):
|
||||
// 旧实现按 `GetTickCount64()` 分桶,帧率一高(`PostMessage` 自持重绘会把它拉高)
|
||||
// 就会在同一系统 tick 内连画多帧而**完全不推进**统计 —— 用户报告
|
||||
// “按住横向光标不动时统计卡住不更新”“回车确认参数后不移动鼠标就停在部分值”。
|
||||
++m_edgeScanFrame;
|
||||
m_edgeScanBudget = kEdgeScanPairsPerFrame;
|
||||
|
||||
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
|
||||
// 超出内存预算的旧条目。此刻没有任何条目被钉住,回收是安全的。
|
||||
m_curvePins.clear();
|
||||
@@ -10799,11 +11029,15 @@ namespace jdi
|
||||
// 滑块行列宽(与 `EdgePanelWidth` 使用同一组算式,避免布局与宽度计算漂移)。
|
||||
const float weakPeakLabelWidth = WeakPeakLabelColumnWidth(m_fonts->uiSmall);
|
||||
const float weakPeakValueWidth = WeakPeakValueColumnWidth(m_fonts->monoSmall);
|
||||
const float weakPeakResetSize = kWeakPeakResetButtonSize * m_scale;
|
||||
const float weakPeakResetGap = kWeakPeakResetGap * m_scale;
|
||||
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);
|
||||
std::max(10.0f * m_scale,
|
||||
(left + width - pad - weakPeakValueWidth - weakPeakResetGap -
|
||||
weakPeakResetSize - 6.0f * m_scale) -
|
||||
weakPeakTrackLeft);
|
||||
float rowY = top + titleHeight + pad;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
@@ -10843,13 +11077,24 @@ namespace jdi
|
||||
layout.rowIterValues.push_back(gd::RectF(
|
||||
iterTrack.X + iterTrack.Width + 6.0f * m_scale, iterRowTop,
|
||||
weakPeakValueWidth, weakPeakSliderRow));
|
||||
// 每行滑块右侧的**复位按钮**(与数值列同一右边界,保证两行上下对齐)。
|
||||
const float resetX = levelTrack.X + levelTrack.Width + 6.0f * m_scale +
|
||||
weakPeakValueWidth + weakPeakResetGap;
|
||||
layout.rowLevelResets.push_back(
|
||||
gd::RectF(resetX, levelRowTop + (weakPeakSliderRow - weakPeakResetSize) * 0.5f,
|
||||
weakPeakResetSize, weakPeakResetSize));
|
||||
layout.rowIterResets.push_back(
|
||||
gd::RectF(resetX, iterRowTop + (weakPeakSliderRow - weakPeakResetSize) * 0.5f,
|
||||
weakPeakResetSize, weakPeakResetSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
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.rowPoints.push_back(gd::RectF());
|
||||
layout.rowIterations.push_back(gd::RectF());
|
||||
@@ -11585,6 +11830,15 @@ namespace jdi
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
// 弱峰调整每行滑块右侧的复位按钮(悬停高亮依赖它的精确矩形)。
|
||||
for (const gd::RectF& rect : layout.rowLevelResets)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowIterResets)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
|
||||
// 鼠标落在面板内时,✕ / 复选框 / 滑块 / 底部按钮的悬停态都依赖精确坐标。
|
||||
const float margin = 6.0f * m_scale;
|
||||
@@ -11709,6 +11963,27 @@ namespace jdi
|
||||
// 面板是**静态**内容:只有被拖动 / 悬停 / 参数变化的那一个真正重画,
|
||||
// 其余直接贴上一帧缓存好的像素(15 个面板全画一遍实测约 33ms,
|
||||
// 而贴图 <1ms —— 拖动弱峰调整滑块时的主要卡顿就来自这一层)。
|
||||
// 「上升沿 / 下降沿」两列排版(列起点固定 ⇒ 多条信号的文字严格垂直对齐)。
|
||||
// ⚠️ 未完成时**两个数字后面都带 `…`**(旧实现只在整行末尾追加一次,
|
||||
// 视觉上只跟在“下降沿”的数字后面;用户明确要求为“上升沿”也补上这个尾缀)。
|
||||
const auto paintCounts = [this, &g](const gd::RectF& rect, std::size_t rising,
|
||||
std::size_t falling, bool complete)
|
||||
{
|
||||
if (EdgeCountsPending(rising, falling, complete))
|
||||
{
|
||||
UiText(g, L"统计中…", m_fonts->monoSmall, rect, m_theme->textMuted, TextAlign::LEFT);
|
||||
return;
|
||||
}
|
||||
const float gap = kEdgePanelStatColumnGap * m_scale;
|
||||
const float columnWidth = std::max(10.0f, (rect.Width - gap) * 0.5f);
|
||||
UiText(g, EdgeRisingText(rising, complete), m_fonts->monoSmall,
|
||||
gd::RectF(rect.X, rect.Y, columnWidth, rect.Height), m_theme->textMuted,
|
||||
TextAlign::LEFT);
|
||||
UiText(g, EdgeFallingText(falling, complete), m_fonts->monoSmall,
|
||||
gd::RectF(rect.X + columnWidth + gap, rect.Y, columnWidth, rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
};
|
||||
|
||||
std::vector<PanelSlot> nextSlots;
|
||||
std::vector<gd::RectF> dirtyRects;
|
||||
const std::uint64_t panelGlobalKey = PanelGlobalKey();
|
||||
@@ -11873,9 +12148,10 @@ namespace jdi
|
||||
: (weakPeak ? tabLane.WeakPeakEnabled(ref)
|
||||
: tabLane.EdgeCenterEnabled(ref));
|
||||
|
||||
// 名称:第一排左侧,避开右侧复选框。
|
||||
// 名称:第一排左侧,避开右侧复选框(超长时省略)。
|
||||
const float nameWidth =
|
||||
std::max(20.0f, checkRect.X - (panel.X + pad) - 6.0f * m_scale);
|
||||
std::min(std::max(20.0f, checkRect.X - (panel.X + pad) - 6.0f * m_scale),
|
||||
kEdgePanelNameMaxWidth * m_scale);
|
||||
UiText(g, UiEllipsize(resolved.data->Title(), m_fonts->uiSmall, nameWidth),
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(panel.X + pad, checkRect.Y - 3.0f * m_scale, nameWidth,
|
||||
@@ -11930,11 +12206,34 @@ namespace jdi
|
||||
track.Height + 16.0f * m_scale),
|
||||
m_theme->textStrong, TextAlign::LEFT);
|
||||
};
|
||||
// 每行滑块右侧的**复位控件**(与「系数/偏移」二级面板里的还原图标同一套绘制):
|
||||
// 悬停时高亮 + 手型光标,点击把该行参数恢复为默认值(幅度 0 / 迭代 1 且上限回初始值)。
|
||||
const auto paintSliderReset = [&](const gd::RectF& rect)
|
||||
{
|
||||
if (rect.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const bool hot =
|
||||
(m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||
UiPointInRect(rect, static_cast<float>(m_mouseX),
|
||||
static_cast<float>(m_mouseY));
|
||||
if (hot)
|
||||
{
|
||||
UiFillRoundRect(
|
||||
g, gd::RectF(rect.X - 1.0f * m_scale, rect.Y - 1.0f * m_scale,
|
||||
rect.Width + 2.0f * m_scale,
|
||||
rect.Height + 2.0f * m_scale),
|
||||
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
|
||||
}
|
||||
PaintPanelResetIcon(g, rect,
|
||||
hot ? m_theme->accent : m_theme->textMuted, m_scale);
|
||||
};
|
||||
|
||||
const gd::RectF& levelTrack = layout.rowLevelTracks[rowIndex];
|
||||
const gd::RectF& iterTrack = layout.rowIterTracks[rowIndex];
|
||||
paintSliderLabel(levelTrack, kWeakPeakLevelLabel);
|
||||
paintSliderLabel(iterTrack, kWeakPeakIterationsLabel);
|
||||
paintSliderLabel(iterTrack, kWeakPeakIterationsSliderLabel);
|
||||
|
||||
const bool levelActive = (m_dragMode == PlotDragMode::PEAK_LEVEL) &&
|
||||
(m_peakDragLane == static_cast<int>(index)) &&
|
||||
@@ -11950,6 +12249,7 @@ namespace jdi
|
||||
(peakItem->amountPercent > 0 ? L"+" : L"") +
|
||||
std::to_wstring(peakItem->amountPercent) + L"%",
|
||||
levelActive);
|
||||
paintSliderReset(layout.rowLevelResets[rowIndex]);
|
||||
|
||||
// 迭代次数滑块:量程 = 1 … 该信号自己的上限。
|
||||
const int peakMax = std::max(kWeakPeakIterationsMin,
|
||||
@@ -11972,6 +12272,7 @@ namespace jdi
|
||||
{
|
||||
paintSliderBody(iterTrack, iterRatio, std::to_wstring(peakIter),
|
||||
iterActive);
|
||||
paintSliderReset(layout.rowIterResets[rowIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12042,10 +12343,8 @@ namespace jdi
|
||||
statsPoints, statsOrder, EdgeRangeActive(),
|
||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling,
|
||||
statsComplete);
|
||||
const std::wstring counts =
|
||||
EdgeCountsText(rising, falling, statsComplete);
|
||||
UiText(g, counts, m_fonts->monoSmall, statsRect, m_theme->textMuted,
|
||||
TextAlign::LEFT);
|
||||
// 两列排版(列起点固定)⇒ 多条信号的“上升沿 / 下降沿”垂直对齐。
|
||||
paintCounts(statsRect, rising, falling, statsComplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12065,10 +12364,12 @@ namespace jdi
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const float textWidth = panel.Width - pad * 2.0f;
|
||||
// 名称与统计数字一律完整绘制(不省略):面板宽度已按文本实测宽度确定。
|
||||
UiText(g, resolved.data->Title(), m_fonts->uiSmall,
|
||||
gd::RectF(panel.X + pad, rowY, textWidth, rowHeight), signalColor,
|
||||
TextAlign::LEFT);
|
||||
// 名称:超长时省略(面板宽度被 `kEdgePanelNameMaxWidth` 限制,不会把面板顶得很长)。
|
||||
UiText(g,
|
||||
UiEllipsize(resolved.data->Title(), m_fonts->uiSmall,
|
||||
std::min(textWidth, kEdgePanelNameMaxWidth * m_scale)),
|
||||
m_fonts->uiSmall, gd::RectF(panel.X + pad, rowY, textWidth, rowHeight),
|
||||
signalColor, TextAlign::LEFT);
|
||||
|
||||
std::size_t rising = 0;
|
||||
std::size_t falling = 0;
|
||||
@@ -12076,10 +12377,8 @@ namespace jdi
|
||||
EdgeCountsFor(*resolved.file, *resolved.data, *resolved.style, tabLane.edgeValue,
|
||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling,
|
||||
complete);
|
||||
const std::wstring counts = EdgeCountsText(rising, falling, complete);
|
||||
UiText(g, counts, m_fonts->monoSmall,
|
||||
gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
paintCounts(gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight), rising,
|
||||
falling, complete);
|
||||
rowY += rowHeight * 2.0f;
|
||||
}
|
||||
|
||||
@@ -12155,14 +12454,13 @@ namespace jdi
|
||||
// 编辑态:强调色外框 + 选区高亮 + 插入符,首次键入即整体替换(与常规输入框一致)。
|
||||
UiDrawEditField(g, fieldRect, m_edit, m_fonts->monoSmall, m_scale);
|
||||
|
||||
// 「迭代次数 N 上限」:输入框**以空内容开始**,未输入任何内容时以灰色提示当前上限值
|
||||
//(用户明确要求:"在未输入任何内容时,输入框内提示当前上限值")。
|
||||
// 提示文本比插入符稍微右移几像素,避免插入符压在上面看不清数字。
|
||||
// 「迭代次数 N 上限」:输入框**以空内容开始**,未输入任何内容时以灰色提示当前上限值。
|
||||
// 提示文案 = “输入上限(当前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 std::wstring hint = L"输入上限(当前" + std::to_wstring(currentMax) + L")";
|
||||
const float hintLeft = fieldRect.X + 11.0f * m_scale;
|
||||
UiTextAtClipped(g, hint, m_fonts->monoSmall, hintLeft,
|
||||
gd::RectF(hintLeft, fieldRect.Y,
|
||||
|
||||
@@ -193,24 +193,65 @@ namespace jdi
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 带正负符号的数值文本:正数也带 `+`(非编辑态显示偏移量时使用)。
|
||||
* @brief 带正负符号的数值文本(非编辑态显示偏移量时使用)。
|
||||
*
|
||||
* 规则(用户明确要求):**正数带 `+`、负数带 `-`、0 不带任何符号**。
|
||||
* (旧实现 0 也会显示 `+0.00`,用户报告“偏移调整的 Y 轴输入框默认值为 0 时也显示了正号,这不正确”。)
|
||||
* @param [IN] value 数值。
|
||||
* @return 形如 `+1.50` / `-0.25` / `+0.00` 的文本。
|
||||
* @return 形如 `+1.50` / `-0.25` / `0.00` 的文本。
|
||||
*/
|
||||
std::wstring FormatSignedNumber(double value)
|
||||
{
|
||||
const std::wstring text = FormatNumberText(std::fabs(value));
|
||||
return ((value < 0.0) ? L"-" : L"+") + text;
|
||||
if (value > 0.0)
|
||||
{
|
||||
return L"+" + text;
|
||||
}
|
||||
if (value < 0.0)
|
||||
{
|
||||
return L"-" + text;
|
||||
}
|
||||
return text; // 0:不带符号
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 把系数格式化为界面文本(与偏移量同一套两位小数规则)。
|
||||
* @brief 把系数格式化为界面文本(与偏移量同一套两位小数规则 + 显式正号)。
|
||||
*
|
||||
* 规则(用户明确要求):“系数调整”输入框**非编辑态**显示时,非 0 正值前面加 `+`,
|
||||
* 负值按原样显示负号,0 不带符号。编辑态仍用不带符号的纯数值(见 `FormatNumberText`)。
|
||||
* @param [IN] gain 系数。
|
||||
* @return 显示文本。
|
||||
*/
|
||||
std::wstring FormatGainText(float gain)
|
||||
{
|
||||
return FormatNumberText(static_cast<double>(gain));
|
||||
const double value = static_cast<double>(gain);
|
||||
if (value > 0.0)
|
||||
{
|
||||
return L"+" + FormatNumberText(value);
|
||||
}
|
||||
return FormatNumberText(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「时间轴」偏移的非编辑态显示文本(`Δt:` + 带正负号的复合单位间隔)。
|
||||
*
|
||||
* 规则(用户明确要求):在 `Δt:` 冒号后的时间显示值**前面加上正负号**,
|
||||
* 只有值为 0 时不显示符号。示例:`Δt:+1h 1min 2s` / `Δt:-5s 60ms` / `Δt:0s`。
|
||||
*(`FormatDurationCompound` 内部取绝对值,因此符号由这里补上。)
|
||||
* @param [IN] seconds 时间偏移(秒,可为负)。
|
||||
* @return 显示文本。
|
||||
*/
|
||||
std::wstring FormatOffsetTimeText(double seconds)
|
||||
{
|
||||
if (seconds > 0.0)
|
||||
{
|
||||
return L"Δt:+" + FormatDurationCompound(seconds);
|
||||
}
|
||||
if (seconds < 0.0)
|
||||
{
|
||||
return L"Δt:-" + FormatDurationCompound(seconds);
|
||||
}
|
||||
return L"Δt:" + FormatDurationCompound(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2012,13 +2053,15 @@ namespace jdi
|
||||
gainHeader.id = CMD_GAIN_RESET; // 点击标题栏右侧的还原图标 → 系数重置为 1
|
||||
items.push_back(gainHeader);
|
||||
|
||||
// 非编辑态带正号(用户明确要求);进入编辑后的初始文本用不带符号的纯数值。
|
||||
const std::wstring gainText = FormatGainText(style->gain);
|
||||
const std::wstring gainEditText = FormatNumberText(static_cast<double>(style->gain));
|
||||
MenuItem gainEdit;
|
||||
gainEdit.kind = MenuItemKind::EDIT;
|
||||
gainEdit.label.clear(); // 不显示说明文字:输入框独占整行
|
||||
gainEdit.id = CMD_GAIN_EDIT;
|
||||
gainEdit.value = gainText;
|
||||
gainEdit.editValue = gainText;
|
||||
gainEdit.editValue = gainEditText;
|
||||
gainEdit.filter = EditFilter::NUMBER;
|
||||
items.push_back(gainEdit);
|
||||
|
||||
@@ -2039,13 +2082,13 @@ namespace jdi
|
||||
items.push_back(offsetY);
|
||||
|
||||
// 「时间轴」:把采样沿时间轴平移(画在 t + offsetTime)。
|
||||
// 非编辑态显示与双光标差值同一套复合单位文本;编辑态显示以秒为单位的数值,
|
||||
// 并在输入框最右侧提示单位 `s`。
|
||||
// 非编辑态显示与双光标差值同一套复合单位文本,并**带正负号**(用户明确要求);
|
||||
// 编辑态显示以秒为单位的数值,并在输入框最右侧提示单位 `s`。
|
||||
MenuItem offsetTime;
|
||||
offsetTime.kind = MenuItemKind::EDIT;
|
||||
offsetTime.label = L"时间轴";
|
||||
offsetTime.id = CMD_OFFSET_TIME;
|
||||
offsetTime.value = L"Δt:" + FormatDurationCompound(style->offsetTime);
|
||||
offsetTime.value = FormatOffsetTimeText(style->offsetTime);
|
||||
offsetTime.editValue = FormatNumberText(style->offsetTime);
|
||||
offsetTime.editSuffix = L"s";
|
||||
offsetTime.filter = EditFilter::NUMBER;
|
||||
@@ -2907,7 +2950,8 @@ namespace jdi
|
||||
}
|
||||
if (m_menu != nullptr)
|
||||
{
|
||||
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain), FormatGainText(style->gain));
|
||||
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain),
|
||||
FormatNumberText(static_cast<double>(style->gain)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2936,7 +2980,7 @@ namespace jdi
|
||||
if (m_menu != nullptr)
|
||||
{
|
||||
m_menu->UpdateItemValue(CMD_OFFSET_TIME,
|
||||
L"Δt:" + FormatDurationCompound(style->offsetTime),
|
||||
FormatOffsetTimeText(style->offsetTime),
|
||||
FormatNumberText(style->offsetTime));
|
||||
}
|
||||
return false;
|
||||
@@ -2959,7 +3003,8 @@ namespace jdi
|
||||
style->gain = 1.0f;
|
||||
if (m_menu != nullptr)
|
||||
{
|
||||
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain), FormatGainText(style->gain));
|
||||
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain),
|
||||
FormatNumberText(static_cast<double>(style->gain)));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -2974,9 +3019,10 @@ namespace jdi
|
||||
style->offsetTime = 0.0;
|
||||
if (m_menu != nullptr)
|
||||
{
|
||||
m_menu->UpdateItemValue(CMD_OFFSET_Y, FormatSignedNumber(0.0), L"0.00");
|
||||
m_menu->UpdateItemValue(CMD_OFFSET_TIME, L"Δt:" + FormatDurationCompound(0.0),
|
||||
L"0.00");
|
||||
m_menu->UpdateItemValue(CMD_OFFSET_Y, FormatSignedNumber(0.0),
|
||||
FormatNumberText(0.0));
|
||||
m_menu->UpdateItemValue(CMD_OFFSET_TIME, FormatOffsetTimeText(0.0),
|
||||
FormatNumberText(0.0));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user