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:
@@ -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