From 4b059201fd5586eeabefcc168bb703adae448acd Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 11:29:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(design):=20=E4=B8=AD=E6=80=A7=E8=89=B2?= =?UTF-8?q?=E7=9B=B8=E5=B8=B8=E9=87=8F=E6=94=B9=E5=AE=B9=E5=B7=AE=E6=96=AD?= =?UTF-8?q?=E8=A8=80=20+=20=E5=8F=96=E8=89=B2=E6=8E=A5=E7=BA=BF=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=8C=87=E5=90=91=E7=9C=9F=E6=AD=A3=E7=9A=84=E5=AE=88?= =?UTF-8?q?=E6=8A=A4=E6=B5=8B=E8=AF=95=EF=BC=88=E5=A4=8D=E5=AE=A1=20r1=20p?= =?UTF-8?q?2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `palettes.rs::neutral_hue_is_the_default_seed_hue`:`to_bits()` 位相等 → 1e-9° 容差。 色相来自 `atan2(a).to_degrees()`,`NEUTRAL_HUE_DEG` 的字面值是在 Windows(UCRT) 上取的, CI 的 ubuntu(glibc) 理论上可差 1 ulp 就让位相等在另一个 OS 上红;而这条要抓的是「换了 `DEFAULT_SEED` 却没复核常量」(偏几十度),1e-9° 足够且不受平台末位差影响。 `tests/card_neutral.rs::sky_card_is_bit_identical_to_the_product_baseline` 是**同平台内** 两个算出来的值互比,继续用位相等(p1 硬约束的钉子,不动)。 常量文档同步说明「为什么不用 `to_bits`」。 - `resolve/mod.rs` 取色分支的注释:原来只写「`tests/card_neutral.rs` 有反证」,而那条用例 测的是 `Seed::scheme` 生成器层、覆盖不到这里(复审 r1 p1-1 的病根)。改成点名 `resolved_image_scheme_keeps_tinting_from_the_seed`——走的就是本函数的公开入口。 `palettes.rs` 仍是 400 行(本轮改 doc 同时压缩了相邻说明,没突破上限)。 --- crates/design/src/color/palettes.rs | 43 ++++++++++++------------ crates/design/src/palette/resolve/mod.rs | 4 ++- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/crates/design/src/color/palettes.rs b/crates/design/src/color/palettes.rs index c1aa192..e2a492e 100644 --- a/crates/design/src/color/palettes.rs +++ b/crates/design/src/color/palettes.rs @@ -66,24 +66,23 @@ pub const DEFAULT_NEUTRAL_CHROMA: f64 = 12.0; /// /// ## 出处:这就是「天蓝」档当前用的那个中性色相 /// -/// 值等于 [`DEFAULT_SEED`](`#3C8DBC`)的 CAM16 色相 **239.51420577630955°**。天蓝档的 -/// 中性调色板本来就是这个色相(`Palettes::from_seed_with_neutral_chroma` 让中性色跟着种子走), -/// 所以它是**沿用**,不是「另选一个冷调」:`Card::scheme` 换用它之后,天蓝卡的 49 个角色 -/// 必须逐值不变(`palette::card` 与 `tests/card_neutral.rs` 都钉了这条)。 +/// 值等于 [`DEFAULT_SEED`](`#3C8DBC`)的 CAM16 色相 **239.51420577630955°**。天蓝档的中性 +/// 调色板本来就是这个色相(`from_seed_with_neutral_chroma` 让中性色跟着种子走),所以它是 +/// **沿用**,不是「另选一个冷调」:`Card::scheme` 换用它之后天蓝卡的 49 个角色必须逐值不变 +/// (`tests/card_neutral.rs` 钉着)。 /// /// ## 为什么要有这个常量(D43) /// -/// 八张内置色卡原先走 TonalSpot 的「中性色也跟种子染色」,于是切卡连窗口底色一起换调子: -/// 浅色档 `surface` 的实测 CAM16 彩度从 **1.29(rose)跨到 12.03(teal)**——tone 98 处暖相 -/// 色域装不下彩度 12,同一个「请求值 12」在暖相上只能兑现 1.29。差 9 倍的底色跳变让用户在 -/// 设置页逐张试色时觉得「每张卡都像换了个应用」。D43 的处置是:**内置色卡固定中性色相, -/// 切卡只换强调色**;动态取色(背景图 / 壁纸)保留逐色染色,那才是用户要的 Monet 体验。 +/// 八张卡原先走 TonalSpot 的「中性色也跟种子染色」,切卡连窗口底色一起换调子:浅色档 +/// `surface` 的实测 CAM16 彩度从 **1.29(rose)跨到 12.03(teal)**——tone 98 处暖相色域装 +/// 不下彩度 12,「请求值 12」在暖相上只能兑现 1.29,9 倍跳变让用户觉得「每张卡都像换了个 +/// 应用」。D43 的处置:**色卡固定中性色相、只换强调色**;动态取色(背景图 / 壁纸)保留逐色 +/// 染色,那才是用户要的 Monet 体验。所以本常量只服务色卡路径,**不要**传给 `Seed::scheme`。 /// -/// 所以这个常量只服务于「色卡路径」,**不要**把它传给取色路径(`Seed::scheme`)。 -/// -/// ⚠️ 换 [`DEFAULT_SEED`] 时本常量会「过期」:`neutral_hue_is_the_default_seed_hue` 用位相等 -/// (`to_bits`)钉住两者的绑定,改了种子它就当场红——那时要想清楚是「色卡跟着新默认色走」 -/// (照抄新色相)还是「色卡保持这条冷调」(把测试改成显式例外),别静默漂移。 +/// ⚠️ 换 [`DEFAULT_SEED`] 时本常量会「过期」:`neutral_hue_is_the_default_seed_hue` 用 +/// 1e-9° 容差钉住两者的绑定(**不用 `to_bits` 位相等**——色相来自跨平台有末位差的 `atan2`), +/// 改了种子它就红——那时要想清楚是「色卡跟着新默认色走」(照抄新色相)还是「色卡保持这条 +/// 冷调」(改成显式例外),别静默漂移。 pub const NEUTRAL_HUE_DEG: f64 = 239.514_205_776_309_55; /// 调色板槽位(M3 的六条 tonal palette)。 @@ -359,14 +358,16 @@ mod tests { #[test] fn neutral_hue_is_the_default_seed_hue() { - // D43 的绑定器:`NEUTRAL_HUE_DEG` 必须是**出厂默认种子的中性色相**,一位不差 - // (位相等,不是近似)。天蓝卡的 49 个角色因此逐值不变;这条一旦红,说明种子换了而 - // 常量没跟着复核——要么照抄新色相(色卡跟着新默认色走),要么把这里改成显式例外。 + // D43 的绑定器。用 1e-9° 容差而非 `to_bits()` 位相等:色相来自 `atan2()`,glibc 与 + // UCRT 理论可差 1 ulp(~1e-14°),位相等会在另一个 OS 的 CI 上红;这条要抓的是「换了 + // `DEFAULT_SEED` 却没复核常量」(偏几十度)。「逐值不变」的位相等钉子在同平台的 + // `card_neutral.rs::sky_card_is_bit_identical_to_the_product_baseline`(两个算出的值 + // 互比,不涉及跨平台字面量)。 let seed_hue = super::super::Hct::from_color(DEFAULT_SEED).hue; - assert_eq!( - NEUTRAL_HUE_DEG.to_bits(), - seed_hue.to_bits(), - "NEUTRAL_HUE_DEG = {NEUTRAL_HUE_DEG:?} 与 DEFAULT_SEED 的色相 {seed_hue:?} 不再相等" + assert!( + (NEUTRAL_HUE_DEG - seed_hue).abs() < 1.0e-9, + "NEUTRAL_HUE_DEG = {NEUTRAL_HUE_DEG:?} 与 DEFAULT_SEED 的色相 {seed_hue:?} \ + 相差超过 1e-9°(换默认种子时:要么照抄新色相,要么把这里改成显式例外)" ); } diff --git a/crates/design/src/palette/resolve/mod.rs b/crates/design/src/palette/resolve/mod.rs index 226f02e..917b6da 100644 --- a/crates/design/src/palette/resolve/mod.rs +++ b/crates/design/src/palette/resolve/mod.rs @@ -180,7 +180,9 @@ fn try_level(input: Option>, card: Card, flavor: SchemeFlavor) -> // 取色方案的表面色**跟着图像种子染色**,只借当前色卡的中性彩度定浓淡(D43): // 这是与色卡路径刻意不同的一处——色卡固定中性色相、只换强调色,取色保留整幅染色 - // (那是用户要的 Monet 体验)。别把两条路径「统一处理」,`tests/card_neutral.rs` 有反证。 + // (那是用户要的 Monet 体验)。别把两条路径「统一处理」:这条接线的守护测试是 + // `tests/card_neutral.rs::resolved_image_scheme_keeps_tinting_from_the_seed`(走的就是 + // 本函数所在的 `resolve_source`;只测 `Seed::scheme` 的那条覆盖不到这里,复审 r1 p1-1)。 let (scheme, adjustments) = converge(seed.scheme(flavor, card.neutral_chroma)); let report = check(&scheme); if !report.is_clean() {