Merge remote-tracking branch 'origin/zoom-ladder-continuous'

This commit is contained in:
2026-07-22 17:40:20 +02:00
35 changed files with 5928 additions and 607 deletions
+16 -4
View File
@@ -435,20 +435,32 @@ func send_named_action(action_name: String, action_data: Variant = null) -> void
##
## window_granularity/window_min_wl_m (T-1150): struct/key plumbing for the
## zoom-ladder quarter rung — district (0/omitted) stays the default for
## every caller in this codebase today; requesting quarter granularity is
## T-1153's job, not wired here.
## every caller in this codebase today.
##
## window_granularity_v2 (T-1152/T-1153): the R5-redesigned string-tag
## granularity ("Quarter"/"District"/"Region") — the ONLY way to request the
## coarser-than-district Region rung the legacy u32 field cannot express.
## Empty string (omitted) is the default for every caller that doesn't pass
## it, byte-compatible with every pre-T-1152 request.
func request_atlas_layers(
body_id: String,
up_to: String = "Topography",
window_center: Variant = null,
window_n: int = 0,
window_granularity: int = 0,
window_min_wl_m: int = 0
window_min_wl_m: int = 0,
window_granularity_v2: String = ""
) -> void:
if test_mode or _bridge == null or state != ConnectionState.CONNECTED:
return
var bytes := Protocol.encode_atlas_layer_request(
body_id, up_to, window_center, window_n, window_granularity, window_min_wl_m
body_id,
up_to,
window_center,
window_n,
window_granularity,
window_min_wl_m,
window_granularity_v2
)
if bytes.is_empty():
return
+23 -2
View File
@@ -40,6 +40,22 @@ class_name AtlasMapProtocol
## it possible to ask, byte-compatible with every existing caller that
## doesn't pass them.
##
## `window_granularity_v2` (T-1152, R5 redesign — see
## server/src/atlas/layer_proxy.rs's `WindowGranularity` doc): the ONLY way to
## express a coarser-than-district rung (`"Region"`) the legacy `u32` field
## cannot encode. A plain STRING variant tag ("Quarter" | "District" |
## "Region"), matching `RoadNodeKind`'s existing wire precedent on this same
## carrier (a bare `#[derive(Serialize, Deserialize)]` enum with no
## `#[serde(rename_all)]` — rmp_serde encodes the Rust variant NAME verbatim,
## not an integer discriminant). OMITTED (not sent as "") when
## `window_granularity_v2` is the empty string — `#[serde(default)]` on the
## Rust side decodes absence as `None`, falling back to the legacy `u32`
## field's `resolve_window_granularity_v2()` precedence rule (that field wins
## over the legacy one whenever present — see that Rust doc for the full
## precedence contract). Every pre-T-1152 caller (and every T-1150 caller that
## only ever sends `window_granularity`) omits this field entirely and stays
## byte-compatible.
##
## **Quantization split (PR #191 review, Hoshe 1 / Tyre C3):** `window_min_wl_m`
## is sent HERE as a raw, unquantized value — this codec does NOT snap it to
## the design doc §5 fixed band set. The SERVER is the one place quantization
@@ -56,7 +72,8 @@ static func encode_atlas_layer_request(
window_center: Variant = null,
window_n: int = 0,
window_granularity: int = 0,
window_min_wl_m: int = 0
window_min_wl_m: int = 0,
window_granularity_v2: String = ""
) -> PackedByteArray:
var msg := {"body_id": body_id, "up_to": up_to}
if window_center != null:
@@ -67,6 +84,8 @@ static func encode_atlas_layer_request(
msg["window_granularity"] = window_granularity
if window_min_wl_m != 0:
msg["window_min_wl_m"] = window_min_wl_m
if not window_granularity_v2.is_empty():
msg["window_granularity_v2"] = window_granularity_v2
var result = mp.encode(msg)
if result.status != null:
push_error("Protocol: encode_atlas_layer_request failed: %s" % result.status)
@@ -91,7 +110,9 @@ static func encode_atlas_layer_request(
## wire decodes to GDScript `null` exactly like every other Option field here.
## The response's `center`/`n` echo (inside the layer dict itself) is the
## client's race-condition/staleness guard (§2) — read by the window cache,
## not unwrapped here.
## not unwrapped here. `granularity_v2` (T-1152) rides inside the same dict,
## a bare string variant tag ("Quarter"/"District"/"Region") — no separate
## top-level unwrap needed, it passes through with everything else.
## Key names "road_graph"/"settlements"/"region_grid"/"quarter_footprints"/
## "district_window" are the CONFIRMED wire contract — identical to
## server/src/atlas/layer_proxy.rs AtlasLayerResponse's field names
+13 -2
View File
@@ -782,16 +782,27 @@ static func encode_request_bookmark_catalog() -> PackedByteArray:
## Omitted callers (every whole-body-layer call site predating T-1138) are
## byte-unchanged. window_granularity/window_min_wl_m (T-1150): same
## byte-compatibility contract, see atlas_map_protocol.gd.
## window_granularity_v2 (T-1152): the R5-redesigned string-tag granularity
## ("Quarter"/"District"/"Region") — the only way to request the Region rung.
## Omitted (empty string) by every caller that doesn't pass it.
static func encode_atlas_layer_request(
body_id: String,
up_to: String = "Topography",
window_center: Variant = null,
window_n: int = 0,
window_granularity: int = 0,
window_min_wl_m: int = 0
window_min_wl_m: int = 0,
window_granularity_v2: String = ""
) -> PackedByteArray:
return _amp().encode_atlas_layer_request(
_mp(), body_id, up_to, window_center, window_n, window_granularity, window_min_wl_m
_mp(),
body_id,
up_to,
window_center,
window_n,
window_granularity,
window_min_wl_m,
window_granularity_v2
)
+60
View File
@@ -240,6 +240,36 @@ func test_encode_atlas_layer_request_carries_granularity_and_min_wl() -> void:
assert_that(decoded.value.get("window_min_wl_m")).is_equal(512)
## T-1152/T-1153: window_granularity_v2 is OMITTED (not sent as "") when at
## its empty-string default — same byte-compatibility contract as
## window_granularity/window_min_wl_m's own default-omission.
func test_encode_atlas_layer_request_omits_granularity_v2_by_default() -> void:
var bytes := Protocol.encode_atlas_layer_request(
"GJ1c", "Topography", Vector2i(140, 260), 32
)
var decoded = Messagepack.decode(bytes)
assert_bool(decoded.value.has("window_granularity_v2")).is_false()
## T-1152/T-1153: a Region-rung request carries window_granularity_v2 as the
## bare string "Region" — the ONLY way to express the coarser-than-district
## rung (WindowGranularity's Rust doc: "the ONLY way to actually request
## Region is window_granularity_v2 = Some(WindowGranularity::Region)"), a
## plain rmp_serde variant-name encoding matching RoadNodeKind's existing
## wire precedent, NOT an integer discriminant.
func test_encode_atlas_layer_request_carries_granularity_v2_region() -> void:
var bytes := Protocol.encode_atlas_layer_request(
"GJ1c", "Topography", Vector2i(0, 0), 6400, 0, 0, "Region"
)
var decoded = Messagepack.decode(bytes)
assert_that(decoded.value.get("window_granularity_v2")).is_equal("Region")
# The legacy window_granularity field is independently omittable — a
# Region request sends ONLY the v2 tag, never a legacy value pretending
# to mean something for Region (WINDOW_GRANULARITY_REGION_KEY is a
# key-space tag the SERVER echoes, never a legal wire INPUT).
assert_bool(decoded.value.has("window_granularity")).is_false()
## §2: district_window is a distinct payload (echoes center/n for the
## client's staleness guard) but the codec passthrough is the same shape as
## every sibling layer — raw.get(), no reshaping. Field types follow the
@@ -265,6 +295,36 @@ func test_atlas_response_district_window_passthrough() -> void:
assert_that((decoded as Dictionary).get("district_window")).is_equal(window)
## T-1152/T-1153, design doc §6 encoding-continuity acceptance: a Region-rung
## response passes through the EXACT SAME codec path as District/Quarter —
## granularity_v2 is just another field in the same dict, no special-cased
## decode branch for the coarser rung. This is the direct regression test for
## "one colorizer family, no per-rung palettes": the wire contract itself
## draws no distinction, so nothing downstream (the overlay's
## cell_grid_side_for_window()/_cell_color()) needs a rung-specific decode
## path either.
func test_atlas_response_district_window_passthrough_region_rung() -> void:
var window := {
"center": [0, 0],
"n": 6400,
"granularity": 4294967295, # WINDOW_GRANULARITY_REGION_KEY (u32::MAX) — key-space tag, not a real multiplier
"granularity_v2": "Region",
"min_wl_m": 0,
"morphology": PackedByteArray([8, 14, 0, 5]),
"elev_q": PackedByteArray([40, 62, 5, 88]),
"temp_dc": [120, 95, AtlasOverlayColors.REGION_TEMP_NONE_DC, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
var raw := {"body_id": "GJ1c", "status": "Ready", "district_window": window}
var decoded: Variant = Protocol.atlas_response_from_raw(raw)
assert_that(decoded).is_not_null()
var district_window: Dictionary = (decoded as Dictionary).get("district_window")
assert_that(district_window).is_equal(window)
assert_str(str(district_window.get("granularity_v2"))).is_equal("Region")
## A body with no window requested (or not yet derived — §1's background-queue
## serving model: the completion may not have landed yet) must decode with
## district_window absent -> null, same "layer hasn't produced yet" contract
+11 -8
View File
@@ -371,16 +371,19 @@ func test_no_descend_signal_without_a_loaded_heightmap() -> void:
assert_int(received.size()).is_equal(0)
## RegionalScreen forwards AtlasViewer's district_descend_requested verbatim —
## the nav-stack wiring atlas_app.gd depends on.
func test_regional_screen_forwards_district_descend_requested() -> void:
## T-1153 (D-226 T-1143-rulings amendment): RegionalScreen no longer wraps
## AtlasViewer or forwards district_descend_requested — the "regional" nav
## entry now opens the continuous zoom ladder (AtlasWindowViewer) directly at
## the canonical orbital frame, retiring the click-through as the sole entry
## (see regional_screen.gd's own doc). This regression-guards the NEW
## wiring: entering "regional" reaches AtlasWindowViewer, not AtlasViewer.
func test_regional_screen_wraps_atlas_window_viewer_not_atlas_viewer() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
var received: Array = []
screen.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
screen._viewer.district_descend_requested.emit(Vector2i(5, 7))
assert_int(received.size()).is_equal(1)
assert_that(received[0]).is_equal(Vector2i(5, 7))
assert_object(screen._viewer).override_failure_message(
"RegionalScreen must wrap AtlasWindowViewer (the zoom ladder) since T-1153,"
+ " not the retired AtlasViewer heightmap-texture display"
).is_instanceof(AtlasWindowViewer)
# =============================================================================
+69
View File
@@ -180,3 +180,72 @@ func test_district_and_quarter_windows_coexist_at_identical_body_center_n() -> v
cache.get_window("GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY)
).is_equal(district_window)
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32, 4)).is_equal(quarter_window)
# =============================================================================
# granularity_v2 (T-1152/T-1153): the string-tag axis — the ONLY thing that
# distinguishes Region from District/Quarter, since Region has no legal
# legacy-int representation (WindowGranularity::legacy_u32() returns None for
# Region — see the server's own doc). This is the SAME mandatory-aliasing
# regression class as the granularity/min_wl_m tests above, extended to the
# new axis.
# =============================================================================
## **MANDATORY aliasing regression (T-1152/T-1153):** a Region-rung key and a
## District-rung key at the IDENTICAL (body_id, center, n, legacy
## granularity, min_wl_m) must be DISTINCT cache keys — the legacy int slot
## alone (both "District" and default-omitted callers pass
## DISTRICT_GRANULARITY=1) cannot tell them apart; granularity_v2 is what
## does.
func test_make_key_distinguishes_granularity_v2_region_from_district() -> void:
var k_district := AtlasWindowCache.make_key(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
)
var k_region := AtlasWindowCache.make_key(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
)
assert_str(k_district).is_not_equal(k_region)
## Omitting granularity_v2 (every pre-T-1152 call site) must produce the SAME
## key as passing the explicit "District" default — byte/string
## compatibility, same contract as the legacy granularity/min_wl_m defaults.
func test_omitted_granularity_v2_matches_explicit_district_default() -> void:
var k_omitted := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
var k_explicit := AtlasWindowCache.make_key(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
)
assert_str(k_omitted).is_equal(k_explicit)
## End-to-end through put()/get_window(): a Region-rung window and a
## District-rung window at the identical (body, center, n) must both be
## independently retrievable — the exact scenario a full-zoom-out-then-back-in
## at the SAME (center, n) would hit if a player oscillates across the
## District/Region boundary.
func test_region_and_district_windows_coexist_at_identical_body_center_n() -> void:
var cache := AtlasWindowCache.new()
var district_window := {"granularity_v2": "District", "id": "district"}
var region_window := {"granularity_v2": "Region", "id": "region"}
cache.put(
"GJ1c", Vector2i(10, 20), 32, district_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
"District"
)
cache.put(
"GJ1c", Vector2i(10, 20), 32, region_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
"Region"
)
assert_int(cache.size()).is_equal(2)
assert_that(
cache.get_window(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
)
).is_equal(district_window)
assert_that(
cache.get_window(
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
)
).is_equal(region_window)
+664
View File
@@ -265,3 +265,667 @@ func test_pole_wall_rows_half_matches_canonicalize_rows_half() -> void:
Vector2i(0, rows_half), radius_km
)
assert_int(canonical.y).is_equal(rows_half)
# =============================================================================
# T-1153: select_rung() — REDESIGNED (live round 3 finding) per-rung
# single-window COVERAGE CEILING model, superseding the original
# `2x`-visual-tolerance-only reading of design doc §5. Select the FINEST
# rung whose own single-window coverage ceiling (MAX_COVERAGE_M) still
# covers the current world extent: Quarter <= 32,768 m; District <=
# 131,072 m; Region otherwise (including tiled coverage beyond its own
# single-window ceiling, a viewer-level concern — see select_rung()'s own
# doc for the full derivation and why this REPLACES the earlier two-gate
# design entirely, not just patches it).
# =============================================================================
## Deep zoom-in (a tiny extent) selects Quarter — comfortably under its own
## 32,768 m ceiling.
func test_select_rung_picks_quarter_well_under_its_ceiling() -> void:
var rung: String = AtlasWindowGeometry.select_rung(2000.0, 1000.0)
assert_str(rung).is_equal("Quarter")
## An extent past Quarter's own ceiling but under District's selects
## District — the finest rung that can still cover it in one window.
func test_select_rung_picks_district_between_the_two_ceilings() -> void:
# 60,000 m is past Quarter's 32,768 m ceiling but well under District's
# 131,072 m one.
var rung: String = AtlasWindowGeometry.select_rung(60_000.0, 100.0)
assert_str(rung).is_equal("District")
## An extent past BOTH Quarter's and District's ceilings selects Region —
## neither finer rung's single window can cover this much world.
func test_select_rung_picks_region_past_both_finer_ceilings() -> void:
var rung: String = AtlasWindowGeometry.select_rung(40_075_264.0, 1920.0)
assert_str(rung).is_equal("Region")
## Exactly AT Quarter's own ceiling (32,768 m) must still select Quarter —
## the rule is `<=`, not `<`.
func test_select_rung_quarter_ceiling_boundary_is_inclusive() -> void:
var rung: String = AtlasWindowGeometry.select_rung(32_768.0, 100.0)
assert_str(rung).is_equal("Quarter")
## One metre past Quarter's ceiling must flip to District — confirms the
## ceiling bites right at its own boundary, not one cell short of it.
func test_select_rung_one_past_quarter_ceiling_is_district() -> void:
var rung: String = AtlasWindowGeometry.select_rung(32_769.0, 100.0)
assert_str(rung).is_equal("District")
## Exactly AT District's own ceiling (131,072 m) must still select District.
func test_select_rung_district_ceiling_boundary_is_inclusive() -> void:
var rung: String = AtlasWindowGeometry.select_rung(131_072.0, 100.0)
assert_str(rung).is_equal("District")
## One metre past District's ceiling must flip to Region.
func test_select_rung_one_past_district_ceiling_is_region() -> void:
var rung: String = AtlasWindowGeometry.select_rung(131_073.0, 100.0)
assert_str(rung).is_equal("Region")
## canvas_px is unused by the coverage rule (kept for signature stability,
## see select_rung()'s own doc) — degenerate/zero values must not change the
## selected rung at all, unlike the old `2x`-tolerance design's special-cased
## fallback.
func test_select_rung_canvas_px_does_not_affect_selection() -> void:
var with_real_canvas: String = AtlasWindowGeometry.select_rung(2000.0, 1000.0)
var with_zero_canvas: String = AtlasWindowGeometry.select_rung(2000.0, 0.0)
assert_str(with_zero_canvas).is_equal(with_real_canvas)
## spacing_for_rung() is select_rung()'s inverse lookup — pin the three known
## values against the D-243 constants directly (not against RUNG_TABLE
## indices, which would just restate the implementation).
func test_spacing_for_rung_matches_d243_constants() -> void:
assert_float(AtlasWindowGeometry.spacing_for_rung("Quarter")).is_equal_approx(512.0, 0.001)
assert_float(AtlasWindowGeometry.spacing_for_rung("District")).is_equal_approx(2048.0, 0.001)
assert_float(AtlasWindowGeometry.spacing_for_rung("Region")).is_equal_approx(204_800.0, 0.001)
## An unknown tag falls back to District — matching the server's own
## "unknown -> District" posture at every wire-decode boundary.
func test_spacing_for_rung_unknown_tag_falls_back_to_district() -> void:
assert_float(AtlasWindowGeometry.spacing_for_rung("Nonsense")).is_equal_approx(2048.0, 0.001)
## MAX_COVERAGE_M's three values, pinned directly against the formulas
## select_rung()'s own doc derives them from — a regression guard
## independent of select_rung()'s own boundary tests above, so a future
## accidental edit to the constants table itself (not just the selection
## logic) is caught here too.
func test_max_coverage_m_matches_derived_formulas() -> void:
assert_float(AtlasWindowGeometry.MAX_COVERAGE_M["Quarter"]).is_equal_approx(32_768.0, 0.001)
assert_float(AtlasWindowGeometry.MAX_COVERAGE_M["District"]).is_equal_approx(131_072.0, 0.001)
assert_float(AtlasWindowGeometry.MAX_COVERAGE_M["Region"]).is_equal_approx(13_107_200.0, 0.001)
## The exact scenario that surfaced the original design flaw
## (live-testing enter_orbital()'s own fit zoom): a whole Earth-like body's
## circumference (~40,075 km, matching AtlasDescendGeometry.district_extent()'s
## own cols*DISTRICT_M for radius=6371km) fitted to a 1920px-wide viewport at
## CELL_PIXEL_SIZE=16 must select Region — the direct regression guard for
## the bug an early version of select_rung() had (picking District here,
## which would have meant the canonical orbital frame requests a
## District-tier derive spanning an entire planet — the exact R1-catastrophe
## cost scenario the design doc §4 rejects).
func test_select_rung_at_orbital_fit_zoom_selects_region() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
var composite_native: float = float(n) * CELL_PIXEL_SIZE
var viewport := Vector2(1920.0, 1080.0)
var fit_zoom: float = maxf(viewport.x, viewport.y) / composite_native
var world_extent: float = AtlasWindowGeometry.world_extent_m(CELL_PIXEL_SIZE, fit_zoom, viewport)
var rung: String = AtlasWindowGeometry.select_rung(
world_extent, maxf(viewport.x, viewport.y)
)
assert_str(rung).override_failure_message(
"the canonical orbital fit-zoom (whole-planet view) must select Region,"
+ " never a District-tier derive spanning an entire planet"
).is_equal("Region")
## **Live round 3 regression, the direct fix target:** at 1600x900 (the
## coordinator's capture viewport), zooming IN from the orbital fit all the
## way to Quarter's own ceiling must pass through District along the way —
## a wheel-zoom gesture crossing world_extent_m from Region's territory down
## to Quarter's must select District for SOME real span of extent in
## between, not skip straight from Region to Quarter (the exact "money shot"
## the coordinator wants capture-worthy: a visible SHARPEN in place, not a
## jump).
func test_select_rung_district_is_reachable_between_region_and_quarter() -> void:
# An extent comfortably between District's and Quarter's ceilings (e.g.
# the midpoint) must select District — proving the band is non-empty,
# unlike the old two-gate design where it was empty by construction at
# every real viewport (see git history / the coordinator's live-round
# finding for the retired analysis).
var midpoint: float = (
(AtlasWindowGeometry.MAX_COVERAGE_M["Quarter"] as float)
+ (AtlasWindowGeometry.MAX_COVERAGE_M["District"] as float)
) * 0.5
var rung: String = AtlasWindowGeometry.select_rung(midpoint, 1600.0)
assert_str(rung).override_failure_message(
"District must be reachable between Quarter's and District's own"
+ " coverage ceilings — the redesigned rule must not skip it"
).is_equal("District")
# =============================================================================
# T-1153: world_extent_m() — the `E` half of the §5 rule, computed from the
# viewer's own zoom/viewport state.
# =============================================================================
## At zoom=1.0, CELL_PIXEL_SIZE=16: one DISTRICT (2,048 m, the fixed display
## unit — see world_extent_m()'s own doc for why this is rung-INDEPENDENT)
## occupies 16 screen px, so a 1920px-wide viewport shows
## 1920/16 * 2048 = 245,760 m.
func test_world_extent_m_at_zoom_one() -> void:
var extent: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
assert_float(extent).is_equal_approx(1920.0 / CELL_PIXEL_SIZE * 2048.0, 1.0)
## Doubling the zoom must HALVE the displayed world extent — zooming in
## shows less world, not more.
func test_world_extent_m_halves_when_zoom_doubles() -> void:
var extent_1x: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
var extent_2x: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 2.0, Vector2(1920.0, 1080.0)
)
assert_float(extent_2x).is_equal_approx(extent_1x * 0.5, 1.0)
## The composite's on-screen footprint is rung-invariant (world_extent_m()'s
## own doc) — a change in held rung with NO change in zoom/viewport must
## leave the displayed world extent UNCHANGED. This is the direct regression
## test for the bug this function's signature once had (a granularity_v2
## parameter that silently changed the formula per rung, when only zoom
## should) — the function no longer TAKES a rung parameter at all, so this
## pins that omission is intentional, not an oversight.
func test_world_extent_m_has_no_rung_parameter() -> void:
var extent_a: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
var extent_b: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
assert_float(extent_a).is_equal_approx(extent_b, 0.001)
## Degenerate zoom (<=0) must not divide by zero — a safe zero extent.
func test_world_extent_m_degenerate_zoom_is_safe() -> void:
var extent: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 0.0, Vector2(1920.0, 1080.0)
)
assert_float(extent).is_equal_approx(0.0, 0.001)
# =============================================================================
# T-1153: is_fully_zoomed_out() — Jeroen's HARD condition's trigger predicate.
# =============================================================================
func test_is_fully_zoomed_out_true_when_extent_covers_full_circumference() -> void:
var radius_km := 6371.0
var circumference_m: float = TAU * radius_km * 1000.0
assert_bool(AtlasWindowGeometry.is_fully_zoomed_out(circumference_m, radius_km)).is_true()
assert_bool(
AtlasWindowGeometry.is_fully_zoomed_out(circumference_m * 1.5, radius_km)
).is_true()
func test_is_fully_zoomed_out_false_when_extent_is_less_than_circumference() -> void:
var radius_km := 6371.0
var circumference_m: float = TAU * radius_km * 1000.0
assert_bool(
AtlasWindowGeometry.is_fully_zoomed_out(circumference_m * 0.5, radius_km)
).is_false()
## A no-radius body (tiny test body) has no circumference concept — never
## auto-resets, matching enter_orbital()'s own no-radius fallback disposition.
func test_is_fully_zoomed_out_false_for_no_radius_body() -> void:
assert_bool(AtlasWindowGeometry.is_fully_zoomed_out(1e12, 0.0)).is_false()
# =============================================================================
# T-1153: screen_center_to_district() — the shared screen<->district formula
# behind both the pan-edge refetch and the rung-reselect refetch.
# =============================================================================
## At the exact center of a symmetric fit (offset centers the composite,
## zoom=1.0), the screen center must map back to the held center exactly.
func test_screen_center_to_district_at_rest_returns_held_center() -> void:
var held_n := 32
var held_center := Vector2i(10, 20)
var composite_native: float = float(held_n) * CELL_PIXEL_SIZE
var viewport := Vector2(composite_native, composite_native)
var offset := Vector2.ZERO # composite exactly fills the viewport, top-left at origin
var result: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, offset, 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
assert_that(result).is_equal(held_center)
## Panning the offset must shift the recovered district position in the
## OPPOSITE direction of the offset shift (dragging the composite right
## reveals districts to the WEST at screen-center).
func test_screen_center_to_district_shifts_with_pan_offset() -> void:
var held_n := 32
var held_center := Vector2i(0, 0)
var composite_native: float = float(held_n) * CELL_PIXEL_SIZE
var viewport := Vector2(composite_native, composite_native)
var at_rest: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, Vector2.ZERO, 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
var panned: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, Vector2(CELL_PIXEL_SIZE * 4.0, 0.0), 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
assert_int(panned.x).override_failure_message(
"dragging the composite EAST (positive offset) must reveal districts to the WEST"
).is_less(at_rest.x)
# =============================================================================
# T-1153 (moved from atlas_window_viewer.gd for testability): WASD held-pan
# direction is exercised live only (reads the global Input singleton) —
# edge-scroll suppression/direction are pure and covered here directly.
# =============================================================================
func test_is_cursor_edge_scrolling_true_near_an_edge() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, false, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_true()
func test_is_cursor_edge_scrolling_false_away_from_any_edge() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, false, Vector2(800.0, 600.0), Vector2(400.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_is_cursor_edge_scrolling_suppressed_when_over_ui() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, true, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_is_cursor_edge_scrolling_suppressed_without_app_focus() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
false, false, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_edge_scroll_direction_points_west_near_left_edge() -> void:
var direction: Vector2 = AtlasWindowGeometry.edge_scroll_direction(
Vector2(800.0, 600.0), Vector2(5.0, 300.0), 24.0
)
assert_float(direction.x).is_less(0.0)
assert_float(direction.y).is_equal_approx(0.0, 0.001)
# =============================================================================
# T-1153, live round 3 (Jeroen's ruling, design doc §4): compute_tile_grid()
# — the orbital rest state's multi-window mosaic.
# =============================================================================
## The exact live-round scenario: GJ380c/Lendel (radius 6238.4 km) needs a
## 3x2 = 6-tile grid — the coordinator's own estimate, confirmed here as an
## executable regression.
func test_compute_tile_grid_lendel_produces_six_tiles() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(6238.4)
assert_int(tiles.size()).override_failure_message(
"GJ380c/Lendel must tile into 3x2=6 windows, matching the coordinator's own"
+ " live-round finding (13,107.2 km single-window coverage vs. 39,198 km"
+ " circumference)"
).is_equal(6)
## A tiny body whose whole circumference fits in ONE Region window's
## coverage ceiling must produce exactly ONE tile — tiling degenerates
## gracefully to the pre-existing single-window behavior when it isn't
## actually needed.
func test_compute_tile_grid_tiny_body_produces_one_tile() -> void:
# radius small enough that circumference << MAX_COVERAGE_M["Region"]
# (13,107,200 m) — a few hundred km radius comfortably qualifies.
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(50.0)
assert_int(tiles.size()).is_equal(1)
assert_that(tiles[0]).is_equal(Vector2i.ZERO)
## A no-radius body (tiny test body) must produce exactly one tile at the
## canonical origin — matching enter_orbital()'s own no-radius fallback
## disposition (no circumference/tiling concept without a radius).
func test_compute_tile_grid_no_radius_produces_single_origin_tile() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(0.0)
assert_int(tiles.size()).is_equal(1)
assert_that(tiles[0]).is_equal(Vector2i.ZERO)
## Every tile center must be a LEGAL canonicalized DistrictPos — column
## wrapped into [0, cols), row clamped into [-rows_half, rows_half] — the
## same range canonicalize_district_center() enforces everywhere else in
## this cluster (pan refetch, entry, rung-reselect). A raw, uncanonicalized
## tile center would fail the server's own normalize_window_center() (or
## silently alias to a different tile than intended).
func test_compute_tile_grid_tiles_are_all_canonicalized() -> void:
var radius_km := 6238.4
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var rows_half: int = int(extent["rows_half"])
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(radius_km)
for tile: Vector2i in tiles:
assert_int(tile.x).override_failure_message(
"tile column %d must be wrapped into [0, %d)" % [tile.x, cols]
).is_greater_equal(0)
assert_int(tile.x).is_less(cols)
assert_int(tile.y).override_failure_message(
"tile row %d must be clamped into [-%d, %d]" % [tile.y, rows_half, rows_half]
).is_greater_equal(-rows_half)
assert_int(tile.y).is_less_equal(rows_half)
## No two tiles may share the same canonicalized center — compute_tile_grid()
## must dedupe (a pole-row clamp or column-wrap collision producing the exact
## same DistrictPos twice would otherwise request/draw the same tile twice,
## wasting a request and drawing one tile over another).
func test_compute_tile_grid_has_no_duplicate_centers() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(6238.4)
var seen: Dictionary = {}
for tile: Vector2i in tiles:
assert_bool(seen.has(tile)).override_failure_message(
"tile center %s appears more than once in the grid" % str(tile)
).is_false()
seen[tile] = true
## The tile grid's own center of mass must land on the canonical origin
## (0,0) — the tile-set's symmetric layout (each axis' centers computed as
## `(index - (count-1)/2) * TILE_N`) is centered on the SAME canonical origin
## enter_orbital() uses, so the tile-set's overall framing agrees with
## single-window enter_orbital()'s own "center on (0,0)" contract.
func test_compute_tile_grid_is_centered_on_the_canonical_origin() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(6238.4)
var sum_col := 0
var sum_row := 0
for tile: Vector2i in tiles:
sum_col += tile.x
sum_row += tile.y
# Column centers wrap (periodic), so a raw average isn't meaningful there
# the way it is for rows — assert row symmetry directly instead (rows
# never wrap, so their average must be very close to 0 for a
# symmetric grid).
var avg_row: float = float(sum_row) / float(tiles.size())
assert_float(avg_row).override_failure_message(
"the tile grid's row centers must average to ~0 (symmetric around the"
+ " canonical origin's equator row)"
).is_equal_approx(0.0, float(AtlasWindowGeometry.TILE_N))
# =============================================================================
# Live round 4: district_to_canvas_local() + recompute_offset_for_held_n_change()
# — the two pure functions behind both round-4 draw-path fixes (tile mosaic
# placement, single-window offset recompute across a rung crossing).
# =============================================================================
## A district AT the held window's own center must land at canvas-local
## `(held_n/2 * cell_px, held_n/2 * cell_px)` — the center of the
## `[0, held_n*cell_px)` square the single-window `Rect2(0,0,extent,extent)`
## draw call already assumes.
func test_district_to_canvas_local_center_district_lands_at_half_extent() -> void:
var held_center := Vector2i(100, 200)
var held_n := 64
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
Vector2(held_center), held_center, held_n, CELL_PIXEL_SIZE
)
var expected: float = float(held_n) * 0.5 * CELL_PIXEL_SIZE
assert_that(result).is_equal(Vector2(expected, expected))
## The window's own top-left corner (held_center - held_n/2) must land at
## canvas-local (0,0) — the exact invariant single-window `_draw()` and
## `fit_window_view()` both assume.
func test_district_to_canvas_local_top_left_corner_lands_at_origin() -> void:
var held_center := Vector2i(0, 0)
var held_n := 32
var top_left := Vector2(held_center) - Vector2.ONE * (float(held_n) * 0.5)
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
top_left, held_center, held_n, CELL_PIXEL_SIZE
)
assert_that(result).is_equal(Vector2.ZERO)
## Live round 4's OWN repro, pinned directly: a tile far from held_center
## (0,0) at whole-body scale (held_n ~19,139, Lendel's raw circumference)
## must NOT land near canvas-local (0,0) — the round-4 bug's exact failure
## mode (treating absolute district (0,0) as the canvas origin regardless of
## held_center/held_n) would place it there instead.
func test_district_to_canvas_local_matches_the_live_round_4_repro_scale() -> void:
var held_center := Vector2i.ZERO
var held_n := 19139 # Lendel's raw district-column count (live round 4's own repro)
var tile_center := Vector2(6400, 0) # one TILE_N east of the body's own center
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
tile_center, held_center, held_n, CELL_PIXEL_SIZE
)
var buggy_result: Vector2 = tile_center * CELL_PIXEL_SIZE # the round-4 bug's own formula
assert_bool(is_equal_approx(result.x, buggy_result.x)).override_failure_message(
"a tile away from held_center must NOT land where the round-4 bug's"
+ " absolute-district-(0,0)-relative formula would put it — got %.1f, the"
+ " buggy formula's own value is %.1f"
% [result.x, buggy_result.x]
).is_false()
## Zero held_n is a degenerate/never-real-in-practice input (a body always
## has SOME district extent) but must not divide-by-zero or crash — `half`
## is simply 0, so the district maps 1:1 to canvas-local (scaled by cell_px).
func test_district_to_canvas_local_zero_held_n_does_not_crash() -> void:
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
Vector2(5, 5), Vector2i.ZERO, 0, CELL_PIXEL_SIZE
)
assert_that(result).is_equal(Vector2(5, 5) * CELL_PIXEL_SIZE)
# =============================================================================
# Live round 5: nearest_wrap_image() — the tile-mosaic WRAP half of "the
# mosaic doesn't fully draw" (the left-third-black repro).
# =============================================================================
## Live round 5's OWN repro, pinned exactly: Lendel's wrapped tile
## canonicalizes to column 12739 (`-6400 mod 19139`) — the CORRECT
## request/cache key — but its nearest wrap-image relative to the canonical
## origin (held_center.x = 0) is -6400, the actual visible position
## immediately west of center.
func test_nearest_wrap_image_matches_the_lendel_repro() -> void:
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, 0, 19139)
assert_int(result).override_failure_message(
"the wrapped tile's nearest wrap-image relative to held_center=0 must be"
+ " -6400 (its actual on-screen position), not 12739 (the correct REQUEST"
+ " key, but the wrong DRAW position)"
).is_equal(-6400)
## The two Lendel tiles that were NEVER wrapped (already close to
## held_center) must round-trip unchanged — the fix must not perturb tiles
## that were already drawing correctly.
func test_nearest_wrap_image_is_a_noop_for_already_nearby_columns() -> void:
var cols := 19139
for col: int in [0, 6400]:
var result: int = AtlasWindowGeometry.nearest_wrap_image(col, 0, cols)
assert_int(result).override_failure_message(
"column %d is already the nearest wrap-image to held_center=0 — must"
+ " be returned unchanged" % col
).is_equal(col)
## The result must always be a LEGAL wrap-image of the canonical column —
## i.e. `result mod cols == canonical_col mod cols` — regardless of which
## image is nearest. This is the correctness invariant the whole function
## exists to preserve: re-expressing a column for DRAWING must never change
## WHICH district it actually refers to.
func test_nearest_wrap_image_preserves_the_canonical_identity() -> void:
var cols := 19139
for held_col: int in [-50000, -1, 0, 1, 9569, 19138, 50000]:
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, held_col, cols)
assert_int(posmod(result, cols)).override_failure_message(
"nearest_wrap_image(12739, %d, %d) = %d must still canonicalize back"
+ " to 12739 — it may only pick a DIFFERENT wrap-image, never a"
+ " different district" % [held_col, cols, result]
).is_equal(12739)
## The chosen wrap-image must be the CLOSEST one to held_center — never
## farther than half the circumference away (otherwise a different
## wrap-image would have been nearer).
func test_nearest_wrap_image_is_within_half_circumference_of_held_center() -> void:
var cols := 19139
for canonical_col: int in [0, 1, 9569, 12739, 19138]:
for held_col: int in [-30000, -500, 0, 500, 25000]:
var result: int = AtlasWindowGeometry.nearest_wrap_image(canonical_col, held_col, cols)
var distance: int = absi(result - held_col)
assert_int(distance).override_failure_message(
(
"nearest_wrap_image(%d, %d, %d) = %d is %d districts from"
+ " held_center — must never exceed half the circumference"
+ " (%d), or a closer wrap-image exists"
)
% [canonical_col, held_col, cols, result, distance, cols / 2]
).is_less_equal(cols / 2)
## `cols <= 0` (no-radius bodies, which never tile per compute_tile_grid()'s
## own doc) must be a safe no-op passthrough — no periodicity to resolve.
func test_nearest_wrap_image_zero_cols_is_a_passthrough() -> void:
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, 0, 0)
assert_int(result).is_equal(12739)
## The coordinator's own draw-position counterpart to
## test_compute_tile_grid_tiles_are_all_canonicalized(): the wrapped tile's
## DRAW rect (via district_to_canvas_local(), fed through
## nearest_wrap_image() the way _draw_tile_mosaic() now does) must land
## SUBSTANTIALLY on-canvas when the view covers the whole body — the exact
## Lendel shape (whole-body fit at entry, held_center at the canonical
## origin). A bare `Rect2.intersects()` check is NOT discriminating enough
## here: at Lendel's own whole-body-fit scale, the BUGGY placement (feeding
## the canonical column directly) happens to clip the viewport edge by only
## a couple of px (confirmed by hand-computation — the tile-grid's own
## edge-to-edge tiling means a full-circumference shift lands almost
## exactly one screen-width away, so `intersects()` alone would pass on a
## near-miss that still reads as "the left third is black" visually).
## Asserting a MEANINGFUL overlap FRACTION (at least half the tile's own
## area) is what actually distinguishes "correctly drawn" from "barely
## clipping the edge."
func test_wrapped_tile_draw_rect_lands_substantially_on_canvas_at_whole_body_view() -> void:
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var held_center := Vector2i.ZERO
var held_n: int = cols # enter_orbital()'s own whole-body held_n
var tile_n: int = AtlasWindowGeometry.TILE_N
var half_tile: float = float(tile_n) * 0.5
# The whole-body fit zoom/viewport (matching enter_orbital()'s own fit).
var viewport := Vector2(1600.0, 900.0)
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
viewport, held_n, CELL_PIXEL_SIZE, 0.0001, 64.0
)
var view_zoom: float = fit["zoom"]
var view_offset: Vector2 = fit["offset"]
# The wrapped tile's own canonical center — mirrors compute_tile_grid()'s
# own dedup/canonicalize step for Lendel's westmost tile.
var wrapped_raw_col := -6400
var canonical_col: int = posmod(wrapped_raw_col, cols)
var draw_col: int = AtlasWindowGeometry.nearest_wrap_image(canonical_col, held_center.x, cols)
var tile_top_left := Vector2(float(draw_col) - half_tile, 0.0 - half_tile)
var local_origin: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
tile_top_left, held_center, held_n, CELL_PIXEL_SIZE
)
var extent_px: float = float(tile_n) * CELL_PIXEL_SIZE
# Canvas-local -> screen space: _canvas.position = view_offset,
# _canvas.scale = view_zoom (AtlasWindowViewer._apply_transform()'s own
# transform, mirrored here since this is a pure-geometry test with no
# live Control/Node2D tree).
var screen_top_left: Vector2 = view_offset + local_origin * view_zoom
var screen_extent: Vector2 = Vector2(extent_px, extent_px) * view_zoom
var tile_rect := Rect2(screen_top_left, screen_extent)
var viewport_rect := Rect2(Vector2.ZERO, viewport)
var overlap: Rect2 = viewport_rect.intersection(tile_rect)
var tile_area: float = screen_extent.x * screen_extent.y
var overlap_fraction: float = 0.0
if tile_area > 0.0:
overlap_fraction = (overlap.size.x * overlap.size.y) / tile_area
assert_float(overlap_fraction).override_failure_message(
(
"the wrapped tile's draw rect %s overlaps the viewport %s by only"
+ " %.1f%% of its own area — must be at least 50%% when the view"
+ " covers the whole body. This is live round 5's 'left third of the"
+ " mosaic is black' repro: drawing the CANONICAL column (%d) directly"
+ " (without nearest_wrap_image()) places this tile off-canvas RIGHT"
+ " instead of its true position on the LEFT"
)
% [tile_rect, viewport_rect, overlap_fraction * 100.0, canonical_col]
).is_greater_equal(0.5)
## The core contract this function exists for: recomputing `_view_offset` so
## a KNOWN screen point continues to map to canvas-local
## `new_held_n/2 * cell_px` (the new window's own center) — i.e. feeding the
## OUTPUT back through district_to_canvas_local()'s own "center district ->
## half-extent local" identity (tested above) and applying the resulting
## transform must reproduce the SAME screen point exactly.
func test_recompute_offset_for_held_n_change_preserves_the_screen_point() -> void:
var screen_point := Vector2(800.0, 450.0)
var view_zoom := 2.5
var new_held_n := 16
var offset: Vector2 = AtlasWindowGeometry.recompute_offset_for_held_n_change(
screen_point, view_zoom, new_held_n, CELL_PIXEL_SIZE
)
var new_local: Vector2 = Vector2.ONE * (float(new_held_n) * 0.5 * CELL_PIXEL_SIZE)
var reconstructed_screen_point: Vector2 = new_local * view_zoom + offset
assert_that(reconstructed_screen_point).is_equal_approx(screen_point, Vector2.ONE * 0.01)
## Live round 4's OWN repro: crossing from Region (~thousands-districts held_n)
## to District (64) or Quarter (16) must produce a DIFFERENT offset than
## leaving `_view_offset` untouched would — pinning that this function's
## OUTPUT actually depends on `new_held_n` (the exact thing the round-4 bug
## got wrong by never calling this function at all).
func test_recompute_offset_for_held_n_change_differs_for_different_held_n() -> void:
var screen_point := Vector2(800.0, 450.0)
var view_zoom := 3.378 # live round 4's own District-band zoom value
var offset_district: Vector2 = AtlasWindowGeometry.recompute_offset_for_held_n_change(
screen_point, view_zoom, 64, CELL_PIXEL_SIZE
)
var offset_quarter: Vector2 = AtlasWindowGeometry.recompute_offset_for_held_n_change(
screen_point, view_zoom, 16, CELL_PIXEL_SIZE
)
assert_that(offset_district).override_failure_message(
"a rung crossing that changes held_n must recompute a DIFFERENT"
+ " _view_offset — reusing the same offset across the crossing is"
+ " exactly the live round 4 bug (composite renders off-canvas)"
).is_not_equal(offset_quarter)
+119 -4
View File
@@ -23,10 +23,14 @@ static func _mock_window(n: int = 2) -> Dictionary:
## 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).
## 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 = ""
@@ -40,6 +44,9 @@ class _ViewerStub:
func get_cell_pixel_size() -> float:
return 16.0
func is_tile_mode() -> bool:
return false
func test_composite_smooth_defaults_true() -> void:
assert_bool(AtlasWindowOverlay.COMPOSITE_SMOOTH).override_failure_message(
@@ -150,3 +157,111 @@ func test_draw_builds_a_texture_through_the_viewer_stub() -> void:
o.viewer = stub
o._draw()
assert_that(o._cached_texture).is_not_null()
# =============================================================================
# T-1152/T-1153: cell_grid_side_for_window() — the district-extent-vs-
# derived-cell-grid split every rung's response now carries.
# =============================================================================
## District (the default/omitted tag): cell_grid_side == n, unchanged from
## the pre-T-1152 identity mapping.
func test_cell_grid_side_for_window_district_matches_n() -> void:
var window: Dictionary = {"n": 32, "granularity_v2": "District"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(32)
## Quarter: 4x MORE cells than districts (WINDOW_GRANULARITY_QUARTER).
func test_cell_grid_side_for_window_quarter_multiplies_by_four() -> void:
var window: Dictionary = {"n": 32, "granularity_v2": "Quarter"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(128)
## Region: FAR FEWER cells than districts — round(n/100), matching
## WindowGranularity::cell_grid_side's own Region branch exactly (the
## "inversion" the server doc calls out: finer rungs multiply, Region divides).
func test_cell_grid_side_for_window_region_divides_by_districts_per_region() -> void:
var window: Dictionary = {"n": 6400, "granularity_v2": "Region"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(64)
## A Region window smaller than one region (n < 100) must still derive a
## minimum 1x1 cell grid, never 0 — matching the server's `.max(1)`.
func test_cell_grid_side_for_window_region_minimum_is_one() -> void:
var window: Dictionary = {"n": 50, "granularity_v2": "Region"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(1)
## Missing granularity_v2 (an old-shape response) falls back to District —
## matching the server's own "unknown -> District" posture at every
## resolution boundary.
func test_cell_grid_side_for_window_missing_tag_falls_back_to_district() -> void:
var window: Dictionary = {"n": 32}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(32)
## T-1152/T-1153, design doc §6 encoding continuity: a Region-rung window (a
## FAR SPARSER cell grid — one region cell spans 100 districts) renders
## through the EXACT SAME _draw()/_rebuild_texture_if_needed() path as
## District — no separate branch, no crash reading past the (much smaller)
## per-cell arrays. This is the direct "same colorizer family at every rung"
## behavioral test the ticket asks for.
func test_draw_builds_a_texture_for_a_region_rung_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
# n=200 districts -> cell_grid_side = round(200/100) = 2 -> 4 cells,
# matching the 4-entry per-cell arrays below (same shape _mock_window()
# uses, just at Region's district-to-cell ratio).
stub.window = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o.viewer = stub
o._draw()
assert_that(o._cached_texture).override_failure_message(
"a Region-rung window must render through the same composite path as District"
).is_not_null()
assert_int(o._cached_texture.get_width()).override_failure_message(
"the built texture's resolution must be the DERIVED cell-grid side (2),"
+ " not the window's district extent (200)"
).is_equal(2)
## §6 "no mode flip" acceptance criterion, restated at the texture-cache
## level: swapping from a District-rung window to a Region-rung window at a
## NEW window object (the progressive-refinement swap) must still go through
## a single rebuild call producing a fresh texture — not a crash, not a
## silently-stale texture sized for the wrong rung.
func test_rebuild_handles_a_rung_swap_from_district_to_region() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var district_window: Dictionary = _mock_window() # n=2, District, 4 cells
o._rebuild_texture_if_needed(
district_window, AtlasWindowOverlay.cell_grid_side_for_window(district_window), ""
)
assert_int(o._cached_texture.get_width()).is_equal(2)
var region_window: Dictionary = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o._rebuild_texture_if_needed(
region_window, AtlasWindowOverlay.cell_grid_side_for_window(region_window), ""
)
assert_int(o._cached_texture.get_width()).override_failure_message(
"a rung swap must rebuild at the NEW rung's derived cell-grid resolution"
).is_equal(2) # region_window's cell_grid_side is also 2 here (200/100) — same size, different data
@@ -0,0 +1,359 @@
## Live round 4: a REAL draw smoke test — the "does anything draw at all"
## gap has now bitten twice (round 4's tile-mosaic coordinate bug AND its
## per-tile-texture-lifetime bug, both invisible to test_atlas_window_overlay.gd's
## existing suite, which only asserts on the CACHE FIELDS being populated —
## never on an actual composited pixel). This file closes that gap
## structurally: render AtlasWindowOverlay into a REAL SubViewport, force a
## GPU sync, grab the rendered Image, and assert a meaningful fraction of
## pixels differ from the background color — for BOTH the single-window path
## (a) and the tile-mosaic path (b), matching the coordinator's explicit ask.
##
## **REQUIRES A REAL RENDERING DRIVER — SKIPS (not fails) under
## `tests/run-godot`'s hardcoded `--headless`** (dummy driver, no GPU texture
## output; confirmed directly: SubViewport.get_texture().get_image() returns
## an all-zero/unusable image under it). This matters beyond "the assertions
## are meaningless there": the push gate runs the FULL suite through
## `tests/run-godot --headless` for every push, for everyone — a loud FAILURE
## here would bounce every future push project-wide, not just report a local
## false negative. Every test below carries the gdUnit4 fuzzer-arg skip
## convention (`_do_skip`/`_skip_reason`, matching test_input_gate_live.gd's
## own server-binary-not-built skip) keyed on `_dummy_renderer_active()`, so
## `tests/run-godot --filter test_atlas_window_overlay_draw_smoke` reports
## green-with-skips under headless, not red.
##
## To actually exercise this file's assertions, run it with a real driver:
## godot4 --display-driver x11 --rendering-driver opengl3 \
## -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c \
## -a res://tests/test_atlas_window_overlay_draw_smoke.gd
## (matching tests/visual_capture.gd's own documented "requires a real
## rendering driver" precedent — see docs/DEVOPS.md's own note on this file.)
class_name TestAtlasWindowOverlayDrawSmoke
extends GdUnitTestSuite
const AtlasWindowOverlay := preload("res://ui/implant/apps/atlas/atlas_window_overlay.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
const COLOR_BG: Color = Color("#0d1117") # AtlasWindowViewer.COLOR_BG, mirrored (private const)
const VIEWPORT_SIZE: Vector2i = Vector2i(512, 512)
## Minimum fraction of the captured image that must differ from COLOR_BG for
## a draw to count as "genuinely rendered something" — low enough to tolerate
## a mostly-water/mostly-one-color composite (round 4's own repro shots were
## legitimately near-uniform ocean at some zooms), high enough that a
## fully-blank/fully-background/fully-white frame (both round 4 bugs) fails it.
const MIN_NON_BACKGROUND_FRACTION: float = 0.05
const SKIP_REASON: String = (
"no real rendering driver (dummy/headless) — run with e.g."
+ " `godot4 --display-driver x11 --rendering-driver opengl3"
+ " -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c"
+ " -a res://tests/test_atlas_window_overlay_draw_smoke.gd` to exercise this file"
)
## True under Godot's `--display-driver headless` (the dummy renderer
## `tests/run-godot`'s hardcoded `--headless` flag selects) — `DisplayServer.
## get_name()` reports `"headless"` there and the real driver name (`"X11"`,
## `"Wayland"`, etc.) otherwise, confirmed directly against both this
## worktree's `tests/run-godot` invocation and a real `--display-driver x11
## --rendering-driver opengl3` run. Named as a function, not a const, since
## `DisplayServer` singleton state isn't available at script-parse time.
static func _dummy_renderer_active() -> bool:
return DisplayServer.get_name() == "headless"
## Single-window viewer stub — mirrors test_atlas_window_overlay.gd's
## _ViewerStub exactly (is_tile_mode() -> false), so this exercises the
## SAME single-window draw path that suite's cache tests cover, just
## through a REAL render instead of inspecting `_cached_texture` directly.
class _SingleWindowViewerStub:
var window: Variant = null
func get_district_window() -> Variant:
return window
func is_overlay_visible(_overlay_id: String) -> bool:
return false
func get_cell_pixel_size() -> float:
return 16.0
func is_tile_mode() -> bool:
return false
## Tile-mode viewer stub — is_tile_mode() -> true, get_tile_set() returns a
## bare object exposing get_tiles() (AtlasWindowOverlay's own duck-typed
## contract, matching AtlasWindowTileSet.get_tiles()'s public shape exactly:
## Array of {"center": Vector2i, "window": Variant}).
class _TileModeViewerStub:
var tiles: Array = []
var held_center: Vector2i = Vector2i.ZERO
var held_n: int = 0
# Live round 5: nearest_wrap_image()'s cols input — 0 here (a no-radius
# passthrough) is fine for these tests, which don't exercise the wrap
# seam itself (that's test_atlas_window_geometry.gd's own coverage);
# this stub only needs to satisfy _draw_tile_mosaic()'s duck-typed call.
var body_radius_km: float = 0.0
func get_district_window() -> Variant:
return null
func is_overlay_visible(_overlay_id: String) -> bool:
return false
func get_cell_pixel_size() -> float:
return 16.0
func is_tile_mode() -> bool:
return true
func get_tile_set() -> Variant:
return _TileSetStub.new(tiles)
func get_held_center() -> Vector2i:
return held_center
func get_held_n() -> int:
return held_n
func get_body_radius_km() -> float:
return body_radius_km
class _TileSetStub:
var _tiles: Array = []
func _init(tiles: Array) -> void:
_tiles = tiles
func get_tiles() -> Array:
return _tiles
## A `Node2D._draw()`-based flat-fill background — deliberately NOT a
## `ColorRect` (a `Control`). A `ColorRect` parented directly under a bare
## `SubViewport` (no intervening `Control` container establishing its own
## layout rect) did not reliably render in this harness: sampled pixels came
## back fully transparent `(0,0,0,0)` regardless of the ColorRect's `color`/
## `size`, even after entering the tree before sizing. `AtlasWindowOverlay`
## itself is a bare `Node2D` using `draw_rect()` for its own background wash
## (`AtlasWindowViewer._draw()`'s own `COLOR_BG` fill) — matching that same,
## already-proven-working `Node2D.draw_rect()` pattern here sidesteps
## whatever `Control`-specific layout/compositing gap caused the ColorRect
## failure, rather than debugging that gap for its own sake.
class _BackgroundRect extends Node2D:
var fill_color: Color = Color.BLACK
var fill_size: Vector2 = Vector2.ZERO
func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, fill_size), fill_color)
static func _mock_window(center: Vector2i = Vector2i.ZERO, n: int = 64) -> Dictionary:
# A checkerboard-ish morphology spread (not all-one-zone) so the built
# composite has genuine color VARIATION, not just "one flat non-background
# color" — closer to what a real terrain response looks like.
var cells: int = n * n
var morphology := PackedByteArray()
var elev_q := PackedByteArray()
morphology.resize(cells)
elev_q.resize(cells)
for i in range(cells):
morphology[i] = (i % 4) as int # cycles through the 4 morphology zones
elev_q[i] = (i * 7) % 100 as int
return {
"center": [center.x, center.y],
"n": n,
"granularity_v2": "District",
"morphology": morphology,
"elev_q": elev_q,
"temp_dc": [],
"moisture_q": PackedByteArray(),
"vegetation": PackedByteArray(),
"glaciation": PackedByteArray(),
}
## Renders `overlay` (any Node2D with its own `_draw()` — an
## AtlasWindowOverlay for (a)/(b) below, or a bare `_BackgroundRect` probe for
## the harness sanity check) parented under a Node2D positioned/scaled the
## way AtlasWindowViewer._canvas would be, into a fresh SubViewport, and
## returns the captured Image. `zoom` mirrors AtlasWindowViewer._canvas.scale
## (real `fit_window_view()` output is a small fraction, e.g. ~0.006 for a
## whole-body tile mosaic per live round 4's own repro) — WITHOUT it, a
## tile's real-world extent (TILE_N * cell_px = 102,400 local units) is so
## much larger than any realistic test viewport that a mis-POSITIONED tile
## still overlaps the frame purely by being gigantic, making the position
## math this test exists to catch silently unfalsifiable (confirmed
## directly: an earlier version of this test without a zoom scale kept
## passing even with live round 4's tile-coordinate bug deliberately
## reintroduced). Also confirmed directly: a hand-rolled duplicate of this
## same SubViewport/settle-loop setup (the harness sanity check's ORIGINAL
## standalone version) was measurably less reliable under a real driver than
## going through this shared path — reuse over duplication here isn't just
## tidiness, it's the more reliable rendering path.
func _render_to_image(overlay: Node2D, zoom: float = 1.0) -> Image:
var sub_viewport := SubViewport.new()
sub_viewport.size = VIEWPORT_SIZE
sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
sub_viewport.transparent_bg = false
add_child(sub_viewport)
auto_free(sub_viewport)
var bg := _BackgroundRect.new()
bg.fill_color = COLOR_BG
bg.fill_size = Vector2(VIEWPORT_SIZE)
sub_viewport.add_child(bg)
bg.queue_redraw()
var canvas := Node2D.new()
canvas.position = Vector2(VIEWPORT_SIZE) * 0.5 # center the composite's local (0,0)
canvas.scale = Vector2(zoom, zoom)
sub_viewport.add_child(canvas)
canvas.add_child(overlay)
overlay.queue_redraw()
# Bounded settle wait, NOT `await RenderingServer.frame_post_draw` — that
# signal never fires under the dummy/headless driver (confirmed directly:
# a first version of this file using it hung for the full 300s
# tests/run-godot wall-clock cap and was force-killed, producing a FALSE
# "0 tests, passed" result — exactly the silent-hang failure mode the
# `_do_skip`/`_dummy_renderer_active()` gate (this file's header doc) now
# avoids structurally instead). A fixed small number of `process_frame`
# awaits settles real rendering — confirmed sufficient against a real
# driver during this fix's own live verification.
for _i in range(6):
await get_tree().process_frame
return sub_viewport.get_texture().get_image()
## Fraction of `image`'s pixels whose RGB differs from COLOR_BG (alpha
## ignored — the ColorRect background is opaque, everything drawn on top of
## it is what's under test).
static func _non_background_fraction(image: Image) -> float:
var w: int = image.get_width()
var h: int = image.get_height()
if w <= 0 or h <= 0:
return 0.0
var total: int = w * h
var differing: int = 0
var bg_rgb: Color = Color(COLOR_BG.r, COLOR_BG.g, COLOR_BG.b, 1.0)
for y in range(h):
for x in range(w):
var px: Color = image.get_pixel(x, y)
var px_rgb: Color = Color(px.r, px.g, px.b, 1.0)
if not px_rgb.is_equal_approx(bg_rgb):
differing += 1
return float(differing) / float(total)
## (a) Single-window path: a District-rung response must render as visibly
## non-background pixels through AtlasWindowOverlay._draw()'s own
## Rect2(0,0,extent,extent) draw call — the "does the OVERLAY actually paint
## something for a real window" half of the gap. Positions it at the
## SubViewport's center via the surrounding Node2D, mirroring _canvas's role
## in the real viewer.
##
## Honest scope note: live round 4's SECOND bug (leaving `_view_offset`
## stale across a rung crossing in `_maybe_reselect_rung()`) lived entirely
## in AtlasWindowViewer's transform bookkeeping, ONE LAYER ABOVE this
## overlay-only test's boundary — it never touched `_draw()` itself, so a
## pure-overlay smoke test structurally cannot reproduce it (there is no
## "stale vs. fresh offset" state to compare inside the overlay alone). That
## regression's coverage is `_maybe_reselect_rung()`'s own unit tests in
## test_atlas_zoom_ladder.gd. This test's job is narrower and still real:
## proving the overlay's draw call itself produces visible output for
## legitimate window data, closing the "the composite Rect2 call is
## silently a no-op" class of bug regardless of which layer caused it.
func test_single_window_draw_produces_visible_pixels() -> void:
# Guarded early-return instead of the _do_skip fuzzer-arg convention:
# gdUnit4 leaks one internal <Node> per fuzzer-skipped test, tripping the
# orphan detector (exit 101) and bouncing the push gate even at 0 failures
# (PR gate run 2026-07-22: "2 skipped | 2 orphans | Exit code: 101").
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
var stub := _SingleWindowViewerStub.new()
stub.window = _mock_window(Vector2i.ZERO, 32)
overlay.viewer = stub
var image: Image = await _render_to_image(overlay)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"single-window composite must render VISIBLE non-background pixels — got"
+ " only %.2f%% of the frame differing from COLOR_BG. This is exactly the"
+ " shape of live round 4's second bug: _view_offset left stale across a"
+ " rung crossing pushed the composite off-canvas, so nothing but"
+ " background/chrome ever appeared, despite the underlying window data"
+ " and draw calls being individually 'correct' in isolation."
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
## (b) Tile-mosaic path: a tile at a district center AWAY from the body's
## own origin must still render as visibly non-background pixels once
## correctly placed via `district_to_canvas_local()`'s shared held_center/
## held_n convention — this is the path round 4's FIRST and THIRD bugs
## (tile-local-origin math ignoring that convention, and per-tile
## ImageTexture objects with no persistent reference being garbage-
## collected/GPU-desynced before their draw command flushed) would both
## have failed. Uses production-realistic scale (live round 4's own Lendel
## repro: raw circumference ~19,139 districts) — see the tile-center
## comment below for why scale matters here specifically.
func test_tile_mosaic_draw_produces_visible_pixels() -> void:
# Guarded early-return, not _do_skip — see the sibling test's comment.
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
var stub := _TileModeViewerStub.new()
var tile_n: int = AtlasWindowGeometry.TILE_N
var cell_px: float = stub.get_cell_pixel_size()
stub.held_center = Vector2i.ZERO
stub.held_n = 19139
# A SINGLE tile chosen so the CORRECT canvas-local formula
# (`district_to_canvas_local()`, anchored at `held_center - held_n/2`)
# lands it centered in the viewport, while the round-4 BUGGY formula
# (anchored at absolute district (0,0) directly) lands it almost
# `held_n/2 * cell_px` local units away — tens of thousands of units at
# this scale, i.e. genuinely fully off a 512x512 viewport, not just
# "shifted but still overlapping" (confirmed by hand-computation: a
# smaller/toy-scale version of this test stayed green with the bug
# reintroduced, because the shift stayed within the viewport bounds
# either way — this scale/center combination is chosen specifically to
# avoid that false-negative).
var half_tile: float = float(tile_n) * 0.5
var half_body: float = float(stub.held_n) * 0.5
var lone_tile_center := Vector2i(roundi(half_tile - half_body), roundi(half_tile - half_body))
stub.tiles = [
{"center": lone_tile_center, "window": _mock_window(lone_tile_center, 64)},
]
overlay.viewer = stub
# A zoom small enough that the buggy-vs-correct shift (~half_body * cell_px
# local units) is comfortably larger than the viewport — see the center
# choice's own doc above for why this specific magnitude matters.
var zoom: float = float(VIEWPORT_SIZE.x) * 1.5 / (half_body * cell_px)
var image: Image = await _render_to_image(overlay, zoom)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"tile mosaic must render VISIBLE non-background pixels across its tiles —"
+ " got only %.2f%% of the frame differing from COLOR_BG. This is exactly"
+ " the shape of live round 4's bugs: (1) tile local-origin computed"
+ " relative to absolute district (0,0) instead of the shared"
+ " held_center/held_n canvas-local convention pushed the whole mosaic"
+ " off-canvas, and (2) even once correctly positioned, an unstored"
+ " per-draw-call ImageTexture rendered as a blank/white gap despite"
+ " provably-correct CPU-side pixel data — both invisible to any test that"
+ " only inspects Dictionary/cache state, never an actual composited pixel."
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
+284 -5
View File
@@ -14,19 +14,36 @@ extends GdUnitTestSuite
# duplicated-load).
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
## Dudley's WINDOW_GRANULARITY_REGION_KEY (server/src/atlas/layer_proxy.rs) —
## `u32::MAX`, the RESERVED KEY-SPACE TAG a real server ALWAYS puts in the
## legacy `granularity` slot for every Region response (never a real
## multiplier — District=1/Quarter=4 are the only legal wire multipliers).
## Do NOT "fix" this to 1 — using a convenient value here is EXACTLY the gap
## the live round caught (a mock that diverges from the wire in the one
## field that matters silently un-repros the bug). See
## AtlasWindowRequest's `_echoed_granularity_matches()` doc for the full
## rationale.
const SERVER_LEGACY_GRANULARITY_REGION_SENTINEL: int = 4294967295
## Build a hand-authored DistrictWindowLayer dict, granularity-aware
## (T-1150) — mirrors test_atlas_window_viewer.gd's own _mock_window(), with
## granularity/min_wl_m added as optional params so callers can build both
## rungs' echo shapes with one helper.
## (T-1150, extended T-1152/T-1153 for granularity_v2) — mirrors
## test_atlas_window_viewer.gd's own _mock_window(), with
## granularity/min_wl_m/granularity_v2 added as optional params so callers
## can build any rung's echo shape with one helper.
static func _mock_window(
center: Vector2i, n: int = 2, granularity: int = 1, min_wl_m: int = 0
center: Vector2i,
n: int = 2,
granularity: int = 1,
min_wl_m: int = 0,
granularity_v2: String = "District"
) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"granularity": granularity,
"min_wl_m": min_wl_m,
"granularity_v2": granularity_v2,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
@@ -57,12 +74,24 @@ func _make_request() -> Variant:
## center/n must be dropped as stale, not accepted — a different rung's
## derive answering a request for a different rung is exactly as stale as a
## mismatched center (T-1150 extends §2's guard to this axis).
##
## **Live-round correction:** the mock MUST carry a mismatched
## `granularity_v2` too (explicit `"Quarter"`, not `_mock_window()`'s
## `"District"` default) — a real Quarter response ALWAYS carries
## `granularity_v2: "Quarter"` on the wire, never the District default this
## test's fixture used to leave implicit. Under the v2-authoritative-when-
## present precedence rule (see on_response()'s own doc), a v2-MATCHING
## response is accepted regardless of what the legacy int says — leaving
## granularity_v2 at its District default here would have made this test
## pass for the wrong reason (an accidentally-matching v2 field masking a
## genuinely mismatched legacy int), exactly the class of gap the live round
## caught in the oversized-orbital round-trip test.
func test_on_response_with_mismatched_granularity_is_dropped_as_stale() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).is_true()
var quarter_window: Dictionary = _mock_window(Vector2i(2, 2), 2, 4, 0)
var quarter_window: Dictionary = _mock_window(Vector2i(2, 2), 2, 4, 0, "Quarter")
req.on_response(_mock_response("GJ380c", quarter_window))
assert_bool(req.is_pending()).override_failure_message(
@@ -70,6 +99,94 @@ func test_on_response_with_mismatched_granularity_is_dropped_as_stale() -> void:
).is_true()
# =============================================================================
# (a2) granularity_v2 mismatch on the echo -> dropped as stale (T-1152/T-1153,
# the axis the legacy int alone cannot express — Region has no legacy value)
# =============================================================================
## request_now() can now ask for Region explicitly (T-1153's rung-reselect
## caller) — a response echoing "District" for the SAME center/n must be
## dropped as stale, the granularity_v2 twin of test (a) above, and the
## ONLY guard that can catch this specific mismatch (the legacy int is
## DISTRICT_GRANULARITY=1 on BOTH sides here, since Region has no legacy
## representation — see WindowGranularity::legacy_u32()'s doc).
func test_on_response_with_mismatched_granularity_v2_is_dropped_as_stale() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_bool(req.is_pending()).is_true()
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 6400, 1, 0, "District")
req.on_response(_mock_response("GJ380c", district_window))
assert_bool(req.is_pending()).override_failure_message(
"a granularity_v2-mismatched response (District answering a Region request)"
+ " must be dropped as stale, leaving the request still pending"
).is_true()
## The matching case: request_now() asking for Region, answered by a Region
## echo at the SAME (center, n) — must be ACCEPTED and cached under the
## Region key, retrievable on a follow-up request without a new network round
## trip.
##
## **Live-round correction:** the mock's legacy `granularity` field is now
## Dudley's ACTUAL wire sentinel (`WINDOW_GRANULARITY_REGION_KEY` =
## `u32::MAX` = 4294967295), not a convenient `1` — the original version of
## this test used `1`, which coincidentally matched the request's own
## pinned `_granularity` and therefore never exercised the real mismatch a
## live server actually produces. See _echoed_granularity_matches()'s own
## doc (atlas_window_request.gd) for why this is load-bearing: without the
## v2-authoritative-when-present fix, THIS test would have failed with the
## real sentinel — it only passed before because the mock was wrong.
func test_on_response_matching_granularity_v2_region_is_accepted_and_cached() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_bool(req.is_pending()).is_true()
var region_window: Dictionary = _mock_window(
Vector2i(0, 0), 6400, SERVER_LEGACY_GRANULARITY_REGION_SENTINEL, 0, "Region"
)
req.on_response(_mock_response("GJ380c", region_window))
assert_bool(req.is_pending()).override_failure_message(
"a response carrying the REAL legacy sentinel (u32::MAX) in the old"
+ " granularity slot must still be accepted — v2 is authoritative"
+ " whenever present, the legacy field must not be compared at all"
).is_false()
var received: Array = []
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_int(received.size()).override_failure_message(
"a second Region request at the same (center, n) must hit the cache"
).is_equal(1)
assert_bool(req.is_pending()).is_false()
## **The direct precedence-rule proof (live-round finding #2, the sharpest
## case):** a response whose `granularity_v2` MATCHES the request but whose
## LEGACY `granularity` field could never possibly match (the Region
## sentinel) must still be ACCEPTED — proving the legacy comparison is
## SKIPPED entirely when v2 is present, not merely "also checked and
## happens to pass." This is the literal shape of the live bug: real server
## responses ALWAYS carry the Region sentinel in the legacy slot, so any
## code path that still consults the legacy field when v2 is already
## authoritative would drop every single one of these, forever.
func test_on_response_v2_match_is_accepted_regardless_of_legacy_field_value() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
var region_window: Dictionary = _mock_window(
Vector2i(0, 0), 6400, SERVER_LEGACY_GRANULARITY_REGION_SENTINEL, 0, "Region"
)
req.on_response(_mock_response("GJ380c", region_window))
assert_bool(req.is_pending()).override_failure_message(
"v2 match must be sufficient on its own — the legacy sentinel value must"
+ " never be consulted once granularity_v2 is present on the response"
).is_false()
# =============================================================================
# (b) old-server-shape response (no granularity/min_wl_m keys) -> defaults
# =============================================================================
@@ -116,6 +233,48 @@ func test_on_response_missing_granularity_and_min_wl_defaults_and_is_accepted()
assert_bool(req.is_pending()).is_false()
## **Live-round sibling test (instruction #2's "old-server path stays
## covered"):** a response that carries the LEGACY `granularity` key WITH AN
## EXPLICIT VALUE (1, i.e. genuinely present, not merely defaulted via
## absence — the case test_on_response_missing_granularity_and_min_wl_defaults_and_is_accepted
## above doesn't exercise, since it omits the key entirely) but has NO
## `granularity_v2` key at all — the true "hypothetically old, pre-T-1152
## server" shape — must still be accepted for a plain District request via
## the legacy-comparison FALLBACK branch in `_echoed_granularity_matches()`.
## This is the other half of the v2-authoritative-when-present precedence
## rule: v2 present -> v2 alone decides; v2 ABSENT -> legacy alone decides
## (never both, never neither).
func test_on_response_legacy_only_no_v2_key_still_accepted_for_district() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(4, 4), 2) # defaults to District granularity
assert_bool(req.is_pending()).is_true()
# Legacy-only shape: "granularity" IS present (district=1), "granularity_v2"
# key is absent entirely — not present-with-a-District-value, ABSENT.
var legacy_only_window := {
"center": [4, 4],
"n": 2,
"granularity": AtlasWindowRequest.DEFAULT_GRANULARITY,
"min_wl_m": 0,
"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]),
}
assert_bool(legacy_only_window.has("granularity_v2")).override_failure_message(
"sanity: this fixture must NOT carry granularity_v2 at all — that's the point"
).is_false()
req.on_response(_mock_response("GJ380c", legacy_only_window))
assert_bool(req.is_pending()).override_failure_message(
"a legacy-only response (granularity=1 present, granularity_v2 absent) must"
+ " still be accepted for a District request via the legacy-fallback branch"
).is_false()
# =============================================================================
# (c) n-clamp mirror (Tyre C1) — quarter n=32 stores clamped n=16
# =============================================================================
@@ -175,3 +334,123 @@ func test_oversized_n_request_stores_clamped_n_and_accepts_matching_echo() -> vo
+ "already clamped to 64 before the request fired"
)
).is_false()
# =============================================================================
# (d) Region clamp mirror (T-1152/T-1153) — mirrors
# server/src/atlas/layer_proxy.rs's clamp_window_n_v2 EXACTLY, including the
# Region branch's bounded halving loop.
#
# PR #192 review (Dudley, server-side analysis): the halving loop is
# PROVABLY UNREACHABLE at current constants — the per-axis clamp to
# SERVER_DISTRICT_WINDOW_MAX_N_REGION (6,400) forecloses it. Brute-forced,
# the max cell_grid_side over ALL reachable (post-per-axis-clamp) n is
# exactly 64 — the wire-cap boundary itself, never over it — so the loop's
# `>` guard is never true for any input. Ruling: the loop STAYS as
# defensive code (a future constant change could make it reachable again),
# but the test suite must not claim it "fires" when it provably doesn't.
# See server/src/atlas/layer_proxy.rs's
# clamp_window_n_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs
# for the server-side property-sweep pin this client-side suite mirrors.
# =============================================================================
## District/Quarter through the v2 mirror must be BYTE-IDENTICAL to the
## legacy mirror — the server's own
## `clamp_window_n_v2_delegates_to_legacy_for_district_and_quarter`
## guarantee, restated client-side.
func test_clamp_window_n_mirror_v2_matches_legacy_for_district_and_quarter() -> void:
assert_int(
AtlasWindowRequest._clamp_window_n_mirror_v2(32, AtlasWindowRequest.GRANULARITY_V2_QUARTER)
).is_equal(AtlasWindowRequest._clamp_window_n_mirror(32, 4))
assert_int(
AtlasWindowRequest._clamp_window_n_mirror_v2(640, AtlasWindowRequest.GRANULARITY_V2_DISTRICT)
).is_equal(AtlasWindowRequest._clamp_window_n_mirror(640, 1))
## The clean Region boundary case: n=6,400 (DISTRICT_WINDOW_MAX_N_REGION,
## the per-axis cap exactly) derives cell_grid_side(6400) = round(6400/100) =
## 64, and 64² = 4,096 = WIRE_CAP_CELLS EXACTLY — the halving loop's `>`
## condition is false at the boundary, so this must clamp to EXACTLY 6,400,
## not halve further. This is the server's own
## `clamp_window_n_v2_region_exact_boundary_n6400_uncontested` guarantee,
## restated client-side (WIRE_CAP_CELLS_SQRT * DISTRICTS_PER_REGION is
## DERIVED to land here exactly, per that constant's own doc).
func test_clamp_window_n_mirror_v2_region_boundary_is_exact() -> void:
var n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(n).is_equal(AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
## Region's per-axis cap: a raw `n` far over DISTRICT_WINDOW_MAX_N_REGION
## (mirroring the server's own `region_request_oversized_n_clamps_and_echoes_clamped_n`
## test's `DISTRICT_WINDOW_MAX_N_REGION * 10` shape) must clamp DOWN — never
## trust the wire — and the result must satisfy BOTH invariants the server's
## own test asserts: `n <= DISTRICT_WINDOW_MAX_N_REGION` AND
## `cell_grid_side(n)^2 <= WIRE_CAP_CELLS`.
func test_clamp_window_n_mirror_v2_region_oversized_n_clamps_within_both_bounds() -> void:
var oversized: int = AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION * 10
var n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
oversized, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(n).override_failure_message(
"echoed n must be clamped to DISTRICT_WINDOW_MAX_N_REGION, not the raw oversized value"
).is_less_equal(AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
var side: int = AtlasWindowRequest._cell_grid_side_region_mirror(n)
assert_int(side * side).override_failure_message(
"clamped cell count must never exceed WIRE_CAP_CELLS at Region granularity either"
).is_less_equal(AtlasWindowRequest.SERVER_WIRE_CAP_CELLS)
## PR #192 review (Dudley's unreachability finding, applied client-side): the
## halving loop's `>` guard is PROVABLY never true at current constants — the
## per-axis clamp to SERVER_DISTRICT_WINDOW_MAX_N_REGION (6,400) happens
## FIRST and unconditionally, and cell_grid_side(6400) = 64 lands EXACTLY on
## the wire-cap boundary (64² = WIRE_CAP_CELLS), never over it. A prior
## version of this test claimed n=6,450 "exercises" the loop firing — it does
## not: 6,450 clamps to 6,400 before the loop ever runs, so the test was
## passing on the per-axis clamp alone, not on anything the loop itself did
## (the same mock-diverges-from-reality class of bug hunted in review round
## 2). Reframed as a property sweep, mirroring the server's own
## `clamp_window_n_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs`
## (Dudley): for every raw n across the legal range (including values far
## past the per-axis cap), (i) the per-axis-clamped n never gets modified any
## further by the loop — pre-loop n and post-clamp n are byte-identical —
## and (ii) the wire-cap invariant holds regardless. The loop itself stays as
## defensive code (a future constant change could make it reachable again);
## this test documents that it is a no-op today rather than asserting a
## behavior that never actually happens.
func test_clamp_window_n_mirror_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs() -> void:
var sample_raw_ns: Array = [
1, 100, 6399, 6400, 6401, 6450, 6500,
AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION * 10,
]
for raw_n: int in sample_raw_ns:
var pre_loop_n: int = clampi(raw_n, 1, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
raw_n, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(clamped_n).override_failure_message(
(
"the per-axis clamp alone must already satisfy the wire cap for"
+ " raw_n=%d — the halving loop is provably unreachable at current"
+ " constants (max cell_grid_side over all reachable n is exactly"
+ " 64, the wire-cap boundary itself), so it must never further"
+ " modify what the per-axis clamp already produced"
) % raw_n
).is_equal(pre_loop_n)
var side: int = AtlasWindowRequest._cell_grid_side_region_mirror(clamped_n)
assert_int(side * side).override_failure_message(
"the wire-cap invariant must hold for raw_n=%d regardless" % raw_n
).is_less_equal(AtlasWindowRequest.SERVER_WIRE_CAP_CELLS)
## n smaller than one region (n < 100) must clamp its cell-grid side to a
## minimum of 1 — cell_grid_side_for_window()'s own `.max(1)` — never a
## degenerate 0x0 grid, matching WindowGranularity::cell_grid_side's own
## documented minimum.
func test_cell_grid_side_region_mirror_minimum_is_one() -> void:
assert_int(AtlasWindowRequest._cell_grid_side_region_mirror(1)).is_equal(1)
assert_int(AtlasWindowRequest._cell_grid_side_region_mirror(50)).is_equal(1)
+204
View File
@@ -0,0 +1,204 @@
## T-1153, live round 3 (Jeroen's ruling, design doc §4): tests for
## AtlasWindowTileSet — the orbital rest-state multi-window mosaic
## orchestration. Same hand-built-response-dict conventions as
## test_atlas_window_request.gd/test_atlas_zoom_ladder.gd; this file is
## about the ORCHESTRATION (N tiles, progressive per-tile arrival,
## teardown), not the tile-grid MATH (already covered directly against
## AtlasWindowGeometry.compute_tile_grid() in test_atlas_window_geometry.gd).
class_name TestAtlasWindowTileSet
extends GdUnitTestSuite
const AtlasWindowTileSet := preload("res://ui/implant/apps/atlas/atlas_window_tile_set.gd")
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
static func _mock_window(center: Vector2i, n: int) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"granularity_v2": "Region",
"morphology": PackedByteArray([1, 2, 3, 4]),
"elev_q": PackedByteArray([10, 20, 30, 40]),
"temp_dc": [0, 0, 0, 0],
"moisture_q": PackedByteArray([0, 0, 0, 0]),
"vegetation": PackedByteArray([0, 0, 0, 0]),
"glaciation": PackedByteArray([0, 0, 0, 0]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
func _make_tile_set() -> Variant:
var owner_stub := RefCounted.new()
var ts = auto_free(AtlasWindowTileSet.new(owner_stub))
add_child(ts)
return ts
# =============================================================================
# enter() — tile grid computation + one request per tile
# =============================================================================
## enter() on a real, tiling-sized body must produce the SAME tile count
## compute_tile_grid() would — 6 for GJ380c/Lendel, the coordinator's own
## live-round number.
func test_enter_produces_the_expected_tile_count_for_lendel() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
assert_int(ts.get_tile_count()).is_equal(6)
assert_bool(ts.is_multi_tile()).is_true()
## A tiny (non-tiling) body produces exactly ONE tile — the degenerate case
## compute_tile_grid() itself already covers; this confirms the ORCHESTRATION
## (not just the grid math) handles it without crashing or requesting zero
## tiles.
func test_enter_tiny_body_produces_one_tile() -> void:
var ts = _make_tile_set()
ts.enter("TinyBody", 50.0)
assert_int(ts.get_tile_count()).is_equal(1)
assert_bool(ts.is_multi_tile()).is_false()
## Every tile must start with a null window (nothing has arrived yet) and
## the tile set must not report "fully arrived" before any response lands.
func test_enter_all_tiles_start_unarrived() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
for tile: Dictionary in ts.get_tiles():
assert_that(tile["window"]).is_null()
assert_bool(ts.is_fully_arrived()).is_false()
## An empty tile set (never entered) must not report "fully arrived" either
## — an empty AND-over-nothing must not vacuously read true.
func test_empty_tile_set_is_not_fully_arrived() -> void:
var ts = _make_tile_set()
assert_bool(ts.is_fully_arrived()).is_false()
# =============================================================================
# Progressive per-tile arrival (design doc §4: "with visible refinement as
# tiles complete") — each tile's response is independent of every other's.
# =============================================================================
## Delivering ONE tile's response must populate ONLY that tile's window,
## leaving every other tile still null — the direct "progressive, not
## block-on-all" regression.
func test_one_tile_arriving_does_not_affect_the_others() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var tiles: Array = ts.get_tiles()
var first_center: Vector2i = tiles[0]["center"]
var window: Dictionary = _mock_window(first_center, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
var updated_tiles: Array = ts.get_tiles()
assert_that(updated_tiles[0]["window"]).override_failure_message(
"the tile whose response arrived must have its window populated"
).is_equal(window)
for i in range(1, updated_tiles.size()):
assert_that(updated_tiles[i]["window"]).override_failure_message(
"tile %d must still be unarrived — only tile 0's response was delivered" % i
).is_null()
## tile_ready must fire with the INDEX of the tile that actually arrived —
## the viewer/overlay needs this to know WHICH tile to redraw, not just
## "something changed".
func test_tile_ready_signal_fires_with_the_correct_index() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var received_indices: Array = []
ts.tile_ready.connect(func(index: int) -> void: received_indices.append(index))
var tiles: Array = ts.get_tiles()
var second_center: Vector2i = tiles[1]["center"]
var window: Dictionary = _mock_window(second_center, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_int(received_indices.size()).is_equal(1)
assert_int(received_indices[0]).is_equal(1)
## Delivering EVERY tile's response must flip is_fully_arrived() to true —
## the mosaic-complete signal the viewer/legend chrome can use.
func test_all_tiles_arriving_flips_fully_arrived() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var tiles: Array = ts.get_tiles()
for tile: Dictionary in tiles:
var window: Dictionary = _mock_window(
tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(ts.is_fully_arrived()).override_failure_message(
"once every tile's response has arrived, the tile set must report fully arrived"
).is_true()
## A response for a body the tile set is NOT currently showing (a stale
## response from a body the player has since navigated away from) must not
## be adopted by any tile — the SAME body_id staleness guard every other
## AtlasWindowRequest-based path already relies on (this is inherited for
## free since each tile IS an AtlasWindowRequest, but pinned here as an
## orchestration-level regression too).
func test_response_for_a_different_body_is_ignored() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var tiles: Array = ts.get_tiles()
var window: Dictionary = _mock_window(
tiles[0]["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ_wrong_body", window))
assert_that(ts.get_tiles()[0]["window"]).is_null()
# =============================================================================
# Teardown — re-entering (a fresh body, or the same body again) must not
# leave stale tile request nodes wired up.
# =============================================================================
## Calling enter() a SECOND time (e.g. re-entering the orbital frame, or
## switching to a different body) must replace the tile set entirely — the
## OLD tiles' indices/centers must not linger.
func test_second_enter_replaces_the_tile_set() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var first_count: int = ts.get_tile_count()
assert_int(first_count).is_equal(6)
ts.enter("TinyBody", 50.0)
assert_int(ts.get_tile_count()).override_failure_message(
"a second enter() must fully replace the tile set, not append to it"
).is_equal(1)
## A response matching an OLD tile set's (body, center) — arriving AFTER a
## second enter() has already torn it down — must not be adopted (or crash):
## the old tile's AtlasWindowRequest node is queue_free()'d, and _tiles no
## longer references it, so a stale signal (if it could somehow still fire)
## has no live entry left to update.
func test_stale_response_after_second_enter_does_not_crash_or_leak() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var old_tiles: Array = ts.get_tiles()
var old_center: Vector2i = old_tiles[0]["center"]
ts.enter("GJ380c", 50.0) # same body_id, different (tiny) radius -> different tile grid
# A response shaped like it's answering the OLD tile set's first tile —
# must not crash, and must not corrupt the NEW tile set's single tile.
var stale_window: Dictionary = _mock_window(
old_center, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", stale_window))
assert_int(ts.get_tile_count()).is_equal(1)
+22 -15
View File
@@ -144,11 +144,15 @@ func test_set_view_and_getters_round_trip() -> void:
assert_that(v.get_view_offset()).is_equal(Vector2(30.0, -10.0))
## T-1153: MIN_ZOOM widened to 0.0005 (from the pre-ladder 0.5) so a
## gas-giant-scale body's enter_orbital() fit zoom is never itself clamped —
## see MIN_ZOOM's own doc. Values here are chosen well outside the new wide
## range on both ends, not the old range's boundary values.
func test_set_view_clamps_to_min_max_zoom() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(0.01, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.001)
v.set_view(0.0000001, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.0001)
v.set_view(1000.0, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MAX_ZOOM, 0.001)
@@ -352,25 +356,28 @@ func test_wasd_diagonal_pan_is_not_faster_than_single_axis() -> void:
).is_equal_approx(axis_distance, 0.01)
## The real scene-tree path: DistrictScreen -> AtlasWindowViewer. Unlike
## drag (which needed _gui_input event delivery, hence the old
## "does an ancestor eat the event" test), WASD pan lives in _process() —
## Godot delivers _process() to every node in the tree regardless of Control
## mouse_filter/ancestry (there is no "topmost control" routing for
## per-frame process callbacks the way there is for _gui_input), so there is
## no equivalent "does DistrictScreen eat it" question for _process() itself.
## What DOES still matter through the real chain is _is_over_ui()'s edge-
## scroll suppression and visibility gating — pinned directly below instead.
func test_wasd_pan_reaches_viewer_through_district_screen_chain() -> void:
var screen: DistrictScreen = auto_free(DistrictScreen.new())
## The real scene-tree path: RegionalScreen -> AtlasWindowViewer (T-1153 —
## RegionalScreen is now the WHOLE ladder's nav entry, superseding the
## retired DistrictScreen nav hop; see atlas_app.gd's own doc for why the
## separate "district" screen retired). Unlike drag (which needed
## _gui_input event delivery, hence the old "does an ancestor eat the
## event" test), WASD pan lives in _process() — Godot delivers _process()
## to every node in the tree regardless of Control mouse_filter/ancestry
## (there is no "topmost control" routing for per-frame process callbacks
## the way there is for _gui_input), so there is no equivalent "does the
## screen eat it" question for _process() itself. What DOES still matter
## through the real chain is _is_over_ui()'s edge-scroll suppression and
## visibility gating — pinned directly below instead.
func test_wasd_pan_reaches_viewer_through_regional_screen_chain() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
screen.enter({"body": {"body_id": "GJ380c"}, "district_center": Vector2i(0, 0)})
screen.enter({"body": {"body_id": "GJ380c"}, "system": {}})
var offset_before: Vector2 = screen._viewer.get_view_offset()
screen._viewer._apply_pan_delta(Vector2(1.0, 0.0), 0.1)
assert_that(screen._viewer.get_view_offset()).override_failure_message(
"a pan tick driven through DistrictScreen's child viewer must still move"
"a pan tick driven through RegionalScreen's child viewer must still move"
+ " _view_offset — no ancestor in the real screen chain blocks it"
).is_not_equal(offset_before)
+920
View File
@@ -0,0 +1,920 @@
## T-1153 (D-226 T-1143-rulings amendment): tests for the continuous
## cursor-anchored zoom ladder — enter_orbital() (the canonical planetary
## frame), progressive refinement (held composite survives a rung-crossing
## request), the full-zoom-out reset (Jeroen's HARD condition), rung
## reselection on zoom, and E/W wrap + pole-wall clamps at Region
## granularity. Split out of test_atlas_window_viewer.gd (which owns the
## pre-T-1153 window-viewer behavior — entry, cache reuse, WASD/edge-scroll,
## fit-and-center) purely for file-length reasons (gdlint max-file-lines);
## same instantiation/mock-response conventions as that file, not a
## different testing philosophy.
class_name TestAtlasZoomLadder
extends GdUnitTestSuite
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## Dudley's WINDOW_GRANULARITY_REGION_KEY (server/src/atlas/layer_proxy.rs) —
## `u32::MAX`, a RESERVED KEY-SPACE TAG the real server ALWAYS puts in the
## legacy `granularity` slot for every Region response (never a real
## multiplier — District=1/Quarter=4 are the only legal wire multipliers).
## Do NOT "fix" this to 1 — that would silently un-repro the live-round bug
## this constant exists to guard against (a real server's actual wire byte,
## not a convenient test value). See _echoed_granularity_matches()'s own doc
## (atlas_window_request.gd) for why this value can NEVER equal a client's
## stored `_granularity` (which stays pinned at DISTRICT_GRANULARITY=1 for
## every rung a T-1152-aware client requests) — that mismatch is exactly
## what silently dropped every Region response before the v2-authoritative
## fix.
const SERVER_LEGACY_GRANULARITY_REGION_SENTINEL: int = 4294967295
## Build a hand-authored DistrictWindowLayer dict (n=2 by default) — mirrors
## test_atlas_window_viewer.gd's own _mock_window().
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
# =============================================================================
# T-1153: enter_orbital() — the canonical planetary frame, the ladder's TOP
# REST STATE (Jeroen's HARD condition, D-226 T-1143-rulings amendment).
# =============================================================================
## enter_orbital() must center on district (0,0) — "district (0,0) sits at
## lon 0 / the equator" (AtlasDescendGeometry's own doc).
func test_enter_orbital_centers_on_the_canonical_origin() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_that(v._held_center).is_equal(Vector2i.ZERO)
## enter_orbital() must request at Region granularity — the orbital view IS
## the Region rung at high n, not a separate screen/mode (the ticket's own
## framing).
func test_enter_orbital_requests_region_granularity() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
## **Superseded by live round 3's tiling fix — retargeted, not deleted.**
## GJ380c/Lendel (radius 6238.4 km) was the ORIGINAL single-window C1 repro
## (raw cols ~19,139 vs. the 6,400 clamp ceiling) — but that SAME threshold
## (`DISTRICT_WINDOW_MAX_N_REGION * DISTRICT_M` = the coverage ceiling
## `compute_tile_grid()` tiles past) means any body needing the n-clamp ALSO
## needs tiling: there is no real body where enter_orbital() takes the
## single-window path with a raw `n` big enough to require clamping.
## GJ380c now correctly enters TILE mode (test_enter_orbital_n_is_the_clamped_value_not_raw_circumference's
## old assertion on a single clamped `_held_n` no longer applies — see
## test_enter_orbital_tile_mode_held_n_is_the_whole_body_extent below for
## what `_held_n` means in tile mode instead). The single-window clamp-mirror
## fix itself remains covered: `_enter_at_rung()`'s own doc/the clamp
## mirror's unit tests (test_atlas_window_request.gd) pin the formula
## directly, and test_zoom_crossing_fires_request_and_accepts_wire_accurate_refinement
## exercises the SAME clamp-mirror lesson at the reselect (not entry)
## boundary, which single-window mode still reaches on the way DOWN from a
## tile-mode zoom-in.
func test_enter_orbital_tile_mode_held_n_is_the_whole_body_extent() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel)
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var raw_cols: int = int(extent["cols"])
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).override_failure_message(
"GJ380c/Lendel needs tiling — enter_orbital() must have entered tile mode"
).is_true()
# In TILE mode, _held_n is the WHOLE body's extent (unclamped) — each
# TILE clamps its own request independently inside AtlasWindowTileSet
# (see that file's own tests), so _held_n here is NOT expected to equal
# any single clamped value the way single-window mode's is.
assert_int(v._held_n).is_equal(raw_cols)
## **The live-round-3 regression, end to end for TILE mode:** enter_orbital()
## on GJ380c/Lendel followed by delivering ONE tile's wire-accurate response
## (clamped n=6,400, "Region" granularity_v2, the legacy sentinel in the old
## granularity slot — exactly what a real server sends) must be ACCEPTED
## into that tile's own slot — not silently dropped. This exercises BOTH
## live-round fixes (the v2-authoritative precedence AND per-tile clamping)
## through the tile-set path specifically, complementing
## test_atlas_window_tile_set.gd's own more granular orchestration tests.
func test_enter_orbital_tile_mode_accepts_a_wire_accurate_tile_response() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
var tile_set = v.get_tile_set()
var tiles: Array = tile_set.get_tiles()
assert_int(tiles.size()).is_greater(1)
var first_tile_center: Vector2i = tiles[0]["center"]
var tile_window: Dictionary = {
"center": [first_tile_center.x, first_tile_center.y],
"n": AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION,
"granularity": SERVER_LEGACY_GRANULARITY_REGION_SENTINEL,
"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]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", tile_window))
assert_that(tile_set.get_tiles()[0]["window"]).override_failure_message(
"a wire-accurate response (clamped n, Region granularity_v2, the legacy"
+ " sentinel) for the first tile must be ACCEPTED into that tile's slot"
).is_equal(tile_window)
## A no-radius body (tiny test body) has no circumference concept —
## enter_orbital() falls back to the District-rung default window rather
## than crashing or deriving a degenerate n.
func test_enter_orbital_no_radius_body_falls_back_to_district() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c"}, {})
assert_str(v._held_granularity_v2).is_equal("District")
assert_int(v._held_n).is_equal(AtlasWindowRequest.DISTRICT_WINDOW_DEFAULT_N)
# =============================================================================
# T-1153: progressive refinement — the held composite survives until the
# replacement arrives (§6 "no mode flip": never a blank frame, never a
# clear-then-redraw).
# =============================================================================
## The core acceptance test: once a window is held, a request for a
## DIFFERENT rung being in-flight must NOT clear `_window` — the old
## composite stays exactly what get_district_window() returns until the new
## rung's response actually arrives and is adopted.
func test_held_window_survives_while_a_different_rung_request_is_in_flight() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
assert_that(v.get_district_window()).is_equal(district_window)
# Simulate a rung-reselect firing a NEW (Region) request without the
# response having arrived yet — direct call, mirroring what
# _maybe_reselect_rung() does internally.
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
assert_that(v.get_district_window()).override_failure_message(
"the OLD composite must survive while a different-rung request is in"
+ " flight — no blank frame, no premature clear"
).is_equal(district_window)
## Once the new rung's response actually arrives (matching the CURRENTLY
## in-flight request's granularity_v2), it swaps in — the composite reference
## changes from the old rung's window to the new one.
func test_new_rung_window_swaps_in_once_it_arrives() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
var region_window: Dictionary = {
"center": [10, 20], "n": 2, "granularity_v2": "Region",
"morphology": PackedByteArray([1, 2, 3, 4]),
"elev_q": PackedByteArray([10, 20, 30, 40]),
"temp_dc": [0, 0, 0, 0],
"moisture_q": PackedByteArray([0, 0, 0, 0]),
"vegetation": PackedByteArray([0, 0, 0, 0]),
"glaciation": PackedByteArray([0, 0, 0, 0]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", region_window))
assert_that(v.get_district_window()).override_failure_message(
"once the new rung's matching response arrives, it must swap in"
).is_equal(region_window)
assert_str(v._held_granularity_v2).is_equal("Region")
## refresh() clear()s via queue_free() (deferred, not synchronous) — a legend
## that has refreshed more than once in the same frame (build-time refresh at
## _ready(), then an entry-time refresh) can have STALE not-yet-freed
## children still parented alongside the new ones. add_component() always
## APPENDS, so the current ImplantHeader is the LAST one in the list, never
## assumed to be [0].
static func _current_legend_header(legend_panel) -> ImplantHeader:
var children: Array = legend_panel.get_implant_children()
for i in range(children.size() - 1, -1, -1):
if children[i] is ImplantHeader:
return children[i]
return null
## PR #192 review (Araminta, BLOCKING): the legend subtitle used to hardcode
## District's own "2.048 km/cell" — a 100x lie whenever the viewer actually
## holds Region (204.8 km/cell). While in the orbital tile-mode rest state
## (Region granularity), the legend must read Region's real spacing, not the
## stale District literal.
func test_legend_subtitle_reflects_region_spacing_in_tile_mode() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — needs tiling, so enters at Region
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
assert_str(v._held_granularity_v2).is_equal("Region")
var header: ImplantHeader = _current_legend_header(v._legend_panel)
assert_str(header._subtitle_label.text).override_failure_message(
"legend subtitle must reflect Region's real 204.800 km/cell spacing while"
+ " the viewer holds Region granularity, not a hardcoded District figure"
).contains("204.800 km/cell")
## Same bug, the other direction: after crossing INTO a single-window District
## rung, the legend must re-render with District's own spacing — proving the
## legend actually refreshes on a rung change rather than being stuck at
## whatever it showed on the FIRST refresh() call (T-1153's _build_legend_panel()
## fires one at _ready() time, before any real rung is held).
func test_legend_subtitle_reflects_district_spacing_after_crossing_in() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
assert_str(v._held_granularity_v2).is_equal("District")
var header: ImplantHeader = _current_legend_header(v._legend_panel)
assert_str(header._subtitle_label.text).override_failure_message(
"legend subtitle must re-render at District's own 2.048 km/cell spacing"
+ " once the viewer holds a District-rung window — proving refresh() is"
+ " actually wired to the rung change, not just called once at build time"
).contains("2.048 km/cell")
## A response for a rung OTHER than what's currently requested (e.g. a
## District response arriving after the viewer has already moved on to a
## Region request — a rapid wheel-zoom race) must be discarded as stale, the
## held composite untouched.
func test_stale_rung_response_after_moving_on_is_discarded() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
# A LATE district-rung response for the same (center, n) arrives after the
# viewer has already moved on to requesting Region — must be dropped.
var late_district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
late_district_window["morphology"] = PackedByteArray([9, 9, 9, 9]) # distinguishable payload
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", late_district_window))
assert_that(v.get_district_window()).override_failure_message(
"a stale response for a rung the viewer has since moved on from must be discarded"
).is_equal(district_window)
# =============================================================================
# T-1153: full-zoom-out reset (Jeroen's HARD condition).
# =============================================================================
## Directly at the canonical frame already (center (0,0), Region granularity)
## must be a no-op — never re-fights a player zooming back IN from the top.
func test_reset_to_canonical_frame_is_noop_when_already_there() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._held_center = Vector2i.ZERO
v._held_granularity_v2 = "Region"
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"already at the canonical frame — the reset must not re-fire"
).is_false()
## A no-radius body must never trigger the reset (no circumference concept —
## matches enter_orbital()'s own guard).
func test_reset_to_canonical_frame_never_fires_for_no_radius_body() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(5, 5), 2)
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).is_false()
## Away from the canonical frame (a drifted District-rung pan/zoom state)
## with a fully-zoomed-out world extent must reset — the direct wiring test
## for Jeroen's HARD condition: enter() at a far-off center, then force the
## view zoom low enough that the displayed extent covers the whole body.
func test_reset_to_canonical_frame_fires_and_re_centers_when_fully_zoomed_out() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 50.0 # tiny synthetic body — small circumference, reachable by a modest zoom-out
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(500, 10), 32)
# Force a very low zoom — a huge displayed world extent, comfortably over
# this tiny body's whole circumference.
v._view_zoom = AtlasWindowViewer.MIN_ZOOM
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"a fully-zoomed-out view on a real-radius body must trigger the reset"
).is_true()
assert_that(v._held_center).override_failure_message(
"the reset must re-center on the canonical origin (0,0)"
).is_equal(Vector2i.ZERO)
assert_str(v._held_granularity_v2).override_failure_message(
"the reset must land on the Region rung — the ladder's top rest state"
).is_equal("Region")
## Live round 5's OWN repro, end to end: enter a TILING body's canonical
## frame, wheel-zoom IN far enough to cross out of tile mode (leaving
## `_held_granularity_v2` STALE at "Region" — a real, expected lag per
## `_maybe_reselect_rung()`'s own "does NOT touch _held_granularity_v2"
## doc, not a bug in that function), then wheel-zoom back OUT past the
## fully-zoomed-out threshold. The reset must fire and land EXACTLY on
## enter_orbital()'s own fit zoom for this body/viewport — not merely
## re-center while leaving `_view_zoom` wherever continued `_zoom_at()`
## scaling left it. Before the fix, the stale "Region" granularity
## satisfied the guard's OLD (center + granularity only) check forever,
## so the reset never fired again and `_view_zoom` kept shrinking via
## plain multiplication all the way to MIN_ZOOM.
func test_reset_after_crossing_out_and_back_snaps_to_the_canonical_fit_zoom() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — a tiling body, the live-repro shape
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: Lendel must enter tile mode — this repro needs a TILING body,"
+ " since that's where _held_granularity_v2 can lag is_tile_mode()"
).is_true()
# Zoom IN far enough to cross out of tile mode (matching
# test_zoom_crossing_recomputes_view_offset_so_the_new_window_is_on_screen's
# own gesture shape).
var cursor_pos := Vector2(800.0, 450.0)
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: this test needs to actually leave tile mode before zooming back out"
).is_false()
assert_str(v._held_granularity_v2).override_failure_message(
"sanity: _held_granularity_v2 must be STALE at Region here (no mock response"
+ " ever adopted a new value) — this is the exact lagging-field condition"
+ " the guard fix targets, not an artificial setup"
).is_equal("Region")
# Zoom back OUT past the fully-zoomed-out threshold — the reset must fire
# (possibly after a few more _zoom_at() ticks, matching a real wheel
# gesture rather than asserting it fires on the very first step back).
for _i in range(200):
v._zoom_at(cursor_pos, 1.0 / 1.05)
if v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"zooming back out past the threshold must re-fire the reset and land back"
+ " in tile mode — the stale-granularity guard bug left this permanently false"
).is_true()
assert_that(v._held_center).is_equal(Vector2i.ZERO)
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
var expected_fit: Dictionary = AtlasWindowGeometry.fit_window_view(
v.size, n, AtlasWindowViewer.CELL_PIXEL_SIZE, AtlasWindowViewer.MIN_ZOOM, AtlasWindowViewer.MAX_ZOOM
)
assert_float(v._view_zoom).override_failure_message(
(
"post-reset _view_zoom (%.6f) must equal enter_orbital()'s own fit zoom"
+ " (%.6f) for this body/viewport — Jeroen's condition is the ORIGINAL"
+ " frame (center AND offset AND fit zoom), not merely re-centered at"
+ " whatever zoom continued _zoom_at() scaling left behind"
)
% [v._view_zoom, expected_fit["zoom"]]
).is_equal_approx(float(expected_fit["zoom"]), 0.000001)
## Live round 5's OWN live-drive repro, exactly: a REAL wheel gesture does
## NOT stop the instant the reset first fires — the coordinator's own
## tmp_drive_ladder.gd keeps sending wheel-down ticks toward a fixed target
## zoom (0.004, chosen below the fit zoom) regardless of the reset. This
## test reproduces that shape directly: continue zooming out PAST the point
## where the reset first re-enters tile mode, all the way to a target zoom
## BELOW the fit value. Before the round-5 fix, `_view_zoom` drifted back
## down from the fit value on every subsequent `_zoom_at()` tick while the
## mode/center/granularity guard read "already canonical" and silently let
## it drift, landing on whatever the LOOP's target zoom happened to be
## instead of the fit value. **Live round 6 update:** the MECHANISM that
## now holds this assertion changed — `_zoom_at()`'s own zoom FLOOR (not a
## re-firing reset) is what keeps `_view_zoom` pinned at fit through
## continued zoom-out ticks; see `_maybe_reset_to_canonical_frame()`'s own
## doc for why re-firing on every tick caused a request storm. This test's
## own assertions are unchanged — only the doc below was updated to match.
func test_reset_resnaps_even_after_continued_zoom_out_past_the_first_reset() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
var expected_fit: Dictionary = AtlasWindowGeometry.fit_window_view(
v.size, n, AtlasWindowViewer.CELL_PIXEL_SIZE, AtlasWindowViewer.MIN_ZOOM, AtlasWindowViewer.MAX_ZOOM
)
var fit_zoom: float = float(expected_fit["zoom"])
# Zoom IN far enough to leave tile mode (same shape as the test above).
var cursor_pos := Vector2(1100.0, 300.0) # matches tmp_drive_ladder.gd's own aim point
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).is_false()
# Zoom back OUT toward a target BELOW the fit zoom — matching
# tmp_drive_ladder.gd's own `_zoom_until(wv, 0.004, false)` exactly
# (Lendel's own fit zoom is ~0.00627, comfortably above this target),
# WITHOUT stopping early the moment tile mode is first regained. A real
# wheel gesture has no way to know when the reset internally fires.
var target_zoom := 0.004
for _i in range(200):
if v._view_zoom <= target_zoom:
break
v._zoom_at(cursor_pos, 1.0 / 1.05)
assert_bool(v.is_tile_mode()).override_failure_message(
"after continued zoom-out past the reset point, the view must settle back"
+ " into tile mode — a genuinely re-snapped canonical frame can't have zoomed"
+ " OUT further than the fit value in the first place"
).is_true()
assert_float(v._view_zoom).override_failure_message(
(
"post-reset _view_zoom (%.6f) must equal the canonical fit zoom (%.6f) even"
+ " though the wheel gesture continued past the point where the reset first"
+ " fired (target was %.6f, BELOW the fit zoom) — _zoom_at()'s own zoom floor"
+ " must keep pinning it at fit through every subsequent tick, not just once"
)
% [v._view_zoom, fit_zoom, target_zoom]
).is_equal_approx(fit_zoom, 0.000001)
## Live round 6's ANTI-STORM test — the exact repro the coordinator's live
## drive caught: drive a REAL continued zoom-out gesture (via `_zoom_at()`,
## the same call path the live drive uses — NOT calling
## `_maybe_reset_to_canonical_frame()` directly with unchanged state, which
## trivially can't reproduce the drift the storm depends on) many ticks past
## the point where the reset first fires — asserts ZERO additional tile-set
## entries occur across the WHOLE gesture. Spies on `AtlasWindowTileSet`'s
## own child `AtlasWindowRequest` node INSTANCES (captured right after the
## FIRST reset) — a fresh `enter_orbital()` call tears down (`queue_free()`s)
## every one of them and creates BRAND NEW ones, so "the same node
## instances are still alive and still the tile set's children after 100
## more ticks" is a direct, non-invasive proxy for "the reset never fired
## again" — no new production instrumentation needed. Before the round-6
## fix, `_zoom_at()`'s continued multiplicative zoom-out drifted `_view_zoom`
## below fit on every subsequent tick, the level-triggered guard read "not
## already there" every time, and `enter_orbital()` fired repeatedly:
## tearing down and recreating the tile set (and its 6 request nodes) every
## tick — exactly the "889 of 897 wire responses arrived during one
## zoom-out phase" storm.
func test_reset_evaluated_repeatedly_at_canonical_frame_issues_zero_additional_requests() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — a tiling body, the live-repro shape
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
# Zoom IN far enough to leave tile mode, then zoom back OUT past the
# first reset — same shape as the round-5 continued-zoom-out test, but
# this time spying on the tile set across the WHOLE remaining gesture
# instead of only checking the final zoom value.
var cursor_pos := Vector2(1100.0, 300.0) # matches tmp_drive_ladder.gd's own aim point
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).is_false()
for _i in range(200):
v._zoom_at(cursor_pos, 1.0 / 1.05)
if v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: the first reset must have fired before spying on the tile set"
).is_true()
var tile_set = v.get_tile_set()
var original_requests: Array = tile_set.get_children()
assert_int(original_requests.size()).override_failure_message(
"sanity: the first reset must have created real tile-request child nodes to spy on"
).is_greater(0)
# Continue the SAME zoom-out gesture 100 MORE ticks past the first
# reset — a real wheel gesture has no way to stop exactly at the reset
# point, and holding the wheel down (or residual scroll momentum) keeps
# sending ticks. None of these must tear down/recreate the tile set.
for _i in range(100):
v._zoom_at(cursor_pos, 1.0 / 1.05)
var current_requests: Array = tile_set.get_children()
assert_int(current_requests.size()).override_failure_message(
"the tile set's child count must be unchanged after 100 more continued"
+ " zoom-out ticks — a changed count means teardown/recreate happened"
).is_equal(original_requests.size())
for i in range(original_requests.size()):
assert_bool(is_instance_valid(original_requests[i])).override_failure_message(
"original tile-request node #%d must still be alive — a storm would have"
+ " queue_free()'d it and created a fresh one" % i
).is_true()
assert_bool(is_same(original_requests[i], current_requests[i])).override_failure_message(
(
"tile-request node #%d must be the SAME instance as right after the"
+ " first reset — a different object at the same index means the tile"
+ " set was torn down and recreated (a storm), even if the count"
+ " coincidentally matches"
)
% i
).is_true()
## Live round 6's BLACK-ENTRY repro: enter_orbital(), then deliver the six
## wire-accurate tile responses WHILE a REAL continued zoom-out gesture (via
## `_zoom_at()`, matching the live drive's actual input shape — a held
## wheel-down keeps sending ticks concurrently with responses streaming in
## from the server) is in flight — asserts all six are accepted and HELD
## (tile set stable throughout, no teardown between delivery and the final
## assertion). Before the round-6 fix, the level-triggered guard fired on
## every zoom-out tick once `_view_zoom` drifted below fit, tearing down the
## tile set mid-delivery and orphaning responses addressed to now-freed
## request nodes — nothing ever accumulated, and the mosaic stayed black
## even though the server dutifully answered every request.
func test_six_tile_responses_survive_concurrent_reset_evaluation_and_are_held() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — 6 tiles, the live-repro shape
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
var tile_set = v.get_tile_set()
var tiles: Array = tile_set.get_tiles()
assert_int(tiles.size()).override_failure_message(
"sanity: Lendel must produce Lendel's own real tile count (6) for this"
+ " repro to be faithful, not a smaller synthetic count"
).is_equal(6)
# Same continued zoom-out gesture as the anti-storm test above — leave
# tile mode, cross back into it (the first reset), then KEEP sending
# zoom-out ticks (a real held wheel has no way to stop exactly at the
# reset point). Responses are delivered interleaved with these ticks,
# exactly matching the live drive's concurrent shape.
var cursor_pos := Vector2(1100.0, 300.0)
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).is_false()
for _i in range(200):
v._zoom_at(cursor_pos, 1.0 / 1.05)
if v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: the first reset must have fired before delivering responses"
).is_true()
for i in range(tiles.size()):
var center: Vector2i = tiles[i]["center"]
var tile_window: Dictionary = {
"center": [center.x, center.y],
"n": AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION,
"granularity": SERVER_LEGACY_GRANULARITY_REGION_SENTINEL,
"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]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", tile_window))
# Interleave several MORE continued zoom-out ticks, matching the live
# drive's per-frame cadence — none of these must tear anything down.
for _tick in range(5):
v._zoom_at(cursor_pos, 1.0 / 1.05)
var final_tiles: Array = tile_set.get_tiles()
assert_int(final_tiles.size()).override_failure_message(
"the tile set must still have all 6 tile slots — a storm mid-delivery"
+ " would have torn it down and rebuilt it with fresh (unfulfilled) slots"
).is_equal(6)
for i in range(final_tiles.size()):
assert_that(final_tiles[i]["window"]).override_failure_message(
(
"tile #%d's window must be HELD (non-null) — all six wire-accurate"
+ " responses delivered during a concurrent continued zoom-out gesture"
+ " must survive to be accepted, not be silently dropped by an"
+ " orphaning teardown"
)
% i
).is_not_null()
## Not fully zoomed out (a normal District-rung view) must NOT trigger the
## reset — only reaching the top of the ladder resets, not every zoom step.
func test_reset_to_canonical_frame_does_not_fire_when_not_fully_zoomed_out() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(500, 10), 32)
v._view_zoom = 1.0 # a normal, non-extreme zoom — nowhere near full planetary coverage
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"an ordinary District-rung view must not trigger the top-rest-state reset"
).is_false()
# =============================================================================
# T-1153: rung reselection — _zoom_at() crossing a rung threshold fires a
# new request without touching the held composite.
# =============================================================================
## Zooming OUT far enough from a District-rung window (small n, so a modest
## zoom-out already covers a huge world extent) must fire a coarser-rung
## request — the wheel-zoom-driven wiring test for _maybe_reselect_rung().
func test_zoom_out_past_district_threshold_requests_a_coarser_rung() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 2) # n=2 — a tiny window, easy to overshoot
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
assert_str(v._window_request.get_granularity_v2()).is_equal("District")
# A big zoom-OUT factor (well under 1.0) from a tiny n=2 window blows the
# displayed world extent WAY past District's threshold.
v._zoom_at(Vector2(400.0, 300.0), 0.01)
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
"zooming out far enough from a small District window must re-request a coarser rung"
).is_not_equal("District")
# The OLD composite must still be what's held — progressive refinement,
# not a block-on-derive clear.
assert_that(v.get_district_window()).is_equal(district_window)
## Zooming IN on a District-rung window (well within its own legal coverage
## band, `(32,768 m, 131,072 m]` per select_rung()'s redesigned per-rung
## ceiling model — viewport-independent since `canvas_px` no longer affects
## selection) must NOT trigger a rung change — this is the "zoom is
## client-side on the already-held composite" case, unchanged for in-rung
## zoom. Sets _view_zoom DIRECTLY to a value inside District's band (rather
## than relying on enter()'s COVER auto-fit, which for a small n can already
## sit right at Quarter's own threshold — a fit's zoom level is a
## display-density choice independent of what rung selection would pick from
## scratch, and this test is specifically about a SINGLE zoom-in STEP not
## crossing a boundary, not about where the auto-fit itself lands). The
## small 100x80 viewport here is incidental (any size works under the new
## viewport-independent model) — kept small only because that's what the
## original version of this test used.
func test_zoom_in_within_district_threshold_does_not_change_rung() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(100.0, 80.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._view_zoom = 0.10666666666666667 # E=120,000m at C=100px — inside District's legal band
v._apply_transform()
v._zoom_at(Vector2(50.0, 40.0), 1.15) # a single ordinary zoom-in step
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
"a single ordinary zoom-in step must not cross a rung threshold"
).is_equal("District")
## **Live round 3 regression, the direct end-to-end fix target:** a real
## wheel-zoom gesture (many `_zoom_at()` ticks, matching the shape a
## continuous mouse-wheel scroll actually produces) crossing from the
## Region rest state down through District into Quarter territory must (i)
## fire a request at the NEW granularity — `_window_request.get_granularity_v2()`
## must have changed by the end of the gesture — and (ii) accept a
## WIRE-ACCURATE response for that request: echoing the REQUEST's own
## (already re-centered, already re-clamped) center/n, which the live round
## found DIFFERS from the ORIGINAL held center (screen-center-anchored
## refinement re-centers on wherever the cursor currently maps to, not
## wherever the player started) — this is the "second latent drop" the
## coordinator specifically flagged: comparing the echo against a STALE
## `_held_center` (frozen at the pre-crossing value) rather than the
## request's own center would silently drop this response too.
## **Live round 3 update:** GJ380c/Lendel now enters TILE mode via
## enter_orbital() (bug B's fix), so this test starts from THERE — zooming
## in far enough crosses Region's coverage ceiling and must LEAVE tile mode
## for the single-window path at the new (finer) rung, exactly the
## `_maybe_reselect_rung()` "leaving_tile_mode" branch this test exercises.
func test_zoom_crossing_fires_request_and_accepts_wire_accurate_refinement() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).override_failure_message(
"GJ380c/Lendel must enter tile mode at the orbital rest state (live round 3)"
).is_true()
# A real wheel-zoom gesture: many ticks, cursor OFF-CENTER (so cursor-
# anchored zoom genuinely drifts the screen-to-district mapping away from
# the canonical origin, not just scaling in place) — matching the live
# drive's actual input shape, not a single synthetic jump. Zooming in far
# enough must cross OUT of Region's coverage ceiling, leaving tile mode.
var cursor_pos := Vector2(1100.0, 300.0) # off-center, biased toward one quadrant
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
# (i) Tile mode must have been LEFT, and a request must have gone out at
# a NEW (finer) granularity via the single-window path.
assert_bool(v.is_tile_mode()).override_failure_message(
"zooming in far enough must leave tile mode for the single-window path"
).is_false()
var request_granularity: String = v._window_request.get_granularity_v2()
assert_str(request_granularity).override_failure_message(
"leaving tile mode must fire a request at a new (finer) granularity"
).is_not_equal("Region")
# (ii) The request's own center/n — read AFTER leaving tile mode, so this
# is whatever _maybe_reselect_rung() actually computed — is what a
# wire-accurate response must echo to be accepted.
var request_center: Vector2i = v._window_request._center
var request_n: int = v._window_request._n
var refinement_window: Dictionary = {
"center": [request_center.x, request_center.y],
"n": request_n,
"granularity_v2": request_granularity,
"morphology": PackedByteArray([1, 2, 3, 4]),
"elev_q": PackedByteArray([10, 20, 30, 40]),
"temp_dc": [0, 0, 0, 0],
"moisture_q": PackedByteArray([0, 0, 0, 0]),
"vegetation": PackedByteArray([0, 0, 0, 0]),
"glaciation": PackedByteArray([0, 0, 0, 0]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", refinement_window))
assert_that(v.get_district_window()).override_failure_message(
"a wire-accurate refinement response (echoing the REQUEST's own center/n/"
+ " granularity after leaving tile mode) must be ACCEPTED — comparing"
+ " against a stale/wrong reference instead of the request's own would"
+ " silently drop this response forever"
).is_equal(refinement_window)
assert_str(v._held_granularity_v2).is_equal(request_granularity)
## Live round 4's SECOND bug, pinned directly: `_maybe_reselect_rung()` must
## recompute `_view_offset` (via AtlasWindowGeometry.
## recompute_offset_for_held_n_change()) the instant `_held_n` changes across
## a rung crossing — leaving it untouched (the round-4 bug) means the single-
## window `Rect2(0,0,extent)` draw call renders at whatever screen position
## the OLD (Region-scale) offset happened to put canvas-local (0,0), which
## for a whole-body `held_n` vs. a 64-district District `held_n` is tens or
## hundreds of thousands of px away from the viewport — the exact "pitch
## black" repro. Asserts the NEW held window's own extent actually overlaps
## the viewport after the crossing, the concrete on-screen consequence a
## stale offset breaks.
func test_zoom_crossing_recomputes_view_offset_so_the_new_window_is_on_screen() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
var cursor_pos := Vector2(1100.0, 300.0)
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: this test needs to actually cross out of tile mode to exercise"
+ " the held_n change _maybe_reselect_rung() must react to"
).is_false()
# The new (post-crossing) window's screen-space rect, using the SAME
# formula the overlay's single-window _draw() itself uses
# (Rect2(0,0,extent,extent) in canvas-local space, then _canvas's own
# position/scale transform — _view_offset/_view_zoom here mirror that
# exactly, since _apply_transform() is what sets _canvas.position/scale).
var extent_screen: float = float(v._held_n) * v.CELL_PIXEL_SIZE * v._view_zoom
var screen_top_left: Vector2 = v._view_offset
var screen_bottom_right: Vector2 = screen_top_left + Vector2(extent_screen, extent_screen)
var viewport_rect := Rect2(Vector2.ZERO, v.size)
var window_rect := Rect2(screen_top_left, Vector2(extent_screen, extent_screen))
assert_bool(viewport_rect.intersects(window_rect)).override_failure_message(
(
"the new (post-crossing) held window's screen rect %s must overlap the"
+ " viewport %s — a stale _view_offset (never recomputed for the new"
+ " held_n=%d) is exactly live round 4's 'pitch black' bug: the composite"
+ " renders somewhere entirely off-canvas despite request/response/data"
+ " all being individually correct"
)
% [window_rect, viewport_rect, v._held_n]
).is_true()
# =============================================================================
# T-1153: E/W wrap and pole-wall clamps at EVERY rung — both are extent-
# relative (CELL_PIXEL_SIZE-based district-space math, unchanged regardless
# of which rung's data is actually held), so they must keep working
# unmodified at Region granularity, not just District/Quarter.
# =============================================================================
## The pole wall, wired through the real _apply_pan_delta() path, must still
## clamp at Region granularity — same mechanism as the existing District-rung
## test (test_wasd_pan_is_clamped_by_the_pole_wall_when_wired), just entered
## via enter_orbital() instead of enter().
func test_pole_wall_clamps_at_region_granularity_too() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
var radius_km := 50.0 # tiny synthetic body — pole wall reachable by an ordinary tick
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
var unclamped_magnitude: float = 500.0 * AtlasWindowViewer.PAN_SPEED_CANVAS_PX_S * v.get_view_zoom()
v._apply_pan_delta(Vector2(0.0, -1.0), 500.0) # "W"/north held, an absurdly long tick
assert_float(absf(v.get_view_offset().y)).override_failure_message(
"the pole wall must still clamp an extreme pan at Region granularity"
).is_less(unclamped_magnitude * 0.5)
## East-west wrap (canonicalize_district_center()) must still apply to the
## pan-edge refloat's resulting center at Region granularity — a pan that
## carries the screen-center column past the body's circumference must wrap
## into [0, cols), never run away to an out-of-range column, exactly as the
## District-rung wrap tests already pin (T-1142 item 6a). At Region's own
## enormous held_n (a whole circumference), an ORDINARY pan tick's
## canvas-space delta is negligible relative to the window's half-extent
## (confirmed: ~0.08 districts per 5-second tick vs. a ~9,772-district
## half-window) — so this drives _maybe_refloat_window() DIRECTLY off a
## manually-set _view_offset large enough to genuinely cross the held
## window's edge, the same "exercise the actual edge-crossing branch, not
## just its no-op early-return" discipline _maybe_refloat_window()'s own
## inside-check comment describes.
func test_pan_edge_refloat_wraps_columns_at_region_granularity_too() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
# Force the held center to sit one column short of the wrap seam, then
# shift the CANVAS offset by more than half the window's own on-screen
# extent — enough to move the screen-center's mapped column past the
# window's far edge (i.e. past `cols`, crossing the seam) regardless of
# Region's huge held_n.
v._held_center = Vector2i(cols - 1, 0)
var half_window_screen_px: float = float(v._held_n) * v.get_cell_pixel_size() * v.get_view_zoom() * 0.5
v._view_offset = v.get_view_offset() - Vector2(half_window_screen_px * 1.5, 0.0)
v._maybe_refloat_window()
assert_int(v._held_center.x).override_failure_message(
"a pan crossing the antimeridian at Region granularity must wrap the"
+ " resulting center into [0, cols), never run past cols"
).is_less(cols)
assert_int(v._held_center.x).is_greater_equal(0)
+19 -38
View File
@@ -1,8 +1,20 @@
class_name AtlasApp
extends ImplantApp
## Atlas implant app (#844, #836, D-191).
## Reach map → system orbital → planet entry → regional heightmap viewer.
## Reach map → system orbital → planet entry → regional zoom ladder.
## Registered as "implant/map" in FULLSCREEN mode.
##
## T-1153 (D-226 T-1143-rulings amendment): the "district" nav-stack screen
## (T-1138's windowed drill-down, a separate nav.push() hop from "regional")
## RETIRES as a nav-stack level — the continuous cursor-anchored zoom ladder
## means descent/ascent through every rung (Region -> District -> Quarter)
## happens INSIDE the "regional" screen via zoom, not by pushing a new
## screen. D-013 "the zoom gesture owns spatial descent" is restored for
## this seam (Jeroen's ruling) — descent is a continuous gesture, not a
## discrete nav hop. Esc from anywhere in the ladder is therefore a single
## nav.pop() back to "system", exactly the same _handle_key() KEY_ESCAPE
## branch every other non-"reach" screen already uses; there is no longer a
## "district" screen entry in the match/registration table.
signal economics_link_requested(system_id: String)
@@ -12,8 +24,7 @@ var _system_lookup: Dictionary = {} # system_id → system dict
var _reach_screen = null # ReachScreen
var _system_screen = null # SystemScreen
var _planet_screen = null # PlanetScreen
var _regional_screen = null # RegionalScreen
var _district_screen = null # DistrictScreen (T-1138)
var _regional_screen = null # RegionalScreen — now the whole zoom ladder (T-1153)
func _ready() -> void:
@@ -47,14 +58,8 @@ func on_install() -> void:
_regional_screen = RegionalScreen.new()
_regional_screen.back_requested.connect(_on_regional_back)
_regional_screen.economics_link_requested.connect(_forward_economics_link)
_regional_screen.district_descend_requested.connect(_on_district_descend_requested)
register_screen("regional", _regional_screen)
_district_screen = DistrictScreen.new()
_district_screen.back_requested.connect(_on_district_back)
register_screen("district", _district_screen)
nav.set_default("reach")
@@ -71,12 +76,11 @@ func _unhandled_key_input(event: InputEvent) -> void:
return
if not event.is_pressed() or event.is_echo():
return
# "regional" (the planetary heightmap, AtlasViewer) and "district" (the
# windowed regional-window screen, AtlasWindowViewer, T-1138) both handle
# their own Esc via _gui_input — same delegation shape for both, so a
# stray M/other unhandled key on either screen doesn't ALSO fire this
# app's own _handle_key underneath the viewer's own handling.
if current_screen_id() == "regional" or current_screen_id() == "district":
# "regional" (the whole zoom ladder, AtlasWindowViewer, T-1153) handles its
# own Esc via _gui_input — a stray M/other unhandled key on that screen
# must not ALSO fire this app's own _handle_key underneath the viewer's
# own handling.
if current_screen_id() == "regional":
return
_handle_key(event as InputEventKey)
get_viewport().set_input_as_handled()
@@ -148,29 +152,6 @@ func _on_regional_back() -> void:
nav.pop()
## T-1138: the planetary click-through descent (§5 entry revision) — pushes
## the "district" screen centered on the click point's derived DistrictPos.
## A real nav.push() (not a swap-in-place, unlike the superseded zoom-
## threshold design) so Esc's existing nav.pop() path (DistrictScreen's own
## back_requested -> _on_district_back below) returns to exactly the
## planetary body the player descended from, at whatever crumb depth got
## them there (reach -> system -> regional -> district).
func _on_district_descend_requested(district_center: Vector2i) -> void:
nav.push("district", {
"body": nav.current_payload().get("body", {}),
"system": nav.current_payload().get("system", {}),
"district_center": district_center,
})
func _on_district_back() -> void:
nav.pop()
func _forward_economics_link(system_id: String) -> void:
economics_link_requested.emit(system_id)
## T-949: the star map arrived — cache it, rebuild the local system
## list/lookup, and push it into whichever screens already exist. set_systems()
## is safe to call again after initial setup (ReachScreen/SystemScreen both
@@ -2,21 +2,27 @@ extends RefCounted
## Client-side LRU cache for DistrictWindowLayer responses (T-1138, D-226
## T-1124 amendment §4 "Client cache policy"; extended T-1150 for the
## granularity/min_wl axes).
## granularity/min_wl axes; extended T-1152/T-1153 for the granularity_v2
## string-tag axis that makes Region representable at all).
##
## Keyed on (body_id, center, n, granularity, min_wl_m) — D-227's determinism
## guarantee (same seed + body + position + derivation params -> same derived
## output, always) means a previously-fetched window is valid FOREVER for
## that body+seed. This is an LRU-evict-only cache: no freshness check, no
## TTL, no invalidation path at all. The only reason an entry ever leaves is
## capacity pressure.
## Keyed on (body_id, center, n, granularity, min_wl_m, granularity_v2) —
## D-227's determinism guarantee (same seed + body + position + derivation
## params -> same derived output, always) means a previously-fetched window is
## valid FOREVER for that body+seed. This is an LRU-evict-only cache: no
## freshness check, no TTL, no invalidation path at all. The only reason an
## entry ever leaves is capacity pressure.
##
## granularity/min_wl_m default to DISTRICT_GRANULARITY/0 (district spacing,
## no octave cutoff) — every pre-T-1150 caller that doesn't pass them keeps
## its existing key shape and cache behavior unchanged. This is the client
## half of the mandatory aliasing fix (T-1150 design doc §3): a
## quarter-granularity request and a district-granularity request at the
## identical (body, center, n) MUST NOT collide on the same cache slot.
## granularity/min_wl_m/granularity_v2 default to
## DISTRICT_GRANULARITY/0/DEFAULT_GRANULARITY_V2 ("District", district
## spacing, no octave cutoff) — every pre-T-1150 caller that doesn't pass them
## keeps its existing key shape and cache behavior unchanged. This is the
## client half of the mandatory aliasing fix (T-1150 design doc §3, extended
## T-1152): a quarter-granularity request, a district-granularity request,
## and a REGION-granularity request all at the identical (body, center, n)
## MUST NOT collide on the same cache slot — the legacy int alone cannot
## distinguish Region (it has no legal legacy-int value, see
## GRANULARITY_V2_REGION's doc), which is exactly why granularity_v2 is a
## SEPARATE key component rather than a replacement for the legacy one.
##
## Godot's Dictionary preserves insertion order, so "move to the end on
## touch, evict from the front on overflow" is the whole LRU implementation —
@@ -33,6 +39,24 @@ const DEFAULT_MAX_ENTRIES: int = 24
## default granularity every pre-T-1150 caller implicitly requests.
const DISTRICT_GRANULARITY: int = 1
## Mirrors the server's WindowGranularity enum (T-1152, R5 redesign,
## layer_proxy.rs) — the string-tag vocabulary rmp_serde encodes a bare
## `#[derive(Serialize, Deserialize)]` enum's variant name as, verbatim (same
## wire convention `RoadNodeKind` already established on this carrier). This
## is the KEY-SPACE axis (T-1153): the legacy int `granularity` param below
## stays wired for every existing District/Quarter caller (byte/behavior
## compatible), but a cache slot is now ALSO qualified by this string so a
## Region-rung window can never alias onto a District/Quarter slot at the
## identical (body, center, n, legacy_granularity, min_wl_m) — the exact
## aliasing risk the T-1150 design doc §3 flagged, extended to the new axis.
const GRANULARITY_V2_QUARTER: String = "Quarter"
const GRANULARITY_V2_DISTRICT: String = "District"
const GRANULARITY_V2_REGION: String = "Region"
## Default v2 tag for every caller that doesn't pass one — matches
## DISTRICT_GRANULARITY's own "district is the implicit default" contract, so
## an omitted v2 tag and an explicit "District" tag key identically.
const DEFAULT_GRANULARITY_V2: String = GRANULARITY_V2_DISTRICT
var _max_entries: int = DEFAULT_MAX_ENTRIES
var _entries: Dictionary = {} # key String -> DistrictWindowLayer Dictionary
@@ -41,34 +65,42 @@ func _init(max_entries: int = DEFAULT_MAX_ENTRIES) -> void:
_max_entries = maxi(1, max_entries)
## Build the cache key from the five fields D-227 + T-1150 make sufficient:
## body_id (which world+body), center (a [row, col] pair or Vector2i), n
## (window extent in districts), granularity (district=1 / quarter=4), and
## min_wl_m (the octave cutoff, 0 = none). String-keyed rather than a nested
## Dictionary/Array key — Godot Dictionary keys compare by value for
## primitives but a consistent stringification sidesteps any
## Vector2i-vs-Array identity mismatch between what a caller happens to hand
## in.
## Build the cache key from the six fields D-227 + T-1150/T-1152 make
## sufficient: body_id (which world+body), center (a [row, col] pair or
## Vector2i), n (window extent in districts), granularity (the legacy int:
## district=1 / quarter=4), min_wl_m (the octave cutoff, 0 = none), and
## granularity_v2 (the T-1152 string tag: "Quarter"/"District"/"Region" — the
## axis that actually distinguishes Region from every finer rung, since
## Region has no legal legacy-int representation and the legacy slot alone
## cannot tell a Region window's cache entry apart from a District one at the
## same (center, n)). String-keyed rather than a nested Dictionary/Array key
## — Godot Dictionary keys compare by value for primitives but a consistent
## stringification sidesteps any Vector2i-vs-Array identity mismatch between
## what a caller happens to hand in.
static func make_key(
body_id: String,
center: Vector2i,
n: int,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> String:
return "%s:%d,%d:%d:%d:%d" % [body_id, center.x, center.y, n, granularity, min_wl_m]
return "%s:%d,%d:%d:%d:%d:%s" % [
body_id, center.x, center.y, n, granularity, min_wl_m, granularity_v2
]
## True if a window is already cached for this exact (body, center, n,
## granularity, min_wl_m).
## granularity, min_wl_m, granularity_v2).
func has(
body_id: String,
center: Vector2i,
n: int,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> bool:
return _entries.has(make_key(body_id, center, n, granularity, min_wl_m))
return _entries.has(make_key(body_id, center, n, granularity, min_wl_m, granularity_v2))
## Fetch a cached window, touching it (move-to-most-recently-used). Returns
@@ -81,9 +113,10 @@ func get_window(
center: Vector2i,
n: int,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> Variant:
var key := make_key(body_id, center, n, granularity, min_wl_m)
var key := make_key(body_id, center, n, granularity, min_wl_m, granularity_v2)
if not _entries.has(key):
return null
var value: Variant = _entries[key]
@@ -101,9 +134,10 @@ func put(
n: int,
window: Dictionary,
granularity: int = DISTRICT_GRANULARITY,
min_wl_m: int = 0
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> void:
var key := make_key(body_id, center, n, granularity, min_wl_m)
var key := make_key(body_id, center, n, granularity, min_wl_m, granularity_v2)
if _entries.has(key):
_entries.erase(key)
_entries[key] = window
@@ -10,6 +10,102 @@ extends RefCounted
## window size, what zoom/offset centers it" math is unit-testable in
## isolation here — a caller does:
## const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
##
## T-1153: also carries the rung-selection rule (design doc
## docs/architecture/atlas-zoom-ladder-t1143.md §5) and the "fully zoomed
## out" reset predicate (Jeroen's D-226 T-1143-rulings HARD condition) — both
## pure functions of (viewport, held state, body), same "geometry lives here,
## side effects live on the viewer" split as the rest of this file. Live
## round 3 also adds the orbital-rest-state TILE GRID computation
## (compute_tile_grid(), near the bottom) — reuses
## AtlasDescendGeometry.district_extent()/canonicalize_district_center() for
## the SAME wrap/clamp discipline every other piece of this cluster already
## depends on, hence the preload below (no circular dependency:
## atlas_descend_geometry.gd never references this file).
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## D-243 rung spacings, metres/cell — the SAME constants
## server/src/atlas/scale.rs and layer_proxy.rs's WindowGranularity::spacing_m
## source from, mirrored here rather than re-derived so the client's rung
## table can never silently drift from the wire contract it's choosing
## between.
const QUARTER_SPACING_M: float = 512.0
const DISTRICT_SPACING_M: float = 2048.0
const REGION_SPACING_M: float = 204_800.0
## Table form, coarsest-first — spacing_for_rung()'s inverse lookup walks
## this. select_rung() (below) does NOT walk this table directly — see that
## function's own doc for why the coarse (Region) and fine (District/
## Quarter) ends are decided by two DIFFERENT tests, not a single ordered
## table scan.
const RUNG_TABLE: Array = [
{"granularity_v2": "Region", "spacing_m": REGION_SPACING_M},
{"granularity_v2": "District", "spacing_m": DISTRICT_SPACING_M},
{"granularity_v2": "Quarter", "spacing_m": QUARTER_SPACING_M},
]
## Server per-axis cap on a District/Quarter-granularity window's `n`
## (mirrors server/src/atlas/layer_proxy.rs's `DISTRICT_WINDOW_MAX_N` — see
## AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N, the existing client-side
## mirror of the same constant, kept in sync there).
const DISTRICT_WINDOW_MAX_N: int = 64
## Wire-size ceiling (mirrors AtlasWindowRequest.SERVER_WIRE_CAP_CELLS /
## server/src/atlas/layer_proxy.rs's WIRE_CAP_CELLS) — the cell-count cap
## EVERY rung's single window is clamped against, per
## `_clamp_window_n_mirror`/`_clamp_window_n_mirror_v2`'s own formulas.
const WIRE_CAP_CELLS: int = 4_096
## Region's own per-axis ceiling (mirrors
## AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION /
## server/src/atlas/layer_proxy.rs's DISTRICT_WINDOW_MAX_N_REGION).
const DISTRICT_WINDOW_MAX_N_REGION: int = 6_400
## Per-tile district extent for the orbital-rest-state tile grid
## (compute_tile_grid(), near the bottom of this file) — the SAME `n` a
## single Region request uses at its own per-axis ceiling. Each tile
## requests exactly this many districts on a side — the largest single
## window the wire budget allows, so tiling uses the FEWEST tiles that can
## cover a given body.
const TILE_N: int = DISTRICT_WINDOW_MAX_N_REGION
## **Live round 3 finding (the actual root cause of "zoom-driven rung
## reselection never fires"):** each rung's SINGLE WINDOW has a hard MAXIMUM
## real-world coverage, derived from the SAME wire-size clamp
## (`_clamp_window_n_mirror_v2`) the request layer already enforces — District
## and Quarter are NOT exempt from this the way the original (§5-literal)
## design assumed. A rung whose own single-window coverage is smaller than
## the CURRENTLY DISPLAYED world extent cannot legally be selected: the
## server would clamp `n` down to fit its own wire budget, producing a
## composite that covers only a FRACTION of the viewport — visually a tiny
## box in the middle of the screen, and (the bug this constant's discovery
## fixes) a composite whose CLAMPED `n` no longer matches whatever `_held_n`
## the viewer was still carrying from the PREVIOUS rung, permanently failing
## `_on_window_ready()`'s staleness check. Computed here ONCE, from the same
## constants `_clamp_window_n_mirror_v2` uses, rather than re-derived per
## rung inline — see MAX_COVERAGE_M below.
##
## - Quarter: per-axis cap `floor(sqrt(WIRE_CAP_CELLS)/4) = 16` districts ->
## cell-grid side `16*4 = 64` cells -> `64 * QUARTER_SPACING_M = 32,768 m`.
## - District: per-axis cap `floor(sqrt(WIRE_CAP_CELLS)/1) = 64` districts ->
## `64 * DISTRICT_SPACING_M = 131,072 m` (unchanged from the original
## coverage-ceiling constant this replaces/generalizes).
## - Region: per-axis cap `DISTRICT_WINDOW_MAX_N_REGION = 6,400` districts ->
## `6,400 * DISTRICT_SPACING_M = 13,107,200 m` — this is a SINGLE window's
## ceiling; bug B's progressive tiling composes MULTIPLE Region windows to
## cover extents beyond this (see the viewer's tile-set model), so this
## constant alone does NOT bound what the ORBITAL REST STATE can show —
## only what one Region REQUEST's response covers.
const MAX_COVERAGE_M: Dictionary = {
"Quarter": 64.0 * QUARTER_SPACING_M,
"District": float(DISTRICT_WINDOW_MAX_N) * DISTRICT_SPACING_M,
"Region": float(DISTRICT_WINDOW_MAX_N_REGION) * DISTRICT_SPACING_M,
}
## Rungs ordered FINEST-first — select_rung() walks this to find the finest
## rung whose own single-window coverage ceiling still covers the current
## extent (never a rung that would silently under-cover the viewport).
const RUNGS_FINEST_FIRST: Array = ["Quarter", "District", "Region"]
## Fit-and-center: given the viewport size and the window's side length in
@@ -116,3 +212,464 @@ static func clamp_pan_offset_to_pole_wall(
var min_y: float = minf(north_wall_screen_y, south_wall_screen_y)
var max_y: float = maxf(north_wall_screen_y, south_wall_screen_y)
return Vector2(offset.x, clampf(offset.y, min_y, max_y))
# =============================================================================
# T-1153: rung selection (design doc §5) — the continuous zoom ladder's join
# point between "what granularity is legal to request" (a rung, a discrete
# set) and "what density the client actually wants" (world extent per canvas
# px, a continuous quantity that tracks the live zoom level).
# =============================================================================
## Rung-selection rule — REDESIGNED (live round 3 finding, superseding the
## original §5-literal `2x`-visual-tolerance-only reading): select the
## FINEST rung whose OWN single-window coverage ceiling (MAX_COVERAGE_M)
## still covers the current world extent. Walks RUNGS_FINEST_FIRST
## (Quarter, District, Region) and returns the first whose ceiling is `>=
## world_extent_m` — the coarser rungs are tried only once the finer ones
## genuinely cannot show the requested extent in a single window.
##
## **Why this replaces the original `2x`-visual-tolerance formula entirely**
## (not just patches its Region case, as an earlier version of this function
## did): the design doc §5 rule ("coarsest rung whose spacing <= 2*(E/C)")
## implicitly assumes every rung's single window CAN cover any extent the
## rule selects it for — true for an unbounded wire budget, false here.
## `_clamp_window_n_mirror_v2` (AtlasWindowRequest) — the SAME clamp the
## server itself enforces — caps every rung's single-window real-world
## coverage at a fixed maximum (`MAX_COVERAGE_M`, this file): Quarter
## 32,768 m, District 131,072 m, Region 13,107,200 m (per single Region
## window — bug B's progressive TILING composes several to cover more, a
## viewer-level concern this function doesn't need to know about). A rung
## selected for an extent BEYOND its own ceiling would have its `n` silently
## clamped server-side to something covering only a FRACTION of the
## viewport — visually a tiny box, AND (the actual live-round bug this
## redesign fixes) a clamped echo that no longer matches whatever `n` the
## viewer was still carrying from the rung it's leaving, permanently failing
## the staleness check in `_on_window_ready()`.
##
## **The `2x` visual-tolerance rule becomes REDUNDANT under this model, not
## contradicted by it** — verified numerically: at the exact zoom where
## Quarter's coverage ceiling (32,768 m) is reached, the `2x` threshold
## (`2*E/C`) works out to ~41 m, far finer than even Quarter's own 512 m
## spacing. This means by the time coverage RELEASES a rung, the visual
## tolerance would ALREADY prefer something finer than that rung offers —
## i.e. every rung this function selects is, by construction, at or past its
## own "as fine as it can usefully be" point. The visual-tolerance rule's
## fine-end guarantee (never show a coarser composite than the screen can
## resolve) is automatically satisfied by "select the finest rung whose
## coverage allows it" — there is no case where the coverage rule picks a
## rung the visual rule would have rejected as too coarse, because Quarter
## (the finest rung) is always the answer whenever ANY rung's visual
## tolerance alone would have mattered.
##
## `world_extent_m`/`canvas_px` are both callers'-choice-of-axis (the held
## window is always square, so either axis of the viewport/extent pair gives
## the same answer — the caller picks one, consistently). `canvas_px` is
## kept as a parameter (unused by the coverage rule itself) for signature
## stability with existing callers and because a future finer-than-Quarter
## rung (block/tile, D-226(d)-gated, out of scope here) would plausibly need
## it again.
##
## Returns the granularity_v2 string tag ("Quarter" | "District" | "Region").
static func select_rung(world_extent_m: float, _canvas_px: float) -> String:
for rung: String in RUNGS_FINEST_FIRST:
if world_extent_m <= float(MAX_COVERAGE_M[rung]):
return rung
return "Region" # extent exceeds even Region's own single-window ceiling -> still Region (tiling's job)
## The metre spacing a given granularity_v2 tag resolves to — the inverse
## lookup select_rung() itself doesn't need but callers computing "what world
## extent does holding N districts at this rung actually cover" do (the
## viewer's own extent-in-real-units header line, and the full-zoom-out
## predicate below).
static func spacing_for_rung(granularity_v2: String) -> float:
for rung: Dictionary in RUNG_TABLE:
if rung["granularity_v2"] == granularity_v2:
return float(rung["spacing_m"])
return DISTRICT_SPACING_M # unknown tag -> district, matching the server's "unknown -> District" posture
# =============================================================================
# T-1153: full-zoom-out reset (Jeroen's D-226 T-1143-rulings HARD condition —
# "a full zoom-out resets to the original canonical planetary frame and
# location", the ladder's top rest state, not a drifted pan state).
# =============================================================================
## True once the requested world extent (at the CURRENT zoom, before any
## further zoom-out) covers the full body — i.e. the player has zoomed out as
## far as the ladder goes and is looking at (at least) the whole equatorial
## circumference. This is the crisp "fully zoomed out" definition the ticket
## asks for: `world_extent_m >= circumference_m` at the fit-zoom floor, rather
## than a fuzzy "close to MIN_ZOOM" heuristic (MIN_ZOOM is a UI clamp
## constant, not a planetary-coverage fact — a small body could hit MIN_ZOOM
## while still showing less than the whole circumference, and a huge body
## could show full coverage before MIN_ZOOM is reached, depending on
## CELL_PIXEL_SIZE/held_n; extent-vs-circumference is the honest test either
## way).
##
## **Distinct from select_rung()'s own coverage ceiling** (District's
## `DISTRICT_WINDOW_MAX_N * DISTRICT_SPACING_M = 131,072 m`, a fixed,
## body-independent number) — this predicate's threshold is the actual
## body's full circumference, always far larger than 131,072 m for any real
## planet. The two compose in the expected order: extent crosses 131,072 m
## first (select_rung() already reports Region well before this predicate
## fires), and only once extent reaches the WHOLE circumference does the
## top-rest-state reset itself trigger. There is no conflict between the two
## thresholds, just two different questions ("which rung" vs. "are we at the
## very top").
static func is_fully_zoomed_out(world_extent_m: float, body_radius_km: float) -> bool:
if body_radius_km <= 0.0:
return false # no-radius (tiny test body) has no circumference concept — never auto-resets
var circumference_m: float = TAU * body_radius_km * 1000.0
return world_extent_m >= circumference_m
# =============================================================================
# T-1153: pure screen<->world math extracted from AtlasWindowViewer for
# testability (the project's stated preference — static funcs over Control
# instance methods wherever the math doesn't need the scene tree).
# =============================================================================
## The world extent (metres) currently displayed across the LARGER viewport
## dimension — the `E` half of the §5 rung-selection rule's `E/C`.
## `cell_pixel_size` is the caller's district-at-zoom-1.0 constant
## (AtlasWindowViewer.CELL_PIXEL_SIZE) — the composite's ON-SCREEN FOOTPRINT
## is ALWAYS `held_n * cell_pixel_size * view_zoom` px for `held_n * DISTRICT_M`
## metres of world, REGARDLESS of which rung is currently held (this is
## exactly the invariant AtlasWindowOverlay.cell_grid_side_for_window()'s doc
## establishes on the render side: `n` districts occupy a FIXED screen
## footprint; only the DERIVED CELL RESOLUTION packed into that footprint
## varies by rung). So the metres-per-screen-px sample density is a pure
## function of `view_zoom` — DISTRICT_SPACING_M / (cell_pixel_size *
## view_zoom) — with NO granularity_v2 parameter needed at all: the rung
## itself is the OUTPUT of this calculation (via select_rung()), not an
## input to it.
static func world_extent_m(cell_pixel_size: float, view_zoom: float, viewport: Vector2) -> float:
var canvas_px: float = cell_pixel_size * view_zoom
if canvas_px <= 0.0:
return 0.0
var screen_px: float = maxf(viewport.x, viewport.y)
return DISTRICT_SPACING_M / canvas_px * screen_px
## The DistrictPos the viewport's screen center currently maps to, in RAW
## absolute district space (un-wrapped, un-clamped — the caller canonicalizes
## the final value it actually stores/sends, matching
## canonicalize_district_center()'s own "canonicalize once, at the boundary"
## discipline). Shared by AtlasWindowViewer's pan-edge refetch
## (_maybe_refloat_window()) and rung-reselect refetch
## (_maybe_reselect_rung()) so both read the SAME screen-to-district formula
## rather than two copies that could drift.
static func screen_center_to_district(
viewport_size: Vector2,
view_offset: Vector2,
view_zoom: float,
cell_pixel_size: float,
held_center: Vector2i,
held_n: int
) -> Vector2i:
var screen_center: Vector2 = viewport_size * 0.5
var canvas_pt: Vector2 = (screen_center - view_offset) / view_zoom
var cell: Vector2 = canvas_pt / cell_pixel_size
var half: float = float(held_n) / 2.0
var abs_col: float = float(held_center.x) - half + cell.x
var abs_row: float = float(held_center.y) - half + cell.y
return Vector2i(roundi(abs_col), roundi(abs_row))
## Live round 4 fix: the exact INVERSE of screen_center_to_district()'s own
## district-space math, in CANVAS-LOCAL space (i.e. _canvas's own child
## coordinate system — BEFORE _view_offset/_view_zoom, which is what
## AtlasWindowOverlay._draw()/_draw_tile_mosaic() draw into, since the
## Node2D's position/scale already carries pan/zoom). Every held-window
## convention in this file agrees canvas-local `(0,0)` is absolute district
## `(held_center - held_n/2)` — single-window `_draw()`'s own
## `Rect2(0,0,extent,extent)` relies on this being true for `held_center` ==
## the window's own center. `_draw_tile_mosaic()`'s per-tile placement must
## use this SAME formula (with the VIEWER's `held_center`/`held_n`, not a
## tile's own center/TILE_N) to land in the same coordinate frame the
## fit/pan/zoom machinery already assumes — drawing tiles relative to
## absolute district (0,0) directly (the live-round-4 bug) silently
## disagreed with fit_window_view()'s own `[0, held_n)`-from-origin
## assumption whenever `held_n` (the WHOLE-BODY extent in tile mode) wasn't
## itself anchored the same way, pushing the entire mosaic off-canvas.
static func district_to_canvas_local(
district: Vector2, held_center: Vector2i, held_n: int, cell_pixel_size: float
) -> Vector2:
var half: float = float(held_n) / 2.0
var local_col: float = (district.x - (float(held_center.x) - half)) * cell_pixel_size
var local_row: float = (district.y - (float(held_center.y) - half)) * cell_pixel_size
return Vector2(local_col, local_row)
## Live round 5 fix (the tile-mosaic WRAP half of "the mosaic doesn't fully
## draw"): `compute_tile_grid()`'s tiles are CANONICAL columns (wrapped into
## `[0, cols)` — the correct, single-valued key for REQUESTS and cache
## coalescing), but a canonical column has infinitely many EQUIVALENT
## on-screen positions (`col`, `col - cols`, `col + cols`, ...), since
## longitude is periodic. `district_to_canvas_local()` is a pure LINEAR
## function with no wrap concept — fed a canonical column directly, it
## places the tile at exactly ONE of those wrap-images, which is only ever
## the visually-correct one by coincidence. Lendel's own repro: the tile
## whose pre-canonicalization center was -6400 canonicalizes to 12739
## (`-6400 mod 19139`) — correct for the request/cache key, but drawing at
## column 12739 directly places it canvas-local ~22308 (off-canvas RIGHT),
## when the tile's actual visible position (immediately west of the
## canonical origin) is at column -6400 (canvas-local ~3169, the LEFT
## third of the mosaic).
##
## The fix: before handing a tile's canonical column to
## `district_to_canvas_local()`, re-express it as whichever wrap-image
## (`canonical_col + k*cols` for integer `k`) is NEAREST `held_center.x` —
## the representative that's actually near the current view, matching how a
## real, non-tiling single-window pan already resolves the "which
## circumnavigation" question implicitly (screen_center_to_district()'s own
## RAW, un-wrapped output). `cols <= 0` (no-radius bodies, which never tile
## per compute_tile_grid()'s own doc) is a safe no-op passthrough — there is
## no periodicity to resolve.
static func nearest_wrap_image(canonical_col: int, held_center_col: int, cols: int) -> int:
if cols <= 0:
return canonical_col
var delta: int = posmod(canonical_col - held_center_col + cols / 2, cols) - cols / 2
return held_center_col + delta
## Live round 4 fix (the SECOND half of the "pitch black" repro, beyond
## district_to_canvas_local()'s tile-mosaic fix above): `_view_offset` is a
## PURE screen<->canvas-local transform, entirely independent of
## `held_center`/`held_n` — cursor-anchored zoom (`_zoom_at()`) never
## references them. But the single-window `_draw()` path draws the held
## composite at canvas-local `Rect2(0,0,extent,extent)`, which is ONLY the
## right place on screen if canvas-local (0,0) still equals
## `held_center - held_n/2` for the NEW rung. `_maybe_reselect_rung()`
## updates `held_center`/`held_n` to the new rung's values (a DIFFERENT
## `held_n` — Region's ~thousands vs. District's 64 vs. Quarter's 16) but
## never touched `_view_offset` to compensate — so canvas-local (0,0)
## silently stopped meaning `held_center - held_n/2` the instant `held_n`
## changed, and the composite (still drawn at local (0,0)) landed wherever
## the STALE offset happened to put it — off-canvas by tens or hundreds of
## thousands of px for a Region-to-District/Quarter crossing (round 4's
## repro), same root shape as the tile-mosaic bug, just on the "one held
## window" side of the split instead of the "many tiles" side.
##
## This is the exact INVERSE construction: given the SAME screen point that
## used to map to `old_local` must now map to canvas-local
## `new_held_n/2 * cell_pixel_size` (i.e. new_held_center's own position
## under the NEW window's `[0, new_held_n)` span), solve for the
## `view_offset` that makes `screen_point == new_local * view_zoom +
## view_offset` true. Pan-edge refetch (`_maybe_refloat_window()`) never
## needed this because it never changes `held_n` — only rung crossings do.
static func recompute_offset_for_held_n_change(
screen_point: Vector2, view_zoom: float, new_held_n: int, cell_pixel_size: float
) -> Vector2:
var new_local: Vector2 = Vector2.ONE * (float(new_held_n) * 0.5 * cell_pixel_size)
return screen_point - new_local * view_zoom
# =============================================================================
# T-1145 item 2 (moved here T-1153 for file-length/testability): WASD/
# arrow-key held-pan direction + edge-scroll suppression/direction — pure
# functions of explicit inputs, no Control/scene-tree dependency.
# =============================================================================
## WASD + arrow keys, read via Input.is_key_pressed() on the PHYSICAL keycode
## (not an InputMap action): W/S/A/D on this project's global InputMap are
## already bound to move_north/move_south/move_east/move_west (gameplay
## movement, D-054 mouse-relative facing) — reusing those actions here would
## make holding W simultaneously pan this map AND queue a gameplay move
## command server-side the moment this implant screen closes back to
## gameplay (InputMapper polls Input.is_action_pressed() unconditionally,
## with no implant-occlusion guard — a genuine pre-existing gap outside this
## ticket's scope, not introduced here). Reading the raw physical keycode
## instead of the shared action name means this screen's WASD use is fully
## independent of whatever the gameplay action happens to be bound to — same
## key, two UNRELATED consumers, neither needs to know about the other.
## Arrow keys have no InputMap action bound at all, so they're conflict-free
## either way. Returns a raw (non-normalized) direction — the caller
## normalizes once after adding the edge-scroll contribution, so N+E doesn't
## move faster than N alone. Reads the global `Input` singleton directly (not
## injected) — this is the one function in this file that isn't a pure
## function of its arguments, kept here anyway to sit beside its two siblings
## below rather than splitting the WASD/edge-scroll trio across two files.
static func held_pan_direction() -> Vector2:
var direction := Vector2.ZERO
if Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP):
direction.y -= 1.0
if Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN):
direction.y += 1.0
if Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT):
direction.x -= 1.0
if Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT):
direction.x += 1.0
return direction
## T-1145 item 2: edge-scroll is suppressed (a) while the cursor is over UI
## (`is_over_ui` — the caller's own _is_over_ui() result, passed in rather
## than called from here since "what counts as UI" is viewer-specific) and
## (b) while the application window itself lacks OS focus (`app_has_focus` —
## otherwise a background window with the cursor left resting near its edge
## from a previous session would silently pan while the player is doing
## something else entirely; Godot's NOTIFICATION_APPLICATION_FOCUS_OUT/IN
## make this directly detectable, the caller's own _notification() wires it).
static func is_cursor_edge_scrolling(
app_has_focus: bool,
is_over_ui: bool,
viewport_size: Vector2,
mouse_pos: Vector2,
edge_margin_px: float
) -> bool:
if not app_has_focus:
return false
if is_over_ui:
return false
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
return false
return (
mouse_pos.x >= 0.0
and mouse_pos.y >= 0.0
and mouse_pos.x <= viewport_size.x
and mouse_pos.y <= viewport_size.y
and (
mouse_pos.x < edge_margin_px
or mouse_pos.y < edge_margin_px
or mouse_pos.x > viewport_size.x - edge_margin_px
or mouse_pos.y > viewport_size.y - edge_margin_px
)
)
## Direction toward whichever edge(s) the cursor is near — same shape as
## held_pan_direction() (a raw, un-normalized Vector2 the caller combines and
## normalizes once).
static func edge_scroll_direction(
viewport_size: Vector2, mouse_pos: Vector2, edge_margin_px: float
) -> Vector2:
var direction := Vector2.ZERO
if mouse_pos.x < edge_margin_px:
direction.x -= 1.0
elif mouse_pos.x > viewport_size.x - edge_margin_px:
direction.x += 1.0
if mouse_pos.y < edge_margin_px:
direction.y -= 1.0
elif mouse_pos.y > viewport_size.y - edge_margin_px:
direction.y += 1.0
return direction
# =============================================================================
# T-1153, live round 3 (Jeroen's ruling, design doc §4): the orbital REST
# STATE must TILE — a single wire-capped Region window (MAX_COVERAGE_M["Region"]
# = 13,107,200 m) covers only a fraction of a real body's circumference
# (Lendel: 39,197,023 m — a single window is ~a third of the body). The top
# rest state composes MULTIPLE Region windows ("progressive capped-density
# TILING", design doc §4) into a mosaic under ONE view transform.
# =============================================================================
## Compute the tile-set grid for the orbital rest state: the minimal set of
## Region-granularity window CENTERS (each `TILE_N` districts wide) whose
## union covers the WHOLE body — columns wrap (canonicalize_district_center()'s
## own east-west periodicity), rows clamp at the poles. Returns an Array of
## Vector2i tile centers, ALREADY CANONICALIZED (duplicates from pole-row
## clamping or (degenerately) column-wrap collisions are DEDUPED — a tiny
## body where multiple nominal tile rows clamp to the identical pole-adjacent
## row, or multiple nominal tile columns wrap to the identical column, must
## not request/draw the same tile twice).
##
## Grid layout: `cols_tiles = ceil(cols / TILE_N)` tiles span the full
## circumference (evenly spaced, centered on column 0 — the canonical
## origin); `rows_tiles = ceil(2*rows_half / TILE_N)` tiles span pole to
## pole (centered on row 0). Each tile's PRE-CANONICALIZATION center is
## `(tile_index - (tile_count-1)/2) * TILE_N` along its axis — symmetric
## around the canonical origin, matching enter_orbital()'s own "canonical
## origin = (0,0)" convention (AtlasDescendGeometry's doc) so the tile set's
## own center-of-mass lands exactly on the canonical frame, not offset from
## it.
##
## No-radius bodies (tiny test bodies, `body_radius_km <= 0`) return a
## single tile at (0,0) — matching enter_orbital()'s own no-radius fallback
## disposition (no circumference/tiling concept for a body with no radius).
static func compute_tile_grid(body_radius_km: float) -> Array:
if body_radius_km <= 0.0:
return [Vector2i.ZERO]
var extent: Dictionary = AtlasDescendGeometryRef.district_extent(body_radius_km)
var cols: int = int(extent["cols"])
var rows_half: int = int(extent["rows_half"])
var rows_total: int = rows_half * 2
var cols_tiles: int = maxi(1, ceili(float(cols) / float(TILE_N)))
var rows_tiles: int = maxi(1, ceili(float(rows_total) / float(TILE_N)))
var col_centers: Array = []
for tx in range(cols_tiles):
var raw_col: int = roundi((float(tx) - (float(cols_tiles - 1) * 0.5)) * float(TILE_N))
col_centers.append(raw_col)
var row_centers: Array = []
for ty in range(rows_tiles):
var raw_row: int = roundi((float(ty) - (float(rows_tiles - 1) * 0.5)) * float(TILE_N))
row_centers.append(raw_row)
# Dedup via a Dictionary keyed on the CANONICALIZED (col, row) pair —
# Godot Dictionary keys compare Vector2i by value, so this is a proper
# set. Insertion order is preserved (Godot Dictionaries are
# order-preserving), giving a deterministic tile ORDER too — the same
# grid always requests/draws in the same sequence, useful for progressive
# arrival to read as a stable left-to-right, top-to-bottom fill rather
# than an unpredictable one.
var seen: Dictionary = {}
var tiles: Array = []
for raw_col: int in col_centers:
for raw_row: int in row_centers:
var canonical: Vector2i = AtlasDescendGeometryRef.canonicalize_district_center(
Vector2i(raw_col, raw_row), body_radius_km
)
if not seen.has(canonical):
seen[canonical] = true
tiles.append(canonical)
return tiles
# =============================================================================
# T-1153: screen header chrome (D-169/D-170) — pure string-building, moved
# here from atlas_window_viewer.gd for file-length (the viewer's own
# `_refresh_screen_header()`/`_location_label()` stay as thin wrappers, since
# both are directly tested by name).
# =============================================================================
## Body name + coordinate label — T-1142: shows the body's proper name
## (falling back to body_id) alongside the held district center, so the
## header never reads as bare "district (col, row)" with no indication of
## WHICH body the player is looking at.
static func location_label(body_display_name: String, held_center: Vector2i) -> String:
return "%s — (%d, %d)" % [body_display_name, held_center.x, held_center.y]
## D-169/D-170 implant chrome (§5): {title, subtitle} for the screen header.
## The subtitle's extent (`held_n` districts) is rung-INVARIANT (n is always
## district extent — see AtlasWindowOverlay.cell_grid_side_for_window()'s
## doc), but the km/cell reading reflects the HELD rung's actual spacing
## (2.048 km District, 0.512 km Quarter, 204.8 km Region) — the "continuous
## metres-per-pixel/extent readout" design doc §6 calls for in place of a
## discrete "you are now in Quarter Mode" label (Jeroen's "no mode
## transition" ruling): the number itself communicates the rung.
static func screen_header_content(
body_display_name: String,
held_center: Vector2i,
held_n: int,
held_granularity_v2: String,
district_m: float
) -> Dictionary:
var label: String = location_label(body_display_name, held_center)
var extent_km: float = float(held_n) * district_m / 1000.0
var spacing_km: float = spacing_for_rung(held_granularity_v2) / 1000.0
var subtitle: String = "%.1f x %.1f km · %.3f km/cell" % [extent_km, extent_km, spacing_km]
return {"title": "REGIONAL — %s" % label.to_upper(), "subtitle": subtitle}
@@ -16,6 +16,7 @@ const PANEL_MARGIN: float = 16.0
const LEGEND_PANEL_WIDTH: float = 260.0
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## The morphology base layer folds its 17 zones into ~5 family rows (§5:
## "mirroring T-1112's 'not everything earns permanent screen space'
@@ -55,13 +56,28 @@ func reposition() -> void:
## Always shows the base-layer key (morphology + elevation reading, always
## on) plus glaciation (always-on modifier), then whichever toggle overlay is
## currently active, if any.
##
## The subtitle's km/cell reading is NOT a fixed "district window" label —
## it was until this fix hardcoded District's own 2.048 km/cell, which was a
## 100x lie whenever the viewer actually holds Region (204.8 km/cell) or
## Quarter (0.512 km/cell). Read through AtlasWindowGeometry.
## spacing_for_rung() — the SAME pure lookup _refresh_screen_header() uses
## for the main screen header's subtitle (screen_header_content()) — keyed
## off the viewer's current get_held_granularity_v2(), so the legend and
## header can never disagree. AtlasWindowViewer calls refresh() at every
## point _held_granularity_v2 changes (_enter_tile_mode(), _enter_at_rung(),
## _on_window_ready()'s rung-swap adoption) — see those call sites.
func refresh() -> void:
if _viewer == null:
return
clear()
visible = true
add_component(ImplantHeader.new("REGIONAL LEGEND", "district window · 2.048 km/cell"))
var spacing_km: float = AtlasWindowGeometry.spacing_for_rung(_viewer.get_held_granularity_v2()) / 1000.0
var subtitle: String = "%s window · %.3f km/cell" % [
_viewer.get_held_granularity_v2().to_lower(), spacing_km
]
add_component(ImplantHeader.new("REGIONAL LEGEND", subtitle))
add_component(ImplantSeparator.new())
_add_morphology_section()
@@ -46,9 +46,25 @@ extends Node2D
## window/overlay state, so the common case (panning within an already-held
## window) is zero rebuild cost — draw_texture_rect() on an already-built
## ImageTexture, same as any other texture draw.
##
## T-1152/T-1153: `n` (the response's echoed district extent) and the DERIVED
## cell-grid side length are now DIFFERENT quantities at every rung except
## District — cell_grid_side_for_window() computes the latter from `n` and
## the response's own `granularity_v2` echo (mirroring the server's
## `WindowGranularity::cell_grid_side` exactly), so a Region-rung response (a
## FAR SPARSER cell grid than its district extent — see that Rust doc's
## "inversion" note) renders through the exact same colorizer pipeline as
## District/Quarter, satisfying the design doc §6 "one colorizer family, no
## per-rung palettes" encoding-continuity requirement — no branch in
## _cell_color()/_temp_cell_color()/etc. below needed any change at all.
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
const REGION_TEMP_NONE_DC: int = AtlasOverlayColors.REGION_TEMP_NONE_DC
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
# T-1153, live round 3: TILE_N (the per-tile district extent) for the mosaic draw path.
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
# Live round 5: `cols` (circumference in districts) for the mosaic's wrap-image draw fix.
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## T-1145 item 3: interim presentation toggle — true renders the smoothed
## Image/ImageTexture composite; false keeps the original crisp per-cell
@@ -78,10 +94,30 @@ var _cached_texture: ImageTexture = null
var _cache_window_ref: Variant = null
var _cache_active_toggle: String = ""
## Live round 4 fix: per-TILE texture cache, keyed by tile index — mirrors
## the single-window cache above, but one slot per mosaic tile (a Dictionary
## of `{window_ref, active_toggle, texture}`, since the mosaic doesn't have a
## single fixed set of tiles the way the single-window path has a single
## fixed field). Building a brand-new, UNSTORED `ImageTexture` every
## `_draw()` call (the round-3 version) left it referenced only by a local
## variable — nothing keeps the RID alive past the function returning, which
## raced against the RenderingServer's deferred draw-command flush and
## rendered as a blank/white tile (the round-4 "pitch black"/white-mosaic
## repro's second half, beyond the coordinate fix above): the CPU-side pixel
## data was provably correct (sampled directly), but the GPU-side texture
## backing it could be gone by composite time. Caching each tile's texture
## as a class-owned Dictionary entry (same reference-identity rebuild-only-
## on-change discipline as `_cached_texture`) keeps it alive exactly as long
## as the single-window composite's own texture already is.
var _tile_texture_cache: Dictionary = {}
func _draw() -> void:
if viewer == null:
return
if viewer.is_tile_mode():
_draw_tile_mosaic()
return
var window: Variant = viewer.get_district_window()
if not window is Dictionary:
return
@@ -94,25 +130,245 @@ func _draw() -> void:
if not (morphology is PackedByteArray or morphology is Array):
return
# T-1152/T-1153: `n` (the response's echoed field) is ALWAYS window extent
# in DISTRICTS at every rung (Dudley's wire contract, DistrictWindowLayer.n's
# own doc) — the per-cell arrays (morphology/elev_q/etc.) are sized by the
# DERIVED cell-grid side, `cell_grid_side_for_window()` below, which equals
# `n` only at District granularity. Quarter packs MORE cells into the same
# n-district extent (`n*4`); Region packs FEWER, since one region cell
# spans 100 districts (`round(n/100).max(1)`). The on-screen EXTENT stays
# `n * cell_px` regardless of rung (CELL_PIXEL_SIZE is defined as "one
# DISTRICT at zoom=1.0" — see the viewer's own doc on that constant) so a
# rung swap at a fixed pan/zoom never jumps the composite's screen footprint
# (§6 "no layout jump") — only the TEXTURE RESOLUTION packed into that
# footprint changes, exactly the "same colorizer family, different LoD"
# picture the design doc describes.
var grid_side: int = cell_grid_side_for_window(w)
if grid_side <= 0:
return
var cell_px: float = viewer.get_cell_pixel_size()
var active_toggle: String = _active_toggle_overlay()
if COMPOSITE_SMOOTH:
_draw_smoothed_composite(w, n, cell_px, active_toggle)
_draw_smoothed_composite(w, n, grid_side, cell_px, active_toggle)
else:
_draw_crisp_composite(w, n, cell_px, active_toggle)
_draw_crisp_composite(w, grid_side, n, cell_px, active_toggle)
## T-1145 item 3: the smoothed path — build/reuse an n x n ImageTexture (one
## pixel per district) and draw it scaled to (n*cell_px) with LINEAR
## filtering. texture_filter is set on `self` (a CanvasItem property) once
## per draw — cheap (a property write, not a texture rebuild) and correct
## even the first time this runs (Godot's engine default already IS linear,
## but this makes the choice explicit rather than relying on an implicit
## project-wide default that could change).
func _draw_smoothed_composite(w: Dictionary, n: int, cell_px: float, active_toggle: String) -> void:
## T-1153, live round 3/4 (Jeroen's ruling, design doc §4): the orbital
## rest-state MOSAIC draw path — one call to the EXISTING single-tile
## composite-building logic (`_rebuild_texture_if_needed()`/
## `_draw_smoothed_composite()`'s own per-tile equivalent below) PER TILE,
## each positioned at its own LOCAL offset in the SAME canvas-local
## coordinate frame the single-window path (and fit_window_view()/
## screen_center_to_district()) already use.
##
## **Live round 4 fix:** the round-3 version placed tiles relative to
## absolute district (0,0) directly (`(tile.center - TILE_N/2) * cell_px`),
## which does NOT match `_fit_and_center()`'s own convention — canvas-local
## (0,0) is `held_center - held_n/2` (AtlasWindowGeometry.
## district_to_canvas_local()'s own doc), and in tile mode `held_n` is the
## WHOLE BODY's extent, not TILE_N. That mismatch pushed the entire mosaic
## off-canvas (round 4's "pitch black" repro) — silent, since nothing
## errors, it just draws somewhere the viewport never shows. Fixed by
## routing every tile's placement through `district_to_canvas_local()` with
## the VIEWER's own `held_center`/`held_n`, the exact reference frame every
## other canvas-local consumer (fit/pan/reselect) already agrees on. Tiles
## that haven't arrived yet (`tile["window"] == null`) are simply SKIPPED —
## no per-tile placeholder draw, letting COLOR_BG show through as the honest
## "nothing here yet" read (the viewer's own `_draw()` already documents why
## no separate whole-viewport fade is needed on top of this).
##
## **Live round 5 fix:** `tile["center"]` is CANONICAL (wrapped into
## `[0, cols)` by `compute_tile_grid()` — correct for REQUESTS/cache keys,
## since longitude is periodic and a canonical column is the single-valued
## key both sides of the wire agree on). But `district_to_canvas_local()`
## is a pure LINEAR function with no wrap concept — handed a canonical
## column directly, it places the tile at exactly ONE of its infinitely
## many equivalent on-screen positions (`col + k*cols`), which is only the
## visually-correct one by coincidence. Lendel's own repro: the tile whose
## true position is immediately WEST of the canonical origin canonicalizes
## to column 12739 (`-6400 mod 19139`) — drawn there directly, it lands
## off-canvas RIGHT, leaving the mosaic's actual LEFT third black. Fixed by
## re-expressing each tile's column via `nearest_wrap_image()` — whichever
## wrap-image is closest to `held_center`, i.e. the one actually near the
## current view — BEFORE handing it to `district_to_canvas_local()`.
func _draw_tile_mosaic() -> void:
var tile_set = viewer.get_tile_set()
if tile_set == null:
return
var cell_px: float = viewer.get_cell_pixel_size()
var active_toggle: String = _active_toggle_overlay()
var held_center: Vector2i = viewer.get_held_center()
var held_n: int = viewer.get_held_n()
var half_tile: float = float(AtlasWindowGeometryRef.TILE_N) * 0.5
var tiles: Array = tile_set.get_tiles()
var cols: int = int(
AtlasDescendGeometryRef.district_extent(viewer.get_body_radius_km()).get("cols", 0)
)
for i in range(tiles.size()):
var tile: Dictionary = tiles[i]
var window: Variant = tile["window"]
if not window is Dictionary:
continue
var w: Dictionary = window
var morphology: Variant = w.get("morphology")
if not (morphology is PackedByteArray or morphology is Array):
continue
var grid_side: int = cell_grid_side_for_window(w)
if grid_side <= 0:
continue
var center: Vector2i = tile["center"]
var draw_col: int = AtlasWindowGeometryRef.nearest_wrap_image(center.x, held_center.x, cols)
var tile_top_left: Vector2 = Vector2(
float(draw_col) - half_tile, float(center.y) - half_tile
)
var local_origin: Vector2 = AtlasWindowGeometryRef.district_to_canvas_local(
tile_top_left, held_center, held_n, cell_px
)
var extent: float = float(AtlasWindowGeometryRef.TILE_N) * cell_px
_draw_one_tile(i, w, grid_side, local_origin, extent, active_toggle)
## One tile's own composite — the SAME crisp/smoothed per-cell pipeline the
## single-window path uses (_cell_color()/_apply_glaciation(), UNCHANGED),
## just drawn at `local_origin` instead of always at (0,0). Each tile gets
## its OWN texture-rebuild cache slot in `_tile_texture_cache`, keyed by
## `tile_index` — sharing ONE `_cached_texture` slot across all tiles (the
## single-window field) would thrash on every draw call as different tiles'
## windows compete for it.
func _draw_one_tile(
tile_index: int,
w: Dictionary,
grid_side: int,
local_origin: Vector2,
extent: float,
active_toggle: String
) -> void:
if not COMPOSITE_SMOOTH:
_draw_crisp_tile(w, grid_side, local_origin, extent, active_toggle)
return
var tile_texture: ImageTexture = _rebuild_tile_texture_if_needed(
tile_index, w, grid_side, active_toggle
)
if tile_texture == null:
return
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
_rebuild_texture_if_needed(w, n, active_toggle)
draw_texture_rect(tile_texture, Rect2(local_origin, Vector2(extent, extent)), false)
## Live round 4 fix: rebuilds (and, critically, KEEPS — see
## `_tile_texture_cache`'s own doc for why an unstored local `ImageTexture`
## silently rendered blank/white) `_tile_texture_cache[tile_index]`'s texture
## ONLY when that tile's window object or the active toggle overlay has
## changed since the last build — the SAME reference-identity discipline
## `_rebuild_texture_if_needed()` uses for the single-window composite, one
## cache entry per tile index instead of one shared field.
func _rebuild_tile_texture_if_needed(
tile_index: int, w: Dictionary, grid_side: int, active_toggle: String
) -> ImageTexture:
var entry: Dictionary = _tile_texture_cache.get(tile_index, {})
if (
is_same(entry.get("window_ref"), w)
and entry.get("active_toggle") == active_toggle
and entry.get("texture") != null
):
return entry["texture"]
var texture: ImageTexture = _build_tile_texture(w, grid_side, active_toggle)
_tile_texture_cache[tile_index] = {
"window_ref": w, "active_toggle": active_toggle, "texture": texture
}
return texture
## Builds a tile's own Image/ImageTexture from its per-cell colors —
## identical pipeline to `_rebuild_texture_if_needed()`, just returning the
## texture directly instead of writing to the single-window cache fields
## (the CALLER, `_rebuild_tile_texture_if_needed()`, owns persisting it).
func _build_tile_texture(w: Dictionary, grid_side: int, active_toggle: String) -> ImageTexture:
var elev_q: Variant = w.get("elev_q")
var glaciation: Variant = w.get("glaciation")
var morphology: Variant = w.get("morphology")
var n_cells: int = morphology.size()
var img := Image.create(grid_side, grid_side, false, Image.FORMAT_RGBA8)
for row in range(grid_side):
for col in range(grid_side):
var i: int = row * grid_side + col
if i >= n_cells:
img.set_pixel(col, row, Color.TRANSPARENT)
continue
var cell_color: Color = _cell_color(w, i, int(morphology[i]), elev_q, active_toggle)
cell_color = _apply_glaciation(cell_color, glaciation, i)
img.set_pixel(col, row, cell_color)
return ImageTexture.create_from_image(img)
## The crisp (non-smoothed) per-tile path — mirrors `_draw_crisp_composite()`
## exactly, just positioned at `local_origin` instead of always at (0,0).
func _draw_crisp_tile(
w: Dictionary, grid_side: int, local_origin: Vector2, extent: float, active_toggle: String
) -> void:
var elev_q: Variant = w.get("elev_q")
var glaciation: Variant = w.get("glaciation")
var morphology: Variant = w.get("morphology")
var n_cells: int = morphology.size()
var screen_cell_px: float = extent / float(grid_side)
for row in range(grid_side):
for col in range(grid_side):
var i: int = row * grid_side + col
if i >= n_cells:
continue
var cell_color: Color = _cell_color(w, i, int(morphology[i]), elev_q, active_toggle)
if cell_color.a <= 0.0:
continue
cell_color = _apply_glaciation(cell_color, glaciation, i)
var cell_origin: Vector2 = local_origin + Vector2(col * screen_cell_px, row * screen_cell_px)
draw_rect(
Rect2(cell_origin, Vector2(screen_cell_px + 0.5, screen_cell_px + 0.5)), cell_color
)
## The derived cell-grid side length (in CELLS) for a window dict `w` —
## mirrors server/src/atlas/layer_proxy.rs's `WindowGranularity::cell_grid_side`
## exactly, reading `w`'s OWN echoed `n`/`granularity_v2` fields rather than
## trusting a caller's separately-tracked rung (the response is the source of
## truth for what it actually contains). Falls back to `n` unchanged
## (District's own identity mapping) for an old-shape response with no
## `granularity_v2` key — matches the server's own "unknown -> District"
## posture and AtlasWindowRequest.on_response()'s own default-to-District
## disposition for the same field.
static func cell_grid_side_for_window(w: Dictionary) -> int:
var n: int = int(w.get("n", 0))
var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT))
match granularity_v2:
AtlasWindowRequest.GRANULARITY_V2_QUARTER:
return n * 4 # WINDOW_GRANULARITY_QUARTER multiplier — D-243 QUARTER_M
AtlasWindowRequest.GRANULARITY_V2_REGION:
return maxi(roundi(float(n) / 100.0), 1) # D-243 DISTRICTS_PER_REGION
_:
return n # District — 1:1
## T-1145 item 3: the smoothed path — build/reuse a `grid_side` x `grid_side`
## ImageTexture (one pixel per DERIVED CELL, T-1152 — not per district, see
## cell_grid_side_for_window()'s doc) and draw it scaled to (n*cell_px), n
## being the window's DISTRICT extent, with LINEAR filtering. texture_filter
## is set on `self` (a CanvasItem property) once per draw — cheap (a property
## write, not a texture rebuild) and correct even the first time this runs
## (Godot's engine default already IS linear, but this makes the choice
## explicit rather than relying on an implicit project-wide default that
## could change).
func _draw_smoothed_composite(
w: Dictionary, n: int, grid_side: int, cell_px: float, active_toggle: String
) -> void:
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
_rebuild_texture_if_needed(w, grid_side, active_toggle)
if _cached_texture == null:
return
var extent: float = float(n) * cell_px
@@ -125,8 +381,9 @@ func _draw_smoothed_composite(w: Dictionary, n: int, cell_px: float, active_togg
## read directly from `w` here (rather than threaded through as params, the
## way the crisp path's _cell_color()/_apply_glaciation() calls already
## receive them) since this function owns the whole per-cell loop, not just
## one cell.
func _rebuild_texture_if_needed(w: Dictionary, n: int, active_toggle: String) -> void:
## one cell. `grid_side` (T-1152) is the DERIVED cell-grid side (see
## cell_grid_side_for_window()), not the window's district extent `n`.
func _rebuild_texture_if_needed(w: Dictionary, grid_side: int, active_toggle: String) -> void:
if (
is_same(_cache_window_ref, w)
and _cache_active_toggle == active_toggle
@@ -139,10 +396,10 @@ func _rebuild_texture_if_needed(w: Dictionary, n: int, active_toggle: String) ->
var morphology: Variant = w.get("morphology")
var n_cells: int = morphology.size()
var img := Image.create(n, n, false, Image.FORMAT_RGBA8)
for row in range(n):
for col in range(n):
var i: int = row * n + col
var img := Image.create(grid_side, grid_side, false, Image.FORMAT_RGBA8)
for row in range(grid_side):
for col in range(grid_side):
var i: int = row * grid_side + col
if i >= n_cells:
img.set_pixel(col, row, Color.TRANSPARENT)
continue
@@ -157,16 +414,22 @@ func _rebuild_texture_if_needed(w: Dictionary, n: int, active_toggle: String) ->
## The ORIGINAL crisp per-cell path — kept byte-for-byte behind
## COMPOSITE_SMOOTH := false so T-1143's design pass can compare both
## renderings directly (see the class doc).
func _draw_crisp_composite(w: Dictionary, n: int, cell_px: float, active_toggle: String) -> void:
## renderings directly (see the class doc). `grid_side` (T-1152) is the
## DERIVED cell-grid side (see cell_grid_side_for_window()); `n` (the
## window's district extent) sizes the on-screen cell pitch so the total
## drawn footprint stays `n * cell_px` regardless of rung.
func _draw_crisp_composite(
w: Dictionary, grid_side: int, n: int, cell_px: float, active_toggle: String
) -> void:
var elev_q: Variant = w.get("elev_q")
var glaciation: Variant = w.get("glaciation")
var morphology: Variant = w.get("morphology")
var n_cells: int = morphology.size()
var screen_cell_px: float = float(n) * cell_px / float(grid_side)
for row in range(n):
for col in range(n):
var i: int = row * n + col
for row in range(grid_side):
for col in range(grid_side):
var i: int = row * grid_side + col
if i >= n_cells:
continue
var cell_color: Color = _cell_color(w, i, int(morphology[i]), elev_q, active_toggle)
@@ -175,7 +438,10 @@ func _draw_crisp_composite(w: Dictionary, n: int, cell_px: float, active_toggle:
cell_color = _apply_glaciation(cell_color, glaciation, i)
# +0.5 overdraw avoids hairline seams between adjacent cells —
# same idiom as _draw_gen_district/_draw_gen_region_grid.
draw_rect(Rect2(col * cell_px, row * cell_px, cell_px + 0.5, cell_px + 0.5), cell_color)
draw_rect(
Rect2(col * screen_cell_px, row * screen_cell_px, screen_cell_px + 0.5, screen_cell_px + 0.5),
cell_color
)
## Which of the three mutually-exclusive toggle overlays (if any) is active.
@@ -41,13 +41,23 @@ const DEBOUNCE_DELAY: float = 0.15 # 150ms, §4/§5
const RETRY_DELAY: float = 0.5 # matches atlas_generation_proxy.gd's GEN_RETRY_DELAY
const MAX_RETRIES: int = 20 # ~10s ceiling, matches atlas_generation_proxy.gd's GEN_MAX_RETRIES
## T-1150 struct/key plumbing: this viewer only ever REQUESTS district
## granularity today (requesting quarter is T-1153's job) — these constants
## exist so the cache key / staleness guard below are granularity-aware from
## day one, not bolted on later.
## T-1150 struct/key plumbing: legacy int granularity — district is the
## default for every caller that doesn't request quarter/Region explicitly.
const DEFAULT_GRANULARITY: int = AtlasWindowCache.DISTRICT_GRANULARITY
const DEFAULT_MIN_WL_M: int = 0
## T-1152/T-1153: the R5-redesigned string-tag granularity — "Quarter" |
## "District" | "Region". This is the axis request_now()/request_debounced()'s
## `granularity_v2` parameter actually varies; the legacy int
## (DEFAULT_GRANULARITY) stays pinned at district for every call this object
## makes, since v2 always wins server-side once present
## (resolve_window_granularity_v2()'s documented precedence) and the legacy
## int cannot express Region at all.
const GRANULARITY_V2_QUARTER: String = AtlasWindowCache.GRANULARITY_V2_QUARTER
const GRANULARITY_V2_DISTRICT: String = AtlasWindowCache.GRANULARITY_V2_DISTRICT
const GRANULARITY_V2_REGION: String = AtlasWindowCache.GRANULARITY_V2_REGION
const DEFAULT_GRANULARITY_V2: String = AtlasWindowCache.DEFAULT_GRANULARITY_V2
## Mirrors server/src/atlas/layer_proxy.rs's DISTRICT_WINDOW_MAX_N /
## WIRE_CAP_CELLS exactly (PR #191 review, Tyre C1). `_clamp_window_n_mirror()`
## below reproduces `clamp_window_n()` bit-for-bit — the load-bearing-mirror
@@ -57,12 +67,24 @@ const DEFAULT_MIN_WL_M: int = 0
const SERVER_DISTRICT_WINDOW_MAX_N: int = 64
const SERVER_WIRE_CAP_CELLS: int = 4_096
## T-1152/T-1153: mirrors server/src/atlas/layer_proxy.rs's
## `DISTRICT_WINDOW_MAX_N_REGION` — the Region-only per-axis ceiling on `n`
## (still window extent in DISTRICTS, per WindowGranularity::cell_grid_side's
## doc: `sqrt(WIRE_CAP_CELLS) * DISTRICTS_PER_REGION = 64 * 100`). Keep in
## sync with the server constant of the same name.
const SERVER_DISTRICT_WINDOW_MAX_N_REGION: int = 6_400
## Mirrors server/src/atlas/scale.rs's DISTRICTS_PER_REGION (D-243: one region
## = 100 districts/side) — the divisor `_cell_grid_side_region_mirror()` needs
## to reproduce `WindowGranularity::cell_grid_side`'s Region branch.
const SERVER_DISTRICTS_PER_REGION: int = 100
var _owner = null # AtlasWindowViewer (untyped to avoid cyclic ref)
var _cache = null # AtlasWindowCache
var _body_id: String = ""
var _center: Vector2i = Vector2i.ZERO
var _n: int = DISTRICT_WINDOW_DEFAULT_N
var _granularity: int = DEFAULT_GRANULARITY
var _granularity_v2: String = DEFAULT_GRANULARITY_V2
var _min_wl_m: int = DEFAULT_MIN_WL_M
var _pending: bool = false
var _retries: int = 0
@@ -127,23 +149,76 @@ static func _clamp_window_n_mirror(raw_n: int, granularity: int) -> int:
return mini(n, maxi(cap_n, 1))
## [`WindowGranularity`]-aware twin of `_clamp_window_n_mirror()` (T-1152/
## T-1153) — mirrors server/src/atlas/layer_proxy.rs's `clamp_window_n_v2`
## EXACTLY, including its Region branch, per the ticket's explicit
## instruction ("replicate the loop exactly, there is NO closed form"). For
## District/Quarter this delegates straight to `_clamp_window_n_mirror()`
## (byte-identical clamped `n`, matching the server's own
## `clamp_window_n_v2_matches_legacy_for_finer_than_district_rungs`
## guarantee). For Region: per-axis clamp to
## `SERVER_DISTRICT_WINDOW_MAX_N_REGION` (6,400), then halve `n` in a bounded
## loop while `_cell_grid_side_region_mirror(n)^2 > SERVER_WIRE_CAP_CELLS` and
## `n > 1` — there is no closed-form inverse of the rounding division
## `cell_grid_side` uses at Region granularity, so this loop is the correct
## (and only) mirror, not an approximation of one.
static func _clamp_window_n_mirror_v2(raw_n: int, granularity_v2: String) -> int:
if granularity_v2 != AtlasWindowCache.GRANULARITY_V2_REGION:
var legacy_granularity: int = (
DEFAULT_GRANULARITY
if granularity_v2 == AtlasWindowCache.GRANULARITY_V2_DISTRICT
else AtlasWindowCache.DISTRICT_GRANULARITY * 4 # "Quarter" — WINDOW_GRANULARITY_QUARTER
)
return _clamp_window_n_mirror(raw_n, legacy_granularity)
var n: int = clampi(raw_n, 1, SERVER_DISTRICT_WINDOW_MAX_N_REGION)
while (
_cell_grid_side_region_mirror(n) * _cell_grid_side_region_mirror(n) > SERVER_WIRE_CAP_CELLS
and n > 1
):
n = int(n / 2.0)
return maxi(n, 1)
## Mirrors `WindowGranularity::cell_grid_side`'s Region branch EXACTLY:
## `round(n / DISTRICTS_PER_REGION).max(1)` — the derived region-cell-grid
## side length (in CELLS) for a window whose extent is `n` DISTRICTS. Rust's
## `f64::round()` is round-half-away-from-zero; GDScript's `roundi()` matches
## that for non-negative inputs (the only domain `n` — always >= 1 here —
## can produce), so this is a faithful mirror, not an approximation.
static func _cell_grid_side_region_mirror(n: int) -> int:
var side: int = roundi(float(n) / float(SERVER_DISTRICTS_PER_REGION))
return maxi(side, 1)
## Entry point + pan re-request: request the window centered on `center`
## (a DistrictPos-equivalent Vector2i) for `body_id`. Cache hit -> immediate
## synchronous window_ready emit, no network traffic at all. Cache miss ->
## fire the request now (the caller — either the initial entry or a
## debounce-fired pan — has already decided this call SHOULD fire; the 150ms
## debounce itself lives in request_debounced() below, not here, so this
## function is also the one entry-mechanic click-through uses directly with
## no debounce at all, matching §5's "first window" contract).
func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEFAULT_N) -> void:
## (a DistrictPos-equivalent Vector2i) for `body_id`, at `granularity_v2`
## ("Quarter" | "District" | "Region", T-1152/T-1153 — District is the
## default for every caller that doesn't ask for a different rung explicitly,
## matching the legacy behavior byte-for-byte when omitted). Cache hit ->
## immediate synchronous window_ready emit, no network traffic at all. Cache
## miss -> fire the request now (the caller — either the initial entry or a
## debounce-fired pan/zoom — has already decided this call SHOULD fire; the
## 150ms debounce itself lives in request_debounced() below, not here, so
## this function is also the one entry-mechanic click-through uses directly
## with no debounce at all, matching §5's "first window" contract).
func request_now(
body_id: String,
center: Vector2i,
n: int = DISTRICT_WINDOW_DEFAULT_N,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> void:
_body_id = body_id
_center = center
_granularity = DEFAULT_GRANULARITY
_granularity = DEFAULT_GRANULARITY # legacy int stays pinned at district — v2 always wins server-side
_granularity_v2 = granularity_v2
_min_wl_m = DEFAULT_MIN_WL_M
_n = _clamp_window_n_mirror(n, _granularity) # Tyre C1 — mirror BEFORE storing/requesting
_n = _clamp_window_n_mirror_v2(n, _granularity_v2) # Tyre C1, extended T-1152 — mirror BEFORE storing
_debounce_timer.stop() # a direct request supersedes any pending debounced one
var cached: Variant = _cache.get_window(body_id, center, _n, _granularity, _min_wl_m)
var cached: Variant = _cache.get_window(
body_id, center, _n, _granularity, _min_wl_m, _granularity_v2
)
if cached != null:
_pending = false
_retries = 0
@@ -152,7 +227,9 @@ func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEF
_pending = true
_retries = 0
SimBridge.request_atlas_layers(body_id, "Topography", center, _n, _granularity, _min_wl_m)
SimBridge.request_atlas_layers(
body_id, "Topography", center, _n, _granularity, _min_wl_m, _granularity_v2
)
## Pan-triggered re-request (§4/§5: "150ms after the last drag-release, not
@@ -160,27 +237,57 @@ func request_now(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEF
## candidate; only the LAST call within the debounce window actually fires
## (Timer.start() on an already-running one-shot timer restarts it — Godot's
## documented behavior — so a flick-and-resettle collapses to one request).
func request_debounced(body_id: String, center: Vector2i, n: int = DISTRICT_WINDOW_DEFAULT_N) -> void:
func request_debounced(
body_id: String,
center: Vector2i,
n: int = DISTRICT_WINDOW_DEFAULT_N,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> void:
_body_id = body_id
_center = center
_granularity = DEFAULT_GRANULARITY
_granularity_v2 = granularity_v2
_min_wl_m = DEFAULT_MIN_WL_M
_n = _clamp_window_n_mirror(n, _granularity) # Tyre C1 — mirror BEFORE storing/requesting
_n = _clamp_window_n_mirror_v2(n, _granularity_v2) # Tyre C1, extended T-1152 — mirror BEFORE storing
_debounce_timer.start()
func _on_debounce_timeout() -> void:
request_now(_body_id, _center, _n)
request_now(_body_id, _center, _n, _granularity_v2)
## Handle an AtlasLayerResponse (routed by the owning viewer from its own
## SimBridge.atlas_layers_received subscription — this object has no signal
## connection of its own, matching atlas_generation_proxy.gd's on_response()
## shape). Ignores responses for a stale body/center/n/granularity/min_wl_m
## (the player panned or navigated away while a request was in flight, or a
## different rung's derive answers a request for a different rung, T-1150) —
## the echoed fields ARE the staleness guard (§2, extended T-1150), compared
## here against what THIS object most recently asked for.
## shape). Ignores responses for a stale body/center/n/min_wl_m/granularity
## (legacy OR v2, see below) — the player panned, zoomed across a rung
## boundary, or navigated away while a request was in flight, or a different
## rung's derive answers a request for a different rung, T-1150/T-1152 — the
## echoed fields ARE the staleness guard (§2, extended T-1150/T-1152),
## compared here against what THIS object most recently asked for.
##
## **Live-round finding (the second C1-shaped bug): v2 is AUTHORITATIVE over
## the legacy field whenever v2 is present — the legacy comparison is
## SKIPPED entirely, not run alongside it.** A T-1152-aware server (this
## codebase's) ALWAYS populates `granularity_v2` on the wire (Dudley's
## contract, `DistrictWindowLayer.granularity_v2`'s own doc: "Always
## populated (never `None`)"), and for `Region` responses specifically the
## LEGACY `granularity` slot carries `WINDOW_GRANULARITY_REGION_KEY`
## (`u32::MAX` = 4294967295) — a reserved KEY-SPACE TAG, not a real
## multiplier, that can never equal this object's own stored `_granularity`
## (which stays pinned at `DEFAULT_GRANULARITY`=1 for every rung this object
## requests, per that field's own doc — the legacy slot has no concept of
## Region at all). Comparing the legacy field UNCONDITIONALLY alongside v2
## therefore drops EVERY Region response as stale forever, even though the
## v2 comparison alone would have correctly accepted it — exactly the live
## bug (`_held_n` fixed; this is the same "old comparison still active
## alongside the new one" class of bug, one layer up in the staleness
## checks). Fix: branch on whether `granularity_v2` is actually PRESENT in
## the response dict (`w.has(...)`, not `w.get(..., default)` — the
## presence/absence distinction is the whole point here) — present (every
## real server, always) -> v2 is the ONLY granularity comparison; absent (a
## hypothetically old, pre-T-1152 server) -> fall back to the legacy
## comparison alone, matching this object's own pre-T-1152 behavior exactly.
func on_response(response: Dictionary) -> void:
if str(response.get("body_id", "")) != _body_id:
return
@@ -205,29 +312,44 @@ func on_response(response: Dictionary) -> void:
var w: Dictionary = window
var echoed_center := _vec_from_center(w.get("center", [0, 0]))
var echoed_n := int(w.get("n", 0))
var echoed_granularity := int(w.get("granularity", AtlasWindowCache.DISTRICT_GRANULARITY))
var echoed_min_wl_m := int(w.get("min_wl_m", 0))
var granularity_matches: bool = _echoed_granularity_matches(w)
if (
echoed_center != _center
or echoed_n != _n
or echoed_granularity != _granularity
or echoed_min_wl_m != _min_wl_m
or not granularity_matches
):
return # stale — answers a window we've since panned away from, or a different rung (§2/T-1150)
return # stale — answers a window we've since panned/zoomed away from, or a different rung
_pending = false
_retries = 0
_cache.put(_body_id, _center, _n, w, _granularity, _min_wl_m)
_cache.put(_body_id, _center, _n, w, _granularity, _min_wl_m, _granularity_v2)
window_ready.emit(w)
## The granularity half of on_response()'s staleness check, split out for the
## v2-authoritative-when-present precedence rule (see on_response()'s own
## doc for the full live-round rationale). Presence, not value, is the
## branch: `w.has("granularity_v2")` — a real server ALWAYS sets this key
## (even if its value happened to coincidentally equal a default), so
## checking presence rather than "is it the default value" is the only
## correct way to distinguish "an old server that never heard of this field"
## from "a new server whose value happens to match."
func _echoed_granularity_matches(w: Dictionary) -> bool:
if w.has("granularity_v2"):
return str(w.get("granularity_v2")) == _granularity_v2
var echoed_granularity := int(w.get("granularity", AtlasWindowCache.DISTRICT_GRANULARITY))
return echoed_granularity == _granularity
func _schedule_retry() -> void:
var timer := get_tree().create_timer(RETRY_DELAY)
timer.timeout.connect(
func() -> void:
if _pending:
SimBridge.request_atlas_layers(
_body_id, "Topography", _center, _n, _granularity, _min_wl_m
_body_id, "Topography", _center, _n, _granularity, _min_wl_m, _granularity_v2
)
)
@@ -236,6 +358,21 @@ func is_pending() -> bool:
return _pending
## The v2 granularity ("Quarter" | "District" | "Region") this object most
## recently asked for — the viewer reads this to know which rung the HELD
## window (once it arrives) actually is, without threading a second copy of
## the state through window_ready's payload.
func get_granularity_v2() -> String:
return _granularity_v2
## Current window extent in districts, as CLAMPED — the viewer's rung-
## selection math needs this to compute the held composite's real-world
## extent regardless of which rung last resolved it.
func get_n() -> int:
return _n
func get_cache() -> Variant:
return _cache
@@ -0,0 +1,166 @@
extends Node
## Orbital rest-state TILE-SET orchestration (T-1153, live round 3 — Jeroen's
## ruling, design doc §4: "the top rest state is the WHOLE body, served as
## progressive capped-density TILING"). A single wire-capped Region window
## (AtlasWindowGeometry.MAX_COVERAGE_M["Region"] = 13,107,200 m) covers only a
## fraction of a real body's circumference (Lendel: ~39,197,023 m — a single
## window is ~a third of the body, the exact live-round finding: shot 01's
## own header read "13107.2 x 13107.2 km" against a 39,198 km circumference).
##
## Owns N independent `AtlasWindowRequest` child instances — one per tile —
## reusing 100% of the EXISTING, already-tested single-window request/cache/
## debounce/retry machinery (atlas_window_request.gd) rather than
## reinventing multi-window orchestration from scratch. Each tile is just a
## Region-granularity window request at its own canonicalized center
## (AtlasWindowGeometry.compute_tile_grid()); distinct centers are already
## distinct cache/coalescing keys (T-1150/T-1152's own aliasing discipline),
## so nothing about the request/cache LAYER needed to change for tiling to
## work — only the ORCHESTRATION (issue N requests instead of one) and the
## DRAWING (a mosaic instead of one composite) are new.
##
## No `class_name` on purpose, matching every other viewer-owned helper in
## this cluster (atlas_window_request.gd/atlas_overlay_bar.gd/
## atlas_legend_panel.gd, review #8 precedent): the owner (AtlasWindowViewer)
## passes itself to `_init()`.
##
## Progressive arrival (design doc §4's own "with visible refinement as
## tiles complete"): each tile's `AtlasWindowRequest.window_ready` connects
## independently — a tile's own `_tiles[i]["window"]` updates the moment
## THAT tile's response lands, with no dependency on any other tile's
## arrival. The viewer/overlay reads `get_tiles()` every draw and renders
## whichever tiles have arrived so far — an empty/border-fade gap for the
## rest, exactly the same "hold what's there, sharpen in place" contract
## single-window progressive refinement already has (§6 "no mode flip"),
## just per-tile instead of per-composite.
signal tile_ready(index: int) # a single tile's window arrived/updated — the viewer redraws
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
var _owner = null # AtlasWindowViewer (untyped to avoid cyclic ref)
var _body_id: String = ""
var _tile_n: int = AtlasWindowGeometry.TILE_N
## Array[Dictionary]: {"center": Vector2i, "request": AtlasWindowRequest,
## "window": Variant (null until arrived)} — one entry per tile, in the SAME
## deterministic order compute_tile_grid() produces (stable fill order, see
## that function's own doc).
var _tiles: Array = []
func _init(owner_ref = null) -> void:
_owner = owner_ref
## Unlike an individual AtlasWindowRequest (which has no signal connection of
## its own — the OWNING viewer forwards responses to it, per that class'
## own doc), the tile set DOES connect directly to
## SimBridge.atlas_layers_received itself and fans a single response out to
## EVERY tile's own `on_response()` — each tile's OWN staleness guard
## (center/n/granularity_v2) decides whether that particular response is
## the one IT was waiting for; only the matching tile ever adopts it. This
## is the same "one shared inbound signal, N independent consumers filtering
## by their own criteria" shape the design already uses elsewhere (every
## AtlasWindowRequest instance filters on its own state from a common
## broadcast — tiling just means N instances share the broadcast instead of
## one).
func _ready() -> void:
SimBridge.atlas_layers_received.connect(_on_atlas_layers_received)
func _exit_tree() -> void:
if SimBridge.atlas_layers_received.is_connected(_on_atlas_layers_received):
SimBridge.atlas_layers_received.disconnect(_on_atlas_layers_received)
func _on_atlas_layers_received(response: Dictionary) -> void:
for tile: Dictionary in _tiles:
var request = tile["request"]
if is_instance_valid(request):
request.on_response(response)
## Enter tile mode for `body_id`/`body_radius_km` — computes the tile grid,
## tears down any PREVIOUS tile set's child request nodes (a fresh
## enter_orbital() on a DIFFERENT body must not leave stale tile requests
## from the old body wired up), and issues one request per tile immediately
## (no debounce — matching AtlasWindowRequest.request_now()'s own "first
## window" contract, §5: entry is never debounced, only pan/rung-reselect
## refetches are).
func enter(body_id: String, body_radius_km: float) -> void:
_teardown()
_body_id = body_id
var centers: Array = AtlasWindowGeometry.compute_tile_grid(body_radius_km)
for i in range(centers.size()):
var center: Vector2i = centers[i]
var request = AtlasWindowRequest.new(self)
request.name = "Tile%d" % i
add_child(request)
var tile_index := i # capture by value for the lambda below
request.window_ready.connect(
func(window: Dictionary) -> void: _on_tile_window_ready(tile_index, window)
)
_tiles.append({"center": center, "request": request, "window": null})
request.request_now(body_id, center, _tile_n, AtlasWindowRequest.GRANULARITY_V2_REGION)
func _on_tile_window_ready(index: int, window: Dictionary) -> void:
if index < 0 or index >= _tiles.size():
return # a stale signal from a torn-down tile set (shouldn't happen — disconnected on teardown)
_tiles[index]["window"] = window
tile_ready.emit(index)
## Tear down every tile's request node — disconnects nothing explicitly
## (queue_free() on a Node disconnects all its own signal connections
## automatically, Godot's documented behavior) but DOES clear `_tiles` so a
## stale index from an in-flight-but-now-orphaned request's eventual
## response can never reach `_on_tile_window_ready()` with a now-meaningless
## index (guarded there too, belt-and-suspenders).
func _teardown() -> void:
for tile: Dictionary in _tiles:
var request = tile["request"]
if is_instance_valid(request):
request.queue_free()
_tiles.clear()
## The current tile set, for the viewer/overlay to draw — an Array of
## {"center": Vector2i, "window": Variant} (the "request" key is internal,
## not exposed here; callers only need center + arrived-or-null window).
func get_tiles() -> Array:
var result: Array = []
for tile: Dictionary in _tiles:
result.append({"center": tile["center"], "window": tile["window"]})
return result
## True once tiling is active for the current body — a body whose whole
## circumference fits in ONE Region window's own coverage ceiling produces
## exactly one tile (compute_tile_grid()'s own degenerate-case doc), so
## `is_multi_tile()` distinguishes "tile set with 1 entry" (still tiling
## machinery, technically) from "genuinely multiple tiles" — the viewer uses
## this to decide whether the tile-set draw path or the ORIGINAL
## single-window draw path is simpler/preferred for a small body (both are
## correct; single-window avoids the extra Node/signal overhead when there's
## only ever going to be one tile).
func is_multi_tile() -> bool:
return _tiles.size() > 1
func get_tile_count() -> int:
return _tiles.size()
## True if every tile currently has an arrived window — the viewer/legend
## chrome can use this to know when the mosaic is "complete" vs. still
## progressively filling in.
func is_fully_arrived() -> bool:
if _tiles.is_empty():
return false
for tile: Dictionary in _tiles:
if tile["window"] == null:
return false
return true
@@ -1,62 +1,72 @@
class_name AtlasWindowViewer
extends Control
## Regional district-resolution window viewer (T-1138, D-226 T-1124
## amendment). Entered via a click-through from the planetary AtlasViewer
## (the 2026-07-21 §5 entry revision — NOT a zoom-threshold LOD swap).
## Renders a DistrictWindowLayer composite: morphology base layer lightness-
## modulated by elev_q, three switchable climate/vegetation overlays, and an
## always-on glaciation ice-tint modifier (drawing itself is
## AtlasWindowOverlay's job — this Control owns input, request orchestration,
## chrome, and the pan/zoom transform).
## Continuous cursor-anchored zoom ladder viewer (T-1153, superseding T-1138's
## click-through-only entry per the D-226 T-1143-rulings amendment — see
## enter_orbital()'s own doc). This IS the "regional" nav entry (T-1152
## client half): the whole ladder from the canonical orbital frame (Region
## rung) down to District/Quarter lives in ONE screen/Control, not a separate
## planetary viewer + windowed drill-down. Renders a DistrictWindowLayer
## composite (or, at the orbital rest state on a large body, a MOSAIC of
## several — see `_tile_mode`/AtlasWindowTileSet, live round 3) at whichever
## rung is currently held: morphology base layer lightness-modulated by
## elev_q, three switchable climate/vegetation overlays, an always-on
## glaciation ice-tint modifier (drawing is AtlasWindowOverlay's job — this
## Control owns input, request orchestration, chrome, pan/zoom). One
## colorizer family renders every rung unchanged (design doc §6).
##
## Design notes (mirroring AtlasViewer's own split, D-226 §5):
## Design notes (mirroring AtlasViewer's own split, D-226 §5, extended T-1153):
## - _canvas (Node2D) holds AtlasWindowOverlay; pan = _canvas.position, zoom
## = _canvas.scale — the SAME transform idiom as the planetary viewer.
## - Zoom is ALWAYS client-side on the already-held composite (§5: "the
## composite is a texture the client zooms client-side... from already-
## held data") — it NEVER triggers a re-request. Only a pan past the held
## window's edge does (§4/§5).
## - _window_request (atlas_window_request.gd) owns the cache/debounce/
## retry — this Control decides WHEN to call it (pan-edge detection,
## entry), never talks to SimBridge directly itself.
## = _canvas.scale.
## - Zoom is client-side on the ALREADY-HELD composite frame-to-frame, but
## CONTINUOUS AND UNCLAMPED ACROSS RUNGS (D-013): crossing a rung's
## coverage ceiling (§5, AtlasWindowGeometry.select_rung()) fires a
## background request for the new granularity while the OLD composite
## keeps drawing — progressive refinement, no blank frame (§6). A pan
## past the held window's edge re-requests the SAME rung at a new center.
## - Zooming fully out snaps to the CANONICAL planetary frame (Jeroen's HARD
## condition, see _maybe_reset_to_canonical_frame()) — on a body needing
## tiling, re-enters `_tile_mode` (live round 3, design doc §4).
## - _window_request (atlas_window_request.gd) owns the single-window
## cache/debounce/retry; _tile_set (atlas_window_tile_set.gd) owns N of
## those for the tiled rest state — this Control decides WHICH is active.
##
## Navigation (T-1145 item 2 — Jeroen's input-model ruling: LMB-drag panning
## BREAKS click semantics with map objects, so it is removed entirely; clicks
## are reserved for map objects, which will land in this window later, e.g.
## settlements):
## Navigation (Jeroen's input-model ruling: LMB-drag panning BREAKS click
## semantics with future map objects, so it's removed entirely):
## WASD / arrow keys continuous pan, held (frame-rate independent, _process)
## Edge scrolling cursor within EDGE_SCROLL_MARGIN_PX of a viewport
## edge pans toward it (suppressed over UI / unfocused)
## Mouse wheel zoom the held composite (client-side only, never refetches)
## Esc back to the planetary view
## Edge scrolling cursor within EDGE_SCROLL_MARGIN_PX pans toward it (suppressed over UI / unfocused)
## Mouse wheel cursor-anchored zoom; crosses rungs continuously (T-1153)
## Esc back (nav.pop())
signal back_pressed
const PANEL_MARGIN: float = 16.0
const OVERLAY_BAR_HEADER_RESERVE: float = 360.0
const MIN_ZOOM: float = 0.5
const MAX_ZOOM: float = 8.0
## T-1153: MIN_ZOOM/MAX_ZOOM are a wide safety clamp on the raw display
## multiplier, NOT a rung boundary — wheel zoom is CONTINUOUS and UNCLAMPED
## ACROSS RUNGS (D-013): crossing a rung's coverage ceiling
## (AtlasWindowGeometry.select_rung()) re-requests a DIFFERENT granularity at
## the SAME apparent screen extent, never clamping _view_zoom itself.
## set_view() (T-1120 capture API) clamps to this same range independently.
## MIN_ZOOM must stay low enough that fit_window_view()'s COVER fit for
## enter_orbital()'s largest legal `n` is never itself clamped (would
## silently show LESS than the whole body). 0.0005 covers a ~120,000 km-
## radius body at a 3840px 4K viewport.
const MIN_ZOOM: float = 0.0005
const MAX_ZOOM: float = 64.0
const ZOOM_STEP: float = 1.15
## T-1145 item 2: WASD/arrow-key continuous pan speed, in CANVAS px/s at
## zoom=1.0 — the ACTUAL screen-space pan rate is this value times the
## CURRENT _view_zoom (see _process()'s pan tick), so panning covers the
## same amount of TERRAIN per second regardless of zoom level. A fixed
## SCREEN-px/s rate (no zoom scaling) would feel painfully slow zoomed in
## (each screen pixel is a fraction of a district) and uncontrollably fast
## zoomed out — scaling by zoom keeps the "how much world passes per
## second" feel constant, matching the ticket's "speed in screen px/s
## scaled by zoom" wording. ~6 districts/s at zoom=1.0 (96/16) — brisk
## enough to cross a default n=32 window in ~5s, not a crawl.
## zoom=1.0 — actual screen-space rate is this times CURRENT _view_zoom, so
## panning covers the same TERRAIN per second regardless of zoom level.
## ~6 districts/s at zoom=1.0 (96/16) — brisk, not a crawl.
const PAN_SPEED_CANVAS_PX_S: float = 96.0
## T-1145 item 2: cursor-to-edge distance (px) that triggers edge-scroll —
## Jeroen's own number ("~24px").
## Jeroen's own number ("~24px"). Uses the SAME speed as WASD (one pan feel,
## two triggers) — no separate constant, _process() reads PAN_SPEED_CANVAS_PX_S for both.
const EDGE_SCROLL_MARGIN_PX: float = 24.0
## Edge-scroll uses the SAME speed as WASD (one pan feel, two triggers) —
## no separate constant, _process() reads PAN_SPEED_CANVAS_PX_S for both.
## Pixel size of one district cell at zoom=1.0 — a fixed on-screen scale
## (unlike AtlasViewer's heightmap, there is no source texture dictating a
@@ -69,11 +79,16 @@ const COLOR_BG: Color = Color("#0d1117")
## Border-fade target (§5 "what renders during the wait"): the underlying
## whole-body heightmap's own background tint, so the newly-exposed edge
## reads as "real data seen through", not a placeholder block. Reuses
## AtlasViewer's own COLOR_HEIGHTMAP_TINT-adjacent dim value rather than
## inventing a new one — this IS a dimmer/less-certain read of the same
## planetary data, not a different visual language.
## AtlasViewer's own COLOR_HEIGHTMAP_TINT-adjacent dim value — a dimmer/
## less-certain read of the same planetary data, not a different visual language.
const COLOR_BORDER_FADE: Color = Color(0.20, 0.24, 0.30, 0.55)
## T-1153/R6: pending-refinement wash — border-fade's referent repointed to
## "the previous derived composite at this position" for a rung-crossing zoom
## (real data stays on screen, unlike the no-composite-yet case above). Same
## hue, lighter alpha — hints something sharper is arriving, not that the view is wrong.
const COLOR_PENDING_REFINEMENT_WASH: Color = Color(0.20, 0.24, 0.30, 0.12)
## D-243: 2,048 m per district side.
const DISTRICT_M: float = 2048.0
@@ -82,6 +97,8 @@ const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_re
# atlas_descend_geometry.gd instead — it already owns district_extent()).
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
# T-1153: orbital rest-state mosaic orchestration.
const AtlasWindowTileSet := preload("res://ui/implant/apps/atlas/atlas_window_tile_set.gd")
# ── Overlay definitions (T-1138 — reuses atlas_overlay_bar.gd/
# atlas_legend_panel.gd's existing duck-typed viewer interface: both call
@@ -119,6 +136,11 @@ var _implant_theme = null
var _window: Variant = null # current DistrictWindowLayer Dictionary, or null while waiting
var _held_center: Vector2i = Vector2i.ZERO
var _held_n: int = 32
## T-1153: the granularity_v2 tag this viewer is currently HOLDING (the
## last-adopted _window's own rung) — distinct from
## _window_request.get_granularity_v2() (most recently REQUESTED, may be a
## different rung already in flight). Defaults to District (see enter()'s doc).
var _held_granularity_v2: String = "District"
# ── Pan/zoom state ─────────────────────────────────────────────────────────
var _view_offset: Vector2 = Vector2.ZERO
@@ -160,6 +182,13 @@ var _screen_header: ImplantHeader = null
var _overlay_bar = null
var _legend_panel = null
var _window_request = null # AtlasWindowRequest
var _tile_set = null # AtlasWindowTileSet (T-1153, live round 3)
## T-1153 (design doc §4): true while showing the orbital rest state as a
## MULTI-WINDOW MOSAIC (AtlasWindowTileSet) instead of the single held
## composite (`_window`). Set by `_enter_tile_mode()`; cleared the moment
## `_maybe_reselect_rung()` crosses OUT of Region.
var _tile_mode: bool = false
func _ready() -> void:
@@ -189,6 +218,11 @@ func _ready() -> void:
add_child(_window_request)
_window_request.window_ready.connect(_on_window_ready)
_tile_set = AtlasWindowTileSet.new(self)
_tile_set.name = "TileSet"
add_child(_tile_set)
_tile_set.tile_ready.connect(_on_tile_ready)
_build_screen_header()
_build_overlay_bar()
_build_legend_panel()
@@ -201,50 +235,122 @@ func _exit_tree() -> void:
SimBridge.atlas_layers_received.disconnect(_on_atlas_layers_received)
## Enter the window screen centered on `district_center` (a DistrictPos-
## equivalent Vector2i, from the planetary click-through's derived position —
## §5's "pan center read as click point"). n defaults to the client's
## interactive default (32), half the server's hard cap.
## Enter the window screen centered on `district_center` at District
## granularity. n defaults to 32. Thin wrapper over _enter_at_rung()
## (T-1153); survives as a direct-call test entry.
##
## T-1142: `district_center` is canonicalized (wrap column / clamp row)
## BEFORE it becomes `_held_center` or reaches the request — matching the
## server's own normalize_window_center() exactly, so the request the client
## sends and the echo the server sends back describe the SAME canonical
## point from the first round-trip (never a raw-vs-normalized mismatch that
## would fail the §2 staleness echo check). Also fits-and-centers the view
## instead of the old zoom=1/offset=ZERO reset (Jeroen's second finding: an
## n=32 composite is 512px native, a postage stamp unfitted in a real
## viewport).
## T-1142: `district_center` is canonicalized BEFORE it becomes
## `_held_center` — matching the server's normalize_window_center().
func enter(
body: Dictionary,
system: Dictionary,
district_center: Vector2i,
n: int = AtlasWindowRequest.DISTRICT_WINDOW_DEFAULT_N
) -> void:
var radius_km: float = float(body.get("body_radius_km", 0.0))
var canonical_center: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
district_center, radius_km
)
_enter_at_rung(body, system, canonical_center, n, AtlasWindowRequest.GRANULARITY_V2_DISTRICT)
## T-1153: enter the ladder at its TOP REST STATE — the canonical orbital
## frame (Jeroen's HARD condition: whole body fitted to canvas, centered at
## the canonical origin). The "regional" nav entry point (T-1152 client
## half): the player lands on a fully-derived Region-rung view of the whole
## body, then wheel-zoom descends CONTINUOUSLY from there. Canonical origin
## = district (0,0), same quantity is_fully_zoomed_out()/
## _maybe_reset_to_canonical_frame() test against. No-radius bodies fall
## back to the District-rung default window.
##
## **Live round 3 (design doc §4): the rest state must TILE.** A single
## wire-capped Region window covers only a fraction of a real body's
## circumference. Once `compute_tile_grid()` returns MORE than one tile,
## entry goes through `_enter_tile_mode()` instead of `_enter_at_rung()`.
func enter_orbital(body: Dictionary, system: Dictionary) -> void:
var radius_km: float = float(body.get("body_radius_km", 0.0))
if radius_km <= 0.0:
_enter_at_rung(
body, system, Vector2i.ZERO,
AtlasWindowRequest.DISTRICT_WINDOW_DEFAULT_N,
AtlasWindowRequest.GRANULARITY_V2_DISTRICT
)
return
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(radius_km)
if tiles.size() > 1:
_enter_tile_mode(body, system, radius_km)
return
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
_enter_at_rung(body, system, Vector2i.ZERO, n, AtlasWindowRequest.GRANULARITY_V2_REGION)
## T-1153, live round 3: the TILE-MODE entry path — same reset discipline as
## `_enter_at_rung()` but populates `_tile_set` instead of `_window_request`.
## `_held_n` carries the WHOLE body's extent unclamped (each TILE clamps
## its own TILE_N-sized request independently).
func _enter_tile_mode(body: Dictionary, system: Dictionary, radius_km: float) -> void:
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
_body = body
_system = system
var radius_km: float = float(_body.get("body_radius_km", 0.0))
_held_center = AtlasDescendGeometry.canonicalize_district_center(district_center, radius_km)
_held_center = Vector2i.ZERO
_held_n = n
_held_granularity_v2 = AtlasWindowRequest.GRANULARITY_V2_REGION
_tile_mode = true
_window = null
_user_adjusted = false
_awaiting_first_window = true
_fit_and_center()
_window_request.reset()
_window_request.request_now(_dict_str(_body, "body_id", ""), _held_center, n)
_tile_set.enter(_dict_str(_body, "body_id", ""), radius_km)
_refresh_screen_header()
_legend_panel.refresh()
grab_focus()
queue_redraw()
_overlay_node.queue_redraw()
## Shared entry path for enter()/enter_orbital() (T-1153) — `district_center`
## must already be canonicalized by the caller. Resets every piece of
## held/request state for a fresh descent, plus _held_granularity_v2.
##
## **C1 clamp-mirror, one layer up:** `n` MUST be clamped via
## `_clamp_window_n_mirror_v2()` BEFORE it becomes `_held_n` — mirroring
## AtlasWindowRequest.request_now()'s own clamp (PR #191 Tyre C1).
func _enter_at_rung(
body: Dictionary,
system: Dictionary,
district_center: Vector2i,
n: int,
granularity_v2: String
) -> void:
var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(n, granularity_v2)
_body = body
_system = system
_held_center = district_center
_held_n = clamped_n
_held_granularity_v2 = granularity_v2
_tile_mode = false # T-1153 live round 3: a single-window entry always leaves tile mode
_window = null
_user_adjusted = false
_awaiting_first_window = true
_fit_and_center()
_window_request.reset()
_window_request.request_now(
_dict_str(_body, "body_id", ""), _held_center, clamped_n, granularity_v2
)
_refresh_screen_header()
_legend_panel.refresh()
grab_focus()
queue_redraw()
_overlay_node.queue_redraw()
## T-1142: fit-and-center — applies AtlasWindowGeometry.fit_window_view()'s
## zoom/offset, then re-clamps the offset to the pole wall (a freshly-fitted
## view can still need the wall on a tiny body whose row span is shorter than
## the window itself — see atlas_window_geometry.gd's clamp function doc).
## Called from enter(), the FIRST _on_window_ready() after entry, and
## NOTIFICATION_RESIZED — never mid-interaction (guarded by _user_adjusted at
## each call site, not here, since the three callers gate slightly differently).
## zoom/offset, then re-clamps the offset to the pole wall. Called from
## enter(), the FIRST _on_window_ready() after entry, and
## NOTIFICATION_RESIZED — never mid-interaction (guarded by _user_adjusted).
func _fit_and_center() -> void:
var viewport: Vector2 = get_rect().size
if viewport == Vector2.ZERO:
@@ -257,11 +363,8 @@ func _fit_and_center() -> void:
_apply_transform()
## T-1142: the pole-wall clamp needs the body's rows_half, in whole districts
## — a no-radius body (tiny test body) has no periodicity/pole concept at the
## DistrictPos level (matching canonicalize_district_center()'s own no-radius
## identity disposition), so the wall is a no-op there (rows_half=0, and
## clamp_pan_offset_to_pole_wall() treats <= 0 as "no wall").
## T-1142: needs the body's rows_half — a no-radius body has no pole
## concept, so the wall is a no-op there (rows_half=0).
func _clamp_offset_to_pole_wall(offset: Vector2) -> Vector2:
var radius_km: float = float(_body.get("body_radius_km", 0.0))
if radius_km <= 0.0:
@@ -277,12 +380,48 @@ func leave() -> void:
## Named get_district_window(), NOT get_window() — Node already defines
## get_window() -> Window (the containing OS window); shadowing it with an
## incompatible return type is a Godot parse error (confirmed the hard way).
## get_window() -> Window; shadowing it with an incompatible type errors.
func get_district_window() -> Variant:
return _window
## T-1153: true while showing the orbital rest state as a multi-window
## mosaic instead of the single held composite — AtlasWindowOverlay reads
## this to pick a draw path.
func is_tile_mode() -> bool:
return _tile_mode
## T-1153: the tile-set orchestrator, for AtlasWindowOverlay's mosaic draw
## path — only meaningful while is_tile_mode() is true.
func get_tile_set() -> Variant:
return _tile_set
## Live round 4: currently-HELD reference frame — AtlasWindowOverlay's
## mosaic draw path converts tile centers into canvas-local space via these
## (see AtlasWindowGeometry.district_to_canvas_local()'s own doc).
func get_held_center() -> Vector2i:
return _held_center
func get_held_n() -> int:
return _held_n
## Currently-HELD rung tag — legend reads this via spacing_for_rung(),
## mirroring _refresh_screen_header()'s own use of the field.
func get_held_granularity_v2() -> String:
return _held_granularity_v2
## Live round 5: current body's radius — mosaic draw needs `cols` for
## nearest_wrap_image()'s wrap resolution. Mirrors the
## `_body.get("body_radius_km", 0.0)` pattern used throughout this file.
func get_body_radius_km() -> float:
return float(_body.get("body_radius_km", 0.0))
## District-cell pixel size at zoom=1.0 — AtlasWindowOverlay reads this
## rather than hardcoding CELL_PIXEL_SIZE itself, so the viewer stays the
## single source of geometry truth (same "viewer owns the transform, overlay
@@ -317,34 +456,68 @@ func _on_atlas_layers_received(response: Dictionary) -> void:
_window_request.on_response(response)
## T-1153: progressive refinement — the ONE place a new rung's window gets
## adopted, deliberately WITHOUT clearing `_window` first. The OLD composite
## stays drawn until this call — no blank frame, no mode flip (§6) —
## because `_window` only ever gets REPLACED, never nulled, once adopted
## (enter()/_enter_at_rung() null it only at a fresh descent, not a swap).
func _on_window_ready(window: Dictionary) -> void:
# Only adopt the window if it still matches what THIS viewer is currently
# showing — AtlasWindowRequest already filtered by its own last-asked
# (center, n) via the echo (§2), but a cache-hit path can fire
# synchronously from enter() before _held_center is what the signal
# handler expects in a re-entrant call; comparing again here is cheap and
# removes any ordering assumption between enter()'s two calls.
# (center, n, granularity_v2) via the echo (§2/T-1150/T-1152), but a
# cache-hit path can fire synchronously from enter() before _held_center
# is what the signal handler expects in a re-entrant call; comparing
# again here is cheap and removes any ordering assumption between
# enter()'s two calls. granularity_v2 (T-1153) is compared too — a
# district-rung response answering a request that's SINCE moved on to a
# region-rung request (rapid wheel-zoom) must not be adopted just because
# center/n happen to still match.
var w_center := _vec_from_center(window.get("center", [0, 0]))
var w_n := int(window.get("n", 0))
if w_center != _held_center or w_n != _held_n:
var w_granularity_v2 := str(
window.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT)
)
if (
w_center != _held_center
or w_n != _held_n
or w_granularity_v2 != _window_request.get_granularity_v2()
):
return
_window = window
_held_granularity_v2 = w_granularity_v2
# T-1142: re-fit on the FIRST composite arrival only (the entry-time fit
# may have used a viewport size the layout hadn't settled into yet — this
# corrects it once) — never on a later pan-triggered arrival, and never
# once the user has manually zoomed/panned (same _user_adjusted guard
# enter()/NOTIFICATION_RESIZED use).
# enter()/NOTIFICATION_RESIZED use). A later rung-swap arrival is
# EXACTLY a "later pan/zoom-triggered arrival" in this sense — it must
# never re-fit either, or a wheel-zoom-triggered rung swap would yank the
# player's view back to a fitted framing mid-gesture.
if _awaiting_first_window and not _user_adjusted:
_fit_and_center()
_awaiting_first_window = false
_refresh_screen_header()
_legend_panel.refresh()
queue_redraw()
_overlay_node.queue_redraw()
## T-1153 (design doc §4 "progressive... with visible refinement as tiles
## complete"): a SINGLE tile's window arrived — redraw so the overlay's
## mosaic loop picks it up. No acceptance/staleness logic needed here (each
## tile's OWN AtlasWindowRequest already filtered before this signal fired).
func _on_tile_ready(_index: int) -> void:
queue_redraw()
_overlay_node.queue_redraw()
# =============================================================================
# View transform (mirrors AtlasViewer's own — pan is real, zoom is client-side
# only and NEVER triggers a re-request per §5)
# View transform (mirrors AtlasViewer's own — pan is real; zoom is CURSOR-
# ANCHORED and CONTINUOUS ACROSS RUNGS (T-1153, D-226 T-1143-rulings
# amendment): the held composite is always drawn client-side-zoomed with NO
# re-request, but crossing a rung's spacing threshold fires a NEW request at
# the new granularity in the background (progressive refinement — see
# _maybe_reselect_rung()'s own doc) while the OLD composite stays on screen.
# =============================================================================
@@ -355,20 +528,166 @@ func _apply_transform() -> void:
_overlay_node.queue_redraw()
## Cursor-anchored zoom (D-013): the CANVAS POINT under the cursor stays
## fixed on screen. Unclamped across rungs (only the wide MIN_ZOOM/MAX_ZOOM
## safety clamp applies); after applying, checks the top rest state.
func _zoom_at(mouse_pos: Vector2, factor: float) -> void:
var new_zoom: float = clampf(_view_zoom * factor, MIN_ZOOM, MAX_ZOOM)
# Live round 6: once settled at the canonical frame, a continued zoom-OUT
# tick must not drift `_view_zoom` below fit (see the reset's own doc for
# why that caused a request storm). Clamping the ZOOM here — not the
# reset guard — keeps the reset edge-triggered. Zoom-IN is never clamped.
if new_zoom < _view_zoom and _is_at_canonical_frame():
var fit_zoom: float = _canonical_fit_zoom()
new_zoom = maxf(new_zoom, fit_zoom)
if is_equal_approx(new_zoom, _view_zoom):
return
var local_before: Vector2 = (mouse_pos - _view_offset) / _view_zoom
_view_zoom = new_zoom
_view_offset = mouse_pos - local_before * _view_zoom
_apply_transform()
if _maybe_reset_to_canonical_frame():
return # the reset already re-fit + re-requested at the top rung
_maybe_reselect_rung()
## Programmatic view control (T-1120 capture-API parity — must survive on
## every viewer this app exposes, per the ticket's explicit note, even one
## that never got user pan/zoom to begin with on the OTHER seam this ticket
## removes it from).
## The world extent (metres) currently displayed across the LARGER viewport
## dimension — the `E` half of the §5 rung-selection rule. Thin wrapper over
## AtlasWindowGeometry.world_extent_m() (a pure function of `_view_zoom`).
func _current_world_extent_m() -> float:
return AtlasWindowGeometry.world_extent_m(CELL_PIXEL_SIZE, _view_zoom, get_rect().size)
## §5 rung-selection rule + progressive refinement (T-1153): after a zoom
## step, recompute the legal rung for the NOW-displayed world extent. If it
## differs from what's HELD, request the new granularity centered on the
## CURRENT screen-center (same formula as _maybe_refloat_window()).
##
## **C1 clamp-mirror, a THIRD layer up:** `_held_n` MUST be re-clamped via
## `_clamp_window_n_mirror_v2()` for the TARGET rung — a stale large
## `_held_n` desyncs `_on_window_ready()`'s staleness check.
##
## Progressive refinement: does NOT touch `_window`/`_held_granularity_v2` —
## the OLD composite keeps drawing until _on_window_ready() adopts the new
## one (§6 "no mode flip"). Live round 5: this lag is what made
## `_maybe_reset_to_canonical_frame()`'s OLD guard misfire.
func _maybe_reselect_rung() -> void:
if _held_n <= 0:
return
var world_extent_m: float = _current_world_extent_m()
var canvas_px: float = maxf(get_rect().size.x, get_rect().size.y)
var target_rung: String = AtlasWindowGeometry.select_rung(world_extent_m, canvas_px)
# T-1153, live round 3: tile mode is TOP-of-the-ladder only. Staying at
# Region means staying tiled; crossing OUT of Region falls through to
# the single-window path below, flipping `_tile_mode` off.
var leaving_tile_mode := false
if _tile_mode:
if target_rung == AtlasWindowRequest.GRANULARITY_V2_REGION:
return
_tile_mode = false
leaving_tile_mode = true
# `leaving_tile_mode` FORCES the request through even if
# `_window_request`'s own STALE granularity_v2 (untouched while tiled)
# happens to already equal `target_rung` by coincidence.
if not leaving_tile_mode and target_rung == _window_request.get_granularity_v2():
return # already requesting (or holding) the rung this extent calls for
var new_center: Vector2i = _screen_center_district()
var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(_held_n, target_rung)
_held_center = new_center
_held_n = clamped_n
# Live round 4: `_view_offset` must be recomputed the instant `held_n`
# changes — see recompute_offset_for_held_n_change()'s own doc for why.
# Anchors on the SAME screen center just used above, preserving §6 "no
# layout jump" across the crossing.
_view_offset = AtlasWindowGeometry.recompute_offset_for_held_n_change(
size * 0.5, _view_zoom, clamped_n, CELL_PIXEL_SIZE
)
_apply_transform()
_window_request.request_debounced(
_dict_str(_body, "body_id", ""), new_center, clamped_n, target_rung
)
## The DistrictPos the current screen center maps to, in RAW absolute
## district space. Thin wrapper over screen_center_to_district() so
## pan-edge/rung-reselect refetch share one formula.
func _screen_center_district() -> Vector2i:
var raw: Vector2i = AtlasWindowGeometry.screen_center_to_district(
size, _view_offset, _view_zoom, CELL_PIXEL_SIZE, _held_center, _held_n
)
var radius_km: float = float(_body.get("body_radius_km", 0.0))
return AtlasDescendGeometry.canonicalize_district_center(raw, radius_km)
## Live round 6: whether the body needs mosaic tiling — mirrors
## enter_orbital()'s own dispatch condition so entry/predicate/guard agree.
func _canonical_tile_mode(radius_km: float) -> bool:
return AtlasWindowGeometry.compute_tile_grid(radius_km).size() > 1
## Live round 6: the fit zoom enter_orbital() lands on for the CURRENT
## body/viewport — read by both the canonical predicate and _zoom_at()'s floor.
func _canonical_fit_zoom() -> float:
var radius_km: float = float(_body.get("body_radius_km", 0.0))
if radius_km <= 0.0:
return _view_zoom # no-radius body — no canonical frame concept, floor is a no-op
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var canonical_n: int = int(extent["cols"])
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
get_rect().size, canonical_n, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
return float(fit["zoom"])
## Live round 6: true when the CURRENT view EXACTLY matches the canonical
## frame — center/mode/granularity plus `_view_zoom` at fit (float epsilon).
func _is_at_canonical_frame() -> bool:
var radius_km: float = float(_body.get("body_radius_km", 0.0))
if radius_km <= 0.0:
return false
return (
_held_center == Vector2i.ZERO
and _held_granularity_v2 == AtlasWindowRequest.GRANULARITY_V2_REGION
and _tile_mode == _canonical_tile_mode(radius_km)
and is_equal_approx(_view_zoom, _canonical_fit_zoom())
)
## Jeroen's HARD condition: "a full zoom-out resets to the original
## canonical planetary frame and location." EDGE-triggered (live round 6):
## fires only on the transition INTO fully-zoomed-out from non-canonical.
##
## **Live round 5 fix:** the old guard checked only
## `_held_center`/`_held_granularity_v2` — a LAGGING field (updated only on
## response adoption). A TILING body's granularity stays stale "Region"
## after zooming IN leaves tile mode, misreading "already canonical" and
## never resetting. Fixed by also requiring `is_tile_mode()` to match.
##
## **Live round 6 fix (round 5's SECOND fix overshot into a storm):** a
## per-tick zoom-equality check on THIS guard made it LEVEL-triggered —
## continued zoom-out kept nudging `_view_zoom` below fit, so the guard
## read "not already there" every tick and `enter_orbital()` fired
## repeatedly: tile set torn down/recreated each time, orphaning in-flight
## responses (nothing held → black), flooding the server (889/897 wire
## responses in one zoom-out phase). Fixed by moving the zoom-drift concern
## to `_zoom_at()`'s own zoom floor instead — this guard's
## mode/center/granularity check alone stays edge-triggered.
func _maybe_reset_to_canonical_frame() -> bool:
var radius_km: float = float(_body.get("body_radius_km", 0.0))
if radius_km <= 0.0:
return false # no-radius body — no canonical frame concept (matches enter_orbital()'s own guard)
var world_extent_m: float = _current_world_extent_m()
if not AtlasWindowGeometry.is_fully_zoomed_out(world_extent_m, radius_km):
return false
if _is_at_canonical_frame():
return false # already at the canonical frame — don't fight a zoom-in-from-the-top gesture
enter_orbital(_body, _system)
return true
## Programmatic view control (T-1120 capture-API parity).
func get_view_zoom() -> float:
return _view_zoom
@@ -389,38 +708,24 @@ func set_view(zoom: float, offset: Vector2) -> void:
# =============================================================================
## After a pan delta (T-1145: WASD/edge-scroll, called from _process()'s pan
## tick every frame the player is actively panning), check whether the
## After a pan delta (T-1145: WASD/edge-scroll), check whether the
## screen-center now maps to a DistrictPos outside the held window's extent
## — if so, float a NEW window centered on that point (§5 "windows float on
## the pan center... not grid-snapped") via the debounced request path.
## — if so, float a NEW window centered on that point via the debounced path.
##
## T-1142 (item 6a): the edge-crossing decision below is computed in RAW
## absolute district space (un-wrapped, un-clamped) — that is the correct
## space for "has the pan carried the view past the held window's edge",
## since the held window's own local bounds are relative to _held_center as
## it was BEFORE this pan. Only the FINAL new_center that becomes the next
## _held_center / the next request is canonicalized (wrap column, clamp
## row) — matching the server's own normalize_window_center() and keeping
## the client's echo-comparison and cache key on the same canonical form the
## server uses (see canonicalize_district_center()'s doc for why this must
## match bit-for-bit). A pan that straddles the antimeridian therefore still
## floats correctly: the pre-canonicalization abs_col can be e.g. -3 or
## district_cols+5, the edge-crossing math treats that as a normal delta from
## the old center, and only the resulting new_center gets wrapped into range
## before it's requested/cached.
## T-1142 (item 6a): the edge-crossing decision is computed in RAW absolute
## district space — only the FINAL new_center is canonicalized, matching the
## server's normalize_window_center().
func _maybe_refloat_window() -> void:
if _held_n <= 0:
return
var screen_center: Vector2 = size * 0.5
var canvas_pt: Vector2 = (screen_center - _view_offset) / _view_zoom
var cell: Vector2 = canvas_pt / CELL_PIXEL_SIZE
# cell is in [0, _held_n) local window space when centered — half-window
# offset from _held_center converts back to absolute district space.
var half: float = float(_held_n) / 2.0
var abs_col: float = float(_held_center.x) - half + cell.x
var abs_row: float = float(_held_center.y) - half + cell.y
var raw_new_center := Vector2i(roundi(abs_col), roundi(abs_row))
if _tile_mode:
# T-1153: the tile set already covers the WHOLE body — no "edge" to
# cross while tiled. _apply_pan_delta() still moves _view_offset;
# this only skips the single-window re-float below.
return
var raw_new_center: Vector2i = AtlasWindowGeometry.screen_center_to_district(
size, _view_offset, _view_zoom, CELL_PIXEL_SIZE, _held_center, _held_n
)
if raw_new_center == _held_center:
return
# Edge-crossing check: only re-request if the screen-center point has
@@ -429,8 +734,9 @@ func _maybe_refloat_window() -> void:
# ticked over by one cell near a boundary) must not spam a request every
# frame. §4: "re-requests only when a pan carries the view past the held
# window's edge."
var local_col: float = abs_col - float(_held_center.x) + half
var local_row: float = abs_row - float(_held_center.y) + half
var half: float = float(_held_n) / 2.0
var local_col: float = float(raw_new_center.x - _held_center.x) + half
var local_row: float = float(raw_new_center.y - _held_center.y) + half
var inside: bool = (
local_col >= 0.0
and local_col < float(_held_n)
@@ -444,7 +750,13 @@ func _maybe_refloat_window() -> void:
raw_new_center, radius_km
)
_held_center = new_center
_window_request.request_debounced(_dict_str(_body, "body_id", ""), new_center, _held_n)
# T-1153: pass the CURRENTLY HELD rung — panning must re-request at the
# SAME granularity it's already showing, never silently reset to the
# request object's District default (that default exists for callers with
# no rung concept of their own; this viewer always has one).
_window_request.request_debounced(
_dict_str(_body, "body_id", ""), new_center, _held_n, _held_granularity_v2
)
# =============================================================================
@@ -454,25 +766,45 @@ func _maybe_refloat_window() -> void:
func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
if _tile_mode:
# T-1153: single-window border-fade/pending-wash don't apply to a
# mosaic — AtlasWindowOverlay's tile draw only paints arrived tiles;
# an unarrived one is an honest gap over COLOR_BG, no separate fade.
return
if _window == null:
# §5 "what renders during the wait": a border-fade to the underlying
# whole-body context rather than black/a spinner. This viewer has no
# resident whole-body texture of its own (that lives on AtlasViewer,
# which this screen has navigated away from) — the honest available
# resident whole-body texture of its own — the honest available
# substitute is a dim fade wash over the held composite's last-known
# extent, reusing _gen_pending_indicator (via the request object's own
# is_pending()) for the "still working" cue rather than new dressing.
# extent, reusing the request object's own is_pending() for the
# "still working" cue rather than new dressing.
_draw_border_fade()
elif _window_request and _window_request.is_pending():
# T-1153/R6: the border-fade's REFERENT repointed — a rung-crossing
# zoom (progressive refinement) leaves `_window` non-null (the OLD
# composite is still the thing on screen, drawn by AtlasWindowOverlay
# as always) while a NEW rung's request is in flight underneath it.
# R6's ruling: "the mechanism survives; its target must be repointed
# to 'the previous derived composite at this position'" — exactly
# this case. A lighter pending wash (not the full opaque fade the
# no-composite-at-all case uses, since there IS real data showing
# through here, not emptiness) signals "sharper detail incoming"
# without implying the current view is stale or wrong.
_draw_pending_refinement_wash()
func _draw_border_fade() -> void:
if not _window_request or not _window_request.is_pending():
return
var extent: float = float(_held_n) * CELL_PIXEL_SIZE * _view_zoom
var top_left: Vector2 = _view_offset
draw_rect(Rect2(top_left, Vector2(extent, extent)), COLOR_BORDER_FADE)
func _draw_pending_refinement_wash() -> void:
var extent: float = float(_held_n) * CELL_PIXEL_SIZE * _view_zoom
var top_left: Vector2 = _view_offset
draw_rect(Rect2(top_left, Vector2(extent, extent)), COLOR_PENDING_REFINEMENT_WASH)
func _build_screen_header() -> void:
_screen_header = ImplantHeader.new()
_screen_header.position = Vector2(PANEL_MARGIN, 16.0)
@@ -483,32 +815,26 @@ func _build_screen_header() -> void:
_screen_header.apply_implant_theme(_implant_theme)
## D-169/D-170 implant chrome (§5): location label (body name + coordinate,
## T-1142 — see _location_label()) + extent-in-real-units subtitle, e.g.
## "4.1 x 4.1 km . 2.0 km/cell".
## D-169/D-170 implant chrome (§5) — title/subtitle text built by the pure
## AtlasWindowGeometry.screen_header_content() (T-1153: moved there for
## file-length; the "continuous metres-per-pixel readout, never a discrete
## mode label" rationale lives on that function's own doc now).
func _refresh_screen_header() -> void:
if _screen_header == null:
return
var location_label: String = _location_label()
var extent_km: float = float(_held_n) * DISTRICT_M / 1000.0
var extent_line: String = "%.1f x %.1f km · %.1f km/cell" % [
extent_km, extent_km, DISTRICT_M / 1000.0
]
var title: String = "REGIONAL — %s" % location_label.to_upper()
_screen_header.set_content(title, extent_line)
var content: Dictionary = AtlasWindowGeometry.screen_header_content(
_dict_str(_body, "proper_name", _dict_str(_body, "body_id", "")),
_held_center, _held_n, _held_granularity_v2, DISTRICT_M
)
_screen_header.set_content(content["title"], content["subtitle"])
## Body name + coordinate label (T-1142: pulls the CHEAP half of T-1141
## forward — the body's proper name was already sitting unused on _body,
## passed through the whole descend chain since T-1138, but this header never
## read it, showing bare "district (col, row)" with no indication of WHICH
## body the player is looking at. T-1141 keeps only the harder half: nearest-
## settlement proximity join (the window carries no settlement data of its
## own — that lives on the planetary gen_l3_settlements overlay, a different
## screen/dataset — a real follow-up, not a silently-guessed one).
## Thin wrapper over AtlasWindowGeometry.location_label() (T-1153: moved
## there for file-length) — kept as a method since it's directly tested.
func _location_label() -> String:
var body_name: String = _dict_str(_body, "proper_name", _dict_str(_body, "body_id", ""))
return "%s — (%d, %d)" % [body_name, _held_center.x, _held_center.y]
return AtlasWindowGeometry.location_label(
_dict_str(_body, "proper_name", _dict_str(_body, "body_id", "")), _held_center
)
# =============================================================================
@@ -516,27 +842,18 @@ func _location_label() -> String:
# =============================================================================
## No city panel / sidebar in this mode (yet) — the window carries no
## settlement join of its own (see _location_label's doc), so there is
## nothing to hit-test against and this always reads false. Wired into
## _gui_input exactly where AtlasViewer's own _is_over_ui is (same guard
## shape) so a future sidebar addition only needs to change THIS function's
## body, not every call site.
## No city panel / sidebar in this mode (yet) — nothing to hit-test against,
## so this always reads false. Wired into _gui_input exactly where
## AtlasViewer's own _is_over_ui is, so a future sidebar addition only
## needs to change THIS function's body.
func _is_over_ui(_pos: Vector2) -> bool:
return false
## T-1145 item 2: LMB-drag panning is GONE (Jeroen's ruling — drag broke click
## semantics with map objects; clicks are reserved for future map objects,
## e.g. settlements). What remains: wheel zoom (unchanged) and tracking the
## local mouse position for edge-scroll (_process() reads _last_mouse_pos —
## it has no InputEvent of its own to read a live position from). WASD/arrow
## panning does NOT go through _gui_input at all — it is a HELD-key,
## continuous, frame-rate-independent pan polled every frame in _process()
## via Input.is_action_pressed()-equivalent raw key checks (Input.is_key_pressed(),
## since WASD has no project-level Input Map action of its own in this
## screen's remit — see _process()'s own doc for why raw physical-keycode
## polling is deliberate here, not a new InputMap action).
## T-1145 item 2: LMB-drag panning is GONE (drag broke click semantics with
## map objects). What remains: wheel zoom and tracking mouse position for
## edge-scroll. WASD/arrow panning does NOT go through _gui_input — it's a
## HELD-key pan polled every frame in _process().
func _gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.is_echo():
_handle_key(event as InputEventKey)
@@ -565,19 +882,9 @@ func _handle_key(event: InputEventKey) -> void:
back_pressed.emit()
## T-1145 item 2: continuous WASD/arrow-key pan + edge-scroll, both applied
## here (not _gui_input) because both are HELD-state effects (keys held down,
## cursor lingering near an edge), not discrete input events — _process()
## polls held state every frame and hands the resulting direction + this
## frame's delta to _apply_pan_delta() (split out for testability — a gdUnit
## test drives _apply_pan_delta(direction, delta) directly with a
## deterministic direction/delta instead of needing to fake Godot's global
## Input singleton reporting a key held, which is what testing THIS
## function's own Input.is_key_pressed() polling would require). Skips
## entirely while this Control is hidden (the screen is not the active
## nav-stack entry) — no wasted per-frame work for an invisible viewer, and
## no phantom panning if some other code path leaves this node in the tree
## but not shown.
## T-1145 item 2: continuous WASD/arrow-key pan + edge-scroll, both HELD-state
## effects polled every frame, handed to _apply_pan_delta() (split out for
## testability). Skips while hidden (screen not the active nav-stack entry).
func _process(delta: float) -> void:
if not visible:
return
@@ -589,15 +896,11 @@ func _process(delta: float) -> void:
_apply_pan_delta(direction, delta)
## The actual pan-tick state mutation, given an ALREADY-DECIDED (but not yet
## normalized) direction and this frame's delta — frame-rate independent
## (motion scales by `delta`, so the same speed at 30fps or 144fps), zoom-
## scaled (PAN_SPEED_CANVAS_PX_S * _view_zoom — see that constant's own doc
## for why), and pole-wall clamped (T-1142, unchanged mechanism, just fed by
## a different input source now). Sets _user_adjusted (T-1145: "WASD/edge/
## zoom all set _user_adjusted") and triggers the SAME pan-edge refetch check
## (§4) drag used to. Split from _process() specifically so a test can call
## this directly with a synthetic direction/delta — see _process()'s own doc.
## The actual pan-tick state mutation, given an ALREADY-DECIDED direction and
## this frame's delta — frame-rate independent, zoom-scaled
## (PAN_SPEED_CANVAS_PX_S * _view_zoom), pole-wall clamped (T-1142). Sets
## _user_adjusted and triggers the pan-edge refetch (§4). Split from
## _process() so a test can call it directly with a synthetic direction/delta.
func _apply_pan_delta(direction: Vector2, delta: float) -> void:
var normalized: Vector2 = direction.normalized() # diagonal isn't faster than a single axis
_user_adjusted = true
@@ -608,84 +911,25 @@ func _apply_pan_delta(direction: Vector2, delta: float) -> void:
## WASD + arrow keys, read via Input.is_key_pressed() on the PHYSICAL keycode
## (not an InputMap action): W/S/A/D on this project's global InputMap are
## already bound to move_north/move_south/move_east/move_west (gameplay
## movement, D-054 mouse-relative facing) — reusing those actions here would
## make holding W simultaneously pan this map AND queue a gameplay move
## command server-side the moment this implant screen closes back to
## gameplay (InputMapper polls Input.is_action_pressed() unconditionally,
## with no implant-occlusion guard — confirmed by reading input_mapper.gd
## directly, a genuine pre-existing gap outside this ticket's scope, not
## introduced here). Reading the raw physical keycode instead of the shared
## action name means this screen's WASD use is fully independent of
## whatever the gameplay action happens to be bound to — same key, two
## UNRELATED consumers, neither needs to know about the other. Arrow keys
## have no InputMap action bound at all (confirmed by grep across
## project.godot's [input] section), so they're conflict-free either way.
## Returns a raw (non-normalized) direction — the caller normalizes once
## after adding the edge-scroll contribution, so N+E doesn't move faster
## than N alone.
## (not an InputMap action) — see AtlasWindowGeometry.held_pan_direction()'s
## doc for the full W/S/A/D-vs-gameplay-movement rationale (moved there
## T-1153 for file-length/testability, unchanged behavior).
func _held_pan_direction() -> Vector2:
var direction := Vector2.ZERO
if Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP):
direction.y -= 1.0
if Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN):
direction.y += 1.0
if Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT):
direction.x -= 1.0
if Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT):
direction.x += 1.0
return direction
return AtlasWindowGeometry.held_pan_direction()
## T-1145 item 2: edge-scroll is suppressed (a) while the cursor is over UI
## (_is_over_ui() — the SAME helper the click-era _gui_input guard used, per
## the ticket's explicit "reuse _is_over_ui" instruction) and (b) while the
## application window itself lacks OS focus (_app_has_focus — otherwise a
## background window with the cursor left resting near its edge from a
## previous session would silently pan while the player is doing something
## else entirely; "if detectable" per the ticket, and Godot's
## NOTIFICATION_APPLICATION_FOCUS_OUT/IN make it directly detectable, see
## _notification()).
## T-1145 item 2: edge-scroll suppression — see
## AtlasWindowGeometry.is_cursor_edge_scrolling()'s doc (moved there T-1153).
func _is_cursor_edge_scrolling() -> bool:
if not _app_has_focus:
return false
if _is_over_ui(_last_mouse_pos):
return false
var sz: Vector2 = size
if sz.x <= 0.0 or sz.y <= 0.0:
return false
var pos: Vector2 = _last_mouse_pos
return (
pos.x >= 0.0
and pos.y >= 0.0
and pos.x <= sz.x
and pos.y <= sz.y
and (
pos.x < EDGE_SCROLL_MARGIN_PX
or pos.y < EDGE_SCROLL_MARGIN_PX
or pos.x > sz.x - EDGE_SCROLL_MARGIN_PX
or pos.y > sz.y - EDGE_SCROLL_MARGIN_PX
)
return AtlasWindowGeometry.is_cursor_edge_scrolling(
_app_has_focus, _is_over_ui(_last_mouse_pos), size, _last_mouse_pos, EDGE_SCROLL_MARGIN_PX
)
## Direction toward whichever edge(s) the cursor is near — same shape as
## _held_pan_direction() (a raw, un-normalized Vector2 the caller combines
## and normalizes once).
## Direction toward whichever edge(s) the cursor is near — see
## AtlasWindowGeometry.edge_scroll_direction()'s doc (moved there T-1153).
func _edge_scroll_direction() -> Vector2:
var sz: Vector2 = size
var pos: Vector2 = _last_mouse_pos
var direction := Vector2.ZERO
if pos.x < EDGE_SCROLL_MARGIN_PX:
direction.x -= 1.0
elif pos.x > sz.x - EDGE_SCROLL_MARGIN_PX:
direction.x += 1.0
if pos.y < EDGE_SCROLL_MARGIN_PX:
direction.y -= 1.0
elif pos.y > sz.y - EDGE_SCROLL_MARGIN_PX:
direction.y += 1.0
return direction
return AtlasWindowGeometry.edge_scroll_direction(size, _last_mouse_pos, EDGE_SCROLL_MARGIN_PX)
# =============================================================================
@@ -727,12 +971,9 @@ func _notification(what: int) -> void:
_position_overlay_bar()
if _legend_panel:
_legend_panel.reposition()
# T-1142: re-fit on resize too, same _user_adjusted guard as the other
# two auto-fit events (enter, first window arrival) — never fights a
# manually-adjusted view. _canvas guard matches _overlay_bar/
# _legend_panel above: NOTIFICATION_RESIZED can fire mid-_ready()
# (anchor_right/anchor_bottom assignment triggers it) BEFORE _canvas
# is constructed — confirmed the hard way (gdUnit add_child() crash).
# T-1142: re-fit on resize (_user_adjusted guard, as other auto-fit
# events). _canvas guard: NOTIFICATION_RESIZED can fire mid-_ready()
# before _canvas exists (gdUnit add_child() crash, confirmed).
if _canvas and not _user_adjusted:
_fit_and_center()
elif what == NOTIFICATION_APPLICATION_FOCUS_OUT:
@@ -1,40 +0,0 @@
class_name DistrictScreen
extends Control
## Regional district-window viewer screen for AtlasApp (T-1138, D-226 T-1124
## amendment). Thin wrapper around AtlasWindowViewer, mirroring
## RegionalScreen's own shape exactly — enter/leave are the nav interface.
##
## Entered via a click-through from AtlasViewer (the "regional" screen),
## carrying the derived DistrictPos the player clicked (§5's entry-revision:
## "pan center read as click point"). Esc goes back to "regional" (the
## planetary heightmap for the same body) — a nav.pop(), not a fresh push, so
## the planetary view's own pan/zoom-removed FIXED state is exactly where the
## player left it.
signal back_requested
var _viewer: AtlasWindowViewer = null
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_viewer = AtlasWindowViewer.new()
_viewer.name = "AtlasWindowViewer"
add_child(_viewer)
_viewer.back_pressed.connect(_on_viewer_back)
func enter(payload: Dictionary) -> void:
var body: Dictionary = payload.get("body", {})
var system: Dictionary = payload.get("system", {})
var center: Vector2i = payload.get("district_center", Vector2i.ZERO)
_viewer.enter(body, system, center)
func leave() -> void:
pass
func _on_viewer_back() -> void:
back_requested.emit()
@@ -1,30 +1,48 @@
class_name RegionalScreen
extends Control
## Regional heightmap viewer screen for AtlasApp (#844, D-191).
## Thin wrapper around AtlasViewer; enter/leave are the nav interface.
## Regional zoom-ladder screen for AtlasApp (#844, D-191; superseded T-1153 —
## D-226 T-1143-rulings amendment). Thin wrapper around AtlasWindowViewer,
## entering at the CANONICAL ORBITAL FRAME (Region rung) via enter_orbital()
## instead of AtlasViewer's retired heightmap-texture show_body() path —
## enter/leave are still the nav interface, unchanged shape.
##
## T-1152 client half: this is the ONE screen for the whole ladder now —
## there is no separate "district" nav hop for the windowed drill-down
## (D-013 "the zoom gesture owns spatial descent" restored for this seam
## means descent is a CONTINUOUS in-screen zoom, not a nav-stack push). Esc
## from anywhere in the ladder is a single nav.pop() back to whatever pushed
## "regional" (system screen) — see atlas_app.gd's _handle_key(), unchanged
## from before this ticket (it already routed Esc through nav.pop() for any
## screen that isn't "reach"/"system"-with-a-panel-open).
##
## `district_descend_requested`/`economics_link_requested` signals retire
## with AtlasViewer's click-through (the reticle/hover-to-descend affordance
## — Jeroen's ruling: retired as the SOLE entry, and no cheap click-target
## exists on the orbital Region-rung view to wire a shortcut onto yet, unlike
## a future settlement-marker click which WOULD have a natural landing
## point — see AtlasWindowViewer's own doc on why enter() still exists as a
## District-rung entry point for exactly that future wiring).
## economics_link_requested is deferred with AtlasViewer's city-click sidebar
## (see the batch report for the full list of what's deferred vs. carried).
signal back_requested
signal economics_link_requested(system_id: String)
signal district_descend_requested(district_center: Vector2i) # T-1138, forwarded from AtlasViewer
var _viewer: AtlasViewer = null
var _viewer: AtlasWindowViewer = null
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_viewer = AtlasViewer.new()
_viewer.name = "AtlasViewer"
_viewer = AtlasWindowViewer.new()
_viewer.name = "AtlasWindowViewer"
add_child(_viewer)
_viewer.back_pressed.connect(_on_viewer_back)
_viewer.economics_link_requested.connect(_on_viewer_economics_link)
_viewer.district_descend_requested.connect(_on_viewer_district_descend)
func enter(payload: Dictionary) -> void:
var body: Dictionary = payload.get("body", {})
var system: Dictionary = payload.get("system", {})
_viewer.show_body(body, system)
_viewer.enter_orbital(body, system)
func leave() -> void:
@@ -33,11 +51,3 @@ func leave() -> void:
func _on_viewer_back() -> void:
back_requested.emit()
func _on_viewer_economics_link(system_id: String) -> void:
economics_link_requested.emit(system_id)
func _on_viewer_district_descend(district_center: Vector2i) -> void:
district_descend_requested.emit(district_center)
+26
View File
@@ -352,6 +352,32 @@ Key components:
- **CauseChain** (production ECS component) — Tracks causal attribution for testable observation sequences (D-030).
- **Deterministic replay** — Server simulation is deterministic given the same seed + input sequence. Replay logs enable regression testing (#201, critical).
### Real-rendering test exception: `test_atlas_window_overlay_draw_smoke.gd`
`tests/run-godot` hardcodes `--headless`, whose dummy driver produces no usable GPU
texture output (`SubViewport.get_texture().get_image()` returns unusable data). One
file needs real pixels — `client/tests/test_atlas_window_overlay_draw_smoke.gd` (T-1153
live round 4) renders `AtlasWindowOverlay` into a `SubViewport` and asserts real terrain
pixels were composited, closing a "did anything draw at all" gap that bit twice
(a tile-mosaic coordinate bug and an unstored-texture GPU-lifetime bug, both invisible
to cache-state-only assertions). It self-detects the dummy driver
(`DisplayServer.get_name() == "headless"`) and **skips** under the standard suite —
`tests/run-godot --filter test_atlas_window_overlay_draw_smoke` reports green-with-skips,
never a false failure that would bounce the push gate. To exercise its real assertions:
```bash
godot4 --display-driver x11 --rendering-driver opengl3 \
-s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c \
-a res://tests/test_atlas_window_overlay_draw_smoke.gd
```
Same underlying constraint as `tests/visual_capture.gd` (`tests/run-visual`), which is
the project's other real-driver exception — currently broken on this branch by the
retired `AtlasViewer` API (T-1157, capture-harness redesign). This file's scenarios are
slated to migrate into that redesigned harness once T-1157 lands, folding it into
`tests/visual.json` for consistency; until then it stays a standalone gdUnit file with
its own skip guard.
## Planning store (pql)
Tickets and decisions live in **pql**, not in the old SQLite wrapper scripts. Decisions
@@ -108,6 +108,10 @@ Confirmed by direct code inspection (not aspirational): `value_noise`, `terrain_
**Split:** rungs govern *what classification granularity is available and legal to request* (a policy/cost question, §2/§10); canvas-resolution sampling governs *what density the client actually asks for*, tracking the viewport continuously. The rung-selection rule (§2) is the join: pick the coarsest legal rung whose spacing is ≤2× the current screen sample spacing.
> **Erratum (2026-07-22, T-1153 implementation — Stig; superseded same day by the round-3 model below):** the single-table-scan reading of the rule above is unsatisfiable for the District rung at any real viewport: with `CELL_PIXEL_SIZE = 16` and the server's `DISTRICT_WINDOW_MAX_N = 64` per-axis cap, the ≤2× visual-tolerance band and the window-coverage ceiling never overlap. An interim two-gate split (coverage ceiling → 2× tolerance) shipped briefly but left District structurally unreachable and Region held indefinitely during pure zoom (caught in the live eyeball rounds, never merged).
>
> **Final model (round 3, as merged):** `AtlasWindowGeometry.select_rung()` is a **unified per-rung coverage-ceiling walk**`MAX_COVERAGE_M` maps each rung to the maximum world extent its wire-capped window can span (Quarter ≈ 32.8 km, District ≈ 131 km, Region ≈ 13,107 km); selection walks finest-first and returns the first rung whose ceiling covers the current extent. The 2× visual-tolerance clause is retired: coverage, not tolerance, is the selector, which restores District as a real rung (the 33131 km extent band) and makes the ladder Quarter → District → Region → **tile mode** (extents beyond Region's single-window ceiling compose a whole-body mosaic of capped Region tiles — the ruling's progressive capped-density tiling; `compute_tile_grid()`, Lendel = 3×2). The one-tier-finer-than-a-pixel guarantee is preserved by construction at Quarter/District scales and bounded at Region scale by the wire cap — the felt-pacing knob is `CELL_PIXEL_SIZE`; widening a rung's band is a `WIRE_CAP_CELLS`/`MAX_N` budget change requiring re-measurement.
**Bounded key space, not unbounded.** A continuous field sampled at arbitrary density would give the cache an unbounded key space (client obstacle flagged in the grounding: the 24-entry exact-match LRU would thrash to ~0% hit rate). This is avoided because **requests snap to quantized rung tiers** (granularity ∈ {1, 4, …}) and integer district-aligned centers — only *display* is continuous; every *request* the wire actually carries is one of a small number of discrete shapes. **Gap flagged by critique:** as specified, `window_min_wl_m` is viewport-continuous (`E/C`) while the cache key/echo tuple is `(body, center, n, granularity)` — same key, different `min_wl`, would silently collide. Fix: **quantize `min_wl_m` to a small fixed set of bands per rung** (matching the rung's own octave bands) rather than passing a raw continuous value, and add the quantized band to both the echo and the cache key. This closes the gap without reopening the unbounded-key problem.
**Region/orbital granularity is unrepresentable in the current sketch.** `window_granularity: u32` with `spacing = DISTRICT_M / granularity` can express district (1) and quarter (4) but not region (coarser than district, i.e. granularity < 1). If the planetary rung's chosen carrier (§4) reuses this same field, the type needs to change to something that can express both directions (e.g. a signed log-scale or an explicit rung enum) — noted for the implementer, not resolved here.
+1 -1
View File
@@ -1669,7 +1669,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
**Amended 2026-07-21 (T-1145 — Jeroen, second companion hands-on, KALLAST window):** three regional-window presentation fixes, all client-only. **Cover-fit supersedes contain:** `fit_window_view()`'s zoom now derives from the LARGER viewport dimension with no margin factor (`max(viewport.x, viewport.y) / composite_native`, not the old `0.9 * min(...)`), so the square district-window composite fills a wide/tall viewport edge to edge instead of leaving side margins, with the shorter axis' data extending into pan-space (the same "cover" concept as CSS `object-fit: cover`) — the existing §4 pan-edge refetch is unaffected (it keys off the screen-center-to-DistrictPos mapping, which any fit already centers on `_held_center` by construction, so no refetch churn at rest). **WASD + edge-scroll supersedes drag-pan:** LMB-drag panning is removed entirely (Jeroen's ruling — drag conflicts with click semantics for the map objects, e.g. settlements, this window will host later); panning is now held WASD/arrow keys (continuous, frame-rate-independent, `_process`-polled, physical-keycode reads to stay independent of the project's existing `move_north`/etc. gameplay-movement InputMap actions bound to the same keys) plus edge-scrolling (cursor within ~24px of a viewport edge, suppressed over UI and while the OS window lacks focus); wheel zoom is unchanged; pole-wall (§5 amendment above) and east-west wrap (T-1142) semantics are preserved unchanged under the new input source. **Smoothing is an interim presentation, pending T-1143:** the composite renders as an `n`×`n` `Image`/`ImageTexture` (one pixel per district, the identical existing per-cell color pipeline) drawn scaled with linear filtering — the same treatment the planetary heightmap already gets — instead of `n`×`n` flat rects, so GPU bilinear sampling reads as a terrain gradient rather than hard blocks; the original crisp per-cell path survives behind a compile-time const specifically so T-1143's design pass can compare both directly, and this smoothing is **not** T-1143's answer to district-tier legibility, only a stopgap ahead of it.
**Amended 2026-07-21 (T-1143 design-pass rulings — Jeroen, after the zoom-ladder design pass, `docs/architecture/atlas-zoom-ladder-t1143.md`):** three rulings on the pass's reserved decisions. **(1) The item-(d) ceiling is opened for the Atlas ladder** — Jeroen: *"we set a new BHAG so old restrictions are up for debate."* The D-166 2026-07-21 zoom-ladder condition ("down to tile scale") is read **literally**: the Atlas windowed viewport may descend below quarter (512 m) toward block/tile granularity. This is an explicit ruling, not erosion — exactly the deliberate revisit the T-1112 §2 anti-erosion clause was hardened to force into the open. Item (d)'s substance survives in narrowed form: chunk/tile/voxel output still never appears as a **whole-body planetary map layer**, and the below-quarter rungs are implementation-gated on their own **measurement pass** (costs/wire for block and tile rungs are unmeasured — design pass §2/§7); the harness-verification path for L5 fill remains primary until that pass lands. **(2) Planetary rung wire carrier: progressive capped-density tiling** riding the generalized `district_window` carrier (granularity parameter, §3 of the design pass) — no new dense-raster wire shape, no forced tagged-envelope migration. **(3) The §5 entry click-through cut is superseded by continuous cursor-anchored zoom**: wheel-zoom carries the view from the orbital frame down through regional granularities continuously, anchored at the cursor, **with the condition that a full zoom-out resets to the original canonical planetary frame and location** (the fixed orbital framing is the ladder's top rest state, not a drifted pan state). The click-through descent and rectangle reticle are retired as the *sole* entry (T-1138's shipped mechanic stands until the continuous ladder replaces it in the same change — close inspection is never stranded, same discipline as the §5 fixed-view transition). D-013's "the zoom gesture owns spatial descent" reading is **restored** for this seam. **Wire-contract note (T-1150, PR #191 review — Tyre):** the `window_granularity` field that ruling (2) rides on expresses **finer-than-district integer multiples only** (1 = district, 4 = quarter today; each new rung is a deliberate widening of `resolve_window_granularity`'s whitelist — the single widening point; unknown values fall back to district, never trusted from the wire). Coarser-than-district reuse (the region/orbital rungs of ruling (2)'s progressive tiling) requires the design pass's R5 signed/log-scale-or-enum redesign of the field — a new magic value is not the path. Recorded here so the type's limit is contract, not only a design-doc risk row.
**Amended 2026-07-21 (T-1143 design-pass rulings — Jeroen, after the zoom-ladder design pass, `docs/architecture/atlas-zoom-ladder-t1143.md`):** three rulings on the pass's reserved decisions. **(1) The item-(d) ceiling is opened for the Atlas ladder** — Jeroen: *"we set a new BHAG so old restrictions are up for debate."* The D-166 2026-07-21 zoom-ladder condition ("down to tile scale") is read **literally**: the Atlas windowed viewport may descend below quarter (512 m) toward block/tile granularity. This is an explicit ruling, not erosion — exactly the deliberate revisit the T-1112 §2 anti-erosion clause was hardened to force into the open. Item (d)'s substance survives in narrowed form: chunk/tile/voxel output still never appears as a **whole-body planetary map layer**, and the below-quarter rungs are implementation-gated on their own **measurement pass** (costs/wire for block and tile rungs are unmeasured — design pass §2/§7); the harness-verification path for L5 fill remains primary until that pass lands. **(2) Planetary rung wire carrier: progressive capped-density tiling** riding the generalized `district_window` carrier (granularity parameter, §3 of the design pass) — no new dense-raster wire shape, no forced tagged-envelope migration. **(3) The §5 entry click-through cut is superseded by continuous cursor-anchored zoom**: wheel-zoom carries the view from the orbital frame down through regional granularities continuously, anchored at the cursor, **with the condition that a full zoom-out resets to the original canonical planetary frame and location** (the fixed orbital framing is the ladder's top rest state, not a drifted pan state). The click-through descent and rectangle reticle are retired as the *sole* entry (T-1138's shipped mechanic stands until the continuous ladder replaces it in the same change — close inspection is never stranded, same discipline as the §5 fixed-view transition). D-013's "the zoom gesture owns spatial descent" reading is **restored** for this seam. **Wire-contract note (T-1150, PR #191 review — Tyre):** the `window_granularity` field that ruling (2) rides on expresses **finer-than-district integer multiples only** (1 = district, 4 = quarter today; each new rung is a deliberate widening of `resolve_window_granularity`'s whitelist — the single widening point; unknown values fall back to district, never trusted from the wire). Coarser-than-district reuse (the region/orbital rungs of ruling (2)'s progressive tiling) requires the design pass's R5 signed/log-scale-or-enum redesign of the field — a new magic value is not the path. Recorded here so the type's limit is contract, not only a design-doc risk row. **Refinement-semantics note (T-1153, PR #192 review — Tyre):** the ladder's progressive cross-rung refinement (hold the coarse composite, fetch the finer rung, swap in place on arrival; per-tile arrival in the orbital mosaic) **extends** the T-1124 §4 float-on-center/debounce async contract — it does not supersede it. §4 still governs the per-request mechanics unchanged (`district_window: None`-until-derived polling, the 150 ms debounce, float-on-center refetch); rung crossings add a second request class on top, per the design pass §3's progressive-refinement model. This record is the one that governs the swap-on-arrival behavior. The legacy `window_granularity: u32` wire field is now fully shadowed by `window_granularity_v2` (the server always echoes both); it is **scheduled for retirement** once pre-T-1152 wire-compat is confirmed unneeded (single-repo client/server pair — no external clients exist today; ticketed).
- **Rationale:** Reusing the real UI — rather than a parallel offline renderer or dumped files — means the debug/review surface never diverges from what ships, and a dropped artifact can't go stale. Agent-navigability converts qualitative "does the synthesis look natural?" review from a manual eyeball pass into an automatable sweep that flags the few outliers for a human. The harness rides seams that already exist (`TickRate::Paused`, the paused-allowlist, `gameplay_occluded`, the bridge framing, the `run-visual` capture primitive) — a naming-and-contract exercise, not a new subsystem.
- **New surface:** server pause-gating (run-conditions on the world phases keyed to a pause command); client `AtlasAgentInterface` (`observe`/`act`, Control-tree walker) + its local transport; the generation overlay rendering + selector + legend; interactive capture wired to `run-visual`.
- **Implementation:** Phase 4 (epic T-750), built bottom-up — auto-pause substrate, T-969 proxy (D-225), T-960 viewer, agent channel, agent capture. Geography is the first consumer.
+269
View File
@@ -1569,6 +1569,128 @@ pub fn derive_at_metres(
)
}
/// The orbital-rung derivation (T-1152, zoom ladder design doc §2/§4): the
/// coarse-granularity twin of [`derive_at_metres`] that skips [`invent_primitives`]
/// entirely — **no coastline warp, no detail-scatter octave sum, no classification
/// noise call of any kind**. Per the design doc's orbital row: "`region_baseline_at_district`
/// only — bilinear blend of 4 region baselines, no `invent_primitives`, no
/// classification [driver]." Orbital sample spacing (≥205 km, D-243's region rung
/// and coarser) sits below `detail_scatter`'s own octave floor
/// (`OCTAVE_WAVELENGTHS_M`'s coarsest entry is 32,768 m ≈ 32.8 km — an order of
/// magnitude finer than a region), so the invented terrain has nothing left to
/// contribute at this spacing; calling it would burn cycles synthesizing detail
/// no orbital pixel can resolve. What DOES vary at orbital spacing is the
/// **envelope** the heightmap itself carries (the `TerrainAnalysis` continental
/// shape) and the **region climate baseline** — this function samples exactly
/// those two, nothing else.
///
/// **Cost model (design doc R1 — measure first):** one `bilinear` (elevation),
/// one `bilinear_bool` (ocean mask), one `region_baseline_at_district` call (its
/// own cost is 4×`derive_region_baseline_c` on a cache miss, O(1) on a cache hit)
/// — no octave sum, no coast-warp trig, no character/envelope computation. See
/// `server/tests/zoom_ladder_bench.rs`'s `bench_derive_orbital_at_metres` for the
/// measured per-cell figure this claim rests on.
///
/// Produces the SAME six-field tail every other rung produces (`morphology_zone`,
/// `elev_q`, `temperature_c`, `moisture_q`, `vegetation_class`, `glaciation_grade`)
/// by routing the bilinear-only primitives through the same
/// [`build_district_profile`] classification tail every other rung uses — one
/// classification pipeline, never a second orbital-only decision tree (D-227:
/// classification thresholds don't get a coarse-rung variant any more than the
/// quarter rung got its own "quarter mode" thresholds, design doc §6).
///
/// **R2 (stepped fields):** `moisture_q`/`temperature_c`/`morphology_zone`/etc.
/// are exactly as stepped here as at every other rung — `region_baseline_at_district`
/// floor-divides to the containing `DistrictPos` regardless of caller spacing (see
/// [`derive_at_metres`]'s own doc on this), so this function does not make
/// temperature MORE continuous at orbital scale; it inherits the same
/// district-tier step the design doc documents as permanent, by construction.
///
/// **`slope_q` is fixed at 0`** — the bilinear-only envelope carries no
/// per-cell slope signal at orbital spacing (`ta.slope_deg` is a district-scale
/// proxy; sampling it here would imply a precision the coarse envelope doesn't
/// have). `slope_q` only affects morphology gates 36 (FjordWall/CliffCoast/
/// BraidedDelta/DuneStrand) and the invented-primitives `carve` term this
/// function never runs — passing 0 means those gates fall through to their
/// low-slope alternatives, which is the correct behavior for a coastline sampled
/// at coarser-than-detail-scatter resolution (no invented ruggedness to report).
pub fn derive_orbital_at_metres(
seed: SeedChain,
body_id: &str,
body_params: &BodyParams,
ta: &TerrainAnalysis,
wx: f64,
wy: f64,
climate: &ClimateConstants,
) -> DistrictProfile {
// Same world-metres -> fractional heightmap pixel + latitude mapping
// derive_at_metres uses — the envelope is the SAME TerrainAnalysis grid at
// every rung, only the sampling density differs.
let (px, py, _world_x_m, _world_y_m, lat_deg) = match body_params.body_radius_km {
Some(r_km) if r_km > 0.0 => {
let circumference_m = std::f64::consts::TAU * r_km * 1000.0;
let meridian_m = std::f64::consts::PI * r_km * 1000.0;
let px = (wx / circumference_m).rem_euclid(1.0) * ta.w as f64;
let lat_frac = (wy / meridian_m).clamp(-0.5, 0.5);
let py = (0.5 + lat_frac) * ta.h.saturating_sub(1) as f64;
(px, py, wx, wy, -lat_frac * 180.0)
}
_ => {
let dm = scale::DISTRICT_M as f64;
let px = (wx / dm).clamp(0.0, ta.w.saturating_sub(1) as f64);
let py = (wy / dm).clamp(0.0, ta.h.saturating_sub(1) as f64);
let lat_deg = if ta.h > 1 {
90.0 - (py / (ta.h - 1) as f64) * 180.0
} else {
0.0
};
(px, py, px * dm, py * dm, lat_deg)
}
};
let params = BodyParams {
latitude_deg: lat_deg,
..body_params.clone()
};
// The envelope only — no coast-warp, no detail-scatter. This is exactly
// `invent_primitives`' step-1 "driver tier" raw bilinear reads, promoted to
// be the FINAL primitives instead of a one-step-stale input to invention.
let elev_q =
((bilinear(&ta.elev_pct, ta.w, ta.h, px, py) as f64 * 100.0).round() as i32).clamp(0, 100);
let ocean_fraction_q = ((bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px, py) as f64 * 100.0)
.round() as i32)
.clamp(0, 100);
// No invented ruggedness at orbital spacing (see the function doc's note
// on slope_q) — the envelope carries no per-cell slope signal this coarse.
let slope_q = 0;
let district_pos: DistrictPos = (
(wx / scale::DISTRICT_M as f64).floor() as i32,
(wy / scale::DISTRICT_M as f64).floor() as i32,
);
let region_baseline_c = region_profile::region_baseline_at_district(
seed.seed(),
body_id,
district_pos,
&params,
climate,
seed,
None, // no pre-built cache; derive on-the-fly, same posture as derive_at_metres
);
build_district_profile(
seed,
&params,
climate,
slope_q,
elev_q,
ocean_fraction_q,
region_baseline_c,
BasinDirection::default(),
)
}
/// Bilinear interpolation of a row-major `f32` field at fractional `(px, py)`.
/// Columns wrap (equirectangular); rows clamp at the poles.
fn bilinear(field: &[f32], w: usize, h: usize, px: f64, py: f64) -> f32 {
@@ -2066,6 +2188,153 @@ mod tests {
);
}
// -------------------------------------------------------------------
// derive_orbital_at_metres (T-1152, design doc §2/§4 orbital row)
// -------------------------------------------------------------------
/// Determinism (D-010/D-227): two independent orbital derives at the same
/// position produce a bit-identical `DistrictProfile`, mirroring
/// `derive_district_is_deterministic`'s pattern for the finer rungs.
#[test]
fn derive_orbital_at_metres_is_deterministic() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::REGION_M as f64;
let a = derive_orbital_at_metres(
test_seed(),
"test_body",
&p,
&ta,
3.0 * dm,
2.0 * dm,
&climate,
);
let b = derive_orbital_at_metres(
test_seed(),
"test_body",
&p,
&ta,
3.0 * dm,
2.0 * dm,
&climate,
);
assert_district_profiles_eq(&a, &b);
}
/// The orbital path must NOT run `invent_primitives` — the design doc's
/// central constraint (§2: "no invent_primitives at orbital wavelengths").
/// Direct proof: `slope_q` is always exactly 0 (invention is the only
/// source of nonzero slope_q at this call depth — see
/// `derive_orbital_at_metres`'s doc on why slope_q is fixed), sampled
/// across enough distinct positions that a nonzero value appearing even
/// once would falsify the claim.
#[test]
fn derive_orbital_at_metres_never_invents_slope() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::REGION_M as f64;
for i in 0..25 {
let wx = (i * 7) as f64 * dm * 0.37;
let wy = (i * 11) as f64 * dm * 0.29;
let prof =
derive_orbital_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate);
assert_eq!(
prof.slope_q, 0,
"orbital derive must never report invented slope (position {i})"
);
}
}
/// The orbital derive's `elev_q`/`temperature_c` must come from the SAME
/// envelope + region-baseline sources `derive_at_metres` reads — not an
/// independent/divergent computation. At a position where the invented
/// scatter happens to contribute exactly zero (impossible to guarantee by
/// construction, so this test instead checks the WEAKER, always-true
/// property: both paths' `elev_q` derive from the same underlying
/// bilinear envelope, so they must be close — within the invented
/// scatter's own bounded contribution range, not arbitrarily different).
/// This guards against the orbital path silently reading a different
/// terrain field entirely (a copy-paste bug this refactor is exactly the
/// kind of change that could introduce).
#[test]
fn derive_orbital_at_metres_elevation_tracks_the_same_envelope() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::DISTRICT_M as f64;
// Sample at a DISTRICT-aligned position (within the orbital function's
// legal domain — it accepts any world position, this just makes the
// district-mode comparison call meaningful) so both paths read the
// exact same fractional heightmap pixel.
let wx = 40.0 * dm;
let wy = 20.0 * dm;
let orbital = derive_orbital_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate);
let full = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0);
// The invented scatter is a bounded perturbation on top of the raw
// envelope (detail_scatter's amplitude is capped well under 100 elev_q
// points) — the two must be in the same ballpark, not exactly equal
// (that would defeat the point of invention existing at all at the
// finer rung) and not wildly different (that would mean the orbital
// path is reading a different field).
let elev_diff = (orbital.elev_q - full.elev_q).abs();
assert!(
elev_diff <= 50,
"orbital elev_q ({}) and full-derive elev_q ({}) must come from the \
same envelope, not diverge arbitrarily",
orbital.elev_q,
full.elev_q
);
}
/// Orbital-scale windows must still fill all six dense wire arrays the
/// client's colorizer family reads (T-1152: "the orbital cells must fill
/// the same six dense arrays the DistrictWindowLayer carries") — this is
/// checked at the `DistrictProfile` level (the pre-packing source of
/// those six fields): every field the packer reads
/// (`morphology_zone`/`elev_q`/`temperature_c`/`moisture_q`/
/// `vegetation_class`/`glaciation_grade`) must be populated the same way
/// regardless of rung — this test asserts the orbital output is a
/// legitimate `DistrictProfile`, not a partially-filled stand-in.
#[test]
fn derive_orbital_at_metres_populates_all_six_wire_fields() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::REGION_M as f64;
let prof = derive_orbital_at_metres(
test_seed(),
"test_body",
&p,
&ta,
5.0 * dm,
3.0 * dm,
&climate,
);
assert!((0..=100).contains(&prof.elev_q));
assert!((0..=100).contains(&prof.moisture_q));
// temperature_c is Some for a breathable-atmosphere body (earth_params).
assert!(prof.temperature_c.is_some());
// morphology_zone/vegetation_class/glaciation_grade are enums with no
// "unset" state — successfully constructing the DistrictProfile at
// all (no panic) is the actual assertion; the field reads below just
// confirm they're reachable typed values, matching the discipline
// `derive_district_is_deterministic` and neighbours already use.
let _ = prof.morphology_zone;
let _ = prof.vegetation_class;
let _ = prof.glaciation_grade;
}
#[test]
fn derive_district_profile_is_deterministic() {
let hm = test_hm();
+100 -42
View File
@@ -34,7 +34,9 @@ use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
use crate::atlas::district_profile::{BodyParams, ClimateConstants, DistrictPos};
use crate::atlas::features::TerrainAnalysis;
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use crate::atlas::layer_proxy::{build_district_window_layer, DistrictWindowLayer};
use crate::atlas::layer_proxy::{
build_district_window_layer, DistrictWindowLayer, WindowGranularity,
};
use crate::atlas::shell::{fill_chunk, FilledChunk};
use crate::atlas::skeleton_gen::{assign_all_block_tags, generate_quarter_skeleton};
use crate::atlas::trait_catalog_reader::ExteriorCatalog;
@@ -208,11 +210,18 @@ pub enum GenWorkItem {
/// before this item is built — never trusted from the wire again here.
center: DistrictPos,
n: u32,
/// Derivation granularity (T-1150) — `WINDOW_GRANULARITY_DISTRICT` (1)
/// or `WINDOW_GRANULARITY_QUARTER` (4). Already resolved via
/// `resolve_window_granularity` by the caller.
granularity: u32,
/// Derivation granularity (T-1150, widened T-1152 to the full
/// [`WindowGranularity`] vocabulary — `District`/`Quarter` (finer)
/// plus `Region` (coarser, T-1152)). Already resolved via
/// `resolve_window_granularity_v2` by the caller — this is a
/// concrete rung, never a raw wire value.
granularity: WindowGranularity,
/// Octave cutoff in whole metres (T-1149/T-1150), `0` = no cutoff.
/// Meaningless for `granularity: Region` (`derive_orbital_at_metres`
/// never calls the octave-scatter path this cuts off) but still
/// carried and echoed uniformly — see `derive_orbital_at_metres`'s
/// doc for why the field is harmless-but-unused there, not
/// special-cased away.
min_wl_m: u32,
},
}
@@ -227,13 +236,17 @@ impl GenWorkItem {
/// Coalescing key for `DeriveWindow` items only — `(connection, body,
/// granularity)` (T-1150, design doc §3 [SOFT] recommendation, extending
/// T-1137's `(connection, body)`). `granularity` is part of the key so an
/// in-flight district-spacing (granularity 1) pan-burst is never
/// superseded by an unrelated quarter-spacing (granularity 4) request for
/// the same connection+body, and vice versa — the two rungs are separate
/// in-flight derives, not competing updates to the same one.
/// T-1137's `(connection, body)`; widened T-1152 to carry the full
/// [`WindowGranularity`] enum rather than the legacy `u32`, so `Region`
/// occupies its own coalescing slot exactly like `District`/`Quarter` do
/// — this is one of the five T-1150 touch points the R5 redesign must
/// carry the new representation through). `granularity` is part of the
/// key so an in-flight district-spacing pan-burst is never superseded by
/// an unrelated quarter- or region-spacing request for the same
/// connection+body, and vice versa — every rung is a separate in-flight
/// derive, not a competing update to the same one.
/// `None` for every other variant (they don't coalesce this way).
pub fn window_supersede_key(&self) -> Option<(ConnectionId, &str, u32)> {
pub fn window_supersede_key(&self) -> Option<(ConnectionId, &str, WindowGranularity)> {
if let GenWorkItem::DeriveWindow {
body_id,
conn_id,
@@ -1150,19 +1163,14 @@ mod tests {
/// coalescing tests can exercise the granularity axis of
/// `window_supersede_key()` without a second near-duplicate helper.
fn derive_window(body_id: &str, conn_id: ConnectionId, center: DistrictPos) -> GenWorkItem {
derive_window_at(
body_id,
conn_id,
center,
crate::atlas::layer_proxy::WINDOW_GRANULARITY_DISTRICT,
)
derive_window_at(body_id, conn_id, center, WindowGranularity::District)
}
fn derive_window_at(
body_id: &str,
conn_id: ConnectionId,
center: DistrictPos,
granularity: u32,
granularity: WindowGranularity,
) -> GenWorkItem {
GenWorkItem::DeriveWindow {
body_id: body_id.to_string(),
@@ -1316,21 +1324,11 @@ mod tests {
let conn = ConnectionId(9);
q.submit_window(
derive_window_at(
"GranBody",
conn,
(0, 0),
crate::atlas::layer_proxy::WINDOW_GRANULARITY_DISTRICT,
),
derive_window_at("GranBody", conn, (0, 0), WindowGranularity::District),
GenPriority::Immediate,
);
q.submit_window(
derive_window_at(
"GranBody",
conn,
(0, 0),
crate::atlas::layer_proxy::WINDOW_GRANULARITY_QUARTER,
),
derive_window_at("GranBody", conn, (0, 0), WindowGranularity::Quarter),
GenPriority::Immediate,
);
assert_eq!(
@@ -1352,21 +1350,11 @@ mod tests {
let conn = ConnectionId(11);
q.submit_window(
derive_window_at(
"SameGranBody",
conn,
(0, 0),
crate::atlas::layer_proxy::WINDOW_GRANULARITY_QUARTER,
),
derive_window_at("SameGranBody", conn, (0, 0), WindowGranularity::Quarter),
GenPriority::Immediate,
);
q.submit_window(
derive_window_at(
"SameGranBody",
conn,
(5, 5),
crate::atlas::layer_proxy::WINDOW_GRANULARITY_QUARTER,
),
derive_window_at("SameGranBody", conn, (5, 5), WindowGranularity::Quarter),
GenPriority::Immediate,
);
assert_eq!(
@@ -1376,6 +1364,76 @@ mod tests {
);
}
/// **PR #192 review — Hoshe 1: zero coalescing coverage for
/// `WindowGranularity::Region` before this test**, despite Region being
/// the highest-fan-out path (progressive capped-density tiling fires
/// multiple concurrent Region `DeriveWindow` items per pan/zoom). Mirrors
/// `submit_window_does_not_coalesce_different_granularity`'s pattern
/// exactly, substituting Region for Quarter: a Region request and a
/// District request for the SAME `(connection, body)` are separate
/// in-flight slots (the coalescing key is `(conn_id, body_id,
/// granularity)`) and must NOT coalesce — both survive as independent
/// pending items.
#[test]
fn submit_window_does_not_coalesce_region_and_district() {
let q = GenerationQueue::with_threads(1);
// See `submit_window_coalesces_same_connection_and_body`'s comment on
// why the occupier must be `analyze()`, not `FillChunk`.
q.submit(analyze("Occupier5"), GenPriority::Low);
let conn = ConnectionId(13);
q.submit_window(
derive_window_at("OrbitalGranBody", conn, (0, 0), WindowGranularity::Region),
GenPriority::Immediate,
);
q.submit_window(
derive_window_at("OrbitalGranBody", conn, (0, 0), WindowGranularity::District),
GenPriority::Immediate,
);
assert_eq!(
q.pending_count(),
2,
"same (connection, body) but Region vs. District must NOT coalesce — \
separate in-flight slots, same as the existing District/Quarter pair"
);
}
/// The coalescing-DOES-happen counterpart to the test above, for Region
/// specifically: two submissions for the SAME `(connection, body,
/// Region)` still collapse to one pending item — confirms Region's
/// coalescing key behaves identically to District/Quarter's, not just
/// that it avoids cross-granularity aliasing.
#[test]
fn submit_window_coalesces_same_connection_body_and_region_granularity() {
let q = GenerationQueue::with_threads(1);
q.submit(analyze("Occupier6"), GenPriority::Low);
let conn = ConnectionId(15);
q.submit_window(
derive_window_at(
"SameOrbitalGranBody",
conn,
(0, 0),
WindowGranularity::Region,
),
GenPriority::Immediate,
);
q.submit_window(
derive_window_at(
"SameOrbitalGranBody",
conn,
(5, 5),
WindowGranularity::Region,
),
GenPriority::Immediate,
);
assert_eq!(
q.pending_count(),
1,
"same (connection, body, Region) must still coalesce to one pending item"
);
}
// -------------------------------------------------------------------
// TerrainAnalysisCache (T-1137, PR #187 review — Tyre C1)
// -------------------------------------------------------------------
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -418,7 +418,7 @@ fn drain_generation_completions(
body_id,
layer.center,
layer.n,
layer.granularity,
layer.granularity_v2,
layer.min_wl_m,
),
*layer,
@@ -914,6 +914,7 @@ mod tests {
window_center: None,
window_n: 0,
window_granularity: 0,
window_granularity_v2: None,
window_min_wl_m: 0,
},
)]));
+2
View File
@@ -1202,6 +1202,7 @@ mod inbound_tests {
window_center: None,
window_n: 0,
window_granularity: 0,
window_granularity_v2: None,
window_min_wl_m: 0,
};
let frame = rmp_serde::to_vec_named(&req).unwrap();
@@ -1249,6 +1250,7 @@ mod inbound_tests {
window_center: None,
window_n: 0,
window_granularity: 0,
window_granularity_v2: None,
window_min_wl_m: 0,
})
.unwrap();
+1
View File
@@ -372,6 +372,7 @@ fn single_tick_drains_all_ready_inbound_frames() {
window_center: None,
window_n: 0,
window_granularity: 0,
window_granularity_v2: None,
window_min_wl_m: 0,
};
let payload = rmp_serde::to_vec_named(&req).expect("failed to serialize");
+2 -1
View File
@@ -6,7 +6,7 @@ use settled_reach_server::atlas::layer1::Layer1Output;
use settled_reach_server::atlas::layer_proxy::{
AtlasLayerResponse, AtlasLayerStatus, DistrictWindowLayer, QuarterFootprintEntry,
QuarterFootprintLayer, RegionGridLayer, RoadGraphEdge, RoadGraphLayer, RoadGraphNode,
SettlementEntry, SettlementLayer, SettlementSizeClass, REGION_TEMP_NONE_DC,
SettlementEntry, SettlementLayer, SettlementSizeClass, WindowGranularity, REGION_TEMP_NONE_DC,
WINDOW_GRANULARITY_DISTRICT,
};
use settled_reach_server::atlas::region_profile::{SeasonPhase, WeatherState};
@@ -727,6 +727,7 @@ fn generate_atlas_layer_response_fixtures() {
center: (10, -5),
n: 2,
granularity: WINDOW_GRANULARITY_DISTRICT,
granularity_v2: WindowGranularity::District,
min_wl_m: 0,
morphology: vec![0, 8, 14, 16], // OpenOcean, AlluvialPlain, Alpine, Wetland
elev_q: vec![0, 45, 98, 60],
+188 -1
View File
@@ -17,11 +17,14 @@
use std::time::Instant;
use settled_reach_server::atlas::district_profile::{
derive_at_metres, BodyParams, ClimateConstants,
derive_at_metres, derive_orbital_at_metres, BodyParams, ClimateConstants,
};
use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::features::TerrainAnalysis;
use settled_reach_server::atlas::heightmap::BodyHeightmap;
use settled_reach_server::atlas::layer_proxy::{
build_district_window_layer, WindowGranularity, DISTRICT_WINDOW_MAX_N_REGION, WIRE_CAP_CELLS,
};
use settled_reach_server::atlas::scale;
use settled_reach_server::seed::{SeedChain, SeedDomain};
@@ -151,3 +154,187 @@ fn bench_derive_at_metres_district_and_quarter_spacing() {
println!();
}
/// Time `n_cells` sequential `derive_orbital_at_metres` calls — the
/// region-baseline-blend-only path (T-1152, design doc §2/§4/§9 R1), no
/// `invent_primitives` call at any point. Mirrors `time_derive_sweep`'s shape
/// exactly so the two numbers are directly comparable.
fn time_orbital_sweep(
seed: SeedChain,
body_id: &str,
params: &BodyParams,
ta: &TerrainAnalysis,
climate: &ClimateConstants,
grid_side: u32,
step_m: f64,
) -> (std::time::Duration, f64) {
let n_cells = (grid_side * grid_side) as u64;
let t0 = Instant::now();
for row in 0..grid_side {
for col in 0..grid_side {
let wx = col as f64 * step_m;
let wy = row as f64 * step_m;
let prof = derive_orbital_at_metres(seed, body_id, params, ta, wx, wy, climate);
std::hint::black_box(prof.elev_q);
}
}
let elapsed = t0.elapsed();
let per_cell_ns = elapsed.as_secs_f64() * 1e9 / n_cells as f64;
(elapsed, per_cell_ns)
}
/// T-1152 / design doc §9 R1: "MEASURE FIRST" — per-cell cost of the
/// orbital-mode region-baseline-blend-only path (no `invent_primitives`) at
/// coarse (region-scale, ≥205 km) spacings, plus a realistic full-orbital-frame
/// extrapolation (1600×900 canvas). This is the number the design doc's §4/§7
/// planetary-rung cost story rested on as an UNMEASURED extrapolation —
/// this test replaces "extrapolated from the uncut per-cell rate" with an
/// actually-measured orbital-path rate.
#[test]
#[ignore]
fn bench_derive_orbital_at_metres_region_spacing() {
let hm = bench_hm();
let ta = bench_ta(&hm);
let params = bench_params();
let climate = ClimateConstants::default();
let seed = SeedChain::root(99).derive(SeedDomain::Body, 1);
let grid_side = 64u32; // 4,096 cells/sweep, same shape as the district/quarter sweeps above
println!("\n=== T-1152 orbital-rung derive_orbital_at_metres benchmark ===");
println!(
"grid: {grid_side}x{grid_side} = {} cells/sweep\n",
grid_side * grid_side
);
let region_m = scale::REGION_M as f64;
// Region spacing (204,800 m) — the coarsest named rung short of the
// planet-wide elastic seam (D-243).
let (elapsed, per_cell_ns) =
time_orbital_sweep(seed, "bench", &params, &ta, &climate, grid_side, region_m);
println!(
"orbital, region spacing (204.8km): {:>8.2} ms total, {:>7.1} ns/cell ({:.3} µs/cell)",
elapsed.as_secs_f64() * 1000.0,
per_cell_ns,
per_cell_ns / 1000.0
);
// Same spacing, for direct comparison: the FULL derive_at_metres path
// (invent_primitives included) at the SAME region spacing — quantifies
// exactly what skipping invention buys, at the spacing where it matters.
let (elapsed_full, per_cell_ns_full) = time_derive_sweep(
seed, "bench", &params, &ta, &climate, grid_side, region_m, 0.0,
);
println!(
"district-mode (full derive_at_metres) at region spacing: {:>8.2} ms total, {:>7.1} ns/cell ({:.3} µs/cell)",
elapsed_full.as_secs_f64() * 1000.0,
per_cell_ns_full,
per_cell_ns_full / 1000.0
);
println!(
"orbital speedup vs. full derive at the same spacing: {:.2}x\n",
per_cell_ns_full / per_cell_ns
);
// Realistic full-orbital-frame estimate: a 1600x900 canvas at
// ~1-2 px/cell equivalents (design doc §4's worked example resolution
// class). Single-thread extrapolation from the MEASURED per-cell rate —
// labelled as an extrapolation, not claimed as independently measured at
// full canvas size (the parallel/chunked throughput is a SEPARATE
// measurement, T-1151's row-chunked par_iter, already landed and reused
// unchanged by the orbital rung's serving path — see the ticket report).
for (label, px_per_cell) in [("1 px/cell", 1u32), ("2 px/cell", 2u32)] {
let cols = 1600 / px_per_cell;
let rows = 900 / px_per_cell;
let cells = (cols as u64) * (rows as u64);
let est_ms = cells as f64 * per_cell_ns / 1e6;
println!(
"full-canvas 1600x900 @ {label} ({cols}x{rows} = {cells} cells): \
{est_ms:.1} ms single-thread (EXTRAPOLATED from the measured per-cell rate above)"
);
}
println!();
}
/// **T-1152 R1 — the number that actually governs interactive latency**, as
/// opposed to the full-canvas single-shot extrapolation above (which the
/// design doc's own carrier ruling makes moot — Jeroen's ruling is
/// progressive capped-density TILING, never a whole-canvas one-shot derive).
/// This measures a single served Region-granularity window tile through the
/// REAL production path (`build_district_window_layer`, including its
/// row-chunked `par_iter`, T-1151) at the wire-size cap — the same function
/// `serve_district_window`/`run_work_item`'s `DeriveWindow` arm calls, not a
/// hand-rolled sweep. This is the measured (not extrapolated) parallel
/// number the design doc's §7 flagged as missing ("no chunked-par_iter
/// benchmark has been run").
#[test]
#[ignore]
fn bench_served_region_window_tile_at_wire_cap() {
let hm = bench_hm();
let ta = bench_ta(&hm);
let params = bench_params();
let climate = ClimateConstants::default();
let seed = SeedChain::root(99).derive(SeedDomain::Body, 1);
println!("\n=== T-1152 served Region-window-tile benchmark (real production path) ===");
// The largest n the server will ever actually derive at Region
// granularity is DISTRICT_WINDOW_MAX_N_REGION, clamped further by
// clamp_window_n_v2 to the WIRE_CAP_CELLS ceiling — use the SAME
// capped n a real client's oversized request would resolve to.
let n = DISTRICT_WINDOW_MAX_N_REGION;
// Warm-up call (first call on a body pays no extra cost here since ta is
// already built — this just avoids counting one-time allocator warm-up
// noise in the timed sample).
let _ = build_district_window_layer(
seed,
"bench",
&params,
&ta,
(0, 0),
n,
&climate,
WindowGranularity::Region,
0,
);
let iterations = 20;
let t0 = Instant::now();
let mut last_side = 0usize;
for _ in 0..iterations {
let layer = build_district_window_layer(
seed,
"bench",
&params,
&ta,
(0, 0),
n,
&climate,
WindowGranularity::Region,
0,
);
last_side = (layer.morphology.len() as f64).sqrt().round() as usize;
std::hint::black_box(layer.elev_q.len());
}
let elapsed = t0.elapsed();
let per_call_ms = elapsed.as_secs_f64() * 1000.0 / iterations as f64;
println!(
"n={n} (DISTRICT_WINDOW_MAX_N_REGION), derived {last_side}x{last_side} region cells \
({} cells, WIRE_CAP_CELLS={WIRE_CAP_CELLS}):",
last_side * last_side
);
println!(
" {iterations} calls, {:.2} ms total, {per_call_ms:.3} ms/call \
(row-chunked par_iter, {} Rayon threads available)",
elapsed.as_secs_f64() * 1000.0,
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(0)
);
println!(
" compare: shipped district n=64 cap measures ~5 ms/call (design doc §7, MEASURED)\n"
);
}