信号面板分组折起动画 + 筛选态折起(项数按筛选计数)+ 悬浮组名可交互与“先滚动再折起” + 适应视图与 Y 轴自适应同源

① 分组折起/展开改为动画过渡(用户要求:被折起的信号向上收起、下方组随之上升、
   离组名越远的成员收起越快、末段减速、总时长合理):
   - 新增 SignalPanel::m_groupAnims + GroupRowScale():每行速度倍率
     1 + 1.2×ordinal/(行数−1)(越靠下越快收尽),叠加 ease-out cubic;
     时长 = clamp(115 + 14×行数, 140, 300) ms。
   - BuildRows 按比例输出非整行高;低于 0.75 逻辑 px 的行不再产生(不绘制/不命中)。
   - 宿主 WM_TIMER(kScrollTimerId) 同时驱动滚动与折起动画;Render 的定时器判定改为
     ScrollAnimationActive() || GroupAnimationActive()。
   - 新增 SetRowClip/ClearRowClip:动画期间行内容裁到本行矩形(否则 DT_VCENTER
     垂直居中的文本会溢出到相邻行)。

② 筛选状态下的分组折起 + 项数按筛选结果计数:
   - 筛选模式改用独立的 m_filterCollapsed(默认展开),不再“筛选时忽略折叠状态”,
     也不再把筛选里的临时折起污染到普通列表的布局选择。
   - ListRow::displayCount 记录当前模式下列出的信号数;组名后方「N 项」只读它
     (筛选时 = 该组内符合筛选条件的信号个数)。
   - 筛选文本变化 / 清空筛选 / 数据变化 ⇒ 清空临时折起状态 + 进行中的动画 + 挂起请求。

③ 悬浮组名可正常交互 + 点击时“先滚动就位再折起”:
   - 根因:HitRow 只按滚动偏移把指针映射到内容位置,而悬浮组名是画在列表顶部的覆盖层
     (其自然位置已滚出列表)⇒ 点击穿透到它下面那一行(折叠三角 / 复选框 / ✕ /
     名称区域全都点不动)。
   - 修法:HitRow 先用 StickyGroupRowIndex() / StickyGroupRowTop() 判定是否落在悬浮行
     矩形内,命中则临时打开 m_rowYOverrideActive 并调用新的 RowHitPart() 算行内区域;
     同时跳过高度为 0 的行(折起动画中已收尽,否则空行会白白吸走一次点击)。
   - 新增 ToggleGroupFromRow():若该行正悬浮(滚动偏移大于该行的内容偏移),先
     StartScrollAnimationTo(该行内容偏移, groupIndex) 并把挂起请求记在
     m_pendingGroupToggle,滚动到位后才启动折起动画;分组行的单击与双击折叠三角都走
     这个入口。滚轮 / 拖滚动条 / 筛选变化 / 切标签页一律清掉挂起请求。

④ 「适应视图」与「Y 轴自适应」彻底同源(单数据点信号):
   - 根因:FitSinglePointCluster 给每个网格另算一套幅值区间(“散点半径留白 + 跨度 1”),
     而 AutoRangeLaneToView 走 PadValueRange ⇒ 同一视界下两者结果不同;且它显式写
     lane.fitYToSelection = false ⇒ 点「适应视图」会把「Y 轴自适应」悄悄关掉。
   - 修法:单点簇只负责时间轴(以该时刻居中 + 一大格 1s),逐网格幅值轴与常规路径 /
     Y 轴自适应完全同源(fitYToSelection → AutoRangeLaneToView,否则 autoRange = true,
     两者之后都接 ApplySinglePointValueMargins),并且绝不改写 fitYToSelection。
   - 有意的行为变化:单点信号「适应视图」的幅值跨度由旧的 1 变为 2×max(1,|值|×0.1)
     (即与 PadValueRange 一致);§9.33 用例 [A] 里“幅值跨度 1”的期望已作废,
     散点仍在网格正中且完整可见(ApplySinglePointValueMargins 兜底)。

验证(离屏 SW_HIDE,不占用鼠标 / 键盘 / 屏幕;进程与线程均 BELOW_NORMAL):
   - 新增 .preflight/perf/panelanim_main.cpp(panelanim.exe)94/94 通过(连跑 3 次稳定):
     折起/展开的分帧过渡与内容高度单调收敛、越靠下的行越矮、末态精确无残留、
     筛选计数与独立折起状态、悬浮组名四个可交互区域全部命中且下层行仍可点击、
     点击后“先滚动到位再折起”、真实 MF4-Record.MF4 单点信号下两种自适应逐位一致且往返稳定。
   - A/B 三组回退(回退绘图区修复 / 禁用悬浮命中优先 / 动画时长置 0)分别 4 / 13 / 8 项如实失败。
   - 既有 verify 49/49、datacheck 69/69、edgecheck 46/46;make clean && make all 零警告。
     (顺带修正本机 verify 探针 4 条与远端新规则不符的过期断言:幅值留白改为调用唯一的
     PadValueRange、派生信号 offsetTime 断言 0(§9.21 已烘入数据)、拖拽换位按“被拖网格
     自身边界”判据的新目标值。)

记忆:Documents/agent_mem.md 新增 §9.35 与 §11.2 第 76–81 条,并更新第 70 条与 §9.33 备注。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-16 01:25:54 +08:00
1 parent cb9d3e4b8f
commit 1d314ddc24
5 files changed
+801 -161

No files matched your search

+187 -1
View File
@@ -91,6 +91,9 @@ namespace jdi
m_hoveredSignal = SignalRef();
m_hoverRow = -1;
m_hoverPart = RowPart::NONE;
// 分组下标可能整体位移 ⇒ 折起动画与挂起的折起请求也不能继续沿用。
m_groupAnims.clear();
m_pendingGroupToggle = -1;
}
/**
@@ -327,6 +330,15 @@ namespace jdi
/** @brief 滚动动画是否正在进行。 */
bool ScrollAnimationActive() const { return m_scrollAnimActive; }
/**
* @brief 推进一步分组折起/展开动画(由宿主定时器驱动)。
* @return 仍有分组动画在进行返回 true(宿主应保持定时重绘)。
*/
bool AdvanceGroupAnimations();
/** @brief 分组折起/展开动画是否正在进行。 */
bool GroupAnimationActive() const { return !m_groupAnims.empty(); }
/**
* @brief 供绘图区联动高亮的「悬停信号」。
*
@@ -409,6 +421,38 @@ namespace jdi
int groupIndex = -1; /**< 所属文件下标。 */
int signalIndex = -1; /**< 信号下标(仅信号行有效)。 */
float height = 0.0f; /**< 行高(像素)。 */
/**
* @brief 分组行显示的项数(仅分组行有效)。
*
* 普通模式 = 该分组的信号总数;筛选模式 = 该分组内**符合筛选条件**的信号个数
*(用户 2026-10-16 要求:筛选时组名后方的项数按筛选结果计数)。
*/
int displayCount = 0;
/**
* @brief 该行所属分组内「参与列出」的序号(仅信号行有效)。
*
* 用于折起动画按「离组名越远越快」计算每行的收缩比例。
*/
int ordinal = 0;
};
/**
* @brief 一个分组的折起/展开动画状态。
*
* 用户 2026-10-16 要求:折起分组时不能下一帧立刻切换布局,而要有一段过渡动画 ——
* 被折起的信号向上收起、下方的分组随之上升;**离组名越远的成员收起越快**,
* 随后整体减速直到收起完成;总时长控制在一个合理范围(百余毫秒到 300ms)。
*
* 动画期间 `BuildRows` 会按 `GroupRowScale()` 给出每行的收缩比例,
* 因此列表内容高度连续变化、下方分组平滑上升。
*/
struct GroupAnim
{
int groupIndex = -1; /**< 目标分组下标。 */
bool collapsing = false; /**< true = 正在折起,false = 正在展开。 */
int rowCount = 0; /**< 该分组参与动画的信号行数。 */
std::uint64_t startMs = 0; /**< 动画起始时刻(`GetTickCount64()`)。 */
std::uint32_t durationMs = 0; /**< 动画总时长(毫秒)。 */
};
/**
@@ -423,6 +467,97 @@ namespace jdi
*/
bool SignalScrollTarget(const SignalRef& ref, float& desired) const;
/**
* @brief 列表内容中某一行顶部相对内容原点的纵向偏移(不包含滚动偏移)。
* @param [IN] row 行下标。
* @return 偏移量(像素)。
*/
float ContentOffsetBeforeRow(int row) const;
/**
* @brief 启动一段滚动动画。
* @param [IN] desired 目标滚动偏移(内部会夹取到合法范围)。
* @param [IN] pendingGroupToggle 滚动到位后要折起/展开的分组下标(-1 = 无)。
*/
void StartScrollAnimationTo(float desired, int pendingGroupToggle = -1);
/**
* @brief 判断某分组在**当前模式**(筛选 / 普通)下是否处于折起状态。
* @param [IN] groupIndex 分组下标。
* @return 折起返回 true。
*/
bool IsGroupCollapsed(int groupIndex) const;
/**
* @brief 设置某分组在**当前模式**下的折起状态。
*
* ⚠️ 筛选模式与普通模式各有一套折起状态(`m_filterCollapsed` / `TabConfig`):
* 筛选只是临时的一个子集视图,不应污染用户在普通列表里的布局选择。
* @param [IN] groupIndex 分组下标。
* @param [IN] collapsed 是否折起。
*/
void SetGroupCollapsed(int groupIndex, bool collapsed);
/**
* @brief 某分组当前会列出的信号数量(筛选模式下为符合筛选条件的个数)。
* @param [IN] groupIndex 分组下标。
* @return 数量。
*/
int ShownSignalCount(int groupIndex) const;
/**
* @brief 查找某分组的折起动画条目下标。
* @param [IN] groupIndex 分组下标。
* @return 条目下标;不存在返回 -1。
*/
int FindGroupAnimation(int groupIndex) const;
/**
* @brief 启动某分组的折起/展开动画。
* @param [IN] groupIndex 分组下标。
* @param [IN] collapsing 是否折起。
* @param [IN] rowCount 该分组参与动画的信号行数。
*/
void StartGroupAnimation(int groupIndex, bool collapsing, int rowCount);
/**
* @brief 取某分组动画中第 ordinal 行的收缩比例。
*
* 规则(用户 2026-10-16 要求):**离组名越远的成员收起速度越快**,整体减速(ease-out)
* 直到完成;因此越靠下的行越早消失,紧接着下方分组平滑上升。
*
* @param [IN] groupIndex 分组下标。
* @param [IN] ordinal 该行在分组内「参与列出」的序号(0 = 紧邻组名的第一行)。
* @param [IN] rowCount 该分组参与动画的行数。
* @return 1 = 完整高度,0 = 完全收起。
*/
float GroupRowScale(int groupIndex, int ordinal, int rowCount) const;
/**
* @brief 切换分组折起状态并播放动画(不处理“悬浮先滚动”的前置动作)。
* @param [IN] groupIndex 分组下标。
*/
void ToggleGroupCollapsedAnimated(int groupIndex);
/**
* @brief 点击分组行:若该组名此刻正悬浮在列表上方,先平滑滚动到顶部再播放折起动画。
*
* 用户 2026-10-16 要求:悬浮状态下点击组名折起时,滚动条要先向上滚动,
* 直到悬浮组名处于**真正完整显示**的位置(即它的自然位置),再执行折起动画。
* @param [IN] groupIndex 分组下标。
* @param [IN] row 分组行下标。
*/
void ToggleGroupFromRow(int groupIndex, int row);
/**
* @brief 计算某行的可交互区域(使用 `RowRect` 的当前几何,含悬浮位置覆盖)。
* @param [IN] row 行下标。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 行内区域类型。
*/
RowPart RowHitPart(int row, float x, float y) const;
/**
* @brief 重新计算「目标」行内信号名与 GX 标签的矩形。
*
@@ -438,7 +573,13 @@ namespace jdi
void PaintTargetRow(gd::Graphics& g);
/** @brief 取消正在进行的滚动动画(用户主动滚动时调用)。 */
void CancelScrollAnimation() { m_scrollAnimActive = false; }
void CancelScrollAnimation()
{
m_scrollAnimActive = false;
// 挂起的“滚动到位后折起”也必须取消:用户已经自己滚动了,
// 再补一次折起会让人莫名其妙。
m_pendingGroupToggle = -1;
}
/**
* @brief 重新构建列表行集合。
@@ -501,6 +642,22 @@ namespace jdi
*/
gd::RectF SignalRemoveRect(int row) const;
/**
* @brief 把后续行内绘制裁剪到指定行矩形(与列表矩形相交)。
*
* 折起动画期间行高连续缩小,行内容必须裁到行矩形内,否则垂直居中的文本会溢出到相邻行。
* @param [IN] rowRect 行矩形。
*/
void SetRowClip(const gd::RectF& rowRect)
{
m_rowClipActive = true;
m_rowClipRect = rowRect;
m_rowClipRect.Intersect(m_listRect);
}
/** @brief 结束行内裁剪(恢复为整个列表矩形)。 */
void ClearRowClip() { m_rowClipActive = false; }
/**
* @brief 绘制文件分组标题行。
* @param [IN,OUT] g 绘图上下文。
@@ -844,6 +1001,35 @@ namespace jdi
float m_scrollAnimTo = 0.0f;
std::uint64_t m_scrollAnimStartMs = 0;
std::uint32_t m_scrollAnimDurationMs = 0;
/**
* @brief 滚动到位后要折起/展开的分组下标(-1 = 无)。
*
* 悬浮组名上点击折叠时,必须先滚动到该组名完整可见,再播放折起动画
*(用户 2026-10-16 要求)。
*/
int m_pendingGroupToggle = -1;
/** @brief 正在进行的分组折起/展开动画(可同时有多个分组)。 */
std::vector<GroupAnim> m_groupAnims;
/**
* @brief 筛选模式下的分组折起状态(按分组下标)。
*
* 用户 2026-10-16 要求筛选结果也能以组为单位折起,但筛选只是一个临时视图,
* 因此使用与普通列表**分开**的一套状态,避免污染用户在普通列表里的布局选择。
* 表长不足 / 越界 = 展开(筛选结果默认展开,便于直接看到匹配项)。
*/
std::vector<bool> m_filterCollapsed;
/**
* @brief 当前正在绘制的行的裁剪矩形(用于折起动画中的部分高度行)。
*
* 行高在折起动画期间连续缩小,若不把行内容裁剪到行矩形内,
* 文本会溢出到相邻行上(`UiText` 是 `DT_VCENTER` 垂直居中,
* 在变矮的矩形里会被裁掉上下两半,这正是想要的“挤压”观感)。
*/
bool m_rowClipActive = false;
gd::RectF m_rowClipRect;
StyleEditField m_editField = StyleEditField::NONE; /**< 当前处于输入状态的字段。 */
int m_editRow = -1; /**< 行内输入所作用的列表行下标。 */