feat(client): size the Global rung to the viewport, not the region grid

Global took its cell counts from `global_cell_counts()` — one gridunit per
region — and discarded the requested extent entirely. On GJ380c that produced
a 191x95 canvas built from a heightmap stored at 512x256: roughly seven times
the available cells thrown away before anything was drawn. The count also
shrank as REGION_M grew, so tuning the scale ladder silently degraded the
opener, which is why the top of the ladder got worse rather than better as
the ladder itself was refined.

Global now fits the largest 2:1 canvas inside the requested extent. It cannot
take its ASPECT from the viewport — the canvas is equirectangular whole-body,
360 degrees of longitude by 180 of latitude, and must stay 2:1 or the cells
stop being square and the map shears — so the existing letterbox absorbs the
remainder. A hostile extent is still clamped; sizing to the request is not
trusting the request.

Global also joins the deep display ratio, making the band uniform. At 5 px
per gridunit a 1920 px window asked for 384 cells across a body whose
heightmap holds 512x256 — discarding stored detail to save work already done.
At 2 px it asks for 960, which is heightmap-native: nothing thrown away,
nothing invented, and the same screen area filled either way.

A body with no radius is not a sphere (asteroid belt, oort cloud) and has no
equirectangular surface to fit. Those degrade to the region grid — a visibly
degenerate 1x1 canvas — rather than a plausible-looking lie at whatever size
the viewport happened to ask for.

project.yaml 0.4.3 -> 0.4.4 invalidates the persisted step-canvas disk cache.
District/Quarter/Block/Chunk kept identical 960x540 cell counts through the
extent inversion, so their cache keys are byte-identical while a District
canvas now covers 3.6 km of ground instead of 1,966 km — a warm cache would
silently serve pre-inversion canvases.

Global still rides the orbital derive, so it carries no courses yet; that is
the next step and is deliberately separate, being a cost question over the
whole body rather than a sizing one.

Pair session with Jeroen, 2026-07-26.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-26 22:45:40 +02:00
co-authored by Claude
parent 0a0419abcc
commit 9c8fcc2f95
4 changed files with 124 additions and 47 deletions
+20 -14
View File
@@ -128,7 +128,8 @@ func test_zero_extent_does_not_divide_by_zero() -> void:
# =============================================================================
# Display ratio — deep/mid 1x1, shallow ~5x5, PRESENTATION only (D-255(a))
# Display ratio — uniform 2x2 since the extent inversion, PRESENTATION only
# (D-255(a): never touches a cache key or a wire request)
# =============================================================================
@@ -149,14 +150,17 @@ func test_display_ratio_deep_rungs_all_share_the_deep_ratio() -> void:
assert_float(deep).is_greater(0.0)
## Only Global keeps the shallow fallback now — Region joined the deep band
## with the extent inversion (it is a real 262x466 km map, not an orbital
## envelope).
func test_only_global_uses_the_shallow_display_ratio() -> void:
assert_float(StepCanvasTransport.display_ratio_for_rung("Global")).is_equal_approx(5.0, 0.001)
assert_float(StepCanvasTransport.display_ratio_for_rung("Region")).is_equal_approx(
StepCanvasTransport.DISPLAY_RATIO_DEEP, 0.001
)
## Every rung reads at the deep ratio since the extent inversion — Region
## because it is a real 262x466 km map rather than an orbital envelope, and
## Global because 5 px/gridunit asked for fewer cells than the heightmap
## already stores. Asserted as a relationship, not a literal, so the ratio
## stays a tunable.
func test_every_rung_uses_the_deep_display_ratio() -> void:
var deep: float = StepCanvasTransport.DISPLAY_RATIO_DEEP
for rung in ["Global", "Region", "District", "Quarter", "Block", "Chunk"]:
assert_float(StepCanvasTransport.display_ratio_for_rung(rung)).override_failure_message(
"%s must read at the deep ratio" % rung
).is_equal_approx(deep, 0.001)
## Region LEFT the orbital set with the extent inversion — it now rides the
@@ -187,9 +191,9 @@ func test_viewport_fit_extent_at_deep_ratio_divides_by_the_deep_ratio() -> void:
assert_that(extent).is_equal(expected)
func test_viewport_fit_extent_at_shallow_ratio_divides_by_the_display_ratio() -> void:
func test_viewport_fit_extent_at_global_divides_by_the_display_ratio() -> void:
var extent: Vector2i = StepCanvasTransport.viewport_fit_extent(Vector2(1000.0, 500.0), "Global")
assert_that(extent).is_equal(Vector2i(200, 100))
assert_that(extent).is_equal(Vector2i(500, 250))
func test_viewport_fit_extent_clamps_to_the_fixed_canvas_max_axis() -> void:
@@ -289,7 +293,7 @@ func test_world_to_local_and_back_round_trips() -> void:
func test_canvas_footprint_px_is_extent_times_display_ratio() -> void:
var footprint: Vector2 = StepCanvasTransport.canvas_footprint_px("Global", Vector2i(100, 50))
assert_that(footprint).is_equal(Vector2(500.0, 250.0)) # 5x5 shallow ratio
assert_that(footprint).is_equal(Vector2(200.0, 100.0)) # 2x2 deep ratio
## Half-extent is now half the rung's own CELL on the short axis, whatever
@@ -329,8 +333,10 @@ func test_every_rung_is_live_on_the_smallest_populated_body() -> void:
## ...but a rung whose cell exceeds the body is not a step down the ladder at
## all — it would zoom OUT. systems.db has two such rocks (6 km and 11 km
## radius, both uninhabited).
## all — it would zoom OUT. The only two such bodies in systems.db are Phobos
## (11 km) and Deimos (6 km), and Sol is out of generator bounds entirely, so
## this guard never fires on in-scope content. Kept because it is free and the
## failure it prevents is silent.
func test_region_is_not_live_on_a_sub_region_body() -> void:
assert_bool(StepCanvasTransport.is_rung_live_on_body("Region", 6.0)).is_false()
assert_bool(StepCanvasTransport.is_rung_live_on_body("District", 6.0)).is_true()
@@ -85,15 +85,25 @@ const DISPLAY_RATIO_SHALLOW: float = 5.0
## (D-255(a): "the deep, ground-level steps where the player is closest to
## visible detail"). Chunk's own "1 screen px per 64 m gridunit, no
## magnification margin" bottom-out rule (D-255(a)) is exactly DISPLAY_RATIO_DEEP.
## D-255 amendment 2026-07-26: **Region moved to the deep ratio.** The shallow
## fallback earned its keep only while a Region canvas was an orbital-scale
## picture where extent, not per-cell fidelity, was what grew. Post-inversion
## Region spans 262x466 km — a provincial map with real terrain in it — so it
## takes the same crispness as every rung below it. Jeroen's brief for this
## session in one line: "the global and region maps need to look the same as
## the smaller level maps, but with obviously a different zoom level."
## D-255 amendment 2026-07-26: **every rung now reads at the deep ratio**, so
## this table is uniform and DISPLAY_RATIO_SHALLOW is currently unreferenced
## (kept, not deleted — pending Jeroen's call, and it documents the retired
## band). Jeroen's brief for this session in one line: "the global and region
## maps need to look the same as the smaller level maps, but with obviously a
## different zoom level."
##
## Region: the shallow fallback earned its keep only while a Region canvas was
## an orbital-scale picture where extent, not per-cell fidelity, was what grew.
## Post-inversion it spans 262x466 km — a provincial map with real terrain in
## it — so it takes the same crispness as every rung below.
##
## Global: at 5 px/gridunit a 1920-px window asked for 384 cells across a body
## whose heightmap is stored at 512x256 — discarding stored detail to save
## work it had already done. At 2 px/gridunit it asks for 960, which is
## heightmap-native: nothing thrown away, nothing invented. Both ratios fill
## the same screen area; only the sampling differs.
const DISPLAY_RATIO_BY_RUNG: Dictionary = {
RUNG_GLOBAL: DISPLAY_RATIO_SHALLOW,
RUNG_GLOBAL: DISPLAY_RATIO_DEEP,
RUNG_REGION: DISPLAY_RATIO_DEEP,
RUNG_DISTRICT: DISPLAY_RATIO_DEEP,
RUNG_QUARTER: DISPLAY_RATIO_DEEP,