feat(ui): step-canvas map component — RTT terrain + stepped zoom (D-255, T-1182)

The two-layer client rebuild per D-255(a)(b)(e), replacing the
_canvas.scale continuous-zoom model with one viewer, one path, all six
rungs:

- step_canvas_protocol.gd: StepCanvasRequest/Response codec against
  the T-1181 wire contract — incl. the discovered png_bytes subtlety
  (rmp_serde without serde_bytes emits a msgpack int-array, not bin;
  decode repacks via PackedByteArray before load_png_from_buffer) and
  the extent-echo rule (read the server-clamped extent, never assume
  the requested one).
- step_canvas/ component: transport (six-rung ladder, cursor-anchored
  scroll steps, edge-scroll/WASD pan with re-request on edge crossing,
  hard reset-to-Global), RTT terrain layer (Image.set_pixel colorize
  per the c1 measured ruling, texture.update reuse on step-cross,
  NEAREST coarse / LINEAR fine per rung), unscaled screen-space
  annotation sibling (courses + settlement markers at literal px),
  in-memory LRU cache (Tier 1; T-1183 layers the disk tiers beneath),
  request lifecycle (pending retry, staleness gate, extent echo).
- Full _canvas.scale retirement in the same change: the zoom-scaled
  canvas model, the _zs compensation family, select_rung /
  MAX_COVERAGE_M / compute_tile_grid, the orbital-mosaic-vs-window
  two-path split, _view_zoom/_canonical_fit_zoom — 10 source files
  deleted; their 14 test suites deleted with them (T-1157 dead-goldens
  rule; replacement visual-capture coverage is re-scoped T-1157).
- Surviving surfaces kept per the ticket: atlas_window_cache.gd's LRU
  shape (the ticket's named file atlas_window_tile_set.gd was the
  retiring orchestrator; the real LRU shape lives in
  atlas_window_cache.gd — cited in step_canvas_cache.gd), overlay
  colors, legend/overlay-bar chrome, AtlasViewer descend geometry.

Determinism boundary per D-255(e): the client interpolates only within
the closed server-supplied input set. 7 new gdUnit suites (164 cases)
incl. a real extent-echo bug caught by its own test during
implementation. Full client suite green (exit 0) with the live-gated
suites running against a worktree server build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 08:53:48 +02:00
co-authored by Claude Fable 5
parent e0a3ab35cf
commit d28d24fd26
43 changed files with 2858 additions and 12241 deletions
+31
View File
@@ -7,6 +7,7 @@ signal atlas_layers_received(response: Dictionary)
signal star_map_received(response: Dictionary) # T-949: StarMapResponse
signal city_names_received(response: Dictionary) # T-949: CityNamesResponse
signal browse_response_received(response: Dictionary) # T-1131/T-1133: BrowseResponse
signal step_canvas_received(response: Dictionary) # T-1182, D-255(c): StepCanvasResponse
signal handshake_complete
signal handshake_failed(reason: String)
@@ -568,6 +569,33 @@ func request_browse_detail(kind: String, entity_id: String) -> void:
)
## Request one step-canvas data canvas (T-1182, D-255(c)) — the stepped Atlas
## ladder's per-rung terrain/annotation payload. Live mode only; the response
## arrives via step_canvas_received. `rung` is one of the six bare-string rung
## tags (StepCanvasTransport.RUNG_*); `center`/`extent` are sent unconditionally
## even for Global (the server ignores them for that rung — see
## step_canvas_protocol.gd's own doc). No client-side polling loop here — the
## D-225 poll/cache/enqueue pattern means a Pending response is the caller's
## cue to retry, mirroring request_atlas_layers()'s own "fire and let the
## response routing decide" shape.
func request_step_canvas(
body_id: String, rung: String, center: Vector2i, extent: Vector2i, min_wl_m: int = 0
) -> void:
if test_mode or _bridge == null or state != ConnectionState.CONNECTED:
return
var bytes := Protocol.encode_step_canvas_request(body_id, rung, center, extent, min_wl_m)
if bytes.is_empty():
return
var err: int = _bridge.send_message(bytes)
if err != OK:
push_error(
(
"SimBridge: failed to send step canvas request for %s/%s: %s"
% [body_id, rung, error_string(err)]
)
)
# Poll for snapshot from simulation.
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
func poll_snapshot() -> Variant:
@@ -609,6 +637,9 @@ func receive_bytes(bytes: PackedByteArray) -> void:
if inbound.kind == "browse":
browse_response_received.emit(inbound.value)
return
if inbound.kind == "step_canvas":
step_canvas_received.emit(inbound.value)
return
if inbound.kind != "snapshot":
push_warning("SimBridge: undecodable frame (%d bytes)" % bytes.size())
return
+42 -8
View File
@@ -31,6 +31,12 @@ static func _amp():
return load("res://scripts/protocol/atlas_map_protocol.gd")
## StepCanvasRequest/StepCanvasResponse codec (T-1182, D-255(c)) — same
## load()-by-path rationale as _bp()/_amp() above.
static func _scp():
return load("res://scripts/protocol/step_canvas_protocol.gd")
# -- Decode: bytes from server → GDScript types --------------------------------
@@ -874,15 +880,41 @@ static func decode_browse_response(bytes: PackedByteArray) -> Variant:
return browse_response_from_raw(decode_raw(bytes))
## Encode a StepCanvasRequest (T-1182, D-255(c)) — the stepped Atlas ladder's
## data-canvas request. Delegates to step_canvas_protocol.gd — see that file
## for the full wire-shape rationale (the sixth Inbound discriminator,
## PNG-per-field decode note).
static func encode_step_canvas_request(
body_id: String, rung: String, center: Vector2i, extent: Vector2i, min_wl_m: int = 0
) -> PackedByteArray:
return _scp().encode_step_canvas_request(_mp(), body_id, rung, center, extent, min_wl_m)
## Build a StepCanvasResponse from an already-decoded raw value. See
## step_canvas_protocol.gd for the full field-by-field wire-shape rationale.
static func step_canvas_response_from_raw(raw: Variant) -> Variant:
return _scp().step_canvas_response_from_raw(raw)
## Decode a StepCanvasResponse from MessagePack bytes. See
## step_canvas_response_from_raw.
static func decode_step_canvas_response(bytes: PackedByteArray) -> Variant:
return step_canvas_response_from_raw(decode_raw(bytes))
## Decode + classify one inbound frame (#960, D-225; T-949 adds starmap/
## citynames; T-1131/T-1133 adds browse). Returns {kind, value}, kind one of
## "snapshot"|"atlas"|"starmap"|"citynames"|"browse"|"unknown" — all msgpack
## maps, told apart by field, most-specific-first: StarMapResponse is the
## only kind with "data" and no "body_id"; CityNamesResponse the only one
## with "cities"; BrowseResponse the only one with its OWN "kind" field
## alongside "status"; anything else carrying "status" is AtlasLayerResponse.
## Lets receive_bytes decode the frame ONCE instead of double-decoding the
## 20 Hz snapshot path.
## citynames; T-1131/T-1133 adds browse; T-1182 adds step_canvas). Returns
## {kind, value}, kind one of
## "snapshot"|"atlas"|"starmap"|"citynames"|"browse"|"step_canvas"|"unknown" —
## all msgpack maps, told apart by field, most-specific-first: StarMapResponse
## is the only kind with "data" and no "body_id"; CityNamesResponse the only
## one with "cities"; BrowseResponse the only one with its OWN "kind" field
## alongside "status"; StepCanvasResponse the only one with "rung" (checked
## BEFORE the generic "status"-only AtlasLayerResponse fallback, since a
## step-canvas response also carries "status" and would otherwise be
## misrouted to "atlas"); anything else carrying "status" is
## AtlasLayerResponse. Lets receive_bytes decode the frame ONCE instead of
## double-decoding the 20 Hz snapshot path.
static func decode_inbound(bytes: PackedByteArray) -> Dictionary:
var raw = decode_raw(bytes)
if not raw is Dictionary:
@@ -895,6 +927,8 @@ static func decode_inbound(bytes: PackedByteArray) -> Dictionary:
return {"kind": "citynames", "value": city_names_response_from_raw(raw)}
if raw.has("kind") and raw.has("status"):
return {"kind": "browse", "value": browse_response_from_raw(raw)}
if raw.has("rung") and raw.has("status"):
return {"kind": "step_canvas", "value": step_canvas_response_from_raw(raw)}
if raw.has("status"):
return {"kind": "atlas", "value": atlas_response_from_raw(raw)}
return {"kind": "snapshot", "value": _decode_snapshot_from_raw(raw)}
@@ -0,0 +1,177 @@
class_name StepCanvasProtocol
## StepCanvasRequest/StepCanvasResponse wire codec (T-1182, D-255(c)) — the
## stepped Atlas ladder's tagged-envelope carrier, executing the D-225
## migration server/src/atlas/step_canvas.rs already ships (T-1181, PR #201).
## Split out of protocol.gd (not folded in), same rationale as
## browse_protocol.gd/atlas_map_protocol.gd — stay under gdlint's
## max-file-lines, one file per wire-shape family.
##
## `step_canvas: true` is the mandatory discriminator field — the SIXTH
## `Inbound` shape server/src/bridge/mod.rs demuxes on (star_map/city_names/
## browse/step_canvas, plus the two field-shape-disambiguated map probes).
##
## **Wire shapes (confirmed against server/src/atlas/step_canvas.rs, the
## authoritative source over any doc drift — T-1181's IMPLEMENTATION REPORT +
## WIRE SHAPE ADDENDUM, 2026-07-25):**
## StepCanvasRequest — a map: {step_canvas: true, body_id: String,
## rung: <bare string, one of "Global"|"Region"|"District"|"Quarter"|
## "Block"|"Chunk">, center: [i64, i64], extent: [u32, u32],
## min_wl_m: u32}. `rung` is a unit-variant enum, encoded as its Rust
## variant NAME verbatim (rmp_serde's bare-enum convention, same as
## granularity_v2/RoadNodeKind/CourseTerminus elsewhere on this wire).
## `center`/`extent` are meaningless for Global (server ignores extent
## entirely, center is echoed but unused) but are still sent — the
## request shape carries them unconditionally, no per-rung omission (the
## server-side struct has no Option on either field).
## StepCanvasResponse — a map: {body_id, rung, center: [i64,i64],
## extent: [u32,u32], min_wl_m: u32, status, canvas: null | EncodedStepCanvas}.
## `extent` is the server-CLAMPED echo (PR #201 review) — ALWAYS read
## this back, never assume the requested extent; `(0,0)` for Global (the
## wire extent is never read for that rung, so there is no clamped value
## to report). `status` is the same bare-string-or-{"Error":"msg"} shape
## every other status enum on this wire uses (Ready|Pending|NotFound|
## Error(String)) — decoded via the shared _decode_status_field() shape
## (duplicated here per browse_protocol.gd's own "genuinely standalone"
## precedent, not shared via a Callable).
## EncodedStepCanvas — a map: {width, height, morphology, elev_q, temp_dc,
## moisture_q, vegetation, settlement_id, glaciation, flooded_q, courses,
## cliffs}. The six PNG-per-field dense planes (morphology/elev_q/
## moisture_q/vegetation/glaciation/flooded_q) are each a map
## {"png_bytes": [...]} — png_bytes is a Rust `Vec<u8>` with NO
## serde_bytes annotation anywhere in this codebase (confirmed: grep for
## serde_bytes across server/src returns nothing), so serde's blanket
## Vec<T> impl serializes it via serialize_seq — a msgpack ARRAY of
## small-integer elements, NOT a `bin` blob. messagepack.gd's decoder
## therefore returns a plain GDScript Array (one int per byte), which
## this codec repacks into a PackedByteArray via the direct
## PackedByteArray(array) constructor before handing it to
## Image.load_png_from_buffer() — decode_png_field() below is the one
## place this conversion happens. temp_dc/settlement_id are the OTHER
## two dense fields, shipped raw MessagePack-native (too wide for an
## 8-bit PNG plane per EncodedTempField/EncodedSettlementField's own
## server-side doc): {"values": [...]} — i16/u32 arrays respectively,
## passed through as plain Arrays, no PackedByteArray repack (their
## per-cell domain doesn't fit a byte anyway). courses/cliffs are sparse
## MessagePack-native lists, passed through unshaped (the annotation
## layer owns interpreting RiverCourse/CliffSegment's own per-entry
## shape, matching atlas_map_protocol.gd's existing "raw passthrough,
## caller reshapes" precedent for district_window's courses field).
## Encode a StepCanvasRequest. `mp` is the loaded messagepack.gd module
## (passed in rather than reloaded here, matching every other codec in this
## cluster). `rung` is one of the six bare-string rung tags
## (StepCanvasTransport.RUNG_* constants) — sent verbatim, no client-side
## validation (the server rejects an unrecognized variant name at decode
## time per step_canvas.rs's own doc: "Unknown -> rejected, never trusted
## from the wire").
static func encode_step_canvas_request(
mp, body_id: String, rung: String, center: Vector2i, extent: Vector2i, min_wl_m: int = 0
) -> PackedByteArray:
var msg := {
"step_canvas": true,
"body_id": body_id,
"rung": rung,
"center": [center.x, center.y],
"extent": [extent.x, extent.y],
"min_wl_m": min_wl_m,
}
var result = mp.encode(msg)
if result.status != null:
push_error("StepCanvasProtocol: encode_step_canvas_request failed: %s" % result.status)
return PackedByteArray()
return result.value
## Shared status-enum decode — bare string ("Ready"/"Pending"/"NotFound") or
## a single-key map ({"Error": "message"}) for the one data variant. Same
## shape browse_protocol.gd/atlas_map_protocol.gd already implement for their
## own status enums; duplicated here rather than shared via a Callable to
## keep this file genuinely standalone (browse_protocol.gd's own stated
## rationale for its copy).
static func _decode_status_field(status_raw: Variant) -> Dictionary:
if status_raw is String:
return {"status": status_raw, "error": ""}
if status_raw is Dictionary and status_raw.has("Error"):
return {"status": "Error", "error": str(status_raw["Error"])}
return {"status": "", "error": ""}
## Repack a decoded {"png_bytes": [...]} field into a PackedByteArray ready
## for Image.load_png_from_buffer(). `field_raw` is the raw decoded Variant
## for one of the six PNG-per-field planes — null/malformed input returns an
## empty PackedByteArray (the caller's Image decode then fails gracefully,
## same "skip the bad entry" posture the rest of this cluster uses rather
## than crashing on a malformed wire payload).
static func decode_png_field(field_raw: Variant) -> PackedByteArray:
if not field_raw is Dictionary:
return PackedByteArray()
var bytes_raw: Variant = (field_raw as Dictionary).get("png_bytes")
if not bytes_raw is Array:
return PackedByteArray()
return PackedByteArray(bytes_raw)
## Build an EncodedStepCanvas Dictionary (GDScript-side shape) from an
## already-decoded raw value. Returns null if `raw` isn't a plausible canvas
## (missing width/height). PNG fields are repacked to PackedByteArray here
## (once, at decode time) — every downstream consumer (the terrain layer)
## works with real PackedByteArray/Image objects, never re-touches the raw
## msgpack Array shape.
static func _decode_encoded_canvas(raw: Variant) -> Variant:
if not raw is Dictionary:
return null
var d: Dictionary = raw
if not d.has("width") or not d.has("height"):
return null
var temp_dc_raw: Variant = d.get("temp_dc")
var settlement_id_raw: Variant = d.get("settlement_id")
return {
"width": int(d.get("width", 0)),
"height": int(d.get("height", 0)),
"morphology": decode_png_field(d.get("morphology")),
"elev_q": decode_png_field(d.get("elev_q")),
"temp_dc": (temp_dc_raw as Dictionary).get("values", []) if temp_dc_raw is Dictionary else [],
"moisture_q": decode_png_field(d.get("moisture_q")),
"vegetation": decode_png_field(d.get("vegetation")),
"settlement_id":
(
(settlement_id_raw as Dictionary).get("values", [])
if settlement_id_raw is Dictionary
else []
),
"glaciation": decode_png_field(d.get("glaciation")),
"flooded_q": decode_png_field(d.get("flooded_q")),
"courses": d.get("courses", []),
"cliffs": d.get("cliffs", []),
}
## Build a StepCanvasResponse Dictionary from an already-decoded raw value.
## Returns null unless it carries "rung" AND "status" — "rung" is the field
## no other decoded response on this wire has (atlas/citynames/browse all
## lack it), making it a safe, unambiguous discriminator for
## Protocol.decode_inbound()'s dispatch.
static func step_canvas_response_from_raw(raw: Variant) -> Variant:
if not raw is Dictionary or not raw.has("rung") or not raw.has("status"):
return null
var d: Dictionary = raw
var decoded_status := _decode_status_field(d.get("status"))
var center_raw: Variant = d.get("center", [0, 0])
var extent_raw: Variant = d.get("extent", [0, 0])
return {
"body_id": str(d.get("body_id", "")),
"rung": str(d.get("rung", "")),
"center": _vec_from_pair(center_raw),
"extent": _vec_from_pair(extent_raw),
"min_wl_m": int(d.get("min_wl_m", 0)),
"status": decoded_status["status"],
"error": decoded_status["error"],
"canvas": _decode_encoded_canvas(d.get("canvas")),
}
static func _vec_from_pair(pair: Variant) -> Vector2i:
if pair is Array and pair.size() >= 2:
return Vector2i(int(pair[0]), int(pair[1]))
return Vector2i.ZERO
-311
View File
@@ -1,311 +0,0 @@
## PR #192 cold-start dossier — coordinator's live repro against a freshly-
## spawned (cold) server (`make atlas` shape, first AnalyzeBody taking
## seconds): BUG 1 (tile-mosaic paint never resolving), the legend-stacking
## half of BUG 2, and BUG 3 (the "DERIVING TERRAIN…" pending-state label,
## round 2). Split out of test_atlas_zoom_ladder.gd purely for file-length
## reasons (gdlint max-file-lines) — same instantiation/mock-response
## conventions as that file, not a different testing philosophy.
## RegionalScreen's own re-entry-guard half of BUG 2 is covered separately
## in test_regional_screen.gd (a different layer — nav, not the viewer).
class_name TestAtlasColdStart
extends GdUnitTestSuite
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## Dudley's WINDOW_GRANULARITY_REGION_KEY sentinel — mirrors
## test_atlas_zoom_ladder.gd's own constant (see that file's doc for why the
## real wire value matters, not a convenient placeholder).
const SERVER_LEGACY_GRANULARITY_REGION_SENTINEL: int = 4294967295
## Build a hand-authored DistrictWindowLayer dict (n=2 by default) — mirrors
## test_atlas_zoom_ladder.gd's own _mock_window().
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
# =============================================================================
# BUG 1 — tile-mosaic paint self-heal.
# =============================================================================
## Counts real _draw() invocations — CanvasItem exposes no public
## "is a redraw pending" query in this Godot version, so the only reliable
## signal that queue_redraw() actually had an effect is the engine calling
## _draw() again on a subsequent frame. Subclasses the REAL AtlasWindowOverlay
## (not a duck-typed stub) so drawing still runs through the genuine
## production code path — this spy only adds counting, nothing else.
class _CountingOverlay extends AtlasWindowOverlay:
var draw_count := 0
func _draw() -> void:
draw_count += 1
super._draw()
## On a cold server, a tile's window_ready can land well after entry's own
## paint window, and a live repro showed the mosaic staying black even with
## every tile held/textured — only resolving on an unrelated gesture.
## _process() must therefore queue a redraw on BOTH the viewer and the
## overlay every frame while any tile is still pending, regardless of
## whether the tile-arrival signal path painted correctly on its own. Proven
## here by swapping the REAL overlay for a _draw()-counting subclass right
## after entry (once the entry-time redraw has already resolved via a real
## frame), then calling _process() directly with NO input/gesture and
## confirming a further frame actually invokes _draw() again — revert-
## verified against a version of _process() with the self-heal removed
## (fails without it, since nothing else re-queues while idle).
func test_process_self_heals_the_overlay_redraw_while_tiles_are_pending() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — needs tiling
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
assert_bool(v.get_tile_set().has_pending_tiles()).override_failure_message(
"sanity: entry must leave every tile pending before any response arrives"
).is_true()
# Swap in the counting spy AFTER entry (so entry's own queue_redraw()
# calls don't pollute the baseline) but the OLD overlay is freed and the
# spy re-added under the same _canvas parent, matching _ready()'s own
# construction shape exactly.
var spy := _CountingOverlay.new()
spy.viewer = v
v._overlay_node.queue_free()
v._overlay_node = spy
v._canvas.add_child(spy)
await get_tree().process_frame # let this frame settle with the spy in place
await get_tree().process_frame
var baseline: int = spy.draw_count
assert_int(baseline).override_failure_message(
"sanity: the spy must have been drawn at least once before the no-input"
+ " frame below, or this test can't distinguish self-heal from a first draw"
).is_greater(0)
# No pan/zoom/gesture — the ONLY thing that should cause another _draw()
# is _process()'s own self-heal, since has_pending_tiles() is still true
# (no response has been delivered).
v._process(0.016)
await get_tree().process_frame
assert_int(spy.draw_count).override_failure_message(
"_process() must queue a redraw every frame while has_pending_tiles()"
+ " is true, with NO input/gesture — draw_count must have advanced past"
+ " the baseline (%d), the cold-start self-heal" % baseline
).is_greater(baseline)
## The self-heal must STOP once every tile has arrived — a redraw queued
## forever regardless of state would just be a disguised always-redraw, not
## a targeted fix for the pending window.
func test_process_stops_self_healing_once_every_tile_has_arrived() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
var tile_set = v.get_tile_set()
for tile: Dictionary in tile_set.get_tiles():
var window: Dictionary = _mock_window(tile["center"])
window["granularity_v2"] = "Region"
window["granularity"] = SERVER_LEGACY_GRANULARITY_REGION_SENTINEL
window["n"] = AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(tile_set.has_pending_tiles()).override_failure_message(
"sanity: every tile must have arrived after this loop"
).is_false()
# =============================================================================
# BUG 2 — legend stacking (the viewer-level half; see test_regional_screen.gd
# for the nav-layer re-entry guard).
# =============================================================================
## Coordinator's live scene dump: WindowLegend measured 260x2343 px, ~10
## legends stacked — ImplantPanel.clear() used deferred queue_free(), so
## same-frame repeat refresh() calls piled new content onto STALE not-yet-
## freed children instead of replacing them. N refresh() calls in the SAME
## frame (no process_frame between them, matching how the actual trigger —
## RegionalScreen.enter() previously lacking its own re-entry guard — landed
## repeat enter_orbital() calls back to back) must leave exactly ONE legend's
## worth of children, not N stacked copies.
func test_legend_refresh_is_idempotent_against_same_frame_re_entry() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var baseline_count: int = v._legend_panel.get_implant_children().size()
for _i in range(10):
v._legend_panel.refresh()
var after_count: int = v._legend_panel.get_implant_children().size()
assert_int(after_count).override_failure_message(
(
"10 same-frame refresh() calls must leave exactly ONE legend's worth of"
+ " children (%d), not %d stacked copies — ImplantPanel.clear() must"
+ " free immediately, not defer via queue_free()"
) % [baseline_count, after_count]
).is_equal(baseline_count)
## PR #192 cold-start round 2: the children-count fix above is necessary but
## not sufficient — the coordinator's live scene dump showed the CONTAINER
## itself measured 260x2343px even after that fix landed. Root cause turned
## out to be BROADER than "only after stacking": this panel is manually
## positioned under AtlasWindowViewer (not inside a parent Container), so
## `size` NEVER tracks a shrinking `get_minimum_size()` on its own at
## all — confirmed directly (instrumented and reverted) that even a
## completely FRESH, never-refreshed-twice legend shows `size` frozen at
## whatever it happened to be on its very first measurement, while
## get_minimum_size() reports the correct value the whole time. The
## regression here therefore compares `size.y` against the RELIABLE ground
## truth (`get_minimum_size().y`, confirmed correct in every trace) rather
## than an earlier `size.y` snapshot — comparing size-to-size would pass
## trivially if BOTH numbers were equally stuck at the same stale value,
## which is exactly what silently happened during earlier drafts of this
## test. Reproduces the stacking shape (N same-frame refresh() calls) for
## realism, matching the coordinator's own trigger — the coordinator's
## acceptance bar: "after N refreshes the panel rect height must be within
## one legend's height" (of the CORRECT single-legend height, i.e. the
## settled minimum size).
func test_legend_panel_shrinks_back_after_a_stacking_window() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
# Reproduce the stacking window directly (same-frame repeat refresh()
# calls, matching the pre-fix trigger shape) — this drives the panel's
# minimum size up the same way N repeat enter_orbital() calls did live.
for _i in range(10):
v._legend_panel.refresh()
await get_tree().process_frame
await get_tree().process_frame
# A further, ordinary refresh() (the kind every real rung change already
# triggers) must leave the panel within one legend's height.
v._legend_panel.refresh()
# reset_to_content_size() is deferred (see its own doc — get_minimum_size()
# is momentarily wrong for RichTextLabel.fit_content children until the
# panel's real width has been laid out once) — a real frame must elapse
# for the deferred reset_size() call to actually run.
await get_tree().process_frame
await get_tree().process_frame
var one_legend_height: float = v._legend_panel.get_minimum_size().y
assert_float(one_legend_height).override_failure_message(
"sanity: a single settled legend must have a real, non-zero measured minimum height"
).is_greater(0.0)
assert_float(v._legend_panel.size.y).override_failure_message(
(
"after a stacking window, the legend panel's rect height (%.1f) must"
+ " shrink back to within one legend's height (%.1f, the panel's own"
+ " correctly-settled get_minimum_size()) — reset_to_content_size()"
+ " must actually collapse the Control back down, not just hold onto"
+ " its previously-grown size"
) % [v._legend_panel.size.y, one_legend_height]
).is_less_equal(one_legend_height + 1.0) # +1.0: float rounding slack
# =============================================================================
# BUG 3 (round 2) — "DERIVING TERRAIN…" label: the subtle per-tile
# COLOR_BORDER_FADE wash alone was invisible in a live cold capture. The
# viewer draws an unmistakable centered label while ZERO tiles have arrived,
# dropping it the instant even one lands.
# =============================================================================
## The viewer must show the label exactly while is_tile_mode() is true AND
## has_any_tile_arrived() is false — the coordinator's "ZERO tiles have
## arrived" trigger condition, pinned directly against real tile-set state
## (not a mock) via a real enter_orbital() on a tiling body. Also exercises
## _draw()'s ACTUAL dispatch to _draw_deriving_terrain_label() through a
## real frame (queue_redraw() + await process_frame, matching the
## _CountingOverlay spy pattern from the BUG 1 self-heal tests above) —
## proving the draw call itself is reachable and doesn't error, not just
## that the underlying predicate is correct.
func test_deriving_terrain_label_condition_true_before_any_tile_arrives() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — needs tiling
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
assert_bool(v._tile_set.has_any_tile_arrived()).override_failure_message(
"sanity: entry must leave every tile unarrived before any response arrives"
).is_false()
v.queue_redraw()
await get_tree().process_frame
## The instant even ONE tile lands, the label's own gate condition must flip
## off — per-tile washes alone are the right treatment once real content is
## visibly filling in (coordinator: "dropping to per-tile washes once the
## first tile lands").
func test_deriving_terrain_label_condition_false_after_one_tile_arrives() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
var tile_set = v.get_tile_set()
var first_tile: Dictionary = tile_set.get_tiles()[0]
var window: Dictionary = _mock_window(first_tile["center"])
window["granularity_v2"] = "Region"
window["granularity"] = SERVER_LEGACY_GRANULARITY_REGION_SENTINEL
window["n"] = AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(tile_set.has_any_tile_arrived()).override_failure_message(
"the label's own gate condition (NOT has_any_tile_arrived()) must flip"
+ " false the instant a single tile lands, dropping the label"
).is_true()
## Single-window mode (District/Quarter/small-body Region, not tile mode)
## never shows this label at all — it's a mosaic-specific cue for the
## "whole orbital rest state is still deriving" case, not every wait state
## (the single-window path already has its own COLOR_BORDER_FADE treatment,
## unchanged by this round).
func test_deriving_terrain_label_never_applies_outside_tile_mode() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: a District-rung enter() must never be tile mode"
).is_false()
## centered_label_baseline() pure geometry: the X component is the
## VIEWPORT-CENTERED text block's left-anchor-adjusted X (viewport center
## minus half the text width — draw_string() itself does the final
## horizontal centering from there via HORIZONTAL_ALIGNMENT_CENTER, this
## only sets up where that alignment measures from); the Y component sits
## at viewport-center (a draw_string() baseline is the text's OWN vertical
## center here, by construction: center.y - text.y/2 + text.y/2 == center.y).
func test_centered_label_baseline_centers_a_symmetric_case() -> void:
var viewport_size := Vector2(1000.0, 800.0)
var text_size := Vector2(200.0, 40.0)
var baseline: Vector2 = AtlasWindowGeometry.centered_label_baseline(viewport_size, text_size)
assert_that(baseline).is_equal(Vector2(400.0, 400.0))
## A zero-size viewport (never laid out yet) must not crash — degenerate
## input, not a real scenario, but the function must stay total.
func test_centered_label_baseline_zero_viewport_does_not_crash() -> void:
var baseline: Vector2 = AtlasWindowGeometry.centered_label_baseline(
Vector2.ZERO, Vector2(100.0, 20.0)
)
assert_that(baseline).is_equal(Vector2(-50.0, 0.0))
+8 -8
View File
@@ -371,19 +371,19 @@ func test_no_descend_signal_without_a_loaded_heightmap() -> void:
assert_int(received.size()).is_equal(0)
## T-1153 (D-226 T-1143-rulings amendment): RegionalScreen no longer wraps
## T-1182 (D-255 stepped Atlas ladder): RegionalScreen no longer wraps
## AtlasViewer or forwards district_descend_requested — the "regional" nav
## entry now opens the continuous zoom ladder (AtlasWindowViewer) directly at
## the canonical orbital frame, retiring the click-through as the sole entry
## (see regional_screen.gd's own doc). This regression-guards the NEW
## wiring: entering "regional" reaches AtlasWindowViewer, not AtlasViewer.
func test_regional_screen_wraps_atlas_window_viewer_not_atlas_viewer() -> void:
## entry opens the stepped six-rung ladder (StepCanvasViewer) directly at the
## Global opener (rung 0) — see regional_screen.gd's own doc. This
## regression-guards the wiring: entering "regional" reaches
## StepCanvasViewer, not AtlasViewer.
func test_regional_screen_wraps_step_canvas_viewer_not_atlas_viewer() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
assert_object(screen._viewer).override_failure_message(
"RegionalScreen must wrap AtlasWindowViewer (the zoom ladder) since T-1153,"
"RegionalScreen must wrap StepCanvasViewer (the stepped ladder) since T-1182,"
+ " not the retired AtlasViewer heightmap-texture display"
).is_instanceof(AtlasWindowViewer)
).is_instanceof(StepCanvasViewer)
# =============================================================================
-979
View File
@@ -1,979 +0,0 @@
## T-1142 (Jeroen's second/third hands-on findings): pure-function tests for
## AtlasWindowViewer's fit-and-center math (fit_window_view) and pole-wall
## pan clamp (clamp_pan_offset_to_pole_wall) — both extracted specifically so
## the "viewport + n -> zoom/offset" transform is unit-testable without a
## live Control tree.
class_name TestAtlasWindowGeometry
extends GdUnitTestSuite
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
const MIN_ZOOM: float = 0.5
const MAX_ZOOM: float = 8.0
const CELL_PIXEL_SIZE: float = 16.0
# =============================================================================
# fit_window_view — the "postage stamp" fix (item 2)
# =============================================================================
## n=32, cell_px=16 -> native composite is 512x512. T-1145 item 1: COVER
## fit derives zoom from the LARGER viewport dimension (1920, not 1080) with
## NO margin factor — zoom = 1920 / 512 = 3.75 — well inside [MIN_ZOOM,
## MAX_ZOOM], so the clamp is a no-op here.
func test_fit_window_view_computes_expected_zoom_for_a_wide_viewport() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(1920.0, 1080.0), 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
var expected_zoom: float = 1920.0 / 512.0
assert_float(fit["zoom"]).is_equal_approx(expected_zoom, 0.001)
## The composite must be CENTERED — offset.x/.y each leave an equal margin on
## both sides of the (n*cell_px*zoom)-sized composite (a NEGATIVE "margin" is
## fine and expected under cover — it just means the composite overhangs
## that axis, checked separately by test_fit_window_view_covers_with_no_gap).
func test_fit_window_view_centers_the_composite() -> void:
var viewport := Vector2(1920.0, 1080.0)
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
viewport, 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
var composite_scaled: float = 32.0 * CELL_PIXEL_SIZE * float(fit["zoom"])
var offset: Vector2 = fit["offset"]
# The composite's right/bottom edge is offset + composite_scaled — the
# margin on the far side must equal the margin on the near side (offset).
var right_margin: float = viewport.x - (offset.x + composite_scaled)
var bottom_margin: float = viewport.y - (offset.y + composite_scaled)
assert_float(right_margin).is_equal_approx(offset.x, 0.01)
assert_float(bottom_margin).is_equal_approx(offset.y, 0.01)
## T-1145 item 1 (Jeroen's round-2 finding, KALLAST window): a wide viewport
## must show NO side margins — the composite's LONG axis (the one the cover
## zoom is derived from) must land EXACTLY at the viewport edges (offset ~=
## 0 on that axis), and the SHORT axis must OVERHANG past both edges
## (negative margin — the composite is bigger than the viewport there,
## exactly what "cover" means). This is the literal assertion the coordinator
## asked for: no side margins at 16:9.
func test_fit_window_view_covers_with_no_gap_on_the_long_axis() -> void:
var viewport := Vector2(1920.0, 1080.0)
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
viewport, 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
var composite_scaled: float = 32.0 * CELL_PIXEL_SIZE * float(fit["zoom"])
var offset: Vector2 = fit["offset"]
# Long axis (X, 1920 > 1080): the composite must span EXACTLY the
# viewport width — zero margin on both sides.
assert_float(offset.x).override_failure_message(
"the long (cover) axis must have NO side margin — offset.x should be ~0"
).is_equal_approx(0.0, 0.5)
var right_margin: float = viewport.x - (offset.x + composite_scaled)
assert_float(right_margin).override_failure_message(
"the long (cover) axis's far edge must have NO margin either"
).is_equal_approx(0.0, 0.5)
# Short axis (Y, 1080 < 1920): the composite must OVERHANG (negative
# margin) past BOTH edges — this is the data that extends into pan-space.
assert_float(offset.y).override_failure_message(
"the short axis must OVERHANG past the top edge (negative offset)"
).is_less(0.0)
## A TALL viewport (portrait) must cover the same way, just with the axes
## swapped — long axis (Y) gets zero margin, short axis (X) overhangs.
func test_fit_window_view_covers_a_tall_viewport_too() -> void:
var viewport := Vector2(1080.0, 1920.0)
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
viewport, 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
var offset: Vector2 = fit["offset"]
assert_float(offset.y).override_failure_message(
"the long (cover) axis (Y, portrait) must have NO side margin"
).is_equal_approx(0.0, 0.5)
assert_float(offset.x).override_failure_message(
"the short axis (X, portrait) must overhang past the left edge"
).is_less(0.0)
## A perfectly square viewport needs NO overhang on either axis — cover and
## contain agree exactly at a 1:1 aspect ratio (the degenerate case where
## "long" and "short" axis are the same).
func test_fit_window_view_square_viewport_has_no_overhang_either_axis() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(1024.0, 1024.0), 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_vector(fit["offset"]).is_equal_approx(Vector2.ZERO, Vector2(0.5, 0.5))
## Jeroen's exact bug: an n=32 composite (512px native) in a real ~1920px
## viewport must NOT render at zoom=1.0 (the old, unfitted "postage stamp"
## behavior) — the fit must scale it up to fill (now: COVER) the viewport.
func test_fit_window_view_scales_up_a_small_composite_to_fill_the_viewport() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(1920.0, 1080.0), 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit["zoom"]).override_failure_message(
"a 512px composite in a 1920x1080 viewport must be scaled UP, not left at 1.0"
).is_greater(1.0)
## A huge n (e.g. n=64 at a tiny viewport) must clamp to MIN_ZOOM, never
## shrink the composite into illegibility below the floor.
func test_fit_window_view_clamps_to_min_zoom_for_a_tiny_viewport() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(200.0, 150.0), 64, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit["zoom"]).is_equal_approx(MIN_ZOOM, 0.001)
## A small n (e.g. n=2) at a huge viewport must clamp to MAX_ZOOM, never
## scale past the ceiling.
func test_fit_window_view_clamps_to_max_zoom_for_a_tiny_composite() -> void:
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(3840.0, 2160.0), 2, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit["zoom"]).is_equal_approx(MAX_ZOOM, 0.001)
## Degenerate inputs (zero viewport, zero n) must not divide by zero — a safe
## fallback (zoom=1.0, offset=ZERO), never a crash or NaN.
func test_fit_window_view_degenerate_inputs_are_safe() -> void:
var fit_zero_viewport: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2.ZERO, 32, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit_zero_viewport["zoom"]).is_equal_approx(1.0, 0.001)
var fit_zero_n: Dictionary = AtlasWindowGeometry.fit_window_view(
Vector2(1920.0, 1080.0), 0, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
)
assert_float(fit_zero_n["zoom"]).is_equal_approx(1.0, 0.001)
# =============================================================================
# clamp_pan_offset_to_pole_wall — item 5 (pole hard wall, row axis only)
# =============================================================================
## Deep inside the valid range (window nowhere near a pole), the clamp must
## be a no-op — offset passes through unchanged.
func test_pole_wall_clamp_is_a_noop_far_from_the_poles() -> void:
var offset := Vector2(10.0, 20.0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
offset, Vector2(1920.0, 1080.0), Vector2i(0, 0), 32, 4785, CELL_PIXEL_SIZE, 1.0
)
assert_that(clamped).is_equal(offset)
## X is NEVER clamped by the pole wall (item 6: east-west is seamless) — even
## an absurdly large X offset passes through untouched.
func test_pole_wall_clamp_never_touches_x() -> void:
var offset := Vector2(999999.0, 0.0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
offset, Vector2(1920.0, 1080.0), Vector2i(0, 0), 32, 4785, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped.x).is_equal_approx(999999.0, 0.001)
## The core pole-wall behavior: dragging FAR past the north pole (offset.y
## driven to an extreme) must clamp — the resulting offset must be LESS than
## the extreme requested, and a SECOND, even-more-extreme drag must produce
## the SAME clamped value (further dragging is inert once pinned at the wall).
func test_pole_wall_clamp_pins_offset_when_dragged_past_the_pole() -> void:
var rows_half := 100
var held_center := Vector2i(0, 90) # near the south pole already (row 90 of 100)
var extreme_offset := Vector2(0.0, 5000.0) # a huge downward drag
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
extreme_offset, Vector2(800.0, 800.0), held_center, 32, rows_half, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped.y).override_failure_message(
"an extreme drag toward the pole must be clamped, not pass through"
).is_less(extreme_offset.y)
var even_more_extreme := Vector2(0.0, 50000.0)
var clamped_again: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
even_more_extreme, Vector2(800.0, 800.0), held_center, 32, rows_half, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped_again.y).override_failure_message(
"further dragging past an already-pinned wall must be inert (same clamped value)"
).is_equal_approx(clamped.y, 0.01)
## Symmetric check on the north side: a huge UPWARD drag near the north pole
## also clamps.
func test_pole_wall_clamp_pins_offset_on_the_north_side_too() -> void:
var rows_half := 100
var held_center := Vector2i(0, -90) # near the north pole
var extreme_offset := Vector2(0.0, -5000.0) # a huge upward drag
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
extreme_offset, Vector2(800.0, 800.0), held_center, 32, rows_half, CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped.y).override_failure_message(
"an extreme drag toward the north pole must be clamped"
).is_greater(extreme_offset.y)
## rows_half <= 0 (a no-radius body, or a degenerate district_extent()) means
## "no wall concept" — the clamp is a no-op, matching
## canonicalize_district_center()'s own no-radius identity disposition.
func test_pole_wall_clamp_is_noop_when_rows_half_is_zero() -> void:
var offset := Vector2(0.0, 999999.0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
offset, Vector2(800.0, 800.0), Vector2i(0, 0), 32, 0, CELL_PIXEL_SIZE, 1.0
)
assert_that(clamped).is_equal(offset)
## Tiny-body edge case (documented open item in atlas_window_viewer.gd's own
## _clamp_offset_to_pole_wall doc): a window TALLER than the whole planet's
## row span (n=64 window, rows_half=10 -> pole-to-pole is only 20 districts)
## must not crash or produce an inverted/degenerate clamp range — the offset
## still comes back as a finite Vector2, and repeated extreme drags still
## converge to a stable pinned value (not NaN, not unbounded).
func test_pole_wall_clamp_handles_a_window_taller_than_the_planet() -> void:
var rows_half := 10
var held_n := 64
var held_center := Vector2i(0, 0)
var clamped: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
Vector2(0.0, 999999.0), Vector2(800.0, 800.0), held_center, held_n, rows_half,
CELL_PIXEL_SIZE, 1.0
)
assert_bool(is_finite(clamped.y)).override_failure_message(
"a window taller than the planet's row span must still produce a finite clamp"
).is_true()
var clamped_again: Vector2 = AtlasWindowGeometry.clamp_pan_offset_to_pole_wall(
Vector2(0.0, 9999999.0), Vector2(800.0, 800.0), held_center, held_n, rows_half,
CELL_PIXEL_SIZE, 1.0
)
assert_float(clamped_again.y).is_equal_approx(clamped.y, 0.01)
# =============================================================================
# Cross-check: clamp bounds derived from district_extent() (the SAME source
# canonicalize_district_center() uses) — confirms the two T-1142 fixes (item
# 5 pole wall, item 6a wrap/clamp) agree on what "the pole" even is.
# =============================================================================
func test_pole_wall_rows_half_matches_canonicalize_rows_half() -> void:
var radius_km := 6238.4 # GJ380c
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var rows_half: int = int(extent["rows_half"])
# A center exactly at (0, rows_half) must canonicalize to itself (already
# at the pole boundary, not past it) — pins that the SAME rows_half both
# fixes consume describes an inclusive boundary, not an exclusive one.
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, rows_half), radius_km
)
assert_int(canonical.y).is_equal(rows_half)
# =============================================================================
# T-1153: select_rung() — REDESIGNED (live round 3 finding) per-rung
# single-window COVERAGE CEILING model, superseding the original
# `2x`-visual-tolerance-only reading of design doc §5. Select the FINEST
# rung whose own single-window coverage ceiling (MAX_COVERAGE_M) still
# covers the current world extent: Quarter <= 32,768 m; District <=
# 131,072 m; Region otherwise (including tiled coverage beyond its own
# single-window ceiling, a viewer-level concern — see select_rung()'s own
# doc for the full derivation and why this REPLACES the earlier two-gate
# design entirely, not just patches it).
# =============================================================================
## Deep zoom-in (a tiny extent) selects Quarter — comfortably under its own
## 32,768 m ceiling.
func test_select_rung_picks_quarter_well_under_its_ceiling() -> void:
var rung: String = AtlasWindowGeometry.select_rung(2000.0, 1000.0)
assert_str(rung).is_equal("Quarter")
## An extent past Quarter's own ceiling but under District's selects
## District — the finest rung that can still cover it in one window.
func test_select_rung_picks_district_between_the_two_ceilings() -> void:
# 60,000 m is past Quarter's 32,768 m ceiling but well under District's
# 131,072 m one.
var rung: String = AtlasWindowGeometry.select_rung(60_000.0, 100.0)
assert_str(rung).is_equal("District")
## An extent past BOTH Quarter's and District's ceilings selects Region —
## neither finer rung's single window can cover this much world.
func test_select_rung_picks_region_past_both_finer_ceilings() -> void:
var rung: String = AtlasWindowGeometry.select_rung(40_075_264.0, 1920.0)
assert_str(rung).is_equal("Region")
## Exactly AT Quarter's own ceiling (32,768 m) must still select Quarter —
## the rule is `<=`, not `<`.
func test_select_rung_quarter_ceiling_boundary_is_inclusive() -> void:
var rung: String = AtlasWindowGeometry.select_rung(32_768.0, 100.0)
assert_str(rung).is_equal("Quarter")
## One metre past Quarter's ceiling must flip to District — confirms the
## ceiling bites right at its own boundary, not one cell short of it.
func test_select_rung_one_past_quarter_ceiling_is_district() -> void:
var rung: String = AtlasWindowGeometry.select_rung(32_769.0, 100.0)
assert_str(rung).is_equal("District")
## Exactly AT District's own ceiling (131,072 m) must still select District.
func test_select_rung_district_ceiling_boundary_is_inclusive() -> void:
var rung: String = AtlasWindowGeometry.select_rung(131_072.0, 100.0)
assert_str(rung).is_equal("District")
## One metre past District's ceiling must flip to Region.
func test_select_rung_one_past_district_ceiling_is_region() -> void:
var rung: String = AtlasWindowGeometry.select_rung(131_073.0, 100.0)
assert_str(rung).is_equal("Region")
## canvas_px is unused by the coverage rule (kept for signature stability,
## see select_rung()'s own doc) — degenerate/zero values must not change the
## selected rung at all, unlike the old `2x`-tolerance design's special-cased
## fallback.
func test_select_rung_canvas_px_does_not_affect_selection() -> void:
var with_real_canvas: String = AtlasWindowGeometry.select_rung(2000.0, 1000.0)
var with_zero_canvas: String = AtlasWindowGeometry.select_rung(2000.0, 0.0)
assert_str(with_zero_canvas).is_equal(with_real_canvas)
## spacing_for_rung() is select_rung()'s inverse lookup — pin the three known
## values against the D-243 constants directly (not against RUNG_TABLE
## indices, which would just restate the implementation).
func test_spacing_for_rung_matches_d243_constants() -> void:
assert_float(AtlasWindowGeometry.spacing_for_rung("Quarter")).is_equal_approx(512.0, 0.001)
assert_float(AtlasWindowGeometry.spacing_for_rung("District")).is_equal_approx(2048.0, 0.001)
assert_float(AtlasWindowGeometry.spacing_for_rung("Region")).is_equal_approx(204_800.0, 0.001)
## An unknown tag falls back to District — matching the server's own
## "unknown -> District" posture at every wire-decode boundary.
func test_spacing_for_rung_unknown_tag_falls_back_to_district() -> void:
assert_float(AtlasWindowGeometry.spacing_for_rung("Nonsense")).is_equal_approx(2048.0, 0.001)
## MAX_COVERAGE_M's three values, pinned directly against the formulas
## select_rung()'s own doc derives them from — a regression guard
## independent of select_rung()'s own boundary tests above, so a future
## accidental edit to the constants table itself (not just the selection
## logic) is caught here too.
func test_max_coverage_m_matches_derived_formulas() -> void:
assert_float(AtlasWindowGeometry.MAX_COVERAGE_M["Quarter"]).is_equal_approx(32_768.0, 0.001)
assert_float(AtlasWindowGeometry.MAX_COVERAGE_M["District"]).is_equal_approx(131_072.0, 0.001)
assert_float(AtlasWindowGeometry.MAX_COVERAGE_M["Region"]).is_equal_approx(13_107_200.0, 0.001)
## The exact scenario that surfaced the original design flaw
## (live-testing enter_orbital()'s own fit zoom): a whole Earth-like body's
## circumference (~40,075 km, matching AtlasDescendGeometry.district_extent()'s
## own cols*DISTRICT_M for radius=6371km) fitted to a 1920px-wide viewport at
## CELL_PIXEL_SIZE=16 must select Region — the direct regression guard for
## the bug an early version of select_rung() had (picking District here,
## which would have meant the canonical orbital frame requests a
## District-tier derive spanning an entire planet — the exact R1-catastrophe
## cost scenario the design doc §4 rejects).
func test_select_rung_at_orbital_fit_zoom_selects_region() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
var composite_native: float = float(n) * CELL_PIXEL_SIZE
var viewport := Vector2(1920.0, 1080.0)
var fit_zoom: float = maxf(viewport.x, viewport.y) / composite_native
var world_extent: float = AtlasWindowGeometry.world_extent_m(CELL_PIXEL_SIZE, fit_zoom, viewport)
var rung: String = AtlasWindowGeometry.select_rung(
world_extent, maxf(viewport.x, viewport.y)
)
assert_str(rung).override_failure_message(
"the canonical orbital fit-zoom (whole-planet view) must select Region,"
+ " never a District-tier derive spanning an entire planet"
).is_equal("Region")
## **Live round 3 regression, the direct fix target:** at 1600x900 (the
## coordinator's capture viewport), zooming IN from the orbital fit all the
## way to Quarter's own ceiling must pass through District along the way —
## a wheel-zoom gesture crossing world_extent_m from Region's territory down
## to Quarter's must select District for SOME real span of extent in
## between, not skip straight from Region to Quarter (the exact "money shot"
## the coordinator wants capture-worthy: a visible SHARPEN in place, not a
## jump).
func test_select_rung_district_is_reachable_between_region_and_quarter() -> void:
# An extent comfortably between District's and Quarter's ceilings (e.g.
# the midpoint) must select District — proving the band is non-empty,
# unlike the old two-gate design where it was empty by construction at
# every real viewport (see git history / the coordinator's live-round
# finding for the retired analysis).
var midpoint: float = (
(AtlasWindowGeometry.MAX_COVERAGE_M["Quarter"] as float)
+ (AtlasWindowGeometry.MAX_COVERAGE_M["District"] as float)
) * 0.5
var rung: String = AtlasWindowGeometry.select_rung(midpoint, 1600.0)
assert_str(rung).override_failure_message(
"District must be reachable between Quarter's and District's own"
+ " coverage ceilings — the redesigned rule must not skip it"
).is_equal("District")
# =============================================================================
# T-1153: world_extent_m() — the `E` half of the §5 rule, computed from the
# viewer's own zoom/viewport state.
# =============================================================================
## At zoom=1.0, CELL_PIXEL_SIZE=16: one DISTRICT (2,048 m, the fixed display
## unit — see world_extent_m()'s own doc for why this is rung-INDEPENDENT)
## occupies 16 screen px, so a 1920px-wide viewport shows
## 1920/16 * 2048 = 245,760 m.
func test_world_extent_m_at_zoom_one() -> void:
var extent: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
assert_float(extent).is_equal_approx(1920.0 / CELL_PIXEL_SIZE * 2048.0, 1.0)
## Doubling the zoom must HALVE the displayed world extent — zooming in
## shows less world, not more.
func test_world_extent_m_halves_when_zoom_doubles() -> void:
var extent_1x: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
var extent_2x: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 2.0, Vector2(1920.0, 1080.0)
)
assert_float(extent_2x).is_equal_approx(extent_1x * 0.5, 1.0)
## The composite's on-screen footprint is rung-invariant (world_extent_m()'s
## own doc) — a change in held rung with NO change in zoom/viewport must
## leave the displayed world extent UNCHANGED. This is the direct regression
## test for the bug this function's signature once had (a granularity_v2
## parameter that silently changed the formula per rung, when only zoom
## should) — the function no longer TAKES a rung parameter at all, so this
## pins that omission is intentional, not an oversight.
func test_world_extent_m_has_no_rung_parameter() -> void:
var extent_a: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
var extent_b: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 1.0, Vector2(1920.0, 1080.0)
)
assert_float(extent_a).is_equal_approx(extent_b, 0.001)
## Degenerate zoom (<=0) must not divide by zero — a safe zero extent.
func test_world_extent_m_degenerate_zoom_is_safe() -> void:
var extent: float = AtlasWindowGeometry.world_extent_m(
CELL_PIXEL_SIZE, 0.0, Vector2(1920.0, 1080.0)
)
assert_float(extent).is_equal_approx(0.0, 0.001)
# =============================================================================
# T-1153: is_fully_zoomed_out() — Jeroen's HARD condition's trigger predicate.
# =============================================================================
func test_is_fully_zoomed_out_true_when_extent_covers_full_circumference() -> void:
var radius_km := 6371.0
var circumference_m: float = TAU * radius_km * 1000.0
assert_bool(AtlasWindowGeometry.is_fully_zoomed_out(circumference_m, radius_km)).is_true()
assert_bool(
AtlasWindowGeometry.is_fully_zoomed_out(circumference_m * 1.5, radius_km)
).is_true()
func test_is_fully_zoomed_out_false_when_extent_is_less_than_circumference() -> void:
var radius_km := 6371.0
var circumference_m: float = TAU * radius_km * 1000.0
assert_bool(
AtlasWindowGeometry.is_fully_zoomed_out(circumference_m * 0.5, radius_km)
).is_false()
## A no-radius body (tiny test body) has no circumference concept — never
## auto-resets, matching enter_orbital()'s own no-radius fallback disposition.
func test_is_fully_zoomed_out_false_for_no_radius_body() -> void:
assert_bool(AtlasWindowGeometry.is_fully_zoomed_out(1e12, 0.0)).is_false()
# =============================================================================
# T-1153: screen_center_to_district() — the shared screen<->district formula
# behind both the pan-edge refetch and the rung-reselect refetch.
# =============================================================================
## At the exact center of a symmetric fit (offset centers the composite,
## zoom=1.0), the screen center must map back to the held center exactly.
func test_screen_center_to_district_at_rest_returns_held_center() -> void:
var held_n := 32
var held_center := Vector2i(10, 20)
var composite_native: float = float(held_n) * CELL_PIXEL_SIZE
var viewport := Vector2(composite_native, composite_native)
var offset := Vector2.ZERO # composite exactly fills the viewport, top-left at origin
var result: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, offset, 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
assert_that(result).is_equal(held_center)
## Panning the offset must shift the recovered district position in the
## OPPOSITE direction of the offset shift (dragging the composite right
## reveals districts to the WEST at screen-center).
func test_screen_center_to_district_shifts_with_pan_offset() -> void:
var held_n := 32
var held_center := Vector2i(0, 0)
var composite_native: float = float(held_n) * CELL_PIXEL_SIZE
var viewport := Vector2(composite_native, composite_native)
var at_rest: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, Vector2.ZERO, 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
var panned: Vector2i = AtlasWindowGeometry.screen_center_to_district(
viewport, Vector2(CELL_PIXEL_SIZE * 4.0, 0.0), 1.0, CELL_PIXEL_SIZE, held_center, held_n
)
assert_int(panned.x).override_failure_message(
"dragging the composite EAST (positive offset) must reveal districts to the WEST"
).is_less(at_rest.x)
# =============================================================================
# T-1153 (moved from atlas_window_viewer.gd for testability): WASD held-pan
# direction is exercised live only (reads the global Input singleton) —
# edge-scroll suppression/direction are pure and covered here directly.
# =============================================================================
func test_is_cursor_edge_scrolling_true_near_an_edge() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, false, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_true()
func test_is_cursor_edge_scrolling_false_away_from_any_edge() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, false, Vector2(800.0, 600.0), Vector2(400.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_is_cursor_edge_scrolling_suppressed_when_over_ui() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
true, true, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_is_cursor_edge_scrolling_suppressed_without_app_focus() -> void:
var result: bool = AtlasWindowGeometry.is_cursor_edge_scrolling(
false, false, Vector2(800.0, 600.0), Vector2(10.0, 300.0), 24.0
)
assert_bool(result).is_false()
func test_edge_scroll_direction_points_west_near_left_edge() -> void:
var direction: Vector2 = AtlasWindowGeometry.edge_scroll_direction(
Vector2(800.0, 600.0), Vector2(5.0, 300.0), 24.0
)
assert_float(direction.x).is_less(0.0)
assert_float(direction.y).is_equal_approx(0.0, 0.001)
# =============================================================================
# T-1153, live round 3 (Jeroen's ruling, design doc §4): compute_tile_grid()
# — the orbital rest state's multi-window mosaic.
# =============================================================================
## The exact live-round scenario: GJ380c/Lendel (radius 6238.4 km) needs a
## 3x2 = 6-tile grid — the coordinator's own estimate, confirmed here as an
## executable regression.
func test_compute_tile_grid_lendel_produces_six_tiles() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(6238.4)
assert_int(tiles.size()).override_failure_message(
"GJ380c/Lendel must tile into 3x2=6 windows, matching the coordinator's own"
+ " live-round finding (13,107.2 km single-window coverage vs. 39,198 km"
+ " circumference)"
).is_equal(6)
## A tiny body whose whole circumference fits in ONE Region window's
## coverage ceiling must produce exactly ONE tile — tiling degenerates
## gracefully to the pre-existing single-window behavior when it isn't
## actually needed.
func test_compute_tile_grid_tiny_body_produces_one_tile() -> void:
# radius small enough that circumference << MAX_COVERAGE_M["Region"]
# (13,107,200 m) — a few hundred km radius comfortably qualifies.
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(50.0)
assert_int(tiles.size()).is_equal(1)
assert_that(tiles[0]).is_equal(Vector2i.ZERO)
## A no-radius body (tiny test body) must produce exactly one tile at the
## canonical origin — matching enter_orbital()'s own no-radius fallback
## disposition (no circumference/tiling concept without a radius).
func test_compute_tile_grid_no_radius_produces_single_origin_tile() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(0.0)
assert_int(tiles.size()).is_equal(1)
assert_that(tiles[0]).is_equal(Vector2i.ZERO)
## Every tile center must be a LEGAL canonicalized DistrictPos — column
## wrapped into [0, cols), row clamped into [-rows_half, rows_half] — the
## same range canonicalize_district_center() enforces everywhere else in
## this cluster (pan refetch, entry, rung-reselect). A raw, uncanonicalized
## tile center would fail the server's own normalize_window_center() (or
## silently alias to a different tile than intended).
func test_compute_tile_grid_tiles_are_all_canonicalized() -> void:
var radius_km := 6238.4
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var rows_half: int = int(extent["rows_half"])
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(radius_km)
for tile: Vector2i in tiles:
assert_int(tile.x).override_failure_message(
"tile column %d must be wrapped into [0, %d)" % [tile.x, cols]
).is_greater_equal(0)
assert_int(tile.x).is_less(cols)
assert_int(tile.y).override_failure_message(
"tile row %d must be clamped into [-%d, %d]" % [tile.y, rows_half, rows_half]
).is_greater_equal(-rows_half)
assert_int(tile.y).is_less_equal(rows_half)
## No two tiles may share the same canonicalized center — compute_tile_grid()
## must dedupe (a pole-row clamp or column-wrap collision producing the exact
## same DistrictPos twice would otherwise request/draw the same tile twice,
## wasting a request and drawing one tile over another).
func test_compute_tile_grid_has_no_duplicate_centers() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(6238.4)
var seen: Dictionary = {}
for tile: Vector2i in tiles:
assert_bool(seen.has(tile)).override_failure_message(
"tile center %s appears more than once in the grid" % str(tile)
).is_false()
seen[tile] = true
## The tile grid's own center of mass must land on the canonical origin
## (0,0) — the tile-set's symmetric layout (each axis' centers computed as
## `(index - (count-1)/2) * TILE_N`) is centered on the SAME canonical origin
## enter_orbital() uses, so the tile-set's overall framing agrees with
## single-window enter_orbital()'s own "center on (0,0)" contract.
func test_compute_tile_grid_is_centered_on_the_canonical_origin() -> void:
var tiles: Array = AtlasWindowGeometry.compute_tile_grid(6238.4)
var sum_col := 0
var sum_row := 0
for tile: Vector2i in tiles:
sum_col += tile.x
sum_row += tile.y
# Column centers wrap (periodic), so a raw average isn't meaningful there
# the way it is for rows — assert row symmetry directly instead (rows
# never wrap, so their average must be very close to 0 for a
# symmetric grid).
var avg_row: float = float(sum_row) / float(tiles.size())
assert_float(avg_row).override_failure_message(
"the tile grid's row centers must average to ~0 (symmetric around the"
+ " canonical origin's equator row)"
).is_equal_approx(0.0, float(AtlasWindowGeometry.TILE_N))
# =============================================================================
# Live round 4: district_to_canvas_local() + recompute_offset_for_held_n_change()
# — the two pure functions behind both round-4 draw-path fixes (tile mosaic
# placement, single-window offset recompute across a rung crossing).
# =============================================================================
## A district AT the held window's own center must land at canvas-local
## `(held_n/2 * cell_px, held_n/2 * cell_px)` — the center of the
## `[0, held_n*cell_px)` square the single-window `Rect2(0,0,extent,extent)`
## draw call already assumes.
func test_district_to_canvas_local_center_district_lands_at_half_extent() -> void:
var held_center := Vector2i(100, 200)
var held_n := 64
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
Vector2(held_center), held_center, held_n, CELL_PIXEL_SIZE
)
var expected: float = float(held_n) * 0.5 * CELL_PIXEL_SIZE
assert_that(result).is_equal(Vector2(expected, expected))
## The window's own top-left corner (held_center - held_n/2) must land at
## canvas-local (0,0) — the exact invariant single-window `_draw()` and
## `fit_window_view()` both assume.
func test_district_to_canvas_local_top_left_corner_lands_at_origin() -> void:
var held_center := Vector2i(0, 0)
var held_n := 32
var top_left := Vector2(held_center) - Vector2.ONE * (float(held_n) * 0.5)
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
top_left, held_center, held_n, CELL_PIXEL_SIZE
)
assert_that(result).is_equal(Vector2.ZERO)
## Live round 4's OWN repro, pinned directly: a tile far from held_center
## (0,0) at whole-body scale (held_n ~19,139, Lendel's raw circumference)
## must NOT land near canvas-local (0,0) — the round-4 bug's exact failure
## mode (treating absolute district (0,0) as the canvas origin regardless of
## held_center/held_n) would place it there instead.
func test_district_to_canvas_local_matches_the_live_round_4_repro_scale() -> void:
var held_center := Vector2i.ZERO
var held_n := 19139 # Lendel's raw district-column count (live round 4's own repro)
var tile_center := Vector2(6400, 0) # one TILE_N east of the body's own center
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
tile_center, held_center, held_n, CELL_PIXEL_SIZE
)
var buggy_result: Vector2 = tile_center * CELL_PIXEL_SIZE # the round-4 bug's own formula
assert_bool(is_equal_approx(result.x, buggy_result.x)).override_failure_message(
"a tile away from held_center must NOT land where the round-4 bug's"
+ " absolute-district-(0,0)-relative formula would put it — got %.1f, the"
+ " buggy formula's own value is %.1f"
% [result.x, buggy_result.x]
).is_false()
## Zero held_n is a degenerate/never-real-in-practice input (a body always
## has SOME district extent) but must not divide-by-zero or crash — `half`
## is simply 0, so the district maps 1:1 to canvas-local (scaled by cell_px).
func test_district_to_canvas_local_zero_held_n_does_not_crash() -> void:
var result: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
Vector2(5, 5), Vector2i.ZERO, 0, CELL_PIXEL_SIZE
)
assert_that(result).is_equal(Vector2(5, 5) * CELL_PIXEL_SIZE)
# =============================================================================
# Live round 5: nearest_wrap_image() — the tile-mosaic WRAP half of "the
# mosaic doesn't fully draw" (the left-third-black repro).
# =============================================================================
## Live round 5's OWN repro, pinned exactly: Lendel's wrapped tile
## canonicalizes to column 12739 (`-6400 mod 19139`) — the CORRECT
## request/cache key — but its nearest wrap-image relative to the canonical
## origin (held_center.x = 0) is -6400, the actual visible position
## immediately west of center.
func test_nearest_wrap_image_matches_the_lendel_repro() -> void:
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, 0, 19139)
assert_int(result).override_failure_message(
"the wrapped tile's nearest wrap-image relative to held_center=0 must be"
+ " -6400 (its actual on-screen position), not 12739 (the correct REQUEST"
+ " key, but the wrong DRAW position)"
).is_equal(-6400)
## The two Lendel tiles that were NEVER wrapped (already close to
## held_center) must round-trip unchanged — the fix must not perturb tiles
## that were already drawing correctly.
func test_nearest_wrap_image_is_a_noop_for_already_nearby_columns() -> void:
var cols := 19139
for col: int in [0, 6400]:
var result: int = AtlasWindowGeometry.nearest_wrap_image(col, 0, cols)
assert_int(result).override_failure_message(
"column %d is already the nearest wrap-image to held_center=0 — must"
+ " be returned unchanged" % col
).is_equal(col)
## The result must always be a LEGAL wrap-image of the canonical column —
## i.e. `result mod cols == canonical_col mod cols` — regardless of which
## image is nearest. This is the correctness invariant the whole function
## exists to preserve: re-expressing a column for DRAWING must never change
## WHICH district it actually refers to.
func test_nearest_wrap_image_preserves_the_canonical_identity() -> void:
var cols := 19139
for held_col: int in [-50000, -1, 0, 1, 9569, 19138, 50000]:
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, held_col, cols)
assert_int(posmod(result, cols)).override_failure_message(
"nearest_wrap_image(12739, %d, %d) = %d must still canonicalize back"
+ " to 12739 — it may only pick a DIFFERENT wrap-image, never a"
+ " different district" % [held_col, cols, result]
).is_equal(12739)
## The chosen wrap-image must be the CLOSEST one to held_center — never
## farther than half the circumference away (otherwise a different
## wrap-image would have been nearer).
func test_nearest_wrap_image_is_within_half_circumference_of_held_center() -> void:
var cols := 19139
for canonical_col: int in [0, 1, 9569, 12739, 19138]:
for held_col: int in [-30000, -500, 0, 500, 25000]:
var result: int = AtlasWindowGeometry.nearest_wrap_image(canonical_col, held_col, cols)
var distance: int = absi(result - held_col)
assert_int(distance).override_failure_message(
(
"nearest_wrap_image(%d, %d, %d) = %d is %d districts from"
+ " held_center — must never exceed half the circumference"
+ " (%d), or a closer wrap-image exists"
)
% [canonical_col, held_col, cols, result, distance, cols / 2]
).is_less_equal(cols / 2)
## `cols <= 0` (no-radius bodies, which never tile per compute_tile_grid()'s
## own doc) must be a safe no-op passthrough — no periodicity to resolve.
func test_nearest_wrap_image_zero_cols_is_a_passthrough() -> void:
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, 0, 0)
assert_int(result).is_equal(12739)
## The coordinator's own draw-position counterpart to
## test_compute_tile_grid_tiles_are_all_canonicalized(): the wrapped tile's
## DRAW rect (via district_to_canvas_local(), fed through
## nearest_wrap_image() the way _draw_tile_mosaic() now does) must land
## SUBSTANTIALLY on-canvas when the view covers the whole body — the exact
## Lendel shape (whole-body fit at entry, held_center at the canonical
## origin). A bare `Rect2.intersects()` check is NOT discriminating enough
## here: at Lendel's own whole-body-fit scale, the BUGGY placement (feeding
## the canonical column directly) happens to clip the viewport edge by only
## a couple of px (confirmed by hand-computation — the tile-grid's own
## edge-to-edge tiling means a full-circumference shift lands almost
## exactly one screen-width away, so `intersects()` alone would pass on a
## near-miss that still reads as "the left third is black" visually).
## Asserting a MEANINGFUL overlap FRACTION (at least half the tile's own
## area) is what actually distinguishes "correctly drawn" from "barely
## clipping the edge."
func test_wrapped_tile_draw_rect_lands_substantially_on_canvas_at_whole_body_view() -> void:
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var held_center := Vector2i.ZERO
var held_n: int = cols # enter_orbital()'s own whole-body held_n
var tile_n: int = AtlasWindowGeometry.TILE_N
var half_tile: float = float(tile_n) * 0.5
# The whole-body fit zoom/viewport (matching enter_orbital()'s own fit).
var viewport := Vector2(1600.0, 900.0)
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
viewport, held_n, CELL_PIXEL_SIZE, 0.0001, 64.0
)
var view_zoom: float = fit["zoom"]
var view_offset: Vector2 = fit["offset"]
# The wrapped tile's own canonical center — mirrors compute_tile_grid()'s
# own dedup/canonicalize step for Lendel's westmost tile.
var wrapped_raw_col := -6400
var canonical_col: int = posmod(wrapped_raw_col, cols)
var draw_col: int = AtlasWindowGeometry.nearest_wrap_image(canonical_col, held_center.x, cols)
var tile_top_left := Vector2(float(draw_col) - half_tile, 0.0 - half_tile)
var local_origin: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
tile_top_left, held_center, held_n, CELL_PIXEL_SIZE
)
var extent_px: float = float(tile_n) * CELL_PIXEL_SIZE
# Canvas-local -> screen space: _canvas.position = view_offset,
# _canvas.scale = view_zoom (AtlasWindowViewer._apply_transform()'s own
# transform, mirrored here since this is a pure-geometry test with no
# live Control/Node2D tree).
var screen_top_left: Vector2 = view_offset + local_origin * view_zoom
var screen_extent: Vector2 = Vector2(extent_px, extent_px) * view_zoom
var tile_rect := Rect2(screen_top_left, screen_extent)
var viewport_rect := Rect2(Vector2.ZERO, viewport)
var overlap: Rect2 = viewport_rect.intersection(tile_rect)
var tile_area: float = screen_extent.x * screen_extent.y
var overlap_fraction: float = 0.0
if tile_area > 0.0:
overlap_fraction = (overlap.size.x * overlap.size.y) / tile_area
assert_float(overlap_fraction).override_failure_message(
(
"the wrapped tile's draw rect %s overlaps the viewport %s by only"
+ " %.1f%% of its own area — must be at least 50%% when the view"
+ " covers the whole body. This is live round 5's 'left third of the"
+ " mosaic is black' repro: drawing the CANONICAL column (%d) directly"
+ " (without nearest_wrap_image()) places this tile off-canvas RIGHT"
+ " instead of its true position on the LEFT"
)
% [tile_rect, viewport_rect, overlap_fraction * 100.0, canonical_col]
).is_greater_equal(0.5)
## The core contract this function exists for: recomputing `_view_offset` so
## a KNOWN screen point continues to map to canvas-local
## `new_held_n/2 * cell_px` (the new window's own center) — i.e. feeding the
## OUTPUT back through district_to_canvas_local()'s own "center district ->
## half-extent local" identity (tested above) and applying the resulting
## transform must reproduce the SAME screen point exactly.
func test_recompute_offset_for_held_n_change_preserves_the_screen_point() -> void:
var screen_point := Vector2(800.0, 450.0)
var view_zoom := 2.5
var new_held_n := 16
var offset: Vector2 = AtlasWindowGeometry.recompute_offset_for_held_n_change(
screen_point, view_zoom, new_held_n, CELL_PIXEL_SIZE
)
var new_local: Vector2 = Vector2.ONE * (float(new_held_n) * 0.5 * CELL_PIXEL_SIZE)
var reconstructed_screen_point: Vector2 = new_local * view_zoom + offset
assert_that(reconstructed_screen_point).is_equal_approx(screen_point, Vector2.ONE * 0.01)
## Live round 4's OWN repro: crossing from Region (~thousands-districts held_n)
## to District (64) or Quarter (16) must produce a DIFFERENT offset than
## leaving `_view_offset` untouched would — pinning that this function's
## OUTPUT actually depends on `new_held_n` (the exact thing the round-4 bug
## got wrong by never calling this function at all).
func test_recompute_offset_for_held_n_change_differs_for_different_held_n() -> void:
var screen_point := Vector2(800.0, 450.0)
var view_zoom := 3.378 # live round 4's own District-band zoom value
var offset_district: Vector2 = AtlasWindowGeometry.recompute_offset_for_held_n_change(
screen_point, view_zoom, 64, CELL_PIXEL_SIZE
)
var offset_quarter: Vector2 = AtlasWindowGeometry.recompute_offset_for_held_n_change(
screen_point, view_zoom, 16, CELL_PIXEL_SIZE
)
assert_that(offset_district).override_failure_message(
"a rung crossing that changes held_n must recompute a DIFFERENT"
+ " _view_offset — reusing the same offset across the crossing is"
+ " exactly the live round 4 bug (composite renders off-canvas)"
).is_not_equal(offset_quarter)
# =============================================================================
# T-1172 round 2: cell_index_for_local_offset() — the shared painter/clip
# index formula (see its own doc for the "why shared, not duplicated" case).
# T-1170: these tests moved here from test_atlas_window_geometry_nature.gd —
# the function itself stayed on THIS file (AtlasWindowGeometry) rather than
# moving to atlas_window_geometry_nature.gd, since it is shared with
# AtlasWindowOverlay's terrain painter, a non-nature consumer — see that
## file's own header doc for the full split rationale.
# =============================================================================
func test_cell_index_for_local_offset_top_left_is_zero_zero() -> void:
var cell: Vector2i = AtlasWindowGeometry.cell_index_for_local_offset(0.0, 0.0, 6400, 64)
assert_that(cell).is_equal(Vector2i(0, 0))
## The exact live-repro numbers from T-1172 round 2's trace: a query whose
## district-space local offset is (2594.09, 5593.15) inside a 6400-wide,
## 64-cell-side window must resolve to (col=25, row=55) — pinned directly
## against the LIVE captured values that closed the investigation (both the
## painter's _build_tile_texture() and the clip independently produced this
## exact pair for the same query in the live trace).
func test_cell_index_for_local_offset_matches_the_live_trace_repro() -> void:
var cell: Vector2i = AtlasWindowGeometry.cell_index_for_local_offset(
2594.0849609375, 5593.15258789062, 6400, 64
)
assert_that(cell).override_failure_message(
"must match the live-captured painter/clip agreement point from the"
+ " T-1172 round 2 investigation — (col=25, row=55)"
).is_equal(Vector2i(25, 55))
func test_cell_index_for_local_offset_bottom_right_boundary_clamps_inside() -> void:
# local offset == n (the exclusive upper boundary) must clamp to the LAST
# cell, not overflow to a nonexistent grid_side'th cell.
var cell: Vector2i = AtlasWindowGeometry.cell_index_for_local_offset(6400.0, 6400.0, 6400, 64)
assert_that(cell).is_equal(Vector2i(63, 63))
func test_cell_index_for_local_offset_zero_n_or_grid_side_returns_sentinel() -> void:
assert_that(AtlasWindowGeometry.cell_index_for_local_offset(10.0, 10.0, 0, 64)).is_equal(
Vector2i(-1, -1)
)
assert_that(AtlasWindowGeometry.cell_index_for_local_offset(10.0, 10.0, 6400, 0)).is_equal(
Vector2i(-1, -1)
)
@@ -1,885 +0,0 @@
## T-1156 wave 1 / T-1170: pure-function tests for AtlasWindowGeometryNature's
## Layer-1 nature-overlay pixel mapping (layer1_pixel_to_world_m/
## world_m_to_district/layer1_pixel_to_canvas_local), per-rung visibility/
## filter policy (skeleton_class_visible_at_rung/course_class_visible_at_rung/
## confluences_visible_at_rung/mouths_visible_at_rung/basins_visible_at_rung/
## attractors_visible_at_rung), the D8 river_downstream decode
## (d8_downstream_target), and course width/opacity readers
## (course_class_width_px/course_class_opacity). Split from
## test_atlas_window_geometry.gd (already close to the gdlint max-file-lines
## cap) — same file-per-concern precedent as test_atlas_window_colors.gd being
## separate from test_atlas_window_overlay.gd.
##
## T-1170: this file's SUBJECT preload moved from AtlasWindowGeometry to
## AtlasWindowGeometryNature (the T-1170 split, see that file's own doc) —
## every symbol tested below now lives there. cell_index_for_local_offset()
## STAYED on AtlasWindowGeometry (shared with the non-nature terrain painter)
## — its tests stay in test_atlas_window_geometry.gd, not duplicated here.
class_name TestAtlasWindowGeometryNature
extends GdUnitTestSuite
const AtlasWindowGeometryNature := preload(
"res://ui/implant/apps/atlas/atlas_window_geometry_nature.gd"
)
const CELL_PIXEL_SIZE: float = 16.0
const DISTRICT_M: float = 2048.0
# =============================================================================
# layer1_pixel_to_world_m — the forward mirror of
# server/src/atlas/district_profile.rs's pixel_to_world_m(), verified against
# that function's source directly (not assumed).
# =============================================================================
## Column 0 is world/longitude 0 on every body — no -0.5 centering, unlike
## rows (longitude wraps and has no "half" concept the way latitude does).
func test_layer1_pixel_to_world_m_col_zero_is_world_x_zero() -> void:
var w: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(0.0, 0.0, 256.0, 128.0, 6371.0)
assert_float(w.x).is_equal_approx(0.0, 0.001)
## Row 0 is the NORTH POLE — server's own comment: `lat_frac = -0.5 = N pole`
## — which the forward map resolves to the MOST NEGATIVE wy (world Y
## increases southward, matching AtlasDescendGeometry.district_pos_at()'s own
## row-increases-southward convention on the inverse side of this mapping).
func test_layer1_pixel_to_world_m_row_zero_is_north_pole_negative_wy() -> void:
var radius_km := 6371.0
var grid_h := 128.0
var w: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(
0.0, 0.0, 256.0, grid_h, radius_km
)
var meridian_m: float = PI * radius_km * 1000.0
assert_float(w.y).is_equal_approx(-0.5 * meridian_m, 1.0)
## Row (grid_h - 1) is the SOUTH POLE — `lat_frac = +0.5 = S` — the most
## POSITIVE wy, the opposite extreme from row 0.
func test_layer1_pixel_to_world_m_last_row_is_south_pole_positive_wy() -> void:
var radius_km := 6371.0
var grid_h := 128.0
var w: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(
grid_h - 1.0, 0.0, 256.0, grid_h, radius_km
)
var meridian_m: float = PI * radius_km * 1000.0
assert_float(w.y).is_equal_approx(0.5 * meridian_m, 1.0)
## The equator row (grid_h / 2, approximately — the exact half-height pixel)
## is world Y ~0 — halfway between the two poles. Not EXACT (the denominator
## is grid_h - 1 = 127, not 128), so the tolerance is loose (200km).
func test_layer1_pixel_to_world_m_mid_row_is_near_equator() -> void:
var w: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(64.0, 0.0, 256.0, 128.0, 6371.0)
assert_float(w.y).is_equal_approx(0.0, 200_000.0)
## Column at grid_w (a full wrap) must equal the FULL circumference — the
## wrap point, matching longitude's periodic (not clamped) treatment.
func test_layer1_pixel_to_world_m_full_width_col_is_full_circumference() -> void:
var radius_km := 6371.0
var grid_w := 256.0
var w: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(
0.0, grid_w, grid_w, 128.0, radius_km
)
var circumference_m: float = TAU * radius_km * 1000.0
assert_float(w.x).is_equal_approx(circumference_m, 5.0)
## No-radius (tiny test body): 1 heightmap pixel = 1 DISTRICT_M metre exactly
## — matching pixel_to_world_m()'s own no-radius fallback and
## AtlasDescendGeometry.district_pos_at()'s no-radius branch on the inverse side.
func test_layer1_pixel_to_world_m_no_radius_is_one_pixel_one_district_m() -> void:
var w: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(3.0, 5.0, 64.0, 64.0, 0.0)
assert_that(w).is_equal(Vector2(5.0 * DISTRICT_M, 3.0 * DISTRICT_M))
## Degenerate grid dims (grid_w/grid_h <= 0) must not divide-by-zero or crash.
func test_layer1_pixel_to_world_m_zero_grid_dims_returns_zero() -> void:
var w: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(1.0, 1.0, 0.0, 0.0, 6371.0)
assert_that(w).is_equal(Vector2.ZERO)
# =============================================================================
# world_m_to_district — one division by DISTRICT_M, sub-district precision
# preserved (not rounded).
# =============================================================================
func test_world_m_to_district_divides_by_district_m() -> void:
var d: Vector2 = AtlasWindowGeometryNature.world_m_to_district(
Vector2(DISTRICT_M * 3.5, DISTRICT_M * -2.25)
)
assert_that(d).is_equal_approx(Vector2(3.5, -2.25), Vector2.ONE * 0.001)
# =============================================================================
# layer1_pixel_to_canvas_local — the full composition, cross-checked against
# AtlasWindowGeometry.district_to_canvas_local() called manually with the
# same intermediate value.
# =============================================================================
## A river pixel at the held window's own center district must land at
## canvas-local half-extent — same invariant
## test_district_to_canvas_local_center_district_lands_at_half_extent()
## pins for the district-space function this one wraps.
func test_layer1_pixel_to_canvas_local_matches_manual_composition() -> void:
var AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
var radius_km := 6371.0
var grid_w := 256.0
var grid_h := 128.0
var held_center := Vector2i(10, 20)
var held_n := 64
var row := 40.0
var col := 80.0
var result: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_canvas_local(
row, col, grid_w, grid_h, radius_km, held_center, held_n, CELL_PIXEL_SIZE
)
var world_m: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(
row, col, grid_w, grid_h, radius_km
)
var district: Vector2 = AtlasWindowGeometryNature.world_m_to_district(world_m)
var expected: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
district, held_center, held_n, CELL_PIXEL_SIZE
)
assert_that(result).is_equal_approx(expected, Vector2.ONE * 0.001)
# =============================================================================
# T-1170 Ruling 2a-2d/5a: d8_downstream_target() — the river_downstream D8
# pointer decode. Direction table CONFIRMED against Dudley's A1
# (server/src/atlas/drainage.rs:35-44): 0=N(-1,0) 1=S(1,0) 2=E(0,1) 3=W(0,-1)
# 4=NE(-1,1) 5=NW(-1,-1) 6=SE(1,1) 7=SW(1,-1). Sentinels: MOUTH=8,
# EDGE_DRAIN=9, TERMINAL=10 (reserved).
# =============================================================================
func test_d8_downstream_target_north_decrements_row() -> void:
var target: Variant = AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 0)
assert_that(target).is_equal(Vector2(9.0, 10.0))
func test_d8_downstream_target_south_increments_row() -> void:
var target: Variant = AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 1)
assert_that(target).is_equal(Vector2(11.0, 10.0))
func test_d8_downstream_target_east_increments_col() -> void:
var target: Variant = AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 2)
assert_that(target).is_equal(Vector2(10.0, 11.0))
func test_d8_downstream_target_west_decrements_col() -> void:
var target: Variant = AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 3)
assert_that(target).is_equal(Vector2(10.0, 9.0))
func test_d8_downstream_target_diagonals_move_both_axes() -> void:
# 4=NE, 5=NW, 6=SE, 7=SW — each a diagonal (row, col) delta of magnitude 1
# on both axes, matching the direction letters' compass meaning.
assert_that(AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 4)).is_equal(
Vector2(9.0, 11.0)
) # NE
assert_that(AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 5)).is_equal(
Vector2(9.0, 9.0)
) # NW
assert_that(AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 6)).is_equal(
Vector2(11.0, 11.0)
) # SE
assert_that(AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 7)).is_equal(
Vector2(11.0, 9.0)
) # SW
## MOUTH (8), EDGE_DRAIN (9), and TERMINAL (10, reserved) are all sentinels
## >= RIVER_DOWNSTREAM_SENTINEL_BASE — every one must decode to `null` (chain
## end, no segment to draw), not a direction lookup.
func test_d8_downstream_target_sentinels_return_null() -> void:
assert_that(
AtlasWindowGeometryNature.d8_downstream_target(
10.0, 10.0, AtlasWindowGeometryNature.RIVER_DOWNSTREAM_MOUTH
)
).is_null()
assert_that(
AtlasWindowGeometryNature.d8_downstream_target(
10.0, 10.0, AtlasWindowGeometryNature.RIVER_DOWNSTREAM_EDGE_DRAIN
)
).is_null()
assert_that(
AtlasWindowGeometryNature.d8_downstream_target(
10.0, 10.0, AtlasWindowGeometryNature.RIVER_DOWNSTREAM_TERMINAL
)
).is_null()
## A malformed/out-of-range direction (negative, or >= sentinel base but not
## one of the three named sentinels — e.g. a future reserved value) must also
## decode to null, not crash on an out-of-bounds D8_DIRECTION_DELTAS index.
func test_d8_downstream_target_out_of_range_returns_null_not_crash() -> void:
assert_that(AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, -1)).is_null()
assert_that(AtlasWindowGeometryNature.d8_downstream_target(10.0, 10.0, 255)).is_null()
## The sentinel base itself (8) is the exact boundary between the last real
## direction (7=SW) and the first sentinel (8=MOUTH) — pin the boundary
## exactly rather than relying only on the interior-value tests above.
func test_d8_downstream_target_boundary_seven_is_direction_eight_is_sentinel() -> void:
assert_that(AtlasWindowGeometryNature.d8_downstream_target(0.0, 0.0, 7)).is_not_null()
assert_that(AtlasWindowGeometryNature.d8_downstream_target(0.0, 0.0, 8)).is_null()
# =============================================================================
# T-1170 Ruling 5a: build_skeleton_chords() — the pure chain-CONSTRUCTION
# function (no draw calls) AtlasWindowNatureOverlay._draw_skeleton_chords()
# delegates to. This is the load-bearing chain-walking logic (visibility
# filtering + D8 decode + sentinel chain-ends), tested here directly rather
# than only through the draw-smoke suite's pixel proof.
# =============================================================================
## Two river cells, cell 0 flows SOUTH (direction 1) into cell 1's own grid
## position — one segment constructed, from cell 0's position to cell 0's
## position + (1, 0) [south]. cls read from river_class at the SAME index.
func test_build_skeleton_chords_constructs_one_segment_for_a_simple_pair() -> void:
var river_cells: Array = [[10, 10], [11, 10]]
var river_class: Array = [
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, AtlasWindowGeometryNature.RIVER_CLASS_TRUNK
]
var river_downstream: Array = [1, AtlasWindowGeometryNature.RIVER_DOWNSTREAM_MOUTH] # 1 = S
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, river_downstream, "Region"
)
assert_int(chords.size()).override_failure_message(
"cell 0 (flows S, a real direction) must construct one segment;"
+ " cell 1 (MOUTH sentinel) must construct none — expected exactly 1 total"
).is_equal(1)
var chord: Dictionary = chords[0]
assert_that(chord["from"]).is_equal(Vector2(10.0, 10.0))
assert_that(chord["to"]).is_equal(Vector2(11.0, 10.0))
assert_int(chord["cls"]).is_equal(AtlasWindowGeometryNature.RIVER_CLASS_TRUNK)
## Sentinel chain ends: MOUTH, EDGE_DRAIN, and TERMINAL (reserved) must each
## construct ZERO segments for their own cell — a chain-end has no downstream
## neighbor to connect to, regardless of which sentinel flavor.
func test_build_skeleton_chords_sentinel_chain_ends_construct_no_segment() -> void:
var river_cells: Array = [[0, 0], [10, 10], [20, 20]]
var river_class: Array = [
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]
var river_downstream: Array = [
AtlasWindowGeometryNature.RIVER_DOWNSTREAM_MOUTH,
AtlasWindowGeometryNature.RIVER_DOWNSTREAM_EDGE_DRAIN,
AtlasWindowGeometryNature.RIVER_DOWNSTREAM_TERMINAL,
]
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, river_downstream, "Region"
)
assert_array(chords).override_failure_message(
"every cell is a sentinel chain-end (MOUTH/EDGE_DRAIN/TERMINAL) —"
+ " zero segments must be constructed"
).is_empty()
## A downstream direction pointing at a class not visible at this rung's
## SKELETON path is filtered by the UPSTREAM cell's own class, not the
## target's — District shows NO skeleton classes at all (T-1170: skeleton
## draws only at Region now), so a District query must construct zero
## segments regardless of the fixture's directions.
func test_build_skeleton_chords_district_rung_constructs_nothing() -> void:
var river_cells: Array = [[10, 10], [11, 10]]
var river_class: Array = [
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, AtlasWindowGeometryNature.RIVER_CLASS_TRUNK
]
var river_downstream: Array = [1, AtlasWindowGeometryNature.RIVER_DOWNSTREAM_MOUTH]
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, river_downstream, "District"
)
assert_array(chords).is_empty()
## river_downstream shorter than river_cells (pre-T-1170 payload / graceful
## empty-Vec decode) — cells with no corresponding index must construct no
## segment, not crash on an out-of-bounds read.
func test_build_skeleton_chords_missing_downstream_entries_construct_nothing() -> void:
var river_cells: Array = [[10, 10], [11, 10], [12, 10]]
var river_class: Array = [
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]
var river_downstream: Array = [1] # only index 0 has a pointer
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, river_downstream, "Region"
)
assert_int(chords.size()).is_equal(1)
## An empty river_downstream array entirely (the actual wire shape Dudley's
## `#[serde(default)]` produces for a pre-T-1170 payload) must construct zero
## segments, not error.
func test_build_skeleton_chords_empty_downstream_array_constructs_nothing() -> void:
var river_cells: Array = [[10, 10], [11, 10]]
var river_class: Array = [
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, AtlasWindowGeometryNature.RIVER_CLASS_TRUNK
]
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, [], "Region"
)
assert_array(chords).is_empty()
## A malformed river_cells entry (not an Array, or too short) is skipped
## entirely — no segment constructed for it, no crash, and it does not
## disturb construction for the OTHER (well-formed) entries in the same
## fixture.
func test_build_skeleton_chords_malformed_cell_entry_is_skipped_not_fatal() -> void:
var river_cells: Array = [[10, 10], "not an array", [12, 10]]
var river_class: Array = [
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]
var river_downstream: Array = [1, 1, AtlasWindowGeometryNature.RIVER_DOWNSTREAM_MOUTH]
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, river_downstream, "Region"
)
assert_int(chords.size()).override_failure_message(
"the malformed middle entry must be skipped without disturbing the"
+ " well-formed entries around it — expected exactly 1 (cell 0 -> S)"
).is_equal(1)
## river_class shorter than river_cells falls back to RIVER_CLASS_FALLBACK
## (TRUNK) for the missing entry — the same graceful-decode posture
## skeleton_class_visible_at_rung()'s own caller already relies on.
func test_build_skeleton_chords_missing_class_entry_falls_back_to_trunk() -> void:
var river_cells: Array = [[10, 10]]
var river_downstream: Array = [1]
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, [], river_downstream, "Region"
)
assert_int(chords.size()).is_equal(1)
assert_int(chords[0]["cls"]).is_equal(AtlasWindowGeometryNature.RIVER_CLASS_FALLBACK)
## REVERT-VERIFICATION pin (per the ticket brief's explicit ask to
## revert-verify the most load-bearing construction path): a chain of THREE
## cells (0 -> 1 -> MOUTH) must construct exactly TWO segments in the correct
## from/to order — proving the chain doesn't just count sentinels correctly
## in isolation (the tests above) but actually threads a multi-hop chain.
## Breaking build_skeleton_chords() to, e.g., always connect cell i to cell
## i+1 by INDEX (the old dot-scatter's adjacency, not a real D8 decode) would
## still pass the single-pair test above by coincidence but fail this one,
## since cell 1's OWN downstream direction (2 = E) does not point at
## cell 2's grid position.
func test_build_skeleton_chords_three_hop_chain_threads_correctly() -> void:
var river_cells: Array = [[0, 0], [1, 0], [1, 5]] # cell 2 is NOT south of cell 1
var river_class: Array = [
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]
var river_downstream: Array = [
1, # cell 0 -> S -> (1, 0), matches cell 1's own grid position
2, # cell 1 -> E -> (1, 1) — NOT cell 2's position (1, 5)
AtlasWindowGeometryNature.RIVER_DOWNSTREAM_MOUTH,
]
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, river_downstream, "Region"
)
assert_int(chords.size()).is_equal(2)
assert_that(chords[0]["from"]).is_equal(Vector2(0.0, 0.0))
assert_that(chords[0]["to"]).is_equal(Vector2(1.0, 0.0))
assert_that(chords[1]["from"]).is_equal(Vector2(1.0, 0.0))
# cell 1's OWN downstream (E) decodes to (1, 1), NOT cell 2's own listed
# position (1, 5) — pinning that this function trusts the D8 DECODE, not
# a by-index lookup into river_cells, exactly per the doc's "the decoded
# target cell is not required to appear in river_cells" contract.
assert_that(chords[1]["to"]).override_failure_message(
"cell 1's downstream target must be its DECODED D8 neighbor (1,1),"
+ " never a by-index lookup into river_cells (which would wrongly"
+ " give (1,5), cell 2's own listed position)"
).is_equal(Vector2(1.0, 1.0))
# =============================================================================
# T-1170 Ruling 5c: the RIVER_CLASS_VISIBLE_BY_RUNG split —
# skeleton_class_visible_at_rung() (Region+ chord-chain path, Ruling 5a) and
# course_class_visible_at_rung() (District/Quarter windowed path, Ruling 5b).
# =============================================================================
func test_skeleton_class_visible_at_rung_region_shows_every_class() -> void:
assert_bool(
AtlasWindowGeometryNature.skeleton_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, "Region"
)
).is_true()
assert_bool(
AtlasWindowGeometryNature.skeleton_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY, "Region"
)
).is_true()
assert_bool(
AtlasWindowGeometryNature.skeleton_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, "Region"
)
).is_true()
## T-1170: the skeleton path no longer draws AT ALL at District/Quarter (the
## chord chain is Region-only — District/Quarter draw courses instead, the
## OTHER table below) — this is a CHANGE from wave 1's original District
## "trunk only" disposition on the single RIVER_CLASS_VISIBLE_BY_RUNG table.
func test_skeleton_class_visible_at_rung_district_and_quarter_show_nothing() -> void:
for cls in [
AtlasWindowGeometryNature.RIVER_CLASS_STREAM,
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]:
assert_bool(
AtlasWindowGeometryNature.skeleton_class_visible_at_rung(cls, "District")
).override_failure_message(
"the skeleton (chord-chain) path must show NOTHING at District —"
+ " District draws courses instead (Ruling 5b)"
).is_false()
assert_bool(
AtlasWindowGeometryNature.skeleton_class_visible_at_rung(cls, "Quarter")
).is_false()
## An unrecognized rung tag falls back to Region's fullest visibility set —
## the cluster's existing "unrecognized -> safest/most permissive already-
## shipped behavior" posture.
func test_skeleton_class_visible_at_rung_unknown_tag_falls_back_to_region() -> void:
assert_bool(
AtlasWindowGeometryNature.skeleton_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, "Bogus"
)
).is_true()
func test_course_class_visible_at_rung_district_shows_trunk_and_tributary_only() -> void:
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, "District"
)
).override_failure_message("District courses must NOT show streams").is_false()
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY, "District"
)
).is_true()
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, "District"
)
).is_true()
## The pre-announced wave-1 revisit executing: Quarter shows ALL THREE
## classes on the course path — "Quarter rivers return".
func test_course_class_visible_at_rung_quarter_shows_every_class() -> void:
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, "Quarter"
)
).override_failure_message("Quarter rivers return — streams must be visible").is_true()
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY, "Quarter"
)
).is_true()
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, "Quarter"
)
).is_true()
## Region never carries courses (Ruling 1) — the course table has no Region
## key at all, and this reader must fail to EMPTY (not fall back to "show
## everything", the opposite fallback direction from the skeleton reader) so
## a caller can never accidentally draw course polylines at Region.
func test_course_class_visible_at_rung_region_shows_nothing() -> void:
for cls in [
AtlasWindowGeometryNature.RIVER_CLASS_STREAM,
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]:
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(cls, "Region")
).override_failure_message(
"courses must never be visible at Region — Region draws the skeleton"
+ " chord chain, never windowed course content"
).is_false()
func test_course_class_visible_at_rung_unknown_tag_falls_back_to_empty() -> void:
assert_bool(
AtlasWindowGeometryNature.course_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, "Bogus"
)
).is_false()
# =============================================================================
# T-1170 Ruling 5b/3h: build_course_render_plan() — pure course-polyline
# CONSTRUCTION (no draw calls), the course-path counterpart to B2's
# build_skeleton_chords(). Synthetic fixtures shaped per Ruling 3h's wire
# shape: {class: u8, points: Vec<(i32,i32)> world-metres, terminus: string}
# — built BEFORE Dudley's A2 (course inventor) lands, per the ticket brief's
# explicit instruction.
# =============================================================================
static func _course_fixture(
cls: int, points: Array, terminus: String = "None"
) -> Dictionary:
return {"edge_id": 1, "class": cls, "points": points, "terminus": terminus}
func test_build_course_render_plan_district_trunk_is_visible_and_constructs_points() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0], [4096, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
assert_int(canvas_pts.size()).is_equal(3)
assert_int(plan["cls"]).is_equal(AtlasWindowGeometryNature.RIVER_CLASS_TRUNK)
assert_str(plan["terminus"]).is_equal("None")
## District does NOT show streams (COURSE_CLASS_VISIBLE_BY_RUNG: District ==
## [TRIBUTARY, TRUNK]) — a stream-class course must construct nothing at
## District, even with perfectly well-formed points.
func test_build_course_render_plan_district_stream_is_not_visible() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, [[0, 0], [2048, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).override_failure_message(
"streams must not draw at District — only tributary+trunk are visible there"
).is_null()
## Quarter rivers return — ALL THREE classes construct at Quarter, including
## streams. This is the wave-1 pre-announced revisit actually landing.
func test_build_course_render_plan_quarter_stream_is_visible() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, [[0, 0], [512, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "Quarter", Vector2i.ZERO, 16, CELL_PIXEL_SIZE
)
assert_that(plan).override_failure_message(
"Quarter rivers return — streams must be visible at Quarter"
).is_not_null()
## Region never carries courses — a course-shaped fixture queried at "Region"
## must construct nothing, regardless of class.
func test_build_course_render_plan_region_constructs_nothing() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "Region", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_null()
## Ruling 3h: terminus MOUTH is preserved through to the plan — the caller
## (the overlay's draw function) reads this to decide whether to draw a
## mouth ring at the LAST canvas point.
func test_build_course_render_plan_preserves_mouth_terminus() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0]], "Mouth"
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
assert_str(plan["terminus"]).is_equal(AtlasWindowGeometryNature.COURSE_TERMINUS_MOUTH)
## EdgeDrain, ContinuesBeyondWindow, and the default None terminus are all
## preserved verbatim too — the PLAN doesn't collapse them, the DRAW caller
## decides presentation (no ring for any of these three).
func test_build_course_render_plan_preserves_edge_drain_and_continues_and_none_termini() -> void:
for terminus in ["EdgeDrain", "ContinuesBeyondWindow", "None"]:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0]], terminus
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_str(plan["terminus"]).is_equal(terminus)
## A course with no `terminus` key at all (an old/malformed payload) defaults
## to COURSE_TERMINUS_NONE (the string "None"), never GDScript `null` or an
## empty string — matching the class-fallback graceful-decode posture used
## throughout this cluster.
func test_build_course_render_plan_missing_terminus_defaults_to_none_string() -> void:
var course: Dictionary = {
"edge_id": 1, "class": AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, "points": [[0, 0], [100, 0]]
}
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_str(plan["terminus"]).is_equal(AtlasWindowGeometryNature.COURSE_TERMINUS_NONE)
## A course missing `class` entirely falls back to RIVER_CLASS_FALLBACK
## (TRUNK) — same posture as the skeleton path's river_class fallback.
func test_build_course_render_plan_missing_class_falls_back_to_trunk() -> void:
var course: Dictionary = {"edge_id": 1, "points": [[0, 0], [100, 0]]}
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
assert_int(plan["cls"]).is_equal(AtlasWindowGeometryNature.RIVER_CLASS_FALLBACK)
## Fewer than 2 points (a degenerate single-point or empty course) has no
## line to draw — must construct null, not a 1-point/0-point polyline.
func test_build_course_render_plan_fewer_than_two_points_constructs_nothing() -> void:
var one_point: Dictionary = _course_fixture(AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0]])
var no_points: Dictionary = _course_fixture(AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [])
assert_that(
AtlasWindowGeometryNature.build_course_render_plan(
one_point, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
).is_null()
assert_that(
AtlasWindowGeometryNature.build_course_render_plan(
no_points, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
).is_null()
## Missing `points` key entirely (not just an empty array) must also
## construct nothing, not crash on a null/missing field read.
func test_build_course_render_plan_missing_points_key_constructs_nothing() -> void:
var course: Dictionary = {"edge_id": 1, "class": AtlasWindowGeometryNature.RIVER_CLASS_TRUNK}
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_null()
## A malformed individual point (not an array, or too short) is skipped —
## not fatal to the whole polyline, matching build_skeleton_chords()'s own
## "skip the bad entry, keep going" posture — as long as >= 2 valid points
## remain.
func test_build_course_render_plan_malformed_point_is_skipped_not_fatal() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], "not a point", [2048, 0], [4096, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
assert_int(canvas_pts.size()).override_failure_message(
"the malformed point must be skipped, leaving exactly the 3 well-formed points"
).is_equal(3)
## Malformed points that leave FEWER than 2 valid entries must still
## construct null (the "too many bad points" case, distinct from "some bad
## points but enough good ones remain" above).
func test_build_course_render_plan_malformed_points_leaving_too_few_constructs_nothing() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], "bad", "also bad"]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_null()
## Points are WORLD METRES (Ruling 3h), not heightmap pixels — cross-checked
## against world_m_to_canvas_local() called manually, proving the plan's
## conversion path matches the documented one-fewer-step-than-skeleton
## pipeline (no layer1_pixel_to_world_m() involved at all).
func test_build_course_render_plan_points_are_world_metres_not_pixels() -> void:
var held_center := Vector2i(5, 5)
var held_n := 64
var world_pt := Vector2(10240.0, -4096.0) # 5 districts east, 2 north of origin
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
[[int(world_pt.x), int(world_pt.y)], [0, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", held_center, held_n, CELL_PIXEL_SIZE
)
var expected: Vector2 = AtlasWindowGeometryNature.world_m_to_canvas_local(
world_pt, held_center, held_n, CELL_PIXEL_SIZE
)
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
assert_that(canvas_pts[0]).is_equal_approx(expected, Vector2.ONE * 0.01)
# =============================================================================
# T-1170 Ruling 5c: course_class_width_px() / course_class_opacity() —
# functional-default companion tables to COURSE_CLASS_VISIBLE_BY_RUNG.
# =============================================================================
func test_course_class_width_px_trunk_widest_stream_thinnest() -> void:
var stream_w: float = AtlasWindowGeometryNature.course_class_width_px(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM
)
var tributary_w: float = AtlasWindowGeometryNature.course_class_width_px(
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY
)
var trunk_w: float = AtlasWindowGeometryNature.course_class_width_px(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK
)
assert_float(trunk_w).override_failure_message(
"trunk course width must be the WIDEST of the three classes"
).is_greater(tributary_w)
assert_float(tributary_w).override_failure_message(
"tributary course width must be strictly between stream and trunk"
).is_greater(stream_w)
func test_course_class_opacity_trunk_most_opaque_stream_least() -> void:
var stream_o: float = AtlasWindowGeometryNature.course_class_opacity(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM
)
var trunk_o: float = AtlasWindowGeometryNature.course_class_opacity(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK
)
assert_float(trunk_o).is_greater(stream_o)
assert_float(trunk_o).override_failure_message("trunk opacity must be fully opaque (1.0)").is_equal_approx(
1.0, 0.0001
)
## An unrecognized class id falls back to the stream (thinnest/most transparent)
## defaults on both tables — the documented, deliberate "unknown -> least
## visually assertive" fallback.
func test_course_class_width_and_opacity_unknown_class_falls_back_to_stream() -> void:
var stream_w: float = AtlasWindowGeometryNature.course_class_width_px(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM
)
var stream_o: float = AtlasWindowGeometryNature.course_class_opacity(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM
)
assert_float(AtlasWindowGeometryNature.course_class_width_px(99)).is_equal_approx(
stream_w, 0.0001
)
assert_float(AtlasWindowGeometryNature.course_class_opacity(99)).is_equal_approx(
stream_o, 0.0001
)
# =============================================================================
# Feature-group per-rung gates — confluences/mouths/basins/attractors.
# =============================================================================
func test_confluences_visible_at_rung_region_true_others_false() -> void:
assert_bool(AtlasWindowGeometryNature.confluences_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometryNature.confluences_visible_at_rung("District")).is_false()
assert_bool(AtlasWindowGeometryNature.confluences_visible_at_rung("Quarter")).is_false()
## Mouths get the one rung-based EXCEPTION in the whole table: District keeps
## them visible (a mouth is always a landmark, per the ruling) — the only
## feature group where District differs from Region's disposition.
func test_mouths_visible_at_rung_region_and_district_true_quarter_false() -> void:
assert_bool(AtlasWindowGeometryNature.mouths_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometryNature.mouths_visible_at_rung("District")).is_true()
assert_bool(AtlasWindowGeometryNature.mouths_visible_at_rung("Quarter")).is_false()
func test_basins_visible_at_rung_region_only() -> void:
assert_bool(AtlasWindowGeometryNature.basins_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometryNature.basins_visible_at_rung("District")).is_false()
assert_bool(AtlasWindowGeometryNature.basins_visible_at_rung("Quarter")).is_false()
func test_attractors_visible_at_rung_region_only() -> void:
assert_bool(AtlasWindowGeometryNature.attractors_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometryNature.attractors_visible_at_rung("District")).is_false()
assert_bool(AtlasWindowGeometryNature.attractors_visible_at_rung("Quarter")).is_false()
# =============================================================================
# Coordinator live-eyeball finding (2026-07-23): zoom_compensated_size() —
# marker sizes must stay CONSTANT on screen regardless of _view_zoom
# (Araminta's ruling), but draw calls execute inside a Node2D whose .scale IS
# _view_zoom — a raw constant gets multiplied by that transform at render
# time. This function pre-divides so the transform's multiply cancels back
# out to the literal screen-space value.
# =============================================================================
## At zoom=1.0 (the canvas transform's identity scale) the compensated size
## must equal the input unchanged — no over/under-correction at the one zoom
## level where compensation is a no-op by construction.
func test_zoom_compensated_size_at_zoom_one_is_unchanged() -> void:
assert_float(AtlasWindowGeometryNature.zoom_compensated_size(2.2, 1.0)).is_equal_approx(2.2, 0.0001)
## The exact regression shape: at Lendel's real orbital fit zoom (~0.0063,
## live drive script), the compensated size must be much LARGER than the
## raw screen-space constant — inversely proportional to zoom — so that once
## the canvas transform re-multiplies it by view_zoom at render time, the
## EFFECTIVE on-screen size lands back at the literal ruling value, not a
## sub-pixel sliver.
func test_zoom_compensated_size_at_orbital_zoom_scales_up_inversely() -> void:
var view_zoom := 0.0063
var screen_space_size := 2.2
var compensated: float = AtlasWindowGeometryNature.zoom_compensated_size(
screen_space_size, view_zoom
)
# Round-trip: compensated * view_zoom must reconstruct the original
# screen-space size — this IS the property that makes the on-screen
# result zoom-invariant (the canvas transform performs exactly this
# multiply at render time).
assert_float(compensated * view_zoom).is_equal_approx(screen_space_size, 0.001)
assert_float(compensated).override_failure_message(
"at a tiny orbital zoom, the compensated size must be dramatically LARGER"
+ " than the raw screen-space constant — that's the whole point of the fix"
).is_greater(screen_space_size * 10.0)
## The exact BUG this fix closes, pinned as a regression: an UNCOMPENSATED
## radius (screen_space_size used directly, the pre-fix behavior) multiplied
## by Lendel's real orbital zoom produces a sub-pixel effective size — this
## is the "the ruling's px value, at orbital fit zoom, is invisible" claim
## from the coordinator's diagnosis, verified numerically rather than just
## asserted.
func test_uncompensated_radius_at_orbital_zoom_would_be_sub_pixel() -> void:
var view_zoom := 0.0063
var raw_screen_space_radius := 2.2 # RIVER_DOT_RADIUS_BY_CLASS_REGION[TRUNK]
var effective_size_if_uncompensated: float = raw_screen_space_radius * view_zoom
assert_float(effective_size_if_uncompensated).override_failure_message(
"an uncompensated radius at orbital zoom must be sub-pixel — pinning the"
+ " numeric magnitude of the bug this fix closes, not just its existence"
).is_less(0.02)
## A degenerate zero (or negative) view_zoom must not divide-by-zero/produce
## infinity/NaN — the floor guard keeps this function total.
func test_zoom_compensated_size_zero_zoom_does_not_blow_up() -> void:
var result: float = AtlasWindowGeometryNature.zoom_compensated_size(2.2, 0.0)
assert_bool(is_finite(result)).override_failure_message(
"a degenerate zero view_zoom must not produce inf/NaN"
).is_true()
@@ -1,252 +0,0 @@
## T-1170 live round (2026-07-23): tests for
## AtlasWindowGeometryNature.zoom_compensated_stroke_width() — the
## STROKE-WIDTH-specific sibling of zoom_compensated_size(), added after the
## course-polyline hairline finding (Araminta's pixel scan of
## D-district-courses.png/Q-quarter-courses.png: a UNIFORM 1px hairline for
## the ENTIRE visible course, no width/opacity variation at all, in BOTH
## District and Quarter captures). Split into its own file rather than
## folded into test_atlas_window_geometry_nature.gd, which was already at the
## gdlint max-file-lines cap — same file-per-concern precedent as every other
## split in this cluster.
##
## Live A/B evidence (temporary instrumentation, since reverted — the
## dossier discipline): draw_line()/draw_polyline() called with a
## canvas-local width in [0.6, 1.0) renders as a flat 1px hairline
## regardless of the input value, confirmed identically on BOTH APIs (ruling
## out a draw_polyline()-specific quirk) — Godot's line rasterizer has a
## ~1.0-canvas-local-unit floor that draw_circle()'s radius parameter does
## NOT share (confirmed: mouth ring radii at the same District/Quarter zoom
## render correctly-sized via the unchanged zoom_compensated_size()/_zs()
## path — only the STROKE WIDTH argument was affected). The compensation
## MATH itself was never wrong (0.373 * 3.75 round-trips to 1.4 exactly) —
## the bug was that nothing floored the intermediate value against Godot's
## own rasterizer minimum before handing it to draw_line()/draw_polyline().
class_name TestAtlasWindowGeometryStrokeWidth
extends GdUnitTestSuite
const AtlasWindowGeometryNature := preload(
"res://ui/implant/apps/atlas/atlas_window_geometry_nature.gd"
)
## At zoom=1.0, unchanged from zoom_compensated_size() — no floor engages
## when the input is already >= 1.0.
func test_zoom_compensated_stroke_width_at_zoom_one_is_unchanged() -> void:
assert_float(
AtlasWindowGeometryNature.zoom_compensated_stroke_width(2.2, 1.0)
).is_equal_approx(2.2, 0.0001)
## The EXACT regression shape this round closes: at District's real fit zoom
## (3.75, live capture), the tributary class's raw table width (1.4px)
## divides to 0.3733 canvas-local — BELOW the 1.0 floor under the OLD
## zoom_compensated_size() path (pinned directly, not just asserted) — and
## zoom_compensated_stroke_width() must instead return exactly 1.0 (the
## floor), never the sub-floor raw division result.
func test_zoom_compensated_stroke_width_district_tributary_hits_the_floor() -> void:
var view_zoom := 3.75 # LENDEL's live District fit zoom, capture-confirmed
var raw_width_px := 1.4 # COURSE_CLASS_WIDTH_PX[RIVER_CLASS_TRIBUTARY]
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_width_px, view_zoom)
assert_float(unfloored).override_failure_message(
"regression pin: the OLD unfloored division must be BELOW 1.0 at this"
+ " zoom — this is the exact numeric shape of the hairline bug"
).is_less(1.0)
var floored: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(raw_width_px, view_zoom)
assert_float(floored).override_failure_message(
"zoom_compensated_stroke_width() must clamp to the 1.0 floor, not the"
+ " sub-pixel unfloored value that collapses to Godot's hairline"
).is_equal_approx(1.0, 0.0001)
## Same shape at Quarter's real fit zoom (7.5, live capture) — the floor
## engages even harder there (raw width divides to 0.1867).
func test_zoom_compensated_stroke_width_quarter_tributary_hits_the_floor() -> void:
var view_zoom := 7.5 # LENDEL's live Quarter fit zoom, capture-confirmed
var raw_width_px := 1.4
var floored: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(raw_width_px, view_zoom)
assert_float(floored).is_equal_approx(1.0, 0.0001)
## Regression pin (PR #195 stroke-width-class shape, per the coordinator's
## explicit ask): for EACH course class, at District's real fit zoom, the
## EFFECTIVE on-screen width the render plan feeds (canvas-local width times
## view_zoom, exactly what the canvas transform multiplies at render time)
## must equal AT LEAST the table value — never less, since the floor can only
## push the effective width UP from what an unfloored divide would produce,
## never down. This is the "does the value actually reaching the screen
## match the table" pin the coordinator asked for, computed both ways
## (floored vs table) rather than eyeballed.
func test_effective_stroke_width_at_district_zoom_meets_table_value_per_class() -> void:
var view_zoom := 3.75
for cls in [
AtlasWindowGeometryNature.RIVER_CLASS_STREAM,
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]:
var table_width: float = AtlasWindowGeometryNature.course_class_width_px(cls)
var canvas_local: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(
table_width, view_zoom
)
var effective_screen_px: float = canvas_local * view_zoom
assert_float(effective_screen_px).override_failure_message(
(
"class %d's effective on-screen stroke width (%.3fpx) must be AT"
+ " LEAST its table value (%.3fpx) — the floor must never make a"
+ " course THINNER than the ruling specifies, only ever thicker"
+ " when the literal value would otherwise be sub-pixel"
)
% [cls, effective_screen_px, table_width]
).is_greater_equal(table_width - 0.001)
## Same pin at Quarter's fit zoom (7.5) — the floor engages harder there
## (streams' 0.9px table value divides to 0.12 canvas-local, furthest below
## the floor of any class/rung combination this batch draws).
func test_effective_stroke_width_at_quarter_zoom_meets_table_value_per_class() -> void:
var view_zoom := 7.5
for cls in [
AtlasWindowGeometryNature.RIVER_CLASS_STREAM,
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY,
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
]:
var table_width: float = AtlasWindowGeometryNature.course_class_width_px(cls)
var canvas_local: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(
table_width, view_zoom
)
var effective_screen_px: float = canvas_local * view_zoom
assert_float(effective_screen_px).is_greater_equal(table_width - 0.001)
## At a LOW zoom (well under 1.0, e.g. an extreme zoom-out within a rung —
## not just Region's orbital case), the floor must NOT engage: the ordinary
## divide-then-scale math must still produce the literal table value exactly,
## matching zoom_compensated_size()'s own unfloored behavior. Pins that the
## floor is a ONE-DIRECTION safety net, not a blanket override.
func test_zoom_compensated_stroke_width_does_not_engage_at_low_zoom() -> void:
var view_zoom := 0.1
var raw_width_px := 2.2
var floored: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(raw_width_px, view_zoom)
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_width_px, view_zoom)
assert_float(floored).override_failure_message(
"at a zoom where the unfloored value is already well above 1.0, the"
+ " floor must be a no-op — identical to zoom_compensated_size()"
).is_equal_approx(unfloored, 0.0001)
## A degenerate zero (or negative) view_zoom must not divide-by-zero/produce
## infinity/NaN — same total-function guarantee as zoom_compensated_size().
func test_zoom_compensated_stroke_width_zero_zoom_does_not_blow_up() -> void:
var result: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(2.2, 0.0)
assert_bool(is_finite(result)).override_failure_message(
"a degenerate zero view_zoom must not produce inf/NaN"
).is_true()
# =============================================================================
# T-1170 live round (2026-07-23, coordinator's mouth-ring finding):
# zoom_compensated_ring_radius() — the RADIUS-SMALLER-THAN-STROKE regime.
# Same A/B-bracket discipline as the stroke-width suite above, applied to
# draw_arc() RING markers (mouth rings), whose radius AND stroke are both
# small zoom-compensated values that can cross each other.
##
## Live A/B evidence (temporary instrumentation, since reverted): at the
## PRODUCTION Quarter-rung radius/stroke pair (radius=0.667, stroke=1.0
## canvas-local units, Quarter fit zoom 7.5), draw_arc() rendered a SOLID
## BLOB, not a hollow ring — confirmed via a re-centered live capture (the
## ORIGINAL "zero ring pixels" symptom was a separate viewport-framing crop,
## not this bug — see zoom_compensated_ring_radius()'s own doc). Bracket
## (stroke fixed at 1.0 canvas-local, radius varied): 0.51 (~stroke/2) ->
## blob; 1.0 (=stroke, the production case) -> blob; 1.5 (1.5x stroke) ->
## hollow ring recovers; 2.0 (2x stroke) -> hollow ring, cleaner. Floor set
## at 2x with margin over the observed 1.0x-blob/1.5x-hollow transition.
# =============================================================================
## At zoom=1.0 with a radius comfortably above stroke*2 already, the floor
## must be a no-op — identical to zoom_compensated_size() directly.
func test_zoom_compensated_ring_radius_no_op_when_radius_already_clears_the_floor() -> void:
var radius: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(5.0, 1.5, 1.0)
assert_float(radius).is_equal_approx(5.0, 0.0001)
## The EXACT regression shape this round closes: at Quarter's real fit zoom
## (7.5, live capture), MOUTH_RING_RADIUS (5.0) divides to 0.667
## canvas-local — BELOW its own paired stroke (1.5/7.5 floored to 1.0 via
## zoom_compensated_stroke_width) — pinned directly, not just asserted.
## zoom_compensated_ring_radius() must instead return stroke * 2.0 (the
## floor), never the sub-floor raw division result that produced the blob.
func test_zoom_compensated_ring_radius_quarter_mouth_ring_hits_the_floor() -> void:
var view_zoom := 7.5 # LENDEL's live Quarter fit zoom, capture-confirmed
var raw_radius := 5.0 # MOUTH_RING_RADIUS
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(1.5, view_zoom)
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_radius, view_zoom)
assert_float(unfloored).override_failure_message(
"regression pin: the OLD unfloored radius must be AT/BELOW the paired"
+ " stroke at this zoom — this is the exact numeric shape of the blob bug"
).is_less_equal(stroke)
var floored: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(
raw_radius, stroke, view_zoom
)
assert_float(floored).override_failure_message(
"zoom_compensated_ring_radius() must clamp to stroke * 2.0 (the floor),"
+ " not the sub-floor unfloored value that renders as a solid blob"
).is_equal_approx(stroke * AtlasWindowGeometryNature.RING_RADIUS_STROKE_MULTIPLIER, 0.0001)
## Regression pin (the stroke-width suite's own "effective on-screen value"
## shape, applied to the radius/stroke RATIO instead of an absolute value):
## for the mouth ring AND halo (the two draw_arc() ring markers in this
## cluster), at Quarter's real fit zoom, the floored radius must be AT LEAST
## RING_RADIUS_STROKE_MULTIPLIER times its own paired stroke — the actual
## geometric property that keeps the ring hollow, verified directly rather
## than just re-checking the numeric floor value in isolation.
func test_ring_radius_stays_at_least_the_multiplier_above_its_stroke_at_quarter_zoom() -> void:
var view_zoom := 7.5
# (raw_radius_px, raw_stroke_px) pairs — the mouth ring and halo's own
# literal call-site arguments in _draw_mouth().
for pair in [[5.0, 1.5], [8.0, 1.0]]:
var raw_radius: float = pair[0]
var raw_stroke: float = pair[1]
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(
raw_stroke, view_zoom
)
var radius: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(
raw_radius, stroke, view_zoom
)
assert_float(radius).override_failure_message(
(
"radius %.3f must be at least %.1fx its paired stroke %.3f — a ratio"
+ " below this rendered as a SOLID BLOB in the live A/B bracket,"
+ " never a hollow ring"
)
% [radius, AtlasWindowGeometryNature.RING_RADIUS_STROKE_MULTIPLIER, stroke]
).is_greater_equal(stroke * AtlasWindowGeometryNature.RING_RADIUS_STROKE_MULTIPLIER - 0.0001)
## At a LOW zoom (e.g. Region's tiny orbital fit, or any zoom where the
## naive radius is already well clear of the floor), the floor must NOT
## engage — matching zoom_compensated_stroke_width()'s own
## does-not-engage-at-low-zoom guarantee. Pins that this is a one-direction
## safety net, not a blanket override.
func test_zoom_compensated_ring_radius_does_not_engage_at_low_zoom() -> void:
var view_zoom := 0.0063 # Lendel's real orbital fit zoom
var raw_radius := 5.0 # MOUTH_RING_RADIUS
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(1.5, view_zoom)
var floored: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(
raw_radius, stroke, view_zoom
)
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_radius, view_zoom)
assert_float(floored).override_failure_message(
"at a zoom where the naive radius is already far above the floor, the"
+ " floor must be a no-op — identical to zoom_compensated_size()"
).is_equal_approx(unfloored, 0.0001)
## A degenerate zero (or negative) view_zoom must not divide-by-zero/produce
## infinity/NaN — same total-function guarantee as the stroke-width sibling.
func test_zoom_compensated_ring_radius_zero_zoom_does_not_blow_up() -> void:
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(1.5, 0.0)
var result: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(5.0, stroke, 0.0)
assert_bool(is_finite(result)).override_failure_message(
"a degenerate zero view_zoom must not produce inf/NaN"
).is_true()
@@ -1,579 +0,0 @@
## T-1156 wave 1: tests for AtlasWindowNatureOverlay — the whole-body
## Layer-1 (river/basin/attractor) draw node on the zoom ladder. Covers
## request/response lifecycle (idempotent-per-body, staleness guard, decode
## tolerance for a missing river_class array) and the draw-gate wiring
## (rung/overlay-bar double-gate), NOT pixel-level draw output — the
## coordinate math itself is covered directly in
## test_atlas_window_geometry_nature.gd, matching test_atlas_window_overlay.gd's
## own "cache/lifecycle here, colorizer pixels elsewhere" split.
class_name TestAtlasWindowNatureOverlay
extends GdUnitTestSuite
const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd")
## Minimal viewer stub — AtlasWindowNatureOverlay only reaches the viewer
## through get_held_granularity_v2()/get_body_radius_km()/get_held_center()/
## get_held_n()/get_cell_pixel_size()/get_view_zoom()/is_overlay_visible(),
## the same duck-typed-viewer precedent test_atlas_window_overlay.gd's
## _ViewerStub already establishes for AtlasWindowOverlay. get_view_zoom()
## added post-live-eyeball (coordinator finding, 2026-07-23): _draw() now
## reads it for the zoom-compensated marker-size fix. is_tile_mode()/
## get_district_window()/get_tile_set() added for T-1172 (the water clip) —
## district_window/tile_set default to null/an empty stub, matching "no
## arrived composite data yet" (the fail-open case) unless a test sets them.
class _ViewerStub:
var held_granularity_v2: String = "Region"
var body_radius_km: float = 6371.0
var held_center: Vector2i = Vector2i.ZERO
var held_n: int = 64
var view_zoom: float = 1.0
var overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": false, "gen_attractors": false}
var tile_mode: bool = false
var district_window: Variant = null
var tile_set: Variant = null
func get_held_granularity_v2() -> String:
return held_granularity_v2
func get_body_radius_km() -> float:
return body_radius_km
func get_held_center() -> Vector2i:
return held_center
func get_held_n() -> int:
return held_n
func get_cell_pixel_size() -> float:
return 16.0
func get_view_zoom() -> float:
return view_zoom
func is_overlay_visible(overlay_id: String) -> bool:
return bool(overlay_visibility.get(overlay_id, false))
func is_tile_mode() -> bool:
return tile_mode
func get_district_window() -> Variant:
return district_window
func get_tile_set() -> Variant:
return tile_set
## Bare tile-set stub — AtlasWindowNatureOverlay's water-clip lookup only
## reaches it through get_tiles(), matching AtlasWindowTileSet's own public
## surface (an Array of {"center": Vector2i, "window": Variant}).
class _TileSetStub:
var tiles: Array = []
func get_tiles() -> Array:
return tiles
static func _mock_layer1(river_class: Variant = null) -> Dictionary:
var rn: Dictionary = {
"river_cells": [[10, 10], [20, 20], [30, 30]],
"confluences": [[15, 15]],
"mouths": [[40, 40]],
}
if river_class != null:
rn["river_class"] = river_class
return {
"river_network": rn,
"drainage_basins": [{"basin_id": 1, "boundary": [[0, 0], [0, 10], [10, 10], [10, 0]]}],
"attractors": [{"position": [10, 10], "strength": 0.5, "attractor_type": "Oasis", "sub_biome": ""}],
"grid_w": 256,
"grid_h": 128,
}
static func _mock_response(body_id: String, layer1: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "layer1": layer1}
## Window-only responses (the OTHER shape SimBridge.atlas_layers_received
## carries, per AtlasWindowRequest's own test conventions) must be ignored —
## `layer1` is null on that envelope, matching atlas_response_from_raw()'s
## "only one of layer1/district_window populated per response" contract.
static func _mock_window_response(body_id: String) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": {"n": 32}, "layer1": null}
func _make_overlay(viewer: Variant = null) -> Variant:
var o = auto_free(AtlasWindowNatureOverlay.new(viewer if viewer != null else _ViewerStub.new()))
add_child(o)
return o
# =============================================================================
# Request lifecycle
# =============================================================================
func test_no_layer1_before_any_request() -> void:
var o = _make_overlay()
assert_that(o.get_layer1()).is_null()
func test_response_for_requested_body_is_adopted() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
var layer1: Dictionary = _mock_layer1()
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
assert_that(o.get_layer1()).is_equal(layer1)
## Staleness guard: a response for a body this node never asked for (or
## navigated away from) must be ignored — same posture
## AtlasGenerationProxy.on_response()'s own body_id guard establishes.
func test_response_for_a_different_body_is_ignored() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
SimBridge.atlas_layers_received.emit(_mock_response("SomeOtherBody", _mock_layer1()))
assert_that(o.get_layer1()).is_null()
## A window-shaped response (the windowed DistrictWindowLayer envelope,
## `layer1` null) must be ignored outright — this node only ever adopts the
## whole-body Layer-1 envelope.
func test_window_only_response_is_ignored() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
SimBridge.atlas_layers_received.emit(_mock_window_response("GJ380c"))
assert_that(o.get_layer1()).is_null()
## request_layer1() for the SAME body id, after data has already arrived,
## must NOT clear the held data — a re-entrant enter_orbital() on the body
## already showing keeps drawing rivers instead of flashing them away.
func test_request_layer1_same_body_after_arrival_keeps_held_data() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
var layer1: Dictionary = _mock_layer1()
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
o.request_layer1("GJ380c")
assert_that(o.get_layer1()).is_equal(layer1)
## request_layer1() for a DIFFERENT body id must clear the previous body's
## held data immediately — the old body's rivers must never draw over the
## new body's terrain during the in-flight gap.
func test_request_layer1_different_body_clears_stale_data() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", _mock_layer1()))
o.request_layer1("AnotherBody")
assert_that(o.get_layer1()).override_failure_message(
"switching bodies must clear the previous body's layer1 data immediately,"
+ " not just leave it drawn until the new response arrives"
).is_null()
func test_request_layer1_empty_body_id_is_a_noop() -> void:
var o = _make_overlay()
o.request_layer1("")
SimBridge.atlas_layers_received.emit(_mock_response("", _mock_layer1()))
assert_that(o.get_layer1()).override_failure_message(
"an empty body_id must never be requested/adopted"
).is_null()
# =============================================================================
# Decode tolerance — layer1 without river_class (pre-T-1156 payload / the
# graceful-fallback empty-array case, Dudley's #[serde(default)] contract).
# =============================================================================
## A response with NO river_class key at all (river_network dict omits it —
## the msgpack-decode equivalent of Dudley's serde default producing an
## empty Vec) must still be adopted without error; per-cell class then falls
## back to RIVER_CLASS_FALLBACK (TRUNK) at draw time, not a crash/decode failure.
func test_layer1_without_river_class_key_is_still_adopted() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
var layer1: Dictionary = _mock_layer1() # no river_class arg -> key absent
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
assert_that(o.get_layer1()).is_equal(layer1)
assert_bool((o.get_layer1()["river_network"] as Dictionary).has("river_class")).is_false()
## An explicitly EMPTY river_class array (the actual wire shape Dudley's
## `#[serde(default)]` produces for a pre-T-1156 payload) must also decode
## without error and be adopted — the per-cell RIVER_CLASS_FALLBACK
## resolution this enables is exercised at draw time by
## test_atlas_window_nature_overlay_draw_smoke.gd (real-render smoke suite;
## draw_circle()/draw_rect() calls require a live render pass under this
## engine version — confirmed directly, matching
## test_atlas_window_overlay_draw_smoke.gd's own header doc on why a plain
## unit test cannot call `_draw()` outside one).
func test_layer1_with_empty_river_class_array_is_adopted() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
var layer1: Dictionary = _mock_layer1(PackedByteArray([]))
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
assert_that(o.get_layer1()).is_equal(layer1)
## No viewer at all (viewer == null, matching AtlasWindowOverlay's own
## "viewer == null -> return" guard convention) is a legal, inert state —
## _draw()'s null-viewer early-return (BEFORE any draw_*() call) is safe to
## call directly since it never reaches the engine's draw-context requirement.
func test_draw_with_null_viewer_is_a_noop() -> void:
var o = auto_free(AtlasWindowNatureOverlay.new(null))
add_child(o)
o._draw()
assert_that(o.get_layer1()).is_null()
## grid_w/grid_h missing or zero (a malformed/degenerate layer1) must decode
## and adopt cleanly — _draw()'s own grid_w<=0/grid_h<=0 early-return (also
## BEFORE any draw_*() call) is exercised the same direct way.
func test_draw_with_zero_grid_dims_returns_before_any_draw_call() -> void:
var o = _make_overlay()
o.request_layer1("GJ380c")
var layer1: Dictionary = _mock_layer1()
layer1["grid_w"] = 0
layer1["grid_h"] = 0
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
o._draw() # grid_w<=0 -> returns before touching the canvas — safe to call directly
assert_that(o.get_layer1()).is_not_null()
## PR #195 review (Tyre I1) regression pin: every stroke-WIDTH argument in
## _draw_attractor_shape() must route through the pre-compensated `px_w`
## param, never a raw numeric literal — Godot multiplies stroke widths by the
## canvas scale exactly like radii, so a raw `2.0` rasterizes at ~0.01px at
## the Region orbital fit zoom (the identical sub-pixel failure the dot/ring
## zoom compensation fixed, missed on glyph outlines in the first pass). The
## draw-smoke suite cannot gate this (its own header documents the vacuous-
## pass mode under X11 BadMatch), so this is a SOURCE-SCAN pin: parse the
## overlay script's _draw_attractor_shape body and assert no draw_arc/
## draw_line call carries a bare numeric width literal. Crude but
## environment-independent, and it pins the exact regression class (someone
## reintroducing a literal width in a new glyph arm).
func test_attractor_shape_stroke_widths_are_never_raw_literals() -> void:
var src: String = (
FileAccess.get_file_as_string("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd")
)
var fn_start := src.find("func _draw_attractor_shape(")
assert_that(fn_start).override_failure_message(
"_draw_attractor_shape must exist in atlas_window_nature_overlay.gd"
).is_not_equal(-1)
var next_fn := src.find("\nfunc ", fn_start + 1)
var body := src.substr(fn_start, (next_fn - fn_start) if next_fn != -1 else -1)
var stroke_re := RegEx.new()
# A draw_arc/draw_line call whose FINAL (width) argument is a bare numeric
# literal: `, <digits[.digits]>)` at call end. px_w-scaled forms
# (`px_w`, `2.0 * px_w`) do not match.
stroke_re.compile("draw_(arc|line)\\([^\\n]*,\\s*\\d+(\\.\\d+)?\\s*\\)")
var hits := stroke_re.search_all(body)
var offenders: Array[String] = []
for hit in hits:
offenders.append(hit.get_string())
assert_array(offenders).override_failure_message(
"raw numeric stroke width(s) in _draw_attractor_shape — route through"
+ " px_w (PR #195 Tyre I1): %s" % [offenders]
).is_empty()
# =============================================================================
# T-1172 — the two-waterline clip. _is_drawn_water()/_district() are called
# directly (both are pure lookups with NO draw_*() call of their own — the
# _draw()-requires-a-live-render-pass constraint the smoke suite exists for
# does not apply to them), matching this file's own "call private helpers
# directly when they're the load-bearing unit" precedent
# (test_draw_with_zero_grid_dims_returns_before_any_draw_call() above already
# calls _draw() itself specifically because its early-return is BEFORE any
# draw call — same reasoning here, one level down).
# =============================================================================
## A 4x4 District window (n=4, grid_side=4) centered on district (0,0),
## spanning [-2, 2) on both axes — cell (0,0) is water, everything else land.
## Mirrors test_atlas_window_water_clip.gd's own _mock_4x4_window() fixture
## shape (kept local here rather than shared — no cross-test-file import
## precedent in this cluster).
static func _mock_4x4_water_corner_window() -> Dictionary:
var morphology := PackedByteArray()
morphology.resize(16)
for i in range(16):
morphology[i] = 8 # AlluvialPlain — land
morphology[0] = 0 # OpenOcean — the single water cell, row 0 col 0
return {"center": [0, 0], "n": 4, "granularity_v2": "District", "morphology": morphology}
func _ctx_for(viewer_stub: _ViewerStub) -> Dictionary:
return {
"grid_w": 256.0,
"grid_h": 128.0,
"radius_km": 0.0, # no-radius: 1 heightmap pixel = 1 district metre (simplest math)
"held_center": viewer_stub.held_center,
"held_n": viewer_stub.held_n,
"cell_px": 16.0,
"cols": 0,
"granularity_v2": viewer_stub.held_granularity_v2,
"view_zoom": viewer_stub.view_zoom,
}
## Single-window mode: a district position resolving to a LAND cell must not
## be clipped (_is_drawn_water() returns false — the dot draws).
func test_is_drawn_water_false_for_a_land_cell_single_window() -> void:
var stub := _ViewerStub.new()
stub.district_window = _mock_4x4_water_corner_window()
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(1.5, 1.5), ctx)).override_failure_message(
"a district position over a LAND cell must not be clipped"
).is_false()
## Single-window mode: a district position resolving to a WATER cell must be
## clipped (_is_drawn_water() returns true — the caller skips drawing).
func test_is_drawn_water_true_for_a_water_cell_single_window() -> void:
var stub := _ViewerStub.new()
stub.district_window = _mock_4x4_water_corner_window()
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(-1.5, -1.5), ctx)).override_failure_message(
"a district position over a WATER (OpenOcean) cell must be clipped"
).is_true()
## Tile mode: same land/water split, but the composite data arrives via
## get_tile_set().get_tiles() instead of get_district_window() — proving the
## clip predicate reaches BOTH path shapes, per the coordinator's explicit
## "both path shapes exercised" ask.
func test_is_drawn_water_works_in_tile_mode_land() -> void:
var stub := _ViewerStub.new()
stub.tile_mode = true
var ts := _TileSetStub.new()
ts.tiles = [{"center": Vector2i.ZERO, "window": _mock_4x4_water_corner_window()}]
stub.tile_set = ts
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(1.5, 1.5), ctx)).override_failure_message(
"tile mode: a district position over a LAND cell must not be clipped"
).is_false()
func test_is_drawn_water_works_in_tile_mode_water() -> void:
var stub := _ViewerStub.new()
stub.tile_mode = true
var ts := _TileSetStub.new()
ts.tiles = [{"center": Vector2i.ZERO, "window": _mock_4x4_water_corner_window()}]
stub.tile_set = ts
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(-1.5, -1.5), ctx)).override_failure_message(
"tile mode: a district position over a WATER cell must be clipped"
).is_true()
## Tyre's rule 5: NO arrived composite data at the queried position (single-
## window mode, window is null — the pre-arrival state) must FAIL OPEN — the
## clip is a presentation refinement, never a data gate.
func test_is_drawn_water_fails_open_with_no_composite_data_single_window() -> void:
var stub := _ViewerStub.new()
stub.district_window = null # nothing arrived yet
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).override_failure_message(
"no arrived composite data must fail OPEN (draw the dot), never clip"
).is_false()
## Same fail-open guarantee in tile mode: no tile set at all (get_tile_set()
## returns null, matching the viewer's own pre-enter_orbital() state).
func test_is_drawn_water_fails_open_with_no_tile_set() -> void:
var stub := _ViewerStub.new()
stub.tile_mode = true
stub.tile_set = null
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).override_failure_message(
"no tile set at all must fail OPEN (draw the dot), never clip"
).is_false()
## Fail-open ALSO covers "a tile set exists but no tile covers this position
## yet" (mid-progressive-arrival) — the coordinator's own "briefly-unclipped
## dot during progressive arrival is fine and self-heals" framing.
func test_is_drawn_water_fails_open_when_no_tile_covers_the_position() -> void:
var stub := _ViewerStub.new()
stub.tile_mode = true
var ts := _TileSetStub.new()
ts.tiles = [{"center": Vector2i(500, 500), "window": null}] # far away, unarrived
stub.tile_set = ts
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).is_false()
## Mouths must be SUPPRESSED (not snapped, not dimmed) on drawn water and
## render exactly as today on drawn land — this suite covers the SHARED
## predicate _draw_rivers() calls for river cells/confluences/mouths alike
## (_is_drawn_water() itself has no notion of "which feature type" — that's
## by design, per the ruling's "same predicate" wording for rule 3). A
## dedicated assertion here pins the WORDING intent (mouth-specific rule 3)
## even though the underlying mechanism is identical to the river-cell tests
## above — a future refactor that special-cases mouths differently should
## still trip this.
func test_mouth_position_on_water_is_suppressed_same_predicate_as_rivers() -> void:
var stub := _ViewerStub.new()
stub.district_window = _mock_4x4_water_corner_window()
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(-1.9, -1.9), ctx)).override_failure_message(
"a mouth position over drawn water must resolve as clipped, via the SAME"
+ " predicate river cells/confluences use — no separate snap/dim path"
).is_true()
func test_mouth_position_on_land_is_not_suppressed() -> void:
var stub := _ViewerStub.new()
stub.district_window = _mock_4x4_water_corner_window()
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(o._is_drawn_water(Vector2(1.9, 1.9), ctx)).override_failure_message(
"a mouth position over drawn land must render exactly as today (not clipped)"
).is_false()
# =============================================================================
# T-1170 Ruling 3g/5a: _segment_touches_drawn_water() — the CHORD SEGMENT
# clip rule (both endpoints + midpoint), replacing the old per-point-only
# clip for the skeleton-chord draw path. The water cell is (0,0) in district
# space, per _mock_4x4_water_corner_window()'s own doc — spans roughly
# [-0.5, 0.5) x [-0.5, 0.5) at this fixture's district granularity.
# =============================================================================
## Both endpoints on land, entirely away from the water cell — no clip.
func test_segment_touches_drawn_water_false_when_fully_on_land() -> void:
var stub := _ViewerStub.new()
stub.district_window = _mock_4x4_water_corner_window()
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(
o._segment_touches_drawn_water(Vector2(1.0, 1.0), Vector2(1.9, 1.9), ctx)
).override_failure_message(
"a segment entirely on land (both endpoints, and therefore its"
+ " midpoint) must not be clipped"
).is_false()
## Either endpoint alone on water clips the whole segment.
func test_segment_touches_drawn_water_true_when_an_endpoint_is_on_water() -> void:
var stub := _ViewerStub.new()
stub.district_window = _mock_4x4_water_corner_window()
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
assert_bool(
o._segment_touches_drawn_water(Vector2(-1.9, -1.9), Vector2(1.9, 1.9), ctx)
).override_failure_message(
"a segment with EITHER endpoint over drawn water must be clipped"
).is_true()
## The decision this rule specifically exists to catch (Ruling 3g's ask, "pick
## the visually cleaner rule, document it, test it"): BOTH endpoints on land,
## on opposite sides of the water cell, with the MIDPOINT landing inside it —
## an endpoints-only rule would miss this entirely (a chord visibly crossing
## open water with neither end clipped). The midpoint sample must catch it.
func test_segment_touches_drawn_water_true_when_only_midpoint_is_on_water() -> void:
var stub := _ViewerStub.new()
stub.district_window = _mock_4x4_water_corner_window()
var o = _make_overlay(stub)
var ctx: Dictionary = _ctx_for(stub)
# The water cell is (col=0, row=0), spanning district [-2,-1) x [-2,-1) in
# this n=4/grid_side=4 fixture (1:1 district-to-cell mapping). Pick
# endpoints that EACH resolve to a DIFFERENT LAND cell adjacent to the
# water corner — (-1.99, -0.9) resolves to (col=0, row=1), land; (-0.9,
# -1.99) resolves to (col=1, row=0), land — but their MIDPOINT
# (-1.445, -1.445) falls squarely inside the water cell (col=0, row=0).
# Verified numerically, not eyeballed (see the two sanity asserts below).
var from_district := Vector2(-1.99, -0.9)
var to_district := Vector2(-0.9, -1.99)
# Sanity: neither endpoint alone is clipped (both resolve to LAND cells)
# — isolates the midpoint as the ONLY reason the segment clips below.
assert_bool(o._is_drawn_water(from_district, ctx)).override_failure_message(
"test setup invariant: the FROM endpoint alone must resolve to land"
).is_false()
assert_bool(o._is_drawn_water(to_district, ctx)).override_failure_message(
"test setup invariant: the TO endpoint alone must resolve to land"
).is_false()
assert_bool(o._segment_touches_drawn_water(from_district, to_district, ctx)).override_failure_message(
"a segment whose ENDPOINTS are both on land but whose MIDPOINT lands"
+ " on drawn water must still be clipped — this is the exact failure"
+ " mode an endpoints-only rule would miss (Ruling 3g's ask)"
).is_true()
# =============================================================================
# T-1170 Ruling 5b (B3): _draw_course_path() early-return gating — the SAME
# "call the function directly when its early-return happens BEFORE any
# draw_*() call" precedent test_draw_with_null_viewer_is_a_noop() and
# test_draw_with_zero_grid_dims_returns_before_any_draw_call() already
# establish. Every case below returns before _draw_one_course() is ever
# reached, so calling _draw_course_path() directly (no SubViewport/render
# context) is safe. This is a SEPARATE data source/gate from the Layer-1
# skeleton path above — none of these tests touch _layer1 at all.
# =============================================================================
## The overlay-bar "gen_rivers" toggle gates the course path too — the SAME
## toggle the skeleton path uses (one player-facing "rivers" control covers
## both presentation surfaces, per the ruling).
func test_draw_course_path_returns_before_any_draw_when_gen_rivers_is_off() -> void:
var stub := _ViewerStub.new()
stub.overlay_visibility["gen_rivers"] = false
stub.district_window = {
"n": 64, "granularity_v2": "District",
"courses": [{"class": 2, "points": [[0, 0], [100, 0]], "terminus": "None"}],
}
var o = _make_overlay(stub)
o._draw_course_path() # must return before draw_polyline() — no crash outside a render context
## No district window at all (single-window mode hasn't arrived yet) — the
## course path must return cleanly, not crash on a null window read.
func test_draw_course_path_returns_before_any_draw_when_no_window() -> void:
var stub := _ViewerStub.new()
stub.district_window = null
var o = _make_overlay(stub)
o._draw_course_path()
## Ruling 3h decode tolerance: a window WITHOUT a `courses` key at all (the
## old/pre-A2 payload shape) must draw NOTHING at District/Quarter except
## mouths-on-land from the skeleton (that's the OTHER path's job) — this
## path itself must simply return, not error or fall back to a dot-scatter.
func test_draw_course_path_missing_courses_field_is_tolerated() -> void:
var stub := _ViewerStub.new()
stub.district_window = {"n": 64, "granularity_v2": "District"} # no "courses" key
var o = _make_overlay(stub)
o._draw_course_path()
## An explicitly present but EMPTY courses array must also be tolerated
## cleanly (the loop simply iterates zero times).
func test_draw_course_path_empty_courses_array_is_tolerated() -> void:
var stub := _ViewerStub.new()
stub.district_window = {"n": 64, "granularity_v2": "District", "courses": []}
var o = _make_overlay(stub)
o._draw_course_path()
## A `courses` field that is present but the WRONG TYPE (not an Array — e.g.
## a malformed/corrupted payload) must be tolerated the same way as a
## missing field, not crash attempting to iterate a non-Array.
func test_draw_course_path_non_array_courses_field_is_tolerated() -> void:
var stub := _ViewerStub.new()
stub.district_window = {"n": 64, "granularity_v2": "District", "courses": "not an array"}
var o = _make_overlay(stub)
o._draw_course_path()
@@ -1,299 +0,0 @@
## T-1156 wave 1: a REAL draw smoke test for AtlasWindowNatureOverlay,
## matching test_atlas_window_overlay_draw_smoke.gd's established pattern —
## `draw_circle()`/`draw_rect()`/`draw_arc()`/`draw_colored_polygon()` calls
## require a live render pass under this engine version (confirmed directly:
## a plain unit test calling `_draw()` outside one throws "Drawing is only
## allowed inside this node's `_draw()`..."). Render into a REAL SubViewport,
## force a settle wait, and assert visible non-background pixels — proving
## the river-dot/mouth-ring/basin-polygon/attractor-glyph draw calls actually
## paint, not just that the state feeding them is correct (that half is
## test_atlas_window_nature_overlay.gd's job).
##
## **REQUIRES A REAL RENDERING DRIVER — SKIPS (not fails) under
## `tests/run-godot`'s hardcoded `--headless`**, same posture/rationale as
## test_atlas_window_overlay_draw_smoke.gd's own header doc (dummy driver,
## no GPU texture output — SubViewport.get_texture().get_image() returns an
## all-zero/unusable image under it).
##
## To actually exercise this file's assertions, run it with a real driver:
## godot4 --display-driver x11 --rendering-driver opengl3 \
## -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c \
## -a res://tests/test_atlas_window_nature_overlay_draw_smoke.gd
##
## **Known limitation, verified directly (2026-07-23 zoom-compensation fix
## verification):** the shared _BackgroundRect/_render_to_image() harness
## (copied from test_atlas_window_overlay_draw_smoke.gd) occasionally fails
## to composite the COLOR_BG fill at all under a real X11/opengl3 run in this
## environment (sampled pixels read (0,0,0,0), not COLOR_BG — an unrelated,
## intermittent X11/SubViewport timing issue, confirmed via an XServer
## "BadMatch" warning in that same run's log). When that happens EVERY pixel
## reads as "non-background" regardless of what this overlay actually draws,
## making a low MIN_NON_BACKGROUND_FRACTION threshold pass VACUOUSLY (it
## would pass even with zoom-compensation deliberately broken — confirmed by
## direct revert-test). The terrain sibling suite is accidentally immune to
## this (its checkerboard composite covers most of the frame regardless of
## background correctness); this suite's SPARSE markers are not. **The
## reliable, environment-independent regression gate for the zoom-
## compensation fix is therefore the pure-function suite in
## test_atlas_window_geometry_nature.gd** (zoom_compensated_size()'s own
## tests) — this smoke suite is a supplementary "does it actually paint"
## check when the harness cooperates, not the primary gate.
class_name TestAtlasWindowNatureOverlayDrawSmoke
extends GdUnitTestSuite
const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd")
const COLOR_BG: Color = Color("#0d1117") # AtlasWindowViewer.COLOR_BG, mirrored (private const)
const VIEWPORT_SIZE: Vector2i = Vector2i(512, 512)
const MIN_NON_BACKGROUND_FRACTION: float = 0.0005 # river dots are sparse — a low bar is honest here
const SKIP_REASON: String = (
"no real rendering driver (dummy/headless) — run with e.g."
+ " `godot4 --display-driver x11 --rendering-driver opengl3"
+ " -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c"
+ " -a res://tests/test_atlas_window_nature_overlay_draw_smoke.gd` to exercise this file"
)
## Matches test_atlas_window_overlay_draw_smoke.gd's own detection exactly.
static func _dummy_renderer_active() -> bool:
return DisplayServer.get_name() == "headless"
## Same duck-typed viewer contract as test_atlas_window_nature_overlay.gd's
## _ViewerStub, minus the SimBridge-signal machinery this smoke test doesn't
## need (layer1 is injected directly via _layer1, not through a response).
## view_zoom MUST be kept in sync with whatever zoom _render_to_image() is
## called with — the whole point of this smoke suite (post-live-eyeball,
## coordinator finding 2026-07-23) is proving markers stay visible at the
## REAL orbital fit zoom, not an artificially large test zoom that would
## mask the zoom-compensation bug the fix addresses. is_tile_mode()/
## get_district_window()/get_tile_set() added for T-1172 (the water clip) —
## defaults to single-window mode with NO arrived composite (district_window
## null), the fail-open case, so these smoke tests keep drawing every marker
## exactly as before the clip existed (this file's own job is proving the
## draw calls paint pixels at all, not exercising the clip's water-detection
## branch — that's test_atlas_window_nature_overlay.gd's job).
class _ViewerStub:
var held_granularity_v2: String = "Region"
var body_radius_km: float = 6371.0
var held_center: Vector2i = Vector2i.ZERO
var held_n: int = 64
var view_zoom: float = 1.0
var overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": true, "gen_attractors": true}
var tile_mode: bool = false
var district_window: Variant = null
var tile_set: Variant = null
func get_held_granularity_v2() -> String:
return held_granularity_v2
func get_body_radius_km() -> float:
return body_radius_km
func get_held_center() -> Vector2i:
return held_center
func get_held_n() -> int:
return held_n
func get_cell_pixel_size() -> float:
return 16.0
func get_view_zoom() -> float:
return view_zoom
func is_overlay_visible(overlay_id: String) -> bool:
return bool(overlay_visibility.get(overlay_id, false))
func is_tile_mode() -> bool:
return tile_mode
func get_district_window() -> Variant:
return district_window
func get_tile_set() -> Variant:
return tile_set
## A dense scatter of river cells spanning the whole held window's canvas
## footprint (not clustered in one corner) — a genuine "does the composite
## draw something visible across the frame" check, same intent as the
## terrain smoke test's checkerboard morphology spread.
static func _mock_layer1_dense() -> Dictionary:
var river_cells: Array = []
var river_class: PackedByteArray = PackedByteArray()
for i in range(40):
river_cells.append([i * 3, i * 3])
river_class.append(2) # trunk — visible at every rung this suite exercises
return {
"river_network": {
"river_cells": river_cells,
"river_class": river_class,
"confluences": [[60, 60]],
"mouths": [[120, 120]],
},
"drainage_basins": [
{"basin_id": 1, "boundary": [[0, 0], [0, 40], [40, 40], [40, 0]]},
],
"attractors": [
{"position": [80, 80], "strength": 0.9, "attractor_type": "Oasis", "sub_biome": ""},
],
"grid_w": 256,
"grid_h": 128,
}
## Same _BackgroundRect/_render_to_image/_non_background_fraction shared
## rendering infrastructure as test_atlas_window_overlay_draw_smoke.gd —
## duplicated rather than imported since gdUnit4 test suites are not
## typically composed via inheritance in this codebase (no precedent for a
## shared test-infra base class in client/tests/), and the block is small.
class _BackgroundRect extends Node2D:
var fill_color: Color = Color.BLACK
var fill_size: Vector2 = Vector2.ZERO
func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, fill_size), fill_color)
func _render_to_image(overlay: Node2D, zoom: float = 1.0) -> Image:
var sub_viewport := SubViewport.new()
sub_viewport.size = VIEWPORT_SIZE
sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
sub_viewport.transparent_bg = false
add_child(sub_viewport)
auto_free(sub_viewport)
var bg := _BackgroundRect.new()
bg.fill_color = COLOR_BG
bg.fill_size = Vector2(VIEWPORT_SIZE)
sub_viewport.add_child(bg)
bg.queue_redraw()
var canvas := Node2D.new()
canvas.position = Vector2(VIEWPORT_SIZE) * 0.5
canvas.scale = Vector2(zoom, zoom)
sub_viewport.add_child(canvas)
canvas.add_child(overlay)
overlay.queue_redraw()
for _i in range(6):
await get_tree().process_frame
return sub_viewport.get_texture().get_image()
static func _non_background_fraction(image: Image) -> float:
var w: int = image.get_width()
var h: int = image.get_height()
if w <= 0 or h <= 0:
return 0.0
var total: int = w * h
var differing: int = 0
var bg_rgb: Color = Color(COLOR_BG.r, COLOR_BG.g, COLOR_BG.b, 1.0)
for y in range(h):
for x in range(w):
var px: Color = image.get_pixel(x, y)
var px_rgb: Color = Color(px.r, px.g, px.b, 1.0)
if not px_rgb.is_equal_approx(bg_rgb):
differing += 1
return float(differing) / float(total)
## Region rung, every toggle on: rivers + confluence + mouth + basin fill +
## attractor glyph must all draw SOMETHING — the "does the composite actually
## paint" proof the state-only unit suite cannot provide.
func test_region_rung_draws_visible_pixels() -> void:
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay := AtlasWindowNatureOverlay.new()
var stub := _ViewerStub.new()
overlay.viewer = stub
overlay._layer1 = _mock_layer1_dense()
overlay._requested_body_id = "SmokeBody"
# Zoom chosen so the held window's canvas footprint (held_n * cell_px =
# 64 * 16 = 1024 units) comfortably fills the 512px viewport.
var zoom: float = float(VIEWPORT_SIZE.x) / (float(stub.held_n) * stub.get_cell_pixel_size())
stub.view_zoom = zoom
var image: Image = await _render_to_image(overlay, zoom)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"the Region-rung nature composite (rivers + confluence + mouth + basin +"
+ " attractor, every toggle on) must render VISIBLE non-background pixels —"
+ " got only %.4f%% of the frame differing from COLOR_BG"
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
## District rung: only trunk rivers (class 2, all cells in the dense fixture
## are trunk) + the mouth carve-out draw; basins/attractors/confluences are
## rung-gated off. Still must produce visible pixels — proving the "fade
## down, don't vanish" posture actually leaves SOMETHING on screen.
func test_district_rung_still_draws_visible_pixels() -> void:
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay := AtlasWindowNatureOverlay.new()
var stub := _ViewerStub.new()
stub.held_granularity_v2 = "District"
overlay.viewer = stub
overlay._layer1 = _mock_layer1_dense()
overlay._requested_body_id = "SmokeBody"
var zoom: float = float(VIEWPORT_SIZE.x) / (float(stub.held_n) * stub.get_cell_pixel_size())
stub.view_zoom = zoom
var image: Image = await _render_to_image(overlay, zoom)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"the District-rung composite (trunk rivers + mouth carve-out only) must"
+ " still render VISIBLE non-background pixels — got only %.4f%% of the"
+ " frame differing from COLOR_BG"
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
## Coordinator live-eyeball regression (2026-07-23): the ORBITAL TILE MOSAIC
## rest state's REAL fit zoom on a Lendel-scale body (~0.0063, confirmed
## directly via a live drive script — held_n=19139 districts,
## cell_px=16, a ~1600px viewport) — NOT the comfortable ~0.5 zoom the
## sibling test above uses. Before the zoom-compensation fix, this exact
## zoom magnitude produced ZERO visible river/mouth pixels (a 2.2px trunk
## dot rasterized at ~0.014 screen px) despite RVR being on and the policy
## table correctly returning full Region visibility — the captures showed
## terrain-only with literally nothing drawn. Pins the regression at
## production scale, not a toy zoom that could accidentally still pass.
func test_orbital_scale_zoom_still_draws_visible_pixels() -> void:
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay := AtlasWindowNatureOverlay.new()
var stub := _ViewerStub.new()
stub.held_n = 19139 # Lendel's own raw circumference, live drive script
stub.view_zoom = 0.0063 # Lendel's own live orbital fit zoom
overlay.viewer = stub
overlay._layer1 = _mock_layer1_dense()
overlay._requested_body_id = "SmokeBody"
var image: Image = await _render_to_image(overlay, stub.view_zoom)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"at Lendel's REAL orbital fit zoom (~0.0063), the Region-rung nature"
+ " composite must still render VISIBLE non-background pixels — got only"
+ " %.4f%% of the frame differing from COLOR_BG. This is exactly the"
+ " coordinator's live-eyeball finding: uncompensated screen-space marker"
+ " sizes get multiplied by the canvas's own zoom transform, vanishing"
+ " sub-pixel at the orbital rest state's tiny fit zoom."
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
-399
View File
@@ -1,399 +0,0 @@
## T-1145 item 3 (interim presentation, pending the T-1143 design pass):
## tests for AtlasWindowOverlay's smoothed-composite texture rebuild cache —
## the "rebuild ONLY when window/overlay/tint inputs change, not per frame"
## requirement. Does not test the actual PIXEL CONTENT of the built texture
## (that content is exactly _cell_color()/_apply_glaciation(), already
## covered by test_atlas_window_colors.gd's colorizer tests — this file is
## about WHEN a rebuild happens, not what color a given cell produces).
class_name TestAtlasWindowOverlay
extends GdUnitTestSuite
static func _mock_window(n: int = 2) -> Dictionary:
return {
"center": [0, 0],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
## Minimal viewer stub — AtlasWindowOverlay only reaches the viewer through
## get_district_window()/is_overlay_visible()/get_cell_pixel_size()/
## 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 = ""
# T-1161: the viewer's currently-HELD rung tag — added alongside the
# per-rung filter tests below. Not read by AtlasWindowOverlay today (the
# overlay trusts each window dict's OWN echoed granularity_v2, per
# cell_grid_side_for_window()'s precedent) but a real AtlasWindowViewer
# exposes get_held_granularity_v2() (T-1153), so the stub carries it too
# for parity with the real duck-typed interface.
var held_granularity_v2: String = "District"
func get_district_window() -> Variant:
return window
func is_overlay_visible(overlay_id: String) -> bool:
return overlay_id == active_overlay
func get_cell_pixel_size() -> float:
return 16.0
func is_tile_mode() -> bool:
return false
func get_held_granularity_v2() -> String:
return held_granularity_v2
## T-1161 reframe: COMPOSITE_SMOOTH is now Axis 1 of TWO independent axes
## (see the file header doc) — the texture-vs-flat-rects PIPELINE choice.
## This assertion survives unchanged: the composite is still a TEXTURE at
## every rung. Axis 2 (which FILTER that texture samples with) is now a
## per-rung runtime decision covered separately below by the
## _filter_for_granularity_v2() tests — it is no longer bundled into this
## compile-time const.
func test_composite_smooth_defaults_true() -> void:
assert_bool(AtlasWindowOverlay.COMPOSITE_SMOOTH).override_failure_message(
"T-1145 item 3 ships the smoothed composite as the DEFAULT presentation"
).is_true()
## A fresh overlay with no draw yet has never built a texture — the cache
## starts empty.
func test_no_texture_before_first_draw() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
assert_that(o._cached_texture).is_null()
## First _rebuild_texture_if_needed() call for a real window builds a texture.
func test_rebuild_builds_a_texture_on_first_call() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
assert_that(o._cached_texture).override_failure_message(
"the first rebuild call for a real window must produce a texture"
).is_not_null()
## Calling _rebuild_texture_if_needed() AGAIN with the SAME window object
## (same reference) and the same active toggle must NOT rebuild — the exact
## same ImageTexture instance survives (reference equality, not just
## "another texture that happens to look the same").
func test_rebuild_is_a_noop_when_window_and_toggle_are_unchanged() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
var first_texture: ImageTexture = o._cached_texture
o._rebuild_texture_if_needed(window, 2, "")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"an unchanged (window, active_toggle) pair must reuse the SAME texture"
+ " object, not rebuild an equivalent-but-new one"
).is_true()
## A DIFFERENT window object (even with identical field VALUES) — matching
## what a fresh server response always is, a new Dictionary — MUST trigger a
## rebuild. This is the reference-vs-value distinction the class doc calls
## out explicitly (is_same(), not a deep compare).
func test_rebuild_fires_for_a_different_window_object_with_same_values() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window_a: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window_a, 2, "")
var first_texture: ImageTexture = o._cached_texture
# A structurally-IDENTICAL but DISTINCT Dictionary object — the exact
# shape a second server response for the same window content would be.
var window_b: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window_b, 2, "")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"a new window object (even with identical field values) must trigger a"
+ " fresh rebuild — the cache key is REFERENCE identity, not value equality"
).is_false()
## Changing the active toggle overlay (same window object) must ALSO trigger
## a rebuild — temp/moisture/veg/base each read different colors per cell.
func test_rebuild_fires_when_active_toggle_changes() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
var first_texture: ImageTexture = o._cached_texture
o._rebuild_texture_if_needed(window, 2, "gen_dw_temp")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"switching the active toggle overlay must trigger a rebuild — the SAME"
+ " window's cells read different colors under a different toggle"
).is_false()
## Panning/zooming (which redraw this node constantly via _apply_transform())
## never touches window/overlay state — repeated rebuild CALLS with identical
## inputs (simulating many redraws while nothing about the DATA changed) must
## all be no-ops after the first, confirming the "not per frame" requirement
## end to end, not just for a single repeat.
func test_repeated_rebuild_calls_with_unchanged_inputs_all_reuse_the_same_texture() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var window: Dictionary = _mock_window()
o._rebuild_texture_if_needed(window, 2, "")
var first_texture: ImageTexture = o._cached_texture
for _i in range(20): # 20 simulated redraws (pan/zoom frames)
o._rebuild_texture_if_needed(window, 2, "")
assert_bool(is_same(o._cached_texture, first_texture)).override_failure_message(
"20 repeated rebuild calls with unchanged inputs must never touch the cache"
).is_true()
## The overlay's real _draw() entry point (via the smoothed path) produces a
## texture through the SAME _ViewerStub duck-typed interface every other
## caller in this cluster uses — an end-to-end sanity check that _draw()
## actually reaches _rebuild_texture_if_needed() for a real window, not just
## that the helper works in isolation.
func test_draw_builds_a_texture_through_the_viewer_stub() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.window = _mock_window()
o.viewer = stub
o._draw()
assert_that(o._cached_texture).is_not_null()
# =============================================================================
# T-1152/T-1153: cell_grid_side_for_window() — the district-extent-vs-
# derived-cell-grid split every rung's response now carries.
# =============================================================================
## District (the default/omitted tag): cell_grid_side == n, unchanged from
## the pre-T-1152 identity mapping.
func test_cell_grid_side_for_window_district_matches_n() -> void:
var window: Dictionary = {"n": 32, "granularity_v2": "District"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(32)
## Quarter: 4x MORE cells than districts (WINDOW_GRANULARITY_QUARTER).
func test_cell_grid_side_for_window_quarter_multiplies_by_four() -> void:
var window: Dictionary = {"n": 32, "granularity_v2": "Quarter"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(128)
## Region: FAR FEWER cells than districts — round(n/100), matching
## WindowGranularity::cell_grid_side's own Region branch exactly (the
## "inversion" the server doc calls out: finer rungs multiply, Region divides).
func test_cell_grid_side_for_window_region_divides_by_districts_per_region() -> void:
var window: Dictionary = {"n": 6400, "granularity_v2": "Region"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(64)
## A Region window smaller than one region (n < 100) must still derive a
## minimum 1x1 cell grid, never 0 — matching the server's `.max(1)`.
func test_cell_grid_side_for_window_region_minimum_is_one() -> void:
var window: Dictionary = {"n": 50, "granularity_v2": "Region"}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(1)
## Missing granularity_v2 (an old-shape response) falls back to District —
## matching the server's own "unknown -> District" posture at every
## resolution boundary.
func test_cell_grid_side_for_window_missing_tag_falls_back_to_district() -> void:
var window: Dictionary = {"n": 32}
assert_int(AtlasWindowOverlay.cell_grid_side_for_window(window)).is_equal(32)
## T-1152/T-1153, design doc §6 encoding continuity: a Region-rung window (a
## FAR SPARSER cell grid — one region cell spans 100 districts) renders
## through the EXACT SAME _draw()/_rebuild_texture_if_needed() path as
## District — no separate branch, no crash reading past the (much smaller)
## per-cell arrays. This is the direct "same colorizer family at every rung"
## behavioral test the ticket asks for.
func test_draw_builds_a_texture_for_a_region_rung_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
# n=200 districts -> cell_grid_side = round(200/100) = 2 -> 4 cells,
# matching the 4-entry per-cell arrays below (same shape _mock_window()
# uses, just at Region's district-to-cell ratio).
stub.window = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o.viewer = stub
o._draw()
assert_that(o._cached_texture).override_failure_message(
"a Region-rung window must render through the same composite path as District"
).is_not_null()
assert_int(o._cached_texture.get_width()).override_failure_message(
"the built texture's resolution must be the DERIVED cell-grid side (2),"
+ " not the window's district extent (200)"
).is_equal(2)
## §6 "no mode flip" acceptance criterion, restated at the texture-cache
## level: swapping from a District-rung window to a Region-rung window at a
## NEW window object (the progressive-refinement swap) must still go through
## a single rebuild call producing a fresh texture — not a crash, not a
## silently-stale texture sized for the wrong rung.
func test_rebuild_handles_a_rung_swap_from_district_to_region() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var district_window: Dictionary = _mock_window() # n=2, District, 4 cells
o._rebuild_texture_if_needed(
district_window, AtlasWindowOverlay.cell_grid_side_for_window(district_window), ""
)
assert_int(o._cached_texture.get_width()).is_equal(2)
var region_window: Dictionary = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o._rebuild_texture_if_needed(
region_window, AtlasWindowOverlay.cell_grid_side_for_window(region_window), ""
)
assert_int(o._cached_texture.get_width()).override_failure_message(
"a rung swap must rebuild at the NEW rung's derived cell-grid resolution"
).is_equal(2) # region_window's cell_grid_side is also 2 here (200/100) — same size, different data
# =============================================================================
# T-1161: _filter_for_granularity_v2() — the per-rung sampling filter policy
# (Araminta's ruling: Region incl. the orbital tile mosaic -> NEAREST;
# District/Quarter -> LINEAR; no hysteresis, no px-per-cell threshold, keyed
# purely on rung IDENTITY).
# =============================================================================
## Region is the sparse rung the ruling targets — GPU bilinear blending at
## 204.8 km/cell reads as smoothing-over-absence, so it samples NEAREST.
func test_filter_for_granularity_v2_region_is_nearest() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Region")).override_failure_message(
"Region must sample TEXTURE_FILTER_NEAREST — dense-enough rungs get LINEAR,"
+ " Region is the sparse one the ruling targets"
).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
## District is dense enough that the bilinear blend reads as texture, not as
## papering over sparse data — LINEAR is earned.
func test_filter_for_granularity_v2_district_is_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("District")).override_failure_message(
"District must sample TEXTURE_FILTER_LINEAR"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## Quarter (4x MORE cells than District) is denser still — also LINEAR.
func test_filter_for_granularity_v2_quarter_is_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Quarter")).override_failure_message(
"Quarter must sample TEXTURE_FILTER_LINEAR"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## An unrecognized/empty tag must NEVER be trusted into the crisp NEAREST
## treatment — mirrors cell_grid_side_for_window()'s own "unknown -> District"
## fallback posture, failing toward the already-shipped LINEAR look rather
## than an unintended NEAREST for a wire shape this code doesn't recognize.
func test_filter_for_granularity_v2_unknown_falls_back_to_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("")).override_failure_message(
"an empty/unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("SomeFutureRung")).override_failure_message(
"an unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## Integration-shaped: a Region-rung window dict, drawn through the real
## _draw() entry point via the _ViewerStub duck-typed interface (same
## end-to-end shape as test_draw_builds_a_texture_for_a_region_rung_window()
## above), must drive the NODE's own texture_filter property to NEAREST —
## not just the helper function in isolation.
func test_draw_sets_node_texture_filter_to_nearest_for_region_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.held_granularity_v2 = "Region"
stub.window = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o.viewer = stub
o._draw()
assert_int(o.texture_filter).override_failure_message(
"a Region-rung window must drive the node's texture_filter to NEAREST after a draw"
).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
## The District-rung counterpart of the above — confirms the node's
## texture_filter lands on LINEAR (not left over from a previous NEAREST
## draw, and not defaulting to NEAREST) for the dense rung.
func test_draw_sets_node_texture_filter_to_linear_for_district_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.window = _mock_window() # District (the default/omitted tag)
o.viewer = stub
o._draw()
assert_int(o.texture_filter).override_failure_message(
"a District-rung window must drive the node's texture_filter to LINEAR after a draw"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## A rung SWAP (Region -> District, on the SAME node) must flip texture_filter
## along with it — confirms the property is recomputed every draw, not
## sticky from the first rung the node ever rendered.
func test_draw_flips_node_texture_filter_on_a_rung_swap() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.window = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o.viewer = stub
o._draw()
assert_int(o.texture_filter).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
stub.window = _mock_window() # swap to District
o._draw()
assert_int(o.texture_filter).override_failure_message(
"swapping to a District-rung window must flip texture_filter to LINEAR,"
+ " not leave it stuck at the previous rung's NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
@@ -1,407 +0,0 @@
## Live round 4: a REAL draw smoke test — the "does anything draw at all"
## gap has now bitten twice (round 4's tile-mosaic coordinate bug AND its
## per-tile-texture-lifetime bug, both invisible to test_atlas_window_overlay.gd's
## existing suite, which only asserts on the CACHE FIELDS being populated —
## never on an actual composited pixel). This file closes that gap
## structurally: render AtlasWindowOverlay into a REAL SubViewport, force a
## GPU sync, grab the rendered Image, and assert a meaningful fraction of
## pixels differ from the background color — for BOTH the single-window path
## (a) and the tile-mosaic path (b), matching the coordinator's explicit ask.
##
## **REQUIRES A REAL RENDERING DRIVER — SKIPS (not fails) under
## `tests/run-godot`'s hardcoded `--headless`** (dummy driver, no GPU texture
## output; confirmed directly: SubViewport.get_texture().get_image() returns
## an all-zero/unusable image under it). This matters beyond "the assertions
## are meaningless there": the push gate runs the FULL suite through
## `tests/run-godot --headless` for every push, for everyone — a loud FAILURE
## here would bounce every future push project-wide, not just report a local
## false negative. Every test below carries the gdUnit4 fuzzer-arg skip
## convention (`_do_skip`/`_skip_reason`, matching test_input_gate_live.gd's
## own server-binary-not-built skip) keyed on `_dummy_renderer_active()`, so
## `tests/run-godot --filter test_atlas_window_overlay_draw_smoke` reports
## green-with-skips under headless, not red.
##
## To actually exercise this file's assertions, run it with a real driver:
## godot4 --display-driver x11 --rendering-driver opengl3 \
## -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c \
## -a res://tests/test_atlas_window_overlay_draw_smoke.gd
## (matching tests/visual_capture.gd's own documented "requires a real
## rendering driver" precedent — see docs/DEVOPS.md's own note on this file.)
class_name TestAtlasWindowOverlayDrawSmoke
extends GdUnitTestSuite
const AtlasWindowOverlay := preload("res://ui/implant/apps/atlas/atlas_window_overlay.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
const COLOR_BG: Color = Color("#0d1117") # AtlasWindowViewer.COLOR_BG, mirrored (private const)
const VIEWPORT_SIZE: Vector2i = Vector2i(512, 512)
## Minimum fraction of the captured image that must differ from COLOR_BG for
## a draw to count as "genuinely rendered something" — low enough to tolerate
## a mostly-water/mostly-one-color composite (round 4's own repro shots were
## legitimately near-uniform ocean at some zooms), high enough that a
## fully-blank/fully-background/fully-white frame (both round 4 bugs) fails it.
const MIN_NON_BACKGROUND_FRACTION: float = 0.05
const SKIP_REASON: String = (
"no real rendering driver (dummy/headless) — run with e.g."
+ " `godot4 --display-driver x11 --rendering-driver opengl3"
+ " -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c"
+ " -a res://tests/test_atlas_window_overlay_draw_smoke.gd` to exercise this file"
)
## True under Godot's `--display-driver headless` (the dummy renderer
## `tests/run-godot`'s hardcoded `--headless` flag selects) — `DisplayServer.
## get_name()` reports `"headless"` there and the real driver name (`"X11"`,
## `"Wayland"`, etc.) otherwise, confirmed directly against both this
## worktree's `tests/run-godot` invocation and a real `--display-driver x11
## --rendering-driver opengl3` run. Named as a function, not a const, since
## `DisplayServer` singleton state isn't available at script-parse time.
static func _dummy_renderer_active() -> bool:
return DisplayServer.get_name() == "headless"
## Single-window viewer stub — mirrors test_atlas_window_overlay.gd's
## _ViewerStub exactly (is_tile_mode() -> false), so this exercises the
## SAME single-window draw path that suite's cache tests cover, just
## through a REAL render instead of inspecting `_cached_texture` directly.
class _SingleWindowViewerStub:
var window: Variant = null
func get_district_window() -> Variant:
return window
func is_overlay_visible(_overlay_id: String) -> bool:
return false
func get_cell_pixel_size() -> float:
return 16.0
func is_tile_mode() -> bool:
return false
## Tile-mode viewer stub — is_tile_mode() -> true, get_tile_set() returns a
## bare object exposing get_tiles() (AtlasWindowOverlay's own duck-typed
## contract, matching AtlasWindowTileSet.get_tiles()'s public shape exactly:
## Array of {"center": Vector2i, "window": Variant}).
class _TileModeViewerStub:
# PR #192 cold-start dossier: AtlasWindowOverlay reads viewer.COLOR_BORDER_FADE
# directly for a pending tile's wash (avoids a cyclic preload of the
# viewer's own script — see that read site's own doc) — mirrored here
# byte-for-byte (AtlasWindowViewer.COLOR_BORDER_FADE, private const).
const COLOR_BORDER_FADE: Color = Color(0.20, 0.24, 0.30, 0.55)
var tiles: Array = []
var held_center: Vector2i = Vector2i.ZERO
var held_n: int = 0
# Live round 5: nearest_wrap_image()'s cols input — 0 here (a no-radius
# passthrough) is fine for these tests, which don't exercise the wrap
# seam itself (that's test_atlas_window_geometry.gd's own coverage);
# this stub only needs to satisfy _draw_tile_mosaic()'s duck-typed call.
var body_radius_km: float = 0.0
func get_district_window() -> Variant:
return null
func is_overlay_visible(_overlay_id: String) -> bool:
return false
func get_cell_pixel_size() -> float:
return 16.0
func is_tile_mode() -> bool:
return true
func get_tile_set() -> Variant:
return _TileSetStub.new(tiles)
func get_held_center() -> Vector2i:
return held_center
func get_held_n() -> int:
return held_n
func get_body_radius_km() -> float:
return body_radius_km
class _TileSetStub:
var _tiles: Array = []
func _init(tiles: Array) -> void:
_tiles = tiles
func get_tiles() -> Array:
return _tiles
## A `Node2D._draw()`-based flat-fill background — deliberately NOT a
## `ColorRect` (a `Control`). A `ColorRect` parented directly under a bare
## `SubViewport` (no intervening `Control` container establishing its own
## layout rect) did not reliably render in this harness: sampled pixels came
## back fully transparent `(0,0,0,0)` regardless of the ColorRect's `color`/
## `size`, even after entering the tree before sizing. `AtlasWindowOverlay`
## itself is a bare `Node2D` using `draw_rect()` for its own background wash
## (`AtlasWindowViewer._draw()`'s own `COLOR_BG` fill) — matching that same,
## already-proven-working `Node2D.draw_rect()` pattern here sidesteps
## whatever `Control`-specific layout/compositing gap caused the ColorRect
## failure, rather than debugging that gap for its own sake.
class _BackgroundRect extends Node2D:
var fill_color: Color = Color.BLACK
var fill_size: Vector2 = Vector2.ZERO
func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, fill_size), fill_color)
static func _mock_window(center: Vector2i = Vector2i.ZERO, n: int = 64) -> Dictionary:
# A checkerboard-ish morphology spread (not all-one-zone) so the built
# composite has genuine color VARIATION, not just "one flat non-background
# color" — closer to what a real terrain response looks like.
var cells: int = n * n
var morphology := PackedByteArray()
var elev_q := PackedByteArray()
morphology.resize(cells)
elev_q.resize(cells)
for i in range(cells):
morphology[i] = (i % 4) as int # cycles through the 4 morphology zones
elev_q[i] = (i * 7) % 100 as int
return {
"center": [center.x, center.y],
"n": n,
"granularity_v2": "District",
"morphology": morphology,
"elev_q": elev_q,
"temp_dc": [],
"moisture_q": PackedByteArray(),
"vegetation": PackedByteArray(),
"glaciation": PackedByteArray(),
}
## Renders `overlay` (any Node2D with its own `_draw()` — an
## AtlasWindowOverlay for (a)/(b) below, or a bare `_BackgroundRect` probe for
## the harness sanity check) parented under a Node2D positioned/scaled the
## way AtlasWindowViewer._canvas would be, into a fresh SubViewport, and
## returns the captured Image. `zoom` mirrors AtlasWindowViewer._canvas.scale
## (real `fit_window_view()` output is a small fraction, e.g. ~0.006 for a
## whole-body tile mosaic per live round 4's own repro) — WITHOUT it, a
## tile's real-world extent (TILE_N * cell_px = 102,400 local units) is so
## much larger than any realistic test viewport that a mis-POSITIONED tile
## still overlaps the frame purely by being gigantic, making the position
## math this test exists to catch silently unfalsifiable (confirmed
## directly: an earlier version of this test without a zoom scale kept
## passing even with live round 4's tile-coordinate bug deliberately
## reintroduced). Also confirmed directly: a hand-rolled duplicate of this
## same SubViewport/settle-loop setup (the harness sanity check's ORIGINAL
## standalone version) was measurably less reliable under a real driver than
## going through this shared path — reuse over duplication here isn't just
## tidiness, it's the more reliable rendering path.
func _render_to_image(overlay: Node2D, zoom: float = 1.0) -> Image:
var sub_viewport := SubViewport.new()
sub_viewport.size = VIEWPORT_SIZE
sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
sub_viewport.transparent_bg = false
add_child(sub_viewport)
auto_free(sub_viewport)
var bg := _BackgroundRect.new()
bg.fill_color = COLOR_BG
bg.fill_size = Vector2(VIEWPORT_SIZE)
sub_viewport.add_child(bg)
bg.queue_redraw()
var canvas := Node2D.new()
canvas.position = Vector2(VIEWPORT_SIZE) * 0.5 # center the composite's local (0,0)
canvas.scale = Vector2(zoom, zoom)
sub_viewport.add_child(canvas)
canvas.add_child(overlay)
overlay.queue_redraw()
# Bounded settle wait, NOT `await RenderingServer.frame_post_draw` — that
# signal never fires under the dummy/headless driver (confirmed directly:
# a first version of this file using it hung for the full 300s
# tests/run-godot wall-clock cap and was force-killed, producing a FALSE
# "0 tests, passed" result — exactly the silent-hang failure mode the
# `_do_skip`/`_dummy_renderer_active()` gate (this file's header doc) now
# avoids structurally instead). A fixed small number of `process_frame`
# awaits settles real rendering — confirmed sufficient against a real
# driver during this fix's own live verification.
for _i in range(6):
await get_tree().process_frame
return sub_viewport.get_texture().get_image()
## Fraction of `image`'s pixels whose RGB differs from COLOR_BG (alpha
## ignored — the ColorRect background is opaque, everything drawn on top of
## it is what's under test).
static func _non_background_fraction(image: Image) -> float:
var w: int = image.get_width()
var h: int = image.get_height()
if w <= 0 or h <= 0:
return 0.0
var total: int = w * h
var differing: int = 0
var bg_rgb: Color = Color(COLOR_BG.r, COLOR_BG.g, COLOR_BG.b, 1.0)
for y in range(h):
for x in range(w):
var px: Color = image.get_pixel(x, y)
var px_rgb: Color = Color(px.r, px.g, px.b, 1.0)
if not px_rgb.is_equal_approx(bg_rgb):
differing += 1
return float(differing) / float(total)
## (a) Single-window path: a District-rung response must render as visibly
## non-background pixels through AtlasWindowOverlay._draw()'s own
## Rect2(0,0,extent,extent) draw call — the "does the OVERLAY actually paint
## something for a real window" half of the gap. Positions it at the
## SubViewport's center via the surrounding Node2D, mirroring _canvas's role
## in the real viewer.
##
## Honest scope note: live round 4's SECOND bug (leaving `_view_offset`
## stale across a rung crossing in `_maybe_reselect_rung()`) lived entirely
## in AtlasWindowViewer's transform bookkeeping, ONE LAYER ABOVE this
## overlay-only test's boundary — it never touched `_draw()` itself, so a
## pure-overlay smoke test structurally cannot reproduce it (there is no
## "stale vs. fresh offset" state to compare inside the overlay alone). That
## regression's coverage is `_maybe_reselect_rung()`'s own unit tests in
## test_atlas_zoom_ladder.gd. This test's job is narrower and still real:
## proving the overlay's draw call itself produces visible output for
## legitimate window data, closing the "the composite Rect2 call is
## silently a no-op" class of bug regardless of which layer caused it.
func test_single_window_draw_produces_visible_pixels() -> void:
# Guarded early-return instead of the _do_skip fuzzer-arg convention:
# gdUnit4 leaks one internal <Node> per fuzzer-skipped test, tripping the
# orphan detector (exit 101) and bouncing the push gate even at 0 failures
# (PR gate run 2026-07-22: "2 skipped | 2 orphans | Exit code: 101").
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
var stub := _SingleWindowViewerStub.new()
stub.window = _mock_window(Vector2i.ZERO, 32)
overlay.viewer = stub
var image: Image = await _render_to_image(overlay)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"single-window composite must render VISIBLE non-background pixels — got"
+ " only %.2f%% of the frame differing from COLOR_BG. This is exactly the"
+ " shape of live round 4's second bug: _view_offset left stale across a"
+ " rung crossing pushed the composite off-canvas, so nothing but"
+ " background/chrome ever appeared, despite the underlying window data"
+ " and draw calls being individually 'correct' in isolation."
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
## (b) Tile-mosaic path: a tile at a district center AWAY from the body's
## own origin must still render as visibly non-background pixels once
## correctly placed via `district_to_canvas_local()`'s shared held_center/
## held_n convention — this is the path round 4's FIRST and THIRD bugs
## (tile-local-origin math ignoring that convention, and per-tile
## ImageTexture objects with no persistent reference being garbage-
## collected/GPU-desynced before their draw command flushed) would both
## have failed. Uses production-realistic scale (live round 4's own Lendel
## repro: raw circumference ~19,139 districts) — see the tile-center
## comment below for why scale matters here specifically.
func test_tile_mosaic_draw_produces_visible_pixels() -> void:
# Guarded early-return, not _do_skip — see the sibling test's comment.
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
var stub := _TileModeViewerStub.new()
var tile_n: int = AtlasWindowGeometry.TILE_N
var cell_px: float = stub.get_cell_pixel_size()
stub.held_center = Vector2i.ZERO
stub.held_n = 19139
# A SINGLE tile chosen so the CORRECT canvas-local formula
# (`district_to_canvas_local()`, anchored at `held_center - held_n/2`)
# lands it centered in the viewport, while the round-4 BUGGY formula
# (anchored at absolute district (0,0) directly) lands it almost
# `held_n/2 * cell_px` local units away — tens of thousands of units at
# this scale, i.e. genuinely fully off a 512x512 viewport, not just
# "shifted but still overlapping" (confirmed by hand-computation: a
# smaller/toy-scale version of this test stayed green with the bug
# reintroduced, because the shift stayed within the viewport bounds
# either way — this scale/center combination is chosen specifically to
# avoid that false-negative).
var half_tile: float = float(tile_n) * 0.5
var half_body: float = float(stub.held_n) * 0.5
var lone_tile_center := Vector2i(roundi(half_tile - half_body), roundi(half_tile - half_body))
stub.tiles = [
{"center": lone_tile_center, "window": _mock_window(lone_tile_center, 64)},
]
overlay.viewer = stub
# A zoom small enough that the buggy-vs-correct shift (~half_body * cell_px
# local units) is comfortably larger than the viewport — see the center
# choice's own doc above for why this specific magnitude matters.
var zoom: float = float(VIEWPORT_SIZE.x) * 1.5 / (half_body * cell_px)
var image: Image = await _render_to_image(overlay, zoom)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"tile mosaic must render VISIBLE non-background pixels across its tiles —"
+ " got only %.2f%% of the frame differing from COLOR_BG. This is exactly"
+ " the shape of live round 4's bugs: (1) tile local-origin computed"
+ " relative to absolute district (0,0) instead of the shared"
+ " held_center/held_n canvas-local convention pushed the whole mosaic"
+ " off-canvas, and (2) even once correctly positioned, an unstored"
+ " per-draw-call ImageTexture rendered as a blank/white gap despite"
+ " provably-correct CPU-side pixel data — both invisible to any test that"
+ " only inspects Dictionary/cache state, never an actual composited pixel."
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
## (c) PR #192 cold-start dossier, BUG 3: a mosaic tile with NO window yet
## (the cold-server "still working" state, every tile in the mosaic at once
## right after enter_orbital() on a real cold server) must render the SAME
## border-fade wash the single-window path already gives its own
## no-composite-yet wait — not a bare COLOR_BG gap that reads as broken.
## Same real-render infrastructure as (a)/(b): a pending tile (`window: null`)
## must still produce visible non-background pixels, proving the wash
## genuinely draws rather than the loop just `continue`-ing past it silently.
func test_pending_tile_gets_a_visible_border_fade_wash() -> void:
if _dummy_renderer_active():
print(SKIP_REASON)
return
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
var stub := _TileModeViewerStub.new()
var tile_n: int = AtlasWindowGeometry.TILE_N
var cell_px: float = stub.get_cell_pixel_size()
stub.held_center = Vector2i.ZERO
stub.held_n = 19139 # GJ380c/Lendel's own raw circumference (live round 4)
# Same centered-tile placement as (b) above, but with `window: null` —
# the pending state this fix targets, instead of a real response.
var half_tile: float = float(tile_n) * 0.5
var half_body: float = float(stub.held_n) * 0.5
var lone_tile_center := Vector2i(roundi(half_tile - half_body), roundi(half_tile - half_body))
stub.tiles = [{"center": lone_tile_center, "window": null}]
overlay.viewer = stub
var zoom: float = float(VIEWPORT_SIZE.x) * 1.5 / (half_body * cell_px)
var image: Image = await _render_to_image(overlay, zoom)
var fraction: float = _non_background_fraction(image)
assert_float(fraction).override_failure_message(
(
"a pending tile (window == null) must still render a visible border-fade"
+ " wash — got only %.2f%% of the frame differing from COLOR_BG, meaning"
+ " the tile is a bare background gap during the cold-server wait, which"
+ " reads as broken rather than 'still working' (coordinator's closing"
+ " question, PR #192 cold-start dossier)."
)
% (fraction * 100.0)
).is_greater(MIN_NON_BACKGROUND_FRACTION)
-652
View File
@@ -1,652 +0,0 @@
## T-1150 (PR #191 review, Hoshe 4): atlas_window_request.gd had NO test file
## at all before this — direct coverage of the granularity/min_wl_m staleness
## guard, the n-clamp mirror (Tyre C1), and the old-server-shape default
## disposition. Follows test_atlas_window_viewer.gd's own
## "AtlasWindowRequest — cache reuse" section conventions (same
## instantiation pattern: `AtlasWindowRequest.new(owner_stub)`, `add_child()`
## for the debounce Timer, hand-built response dicts) rather than
## re-inventing a shape.
class_name TestAtlasWindowRequest
extends GdUnitTestSuite
# atlas_window_request.gd has no class_name (review #8 precedent throughout
# this cluster) — preloaded once here, not re-load()ed per test (gdlint
# duplicated-load).
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
## Dudley's WINDOW_GRANULARITY_REGION_KEY (server/src/atlas/layer_proxy.rs) —
## `u32::MAX`, the RESERVED KEY-SPACE TAG a real server ALWAYS puts in the
## legacy `granularity` slot for every Region response (never a real
## multiplier — District=1/Quarter=4 are the only legal wire multipliers).
## Do NOT "fix" this to 1 — using a convenient value here is EXACTLY the gap
## the live round caught (a mock that diverges from the wire in the one
## field that matters silently un-repros the bug). See
## AtlasWindowRequest's `_echoed_granularity_matches()` doc for the full
## rationale.
const SERVER_LEGACY_GRANULARITY_REGION_SENTINEL: int = 4294967295
## Build a hand-authored DistrictWindowLayer dict, granularity-aware
## (T-1150, extended T-1152/T-1153 for granularity_v2) — mirrors
## test_atlas_window_viewer.gd's own _mock_window(), with
## granularity/min_wl_m/granularity_v2 added as optional params so callers
## can build any rung's echo shape with one helper.
static func _mock_window(
center: Vector2i,
n: int = 2,
granularity: int = 1,
min_wl_m: int = 0,
granularity_v2: String = "District"
) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"granularity": granularity,
"min_wl_m": min_wl_m,
"granularity_v2": granularity_v2,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
## PR #192 cold-start round 3: the WIRE-ACCURATE shape of a cold body's
## first-ever response (server/src/atlas/layer_proxy.rs's
## get_or_generate()/serve_district_window(), whole-body cache MISS branch —
## `AtlasLayerResponse { status: Pending, district_window: None, ... }`,
## confirmed directly against that source). `body_id` is the only
## identifying field.
static func _pending_response(body_id: String) -> Dictionary:
return {"body_id": body_id, "status": "Pending", "district_window": null}
static func _not_found_response(body_id: String) -> Dictionary:
return {"body_id": body_id, "status": "NotFound", "district_window": null}
## Matches atlas_map_protocol.gd's `_decode_status_field()` — the decoded
## `AtlasLayerStatus::Error(String)` variant's status STRING is always just
## "Error" (the message rides in a separate `error` field).
static func _error_response(body_id: String, message: String = "boom") -> Dictionary:
return {"body_id": body_id, "status": "Error", "error": message, "district_window": null}
func _make_request() -> Variant:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
return req
# =============================================================================
# (a) granularity mismatch on the echo -> dropped as stale
# =============================================================================
## The mandatory item-(a) case: request_now() asks at the default district
## granularity (1); a response echoing granularity=4 (quarter) for the SAME
## center/n must be dropped as stale, not accepted — a different rung's
## derive answering a request for a different rung is exactly as stale as a
## mismatched center (T-1150 extends §2's guard to this axis).
##
## **Live-round correction:** the mock MUST carry a mismatched
## `granularity_v2` too (explicit `"Quarter"`, not `_mock_window()`'s
## `"District"` default) — a real Quarter response ALWAYS carries
## `granularity_v2: "Quarter"` on the wire, never the District default this
## test's fixture used to leave implicit. Under the v2-authoritative-when-
## present precedence rule (see on_response()'s own doc), a v2-MATCHING
## response is accepted regardless of what the legacy int says — leaving
## granularity_v2 at its District default here would have made this test
## pass for the wrong reason (an accidentally-matching v2 field masking a
## genuinely mismatched legacy int), exactly the class of gap the live round
## caught in the oversized-orbital round-trip test.
func test_on_response_with_mismatched_granularity_is_dropped_as_stale() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).is_true()
var quarter_window: Dictionary = _mock_window(Vector2i(2, 2), 2, 4, 0, "Quarter")
req.on_response(_mock_response("GJ380c", quarter_window))
assert_bool(req.is_pending()).override_failure_message(
"a granularity-mismatched response must be dropped as stale, leaving the district request still pending"
).is_true()
# =============================================================================
# (a2) granularity_v2 mismatch on the echo -> dropped as stale (T-1152/T-1153,
# the axis the legacy int alone cannot express — Region has no legacy value)
# =============================================================================
## request_now() can now ask for Region explicitly (T-1153's rung-reselect
## caller) — a response echoing "District" for the SAME center/n must be
## dropped as stale, the granularity_v2 twin of test (a) above, and the
## ONLY guard that can catch this specific mismatch (the legacy int is
## DISTRICT_GRANULARITY=1 on BOTH sides here, since Region has no legacy
## representation — see WindowGranularity::legacy_u32()'s doc).
func test_on_response_with_mismatched_granularity_v2_is_dropped_as_stale() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_bool(req.is_pending()).is_true()
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 6400, 1, 0, "District")
req.on_response(_mock_response("GJ380c", district_window))
assert_bool(req.is_pending()).override_failure_message(
"a granularity_v2-mismatched response (District answering a Region request)"
+ " must be dropped as stale, leaving the request still pending"
).is_true()
## The matching case: request_now() asking for Region, answered by a Region
## echo at the SAME (center, n) — must be ACCEPTED and cached under the
## Region key, retrievable on a follow-up request without a new network round
## trip.
##
## **Live-round correction:** the mock's legacy `granularity` field is now
## Dudley's ACTUAL wire sentinel (`WINDOW_GRANULARITY_REGION_KEY` =
## `u32::MAX` = 4294967295), not a convenient `1` — the original version of
## this test used `1`, which coincidentally matched the request's own
## pinned `_granularity` and therefore never exercised the real mismatch a
## live server actually produces. See _echoed_granularity_matches()'s own
## doc (atlas_window_request.gd) for why this is load-bearing: without the
## v2-authoritative-when-present fix, THIS test would have failed with the
## real sentinel — it only passed before because the mock was wrong.
func test_on_response_matching_granularity_v2_region_is_accepted_and_cached() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_bool(req.is_pending()).is_true()
var region_window: Dictionary = _mock_window(
Vector2i(0, 0), 6400, SERVER_LEGACY_GRANULARITY_REGION_SENTINEL, 0, "Region"
)
req.on_response(_mock_response("GJ380c", region_window))
assert_bool(req.is_pending()).override_failure_message(
"a response carrying the REAL legacy sentinel (u32::MAX) in the old"
+ " granularity slot must still be accepted — v2 is authoritative"
+ " whenever present, the legacy field must not be compared at all"
).is_false()
var received: Array = []
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
assert_int(received.size()).override_failure_message(
"a second Region request at the same (center, n) must hit the cache"
).is_equal(1)
assert_bool(req.is_pending()).is_false()
## **The direct precedence-rule proof (live-round finding #2, the sharpest
## case):** a response whose `granularity_v2` MATCHES the request but whose
## LEGACY `granularity` field could never possibly match (the Region
## sentinel) must still be ACCEPTED — proving the legacy comparison is
## SKIPPED entirely when v2 is present, not merely "also checked and
## happens to pass." This is the literal shape of the live bug: real server
## responses ALWAYS carry the Region sentinel in the legacy slot, so any
## code path that still consults the legacy field when v2 is already
## authoritative would drop every single one of these, forever.
func test_on_response_v2_match_is_accepted_regardless_of_legacy_field_value() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(0, 0), 6400, AtlasWindowRequest.GRANULARITY_V2_REGION)
var region_window: Dictionary = _mock_window(
Vector2i(0, 0), 6400, SERVER_LEGACY_GRANULARITY_REGION_SENTINEL, 0, "Region"
)
req.on_response(_mock_response("GJ380c", region_window))
assert_bool(req.is_pending()).override_failure_message(
"v2 match must be sufficient on its own — the legacy sentinel value must"
+ " never be consulted once granularity_v2 is present on the response"
).is_false()
# =============================================================================
# (b) old-server-shape response (no granularity/min_wl_m keys) -> defaults
# =============================================================================
## A response from a hypothetical pre-T-1150 server (or any response whose
## district_window dict simply omits the new keys) must decode granularity
## as district (1) and min_wl_m as 0 via the same defaulting on_response()
## already applies — and since request_now()'s own defaults are identical,
## the response is ACCEPTED, not treated as stale just because two keys are
## missing.
func test_on_response_missing_granularity_and_min_wl_defaults_and_is_accepted() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(3, 3), 2)
assert_bool(req.is_pending()).is_true()
# Old-shape window: no "granularity"/"min_wl_m" keys at all.
var old_shape_window := {
"center": [3, 3],
"n": 2,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
req.on_response(_mock_response("GJ380c", old_shape_window))
assert_bool(req.is_pending()).override_failure_message(
(
"an old-server-shape response (missing granularity/min_wl_m) must "
+ "default to district/0 and be ACCEPTED, not dropped as stale"
)
).is_false()
var received: Array = []
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
# Re-request the same (body, center, n) — must now be a cache hit, proving
# on_response() actually stored the old-shape window under the
# district/0 key, not silently discarding it.
req.request_now("GJ380c", Vector2i(3, 3), 2)
assert_int(received.size()).is_equal(1)
assert_bool(req.is_pending()).is_false()
## **Live-round sibling test (instruction #2's "old-server path stays
## covered"):** a response that carries the LEGACY `granularity` key WITH AN
## EXPLICIT VALUE (1, i.e. genuinely present, not merely defaulted via
## absence — the case test_on_response_missing_granularity_and_min_wl_defaults_and_is_accepted
## above doesn't exercise, since it omits the key entirely) but has NO
## `granularity_v2` key at all — the true "hypothetically old, pre-T-1152
## server" shape — must still be accepted for a plain District request via
## the legacy-comparison FALLBACK branch in `_echoed_granularity_matches()`.
## This is the other half of the v2-authoritative-when-present precedence
## rule: v2 present -> v2 alone decides; v2 ABSENT -> legacy alone decides
## (never both, never neither).
func test_on_response_legacy_only_no_v2_key_still_accepted_for_district() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(4, 4), 2) # defaults to District granularity
assert_bool(req.is_pending()).is_true()
# Legacy-only shape: "granularity" IS present (district=1), "granularity_v2"
# key is absent entirely — not present-with-a-District-value, ABSENT.
var legacy_only_window := {
"center": [4, 4],
"n": 2,
"granularity": AtlasWindowRequest.DEFAULT_GRANULARITY,
"min_wl_m": 0,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
assert_bool(legacy_only_window.has("granularity_v2")).override_failure_message(
"sanity: this fixture must NOT carry granularity_v2 at all — that's the point"
).is_false()
req.on_response(_mock_response("GJ380c", legacy_only_window))
assert_bool(req.is_pending()).override_failure_message(
"a legacy-only response (granularity=1 present, granularity_v2 absent) must"
+ " still be accepted for a District request via the legacy-fallback branch"
).is_false()
# =============================================================================
# (c) n-clamp mirror (Tyre C1) — quarter n=32 stores clamped n=16
# =============================================================================
## **Item (c) as literally scoped by the ticket** ("the clamp-mirror from
## item 1"): `_clamp_window_n_mirror()` reproduces the server's
## `clamp_window_n(raw_n, granularity)` bit-for-bit, INCLUDING the quarter
## n=32 -> 16 case — pinned directly against the static helper, independent
## of the request/response plumbing (`request_now()` has no public
## "request quarter" entry point today; T-1150 is struct/key plumbing only,
## requesting quarter is T-1153's job — see the class-level docstring on
## `_clamp_window_n_mirror()` for why calling `request_now()` at district
## granularity can never itself exercise the quarter branch: it unconditionally
## resets `_granularity` to district BEFORE clamping, by design, since no
## caller can ask for quarter yet).
func test_clamp_window_n_mirror_matches_server_formula_at_quarter_n32() -> void:
assert_int(AtlasWindowRequest._clamp_window_n_mirror(32, 4)).is_equal(16)
# District granularity: the per-axis cap (64) governs, matching the
# server's clamp_window_n_district_granularity_uses_per_axis_cap test.
assert_int(AtlasWindowRequest._clamp_window_n_mirror(640, 1)).is_equal(64)
# Small n well under budget at quarter granularity stays unclamped,
# matching clamp_window_n_quarter_granularity_leaves_small_n_unclamped.
assert_int(AtlasWindowRequest._clamp_window_n_mirror(8, 4)).is_equal(8)
## **Item (c), the request/response half:** `request_now()` actually WIRES
## the mirror in (not just defines it) — a request for a district-legal but
## per-axis-oversized `n` (e.g. 640, mirroring the server's own
## `DISTRICT_WINDOW_MAX_N*10` oversized-request test) stores the CLAMPED
## `_n=64`, so a server response echoing the server's OWN clamped n=64 is
## ACCEPTED, not rejected as stale for "not matching" the raw 640 that was
## asked for. This is the exact n-clamp/echo/staleness triangle Tyre C1
## flagged, exercised through the reachable (district) path today; the
## quarter-specific n=32->16 number is pinned by the formula test above since
## no public API can drive quarter through `request_now()` yet.
func test_oversized_n_request_stores_clamped_n_and_accepts_matching_echo() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(4, 4), 640)
assert_int(req._n).override_failure_message(
(
"request_now() must mirror the server's clamp_window_n(640, granularity=1) "
+ "== 64 BEFORE storing _n, not store the raw requested 640"
)
).is_equal(64)
assert_bool(req.is_pending()).is_true()
# The server's real response for this request echoes n=64 (its own
# clamp_window_n() result) — must be ACCEPTED, not stale.
var clamped_echo: Dictionary = _mock_window(Vector2i(4, 4), 64, 1, 0)
req.on_response(_mock_response("GJ380c", clamped_echo))
assert_bool(req.is_pending()).override_failure_message(
(
"a response echoing the CLAMPED n=64 must be accepted, since _n was "
+ "already clamped to 64 before the request fired"
)
).is_false()
# =============================================================================
# (d) Region clamp mirror (T-1152/T-1153) — mirrors
# server/src/atlas/layer_proxy.rs's clamp_window_n_v2 EXACTLY, including the
# Region branch's bounded halving loop.
#
# PR #192 review (Dudley, server-side analysis): the halving loop is
# PROVABLY UNREACHABLE at current constants — the per-axis clamp to
# SERVER_DISTRICT_WINDOW_MAX_N_REGION (6,400) forecloses it. Brute-forced,
# the max cell_grid_side over ALL reachable (post-per-axis-clamp) n is
# exactly 64 — the wire-cap boundary itself, never over it — so the loop's
# `>` guard is never true for any input. Ruling: the loop STAYS as
# defensive code (a future constant change could make it reachable again),
# but the test suite must not claim it "fires" when it provably doesn't.
# See server/src/atlas/layer_proxy.rs's
# clamp_window_n_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs
# for the server-side property-sweep pin this client-side suite mirrors.
# =============================================================================
## District/Quarter through the v2 mirror must be BYTE-IDENTICAL to the
## legacy mirror — the server's own
## `clamp_window_n_v2_delegates_to_legacy_for_district_and_quarter`
## guarantee, restated client-side.
func test_clamp_window_n_mirror_v2_matches_legacy_for_district_and_quarter() -> void:
assert_int(
AtlasWindowRequest._clamp_window_n_mirror_v2(32, AtlasWindowRequest.GRANULARITY_V2_QUARTER)
).is_equal(AtlasWindowRequest._clamp_window_n_mirror(32, 4))
assert_int(
AtlasWindowRequest._clamp_window_n_mirror_v2(640, AtlasWindowRequest.GRANULARITY_V2_DISTRICT)
).is_equal(AtlasWindowRequest._clamp_window_n_mirror(640, 1))
## The clean Region boundary case: n=6,400 (DISTRICT_WINDOW_MAX_N_REGION,
## the per-axis cap exactly) derives cell_grid_side(6400) = round(6400/100) =
## 64, and 64² = 4,096 = WIRE_CAP_CELLS EXACTLY — the halving loop's `>`
## condition is false at the boundary, so this must clamp to EXACTLY 6,400,
## not halve further. This is the server's own
## `clamp_window_n_v2_region_exact_boundary_n6400_uncontested` guarantee,
## restated client-side (WIRE_CAP_CELLS_SQRT * DISTRICTS_PER_REGION is
## DERIVED to land here exactly, per that constant's own doc).
func test_clamp_window_n_mirror_v2_region_boundary_is_exact() -> void:
var n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(n).is_equal(AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
## Region's per-axis cap: a raw `n` far over DISTRICT_WINDOW_MAX_N_REGION
## (mirroring the server's own `region_request_oversized_n_clamps_and_echoes_clamped_n`
## test's `DISTRICT_WINDOW_MAX_N_REGION * 10` shape) must clamp DOWN — never
## trust the wire — and the result must satisfy BOTH invariants the server's
## own test asserts: `n <= DISTRICT_WINDOW_MAX_N_REGION` AND
## `cell_grid_side(n)^2 <= WIRE_CAP_CELLS`.
func test_clamp_window_n_mirror_v2_region_oversized_n_clamps_within_both_bounds() -> void:
var oversized: int = AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION * 10
var n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
oversized, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(n).override_failure_message(
"echoed n must be clamped to DISTRICT_WINDOW_MAX_N_REGION, not the raw oversized value"
).is_less_equal(AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
var side: int = AtlasWindowRequest._cell_grid_side_region_mirror(n)
assert_int(side * side).override_failure_message(
"clamped cell count must never exceed WIRE_CAP_CELLS at Region granularity either"
).is_less_equal(AtlasWindowRequest.SERVER_WIRE_CAP_CELLS)
## PR #192 review (Dudley's unreachability finding, applied client-side): the
## halving loop's `>` guard is PROVABLY never true at current constants — the
## per-axis clamp to SERVER_DISTRICT_WINDOW_MAX_N_REGION (6,400) happens
## FIRST and unconditionally, and cell_grid_side(6400) = 64 lands EXACTLY on
## the wire-cap boundary (64² = WIRE_CAP_CELLS), never over it. A prior
## version of this test claimed n=6,450 "exercises" the loop firing — it does
## not: 6,450 clamps to 6,400 before the loop ever runs, so the test was
## passing on the per-axis clamp alone, not on anything the loop itself did
## (the same mock-diverges-from-reality class of bug hunted in review round
## 2). Reframed as a property sweep, mirroring the server's own
## `clamp_window_n_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs`
## (Dudley): for every raw n across the legal range (including values far
## past the per-axis cap), (i) the per-axis-clamped n never gets modified any
## further by the loop — pre-loop n and post-clamp n are byte-identical —
## and (ii) the wire-cap invariant holds regardless. The loop itself stays as
## defensive code (a future constant change could make it reachable again);
## this test documents that it is a no-op today rather than asserting a
## behavior that never actually happens.
func test_clamp_window_n_mirror_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs() -> void:
var sample_raw_ns: Array = [
1, 100, 6399, 6400, 6401, 6450, 6500,
AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION * 10,
]
for raw_n: int in sample_raw_ns:
var pre_loop_n: int = clampi(raw_n, 1, AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION)
var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(
raw_n, AtlasWindowRequest.GRANULARITY_V2_REGION
)
assert_int(clamped_n).override_failure_message(
(
"the per-axis clamp alone must already satisfy the wire cap for"
+ " raw_n=%d — the halving loop is provably unreachable at current"
+ " constants (max cell_grid_side over all reachable n is exactly"
+ " 64, the wire-cap boundary itself), so it must never further"
+ " modify what the per-axis clamp already produced"
) % raw_n
).is_equal(pre_loop_n)
var side: int = AtlasWindowRequest._cell_grid_side_region_mirror(clamped_n)
assert_int(side * side).override_failure_message(
"the wire-cap invariant must hold for raw_n=%d regardless" % raw_n
).is_less_equal(AtlasWindowRequest.SERVER_WIRE_CAP_CELLS)
## n smaller than one region (n < 100) must clamp its cell-grid side to a
## minimum of 1 — cell_grid_side_for_window()'s own `.max(1)` — never a
## degenerate 0x0 grid, matching WindowGranularity::cell_grid_side's own
## documented minimum.
func test_cell_grid_side_region_mirror_minimum_is_one() -> void:
assert_int(AtlasWindowRequest._cell_grid_side_region_mirror(1)).is_equal(1)
assert_int(AtlasWindowRequest._cell_grid_side_region_mirror(50)).is_equal(1)
# =============================================================================
# PR #192 cold-start round 3 — the single-window half of the launch-shape
# gap: the SAME status-gate bug the tile fan-out has (test_atlas_window_tile_set.gd's
# own regressions) applies equally here, since on_response() is the shared
# class both paths use. A first descent onto a cold body with NO tiles
# (District/Quarter rung, or a small Region body) hits the identical
# whole-body-cache-miss -> status:"Pending" wire shape.
# =============================================================================
## The exact bug, single-window shape: a whole-response Pending on a cold
## first request must increment the retry counter and schedule a re-poll —
## not be silently dropped by the OLD `status != "Ready" -> return` gate.
func test_cold_request_whole_response_pending_increments_retry_count() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).is_true()
req.on_response(_pending_response("GJ380c"))
assert_bool(req.is_pending()).override_failure_message(
"a whole-response Pending must leave the request still pending, not"
+ " silently give up"
).is_true()
assert_int(req._retries).override_failure_message(
"a whole-response Pending must increment the retry counter — the"
+ " exact bug: the OLD status-gate dropped this before ever reaching"
+ " the retry-scheduling code, leaving retries at 0 forever"
).is_equal(1)
## Full convergence: a whole-response Pending, then a real Ready for the
## RE-REQUEST, must be accepted — proving the retry loop's own re-request
## actually gets picked up, not just that the counter increments. The
## mid-test retries==1 assertion is what makes this genuinely load-bearing:
## without it, a Ready delivered ANY time after a Pending (retried or not)
## trivially passes this test's final assertion, since accepting a fresh
## Ready response was never the broken behavior — only the retry itself was.
func test_cold_request_converges_after_pending_then_real_ready() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(2, 2), 2)
req.on_response(_pending_response("GJ380c"))
assert_bool(req.is_pending()).is_true()
assert_int(req._retries).override_failure_message(
"sanity: the retry must have actually been scheduled before this test"
+ " waits for it to fire — otherwise the final assertion below would"
+ " pass even if the retry never happened at all"
).is_equal(1)
await get_tree().create_timer(0.6).timeout # past the first retry delay
var window: Dictionary = _mock_window(Vector2i(2, 2), 2)
req.on_response(_mock_response("GJ380c", window))
assert_bool(req.is_pending()).override_failure_message(
"a real Ready response after the pending/retry cycle must be accepted"
).is_false()
## NotFound must give up immediately, not retry.
func test_cold_request_not_found_gives_up_immediately_without_retry() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(2, 2), 2)
req.on_response(_not_found_response("GJ380c"))
assert_bool(req.is_pending()).override_failure_message(
"NotFound must give up immediately, not stay pending waiting for a retry"
).is_false()
assert_int(req._retries).is_equal(0)
## Error must give up immediately too.
func test_cold_request_error_gives_up_immediately_without_retry() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(2, 2), 2)
req.on_response(_error_response("GJ380c"))
assert_bool(req.is_pending()).override_failure_message(
"Error must give up immediately, not stay pending waiting for a retry"
).is_false()
assert_int(req._retries).is_equal(0)
## PR #193 review (Hoshe): a whole-response Pending for a DIFFERENT body
## must not touch this request's retry state — the body_id guard runs
## BEFORE the status branch in on_response(), so someone else's cold-body
## pending can never burn one of OUR 30 retries (or reschedule our timer).
## Structurally guaranteed by guard ordering today; this test pins the
## ordering, because a refactor that moves the status branch first would
## silently cross-wire every concurrent cold descent (multi-body Atlas
## browsing, or the orbital tile fan-out where all requests share one
## broadcast signal). The final Ready-for-OUR-body assertion proves the
## request is genuinely unaffected, not just un-retried.
func test_pending_for_a_different_body_does_not_touch_retry_state() -> void:
var req = _make_request()
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).is_true()
req.on_response(_pending_response("OtherBody"))
assert_int(req._retries).override_failure_message(
"a Pending for a DIFFERENT body must not increment OUR retry counter"
+ " — the body_id guard must run before the status branch"
).is_equal(0)
assert_bool(req.is_pending()).override_failure_message(
"a wrong-body Pending must leave the request still pending its own"
+ " response, neither given up nor retried"
).is_true()
var window: Dictionary = _mock_window(Vector2i(2, 2), 2)
req.on_response(_mock_response("GJ380c", window))
assert_bool(req.is_pending()).override_failure_message(
"after ignoring a wrong-body Pending, our OWN Ready must still be"
+ " accepted normally — the request state must be genuinely untouched"
).is_false()
assert_int(req._retries).is_equal(0)
# =============================================================================
# _retry_delay_for() — deterministic exponential backoff + per-tile stagger
# (PR #192 cold-start round 3 hardening: 6 tiles retrying in perfect
# lockstep on a whole-response Pending is a real request-pulse risk even
# though it isn't what caused the starvation bug above).
# =============================================================================
func test_retry_delay_for_first_retry_is_the_initial_delay() -> void:
assert_float(AtlasWindowRequest._retry_delay_for(1, 0)).is_equal_approx(
AtlasWindowRequest.INITIAL_RETRY_DELAY, 0.0001
)
func test_retry_delay_for_doubles_each_retry_until_the_cap() -> void:
assert_float(AtlasWindowRequest._retry_delay_for(1, 0)).is_equal_approx(0.5, 0.0001)
assert_float(AtlasWindowRequest._retry_delay_for(2, 0)).is_equal_approx(1.0, 0.0001)
assert_float(AtlasWindowRequest._retry_delay_for(3, 0)).is_equal_approx(2.0, 0.0001)
assert_float(AtlasWindowRequest._retry_delay_for(4, 0)).is_equal_approx(4.0, 0.0001)
# Retry 5 would double past MAX_RETRY_DELAY (8.0) — must clamp, not keep growing.
assert_float(AtlasWindowRequest._retry_delay_for(5, 0)).is_equal_approx(
AtlasWindowRequest.MAX_RETRY_DELAY, 0.0001
)
assert_float(AtlasWindowRequest._retry_delay_for(20, 0)).is_equal_approx(
AtlasWindowRequest.MAX_RETRY_DELAY, 0.0001
)
## The deterministic stagger: tile index i's delay is offset by
## STAGGER_STEP*i on top of the same backoff schedule — directly assertable,
## not a randomized jitter a test would have to tolerance-check.
func test_retry_delay_for_staggers_deterministically_by_tile_index() -> void:
var base: float = AtlasWindowRequest._retry_delay_for(1, 0)
for i in range(6):
var expected: float = base + AtlasWindowRequest.STAGGER_STEP * float(i)
assert_float(AtlasWindowRequest._retry_delay_for(1, i)).override_failure_message(
"tile index %d's first-retry delay must be exactly base + STAGGER_STEP*%d" % [i, i]
).is_equal_approx(expected, 0.0001)
## Six tiles that all went pending in the same frame must NOT all retry at
## the exact same instant — the anti-storm property this hardening exists
## for, pinned directly: every tile's delay for the SAME retry count must be
## strictly increasing with its stagger index.
func test_retry_delay_for_six_tiles_never_collide_on_the_same_retry() -> void:
var delays: Array = []
for i in range(6):
delays.append(AtlasWindowRequest._retry_delay_for(1, i))
for i in range(1, delays.size()):
assert_float(delays[i]).override_failure_message(
"tile %d's delay must be strictly greater than tile %d's — a storm"
+ " pulse means two tiles retrying at the same instant" % [i, i - 1]
).is_greater(delays[i - 1])
-434
View File
@@ -1,434 +0,0 @@
## 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}
## PR #192 cold-start round 3: the WIRE-ACCURATE shape of a cold body's
## first-ever response — server/src/atlas/layer_proxy.rs's
## get_or_generate()/serve_district_window() on a whole-body cache MISS
## builds `AtlasLayerResponse { status: Pending, district_window: None, ... }`
## (confirmed directly against that source). `body_id` is the ONLY
## identifying field — no center/n/granularity anywhere, matching the real
## wire's total lack of per-request attribution on this specific shape.
static func _pending_response(body_id: String) -> Dictionary:
return {"body_id": body_id, "status": "Pending", "district_window": null}
static func _not_found_response(body_id: String) -> Dictionary:
return {"body_id": body_id, "status": "NotFound", "district_window": null}
## Matches atlas_map_protocol.gd's `_decode_status_field()` — the decoded
## `AtlasLayerStatus::Error(String)` variant's status STRING is always just
## "Error" (the message rides in a separate `error` field, not appended to
## the status string), confirmed against that decoder directly.
static func _error_response(body_id: String, message: String = "boom") -> Dictionary:
return {"body_id": body_id, "status": "Error", "error": message, "district_window": null}
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()
## PR #192 cold-start round 3 hardening: each tile's own AtlasWindowRequest
## must get a DISTINCT, index-matching `_stagger_index` — the anti-storm
## property (deterministic retry-delay stagger, see
## AtlasWindowRequest._retry_delay_for()'s own doc) depends entirely on this
## wiring; without it every tile silently staggers at index 0 and retries
## in lockstep again, exactly the storm risk this hardening exists to close.
func test_enter_wires_a_distinct_stagger_index_per_tile() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
for i in range(ts._tiles.size()):
var req = ts._tiles[i]["request"]
assert_int(req._stagger_index).override_failure_message(
"tile %d's request must be wired with _stagger_index=%d, matching"
+ " its own position in the tile set" % [i, i]
).is_equal(i)
## 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()
# =============================================================================
# PR #192 cold-start round 3 — THE launch-shape regression: a cold body's
# FIRST-EVER response is a wire-accurate whole-response Pending (status
# "Pending", district_window null, body_id only — no center/n/granularity
# anywhere, confirmed against server/src/atlas/layer_proxy.rs directly).
# Driven through the REAL fan-out (SimBridge.atlas_layers_received.emit(),
# reaching every tile via tile_set._on_atlas_layers_received), not a direct
# tile.on_response() call — the coordinator's own regression-discipline ask,
# since a direct per-tile call is implicit attribution a real wire fan-out
# doesn't have. Before the status-gate fix, on_response()'s FIRST line
# (`status != "Ready" -> return`) dropped this response for every tile
# before ever reaching the retry-scheduling code — retries stayed at 0
# forever, matching the coordinator's own live cold-server capture exactly.
# =============================================================================
## The exact bug: a whole-response Pending on cold entry must increment
## every tile's retry counter and schedule a re-poll — not be silently
## dropped. Fails hard against the pre-fix code (retries stay 0 forever).
func test_cold_entry_whole_response_pending_increments_every_tiles_retry_count() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
SimBridge.atlas_layers_received.emit(_pending_response("GJ380c"))
for tile_dict: Dictionary in ts._tiles:
var req = tile_dict["request"]
assert_bool(req.is_pending()).override_failure_message(
"every tile must still be pending after a whole-response Pending"
).is_true()
assert_int(req._retries).override_failure_message(
"a whole-response Pending must increment the retry counter — the"
+ " exact bug: the OLD status-gate silently dropped this before"
+ " ever reaching the retry-scheduling code, leaving retries at 0 forever"
).is_equal(1)
## The full convergence: repeated whole-response Pendings (simulating a slow
## cold AnalyzeBody), THEN real per-tile Ready responses for the tiles' own
## RE-REQUESTS — every tile must eventually fill. No re-request means this
## hangs (waiting past the retry delay for a re-poll that never happens) or
## fails (has_pending_tiles() never flips false) — exactly the launch-shape
## gap the coordinator named: "every unit test delivered Ready immediately."
func test_cold_entry_converges_after_repeated_pending_then_real_readies() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var tiles: Array = ts.get_tiles()
# Two consecutive whole-response Pendings — a slow cold derive, not a
# single flip. Real waits so the scheduled retry timers can actually fire.
SimBridge.atlas_layers_received.emit(_pending_response("GJ380c"))
await get_tree().create_timer(0.7).timeout # past the first backoff delay
SimBridge.atlas_layers_received.emit(_pending_response("GJ380c"))
await get_tree().create_timer(0.8).timeout # past the second (staggered) delay
assert_bool(ts.has_pending_tiles()).override_failure_message(
"sanity: still pending after 2 cold cycles"
).is_true()
for tile_dict: Dictionary in ts._tiles:
var req = tile_dict["request"]
assert_int(req._retries).override_failure_message(
"sanity: each tile's retry counter must have actually incremented"
+ " twice — otherwise the final assertion below would pass even if"
+ " the retries never happened at all (a fresh Ready was never the"
+ " broken behavior, only the retry itself was)"
).is_equal(2)
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.has_pending_tiles()).override_failure_message(
"after the retries fire and real Readies land, every tile must have adopted its window"
).is_false()
assert_bool(ts.is_fully_arrived()).is_true()
## NotFound must give up IMMEDIATELY, not retry — a body that doesn't exist
## will never resolve by waiting (the coordinator's own "give up on error
## responses, not elapsed patience" framing — this replaces the old
## elapsed-retries-only give-up policy with a correctness-based one for the
## cases where retrying is provably pointless).
func test_cold_entry_not_found_gives_up_immediately_without_retry() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
SimBridge.atlas_layers_received.emit(_not_found_response("GJ380c"))
for tile_dict: Dictionary in ts._tiles:
var req = tile_dict["request"]
assert_bool(req.is_pending()).override_failure_message(
"NotFound must give up immediately, not stay pending waiting for a retry"
).is_false()
assert_int(req._retries).override_failure_message(
"NotFound must never increment the retry counter — retrying a"
+ " nonexistent body is provably pointless"
).is_equal(0)
## Error must give up immediately too, same reasoning as NotFound — a
## resolve/IO failure won't resolve itself by polling.
func test_cold_entry_error_gives_up_immediately_without_retry() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
SimBridge.atlas_layers_received.emit(_error_response("GJ380c"))
for tile_dict: Dictionary in ts._tiles:
var req = tile_dict["request"]
assert_bool(req.is_pending()).override_failure_message(
"Error must give up immediately, not stay pending waiting for a retry"
).is_false()
assert_int(req._retries).is_equal(0)
# =============================================================================
# has_pending_tiles() / has_any_tile_arrived() — PR #192 cold-start dossier.
# Distinct predicates (both can be true at once, mid-arrival): the viewer's
# self-healing redraw (BUG 1) polls has_pending_tiles(); the "DERIVING
# TERRAIN…" label (BUG 3) polls has_any_tile_arrived() to know when to drop.
# =============================================================================
func test_has_pending_tiles_true_immediately_after_enter() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
assert_bool(ts.has_pending_tiles()).override_failure_message(
"every tile is unarrived right after enter() — has_pending_tiles() must be true"
).is_true()
func test_has_pending_tiles_false_once_every_tile_has_arrived() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
for tile: Dictionary in ts.get_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.has_pending_tiles()).is_false()
func test_has_pending_tiles_true_while_only_some_tiles_have_arrived() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var first_tile: Dictionary = ts.get_tiles()[0]
var window: Dictionary = _mock_window(
first_tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(ts.has_pending_tiles()).override_failure_message(
"5 of 6 tiles still unarrived — has_pending_tiles() must stay true"
).is_true()
func test_has_any_tile_arrived_false_immediately_after_enter() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
assert_bool(ts.has_any_tile_arrived()).override_failure_message(
"nothing has arrived right after enter() — has_any_tile_arrived() must be false"
).is_false()
## The exact mid-arrival case both predicates must agree can coexist: one
## tile in, five still pending — the point the "DERIVING TERRAIN…" label
## must drop (has_any_tile_arrived() flips true) while the self-heal must
## keep redrawing (has_pending_tiles() stays true).
func test_has_any_tile_arrived_true_after_a_single_tile_lands() -> void:
var ts = _make_tile_set()
ts.enter("GJ380c", 6238.4)
var first_tile: Dictionary = ts.get_tiles()[0]
var window: Dictionary = _mock_window(
first_tile["center"], AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION
)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_bool(ts.has_any_tile_arrived()).is_true()
assert_bool(ts.has_pending_tiles()).override_failure_message(
"sanity: the other 5 tiles are still pending at the same moment"
).is_true()
func test_has_any_tile_arrived_false_for_an_empty_tile_set() -> void:
var ts = _make_tile_set()
assert_bool(ts.has_any_tile_arrived()).override_failure_message(
"an empty tile set (never entered) must not vacuously report arrival"
).is_false()
## 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)
-917
View File
@@ -1,917 +0,0 @@
## T-1138 (D-226 T-1124 amendment §1-§5): tests for AtlasWindowViewer + its
## companion request/cache orchestration (atlas_window_request.gd) — pure
## logic against hand-built AtlasLayerResponse-shaped dicts, matching the
## ticket's "unit tests against hand-built response dicts" instruction. Live
## end-to-end verification against a real spawned server is separate
## (companion-run evidence, not gdUnit — this file never touches SimBridge's
## live-mode path, only the response-handling/cache/overlay logic that path
## eventually feeds).
class_name TestAtlasWindowViewer
extends GdUnitTestSuite
# atlas_window_request.gd has no class_name (review #8 precedent throughout
# this cluster) — preloaded once here, not re-load()ed per test (gdlint
# duplicated-load).
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
# T-1142: district_extent()/canonicalize_district_center() — used to derive
# real (cols/rows_half) bounds for the wrap/pole-wall tests below.
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
# T-1156 wave 1 round 3: AtlasWindowNatureOverlay has no class_name (matching
# atlas_overlay_bar.gd/atlas_window_request.gd's own no-class_name precedent,
# review #8) — subclassing it (the _CountingNatureOverlay spy below) needs
# the preloaded script's PATH via `extends`, not a global class name.
const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd")
## Build a hand-authored DistrictWindowLayer dict (n=2, matching the shape
## district_grid/region_grid fixtures already use elsewhere in this suite).
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
# =============================================================================
# AtlasWindowViewer — entry + overlay defs
# =============================================================================
func test_enter_with_no_response_leaves_window_null_and_pending() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20))
assert_that(v.get_district_window()).is_null()
## Feeding a matching Ready response (via the SAME SimBridge.atlas_layers_received
## routing path the viewer subscribes to in _ready()) must populate the window.
func test_enter_then_matching_response_populates_window() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v.get_district_window()).is_equal(window)
## A response for a DIFFERENT body must not populate the window — the
## body_id scoping AtlasWindowRequest.on_response() checks.
func test_response_for_different_body_is_ignored() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ_wrong_body", window))
assert_that(v.get_district_window()).is_null()
## A response whose echoed (center, n) does NOT match what was last asked for
## is stale — §2's race-condition guard. Simulates a superseded-by-a-later-pan
## response arriving after the fact.
func test_response_with_mismatched_echo_is_discarded_as_stale() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var stale_window: Dictionary = _mock_window(Vector2i(99, 99), 2) # wrong center
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", stale_window))
assert_that(v.get_district_window()).is_null()
## §1: an as-yet-underived window rides as `district_window: None` inside a
## Ready response — this is NOT an error, the viewer just keeps waiting
## (get_district_window() stays null, no crash, no window content shown).
func test_ready_response_with_null_district_window_keeps_waiting() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", null))
assert_that(v.get_district_window()).is_null()
func test_overlay_defs_include_the_three_toggle_ids() -> void:
var ids: Array = []
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).contains(["gen_dw_temp", "gen_dw_moisture", "gen_dw_veg"])
## Glaciation is explicitly NOT a toggle id (§5: "an always-on modifier, not
## a toggle") — a regression here would silently re-introduce it as a switch.
func test_overlay_defs_do_not_include_glaciation() -> void:
var ids: Array = []
for d: Dictionary in AtlasWindowViewer.OVERLAY_DEFS:
ids.append(d["id"])
assert_that(ids).not_contains(["gen_dw_glaciation", "gen_dw_ice"])
func test_set_overlay_visible_toggles_and_is_overlay_visible_reflects_it() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_false()
v.set_overlay_visible("gen_dw_temp", true)
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_true()
func test_set_overlay_visible_unknown_id_is_a_noop() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_overlay_visible("not_a_real_overlay", true)
assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false()
## PR #195 review (Hoshe): named default-visibility pins for the T-1156
## nature overlays at fresh-viewer construction, per Araminta's ruling —
## RVR ON (rivers are load-bearing geography, a first-time Atlas viewer sees
## them without hunting for a toggle: the single most player-visible behavior
## the feature ships), BAS and ATR OFF (secondary/dev-facing analytical
## layers, opt-in). Without these, a refactor of the _ready() visibility-init
## loop could silently flip the defaults and only a live capture would
## notice — gen_basins was previously covered only incidentally by the
## toggle-redraw spy test above.
func test_nature_overlay_defaults_rivers_on_basins_and_attractors_off() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
assert_bool(v.is_overlay_visible("gen_rivers")).override_failure_message(
"gen_rivers must default ON (Araminta's RVR-on ruling, T-1156 wave 1)"
).is_true()
assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message(
"gen_basins must default OFF (opt-in analytical layer)"
).is_false()
assert_bool(v.is_overlay_visible("gen_attractors")).override_failure_message(
"gen_attractors must default OFF (dev-facing detail, opt-in)"
).is_false()
## Counts real _draw() invocations on AtlasWindowNatureOverlay — CanvasItem
## exposes no public "is a redraw pending" query in this Godot version
## (confirmed directly: is_queued_for_redraw() does not exist on Node2D here
## — an earlier version of this test assumed it did and failed with
## "Invalid call. Nonexistent function"), so the only reliable signal that
## queue_redraw() actually had an effect is the engine calling _draw() again
## on a subsequent frame. Matches test_atlas_cold_start.gd's own
## _CountingOverlay precedent exactly (same file's own doc: "the only
## reliable signal... is the engine calling _draw() again") — subclasses the
## REAL AtlasWindowNatureOverlay so drawing still runs through genuine
## production code, this spy only adds counting.
class _CountingNatureOverlay extends AtlasWindowNatureOverlay:
var draw_count := 0
func _draw() -> void:
draw_count += 1
super._draw()
## Coordinator live-eyeball round 3 (2026-07-23): R1/R2 captures were
## byte-identical because a scratch drive script called
## set_overlay_visible("BAS", true) — the button LABEL, not the overlay id
## ("gen_basins") — which set_overlay_visible()'s own `not
## _overlay_visibility.has(overlay_id): push_warning(...); return` guard
## silently no-ops on. Real callers (atlas_overlay_bar.gd's
## _on_toggle_changed()) always pass def["id"], never the label, so product
## code was never actually broken — but this pins the EXACT gate the
## coordinator asked to verify: toggling gen_basins via the real viewer API
## must (a) flip is_overlay_visible("gen_basins") — the nature overlay's OWN
## draw gate, read live via viewer.is_overlay_visible() at _draw() time, not
## a stale copy — AND (b) actually cause the NATURE overlay (not just the
## terrain overlay/viewer) to redraw on the next frame, proven by swapping in
## a _draw()-counting spy (matching test_atlas_cold_start.gd's
## _CountingOverlay pattern) and confirming draw_count advances past a
## settled baseline after the toggle, with no other gesture.
func test_set_overlay_visible_gen_basins_flips_gate_and_redraws_nature_overlay() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message(
"gen_basins must default to OFF (Araminta's ruling — BAS defaults off)"
).is_false()
# Swap in the counting spy (matching _CountingOverlay's own swap-after-
# construction shape) so entry's own queue_redraw() calls don't pollute
# the baseline, then let it settle before touching the toggle.
var spy := _CountingNatureOverlay.new(v)
v._nature_overlay.queue_free()
v._nature_overlay = spy
v._canvas.add_child(spy)
await get_tree().process_frame
await get_tree().process_frame
var baseline: int = spy.draw_count
assert_int(baseline).override_failure_message(
"sanity: the spy must have drawn at least once before the toggle, or"
+ " this test can't distinguish 'redrawn BY the toggle' from 'never"
+ " drawn at all'"
).is_greater(0)
v.set_overlay_visible("gen_basins", true)
await get_tree().process_frame
assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message(
"the toggle must flip the draw gate is_overlay_visible() reads live"
).is_true()
assert_int(spy.draw_count).override_failure_message(
"the toggle must queue_redraw() the NATURE overlay specifically —"
+ " queue_redraw() on the viewer/terrain overlay alone leaves the"
+ " nature node's last frame cached (a Node2D child does not redraw"
+ " because its sibling did) — draw_count must have advanced past the"
+ " baseline (%d)" % baseline
).is_greater(baseline)
## T-1170 B3 (PR #197 review, Hoshe #3): _on_window_ready() now also calls
## _nature_overlay.queue_redraw() (atlas_window_viewer.gd:507) — courses ride
## `DistrictWindowLayer.courses`, the SAME `_window` this handler adopts, so
## a window arrival that never redraws the nature overlay would leave freshly
## arrived courses invisible until an UNRELATED pan/zoom gesture happened to
## redraw it. Same spy-and-baseline shape as
## test_set_overlay_visible_gen_basins_flips_gate_and_redraws_nature_overlay()
## above — this is the exact pattern PR #196 established for "prove a
## specific queue_redraw() call site actually fires", applied to the
## window-arrival call site instead of the toggle call site.
##
## **Isolation note (live finding while writing this test):** `_on_window_ready()`
## ALSO calls `_fit_and_center()` on the first-ever arrival
## (`_awaiting_first_window and not _user_adjusted`), and `_fit_and_center()`
## itself already ends in `_apply_transform()`, which redraws the nature
## overlay through a SEPARATE, pre-existing call site. That path would mask
## a broken/removed line 507 (both call sites fire on a fresh entry's first
## arrival, so removing just one wouldn't drop draw_count below baseline).
## Setting `_user_adjusted = true` before the response arrives — the SAME
## guard a real pan/zoom gesture sets (`_maybe_refloat_window()`/`_zoom_at()`)
## — skips the fit-and-center branch, so ONLY line 507 can be the source of
## any redraw the assertion below observes. This is a real, reachable state
## (any window arrival after the player's first manual pan/zoom), not a
## test-only fiction.
func test_window_arrival_redraws_nature_overlay() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
v._user_adjusted = true # isolate line 507 from the first-arrival fit-and-center redraw
# Swap in the counting spy AFTER enter() (matching the gen_basins test's
# own "swap after construction, then let it settle" shape) so enter()'s
# own queue_redraw() calls don't pollute the baseline.
var spy := _CountingNatureOverlay.new(v)
v._nature_overlay.queue_free()
v._nature_overlay = spy
v._canvas.add_child(spy)
await get_tree().process_frame
await get_tree().process_frame
var baseline: int = spy.draw_count
assert_int(baseline).override_failure_message(
"sanity: the spy must have drawn at least once before the window"
+ " arrives, or this test can't distinguish 'redrawn BY the arrival'"
+ " from 'never drawn at all'"
).is_greater(0)
var window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
await get_tree().process_frame
assert_that(v.get_district_window()).override_failure_message(
"sanity: the response must actually have been adopted (matching echo)"
+ " or this test proves nothing about the arrival path specifically"
).is_equal(window)
assert_int(spy.draw_count).override_failure_message(
"_on_window_ready() must queue_redraw() the NATURE overlay — courses"
+ " ride the SAME _window this handler adopts, so a window arrival"
+ " that doesn't redraw the nature overlay leaves freshly arrived"
+ " courses invisible until an unrelated pan/zoom happens to redraw"
+ " it — draw_count must have advanced past the baseline (%d)" % baseline
).is_greater(baseline)
## The RUNG-SWAP arrival case (a later _on_window_ready() call for a
## DIFFERENT granularity_v2 than the one the viewer entered at — e.g. a
## wheel-zoom crossing from District into Quarter) — cheap to cover in the
## SAME test file per the review's own "if cheap" allowance. Confirms the
## redraw fires on EVERY window adoption, not just the first-ever one
## (T-1153's progressive-refinement doc is explicit that _window only ever
## gets REPLACED, never renulled, on a rung swap).
##
## **Uses `_window_request.request_now()` directly, NOT `_enter_at_rung()`**
## — a live finding while writing this test: `_enter_at_rung()` sets
## `_awaiting_first_window = true` again (it's the SAME reset path a fresh
## descent uses), which would route the swap response back through
## `_fit_and_center()`'s OWN redraw call site, masking line 507 exactly like
## the note on the test above. The REAL production rung-swap path,
## `_maybe_reselect_rung()`, never touches `_awaiting_first_window` at all —
## it only calls `_window_request.request_debounced(...)`. `request_now()`
## (the non-debounced sibling, same effect minus the timer) is called
## directly here to update `_window_request`'s own `_granularity_v2` — the
## exact field `_on_window_ready()`'s echo-matching guard reads — mirroring
## the real path's state change without needing a live debounce timer in a
## unit test.
func test_rung_swap_window_arrival_redraws_nature_overlay() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
# First arrival (District, matches enter()'s own default rung) — settles
# the viewer into a held window, exactly as a real progressive-refinement
# sequence would before a rung swap. Uses the REAL (non-spy) nature
# overlay for this leg — only the swap leg itself needs the spy.
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
await get_tree().process_frame
assert_that(v.get_district_window()).override_failure_message(
"sanity: the first (District) arrival must have been adopted before"
+ " simulating the swap"
).is_equal(district_window)
# Now swap in the spy and simulate the RUNG SWAP itself — update the
# request's echoed granularity_v2 to "Quarter" (what
# _maybe_reselect_rung() -> request_debounced() would do on a real
# wheel-zoom crossing) WITHOUT touching _awaiting_first_window, so the
# response below takes the "not first window" branch — the genuinely
# different code path from the test above.
var spy := _CountingNatureOverlay.new(v)
v._nature_overlay.queue_free()
v._nature_overlay = spy
v._canvas.add_child(spy)
v._window_request.request_now(
"GJ380c", Vector2i(10, 20), 2, AtlasWindowRequest.GRANULARITY_V2_QUARTER
)
await get_tree().process_frame
await get_tree().process_frame
var baseline: int = spy.draw_count
assert_int(baseline).override_failure_message(
"sanity: the spy must have drawn at least once before the rung-swap"
+ " response arrives"
).is_greater(0)
var quarter_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
quarter_window["granularity_v2"] = "Quarter"
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", quarter_window))
await get_tree().process_frame
assert_that(v.get_district_window()).override_failure_message(
"sanity: the rung-swap response must actually have been adopted"
).is_equal(quarter_window)
assert_int(spy.draw_count).override_failure_message(
"a RUNG-SWAP window arrival (a later _on_window_ready() call at a"
+ " DIFFERENT granularity_v2 than entry) must ALSO redraw the nature"
+ " overlay — draw_count must have advanced past the post-first-"
+ " arrival baseline (%d)" % baseline
).is_greater(baseline)
# =============================================================================
# T-1120 capture-API parity (the ticket's explicit note: must survive here too)
# =============================================================================
func test_set_view_and_getters_round_trip() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(2.5, Vector2(30.0, -10.0))
assert_that(v.get_view_zoom()).is_equal_approx(2.5, 0.001)
assert_that(v.get_view_offset()).is_equal(Vector2(30.0, -10.0))
## T-1153: MIN_ZOOM widened to 0.0005 (from the pre-ladder 0.5) so a
## gas-giant-scale body's enter_orbital() fit zoom is never itself clamped —
## see MIN_ZOOM's own doc. Values here are chosen well outside the new wide
## range on both ends, not the old range's boundary values.
func test_set_view_clamps_to_min_max_zoom() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.set_view(0.0000001, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MIN_ZOOM, 0.0001)
v.set_view(1000.0, Vector2.ZERO)
assert_that(v.get_view_zoom()).is_equal_approx(AtlasWindowViewer.MAX_ZOOM, 0.001)
# =============================================================================
# AtlasWindowRequest — cache reuse (§4's Esc-then-re-enter / pan-back hit)
# =============================================================================
func test_window_request_cache_hit_emits_synchronously_no_pending() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
# Prime the cache directly (bypassing the network path) — the ticket's
# own instruction: unit test against hand-built response dicts.
req.get_cache().put("GJ380c", Vector2i(1, 1), 2, _mock_window(Vector2i(1, 1), 2))
var received: Array = []
req.window_ready.connect(func(w: Dictionary) -> void: received.append(w))
req.request_now("GJ380c", Vector2i(1, 1), 2)
assert_int(received.size()).is_equal(1)
assert_bool(req.is_pending()).override_failure_message(
"a cache hit must never leave the request pending"
).is_false()
func test_window_request_cache_miss_leaves_pending_true() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
req.request_now("GJ380c", Vector2i(5, 5), 2)
assert_bool(req.is_pending()).is_true()
## on_response() with a matching Ready+window response resolves the pending
## request AND populates the cache — verified by a second request_now() call
## for the same (center, n) becoming a cache hit with zero additional pending.
func test_on_response_resolves_and_populates_cache_for_next_request() -> void:
var owner_stub := RefCounted.new()
var req = auto_free(AtlasWindowRequest.new(owner_stub))
add_child(req)
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).is_true()
var window: Dictionary = _mock_window(Vector2i(2, 2), 2)
req.on_response(_mock_response("GJ380c", window))
assert_bool(req.is_pending()).is_false()
# Re-request the SAME (body, center, n) — must be a cache hit, no pending.
req.request_now("GJ380c", Vector2i(2, 2), 2)
assert_bool(req.is_pending()).override_failure_message(
"a second request for an already-resolved window must hit the cache"
).is_false()
# =============================================================================
# T-1142 item 2: fit-and-center on entry (Jeroen's "postage stamp" finding)
# =============================================================================
## enter() must fit-and-center, NOT reset to the old zoom=1.0/offset=ZERO.
## With a real viewport size set on the Control, the fitted zoom for an
## n=32 default window must scale up past 1.0 (matches
## test_atlas_window_geometry.gd's own fit math, exercised here through the
## real enter() call path instead of the pure function directly).
func test_enter_fits_and_centers_instead_of_resetting_to_zoom_one() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32)
assert_float(v.get_view_zoom()).override_failure_message(
"an n=32 (512px native) composite in a 1920x1080 viewport must be fitted"
+ " (zoom > 1.0), not left at the old zoom=1.0 postage-stamp default"
).is_greater(1.0)
## After enter()'s fit, the offset must not be Vector2.ZERO (the old
## behavior) — it must be the CENTERING offset the fit produces.
func test_enter_offset_is_not_the_old_zero_default() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1920.0, 1080.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 32)
assert_that(v.get_view_offset()).override_failure_message(
"a fitted+centered composite in a 1920x1080 viewport should not sit at (0,0)"
).is_not_equal(Vector2.ZERO)
# =============================================================================
# T-1142 item 3: header carries the body's proper name (cheap half of T-1141)
# =============================================================================
func test_header_location_label_includes_body_proper_name() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(5, 5), 2)
assert_str(v._location_label()).contains("Lendel")
## No proper_name on the body dict -> falls back to body_id (matches
## AtlasViewer's own _refresh_screen_header fallback chain exactly).
func test_header_location_label_falls_back_to_body_id() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ903b"}, {}, Vector2i(5, 5), 2)
assert_str(v._location_label()).contains("GJ903b")
func test_header_location_label_still_includes_the_coordinates() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "proper_name": "Lendel"}, {}, Vector2i(42, -7), 2)
var label: String = v._location_label()
assert_str(label).contains("42")
assert_str(label).contains("-7")
# =============================================================================
# T-1145 item 2: WASD/edge-scroll pan REPLACES drag-pan entirely (Jeroen's
# input-model ruling — LMB-drag broke click semantics with future map
# objects). Testable-shape choice (per the ticket's explicit either/or):
# _apply_pan_delta(direction, delta) is the extracted, testable pan-tick —
# calling it DIRECTLY with a synthetic direction/delta is preferred over
# synthesizing InputEventKey events through _gui_input, because WASD panning
# is NOT event-routed at all (it is Input.is_key_pressed() polling inside
# _process(), see _held_pan_direction()'s own doc) — synthesizing a key EVENT
# would exercise nothing (no _gui_input branch reads WASD), and driving it
# through Godot's actual global Input singleton state (Input.action_press()
# et al) would work but couples every test to mutating engine-global state
# that must then be carefully reset, for zero additional coverage over
# calling the already-extracted pure-ish tick function directly. This
# confirms the HANDLER/tick logic itself (offset movement, pole wall, wrap,
# _user_adjusted, refetch) exactly as the old drag tests did; a live human
# drive (WASD held down, edge-scroll near a real screen edge) is the
## lead's own stated live-verification step for what a real key-repeat/mouse-
## position sequence produces end to end.
# =============================================================================
## _apply_pan_delta() must move _view_offset — the WASD-input-model
## equivalent of the old test_drag_pan_moves_view_offset.
func test_wasd_pan_moves_view_offset() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
# body_radius_km absent -> no pole wall (identity clamp), isolating the
# pan-delta math itself from item 5's clamp in this test.
var offset_before: Vector2 = v.get_view_offset()
v._apply_pan_delta(Vector2(1.0, 0.0), 0.1) # "D"/east held for one tick
assert_that(v.get_view_offset()).override_failure_message(
"a pan tick must move _view_offset away from its pre-pan value"
).is_not_equal(offset_before)
## Frame-rate independence (T-1145's explicit requirement): the SAME held
## direction over a LONGER delta must move the view FARTHER — proportionally,
## not by some fixed per-tick step. Two short ticks must (within float
## rounding) equal one long tick of the combined duration.
func test_wasd_pan_is_frame_rate_independent() -> void:
var v1: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v1)
v1.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
v1._apply_pan_delta(Vector2(1.0, 0.0), 0.02)
v1._apply_pan_delta(Vector2(1.0, 0.0), 0.02)
var v2: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v2)
v2.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
v2._apply_pan_delta(Vector2(1.0, 0.0), 0.04)
assert_vector(v1.get_view_offset()).override_failure_message(
"two 0.02s ticks must move the view the same distance as one 0.04s tick"
).is_equal_approx(v2.get_view_offset(), Vector2(0.01, 0.01))
## Diagonal input (e.g. W+D held together) must NOT pan faster than a single
## axis — _apply_pan_delta() normalizes the direction before applying speed.
func test_wasd_diagonal_pan_is_not_faster_than_single_axis() -> void:
var v_diag: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v_diag)
v_diag.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
var before_diag: Vector2 = v_diag.get_view_offset()
v_diag._apply_pan_delta(Vector2(1.0, -1.0), 0.1) # D+W (east+north) held together
var diag_distance: float = before_diag.distance_to(v_diag.get_view_offset())
var v_axis: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v_axis)
v_axis.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
var before_axis: Vector2 = v_axis.get_view_offset()
v_axis._apply_pan_delta(Vector2(1.0, 0.0), 0.1) # D (east) alone
var axis_distance: float = before_axis.distance_to(v_axis.get_view_offset())
assert_float(diag_distance).override_failure_message(
"diagonal WASD must travel the SAME distance per tick as a single axis, not faster"
).is_equal_approx(axis_distance, 0.01)
## The real scene-tree path: RegionalScreen -> AtlasWindowViewer (T-1153 —
## RegionalScreen is now the WHOLE ladder's nav entry, superseding the
## retired DistrictScreen nav hop; see atlas_app.gd's own doc for why the
## separate "district" screen retired). Unlike drag (which needed
## _gui_input event delivery, hence the old "does an ancestor eat the
## event" test), WASD pan lives in _process() — Godot delivers _process()
## to every node in the tree regardless of Control mouse_filter/ancestry
## (there is no "topmost control" routing for per-frame process callbacks
## the way there is for _gui_input), so there is no equivalent "does the
## screen eat it" question for _process() itself. What DOES still matter
## through the real chain is _is_over_ui()'s edge-scroll suppression and
## visibility gating — pinned directly below instead.
func test_wasd_pan_reaches_viewer_through_regional_screen_chain() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
screen.enter({"body": {"body_id": "GJ380c"}, "system": {}})
var offset_before: Vector2 = screen._viewer.get_view_offset()
screen._viewer._apply_pan_delta(Vector2(1.0, 0.0), 0.1)
assert_that(screen._viewer.get_view_offset()).override_failure_message(
"a pan tick driven through RegionalScreen's child viewer must still move"
+ " _view_offset — no ancestor in the real screen chain blocks it"
).is_not_equal(offset_before)
# =============================================================================
# T-1142 item 5: pole hard wall wired into the (now WASD) pan handler
# =============================================================================
## A window already near the pole, panned FAR toward it, must have its
## offset clamped by the real _apply_pan_delta() path (not just the pure
## function in isolation — this confirms the wiring, not just the math).
## A synthetic small body (NOT GJ380c's real ~6238km radius) is used
## deliberately: with a real body's huge rows_half (~4785 for GJ380c), the
## wall sits so far away that even a long held-key tick never reaches it —
## the wall is real but the test would need an implausibly long hold to
## trigger it. A small synthetic radius (-> a small rows_half) keeps the
## wall reachable by an ordinary tick while exercising the exact same code
## path.
func test_wasd_pan_is_clamped_by_the_pole_wall_when_wired() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
# A tiny synthetic radius -> district_extent().rows_half is small (a few
# hundred districts), so the pole wall is within reach of an ordinary
# pan tick. Center 10 districts from the north pole.
var radius_km := 50.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var rows_half: int = int(extent["rows_half"])
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, -rows_half + 10), 32)
# An absurdly long single tick (500s — no real frame is ever this long,
# deliberately so the UNCLAMPED delta is orders of magnitude larger than
# any plausible wall position, making "was it actually clamped" an
# unambiguous check rather than a fragile near-boundary comparison).
var unclamped_magnitude: float = 500.0 * AtlasWindowViewer.PAN_SPEED_CANVAS_PX_S * v.get_view_zoom()
v._apply_pan_delta(Vector2(0.0, -1.0), 500.0) # "W"/north held
var message: String = (
"a pan toward the pole with an unclamped magnitude of %.0f must land"
+ " nowhere near that far — the wall must have clamped it"
) % unclamped_magnitude
assert_float(absf(v.get_view_offset().y)).override_failure_message(message).is_less(
unclamped_magnitude * 0.5
)
# =============================================================================
# T-1142 item 6: east-west wrap — canonicalization on entry + cache reuse
# =============================================================================
## enter() canonicalizes an out-of-range center BEFORE it becomes
## _held_center — a column past the body's circumference wraps into range.
func test_enter_canonicalizes_an_out_of_range_center() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols + 50, 0), 32)
var window: Dictionary = _mock_window(Vector2i(50, 0), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v.get_district_window()).override_failure_message(
"the response must be adopted under the CANONICALIZED center (50, 0),"
+ " matching what the server would echo back for the wrapped request"
).is_equal(window)
## A center ONE column past the seam (item 6b): the SAME cache key as its
## twin at column 0 — a full-circumnavigation pan back to the seam must hit
## cache, not re-derive, because both requests canonicalize to the same
## (body, center, n) key.
func test_center_one_column_past_the_seam_shares_a_cache_key_with_its_twin() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var v1: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v1)
v1.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(cols, 50), 32)
var v2: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v2)
v2.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(0, 50), 32)
# Both must adopt the SAME server response (keyed on the same
# canonicalized center) — proves the cache key (and the outbound
# request) canonicalize identically for the seam and its twin.
var window: Dictionary = _mock_window(Vector2i(0, 50), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", window))
assert_that(v1.get_district_window()).is_equal(window)
assert_that(v2.get_district_window()).is_equal(window)
# =============================================================================
# _user_adjusted guard (PR #188 review) — the flag exists so auto-fit NEVER
# fights a manually-adjusted view. The one branch that makes that true
# (resize while user-adjusted) had no coverage; both directions pinned here.
# T-1145: the ORIGINAL version drove this through a synthetic drag sequence
# (_gui_input); drag is gone (item 2), so this now drives a WASD press
# instead — via _apply_pan_delta() directly, same testable-shape choice
# documented at the top of the WASD section above (a real key-repeat
# sequence through _gui_input would exercise nothing, since WASD panning
# never goes through _gui_input at all).
# =============================================================================
func test_resize_after_manual_wasd_press_keeps_user_view() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1280.0, 720.0)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(10, 20), 32)
v._apply_pan_delta(Vector2(1.0, -1.0), 0.1) # a single "D+W" tick — sets _user_adjusted
var user_zoom: float = v.get_view_zoom()
var user_offset: Vector2 = v.get_view_offset()
v.size = Vector2(1600.0, 900.0)
v.notification(Control.NOTIFICATION_RESIZED)
assert_float(v.get_view_zoom()).override_failure_message(
"resize while user-adjusted must NOT re-fit — zoom belongs to the user"
).is_equal_approx(user_zoom, 0.0001)
assert_vector(v.get_view_offset()).override_failure_message(
"resize while user-adjusted must NOT re-center — offset belongs to the user"
).is_equal_approx(user_offset, Vector2(0.001, 0.001))
func test_resize_without_user_adjustment_refits() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1280.0, 720.0)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(10, 20), 32)
var fitted_zoom: float = v.get_view_zoom()
v.size = Vector2(640.0, 360.0)
v.notification(Control.NOTIFICATION_RESIZED)
assert_float(v.get_view_zoom()).override_failure_message(
"resize with no manual adjustment must re-fit to the new viewport"
).is_not_equal(fitted_zoom)
# =============================================================================
# T-1145 item 2: edge-scroll suppression — over UI (_is_over_ui reuse) and
# unfocused-window (_app_has_focus, NOTIFICATION_APPLICATION_FOCUS_OUT/IN).
# =============================================================================
## Cursor within EDGE_SCROLL_MARGIN_PX of the left edge -> edge-scrolling.
func test_edge_scroll_detects_cursor_near_the_left_edge() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v._last_mouse_pos = Vector2(10.0, 300.0) # within 24px of x=0
assert_bool(v._is_cursor_edge_scrolling()).is_true()
## Cursor well inside the viewport (nowhere near any edge) -> NOT edge-scrolling.
func test_edge_scroll_does_not_trigger_away_from_any_edge() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v._last_mouse_pos = Vector2(400.0, 300.0) # dead center
assert_bool(v._is_cursor_edge_scrolling()).is_false()
## Cursor near the RIGHT edge (not just left) also triggers — all four edges
## are live, not just one.
func test_edge_scroll_detects_cursor_near_the_right_edge() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v._last_mouse_pos = Vector2(795.0, 300.0) # within 24px of x=800
assert_bool(v._is_cursor_edge_scrolling()).is_true()
## The direction produced when edge-scrolling near the left edge must point
## WEST (negative X) — toward the edge the cursor is near, matching WASD's
## own "A pans toward more western content" semantics exactly (same sign
## convention, same _apply_pan_delta() consumer).
func test_edge_scroll_direction_points_toward_the_near_edge() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v._last_mouse_pos = Vector2(5.0, 300.0)
var direction: Vector2 = v._edge_scroll_direction()
assert_float(direction.x).override_failure_message(
"edge-scroll near the LEFT edge must produce a WESTWARD (negative x) direction"
).is_less(0.0)
assert_float(direction.y).is_equal_approx(0.0, 0.001)
## Reuses _is_over_ui() (the ticket's explicit instruction) — this screen's
## own _is_over_ui() always returns false today (no city panel yet, see its
## own doc), so edge-scroll near an edge must still trigger; the POINT of
## this test is pinning that the suppression call-site exists and reads
## _is_over_ui's real return value, not that it currently suppresses
## anything (nothing to suppress against yet on this screen).
func test_edge_scroll_over_ui_uses_is_over_ui() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v._last_mouse_pos = Vector2(5.0, 300.0)
assert_bool(v._is_over_ui(v._last_mouse_pos)).override_failure_message(
"AtlasWindowViewer._is_over_ui() has no UI surface yet (see its own doc) —"
+ " this pins that baseline so a future sidebar addition's test failure here"
+ " signals the edge-scroll suppression wiring needs a look, not a silent pass"
).is_false()
assert_bool(v._is_cursor_edge_scrolling()).is_true()
## _app_has_focus defaults true (a freshly-entered screen assumes OS focus).
func test_app_focus_defaults_true() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
assert_bool(v._app_has_focus).is_true()
## NOTIFICATION_APPLICATION_FOCUS_OUT flips _app_has_focus false, and edge-
## scroll must stop triggering even with the cursor still parked at an edge
## — "if detectable" per the ticket; Godot's own focus notification IS
## directly detectable, so this pins that it is actually wired.
func test_app_focus_out_suppresses_edge_scroll() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v._last_mouse_pos = Vector2(5.0, 300.0)
assert_bool(v._is_cursor_edge_scrolling()).override_failure_message(
"sanity: edge-scroll must be live before focus-out"
).is_true()
v.notification(Control.NOTIFICATION_APPLICATION_FOCUS_OUT)
assert_bool(v._app_has_focus).is_false()
assert_bool(v._is_cursor_edge_scrolling()).override_failure_message(
"edge-scroll must be suppressed while the OS window lacks focus"
).is_false()
## NOTIFICATION_APPLICATION_FOCUS_IN restores edge-scroll after a focus-out.
func test_app_focus_in_restores_edge_scroll() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v._last_mouse_pos = Vector2(5.0, 300.0)
v.notification(Control.NOTIFICATION_APPLICATION_FOCUS_OUT)
v.notification(Control.NOTIFICATION_APPLICATION_FOCUS_IN)
assert_bool(v._app_has_focus).is_true()
assert_bool(v._is_cursor_edge_scrolling()).is_true()
# =============================================================================
# T-1145 item 2: WASD reads the PHYSICAL keycode, independent of the
# gameplay move_north/move_south/move_east/move_west InputMap actions those
# SAME keys are already bound to project-wide (D-054). This is a structural
# check, not a live-input one (gdUnit's headless mode does not transport real
# InputEvents, per this suite's own established note) — it pins that
## _held_pan_direction() calls Input.is_key_pressed() (physical keycode), NOT
## Input.is_action_pressed("move_north") or similar, by inspecting that no
## project Input Map action name appears anywhere in this function's own
## reachable behavior. The live independence claim itself (holding W pans
## the map AND does not also queue a gameplay move) is the lead's own
## live-verification step.
# =============================================================================
## project.godot's move_north/move_south/move_east/move_west actions are
## ALREADY bound to W/S/A/D physical keys (confirmed by direct inspection of
## project.godot's [input] section during T-1145 implementation) — this test
## exists purely as a living pin of that fact, so the rationale in
## _held_pan_direction()'s own doc comment (why raw keycodes, not the shared
## action) stays true if the project's key bindings are ever edited.
func test_wasd_keys_are_the_same_physical_keys_as_gameplay_movement_actions() -> void:
var action_to_key: Dictionary = {
"move_north": KEY_W, "move_west": KEY_A, "move_south": KEY_S, "move_east": KEY_D
}
for action: String in action_to_key.keys():
assert_bool(InputMap.has_action(action)).override_failure_message(
"expected gameplay action '%s' to exist in the project InputMap" % action
).is_true()
var bound_to_key: bool = false
for input_event: InputEvent in InputMap.action_get_events(action):
if input_event is InputEventKey and (input_event as InputEventKey).physical_keycode == action_to_key[action]:
bound_to_key = true
break
assert_bool(bound_to_key).override_failure_message(
(
"expected '%s' to be bound to physical keycode %d — if this ever"
+ " stops being true, _held_pan_direction()'s own doc comment"
+ " (why it reads Input.is_key_pressed() instead of the shared"
+ " action) should be re-checked, not silently left stale"
) % [action, action_to_key[action]]
).is_true()
@@ -1,368 +0,0 @@
## T-1172: pure-function tests for AtlasWindowWaterClip — the two-waterline
## clip's cell-lookup math (cell_grid_side_for_window/morphology_zone_in_window/
## resolve_morphology_zone). Split into its own file matching this cluster's
## own "one pure-geometry file, one test file" precedent
## (test_atlas_window_geometry_nature.gd next to atlas_window_geometry.gd's
## nature-overlay additions).
class_name TestAtlasWindowWaterClip
extends GdUnitTestSuite
const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd")
const MORPHOLOGY_OPEN_OCEAN: int = 0
const MORPHOLOGY_LAND: int = 8 # AlluvialPlain — any non-water zone works
## A window dict shaped exactly like a real DistrictWindowLayer — `center`
## as a [x,y] array (the msgpack-decoded wire shape, matching every other
## mock window in this cluster's tests), `n` districts wide, `granularity_v2`
## District (1:1 cell:district, the simplest case), and a `grid_side x
## grid_side` morphology array where `grid_side == n`.
static func _mock_district_window(
center: Vector2i, n: int, morphology: PackedByteArray
) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"granularity_v2": "District",
"morphology": morphology,
}
# =============================================================================
# cell_grid_side_for_window — mirrors AtlasWindowOverlay.cell_grid_side_for_window()
# =============================================================================
func test_cell_grid_side_district_is_n_unchanged() -> void:
var w := {"n": 32, "granularity_v2": "District"}
assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(32)
func test_cell_grid_side_quarter_is_n_times_four() -> void:
var w := {"n": 16, "granularity_v2": "Quarter"}
assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(64)
func test_cell_grid_side_region_is_n_over_hundred_rounded() -> void:
var w := {"n": 6400, "granularity_v2": "Region"}
assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(64)
func test_cell_grid_side_region_floors_at_one() -> void:
var w := {"n": 1, "granularity_v2": "Region"}
assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(1)
func test_cell_grid_side_unknown_granularity_falls_back_to_district() -> void:
var w := {"n": 32} # no granularity_v2 key at all
assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(32)
# =============================================================================
# morphology_zone_in_window — single-window cell lookup
# =============================================================================
## A 4x4 District window (n=4, grid_side=4) centered on district (0,0),
## spanning [-2, 2) on both axes. Cell (0,0) [top-left, covering district
## x in [-2,-1), y in [-2,-1)] is water; the rest is land.
static func _mock_4x4_window() -> Dictionary:
var morphology := PackedByteArray()
morphology.resize(16)
for i in range(16):
morphology[i] = MORPHOLOGY_LAND
morphology[0] = MORPHOLOGY_OPEN_OCEAN # row 0, col 0
return _mock_district_window(Vector2i.ZERO, 4, morphology)
func test_morphology_zone_in_window_reads_the_water_cell() -> void:
var w: Dictionary = _mock_4x4_window()
# District (-1.5, -1.5) falls in cell (row 0, col 0) — the water cell.
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(-1.5, -1.5), w)
assert_int(zone).is_equal(MORPHOLOGY_OPEN_OCEAN)
func test_morphology_zone_in_window_reads_a_land_cell() -> void:
var w: Dictionary = _mock_4x4_window()
# District (1.5, 1.5) falls in cell (row 3, col 3) — land.
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(1.5, 1.5), w)
assert_int(zone).is_equal(MORPHOLOGY_LAND)
func test_morphology_zone_in_window_outside_extent_is_no_data() -> void:
var w: Dictionary = _mock_4x4_window()
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(100.0, 100.0), w)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
func test_morphology_zone_in_window_null_window_is_no_data() -> void:
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2.ZERO, null)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
func test_morphology_zone_in_window_missing_morphology_array_is_no_data() -> void:
var w := {"center": [0, 0], "n": 4, "granularity_v2": "District"}
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2.ZERO, w)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
func test_morphology_zone_in_window_zero_n_is_no_data() -> void:
var w := {
"center": [0, 0], "n": 0, "granularity_v2": "District", "morphology": PackedByteArray()
}
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2.ZERO, w)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
## The exact top-left/bottom-right boundary districts must resolve to the
## edge cells, not silently miss due to an off-by-one in the containment
## test — [-2, 2) is half-open, so -2.0 is IN, 2.0 is OUT.
func test_morphology_zone_in_window_boundary_inclusive_at_min() -> void:
var w: Dictionary = _mock_4x4_window()
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(-2.0, -2.0), w)
assert_int(zone).is_equal(MORPHOLOGY_OPEN_OCEAN)
func test_morphology_zone_in_window_boundary_exclusive_at_max() -> void:
var w: Dictionary = _mock_4x4_window()
var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(2.0, 2.0), w)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
# =============================================================================
# resolve_morphology_zone — the single-window / tile-mode dispatch
# =============================================================================
func test_resolve_single_window_mode_reads_the_window_directly() -> void:
var w: Dictionary = _mock_4x4_window()
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(-1.5, -1.5), false, w, [], 0
)
assert_int(zone).is_equal(MORPHOLOGY_OPEN_OCEAN)
func test_resolve_single_window_mode_null_window_is_no_data() -> void:
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(Vector2.ZERO, false, null, [], 0)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
## Tile mode: two tiles, each its own 4x4 window, centered far enough apart
## that a queried district only falls inside ONE of them.
func test_resolve_tile_mode_finds_the_containing_tile() -> void:
var morph_a := PackedByteArray()
morph_a.resize(16)
for i in range(16):
morph_a[i] = MORPHOLOGY_LAND
var tile_a := {
"center": Vector2i(0, 0), "window": _mock_district_window(Vector2i.ZERO, 4, morph_a)
}
var morph_b := PackedByteArray()
morph_b.resize(16)
for i in range(16):
morph_b[i] = MORPHOLOGY_OPEN_OCEAN
var tile_b := {
"center": Vector2i(100, 0), "window": _mock_district_window(Vector2i(100, 0), 4, morph_b)
}
var tiles: Array = [tile_a, tile_b]
var zone_in_a: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(0.0, 0.0), true, null, tiles, 0
)
var zone_in_b: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(100.0, 0.0), true, null, tiles, 0
)
assert_int(zone_in_a).override_failure_message(
"a district position inside tile A's extent must read tile A's own cell"
).is_equal(MORPHOLOGY_LAND)
assert_int(zone_in_b).override_failure_message(
"a district position inside tile B's extent must read tile B's own cell"
).is_equal(MORPHOLOGY_OPEN_OCEAN)
func test_resolve_tile_mode_position_outside_every_tile_is_no_data() -> void:
var morph := PackedByteArray()
morph.resize(16)
var tile := {"center": Vector2i(0, 0), "window": _mock_district_window(Vector2i.ZERO, 4, morph)}
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(9999.0, 9999.0), true, null, [tile], 0
)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
## A tile whose window hasn't arrived yet (`window: null`, matching
## AtlasWindowTileSet.get_tiles()'s own "unarrived" shape) must be skipped,
## not crash — the scan continues to the next tile / falls through to
## MORPHOLOGY_ZONE_NO_DATA.
func test_resolve_tile_mode_skips_unarrived_tiles() -> void:
var unarrived := {"center": Vector2i(0, 0), "window": null}
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(0.0, 0.0), true, null, [unarrived], 0
)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
func test_resolve_tile_mode_empty_tile_list_is_no_data() -> void:
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(Vector2.ZERO, true, null, [], 0)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
## Live round 2 fix (coordinator's trace, T-1172): a tile whose CANONICAL
## center is far from `held_center` (the seam-tile case — exactly Lendel's
## own live repro, tile canonical center 12739 drawn at draw_col=-6400) must
## have its CENTER wrapped toward `held_center_x` — mirroring
## AtlasWindowOverlay._draw_tile_mosaic()'s own `draw_col =
## nearest_wrap_image(center.x, held_center.x, cols)` EXACTLY — before
## testing containment. The query `district` is assumed ALREADY expressed in
## the held-center-wrapped frame (AtlasWindowNatureOverlay._district()'s own
## contract) and is NOT separately re-wrapped.
##
## Original (pre-fix) test asserted the INVERSE — wrapping the query toward
## the tile's raw canonical center — which was the actual bug: it happened
## to land inside the tile's CANONICAL (unwrapped) span by coincidental mod
## arithmetic, silently testing the WRONG real-world location whenever a
## tile needed wrapping to appear on screen at all. Live capture evidence:
## a river dot at district.x=-9569 sitting on the painter's WEST wrap-image
## of a seam tile (canonical center 12739, draw_col=-6400) read a real but
## wrong-location land cell under the old code, and only stopped doing so
## once resolve_morphology_zone() wrapped the TILE's center instead.
func test_resolve_tile_mode_wraps_the_tiles_own_center_toward_held_center() -> void:
var cols := 100
var morph := PackedByteArray()
morph.resize(16)
for i in range(16):
morph[i] = MORPHOLOGY_OPEN_OCEAN
# Tile's canonical center is column 98 (far east) — but its nearest
# wrap-image to held_center=0 is column -2 (98 - 100), matching the
# Lendel seam tile's own shape (canonical 12739 -> draw_col -6400).
var tile := {"center": Vector2i(98, 0), "window": _mock_district_window(Vector2i(98, 0), 4, morph)}
# Query at column -2.5 — inside the tile's WRAP-IMAGE span [-4, 0), the
# real on-screen location, held_center-relative (the caller's own
# _district() contract) — NOT inside the canonical span [96, 100).
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(-2.5, 0.0), true, null, [tile], cols, 0
)
assert_int(zone).override_failure_message(
"the tile's CENTER must be wrapped toward held_center_x (mirroring the"
+ " painter's draw_col computation) so a query already expressed in the"
+ " held-center frame resolves against the tile's REAL on-screen wrap-image"
).is_equal(MORPHOLOGY_OPEN_OCEAN)
## The INVERSE position — a query at the tile's CANONICAL (unwrapped) span —
## must NOT resolve against this tile once wrapping is applied, since that
## span is no longer where the tile actually draws relative to held_center.
## Pins that the fix doesn't just "also succeed at the old span" by accident.
func test_resolve_tile_mode_does_not_match_the_tiles_stale_canonical_span() -> void:
var cols := 100
var morph := PackedByteArray()
morph.resize(16)
for i in range(16):
morph[i] = MORPHOLOGY_OPEN_OCEAN
var tile := {"center": Vector2i(98, 0), "window": _mock_district_window(Vector2i(98, 0), 4, morph)}
# Query at column 97 — inside the tile's CANONICAL span [96,100) — but
# that is NOT where this tile is drawn relative to held_center=0 (it's
# drawn at the wrap-image [-4,0) instead), so this must NOT resolve.
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(97.0, 0.0), true, null, [tile], cols, 0
)
assert_int(zone).override_failure_message(
"a query at the tile's stale CANONICAL span must not resolve against it"
+ " once the tile is wrapped toward held_center — that span is not where"
+ " the tile actually draws on screen"
).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
# =============================================================================
# T-1172 round 2 (coordinator's "wire-accurate fixture" hardening ask —
# cold-start batch discipline): a fixture derived from an ACTUAL live tile
# response captured during the round-2 investigation, at the REAL wire scale
# (n=6400, grid_side=64, Region granularity) — not a hand-shrunk 4x4 mock.
# The round-2 bug (wrapping the query toward the tile instead of the tile
# toward held_center) passed EVERY test against the small mocks above,
## because those mocks never modeled a tile whose canonical center is FAR
# from held_center — the exact condition the bug needed to manifest. This
# fixture reproduces that condition at production scale, so a future
# regression of the same SHAPE (stub-and-code silently agreeing on a wrong
# convention) can't hide behind "the small tests still pass."
# =============================================================================
## Lendel's own seam tile from the live drive capture that closed T-1172
## round 2 (client/tmp_drive_clip.gd, SR_LIVE=1 against the worktree release
## server): canonical center (12739, -3200), TILE_N=6400 districts,
## Region granularity (grid_side = round(6400/100) = 64). `cols=19139`
## matches Lendel's real district_extent() circumference. The morphology
## array is NOT the real 4096-byte payload (too large to hand-author) — only
## the ONE cell index the live trace actually resolved for the
## district=-9569.414 repro query (idx=1024, col=0/row=16 — the exact
## INDEX_TRACE line from the live investigation) is given a real value;
## every other cell is left at 0 (OpenOcean), which is irrelevant here since
## this fixture exists to pin the WRAP resolution reaching the CORRECT
## tile/cell pair, not to re-verify the index math itself (already covered
## above and in test_atlas_window_geometry_nature.gd).
static func _lendel_seam_tile_fixture() -> Dictionary:
var morph := PackedByteArray()
morph.resize(4096)
morph[1024] = MORPHOLOGY_LAND # col=0, row=16 — the live-traced cell
return {
"center": Vector2i(12739, -3200),
"window": {
"center": [12739, -3200],
"n": 6400,
"granularity_v2": "Region",
"morphology": morph,
}
}
## The exact district position from the live capture that ORIGINALLY exposed
## the round-2 bug (district.x=-9569.414 — a dot visibly sitting on the
## painter's WEST wrap-image of the seam tile). held_center_x=0 (the
## viewer's canonical orbital-frame origin, cols=19139 (Lendel's real
## circumference in districts). Must resolve to the SAME land zone the live
## painter trace independently confirmed for this exact query.
##
## Honest note (found DURING revert-verification, worth recording): for a
## SINGLE tile in isolation, the old (query-wrapped-toward-tile) and new
## (tile-wrapped-toward-held_center) formulas are mathematically GUARANTEED
## to agree whenever local_x lands in-range for both — both reduce to
## `query - tile_center (mod cols)`, and a valid `local_x` is unique in
## `[0, n)`. This single-tile fixture therefore does NOT independently
## distinguish old from new (confirmed: it still passes with the pre-fix
## code) — it locks in the real wire-scale numbers as a realistic regression
## fixture (shared index formula, tile shape, wrap arithmetic all exercised
## together), not as the old-vs-new discriminator. The tests that DO reliably
## catch the round-2 regression are
## test_resolve_tile_mode_wraps_the_tiles_own_center_toward_held_center and
## test_resolve_tile_mode_does_not_match_the_tiles_stale_canonical_span above
## (confirmed: the latter fails by name against the reverted code) — the
## real-world bug's actual mechanism was the MULTI-TILE SCAN ORDER matching
## the WRONG tile's data before reaching the right one, not a single-tile
## formula divergence; a true multi-tile live reproduction would need the
## full 6-tile fixture, impractical to hand-author at full 4096-cell scale.
func test_wire_accurate_lendel_seam_tile_resolves_correctly() -> void:
var tile: Dictionary = _lendel_seam_tile_fixture()
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(-9569.414, -4784.793), true, null, [tile], 19139, 0
)
assert_int(zone).override_failure_message(
"the live T-1172 round 2 repro position must resolve against the seam"
+ " tile's WRAPPED (on-screen) image and read the real traced land zone"
+ " — a regression here reproduces the ORIGINAL over-ocean-dots bug"
).is_equal(MORPHOLOGY_LAND)
## The SAME fixture, queried at a position that legitimately falls OUTSIDE
## even the wrapped tile's span (nowhere near either wrap-image) — must fail
## open (NO_DATA), not silently match by coincidental mod arithmetic (the
## general shape of the original bug, pinned generically here in case a
## future change reintroduces a different mod-arithmetic coincidence).
func test_wire_accurate_lendel_seam_tile_out_of_range_query_is_no_data() -> void:
var tile: Dictionary = _lendel_seam_tile_fixture()
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
Vector2(500.0, 500.0), true, null, [tile], 19139, 0
)
assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA)
-952
View File
@@ -1,952 +0,0 @@
## T-1153 (D-226 T-1143-rulings amendment): tests for the continuous
## cursor-anchored zoom ladder — enter_orbital() (the canonical planetary
## frame), progressive refinement (held composite survives a rung-crossing
## request), the full-zoom-out reset (Jeroen's HARD condition), rung
## reselection on zoom, and E/W wrap + pole-wall clamps at Region
## granularity. Split out of test_atlas_window_viewer.gd (which owns the
## pre-T-1153 window-viewer behavior — entry, cache reuse, WASD/edge-scroll,
## fit-and-center) purely for file-length reasons (gdlint max-file-lines);
## same instantiation/mock-response conventions as that file, not a
## different testing philosophy.
class_name TestAtlasZoomLadder
extends GdUnitTestSuite
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## Dudley's WINDOW_GRANULARITY_REGION_KEY (server/src/atlas/layer_proxy.rs) —
## `u32::MAX`, a RESERVED KEY-SPACE TAG the real server ALWAYS puts in the
## legacy `granularity` slot for every Region response (never a real
## multiplier — District=1/Quarter=4 are the only legal wire multipliers).
## Do NOT "fix" this to 1 — that would silently un-repro the live-round bug
## this constant exists to guard against (a real server's actual wire byte,
## not a convenient test value). See _echoed_granularity_matches()'s own doc
## (atlas_window_request.gd) for why this value can NEVER equal a client's
## stored `_granularity` (which stays pinned at DISTRICT_GRANULARITY=1 for
## every rung a T-1152-aware client requests) — that mismatch is exactly
## what silently dropped every Region response before the v2-authoritative
## fix.
const SERVER_LEGACY_GRANULARITY_REGION_SENTINEL: int = 4294967295
## Build a hand-authored DistrictWindowLayer dict (n=2 by default) — mirrors
## test_atlas_window_viewer.gd's own _mock_window().
static func _mock_window(center: Vector2i, n: int = 2) -> Dictionary:
return {
"center": [center.x, center.y],
"n": n,
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
# =============================================================================
# T-1153: enter_orbital() — the canonical planetary frame, the ladder's TOP
# REST STATE (Jeroen's HARD condition, D-226 T-1143-rulings amendment).
# =============================================================================
## enter_orbital() must center on district (0,0) — "district (0,0) sits at
## lon 0 / the equator" (AtlasDescendGeometry's own doc).
func test_enter_orbital_centers_on_the_canonical_origin() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_that(v._held_center).is_equal(Vector2i.ZERO)
## enter_orbital() must request at Region granularity — the orbital view IS
## the Region rung at high n, not a separate screen/mode (the ticket's own
## framing).
func test_enter_orbital_requests_region_granularity() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
## **Superseded by live round 3's tiling fix — retargeted, not deleted.**
## GJ380c/Lendel (radius 6238.4 km) was the ORIGINAL single-window C1 repro
## (raw cols ~19,139 vs. the 6,400 clamp ceiling) — but that SAME threshold
## (`DISTRICT_WINDOW_MAX_N_REGION * DISTRICT_M` = the coverage ceiling
## `compute_tile_grid()` tiles past) means any body needing the n-clamp ALSO
## needs tiling: there is no real body where enter_orbital() takes the
## single-window path with a raw `n` big enough to require clamping.
## GJ380c now correctly enters TILE mode (test_enter_orbital_n_is_the_clamped_value_not_raw_circumference's
## old assertion on a single clamped `_held_n` no longer applies — see
## test_enter_orbital_tile_mode_held_n_is_the_whole_body_extent below for
## what `_held_n` means in tile mode instead). The single-window clamp-mirror
## fix itself remains covered: `_enter_at_rung()`'s own doc/the clamp
## mirror's unit tests (test_atlas_window_request.gd) pin the formula
## directly, and test_zoom_crossing_fires_request_and_accepts_wire_accurate_refinement
## exercises the SAME clamp-mirror lesson at the reselect (not entry)
## boundary, which single-window mode still reaches on the way DOWN from a
## tile-mode zoom-in.
func test_enter_orbital_tile_mode_held_n_is_the_whole_body_extent() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel)
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var raw_cols: int = int(extent["cols"])
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).override_failure_message(
"GJ380c/Lendel needs tiling — enter_orbital() must have entered tile mode"
).is_true()
# In TILE mode, _held_n is the WHOLE body's extent (unclamped) — each
# TILE clamps its own request independently inside AtlasWindowTileSet
# (see that file's own tests), so _held_n here is NOT expected to equal
# any single clamped value the way single-window mode's is.
assert_int(v._held_n).is_equal(raw_cols)
## Coordinator live-eyeball dossier (2026-07-23, suspect 2 — DIAGNOSED FALSE
## but pinned as a regression guard anyway per the coordinator's own
## instruction): AtlasWindowNatureOverlay's _draw() reads
## viewer.get_held_granularity_v2() to key its per-rung policy tables
## (RIVER_CLASS_VISIBLE_BY_RUNG etc.) — if that accessor returned anything
## other than the EXACT string "Region" while is_tile_mode() is true (an
## empty string, a stale District default, a different-cased tag...), the
## visibility tables would silently return their empty/default disposition
## and NOTHING would draw, indistinguishable from the live captures'
## "literally zero river dots" symptom. Live drive-script evidence
## (NATURE_DEBUG print, since removed) confirmed this was NOT the actual bug
## — get_held_granularity_v2() already correctly returns "Region" in tile
## mode — but this test makes that fact load-bearing instead of merely
## observed once, so a future refactor of _enter_tile_mode()'s
## _held_granularity_v2 assignment trips a named failure here.
func test_get_held_granularity_v2_is_exactly_region_string_in_tile_mode() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — needs tiling
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).override_failure_message(
"this test's premise requires tile mode — Lendel must still need tiling"
).is_true()
assert_str(v.get_held_granularity_v2()).override_failure_message(
"AtlasWindowNatureOverlay's _draw() keys its ENTIRE per-rung policy off"
+ " this exact string — anything other than the literal 'Region' silently"
+ " empties every visibility table and draws nothing, indistinguishable"
+ " from the live-capture symptom (zero river dots at the orbital rest state)"
).is_equal("Region")
## **The live-round-3 regression, end to end for TILE mode:** enter_orbital()
## on GJ380c/Lendel followed by delivering ONE tile's wire-accurate response
## (clamped n=6,400, "Region" granularity_v2, the legacy sentinel in the old
## granularity slot — exactly what a real server sends) must be ACCEPTED
## into that tile's own slot — not silently dropped. This exercises BOTH
## live-round fixes (the v2-authoritative precedence AND per-tile clamping)
## through the tile-set path specifically, complementing
## test_atlas_window_tile_set.gd's own more granular orchestration tests.
func test_enter_orbital_tile_mode_accepts_a_wire_accurate_tile_response() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
var tile_set = v.get_tile_set()
var tiles: Array = tile_set.get_tiles()
assert_int(tiles.size()).is_greater(1)
var first_tile_center: Vector2i = tiles[0]["center"]
var tile_window: Dictionary = {
"center": [first_tile_center.x, first_tile_center.y],
"n": AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION,
"granularity": SERVER_LEGACY_GRANULARITY_REGION_SENTINEL,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", tile_window))
assert_that(tile_set.get_tiles()[0]["window"]).override_failure_message(
"a wire-accurate response (clamped n, Region granularity_v2, the legacy"
+ " sentinel) for the first tile must be ACCEPTED into that tile's slot"
).is_equal(tile_window)
## A no-radius body (tiny test body) has no circumference concept —
## enter_orbital() falls back to the District-rung default window rather
## than crashing or deriving a degenerate n.
func test_enter_orbital_no_radius_body_falls_back_to_district() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c"}, {})
assert_str(v._held_granularity_v2).is_equal("District")
assert_int(v._held_n).is_equal(AtlasWindowRequest.DISTRICT_WINDOW_DEFAULT_N)
# =============================================================================
# T-1153: progressive refinement — the held composite survives until the
# replacement arrives (§6 "no mode flip": never a blank frame, never a
# clear-then-redraw).
# =============================================================================
## The core acceptance test: once a window is held, a request for a
## DIFFERENT rung being in-flight must NOT clear `_window` — the old
## composite stays exactly what get_district_window() returns until the new
## rung's response actually arrives and is adopted.
func test_held_window_survives_while_a_different_rung_request_is_in_flight() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
assert_that(v.get_district_window()).is_equal(district_window)
# Simulate a rung-reselect firing a NEW (Region) request without the
# response having arrived yet — direct call, mirroring what
# _maybe_reselect_rung() does internally.
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
assert_that(v.get_district_window()).override_failure_message(
"the OLD composite must survive while a different-rung request is in"
+ " flight — no blank frame, no premature clear"
).is_equal(district_window)
## Once the new rung's response actually arrives (matching the CURRENTLY
## in-flight request's granularity_v2), it swaps in — the composite reference
## changes from the old rung's window to the new one.
func test_new_rung_window_swaps_in_once_it_arrives() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
var region_window: Dictionary = {
"center": [10, 20], "n": 2, "granularity_v2": "Region",
"morphology": PackedByteArray([1, 2, 3, 4]),
"elev_q": PackedByteArray([10, 20, 30, 40]),
"temp_dc": [0, 0, 0, 0],
"moisture_q": PackedByteArray([0, 0, 0, 0]),
"vegetation": PackedByteArray([0, 0, 0, 0]),
"glaciation": PackedByteArray([0, 0, 0, 0]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", region_window))
assert_that(v.get_district_window()).override_failure_message(
"once the new rung's matching response arrives, it must swap in"
).is_equal(region_window)
assert_str(v._held_granularity_v2).is_equal("Region")
## refresh() clear()s via queue_free() (deferred, not synchronous) — a legend
## that has refreshed more than once in the same frame (build-time refresh at
## _ready(), then an entry-time refresh) can have STALE not-yet-freed
## children still parented alongside the new ones. add_component() always
## APPENDS, so the current ImplantHeader is the LAST one in the list, never
## assumed to be [0].
static func _current_legend_header(legend_panel) -> ImplantHeader:
var children: Array = legend_panel.get_implant_children()
for i in range(children.size() - 1, -1, -1):
if children[i] is ImplantHeader:
return children[i]
return null
## PR #192 review (Araminta, BLOCKING): the legend subtitle used to hardcode
## District's own "2.048 km/cell" — a 100x lie whenever the viewer actually
## holds Region (204.8 km/cell). While in the orbital tile-mode rest state
## (Region granularity), the legend must read Region's real spacing, not the
## stale District literal.
func test_legend_subtitle_reflects_region_spacing_in_tile_mode() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 6238.4 # GJ380c (Lendel) — needs tiling, so enters at Region
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
assert_str(v._held_granularity_v2).is_equal("Region")
var header: ImplantHeader = _current_legend_header(v._legend_panel)
assert_str(header._subtitle_label.text).override_failure_message(
"legend subtitle must reflect Region's real 204.800 km/cell spacing while"
+ " the viewer holds Region granularity, not a hardcoded District figure"
).contains("204.800 km/cell")
## Same bug, the other direction: after crossing INTO a single-window District
## rung, the legend must re-render with District's own spacing — proving the
## legend actually refreshes on a rung change rather than being stuck at
## whatever it showed on the FIRST refresh() call (T-1153's _build_legend_panel()
## fires one at _ready() time, before any real rung is held).
func test_legend_subtitle_reflects_district_spacing_after_crossing_in() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
assert_str(v._held_granularity_v2).is_equal("District")
var header: ImplantHeader = _current_legend_header(v._legend_panel)
assert_str(header._subtitle_label.text).override_failure_message(
"legend subtitle must re-render at District's own 2.048 km/cell spacing"
+ " once the viewer holds a District-rung window — proving refresh() is"
+ " actually wired to the rung change, not just called once at build time"
).contains("2.048 km/cell")
## A response for a rung OTHER than what's currently requested (e.g. a
## District response arriving after the viewer has already moved on to a
## Region request — a rapid wheel-zoom race) must be discarded as stale, the
## held composite untouched.
func test_stale_rung_response_after_moving_on_is_discarded() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(10, 20), 2)
var district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._window_request.request_debounced("GJ380c", Vector2i(10, 20), 2, "Region")
# A LATE district-rung response for the same (center, n) arrives after the
# viewer has already moved on to requesting Region — must be dropped.
var late_district_window: Dictionary = _mock_window(Vector2i(10, 20), 2)
late_district_window["morphology"] = PackedByteArray([9, 9, 9, 9]) # distinguishable payload
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", late_district_window))
assert_that(v.get_district_window()).override_failure_message(
"a stale response for a rung the viewer has since moved on from must be discarded"
).is_equal(district_window)
# =============================================================================
# T-1153: full-zoom-out reset (Jeroen's HARD condition).
# =============================================================================
## Directly at the canonical frame already (center (0,0), Region granularity)
## must be a no-op — never re-fights a player zooming back IN from the top.
func test_reset_to_canonical_frame_is_noop_when_already_there() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._held_center = Vector2i.ZERO
v._held_granularity_v2 = "Region"
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"already at the canonical frame — the reset must not re-fire"
).is_false()
## A no-radius body must never trigger the reset (no circumference concept —
## matches enter_orbital()'s own guard).
func test_reset_to_canonical_frame_never_fires_for_no_radius_body() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(5, 5), 2)
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).is_false()
## Away from the canonical frame (a drifted District-rung pan/zoom state)
## with a fully-zoomed-out world extent must reset — the direct wiring test
## for Jeroen's HARD condition: enter() at a far-off center, then force the
## view zoom low enough that the displayed extent covers the whole body.
func test_reset_to_canonical_frame_fires_and_re_centers_when_fully_zoomed_out() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
var radius_km := 50.0 # tiny synthetic body — small circumference, reachable by a modest zoom-out
v.enter({"body_id": "GJ380c", "body_radius_km": radius_km}, {}, Vector2i(500, 10), 32)
# Force a very low zoom — a huge displayed world extent, comfortably over
# this tiny body's whole circumference.
v._view_zoom = AtlasWindowViewer.MIN_ZOOM
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"a fully-zoomed-out view on a real-radius body must trigger the reset"
).is_true()
assert_that(v._held_center).override_failure_message(
"the reset must re-center on the canonical origin (0,0)"
).is_equal(Vector2i.ZERO)
assert_str(v._held_granularity_v2).override_failure_message(
"the reset must land on the Region rung — the ladder's top rest state"
).is_equal("Region")
## Live round 5's OWN repro, end to end: enter a TILING body's canonical
## frame, wheel-zoom IN far enough to cross out of tile mode (leaving
## `_held_granularity_v2` STALE at "Region" — a real, expected lag per
## `_maybe_reselect_rung()`'s own "does NOT touch _held_granularity_v2"
## doc, not a bug in that function), then wheel-zoom back OUT past the
## fully-zoomed-out threshold. The reset must fire and land EXACTLY on
## enter_orbital()'s own fit zoom for this body/viewport — not merely
## re-center while leaving `_view_zoom` wherever continued `_zoom_at()`
## scaling left it. Before the fix, the stale "Region" granularity
## satisfied the guard's OLD (center + granularity only) check forever,
## so the reset never fired again and `_view_zoom` kept shrinking via
## plain multiplication all the way to MIN_ZOOM.
func test_reset_after_crossing_out_and_back_snaps_to_the_canonical_fit_zoom() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — a tiling body, the live-repro shape
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: Lendel must enter tile mode — this repro needs a TILING body,"
+ " since that's where _held_granularity_v2 can lag is_tile_mode()"
).is_true()
# Zoom IN far enough to cross out of tile mode (matching
# test_zoom_crossing_recomputes_view_offset_so_the_new_window_is_on_screen's
# own gesture shape).
var cursor_pos := Vector2(800.0, 450.0)
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: this test needs to actually leave tile mode before zooming back out"
).is_false()
assert_str(v._held_granularity_v2).override_failure_message(
"sanity: _held_granularity_v2 must be STALE at Region here (no mock response"
+ " ever adopted a new value) — this is the exact lagging-field condition"
+ " the guard fix targets, not an artificial setup"
).is_equal("Region")
# Zoom back OUT past the fully-zoomed-out threshold — the reset must fire
# (possibly after a few more _zoom_at() ticks, matching a real wheel
# gesture rather than asserting it fires on the very first step back).
for _i in range(200):
v._zoom_at(cursor_pos, 1.0 / 1.05)
if v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"zooming back out past the threshold must re-fire the reset and land back"
+ " in tile mode — the stale-granularity guard bug left this permanently false"
).is_true()
assert_that(v._held_center).is_equal(Vector2i.ZERO)
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
var expected_fit: Dictionary = AtlasWindowGeometry.fit_window_view(
v.size, n, AtlasWindowViewer.CELL_PIXEL_SIZE, AtlasWindowViewer.MIN_ZOOM, AtlasWindowViewer.MAX_ZOOM
)
assert_float(v._view_zoom).override_failure_message(
(
"post-reset _view_zoom (%.6f) must equal enter_orbital()'s own fit zoom"
+ " (%.6f) for this body/viewport — Jeroen's condition is the ORIGINAL"
+ " frame (center AND offset AND fit zoom), not merely re-centered at"
+ " whatever zoom continued _zoom_at() scaling left behind"
)
% [v._view_zoom, expected_fit["zoom"]]
).is_equal_approx(float(expected_fit["zoom"]), 0.000001)
## Live round 5's OWN live-drive repro, exactly: a REAL wheel gesture does
## NOT stop the instant the reset first fires — the coordinator's own
## tmp_drive_ladder.gd keeps sending wheel-down ticks toward a fixed target
## zoom (0.004, chosen below the fit zoom) regardless of the reset. This
## test reproduces that shape directly: continue zooming out PAST the point
## where the reset first re-enters tile mode, all the way to a target zoom
## BELOW the fit value. Before the round-5 fix, `_view_zoom` drifted back
## down from the fit value on every subsequent `_zoom_at()` tick while the
## mode/center/granularity guard read "already canonical" and silently let
## it drift, landing on whatever the LOOP's target zoom happened to be
## instead of the fit value. **Live round 6 update:** the MECHANISM that
## now holds this assertion changed — `_zoom_at()`'s own zoom FLOOR (not a
## re-firing reset) is what keeps `_view_zoom` pinned at fit through
## continued zoom-out ticks; see `_maybe_reset_to_canonical_frame()`'s own
## doc for why re-firing on every tick caused a request storm. This test's
## own assertions are unchanged — only the doc below was updated to match.
func test_reset_resnaps_even_after_continued_zoom_out_past_the_first_reset() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var n: int = int(extent["cols"])
var expected_fit: Dictionary = AtlasWindowGeometry.fit_window_view(
v.size, n, AtlasWindowViewer.CELL_PIXEL_SIZE, AtlasWindowViewer.MIN_ZOOM, AtlasWindowViewer.MAX_ZOOM
)
var fit_zoom: float = float(expected_fit["zoom"])
# Zoom IN far enough to leave tile mode (same shape as the test above).
var cursor_pos := Vector2(1100.0, 300.0) # matches tmp_drive_ladder.gd's own aim point
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).is_false()
# Zoom back OUT toward a target BELOW the fit zoom — matching
# tmp_drive_ladder.gd's own `_zoom_until(wv, 0.004, false)` exactly
# (Lendel's own fit zoom is ~0.00627, comfortably above this target),
# WITHOUT stopping early the moment tile mode is first regained. A real
# wheel gesture has no way to know when the reset internally fires.
var target_zoom := 0.004
for _i in range(200):
if v._view_zoom <= target_zoom:
break
v._zoom_at(cursor_pos, 1.0 / 1.05)
assert_bool(v.is_tile_mode()).override_failure_message(
"after continued zoom-out past the reset point, the view must settle back"
+ " into tile mode — a genuinely re-snapped canonical frame can't have zoomed"
+ " OUT further than the fit value in the first place"
).is_true()
assert_float(v._view_zoom).override_failure_message(
(
"post-reset _view_zoom (%.6f) must equal the canonical fit zoom (%.6f) even"
+ " though the wheel gesture continued past the point where the reset first"
+ " fired (target was %.6f, BELOW the fit zoom) — _zoom_at()'s own zoom floor"
+ " must keep pinning it at fit through every subsequent tick, not just once"
)
% [v._view_zoom, fit_zoom, target_zoom]
).is_equal_approx(fit_zoom, 0.000001)
## Live round 6's ANTI-STORM test — the exact repro the coordinator's live
## drive caught: drive a REAL continued zoom-out gesture (via `_zoom_at()`,
## the same call path the live drive uses — NOT calling
## `_maybe_reset_to_canonical_frame()` directly with unchanged state, which
## trivially can't reproduce the drift the storm depends on) many ticks past
## the point where the reset first fires — asserts ZERO additional tile-set
## entries occur across the WHOLE gesture. Spies on `AtlasWindowTileSet`'s
## own child `AtlasWindowRequest` node INSTANCES (captured right after the
## FIRST reset) — a fresh `enter_orbital()` call tears down (`queue_free()`s)
## every one of them and creates BRAND NEW ones, so "the same node
## instances are still alive and still the tile set's children after 100
## more ticks" is a direct, non-invasive proxy for "the reset never fired
## again" — no new production instrumentation needed. Before the round-6
## fix, `_zoom_at()`'s continued multiplicative zoom-out drifted `_view_zoom`
## below fit on every subsequent tick, the level-triggered guard read "not
## already there" every time, and `enter_orbital()` fired repeatedly:
## tearing down and recreating the tile set (and its 6 request nodes) every
## tick — exactly the "889 of 897 wire responses arrived during one
## zoom-out phase" storm.
func test_reset_evaluated_repeatedly_at_canonical_frame_issues_zero_additional_requests() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — a tiling body, the live-repro shape
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
# Zoom IN far enough to leave tile mode, then zoom back OUT past the
# first reset — same shape as the round-5 continued-zoom-out test, but
# this time spying on the tile set across the WHOLE remaining gesture
# instead of only checking the final zoom value.
var cursor_pos := Vector2(1100.0, 300.0) # matches tmp_drive_ladder.gd's own aim point
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).is_false()
for _i in range(200):
v._zoom_at(cursor_pos, 1.0 / 1.05)
if v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: the first reset must have fired before spying on the tile set"
).is_true()
var tile_set = v.get_tile_set()
var original_requests: Array = tile_set.get_children()
assert_int(original_requests.size()).override_failure_message(
"sanity: the first reset must have created real tile-request child nodes to spy on"
).is_greater(0)
# Continue the SAME zoom-out gesture 100 MORE ticks past the first
# reset — a real wheel gesture has no way to stop exactly at the reset
# point, and holding the wheel down (or residual scroll momentum) keeps
# sending ticks. None of these must tear down/recreate the tile set.
for _i in range(100):
v._zoom_at(cursor_pos, 1.0 / 1.05)
var current_requests: Array = tile_set.get_children()
assert_int(current_requests.size()).override_failure_message(
"the tile set's child count must be unchanged after 100 more continued"
+ " zoom-out ticks — a changed count means teardown/recreate happened"
).is_equal(original_requests.size())
for i in range(original_requests.size()):
assert_bool(is_instance_valid(original_requests[i])).override_failure_message(
"original tile-request node #%d must still be alive — a storm would have"
+ " queue_free()'d it and created a fresh one" % i
).is_true()
assert_bool(is_same(original_requests[i], current_requests[i])).override_failure_message(
(
"tile-request node #%d must be the SAME instance as right after the"
+ " first reset — a different object at the same index means the tile"
+ " set was torn down and recreated (a storm), even if the count"
+ " coincidentally matches"
)
% i
).is_true()
## Live round 6's BLACK-ENTRY repro: enter_orbital(), then deliver the six
## wire-accurate tile responses WHILE a REAL continued zoom-out gesture (via
## `_zoom_at()`, matching the live drive's actual input shape — a held
## wheel-down keeps sending ticks concurrently with responses streaming in
## from the server) is in flight — asserts all six are accepted and HELD
## (tile set stable throughout, no teardown between delivery and the final
## assertion). Before the round-6 fix, the level-triggered guard fired on
## every zoom-out tick once `_view_zoom` drifted below fit, tearing down the
## tile set mid-delivery and orphaning responses addressed to now-freed
## request nodes — nothing ever accumulated, and the mosaic stayed black
## even though the server dutifully answered every request.
func test_six_tile_responses_survive_concurrent_reset_evaluation_and_are_held() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — 6 tiles, the live-repro shape
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
var tile_set = v.get_tile_set()
var tiles: Array = tile_set.get_tiles()
assert_int(tiles.size()).override_failure_message(
"sanity: Lendel must produce Lendel's own real tile count (6) for this"
+ " repro to be faithful, not a smaller synthetic count"
).is_equal(6)
# Same continued zoom-out gesture as the anti-storm test above — leave
# tile mode, cross back into it (the first reset), then KEEP sending
# zoom-out ticks (a real held wheel has no way to stop exactly at the
# reset point). Responses are delivered interleaved with these ticks,
# exactly matching the live drive's concurrent shape.
var cursor_pos := Vector2(1100.0, 300.0)
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).is_false()
for _i in range(200):
v._zoom_at(cursor_pos, 1.0 / 1.05)
if v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: the first reset must have fired before delivering responses"
).is_true()
for i in range(tiles.size()):
var center: Vector2i = tiles[i]["center"]
var tile_window: Dictionary = {
"center": [center.x, center.y],
"n": AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION,
"granularity": SERVER_LEGACY_GRANULARITY_REGION_SENTINEL,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", tile_window))
# Interleave several MORE continued zoom-out ticks, matching the live
# drive's per-frame cadence — none of these must tear anything down.
for _tick in range(5):
v._zoom_at(cursor_pos, 1.0 / 1.05)
var final_tiles: Array = tile_set.get_tiles()
assert_int(final_tiles.size()).override_failure_message(
"the tile set must still have all 6 tile slots — a storm mid-delivery"
+ " would have torn it down and rebuilt it with fresh (unfulfilled) slots"
).is_equal(6)
for i in range(final_tiles.size()):
assert_that(final_tiles[i]["window"]).override_failure_message(
(
"tile #%d's window must be HELD (non-null) — all six wire-accurate"
+ " responses delivered during a concurrent continued zoom-out gesture"
+ " must survive to be accepted, not be silently dropped by an"
+ " orphaning teardown"
)
% i
).is_not_null()
## Not fully zoomed out (a normal District-rung view) must NOT trigger the
## reset — only reaching the top of the ladder resets, not every zoom step.
func test_reset_to_canonical_frame_does_not_fire_when_not_fully_zoomed_out() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {}, Vector2i(500, 10), 32)
v._view_zoom = 1.0 # a normal, non-extreme zoom — nowhere near full planetary coverage
var fired: bool = v._maybe_reset_to_canonical_frame()
assert_bool(fired).override_failure_message(
"an ordinary District-rung view must not trigger the top-rest-state reset"
).is_false()
# =============================================================================
# T-1153: rung reselection — _zoom_at() crossing a rung threshold fires a
# new request without touching the held composite.
# =============================================================================
## Zooming OUT far enough from a District-rung window (small n, so a modest
## zoom-out already covers a huge world extent) must fire a coarser-rung
## request — the wheel-zoom-driven wiring test for _maybe_reselect_rung().
func test_zoom_out_past_district_threshold_requests_a_coarser_rung() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 600.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 2) # n=2 — a tiny window, easy to overshoot
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 2)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
assert_str(v._window_request.get_granularity_v2()).is_equal("District")
# A big zoom-OUT factor (well under 1.0) from a tiny n=2 window blows the
# displayed world extent WAY past District's threshold.
v._zoom_at(Vector2(400.0, 300.0), 0.01)
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
"zooming out far enough from a small District window must re-request a coarser rung"
).is_not_equal("District")
# The OLD composite must still be what's held — progressive refinement,
# not a block-on-derive clear.
assert_that(v.get_district_window()).is_equal(district_window)
## Zooming IN on a District-rung window (well within its own legal coverage
## band, `(32,768 m, 131,072 m]` per select_rung()'s redesigned per-rung
## ceiling model — viewport-independent since `canvas_px` no longer affects
## selection) must NOT trigger a rung change — this is the "zoom is
## client-side on the already-held composite" case, unchanged for in-rung
## zoom. Sets _view_zoom DIRECTLY to a value inside District's band (rather
## than relying on enter()'s COVER auto-fit, which for a small n can already
## sit right at Quarter's own threshold — a fit's zoom level is a
## display-density choice independent of what rung selection would pick from
## scratch, and this test is specifically about a SINGLE zoom-in STEP not
## crossing a boundary, not about where the auto-fit itself lands). The
## small 100x80 viewport here is incidental (any size works under the new
## viewport-independent model) — kept small only because that's what the
## original version of this test used.
func test_zoom_in_within_district_threshold_does_not_change_rung() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(100.0, 80.0)
v.enter({"body_id": "GJ380c"}, {}, Vector2i(0, 0), 32)
var district_window: Dictionary = _mock_window(Vector2i(0, 0), 32)
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", district_window))
v._view_zoom = 0.10666666666666667 # E=120,000m at C=100px — inside District's legal band
v._apply_transform()
v._zoom_at(Vector2(50.0, 40.0), 1.15) # a single ordinary zoom-in step
assert_str(v._window_request.get_granularity_v2()).override_failure_message(
"a single ordinary zoom-in step must not cross a rung threshold"
).is_equal("District")
## **Live round 3 regression, the direct end-to-end fix target:** a real
## wheel-zoom gesture (many `_zoom_at()` ticks, matching the shape a
## continuous mouse-wheel scroll actually produces) crossing from the
## Region rest state down through District into Quarter territory must (i)
## fire a request at the NEW granularity — `_window_request.get_granularity_v2()`
## must have changed by the end of the gesture — and (ii) accept a
## WIRE-ACCURATE response for that request: echoing the REQUEST's own
## (already re-centered, already re-clamped) center/n, which the live round
## found DIFFERS from the ORIGINAL held center (screen-center-anchored
## refinement re-centers on wherever the cursor currently maps to, not
## wherever the player started) — this is the "second latent drop" the
## coordinator specifically flagged: comparing the echo against a STALE
## `_held_center` (frozen at the pre-crossing value) rather than the
## request's own center would silently drop this response too.
## **Live round 3 update:** GJ380c/Lendel now enters TILE mode via
## enter_orbital() (bug B's fix), so this test starts from THERE — zooming
## in far enough crosses Region's coverage ceiling and must LEAVE tile mode
## for the single-window path at the new (finer) rung, exactly the
## `_maybe_reselect_rung()` "leaving_tile_mode" branch this test exercises.
func test_zoom_crossing_fires_request_and_accepts_wire_accurate_refinement() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).override_failure_message(
"GJ380c/Lendel must enter tile mode at the orbital rest state (live round 3)"
).is_true()
# A real wheel-zoom gesture: many ticks, cursor OFF-CENTER (so cursor-
# anchored zoom genuinely drifts the screen-to-district mapping away from
# the canonical origin, not just scaling in place) — matching the live
# drive's actual input shape, not a single synthetic jump. Zooming in far
# enough must cross OUT of Region's coverage ceiling, leaving tile mode.
var cursor_pos := Vector2(1100.0, 300.0) # off-center, biased toward one quadrant
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
# (i) Tile mode must have been LEFT, and a request must have gone out at
# a NEW (finer) granularity via the single-window path.
assert_bool(v.is_tile_mode()).override_failure_message(
"zooming in far enough must leave tile mode for the single-window path"
).is_false()
var request_granularity: String = v._window_request.get_granularity_v2()
assert_str(request_granularity).override_failure_message(
"leaving tile mode must fire a request at a new (finer) granularity"
).is_not_equal("Region")
# (ii) The request's own center/n — read AFTER leaving tile mode, so this
# is whatever _maybe_reselect_rung() actually computed — is what a
# wire-accurate response must echo to be accepted.
var request_center: Vector2i = v._window_request._center
var request_n: int = v._window_request._n
var refinement_window: Dictionary = {
"center": [request_center.x, request_center.y],
"n": request_n,
"granularity_v2": request_granularity,
"morphology": PackedByteArray([1, 2, 3, 4]),
"elev_q": PackedByteArray([10, 20, 30, 40]),
"temp_dc": [0, 0, 0, 0],
"moisture_q": PackedByteArray([0, 0, 0, 0]),
"vegetation": PackedByteArray([0, 0, 0, 0]),
"glaciation": PackedByteArray([0, 0, 0, 0]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", refinement_window))
assert_that(v.get_district_window()).override_failure_message(
"a wire-accurate refinement response (echoing the REQUEST's own center/n/"
+ " granularity after leaving tile mode) must be ACCEPTED — comparing"
+ " against a stale/wrong reference instead of the request's own would"
+ " silently drop this response forever"
).is_equal(refinement_window)
assert_str(v._held_granularity_v2).is_equal(request_granularity)
## Live round 4's SECOND bug, pinned directly: `_maybe_reselect_rung()` must
## recompute `_view_offset` (via AtlasWindowGeometry.
## recompute_offset_for_held_n_change()) the instant `_held_n` changes across
## a rung crossing — leaving it untouched (the round-4 bug) means the single-
## window `Rect2(0,0,extent)` draw call renders at whatever screen position
## the OLD (Region-scale) offset happened to put canvas-local (0,0), which
## for a whole-body `held_n` vs. a 64-district District `held_n` is tens or
## hundreds of thousands of px away from the viewport — the exact "pitch
## black" repro. Asserts the NEW held window's own extent actually overlaps
## the viewport after the crossing, the concrete on-screen consequence a
## stale offset breaks.
func test_zoom_crossing_recomputes_view_offset_so_the_new_window_is_on_screen() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(1600.0, 900.0)
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_bool(v.is_tile_mode()).is_true()
var cursor_pos := Vector2(1100.0, 300.0)
for _i in range(60):
v._zoom_at(cursor_pos, 1.15)
if not v.is_tile_mode():
break
assert_bool(v.is_tile_mode()).override_failure_message(
"sanity: this test needs to actually cross out of tile mode to exercise"
+ " the held_n change _maybe_reselect_rung() must react to"
).is_false()
# The new (post-crossing) window's screen-space rect, using the SAME
# formula the overlay's single-window _draw() itself uses
# (Rect2(0,0,extent,extent) in canvas-local space, then _canvas's own
# position/scale transform — _view_offset/_view_zoom here mirror that
# exactly, since _apply_transform() is what sets _canvas.position/scale).
var extent_screen: float = float(v._held_n) * v.CELL_PIXEL_SIZE * v._view_zoom
var screen_top_left: Vector2 = v._view_offset
var screen_bottom_right: Vector2 = screen_top_left + Vector2(extent_screen, extent_screen)
var viewport_rect := Rect2(Vector2.ZERO, v.size)
var window_rect := Rect2(screen_top_left, Vector2(extent_screen, extent_screen))
assert_bool(viewport_rect.intersects(window_rect)).override_failure_message(
(
"the new (post-crossing) held window's screen rect %s must overlap the"
+ " viewport %s — a stale _view_offset (never recomputed for the new"
+ " held_n=%d) is exactly live round 4's 'pitch black' bug: the composite"
+ " renders somewhere entirely off-canvas despite request/response/data"
+ " all being individually correct"
)
% [window_rect, viewport_rect, v._held_n]
).is_true()
# =============================================================================
# T-1153: E/W wrap and pole-wall clamps at EVERY rung — both are extent-
# relative (CELL_PIXEL_SIZE-based district-space math, unchanged regardless
# of which rung's data is actually held), so they must keep working
# unmodified at Region granularity, not just District/Quarter.
# =============================================================================
## The pole wall, wired through the real _apply_pan_delta() path, must still
## clamp at Region granularity — same mechanism as the existing District-rung
## test (test_wasd_pan_is_clamped_by_the_pole_wall_when_wired), just entered
## via enter_orbital() instead of enter().
func test_pole_wall_clamps_at_region_granularity_too() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
var radius_km := 50.0 # tiny synthetic body — pole wall reachable by an ordinary tick
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
var unclamped_magnitude: float = 500.0 * AtlasWindowViewer.PAN_SPEED_CANVAS_PX_S * v.get_view_zoom()
v._apply_pan_delta(Vector2(0.0, -1.0), 500.0) # "W"/north held, an absurdly long tick
assert_float(absf(v.get_view_offset().y)).override_failure_message(
"the pole wall must still clamp an extreme pan at Region granularity"
).is_less(unclamped_magnitude * 0.5)
## East-west wrap (canonicalize_district_center()) must still apply to the
## pan-edge refloat's resulting center at Region granularity — a pan that
## carries the screen-center column past the body's circumference must wrap
## into [0, cols), never run away to an out-of-range column, exactly as the
## District-rung wrap tests already pin (T-1142 item 6a). At Region's own
## enormous held_n (a whole circumference), an ORDINARY pan tick's
## canvas-space delta is negligible relative to the window's half-extent
## (confirmed: ~0.08 districts per 5-second tick vs. a ~9,772-district
## half-window) — so this drives _maybe_refloat_window() DIRECTLY off a
## manually-set _view_offset large enough to genuinely cross the held
## window's edge, the same "exercise the actual edge-crossing branch, not
## just its no-op early-return" discipline _maybe_refloat_window()'s own
## inside-check comment describes.
func test_pan_edge_refloat_wraps_columns_at_region_granularity_too() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
v.size = Vector2(800.0, 800.0)
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
assert_str(v._held_granularity_v2).is_equal("Region")
# Force the held center to sit one column short of the wrap seam, then
# shift the CANVAS offset by more than half the window's own on-screen
# extent — enough to move the screen-center's mapped column past the
# window's far edge (i.e. past `cols`, crossing the seam) regardless of
# Region's huge held_n.
v._held_center = Vector2i(cols - 1, 0)
var half_window_screen_px: float = float(v._held_n) * v.get_cell_pixel_size() * v.get_view_zoom() * 0.5
v._view_offset = v.get_view_offset() - Vector2(half_window_screen_px * 1.5, 0.0)
v._maybe_refloat_window()
assert_int(v._held_center.x).override_failure_message(
"a pan crossing the antimeridian at Region granularity must wrap the"
+ " resulting center into [0, cols), never run past cols"
).is_less(cols)
assert_int(v._held_center.x).is_greater_equal(0)
-90
View File
@@ -1,90 +0,0 @@
## PR #192 cold-start dossier (BUG 2): RegionalScreen.enter() tests. Split
## into its own file rather than folded into test_atlas_zoom_ladder.gd —
## these exercise the NAV-LAYER re-entry guard (RegionalScreen itself), not
## AtlasWindowViewer's own zoom-ladder mechanics that suite already owns.
class_name TestRegionalScreen
extends GdUnitTestSuite
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
## Dudley's WINDOW_GRANULARITY_REGION_KEY sentinel — mirrors
## test_atlas_zoom_ladder.gd's own constant (see that file's doc for why the
## real wire value matters, not a convenient placeholder).
const SERVER_LEGACY_GRANULARITY_REGION_SENTINEL: int = 4294967295
static func _mock_response(body_id: String, window: Variant) -> Dictionary:
return {"body_id": body_id, "status": "Ready", "district_window": window}
## BUG 2 root cause: ImplantApp._on_screen_changed() calls enter()
## UNCONDITIONALLY on every screen_changed, including a repeat
## nav.push("regional", ...) landing on the SAME screen already showing
## (reachable from more than one input path — body-click, panel+Enter — and
## plausible for a player to trigger twice on a slow cold server before the
## first descent settles). Without RegionalScreen's own guard, a repeat
## entry re-ran the FULL enter_orbital() teardown/rebuild — tearing down
## every in-flight tile request node and rebuilding fresh (null-window) ones
## — orphaning whatever had already arrived, plus refreshing the legend
## from scratch each time (the confirmed ~10x legend stack). Proven here by
## an ARRIVED tile's data: a teardown+rebuild resets it to null; a genuine
## no-op leaves it exactly as it was — `is_same()` on `_tile_set` itself
## can't tell (that Node is a fixed field, never reassigned — only its
## INTERNAL request children get torn down and rebuilt).
func test_repeat_enter_for_the_same_body_does_not_orphan_an_arrived_tile() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
var body: Dictionary = {"body_id": "GJ380c", "body_radius_km": 6238.4}
screen.enter({"body": body, "system": {}})
var tile_set = screen._viewer.get_tile_set()
var first_tile_center: Vector2i = tile_set.get_tiles()[0]["center"]
var arrived_window: Dictionary = {
"center": [first_tile_center.x, first_tile_center.y],
"n": AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION,
"granularity": SERVER_LEGACY_GRANULARITY_REGION_SENTINEL,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", arrived_window))
assert_that(tile_set.get_tiles()[0]["window"]).override_failure_message(
"sanity: the first tile's response must have been adopted before the repeat enter()"
).is_equal(arrived_window)
screen.enter({"body": body, "system": {}})
assert_that(screen._viewer.get_tile_set().get_tiles()[0]["window"]).override_failure_message(
"a repeat enter() for the SAME body must not orphan an already-arrived"
+ " tile — a teardown+rebuild resets every tile's window back to null,"
+ " which is the confirmed source of the cold-start legend stacking bug"
).is_equal(arrived_window)
## A GENUINE body change (different body_id) must still enter fresh — the
## guard is scoped to "the same body, re-entered", never a blanket "ignore
## the second enter() call ever".
func test_enter_for_a_different_body_still_re_enters() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
screen.enter({"body": {"body_id": "GJ380c", "body_radius_km": 6238.4}, "system": {}})
screen.enter({"body": {"body_id": "OtherBody", "body_radius_km": 100.0}, "system": {}})
assert_str(screen._viewer.get_body_id()).override_failure_message(
"a genuinely different body must still re-enter — not be swallowed by"
+ " the same-body guard"
).is_equal("OtherBody")
## get_body_id() itself: empty before any entry, the entered body's id after.
func test_get_body_id_reflects_the_currently_held_body() -> void:
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
add_child(v)
assert_str(v.get_body_id()).is_equal("")
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_str(v.get_body_id()).is_equal("GJ380c")
@@ -0,0 +1,76 @@
## T-1182 tests: StepCanvasAnnotationLayer — the unscaled screen-space
## sibling's world->screen placement math (course polylines, settlement
## markers) and course visibility/terminus handling. Draw-call correctness
## itself needs a live render pass (this cluster's existing "state-level is
## fine" allowance, per test_atlas_descend_entry.gd's own precedent) — these
## tests pin the FRAME state (_world_to_local, _cell_center_world_m) a draw
## call would read from, without requiring a SubViewport.
class_name TestStepCanvasAnnotationLayer
extends GdUnitTestSuite
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
func test_set_frame_stores_the_frame_and_triggers_no_crash_on_draw() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var canvas := {
"width": 4,
"height": 4,
"courses": [],
"settlement_id": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
}
layer.set_frame(canvas, Vector2(1000.0, 2000.0), "District", Vector2i(4, 4))
# No assertion beyond "did not crash" — set_frame()/queue_redraw() with a
# well-formed empty-feature canvas is the baseline no-op path every
# richer test below builds on.
assert_object(layer).is_not_null()
func test_clear_frame_drops_the_held_canvas() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame({"width": 1, "height": 1, "courses": []}, Vector2.ZERO, "Chunk", Vector2i(1, 1))
layer.clear_frame()
assert_that(layer._canvas).is_null()
## _cell_center_world_m() is the inverse of step_canvas.rs's own per-cell
## placement (center_world_m + (col - half_w) * step_m) — a settlement id
## read from cell (col, row) must map back to the world point that cell was
## actually derived at.
func test_cell_center_world_m_matches_the_servers_own_per_cell_placement() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame({"width": 4, "height": 4, "courses": []}, Vector2(0.0, 0.0), "District", Vector2i(4, 4))
# half_w = half_h = 2; spacing = 2048. Cell (0,0) -> (0-2)*2048 = -4096 on
# both axes; cell (2,2) (the center-ish cell) -> (2-2)*2048 = 0.
assert_that(layer._cell_center_world_m(0, 0)).is_equal(Vector2(-4096.0, -4096.0))
assert_that(layer._cell_center_world_m(2, 2)).is_equal(Vector2.ZERO)
func test_cell_center_world_m_offsets_by_the_frames_world_center() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame(
{"width": 2, "height": 2, "courses": []}, Vector2(10_000.0, 20_000.0), "Chunk", Vector2i(2, 2)
)
# half_w = half_h = 1; spacing = 64. Cell (1,1) -> center + (1-1)*64 = center.
assert_that(layer._cell_center_world_m(1, 1)).is_equal(Vector2(10_000.0, 20_000.0))
## _world_to_local() delegates to StepCanvasTransport.world_m_to_canvas_local
## with the layer's OWN held frame — this pins that the layer actually reads
## its stored _world_center/_rung/_extent_cells, not stale defaults.
func test_world_to_local_uses_the_held_frame() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var world_center := Vector2(5_000.0, -3_000.0)
var extent := Vector2i(32, 32)
layer.set_frame({"width": 32, "height": 32, "courses": []}, world_center, "Quarter", extent)
var expected: Vector2 = StepCanvasTransport.world_m_to_canvas_local(
world_center, world_center, "Quarter", extent
)
assert_that(layer._world_to_local(world_center)).is_equal_approx(expected, Vector2(0.01, 0.01))
+123
View File
@@ -0,0 +1,123 @@
## T-1182 tests: step_canvas_cache.gd — the client-side in-memory LRU cache
## for decoded step-canvas payloads. Keyed on (body_id, rung, center, extent,
## min_wl_m), the exact tuple server/src/atlas/step_canvas.rs's own
## StepCanvasCache keys on. Mirrors test_atlas_window_cache.gd's own
## conventions (the surviving LRU shape this file is adapted from).
class_name TestStepCanvasCache
extends GdUnitTestSuite
const StepCanvasCache := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_cache.gd")
func test_make_key_distinguishes_body_rung_center_and_extent() -> void:
var k1 := StepCanvasCache.make_key("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))
var k2 := StepCanvasCache.make_key("GJ1d", "District", Vector2i(10, 20), Vector2i(64, 64))
var k3 := StepCanvasCache.make_key("GJ1c", "Chunk", Vector2i(10, 20), Vector2i(64, 64))
var k4 := StepCanvasCache.make_key("GJ1c", "District", Vector2i(11, 20), Vector2i(64, 64))
var k5 := StepCanvasCache.make_key("GJ1c", "District", Vector2i(10, 20), Vector2i(32, 32))
assert_str(k1).is_not_equal(k2)
assert_str(k1).is_not_equal(k3)
assert_str(k1).is_not_equal(k4)
assert_str(k1).is_not_equal(k5)
## Global collapses center/extent to a fixed sentinel regardless of what's
## passed — every Global request for the same body_id must land on ONE slot.
func test_make_key_global_ignores_center_and_extent() -> void:
var k1 := StepCanvasCache.make_key("GJ1c", "Global", Vector2i(10, 20), Vector2i(64, 64))
var k2 := StepCanvasCache.make_key("GJ1c", "Global", Vector2i(999, -999), Vector2i(1, 1))
assert_str(k1).is_equal(k2)
func test_miss_returns_null_and_has_reports_false() -> void:
var cache := StepCanvasCache.new()
assert_that(cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_null()
assert_bool(cache.has("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_false()
func test_put_then_get_round_trips_exact_canvas() -> void:
var cache := StepCanvasCache.new()
var canvas := {"width": 64, "height": 64}
cache.put("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64), canvas)
assert_bool(cache.has("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))).is_true()
assert_that(cache.get_canvas("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))).is_equal(
canvas
)
## D-227: a canvas fetched once is valid FOREVER for that exact key — no
## expiry, no invalidation path.
func test_cached_canvas_never_expires() -> void:
var cache := StepCanvasCache.new()
var canvas := {"width": 1}
cache.put("GJ1c", "Chunk", Vector2i.ZERO, Vector2i(1, 1), canvas)
for _i in range(50):
assert_that(cache.get_canvas("GJ1c", "Chunk", Vector2i.ZERO, Vector2i(1, 1))).is_equal(canvas)
func test_different_rungs_at_identical_center_extent_do_not_collide() -> void:
var cache := StepCanvasCache.new()
var district_canvas := {"rung": "District"}
var chunk_canvas := {"rung": "Chunk"}
cache.put("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64), district_canvas)
cache.put("GJ1c", "Chunk", Vector2i(10, 20), Vector2i(64, 64), chunk_canvas)
assert_int(cache.size()).is_equal(2)
assert_that(cache.get_canvas("GJ1c", "District", Vector2i(10, 20), Vector2i(64, 64))).is_equal(
district_canvas
)
assert_that(cache.get_canvas("GJ1c", "Chunk", Vector2i(10, 20), Vector2i(64, 64))).is_equal(
chunk_canvas
)
func test_put_overwrites_existing_key() -> void:
var cache := StepCanvasCache.new()
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 1})
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"v": 2})
assert_int(cache.size()).is_equal(1)
assert_that(cache.get_canvas("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_equal(
{"v": 2}
)
func test_eviction_drops_least_recently_used_on_overflow() -> void:
var cache := StepCanvasCache.new(2)
cache.put("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64), {"id": "a"})
cache.put("GJ1c", "District", Vector2i(1, 0), Vector2i(64, 64), {"id": "b"})
cache.put("GJ1c", "District", Vector2i(2, 0), Vector2i(64, 64), {"id": "c"})
assert_int(cache.size()).is_equal(2)
assert_bool(cache.has("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))).override_failure_message(
"oldest entry should have been evicted"
).is_false()
assert_bool(cache.has("GJ1c", "District", Vector2i(2, 0), Vector2i(64, 64))).is_true()
func test_get_touches_entry_and_protects_it_from_eviction() -> void:
var cache := StepCanvasCache.new(2)
cache.put("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64), {"id": "a"})
cache.put("GJ1c", "District", Vector2i(1, 0), Vector2i(64, 64), {"id": "b"})
cache.get_canvas("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
cache.put("GJ1c", "District", Vector2i(2, 0), Vector2i(64, 64), {"id": "c"})
assert_bool(cache.has("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))).override_failure_message(
"touched entry should survive eviction"
).is_true()
assert_bool(cache.has("GJ1c", "District", Vector2i(1, 0), Vector2i(64, 64))).override_failure_message(
"untouched entry should be the one evicted"
).is_false()
func test_max_entries_clamped_to_at_least_one() -> void:
var cache := StepCanvasCache.new(0)
cache.put("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64), {"id": "a"})
cache.put("GJ1c", "District", Vector2i(1, 0), Vector2i(64, 64), {"id": "b"})
assert_int(cache.size()).is_equal(1)
func test_clear_empties_the_cache() -> void:
var cache := StepCanvasCache.new()
cache.put("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64), {"id": "a"})
cache.clear()
assert_int(cache.size()).is_equal(0)
assert_bool(cache.has("GJ1c", "District", Vector2i.ZERO, Vector2i(64, 64))).is_false()
+128
View File
@@ -0,0 +1,128 @@
## T-1182 tests: step_canvas_colorize.gd — per-cell colorize mapping (c1
## ruling: CPU Image.set_pixel, reusing AtlasOverlayColors' existing ramp
## functions verbatim). Builds small synthetic L8 Images directly (no PNG
## decode round-trip needed here — that's step_canvas_terrain_layer's own
## job, covered separately) to pin the CellPlanes -> Color mapping in
## isolation.
class_name TestStepCanvasColorize
extends GdUnitTestSuite
const StepCanvasColorize := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd")
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
static func _l8_image(values: Array, width: int, height: int) -> Image:
var img := Image.create(width, height, false, Image.FORMAT_L8)
for i in range(values.size()):
var col: int = i % width
var row: int = i / width
var v: float = float(values[i]) / 255.0
img.set_pixel(col, row, Color(v, v, v, 1.0))
return img
static func _planes(
morphology: Array, elev_q: Array, moisture_q: Array, vegetation: Array, glaciation: Array,
temp_dc: Array, width: int, height: int
) -> StepCanvasColorize.CellPlanes:
var p := StepCanvasColorize.CellPlanes.new()
p.width = width
p.height = height
p.morphology = _l8_image(morphology, width, height)
p.elev_q = _l8_image(elev_q, width, height)
p.moisture_q = _l8_image(moisture_q, width, height)
p.vegetation = _l8_image(vegetation, width, height)
p.glaciation = _l8_image(glaciation, width, height)
p.temp_dc = temp_dc
return p
func test_base_color_reads_morphology_and_elevation_together() -> void:
var planes := _planes([8], [50], [0], [0], [0], [], 1, 1)
var got: Color = StepCanvasColorize.cell_color(planes, 0, 0, "")
var base: Color = AtlasOverlayColors.district_window_morphology_color(8)
var expected: Color = AtlasOverlayColors.district_window_elevation_lightness(base, 50)
_assert_color_approx(got, expected)
func test_temp_toggle_replaces_base_reading_entirely() -> void:
var planes := _planes([8], [50], [0], [0], [0], [120], 1, 1)
var got: Color = StepCanvasColorize.cell_color(planes, 0, 0, StepCanvasColorize.TOGGLE_TEMP)
var expected: Color = AtlasOverlayColors.region_temp_color(120)
_assert_color_approx(got, expected)
func test_temp_toggle_airless_sentinel_is_transparent() -> void:
var planes := _planes([8], [50], [0], [0], [0], [-32768], 1, 1)
var got: Color = StepCanvasColorize.cell_color(planes, 0, 0, StepCanvasColorize.TOGGLE_TEMP)
assert_that(got).is_equal(Color.TRANSPARENT)
func test_moisture_toggle_ramps_dry_to_wet() -> void:
var planes := _planes([8], [50], [100], [0], [0], [], 1, 1)
var got: Color = StepCanvasColorize.cell_color(planes, 0, 0, StepCanvasColorize.TOGGLE_MOISTURE)
_assert_color_approx(got, StepCanvasColorize.COLOR_MOISTURE_WET)
func test_vegetation_toggle_marine_is_transparent() -> void:
var planes := _planes([8], [50], [0], [6], [0], [], 1, 1) # VEGETATION_MARINE = 6
var got: Color = StepCanvasColorize.cell_color(planes, 0, 0, StepCanvasColorize.TOGGLE_VEGETATION)
assert_that(got).is_equal(Color.TRANSPARENT)
func test_glaciation_modifier_tints_the_base_layer() -> void:
var planes_no_ice := _planes([8], [50], [0], [0], [0], [], 1, 1)
var planes_ice_cap := _planes([8], [50], [0], [0], [4], [], 1, 1) # grade 4 = ice cap
var base_color: Color = StepCanvasColorize.cell_color(planes_no_ice, 0, 0, "")
var tinted: Color = StepCanvasColorize.cell_color(planes_ice_cap, 0, 0, "")
assert_that(tinted).is_not_equal(base_color)
func test_glaciation_grade_zero_is_a_no_op() -> void:
var planes := _planes([8], [50], [0], [0], [0], [], 1, 1)
var base_color: Color = AtlasOverlayColors.district_window_elevation_lightness(
AtlasOverlayColors.district_window_morphology_color(8), 50
)
var got: Color = StepCanvasColorize.cell_color(planes, 0, 0, "")
_assert_color_approx(got, base_color)
func test_null_plane_falls_back_to_zero_rather_than_crashing() -> void:
var p := StepCanvasColorize.CellPlanes.new()
p.width = 1
p.height = 1
p.morphology = null
p.elev_q = null
p.moisture_q = null
p.vegetation = null
p.glaciation = null
p.temp_dc = []
# Must not crash — morphology/elev_q both read as 0 (OpenOcean, elev 0).
var got: Color = StepCanvasColorize.cell_color(p, 0, 0, "")
assert_that(got).is_equal(
AtlasOverlayColors.district_window_elevation_lightness(
AtlasOverlayColors.district_window_morphology_color(0), 0
)
)
func test_out_of_bounds_cell_index_returns_zero_not_a_crash() -> void:
var planes := _planes([8], [50], [0], [0], [0], [], 1, 1)
var got: Color = StepCanvasColorize.cell_color(planes, 99, 99, "")
assert_that(got).is_equal(
AtlasOverlayColors.district_window_elevation_lightness(
AtlasOverlayColors.district_window_morphology_color(0), 0
)
)
## gdUnit4's generic assert_that() has no Color-typed is_equal_approx() —
## per-channel float comparison, matching test_atlas_window_colors.gd's own
## _assert_color_approx() precedent (this is a deliberate duplicate, same
## rationale that file's own header gives for its palette-constant copies:
## each file owns its own small pure helper rather than a cross-suite import).
func _assert_color_approx(actual: Color, expected: Color) -> void:
assert_float(actual.r).is_equal_approx(expected.r, 0.01)
assert_float(actual.g).is_equal_approx(expected.g, 0.01)
assert_float(actual.b).is_equal_approx(expected.b, 0.01)
assert_float(actual.a).is_equal_approx(expected.a, 0.01)
+212
View File
@@ -0,0 +1,212 @@
## T-1182 tests: StepCanvasRequest/StepCanvasResponse wire codec
## (step_canvas_protocol.gd, delegated via protocol.gd) — the D-255(c)
## tagged-envelope carrier. Mirrors test_atlas_data_delivery.gd's own
## encode/decode round-trip + decode_inbound classification conventions.
class_name TestStepCanvasProtocol
extends GdUnitTestSuite
# =============================================================================
# Encode
# =============================================================================
func test_encode_step_canvas_request_carries_discriminator_and_fields() -> void:
var bytes := Protocol.encode_step_canvas_request(
"GJ380c", "District", Vector2i(10, 20), Vector2i(64, 64), 512
)
assert_int(bytes.size()).is_greater(0)
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status == null).is_true()
var raw: Dictionary = decoded.value
assert_bool(raw.get("step_canvas")).is_true()
assert_str(raw.get("body_id")).is_equal("GJ380c")
assert_str(raw.get("rung")).is_equal("District")
assert_that(raw.get("center")).is_equal([10, 20])
assert_that(raw.get("extent")).is_equal([64, 64])
assert_int(raw.get("min_wl_m")).is_equal(512)
## The rung is sent as a bare string (rmp_serde's unit-variant convention),
## never a raw integer — a Global request must carry the literal tag
## "Global", matching step_canvas.rs's own StepCanvasRung enum encoding.
func test_encode_step_canvas_request_global_rung_is_bare_string() -> void:
var bytes := Protocol.encode_step_canvas_request(
"GJ380c", "Global", Vector2i.ZERO, Vector2i.ZERO, 0
)
var decoded = Messagepack.decode(bytes)
assert_str(decoded.value.get("rung")).is_equal("Global")
# =============================================================================
# Decode — status variants
# =============================================================================
func test_step_canvas_response_from_raw_decodes_ready_status() -> void:
var raw := {
"body_id": "GJ380c",
"rung": "District",
"center": [10, 20],
"extent": [64, 64],
"min_wl_m": 0,
"status": "Ready",
"canvas": null,
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
assert_str(decoded["status"]).is_equal("Ready")
assert_str(decoded["error"]).is_equal("")
assert_str(decoded["rung"]).is_equal("District")
assert_that(decoded["center"]).is_equal(Vector2i(10, 20))
assert_that(decoded["extent"]).is_equal(Vector2i(64, 64))
func test_step_canvas_response_from_raw_decodes_pending_status() -> void:
var raw := {"body_id": "GJ380c", "rung": "Chunk", "status": "Pending"}
var decoded = Protocol.step_canvas_response_from_raw(raw)
assert_str(decoded["status"]).is_equal("Pending")
assert_that(decoded["canvas"]).is_null()
func test_step_canvas_response_from_raw_decodes_error_status() -> void:
var raw := {
"body_id": "GJ380c", "rung": "Region", "status": {"Error": "no heightmap"}
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
assert_str(decoded["status"]).is_equal("Error")
assert_str(decoded["error"]).is_equal("no heightmap")
## Not a step-canvas response (no "rung" key) -> null, so decode_inbound's
## dispatch doesn't misroute a plain AtlasLayerResponse here.
func test_step_canvas_response_from_raw_returns_null_without_rung_key() -> void:
var raw := {"body_id": "GJ380c", "status": "Ready"}
assert_that(Protocol.step_canvas_response_from_raw(raw)).is_null()
# =============================================================================
# decode_inbound classification — the "rung" discriminator must win BEFORE
# the generic "status"-only AtlasLayerResponse fallback (a step-canvas
# response also carries "status").
# =============================================================================
func test_decode_inbound_classifies_step_canvas() -> void:
var raw := {"body_id": "GJ380c", "rung": "District", "status": "Pending"}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound["kind"]).is_equal("step_canvas")
func test_decode_inbound_still_classifies_plain_atlas_response() -> void:
var raw := {"body_id": "GJ380c", "status": "Ready"}
var encoded = Messagepack.encode(raw)
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
assert_str(inbound["kind"]).is_equal("atlas")
# =============================================================================
# EncodedStepCanvas — the PNG-per-field array-of-int wire shape
# (step_canvas.rs's png_bytes: Vec<u8> with NO serde_bytes anywhere in this
# codebase serializes via serialize_seq, a msgpack ARRAY of ints, never a
# `bin` blob — decode_png_field()/_decode_encoded_canvas() must repack that
# Array into a PackedByteArray, not expect messagepack.gd's bin_8/16/32 path).
# =============================================================================
func test_decode_png_field_repacks_array_of_ints_to_packed_byte_array() -> void:
# A 1x1 all-black L8 PNG's real byte stream, as it would arrive already
# decoded off the wire (a plain Array of ints, one per byte) — using real
# PNG magic bytes so a downstream Image.load_png_from_buffer() call
# would also succeed, not just this repack step in isolation.
var png_bytes := PackedByteArray([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
var as_array: Array = []
for b in png_bytes:
as_array.append(b)
var field_raw := {"png_bytes": as_array}
var result: PackedByteArray = Protocol.step_canvas_response_from_raw(
{
"body_id": "GJ380c",
"rung": "Chunk",
"status": "Ready",
"canvas":
{
"width": 1,
"height": 1,
"morphology": field_raw,
"elev_q": {},
"temp_dc": {"values": []},
"moisture_q": {},
"vegetation": {},
"settlement_id": {"values": []},
"glaciation": {},
"flooded_q": {},
"courses": [],
"cliffs": [],
},
}
)["canvas"]["morphology"]
assert_that(result).is_equal(png_bytes)
func test_decode_encoded_canvas_passes_through_temp_dc_and_settlement_id_as_arrays() -> void:
var raw := {
"body_id": "GJ380c",
"rung": "Chunk",
"status": "Ready",
"canvas":
{
"width": 2,
"height": 1,
"morphology": {},
"elev_q": {},
"temp_dc": {"values": [120, -32768]},
"moisture_q": {},
"vegetation": {},
"settlement_id": {"values": [0, 7]},
"glaciation": {},
"flooded_q": {},
"courses": [],
"cliffs": [],
},
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
var canvas: Dictionary = decoded["canvas"]
assert_that(canvas["temp_dc"]).is_equal([120, -32768])
assert_that(canvas["settlement_id"]).is_equal([0, 7])
func test_decode_encoded_canvas_passes_through_courses_and_cliffs_unshaped() -> void:
var courses := [{"edge_id": 1, "class": 2, "points": [[0, 0], [100, 100]], "terminus": "Mouth"}]
var cliffs := [{"point": [5, 5], "channel_depth_dm": 10, "cliff_edge": true}]
var raw := {
"body_id": "GJ380c",
"rung": "Chunk",
"status": "Ready",
"canvas":
{
"width": 1,
"height": 1,
"morphology": {},
"elev_q": {},
"temp_dc": {"values": []},
"moisture_q": {},
"vegetation": {},
"settlement_id": {"values": []},
"glaciation": {},
"flooded_q": {},
"courses": courses,
"cliffs": cliffs,
},
}
var decoded = Protocol.step_canvas_response_from_raw(raw)
var canvas: Dictionary = decoded["canvas"]
assert_that(canvas["courses"]).is_equal(courses)
assert_that(canvas["cliffs"]).is_equal(cliffs)
func test_decode_png_field_malformed_input_returns_empty_packed_byte_array() -> void:
assert_that(Protocol._scp().decode_png_field(null)).is_equal(PackedByteArray())
assert_that(Protocol._scp().decode_png_field({"png_bytes": "not an array"})).is_equal(
PackedByteArray()
)
+208
View File
@@ -0,0 +1,208 @@
## T-1182 tests: step_canvas_request.gd — request lifecycle (cache hit/miss,
## staleness gate, the extent ECHO rule). Live mode is required for
## SimBridge.request_step_canvas() to actually send (test_mode short-circuits
## it), so these tests exercise on_response()/request_now() against a
## directly-constructed StepCanvasRequest node without a live bridge
## connection — request_now() on a cache MISS calls into SimBridge, which is
## a silent no-op in test_mode (SimBridge.test_mode defaults true outside
## SR_LIVE=1), so no live server is needed for these assertions.
class_name TestStepCanvasRequest
extends GdUnitTestSuite
const StepCanvasRequestScript := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_request.gd")
func test_cache_hit_emits_canvas_ready_synchronously_with_no_pending_state() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
var canvas := {"width": 64, "height": 64}
req.get_cache().put("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64), canvas)
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
assert_int(received.size()).is_equal(1)
assert_that(received[0]).is_equal(canvas)
assert_bool(req.is_pending()).is_false()
func test_cache_miss_sets_pending_true() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))
assert_bool(req.is_pending()).is_true()
func test_on_response_ignores_a_response_for_a_different_body() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "SomeOtherBody",
"rung": "District",
"center": Vector2i(0, 0),
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 1},
}
)
assert_int(received.size()).is_equal(0)
func test_on_response_ignores_a_response_for_a_different_rung() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "GJ1c",
"rung": "Chunk", # a different rung answering — must be ignored
"center": Vector2i(0, 0),
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 1},
}
)
assert_int(received.size()).is_equal(0)
func test_on_response_ignores_a_stale_center_for_a_fixed_rung() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "GJ1c",
"rung": "District",
"center": Vector2i(999, 999), # answers a since-panned-away-from center
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 1},
}
)
assert_int(received.size()).is_equal(0)
## Global ignores center/extent server-side — a Global response's staleness
## check must NOT compare center at all (an echoed (0,0) sentinel must not
## be rejected as "stale" against whatever was requested).
func test_on_response_global_rung_ignores_center_in_staleness_check() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "GJ1c",
"rung": "Global",
"center": Vector2i.ZERO,
"extent": Vector2i.ZERO,
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 19_139, "height": 9_569},
}
)
assert_int(received.size()).is_equal(1)
## The extent ECHO rule (T-1181 wire addendum, mandatory): held extent comes
## from the RESPONSE's own echoed extent, never the requested one — a
## server-side clamp can shrink the actual canvas below what was asked for.
func test_on_response_holds_the_echoed_extent_not_the_requested_one() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(9_999, 9_999))
req.on_response(
{
"body_id": "GJ1c",
"rung": "Chunk",
"center": Vector2i(0, 0),
"extent": Vector2i(3_840, 2_160), # server-clamped echo, smaller than requested
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 3_840, "height": 2_160},
}
)
assert_that(req.get_held_extent()).is_equal(Vector2i(3_840, 2_160))
## Global's held extent comes from the canvas's own width/height (the wire
## extent echo is a fixed (0,0) sentinel for that rung — nothing to read).
func test_on_response_global_held_extent_derives_from_canvas_dimensions() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "Global", Vector2i.ZERO, Vector2i.ZERO)
req.on_response(
{
"body_id": "GJ1c",
"rung": "Global",
"center": Vector2i.ZERO,
"extent": Vector2i.ZERO,
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 19_139, "height": 9_569},
}
)
assert_that(req.get_held_extent()).is_equal(Vector2i(19_139, 9_569))
func test_on_response_ready_with_null_canvas_retries_rather_than_adopting() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "District", Vector2i(0, 0), Vector2i(64, 64))
var received: Array = []
req.canvas_ready.connect(func(c: Dictionary) -> void: received.append(c))
req.on_response(
{
"body_id": "GJ1c",
"rung": "District",
"center": Vector2i(0, 0),
"min_wl_m": 0,
"status": "Ready",
"canvas": null,
}
)
assert_int(received.size()).is_equal(0)
assert_bool(req.is_pending()).is_true()
func test_on_response_not_found_gives_up_immediately() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "Chunk", Vector2i(0, 0), Vector2i(64, 64))
req.on_response({"body_id": "GJ1c", "rung": "Chunk", "status": "NotFound"})
assert_bool(req.is_pending()).is_false()
func test_on_response_stores_a_fresh_ready_canvas_in_the_cache() -> void:
var req = auto_free(StepCanvasRequestScript.new())
add_child(req)
req.request_now("GJ1c", "District", Vector2i(5, 5), Vector2i(64, 64))
req.on_response(
{
"body_id": "GJ1c",
"rung": "District",
"center": Vector2i(5, 5),
"extent": Vector2i(64, 64),
"min_wl_m": 0,
"status": "Ready",
"canvas": {"width": 64, "height": 64},
}
)
assert_bool(req.get_cache().has("GJ1c", "District", Vector2i(5, 5), Vector2i(64, 64))).is_true()
+190
View File
@@ -0,0 +1,190 @@
## T-1182 tests: step_canvas_transport.gd — the D-255(a) six-rung stepped
## transport state machine (rung ladder, cursor-anchored step math,
## viewport-fit extent, world<->canvas-local projection). All pure
## functions, no scene tree needed.
class_name TestStepCanvasTransport
extends GdUnitTestSuite
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
# =============================================================================
# Rung ladder — index <-> name, scroll clamping
# =============================================================================
func test_rung_at_index_zero_is_global() -> void:
assert_str(StepCanvasTransport.rung_at_index(0)).is_equal(StepCanvasTransport.RUNG_GLOBAL)
func test_rung_at_index_five_is_chunk_the_deepest() -> void:
assert_str(StepCanvasTransport.rung_at_index(5)).is_equal(StepCanvasTransport.RUNG_CHUNK)
func test_rung_at_index_clamps_out_of_range_indices() -> void:
assert_str(StepCanvasTransport.rung_at_index(-3)).is_equal(StepCanvasTransport.RUNG_GLOBAL)
assert_str(StepCanvasTransport.rung_at_index(99)).is_equal(StepCanvasTransport.RUNG_CHUNK)
func test_index_for_rung_round_trips_every_ladder_entry() -> void:
for i in range(StepCanvasTransport.RUNG_LADDER.size()):
var rung: String = StepCanvasTransport.rung_at_index(i)
assert_int(StepCanvasTransport.index_for_rung(rung)).is_equal(i)
func test_index_for_rung_unrecognized_returns_negative_one() -> void:
assert_int(StepCanvasTransport.index_for_rung("Sector")).is_equal(-1)
func test_scroll_step_descends_one_notch_at_a_time() -> void:
assert_int(StepCanvasTransport.scroll_step(0, 1)).is_equal(1)
assert_int(StepCanvasTransport.scroll_step(2, 1)).is_equal(3)
func test_scroll_step_ascends_one_notch_at_a_time() -> void:
assert_int(StepCanvasTransport.scroll_step(3, -1)).is_equal(2)
func test_scroll_step_clamps_at_the_deepest_rung() -> void:
assert_int(StepCanvasTransport.scroll_step(5, 1)).is_equal(5)
func test_scroll_step_clamps_at_the_global_opener() -> void:
assert_int(StepCanvasTransport.scroll_step(0, -1)).is_equal(0)
func test_scroll_step_zero_direction_is_a_no_op() -> void:
assert_int(StepCanvasTransport.scroll_step(2, 0)).is_equal(2)
# =============================================================================
# D-243 gridunit spacing — pinned against the same metre values scale.rs uses
# =============================================================================
func test_spacing_for_rung_matches_d243_metre_values() -> void:
assert_float(StepCanvasTransport.spacing_for_rung("Global")).is_equal_approx(204_800.0, 0.01)
assert_float(StepCanvasTransport.spacing_for_rung("Region")).is_equal_approx(204_800.0, 0.01)
assert_float(StepCanvasTransport.spacing_for_rung("District")).is_equal_approx(2_048.0, 0.01)
assert_float(StepCanvasTransport.spacing_for_rung("Quarter")).is_equal_approx(512.0, 0.01)
assert_float(StepCanvasTransport.spacing_for_rung("Block")).is_equal_approx(128.0, 0.01)
assert_float(StepCanvasTransport.spacing_for_rung("Chunk")).is_equal_approx(64.0, 0.01)
# =============================================================================
# Display ratio — deep/mid 1x1, shallow ~5x5, PRESENTATION only (D-255(a))
# =============================================================================
func test_display_ratio_deep_rungs_are_one_to_one() -> void:
for rung in ["District", "Quarter", "Block", "Chunk"]:
assert_float(StepCanvasTransport.display_ratio_for_rung(rung)).is_equal_approx(1.0, 0.001)
func test_display_ratio_shallow_rungs_use_the_five_x_five_fallback() -> void:
for rung in ["Global", "Region"]:
assert_float(StepCanvasTransport.display_ratio_for_rung(rung)).is_equal_approx(5.0, 0.001)
func test_is_orbital_rung_true_only_for_global_and_region() -> void:
assert_bool(StepCanvasTransport.is_orbital_rung("Global")).is_true()
assert_bool(StepCanvasTransport.is_orbital_rung("Region")).is_true()
assert_bool(StepCanvasTransport.is_orbital_rung("District")).is_false()
assert_bool(StepCanvasTransport.is_orbital_rung("Chunk")).is_false()
# =============================================================================
# Viewport-fit extent — the client half of "viewport-sized canvas"
# =============================================================================
func test_viewport_fit_extent_at_deep_ratio_matches_viewport_pixels() -> void:
# 1x1 ratio -> extent in gridunits == viewport px, 1:1.
var extent: Vector2i = StepCanvasTransport.viewport_fit_extent(Vector2(800.0, 600.0), "Chunk")
assert_that(extent).is_equal(Vector2i(800, 600))
func test_viewport_fit_extent_at_shallow_ratio_divides_by_the_display_ratio() -> void:
var extent: Vector2i = StepCanvasTransport.viewport_fit_extent(Vector2(1000.0, 500.0), "Region")
assert_that(extent).is_equal(Vector2i(200, 100))
func test_viewport_fit_extent_clamps_to_the_fixed_canvas_max_axis() -> void:
var extent: Vector2i = StepCanvasTransport.viewport_fit_extent(
Vector2(20_000.0, 20_000.0), "Chunk"
)
assert_int(extent.x).is_equal(StepCanvasTransport.FIXED_CANVAS_MAX_AXIS)
assert_int(extent.y).is_equal(StepCanvasTransport.FIXED_CANVAS_MAX_AXIS)
func test_viewport_fit_extent_never_produces_a_zero_axis() -> void:
var extent: Vector2i = StepCanvasTransport.viewport_fit_extent(Vector2(0.0, 0.0), "Chunk")
assert_int(extent.x).is_greater_equal(1)
assert_int(extent.y).is_greater_equal(1)
# =============================================================================
# Gridunit snapping — cache-key stability for repeated "same spot" requests
# =============================================================================
func test_snap_to_gridunit_snaps_to_the_rungs_own_spacing() -> void:
var snapped: Vector2i = StepCanvasTransport.snap_to_gridunit(Vector2(2100.0, -1000.0), "District")
# District spacing = 2048 m: 2100 rounds to 1*2048=2048, -1000 rounds to 0.
assert_int(snapped.x).is_equal(2048)
assert_int(snapped.y).is_equal(0)
func test_snap_to_gridunit_is_idempotent_once_already_on_grid() -> void:
var once: Vector2i = StepCanvasTransport.snap_to_gridunit(Vector2(4096.0, 6144.0), "District")
var world_again := Vector2(once.x, once.y)
var twice: Vector2i = StepCanvasTransport.snap_to_gridunit(world_again, "District")
assert_that(once).is_equal(twice)
# =============================================================================
# World <-> canvas-local projection — the shared transform both the terrain
# and annotation layers agree on by construction
# =============================================================================
func test_world_m_to_canvas_local_centers_the_world_center_on_the_canvas_center() -> void:
var extent := Vector2i(64, 64)
var rung := "District"
var world_center := Vector2(10_000.0, 20_000.0)
var local: Vector2 = StepCanvasTransport.world_m_to_canvas_local(
world_center, world_center, rung, extent
)
var expected_center: Vector2 = StepCanvasTransport.canvas_footprint_px(rung, extent) * 0.5
assert_that(local).is_equal_approx(expected_center, Vector2(0.01, 0.01))
## world_m_to_canvas_local() and canvas_local_to_world_m() must be exact
## inverses of one another — a round trip through both must recover the
## original world point (within float tolerance). This is the invariant the
## cursor-anchored scroll step depends on: whatever point the cursor reads
## as "under it" before a scroll must be the SAME point after re-deriving
## from the new step's own frame.
func test_world_to_local_and_back_round_trips() -> void:
var extent := Vector2i(128, 96)
var rung := "Quarter"
var world_center := Vector2(50_000.0, -30_000.0)
var original_world := Vector2(51_200.0, -29_500.0)
var local: Vector2 = StepCanvasTransport.world_m_to_canvas_local(
original_world, world_center, rung, extent
)
var recovered_world: Vector2 = StepCanvasTransport.canvas_local_to_world_m(
local, world_center, rung, extent
)
assert_that(recovered_world).is_equal_approx(original_world, Vector2(0.5, 0.5))
func test_canvas_footprint_px_is_extent_times_display_ratio() -> void:
var footprint: Vector2 = StepCanvasTransport.canvas_footprint_px("Region", Vector2i(100, 50))
assert_that(footprint).is_equal(Vector2(500.0, 250.0)) # 5x5 shallow ratio
func test_half_extent_m_is_half_the_cell_count_times_spacing() -> void:
var half: float = StepCanvasTransport.half_extent_m("District", 64)
assert_float(half).is_equal_approx(64.0 * 0.5 * 2048.0, 0.01)
+110
View File
@@ -0,0 +1,110 @@
## T-1182 tests: StepCanvasViewer — the rung transport state machine
## (enter() lands on the Global opener, scroll steps through the ladder,
## overlay toggle wiring) and RegionalScreen's re-entry guard against the
## new viewer. test_mode (SimBridge default outside SR_LIVE=1) means
## request_step_canvas() is a silent no-op — these tests exercise
## client-side state only, matching test_atlas_view_api.gd's own
## no-live-server convention for viewer-internals tests.
class_name TestStepCanvasViewer
extends GdUnitTestSuite
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
func test_enter_lands_on_the_global_opener() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
func test_get_body_id_reflects_the_entered_body() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
assert_str(v.get_body_id()).is_equal("")
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
assert_str(v.get_body_id()).is_equal("GJ380c")
## Scrolling one notch descends the ladder — cursor-anchored, so a cursor
## position must be supplied; the rung index advances by exactly one.
func test_scroll_rung_descends_one_notch() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._scroll_rung(1, Vector2(400.0, 300.0))
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
func test_scroll_rung_clamps_at_the_deepest_rung() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
for _i in range(10):
v._scroll_rung(1, Vector2(400.0, 300.0))
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_CHUNK)
func test_reset_to_global_returns_from_a_deep_rung() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.enter({"body_id": "GJ380c", "body_radius_km": 6238.4}, {})
v._scroll_rung(1, Vector2(400.0, 300.0))
v._scroll_rung(1, Vector2(400.0, 300.0))
v._reset_to_global()
assert_str(v.get_held_rung()).is_equal(StepCanvasTransport.RUNG_GLOBAL)
func test_overlay_visibility_defaults_to_off_for_every_toggle() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
for def: Dictionary in v.get_overlay_defs():
assert_bool(v.is_overlay_visible(def["id"])).is_false()
func test_set_overlay_visible_updates_state() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.set_overlay_visible("gen_dw_temp", true)
assert_bool(v.is_overlay_visible("gen_dw_temp")).is_true()
func test_set_overlay_visible_unknown_id_is_a_no_op() -> void:
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
add_child(v)
v.set_overlay_visible("not_a_real_overlay", true)
assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false()
# =============================================================================
# RegionalScreen re-entry guard (BUG 2 lineage, carried forward from the
# retired AtlasWindowViewer-era regression) — now against StepCanvasViewer.
# =============================================================================
func test_regional_screen_repeat_enter_for_the_same_body_is_a_no_op() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
var body: Dictionary = {"body_id": "GJ380c", "body_radius_km": 6238.4}
screen.enter({"body": body, "system": {}})
screen._viewer._scroll_rung(1, Vector2(400.0, 300.0))
assert_str(screen._viewer.get_held_rung()).is_equal(StepCanvasTransport.RUNG_REGION)
screen.enter({"body": body, "system": {}})
# A no-op re-entry must NOT reset the held rung back to Global — that
# would be the exact "repeat enter tears down in-flight state" class the
# retired viewer's own cold-start guard existed to prevent.
assert_str(screen._viewer.get_held_rung()).override_failure_message(
"a repeat enter() for the SAME body must not reset the held rung"
).is_equal(StepCanvasTransport.RUNG_REGION)
func test_regional_screen_different_body_still_re_enters() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
screen.enter({"body": {"body_id": "GJ380c", "body_radius_km": 6238.4}, "system": {}})
screen.enter({"body": {"body_id": "OtherBody", "body_radius_km": 100.0}, "system": {}})
assert_str(screen._viewer.get_body_id()).is_equal("OtherBody")
+4 -4
View File
@@ -76,10 +76,10 @@ func _unhandled_key_input(event: InputEvent) -> void:
return
if not event.is_pressed() or event.is_echo():
return
# "regional" (the whole zoom ladder, AtlasWindowViewer, T-1153) handles its
# own Esc via _gui_input — a stray M/other unhandled key on that screen
# must not ALSO fire this app's own _handle_key underneath the viewer's
# own handling.
# "regional" (the whole stepped zoom ladder, StepCanvasViewer, T-1182)
# handles its own Esc via _gui_input — a stray M/other unhandled key on
# that screen must not ALSO fire this app's own _handle_key underneath
# the viewer's own handling.
if current_screen_id() == "regional":
return
_handle_key(event as InputEventKey)
@@ -1,727 +0,0 @@
extends RefCounted
## Pure geometry helpers for AtlasWindowViewer's fit/pan transform (T-1142 —
## Jeroen's second hands-on finding: enter() reset zoom to 1.0/offset to ZERO
## with no fit, so an n=32 composite (512px native) rendered as a postage
## stamp in a ~1900px viewport). Factored out of atlas_window_viewer.gd for
## the same reason atlas_descend_geometry.gd was factored out of
## atlas_viewer.gd (T-1138): the actual _canvas.position/.scale WRITES stay on
## the viewer (Node-tree side effects), but the pure "given a viewport and a
## window size, what zoom/offset centers it" math is unit-testable in
## isolation here — a caller does:
## const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
##
## T-1153: also carries the rung-selection rule (design doc
## docs/architecture/atlas-zoom-ladder-t1143.md §5) and the "fully zoomed
## out" reset predicate (Jeroen's D-226 T-1143-rulings HARD condition) — both
## pure functions of (viewport, held state, body), same "geometry lives here,
## side effects live on the viewer" split as the rest of this file. Live
## round 3 also adds the orbital-rest-state TILE GRID computation
## (compute_tile_grid(), near the bottom) — reuses
## AtlasDescendGeometry.district_extent()/canonicalize_district_center() for
## the SAME wrap/clamp discipline every other piece of this cluster already
## depends on, hence the preload below (no circular dependency:
## atlas_descend_geometry.gd never references this file).
##
## T-1170: the T-1156 wave-1 nature-overlay (river/basin/attractor) pixel
## mapping and per-rung visibility policy (RIVER_CLASS_*, layer1_pixel_to_*,
## *_visible_at_rung, zoom_compensated_size) moved OUT of this file to
## atlas_window_geometry_nature.gd (this file was at 954/1000 gdlint
## max-file-lines when the move happened) — see that file's own header doc.
## cell_index_for_local_offset() (T-1172, near the bottom of this file) stayed
## here since it is shared with AtlasWindowOverlay's terrain painter, a
## non-nature consumer.
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## D-243 rung spacings, metres/cell — the SAME constants
## server/src/atlas/scale.rs and layer_proxy.rs's WindowGranularity::spacing_m
## source from, mirrored here rather than re-derived so the client's rung
## table can never silently drift from the wire contract it's choosing
## between.
const QUARTER_SPACING_M: float = 512.0
const DISTRICT_SPACING_M: float = 2048.0
const REGION_SPACING_M: float = 204_800.0
## Table form, coarsest-first — spacing_for_rung()'s inverse lookup walks
## this. select_rung() (below) does NOT walk this table directly — see that
## function's own doc for why the coarse (Region) and fine (District/
## Quarter) ends are decided by two DIFFERENT tests, not a single ordered
## table scan.
const RUNG_TABLE: Array = [
{"granularity_v2": "Region", "spacing_m": REGION_SPACING_M},
{"granularity_v2": "District", "spacing_m": DISTRICT_SPACING_M},
{"granularity_v2": "Quarter", "spacing_m": QUARTER_SPACING_M},
]
## Server per-axis cap on a District/Quarter-granularity window's `n`
## (mirrors server/src/atlas/layer_proxy.rs's `DISTRICT_WINDOW_MAX_N` — see
## AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N, the existing client-side
## mirror of the same constant, kept in sync there).
const DISTRICT_WINDOW_MAX_N: int = 64
## Wire-size ceiling (mirrors AtlasWindowRequest.SERVER_WIRE_CAP_CELLS /
## server/src/atlas/layer_proxy.rs's WIRE_CAP_CELLS) — the cell-count cap
## EVERY rung's single window is clamped against, per
## `_clamp_window_n_mirror`/`_clamp_window_n_mirror_v2`'s own formulas.
const WIRE_CAP_CELLS: int = 4_096
## Region's own per-axis ceiling (mirrors
## AtlasWindowRequest.SERVER_DISTRICT_WINDOW_MAX_N_REGION /
## server/src/atlas/layer_proxy.rs's DISTRICT_WINDOW_MAX_N_REGION).
const DISTRICT_WINDOW_MAX_N_REGION: int = 6_400
## Per-tile district extent for the orbital-rest-state tile grid
## (compute_tile_grid(), near the bottom of this file) — the SAME `n` a
## single Region request uses at its own per-axis ceiling. Each tile
## requests exactly this many districts on a side — the largest single
## window the wire budget allows, so tiling uses the FEWEST tiles that can
## cover a given body.
const TILE_N: int = DISTRICT_WINDOW_MAX_N_REGION
## **Live round 3 finding (the actual root cause of "zoom-driven rung
## reselection never fires"):** each rung's SINGLE WINDOW has a hard MAXIMUM
## real-world coverage, derived from the SAME wire-size clamp
## (`_clamp_window_n_mirror_v2`) the request layer already enforces — District
## and Quarter are NOT exempt from this the way the original (§5-literal)
## design assumed. A rung whose own single-window coverage is smaller than
## the CURRENTLY DISPLAYED world extent cannot legally be selected: the
## server would clamp `n` down to fit its own wire budget, producing a
## composite that covers only a FRACTION of the viewport — visually a tiny
## box in the middle of the screen, and (the bug this constant's discovery
## fixes) a composite whose CLAMPED `n` no longer matches whatever `_held_n`
## the viewer was still carrying from the PREVIOUS rung, permanently failing
## `_on_window_ready()`'s staleness check. Computed here ONCE, from the same
## constants `_clamp_window_n_mirror_v2` uses, rather than re-derived per
## rung inline — see MAX_COVERAGE_M below.
##
## - Quarter: per-axis cap `floor(sqrt(WIRE_CAP_CELLS)/4) = 16` districts ->
## cell-grid side `16*4 = 64` cells -> `64 * QUARTER_SPACING_M = 32,768 m`.
## - District: per-axis cap `floor(sqrt(WIRE_CAP_CELLS)/1) = 64` districts ->
## `64 * DISTRICT_SPACING_M = 131,072 m` (unchanged from the original
## coverage-ceiling constant this replaces/generalizes).
## - Region: per-axis cap `DISTRICT_WINDOW_MAX_N_REGION = 6,400` districts ->
## `6,400 * DISTRICT_SPACING_M = 13,107,200 m` — this is a SINGLE window's
## ceiling; bug B's progressive tiling composes MULTIPLE Region windows to
## cover extents beyond this (see the viewer's tile-set model), so this
## constant alone does NOT bound what the ORBITAL REST STATE can show —
## only what one Region REQUEST's response covers.
const MAX_COVERAGE_M: Dictionary = {
"Quarter": 64.0 * QUARTER_SPACING_M,
"District": float(DISTRICT_WINDOW_MAX_N) * DISTRICT_SPACING_M,
"Region": float(DISTRICT_WINDOW_MAX_N_REGION) * DISTRICT_SPACING_M,
}
## Rungs ordered FINEST-first — select_rung() walks this to find the finest
## rung whose own single-window coverage ceiling still covers the current
## extent (never a rung that would silently under-cover the viewport).
const RUNGS_FINEST_FIRST: Array = ["Quarter", "District", "Region"]
## Fit-and-center: given the viewport size and the window's side length in
## districts, compute the zoom/offset that COVERS the viewport (fills it edge
## to edge, no side margins) and centers the composite. Mirrors AtlasViewer's
## own _fit_to_view() shape (fit, then center) but as a pure function
## returning {zoom, offset} instead of writing _view_zoom/_view_offset
## directly, so AtlasWindowViewer.enter()/(_on_window_ready)/NOTIFICATION_RESIZED
## can all call the SAME formula without three copies of the math drifting.
##
## T-1145 item 1 (Jeroen's round-2 finding, KALLAST window): the ORIGINAL fit
## was CONTAIN (zoom from the SMALLER viewport dimension, with a 0.9 margin
## factor) — in a wide viewport this left large side margins around a square
## composite (the window data is always n x n, a square, regardless of
## viewport aspect). Changed to COVER: zoom from the LARGER viewport
## dimension, with NO margin factor — a margin on the CONTAIN axis (the one
## the zoom is computed from) is a deliberate breathing-room choice; the
## exact same margin on the COVER axis would be a literal gap at the
## viewport's own edge, which is precisely the "no side margins" defect this
## fix removes. The composite therefore fills the screen edge to edge on its
## long axis (scaled side == max(viewport.x, viewport.y) exactly) and
## overhangs past both edges on its short axis (exactly the same "cover"
## concept CSS background-size/object-fit use — fill the frame, crop what
## doesn't fit, never letterbox). This is honest for a square dataset in a
## non-square frame: at rest, the player sees a full-bleed slice of the
## window, and panning (T-1145 item 2: WASD/edge-scroll) reveals the rest,
## including triggering the existing pan-edge refetch (§4) exactly as
## intended — cover does not change what "past the window edge" means, only
## how much of the window is visible before the player pans at all.
##
## zoom = clampf(max(viewport.x, viewport.y) / (n * cell_px), MIN_ZOOM, MAX_ZOOM)
## offset centers the (n * cell_px * zoom)-sized composite on the viewport,
## exactly as the old contain fit did.
static func fit_window_view(
viewport: Vector2, n: int, cell_px: float, min_zoom: float, max_zoom: float
) -> Dictionary:
if n <= 0 or cell_px <= 0.0 or viewport.x <= 0.0 or viewport.y <= 0.0:
return {"zoom": 1.0, "offset": Vector2.ZERO}
var composite_native: float = float(n) * cell_px
var zoom: float = clampf(maxf(viewport.x, viewport.y) / composite_native, min_zoom, max_zoom)
var composite_scaled: Vector2 = Vector2(composite_native, composite_native) * zoom
var offset: Vector2 = (viewport - composite_scaled) * 0.5
return {"zoom": zoom, "offset": offset}
## T-1142 addendum (Jeroen — pole-wall ruling): the pan offset's Y component
## must never let the WINDOW EDGE (not merely the window center) cross the
## body's row extent — panning past a pole would ask the derive for rows
## beyond ±rows_half, which the server clamps (T-1142's own
## normalize_window_center) into a smeared repeated-clamped-latitude band,
## not real topology. The wall is therefore drawn at the edge of the ACTUAL
## valid row range, honestly reflecting "this is where the world ends", not
## an arbitrary UI limit.
##
## Inputs are all in the SAME units the caller's _view_offset/_view_zoom
## already use (canvas px = district cells * cell_px, screen px after zoom):
## `held_center`/`held_n` describe the currently-fetched window (its center
## district row and side length); `rows_half` is the body's half-meridian
## extent in districts (district_extent()'s "rows_half", i.e. equator-to-pole
## in whole districts); `cell_px`/`zoom` convert districts to screen pixels.
## Returns the Y-clamped offset — X is untouched (no wall on longitude, T-1142
## item 6: circumnavigation is seamless, only the row axis is a hard boundary).
static func clamp_pan_offset_to_pole_wall(
offset: Vector2,
view_size: Vector2,
held_center: Vector2i,
held_n: int,
rows_half: int,
cell_px: float,
zoom: float
) -> Vector2:
if rows_half <= 0 or held_n <= 0 or cell_px <= 0.0 or zoom <= 0.0:
return offset
# The held window spans districts [held_center.y - held_n/2, held_center.y
# + held_n/2) — its top/bottom edges in ABSOLUTE district-row space.
var half_n: float = float(held_n) * 0.5
var window_top_row: float = float(held_center.y) - half_n
var window_bottom_row: float = float(held_center.y) + half_n
# Canvas-space (pre-zoom) distance from the window's local origin (row 0
# of the held composite, i.e. window_top_row) to the pole boundary rows.
# A pole boundary that falls OUTSIDE the held window's own row span is not
# reachable by panning within this fetch at all (clampf below is then a
# no-op in that direction) — the wall only bites once a pan would expose
# rows the held window doesn't cover AND those rows would cross the pole.
var north_wall_local_row: float = float(-rows_half) - window_top_row
var south_wall_local_row: float = float(rows_half) - window_top_row
# Screen-space Y bound: offset.y is the screen position of canvas-Y=0
# (the composite's top edge). Moving the composite DOWN (offset.y
# increasing) reveals rows ABOVE window_top_row — i.e. moves the visible
# top edge toward the north wall. The composite's top edge, in canvas
# units, must never be dragged past the north wall's canvas position, and
# the bottom edge (view_size.y below the top, in screen space) must never
# be dragged past the south wall's.
var north_wall_screen_y: float = -north_wall_local_row * cell_px * zoom
var south_wall_screen_y: float = view_size.y - south_wall_local_row * cell_px * zoom
# offset.y is clamped so the top edge never exceeds the north wall
# (offset.y <= north_wall_screen_y keeps the top edge from being pulled
# DOWN past the wall — i.e. revealing north of it) and the bottom edge
# never exceeds the south wall on the other side. When the window's own
# span doesn't reach a wall, that wall's bound is on the permissive side
# of the other and clampf's min/max ordering still holds (min >= max only
# when BOTH walls are inside the span and the window is taller than the
# pole-to-pole distance — see the "tiny body" doc note on the caller).
var min_y: float = minf(north_wall_screen_y, south_wall_screen_y)
var max_y: float = maxf(north_wall_screen_y, south_wall_screen_y)
return Vector2(offset.x, clampf(offset.y, min_y, max_y))
# =============================================================================
# T-1153: rung selection (design doc §5) — the continuous zoom ladder's join
# point between "what granularity is legal to request" (a rung, a discrete
# set) and "what density the client actually wants" (world extent per canvas
# px, a continuous quantity that tracks the live zoom level).
# =============================================================================
## Rung-selection rule — REDESIGNED (live round 3 finding, superseding the
## original §5-literal `2x`-visual-tolerance-only reading): select the
## FINEST rung whose OWN single-window coverage ceiling (MAX_COVERAGE_M)
## still covers the current world extent. Walks RUNGS_FINEST_FIRST
## (Quarter, District, Region) and returns the first whose ceiling is `>=
## world_extent_m` — the coarser rungs are tried only once the finer ones
## genuinely cannot show the requested extent in a single window.
##
## **Why this replaces the original `2x`-visual-tolerance formula entirely**
## (not just patches its Region case, as an earlier version of this function
## did): the design doc §5 rule ("coarsest rung whose spacing <= 2*(E/C)")
## implicitly assumes every rung's single window CAN cover any extent the
## rule selects it for — true for an unbounded wire budget, false here.
## `_clamp_window_n_mirror_v2` (AtlasWindowRequest) — the SAME clamp the
## server itself enforces — caps every rung's single-window real-world
## coverage at a fixed maximum (`MAX_COVERAGE_M`, this file): Quarter
## 32,768 m, District 131,072 m, Region 13,107,200 m (per single Region
## window — bug B's progressive TILING composes several to cover more, a
## viewer-level concern this function doesn't need to know about). A rung
## selected for an extent BEYOND its own ceiling would have its `n` silently
## clamped server-side to something covering only a FRACTION of the
## viewport — visually a tiny box, AND (the actual live-round bug this
## redesign fixes) a clamped echo that no longer matches whatever `n` the
## viewer was still carrying from the rung it's leaving, permanently failing
## the staleness check in `_on_window_ready()`.
##
## **The `2x` visual-tolerance rule becomes REDUNDANT under this model, not
## contradicted by it** — verified numerically: at the exact zoom where
## Quarter's coverage ceiling (32,768 m) is reached, the `2x` threshold
## (`2*E/C`) works out to ~41 m, far finer than even Quarter's own 512 m
## spacing. This means by the time coverage RELEASES a rung, the visual
## tolerance would ALREADY prefer something finer than that rung offers —
## i.e. every rung this function selects is, by construction, at or past its
## own "as fine as it can usefully be" point. The visual-tolerance rule's
## fine-end guarantee (never show a coarser composite than the screen can
## resolve) is automatically satisfied by "select the finest rung whose
## coverage allows it" — there is no case where the coverage rule picks a
## rung the visual rule would have rejected as too coarse, because Quarter
## (the finest rung) is always the answer whenever ANY rung's visual
## tolerance alone would have mattered.
##
## `world_extent_m`/`canvas_px` are both callers'-choice-of-axis (the held
## window is always square, so either axis of the viewport/extent pair gives
## the same answer — the caller picks one, consistently). `canvas_px` is
## kept as a parameter (unused by the coverage rule itself) for signature
## stability with existing callers and because a future finer-than-Quarter
## rung (block/tile, D-226(d)-gated, out of scope here) would plausibly need
## it again.
##
## Returns the granularity_v2 string tag ("Quarter" | "District" | "Region").
static func select_rung(world_extent_m: float, _canvas_px: float) -> String:
for rung: String in RUNGS_FINEST_FIRST:
if world_extent_m <= float(MAX_COVERAGE_M[rung]):
return rung
return "Region" # extent exceeds even Region's own single-window ceiling -> still Region (tiling's job)
## The metre spacing a given granularity_v2 tag resolves to — the inverse
## lookup select_rung() itself doesn't need but callers computing "what world
## extent does holding N districts at this rung actually cover" do (the
## viewer's own extent-in-real-units header line, and the full-zoom-out
## predicate below).
static func spacing_for_rung(granularity_v2: String) -> float:
for rung: Dictionary in RUNG_TABLE:
if rung["granularity_v2"] == granularity_v2:
return float(rung["spacing_m"])
return DISTRICT_SPACING_M # unknown tag -> district, matching the server's "unknown -> District" posture
# =============================================================================
# T-1153: full-zoom-out reset (Jeroen's D-226 T-1143-rulings HARD condition —
# "a full zoom-out resets to the original canonical planetary frame and
# location", the ladder's top rest state, not a drifted pan state).
# =============================================================================
## True once the requested world extent (at the CURRENT zoom, before any
## further zoom-out) covers the full body — i.e. the player has zoomed out as
## far as the ladder goes and is looking at (at least) the whole equatorial
## circumference. This is the crisp "fully zoomed out" definition the ticket
## asks for: `world_extent_m >= circumference_m` at the fit-zoom floor, rather
## than a fuzzy "close to MIN_ZOOM" heuristic (MIN_ZOOM is a UI clamp
## constant, not a planetary-coverage fact — a small body could hit MIN_ZOOM
## while still showing less than the whole circumference, and a huge body
## could show full coverage before MIN_ZOOM is reached, depending on
## CELL_PIXEL_SIZE/held_n; extent-vs-circumference is the honest test either
## way).
##
## **Distinct from select_rung()'s own coverage ceiling** (District's
## `DISTRICT_WINDOW_MAX_N * DISTRICT_SPACING_M = 131,072 m`, a fixed,
## body-independent number) — this predicate's threshold is the actual
## body's full circumference, always far larger than 131,072 m for any real
## planet. The two compose in the expected order: extent crosses 131,072 m
## first (select_rung() already reports Region well before this predicate
## fires), and only once extent reaches the WHOLE circumference does the
## top-rest-state reset itself trigger. There is no conflict between the two
## thresholds, just two different questions ("which rung" vs. "are we at the
## very top").
static func is_fully_zoomed_out(world_extent_m: float, body_radius_km: float) -> bool:
if body_radius_km <= 0.0:
return false # no-radius (tiny test body) has no circumference concept — never auto-resets
var circumference_m: float = TAU * body_radius_km * 1000.0
return world_extent_m >= circumference_m
# =============================================================================
# T-1153: pure screen<->world math extracted from AtlasWindowViewer for
# testability (the project's stated preference — static funcs over Control
# instance methods wherever the math doesn't need the scene tree).
# =============================================================================
## The world extent (metres) currently displayed across the LARGER viewport
## dimension — the `E` half of the §5 rung-selection rule's `E/C`.
## `cell_pixel_size` is the caller's district-at-zoom-1.0 constant
## (AtlasWindowViewer.CELL_PIXEL_SIZE) — the composite's ON-SCREEN FOOTPRINT
## is ALWAYS `held_n * cell_pixel_size * view_zoom` px for `held_n * DISTRICT_M`
## metres of world, REGARDLESS of which rung is currently held (this is
## exactly the invariant AtlasWindowOverlay.cell_grid_side_for_window()'s doc
## establishes on the render side: `n` districts occupy a FIXED screen
## footprint; only the DERIVED CELL RESOLUTION packed into that footprint
## varies by rung). So the metres-per-screen-px sample density is a pure
## function of `view_zoom` — DISTRICT_SPACING_M / (cell_pixel_size *
## view_zoom) — with NO granularity_v2 parameter needed at all: the rung
## itself is the OUTPUT of this calculation (via select_rung()), not an
## input to it.
static func world_extent_m(cell_pixel_size: float, view_zoom: float, viewport: Vector2) -> float:
var canvas_px: float = cell_pixel_size * view_zoom
if canvas_px <= 0.0:
return 0.0
var screen_px: float = maxf(viewport.x, viewport.y)
return DISTRICT_SPACING_M / canvas_px * screen_px
## The DistrictPos the viewport's screen center currently maps to, in RAW
## absolute district space (un-wrapped, un-clamped — the caller canonicalizes
## the final value it actually stores/sends, matching
## canonicalize_district_center()'s own "canonicalize once, at the boundary"
## discipline). Shared by AtlasWindowViewer's pan-edge refetch
## (_maybe_refloat_window()) and rung-reselect refetch
## (_maybe_reselect_rung()) so both read the SAME screen-to-district formula
## rather than two copies that could drift.
static func screen_center_to_district(
viewport_size: Vector2,
view_offset: Vector2,
view_zoom: float,
cell_pixel_size: float,
held_center: Vector2i,
held_n: int
) -> Vector2i:
var screen_center: Vector2 = viewport_size * 0.5
var canvas_pt: Vector2 = (screen_center - view_offset) / view_zoom
var cell: Vector2 = canvas_pt / cell_pixel_size
var half: float = float(held_n) / 2.0
var abs_col: float = float(held_center.x) - half + cell.x
var abs_row: float = float(held_center.y) - half + cell.y
return Vector2i(roundi(abs_col), roundi(abs_row))
## Live round 4 fix: the exact INVERSE of screen_center_to_district()'s own
## district-space math, in CANVAS-LOCAL space (i.e. _canvas's own child
## coordinate system — BEFORE _view_offset/_view_zoom, which is what
## AtlasWindowOverlay._draw()/_draw_tile_mosaic() draw into, since the
## Node2D's position/scale already carries pan/zoom). Every held-window
## convention in this file agrees canvas-local `(0,0)` is absolute district
## `(held_center - held_n/2)` — single-window `_draw()`'s own
## `Rect2(0,0,extent,extent)` relies on this being true for `held_center` ==
## the window's own center. `_draw_tile_mosaic()`'s per-tile placement must
## use this SAME formula (with the VIEWER's `held_center`/`held_n`, not a
## tile's own center/TILE_N) to land in the same coordinate frame the
## fit/pan/zoom machinery already assumes — drawing tiles relative to
## absolute district (0,0) directly (the live-round-4 bug) silently
## disagreed with fit_window_view()'s own `[0, held_n)`-from-origin
## assumption whenever `held_n` (the WHOLE-BODY extent in tile mode) wasn't
## itself anchored the same way, pushing the entire mosaic off-canvas.
static func district_to_canvas_local(
district: Vector2, held_center: Vector2i, held_n: int, cell_pixel_size: float
) -> Vector2:
var half: float = float(held_n) / 2.0
var local_col: float = (district.x - (float(held_center.x) - half)) * cell_pixel_size
var local_row: float = (district.y - (float(held_center.y) - half)) * cell_pixel_size
return Vector2(local_col, local_row)
## Live round 5 fix (the tile-mosaic WRAP half of "the mosaic doesn't fully
## draw"): `compute_tile_grid()`'s tiles are CANONICAL columns (wrapped into
## `[0, cols)` — the correct, single-valued key for REQUESTS and cache
## coalescing), but a canonical column has infinitely many EQUIVALENT
## on-screen positions (`col`, `col - cols`, `col + cols`, ...), since
## longitude is periodic. `district_to_canvas_local()` is a pure LINEAR
## function with no wrap concept — fed a canonical column directly, it
## places the tile at exactly ONE of those wrap-images, which is only ever
## the visually-correct one by coincidence. Lendel's own repro: the tile
## whose pre-canonicalization center was -6400 canonicalizes to 12739
## (`-6400 mod 19139`) — correct for the request/cache key, but drawing at
## column 12739 directly places it canvas-local ~22308 (off-canvas RIGHT),
## when the tile's actual visible position (immediately west of the
## canonical origin) is at column -6400 (canvas-local ~3169, the LEFT
## third of the mosaic).
##
## The fix: before handing a tile's canonical column to
## `district_to_canvas_local()`, re-express it as whichever wrap-image
## (`canonical_col + k*cols` for integer `k`) is NEAREST `held_center.x` —
## the representative that's actually near the current view, matching how a
## real, non-tiling single-window pan already resolves the "which
## circumnavigation" question implicitly (screen_center_to_district()'s own
## RAW, un-wrapped output). `cols <= 0` (no-radius bodies, which never tile
## per compute_tile_grid()'s own doc) is a safe no-op passthrough — there is
## no periodicity to resolve.
static func nearest_wrap_image(canonical_col: int, held_center_col: int, cols: int) -> int:
if cols <= 0:
return canonical_col
var delta: int = posmod(canonical_col - held_center_col + cols / 2, cols) - cols / 2
return held_center_col + delta
## Live round 4 fix (the SECOND half of the "pitch black" repro, beyond
## district_to_canvas_local()'s tile-mosaic fix above): `_view_offset` is a
## PURE screen<->canvas-local transform, entirely independent of
## `held_center`/`held_n` — cursor-anchored zoom (`_zoom_at()`) never
## references them. But the single-window `_draw()` path draws the held
## composite at canvas-local `Rect2(0,0,extent,extent)`, which is ONLY the
## right place on screen if canvas-local (0,0) still equals
## `held_center - held_n/2` for the NEW rung. `_maybe_reselect_rung()`
## updates `held_center`/`held_n` to the new rung's values (a DIFFERENT
## `held_n` — Region's ~thousands vs. District's 64 vs. Quarter's 16) but
## never touched `_view_offset` to compensate — so canvas-local (0,0)
## silently stopped meaning `held_center - held_n/2` the instant `held_n`
## changed, and the composite (still drawn at local (0,0)) landed wherever
## the STALE offset happened to put it — off-canvas by tens or hundreds of
## thousands of px for a Region-to-District/Quarter crossing (round 4's
## repro), same root shape as the tile-mosaic bug, just on the "one held
## window" side of the split instead of the "many tiles" side.
##
## This is the exact INVERSE construction: given the SAME screen point that
## used to map to `old_local` must now map to canvas-local
## `new_held_n/2 * cell_pixel_size` (i.e. new_held_center's own position
## under the NEW window's `[0, new_held_n)` span), solve for the
## `view_offset` that makes `screen_point == new_local * view_zoom +
## view_offset` true. Pan-edge refetch (`_maybe_refloat_window()`) never
## needed this because it never changes `held_n` — only rung crossings do.
static func recompute_offset_for_held_n_change(
screen_point: Vector2, view_zoom: float, new_held_n: int, cell_pixel_size: float
) -> Vector2:
var new_local: Vector2 = Vector2.ONE * (float(new_held_n) * 0.5 * cell_pixel_size)
return screen_point - new_local * view_zoom
# =============================================================================
# T-1145 item 2 (moved here T-1153 for file-length/testability): WASD/
# arrow-key held-pan direction + edge-scroll suppression/direction — pure
# functions of explicit inputs, no Control/scene-tree dependency.
# =============================================================================
## WASD + arrow keys, read via Input.is_key_pressed() on the PHYSICAL keycode
## (not an InputMap action): W/S/A/D on this project's global InputMap are
## already bound to move_north/move_south/move_east/move_west (gameplay
## movement, D-054 mouse-relative facing) — reusing those actions here would
## make holding W simultaneously pan this map AND queue a gameplay move
## command server-side the moment this implant screen closes back to
## gameplay (InputMapper polls Input.is_action_pressed() unconditionally,
## with no implant-occlusion guard — a genuine pre-existing gap outside this
## ticket's scope, not introduced here). Reading the raw physical keycode
## instead of the shared action name means this screen's WASD use is fully
## independent of whatever the gameplay action happens to be bound to — same
## key, two UNRELATED consumers, neither needs to know about the other.
## Arrow keys have no InputMap action bound at all, so they're conflict-free
## either way. Returns a raw (non-normalized) direction — the caller
## normalizes once after adding the edge-scroll contribution, so N+E doesn't
## move faster than N alone. Reads the global `Input` singleton directly (not
## injected) — this is the one function in this file that isn't a pure
## function of its arguments, kept here anyway to sit beside its two siblings
## below rather than splitting the WASD/edge-scroll trio across two files.
static func held_pan_direction() -> Vector2:
var direction := Vector2.ZERO
if Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP):
direction.y -= 1.0
if Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN):
direction.y += 1.0
if Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT):
direction.x -= 1.0
if Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT):
direction.x += 1.0
return direction
## T-1145 item 2: edge-scroll is suppressed (a) while the cursor is over UI
## (`is_over_ui` — the caller's own _is_over_ui() result, passed in rather
## than called from here since "what counts as UI" is viewer-specific) and
## (b) while the application window itself lacks OS focus (`app_has_focus` —
## otherwise a background window with the cursor left resting near its edge
## from a previous session would silently pan while the player is doing
## something else entirely; Godot's NOTIFICATION_APPLICATION_FOCUS_OUT/IN
## make this directly detectable, the caller's own _notification() wires it).
static func is_cursor_edge_scrolling(
app_has_focus: bool,
is_over_ui: bool,
viewport_size: Vector2,
mouse_pos: Vector2,
edge_margin_px: float
) -> bool:
if not app_has_focus:
return false
if is_over_ui:
return false
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
return false
return (
mouse_pos.x >= 0.0
and mouse_pos.y >= 0.0
and mouse_pos.x <= viewport_size.x
and mouse_pos.y <= viewport_size.y
and (
mouse_pos.x < edge_margin_px
or mouse_pos.y < edge_margin_px
or mouse_pos.x > viewport_size.x - edge_margin_px
or mouse_pos.y > viewport_size.y - edge_margin_px
)
)
## Direction toward whichever edge(s) the cursor is near — same shape as
## held_pan_direction() (a raw, un-normalized Vector2 the caller combines and
## normalizes once).
static func edge_scroll_direction(
viewport_size: Vector2, mouse_pos: Vector2, edge_margin_px: float
) -> Vector2:
var direction := Vector2.ZERO
if mouse_pos.x < edge_margin_px:
direction.x -= 1.0
elif mouse_pos.x > viewport_size.x - edge_margin_px:
direction.x += 1.0
if mouse_pos.y < edge_margin_px:
direction.y -= 1.0
elif mouse_pos.y > viewport_size.y - edge_margin_px:
direction.y += 1.0
return direction
# =============================================================================
# T-1153, live round 3 (Jeroen's ruling, design doc §4): the orbital REST
# STATE must TILE — a single wire-capped Region window (MAX_COVERAGE_M["Region"]
# = 13,107,200 m) covers only a fraction of a real body's circumference
# (Lendel: 39,197,023 m — a single window is ~a third of the body). The top
# rest state composes MULTIPLE Region windows ("progressive capped-density
# TILING", design doc §4) into a mosaic under ONE view transform.
# =============================================================================
## Compute the tile-set grid for the orbital rest state: the minimal set of
## Region-granularity window CENTERS (each `TILE_N` districts wide) whose
## union covers the WHOLE body — columns wrap (canonicalize_district_center()'s
## own east-west periodicity), rows clamp at the poles. Returns an Array of
## Vector2i tile centers, ALREADY CANONICALIZED (duplicates from pole-row
## clamping or (degenerately) column-wrap collisions are DEDUPED — a tiny
## body where multiple nominal tile rows clamp to the identical pole-adjacent
## row, or multiple nominal tile columns wrap to the identical column, must
## not request/draw the same tile twice).
##
## Grid layout: `cols_tiles = ceil(cols / TILE_N)` tiles span the full
## circumference (evenly spaced, centered on column 0 — the canonical
## origin); `rows_tiles = ceil(2*rows_half / TILE_N)` tiles span pole to
## pole (centered on row 0). Each tile's PRE-CANONICALIZATION center is
## `(tile_index - (tile_count-1)/2) * TILE_N` along its axis — symmetric
## around the canonical origin, matching enter_orbital()'s own "canonical
## origin = (0,0)" convention (AtlasDescendGeometry's doc) so the tile set's
## own center-of-mass lands exactly on the canonical frame, not offset from
## it.
##
## No-radius bodies (tiny test bodies, `body_radius_km <= 0`) return a
## single tile at (0,0) — matching enter_orbital()'s own no-radius fallback
## disposition (no circumference/tiling concept for a body with no radius).
static func compute_tile_grid(body_radius_km: float) -> Array:
if body_radius_km <= 0.0:
return [Vector2i.ZERO]
var extent: Dictionary = AtlasDescendGeometryRef.district_extent(body_radius_km)
var cols: int = int(extent["cols"])
var rows_half: int = int(extent["rows_half"])
var rows_total: int = rows_half * 2
var cols_tiles: int = maxi(1, ceili(float(cols) / float(TILE_N)))
var rows_tiles: int = maxi(1, ceili(float(rows_total) / float(TILE_N)))
var col_centers: Array = []
for tx in range(cols_tiles):
var raw_col: int = roundi((float(tx) - (float(cols_tiles - 1) * 0.5)) * float(TILE_N))
col_centers.append(raw_col)
var row_centers: Array = []
for ty in range(rows_tiles):
var raw_row: int = roundi((float(ty) - (float(rows_tiles - 1) * 0.5)) * float(TILE_N))
row_centers.append(raw_row)
# Dedup via a Dictionary keyed on the CANONICALIZED (col, row) pair —
# Godot Dictionary keys compare Vector2i by value, so this is a proper
# set. Insertion order is preserved (Godot Dictionaries are
# order-preserving), giving a deterministic tile ORDER too — the same
# grid always requests/draws in the same sequence, useful for progressive
# arrival to read as a stable left-to-right, top-to-bottom fill rather
# than an unpredictable one.
var seen: Dictionary = {}
var tiles: Array = []
for raw_col: int in col_centers:
for raw_row: int in row_centers:
var canonical: Vector2i = AtlasDescendGeometryRef.canonicalize_district_center(
Vector2i(raw_col, raw_row), body_radius_km
)
if not seen.has(canonical):
seen[canonical] = true
tiles.append(canonical)
return tiles
# =============================================================================
# T-1153: screen header chrome (D-169/D-170) — pure string-building, moved
# here from atlas_window_viewer.gd for file-length (the viewer's own
# `_refresh_screen_header()`/`_location_label()` stay as thin wrappers, since
# both are directly tested by name).
# =============================================================================
## Body name + coordinate label — T-1142: shows the body's proper name
## (falling back to body_id) alongside the held district center, so the
## header never reads as bare "district (col, row)" with no indication of
## WHICH body the player is looking at.
static func location_label(body_display_name: String, held_center: Vector2i) -> String:
return "%s — (%d, %d)" % [body_display_name, held_center.x, held_center.y]
## D-169/D-170 implant chrome (§5): {title, subtitle} for the screen header.
## The subtitle's extent (`held_n` districts) is rung-INVARIANT (n is always
## district extent — see AtlasWindowOverlay.cell_grid_side_for_window()'s
## doc), but the km/cell reading reflects the HELD rung's actual spacing
## (2.048 km District, 0.512 km Quarter, 204.8 km Region) — the "continuous
## metres-per-pixel/extent readout" design doc §6 calls for in place of a
## discrete "you are now in Quarter Mode" label (Jeroen's "no mode
## transition" ruling): the number itself communicates the rung.
static func screen_header_content(
body_display_name: String,
held_center: Vector2i,
held_n: int,
held_granularity_v2: String,
district_m: float
) -> Dictionary:
var label: String = location_label(body_display_name, held_center)
var extent_km: float = float(held_n) * district_m / 1000.0
var spacing_km: float = spacing_for_rung(held_granularity_v2) / 1000.0
var subtitle: String = "%.1f x %.1f km · %.3f km/cell" % [extent_km, extent_km, spacing_km]
return {"title": "REGIONAL — %s" % label.to_upper(), "subtitle": subtitle}
## Cold-start dossier: the baseline position draw_string() needs to render
## `text` horizontally AND vertically centered inside `viewport_size` — pure
## geometry, split out of AtlasWindowViewer._draw_deriving_terrain_label()
## for file-length (gdlint max-file-lines), not a different concern. Callers
## pass HORIZONTAL_ALIGNMENT_CENTER to draw_string() themselves (that part
## isn't pure — it needs the real Font instance) — this only computes the Y
## baseline offset and the X center point draw_string()'s own centering
## then works from.
static func centered_label_baseline(viewport_size: Vector2, text_size: Vector2) -> Vector2:
var center: Vector2 = viewport_size * 0.5
return center - text_size * 0.5 + Vector2(0.0, text_size.y * 0.5)
## T-1172 round 2 (coordinator's "reconsider the split" ask): the SHARED
## cell-index formula both AtlasWindowOverlay's terrain painter (which builds
## the drawn `grid_side x grid_side` per-cell texture — `i = row * grid_side
## + col`, `img.set_pixel(col, row, ...)`) and AtlasWindowWaterClip's clip
## predicate (which must read the EXACT SAME cell for a given position, or
## the clip silently disagrees with what's actually painted) both need. The
## live trace that closed T-1172 round 2's investigation PROVED this formula
## itself was never the bug (painter and clip independently computed the
## identical col/row/idx for the same query throughout) — the actual bug was
## in the WRAP resolution one layer up (resolve_morphology_zone()'s own doc)
## — but factoring the index math into ONE shared function here, rather than
## two independently-maintained copies (atlas_window_overlay.gd's inline
## `row * grid_side + col` vs. the old duplicate in
## atlas_window_water_clip.gd), removes the STRUCTURAL risk of a future
## divergence in exactly the way the coordinator flagged as the general
## danger class ("stub-and-code agreeing on the wrong convention" — here,
## PAINTER-and-clip could drift the same way without a shared source).
## `local_x`/`local_y` are DISTRICT-SPACE offsets from the window's own
## top-left corner (`center - n/2`), in `[0, n)` — the SAME quantity both
## call sites already compute before this function is reached.
static func cell_index_for_local_offset(
local_x: float, local_y: float, n: int, grid_side: int
) -> Vector2i:
if n <= 0 or grid_side <= 0:
return Vector2i(-1, -1)
var col: int = clampi(int(floor(local_x / float(n) * float(grid_side))), 0, grid_side - 1)
var row: int = clampi(int(floor(local_y / float(n) * float(grid_side))), 0, grid_side - 1)
return Vector2i(col, row)
@@ -1,685 +0,0 @@
extends RefCounted
## Nature-overlay (river/basin/attractor) pure geometry + per-rung policy —
## split out of atlas_window_geometry.gd (T-1170, that file was at 954/1000
## gdlint max-file-lines when this batch started) exactly the same way
## test_atlas_window_geometry_nature.gd was already split from
## test_atlas_window_geometry.gd — one file, one concern, room to grow. Every
## symbol below moved VERBATIM from atlas_window_geometry.gd; no behavior
## change in this split itself. atlas_window_nature_overlay.gd is the only
## runtime consumer (verified: grep across client/ before the move) and now
## preloads THIS file instead.
##
## Contains:
## - T-1156 wave 1 whole-body Layer-1 pixel-space -> canvas-local mapping
## (layer1_pixel_to_world_m/world_m_to_district/layer1_pixel_to_canvas_local)
## - T-1156 wave 1 per-rung skeleton visibility/styling policy (RIVER_CLASS_*,
## CONFLUENCES/MOUTHS/BASINS/ATTRACTORS_VISIBLE_BY_RUNG, dot/ring/attractor
## size consts) — RENAMED this batch (T-1170 Ruling 5c, see below) from
## RIVER_CLASS_VISIBLE_BY_RUNG to SKELETON_CLASS_VISIBLE_BY_RUNG.
## - zoom_compensated_size() — the screen-space marker-size zoom-compensation
## fix (coordinator live-eyeball finding, 2026-07-23).
##
## atlas_window_geometry.gd retains cell_index_for_local_offset() (T-1172
## round 2) rather than moving it here — that function is shared with
## AtlasWindowOverlay's terrain painter (a non-nature consumer), so it stays
## on the base file both files already depend on, avoiding a nature-file ->
## base-file dependency for a symbol the base file's own painter needs too.
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## atlas_window_geometry.gd never depends on this file (verified: no preload
## of atlas_window_geometry_nature.gd anywhere in that file) — so preloading
## it back here is safe, no circular dependency, matching the pattern
## AtlasWindowOverlay/AtlasWindowWaterClip already use for
## AtlasWindowGeometryRef.
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## D-243 district spacing, metres/district — this file's own copy of
## AtlasWindowGeometry.DISTRICT_SPACING_M (duplicated, not preloaded-and-read,
## matching this cluster's existing "each file owns its own reading of a
## small pure constant rather than force a dependency" precedent —
## atlas_overlay_colors.gd's header doc states this explicitly; the same
## rationale that kept atlas_window_water_clip.gd's cell_grid_side_for_window()
## a deliberate duplicate rather than a shared call applies here). MUST stay
## numerically identical to the base file's constant — both ultimately trace
## to D-243's 2,048 m district spacing, which is locked project vocabulary,
## not a value expected to drift.
const DISTRICT_SPACING_M: float = 2048.0
# =============================================================================
# T-1156 wave 1 / T-1170: per-rung nature-overlay visibility/styling policy
# (Araminta's presentation ruling, 2026-07-23 — supersedes Tyre's provisional
# add-detail-as-you-descend mapping the ticket brief originally carried).
#
# T-1170 Ruling 5c (Tyre, 2026-07-23) — THE REVISIT, split on the carrier
# axis: the single RIVER_CLASS_VISIBLE_BY_RUNG table is replaced by TWO
# tables, one per presentation surface —
# - SKELETON_CLASS_VISIBLE_BY_RUNG: the Region+ whole-body skeleton-chord
# path (Ruling 5a) — unchanged posture from wave 1, Region shows every
# class.
# - COURSE_CLASS_VISIBLE_BY_RUNG: the District/Quarter windowed course-
# polyline path (Ruling 5b) — THIS is where "Quarter rivers return"
# (the pre-announced wave-1 fade-down revisit executes): District shows
# trunk+tributary, Quarter shows all three classes.
# Companion per-class width/opacity tables (COURSE_CLASS_WIDTH_PX/
# COURSE_CLASS_OPACITY) carry FUNCTIONAL DEFAULTS per the ruling brief
# (trunk widest ~2.2px, tributary ~1.4px, stream ~0.9px, screen-space via the
# existing zoom-compensation discipline) — Araminta's forthcoming presentation
# ruling edits THESE TABLES AND ONLY THESE TABLES, same single-revisit-point
# discipline wave 1 established for RIVER_CLASS_VISIBLE_BY_RUNG itself.
# =============================================================================
## River class ids — mirrors server/src/atlas/body_world_state.rs
## RiverNetwork.river_class's own doc exactly (0=stream, 1=tributary,
## 2=trunk). A `river_class` array shorter than `river_cells` (pre-T-1156
## payload, or the graceful-fallback empty-array case) has no per-cell class
## to read — RIVER_CLASS_FALLBACK is what a missing entry resolves to: TRUNK,
## so an old/absent river_class array still shows something at every rung
## rather than silently vanishing (Dudley's `#[serde(default)]` empty-array
## contract makes "index out of range" the normal case for a pre-T-1156
## response, not an edge case to special-case away).
const RIVER_CLASS_STREAM: int = 0
const RIVER_CLASS_TRIBUTARY: int = 1
const RIVER_CLASS_TRUNK: int = 2
const RIVER_CLASS_FALLBACK: int = RIVER_CLASS_TRUNK
# =============================================================================
# T-1170 Ruling 2a-2d/5a: river_downstream D8 pointer decode — the wire
# convention `RiverNetwork.river_downstream` (Vec<u8>, index-aligned with
# river_cells) encodes per river cell: a DIRECTION 0-7 into an adjacent D8
# neighbor, or a SENTINEL >= RIVER_DOWNSTREAM_SENTINEL_BASE marking a chain
# end (MOUTH/EDGE_DRAIN/reserved-TERMINAL). CONFIRMED against Dudley's A1
# (server/src/atlas/drainage.rs:35-44, landed 0fea69feb, relayed by the
# coordinator) — these are the REAL shipped values, final until/unless the
# server's own encoding changes, in which case this is the one place to
# repoint.
# =============================================================================
## Sentinel base — any river_downstream value >= this is a chain-end
## sentinel, not a direction. Direction values are 0-7 (8 real D8 neighbors);
## sentinels start immediately above at 8.
const RIVER_DOWNSTREAM_SENTINEL_BASE: int = 8
const RIVER_DOWNSTREAM_MOUTH: int = 8
const RIVER_DOWNSTREAM_EDGE_DRAIN: int = 9
## TERMINAL is reserved/unused in round 1 (Ruling 2c/7b — future endorheic
## basin support) — this client never expects to see it on real data yet, but
## decodes it identically to EDGE_DRAIN (chain end, no ring) rather than
## treating an unrecognized-but-in-sentinel-range value as an error, so a
## future server enabling TERMINAL needs no client change to degrade
## gracefully (it would just draw as an unmarked chain end until a future
## ticket gives it its own ring treatment, exactly EDGE_DRAIN's own current
## disposition).
const RIVER_DOWNSTREAM_TERMINAL: int = 10
## D8 direction index (0-7) -> (row_delta, col_delta), CONFIRMED against
## drainage.rs:35-44's own fdir table order (not assumed/guessed — the
## coordinator relayed this explicitly from Dudley's A1 source): row
## increases SOUTH (matching layer1_pixel_to_world_m()'s own "row 0 = north
## pole" convention, confirmed the same convention on both sides of this
## mapping), col increases EAST and WRAPS at the antimeridian (handled by the
## caller's existing nearest-wrap-image discipline, same as every other
## column value flowing through this file — this table itself has no wrap
## concept, it is pure grid-adjacency).
## 0 = N (-1, 0) 4 = NE (-1, 1)
## 1 = S ( 1, 0) 5 = NW (-1, -1)
## 2 = E ( 0, 1) 6 = SE ( 1, 1)
## 3 = W ( 0, -1) 7 = SW ( 1, -1)
const D8_DIRECTION_DELTAS: Array = [
Vector2i(-1, 0), # 0 N
Vector2i(1, 0), # 1 S
Vector2i(0, 1), # 2 E
Vector2i(0, -1), # 3 W
Vector2i(-1, 1), # 4 NE
Vector2i(-1, -1), # 5 NW
Vector2i(1, 1), # 6 SE
Vector2i(1, -1), # 7 SW
]
# =============================================================================
# T-1170 Ruling 3h/5b: RiverCourse.terminus wire vocabulary — the
# CourseTerminus enum's variant NAMES as they arrive over msgpack (bare
# strings, the SAME "unit variant -> string tag" convention granularity_v2
# already uses on this same wire — see AtlasMapProtocol's own doc). `None` on
# the Rust side (a mid-window course that neither reaches a real mouth nor
# the window edge — the ordinary "ends because the chord's amplitude taper
# reached zero at a confluence/headwater anchor cell inside this window"
## case) decodes to the bare string "None" per rmp_serde's unit-variant
## convention — NOT GDScript `null`. Callers must compare against the STRING
## constant below, never `== null`.
# =============================================================================
const COURSE_TERMINUS_NONE: String = "None"
const COURSE_TERMINUS_MOUTH: String = "Mouth"
const COURSE_TERMINUS_EDGE_DRAIN: String = "EdgeDrain"
const COURSE_TERMINUS_CONTINUES_BEYOND_WINDOW: String = "ContinuesBeyondWindow"
## Region+ SKELETON path (Ruling 5a) — the whole-body chord-chain draw, built
## from river_downstream. Region shows every class (the full skeleton) — this
## table's posture is UNCHANGED from wave 1's original
## RIVER_CLASS_VISIBLE_BY_RUNG (renamed, not re-tuned). District/Quarter keys
## are retained (both empty) purely so a caller that queries this table by an
## unexpected rung tag gets the same documented "nothing visible" answer wave
## 1 shipped, rather than a KeyError — the SKELETON path itself is only ever
## drawn at Region in practice (District/Quarter draw courses, the OTHER
## table, per Ruling 5b).
const SKELETON_CLASS_VISIBLE_BY_RUNG: Dictionary = {
"Region": [RIVER_CLASS_STREAM, RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
"District": [],
"Quarter": [],
}
## District/Quarter COURSE path (Ruling 5b/5c) — the windowed polyline draw,
## built from DistrictWindowLayer.courses. District: trunk+tributary (streams
## stay off at District — the ruling's own example enumeration). Quarter:
## ALL THREE classes — "Quarter rivers return", the pre-announced wave-1
## fade-down revisit executing here. Region is not a key here at all (Region
## never draws courses — it draws the skeleton chord chain, the OTHER table)
## — a caller must not query this table at Region; river_class_visible_at_rung()
## style readers for this table live on this file too and fall back safely
## for an unrecognized tag (see course_class_visible_at_rung()'s own doc).
const COURSE_CLASS_VISIBLE_BY_RUNG: Dictionary = {
"District": [RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
"Quarter": [RIVER_CLASS_STREAM, RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
}
## Per-rung feature-group toggles beyond river-cell class filtering — whether
## confluences/mouths/basins/attractors draw at all at a given rung (each
## still additionally gated by its own overlay-bar toggle, RVR/BAS/ATR, where
## applicable — this table is the RUNG gate, the overlay bar is the PLAYER
## gate, both must pass). Mouths get the one rung-based exception in the whole
## table: District keeps them at full Region styling/opacity (a mouth is
## always a landmark, per the ruling) while every other District river feature
## is suppressed or de-emphasized.
const CONFLUENCES_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
const MOUTHS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": true, "Quarter": false}
const BASINS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
const ATTRACTORS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
## Per-rung river dot styling (screen-space px, at zoom=1.0 — the same
## "canvas-local px" domain every other drawn feature in this cluster already
## uses, scaled by the caller's own view zoom like everything else in
## `_canvas`). District trunk dots are smaller AND drawn at reduced opacity
## (80% — raised from the ruling's initial 60% in Araminta's PR #195 capture
## review: at 1.6px/60% the dot was "essentially invisible without knowing
## where to look", underselling the 'a major river crosses near here' intent;
## 2.0px/80% keeps the fade-down ladder vs Region's 2.2px/100% without
## reading as accidentally-erased) — the "fade down" the ruling describes;
## Region dots are full-strength opacity (alpha baked into the reused
## COLOR_GEN_RIVER/COLOR_GEN_MOUTH constants themselves, alpha 1.0).
##
## T-1170: these RIVER_DOT_* consts now describe the Region SKELETON path
## ONLY (Ruling 5a's chord-chain draw reuses the same per-class radii the old
## dot-scatter used — chords are drawn at these widths, not a new table).
## RIVER_DOT_RADIUS_DISTRICT_TRUNK/RIVER_DOT_OPACITY_DISTRICT_TRUNK are DEAD
## at District now that District draws courses (Ruling 5b/3g retires the
## District dot-scatter entirely) — left in place, unread by any T-1170 draw
## path, rather than deleted mid-batch: B3 (course polyline drawing) is the
## change that stops calling them; deleting here would be a premature edit to
## a still-referenced-by-wave-1-code constant ahead of that landing.
const RIVER_DOT_RADIUS_BY_CLASS_REGION: Dictionary = {
RIVER_CLASS_STREAM: 0.9,
RIVER_CLASS_TRIBUTARY: 1.4,
RIVER_CLASS_TRUNK: 2.2,
}
const RIVER_CONFLUENCE_RADIUS_REGION: float = 3.5
const RIVER_DOT_RADIUS_DISTRICT_TRUNK: float = 2.0
const RIVER_DOT_OPACITY_DISTRICT_TRUNK: float = 0.8
## T-1170 Ruling 5c: course polyline per-class width/opacity, District/Quarter
## COURSE path companion tables to COURSE_CLASS_VISIBLE_BY_RUNG above.
## FUNCTIONAL DEFAULTS ONLY (the ruling's own numbers) — Araminta's
## forthcoming presentation ruling edits these two tables and only these two
## tables, same discipline as every other single-revisit-point table in this
## file. Widths are screen-space px at zoom=1.0, routed through
## zoom_compensated_size()/the caller's `_zs()` wrapper before reaching
## draw_polyline() exactly like every other marker size in this cluster (PR
## #195's stroke-width miss is the standing regression class this discipline
## exists to prevent — see zoom_compensated_size()'s own doc). Opacities are
## plain [0,1] alpha multipliers on COLOR_GEN_RIVER, no zoom involvement.
## Trunk widest / stream thinnest, matching the Region skeleton's own
## per-class radius ordering (RIVER_DOT_RADIUS_BY_CLASS_REGION) so the visual
## "trunk is the biggest river" read is consistent whether the player is
## looking at the Region chord chain or a District/Quarter course polyline.
const COURSE_CLASS_WIDTH_PX: Dictionary = {
RIVER_CLASS_STREAM: 0.9,
RIVER_CLASS_TRIBUTARY: 1.4,
RIVER_CLASS_TRUNK: 2.2,
}
const COURSE_CLASS_OPACITY: Dictionary = {
RIVER_CLASS_STREAM: 0.8,
RIVER_CLASS_TRIBUTARY: 0.9,
RIVER_CLASS_TRUNK: 1.0,
}
## Mouth double-ring geometry (Region AND District — mouths never de-emphasize,
## per the ruling) — verbatim from the retired atlas_marker_overlay.gd
## _draw_gen_rivers() (:537-539), reused exactly, not re-tuned. T-1170: also
## the mouth-ring geometry for REAL course termini (Ruling 5b/3e) — one
## geometry, both presentation surfaces (skeleton chord ends at Region,
## course polyline ends at District/Quarter).
const MOUTH_RING_RADIUS: float = 5.0
const MOUTH_HALO_RADIUS: float = 8.0
const MOUTH_HALO_ALPHA: float = 0.30
## T-1170 live round (2026-07-23, coordinator's mouth-ring blob finding):
## the minimum radius/stroke RATIO a draw_arc() ring needs to render hollow
## rather than degenerate into a solid blob — see
## zoom_compensated_ring_radius()'s own doc for the full A/B bracket
## evidence (radius=1x stroke -> blob, 1.5x -> hollow, floor set at 2x with
## margin over the observed transition).
const RING_RADIUS_STROKE_MULTIPLIER: float = 2.0
## Attractor minimum-strength gate — verbatim from the retired
## atlas_marker_overlay.gd GEN_ATTRACTOR_MIN_STRENGTH (:44). Region-only per
## the ruling (ATTRACTORS_VISIBLE_BY_RUNG), wave 1 has no attractor rendering
## at any other rung to gate.
const ATTRACTOR_MIN_STRENGTH: float = 0.15
# =============================================================================
# T-1156 wave 1: whole-body Layer-1 (river/basin/attractor) pixel-space ->
# canvas-local mapping for the zoom ladder — the nature-overlay counterpart to
# the district/canvas machinery in atlas_window_geometry.gd. Layer-1's
# `river_network`/`drainage_basins`/`attractors` positions are (row, col)
# heightmap-pixel coordinates in a `grid_w`(cols) x `grid_h`(rows) working
# grid (Rust `Layer1Output.grid_w/grid_h` = `BodyHeightmap.width/height` = the
# SAME `TerrainAnalysis.w/h` river/attractor extraction ran against —
# server/src/atlas/layer1.rs, features.rs `TerrainAnalysis::analyze`). This is
# NOT the atlas_marker_overlay.gd `_gen_pos()` texture-fraction mapping (that
# maps onto a DISPLAYED heightmap texture on the retired planetary screen) —
# the ladder has no resident heightmap texture at all, so pixel positions must
# go all the way to WORLD METRES -> DISTRICT space -> canvas-local, the same
# frame AtlasWindowGeometry.district_to_canvas_local() already establishes for
# every other drawn feature on this screen.
# =============================================================================
## Heightmap pixel (row, col) -> absolute world metres (wx east, wy south),
## mirroring server/src/atlas/district_profile.rs's `pixel_to_world_m()`
## EXACTLY (verified against that function's source, not assumed): longitude
## WRAPS and is addressed by the plain column fraction (`col / grid_w`) against
## the full circumference — column 0 sits at world/longitude 0, no -0.5
## centering unlike latitude. Latitude CLAMPS at the poles and is addressed by
## `row / (grid_h - 1) - 0.5`, i.e. row 0 is exactly the pole (lat_frac -0.5 =
## north pole = wy negative-most) and row (grid_h - 1) is exactly the opposite
## pole (lat_frac +0.5 = south pole = wy positive-most) — the SAME "row
## increases southward" convention AtlasDescendGeometry.district_pos_at()
## already assumes for its own (inverse-direction) pixel<->district mapping,
## confirmed here to be the same convention layer1's grid uses, not a
## different one that happens to share variable names.
##
## No-radius bodies (body_radius_km <= 0, tiny test bodies): 1 heightmap pixel
## = 1 district-spacing metre, matching pixel_to_world_m()'s own no-radius
## fallback (`px * scale::DISTRICT_M`) and district_pos_at()'s no-radius
## branch on the other side of this mapping. Uses this file's own
## DISTRICT_SPACING_M (the same 2,048 m/district constant — see that const's
## own doc for why it's a deliberate duplicate, not a preload-and-read).
static func layer1_pixel_to_world_m(
row: float, col: float, grid_w: float, grid_h: float, body_radius_km: float
) -> Vector2:
if grid_w <= 0.0 or grid_h <= 0.0:
return Vector2.ZERO
if body_radius_km <= 0.0:
return Vector2(col * DISTRICT_SPACING_M, row * DISTRICT_SPACING_M)
var circumference_m: float = TAU * body_radius_km * 1000.0
var meridian_m: float = PI * body_radius_km * 1000.0
var wx: float = (col / grid_w) * circumference_m
var lat_frac: float = (row / (grid_h - 1.0) - 0.5) if grid_h > 1.0 else 0.0
var wy: float = lat_frac * meridian_m
return Vector2(wx, wy)
## World metres -> fractional DistrictPos (NOT rounded to an integer district
## — a river dot's true position is sub-district-precise even though the
## window grid itself is district-granular; rounding here would visibly snap
## every river pixel onto a district lattice). DISTRICT_SPACING_M is this
## file's own existing constant (2,048 m/district, D-243) — one division, no
## re-derivation.
static func world_m_to_district(world_m: Vector2) -> Vector2:
return world_m / DISTRICT_SPACING_M
## The full pixel(row,col) -> canvas-local composition a nature-overlay draw
## call needs in one step: heightmap pixel -> world metres -> fractional
## district -> canvas-local (via AtlasWindowGeometry.district_to_canvas_local(),
## reused verbatim so a river dot lands in exactly the same coordinate frame
## every other drawn feature on this screen already agrees on — pan/zoom/rung
## crossings all move the SAME transform under everything drawn into
## `_canvas`). Wrap resolution (AtlasWindowGeometry.nearest_wrap_image()) is
## the CALLER's job, same split the tile mosaic draw path already uses — this
## function's `district` output is the RAW (un-wrapped) fractional position; a
## caller iterating river cells against a specific held window picks the
## nearest wrap-image of the COLUMN only (rows never wrap, matching every
## other wrap-aware caller in this cluster).
static func layer1_pixel_to_canvas_local(
row: float,
col: float,
grid_w: float,
grid_h: float,
body_radius_km: float,
held_center: Vector2i,
held_n: int,
cell_pixel_size: float
) -> Vector2:
var world_m: Vector2 = layer1_pixel_to_world_m(row, col, grid_w, grid_h, body_radius_km)
var district: Vector2 = world_m_to_district(world_m)
return AtlasWindowGeometryRef.district_to_canvas_local(district, held_center, held_n, cell_pixel_size)
## T-1170 Ruling 5b/3h: RiverCourse.points are already WORLD METRES on the
## wire (unlike the skeleton path's heightmap-pixel `river_cells` — see
## Ruling 3h's wire shape doc) — one fewer conversion step than
## layer1_pixel_to_canvas_local() above: world metres -> fractional district
## (world_m_to_district(), reused verbatim) -> canvas-local
## (AtlasWindowGeometry.district_to_canvas_local(), same shared transform
## every other drawn feature on this screen uses). No pixel-grid/body-radius
## step at all — courses have no heightmap-pixel domain to convert out of.
static func world_m_to_canvas_local(
world_m: Vector2, held_center: Vector2i, held_n: int, cell_pixel_size: float
) -> Vector2:
var district: Vector2 = world_m_to_district(world_m)
return AtlasWindowGeometryRef.district_to_canvas_local(district, held_center, held_n, cell_pixel_size)
# =============================================================================
# T-1170 Ruling 2a-2d/5a: river_downstream D8 pointer decode.
# =============================================================================
## Decode one river cell's `river_downstream` wire value into its downstream
## neighbor's (row, col) heightmap-pixel position, or `null` if the value is
## a chain-end sentinel (MOUTH/EDGE_DRAIN/TERMINAL) or an out-of-range/
## malformed direction. `row`/`col` are the UPSTREAM cell's own pixel
## position (float, matching this file's own row/col domain everywhere
## else); the return value (when non-null) is a Vector2 in that SAME
## (row, col) pixel domain — NOT yet converted to world metres/district/
## canvas-local, that conversion is the caller's job via the usual
## layer1_pixel_to_world_m()/world_m_to_district() pipeline, exactly as if
## the target were itself an entry read out of `river_cells`.
##
## Deliberately returns the RAW grid-adjacent position rather than looking it
## up in a `river_cells` array — a D8 downstream pointer always names a real
## adjacent grid cell by construction (that is what D8 flow direction means),
## whether or not that specific cell independently appears in whatever
## (possibly filtered) `river_cells` list the caller is iterating.
static func d8_downstream_target(row: float, col: float, downstream_raw: int) -> Variant:
if downstream_raw < 0 or downstream_raw >= RIVER_DOWNSTREAM_SENTINEL_BASE:
return null # sentinel or malformed — no real direction to decode
var delta: Vector2i = D8_DIRECTION_DELTAS[downstream_raw]
return Vector2(row + float(delta.x), col + float(delta.y))
## T-1170 Ruling 5a — pure chord-chain CONSTRUCTION (no draw calls, no water
## clip, no canvas-local conversion): given `river_cells`/`river_class`/
## `river_downstream` (the raw decoded river_network sub-dict arrays) and a
## `granularity_v2` rung tag, returns an Array of
## `{"from": Vector2, "to": Vector2, "cls": int}` dicts — one per river cell
## whose class is visible at this rung AND whose river_downstream pointer
## resolves to a real direction (not a sentinel, not out of range, not
## missing). `from`/`to` are in the SAME (row, col) heightmap-pixel domain
## `river_cells` entries themselves use — the caller converts to world
## metres/district/canvas-local and applies the water clip, exactly as if it
## had built this list inline (this function exists so that CONSTRUCTION is
## unit-testable without a live render pass — draw_line() itself requires
## one, per this cluster's own "pure function tests are the gate" draw-smoke
## caveat, so the chain-walking logic that actually decides WHICH segments
## exist must not be entangled with the draw call that paints them).
##
## Split out of AtlasWindowNatureOverlay._draw_skeleton_chords() specifically
## so a test can assert "this exact set of segments was constructed from
## this exact fixture" (including the sentinel-chain-end and malformed-input
## cases) without a SubViewport/render context — matching this file's
## existing "geometry/construction here, draw calls in the overlay node"
## split for every other piece of this cluster.
static func build_skeleton_chords(
river_cells: Array, river_class: Array, river_downstream: Array, granularity_v2: String
) -> Array:
var chords: Array = []
for idx in range(river_cells.size()):
var c: Variant = river_cells[idx]
if not (c is Array and c.size() >= 2):
continue
var cls: int = int(river_class[idx]) if idx < river_class.size() else RIVER_CLASS_FALLBACK
if not skeleton_class_visible_at_rung(cls, granularity_v2):
continue
if idx >= river_downstream.size():
continue # no downstream pointer for this cell yet — no segment
var downstream_raw: int = int(river_downstream[idx])
var row: float = float(c[0])
var col: float = float(c[1])
var target: Variant = d8_downstream_target(row, col, downstream_raw)
if target == null:
continue # sentinel (MOUTH/EDGE_DRAIN/TERMINAL) or malformed direction — chain end
chords.append({"from": Vector2(row, col), "to": target, "cls": cls})
return chords
## T-1170 Ruling 5b/3h — pure course-polyline CONSTRUCTION (no draw calls):
## given one raw `RiverCourse` dict (as decoded off the wire — `class`,
## `points` (world-metres `[x,y]` pairs), `terminus` (a bare string tag)) and
## the window's own `granularity_v2`/`held_center`/`held_n`/`cell_pixel_size`,
## returns `null` if the course should not draw at all at this rung (class
## not visible, missing/degenerate points), or
## `{"canvas_pts": PackedVector2Array, "cls": int, "terminus": String}`
## ready for the caller to draw_polyline() + terminus-marker dispatch.
##
## Class defaults to RIVER_CLASS_FALLBACK (TRUNK) when missing, the same
## graceful-decode posture as the skeleton path's river_class fallback.
## `terminus` defaults to COURSE_TERMINUS_NONE when missing — an ordinary
## interior/no-marker ending, never crashing on an old/malformed payload.
## Malformed individual points are skipped (not fatal to the whole polyline,
## matching build_skeleton_chords()'s own "skip the bad entry, keep going"
## posture) — if fewer than 2 valid points remain after skipping, returns
## `null` (nothing to draw a line between).
static func build_course_render_plan(
course: Dictionary,
granularity_v2: String,
held_center: Vector2i,
held_n: int,
cell_pixel_size: float
) -> Variant:
var cls: int = int(course.get("class", RIVER_CLASS_FALLBACK))
if not course_class_visible_at_rung(cls, granularity_v2):
return null
var points_raw: Variant = course.get("points")
if not points_raw is Array or (points_raw as Array).size() < 2:
return null
var canvas_pts: PackedVector2Array = PackedVector2Array()
for pt: Variant in points_raw:
if not (pt is Array and pt.size() >= 2):
continue # malformed point — skip it, don't fail the whole polyline
var world_m := Vector2(float(pt[0]), float(pt[1]))
canvas_pts.append(world_m_to_canvas_local(world_m, held_center, held_n, cell_pixel_size))
if canvas_pts.size() < 2:
return null # too many malformed points left too few to draw a line
var terminus: String = str(course.get("terminus", COURSE_TERMINUS_NONE))
return {"canvas_pts": canvas_pts, "cls": cls, "terminus": terminus}
# =============================================================================
# T-1156 wave 1 / T-1170: per-rung nature-overlay visibility policy READERS.
# The policy TABLES themselves live up in the top-of-file const block per
# class-definitions-order.
# =============================================================================
## Whether a river cell of `river_class` should draw on the Region+ SKELETON
## path (Ruling 5a) at `granularity_v2`. An unrecognized rung tag falls back
## to Region's (fullest) visibility set — matching this cluster's existing
## "unrecognized -> most permissive/safest already-shipped behavior" posture
## (see AtlasWindowOverlay._filter_for_granularity_v2()'s own doc for the same
## fallback shape, there choosing the safer LINEAR filter for an unknown tag).
static func skeleton_class_visible_at_rung(river_class: int, granularity_v2: String) -> bool:
var visible: Array = SKELETON_CLASS_VISIBLE_BY_RUNG.get(
granularity_v2, SKELETON_CLASS_VISIBLE_BY_RUNG["Region"]
)
return visible.has(river_class)
## Whether a river class should draw on the District/Quarter COURSE path
## (Ruling 5b) at `granularity_v2`. No Region key exists in
## COURSE_CLASS_VISIBLE_BY_RUNG (Region never draws courses) — an unrecognized
## OR Region tag both fall back to an EMPTY array (nothing visible), the
## inverse fallback posture from skeleton_class_visible_at_rung() above,
## deliberately: falling back to "show everything" for a course-path query at
## an unexpected rung would risk drawing course polylines at Region, which no
## window response ever carries (courses are windowed-only content, Ruling 1)
## — failing to EMPTY is the safe direction on this specific table.
static func course_class_visible_at_rung(river_class: int, granularity_v2: String) -> bool:
var visible: Array = COURSE_CLASS_VISIBLE_BY_RUNG.get(granularity_v2, [])
return visible.has(river_class)
static func confluences_visible_at_rung(granularity_v2: String) -> bool:
return bool(CONFLUENCES_VISIBLE_BY_RUNG.get(granularity_v2, true))
static func mouths_visible_at_rung(granularity_v2: String) -> bool:
return bool(MOUTHS_VISIBLE_BY_RUNG.get(granularity_v2, true))
static func basins_visible_at_rung(granularity_v2: String) -> bool:
return bool(BASINS_VISIBLE_BY_RUNG.get(granularity_v2, true))
static func attractors_visible_at_rung(granularity_v2: String) -> bool:
return bool(ATTRACTORS_VISIBLE_BY_RUNG.get(granularity_v2, true))
## Per-class course polyline width (screen-space px, zoom=1.0 domain — see the
## const's own doc). Falls back to the stream (thinnest) width for an
## unrecognized class id, matching RIVER_DOT_RADIUS_BY_CLASS_REGION's own
## `.get(cls, 2.2)` call-site fallback shape on the skeleton side (there the
## fallback is trunk/widest — the caller passes a literal default; here the
## table itself owns a documented fallback since this is a NAMED reader, not
## an inline `.get()`).
static func course_class_width_px(river_class: int) -> float:
return float(COURSE_CLASS_WIDTH_PX.get(river_class, COURSE_CLASS_WIDTH_PX[RIVER_CLASS_STREAM]))
## Per-class course polyline opacity multiplier on COLOR_GEN_RIVER. Same
## unrecognized-class fallback posture as course_class_width_px() above.
static func course_class_opacity(river_class: int) -> float:
return float(COURSE_CLASS_OPACITY.get(river_class, COURSE_CLASS_OPACITY[RIVER_CLASS_STREAM]))
## Coordinator live-eyeball finding (2026-07-23): Araminta's ruling specifies
## nature-overlay marker sizes as SCREEN-SPACE px, constant regardless of
## zoom — but every draw call in this cluster (river dots, mouth rings, basin
## line widths, T-1170 course polylines/chords) executes inside `_canvas`, a
## Node2D whose `.scale` IS `_view_zoom` (AtlasWindowViewer._apply_transform()).
## A raw radius/width constant handed to draw_circle()/draw_arc()/
## draw_polyline() therefore gets multiplied by `_view_zoom` at render time —
## invisible at the Region orbital tile mosaic's fit zoom (~0.0063 for Lendel:
## a 2.2px trunk-river dot rasterizes at ~0.014 screen px, sub-pixel), even
## though the SAME drawing code produces a correctly-sized (visible) mouth
## ring at District's much larger fit zoom (~3.75, live capture confirmed
## this). The fix: every marker's draw-time radius/width must be pre-divided
## by `view_zoom` so the canvas transform's multiply cancels back out to the
## ruling's literal screen-space value. `view_zoom` is clamped to a small
## positive floor (MIN_ZOOM's own order of magnitude) to avoid a
## divide-by-zero/near-zero blowup on a degenerate zero-zoom caller — this
## floor is far below any legal `_view_zoom` (AtlasWindowViewer.MIN_ZOOM =
## 0.0005), so it is inert for every real caller and only guards a malformed
## test input.
static func zoom_compensated_size(screen_space_size: float, view_zoom: float) -> float:
return screen_space_size / maxf(view_zoom, 0.0001)
## T-1170 live-round finding (2026-07-23, coordinator/Araminta's pixel-scan
## of the course captures — D-district-courses.png/Q-quarter-courses.png
## showed a UNIFORM 1px hairline for the entire course, no width/opacity
## variation at all): zoom_compensated_size() is correct arithmetic (verified:
## 2.2 / 3.75 = 0.5867, and 0.5867 * 3.75 round-trips to 2.2 exactly — the
## compensation MATH has never been the bug), but it has NO FLOOR against
## Godot's own STROKE-WIDTH rasterizer minimum — confirmed empirically via a
## live A/B bracket (temporary instrumentation, since reverted): draw_line()/
## draw_polyline() called with a width in [0.6, 1.0) canvas-local units
## renders as a flat 1px hairline REGARDLESS of the input value, identically
## on both APIs (ruling out a draw_polyline()-specific quirk) — Godot's line
## rasterizer treats any width below ~1.0 the same as its historical
## width=-1.0 "hairline" sentinel, rather than continuing to shrink the
## antialiased stroke sub-pixel the way draw_circle()'s radius parameter
## does (mouth rings at the SAME District/Quarter zoom levels render
## correctly-sized — confirmed, radii have no equivalent floor).
##
## District/Quarter fit zooms (3.75/7.5+, and the player can zoom further
## within a rung) divide COURSE_CLASS_WIDTH_PX's 0.9-2.2px table values down
## to 0.12-0.59 canvas-local units — BELOW the 1.0 floor — so every course
## class collapses to the identical hairline the moment view_zoom exceeds
## roughly `screen_space_size` itself. This is the STROKE-WIDTH-SPECIFIC
## sibling of zoom_compensated_size() (which remains correct and unchanged
## for radii/point sizes, its own existing floor is a divide-by-zero guard
## only, not a rasterizer-minimum guard) — a SEPARATE function because the
## two draw families have genuinely different Godot-side minimums, not a
## single shared bug.
##
## The fix clamps the OUTPUT to a 1.0 canvas-local-unit floor — the closest
## representable value to "as thin as Godot's rasterizer can actually draw a
## non-hairline stroke" — rather than letting the divide produce a
## sub-floor value that Godot silently reinterprets as hairline anyway. This
## is an honest floor, not a workaround: below it, EVERY value (0.373, 0.6,
## 0.9999...) already rendered identically as hairline before this fix, so
## clamping to exactly 1.0 changes nothing about what could already be drawn
## at that zoom — it only stops different classes/rungs from silently
## collapsing to the SAME wrong result and starts drawing the class/opacity
## variation the ruling specifies. At extreme zoom-in (small view_zoom
## relative to the literal px value) the floor never engages — the same
## divide-then-scale math takes over exactly as design intends, matching
## zoom_compensated_size()'s own behavior at Region's tiny fit zoom.
static func zoom_compensated_stroke_width(screen_space_size: float, view_zoom: float) -> float:
return maxf(zoom_compensated_size(screen_space_size, view_zoom), 1.0)
## T-1170 live round (2026-07-23, coordinator's mouth-ring finding): the
## RADIUS-SMALLER-THAN-STROKE regime — a THIRD sibling to
## zoom_compensated_size()/zoom_compensated_stroke_width(), needed
## specifically for draw_arc() RING markers (mouth rings — the only
## draw_arc() caller in this file whose radius and stroke width are BOTH
## small, zoom-compensated values that can cross each other).
##
## Root-cause evidence (live A/B bracket, temporary instrumentation, since
## reverted — real running client via SR_LIVE=1, real x11/opengl3 driver):
## the ORIGINAL "zero ring pixels" report turned out to be a SEPARATE,
## already-correct-code issue — the terminus point legitimately sits near
## the requesting window's own edge, and the fit-and-center COVER strategy
## crops that edge off the visible viewport (screen_p verified computed as
## (1755, -191) against a 1920x1080 frame — above the top edge, not a
## drawing bug). Panning the view to re-center the SAME point (verified via
## AtlasWindowViewer.set_view()) proves the ring genuinely draws — but as a
## SOLID BLOB, not a hollow ring, at the production radius/stroke pair
## (radius=0.667, stroke=1.0 canvas-local units, Quarter fit zoom 7.5):
## draw_arc()'s stroke, centered ON the radius circle, extends inward past
## the circle's own center once stroke exceeds ~1x the radius, filling the
## hole. Bracket results (stroke fixed at 1.0 canvas-local, radius varied):
## radius=0.51 (~stroke/2) -> still a solid blob; radius=1.0 (=stroke) ->
## solid blob (the production case); radius=1.5 (1.5x stroke) -> hollow ring
## recovers; radius=2.0 (2x stroke) -> hollow ring, cleaner. The blob
## persists past the naive geometric threshold (radius > stroke/2, where an
## infinitely-thin/perfectly-antialiased ring would already have a hole)
## because draw_arc()'s low tessellation (18 points, this file's own call)
## plus antialiasing blur eat into the theoretical hole at these tiny
## absolute magnitudes — an empirical floor, not a derived one, chosen with
## margin over the observed 1.0x-blob/1.5x-hollow transition rather than
## shaving the boundary exactly.
##
## The fix: floor the RADIUS at `stroke * RING_RADIUS_STROKE_MULTIPLIER`
## (2.0, the top-of-file const — verified clean in the bracket above)
## whenever the naive zoom-compensated radius would fall below it — the
## same "floor the OUTPUT, never let a sub-threshold value reach Godot's
## renderer" pattern zoom_compensated_stroke_width() already established,
## applied to the paired radius/stroke relationship a lone-value floor
## can't express (unlike the stroke-width floor, this one's threshold is
## RELATIVE to another draw-time value, not an absolute constant). At every
## zoom where the naive radius already clears the floor on its own
## (Region's dot radii, or any District/Quarter case wide enough), this is
## an exact no-op — identical to calling zoom_compensated_size() directly.
static func zoom_compensated_ring_radius(
screen_space_radius: float, stroke_width_canvas_local: float, view_zoom: float
) -> float:
var naive_radius: float = zoom_compensated_size(screen_space_radius, view_zoom)
return maxf(naive_radius, stroke_width_canvas_local * RING_RADIUS_STROKE_MULTIPLIER)
@@ -1,750 +0,0 @@
extends Node2D
## Draws the whole-body Layer-1 nature overlays (rivers/basins/attractors,
## T-1156 wave 1) on the AtlasWindowViewer zoom ladder. Child of
## AtlasWindowViewer._canvas, ABOVE the terrain composite (AtlasWindowOverlay)
## and below UI chrome — same parent, same pan/zoom transform, drawn after so
## river dots/basin fills sit on top of the terrain colorizer.
##
## T-1170 (Ruling 5a): the Region+ river dot-scatter upgraded to CONNECTED
## STRAIGHT CHORDS via river_downstream (_draw_skeleton_chords()) — per
## Ruling 3b this chord chain IS the rung-truncated course at Region
## truncation, not an approximation of it. District/Quarter no longer draw
## the (now-retired) dot-scatter at all; they draw windowed course polylines
## instead (Ruling 5b, _draw_courses()).
##
## T-1170 Ruling 5b (B3): course polylines ride `DistrictWindowLayer.courses`
## — a SEPARATE data source from `_layer1` above (courses arrive on the
## WINDOWED response, `viewer.get_district_window()`, not the whole-body
## Layer-1 response this node requests via request_layer1()). _draw() is
## therefore two INDEPENDENT gates, not one: the skeleton/basin/attractor
## path gates on `_layer1 != null` (unchanged); the course path gates on
## `viewer.get_district_window()` being a Dictionary with a `courses` key,
## entirely independent of whether Layer-1 has arrived yet — a player who
## descends straight to District without the whole-body fetch completing
## still sees courses the moment the window arrives. NO T-1172 water clip on
## this path (Ruling 3g) — courses carry real rung-consistent termini
## server-side (the whole POINT of windowing course invention, Ruling 1d).
##
## This is a PORT, not a reactivation, of the retired planetary-screen draw
## code (atlas_marker_overlay.gd:523-572, _draw_gen_rivers/_draw_gen_basins/
## _draw_gen_attractors) — atlas_marker_overlay.gd stays retired/unreachable.
## The drawing IDEAS survive (polygon basins, glyph-free double-ring mouths,
## draw order basins-under-rivers-under-attractors; the dot-scatter idea
## itself is superseded at Region by T-1170's chord chain, see above); the
## COORDINATE MAPPING does not — the retired code projected onto a resident
## displayed heightmap TEXTURE (_gen_pos(), texture-fraction space) that this
## ladder screen has no equivalent of. Positions here go all the way through
## world metres -> district -> canvas-local
## (AtlasWindowGeometryNature.layer1_pixel_to_canvas_local()), the same frame every
## other drawn feature on this screen already shares, wrap-resolved exactly
## like the tile mosaic resolves terrain tiles.
##
## Data source: the WHOLE-BODY Layer-1 response (`layer1` key on the shared
## AtlasLayerResponse envelope, SimBridge.atlas_layers_received) — a SEPARATE
## fetch from the windowed DistrictWindowLayer composite AtlasWindowOverlay
## draws (both responses ride the SAME signal, discriminated by which
## envelope key is populated — SimBridge/atlas_map_protocol.gd's decode
## always includes both `layer1` and `district_window` keys, only one
## non-null per response, per that decoder's own doc). This node connects to
## SimBridge.atlas_layers_received DIRECTLY (mirroring
## atlas_window_tile_set.gd's own "one shared inbound signal, N independent
## consumers filtering by their own criteria" shape) rather than being routed
## through AtlasWindowViewer's own _on_atlas_layers_received() — the viewer
## stays at the gdlint max-file-lines cap with this node needing zero new
## lines in that function. Requested once per body entry via request_layer1()
## (call sites: the viewer's _enter_at_rung() and _enter_tile_mode() — every
## fresh descent funnels through one of those two; enter() itself is a thin
## wrapper over _enter_at_rung and has no call of its own), which owns
## clearing stale data on a body change itself
## (see that function's own doc, no separate reset() call needed) — cached
## thereafter, rivers are static per body, no re-request on pan/zoom/rung
## crossing.
##
## No `class_name` on purpose, matching every other viewer-owned helper in
## this cluster (atlas_overlay_bar.gd/atlas_window_request.gd/
## atlas_window_tile_set.gd, review #8 precedent): the owner
## (AtlasWindowViewer) passes itself to `_init()`.
##
## Redraw wiring: no _process() poll — AtlasWindowViewer/AtlasWindowOverlay
## already call `_overlay_node.queue_redraw()` on every pan/zoom/rung-
## crossing/window-arrival event; adding this node as a SIBLING of
## AtlasWindowOverlay under `_canvas` means the viewer's existing redraw call
## sites need exactly one more line each (`_nature_overlay.queue_redraw()`
## alongside the existing `_overlay_node.queue_redraw()`) — trivial wiring on
## the viewer, no new redraw PATH. This node's OWN _on_atlas_layers_received()
## also queue_redraw()s directly (the direct-signal-connection path bypasses
## the viewer's own arrival redraw calls, so it must trigger its own). Once
## layer1 arrives for a body it never goes stale until the next
## enter()/enter_orbital(), so unlike the tile mosaic's pending-tile poll,
## this node never needs a _process() self-heal.
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
# T-1170: the nature-overlay pixel-mapping + per-rung visibility policy split
# out of atlas_window_geometry.gd — see that file's own doc.
const AtlasWindowGeometryNature := preload("res://ui/implant/apps/atlas/atlas_window_geometry_nature.gd")
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
# T-1172: two-waterline clip — see that file's own header doc.
const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd")
## Reused verbatim from the retired atlas_marker_overlay.gd (Araminta's
## ruling: "reuse the retired palette exactly") — same values, same source of
## truth, just no longer read from the retired file.
const COLOR_GEN_RIVER: Color = Color(0.353, 0.647, 0.776, 1.0)
const COLOR_GEN_MOUTH: Color = Color(0.353, 0.647, 0.776, 1.0)
const COLOR_GEN_BASIN_FILL: Color = Color(0.20, 0.35, 0.55, 0.06)
const COLOR_GEN_BASIN_LINE: Color = Color(0.45, 0.65, 0.85, 0.45)
var viewer = null # AtlasWindowViewer (untyped to avoid cyclic ref)
## Whole-body Layer1Output dict (river_network/drainage_basins/attractors/
## grid_w/grid_h), or null before the first response for the current body.
## Set by _on_atlas_layers_received() (this node's own direct signal
## connection); request_layer1()/reset() manage the request lifecycle.
var _layer1: Variant = null
var _requested_body_id: String = ""
func _init(viewer_ref = null) -> void:
viewer = viewer_ref
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)
## Adopt a Layer-1 response — ignores non-Layer1 responses (the windowed
## DistrictWindowLayer envelope leaves `layer1` null, per
## atlas_response_from_raw()'s own doc) and responses for a body this node
## didn't ask for (the player navigated away while the request was in
## flight, or this node never issued a request at all — an empty
## `_requested_body_id` must never match an empty response `body_id`,
## matching request_layer1()'s own "empty body_id is never a valid request"
## guard), matching AtlasGenerationProxy.on_response()'s own staleness guard.
func _on_atlas_layers_received(response: Dictionary) -> void:
if _requested_body_id.is_empty():
return
var layer1: Variant = response.get("layer1")
if layer1 == null:
return
if str(response.get("body_id", "")) != _requested_body_id:
return
_layer1 = layer1
queue_redraw()
## Request a body's Layer-1 data — the ONE entry point every fresh-descent
## viewer path (enter()/_enter_at_rung()/_enter_tile_mode()) calls, so it owns
## its own reset-on-body-change instead of requiring callers to remember to
## call reset() first (a single call site per entry path stays a one-line
## addition to atlas_window_viewer.gd, keeping that file at its gdlint cap).
## Idempotent per body — a second call for the SAME body_id (e.g. a
## re-entrant enter_orbital()/rung-crossing re-descent on the body already
## showing) is a no-op, keeping the held data drawn rather than flashing it
## away and re-fetching. A DIFFERENT body_id clears the stale data FIRST (so
## the previous body's rivers never draw over the new body's terrain during
## the gap) then re-requests. No-op on an empty body_id (matches
## AtlasGenerationProxy.request()'s own guard).
func request_layer1(body_id: String) -> void:
if body_id.is_empty():
return
if body_id == _requested_body_id and _layer1 != null:
return
if body_id != _requested_body_id:
_layer1 = null
_requested_body_id = body_id
SimBridge.request_atlas_layers(body_id)
func get_layer1() -> Variant:
return _layer1
## T-1170 (B3): TWO INDEPENDENT draw gates, not one — see the class doc's own
## "two independent gates" paragraph. The skeleton/basin/attractor path
## (Layer-1, whole-body) is unchanged from wave 1; the course path (windowed,
## Ruling 5b) is a SEPARATE early-return chain reaching _draw_courses(),
## checked regardless of whether `_layer1` has arrived — a player descending
## straight into District/Quarter must see courses without waiting on the
## whole-body Layer-1 fetch this node happens to also own.
func _draw() -> void:
if viewer == null:
return
_draw_skeleton_path()
_draw_course_path()
## The pre-T-1170 draw gate, unchanged in shape: whole-body Layer-1
## (rivers/basins/attractors), gated on `_layer1` having arrived.
func _draw_skeleton_path() -> void:
if _layer1 == null:
return
var rn: Variant = _layer1.get("river_network")
if not rn is Dictionary:
return
var grid_w: float = float(_layer1.get("grid_w", 0))
var grid_h: float = float(_layer1.get("grid_h", 0))
if grid_w <= 0.0 or grid_h <= 0.0:
return
var granularity_v2: String = viewer.get_held_granularity_v2()
var radius_km: float = viewer.get_body_radius_km()
var held_center: Vector2i = viewer.get_held_center()
var held_n: int = viewer.get_held_n()
var cell_px: float = viewer.get_cell_pixel_size()
var cols: int = _cols_for_wrap(radius_km)
var ctx := {
"grid_w": grid_w,
"grid_h": grid_h,
"radius_km": radius_km,
"held_center": held_center,
"held_n": held_n,
"cell_px": cell_px,
"cols": cols,
"granularity_v2": granularity_v2,
# Coordinator live-eyeball finding (2026-07-23): every marker size
# below is drawn as a SCREEN-SPACE constant (Araminta's ruling), but
# draw calls execute inside _canvas, whose .scale IS view_zoom — a
# raw constant gets multiplied by that transform at render time,
# invisible at the Region orbital tile mosaic's tiny fit zoom
# (~0.006). zs() below pre-divides by view_zoom so the transform's
# multiply cancels back to the literal screen-space value. See
# AtlasWindowGeometryNature.zoom_compensated_size()'s own doc.
"view_zoom": viewer.get_view_zoom(),
}
if AtlasWindowGeometryNature.basins_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
"gen_basins"
):
_draw_basins(ctx)
if viewer.is_overlay_visible("gen_rivers"):
_draw_rivers(rn, ctx)
if AtlasWindowGeometryNature.attractors_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
"gen_attractors"
):
_draw_attractors(ctx)
## T-1170 Ruling 5b (B3): the District/Quarter windowed COURSE path — an
## entirely separate data source (`viewer.get_district_window()`) and draw
## gate from _draw_skeleton_path() above. Gated on `gen_rivers` (the SAME
## overlay-bar toggle the skeleton path uses — one player-facing "rivers"
## toggle covers both presentation surfaces, matching Araminta's ruling that
## the two paths are one continuous feature from the player's perspective,
## not two separate layers to independently show/hide).
##
## Region NEVER reaches this function's draw calls (course_class_visible_at_
## rung() has no Region key, always false there — Region draws the skeleton
## chord chain, never windowed course content, per Ruling 1). Tile mode
## (the Region orbital mosaic) also never carries `district_window` data at
## all (get_district_window() is single-window-mode-only, per that
## accessor's own doc — courses simply never reach this path in tile mode by
## construction, no separate is_tile_mode() guard needed here).
func _draw_course_path() -> void:
if not viewer.is_overlay_visible("gen_rivers"):
return
var window: Variant = viewer.get_district_window()
if not window is Dictionary:
return
var w: Dictionary = window
var courses: Variant = w.get("courses")
if not courses is Array:
return # missing `courses` field (old/pre-A2 payload) — draw nothing, see class doc
var granularity_v2: String = str(w.get("granularity_v2", "District"))
var ctx := {
"held_center": viewer.get_held_center(),
"held_n": viewer.get_held_n(),
"cell_px": viewer.get_cell_pixel_size(),
"granularity_v2": granularity_v2,
"view_zoom": viewer.get_view_zoom(),
}
for course: Variant in courses:
if not course is Dictionary:
continue
_draw_one_course(course, ctx)
## T-1170 Ruling 5b: one RiverCourse's polyline draw — class-filtered per
## COURSE_CLASS_VISIBLE_BY_RUNG, width/opacity from the COURSE_CLASS_WIDTH_PX/
## COURSE_CLASS_OPACITY companion tables (zoom-compensated via _zs(), the SAME
## discipline every other marker in this cluster follows — PR #195's
## stroke-width miss is the standing regression class this exists to
## prevent). Points arrive as `Vec<(i32,i32)>` WORLD METRES (Ruling 3h, NOT
## heightmap pixels — see world_m_to_canvas_local()'s own doc for why this is
## one conversion step shorter than the skeleton path). NO T-1172 water
## clip on this path (Ruling 3g) — courses carry real rung-consistent
## termini server-side; that is the entire point of windowing course
## invention (Ruling 1d).
##
## Terminus handling (Ruling 3h's CourseTerminus vocabulary):
## - Mouth: double-ring at the LAST point (the real coast anchor — mouths
## return as real geometry here, per Ruling 3g/3e).
## - EdgeDrain: no ring (Ruling 3f — pole-edge drains are grid artifacts,
## not river-meets-sea events; same disposition as the skeleton path's
## EDGE_DRAIN sentinel).
## - ContinuesBeyondWindow: draw to the last point, no marker (the course
## keeps going outside this window's crop — nothing to mark AT this
## window's edge, the polyline simply stops because the data stops).
## - None: an ordinary interior terminus (a headwater/confluence anchor
## inside this window) — no marker, same as ContinuesBeyondWindow's "just
## stop drawing" treatment; the two differ in MEANING (why the points ran
## out) but not in PRESENTATION (neither gets a ring).
## The actual gating/construction (class visibility, point decode, terminus
## lookup) is delegated to AtlasWindowGeometryNature.build_course_render_plan()
## — a pure function with no draw calls, unit-tested directly in
## test_atlas_window_geometry_nature.gd, the SAME split B2's
## build_skeleton_chords() already established. This function's own job is
## just the draw calls the plan feeds.
func _draw_one_course(course: Dictionary, ctx: Dictionary) -> void:
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, ctx["granularity_v2"], ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
if plan == null:
return
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
var cls: int = plan["cls"]
var terminus: String = plan["terminus"]
var width: float = _zs_stroke(AtlasWindowGeometryNature.course_class_width_px(cls), ctx)
var opacity: float = AtlasWindowGeometryNature.course_class_opacity(cls)
var color := Color(COLOR_GEN_RIVER.r, COLOR_GEN_RIVER.g, COLOR_GEN_RIVER.b, COLOR_GEN_RIVER.a * opacity)
draw_polyline(canvas_pts, color, width)
if terminus == AtlasWindowGeometryNature.COURSE_TERMINUS_MOUTH:
_draw_mouth(canvas_pts[canvas_pts.size() - 1], ctx)
# EdgeDrain/ContinuesBeyondWindow/None: no marker — draw to the last
# point and stop, per the doc above.
## Circumference in districts, for nearest_wrap_image()'s wrap resolution —
## mirrors AtlasWindowOverlay._draw_tile_mosaic()'s own `cols` computation
## exactly (same source, same reason: only meaningful in tile/orbital mode on
## a real body; a no-radius body has no periodicity, `cols=0` is
## nearest_wrap_image()'s own documented no-op passthrough).
func _cols_for_wrap(radius_km: float) -> int:
if radius_km <= 0.0:
return 0
return int(AtlasDescendGeometryRef.district_extent(radius_km).get("cols", 0))
## Zoom-compensated screen-space size — thin per-ctx wrapper over
## AtlasWindowGeometryNature.zoom_compensated_size() (see that function's own
## doc for the "why divide" rationale). Every draw_circle()/draw_arc() RADIUS
## in this file routes through this so Araminta's "constant on-screen size"
## ruling holds at every rung/zoom. NOT for stroke widths — see _zs_stroke()
## below, added T-1170 live round (2026-07-23) after the course-polyline
## hairline finding: draw_line()/draw_polyline() STROKE WIDTH arguments have
## a Godot-side rasterizer floor radii don't share (confirmed empirically —
## zoom_compensated_stroke_width()'s own doc has the full A/B evidence).
func _zs(screen_space_size: float, ctx: Dictionary) -> float:
return AtlasWindowGeometryNature.zoom_compensated_size(screen_space_size, ctx["view_zoom"])
## T-1170 live round (2026-07-23): the STROKE-WIDTH-specific sibling of
## _zs() — every draw_line()/draw_polyline()/draw_arc() STROKE WIDTH
## argument (never a radius/point-size) in this file routes through this
## instead of _zs(), so the width never crosses Godot's ~1.0-canvas-local-
## unit line-rasterizer floor and silently collapses to an
## indistinguishable hairline. See
## AtlasWindowGeometryNature.zoom_compensated_stroke_width()'s own doc for
## the full live-repro evidence (the course-path pixel scan that found this:
## a uniform 1px hairline with zero class/width variation in both District
## and Quarter captures).
func _zs_stroke(screen_space_size: float, ctx: Dictionary) -> float:
return AtlasWindowGeometryNature.zoom_compensated_stroke_width(screen_space_size, ctx["view_zoom"])
## T-1170 live round (2026-07-23, coordinator's mouth-ring blob finding): the
## RING-RADIUS-specific sibling of _zs()/_zs_stroke() — every draw_arc() ring
## marker whose radius and stroke are BOTH small, zoom-compensated values
## (currently: the two _draw_mouth() rings) routes its RADIUS through this
## instead of plain _zs(), so the radius never falls at-or-below its own
## paired stroke width and degenerates from a hollow ring into a solid blob.
## See AtlasWindowGeometryNature.zoom_compensated_ring_radius()'s own doc for
## the full A/B bracket evidence (radius=1x stroke -> blob, 1.5x -> hollow,
## floor set at 2x with margin). `stroke_canvas_local` is the ALREADY
## zoom-compensated stroke value (this function's own caller passes
## _zs_stroke()'s result, not a raw screen-space width) — the floor compares
## against the SAME canvas-local units the naive radius divide produces.
func _zs_ring_radius(screen_space_radius: float, stroke_canvas_local: float, ctx: Dictionary) -> float:
return AtlasWindowGeometryNature.zoom_compensated_ring_radius(
screen_space_radius, stroke_canvas_local, ctx["view_zoom"]
)
## Pixel (row, col) -> fractional district position, wrap-resolved against
## the currently-HELD view's own center (ctx["held_center"]) — the
## representative wrap-image _pos()'s canvas conversion needs. T-1172: this
## is also the value fed to the water-clip lookup (_is_drawn_water()) — NOT
## a separately-computed position — so the clip test and the actual drawn
## position can never disagree about which longitude wrap-image is meant.
## Tile-mode's own per-tile wrap re-resolution (AtlasWindowWaterClip.
## resolve_morphology_zone()) re-derives whichever wrap-image a SPECIFIC
## tile needs internally; feeding it this held-center-wrapped value is a
## safe, consistent starting representative either way (longitude is
## periodic — any wrap-image of the same district resolves to the same
## real-world position).
func _district(row: float, col: float, ctx: Dictionary) -> Vector2:
var world_m: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(
row, col, ctx["grid_w"], ctx["grid_h"], ctx["radius_km"]
)
var district: Vector2 = AtlasWindowGeometryNature.world_m_to_district(world_m)
var cols: int = ctx["cols"]
if cols > 0:
var held_center: Vector2i = ctx["held_center"]
var wrapped_col: float = float(
AtlasWindowGeometry.nearest_wrap_image(roundi(district.x), held_center.x, cols)
)
# Preserve the SUB-district fractional offset nearest_wrap_image()'s
# integer rounding would otherwise discard — river dots are not
# district-lattice-snapped (see world_m_to_district()'s own doc).
district.x = wrapped_col + (district.x - roundi(district.x))
return district
## Pixel (row, col) -> canvas-local, wrap-resolved to whichever longitude
## image is nearest the currently-held view — the SAME two-step
## (map-then-nearest-wrap) the tile mosaic draw path uses, just for a single
## point instead of a tile's four corners. Thin wrapper over _district() +
## AtlasWindowGeometry.district_to_canvas_local() (T-1172 split: callers that
## also need the water-clip test call _district() directly instead, so the
## SAME resolved district feeds both the draw position and the clip check).
func _pos(row: float, col: float, ctx: Dictionary) -> Vector2:
return AtlasWindowGeometry.district_to_canvas_local(
_district(row, col, ctx), ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
## T-1172: whether the composite cell covering `district` is drawn as water
## (OpenOcean/Lake) — FAILS OPEN (returns false, "not water", i.e. draw the
## dot) when no arrived composite data covers the position, per Tyre's rule
## 5 ("the clip is a presentation refinement, never a data gate"). Resolves
## through AtlasWindowWaterClip.resolve_morphology_zone(), which handles
## BOTH the single-window rung path and Region tile mode internally — this
## function never branches on viewer.is_tile_mode() itself, matching that
## function's own "single dispatch point" doc. `ctx["held_center"].x` is
## threaded through (live round 2, coordinator's trace) — tile-mode
## resolution must wrap each tile's CENTER toward held_center EXACTLY like
## AtlasWindowOverlay._draw_tile_mosaic()'s own `draw_col` computation, or
## the clip silently tests the wrong wrap-image of a seam tile (see
## resolve_morphology_zone()'s own doc for the live repro).
func _is_drawn_water(district: Vector2, ctx: Dictionary) -> bool:
var is_tile_mode: bool = viewer.is_tile_mode()
var single_window: Variant = null if is_tile_mode else viewer.get_district_window()
var tiles: Array = []
if is_tile_mode:
var tile_set = viewer.get_tile_set()
if tile_set != null:
tiles = tile_set.get_tiles()
var held_center: Vector2i = ctx["held_center"]
var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(
district, is_tile_mode, single_window, tiles, ctx["cols"], held_center.x
)
if zone == AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA:
return false
return AtlasOverlayColors.is_morphology_water(zone)
## T-1172 clip — RETAINED for this Region-skeleton path only (Ruling 3g: the
## clip retires for the District/Quarter COURSE-drawing rungs — see
## _draw_courses() below — because courses carry real rung-consistent
## termini and the clip's job is done there; the Region skeleton path keeps
## drawing against a rung-dependent drawn coast and needs the presentation-
## frame reconciliation until Region itself goes windowed, T-1143 ruling 2).
## River cells, confluences, and mouths are each dropped (strict, no snap)
## when their resolved composite cell reads as drawn water — see
## AtlasWindowWaterClip's own header doc for the two-waterline rationale.
## Basins are explicitly OUT OF SCOPE (Tyre's rule 4) — untouched.
##
## T-1170 Ruling 5a: at Region+, river cells draw as CONNECTED STRAIGHT
## CHORDS (each river cell to its river_downstream neighbor) instead of a
## dot-scatter — see _draw_skeleton_chords() below, called from here.
## District/Quarter no longer reach this function's river-cell/confluence
## loop at all (SKELETON_CLASS_VISIBLE_BY_RUNG has empty District/Quarter
## entries) — they draw via _draw_courses() instead (Ruling 5b), wired from
## _draw().
func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
var granularity_v2: String = ctx["granularity_v2"]
_draw_skeleton_chords(rn, ctx)
if AtlasWindowGeometryNature.confluences_visible_at_rung(granularity_v2):
for cf: Variant in rn.get("confluences", []):
if cf is Array and cf.size() >= 2:
var district: Vector2 = _district(float(cf[0]), float(cf[1]), ctx)
if _is_drawn_water(district, ctx):
continue
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
var radius: float = _zs(AtlasWindowGeometryNature.RIVER_CONFLUENCE_RADIUS_REGION, ctx)
draw_circle(p, radius, COLOR_GEN_RIVER)
if AtlasWindowGeometryNature.mouths_visible_at_rung(granularity_v2):
for m: Variant in rn.get("mouths", []):
if m is Array and m.size() >= 2:
var district: Vector2 = _district(float(m[0]), float(m[1]), ctx)
# T-1172 rule 3: mouths are SUPPRESSED (not snapped, not
# dimmed) when their cell reads as drawn water — a mouth is
# the worst-case disagreement by construction (the last LAND
# cell on the RAW coast; wherever the drawn coast is
# displaced inland, the mouth renders offshore).
if _is_drawn_water(district, ctx):
continue
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
_draw_mouth(p, ctx)
## T-1170 Ruling 5a — the Region+ skeleton-chord draw: each river cell whose
## class is visible at this rung draws a STRAIGHT LINE SEGMENT to its
## `river_downstream` neighbor. `river_network.river_downstream` is a u8 PER
## RIVER CELL (index-aligned with river_cells, the SAME alignment convention
## river_class already uses) encoding a **D8 DIRECTION** (0-7, see
## AtlasWindowGeometryNature.D8_DIRECTION_DELTAS — NOT a river_cells index;
## the target cell's grid position is `c + delta`, decoded via
## AtlasWindowGeometryNature.d8_downstream_target()), with SENTINEL values
## `>= RIVER_DOWNSTREAM_SENTINEL_BASE` for MOUTH/EDGE_DRAIN/reserved-TERMINAL
## (Ruling 2c). Direction-index convention and sentinel values CONFIRMED
## against Dudley's A1 (server/src/atlas/drainage.rs:35-44, landed
## 0fea69feb; relayed by the coordinator, not guessed) — MOUTH=8,
## EDGE_DRAIN=9, TERMINAL=10 (reserved/unused), directions 0-7 = N/S/E/W/NE/
## NW/SE/SW. Every place this convention is encoded is a SINGLE named
## constant group on AtlasWindowGeometryNature (D8_DIRECTION_DELTAS /
## RIVER_DOWNSTREAM_SENTINEL_BASE / RIVER_DOWNSTREAM_MOUTH /
## RIVER_DOWNSTREAM_EDGE_DRAIN / RIVER_DOWNSTREAM_TERMINAL) — see that file's
## own doc.
##
## Per Ruling 3b, these chords ARE the rung-truncated course at Region (no
## octave warp survives at Region spacing — the invented course degenerates
## exactly to this chord), NOT an approximation of it — one function (the
## server's course inventor, eventually), every rung, this is simply what it
## looks like with zero surviving octaves.
##
## Sentinel dispositions: MOUTH and EDGE_DRAIN both END the chain — no
## downstream segment is drawn for a sentinel-terminated cell (there is no
## real neighbor cell to connect to). EDGE_DRAIN gets NO mouth ring (Ruling
## 3f — pole-edge drains are grid artifacts, not river-meets-sea events; the
## existing mouths array/_draw_mouth() call in _draw_rivers() is already
## scoped to real MOUTH sentinels via rn["mouths"], server-side, per Ruling
## 3f's "extract_river_network stops classifying grid-edge exits into
## mouths" — this function draws NO ring at all, sentinel or otherwise, that
## is _draw_rivers()'s mouths-array job).
##
## `river_downstream` missing or shorter than `river_cells` (pre-T-1170
## payload — Dudley's `#[serde(default)]` empty-Vec contract, the exact same
## graceful-decode shape river_class already established) means NO chord
## segment can be drawn for that index at all (there is no real downstream
## direction to connect to, unlike the class-fallback case where TRUNK is a
## safe visual default) — those cells draw NOTHING at Region until the field
## arrives, a graceful (not crashing) degradation, documented here rather
## than silently falling back to the old dot-scatter (which would require
## carrying that whole second code path forward past this ticket). The
## decoded target cell is ALSO not required to appear in `river_cells` itself
## (the chord draws to the raw grid position `c + delta`, not to a looked-up
## river-cell entry) — a downstream D8 pointer always names a real adjacent
## grid cell by construction, whether or not that cell independently made it
## into the (possibly rung/threshold-filtered) `river_cells` list.
##
## The actual chain-CONSTRUCTION (which segments exist at all, given the
## fixture and rung) is delegated to
## AtlasWindowGeometryNature.build_skeleton_chords() — a pure function with
## no draw calls, unit-tested directly in
## test_atlas_window_geometry_nature.gd (the sentinel/malformed/visibility
## cases). This function's own job is the remaining per-segment work that DOES
## need the overlay's own state: the water clip (_segment_touches_drawn_water(),
## needs the composite/tile data only the overlay holds) and the actual
## draw_line() call (needs a live render pass).
func _draw_skeleton_chords(rn: Dictionary, ctx: Dictionary) -> void:
var granularity_v2: String = ctx["granularity_v2"]
var river_cells: Array = rn.get("river_cells", [])
var river_class: Array = rn.get("river_class", [])
var river_downstream: Array = rn.get("river_downstream", [])
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
river_cells, river_class, river_downstream, granularity_v2
)
for chord: Dictionary in chords:
var from_rc: Vector2 = chord["from"]
var to_rc: Vector2 = chord["to"]
var cls: int = chord["cls"]
var from_district: Vector2 = _district(from_rc.x, from_rc.y, ctx)
var to_district: Vector2 = _district(to_rc.x, to_rc.y, ctx)
# T-1172 clip (Region-only, retained per Ruling 3g): a segment is
# clipped when EITHER endpoint OR its midpoint resolves to drawn
# water — see _segment_touches_drawn_water()'s own doc for why this
# three-point rule was chosen over an endpoints-only test.
if _segment_touches_drawn_water(from_district, to_district, ctx):
continue
var from_p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
from_district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
var to_p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
to_district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
var width: float = AtlasWindowGeometryNature.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2)
draw_line(from_p, to_p, COLOR_GEN_RIVER, _zs_stroke(width, ctx))
## T-1172 clip rule for a CHORD SEGMENT (as opposed to a single point, which
## is what the pre-T-1170 dot-scatter clipped): tested at the segment's TWO
## ENDPOINTS AND its MIDPOINT, clipping the whole segment if ANY of those
## three samples resolves to drawn water. **Decision, documented per the
## ruling's ask ("pick the visually cleaner rule, document it, test it"):**
## endpoints-only was rejected because a chord that DIPS through a coastal
## composite cell without either endpoint landing in it (a river cell just
## inland connecting to a river cell just inland on the OTHER side of a
## narrow drawn-water inlet/bay) would draw a visible line segment crossing
## open water with neither end clipped — worse than the old dot-scatter's
## per-point clip, which never had this failure mode since a dot has no
## extent to cross anything. Midpoint-only was rejected symmetrically: a
## long chord whose midpoint happens to land on drawn land while both real
## endpoints sit in drawn water would draw an uncllipped segment starting and
## ending in the ocean. Three-point (both ends + midpoint) catches the
## common cases of both failure modes at negligible extra cost (one more
## _is_drawn_water() lookup per segment) without requiring a full
## segment-rasterization walk — chords at Region spacing (~76 km apart) are
## short enough relative to Region's own 204.8 km composite cell that a
## single midpoint sample is a reasonable proxy for "does this segment pass
## through this cell", matching the coarseness the Region rung already draws
## at everywhere else in this file (204.8 km cells, not sub-cell precision).
func _segment_touches_drawn_water(from_district: Vector2, to_district: Vector2, ctx: Dictionary) -> bool:
if _is_drawn_water(from_district, ctx):
return true
if _is_drawn_water(to_district, ctx):
return true
var mid_district: Vector2 = (from_district + to_district) * 0.5
return _is_drawn_water(mid_district, ctx)
## Double-ring sea-terminus marker — verbatim geometry from the retired
## atlas_marker_overlay.gd _draw_gen_rivers() (:536-539). Mouths never fade
## (Araminta's ruling: "a mouth is always a landmark") — same styling at
## every rung it's visible at (Region, District; never Quarter). T-1170:
## also the marker for REAL course termini (Ruling 5b/3e) — one geometry
## function, both presentation surfaces.
func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
var ring_stroke: float = _zs_stroke(1.5, ctx)
draw_arc(
p,
_zs_ring_radius(AtlasWindowGeometryNature.MOUTH_RING_RADIUS, ring_stroke, ctx),
0.0,
TAU,
18,
COLOR_GEN_MOUTH,
ring_stroke
)
var halo := Color(
COLOR_GEN_MOUTH.r,
COLOR_GEN_MOUTH.g,
COLOR_GEN_MOUTH.b,
AtlasWindowGeometryNature.MOUTH_HALO_ALPHA
)
var halo_stroke: float = _zs_stroke(1.0, ctx)
draw_arc(
p,
_zs_ring_radius(AtlasWindowGeometryNature.MOUTH_HALO_RADIUS, halo_stroke, ctx),
0.0,
TAU,
22,
halo,
halo_stroke
)
## Basins — Region only, binary (no fade), per the ruling. Polygon fill +
## boundary polyline, verbatim geometry from the retired
## atlas_marker_overlay.gd _draw_gen_basins() (:542-557), coordinate mapping
## replaced with _pos() (this file's wrap-aware canvas-local mapping) in place
## of the retired _gen_pos() texture-fraction mapping. The FILL polygon's
## points are positions (never zoom-compensated — the fill must track the
## real district-space shape); only the boundary LINE's width is a
## screen-space marker size and goes through _zs().
func _draw_basins(ctx: Dictionary) -> void:
for b: Variant in _layer1.get("drainage_basins", []):
if not b is Dictionary:
continue
var boundary: Array = b.get("boundary", [])
var pts: PackedVector2Array = PackedVector2Array()
for pt: Variant in boundary:
if pt is Array and pt.size() >= 2:
pts.append(_pos(float(pt[0]), float(pt[1]), ctx))
if pts.size() < 2:
continue
if pts.size() >= 3:
draw_colored_polygon(pts, COLOR_GEN_BASIN_FILL)
var loop: PackedVector2Array = pts.duplicate()
loop.append(pts[0])
draw_polyline(loop, COLOR_GEN_BASIN_LINE, _zs_stroke(0.8, ctx), true)
## Attractors — Region only, wave 1 (per the ruling; District/Quarter never
## reach this function since _draw() gates the whole call on
## attractors_visible_at_rung()). Ported from the retired
## atlas_marker_overlay.gd _draw_gen_attractors()/_draw_attractor_shape()
## (:560-...) — the shape vocabulary (7 attractor-type glyphs) is Araminta's
## existing design, unchanged; only the coordinate mapping moves to _pos()
## and the size is zoom-compensated before reaching the shape drawer (that
## function stays a pure "draw this size at this position", zoom-agnostic).
func _draw_attractors(ctx: Dictionary) -> void:
for a: Variant in _layer1.get("attractors", []):
if not a is Dictionary:
continue
var strength: float = float(a.get("strength", 0.0))
if strength < AtlasWindowGeometryNature.ATTRACTOR_MIN_STRENGTH:
continue
var pos_rc: Variant = a.get("position")
if not pos_rc is Array or pos_rc.size() < 2:
continue
var p: Vector2 = _pos(float(pos_rc[0]), float(pos_rc[1]), ctx)
var size: float = _zs(5.0 + strength * 4.0, ctx)
var color: Color = AtlasOverlayColors.sub_biome_color(str(a.get("sub_biome", "")))
_draw_attractor_shape(str(a.get("attractor_type", "")), p, size, color, _zs_stroke(1.0, ctx))
## Attractor type -> marker shape — from the retired atlas_marker_overlay.gd
## _draw_attractor_shape(). `size` AND `px_w` (the 1-screen-px stroke unit)
## both arrive ALREADY zoom-compensated from _draw_attractors() — this
## function stays a pure "draw at these literal dimensions" primitive with no
## ctx/zoom knowledge of its own. px_w exists because Godot multiplies stroke
## WIDTH args by the canvas scale exactly like radii (PR #195 review, Tyre
## I1: the retired code's raw 1.0/2.0 widths rasterized at ~0.01px at the
## Region orbital fit zoom — the same sub-pixel failure the dot/ring
## compensation fixed, missed on glyph outlines).
func _draw_attractor_shape(
atype: String, pos: Vector2, size: float, color: Color, px_w: float
) -> void:
match atype:
"RiverMouth":
draw_circle(pos, size, color)
"Confluence":
draw_circle(pos, size * 0.8, color)
draw_arc(pos, size * 1.3, 0.0, TAU, 12, color, px_w)
"Alpine", "PassEntrance":
var pts := PackedVector2Array(
[
pos + Vector2(0, -size),
pos + Vector2(-size * 0.8, size * 0.6),
pos + Vector2(size * 0.8, size * 0.6),
]
)
draw_colored_polygon(pts, color)
"Coastal", "NaturalHarbor":
draw_arc(pos, size, PI * 0.15, PI * 0.85, 10, color, 2.0 * px_w)
"Oasis":
draw_circle(pos, size * 0.5, color)
for i in range(6):
var ang: float = TAU * float(i) / 6.0
draw_line(pos, pos + Vector2(cos(ang), sin(ang)) * size, color, px_w)
_:
draw_circle(pos, size * 0.6, color)
@@ -1,600 +0,0 @@
class_name AtlasWindowOverlay
extends Node2D
## Draws the DistrictWindowLayer composite for AtlasWindowViewer (T-1138,
## D-226 T-1124 amendment §5). Child of AtlasWindowViewer._canvas so it
## inherits the pan transform (zoom is client-side texture zoom on the
## already-held composite, §5 — never a re-fetch).
##
## Draw order (bottom to top), matching the amendment's compositing model:
## 1. Base layer — morphology hue, lightness-modulated by elev_q. Always on,
## no toggle id (§5: "it IS this screen's terrain layer").
## 2. Toggle overlays (mutually independent, at most one drawn per cell —
## each REPLACES the base read for that cell rather than blending, so
## switching between temp/moisture/veg never fights the base hue):
## gen_dw_temp / gen_dw_moisture / gen_dw_veg.
## 3. Glaciation ice-tint MODIFIER — composited over whichever layer is
## showing (base or a toggle), always-on, not a toggle id of its own.
##
## Reads window data via viewer.get_district_window() (a Dictionary or null) — this
## overlay draws nothing until the viewer has a window (border-fade during
## the wait is the VIEWER's job, drawn separately underneath this node, not
## here — this node is purely "draw the composite when there is one").
##
## T-1145 item 3 (interim presentation, pending the T-1143 design pass):
## COMPOSITE_SMOOTH := true renders the composite as an n x n Image (one
## pixel per district, EXACT same per-cell color pipeline this file always
## had — _cell_color()/_apply_glaciation() are UNCHANGED) converted to an
## ImageTexture and drawn scaled with texture-filtered sampling, instead of
## n*n flat draw_rect() calls. The crisp per-cell rect path SURVIVES behind
## the const (COMPOSITE_SMOOTH := false) so T-1143's design pass can compare
## both renderings directly — this is explicitly an INTERIM presentation, not
## the final answer on district-tier legibility (T-1143 owns that design).
##
## T-1161 (Araminta's per-rung filter ruling, PR #192 review follow-up): the
## smoothed path's PIPELINE (texture-vs-flat-rects) and its SAMPLING FILTER
## (how the GPU reads that texture) are now two INDEPENDENT axes, not one
## bundled choice:
## - Axis 1 — PIPELINE: COMPOSITE_SMOOTH (compile-time const, unchanged by
## this ticket). true = draw a texture; false = per-cell draw_rect(). The
## composite is a TEXTURE at every rung when COMPOSITE_SMOOTH is true —
## this axis does not vary per rung.
## - Axis 2 — FILTER: _filter_for_granularity_v2() (runtime, keyed on rung
## IDENTITY via granularity_v2, T-1161). Region (incl. the orbital tile
## mosaic) samples TEXTURE_FILTER_NEAREST — GPU bilinear stretch at
## 204.8 km/cell reads as a near-featureless soft gradient, technically
## honest LoD but visually indistinguishable from the coarse-composite
## smoothing-over-absence the mandate was written to kill (T-1161's own
## description). District and Quarter sample TEXTURE_FILTER_LINEAR — cell
## density there reads as texture, not smoothing-over-absence, so the
## bilinear blend is earned. No hysteresis, no px-per-cell threshold —
## the filter is a pure function of which rung's data is being drawn.
## The crisp draw_rect() path has no sampling-filter concept at all (no
## texture involved) — its comparison/debug role per the paragraph above is
## unaffected by this axis.
##
## The texture is REBUILT only when its inputs change (the window object
## itself — a new DistrictWindowLayer arriving is a new Dictionary, checked
## by REFERENCE via is_same(), not a per-field deep compare — or the active
## toggle overlay id), not per frame/per redraw. Panning and zooming redraw
## this node constantly (every _apply_transform() call) but never touch
## window/overlay state, so the common case (panning within an already-held
## window) is zero rebuild cost — draw_texture_rect() on an already-built
## ImageTexture, same as any other texture draw.
##
## T-1152/T-1153: `n` (the response's echoed district extent) and the DERIVED
## cell-grid side length are now DIFFERENT quantities at every rung except
## District — cell_grid_side_for_window() computes the latter from `n` and
## the response's own `granularity_v2` echo (mirroring the server's
## `WindowGranularity::cell_grid_side` exactly), so a Region-rung response (a
## FAR SPARSER cell grid than its district extent — see that Rust doc's
## "inversion" note) renders through the exact same colorizer pipeline as
## District/Quarter, satisfying the design doc §6 "one colorizer family, no
## per-rung palettes" encoding-continuity requirement — no branch in
## _cell_color()/_temp_cell_color()/etc. below needed any change at all.
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
const REGION_TEMP_NONE_DC: int = AtlasOverlayColors.REGION_TEMP_NONE_DC
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
# T-1153, live round 3: TILE_N (the per-tile district extent) for the mosaic draw path.
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
# Live round 5: `cols` (circumference in districts) for the mosaic's wrap-image draw fix.
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
## T-1145 item 3: interim presentation toggle — true renders the smoothed
## Image/ImageTexture composite; false keeps the original crisp per-cell
## draw_rect() path (both call the SAME _cell_color()/_apply_glaciation()
## pipeline, so switching this never changes WHAT color a cell reads, only
## HOW it's rendered). Left as a compile-time const, not a runtime toggle —
## T-1143's design pass is expected to pick a winner, not ship a player-
## facing switch between them.
const COMPOSITE_SMOOTH: bool = true
## Moisture ramp reuses SUB_BIOME_COLORS' dry-sand->wet-teal ENDPOINTS (§5) —
## not the categorical lookup itself (that's keyed by sub-biome NAME, not a
## 0-100 quantity). Endpoints pulled from the existing dry/wet entries in that
## table: Desert (arid/dry) and TropicalWet (wet/coastal).
const COLOR_MOISTURE_DRY: Color = Color(0.78, 0.62, 0.35, 1.0) # sand — matches SUB_BIOME_COLORS.Desert
const COLOR_MOISTURE_WET: Color = Color(0.25, 0.72, 0.65, 1.0) # teal — matches SUB_BIOME_COLORS.TropicalWet
var viewer = null # AtlasWindowViewer (untyped to avoid cyclic ref)
## T-1145 item 3: texture rebuild cache — see the class doc's "REBUILT only
## when its inputs change" paragraph. _cache_window_ref is compared by
## REFERENCE (is_same()), not value — a fresh DistrictWindowLayer response is
## always a NEW Dictionary object (built by atlas_map_protocol.gd's decode),
## so reference identity is both correct AND far cheaper than a deep compare
## of a potentially-4096-cell dictionary on every _draw() call.
var _cached_texture: ImageTexture = null
var _cache_window_ref: Variant = null
var _cache_active_toggle: String = ""
## Live round 4 fix: per-TILE texture cache, keyed by tile index — mirrors
## the single-window cache above, but one slot per mosaic tile (a Dictionary
## of `{window_ref, active_toggle, texture}`, since the mosaic doesn't have a
## single fixed set of tiles the way the single-window path has a single
## fixed field). Building a brand-new, UNSTORED `ImageTexture` every
## `_draw()` call (the round-3 version) left it referenced only by a local
## variable — nothing keeps the RID alive past the function returning, which
## raced against the RenderingServer's deferred draw-command flush and
## rendered as a blank/white tile (the round-4 "pitch black"/white-mosaic
## repro's second half, beyond the coordinate fix above): the CPU-side pixel
## data was provably correct (sampled directly), but the GPU-side texture
## backing it could be gone by composite time. Caching each tile's texture
## as a class-owned Dictionary entry (same reference-identity rebuild-only-
## on-change discipline as `_cached_texture`) keeps it alive exactly as long
## as the single-window composite's own texture already is.
var _tile_texture_cache: Dictionary = {}
func _draw() -> void:
if viewer == null:
return
if viewer.is_tile_mode():
_draw_tile_mosaic()
return
var window: Variant = viewer.get_district_window()
if not window is Dictionary:
return
var w: Dictionary = window
var n: int = int(w.get("n", 0))
if n <= 0:
return
var morphology: Variant = w.get("morphology")
if not (morphology is PackedByteArray or morphology is Array):
return
# T-1152/T-1153: `n` (the response's echoed field) is ALWAYS window extent
# in DISTRICTS at every rung (Dudley's wire contract, DistrictWindowLayer.n's
# own doc) — the per-cell arrays (morphology/elev_q/etc.) are sized by the
# DERIVED cell-grid side, `cell_grid_side_for_window()` below, which equals
# `n` only at District granularity. Quarter packs MORE cells into the same
# n-district extent (`n*4`); Region packs FEWER, since one region cell
# spans 100 districts (`round(n/100).max(1)`). The on-screen EXTENT stays
# `n * cell_px` regardless of rung (CELL_PIXEL_SIZE is defined as "one
# DISTRICT at zoom=1.0" — see the viewer's own doc on that constant) so a
# rung swap at a fixed pan/zoom never jumps the composite's screen footprint
# (§6 "no layout jump") — only the TEXTURE RESOLUTION packed into that
# footprint changes, exactly the "same colorizer family, different LoD"
# picture the design doc describes.
var grid_side: int = cell_grid_side_for_window(w)
if grid_side <= 0:
return
var cell_px: float = viewer.get_cell_pixel_size()
var active_toggle: String = _active_toggle_overlay()
if COMPOSITE_SMOOTH:
_draw_smoothed_composite(w, n, grid_side, cell_px, active_toggle)
else:
_draw_crisp_composite(w, grid_side, n, cell_px, active_toggle)
## T-1153, live round 3/4 (Jeroen's ruling, design doc §4): the orbital
## rest-state MOSAIC draw path — one call to the EXISTING single-tile
## composite-building logic (`_rebuild_texture_if_needed()`/
## `_draw_smoothed_composite()`'s own per-tile equivalent below) PER TILE,
## each positioned at its own LOCAL offset in the SAME canvas-local
## coordinate frame the single-window path (and fit_window_view()/
## screen_center_to_district()) already use.
##
## **Live round 4 fix:** the round-3 version placed tiles relative to
## absolute district (0,0) directly (`(tile.center - TILE_N/2) * cell_px`),
## which does NOT match `_fit_and_center()`'s own convention — canvas-local
## (0,0) is `held_center - held_n/2` (AtlasWindowGeometry.
## district_to_canvas_local()'s own doc), and in tile mode `held_n` is the
## WHOLE BODY's extent, not TILE_N. That mismatch pushed the entire mosaic
## off-canvas (round 4's "pitch black" repro) — silent, since nothing
## errors, it just draws somewhere the viewport never shows. Fixed by
## routing every tile's placement through `district_to_canvas_local()` with
## the VIEWER's own `held_center`/`held_n`, the exact reference frame every
## other canvas-local consumer (fit/pan/reselect) already agrees on. Tiles
## that haven't arrived yet (`tile["window"] == null`) are simply SKIPPED —
## no per-tile placeholder draw, letting COLOR_BG show through as the honest
## "nothing here yet" read (the viewer's own `_draw()` already documents why
## no separate whole-viewport fade is needed on top of this).
##
## **Live round 5 fix:** `tile["center"]` is CANONICAL (wrapped into
## `[0, cols)` by `compute_tile_grid()` — correct for REQUESTS/cache keys,
## since longitude is periodic and a canonical column is the single-valued
## key both sides of the wire agree on). But `district_to_canvas_local()`
## is a pure LINEAR function with no wrap concept — handed a canonical
## column directly, it places the tile at exactly ONE of its infinitely
## many equivalent on-screen positions (`col + k*cols`), which is only the
## visually-correct one by coincidence. Lendel's own repro: the tile whose
## true position is immediately WEST of the canonical origin canonicalizes
## to column 12739 (`-6400 mod 19139`) — drawn there directly, it lands
## off-canvas RIGHT, leaving the mosaic's actual LEFT third black. Fixed by
## re-expressing each tile's column via `nearest_wrap_image()` — whichever
## wrap-image is closest to `held_center`, i.e. the one actually near the
## current view — BEFORE handing it to `district_to_canvas_local()`.
func _draw_tile_mosaic() -> void:
var tile_set = viewer.get_tile_set()
if tile_set == null:
return
var cell_px: float = viewer.get_cell_pixel_size()
var active_toggle: String = _active_toggle_overlay()
var held_center: Vector2i = viewer.get_held_center()
var held_n: int = viewer.get_held_n()
var half_tile: float = float(AtlasWindowGeometryRef.TILE_N) * 0.5
var tiles: Array = tile_set.get_tiles()
var cols: int = int(
AtlasDescendGeometryRef.district_extent(viewer.get_body_radius_km()).get("cols", 0)
)
for i in range(tiles.size()):
var tile: Dictionary = tiles[i]
var window: Variant = tile["window"]
var center: Vector2i = tile["center"]
var draw_col: int = AtlasWindowGeometryRef.nearest_wrap_image(center.x, held_center.x, cols)
var tile_top_left: Vector2 = Vector2(
float(draw_col) - half_tile, float(center.y) - half_tile
)
var local_origin: Vector2 = AtlasWindowGeometryRef.district_to_canvas_local(
tile_top_left, held_center, held_n, cell_px
)
var extent: float = float(AtlasWindowGeometryRef.TILE_N) * cell_px
# Coordinator ask (cold-start dossier): a bare COLOR_BG gap for an
# unarrived tile reads as broken, not "still working" — a cold
# server's first AnalyzeBody can take seconds, during which every
# tile in the mosaic is exactly this state at once. Same treatment
# the single-window path already gives its OWN no-composite-yet wait
# (AtlasWindowViewer._draw_border_fade()) — read off the live
# `viewer` instance rather than a preload of its script (that field
# is deliberately untyped to avoid a cyclic ref, see its own doc; a
# const is reachable through an instance either way).
if not window is Dictionary:
draw_rect(Rect2(local_origin, Vector2(extent, extent)), viewer.COLOR_BORDER_FADE)
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
_draw_one_tile(i, w, grid_side, local_origin, extent, active_toggle)
## One tile's own composite — the SAME crisp/smoothed per-cell pipeline the
## single-window path uses (_cell_color()/_apply_glaciation(), UNCHANGED),
## just drawn at `local_origin` instead of always at (0,0). Each tile gets
## its OWN texture-rebuild cache slot in `_tile_texture_cache`, keyed by
## `tile_index` — sharing ONE `_cached_texture` slot across all tiles (the
## single-window field) would thrash on every draw call as different tiles'
## windows compete for it.
##
## T-1161: every mosaic tile is a Region-rung request (atlas_window_tile_set.gd
## requests tiles at AtlasWindowRequest.GRANULARITY_V2_REGION), so the mosaic
## as a whole is in scope for the Region -> NEAREST ruling. As with the
## single-window path, the filter is read from THIS tile's own echoed `w`
## rather than assumed, via the shared `_filter_for_granularity_v2()` helper
## — one policy, two call sites, no duplicated match statement.
func _draw_one_tile(
tile_index: int,
w: Dictionary,
grid_side: int,
local_origin: Vector2,
extent: float,
active_toggle: String
) -> void:
if not COMPOSITE_SMOOTH:
_draw_crisp_tile(w, grid_side, local_origin, extent, active_toggle)
return
var tile_texture: ImageTexture = _rebuild_tile_texture_if_needed(
tile_index, w, grid_side, active_toggle
)
if tile_texture == null:
return
var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT))
texture_filter = _filter_for_granularity_v2(granularity_v2)
draw_texture_rect(tile_texture, Rect2(local_origin, Vector2(extent, extent)), false)
## Live round 4 fix: rebuilds (and, critically, KEEPS — see
## `_tile_texture_cache`'s own doc for why an unstored local `ImageTexture`
## silently rendered blank/white) `_tile_texture_cache[tile_index]`'s texture
## ONLY when that tile's window object or the active toggle overlay has
## changed since the last build — the SAME reference-identity discipline
## `_rebuild_texture_if_needed()` uses for the single-window composite, one
## cache entry per tile index instead of one shared field.
func _rebuild_tile_texture_if_needed(
tile_index: int, w: Dictionary, grid_side: int, active_toggle: String
) -> ImageTexture:
var entry: Dictionary = _tile_texture_cache.get(tile_index, {})
if (
is_same(entry.get("window_ref"), w)
and entry.get("active_toggle") == active_toggle
and entry.get("texture") != null
):
return entry["texture"]
var texture: ImageTexture = _build_tile_texture(w, grid_side, active_toggle)
_tile_texture_cache[tile_index] = {
"window_ref": w, "active_toggle": active_toggle, "texture": texture
}
return texture
## Builds a tile's own Image/ImageTexture from its per-cell colors —
## identical pipeline to `_rebuild_texture_if_needed()`, just returning the
## texture directly instead of writing to the single-window cache fields
## (the CALLER, `_rebuild_tile_texture_if_needed()`, owns persisting it).
func _build_tile_texture(w: Dictionary, grid_side: int, active_toggle: String) -> ImageTexture:
var elev_q: Variant = w.get("elev_q")
var glaciation: Variant = w.get("glaciation")
var morphology: Variant = w.get("morphology")
var n_cells: int = morphology.size()
var img := Image.create(grid_side, grid_side, false, Image.FORMAT_RGBA8)
for row in range(grid_side):
for col in range(grid_side):
var i: int = row * grid_side + col
if i >= n_cells:
img.set_pixel(col, row, Color.TRANSPARENT)
continue
var cell_color: Color = _cell_color(w, i, int(morphology[i]), elev_q, active_toggle)
cell_color = _apply_glaciation(cell_color, glaciation, i)
img.set_pixel(col, row, cell_color)
return ImageTexture.create_from_image(img)
## The crisp (non-smoothed) per-tile path — mirrors `_draw_crisp_composite()`
## exactly, just positioned at `local_origin` instead of always at (0,0).
func _draw_crisp_tile(
w: Dictionary, grid_side: int, local_origin: Vector2, extent: float, active_toggle: String
) -> void:
var elev_q: Variant = w.get("elev_q")
var glaciation: Variant = w.get("glaciation")
var morphology: Variant = w.get("morphology")
var n_cells: int = morphology.size()
var screen_cell_px: float = extent / float(grid_side)
for row in range(grid_side):
for col in range(grid_side):
var i: int = row * grid_side + col
if i >= n_cells:
continue
var cell_color: Color = _cell_color(w, i, int(morphology[i]), elev_q, active_toggle)
if cell_color.a <= 0.0:
continue
cell_color = _apply_glaciation(cell_color, glaciation, i)
var cell_origin: Vector2 = local_origin + Vector2(col * screen_cell_px, row * screen_cell_px)
draw_rect(
Rect2(cell_origin, Vector2(screen_cell_px + 0.5, screen_cell_px + 0.5)), cell_color
)
## The derived cell-grid side length (in CELLS) for a window dict `w` —
## mirrors server/src/atlas/layer_proxy.rs's `WindowGranularity::cell_grid_side`
## exactly, reading `w`'s OWN echoed `n`/`granularity_v2` fields rather than
## trusting a caller's separately-tracked rung (the response is the source of
## truth for what it actually contains). Falls back to `n` unchanged
## (District's own identity mapping) for an old-shape response with no
## `granularity_v2` key — matches the server's own "unknown -> District"
## posture and AtlasWindowRequest.on_response()'s own default-to-District
## disposition for the same field.
static func cell_grid_side_for_window(w: Dictionary) -> int:
var n: int = int(w.get("n", 0))
var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT))
match granularity_v2:
AtlasWindowRequest.GRANULARITY_V2_QUARTER:
return n * 4 # WINDOW_GRANULARITY_QUARTER multiplier — D-243 QUARTER_M
AtlasWindowRequest.GRANULARITY_V2_REGION:
return maxi(roundi(float(n) / 100.0), 1) # D-243 DISTRICTS_PER_REGION
_:
return n # District — 1:1
## T-1161 (Araminta's per-rung filter ruling): the sampling filter to use for
## the smoothed composite's texture, keyed on RUNG IDENTITY alone via
## `granularity_v2` — no hysteresis, no px-per-cell/zoom threshold. Region
## (204.8 km/cell — the same rung the orbital tile mosaic draws at, since
## every mosaic tile is itself a Region-rung window per `_draw_tile_mosaic()`)
## reads NEAREST: at that density, GPU bilinear blending between real derived
## samples is technically honest LoD but visually indistinguishable from the
## coarse-composite-stretched smoothing-over-absence the mandate was written
## to kill — the spirit is violated even though the letter ("never magnified
## interpolation") is not. District and Quarter read LINEAR: cell density at
## those rungs is high enough that the blend reads as texture, not as papering
## over sparse data. Mirrors `cell_grid_side_for_window()`'s own posture on an
## unknown/missing tag — an unrecognized wire value must never be trusted into
## the crisp NEAREST treatment, so it falls back to District's LINEAR instead
## of Region's NEAREST (fail toward the safer/already-shipped look).
static func _filter_for_granularity_v2(granularity_v2: String) -> CanvasItem.TextureFilter:
match granularity_v2:
AtlasWindowRequest.GRANULARITY_V2_REGION:
return CanvasItem.TEXTURE_FILTER_NEAREST
_:
return CanvasItem.TEXTURE_FILTER_LINEAR # District, Quarter, and unknown/missing fallback
## T-1145 item 3: the smoothed path — build/reuse a `grid_side` x `grid_side`
## ImageTexture (one pixel per DERIVED CELL, T-1152 — not per district, see
## cell_grid_side_for_window()'s doc) and draw it scaled to (n*cell_px), n
## being the window's DISTRICT extent. texture_filter is set on `self` (a
## CanvasItem property) once per draw — cheap (a property write, not a
## texture rebuild). T-1161: the filter itself is now PER-RUNG, read from
## `w`'s own echoed `granularity_v2` (the same "response is the source of
## truth" posture cell_grid_side_for_window() already uses) via
## `_filter_for_granularity_v2()`, rather than an unconditional LINEAR.
func _draw_smoothed_composite(
w: Dictionary, n: int, grid_side: int, cell_px: float, active_toggle: String
) -> void:
var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT))
texture_filter = _filter_for_granularity_v2(granularity_v2)
_rebuild_texture_if_needed(w, grid_side, active_toggle)
if _cached_texture == null:
return
var extent: float = float(n) * cell_px
draw_texture_rect(_cached_texture, Rect2(0.0, 0.0, extent, extent), false)
## Rebuilds _cached_texture from `w`'s per-cell colors ONLY when the window
## object or the active toggle overlay has changed since the last build —
## see the class doc's rebuild-cost paragraph. `elev_q` and `glaciation` are
## read directly from `w` here (rather than threaded through as params, the
## way the crisp path's _cell_color()/_apply_glaciation() calls already
## receive them) since this function owns the whole per-cell loop, not just
## one cell. `grid_side` (T-1152) is the DERIVED cell-grid side (see
## cell_grid_side_for_window()), not the window's district extent `n`.
func _rebuild_texture_if_needed(w: Dictionary, grid_side: int, active_toggle: String) -> void:
if (
is_same(_cache_window_ref, w)
and _cache_active_toggle == active_toggle
and _cached_texture != null
):
return # inputs unchanged since the last build — reuse the existing texture
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)
_cached_texture = ImageTexture.create_from_image(img)
_cache_window_ref = w
_cache_active_toggle = active_toggle
## The ORIGINAL crisp per-cell path — kept byte-for-byte behind
## COMPOSITE_SMOOTH := false so T-1143's design pass can compare both
## renderings directly (see the class doc). `grid_side` (T-1152) is the
## DERIVED cell-grid side (see cell_grid_side_for_window()); `n` (the
## window's district extent) sizes the on-screen cell pitch so the total
## drawn footprint stays `n * cell_px` regardless of rung.
##
## Note (T-1161): this path never touches the node-level `texture_filter`
## property — draw_rect() has no texture/sampling-filter concept, so there is
## nothing to set. That is inert today only because nothing else reads
## `texture_filter` while this path is active; it is not a bug to fix here,
## just worth stating since the smoothed path now sets that property
## per-rung and a reader might otherwise wonder why this path doesn't.
func _draw_crisp_composite(
w: Dictionary, grid_side: int, n: int, cell_px: float, active_toggle: String
) -> void:
var elev_q: Variant = w.get("elev_q")
var glaciation: Variant = w.get("glaciation")
var morphology: Variant = w.get("morphology")
var n_cells: int = morphology.size()
var screen_cell_px: float = float(n) * cell_px / float(grid_side)
for row in range(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 # Marine-transparent or otherwise "don't draw" (cheaper than a 0-alpha rect)
cell_color = _apply_glaciation(cell_color, glaciation, i)
# +0.5 overdraw avoids hairline seams between adjacent cells —
# same idiom as _draw_gen_district/_draw_gen_region_grid.
draw_rect(
Rect2(col * screen_cell_px, row * screen_cell_px, screen_cell_px + 0.5, screen_cell_px + 0.5),
cell_color
)
## Which of the three mutually-exclusive toggle overlays (if any) is active.
## At most one draws — §5 does not describe blending two toggles together,
## and doing so would fight the "one colorizer, one read" legibility goal the
## whole layer design optimizes for. First-match-wins on ties (should never
## happen — the overlay bar toggles independently, but this keeps the draw
## deterministic instead of implicitly depending on dictionary iteration
## order if more than one somehow ends up true).
func _active_toggle_overlay() -> String:
if viewer.is_overlay_visible("gen_dw_temp"):
return "gen_dw_temp"
if viewer.is_overlay_visible("gen_dw_moisture"):
return "gen_dw_moisture"
if viewer.is_overlay_visible("gen_dw_veg"):
return "gen_dw_veg"
return ""
func _cell_color(
w: Dictionary, i: int, morphology_zone: int, elev_q: Variant, active_toggle: String
) -> Color:
match active_toggle:
"gen_dw_temp":
return _temp_cell_color(w.get("temp_dc"), i)
"gen_dw_moisture":
return _moisture_cell_color(w.get("moisture_q"), i)
"gen_dw_veg":
return _veg_cell_color(w.get("vegetation"), i)
_:
return _base_cell_color(morphology_zone, elev_q, i)
## Base layer: morphology hue, lightness-modulated by elev_q (§5's one
## `0.7 + 0.3*(elev_q/100)` multiply per cell).
func _base_cell_color(morphology_zone: int, elev_q: Variant, i: int) -> Color:
var base: Color = AtlasOverlayColors.district_window_morphology_color(morphology_zone)
var eq: int = _dense_int(elev_q, i, 50)
return AtlasOverlayColors.district_window_elevation_lightness(base, eq)
## gen_dw_temp: reuses T-1118's region_temp_color() EXACTLY — same i16
## deci-°C domain, same REGION_TEMP_NONE_DC sentinel disposition (skip the
## cell entirely, matching _draw_gen_region_grid's airless treatment) — one
## colorizer across both zoom levels, per the amendment's consistency ruling.
func _temp_cell_color(temp_dc: Variant, i: int) -> Color:
if temp_dc == null:
return Color.TRANSPARENT
var t: int = _dense_int(temp_dc, i, REGION_TEMP_NONE_DC)
if t == REGION_TEMP_NONE_DC:
return Color.TRANSPARENT # airless — no reading, skip the cell (matches region-grid precedent)
return AtlasOverlayColors.region_temp_color(t)
## gen_dw_moisture: dry-sand -> wet-teal ramp over the existing SUB_BIOME_COLORS
## endpoints (§5).
func _moisture_cell_color(moisture_q: Variant, i: int) -> Color:
if moisture_q == null:
return Color.TRANSPARENT
var m: int = clampi(_dense_int(moisture_q, i, 50), 0, 100)
return COLOR_MOISTURE_DRY.lerp(COLOR_MOISTURE_WET, float(m) / 100.0)
## gen_dw_veg: green-family ramp, Marine transparent (§3/§5 — non-negotiable
## per the amendment; see atlas_overlay_colors.gd's vegetation_color() doc).
func _veg_cell_color(vegetation: Variant, i: int) -> Color:
if vegetation == null:
return Color.TRANSPARENT
return AtlasOverlayColors.vegetation_color(_dense_int(vegetation, i, 0))
## Glaciation: an always-on MODIFIER (never a toggle id), composited over
## whichever layer is currently showing — the base or one of the three
## toggles (§5).
func _apply_glaciation(cell_color: Color, glaciation: Variant, i: int) -> Color:
if glaciation == null:
return cell_color
var grade: int = _dense_int(glaciation, i, 0)
return AtlasOverlayColors.glaciation_tint(cell_color, grade)
## Reads element `i` from a dense numeric array field regardless of whether
## the messagepack decode produced a PackedByteArray (u8 fields) or a plain
## Array (i16 temp_dc — rmp_serde without serde_bytes, matching the existing
## region_grid _dense_int precedent in test_atlas_overlays.gd, now needed at
## RUNTIME here too, not just in a test helper).
static func _dense_int(arr: Variant, i: int, fallback: int) -> int:
if (arr is Array or arr is PackedByteArray) and i < arr.size():
return int(arr[i])
return fallback
@@ -1,464 +0,0 @@
extends Node
## District-window request orchestration for AtlasWindowViewer (T-1138, D-226
## T-1124 amendment §1/§4). Owns the cache, the pan-triggered re-request
## policy, the post-drag-release debounce, and the retry loop for the
## queue-based background derive (PR #185 finding: a window response arrives
## on a LATER TICK, not synchronously — the exact same "None until derived,
## re-poll" contract atlas_generation_proxy.gd's Layer1 path already handles,
## reused here rather than re-invented).
##
## This script has no `class_name` on purpose, matching every other
## viewer-owned helper in this cluster (atlas_overlay_bar.gd/
## atlas_legend_panel.gd/atlas_generation_proxy.gd, review #8 precedent): the
## owner (AtlasWindowViewer) passes itself to _init(), and a `class_name` +
## required-arg _init() combo is a Godot editor footgun. `extends Node` (not
## RefCounted) because it needs get_tree() for the debounce/retry timers —
## added as a child via
## load("res://ui/implant/apps/atlas/atlas_window_request.gd").new(self).
##
## §4 policy fixed here (the constants + the debounce, NOT the pan-edge
## detection — that's the viewer's job, since it owns the screen-to-district
## geometry):
## - DISTRICT_WINDOW_DEFAULT_N = 32 (client's interactive default, half the
## server's DISTRICT_WINDOW_MAX_N = 64 hard cap — §4 pins both numbers;
## the cap itself is a server-side clamp this client never needs to
## duplicate, only stay under so a request is never silently clamped in
## a way the client didn't expect).
## - 150ms post-drag-release debounce — long enough to collapse a
## flick-and-resettle into one request, short enough that a deliberate
## single pan-and-stop never feels delayed (§4/§5 wording, identical).
## - Cache-hit is instant (no request at all) — §4's "D-227 makes exact-
## repeat the common case for Esc-then-re-enter and pan-back" is what
## makes this the common path, not the minority one.
signal window_ready(window: Dictionary) # emitted on a cache hit OR a fresh Ready response
const AtlasWindowCache := preload("res://ui/implant/apps/atlas/atlas_window_cache.gd")
const DISTRICT_WINDOW_DEFAULT_N: int = 32
const DEBOUNCE_DELAY: float = 0.15 # 150ms, §4/§5
## Cold-start dossier (PR #192 round 3): the retry-on-PENDING loop used to be
## a flat RETRY_DELAY=0.5s / MAX_RETRIES=20 (~10s ceiling), copied verbatim
## from atlas_generation_proxy.gd's Layer1 poll — a DIFFERENT, typically
## faster derive. The real starvation bug turned out to be the status-gate
## fix in on_response() (see that function's own doc) — this backoff/stagger
## work is HARDENING landed alongside it, not the fix itself: once the
## status-gate fix makes 6 independent tiles all correctly retry on a
## whole-response Pending, they do so in perfect lockstep (all six went
## pending at entry within the same frame, so all six retry timers fire
## within the same frame too) — six re-requests every RETRY_DELAY, in sync,
## is exactly the "storm" shape worth damping even though it isn't what
## caused the starvation. Exponential backoff (INITIAL_RETRY_DELAY doubling
## to MAX_RETRY_DELAY) plus a DETERMINISTIC per-tile stagger
## (STAGGER_STEP * stagger_index, set once by the owning AtlasWindowTileSet
## at construction — see `_stagger_index`) spread that pulse into a trickle:
## tile 0 retries at 0.5s, tile 1 at 0.6s, tile 2 at 0.7s, etc. — deterministic
## and directly assertable in a test, not a randomized jitter a test would
## have to tolerance-check. Backoff ALSO buys a much longer wall-clock window
## from a modest MAX_RETRIES increase (~110s at 30 retries, see
## _retry_delay_for()'s own doc) without ever polling aggressively for that
## whole span. A fast (already-warm) response still resolves on retry #1,
## unaffected — backoff/stagger only matter once a request is genuinely
## still pending past the first cycle.
const INITIAL_RETRY_DELAY: float = 0.5 # first retry, matches the old flat RETRY_DELAY
const MAX_RETRY_DELAY: float = 4.0 # backoff ceiling — never polls slower than this
const STAGGER_STEP: float = 0.1 # per-tile-index offset — tile i retries STAGGER_STEP*i later
const MAX_RETRIES: int = 30 # ~110s wall-clock at the backoff schedule above
## T-1150 struct/key plumbing: legacy int granularity — district is the
## default for every caller that doesn't request quarter/Region explicitly.
const DEFAULT_GRANULARITY: int = AtlasWindowCache.DISTRICT_GRANULARITY
const DEFAULT_MIN_WL_M: int = 0
## T-1152/T-1153: the R5-redesigned string-tag granularity — "Quarter" |
## "District" | "Region". This is the axis request_now()/request_debounced()'s
## `granularity_v2` parameter actually varies; the legacy int
## (DEFAULT_GRANULARITY) stays pinned at district for every call this object
## makes, since v2 always wins server-side once present
## (resolve_window_granularity_v2()'s documented precedence) and the legacy
## int cannot express Region at all.
const GRANULARITY_V2_QUARTER: String = AtlasWindowCache.GRANULARITY_V2_QUARTER
const GRANULARITY_V2_DISTRICT: String = AtlasWindowCache.GRANULARITY_V2_DISTRICT
const GRANULARITY_V2_REGION: String = AtlasWindowCache.GRANULARITY_V2_REGION
const DEFAULT_GRANULARITY_V2: String = AtlasWindowCache.DEFAULT_GRANULARITY_V2
## Mirrors server/src/atlas/layer_proxy.rs's DISTRICT_WINDOW_MAX_N /
## WIRE_CAP_CELLS exactly (PR #191 review, Tyre C1). `_clamp_window_n_mirror()`
## below reproduces `clamp_window_n()` bit-for-bit — the load-bearing-mirror
## pattern `AtlasDescendGeometry.canonicalize_district_center()` already uses
## for the server's `normalize_window_center()`. Keep both numbers in sync
## with the server constants of the same name if either ever changes.
const SERVER_DISTRICT_WINDOW_MAX_N: int = 64
const SERVER_WIRE_CAP_CELLS: int = 4_096
## T-1152/T-1153: mirrors server/src/atlas/layer_proxy.rs's
## `DISTRICT_WINDOW_MAX_N_REGION` — the Region-only per-axis ceiling on `n`
## (still window extent in DISTRICTS, per WindowGranularity::cell_grid_side's
## doc: `sqrt(WIRE_CAP_CELLS) * DISTRICTS_PER_REGION = 64 * 100`). Keep in
## sync with the server constant of the same name.
const SERVER_DISTRICT_WINDOW_MAX_N_REGION: int = 6_400
## Mirrors server/src/atlas/scale.rs's DISTRICTS_PER_REGION (D-243: one region
## = 100 districts/side) — the divisor `_cell_grid_side_region_mirror()` needs
## to reproduce `WindowGranularity::cell_grid_side`'s Region branch.
const SERVER_DISTRICTS_PER_REGION: int = 100
var _owner = null # AtlasWindowViewer (untyped to avoid cyclic ref)
var _cache = null # AtlasWindowCache
var _body_id: String = ""
var _center: Vector2i = Vector2i.ZERO
var _n: int = DISTRICT_WINDOW_DEFAULT_N
var _granularity: int = DEFAULT_GRANULARITY
var _granularity_v2: String = DEFAULT_GRANULARITY_V2
var _min_wl_m: int = DEFAULT_MIN_WL_M
var _pending: bool = false
var _retries: int = 0
var _debounce_timer: Timer = null
## Cold-start dossier round 3: deterministic per-request stagger index for
## the retry backoff (see STAGGER_STEP's own doc) — 0 for the single-window
## viewer's own request (no fan-out, nothing to desync from), the tile's own
## index (0..5) for a tile-set-owned request (AtlasWindowTileSet.enter()
## sets this once at construction, right after AtlasWindowRequest.new()).
var _stagger_index: int = 0
func _init(owner_ref = null) -> void:
_owner = owner_ref
_cache = AtlasWindowCache.new()
func _ready() -> void:
_debounce_timer = Timer.new()
_debounce_timer.name = "DebounceTimer"
_debounce_timer.one_shot = true
_debounce_timer.wait_time = DEBOUNCE_DELAY
_debounce_timer.timeout.connect(_on_debounce_timeout)
add_child(_debounce_timer)
## Reset for a fresh entry into the regional window mode (new body/center) —
## clears in-flight retry bookkeeping but NOT the cache (D-227: a cached
## window is valid forever regardless of which body/center the viewer is
## currently showing; clearing on every entry would throw away exactly the
## Esc-then-re-enter hit §4 promises).
func reset() -> void:
_pending = false
_retries = 0
if _debounce_timer:
_debounce_timer.stop()
## Mirrors server/src/atlas/layer_proxy.rs's `clamp_window_n(raw_n,
## granularity)` EXACTLY (PR #191 review, Tyre C1 — "the sharpest" finding):
## `serve_district_window` echoes the CLAMPED `n` back in
## `DistrictWindowLayer.n`, but `on_response()`'s staleness guard compares the
## echo against `_n`. Without this mirror, `_n` would hold the RAW requested
## value while the server echoes the CLAMPED one — the moment a caller
## requests quarter (granularity=4) at n=32, the server clamps to n=16 and
## echoes THAT, `on_response()` sees `echoed_n=16 != _n=32`, decides the
## response is stale, and the window silently never loads (no error, no log
## on this side — just an eternally-pending request).
##
## Clamping HERE, before `_n` is ever stored or sent, means `_n` already
## equals what the server will echo — no drift between the two sides, the
## SAME load-bearing-mirror pattern `AtlasDescendGeometry.
## canonicalize_district_center()` uses for the server's
## `normalize_window_center()` (see that function's docstring for the general
## rationale: canonicalizing before the request is sent means the client's
## held state already equals what the server will echo back).
##
## Formula, bit-for-bit: `n = raw_n.clamp(1, SERVER_DISTRICT_WINDOW_MAX_N)`,
## then `n = min(n, floor(sqrt(SERVER_WIRE_CAP_CELLS) / max(granularity, 1)))`
## — applied in that order (per-axis cap first, then the granularity-aware
## wire-size ceiling), matching `clamp_window_n`'s own comment ("Applied AFTER
## the per-axis clamp so a request that already satisfies
## DISTRICT_WINDOW_MAX_N still shrinks further at granularity 4").
static func _clamp_window_n_mirror(raw_n: int, granularity: int) -> int:
var n: int = clampi(raw_n, 1, SERVER_DISTRICT_WINDOW_MAX_N)
var g: int = maxi(granularity, 1)
var cap_n: int = int(floor(sqrt(float(SERVER_WIRE_CAP_CELLS)) / float(g)))
return mini(n, maxi(cap_n, 1))
## [`WindowGranularity`]-aware twin of `_clamp_window_n_mirror()` (T-1152/
## T-1153) — mirrors server/src/atlas/layer_proxy.rs's `clamp_window_n_v2`
## EXACTLY, including its Region branch, per the ticket's explicit
## instruction ("replicate the loop exactly, there is NO closed form"). For
## District/Quarter this delegates straight to `_clamp_window_n_mirror()`
## (byte-identical clamped `n`, matching the server's own
## `clamp_window_n_v2_matches_legacy_for_finer_than_district_rungs`
## guarantee). For Region: per-axis clamp to
## `SERVER_DISTRICT_WINDOW_MAX_N_REGION` (6,400), then halve `n` in a bounded
## loop while `_cell_grid_side_region_mirror(n)^2 > SERVER_WIRE_CAP_CELLS` and
## `n > 1` — there is no closed-form inverse of the rounding division
## `cell_grid_side` uses at Region granularity, so this loop is the correct
## (and only) mirror, not an approximation of one.
static func _clamp_window_n_mirror_v2(raw_n: int, granularity_v2: String) -> int:
if granularity_v2 != AtlasWindowCache.GRANULARITY_V2_REGION:
var legacy_granularity: int = (
DEFAULT_GRANULARITY
if granularity_v2 == AtlasWindowCache.GRANULARITY_V2_DISTRICT
else AtlasWindowCache.DISTRICT_GRANULARITY * 4 # "Quarter" — WINDOW_GRANULARITY_QUARTER
)
return _clamp_window_n_mirror(raw_n, legacy_granularity)
var n: int = clampi(raw_n, 1, SERVER_DISTRICT_WINDOW_MAX_N_REGION)
while (
_cell_grid_side_region_mirror(n) * _cell_grid_side_region_mirror(n) > SERVER_WIRE_CAP_CELLS
and n > 1
):
n = int(n / 2.0)
return maxi(n, 1)
## Mirrors `WindowGranularity::cell_grid_side`'s Region branch EXACTLY:
## `round(n / DISTRICTS_PER_REGION).max(1)` — the derived region-cell-grid
## side length (in CELLS) for a window whose extent is `n` DISTRICTS. Rust's
## `f64::round()` is round-half-away-from-zero; GDScript's `roundi()` matches
## that for non-negative inputs (the only domain `n` — always >= 1 here —
## can produce), so this is a faithful mirror, not an approximation.
static func _cell_grid_side_region_mirror(n: int) -> int:
var side: int = roundi(float(n) / float(SERVER_DISTRICTS_PER_REGION))
return maxi(side, 1)
## Entry point + pan re-request: request the window centered on `center`
## (a DistrictPos-equivalent Vector2i) for `body_id`, at `granularity_v2`
## ("Quarter" | "District" | "Region", T-1152/T-1153 — District is the
## default for every caller that doesn't ask for a different rung explicitly,
## matching the legacy behavior byte-for-byte when omitted). Cache hit ->
## immediate synchronous window_ready emit, no network traffic at all. Cache
## miss -> fire the request now (the caller — either the initial entry or a
## debounce-fired pan/zoom — has already decided this call SHOULD fire; the
## 150ms debounce itself lives in request_debounced() below, not here, so
## this function is also the one entry-mechanic click-through uses directly
## with no debounce at all, matching §5's "first window" contract).
func request_now(
body_id: String,
center: Vector2i,
n: int = DISTRICT_WINDOW_DEFAULT_N,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> void:
_body_id = body_id
_center = center
_granularity = DEFAULT_GRANULARITY # legacy int stays pinned at district — v2 always wins server-side
_granularity_v2 = granularity_v2
_min_wl_m = DEFAULT_MIN_WL_M
_n = _clamp_window_n_mirror_v2(n, _granularity_v2) # Tyre C1, extended T-1152 — mirror BEFORE storing
_debounce_timer.stop() # a direct request supersedes any pending debounced one
var cached: Variant = _cache.get_window(
body_id, center, _n, _granularity, _min_wl_m, _granularity_v2
)
if cached != null:
_pending = false
_retries = 0
window_ready.emit(cached)
return
_pending = true
_retries = 0
SimBridge.request_atlas_layers(
body_id, "Topography", center, _n, _granularity, _min_wl_m, _granularity_v2
)
## Pan-triggered re-request (§4/§5: "150ms after the last drag-release, not
## per-drag-frame"). The viewer calls this on every pan-edge-crossing
## candidate; only the LAST call within the debounce window actually fires
## (Timer.start() on an already-running one-shot timer restarts it — Godot's
## documented behavior — so a flick-and-resettle collapses to one request).
func request_debounced(
body_id: String,
center: Vector2i,
n: int = DISTRICT_WINDOW_DEFAULT_N,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> void:
_body_id = body_id
_center = center
_granularity = DEFAULT_GRANULARITY
_granularity_v2 = granularity_v2
_min_wl_m = DEFAULT_MIN_WL_M
_n = _clamp_window_n_mirror_v2(n, _granularity_v2) # Tyre C1, extended T-1152 — mirror BEFORE storing
_debounce_timer.start()
func _on_debounce_timeout() -> void:
request_now(_body_id, _center, _n, _granularity_v2)
## Handle an AtlasLayerResponse (routed by the owning viewer from its own
## SimBridge.atlas_layers_received subscription — this object has no signal
## connection of its own, matching atlas_generation_proxy.gd's on_response()
## shape). Ignores responses for a stale body/center/n/min_wl_m/granularity
## (legacy OR v2, see below) — the player panned, zoomed across a rung
## boundary, or navigated away while a request was in flight, or a different
## rung's derive answers a request for a different rung, T-1150/T-1152 — the
## echoed fields ARE the staleness guard (§2, extended T-1150/T-1152),
## compared here against what THIS object most recently asked for.
##
## **Live-round finding (the second C1-shaped bug): v2 is AUTHORITATIVE over
## the legacy field whenever v2 is present — the legacy comparison is
## SKIPPED entirely, not run alongside it.** A T-1152-aware server (this
## codebase's) ALWAYS populates `granularity_v2` on the wire (Dudley's
## contract, `DistrictWindowLayer.granularity_v2`'s own doc: "Always
## populated (never `None`)"), and for `Region` responses specifically the
## LEGACY `granularity` slot carries `WINDOW_GRANULARITY_REGION_KEY`
## (`u32::MAX` = 4294967295) — a reserved KEY-SPACE TAG, not a real
## multiplier, that can never equal this object's own stored `_granularity`
## (which stays pinned at `DEFAULT_GRANULARITY`=1 for every rung this object
## requests, per that field's own doc — the legacy slot has no concept of
## Region at all). Comparing the legacy field UNCONDITIONALLY alongside v2
## therefore drops EVERY Region response as stale forever, even though the
## v2 comparison alone would have correctly accepted it — exactly the live
## bug (`_held_n` fixed; this is the same "old comparison still active
## alongside the new one" class of bug, one layer up in the staleness
## checks). Fix: branch on whether `granularity_v2` is actually PRESENT in
## the response dict (`w.has(...)`, not `w.get(..., default)` — the
## presence/absence distinction is the whole point here) — present (every
## real server, always) -> v2 is the ONLY granularity comparison; absent (a
## hypothetically old, pre-T-1152 server) -> fall back to the legacy
## comparison alone, matching this object's own pre-T-1152 behavior exactly.
## PR #192 cold-start round 3: the coordinator's live cold-server capture
## (retries=0, pending=true, forever) exposed that the OLD version of this
## function returned unconditionally whenever the WHOLE response's status
## wasn't "Ready" — treating a cold body's `status: "Pending"` (the FIRST
## request against a whole-body cache miss, before ANY layer including the
## window has even been queued — `serve_district_window`/`get_or_generate()`
## in server/src/atlas/layer_proxy.rs) identically to `NotFound`/`Error`: a
## silent no-op, never reaching the retry-scheduling code at all. Confirmed
## server-side: `status: Ready` is set ONLY on the whole-body cache-HIT
## branch, entirely independent of whether the WINDOW itself has resolved —
## so a cold body's first-ever window request gets `Pending` at the OUTER
## layer, while a body someone has already warmed (a later connection, or
## this SAME connection's own re-request once its own AnalyzeBody has
## landed) gets `Ready` with `district_window: null` inside it, correctly
## reaching the retry branch below. Same "still generating" signal, two
## different wire shapes depending on which cache warmed first — the fix is
## to treat BOTH as the identical retry-worthy state, matching
## atlas_generation_proxy.gd's own on_response() `match` shape exactly
## (Ready -> handle, Pending -> retry, NotFound/Error -> give up now, not
## after MAX_RETRIES: a real error is never going to resolve by waiting).
func on_response(response: Dictionary) -> void:
if str(response.get("body_id", "")) != _body_id:
return
var status := str(response.get("status", ""))
if status == "Pending":
_retry_if_pending()
return
if status != "Ready":
_pending = false # NotFound / Error — a real failure, not a queue wait; give up now
return
var window: Variant = response.get("district_window")
if window == null:
# §1: an as-yet-underived window rides as `district_window: None`
# inside an OUTER-Ready response — the whole-body cache already
# warmed, but this specific window hasn't derived yet. Same
# "still generating" signal the outer-Pending branch above handles,
# just the OTHER wire shape it can arrive in.
_retry_if_pending()
return
var w: Dictionary = window
var echoed_center := _vec_from_center(w.get("center", [0, 0]))
var echoed_n := int(w.get("n", 0))
var echoed_min_wl_m := int(w.get("min_wl_m", 0))
var granularity_matches: bool = _echoed_granularity_matches(w)
if (
echoed_center != _center
or echoed_n != _n
or echoed_min_wl_m != _min_wl_m
or not granularity_matches
):
return # stale — answers a window we've since panned/zoomed away from, or a different rung
_pending = false
_retries = 0
_cache.put(_body_id, _center, _n, w, _granularity, _min_wl_m, _granularity_v2)
window_ready.emit(w)
## Shared "still generating, re-poll" logic for BOTH wire shapes on_response()
## can see it in (outer status=="Pending", or inner district_window==null
## inside an outer Ready) — re-request until the derive lands or the retry
## ceiling is hit (queue-based serving, PR #185 — the response lands on a
## LATER tick, never this same round-trip).
func _retry_if_pending() -> void:
if not _pending:
return
if _retries < MAX_RETRIES:
_retries += 1
_schedule_retry()
else:
_pending = false # gave up — caller's border-fade / empty state persists
## The granularity half of on_response()'s staleness check, split out for the
## v2-authoritative-when-present precedence rule (see on_response()'s own
## doc for the full live-round rationale). Presence, not value, is the
## branch: `w.has("granularity_v2")` — a real server ALWAYS sets this key
## (even if its value happened to coincidentally equal a default), so
## checking presence rather than "is it the default value" is the only
## correct way to distinguish "an old server that never heard of this field"
## from "a new server whose value happens to match."
func _echoed_granularity_matches(w: Dictionary) -> bool:
if w.has("granularity_v2"):
return str(w.get("granularity_v2")) == _granularity_v2
var echoed_granularity := int(w.get("granularity", AtlasWindowCache.DISTRICT_GRANULARITY))
return echoed_granularity == _granularity
## Pure: the exponential-backoff delay for retry attempt number `retry_count`
## (1-indexed — the FIRST retry, right after the initial request's own
## PENDING answer, uses `retry_count=1`), staggered by `stagger_index`
## (STAGGER_STEP*stagger_index added on top — deterministic, not randomized,
## so a test can assert the exact delay sequence for tile N directly). Split
## out from _schedule_retry() as a pure function for the same reason every
## other formula in this file is: directly unit-testable without a live
## Timer/SceneTree.
static func _retry_delay_for(retry_count: int, stagger_index: int) -> float:
var base: float = INITIAL_RETRY_DELAY * pow(2.0, float(maxi(retry_count - 1, 0)))
var capped: float = minf(base, MAX_RETRY_DELAY)
return capped + STAGGER_STEP * float(stagger_index)
func _schedule_retry() -> void:
var delay: float = _retry_delay_for(_retries, _stagger_index)
var timer := get_tree().create_timer(delay)
timer.timeout.connect(
func() -> void:
if _pending:
SimBridge.request_atlas_layers(
_body_id, "Topography", _center, _n, _granularity, _min_wl_m, _granularity_v2
)
)
func is_pending() -> bool:
return _pending
## The v2 granularity ("Quarter" | "District" | "Region") this object most
## recently asked for — the viewer reads this to know which rung the HELD
## window (once it arrives) actually is, without threading a second copy of
## the state through window_ready's payload.
func get_granularity_v2() -> String:
return _granularity_v2
## Current window extent in districts, as CLAMPED — the viewer's rung-
## selection math needs this to compute the held composite's real-world
## extent regardless of which rung last resolved it.
func get_n() -> int:
return _n
func get_cache() -> Variant:
return _cache
static func _vec_from_center(center: Variant) -> Vector2i:
if center is Array and center.size() >= 2:
return Vector2i(int(center[0]), int(center[1]))
return Vector2i.ZERO
@@ -1,203 +0,0 @@
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
# Cold-start dossier round 3 hardening: deterministic per-tile retry
# stagger (STAGGER_STEP*i) — without it, all 6 tiles go pending in the
# same frame and retry in perfect lockstep, a request pulse every
# RETRY_DELAY instead of a spread trickle. Set BEFORE request_now()
# so it's already in place for the very first retry, if one fires.
request._stagger_index = 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 while at least one tile's `window` hasn't arrived yet — the viewer's
## cold-start self-healing redraw (see AtlasWindowViewer._process()'s own
## doc) polls this every frame so a mosaic's paint can never silently wedge
## behind a lost/late queue_redraw() no matter which signal edge it was
## supposed to ride in on. Also the source of truth for whether the §4
## pending treatment should show.
func has_pending_tiles() -> bool:
for tile: Dictionary in _tiles:
if tile["window"] == null:
return true
return false
## True once at least ONE tile has a real window — distinct from
## has_pending_tiles()'s "at least one MISSING" (both can be true at once,
## mid-arrival). PR #192 cold-start round 2: a cold server's first
## AnalyzeBody can take >10s with ZERO tiles landed the whole time — the
## per-tile border-fade wash alone (subtle, same color as every OTHER
## no-data-yet state) read as broken darkness in a live cold capture, not
## loading. The viewer uses this to gate an unmistakable "DERIVING
## TERRAIN…" label: shown while this is false (nothing has arrived at all —
## the reassurance is needed most), dropped the moment even one tile lands
## (per-tile washes alone read fine once real content is visibly filling in
## around the gaps).
func has_any_tile_arrived() -> bool:
for tile: Dictionary in _tiles:
if tile["window"] != null:
return true
return false
## 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
File diff suppressed because it is too large Load Diff
@@ -1,171 +0,0 @@
extends RefCounted
## T-1172 — the river-skeleton waterline-clip fix. Pure geometry, split into
## its own file (not folded into atlas_window_geometry.gd, which is already
## close to the gdlint max-file-lines cap): the river skeleton's own SOURCE
## (server/src/atlas/drainage.rs) is filtered against the RAW heightmap sea
## level, but the DRAWN ocean this client actually paints is the derived
## MorphologyZone verdict (server/src/atlas/district_profile.rs) — which
## post-T-1162 includes the coast-warp invention (the drawn coastline is
## deterministically displaced from the heightmap coast) and, at Region
## rung, aggregates to 204.8 km cells. These are two independently-computed
## waterlines that can legitimately disagree; Tyre's ruling (T-1172): no
## single server waterline is well-defined, so the fix is a CLIENT draw-time
## clip against whichever composite cell is currently ON SCREEN at a given
## river dot's position — strict drop, no snap (a dot that lands on drawn
## water is simply not drawn; Region's 205 km cells may amputate a river's
## final coastal dots, an accepted cost per the ruling).
##
## T-1170 Ruling 3g update (RESTRUCTURED, not blanket-retired): the clip is
## RETIRED for the District/Quarter COURSE-drawing rungs
## (AtlasWindowNatureOverlay._draw_course_path()/_draw_one_course()) —
## courses carry real rung-consistent termini invented server-side against
## the SAME rung's drawn coast, so the clip's job is already done there. This
## file's clip machinery is STILL LIVE and used at the Region SKELETON path
## (_draw_skeleton_chords()/_segment_touches_drawn_water()) — Region still
## draws the whole-body skeleton against a rung-dependent drawn coast, which
## is precisely the presentation-frame reconciliation this file exists for.
## The Region clip is PERMANENT-UNTIL-REGION-GOES-WINDOWED (T-1143 ruling 2's
## progressive tiling) — when Region itself becomes a windowed rung, it
## inherits windowed courses too, and this file retires entirely at that
## point, not before.
##
## const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd")
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
## Sentinel returned by the lookups below when no arrived composite data
## covers the queried position — either the position is outside every
## held/tiled window's own extent, or the window/tile at that position
## hasn't arrived yet. The caller (AtlasWindowNatureOverlay) must FAIL OPEN
## on this sentinel (draw the dot) — Tyre's rule 5: the clip is a
## presentation refinement, never a data gate. Chosen as -1 (not a legal
## MorphologyZone discriminant, which is always >= 0) so it can never be
## mistaken for a real "not water" zone.
const MORPHOLOGY_ZONE_NO_DATA: int = -1
## The derived per-cell grid side length (CELLS) for a window dict `w` — a
## DELIBERATE duplicate of AtlasWindowOverlay.cell_grid_side_for_window(),
## not a shared call, matching this codebase's own "each file owns its own
## reading of a small pure lookup rather than force a dependency" precedent
## (atlas_overlay_colors.gd's header doc states this explicitly for the
## color-palette case; the SAME rationale applies here: atlas_window_overlay.gd
## already depends on atlas_window_geometry.gd, so a dependency back from
## there — or from this file, if it lived there — would risk a circular or
## at least confusing import graph). Mirrors
## server/src/atlas/layer_proxy.rs's `WindowGranularity::cell_grid_side`
## exactly, matching the canonical function's own doc byte-for-byte in intent.
static func cell_grid_side_for_window(w: Dictionary) -> int:
var n: int = int(w.get("n", 0))
var granularity_v2 := str(w.get("granularity_v2", "District"))
match granularity_v2:
"Quarter":
return n * 4
"Region":
return maxi(roundi(float(n) / 100.0), 1)
_:
return n
## Resolve a FRACTIONAL district position to the MorphologyZone discriminant
## of the composite cell covering it, for a SINGLE window dict `w` (the
## single-window rung path: District/Quarter, and each individual Region
## tile in tile mode share this same per-window shape). Returns
## MORPHOLOGY_ZONE_NO_DATA if `w` is null/malformed, has no morphology array,
## or `district` falls outside `w`'s own `[center - n/2, center + n/2)`
## extent (the SAME containment convention
## AtlasWindowGeometry.district_to_canvas_local() uses, so a position judged
## "inside" here is exactly the position that would draw as part of THIS
## window's composite on screen — no separate containment rule to drift out
## of sync with the actual paint).
static func morphology_zone_in_window(district: Vector2, w: Variant) -> int:
if not w is Dictionary:
return MORPHOLOGY_ZONE_NO_DATA
var window: Dictionary = w
var center_raw: Variant = window.get("center", [0, 0])
var center: Vector2i = (
Vector2i(int(center_raw[0]), int(center_raw[1])) if center_raw is Array else Vector2i.ZERO
)
var n: int = int(window.get("n", 0))
if n <= 0:
return MORPHOLOGY_ZONE_NO_DATA
var half: float = float(n) * 0.5
var local_x: float = district.x - (float(center.x) - half)
var local_y: float = district.y - (float(center.y) - half)
if local_x < 0.0 or local_x >= float(n) or local_y < 0.0 or local_y >= float(n):
return MORPHOLOGY_ZONE_NO_DATA
var morphology: Variant = window.get("morphology")
if not (morphology is PackedByteArray or morphology is Array):
return MORPHOLOGY_ZONE_NO_DATA
var grid_side: int = cell_grid_side_for_window(window)
if grid_side <= 0:
return MORPHOLOGY_ZONE_NO_DATA
# T-1172 round 2: SHARED index formula with the terrain painter
# (AtlasWindowGeometry.cell_index_for_local_offset() — see its own doc
# for why this is now factored out instead of duplicated).
var cell: Vector2i = AtlasWindowGeometryRef.cell_index_for_local_offset(
local_x, local_y, n, grid_side
)
var idx: int = cell.y * grid_side + cell.x
if idx < 0 or idx >= morphology.size():
return MORPHOLOGY_ZONE_NO_DATA
return int(morphology[idx])
## Resolve a fractional district position to a MorphologyZone discriminant
## across BOTH viewer modes — the single dispatch point
## AtlasWindowNatureOverlay's clip predicate calls, so it never needs its own
## is_tile_mode() branch. Single-window mode: one direct
## morphology_zone_in_window() call against `single_window`. Tile mode:
## linear scan of `tiles` (Array of {"center": Vector2i, "window": Variant},
## AtlasWindowTileSet.get_tiles()'s own shape) for whichever tile's ON-SCREEN
## extent contains the position — each tile's OWN echoed `window["n"]` is
## used for the actual containment test (not TILE_N assumed), matching this
## cluster's "the response is the source of truth for what it actually
## contains" precedent, since a clamped/still-arriving tile's real extent
## can differ from the nominal per-tile request size.
##
## **Live round 2 fix (coordinator's trace, T-1172):** the wrap resolution
## MUST mirror AtlasWindowOverlay._draw_tile_mosaic()'s own
## `draw_col = nearest_wrap_image(center.x, held_center.x, cols)` EXACTLY —
## wrap the TILE'S OWN CENTER toward `held_center` (the viewer's currently-
## displayed reference frame), then test the (already held-center-wrapped)
## query `district` against that RESOLVED center. The original version did
## the inverse — wrapped the QUERY toward the tile's raw CANONICAL center —
## which is not the same operation and silently tested containment against
## the WRONG wrap-image of the tile for any tile whose canonical center is
## far from `held_center` (i.e. any tile that needs wrapping to appear
## on-screen at all — confirmed live: a dot at district.x=-9569 visibly
## sitting on the painter's WEST wrap-image of the seam tile
## (canonical center 12739, drawn at draw_col=-6400) was tested by the old
## code against that tile's EAST/canonical span `[9539, 15939)` instead —
## landed inside it by coincidence (mod arithmetic), read a real but
## WRONG-LOCATION land cell, and never clipped). `district.x` is assumed
## ALREADY wrap-resolved near `held_center` by the caller (AtlasWindowNatureOverlay.
## _district()'s own contract) — this function does not re-wrap it, only the
## tile centers, exactly mirroring the painter's own asymmetry (the painter
## never wrap-resolves the query either — canvas-local coordinates are
## already in the held-center frame by construction).
static func resolve_morphology_zone(
district: Vector2, is_tile_mode: bool, single_window: Variant, tiles: Array, cols: int,
held_center_x: int = 0
) -> int:
if not is_tile_mode:
return morphology_zone_in_window(district, single_window)
for tile: Dictionary in tiles:
var window: Variant = tile.get("window")
if not window is Dictionary:
continue
var tile_center: Vector2i = tile.get("center", Vector2i.ZERO)
var draw_col: int = tile_center.x
if cols > 0:
draw_col = AtlasWindowGeometryRef.nearest_wrap_image(tile_center.x, held_center_x, cols)
var effective_window: Dictionary = window
if draw_col != tile_center.x:
effective_window = (window as Dictionary).duplicate()
effective_window["center"] = [draw_col, tile_center.y]
var zone: int = morphology_zone_in_window(district, effective_window)
if zone != MORPHOLOGY_ZONE_NO_DATA:
return zone
return MORPHOLOGY_ZONE_NO_DATA
@@ -1,62 +1,46 @@
class_name RegionalScreen
extends Control
## Regional zoom-ladder screen for AtlasApp (#844, D-191; superseded T-1153
## D-226 T-1143-rulings amendment). Thin wrapper around AtlasWindowViewer,
## entering at the CANONICAL ORBITAL FRAME (Region rung) via enter_orbital()
## instead of AtlasViewer's retired heightmap-texture show_body() path —
## enter/leave are still the nav interface, unchanged shape.
## Regional zoom-ladder screen for AtlasApp (#844, D-191; rebuilt T-1182
## D-255 stepped Atlas ladder). Thin wrapper around StepCanvasViewer,
## entering at the Global opener (rung 0, D-255(a)) via enter() — enter/leave
## are still the nav interface, unchanged shape from the retired
## AtlasWindowViewer/enter_orbital() this replaces.
##
## T-1152 client half: this is the ONE screen for the whole ladder now —
## there is no separate "district" nav hop for the windowed drill-down
## (D-013 "the zoom gesture owns spatial descent" restored for this seam
## means descent is a CONTINUOUS in-screen zoom, not a nav-stack push). Esc
## T-1182: this is the ONE screen for the whole six-rung ladder — Global
## through Chunk are all served by the SAME StepCanvasViewer, no separate
## orbital-mosaic-vs-window split (that split retired with AtlasWindowViewer
## itself, D-255(a): "Global opener is rung 0, one viewer one path"). Esc
## from anywhere in the ladder is a single nav.pop() back to whatever pushed
## "regional" (system screen) — see atlas_app.gd's _handle_key(), unchanged
## from before this ticket (it already routed Esc through nav.pop() for any
## screen that isn't "reach"/"system"-with-a-panel-open).
##
## `district_descend_requested`/`economics_link_requested` signals retire
## with AtlasViewer's click-through (the reticle/hover-to-descend affordance
## — Jeroen's ruling: retired as the SOLE entry, and no cheap click-target
## exists on the orbital Region-rung view to wire a shortcut onto yet, unlike
## a future settlement-marker click which WOULD have a natural landing
## point — see AtlasWindowViewer's own doc on why enter() still exists as a
## District-rung entry point for exactly that future wiring).
## economics_link_requested is deferred with AtlasViewer's city-click sidebar
## (see the batch report for the full list of what's deferred vs. carried).
## "regional" (system screen) — see atlas_app.gd's _handle_key(), unchanged.
signal back_requested
var _viewer: AtlasWindowViewer = null
var _viewer: StepCanvasViewer = null
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_STOP
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_viewer = AtlasWindowViewer.new()
_viewer.name = "AtlasWindowViewer"
_viewer = StepCanvasViewer.new()
_viewer.name = "StepCanvasViewer"
add_child(_viewer)
_viewer.back_pressed.connect(_on_viewer_back)
## Cold-start dossier (BUG 2, PR #192 review): ImplantApp._on_screen_changed()
## calls enter() unconditionally on EVERY screen_changed, including a repeat
## nav.push("regional", ...) that lands on the SAME screen already showing —
## reachable from more than one input path (body-click, panel+Enter) and,
## on a slow cold server, plausible for a player to trigger twice before the
## first descent settles. Without this guard, a repeat push tore down and
## rebuilt the whole tile set (orphaning in-flight requests, live round 6's
## exact storm shape for a different trigger) and refreshed the legend from
## scratch each time — confirmed source of the ~10x legend stack. Guarded
## on body_id alone (not a deep payload compare): the same body, re-entered,
## should always resume the SAME orbital session already in flight, never
## restart it — an actual body CHANGE (different id) still re-enters fresh.
## Cold-start dossier (BUG 2, PR #192 review — carried forward): ImplantApp.
## _on_screen_changed() calls enter() unconditionally on EVERY screen_changed,
## including a repeat nav.push("regional", ...) that lands on the SAME screen
## already showing — reachable from more than one input path (body-click,
## panel+Enter). Guarded on body_id alone (not a deep payload compare): the
## same body, re-entered, should always resume the SAME session already in
## flight, never restart it — an actual body CHANGE (different id) still
## re-enters fresh.
func enter(payload: Dictionary) -> void:
var body: Dictionary = payload.get("body", {})
var system: Dictionary = payload.get("system", {})
if str(body.get("body_id", "")) == _viewer.get_body_id():
return
_viewer.enter_orbital(body, system)
_viewer.enter(body, system)
func leave() -> void:
@@ -0,0 +1,178 @@
class_name StepCanvasAnnotationLayer
extends Node2D
## The unscaled screen-space annotation sibling (T-1182, D-255(a)/(e)) —
## river course polylines, settlement markers, drawn at LITERAL px sizes,
## positions world->screen transformed per-frame via a plain linear map
## (StepCanvasTransport.world_m_to_canvas_local()). This is the layer that
## makes `_zs()`/`_zs_stroke()`/`_zs_ring_radius()` structurally
## unnecessary: because this Node2D is NEVER scaled (no `.scale` write
## anywhere in this file, unlike the retired `_canvas.scale` model), a
## constant like COURSE_WIDTH_PX below already IS the on-screen width with
## no compensating division — "by construction, not by discipline" per
## Stig's round-1 design doc.
##
## Data source: the SAME decoded StepCanvasResponse `canvas` Dictionary the
## terrain layer reads (`courses`/`cliffs`/`settlement_id` — sparse
## MessagePack-native lists + one dense array, all arriving together on the
## one flat tagged response, D-255(c): "one flat tagged response carries
## every field together"). No separate whole-body Layer-1 fetch (unlike the
## retired atlas_window_nature_overlay.gd's two-source model) — every rung's
## annotations come from that SAME rung's own canvas.
##
## Draw order (bottom to top): course polylines, then settlement markers —
## matches the retired cluster's "basins-under-rivers-under-attractors"
## precedent (features layer over terrain, points over lines).
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
## Reused verbatim from the retired atlas_window_nature_overlay.gd (itself
## reused from the retired atlas_marker_overlay.gd, "Araminta's ruling:
## reuse the retired palette exactly") — same values, same source of truth.
const COLOR_RIVER: Color = Color(0.353, 0.647, 0.776, 1.0)
const COLOR_MOUTH: Color = Color(0.353, 0.647, 0.776, 1.0)
const COLOR_SETTLEMENT: Color = Color(0.94, 0.82, 0.38, 1.0)
## River class ids — mirrors server/src/atlas/body_world_state.rs
## RiverNetwork.river_class's own doc (0=stream, 1=tributary, 2=trunk),
## same vocabulary the retired atlas_window_geometry_nature.gd used.
const RIVER_CLASS_STREAM: int = 0
const RIVER_CLASS_TRIBUTARY: int = 1
const RIVER_CLASS_TRUNK: int = 2
## Course polyline width/opacity per class — LITERAL screen-space px/alpha,
## no zoom compensation needed (this layer is never scaled). Same functional
## defaults as the retired COURSE_CLASS_WIDTH_PX/COURSE_CLASS_OPACITY tables
## (Araminta's ruling, trunk widest/stream thinnest).
const COURSE_CLASS_WIDTH_PX: Dictionary = {
RIVER_CLASS_STREAM: 0.9, RIVER_CLASS_TRIBUTARY: 1.4, RIVER_CLASS_TRUNK: 2.2
}
const COURSE_CLASS_OPACITY: Dictionary = {
RIVER_CLASS_STREAM: 0.8, RIVER_CLASS_TRIBUTARY: 0.9, RIVER_CLASS_TRUNK: 1.0
}
const MOUTH_RING_RADIUS_PX: float = 5.0
const MOUTH_HALO_RADIUS_PX: float = 8.0
const MOUTH_HALO_ALPHA: float = 0.30
const SETTLEMENT_MARKER_RADIUS_PX: float = 3.5
const COURSE_TERMINUS_MOUTH: String = "Mouth"
var _canvas: Variant = null # decoded StepCanvasResponse.canvas — null until first arrival
var _world_center: Vector2 = Vector2.ZERO
var _rung: String = StepCanvasTransport.RUNG_DISTRICT
var _extent_cells: Vector2i = Vector2i.ZERO
## Adopt a new canvas + its request frame (world center, rung, extent) — the
## world->screen projection for every drawn feature depends on all three,
## so they're set together, matching the terrain layer's own texture-plus-
## frame handoff.
func set_frame(
canvas: Variant, world_center: Vector2, rung: String, extent_cells: Vector2i
) -> void:
_canvas = canvas
_world_center = world_center
_rung = rung
_extent_cells = extent_cells
queue_redraw()
func clear_frame() -> void:
_canvas = null
queue_redraw()
func _draw() -> void:
if not _canvas is Dictionary:
return
var d: Dictionary = _canvas
_draw_courses(d.get("courses", []))
_draw_settlements(d)
func _draw_courses(courses: Array) -> void:
for course_raw: Variant in courses:
if not course_raw is Dictionary:
continue
_draw_one_course(course_raw)
func _draw_one_course(course: Dictionary) -> void:
var cls: int = int(course.get("class", RIVER_CLASS_TRUNK))
var points_raw: Variant = course.get("points")
if not points_raw is Array or (points_raw as Array).size() < 2:
return
var screen_pts := PackedVector2Array()
for pt: Variant in points_raw:
if not (pt is Array and pt.size() >= 2):
continue
var world_m := Vector2(float(pt[0]), float(pt[1]))
screen_pts.append(_world_to_local(world_m))
if screen_pts.size() < 2:
return
var width: float = float(COURSE_CLASS_WIDTH_PX.get(cls, COURSE_CLASS_WIDTH_PX[RIVER_CLASS_STREAM]))
var opacity: float = float(COURSE_CLASS_OPACITY.get(cls, COURSE_CLASS_OPACITY[RIVER_CLASS_STREAM]))
var color := Color(COLOR_RIVER.r, COLOR_RIVER.g, COLOR_RIVER.b, COLOR_RIVER.a * opacity)
draw_polyline(screen_pts, color, width, true)
var terminus: String = str(course.get("terminus", ""))
if terminus == COURSE_TERMINUS_MOUTH:
_draw_mouth_ring(screen_pts[screen_pts.size() - 1])
func _draw_mouth_ring(local_pt: Vector2) -> void:
var halo_color := Color(COLOR_MOUTH.r, COLOR_MOUTH.g, COLOR_MOUTH.b, MOUTH_HALO_ALPHA)
draw_arc(local_pt, MOUTH_HALO_RADIUS_PX, 0.0, TAU, 24, halo_color, 3.0)
draw_arc(local_pt, MOUTH_RING_RADIUS_PX, 0.0, TAU, 18, COLOR_MOUTH, 1.5)
## Settlement markers — one per DISTINCT non-zero `settlement_id` cell,
## drawn at the cell's own world-metre center (not every covered cell — a
## marker per settlement anchor reads as a landmark, a marker per cell would
## flood the layer with a proximity-radius-sized cluster of dots). Cheap:
## settlement coverage is sparse by construction (SETTLEMENT_COVERAGE_RADIUS_M
## is a small disc relative to a fixed-rung canvas), so a single linear scan
## collecting first-seen positions per id is a one-off cost per canvas
## arrival, not a per-frame cost (this function only runs inside _draw(),
## itself only invoked on queue_redraw(), never a _process() poll).
func _draw_settlements(canvas: Dictionary) -> void:
var settlement_id: Variant = canvas.get("settlement_id")
if not (settlement_id is Array or settlement_id is PackedByteArray):
return
var width: int = int(canvas.get("width", 0))
if width <= 0:
return
var seen: Dictionary = {} # settlement id -> true, first-seen-cell-only
var ids: Array = settlement_id
for i in range(ids.size()):
var sid: int = int(ids[i])
if sid == 0 or seen.has(sid):
continue
seen[sid] = true
var col: int = i % width
var row: int = i / width
var world_m: Vector2 = _cell_center_world_m(col, row)
var local_pt: Vector2 = _world_to_local(world_m)
draw_circle(local_pt, SETTLEMENT_MARKER_RADIUS_PX, COLOR_SETTLEMENT)
## The world-metre center of gridunit (col, row) in the currently-held
## canvas — the inverse of step_canvas.rs's own per-cell placement
## (`center_world_m + (col - half_w) * step_m`), mirrored client-side so a
## settlement marker lands on the exact cell its id was read from.
func _cell_center_world_m(col: int, row: int) -> Vector2:
var spacing: float = StepCanvasTransport.spacing_for_rung(_rung)
var half_w: float = float(_extent_cells.x) * 0.5
var half_h: float = float(_extent_cells.y) * 0.5
return Vector2(
_world_center.x + (float(col) - half_w) * spacing,
_world_center.y + (float(row) - half_h) * spacing
)
func _world_to_local(world_m: Vector2) -> Vector2:
return StepCanvasTransport.world_m_to_canvas_local(world_m, _world_center, _rung, _extent_cells)
@@ -0,0 +1,107 @@
extends RefCounted
## Client-side in-memory LRU cache for decoded StepCanvasResponse payloads
## (T-1182, D-255(d): "cheapest-first: client in-memory LRU -> client
## disk-backed FileAccess store... -> server"). This IS the surviving LRU
## SHAPE the T-1182 ticket names — adapted from atlas_window_cache.gd's
## erase+reinsert-is-MRU idiom (Godot Dictionary preserves insertion order,
## so "move to the end on touch, evict from the front on overflow" is the
## whole implementation, no separate linked-list/counter bookkeeping), NOT
## deleted, per the ticket's own "SURVIVING SURFACES" instruction.
##
## Keyed on (body_id, rung, center, extent, min_wl_m) — the exact tuple
## server/src/atlas/step_canvas.rs's own StepCanvasCache keys on
## (StepCanvasKey = (String, StepCanvasRung, (i64,i64), (u32,u32), u32)) —
## so a client-side cache hit and a server-side cache hit are asking the
## identical question. D-227 determinism means a previously-fetched canvas
## is valid FOREVER for that exact key (geometry tier) — this is an
## LRU-evict-only cache: no freshness check, no TTL, no invalidation path.
## The disk-backed tier (T-1183) and its two-axis storage-eviction sweep are
## a SEPARATE, later concern layered underneath this one, per D-255(d)'s own
## three-tier split — this file is tier 1 only.
##
## Global (rung 0) entries key on `center=(0,0)`/`extent=(0,0)` unconditionally
## (the wire's own convention — center/extent are meaningless but still sent
## per step_canvas_protocol.gd's doc) — so every Global request for the same
## body_id collides on ONE cache slot, matching the server's own always-keep
## single-entry-per-body GlobalTierCache. This cache does NOT special-case
## a retention floor for Global the way the server's GlobalTierCache does
## (D-255(d)'s "never evicted by either eviction axis" is a SERVER-side
## always-keep guarantee) — client-side, Global is still just the
## most-recently-touched entry like anything else, protected from ordinary
## LRU pressure only by being re-touched every time the player returns to
## the top of the ladder (the common case, per this cluster's existing
## "Esc-then-re-enter and pan-back... makes this the common path"
## precedent). A dedicated client-side retention floor is exactly the
## T-1183 disk-tier ticket's Tier-1 scope, not duplicated here.
const DEFAULT_MAX_ENTRIES: int = 24
var _max_entries: int = DEFAULT_MAX_ENTRIES
var _entries: Dictionary = {} # key String -> decoded StepCanvasResponse Dictionary
func _init(max_entries: int = DEFAULT_MAX_ENTRIES) -> void:
_max_entries = maxi(1, max_entries)
## Build the cache key. `rung == "Global"` collapses center/extent to a
## fixed sentinel (0,0) regardless of what's passed — matching the wire's
## own "Global ignores center/extent" contract, so a caller that
## accidentally passes a stale center/extent for a Global request still
## lands on the correct single per-body slot.
static func make_key(
body_id: String, rung: String, center: Vector2i, extent: Vector2i, min_wl_m: int = 0
) -> String:
var key_center: Vector2i = Vector2i.ZERO if rung == "Global" else center
var key_extent: Vector2i = Vector2i.ZERO if rung == "Global" else extent
return "%s:%s:%d,%d:%d,%d:%d" % [
body_id, rung, key_center.x, key_center.y, key_extent.x, key_extent.y, min_wl_m
]
func has(
body_id: String, rung: String, center: Vector2i, extent: Vector2i, min_wl_m: int = 0
) -> bool:
return _entries.has(make_key(body_id, rung, center, extent, min_wl_m))
## Fetch a cached canvas, touching it (move-to-most-recently-used). Returns
## null on a miss.
func get_canvas(
body_id: String, rung: String, center: Vector2i, extent: Vector2i, min_wl_m: int = 0
) -> Variant:
var key := make_key(body_id, rung, center, extent, min_wl_m)
if not _entries.has(key):
return null
var value: Variant = _entries[key]
_entries.erase(key)
_entries[key] = value
return value
## Store a decoded canvas, evicting the least-recently-used entry if over
## capacity. Overwriting an existing key also counts as a touch.
func put(
body_id: String,
rung: String,
center: Vector2i,
extent: Vector2i,
canvas: Dictionary,
min_wl_m: int = 0
) -> void:
var key := make_key(body_id, rung, center, extent, min_wl_m)
if _entries.has(key):
_entries.erase(key)
_entries[key] = canvas
while _entries.size() > _max_entries:
var oldest_key: String = _entries.keys()[0]
_entries.erase(oldest_key)
func size() -> int:
return _entries.size()
func clear() -> void:
_entries.clear()
@@ -0,0 +1,115 @@
extends RefCounted
## Per-cell colorize mapping for the step-canvas terrain layer (T-1182, c1
## ruling: CPU `Image.set_pixel` coloring, confirmed round 2 — "ship CPU
## coloring... use Image.set_pixel, not a hand-rolled buffer write"). Reuses
## AtlasOverlayColors' EXISTING ramp functions verbatim — the same base/
## toggle/modifier compositing model atlas_window_overlay.gd's
## `_cell_color()`/`_apply_glaciation()` already ship (morphology hue x
## elev_q lightness base layer; temp/moisture/vegetation mutually-exclusive
## toggle overlays that REPLACE the base read; glaciation as an always-on
## post-modifier). No new palette, no new ramp — the c1 ruling's whole
## premise is that today's coloring code already proves the CPU path works;
## this file is that same pipeline pointed at RawStepCanvas-shaped decoded
## fields instead of DistrictWindowLayer ones.
##
## L8 single-channel reads (Stig round-1 measurement appendix ⑤: "4-9x
## cheaper than RGBA8 at every size... a free win if any wire field... can
## ship single-channel before the client colorizes it") — morphology/elev_q/
## moisture_q/vegetation/glaciation/flooded_q all decode as Grayscale PNGs
## server-side (step_canvas.rs's png_encode_u8_plane: "enc.set_color(png::
## ColorType::Grayscale)"), so Image.load_png_from_buffer() on the client
## already produces an L8 (FORMAT_L8) Image for each plane — this file reads
## them via get_pixel().r8 (the grayscale intensity IS the raw u8
## classification/quantized value, no separate channel unpacking needed).
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
const TOGGLE_TEMP: String = "gen_dw_temp"
const TOGGLE_MOISTURE: String = "gen_dw_moisture"
const TOGGLE_VEGETATION: String = "gen_dw_veg"
const COLOR_MOISTURE_DRY: Color = Color(0.78, 0.62, 0.35, 1.0)
const COLOR_MOISTURE_WET: Color = Color(0.25, 0.72, 0.65, 1.0)
const REGION_TEMP_NONE_DC: int = AtlasOverlayColors.REGION_TEMP_NONE_DC
## One decoded plane set, pre-extracted from the four L8 Images + the two
## raw-array fields a caller needs per cell — built once per arrived canvas
## (see StepCanvasTerrainLayer.build_texture()), not re-decoded per pixel.
## `temp_dc`/`settlement_id` stay as plain Arrays (their domain doesn't fit
## a byte plane — see step_canvas_protocol.gd's own doc).
class CellPlanes:
var morphology: Image
var elev_q: Image
var moisture_q: Image
var vegetation: Image
var glaciation: Image
var temp_dc: Array
var width: int
var height: int
## The composited color for cell (col, row), given the active toggle (empty
## string = base layer only). Mirrors atlas_window_overlay.gd's
## `_cell_color()`/`_apply_glaciation()` composition order exactly: base or
## ONE toggle (never blended), then the glaciation modifier over whichever
## is showing.
static func cell_color(planes: CellPlanes, col: int, row: int, active_toggle: String) -> Color:
var base: Color
match active_toggle:
TOGGLE_TEMP:
base = _temp_color(planes, col, row)
TOGGLE_MOISTURE:
base = _moisture_color(planes, col, row)
TOGGLE_VEGETATION:
base = _vegetation_color(planes, col, row)
_:
base = _base_color(planes, col, row)
return _apply_glaciation(planes, col, row, base)
static func _base_color(planes: CellPlanes, col: int, row: int) -> Color:
var zone: int = _l8_value(planes.morphology, col, row)
var eq: int = _l8_value(planes.elev_q, col, row)
var base: Color = AtlasOverlayColors.district_window_morphology_color(zone)
return AtlasOverlayColors.district_window_elevation_lightness(base, eq)
static func _temp_color(planes: CellPlanes, col: int, row: int) -> Color:
var idx: int = row * planes.width + col
if idx < 0 or idx >= planes.temp_dc.size():
return Color.TRANSPARENT
var t: int = int(planes.temp_dc[idx])
if t == REGION_TEMP_NONE_DC:
return Color.TRANSPARENT
return AtlasOverlayColors.region_temp_color(t)
static func _moisture_color(planes: CellPlanes, col: int, row: int) -> Color:
var m: int = clampi(_l8_value(planes.moisture_q, col, row), 0, 100)
return COLOR_MOISTURE_DRY.lerp(COLOR_MOISTURE_WET, float(m) / 100.0)
static func _vegetation_color(planes: CellPlanes, col: int, row: int) -> Color:
return AtlasOverlayColors.vegetation_color(_l8_value(planes.vegetation, col, row))
static func _apply_glaciation(planes: CellPlanes, col: int, row: int, base: Color) -> Color:
if planes.glaciation == null:
return base
return AtlasOverlayColors.glaciation_tint(base, _l8_value(planes.glaciation, col, row))
## Read one L8 plane's raw byte value at (col, row) — Image.get_pixel()
## returns a Color whose .r channel IS the grayscale intensity in [0,1] for
## an L8/FORMAT_L8 image; multiplying back by 255 and rounding recovers the
## original u8 (Godot's own get_pixel()/set_pixel() round-trip contract for
## 8-bit formats). Out-of-bounds/null plane returns 0 (the same "skip/
## fall back to zone 0" softness the rest of this cluster's dense-array
## readers already use for a malformed/short field).
static func _l8_value(plane: Image, col: int, row: int) -> int:
if plane == null or col < 0 or row < 0 or col >= plane.get_width() or row >= plane.get_height():
return 0
return int(round(plane.get_pixel(col, row).r * 255.0))
@@ -1,34 +1,29 @@
extends ImplantPanel
## Legend for AtlasWindowViewer's regional-window overlays (T-1138, D-226
## T-1124 amendment §5). Mirrors atlas_legend_panel.gd's data-driven shape
## one spec entry per overlay id, refresh() shows only the active ones — but
## scoped to the district-window screen's OWN toggle set (gen_dw_temp/
## gen_dw_moisture/gen_dw_veg) plus the two always-on layers that need a key
## even though they have no toggle id of their own: the morphology base
## (folded to ~5 family rows, per §5's "not everything earns permanent screen
## space" instinct) and the glaciation ice-tint modifier.
## Legend for StepCanvasViewer's terrain layer (T-1182) — adapted from the
## retired atlas_window_legend.gd's data-driven shape (one spec entry per
## overlay id, refresh() shows only the active ones) to the D-255(a) rung
## vocabulary. Chrome only — the compositing model (morphology base folded
## to family rows + glaciation always-on modifier + at-most-one active
## toggle section) is unchanged from the retired panel, per the ticket's own
## "compositing/legend/overlay-bar chrome (call-site updates only)"
## instruction.
##
## No `class_name` on purpose, matching atlas_legend_panel.gd (review #8
## precedent): the owner (AtlasWindowViewer) passes itself to _init().
## No `class_name` on purpose, matching every other viewer-owned helper in
## this cluster.
const PANEL_MARGIN: float = 16.0
const LEGEND_PANEL_WIDTH: float = 260.0
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
## The morphology base layer folds its 17 zones into ~5 family rows (§5:
## "mirroring T-1112's 'not everything earns permanent screen space'
## discipline") — the full 17-zone mapping stays in the city-click sidebar's
## reach, not duplicated here. Representative hue per family, picked from
## MORPHOLOGY_RGB_OPAQUE's own entries rather than a fresh set of colors.
const MORPHOLOGY_FAMILY_ROWS: Array = [
{"label": "water", "zones": [0, 1]}, # OpenOcean, Lake
{"label": "coastal / transition", "zones": [2, 3, 4, 5, 6, 7]}, # TidalFlat..Estuarine
{"label": "plains / river", "zones": [8, 9, 10, 11, 12]}, # AlluvialPlain..ValleyFloor
{"label": "upland", "zones": [13, 14]}, # MountainPass, Alpine
{"label": "volcanic / wetland", "zones": [15, 16]}, # Volcanic, Wetland
{"label": "water", "zones": [0, 1]},
{"label": "coastal / transition", "zones": [2, 3, 4, 5, 6, 7]},
{"label": "plains / river", "zones": [8, 9, 10, 11, 12]},
{"label": "upland", "zones": [13, 14]},
{"label": "volcanic / wetland", "zones": [15, 16]},
]
const GLACIATION_ROWS: Array = [
@@ -39,7 +34,7 @@ const GLACIATION_ROWS: Array = [
{"grade": 4, "label": "ice cap"},
]
var _viewer = null # AtlasWindowViewer (untyped to avoid cyclic ref)
var _viewer = null # StepCanvasViewer (untyped to avoid cyclic ref)
func _init(viewer_ref = null) -> void:
@@ -53,31 +48,15 @@ func reposition() -> void:
position = Vector2(PANEL_MARGIN, 60.0)
## Always shows the base-layer key (morphology + elevation reading, always
## on) plus glaciation (always-on modifier), then whichever toggle overlay is
## currently active, if any.
##
## The subtitle's km/cell reading is NOT a fixed "district window" label —
## it was until this fix hardcoded District's own 2.048 km/cell, which was a
## 100x lie whenever the viewer actually holds Region (204.8 km/cell) or
## Quarter (0.512 km/cell). Read through AtlasWindowGeometry.
## spacing_for_rung() — the SAME pure lookup _refresh_screen_header() uses
## for the main screen header's subtitle (screen_header_content()) — keyed
## off the viewer's current get_held_granularity_v2(), so the legend and
## header can never disagree. AtlasWindowViewer calls refresh() at every
## point _held_granularity_v2 changes (_enter_tile_mode(), _enter_at_rung(),
## _on_window_ready()'s rung-swap adoption) — see those call sites.
func refresh() -> void:
if _viewer == null:
return
clear()
visible = true
var spacing_km: float = AtlasWindowGeometry.spacing_for_rung(_viewer.get_held_granularity_v2()) / 1000.0
var subtitle: String = "%s window · %.3f km/cell" % [
_viewer.get_held_granularity_v2().to_lower(), spacing_km
]
add_component(ImplantHeader.new("REGIONAL LEGEND", subtitle))
var spacing_km: float = StepCanvasTransport.spacing_for_rung(_viewer.get_held_rung()) / 1000.0
var subtitle: String = "%s · %.3f km/gridunit" % [_viewer.get_held_rung().to_lower(), spacing_km]
add_component(ImplantHeader.new("ATLAS LEGEND", subtitle))
add_component(ImplantSeparator.new())
_add_morphology_section()
@@ -119,7 +98,7 @@ func _add_glaciation_section() -> void:
func _add_toggle_section(overlay_id: String) -> void:
match overlay_id:
"gen_dw_temp":
add_component(ImplantTextBlock.new("TEMPERATURE — cold->hot ramp (region colorizer, reused)"))
add_component(ImplantTextBlock.new("TEMPERATURE — cold->hot ramp"))
add_component(
ImplantDataRow.new("▦ cold", AtlasOverlayColors.COLOR_REGION_TEMP_COLD)
)
@@ -0,0 +1,194 @@
extends Node
## Step-canvas request orchestration for StepCanvasViewer (T-1182, D-255(c)/
## (d)). Owns the in-memory LRU cache, fires StepCanvasRequest frames, and
## retries on a Pending response — the same D-225 poll/cache/enqueue serving
## model atlas_window_request.gd already established for the legacy
## district_window carrier, now pointed at the new tagged envelope.
##
## No `class_name` on purpose, matching every other viewer-owned helper in
## this cluster (atlas_overlay_bar.gd/atlas_window_request.gd, established
## precedent): the owner (StepCanvasViewer) passes itself to `_init()`.
##
## **The extent ECHO rule (T-1181 wire addendum, mandatory per this ticket's
## own read-first list): read the echoed extent, NEVER assume the requested
## one.** `on_response()` below stores `_held_extent` from the RESPONSE's own
## `extent` field, not from whatever was sent — a server-side clamp
## (STEP_CANVAS_MAX_EXTENT_AXIS/CELLS) can shrink the actual canvas below
## what was asked for, and every downstream consumer (the terrain layer's
## footprint math, the annotation layer's cell-center math) must agree with
## what actually arrived, not what was requested. Global responses echo
## `(0, 0)` for extent (the wire's own convention — see
## step_canvas_protocol.gd's doc) — callers reading `get_held_extent()` for
## a Global-held canvas must special-case it themselves (the viewer does,
## via StepCanvasTransport.RUNG_GLOBAL checks), matching this ticket's own
## "Global rung ignores wire extent" instruction.
signal canvas_ready(response: Dictionary) # emitted on a cache hit OR a fresh Ready response
const StepCanvasCache := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_cache.gd")
const INITIAL_RETRY_DELAY: float = 0.5
const MAX_RETRY_DELAY: float = 4.0
const MAX_RETRIES: int = 30
var _owner = null # StepCanvasViewer (untyped to avoid cyclic ref)
var _cache: Variant = null # StepCanvasCache
var _body_id: String = ""
var _rung: String = ""
var _center: Vector2i = Vector2i.ZERO
var _extent: Vector2i = Vector2i.ZERO
var _min_wl_m: int = 0
## The LAST ADOPTED (Ready, matching) response's own echoed extent — see the
## class doc's "extent ECHO rule". Distinct from `_extent` (the most
## recently REQUESTED extent, which may not match what a still-in-flight
## request will echo back).
var _held_extent: Vector2i = Vector2i.ZERO
var _pending: bool = false
var _retries: int = 0
func _init(owner_ref = null) -> void:
_owner = owner_ref
_cache = StepCanvasCache.new()
## Reset in-flight bookkeeping for a fresh body/rung entry — does NOT clear
## the cache (D-227: a cached canvas is valid forever for that exact key).
func reset() -> void:
_pending = false
_retries = 0
## Fire (or serve from cache) a step-canvas request. Cache hit -> immediate
## synchronous canvas_ready emit, no network traffic. Cache miss -> send the
## request now; the response (or a Pending retry chain) arrives later via
## on_response().
func request_now(
body_id: String, rung: String, center: Vector2i, extent: Vector2i, min_wl_m: int = 0
) -> void:
_body_id = body_id
_rung = rung
_center = center
_extent = extent
_min_wl_m = min_wl_m
var cached: Variant = _cache.get_canvas(body_id, rung, center, extent, min_wl_m)
if cached != null:
_pending = false
_retries = 0
_held_extent = _echoed_extent(cached, rung, extent)
canvas_ready.emit(cached)
return
_pending = true
_retries = 0
SimBridge.request_step_canvas(body_id, rung, center, extent, min_wl_m)
## Handle a StepCanvasResponse (routed by the owning viewer from its own
## SimBridge.step_canvas_received subscription). Ignores a response for a
## stale body/rung/center/extent/min_wl_m (the player scrolled to a
## different step, or panned, while this was in flight) — the echoed fields
## are the staleness guard, same discipline atlas_window_request.gd's own
## on_response() uses for the legacy carrier.
func on_response(response: Dictionary) -> void:
if not _matches_this_request(response):
return
var status := str(response.get("status", ""))
if status == "Pending":
_retry_if_pending()
return
if status != "Ready":
_pending = false # NotFound / Error — a real failure, give up now
return
if not _echo_matches_held_frame(response):
return
var canvas: Variant = response.get("canvas")
if canvas == null:
# A Ready-status response with no canvas payload — treat as still
# generating (matches atlas_window_request.gd's own "district_window:
# None inside an outer Ready" precedent for the same underlying
# concept, one carrier over).
_retry_if_pending()
return
_pending = false
_retries = 0
_held_extent = _echoed_extent(canvas, _rung, response.get("extent", Vector2i.ZERO))
_cache.put(_body_id, _rung, _center, _extent, canvas, _min_wl_m)
canvas_ready.emit(canvas)
## Coarse staleness gate: body_id/rung identity alone (before touching the
## echoed frame fields, which only mean something once identity matches).
func _matches_this_request(response: Dictionary) -> bool:
if str(response.get("body_id", "")) != _body_id:
return false
return str(response.get("rung", "")) == _rung
## Fine staleness gate: the echoed center/min_wl_m must match what THIS
## object most recently asked for. Global ignores center/extent server-side
## (step_canvas_protocol.gd's own doc) — the guard skips both fields for
## that rung, matching the wire's own "meaningless but still echoed"
## contract.
func _echo_matches_held_frame(response: Dictionary) -> bool:
var echoed_min_wl_m: int = int(response.get("min_wl_m", 0))
if echoed_min_wl_m != _min_wl_m:
return false
if _rung == "Global":
return true
var echoed_center: Vector2i = response.get("center", Vector2i.ZERO)
return echoed_center == _center
## Extent to hold for downstream consumers — echoed extent for a fixed rung,
## the CANVAS's own width/height for Global (whose wire extent echo is a
## fixed (0,0) sentinel, per the class doc).
static func _echoed_extent(canvas: Variant, rung: String, echoed: Vector2i) -> Vector2i:
if rung == "Global" and canvas is Dictionary:
var d: Dictionary = canvas
return Vector2i(int(d.get("width", 0)), int(d.get("height", 0)))
return echoed
func _retry_if_pending() -> void:
if not _pending:
return
if _retries < MAX_RETRIES:
_retries += 1
_schedule_retry()
else:
_pending = false
static func _retry_delay_for(retry_count: int) -> float:
var base: float = INITIAL_RETRY_DELAY * pow(2.0, float(maxi(retry_count - 1, 0)))
return minf(base, MAX_RETRY_DELAY)
func _schedule_retry() -> void:
var delay: float = _retry_delay_for(_retries)
var timer := get_tree().create_timer(delay)
timer.timeout.connect(
func() -> void:
if _pending:
SimBridge.request_step_canvas(_body_id, _rung, _center, _extent, _min_wl_m)
)
func is_pending() -> bool:
return _pending
func get_held_extent() -> Vector2i:
return _held_extent
func get_cache() -> Variant:
return _cache
@@ -0,0 +1,146 @@
class_name StepCanvasTerrainLayer
extends Node2D
## The RTT terrain layer (T-1182, D-255(a)/(b)/(e)) — one ImageTexture per
## held step canvas, built server-side-derived / client-colorized, drawn
## texel-exact at the rung's own display ratio. This is the PROMOTION Stig's
## round-1 doc names: "_rebuild_texture_if_needed/_build_tile_texture in
## atlas_window_overlay.gd generalized from 'per-tile mosaic composite' to
## 'the one and only terrain path'" — same Image/ImageTexture/set_pixel
## pipeline, now universal across all six D-255(a) rungs instead of being
## split across a single-window path and a tile-mosaic path.
##
## **texture.update() reuse on step-cross** (c1/⑤ ruling: "prefer
## texture.update() reuse over fresh create_from_image() per step... not for
## raw speed... but because it avoids per-step Texture object churn on the
## RenderingServer side"): rebuild_from_canvas() reuses `_texture` via
## `.update()` whenever the new canvas is the SAME pixel size as the held
## one (the common case — every fixed rung requests the same viewport-fit
## extent repeatedly), and only calls `ImageTexture.create_from_image()` when
## the size actually changes (a rung crossing to a differently-sized canvas,
## or the very first canvas).
##
## **NEAREST/LINEAR per rung** (_filter_for_rung(), T-1161's surviving
## ruling, ticket SURVIVING SURFACES: "_filter_for_granularity_v2... is
## wired"): Global/Region sample NEAREST (coarse orbital-scale data —
## bilinear blending between real derived samples reads as smoothing-over-
## absence); District through Chunk sample LINEAR (cell density earns the
## blend). Renamed for the D-255(a) rung vocabulary but the POLICY is
## unchanged from the retired atlas_window_overlay.gd's own ruling.
##
## **Determinism boundary (D-255(e)):** this node draws EXACTLY the cells
## the server supplied, at the display ratio's texture-to-viewport resize
## (the one sanctioned display-time scale) — it never invents a sample
## between two server cells, never smooths across a canvas edge. The GPU's
## own LINEAR filter, where selected, blends between ADJACENT REAL SAMPLES
## already present in the texture — this is presentation resampling of a
## closed input set, not invention of a new one, matching D-255(e)'s own
## "texture-to-viewport resize" exemption.
const StepCanvasColorize := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd")
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
var _texture: ImageTexture = null
var _texture_size: Vector2i = Vector2i.ZERO
var _canvas_ref: Variant = null # reference-identity rebuild-only-on-change guard
var _active_toggle: String = ""
var _footprint_px: Vector2 = Vector2.ZERO
var _held_rung: String = StepCanvasTransport.RUNG_DISTRICT
## Rebuild (or reuse) the held texture from a decoded StepCanvasResponse's
## `canvas` Dictionary (step_canvas_protocol.gd's shape: width/height +
## PackedByteArray PNG planes + raw temp_dc/settlement_id arrays). No-op if
## `canvas` is the SAME object (reference identity, is_same()) and the
## active toggle hasn't changed — matches atlas_window_overlay.gd's own
## "REBUILT only when its inputs change" discipline, now the terrain layer's
## own hold contract for the "hold-fetch-swap, not blend-fetch-swap"
## step-cross sequence (round-1 §2): the OLD texture keeps drawing (this
## function simply isn't called) while a new canvas is in flight.
func rebuild_from_canvas(canvas: Dictionary, rung: String, active_toggle: String) -> void:
if is_same(_canvas_ref, canvas) and _active_toggle == active_toggle and _texture != null:
return
var width: int = int(canvas.get("width", 0))
var height: int = int(canvas.get("height", 0))
if width <= 0 or height <= 0:
return
var planes := _decode_planes(canvas, width, height)
var img := Image.create(width, height, false, Image.FORMAT_RGBA8)
for row in range(height):
for col in range(width):
img.set_pixel(col, row, StepCanvasColorize.cell_color(planes, col, row, active_toggle))
var new_size := Vector2i(width, height)
if _texture != null and _texture_size == new_size:
_texture.update(img)
else:
_texture = ImageTexture.create_from_image(img)
_texture_size = new_size
_canvas_ref = canvas
_active_toggle = active_toggle
_held_rung = rung
_footprint_px = StepCanvasTransport.canvas_footprint_px(rung, new_size)
texture_filter = _filter_for_rung(rung)
queue_redraw()
## Decode the four L8-plane PackedByteArrays (via Image.load_png_from_buffer,
## per step_canvas_protocol.gd's own PNG-per-field wire note) plus the two
## raw arrays into one CellPlanes bundle, once per rebuild.
func _decode_planes(canvas: Dictionary, width: int, height: int) -> StepCanvasColorize.CellPlanes:
var planes := StepCanvasColorize.CellPlanes.new()
planes.width = width
planes.height = height
planes.morphology = _decode_l8_plane(canvas.get("morphology"))
planes.elev_q = _decode_l8_plane(canvas.get("elev_q"))
planes.moisture_q = _decode_l8_plane(canvas.get("moisture_q"))
planes.vegetation = _decode_l8_plane(canvas.get("vegetation"))
planes.glaciation = _decode_l8_plane(canvas.get("glaciation"))
planes.temp_dc = canvas.get("temp_dc", [])
return planes
## PackedByteArray PNG bytes -> Image, or null on a decode failure/empty
## input (a malformed/missing plane draws as the composite's own fallback
## color for that cell — StepCanvasColorize's _l8_value() treats a null
## plane as 0, never crashes).
static func _decode_l8_plane(field: Variant) -> Variant:
if not field is PackedByteArray or (field as PackedByteArray).is_empty():
return null
var img := Image.new()
var err: int = img.load_png_from_buffer(field)
if err != OK:
push_warning("StepCanvasTerrainLayer: PNG plane decode failed (err %d)" % err)
return null
return img
## T-1161's surviving per-rung filter ruling (ticket: "_filter_for_
## granularity_v2... is wired"), repointed at the D-255(a) rung vocabulary.
static func _filter_for_rung(rung: String) -> CanvasItem.TextureFilter:
if StepCanvasTransport.is_orbital_rung(rung):
return CanvasItem.TEXTURE_FILTER_NEAREST
return CanvasItem.TEXTURE_FILTER_LINEAR
func _draw() -> void:
if _texture == null or _footprint_px == Vector2.ZERO:
return
draw_texture_rect(_texture, Rect2(Vector2.ZERO, _footprint_px), false)
## The currently-held canvas's on-screen footprint (px) — the annotation
## layer's world->screen projection targets this SAME rect (D-255(a):
## both layers agree on placement by construction, not reconciliation).
func get_footprint_px() -> Vector2:
return _footprint_px
func get_held_rung() -> String:
return _held_rung
func has_texture() -> bool:
return _texture != null
@@ -0,0 +1,243 @@
extends RefCounted
## Pure stepped-transport geometry for the D-255(a) six-rung Atlas ladder
## (T-1182). This is the FULL replacement for the retired
## `_canvas.scale`/`_view_zoom` continuous-zoom model — there is no float
## zoom here at all, only a discrete rung INDEX (0-5) and integer world-metre
## step math. Every function is a pure function of its arguments (no Node/
## scene-tree dependency), matching this cluster's existing
## atlas_window_geometry.gd discipline, so the transport state machine is
## directly unit-testable.
##
## D-255(a) ladder, rung index -> D-243 gridunit spacing (metres). Rung 0
## (Global) has no single spacing float in the same sense as the fixed rungs
## (its gridunit is "one region" but its CANVAS EXTENT floats per body) — see
## RUNG_SPACING_M's own doc.
const RUNG_GLOBAL: String = "Global"
const RUNG_REGION: String = "Region"
const RUNG_DISTRICT: String = "District"
const RUNG_QUARTER: String = "Quarter"
const RUNG_BLOCK: String = "Block"
const RUNG_CHUNK: String = "Chunk"
## Coarsest (0) to deepest (5) — the SIX levels D-255(a) names, index IS the
## rung. scroll_step()/rung_at_index() below are the only places this table
## is walked; every other consumer works in terms of the rung NAME (matching
## the wire's own bare-string vocabulary, never a raw integer sent over the
## bridge).
const RUNG_LADDER: Array = [
RUNG_GLOBAL, RUNG_REGION, RUNG_DISTRICT, RUNG_QUARTER, RUNG_BLOCK, RUNG_CHUNK
]
## D-243 gridunit spacing in metres for every FIXED rung — mirrors
## server/src/atlas/scale.rs's own constants exactly (REGION_M/DISTRICT_M/
## QUARTER_M/BLOCK_M/CHUNK_M), so the client's rung table can never silently
## drift from the wire contract it's choosing between. Global has no single
## spacing value in the fixed sense (D-255(a): "a Global gridunit and a
## Region gridunit are both 'one region' wide" — step_canvas.rs's own
## StepCanvasRung::spacing_m() returns REGION_M for Global too, "a
## harmless-but-correct value... so ordering/comparison call sites... get a
## sane, documented number rather than 0 or a panic") — mirrored here for the
## same reason.
const RUNG_SPACING_M: Dictionary = {
RUNG_GLOBAL: 204_800.0,
RUNG_REGION: 204_800.0,
RUNG_DISTRICT: 2_048.0,
RUNG_QUARTER: 512.0,
RUNG_BLOCK: 128.0,
RUNG_CHUNK: 64.0,
}
## D-255(a) display-ratio band: screen-px per gridunit. 1x1 at the deep/mid
## rungs (Stig round-2 §(d): "crisper display costs nothing extra... no cost
## reason to fall back to 5x5 at the steps where fidelity matters most"), the
## ~5x5 fallback earning its keep only at the shallow/orbital end where
## canvas EXTENT (not spacing) is what's growing. This is PURELY a
## presentation parameter (D-255(a)/D-243 gridunit amendment: "a free,
## client-side, viewport-dependent parameter, kept architecturally separate
## from gridunit spacing") — it never touches a cache key or a wire request,
## only how many screen px one already-fetched gridunit occupies.
const DISPLAY_RATIO_DEEP: float = 1.0
const DISPLAY_RATIO_SHALLOW: float = 5.0
## Global/Region read at the shallow ratio (orbital-scale canvases — extent
## is what dominates, not per-cell fidelity); District..Chunk read at 1x1
## (D-255(a): "the deep, ground-level steps where the player is closest to
## visible detail"). Chunk's own "1 screen px per 64 m gridunit, no
## magnification margin" bottom-out rule (D-255(a)) is exactly DISPLAY_RATIO_DEEP.
const DISPLAY_RATIO_BY_RUNG: Dictionary = {
RUNG_GLOBAL: DISPLAY_RATIO_SHALLOW,
RUNG_REGION: DISPLAY_RATIO_SHALLOW,
RUNG_DISTRICT: DISPLAY_RATIO_DEEP,
RUNG_QUARTER: DISPLAY_RATIO_DEEP,
RUNG_BLOCK: DISPLAY_RATIO_DEEP,
RUNG_CHUNK: DISPLAY_RATIO_DEEP,
}
## Fixed-rung canvas pixel budget (D-255(a)/(b): "the fixed 3840x2160 px
## budget... every step except Global") — mirrors
## server/src/atlas/step_canvas.rs's STEP_CANVAS_MAX_EXTENT_AXIS/
## STEP_CANVAS_MAX_EXTENT_CELLS ceiling. The client requests THIS extent
## (subject to viewport-fit shrinking, see viewport_fit_extent()) for every
## fixed rung; Global's extent is never read from this constant at all (the
## server derives it from the body's own region grid — the client sends
## SOME extent value per the wire's unconditional field, but it is IGNORED
## server-side, per step_canvas_protocol.gd's own doc).
const FIXED_CANVAS_MAX_AXIS: int = 3_840
## The rung name at ladder index `i`, clamped to the legal [0, 5] range —
## the one place RUNG_LADDER is indexed into, so a caller passing an
## out-of-range index (a scroll past either end) gets the nearest legal rung
## rather than an array-bounds error.
static func rung_at_index(index: int) -> String:
var clamped: int = clampi(index, 0, RUNG_LADDER.size() - 1)
return RUNG_LADDER[clamped]
## The ladder index for a rung name, or -1 if unrecognized (defensive — every
## real caller passes a RUNG_* constant, but an unrecognized wire echo must
## never silently alias to index 0/Global).
static func index_for_rung(rung: String) -> int:
return RUNG_LADDER.find(rung)
## Scroll one notch: `direction` > 0 descends (coarser -> finer, e.g.
## Region -> District), < 0 ascends (finer -> coarser). Clamped at both ends
## — scrolling past Chunk stays at Chunk, scrolling past Global stays at
## Global (the "full-zoom-out reset" is a SEPARATE explicit action, not a
## side effect of this function — see StepCanvasViewer's own reset-to-Global
## handling).
static func scroll_step(current_index: int, direction: int) -> int:
var delta: int = 1 if direction > 0 else (-1 if direction < 0 else 0)
return clampi(current_index + delta, 0, RUNG_LADDER.size() - 1)
static func spacing_for_rung(rung: String) -> float:
return float(RUNG_SPACING_M.get(rung, RUNG_SPACING_M[RUNG_DISTRICT]))
static func display_ratio_for_rung(rung: String) -> float:
return float(DISPLAY_RATIO_BY_RUNG.get(rung, DISPLAY_RATIO_DEEP))
## True for the two rungs that ride derive_orbital_at_metres server-side
## (Global/Region, step_canvas.rs's own StepCanvasRung::uses_orbital_derive())
## — mirrored here purely for READABILITY at call sites that branch on it
## (e.g. "does this rung's canvas ever carry courses" — Global/Region never
## do, matching invent_courses_for_canvas()'s own early return), not because
## the client makes any derivation decision itself (D-255(e): derivation
## stays server-side, full stop).
static func is_orbital_rung(rung: String) -> bool:
return rung == RUNG_GLOBAL or rung == RUNG_REGION
## World-metre HALF-EXTENT (radius from center to edge) a fixed-rung canvas
## of `extent_cells` x `extent_cells` covers, given the rung's own gridunit
## spacing — the request-sizing half of the cursor-anchored step math.
static func half_extent_m(rung: String, extent_cells: int) -> float:
return float(extent_cells) * 0.5 * spacing_for_rung(rung)
## Cursor-anchored step center (D-255(a)/(e), the workshop's own "the center
## the new canvas should be requested around is the world point under the
## cursor" rule): given the CURRENT step's world transform (center in world
## metres, spacing, canvas cell extent, display ratio) and a cursor position
## in canvas-local px (canvas-local = the terrain layer's own local space,
## origin at the canvas's top-left, BEFORE any screen offset), returns the
## world-metre point under the cursor. This is the point the NEXT step's
## request should center on — computed once per scroll notch, not per frame.
static func canvas_local_to_world_m(
canvas_local: Vector2, world_center: Vector2, rung: String, extent_cells: Vector2i
) -> Vector2:
var spacing: float = spacing_for_rung(rung)
var ratio: float = display_ratio_for_rung(rung)
var px_per_gridunit: float = maxf(ratio, 0.0001)
var half_w_m: float = float(extent_cells.x) * 0.5 * spacing
var half_h_m: float = float(extent_cells.y) * 0.5 * spacing
var local_m: Vector2 = canvas_local / px_per_gridunit * spacing
return Vector2(
world_center.x - half_w_m + local_m.x, world_center.y - half_h_m + local_m.y
)
## Inverse of canvas_local_to_world_m() — world metres -> this step's
## canvas-local px (the terrain/annotation layers' shared world->screen
## projection, D-255(e): "texture-to-viewport resize" is the one sanctioned
## display-time scale, this is that same linear map applied to a point
## rather than a texture).
static func world_m_to_canvas_local(
world_m: Vector2, world_center: Vector2, rung: String, extent_cells: Vector2i
) -> Vector2:
var spacing: float = spacing_for_rung(rung)
var ratio: float = display_ratio_for_rung(rung)
var px_per_gridunit: float = maxf(ratio, 0.0001)
var half_w_m: float = float(extent_cells.x) * 0.5 * spacing
var half_h_m: float = float(extent_cells.y) * 0.5 * spacing
var local_m: Vector2 = Vector2(world_m.x - world_center.x + half_w_m, world_m.y - world_center.y + half_h_m)
return local_m / spacing * px_per_gridunit
## The on-screen footprint (px) of a fixed-rung canvas at its display ratio —
## `extent_cells * display_ratio`. The terrain layer draws its texture into
## exactly this Rect2 size; the annotation layer's world->screen projection
## targets the same footprint so both layers agree on where a world point
## lands, by construction (no separate reconciliation step).
static func canvas_footprint_px(rung: String, extent_cells: Vector2i) -> Vector2:
var ratio: float = display_ratio_for_rung(rung)
return Vector2(extent_cells) * ratio
## Fit a fixed-rung request's extent (in gridunits) to the viewport, capped
## at FIXED_CANVAS_MAX_AXIS per axis (D-255(a)/(b)'s own budget) and at the
## rung's own display ratio — this is the CLIENT's half of "viewport-sized
## canvas" (Dudley's server-side policy is the derivation-cost/D-226(d)
## argument; this is the client not requesting more than it can display).
## `viewport_px` is the on-screen area to fill; the request extent in
## GRIDUNITS is `viewport_px / display_ratio`, clamped to
## [1, FIXED_CANVAS_MAX_AXIS] per axis — the server clamps independently too
## (never trust the echo to equal the request), see StepCanvasViewer's own
## "read the echoed extent" discipline.
static func viewport_fit_extent(viewport_px: Vector2, rung: String) -> Vector2i:
var ratio: float = maxf(display_ratio_for_rung(rung), 0.0001)
var cells: Vector2 = viewport_px / ratio
var w: int = clampi(int(ceil(cells.x)), 1, FIXED_CANVAS_MAX_AXIS)
var h: int = clampi(int(ceil(cells.y)), 1, FIXED_CANVAS_MAX_AXIS)
return Vector2i(w, h)
## WASD + arrow keys, read via Input.is_key_pressed() on the PHYSICAL keycode
## (not an InputMap action) — same rationale the retired
## atlas_window_geometry.gd's own held_pan_direction() documented: this
## project's global InputMap already binds W/S/A/D to gameplay movement
## actions, so reading the raw physical keycode keeps this screen's pan
## input independent of whatever gameplay's own action bindings are. Returns
## a raw (non-normalized) direction — the caller normalizes once after
## adding the edge-scroll contribution.
static func held_pan_direction() -> Vector2:
var direction := Vector2.ZERO
if Input.is_key_pressed(KEY_W) or Input.is_key_pressed(KEY_UP):
direction.y -= 1.0
if Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_DOWN):
direction.y += 1.0
if Input.is_key_pressed(KEY_A) or Input.is_key_pressed(KEY_LEFT):
direction.x -= 1.0
if Input.is_key_pressed(KEY_D) or Input.is_key_pressed(KEY_RIGHT):
direction.x += 1.0
return direction
## Snap a world-metre point to the rung's own gridunit grid — the request
## `center` a fixed-rung StepCanvasRequest sends should land on a grid line
## so repeated requests at "the same spot" produce the identical cache key
## (D-227: a canvas for a fixed seed never changes, so exact-repeat cache
## hits are the common case worth protecting). Global ignores center
## entirely server-side (step_canvas_protocol.gd's own doc) so snapping is a
## harmless no-op there.
static func snap_to_gridunit(world_m: Vector2, rung: String) -> Vector2i:
var spacing: float = spacing_for_rung(rung)
if spacing <= 0.0:
return Vector2i(int(round(world_m.x)), int(round(world_m.y)))
return Vector2i(
int(round(world_m.x / spacing)) * int(spacing), int(round(world_m.y / spacing)) * int(spacing)
)
@@ -0,0 +1,523 @@
class_name StepCanvasViewer
extends Control
## The stepped Atlas ladder viewer (T-1182, D-255) — replaces
## AtlasWindowViewer/the `_canvas.scale` continuous-zoom model wholesale.
## "Global opener is rung 0, one viewer one path" (D-255(a) design premise,
## carried verbatim from the ticket): there is no separate orbital-mosaic
## viewer and windowed-drill-down viewer — every rung, including the Global
## body-surface opener, is served by this ONE Control through the SAME
## StepCanvasRequest/Response tagged envelope (T-1181).
##
## Structure (Stig round-1 §1, "naturally three pieces again"):
## - THIS Control: input/pan, rung-transport orchestration, chrome.
## - StepCanvasTerrainLayer (Node2D child of `_canvas`): RTT terrain,
## texel-exact, drawn at the rung's display ratio.
## - StepCanvasAnnotationLayer (Node2D child of `_canvas`, drawn AFTER the
## terrain layer): unscaled screen-space courses/settlement markers.
## Both layers live under ONE `_canvas` Node2D whose `.position` is the pan
## offset ONLY — there is no `.scale` write anywhere in this file (the whole
## point of the retirement: "there is no more zoom-scaled canvas").
##
## Stepped transport (D-255(a)): a discrete rung INDEX (0-5,
## StepCanvasTransport.RUNG_LADDER), never a float zoom. Mouse wheel scrolls
## one rung notch per detent, cursor-anchored (the world point under the
## cursor becomes the next step's request center — Stig round-1 §2).
## Edge-scroll/WASD pan within a held rung; panning past the held canvas's
## own edge re-requests the SAME rung at a new center (mirrors the retired
## viewer's own pan-edge refetch, just against the new wire). A hard
## zoom-out past rung 0 resets to the Global frame (D-255(a)'s "hard
## full-zoom-out reset").
##
## Hold-fetch-swap (Stig round-1 §2, the shipped baseline — morph/tween is
## an optional cosmetic follow-on, NOT built here per the ticket): on a
## scroll-step, the CURRENT step's texture stays displayed, unscaled, while
## the new step's request is in flight (StepCanvasTerrainLayer simply isn't
## rebuilt until the new canvas arrives — "hold" is the ABSENCE of a
## premature rebuild, not a separate code path).
signal back_pressed
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
const StepCanvasTerrainLayer := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_terrain_layer.gd")
const StepCanvasAnnotationLayer := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_annotation_layer.gd")
const StepCanvasRequest := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_request.gd")
const PANEL_MARGIN: float = 16.0
const OVERLAY_BAR_HEADER_RESERVE: float = 360.0
const PAN_SPEED_PX_S: float = 220.0
const EDGE_SCROLL_MARGIN_PX: float = 24.0
const COLOR_BG: Color = Color("#0d1117")
const COLOR_BORDER_FADE: Color = Color(0.20, 0.24, 0.30, 0.55)
const COLOR_PENDING_WASH: Color = Color(0.20, 0.24, 0.30, 0.12)
const COLOR_DERIVING_LABEL: Color = Color("#667788")
const OVERLAY_DEFS: Array = [
{
"id": "gen_dw_temp",
"label": "TMP",
"group": "toggle",
"tooltip": "Temperature — region-ramp colorizer."
},
{
"id": "gen_dw_moisture",
"label": "MST",
"group": "toggle",
"tooltip": "Moisture — dry-to-wet ramp."
},
{
"id": "gen_dw_veg",
"label": "VEG",
"group": "toggle",
"tooltip": "Vegetation — green-family ramp. Marine reads transparent."
},
]
# ── Context (set by enter()) ──────────────────────────────────────────────
var _body: Dictionary = {}
var _system: Dictionary = {}
var _implant_theme = null
# ── Rung transport state ──────────────────────────────────────────────────
var _rung_index: int = 0 # 0 == Global, the entry rung (D-255(a))
var _world_center: Vector2 = Vector2.ZERO
var _held_rung: String = StepCanvasTransport.RUNG_GLOBAL
var _held_extent: Vector2i = Vector2i.ZERO
# ── Pan state (position only — NO scale/zoom field anywhere) ─────────────
var _view_offset: Vector2 = Vector2.ZERO
var _last_mouse_pos: Vector2 = Vector2(-1.0, -1.0)
var _app_has_focus: bool = true
# ── Overlay visibility ─────────────────────────────────────────────────────
var _overlay_visibility: Dictionary = {}
# ── Child nodes ────────────────────────────────────────────────────────────
var _canvas: Node2D = null
var _terrain_layer: StepCanvasTerrainLayer = null
var _annotation_layer: StepCanvasAnnotationLayer = null
var _screen_header: ImplantHeader = null
var _overlay_bar = null
var _legend_panel = null
var _request = null # StepCanvasRequest
func _ready() -> void:
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = Control.GROW_DIRECTION_BOTH
grow_vertical = Control.GROW_DIRECTION_BOTH
mouse_filter = Control.MOUSE_FILTER_STOP
focus_mode = Control.FOCUS_ALL
_implant_theme = load("res://ui/implant/default_implant.tres")
for def: Dictionary in OVERLAY_DEFS:
_overlay_visibility[def["id"]] = false
_canvas = Node2D.new()
_canvas.name = "StepCanvas"
add_child(_canvas)
_terrain_layer = StepCanvasTerrainLayer.new()
_terrain_layer.name = "TerrainLayer"
_canvas.add_child(_terrain_layer)
_annotation_layer = StepCanvasAnnotationLayer.new()
_annotation_layer.name = "AnnotationLayer"
_canvas.add_child(_annotation_layer)
_request = StepCanvasRequest.new(self)
_request.name = "Request"
add_child(_request)
_request.canvas_ready.connect(_on_canvas_ready)
_build_screen_header()
_build_overlay_bar()
_build_legend_panel()
SimBridge.step_canvas_received.connect(_on_step_canvas_received)
func _exit_tree() -> void:
if SimBridge.step_canvas_received.is_connected(_on_step_canvas_received):
SimBridge.step_canvas_received.disconnect(_on_step_canvas_received)
## Enter the ladder at the Global opener (rung 0) — the sole entry point
## (D-255(a): "Global opener is rung 0, one viewer one path"). Replaces both
## the retired enter()/enter_orbital() split — there is no District-rung
## direct-entry variant anymore, since descent from Global is a continuous
## scroll gesture, not a nav-stack choice.
func enter(body: Dictionary, system: Dictionary) -> void:
_body = body
_system = system
_rung_index = 0
_world_center = Vector2.ZERO
_held_rung = StepCanvasTransport.RUNG_GLOBAL
_held_extent = Vector2i.ZERO
_view_offset = Vector2.ZERO
_request.reset()
_annotation_layer.clear_frame()
_fire_request()
_refresh_screen_header()
if _legend_panel:
_legend_panel.refresh()
grab_focus()
queue_redraw()
func leave() -> void:
pass
func get_body_id() -> String:
return _dict_str(_body, "body_id", "")
func get_held_rung() -> String:
return _held_rung
func is_overlay_visible(overlay_id: String) -> bool:
return bool(_overlay_visibility.get(overlay_id, false))
func set_overlay_visible(overlay_id: String, visible_state: bool) -> void:
if not _overlay_visibility.has(overlay_id):
push_warning("StepCanvasViewer: unknown overlay id '%s'" % overlay_id)
return
_overlay_visibility[overlay_id] = visible_state
_rebuild_terrain_texture()
if _legend_panel:
_legend_panel.refresh()
func get_overlay_defs() -> Array:
return OVERLAY_DEFS
# =============================================================================
# Request lifecycle
# =============================================================================
## Fire the current step's request — cursor-anchored center + viewport-fit
## extent for a fixed rung; Global sends the same fields unconditionally
## (ignored server-side, per step_canvas_protocol.gd's own doc).
func _fire_request() -> void:
var extent: Vector2i = _request_extent()
var center: Vector2i = StepCanvasTransport.snap_to_gridunit(_world_center, _held_rung)
_request.request_now(get_body_id(), _held_rung, center, extent)
func _request_extent() -> Vector2i:
var viewport: Vector2 = get_rect().size
if viewport == Vector2.ZERO:
viewport = Vector2(1280.0, 720.0)
return StepCanvasTransport.viewport_fit_extent(viewport, _held_rung)
func _on_step_canvas_received(response: Dictionary) -> void:
_request.on_response(response)
## A canvas is ready (cache hit OR a fresh Ready response) — adopt it. This
## is the ONE place the terrain/annotation layers are told to rebuild; until
## this fires, the layers keep showing whatever they already held (the
## hold-fetch-swap contract — Stig round-1 §2).
func _on_canvas_ready(canvas: Dictionary) -> void:
_held_extent = _request.get_held_extent()
_rebuild_terrain_texture(canvas)
_annotation_layer.set_frame(canvas, _world_center, _held_rung, _held_extent)
_refresh_screen_header()
if _legend_panel:
_legend_panel.refresh()
queue_redraw()
func _rebuild_terrain_texture(canvas: Variant = null) -> void:
var c: Variant = canvas if canvas != null else _terrain_layer._canvas_ref
if not c is Dictionary:
return
_terrain_layer.rebuild_from_canvas(c, _held_rung, _active_toggle_overlay())
func _active_toggle_overlay() -> String:
if is_overlay_visible("gen_dw_temp"):
return "gen_dw_temp"
if is_overlay_visible("gen_dw_moisture"):
return "gen_dw_moisture"
if is_overlay_visible("gen_dw_veg"):
return "gen_dw_veg"
return ""
# =============================================================================
# Rung transport — cursor-anchored scroll step, edge-crossing pan re-request
# =============================================================================
## One scroll-wheel notch: `direction` > 0 descends (coarser -> finer),
## < 0 ascends. Cursor-anchored (D-255(a)/Stig round-1 §2): the world point
## under the cursor becomes the new step's request center, so the player's
## point of interest stays put across the crossing.
func _scroll_rung(direction: int, cursor_local: Vector2) -> void:
var cursor_world: Vector2 = StepCanvasTransport.canvas_local_to_world_m(
cursor_local - _view_offset, _world_center, _held_rung, _held_extent
)
var new_index: int = StepCanvasTransport.scroll_step(_rung_index, direction)
if new_index == _rung_index:
return
_rung_index = new_index
_held_rung = StepCanvasTransport.rung_at_index(_rung_index)
_world_center = cursor_world if _held_rung != StepCanvasTransport.RUNG_GLOBAL else Vector2.ZERO
_view_offset = Vector2.ZERO
_fire_request()
_refresh_screen_header()
queue_redraw()
## Hard reset to the Global opener (D-255(a): "a hard full-zoom-out reset to
## the canonical Global body-surface frame").
func _reset_to_global() -> void:
if _rung_index == 0:
return
_rung_index = 0
_held_rung = StepCanvasTransport.RUNG_GLOBAL
_world_center = Vector2.ZERO
_view_offset = Vector2.ZERO
_fire_request()
_refresh_screen_header()
queue_redraw()
## Pan-edge re-request: once the view has panned far enough that the held
## canvas's own edge would show, re-request the SAME rung at a new center —
## mirrors the retired viewer's own _maybe_refloat_window(), against the new
## wire's request shape. Global never re-requests on pan (its canvas is the
## whole body, D-255(a) — no edge to cross).
func _maybe_refloat() -> void:
if _held_rung == StepCanvasTransport.RUNG_GLOBAL:
return
var footprint: Vector2 = _terrain_layer.get_footprint_px()
if footprint == Vector2.ZERO:
return
var half: Vector2 = footprint * 0.5
# view_offset is the SCREEN position of canvas-local (0,0) — the canvas's
# center in canvas-local space is `half`. Once panning has moved that
# point more than half the footprint away from screen-center, the edge
# is at or past the viewport's own center — time to re-float.
var screen_center: Vector2 = get_rect().size * 0.5
var canvas_center_screen: Vector2 = _view_offset + half
var drift: Vector2 = canvas_center_screen - screen_center
if absf(drift.x) < half.x * 0.5 and absf(drift.y) < half.y * 0.5:
return
var new_center_world: Vector2 = StepCanvasTransport.canvas_local_to_world_m(
screen_center - _view_offset, _world_center, _held_rung, _held_extent
)
_world_center = new_center_world
_view_offset = Vector2.ZERO
_fire_request()
# =============================================================================
# Drawing
# =============================================================================
func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, get_rect().size), COLOR_BG)
if not _terrain_layer.has_texture():
_draw_border_fade()
elif _request.is_pending():
_draw_pending_wash()
func _draw_border_fade() -> void:
draw_rect(Rect2(_view_offset, get_rect().size), COLOR_BORDER_FADE)
_draw_deriving_label()
func _draw_pending_wash() -> void:
draw_rect(Rect2(_view_offset, get_rect().size), COLOR_PENDING_WASH)
func _draw_deriving_label() -> void:
var label := "DERIVING TERRAIN…"
var font := ThemeDB.fallback_font
var font_size := 20
var text_size: Vector2 = font.get_string_size(label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
var viewport: Vector2 = get_rect().size
var center: Vector2 = viewport * 0.5
var baseline: Vector2 = center - text_size * 0.5 + Vector2(0.0, text_size.y * 0.5)
draw_string(
font, baseline, label, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size, COLOR_DERIVING_LABEL
)
func _apply_transform() -> void:
_canvas.position = _view_offset
queue_redraw()
# =============================================================================
# Chrome
# =============================================================================
func _build_screen_header() -> void:
_screen_header = ImplantHeader.new()
_screen_header.position = Vector2(PANEL_MARGIN, 16.0)
_screen_header.custom_minimum_size.x = 320.0
_screen_header.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_screen_header)
if _implant_theme:
_screen_header.apply_implant_theme(_implant_theme)
func _refresh_screen_header() -> void:
if _screen_header == null:
return
var name_label: String = _dict_str(_body, "proper_name", _dict_str(_body, "body_id", ""))
var spacing_km: float = StepCanvasTransport.spacing_for_rung(_held_rung) / 1000.0
var title := "ATLAS — %s" % name_label.to_upper()
var subtitle := "%s · %.3f km/gridunit" % [_held_rung.to_upper(), spacing_km]
_screen_header.set_content(title, subtitle)
func _build_overlay_bar() -> void:
var BarScript := load("res://ui/implant/apps/atlas/atlas_overlay_bar.gd")
_overlay_bar = BarScript.new(self)
_overlay_bar.name = "OverlayBar"
add_child(_overlay_bar)
_position_overlay_bar()
func _position_overlay_bar() -> void:
var sz: Vector2 = get_rect().size
if sz == Vector2.ZERO:
sz = Vector2(1280.0, 720.0)
var avail_w: float = maxf(sz.x - OVERLAY_BAR_HEADER_RESERVE - PANEL_MARGIN * 2.0, 200.0)
_overlay_bar.position = Vector2(sz.x - avail_w - PANEL_MARGIN, PANEL_MARGIN)
_overlay_bar.size = Vector2(avail_w, 0.0)
func _build_legend_panel() -> void:
var LegendScript := load("res://ui/implant/apps/atlas/step_canvas/step_canvas_legend.gd")
_legend_panel = LegendScript.new(self)
_legend_panel.name = "Legend"
_legend_panel.theme_resource = _implant_theme
add_child(_legend_panel)
_legend_panel.refresh()
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED:
if _overlay_bar:
_position_overlay_bar()
if _legend_panel:
_legend_panel.reposition()
elif what == NOTIFICATION_APPLICATION_FOCUS_OUT:
_app_has_focus = false
elif what == NOTIFICATION_APPLICATION_FOCUS_IN:
_app_has_focus = true
# =============================================================================
# Input
# =============================================================================
func _is_over_ui(_pos: Vector2) -> bool:
return false
func _gui_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.is_echo():
_handle_key(event as InputEventKey)
return
if (
event is InputEventMouseButton
and _is_over_ui((event as InputEventMouseButton).global_position)
):
return
if event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
_scroll_rung(1, mb.position)
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
_scroll_rung(-1, mb.position)
elif event is InputEventMouseMotion:
_last_mouse_pos = (event as InputEventMouseMotion).position
func _handle_key(event: InputEventKey) -> void:
if event.keycode == KEY_ESCAPE:
back_pressed.emit()
func _process(delta: float) -> void:
if not visible:
return
var direction: Vector2 = StepCanvasTransport.held_pan_direction()
if _is_cursor_edge_scrolling():
direction += _edge_scroll_direction()
if direction == Vector2.ZERO:
return
_apply_pan_delta(direction, delta)
func _apply_pan_delta(direction: Vector2, delta: float) -> void:
var normalized: Vector2 = direction.normalized()
_view_offset -= normalized * PAN_SPEED_PX_S * delta
_apply_transform()
_maybe_refloat()
func _is_cursor_edge_scrolling() -> bool:
if not _app_has_focus or _is_over_ui(_last_mouse_pos):
return false
var viewport: Vector2 = size
if viewport.x <= 0.0 or viewport.y <= 0.0:
return false
return (
_last_mouse_pos.x >= 0.0
and _last_mouse_pos.y >= 0.0
and _last_mouse_pos.x <= viewport.x
and _last_mouse_pos.y <= viewport.y
and (
_last_mouse_pos.x < EDGE_SCROLL_MARGIN_PX
or _last_mouse_pos.y < EDGE_SCROLL_MARGIN_PX
or _last_mouse_pos.x > viewport.x - EDGE_SCROLL_MARGIN_PX
or _last_mouse_pos.y > viewport.y - EDGE_SCROLL_MARGIN_PX
)
)
func _edge_scroll_direction() -> Vector2:
var viewport: Vector2 = size
var direction := Vector2.ZERO
if _last_mouse_pos.x < EDGE_SCROLL_MARGIN_PX:
direction.x -= 1.0
elif _last_mouse_pos.x > viewport.x - EDGE_SCROLL_MARGIN_PX:
direction.x += 1.0
if _last_mouse_pos.y < EDGE_SCROLL_MARGIN_PX:
direction.y -= 1.0
elif _last_mouse_pos.y > viewport.y - EDGE_SCROLL_MARGIN_PX:
direction.y += 1.0
return direction
static func _dict_str(d: Dictionary, key: String, fallback: String) -> String:
var v: Variant = d.get(key)
if v == null:
return fallback
var s: String = str(v)
if s.is_empty():
return fallback
return s