Files
settled-reach/client/tests/test_atlas_window_overlay.gd
T
jpmschweitzerandClaude Fable 5 c9028af77e feat(ui): T-1161 — per-rung composite filter policy (Region NEAREST, District/Quarter LINEAR)
Araminta's ruling (PR #192 follow-up): filter keyed on rung IDENTITY via
the window's own echoed granularity_v2 — Region (incl. the orbital tile
mosaic, whose tiles are all Region-rung requests) samples NEAREST because
GPU bilinear at 204.8 km/cell reads as smoothing-over-absence; District/
Quarter keep LINEAR where cell density earns the blend. One shared helper
(_filter_for_granularity_v2) at both draw call sites; unknown/missing
wire tags fall back to LINEAR (never trusted into NEAREST). COMPOSITE_
SMOOTH survives as the independent compile-time pipeline axis — the
two-axes split is documented in the file header. Washes/border fades
untouched per the ruling. Focused suite 44/44; revert-verified (helper
hardcoded LINEAR -> exactly the three Region-NEAREST tests fail).

Tickets: T-1161

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 22:36:05 +02:00

400 lines
18 KiB
GDScript

## T-1145 item 3 (interim presentation, pending the T-1143 design pass):
## tests for AtlasWindowOverlay's smoothed-composite texture rebuild cache —
## the "rebuild ONLY when window/overlay/tint inputs change, not per frame"
## requirement. Does not test the actual PIXEL CONTENT of the built texture
## (that content is exactly _cell_color()/_apply_glaciation(), already
## covered by test_atlas_window_colors.gd's colorizer tests — this file is
## about WHEN a rebuild happens, not what color a given cell produces).
class_name TestAtlasWindowOverlay
extends GdUnitTestSuite
static func _mock_window(n: int = 2) -> Dictionary:
return {
"center": [0, 0],
"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]),
}
## Minimal viewer stub — AtlasWindowOverlay only reaches the viewer through
## get_district_window()/is_overlay_visible()/get_cell_pixel_size()/
## is_tile_mode(), so a bare stub with just those methods is a legitimate
## "viewer" for these tests, matching the duck-typed-viewer precedent this
## whole overlay cluster already relies on (atlas_overlay_bar.gd/
## atlas_legend_panel.gd). is_tile_mode() always returns false — this suite
## covers the single-window composite-cache path only; the tile mosaic path
## is covered separately by test_atlas_window_tile_set.gd + the viewer's own
## is_tile_mode()-branching tests.
class _ViewerStub:
var window: Variant = null
var active_overlay: String = ""
# T-1161: the viewer's currently-HELD rung tag — added alongside the
# per-rung filter tests below. Not read by AtlasWindowOverlay today (the
# overlay trusts each window dict's OWN echoed granularity_v2, per
# cell_grid_side_for_window()'s precedent) but a real AtlasWindowViewer
# exposes get_held_granularity_v2() (T-1153), so the stub carries it too
# for parity with the real duck-typed interface.
var held_granularity_v2: String = "District"
func get_district_window() -> Variant:
return window
func is_overlay_visible(overlay_id: String) -> bool:
return overlay_id == active_overlay
func get_cell_pixel_size() -> float:
return 16.0
func is_tile_mode() -> bool:
return false
func get_held_granularity_v2() -> String:
return held_granularity_v2
## T-1161 reframe: COMPOSITE_SMOOTH is now Axis 1 of TWO independent axes
## (see the file header doc) — the texture-vs-flat-rects PIPELINE choice.
## This assertion survives unchanged: the composite is still a TEXTURE at
## every rung. Axis 2 (which FILTER that texture samples with) is now a
## per-rung runtime decision covered separately below by the
## _filter_for_granularity_v2() tests — it is no longer bundled into this
## compile-time const.
func test_composite_smooth_defaults_true() -> void:
assert_bool(AtlasWindowOverlay.COMPOSITE_SMOOTH).override_failure_message(
"T-1145 item 3 ships the smoothed composite as the DEFAULT presentation"
).is_true()
## A fresh overlay with no draw yet has never built a texture — the cache
## starts empty.
func test_no_texture_before_first_draw() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
assert_that(o._cached_texture).is_null()
## First _rebuild_texture_if_needed() call for a real window builds a texture.
func test_rebuild_builds_a_texture_on_first_call() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
assert_that(o._cached_texture).override_failure_message(
"the first rebuild call for a real window must produce a texture"
).is_not_null()
## Calling _rebuild_texture_if_needed() AGAIN with the SAME window object
## (same reference) and the same active toggle must NOT rebuild — the exact
## same ImageTexture instance survives (reference equality, not just
## "another texture that happens to look the same").
func test_rebuild_is_a_noop_when_window_and_toggle_are_unchanged() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
var first_texture: ImageTexture = o._cached_texture
o._rebuild_texture_if_needed(window, 2, "")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"an unchanged (window, active_toggle) pair must reuse the SAME texture"
+ " object, not rebuild an equivalent-but-new one"
).is_true()
## A DIFFERENT window object (even with identical field VALUES) — matching
## what a fresh server response always is, a new Dictionary — MUST trigger a
## rebuild. This is the reference-vs-value distinction the class doc calls
## out explicitly (is_same(), not a deep compare).
func test_rebuild_fires_for_a_different_window_object_with_same_values() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window_a: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window_a, 2, "")
var first_texture: ImageTexture = o._cached_texture
# A structurally-IDENTICAL but DISTINCT Dictionary object — the exact
# shape a second server response for the same window content would be.
var window_b: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window_b, 2, "")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"a new window object (even with identical field values) must trigger a"
+ " fresh rebuild — the cache key is REFERENCE identity, not value equality"
).is_false()
## Changing the active toggle overlay (same window object) must ALSO trigger
## a rebuild — temp/moisture/veg/base each read different colors per cell.
func test_rebuild_fires_when_active_toggle_changes() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
var first_texture: ImageTexture = o._cached_texture
o._rebuild_texture_if_needed(window, 2, "gen_dw_temp")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"switching the active toggle overlay must trigger a rebuild — the SAME"
+ " window's cells read different colors under a different toggle"
).is_false()
## Panning/zooming (which redraw this node constantly via _apply_transform())
## never touches window/overlay state — repeated rebuild CALLS with identical
## inputs (simulating many redraws while nothing about the DATA changed) must
## all be no-ops after the first, confirming the "not per frame" requirement
## end to end, not just for a single repeat.
func test_repeated_rebuild_calls_with_unchanged_inputs_all_reuse_the_same_texture() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
var first_texture: ImageTexture = o._cached_texture
for _i in range(20): # 20 simulated redraws (pan/zoom frames)
o._rebuild_texture_if_needed(window, 2, "")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"20 repeated rebuild calls with unchanged inputs must never touch the cache"
).is_true()
## The overlay's real _draw() entry point (via the smoothed path) produces a
## texture through the SAME _ViewerStub duck-typed interface every other
## caller in this cluster uses — an end-to-end sanity check that _draw()
## actually reaches _rebuild_texture_if_needed() for a real window, not just
## that the helper works in isolation.
func test_draw_builds_a_texture_through_the_viewer_stub() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.window = _mock_window()
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
# =============================================================================
# T-1161: _filter_for_granularity_v2() — the per-rung sampling filter policy
# (Araminta's ruling: Region incl. the orbital tile mosaic -> NEAREST;
# District/Quarter -> LINEAR; no hysteresis, no px-per-cell threshold, keyed
# purely on rung IDENTITY).
# =============================================================================
## Region is the sparse rung the ruling targets — GPU bilinear blending at
## 204.8 km/cell reads as smoothing-over-absence, so it samples NEAREST.
func test_filter_for_granularity_v2_region_is_nearest() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Region")).override_failure_message(
"Region must sample TEXTURE_FILTER_NEAREST — dense-enough rungs get LINEAR,"
+ " Region is the sparse one the ruling targets"
).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
## District is dense enough that the bilinear blend reads as texture, not as
## papering over sparse data — LINEAR is earned.
func test_filter_for_granularity_v2_district_is_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("District")).override_failure_message(
"District must sample TEXTURE_FILTER_LINEAR"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## Quarter (4x MORE cells than District) is denser still — also LINEAR.
func test_filter_for_granularity_v2_quarter_is_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Quarter")).override_failure_message(
"Quarter must sample TEXTURE_FILTER_LINEAR"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## An unrecognized/empty tag must NEVER be trusted into the crisp NEAREST
## treatment — mirrors cell_grid_side_for_window()'s own "unknown -> District"
## fallback posture, failing toward the already-shipped LINEAR look rather
## than an unintended NEAREST for a wire shape this code doesn't recognize.
func test_filter_for_granularity_v2_unknown_falls_back_to_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("")).override_failure_message(
"an empty/unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("SomeFutureRung")).override_failure_message(
"an unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## Integration-shaped: a Region-rung window dict, drawn through the real
## _draw() entry point via the _ViewerStub duck-typed interface (same
## end-to-end shape as test_draw_builds_a_texture_for_a_region_rung_window()
## above), must drive the NODE's own texture_filter property to NEAREST —
## not just the helper function in isolation.
func test_draw_sets_node_texture_filter_to_nearest_for_region_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.held_granularity_v2 = "Region"
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_int(o.texture_filter).override_failure_message(
"a Region-rung window must drive the node's texture_filter to NEAREST after a draw"
).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
## The District-rung counterpart of the above — confirms the node's
## texture_filter lands on LINEAR (not left over from a previous NEAREST
## draw, and not defaulting to NEAREST) for the dense rung.
func test_draw_sets_node_texture_filter_to_linear_for_district_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.window = _mock_window() # District (the default/omitted tag)
o.viewer = stub
o._draw()
assert_int(o.texture_filter).override_failure_message(
"a District-rung window must drive the node's texture_filter to LINEAR after a draw"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## A rung SWAP (Region -> District, on the SAME node) must flip texture_filter
## along with it — confirms the property is recomputed every draw, not
## sticky from the first rung the node ever rendered.
func test_draw_flips_node_texture_filter_on_a_rung_swap() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
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_int(o.texture_filter).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
stub.window = _mock_window() # swap to District
o._draw()
assert_int(o.texture_filter).override_failure_message(
"swapping to a District-rung window must flip texture_filter to LINEAR,"
+ " not leave it stuck at the previous rung's NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)