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
@@ -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