Merge remote-tracking branch 'origin/river-courses'

This commit is contained in:
2026-07-23 16:08:14 +02:00
32 changed files with 10295 additions and 973 deletions
@@ -929,3 +929,51 @@ func test_recompute_offset_for_held_n_change_differs_for_different_held_n() -> v
+ " _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)
)
+677 -118
View File
@@ -1,15 +1,26 @@
## 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.
## 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 AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
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
@@ -25,7 +36,7 @@ const DISTRICT_M: float = 2048.0
## 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)
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)
@@ -36,7 +47,9 @@ func test_layer1_pixel_to_world_m_col_zero_is_world_x_zero() -> void:
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 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)
@@ -46,7 +59,7 @@ func test_layer1_pixel_to_world_m_row_zero_is_north_pole_negative_wy() -> void:
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(
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
@@ -57,7 +70,7 @@ func test_layer1_pixel_to_world_m_last_row_is_south_pole_positive_wy() -> void:
## 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)
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)
@@ -66,7 +79,9 @@ func test_layer1_pixel_to_world_m_mid_row_is_near_equator() -> void:
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 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)
@@ -75,13 +90,13 @@ func test_layer1_pixel_to_world_m_full_width_col_is_full_circumference() -> void
## — 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)
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 = AtlasWindowGeometry.layer1_pixel_to_world_m(1.0, 1.0, 0.0, 0.0, 6371.0)
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)
@@ -92,13 +107,16 @@ func test_layer1_pixel_to_world_m_zero_grid_dims_returns_zero() -> void:
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))
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
# district_to_canvas_local() called manually with the same intermediate value.
# AtlasWindowGeometry.district_to_canvas_local() called manually with the
# same intermediate value.
# =============================================================================
@@ -107,6 +125,7 @@ func test_world_m_to_district_divides_by_district_m() -> void:
## 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
@@ -115,14 +134,14 @@ func test_layer1_pixel_to_canvas_local_matches_manual_composition() -> void:
var row := 40.0
var col := 80.0
var result: Vector2 = AtlasWindowGeometry.layer1_pixel_to_canvas_local(
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 = AtlasWindowGeometry.layer1_pixel_to_world_m(
var world_m: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(
row, col, grid_w, grid_h, radius_km
)
var district: Vector2 = AtlasWindowGeometry.world_m_to_district(world_m)
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
)
@@ -130,94 +149,675 @@ func test_layer1_pixel_to_canvas_local_matches_manual_composition() -> void:
# =============================================================================
# Per-rung river-class visibility (Araminta's ruling, 2026-07-23) —
# river_class_visible_at_rung()
# 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_river_class_visible_at_rung_region_shows_every_class() -> void:
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(
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"
AtlasWindowGeometryNature.skeleton_class_visible_at_rung(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, "Region"
)
).is_true()
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_TRUNK, "Region")
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()
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()
## 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_river_class_visible_at_rung_unknown_tag_falls_back_to_region() -> void:
func test_skeleton_class_visible_at_rung_unknown_tag_falls_back_to_region() -> void:
assert_bool(
AtlasWindowGeometry.river_class_visible_at_rung(AtlasWindowGeometry.RIVER_CLASS_STREAM, "Bogus")
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(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()
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(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()
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(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()
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(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()
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()
# =============================================================================
@@ -234,7 +834,7 @@ func test_attractors_visible_at_rung_region_only() -> void:
## 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(AtlasWindowGeometry.zoom_compensated_size(2.2, 1.0)).is_equal_approx(2.2, 0.0001)
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,
@@ -246,7 +846,9 @@ func test_zoom_compensated_size_at_zoom_one_is_unchanged() -> void:
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 = AtlasWindowGeometry.zoom_compensated_size(screen_space_size, view_zoom)
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
@@ -277,50 +879,7 @@ func test_uncompensated_radius_at_orbital_zoom_would_be_sub_pixel() -> void:
## 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 = AtlasWindowGeometry.zoom_compensated_size(2.2, 0.0)
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()
# =============================================================================
# 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).
# =============================================================================
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)
)
@@ -0,0 +1,252 @@
## 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()
@@ -442,3 +442,138 @@ func test_mouth_position_on_land_is_not_suppressed() -> void:
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()
+140
View File
@@ -233,6 +233,146 @@ func test_set_overlay_visible_gen_basins_flips_gate_and_redraws_nature_overlay()
).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)
# =============================================================================
@@ -22,6 +22,15 @@ extends RefCounted
## 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
@@ -108,93 +117,6 @@ 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
## (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). 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 = 2.0
const RIVER_DOT_OPACITY_DISTRICT_TRUNK: float = 0.8
## 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
@@ -775,155 +697,6 @@ static func centered_label_baseline(viewport_size: Vector2, text_size: Vector2)
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))
## 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) 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-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
@@ -0,0 +1,685 @@
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)
@@ -6,16 +6,37 @@ extends Node2D
## 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 (dot-scatter rivers, polygon basins, glyph-free
## double-ring mouths, draw order basins-under-rivers-under-attractors); the
## 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
## (AtlasWindowGeometry.layer1_pixel_to_canvas_local()), the same frame every
## (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.
##
@@ -59,6 +80,9 @@ extends Node2D
## 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.
@@ -142,8 +166,24 @@ 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 or _layer1 == null:
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:
@@ -176,22 +216,114 @@ func _draw() -> void:
# 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
# AtlasWindowGeometry.zoom_compensated_size()'s own doc.
# AtlasWindowGeometryNature.zoom_compensated_size()'s own doc.
"view_zoom": viewer.get_view_zoom(),
}
if AtlasWindowGeometry.basins_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
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 AtlasWindowGeometry.attractors_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
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
@@ -204,12 +336,48 @@ func _cols_for_wrap(radius_km: float) -> int:
## Zoom-compensated screen-space size — thin per-ctx wrapper over
## AtlasWindowGeometry.zoom_compensated_size() (see that function's own doc
## for the "why divide" rationale). Every draw_circle()/draw_arc()/
## draw_polyline() radius or line-width in this file routes through this so
## Araminta's "constant on-screen size" ruling holds at every rung/zoom.
## 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 AtlasWindowGeometry.zoom_compensated_size(screen_space_size, ctx["view_zoom"])
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
@@ -225,10 +393,10 @@ func _zs(screen_space_size: float, ctx: Dictionary) -> float:
## 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 = AtlasWindowGeometry.layer1_pixel_to_world_m(
var world_m: Vector2 = AtlasWindowGeometryNature.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 district: Vector2 = AtlasWindowGeometryNature.world_m_to_district(world_m)
var cols: int = ctx["cols"]
if cols > 0:
var held_center: Vector2i = ctx["held_center"]
@@ -285,50 +453,30 @@ func _is_drawn_water(district: Vector2, ctx: Dictionary) -> bool:
return AtlasOverlayColors.is_morphology_water(zone)
## T-1172 clip — retire when T-1170 course invention terminates courses at
## the invented coast. 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-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"]
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 district: Vector2 = _district(float(c[0]), float(c[1]), ctx)
# T-1172 clip — retire when T-1170 course invention terminates at the invented coast.
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"]
)
if is_region:
var radius: float = AtlasWindowGeometry.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2)
draw_circle(p, _zs(radius, ctx), 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, _zs(AtlasWindowGeometry.RIVER_DOT_RADIUS_DISTRICT_TRUNK, ctx), faded)
_draw_skeleton_chords(rn, ctx)
if AtlasWindowGeometry.confluences_visible_at_rung(granularity_v2):
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)
@@ -337,10 +485,10 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
var radius: float = _zs(AtlasWindowGeometry.RIVER_CONFLUENCE_RADIUS_REGION, ctx)
var radius: float = _zs(AtlasWindowGeometryNature.RIVER_CONFLUENCE_RADIUS_REGION, ctx)
draw_circle(p, radius, COLOR_GEN_RIVER)
if AtlasWindowGeometry.mouths_visible_at_rung(granularity_v2):
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)
@@ -357,18 +505,161 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
_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).
## 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(AtlasWindowGeometry.MOUTH_RING_RADIUS, ctx), 0.0, TAU, 18, COLOR_GEN_MOUTH, _zs(1.5, ctx)
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, AtlasWindowGeometry.MOUTH_HALO_ALPHA
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
)
draw_arc(p, _zs(AtlasWindowGeometry.MOUTH_HALO_RADIUS, ctx), 0.0, TAU, 22, halo, _zs(1.0, ctx))
## Basins — Region only, binary (no fade), per the ruling. Polygon fill +
@@ -394,7 +685,7 @@ func _draw_basins(ctx: Dictionary) -> void:
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(0.8, ctx), true)
draw_polyline(loop, COLOR_GEN_BASIN_LINE, _zs_stroke(0.8, ctx), true)
## Attractors — Region only, wave 1 (per the ruling; District/Quarter never
@@ -410,7 +701,7 @@ func _draw_attractors(ctx: Dictionary) -> void:
if not a is Dictionary:
continue
var strength: float = float(a.get("strength", 0.0))
if strength < AtlasWindowGeometry.ATTRACTOR_MIN_STRENGTH:
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:
@@ -418,7 +709,7 @@ func _draw_attractors(ctx: Dictionary) -> void:
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(1.0, ctx))
_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
@@ -504,6 +504,7 @@ func _on_window_ready(window: Dictionary) -> void:
_legend_panel.refresh()
queue_redraw()
_overlay_node.queue_redraw()
_nature_overlay.queue_redraw() # T-1170 B3: courses read THIS window too
## T-1153 (design doc §4 "progressive... with visible refinement as tiles
@@ -14,9 +14,21 @@ extends RefCounted
## 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-1172 clip —
## retire when T-1170 course invention terminates courses at the invented
## coast.
## 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")
@@ -156,6 +156,20 @@ Release build, this session, body GJ338Bd, seed "yolo", 16 cores / Rayon pool 14
| Octave-cutoff derive (`min_wavelength_m`-bearing) | — | not measured | **UNBUILT, UNMEASURED** — every planetary-rung and canvas-sampling latency claim resting on "cheaper because fewer octaves" is an unmeasured extrapolation on top of code that doesn't exist yet |
| Full-canvas planetary sample, 1920×1080 (~2.07M cells) | — | ~2.63.0 s single-thread; ~0.20.3 s parallel (claimed) | **ESTIMATED, UNVERIFIED** — depends on both the unbuilt cutoff and the unbenchmarked parallel throughput above; treat as directional only |
> **UNMEASURED gap closed (2026-07-23, T-1170 A2 discipline item 1 — Dudley).** `zoom_ladder_bench.rs`'s three `#[ignore]`d benches (dormant since T-1149/T-1152) were run `--release --ignored` on this session's hardware (16 cores, same class of machine as the §7 table above) before the T-1170 course inventor landed, per the batch's own discipline requirement. Numbers below are **MEASURED**, not extrapolated — this table row is retired as UNBUILT/UNMEASURED for the octave-cutoff case specifically (the cutoff plumbing has existed since T-1162; what was missing was ever actually running the bench):
>
> | Sweep (64×64 = 4,096 cells, release) | Total | ns/cell | µs/cell |
> |---|---|---|---|
> | District spacing (2,048 m), cutoff=0 (uncut) | 12.33 ms | 3,009.4 | 3.009 |
> | District spacing (2,048 m), cutoff=2,048 m | 7.31 ms | 1,784.7 | 1.785 |
> | Quarter spacing (512 m), cutoff=512 m | 7.47 ms | 1,822.9 | 1.823 |
> | Orbital (`derive_orbital_at_metres`), Region spacing (204.8 km) | 5.96 ms | 1,454.2 | 1.454 |
> | Full `derive_at_metres` (invention included) at Region spacing, for comparison | 11.18 ms | 2,729.4 | 2.729 |
> | Orbital speedup vs. full derive at the same (Region) spacing | — | — | **1.88×** |
> | Served Region window at `DISTRICT_WINDOW_MAX_N_REGION` (n=6,400 → 64×64=4,096 cells, `WIRE_CAP_CELLS` capped, real production `build_district_window_layer` path, row-chunked `par_iter`, 16 Rayon threads) | 12.35 ms / 20 calls | — | **0.617 ms/call** |
>
> Extrapolated full-canvas figures (1600×900 @ 1/2 px-per-cell) from the orbital per-cell rate: ~2,094 ms single-thread @1px/cell, ~524 ms @2px/cell — still **EXTRAPOLATED**, not independently measured at full canvas size (that remains a separate, not-yet-run measurement; the per-cell rate itself is now real). The octave cutoff itself is confirmed cheaper (cutoff=2,048 m runs at ~59% of the uncut cost at District spacing) rather than assumed cheaper. This closes the "not load-bearing for T-1170's design, but the gap closes now" item from the T-1170 ruling's Discipline §1 — the course inventor's Stage A/B split does not depend on these numbers (it deliberately avoids per-candidate `derive_at_metres` calls, Ruling 3b), but the gap this table flagged is no longer open.
**Interactive-latency verdict:** district and quarter rungs (capped) are comfortably interactive on a warm `TerrainAnalysis` — 15 ms per response at current cap sizes. The **only** rung with a real latency question is planetary/canvas-fill, and its numbers are two extrapolation-hops from anything actually measured this session. **No implementation should proceed on the planetary rung's cost story without first (a) prototyping the octave cutoff and re-measuring, and (b) chunked-`par_iter`-ing the window loop and re-measuring** — both are cheap to do (the probe binary already exists) and should happen before, not during, the follow-up ticket.
---
+148
View File
@@ -0,0 +1,148 @@
---
title: "River Course Invention — T-1170/T-1168 Design Ruling"
description: Binding design ruling for course invention + riparian vegetation (Tyre, from the three-audit design pass)
type: design
status: binding
round: T-1170
created: 2026-07-23
---
*cracks knuckles* — I've read both tickets in full, the D-226 amendment block (wave-1 carrier note, two-waterline note, T-1137 queue ruling, windowed-family ceiling), D-227, D-243, and the T-1143 zoom-ladder serving model. The audits are code-true and the tension called out in point 1 is real — I created it, and it resolves cleanly once the carrier rule is split on the right axis. Ruling follows.
---
# TYRE — Binding Design Ruling: T-1170 (river course invention) + T-1168 (riparian vegetation)
**Date:** 2026-07-23. **Status:** binding for this batch; implementers code from this directly. Governance capture text in Ruling 6.
**The keystone insight, stated once up front:** the framework tension between my wave-1 carrier note ("discrete geometry rides whole-body") and my two-waterline note ("invention detail is rung-indexed") dissolves when you split on the correct axis. The wave-1 rule was written for the **skeleton** — a fixed, finite, rung-independent graph, computed once, valid forever. Course geometry is not skeleton; it is **invention** — the linear sibling of the coast crinkle, refined per rung by construction. Skeleton rides whole-body; invention rides the window. The rule wasn't wrong, it was under-specified, and this ruling refines it rather than reversing it.
---
## Ruling 1 — Invention locus and wire carrier: server-side, per-window, inside `DistrictWindowLayer`
**1a. Locus: server, per-window, at serve time — Dudley's option (b). Binding.** Course geometry is invented inside the window derive (`GenWorkItem::DeriveWindow`, background queue per T-1137 — queue discipline [HARD], unchanged), at the window's granularity and `min_wavelength_m`. Dudley's numbers close this: +0.090.21 ms against a measured ~5 ms window baseline, under 5% — noise. Whole-body precompute at Quarter detail is **rejected** on payload grounds (~4.4 MB dense-body for geometry 99% never viewed — categorically the wrong shape under D-227 derive-don't-store). Client-side GDScript invention is **rejected** on the determinism surface, not CPU: a byte-exact two-language mirror of `splitmix64`/`value_noise`/octave tables is a standing D-010/D-227 liability the project has deliberately avoided everywhere else, purchased for a compute saving option (b) proves unnecessary.
**1b. Carrier: a new vector field inside the existing windowed payload. The windowed-family ceiling is untouched — and here is why, precisely.** The ceiling ([HARD], D-226 T-1124 §2) counts **windowed-query fields on `AtlasLayerResponse`**. It exists to prevent a second concurrently-in-flight windowed *query* needing per-field request correlation. Course geometry is not a second query: it is **content of the one windowed payload**, keyed by the same `(body, center, n, granularity)` echo, arriving in the same response, stale-discardable by the same rule. `DistrictWindowLayer` gains `courses: Vec<RiverCourse>` (`#[serde(default)]`), exactly as it holds six dense arrays today. No ceiling impact, no tagged-envelope migration, no new request class.
**1c. The carrier rule, refined (this is the governance-grade restatement — capture text in Ruling 6):**
> Three-way, replacing the wave-1 two-way rule: **(i)** rung-independent discrete features — skeletons, graphs, point markers, computed once and valid forever — ride the whole-body overlay family, filtered per-rung client-side. **(ii)** Continuous per-metre fields ride the windowed per-cell arrays. **(iii)** **Rung-indexed invented detail rides the windowed payload regardless of geometric kind** — raster or vector — because rung is a *request parameter*, and only the windowed query carries one. The coast crinkle has always followed (iii) implicitly (it arrives baked into windowed cell verdicts); course polylines are its vector sibling, arriving alongside them.
**1d. The two-waterline problem dissolves under this carrier.** A whole-body course would need a rung-parametrized terminus (the fork the audit flagged as sharpest). A windowed course does not: the server invents the course *at a known rung*, in the same pass that computes that rung's per-cell water verdicts — so the terminus is resolved against the drawn coast **at that rung**, server-side, consistently. Nothing whole-body ever carries a terminus. The skeleton stays rung-independent, as the two-waterline note requires.
**1e. Window-independence invariant (binding, tested).** The geometry is a pure function of `(seed, body, edge, rung)`**never** of the window rect. Stations are generated at deterministic global arc-length positions along the edge; the window *crops*, it never re-parametrizes. Two overlapping windows at the same granularity must produce byte-identical points for the shared stretch. This is the course analog of the coast warp's absolute-world-metre keying and it is a mandatory determinism test (see Discipline).
---
## Ruling 2 — Adjacency: persisted downstream pointer. Reconstruction rejected.
**2a.** `RiverNetwork` gains `river_downstream: Vec<u8>` — additive, `#[serde(default)]`, parallel to `river_cells` (the exact `river_class` precedent). Captured in `extract_river_network` (`drainage.rs:247-349`) where `fdir[i]` is already in scope and currently discarded after its boolean use. One `.map()`, zero new grid passes, ~1 byte/river cell. This is the **opening move** of the batch — everything else consumes it.
**2b. Reconstruction is rejected** on the Si audit's confluence finding: adjacency alone cannot distinguish inflow from outflow at a 3-river-neighbor confluence, and re-deriving direction from elevation is re-running D8 badly. We had the true answer in hand and threw it away; stop throwing it away. The D-203 memory-frugality rationale for discarding covered the 131 KB *full grid*, not the river-cell projection.
**2c. Sentinel vocabulary (values in the u8 space above 7):** `MOUTH` (flow reaches raw-sea) · `EDGE_DRAIN` (flow exits the grid's top/bottom edge — a grid artifact, *not* a mouth, per Ruling 3f) · `TERMINAL`**reserved now, unused in round 1** — flow ends in an interior sink (future endorheic basin / inland delta, see Ruling 7b). Reserving the value costs one enum arm and keeps the future option additive instead of a wire migration.
**2d.** Each edge is uniquely identified by its **upstream cell** (every river cell has exactly one downstream pointer), so `edge_id = pack(u16,u16) → u32`. This is the seeding key (Ruling 3a).
---
## Ruling 3 — Course algorithm
**3a. Seeding (D-227/D-010 discipline).** `SeedChain::for_body(...).derive(SeedDomain::RiverCourse, edge_id as u64)`, new `SeedDomain` variant, with a distinct `RIVER_COURSE_WARP_SALT` on the noise stream (the `COAST_WARP_SALT` pattern verbatim) — never correlated with coast warp, terrain scatter, or vegetation massif fields at the same position. Pure `(seed, body, edge, rung)`.
**3b. Two-stage shape — and the stage split is load-bearing for cross-rung stability:**
- **Stage A — coarse valley-seeking control path. Rung-INDEPENDENT.** Between the two anchor points (upstream/downstream cell centres — skeleton truth, never moved), place control stations at chord/8 (~9.6 km). At each, evaluate k=5 candidate perpendicular offsets within the amplitude envelope; score = bilinear `ta.elev_pct` at the candidate (plus a small continuity penalty against the previous chosen offset to prevent zigzag); pick minimum. **The elevation proxy is the bilinear `TerrainAnalysis` read — the same tradeoff the coast warp already made at `district_profile.rs:1152-1157` — NOT `derive_at_metres` per candidate.** Dudley's numbers make this affordable (+µs/segment); the Si audit's unmeasured-derive-cost gap is thereby not load-bearing for this design (we still close the measurement gap — see Discipline). Stage A is computed identically at every rung, so the coarse course never moves as you refine.
- **Stage B — fine warp octaves. Rung-INDEXED.** Perpendicular scalar displacement of intermediate stations via salted multi-octave value noise keyed on global arc-length, octave band from ~chord/2 (~38 km) down to the window's `min_wavelength_m` hard-truncate (the `warp_fbm` cutoff idiom, T-1162 rung discipline, reused exactly). Station spacing = the rung's sample spacing (2,048 m District / 512 m Quarter — Dudley's ~37/~150 points per segment), at global arc-length multiples so windows agree (Ruling 1e).
**Cross-rung invariant that falls out:** the Quarter course is the District course plus octaves in the (1,024 m..4,096 m) band — displacement between rungs is bounded by the truncated-octave amplitude sum, sub-cell at District spacing. And at Region spacing no octaves survive at all, so **the rung-truncated course degenerates to the straight chord — which means the client's Region-rung skeleton-chord drawing (Ruling 5a) IS the course at that rung**, not an approximation. That's the kind of unification I get excited about: one function, every rung, no special cases.
**3c. Amplitude envelope.** Sine-shaped taper to **zero at both endpoints** — this is what makes confluences work for free: every edge into and out of a cell meets exactly at the cell-centre anchor, C0-continuous, and the confluence marker draws at the anchor. Peak amplitude ≤ ~8% of chord (~6 km), scaled down by local `ta.slope_deg` (steep terrain → straighter) and up modestly by `river_class` (trunks meander wider). All tunables; taper-to-zero and the ≤ half-cell hard cap are binding.
**3d. Valley preference is IN for round 1** (costed at noise level by Dudley), via Stage A only. This is the round-1 answer to the wave-1 "rivers may cross invented hills" gap: courses *prefer* invented-terrain valleys at coarse wavelengths; residual fine-octave conflicts (a course crossing a `voxel_relief` hillock) are **accepted and documented**. The converse mechanism — terrain invention becoming course-aware (valley carving along courses) — is explicitly deferred; it belongs at the block/tile rungs and Phase 5 fill, noted in governance as the future direction, not built now.
**3e. Termination at the invented coast (the T-1172 retirement mechanism).** For an edge whose downstream sentinel is `MOUTH`: walk stations upstream→downstream, sampling the **same rung-consistent morphology water verdict the window's own cells use** (the `open_water`/morphology path, at the window's `min_wavelength_m` — not raw `ocean_frac`, so the terminus agrees with what is drawn). First water station → bisect against the previous land station (fixed 6 iterations) → terminus point, flagged `Mouth` on the wire. If the final anchor still samples land at this rung (drawn coast receded past the raw-sea cell — the inland-mouth oddity, fixed by construction here), extend along the D8 direction up to one cell length probing; if still no water, terminate at the anchor with **no** mouth flag (degenerate, rare, bounded). A few dozen probes per mouth edge, on the background queue — within the T-1137 discipline by construction.
**3f. Pole-edge drains are not mouths. Binding.** `EDGE_DRAIN` termini get no mouth flag and no double-ring; the course ends at the last in-grid station. Server-side, `extract_river_network` stops classifying grid-edge exits into `mouths` (deliberate Layer-1 golden re-pin, see Discipline). They are grid artifacts, exactly as Jeroen's capture question suspected.
**3g. T-1172 clip: restructured, not blanket-deleted.** The three marker sites in `atlas_window_nature_overlay.gd` (lines 288/311/347) retire **for the course-drawing rungs** (District/Quarter) — courses carry real rung-consistent termini, the clip's job is done there, and suppressed offshore mouths return as real geometry. The **Region-rung skeleton path keeps a clip**: Region still draws whole-body skeleton against a rung-dependent drawn coast, which is precisely the presentation-frame reconciliation the two-waterline note prescribes. Rewrite the marker comments: the surviving clip is documented as permanent-until-Region-goes-windowed (T-1143 ruling 2's progressive tiling), at which point Region inherits windowed courses and the last clip retires.
**3h. Wire shape.** `RiverCourse { edge_id: u32, class: u8, points: Vec<(i32,i32)> /* world-metres */, terminus: CourseTerminus /* None | Mouth | EdgeDrain | ContinuesBeyondWindow */ }`. Only edges whose chord-inflated-by-max-amplitude bounding box intersects the window ship; points cropped to window + one station beyond each edge of it. ~12 KB typical per window. Well inside every budget.
---
## Ruling 4 — T-1168: shape and sequencing
**4a. The mechanism: a scale-free point-sample distance test. This dissolves the blotch problem rather than mitigating it.** `near_perennial_water(sample_pos) = distance(sample_pos, nearest invented course polyline) ≤ band_m`, where `band_m` defaults to D-239 §8's governed 13 tiles (class-scaled: thicket band for trunks, scrub band for streams — tunable constants, governed default). Because the test is a pure point predicate against real course geometry, it is **automatically correct at every sampling density**: at 512 m Quarter spacing it essentially never fires (the band is sub-cell — honest, no over-fattening, the exact Lendel-failure discipline), and at 1 m tile spacing it fires on exactly the 13 tile bank strip. No per-rung riparian policy exists or is needed. The audit's skeleton-disc model — any "within N km of a 76 km-spaced skeleton point" test — is **rejected outright**: it is off by 34 orders of magnitude from the governed band and cannot be tuned into anything but green blotches.
**4b. Consequently the 512×256 distance-field-over-grid proposal is also rejected** — a grid quantized at 76 km cannot express a metres-scale band. The test runs against course polylines resident in the derive context: in the window path, T-1170's already-invented courses (coarse bounding-box cull, then point-to-segment distance — most cells cull to zero edges); in the batch path, courses for edges near the district, invented on demand via the same pure function (cachable per derive batch). The Si audit's observation that `gen_queue.rs:626` discards the `Layer1Output` **still gets fixed** — the window path needs `RiverNetwork` (cells + downstream pointers) resident to know which edges exist; retain it in `TerrainAnalysisCache` instead of re-deriving-and-discarding.
**4c. Hard sequencing: T-1168's riparian test lands AFTER T-1170's course inventor, full stop.** The test is meaningless without course geometry (4a). The **threading scaffolding is exempt** and proceeds in parallel — it is mechanical and fully mapped by the audit: `build_district_profile` gains the riparian signal parameter consumed at `district_profile.rs:1469`; threaded via `derive_all_districts``derive_district_profile` (batch, sourced from `cascade.rs`'s already-unpacked `river_network`, the `road_graph` precedent) and `build_district_window_layer``derive_window_cell``derive_at_metres`/`derive_orbital_at_metres` (window, sourced from the retained `Layer1Output`). Until the course inventor lands, the provider returns "no course in range" — wired but inert, zero behavioral change, zero golden churn.
**4d. T-1162 composition contract, re-affirmed as binding:** `near_perennial_water` is a separate boolean/geometry signal into `derive_vegetation`; it never touches `moisture_q`, and `moisture_perturb_q` never reads river data. The existing parameter separation already enforces this — the implementer's only job is not to break it. Climate gating stands: extreme-cold Barren beats riparian (ice is geology per D-227); `Marine`/airless precedence unchanged.
**4e. Experience honesty (what Jeroen will see):** after this batch, District/Quarter windows show continuous meandering rivers with real coast mouths. They will **not** show visible green riparian corridors — under the governed 13 tile band that is sub-cell at every Atlas rung, and honestly so. T-1168's visible payoff arrives at the block/tile rungs (opened by T-1143 ruling 1) and Phase 5 ground truth. If Atlas-visible green corridors are *wanted*, that is a design widening of D-239 §8 — flagged to Jeroen in Ruling 7a, one constant away, not an architecture change.
---
## Ruling 5 — Per-rung presentation surface
**5a. Region and coarser (skeleton path, whole-body `layer1`):** the client upgrades dots to **connected straight chords** via `river_downstream` — which per Ruling 3b *is* the rung-truncated course, so this is not an approximation, it is the course at Region truncation. Clip retained (Ruling 3g). Confluence/mouth markers from the skeleton lists (mouths now excluding pole drains).
**5b. District and Quarter (course path, windowed):** polylines from `DistrictWindowLayer.courses` via the existing `_pos()`/`draw_polyline` plumbing (`_draw_basins` precedent — no new transform code). Mouth markers at `Mouth` termini. No clip. **Quarter rivers return** — the streams class draws at Quarter, executing exactly the pre-announced fade-down revisit from the wave-1 visibility note.
**5c. The revisit point is preserved and split:** `RIVER_CLASS_VISIBLE_BY_RUNG` is replaced by two tables in `atlas_window_geometry.gd``SKELETON_CLASS_VISIBLE_BY_RUNG` (Region+, path 5a) and `COURSE_CLASS_VISIBLE_BY_RUNG` (District/Quarter, path 5b), with per-class width/opacity companion tables. **This is where Araminta's forthcoming presentation ruling plugs in** — implementers land functional defaults (trunk widest, streams thinnest, class-filtered per rung: District trunk+tributary, Quarter all three), and Araminta's ruling edits those tables and only those tables. Same single-revisit-point discipline as wave 1.
---
## Ruling 6 — Governance captures (exact text)
Append to the D-226 amendment block (`governance/decisions/architecture.md`), dated 2026-07-23:
> **Course-invention carrier note (T-1170, 2026-07-23 — Tyre):** river course geometry below D8 resolution is **invention, not skeleton**, and rides the **windowed payload** as a vector field inside `DistrictWindowLayer` (`courses`, additive) — invented server-side per window on the background queue (T-1137 discipline) at the window's rung, terminated server-side against the same rung's morphology water verdict the window's cells carry. This does **not** count against the windowed-family ceiling (§2 [HARD]): the ceiling counts windowed *query* fields on `AtlasLayerResponse`; courses are content of the single existing windowed payload, same echo key, same staleness semantics. The wave-1 carrier rule is hereby refined three-way: **(i)** rung-independent discrete features (skeletons, graphs, markers — computed once, valid forever) ride the whole-body family, filtered per-rung client-side; **(ii)** continuous per-metre fields ride the windowed per-cell arrays; **(iii)** rung-indexed invented detail rides the windowed payload regardless of geometric kind, because rung is a request parameter and only the windowed query carries one — the coast crinkle has always implicitly been (iii); courses are its vector sibling. The two-waterline note's reconciliation resolves per-rung server-side for courses (the terminus is windowed content at a known rung); the Region-rung skeleton path retains the presentation-frame clip until Region itself goes windowed (T-1143 ruling 2), which retires the last clip site. The skeleton itself gains `river_downstream` (additive per-river-cell D8 pointer with `MOUTH`/`EDGE_DRAIN`/reserved-`TERMINAL` sentinels) on `RiverNetwork` — whole-body, rung-independent, per rule (i). Pole-edge drains are grid artifacts, not mouths — excluded from `mouths` at extraction.
Append to D-227's amendment trail:
> **Amended 2026-07-23 (T-1170 — river courses join the invention family):** the "invented deterministically" clause now covers linear features: river courses between D8 cells are pure `(seed, body, edge, rung)` functions — a rung-independent valley-seeking coarse path (bilinear `TerrainAnalysis` proxy, never full re-derivation) plus rung-indexed perpendicular warp octaves under the `min_wavelength_m` truncation discipline, distinct salted stream, amplitude tapered to zero at cell-centre anchors (confluence continuity), stations at global arc-length positions (window-independent). Round-1 relief reconciliation is course-follows-terrain (valley preference); terrain-carves-for-course is the deferred converse, targeted at block/tile rungs.
Also: fix the stale `scale.rs:107` grid-shape comment (one line, audit nit) in whichever PR touches that file first.
---
## Ruling 7 — Jeroen items
**7a. Riparian visibility at Atlas rungs (genuine design call).** Under D-239 §8's governed 13 tile band, riparian vegetation is honestly invisible at every Atlas rung (Ruling 4e) — the drawn river itself is the only visible river response at District/Quarter. If you want the Nile effect — a visible green corridor at Atlas scale — that is a *design widening* of the band (e.g. floodplain-riparian corridors of hundreds of metres on alluvial-plain morphology, class- and morphology-scaled). The mechanism ships scale-free, so this is one governed constant/function away, zero architecture change. Your call, not blocking this batch.
**7b. Endorheic basins (one paragraph, as asked).** This design does not foreclose deliberate endorheic basins or inland deltas — it deliberately cheapens the future path. The `TERMINAL` downstream sentinel is reserved now (Ruling 2c), so an interior sink becomes an additive drainage-extraction change, not a wire migration; and the termination mechanism (walk stations to the first rung-consistent *water* verdict — which is the morphology verdict, covering lakes, not just ocean) generalizes unchanged to a terminal lake shoreline. When you want endorheic basins, the work is in D8 sink retention and lake morphology, and every piece this batch builds — pointers, courses, termination, riparian — consumes them without modification. Nothing to design today.
---
## Discipline — benches, goldens, determinism tests
1. **Run the dormant bench first.** `zoom_ladder_bench.rs` `--release --ignored`, before the course inventor merges; record the numbers in `docs/architecture/atlas-zoom-ladder-t1143.md` (which itself flags them UNMEASURED). Cheap, closes a documented gap this batch would otherwise inherit blind. Not load-bearing for the design (Ruling 3b avoided depending on it) but the gap closes now.
2. **Course-cost bench:** window derive with courses on/off; assert the delta stays under ~5% of the window baseline (Dudley's envelope).
3. **Determinism tests (mandatory, new):** (a) overlapping-windows agreement — two windows sharing an edge produce byte-identical course stations in the overlap (Ruling 1e); (b) cross-rung stability — Quarter course points within the truncated-octave amplitude bound of the District polyline (Ruling 3b); (c) salt isolation — course stream uncorrelated with coast warp at shared positions (the existing isolation-test pattern).
4. **Goldens, deliberate re-pin discipline** (`UPDATE_GOLDEN=1` + rationale comment, the T-1162 precedent): `cascade_golden` re-pins (mouths list loses pole drains; `river_downstream` added). `window_derivation_golden`: first **verify empirically** whether the 128×64 fixture produces river cells near the sweep positions (audit: unconfirmed); add a course-bearing sweep position either way so courses are actually pinned. `believability`: expected unchanged (the point test cannot fire at district spacing) — **verify, don't assume**; an unexpected re-pin there is a bug signal, not a regen chore.
5. **Full `cargo test` before push** (the golden harnesses are separate test binaries; `--lib` misses them), and the live-screenshot pixel-diff eyeball check on the merged result, cold-launched — this batch is rendered-path.
---
## Work split and sequencing
**Dudley — server (owns the cost model he probed):**
- **A1** (opening move, unblocks everything): `river_downstream` + sentinels + pole-drain mouth exclusion + Layer-1 golden re-pin.
- **A2**: course inventor (Ruling 3a3d, 3h) in the window derive + `Layer1Output` retention in `TerrainAnalysisCache` + wire field.
- **A3**: termination + mouth flags (3e3f) — with A2.
- **A5** (after A2): T-1168 riparian point test wired into the threaded signal, batch + window paths.
**Stig — client + mechanical threading:**
- **B1** (parallel with A2): T-1168 threading scaffolding (Ruling 4c), inert provider, zero behavior change.
- **B2** (needs A1 only): Region skeleton-chord drawing.
- **B3** (needs A2/A3): course polyline drawing, mouth markers, clip restructure (3g), visibility-table split (5c) with functional defaults for Araminta.
**Hard edges:** A1 → everything. A2 → {A3, A5, B3}. B1 ∥ A2, converging at A5. Nothing else serializes.
**Difficulty tier, honestly: challenging but doable — and the architecture is on our side.** Every primitive exists (`value_noise`, `splitmix64`, the cutoff idiom, the queue, the polyline draw path); this batch composes them, it doesn't invent machinery. The one place I'd have called "extremely difficult" — reconciling a whole-body course with a rung-indexed coastline — stopped being a problem the moment courses went windowed. That's the ruling doing the work.
— Tyre
File diff suppressed because one or more lines are too long
+79
View File
@@ -54,8 +54,87 @@ pub struct RiverNetwork {
/// empty, never a decode error (the additive T-1124 §1 pattern).
#[serde(default)]
pub river_class: Vec<u8>,
/// Per-`river_cells`-entry D8 downstream pointer (T-1170 Ruling 2a) — same
/// index, same length as `river_cells`/`river_class`. Each river cell has
/// exactly one downstream direction, so this is the exact same shape as
/// `river_class`, captured in `extract_river_network` where `fdir[i]` is
/// already in scope (one `.map()`, no new grid pass).
///
/// **Values 07:** an index into `drainage::D8` — the downstream neighbor
/// direction, i.e. "this river cell flows toward `D8[value]`".
///
/// **Sentinel values above 7 (Ruling 2c):**
/// - [`RIVER_DOWNSTREAM_MOUTH`] — flow reaches a raw-sea cell (the river's
/// mouth in the D8 sense; T-1170's course inventor walks stations from
/// here to the invented-coast terminus, Ruling 3e).
/// - [`RIVER_DOWNSTREAM_EDGE_DRAIN`] — flow exits the grid's top/bottom
/// edge. This is a **grid artifact, not a mouth** (Ruling 3f) — pole-edge
/// exits are deliberately excluded from `mouths` at extraction (see
/// `extract_river_network`'s doc).
/// - [`RIVER_DOWNSTREAM_TERMINAL`] — **reserved, unused in round 1.** Flow
/// ends in an interior sink (future endorheic basin / inland delta,
/// Ruling 7b). Reserving the value now keeps that future path additive
/// (no wire migration) rather than requiring a new sentinel later.
///
/// Why persist rather than reconstruct from adjacency alone (Ruling 2b):
/// at a 3-river-neighbor confluence, adjacency cannot distinguish inflow
/// from outflow, and re-deriving direction from elevation is re-running D8
/// badly — the true answer (`fdir[i]`) is already computed and in scope at
/// extraction time; discarding it and re-deriving later is strictly worse
/// than keeping the ~1 byte/river-cell projection. The former D-203
/// memory-frugality rationale for discarding covered the 131 KB *full*
/// `fdir` grid, not this per-river-cell projection.
///
/// `#[serde(default)]` — same additive pattern as `river_class`: an absent
/// array (pre-T-1170 payload/fixture) decodes to empty, never an error.
#[serde(default)]
pub river_downstream: Vec<u8>,
/// Per-`river_cells`-entry seaward neighbor position (T-1170 PR #197
/// review, Hoshe #1) — same index/length shape as `river_class`/
/// `river_downstream`. **Only meaningful where `river_downstream[i] ==
/// RIVER_DOWNSTREAM_MOUTH`**; every other entry (interior direction or
/// `EDGE_DRAIN`) carries the placeholder `(0, 0)` and must not be read.
///
/// **Why this exists — the second instance of the Ruling-2b anti-pattern.**
/// `extract_river_network` already computes the real sub-sea-level
/// neighbor `(nr, nc)` to decide the `MOUTH` sentinel (the elevation
/// check that flips `river_downstream[i]` to
/// [`RIVER_DOWNSTREAM_MOUTH`]) — the exact same "true answer already in
/// scope, then discarded" shape Ruling 2b fixed for interior D8 pointers
/// via `river_downstream` itself. Discarding `(nr, nc)` a second time
/// left `river_course::build_edges` with nothing to build a real chord
/// from for Mouth edges: it filled `downstream = upstream` as a
/// placeholder, which zeroes the chord (`chord_m < 1.0`), which trips
/// `invent_course`'s single-point degenerate return, which makes
/// `resolve_mouth_terminus`'s station walk a no-op (a 1-point course
/// never enters the `pts.len() >= 2` probe branch either) — every Mouth
/// edge silently resolved `CourseTerminus::None` instead of `Mouth`.
/// Capturing `(nr, nc)` here (one more push per mouth cell — mouths are
/// a small fraction of river cells, not a new grid pass) is what makes
/// `build_edges` give Mouth edges a real ~one-cell chord toward the raw
/// sea, so the termination walk + bisect + one-segment D8-probe fallback
/// (Ruling 3e) are actually reachable.
///
/// `#[serde(default)]` — same additive pattern as `river_class`/
/// `river_downstream`.
#[serde(default)]
pub river_seaward: Vec<(u16, u16)>,
}
/// [`RiverNetwork::river_downstream`] sentinel: this river cell's D8 flow
/// reaches a raw-sea cell (T-1170 Ruling 2c). Values 07 are real D8 direction
/// indices, so sentinels start at 8.
pub const RIVER_DOWNSTREAM_MOUTH: u8 = 8;
/// [`RiverNetwork::river_downstream`] sentinel: this river cell's flow exits
/// the grid's top/bottom (polar) edge — a grid artifact, never a mouth
/// (T-1170 Ruling 2c/3f).
pub const RIVER_DOWNSTREAM_EDGE_DRAIN: u8 = 9;
/// [`RiverNetwork::river_downstream`] sentinel: **reserved, unused in round
/// 1.** Flow terminates in an interior sink (future endorheic basin / inland
/// delta, T-1170 Ruling 2c/7b). Reserving this value now is what makes that
/// future extension additive rather than a wire migration.
pub const RIVER_DOWNSTREAM_TERMINAL: u8 = 10;
/// One drainage basin / province derived from watershed analysis (D-205).
/// Stub — boundary polyline data comes from atlas_province_boundaries.
#[derive(Debug, Clone, Serialize, Deserialize)]
+6
View File
@@ -305,6 +305,11 @@ pub fn run_cascade_from_heightmap(
// thalweg direction into each DistrictProfile.basin_direction
// (T-1047). Pass the map through derive_all_districts.
let basin_dirs = snapshot.layer1.as_ref().map(|l1| &l1.district_basin_dirs);
// T-1168 Ruling 4c: same `snapshot.layer1` source as
// `basin_dirs`/`river_cells` (the `road_graph` precedent
// below) — the river network for the batch-path riparian
// signal.
let river_network = snapshot.layer1.as_ref().map(|l1| &l1.river_network);
let districts = district_profile::derive_all_districts(
body_seed,
params,
@@ -312,6 +317,7 @@ pub fn run_cascade_from_heightmap(
scale::HEIGHTMAP_CELLS_PER_DISTRICT,
&snapshot.body_id,
basin_dirs,
river_network,
);
snapshot.layer_district = Some(LayerDistrictOutput { districts });
}
+267 -24
View File
@@ -22,9 +22,11 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::atlas::body_world_state::RiverNetwork;
use crate::atlas::coast_invention;
use crate::atlas::features::TerrainAnalysis;
use crate::atlas::region_profile::{self, RegionProfile};
use crate::atlas::river_course;
use crate::atlas::scale::{self, BasinDirection, RegionPos};
use crate::seed::SeedChain;
use crate::simulation::generator::MorphologyZone;
@@ -1230,7 +1232,17 @@ fn invent_primitives(
/// the batch path so both derivation paths key the invention noise fields on
/// the same world-metre convention. Radius-less bodies fall back to the
/// 1-working-pixel = 1-district convention `derive_district` uses.
fn pixel_to_world_m(px: f64, py: f64, w: usize, h: usize, radius_km: Option<f64>) -> (f64, f64) {
///
/// `pub(crate)` (T-1170): also used by the river course inventor
/// (`river_course::cell_world_m`) to resolve a river cell's pixel position to
/// its world-metre anchor — the SAME mapping, reused rather than duplicated.
pub(crate) fn pixel_to_world_m(
px: f64,
py: f64,
w: usize,
h: usize,
radius_km: Option<f64>,
) -> (f64, f64) {
match radius_km {
Some(r) if r > 0.0 => {
let wx = px / w.max(1) as f64 * (std::f64::consts::TAU * r * 1000.0);
@@ -1245,6 +1257,42 @@ fn pixel_to_world_m(px: f64, py: f64, w: usize, h: usize, radius_km: Option<f64>
}
}
/// Absolute world metres → fractional working-grid pixel — the inverse of
/// [`pixel_to_world_m`], and the SAME mapping [`derive_at_metres`]/
/// [`derive_orbital_at_metres`] compute inline for their own `(px, py)`
/// derivation (T-1170: extracted as a standalone `pub(crate)` helper rather
/// than duplicated a third time, for the river course inventor's Stage A
/// valley-seeking control path, which needs bilinear `elev_pct` reads at
/// arbitrary world positions without paying for a full `DistrictProfile`
/// derive per candidate — Ruling 3b, binding: "NOT `derive_at_metres` per
/// candidate"). Returns `(px, py)` only — callers that also need latitude
/// (temperature-sensitive derivation) still compute it themselves; the course
/// inventor's Stage A elevation proxy has no use for latitude.
pub(crate) fn world_m_to_pixel(
wx: f64,
wy: f64,
w: usize,
h: usize,
radius_km: Option<f64>,
) -> (f64, f64) {
match radius_km {
Some(r_km) if r_km > 0.0 => {
let circumference_m = std::f64::consts::TAU * r_km * 1000.0;
let meridian_m = std::f64::consts::PI * r_km * 1000.0;
let px = (wx / circumference_m).rem_euclid(1.0) * w as f64;
let lat_frac = (wy / meridian_m).clamp(-0.5, 0.5);
let py = (0.5 + lat_frac) * h.saturating_sub(1) as f64;
(px, py)
}
_ => {
let dm = scale::DISTRICT_M as f64;
let px = (wx / dm).clamp(0.0, w.saturating_sub(1) as f64);
let py = (wy / dm).clamp(0.0, h.saturating_sub(1) as f64);
(px, py)
}
}
}
// ---------------------------------------------------------------------------
// Public derivation function
// ---------------------------------------------------------------------------
@@ -1272,6 +1320,14 @@ fn pixel_to_world_m(px: f64, py: f64, w: usize, h: usize, radius_km: Option<f64>
/// - `region_cache` — pre-computed [`RegionProfile`] map keyed by [`RegionPos`];
/// if a neighbour region is missing it is derived on the fly. Build with
/// [`region_profile::derive_regions_for_body`] before calling this in a loop.
/// - `river_network` — the body's [`RiverNetwork`] (T-1168, Ruling 4b/4c),
/// consulted for the riparian point test via
/// [`river_course::near_perennial_water_at`] (edges near this district
/// invented on demand, the same pure function the window path uses).
/// `None` when no river network is available (e.g. a body with no Layer-1
/// drainage pass, or a caller that predates T-1168) — the riparian signal
/// degrades to `false` in that case, matching the pre-T-1168 hardcoded
/// default exactly, never a panic or an error.
pub fn derive_district_profile(
seed: SeedChain,
body_params: &BodyParams,
@@ -1282,6 +1338,7 @@ pub fn derive_district_profile(
body_id: &str,
region_cache: &BTreeMap<RegionPos, RegionProfile>,
basin_direction: BasinDirection,
river_network: Option<&RiverNetwork>,
) -> DistrictProfile {
let (rx, ry) = pos;
let w = ta.w;
@@ -1332,6 +1389,24 @@ pub fn derive_district_profile(
0.0, // batch path — no octave cutoff, matches derive_district's default
);
// T-1168 Ruling 4b: batch-path riparian signal — edges near this
// district invented on demand via the SAME pure function the window
// path uses. `river_network.is_none()` degrades to `false` (see this
// function's doc), never a panic.
let near_perennial_water = river_network
.map(|rn| {
river_course::near_perennial_water_at(
seed,
ta,
body_params,
rn,
(world_x_m, world_y_m),
scale::DISTRICT_M as f64,
0.0, // batch path — no octave cutoff, matches this function's own default
)
})
.unwrap_or(false);
build_district_profile(
seed,
body_params,
@@ -1344,6 +1419,7 @@ pub fn derive_district_profile(
world_x_m,
world_y_m,
0.0, // batch path — no octave cutoff, matches derive_district's default
near_perennial_water,
)
}
@@ -1381,6 +1457,18 @@ pub fn derive_district_profile(
/// always active wherever `VegetationEnvelope::ceiling_q > 0`, mirroring the
/// coast invention's own always-on posture (the ceiling being zero, not a
/// separate flag, is what turns it off on airless/dry bodies).
///
/// ## Riparian signal (T-1168, Ruling 4a-4d)
///
/// `near_perennial_water` is the T-1168 riparian point test result for
/// `(world_x_m, world_y_m)` — a separate boolean signal into
/// [`derive_vegetation`], computed by the caller (window path: distance to
/// the retained `Layer1Output`'s invented courses; batch path: distance to
/// on-demand-invented courses for nearby edges — both via the SAME pure
/// [`crate::atlas::river_course::near_perennial_water`] function). **This
/// value NEVER touches `moisture_q`** (Ruling 4d, binding, re-affirmed): it
/// is threaded straight through to `derive_vegetation` unchanged, after every
/// moisture/temperature/morphology field above it has already been resolved.
#[allow(clippy::too_many_arguments)]
fn build_district_profile(
seed: SeedChain,
@@ -1394,6 +1482,7 @@ fn build_district_profile(
world_x_m: f64,
world_y_m: f64,
min_wavelength_m: f64,
near_perennial_water: bool,
) -> DistrictProfile {
let tectonic_class = derive_tectonic_class(body_params);
@@ -1458,15 +1547,21 @@ fn build_district_profile(
moisture_q,
);
// Vegetation class (T-1025, D-239 §8). No riparian signal at district scale yet
// (requires perennial waterway map from L2+); default to false for now.
// L2 ChunkContext will override per-tile once drainage data is threaded through.
// open_water = the morphology verdict (T-1126) — never a threshold of its own.
// Vegetation class (T-1025, D-239 §8). near_perennial_water (T-1168) is
// the caller-computed riparian point test result — see this function's
// doc for the full threading contract (Ruling 4a-4d). open_water = the
// morphology verdict (T-1126) — never a threshold of its own.
let open_water = matches!(
morphology_zone,
MorphologyZone::OpenOcean | MorphologyZone::Lake
);
let vegetation_class = derive_vegetation(temperature_c, moisture_q, elev_q, false, open_water);
let vegetation_class = derive_vegetation(
temperature_c,
moisture_q,
elev_q,
near_perennial_water,
open_water,
);
DistrictProfile {
morphology_zone,
@@ -1529,6 +1624,7 @@ pub fn derive_district(
dy as f64 * dm,
climate,
0.0,
&[],
)
}
@@ -1546,6 +1642,19 @@ pub fn derive_district(
/// = no cutoff = [`derive_district`]'s existing behavior.
///
/// `body_id` is required for the D-243 §4 climate edge-fuzz warp domain separation.
///
/// `nearby_courses` (T-1168, Ruling 4b/4c): pre-invented river courses
/// (already culled to the caller's neighbourhood — the window path's own
/// bbox cull, `layer_proxy::build_courses_for_window`) consulted for the
/// riparian point test via [`river_course::near_perennial_water`]. Passing
/// `&[]` (the common case for a position far from any river, and every
/// pre-T-1168 caller via [`derive_district`]) is exactly the old hardcoded
/// `false` default — byte-identical output for every caller that doesn't
/// thread real course geometry through. This is a PRE-INVENTED slice, not a
/// `RiverNetwork` — this function is called once per window CELL (thousands
/// of times per window), so re-inventing courses on every call here (rather
/// than once per window) would be the exact per-candidate-derive cost this
/// whole batch's Ruling 3b was written to avoid.
#[allow(clippy::too_many_arguments)]
pub fn derive_at_metres(
seed: SeedChain,
@@ -1556,6 +1665,7 @@ pub fn derive_at_metres(
wy: f64,
climate: &ClimateConstants,
min_wavelength_m: f64,
nearby_courses: &[river_course::InventedCourse],
) -> DistrictProfile {
// World metres -> fractional heightmap pixel + latitude. Mirrors
// `derive_district`'s former inline mapping exactly, just keyed on
@@ -1645,6 +1755,13 @@ pub fn derive_at_metres(
// (derive_all_districts), which threads the true D8 direction from L1;
// this on-demand path is the fallback for positions derived outside that
// pass, where a meaningful basin_direction isn't available.
//
// T-1168 Ruling 4a: the riparian point test against the caller-supplied
// (already-culled) course slice — the SAME pure predicate the batch path
// uses via `near_perennial_water_at`.
let near_perennial_water =
river_course::near_perennial_water((world_x_m, world_y_m), nearby_courses);
build_district_profile(
seed,
&params,
@@ -1657,6 +1774,7 @@ pub fn derive_at_metres(
world_x_m,
world_y_m,
min_wavelength_m,
near_perennial_water,
)
}
@@ -1790,12 +1908,27 @@ pub fn derive_orbital_at_metres(
// the vegetation verdict even though slope/elevation stay
// envelope-only at this rung.
0.0,
// T-1168 Ruling 4e/5a: NO windowed course invention at Region
// granularity (`layer_proxy::build_courses_for_window` early-returns
// for `WindowGranularity::Region` — the whole-body skeleton path
// draws Region-rung rivers instead, Ruling 5a). Always `false` here
// — honest, not a gap: even if courses existed at Region, the 1-3 m
// riparian band is many orders of magnitude below Region's ~205 km
// spacing and could never fire (Ruling 4e).
false,
)
}
/// Bilinear interpolation of a row-major `f32` field at fractional `(px, py)`.
/// Columns wrap (equirectangular); rows clamp at the poles.
fn bilinear(field: &[f32], w: usize, h: usize, px: f64, py: f64) -> f32 {
///
/// `pub(crate)` (T-1170): also the elevation-proxy read the river course
/// inventor's Stage A valley-seeking control path uses
/// (`river_course::score_candidate`) — the SAME bilinear-`elev_pct` tradeoff
/// the coast warp already makes (`invent_primitives`'s step 3), reused rather
/// than re-implemented so the two invention fields can never silently drift
/// on interpolation semantics.
pub(crate) fn bilinear(field: &[f32], w: usize, h: usize, px: f64, py: f64) -> f32 {
if w == 0 || h == 0 {
return 0.0;
}
@@ -1869,6 +2002,11 @@ fn bilinear_bool(mask: &[bool], w: usize, h: usize, px: f64, py: f64) -> f32 {
/// from the map; missing entries (edge districts with no land cells) default to
/// `BasinDirection::North`. When `None` (tests / paths before Layer 1 runs),
/// every district gets `BasinDirection::North`.
///
/// `river_network` (T-1168, Ruling 4c) is threaded straight through to every
/// [`derive_district_profile`] call for the batch-path riparian signal — the
/// `road_graph` precedent (`cascade.rs`'s already-unpacked
/// `layer1.river_network`, same source, same threading pattern).
pub fn derive_all_districts(
seed: SeedChain,
body_params: &BodyParams,
@@ -1876,6 +2014,7 @@ pub fn derive_all_districts(
grid_cells_per_district: usize,
body_id: &str,
basin_dirs: Option<&BTreeMap<DistrictPos, BasinDirection>>,
river_network: Option<&RiverNetwork>,
) -> BTreeMap<DistrictPos, DistrictProfile> {
let climate = ClimateConstants::default();
let gcpr = grid_cells_per_district.max(1);
@@ -1939,6 +2078,7 @@ pub fn derive_all_districts(
body_id,
&region_cache,
basin_direction,
river_network,
);
out.insert(pos, profile);
}
@@ -1990,7 +2130,7 @@ mod tests {
let hm = test_hm();
let ta = test_ta(&hm);
let params = BodyParams::default();
let districts = derive_all_districts(test_seed(), &params, &ta, 8, "test_body", None);
let districts = derive_all_districts(test_seed(), &params, &ta, 8, "test_body", None, None);
// Expected: ceil(64/8) × ceil(32/8) = 8 × 4 = 32 districts.
assert_eq!(districts.len(), 32, "district count mismatch");
@@ -2008,7 +2148,7 @@ mod tests {
let params = BodyParams::default();
// Real DistrictPos keys from a baseline (None) run.
let baseline = derive_all_districts(test_seed(), &params, &ta, 8, "test_body", None);
let baseline = derive_all_districts(test_seed(), &params, &ta, 8, "test_body", None, None);
let mut keys = baseline.keys().copied();
let pos_east = keys.next().expect("at least one district");
let pos_south = keys.next().expect("at least two districts");
@@ -2018,8 +2158,15 @@ mod tests {
basin_dirs.insert(pos_east, BasinDirection::East);
basin_dirs.insert(pos_south, BasinDirection::South);
let districts =
derive_all_districts(test_seed(), &params, &ta, 8, "test_body", Some(&basin_dirs));
let districts = derive_all_districts(
test_seed(),
&params,
&ta,
8,
"test_body",
Some(&basin_dirs),
None,
);
assert_eq!(districts[&pos_east].basin_direction, BasinDirection::East);
assert_eq!(districts[&pos_south].basin_direction, BasinDirection::South);
@@ -2182,6 +2329,7 @@ mod tests {
dp.1 as f64 * dm,
&climate,
0.0,
&[],
);
assert_district_profiles_eq(&via_wrapper, &via_metres);
}
@@ -2212,6 +2360,7 @@ mod tests {
dp.1 as f64 * dm,
&climate,
0.0,
&[],
);
assert_district_profiles_eq(&via_wrapper, &via_metres);
}
@@ -2258,6 +2407,7 @@ mod tests {
-567.0 * dm + 512.0,
&climate,
512.0,
&[],
);
assert!((0..=100).contains(&prof.elev_q));
assert!((0..=100).contains(&prof.slope_q));
@@ -2280,7 +2430,17 @@ mod tests {
for i in 0..20 {
let wx = (100 + i * 37) as f64 * dm;
let wy = (100 + i * 53) as f64 * dm;
let uncut = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0);
let uncut = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
0.0,
&[],
);
let cut = derive_at_metres(
test_seed(),
"test_body",
@@ -2290,6 +2450,7 @@ mod tests {
wy,
&climate,
8_193.0, // above the two finest OCTAVE_WAVELENGTHS_M entries
&[],
);
if uncut.elev_q != cut.elev_q || uncut.slope_q != cut.slope_q {
any_differs = true;
@@ -2322,8 +2483,28 @@ mod tests {
for i in 0..12 {
let wx = (300 + i * 41) as f64 * dm * 0.1;
let wy = (300 + i * 29) as f64 * dm * 0.1;
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
let a = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
1_024.0,
&[],
);
let b = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
1_024.0,
&[],
);
assert_district_profiles_eq(&a, &b);
}
}
@@ -2355,8 +2536,28 @@ mod tests {
for i in 0..20 {
let wx = (150 + i * 47) as f64 * dm;
let wy = (150 + i * 31) as f64 * dm;
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
let a = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
4_096.0,
&[],
);
let b = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
4_096.0,
&[],
);
assert_district_profiles_eq(&a, &b);
}
}
@@ -2409,10 +2610,28 @@ mod tests {
for i in 0..20 {
let wx = (150 + i * 47) as f64 * dm;
let wy = (150 + i * 31) as f64 * dm;
let district =
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
let quarter =
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
let district = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
4_096.0,
&[],
);
let quarter = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
1_024.0,
&[],
);
if district.elev_q != quarter.elev_q
|| district.slope_q != quarter.slope_q
|| district.moisture_q != quarter.moisture_q
@@ -2454,6 +2673,7 @@ mod tests {
wy,
&climate,
300_000.0,
&[],
);
// An even more extreme cutoff must produce the SAME result — once
// every octave is truncated, going coarser still changes nothing.
@@ -2466,6 +2686,7 @@ mod tests {
wy,
&climate,
10_000_000.0,
&[],
);
assert_district_profiles_eq(&far_above, &even_further);
}
@@ -2507,6 +2728,7 @@ mod tests {
base_wy,
&climate,
2_048.0,
&[],
)
.vegetation_class;
@@ -2522,8 +2744,17 @@ mod tests {
for dx in -2..2 {
let wx = base_wx + dx as f64 * qm;
let wy = base_wy + dy as f64 * qm;
let prof =
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
let prof = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
1_024.0,
&[],
);
if prof.vegetation_class != VegetationClass::Marine {
*tally.entry(prof.vegetation_class as u8).or_insert(0) += 1;
}
@@ -2636,7 +2867,17 @@ mod tests {
let wx = 40.0 * dm;
let wy = 20.0 * dm;
let orbital = derive_orbital_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate);
let full = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0);
let full = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
0.0,
&[],
);
// The invented scatter is a bounded perturbation on top of the raw
// envelope (detail_scatter's amplitude is capped well under 100 elev_q
@@ -2717,6 +2958,7 @@ mod tests {
"test_body",
&BTreeMap::new(),
BasinDirection::North,
None,
);
let p2 = derive_district_profile(
test_seed(),
@@ -2728,6 +2970,7 @@ mod tests {
"test_body",
&BTreeMap::new(),
BasinDirection::North,
None,
);
// Equality via serialized fields (no PartialEq on MorphologyZone — compare by name).
assert_eq!(
@@ -2822,7 +3065,7 @@ mod tests {
let hm = test_hm();
let ta = test_ta(&hm);
let params = BodyParams::default();
let districts = derive_all_districts(test_seed(), &params, &ta, 8, "test_body", None);
let districts = derive_all_districts(test_seed(), &params, &ta, 8, "test_body", None, None);
// BTreeMap iterates in sorted key order — verify the first key is (0,0).
let first = districts.keys().next().expect("at least one district");
assert_eq!(*first, (0, 0), "first district must be at origin");
+223 -24
View File
@@ -19,7 +19,9 @@
use std::collections::VecDeque;
use crate::atlas::body_world_state::{DrainageBasin, RiverNetwork};
use crate::atlas::body_world_state::{
DrainageBasin, RiverNetwork, RIVER_DOWNSTREAM_EDGE_DRAIN, RIVER_DOWNSTREAM_MOUTH,
};
use crate::simulation::generator::TerritorialStatus;
/// A cell is a river cell when its flow accumulation exceeds this threshold (D-208).
@@ -41,6 +43,18 @@ const D8: [(i32, i32); 8] = [
(1, -1), // SW
];
/// `(dr, dc)` for D8 direction index `k` (0-7) — the same fixed priority-order
/// table [`extract_river_network`]/`flow_direction` use internally, exposed
/// `pub(crate)` so downstream consumers of [`crate::atlas::body_world_state::
/// RiverNetwork::river_downstream`] (T-1170's river course inventor) can walk
/// a river cell's D8 pointer without duplicating the table. Panics on an
/// out-of-range index — callers must check against the
/// `RIVER_DOWNSTREAM_MOUTH`/`RIVER_DOWNSTREAM_EDGE_DRAIN`/
/// `RIVER_DOWNSTREAM_TERMINAL` sentinels (values ≥ 8) before calling this.
pub(crate) fn d8_offset(k: u8) -> (i32, i32) {
D8[k as usize]
}
/// Result of the full D8 drainage analysis for one body.
#[derive(Debug, Clone)]
pub struct DrainageResult {
@@ -316,35 +330,80 @@ fn extract_river_network(
.map(|i| ((i / w) as u16, (i % w) as u16))
.collect();
// Mouths: river cells that flow to a sea cell or to the polar edge.
let mouths: Vec<(u16, u16)> = (0..n)
.filter(|&i| {
if !is_river[i] {
return false;
}
let r = i / w;
let c = i % w;
let k = fdir[i];
if k < 0 {
return true; // no outflow — edge
}
let (dr, dc) = D8[k as usize];
let nr = r as i32 + dr;
let nc = (c as i32 + dc).rem_euclid(w as i32) as usize;
if nr < 0 || nr >= h as i32 {
return true; // polar edge
}
// Flows into a sub-sea-level cell = mouth
elevation[nr as usize * w + nc] < sea_level
})
.map(|i| ((i / w) as u16, (i % w) as u16))
.collect();
// Mouths + river_downstream (T-1170 Ruling 2a/2c/3f): a single pass over
// `river_cells`, in the SAME order, computing both the D8-downstream
// sentinel/pointer AND whether this cell is a mouth. `fdir[i]` is already
// in scope here — capturing it as `river_downstream` costs one extra push
// per river cell, no new grid pass (Ruling 2a's binding requirement).
//
// **Pole-edge exits are NOT mouths (Ruling 3f, binding).** A river cell
// with no outflow because its D8 walk ran off the grid's top/bottom row
// is a grid artifact — the equirectangular projection simply stops there,
// there is no sea. The former code classified this the same as a real
// sea-adjacent mouth, which rendered double-ring mouth markers in polar
// ice with no sea in sight (Jeroen's capture question, T-1170 ticket).
// `RIVER_DOWNSTREAM_EDGE_DRAIN` cells are excluded from `mouths` here;
// T-1170's course inventor (Ruling 3f) ends their course geometry at the
// last in-grid station with no mouth flag.
//
// A flat-peak interior cell (no outflow, but NOT at a pole row) is D8's
// other `k < 0` case — vanishingly rare for a cell that also cleared
// `RIVER_THRESHOLD`, but handled the same as `EDGE_DRAIN` (no downstream
// neighbor to point at, not a sea mouth) rather than crashing the
// pointer's "always points somewhere real" contract.
//
// river_seaward (T-1170 PR #197 review, Hoshe #1): captured in the SAME
// pass, parallel to river_downstream — `(nr, nc)` is already computed
// here to decide the MOUTH sentinel; discarding it after the elevation
// check (the former code) was the second instance of the exact
// discard-then-need-it-later anti-pattern Ruling 2b fixed for
// river_downstream itself. Non-mouth entries get the `(0, 0)` placeholder
// (documented on the field as unreadable outside the MOUTH case).
let mut mouths: Vec<(u16, u16)> = Vec::new();
let mut river_downstream: Vec<u8> = Vec::new();
let mut river_seaward: Vec<(u16, u16)> = Vec::new();
for i in 0..n {
if !is_river[i] {
continue;
}
let r = i / w;
let c = i % w;
let k = fdir[i];
if k < 0 {
// No outflow at all — edge/flat-peak. Not a mouth (Ruling 3f).
river_downstream.push(RIVER_DOWNSTREAM_EDGE_DRAIN);
river_seaward.push((0, 0));
continue;
}
let (dr, dc) = D8[k as usize];
let nr = r as i32 + dr;
let nc = (c as i32 + dc).rem_euclid(w as i32) as usize;
if nr < 0 || nr >= h as i32 {
// Flow direction points off the polar edge — a grid artifact,
// not a mouth (Ruling 3f, the pole-edge-drain fix).
river_downstream.push(RIVER_DOWNSTREAM_EDGE_DRAIN);
river_seaward.push((0, 0));
continue;
}
if elevation[nr as usize * w + nc] < sea_level {
// Flows into a sub-sea-level cell — a real mouth. Capture the
// seaward neighbor position (Hoshe #1) alongside the sentinel.
mouths.push((r as u16, c as u16));
river_downstream.push(RIVER_DOWNSTREAM_MOUTH);
river_seaward.push((nr as u16, nc as u16));
continue;
}
river_downstream.push(k as u8);
river_seaward.push((0, 0));
}
RiverNetwork {
river_cells,
confluences,
mouths,
river_class,
river_downstream,
river_seaward,
}
}
@@ -1040,4 +1099,144 @@ mod tests {
"the body's max river-cell accumulation should be trunk"
);
}
// -----------------------------------------------------------------------
// river_downstream (T-1170 Ruling 2a/2c/3f)
// -----------------------------------------------------------------------
#[test]
fn river_downstream_parallel_to_river_cells() {
let elev = slope_grid(512, 256);
let result = analyze(&elev, 512, 256, 0.3);
let rn = &result.river_network;
assert_eq!(
rn.river_cells.len(),
rn.river_downstream.len(),
"river_downstream must be parallel/aligned with river_cells"
);
assert!(!rn.river_cells.is_empty());
}
#[test]
fn river_downstream_values_are_direction_or_sentinel() {
// Every entry is either a real D8 index (0-7) or one of the T-1170
// sentinels (MOUTH=8, EDGE_DRAIN=9); TERMINAL=10 is reserved/unused.
let elev = slope_grid(512, 256);
let result = analyze(&elev, 512, 256, 0.3);
for &v in &result.river_network.river_downstream {
assert!(
v <= RIVER_DOWNSTREAM_EDGE_DRAIN,
"unexpected river_downstream value {v} (round-1 only emits 0-7, MOUTH=8, \
EDGE_DRAIN=9 — TERMINAL=10 is reserved and unused)"
);
}
}
#[test]
fn river_downstream_mouth_sentinel_matches_mouths_list() {
// Every river cell whose river_downstream is MOUTH must appear in
// `mouths`, and every entry of `mouths` must have MOUTH as its
// river_downstream — the two are the same underlying classification,
// captured in the same pass (Ruling 2a/3f).
use crate::atlas::heightmap::load_heightmap_png;
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
let heightmap =
load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap");
let small = heightmap.downsample(256, 128);
let result = analyze(&small.data, small.width, small.height, small.sea_level);
let rn = &result.river_network;
assert!(!rn.mouths.is_empty(), "GJ1c should have real sea mouths");
let mouth_set: std::collections::BTreeSet<(u16, u16)> = rn.mouths.iter().copied().collect();
for (i, &pos) in rn.river_cells.iter().enumerate() {
let is_mouth_sentinel = rn.river_downstream[i] == RIVER_DOWNSTREAM_MOUTH;
let is_in_mouths_list = mouth_set.contains(&pos);
assert_eq!(
is_mouth_sentinel, is_in_mouths_list,
"cell {pos:?}: MOUTH sentinel ({is_mouth_sentinel}) must agree with \
mouths-list membership ({is_in_mouths_list})"
);
}
}
#[test]
fn pole_edge_drains_are_not_mouths() {
// Ruling 3f, binding: a river cell with no valid D8 outflow is a grid
// artifact, not a river-meets-sea event.
//
// **Why this test targets the flat/no-outflow (`k < 0`) case, not a
// literal "flow direction points past row 0" case:** `flow_direction`
// (this file, `fn flow_direction`) bounds-checks every D8 candidate
// BEFORE comparing drops (`if nr < 0 || nr >= h { continue; }`) — a
// row-0 cell can therefore never even be ASSIGNED a north-pointing
// `fdir` in the first place; the off-grid-direction branch in
// `extract_river_network`'s `river_downstream` computation exists as
// correct defensive code but is structurally unreachable given this
// invariant. The real, reachable "pole-edge-drain" case (confirmed
// against the committed GJ1c golden fixture, which has EDGE_DRAIN
// cells at several rows including row 0) is `k < 0`: a cell with NO
// neighbor at a strictly lower elevation — most commonly a flat
// plateau at the grid's fringe, which the depression-fill/flow
// algorithm cannot route off of. This fixture constructs exactly
// that: a perfectly flat plateau at row 0 (identical elevation
// across the whole top row, so no cell in it has a positive-drop
// neighbor and `flow_direction` assigns `k=-1` — verified this
// reproduces before ever reasoning about mouths) that river cells
// from a converging valley drain into, with no ocean anywhere.
let (w, h) = (64usize, 64usize);
let n = w * h;
let center_col = (w / 2) as f32;
let elev: Vec<f32> = (0..n)
.map(|i| {
let r = i / w;
let c = (i % w) as f32;
if r == 0 {
return 0.15; // flat plateau — no cell here has a strictly lower neighbor
}
// V-shaped valley converging on center_col, sloping down
// toward row 0 (but never reaching the plateau's own
// elevation until row 1, so row-1 cells drain INTO the flat
// row-0 plateau and then have nowhere further to go).
let dist_from_center = (c - center_col).abs() / center_col;
let valley = 0.15 + dist_from_center * 0.6;
let pole_gradient = (r as f32 / h as f32) * 0.25;
(valley + pole_gradient).clamp(0.0, 1.0)
})
.collect();
let result = analyze(&elev, w as u32, h as u32, 0.0);
let rn = &result.river_network;
assert!(
!rn.river_cells.is_empty(),
"the converging-valley fixture must clear RIVER_THRESHOLD — if this starts \
failing, the fixture (not the production code) needs retuning, since an \
empty river_cells silently no-ops every assertion below"
);
assert!(
rn.river_downstream.contains(&RIVER_DOWNSTREAM_EDGE_DRAIN),
"expected at least one EDGE_DRAIN-sentinel river cell (the flat-plateau case) \
on this fixture — downstream values were {:?}",
rn.river_downstream
);
assert!(
rn.mouths.is_empty(),
"an all-land, pole-draining world must have zero mouths — got {:?}",
rn.mouths
);
assert!(
!rn.river_downstream.contains(&RIVER_DOWNSTREAM_MOUTH),
"an all-land world must never emit a MOUTH sentinel"
);
}
#[test]
fn river_downstream_deterministic() {
let elev = slope_grid(64, 32);
let r1 = analyze(&elev, 64, 32, 0.3);
let r2 = analyze(&elev, 64, 32, 0.3);
assert_eq!(
r1.river_network.river_downstream, r2.river_network.river_downstream,
"river_downstream must be deterministic (D-010/D-208)"
);
}
}
+85 -32
View File
@@ -34,6 +34,7 @@ use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
use crate::atlas::district_profile::{BodyParams, ClimateConstants, DistrictPos};
use crate::atlas::features::TerrainAnalysis;
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use crate::atlas::layer1::Layer1Output;
use crate::atlas::layer_proxy::{
build_district_window_layer, DistrictWindowLayer, WindowGranularity,
};
@@ -271,7 +272,24 @@ pub enum GenCompletion {
BodyAnalyzed {
body_id: String,
/// The computed world state, ready for `BodyWorldStateCache::insert`.
state: BodyWorldState,
/// Boxed to keep `GenCompletion` variant sizes balanced — the same
/// discipline as `SkeletonGenerated`/`ChunkFilled`/`WindowDerived`
/// below, all boxed for the same reason (`GenCompletion`'s overall
/// size is bounded by its largest unboxed variant; a `BodyWorldState`
/// field carried by value here would force every other variant to
/// pay for its full stack size on every match/move).
///
/// **Not this variant's own recent growth** (T-1170 PR #197 review,
/// Tyre issue 2 — the prior comment here overattributed the boxing
/// rationale): `BodyWorldState` itself only grew by
/// `RiverNetwork`'s two new `Vec` fields (`river_downstream`,
/// `river_seaward` — a few bytes/river-cell, Ruling 2a / Hoshe #1).
/// The much larger `Layer1Output` retention (T-1170 Ruling 4b) lives
/// on `TerrainAnalysisCache` — a queue-scoped struct further down
/// this file, never part of `BodyWorldState`/`BodyWorldStateCache` at
/// all. The box here predates T-1170 and stays for the pre-existing
/// variant-size-balancing reason, unrelated to this ticket's growth.
state: Box<BodyWorldState>,
},
SkeletonGenerated {
city_id: u64,
@@ -585,9 +603,23 @@ impl Default for GenerationQueue {
/// D-227-pure derived window (valid forever, no recency signal to track),
/// which body a player keeps panning around IS a recency signal, so
/// access-order eviction is the right fit here.
///
/// **T-1170 (Ruling 4b) — also retains the `Layer1Output` produced alongside
/// `TerrainAnalysis`, not just the latter.** The original entry only kept
/// `TerrainAnalysis` and discarded `run_layer1`'s `Layer1Output` half (the
/// `let (_, ta) = run_layer1(...)` at the old call site) — fine for the
/// six-array district/quarter/vegetation classification the window path
/// used before this ticket, but it meant the window derive path had no way
/// to know which river edges exist near a window without re-running the
/// whole ~45 ms drainage pass a second time. Since this cache already pays
/// that cost once per body and holds the result for the session, keeping
/// BOTH halves of `run_layer1`'s return value is free — `Layer1Output`
/// itself is small (a `RiverNetwork` + basin list + attractor list, not the
/// full grid) relative to `TerrainAnalysis`'s ~1.52 MB of dense per-cell
/// Vecs.
#[derive(Debug)]
struct TerrainAnalysisCache {
entries: std::collections::BTreeMap<String, (TerrainAnalysis, u64)>,
entries: std::collections::BTreeMap<String, (Layer1Output, TerrainAnalysis, u64)>,
/// Monotonic access counter (substitutes for `BodyWorldStateCache`'s
/// `SimTick` — there is no tick concept on a background Rayon thread).
clock: u64,
@@ -607,36 +639,42 @@ impl TerrainAnalysisCache {
}
}
/// Look up a cached `TerrainAnalysis` for `body_id`, re-deriving via
/// `run_layer1` on a miss and inserting the result (evicting the LRU
/// entry first if at capacity). Bumps the access clock on both a hit and
/// a fresh insert (both are "this body was just used").
/// Look up a cached `(Layer1Output, TerrainAnalysis)` pair for `body_id`,
/// re-deriving via `run_layer1` on a miss and inserting the result
/// (evicting the LRU entry first if at capacity). Bumps the access clock
/// on both a hit and a fresh insert (both are "this body was just used").
///
/// Returns both halves of `run_layer1`'s output (T-1170 Ruling 4b) — the
/// window derive path (`GenWorkItem::DeriveWindow`) needs `Layer1Output`'s
/// `RiverNetwork` to know which river edges exist near the requested
/// window, in addition to the `TerrainAnalysis` it always needed.
fn get_or_derive(
&mut self,
body_id: &str,
heightmap: &crate::atlas::heightmap::BodyHeightmap,
) -> TerrainAnalysis {
) -> (Layer1Output, TerrainAnalysis) {
self.clock += 1;
let now = self.clock;
if let Some((ta, last_used)) = self.entries.get_mut(body_id) {
if let Some((l1, ta, last_used)) = self.entries.get_mut(body_id) {
*last_used = now;
return ta.clone();
return (l1.clone(), ta.clone());
}
let (_, ta) = crate::atlas::layer1::run_layer1(heightmap);
let (l1, ta) = crate::atlas::layer1::run_layer1(heightmap);
if self.entries.len() >= self.capacity && !self.entries.contains_key(body_id) {
if let Some(victim) = self
.entries
.iter()
.min_by_key(|(_, (_, last_used))| *last_used)
.min_by_key(|(_, (_, _, last_used))| *last_used)
.map(|(id, _)| id.clone())
{
self.entries.remove(&victim);
}
}
self.entries.insert(body_id.to_string(), (ta.clone(), now));
ta
self.entries
.insert(body_id.to_string(), (l1.clone(), ta.clone(), now));
(l1, ta)
}
#[cfg(test)]
@@ -705,7 +743,7 @@ fn run_work_item(
);
GenCompletion::BodyAnalyzed {
body_id: body_id.clone(),
state: snapshot.into_body_world_state(),
state: Box::new(snapshot.into_body_world_state()),
}
}
Err(e) => GenCompletion::Failed {
@@ -794,15 +832,22 @@ fn run_work_item(
} else {
hm
};
// TerrainAnalysis via the per-body LRU (T-1137 binding decision +
// PR #187 review C1): first window on a body pays the ~45 ms
// run_layer1 re-derive and populates the cache entry; every
// subsequent window on the SAME body (until eviction) hits the
// cache and skips straight to the ~729 ms per-window pack below.
// This is the memoized form of the SAME workaround
// aliveness_probe --render uses when CascadeSnapshot.terrain_analysis
// is None (it has no cache — a one-shot CLI run doesn't need one).
let ta = terrain_cache
// (Layer1Output, TerrainAnalysis) via the per-body LRU (T-1137
// binding decision + PR #187 review C1, extended T-1170 Ruling
// 4b to retain Layer1Output too): first window on a body pays
// the ~45 ms run_layer1 re-derive and populates the cache
// entry; every subsequent window on the SAME body (until
// eviction) hits the cache and skips straight to the ~729 ms
// per-window pack below. This is the memoized form of the SAME
// workaround aliveness_probe --render uses when
// CascadeSnapshot.terrain_analysis is None (it has no cache —
// a one-shot CLI run doesn't need one).
//
// `l1.river_network` is what lets the window derive know which
// river edges exist near this window (T-1170 A2) without a
// second drainage pass — the fix for the former
// `let (_, ta) = run_layer1(...)` discard (Ruling 4b).
let (l1, ta) = terrain_cache
.lock()
.unwrap()
.get_or_derive(body_id, &working);
@@ -812,6 +857,7 @@ fn run_work_item(
body_id,
body_params,
&ta,
&l1.river_network,
*center,
*n,
&climate,
@@ -1459,32 +1505,39 @@ mod tests {
}
/// A miss re-derives and populates the entry; a subsequent hit for the
/// SAME body returns an equal `TerrainAnalysis` (D-227: the same
/// heightmap always derives to the same analysis) WITHOUT growing the
/// cache — `len()` stays at 1, proving the second call short-circuited
/// past `run_layer1` rather than deriving-then-overwriting.
/// SAME body returns an equal `(Layer1Output, TerrainAnalysis)` pair
/// (D-227: the same heightmap always derives to the same analysis)
/// WITHOUT growing the cache — `len()` stays at 1, proving the second
/// call short-circuited past `run_layer1` rather than
/// deriving-then-overwriting.
#[test]
fn terrain_analysis_cache_hit_reuses_entry() {
let mut cache = TerrainAnalysisCache::new(8);
let hm = window_test_hm();
assert!(!cache.contains("BodyA"));
let first = cache.get_or_derive("BodyA", &hm);
let (l1_first, ta_first) = cache.get_or_derive("BodyA", &hm);
assert_eq!(cache.len(), 1);
assert!(cache.contains("BodyA"));
let second = cache.get_or_derive("BodyA", &hm);
let (l1_second, ta_second) = cache.get_or_derive("BodyA", &hm);
assert_eq!(
cache.len(),
1,
"a hit must not insert a second entry for the same body"
);
assert_eq!(
first.ocean_mask, second.ocean_mask,
ta_first.ocean_mask, ta_second.ocean_mask,
"same heightmap → identical re-derived analysis (D-227)"
);
assert_eq!(first.slope_deg, second.slope_deg);
assert_eq!(first.elev_pct, second.elev_pct);
assert_eq!(ta_first.slope_deg, ta_second.slope_deg);
assert_eq!(ta_first.elev_pct, ta_second.elev_pct);
// T-1170 Ruling 4b: Layer1Output (river_network in particular) is
// ALSO retained and identically re-derived, not just TerrainAnalysis.
assert_eq!(
l1_first.river_network.river_cells, l1_second.river_network.river_cells,
"Layer1Output.river_network must also be cached/reused, not just TerrainAnalysis"
);
}
/// Different bodies get independent entries, and a capacity-2 cache
File diff suppressed because it is too large Load Diff
+1
View File
@@ -28,6 +28,7 @@ pub mod layer_proxy;
pub mod mosaic;
pub mod plugin;
pub mod region_profile;
pub mod river_course;
pub mod road_graph;
pub mod scale;
pub mod shell;
+1 -1
View File
@@ -359,7 +359,7 @@ fn drain_generation_completions(
);
}
}
cache.insert(state);
cache.insert(*state);
}
GenCompletion::Failed { item, reason } => {
tracing::warn!(?item, %reason, "background generation work item failed");
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -104,7 +104,11 @@ pub fn chunk_to_region(c: ChunkPos) -> RegionPos {
// ---------------------------------------------------------------------------
/// Number of working-heightmap-grid pixels per district side on the standard
/// cascade working resolution (~128×64 working grid; D-203, D-239 §1, T-1023).
/// cascade working resolution (512×256 working grid, `heightmap::GRID_W` ×
/// `heightmap::GRID_H`; D-203, D-239 §1, T-1023). (T-1170 audit nit: this
/// comment previously read "~128×64" — stale since the working grid was
/// widened; the actual grid-shape source of truth is `heightmap::GRID_W`/
/// `GRID_H`, not a number restated here.)
///
/// This is NOT a metre-scale constant — it is the `grid_cells_per_district`
/// parameter passed to [`crate::atlas::district_profile::derive_all_districts`].
+10
View File
@@ -146,6 +146,15 @@ pub enum SeedDomain {
/// independent entropy — the `assign_block_tags` lesson (distinct
/// sub-chains per field, not one shared roll) applies here too.
TraitExterior = 16,
/// River course invention (T-1170, Ruling 3a): the linear sibling of the
/// coastline warp, keyed by `edge_id` (the packed upstream-cell u32 of a
/// D8 river edge, see `atlas::river_course`). Distinct domain so the
/// course's Stage-B perpendicular warp octaves can never correlate with
/// the coast warp, terrain scatter, or vegetation massif fields sampled
/// at the same world position (D-224 domain separation, the module's own
/// `RIVER_COURSE_WARP_SALT` provides a second, position-keyed layer of
/// isolation on top of this domain tag).
RiverCourse = 17,
}
/// A position in the deterministic seed tree (D-224).
@@ -311,6 +320,7 @@ mod tests {
assert_eq!(SeedDomain::TraitDistrict as u64, 14);
assert_eq!(SeedDomain::TraitSwerve as u64, 15);
assert_eq!(SeedDomain::TraitExterior as u64, 16);
assert_eq!(SeedDomain::RiverCourse as u64, 17);
}
#[test]
+38
View File
@@ -18,6 +18,44 @@
//! Golden captured on x86_64. The downsample and sub-biome cost use f32, so a
//! different architecture could in principle round differently — regenerate
//! per-arch if CI ever moves off x86_64.
//!
//! **Deliberate re-pin (T-1170 Ruling 2a/2c/3f, A1):** `RiverNetwork` gained
//! an additive `river_downstream: Vec<u8>` field (per-`river_cells`-entry D8
//! downstream pointer + MOUTH/EDGE_DRAIN sentinel), and `extract_river_network`
//! stopped classifying pole-edge D8 exits (flow running off the grid's
//! top/bottom row) as `mouths` — they are grid artifacts, not river-meets-sea
//! events (Ruling 3f, Jeroen's capture question). On this fixture (GJ1c,
//! 256×128 downsample) that drops `mouths` from 19 to 3: 16 of the 19 were
//! pole-edge exits (now `RIVER_DOWNSTREAM_EDGE_DRAIN`), leaving the 3 real
//! sea-adjacent mouths (`RIVER_DOWNSTREAM_MOUTH`). `river_cells`/`attractors`
//! counts are unchanged (93/256) — this is a pure re-classification + one new
//! additive field, not a drainage-algorithm change.
//!
//! **Attractor cascade correction (T-1170 PR #197 review, Hoshe #4):** the A1
//! commit's "pure reclassification" framing overclaimed — `attractors` is
//! count-PARITY (256/256), not byte-identical. Mouths dropping 19→3 shrinks
//! `TerrainAnalysis::water_dist`'s seed set (`compute_water_dist` seeds from
//! `river_network.mouths`), which shifts the water-distance field, which
//! feeds `RawAttractor::strength` scoring in `features::extract_attractors`.
//! This is a principled, in-scope cascade (the pole-edge cells genuinely
//! aren't water-distance sources anymore) — not a bug — but it is a REAL
//! field-level change, not a no-op re-tagging. Restated accurately here so
//! the record doesn't imply byte-identical attractor output.
//!
//! **Second deliberate re-pin (T-1170 PR #197 review, Hoshe #1):**
//! `RiverNetwork` gained a second additive field, `river_seaward:
//! Vec<(u16,u16)>` — the real seaward neighbor position for MOUTH-sentinel
//! river cells, captured in the SAME `extract_river_network` pass (the
//! elevation check that already computes `(nr, nc)` to decide the MOUTH
//! sentinel). Fixes a second instance of the discard-then-need-it-later
//! anti-pattern Ruling 2b's `river_downstream` field fixed for interior D8
//! pointers: `river_course::build_edges` needs a real seaward cell to give
//! Mouth edges a non-degenerate chord (see `river_course.rs`'s
//! `build_edges` doc and `layer_proxy::tests::
//! all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none` for the full
//! bug/fix story). `river_cells`/`mouths`/`attractors` counts unchanged by
//! this second re-pin (93/3/256) — purely the new parallel array, 3 non-
//! placeholder entries (one per real mouth).
use std::path::PathBuf;
+1
View File
@@ -1933,6 +1933,7 @@ fn derive_profile_for_body(params: &BodyParams) -> DistrictProfile {
"test_body",
&BTreeMap::new(),
settled_reach_server::atlas::scale::BasinDirection::default(),
None,
)
}
+3
View File
@@ -558,6 +558,8 @@ fn generate_atlas_layer_response_fixtures() {
confluences: vec![],
mouths: vec![(12, 58)],
river_class: vec![1, 2],
river_downstream: vec![2, 8], // 2=E direction; 8=MOUTH sentinel
river_seaward: vec![(0, 0), (12, 60)], // meaningful only for the MOUTH entry
},
drainage_basins: vec![DrainageBasin {
basin_id: 1,
@@ -736,6 +738,7 @@ fn generate_atlas_layer_response_fixtures() {
moisture_q: vec![90, 55, 0, 100],
vegetation: vec![6, 3, 0, 5], // Marine, Forest, Absent, RiparianThicket
glaciation: vec![0, 0, 4, 1], // None, None, IceCap, Light
courses: vec![],
};
let ready_with_window = AtlasLayerResponse {
body_id: "GJ1c".into(),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -254,6 +254,57 @@
"moisture_q": 43,
"vegetation": 1
},
{
"label": "river_course",
"rung": "district",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 2,
"elev_q": 8,
"ocean_fraction_q": 0,
"temperature_dc": 14,
"moisture_q": 49,
"vegetation": 3
},
{
"label": "river_course",
"rung": "quarter",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 3,
"elev_q": 4,
"ocean_fraction_q": 0,
"temperature_dc": 34,
"moisture_q": 50,
"vegetation": 3
},
{
"label": "river_course",
"rung": "region",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 0,
"elev_q": 11,
"ocean_fraction_q": 0,
"temperature_dc": -2,
"moisture_q": 49,
"vegetation": 3
},
{
"label": "airless_dry/coastal_a",
"rung": "district",
@@ -509,6 +560,57 @@
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/river_course",
"rung": "district",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 3,
"elev_q": 5,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/river_course",
"rung": "quarter",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 3,
"elev_q": 6,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/river_course",
"rung": "region",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 0,
"elev_q": 5,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "volcanic_coast/coastal_a",
"rung": "district",
@@ -763,5 +865,56 @@
"temperature_dc": 555,
"moisture_q": 30,
"vegetation": 2
},
{
"label": "volcanic_coast/river_course",
"rung": "district",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 4096,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 4,
"elev_q": 14,
"ocean_fraction_q": 0,
"temperature_dc": 500,
"moisture_q": 47,
"vegetation": 3
},
{
"label": "volcanic_coast/river_course",
"rung": "quarter",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 1024,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 6,
"elev_q": 5,
"ocean_fraction_q": 0,
"temperature_dc": 547,
"moisture_q": 49,
"vegetation": 3
},
{
"label": "volcanic_coast/river_course",
"rung": "region",
"wx_m": 36277344,
"wy_m": -6830545,
"min_wl_m": 0,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 0,
"elev_q": 18,
"ocean_fraction_q": 0,
"temperature_dc": 480,
"moisture_q": 47,
"vegetation": 3
}
]
+214 -1
View File
@@ -39,6 +39,7 @@ use settled_reach_server::atlas::district_profile::{
use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::features::TerrainAnalysis;
use settled_reach_server::atlas::heightmap::BodyHeightmap;
use settled_reach_server::atlas::river_course;
use settled_reach_server::atlas::scale;
use settled_reach_server::seed::{SeedChain, SeedDomain};
@@ -163,6 +164,20 @@ fn sweep_positions() -> Vec<(&'static str, f64, f64)> {
("coastal_c", 2_100_000.0, 1_560_000.0),
("inland", 500_000.0, 3_000_000.0),
("high_lat", 1_200_000.0, 8_500_000.0),
// T-1170 A2 Discipline item 4: empirically verified (probe run against
// this fixture, `sample_hm()`/`sample_params()`) that `sample_hm()`
// produces a real river-cell chain around working-grid pixel
// (row=10, col=116) — NONE of the original five sweep positions
// (pixel cols ~1.6-6.7) land anywhere near it. This position converts
// that pixel to world metres (same `world_m_to_pixel` inverse the
// production mapping uses) so the golden sweep also exercises
// `derive_at_metres` genuinely close to invented river geometry —
// closing the "believability expected unchanged, verify, don't
// assume" discipline item for the district-profile-only fields this
// golden already pins (courses themselves are pinned separately
// below, `river_course_golden_regression`, since this sweep's
// `derive_at_metres` calls never touch `RiverCourse` at all).
("river_course", 36_277_344.8, -6_830_545.5),
]
}
@@ -182,7 +197,7 @@ fn derive_golden_sample(
let prof = if orbital {
derive_orbital_at_metres(seed, body_id, params, ta, wx, wy, climate)
} else {
derive_at_metres(seed, body_id, params, ta, wx, wy, climate, min_wl_m)
derive_at_metres(seed, body_id, params, ta, wx, wy, climate, min_wl_m, &[])
};
GoldenSample {
label: label.to_string(),
@@ -417,3 +432,201 @@ fn golden_cutoffs_match_the_scale_ladder() {
assert_eq!(2 * scale::DISTRICT_M, DISTRICT_MIN_WL_M as i32);
assert_eq!(2 * scale::QUARTER_M, QUARTER_MIN_WL_M as i32);
}
// ---------------------------------------------------------------------------
// River course golden (T-1170 A2, Discipline item 4)
// ---------------------------------------------------------------------------
//
// Empirically verified (probe run against `sample_hm()`/`sample_params()`):
// this fixture body produces a real river-cell chain around working-grid
// pixel (row≈10, col=116) — the "river_course" sweep position above converts
// that pixel to world metres. This section pins the ACTUAL invented course
// geometry for an edge from that chain, at both District and Quarter station
// spacing, so courses themselves — not just the district-profile fields the
// main golden above covers — are regression-pinned.
//
// **Mouth coverage (T-1170 PR #197 review, Hoshe #2):** the sample set below
// also includes a real Mouth-terminus edge from `sample_hm()` (empirically
// probed: 4 mouth edges exist in this fixture) — `invent_course`'s raw output
// for that edge is pinned here (edge_id/class/terminus/points), closing the
// "zero Mouth coverage in any golden" gap at the invention layer. The FULL
// end-to-end path (invent → crop → `resolve_mouth_terminus` →
// `CourseTerminus::Mouth`) is covered separately and permanently by
// `layer_proxy::tests::all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none`
// (in-crate, since `crop_course_to_window`/`resolve_mouth_terminus` are
// private to `layer_proxy.rs` and unreachable from this integration test) —
// that test is the actual Hoshe #1 acceptance criterion (3/3 real mouths);
// this golden's job is regression-pinning the raw invented geometry, not
// re-proving the crop/resolve path.
const RIVER_COURSE_GOLDEN_FILE: &str = "tests/golden/river_course_golden.json";
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Clone)]
struct GoldenCourseSample {
rung: String,
edge_id: u32,
class: u8,
terminus: String,
/// Points rounded to the nearest metre (D-010 integer boundary at the
/// golden-pinning layer — the production wire path itself rounds to
/// `i32` metres, `layer_proxy::crop_course_to_window`).
points: Vec<(i64, i64)>,
}
fn river_course_golden_samples() -> Vec<GoldenCourseSample> {
let hm = sample_hm();
let ta = sample_ta(&hm);
let params = sample_params();
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7);
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
let edges = river_course::build_edges(&dr.river_network);
// Pick the interior edge whose upstream cell is closest to (row=10,
// col=116) — deterministic (BTreeMap-free linear scan, fixed tie-break
// by edge_id) rather than hardcoding an index that could silently shift
// if `build_edges`' ordering ever changes.
let target = edges
.iter()
.filter(|e| e.terminus == river_course::EdgeTerminusKind::Interior)
.min_by_key(|e| {
let dr = e.upstream.0 as i64 - 10;
let dc = e.upstream.1 as i64 - 116;
(dr * dr + dc * dc, e.edge_id)
})
.expect("sample_hm() fixture must have at least one interior river edge near (10, 116)");
// Hoshe #2: a real Mouth-terminus edge, deterministically selected as the
// lowest edge_id among the fixture's mouth edges (fixed tie-break, no
// hardcoded index).
let mouth_target = edges
.iter()
.filter(|e| e.terminus == river_course::EdgeTerminusKind::Mouth)
.min_by_key(|e| e.edge_id)
.expect("sample_hm() fixture must have at least one Mouth edge (empirically verified: 4)");
let mut out = Vec::new();
for (rung, spacing_m, min_wl_m) in [
("district", DISTRICT_MIN_WL_M, DISTRICT_MIN_WL_M),
("quarter", QUARTER_MIN_WL_M, QUARTER_MIN_WL_M),
] {
let course = river_course::invent_course(seed, target, &ta, &params, spacing_m, min_wl_m);
out.push(GoldenCourseSample {
rung: rung.to_string(),
edge_id: course.edge_id,
class: course.class,
terminus: format!("{:?}", course.terminus),
points: course
.points
.iter()
.map(|p| (p.0.round() as i64, p.1.round() as i64))
.collect(),
});
let mouth_course =
river_course::invent_course(seed, mouth_target, &ta, &params, spacing_m, min_wl_m);
out.push(GoldenCourseSample {
rung: format!("{rung}_mouth"),
edge_id: mouth_course.edge_id,
class: mouth_course.class,
terminus: format!("{:?}", mouth_course.terminus),
points: mouth_course
.points
.iter()
.map(|p| (p.0.round() as i64, p.1.round() as i64))
.collect(),
});
}
out
}
#[test]
fn river_course_golden_regression() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let golden_path = manifest.join(RIVER_COURSE_GOLDEN_FILE);
let run1 = river_course_golden_samples();
let run2 = river_course_golden_samples();
assert_eq!(
run1, run2,
"double-derivation mismatch — course invention determinism is broken (D-010/D-227)"
);
let actual_json = serde_json::to_string_pretty(&run1).expect("serialize") + "\n";
if std::env::var("UPDATE_GOLDEN").is_ok() {
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
std::fs::write(&golden_path, &actual_json).expect("write golden");
eprintln!(
"Golden written: {} ({} bytes)",
golden_path.display(),
actual_json.len()
);
return;
}
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
panic!(
"Golden file not found: {}.\n\
First run: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n{e}",
golden_path.display()
)
});
let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual");
let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden");
if actual_v != golden_v {
panic!(
"River-course golden mismatch — course invention changed.\n\
Update: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n\
Golden: {}\nActual: {}",
golden_json.trim(),
actual_json.trim()
);
}
}
/// The course golden's target edge must genuinely differ in point geometry
/// between District and Quarter station spacing (more, finer-spaced stations
/// at Quarter — Ruling 3b's cross-rung invariant) — otherwise the golden
/// would be pinning two identical rungs and the test would give false
/// confidence.
#[test]
fn river_course_rungs_have_different_station_counts() {
let samples = river_course_golden_samples();
let district = samples.iter().find(|s| s.rung == "district").unwrap();
let quarter = samples.iter().find(|s| s.rung == "quarter").unwrap();
assert!(
quarter.points.len() > district.points.len(),
"Quarter's finer station spacing must produce MORE points than District \
(district={}, quarter={})",
district.points.len(),
quarter.points.len()
);
}
/// **T-1170 PR #197 review, Hoshe #2 (golden coverage):** the pinned Mouth
/// edge samples must be genuine, non-degenerate courses (`terminus == "Mouth"`,
/// `points.len() >= 2`) — the direct golden-level check that the Hoshe #1 fix
/// (real seaward chord via `RiverNetwork::river_seaward`) reaches this fixture
/// too, not just the dedicated GJ1c acceptance test.
#[test]
fn river_course_mouth_samples_are_non_degenerate() {
let samples = river_course_golden_samples();
for rung in ["district_mouth", "quarter_mouth"] {
let sample = samples
.iter()
.find(|s| s.rung == rung)
.unwrap_or_else(|| panic!("missing golden sample for rung {rung}"));
assert_eq!(
sample.terminus, "Mouth",
"{rung}: build_edges must produce a Mouth-terminus RiverEdge for the pinned target"
);
assert!(
sample.points.len() >= 2,
"{rung}: Mouth edge invented a degenerate {}-point course — the chord-length fix \
(Hoshe #1) regressed for this fixture",
sample.points.len()
);
}
}
+227 -3
View File
@@ -23,7 +23,8 @@ use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::features::TerrainAnalysis;
use settled_reach_server::atlas::heightmap::BodyHeightmap;
use settled_reach_server::atlas::layer_proxy::{
build_district_window_layer, WindowGranularity, DISTRICT_WINDOW_MAX_N_REGION, WIRE_CAP_CELLS,
build_district_window_layer, WindowGranularity, DISTRICT_WINDOW_MAX_N,
DISTRICT_WINDOW_MAX_N_REGION, WIRE_CAP_CELLS,
};
use settled_reach_server::atlas::scale;
use settled_reach_server::seed::{SeedChain, SeedDomain};
@@ -54,6 +55,12 @@ fn bench_ta(hm: &BodyHeightmap) -> TerrainAnalysis {
TerrainAnalysis::analyze(hm, &dr)
}
fn bench_river_network(
hm: &BodyHeightmap,
) -> settled_reach_server::atlas::body_world_state::RiverNetwork {
drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level).river_network
}
fn bench_params() -> BodyParams {
BodyParams {
hydrosphere: Some("ocean".into()),
@@ -83,8 +90,17 @@ fn time_derive_sweep(
for col in 0..grid_side {
let wx = col as f64 * step_m;
let wy = row as f64 * step_m;
let prof =
derive_at_metres(seed, body_id, params, ta, wx, wy, climate, min_wavelength_m);
let prof = derive_at_metres(
seed,
body_id,
params,
ta,
wx,
wy,
climate,
min_wavelength_m,
&[],
);
// Prevent the optimizer from hoisting the call out of the loop.
std::hint::black_box(prof.elev_q);
}
@@ -273,6 +289,7 @@ fn bench_derive_orbital_at_metres_region_spacing() {
fn bench_served_region_window_tile_at_wire_cap() {
let hm = bench_hm();
let ta = bench_ta(&hm);
let rn = bench_river_network(&hm);
let params = bench_params();
let climate = ClimateConstants::default();
let seed = SeedChain::root(99).derive(SeedDomain::Body, 1);
@@ -293,6 +310,7 @@ fn bench_served_region_window_tile_at_wire_cap() {
"bench",
&params,
&ta,
&rn,
(0, 0),
n,
&climate,
@@ -309,6 +327,7 @@ fn bench_served_region_window_tile_at_wire_cap() {
"bench",
&params,
&ta,
&rn,
(0, 0),
n,
&climate,
@@ -338,3 +357,208 @@ fn bench_served_region_window_tile_at_wire_cap() {
" compare: shipped district n=64 cap measures ~5 ms/call (design doc §7, MEASURED)\n"
);
}
/// **T-1170 Discipline item 2 (mandatory): course-cost bench.** Window
/// derive with courses on vs. off, at District granularity, real cap `n=64`
/// — the shape Tyre's cost probe measured (+0.09-0.21 ms against a ~5 ms
/// baseline, under 5%). "Off" uses an empty `RiverNetwork` (zero edges to
/// invent, exactly the pre-T-1170 cost shape); "on" uses a real body with
/// genuine river geometry (GJ1c) so the course inventor's Stage A/B pipeline
/// actually runs for the edges that cull into the window, not a synthetic
/// gradient body that might have zero river cells at all.
#[test]
#[ignore]
fn bench_course_cost_on_vs_off() {
use settled_reach_server::atlas::body_world_state::RiverNetwork;
use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::heightmap::load_heightmap_png;
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap");
let small = heightmap.downsample(512, 256); // GRID_W x GRID_H, the real production working grid
let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level);
let ta = TerrainAnalysis::analyze(&small, &dr);
let rn_on = &dr.river_network;
let rn_off = RiverNetwork::default();
assert!(
!rn_on.river_cells.is_empty(),
"GJ1c at production working resolution must have river cells for this bench to be meaningful"
);
let params = BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
};
let climate = ClimateConstants::default();
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7);
let n = DISTRICT_WINDOW_MAX_N; // the real n=64 shipped cap
// Centre the window on a real river cell — a window at the world origin
// (unrelated to where GJ1c's rivers actually are) would cull EVERY edge
// out and measure nothing but baseline noise. Convert a real river cell
// to world metres (the SAME pixel_to_world_m formula
// `district_profile.rs` uses internally — inlined here since that
// function is `pub(crate)`, not reachable from an integration test),
// then to the DistrictPos the window centres on.
let river_cell = dr.river_network.river_cells[dr.river_network.river_cells.len() / 2];
let r_km = params.body_radius_km.unwrap();
let world_pos = (
river_cell.1 as f64 / ta.w as f64 * (std::f64::consts::TAU * r_km * 1000.0),
(river_cell.0 as f64 / (ta.h - 1) as f64 - 0.5) * (std::f64::consts::PI * r_km * 1000.0),
);
let center: (i32, i32) = (
(world_pos.0 / scale::DISTRICT_M as f64).floor() as i32,
(world_pos.1 / scale::DISTRICT_M as f64).floor() as i32,
);
println!("\n=== T-1170 course-cost bench (District, n={n}, real GJ1c river geometry) ===");
println!(" window centred at district {center:?} (river cell {river_cell:?})");
// Warm-up (allocator/cache warm, not counted).
let _ = build_district_window_layer(
seed,
"GJ1c",
&params,
&ta,
&rn_off,
center,
n,
&climate,
WindowGranularity::District,
0,
);
let _ = build_district_window_layer(
seed,
"GJ1c",
&params,
&ta,
rn_on,
center,
n,
&climate,
WindowGranularity::District,
0,
);
let iterations = 1000; // higher count than the other benches — window cost here is ~1 ms, noisy at low n
let t_off = Instant::now();
for _ in 0..iterations {
let layer = build_district_window_layer(
seed,
"GJ1c",
&params,
&ta,
&rn_off,
center,
n,
&climate,
WindowGranularity::District,
0,
);
std::hint::black_box(layer.morphology.len());
}
let elapsed_off = t_off.elapsed();
let ms_off = elapsed_off.as_secs_f64() * 1000.0 / iterations as f64;
let t_on = Instant::now();
let mut courses_seen = 0usize;
for _ in 0..iterations {
let layer = build_district_window_layer(
seed,
"GJ1c",
&params,
&ta,
rn_on,
center,
n,
&climate,
WindowGranularity::District,
0,
);
courses_seen = layer.courses.len();
std::hint::black_box(layer.morphology.len());
}
let elapsed_on = t_on.elapsed();
let ms_on = elapsed_on.as_secs_f64() * 1000.0 / iterations as f64;
assert!(
courses_seen > 0,
"bench measured nothing meaningful — the window at {center:?} culled every edge out; \
re-pick a district position genuinely near GJ1c's river geometry"
);
let delta_pct = ((ms_on - ms_off) / ms_off) * 100.0;
println!(
" courses OFF (empty RiverNetwork): {:.3} ms/call ({iterations} calls, {:.2} ms total)",
ms_off,
elapsed_off.as_secs_f64() * 1000.0
);
println!(
" courses ON (real GJ1c network): {:.3} ms/call ({iterations} calls, {:.2} ms total, \
{courses_seen} courses in the n={n} window at {center:?})",
ms_on,
elapsed_on.as_secs_f64() * 1000.0
);
println!(" delta: {delta_pct:+.1}% (Discipline item 2 budget: < ~5%)\n");
}
#[test]
#[ignore]
fn bench_near_perennial_water_percell_isolated() {
use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::heightmap::load_heightmap_png;
use settled_reach_server::atlas::river_course;
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode heightmap");
let small = heightmap.downsample(512, 256);
let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level);
let ta = TerrainAnalysis::analyze(&small, &dr);
let params = BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
};
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7);
let edges = river_course::build_edges(&dr.river_network);
let edge = &edges[edges.len() / 2];
let course = river_course::invent_course(seed, edge, &ta, &params, 2048.0, 0.0);
let courses = vec![course];
let n = 4096u32;
let t0 = Instant::now();
let mut count = 0;
for i in 0..n {
let pos = (i as f64 * 100.0, i as f64 * 37.0);
if river_course::near_perennial_water(pos, &courses) {
count += 1;
}
}
let elapsed = t0.elapsed();
eprintln!(
"near_perennial_water: {:.3} ns/call ({n} calls, {} hits)",
elapsed.as_secs_f64() * 1e9 / n as f64,
count
);
// invent_course cost, isolated.
let t1 = Instant::now();
for _ in 0..100 {
let c = river_course::invent_course(seed, edge, &ta, &params, 2048.0, 0.0);
std::hint::black_box(c.points.len());
}
eprintln!(
"invent_course: {:.3} us/call",
t1.elapsed().as_secs_f64() * 1e6 / 100.0
);
}