feat(ui): cap rung extents to the body + fit/center the Global canvas (T-1189, T-1192)
The two client fixes for the Atlas frames Jeroen flagged. Region rung no longer requests more planet than exists: cap_extent_to_body() caps the viewport-fit extent at the body's own region grid (the Global canvas extent echo — cols=regions_per_equator, rows=cols/2), matched by gridunit SPACING not rung name, applied before the request/cache key is built; cold-start scroll-before-Global-echo requests uncapped and lets the server clamp (documented). Kills both the side-by-side continent repeat and the past-the-pole stripe smear. Global opener now fit-scales and centers through one shared letterbox mechanism (center_offset/integer_fit_scale/fit_scale, 75%-coverage integer-vs-fractional decision, NEAREST already forced on orbital rungs per D-255's escape hatch) also used for the Region cap's letterbox remainder. Legend column reserved before fitting — pinned to the legend's own width by a cross-constant test, never overlapping the canvas. Also fixes a latent crash: NOTIFICATION_RESIZED fires mid-_ready() before children exist; null guard in the resize path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ class_name TestStepCanvasLegend
|
||||
extends GdUnitTestSuite
|
||||
|
||||
const LegendScript := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_legend.gd")
|
||||
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
|
||||
|
||||
|
||||
func test_legend_starts_hidden_before_refresh() -> void:
|
||||
@@ -68,3 +69,14 @@ func test_reposition_sets_a_fixed_panel_margin_position() -> void:
|
||||
auto_free(legend)
|
||||
legend.reposition()
|
||||
assert_that(legend.position).is_equal(Vector2(LegendScript.PANEL_MARGIN, 60.0))
|
||||
|
||||
|
||||
## T-1192: StepCanvasTransport.LEGEND_COLUMN_PX (the Global fit-scale
|
||||
## reservation StepCanvasViewer applies) must stay derived from this SAME
|
||||
## panel's own width/margin — a drift here would silently reopen the
|
||||
## "legend overlaps the canvas" defect on one side while the OTHER side
|
||||
## thinks it already reserved enough room.
|
||||
func test_reserved_column_px_matches_the_transport_sides_own_constant() -> void:
|
||||
assert_float(LegendScript.RESERVED_COLUMN_PX).is_equal_approx(
|
||||
StepCanvasTransport.LEGEND_COLUMN_PX, 0.01
|
||||
)
|
||||
|
||||
@@ -188,3 +188,174 @@ func test_canvas_footprint_px_is_extent_times_display_ratio() -> void:
|
||||
func test_half_extent_m_is_half_the_cell_count_times_spacing() -> void:
|
||||
var half: float = StepCanvasTransport.half_extent_m("District", 64)
|
||||
assert_float(half).is_equal_approx(64.0 * 0.5 * 2048.0, 0.01)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1189: extent cap to the body's own region grid — the sideways-repeat /
|
||||
# pole-smear fix. The Global echo IS the cap (its canvas already equals the
|
||||
# body's region grid, D-255(a): "the Global canvas IS the whole body at
|
||||
# region spacing"), so no unit conversion is needed — Region shares Global's
|
||||
# gridunit spacing exactly.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_cap_extent_to_body_clamps_region_to_the_global_echo() -> void:
|
||||
# T-1183 eyeball: 384x216 requested at Region on GJ1c, whose Global echo
|
||||
# is 177x88 — the requested extent overruns the body on both axes.
|
||||
var capped: Vector2i = StepCanvasTransport.cap_extent_to_body(
|
||||
Vector2i(384, 216), "Region", Vector2i(177, 88)
|
||||
)
|
||||
assert_that(capped).is_equal(Vector2i(177, 88))
|
||||
|
||||
|
||||
func test_cap_extent_to_body_is_a_no_op_when_already_inside_the_grid() -> void:
|
||||
var capped: Vector2i = StepCanvasTransport.cap_extent_to_body(
|
||||
Vector2i(100, 40), "Region", Vector2i(177, 88)
|
||||
)
|
||||
assert_that(capped).is_equal(Vector2i(100, 40))
|
||||
|
||||
|
||||
## Shape-generic per the ticket: the guard compares SPACING, not rung name,
|
||||
## so it caps ANY rung sharing Global's spacing, not just a hardcoded
|
||||
## "Region" check. District's spacing (2048 m) differs from Global's
|
||||
## (204,800 m), so it must NEVER be capped by the body-grid cell count —
|
||||
## capping cell counts across mismatched spacings would be a unit error.
|
||||
func test_cap_extent_to_body_leaves_finer_rungs_uncapped() -> void:
|
||||
var capped: Vector2i = StepCanvasTransport.cap_extent_to_body(
|
||||
Vector2i(3000, 3000), "District", Vector2i(177, 88)
|
||||
)
|
||||
assert_that(capped).is_equal(Vector2i(3000, 3000))
|
||||
|
||||
|
||||
## Cold-start fallback (T-1189, StepCanvasViewer's own documented choice):
|
||||
## before any Global response has arrived, `_global_body_extent` is ZERO —
|
||||
## cap_extent_to_body() must leave the request UNCAPPED on a non-positive
|
||||
## axis (server clamps independently) rather than clamping to zero cells.
|
||||
func test_cap_extent_to_body_uncapped_when_global_echo_not_yet_available() -> void:
|
||||
var capped: Vector2i = StepCanvasTransport.cap_extent_to_body(
|
||||
Vector2i(384, 216), "Region", Vector2i.ZERO
|
||||
)
|
||||
assert_that(capped).is_equal(Vector2i(384, 216))
|
||||
|
||||
|
||||
## A mixed case: one axis of the Global echo has arrived-and-is-real, the
|
||||
## other is still ZERO (shouldn't happen in practice since both arrive
|
||||
## together, but the function must handle each axis independently rather
|
||||
## than assuming both-or-neither).
|
||||
func test_cap_extent_to_body_caps_only_the_positive_echo_axis() -> void:
|
||||
var capped: Vector2i = StepCanvasTransport.cap_extent_to_body(
|
||||
Vector2i(384, 216), "Region", Vector2i(177, 0)
|
||||
)
|
||||
assert_that(capped).is_equal(Vector2i(177, 216))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1189/T-1192: shared letterbox/centering mechanism
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_center_offset_centers_a_smaller_canvas_in_a_larger_viewport() -> void:
|
||||
var offset: Vector2 = StepCanvasTransport.center_offset(
|
||||
Vector2(800.0, 400.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_that(offset).is_equal(Vector2((1920.0 - 800.0) * 0.5, (1080.0 - 400.0) * 0.5))
|
||||
|
||||
|
||||
func test_center_offset_is_zero_when_canvas_exactly_fills_the_viewport() -> void:
|
||||
var offset: Vector2 = StepCanvasTransport.center_offset(
|
||||
Vector2(1920.0, 1080.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_that(offset).is_equal(Vector2.ZERO)
|
||||
|
||||
|
||||
func test_center_offset_goes_negative_when_the_canvas_overflows_the_viewport() -> void:
|
||||
# A canvas bigger than the viewport on an axis crops rather than shrinks
|
||||
# (matches every fixed rung's own "canvas can exceed the viewport"
|
||||
# precedent) — a negative offset on that axis is the correct, honest
|
||||
# result, not clamped to zero.
|
||||
var offset: Vector2 = StepCanvasTransport.center_offset(
|
||||
Vector2(2000.0, 400.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_float(offset.x).is_less(0.0)
|
||||
assert_float(offset.y).is_greater(0.0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1192: Global integer-fit scale — D-255 texel-exactness
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_integer_fit_scale_picks_the_largest_multiple_that_fits_both_axes() -> void:
|
||||
# GJ1c reference case: 177x88 texels at the 5x5 shallow display ratio =
|
||||
# 885x440 px footprint, fit against a full 1920x1080 viewport.
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(
|
||||
Vector2(885.0, 440.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_int(scale).is_equal(2)
|
||||
|
||||
|
||||
func test_integer_fit_scale_is_bounded_by_the_tighter_axis() -> void:
|
||||
# Wide-but-short viewport: x could fit 4x, y only fits 1x — the smaller
|
||||
# wins (never overflow either axis).
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(
|
||||
Vector2(100.0, 100.0), Vector2(1000.0, 150.0)
|
||||
)
|
||||
assert_int(scale).is_equal(1)
|
||||
|
||||
|
||||
func test_integer_fit_scale_never_drops_below_one() -> void:
|
||||
# A canvas larger than the viewport still gets scale 1 (draw at native
|
||||
# size and let it exceed/crop), never a shrink below native.
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(
|
||||
Vector2(3000.0, 3000.0), Vector2(800.0, 600.0)
|
||||
)
|
||||
assert_int(scale).is_equal(1)
|
||||
|
||||
|
||||
func test_integer_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> void:
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(Vector2.ZERO, Vector2(800.0, 600.0))
|
||||
assert_int(scale).is_equal(1)
|
||||
|
||||
|
||||
func test_fit_scale_matches_the_integer_fit_when_coverage_is_high() -> void:
|
||||
# The GJ1c full-viewport case: integer 2x covers well over the
|
||||
# FIT_MIN_COVERAGE_RATIO bar, so fit_scale() must agree with
|
||||
# integer_fit_scale() exactly (no fractional fallback).
|
||||
var scale: float = StepCanvasTransport.fit_scale(
|
||||
Vector2(885.0, 440.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_float(scale).is_equal_approx(2.0, 0.001)
|
||||
|
||||
|
||||
## The GJ1c reference case AFTER the legend column is reserved (T-1192):
|
||||
## available area shrinks to 1628x1080, so the 2x integer candidate (1770 px
|
||||
## wide) no longer fits — integer_fit_scale() drops to 1x, which only covers
|
||||
## ~41% of the tighter available axis, well under FIT_MIN_COVERAGE_RATIO —
|
||||
## fit_scale() must fall back to the non-integer uniform fit that fills the
|
||||
## tighter (x) axis exactly, not settle for the sparse 1x frame.
|
||||
func test_fit_scale_falls_back_to_fractional_fit_on_excessive_letterboxing() -> void:
|
||||
var scale: float = StepCanvasTransport.fit_scale(
|
||||
Vector2(885.0, 440.0), Vector2(1628.0, 1080.0)
|
||||
)
|
||||
assert_float(scale).is_greater(1.0)
|
||||
assert_float(scale).is_less(2.0)
|
||||
# The fractional fit fills the tighter (x) axis exactly.
|
||||
assert_float(885.0 * scale).is_equal_approx(1628.0, 0.01)
|
||||
|
||||
|
||||
func test_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> void:
|
||||
var scale: float = StepCanvasTransport.fit_scale(Vector2.ZERO, Vector2(800.0, 600.0))
|
||||
assert_float(scale).is_equal_approx(1.0, 0.001)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1192: shared legend-column reservation constant
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_legend_column_px_is_positive_and_matches_the_legend_panels_own_sizing() -> void:
|
||||
# Pinned against step_canvas_legend.gd's own RESERVED_COLUMN_PX (260 +
|
||||
# 16*2 = 292) — the two constants must never drift apart, since the
|
||||
# "beside, never over" guarantee depends on both sides agreeing on the
|
||||
# SAME reserved width.
|
||||
assert_float(StepCanvasTransport.LEGEND_COLUMN_PX).is_equal_approx(292.0, 0.01)
|
||||
|
||||
@@ -10,6 +10,12 @@ 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)
|
||||
|
||||
|
||||
func test_enter_lands_on_the_global_opener() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
@@ -385,3 +391,264 @@ func test_disk_sweep_timeout_handler_runs_background_sweep_for_the_current_body(
|
||||
# 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 = auto_free(StepCanvasViewer.new())
|
||||
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 = auto_free(StepCanvasViewer.new())
|
||||
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 = auto_free(StepCanvasViewer.new())
|
||||
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 = auto_free(StepCanvasViewer.new())
|
||||
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 = auto_free(StepCanvasViewer.new())
|
||||
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)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 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 = auto_free(StepCanvasViewer.new())
|
||||
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 texel-exactness (T-1192): at a large (4K-class) viewport, GJ1c's
|
||||
## 885x440 raw footprint (177x88 texels x 5x5 shallow display ratio) clears
|
||||
## the coverage bar even AFTER the legend column is reserved, so the
|
||||
## INTEGER fit wins outright — verified end-to-end through the real viewer
|
||||
## wiring, not just the pure transport function this mirrors. (The T-1183
|
||||
## reference 1920x1080 viewport is deliberately NOT used here — at that
|
||||
## size the legend-column reservation starves the integer candidate below
|
||||
## the coverage bar and the fractional escape hatch fires instead, covered
|
||||
## separately by test_global_canvas_uses_fractional_fit_when_legend_column_
|
||||
## starves_the_integer_fit() below.)
|
||||
func test_global_canvas_scale_is_an_integer_multiple_of_the_raw_footprint() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
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)
|
||||
|
||||
assert_that(v._canvas.scale).is_equal(Vector2(v._canvas_scale, v._canvas_scale))
|
||||
assert_float(v._canvas_scale).override_failure_message(
|
||||
"the Global fit scale must be a whole number of texture pixels"
|
||||
+ " when it clears the coverage bar (D-255 texel-exactness)"
|
||||
).is_equal_approx(roundf(v._canvas_scale), 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 = auto_free(StepCanvasViewer.new())
|
||||
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 = auto_free(StepCanvasViewer.new())
|
||||
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 = auto_free(StepCanvasViewer.new())
|
||||
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)
|
||||
|
||||
|
||||
## Small-canvas fallback (D-255's own escape hatch): once the legend column
|
||||
## eats enough of the available width that the integer fit falls under
|
||||
## FIT_MIN_COVERAGE_RATIO, the viewer must fall back to the fractional fit
|
||||
## rather than settling for a sparse integer frame — end-to-end through the
|
||||
## real viewer, mirroring test_fit_scale_falls_back_to_fractional_fit_on_
|
||||
## excessive_letterboxing in test_step_canvas_transport.gd.
|
||||
func test_global_canvas_uses_fractional_fit_when_legend_column_starves_the_integer_fit() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
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)
|
||||
|
||||
# GJ1c at 1920x1080 with the legend column reserved: 2x no longer fits
|
||||
# (1770 > 1628 available), 1x covers only ~41% of the tighter axis — well
|
||||
# under the 75% coverage bar, so a non-integer fit must have been chosen.
|
||||
assert_float(v._canvas_scale).override_failure_message(
|
||||
"the reserved legend column must starve the 2x integer candidate at"
|
||||
+ " this reference viewport, forcing the fractional fit"
|
||||
).is_greater(1.0)
|
||||
|
||||
Reference in New Issue
Block a user