Two viewer fixes from the same pair session; they share step_canvas_viewer.gd so they land together. FETCH READOUT. A cold derive takes seconds and the map gave no honest sign of it. Root cause found while building the replacement: a Control paints its own _draw() BEFORE its children, so everything the viewer drew itself — the old DERIVING TERRAIN label AND the pending wash — was painted UNDER the terrain canvas, visible only when no texture existed at all, i.e. never in the slow-fetch case they existed for. That has been the state since the stepped viewer shipped. The readout now lives in its own overlay node added after the canvas: a centered implant-idiom panel, DOWNLOADING MAP DATA, indeterminate sweep, 250 ms grace so cache hits never flash it, held canvas still drawing beneath. Indeterminate by design — the server reports no derive sub-steps, and a progress fraction we cannot source would be invented. ONE SETTLE TIMER, THREE TRIGGERS. The viewer never re-requested a canvas on resize, so one derived for a smaller window letterboxed forever in a bigger one — the map not filling the frame. And the pan-edge refetch fired from inside the per-frame pan loop the instant its threshold was crossed, so a held edge-scroll issued a fresh request AND snapped the view on every frame past it. Both are the same event: the user is still moving. A shared one-shot timer now collapses them into a single request at the resting state, with a hard pan threshold that still fires immediately when the canvas edge is about to enter view (waiting there would show empty background), and drifting back inside the soft threshold cancels the pending request. Resize reaches this Control identically whether the OS window or a diegetic in-implant parent changed, so both sources are covered. Two new tests pin the soft path (schedules, does not refloat or snap) and its cancellation; the pre-existing threshold test was verified to still discriminate — its drift trips the new hard threshold — rather than passing vacuously. Co-Authored-By: Claude <noreply@anthropic.com>
980 lines
45 KiB
GDScript
980 lines
45 KiB
GDScript
## T-1182 tests: StepCanvasViewer — the rung transport state machine
|
|
## (enter() lands on the Global opener, scroll steps through the ladder,
|
|
## overlay toggle wiring, pan-edge re-request, edge-scroll) and
|
|
## RegionalScreen's re-entry guard against the new viewer. test_mode
|
|
## (SimBridge default outside SR_LIVE=1) means request_step_canvas() is a
|
|
## silent no-op — these tests exercise client-side state only, no live
|
|
## server needed.
|
|
class_name TestStepCanvasViewer
|
|
extends GdUnitTestSuite
|
|
|
|
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
|
|
|
|
## GJ1c's own region-grid shape from the T-1183 eyeball (177x88) — reused
|
|
## across the T-1189/T-1192 section below so every test is grounded in the
|
|
## actual regression captured in
|
|
## .cache/screenshots/t1183-eyeball-run2/02-region.png.
|
|
const GJ1C_GLOBAL_EXTENT := Vector2i(177, 88)
|
|
|
|
## Isolated Tier-2/3 disk-cache root for every directly-constructed viewer
|
|
## (T-1193 first slice). The default user://atlas_cache/ is SHARED across the
|
|
## whole machine — every worktree's gate run, live capture driver, and real
|
|
## play session writes the same directory — and T-1183's disk-cache lookup
|
|
## short-circuits BEFORE test_mode's silent-no-op IPC, so a warm shared cache
|
|
## delivers real canvases into tests written against "nothing ever arrives"
|
|
## (2026-07-25: a concurrent live GJ380c Global capture flipped the two
|
|
## before-any-canvas tests in another worktree's gate run). In test_mode the
|
|
## isolated root stays empty forever: no canvas ever arrives, so the cache
|
|
## never writes.
|
|
const TEST_DISK_CACHE_ROOT := "user://test_step_canvas_viewer_cache/"
|
|
|
|
|
|
func _make_viewer() -> StepCanvasViewer:
|
|
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
|
v.disk_cache_root_override = TEST_DISK_CACHE_ROOT
|
|
return v
|
|
|
|
|
|
func test_enter_lands_on_the_global_opener() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
|
|
func test_get_body_id_reflects_the_entered_body() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
assert_str(v.get_body_id()).is_equal("")
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_str(v.get_body_id()).is_equal("GJ380c")
|
|
|
|
|
|
## Scrolling one notch descends the ladder — cursor-anchored, so a cursor
|
|
## position must be supplied; the rung index advances by exactly one.
|
|
func test_scroll_rung_descends_one_notch() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
|
|
|
|
|
|
func test_scroll_rung_clamps_at_the_deepest_rung() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
for _i in range(10):
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_CHUNK)
|
|
|
|
|
|
func test_reset_to_global_returns_from_a_deep_rung() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
v._reset_to_global()
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
|
|
## PR #203 review (Hoshe finding 2): the hard full-zoom-out reset — a
|
|
## scroll-out gesture while ALREADY at Global (rung 0), with the view
|
|
## drifted from the canonical un-panned frame, must snap the view back to
|
|
## center (Jeroen's explicit HARD condition, carried from the retired
|
|
## viewer's own _maybe_reset_to_canonical_frame()). Behavioral, through the
|
|
## real input entry point (_scroll_rung with direction=-1), not a direct
|
|
## _reset_to_global() call — this is what would have caught the dead-code
|
|
## regression (scroll_step() clamping at index 0 meant _scroll_rung()
|
|
## returned before ever reaching a reset call).
|
|
func test_scroll_out_at_global_after_a_pan_resets_the_view() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
v._apply_pan_delta(Vector2(1.0, 0.0), 1.0) # drift the view off-center
|
|
assert_bool(v._is_global_view_drifted()).override_failure_message(
|
|
"test setup: a pan at Global must actually drift the view"
|
|
).is_true()
|
|
|
|
v._scroll_rung(-1, Vector2(400.0, 300.0)) # scroll OUT — already at rung 0
|
|
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
assert_bool(v._is_global_view_drifted()).override_failure_message(
|
|
"a scroll-out past the top of the ladder must hard-reset the drifted"
|
|
+ " Global view back to its canonical (centered) frame"
|
|
).is_false()
|
|
|
|
|
|
## The inverse guard: scrolling out while ALREADY at the canonical
|
|
## (un-drifted) Global frame must stay a no-op — the reset is edge-triggered
|
|
## on genuine drift, not a per-scroll unconditional reset.
|
|
func test_scroll_out_at_undrifted_global_is_a_no_op() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(-1, Vector2(400.0, 300.0))
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
assert_bool(v._is_global_view_drifted()).is_false()
|
|
|
|
|
|
func test_overlay_visibility_defaults_to_off_for_every_toggle() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
for def: Dictionary in v.get_overlay_defs():
|
|
assert_bool(v.is_overlay_visible(def["id"])).is_false()
|
|
|
|
|
|
func test_set_overlay_visible_updates_state() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.set_overlay_visible("gen_dw_temp", true)
|
|
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_true()
|
|
|
|
|
|
func test_set_overlay_visible_unknown_id_is_a_no_op() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.set_overlay_visible("not_a_real_overlay", true)
|
|
assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false()
|
|
|
|
|
|
# =============================================================================
|
|
# RegionalScreen re-entry guard (BUG 2 lineage, carried forward from the
|
|
# retired AtlasWindowViewer-era regression) — now against StepCanvasViewer.
|
|
# =============================================================================
|
|
|
|
|
|
func test_regional_screen_repeat_enter_for_the_same_body_is_a_no_op() -> void:
|
|
var screen: RegionalScreen = auto_free(RegionalScreen.new())
|
|
add_child(screen)
|
|
var body: Dictionary = {"body_id": "GJ380c", "body_radius_km": 6238.4}
|
|
screen.enter({"body": body, "system": {}})
|
|
screen._viewer._scroll_rung(1, Vector2(400.0, 300.0))
|
|
assert_str(screen._viewer.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
|
|
|
|
screen.enter({"body": body, "system": {}})
|
|
|
|
# A no-op re-entry must NOT reset the held rung back to Global — that
|
|
# would be the exact "repeat enter tears down in-flight state" class the
|
|
# retired viewer's own cold-start guard existed to prevent.
|
|
assert_str(screen._viewer.get_held_rung()).override_failure_message(
|
|
"a repeat enter() for the SAME body must not reset the held rung"
|
|
).is_equal(StepCanvasTransport.RUNG_REGION)
|
|
|
|
|
|
func test_regional_screen_different_body_still_re_enters() -> void:
|
|
var screen: RegionalScreen = auto_free(RegionalScreen.new())
|
|
add_child(screen)
|
|
screen.enter({"body": {"body_id": "GJ380c", "body_radius_km": 6238.4}, "system": {}})
|
|
|
|
screen.enter({"body": {"body_id": "OtherBody", "body_radius_km": 100.0}, "system": {}})
|
|
|
|
assert_str(screen._viewer.get_body_id()).is_equal("OtherBody")
|
|
|
|
|
|
## Relocated from test_atlas_descend_entry.gd (T-1182 PR #203 review — the
|
|
## AtlasViewer cluster orphan retirement, Tyre finding). This is the one live
|
|
## regression guard from that suite: RegionalScreen must wrap StepCanvasViewer
|
|
## (the stepped ladder), never fall back to the now-deleted AtlasViewer
|
|
## heightmap-texture display — a direct type-identity check, distinct from
|
|
## the behavioral tests above (which would only fail indirectly, via a
|
|
## missing method, if this ever regressed).
|
|
func test_regional_screen_wraps_step_canvas_viewer_not_atlas_viewer() -> void:
|
|
var screen: RegionalScreen = auto_free(RegionalScreen.new())
|
|
add_child(screen)
|
|
assert_object(screen._viewer).override_failure_message(
|
|
"RegionalScreen must wrap StepCanvasViewer (the stepped ladder) since T-1182,"
|
|
+ " not the retired AtlasViewer heightmap-texture display"
|
|
).is_instanceof(StepCanvasViewer)
|
|
|
|
|
|
# =============================================================================
|
|
# PR #203 review (Hoshe notes): pan-edge re-request (_maybe_refloat) and
|
|
# edge-scroll pan — previously untested. _maybe_refloat() only does anything
|
|
# once the terrain layer holds a real texture (get_footprint_px() is
|
|
# ZERO/inert until then), so these tests drive StepCanvasTerrainLayer.
|
|
# rebuild_from_canvas() directly (bypassing the network — a decoded canvas
|
|
# dict is all it needs) to put the viewer into the "holding a real canvas"
|
|
# state _maybe_refloat's early-out guards against.
|
|
# =============================================================================
|
|
|
|
|
|
static func _synthetic_canvas(width: int, height: int) -> Dictionary:
|
|
return {
|
|
"width": width,
|
|
"height": height,
|
|
"morphology": null,
|
|
"elev_q": null,
|
|
"moisture_q": null,
|
|
"vegetation": null,
|
|
"glaciation": null,
|
|
"temp_dc": [],
|
|
"settlement_id": [],
|
|
"courses": [],
|
|
"cliffs": [],
|
|
}
|
|
|
|
|
|
func test_maybe_refloat_is_inert_before_any_canvas_has_arrived() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # District — footprint still ZERO, nothing arrived
|
|
var world_center_before: Vector2 = v._world_center
|
|
v._maybe_refloat()
|
|
assert_that(v._world_center).is_equal(world_center_before)
|
|
|
|
|
|
## A small pan (well under half the canvas footprint) must NOT re-float —
|
|
## the held canvas keeps drawing, no re-request (§4/§5's "only when a pan
|
|
## carries the view past the held window's edge").
|
|
func test_maybe_refloat_does_not_refloat_on_a_small_pan() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0) # a real viewport size — _maybe_refloat's
|
|
# drift math is relative to get_rect().size's own center; leaving this at
|
|
# the default ZERO would make screen_center ZERO too, so even a tiny
|
|
# view_offset reads as "drifted past the canvas's own half-footprint"
|
|
# (drift = view_offset + half, threshold = half*0.5) — a test-harness
|
|
# artifact, not the behavior under test.
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # District
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(64, 64), v.get_held_rung(), "")
|
|
# _scroll_rung() re-centers view_offset to ZERO on arrival, which under a
|
|
# real 800x600 viewport already puts the canvas center near screen center
|
|
# (both small relative to the viewport) — center the canvas explicitly so
|
|
# "small pan" starts from a known-centered baseline.
|
|
v._view_offset = v.size * 0.5 - v._terrain_layer.get_footprint_px() * 0.5
|
|
|
|
var world_center_before: Vector2 = v._world_center
|
|
v._view_offset += Vector2(2.0, 0.0) # tiny drift, far under half the footprint
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).override_failure_message(
|
|
"a small pan must not re-float the held canvas"
|
|
).is_equal(world_center_before)
|
|
|
|
|
|
## A large pan (past half the canvas footprint) DOES re-float — new
|
|
## world_center, view_offset reset to ZERO (the canvas re-centers under the
|
|
## new request).
|
|
func test_maybe_refloat_refloats_once_the_pan_crosses_the_hard_threshold() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # District
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(64, 64), v.get_held_rung(), "")
|
|
|
|
var world_center_before: Vector2 = v._world_center
|
|
var footprint: Vector2 = v._terrain_layer.get_footprint_px()
|
|
v._view_offset = Vector2(footprint.x, 0.0) # far past half the footprint
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).override_failure_message(
|
|
"a pan past the edge threshold must re-float (new world_center)"
|
|
).is_not_equal(world_center_before)
|
|
assert_that(v._view_offset).override_failure_message(
|
|
"re-floating resets view_offset to ZERO (the canvas re-centers)"
|
|
).is_equal(Vector2.ZERO)
|
|
|
|
|
|
## Pair session 2026-07-26 — the SOFT threshold: a pan that crosses it must
|
|
## NOT re-float on the spot. It schedules the shared settle timer and leaves
|
|
## the view alone, so a held edge-scroll keeps panning the current canvas
|
|
## smoothly and issues ONE request when it stops, instead of a request plus a
|
|
## view snap on every frame past the threshold (the old behavior).
|
|
func test_soft_pan_drift_schedules_the_settle_instead_of_refloating_now() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # District
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(512, 384), v.get_held_rung(), "")
|
|
v._view_offset = v._centered_view_offset()
|
|
|
|
# Drift to just past SOFT but well short of HARD.
|
|
var half: Vector2 = v._terrain_layer.get_footprint_px() * 0.5
|
|
var target: float = (
|
|
StepCanvasViewer.PAN_REFLOAT_SOFT_FRACTION + StepCanvasViewer.PAN_REFLOAT_HARD_FRACTION
|
|
) * 0.5
|
|
var screen_center: Vector2 = v.get_rect().size * 0.5
|
|
v._view_offset = screen_center - half + Vector2(half.x * target, 0.0)
|
|
|
|
var fraction: float = v._pan_drift_fraction()
|
|
assert_float(fraction).override_failure_message(
|
|
"test setup: drift must land between the soft and hard thresholds"
|
|
).is_between(StepCanvasViewer.PAN_REFLOAT_SOFT_FRACTION, StepCanvasViewer.PAN_REFLOAT_HARD_FRACTION)
|
|
|
|
var center_before: Vector2 = v._world_center
|
|
var offset_before: Vector2 = v._view_offset
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).override_failure_message(
|
|
"a soft-threshold pan must NOT re-float immediately"
|
|
).is_equal(center_before)
|
|
assert_that(v._view_offset).override_failure_message(
|
|
"a soft-threshold pan must not snap the view — the held canvas keeps panning"
|
|
).is_equal(offset_before)
|
|
assert_bool(v._refetch_settle_timer.is_stopped()).override_failure_message(
|
|
"a soft-threshold pan must schedule the shared settle timer"
|
|
).is_false()
|
|
|
|
|
|
## The inverse: panning back inside the soft threshold cancels the pending
|
|
## settle — the view no longer wants a different canvas, so the request that
|
|
## was about to go out must not.
|
|
func test_panning_back_inside_the_soft_threshold_cancels_the_pending_settle() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0))
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(512, 384), v.get_held_rung(), "")
|
|
|
|
v._refetch_settle_timer.start() # pretend a soft crossing already scheduled one
|
|
v._view_offset = v._centered_view_offset() # centred == zero drift
|
|
v._maybe_refloat()
|
|
|
|
assert_bool(v._refetch_settle_timer.is_stopped()).override_failure_message(
|
|
"drifting back inside the soft threshold must cancel the pending refetch"
|
|
).is_true()
|
|
|
|
|
|
## Global never re-floats on pan (D-255(a): its canvas is the whole body,
|
|
## no edge to cross) — even with a real texture held and a huge drift.
|
|
func test_maybe_refloat_is_a_no_op_at_global_rung() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._terrain_layer.rebuild_from_canvas(_synthetic_canvas(200, 100), v.get_held_rung(), "")
|
|
|
|
var world_center_before: Vector2 = v._world_center
|
|
v._view_offset = Vector2(9_999.0, 9_999.0)
|
|
v._maybe_refloat()
|
|
|
|
assert_that(v._world_center).is_equal(world_center_before)
|
|
|
|
|
|
# =============================================================================
|
|
# Edge-scroll: suppression conditions + direction.
|
|
# =============================================================================
|
|
|
|
|
|
func test_edge_scroll_suppressed_without_application_focus() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = false
|
|
v._last_mouse_pos = Vector2(2.0, 300.0) # well inside the edge margin
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_false()
|
|
|
|
|
|
func test_edge_scroll_suppressed_when_cursor_has_never_moved_over_the_control() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
# _last_mouse_pos defaults to (-1, -1) — an impossible in-bounds position,
|
|
# so edge-scroll never fires before the mouse has moved over the control
|
|
# at least once (matches the retired viewer's own documented contract).
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_false()
|
|
|
|
|
|
func test_edge_scroll_active_near_the_left_edge() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = true
|
|
v._last_mouse_pos = Vector2(2.0, 300.0)
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_true()
|
|
var direction: Vector2 = v._edge_scroll_direction()
|
|
assert_float(direction.x).is_less(0.0)
|
|
assert_float(direction.y).is_equal(0.0)
|
|
|
|
|
|
func test_edge_scroll_active_near_the_right_edge() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = true
|
|
v._last_mouse_pos = Vector2(798.0, 300.0)
|
|
var direction: Vector2 = v._edge_scroll_direction()
|
|
assert_float(direction.x).is_greater(0.0)
|
|
|
|
|
|
func test_edge_scroll_inactive_well_inside_the_viewport() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(800.0, 600.0)
|
|
v._app_has_focus = true
|
|
v._last_mouse_pos = Vector2(400.0, 300.0) # dead center — far from any edge
|
|
assert_bool(v._is_cursor_edge_scrolling()).is_false()
|
|
|
|
|
|
# =============================================================================
|
|
# T-1183: disk-cache sweep-trigger wiring
|
|
# =============================================================================
|
|
|
|
|
|
## enter() must invoke the disk cache's (2a) visit sweep for the entered
|
|
## body — a smoke test that the wiring exists and doesn't crash; the sweep
|
|
## LOGIC itself (what gets evicted and why) is covered exhaustively by
|
|
## test_step_canvas_disk_cache.gd. Uses a distinctive body_id with nothing
|
|
## ever cached under it, so the sweep is a true no-op read (no writes to the
|
|
## real user://atlas_cache/ directory this test could leak).
|
|
func test_enter_runs_the_disk_cache_visit_sweep_without_crashing() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "T1183_sweep_smoke_test_body", "body_radius_km": 6238.4}, {})
|
|
# If the wiring is broken (e.g. calling a method that doesn't exist), the
|
|
# enter() call itself would already have failed above — reaching here
|
|
# with the expected held rung is the assertion.
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
|
|
## The coarse background sweep timer exists, is not per-frame (a real
|
|
## Timer node, not a _process()-driven counter), autostarts, and is set to
|
|
## the documented coarse interval — never a sub-frame or per-frame value.
|
|
func test_disk_sweep_timer_is_coarse_and_autostarts() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
var timer: Timer = v.get_node("DiskSweepTimer")
|
|
assert_object(timer).is_not_null()
|
|
# Godot resets Timer.autostart to false once the timer has actually
|
|
# started after entering the tree (documented engine behavior — the flag
|
|
# is a one-shot "start me on _ready()" instruction, not a persistent
|
|
# state mirror). The real behavioral guarantee is "the timer is running,
|
|
# unpaused, without anyone having to call start() explicitly" —
|
|
# is_stopped() == false is the correct read of that.
|
|
assert_bool(timer.is_stopped()).override_failure_message(
|
|
"the disk sweep timer must autostart running, no explicit start() call needed"
|
|
).is_false()
|
|
assert_float(timer.wait_time).override_failure_message(
|
|
"the disk sweep timer must be coarse (minutes), never a per-frame interval"
|
|
).is_greater_equal(60.0)
|
|
|
|
|
|
## The timer's timeout must actually route to the disk cache's
|
|
## run_background_sweep() for the currently-entered body — verified by
|
|
## invoking the private handler directly (the same "call the handler, don't
|
|
## wait on a real Timer" pattern used elsewhere in this cluster for
|
|
## non-blocking test speed) against an injected-root request so this test
|
|
## touches no real cache files.
|
|
func test_disk_sweep_timeout_handler_runs_background_sweep_for_the_current_body() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "T1183_sweep_smoke_test_body", "body_radius_km": 6238.4}, {})
|
|
# No live server, no cached entries for this body — the assertion is
|
|
# that calling the handler does not crash and leaves the (empty) cache
|
|
# consistent, mirroring the enter()-sweep smoke test above.
|
|
v._on_disk_sweep_timeout()
|
|
assert_int(v.get_request().get_disk_cache().entry_count("T1183_sweep_smoke_test_body")).is_equal(0)
|
|
|
|
|
|
# =============================================================================
|
|
# T-1189: extent cap wired end-to-end (viewer -> transport), plus the
|
|
# cache-key consistency the ticket calls out explicitly ("capping happens
|
|
# BEFORE the request is issued so keys stay consistent"). No live server —
|
|
# a Global canvas is landed via the SAME Tier-1 cache-hit path
|
|
# StepCanvasRequest's own tests use (get_cache().put() + request_now()'s
|
|
# synchronous cache-hit emit), so the full _on_canvas_ready wiring runs for
|
|
# real rather than being shortcut.
|
|
# =============================================================================
|
|
|
|
|
|
## Land a Global canvas of the given size into the viewer's OWN cache (Tier
|
|
## 1), then fire the request that the real cache-hit path serves
|
|
## synchronously — same mechanism test_step_canvas_request.gd's own
|
|
## cache-hit tests use, now driven through the viewer so _on_canvas_ready()
|
|
## and _global_body_extent actually populate through the real signal wiring.
|
|
static func _land_global_canvas(v: StepCanvasViewer, width: int, height: int) -> void:
|
|
var req: Variant = v.get_request()
|
|
req.get_cache().put(
|
|
v.get_body_id(),
|
|
"Global",
|
|
Vector2i.ZERO,
|
|
Vector2i.ZERO,
|
|
TestStepCanvasViewer._synthetic_canvas(width, height)
|
|
)
|
|
v._fire_request() # Global's own request — served from the cache hit just landed
|
|
|
|
|
|
func test_global_canvas_arrival_populates_the_body_extent_cap_source() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
assert_that(v._global_body_extent).is_equal(GJ1C_GLOBAL_EXTENT)
|
|
|
|
|
|
## The T-1183 eyeball regression itself: once the Global echo has landed,
|
|
## scrolling to Region and firing its request must produce a CAPPED extent
|
|
## — never the raw viewport-fit 384x216 that overran the body on both axes.
|
|
func test_region_request_extent_is_capped_to_the_landed_global_extent() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0) # the T-1183 eyeball's own viewport
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
v._scroll_rung(1, Vector2(960.0, 540.0)) # descend to Region — fires the capped request
|
|
var extent: Vector2i = v._request_extent()
|
|
|
|
assert_int(extent.x).override_failure_message(
|
|
"Region's requested extent must never exceed the body's own region-grid width"
|
|
).is_less_equal(GJ1C_GLOBAL_EXTENT.x)
|
|
assert_int(extent.y).override_failure_message(
|
|
"Region's requested extent must never exceed the body's own region-grid height"
|
|
).is_less_equal(GJ1C_GLOBAL_EXTENT.y)
|
|
assert_that(extent).is_equal(GJ1C_GLOBAL_EXTENT) # 1920x1080 viewport-fit exceeds 177x88 on both axes
|
|
|
|
|
|
## Cold-start fallback (documented on StepCanvasViewer._request_extent()):
|
|
## before ANY Global response has landed, `_global_body_extent` is still
|
|
## ZERO — the Region request must go out UNCAPPED (server clamps
|
|
## independently) rather than silently collapsing to a zero-cell request.
|
|
func test_region_request_extent_is_uncapped_before_the_global_echo_lands() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
# No _land_global_canvas() call — simulates scrolling in before the
|
|
# FIRST (Global) request's response has arrived.
|
|
|
|
v._scroll_rung(1, Vector2(960.0, 540.0))
|
|
var extent: Vector2i = v._request_extent()
|
|
|
|
var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "Region")
|
|
assert_that(extent).override_failure_message(
|
|
"before the Global echo lands, the Region request must be the ordinary"
|
|
+ " uncapped viewport-fit extent, not silently zeroed"
|
|
).is_equal(uncapped)
|
|
|
|
|
|
## Cache-key discipline (T-1182/T-1183, ticket's own explicit call-out): the
|
|
## cap must be applied BEFORE _fire_request() builds the request, so the
|
|
## extent that becomes part of the cache key is the SAME capped value that
|
|
## gets served — a request for "the same spot" must always resolve to the
|
|
## same key, capped or not, never a key built from one extent and served
|
|
## under another.
|
|
func test_capped_extent_matches_what_the_request_actually_sends() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
v._scroll_rung(1, Vector2(960.0, 540.0)) # Region — fires _fire_request() internally
|
|
|
|
# _request_extent() is the SAME function _fire_request() calls to build
|
|
# the outbound request/cache key — calling it again here must be
|
|
# idempotent and match what was actually requested (no separate,
|
|
# divergent cap path).
|
|
var extent_now: Vector2i = v._request_extent()
|
|
assert_that(extent_now).is_equal(GJ1C_GLOBAL_EXTENT)
|
|
|
|
# The Tier-1 cache key StepCanvasCache builds from this SAME extent must
|
|
# be a real, findable key once a response for it lands — proving the
|
|
# capped extent (not the raw viewport-fit one) is what keys the cache.
|
|
var req: Variant = v.get_request()
|
|
var center: Vector2i = StepCanvasTransport.snap_to_gridunit(v._world_center, "Region")
|
|
var canvas := TestStepCanvasViewer._synthetic_canvas(GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
req.get_cache().put("T1189_extent_letterbox_test_body", "Region", center, extent_now, canvas)
|
|
assert_bool(
|
|
req.get_cache().has("T1189_extent_letterbox_test_body", "Region", center, GJ1C_GLOBAL_EXTENT, 0)
|
|
).override_failure_message(
|
|
"a cache entry stored under the CAPPED extent must be reachable"
|
|
+ " under that same capped extent — key consistency"
|
|
).is_true()
|
|
|
|
|
|
## Shape-generic guard: District's spacing differs from Global's, so its
|
|
## request extent must be completely unaffected by a landed Global canvas —
|
|
## proving the cap is spacing-keyed, not applied indiscriminately to every
|
|
## rung once a Global extent is known.
|
|
func test_district_request_extent_is_never_capped_by_the_global_extent() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
v._scroll_rung(1, Vector2(960.0, 540.0)) # Region
|
|
v._scroll_rung(1, Vector2(960.0, 540.0)) # District
|
|
var extent: Vector2i = v._request_extent()
|
|
|
|
var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "District")
|
|
assert_that(extent).is_equal(uncapped)
|
|
|
|
|
|
## Hoshe (review round 2): _global_body_extent must NOT leak across a body
|
|
## switch — land a Global extent for body A, enter() body B, and prove BOTH
|
|
## that the cap source itself reads back ZERO for the new body AND that a
|
|
## Region request for body B before ITS OWN Global echo lands goes out
|
|
## UNCAPPED (never silently capped by body A's leftover grid). The reset
|
|
## already exists at StepCanvasViewer.enter() ("a new body has its own
|
|
## region grid") — this proves it, through the real enter()/land/enter()
|
|
## sequence rather than asserting the field directly only.
|
|
func test_global_body_extent_resets_on_a_different_body_and_does_not_leak() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
assert_that(v._global_body_extent).override_failure_message(
|
|
"test setup: body A must actually have a landed cap source"
|
|
).is_equal(GJ1C_GLOBAL_EXTENT)
|
|
|
|
v.enter({"body_id": "T1189_extent_cap_body_b", "body_radius_km": 3000.0}, {})
|
|
|
|
assert_that(v._global_body_extent).override_failure_message(
|
|
"entering a DIFFERENT body must reset the cap source to ZERO — body"
|
|
+ " A's region grid must never leak into body B's requests"
|
|
).is_equal(Vector2i.ZERO)
|
|
|
|
v._scroll_rung(1, Vector2(960.0, 540.0)) # Region, for body B — no Global echo yet
|
|
var extent: Vector2i = v._request_extent()
|
|
var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "Region")
|
|
assert_that(extent).override_failure_message(
|
|
"body B's Region request, before body B's own Global echo has"
|
|
+ " landed, must go out UNCAPPED — never capped by body A's stale"
|
|
+ " leftover region-grid extent"
|
|
).is_equal(uncapped)
|
|
|
|
|
|
# =============================================================================
|
|
# T-1189/T-1192: shared letterbox mechanism — centering + Global fit scale.
|
|
# =============================================================================
|
|
|
|
|
|
## T-1192's own headline defect: the Global canvas must no longer draw
|
|
## top-left-anchored at Vector2.ZERO — once a canvas lands, the viewer must
|
|
## have computed a non-zero centering offset (unless the canvas happens to
|
|
## exactly fill the viewport, not the case here).
|
|
func test_global_canvas_arrival_centers_the_view_not_top_left() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
assert_that(v._view_offset).override_failure_message(
|
|
"a landed Global canvas smaller than the viewport must be CENTERED,"
|
|
+ " never left at the raw top-left Vector2.ZERO anchor"
|
|
).is_not_equal(Vector2.ZERO)
|
|
|
|
|
|
## D-255 amendment 2026-07-25 texel-exactness: the `_canvas.scale` value
|
|
## itself is NOT required to be a whole number (9/5 = 1.8 is entirely
|
|
## legitimate) — what MUST be exact is the resulting on-screen
|
|
## pixels-per-gridunit ratio. Verified end-to-end through the real viewer
|
|
## wiring: `_canvas_scale * base_display_ratio` (the rung's own display
|
|
## ratio, 5.0 for Global) must land on an exact integer, for both the
|
|
## legend-reserved 1920x1080 case (R=9, scale=1.8) AND the 4K case (R=20,
|
|
## scale=4.0) — the SAME formula, no separate coverage-threshold branch.
|
|
func test_global_canvas_scale_yields_an_exact_integer_pixels_per_gridunit_ratio() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
assert_that(v._canvas.scale).is_equal(Vector2(v._canvas_scale, v._canvas_scale))
|
|
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
|
|
var effective_px_per_gridunit: float = v._canvas_scale * base_ratio
|
|
assert_float(effective_px_per_gridunit).override_failure_message(
|
|
"the FINAL on-screen pixels-per-gridunit ratio must be an exact"
|
|
+ " integer even when the _canvas.scale multiplier itself is not"
|
|
).is_equal_approx(roundf(effective_px_per_gridunit), 0.001)
|
|
# The lead's own cited reference number for this exact scenario.
|
|
assert_float(effective_px_per_gridunit).is_equal_approx(9.0, 0.001)
|
|
|
|
|
|
## Same invariant at a large (4K-class) viewport, where the integer ratio
|
|
## (20) happens to make the _canvas.scale multiplier itself a whole number
|
|
## too (20/5 = 4.0) — confirms the 1080p case above isn't a coincidence of
|
|
## a small viewport, just the same formula at a different achievable ratio.
|
|
func test_global_canvas_scale_at_4k_also_yields_an_exact_integer_ratio() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(3840.0, 2160.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
|
|
var effective_px_per_gridunit: float = v._canvas_scale * base_ratio
|
|
assert_float(effective_px_per_gridunit).is_equal_approx(20.0, 0.001)
|
|
|
|
|
|
## Fixed rungs must NEVER receive the Global fit multiplier — `_canvas.scale`
|
|
## stays 1.0 once the player has descended past Global, even though a
|
|
## Global canvas was landed earlier in the same session.
|
|
func test_fixed_rung_canvas_scale_stays_one_after_descending_from_global() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
v._scroll_rung(1, Vector2(960.0, 540.0)) # Region
|
|
v._terrain_layer.rebuild_from_canvas(
|
|
TestStepCanvasViewer._synthetic_canvas(200, 100), v.get_held_rung(), ""
|
|
)
|
|
v._recompute_canvas_transform()
|
|
|
|
assert_float(v._canvas_scale).is_equal_approx(1.0, 0.001)
|
|
assert_that(v._canvas.scale).is_equal(Vector2.ONE)
|
|
|
|
|
|
## Legend non-overlap (T-1192: "lay the legend out beside the canvas... never
|
|
## over it"): the legend panel sits at a fixed left-column position
|
|
## (PANEL_MARGIN, ...) with a known width — once a Global canvas is landed
|
|
## and centered, its drawn rect's LEFT edge must be at or past the legend's
|
|
## own right edge, never underneath it.
|
|
func test_global_canvas_left_edge_never_overlaps_the_legend_column() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
var canvas_left_edge: float = v._view_offset.x
|
|
assert_float(canvas_left_edge).override_failure_message(
|
|
"the Global canvas's drawn left edge must be at or past the reserved"
|
|
+ " legend column — the legend must never be covered by the map"
|
|
).is_greater_equal(StepCanvasTransport.LEGEND_COLUMN_PX - 0.01)
|
|
|
|
|
|
## Resize must re-fit/re-center a HELD canvas, not just a freshly-arriving
|
|
## one — _notification(NOTIFICATION_RESIZED) wires _recompute_canvas_transform()
|
|
## for exactly this case.
|
|
func test_resize_recenters_an_already_held_global_canvas() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(1920.0, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
var offset_before: Vector2 = v._view_offset
|
|
|
|
v.size = Vector2(1280.0, 720.0)
|
|
v._notification(Control.NOTIFICATION_RESIZED)
|
|
|
|
assert_that(v._view_offset).override_failure_message(
|
|
"a resize must re-center the held canvas for the NEW viewport size"
|
|
).is_not_equal(offset_before)
|
|
|
|
|
|
## Hoshe (review round 2): a narrow viewport where GJ1c's canvas exceeds
|
|
## the available width on the gridunit lattice itself (177 gridunits >
|
|
## available px after the legend column is reserved) used to make the OLD
|
|
## fractional-fit branch return a sub-1x scale (0.39x) — a real downscale
|
|
## below native resolution, violating "never below native". The integer
|
|
## px/gridunit ratio floors at 1 instead: `_canvas_scale` must never drop
|
|
## under 1.0, end-to-end through the real viewer wiring, not just the pure
|
|
## transport function this mirrors.
|
|
func test_global_canvas_scale_never_drops_below_native_on_a_narrow_viewport() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.size = Vector2(348.0 + StepCanvasTransport.LEGEND_COLUMN_PX, 1080.0)
|
|
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
|
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
|
|
|
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
|
|
assert_float(v._canvas_scale * base_ratio).override_failure_message(
|
|
"the effective pixels-per-gridunit ratio must floor at 1 (native),"
|
|
+ " never a sub-1x downscale, even on a viewport this narrow"
|
|
).is_equal_approx(1.0, 0.001)
|
|
|
|
|
|
# =============================================================================
|
|
# T-971 (AtlasAgentInterface): jump_to() — the fixed-center revisit seam —
|
|
# and get_current_canvas_summary().
|
|
# =============================================================================
|
|
|
|
|
|
## jump_to() must set the SAME held rung/world_center a cursor-anchored
|
|
## scroll to that same spot would land on — this is the "same cache key"
|
|
## guarantee AtlasAgentInterface's jump_to_center intent depends on
|
|
## (verified end-to-end, through act(), in test_atlas_agent_interface.gd;
|
|
## this is the narrower unit-level check directly against the viewer).
|
|
func test_jump_to_sets_held_rung_and_world_center() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
|
|
v.jump_to(Vector2(500.0, -250.0), StepCanvasTransport.RUNG_QUARTER)
|
|
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_QUARTER)
|
|
assert_that(v.get_world_center()).is_equal(Vector2(500.0, -250.0))
|
|
|
|
|
|
## Omitting `rung` keeps whatever rung is currently held — the "revisit
|
|
## within the same rung" common case shouldn't require repeating it.
|
|
func test_jump_to_keeps_current_rung_when_omitted() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # Region
|
|
|
|
v.jump_to(Vector2(10.0, 20.0))
|
|
|
|
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
|
|
|
|
|
|
## Jumping to Global always forces world_center to ZERO — Global has no
|
|
## panned-center concept (mirrors _scroll_rung()'s own Global-rung handling).
|
|
func test_jump_to_global_forces_world_center_to_zero() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # Region
|
|
|
|
v.jump_to(Vector2(777.0, 888.0), StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
assert_that(v.get_world_center()).is_equal(Vector2.ZERO)
|
|
|
|
|
|
func test_jump_to_unrecognized_rung_is_a_no_op() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
var rung_before: String = v.get_held_rung()
|
|
var center_before: Vector2 = v.get_world_center()
|
|
|
|
v.jump_to(Vector2(1.0, 2.0), "NotARealRung")
|
|
|
|
assert_str(v.get_held_rung()).is_equal(rung_before)
|
|
assert_that(v.get_world_center()).is_equal(center_before)
|
|
|
|
|
|
func test_get_current_canvas_summary_before_any_canvas_arrives() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
|
|
var summary: Dictionary = v.get_current_canvas_summary()
|
|
|
|
assert_bool(summary.get("has_canvas", true)).is_false()
|
|
assert_str(summary.get("rung", "")).is_equal(StepCanvasTransport.RUNG_GLOBAL)
|
|
|
|
|
|
## The T-1157-inventory-relevant correctness check: course/cliff/settlement
|
|
## counts must match a fixture canvas exactly, including the per-class
|
|
## course histogram and settlement id dedup (mirrors
|
|
## StepCanvasAnnotationLayer._draw_settlements()'s own dedup discipline —
|
|
## covering the same cell id twice must not double-count).
|
|
func test_get_current_canvas_summary_counts_match_a_fixture_canvas() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
v._scroll_rung(1, Vector2(400.0, 300.0)) # Region
|
|
|
|
var canvas: Dictionary = _synthetic_canvas(4, 4)
|
|
canvas["courses"] = [
|
|
{"class": 0, "points": [[0.0, 0.0], [1.0, 1.0]]},
|
|
{"class": 0, "points": [[2.0, 2.0], [3.0, 3.0]]},
|
|
{"class": 2, "points": [[4.0, 4.0], [5.0, 5.0]]},
|
|
]
|
|
canvas["cliffs"] = [{"a": 1}, {"b": 2}]
|
|
# 4x4 grid; ids 5 and 5 (repeat, same settlement footprint) dedup to one,
|
|
# id 9 is a second distinct settlement, 0 is "no settlement" and ignored.
|
|
canvas["settlement_id"] = [
|
|
0, 5, 5, 0,
|
|
0, 0, 0, 0,
|
|
9, 0, 0, 0,
|
|
0, 0, 0, 0,
|
|
]
|
|
v._on_canvas_ready(canvas)
|
|
|
|
var summary: Dictionary = v.get_current_canvas_summary()
|
|
|
|
assert_bool(summary.get("has_canvas", false)).is_true()
|
|
assert_int(summary.get("canvas_width", 0)).is_equal(4)
|
|
assert_int(summary.get("canvas_height", 0)).is_equal(4)
|
|
assert_int(summary.get("course_count", 0)).is_equal(3)
|
|
assert_int(summary.get("cliff_count", 0)).is_equal(2)
|
|
assert_int(summary.get("settlement_count", 0)).is_equal(2)
|
|
var by_class: Dictionary = summary.get("course_count_by_class", {})
|
|
assert_int(int(by_class.get(0, 0))).is_equal(2)
|
|
assert_int(int(by_class.get(2, 0))).is_equal(1)
|
|
|
|
|
|
# =============================================================================
|
|
# T-1197 PR #217 review (Hoshe): header-panel-vs-legend-panel vertical
|
|
# non-overlap — the exact regression this review round caught. Mirrors the
|
|
# T-1192 precedent above (test_global_canvas_left_edge_never_overlaps_the_legend_column,
|
|
# line ~674): a geometric non-overlap invariant against the REAL viewer
|
|
# wiring, not a hand-computed expected pixel value that could silently drift
|
|
# out of sync with the production layout the same way the old hardcoded
|
|
# Vector2(PANEL_MARGIN, 60.0) drifted out of sync with the header's real
|
|
# grown footprint.
|
|
# =============================================================================
|
|
|
|
|
|
## The screen header panel and the legend panel must never vertically
|
|
## overlap: the legend's TOP edge (position.y) must be at or below the
|
|
## header's BOTTOM edge (position.y + size.y). Before the T-1197 PR #217 fix,
|
|
## step_canvas_legend.gd's reposition() hardcoded Y=60.0 — a constant tuned
|
|
## for the OLD bare-ImplantHeader footprint — so once the header grew its own
|
|
## ImplantPanel wrapper (border + content margins), the legend's fixed Y sat
|
|
## INSIDE the header panel's new, taller footprint: the two fused into one
|
|
## unbroken double-height box with zero terrain gap between them (pixel-
|
|
## proven independently by both PR #217 reviewers). This test pins the
|
|
## invariant directly against the real _screen_header_panel/_legend_panel
|
|
## Controls the production layout builds, not a copy of the geometry math.
|
|
func test_legend_panel_never_overlaps_the_header_panel_vertically() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
|
|
# Both panels are manually positioned (implant_panel.gd's own doc: "not
|
|
# itself inside a parent Container, so nothing else forces a re-measure"),
|
|
# and reset_to_content_size()/reposition() are deferred — award one idle
|
|
# frame so the REAL settled sizes are in place before asserting, exactly
|
|
# like the deferred-resize idiom both panels already rely on in production
|
|
# (see _build_screen_header()'s own call to reset_to_content_size(), and
|
|
# _ready()'s own deferred reposition() call added alongside this test).
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
|
|
var header_top: float = v._screen_header_panel.position.y
|
|
var header_bottom: float = header_top + v._screen_header_panel.size.y
|
|
var legend_top: float = v._legend_panel.position.y
|
|
|
|
assert_float(legend_top).override_failure_message(
|
|
(
|
|
"the legend panel's top edge (y=%.1f) must be AT OR BELOW the header"
|
|
+ " panel's measured bottom edge (y=%.1f) — a smaller value means the"
|
|
+ " two panels overlap/fuse into one box, the exact PR #217 regression"
|
|
)
|
|
% [legend_top, header_bottom]
|
|
).is_greater_equal(header_bottom - 0.01)
|
|
|
|
|
|
## The gap must be a REAL, visible gap — not just "touching at exactly the
|
|
## same pixel" (which would still satisfy >= but reads as fused on screen).
|
|
## Pins the fixed HEADER_LEGEND_GAP_PX constant is actually being applied,
|
|
## not merely that overlap happens to be avoided by coincidence of content
|
|
## size on this particular test body.
|
|
func test_legend_panel_leaves_a_real_gap_below_the_header_panel() -> void:
|
|
var v: StepCanvasViewer = _make_viewer()
|
|
add_child(v)
|
|
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
|
|
var header_bottom: float = v._screen_header_panel.position.y + v._screen_header_panel.size.y
|
|
var legend_top: float = v._legend_panel.position.y
|
|
var gap: float = legend_top - header_bottom
|
|
|
|
assert_float(gap).override_failure_message(
|
|
(
|
|
"expected a visible gap of at least %.1fpx between the header panel's"
|
|
+ " bottom (y=%.1f) and the legend panel's top (y=%.1f), got %.1fpx —"
|
|
+ " panels that merely touch still read as one fused box on screen"
|
|
)
|
|
% [v.get_header_legend_gap_px(), header_bottom, legend_top, gap]
|
|
).is_greater_equal(v.get_header_legend_gap_px() - 0.01)
|