diff --git a/crates/app-iced/src/components/gallery/probe.rs b/crates/app-iced/src/components/gallery/probe.rs index 6cb6121..838327a 100644 --- a/crates/app-iced/src/components/gallery/probe.rs +++ b/crates/app-iced/src/components/gallery/probe.rs @@ -36,20 +36,32 @@ fn ui(flavor: SchemeFlavor) -> Ui { /// `ULL_GALLERY_SHOT` → 截图路径(相对路径按**工作区根**解释:`cargo test` 的 cwd 是 crate 目录)。 /// 一条 1dp 细线在竖直方向的「墨量覆盖率」:各行相对底色的不透明度之和(1dp ⇒ ≈1.0)。 +/// +/// **只统计「长横线行」**(连续非底色像素 > 900):标题/正文那些行也有几百个零散墨迹像素, +/// 按「任意非底色像素数」筛会把文字算进墨量(首版实测 57.9,明显是文字被算进去了)。 fn line_coverage(shot: &Shot, surface: [u8; 3], line: [u8; 3]) -> f32 { let mut total = 0.0; for y in 0..shot.height { + let mut longest = 0; + let mut current = 0; let mut sum = 0.0f32; - let mut count = 0usize; + let mut run_sum = 0.0f32; for x in 0..shot.width { let pixel = shot.at(x, y); if !matches(pixel, surface) { - count += 1; - sum += delta_ratio(pixel, surface, line); + current += 1; + run_sum += delta_ratio(pixel, surface, line); + if current > longest { + longest = current; + sum = run_sum; + } + } else { + current = 0; + run_sum = 0.0; } } - if count > 300 { - total += sum / count as f32; + if longest > 900 { + total += sum / longest as f32; } } total @@ -171,6 +183,13 @@ fn gallery_geometry_matches_the_tokens() { } let true_height = true_bottom - true_top + 1; + // **精确 40**:变异成 42 必须红。上一版写的是 `(39..=41).contains(..)`, + // reviewer 把高度改成 42 重新出图后断言照样绿 —— 那就是「渲层断言等于没有」。 + assert_eq!( + true_height, 40, + "可见高必须精确 40(12b 1.1);变异成 42 这里就会红" + ); + // 圆角档位:量**顶部那一行的左内缩**。药丸(r = 高/2 = 20)在离顶端 1px 处内缩 // ≈ r − √(2r−1) = 13.8;r=8 只有 ≈4.1;直角为 0。比「数圆角行数」抗锯齿得多。 let top_row_left = (label_button.left..=label_button.right) @@ -182,31 +201,45 @@ fn gallery_geometry_matches_the_tokens() { "药丸圆角应 = 20(12b 1.2):顶部行左内缩应 ≈14,实测 {top_inset}(8 圆角 ≈4,直角 0)" ); - // ---- 12b 1.4 / 1.6:内边距 16、图标↔文字 8(用带前置图标的 Filled 格量)---- - let ink = shot.ink_inside(&label_button, [255, 255, 255]); - if let Some(ink) = ink { - let icon_pad = ink.left - label_button.left; - // 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 的左边距 ⇒ 墨迹落在 21 附近 - assert!( - (19..=23).contains(&icon_pad), - "12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}" - ); - } else { - panic!("Filled 格应带前置图标(量内边距用)"); + // ---- 12b 1.4:带前置图标的左内边距 = 16(用 Filled 格量,避开圆角只扫中间带)---- + // 判「墨迹」的方式:比容器色更接近 OnPrimary。不要用字面量白——浅色页面底色 (246,250,255) + // 与白在容差内,会把圆角外的底色当成墨迹(首次实现就踩了,量出 3)。 + let on_primary = rgb(ui.color(Role::OnPrimary)); + let mut left_ink: Option = None; + for y in (label_button.top + 12)..=(label_button.bottom - 12) { + for x in (label_button.left + 4)..=label_button.right { + if nearer(shot.at(x, y), on_primary, fill) { + left_ink = Some(left_ink.map_or(x, |best: usize| best.min(x))); + } + } } + let icon_pad = left_ink.expect("Filled 格应带前置图标(量内边距用)") - label_button.left; + // 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 左边距 ⇒ 墨迹落在 21 附近 + // (16 → 21;24 → 29;8 → 13,三档互不重叠) + assert!( + (18..=24).contains(&icon_pad), + "12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}" + ); - // ---- Q9:IconButton 的字形不许撑满 40 盒 --- // ---- 12b 1.4 / 1.6:内边距 16、图标↔文字 8(用带前置图标的 Filled 格量)---- - let ink = shot.ink_inside(&label_button, [255, 255, 255]); - if let Some(ink) = ink { - let icon_pad = ink.left - label_button.left; - // 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 的左边距 ⇒ 墨迹落在 21 附近 - assert!( - (19..=23).contains(&icon_pad), - "12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}" - ); - } else { - panic!("Filled 格应带前置图标(量内边距用)"); + // ---- Q9:IconButton 的字形不许撑满 40 盒 --- // ---- 12b 1.4:带前置图标的左内边距 = 16(用 Filled 格量,避开圆角只扫中间带)---- + // 判「墨迹」的方式:比容器色更接近 OnPrimary。不要用字面量白——浅色页面底色 (246,250,255) + // 与白在容差内,会把圆角外的底色当成墨迹(首次实现就踩了,量出 3)。 + let on_primary = rgb(ui.color(Role::OnPrimary)); + let mut left_ink: Option = None; + for y in (label_button.top + 12)..=(label_button.bottom - 12) { + for x in (label_button.left + 4)..=label_button.right { + if nearer(shot.at(x, y), on_primary, fill) { + left_ink = Some(left_ink.map_or(x, |best: usize| best.min(x))); + } + } } + let icon_pad = left_ink.expect("Filled 格应带前置图标(量内边距用)") - label_button.left; + // 图标盒左缘 = 16;字形在 24 viewBox 里自带 ≈5.3 左边距 ⇒ 墨迹落在 21 附近 + // (16 → 21;24 → 29;8 → 13,三档互不重叠) + assert!( + (18..=24).contains(&icon_pad), + "12b 1.4:带图标左内边距应 16(墨迹 ≈21),实测 {icon_pad}" + ); // ---- Q9:IconButton 的字形不许撑满 40 盒 ----------------------------------- let tonal = rgb(ui.color(Role::SecondaryContainer)); @@ -269,10 +302,11 @@ fn gallery_geometry_matches_the_tokens() { // **墨量覆盖率**:一条 1dp 的线,其各行相对底色的不透明覆盖率之和应 ≈1.0。 // 1.5dp 的线会到 ≈1.5,2dp 会到 ≈2.0 —— 这是「容忍 1.5 倍粗」的补丁。 let full = rgb(ui.color(Role::OutlineVariant)); - let coverage = line_coverage(&shot, surface, full); + // 屏上正好 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)" + "单条分隔线墨量覆盖率应 ≈1.0(= 1dp),实测 {coverage:.2}(1.5dp 会到 ~1.5)" ); // ---- 12b 1.8:禁用格必须与静息格不同 ------------------------------------ @@ -300,8 +334,10 @@ fn gallery_geometry_matches_the_tokens() { if bbox.width() < 60 { continue; } - for y in (bbox.top + 2)..=(bbox.bottom - 2) { - for x in (bbox.left + 2)..=(bbox.right - 2) { + // 只扫**内带**:圆角外的页面底色 (246,250,255) 与 OnPrimary(255,255,255) 在容差内, + // 贴边扫描会把圆角透出的底色当成「满档内容」(首版就在 (866,162) 误报)。 + for y in (bbox.top + 12)..=(bbox.bottom - 12) { + for x in (bbox.left + 12)..=(bbox.right - 12) { assert!( !matches(shot.at(x, y), full_content), "禁用容器内出现满档 OnPrimary @({x},{y}):禁用标签没降到 38%" @@ -324,6 +360,7 @@ fn gallery_geometry_matches_the_tokens() { true_height, top_inset, primary.len(), + runs, ink_px, coverage, glyph_width