面板限高与滚动改造 + 边沿统计不再卡在“统计中…” + 组名折起真正收起
一、浮动面板(用户报告:下方空白、滚动时画到面板外、未改造的面板让一切交互卡顿) 1. ⭐ 根因:旧「逐条目」排布没有高度上限 —— 288 条信号时面板 13010px(绘图区 846px) ⇒ PanelRectInsideLayer 恒假 ⇒ 面板像素缓存一次都存不进去 ⇒ 每帧整块重画。 现在 5 个面板全部限高 + 可滚动,旧的 itemized 布局/绘制/交互整体删除。 2. 无按钮面板(中心轴边沿计数)不再预留「按钮行 + pad」⇒ 底部空白 55px → 8.8px(1 个 pad)。 3. ⚠️ 统计文字改用 UiTextClipped:GDI 文本不受 GDI+ SetClip 约束,滚动时画到标题栏之外。 4. 「弱峰调整」改造为与均值滤波同款的分组列表(条目栏 + 右侧复选框 + 展开后的两行滑块 + 复位按钮 + 底部「构建为新信号」);新增 Lane::weakPeakScroll / SetLaneWeakPeakScroll, WeakPeakItem 增加 collapsed。 5. 「边沿计数」保留两行排版,但内容区同样限高、可滚动(新增 Lane::edgeCountScroll)。 6. 鼠标指针:列表式面板的条目栏**只有已勾选**才显示手型(未勾选点击无效果,保持箭头); 参数行控件只在参数行展开时给手型 / 左右箭头。 7. 内部标志 m_meanDragCenter / m_editMeanCenter(bool)改为 m_meanDragKind / m_editMeanKind (EdgePanelKind),以便 MEAN / CENTER / PEAK / COUNT 共用同一套滚动与滑块拖拽。 二、边沿统计“全部统计中…且永不更新”(288 信号合并到同一网格) 1. ⭐ 根因:累积状态上限固定 64 < 信号数 288 ⇒ 新建状态总淘汰未完成的 ⇒ 没有任何状态能扫完。 新增 EdgeScanStateLimit() = max(64, 全部信号数 × 2 + 16)。 2. 去掉每状态 60000 的固定配额下限(状态一多就把整帧预算耗光),改为 budget / 未完成状态数; 分母只数未完成状态,短信号扫完后立刻把预算让给长信号。 三、组名折起后“三角已收起但信号行仍在、仍可点选” 1. ⭐ 根因:RequestEdgeScanFrame() 在每个未完成状态里各 Post 一次(单帧最多 288 次), 把优先级最低的 WM_TIMER 饿死 ⇒ AdvanceGroupAnimations 永不执行 ⇒ 折起动画永不结束。 修法:每帧最多 Post 一次(m_edgeScanRequestFrame)。 2. 另一道保险:SignalPanel::EnsureRows() 开头推进一次动画 —— 动画进度只依赖挂钟时间, 不依赖消息能否按时到达。 四、验证(离屏探针 .preflight/p20,全部 SW_HIDE + 合成消息,不占用鼠标/键盘/屏幕) p20.exe 9/9:面板限高且像素缓存真的写进去了、288 个统计全部 complete、中心轴面板底部留白、 组名折起后行集合无该组信号、两组都折起后 signalRows == 0(真实消息 + WM_TIMER)。 p20c.exe 11/11:弱峰面板限高/可滚动/缓存命中、展开与收起、滚动生效、条目栏手型规则、 中心轴滚动时标题栏 0 像素变化(A/B 关掉裁剪 ⇒ 805px)、288 信号空闲帧 2.6ms。 make release / make debug 零 warning。
This commit is contained in:
1 parent
334015c1a9
commit
7a6fcd68e2
9 files changed
+914
-795
No files matched your search
@@ -1103,6 +1103,21 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
bool SetLaneEdgeCenterScroll(int laneIndex, float scroll);
|
bool SetLaneEdgeCenterScroll(int laneIndex, float scroll);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置某网格「弱峰调整」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
* @return 偏移真的发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool SetLaneWeakPeakScroll(int laneIndex, float scroll);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置某网格「边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
*
|
||||||
|
* 该面板仍是旧式「每信号两行」排版,但内容可以滚动:上百个信号合并到同一网格时
|
||||||
|
* 内容高达上万像素,不限高会让面板远超绘图区(面板像素缓存因此完全失效)。
|
||||||
|
* @return 偏移真的发生变化返回 true。
|
||||||
|
*/
|
||||||
|
bool SetLaneEdgeCountScroll(int laneIndex, float scroll);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
* @brief 切换某网格「自由标记」面板里某个信号分组的收起状态。
|
||||||
* @return 切换后为「已收起」返回 true。
|
* @return 切换后为「已收起」返回 true。
|
||||||
@@ -2814,13 +2829,15 @@ namespace jdi
|
|||||||
int m_meanDragLane = -1; /**< 正在拖动「均值滤波」参数滑块的面板所属网格。 */
|
int m_meanDragLane = -1; /**< 正在拖动「均值滤波」参数滑块的面板所属网格。 */
|
||||||
SignalRef m_meanDragRef; /**< 正在拖动「均值滤波」参数滑块的信号(每信号一套参数)。 */
|
SignalRef m_meanDragRef; /**< 正在拖动「均值滤波」参数滑块的信号(每信号一套参数)。 */
|
||||||
/**
|
/**
|
||||||
* @brief 当前参数滑块拖拽的目标是**中心轴边沿计数**面板(而不是均值滤波)。
|
* @brief 当前「列表式面板」参数滑块 / 滚动条拖拽的目标面板种类。
|
||||||
*
|
*
|
||||||
* 「中心轴边沿计数」自 2026-11-02 起与「均值滤波」共用同一套可滚动列表与滑块交互,
|
* 「均值滤波」「中心轴边沿计数」「弱峰调整」自 2026-11-02 起共用同一套**可滚动列表**
|
||||||
* 因此复用 `MEAN_POINTS` / `MEAN_ITERATIONS` / `MEAN_SCROLL` 三个拖拽模式;
|
* (信号色叠底条目栏 + 右侧复选框 + 展开后的参数滑块)与同一套拖拽模式
|
||||||
* 这个标志说明它们的写入目标是 `Lane::edgeCenterItems` 还是 `Lane::meanFilterItems`。
|
*(`MEAN_POINTS` / `MEAN_ITERATIONS` / `MEAN_SCROLL`);本字段说明它们的写入目标是
|
||||||
|
* `Lane::meanFilterItems` / `edgeCenterItems` / `weakPeakItems` 中的哪一个。
|
||||||
|
* 「边沿计数」面板(旧式两行排版)也用它区分滚动目标。
|
||||||
*/
|
*/
|
||||||
bool m_meanDragCenter = false;
|
EdgePanelKind m_meanDragKind = EdgePanelKind::MEAN;
|
||||||
/**
|
/**
|
||||||
* @brief 正在编辑「均值滤波」某个滑块上限时,输入框内灰色提示用的**当前上限值**。
|
* @brief 正在编辑「均值滤波」某个滑块上限时,输入框内灰色提示用的**当前上限值**。
|
||||||
*
|
*
|
||||||
@@ -2829,12 +2846,13 @@ namespace jdi
|
|||||||
int m_meanEditCurrentMax = 0;
|
int m_meanEditCurrentMax = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 正在编辑「滑块上限」的是**中心轴边沿计数**面板(而不是均值滤波)。
|
* @brief 正在编辑「滑块上限」的目标面板种类(均值滤波 / 中心轴边沿计数)。
|
||||||
*
|
*
|
||||||
* 与 `m_meanDragCenter` 同一思路:两者共用 `MEAN_POINTS_MAX` / `MEAN_ITERATIONS_MAX`
|
* 与 `m_meanDragKind` 同一思路:两者共用 `MEAN_POINTS_MAX` / `MEAN_ITERATIONS_MAX`
|
||||||
* 两个编辑目标,这个标志说明提交时要写到 `Lane::edgeCenterItems` 还是 `meanFilterItems`。
|
* 两个编辑目标,本字段说明提交时要写到 `Lane::edgeCenterItems` 还是 `meanFilterItems`。
|
||||||
|
* 「弱峰调整」仍用自己的 `PEAK_ITERATIONS_MAX` 与 `m_peakEditCurrentMax`。
|
||||||
*/
|
*/
|
||||||
bool m_editMeanCenter = false;
|
EdgePanelKind m_editMeanKind = EdgePanelKind::MEAN;
|
||||||
/**
|
/**
|
||||||
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
|
* @brief 正在编辑「迭代次数 N 上限」时,输入框内灰色提示用的**当前上限值**。
|
||||||
*
|
*
|
||||||
@@ -3061,9 +3079,25 @@ namespace jdi
|
|||||||
* 用户看到的就是“按住光标不动时统计卡住、要动鼠标才更新”。
|
* 用户看到的就是“按住光标不动时统计卡住、要动鼠标才更新”。
|
||||||
*/
|
*/
|
||||||
mutable std::uint64_t m_edgeScanFrame = 0;
|
mutable std::uint64_t m_edgeScanFrame = 0;
|
||||||
|
/**
|
||||||
|
* @brief 上一次“请求下一帧”发生在哪一帧(收敛成**每帧最多一次** `PostMessage`)。
|
||||||
|
*
|
||||||
|
* ⚠️ 旧实现在 `EdgeScanFor` 里按“未完成状态”逐个 Post:288 条信号时单帧 288 次,
|
||||||
|
* 把消息队列和重绘打满,`WM_TIMER`(分组折起动画)因此被饿死(用户 2026-11-02 报告)。
|
||||||
|
*/
|
||||||
|
mutable std::uint64_t m_edgeScanRequestFrame = static_cast<std::uint64_t>(-1);
|
||||||
/** 诊断计数:中心轴本地曲线计算失败次数(内存不足等)。0 = 一切正常。 */
|
/** 诊断计数:中心轴本地曲线计算失败次数(内存不足等)。0 = 一切正常。 */
|
||||||
mutable std::size_t m_edgeScanLocalCurveFails = 0;
|
mutable std::size_t m_edgeScanLocalCurveFails = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 累积状态上限(随可见信号总数缩放)。
|
||||||
|
*
|
||||||
|
* 每个信号最多两类状态(普通边沿计数 / 中心轴边沿计数),因此上限 ≥ `2 × 信号数`。
|
||||||
|
* 固定上限(旧 64)在 288 条信号合并到同一网格时会让所有状态互相淘汰 ⇒
|
||||||
|
* 面板永远停在“统计中…”(用户 2026-11-02 报告)。
|
||||||
|
*/
|
||||||
|
std::size_t EdgeScanStateLimit() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 边沿统计的统一实现(普通边沿计数与中心轴边沿计数共用)。
|
* @brief 边沿统计的统一实现(普通边沿计数与中心轴边沿计数共用)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1828,6 +1828,40 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlotView::SetLaneWeakPeakScroll(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.weakPeakScroll - clamped) < 0.5f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 滚动只影响面板内部像素,缓冲区/场景都不变(面板像素缓存按键失效即可)。
|
||||||
|
lane.weakPeakScroll = clamped;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlotView::SetLaneEdgeCountScroll(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.edgeCountScroll - clamped) < 0.5f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 滚动只影响面板内部像素,缓冲区/场景都不变(面板像素缓存按键失效即可)。
|
||||||
|
lane.edgeCountScroll = 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()))
|
||||||
@@ -2609,6 +2643,16 @@ namespace jdi
|
|||||||
// 预算也就永远不推进(用户报告:"面板过去很久都一直显示统计中,
|
// 预算也就永远不推进(用户报告:"面板过去很久都一直显示统计中,
|
||||||
// 只有当鼠标瞄到浮动面板上时才会显示统计好的值")。
|
// 只有当鼠标瞄到浮动面板上时才会显示统计好的值")。
|
||||||
// 与派生曲线就绪用同一条消息(宿主只失效绘图区、不置脏,代价极小)。
|
// 与派生曲线就绪用同一条消息(宿主只失效绘图区、不置脏,代价极小)。
|
||||||
|
//
|
||||||
|
// ⚠️⚠️ **每个绘制帧最多 Post 一次**(用户 2026-11-02 报告“上百条信号时所有交互都卡”):
|
||||||
|
// 本函数在 `EdgeScanFor` 里按“未完成的状态”逐个调用,288 条信号时单帧会 Post 288 次!
|
||||||
|
// 大量 `WM_MSG`+重绘会把优先级更低的 `WM_TIMER` 饿死(分组折起动画因此永远结束不了),
|
||||||
|
// 同时把 CPU 占满、拖慢所有交互。用一个“本帧已请求”标志收敛成一次。
|
||||||
|
if (m_edgeScanRequestFrame == m_edgeScanFrame)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_edgeScanRequestFrame = m_edgeScanFrame;
|
||||||
const HWND host = m_host;
|
const HWND host = m_host;
|
||||||
if (host != nullptr)
|
if (host != nullptr)
|
||||||
{
|
{
|
||||||
@@ -2616,9 +2660,22 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlotView::InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly)
|
std::size_t PlotView::EdgeScanStateLimit() const
|
||||||
{
|
{
|
||||||
// ⚠️ **定向作废**:只有被改动的那个信号(并且只限需要的那一类)要重算,
|
// 每个信号最多两类累积状态(普通边沿计数 / 中心轴边沿计数),另留一点余量。
|
||||||
|
// ⚠️ 不能用“当前网格内信号数”:边缘面板可能同时开在多个网格上,
|
||||||
|
// 而这里用全仓可见信号总数更省心(多出来的容量只是几个几十 KB 的块和数组)。
|
||||||
|
std::size_t signalCount = 0;
|
||||||
|
if (m_dataSet != nullptr)
|
||||||
|
{
|
||||||
|
signalCount = m_dataSet->SignalCount();
|
||||||
|
}
|
||||||
|
const std::size_t scaled = signalCount * 2u + 16u;
|
||||||
|
return std::max(kEdgeScanStateLimit, scaled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlotView::InvalidateEdgeScansFor(const SignalData* signal, bool centreOnly)
|
||||||
|
{ // ⚠️ **定向作废**:只有被改动的那个信号(并且只限需要的那一类)要重算,
|
||||||
// 其它信号的键没有变化、累计的块和依然有效 → 必须原样保留。
|
// 其它信号的键没有变化、累计的块和依然有效 → 必须原样保留。
|
||||||
// 否则(过去用 `m_edgeScans.clear()`)会连**普通边沿计数**与**其它信号的中心轴统计**
|
// 否则(过去用 `m_edgeScans.clear()`)会连**普通边沿计数**与**其它信号的中心轴统计**
|
||||||
// 一起丢掉,于是两个浮动面板里的每一个数字都要从头重新扫描:
|
// 一起丢掉,于是两个浮动面板里的每一个数字都要从头重新扫描:
|
||||||
@@ -2698,7 +2755,13 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
if (state == nullptr)
|
if (state == nullptr)
|
||||||
{
|
{
|
||||||
if (m_edgeScans.size() >= kEdgeScanStateLimit)
|
// ⚠️⚠️ **累积状态上限必须随“信号数”缩放**(用户 2026-11-02 报告:288 条信号合并到
|
||||||
|
// 同一网格、开启「边沿计数」后**全部停在“统计中…”**且永不更新):
|
||||||
|
// 旧上限固定 64,而面板会在一帧内为**每条信号**各创建一个状态(288 条 > 64)⇒
|
||||||
|
// 新建状态时总是把队首(尚未扫完的)淘汰 ⇒ 没有任何状态能活到扫完 ⇒ 永远重头开始。
|
||||||
|
// 每个信号最多两类状态(普通 / 中心轴),因此上限取 `2×信号数 + 余量`。
|
||||||
|
const std::size_t stateLimit = EdgeScanStateLimit();
|
||||||
|
if (m_edgeScans.size() >= stateLimit)
|
||||||
{
|
{
|
||||||
// ⚠️ 超限时**优先淘汰「已扫完」的最旧状态**(它重算很快、而且通常已经不需要再变),
|
// ⚠️ 超限时**优先淘汰「已扫完」的最旧状态**(它重算很快、而且通常已经不需要再变),
|
||||||
// 绝不淘汰正在推进的那个 —— 否则其它信号会被迫从头重扫
|
// 绝不淘汰正在推进的那个 —— 否则其它信号会被迫从头重扫
|
||||||
@@ -2891,10 +2954,24 @@ namespace jdi
|
|||||||
const bool suppressPartial = state->justReset && !state->complete;
|
const bool suppressPartial = state->justReset && !state->complete;
|
||||||
if (!state->complete && !state->blockRising.empty())
|
if (!state->complete && !state->blockRising.empty())
|
||||||
{
|
{
|
||||||
const std::size_t activeStates = std::max<std::size_t>(1, m_edgeScans.size());
|
// ⚠️ 分母只数**还没扫完**的状态:已完成的状态不再消耗配额,若把它们也算进分母,
|
||||||
|
// 上百条短信号里夹着几条千万级长信号时,长信号每帧只能分到 1/300 的预算
|
||||||
|
// (实测要 20 分钟才出结果)。按“未完成数”分摊后,短信号一完成就把预算让出来。
|
||||||
|
std::size_t activeStates = 0;
|
||||||
|
for (const EdgeScanState& candidate : m_edgeScans)
|
||||||
|
{
|
||||||
|
if (!candidate.complete)
|
||||||
|
{
|
||||||
|
++activeStates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeStates = std::max<std::size_t>(1, activeStates);
|
||||||
const std::size_t share = std::max<std::size_t>(kEdgeScanFairShare, activeStates);
|
const std::size_t share = std::max<std::size_t>(kEdgeScanFairShare, activeStates);
|
||||||
const std::size_t perState =
|
// ⚠️ **不要给每状态一个固定下限**(旧实现是 `max(60000, budget/share)`):
|
||||||
std::max<std::size_t>(kEdgeScanPairsPerStateMin, kEdgeScanPairsPerFrame / share);
|
// 状态数一多,下限就让**前几个状态**(恰好是网格里最前面的信号)吃掉整帧预算,
|
||||||
|
// 后面的信号永远分不到配额 ⇒ 一直显示“统计中…”(用户 2026-11-02 报告的
|
||||||
|
// 288 信号场景)。改为纯按未完成状态数平分本帧预算(至多 1)。
|
||||||
|
const std::size_t perState = std::max<std::size_t>(1, m_edgeScanBudget / share);
|
||||||
if (state->pumpedFrame != m_edgeScanFrame)
|
if (state->pumpedFrame != m_edgeScanFrame)
|
||||||
{
|
{
|
||||||
state->pumpedFrame = m_edgeScanFrame;
|
state->pumpedFrame = m_edgeScanFrame;
|
||||||
|
|||||||
@@ -651,7 +651,8 @@ 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)];
|
||||||
const Lane::MeanFilterItem* existing = m_editMeanCenter
|
const bool centerEdit = (m_editMeanKind == EdgePanelKind::CENTER);
|
||||||
|
const Lane::MeanFilterItem* existing = centerEdit
|
||||||
? lane.FindEdgeCenterItem(m_editMeanRef)
|
? lane.FindEdgeCenterItem(m_editMeanRef)
|
||||||
: lane.FindMeanFilterItem(m_editMeanRef);
|
: lane.FindMeanFilterItem(m_editMeanRef);
|
||||||
if (existing == nullptr)
|
if (existing == nullptr)
|
||||||
@@ -662,12 +663,12 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
const bool isPoints = (target == PlotEditTarget::MEAN_POINTS_MAX);
|
const bool isPoints = (target == PlotEditTarget::MEAN_POINTS_MAX);
|
||||||
const int minimum =
|
const int minimum =
|
||||||
isPoints ? (m_editMeanCenter ? 0 : kMeanFilterPointsMin) : kMeanFilterIterationsMin;
|
isPoints ? (centerEdit ? 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 = m_editMeanCenter
|
Lane::MeanFilterItem& item = centerEdit
|
||||||
? lane.EnsureEdgeCenterItem(m_editMeanRef)
|
? lane.EnsureEdgeCenterItem(m_editMeanRef)
|
||||||
: lane.EnsureMeanFilterItem(m_editMeanRef);
|
: lane.EnsureMeanFilterItem(m_editMeanRef);
|
||||||
if (isPoints)
|
if (isPoints)
|
||||||
@@ -682,7 +683,7 @@ 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)
|
if (centerEdit)
|
||||||
{
|
{
|
||||||
InvalidateEdgeScansFor(ResolveSignal(m_editMeanRef).data, true);
|
InvalidateEdgeScansFor(ResolveSignal(m_editMeanRef).data, true);
|
||||||
}
|
}
|
||||||
@@ -1070,59 +1071,93 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 「均值滤波 / 中心轴边沿计数」可滚动列表:条目栏(含复选框)→ 手型;
|
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整」可滚动列表:
|
||||||
// 滑块轨道 → 左右箭头;数值(可点击改上限)与复位按钮 → 手型;
|
// * 条目栏:**只有该信号已被勾选**时才给手型(未勾选时点击毫无效果,
|
||||||
// 内容区滚动条 → 上下箭头。与「弱峰调整」同一套观感。
|
// 用户 2026-11-02 明确要求此时不要改变鼠标指针形状);
|
||||||
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::MEAN ||
|
// * 滑块轨道 → 左右箭头;数值(可点击改上限)与复位按钮 → 手型;
|
||||||
static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::CENTER)
|
// * 内容区滚动条 → 上下箭头。
|
||||||
{
|
{
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowHeaders.size(); ++rowIndex)
|
const EdgePanelKind cursorKind = static_cast<EdgePanelKind>(panelKind);
|
||||||
|
const bool listCursor = (cursorKind == EdgePanelKind::MEAN ||
|
||||||
|
cursorKind == EdgePanelKind::CENTER ||
|
||||||
|
cursorKind == EdgePanelKind::PEAK);
|
||||||
|
const Lane& cursorLane =
|
||||||
|
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
|
||||||
|
if (listCursor)
|
||||||
{
|
{
|
||||||
if (layout.rowHeaders[rowIndex].Width > 0.0f &&
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowHeaders.size(); ++rowIndex)
|
||||||
UiPointInRect(layout.rowHeaders[rowIndex], static_cast<float>(x),
|
|
||||||
static_cast<float>(y)))
|
|
||||||
{
|
{
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
if (layout.rowHeaders[rowIndex].Width > 0.0f &&
|
||||||
}
|
UiPointInRect(layout.rowHeaders[rowIndex], static_cast<float>(x),
|
||||||
if (rowIndex < layout.rowLevelValues.size() &&
|
static_cast<float>(y)))
|
||||||
layout.rowLevelValues[rowIndex].Width > 0.0f &&
|
{
|
||||||
UiPointInRect(layout.rowLevelValues[rowIndex], static_cast<float>(x),
|
bool checked = false;
|
||||||
static_cast<float>(y)))
|
if (cursorKind == EdgePanelKind::PEAK)
|
||||||
{
|
{
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
checked = cursorLane.FindWeakPeakItem(
|
||||||
}
|
layout.rowRefs[rowIndex]) != nullptr;
|
||||||
if (rowIndex < layout.rowIterValues.size() &&
|
}
|
||||||
layout.rowIterValues[rowIndex].Width > 0.0f &&
|
else if (cursorKind == EdgePanelKind::CENTER)
|
||||||
UiPointInRect(layout.rowIterValues[rowIndex], static_cast<float>(x),
|
{
|
||||||
static_cast<float>(y)))
|
checked = cursorLane.FindEdgeCenterItem(
|
||||||
{
|
layout.rowRefs[rowIndex]) != nullptr;
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
}
|
||||||
}
|
else
|
||||||
if (rowIndex < layout.rowLevelResets.size() &&
|
{
|
||||||
layout.rowLevelResets[rowIndex].Width > 0.0f &&
|
checked = cursorLane.FindMeanFilterItem(
|
||||||
UiPointInRect(layout.rowLevelResets[rowIndex], static_cast<float>(x),
|
layout.rowRefs[rowIndex]) != nullptr;
|
||||||
static_cast<float>(y)))
|
}
|
||||||
{
|
return checked ? LoadCursorW(nullptr, IDC_HAND) : nullptr;
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
}
|
||||||
}
|
const bool hasParams =
|
||||||
if (rowIndex < layout.rowIterResets.size() &&
|
rowIndex < layout.rowLevelTracks.size() &&
|
||||||
layout.rowIterResets[rowIndex].Width > 0.0f &&
|
layout.rowLevelTracks[rowIndex].Width > 0.0f;
|
||||||
UiPointInRect(layout.rowIterResets[rowIndex], static_cast<float>(x),
|
if (!hasParams)
|
||||||
static_cast<float>(y)))
|
{
|
||||||
{
|
continue; // 参数行未展开(未勾选 / 已收起)⇒ 该行没有可交互控件
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
}
|
||||||
}
|
// 「均值点数 / 调整幅度」数值:均值滤波与中心轴可点击改上限(弱峰不能)。
|
||||||
if (rowIndex < layout.rowLevelTracks.size() &&
|
if (cursorKind != EdgePanelKind::PEAK &&
|
||||||
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale,
|
rowIndex < layout.rowLevelValues.size() &&
|
||||||
static_cast<float>(x), static_cast<float>(y)))
|
layout.rowLevelValues[rowIndex].Width > 0.0f &&
|
||||||
{
|
UiPointInRect(layout.rowLevelValues[rowIndex], static_cast<float>(x),
|
||||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
static_cast<float>(y)))
|
||||||
}
|
{
|
||||||
if (rowIndex < layout.rowIterTracks.size() &&
|
return LoadCursorW(nullptr, IDC_HAND);
|
||||||
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale,
|
}
|
||||||
static_cast<float>(x), static_cast<float>(y)))
|
if (rowIndex < layout.rowIterValues.size() &&
|
||||||
{
|
layout.rowIterValues[rowIndex].Width > 0.0f &&
|
||||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
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 &&
|
if (layout.scrollThumb.Width > 0.0f &&
|
||||||
@@ -1131,46 +1166,7 @@ namespace jdi
|
|||||||
return LoadCursorW(nullptr, IDC_SIZENS);
|
return LoadCursorW(nullptr, IDC_SIZENS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 「弱峰调整」的逐信号滑块:轨道 → 左右箭头;迭代次数右侧数值 → 手型(可点击改上限)。
|
|
||||||
if (static_cast<EdgePanelKind>(panelKind) == EdgePanelKind::PEAK)
|
|
||||||
{
|
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowIterValues.size(); ++rowIndex)
|
|
||||||
{
|
|
||||||
if (layout.rowIterValues[rowIndex].Width > 0.0f &&
|
|
||||||
UiPointInRect(layout.rowIterValues[rowIndex], static_cast<float>(x),
|
|
||||||
static_cast<float>(y)))
|
|
||||||
{
|
|
||||||
return LoadCursorW(nullptr, IDC_HAND);
|
|
||||||
}
|
|
||||||
// 两行滑块右侧的**复位控件**:与其它可点击图标一致,用手型光标提示可点击。
|
|
||||||
if (rowIndex < layout.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)))
|
|
||||||
{
|
|
||||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
|
||||||
}
|
|
||||||
if (rowIndex < layout.rowLevelTracks.size() &&
|
|
||||||
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale,
|
|
||||||
static_cast<float>(x), static_cast<float>(y)))
|
|
||||||
{
|
|
||||||
return LoadCursorW(nullptr, IDC_SIZEWE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 面板主体(含底部编辑行与参数输入框):保持原始箭头。
|
// 面板主体(含底部编辑行与参数输入框):保持原始箭头。
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -1540,19 +1536,19 @@ namespace jdi
|
|||||||
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)];
|
||||||
const Lane::MeanFilterItem* existingItem = m_meanDragCenter
|
const Lane::MeanFilterItem* existingItem = (m_meanDragKind == EdgePanelKind::CENTER)
|
||||||
? lane.FindEdgeCenterItem(m_meanDragRef)
|
? lane.FindEdgeCenterItem(m_meanDragRef)
|
||||||
: lane.FindMeanFilterItem(m_meanDragRef);
|
: lane.FindMeanFilterItem(m_meanDragRef);
|
||||||
if (existingItem == nullptr)
|
if (existingItem == nullptr)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Lane::MeanFilterItem& item = m_meanDragCenter
|
Lane::MeanFilterItem& item = (m_meanDragKind == EdgePanelKind::CENTER)
|
||||||
? lane.EnsureEdgeCenterItem(m_meanDragRef)
|
? lane.EnsureEdgeCenterItem(m_meanDragRef)
|
||||||
: lane.EnsureMeanFilterItem(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 = EdgePanelLayoutFor(
|
const EdgePanelLayout layout = EdgePanelLayoutFor(
|
||||||
m_meanDragLane, m_meanDragCenter ? EdgePanelKind::CENTER : EdgePanelKind::MEAN);
|
m_meanDragLane, m_meanDragKind);
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -1578,7 +1574,7 @@ namespace jdi
|
|||||||
if (pointsMode)
|
if (pointsMode)
|
||||||
{
|
{
|
||||||
// ⚠️ 下限:均值滤波 = 1(用户 2026-10-30 要求不能为 0);中心轴 = 0(0 = 不平滑)。
|
// ⚠️ 下限:均值滤波 = 1(用户 2026-10-30 要求不能为 0);中心轴 = 0(0 = 不平滑)。
|
||||||
const int floorValue = m_meanDragCenter ? 0 : kMeanFilterPointsMin;
|
const int floorValue = (m_meanDragKind == EdgePanelKind::CENTER) ? 0 : kMeanFilterPointsMin;
|
||||||
const int maximum =
|
const int maximum =
|
||||||
std::max(floorValue, std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
std::max(floorValue, std::min(kMeanFilterPointsMaxLimit, item.maxPoints));
|
||||||
item.points =
|
item.points =
|
||||||
@@ -1601,7 +1597,7 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
m_meanCurveCache.clear();
|
m_meanCurveCache.clear();
|
||||||
// 中心轴统计依赖参数 ⇒ 参数一变必须定向作废该信号的累积状态。
|
// 中心轴统计依赖参数 ⇒ 参数一变必须定向作废该信号的累积状态。
|
||||||
if (m_meanDragCenter)
|
if ((m_meanDragKind == EdgePanelKind::CENTER))
|
||||||
{
|
{
|
||||||
InvalidateEdgeScansFor(ResolveSignal(m_meanDragRef).data, true);
|
InvalidateEdgeScansFor(ResolveSignal(m_meanDragRef).data, true);
|
||||||
}
|
}
|
||||||
@@ -1750,7 +1746,7 @@ namespace jdi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const EdgePanelLayout layout = EdgePanelLayoutFor(
|
const EdgePanelLayout layout = EdgePanelLayoutFor(
|
||||||
m_markerScrollDragLane, m_meanDragCenter ? EdgePanelKind::CENTER : EdgePanelKind::MEAN);
|
m_markerScrollDragLane, m_meanDragKind);
|
||||||
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);
|
||||||
@@ -1763,10 +1759,23 @@ 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;
|
||||||
const bool scrolled =
|
const float target = ratio * maxScroll;
|
||||||
m_meanDragCenter
|
bool scrolled = false;
|
||||||
? SetLaneEdgeCenterScroll(m_markerScrollDragLane, ratio * maxScroll)
|
switch (m_meanDragKind)
|
||||||
: SetLaneMeanFilterScroll(m_markerScrollDragLane, ratio * maxScroll);
|
{
|
||||||
|
case EdgePanelKind::COUNT:
|
||||||
|
scrolled = SetLaneEdgeCountScroll(m_markerScrollDragLane, target);
|
||||||
|
break;
|
||||||
|
case EdgePanelKind::CENTER:
|
||||||
|
scrolled = SetLaneEdgeCenterScroll(m_markerScrollDragLane, target);
|
||||||
|
break;
|
||||||
|
case EdgePanelKind::PEAK:
|
||||||
|
scrolled = SetLaneWeakPeakScroll(m_markerScrollDragLane, target);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (scrolled)
|
if (scrolled)
|
||||||
{
|
{
|
||||||
m_dragMoved = true;
|
m_dragMoved = true;
|
||||||
@@ -2341,16 +2350,29 @@ namespace jdi
|
|||||||
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目
|
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板:逐信号条目
|
||||||
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
|
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
|
||||||
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
|
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
|
||||||
kind == EdgePanelKind::PEAK) &&
|
kind == EdgePanelKind::PEAK || kind == EdgePanelKind::COUNT) &&
|
||||||
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 centerMode = (kind == EdgePanelKind::CENTER);
|
||||||
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
||||||
// 「均值滤波」与「中心轴边沿计数」自 2026-11-02 起共用同一套**可滚动列表**交互
|
// 「均值滤波」「中心轴边沿计数」「弱峰调整」自 2026-11-02 起共用同一套
|
||||||
//(信号色叠底条目栏 + 右侧复选框 + 展开后的两行滑块)。
|
//**可滚动列表**交互(信号色叠底条目栏 + 右侧复选框 + 展开后的两行参数滑块)。
|
||||||
const bool listMode = (meanMode || centerMode);
|
const bool listMode = (meanMode || centerMode || boostMode);
|
||||||
// 「均值滤波」:组头三角(展开 / 收起参数行)+ 两行参数滑块
|
// 「边沿计数」仍是最简单的两行列表,只需要滚动条拖动。
|
||||||
|
if (kind == EdgePanelKind::COUNT)
|
||||||
|
{
|
||||||
|
if (layout.scrollThumb.Width > 0.0f &&
|
||||||
|
UiPointInRect(layout.scrollThumb, fx, fy))
|
||||||
|
{
|
||||||
|
m_dragMode = PlotDragMode::MEAN_SCROLL;
|
||||||
|
m_markerScrollDragLane = index;
|
||||||
|
m_markerScrollGrabOffset = fy - layout.scrollThumb.Y;
|
||||||
|
m_meanDragKind = EdgePanelKind::COUNT;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 「列表式面板」:组头三角(展开 / 收起参数行)+ 两行参数滑块
|
||||||
//(轨道拖动 / 点击数值改上限 / 右侧复位按钮)。
|
//(轨道拖动 / 点击数值改上限 / 右侧复位按钮)。
|
||||||
// ⚠️ 所有命中都要求落在**内容区**之内:滚动到可视范围之外的行仍保留矩形,
|
// ⚠️ 所有命中都要求落在**内容区**之内:滚动到可视范围之外的行仍保留矩形,
|
||||||
// 若不加这条限制,它们会抢走底部「构建为新信号」按钮的点击。
|
// 若不加这条限制,它们会抢走底部「构建为新信号」按钮的点击。
|
||||||
@@ -2375,11 +2397,110 @@ 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;
|
m_meanDragKind = kind;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const bool insideContent =
|
const bool insideContent =
|
||||||
UiPointInRect(layout.content, fx, fy);
|
UiPointInRect(layout.content, fx, fy);
|
||||||
|
// 「弱峰调整」:两行滑块 = 调整幅度(-400% … +400%)/ 迭代次数(1 … 自定上限)。
|
||||||
|
// 语义与该面板旧式排版完全一致,只是外观改成与均值滤波同款的可滚动列表。
|
||||||
|
if (boostMode)
|
||||||
|
{
|
||||||
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||||
|
{
|
||||||
|
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||||
|
if (!insideContent)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const Lane::WeakPeakItem* peakItem = meanLane.FindWeakPeakItem(ref);
|
||||||
|
const bool hasParams =
|
||||||
|
peakItem != nullptr && rowIndex < layout.rowLevelTracks.size() &&
|
||||||
|
layout.rowLevelTracks[rowIndex].Width > 0.0f;
|
||||||
|
if (!hasParams)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Lane::WeakPeakItem& item = meanLane.EnsureWeakPeakItem(ref);
|
||||||
|
// 复位:调整幅度 → 0%
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// 复位:迭代次数 → 1(上限恢复为默认上限)。
|
||||||
|
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 (rowIndex < layout.rowIterValues.size() &&
|
||||||
|
layout.rowIterValues[rowIndex].Width > 0.0f &&
|
||||||
|
UiPointInRect(layout.rowIterValues[rowIndex], fx, fy))
|
||||||
|
{
|
||||||
|
m_editMeanRef = ref;
|
||||||
|
m_peakEditCurrentMax = item.maxIterations;
|
||||||
|
const gd::RectF& track = layout.rowIterTracks[rowIndex];
|
||||||
|
const gd::RectF& value = layout.rowIterValues[rowIndex];
|
||||||
|
const gd::RectF rowRect(track.X, value.Y, value.X + value.Width - track.X,
|
||||||
|
value.Height);
|
||||||
|
BeginEdit(PlotEditTarget::PEAK_ITERATIONS_MAX, index, L"", rowRect);
|
||||||
|
m_edit.Begin(L"", false, EditFilter::BYTE, 0u);
|
||||||
|
m_dragMode = PlotDragMode::NONE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 拖动「迭代次数」滑块。
|
||||||
|
if (rowIndex < layout.rowIterTracks.size() &&
|
||||||
|
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale, fx, fy))
|
||||||
|
{
|
||||||
|
m_dragMode = PlotDragMode::PEAK_ITERATIONS;
|
||||||
|
m_peakDragLane = index;
|
||||||
|
m_peakDragRef = ref;
|
||||||
|
const double ratio =
|
||||||
|
UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
|
||||||
|
const int peakMax = std::max(
|
||||||
|
kWeakPeakIterationsMin,
|
||||||
|
std::min(kWeakPeakIterationsMaxLimit, item.maxIterations));
|
||||||
|
item.iterations = std::max(
|
||||||
|
kWeakPeakIterationsMin,
|
||||||
|
std::min(peakMax,
|
||||||
|
kWeakPeakIterationsMin +
|
||||||
|
static_cast<int>(std::lround(
|
||||||
|
ratio * (peakMax - kWeakPeakIterationsMin)))));
|
||||||
|
m_weakPeakCurveCache.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 拖动「调整幅度」滑块(0 在正中间 ⇒ ±400%)。
|
||||||
|
if (rowIndex < layout.rowLevelTracks.size() &&
|
||||||
|
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
|
||||||
|
{
|
||||||
|
m_dragMode = PlotDragMode::PEAK_LEVEL;
|
||||||
|
m_peakDragLane = index;
|
||||||
|
m_peakDragRef = ref;
|
||||||
|
item.amountPercent = static_cast<int>(std::lround(
|
||||||
|
(UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx) * 2.0 - 1.0) *
|
||||||
|
100.0 * kWeakPeakSliderScale));
|
||||||
|
item.amountPercent = std::max(
|
||||||
|
kWeakPeakAmountMin,
|
||||||
|
std::min(kWeakPeakAmountMax, item.amountPercent));
|
||||||
|
m_weakPeakCurveCache.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||||
{
|
{
|
||||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||||
@@ -2433,7 +2554,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
m_editMeanRef = ref;
|
m_editMeanRef = ref;
|
||||||
m_meanEditCurrentMax = item.maxPoints;
|
m_meanEditCurrentMax = item.maxPoints;
|
||||||
m_editMeanCenter = centerMode;
|
m_editMeanKind = kind;
|
||||||
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,
|
||||||
@@ -2451,7 +2572,7 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
m_editMeanRef = ref;
|
m_editMeanRef = ref;
|
||||||
m_meanEditCurrentMax = item.maxIterations;
|
m_meanEditCurrentMax = item.maxIterations;
|
||||||
m_editMeanCenter = centerMode;
|
m_editMeanKind = kind;
|
||||||
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,
|
||||||
@@ -2472,7 +2593,7 @@ namespace jdi
|
|||||||
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;
|
m_meanDragKind = kind;
|
||||||
const double ratio = UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx);
|
const double ratio = UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx);
|
||||||
item.points = std::max(
|
item.points = std::max(
|
||||||
floorValue,
|
floorValue,
|
||||||
@@ -2495,7 +2616,7 @@ 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;
|
m_meanDragKind = kind;
|
||||||
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,
|
||||||
@@ -2511,198 +2632,84 @@ namespace jdi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 条目栏(信号色叠底那一整行):
|
|
||||||
// * 点在右端复选框上 → 切换"是否启用该信号的预览"(与复选框同一语义,
|
|
||||||
// 因为整行已被本函数消费,复选框自己收不到点击了);
|
|
||||||
// * 已启用 → 整行切换参数行的收起 / 展开;
|
|
||||||
// * **未启用 → 什么都不做**(用户 2026-10-30 明确要求:未勾选前点击条目栏
|
|
||||||
// 既不能自动勾选、也不能展开 / 收起,只能点复选框)。
|
|
||||||
// 仍然消费这次点击(不穿透到底层的平移/选区等)。
|
|
||||||
if (rowIndex < layout.rowHeaders.size() &&
|
|
||||||
layout.rowHeaders[rowIndex].Width > 0.0f &&
|
|
||||||
UiPointInRect(layout.rowHeaders[rowIndex], fx, fy))
|
|
||||||
{
|
|
||||||
const bool onCheckBox =
|
|
||||||
rowIndex < layout.rowChecks.size() &&
|
|
||||||
layout.rowChecks[rowIndex].Width > 0.0f &&
|
|
||||||
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
|
|
||||||
Lane::MeanFilterItem* existing =
|
|
||||||
const_cast<Lane::MeanFilterItem*>(findItem(ref));
|
|
||||||
if (onCheckBox)
|
|
||||||
{
|
|
||||||
if (centerMode)
|
|
||||||
{
|
|
||||||
ToggleLaneEdgeCenterSignal(index, ref);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ToggleLaneMeanFilterSignal(index, ref);
|
|
||||||
}
|
|
||||||
m_meanRowHoverLane = -1;
|
|
||||||
m_meanRowHoverRef = SignalRef();
|
|
||||||
}
|
|
||||||
else if (existing != nullptr)
|
|
||||||
{
|
|
||||||
// 已启用:整行切换参数行的收起 / 展开。
|
|
||||||
existing->collapsed = !existing->collapsed;
|
|
||||||
}
|
|
||||||
m_dragMode = PlotDragMode::NONE;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Lane& tabLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
|
||||||
|
|
||||||
// 「弱峰调整」:先判两行滑块(数值文本 → 改上限;轨道 → 拖动)。
|
|
||||||
if (boostMode)
|
|
||||||
{
|
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
|
||||||
{
|
|
||||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
|
||||||
const bool hasLevel = rowIndex < layout.rowLevelTracks.size() &&
|
|
||||||
layout.rowLevelTracks[rowIndex].Width > 0.0f;
|
|
||||||
if (!hasLevel)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (tabLane.FindWeakPeakItem(ref) == nullptr)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Lane::WeakPeakItem& item = tabLane.EnsureWeakPeakItem(ref);
|
|
||||||
// 两行滑块右侧的**复位控件**:点击恢复该行参数的默认值
|
|
||||||
//(幅度 → 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 &&
|
|
||||||
UiPointInRect(layout.rowIterValues[rowIndex], fx, fy))
|
|
||||||
{
|
|
||||||
m_editMeanRef = ref;
|
|
||||||
m_peakEditCurrentMax = item.maxIterations;
|
|
||||||
// 输入框以**空内容**开始:空内容时显示当前上限作为灰色提示。
|
|
||||||
// 输入框覆盖"整条滑块行"(从轨道左端到数值文本右端)。
|
|
||||||
const gd::RectF& track = layout.rowIterTracks[rowIndex];
|
|
||||||
const gd::RectF& value = layout.rowIterValues[rowIndex];
|
|
||||||
const gd::RectF rowRect(track.X, value.Y, value.X + value.Width - track.X,
|
|
||||||
value.Height);
|
|
||||||
BeginEdit(PlotEditTarget::PEAK_ITERATIONS_MAX, index, L"", rowRect);
|
|
||||||
// `BeginEdit` 默认用 NUMBER 过滤(允许负号/小数点);这里改为**纯数字**,
|
|
||||||
// 从源头保证"只允许输入正整数"(负数、小数根本无法键入,粘贴同样被清洗)。
|
|
||||||
m_edit.Begin(L"", false, EditFilter::BYTE, 0u);
|
|
||||||
m_dragMode = PlotDragMode::NONE;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (layout.rowIterTracks[rowIndex].Width > 0.0f &&
|
|
||||||
WeakPeakSliderHit(layout.rowIterTracks[rowIndex], m_scale, fx, fy))
|
|
||||||
{
|
|
||||||
m_dragMode = PlotDragMode::PEAK_ITERATIONS;
|
|
||||||
m_peakDragLane = index;
|
|
||||||
m_peakDragRef = ref;
|
|
||||||
const double ratio = UiSliderRatioFromX(layout.rowIterTracks[rowIndex], fx);
|
|
||||||
const int peakMax = std::max(kWeakPeakIterationsMin,
|
|
||||||
std::min(kWeakPeakIterationsMaxLimit,
|
|
||||||
item.maxIterations));
|
|
||||||
item.iterations = std::max(
|
|
||||||
kWeakPeakIterationsMin,
|
|
||||||
std::min(peakMax, kWeakPeakIterationsMin +
|
|
||||||
static_cast<int>(std::lround(
|
|
||||||
ratio * (peakMax - kWeakPeakIterationsMin)))));
|
|
||||||
m_weakPeakCurveCache.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (layout.rowLevelTracks[rowIndex].Width > 0.0f &&
|
|
||||||
WeakPeakSliderHit(layout.rowLevelTracks[rowIndex], m_scale, fx, fy))
|
|
||||||
{
|
|
||||||
m_dragMode = PlotDragMode::PEAK_LEVEL;
|
|
||||||
m_peakDragLane = index;
|
|
||||||
m_peakDragRef = ref;
|
|
||||||
// 单击轨道:直接写入该处对应的幅度(-400% … +400%,0 在正中间)。
|
|
||||||
item.amountPercent = static_cast<int>(std::lround(
|
|
||||||
(UiSliderRatioFromX(layout.rowLevelTracks[rowIndex], fx) * 2.0 - 1.0) *
|
|
||||||
100.0 * kWeakPeakSliderScale));
|
|
||||||
item.amountPercent = std::max(
|
|
||||||
kWeakPeakAmountMin,
|
|
||||||
std::min(kWeakPeakAmountMax, item.amountPercent));
|
|
||||||
m_weakPeakCurveCache.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 条目栏(信号色叠底那一整行):
|
||||||
|
// * 点在右端复选框上 → 切换「是否启用该信号的预览」;
|
||||||
|
// * 已启用 → 整行切换参数行的收起 / 展开;
|
||||||
|
// * **未启用 → 什么都不做**(用户 2026-10-30 明确要求:未勾选前点击条目栏
|
||||||
|
// 既不能自动勾选、也不能展开 / 收起,只能点复选框;此时鼠标指针也保持箭头)。
|
||||||
|
// 仍然消费这次点击(不穿透到底层的平移 / 选区等)。
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||||
{
|
{
|
||||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
if (!insideContent)
|
||||||
// 第一排右侧的复选框 → 切换该信号的预览勾选状态。
|
|
||||||
if (layout.rowChecks[rowIndex].Width > 0.0f &&
|
|
||||||
UiPointInRect(layout.rowChecks[rowIndex], fx, fy))
|
|
||||||
{
|
{
|
||||||
if (meanMode)
|
break;
|
||||||
{
|
}
|
||||||
ToggleLaneMeanFilterSignal(index, ref);
|
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||||
}
|
if (rowIndex >= layout.rowHeaders.size() ||
|
||||||
else if (boostMode)
|
layout.rowHeaders[rowIndex].Width <= 0.0f ||
|
||||||
|
!UiPointInRect(layout.rowHeaders[rowIndex], fx, fy))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool onCheckBox =
|
||||||
|
rowIndex < layout.rowChecks.size() &&
|
||||||
|
layout.rowChecks[rowIndex].Width > 0.0f &&
|
||||||
|
UiPointInRect(layout.rowChecks[rowIndex], fx, fy);
|
||||||
|
bool checked = false;
|
||||||
|
bool collapsed = false;
|
||||||
|
if (boostMode)
|
||||||
|
{
|
||||||
|
const Lane::WeakPeakItem* peakItem = meanLane.FindWeakPeakItem(ref);
|
||||||
|
checked = (peakItem != nullptr);
|
||||||
|
collapsed = (peakItem != nullptr && peakItem->collapsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const Lane::MeanFilterItem* meanItem = findItem(ref);
|
||||||
|
checked = (meanItem != nullptr);
|
||||||
|
collapsed = (meanItem != nullptr && meanItem->collapsed);
|
||||||
|
}
|
||||||
|
if (onCheckBox)
|
||||||
|
{
|
||||||
|
if (boostMode)
|
||||||
{
|
{
|
||||||
ToggleLaneWeakPeakSignal(index, ref);
|
ToggleLaneWeakPeakSignal(index, ref);
|
||||||
}
|
}
|
||||||
else
|
else if (centerMode)
|
||||||
{
|
{
|
||||||
ToggleLaneEdgeCenterSignal(index, ref);
|
ToggleLaneEdgeCenterSignal(index, ref);
|
||||||
}
|
}
|
||||||
// 勾选变化只改变该信号“是否显示”,它的统计状态(与参数相关)依然有效,
|
else
|
||||||
// 其它信号更是完全无关 ⇒ **不要**清任何缓存(清掉就会让所有统计重算)。
|
{
|
||||||
// 这里**不**整体作废场景缓存(交给 `m_sceneDerived` 快照驱动局部重画)。
|
ToggleLaneMeanFilterSignal(index, ref);
|
||||||
m_dragMode = PlotDragMode::NONE;
|
}
|
||||||
return true;
|
m_meanRowHoverLane = -1;
|
||||||
|
m_meanRowHoverRef = SignalRef();
|
||||||
}
|
}
|
||||||
// 第二排的两个输入框 → 就地编辑该信号的参数(仅 MEAN / CENTER 面板;
|
else if (checked)
|
||||||
// 弱峰调整用的是滑块,已在上面处理)。
|
|
||||||
if (layout.rowPoints[rowIndex].Width > 0.0f &&
|
|
||||||
UiPointInRect(layout.rowPoints[rowIndex], fx, fy))
|
|
||||||
{
|
{
|
||||||
m_editMeanRef = ref;
|
// 已启用:整行切换参数行的收起 / 展开。
|
||||||
const Lane::MeanFilterItem* item =
|
if (boostMode)
|
||||||
meanMode ? tabLane.FindMeanFilterItem(ref)
|
{
|
||||||
: tabLane.FindEdgeCenterItem(ref);
|
Lane::WeakPeakItem* item =
|
||||||
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_POINTS
|
const_cast<Lane::WeakPeakItem*>(meanLane.FindWeakPeakItem(ref));
|
||||||
: PlotEditTarget::EDGE_CENTER_POINTS,
|
item->collapsed = !collapsed;
|
||||||
index, std::to_wstring(item != nullptr ? item->points : 5),
|
}
|
||||||
layout.rowPoints[rowIndex]);
|
else
|
||||||
return true;
|
{
|
||||||
}
|
Lane::MeanFilterItem* item =
|
||||||
if (layout.rowIterations[rowIndex].Width > 0.0f &&
|
const_cast<Lane::MeanFilterItem*>(findItem(ref));
|
||||||
UiPointInRect(layout.rowIterations[rowIndex], fx, fy))
|
item->collapsed = !collapsed;
|
||||||
{
|
}
|
||||||
m_editMeanRef = ref;
|
|
||||||
const Lane::MeanFilterItem* item =
|
|
||||||
meanMode ? tabLane.FindMeanFilterItem(ref)
|
|
||||||
: tabLane.FindEdgeCenterItem(ref);
|
|
||||||
BeginEdit(meanMode ? PlotEditTarget::MEAN_FILTER_ITERATIONS
|
|
||||||
: PlotEditTarget::EDGE_CENTER_ITERATIONS,
|
|
||||||
index, std::to_wstring(item != nullptr ? item->iterations : 1),
|
|
||||||
layout.rowIterations[rowIndex]);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
m_dragMode = PlotDragMode::NONE;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (boostMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
|
if (boostMode && layout.button.Width > 0.0f && UiPointInRect(layout.button, fx, fy))
|
||||||
{
|
{
|
||||||
@@ -3208,7 +3215,7 @@ namespace jdi
|
|||||||
// 「均值滤波 / 中心轴边沿计数」参数滑块拖拽结束:同样清掉目标。
|
// 「均值滤波 / 中心轴边沿计数」参数滑块拖拽结束:同样清掉目标。
|
||||||
m_meanDragLane = -1;
|
m_meanDragLane = -1;
|
||||||
m_meanDragRef = SignalRef();
|
m_meanDragRef = SignalRef();
|
||||||
m_meanDragCenter = false;
|
m_meanDragKind = EdgePanelKind::MEAN;
|
||||||
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
// 「标记透明度」滑块 / 面板内容滚动条拖拽结束。
|
||||||
m_markerOpacityDragLane = -1;
|
m_markerOpacityDragLane = -1;
|
||||||
m_markerScrollDragLane = -1;
|
m_markerScrollDragLane = -1;
|
||||||
@@ -3260,9 +3267,10 @@ namespace jdi
|
|||||||
panelLane, panelKind))
|
panelLane, panelKind))
|
||||||
{
|
{
|
||||||
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
|
const EdgePanelKind hoveredKind = static_cast<EdgePanelKind>(panelKind);
|
||||||
// 「均值滤波 / 中心轴边沿计数」面板:内容区超高时用滚轮翻页
|
// 「均值滤波 / 中心轴边沿计数 / 弱峰调整 / 边沿计数」面板:内容区超高时用滚轮翻页
|
||||||
//(上百个信号合并时必备)。
|
//(上百个信号合并时必备)。
|
||||||
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER)
|
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER ||
|
||||||
|
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT)
|
||||||
{
|
{
|
||||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
|
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
|
||||||
if (layout.content.Width > 0.0f &&
|
if (layout.content.Width > 0.0f &&
|
||||||
@@ -3277,16 +3285,27 @@ namespace jdi
|
|||||||
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
|
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
|
||||||
const float current = (hoveredKind == EdgePanelKind::CENTER)
|
const float current = (hoveredKind == EdgePanelKind::CENTER)
|
||||||
? wheelLane.edgeCenterScroll
|
? wheelLane.edgeCenterScroll
|
||||||
|
: (hoveredKind == EdgePanelKind::PEAK)
|
||||||
|
? wheelLane.weakPeakScroll
|
||||||
|
: (hoveredKind == EdgePanelKind::COUNT)
|
||||||
|
? wheelLane.edgeCountScroll
|
||||||
: wheelLane.meanFilterScroll;
|
: 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);
|
||||||
const float target = std::max(0.0f, std::min(next, maxScroll));
|
const float target = std::max(0.0f, std::min(next, maxScroll));
|
||||||
if (hoveredKind == EdgePanelKind::CENTER)
|
switch (hoveredKind)
|
||||||
{
|
{
|
||||||
|
case EdgePanelKind::CENTER:
|
||||||
SetLaneEdgeCenterScroll(panelLane, target);
|
SetLaneEdgeCenterScroll(panelLane, target);
|
||||||
}
|
break;
|
||||||
else
|
case EdgePanelKind::PEAK:
|
||||||
{
|
SetLaneWeakPeakScroll(panelLane, target);
|
||||||
|
break;
|
||||||
|
case EdgePanelKind::COUNT:
|
||||||
|
SetLaneEdgeCountScroll(panelLane, target);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
SetLaneMeanFilterScroll(panelLane, target);
|
SetLaneMeanFilterScroll(panelLane, target);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4171,5 +4190,4 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|
||||||
@@ -3774,6 +3774,8 @@ namespace jdi
|
|||||||
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->amountPercent + 2 : 0));
|
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->amountPercent + 2 : 0));
|
||||||
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->iterations + 2 : 0));
|
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->iterations + 2 : 0));
|
||||||
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
|
mix(static_cast<std::uint64_t>(peakItem != nullptr ? peakItem->maxIterations + 2 : 0));
|
||||||
|
// 「弱峰调整」的条目栏三角(展开 / 收起)直接改变面板像素 ⇒ 必须进键。
|
||||||
|
mix((peakItem != nullptr && peakItem->collapsed) ? 1ull : 0ull);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,必须进缓存键**(用户报告的缺陷根因):
|
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,必须进缓存键**(用户报告的缺陷根因):
|
||||||
@@ -4381,21 +4383,25 @@ namespace jdi
|
|||||||
// ⚠️ 未完成时**两个数字后面都带 `…`**(旧实现只在整行末尾追加一次,
|
// ⚠️ 未完成时**两个数字后面都带 `…`**(旧实现只在整行末尾追加一次,
|
||||||
// 视觉上只跟在“下降沿”的数字后面;用户明确要求为“上升沿”也补上这个尾缀)。
|
// 视觉上只跟在“下降沿”的数字后面;用户明确要求为“上升沿”也补上这个尾缀)。
|
||||||
const auto paintCounts = [this, &g](const gd::RectF& rect, std::size_t rising,
|
const auto paintCounts = [this, &g](const gd::RectF& rect, std::size_t rising,
|
||||||
std::size_t falling, bool complete)
|
std::size_t falling, bool complete, const gd::RectF& clip)
|
||||||
{
|
{
|
||||||
|
// ⚠️ **必须用 `UiTextClipped`**:GDI 文本不受 GDI+ `SetClip` 约束,
|
||||||
|
// 内容区滚动时统计文字会画到面板标题栏 / 面板之外
|
||||||
|
//(用户 2026-11-02 报告“滚动时绘制超出了浮动面板”的直接原因)。
|
||||||
if (EdgeCountsPending(rising, falling, complete))
|
if (EdgeCountsPending(rising, falling, complete))
|
||||||
{
|
{
|
||||||
UiText(g, L"统计中…", m_fonts->monoSmall, rect, m_theme->textMuted, TextAlign::LEFT);
|
UiTextClipped(g, L"统计中…", m_fonts->monoSmall, rect, m_theme->textMuted,
|
||||||
|
TextAlign::LEFT, clip);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const float gap = kEdgePanelStatColumnGap * m_scale;
|
const float gap = kEdgePanelStatColumnGap * m_scale;
|
||||||
const float columnWidth = std::max(10.0f, (rect.Width - gap) * 0.5f);
|
const float columnWidth = std::max(10.0f, (rect.Width - gap) * 0.5f);
|
||||||
UiText(g, EdgeRisingText(rising, complete), m_fonts->monoSmall,
|
UiTextClipped(g, EdgeRisingText(rising, complete), m_fonts->monoSmall,
|
||||||
gd::RectF(rect.X, rect.Y, columnWidth, rect.Height), m_theme->textMuted,
|
gd::RectF(rect.X, rect.Y, columnWidth, rect.Height), m_theme->textMuted,
|
||||||
TextAlign::LEFT);
|
TextAlign::LEFT, clip);
|
||||||
UiText(g, EdgeFallingText(falling, complete), m_fonts->monoSmall,
|
UiTextClipped(g, EdgeFallingText(falling, complete), m_fonts->monoSmall,
|
||||||
gd::RectF(rect.X + columnWidth + gap, rect.Y, columnWidth, rect.Height),
|
gd::RectF(rect.X + columnWidth + gap, rect.Y, columnWidth, rect.Height),
|
||||||
m_theme->textMuted, TextAlign::LEFT);
|
m_theme->textMuted, TextAlign::LEFT, clip);
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<PanelSlot> nextSlots;
|
std::vector<PanelSlot> nextSlots;
|
||||||
@@ -4483,8 +4489,7 @@ namespace jdi
|
|||||||
// **可滚动分支**(`if (listStyle)` 内)。若它们仍被算作 `itemized`,旧分支会
|
// **可滚动分支**(`if (listStyle)` 内)。若它们仍被算作 `itemized`,旧分支会
|
||||||
// **再画一遍**每行的名称 + 复选框 —— 且旧分支**没有任何裁切**,滚动时这些元素就会
|
// **再画一遍**每行的名称 + 复选框 —— 且旧分支**没有任何裁切**,滚动时这些元素就会
|
||||||
// 画到标题栏上(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
|
// 画到标题栏上(用户 2026-10-30 报告“元素超出浮动面板”的根因)。
|
||||||
const bool listStyle = (meanFilter || centerAxis);
|
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||||
const bool itemized = weakPeak;
|
|
||||||
const bool hasButton = (meanFilter || weakPeak);
|
const bool hasButton = (meanFilter || weakPeak);
|
||||||
{
|
{
|
||||||
if (index >= count)
|
if (index >= count)
|
||||||
@@ -4715,225 +4720,6 @@ namespace jdi
|
|||||||
// 折起的面板只保留标题栏:内容区(条目 / 分组栏 / 按钮 / 滚动条)全部省略。
|
// 折起的面板只保留标题栏:内容区(条目 / 分组栏 / 按钮 / 滚动条)全部省略。
|
||||||
if (!layout.collapsed)
|
if (!layout.collapsed)
|
||||||
{
|
{
|
||||||
// 「中心轴边沿计数」/「均值滤波」/「弱峰调整」面板都采用「逐条目」排布:
|
|
||||||
// 第一排 = 名称 + 复选框,第二排 = 两个参数输入框(仅勾选的条目,
|
|
||||||
// 弱峰调整没有参数输入框),中心轴再多第三排 = 对应的统计结果。
|
|
||||||
if (itemized)
|
|
||||||
{
|
|
||||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
|
||||||
{
|
|
||||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
|
||||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
|
||||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
|
||||||
const gd::RectF& checkRect = layout.rowChecks[rowIndex];
|
|
||||||
const bool checked = meanFilter ? tabLane.MeanFilterEnabled(ref)
|
|
||||||
: (weakPeak ? tabLane.WeakPeakEnabled(ref)
|
|
||||||
: tabLane.EdgeCenterEnabled(ref));
|
|
||||||
|
|
||||||
// 名称:第一排左侧,避开右侧复选框(超长时省略)。
|
|
||||||
const float nameWidth =
|
|
||||||
std::min(std::max(20.0f, checkRect.X - (panel.X + pad) - 6.0f * m_scale),
|
|
||||||
kEdgePanelNameMaxWidth * m_scale);
|
|
||||||
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameWidth),
|
|
||||||
m_fonts->uiSmall,
|
|
||||||
gd::RectF(panel.X + pad, checkRect.Y - 3.0f * m_scale, nameWidth,
|
|
||||||
checkRect.Height + 6.0f * m_scale),
|
|
||||||
signalColor, TextAlign::LEFT);
|
|
||||||
UiDrawCheckBox(g, checkRect, checked ? 1 : 0, 3.0f * m_scale);
|
|
||||||
|
|
||||||
if (!checked)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MEAN / CENTER 的平滑参数条目(弱峰调整用自己的 `WeakPeakItem`)。
|
|
||||||
const Lane::MeanFilterItem* item =
|
|
||||||
(meanFilter || centerAxis)
|
|
||||||
? (meanFilter ? tabLane.FindMeanFilterItem(ref)
|
|
||||||
: tabLane.FindEdgeCenterItem(ref))
|
|
||||||
: nullptr;
|
|
||||||
|
|
||||||
// 「弱峰调整」:每个被勾选的信号各有两行**滑块**
|
|
||||||
//(调整幅度 -200% – +200% / 迭代次数 1 … 该信号自定义上限)。
|
|
||||||
// 迭代次数行的右侧数值可点击 → 整行变为输入框以修改滑块上限。
|
|
||||||
if (weakPeak)
|
|
||||||
{
|
|
||||||
const Lane::WeakPeakItem* peakItem = tabLane.FindWeakPeakItem(ref);
|
|
||||||
if (peakItem != nullptr && rowIndex < layout.rowLevelTracks.size() &&
|
|
||||||
layout.rowLevelTracks[rowIndex].Width > 0.0f)
|
|
||||||
{
|
|
||||||
// 两行滑块的「标签」始终绘制(编辑上限时也要能看出在改哪一项);
|
|
||||||
// 「轨道 + 数值」在编辑上限时改由 `PaintEditField` 画成输入框。
|
|
||||||
const auto paintSliderLabel = [&](const gd::RectF& track,
|
|
||||||
const wchar_t* label)
|
|
||||||
{
|
|
||||||
const float labelColumn =
|
|
||||||
WeakPeakLabelColumnWidth(m_fonts->uiSmall);
|
|
||||||
const float labelLeft =
|
|
||||||
track.X - 6.0f * m_scale - labelColumn;
|
|
||||||
UiText(g, label, m_fonts->uiSmall,
|
|
||||||
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
|
|
||||||
labelColumn, track.Height + 16.0f * m_scale),
|
|
||||||
m_theme->textMuted, TextAlign::LEFT);
|
|
||||||
};
|
|
||||||
const auto paintSliderBody = [&](const gd::RectF& track, double ratio,
|
|
||||||
const std::wstring& valueText,
|
|
||||||
bool active)
|
|
||||||
{
|
|
||||||
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
|
|
||||||
UiText(g, valueText, m_fonts->monoSmall,
|
|
||||||
gd::RectF(track.X + track.Width + 6.0f * m_scale,
|
|
||||||
track.Y - 8.0f * m_scale,
|
|
||||||
WeakPeakValueColumnWidth(m_fonts->monoSmall),
|
|
||||||
track.Height + 16.0f * m_scale),
|
|
||||||
m_theme->textStrong, TextAlign::LEFT);
|
|
||||||
};
|
|
||||||
// 每行滑块右侧的**复位控件**(与「系数/偏移」二级面板里的还原图标同一套绘制):
|
|
||||||
// 悬停时高亮 + 手型光标,点击把该行参数恢复为默认值(幅度 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, kWeakPeakIterationsSliderLabel);
|
|
||||||
|
|
||||||
const bool levelActive = (m_dragMode == PlotDragMode::PEAK_LEVEL) &&
|
|
||||||
(m_peakDragLane == static_cast<int>(index)) &&
|
|
||||||
(m_peakDragRef == ref);
|
|
||||||
const double levelRatio =
|
|
||||||
(static_cast<double>(std::max(kWeakPeakAmountMin,
|
|
||||||
std::min(kWeakPeakAmountMax,
|
|
||||||
peakItem->amountPercent))) /
|
|
||||||
(100.0 * kWeakPeakSliderScale) +
|
|
||||||
1.0) *
|
|
||||||
0.5;
|
|
||||||
paintSliderBody(levelTrack, levelRatio,
|
|
||||||
(peakItem->amountPercent > 0 ? L"+" : L"") +
|
|
||||||
std::to_wstring(peakItem->amountPercent) + L"%",
|
|
||||||
levelActive);
|
|
||||||
paintSliderReset(layout.rowLevelResets[rowIndex]);
|
|
||||||
|
|
||||||
// 迭代次数滑块:量程 = 1 … 该信号自己的上限。
|
|
||||||
const int peakMax = std::max(kWeakPeakIterationsMin,
|
|
||||||
std::min(kWeakPeakIterationsMaxLimit,
|
|
||||||
peakItem->maxIterations));
|
|
||||||
const int peakIter = std::max(
|
|
||||||
kWeakPeakIterationsMin, std::min(peakMax, peakItem->iterations));
|
|
||||||
const double iterRatio =
|
|
||||||
(peakMax > kWeakPeakIterationsMin)
|
|
||||||
? (static_cast<double>(peakIter - kWeakPeakIterationsMin) /
|
|
||||||
static_cast<double>(peakMax - kWeakPeakIterationsMin))
|
|
||||||
: 0.0;
|
|
||||||
const bool iterActive = (m_dragMode == PlotDragMode::PEAK_ITERATIONS) &&
|
|
||||||
(m_peakDragLane == static_cast<int>(index)) &&
|
|
||||||
(m_peakDragRef == ref);
|
|
||||||
const bool editingMax =
|
|
||||||
(m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX) &&
|
|
||||||
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
|
|
||||||
if (!editingMax)
|
|
||||||
{
|
|
||||||
paintSliderBody(iterTrack, iterRatio, std::to_wstring(peakIter),
|
|
||||||
iterActive);
|
|
||||||
paintSliderReset(layout.rowIterResets[rowIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MEAN / CENTER:第二排的两个参数输入框。
|
|
||||||
if (!weakPeak)
|
|
||||||
{
|
|
||||||
const int shownPoints = (item != nullptr) ? item->points : 5;
|
|
||||||
const int shownOrder = (item != nullptr) ? item->iterations : 1;
|
|
||||||
const PlotEditTarget pointsTarget =
|
|
||||||
meanFilter ? PlotEditTarget::MEAN_FILTER_POINTS
|
|
||||||
: PlotEditTarget::EDGE_CENTER_POINTS;
|
|
||||||
const PlotEditTarget orderTarget =
|
|
||||||
meanFilter ? PlotEditTarget::MEAN_FILTER_ITERATIONS
|
|
||||||
: PlotEditTarget::EDGE_CENTER_ITERATIONS;
|
|
||||||
const auto paintField = [&](const gd::RectF& rect, const wchar_t* label,
|
|
||||||
int value, PlotEditTarget target)
|
|
||||||
{
|
|
||||||
if (rect.Width <= 0.0f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
|
|
||||||
UiText(g, label, m_fonts->uiSmall,
|
|
||||||
gd::RectF(rect.X, rect.Y, labelWidth, rect.Height), m_theme->textMuted,
|
|
||||||
TextAlign::LEFT);
|
|
||||||
|
|
||||||
const float fieldWidth =
|
|
||||||
std::max(kEdgePanelEditFieldWidth * m_scale * 0.75f,
|
|
||||||
UiMeasureTextWidth(m_fonts->monoSmall, std::to_wstring(value)) +
|
|
||||||
18.0f * m_scale);
|
|
||||||
const float fieldLeft = rect.X + rect.Width - fieldWidth;
|
|
||||||
const float fieldHeight = rect.Height - 4.0f * m_scale;
|
|
||||||
const gd::RectF fieldRect(fieldLeft, rect.Y + 2.0f * m_scale, fieldWidth,
|
|
||||||
fieldHeight);
|
|
||||||
|
|
||||||
const bool editingThis = (m_editTarget == target) &&
|
|
||||||
(m_editLane == static_cast<int>(index)) &&
|
|
||||||
(m_editMeanRef == ref);
|
|
||||||
if (editingThis)
|
|
||||||
{
|
|
||||||
UiDrawEditField(g, fieldRect, m_edit, m_fonts->monoSmall, m_scale);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UiFillRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->pressed);
|
|
||||||
UiDrawRoundRect(g, fieldRect, 3.0f * m_scale, m_theme->border, 1.0f);
|
|
||||||
UiText(g, std::to_wstring(value), m_fonts->monoSmall, fieldRect,
|
|
||||||
m_theme->textStrong, TextAlign::CENTER);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
paintField(layout.rowPoints[rowIndex], L"均值点数 X", shownPoints, pointsTarget);
|
|
||||||
paintField(layout.rowIterations[rowIndex], kWeakPeakIterationsLabel, shownOrder,
|
|
||||||
orderTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第三排(中心轴):该信号在当前参数下的上升沿/下降沿统计结果。
|
|
||||||
if (centerAxis && rowIndex < layout.rowStats.size() &&
|
|
||||||
layout.rowStats[rowIndex].Width > 0.0f)
|
|
||||||
{
|
|
||||||
const gd::RectF& statsRect = layout.rowStats[rowIndex];
|
|
||||||
const int statsPoints = (item != nullptr) ? item->points : 5;
|
|
||||||
const int statsOrder = (item != nullptr) ? item->iterations : 1;
|
|
||||||
std::size_t rising = 0;
|
|
||||||
std::size_t falling = 0;
|
|
||||||
bool statsComplete = false;
|
|
||||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style,
|
|
||||||
statsPoints, statsOrder, EdgeRangeActive(),
|
|
||||||
EdgeRangeMin(), EdgeRangeMax(), rising, falling,
|
|
||||||
statsComplete);
|
|
||||||
// 两列排版(列起点固定)⇒ 多条信号的“上升沿 / 下降沿”垂直对齐。
|
|
||||||
paintCounts(statsRect, rising, falling, statsComplete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
// 「均值滤波」面板:逐信号的**信号色叠底标题栏**(左侧展开三角 + 信号名 +
|
||||||
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
// 右端复选框)。复选框勾选后才出现三角、才能展开下面的两行参数滑块
|
||||||
@@ -5036,10 +4822,17 @@ 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);
|
||||||
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`
|
// 「均值滤波」看 `meanFilterItems`、「中心轴边沿计数」看 `edgeCenterItems`
|
||||||
// (两者都是 `Lane::MeanFilterItem`,字段完全一致)。
|
//(两者都是 `Lane::MeanFilterItem`,字段完全一致);「弱峰调整」看 `weakPeakItems`。
|
||||||
const Lane::MeanFilterItem* item = meanFilter ? tabLane.FindMeanFilterItem(ref)
|
const Lane::WeakPeakItem* peakItem =
|
||||||
: tabLane.FindEdgeCenterItem(ref);
|
weakPeak ? tabLane.FindWeakPeakItem(ref) : nullptr;
|
||||||
const bool checked = (item != nullptr);
|
const Lane::MeanFilterItem* item =
|
||||||
|
weakPeak ? nullptr
|
||||||
|
: (meanFilter ? tabLane.FindMeanFilterItem(ref)
|
||||||
|
: tabLane.FindEdgeCenterItem(ref));
|
||||||
|
const bool checked = weakPeak ? (peakItem != nullptr) : (item != nullptr);
|
||||||
|
const bool rowCollapsed =
|
||||||
|
weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||||
|
: (item != nullptr && item->collapsed);
|
||||||
|
|
||||||
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
|
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
|
||||||
// 已经完整滚出可视范围的行在上面就 `continue` 了,这里不再额外门控。
|
// 已经完整滚出可视范围的行在上面就 `continue` 了,这里不再额外门控。
|
||||||
@@ -5055,11 +4848,11 @@ namespace jdi
|
|||||||
|
|
||||||
// 左侧三角(仅勾选时存在):朝下 = 展开、朝右 = 收起。
|
// 左侧三角(仅勾选时存在):朝下 = 展开、朝右 = 收起。
|
||||||
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
|
const gd::RectF& triangle = layout.rowTriangles[rowIndex];
|
||||||
if (triangle.Width > 0.0f && item != nullptr)
|
if (triangle.Width > 0.0f && checked)
|
||||||
{
|
{
|
||||||
UiDrawTriangleIcon(g, triangle, ink.text,
|
UiDrawTriangleIcon(g, triangle, ink.text,
|
||||||
item->collapsed ? IconDirection::RIGHT
|
rowCollapsed ? IconDirection::RIGHT
|
||||||
: IconDirection::DOWN);
|
: IconDirection::DOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 名称:从三角右侧到复选框左侧。
|
// 名称:从三角右侧到复选框左侧。
|
||||||
@@ -5075,7 +4868,7 @@ namespace jdi
|
|||||||
TextAlign::LEFT, layout.content);
|
TextAlign::LEFT, layout.content);
|
||||||
paintMeanCheck(check, checked, ink, signalColor);
|
paintMeanCheck(check, checked, ink, signalColor);
|
||||||
|
|
||||||
if (!checked || item->collapsed)
|
if (!checked || rowCollapsed)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -5090,9 +4883,11 @@ namespace jdi
|
|||||||
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
|
EdgeCenterCountsFor(*resolved.axis, *resolved.data, *resolved.style, item->points,
|
||||||
item->iterations, EdgeRangeActive(), EdgeRangeMin(),
|
item->iterations, EdgeRangeActive(), EdgeRangeMin(),
|
||||||
EdgeRangeMax(), rising, falling, complete);
|
EdgeRangeMax(), rising, falling, complete);
|
||||||
paintCounts(layout.rowStats[rowIndex], rising, falling, complete);
|
paintCounts(layout.rowStats[rowIndex], rising, falling, complete,
|
||||||
|
layout.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
|
// 两行参数滑块(标签 / 轨道 / 数值 / 复位按钮)。
|
||||||
const auto paintMeanSlider = [&](const gd::RectF& track,
|
const auto paintMeanSlider = [&](const gd::RectF& track,
|
||||||
const gd::RectF& valueRect,
|
const gd::RectF& valueRect,
|
||||||
@@ -5140,6 +4935,105 @@ namespace jdi
|
|||||||
// ⚠️ 「均值点数」的最小值:**均值滤波 = 1**(用户 2026-10-30 要求不能为 0,
|
// ⚠️ 「均值点数」的最小值:**均值滤波 = 1**(用户 2026-10-30 要求不能为 0,
|
||||||
// 0 与不做平滑完全等价,看起来像功能坏了);**中心轴边沿计数 = 0**
|
// 0 与不做平滑完全等价,看起来像功能坏了);**中心轴边沿计数 = 0**
|
||||||
// (0 = 不平滑,直接对原始采样值统计边沿 —— 这是有意义的选项,保持旧语义)。
|
// (0 = 不平滑,直接对原始采样值统计边沿 —— 这是有意义的选项,保持旧语义)。
|
||||||
|
if (weakPeak)
|
||||||
|
{
|
||||||
|
// 「弱峰调整」两行滑块:调整幅度(0 在正中间 ⇒ ±400%)与迭代次数(1 … 自定上限)。
|
||||||
|
const int peakAmount = std::max(
|
||||||
|
kWeakPeakAmountMin, std::min(kWeakPeakAmountMax, peakItem->amountPercent));
|
||||||
|
const int peakMax = std::max(
|
||||||
|
kWeakPeakIterationsMin,
|
||||||
|
std::min(kWeakPeakIterationsMaxLimit, peakItem->maxIterations));
|
||||||
|
const int peakIter =
|
||||||
|
std::max(kWeakPeakIterationsMin, std::min(peakMax, peakItem->iterations));
|
||||||
|
const bool levelActive = (m_dragMode == PlotDragMode::PEAK_LEVEL) &&
|
||||||
|
(m_peakDragLane == static_cast<int>(index)) &&
|
||||||
|
(m_peakDragRef == ref);
|
||||||
|
const bool iterActive = (m_dragMode == PlotDragMode::PEAK_ITERATIONS) &&
|
||||||
|
(m_peakDragLane == static_cast<int>(index)) &&
|
||||||
|
(m_peakDragRef == ref);
|
||||||
|
const bool editingMax =
|
||||||
|
(m_editTarget == PlotEditTarget::PEAK_ITERATIONS_MAX) &&
|
||||||
|
(m_editLane == static_cast<int>(index)) && (m_editMeanRef == ref);
|
||||||
|
// 幅度滑块:比例 = (幅度 / (100*4) + 1) / 2(0% 在正中间)。
|
||||||
|
const auto paintPeakSlider = [&](const gd::RectF& track,
|
||||||
|
const gd::RectF& valueRect,
|
||||||
|
const gd::RectF& resetRect,
|
||||||
|
const wchar_t* label, double ratio,
|
||||||
|
const std::wstring& valueText, bool active,
|
||||||
|
bool hideBody)
|
||||||
|
{
|
||||||
|
const float labelLeft = track.X - 6.0f * m_scale -
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmall, label);
|
||||||
|
UiTextClipped(g, label, m_fonts->uiSmall,
|
||||||
|
gd::RectF(labelLeft, track.Y - 8.0f * m_scale,
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmall, label),
|
||||||
|
track.Height + 16.0f * m_scale),
|
||||||
|
m_theme->textMuted, TextAlign::LEFT, layout.content);
|
||||||
|
if (hideBody)
|
||||||
|
{
|
||||||
|
return; // 正在编辑上限:轨道与数值改由 `PaintEditField` 画
|
||||||
|
}
|
||||||
|
UiDrawSlider(g, track, ratio, m_theme->accent, active, m_scale);
|
||||||
|
UiTextClipped(g, valueText, m_fonts->monoSmall,
|
||||||
|
gd::RectF(valueRect.X, valueRect.Y, valueColumn,
|
||||||
|
valueRect.Height),
|
||||||
|
m_theme->textStrong, TextAlign::LEFT, layout.content);
|
||||||
|
const bool hot = (m_dragMode != PlotDragMode::EDGE_PANEL_MOVE) &&
|
||||||
|
UiPointInRect(resetRect,
|
||||||
|
static_cast<float>(m_mouseX),
|
||||||
|
static_cast<float>(m_mouseY));
|
||||||
|
if (hot)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(
|
||||||
|
g, gd::RectF(resetRect.X - 1.0f * m_scale,
|
||||||
|
resetRect.Y - 1.0f * m_scale,
|
||||||
|
resetRect.Width + 2.0f * m_scale,
|
||||||
|
resetRect.Height + 2.0f * m_scale),
|
||||||
|
3.0f * m_scale, WithAlpha(m_theme->accent, 0x33));
|
||||||
|
}
|
||||||
|
PaintPanelResetIcon(g, resetRect,
|
||||||
|
hot ? m_theme->accent : m_theme->textMuted,
|
||||||
|
m_scale);
|
||||||
|
};
|
||||||
|
const double levelRatio =
|
||||||
|
(static_cast<double>(peakAmount) / (100.0 * kWeakPeakSliderScale) + 1.0) * 0.5;
|
||||||
|
const double iterRatio =
|
||||||
|
(peakMax > kWeakPeakIterationsMin)
|
||||||
|
? (static_cast<double>(peakIter - kWeakPeakIterationsMin) /
|
||||||
|
static_cast<double>(peakMax - kWeakPeakIterationsMin))
|
||||||
|
: 0.0;
|
||||||
|
if (rowIndex < layout.rowLevelTracks.size() &&
|
||||||
|
layout.rowLevelTracks[rowIndex].Width > 0.0f)
|
||||||
|
{
|
||||||
|
paintPeakSlider(
|
||||||
|
layout.rowLevelTracks[rowIndex],
|
||||||
|
rowIndex < layout.rowLevelValues.size()
|
||||||
|
? layout.rowLevelValues[rowIndex]
|
||||||
|
: gd::RectF(),
|
||||||
|
rowIndex < layout.rowLevelResets.size()
|
||||||
|
? layout.rowLevelResets[rowIndex]
|
||||||
|
: gd::RectF(),
|
||||||
|
kWeakPeakLevelLabel, levelRatio,
|
||||||
|
(peakAmount > 0 ? L"+" : L"") + std::to_wstring(peakAmount) + L"%",
|
||||||
|
levelActive, false);
|
||||||
|
}
|
||||||
|
if (rowIndex < layout.rowIterTracks.size() &&
|
||||||
|
layout.rowIterTracks[rowIndex].Width > 0.0f)
|
||||||
|
{
|
||||||
|
paintPeakSlider(
|
||||||
|
layout.rowIterTracks[rowIndex],
|
||||||
|
rowIndex < layout.rowIterValues.size()
|
||||||
|
? layout.rowIterValues[rowIndex]
|
||||||
|
: gd::RectF(),
|
||||||
|
rowIndex < layout.rowIterResets.size()
|
||||||
|
? layout.rowIterResets[rowIndex]
|
||||||
|
: gd::RectF(),
|
||||||
|
kWeakPeakIterationsSliderLabel, iterRatio,
|
||||||
|
std::to_wstring(peakIter), iterActive, editingMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
const int pointsFloor = centerAxis ? 0 : kMeanFilterPointsMin;
|
const int pointsFloor = centerAxis ? 0 : kMeanFilterPointsMin;
|
||||||
const int pointsMax = std::max(pointsFloor,
|
const int pointsMax = std::max(pointsFloor,
|
||||||
std::min(kMeanFilterPointsMaxLimit,
|
std::min(kMeanFilterPointsMaxLimit,
|
||||||
@@ -5153,11 +5047,12 @@ 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_meanDragCenter == centerAxis);
|
(m_meanDragRef == ref) &&
|
||||||
|
(m_meanDragKind == kind);
|
||||||
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);
|
(m_meanDragKind == kind);
|
||||||
if (rowIndex < layout.rowLevelTracks.size())
|
if (rowIndex < layout.rowLevelTracks.size())
|
||||||
{
|
{
|
||||||
paintMeanSlider(layout.rowLevelTracks[rowIndex],
|
paintMeanSlider(layout.rowLevelTracks[rowIndex],
|
||||||
@@ -5182,6 +5077,8 @@ namespace jdi
|
|||||||
kMeanFilterIterationsSliderLabel, iterations,
|
kMeanFilterIterationsSliderLabel, iterations,
|
||||||
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
|
kMeanFilterIterationsMin, iterationsMax, iterationsActive);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Restore(meanSave);
|
g.Restore(meanSave);
|
||||||
@@ -5537,38 +5434,63 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float rowY = panel.Y + titleHeight + pad;
|
// 「边沿计数」:逐信号两行(名称 / 上升沿·下降沿)。内容区**可滚动且按内容区裁剪**
|
||||||
for (const SignalRef& ref : tabLane.signals)
|
//(上百个信号时内容高达上万像素:限高后出现滚动条,面板高度被绘图区夹住,
|
||||||
|
// 面板像素缓存才存得进去 —— 否则每帧整块重画,所有交互都会卡顿)。
|
||||||
{
|
{
|
||||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
const int contentState = g.Save();
|
||||||
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
|
g.SetClip(layout.content, gd::CombineModeIntersect);
|
||||||
|
float rowY = layout.content.Y - layout.scrollOffset;
|
||||||
|
for (const SignalRef& ref : tabLane.signals)
|
||||||
{
|
{
|
||||||
continue;
|
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||||
}
|
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
|
||||||
if (itemized || freeMarker || listStyle)
|
{
|
||||||
{
|
continue;
|
||||||
// 逐条目绘制已在上面完成(自由标记同样是逐条目排布,
|
}
|
||||||
// 均值滤波走自己的可滚动分支)。
|
if (freeMarker || listStyle)
|
||||||
continue;
|
{
|
||||||
}
|
// 这两种排布已在上面画完(各自有自己的可滚动分支)。
|
||||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
continue;
|
||||||
const float textWidth = panel.Width - pad * 2.0f;
|
}
|
||||||
// 名称:超长时省略(面板宽度被 `kEdgePanelNameMaxWidth` 限制,不会把面板顶得很长)。
|
if (rowY + rowHeight * 2.0f <= layout.content.Y ||
|
||||||
UiText(g,
|
rowY >= layout.content.Y + layout.content.Height)
|
||||||
UiEllipsize(resolved.data->name, m_fonts->uiSmall,
|
{
|
||||||
std::min(textWidth, kEdgePanelNameMaxWidth * m_scale)),
|
rowY += rowHeight * 2.0f; // 整行滚出可视范围
|
||||||
m_fonts->uiSmall, gd::RectF(panel.X + pad, rowY, textWidth, rowHeight),
|
continue;
|
||||||
signalColor, TextAlign::LEFT);
|
}
|
||||||
|
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||||
|
const float textWidth = layout.content.Width;
|
||||||
|
// 名称:超长时省略(面板宽度被 `kEdgePanelNameMaxWidth` 限制,不会把面板顶得很长)。
|
||||||
|
// ⚠️ GDI 文本必须走 `UiTextClipped`,否则滚动时会画到标题栏 / 面板之外。
|
||||||
|
UiTextClipped(g,
|
||||||
|
UiEllipsize(resolved.data->name, m_fonts->uiSmall,
|
||||||
|
std::min(textWidth, kEdgePanelNameMaxWidth * m_scale)),
|
||||||
|
m_fonts->uiSmall,
|
||||||
|
gd::RectF(layout.content.X, rowY, textWidth, rowHeight),
|
||||||
|
signalColor, TextAlign::LEFT, layout.content);
|
||||||
|
|
||||||
std::size_t rising = 0;
|
std::size_t rising = 0;
|
||||||
std::size_t falling = 0;
|
std::size_t falling = 0;
|
||||||
bool complete = true;
|
bool complete = true;
|
||||||
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style, tabLane.edgeValue,
|
EdgeCountsFor(*resolved.axis, *resolved.data, *resolved.style,
|
||||||
EdgeRangeActive(), EdgeRangeMin(), EdgeRangeMax(), rising, falling,
|
tabLane.edgeValue, EdgeRangeActive(), EdgeRangeMin(),
|
||||||
complete);
|
EdgeRangeMax(), rising, falling, complete);
|
||||||
paintCounts(gd::RectF(panel.X + pad, rowY + rowHeight, textWidth, rowHeight), rising,
|
paintCounts(
|
||||||
falling, complete);
|
gd::RectF(layout.content.X, rowY + rowHeight, textWidth, rowHeight), rising,
|
||||||
rowY += rowHeight * 2.0f;
|
falling, complete, layout.content);
|
||||||
|
rowY += rowHeight * 2.0f;
|
||||||
|
}
|
||||||
|
g.Restore(contentState);
|
||||||
|
|
||||||
|
// 内容滚动条(只在内容超高时出现)。
|
||||||
|
if (layout.scrollTrack.Width > 0.0f)
|
||||||
|
{
|
||||||
|
UiFillRoundRect(g, layout.scrollTrack, layout.scrollTrack.Width * 0.5f,
|
||||||
|
WithAlpha(m_theme->border, 0x88));
|
||||||
|
UiFillRoundRect(g, layout.scrollThumb, layout.scrollThumb.Width * 0.5f,
|
||||||
|
m_theme->borderStrong);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
|
// 逐条目面板底部按钮行:「构建为新信号」(悬停高亮);
|
||||||
@@ -6006,4 +5928,3 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace jdi
|
} // namespace jdi
|
||||||
|
|
||||||
@@ -28,10 +28,12 @@ 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);
|
||||||
// 「弱峰调整」仍是旧式逐条目面板(名称 + 复选框 + 两行滑块)。
|
// 「均值滤波」「中心轴边沿计数」「弱峰调整」自 2026-11-02 起全部是**可滚动的分组列表**
|
||||||
// ⚠️ 「均值滤波」与「中心轴边沿计数」已改为**可滚动的分组列表**(信号色叠底条目栏),
|
//(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行)。
|
||||||
// 它们的宽度由下面的 `groupWidth` / 滑块行分支确定。
|
// ⚠️ 旧式「逐条目」排版(名称 + 复选框 + 参数行,无滚动)已彻底删除:
|
||||||
const bool itemized = weakPeak;
|
// 上百个信号时它会算出上万像素高的面板,超出绘图区后**面板像素缓存一次都存不进去**
|
||||||
|
// ⇒ 每一帧整块重画 ⇒ 所有交互卡顿(用户 2026-11-02 报告)。
|
||||||
|
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||||
|
|
||||||
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
||||||
// + 光标区间尾缀)。
|
// + 光标区间尾缀)。
|
||||||
@@ -65,16 +67,10 @@ namespace jdi
|
|||||||
std::min(UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->name),
|
std::min(UiMeasureTextWidth(m_fonts->uiSmall, resolved.data->name),
|
||||||
kEdgePanelNameMaxWidth * m_scale);
|
kEdgePanelNameMaxWidth * m_scale);
|
||||||
nameColumnWidth = std::max(nameColumnWidth, nameWidth);
|
nameColumnWidth = std::max(nameColumnWidth, nameWidth);
|
||||||
if (itemized)
|
if (listStyle)
|
||||||
{
|
{
|
||||||
// 逐条目面板:第一排 = 名称 + 复选框;第三排(中心轴)= 统计结果。
|
// 条目 = **信号色叠底条目栏**(三角 + 名称 + 复选框)与参数行
|
||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + nameWidth +
|
//(标签 + 轨道 + 数值 + 复位按钮),右侧还要给滚动条留位。
|
||||||
kEdgePanelMeanCheckSize * m_scale + 6.0f * m_scale);
|
|
||||||
}
|
|
||||||
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 +
|
||||||
@@ -126,19 +122,22 @@ namespace jdi
|
|||||||
std::max(nameColumnWidth, countsWidth));
|
std::max(nameColumnWidth, countsWidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meanFilter || centerAxis)
|
if (listStyle)
|
||||||
{
|
{
|
||||||
// 两行滑块 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(两行共用同一列宽),
|
// 两行滑块 = 「标签列 + 轨道(最小宽度)+ 数值列 + 复位按钮」(两行共用同一列宽),
|
||||||
// 右侧再给滚动条留位(上百个信号时内容必然超高)。
|
// 右侧再给滚动条留位(上百个信号时内容必然超高)。
|
||||||
const float labelColumn =
|
const float labelColumn =
|
||||||
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
||||||
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
|
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
|
||||||
|
const float weakPeakLabelColumn =
|
||||||
|
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakLevelLabel),
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakIterationsSliderLabel));
|
||||||
const float valueColumn =
|
const float valueColumn =
|
||||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
||||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
|
UiMeasureTextWidth(m_fonts->monoSmall, L"-400%"));
|
||||||
const float sliderRowWidth = labelColumn + 6.0f * m_scale + kWeakPeakTrackMinWidth * m_scale +
|
const float sliderRowWidth = std::max(labelColumn, weakPeakLabelColumn) + 6.0f * m_scale +
|
||||||
6.0f * m_scale + valueColumn + kWeakPeakResetGap * m_scale +
|
kWeakPeakTrackMinWidth * m_scale + 6.0f * m_scale + valueColumn +
|
||||||
kWeakPeakResetButtonSize * m_scale;
|
kWeakPeakResetGap * m_scale + kWeakPeakResetButtonSize * m_scale;
|
||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + sliderRowWidth +
|
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + sliderRowWidth +
|
||||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||||
}
|
}
|
||||||
@@ -153,7 +152,7 @@ namespace jdi
|
|||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + rowWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meanFilter || weakPeak)
|
if (listStyle)
|
||||||
{
|
{
|
||||||
// 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。
|
// 底部按钮行 = 「构建为新信号」,按按钮文字实测宽度确定下限。
|
||||||
const float buttonWidth =
|
const float buttonWidth =
|
||||||
@@ -161,6 +160,13 @@ namespace jdi
|
|||||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
|
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kind == EdgePanelKind::COUNT)
|
||||||
|
{
|
||||||
|
// 「边沿计数」:右侧要给内容滚动条留位(上百个信号时内容必然超高)。
|
||||||
|
width = std::max(width, kEdgePanelPad * 3.0f * m_scale +
|
||||||
|
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||||
|
}
|
||||||
|
|
||||||
if (kind == EdgePanelKind::MARK)
|
if (kind == EdgePanelKind::MARK)
|
||||||
{
|
{
|
||||||
// 「自由标记」面板:宽度 = 六列(取「缺省列宽」「实测内容宽度」「列标题宽度」三者
|
// 「自由标记」面板:宽度 = 六列(取「缺省列宽」「实测内容宽度」「列标题宽度」三者
|
||||||
@@ -254,8 +260,7 @@ namespace jdi
|
|||||||
// ⚠️ 「中心轴边沿计数」从 2026-11-02 起同样改为**可滚动的分组列表**(用户明确要求
|
// ⚠️ 「中心轴边沿计数」从 2026-11-02 起同样改为**可滚动的分组列表**(用户明确要求
|
||||||
// “改造为与均值滤波相同的风格”)⇒ 也不走 `itemized`。它没有「构建为新信号」按钮,
|
// “改造为与均值滤波相同的风格”)⇒ 也不走 `itemized`。它没有「构建为新信号」按钮,
|
||||||
// 但内容多一行「上升沿 / 下降沿」统计。
|
// 但内容多一行「上升沿 / 下降沿」统计。
|
||||||
const bool listStyle = (meanFilter || centerAxis);
|
const bool listStyle = (meanFilter || centerAxis || weakPeak);
|
||||||
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
|
||||||
@@ -299,27 +304,27 @@ namespace jdi
|
|||||||
const float titleHeight = kEdgePanelTitleHeight * m_scale;
|
const float titleHeight = kEdgePanelTitleHeight * m_scale;
|
||||||
const float rowHeight = kEdgePanelRowHeight * m_scale;
|
const float rowHeight = kEdgePanelRowHeight * m_scale;
|
||||||
const float pad = kEdgePanelPad * m_scale;
|
const float pad = kEdgePanelPad * m_scale;
|
||||||
// 「中心轴边沿计数」「均值滤波」「弱峰调整」共用同一套「逐条目」排布,
|
// 「中心轴边沿计数」「均值滤波」「弱峰调整」共用同一套**可滚动列表**排布,
|
||||||
// 因此不再有独立的底部编辑行;只有「边沿计数」保留两行文本式排版。
|
// 因此不再有独立的底部编辑行;只有「边沿计数」保留两行文本式排版(同样可滚动)。
|
||||||
const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
|
const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
|
||||||
const float rowsPerSignal = meanFilter ? 1.0f : 2.0f;
|
const float rowsPerSignal = 2.0f; // 「边沿计数」每信号两行(名称 / 上升沿·下降沿)
|
||||||
|
// 面板底部(内容区之下)占的纵向空间:有按钮时 = pad + 按钮 + pad,
|
||||||
|
// 无按钮时只有 pad。⚠️ 旧实现无条件按「按钮行 + 三个 pad」预留 ——
|
||||||
|
// 无按钮的面板(中心轴边沿计数)因此在内容区下方留下**一大块空白**
|
||||||
|
//(= 按钮高度 30 逻辑像素 + 多出的 pad,用户 2026-11-02 报告)。
|
||||||
|
const float footerHeight = hasButton ? (buttonHeight + pad) : 0.0f;
|
||||||
|
const float bottomPad = pad;
|
||||||
|
|
||||||
// 逐条目面板:每个条目 = 第一排(信号名 + 复选框)
|
|
||||||
// + 参数部分(**只在该条目被勾选时才占位**,因此面板高度随勾选数量动态变化):
|
|
||||||
// * MEAN / CENTER:一行「均值点数 X + 迭代次数 N」输入框(中心轴再多一行统计结果);
|
|
||||||
// * PEAK:两行滑块(调整幅度 % / 迭代次数 N),每个信号各有一套自己的参数。
|
|
||||||
const bool itemHasParams = (centerAxis || weakPeak);
|
|
||||||
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
|
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||||
float meanRowHeight = 0.0f;
|
|
||||||
float itemsHeight = 0.0f;
|
|
||||||
float meanContentHeight = 0.0f;
|
float meanContentHeight = 0.0f;
|
||||||
float height = 0.0f;
|
float height = 0.0f;
|
||||||
if (listStyle)
|
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;
|
||||||
@@ -331,9 +336,19 @@ namespace jdi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
content += groupRow;
|
content += groupRow;
|
||||||
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
bool expanded = false;
|
||||||
: lane.FindEdgeCenterItem(ref);
|
if (weakPeak)
|
||||||
if (item != nullptr && !item->collapsed)
|
{
|
||||||
|
const Lane::WeakPeakItem* item = lane.FindWeakPeakItem(ref);
|
||||||
|
expanded = (item != nullptr && !item->collapsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
||||||
|
: lane.FindEdgeCenterItem(ref);
|
||||||
|
expanded = (item != nullptr && !item->collapsed);
|
||||||
|
}
|
||||||
|
if (expanded)
|
||||||
{
|
{
|
||||||
content += sliderRow * 2.0f;
|
content += sliderRow * 2.0f;
|
||||||
if (centerAxis)
|
if (centerAxis)
|
||||||
@@ -343,50 +358,29 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
meanContentHeight = content;
|
meanContentHeight = content;
|
||||||
const float budget = m_plotRect.Height - (titleHeight + pad * 3.0f + buttonHeight);
|
const float budget =
|
||||||
|
m_plotRect.Height - (titleHeight + pad + bottomPad + footerHeight);
|
||||||
const float maxContent =
|
const float maxContent =
|
||||||
std::max(3.0f * groupRow,
|
std::max(3.0f * groupRow,
|
||||||
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(3.0f * groupRow, budget)));
|
std::min(kFreeMarkerContentMaxHeight * m_scale, std::max(3.0f * groupRow, budget)));
|
||||||
const float view = std::min(content, maxContent);
|
const float view = std::min(content, maxContent);
|
||||||
height = titleHeight + pad + view + pad + buttonHeight + pad;
|
height = titleHeight + pad + view + bottomPad + footerHeight;
|
||||||
}
|
}
|
||||||
else if (itemized)
|
else if (freeMarker)
|
||||||
{
|
{
|
||||||
meanRowHeight = kEdgePanelMeanRowHeight * m_scale;
|
// 「自由标记」的高度在下面的 `if (freeMarker)` 分支里计算(含底部固定区)。
|
||||||
for (const SignalRef& ref : lane.signals)
|
height = titleHeight;
|
||||||
{
|
|
||||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
|
||||||
if (resolved.data == nullptr || !resolved.visible)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
itemsHeight += meanRowHeight;
|
|
||||||
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref)
|
|
||||||
: (weakPeak ? lane.WeakPeakEnabled(ref)
|
|
||||||
: lane.EdgeCenterEnabled(ref));
|
|
||||||
if (checked && itemHasParams)
|
|
||||||
{
|
|
||||||
if (weakPeak)
|
|
||||||
{
|
|
||||||
itemsHeight += weakPeakSliderRow * 2.0f;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
itemsHeight += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale;
|
|
||||||
if (centerAxis)
|
|
||||||
{
|
|
||||||
itemsHeight += kEdgePanelMeanRowHeight * m_scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 底部按钮行:上方与条目之间、下方与面板下边界之间各留一个 `pad`,
|
|
||||||
// 与面板的左右内边距完全一致(原先按钮直接贴着下边框,看起来很不协调)。
|
|
||||||
height = titleHeight + pad + itemsHeight + pad + buttonHeight + (hasButton ? pad : 0.0f);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal + pad;
|
// 「边沿计数」:每信号两行(名称 / 上升沿·下降沿),内容超高时滚动。
|
||||||
|
const float content = static_cast<float>(rowCount) * rowHeight * rowsPerSignal;
|
||||||
|
meanContentHeight = content;
|
||||||
|
const float budget = m_plotRect.Height - (titleHeight + pad + bottomPad);
|
||||||
|
const float maxContent =
|
||||||
|
std::max(2.0f * rowHeight, std::min(kFreeMarkerContentMaxHeight * m_scale, budget));
|
||||||
|
const float view = std::min(content, maxContent);
|
||||||
|
height = titleHeight + pad + view + bottomPad;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 「自由标记」面板:按信号分组(**分组栏** + 下属固定点条目),
|
// 「自由标记」面板:按信号分组(**分组栏** + 下属固定点条目),
|
||||||
@@ -574,11 +568,13 @@ namespace jdi
|
|||||||
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
||||||
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
|
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||||
|
const float footerHeight = hasButton ? (buttonRowHeight + pad) : 0.0f;
|
||||||
const float viewHeight =
|
const float viewHeight =
|
||||||
std::max(0.0f, height - titleHeight - pad * 3.0f - buttonRowHeight);
|
std::max(0.0f, height - titleHeight - pad - pad - footerHeight);
|
||||||
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 storedScroll = meanFilter ? lane.meanFilterScroll : lane.edgeCenterScroll;
|
const float storedScroll = meanFilter ? lane.meanFilterScroll
|
||||||
|
: (weakPeak ? lane.weakPeakScroll : lane.edgeCenterScroll);
|
||||||
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
|
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));
|
||||||
@@ -600,11 +596,13 @@ namespace jdi
|
|||||||
|
|
||||||
// 两行滑块的列宽(与 `EdgePanelWidth` 同一组算式 ⇒ 布局与宽度不会漂移)。
|
// 两行滑块的列宽(与 `EdgePanelWidth` 同一组算式 ⇒ 布局与宽度不会漂移)。
|
||||||
const float labelColumn =
|
const float labelColumn =
|
||||||
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
std::max(std::max(UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterPointsSliderLabel),
|
||||||
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel));
|
UiMeasureTextWidth(m_fonts->uiSmall, kMeanFilterIterationsSliderLabel)),
|
||||||
|
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakLevelLabel),
|
||||||
|
UiMeasureTextWidth(m_fonts->uiSmall, kWeakPeakIterationsSliderLabel)));
|
||||||
const float valueColumn =
|
const float valueColumn =
|
||||||
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"100000"),
|
||||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1000"));
|
UiMeasureTextWidth(m_fonts->monoSmall, L"-400%"));
|
||||||
const float resetSize = kWeakPeakResetButtonSize * m_scale;
|
const float resetSize = kWeakPeakResetButtonSize * m_scale;
|
||||||
const float resetGap = kWeakPeakResetGap * m_scale;
|
const float resetGap = kWeakPeakResetGap * m_scale;
|
||||||
const float trackHeight = kWeakPeakTrackHeight * m_scale;
|
const float trackHeight = kWeakPeakTrackHeight * m_scale;
|
||||||
@@ -624,9 +622,21 @@ namespace jdi
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
bool checked = false;
|
||||||
: lane.FindEdgeCenterItem(ref);
|
bool itemCollapsed = false;
|
||||||
const bool checked = (item != nullptr);
|
if (weakPeak)
|
||||||
|
{
|
||||||
|
const Lane::WeakPeakItem* item = lane.FindWeakPeakItem(ref);
|
||||||
|
checked = (item != nullptr);
|
||||||
|
itemCollapsed = (item != nullptr && item->collapsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const Lane::MeanFilterItem* item = meanFilter ? lane.FindMeanFilterItem(ref)
|
||||||
|
: lane.FindEdgeCenterItem(ref);
|
||||||
|
checked = (item != nullptr);
|
||||||
|
itemCollapsed = (item != nullptr && item->collapsed);
|
||||||
|
}
|
||||||
layout.rowRefs.push_back(ref);
|
layout.rowRefs.push_back(ref);
|
||||||
// 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。
|
// 组头(信号色叠底):整行宽度,左侧三角(仅勾选时)+ 名称,右端复选框。
|
||||||
layout.rowHeaders.push_back(
|
layout.rowHeaders.push_back(
|
||||||
@@ -647,7 +657,7 @@ namespace jdi
|
|||||||
rowY += groupRow;
|
rowY += groupRow;
|
||||||
|
|
||||||
// 参数行:只有勾选且未收起时才有几何(未勾选的行两者都是空矩形)。
|
// 参数行:只有勾选且未收起时才有几何(未勾选的行两者都是空矩形)。
|
||||||
if (!checked || item->collapsed)
|
if (!checked || itemCollapsed)
|
||||||
{
|
{
|
||||||
layout.rowLevelTracks.push_back(gd::RectF());
|
layout.rowLevelTracks.push_back(gd::RectF());
|
||||||
layout.rowLevelValues.push_back(gd::RectF());
|
layout.rowLevelValues.push_back(gd::RectF());
|
||||||
@@ -684,139 +694,30 @@ namespace jdi
|
|||||||
rowY += sliderRow;
|
rowY += sliderRow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (itemized)
|
else if (!freeMarker)
|
||||||
{
|
{
|
||||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
// 「边沿计数」:内容区 = 逐信号两行(名称 / 上升沿·下降沿),可滚动。
|
||||||
if (hasButton)
|
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||||
|
const float viewHeight = std::max(0.0f, height - titleHeight - pad - pad);
|
||||||
|
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||||
|
const float maxScroll = std::max(0.0f, meanContentHeight - viewHeight);
|
||||||
|
const float scroll = std::max(0.0f, std::min(lane.edgeCountScroll, maxScroll));
|
||||||
|
const float innerWidth =
|
||||||
|
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.markerContentHeight = meanContentHeight;
|
||||||
|
layout.scrollOffset = scroll;
|
||||||
|
if (scrollable)
|
||||||
{
|
{
|
||||||
// 底部按钮与面板下边界之间留出与左右、与内容相同的 `pad` 间距
|
layout.scrollTrack =
|
||||||
//(原先按钮行直接贴着下边框,视觉上与其它元素不协调);
|
gd::RectF(left + pad + innerWidth + 4.0f * m_scale, layout.content.Y, scrollW,
|
||||||
// 面板高度已含这份 pad,因此这里只上移按钮行即可。
|
viewHeight);
|
||||||
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
|
const float thumbHeight =
|
||||||
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
|
std::max(18.0f * m_scale, viewHeight * (viewHeight / meanContentHeight));
|
||||||
}
|
const float thumbTop =
|
||||||
|
layout.content.Y +
|
||||||
// 逐信号条目:第一排 = 名称 + 复选框(复选框靠右);
|
(viewHeight - thumbHeight) * ((maxScroll > 0.0f) ? (scroll / maxScroll) : 0.0f);
|
||||||
// 参数部分:MEAN / CENTER = 一行两个输入框(中心轴再多一行统计结果);
|
layout.scrollThumb = gd::RectF(layout.scrollTrack.X, thumbTop, scrollW, thumbHeight);
|
||||||
// PEAK = 两行滑块(每个信号各有自己的等级与迭代次数)。
|
|
||||||
const float checkSize = kEdgePanelMeanCheckSize * m_scale;
|
|
||||||
const float fieldHeight = kEdgePanelMeanEditHeight * m_scale;
|
|
||||||
const float halfWidth = std::max(40.0f, (width - pad * 2.0f - 8.0f * m_scale) * 0.5f);
|
|
||||||
// 滑块行列宽(与 `EdgePanelWidth` 使用同一组算式,避免布局与宽度计算漂移)。
|
|
||||||
const float weakPeakLabelWidth = WeakPeakLabelColumnWidth(m_fonts->uiSmall);
|
|
||||||
const float weakPeakValueWidth = WeakPeakValueColumnWidth(m_fonts->monoSmall);
|
|
||||||
const float 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 - weakPeakResetGap -
|
|
||||||
weakPeakResetSize - 6.0f * m_scale) -
|
|
||||||
weakPeakTrackLeft);
|
|
||||||
float rowY = top + titleHeight + pad;
|
|
||||||
for (const SignalRef& ref : lane.signals)
|
|
||||||
{
|
|
||||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
|
||||||
if (resolved.data == nullptr || !resolved.visible)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
layout.rowRefs.push_back(ref);
|
|
||||||
layout.rowChecks.push_back(gd::RectF(
|
|
||||||
left + width - pad - checkSize, rowY + (meanRowHeight - checkSize) * 0.5f, checkSize,
|
|
||||||
checkSize));
|
|
||||||
const bool checked = meanFilter ? lane.MeanFilterEnabled(ref)
|
|
||||||
: (weakPeak ? lane.WeakPeakEnabled(ref)
|
|
||||||
: lane.EdgeCenterEnabled(ref));
|
|
||||||
if (weakPeak)
|
|
||||||
{
|
|
||||||
// 每个被勾选的信号各有两行滑块:调整幅度(-400% – +400%)与迭代次数(1 … 上限)。
|
|
||||||
if (checked)
|
|
||||||
{
|
|
||||||
const float levelRowTop = rowY + meanRowHeight;
|
|
||||||
const float iterRowTop = levelRowTop + weakPeakSliderRow;
|
|
||||||
const gd::RectF levelTrack(
|
|
||||||
weakPeakTrackLeft,
|
|
||||||
levelRowTop + (weakPeakSliderRow - weakPeakTrackHeight) * 0.5f,
|
|
||||||
weakPeakTrackWidth, weakPeakTrackHeight);
|
|
||||||
const gd::RectF iterTrack(
|
|
||||||
weakPeakTrackLeft,
|
|
||||||
iterRowTop + (weakPeakSliderRow - weakPeakTrackHeight) * 0.5f,
|
|
||||||
weakPeakTrackWidth, weakPeakTrackHeight);
|
|
||||||
layout.rowLevelTracks.push_back(levelTrack);
|
|
||||||
layout.rowLevelValues.push_back(gd::RectF(
|
|
||||||
levelTrack.X + levelTrack.Width + 6.0f * m_scale, levelRowTop,
|
|
||||||
weakPeakValueWidth, weakPeakSliderRow));
|
|
||||||
layout.rowIterTracks.push_back(iterTrack);
|
|
||||||
layout.rowIterValues.push_back(gd::RectF(
|
|
||||||
iterTrack.X + iterTrack.Width + 6.0f * m_scale, iterRowTop,
|
|
||||||
weakPeakValueWidth, weakPeakSliderRow));
|
|
||||||
// 每行滑块右侧的**复位按钮**(与数值列同一右边界,保证两行上下对齐)。
|
|
||||||
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());
|
|
||||||
layout.rowStats.push_back(gd::RectF());
|
|
||||||
|
|
||||||
rowY += meanRowHeight;
|
|
||||||
if (checked)
|
|
||||||
{
|
|
||||||
rowY += weakPeakSliderRow * 2.0f;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (checked && itemHasParams)
|
|
||||||
{
|
|
||||||
const float editTop = rowY + meanRowHeight;
|
|
||||||
layout.rowPoints.push_back(
|
|
||||||
gd::RectF(left + pad, editTop, halfWidth, fieldHeight));
|
|
||||||
layout.rowIterations.push_back(
|
|
||||||
gd::RectF(left + pad + halfWidth + 8.0f * m_scale, editTop, halfWidth, fieldHeight));
|
|
||||||
if (centerAxis)
|
|
||||||
{
|
|
||||||
layout.rowStats.push_back(
|
|
||||||
gd::RectF(left + pad, editTop + fieldHeight, width - pad * 2.0f,
|
|
||||||
kEdgePanelMeanRowHeight * m_scale));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
layout.rowStats.push_back(gd::RectF());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
layout.rowPoints.push_back(gd::RectF());
|
|
||||||
layout.rowIterations.push_back(gd::RectF());
|
|
||||||
layout.rowStats.push_back(gd::RectF());
|
|
||||||
}
|
|
||||||
|
|
||||||
rowY += meanRowHeight;
|
|
||||||
if (checked && itemHasParams)
|
|
||||||
{
|
|
||||||
rowY += (kEdgePanelMeanEditHeight + kEdgePanelMeanRowGap) * m_scale;
|
|
||||||
if (centerAxis)
|
|
||||||
{
|
|
||||||
rowY += kEdgePanelMeanRowHeight * m_scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -615,6 +615,15 @@ namespace jdi
|
|||||||
|
|
||||||
void SignalPanel::EnsureRows()
|
void SignalPanel::EnsureRows()
|
||||||
{
|
{
|
||||||
|
// ⚠️ 动画推进只依赖挂钟时间(`AdvanceGroupAnimations`),而驱动它的 `WM_TIMER` 是
|
||||||
|
// **最低优先级**的消息:绘图区若在自持刷新(例如边沿统计未完成时不断请求新帧),
|
||||||
|
// `WM_TIMER` 会被饿死 ⇒ 折起动画永远结束不了 ⇒ 组名三角已经显示“已收起”,
|
||||||
|
// 但行集合里仍保留着该组的信号行(可被点选)。用户 2026-11-02 报告的正是这个现象。
|
||||||
|
// 因此绘制 / 命中路径也顺手推进一步:动画是否结束只看时间,不依赖消息到达。
|
||||||
|
if (!m_groupAnims.empty())
|
||||||
|
{
|
||||||
|
AdvanceGroupAnimations();
|
||||||
|
}
|
||||||
if (m_rowsDirty)
|
if (m_rowsDirty)
|
||||||
{
|
{
|
||||||
BuildRows();
|
BuildRows();
|
||||||
|
|||||||
@@ -9061,3 +9061,128 @@ make release / make debug 零 warning
|
|||||||
(`RowPart::CHECK`)—— 想测折叠必须点名称区。
|
(`RowPart::CHECK`)—— 想测折叠必须点名称区。
|
||||||
* ⚠️ 「行集合签名是否稳定」不能作为断言:动画**结束那一帧**签名必然变化一次(动画标记位
|
* ⚠️ 「行集合签名是否稳定」不能作为断言:动画**结束那一帧**签名必然变化一次(动画标记位
|
||||||
从 5 → 0),这是正确的。要断言的是"动画结束后 `m_rowsSignature == RowsSignature()`"。
|
从 5 → 0),这是正确的。要断言的是"动画结束后 `m_rowsSignature == RowsSignature()`"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-11-02 第二十轮(本机):面板限高/裁剪/弱峰列表化 + 边沿统计"统计中…"根治 + 组名折起无法完成
|
||||||
|
|
||||||
|
> 用户复现的四组问题(`VERY_BIG_DATA.CSV` + `MF4-Record.MF4` 全选后合并成同一网格):
|
||||||
|
> ①「中心轴边沿计数」面板下方有**无意义空白**;② 勾选全部信号后滚动,**绘制超出面板**;
|
||||||
|
> ③ 启用「边沿计数」后来自 VERY_BIG_DATA 的信号**永远"统计中…"**;
|
||||||
|
> ④ 折起分组后**三角已收起但下属信号行仍在、仍可点选**(点第二个组时第一个组才真正收起);
|
||||||
|
> ⑤ 启用**尚未改造**的实验性功能后一切交互(光标/动画)都很卡。
|
||||||
|
|
||||||
|
### 1. ⭐⭐ 根本原因:**面板高度没有上限 ⇒ 面板像素缓存永远存不进去**
|
||||||
|
|
||||||
|
`itemized`(旧式逐条目)排布按「每勾选信号 2 行 + 每行 22 逻辑像素」累加,288 条信号时
|
||||||
|
面板高达 **13010px**(绘图区只有 846px)。`PanelRectInsideLayer()` 因此恒为假 ⇒
|
||||||
|
**面板像素缓存一次都没写进去** ⇒ 每帧整块重画(288 行 × 十来个 GDI+ 原语)。
|
||||||
|
「边沿计数」与(改造前的)「弱峰调整」都是这个排布,所以"只要开这两个功能就卡"。
|
||||||
|
|
||||||
|
**硬规则(新)**:**任何**实验性功能面板的高度都必须被绘图区夹住:
|
||||||
|
`budget = plotH - (标题 + pad + 底部区)`,`view = min(内容高, maxContent)`,
|
||||||
|
内容超出时右侧出现**滚动条**。`itemized` 排布已**整体删除**(布局 + 绘制 + 交互),
|
||||||
|
现在 5 个面板全部是「有高度上限 + 可滚动」的实现。
|
||||||
|
|
||||||
|
### 2. 四个面板的滚动偏移(`Lane`)
|
||||||
|
|
||||||
|
| 面板 | 偏移字段 | setter |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 均值滤波 | `meanFilterScroll` | `SetLaneMeanFilterScroll` |
|
||||||
|
| 中心轴边沿计数 | `edgeCenterScroll` | `SetLaneEdgeCenterScroll` |
|
||||||
|
| **弱峰调整(新)** | **`weakPeakScroll`** | **`SetLaneWeakPeakScroll`** |
|
||||||
|
| **边沿计数(新)** | **`edgeCountScroll`** | **`SetLaneEdgeCountScroll`** |
|
||||||
|
|
||||||
|
前三个是**列表式**(信号色叠底条目栏 + 右侧复选框 + 展开后的两行滑块);
|
||||||
|
「边沿计数」保留旧式两行排版,但同样有 `layout.content`(裁剪 + 滚动条 + 滚轮 / 拖动)。
|
||||||
|
|
||||||
|
### 3. ⚠️⚠️ 无按钮面板的底部留白(用户指出的"无意义空白")
|
||||||
|
|
||||||
|
旧高度算式**无条件**按 `标题 + pad + 内容 + pad + 按钮 + pad` 预留,而 `viewHeight`
|
||||||
|
又无条件减去 `kEdgePanelButtonHeight`;「中心轴边沿计数」没有「构建为新信号」按钮 ⇒
|
||||||
|
内容区下方白留了 **按钮高度 30 + pad 7 = 46 逻辑像素**(实测 55px @125%)。
|
||||||
|
现改为:
|
||||||
|
|
||||||
|
```
|
||||||
|
footer = hasButton ? (buttonHeight + pad) : 0
|
||||||
|
height = 标题 + pad + view + pad + footer
|
||||||
|
viewHeight = height - 标题 - 2*pad - footer // = view
|
||||||
|
budget = plotH - (标题 + pad + pad + footer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. ⚠️⚠️ "绘制超出面板" = **GDI 文字不受 GDI+ 裁剪约束**
|
||||||
|
|
||||||
|
`paintCounts`(上升沿 / 下降沿)用的是 `UiText`(`DrawTextW`)⇒ `g.SetClip(layout.content)`
|
||||||
|
对它**无效**,滚动时数字会画到标题栏 / 面板之外。现统一改 `UiTextClipped(..., layout.content)`。
|
||||||
|
**A/B 已证**:改回 `UiText` 后,标题栏区域像素差 **805px**(x1..241、y12..24)。
|
||||||
|
|
||||||
|
**硬规则**:凡"可滚动内容区"里的文字一律 `UiTextClipped`(含名称、标签、数值、统计、提示)。
|
||||||
|
`UiFillRoundRect` 的 GDI 快路径(≥20000px² 且不透明)同样不认裁剪 —— 条目栏宽 ≈ 420px,
|
||||||
|
面积 < 20000,目前安全;**宽度再放大时要复核**。
|
||||||
|
|
||||||
|
### 5. ⭐⭐ 「统计中…」永不更新 = **状态上限 64 < 信号数** + 预算分摊不公平
|
||||||
|
|
||||||
|
`EdgeScanFor` 的累积状态上限原本固定 `kEdgeScanStateLimit = 64`,
|
||||||
|
而面板会在同一帧为**每条信号**各创建一个状态(288 > 64)⇒ 新建时总淘汰队首(未完成的)
|
||||||
|
⇒ **没有任何状态能扫完** ⇒ 永远"统计中…"。
|
||||||
|
|
||||||
|
- 新增 `PlotView::EdgeScanStateLimit()`:`max(64, 信号总数 × 2 + 16)`(每信号最多两类状态)。
|
||||||
|
- **每帧配额去掉固定下限** `kEdgeScanPairsPerStateMin(60000)`:状态一多,下限会让最前面的
|
||||||
|
几个状态吃掉整帧预算,其余永远 0。现为 `budget / share`(至少 1),**`share` 只数「未完成」
|
||||||
|
状态**(否则完成状态仍占分母,长信号每帧只分到 1/300,实测要 20 分钟)。
|
||||||
|
- 实测:288 条信号(含 5 条 25.8M 采样)——短信号 1~2 帧内完成,长信号随后独享整帧预算。
|
||||||
|
|
||||||
|
### 6. ⭐⭐ 组名折起"三角已收起、行还在" = **`WM_TIMER` 被饿死**
|
||||||
|
|
||||||
|
`RequestEdgeScanFrame()` 原本在**每个未完成状态**里各 Post 一次
|
||||||
|
⇒ 288 条信号时单帧 **288 次 `PostMessage`** + 高帧率重绘 ⇒ 优先级更低的 `WM_TIMER`
|
||||||
|
拿不到执行机会 ⇒ `AdvanceGroupAnimations()` 永远不跑 ⇒ 折起动画永不结束 ⇒
|
||||||
|
行集合里的信号行永远不会被移除(三角却是从 `IsGroupCollapsed()` 直接画的,所以"看起来收起了")。
|
||||||
|
点击下一个组时,那次点击触发的绘制里又推进一次动画,于是"上一组这时才真正收起"。
|
||||||
|
|
||||||
|
修法两条(都要):
|
||||||
|
1. `RequestEdgeScanFrame()` **每帧最多一次**(`m_edgeScanRequestFrame == m_edgeScanFrame` 直接返回);
|
||||||
|
2. `SignalPanel::EnsureRows()` 开头:`if (!m_groupAnims.empty()) AdvanceGroupAnimations();`
|
||||||
|
—— 动画进度只依赖挂钟时间,**不应依赖消息能否按时到达**。
|
||||||
|
|
||||||
|
### 7. 「弱峰调整」改为列表式(与均值滤波同款)
|
||||||
|
|
||||||
|
- `WeakPeakItem` 新增 `collapsed`;面板 = 条目栏(三角 + 名称 + 复选框)+ 两行滑块
|
||||||
|
(调整幅度 ±400% / 迭代次数 1…自定上限)+ 右侧复位 + 底部「构建为新信号」。
|
||||||
|
- 交互全部复用 `MEAN_*` 拖拽模式(滚动用 `MEAN_SCROLL`),滑块拖动仍用
|
||||||
|
`PEAK_LEVEL / PEAK_ITERATIONS`,上限编辑仍用 `PEAK_ITERATIONS_MAX + m_peakEditCurrentMax`。
|
||||||
|
- ⚠️ `m_meanDragCenter`(bool) / `m_editMeanCenter`(bool) → **`m_meanDragKind` / `m_editMeanKind`**
|
||||||
|
(`EdgePanelKind`):现在有 MEAN / CENTER / PEAK / COUNT 四个写入目标。
|
||||||
|
- **鼠标指针**:列表式面板的条目栏只有**已勾选**时才显示手型;未勾选时保持箭头
|
||||||
|
(此时点击没有任何效果,用户 2026-11-02 明确要求)。
|
||||||
|
参数行的数值 / 复位 / 轨道也只在参数行展开(`rowLevelTracks.Width > 0`)时才给手型/左右箭头。
|
||||||
|
|
||||||
|
### 8. 验证(`.preflight/p20`,离屏 `SW_HIDE` + 合成消息)
|
||||||
|
|
||||||
|
合成数据:5 列 + 283 列两个 CSV → 两组 → 全部显示 → 合并成 1 个网格(288 信号)。
|
||||||
|
|
||||||
|
```
|
||||||
|
p20.exe 9/9
|
||||||
|
合并/信号数;COUNT 面板高 375 ≤ 绘图区 846 且 **像素缓存真的写进去了**(slot.key != 0);
|
||||||
|
288 个边沿统计全部 complete(states=288);
|
||||||
|
CENTER 面板底部留白 8.8px(= 1 个 pad,修前 55px);
|
||||||
|
组名折起后行集合无该组信号行;两组都折起后 signalRows == 0(真实 WM_LBUTTONDOWN/UP +
|
||||||
|
WM_TIMER(kScrollTimerId=3) + Render)
|
||||||
|
p20c.exe 11/11
|
||||||
|
PEAK 面板限高/可滚动/缓存命中;勾选展开两行滑块、收起后内容缩小;滚动偏移生效;
|
||||||
|
MEAN 条目栏:已勾选→手型、未勾选→箭头;
|
||||||
|
**CENTER 滚动到统计行越过内容区顶部时,标题栏区域 0 像素变化**(A/B 关掉裁剪 ⇒ 805px);
|
||||||
|
288 信号 + 面板打开时空闲帧 avg 2.6ms
|
||||||
|
make release / make debug 零 warning
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. 踩坑
|
||||||
|
|
||||||
|
- ⚠️ PowerShell 5.1 按 ANSI 读 `.ps1` ⇒ **脚本里不能出现中文**(连 `'条目栏'` 这种
|
||||||
|
单引号短串都会把解析器带崩,报"字符串缺少终止符")。断言数据放 UTF-8 文件、脚本纯 ASCII。
|
||||||
|
- ⚠️ `SetLaneMeanFilter/SetLaneWeakPeak/SetLaneEdgeCenterCount(lane, true)` 会**自动勾选
|
||||||
|
该网格第一个信号** ⇒ 断言"面板初始全未勾选"必然失败;要测未勾选行请用第 2 行。
|
||||||
|
- ⚠️ 面板圆角外侧的 1~2px 是"面板 AA 边缘 vs 缓存贴图(圆角裁剪)"的合法差异,
|
||||||
|
取像素做 A/B 时要**避开圆角 8px**,否则会误报 7 个像素。
|
||||||
|
- ⚠️ `git diff --numstat` 出现整文件级别的增删时要先怀疑行尾/BOM 被 PowerShell 改掉;
|
||||||
|
本轮所有写回都用 `[System.IO.File]::WriteAllText(... UTF8Encoding($false))`。
|
||||||
@@ -805,6 +805,13 @@ namespace jdi
|
|||||||
int amountPercent = 0; /**< 调整幅度(-400–400,默认 0 = 不改动)。 */
|
int amountPercent = 0; /**< 调整幅度(-400–400,默认 0 = 不改动)。 */
|
||||||
int iterations = 1; /**< 迭代次数 N(1 … `maxIterations`)。 */
|
int iterations = 1; /**< 迭代次数 N(1 … `maxIterations`)。 */
|
||||||
int maxIterations = 10; /**< 「迭代次数 N」滑块的上限(用户可在面板上修改,恒 ≥ 1)。 */
|
int maxIterations = 10; /**< 「迭代次数 N」滑块的上限(用户可在面板上修改,恒 ≥ 1)。 */
|
||||||
|
/**
|
||||||
|
* @brief 参数行是否被**收起**(只保留信号色叠底的条目栏)。
|
||||||
|
*
|
||||||
|
* 「弱峰调整」面板自 2026-11-02 起与「均值滤波 / 中心轴边沿计数」同一套可滚动列表:
|
||||||
|
* 勾选后条目栏左侧出现展开三角,单击它切换收起 / 展开。未勾选时本字段无意义。
|
||||||
|
*/
|
||||||
|
bool collapsed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1076,6 +1083,25 @@ namespace jdi
|
|||||||
*/
|
*/
|
||||||
float edgeCenterScroll = 0.0f;
|
float edgeCenterScroll = 0.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 「弱峰调整」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
*
|
||||||
|
* 与 `meanFilterScroll` / `edgeCenterScroll` 完全同一套语义:面板自 2026-11-02 起
|
||||||
|
* 改为**可滚动的分组列表**(信号色叠底条目栏 + 右侧复选框 + 展开后的两行滑块),
|
||||||
|
* 底部「构建为新信号」按钮固定不滚动。
|
||||||
|
*/
|
||||||
|
float weakPeakScroll = 0.0f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 「边沿计数」面板内容的滚动偏移(客户区像素,0 = 顶部)。
|
||||||
|
*
|
||||||
|
* 该面板仍是「每信号两行(名称 / 上升沿·下降沿)」的旧式排版,但**内容可以滚动**:
|
||||||
|
* 上百个信号合并到同一网格时内容高达上万像素,若不限高,面板会远超绘图区
|
||||||
|
* ⇒ 面板像素缓存一次都存不进去 ⇒ 每一帧都要整块重画(用户 2026-11-02 报告
|
||||||
|
* “开启该功能后所有交互都非常卡顿”)。
|
||||||
|
*/
|
||||||
|
float edgeCountScroll = 0.0f;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ namespace jdi
|
|||||||
float meanFilterScroll = 0.0f;
|
float meanFilterScroll = 0.0f;
|
||||||
/** @brief 「中心轴边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCenterScroll`)。 */
|
/** @brief 「中心轴边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCenterScroll`)。 */
|
||||||
float edgeCenterScroll = 0.0f;
|
float edgeCenterScroll = 0.0f;
|
||||||
|
/** @brief 「弱峰调整」面板内容的滚动偏移(详见 `Lane::weakPeakScroll`)。 */
|
||||||
|
float weakPeakScroll = 0.0f;
|
||||||
|
/** @brief 「边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCountScroll`)。 */
|
||||||
|
float edgeCountScroll = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 先记下每个信号原来所在网格的网格自身状态:
|
// 先记下每个信号原来所在网格的网格自身状态:
|
||||||
@@ -176,6 +180,8 @@ namespace jdi
|
|||||||
carry.edgeCenterItems = lane.edgeCenterItems;
|
carry.edgeCenterItems = lane.edgeCenterItems;
|
||||||
carry.edgeCenterPanel = lane.edgeCenterPanel;
|
carry.edgeCenterPanel = lane.edgeCenterPanel;
|
||||||
carry.edgeCenterScroll = lane.edgeCenterScroll;
|
carry.edgeCenterScroll = lane.edgeCenterScroll;
|
||||||
|
carry.weakPeakScroll = lane.weakPeakScroll;
|
||||||
|
carry.edgeCountScroll = lane.edgeCountScroll;
|
||||||
carry.meanFilter = lane.meanFilter;
|
carry.meanFilter = lane.meanFilter;
|
||||||
carry.meanFilterItems = lane.meanFilterItems;
|
carry.meanFilterItems = lane.meanFilterItems;
|
||||||
carry.meanFilterPanel = lane.meanFilterPanel;
|
carry.meanFilterPanel = lane.meanFilterPanel;
|
||||||
@@ -237,6 +243,8 @@ namespace jdi
|
|||||||
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.edgeCenterScroll = carry.edgeCenterScroll;
|
||||||
|
lane.weakPeakScroll = carry.weakPeakScroll;
|
||||||
|
lane.edgeCountScroll = carry.edgeCountScroll;
|
||||||
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