From 60faf667a54c1aabe46f8456af052748ea17b03b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Jul 2026 08:55:29 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20T-1156=20live=20rounds=20=E2=80=94?= =?UTF-8?q?=20zoom-compensated=20marker=20sizes;=20toggle-redraw=20regress?= =?UTF-8?q?ion=20pin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live round 2 (the real bug): every nature-overlay marker size was a raw screen-space constant drawn inside _canvas, whose scale IS view_zoom — at Lendel's orbital fit zoom (0.0063) a 2.2px trunk dot rendered at ~0.014px, invisible; the same code at District's 3.75 zoom produced the correctly-visible mouth ring, which is why one capture worked and the headline rung didn't. Fixed via AtlasWindowGeometry.zoom_compensated_ size() (pure, floor-guarded) wired through every radius/line-width; basin FILL points are positions and correctly stay unscaled. Suspect tile-mode-rung-detection was ruled out live (granularity_v2=Region confirmed in tile mode) but pinned with a named regression test anyway. +8 pure-function tests incl. a numeric pin of the pre-fix magnitude (<0.02px at orbital zoom); revert-verified by name. Draw-smoke suite documented as supplementary (the shared SubViewport background harness can pass vacuously under X11 BadMatch — the pure suite is the gate). Live round 3 (drive-script bug, no product change): the lead's scratch drive passed the button LABEL to set_overlay_visible() and the unknown- id guard silently no-op'd — but the chase banked a real pin: test_set_overlay_visible_gen_basins_flips_gate_and_redraws_nature_ overlay (draw-counting spy per the cold-start precedent; is_queued_for_ redraw does not exist in this build). Revert-verified. Basins verified live: 7 Lendel watershed boundaries render at the ruling's alphas. Suites: viewer 76/76, geometry-nature 42/42, nature- overlay 22/22, zoom-ladder 50/50, no regressions across the cluster. Tickets: T-1156 Co-Authored-By: Claude Fable 5 --- .../test_atlas_window_geometry_nature.gd | 63 +++++++++++++++ .../tests/test_atlas_window_nature_overlay.gd | 12 ++- ..._atlas_window_nature_overlay_draw_smoke.gd | 68 ++++++++++++++++ client/tests/test_atlas_window_viewer.gd | 79 +++++++++++++++++++ client/tests/test_atlas_zoom_ladder.gd | 32 ++++++++ .../apps/atlas/atlas_window_geometry.gd | 22 ++++++ .../apps/atlas/atlas_window_nature_overlay.gd | 53 ++++++++++--- 7 files changed, 314 insertions(+), 15 deletions(-) diff --git a/client/tests/test_atlas_window_geometry_nature.gd b/client/tests/test_atlas_window_geometry_nature.gd index cbd513b0a..3c7d63315 100644 --- a/client/tests/test_atlas_window_geometry_nature.gd +++ b/client/tests/test_atlas_window_geometry_nature.gd @@ -218,3 +218,66 @@ 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() + + +# ============================================================================= +# Coordinator live-eyeball finding (2026-07-23): zoom_compensated_size() — +# marker sizes must stay CONSTANT on screen regardless of _view_zoom +# (Araminta's ruling), but draw calls execute inside a Node2D whose .scale IS +# _view_zoom — a raw constant gets multiplied by that transform at render +# time. This function pre-divides so the transform's multiply cancels back +# out to the literal screen-space value. +# ============================================================================= + + +## At zoom=1.0 (the canvas transform's identity scale) the compensated size +## must equal the input unchanged — no over/under-correction at the one zoom +## level where compensation is a no-op by construction. +func test_zoom_compensated_size_at_zoom_one_is_unchanged() -> void: + assert_float(AtlasWindowGeometry.zoom_compensated_size(2.2, 1.0)).is_equal_approx(2.2, 0.0001) + + +## The exact regression shape: at Lendel's real orbital fit zoom (~0.0063, +## live drive script), the compensated size must be much LARGER than the +## raw screen-space constant — inversely proportional to zoom — so that once +## the canvas transform re-multiplies it by view_zoom at render time, the +## EFFECTIVE on-screen size lands back at the literal ruling value, not a +## sub-pixel sliver. +func test_zoom_compensated_size_at_orbital_zoom_scales_up_inversely() -> void: + var view_zoom := 0.0063 + var screen_space_size := 2.2 + var compensated: float = AtlasWindowGeometry.zoom_compensated_size(screen_space_size, view_zoom) + # Round-trip: compensated * view_zoom must reconstruct the original + # screen-space size — this IS the property that makes the on-screen + # result zoom-invariant (the canvas transform performs exactly this + # multiply at render time). + assert_float(compensated * view_zoom).is_equal_approx(screen_space_size, 0.001) + assert_float(compensated).override_failure_message( + "at a tiny orbital zoom, the compensated size must be dramatically LARGER" + + " than the raw screen-space constant — that's the whole point of the fix" + ).is_greater(screen_space_size * 10.0) + + +## The exact BUG this fix closes, pinned as a regression: an UNCOMPENSATED +## radius (screen_space_size used directly, the pre-fix behavior) multiplied +## by Lendel's real orbital zoom produces a sub-pixel effective size — this +## is the "the ruling's px value, at orbital fit zoom, is invisible" claim +## from the coordinator's diagnosis, verified numerically rather than just +## asserted. +func test_uncompensated_radius_at_orbital_zoom_would_be_sub_pixel() -> void: + var view_zoom := 0.0063 + var raw_screen_space_radius := 2.2 # RIVER_DOT_RADIUS_BY_CLASS_REGION[TRUNK] + var effective_size_if_uncompensated: float = raw_screen_space_radius * view_zoom + assert_float(effective_size_if_uncompensated).override_failure_message( + "an uncompensated radius at orbital zoom must be sub-pixel — pinning the" + + " numeric magnitude of the bug this fix closes, not just its existence" + ).is_less(0.02) + + +## A degenerate zero (or negative) view_zoom must not divide-by-zero/produce +## infinity/NaN — the floor guard keeps this function total. +func test_zoom_compensated_size_zero_zoom_does_not_blow_up() -> void: + var result: float = AtlasWindowGeometry.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() diff --git a/client/tests/test_atlas_window_nature_overlay.gd b/client/tests/test_atlas_window_nature_overlay.gd index 19235727c..6cd07799e 100644 --- a/client/tests/test_atlas_window_nature_overlay.gd +++ b/client/tests/test_atlas_window_nature_overlay.gd @@ -14,14 +14,17 @@ const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_win ## Minimal viewer stub — AtlasWindowNatureOverlay only reaches the viewer ## through get_held_granularity_v2()/get_body_radius_km()/get_held_center()/ -## get_held_n()/get_cell_pixel_size()/is_overlay_visible(), the same -## duck-typed-viewer precedent test_atlas_window_overlay.gd's _ViewerStub -## already establishes for AtlasWindowOverlay. +## get_held_n()/get_cell_pixel_size()/get_view_zoom()/is_overlay_visible(), +## the same duck-typed-viewer precedent test_atlas_window_overlay.gd's +## _ViewerStub already establishes for AtlasWindowOverlay. get_view_zoom() +## added post-live-eyeball (coordinator finding, 2026-07-23): _draw() now +## reads it for the zoom-compensated marker-size fix. class _ViewerStub: var held_granularity_v2: String = "Region" var body_radius_km: float = 6371.0 var held_center: Vector2i = Vector2i.ZERO var held_n: int = 64 + var view_zoom: float = 1.0 var overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": false, "gen_attractors": false} func get_held_granularity_v2() -> String: @@ -39,6 +42,9 @@ class _ViewerStub: func get_cell_pixel_size() -> float: return 16.0 + func get_view_zoom() -> float: + return view_zoom + func is_overlay_visible(overlay_id: String) -> bool: return bool(overlay_visibility.get(overlay_id, false)) diff --git a/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd b/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd index 84ecaa21d..bb11b35d2 100644 --- a/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd +++ b/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd @@ -19,6 +19,25 @@ ## godot4 --display-driver x11 --rendering-driver opengl3 \ ## -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c \ ## -a res://tests/test_atlas_window_nature_overlay_draw_smoke.gd +## +## **Known limitation, verified directly (2026-07-23 zoom-compensation fix +## verification):** the shared _BackgroundRect/_render_to_image() harness +## (copied from test_atlas_window_overlay_draw_smoke.gd) occasionally fails +## to composite the COLOR_BG fill at all under a real X11/opengl3 run in this +## environment (sampled pixels read (0,0,0,0), not COLOR_BG — an unrelated, +## intermittent X11/SubViewport timing issue, confirmed via an XServer +## "BadMatch" warning in that same run's log). When that happens EVERY pixel +## reads as "non-background" regardless of what this overlay actually draws, +## making a low MIN_NON_BACKGROUND_FRACTION threshold pass VACUOUSLY (it +## would pass even with zoom-compensation deliberately broken — confirmed by +## direct revert-test). The terrain sibling suite is accidentally immune to +## this (its checkerboard composite covers most of the frame regardless of +## background correctness); this suite's SPARSE markers are not. **The +## reliable, environment-independent regression gate for the zoom- +## compensation fix is therefore the pure-function suite in +## test_atlas_window_geometry_nature.gd** (zoom_compensated_size()'s own +## tests) — this smoke suite is a supplementary "does it actually paint" +## check when the harness cooperates, not the primary gate. class_name TestAtlasWindowNatureOverlayDrawSmoke extends GdUnitTestSuite @@ -44,11 +63,17 @@ static func _dummy_renderer_active() -> bool: ## Same duck-typed viewer contract as test_atlas_window_nature_overlay.gd's ## _ViewerStub, minus the SimBridge-signal machinery this smoke test doesn't ## need (layer1 is injected directly via _layer1, not through a response). +## view_zoom MUST be kept in sync with whatever zoom _render_to_image() is +## called with — the whole point of this smoke suite (post-live-eyeball, +## coordinator finding 2026-07-23) is proving markers stay visible at the +## REAL orbital fit zoom, not an artificially large test zoom that would +## mask the zoom-compensation bug the fix addresses. class _ViewerStub: var held_granularity_v2: String = "Region" var body_radius_km: float = 6371.0 var held_center: Vector2i = Vector2i.ZERO var held_n: int = 64 + var view_zoom: float = 1.0 var overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": true, "gen_attractors": true} func get_held_granularity_v2() -> String: @@ -66,6 +91,9 @@ class _ViewerStub: func get_cell_pixel_size() -> float: return 16.0 + func get_view_zoom() -> float: + return view_zoom + func is_overlay_visible(overlay_id: String) -> bool: return bool(overlay_visibility.get(overlay_id, false)) @@ -171,6 +199,7 @@ func test_region_rung_draws_visible_pixels() -> void: # Zoom chosen so the held window's canvas footprint (held_n * cell_px = # 64 * 16 = 1024 units) comfortably fills the 512px viewport. var zoom: float = float(VIEWPORT_SIZE.x) / (float(stub.held_n) * stub.get_cell_pixel_size()) + stub.view_zoom = zoom var image: Image = await _render_to_image(overlay, zoom) var fraction: float = _non_background_fraction(image) @@ -200,6 +229,7 @@ func test_district_rung_still_draws_visible_pixels() -> void: overlay._requested_body_id = "SmokeBody" var zoom: float = float(VIEWPORT_SIZE.x) / (float(stub.held_n) * stub.get_cell_pixel_size()) + stub.view_zoom = zoom var image: Image = await _render_to_image(overlay, zoom) var fraction: float = _non_background_fraction(image) @@ -211,3 +241,41 @@ func test_district_rung_still_draws_visible_pixels() -> void: ) % (fraction * 100.0) ).is_greater(MIN_NON_BACKGROUND_FRACTION) + + +## Coordinator live-eyeball regression (2026-07-23): the ORBITAL TILE MOSAIC +## rest state's REAL fit zoom on a Lendel-scale body (~0.0063, confirmed +## directly via a live drive script — held_n=19139 districts, +## cell_px=16, a ~1600px viewport) — NOT the comfortable ~0.5 zoom the +## sibling test above uses. Before the zoom-compensation fix, this exact +## zoom magnitude produced ZERO visible river/mouth pixels (a 2.2px trunk +## dot rasterized at ~0.014 screen px) despite RVR being on and the policy +## table correctly returning full Region visibility — the captures showed +## terrain-only with literally nothing drawn. Pins the regression at +## production scale, not a toy zoom that could accidentally still pass. +func test_orbital_scale_zoom_still_draws_visible_pixels() -> void: + if _dummy_renderer_active(): + print(SKIP_REASON) + return + var overlay := AtlasWindowNatureOverlay.new() + var stub := _ViewerStub.new() + stub.held_n = 19139 # Lendel's own raw circumference, live drive script + stub.view_zoom = 0.0063 # Lendel's own live orbital fit zoom + overlay.viewer = stub + overlay._layer1 = _mock_layer1_dense() + overlay._requested_body_id = "SmokeBody" + + var image: Image = await _render_to_image(overlay, stub.view_zoom) + var fraction: float = _non_background_fraction(image) + + assert_float(fraction).override_failure_message( + ( + "at Lendel's REAL orbital fit zoom (~0.0063), the Region-rung nature" + + " composite must still render VISIBLE non-background pixels — got only" + + " %.4f%% of the frame differing from COLOR_BG. This is exactly the" + + " coordinator's live-eyeball finding: uncompensated screen-space marker" + + " sizes get multiplied by the canvas's own zoom transform, vanishing" + + " sub-pixel at the orbital rest state's tiny fit zoom." + ) + % (fraction * 100.0) + ).is_greater(MIN_NON_BACKGROUND_FRACTION) diff --git a/client/tests/test_atlas_window_viewer.gd b/client/tests/test_atlas_window_viewer.gd index 127efedfe..621ac57e0 100644 --- a/client/tests/test_atlas_window_viewer.gd +++ b/client/tests/test_atlas_window_viewer.gd @@ -16,6 +16,11 @@ const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_re # T-1142: district_extent()/canonicalize_district_center() — used to derive # real (cols/rows_half) bounds for the wrap/pole-wall tests below. const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd") +# T-1156 wave 1 round 3: AtlasWindowNatureOverlay has no class_name (matching +# atlas_overlay_bar.gd/atlas_window_request.gd's own no-class_name precedent, +# review #8) — subclassing it (the _CountingNatureOverlay spy below) needs +# the preloaded script's PATH via `extends`, not a global class name. +const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd") ## Build a hand-authored DistrictWindowLayer dict (n=2, matching the shape @@ -131,6 +136,80 @@ func test_set_overlay_visible_unknown_id_is_a_noop() -> void: assert_bool(v.is_overlay_visible("not_a_real_overlay")).is_false() +## Counts real _draw() invocations on AtlasWindowNatureOverlay — CanvasItem +## exposes no public "is a redraw pending" query in this Godot version +## (confirmed directly: is_queued_for_redraw() does not exist on Node2D here +## — an earlier version of this test assumed it did and failed with +## "Invalid call. Nonexistent function"), so the only reliable signal that +## queue_redraw() actually had an effect is the engine calling _draw() again +## on a subsequent frame. Matches test_atlas_cold_start.gd's own +## _CountingOverlay precedent exactly (same file's own doc: "the only +## reliable signal... is the engine calling _draw() again") — subclasses the +## REAL AtlasWindowNatureOverlay so drawing still runs through genuine +## production code, this spy only adds counting. +class _CountingNatureOverlay extends AtlasWindowNatureOverlay: + var draw_count := 0 + + func _draw() -> void: + draw_count += 1 + super._draw() + + +## Coordinator live-eyeball round 3 (2026-07-23): R1/R2 captures were +## byte-identical because a scratch drive script called +## set_overlay_visible("BAS", true) — the button LABEL, not the overlay id +## ("gen_basins") — which set_overlay_visible()'s own `not +## _overlay_visibility.has(overlay_id): push_warning(...); return` guard +## silently no-ops on. Real callers (atlas_overlay_bar.gd's +## _on_toggle_changed()) always pass def["id"], never the label, so product +## code was never actually broken — but this pins the EXACT gate the +## coordinator asked to verify: toggling gen_basins via the real viewer API +## must (a) flip is_overlay_visible("gen_basins") — the nature overlay's OWN +## draw gate, read live via viewer.is_overlay_visible() at _draw() time, not +## a stale copy — AND (b) actually cause the NATURE overlay (not just the +## terrain overlay/viewer) to redraw on the next frame, proven by swapping in +## a _draw()-counting spy (matching test_atlas_cold_start.gd's +## _CountingOverlay pattern) and confirming draw_count advances past a +## settled baseline after the toggle, with no other gesture. +func test_set_overlay_visible_gen_basins_flips_gate_and_redraws_nature_overlay() -> void: + var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) + add_child(v) + assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message( + "gen_basins must default to OFF (Araminta's ruling — BAS defaults off)" + ).is_false() + + # Swap in the counting spy (matching _CountingOverlay's own swap-after- + # construction shape) so entry's own queue_redraw() calls don't pollute + # the baseline, then let it settle before touching the toggle. + var spy := _CountingNatureOverlay.new(v) + v._nature_overlay.queue_free() + v._nature_overlay = spy + v._canvas.add_child(spy) + + await get_tree().process_frame + await get_tree().process_frame + var baseline: int = spy.draw_count + assert_int(baseline).override_failure_message( + "sanity: the spy must have drawn at least once before the toggle, or" + + " this test can't distinguish 'redrawn BY the toggle' from 'never" + + " drawn at all'" + ).is_greater(0) + + v.set_overlay_visible("gen_basins", true) + await get_tree().process_frame + + assert_bool(v.is_overlay_visible("gen_basins")).override_failure_message( + "the toggle must flip the draw gate is_overlay_visible() reads live" + ).is_true() + assert_int(spy.draw_count).override_failure_message( + "the toggle must queue_redraw() the NATURE overlay specifically —" + + " queue_redraw() on the viewer/terrain overlay alone leaves the" + + " nature node's last frame cached (a Node2D child does not redraw" + + " because its sibling did) — draw_count must have advanced past the" + + " baseline (%d)" % baseline + ).is_greater(baseline) + + # ============================================================================= # T-1120 capture-API parity (the ticket's explicit note: must survive here too) # ============================================================================= diff --git a/client/tests/test_atlas_zoom_ladder.gd b/client/tests/test_atlas_zoom_ladder.gd index c6b775cfe..05d45623f 100644 --- a/client/tests/test_atlas_zoom_ladder.gd +++ b/client/tests/test_atlas_zoom_ladder.gd @@ -109,6 +109,38 @@ func test_enter_orbital_tile_mode_held_n_is_the_whole_body_extent() -> void: assert_int(v._held_n).is_equal(raw_cols) +## Coordinator live-eyeball dossier (2026-07-23, suspect 2 — DIAGNOSED FALSE +## but pinned as a regression guard anyway per the coordinator's own +## instruction): AtlasWindowNatureOverlay's _draw() reads +## viewer.get_held_granularity_v2() to key its per-rung policy tables +## (RIVER_CLASS_VISIBLE_BY_RUNG etc.) — if that accessor returned anything +## other than the EXACT string "Region" while is_tile_mode() is true (an +## empty string, a stale District default, a different-cased tag...), the +## visibility tables would silently return their empty/default disposition +## and NOTHING would draw, indistinguishable from the live captures' +## "literally zero river dots" symptom. Live drive-script evidence +## (NATURE_DEBUG print, since removed) confirmed this was NOT the actual bug +## — get_held_granularity_v2() already correctly returns "Region" in tile +## mode — but this test makes that fact load-bearing instead of merely +## observed once, so a future refactor of _enter_tile_mode()'s +## _held_granularity_v2 assignment trips a named failure here. +func test_get_held_granularity_v2_is_exactly_region_string_in_tile_mode() -> void: + var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new()) + add_child(v) + var radius_km := 6238.4 # GJ380c (Lendel) — needs tiling + v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {}) + + assert_bool(v.is_tile_mode()).override_failure_message( + "this test's premise requires tile mode — Lendel must still need tiling" + ).is_true() + assert_str(v.get_held_granularity_v2()).override_failure_message( + "AtlasWindowNatureOverlay's _draw() keys its ENTIRE per-rung policy off" + + " this exact string — anything other than the literal 'Region' silently" + + " empties every visibility table and draws nothing, indistinguishable" + + " from the live-capture symptom (zero river dots at the orbital rest state)" + ).is_equal("Region") + + ## **The live-round-3 regression, end to end for TILE mode:** enter_orbital() ## on GJ380c/Lendel followed by delivering ONE tile's wire-accurate response ## (clamped n=6,400, "Region" granularity_v2, the legacy sentinel in the old diff --git a/client/ui/implant/apps/atlas/atlas_window_geometry.gd b/client/ui/implant/apps/atlas/atlas_window_geometry.gd index 5dd278500..ea669d1a5 100644 --- a/client/ui/implant/apps/atlas/atlas_window_geometry.gd +++ b/client/ui/implant/apps/atlas/atlas_window_geometry.gd @@ -895,3 +895,25 @@ static func basins_visible_at_rung(granularity_v2: String) -> bool: 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) diff --git a/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd b/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd index 12e8c05b7..eea4ff083 100644 --- a/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd +++ b/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd @@ -165,6 +165,15 @@ func _draw() -> void: "cell_px": cell_px, "cols": cols, "granularity_v2": granularity_v2, + # Coordinator live-eyeball finding (2026-07-23): every marker size + # below is drawn as a SCREEN-SPACE constant (Araminta's ruling), but + # draw calls execute inside _canvas, whose .scale IS view_zoom — a + # raw constant gets multiplied by that transform at render time, + # invisible at the Region orbital tile mosaic's tiny fit zoom + # (~0.006). zs() below pre-divides by view_zoom so the transform's + # multiply cancels back to the literal screen-space value. See + # AtlasWindowGeometry.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( @@ -190,6 +199,15 @@ func _cols_for_wrap(radius_km: float) -> int: return int(AtlasDescendGeometryRef.district_extent(radius_km).get("cols", 0)) +## 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. +func _zs(screen_space_size: float, ctx: Dictionary) -> float: + return AtlasWindowGeometry.zoom_compensated_size(screen_space_size, ctx["view_zoom"]) + + ## Pixel (row, col) -> canvas-local, wrap-resolved to whichever longitude ## image is nearest the currently-held view — the SAME two-step ## (map-then-nearest-wrap) the tile mosaic draw path uses, just for a single @@ -234,7 +252,7 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void: var p: Vector2 = _pos(float(c[0]), float(c[1]), ctx) if is_region: var radius: float = AtlasWindowGeometry.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2) - draw_circle(p, radius, COLOR_GEN_RIVER) + 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. @@ -244,37 +262,43 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void: COLOR_GEN_RIVER.b, COLOR_GEN_RIVER.a * AtlasWindowGeometry.RIVER_DOT_OPACITY_DISTRICT_TRUNK ) - draw_circle(p, AtlasWindowGeometry.RIVER_DOT_RADIUS_DISTRICT_TRUNK, faded) + draw_circle(p, _zs(AtlasWindowGeometry.RIVER_DOT_RADIUS_DISTRICT_TRUNK, ctx), faded) if AtlasWindowGeometry.confluences_visible_at_rung(granularity_v2): for cf: Variant in rn.get("confluences", []): if cf is Array and cf.size() >= 2: var p: Vector2 = _pos(float(cf[0]), float(cf[1]), ctx) - draw_circle(p, AtlasWindowGeometry.RIVER_CONFLUENCE_RADIUS_REGION, COLOR_GEN_RIVER) + var radius: float = _zs(AtlasWindowGeometry.RIVER_CONFLUENCE_RADIUS_REGION, ctx) + draw_circle(p, radius, COLOR_GEN_RIVER) if AtlasWindowGeometry.mouths_visible_at_rung(granularity_v2): for m: Variant in rn.get("mouths", []): if m is Array and m.size() >= 2: - _draw_mouth(_pos(float(m[0]), float(m[1]), ctx)) + _draw_mouth(_pos(float(m[0]), float(m[1]), ctx), ctx) ## Double-ring sea-terminus marker — verbatim geometry from the retired ## atlas_marker_overlay.gd _draw_gen_rivers() (:536-539). Mouths never fade ## (Araminta's ruling: "a mouth is always a landmark") — same styling at ## every rung it's visible at (Region, District; never Quarter). -func _draw_mouth(p: Vector2) -> void: - draw_arc(p, AtlasWindowGeometry.MOUTH_RING_RADIUS, 0.0, TAU, 18, COLOR_GEN_MOUTH, 1.5) +func _draw_mouth(p: Vector2, ctx: Dictionary) -> void: + draw_arc( + p, _zs(AtlasWindowGeometry.MOUTH_RING_RADIUS, ctx), 0.0, TAU, 18, COLOR_GEN_MOUTH, _zs(1.5, ctx) + ) var halo := Color( COLOR_GEN_MOUTH.r, COLOR_GEN_MOUTH.g, COLOR_GEN_MOUTH.b, AtlasWindowGeometry.MOUTH_HALO_ALPHA ) - draw_arc(p, AtlasWindowGeometry.MOUTH_HALO_RADIUS, 0.0, TAU, 22, halo, 1.0) + 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 + ## boundary polyline, verbatim geometry from the retired ## atlas_marker_overlay.gd _draw_gen_basins() (:542-557), coordinate mapping ## replaced with _pos() (this file's wrap-aware canvas-local mapping) in place -## of the retired _gen_pos() texture-fraction mapping. +## of the retired _gen_pos() texture-fraction mapping. The FILL polygon's +## points are positions (never zoom-compensated — the fill must track the +## real district-space shape); only the boundary LINE's width is a +## screen-space marker size and goes through _zs(). func _draw_basins(ctx: Dictionary) -> void: for b: Variant in _layer1.get("drainage_basins", []): if not b is Dictionary: @@ -290,7 +314,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, 0.8, true) + draw_polyline(loop, COLOR_GEN_BASIN_LINE, _zs(0.8, ctx), true) ## Attractors — Region only, wave 1 (per the ruling; District/Quarter never @@ -298,7 +322,9 @@ func _draw_basins(ctx: Dictionary) -> void: ## attractors_visible_at_rung()). Ported from the retired ## atlas_marker_overlay.gd _draw_gen_attractors()/_draw_attractor_shape() ## (:560-...) — the shape vocabulary (7 attractor-type glyphs) is Araminta's -## existing design, unchanged; only the coordinate mapping moves to _pos(). +## existing design, unchanged; only the coordinate mapping moves to _pos() +## and the size is zoom-compensated before reaching the shape drawer (that +## function stays a pure "draw this size at this position", zoom-agnostic). func _draw_attractors(ctx: Dictionary) -> void: for a: Variant in _layer1.get("attractors", []): if not a is Dictionary: @@ -310,13 +336,16 @@ func _draw_attractors(ctx: Dictionary) -> void: if not pos_rc is Array or pos_rc.size() < 2: continue var p: Vector2 = _pos(float(pos_rc[0]), float(pos_rc[1]), ctx) - var size: float = 5.0 + strength * 4.0 + var 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) ## Attractor type -> marker shape — verbatim from the retired -## atlas_marker_overlay.gd _draw_attractor_shape(), ported unchanged. +## atlas_marker_overlay.gd _draw_attractor_shape(), ported unchanged. `size` +## arrives ALREADY zoom-compensated from _draw_attractors() — this function +## stays a pure "draw at this literal size" primitive with no ctx/zoom +## knowledge of its own, matching the retired code's own signature exactly. func _draw_attractor_shape(atype: String, pos: Vector2, size: float, color: Color) -> void: match atype: "RiverMouth":