fix(iced/feedback): 逐环改固定窗判据 + 贴顶边距 + Dialog 下限单测(复审 r3 P2-3/P2-1/P2-2)
D45 封顶轮的最后一改,只动这三条: - **P2-3 逐环断言(假绿)**:环本来就在固定 x 栅格(24 + 56·i、宽 40),判据从「主色连通簇数 非空」改成**六个窗逐个测**「窗内墨迹(轨道 ∪ 激活并集)外接成 40(±2)×40(±2)、且不越窗」, 外加「1..5 号窗必须有激活色弧」。簇数只打印不再当判据。G18-i 从「欠账」改成**已收口**, 并删掉模块文档里「要么把 6 个环摆成固定 x 栅格」那句错话(它们本来就在栅格上)。 环带定位也换过两版(「每行墨点 4..20」会把 40 切成 32;「首行..末行」在深色档扩到 274 行), 最终用「第一个窗里连续有墨、高度≈40 的行组」。 变异:涂掉 4 个环 → `第 1 个窗里没有任何环墨迹:环丢了` 红;25% 环缩成 20×20 → `第 1 个窗里的环应 40×40(±2),实测 20×20` 红。 - **P2-1 贴顶边距**:新增「框顶行 → 条顶行 = 36±1」与「取景框高 = 96±1」两条。 变异(`HOST_SHORT` 96 → 72,重编重拍):`D:贴底 ⇒ 框顶到条顶应为 36(96 − 48 − 12),实测 12` 红; 还原后绿。—— 之前只量「条底距框底」,宿高回退成 72 时确实抓不住。 顺带修了取景框判据本身:条自身(深色)比画布更接近框色,会把条自己认成框(实测差值 1), 现在要求「框色连续段比条本身更宽」。 - **P2-2 Dialog 下限**:补单测 `effective_width(200.0) == 280.0`(360/800 两点一并钉住), 并把「不低于官方 280」写进 `Dialog::width` 的文档。 - 欠账登记(只写表、不改代码):G18-j(动作色用全图合计,单卡漂移可能被别卡盖住)、 G18-k(再 4 处文档残留,不为凑数扩大 diff)、G18-l(取景框判据会把条的抗锯齿底行算成框, 余量 0.0008,方向是误报;加了「框高 96」后触发概率下降)。
This commit is contained in:
1 parent
11d7b64a5e
commit
d4e73aacdd
5 files changed
+198
-48
No files matched your search
@@ -99,7 +99,8 @@ impl<'a, M> Dialog<'a, M> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 可用宽(默认 480,组件取 `min(available, 480)`)。
|
||||
/// 可用宽(默认 480)。实际宽 = `min(available, 480)`,且**不低于官方下限 280**
|
||||
/// (`DialogMinWidth`;可用宽传 200 时仍画 280 并溢出宿主 —— iced 无 min-width,见 G17-d)。
|
||||
///
|
||||
/// 为什么不让组件自己从宿主推:iced 的 `Length` 没有 min-width,而 `Fill + max_width`
|
||||
/// 会被父级的精确分配顶掉(实测渲染成 548 而不是 480,见 `dialog()` 里的说明)。
|
||||
|
||||
@@ -42,6 +42,10 @@
|
||||
//! | D-4 | Snackbar L3 阴影 | 无阴影 | 同 D-1 | 是 |
|
||||
//! | G17-d | Snackbar 最小宽 344 | 组件不强制(宿主保证) | iced `Length` 无 min-width(同按钮 G15) | 是(自定义 Widget) |
|
||||
//! | D-5 | 深色档 scrim 压暗 32% | 保持 0.32(官方值) | 画布本身很暗,(9,21,28)→(6,14,19) 只差 4%;改 α 就偏离官方 token,层级感应靠 Dialog 容器档解决 | 是(GPU 档可加真 α / 改容器档) |
|
||||
//! | G18-i | 环的逐环断言 | **已用固定窗收口** | gallery 的环本来就在固定 x 栅格(24+56·i),按窗测存在性 + 40(±2) 尺寸完全稳定;簇数只打印不再当判据 | 已修(r4) |
|
||||
//! | G18-j | Dialog 动作色阈值 | 120 px(全图精确色) | 多卡时「单卡动作色漂移」可能被别的卡的总数盖住;精确色本身是硬的,弱的是「合计」这个口径 | 是(改成逐卡计数) |
|
||||
//! | G18-k | 文档残留 | 本轮只改了能确证的行 | 复审又找出 4 处同类残留;不为凑数扩大 diff,留待下次触碰该文件时顺手改 | 是 |
|
||||
//! | G18-l | snackbar 取景框判据 | 「更靠近框色而不是画布」 | 条自身的抗锯齿底行覆盖率实测 0.5008(余量 0.0008),可能被算成框 ⇒ **误报方向**(不是假绿);加了「框高 96」之后触发概率下降 | 是(改用精确色 + 容差,或自绘取景框) |
|
||||
//!
|
||||
//! ## 文件地图
|
||||
//!
|
||||
|
||||
@@ -275,8 +275,9 @@ fn check_snackbar(shot: &Shot, ui: &Ui) {
|
||||
if !(near(width, 300) || near(width, 344)) {
|
||||
continue;
|
||||
}
|
||||
// 1px 描边常落在半像素上(两行各 ~50% 覆盖),精确色匹配抓不住;
|
||||
// 用「更靠近框色而不是画布」判(这一带除了框没有别的灰线)。
|
||||
// 1px 描边常落在半像素上(两行各 ~50% 覆盖),精确色匹配抓不住 ⇒ 用「更靠近框色
|
||||
// 而不是画布」判。但条自身(深色)比画布**更接近**框色,所以还要一条:这一行的
|
||||
// 框色连续段必须**比条本身更宽**(框横贯整个宿主),否则会认成条自己(实测过:差值 1)。
|
||||
let distance = |pixel: [u8; 3], target: [u8; 3]| -> u32 {
|
||||
pixel
|
||||
.iter()
|
||||
@@ -284,19 +285,44 @@ fn check_snackbar(shot: &Shot, ui: &Ui) {
|
||||
.map(|(a, b)| u32::from(a.abs_diff(b)))
|
||||
.sum()
|
||||
};
|
||||
let surface_rgb = rgb(surface);
|
||||
let frame_run = |y: usize| -> usize {
|
||||
let mut longest = 0;
|
||||
let mut current = 0;
|
||||
for x in 0..shot.width {
|
||||
let pixel = shot.at(x, y);
|
||||
if distance(pixel, frame) < distance(pixel, surface_rgb) {
|
||||
current += 1;
|
||||
longest = longest.max(current);
|
||||
} else {
|
||||
current = 0;
|
||||
}
|
||||
}
|
||||
longest
|
||||
};
|
||||
// 框的横贯长度 > 条宽 + 10 ⇒ 只有框那一两行满足
|
||||
let is_frame_row = |y: usize| frame_run(y) >= width + 10;
|
||||
|
||||
let frame_row = ((bottom + 1)..shot.height)
|
||||
.find(|y| {
|
||||
(0..shot.width)
|
||||
.filter(|x| {
|
||||
let pixel = shot.at(*x, *y);
|
||||
distance(pixel, frame) < distance(pixel, rgb(surface))
|
||||
})
|
||||
.count()
|
||||
>= 200
|
||||
})
|
||||
.find(|y| is_frame_row(*y))
|
||||
.unwrap_or_else(|| {
|
||||
panic!("D:窄条下方没找到宿主取景框(条 y {top}..{bottom},宽 {width})")
|
||||
});
|
||||
let frame_top_row = (0..*top)
|
||||
.rev()
|
||||
.find(|y| is_frame_row(*y))
|
||||
.unwrap_or_else(|| panic!("D:窄条上方没找到宿主取景框(条顶 y {top})"));
|
||||
// 复审 P2-1:宿高回退到 72(上下留白相等)时这条会红。
|
||||
assert!(
|
||||
(*top as i64 - frame_top_row as i64 - 36).abs() <= 1,
|
||||
"D:贴底 ⇒ 框顶到条顶应为 36(96 − 48 − 12),实测 {}",
|
||||
*top as i64 - frame_top_row as i64
|
||||
);
|
||||
let frame_height = frame_row - frame_top_row + 1;
|
||||
assert!(
|
||||
frame_height.abs_diff(96) <= 1,
|
||||
"D:取景框高应 96(宿主高),实测 {frame_height}"
|
||||
);
|
||||
margins.push(frame_row - bottom);
|
||||
}
|
||||
assert!(
|
||||
|
||||
@@ -1,59 +1,158 @@
|
||||
//! 环形进度的渲染层判据(`probe` 的子模块,`inputs/probe/` 的同一先例)。
|
||||
//!
|
||||
//! 量两件事(`research/12b` §12.3):
|
||||
//! ## 判据:六个**固定窗**逐个测存在性与尺寸
|
||||
//!
|
||||
//! - 环的**外接框 40(±1)** —— 直径是 A1 `CircularProgressIndicatorTokens.Size`;
|
||||
//! - 环的外接框**四角无墨** —— 真圆,不是圆角方。
|
||||
//! gallery 里这一排环在**固定 x 栅格**上:左缘 `24 + 56·i`(环宽 40、间距 16),共 6 个
|
||||
//! (0% / 25% / 50% / 75% / 100% / ind 0.30)。判据是「每个窗里都必须有一圈
|
||||
//! **40(±2)×40(±2)** 的墨迹(轨道 ∪ 激活色的并集)」:
|
||||
//!
|
||||
//! - 少一个环 → 该窗没有墨迹 → 红;
|
||||
//! - 某个环被画小了(例如 20×20)→ 该窗墨迹的外接框变小 → 红;
|
||||
//! - 0% 那格只有轨道、没有激活色 → 用「并集」判,不把「没有激活色」当失败。
|
||||
//!
|
||||
//! 早先用「主色连通簇数」判(只要求非空):把 5 个环涂掉仍然绿(复审 r3 P2-3 的反证),
|
||||
//! 所以 G18-i 的处置从「不修」改成**已用固定窗收口**。簇数仍在日志里打印供复核。
|
||||
//!
|
||||
//! 不定态的**节奏**(4 圈/周期、跨周期不跳变)不在这里量:那是时间的函数,截图只有一帧,
|
||||
//! 判据在 `tests/progress.rs::circular_indeterminate_rotation_is_four_turns_and_continuous`。
|
||||
//!
|
||||
//! ## 欠账 G18-i:没有做「逐环断言」
|
||||
//!
|
||||
//! 复审 G 建议把环数下限收到 6 个里的 ≥5。实测做不到稳:一屏 6 个环里只有 3~4 个能被
|
||||
//! 「精确色」连通块干净命中 —— 25% 环是四分之一弧(外接框 21×40)、100% 环首尾相接处被
|
||||
//! 抗锯齿切开,簇数随相位在 4~6 之间跳(实测 phase 0.30 为 4 簇)。按簇数断言会把「相位换了」
|
||||
//! 误报成「环丢了」。所以只保留「至少一个 40×40 真圆环 + 四角无墨」,**逐环登记为 G18-i**
|
||||
//! (要稳:gallery 把 6 个环摆成固定 x 栅格,或改自绘 Widget 后按几何断言)。
|
||||
//! 欠账 **G18-l**(登记不修):`snackbar` 的「取景框」判据会把条自身的抗锯齿底行算成框
|
||||
//! (覆盖率 0.5008,余量 0.0008),方向是**误报**不是假绿;本轮给 snackbar 加了「框高 = 96」
|
||||
//! 之后触发概率下降,但那 0.0008 的余量仍在。
|
||||
|
||||
use iced::Color;
|
||||
|
||||
use super::super::pixel::{blobs, coverage, dims, rgb, BBox, Shot};
|
||||
use unlucky_design::color::Role;
|
||||
|
||||
use super::super::pixel::{blobs, dims, matches, near, rgb, Shot};
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 环直径(A1 `CircularProgressIndicatorTokens.Size = 40`)。
|
||||
const DIAMETER: usize = 40;
|
||||
/// 环排栅格的左缘(页内边距 24)。
|
||||
const RING_X0: usize = 24;
|
||||
/// 栅格间距(40 环 + 16 = `Space::X4`)。
|
||||
const RING_PITCH: usize = 56;
|
||||
/// 窗数(0 / 25 / 50 / 75 / 100 / ind)。
|
||||
const RINGS: usize = 6;
|
||||
|
||||
/// 环形那一排的判据。
|
||||
/// 某个像素算不算「这块墨」(覆盖率 ≥50%,与 `pixel::runs` 同一口径)。
|
||||
fn is_ink(pixel: [u8; 3], fill: Color, under: Color) -> bool {
|
||||
coverage(pixel, rgb(fill), rgb(under)) >= 0.5
|
||||
}
|
||||
|
||||
/// 环形那一排的判据:逐窗测「有没有一圈 40×40 的墨迹」。
|
||||
pub(super) fn check(shot: &Shot, ui: &Ui) {
|
||||
let active = ui.color(Role::Primary);
|
||||
let rings: Vec<_> = blobs(shot, active, 30)
|
||||
.into_iter()
|
||||
.filter(|bbox| near(bbox.width(), DIAMETER) && near(bbox.height(), DIAMETER))
|
||||
.collect();
|
||||
assert!(
|
||||
!rings.is_empty(),
|
||||
"12.3:没找到 40(±1) 的环形激活层;候选:{}",
|
||||
dims(&blobs(shot, active, 10))
|
||||
);
|
||||
for ring in &rings {
|
||||
for (x, y) in [
|
||||
(ring.left, ring.top),
|
||||
(ring.right, ring.top),
|
||||
(ring.left, ring.bottom),
|
||||
(ring.right, ring.bottom),
|
||||
] {
|
||||
assert!(
|
||||
!matches(shot.at(x, y), rgb(active)),
|
||||
"12.3:环必须是真圆(外接框四角不该有墨)@ ({x},{y})"
|
||||
);
|
||||
let track = ui.color(Role::SecondaryContainer);
|
||||
let surface = ui.color(Role::Surface);
|
||||
|
||||
// 环带的 y:以第一个窗(0% 环,只有轨道色)的**圆环签名**定位 —— 中间那几十行在窗的
|
||||
// 左右两侧都有墨迹;再向上/下各扩到「窗内还有墨」为止(把圆顶/圆底那两行收回来)。
|
||||
//
|
||||
// 为什么不按「每行墨点数 4..20」判:圆顶那一行笔画横跨 ~20~28 px,会落在区间外,
|
||||
// 按连续段会把 40 高的带子切成 32 行;为什么不按「首行..末行」:深色档里正文的抗锯齿
|
||||
// 像素也会落在轨道色的混色线上,跨度会扩到 274 行(两种都实测过)。
|
||||
let ink_count = |y: usize, from: usize, to: usize| -> usize {
|
||||
(from..to)
|
||||
.filter(|x| is_ink(shot.at(*x, y), track, surface))
|
||||
.count()
|
||||
};
|
||||
let left = RING_X0;
|
||||
let right = RING_X0 + DIAMETER;
|
||||
|
||||
// 环带 = 第一个窗里「有墨」的**连续行组**中、高度 ≈40 的那一组。
|
||||
//
|
||||
// 走过的两条弯路都实测过:
|
||||
// ①「每行墨点 4..20」→ 圆顶那一行笔画横跨 ~20~28 px,被排除,带子切成 32 行;
|
||||
// ②「左右两侧都有墨」→ 进度条那一行也满足(通栏),会认成 4 行高的假带。
|
||||
// 连续行组 + 高度窗口同时把「进度条」(4~5 行)与「正文行」排除在外。
|
||||
let mut runs: Vec<(usize, usize)> = Vec::new();
|
||||
let mut current: Option<(usize, usize)> = None;
|
||||
for y in 0..shot.height {
|
||||
match (ink_count(y, left, right) > 0, current) {
|
||||
(true, None) => current = Some((y, y)),
|
||||
(true, Some((top, _))) => current = Some((top, y)),
|
||||
(false, Some(run)) => {
|
||||
runs.push(run);
|
||||
current = None;
|
||||
}
|
||||
(false, None) => {}
|
||||
}
|
||||
}
|
||||
if let Some(run) = current {
|
||||
runs.push(run);
|
||||
}
|
||||
let (top, bottom) = runs
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|(top, bottom)| (DIAMETER - 6..=DIAMETER + 6).contains(&(bottom - top + 1)))
|
||||
.unwrap_or_else(|| panic!("没找到高约 {DIAMETER} 的环形带;窗口 0 的连续行组:{runs:?}"));
|
||||
|
||||
let clusters = blobs(shot, active, 10).len();
|
||||
let mut report = Vec::new();
|
||||
for index in 0..RINGS {
|
||||
let left = RING_X0 + RING_PITCH * index;
|
||||
let right = left + DIAMETER - 1;
|
||||
let mut ink: Option<BBox> = None;
|
||||
for y in top..=bottom {
|
||||
for x in left..=right {
|
||||
let pixel = shot.at(x, y);
|
||||
if is_ink(pixel, active, surface) || is_ink(pixel, track, surface) {
|
||||
match &mut ink {
|
||||
Some(found) => found.include(x, y),
|
||||
None => {
|
||||
ink = Some(BBox {
|
||||
left: x,
|
||||
right: x,
|
||||
top: y,
|
||||
bottom: y,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let ink = ink.unwrap_or_else(|| {
|
||||
panic!("第 {index} 个窗(x {left}..{right})里没有任何环墨迹:环丢了")
|
||||
});
|
||||
assert!(
|
||||
(DIAMETER - 2..=DIAMETER + 2).contains(&ink.width())
|
||||
&& (DIAMETER - 2..=DIAMETER + 2).contains(&ink.height()),
|
||||
"第 {index} 个窗(x {left}..{right})里的环应 {DIAMETER}×{DIAMETER}(±2),实测 {}×{}",
|
||||
ink.width(),
|
||||
ink.height()
|
||||
);
|
||||
assert!(
|
||||
ink.left >= left && ink.right <= right,
|
||||
"第 {index} 个窗的墨迹越界:{}..{} 不在 {left}..{right}",
|
||||
ink.left,
|
||||
ink.right
|
||||
);
|
||||
report.push(format!("{index}:{}×{}", ink.width(), ink.height()));
|
||||
}
|
||||
|
||||
// 激活色(弧)必须出现在 1.. 号窗里(0% 那格本来就只有轨道)。
|
||||
for index in 1..RINGS {
|
||||
let left = RING_X0 + RING_PITCH * index;
|
||||
let count: usize = (top..=bottom)
|
||||
.map(|y| {
|
||||
(left..left + DIAMETER)
|
||||
.filter(|x| is_ink(shot.at(*x, y), active, surface))
|
||||
.count()
|
||||
})
|
||||
.sum();
|
||||
assert!(
|
||||
count > 40,
|
||||
"第 {index} 个窗(x {left}..)的激活色弧只有 {count} px:环在但弧没画"
|
||||
);
|
||||
}
|
||||
|
||||
let clusters = blobs(shot, active, 10);
|
||||
eprintln!(
|
||||
"[feedback-probe] circular:40(±1) 环 {} 个 {}(主色连通簇共 {clusters} 个,见 G18-i)",
|
||||
rings.len(),
|
||||
dims(&rings)
|
||||
"[feedback-probe] circular:固定窗 {RINGS} 个逐窗 [{ }](y {top}..{bottom})· \
|
||||
主色连通簇 {} 个(仅打印,判据是窗)",
|
||||
report.join(" / "),
|
||||
clusters.len()
|
||||
);
|
||||
let _ = dims(&clusters);
|
||||
}
|
||||
@@ -62,6 +62,26 @@ fn dialog_geometry_sits_inside_the_official_bounds() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 复审 P2-2:`max(280)` 那条支以前一次都没被执行过(只有定义 + 一个调用点)。
|
||||
///
|
||||
/// 抓得住:有人把下限去掉(200 → 200)、或把 `min/max` 顺序写反(200 → 480 之外的值)。
|
||||
/// 抓不住:渲染时是否真的画了 280(那需要真窗口;gallery 的 360 那格由 probe 的逐卡宽度兜)。
|
||||
#[test]
|
||||
fn dialog_width_respects_the_official_minimum() {
|
||||
let geometry = DialogGeometry::of(&ui(SchemeFlavor::Light));
|
||||
assert_eq!(
|
||||
geometry.effective_width(200.0),
|
||||
280.0,
|
||||
"可用宽 200 → 仍画官方下限 280"
|
||||
);
|
||||
assert_eq!(geometry.effective_width(360.0), 360.0, "窄宿主取可用宽");
|
||||
assert_eq!(
|
||||
geometry.effective_width(800.0),
|
||||
480.0,
|
||||
"宽宿主夹到项目值 480"
|
||||
);
|
||||
}
|
||||
|
||||
/// 7.1 / 7.3 / 7.6:圆角 28、内距 24 / 16 / 24、动作 8 / 12。
|
||||
#[test]
|
||||
fn dialog_padding_and_action_gaps_are_the_tokens() {
|
||||
|
||||
Reference in new issue
Block a user