Cover-fit: fit_window_view zooms from the viewport's LARGER dimension, no margin factor (any factor under 1.0 leaves a long-axis gap — checked numerically) — the composite fills edge to edge, overhanging the short axis into pan-space; the refloat center-equality early-return already prevents refetch churn at rest (proved, not just tested). Input model (Jeroen: drag breaks click semantics with map objects): LMB-drag pan REMOVED from the regional window; clicks are object-reserved. Pan = held WASD/arrows polled in _process (delta- and zoom-scaled, camera-pans-toward-key convention verified numerically) plus edge-scroll within 24px of the viewport border; both suppressed over UI and on OS focus loss; both set _user_adjusted; wheel zoom and Esc unchanged. Reads RAW physical keycodes deliberately — independent of the shared D-054 move_* InputMap actions bound to the same keys (whose occlusion-leak is pre-existing and now ticketed as T-1146). Pole wall + east-west wrap unchanged, re-driven through the new inputs; drag tests replaced, not kept. Smoothed composite (interim pending T-1143): per-cell colors bake into an n x n Image/ImageTexture (exact existing colorizer incl. overlay + ice tint) drawn once with LINEAR filtering — GPU bilinear reads as terrain, the planetary heightmap's own treatment. Crisp per-cell path preserved behind COMPOSITE_SMOOTH for T-1143 A/B. Rebuild only on reference-identity change of window/toggle (is_same — verified true reference equality; value-equal distinct dicts DO rebuild). Governance: T-1145 amendment paragraph on D-226 T-1124 SS5 (all three supersessions); pql decisions validate ok. Suites: window_viewer 74/74, window_geometry 32/32, window_overlay (new) 16/16; gdlint clean on all six files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
153 lines
6.5 KiB
GDScript
153 lines
6.5 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(), so a
|
|
## bare stub with just those three 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).
|
|
class _ViewerStub:
|
|
var window: Variant = null
|
|
var active_overlay: String = ""
|
|
|
|
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 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()
|