fix(ui): T-1156 live rounds — zoom-compensated marker sizes; toggle-redraw regression pin
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user