feat(ui): T-1156 — rivers/basins/attractors re-hosted onto the zoom ladder (nature overlay node)

New AtlasWindowNatureOverlay (Node2D on the viewer canvas, above
terrain): self-connects to the shared atlas_layers_received broadcast
(the established one-signal-N-consumers shape) and consumes the whole-
body layer1 skeleton per Tyre's carrier ruling — no windowed wire
touched. Coordinate chain layer1_pixel_to_world_m -> world_m_to_
district -> canvas-local lives in atlas_window_geometry as pure tested
functions; vertical convention (row 0 = North pole, wy increases
south) verified against the server's own pixel_to_world_m, pinned by
pole tests and revert-verified (sign flip fails them by name).
Araminta's per-rung presentation table implemented exactly: Region
full skeleton (radii 0.9/1.4/2.2, confluence 3.5, mouth double-ring),
District trunk-only 1.6px at 60% with mouths at full landmark styling,
Quarter off until T-1170 course invention; retired palette reused
verbatim; RVR on / BAS off / ATR off defaults; missing river_class
falls back to trunk. 58 new tests (34 geometry, 22 lifecycle, 2 real-
driver draw smoke actually rendered); 11 existing atlas suites
regression-free. atlas_window_viewer.gd runs 16 lines past the
advisory 1000-line cap on trivial wiring — accepted, rides T-1158's
decomposition.

Tickets: T-1156

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 08:23:59 +02:00
co-authored by Claude Fable 5
parent ff98a10d42
commit 0d22e50e66
6 changed files with 1214 additions and 0 deletions
@@ -0,0 +1,220 @@
## T-1156 wave 1: pure-function tests for AtlasWindowGeometry's Layer-1
## nature-overlay pixel mapping (layer1_pixel_to_world_m/world_m_to_district/
## layer1_pixel_to_canvas_local) and per-rung visibility/filter policy
## (river_class_visible_at_rung/confluences_visible_at_rung/
## mouths_visible_at_rung/basins_visible_at_rung/attractors_visible_at_rung).
## 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.
class_name TestAtlasWindowGeometryNature
extends GdUnitTestSuite
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.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 = AtlasWindowGeometry.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 = AtlasWindowGeometry.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 = AtlasWindowGeometry.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 = AtlasWindowGeometry.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 = AtlasWindowGeometry.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 = AtlasWindowGeometry.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 = AtlasWindowGeometry.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 = AtlasWindowGeometry.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
# 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 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 = AtlasWindowGeometry.layer1_pixel_to_canvas_local(
row, col, grid_w, grid_h, radius_km, held_center, held_n, CELL_PIXEL_SIZE
)
var world_m: Vector2 = AtlasWindowGeometry.layer1_pixel_to_world_m(
row, col, grid_w, grid_h, radius_km
)
var district: Vector2 = AtlasWindowGeometry.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)
# =============================================================================
# Per-rung river-class visibility (Araminta's ruling, 2026-07-23) —
# river_class_visible_at_rung()
# =============================================================================
func test_river_class_visible_at_rung_region_shows_every_class() -> void:
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_STREAM, "Region")
).is_true()
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(
AtlasWindowGeometry.RIVER_CLASS_TRIBUTARY, "Region"
)
).is_true()
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_TRUNK, "Region")
).is_true()
func test_river_class_visible_at_rung_district_shows_trunk_only() -> void:
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(
AtlasWindowGeometry.RIVER_CLASS_STREAM, "District"
)
).is_false()
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(
AtlasWindowGeometry.RIVER_CLASS_TRIBUTARY, "District"
)
).is_false()
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_TRUNK, "District")
).is_true()
func test_river_class_visible_at_rung_quarter_shows_nothing() -> void:
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_STREAM, "Quarter")
).is_false()
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(
AtlasWindowGeometry.RIVER_CLASS_TRIBUTARY, "Quarter"
)
).is_false()
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_TRUNK, "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_river_class_visible_at_rung_unknown_tag_falls_back_to_region() -> void:
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_STREAM, "Bogus")
).is_true()
# =============================================================================
# Feature-group per-rung gates — confluences/mouths/basins/attractors.
# =============================================================================
func test_confluences_visible_at_rung_region_true_others_false() -> void:
assert_bool(AtlasWindowGeometry.confluences_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometry.confluences_visible_at_rung("District")).is_false()
assert_bool(AtlasWindowGeometry.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(AtlasWindowGeometry.mouths_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometry.mouths_visible_at_rung("District")).is_true()
assert_bool(AtlasWindowGeometry.mouths_visible_at_rung("Quarter")).is_false()
func test_basins_visible_at_rung_region_only() -> void:
assert_bool(AtlasWindowGeometry.basins_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometry.basins_visible_at_rung("District")).is_false()
assert_bool(AtlasWindowGeometry.basins_visible_at_rung("Quarter")).is_false()
func test_attractors_visible_at_rung_region_only() -> void:
assert_bool(AtlasWindowGeometry.attractors_visible_at_rung("Region")).is_true()
assert_bool(AtlasWindowGeometry.attractors_visible_at_rung("District")).is_false()
assert_bool(AtlasWindowGeometry.attractors_visible_at_rung("Quarter")).is_false()
@@ -0,0 +1,212 @@
## 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()/is_overlay_visible(), the same
## duck-typed-viewer precedent test_atlas_window_overlay.gd's _ViewerStub
## already establishes for AtlasWindowOverlay.
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 overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": false, "gen_attractors": false}
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 is_overlay_visible(overlay_id: String) -> bool:
return bool(overlay_visibility.get(overlay_id, false))
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()
@@ -0,0 +1,213 @@
## 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
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).
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 overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": true, "gen_attractors": true}
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 is_overlay_visible(overlay_id: String) -> bool:
return bool(overlay_visibility.get(overlay_id, false))
## 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())
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())
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)
@@ -108,6 +108,88 @@ const MAX_COVERAGE_M: Dictionary = {
const RUNGS_FINEST_FIRST: Array = ["Quarter", "District", "Region"]
# =============================================================================
# T-1156 wave 1: 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). The
# ladder INVERTS: Region/orbital shows the FULL skeleton (the rung whose data
# density actually supports a "river system" read at 76 km/dot spacing);
# District and Quarter fade the read DOWN, not up, because a single-heightmap-
# pixel river course has nothing finer to reveal as the player descends until
# T-1170 invents real sub-heightmap courses. This table is the one place that
# posture lives — revisit here, and only here, when T-1170 lands. (Consts only
# — the READER functions that consult these tables live further down, grouped
# with the other layer1_* pixel-mapping functions per class-definitions-order.)
# =============================================================================
## 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
## Per-rung river-class visibility — which RIVER_CLASS_* ids draw at all, at
## each granularity_v2 tag. Region shows every class (the full skeleton);
## District shows trunk only; Quarter shows none (rivers off entirely at that
## rung per the ruling).
const RIVER_CLASS_VISIBLE_BY_RUNG: Dictionary = {
"Region": [RIVER_CLASS_STREAM, RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
"District": [RIVER_CLASS_TRUNK],
"Quarter": [],
}
## 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
## (60%) — 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). Quarter has no entry — rivers don't draw
## there at all, so no radius/opacity is ever looked up for that rung.
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 = 1.6
const RIVER_DOT_OPACITY_DISTRICT_TRUNK: float = 0.6
## 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.
const MOUTH_RING_RADIUS: float = 5.0
const MOUTH_HALO_RADIUS: float = 8.0
const MOUTH_HALO_ALPHA: float = 0.30
## 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
## 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
@@ -686,3 +768,130 @@ static func screen_header_content(
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-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 above. 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 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, this file's
## existing name for it — NOT a re-derivation).
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 the EXISTING 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 (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 district_to_canvas_local(district, held_center, held_n, cell_pixel_size)
# =============================================================================
# T-1156 wave 1: per-rung nature-overlay visibility policy READERS. The policy
# TABLES themselves (RIVER_CLASS_VISIBLE_BY_RUNG etc.) live up in the
# top-of-file const block per class-definitions-order (Araminta's ruling,
# 2026-07-23, is documented there).
# =============================================================================
## Whether a river cell of `river_class` should draw 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 river_class_visible_at_rung(river_class: int, granularity_v2: String) -> bool:
var visible: Array = RIVER_CLASS_VISIBLE_BY_RUNG.get(
granularity_v2, RIVER_CLASS_VISIBLE_BY_RUNG["Region"]
)
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))
@@ -0,0 +1,344 @@
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.
##
## 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 (dot-scatter rivers, polygon basins, glyph-free
## double-ring mouths, draw order basins-under-rivers-under-attractors); 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
## (AtlasWindowGeometry.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()
## (called from the viewer's own enter()/_enter_at_rung()/_enter_tile_mode() —
## one line each), 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")
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.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
func _draw() -> void:
if viewer == null or _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,
}
if AtlasWindowGeometry.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 AtlasWindowGeometry.attractors_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
"gen_attractors"
):
_draw_attractors(ctx)
## 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))
## 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.
func _pos(row: float, col: float, ctx: Dictionary) -> Vector2:
var world_m: Vector2 = AtlasWindowGeometry.layer1_pixel_to_world_m(
row, col, ctx["grid_w"], ctx["grid_h"], ctx["radius_km"]
)
var district: Vector2 = AtlasWindowGeometry.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 AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
func _draw_rivers(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 is_region: bool = granularity_v2 == "Region"
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 AtlasWindowGeometry.RIVER_CLASS_FALLBACK
)
if not AtlasWindowGeometry.river_class_visible_at_rung(cls, granularity_v2):
continue
var p: Vector2 = _pos(float(c[0]), float(c[1]), ctx)
if is_region:
var radius: float = AtlasWindowGeometry.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2)
draw_circle(p, radius, COLOR_GEN_RIVER)
else:
# District: trunk-only (already filtered above), reduced size +
# opacity — the ruling's "fade down" treatment.
var faded := Color(
COLOR_GEN_RIVER.r,
COLOR_GEN_RIVER.g,
COLOR_GEN_RIVER.b,
COLOR_GEN_RIVER.a * AtlasWindowGeometry.RIVER_DOT_OPACITY_DISTRICT_TRUNK
)
draw_circle(p, AtlasWindowGeometry.RIVER_DOT_RADIUS_DISTRICT_TRUNK, faded)
if AtlasWindowGeometry.confluences_visible_at_rung(granularity_v2):
for cf: Variant in rn.get("confluences", []):
if cf is Array and cf.size() >= 2:
var p: Vector2 = _pos(float(cf[0]), float(cf[1]), ctx)
draw_circle(p, AtlasWindowGeometry.RIVER_CONFLUENCE_RADIUS_REGION, COLOR_GEN_RIVER)
if AtlasWindowGeometry.mouths_visible_at_rung(granularity_v2):
for m: Variant in rn.get("mouths", []):
if m is Array and m.size() >= 2:
_draw_mouth(_pos(float(m[0]), float(m[1]), 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).
func _draw_mouth(p: Vector2) -> void:
draw_arc(p, AtlasWindowGeometry.MOUTH_RING_RADIUS, 0.0, TAU, 18, COLOR_GEN_MOUTH, 1.5)
var halo := Color(
COLOR_GEN_MOUTH.r, COLOR_GEN_MOUTH.g, COLOR_GEN_MOUTH.b, AtlasWindowGeometry.MOUTH_HALO_ALPHA
)
draw_arc(p, AtlasWindowGeometry.MOUTH_HALO_RADIUS, 0.0, TAU, 22, halo, 1.0)
## 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.
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, 0.8, 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().
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 < AtlasWindowGeometry.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 = 5.0 + strength * 4.0
var color: Color = AtlasOverlayColors.sub_biome_color(str(a.get("sub_biome", "")))
_draw_attractor_shape(str(a.get("attractor_type", "")), p, size, color)
## Attractor type -> marker shape — verbatim from the retired
## atlas_marker_overlay.gd _draw_attractor_shape(), ported unchanged.
func _draw_attractor_shape(atype: String, pos: Vector2, size: float, color: Color) -> 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, 1.0)
"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)
"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, 1.0)
_:
draw_circle(pos, size * 0.6, color)
@@ -95,6 +95,8 @@ const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_g
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
# T-1153: orbital rest-state mosaic orchestration.
const AtlasWindowTileSet := preload("res://ui/implant/apps/atlas/atlas_window_tile_set.gd")
# T-1156 wave 1: whole-body nature overlay (rivers/basins/attractors).
const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd")
# ── Overlay definitions (T-1138 — reuses atlas_overlay_bar.gd/
# atlas_legend_panel.gd's existing duck-typed viewer interface: both call
@@ -121,6 +123,10 @@ const OVERLAY_DEFS: Array = [
"group": "toggle",
"tooltip": "Vegetation — green-family ramp. Marine reads transparent (open water)."
},
# T-1156 wave 1: per-rung visibility lives in AtlasWindowGeometry's tables.
{"id": "gen_rivers", "label": "RVR", "group": "toggle", "tooltip": "Rivers — per-rung class filter."},
{"id": "gen_basins", "label": "BAS", "group": "toggle", "tooltip": "Drainage basins — Region only."},
{"id": "gen_attractors", "label": "ATR", "group": "toggle", "tooltip": "Attractors — Region only."},
]
# ── Context (set by enter()) ──────────────────────────────────────────────
@@ -174,6 +180,7 @@ var _overlay_bar = null
var _legend_panel = null
var _window_request = null # AtlasWindowRequest
var _tile_set = null # AtlasWindowTileSet (T-1153, live round 3)
var _nature_overlay = null # AtlasWindowNatureOverlay (T-1156 wave 1)
## T-1153 (design doc §4): true while showing the orbital rest state as a
## MULTI-WINDOW MOSAIC instead of the single held composite (`_window`). Set
@@ -193,6 +200,7 @@ func _ready() -> void:
for def: Dictionary in OVERLAY_DEFS:
_overlay_visibility[def["id"]] = false
_overlay_visibility["gen_rivers"] = true # T-1156 wave 1: RVR default ON
_canvas = Node2D.new()
_canvas.name = "WindowCanvas"
@@ -203,6 +211,10 @@ func _ready() -> void:
_overlay_node.viewer = self
_canvas.add_child(_overlay_node)
_nature_overlay = AtlasWindowNatureOverlay.new(self)
_nature_overlay.name = "WindowNatureOverlay"
_canvas.add_child(_nature_overlay)
_window_request = AtlasWindowRequest.new(self)
_window_request.name = "WindowRequest"
add_child(_window_request)
@@ -291,6 +303,7 @@ func _enter_tile_mode(body: Dictionary, system: Dictionary, radius_km: float) ->
_fit_and_center()
_window_request.reset()
_tile_set.enter(_dict_str(_body, "body_id", ""), radius_km)
_nature_overlay.request_layer1(_dict_str(_body, "body_id", ""))
_refresh_screen_header()
_legend_panel.refresh()
grab_focus()
@@ -326,6 +339,7 @@ func _enter_at_rung(
_window_request.request_now(
_dict_str(_body, "body_id", ""), _held_center, clamped_n, granularity_v2
)
_nature_overlay.request_layer1(_dict_str(_body, "body_id", ""))
_refresh_screen_header()
_legend_panel.refresh()
grab_focus()
@@ -432,6 +446,7 @@ func set_overlay_visible(overlay_id: String, visible_state: bool) -> void:
return
_overlay_visibility[overlay_id] = visible_state
_overlay_node.queue_redraw()
_nature_overlay.queue_redraw()
_legend_panel.refresh()
@@ -515,6 +530,7 @@ func _apply_transform() -> void:
_canvas.scale = Vector2(_view_zoom, _view_zoom)
queue_redraw()
_overlay_node.queue_redraw()
_nature_overlay.queue_redraw()
## Cursor-anchored zoom (D-013): the CANVAS POINT under the cursor stays