docs(app-iced): 值字级注释改真 + probe 只认 buttons/divider + 截图主题钉死回归测试(M229 复审 r2)

1) P1-2 的注释值与量测对齐(四处):`elevated.rs` 的 `PAD`/`Texts::title`/`texts()` 与
   `pages/instances/detail.rs` 还写着「值 14/400」,而实现是 12/400;`PAD` 文档更把 12px 的
   118px 量测挂在「14px 下」。全部改成真值,并把 14px=138px 与 12px=118px 两个量测并列写清。

2) probe 门禁只解决一半:`SECTION=buttons` 绿、`SECTION=all` 仍红(sizes 段的实心块撞「按钮
   40 高」判据),断言文案却写「all 或 buttons」。现在:
   - 只支持 `buttons`(按钮矩阵,实心块判据**只量矩阵区**,后续段不再参与)与 `divider`
     (分隔线两条判据,独立出口);
   - `all` / 其它 / 未给 → 明确失败并给理由(整页比 1180×760 高,分隔线段与按钮矩阵不可能同屏;
     整页图的 chip/卡片会被当按钮量);
   - 模块文档的复现命令补上 `ULL_GALLERY_SECTION`(以前照着跑会 panic);
   - `gallery/mod.rs` 的「五段」→ 七段;`gallery/buttons.rs` 的「8 行 + icon·standard」→ 9 行 + 四式。

3) 截图通道的主题档钉死此前没有回归测试(复审 r2 P2-a)。新增
   `the_screenshot_channel_pins_the_theme_mode_instead_of_reading_the_file`:两份不同的机器配置 +
   同一组截图参数 ⇒ 同一个档位且等于参数;反证非截图通道下两份配置确实分出两档。
   变异自证:把分支改回 `bridge.load().1` → 该测试红(left: Dark, right: Light)。

门禁:fmt --check 0 / check -p unlucky-app-iced --all-targets 0 / test -p unlucky-app-iced 0
(887 passed + 6 integration)/ clippy -p unlucky-app-iced --all-targets -D warnings 0 / 400 行扫描干净。
probe 亲跑:buttons 浅+深两张 sheet OK,divider 一张 OK,`all` 按预期被拒。
This commit is contained in:
AskaEth committed 2026-11-11 01:49:16 +08:00
1 parent 8958b7f67d
commit f5a2610e4d
7 files changed
+205 -69

No files matched your search

+84 -55
View File
@@ -92,11 +92,6 @@ fn delta_ratio(pixel: [u8; 3], surface: [u8; 3], line: [u8; 3]) -> f32 {
/// `ULL_GALLERY_SECTION=buttons`,不满足就明确失败——「跳过」会把「指错了图」变成静默通过。
fn shot_path() -> Option<std::path::PathBuf> {
let raw = std::env::var_os("ULL_GALLERY_SHOT")?;
let section = std::env::var("ULL_GALLERY_SECTION").unwrap_or_default();
assert!(
!section.trim().is_empty(),
"这份 probe 只认 `--screen=gallery` 出的 sheet:请同时给 `ULL_GALLERY_SECTION` (`all` 或 `buttons`)。整页图(页面自己的 chip / 卡片,实心块高 21)会被当成按钮量, 得到的是误导性的红。"
);
let path = std::path::PathBuf::from(raw);
if path.is_absolute() || path.exists() {
return Some(path);
@@ -132,6 +127,31 @@ fn gallery_geometry_matches_the_tokens() {
};
let ui = ui(flavor);
// 这份 probe **只认两种 sheet**(复审 r2 P2-b 的结论):
//
// - `ULL_GALLERY_SECTION=buttons`:按钮矩阵(五式 × 五态 + 图标四式)——本 probe 的主判据;
// - `ULL_GALLERY_SECTION=divider`:分隔线的两条判据(1px / 墨量覆盖率)。
//
// **为什么不支持 `all`**:gallery 整页比 1180×760 的视口高,`all` 的 sheet 首屏里根本看不到
// 分隔线段(实测 0 条长横线),而 sizes 段的实心块又会被「按钮 40 高」的判据量到。一张图装不下
// 两段判据,所以按段出图、按段断言。
//
// **为什么必须给 SECTION**:整页图(下载页/设置页)的 chip 与卡片会被当成按钮量,
// 那是误导性的红,不是缺陷。
let section = std::env::var("ULL_GALLERY_SECTION")
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
assert!(
section == "buttons" || section == "divider",
"这份 probe 只支持 `ULL_GALLERY_SECTION=buttons`(按钮矩阵)或 `divider`(分隔线); 当前 {section:?}。`all` 不行:整页比视口高,分隔线段与按钮矩阵不可能同屏。"
);
// 分隔线那段是**独立出口**:`divider` 段的 sheet 里没有按钮矩阵,判据也不同。
if section == "divider" {
check_divider(&shot, &ui);
return;
}
// ---- 12b 1.1 / 1.2:实心按钮 40 高、药丸圆角 20 ------------------------------
let primary = shot.bbox_of(rgb(ui.color(Role::Primary)));
assert!(
@@ -140,12 +160,32 @@ fn gallery_geometry_matches_the_tokens() {
primary.len()
);
// **只判「按钮矩阵那一段」**:`--screen=gallery` 默认(`ULL_GALLERY_SECTION=all`)会把
// sizes / divider / selection / inputs / feedback / navigation 也画进来,它们各自的实心块
// (chips 底、卡片、stop indicator…)不该被「按钮 40 高」这条判据量到 —— 复审 r2 P2-b 的现场
// 就是 `all` 模式下在 y≈715 撞到一块 21 高的实心块,probe 红在一个根本不在量程里的东西上。
//
// 矩阵区的上下界由**第一块文字按钮**(宽 > 60、高 40)的顶边决定,往下 9 行 + 8px 余量。
let matrix_top = primary
.iter()
.filter(|bbox| {
bbox.area() >= 800 && bbox.width() > 60 && (39..=41).contains(&bbox.height())
})
.map(|bbox| bbox.top)
.min()
.expect("没找到文字按钮的实心块(矩阵不在画面里?)");
let matrix_bottom = matrix_top + rows::ROW_COUNT * rows::ROW_STEP + 8;
let mut label_button = None;
for bbox in &primary {
// 字形(OnPrimary)会把实心块割出小碎片,只判「成块的」(40×40 = 1600)
if bbox.area() < 800 {
continue;
}
// 矩阵之外(后续段)的实心块不在这条判据的量程里。
if bbox.top >= matrix_bottom {
continue;
}
assert!(
(39..=41).contains(&bbox.height()),
"实心块高 {}(参考 40,12b 1.1)@ {:?}",
@@ -263,52 +303,6 @@ fn gallery_geometry_matches_the_tokens() {
}
assert!(measured_glyph, "没量到 IconButton 的字形");
// ---- 22.1:分隔线 1px(按「非底色的长横线」判,不按精确色:线被抗锯齿摊到两行)----
//
// **只在分隔线段被渲染时判**:`ULL_GALLERY_SECTION=buttons` 的 sheet 里没有分隔线,
// 那一段是「没画」而不是「画错了」,断言只会误报(复审 P2 的同一类问题)。
let section = std::env::var("ULL_GALLERY_SECTION")
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let has_divider = section.is_empty() || section == "all" || section == "divider";
let surface = rgb(ui.color(Role::Surface));
let mut runs = 0usize;
let mut coverage = f32::NAN;
if has_divider {
for y in 0..shot.height {
let mut longest = 0;
let mut current = 0;
for x in 0..shot.width {
if !matches(shot.at(x, y), surface) {
current += 1;
longest = longest.max(current);
} else {
current = 0;
}
}
if longest > 300 {
runs += 1;
}
}
// 两条线 × 各摊 1~2 行 ⇒ 2..=4 行
assert!(
(2..=4).contains(&runs),
"分隔线应 2 条(每条 1px,抗锯齿最多摊 2 行)⇒ 2..=4 行,实测 {runs} 行"
);
// **墨量覆盖率**:一条 1dp 的线,其各行相对底色的不透明覆盖率之和应 ≈1.0。
// 1.5dp 的线会到 ≈1.5,2dp 会到 ≈2.0 —— 这是「容忍 1.5 倍粗」的补丁。
let full = rgb(ui.color(Role::OutlineVariant));
// 屏上正好 2 条线(通栏 + 列表内缩),所以把总墨量除以 2 得到「单条线的覆盖率」
coverage = line_coverage(&shot, surface, full) / 2.0;
assert!(
(0.8..=1.3).contains(&coverage),
"单条分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)"
);
} else {
eprintln!("[probe] section={section:?} 未渲染 divider 段,跳过分隔线判据");
}
// ---- 12b 1.8 禁用态:容器 + **每一行**的内容色(A1 逐变体不同)----------------
//
// 上一版把两个 reviewer 都指出的重复块留在这里(`disabled_colors` 被调两次、
@@ -350,18 +344,53 @@ fn gallery_geometry_matches_the_tokens() {
Filled 药丸 {}×{}(参考 高 40 / 宽 ≥58)
顶行左内缩 {}(r=20 应 ≈14;8 圆角 ≈4;直角 0)
实心块 {} 个(含 40×40 IconButton)
分隔线长行 {} 行(每条 1px;未渲染 divider 段时为 0)
禁用容器 (on-surface@10%,按钮族) 命中 · 逐行禁用内容断言 {} 行通过
分隔线墨量覆盖率 {:.2}(1dp 应 ≈1.0)
字形墨迹 {}px(24 盒应 16~26)",
flavor,
label_button.width(),
true_height,
top_inset,
primary.len(),
runs,
rows::ROW_COUNT,
coverage,
glyph_width
);
}
/// `ULL_GALLERY_SECTION=divider` 的两条判据:分隔线 2 条(各摊 1~2 行)与单条墨量覆盖率 ≈1.0。
///
/// 单独成函数:`divider` 段的 sheet 没有按钮矩阵,走不到主流程的按钮判据;而主流程也不需要
/// 分隔线(`buttons` 段的 sheet 里根本没画线,断言只会误报)。
fn check_divider(shot: &Shot, ui: &Ui) {
let surface = rgb(ui.color(Role::Surface));
let mut runs = 0usize;
for y in 0..shot.height {
let mut longest = 0;
let mut current = 0;
for x in 0..shot.width {
if !matches(shot.at(x, y), surface) {
current += 1;
longest = longest.max(current);
} else {
current = 0;
}
}
if longest > 300 {
runs += 1;
}
}
// 两条线 × 各摊 1~2 行 ⇒ 2..=4 行
assert!(
(2..=4).contains(&runs),
"分隔线应 2 条(每条 1px,抗锯齿最多摊 2 行)⇒ 2..=4 行,实测 {runs} 行"
);
// **墨量覆盖率**:一条 1dp 的线,其各行相对底色的不透明覆盖率之和应 ≈1.0。
// 1.5dp 的线会到 ≈1.5,2dp 会到 ≈2.0 —— 这是「容忍 1.5 倍粗」的补丁。
let full = rgb(ui.color(Role::OutlineVariant));
// 屏上正好 2 条线(通栏 + 列表内缩),所以把总墨量除以 2 得到「单条线的覆盖率」
let coverage = line_coverage(shot, surface, full) / 2.0;
assert!(
(0.8..=1.3).contains(&coverage),
"单条分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)"
);
eprintln!("[probe] OK section=divider:分隔线 {runs} 行、单条覆盖率 {coverage:.2}");
}