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

+18 -7
View File
@@ -115,12 +115,22 @@ pub(crate) fn rank(clusters: &[Cluster]) -> Vec<Ranked> {
ranked
}
/// 候选里最高的 CAM16 彩度——被 `Score` 全部过滤时用它生成「这图太灰了」的提示。
pub(crate) fn max_chroma(clusters: &[Cluster]) -> f64 {
clusters
.iter()
.map(|c| Hct::from_color(c.color).chroma)
.fold(0.0_f64, f64::max)
/// 彩度最高的那个簇——被 `Score` 全部过滤时,它是「最接近候选门槛」的那一族。
///
/// 取色失败(`Failure::LowChroma`)时**色值与彩度都从这一个簇取**,保证二者成对:
/// 之前的两条路径一条报 `Score` 榜首、一条报「彩度最高」,而后者还拿着**另一个**簇的色值,
/// 于是同一个 `LowChroma` 里的 `color` 与 `chroma` 可能不是一回事(复审 r1 p2-4.2)。
///
/// 同彩度时取像素多的那一个(结果确定,不受簇顺序影响)。
pub(crate) fn most_chromatic(clusters: &[Cluster]) -> Option<&Cluster> {
clusters.iter().max_by(|a, b| {
let key = |cluster: &Cluster| (Hct::from_color(cluster.color).chroma, cluster.population);
let (ca, pa) = key(a);
let (cb, pb) = key(b);
ca.partial_cmp(&cb)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| pa.cmp(&pb))
})
}
fn hue_bucket(hue: f64) -> usize {
@@ -157,7 +167,8 @@ mod tests {
cluster("#0A0A0A", 300),
];
assert!(rank(&clusters).is_empty());
assert!(max_chroma(&clusters) < CUTOFF_CHROMA);
let most = most_chromatic(&clusters).expect("灰图也有簇");
assert!(Hct::from_color(most.color).chroma < CUTOFF_CHROMA);
}
#[test]