fix(ui): PR #217 review fixes — derived legend offset, 960x540 goldens, non-overlap tests

The legend's reposition() now derives its Y from the header panel's
MEASURED bottom plus a named 12px gap (new accessors on the viewer;
deferred recompute so it reads settled layout, refreshed on header
content change) — the old hardcoded 60.0 sat 17px inside the wrapped
header's real 77px bottom, fusing the panels. Pixel-proven at the
evidence center: bottom=77.0, legend top=89.0, gap=12.0 exact. All 13
goldens regenerated at the DOCUMENTED 960x540 (the 1280x720 rider was
my own CLI flag, not a config; PIL-verified) with two-run byte
stability. Two structural tests pin the header/legend relationship
(T-1192 precedent), proven failing-first against the old constant
(-17.0px reported), and the legacy test that pinned the literal 60.0
now asserts the derived relationship so it cannot re-enforce drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 15:13:11 +02:00
co-authored by Claude Fable 5
parent 34ecdad46f
commit a4d9a4fa09
17 changed files with 176 additions and 4 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+19 -2
View File
@@ -73,13 +73,30 @@ func test_refresh_reflects_the_active_overlay_toggle() -> void:
).is_greater(rows_before)
func test_reposition_sets_a_fixed_panel_margin_position() -> void:
## T-1197 PR #217 review (both reviewers, pixel-proven): this used to assert
## a hardcoded Vector2(PANEL_MARGIN, 60.0) — the same "60.0" constant whose
## drift out of sync with the header panel's REAL grown footprint (once
## step_canvas_viewer.gd wrapped the header in its own ImplantPanel) caused
## the two panels to fuse into one double-height box. The fix removed that
## hardcoded Y from production code (step_canvas_legend.gd's reposition() now
## derives it from StepCanvasViewer.get_header_panel_bottom_y() +
## get_header_legend_gap_px()) — this test must assert the SAME derived
## relationship, not a second copy of the old magic number, or it would go on
## silently enforcing the exact drift-prone shape production code just
## stopped doing (test_step_canvas_viewer.gd's
## test_legend_panel_never_overlaps_the_header_panel_vertically/
## test_legend_panel_leaves_a_real_gap_below_the_header_panel are the fuller
## non-overlap/gap proof against the REAL viewer-owned legend; this one stays
## a narrow smoke check on a standalone legend instance, matching this file's
## own "smoke coverage" scope note at the top).
func test_reposition_derives_y_from_the_headers_measured_bottom_plus_gap() -> void:
var v: StepCanvasViewer = _make_viewer()
add_child(v)
var legend = LegendScript.new(v)
auto_free(legend)
legend.reposition()
assert_that(legend.position).is_equal(Vector2(LegendScript.PANEL_MARGIN, 60.0))
var expected_y: float = v.get_header_panel_bottom_y() + v.get_header_legend_gap_px()
assert_that(legend.position).is_equal(Vector2(LegendScript.PANEL_MARGIN, expected_y))
## T-1192 review fix: RESERVED_COLUMN_PX is now a direct read of
+78
View File
@@ -837,3 +837,81 @@ func test_get_current_canvas_summary_counts_match_a_fixture_canvas() -> void:
var by_class: Dictionary = summary.get("course_count_by_class", {})
assert_int(int(by_class.get(0, 0))).is_equal(2)
assert_int(int(by_class.get(2, 0))).is_equal(1)
# =============================================================================
# T-1197 PR #217 review (Hoshe): header-panel-vs-legend-panel vertical
# non-overlap — the exact regression this review round caught. Mirrors the
# T-1192 precedent above (test_global_canvas_left_edge_never_overlaps_the_legend_column,
# line ~674): a geometric non-overlap invariant against the REAL viewer
# wiring, not a hand-computed expected pixel value that could silently drift
# out of sync with the production layout the same way the old hardcoded
# Vector2(PANEL_MARGIN, 60.0) drifted out of sync with the header's real
# grown footprint.
# =============================================================================
## The screen header panel and the legend panel must never vertically
## overlap: the legend's TOP edge (position.y) must be at or below the
## header's BOTTOM edge (position.y + size.y). Before the T-1197 PR #217 fix,
## step_canvas_legend.gd's reposition() hardcoded Y=60.0 — a constant tuned
## for the OLD bare-ImplantHeader footprint — so once the header grew its own
## ImplantPanel wrapper (border + content margins), the legend's fixed Y sat
## INSIDE the header panel's new, taller footprint: the two fused into one
## unbroken double-height box with zero terrain gap between them (pixel-
## proven independently by both PR #217 reviewers). This test pins the
## invariant directly against the real _screen_header_panel/_legend_panel
## Controls the production layout builds, not a copy of the geometry math.
func test_legend_panel_never_overlaps_the_header_panel_vertically() -> void:
var v: StepCanvasViewer = _make_viewer()
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
# Both panels are manually positioned (implant_panel.gd's own doc: "not
# itself inside a parent Container, so nothing else forces a re-measure"),
# and reset_to_content_size()/reposition() are deferred — award one idle
# frame so the REAL settled sizes are in place before asserting, exactly
# like the deferred-resize idiom both panels already rely on in production
# (see _build_screen_header()'s own call to reset_to_content_size(), and
# _ready()'s own deferred reposition() call added alongside this test).
await get_tree().process_frame
await get_tree().process_frame
var header_top: float = v._screen_header_panel.position.y
var header_bottom: float = header_top + v._screen_header_panel.size.y
var legend_top: float = v._legend_panel.position.y
assert_float(legend_top).override_failure_message(
(
"the legend panel's top edge (y=%.1f) must be AT OR BELOW the header"
+ " panel's measured bottom edge (y=%.1f) — a smaller value means the"
+ " two panels overlap/fuse into one box, the exact PR #217 regression"
)
% [legend_top, header_bottom]
).is_greater_equal(header_bottom - 0.01)
## The gap must be a REAL, visible gap — not just "touching at exactly the
## same pixel" (which would still satisfy >= but reads as fused on screen).
## Pins the fixed HEADER_LEGEND_GAP_PX constant is actually being applied,
## not merely that overlap happens to be avoided by coincidence of content
## size on this particular test body.
func test_legend_panel_leaves_a_real_gap_below_the_header_panel() -> void:
var v: StepCanvasViewer = _make_viewer()
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
await get_tree().process_frame
await get_tree().process_frame
var header_bottom: float = v._screen_header_panel.position.y + v._screen_header_panel.size.y
var legend_top: float = v._legend_panel.position.y
var gap: float = legend_top - header_bottom
assert_float(gap).override_failure_message(
(
"expected a visible gap of at least %.1fpx between the header panel's"
+ " bottom (y=%.1f) and the legend panel's top (y=%.1f), got %.1fpx —"
+ " panels that merely touch still read as one fused box on screen"
)
% [v.get_header_legend_gap_px(), header_bottom, legend_top, gap]
).is_greater_equal(v.get_header_legend_gap_px() - 0.01)