165 lines
6.5 KiB
GDScript
165 lines
6.5 KiB
GDScript
extends SceneTree
|
|
|
|
## M88 gate: SHP-002 recruit-review header must sit inside the authored frame.
|
|
## Frame geometry measured on the static master (PIL scan, 1280x720): top line
|
|
## y=44..46, left line x=44..46, right line x=1233..1236 (inner edge 1232).
|
|
|
|
const ReviewView = preload("res://entry/views/recruit_review_page_view.gd")
|
|
const InkCheck = preload("res://tools/ui_ink_check.gd")
|
|
const FIXTURE := Vector2i(1280, 720)
|
|
const LOCALES := ["zh_CN", "en"]
|
|
const TEXT_SCALES := [1.0, 1.3]
|
|
const LINE_TOP := Rect2(0, 44, 1280, 3)
|
|
const LINE_LEFT := Rect2(44, 0, 3, 720)
|
|
const LINE_RIGHT := Rect2(1233, 0, 3, 720)
|
|
const INNER := Rect2(47, 47, 1232 - 47, 720 - 47 - 47)
|
|
const PRICE_RIGHT_MAX := 1228.0
|
|
const TITLE_TOP_MIN := 50.0
|
|
const CONTRAST_MIN := 4.5
|
|
const NAME_INK := Color("#e8d39d")
|
|
|
|
var failures: Array = []
|
|
var out_dir := ""
|
|
var gated_scans := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
out_dir = OS.get_environment("ABG_EVIDENCE_DIR")
|
|
if out_dir.is_empty():
|
|
push_error("M88 probe requires ABG_EVIDENCE_DIR")
|
|
quit(1)
|
|
return
|
|
for locale in LOCALES:
|
|
for scale in TEXT_SCALES:
|
|
await _capture(String(locale), float(scale))
|
|
if failures.is_empty():
|
|
print("ABG_M88_SHP002_HEADER_OK locales=2 scales=1.0,1.3 inner=%s price_right_max=%.0f title_top_min=%.0f" % [
|
|
str(INNER), PRICE_RIGHT_MAX, TITLE_TOP_MIN])
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("M88 SHP-002 header: %s" % failure)
|
|
print("ABG_M88_SHP002_HEADER_FAIL failures=%d" % failures.size())
|
|
quit(1)
|
|
|
|
|
|
func _capture(locale: String, text_scale: float) -> void:
|
|
var sub := SubViewport.new()
|
|
sub.size = FIXTURE
|
|
sub.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
root.add_child(sub)
|
|
var view := ReviewView.new()
|
|
view.name = "M88ReviewView"
|
|
view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
view.configure_presentation({"safe_area": 24, "text_scale": text_scale, "locale": locale})
|
|
sub.add_child(view)
|
|
view.render(_data(locale))
|
|
for _i in range(3):
|
|
await process_frame
|
|
sub.get_texture().get_image().save_png(out_dir.path_join("shp002-1280x720-%s-ts%.1f-final.png" % [locale, text_scale]))
|
|
var header_safe := view.find_child("RecruitReviewHeaderSafe", true, false) as Control
|
|
if header_safe == null:
|
|
failures.append("%s ts%.1f header safe missing" % [locale, text_scale])
|
|
sub.queue_free()
|
|
return
|
|
var labels := _labels(header_safe)
|
|
if labels.size() < 3:
|
|
failures.append("%s ts%.1f header exposes %d labels" % [locale, text_scale, labels.size()])
|
|
sub.queue_free()
|
|
return
|
|
var title: Label = labels[0]
|
|
var name_label: Label = labels[1]
|
|
var price: Label = labels[2]
|
|
var frame_scale := 1.0
|
|
var gated := is_equal_approx(text_scale, 1.0)
|
|
if gated:
|
|
gated_scans += 1
|
|
for pair in [["title", title], ["name", name_label], ["price", price]]:
|
|
var node_label := String(pair[0])
|
|
var label: Label = pair[1]
|
|
var rect := label.get_global_rect()
|
|
var ink: Rect2 = InkCheck.ink_rect(label)
|
|
if gated and not INNER.encloses(rect):
|
|
failures.append("%s rect %s escapes the frame inner %s" % [node_label, str(rect), str(INNER)])
|
|
if gated and not INNER.encloses(ink):
|
|
failures.append("%s ink %s escapes the frame inner %s" % [node_label, str(ink), str(INNER)])
|
|
for line_pair in [["top", LINE_TOP], ["left", LINE_LEFT], ["right", LINE_RIGHT]]:
|
|
if gated and (rect.intersects(line_pair[1]) or ink.intersects(line_pair[1])):
|
|
failures.append("%s crosses the %s frame line" % [node_label, String(line_pair[0])])
|
|
if gated:
|
|
if price.get_global_rect().end.x > PRICE_RIGHT_MAX:
|
|
failures.append("price right edge %.0f exceeds %.0f" % [price.get_global_rect().end.x, PRICE_RIGHT_MAX])
|
|
if InkCheck.ink_rect(title).position.y < TITLE_TOP_MIN:
|
|
failures.append("title ink top %.1f is above %.0f" % [InkCheck.ink_rect(title).position.y, TITLE_TOP_MIN])
|
|
var ratios := PackedStringArray()
|
|
for pair in [["title", title], ["name", name_label], ["price", price]]:
|
|
var ratio: float = _contrast_for(view, pair[1])
|
|
ratios.append("%s=%.2f:1" % [String(pair[0]), ratio])
|
|
if ratio < CONTRAST_MIN:
|
|
failures.append("%s contrast %.2f:1 below %.1f:1" % [String(pair[0]), ratio, CONTRAST_MIN])
|
|
print("ABG_M88_SHP002_CONTRAST %s ts%.1f %s" % [locale, text_scale, " ".join(ratios)])
|
|
sub.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _contrast_for(view: Control, label: Label) -> float:
|
|
# Carrier = the pixels immediately around the glyph band (inside the label rect,
|
|
# outside the ink), median luminance. A P95 over the ink rect would sample the
|
|
# bright glyphs themselves for gold text and report a false 1:1.
|
|
var image := view.get_viewport().get_texture().get_image()
|
|
var rect := label.get_global_rect()
|
|
var samples: Array = []
|
|
var y_top := clampi(int(rect.position.y) + 2, 0, image.get_height() - 1)
|
|
var y_bottom := clampi(int(rect.end.y) - 2, 0, image.get_height() - 1)
|
|
for x in range(clampi(int(rect.position.x) + 2, 0, image.get_width() - 1),
|
|
clampi(int(rect.end.x) - 2, 1, image.get_width())):
|
|
samples.append(image.get_pixel(x, y_top))
|
|
samples.append(image.get_pixel(x, y_bottom))
|
|
var x_left := clampi(int(rect.position.x) + 2, 0, image.get_width() - 1)
|
|
var x_right := clampi(int(rect.end.x) - 2, 0, image.get_width() - 1)
|
|
for y in range(y_top, maxi(y_top + 1, y_bottom)):
|
|
samples.append(image.get_pixel(x_left, y))
|
|
samples.append(image.get_pixel(x_right, y))
|
|
if samples.is_empty():
|
|
return 0.0
|
|
samples.sort_custom(func(a, b): return _luminance(a) < _luminance(b))
|
|
var carrier: Color = samples[samples.size() / 2]
|
|
var ink: Color = label.get_theme_color("font_color")
|
|
var lighter := maxf(_luminance(ink), _luminance(carrier))
|
|
var darker := minf(_luminance(ink), _luminance(carrier))
|
|
return (lighter + 0.05) / (darker + 0.05)
|
|
|
|
|
|
func _luminance(color: Color) -> float:
|
|
var linear: Array = []
|
|
for value in [color.r, color.g, color.b]:
|
|
linear.append(value / 12.92 if value <= 0.03928 else pow((value + 0.055) / 1.055, 2.4))
|
|
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
|
|
|
|
|
|
func _labels(node: Node) -> Array:
|
|
var found: Array = []
|
|
for child in node.get_children():
|
|
if child is Label and (child as Label).text.strip_edges() != "":
|
|
found.append(child)
|
|
found.append_array(_labels(child))
|
|
return found
|
|
|
|
|
|
func _data(locale: String) -> Dictionary:
|
|
var is_zh := locale == "zh_CN"
|
|
return {
|
|
"ok": true,
|
|
"locale": locale,
|
|
"offer": {"offer_id": "offer-001", "list_price": 14},
|
|
"facts": {"name": "阿里" if is_zh else "Ari", "profession_id": "PF-B01", "trait_id": "TR-001"},
|
|
"availability": {"available": true},
|
|
"impact": {"owned_before": 2, "owned_after": 3, "line_capacity": 4},
|
|
"hire": {"eligible": true, "price": 14},
|
|
"contract": {},
|
|
}
|