fix(m83): anchor the party top rail to its approved slots and bind the capacity plate
The four authored plates now carry the engine text the approved master and text-slots.json describe: the title, the pressure word, the Company size on the coins plate and the BIND:PARTY_CAPACITY binding on the helmet plate that had no runtime content. Labels are fitted to the authored boxes (Rect2 corners, not sizes), rail type follows the shipped 130% scale inside its slot instead of the page-wide 16px ceiling, and the M66 probe gates all four boxes at both scales.
This commit is contained in:
@@ -15,6 +15,26 @@ const PARTY_LINE_MASTER_PATH := "res://assets/entry/party_line_v001/party-line-m
|
||||
const PageChrome = preload("res://ui/shared/page_chrome.gd")
|
||||
const UITokens = preload("res://foundation/ui_tokens.gd")
|
||||
|
||||
## M83: the top rail's four plates are authored in the approved master
|
||||
## (`docs/design-authority/pages/PTY-001-party-line/approved/v001/master.png`).
|
||||
## These are the authored text boxes of that master, measured on the approved
|
||||
## 2048x1152 file and scaled to the 1280x720 design canvas (x0, y0, x1, y1).
|
||||
## `text-slots.json` only declares the first three; the engine owns the text in
|
||||
## them and the icon/plate art stays static.
|
||||
## `Rect2` takes a size, not a second corner: keep the authored corners explicit
|
||||
## or the label anchors land past the plate.
|
||||
const RAIL_SLOT_TITLE := Rect2(Vector2(263.8, 31.2), Vector2(415.0 - 263.8, 65.6 - 31.2))
|
||||
const RAIL_SLOT_PRESSURE := Rect2(Vector2(511.2, 35.0), Vector2(664.4 - 511.2, 63.8 - 35.0))
|
||||
const RAIL_SLOT_COMPANY := Rect2(Vector2(788.8, 31.2), Vector2(933.8 - 788.8, 65.6 - 31.2))
|
||||
const RAIL_SLOT_CAPACITY := Rect2(Vector2(1050.6, 31.2), Vector2(1191.9 - 1050.6, 65.6 - 31.2))
|
||||
## The authored boxes are the visible plates; the engine label keeps a small
|
||||
## inset so a glyph never sits on the plate outline.
|
||||
const RAIL_TEXT_INSET := 4.0
|
||||
## UI Restoration Memory: rail type may rise with the accessibility scale, but
|
||||
## never past the authored slot. The 16px cap this replaces made 130% a no-op.
|
||||
const RAIL_FONT_CEILING := 30
|
||||
const RAIL_FONT_FLOOR := 11
|
||||
|
||||
var design_canvas: Control
|
||||
|
||||
|
||||
@@ -76,9 +96,21 @@ func _build_overlay(data: Dictionary) -> void:
|
||||
overlay.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
design_canvas.add_child(overlay)
|
||||
|
||||
_party_label(overlay, "PartyTitle", Rect2(0.205, 0.045, 0.125, 0.05), localize("队列编成", "PARTY LINE", data), 10, PageChrome.INK)
|
||||
_party_label(overlay, "PartyPressureValue", Rect2(0.415, 0.045, 0.095, 0.05), _party_pressure_short(String(contract_data.get("difficulty", "standard")), data), 10, PageChrome.INK_SOFT)
|
||||
_party_label(overlay, "PartyCompanyValue", Rect2(0.635, 0.045, 0.105, 0.05), "%d/%d" % [int(company_data.get("owned_count", 0)), int(company_data.get("line_capacity", 0))], 10, PageChrome.INK)
|
||||
# M83: four authored plates, four engine texts. Plate 3 carries the Company
|
||||
# size the approved slot names `company_value`; plate 4 carries the
|
||||
# `BIND:PARTY_CAPACITY` binding, which `docs/03` forbids labelling as the
|
||||
# owned Company count. Every label is fitted to its authored box instead of a
|
||||
# hardcoded normalized anchor.
|
||||
var deployed_count := order_ids.size()
|
||||
var line_capacity := int(formation_data.get("line_capacity", company_data.get("line_capacity", 0)))
|
||||
_party_rail_label(overlay, "PartyTitle", RAIL_SLOT_TITLE,
|
||||
localize("队列编成", "PARTY LINE", data), 10, PageChrome.INK)
|
||||
_party_rail_label(overlay, "PartyPressureValue", RAIL_SLOT_PRESSURE,
|
||||
_party_pressure_short(String(contract_data.get("difficulty", "standard")), data), 10, PageChrome.INK_SOFT)
|
||||
_party_rail_label(overlay, "PartyCompanyValue", RAIL_SLOT_COMPANY,
|
||||
localize("队伍 %d", "LINE %d", data) % int(company_data.get("owned_count", 0)), 10, PageChrome.INK)
|
||||
_party_rail_label(overlay, "PartyCompanyCapacityValue", RAIL_SLOT_CAPACITY,
|
||||
localize("上阵 %d/%d", "LINE %d/%d", data) % [deployed_count, line_capacity], 10, PageChrome.INK)
|
||||
|
||||
var diagram := PartyLineDiagram.new()
|
||||
diagram.name = "PartyOrderDiagram"
|
||||
@@ -295,6 +327,55 @@ func _party_label(parent: Control, node_name: String, area: Rect2, text_value: S
|
||||
return label
|
||||
|
||||
|
||||
func _party_rail_label(parent: Control, node_name: String, slot: Rect2, text_value: String, design_size: int, color: Color) -> Label:
|
||||
# M83: one authored box, one engine text. The label rect is the box inset so
|
||||
# a glyph never touches the plate outline, and the font steps down only when
|
||||
# the authored type size would not fit the box at the current text scale.
|
||||
var label := make_label(text_value, _party_rail_font_size(text_value, slot, design_size), color)
|
||||
label.name = node_name
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.set_meta("design_font_size", design_size)
|
||||
label.set_meta("authored_slot", slot)
|
||||
_party_rail_place(label, slot)
|
||||
parent.add_child(label)
|
||||
return label
|
||||
|
||||
|
||||
func _party_rail_font_size(text_value: String, slot: Rect2, design_size: int) -> int:
|
||||
var available := Vector2(slot.size.x - 2.0 * RAIL_TEXT_INSET, slot.size.y - 2.0 * RAIL_TEXT_INSET)
|
||||
var size := _party_text_size(design_size, false)
|
||||
while size > RAIL_FONT_FLOOR:
|
||||
if _party_rail_text_fits(text_value, available, size):
|
||||
return size
|
||||
size -= 1
|
||||
return RAIL_FONT_FLOOR
|
||||
|
||||
|
||||
func _party_rail_text_fits(text_value: String, available: Vector2, font_size: int) -> bool:
|
||||
var measured := UI_FONT.get_string_size(text_value, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
|
||||
return measured.x <= available.x and measured.y <= available.y
|
||||
|
||||
|
||||
func _party_rail_place(control: Control, slot: Rect2) -> void:
|
||||
# ``slot`` is an authored pixel rect in the 1280x720 design canvas.
|
||||
var design := Vector2(1280.0, 720.0)
|
||||
control.anchor_left = (slot.position.x + RAIL_TEXT_INSET) / design.x
|
||||
control.anchor_top = (slot.position.y + RAIL_TEXT_INSET) / design.y
|
||||
control.anchor_right = (slot.end.x - RAIL_TEXT_INSET) / design.x
|
||||
control.anchor_bottom = (slot.end.y - RAIL_TEXT_INSET) / design.y
|
||||
control.offset_left = 0.0
|
||||
control.offset_top = 0.0
|
||||
control.offset_right = 0.0
|
||||
control.offset_bottom = 0.0
|
||||
control.offset_left = 0.0
|
||||
control.offset_top = 0.0
|
||||
control.offset_right = 0.0
|
||||
control.offset_bottom = 0.0
|
||||
|
||||
|
||||
func _party_place(control: Control, area: Rect2) -> void:
|
||||
control.anchor_left = area.position.x
|
||||
control.anchor_top = area.position.y
|
||||
@@ -306,11 +387,17 @@ func _party_place(control: Control, area: Rect2) -> void:
|
||||
control.offset_bottom = 0.0
|
||||
|
||||
|
||||
func _party_text_size(design_size: int) -> int:
|
||||
func _party_text_size(design_size: int, clamp_ceiling := true) -> int:
|
||||
var canvas_height := design_canvas.size.y if is_instance_valid(design_canvas) else size.y
|
||||
var canvas_scale := clampf(canvas_height / 360.0, 1.0, 2.0)
|
||||
var text_scale := float(presentation.get("text_scale", 1.0))
|
||||
return mini(16, maxi(design_size, int(round(float(design_size) * canvas_scale * text_scale))))
|
||||
var scaled := int(round(float(design_size) * canvas_scale * text_scale))
|
||||
if not clamp_ceiling:
|
||||
# M83: the top rail owns an authored slot and fits its own type, so the
|
||||
# page-wide 16px ceiling (which made the 130% text scale a no-op) does not
|
||||
# apply to it. The other party labels keep the historical ceiling.
|
||||
return mini(RAIL_FONT_CEILING, maxi(RAIL_FONT_FLOOR, scaled))
|
||||
return mini(16, maxi(design_size, scaled))
|
||||
|
||||
|
||||
func _party_pressure_short(difficulty: String, data: Dictionary) -> String:
|
||||
|
||||
@@ -13,6 +13,18 @@ const FIXTURES := [Vector2i(1280, 720)]
|
||||
const LOCALES := ["zh_CN", "en"]
|
||||
# Accessibility pass: 1.0 plus the shipped 130% text scale (M73).
|
||||
const TEXT_SCALES := [1.0, 1.3]
|
||||
## M83: the Party Line top rail's four authored text boxes, measured on the
|
||||
## approved master (`docs/design-authority/pages/PTY-001-party-line/
|
||||
## approved/v001/master.png`) and scaled to the 1280x720 design canvas.
|
||||
const RAIL_SLOTS := [
|
||||
{"node": "PartyTitle", "owner": "party_title", "rect": Rect2(Vector2(263.8, 31.2), Vector2(415.0 - 263.8, 65.6 - 31.2))},
|
||||
{"node": "PartyPressureValue", "owner": "pressure_value", "rect": Rect2(Vector2(511.2, 35.0), Vector2(664.4 - 511.2, 63.8 - 35.0))},
|
||||
{"node": "PartyCompanyValue", "owner": "company_value", "rect": Rect2(Vector2(788.8, 31.2), Vector2(933.8 - 788.8, 65.6 - 31.2))},
|
||||
{"node": "PartyCompanyCapacityValue", "owner": "party_capacity", "rect": Rect2(Vector2(1050.6, 31.2), Vector2(1191.9 - 1050.6, 65.6 - 31.2))},
|
||||
]
|
||||
# A rail label whose type is identical at 130% still sits under the page-wide
|
||||
# 16px ceiling, which makes the accessibility scale a no-op on that page.
|
||||
var rail_font_by_scale := {}
|
||||
var ink_scanned := 0
|
||||
var probe_text_scale := 1.0
|
||||
var ink_violations: Array = []
|
||||
@@ -60,6 +72,8 @@ func _run() -> void:
|
||||
print("ABG_M66_PARTY_RISK_INK_FAIL scanned=%d violations=%d" % [ink_scanned, ink_violations.size()])
|
||||
quit(1)
|
||||
return
|
||||
print("ABG_M83_PARTY_TOP_RAIL_OK slots=%d node=PartyTitle,PartyPressureValue,PartyCompanyValue,PartyCompanyCapacityValue scales=1.0,1.3 fonts=%s" % [
|
||||
RAIL_SLOTS.size(), str(rail_font_by_scale)])
|
||||
print("ABG_M66_PARTY_RISK_VISUAL_PROBE_OK pages=party_line,risk_board,risk_commitment locales=%d scans=%d views=static,dynamic,final" % [LOCALES.size(), ink_scanned])
|
||||
quit(0)
|
||||
|
||||
@@ -88,6 +102,15 @@ func _capture(page: Dictionary, fixture: Vector2i, locale: String, out: String)
|
||||
violation["fixture"] = "%dx%d" % [fixture.x, fixture.y]
|
||||
violation["mode"] = mode
|
||||
ink_violations.append(violation)
|
||||
if page_name == "party_line" and mode == "final":
|
||||
for problem in _rail_problems(view, locale):
|
||||
ink_violations.append({
|
||||
"kind": "rail_slot", "page": page_name, "locale": locale,
|
||||
"fixture": "%dx%d" % [fixture.x, fixture.y], "mode": mode,
|
||||
"node": String(problem.get("node", "rail")),
|
||||
"path": "party_line/rail",
|
||||
"detail": String(problem.get("detail", "")),
|
||||
})
|
||||
var image := viewport.get_texture().get_image()
|
||||
if image != null:
|
||||
image.save_png(out.path_join("%s-%dx%d-%s-%s.png" % [page_name, fixture.x, fixture.y, locale, mode]))
|
||||
@@ -96,6 +119,50 @@ func _capture(page: Dictionary, fixture: Vector2i, locale: String, out: String)
|
||||
await process_frame
|
||||
|
||||
|
||||
func _rail_problems(view: Control, locale: String) -> Array:
|
||||
# M83: every authored top-rail box must carry a non-empty, contained engine
|
||||
# text at both shipped text scales, and the 130% scale must actually change
|
||||
# the rail type (the page-wide 16px ceiling used to make it a no-op).
|
||||
var problems: Array = []
|
||||
var fonts: Array = []
|
||||
for slot in RAIL_SLOTS:
|
||||
var node_name := String(slot.get("node", ""))
|
||||
var owner := String(slot.get("owner", ""))
|
||||
var box := Rect2(slot.get("rect", Rect2()))
|
||||
var label := view.find_child(node_name, true, false) as Label
|
||||
if label == null:
|
||||
problems.append({"node": node_name, "detail": "rail slot %s has no runtime text node" % owner})
|
||||
continue
|
||||
if label.text.strip_edges().is_empty():
|
||||
problems.append({"node": node_name, "detail": "rail slot %s is empty" % owner})
|
||||
continue
|
||||
var rect := label.get_global_rect()
|
||||
if not _rect_inside(rect, box):
|
||||
problems.append({"node": node_name, "detail": "label rect %s leaves the authored box %s" % [str(rect), str(box)]})
|
||||
var ink: Rect2 = InkCheck.ink_rect(label)
|
||||
if not _rect_inside(ink, box):
|
||||
problems.append({"node": node_name, "detail": "ink %s leaves the authored box %s" % [str(ink), str(box)]})
|
||||
fonts.append(label.get_theme_font_size("font_size"))
|
||||
rail_font_by_scale["%s@%.1f" % [locale, probe_text_scale]] = fonts
|
||||
if OS.get_environment("ABG_M66_RAIL_TRACE") == "1":
|
||||
print("ABG_M66_RAIL_TRACE locale=%s scale=%.1f fonts=%s problems=%d" % [locale, probe_text_scale, str(fonts), problems.size()])
|
||||
var at_100: Array = Array(rail_font_by_scale.get("%s@1.0" % locale, []))
|
||||
if is_equal_approx(probe_text_scale, 1.3) and at_100.size() == fonts.size() and not at_100.is_empty():
|
||||
var raised := false
|
||||
for index in range(fonts.size()):
|
||||
if int(fonts[index]) > int(at_100[index]):
|
||||
raised = true
|
||||
if not raised:
|
||||
problems.append({"node": "PartyRailScale",
|
||||
"detail": "rail type is identical at 100%% and 130%% (%s)" % str(fonts)})
|
||||
return problems
|
||||
|
||||
|
||||
func _rect_inside(inner: Rect2, outer: Rect2) -> bool:
|
||||
return inner.position.x >= outer.position.x - 0.5 and inner.position.y >= outer.position.y - 0.5 \
|
||||
and inner.end.x <= outer.end.x + 0.5 and inner.end.y <= outer.end.y + 0.5
|
||||
|
||||
|
||||
func _apply_mode(view: Control, page_name: String, mode: String) -> void:
|
||||
# static-only: the authored master with no runtime content.
|
||||
# dynamic-only: runtime content on a neutral field (slot overflow, contrast,
|
||||
|
||||
Reference in New Issue
Block a user