fix(ui): PR #205 review round — integer px-per-gridunit fit replaces fractional (T-1189, T-1192)

The fractional fit branch is deleted, resolving both review findings at
the root: tyre showed its comments cited D-255 for an exception the
record does not contain (the language came from the lead's ticket text,
not governance), and hoshe showed it returned sub-1x for a canvas
exceeding the viewport on one axis. Replacement: fit_scale_ratio()
chooses the largest integer pixels-per-gridunit R fitting both
legend-reserved axes, floored at 1 (over-viewport draws native and
crops like every fixed rung) — the fine-grained integer lattice (GJ1c
1080p -> 9px/gu = 1593x792, ~98% width; 4K -> 20) that makes the
fractional hatch unnecessary. center_offset() floors to whole pixels
(half-pixel centering would blur the texel grid). Doc comments cite the
real sanction (D-255 amendment 2026-07-25, this branch). Legend column
constant is now canonical in transport, read directly by the legend
(was an independently-typed literal); its test asserts real geometry.
New regression test proves _global_body_extent clears on body switch
and never caps another body's requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:14:12 +02:00
co-authored by Claude Fable 5
parent 915a6790f0
commit 374b195594
8 changed files with 312 additions and 176 deletions
+21 -9
View File
@@ -71,12 +71,24 @@ func test_reposition_sets_a_fixed_panel_margin_position() -> void:
assert_that(legend.position).is_equal(Vector2(LegendScript.PANEL_MARGIN, 60.0))
## T-1192: StepCanvasTransport.LEGEND_COLUMN_PX (the Global fit-scale
## reservation StepCanvasViewer applies) must stay derived from this SAME
## panel's own width/margin — a drift here would silently reopen the
## "legend overlaps the canvas" defect on one side while the OTHER side
## thinks it already reserved enough room.
func test_reserved_column_px_matches_the_transport_sides_own_constant() -> void:
assert_float(LegendScript.RESERVED_COLUMN_PX).is_equal_approx(
StepCanvasTransport.LEGEND_COLUMN_PX, 0.01
)
## T-1192 review fix: RESERVED_COLUMN_PX is now a direct read of
## StepCanvasTransport.LEGEND_COLUMN_PX (not an independently-typed
## literal), so a const-vs-const cross-pin test would be structurally
## unable to fail — it is the SAME value by construction. What's still
## worth proving directly is the geometric guarantee that constant is FOR:
## the legend's own ACTUAL laid-out right edge (`reposition()`'s position.x
## + the panel's real minimum width) must land at or before the reserved
## column, with room to spare — never right up against it, and certainly
## never past it into where the canvas is centered from.
func test_legend_actual_right_edge_stays_inside_the_reserved_column() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
var legend = LegendScript.new(v)
auto_free(legend)
legend.reposition()
var actual_right_edge: float = legend.position.x + legend.custom_minimum_size.x
assert_float(actual_right_edge).override_failure_message(
"the legend's real laid-out right edge must stay inside the column"
+ " StepCanvasViewer reserves for it, with margin to spare"
).is_less(StepCanvasTransport.LEGEND_COLUMN_PX)
+129 -57
View File
@@ -280,72 +280,143 @@ func test_center_offset_goes_negative_when_the_canvas_overflows_the_viewport() -
assert_float(offset.y).is_greater(0.0)
# =============================================================================
# T-1192: Global integer-fit scale — D-255 texel-exactness
# =============================================================================
func test_integer_fit_scale_picks_the_largest_multiple_that_fits_both_axes() -> void:
# GJ1c reference case: 177x88 texels at the 5x5 shallow display ratio =
# 885x440 px footprint, fit against a full 1920x1080 viewport.
var scale: int = StepCanvasTransport.integer_fit_scale(
Vector2(885.0, 440.0), Vector2(1920.0, 1080.0)
## D-255 texel-exactness: an integer-px/gridunit canvas (fit_scale_ratio())
## can still land on an ODD-vs-viewport remainder that halves to a .5px
## boundary — GJ1c's own 1593x792 footprint in a 1628x1080 available area
## ((1628-1593)*0.5 = 17.5) is the real case this guards. The offset must be
## FLOORED to a whole pixel, never left fractional (a fractional offset
## would blur the texel grid this whole mechanism exists to keep crisp).
func test_center_offset_floors_a_half_pixel_remainder_to_a_whole_pixel() -> void:
var offset: Vector2 = StepCanvasTransport.center_offset(
Vector2(1593.0, 792.0), Vector2(1628.0, 1080.0)
)
assert_int(scale).is_equal(2)
assert_float(offset.x).is_equal_approx(17.0, 0.001)
assert_float(offset.y).is_equal_approx(144.0, 0.001)
func test_integer_fit_scale_is_bounded_by_the_tighter_axis() -> void:
# Wide-but-short viewport: x could fit 4x, y only fits 1x — the smaller
# =============================================================================
# T-1192: Global integer PIXELS-PER-GRIDUNIT fit — D-255 amendment
# 2026-07-25 (rung-0's display ratio is viewport-fitted per body to an
# INTEGER px/gridunit ratio; the fractional-fit branch a prior round of this
# ticket carried was a mis-citation of D-255 — the record's actual mandate
# is unqualified texel-exact — and independently a real bug (Hoshe): a
# canvas exceeding the viewport on one axis could fit_scale() down to
# sub-1x, violating the "never below native resolution" invariant. Fixed at
# the root by fitting the INTEGER ratio, not a fraction of the whole
# footprint — this lattice is fine-grained (per gridunit, not per 5px-base
# footprint step), so the coverage-threshold escape hatch this section used
# to need does not come up: the achievable ratios are close enough together
# that the largest one that fits is always a good use of the frame.
# =============================================================================
func test_fit_scale_ratio_picks_the_largest_px_per_gridunit_that_fits_both_axes() -> void:
# GJ1c reference case (T-1183 eyeball): 177x88 gridunits against a full
# 1920x1080 viewport (no legend reservation) — floor(1920/177)=10,
# floor(1080/88)=12, the tighter axis (x) wins.
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(177.0, 88.0), Vector2(1920.0, 1080.0)
)
assert_int(ratio).is_equal(10)
## The GJ1c reference case AFTER the legend column is reserved (T-1192, the
## StepCanvasViewer end-to-end scenario): available area shrinks to
## 1628x1080 — floor(1628/177)=9, floor(1080/88)=12 — the lead's own cited
## reference number for this exact case.
func test_fit_scale_ratio_matches_the_gj1c_legend_reserved_reference_case() -> void:
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(177.0, 88.0), Vector2(1628.0, 1080.0)
)
assert_int(ratio).is_equal(9)
## GJ1c at 4K, legend column reserved (3840 - 292 = 3548 available) — the
## lead's own cited reference number for a large viewport.
func test_fit_scale_ratio_matches_the_gj1c_4k_reference_case() -> void:
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(177.0, 88.0), Vector2(3548.0, 2160.0)
)
assert_int(ratio).is_equal(20)
## Hoshe's repro, now a regression test: a canvas whose BASE footprint
## (885x440 px at the old 5x5-multiple framing) exceeds a narrow 348px-wide
## available viewport used to make fit_scale() return 0.39x — a sub-1x
## downscale violating "never below native resolution". The integer
## px/gridunit ratio floors at 1 instead: draws at native (1 px/gridunit)
## and crops/pans, exactly like every other fixed rung's own
## exceeds-the-viewport precedent.
func test_fit_scale_ratio_floors_at_one_when_gridunits_exceed_a_narrow_viewport() -> void:
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(177.0, 88.0), Vector2(348.0, 1080.0)
)
assert_int(ratio).is_equal(1)
## A small moon's Global canvas (few gridunits) in a large viewport still
## only wins as large an integer ratio as fits — no special-casing for a
## small canvas, same formula, much larger achievable ratio.
func test_fit_scale_ratio_a_tiny_moon_canvas_wins_a_large_integer_ratio() -> void:
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(20.0, 20.0), Vector2(1920.0, 1080.0)
)
assert_int(ratio).is_equal(54)
## Exact-fit boundary: the viewport is PRECISELY `extent * 3` on both axes —
## the ratio must land exactly on 3, not overshoot to 4 (floor(exact) must
## not round up) and not undershoot to 2 (an exact multiple is a legal fit,
## not treated as "just barely doesn't fit").
func test_fit_scale_ratio_exact_multiple_boundary_lands_on_the_multiple() -> void:
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(100.0, 50.0), Vector2(300.0, 150.0)
)
assert_int(ratio).is_equal(3)
## One pixel short of the exact multiple must drop to the NEXT integer down
## — confirms the boundary isn't fuzzy/off-by-one in the other direction.
func test_fit_scale_ratio_one_pixel_short_of_the_multiple_drops_a_step() -> void:
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(100.0, 50.0), Vector2(299.0, 150.0)
)
assert_int(ratio).is_equal(2)
func test_fit_scale_ratio_is_bounded_by_the_tighter_axis() -> void:
# Wide-but-short viewport: x could fit 10x, y only fits 1x — the smaller
# wins (never overflow either axis).
var scale: int = StepCanvasTransport.integer_fit_scale(
var ratio: int = StepCanvasTransport.fit_scale_ratio(
Vector2(100.0, 100.0), Vector2(1000.0, 150.0)
)
assert_int(scale).is_equal(1)
assert_int(ratio).is_equal(1)
func test_integer_fit_scale_never_drops_below_one() -> void:
# A canvas larger than the viewport still gets scale 1 (draw at native
# size and let it exceed/crop), never a shrink below native.
var scale: int = StepCanvasTransport.integer_fit_scale(
Vector2(3000.0, 3000.0), Vector2(800.0, 600.0)
)
assert_int(scale).is_equal(1)
func test_fit_scale_ratio_handles_a_zero_extent_axis_without_dividing_by_zero() -> void:
var ratio: int = StepCanvasTransport.fit_scale_ratio(Vector2.ZERO, Vector2(800.0, 600.0))
assert_int(ratio).is_equal(1)
func test_integer_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> void:
var scale: int = StepCanvasTransport.integer_fit_scale(Vector2.ZERO, Vector2(800.0, 600.0))
assert_int(scale).is_equal(1)
## fit_scale_from_ratio() converts the INTEGER px/gridunit ratio into the
## `_canvas.scale` multiplier applied on top of a texture already rendered
## at the rung's own base display ratio (5x5 for Global) — this multiplier
## itself may be a non-integer float (9/5 = 1.8), and that is CORRECT:
## texel-exactness is about the final ratio being a whole number, not the
## Node2D scale field.
func test_fit_scale_from_ratio_divides_by_the_base_display_ratio() -> void:
var scale: float = StepCanvasTransport.fit_scale_from_ratio(9, 5.0)
assert_float(scale).is_equal_approx(1.8, 0.001)
func test_fit_scale_matches_the_integer_fit_when_coverage_is_high() -> void:
# The GJ1c full-viewport case: integer 2x covers well over the
# FIT_MIN_COVERAGE_RATIO bar, so fit_scale() must agree with
# integer_fit_scale() exactly (no fractional fallback).
var scale: float = StepCanvasTransport.fit_scale(
Vector2(885.0, 440.0), Vector2(1920.0, 1080.0)
)
assert_float(scale).is_equal_approx(2.0, 0.001)
func test_fit_scale_from_ratio_at_the_gj1c_4k_reference_case() -> void:
var scale: float = StepCanvasTransport.fit_scale_from_ratio(20, 5.0)
assert_float(scale).is_equal_approx(4.0, 0.001)
## The GJ1c reference case AFTER the legend column is reserved (T-1192):
## available area shrinks to 1628x1080, so the 2x integer candidate (1770 px
## wide) no longer fits — integer_fit_scale() drops to 1x, which only covers
## ~41% of the tighter available axis, well under FIT_MIN_COVERAGE_RATIO —
## fit_scale() must fall back to the non-integer uniform fit that fills the
## tighter (x) axis exactly, not settle for the sparse 1x frame.
func test_fit_scale_falls_back_to_fractional_fit_on_excessive_letterboxing() -> void:
var scale: float = StepCanvasTransport.fit_scale(
Vector2(885.0, 440.0), Vector2(1628.0, 1080.0)
)
assert_float(scale).is_greater(1.0)
assert_float(scale).is_less(2.0)
# The fractional fit fills the tighter (x) axis exactly.
assert_float(885.0 * scale).is_equal_approx(1628.0, 0.01)
func test_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> void:
var scale: float = StepCanvasTransport.fit_scale(Vector2.ZERO, Vector2(800.0, 600.0))
assert_float(scale).is_equal_approx(1.0, 0.001)
func test_fit_scale_from_ratio_handles_a_zero_base_ratio_without_dividing_by_zero() -> void:
var scale: float = StepCanvasTransport.fit_scale_from_ratio(9, 0.0)
assert_float(scale).is_greater(0.0)
# =============================================================================
@@ -353,9 +424,10 @@ func test_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> voi
# =============================================================================
func test_legend_column_px_is_positive_and_matches_the_legend_panels_own_sizing() -> void:
# Pinned against step_canvas_legend.gd's own RESERVED_COLUMN_PX (260 +
# 16*2 = 292) — the two constants must never drift apart, since the
# "beside, never over" guarantee depends on both sides agreeing on the
# SAME reserved width.
## StepCanvasTransport is the CANONICAL source for this width (T-1192
## review fix) — step_canvas_legend.gd's own RESERVED_COLUMN_PX is now a
## direct read of THIS constant, not an independently-typed literal, so
## there is no separate cross-pin test needed here; this just pins the
## canonical value itself (260 panel width + 16*2 margin = 292).
func test_legend_column_px_is_the_panel_width_plus_margin_on_both_sides() -> void:
assert_float(StepCanvasTransport.LEGEND_COLUMN_PX).is_equal_approx(292.0, 0.01)
+85 -31
View File
@@ -529,6 +529,42 @@ func test_district_request_extent_is_never_capped_by_the_global_extent() -> void
assert_that(extent).is_equal(uncapped)
## Hoshe (review round 2): _global_body_extent must NOT leak across a body
## switch — land a Global extent for body A, enter() body B, and prove BOTH
## that the cap source itself reads back ZERO for the new body AND that a
## Region request for body B before ITS OWN Global echo lands goes out
## UNCAPPED (never silently capped by body A's leftover grid). The reset
## already exists at StepCanvasViewer.enter() ("a new body has its own
## region grid") — this proves it, through the real enter()/land/enter()
## sequence rather than asserting the field directly only.
func test_global_body_extent_resets_on_a_different_body_and_does_not_leak() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
assert_that(v._global_body_extent).override_failure_message(
"test setup: body A must actually have a landed cap source"
).is_equal(GJ1C_GLOBAL_EXTENT)
v.enter({"body_id": "T1189_extent_cap_body_b", "body_radius_km": 3000.0}, {})
assert_that(v._global_body_extent).override_failure_message(
"entering a DIFFERENT body must reset the cap source to ZERO — body"
+ " A's region grid must never leak into body B's requests"
).is_equal(Vector2i.ZERO)
v._scroll_rung(1, Vector2(960.0, 540.0)) # Region, for body B — no Global echo yet
var extent: Vector2i = v._request_extent()
var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "Region")
assert_that(extent).override_failure_message(
"body B's Region request, before body B's own Global echo has"
+ " landed, must go out UNCAPPED — never capped by body A's stale"
+ " leftover region-grid extent"
).is_equal(uncapped)
# =============================================================================
# T-1189/T-1192: shared letterbox mechanism — centering + Global fit scale.
# =============================================================================
@@ -551,28 +587,46 @@ func test_global_canvas_arrival_centers_the_view_not_top_left() -> void:
).is_not_equal(Vector2.ZERO)
## D-255 texel-exactness (T-1192): at a large (4K-class) viewport, GJ1c's
## 885x440 raw footprint (177x88 texels x 5x5 shallow display ratio) clears
## the coverage bar even AFTER the legend column is reserved, so the
## INTEGER fit wins outright — verified end-to-end through the real viewer
## wiring, not just the pure transport function this mirrors. (The T-1183
## reference 1920x1080 viewport is deliberately NOT used here — at that
## size the legend-column reservation starves the integer candidate below
## the coverage bar and the fractional escape hatch fires instead, covered
## separately by test_global_canvas_uses_fractional_fit_when_legend_column_
## starves_the_integer_fit() below.)
func test_global_canvas_scale_is_an_integer_multiple_of_the_raw_footprint() -> void:
## D-255 amendment 2026-07-25 texel-exactness: the `_canvas.scale` value
## itself is NOT required to be a whole number (9/5 = 1.8 is entirely
## legitimate) — what MUST be exact is the resulting on-screen
## pixels-per-gridunit ratio. Verified end-to-end through the real viewer
## wiring: `_canvas_scale * base_display_ratio` (the rung's own display
## ratio, 5.0 for Global) must land on an exact integer, for both the
## legend-reserved 1920x1080 case (R=9, scale=1.8) AND the 4K case (R=20,
## scale=4.0) — the SAME formula, no separate coverage-threshold branch.
func test_global_canvas_scale_yields_an_exact_integer_pixels_per_gridunit_ratio() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
assert_that(v._canvas.scale).is_equal(Vector2(v._canvas_scale, v._canvas_scale))
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
var effective_px_per_gridunit: float = v._canvas_scale * base_ratio
assert_float(effective_px_per_gridunit).override_failure_message(
"the FINAL on-screen pixels-per-gridunit ratio must be an exact"
+ " integer even when the _canvas.scale multiplier itself is not"
).is_equal_approx(roundf(effective_px_per_gridunit), 0.001)
# The lead's own cited reference number for this exact scenario.
assert_float(effective_px_per_gridunit).is_equal_approx(9.0, 0.001)
## Same invariant at a large (4K-class) viewport, where the integer ratio
## (20) happens to make the _canvas.scale multiplier itself a whole number
## too (20/5 = 4.0) — confirms the 1080p case above isn't a coincidence of
## a small viewport, just the same formula at a different achievable ratio.
func test_global_canvas_scale_at_4k_also_yields_an_exact_integer_ratio() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.size = Vector2(3840.0, 2160.0)
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
assert_that(v._canvas.scale).is_equal(Vector2(v._canvas_scale, v._canvas_scale))
assert_float(v._canvas_scale).override_failure_message(
"the Global fit scale must be a whole number of texture pixels"
+ " when it clears the coverage bar (D-255 texel-exactness)"
).is_equal_approx(roundf(v._canvas_scale), 0.001)
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
var effective_px_per_gridunit: float = v._canvas_scale * base_ratio
assert_float(effective_px_per_gridunit).is_equal_approx(20.0, 0.001)
## Fixed rungs must NEVER receive the Global fit multiplier — `_canvas.scale`
@@ -632,23 +686,23 @@ func test_resize_recenters_an_already_held_global_canvas() -> void:
).is_not_equal(offset_before)
## Small-canvas fallback (D-255's own escape hatch): once the legend column
## eats enough of the available width that the integer fit falls under
## FIT_MIN_COVERAGE_RATIO, the viewer must fall back to the fractional fit
## rather than settling for a sparse integer frame — end-to-end through the
## real viewer, mirroring test_fit_scale_falls_back_to_fractional_fit_on_
## excessive_letterboxing in test_step_canvas_transport.gd.
func test_global_canvas_uses_fractional_fit_when_legend_column_starves_the_integer_fit() -> void:
## Hoshe (review round 2): a narrow viewport where GJ1c's canvas exceeds
## the available width on the gridunit lattice itself (177 gridunits >
## available px after the legend column is reserved) used to make the OLD
## fractional-fit branch return a sub-1x scale (0.39x) — a real downscale
## below native resolution, violating "never below native". The integer
## px/gridunit ratio floors at 1 instead: `_canvas_scale` must never drop
## under 1.0, end-to-end through the real viewer wiring, not just the pure
## transport function this mirrors.
func test_global_canvas_scale_never_drops_below_native_on_a_narrow_viewport() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.size = Vector2(348.0 + StepCanvasTransport.LEGEND_COLUMN_PX, 1080.0)
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
# GJ1c at 1920x1080 with the legend column reserved: 2x no longer fits
# (1770 > 1628 available), 1x covers only ~41% of the tighter axis — well
# under the 75% coverage bar, so a non-integer fit must have been chosen.
assert_float(v._canvas_scale).override_failure_message(
"the reserved legend column must starve the 2x integer candidate at"
+ " this reference viewport, forcing the fractional fit"
).is_greater(1.0)
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
assert_float(v._canvas_scale * base_ratio).override_failure_message(
"the effective pixels-per-gridunit ratio must floor at 1 (native),"
+ " never a sub-1x downscale, even on a viewport this narrow"
).is_equal_approx(1.0, 0.001)