fix(design): Outcome::NotReached / 色相环回绕断言 / LowChroma 口径统一 / 文档指向(复审 r1 p2-4 + 色彩 r1 p2-2/3/4)

- Outcome 新增 NotReached:区分「用户没提供」与「更高级成功所以没轮到」,
  UI 不会再对着用户已选的卡说「色卡:未选择」;新增 not_reached_is_distinguishable_from_not_provided
- cards_are_visually_distinct 补色相环回绕间隔(原来只比排序相邻差,350° 的卡会静默通过)
- Failure::LowChroma 的 color 与 chroma 现在永远来自同一个簇(原来一条路径拿 A 簇的色值配 B 簇的彩度);
  两条路径的口径写进文档注释,并提醒 UI 别把数字写成「彩度 X < 门槛 8」
- card.rs:测试名指向修正(card_keys_are_stable_and_unique,测试就在本文件)、
  「对比度 8.4:1」的假示例换成实测区间(light 6.43~6.49 / dark 7.70~7.75)、
  中性彩度那段补上「请求值相同 ≠ 实际表面同冷度」的事实与 D43 去向
This commit is contained in:
AskaEth committed 2026-11-02 11:05:02 +08:00
1 parent 948a3685cd
commit f4c48752c2
6 files changed
+122 -28

No files matched your search

+3 -1
View File
@@ -19,6 +19,7 @@
//! | 情形 | 结果 | `Provenance` 里留下 |
//! |---|---|---|
//! | 没有背景图 | 跳到 ② | `Outcome::Absent` |
//! | 更高级已经成功 | 不再看这一级 | `Outcome::NotReached`(**不等于**「用户没提供」) |
//! | 背景图解码失败 | 跳到 ② | `Outcome::Failed(Skip::Undecodable)` |
//! | 背景图太小 / 像素缓冲不对 / 全透明 | 跳到 ② | `… TooSmall / BadDimensions / NoOpaquePixels` |
//! | 背景图彩度不足(D40 的 `< 8`) | 跳到 ② | `… LowChroma { chroma }` |
@@ -97,7 +98,8 @@ pub fn resolve_source(
flavor: SchemeFlavor,
) -> (Scheme, Provenance) {
let card = preset.map(Card::from).unwrap_or(DEFAULT_CARD);
let mut outcomes = [Outcome::Absent; 3];
// 先假定「没轮到」,逐级评估时再改成 Absent / Failed / Used。
let mut outcomes = [Outcome::NotReached; 3];
for (level, input) in [
(Level::BackgroundImage, background),
@@ -51,7 +51,17 @@ pub enum Outcome {
/// 用了这一级。
Used,
/// 用户没提供这一级(没设背景图 / 没开莫奈取色)。
///
/// 注意与 [`Outcome::NotReached`] 的区别:`Absent` 是「用户没有这一级的输入」,
/// 而「用户提供了、但更高级已经成功所以没轮到」是 `NotReached`。两者对 UI 的含义完全不同——
/// 把后者说成「未选择」,用户明明选了卡却被告知「色卡:未选择」。
Absent,
/// 有输入、但因为更高级已经成功,**没轮到**这一级。
///
/// 只可能出现在低优先级那一侧:背景图成功 → 壁纸与色卡都是 `NotReached`。
/// 色卡即使 `NotReached` 也可能参与生成(它的中性彩度决定表面色温),所以 UI 要看
/// [`Provenance::card`] 而不是把 `NotReached` 当成「没用它」。
NotReached,
/// 有输入但不合格,已跳过。
Failed(Skip),
}
+40 -2
View File
@@ -24,7 +24,7 @@ fn level_one_wins_when_background_is_good() {
);
assert_eq!(provenance.level, Level::BackgroundImage);
assert_eq!(provenance.background(), Outcome::Used);
assert_eq!(provenance.preset(), Outcome::Absent);
assert_eq!(provenance.preset(), Outcome::NotReached);
assert!(provenance.hint_key().is_none());
assert!(check(&scheme).is_clean());
assert!(provenance.seed.is_some());
@@ -122,7 +122,11 @@ fn background_beats_wallpaper_when_both_work() {
SchemeFlavor::Dark,
);
assert_eq!(provenance.level, Level::BackgroundImage);
assert_eq!(provenance.wallpaper(), Outcome::Absent, "② 根本没轮到");
assert_eq!(
provenance.wallpaper(),
Outcome::NotReached,
"② 有输入但根本没轮到——不等于「没提供」"
);
assert!(!provenance.did_fall_back());
}
@@ -202,3 +206,37 @@ fn a_five_percent_accent_drives_level_one() {
assert_ne!(scheme, CardKey::Rose.card().scheme(SchemeFlavor::Light));
assert!(crate::palette::wcag::check(&scheme).is_clean());
}
#[test]
fn not_reached_is_distinguishable_from_not_provided() {
// 复审(色彩 r1 p2-2):`Absent` 曾同时表示「用户没提供」和「更高级成功所以没轮到」,
// UI 照字面写「色卡:未选择」就会与事实相反(用户明明选了卡,而且它的中性彩度参与了生成)。
let pixels = solid(Color::rgb(0xC0, 0x39, 0x2B), 4, 4);
// 一级成功:壁纸与色卡都是「没轮到」,但 card 仍然是用户选的那张。
let (_, provenance) = resolve_source(
Some(ImageInput::pixels(&pixels, 4, 4)),
Some(ImageInput::Undecodable), // 壁纸**提供了**,只是没轮到
Some(CardKey::Forest),
SchemeFlavor::Light,
);
assert_eq!(provenance.level, Level::BackgroundImage);
assert_eq!(provenance.wallpaper(), Outcome::NotReached);
assert_eq!(provenance.preset(), Outcome::NotReached);
assert_eq!(provenance.card, CardKey::Forest, "没轮到 ≠ 没选择");
// 真的没提供:背景图 `None` → Absent;壁纸成功 → 色卡 NotReached。
let (_, provenance) = resolve_source(
None,
Some(ImageInput::pixels(&pixels, 4, 4)),
Some(CardKey::Forest),
SchemeFlavor::Light,
);
assert_eq!(provenance.background(), Outcome::Absent);
assert_eq!(provenance.preset(), Outcome::NotReached);
assert_eq!(
provenance.reason_codes(),
Vec::<(&'static str, &'static str)>::new(),
"没提供 / 没轮到都不是「失败」,不该进原因码"
);
}