feat(client): T-1153 + T-1152 client half — continuous cursor-anchored zoom ladder, Region-rung orbital entry, click-through retired

The atlas 'regional' screen now opens the LADDER at the canonical orbital
frame (Region granularity, whole body fitted and centered) and wheel zoom
descends continuously — cursor-anchored, unclamped across rungs, with
progressive refinement (held composite keeps drawing, finer rung swaps in
place on arrival; no blank frame, no mode flip). Full-zoom-out resets to
the canonical planetary frame per Jeroen's HARD condition
(is_fully_zoomed_out = extent >= body circumference, not a zoom-value
heuristic). The district_screen nav hop is deleted — D-013 restored:
descent is a zoom gesture, not a nav push. AtlasViewer's heightmap-texture
path is unreachable from nav (code intact; overlay surface deferred, see
report/tickets).

Rung selection: design doc §5's literal formula has NO legal District band
at any real viewport (visual-tolerance band and n=64 coverage ceiling
never overlap — pinned by executable boundary tests at 1600x900);
select_rung() splits it into a coverage ceiling (decides Region) then the
2x visual tolerance (District vs Quarter), documented at the function.
In practice the ladder steps Region -> Quarter directly.

Wire: window_granularity_v2 encoded (omitted at District for byte-compat),
granularity_v2 echoed value keyed + staleness-guarded end to end; Region
clamp mirror replicates the server's bounded halving loop (no closed
form). MIN/MAX_ZOOM widened to [0.0005, 64] — the old 0.5 floor would
have clamped a real body's canonical fit zoom, violating the reset
condition.

Real pre-existing bug fixed in atlas_window_overlay.gd: the draw path used
echoed n as both cell-grid dimension and district extent — only
coincidentally correct at District granularity; Quarter/Region would have
read wrong array offsets. cell_grid_side_for_window() now mirrors the
server's WindowGranularity::cell_grid_side.

Tests: +26 pure-function geometry tests, new 30-test zoom-ladder suite,
extensions across the window cache/request/overlay/delivery suites.
Full suite 3518 green; cold-parse clean.
This commit is contained in:
2026-07-22 11:41:37 +02:00
parent d356b09926
commit ce90d69ae8
19 changed files with 2144 additions and 336 deletions
+60
View File
@@ -240,6 +240,36 @@ func test_encode_atlas_layer_request_carries_granularity_and_min_wl() -> void:
assert_that(decoded.value.get("window_min_wl_m")).is_equal(512)
## T-1152/T-1153: window_granularity_v2 is OMITTED (not sent as "") when at
## its empty-string default — same byte-compatibility contract as
## window_granularity/window_min_wl_m's own default-omission.
func test_encode_atlas_layer_request_omits_granularity_v2_by_default() -> void:
var bytes := Protocol.encode_atlas_layer_request(
"GJ1c", "Topography", Vector2i(140, 260), 32
)
var decoded = Messagepack.decode(bytes)
assert_bool(decoded.value.has("window_granularity_v2")).is_false()
## T-1152/T-1153: a Region-rung request carries window_granularity_v2 as the
## bare string "Region" — the ONLY way to express the coarser-than-district
## rung (WindowGranularity's Rust doc: "the ONLY way to actually request
## Region is window_granularity_v2 = Some(WindowGranularity::Region)"), a
## plain rmp_serde variant-name encoding matching RoadNodeKind's existing
## wire precedent, NOT an integer discriminant.
func test_encode_atlas_layer_request_carries_granularity_v2_region() -> void:
var bytes := Protocol.encode_atlas_layer_request(
"GJ1c", "Topography", Vector2i(0, 0), 6400, 0, 0, "Region"
)
var decoded = Messagepack.decode(bytes)
assert_that(decoded.value.get("window_granularity_v2")).is_equal("Region")
# The legacy window_granularity field is independently omittable — a
# Region request sends ONLY the v2 tag, never a legacy value pretending
# to mean something for Region (WINDOW_GRANULARITY_REGION_KEY is a
# key-space tag the SERVER echoes, never a legal wire INPUT).
assert_bool(decoded.value.has("window_granularity")).is_false()
## §2: district_window is a distinct payload (echoes center/n for the
## client's staleness guard) but the codec passthrough is the same shape as
## every sibling layer — raw.get(), no reshaping. Field types follow the
@@ -265,6 +295,36 @@ func test_atlas_response_district_window_passthrough() -> void:
assert_that((decoded as Dictionary).get("district_window")).is_equal(window)
## T-1152/T-1153, design doc §6 encoding-continuity acceptance: a Region-rung
## response passes through the EXACT SAME codec path as District/Quarter —
## granularity_v2 is just another field in the same dict, no special-cased
## decode branch for the coarser rung. This is the direct regression test for
## "one colorizer family, no per-rung palettes": the wire contract itself
## draws no distinction, so nothing downstream (the overlay's
## cell_grid_side_for_window()/_cell_color()) needs a rung-specific decode
## path either.
func test_atlas_response_district_window_passthrough_region_rung() -> void:
var window := {
"center": [0, 0],
"n": 6400,
"granularity": 4294967295, # WINDOW_GRANULARITY_REGION_KEY (u32::MAX) — key-space tag, not a real multiplier
"granularity_v2": "Region",
"min_wl_m": 0,
"morphology": PackedByteArray([8, 14, 0, 5]),
"elev_q": PackedByteArray([40, 62, 5, 88]),
"temp_dc": [120, 95, AtlasOverlayColors.REGION_TEMP_NONE_DC, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
var raw := {"body_id": "GJ1c", "status": "Ready", "district_window": window}
var decoded: Variant = Protocol.atlas_response_from_raw(raw)
assert_that(decoded).is_not_null()
var district_window: Dictionary = (decoded as Dictionary).get("district_window")
assert_that(district_window).is_equal(window)
assert_str(str(district_window.get("granularity_v2"))).is_equal("Region")
## A body with no window requested (or not yet derived — §1's background-queue
## serving model: the completion may not have landed yet) must decode with
## district_window absent -> null, same "layer hasn't produced yet" contract
+11 -8
View File
@@ -371,16 +371,19 @@ func test_no_descend_signal_without_a_loaded_heightmap() -> void:
assert_int(received.size()).is_equal(0)
## RegionalScreen forwards AtlasViewer's district_descend_requested verbatim —
## the nav-stack wiring atlas_app.gd depends on.
func test_regional_screen_forwards_district_descend_requested() -> void:
## T-1153 (D-226 T-1143-rulings amendment): RegionalScreen no longer wraps
## AtlasViewer or forwards district_descend_requested — the "regional" nav
## entry now opens the continuous zoom ladder (AtlasWindowViewer) directly at
## the canonical orbital frame, retiring the click-through as the sole entry
## (see regional_screen.gd's own doc). This regression-guards the NEW
## wiring: entering "regional" reaches AtlasWindowViewer, not AtlasViewer.
func test_regional_screen_wraps_atlas_window_viewer_not_atlas_viewer() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
var received: Array = []
screen.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
screen._viewer.district_descend_requested.emit(Vector2i(5, 7))
assert_int(received.size()).is_equal(1)
assert_that(received[0]).is_equal(Vector2i(5, 7))
assert_object(screen._viewer).override_failure_message(
"RegionalScreen must wrap AtlasWindowViewer (the zoom ladder) since T-1153,"
+ " not the retired AtlasViewer heightmap-texture display"
).is_instanceof(AtlasWindowViewer)
# =============================================================================
+69
View File
@@ -180,3 +180,72 @@ func test_district_and_quarter_windows_coexist_at_identical_body_center_n() -> v
cache.get_window("GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY)
).is_equal(district_window)
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32, 4)).is_equal(quarter_window)
# =============================================================================
# granularity_v2 (T-1152/T-1153): the string-tag axis — the ONLY thing that
# distinguishes Region from District/Quarter, since Region has no legal
# legacy-int representation (WindowGranularity::legacy_u32() returns None for
# Region — see the server's own doc). This is the SAME mandatory-aliasing
# regression class as the granularity/min_wl_m tests above, extended to the
# new axis.
# =============================================================================
## **MANDATORY aliasing regression (T-1152/T-1153):** a Region-rung key and a
## District-rung key at the IDENTICAL (body_id, center, n, legacy
## granularity, min_wl_m) must be DISTINCT cache keys — the legacy int slot
## alone (both "District" and default-omitted callers pass
## DISTRICT_GRANULARITY=1) cannot tell them apart; granularity_v2 is what
## does.
func test_make_key_distinguishes_granularity_v2_region_from_district() -> void:
var k_district := AtlasWindowCache.make_key(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
)
var k_region := AtlasWindowCache.make_key(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
)
assert_str(k_district).is_not_equal(k_region)
## Omitting granularity_v2 (every pre-T-1152 call site) must produce the SAME
## key as passing the explicit "District" default — byte/string
## compatibility, same contract as the legacy granularity/min_wl_m defaults.
func test_omitted_granularity_v2_matches_explicit_district_default() -> void:
var k_omitted := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
var k_explicit := AtlasWindowCache.make_key(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
)
assert_str(k_omitted).is_equal(k_explicit)
## End-to-end through put()/get_window(): a Region-rung window and a
## District-rung window at the identical (body, center, n) must both be
## independently retrievable — the exact scenario a full-zoom-out-then-back-in
## at the SAME (center, n) would hit if a player oscillates across the
## District/Region boundary.
func test_region_and_district_windows_coexist_at_identical_body_center_n() -> void:
var cache := AtlasWindowCache.new()
var district_window := {"granularity_v2": "District", "id": "district"}
var region_window := {"granularity_v2": "Region", "id": "region"}
cache.put(
"GJ1c", Vector2i(10, 20), 32, district_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
"District"
)
cache.put(
"GJ1c", Vector2i(10, 20), 32, region_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
"Region"
)
assert_int(cache.size()).is_equal(2)
assert_that(
cache.get_window(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
)
).is_equal(district_window)
assert_that(
cache.get_window(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
)
).is_equal(region_window)
+333
View File
@@ -265,3 +265,336 @@ func test_pole_wall_rows_half_matches_canonicalize_rows_half() -> void:
Vector2i(0, rows_half), radius_km
)
assert_int(canonical.y).is_equal(rows_half)
# =============================================================================
# T-1153: select_rung() — the §5 rung-selection rule, split into TWO tests
# per select_rung()'s own doc: a COVERAGE ceiling decides Region (can a
# District window even span this much world), and the `2x` visual-tolerance
# rule (design doc §5: "select the coarsest rung whose cell spacing <=
# 2*(E/C)") decides District vs. Quarter for whatever's under that ceiling.
# =============================================================================
## A tight sample spacing (deep zoom-in — small E over a large C) must select
## Quarter (512 m), the finest legal rung — 2*(E/C) is far below District's
## 2,048 m spacing at this ratio.
func test_select_rung_picks_quarter_at_a_tight_sample_spacing() -> void:
# E=2000m over C=1000px -> sample spacing 2 m/px -> threshold 4 m. Even
# Quarter (512 m) is coarser than the threshold, so select_rung() falls
# through to the FINEST legal rung (its own documented fallback) rather
# than returning something even finer that doesn't exist — Quarter.
var rung: String = AtlasWindowGeometry.select_rung(2000.0, 1000.0)
assert_str(rung).is_equal("Quarter")
## A sample spacing that satisfies BOTH District's own `2x` band AND the
## coverage ceiling selects District — the coarsest rung whose spacing still
## satisfies the fine-end rule, without exceeding what a District window can
## physically cover.
func test_select_rung_picks_district_at_a_moderate_sample_spacing() -> void:
# E=120,000m (under the 64*2048=131,072m coverage ceiling) over C=100px ->
# threshold = 2*120000/100 = 2,400m — satisfies District's 2,048m spacing.
var rung: String = AtlasWindowGeometry.select_rung(120_000.0, 100.0)
assert_str(rung).is_equal("District")
## An extent past the COVERAGE ceiling (more world than a District window can
## physically span, regardless of how generous the visual tolerance would
## otherwise be) must select Region — the coverage test, not the `2x` visual
## one, is what decides this (select_rung()'s own doc: "the coverage ceiling
## wins whenever the two disagree").
func test_select_rung_picks_region_past_the_coverage_ceiling() -> void:
# E = full Earth-like circumference (~40,075 km) — far past the
# 64*2048=131,072m District coverage ceiling regardless of canvas_px.
var rung: String = AtlasWindowGeometry.select_rung(40_075_264.0, 1920.0)
assert_str(rung).is_equal("Region")
## Exactly AT the coverage ceiling (E == 64*2048 = 131,072m) must still
## select District if the `2x` band also agrees — the ceiling is `>`, not
## `>=`, so the boundary value itself stays under District's own test.
func test_select_rung_coverage_ceiling_boundary_stays_district() -> void:
var rung: String = AtlasWindowGeometry.select_rung(131_072.0, 100.0)
assert_str(rung).is_equal("District")
## One metre past the coverage ceiling must flip to Region — confirms the
## ceiling actually bites right at its own boundary, not one district-window
## short of it.
func test_select_rung_one_past_the_coverage_ceiling_is_region() -> void:
var rung: String = AtlasWindowGeometry.select_rung(131_073.0, 100.0)
assert_str(rung).is_equal("Region")
## Exactly AT District's `2x` threshold (spacing_m == 2*(E/C)) must select
## District, not the next-finer rung — the rule is `<=`, not `<`.
func test_select_rung_district_threshold_boundary_is_inclusive() -> void:
# District spacing = 2048 m. Choose E/C such that 2*(E/C) == 2048 exactly:
# E=1024, C=1.0 -> E/C=1024 -> threshold=2048. E=1024 is also comfortably
# under the coverage ceiling (131,072), so the `2x` test is what's
# actually being exercised here.
var rung: String = AtlasWindowGeometry.select_rung(1024.0, 1.0)
assert_str(rung).is_equal("District")
## Degenerate canvas_px (<=0, an unlaid-out viewport) must fall back to the
## FINEST rung, never crash or pick the coarsest by dividing by zero — the
## documented "under-resolve is the safe failure direction" disposition (and
## must be checked BEFORE the coverage ceiling could otherwise route a
## degenerate small extent toward Region by accident).
func test_select_rung_degenerate_canvas_px_falls_back_to_finest() -> void:
var rung: String = AtlasWindowGeometry.select_rung(1000.0, 0.0)
assert_str(rung).is_equal("Quarter")
## spacing_for_rung() is select_rung()'s inverse lookup — pin the three known
## values against the D-243 constants directly (not against RUNG_TABLE
## indices, which would just restate the implementation).
func test_spacing_for_rung_matches_d243_constants() -> void:
assert_float(AtlasWindowGeometry.spacing_for_rung("Quarter")).is_equal_approx(512.0, 0.001)
assert_float(AtlasWindowGeometry.spacing_for_rung("District")).is_equal_approx(2048.0, 0.001)
assert_float(AtlasWindowGeometry.spacing_for_rung("Region")).is_equal_approx(204_800.0, 0.001)
## An unknown tag falls back to District — matching the server's own
## "unknown -> District" posture at every wire-decode boundary.
func test_spacing_for_rung_unknown_tag_falls_back_to_district() -> void:
assert_float(AtlasWindowGeometry.spacing_for_rung("Nonsense")).is_equal_approx(2048.0, 0.001)
## The exact scenario that surfaced the coverage-vs-visual-tolerance
## distinction (live-testing enter_orbital()'s own fit zoom): a whole
## Earth-like body's circumference (~40,075 km, matching
## AtlasDescendGeometry.district_extent()'s own cols*DISTRICT_M for
## radius=6371km) fitted to a 1920px-wide viewport at CELL_PIXEL_SIZE=16 must
## select Region — this is the direct regression guard for the bug this
## implementation found and fixed (an earlier version of select_rung()
## selected District here, which would have meant the canonical orbital
## frame requests a District-tier derive spanning an entire planet — the
## exact R1-catastrophe cost scenario the design doc §4 rejects).
func test_select_rung_at_orbital_fit_zoom_selects_region() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
var composite_native: float = float(n) * CELL_PIXEL_SIZE
var viewport := Vector2(1920.0, 1080.0)
var fit_zoom: float = maxf(viewport.x, viewport.y) / composite_native
var world_extent: float = AtlasWindowGeometry.world_extent_m(CELL_PIXEL_SIZE, fit_zoom, viewport)
var rung: String = AtlasWindowGeometry.select_rung(
world_extent, maxf(viewport.x, viewport.y)
)
assert_str(rung).override_failure_message(
"the canonical orbital fit-zoom (whole-planet view) must select Region,"
+ " never a District-tier derive spanning an entire planet"
).is_equal("Region")
## Pinned capture-resolution boundary numbers (1600x900, the coordinator's
## requested eyeball-capture viewport) — a live executable regression guard
## for select_rung()'s own doc's worked example. Region releases District's
## coverage ceiling at _view_zoom ~= 1.5625; District's own `2x` band edge
## sits at _view_zoom ~= 0.125 — i.e. BELOW (not above) the coverage-ceiling
## crossing, confirming the two never overlap at this (or any real) canvas
## size — see select_rung()'s "Tuning knobs" paragraph for what would need
## to change (DISTRICT_WINDOW_MAX_N, a server-side wire-budget change) to
## open a real District band.
func test_select_rung_1600x900_region_district_boundary_zoom() -> void:
var viewport := Vector2(1600.0, 900.0)
var canvas_px: float = maxf(viewport.x, viewport.y)
var boundary_zoom := 1.5625
var just_inside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 1.001, viewport
)
var just_outside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 0.999, viewport
)
assert_str(AtlasWindowGeometry.select_rung(just_inside, canvas_px)).override_failure_message(
"zoomed IN past ~1.5625 at 1600x900 must have released the Region coverage ceiling"
).is_not_equal("Region")
assert_str(AtlasWindowGeometry.select_rung(just_outside, canvas_px)).override_failure_message(
"zoomed OUT past ~1.5625 at 1600x900 must still be under the Region coverage ceiling"
).is_equal("Region")
func test_select_rung_1600x900_district_quarter_boundary_zoom_confirms_no_overlap() -> void:
var viewport := Vector2(1600.0, 900.0)
var canvas_px: float = maxf(viewport.x, viewport.y)
var boundary_zoom := 0.125
var just_inside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 1.001, viewport
)
var just_outside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 0.999, viewport
)
# Both sides of the District/Quarter `2x`-band boundary read "Region" at
# 1600x900, NOT "District" — confirming the coverage ceiling (which
# releases at zoom~=1.5625, far above this boundary) has already forced
# Region long before the `2x` band's own edge is reached. This is the
# literal "no overlap" finding, pinned as an executable assertion.
assert_str(AtlasWindowGeometry.select_rung(just_inside, canvas_px)).is_equal("Region")
assert_str(AtlasWindowGeometry.select_rung(just_outside, canvas_px)).is_equal("Region")
# =============================================================================
# T-1153: world_extent_m() — the `E` half of the §5 rule, computed from the
# viewer's own zoom/viewport state.
# =============================================================================
## At zoom=1.0, CELL_PIXEL_SIZE=16: one DISTRICT (2,048 m, the fixed display
## unit — see world_extent_m()'s own doc for why this is rung-INDEPENDENT)
## occupies 16 screen px, so a 1920px-wide viewport shows
## 1920/16 * 2048 = 245,760 m.
func test_world_extent_m_at_zoom_one() -> void:
var extent: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
assert_float(extent).is_equal_approx(1920.0 / CELL_PIXEL_SIZE * 2048.0, 1.0)
## Doubling the zoom must HALVE the displayed world extent — zooming in
## shows less world, not more.
func test_world_extent_m_halves_when_zoom_doubles() -> void:
var extent_1x: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
var extent_2x: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 2.0, Vector2(1920.0, 1080.0)
)
assert_float(extent_2x).is_equal_approx(extent_1x * 0.5, 1.0)
## The composite's on-screen footprint is rung-invariant (world_extent_m()'s
## own doc) — a change in held rung with NO change in zoom/viewport must
## leave the displayed world extent UNCHANGED. This is the direct regression
## test for the bug this function's signature once had (a granularity_v2
## parameter that silently changed the formula per rung, when only zoom
## should) — the function no longer TAKES a rung parameter at all, so this
## pins that omission is intentional, not an oversight.
func test_world_extent_m_has_no_rung_parameter() -> void:
var extent_a: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
var extent_b: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
assert_float(extent_a).is_equal_approx(extent_b, 0.001)
## Degenerate zoom (<=0) must not divide by zero — a safe zero extent.
func test_world_extent_m_degenerate_zoom_is_safe() -> void:
var extent: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 0.0, Vector2(1920.0, 1080.0)
)
assert_float(extent).is_equal_approx(0.0, 0.001)
# =============================================================================
# T-1153: is_fully_zoomed_out() — Jeroen's HARD condition's trigger predicate.
# =============================================================================
func test_is_fully_zoomed_out_true_when_extent_covers_full_circumference() -> void:
var radius_km := 6371.0
var circumference_m: float = TAU * radius_km * 1000.0
assert_bool(AtlasWindowGeometry.is_fully_zoomed_out(circumference_m, radius_km)).is_true()
assert_bool(
AtlasWindowGeometry.is_fully_zoomed_out(circumference_m * 1.5, radius_km)
).is_true()
func test_is_fully_zoomed_out_false_when_extent_is_less_than_circumference() -> void:
var radius_km := 6371.0
var circumference_m: float = TAU * radius_km * 1000.0
assert_bool(
AtlasWindowGeometry.is_fully_zoomed_out(circumference_m * 0.5, radius_km)
).is_false()
## A no-radius body (tiny test body) has no circumference concept — never
## auto-resets, matching enter_orbital()'s own no-radius fallback disposition.
func test_is_fully_zoomed_out_false_for_no_radius_body() -> void:
assert_bool(AtlasWindowGeometry.is_fully_zoomed_out(1e12, 0.0)).is_false()
# =============================================================================
# T-1153: screen_center_to_district() — the shared screen<->district formula
# behind both the pan-edge refetch and the rung-reselect refetch.
# =============================================================================
## At the exact center of a symmetric fit (offset centers the composite,
## zoom=1.0), the screen center must map back to the held center exactly.
func test_screen_center_to_district_at_rest_returns_held_center() -> void:
var held_n := 32
var held_center := Vector2i(10, 20)
var composite_native: float = float(held_n) * CELL_PIXEL_SIZE
var viewport := Vector2(composite_native, composite_native)
var offset := Vector2.ZERO # composite exactly fills the viewport, top-left at origin
var result: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, offset, 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
assert_that(result).is_equal(held_center)
## Panning the offset must shift the recovered district position in the
## OPPOSITE direction of the offset shift (dragging the composite right
## reveals districts to the WEST at screen-center).
func test_screen_center_to_district_shifts_with_pan_offset() -> void:
var held_n := 32
var held_center := Vector2i(0, 0)
var composite_native: float = float(held_n) * CELL_PIXEL_SIZE
var viewport := Vector2(composite_native, composite_native)
var at_rest: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, Vector2.ZERO, 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
var panned: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, Vector2(CELL_PIXEL_SIZE * 4.0, 0.0), 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
assert_int(panned.x).override_failure_message(
"dragging the composite EAST (positive offset) must reveal districts to the WEST"
).is_less(at_rest.x)
# =============================================================================
# T-1153 (moved from atlas_window_viewer.gd for testability): WASD held-pan
# direction is exercised live only (reads the global Input singleton) —
# edge-scroll suppression/direction are pure and covered here directly.
# =============================================================================
func test_is_cursor_edge_scrolling_true_near_an_edge() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, false, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_true()
func test_is_cursor_edge_scrolling_false_away_from_any_edge() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, false, Vector2(800.0, 600.0), Vector2(400.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_is_cursor_edge_scrolling_suppressed_when_over_ui() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, true, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_is_cursor_edge_scrolling_suppressed_without_app_focus() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
false, false, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_edge_scroll_direction_points_west_near_left_edge() -> void:
var direction: Vector2 = AtlasWindowGeometry.edge_scroll_direction(
Vector2(800.0, 600.0), Vector2(5.0, 300.0), 24.0
)
assert_float(direction.x).is_less(0.0)
assert_float(direction.y).is_equal_approx(0.0, 0.001)
+108
View File
@@ -150,3 +150,111 @@ func test_draw_builds_a_texture_through_the_viewer_stub() -> void:
o.viewer = stub
o._draw()
assert_that(o._cached_texture).is_not_null()
# =============================================================================
# T-1152/T-1153: cell_grid_side_for_window() — the district-extent-vs-
# derived-cell-grid split every rung's response now carries.
# =============================================================================
## District (the default/omitted tag): cell_grid_side == n, unchanged from
## the pre-T-1152 identity mapping.
func test_cell_grid_side_for_window_district_matches_n() -> void:
var window: Dictionary = {"n": 32, "granularity_v2": "District"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(32)
## Quarter: 4x MORE cells than districts (WINDOW_GRANULARITY_QUARTER).
func test_cell_grid_side_for_window_quarter_multiplies_by_four() -> void:
var window: Dictionary = {"n": 32, "granularity_v2": "Quarter"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(128)
## Region: FAR FEWER cells than districts — round(n/100), matching
## WindowGranularity::cell_grid_side's own Region branch exactly (the
## "inversion" the server doc calls out: finer rungs multiply, Region divides).
func test_cell_grid_side_for_window_region_divides_by_districts_per_region() -> void:
var window: Dictionary = {"n": 6400, "granularity_v2": "Region"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(64)
## A Region window smaller than one region (n < 100) must still derive a
## minimum 1x1 cell grid, never 0 — matching the server's `.max(1)`.
func test_cell_grid_side_for_window_region_minimum_is_one() -> void:
var window: Dictionary = {"n": 50, "granularity_v2": "Region"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(1)
## Missing granularity_v2 (an old-shape response) falls back to District —
## matching the server's own "unknown -> District" posture at every
## resolution boundary.
func test_cell_grid_side_for_window_missing_tag_falls_back_to_district() -> void:
var window: Dictionary = {"n": 32}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(32)
## T-1152/T-1153, design doc §6 encoding continuity: a Region-rung window (a
## FAR SPARSER cell grid — one region cell spans 100 districts) renders
## through the EXACT SAME _draw()/_rebuild_texture_if_needed() path as
## District — no separate branch, no crash reading past the (much smaller)
## per-cell arrays. This is the direct "same colorizer family at every rung"
## behavioral test the ticket asks for.
func test_draw_builds_a_texture_for_a_region_rung_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
# n=200 districts -> cell_grid_side = round(200/100) = 2 -> 4 cells,
# matching the 4-entry per-cell arrays below (same shape _mock_window()
# uses, just at Region's district-to-cell ratio).
stub.window = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o.viewer = stub
o._draw()
assert_that(o._cached_texture).override_failure_message(
"a Region-rung window must render through the same composite path as District"
).is_not_null()
assert_int(o._cached_texture.get_width()).override_failure_message(
"the built texture's resolution must be the DERIVED cell-grid side (2),"
+ " not the window's district extent (200)"
).is_equal(2)
## §6 "no mode flip" acceptance criterion, restated at the texture-cache
## level: swapping from a District-rung window to a Region-rung window at a
## NEW window object (the progressive-refinement swap) must still go through
## a single rebuild call producing a fresh texture — not a crash, not a
## silently-stale texture sized for the wrong rung.
func test_rebuild_handles_a_rung_swap_from_district_to_region() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var district_window: Dictionary = _mock_window() # n=2, District, 4 cells
o._rebuild_texture_if_needed(
district_window, AtlasWindowOverlay.cell_grid_side_for_window(district_window), ""
)
assert_int(o._cached_texture.get_width()).is_equal(2)
var region_window: Dictionary = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o._rebuild_texture_if_needed(
region_window, AtlasWindowOverlay.cell_grid_side_for_window(region_window), ""
)
assert_int(o._cached_texture.get_width()).override_failure_message(
"a rung swap must rebuild at the NEW rung's derived cell-grid resolution"
).is_equal(2) # region_window's cell_grid_side is also 2 here (200/100) — same size, different data
+142 -4
View File
@@ -16,17 +16,23 @@ const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_re
## Build a hand-authored DistrictWindowLayer dict, granularity-aware
## (T-1150) — mirrors test_atlas_window_viewer.gd's own _mock_window(), with
## granularity/min_wl_m added as optional params so callers can build both
## rungs' echo shapes with one helper.
## (T-1150, extended T-1152/T-1153 for granularity_v2) — mirrors
## test_atlas_window_viewer.gd's own _mock_window(), with
## granularity/min_wl_m/granularity_v2 added as optional params so callers
## can build any rung's echo shape with one helper.
static func _mock_window(
center: Vector2i, n: int = 2, granularity: int = 1, min_wl_m: int = 0
center: Vector2i,
n: int = 2,
granularity: int = 1,
min_wl_m: int = 0,
granularity_v2: String = "District"
) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"granularity": granularity,
"min_wl_m": min_wl_m,
"granularity_v2": granularity_v2,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
@@ -70,6 +76,54 @@ func test_on_response_with_mismatched_granularity_is_dropped_as_stale() -> void:
).is_true()
# =============================================================================
# (a2) granularity_v2 mismatch on the echo -> dropped as stale (T-1152/T-1153,
# the axis the legacy int alone cannot express — Region has no legacy value)
# =============================================================================
## request_now() can now ask for Region explicitly (T-1153's rung-reselect
## caller) — a response echoing "District" for the SAME center/n must be
## dropped as stale, the granularity_v2 twin of test (a) above, and the
## ONLY guard that can catch this specific mismatch (the legacy int is
## DISTRICT_GRANULARITY=1 on BOTH sides here, since Region has no legacy
## representation — see WindowGranularity::legacy_u32()'s doc).
func test_on_response_with_mismatched_granularity_v2_is_dropped_as_stale() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_bool(req.is_pending()).is_true()
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 6400, 1, 0, "District")
req.on_response(_mock_response("GJ380c", district_window))
assert_bool(req.is_pending()).override_failure_message(
"a granularity_v2-mismatched response (District answering a Region request)"
+ " must be dropped as stale, leaving the request still pending"
).is_true()
## The matching case: request_now() asking for Region, answered by a Region
## echo at the SAME (center, n) — must be ACCEPTED and cached under the
## Region key, retrievable on a follow-up request without a new network round
## trip.
func test_on_response_matching_granularity_v2_region_is_accepted_and_cached() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_bool(req.is_pending()).is_true()
var region_window: Dictionary = _mock_window(Vector2i(0, 0), 6400, 1, 0, "Region")
req.on_response(_mock_response("GJ380c", region_window))
assert_bool(req.is_pending()).is_false()
var received: Array = []
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_int(received.size()).override_failure_message(
"a second Region request at the same (center, n) must hit the cache"
).is_equal(1)
assert_bool(req.is_pending()).is_false()
# =============================================================================
# (b) old-server-shape response (no granularity/min_wl_m keys) -> defaults
# =============================================================================
@@ -175,3 +229,87 @@ func test_oversized_n_request_stores_clamped_n_and_accepts_matching_echo() -> vo
+ "already clamped to 64 before the request fired"
)
).is_false()
# =============================================================================
# (d) Region clamp mirror (T-1152/T-1153) — mirrors
# server/src/atlas/layer_proxy.rs's clamp_window_n_v2 EXACTLY, including the
# Region branch's bounded halving loop (no closed form, per Dudley's own
# doc — "replicate the loop exactly").
# =============================================================================
## District/Quarter through the v2 mirror must be BYTE-IDENTICAL to the
## legacy mirror — the server's own
## `clamp_window_n_v2_matches_legacy_for_finer_than_district_rungs`
## guarantee, restated client-side.
func test_clamp_window_n_mirror_v2_matches_legacy_for_district_and_quarter() -> void:
assert_int(
AtlasWindowRequest._clamp_window_n_mirror_v2(32, AtlasWindowRequest.GRANULARITY_V2_QUARTER)
).is_equal(AtlasWindowRequest._clamp_window_n_mirror(32, 4))
assert_int(
AtlasWindowRequest._clamp_window_n_mirror_v2(640, AtlasWindowRequest.GRANULARITY_V2_DISTRICT)
).is_equal(AtlasWindowRequest._clamp_window_n_mirror(640, 1))
## The clean Region boundary case: n=6,400 (DISTRICT_WINDOW_MAX_N_REGION,
## the per-axis cap exactly) derives cell_grid_side(6400) = round(6400/100) =
## 64, and 64² = 4,096 = WIRE_CAP_CELLS EXACTLY — the halving loop's `>`
## condition is false at the boundary, so this must clamp to EXACTLY 6,400,
## not halve further. This is the server's own
## `region_per_axis_cap_lands_exactly_on_wire_cap_when_uncontested`-shaped
## boundary (WIRE_CAP_CELLS_SQRT * DISTRICTS_PER_REGION is DERIVED to land
## here exactly, per that constant's own doc).
func test_clamp_window_n_mirror_v2_region_boundary_is_exact() -> void:
var n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(n).is_equal(AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
## Region's per-axis cap: a raw `n` far over DISTRICT_WINDOW_MAX_N_REGION
## (mirroring the server's own `region_request_oversized_n_clamps_and_echoes_clamped_n`
## test's `DISTRICT_WINDOW_MAX_N_REGION * 10` shape) must clamp DOWN — never
## trust the wire — and the result must satisfy BOTH invariants the server's
## own test asserts: `n <= DISTRICT_WINDOW_MAX_N_REGION` AND
## `cell_grid_side(n)^2 <= WIRE_CAP_CELLS`.
func test_clamp_window_n_mirror_v2_region_oversized_n_clamps_within_both_bounds() -> void:
var oversized: int = AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION * 10
var n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
oversized, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(n).override_failure_message(
"echoed n must be clamped to DISTRICT_WINDOW_MAX_N_REGION, not the raw oversized value"
).is_less_equal(AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
var side: int = AtlasWindowRequest._cell_grid_side_region_mirror(n)
assert_int(side * side).override_failure_message(
"clamped cell count must never exceed WIRE_CAP_CELLS at Region granularity either"
).is_less_equal(AtlasWindowRequest.SERVER_WIRE_CAP_CELLS)
## A REGION request whose derived grid would exceed WIRE_CAP_CELLS (n set so
## cell_grid_side(n) rounds to 65, just over the 64-per-axis wire-size
## ceiling) must actually HALVE — this is the case the boundary test above
## deliberately sits just below, so this one confirms the loop body actually
## fires, not just that its guard conditions are correct at the edges.
func test_clamp_window_n_mirror_v2_region_halves_when_over_wire_cap() -> void:
# n=6,450 -> cell_grid_side = round(64.5) = 65 (round-half-away-from-zero,
# matching Rust's f64::round() and GDScript's roundi() for non-negative
# inputs) -> 65² = 4,225 > WIRE_CAP_CELLS (4,096) -> must halve at least once.
var n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
6450, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(n).override_failure_message(
"a Region request whose grid exceeds WIRE_CAP_CELLS must be halved down, not left at 6,450"
).is_less(6450)
var side: int = AtlasWindowRequest._cell_grid_side_region_mirror(n)
assert_int(side * side).is_less_equal(AtlasWindowRequest.SERVER_WIRE_CAP_CELLS)
## n smaller than one region (n < 100) must clamp its cell-grid side to a
## minimum of 1 — cell_grid_side_for_window()'s own `.max(1)` — never a
## degenerate 0x0 grid, matching WindowGranularity::cell_grid_side's own
## documented minimum.
func test_cell_grid_side_region_mirror_minimum_is_one() -> void:
assert_int(AtlasWindowRequest._cell_grid_side_region_mirror(1)).is_equal(1)
assert_int(AtlasWindowRequest._cell_grid_side_region_mirror(50)).is_equal(1)
+22 -15
View File
@@ -144,11 +144,15 @@ func test_set_view_and_getters_round_trip() -> void:
assert_that(v.get_view_offset()).is_equal(Vector2(30.0, -10.0))
## T-1153: MIN_ZOOM widened to 0.0005 (from the pre-ladder 0.5) so a
## gas-giant-scale body's enter_orbital() fit zoom is never itself clamped —
## see MIN_ZOOM's own doc. Values here are chosen well outside the new wide
## range on both ends, not the old range's boundary values.
func test_set_view_clamps_to_min_max_zoom() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(0.01, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.001)
v.set_view(0.0000001, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.0001)
v.set_view(1000.0, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MAX_ZOOM, 0.001)
@@ -352,25 +356,28 @@ func test_wasd_diagonal_pan_is_not_faster_than_single_axis() -> void:
).is_equal_approx(axis_distance, 0.01)
## The real scene-tree path: DistrictScreen -> AtlasWindowViewer. Unlike
## drag (which needed _gui_input event delivery, hence the old
## "does an ancestor eat the event" test), WASD pan lives in _process() —
## Godot delivers _process() to every node in the tree regardless of Control
## mouse_filter/ancestry (there is no "topmost control" routing for
## per-frame process callbacks the way there is for _gui_input), so there is
## no equivalent "does DistrictScreen eat it" question for _process() itself.
## What DOES still matter through the real chain is _is_over_ui()'s edge-
## scroll suppression and visibility gating — pinned directly below instead.
func test_wasd_pan_reaches_viewer_through_district_screen_chain() -> void:
var screen: DistrictScreen = auto_free(DistrictScreen.new())
## The real scene-tree path: RegionalScreen -> AtlasWindowViewer (T-1153 —
## RegionalScreen is now the WHOLE ladder's nav entry, superseding the
## retired DistrictScreen nav hop; see atlas_app.gd's own doc for why the
## separate "district" screen retired). Unlike drag (which needed
## _gui_input event delivery, hence the old "does an ancestor eat the
## event" test), WASD pan lives in _process() — Godot delivers _process()
## to every node in the tree regardless of Control mouse_filter/ancestry
## (there is no "topmost control" routing for per-frame process callbacks
## the way there is for _gui_input), so there is no equivalent "does the
## screen eat it" question for _process() itself. What DOES still matter
## through the real chain is _is_over_ui()'s edge-scroll suppression and
## visibility gating — pinned directly below instead.
func test_wasd_pan_reaches_viewer_through_regional_screen_chain() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
screen.enter({"body": {"body_id": "GJ380c"}, "district_center": Vector2i(0, 0)})
screen.enter({"body": {"body_id": "GJ380c"}, "system": {}})
var offset_before: Vector2 = screen._viewer.get_view_offset()
screen._viewer._apply_pan_delta(Vector2(1.0, 0.0), 0.1)
assert_that(screen._viewer.get_view_offset()).override_failure_message(
"a pan tick driven through DistrictScreen's child viewer must still move"
"a pan tick driven through RegionalScreen's child viewer must still move"
+ " _view_offset — no ancestor in the real screen chain blocks it"
).is_not_equal(offset_before)
+361
View File
@@ -0,0 +1,361 @@
## T-1153 (D-226 T-1143-rulings amendment): tests for the continuous
## cursor-anchored zoom ladder — enter_orbital() (the canonical planetary
## frame), progressive refinement (held composite survives a rung-crossing
## request), the full-zoom-out reset (Jeroen's HARD condition), rung
## reselection on zoom, and E/W wrap + pole-wall clamps at Region
## granularity. Split out of test_atlas_window_viewer.gd (which owns the
## pre-T-1153 window-viewer behavior — entry, cache reuse, WASD/edge-scroll,
## fit-and-center) purely for file-length reasons (gdlint max-file-lines);
## same instantiation/mock-response conventions as that file, not a
## different testing philosophy.
class_name TestAtlasZoomLadder
extends GdUnitTestSuite
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## Build a hand-authored DistrictWindowLayer dict (n=2 by default) — mirrors
## test_atlas_window_viewer.gd's own _mock_window().
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
# =============================================================================
# T-1153: enter_orbital() — the canonical planetary frame, the ladder's TOP
# REST STATE (Jeroen's HARD condition, D-226 T-1143-rulings amendment).
# =============================================================================
## enter_orbital() must center on district (0,0) — "district (0,0) sits at
## lon 0 / the equator" (AtlasDescendGeometry's own doc).
func test_enter_orbital_centers_on_the_canonical_origin() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_that(v._held_center).is_equal(Vector2i.ZERO)
## enter_orbital() must request at Region granularity — the orbital view IS
## the Region rung at high n, not a separate screen/mode (the ticket's own
## framing).
func test_enter_orbital_requests_region_granularity() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
## enter_orbital()'s `n` must equal the body's full equatorial circumference
## in districts (district_extent().cols) — the whole body fitted to the
## canvas, per Jeroen's HARD condition wording.
func test_enter_orbital_n_covers_the_full_circumference() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_int(v._held_n).is_equal(int(extent["cols"]))
## A no-radius body (tiny test body) has no circumference concept —
## enter_orbital() falls back to the District-rung default window rather
## than crashing or deriving a degenerate n.
func test_enter_orbital_no_radius_body_falls_back_to_district() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c"}, {})
assert_str(v._held_granularity_v2).is_equal("District")
assert_int(v._held_n).is_equal(AtlasWindowRequest.DISTRICT_WINDOW_DEFAULT_N)
# =============================================================================
# T-1153: progressive refinement — the held composite survives until the
# replacement arrives (§6 "no mode flip": never a blank frame, never a
# clear-then-redraw).
# =============================================================================
## The core acceptance test: once a window is held, a request for a
## DIFFERENT rung being in-flight must NOT clear `_window` — the old
## composite stays exactly what get_district_window() returns until the new
## rung's response actually arrives and is adopted.
func test_held_window_survives_while_a_different_rung_request_is_in_flight() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
assert_that(v.get_district_window()).is_equal(district_window)
# Simulate a rung-reselect firing a NEW (Region) request without the
# response having arrived yet — direct call, mirroring what
# _maybe_reselect_rung() does internally.
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
assert_that(v.get_district_window()).override_failure_message(
"the OLD composite must survive while a different-rung request is in"
+ " flight — no blank frame, no premature clear"
).is_equal(district_window)
## Once the new rung's response actually arrives (matching the CURRENTLY
## in-flight request's granularity_v2), it swaps in — the composite reference
## changes from the old rung's window to the new one.
func test_new_rung_window_swaps_in_once_it_arrives() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
var region_window: Dictionary = {
"center": [10, 20], "n": 2, "granularity_v2": "Region",
"morphology": PackedByteArray([1, 2, 3, 4]),
"elev_q": PackedByteArray([10, 20, 30, 40]),
"temp_dc": [0, 0, 0, 0],
"moisture_q": PackedByteArray([0, 0, 0, 0]),
"vegetation": PackedByteArray([0, 0, 0, 0]),
"glaciation": PackedByteArray([0, 0, 0, 0]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", region_window))
assert_that(v.get_district_window()).override_failure_message(
"once the new rung's matching response arrives, it must swap in"
).is_equal(region_window)
assert_str(v._held_granularity_v2).is_equal("Region")
## A response for a rung OTHER than what's currently requested (e.g. a
## District response arriving after the viewer has already moved on to a
## Region request — a rapid wheel-zoom race) must be discarded as stale, the
## held composite untouched.
func test_stale_rung_response_after_moving_on_is_discarded() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
# A LATE district-rung response for the same (center, n) arrives after the
# viewer has already moved on to requesting Region — must be dropped.
var late_district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
late_district_window["morphology"] = PackedByteArray([9, 9, 9, 9]) # distinguishable payload
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", late_district_window))
assert_that(v.get_district_window()).override_failure_message(
"a stale response for a rung the viewer has since moved on from must be discarded"
).is_equal(district_window)
# =============================================================================
# T-1153: full-zoom-out reset (Jeroen's HARD condition).
# =============================================================================
## Directly at the canonical frame already (center (0,0), Region granularity)
## must be a no-op — never re-fights a player zooming back IN from the top.
func test_reset_to_canonical_frame_is_noop_when_already_there() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._held_center = Vector2i.ZERO
v._held_granularity_v2 = "Region"
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"already at the canonical frame — the reset must not re-fire"
).is_false()
## A no-radius body must never trigger the reset (no circumference concept —
## matches enter_orbital()'s own guard).
func test_reset_to_canonical_frame_never_fires_for_no_radius_body() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(5, 5), 2)
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).is_false()
## Away from the canonical frame (a drifted District-rung pan/zoom state)
## with a fully-zoomed-out world extent must reset — the direct wiring test
## for Jeroen's HARD condition: enter() at a far-off center, then force the
## view zoom low enough that the displayed extent covers the whole body.
func test_reset_to_canonical_frame_fires_and_re_centers_when_fully_zoomed_out() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 50.0 # tiny synthetic body — small circumference, reachable by a modest zoom-out
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(500, 10), 32)
# Force a very low zoom — a huge displayed world extent, comfortably over
# this tiny body's whole circumference.
v._view_zoom = AtlasWindowViewer.MIN_ZOOM
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"a fully-zoomed-out view on a real-radius body must trigger the reset"
).is_true()
assert_that(v._held_center).override_failure_message(
"the reset must re-center on the canonical origin (0,0)"
).is_equal(Vector2i.ZERO)
assert_str(v._held_granularity_v2).override_failure_message(
"the reset must land on the Region rung — the ladder's top rest state"
).is_equal("Region")
## Not fully zoomed out (a normal District-rung view) must NOT trigger the
## reset — only reaching the top of the ladder resets, not every zoom step.
func test_reset_to_canonical_frame_does_not_fire_when_not_fully_zoomed_out() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(500, 10), 32)
v._view_zoom = 1.0 # a normal, non-extreme zoom — nowhere near full planetary coverage
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"an ordinary District-rung view must not trigger the top-rest-state reset"
).is_false()
# =============================================================================
# T-1153: rung reselection — _zoom_at() crossing a rung threshold fires a
# new request without touching the held composite.
# =============================================================================
## Zooming OUT far enough from a District-rung window (small n, so a modest
## zoom-out already covers a huge world extent) must fire a coarser-rung
## request — the wheel-zoom-driven wiring test for _maybe_reselect_rung().
func test_zoom_out_past_district_threshold_requests_a_coarser_rung() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 2) # n=2 — a tiny window, easy to overshoot
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
assert_str(v._window_request.get_granularity_v2()).is_equal("District")
# A big zoom-OUT factor (well under 1.0) from a tiny n=2 window blows the
# displayed world extent WAY past District's threshold.
v._zoom_at(Vector2(400.0, 300.0), 0.01)
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
"zooming out far enough from a small District window must re-request a coarser rung"
).is_not_equal("District")
# The OLD composite must still be what's held — progressive refinement,
# not a block-on-derive clear.
assert_that(v.get_district_window()).is_equal(district_window)
## Zooming IN on a District-rung window (well within its own legal spacing
## band) must NOT trigger a rung change — this is the "zoom is client-side on
## the already-held composite" case, unchanged for in-rung zoom. Sets
## _view_zoom DIRECTLY to a value inside District's legal band (rather than
## relying on enter()'s COVER auto-fit, which for a small n can already sit
## right at Quarter's own threshold — a fit's zoom level is a display-density
## choice independent of what rung selection would pick from scratch, and
## this test is specifically about a SINGLE zoom-in STEP not crossing a
## boundary, not about where the auto-fit itself lands). District's legal
## band (select_rung()'s own doc: the coverage ceiling and the `2x` visual
## band only overlap at small viewports — `canvas_px <= DISTRICT_WINDOW_MAX_N
## * DISTRICT_SPACING_M / 1024 = 128px`) requires a SMALL viewport here,
## unlike most of this suite's 800x600/1920x1080 fixtures.
func test_zoom_in_within_district_threshold_does_not_change_rung() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(100.0, 80.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._view_zoom = 0.10666666666666667 # E=120,000m at C=100px — inside District's legal band
v._apply_transform()
v._zoom_at(Vector2(50.0, 40.0), 1.15) # a single ordinary zoom-in step
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
"a single ordinary zoom-in step must not cross a rung threshold"
).is_equal("District")
# =============================================================================
# T-1153: E/W wrap and pole-wall clamps at EVERY rung — both are extent-
# relative (CELL_PIXEL_SIZE-based district-space math, unchanged regardless
# of which rung's data is actually held), so they must keep working
# unmodified at Region granularity, not just District/Quarter.
# =============================================================================
## The pole wall, wired through the real _apply_pan_delta() path, must still
## clamp at Region granularity — same mechanism as the existing District-rung
## test (test_wasd_pan_is_clamped_by_the_pole_wall_when_wired), just entered
## via enter_orbital() instead of enter().
func test_pole_wall_clamps_at_region_granularity_too() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
var radius_km := 50.0 # tiny synthetic body — pole wall reachable by an ordinary tick
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
var unclamped_magnitude: float = 500.0 * AtlasWindowViewer.PAN_SPEED_CANVAS_PX_S * v.get_view_zoom()
v._apply_pan_delta(Vector2(0.0, -1.0), 500.0) # "W"/north held, an absurdly long tick
assert_float(absf(v.get_view_offset().y)).override_failure_message(
"the pole wall must still clamp an extreme pan at Region granularity"
).is_less(unclamped_magnitude * 0.5)
## East-west wrap (canonicalize_district_center()) must still apply to the
## pan-edge refloat's resulting center at Region granularity — a pan that
## carries the screen-center column past the body's circumference must wrap
## into [0, cols), never run away to an out-of-range column, exactly as the
## District-rung wrap tests already pin (T-1142 item 6a). At Region's own
## enormous held_n (a whole circumference), an ORDINARY pan tick's
## canvas-space delta is negligible relative to the window's half-extent
## (confirmed: ~0.08 districts per 5-second tick vs. a ~9,772-district
## half-window) — so this drives _maybe_refloat_window() DIRECTLY off a
## manually-set _view_offset large enough to genuinely cross the held
## window's edge, the same "exercise the actual edge-crossing branch, not
## just its no-op early-return" discipline _maybe_refloat_window()'s own
## inside-check comment describes.
func test_pan_edge_refloat_wraps_columns_at_region_granularity_too() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
# Force the held center to sit one column short of the wrap seam, then
# shift the CANVAS offset by more than half the window's own on-screen
# extent — enough to move the screen-center's mapped column past the
# window's far edge (i.e. past `cols`, crossing the seam) regardless of
# Region's huge held_n.
v._held_center = Vector2i(cols - 1, 0)
var half_window_screen_px: float = float(v._held_n) * v.get_cell_pixel_size() * v.get_view_zoom() * 0.5
v._view_offset = v.get_view_offset() - Vector2(half_window_screen_px * 1.5, 0.0)
v._maybe_refloat_window()
assert_int(v._held_center.x).override_failure_message(
"a pan crossing the antimeridian at Region granularity must wrap the"
+ " resulting center into [0, cols), never run past cols"
).is_less(cols)
assert_int(v._held_center.x).is_greater_equal(0)