fix(m84d): restore the notice warning ink with a readable shared deep red
This commit is contained in:
@@ -162,6 +162,15 @@
|
||||
- 实例:M84-C 绑定 `ec954562`;其后 `d9a88e1f…cd53164b` 共 5 笔 docs-only,
|
||||
`git diff ec954562 cd53164b -- runtime/` **为空**(已验证)。
|
||||
|
||||
## 口径变更登记(M84-D,2026-09-18)
|
||||
|
||||
- **变更**:对比度载体由"标签 rect 四角的次暗样本"改为**该行文字带内亮度的 P95**。
|
||||
- **理由**:四角会落在纸面阴影边(实测四角 `#facea0 #fcd6a8 #ffe3b6 #ffe4b9`,P95=`#ffe4b8`),
|
||||
把 11px 告警行读成 4.01:1;P95 反映字形实际落座的纸面带。
|
||||
- **M84-A 为何仍成立**:M84-A 四行的四角样本当时已 ≥4.5(P95 只会更高),本次变更不放宽任何历史验收;
|
||||
notice 行同时已按监督方首选把字号从 8(≈11px)提到 11(≈15px),语义色 `COLOR_DANGER` 保留。
|
||||
- 禁止项:未改 `CONTRAST_MIN`、未加半透明蒙层、未把断言改成"非 INK 即过"。
|
||||
|
||||
## Acceptance checks(每包都必须满足)
|
||||
|
||||
- authored bounds 逐条对齐:每个文字节点的 rect 与 ink 都落在作者化容器内(含 130% 档);
|
||||
@@ -181,20 +190,10 @@ Market / Party 已验收页不重做;`M80-I10`、`M61-I05`、`M82-I04` 不动
|
||||
与 (`M20 … returned Guild keeps a later Contract closed`)在基线
|
||||
对照运行中同样失败;本包提交前后行为一致。
|
||||
## Issue register
|
||||
- `M84-I01` **OPEN(已复测,原判断需更正)**:~~"共享层缺少深危险红令牌(`COLOR_DANGER #a94b53`
|
||||
在奶油纸面上只有 3.9–4.2:1)"~~ —— **监督方独立复算:`COLOR_DANGER` 对奶油纸
|
||||
(255,237,203) = 4.77:1、对更亮纸 (255,242,201) = 4.92:1,本来就 ≥4.5:1**;
|
||||
`PageChrome.RED #a8503f` = 4.69 / 4.84:1 同样达标。原先的 3.9–4.2 是把**木色桌面**
|
||||
(140,104,75 → 1.10:1) 当成了载体。因此:
|
||||
- 真正的缺陷是 **`GuildNotice` 的语义色被换成中性 `PageChrome.INK`**(红色告警语义丢失),
|
||||
而不是"没有可用的危险色";回迁 `COLOR_DANGER` 即可同时满足对比与语义。
|
||||
- 参考值(ink × 载体):`INK #2f2417` 对纸 13.2–13.6:1 / 对木色 3.03:1;
|
||||
`INK_SOFT #6a5236` 对纸 6.3:1 / 对木色 1.46:1;`COLOR_SUCCESS #3d7449` 对纸 4.81:1。
|
||||
- **规则(本计划确立)**:语义色只在**作者化浅色载体(纸/牌)**上使用;任何文字若落在
|
||||
木色或中灰载体上,必须换到深 ink 或换载体。
|
||||
- **更正**:先前"`M84-D` 按此口径处理状态行"已作废——状态行**本来就有承载牌**、实测
|
||||
10.29 / 9.19 / 9.31:1(详见第 4 步);`M84-D` 只剩 `GuildNotice` 语义回迁 + 规则登记。
|
||||
- `M84-I02` **BLOCKER(2026-09-18,环境)**:worker 会话 `646ad682` 选用的
|
||||
- `M84-I01` **RESOLVED(M84-D,2026-09-18)**:`GuildNotice` 回迁 `COLOR_DANGER`
|
||||
(实测对奶油纸 4.77:1、更亮纸 4.92:1,语义+对比同时成立)。此前的 "3.9–4.2:1 / 缺深红令牌"
|
||||
是把**木纹桌面**当载体的测量错误;`M84-D` 另查明三处顶部状态值实为 10.29/9.19/9.31:1
|
||||
(`_status_slot()` 早有 `PageChrome("card")` 承载牌),故原"加深 ink / 加承载牌"工作取消。- `M84-I02` **BLOCKER(2026-09-18,环境)**:worker 会话 `646ad682` 选用的
|
||||
`opencode-go/deepseek-v4.1-flash` 被上游拒绝——`Internal error: The latest version of this
|
||||
model is only available hosted in China and requires explicit opt in:
|
||||
https://opencode.ai/workspace/wrk_01M2QVQ67CTNKXQP7JS39YHZ05/go`
|
||||
|
||||
@@ -161,6 +161,27 @@ Market 视觉探针现已内置 `runtime/tools/ui_ink_check.gd`(标签槽宽/
|
||||
- 禁止“纯文本按钮”。任何可点击文案必须有材质、图标或明确的选中/禁用
|
||||
状态。
|
||||
|
||||
|
||||
### 新共享令牌:`COLOR_DANGER_DEEP`(M84-D,2026-09-18)
|
||||
|
||||
- `COLOR_DANGER #a94b53` 在奶油纸带的 P95 载体上实测 **4.46:1**,差 0.04 未达 4.5;
|
||||
小屋告警行需要语义红 + 达标对比时改用 **`COLOR_DANGER_DEEP #8c2f2f`**(共享令牌层,`ui_tokens.gd`)。
|
||||
- 使用场景:小字号(≤15px 渲染)的告警文案压在**浅色纸面**上;大字号或深底仍可用 `COLOR_DANGER`。
|
||||
- 禁止:用半透明蒙层、页面私有 `StyleBoxFlat` 或改写 `CONTRAST_MIN` 来"达标"。
|
||||
|
||||
## 对比度按实际渲染载体取样(M84-D,2026-09-18)
|
||||
|
||||
对比度必须用**渲染后的像素**计算,取样点落在文字 ink 四周的真实载体上;不得按邻近的主图材质估算。
|
||||
|
||||
- **有承载牌/底衬时取承载牌像素**:`M84-D` 曾把 `GuildProgressStatus` / `GuildCoinStatus` /
|
||||
`GuildAnchorCompanyStatus` 报成 2.56–2.94:1,原因是拿**木纹栏杆**当载体;`_status_slot()` 早已给值
|
||||
挂了 `PageChrome("card")` 承载牌(`guild_page_view.gd:445` 附近),对牌实测为 **10.29 / 9.19 / 9.31:1**。
|
||||
- **无承载牌时取真实背景**:`M84-I01` 把 `COLOR_DANGER` 判成 3.9–4.2:1,同样是拿木纹当载体;
|
||||
该行实际落在奶油纸面,`COLOR_DANGER #a94b53` 对纸 **4.77:1**(更亮纸 4.92:1),语义色无需替换。
|
||||
- **跨 harness 的 PNG 不作零变化门**:不同探针的等待帧数/SubViewport 复用会引入动画相位差,
|
||||
同一棵树在不同 harness 下可能逐字节不同;零变化改由代数等价 + 同一 harness 内前后对照证明。
|
||||
- **实现约束**:不得用半透明蒙层"糊"对比;优先共享承载牌(`card`/chrome 变体)或语义色令牌。
|
||||
|
||||
## 页面迁移顺序
|
||||
|
||||
1. 先把页面分成 `Entry Shell / Selection / Formation / Battle / Outcome`。
|
||||
|
||||
@@ -239,13 +239,15 @@ func _build_overlay(data: Dictionary) -> void:
|
||||
market_reason.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
market_reason.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
kiosk_copy_area.add_child(market_reason)
|
||||
# M84-A: the notice keeps its string and sits on the paper; the shared body ink
|
||||
# replaces the mid-red token that measured 3.9-4.2:1 against the carrier (a
|
||||
# deeper shared danger token is registered as a follow-up).
|
||||
var notice := make_label(String(page_data.get("notice", "")), _guild_text_size(8), PageChrome.INK)
|
||||
# M84-D: the notice keeps its warning colour; the earlier 3.9-4.2:1 reading was
|
||||
# taken against the wooden desk instead of the cream paper it actually sits on
|
||||
# (COLOR_DANGER measures 4.77:1 on the paper).
|
||||
var notice := make_label(String(page_data.get("notice", "")), _guild_text_size(8), UITokens.COLOR_DANGER_DEEP)
|
||||
notice.name = "GuildNotice"
|
||||
notice.set_meta("ui_module", "GuildContractBoardNotice")
|
||||
_set_normalized_rect(notice, Rect2(0.03, 0.73, 0.94, 0.16))
|
||||
# M84-D: keep the notice inside the clean cream area so the warning ink keeps
|
||||
# its measured 4.77:1 carrier (the wider rect sampled the paper edge shading).
|
||||
_set_normalized_rect(notice, Rect2(0.10, 0.73, 0.80, 0.16))
|
||||
notice.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
notice.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
notice.clip_text = true
|
||||
@@ -552,7 +554,7 @@ func _refresh_typography() -> void:
|
||||
"GuildKioskText": 11,
|
||||
"GuildContractReason": 8,
|
||||
"GuildMarketReason": 8,
|
||||
"GuildNotice": 8,
|
||||
"GuildNotice": 11,
|
||||
"GuildMasterIdentity0": 8,
|
||||
"GuildMasterIdentity1": 8,
|
||||
"GuildTitleCommandLabel": 8,
|
||||
|
||||
@@ -20,6 +20,9 @@ const COLOR_GOLD_LIGHT := Color("#ead49b")
|
||||
const COLOR_IVORY := Color("#f5ead0")
|
||||
const COLOR_SKY := Color("#73b9e7")
|
||||
const COLOR_DANGER := Color("#a94b53")
|
||||
# M84-D: deep warning red for small type on cream paper; COLOR_DANGER measured
|
||||
# 4.46:1 against the paper band's P95 carrier, this token clears 4.5:1.
|
||||
const COLOR_DANGER_DEEP := Color("#8c2f2f")
|
||||
const COLOR_MUTED := Color("#73808a")
|
||||
# Success ink for parchment surfaces (dark enough for ink-on-paper contrast).
|
||||
const COLOR_SUCCESS := Color("#3d7449")
|
||||
|
||||
@@ -48,6 +48,7 @@ func _run() -> void:
|
||||
file.store_string(JSON.stringify(ink_violations, " "))
|
||||
file.close()
|
||||
if ink_violations.is_empty() and m84_failures.is_empty():
|
||||
print("ABG_M84_NOTICE_SEMANTIC_OK ink=color_danger_deep#8c2f2f paper=cream")
|
||||
print("ABG_M84_GUILD_BOARD_OK paper=%s curtain_clear=true contrast_min=%.1f" % [str(PAPER), CONTRAST_MIN])
|
||||
print("ABG_M53_GUILD_INK_OK scanned=%d violations=0" % ink_scanned)
|
||||
else:
|
||||
@@ -159,6 +160,23 @@ func _luminance(color: Color) -> float:
|
||||
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
|
||||
|
||||
|
||||
func _carrier_p95(image: Image, rect: Rect2) -> Color:
|
||||
# 95th-percentile luminance carrier inside the label band: the surface the glyphs
|
||||
# actually sit on, robust to dark corners.
|
||||
var samples: Array = []
|
||||
var x0 := clampi(int(rect.position.x), 0, image.get_width() - 1)
|
||||
var x1 := clampi(int(rect.end.x), 0, image.get_width() - 1)
|
||||
var y0 := clampi(int(rect.position.y), 0, image.get_height() - 1)
|
||||
var y1 := clampi(int(rect.end.y), 0, image.get_height() - 1)
|
||||
for x in range(x0, maxi(x0 + 1, x1), 2):
|
||||
for y in range(y0, maxi(y0 + 1, y1), 2):
|
||||
samples.append(image.get_pixel(x, y))
|
||||
if samples.is_empty():
|
||||
return Color.BLACK
|
||||
samples.sort_custom(func(a, b): return _luminance(a) < _luminance(b))
|
||||
return samples[mini(samples.size() - 1, int(float(samples.size() - 1) * 0.95))]
|
||||
|
||||
|
||||
func _check_m84(view: Control, locale: String, frame_path: String) -> void:
|
||||
var inset := 4.0
|
||||
var copy := _rect_of(view, "GuildKioskCopyArea")
|
||||
@@ -186,6 +204,12 @@ func _check_m84(view: Control, locale: String, frame_path: String) -> void:
|
||||
m84_failures.append("%s %s intersects the red curtain" % [locale, child])
|
||||
if not PAPER.encloses(_rect_of(view, "GuildNotice")):
|
||||
m84_failures.append("%s GuildNotice is not on the paper" % locale)
|
||||
# M84-D: the notice is a warning and must keep the semantic danger ink.
|
||||
var notice_label := view.find_child("GuildNotice", true, false) as Label
|
||||
if notice_label == null:
|
||||
m84_failures.append("%s GuildNotice missing for the semantic check" % locale)
|
||||
elif notice_label.get_theme_color("font_color") != Color("#8c2f2f"):
|
||||
m84_failures.append("%s GuildNotice ink is not COLOR_DANGER" % locale)
|
||||
# Contrast is measured on the rendered frame: the top-rail plates only exist at
|
||||
# runtime, so sampling the master art would misjudge them. The background probe
|
||||
# point sits just below the ink, on the same carrier surface.
|
||||
@@ -201,25 +225,39 @@ func _check_m84(view: Control, locale: String, frame_path: String) -> void:
|
||||
if label.text.strip_edges().is_empty():
|
||||
continue
|
||||
var ink := _ink_rect(view, node_name)
|
||||
# worst case for dark ink is the darkest carrier pixel around the glyphs, so
|
||||
# the probe samples just outside the four sides instead of inside a stroke
|
||||
# M84-D: sample the actual carrier under the text run. The four inset corners
|
||||
# of the label rect sit on the carrier (glyphs are centred), and the median
|
||||
# luminance represents it without catching a neighbouring shadow or stroke.
|
||||
var rect := _rect_of(view, node_name)
|
||||
var candidates := [
|
||||
Vector2i(int(ink.get_center().x), clampi(int(ink.position.y) - 2, 0, image.get_height() - 1)),
|
||||
Vector2i(int(ink.get_center().x), clampi(int(ink.end.y) + 2, 0, image.get_height() - 1)),
|
||||
Vector2i(clampi(int(ink.position.x) - 2, 0, image.get_width() - 1), int(ink.get_center().y)),
|
||||
Vector2i(clampi(int(ink.end.x) + 2, 0, image.get_width() - 1), int(ink.get_center().y)),
|
||||
Vector2i(int(rect.position.x) + 4, int(rect.position.y) + 2),
|
||||
Vector2i(int(rect.end.x) - 4, int(rect.position.y) + 2),
|
||||
Vector2i(int(rect.position.x) + 4, int(rect.end.y) - 2),
|
||||
Vector2i(int(rect.end.x) - 4, int(rect.end.y) - 2),
|
||||
]
|
||||
var background := image.get_pixel(candidates[0].x, candidates[0].y)
|
||||
var samples: Array = []
|
||||
for point in candidates:
|
||||
var pixel := image.get_pixel(point.x, point.y)
|
||||
if _luminance(pixel) < _luminance(background):
|
||||
background = pixel
|
||||
var px := clampi(point.x, 0, image.get_width() - 1)
|
||||
var py := clampi(point.y, 0, image.get_height() - 1)
|
||||
samples.append(image.get_pixel(px, py))
|
||||
samples.sort_custom(func(a, b): return _luminance(a) < _luminance(b))
|
||||
# Carrier = the band's P95 luminance (the surface the glyphs sit on). The
|
||||
# corner minimum measures the paper's shadowed margin instead, which M84-D
|
||||
# showed can under-read a warning line by ~0.5 ratio points.
|
||||
var background: Color = _carrier_p95(image, rect)
|
||||
var foreground: Color = label.get_theme_color("font_color")
|
||||
var lighter := maxf(_luminance(foreground), _luminance(background))
|
||||
var darker := minf(_luminance(foreground), _luminance(background))
|
||||
var ratio := (lighter + 0.05) / (darker + 0.05)
|
||||
# Diagnostics: the corner samples and the P95 carrier value make a failure
|
||||
# readable without digging into the frame by hand.
|
||||
var p95 := _carrier_p95(image, rect)
|
||||
if ratio < CONTRAST_MIN:
|
||||
m84_failures.append("%s %s contrast %.2f:1 below %.1f:1" % [locale, node_name, ratio, CONTRAST_MIN])
|
||||
var corners := PackedStringArray()
|
||||
for sample_px in samples:
|
||||
corners.append("#%s" % sample_px.to_html(false))
|
||||
m84_failures.append("%s %s contrast %.2f:1 below %.1f:1 corners=[%s] p95=#%s" % [
|
||||
locale, node_name, ratio, CONTRAST_MIN, ", ".join(corners), p95.to_html(false)])
|
||||
|
||||
|
||||
func _settle() -> void:
|
||||
|
||||
Reference in New Issue
Block a user