fix(ui): 选区截取面板空白真正根因、拖拽指示线层序、新建分组修复、避让动画更顺

一、选区截取面板「条目区与按钮之间的空白」(连续三轮未修好的那个)
根因不是内容区几何,而是**底部固定区高度算了两行**:footerHeight 用
`buttonHeight * 2 + 6 + pad` 预留,而布局把「截取为新信号 / 原始信号截取」
两个按钮并排画在同一行 ⇒ 内容区下边界与按钮上边界之间空出一整行。
改为按一行算(两处:高度算式分支 + 布局几何分支同步修改)。
数值核对(scale=1.25、1 条可见信号):GAP 53.8px → 8.8px(= 一个 pad 的预期留白)。

二、拖拽插入指示线不再盖住被拖条目
PaintSignalDragGhost 的绘制顺序改为「插入指示线 → 阴影 → 幽灵行 → 描边」,
指示线始终位于跟随鼠标的那部分之下。

三、「移动到…」二级菜单
* 条目文案改为「组号 | 组名」(不再是括号形式)。
* 修复「新建分组」点了没反应:DataSet::AddGroup 拒收「无信号且无时间轴」的空分组,
  旧实现「先建空组再搬信号」必然在建组那一步失败。新增
  DataSet::MoveSignalToNewGroup 把建组 + 搬运时间轴 + 搬运信号做成原子操作(失败回滚)。
* TabConfig::RemapAfterSignalMove 在目标分组是新建时自行扩展 m_visible
  (调用方不再先 SyncToDataSet —— 那次同步会按旧下标清理引用)。
* Application::MoveSignalCore 增加 newGroupName 参数并返回 bool,避免失败仍报"已移动"。

四、信号拖拽避让动画更流畅
* 插值系数按真实帧间隔计算:1 - exp(-dt/22ms),dt 夹在 1~60ms,帧间隔抖动时不再忽快忽慢。
* 鼠标移动事件里也推进一次插值,快速拖动时让位效果跟得上指针。

验证:删 .o 全量重编译零 error / 零 warning,产物 875,008 B(< 1MB 预警阈值)。
本轮未占用鼠标/键盘/屏幕,未运行任何 GUI 探针;面板空白改为纯数值算式核对。
规则已同步到 Documents/agent_mem.md(含"改面板几何必须数值验证"的教训条目)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-11-03 20:09:01 +08:00
1 parent 97b361dedd
commit 328a3b3b59
10 files changed
+259 -75

No files matched your search

+14 -1
View File
@@ -42,7 +42,13 @@ namespace jdi
* 取值区间必须与 `CMD_LANE_BASE` 的区间(300–399)和 `CMD_LANE_NEW` 互不重叠。
*/
CMD_GROUP_MOVE_BASE = 600,
CMD_GROUP_MOVE_NEW = 700, /**< 「移动到…」二级菜单底部:新建分组并把信号移进去。 */
/**
* @brief 「移动到…」二级菜单底部:新建分组并把信号移进去。
*
* ⚠️ 与 `CMD_GROUP_MOVE_BASE` 之间留足空间(600–949 = 最多 350 个分组条目),
* 否则分组数量一多,`CMD_GROUP_MOVE_BASE + index` 就会撞上本命令。
*/
CMD_GROUP_MOVE_NEW = 950,
CMD_GAIN_EDIT = 500, /**< 「系数」二级面板里的系数输入框(幂等提交)。 */
CMD_GAIN_RESET = 501, /**< 「系数调整」标题栏右侧的还原按钮:把系数重置为 1。 */
CMD_OFFSET_RESET = 502, /**< 「偏移调整」标题栏右侧的还原按钮:两个偏移都恢复为默认值。 */
@@ -1538,6 +1544,13 @@ namespace jdi
bool m_signalDragDropBefore = false; /**< 插入点在该行的上方(否则下方)。 */
std::vector<float> m_signalDragShift; /**< 各行避让位移的**目标值**(像素)。 */
std::vector<float> m_signalDragShiftCur; /**< 各行避让位移的**当前值**(逐帧插值)。 */
/**
* @brief 避让插值上一次推进的时刻(`GetTickCount64`,0 = 尚未推进过)。
*
* 插值系数按**真实帧间隔**算(`1 - exp(-dt/τ)`),因此不论是 60fps 定时器帧
* 还是鼠标快速拖动带来的高频帧,观感一致;固定系数在帧间隔抖动时会显得僵硬。
*/
std::uint64_t m_signalDragAnimLastMs = 0;
/**
* @brief 筛选模式下的分组折起状态(按分组下标)。
@@ -1028,6 +1028,7 @@ namespace jdi
m_signalDragDropBefore = false;
m_signalDragShift.clear();
m_signalDragShiftCur.clear();
m_signalDragAnimLastMs = 0;
}
bool SignalPanel::UpdateSignalDrag(int x, int y)
@@ -1054,6 +1055,10 @@ namespace jdi
}
UpdateSignalDragTargets();
// ⚠️ 鼠标移动时**也**推进一步插值:宿主定时器约 10ms 一帧,快速拖动时
// 避让动画会明显落后于鼠标(用户 2026-11-03 反馈“快速拖动略显僵硬”)。
// 在移动事件里补推进一次,让让位效果跟得上指针。
AdvanceSignalDragAnimation();
return true;
}
@@ -1190,6 +1195,7 @@ namespace jdi
m_signalDragDropBefore = false;
m_signalDragShift.clear();
m_signalDragShiftCur.clear();
m_signalDragAnimLastMs = 0;
}
void SignalPanel::FinishSignalDrag()
@@ -1233,14 +1239,27 @@ namespace jdi
{
m_signalDragShiftCur.resize(m_signalDragShift.size(), 0.0f);
}
// 逐帧向目标位移插值(宿主约 10ms 一帧 ⇒ 约 5 帧收敛,观感是"平滑让位")。
const float factor = 0.32f;
// 按**真实帧间隔**求插值系数:`1 - exp(-dt/τ)`。
// ⚠️ 不能用固定系数:鼠标移动帧(可能 3~5ms)与定时器帧(约 10ms)混杂时,
// 固定系数会让动画忽快忽慢、拖动时显得僵硬(用户 2026-11-03 反馈)。
const std::uint64_t now = GetTickCount64();
float deltaMs = 16.0f;
if (m_signalDragAnimLastMs != 0 && now >= m_signalDragAnimLastMs)
{
deltaMs = static_cast<float>(now - m_signalDragAnimLastMs);
}
m_signalDragAnimLastMs = now;
deltaMs = std::max(1.0f, std::min(deltaMs, 60.0f));
// τ = 22ms:16ms 一帧时系数约 0.52(约 3 帧收敛),既跟手又有过渡。
const float factor = 1.0f - std::exp(-deltaMs / 22.0f);
for (std::size_t index = 0; index < m_signalDragShiftCur.size(); ++index)
{
const float target = m_signalDragShift[index];
float& current = m_signalDragShiftCur[index];
const float delta = target - current;
if (std::fabs(delta) < 0.5f)
if (std::fabs(delta) < 0.4f)
{
current = target;
}
@@ -1273,23 +1292,10 @@ namespace jdi
const int state = g.Save();
g.SetClip(m_listRect, gd::CombineModeIntersect);
// 浮起的阴影:让条目明显"脱离"列表本体。
UiFillRoundRect(g, gd::RectF(natural.X + 2.0f * m_scale, top + 3.0f * m_scale,
natural.Width, height),
4.0f * m_scale, 0x33000000u);
// 复用行绘制:临时把行顶部覆盖到鼠标位置(与「组名悬浮」同一机制),
// 因此名称、复选框、GX 徽标、✕ 等全部随行一起移动,无需另写一套绘制。
m_rowYOverrideActive = true;
m_rowYOverrideY = top;
PaintSignalRow(g, m_signalDragRow);
m_rowYOverrideActive = false;
UiDrawRoundRect(g, gd::RectF(natural.X + 1.0f * m_scale, top + 1.0f * m_scale,
natural.Width - 2.0f * m_scale, height - 2.0f * m_scale),
4.0f * m_scale, m_theme->accent, 1.6f * m_scale);
// 插入位置指示线:明确告诉用户"松手后落在哪里"。
// ⚠️⚠️ **插入位置指示线必须最先画**(= 位于被拖条目之下):
// 用户 2026-11-03 明确要求“不要覆盖到正在拖拽的那个元素上,
// 始终处于跟随鼠标移动的绘制部分下方”。旧实现把它画在幽灵行之后,
// 条目恰好被拖到落点附近时,蓝线会横穿条目本体。
if (m_signalDragDropRow >= 0)
{
float lineY = 0.0f;
@@ -1310,6 +1316,22 @@ namespace jdi
}
}
// 浮起的阴影:让条目明显"脱离"列表本体(画在指示线之上,遮住穿过的线)。
UiFillRoundRect(g, gd::RectF(natural.X + 2.0f * m_scale, top + 3.0f * m_scale,
natural.Width, height),
4.0f * m_scale, 0x33000000u);
// 复用行绘制:临时把行顶部覆盖到鼠标位置(与「组名悬浮」同一机制),
// 因此名称、复选框、GX 徽标、✕ 等全部随行一起移动,无需另写一套绘制。
m_rowYOverrideActive = true;
m_rowYOverrideY = top;
PaintSignalRow(g, m_signalDragRow);
m_rowYOverrideActive = false;
UiDrawRoundRect(g, gd::RectF(natural.X + 1.0f * m_scale, top + 1.0f * m_scale,
natural.Width - 2.0f * m_scale, height - 2.0f * m_scale),
4.0f * m_scale, m_theme->accent, 1.6f * m_scale);
g.Restore(state);
}
@@ -233,7 +233,7 @@ namespace jdi
items.push_back(cloneAdjusted);
// 「移动到…」二级菜单(用户 2026-11-03 要求,位置在「仅显示该信号」**之上**):
// 条目为「分组 N(组名)」+ 当前所在分组打勾;末尾分割线之下是「新建分组」。
// 条目为「组号 | 组名」+ 当前所在分组打勾;末尾分割线之下是「新建分组」。
// 结构与信号名右侧 GX 徽标点击后的「所属网格」菜单保持一致,但**没有标题行**,
// 且底部动作换成「新建分组」(新建分组后把信号移进去)。
MenuItem moveMenu;
@@ -248,7 +248,9 @@ namespace jdi
continue;
}
MenuItem entry;
entry.label = L"分组 " + std::to_wstring(index + 1) + L"(" + group->name + L")";
// ⚠️ 显示形式为「组号 + 竖线 + 组名」(用户 2026-11-03 明确要求),
// **不要**用括号把组名括起来(旧实现 `分组 1(名称)` 已被要求改掉)。
entry.label = std::to_wstring(index + 1) + L" | " + group->name;
entry.id = CMD_GROUP_MOVE_BASE + static_cast<int>(index);
entry.checked = (static_cast<int>(index) == ref.groupIndex);
moveMenu.submenu.push_back(entry);