fix(m82): gate the market dock in authored and play space and record the final bounds

This commit is contained in:
2026-09-18 01:57:05 +08:00
parent 52f9e1d580
commit ea9a3e8bb9
2 changed files with 136 additions and 28 deletions
@@ -26,6 +26,17 @@ Market 页在三种状态下各自成立:
(全局 145.6..181.6);**分隔横线在 `band.end.y-6` → 全局 175.6**。
- 结论:横线落在标题槽内,130% 字号时压到文字 → 需按实际尺寸计算标题槽(不得写死分数)。
## 最终 authored bounds(2026-09-17,实测后重记)
- 外层 `MarketContextSurface`(`M52MarketSlotAdapter.ledger_area`):**`Rect2(0.630, 0.125, 0.345, 0.715)`**
—— 上移至顶栏下方、下抵 footer 上方,为 130% 让出纵向空间(1280×720 时 806.4, 90.0 / 441.6×514.8)。
- `ledger_stack` 的槽序与高度(`hired en@1.3`,ledger 441.6×514.8,band 67):
band→portrait→identity→profession→detail→trait→cost→dock 顺序固定,相邻不重叠、全部 contained;
**portrait ≥72 play-size(新硬断言)**,空间不足时按存在内容跳行(空 trait/profession 不占高)。
- 行高来源:detail = `micro` token 实测行高 × (1 或 2 行) + 10;trait/cost/identity/profession 按 token×相对缩放并只保留实测所需。
- 探针新增硬断言:portrait ≥72;icon 与 availability ink 不相交;availability 与 amount ink 不相交;
detail 无 overrun 且 `get_line_count() <= get_visible_line_count()`;空态 `chrome_variant == ledger_empty`。
## Ordered steps
1. **M82-A**:本计划(提交一)。
@@ -65,5 +76,8 @@ Market 页在三种状态下各自成立:
- `M82-I01` Guild:`GuildKioskCopyArea` 文字落在作者化红帷幔 + 状态行字号/对比弱(另包)。
- `M82-I02` Member:详情区信息架构/密度不足(另包)。
- `M82-I04` **OPEN(2026-09-17,owner 目检发现)**:中文 hired 的详情行出现语言混排
(`999 Coin · 队伍 8 → 9`,货币词为英文)。属文案语义/本地化,M82 frozen scope 禁止顺手修改,
登记为后续窄包处理(与 M61-I05/M80-I10 无耦合)。
- `M82-I03` Party:第 4 图标槽无 runtime content、`PartyCompanyValue` 锚位错位(另包,需 Owner 指定该槽事实)。
- 本包内发现的新问题只登记,不顺手修。
+122 -28
View File
@@ -89,38 +89,57 @@ func _run() -> void:
quit(0)
func _dock_bounds(view: Control, ui_factor := 1.0) -> Dictionary:
# Node rects are authored in the 1280x720 design space (the canvas transform only
# scales rendering), so the position expectations are design-space constants and
# the hit-size floor is checked in play pixels through the class fit factor.
# The row lives inside the centred canvas; its authored x/y tokens are checked
# in canvas-authored space (inverse of the canvas transform), while the physical
# hit floor stays on real screen pixels.
var back := view.find_child("MarketBackGuild", true, false) as Button
var ret := view.find_child("MarketReturnContract", true, false) as Button
if back == null or ret == null:
return {"ok": false, "detail": "missing dock command"}
var b: Rect2 = back.get_global_rect()
var r: Rect2 = ret.get_global_rect()
var canvas := view.find_child("MarketMasterCanvas", true, false) as Control
var to_authored := Transform2D.IDENTITY
if canvas != null:
to_authored = canvas.get_global_transform_with_canvas().affine_inverse()
var b_global: Rect2 = back.get_global_rect()
var r_global: Rect2 = ret.get_global_rect()
var b := _transform_rect(to_authored, b_global)
var r := _transform_rect(to_authored, r_global)
var problems: Array = []
if int(b.position.x) < 40 or int(b.position.x) > 80:
problems.append("back_left=%d" % int(b.position.x))
problems.append("back_left=%d (authored)" % int(b.position.x))
if int(r.end.x) < 1200 or int(r.end.x) > 1272:
problems.append("return_right=%d" % int(r.end.x))
problems.append("return_right=%d (authored)" % int(r.end.x))
if absf(b.size.y - r.size.y) > 2.0:
problems.append("height %d vs %d" % [int(b.size.y), int(r.size.y)])
if absf(b.position.y - r.position.y) > 2.0:
problems.append("baseline %d vs %d" % [int(b.position.y), int(r.position.y)])
var play_back := _play_size(back)
if play_back.y < 48.0:
problems.append("back_height=%.0fpx play (floor 48)" % play_back.y)
# Physical floor on real screen pixels; the authored ceiling stays on the
# canvas-authored rect (no scale factor, no tolerance multiplier).
if b_global.size.y < 48.0:
problems.append("back_height=%.0fpx play (floor 48)" % b_global.size.y)
if b.size.y > 60.0:
problems.append("back_height=%d exceeds the 60px token" % int(b.size.y))
var detail := "back x %d..%d y %d..%d | return x %d..%d y %d..%d" % [
problems.append("back_height=%d authored exceeds the 60px token" % int(b.size.y))
var detail := "authored back x %d..%d y %d..%d | return x %d..%d y %d..%d | play h=%.0f" % [
int(b.position.x), int(b.end.x), int(b.position.y), int(b.end.y),
int(r.position.x), int(r.end.x), int(r.position.y), int(r.end.y),
int(r.position.x), int(r.end.x), int(r.position.y), int(r.end.y), b_global.size.y,
]
if not problems.is_empty():
return {"ok": false, "detail": "%s | %s" % [",".join(problems), detail]}
return {"ok": true, "detail": detail}
func _transform_rect(xform: Transform2D, rect: Rect2) -> Rect2:
var a := xform * rect.position
var b_pt := xform * Vector2(rect.end.x, rect.position.y)
var c := xform * Vector2(rect.position.x, rect.end.y)
var d := xform * rect.end
var xs := [a.x, b_pt.x, c.x, d.x]
var ys := [a.y, b_pt.y, c.y, d.y]
xs.sort()
ys.sort()
return Rect2(xs[0], ys[0], xs[xs.size() - 1] - xs[0], ys[ys.size() - 1] - ys[0])
func _variant_label(longest: bool, kind: String, text_scale: float = 1.0) -> String:
var base := kind if kind != "recruit" else ("longest" if longest else "normal")
return base if is_equal_approx(text_scale, 1.0) else "%s@%.1f" % [base, text_scale]
@@ -147,32 +166,39 @@ func _capture_fixture(width: int, height: int, locale: String, longest: bool, ki
"variant": _variant_label(longest, kind, text_scale), "locale": locale,
"size": "%dx%d" % [capture_size.x, capture_size.y],
"detail": "requested %dx%d but captured %dx%d" % [width, height, capture_size.x, capture_size.y]})
# The shipped window lays the 1280x720 design out and scales it into the real
# window (canvas_items + keep). A SubViewport is not a Window, so the same
# semantics are reproduced here: the page is authored on a 1280x720 stage and
# the stage is scaled/centred into the requested viewport. That keeps the
# authored contract identical across classes while PNG pixels follow `requested`.
var stage := Control.new()
stage.name = "M82DesignStage"
stage.size = Vector2(1280.0, 720.0)
sub.add_child(stage)
var fit := minf(float(width) / 1280.0, float(height) / 720.0)
var offset := Vector2((float(width) - 1280.0 * fit) * 0.5, (float(height) - 720.0 * fit) * 0.5)
sub.canvas_transform = Transform2D(Vector2(fit, 0.0), Vector2(0.0, fit), offset)
# M82: one geometry source only. The view enters the requested-size SubViewport
# first, then a full-rect preset with zeroed offsets owns its size; setting
# `view.size` as well doubled the layout (full anchors + size offsets).
var view := MarketPageView.new()
view.name = "MarketVisualProbeView"
view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
view.configure_presentation({"safe_area": 24, "text_scale": text_scale, "locale": locale})
stage.add_child(view)
sub.add_child(view)
view.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
view.offset_left = 0.0
view.offset_top = 0.0
view.offset_right = 0.0
view.offset_bottom = 0.0
await _settle()
_note("%s before_render" % kind)
view.render(_fixture_data(locale, longest, kind))
_note("%s after_render" % kind)
await _settle()
_note("%s after_first_settle" % kind)
var label := _variant_label(longest, kind, text_scale)
await _save_image(sub, "%s/%s-%dx%d-%s-final.png" % [output_root, locale, capture_size.x, capture_size.y, label])
var final_path := "%s/%s-%dx%d-%s-final.png" % [output_root, locale, capture_size.x, capture_size.y, label]
await _save_image(sub, final_path)
for problem in _edge_bar_problems(final_path):
if strict:
ink_violations.append({"node": "MarketCoverage", "kind": "bars", "path": kind,
"variant": label, "locale": locale, "size": "%dx%d" % [capture_size.x, capture_size.y],
"detail": problem})
else:
print("ABG_M82_SMOKE_NOTE %dx%d %s %s bars: %s" % [capture_size.x, capture_size.y, locale, label, problem])
_trace_coverage(view, Vector2(capture_size))
var violations := InkCheck.scan(view)
for problem in _coverage_problems(view, Vector2(capture_size)):
violations.append({"node": "MarketCoverage", "kind": "coverage", "path": kind, "detail": problem})
_note("%s after_scan" % kind)
if strict:
for problem in _state_problems(view, kind, locale, text_scale):
@@ -481,6 +507,74 @@ func _trace_stack(ledger: Control, kind: String, locale: String, scale: float) -
if node != null:
print("ABG_M82_TRACE global %-16s pos=%s size=%s" % [node_name, str(node.global_position), str(node.size)])
func _coverage_problems(view: Control, viewport_size: Vector2) -> Array:
# AGENTS: centred design canvas for geometry, aspect-cover for the world
# background. Everything is compared in the capture viewport's own space.
var problems: Array = []
var view_rect := view.get_global_rect()
if absf(view_rect.position.x) > 1.0 or absf(view_rect.position.y) > 1.0 \
or absf(view_rect.size.x - viewport_size.x) > 1.0 or absf(view_rect.size.y - viewport_size.y) > 1.0:
problems.append("view %s != viewport %s" % [str(view_rect), str(viewport_size)])
var background := view.find_child("M52MarketWorldBackground", true, false) as Control
if background == null:
problems.append("world background node missing")
else:
var bg := background.get_global_rect()
if absf(bg.position.x) > 1.0 or absf(bg.position.y) > 1.0 \
or absf(bg.size.x - viewport_size.x) > 1.0 or absf(bg.size.y - viewport_size.y) > 1.0:
problems.append("world background %s != viewport %s" % [str(bg), str(viewport_size)])
var canvas := view.find_child("MarketMasterCanvas", true, false) as Control
if canvas == null:
problems.append("master canvas node missing")
else:
var art := canvas.get_global_rect()
var expect_h := minf(viewport_size.y, viewport_size.x * 9.0 / 16.0)
var expect_w := expect_h * 16.0 / 9.0
var expect_x := (viewport_size.x - expect_w) * 0.5
if absf(art.position.x - expect_x) > 1.5 or absf(art.size.x - expect_w) > 1.5 or absf(art.size.y - expect_h) > 1.5:
problems.append("master canvas %s != centred 16:9 %s" % [str(art),
str(Rect2(expect_x, 0.0, expect_w, expect_h))])
return problems
func _trace_coverage(view: Control, viewport_size: Vector2) -> void:
if not trace:
return
var background := view.find_child("M52MarketWorldBackground", true, false) as Control
var canvas := view.find_child("MarketMasterCanvas", true, false) as Control
print("ABG_M82_TRACE coverage viewport=%s view=%s background=%s canvas=%s" % [
str(viewport_size), str(view.get_global_rect()),
str(background.get_global_rect()) if background != null else "missing",
str(canvas.get_global_rect()) if canvas != null else "missing"])
func _edge_bar_problems(image_path: String) -> Array:
# A clear-colour bar on any edge fails the capture (aspect-cover background).
var problems: Array = []
var image := Image.new()
if image.load(image_path) != OK:
problems.append("saved frame could not be read back")
return problems
var clear: Color = ProjectSettings.get_setting("rendering/environment/defaults/default_clear_color", Color(0.3, 0.3, 0.3))
var w := image.get_width()
var h := image.get_height()
var edges := {
"left": [[1, h / 4], [1, h / 2], [1, (h * 3) / 4]],
"right": [[w - 2, h / 4], [w - 2, h / 2], [w - 2, (h * 3) / 4]],
"top": [[w / 4, 1], [w / 2, 1], [(w * 3) / 4, 1]],
"bottom": [[w / 4, h - 2], [w / 2, h - 2], [(w * 3) / 4, h - 2]],
}
for edge in edges:
var matches := 0
for point in edges[edge]:
var px := image.get_pixel(int(point[0]), int(point[1]))
if absf(px.r - clear.r) < 0.02 and absf(px.g - clear.g) < 0.02 and absf(px.b - clear.b) < 0.02:
matches += 1
if matches >= 2:
problems.append("%s edge shows the clear colour" % edge)
return problems
func _trace_floors(ledger: Control, kind: String) -> void:
if not trace or kind == "empty":
return