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:
2026-07-23 08:55:29 +02:00
co-authored by Claude Fable 5
parent 0d22e50e66
commit 60faf667a5
7 changed files with 314 additions and 15 deletions
@@ -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":