信号面板分组折起动画 + 筛选态折起(项数按筛选计数)+ 悬浮组名可交互与“先滚动再折起” + 适应视图与 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:
1 parent
cb9d3e4b8f
commit
1d314ddc24
5 files changed
+801
-161
No files matched your search
@@ -1684,6 +1684,11 @@ namespace jdi
|
||||
double sharedTime)
|
||||
{
|
||||
// 时间轴:该时刻居中、**一大格 = 1s**(视界 = 大格数 × 1s)。
|
||||
//
|
||||
// 幅值轴与其余场景**共用同一段逻辑**(见函数末尾):`fitYToSelection` 的网格走
|
||||
// `AutoRangeLaneToView`、其余走整段自适应(`ComputeLaneRange`),两者都只由
|
||||
// `PadValueRange` 提供留白。这样「适应视图」与「Y 轴自适应」互为幂等 ——
|
||||
// 同一视界下反复切换两者不会改变区间,也不会关掉 Y 轴自适应开关(用户 2026-10-16)。
|
||||
const double span =
|
||||
static_cast<double>(kTimeTickTarget) * kSinglePointClusterSecondsPerCell;
|
||||
SetTimeView(sharedTime - span * 0.5, sharedTime + span * 0.5);
|
||||
@@ -1701,50 +1706,30 @@ namespace jdi
|
||||
? std::wstring()
|
||||
: L"横向或纵向拖动以放大选择区域,ESC退出选区模式");
|
||||
|
||||
// ---- 逐网格的幅值轴 ----
|
||||
// ⚠️ **必须与 `FitAll()` 的常规路径、以及「Y 轴自适应」(`AutoRangeLaneToView`)同源**:
|
||||
// 旧实现在这条特例里按“散点半径留白 + 跨度 1”另算一套区间,而 Y 轴自适应走
|
||||
// `PadValueRange`,于是对**同一个视界**两者会算出不同的幅值范围
|
||||
//(用户 2026-10-16 报告:单数据点信号上「适应视图」与「Y 轴自适应」的范围不一致);
|
||||
// 并且旧实现还把 `fitYToSelection` 置回 false —— 原本开着「Y 轴自适应」时点一下
|
||||
// 「适应视图」会把这个开关**悄悄关掉**。
|
||||
// 现在两者共用同一段逻辑:留白只来自 `PadValueRange`,且**不改动** Y 轴自适应开关。
|
||||
const float laneHeight = FitLaneHeight();
|
||||
for (std::size_t index = 0; index < perLane.size() && index < m_tab->Lanes().size(); ++index)
|
||||
{
|
||||
const std::vector<FitContribution>& items = perLane[index];
|
||||
double valueLow = 0.0;
|
||||
double valueHigh = 0.0;
|
||||
float radius = 0.0f;
|
||||
bool any = false;
|
||||
for (const FitContribution& item : items)
|
||||
const int laneIndex = static_cast<int>(index);
|
||||
if (m_tab->Lanes()[index].fitYToSelection)
|
||||
{
|
||||
if (!item.singlePoint || !std::isfinite(item.pointValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
valueLow = any ? std::min(valueLow, item.pointValue) : item.pointValue;
|
||||
valueHigh = any ? std::max(valueHigh, item.pointValue) : item.pointValue;
|
||||
radius = std::max(radius, item.dotRadius);
|
||||
any = true;
|
||||
// 已开启「选区 Y 轴自适应」的网格按当前(刚设好的)时间视图重新匹配 ——
|
||||
// 与工具栏开关打开时的算法完全一致,因此两种操作互为幂等。
|
||||
AutoRangeLaneToView(laneIndex);
|
||||
}
|
||||
if (!any)
|
||||
else
|
||||
{
|
||||
continue; // 该网格没有可见单值信号:保持原样
|
||||
m_tab->Lanes()[index].autoRange = true;
|
||||
}
|
||||
|
||||
// 幅值跨度:以容纳"网格内的全部数据点"为下限(退化成单值时取 1,
|
||||
// 与"单值信号居中显示"的观感一致),并保证**每个散点都完整在网格内**。
|
||||
double range = std::max(1.0, valueHigh - valueLow);
|
||||
if (laneHeight > 1.0f && radius > 0.0f)
|
||||
{
|
||||
for (int pass = 0; pass < 2; ++pass)
|
||||
{
|
||||
const double margin =
|
||||
(static_cast<double>(radius) + kSinglePointBoundaryPadPixels) * range /
|
||||
static_cast<double>(laneHeight);
|
||||
range = std::max(1.0, valueHigh - valueLow + 2.0 * margin);
|
||||
}
|
||||
}
|
||||
const double center = (valueLow + valueHigh) * 0.5;
|
||||
|
||||
Lane& lane = m_tab->Lanes()[index];
|
||||
lane.autoRange = false;
|
||||
lane.fitYToSelection = false;
|
||||
lane.yMin = center - range * 0.5;
|
||||
lane.yMax = center + range * 0.5;
|
||||
// 补齐“被单值散点顶住的边界”(散点本来就完整可见时保持不变)。
|
||||
ApplySinglePointValueMargins(laneIndex, perLane[index], laneHeight);
|
||||
}
|
||||
|
||||
InvalidateSceneCache();
|
||||
|
||||
Reference in new issue
Block a user