fix(client): T-1153/T-1152 round 3 — unified rung coverage model, reselect clamp mirror, orbital tile-set mosaic

Bug A (zoom reselection dead): select_rung() redesigned from the two-gate
split to a unified per-rung coverage-ceiling walk (MAX_COVERAGE_M,
finest-first) — which also restores District as a reachable rung
(33-131km extent band; the two-gate model had made it structurally
unreachable). Plus the THIRD instance of the PR #191 C1 clamp-mirror bug
class: _maybe_reselect_rung fed the old rung's _held_n raw into the new
rung's request — the server clamped, the held value didn't, every
cross-rung response stale-dropped. Re-clamped at the boundary; the
leaving_tile_mode flag also fixes a stale request-granularity edge case.

Bug B (orbital = whole body): genuine tile-set model. compute_tile_grid
composes ceil-divided Region-tile centers (canonicalized column wrap,
row clamp, Dictionary-set dedup; Lendel = 3x2 = 6 tiles);
AtlasWindowTileSet owns one AtlasWindowRequest per tile (reusing all
tested request/cache/debounce machinery), fans responses out, per-tile
tile_ready — genuinely progressive; the overlay draws arrived tiles as a
mosaic under the one view transform. enter_orbital branches to tile mode
when >1 tile; single-window path unchanged below the coverage ceiling.
E/W wrap at tile seams live-confirmed (col -6400 wraps to 12739). Pole-
row dedup verified correct but flagged honestly as currently-unreachable
defensive logic (no real radius triggers row collision).

All fixes + fallout verified via revert/confirm-fails/restore. Targeted
suites 256/256; gdlint clean; new 20-test tile-set suite.
This commit is contained in:
2026-07-22 13:03:38 +02:00
parent 11f7927f24
commit 6195fd28b3
8 changed files with 1330 additions and 533 deletions
+189 -110
View File
@@ -268,84 +268,75 @@ func test_pole_wall_rows_half_matches_canonicalize_rows_half() -> void:
# =============================================================================
# T-1153: select_rung() — the §5 rung-selection rule, split into TWO tests
# per select_rung()'s own doc: a COVERAGE ceiling decides Region (can a
# District window even span this much world), and the `2x` visual-tolerance
# rule (design doc §5: "select the coarsest rung whose cell spacing <=
# 2*(E/C)") decides District vs. Quarter for whatever's under that ceiling.
# 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).
# =============================================================================
## A tight sample spacing (deep zoom-in — small E over a large C) must select
## Quarter (512 m), the finest legal rung — 2*(E/C) is far below District's
## 2,048 m spacing at this ratio.
func test_select_rung_picks_quarter_at_a_tight_sample_spacing() -> void:
# E=2000m over C=1000px -> sample spacing 2 m/px -> threshold 4 m. Even
# Quarter (512 m) is coarser than the threshold, so select_rung() falls
# through to the FINEST legal rung (its own documented fallback) rather
# than returning something even finer that doesn't exist — Quarter.
## 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")
## A sample spacing that satisfies BOTH District's own `2x` band AND the
## coverage ceiling selects District — the coarsest rung whose spacing still
## satisfies the fine-end rule, without exceeding what a District window can
## physically cover.
func test_select_rung_picks_district_at_a_moderate_sample_spacing() -> void:
# E=120,000m (under the 64*2048=131,072m coverage ceiling) over C=100px ->
# threshold = 2*120000/100 = 2,400m — satisfies District's 2,048m spacing.
var rung: String = AtlasWindowGeometry.select_rung(120_000.0, 100.0)
## 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 the COVERAGE ceiling (more world than a District window can
## physically span, regardless of how generous the visual tolerance would
## otherwise be) must select Region — the coverage test, not the `2x` visual
## one, is what decides this (select_rung()'s own doc: "the coverage ceiling
## wins whenever the two disagree").
func test_select_rung_picks_region_past_the_coverage_ceiling() -> void:
# E = full Earth-like circumference (~40,075 km) — far past the
# 64*2048=131,072m District coverage ceiling regardless of canvas_px.
## 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 the coverage ceiling (E == 64*2048 = 131,072m) must still
## select District if the `2x` band also agrees — the ceiling is `>`, not
## `>=`, so the boundary value itself stays under District's own test.
func test_select_rung_coverage_ceiling_boundary_stays_district() -> void:
## 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 the coverage ceiling must flip to Region — confirms the
## ceiling actually bites right at its own boundary, not one district-window
## short of it.
func test_select_rung_one_past_the_coverage_ceiling_is_region() -> void:
## 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")
## Exactly AT District's `2x` threshold (spacing_m == 2*(E/C)) must select
## District, not the next-finer rung — the rule is `<=`, not `<`.
func test_select_rung_district_threshold_boundary_is_inclusive() -> void:
# District spacing = 2048 m. Choose E/C such that 2*(E/C) == 2048 exactly:
# E=1024, C=1.0 -> E/C=1024 -> threshold=2048. E=1024 is also comfortably
# under the coverage ceiling (131,072), so the `2x` test is what's
# actually being exercised here.
var rung: String = AtlasWindowGeometry.select_rung(1024.0, 1.0)
assert_str(rung).is_equal("District")
## Degenerate canvas_px (<=0, an unlaid-out viewport) must fall back to the
## FINEST rung, never crash or pick the coarsest by dividing by zero — the
## documented "under-resolve is the safe failure direction" disposition (and
## must be checked BEFORE the coverage ceiling could otherwise route a
## degenerate small extent toward Region by accident).
func test_select_rung_degenerate_canvas_px_falls_back_to_finest() -> void:
var rung: String = AtlasWindowGeometry.select_rung(1000.0, 0.0)
assert_str(rung).is_equal("Quarter")
## 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
@@ -363,16 +354,26 @@ 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)
## The exact scenario that surfaced the coverage-vs-visual-tolerance
## distinction (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 — this is the direct regression guard for the bug this
## implementation found and fixed (an earlier version of select_rung()
## selected 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).
## 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)
@@ -390,50 +391,29 @@ func test_select_rung_at_orbital_fit_zoom_selects_region() -> void:
).is_equal("Region")
## Pinned capture-resolution boundary numbers (1600x900, the coordinator's
## requested eyeball-capture viewport) — a live executable regression guard
## for select_rung()'s own doc's worked example. Region releases District's
## coverage ceiling at _view_zoom ~= 1.5625; District's own `2x` band edge
## sits at _view_zoom ~= 0.125 — i.e. BELOW (not above) the coverage-ceiling
## crossing, confirming the two never overlap at this (or any real) canvas
## size — see select_rung()'s "Tuning knobs" paragraph for what would need
## to change (DISTRICT_WINDOW_MAX_N, a server-side wire-budget change) to
## open a real District band.
func test_select_rung_1600x900_region_district_boundary_zoom() -> void:
var viewport := Vector2(1600.0, 900.0)
var canvas_px: float = maxf(viewport.x, viewport.y)
var boundary_zoom := 1.5625
var just_inside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 1.001, viewport
)
var just_outside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 0.999, viewport
)
assert_str(AtlasWindowGeometry.select_rung(just_inside, canvas_px)).override_failure_message(
"zoomed IN past ~1.5625 at 1600x900 must have released the Region coverage ceiling"
).is_not_equal("Region")
assert_str(AtlasWindowGeometry.select_rung(just_outside, canvas_px)).override_failure_message(
"zoomed OUT past ~1.5625 at 1600x900 must still be under the Region coverage ceiling"
).is_equal("Region")
func test_select_rung_1600x900_district_quarter_boundary_zoom_confirms_no_overlap() -> void:
var viewport := Vector2(1600.0, 900.0)
var canvas_px: float = maxf(viewport.x, viewport.y)
var boundary_zoom := 0.125
var just_inside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 1.001, viewport
)
var just_outside: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, boundary_zoom * 0.999, viewport
)
# Both sides of the District/Quarter `2x`-band boundary read "Region" at
# 1600x900, NOT "District" — confirming the coverage ceiling (which
# releases at zoom~=1.5625, far above this boundary) has already forced
# Region long before the `2x` band's own edge is reached. This is the
# literal "no overlap" finding, pinned as an executable assertion.
assert_str(AtlasWindowGeometry.select_rung(just_inside, canvas_px)).is_equal("Region")
assert_str(AtlasWindowGeometry.select_rung(just_outside, canvas_px)).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")
# =============================================================================
@@ -598,3 +578,102 @@ func test_edge_scroll_direction_points_west_near_left_edge() -> void:
)
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))
+11 -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(
+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)
+142 -87
View File
@@ -73,81 +73,64 @@ func test_enter_orbital_requests_region_granularity() -> void:
assert_str(v._held_granularity_v2).is_equal("Region")
## enter_orbital()'s `n` intent is the body's full equatorial circumference
## in districts (district_extent().cols) — the whole body fitted to the
## canvas, per Jeroen's HARD condition wording — BUT `_held_n` is what
## actually gets STORED/SENT, and that must be the CLAMPED value
## (live-round finding: the raw cols value, routinely tens of thousands at
## Region granularity, was stored unclamped while AtlasWindowRequest clamped
## before sending — see _enter_at_rung()'s own doc for the full C1-one-layer-up
## story). GJ380c's real cols (~19,139 per the live repro) exceeds
## DISTRICT_WINDOW_MAX_N_REGION's clamp ceiling, so this body is the exact
## regression case, not a hypothetical.
func test_enter_orbital_n_is_the_clamped_value_not_raw_circumference() -> void:
## **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) — the live-repro body
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}, {})
var expected_clamped: int = AtlasWindowRequest._clamp_window_n_mirror_v2(raw_cols, "Region")
var failure_msg: String = (
"_held_n must be the CLAMPED n (%d), matching what the server will echo —"
+ " not the raw circumference (%d), which the server would clamp down and"
+ " every response would then fail the w_n != _held_n staleness check"
) % [expected_clamped, raw_cols]
assert_int(v._held_n).override_failure_message(failure_msg).is_equal(expected_clamped)
# GJ380c's raw circumference must actually exceed the clamp — otherwise this
# test would pass trivially without exercising the clamp at all.
assert_int(raw_cols).override_failure_message(
"GJ380c's raw district-column count must exceed the Region clamp ceiling"
+ " for this to be a real regression guard, not a no-op"
).is_greater(expected_clamped)
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 regression, end to end (fix #1: the n-clamp mirror one
## layer up):** enter_orbital() on a real-sized body (GJ380c/Lendel, radius
## 6238.4 km, raw cols far past the Region clamp ceiling) followed by a
## server response echoing the CLAMPED n + "Region" granularity must be
## ACCEPTED and become the held window — not silently dropped as stale
## forever (the exact live bug: `wv._held_n = 19139` vs. echoed `6400`,
## blank ladder on every real-sized body). This is the round-trip the
## existing suite never exercised — every prior enter_orbital() test
## asserted on request-side state only, never delivered a response.
##
## **WIRE-ACCURATE response shape (fix #2, second live-round finding):** the
## response dict below carries `"granularity":
## SERVER_LEGACY_GRANULARITY_REGION_SENTINEL` explicitly — the ACTUAL byte a
## real server sends, not the field's absence. The first version of this
## test omitted the legacy key entirely, which let `w.get("granularity",
## DEFAULT)` silently default to `1` (matching `_granularity`'s own pinned
## value) — an ACCIDENTAL pass that never exercised the real sentinel
## mismatch, exactly the class of gap the live round exists to catch. This
## version fails without the v2-authoritative-when-present fix in
## `_echoed_granularity_matches()`.
func test_enter_orbital_oversized_body_accepts_the_clamped_region_response() -> void:
## **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
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var raw_cols: int = int(extent["cols"])
var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(raw_cols, "Region")
# Sanity: this body must actually need clamping, or the test proves nothing.
assert_int(raw_cols).is_greater(clamped_n)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_that(v.get_district_window()).override_failure_message(
"no response delivered yet — must still be null"
).is_null()
assert_bool(v.is_tile_mode()).is_true()
# The server's REAL response: echoes the CLAMPED n, "Region" granularity_v2
# (String), center (0,0), AND the legacy sentinel in "granularity" — exactly
# what handle_atlas_request/clamp_window_n_v2 actually produces on the wire
# for an oversized orbital request (confirmed against Dudley's contract).
var region_window: Dictionary = {
"center": [0, 0],
"n": clamped_n,
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]),
@@ -157,19 +140,12 @@ func test_enter_orbital_oversized_body_accepts_the_clamped_region_response() ->
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", region_window))
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", tile_window))
var failure_msg: String = (
"a response echoing the server's own clamped n + Region granularity_v2 (with"
+ " the legacy sentinel u32::MAX in the old granularity slot) must be ACCEPTED"
+ " and become the held window — the live bug left this permanently null"
+ " (w_n=%d never matched a stale unclamped _held_n=%d, THEN the legacy"
+ " sentinel never matched the stored _granularity=1) on every real-sized body"
) % [clamped_n, raw_cols]
assert_that(v.get_district_window()).override_failure_message(failure_msg).is_equal(
region_window
)
assert_str(v._held_granularity_v2).is_equal("Region")
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 —
@@ -364,19 +340,20 @@ func test_zoom_out_past_district_threshold_requests_a_coarser_rung() -> void:
assert_that(v.get_district_window()).is_equal(district_window)
## Zooming IN on a District-rung window (well within its own legal spacing
## band) 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 legal 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). District's legal
## band (select_rung()'s own doc: the coverage ceiling and the `2x` visual
## band only overlap at small viewports — `canvas_px <= DISTRICT_WINDOW_MAX_N
## * DISTRICT_SPACING_M / 1024 = 128px`) requires a SMALL viewport here,
## unlike most of this suite's 800x600/1920x1080 fixtures.
## 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)
@@ -394,6 +371,84 @@ func test_zoom_in_within_district_threshold_does_not_change_rung() -> void:
).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)
# =============================================================================
# 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
@@ -15,7 +15,14 @@ extends RefCounted
## 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.
## 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
@@ -40,11 +47,66 @@ const RUNG_TABLE: Array = [
## 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). select_rung() uses this
## to answer "CAN a District-granularity window even cover this much world at
## all" — the coarse-end ceiling, distinct from the fine-end `2x` tolerance.
## 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
## districts, compute the zoom/offset that COVERS the viewport (fills it edge
@@ -160,90 +222,61 @@ static func clamp_pan_offset_to_pole_wall(
# =============================================================================
## Rung-selection rule (design doc §5, restated): for a world extent `E`
## metres shown across canvas `C` px, sample spacing is `E/C`. Select the
## COARSEST rung whose cell spacing is `<= 2*(E/C)` — one tier finer than a
## screen pixel, never coarser (never magnified-interpolation of a coarser
## composite, the literal thing the D-166 corollary forbids). This `2x`
## tolerance gates the FINE end only (District vs. Quarter) — see the
## coarse-end paragraph below for why Region is decided by a DIFFERENT test.
## 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).
##
## **Region is selected by a COVERAGE test, not the `2x` visual tolerance**
## (found live-testing the orbital-entry fit zoom, where E covers a whole
## planetary circumference). The `2x` formula is calibrated to catch the
## ZOOM-IN failure mode the corollary names explicitly — never request
## coarser derivation than the screen can currently resolve — and has no
## meaningful symmetric zoom-OUT reading: testing Region's own 204.8 km
## spacing against the SAME threshold that gates District/Quarter would
## reject Region at essentially every normal screen resolution (a whole-body
## view's sample spacing is tens of km/px, and 2x that is still far under
## 204.8 km — even though visually a ~5-10-screen-px-per-cell Region view
## reads perfectly fine, nowhere near "magnified interpolation"). The
## GENUINELY load-bearing question at the coarse end is different: can a
## District-granularity window (capped server-side at
## DISTRICT_WINDOW_MAX_N=64 districts, ~131 km per side) physically COVER
## the extent being displayed at all? Once it can't, Region is the only rung
## that CAN — this is a coverage/capacity fact, not a resolution-legibility
## judgment, and it's what actually decides "zoom out past the district rung
## transitions to Region" per the ticket's own framing.
##
## **A third finding, resolving the above two against each other:** at
## CELL_PIXEL_SIZE=16 (the shipped display scale), the fine-end `2x` band
## that would select District and the coarse-end coverage ceiling that
## selects Region do not meet — District's OWN native resolution already
## reads as "too fine" (wants Quarter) well before its 64-district coverage
## cap becomes binding (wants Region), leaving NO zoom range where the `2x`
## formula alone would ever pick District. Since the coverage ceiling is a
## hard CAPABILITY limit (a District request literally cannot serve more
## world than its per-axis cap covers) while the `2x` band is a QUALITY
## preference (finer than strictly needed is wasteful, not wrong), the
## coverage ceiling wins whenever the two disagree: check it FIRST, and only
## consult the `2x` band to choose between District and Quarter for whatever
## extent remains under that ceiling. This is a genuine engineering call this
## implementation makes (flagged to the team, not a design-doc-literal
## derivation) — see docs/architecture/atlas-zoom-ladder-t1143.md §5 Risk R4
## ("Region rung is named but unscoped") for the open design question this
## resolves pragmatically rather than by further design-pass iteration.
##
## **Whether a District band exists at all is independent of CELL_PIXEL_SIZE**
## — it cancels out of the "does District's `2x` band overlap the coverage
## ceiling" condition entirely. The condition reduces to `canvas_px <=
## DISTRICT_WINDOW_MAX_N * DISTRICT_SPACING_M / 1024` — i.e. `canvas_px <=
## 128px` at the shipped constants. At every real viewport (800px+), this is
## never satisfied: District's band is empty by construction, and the ladder
## in practice steps Region -> Quarter directly at any normal screen size.
## Verified numerically at 1600x900 (canvas_px=1600): the Region/District
## crossing (world_extent_m == the coverage ceiling) sits at `_view_zoom ≈
## 1.5625`, and the District/Quarter crossing (the `2x` band's own edge)
## sits at `_view_zoom ≈ 0.125` — i.e. the `2x` band's own boundary is
## already PAST (a smaller zoom than) where the coverage ceiling releases
## District, so the two never overlap in the zoomed-in direction either.
## **Tuning knobs, if a real District band is wanted:** the ONLY lever that
## opens the gap is `DISTRICT_WINDOW_MAX_N` (currently 64, mirrored from the
## server's own per-axis cap) — it would need to reach `1024 * canvas_px /
## DISTRICT_SPACING_M` (≈800 at a 1600px canvas) to open a band there, a
## substantial server-side wire-size change (T-1150's `WIRE_CAP_CELLS`
## budget), not a client-only tuning knob. `CELL_PIXEL_SIZE` does NOT affect
## whether a band exists — it only shifts WHERE both crossing zooms sit on
## the wheel gesture (scaling both proportionally, preserving their ~12.5x
## gap), i.e. it is the felt-pacing knob for how much wheel travel separates
## Region from Quarter, not a way to reintroduce District.
## 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:
if world_extent_m > float(DISTRICT_WINDOW_MAX_N) * DISTRICT_SPACING_M:
return "Region" # coverage ceiling — District physically cannot span this much world
if canvas_px <= 0.0:
return "Quarter" # finest — an unlaid-out viewport must under-resolve, not over-resolve
var sample_spacing_m: float = world_extent_m / canvas_px
var threshold_m: float = 2.0 * sample_spacing_m
if DISTRICT_SPACING_M <= threshold_m:
return "District"
return "Quarter" # threshold too small for even District's own spacing -> finest legal rung
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
@@ -438,3 +471,116 @@ static func edge_scroll_direction(
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}
@@ -61,6 +61,8 @@ extends Node2D
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")
## T-1145 item 3: interim presentation toggle — true renders the smoothed
## Image/ImageTexture composite; false keeps the original crisp per-cell
@@ -94,6 +96,9 @@ var _cache_active_toggle: String = ""
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
@@ -132,6 +137,125 @@ func _draw() -> void:
_draw_crisp_composite(w, grid_side, n, cell_px, active_toggle)
## T-1153, live round 3 (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 from the tile-set's reference
## origin (district (0,0), matching `_enter_tile_mode()`'s own
## `_held_center = Vector2i.ZERO`). A tile centered at absolute district
## `tile.center` occupies local canvas space
## `[(tile.center - TILE_N/2) * cell_px, (tile.center + TILE_N/2) * cell_px)`
## — the SAME "window spans [center - n/2, center + n/2)" convention the
## single-window draw path already uses, just evaluated per tile instead of
## once for the whole composite. 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).
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 half_tile: float = float(AtlasWindowGeometryRef.TILE_N) * 0.5
for tile: Dictionary in tile_set.get_tiles():
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 local_origin: Vector2 = Vector2(
(float(center.x) - half_tile) * cell_px, (float(center.y) - half_tile) * cell_px
)
var extent: float = float(AtlasWindowGeometryRef.TILE_N) * cell_px
_draw_one_tile(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 (keyed by the tile's own window
## reference, via `_tile_texture_cache` below) — 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(
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 = _build_tile_texture(w, grid_side, active_toggle)
if tile_texture == null:
return
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
draw_texture_rect(tile_texture, Rect2(local_origin, Vector2(extent, extent)), false)
## Builds (uncached — see the class doc's rebuild-cost paragraph for why the
## SINGLE-window path caches by reference; a per-tile cache keyed the same
## way would need a Dictionary keyed on tile index, a reasonable follow-up
## if mosaic redraw cost ever matters in practice, not attempted here since
## the mosaic is drawn only at the orbital rest state, never mid-interaction
## at a high redraw rate the way single-window zoom/pan is) 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.
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
@@ -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
@@ -3,74 +3,64 @@ extends Control
## 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 now (T-1152
## 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 granularity lives in ONE screen/Control,
## not a separate planetary heightmap viewer + a windowed drill-down. Renders
## a DistrictWindowLayer composite at whichever rung is currently held:
## 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). One colorizer
## family renders every rung unchanged (design doc §6) — AtlasWindowOverlay
## never branches on granularity_v2 for COLOR, only for the derived
## cell-grid's RESOLUTION (cell_grid_side_for_window()).
## 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, extended T-1153):
## - _canvas (Node2D) holds AtlasWindowOverlay; pan = _canvas.position, zoom
## = _canvas.scale — the SAME transform idiom as the (retired) planetary
## viewer.
## - Zoom is client-side on the ALREADY-HELD composite frame-to-frame (never
## blocks on a re-derive), but is CONTINUOUS AND UNCLAMPED ACROSS RUNGS
## (T-1153, D-013 restored for this seam): crossing a rung's spacing
## threshold (§5 rung-selection rule) fires a background request for the
## new granularity while the OLD composite keeps drawing — progressive
## refinement, no blank frame, no mode flip (§6). A pan past the held
## window's edge re-requests the SAME rung at a new center (§4/§5,
## unchanged from T-1138).
## = _canvas.scale.
## - Zoom is client-side on the ALREADY-HELD composite frame-to-frame, but
## CONTINUOUS AND UNCLAMPED ACROSS RUNGS (D-013 restored for this seam):
## crossing a rung's coverage ceiling (§5, redesigned per live round 3 —
## see AtlasWindowGeometry.select_rung()) fires a background request for
## the new granularity while the OLD composite keeps drawing —
## progressive refinement, no blank frame, no mode flip (§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().
## - _window_request (atlas_window_request.gd) owns the cache/debounce/
## retry — this Control decides WHEN to call it (pan-edge detection,
## rung-reselect, entry), never talks to SimBridge directly itself.
## condition) — see _maybe_reset_to_canonical_frame() — which, on a body
## needing tiling, re-enters `_tile_mode` (live round 3, design doc §4:
## "the top rest state is the WHOLE body, served as progressive
## capped-density TILING").
## - _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 cursor-anchored zoom; crosses rungs continuously (T-1153)
## Esc back (nav.pop() — the "district" nav-stack entry is
## gone as a separate hop, see atlas_app.gd's own doc)
## Esc back (nav.pop())
signal back_pressed
const PANEL_MARGIN: float = 16.0
const OVERLAY_BAR_HEADER_RESERVE: float = 360.0
## T-1153: MIN_ZOOM/MAX_ZOOM stay a wide safety clamp on the raw display
## multiplier (never letting _view_zoom collapse to zero or run away toward
## infinity) — they are NOT a rung boundary any more. Wheel zoom is now
## CONTINUOUS and UNCLAMPED ACROSS RUNGS (D-226 T-1143-rulings amendment,
## Jeroen's seam ruling: "D-013's zoom gesture owns spatial descent restored
## for this seam"): crossing a rung's spacing threshold (§5's rung-selection
## rule, AtlasWindowGeometry.select_rung()) re-requests a DIFFERENT
## granularity window at the SAME apparent screen extent, it does not clamp
## _view_zoom itself. The programmatic capture API (set_view(), T-1120) still
## clamps to this same wide range — a capture harness driving a specific
## zoom/offset pair has no rung-crossing concept of its own to trigger.
## 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 restored for this seam): 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` (a whole equatorial circumference, up
## to hundreds of thousands of districts on a gas-giant-scale body) is never
## itself clamped — a clamped fit zoom would silently show LESS than the
## whole body, breaking Jeroen's HARD condition ("the whole body fitted to
## the canvas") at exactly the moment it matters most. 0.0005 covers a
## ~120,000 km-radius body (n≈368,000 districts) at a 3840px 4K viewport with
## headroom; a real fit_zoom this low is expected and correct at the
## canonical orbital frame, not a bug.
## enter_orbital()'s largest legal `n` (up to hundreds of thousands of
## districts on a gas-giant-scale body) is never itself clamped — that would
## silently show LESS than the whole body, breaking Jeroen's HARD condition.
## 0.0005 covers a ~120,000 km-radius body at a 3840px 4K viewport with
## headroom.
const MIN_ZOOM: float = 0.0005
const MAX_ZOOM: float = 64.0
const ZOOM_STEP: float = 1.15
@@ -125,6 +115,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
@@ -213,6 +205,15 @@ 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 (Jeroen's ruling, 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()` when
## compute_tile_grid() produces more than one tile; cleared the moment
## `_maybe_reselect_rung()` crosses OUT of Region — tiling is purely a
## TOP-of-the-ladder concern, never active below Region.
var _tile_mode: bool = false
func _ready() -> void:
@@ -242,6 +243,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()
@@ -254,29 +260,20 @@ 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 a click-through's derived position — §5's "pan
## center read as click point") at District granularity. n defaults to the
## client's interactive default (32), half the server's hard cap. Kept as a
## thin District-rung wrapper over _enter_at_rung() (T-1153) — a click-to-
## descend-to-point shortcut on top of the continuous ladder (Jeroen's
## ruling: "if a click-to-descend-to-point remains cheap to keep... wired to
## the same descent path"). No screen currently calls this directly (the
## retired planetary click-through it served no longer exists — see
## atlas_app.gd's own doc); it survives as the landing point a future
## map-object click (e.g. a settlement marker on the Region-rung view) would
## wire into, and as a direct-call entry for tests/tools that want a
## District-rung window without going through enter_orbital() first.
## Enter the window screen centered on `district_center` (a DistrictPos, from
## a click-through's derived position) at District granularity. n defaults to
## 32, half the server's hard cap. Kept as a thin District-rung wrapper over
## _enter_at_rung() (T-1153) — a click-to-descend-to-point shortcut on top of
## the continuous ladder (Jeroen's ruling). No screen currently calls this
## directly (the retired planetary click-through it served no longer exists
## — see atlas_app.gd's own doc); it survives as the landing point a future
## map-object click would wire into, and as a direct-call entry for tests.
##
## 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).
## server's normalize_window_center() so the client's echo comparison never
## mismatches. Also fits-and-centers the view instead of resetting to
## zoom=1/offset=ZERO.
func enter(
body: Dictionary,
system: Dictionary,
@@ -292,25 +289,28 @@ func enter(
## T-1153: enter the ladder at its TOP REST STATE — the canonical orbital
## frame (Jeroen's HARD condition: "the whole body fitted to the canvas,
## centered at the body's canonical origin"). This is the new "regional" nav
## entry point (T-1152 client half — supersedes AtlasViewer's heightmap
## texture as the sole entry): the player lands on a fully-derived Region-rung
## view of the whole body, then wheel-zoom descends CONTINUOUSLY from there —
## no separate planetary screen, no click-through required to reach the
## windowed view at all (though enter() above stays wired for a
## click-to-descend shortcut, per Jeroen's ruling).
## centered at the body's canonical origin"). This is 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 — no separate planetary screen, no click-through required
## (though enter() below stays wired for a click-to-descend shortcut, per
## Jeroen's ruling).
##
## Canonical origin = district (0,0) — "district (0,0) sits at lon 0 / the
## equator" (AtlasDescendGeometry's own doc, mirroring
## district_profile.rs). Canonical extent = the WHOLE equatorial
## circumference in districts (district_extent().cols), i.e. one full
## circumnavigation — the same quantity is_fully_zoomed_out()/the
## full-zoom-out reset (see _maybe_reset_to_canonical_frame()) test against,
## so entry and reset always agree on what "the top" means. No-radius bodies
## (tiny test bodies) fall back to the District-rung default window — there
## is no planetary circumference concept to derive a Region-rung n from (same
## fallback disposition AtlasDescendGeometry's own no-radius branches use
## throughout).
## equator" (AtlasDescendGeometry's own doc). Canonical extent = the WHOLE
## equatorial circumference in districts, the same quantity
## is_fully_zoomed_out()/_maybe_reset_to_canonical_frame() test against, so
## entry and reset always agree on what "the top" means. No-radius bodies
## fall back to the District-rung default window (no circumference concept).
##
## **Live round 3 (Jeroen's ruling, design doc §4): the rest state must
## TILE.** A single wire-capped Region window covers at most
## `AtlasWindowGeometry.MAX_COVERAGE_M["Region"]` (13,107,200 m) — a THIRD of
## Lendel's ~39,197,023 m circumference (shot 01's own header: "13107.2 x
## 13107.2 km"). Once `compute_tile_grid()` returns MORE than one tile,
## entry goes through `_enter_tile_mode()` instead of `_enter_at_rung()`; a
## body whose circumference fits one Region window's ceiling still gets
## exactly one "tile" (the degenerate case) and stays single-window.
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:
@@ -320,31 +320,55 @@ func enter_orbital(body: Dictionary, system: Dictionary) -> void:
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`,
## setting `_tile_mode = true` so drawing/reset/reselect read the mosaic.
## `_held_n` carries the WHOLE body's extent unclamped (each TILE clamps its
## own TILE_N-sized request independently inside AtlasWindowTileSet), so the
## extent math elsewhere needs no tile-specific branch.
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
_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()
_tile_set.enter(_dict_str(_body, "body_id", ""), radius_km)
_refresh_screen_header()
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 (enter_orbital()'s (0,0) needs
## no canonicalization; enter()'s does its own before calling in). Resets
## every piece of held/request state for a fresh descent, exactly as the
## pre-T-1153 enter() always did, plus the new _held_granularity_v2 tracking.
## must already be canonicalized by the caller. Resets every piece of
## held/request state for a fresh descent, plus _held_granularity_v2.
##
## **The C1 clamp-mirror lesson, one layer up (live-round finding):** `n`
## MUST be clamped via `_clamp_window_n_mirror_v2()` BEFORE it becomes
## `_held_n` — mirroring exactly what AtlasWindowRequest.request_now()
## already does to ITS OWN `_n` before storing/sending (see that function's
## own doc for the original PR #191 Tyre C1 finding). Storing the RAW `n`
## here (e.g. enter_orbital()'s full district_extent().cols, routinely tens
## of thousands at Region granularity, versus the server's clamped echo of
## at most DISTRICT_WINDOW_MAX_N_REGION=6,400) left `_held_n` permanently
## disagreeing with what the server would ever actually echo — every
## orbital-rung response was silently rejected as stale by
## _on_window_ready()'s `w_n != _held_n` check, hanging the ladder on every
## real-sized body. `_maybe_reselect_rung()`/`_maybe_refloat_window()` both
## read `_held_n` (never re-derive it), so clamping here — the ONE write
## site — fixes every downstream caller too, not just entry.
## **C1 clamp-mirror, one layer up (live-round finding):** `n` MUST be
## clamped via `_clamp_window_n_mirror_v2()` BEFORE it becomes `_held_n` —
## mirroring what AtlasWindowRequest.request_now() already does to ITS OWN
## `_n` (PR #191 Tyre C1). Storing RAW `n` (e.g. enter_orbital()'s full
## district_extent().cols, tens of thousands at Region, vs. the server's
## clamped echo of at most 6,400) left `_held_n` permanently disagreeing
## with the server's echo — every orbital response silently rejected as
## stale forever. `_maybe_reselect_rung()`/`_maybe_refloat_window()` both
## read `_held_n` unchanged, so clamping here fixes every downstream caller.
func _enter_at_rung(
body: Dictionary,
system: Dictionary,
@@ -358,6 +382,7 @@ func _enter_at_rung(
_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
@@ -391,11 +416,9 @@ 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, in whole districts — a no-radius body
## has no pole concept (matching canonicalize_district_center()'s own
## no-radius identity), 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:
@@ -417,6 +440,19 @@ 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
## 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
@@ -451,15 +487,11 @@ func _on_atlas_layers_received(response: Dictionary) -> void:
_window_request.on_response(response)
## T-1153: progressive refinement — this is the ONE place a new rung's
## window gets adopted (swapped in), and it deliberately does NOT clear
## `_window` first. The OLD composite (whatever rung it was) stays drawn
## every frame up to and including the one before this call — no blank
## frame, no mode flip (§6 acceptance criterion) — because `_window` is a
## single-slot "the composite currently drawn" reference that only ever gets
## REPLACED, never nulled, once a window has been adopted at least once
## (enter()/_enter_at_rung() nulls it only at a fresh descent, a real
## navigation event, not a rung swap).
## 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
@@ -500,6 +532,15 @@ func _on_window_ready(window: Dictionary) -> void:
_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 CURSOR-
# ANCHORED and CONTINUOUS ACROSS RUNGS (T-1153, D-226 T-1143-rulings
@@ -517,14 +558,11 @@ func _apply_transform() -> void:
_overlay_node.queue_redraw()
## Cursor-anchored zoom (D-013 restored for this seam, Jeroen's ruling): the
## CANVAS POINT under the cursor stays fixed on screen across the zoom step
## zooming toward the cursor, not the view center. Unclamped ACROSS RUNGS
## (only the wide MIN_ZOOM/MAX_ZOOM safety clamp applies to the raw
## multiplier itself — see that constant's own doc); after applying the new
## zoom, checks whether the currently-displayed world extent now calls for a
## different rung (_maybe_reselect_rung()) and whether the view has reached
## the ladder's top rest state (_maybe_reset_to_canonical_frame()).
## Cursor-anchored zoom (D-013 restored for this seam): the CANVAS POINT
## under the cursor stays fixed on screen across the zoom step. Unclamped
## ACROSS RUNGS (only the wide MIN_ZOOM/MAX_ZOOM safety clamp applies — see
## that constant's own doc); after applying, checks whether the extent now
## calls for a different rung or 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)
if is_equal_approx(new_zoom, _view_zoom):
@@ -539,59 +577,71 @@ func _zoom_at(mouse_pos: Vector2, factor: float) -> void:
## The world extent (metres) currently displayed across the LARGER viewport
## dimension — the `E` half of the §5 rung-selection rule's `E/C`. A pure
## function of `_view_zoom` (see AtlasWindowGeometry.world_extent_m()'s own
## doc for why the currently-held rung is NOT an input: the composite's
## on-screen footprint is rung-invariant by construction, so sample density
## depends only on zoom). Thin wrapper kept here so callers don't need to
## know the pure function lives on AtlasWindowGeometry (T-1153 — extracted
## there, alongside select_rung(), to keep the §5 math unit-testable without
## a Control in the tree).
## dimension — the `E` half of the §5 rung-selection rule. A pure function
## of `_view_zoom` (see AtlasWindowGeometry.world_extent_m()'s own doc for
## why the held rung is NOT an input). Thin wrapper over that pure function.
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 coarsest legal rung for the NOW-displayed world extent
## (_current_world_extent_m() over the viewport's larger dimension). If that
## differs from what's currently HELD on screen, request the new granularity
## centered on the CURRENT screen-center's district position (reusing
## _screen_center_district() — the same screen-to-district math
## _maybe_refloat_window() already established, which is rung-agnostic since
## CELL_PIXEL_SIZE is always district-based regardless of the held rung's
## true cell spacing — see atlas_window_overlay.gd's cell_grid_side_for_window()
## doc for why that's true).
## 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 (_screen_center_district(), the same formula
## _maybe_refloat_window() uses).
##
## Progressive refinement, not block-on-derive: this does NOT touch `_window`
## or `_held_granularity_v2` — the OLD composite keeps drawing every frame
## (border-fade/pending-indicator per R6 shows the request is in flight, see
## _draw_border_fade()) until _on_window_ready() adopts the NEW rung's window
## once it actually arrives (§6 "no mode flip": never a blank frame, never a
## clear-then-redraw).
## **C1 clamp-mirror, a THIRD layer up (live round 3):** `_held_n` MUST be
## re-clamped via `_clamp_window_n_mirror_v2()` for the TARGET rung, not left
## at the PREVIOUS rung's clamp — crossing rungs changes the clamp ceiling
## (Region caps at 6,400; District/Quarter at 64), so a stale Region-sized
## `_held_n` fed into a Quarter request gets server-clamped small while
## `_held_n` stays large — `_on_window_ready()`'s `w_n != _held_n` then
## drops every cross-rung refinement forever. Same bug as _enter_at_rung(),
## recurring at the CROSSING boundary.
##
## 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": never a blank frame, never clear-then-redraw).
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)
if target_rung == _window_request.get_granularity_v2():
# T-1153, live round 3: tile mode is TOP-of-the-ladder only (coordinator's
# own scoping — "inside-zoom can stay single-window as now"). Staying at
# Region means staying tiled (zooming within a mosaic is a client-side
# scale on the SAME held tiles, like single-window zoom on one
# composite). 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 (never touched while tiled)
# happens to already equal `target_rung` by coincidence — without this,
# the early-return below would skip the request that's supposed to
# POPULATE `_window` for the first time since tile mode replaced it.
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
_window_request.request_debounced(
_dict_str(_body, "body_id", ""), new_center, _held_n, target_rung
_dict_str(_body, "body_id", ""), new_center, clamped_n, target_rung
)
## The DistrictPos the current screen center maps to, in RAW absolute
## district space (matching _maybe_refloat_window()'s own convention — only
## the caller canonicalizes the final value it actually stores/sends). Thin
## wrapper over AtlasWindowGeometry.screen_center_to_district() (T-1153 —
## extracted alongside the rung-selection math for the same testability
## reason) so both the pan-edge refetch and the rung-reselect refetch share
## ONE screen-to-district formula rather than two copies that could drift
## (the exact lesson _maybe_refloat_window()'s own doc already establishes
## for the pan case).
## district space (the caller canonicalizes the final stored/sent value).
## Thin wrapper over AtlasWindowGeometry.screen_center_to_district() so both
## the pan-edge refetch and the 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
@@ -600,19 +650,13 @@ func _screen_center_district() -> Vector2i:
return AtlasDescendGeometry.canonicalize_district_center(raw, radius_km)
## Jeroen's HARD condition (D-226 T-1143-rulings amendment): "a full
## zoom-out resets to the original canonical planetary frame and location" —
## the ladder's TOP REST STATE, never a drifted pan/zoom-out state. Fires
## when the CURRENTLY DISPLAYED world extent (at _held_granularity_v2, the
## rung actually on screen — deliberately NOT the in-flight request's rung,
## so this can't fire prematurely off a request that hasn't landed yet)
## covers the whole body (AtlasWindowGeometry.is_fully_zoomed_out()) AND the
## player isn't ALREADY sitting at the canonical frame (center == (0,0) —
## re-entering the SAME enter_orbital() state on every zoom tick past the
## threshold would fight a player trying to zoom back IN from the top, since
## every zoom-out tick would keep re-snapping to the identical framing).
## Returns true if it fired (the caller should skip _maybe_reselect_rung() —
## the reset already re-requested at the canonical Region-rung window).
## Jeroen's HARD condition: "a full zoom-out resets to the original
## canonical planetary frame and location" — the ladder's TOP REST STATE,
## never a drifted pan/zoom-out state. Fires when the CURRENTLY DISPLAYED
## extent (at _held_granularity_v2, deliberately NOT the in-flight request's
## rung) covers the whole body AND the player isn't ALREADY at the canonical
## frame (re-snapping every tick would fight a zoom-in-from-the-top
## gesture). Returns true if it fired (caller skips _maybe_reselect_rung()).
func _maybe_reset_to_canonical_frame() -> bool:
var radius_km: float = float(_body.get("body_radius_km", 0.0))
if radius_km <= 0.0:
@@ -650,29 +694,26 @@ 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 (un-wrapped, un-clamped) — the held window's own local
## bounds are relative to _held_center as it was BEFORE this pan. Only the
## FINAL new_center is canonicalized (wrap column, clamp row), matching the
## server's normalize_window_center() so the echo comparison/cache key stay
## on the same canonical form. A pan straddling the antimeridian still
## floats correctly: the pre-canonicalization abs_col can go negative or
## past cols, and only the resulting new_center gets wrapped before use.
func _maybe_refloat_window() -> void:
if _held_n <= 0:
return
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
)
@@ -716,6 +757,11 @@ 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
@@ -760,39 +806,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". T-1153: the extent (`n` districts) is
## rung-INVARIANT (n is always district extent — see
## AtlasWindowOverlay.cell_grid_side_for_window()'s doc), but the km/cell
## reading must reflect the HELD rung's actual spacing (2.048 km at District,
## 0.512 km at Quarter, 204.8 km at Region) — this is the "continuous
## metres-per-pixel/extent readout" the 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, no named
## mode chrome does.
## 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 spacing_km: float = AtlasWindowGeometry.spacing_for_rung(_held_granularity_v2) / 1000.0
var extent_line: String = "%.1f x %.1f km · %.3f km/cell" % [extent_km, extent_km, spacing_km]
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
)
# =============================================================================
@@ -811,16 +844,10 @@ func _is_over_ui(_pos: Vector2) -> bool:
## 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).
## semantics with map objects). What remains: wheel zoom and tracking the
## local mouse position for edge-scroll (_process() has no InputEvent of its
## own). WASD/arrow panning does NOT go through _gui_input — it's a
## HELD-key, frame-rate-independent 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)
@@ -850,18 +877,11 @@ func _handle_key(event: InputEventKey) -> void:
## 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.
## here (not _gui_input) since both are HELD-state effects, not discrete
## events — polls held state every frame and hands the direction + delta to
## _apply_pan_delta() (split out for testability — a gdUnit test drives it
## directly rather than faking Godot's global Input singleton). Skips while
## hidden (screen not the active nav-stack entry).
func _process(delta: float) -> void:
if not visible:
return
@@ -873,15 +893,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