fix(m82): gate the market dock in authored and play space and record the final bounds
This commit is contained in:
@@ -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 指定该槽事实)。
|
||||
- 本包内发现的新问题只登记,不顺手修。
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user