fix(app-iced): 修 about_center 的平移项(模态从「斜滑」回到「绕中心缩放」)+ 补几何断言

复审 r3 的 P1:about_center 写成 T(-c)·S·T(c),按 iced_core 的「先右后左」算出来是
p → s·p − (1−s)c,不动点在 (−cx,−cy)(窗口外的镜像点)⇒ 卡片开框时从左上斜滑约
(1−s)·c,而不是绕中心缩放。改成 T(c)·S·T(-c)(= s·p + (1−s)c,不动点就是 c),注释同步。

根因是 zoom.rs 里 0 条 #[test](矩阵从没被断言过,967 条全绿抓不住)。新增
tests::about_center_keeps_the_center_fixed:中心不动(<1e-3)、尺寸 = 倍率、缩小时左上角
收向中心;三组矩形 × 三档倍率。把 translate 换回去即红。

顺带补齐「抓得住/抓不住」:components/motion/** 与 motion_sync.rs、platform/motion.rs 里
31 条用例现在全部两侧都有(脚本核对 missing=0)。
This commit is contained in:
AskaEth committed 2026-11-12 10:50:04 +08:00
1 parent b6127b4735
commit add8617494
4 files changed
+86 -6

No files matched your search

@@ -320,8 +320,9 @@ mod tests {
/// **退场窗口内那个框已经关掉、且不可再交互**(tower 的硬要求)。四件事:①逻辑状态当拍就空;
/// ②画面还在退场(`dialog_model` 仍 `Some`);③150ms 内再点一次**什么都不发生**(`refreshes`
/// 不涨 = `on_dialog` 的闸门);④**Esc 那条路径**也挡住(`escape` 的闸门)。**抓不住**:几何级
/// 命中(单测没有 iced 布局树,`zoom.rs` 也不改命中矩形)——那一层由闸门保证。
/// 不涨 = `on_dialog` 的闸门);④**Esc 那条路径**也挡住(`escape` 的闸门)。
/// **抓得住**:删掉任一道闸门(`on_dialog` / `escape`)或把逻辑关闭改成延后,①②③④ 各自红。
/// **抓不住**:几何级命中(单测没有 iced 布局树,`zoom.rs` 也不改命中矩形)——那一层由闸门保证。
#[test]
fn the_exiting_dialog_is_already_closed_and_swallows_clicks() {
use crate::app::dialog::Message as DialogMessage;
@@ -310,6 +310,10 @@ fn none_scale_is_strictly_zero_not_merely_small() {
// ------------------------------------------------------------------ 触发判据
/// 重复绘制**永远不会**重新起跑(否则每帧都重启动画 = 永远停不下来)。
///
/// 抓得住:把「标识没变 ⇒ 不起跑」的判断写错(比如每个 view 都 `play_page`)——30 次重复绘制后
/// `is_animating()` 仍为真、帧泵永远停不下来,本条红。
/// 抓不住:真实屏幕上"重画是否真的只是重画"(那要看 iced 的订阅表,归 `app::subscription` 的用例)。
#[test]
fn repeated_paints_never_restart_the_animation() {
let mut pump = standard();
@@ -324,6 +328,9 @@ fn repeated_paints_never_restart_the_animation() {
}
/// 不归 pages 画的屏(账户 / AI 独立页)**不起跑**;回到某一屏照样跑。
///
/// 抓得住:把 `screen == None` 也当成一次换页起跑(会白跑几百毫秒的帧却没人采样)。
/// 抓不住:账户页 / AI 页自己的渲染(它们不走 `pages::render`,不归本 pump 管)。
#[test]
fn screens_rendered_elsewhere_never_start_a_track() {
let mut pump = standard();
+72 -4
View File
@@ -42,16 +42,22 @@ where
})
}
/// 绕**中心**缩放的变换:`T(-c) · S · T(c)`(`Mul` 的乘法序 = 先右后左,见 iced_core 的
/// `impl Mul for Transformation`)。
/// 绕**中心**缩放的变换:`T(c) · S · T(-c)`。
///
/// `Mul` 的乘法序是**先右后左**(`impl Mul for Transformation` 用 `self.0 * rhs.0`,点按
/// `p' = M·p` 变换)⇒ 这个式子算出来是 `p → s(p − c) + c = s·p + (1−s)c`,**不动点正是 `c`**。
///
/// ⚠️ 上一次写反了:`T(-c) · S · T(c)` 会得到 `s·p − (1−s)c`,不动点在 `(−cx, −cy)` ——窗口外的
/// 镜像点,于是"缩放"变成了"从左上斜滑进来"(复审 r3 用真 iced_core 实跑抓到:card 中心
/// (700,450) 漂到 (644,414))。`tests::about_center_keeps_the_center_fixed` 把这条钉住。
fn about_center(bounds: Rectangle, scale: f32) -> Transformation {
let (cx, cy) = (
bounds.x + bounds.width / 2.0,
bounds.y + bounds.height / 2.0,
);
Transformation::translate(-cx, -cy)
Transformation::translate(cx, cy)
* Transformation::scale(scale)
* Transformation::translate(cx, cy)
* Transformation::translate(-cx, -cy)
}
struct Scaled<'a, Message, Theme, Renderer> {
@@ -184,3 +190,65 @@ where
.overlay(tree, layout, renderer, viewport, translation)
}
}
#[cfg(test)]
mod tests {
use super::*;
use iced::{Point, Size};
/// **绕中心缩放:中心不动、尺寸按倍率**(复审 r3 的 P1 就是这条从未被断言过)。
///
/// 抓得住:①把两个 `translate` 乘反(上一版的 bug:不动点落在 `(−cx, −cy)`,窗口外的镜像点
/// ⇒ 卡片从左上斜滑进来)——中心断言立刻以几十像素的偏差红;②倍率写错 / 漏乘(尺寸断言红);
/// ③把 `about_center` 改成纯缩放(不含平移)——同样红。
/// 抓不住:**像素**。这里断言的是几何矩阵;两个渲染后端都按同一个 `Transformation` 画
/// (`filled`/`text` 的 `bounds * transformation`),所以"几何等价 ⇒ 像素等价"成立的前提是
/// **后端真的把矩阵乘上了**——那一条由真机截图看,不由本用例保证。
#[test]
fn about_center_keeps_the_center_fixed() {
// 默认窗口 1180×760 里居中的卡片(DialogGeometry::max_width = 560)
for (rect, scale) in [
(
Rectangle::new(Point::new(310.0, 220.0), Size::new(560.0, 320.0)),
0.96_f32,
),
(
Rectangle::new(Point::new(310.0, 220.0), Size::new(560.0, 320.0)),
0.5,
),
(
Rectangle::new(Point::new(0.0, 0.0), Size::new(384.0, 288.0)),
1.04,
),
] {
let matrix = about_center(rect, scale);
let center = rect.center();
let moved = center * matrix;
assert!(
(moved.x - center.x).abs() < 1.0e-3 && (moved.y - center.y).abs() < 1.0e-3,
"scale={scale}: 中心必须不动,实测 ({}, {}) 应为 ({}, {})",
moved.x,
moved.y,
center.x,
center.y
);
let size = rect.size() * matrix;
assert!(
(size.width - rect.width * scale).abs() < 1.0e-3
&& (size.height - rect.height * scale).abs() < 1.0e-3,
"scale={scale}: 尺寸必须是 {scale} 倍,实测 {size:?}"
);
// 缩小时左上角必须往中心方向收(不是往外飞)
if scale < 1.0 {
let corner = rect.position() * matrix;
assert!(
corner.x > rect.x && corner.x < center.x,
"scale={scale}: 左上角 x 应收向中心,实测 {}",
corner.x
);
}
}
}
}
+4
View File
@@ -208,6 +208,10 @@ mod tests {
use super::*;
/// 三态必须**三**个值可区分:`Unknown` 不能被当成 `Reduced`(M256 的硬约束 C)。
///
/// 抓得住:把 `is_reduced()` 写成「不是 AnimationsOn 就算 Reduced」(= 探针失败就静默关动效,
/// 于是某些机器上动效神秘消失)——第一条断言立刻红。
/// 抓不住:真机上这三个值读得对不对(那要改系统设置,见报告的未验清单)。
#[test]
fn unknown_is_not_reduced() {
assert!(!ReduceMotion::Unknown.is_reduced());