fix(app-iced): M79 r2 —— 分隔线守卫换成「锚定细线行 + 整段跨度数段」
代码复审 r2 P1-1:上一条守卫只认 r1 那种「半透明 AA 缝」,四处假通过。
两个结构性原因与对应修法:
1. **窗口被截成 150px**:`longest_h_line` 凑满 150px 就 return,`line_segments` 只在那
150px 里数段 → 分隔线实长 242px,只看前 62%;缺口在窗口外看不见、在窗口内则被
「跳到缺口之后重新起跑」滑过。现在 `find_thin_line` 返回的是**该行整段墨迹跨度**,
`line_segments` 在这个跨度上数段。
2. **行的锚定不稳**:原来「第一条 ≥150px 的连续带」能被五态行的 40px 高底色带、
`Primary` 指示条顶替(mutE/mutF 会量到完全不同的元素)。现在四条判据一起锚定:
跨度 ≥200 · 近乎实心(墨迹 ≥ 跨度×4/5)· 薄(跨中点纵向墨迹 ≤3 行)· 低对比
(线色与底色单通道差 ≤120)。
`TOL=6` 的口径写进注释:**单通道**最大可辨差;40% 覆盖的抗锯齿行实测差 `(9,6,4)`,
6 能看见它又挡住 ±3 渲染噪声;相邻那行是满覆盖(线色差 25/17/10),所以行锚定不会丢。
五次验收(真跑 probe,退出码即判据;变异图在 `target/m79/{mutB,mutC,mutE,mutF}/`
由 r2/r1 图改像素生成,不入库):
| 图 | 结果 | 实测 |
|---|---|---|
| r2 原图 | **rc=0 绿** | 1 段 |
| `before/`(r1 逐 tab + AA 缝) | **rc=101 红** | 3 段,y=471 x24..265 |
| mutB(缺口 x=200..201,旧窗口外) | **rc=101 红** | 2 段,y=471 x24..266 |
| mutC(缺口 x=100..101,旧窗口内) | **rc=101 红** | 2 段,y=471 x24..266 |
| mutE(r1 两个缝位 102/192 挖真缝) | **rc=101 红** | 3 段,**y=471 x24..266**(仍是分隔线那行,不是 y=503 的五态带) |
| mutF(额外:r1 分段线 + 上方 200px Primary 带) | **rc=101 红** | 3 段,y=471(锚定没被 Primary 带顶替) |
门禁:fmt ✓ · `cargo test -p unlucky-app-iced` 197 passed · 全套 probe 对 r2 图 5 passed ·
`clippy --all-targets -D warnings` 干净。未改任何组件实现。
This commit is contained in:
1 parent
55a775fca7
commit
dffae2c429
2 files changed
+77
-26
No files matched your search
@@ -198,20 +198,21 @@ fn tabs_indicator_tracks_the_selected_tab() {
|
||||
);
|
||||
measured.push((bar.width(), ink));
|
||||
}
|
||||
// 6.6:Secondary 的分隔线必须是**一条连续直线**(逐 tab 分段画会在相邻段之间留 1px 亮缝,
|
||||
// 6.6:Secondary 的分隔线必须是**一条连续直线**(逐 tab 分段画会在相邻段之间留缝,
|
||||
// 视觉 r1 P2-2 实测 `25..101 | 103..191 | 193..265`)。
|
||||
//
|
||||
// 判法:1px 的线画在分数像素上 → 真实像素是「线色 ⊗ 页面底色」的**混合值**(实测
|
||||
// `(221,233,245)`,不是 `SurfaceVariant` 的原值),所以不能按精确色找块。改成:
|
||||
// 先找「与页面底色不同、横跨 >150px 的那一行」,再用**该行自己的颜色**数连通段。
|
||||
// 判法(三处都是被真实假通过逼出来的,见 `scan::Shot::find_thin_line` 文档):
|
||||
// ① 按「细 + 近乎实心 + 低对比 + 跨度 ≥200」锚定**分隔线那一行**(跳过标题行、
|
||||
// 五态行的 40px 底色带、Primary 指示条);
|
||||
// ② 在该行的**整段墨迹跨度**上数段——不是在某次 run 的 150px 窗口里数,否则缺口会被滑过。
|
||||
let surface = rgb(ui.color(Role::Surface));
|
||||
let divider = shot
|
||||
.longest_h_line(surface, 150)
|
||||
.expect("Secondary 那一行应有一条长横线");
|
||||
let segments = shot.line_segments(divider.0, divider.1, divider.2);
|
||||
let (y, x0, x1) = shot
|
||||
.find_thin_line(surface, 200, 120)
|
||||
.expect("Secondary 那一行应有一条细长横线");
|
||||
let segments = shot.line_segments(y, x0, x1);
|
||||
assert_eq!(
|
||||
segments, 1,
|
||||
"Secondary 分隔线应是一条直线,实测 {segments} 段(分段画会留缝)"
|
||||
"Secondary 分隔线应是一条直线,实测 {segments} 段(量到 y={y}, x{x0}..{x1})—— 分段画会在交界处留缝"
|
||||
);
|
||||
|
||||
let (short_bar, long_bar) = (measured[0].0, measured[1].0);
|
||||
|
||||
@@ -181,30 +181,80 @@ impl Shot {
|
||||
span
|
||||
}
|
||||
|
||||
/// 全图里**第一行**横跨 ≥ `min_len` 的「与底色不同」的连续像素:返回 `(y, x0, x1)`。
|
||||
/// 找「一条**细、低对比、近乎实心**的长横线」,返回 `(y, x0, x1)`:
|
||||
/// `y` 是线最实的那一行,`x0..x1` 是**该行整段墨迹的跨度**(不是某一次连续 run 的窗口)。
|
||||
///
|
||||
/// 用于找 1px 的分隔线:它的颜色是混合值,不能按精确色找。
|
||||
pub(super) fn longest_h_line(
|
||||
/// 四条判据,每条都对应一次真实的假通过(视觉 r1 P2-2 复核时被点出来的):
|
||||
///
|
||||
/// 1. **跨度 ≥ `min_len`**(200):`Secondary` 分隔线整行 242px;标题/标签行凑不满,能跳过它们。
|
||||
/// 2. **近乎实心**(墨迹数 ≥ 跨度 × 4/5):逐 tab 分段时中间是真缺口,但只有 1~2px,仍算实心
|
||||
/// ——**必须先能找到这一行**,再由 [`Shot::line_segments`] 把缺口数成多段。
|
||||
/// (早先「凑满 150px 就 return,然后在窗口里数段」的写法会滑过缺口,永远数到 1。)
|
||||
/// 3. **薄**(跨中点纵向墨迹 ≤ 3 行):五态行里的 hover 底色是 40px 高的带,被这条排除
|
||||
/// (早先的写法会把那条 224px 的带选中,量到完全不同的元素)。
|
||||
/// 4. **低对比**(线色与底色的单通道最大差 ≤ `max_contrast`):`Primary` 指示条是高饱和色,被排除。
|
||||
///
|
||||
/// `TOL` 的口径:**单通道**最大可辨差,取 6。40% 覆盖的抗锯齿行实测单通道差 `(9,6,4)`,
|
||||
/// 6 能看见它、又挡住 ±3 的渲染噪声;覆盖再低的行会落到判据外,但**相邻那行是满覆盖**
|
||||
/// (实测线色差 25/17/10),所以行的锚定不会丢。
|
||||
pub(super) fn find_thin_line(
|
||||
&self,
|
||||
background: Rgb,
|
||||
min_len: usize,
|
||||
max_contrast: u8,
|
||||
) -> Option<(usize, usize, usize)> {
|
||||
const TOL: u8 = 6;
|
||||
let is_ink = |pixel: Rgb| differs(pixel, background, TOL);
|
||||
let contrast = |pixel: Rgb| {
|
||||
pixel
|
||||
.iter()
|
||||
.zip(background)
|
||||
.map(|(p, q)| p.abs_diff(q))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
for y in 0..self.height {
|
||||
let mut run = 0;
|
||||
let mut start = 0;
|
||||
for x in 0..self.width {
|
||||
if differs(self.at(x, y), background, 6) {
|
||||
if run == 0 {
|
||||
start = x;
|
||||
}
|
||||
run += 1;
|
||||
if run >= min_len {
|
||||
return Some((y, start, x));
|
||||
}
|
||||
} else {
|
||||
run = 0;
|
||||
}
|
||||
let ink: Vec<usize> = (0..self.width).filter(|x| is_ink(self.at(*x, y))).collect();
|
||||
let (Some(&first), Some(&last)) = (ink.first(), ink.last()) else {
|
||||
continue;
|
||||
};
|
||||
let len = last - first + 1;
|
||||
if len < min_len || ink.len() * 5 < len * 4 {
|
||||
continue;
|
||||
}
|
||||
// 薄:跨中点纵向墨迹 ≤ 3 行
|
||||
let mid = (first + last) / 2;
|
||||
let mut top = y;
|
||||
while top > 0 && is_ink(self.at(mid, top - 1)) {
|
||||
top -= 1;
|
||||
}
|
||||
let mut bottom = y;
|
||||
while bottom + 1 < self.height && is_ink(self.at(mid, bottom + 1)) {
|
||||
bottom += 1;
|
||||
}
|
||||
if bottom - top + 1 > 3 {
|
||||
continue;
|
||||
}
|
||||
// 低对比:取这条细带里最实的一行(满覆盖),跨段与线色都按那一行算
|
||||
let best = (top..=bottom)
|
||||
.max_by_key(|candidate| {
|
||||
(first..=last)
|
||||
.filter(|x| is_ink(self.at(*x, *candidate)))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(y);
|
||||
let line = self.at(
|
||||
(first..=last)
|
||||
.find(|x| is_ink(self.at(*x, best)))
|
||||
.unwrap_or(mid),
|
||||
best,
|
||||
);
|
||||
let c = contrast(line);
|
||||
if c > max_contrast || c <= TOL {
|
||||
continue;
|
||||
}
|
||||
return Some((best, first, last));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user