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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user