288 信号卡顿根治 + 中心轴面板重做 + 标签页默认状态
一、卡顿真因(实测 29.8ms/帧 → 6.6ms/帧) 1. 中心轴面板高度没有上限:288 个信号时面板高 9996px(绘图区只有 1141px) ⇒ PanelRectInsideLayer 恒为假 ⇒ 面板像素缓存一次都存不进去 ⇒ 每帧全量重画。 2. PanelGlobalKey 混入鼠标驱动状态(m_hoverSignal / m_hoverLane / m_hoverCursor / CursorTime / EdgeRangeMin/Max)⇒ 鼠标一动键就变 ⇒ 所有面板每帧重画; PanelSlotKey 里还会为每个信号调一次 EdgeCenterCountsFor(单帧 288 次查询)。 3. 缓存命中路径的投影(与面板同大的半透明抗锯齿圆角矩形)每帧重画,GDI+ 实测 > 20ms。 修法: * 新增 PlotView::InputIsInteractive()(拖拽中 / 鼠标 140ms 内移动过)。交互期间冻结 全部鼠标驱动项与统计数字、跳过投影、统计预算降到 8 万(静止时 40 万)。 m_meanRowHoverLane/Ref 例外 —— 它直接改面板像素且只在进出某行时变化。 * kEdgeScanPairsPerFrame 由 1,200,000 降为 400,000。 * 中心轴面板重做为可滚动列表 ⇒ 面板高度被绘图区夹住、缓存可用、只画可见行。 二、「中心轴边沿计数」面板重做(与「均值滤波」同风格) 条目栏为信号色叠底的可折叠栏 + 右端复选框;栏下同一行显示「上升沿 / 下降沿」; 再下面两行滑块(均值点数 / 迭代次数,标签不带 X/N),数值可点击输入上限、 右侧复位按钮;整个面板可滚动;未勾选时条目栏不可展开。 新增 Lane::edgeCenterScroll + SetLaneEdgeCenterScroll;交互复用 MEAN_* 拖拽模式 + m_meanDragCenter / m_editMeanCenter 标志分派;光标形状与均值滤波一致。 三、标签页 * 唯一标签页点 ✕ 不再弹警告:直接 erase 后 NewTab(true),名字必然为「视图 1」。 * 新建标签页(含手动新建)默认:组全部折起、无选中信号(清空全局 Selected)、 样式区无目标。 四、验证 .preflight/bugcheck.exe 59/59(A1 空闲 0.7ms、A2 平移 2.5ms、A3 双光标 2.4ms、 A4 自由标记 5.2ms、A5 中心轴拖动 6.6ms;B3 真实 WM_TIMER+WM_PAINT 路径; B5 快速连点与动画反向;C 中心轴面板交互;D 标签页默认状态) panelcheck 71/71、markercheck 13/13、ringcheck 6/6 make release / make debug 零 warning
This commit is contained in:
1 parent
8271879ede
commit
944c074ab3
11 files changed
+618
-122
No files matched your search
@@ -22,6 +22,12 @@ namespace jdi
|
|||||||
tab.SetAllCollapsed(true);
|
tab.SetAllCollapsed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⚠️ **新标签页不存在任何被点选的信号**(用户 2026-11-02 要求):
|
||||||
|
// 选中状态是**全局共享**的(`DataSet::Selected()`),不清掉的话新建出来的标签页会
|
||||||
|
// 直接带着上一个标签页的选中信号 —— 样式编辑区于是有自己的“目标”,与新页“空白”
|
||||||
|
// 的预期不符。清空后样式区回到「未选择信号」状态,直到用户手动点选任意信号。
|
||||||
|
m_dataSet.SetSelected(SignalRef());
|
||||||
|
|
||||||
m_tabs.push_back(std::move(tab));
|
m_tabs.push_back(std::move(tab));
|
||||||
SetActiveTab(static_cast<int>(m_tabs.size()) - 1);
|
SetActiveTab(static_cast<int>(m_tabs.size()) - 1);
|
||||||
}
|
}
|
||||||
@@ -34,7 +40,14 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
if (m_tabs.size() <= 1)
|
if (m_tabs.size() <= 1)
|
||||||
{
|
{
|
||||||
SetStatusMessage(L"至少需要保留一个标签页。", true);
|
// ⚠️ **唯一标签页不弹警告**(用户 2026-11-02 要求):直接删掉它并新建一个默认的
|
||||||
|
// 「视图 1」—— 组全部折起、无选中信号、样式区无目标(由 `NewTab(true)` 保证)。
|
||||||
|
// ⚠️ `m_tabs` 清空后 `NextTabName()` 必然返回「视图 1」,编号自然从 1 重新开始。
|
||||||
|
// erase 与 `NewTab` 之间**不要**碰 `m_panel` / `m_plot`(它们的上下文仍指向
|
||||||
|
// 刚被释放的 `TabConfig`);`NewTab` 里的 `SetActiveTab` 会第一时间重新绑定。
|
||||||
|
m_tabs.erase(m_tabs.begin() + index);
|
||||||
|
m_activeTab = 0;
|
||||||
|
NewTab(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1097,6 +1097,12 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool SetLaneMeanFilterScroll(int laneIndex, float scroll);
|
bool SetLaneMeanFilterScroll(int laneIndex, float scroll);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置某网格「中心轴边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
* @return 偏移真的发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool SetLaneEdgeCenterScroll(int laneIndex, float scroll);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
||||||
* @return 切换后为「已收起」返回 true。
|
* @return 切换后为「已收起」返回 true。
|
||||||
@@ -2807,12 +2813,28 @@ namespace jdi
|
|||||||
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
|
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
|
||||||
int m_meanDragLane = -1; /**< 正在拖动「均值滤波」参数滑块的面板所属网格。 */
|
int m_meanDragLane = -1; /**< 正在拖动「均值滤波」参数滑块的面板所属网格。 */
|
||||||
SignalRef m_meanDragRef; /**< 正在拖动「均值滤波」参数滑块的信号(每信号一套参数)。 */
|
SignalRef m_meanDragRef; /**< 正在拖动「均值滤波」参数滑块的信号(每信号一套参数)。 */
|
||||||
|
/**
|
||||||
|
* @brief 当前参数滑块拖拽的目标是**中心轴边沿计数**面板(而不是均值滤波)。
|
||||||
|
*
|
||||||
|
* 「中心轴边沿计数」自 2026-11-02 起与「均值滤波」共用同一套可滚动列表与滑块交互,
|
||||||
|
* 因此复用 `MEAN_POINTS` / `MEAN_ITERATIONS` / `MEAN_SCROLL` 三个拖拽模式;
|
||||||
|
* 这个标志说明它们的写入目标是 `Lane::edgeCenterItems` 还是 `Lane::meanFilterItems`。
|
||||||
|
*/
|
||||||
|
bool m_meanDragCenter = false;
|
||||||
/**
|
/**
|
||||||
* @brief 正在编辑「均值滤波」某个滑块上限时,输入框内灰色提示用的**当前上限值**。
|
* @brief 正在编辑「均值滤波」某个滑块上限时,输入框内灰色提示用的**当前上限值**。
|
||||||
*
|
*
|
||||||
* 与 `m_peakEditCurrentMax` 同一语义(输入框以空内容开始,空内容时显示当前上限)。
|
* 与 `m_peakEditCurrentMax` 同一语义(输入框以空内容开始,空内容时显示当前上限)。
|
||||||
*/
|
*/
|
||||||
int m_meanEditCurrentMax = 0;
|
int m_meanEditCurrentMax = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 正在编辑「滑块上限」的是**中心轴边沿计数**面板(而不是均值滤波)。
|
||||||
|
*
|
||||||
|
* 与 `m_meanDragCenter` 同一思路:两者共用 `MEAN_POINTS_MAX` / `MEAN_ITERATIONS_MAX`
|
||||||
|
* 两个编辑目标,这个标志说明提交时要写到 `Lane::edgeCenterItems` 还是 `meanFilterItems`。
|
||||||
|
*/
|
||||||
|
bool m_editMeanCenter = false;
|
||||||
/**
|
/**
|
||||||
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
|
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
|
||||||
*
|
*
|
||||||
@@ -2867,6 +2889,32 @@ namespace jdi
|
|||||||
/** 「弱峰调整」每行滑块右侧的复位控件是否处于悬停态(用于“进入/离开”时只提示一次)。 */
|
/** 「弱峰调整」每行滑块右侧的复位控件是否处于悬停态(用于“进入/离开”时只提示一次)。 */
|
||||||
bool m_peakResetHover = false;
|
bool m_peakResetHover = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 最近一次鼠标活动(移动 / 按下 / 滚轮)的时间戳(`GetTickCount64()`)。
|
||||||
|
*
|
||||||
|
* 用途:**边沿统计的每帧预算**要区分“交互中”和“静止”两种状态(见
|
||||||
|
* `kEdgeScanPairsPerFrameInteractive`)。交互期间压小预算保住跟手;鼠标停下
|
||||||
|
* `kEdgeScanInteractiveIdleMs` 之后自动恢复高预算,把统计快速补齐。
|
||||||
|
* 初值 0 ⇒ 启动后第一帧就算作“静止”(可以全速推进统计)。
|
||||||
|
*/
|
||||||
|
std::uint64_t m_lastInputActivityMs = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 当前是否处于「交互中」(正在拖拽,或鼠标刚刚移动过)。
|
||||||
|
*
|
||||||
|
* 两个用途(都用它保证同一个判定口径):
|
||||||
|
* * `Paint` 决定边沿统计本帧的扫描预算(交互 ⇒ 小预算,保住跟手);
|
||||||
|
* * `PanelGlobalKey` / `PanelSlotKey` 决定**是否把边沿统计数字与光标时间计入面板像素缓存键**:
|
||||||
|
* 交互期间不计入 ⇒ 键保持稳定 ⇒ 面板直接复用上一帧像素、**不做**每帧数百次统计查询
|
||||||
|
* 与整块重绘。
|
||||||
|
*
|
||||||
|
* ⚠️ 用户 2026-11-02 报告:288 个信号合并到同一网格、开启中心轴边沿计数后,
|
||||||
|
* 拖动横向光标/移动鼠标每帧要 30ms。根因正是「每帧 × 每个信号 × 一次
|
||||||
|
* `EdgeCenterCountsFor` 区间汇总」被塞进了缓存键计算里,且结果每帧都让键变化
|
||||||
|
* ⇒ 面板缓存永远不命中、每帧重画整块面板。
|
||||||
|
*/
|
||||||
|
bool InputIsInteractive() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 上一帧鼠标悬停在「均值滤波 / 弱峰调整」面板的哪个**条目栏**上。
|
* @brief 上一帧鼠标悬停在「均值滤波 / 弱峰调整」面板的哪个**条目栏**上。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -625,8 +625,26 @@ namespace jdi
|
|||||||
* 而系统时钟粒度约 15.6ms —— 当帧率高于它时(`PostMessage` 自持重绘会把帧率拉高),
|
* 而系统时钟粒度约 15.6ms —— 当帧率高于它时(`PostMessage` 自持重绘会把帧率拉高),
|
||||||
* 同一 tick 内的多帧**完全不推进**(用户报告:“按住横向光标不动时统计卡在统计中”,
|
* 同一 tick 内的多帧**完全不推进**(用户报告:“按住横向光标不动时统计卡在统计中”,
|
||||||
* “回车确认参数后不移动鼠标就一直停在部分值”)。
|
* “回车确认参数后不移动鼠标就一直停在部分值”)。
|
||||||
|
*
|
||||||
|
* ⚠️⚠️ **这是「非交互帧」的预算**:用户 2026-11-02 报告——288 个信号合并到同一网格、
|
||||||
|
* 开启中心轴边沿计数后,拖动横向光标每帧要 30ms(实测),界面完全跟不上手。
|
||||||
|
* 原因就是本预算太大:中心轴每个采样对还要按“均值点数 × 迭代次数”算一次均值窗口,
|
||||||
|
* 1200000 对 × 11 次浮点运算 ≈ 3 位千万级运算/帧。
|
||||||
|
* 现在交互期间改用 `kEdgeScanPairsPerFrameInteractive`(见 `Paint` 开头)。
|
||||||
*/
|
*/
|
||||||
inline const std::size_t kEdgeScanPairsPerFrame = 1200000;
|
inline const std::size_t kEdgeScanPairsPerFrame = 400000;
|
||||||
|
/**
|
||||||
|
* @brief **交互帧**(正在拖动 / 鼠标刚移动过)允许的边沿统计预算。
|
||||||
|
*
|
||||||
|
* 交互时首要目标是跟手:把预算压到非交互帧的 1/5(实测单帧统计开销 ≈6ms 以内),
|
||||||
|
* 鼠标停下 `kEdgeScanInteractiveIdleMs` 之后自动恢复高预算、快速把统计补齐。
|
||||||
|
* ⚠️ 不能降到 0 —— 那样“按住横向光标不动”时统计会停在“统计中…”。
|
||||||
|
*/
|
||||||
|
inline const std::size_t kEdgeScanPairsPerFrameInteractive = 80000;
|
||||||
|
/**
|
||||||
|
* @brief 鼠标停止移动多久之后恢复高统计预算(毫秒)。
|
||||||
|
*/
|
||||||
|
inline const std::uint64_t kEdgeScanInteractiveIdleMs = 140;
|
||||||
/**
|
/**
|
||||||
* @brief 每个累积状态每帧的扫描配额下限(防止多个状态时前面的把预算吃光)。
|
* @brief 每个累积状态每帧的扫描配额下限(防止多个状态时前面的把预算吃光)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1414,6 +1414,8 @@ namespace jdi
|
|||||||
if (lane.EdgeCenterEnabled(ref))
|
if (lane.EdgeCenterEnabled(ref))
|
||||||
{
|
{
|
||||||
lane.RemoveEdgeCenterItem(ref);
|
lane.RemoveEdgeCenterItem(ref);
|
||||||
|
// 该信号的累积统计状态一并释放(否则会占着 `kEdgeScanStateLimit` 的名额)。
|
||||||
|
InvalidateEdgeScansFor(ResolveSignal(ref).data, true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
lane.EnsureEdgeCenterItem(ref);
|
lane.EnsureEdgeCenterItem(ref);
|
||||||
@@ -1809,6 +1811,23 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlotView::SetLaneEdgeCenterScroll(int laneIndex, float scroll)
|
||||||
|
{
|
||||||
|
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||||
|
const float clamped = std::max(0.0f, scroll);
|
||||||
|
if (std::fabs(lane.edgeCenterScroll - clamped) < 0.5f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 滚动只影响面板内部像素,缓冲区/场景都不变(面板像素缓存按键失效即可)。
|
||||||
|
lane.edgeCenterScroll = clamped;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool PlotView::ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref)
|
bool PlotView::ToggleLaneFreeMarkerGroup(int laneIndex, const SignalRef& ref)
|
||||||
{
|
{
|
||||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
|
|||||||
@@ -651,19 +651,25 @@ namespace jdi
|
|||||||
target == PlotEditTarget::MEAN_ITERATIONS_MAX)
|
target == PlotEditTarget::MEAN_ITERATIONS_MAX)
|
||||||
{
|
{
|
||||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||||
if (lane.FindMeanFilterItem(m_editMeanRef) == nullptr)
|
const Lane::MeanFilterItem* existing = m_editMeanCenter
|
||||||
|
? lane.FindEdgeCenterItem(m_editMeanRef)
|
||||||
|
: lane.FindMeanFilterItem(m_editMeanRef);
|
||||||
|
if (existing == nullptr)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (parsedOk)
|
if (parsedOk)
|
||||||
{
|
{
|
||||||
const bool isPoints = (target == PlotEditTarget::MEAN_POINTS_MAX);
|
const bool isPoints = (target == PlotEditTarget::MEAN_POINTS_MAX);
|
||||||
const int minimum = isPoints ? kMeanFilterPointsMin : kMeanFilterIterationsMin;
|
const int minimum =
|
||||||
|
isPoints ? (m_editMeanCenter ? 0 : kMeanFilterPointsMin) : kMeanFilterIterationsMin;
|
||||||
const int limit = isPoints ? kMeanFilterPointsMaxLimit : kMeanFilterIterationsMaxLimit;
|
const int limit = isPoints ? kMeanFilterPointsMaxLimit : kMeanFilterIterationsMaxLimit;
|
||||||
const long long value = static_cast<long long>(std::llround(parsed));
|
const long long value = static_cast<long long>(std::llround(parsed));
|
||||||
if (value >= minimum && value <= limit)
|
if (value >= minimum && value <= limit)
|
||||||
{
|
{
|
||||||
Lane::MeanFilterItem& item = lane.EnsureMeanFilterItem(m_editMeanRef);
|
Lane::MeanFilterItem& item = m_editMeanCenter
|
||||||
|
? lane.EnsureEdgeCenterItem(m_editMeanRef)
|
||||||
|
: lane.EnsureMeanFilterItem(m_editMeanRef);
|
||||||
if (isPoints)
|
if (isPoints)
|
||||||
{
|
{
|
||||||
item.maxPoints = static_cast<int>(value);
|
item.maxPoints = static_cast<int>(value);
|
||||||
@@ -676,6 +682,10 @@ namespace jdi
|
|||||||
std::max(minimum, std::min(item.maxIterations, item.iterations));
|
std::max(minimum, std::min(item.maxIterations, item.iterations));
|
||||||
}
|
}
|
||||||
m_meanCurveCache.clear();
|
m_meanCurveCache.clear();
|
||||||
|
if (m_editMeanCenter)
|
||||||
|
{
|
||||||
|
InvalidateEdgeScansFor(ResolveSignal(m_editMeanRef).data, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -1060,6 +1070,67 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 「均值滤波 / 中心轴边沿计数」可滚动列表:条目栏(含复选框)→ 手型;
|
||||||
|
// 滑块轨道 → 左右箭头;数值(可点击改上限)与复位按钮 → 手型;
|
||||||
|
// 内容区滚动条 → 上下箭头。与「弱峰调整」同一套观感。
|
||||||
|
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MEAN ||
|
||||||
|
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::CENTER)
|
||||||
|
{
|
||||||
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowHeaders.size(); ++rowIndex)
|
||||||
|
{
|
||||||
|
if (layout.rowHeaders[rowIndex].Width > 0.0f &&
|
||||||
|
UiPointInRect(layout.rowHeaders[rowIndex], static_cast<float>(x),
|
||||||
|
static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_HAND);
|
||||||
|
}
|
||||||
|
if (rowIndex < layout.rowLevelValues.size() &&
|
||||||
|
layout.rowLevelValues[rowIndex].Width > 0.0f &&
|
||||||
|
UiPointInRect(layout.rowLevelValues[rowIndex], static_cast<float>(x),
|
||||||
|
static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_HAND);
|
||||||
|
}
|
||||||
|
if (rowIndex < layout.rowIterValues.size() &&
|
||||||
|
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.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.rowLevelTracks.size() &&
|
||||||
|
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale,
|
||||||
|
static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_SIZEWE);
|
||||||
|
}
|
||||||
|
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 (layout.scrollThumb.Width > 0.0f &&
|
||||||
|
UiPointInRect(layout.scrollThumb, static_cast<float>(x), static_cast<float>(y)))
|
||||||
|
{
|
||||||
|
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 「弱峰调整」的逐信号滑块:轨道 → 左右箭头;迭代次数右侧数值 → 手型(可点击改上限)。
|
// 「弱峰调整」的逐信号滑块:轨道 → 左右箭头;迭代次数右侧数值 → 手型(可点击改上限)。
|
||||||
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK)
|
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK)
|
||||||
{
|
{
|
||||||
@@ -1164,11 +1235,32 @@ namespace jdi
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlotView::InputIsInteractive() const
|
||||||
|
{
|
||||||
|
if (m_dragMode != PlotDragMode::NONE)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (m_lastInputActivityMs == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::uint64_t now = GetTickCount64();
|
||||||
|
return (now >= m_lastInputActivityMs) &&
|
||||||
|
((now - m_lastInputActivityMs) < kEdgeScanInteractiveIdleMs);
|
||||||
|
}
|
||||||
|
|
||||||
bool PlotView::OnMouseMove(int x, int y)
|
bool PlotView::OnMouseMove(int x, int y)
|
||||||
{
|
{
|
||||||
const bool mouseMoved = (x != m_mouseX) || (y != m_mouseY);
|
const bool mouseMoved = (x != m_mouseX) || (y != m_mouseY);
|
||||||
m_mouseX = x;
|
m_mouseX = x;
|
||||||
m_mouseY = y;
|
m_mouseY = y;
|
||||||
|
// 交互活动时间戳:`Paint` 用它决定边沿统计本帧用「交互预算」还是「静止预算」。
|
||||||
|
// 只有真正的移动才更新 —— 悬停不动时统计应当全速推进。
|
||||||
|
if (mouseMoved)
|
||||||
|
{
|
||||||
|
m_lastInputActivityMs = GetTickCount64();
|
||||||
|
}
|
||||||
|
|
||||||
if (!HasDrawableContent())
|
if (!HasDrawableContent())
|
||||||
{
|
{
|
||||||
@@ -1441,21 +1533,26 @@ namespace jdi
|
|||||||
case PlotDragMode::MEAN_POINTS:
|
case PlotDragMode::MEAN_POINTS:
|
||||||
case PlotDragMode::MEAN_ITERATIONS:
|
case PlotDragMode::MEAN_ITERATIONS:
|
||||||
{
|
{
|
||||||
// 「均值滤波」某信号的参数滑块:量程 = 下限 … 该信号自己的上限。
|
// 「均值滤波 / 中心轴边沿计数」某信号的参数滑块:量程 = 下限 … 该信号自己的上限。
|
||||||
if (m_tab == nullptr || m_meanDragLane < 0 ||
|
if (m_tab == nullptr || m_meanDragLane < 0 ||
|
||||||
m_meanDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
m_meanDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_meanDragLane)];
|
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(m_meanDragLane)];
|
||||||
if (lane.FindMeanFilterItem(m_meanDragRef) == nullptr)
|
const Lane::MeanFilterItem* existingItem = m_meanDragCenter
|
||||||
|
? lane.FindEdgeCenterItem(m_meanDragRef)
|
||||||
|
: lane.FindMeanFilterItem(m_meanDragRef);
|
||||||
|
if (existingItem == nullptr)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Lane::MeanFilterItem& item = lane.EnsureMeanFilterItem(m_meanDragRef);
|
Lane::MeanFilterItem& item = m_meanDragCenter
|
||||||
|
? lane.EnsureEdgeCenterItem(m_meanDragRef)
|
||||||
|
: lane.EnsureMeanFilterItem(m_meanDragRef);
|
||||||
const bool pointsMode = (m_dragMode == PlotDragMode::MEAN_POINTS);
|
const bool pointsMode = (m_dragMode == PlotDragMode::MEAN_POINTS);
|
||||||
const EdgePanelLayout layout =
|
const EdgePanelLayout layout = EdgePanelLayoutFor(
|
||||||
EdgePanelLayoutFor(m_meanDragLane, EdgePanelKind::MEAN);
|
m_meanDragLane, m_meanDragCenter ? EdgePanelKind::CENTER : EdgePanelKind::MEAN);
|
||||||
int rowIndex = -1;
|
int rowIndex = -1;
|
||||||
for (std::size_t candidate = 0; candidate < layout.rowRefs.size(); ++candidate)
|
for (std::size_t candidate = 0; candidate < layout.rowRefs.size(); ++candidate)
|
||||||
{
|
{
|
||||||
@@ -1480,13 +1577,15 @@ namespace jdi
|
|||||||
UiSliderRatioFromX(tracks[static_cast<std::size_t>(rowIndex)], fx);
|
UiSliderRatioFromX(tracks[static_cast<std::size_t>(rowIndex)], fx);
|
||||||
if (pointsMode)
|
if (pointsMode)
|
||||||
{
|
{
|
||||||
const int maximum = std::max(
|
// ⚠️ 下限:均值滤波 = 1(用户 2026-10-30 要求不能为 0);中心轴 = 0(0 = 不平滑)。
|
||||||
kMeanFilterPointsMin, std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
const int floorValue = m_meanDragCenter ? 0 : kMeanFilterPointsMin;
|
||||||
item.points = std::max(
|
const int maximum =
|
||||||
kMeanFilterPointsMin,
|
std::max(floorValue, std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
||||||
std::min(maximum,
|
item.points =
|
||||||
kMeanFilterPointsMin +
|
std::max(floorValue,
|
||||||
static_cast<int>(std::lround(ratio * (maximum - kMeanFilterPointsMin)))));
|
std::min(maximum,
|
||||||
|
floorValue +
|
||||||
|
static_cast<int>(std::lround(ratio * (maximum - floorValue)))));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1501,6 +1600,11 @@ namespace jdi
|
|||||||
ratio * (maximum - kMeanFilterIterationsMin)))));
|
ratio * (maximum - kMeanFilterIterationsMin)))));
|
||||||
}
|
}
|
||||||
m_meanCurveCache.clear();
|
m_meanCurveCache.clear();
|
||||||
|
// 中心轴统计依赖参数 ⇒ 参数一变必须定向作废该信号的累积状态。
|
||||||
|
if (m_meanDragCenter)
|
||||||
|
{
|
||||||
|
InvalidateEdgeScansFor(ResolveSignal(m_meanDragRef).data, true);
|
||||||
|
}
|
||||||
m_dragMoved = true;
|
m_dragMoved = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1639,14 +1743,14 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
case PlotDragMode::MEAN_SCROLL:
|
case PlotDragMode::MEAN_SCROLL:
|
||||||
{
|
{
|
||||||
// 「均值滤波」面板内容滚动条:按滑块在轨道里的位置换算滚动偏移。
|
// 「均值滤波 / 中心轴边沿计数」面板内容滚动条:按滑块在轨道里的位置换算滚动偏移。
|
||||||
if (m_tab == nullptr || m_markerScrollDragLane < 0 ||
|
if (m_tab == nullptr || m_markerScrollDragLane < 0 ||
|
||||||
m_markerScrollDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
m_markerScrollDragLane >= static_cast<int>(m_tab->Lanes().size()))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const EdgePanelLayout layout =
|
const EdgePanelLayout layout = EdgePanelLayoutFor(
|
||||||
EdgePanelLayoutFor(m_markerScrollDragLane, EdgePanelKind::MEAN);
|
m_markerScrollDragLane, m_meanDragCenter ? EdgePanelKind::CENTER : EdgePanelKind::MEAN);
|
||||||
const float travel = layout.scrollTrack.Height - layout.scrollThumb.Height;
|
const float travel = layout.scrollTrack.Height - layout.scrollThumb.Height;
|
||||||
const float maxScroll =
|
const float maxScroll =
|
||||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||||
@@ -1659,7 +1763,11 @@ namespace jdi
|
|||||||
std::min(static_cast<float>(y) - m_markerScrollGrabOffset,
|
std::min(static_cast<float>(y) - m_markerScrollGrabOffset,
|
||||||
layout.scrollTrack.Y + travel));
|
layout.scrollTrack.Y + travel));
|
||||||
const float ratio = (thumbTop - layout.scrollTrack.Y) / travel;
|
const float ratio = (thumbTop - layout.scrollTrack.Y) / travel;
|
||||||
if (SetLaneMeanFilterScroll(m_markerScrollDragLane, ratio * maxScroll))
|
const bool scrolled =
|
||||||
|
m_meanDragCenter
|
||||||
|
? SetLaneEdgeCenterScroll(m_markerScrollDragLane, ratio * maxScroll)
|
||||||
|
: SetLaneMeanFilterScroll(m_markerScrollDragLane, ratio * maxScroll);
|
||||||
|
if (scrolled)
|
||||||
{
|
{
|
||||||
m_dragMoved = true;
|
m_dragMoved = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -1827,17 +1935,18 @@ namespace jdi
|
|||||||
hoverPanelKind);
|
hoverPanelKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 「均值滤波 / 弱峰调整」面板**条目栏**的悬停 → 高亮该信号的预览曲线
|
// 「均值滤波 / 弱峰调整 / 中心轴边沿计数」面板**条目栏**的悬停 → 高亮该条目
|
||||||
//(用户 2026-10-30 要求:"鼠标处于条目栏上方时,对应的预览曲线需要高亮提示")。
|
//(用户 2026-10-30 要求:"鼠标处于条目栏上方时,对应的预览曲线需要高亮提示";
|
||||||
|
// 中心轴边沿计数自 2026-11-02 起同样有条目栏,条目本身也会叠一层强调色)。
|
||||||
{
|
{
|
||||||
int hoverLane = -1;
|
int hoverLane = -1;
|
||||||
SignalRef hoverRef;
|
SignalRef hoverRef;
|
||||||
|
const EdgePanelKind hoverKind = static_cast<EdgePanelKind>(hoverPanelKind);
|
||||||
if (overEdgePanel && m_tab != nullptr && panelLane >= 0 &&
|
if (overEdgePanel && m_tab != nullptr && panelLane >= 0 &&
|
||||||
(static_cast<EdgePanelKind>(hoverPanelKind) == EdgePanelKind::MEAN ||
|
(hoverKind == EdgePanelKind::MEAN || hoverKind == EdgePanelKind::PEAK ||
|
||||||
static_cast<EdgePanelKind>(hoverPanelKind) == EdgePanelKind::PEAK))
|
hoverKind == EdgePanelKind::CENTER))
|
||||||
{
|
{
|
||||||
const EdgePanelLayout layout =
|
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoverKind);
|
||||||
EdgePanelLayoutFor(panelLane, static_cast<EdgePanelKind>(hoverPanelKind));
|
|
||||||
if (UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)))
|
if (UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)))
|
||||||
{
|
{
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||||
@@ -2236,14 +2345,29 @@ namespace jdi
|
|||||||
m_tab != nullptr)
|
m_tab != nullptr)
|
||||||
{
|
{
|
||||||
const bool meanMode = (kind == EdgePanelKind::MEAN);
|
const bool meanMode = (kind == EdgePanelKind::MEAN);
|
||||||
|
const bool centerMode = (kind == EdgePanelKind::CENTER);
|
||||||
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
||||||
|
// 「均值滤波」与「中心轴边沿计数」自 2026-11-02 起共用同一套**可滚动列表**交互
|
||||||
|
//(信号色叠底条目栏 + 右侧复选框 + 展开后的两行滑块)。
|
||||||
|
const bool listMode = (meanMode || centerMode);
|
||||||
// 「均值滤波」:组头三角(展开 / 收起参数行)+ 两行参数滑块
|
// 「均值滤波」:组头三角(展开 / 收起参数行)+ 两行参数滑块
|
||||||
//(轨道拖动 / 点击数值改上限 / 右侧复位按钮)。
|
//(轨道拖动 / 点击数值改上限 / 右侧复位按钮)。
|
||||||
// ⚠️ 所有命中都要求落在**内容区**之内:滚动到可视范围之外的行仍保留矩形,
|
// ⚠️ 所有命中都要求落在**内容区**之内:滚动到可视范围之外的行仍保留矩形,
|
||||||
// 若不加这条限制,它们会抢走底部「构建为新信号」按钮的点击。
|
// 若不加这条限制,它们会抢走底部「构建为新信号」按钮的点击。
|
||||||
if (meanMode)
|
if (listMode)
|
||||||
{
|
{
|
||||||
Lane& meanLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
Lane& meanLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
||||||
|
// 本面板的条目查找 / 创建(两者共用 `Lane::MeanFilterItem`)。
|
||||||
|
const auto findItem = [&meanLane, centerMode](const SignalRef& ref)
|
||||||
|
-> const Lane::MeanFilterItem* {
|
||||||
|
return centerMode ? meanLane.FindEdgeCenterItem(ref)
|
||||||
|
: meanLane.FindMeanFilterItem(ref);
|
||||||
|
};
|
||||||
|
const auto ensureItem = [&meanLane, centerMode](const SignalRef& ref)
|
||||||
|
-> Lane::MeanFilterItem& {
|
||||||
|
return centerMode ? meanLane.EnsureEdgeCenterItem(ref)
|
||||||
|
: meanLane.EnsureMeanFilterItem(ref);
|
||||||
|
};
|
||||||
// 内容区右侧的滚动条滑块:按下即进入拖动(与「自由标记」面板同一交互)。
|
// 内容区右侧的滚动条滑块:按下即进入拖动(与「自由标记」面板同一交互)。
|
||||||
if (layout.scrollThumb.Width > 0.0f &&
|
if (layout.scrollThumb.Width > 0.0f &&
|
||||||
UiPointInRect(layout.scrollThumb, fx, fy))
|
UiPointInRect(layout.scrollThumb, fx, fy))
|
||||||
@@ -2251,6 +2375,7 @@ namespace jdi
|
|||||||
m_dragMode = PlotDragMode::MEAN_SCROLL;
|
m_dragMode = PlotDragMode::MEAN_SCROLL;
|
||||||
m_markerScrollDragLane = index;
|
m_markerScrollDragLane = index;
|
||||||
m_markerScrollGrabOffset = fy - layout.scrollThumb.Y;
|
m_markerScrollGrabOffset = fy - layout.scrollThumb.Y;
|
||||||
|
m_meanDragCenter = centerMode;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const bool insideContent =
|
const bool insideContent =
|
||||||
@@ -2267,10 +2392,10 @@ namespace jdi
|
|||||||
const bool hasParams =
|
const bool hasParams =
|
||||||
rowIndex < layout.rowLevelTracks.size() &&
|
rowIndex < layout.rowLevelTracks.size() &&
|
||||||
layout.rowLevelTracks[rowIndex].Width > 0.0f &&
|
layout.rowLevelTracks[rowIndex].Width > 0.0f &&
|
||||||
meanLane.FindMeanFilterItem(ref) != nullptr;
|
findItem(ref) != nullptr;
|
||||||
if (hasParams)
|
if (hasParams)
|
||||||
{
|
{
|
||||||
Lane::MeanFilterItem& item = meanLane.EnsureMeanFilterItem(ref);
|
Lane::MeanFilterItem& item = ensureItem(ref);
|
||||||
// 复位:均值点数 → 默认值(同时把滑块上限恢复为默认上限)。
|
// 复位:均值点数 → 默认值(同时把滑块上限恢复为默认上限)。
|
||||||
if (rowIndex < layout.rowLevelResets.size() &&
|
if (rowIndex < layout.rowLevelResets.size() &&
|
||||||
layout.rowLevelResets[rowIndex].Width > 0.0f &&
|
layout.rowLevelResets[rowIndex].Width > 0.0f &&
|
||||||
@@ -2279,6 +2404,10 @@ namespace jdi
|
|||||||
item.points = kMeanFilterPointsDefault;
|
item.points = kMeanFilterPointsDefault;
|
||||||
item.maxPoints = kMeanFilterPointsDefaultMax;
|
item.maxPoints = kMeanFilterPointsDefaultMax;
|
||||||
m_meanCurveCache.clear();
|
m_meanCurveCache.clear();
|
||||||
|
if (centerMode)
|
||||||
|
{
|
||||||
|
InvalidateEdgeScansFor(ResolveSignal(ref).data, true);
|
||||||
|
}
|
||||||
m_dragMode = PlotDragMode::NONE;
|
m_dragMode = PlotDragMode::NONE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2290,6 +2419,10 @@ namespace jdi
|
|||||||
item.iterations = kMeanFilterIterationsMin;
|
item.iterations = kMeanFilterIterationsMin;
|
||||||
item.maxIterations = kMeanFilterIterationsDefaultMax;
|
item.maxIterations = kMeanFilterIterationsDefaultMax;
|
||||||
m_meanCurveCache.clear();
|
m_meanCurveCache.clear();
|
||||||
|
if (centerMode)
|
||||||
|
{
|
||||||
|
InvalidateEdgeScansFor(ResolveSignal(ref).data, true);
|
||||||
|
}
|
||||||
m_dragMode = PlotDragMode::NONE;
|
m_dragMode = PlotDragMode::NONE;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2300,6 +2433,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
m_editMeanRef = ref;
|
m_editMeanRef = ref;
|
||||||
m_meanEditCurrentMax = item.maxPoints;
|
m_meanEditCurrentMax = item.maxPoints;
|
||||||
|
m_editMeanCenter = centerMode;
|
||||||
const gd::RectF& track = layout.rowLevelTracks[rowIndex];
|
const gd::RectF& track = layout.rowLevelTracks[rowIndex];
|
||||||
const gd::RectF& value = layout.rowLevelValues[rowIndex];
|
const gd::RectF& value = layout.rowLevelValues[rowIndex];
|
||||||
const gd::RectF rowRect(track.X, value.Y,
|
const gd::RectF rowRect(track.X, value.Y,
|
||||||
@@ -2317,6 +2451,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
m_editMeanRef = ref;
|
m_editMeanRef = ref;
|
||||||
m_meanEditCurrentMax = item.maxIterations;
|
m_meanEditCurrentMax = item.maxIterations;
|
||||||
|
m_editMeanCenter = centerMode;
|
||||||
const gd::RectF& track = layout.rowIterTracks[rowIndex];
|
const gd::RectF& track = layout.rowIterTracks[rowIndex];
|
||||||
const gd::RectF& value = layout.rowIterValues[rowIndex];
|
const gd::RectF& value = layout.rowIterValues[rowIndex];
|
||||||
const gd::RectF rowRect(track.X, value.Y,
|
const gd::RectF rowRect(track.X, value.Y,
|
||||||
@@ -2331,21 +2466,23 @@ namespace jdi
|
|||||||
if (rowIndex < layout.rowLevelTracks.size() &&
|
if (rowIndex < layout.rowLevelTracks.size() &&
|
||||||
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
|
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
|
||||||
{
|
{
|
||||||
const int maximum = std::max(
|
const int floorValue = centerMode ? 0 : kMeanFilterPointsMin;
|
||||||
kMeanFilterPointsMin,
|
const int maximum =
|
||||||
std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
std::max(floorValue, std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
||||||
m_dragMode = PlotDragMode::MEAN_POINTS;
|
m_dragMode = PlotDragMode::MEAN_POINTS;
|
||||||
m_meanDragLane = index;
|
m_meanDragLane = index;
|
||||||
m_meanDragRef = ref;
|
m_meanDragRef = ref;
|
||||||
|
m_meanDragCenter = centerMode;
|
||||||
const double ratio = UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx);
|
const double ratio = UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx);
|
||||||
item.points = std::max(
|
item.points = std::max(
|
||||||
kMeanFilterPointsMin,
|
floorValue,
|
||||||
std::min(maximum,
|
std::min(maximum,
|
||||||
kMeanFilterPointsMin +
|
floorValue + static_cast<int>(std::lround(ratio * (maximum - floorValue)))));
|
||||||
static_cast<int>(std::lround(
|
|
||||||
ratio *
|
|
||||||
(maximum - kMeanFilterPointsMin)))));
|
|
||||||
m_meanCurveCache.clear();
|
m_meanCurveCache.clear();
|
||||||
|
if (centerMode)
|
||||||
|
{
|
||||||
|
InvalidateEdgeScansFor(ResolveSignal(ref).data, true);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// 拖动「迭代次数」滑块。
|
// 拖动「迭代次数」滑块。
|
||||||
@@ -2358,15 +2495,19 @@ namespace jdi
|
|||||||
m_dragMode = PlotDragMode::MEAN_ITERATIONS;
|
m_dragMode = PlotDragMode::MEAN_ITERATIONS;
|
||||||
m_meanDragLane = index;
|
m_meanDragLane = index;
|
||||||
m_meanDragRef = ref;
|
m_meanDragRef = ref;
|
||||||
|
m_meanDragCenter = centerMode;
|
||||||
const double ratio = UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
|
const double ratio = UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
|
||||||
item.iterations = std::max(
|
item.iterations = std::max(
|
||||||
kMeanFilterIterationsMin,
|
kMeanFilterIterationsMin,
|
||||||
std::min(maximum,
|
std::min(maximum,
|
||||||
kMeanFilterIterationsMin +
|
kMeanFilterIterationsMin +
|
||||||
static_cast<int>(std::lround(
|
static_cast<int>(std::lround(
|
||||||
ratio *
|
ratio * (maximum - kMeanFilterIterationsMin)))));
|
||||||
(maximum - kMeanFilterIterationsMin)))));
|
|
||||||
m_meanCurveCache.clear();
|
m_meanCurveCache.clear();
|
||||||
|
if (centerMode)
|
||||||
|
{
|
||||||
|
InvalidateEdgeScansFor(ResolveSignal(ref).data, true);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2385,11 +2526,18 @@ namespace jdi
|
|||||||
rowIndex < layout.rowChecks.size() &&
|
rowIndex < layout.rowChecks.size() &&
|
||||||
layout.rowChecks[rowIndex].Width > 0.0f &&
|
layout.rowChecks[rowIndex].Width > 0.0f &&
|
||||||
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
|
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
|
||||||
Lane::MeanFilterItem* existing = const_cast<Lane::MeanFilterItem*>(
|
Lane::MeanFilterItem* existing =
|
||||||
meanLane.FindMeanFilterItem(ref));
|
const_cast<Lane::MeanFilterItem*>(findItem(ref));
|
||||||
if (onCheckBox)
|
if (onCheckBox)
|
||||||
{
|
{
|
||||||
ToggleLaneMeanFilterSignal(index, ref);
|
if (centerMode)
|
||||||
|
{
|
||||||
|
ToggleLaneEdgeCenterSignal(index, ref);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ToggleLaneMeanFilterSignal(index, ref);
|
||||||
|
}
|
||||||
m_meanRowHoverLane = -1;
|
m_meanRowHoverLane = -1;
|
||||||
m_meanRowHoverRef = SignalRef();
|
m_meanRowHoverRef = SignalRef();
|
||||||
}
|
}
|
||||||
@@ -3057,9 +3205,10 @@ namespace jdi
|
|||||||
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
|
// 弱峰滑块拖拽结束:清掉目标(避免留下上一轮的信号引用)。
|
||||||
m_peakDragLane = -1;
|
m_peakDragLane = -1;
|
||||||
m_peakDragRef = SignalRef();
|
m_peakDragRef = SignalRef();
|
||||||
// 「均值滤波」参数滑块拖拽结束:同样清掉目标。
|
// 「均值滤波 / 中心轴边沿计数」参数滑块拖拽结束:同样清掉目标。
|
||||||
m_meanDragLane = -1;
|
m_meanDragLane = -1;
|
||||||
m_meanDragRef = SignalRef();
|
m_meanDragRef = SignalRef();
|
||||||
|
m_meanDragCenter = false;
|
||||||
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
||||||
m_markerOpacityDragLane = -1;
|
m_markerOpacityDragLane = -1;
|
||||||
m_markerScrollDragLane = -1;
|
m_markerScrollDragLane = -1;
|
||||||
@@ -3111,10 +3260,11 @@ namespace jdi
|
|||||||
panelLane, panelKind))
|
panelLane, panelKind))
|
||||||
{
|
{
|
||||||
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
|
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
|
||||||
// 「均值滤波」面板:内容区超高时用滚轮翻页(上百个信号合并时必备)。
|
// 「均值滤波 / 中心轴边沿计数」面板:内容区超高时用滚轮翻页
|
||||||
if (hoveredKind == EdgePanelKind::MEAN)
|
//(上百个信号合并时必备)。
|
||||||
|
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER)
|
||||||
{
|
{
|
||||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, EdgePanelKind::MEAN);
|
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
|
||||||
if (layout.content.Width > 0.0f &&
|
if (layout.content.Width > 0.0f &&
|
||||||
UiPointInRect(layout.content, static_cast<float>(m_mouseX),
|
UiPointInRect(layout.content, static_cast<float>(m_mouseX),
|
||||||
static_cast<float>(m_mouseY)))
|
static_cast<float>(m_mouseY)))
|
||||||
@@ -3123,11 +3273,21 @@ namespace jdi
|
|||||||
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
std::max(0.0f, layout.markerContentHeight - layout.content.Height);
|
||||||
if (maxScroll > 0.0f)
|
if (maxScroll > 0.0f)
|
||||||
{
|
{
|
||||||
const float current =
|
const Lane& wheelLane =
|
||||||
Tab().Lanes()[static_cast<std::size_t>(panelLane)].meanFilterScroll;
|
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
|
||||||
|
const float current = (hoveredKind == EdgePanelKind::CENTER)
|
||||||
|
? wheelLane.edgeCenterScroll
|
||||||
|
: wheelLane.meanFilterScroll;
|
||||||
const float next = current - static_cast<float>(notches * 40.0 * m_scale);
|
const float next = current - static_cast<float>(notches * 40.0 * m_scale);
|
||||||
SetLaneMeanFilterScroll(panelLane,
|
const float target = std::max(0.0f, std::min(next, maxScroll));
|
||||||
std::max(0.0f, std::min(next, maxScroll)));
|
if (hoveredKind == EdgePanelKind::CENTER)
|
||||||
|
{
|
||||||
|
SetLaneEdgeCenterScroll(panelLane, target);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetLaneMeanFilterScroll(panelLane, target);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -815,8 +815,18 @@ namespace jdi
|
|||||||
// 就会在同一系统 tick 内连画多帧而**完全不推进**统计 —— 用户报告
|
// 就会在同一系统 tick 内连画多帧而**完全不推进**统计 —— 用户报告
|
||||||
// “按住横向光标不动时统计卡住不更新”“回车确认参数后不移动鼠标就停在部分值”。
|
// “按住横向光标不动时统计卡住不更新”“回车确认参数后不移动鼠标就停在部分值”。
|
||||||
++m_edgeScanFrame;
|
++m_edgeScanFrame;
|
||||||
m_edgeScanBudget = kEdgeScanPairsPerFrame;
|
// ⚠️⚠️ **交互帧必须用小预算**(用户 2026-11-02 报告):288 个信号合并到同一网格、
|
||||||
|
// 开启中心轴边沿计数后,拖动横向光标每帧要 30ms,界面完全跟不上手。中心轴每个采样对
|
||||||
|
// 还要按「均值点数 × 迭代次数」算一次均值窗口 ⇒ 120 万对 ≈ 3 千万次浮点运算/帧。
|
||||||
|
// 现在:正在拖拽、或鼠标刚刚移动过(`kEdgeScanInteractiveIdleMs` 内)⇒ 用交互预算
|
||||||
|
// (1/5),优先保证界面跟手;鼠标停下约 0.14s 后自动恢复高预算、把统计快速补齐。
|
||||||
|
// ⚠️⚠️ **交互帧必须用小预算**(用户 2026-11-02 报告):288 个信号合并到同一网格、
|
||||||
|
// 开启中心轴边沿计数后,拖动横向光标每帧要 30ms,界面完全跟不上手。中心轴每个采样对
|
||||||
|
// 还要按「均值点数 × 迭代次数」算一次均值窗口 ⇒ 120 万对 ≈ 3 千万次浮点运算/帧。
|
||||||
|
// 现在:正在拖拽、或鼠标刚刚移动过(`kEdgeScanInteractiveIdleMs` 内)⇒ 用交互预算
|
||||||
|
// (1/5),优先保证界面跟手;鼠标停下约 0.14s 后自动恢复高预算、把统计快速补齐。
|
||||||
|
m_edgeScanBudget =
|
||||||
|
InputIsInteractive() ? kEdgeScanPairsPerFrameInteractive : kEdgeScanPairsPerFrame;
|
||||||
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
|
// 双缓冲:清掉上一帧钉住的曲线条目(本帧会按需重新钉住),并清理已被更新条目覆盖 /
|
||||||
// 超出内存预算的旧条目。此刻没有任何条目被钉住,回收是安全的。
|
// 超出内存预算的旧条目。此刻没有任何条目被钉住,回收是安全的。
|
||||||
m_curvePins.clear();
|
m_curvePins.clear();
|
||||||
@@ -3483,33 +3493,89 @@ namespace jdi
|
|||||||
mix(static_cast<std::uint64_t>(m_laneDragFrom + 2));
|
mix(static_cast<std::uint64_t>(m_laneDragFrom + 2));
|
||||||
mix(static_cast<std::uint64_t>(m_laneDragTarget + 2));
|
mix(static_cast<std::uint64_t>(m_laneDragTarget + 2));
|
||||||
mix(static_cast<std::uint64_t>(m_laneDragMouseY + 4096));
|
mix(static_cast<std::uint64_t>(m_laneDragMouseY + 4096));
|
||||||
mix(static_cast<std::uint64_t>(m_hoverLane + 2));
|
// ⚠️⚠️ **交互期间冻结全部“随鼠标变化”的项**(用户 2026-11-02 报告“移动鼠标/拖动光标卡顿”):
|
||||||
mix(m_cursorHover ? 1ull : 0ull);
|
// 本键是**全部**面板像素缓存的总开关(`panelGlobalSame`)—— 只要它每帧变,所有面板都会
|
||||||
mix(static_cast<std::uint64_t>(m_hoverCursor + 2));
|
// 重画。鼠标在绘图区移动时 `m_hoverSignal`(甚至 `m_hoverLane` / `m_hoverCursor`)会不停
|
||||||
|
// 变化,于是“移动鼠标 → 每帧重画全部面板(288 个信号的中心轴面板 22ms)”成了死循环。
|
||||||
|
// 这些叠加层都画在**面板之下**、面板本身不透明且贴回时只贴圆角形状之内,因此交互期间
|
||||||
|
// 直接复用面板像素是安全的;鼠标停下后键自动恢复,面板重画一次即可。
|
||||||
|
const bool interactivePanelKey = InputIsInteractive();
|
||||||
|
if (interactivePanelKey)
|
||||||
|
{
|
||||||
|
mix(0x9E3779B9ull);
|
||||||
|
mix(0x9E3779B9ull);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mix(static_cast<std::uint64_t>(m_hoverLane + 2));
|
||||||
|
mix(m_cursorHover ? 1ull : 0ull);
|
||||||
|
mix(static_cast<std::uint64_t>(m_hoverCursor + 2));
|
||||||
|
}
|
||||||
mix(static_cast<std::uint64_t>(m_activeCursor));
|
mix(static_cast<std::uint64_t>(m_activeCursor));
|
||||||
mix(m_dragMoved ? 1ull : 0ull);
|
mix(m_dragMoved ? 1ull : 0ull);
|
||||||
mixRect(m_rubberRect);
|
mixRect(m_rubberRect);
|
||||||
mixRef(m_hoverSignal);
|
if (interactivePanelKey)
|
||||||
mixRef(m_hoverLegend);
|
{
|
||||||
|
mix(0x9E3779B9ull);
|
||||||
|
mix(0x9E3779B9ull);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mixRef(m_hoverSignal);
|
||||||
|
mixRef(m_hoverLegend);
|
||||||
|
}
|
||||||
mixRef(m_dragSignal);
|
mixRef(m_dragSignal);
|
||||||
mixRef(SelectedRef());
|
mixRef(SelectedRef());
|
||||||
mix(static_cast<std::uint64_t>(m_edgePanelDragLane + 2));
|
mix(static_cast<std::uint64_t>(m_edgePanelDragLane + 2));
|
||||||
mix(static_cast<std::uint64_t>(m_edgePanelDragKind));
|
mix(static_cast<std::uint64_t>(m_edgePanelDragKind));
|
||||||
|
// ⚠️ 「均值滤波 / 中心轴边沿计数」**条目栏的悬停高亮**必须**始终**参与全局键:
|
||||||
|
// 它直接改变面板像素(行头叠一层强调色),而且只在鼠标真正进入/离开某一行时才变化。
|
||||||
|
// 若跟着上面那些项一起在交互期间冻结,拖动鼠标到条目栏上高亮就不会出现(用户要求
|
||||||
|
// “鼠标处于条目栏上方时对应曲线/条目需要高亮提示”)。
|
||||||
|
mix(static_cast<std::uint64_t>(m_meanRowHoverLane + 2));
|
||||||
|
mixRef(m_meanRowHoverRef);
|
||||||
|
|
||||||
// 时间光标与统计区间:面板标题的时间后缀、以及「中心轴」行的上升/下降沿数字都依赖它们。
|
// 时间光标与统计区间:面板标题的时间后缀、以及「中心轴」行的上升/下降沿数字都依赖它们。
|
||||||
|
// ⚠️⚠️ **交互期间必须用固定占位值**:拖动光标时时间每帧都在变,键一变就要重画全部面板。
|
||||||
mix(static_cast<std::uint64_t>(Tab().CursorMode()));
|
mix(static_cast<std::uint64_t>(Tab().CursorMode()));
|
||||||
mixDouble(Tab().CursorTime());
|
if (interactivePanelKey)
|
||||||
mixDouble(Tab().CursorTimeAlt());
|
{
|
||||||
|
mixDouble(0.0);
|
||||||
|
mixDouble(0.0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mixDouble(Tab().CursorTime());
|
||||||
|
mixDouble(Tab().CursorTimeAlt());
|
||||||
|
}
|
||||||
mix(EdgeRangeActive() ? 1ull : 0ull);
|
mix(EdgeRangeActive() ? 1ull : 0ull);
|
||||||
mixDouble(EdgeRangeMin());
|
if (interactivePanelKey)
|
||||||
mixDouble(EdgeRangeMax());
|
{
|
||||||
|
// 统计区间来自选区时不随鼠标变化;但若它来自光标就会每帧变 —— 一并冻结。
|
||||||
|
mixDouble(0.0);
|
||||||
|
mixDouble(0.0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mixDouble(EdgeRangeMin());
|
||||||
|
mixDouble(EdgeRangeMax());
|
||||||
|
}
|
||||||
|
|
||||||
// 跟随鼠标的叠加层(悬停光晕 / 图例半高亮 / 信号拖拽虚影 / 提示动画)位置随时在变:
|
// 跟随鼠标的叠加层(悬停光晕 / 图例半高亮 / 信号拖拽虚影 / 提示动画)位置随时在变:
|
||||||
// 只要它存在,就把所有面板缓存作废,避免它的残影被一起缓存进面板像素。
|
// 只要它存在,就把所有面板缓存作废,避免它的残影被一起缓存进面板像素。
|
||||||
|
//
|
||||||
|
// ⚠️⚠️ **交互期间必须忽略它**(用户 2026-11-02 报告“移动鼠标/拖动光标卡顿”):
|
||||||
|
// 鼠标在绘图区里移动时 `m_hoverSignal` 常常有效(悬停在某条曲线上),这里就会把
|
||||||
|
// **鼠标坐标**混进全局键 ⇒ 键每帧都变 ⇒ 全部面板每帧重画。288 个信号的中心轴面板
|
||||||
|
// 一块就要 ~23ms(实测:面板开 27.6ms / 面板关 4.3ms)。
|
||||||
|
// 而悬停光晕、拖拽虚影都画在**面板之下**,面板本身不透明且贴回时只贴圆角形状之内
|
||||||
|
// (见下面的 `CreateRoundRectRgn` 裁剪 + 每帧重画投影),因此交互期间直接复用面板像素
|
||||||
|
// 是安全的;鼠标停下后键自动恢复,面板重画一次即可。
|
||||||
const bool mouseDrivenOverlay =
|
const bool mouseDrivenOverlay =
|
||||||
m_hoverSignal.IsValid() || m_hoverLegend.IsValid() ||
|
!interactivePanelKey &&
|
||||||
m_dragMode == PlotDragMode::MOVE_SIGNAL || m_dragMode == PlotDragMode::RUBBER_SELECT ||
|
(m_hoverSignal.IsValid() || m_hoverLegend.IsValid() ||
|
||||||
HintNeedsAnimation();
|
m_dragMode == PlotDragMode::MOVE_SIGNAL || m_dragMode == PlotDragMode::RUBBER_SELECT ||
|
||||||
|
HintNeedsAnimation());
|
||||||
mix(mouseDrivenOverlay ? 1ull : 0ull);
|
mix(mouseDrivenOverlay ? 1ull : 0ull);
|
||||||
if (mouseDrivenOverlay)
|
if (mouseDrivenOverlay)
|
||||||
{
|
{
|
||||||
@@ -3717,7 +3783,17 @@ namespace jdi
|
|||||||
// 才会显示出早已算完的正确结果。
|
// 才会显示出早已算完的正确结果。
|
||||||
// ⇒ 把该面板当前要显示的 (上升沿, 下降沿, 是否扫完) 一并 mix 进去:推进会让键变化 ⇒
|
// ⇒ 把该面板当前要显示的 (上升沿, 下降沿, 是否扫完) 一并 mix 进去:推进会让键变化 ⇒
|
||||||
// 面板重绘 ⇒ 显示随进度更新,直到扫完(扫完后数字不再变,重新命中缓存)。
|
// 面板重绘 ⇒ 显示随进度更新,直到扫完(扫完后数字不再变,重新命中缓存)。
|
||||||
if (kind == EdgePanelKind::COUNT)
|
//
|
||||||
|
// ⚠️⚠️ **但交互期间必须跳过这一整段**(用户 2026-11-02 报告“拖动横向光标卡顿”):
|
||||||
|
// * 计算这一段本身要为**每个信号**调一次 `EdgeCountsFor` / `EdgeCenterCountsFor`
|
||||||
|
// (含区间边界块扫描)—— 288 个信号时单帧 288 次查询;
|
||||||
|
// * 而且中间数字每帧都在变 ⇒ 键每帧都变 ⇒ 面板像素缓存**永远不命中**,
|
||||||
|
// 于是每帧都要把整块面板(288 行 × 十来个 GDI+ 原语)重画一遍 ⇒ 实测 30ms/帧。
|
||||||
|
// 拖动/鼠标移动期间数字本来就在飞速重算,**显示中间值毫无意义**;此时让键保持稳定
|
||||||
|
// (用一个固定常量占位)⇒ 面板直接贴上一帧像素 ⇒ 真正跟手。鼠标停下
|
||||||
|
// `kEdgeScanInteractiveIdleMs` 之后恢复完整键 ⇒ 面板重画并显示最终数字。
|
||||||
|
const bool interactiveKey = InputIsInteractive();
|
||||||
|
if (kind == EdgePanelKind::COUNT && !interactiveKey)
|
||||||
{
|
{
|
||||||
for (const SignalRef& ref : lane.signals)
|
for (const SignalRef& ref : lane.signals)
|
||||||
{
|
{
|
||||||
@@ -3737,7 +3813,7 @@ namespace jdi
|
|||||||
mix(complete ? 1ull : 0ull);
|
mix(complete ? 1ull : 0ull);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (kind == EdgePanelKind::CENTER)
|
else if (kind == EdgePanelKind::CENTER && !interactiveKey)
|
||||||
{
|
{
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||||
{
|
{
|
||||||
@@ -4330,6 +4406,9 @@ namespace jdi
|
|||||||
(m_editTarget == PlotEditTarget::NONE) && EnsurePanelCache();
|
(m_editTarget == PlotEditTarget::NONE) && EnsurePanelCache();
|
||||||
const bool panelGlobalSame =
|
const bool panelGlobalSame =
|
||||||
panelCacheReady && m_panelValid && panelGlobalKey == m_panelGlobalKey;
|
panelCacheReady && m_panelValid && panelGlobalKey == m_panelGlobalKey;
|
||||||
|
// 交互帧(拖动 / 鼠标刚移动)的标志:缓存命中的贴图分支据此跳过昂贵的投影重画
|
||||||
|
//(见下面 `UiFillRoundRect` 的说明)。
|
||||||
|
const bool interactiveFrame = InputIsInteractive();
|
||||||
// ⚠️ **按层序从下往上绘制**(与命中测试、像素缓存的层叠顺序完全一致):
|
// ⚠️ **按层序从下往上绘制**(与命中测试、像素缓存的层叠顺序完全一致):
|
||||||
// `m_panelOrder` 队尾 = 最上层 = 最后绘制。
|
// `m_panelOrder` 队尾 = 最上层 = 最后绘制。
|
||||||
SyncPanelOrder();
|
SyncPanelOrder();
|
||||||
@@ -4399,12 +4478,13 @@ namespace jdi
|
|||||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||||
// ⚠️ `itemized` 只表示**旧的逐条目排布**(中心轴 / 弱峰调整)。
|
// ⚠️ `itemized` 只表示**旧的逐条目排布**(现在只剩「弱峰调整」)。
|
||||||
// 「均值滤波」自 2026-10-30 起有自己的**可滚动分支**(`if (meanFilter)` 内),
|
// 「均值滤波」自 2026-10-30 起、**「中心轴边沿计数」自 2026-11-02 起**都有自己的
|
||||||
// 若它仍被算作 `itemized`,旧分支会**再画一遍**每行的名称 + 复选框 ——
|
// **可滚动分支**(`if (listStyle)` 内)。若它们仍被算作 `itemized`,旧分支会
|
||||||
// 且旧分支**没有任何裁切**,滚动时这些元素就会画到标题栏上
|
// **再画一遍**每行的名称 + 复选框 —— 且旧分支**没有任何裁切**,滚动时这些元素就会
|
||||||
//(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
|
// 画到标题栏上(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
|
||||||
const bool itemized = (centerAxis || weakPeak);
|
const bool listStyle = (meanFilter || centerAxis);
|
||||||
|
const bool itemized = weakPeak;
|
||||||
const bool hasButton = (meanFilter || weakPeak);
|
const bool hasButton = (meanFilter || weakPeak);
|
||||||
{
|
{
|
||||||
if (index >= count)
|
if (index >= count)
|
||||||
@@ -4476,7 +4556,13 @@ namespace jdi
|
|||||||
// (否则平移会重画全部面板),于是整块贴回时就把旧波形"复印"了一圈。
|
// (否则平移会重画全部面板),于是整块贴回时就把旧波形"复印"了一圈。
|
||||||
// 现在:① 先把**投影**按本帧重新画一遍(它在圆角形状之外,不能靠缓存);
|
// 现在:① 先把**投影**按本帧重新画一遍(它在圆角形状之外,不能靠缓存);
|
||||||
// ② 用圆角区域裁剪后只 BitBlt 面板本体(圆角之外仍是本帧场景)。
|
// ② 用圆角区域裁剪后只 BitBlt 面板本体(圆角之外仍是本帧场景)。
|
||||||
if (!m_fastFrame)
|
// ⚠️⚠️ **投影不能每帧重画**(用户 2026-11-02 报告“移动鼠标/拖动光标卡顿”):
|
||||||
|
// 它是一块面积与面板同大的**半透明抗锯齿圆角矩形**(中心轴面板高近 950px、
|
||||||
|
// 宽 400px),GDI+ 软件渲染这一块实测要 **20ms 以上** —— 而缓存命中的
|
||||||
|
// 贴图本身只要零点几毫秒。交互期间(拖动 / 鼠标刚移动)直接跳过投影:
|
||||||
|
// 面板圆角之外保留上一帧的像素(此时那块区域本来也是静止的场景),
|
||||||
|
// 鼠标停下后键恢复、面板重画一次即补上投影。
|
||||||
|
if (!m_fastFrame && !interactiveFrame)
|
||||||
{
|
{
|
||||||
UiFillRoundRect(g, gd::RectF(panel.X + 1.0f * m_scale,
|
UiFillRoundRect(g, gd::RectF(panel.X + 1.0f * m_scale,
|
||||||
panel.Y + 2.0f * m_scale, panel.Width,
|
panel.Y + 2.0f * m_scale, panel.Width,
|
||||||
@@ -4852,7 +4938,7 @@ namespace jdi
|
|||||||
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
||||||
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
||||||
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
|
// (「均值点数」/「迭代次数」);内容超高时可滚动(滚动条在内容区右侧)。
|
||||||
if (meanFilter)
|
if (listStyle)
|
||||||
{
|
{
|
||||||
const int meanSave = g.Save();
|
const int meanSave = g.Save();
|
||||||
// 内容必须裁到内容区:滚动时行会越出可视范围。
|
// 内容必须裁到内容区:滚动时行会越出可视范围。
|
||||||
@@ -4938,6 +5024,10 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
extendRow(layout.rowIterResets[rowIndex]);
|
extendRow(layout.rowIterResets[rowIndex]);
|
||||||
}
|
}
|
||||||
|
if (rowIndex < layout.rowStats.size())
|
||||||
|
{
|
||||||
|
extendRow(layout.rowStats[rowIndex]);
|
||||||
|
}
|
||||||
if (rowBottom <= layout.content.Y ||
|
if (rowBottom <= layout.content.Y ||
|
||||||
rowTop >= layout.content.Y + layout.content.Height)
|
rowTop >= layout.content.Y + layout.content.Height)
|
||||||
{
|
{
|
||||||
@@ -4945,7 +5035,10 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||||
const UiInk ink = UiContrastInk(signalColor);
|
const UiInk ink = UiContrastInk(signalColor);
|
||||||
const Lane::MeanFilterItem* item = tabLane.FindMeanFilterItem(ref);
|
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`
|
||||||
|
// (两者都是 `Lane::MeanFilterItem`,字段完全一致)。
|
||||||
|
const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref)
|
||||||
|
: tabLane.FindEdgeCenterItem(ref);
|
||||||
const bool checked = (item != nullptr);
|
const bool checked = (item != nullptr);
|
||||||
|
|
||||||
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
|
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
|
||||||
@@ -4987,6 +5080,19 @@ namespace jdi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
|
||||||
|
if (centerAxis && rowIndex < layout.rowStats.size() &&
|
||||||
|
layout.rowStats[rowIndex].Width > 0.0f)
|
||||||
|
{
|
||||||
|
std::size_t rising = 0;
|
||||||
|
std::size_t falling = 0;
|
||||||
|
bool complete = true;
|
||||||
|
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
|
||||||
|
item->iterations, EdgeRangeActive(), EdgeRangeMin(),
|
||||||
|
EdgeRangeMax(), rising, falling, complete);
|
||||||
|
paintCounts(layout.rowStats[rowIndex], rising, falling, complete);
|
||||||
|
}
|
||||||
|
|
||||||
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
|
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
|
||||||
const auto paintMeanSlider = [&](const gd::RectF& track,
|
const auto paintMeanSlider = [&](const gd::RectF& track,
|
||||||
const gd::RectF& valueRect,
|
const gd::RectF& valueRect,
|
||||||
@@ -5031,11 +5137,15 @@ namespace jdi
|
|||||||
hot ? m_theme->accent : m_theme->textMuted, m_scale);
|
hot ? m_theme->accent : m_theme->textMuted, m_scale);
|
||||||
};
|
};
|
||||||
|
|
||||||
const int pointsMax = std::max(kMeanFilterPointsMin,
|
// ⚠️ 「均值点数」的最小值:**均值滤波 = 1**(用户 2026-10-30 要求不能为 0,
|
||||||
|
// 0 与不做平滑完全等价,看起来像功能坏了);**中心轴边沿计数 = 0**
|
||||||
|
// (0 = 不平滑,直接对原始采样值统计边沿 —— 这是有意义的选项,保持旧语义)。
|
||||||
|
const int pointsFloor = centerAxis ? 0 : kMeanFilterPointsMin;
|
||||||
|
const int pointsMax = std::max(pointsFloor,
|
||||||
std::min(kMeanFilterPointsMaxLimit,
|
std::min(kMeanFilterPointsMaxLimit,
|
||||||
item->maxPoints));
|
item->maxPoints));
|
||||||
const int points =
|
const int points =
|
||||||
std::max(kMeanFilterPointsMin, std::min(pointsMax, item->points));
|
std::max(pointsFloor, std::min(pointsMax, item->points));
|
||||||
const int iterationsMax = std::max(kMeanFilterIterationsMin,
|
const int iterationsMax = std::max(kMeanFilterIterationsMin,
|
||||||
std::min(kMeanFilterIterationsMaxLimit,
|
std::min(kMeanFilterIterationsMaxLimit,
|
||||||
item->maxIterations));
|
item->maxIterations));
|
||||||
@@ -5043,10 +5153,11 @@ namespace jdi
|
|||||||
kMeanFilterIterationsMin, std::min(iterationsMax, item->iterations));
|
kMeanFilterIterationsMin, std::min(iterationsMax, item->iterations));
|
||||||
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
|
const bool pointsActive = (m_dragMode == PlotDragMode::MEAN_POINTS) &&
|
||||||
(m_meanDragLane == static_cast<int>(index)) &&
|
(m_meanDragLane == static_cast<int>(index)) &&
|
||||||
(m_meanDragRef == ref);
|
(m_meanDragRef == ref) && (m_meanDragCenter == centerAxis);
|
||||||
const bool iterationsActive =
|
const bool iterationsActive =
|
||||||
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
|
(m_dragMode == PlotDragMode::MEAN_ITERATIONS) &&
|
||||||
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref);
|
(m_meanDragLane == static_cast<int>(index)) && (m_meanDragRef == ref) &&
|
||||||
|
(m_meanDragCenter == centerAxis);
|
||||||
if (rowIndex < layout.rowLevelTracks.size())
|
if (rowIndex < layout.rowLevelTracks.size())
|
||||||
{
|
{
|
||||||
paintMeanSlider(layout.rowLevelTracks[rowIndex],
|
paintMeanSlider(layout.rowLevelTracks[rowIndex],
|
||||||
@@ -5057,7 +5168,7 @@ namespace jdi
|
|||||||
? layout.rowLevelResets[rowIndex]
|
? layout.rowLevelResets[rowIndex]
|
||||||
: gd::RectF(),
|
: gd::RectF(),
|
||||||
kMeanFilterPointsSliderLabel, points,
|
kMeanFilterPointsSliderLabel, points,
|
||||||
kMeanFilterPointsMin, pointsMax, pointsActive);
|
pointsFloor, pointsMax, pointsActive);
|
||||||
}
|
}
|
||||||
if (rowIndex < layout.rowIterTracks.size())
|
if (rowIndex < layout.rowIterTracks.size())
|
||||||
{
|
{
|
||||||
@@ -5434,7 +5545,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (itemized || freeMarker || meanFilter)
|
if (itemized || freeMarker || listStyle)
|
||||||
{
|
{
|
||||||
// 逐条目绘制已在上面完成(自由标记同样是逐条目排布,
|
// 逐条目绘制已在上面完成(自由标记同样是逐条目排布,
|
||||||
// 均值滤波走自己的可滚动分支)。
|
// 均值滤波走自己的可滚动分支)。
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ namespace jdi
|
|||||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」都是逐条目面板(名称 + 复选框)。
|
// 「弱峰调整」仍是旧式逐条目面板(名称 + 复选框 + 两行滑块)。
|
||||||
const bool itemized = (centerAxis || meanFilter || weakPeak);
|
// ⚠️ 「均值滤波」与「中心轴边沿计数」已改为**可滚动的分组列表**(信号色叠底条目栏),
|
||||||
|
// 它们的宽度由下面的 `groupWidth` / 滑块行分支确定。
|
||||||
|
const bool itemized = weakPeak;
|
||||||
|
|
||||||
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
||||||
// + 光标区间尾缀)。
|
// + 光标区间尾缀)。
|
||||||
@@ -69,10 +71,10 @@ namespace jdi
|
|||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
|
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
|
||||||
kEdgePanelMeanCheckSize * m_scale + 6.0f * m_scale);
|
kEdgePanelMeanCheckSize * m_scale + 6.0f * m_scale);
|
||||||
}
|
}
|
||||||
if (meanFilter)
|
if (meanFilter || centerAxis)
|
||||||
{
|
{
|
||||||
// 「均值滤波」的条目 = **信号色叠底标题栏**(三角 + 名称 + 复选框)
|
// 「均值滤波 / 中心轴边沿计数」的条目 = **信号色叠底条目栏**(三角 + 名称 + 复选框)
|
||||||
// 与两行滑块(标签 + 轨道 + 数值 + 复位按钮),右侧还要给滚动条留位。
|
// 与参数行(标签 + 轨道 + 数值 + 复位按钮),右侧还要给滚动条留位。
|
||||||
const float groupWidth = kEdgePanelPad * 2.0f * m_scale +
|
const float groupWidth = kEdgePanelPad * 2.0f * m_scale +
|
||||||
kEdgePanelDisclosureWidth * m_scale + 6.0f * m_scale +
|
kEdgePanelDisclosureWidth * m_scale + 6.0f * m_scale +
|
||||||
nameWidth + 6.0f * m_scale +
|
nameWidth + 6.0f * m_scale +
|
||||||
@@ -124,21 +126,7 @@ namespace jdi
|
|||||||
std::max(nameColumnWidth, countsWidth));
|
std::max(nameColumnWidth, countsWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (centerAxis || meanFilter)
|
if (meanFilter || centerAxis)
|
||||||
{
|
|
||||||
// 参数行 = 两个「说明文字 + 数值框」,必须保证文字不被数值框压住。
|
|
||||||
// (CENTER:「均值点数 X」+「迭代次数 N」。MEAN 已改为滑块,见下面单独的分支。)
|
|
||||||
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,
|
|
||||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000000") + 18.0f * m_scale);
|
|
||||||
const float halfWidth = std::max(labelXWidth, labelN) + 6.0f * m_scale + fieldWidth;
|
|
||||||
const float rowWidth = halfWidth * 2.0f + 8.0f * m_scale;
|
|
||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meanFilter)
|
|
||||||
{
|
{
|
||||||
// 两行滑块 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(两行共用同一列宽),
|
// 两行滑块 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(两行共用同一列宽),
|
||||||
// 右侧再给滚动条留位(上百个信号时内容必然超高)。
|
// 右侧再给滚动条留位(上百个信号时内容必然超高)。
|
||||||
@@ -263,7 +251,11 @@ namespace jdi
|
|||||||
// 「均值滤波」与「弱峰调整」共用同一套「逐条目 + 底部按钮」排布。
|
// 「均值滤波」与「弱峰调整」共用同一套「逐条目 + 底部按钮」排布。
|
||||||
// ⚠️ 「均值滤波」从 2026-10-30 起改为**可滚动的分组列表**(信号色叠底标题栏 +
|
// ⚠️ 「均值滤波」从 2026-10-30 起改为**可滚动的分组列表**(信号色叠底标题栏 +
|
||||||
// 展开后的两行滑块),与「自由标记」同一套浏览方式 ⇒ 不再走这里的 `itemized` 分支。
|
// 展开后的两行滑块),与「自由标记」同一套浏览方式 ⇒ 不再走这里的 `itemized` 分支。
|
||||||
const bool itemized = (centerAxis || weakPeak);
|
// ⚠️ 「中心轴边沿计数」从 2026-11-02 起同样改为**可滚动的分组列表**(用户明确要求
|
||||||
|
// “改造为与均值滤波相同的风格”)⇒ 也不走 `itemized`。它没有「构建为新信号」按钮,
|
||||||
|
// 但内容多一行「上升沿 / 下降沿」统计。
|
||||||
|
const bool listStyle = (meanFilter || centerAxis);
|
||||||
|
const bool itemized = weakPeak;
|
||||||
const bool hasButton = (meanFilter || weakPeak);
|
const bool hasButton = (meanFilter || weakPeak);
|
||||||
const bool enabled = meanFilter ? lane.meanFilter
|
const bool enabled = meanFilter ? lane.meanFilter
|
||||||
: (weakPeak ? lane.weakPeak
|
: (weakPeak ? lane.weakPeak
|
||||||
@@ -322,11 +314,12 @@ namespace jdi
|
|||||||
float itemsHeight = 0.0f;
|
float itemsHeight = 0.0f;
|
||||||
float meanContentHeight = 0.0f;
|
float meanContentHeight = 0.0f;
|
||||||
float height = 0.0f;
|
float height = 0.0f;
|
||||||
if (meanFilter)
|
if (listStyle)
|
||||||
{
|
{
|
||||||
// 「均值滤波」:内容区 = 逐信号的**信号色叠底标题栏**(内容总是显示,
|
// 「均值滤波 / 中心轴边沿计数」:内容区 = 逐信号的**信号色叠底条目栏**
|
||||||
// 用于勾选/取消预览)+(勾选且未收起时)两行参数滑块。
|
//(内容总是显示,用于勾选/取消)+(勾选且未收起时)参数行。
|
||||||
// 内容超高时出现滚动条;底部「构建为新信号」按钮固定不滚动。
|
// 「中心轴」的参数行 = 一行「上升沿 / 下降沿」统计 + 两行滑块;
|
||||||
|
// 「均值滤波」= 两行滑块。内容超高时出现滚动条;底部「构建为新信号」按钮固定不滚动。
|
||||||
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
||||||
const float sliderRow = weakPeakSliderRow;
|
const float sliderRow = weakPeakSliderRow;
|
||||||
float content = 0.0f;
|
float content = 0.0f;
|
||||||
@@ -338,10 +331,15 @@ namespace jdi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
content += groupRow;
|
content += groupRow;
|
||||||
const Lane::MeanFilterItem* item = lane.FindMeanFilterItem(ref);
|
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
||||||
|
: lane.FindEdgeCenterItem(ref);
|
||||||
if (item != nullptr && !item->collapsed)
|
if (item != nullptr && !item->collapsed)
|
||||||
{
|
{
|
||||||
content += sliderRow * 2.0f;
|
content += sliderRow * 2.0f;
|
||||||
|
if (centerAxis)
|
||||||
|
{
|
||||||
|
content += groupRow; // 统计行(上升沿 / 下降沿)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
meanContentHeight = content;
|
meanContentHeight = content;
|
||||||
@@ -562,12 +560,16 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
if (meanFilter)
|
if (listStyle)
|
||||||
{
|
{
|
||||||
// 「均值滤波」:可滚动内容区(逐信号的信号色叠底标题栏 + 展开后的两行滑块)+ 底部按钮。
|
// 「均值滤波 / 中心轴边沿计数」:可滚动内容区
|
||||||
|
//(逐信号的信号色叠底条目栏 + 展开后的统计行与两行滑块)+(均值滤波)底部按钮。
|
||||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||||
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
|
if (hasButton)
|
||||||
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
|
{
|
||||||
|
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
|
||||||
|
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
||||||
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
|
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||||
@@ -576,7 +578,8 @@ namespace jdi
|
|||||||
std::max(0.0f, height - titleHeight - pad * 3.0f - buttonRowHeight);
|
std::max(0.0f, height - titleHeight - pad * 3.0f - buttonRowHeight);
|
||||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||||
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||||
const float scroll = std::max(0.0f, std::min(lane.meanFilterScroll, maxScroll));
|
const float storedScroll = meanFilter ? lane.meanFilterScroll : lane.edgeCenterScroll;
|
||||||
|
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
|
||||||
const float innerWidth =
|
const float innerWidth =
|
||||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||||
layout.content = gd::RectF(left + pad, top + titleHeight + pad, innerWidth, viewHeight);
|
layout.content = gd::RectF(left + pad, top + titleHeight + pad, innerWidth, viewHeight);
|
||||||
@@ -621,7 +624,8 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const Lane::MeanFilterItem* item = lane.FindMeanFilterItem(ref);
|
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
||||||
|
: lane.FindEdgeCenterItem(ref);
|
||||||
const bool checked = (item != nullptr);
|
const bool checked = (item != nullptr);
|
||||||
layout.rowRefs.push_back(ref);
|
layout.rowRefs.push_back(ref);
|
||||||
// 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。
|
// 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。
|
||||||
@@ -653,6 +657,12 @@ namespace jdi
|
|||||||
layout.rowIterResets.push_back(gd::RectF());
|
layout.rowIterResets.push_back(gd::RectF());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// 「中心轴边沿计数」:参数行最上面是**同一行**的「上升沿 / 下降沿」统计。
|
||||||
|
if (centerAxis && !layout.rowStats.empty())
|
||||||
|
{
|
||||||
|
layout.rowStats.back() = gd::RectF(contentLeft, rowY, innerWidth, groupRow);
|
||||||
|
rowY += groupRow;
|
||||||
|
}
|
||||||
const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
|
const gd::RectF pointsTrack(trackLeft, rowY + (sliderRow - trackHeight) * 0.5f,
|
||||||
trackWidth, trackHeight);
|
trackWidth, trackHeight);
|
||||||
layout.rowLevelTracks.push_back(pointsTrack);
|
layout.rowLevelTracks.push_back(pointsTrack);
|
||||||
|
|||||||
@@ -382,14 +382,6 @@ namespace jdi
|
|||||||
m_rowTop[index + 1] = m_rowTop[index] + m_rows[index].height;
|
m_rowTop[index + 1] = m_rowTop[index] + m_rows[index].height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 行顶前缀和:`m_rowTop[i]` = 第 0..i-1 行高度之和(长度 = 行数 + 1)。
|
|
||||||
// 行集合与行高只在这里变化 ⇒ 在此重建是唯一权威的时机。
|
|
||||||
m_rowTop.assign(m_rows.size() + 1, 0.0f);
|
|
||||||
for (std::size_t index = 0; index < m_rows.size(); ++index)
|
|
||||||
{
|
|
||||||
m_rowTop[index + 1] = m_rowTop[index] + m_rows[index].height;
|
|
||||||
}
|
|
||||||
|
|
||||||
float totalShift = 0.0f;
|
float totalShift = 0.0f;
|
||||||
for (const GroupAnim& anim : m_groupAnims)
|
for (const GroupAnim& anim : m_groupAnims)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8941,3 +8941,115 @@ rendercheck 8 个场景哈希与基线完全一致;make release / make debug
|
|||||||
白改了一轮断言。
|
白改了一轮断言。
|
||||||
* ⚠️ PowerShell 里用 `'...'` 拼接含 `%d` / 引号 / 长中文的补丁串极易被截断(出现 `>>` 续行提示后
|
* ⚠️ PowerShell 里用 `'...'` 拼接含 `%d` / 引号 / 长中文的补丁串极易被截断(出现 `>>` 续行提示后
|
||||||
只能 Ctrl+C)。**补丁一律写成 `.git\xxx.ps1`(纯 ASCII)+ `%TEMP%\xxx.txt` 数据文件**。
|
只能 Ctrl+C)。**补丁一律写成 `.git\xxx.ps1`(纯 ASCII)+ `%TEMP%\xxx.txt` 数据文件**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-11-02 第十九轮(本机):288 信号卡顿根治 + 中心轴面板重做 + 标签页默认状态
|
||||||
|
|
||||||
|
### 1. 卡顿真因:**面板缓存键把"鼠标驱动的一切"都混了进去**
|
||||||
|
|
||||||
|
复现:`VERY_BIG_DATA.CSV` + `MF4-Record.MF4` 全部信号合并到**同一网格**(288 信号),
|
||||||
|
开「中心轴边沿计数」后拖动横向光标/移动鼠标,实测 **29.8ms/帧**(修复后 **6.6ms**)。
|
||||||
|
|
||||||
|
分解(`.preflight/bugcheck.exe` A5b):
|
||||||
|
|
||||||
|
| 场景 | 单帧 |
|
||||||
|
| --- | --- |
|
||||||
|
| 面板开 + 统计开 | 27.6ms |
|
||||||
|
| **面板关 + 统计开** | **4.1ms** |
|
||||||
|
| 面板关 + 统计关 | 6.6ms(噪声) |
|
||||||
|
|
||||||
|
⇒ 27.6−4.1 ≈ **23ms 全在面板绘制**,而**不是**统计扫描。三个叠加原因:
|
||||||
|
|
||||||
|
1. ⚠️⚠️ **中心轴面板高度没有上限**:`itemized` 旧分支按"每个勾选信号两行 + 每行 22px"
|
||||||
|
累加,288 个信号 ⇒ **面板高 9996px**(绘图区只有 1141px)。`PanelRectInsideLayer()`
|
||||||
|
因此永远为假 ⇒ **面板像素缓存一次都存不进去** ⇒ 每帧全量重画 288 行。
|
||||||
|
2. ⚠️⚠️ **`PanelGlobalKey` 混入鼠标驱动状态**:`m_hoverSignal` / `m_hoverLane` /
|
||||||
|
`m_hoverCursor` / `Tab().CursorTime()` / `EdgeRangeMin/Max` 等只要鼠标一动就变
|
||||||
|
⇒ `panelGlobalSame` 每帧为假 ⇒ 所有面板每帧重画。而 `PanelSlotKey` 里还会为**每个信号**
|
||||||
|
调一次 `EdgeCenterCountsFor`(含区间边界块扫描)⇒ 单帧 288 次查询。
|
||||||
|
3. 缓存命中路径里的**投影**(与面板同大的半透明抗锯齿圆角矩形,400×950px)每帧重画,
|
||||||
|
GDI+ 软件渲染实测 > 20ms。
|
||||||
|
|
||||||
|
**修法**:
|
||||||
|
* 新增 `PlotView::InputIsInteractive()`(正在拖拽、或鼠标在 `kEdgeScanInteractiveIdleMs`
|
||||||
|
= 140ms 内移动过)。**交互期间**:
|
||||||
|
- `PanelGlobalKey` 冻结全部鼠标驱动项(`m_hoverSignal` / `m_hoverLegend` / `m_hoverLane` /
|
||||||
|
`m_hoverCursor` / `m_cursorHover` / `CursorTime` / `EdgeRangeMin/Max`)与
|
||||||
|
`mouseDrivenOverlay`;`PanelSlotKey` 跳过整段边沿统计数字;
|
||||||
|
- 缓存命中路径**跳过投影**;`m_edgeScanBudget` 用 `kEdgeScanPairsPerFrameInteractive`(8 万,
|
||||||
|
静止时 40 万)。
|
||||||
|
- ⚠️ **`m_meanRowHoverLane/Ref` 必须例外**:它直接改变面板像素、且只在进出某一行时变 ——
|
||||||
|
一起冻结的话"鼠标移到条目栏上高亮"就永远不出现。
|
||||||
|
- 鼠标停下 140ms 后键自动恢复 ⇒ 面板重画一次显示最终数字(不会"停在统计中")。
|
||||||
|
* 统计预算 `kEdgeScanPairsPerFrame` **1,200,000 → 400,000**(中心轴每个采样对还要按
|
||||||
|
`均值点数 × 迭代次数` 算一次均值窗口 ⇒ 120 万对 ≈ 3 千万次浮点运算/帧)。
|
||||||
|
* **中心轴面板重做为可滚动列表**(见第 2 节)⇒ 面板高度被 `m_plotRect.Height` 夹住、
|
||||||
|
缓存能存进去、只画可见行。
|
||||||
|
|
||||||
|
### 2. 「中心轴边沿计数」面板重做(与「均值滤波」完全同风格)
|
||||||
|
|
||||||
|
用户要求:条目栏是**信号色叠底的折叠栏**、右端复选框代表"是否统计该信号"、栏下**同一行**
|
||||||
|
显示「上升沿 / 下降沿」、再下面两行滑块(均值点数 / 迭代次数,标签不带 X/N)、
|
||||||
|
数值可点击输入上限、右侧复位按钮、整个面板可滚动、**未勾选时条目栏不可展开**。
|
||||||
|
|
||||||
|
实现要点:
|
||||||
|
* `EdgePanelLayoutFor`:`listStyle = (meanFilter || centerAxis)` 取代原来的 `if (meanFilter)`;
|
||||||
|
`itemized` 现在**只剩** `weakPeak`。中心轴的内容 = 条目栏 + (勾选且未收起时)
|
||||||
|
**统计行 + 两行滑块**(统计行在 `rowStats`,滑块复用 `rowLevel*` / `rowIter*`)。
|
||||||
|
* 新增 `Lane::edgeCenterScroll`(+ `LaneCarry` 同步)+ `PlotView::SetLaneEdgeCenterScroll()`。
|
||||||
|
滚动偏移通过 `layout.scrollOffset` 自动进 `PanelSlotKey`(无需额外 mix)。
|
||||||
|
* 滑块最大值:**均值滤波 = 1**(用户 2026-10-30 要求不能为 0),**中心轴 = 0**
|
||||||
|
(0 = 不平滑,直接对原始值统计边沿,保持旧语义)。
|
||||||
|
* 交互复用 `PlotDragMode::MEAN_POINTS / MEAN_ITERATIONS / MEAN_SCROLL` + 新标志
|
||||||
|
`m_meanDragCenter`(拖拽/滚动目标面板)+ `m_editMeanCenter`(上限输入目标面板);
|
||||||
|
复位按钮、点击数值改上限、滚轮翻页、光标形状(条目栏/数值/复位=手型,滑块=左右箭头,
|
||||||
|
滚动条=上下箭头)全部共用同一段代码。
|
||||||
|
* 参数/勾选变化必须 `InvalidateEdgeScansFor(signal, true)`(定向作废,避免"改一个参数
|
||||||
|
其它信号也重算")。
|
||||||
|
|
||||||
|
### 3. 标签页
|
||||||
|
|
||||||
|
* ⚠️ **唯一标签页点 ✕ 不再弹警告**:`CloseTab` 在 `m_tabs.size() <= 1` 时直接 erase 再
|
||||||
|
`NewTab(true)` ⇒ 名字必然是「视图 1」(`m_tabs` 清空后 `NextTabName()` 取最小未占用编号)。
|
||||||
|
⚠️ erase 与 `NewTab` 之间**不要**碰 `m_panel` / `m_plot`(上下文仍指向已释放的 `TabConfig`)。
|
||||||
|
* **新建标签页的默认状态**(手动新建也走同一入口):`NewTab()` 里新增
|
||||||
|
`m_dataSet.SetSelected(SignalRef())` —— 选中状态是**全局共享**的,不清掉的话新页会带着
|
||||||
|
上一个页的选中信号 ⇒ 样式编辑区有自己的"目标"。清空后样式区回到「未选择信号」。
|
||||||
|
组全部折起由 `SetGroupsCollapsedByDefault(true)` + `SetAllCollapsed(true)` 保证(既有行为)。
|
||||||
|
|
||||||
|
### 4. 顺带修正
|
||||||
|
|
||||||
|
* `PanelGlobalKey` 里 `m_hoverCursor/m_cursorHover/m_hoverLane` 也纳入交互冻结(只影响叠加层)。
|
||||||
|
* 光标形状:`QueryCursor` 对 MEAN/CENTER 可滚动列表补上条目栏/数值/复位/滑块/滚动条的
|
||||||
|
手型与箭头(原来只有 PEAK 有)。
|
||||||
|
|
||||||
|
### 5. 验证
|
||||||
|
|
||||||
|
```
|
||||||
|
.preflight/bugcheck.exe 59/59
|
||||||
|
A1 空闲 0.7ms · A2 平移 2.5ms · A3 双光标 2.4ms · A4 自由标记光标 5.2ms
|
||||||
|
A5 中心轴拖动 6.6ms(修前 29.8ms)
|
||||||
|
A5b 分解:面板开 5.7ms / 面板关 4.9ms / 无光标 7.2ms;面板 460.5px(修前 9996px)
|
||||||
|
B 组名展开/收起(283 行):行集合 0 ↔ 283、签名自洽
|
||||||
|
B3 真实消息路径(WM_LBUTTONDOWN/UP + WM_TIMER + WM_PAINT):展开 90 帧 / 收起 95 帧
|
||||||
|
B5 快速连点 3 次 / 动画中途反向:三角与行集合始终一致
|
||||||
|
C 中心轴面板:未勾选条目栏点击无效 / 复选框启用 / 整行收起展开 /
|
||||||
|
拖滑块 90% → 45 点 / 复位 → 5 点且上限 50 / 滚轮滚动 60px
|
||||||
|
D 唯一标签页关闭 → 「视图 1」+ 全折起 + 无选中;手动新建同样
|
||||||
|
.preflight/panelcheck.exe 71/71 markercheck.exe 13/13 ringcheck.exe 6/6
|
||||||
|
make release / make debug 零 warning
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. 踩坑
|
||||||
|
|
||||||
|
* ⚠️⚠️ **`PaintEdgeCursor` 的"笨重"必须先分段测量**:本轮一开始把 30ms 归因于"统计扫描"
|
||||||
|
和"统计数字进缓存键",改了预算与键都不见效;直到做了「面板开/关」「统计开/关」的
|
||||||
|
A/B 才定位到**面板高度超界 ⇒ 像素缓存从未命中**。**判据要用"缓存是否真的被用上"
|
||||||
|
(`m_panelSlots[0].key != 0` + `PanelRectInsideLayer`),而不是"看起来像重画"**。
|
||||||
|
* ⚠️ 探针里模拟点击面板前**必须关掉其它实验性功能**:它们默认位置同样在绘图区右上角,
|
||||||
|
`TopPanelAtPoint` 只返回最上层那一个 ⇒ 命中全被另一个面板抢走(表现为"点了没反应")。
|
||||||
|
* ⚠️ 组名行的点击热区:`X = 行矩形 40%` 处是名称(BODY),`x = 40px` 附近是**复选框**
|
||||||
|
(`RowPart::CHECK`)—— 想测折叠必须点名称区。
|
||||||
|
* ⚠️ 「行集合签名是否稳定」不能作为断言:动画**结束那一帧**签名必然变化一次(动画标记位
|
||||||
|
从 5 → 0),这是正确的。要断言的是"动画结束后 `m_rowsSignature == RowsSignature()`"。
|
||||||
@@ -1067,6 +1067,15 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
float meanFilterScroll = 0.0f;
|
float meanFilterScroll = 0.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 「中心轴边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
*
|
||||||
|
* 与 `meanFilterScroll` 完全同一套语义:面板自 2026-11-02 起改为**可滚动的分组列表**
|
||||||
|
*(信号色叠底条目栏 + 右侧复选框 + 展开后的统计行与两行参数滑块),
|
||||||
|
* 内容超出可视高度时右侧出现滚动条(否则上百个信号会把面板顶出绘图区)。
|
||||||
|
*/
|
||||||
|
float edgeCenterScroll = 0.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ namespace jdi
|
|||||||
std::vector<SignalRef> freeMarkerCollapsedGroups;
|
std::vector<SignalRef> freeMarkerCollapsedGroups;
|
||||||
/** @brief 「均值滤波」面板内容的滚动偏移(详见 `Lane::meanFilterScroll`)。 */
|
/** @brief 「均值滤波」面板内容的滚动偏移(详见 `Lane::meanFilterScroll`)。 */
|
||||||
float meanFilterScroll = 0.0f;
|
float meanFilterScroll = 0.0f;
|
||||||
|
/** @brief 「中心轴边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCenterScroll`)。 */
|
||||||
|
float edgeCenterScroll = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 先记下每个信号原来所在网格的网格自身状态:
|
// 先记下每个信号原来所在网格的网格自身状态:
|
||||||
@@ -173,6 +175,7 @@ namespace jdi
|
|||||||
carry.edgeCenter = lane.edgeCenter;
|
carry.edgeCenter = lane.edgeCenter;
|
||||||
carry.edgeCenterItems = lane.edgeCenterItems;
|
carry.edgeCenterItems = lane.edgeCenterItems;
|
||||||
carry.edgeCenterPanel = lane.edgeCenterPanel;
|
carry.edgeCenterPanel = lane.edgeCenterPanel;
|
||||||
|
carry.edgeCenterScroll = lane.edgeCenterScroll;
|
||||||
carry.meanFilter = lane.meanFilter;
|
carry.meanFilter = lane.meanFilter;
|
||||||
carry.meanFilterItems = lane.meanFilterItems;
|
carry.meanFilterItems = lane.meanFilterItems;
|
||||||
carry.meanFilterPanel = lane.meanFilterPanel;
|
carry.meanFilterPanel = lane.meanFilterPanel;
|
||||||
@@ -233,6 +236,7 @@ namespace jdi
|
|||||||
lane.edgeCenterPanel.collapsed = carry.edgeCenterPanel.collapsed;
|
lane.edgeCenterPanel.collapsed = carry.edgeCenterPanel.collapsed;
|
||||||
lane.edgeCenterPanel.x = carry.edgeCenterPanel.x;
|
lane.edgeCenterPanel.x = carry.edgeCenterPanel.x;
|
||||||
lane.edgeCenterPanel.y = carry.edgeCenterPanel.y;
|
lane.edgeCenterPanel.y = carry.edgeCenterPanel.y;
|
||||||
|
lane.edgeCenterScroll = carry.edgeCenterScroll;
|
||||||
lane.meanFilter = carry.meanFilter;
|
lane.meanFilter = carry.meanFilter;
|
||||||
lane.meanFilterItems = carry.meanFilterItems;
|
lane.meanFilterItems = carry.meanFilterItems;
|
||||||
lane.meanFilterPanel.open = carry.meanFilterPanel.open;
|
lane.meanFilterPanel.open = carry.meanFilterPanel.open;
|
||||||
|
|||||||
Reference in new issue
Block a user