Entry per Jeroen's 2026-07-21 revision: planetary heightmap is now FIXED — all drag-pan/wheel-zoom input removed (set_view/get_view_* capture API survives for the golden harness); hover shows a not-to-scale bracket reticle with the real extent labeled (a true n=32 rectangle is sub-pixel on the planetary canvas — the honest representation given the morph transition is deferred), and a click that misses every city marker descends (city-click wins — one gesture, two contextual reads, no modifier). Descent pushes a new 'district' nav screen centered on the click point's DistrictPos via atlas_descend_geometry.district_pos_at (the pixel-to-district inverse of the server mapping, verified against scale.rs). Regional mode: atlas_window_viewer draws the composite (morphology x elev_q lightness base; temp/moisture/veg toggles — temp reuses the region-ramp colorizer exactly; Marine=6 transparent; glaciation always-on tint matching apply_ice_tint's REAL gate, None|Light no-op, over the amendment's looser prose — documented); pan-on-held-composite with edge-crossing refetch + border-fade during the queue-based derive wait; zoom never refetches. atlas_window_cache: LRU keyed (body_id, center, n), touch-on-read, evict-only, no freshness (D-227). atlas_window_request mirrors the generation-proxy pending-retry shape for None-until-derived. Codec: window params omitted from the wire when absent — byte-identical for every existing caller. Live-verified against the T-1137 server in-worktree: real round-trip on a GJ380c coastal district (6 fields x 1024 cells), echo staleness guard, genuine ~1.4s background-derive wait, pan-edge refetch to an adjacent window, cache-hit on re-descent with zero network. Full client suite 3194/3194; gdlint clean on all 18 files. Open follow-ups flagged in-code: header location label always falls back to coordinates (nearest-settlement needs a join the district window does not carry); atlas_standalone.gd's 'atlas_app.gd is never modified' doc line is now imprecise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
7.1 KiB
GDScript
163 lines
7.1 KiB
GDScript
## T-1138 (D-226 T-1124 amendment §5 entry revision): tests for the planetary
|
||
## AtlasViewer's click-through descent — the fixed view (no drag-pan/wheel-
|
||
## zoom), the pixel-to-DistrictPos inverse mapping (atlas_descend_geometry.gd),
|
||
## and the city-click-wins disambiguation rule.
|
||
class_name TestAtlasDescendEntry
|
||
extends GdUnitTestSuite
|
||
|
||
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
||
|
||
|
||
# =============================================================================
|
||
# atlas_descend_geometry.gd — pure geometry (no scene tree needed)
|
||
# =============================================================================
|
||
|
||
|
||
## The default n=32 window's real footprint is 32 * 2.048 km = 65.536 km, so
|
||
## the label reads "~66 x 66 km" (rounded) — pins the number the amendment's
|
||
## "honest labeling" resolution depends on.
|
||
func test_reticle_label_extent_matches_district_window_default_n() -> void:
|
||
var label: Dictionary = AtlasDescendGeometry.reticle_label(Vector2(100.0, 100.0))
|
||
var expected_km: float = 32.0 * 2048.0 / 1000.0
|
||
assert_str(label["text"]).is_equal("~%.0f × %.0f km" % [expected_km, expected_km])
|
||
|
||
|
||
func test_reticle_label_position_offsets_from_center() -> void:
|
||
var center := Vector2(50.0, 60.0)
|
||
var label: Dictionary = AtlasDescendGeometry.reticle_label(center)
|
||
var pos: Vector2 = label["position"]
|
||
assert_float(pos.x).is_greater(center.x) # offset to the right, per §5's resolution
|
||
|
||
|
||
## Eight segments (four L-shaped bracket corners, two arms each) — every
|
||
## segment's "from" endpoint is exactly one of the four corner anchors, and no
|
||
## segment has zero length (a degenerate reticle would be invisible).
|
||
func test_reticle_segments_has_eight_nonzero_segments() -> void:
|
||
var segments: Array = AtlasDescendGeometry.reticle_segments(Vector2(200.0, 150.0))
|
||
assert_int(segments.size()).is_equal(8)
|
||
for seg: Array in segments:
|
||
assert_that(seg[0]).override_failure_message(
|
||
"a reticle segment must not be zero-length"
|
||
).is_not_equal(seg[1])
|
||
|
||
|
||
## The reticle's overall bounding box is centered on `center` and sized by
|
||
## DESCEND_RETICLE_SIZE — a regression guard against an off-center or
|
||
## mis-scaled bracket.
|
||
func test_reticle_segments_centered_on_input_point() -> void:
|
||
var center := Vector2(300.0, 300.0)
|
||
var segments: Array = AtlasDescendGeometry.reticle_segments(center)
|
||
var min_pt := Vector2(INF, INF)
|
||
var max_pt := Vector2(-INF, -INF)
|
||
for seg: Array in segments:
|
||
for p: Vector2 in seg:
|
||
min_pt.x = minf(min_pt.x, p.x)
|
||
min_pt.y = minf(min_pt.y, p.y)
|
||
max_pt.x = maxf(max_pt.x, p.x)
|
||
max_pt.y = maxf(max_pt.y, p.y)
|
||
var bbox_center: Vector2 = (min_pt + max_pt) * 0.5
|
||
assert_float(bbox_center.x).is_equal_approx(center.x, 0.01)
|
||
assert_float(bbox_center.y).is_equal_approx(center.y, 0.01)
|
||
|
||
|
||
# =============================================================================
|
||
# district_pos_at — pixel-to-DistrictPos inverse mapping
|
||
# =============================================================================
|
||
|
||
|
||
## No radius (tiny test body fallback, matches the server's own derive_district
|
||
## fallback): the district grid IS the heightmap grid 1:1, so a canvas point
|
||
## maps to the nearest integer district coordinate directly.
|
||
func test_district_pos_at_no_radius_is_1to1_pixel_mapping() -> void:
|
||
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(
|
||
Vector2(12.4, 7.6), 100.0, 100.0, 0.0
|
||
)
|
||
assert_that(pos).is_equal(Vector2i(12, 8))
|
||
|
||
|
||
func test_district_pos_at_zero_texture_size_is_safe() -> void:
|
||
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(Vector2(10.0, 10.0), 0.0, 0.0, 6371.0)
|
||
assert_that(pos).is_equal(Vector2i.ZERO)
|
||
|
||
|
||
## The equatorial center of the texture (px = tex_w/2) is district column
|
||
## district_cols/2 on a body with a radius (equator wraps at district (0,0)
|
||
## per the server's own doc: "district (0,0) sits at lon 0 / the equator").
|
||
## The vertical center (py = tex_h/2) is the equator row (row 0).
|
||
func test_district_pos_at_center_of_texture_is_near_equator_row_zero() -> void:
|
||
var radius_km := 6371.0
|
||
var tex_w := 1024.0
|
||
var tex_h := 512.0
|
||
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(
|
||
Vector2(0.0, tex_h * 0.5), tex_w, tex_h, radius_km
|
||
)
|
||
assert_int(pos.y).override_failure_message(
|
||
"the vertical texture center must map to row 0 (the equator)"
|
||
).is_equal(0)
|
||
|
||
|
||
## Panning across the texture's full width sweeps through the FULL district
|
||
## column range (not clamped to a tiny sub-range) — a coarse sanity check that
|
||
## district_cols is actually being derived from the body's circumference, not
|
||
## left at some degenerate default.
|
||
func test_district_pos_at_sweeps_full_column_range_across_texture_width() -> void:
|
||
var radius_km := 6371.0
|
||
var tex_w := 1024.0
|
||
var tex_h := 512.0
|
||
var left: Vector2i = AtlasDescendGeometry.district_pos_at(
|
||
Vector2(0.0, tex_h * 0.5), tex_w, tex_h, radius_km
|
||
)
|
||
var right: Vector2i = AtlasDescendGeometry.district_pos_at(
|
||
Vector2(tex_w - 1.0, tex_h * 0.5), tex_w, tex_h, radius_km
|
||
)
|
||
# An Earth-radius body has thousands of equatorial districts (circumference
|
||
# ~40,075 km / 2.048 km per district ≈ 19,568) — near-full-width should
|
||
# sweep a large fraction of that, not a handful of columns.
|
||
assert_int(absi(right.x - left.x)).is_greater(1000)
|
||
|
||
|
||
# =============================================================================
|
||
# AtlasViewer — fixed view (no drag-pan/wheel-zoom) + click-through descent
|
||
# =============================================================================
|
||
|
||
|
||
## T-1120 capture API (set_view/get_view_offset/get_view_zoom) must survive
|
||
## the removal of user pan/zoom — this is the ticket's own explicit note, and
|
||
## the visual-golden harness depends on it.
|
||
func test_set_view_still_works_after_pan_zoom_removal() -> void:
|
||
var v: AtlasViewer = auto_free(AtlasViewer.new())
|
||
add_child(v)
|
||
v.set_view(3.0, Vector2(50.0, -20.0))
|
||
assert_that(v.get_view_zoom()).is_equal_approx(3.0, 0.001)
|
||
assert_that(v.get_view_offset()).is_equal(Vector2(50.0, -20.0))
|
||
|
||
|
||
## Clicking (with no heightmap loaded — the guard AtlasViewer's _gui_input
|
||
## checks first) must not emit a descend request — there is nothing to
|
||
## descend into yet.
|
||
func test_no_descend_signal_without_a_loaded_heightmap() -> void:
|
||
var v: AtlasViewer = auto_free(AtlasViewer.new())
|
||
add_child(v)
|
||
var received: Array = []
|
||
v.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
|
||
# _heightmap_texture stays null (no show_body() call) — _gui_input's
|
||
# early-return guard should prevent any click handling at all.
|
||
var mb := InputEventMouseButton.new()
|
||
mb.button_index = MOUSE_BUTTON_LEFT
|
||
mb.pressed = true
|
||
mb.position = Vector2(100.0, 100.0)
|
||
v._gui_input(mb)
|
||
assert_int(received.size()).is_equal(0)
|
||
|
||
|
||
## RegionalScreen forwards AtlasViewer's district_descend_requested verbatim —
|
||
## the nav-stack wiring atlas_app.gd depends on.
|
||
func test_regional_screen_forwards_district_descend_requested() -> void:
|
||
var screen: RegionalScreen = auto_free(RegionalScreen.new())
|
||
add_child(screen)
|
||
var received: Array = []
|
||
screen.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
|
||
screen._viewer.district_descend_requested.emit(Vector2i(5, 7))
|
||
assert_int(received.size()).is_equal(1)
|
||
assert_that(received[0]).is_equal(Vector2i(5, 7))
|