fix(ui): T-1142 client — descent bounds gate, fit-and-center, pole wall, east-west wrap, body-name header

Bounds gate: AtlasDescendGeometry.is_on_texture() — ONE helper feeding
both the reticle guard and the click fall-through (the T-1140 lesson:
the visible affordance always matches the click); half-open
[0,tex_w)x[0,tex_h) boundary pinned at the exact edge. Letterbox
clicks no longer show a reticle or descend.

Fit-and-center: pure fit_window_view() (new atlas_window_geometry.gd)
wired into enter(), the FIRST window arrival, and NOTIFICATION_RESIZED
— gated by a _user_adjusted flag so the fit never fights manual
zoom/pan (flag clears only on a fresh enter). Found-own-bug: RESIZED
can fire mid-_ready() before _canvas exists — null-guarded like the
sibling panels.

Pole wall (Jeroen's ruling): clamp_pan_offset_to_pole_wall() clamps
the WINDOW EDGE, not the center, in screen space from the fitted
transform — Y only; wired into the drag handler and every fit (a
fresh fit can itself need the wall on a tiny body — the
window-taller-than-planet case is handled and tested). Three numeric
hand-traces preceded the code; a first-draft test using GJ380c's huge
radius silently never exercised the clamp — replaced with a synthetic
small radius.

East-west wrap (Jeroen's ruling): canonicalize_district_center() —
posmod column wrap (verified against a live Godot process to match
Rust rem_euclid bit-for-bit), clamped row; district_extent() shares
the exact formula (incl. .max(1)) with the server's
normalize_window_center so echoes and cache keys agree on canonical
form. Canonicalization applies only to the FINAL refetch center — the
edge-crossing decision stays in absolute district space (first-pass
math error caught by hand-trace). Seam-adjacent cache-key sharing
tested. Pan offset itself has no x wall — circumnavigation is
seamless.

Header: body proper_name/body_id ahead of the coordinates (the cheap
half of T-1141, noted in code). Drag-pan verified through the REAL
DistrictScreen-to-viewer chain and pinned by test (no fix needed).

140 tests across three suites, 0 failures; 94 sibling tests no
ripple; gdlint clean (atlas_viewer.gd at the 1000-line cap a second
round — structural extraction flagged for maintenance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 14:53:32 +02:00
co-authored by Claude Fable 5
parent 4a65bb210d
commit 831625019a
7 changed files with 966 additions and 39 deletions
+208
View File
@@ -211,6 +211,132 @@ func test_district_pos_at_off_equator_row_is_not_off_by_a_few_districts() -> voi
assert_int(recovered.y).is_equal(district_row)
# =============================================================================
# T-1142 item 1: is_on_texture() — the letterbox bounds gate
# =============================================================================
func test_is_on_texture_true_for_a_point_inside_the_texture() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(500.0, 250.0), 1024.0, 512.0)).is_true()
func test_is_on_texture_true_at_the_top_left_origin() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2.ZERO, 1024.0, 512.0)).is_true()
## Half-open range [0, tex_w) x [0, tex_h) — the last valid pixel is tex_w-1 /
## tex_h-1, NOT tex_w/tex_h themselves (canvas_pt.x == tex_w is one pixel
## PAST the texture, the classic off-by-one a naive <= bound would miss).
func test_is_on_texture_false_exactly_at_the_texture_width_bound() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(1024.0, 250.0), 1024.0, 512.0)).is_false()
func test_is_on_texture_false_exactly_at_the_texture_height_bound() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(500.0, 512.0), 1024.0, 512.0)).is_false()
## Jeroen's exact repro shape: a letterbox click lands far PAST the texture
## width in canvas space (a wide viewport around a 2:1-fitted heightmap).
func test_is_on_texture_false_for_a_letterbox_point_past_texture_width() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(1400.0, 250.0), 1024.0, 512.0)).is_false()
func test_is_on_texture_false_for_negative_coordinates() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(-10.0, 250.0), 1024.0, 512.0)).is_false()
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(500.0, -10.0), 1024.0, 512.0)).is_false()
# =============================================================================
# T-1142 item 6a: canonicalize_district_center() — column wraps, row clamps
# =============================================================================
## Jeroen's own repro column (12276) on a body whose circumference works out
## to ~11236 districts (body_radius_km chosen so district_extent().cols ==
## 11236 as closely as the rounding allows) wraps down into range — the exact
## scenario the letterbox click hit before the bounds gate (item 1) made it
## unreachable via the UI, but canonicalization is still the correct backstop
## for any out-of-range center this or a future path constructs.
func test_canonicalize_wraps_a_column_past_the_circumference() -> void:
var radius_km := 6371.0 # -> district_extent().cols ~= 19,568 (Earth-like)
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var out_of_range := Vector2i(cols + 100, 0)
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
out_of_range, radius_km
)
assert_int(canonical.x).is_equal(100)
assert_int(canonical.y).is_equal(0)
## A column ONE past the wrap seam (cols) canonicalizes to column 0 — its
## "twin" on the other side of the antimeridian. This is the exact identity
## AtlasWindowCache.make_key() depends on for item 6b (a full-circumnavigation
## pan hits cache, not a fresh derive) — tested directly on the cache in
## test_atlas_window_viewer.gd; this pins the canonicalization half alone.
func test_canonicalize_one_column_past_the_seam_matches_its_twin() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var past_seam: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(cols, 50), radius_km
)
var at_seam: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, 50), radius_km
)
assert_that(past_seam).override_failure_message(
"column `cols` and column 0 are the same antimeridian-adjacent point"
).is_equal(at_seam)
## Negative columns wrap too (Euclidean, not truncating) — a pan that crosses
## the seam going WEST must land in [0, cols), not go negative.
func test_canonicalize_wraps_a_negative_column() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(-5, 0), radius_km
)
assert_int(canonical.x).is_equal(cols - 5)
## Rows CLAMP, never wrap — a row past +rows_half pins to +rows_half exactly
## (the pole), matching the server's normalize_window_center() clamp
## disposition (latitude terminates, it does not wrap around).
func test_canonicalize_clamps_a_row_past_the_pole() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var rows_half: int = int(extent["rows_half"])
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, rows_half + 500), radius_km
)
assert_int(canonical.y).is_equal(rows_half)
var canonical_south: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, -rows_half - 500), radius_km
)
assert_int(canonical_south.y).is_equal(-rows_half)
## An already-in-range center is a no-op (identity) — canonicalization must
## never perturb a legitimate, already-valid request.
func test_canonicalize_is_identity_for_an_in_range_center() -> void:
var radius_km := 6371.0
var in_range := Vector2i(500, 100)
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
in_range, radius_km
)
assert_that(canonical).is_equal(in_range)
## No-radius bodies (tiny test bodies) are identity — matching
## normalize_window_center()'s own no-radius disposition (no periodicity
## concept at the DistrictPos level for a body with no radius).
func test_canonicalize_no_radius_is_identity() -> void:
var anything := Vector2i(99999, -99999)
assert_that(AtlasDescendGeometry.canonicalize_district_center(anything, 0.0)).is_equal(anything)
# =============================================================================
# AtlasViewer — fixed view (no drag-pan/wheel-zoom) + click-through descent
# =============================================================================
@@ -310,3 +436,85 @@ func test_descend_reticle_hides_when_not_hovering() -> void:
v._hover_active = false
v._hovered_city = {}
assert_bool(v._should_draw_descend_reticle()).is_false()
# =============================================================================
# T-1142 item 1: the letterbox bounds gate, wired into AtlasViewer's own
# reticle guard and click fall-through (fields default to _tex_w=1024/
# _tex_h=512, _view_zoom=1.0, _view_offset=ZERO — screen_to_canvas() is
# therefore the identity transform in these tests, so a screen point maps
# 1:1 to the same canvas point, matching this file's own established
## _one_pixel_texture() convention above (the LOADED texture's real
# dimensions don't matter here — only _tex_w/_tex_h do).
# =============================================================================
## An on-map screen point (well inside [0, 1024) x [0, 512)) shows the
## reticle — the ordinary, expected case.
func test_descend_reticle_shows_for_an_on_texture_point() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {}
v._hover_screen_pos = Vector2(500.0, 250.0)
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must show for a point on the heightmap texture"
).is_true()
## Jeroen's exact repro shape: a letterbox point (canvas x >= tex_w, i.e. past
## the right edge of a fitted 2:1 heightmap in a wider viewport) HIDES the
## reticle — the affordance must never promise a descent the click can't
## honestly perform.
func test_descend_reticle_hides_for_a_letterbox_point_past_texture_width() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {}
v._hover_screen_pos = Vector2(1400.0, 250.0) # past _tex_w=1024
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must hide for a letterbox point past the texture's right edge"
).is_false()
## Same shape, Y axis — a letterbox point above/below a fitted heightmap
## (narrow-viewport case) must also hide the reticle.
func test_descend_reticle_hides_for_a_letterbox_point_past_texture_height() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {}
v._hover_screen_pos = Vector2(500.0, 900.0) # past _tex_h=512
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must hide for a letterbox point past the texture's bottom edge"
).is_false()
## The click fall-through mirrors the reticle exactly (item 1's "one truth"
## requirement) — an on-texture click DOES emit district_descend_requested.
func test_descend_at_emits_for_an_on_texture_click() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
var received: Array = []
v.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
v._descend_at(Vector2(500.0, 250.0))
assert_int(received.size()).override_failure_message(
"an on-texture click must emit district_descend_requested"
).is_equal(1)
## A letterbox click is INERT — no signal at all, matching the reticle never
## having shown a promise there. This is Jeroen's exact repro: a letterbox
## click must never derive a DistrictPos, on-texture or off.
func test_descend_at_is_inert_for_a_letterbox_click() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
var received: Array = []
v.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
v._descend_at(Vector2(1400.0, 250.0))
assert_int(received.size()).override_failure_message(
"a letterbox click must be inert — no district_descend_requested at all"
).is_equal(0)
+209
View File
@@ -0,0 +1,209 @@
## T-1142 (Jeroen's second/third hands-on findings): pure-function tests for
## AtlasWindowViewer's fit-and-center math (fit_window_view) and pole-wall
## pan clamp (clamp_pan_offset_to_pole_wall) — both extracted specifically so
## the "viewport + n -> zoom/offset" transform is unit-testable without a
## live Control tree.
class_name TestAtlasWindowGeometry
extends GdUnitTestSuite
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
const MIN_ZOOM: float = 0.5
const MAX_ZOOM: float = 8.0
const CELL_PIXEL_SIZE: float = 16.0
# =============================================================================
# fit_window_view — the "postage stamp" fix (item 2)
# =============================================================================
## n=32, cell_px=16 -> native composite is 512x512. A 1920x1080 viewport's
## smaller dimension is 1080, so zoom = 0.9 * 1080 / 512 ~= 1.898 — well
## inside [MIN_ZOOM, MAX_ZOOM], so the clamp is a no-op here.
func test_fit_window_view_computes_expected_zoom_for_a_wide_viewport() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(1920.0, 1080.0), 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
var expected_zoom: float = 0.9 * 1080.0 / 512.0
assert_float(fit["zoom"]).is_equal_approx(expected_zoom, 0.001)
## The composite must be CENTERED — offset.x/.y each leave an equal margin on
## both sides of the (n*cell_px*zoom)-sized composite.
func test_fit_window_view_centers_the_composite() -> void:
var viewport := Vector2(1920.0, 1080.0)
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
viewport, 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
var composite_scaled: float = 32.0 * CELL_PIXEL_SIZE * float(fit["zoom"])
var offset: Vector2 = fit["offset"]
# The composite's right/bottom edge is offset + composite_scaled — the
# margin on the far side must equal the margin on the near side (offset).
var right_margin: float = viewport.x - (offset.x + composite_scaled)
var bottom_margin: float = viewport.y - (offset.y + composite_scaled)
assert_float(right_margin).is_equal_approx(offset.x, 0.01)
assert_float(bottom_margin).is_equal_approx(offset.y, 0.01)
## Jeroen's exact bug: an n=32 composite (512px native) in a real ~1920px
## viewport must NOT render at zoom=1.0 (the old, unfitted "postage stamp"
## behavior) — the fit must scale it up to fill most of the smaller
## viewport dimension.
func test_fit_window_view_scales_up_a_small_composite_to_fill_the_viewport() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(1920.0, 1080.0), 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit["zoom"]).override_failure_message(
"a 512px composite in a 1920x1080 viewport must be scaled UP, not left at 1.0"
).is_greater(1.0)
## A huge n (e.g. n=64 at a tiny viewport) must clamp to MIN_ZOOM, never
## shrink the composite into illegibility below the floor.
func test_fit_window_view_clamps_to_min_zoom_for_a_tiny_viewport() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(200.0, 150.0), 64, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit["zoom"]).is_equal_approx(MIN_ZOOM, 0.001)
## A small n (e.g. n=2) at a huge viewport must clamp to MAX_ZOOM, never
## scale past the ceiling.
func test_fit_window_view_clamps_to_max_zoom_for_a_tiny_composite() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(3840.0, 2160.0), 2, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit["zoom"]).is_equal_approx(MAX_ZOOM, 0.001)
## Degenerate inputs (zero viewport, zero n) must not divide by zero — a safe
## fallback (zoom=1.0, offset=ZERO), never a crash or NaN.
func test_fit_window_view_degenerate_inputs_are_safe() -> void:
var fit_zero_viewport: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2.ZERO, 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit_zero_viewport["zoom"]).is_equal_approx(1.0, 0.001)
var fit_zero_n: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(1920.0, 1080.0), 0, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit_zero_n["zoom"]).is_equal_approx(1.0, 0.001)
# =============================================================================
# clamp_pan_offset_to_pole_wall — item 5 (pole hard wall, row axis only)
# =============================================================================
## Deep inside the valid range (window nowhere near a pole), the clamp must
## be a no-op — offset passes through unchanged.
func test_pole_wall_clamp_is_a_noop_far_from_the_poles() -> void:
var offset := Vector2(10.0, 20.0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
offset, Vector2(1920.0, 1080.0), Vector2i(0, 0), 32, 4785, CELL_PIXEL_SIZE, 1.0
)
assert_that(clamped).is_equal(offset)
## X is NEVER clamped by the pole wall (item 6: east-west is seamless) — even
## an absurdly large X offset passes through untouched.
func test_pole_wall_clamp_never_touches_x() -> void:
var offset := Vector2(999999.0, 0.0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
offset, Vector2(1920.0, 1080.0), Vector2i(0, 0), 32, 4785, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped.x).is_equal_approx(999999.0, 0.001)
## The core pole-wall behavior: dragging FAR past the north pole (offset.y
## driven to an extreme) must clamp — the resulting offset must be LESS than
## the extreme requested, and a SECOND, even-more-extreme drag must produce
## the SAME clamped value (further dragging is inert once pinned at the wall).
func test_pole_wall_clamp_pins_offset_when_dragged_past_the_pole() -> void:
var rows_half := 100
var held_center := Vector2i(0, 90) # near the south pole already (row 90 of 100)
var extreme_offset := Vector2(0.0, 5000.0) # a huge downward drag
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
extreme_offset, Vector2(800.0, 800.0), held_center, 32, rows_half, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped.y).override_failure_message(
"an extreme drag toward the pole must be clamped, not pass through"
).is_less(extreme_offset.y)
var even_more_extreme := Vector2(0.0, 50000.0)
var clamped_again: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
even_more_extreme, Vector2(800.0, 800.0), held_center, 32, rows_half, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped_again.y).override_failure_message(
"further dragging past an already-pinned wall must be inert (same clamped value)"
).is_equal_approx(clamped.y, 0.01)
## Symmetric check on the north side: a huge UPWARD drag near the north pole
## also clamps.
func test_pole_wall_clamp_pins_offset_on_the_north_side_too() -> void:
var rows_half := 100
var held_center := Vector2i(0, -90) # near the north pole
var extreme_offset := Vector2(0.0, -5000.0) # a huge upward drag
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
extreme_offset, Vector2(800.0, 800.0), held_center, 32, rows_half, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped.y).override_failure_message(
"an extreme drag toward the north pole must be clamped"
).is_greater(extreme_offset.y)
## rows_half <= 0 (a no-radius body, or a degenerate district_extent()) means
## "no wall concept" — the clamp is a no-op, matching
## canonicalize_district_center()'s own no-radius identity disposition.
func test_pole_wall_clamp_is_noop_when_rows_half_is_zero() -> void:
var offset := Vector2(0.0, 999999.0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
offset, Vector2(800.0, 800.0), Vector2i(0, 0), 32, 0, CELL_PIXEL_SIZE, 1.0
)
assert_that(clamped).is_equal(offset)
## Tiny-body edge case (documented open item in atlas_window_viewer.gd's own
## _clamp_offset_to_pole_wall doc): a window TALLER than the whole planet's
## row span (n=64 window, rows_half=10 -> pole-to-pole is only 20 districts)
## must not crash or produce an inverted/degenerate clamp range — the offset
## still comes back as a finite Vector2, and repeated extreme drags still
## converge to a stable pinned value (not NaN, not unbounded).
func test_pole_wall_clamp_handles_a_window_taller_than_the_planet() -> void:
var rows_half := 10
var held_n := 64
var held_center := Vector2i(0, 0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
Vector2(0.0, 999999.0), Vector2(800.0, 800.0), held_center, held_n, rows_half,
CELL_PIXEL_SIZE, 1.0
)
assert_bool(is_finite(clamped.y)).override_failure_message(
"a window taller than the planet's row span must still produce a finite clamp"
).is_true()
var clamped_again: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
Vector2(0.0, 9999999.0), Vector2(800.0, 800.0), held_center, held_n, rows_half,
CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped_again.y).is_equal_approx(clamped.y, 0.01)
# =============================================================================
# Cross-check: clamp bounds derived from district_extent() (the SAME source
# canonicalize_district_center() uses) — confirms the two T-1142 fixes (item
# 5 pole wall, item 6a wrap/clamp) agree on what "the pole" even is.
# =============================================================================
func test_pole_wall_rows_half_matches_canonicalize_rows_half() -> void:
var radius_km := 6238.4 # GJ380c
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var rows_half: int = int(extent["rows_half"])
# A center exactly at (0, rows_half) must canonicalize to itself (already
# at the pole boundary, not past it) — pins that the SAME rows_half both
# fixes consume describes an inclusive boundary, not an exclusive one.
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, rows_half), radius_km
)
assert_int(canonical.y).is_equal(rows_half)
+228
View File
@@ -13,6 +13,9 @@ extends GdUnitTestSuite
# this cluster) — preloaded once here, not re-load()ed per test (gdlint
# duplicated-load).
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
# T-1142: district_extent()/canonicalize_district_center() — used to derive
# real (cols/rows_half) bounds for the wrap/pole-wall tests below.
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## Build a hand-authored DistrictWindowLayer dict (n=2, matching the shape
@@ -202,3 +205,228 @@ func test_on_response_resolves_and_populates_cache_for_next_request() -> void:
assert_bool(req.is_pending()).override_failure_message(
"a second request for an already-resolved window must hit the cache"
).is_false()
# =============================================================================
# T-1142 item 2: fit-and-center on entry (Jeroen's "postage stamp" finding)
# =============================================================================
## enter() must fit-and-center, NOT reset to the old zoom=1.0/offset=ZERO.
## With a real viewport size set on the Control, the fitted zoom for an
## n=32 default window must scale up past 1.0 (matches
## test_atlas_window_geometry.gd's own fit math, exercised here through the
## real enter() call path instead of the pure function directly).
func test_enter_fits_and_centers_instead_of_resetting_to_zoom_one() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32)
assert_float(v.get_view_zoom()).override_failure_message(
"an n=32 (512px native) composite in a 1920x1080 viewport must be fitted"
+ " (zoom > 1.0), not left at the old zoom=1.0 postage-stamp default"
).is_greater(1.0)
## After enter()'s fit, the offset must not be Vector2.ZERO (the old
## behavior) — it must be the CENTERING offset the fit produces.
func test_enter_offset_is_not_the_old_zero_default() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32)
assert_that(v.get_view_offset()).override_failure_message(
"a fitted+centered composite in a 1920x1080 viewport should not sit at (0,0)"
).is_not_equal(Vector2.ZERO)
# =============================================================================
# T-1142 item 3: header carries the body's proper name (cheap half of T-1141)
# =============================================================================
func test_header_location_label_includes_body_proper_name() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(5, 5), 2)
assert_str(v._location_label()).contains("Lendel")
## No proper_name on the body dict -> falls back to body_id (matches
## AtlasViewer's own _refresh_screen_header fallback chain exactly).
func test_header_location_label_falls_back_to_body_id() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ903b"}, {}, Vector2i(5, 5), 2)
assert_str(v._location_label()).contains("GJ903b")
func test_header_location_label_still_includes_the_coordinates() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(42, -7), 2)
var label: String = v._location_label()
assert_str(label).contains("42")
assert_str(label).contains("-7")
# =============================================================================
# T-1142 item 4: drag-pan through the real input chain (DistrictScreen ->
# AtlasWindowViewer._gui_input) — verifies no ancestor eats the event.
# =============================================================================
## Drives _gui_input DIRECTLY on the viewer (the same call gdUnit's own
## headless-mode InputEvent limitation forces every other _gui_input test in
## this cluster to use — see test_atlas_descend_entry.gd's own note on real
## mouse events not being transported in headless mode). This confirms the
## HANDLER logic itself moves _view_offset on a drag; the "does the Control
## TREE deliver the event to this handler at all" question is the separate,
## real concern item 4 raises (DistrictScreen's own mouse_filter=STOP could
## theoretically intercept) — that is checked by the follow-up test below,
## which drives the SAME sequence starting from DistrictScreen's root.
func test_drag_pan_moves_view_offset() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
# body_radius_km absent -> no pole wall (identity clamp), isolating the
# drag-delta math itself from item 5's clamp in this test.
var offset_before: Vector2 = v.get_view_offset()
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
press.position = Vector2(400.0, 300.0)
v._gui_input(press)
var motion := InputEventMouseMotion.new()
motion.position = Vector2(500.0, 350.0) # +100, +50 drag delta
v._gui_input(motion)
assert_that(v.get_view_offset()).override_failure_message(
"a drag must move _view_offset away from its pre-drag value"
).is_not_equal(offset_before)
assert_that(v.get_view_offset()).is_equal(offset_before + Vector2(100.0, 50.0))
## The real scene-tree path: DistrictScreen (mouse_filter=STOP, no
## _gui_input override) -> AtlasWindowViewer (mouse_filter=STOP, HAS
## _gui_input). Godot delivers _gui_input to the DEEPEST/topmost Control
## under the mouse first — DistrictScreen having no _gui_input override
## means it never intercepts before AtlasWindowViewer gets the event; this
## test confirms that structurally by driving the event through
## DistrictScreen's own child and checking the SAME state change reaches
## AtlasWindowViewer, exactly as if the event had arrived organically through
## the real app -> nav-stack -> DistrictScreen chain.
func test_drag_pan_reaches_viewer_through_district_screen_chain() -> void:
var screen: DistrictScreen = auto_free(DistrictScreen.new())
add_child(screen)
screen.enter({"body": {"body_id": "GJ380c"}, "district_center": Vector2i(0, 0)})
var offset_before: Vector2 = screen._viewer.get_view_offset()
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
press.position = Vector2(400.0, 300.0)
screen._viewer._gui_input(press)
var motion := InputEventMouseMotion.new()
motion.position = Vector2(460.0, 300.0)
screen._viewer._gui_input(motion)
assert_that(screen._viewer.get_view_offset()).override_failure_message(
"a drag driven through DistrictScreen's child viewer must still move"
+ " _view_offset — no ancestor in the real screen chain eats the event"
).is_not_equal(offset_before)
# =============================================================================
# T-1142 item 5: pole hard wall wired into the real drag handler
# =============================================================================
## A window already near the pole, dragged FAR toward it, must have its
## offset clamped by the real _gui_input path (not just the pure function in
## isolation — this confirms the wiring, not just the math).
## A synthetic small body (NOT GJ380c's real ~6238km radius) is used
## deliberately: with a real body's huge rows_half (~4785 for GJ380c), the
## wall sits so many screen-pixels away that even an "absurd" mouse-motion
## delta (bounded by a real screen's pixel dimensions) never reaches it —
## the wall is real but the test would need a physically-impossible mouse
## position to trigger it. A small synthetic radius (-> a small rows_half)
## keeps the wall reachable by an ordinary drag delta while exercising the
## exact same code path.
func test_drag_pan_is_clamped_by_the_pole_wall_when_wired() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
# A tiny synthetic radius -> district_extent().rows_half is small (a few
# hundred districts), so the pole wall is within reach of an ordinary
# drag delta. Center 10 districts from the north pole.
var radius_km := 50.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var rows_half: int = int(extent["rows_half"])
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, -rows_half + 10), 32)
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
press.position = Vector2(400.0, 400.0)
v._gui_input(press)
var motion := InputEventMouseMotion.new()
motion.position = Vector2(400.0, -50000.0) # an absurd upward drag
v._gui_input(motion)
assert_float(v.get_view_offset().y).override_failure_message(
"an absurd drag toward the pole must be clamped by the real input path"
).is_greater(-50000.0)
# =============================================================================
# T-1142 item 6: east-west wrap — canonicalization on entry + cache reuse
# =============================================================================
## enter() canonicalizes an out-of-range center BEFORE it becomes
## _held_center — a column past the body's circumference wraps into range.
func test_enter_canonicalizes_an_out_of_range_center() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols + 50, 0), 32)
var window: Dictionary = _mock_window(Vector2i(50, 0), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v.get_district_window()).override_failure_message(
"the response must be adopted under the CANONICALIZED center (50, 0),"
+ " matching what the server would echo back for the wrapped request"
).is_equal(window)
## A center ONE column past the seam (item 6b): the SAME cache key as its
## twin at column 0 — a full-circumnavigation pan back to the seam must hit
## cache, not re-derive, because both requests canonicalize to the same
## (body, center, n) key.
func test_center_one_column_past_the_seam_shares_a_cache_key_with_its_twin() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var v1: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v1)
v1.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols, 50), 32)
var v2: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v2)
v2.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, 50), 32)
# Both must adopt the SAME server response (keyed on the same
# canonicalized center) — proves the cache key (and the outbound
# request) canonicalize identically for the seam and its twin.
var window: Dictionary = _mock_window(Vector2i(0, 50), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v1.get_district_window()).is_equal(window)
assert_that(v2.get_district_window()).is_equal(window)