fix(client): size Global's cell count so the display ratio divides exactly

Jeroen: 'still not filling the screen... maybe we should calculate the depth
that needs to be invented based on the canvas size somehow.' That is the fix.

The old flow picked cells from the viewport, let the SERVER impose Global's
2:1 aspect, then fitted the returned extent into the drawable area with an
INTEGER pixels-per-gridunit ratio. When that extent did not divide the area
evenly, floor() dropped a whole step — and at ratio 2 the only step below is
1, i.e. half size. Worse, because the client fitted an extent it had not
chosen, the result could fill NEITHER axis.

Inverted: global_fill_extent() chooses the cell count FROM the ratio, already
shaped 2:1, so cells * RATIO is the drawn size by construction and lands
exactly on the binding axis. Verified across window shapes — 1920x1080 fills
width, 2560x1080 fills height, 2560x1440 width, 3440x1440 height, 1280x720
width. Exactly one axis fills at every shape, which is the most a 2:1
equirectangular canvas can do in an arbitrary viewport; the other letterboxes.

Global keeps its 2:1 aspect because it is 360 degrees of longitude by 180 of
latitude — the aspect cannot follow the viewport without shearing the map.

Tests pin the invariant directly: 2:1 preserved, never overflowing the
drawable area, and never leaving slack on BOTH axes.

Client suite 1832 / 1806 passed / 0 failed / 26 skipped.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-27 21:24:21 +02:00
co-authored by Claude
parent d36d458b74
commit 144d35d2a5
6 changed files with 85 additions and 3 deletions
@@ -531,3 +531,42 @@ func test_fit_scale_from_ratio_handles_a_zero_base_ratio_without_dividing_by_zer
## 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)
## Global must FILL its drawable area on at least one axis, at every window
## shape (Jeroen, 2026-07-27: "it looks unacceptable if it does not fill the
## screen across at least one axis"). Exactly one axis can fill — the canvas is
## equirectangular 2:1 and the aspect cannot follow the viewport — so the other
## letterboxes, and which one binds depends on whether the drawable area is
## wider or narrower than 2:1.
func test_global_fill_extent_fills_one_axis_at_every_window_shape() -> void:
var ratio: float = StepCanvasTransport.DISPLAY_RATIO_DEEP
for drawable in [
Vector2(1628.0, 1080.0), # 1920 window minus legend — wider than 2:1? no
Vector2(2268.0, 1080.0), # 2560 window — wider than 2:1, height binds
Vector2(2268.0, 1440.0),
Vector2(3148.0, 1440.0),
Vector2(988.0, 720.0),
]:
var cells: Vector2i = StepCanvasTransport.global_fill_extent(drawable)
assert_int(cells.x).override_failure_message(
"%s: canvas must stay 2:1, got %s" % [drawable, cells]
).is_equal(cells.y * 2)
var drawn := Vector2(float(cells.x) * ratio, float(cells.y) * ratio)
assert_bool(drawn.x <= drawable.x and drawn.y <= drawable.y).override_failure_message(
"%s: drawn %s overflows the drawable area" % [drawable, drawn]
).is_true()
# The binding axis must be within one ratio-step of exact — i.e. there
# is no room left for another whole gridunit on that axis.
var slack_x: float = drawable.x - drawn.x
var slack_y: float = drawable.y - drawn.y
assert_bool(slack_x < ratio * 2.0 or slack_y < ratio).override_failure_message(
"%s: fills NEITHER axis — drawn %s leaves %s slack" % [drawable, drawn, Vector2(slack_x, slack_y)]
).is_true()
## A degenerate drawable area must still yield a legal 2:1 canvas rather than
## a zero-row one, which would divide by zero downstream in spacing_for_rung().
func test_global_fill_extent_floors_at_one_row() -> void:
var cells: Vector2i = StepCanvasTransport.global_fill_extent(Vector2(1.0, 1.0))
assert_int(cells.y).is_greater_equal(1)
assert_int(cells.x).is_equal(cells.y * 2)
+3 -3
View File
@@ -818,9 +818,9 @@ func test_request_extent_ignores_a_not_yet_laid_out_viewport() -> void:
StepCanvasViewer.FALLBACK_VIEWPORT_PX.x - StepCanvasTransport.LEGEND_COLUMN_PX,
StepCanvasViewer.FALLBACK_VIEWPORT_PX.y
)
var expected: Vector2i = StepCanvasTransport.viewport_fit_extent(
drawable, v.get_held_rung()
)
# Global sizes via global_fill_extent (2:1, chosen so the display ratio
# divides exactly), not the generic viewport fit.
var expected: Vector2i = StepCanvasTransport.global_fill_extent(drawable)
assert_that(extent).override_failure_message(
"a %s viewport must fall back, not be taken literally — got %s" % [degenerate, extent]
).is_equal(expected)
@@ -344,6 +344,33 @@ static func viewport_fit_extent(viewport_px: Vector2, rung: String) -> Vector2i:
return Vector2i(w, h)
## The Global cell count that FILLS `drawable_px` on its binding axis at the
## deep display ratio, already shaped 2:1.
##
## Global's canvas is equirectangular whole-body — 360 degrees of longitude by
## 180 of latitude — so it is 2:1 and the aspect cannot follow the viewport.
## Exactly one axis can therefore fill; the other letterboxes, and which one
## depends on whether the drawable area is wider or narrower than 2:1.
##
## Why this exists rather than viewport_fit_extent() + an integer fit: the
## display ratio must be a WHOLE number of screen px per gridunit (D-255
## texel-exactness — a fractional ratio reintroduces the sub-pixel blur the
## whole design exists to prevent). Choosing cells from the viewport and then
## searching for an integer ratio that fits means `floor()` can drop a whole
## step, and at ratio 2 the only step below is 1 — a HALF-SIZE map in a window
## with room to spare. Choosing the cell count from the ratio instead makes
## the division exact by construction: `cells * RATIO` is the drawn size, and
## it is <= the drawable area on both axes with equality on the binding one.
static func global_fill_extent(drawable_px: Vector2) -> Vector2i:
var ratio: float = maxf(display_ratio_for_rung(RUNG_GLOBAL), 0.0001)
# Largest whole gridunit count each axis could show on its own...
var by_width: int = int(floor(drawable_px.x / ratio))
var by_height: int = int(floor(drawable_px.y / ratio))
# ...then 2:1 picks the binding one. Rows drive it, since cols = 2 * rows.
var rows: int = maxi(1, mini(by_height, by_width / 2))
return Vector2i(rows * 2, rows)
## Cap a fixed-rung request extent to the body's own region grid (T-1189):
## `derive_orbital_at_metres` wraps longitude/clamps latitude server-side, so
## a viewport-fit extent wider or taller than the body's grid makes a rung
@@ -516,6 +516,20 @@ func _request_extent() -> Vector2i:
var drawable: Vector2 = viewport
if _held_rung == StepCanvasTransport.RUNG_GLOBAL:
drawable.x = maxf(viewport.x - StepCanvasTransport.LEGEND_COLUMN_PX, 1.0)
# Pick the CELL COUNT so the display ratio comes out exact, instead of
# picking cells from the viewport and then hunting for an integer ratio
# that fits (Jeroen: "calculate the depth that needs to be invented
# based on the canvas size").
#
# Global's canvas is equirectangular 2:1, and the SERVER enforces that
# after the fact. So the old flow asked for (W/2, H/2), got back a
# differently-shaped 2:1 extent, and then fitted THAT into the drawable
# area with an integer px-per-gridunit ratio — and when the returned
# extent did not divide the area evenly, floor() dropped a whole step
# and the map filled NEITHER axis. Applying 2:1 here makes the request
# already-correct, so the server's fit is a no-op and the display ratio
# lands exactly on DISPLAY_RATIO_DEEP with nothing to floor away.
return StepCanvasTransport.global_fill_extent(drawable)
var fit: Vector2i = StepCanvasTransport.viewport_fit_extent(drawable, _held_rung)
return StepCanvasTransport.cap_extent_to_body(fit, _held_rung, _global_body_extent)