fix(ui): mouth-ring radius floored at 2x stroke — the radius-smaller-than-stroke draw_arc regime

Two separable findings from the live A/B (real driver): (1) the
original zero-ring report was a viewport-framing crop — the ring
fired all along at screen (1755,-191), above the frame under the
COVER fit; recentering via set_view proved the path live. (2) The
real bug once in-frame: at Quarter fit zoom the compensated ring
radius (5.0/7.5=0.667 canvas) fell below the floored stroke (1.0
canvas) and draw_arc's stroke filled its own hole — a solid blob,
not a ring. Bracket: 1x stroke=blob, 1.5x=hollow recovers, 2x=clean.
Fix: zoom_compensated_ring_radius() floors the radius at 2x the
paired stroke, wired through _zs_ring_radius() for both ring and
halo arcs; verified live producing a clean hollow double-ring. The
third member of the Godot sub-canvas-unit rasterizer family (width
floor, stroke-vs-width sites, now radius-vs-stroke) — all recorded
for the T-1176 render-mechanism discussion. 10 pin tests in the
stroke-width suite; revert-verified (floor drop -> named failures);
full client suite 3956/3956; gdlint clean.

Tickets: T-1170

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 15:47:33 +02:00
co-authored by Claude Fable 5
parent c804f2c239
commit f9ab3088ab
3 changed files with 197 additions and 3 deletions
@@ -140,3 +140,113 @@ func test_zoom_compensated_stroke_width_zero_zoom_does_not_blow_up() -> void:
assert_bool(is_finite(result)).override_failure_message(
"a degenerate zero view_zoom must not produce inf/NaN"
).is_true()
# =============================================================================
# T-1170 live round (2026-07-23, coordinator's mouth-ring finding):
# zoom_compensated_ring_radius() — the RADIUS-SMALLER-THAN-STROKE regime.
# Same A/B-bracket discipline as the stroke-width suite above, applied to
# draw_arc() RING markers (mouth rings), whose radius AND stroke are both
# small zoom-compensated values that can cross each other.
##
## Live A/B evidence (temporary instrumentation, since reverted): at the
## PRODUCTION Quarter-rung radius/stroke pair (radius=0.667, stroke=1.0
## canvas-local units, Quarter fit zoom 7.5), draw_arc() rendered a SOLID
## BLOB, not a hollow ring — confirmed via a re-centered live capture (the
## ORIGINAL "zero ring pixels" symptom was a separate viewport-framing crop,
## not this bug — see zoom_compensated_ring_radius()'s own doc). Bracket
## (stroke fixed at 1.0 canvas-local, radius varied): 0.51 (~stroke/2) ->
## blob; 1.0 (=stroke, the production case) -> blob; 1.5 (1.5x stroke) ->
## hollow ring recovers; 2.0 (2x stroke) -> hollow ring, cleaner. Floor set
## at 2x with margin over the observed 1.0x-blob/1.5x-hollow transition.
# =============================================================================
## At zoom=1.0 with a radius comfortably above stroke*2 already, the floor
## must be a no-op — identical to zoom_compensated_size() directly.
func test_zoom_compensated_ring_radius_no_op_when_radius_already_clears_the_floor() -> void:
var radius: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(5.0, 1.5, 1.0)
assert_float(radius).is_equal_approx(5.0, 0.0001)
## The EXACT regression shape this round closes: at Quarter's real fit zoom
## (7.5, live capture), MOUTH_RING_RADIUS (5.0) divides to 0.667
## canvas-local — BELOW its own paired stroke (1.5/7.5 floored to 1.0 via
## zoom_compensated_stroke_width) — pinned directly, not just asserted.
## zoom_compensated_ring_radius() must instead return stroke * 2.0 (the
## floor), never the sub-floor raw division result that produced the blob.
func test_zoom_compensated_ring_radius_quarter_mouth_ring_hits_the_floor() -> void:
var view_zoom := 7.5 # LENDEL's live Quarter fit zoom, capture-confirmed
var raw_radius := 5.0 # MOUTH_RING_RADIUS
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(1.5, view_zoom)
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_radius, view_zoom)
assert_float(unfloored).override_failure_message(
"regression pin: the OLD unfloored radius must be AT/BELOW the paired"
+ " stroke at this zoom — this is the exact numeric shape of the blob bug"
).is_less_equal(stroke)
var floored: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(
raw_radius, stroke, view_zoom
)
assert_float(floored).override_failure_message(
"zoom_compensated_ring_radius() must clamp to stroke * 2.0 (the floor),"
+ " not the sub-floor unfloored value that renders as a solid blob"
).is_equal_approx(stroke * AtlasWindowGeometryNature.RING_RADIUS_STROKE_MULTIPLIER, 0.0001)
## Regression pin (the stroke-width suite's own "effective on-screen value"
## shape, applied to the radius/stroke RATIO instead of an absolute value):
## for the mouth ring AND halo (the two draw_arc() ring markers in this
## cluster), at Quarter's real fit zoom, the floored radius must be AT LEAST
## RING_RADIUS_STROKE_MULTIPLIER times its own paired stroke — the actual
## geometric property that keeps the ring hollow, verified directly rather
## than just re-checking the numeric floor value in isolation.
func test_ring_radius_stays_at_least_the_multiplier_above_its_stroke_at_quarter_zoom() -> void:
var view_zoom := 7.5
# (raw_radius_px, raw_stroke_px) pairs — the mouth ring and halo's own
# literal call-site arguments in _draw_mouth().
for pair in [[5.0, 1.5], [8.0, 1.0]]:
var raw_radius: float = pair[0]
var raw_stroke: float = pair[1]
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(
raw_stroke, view_zoom
)
var radius: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(
raw_radius, stroke, view_zoom
)
assert_float(radius).override_failure_message(
(
"radius %.3f must be at least %.1fx its paired stroke %.3f — a ratio"
+ " below this rendered as a SOLID BLOB in the live A/B bracket,"
+ " never a hollow ring"
)
% [radius, AtlasWindowGeometryNature.RING_RADIUS_STROKE_MULTIPLIER, stroke]
).is_greater_equal(stroke * AtlasWindowGeometryNature.RING_RADIUS_STROKE_MULTIPLIER - 0.0001)
## At a LOW zoom (e.g. Region's tiny orbital fit, or any zoom where the
## naive radius is already well clear of the floor), the floor must NOT
## engage — matching zoom_compensated_stroke_width()'s own
## does-not-engage-at-low-zoom guarantee. Pins that this is a one-direction
## safety net, not a blanket override.
func test_zoom_compensated_ring_radius_does_not_engage_at_low_zoom() -> void:
var view_zoom := 0.0063 # Lendel's real orbital fit zoom
var raw_radius := 5.0 # MOUTH_RING_RADIUS
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(1.5, view_zoom)
var floored: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(
raw_radius, stroke, view_zoom
)
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_radius, view_zoom)
assert_float(floored).override_failure_message(
"at a zoom where the naive radius is already far above the floor, the"
+ " floor must be a no-op — identical to zoom_compensated_size()"
).is_equal_approx(unfloored, 0.0001)
## A degenerate zero (or negative) view_zoom must not divide-by-zero/produce
## infinity/NaN — same total-function guarantee as the stroke-width sibling.
func test_zoom_compensated_ring_radius_zero_zoom_does_not_blow_up() -> void:
var stroke: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(1.5, 0.0)
var result: float = AtlasWindowGeometryNature.zoom_compensated_ring_radius(5.0, stroke, 0.0)
assert_bool(is_finite(result)).override_failure_message(
"a degenerate zero view_zoom must not produce inf/NaN"
).is_true()
@@ -263,6 +263,14 @@ const MOUTH_RING_RADIUS: float = 5.0
const MOUTH_HALO_RADIUS: float = 8.0
const MOUTH_HALO_ALPHA: float = 0.30
## T-1170 live round (2026-07-23, coordinator's mouth-ring blob finding):
## the minimum radius/stroke RATIO a draw_arc() ring needs to render hollow
## rather than degenerate into a solid blob — see
## zoom_compensated_ring_radius()'s own doc for the full A/B bracket
## evidence (radius=1x stroke -> blob, 1.5x -> hollow, floor set at 2x with
## margin over the observed transition).
const RING_RADIUS_STROKE_MULTIPLIER: float = 2.0
## Attractor minimum-strength gate — verbatim from the retired
## atlas_marker_overlay.gd GEN_ATTRACTOR_MIN_STRENGTH (:44). Region-only per
## the ruling (ATTRACTORS_VISIBLE_BY_RUNG), wave 1 has no attractor rendering
@@ -625,3 +633,53 @@ static func zoom_compensated_size(screen_space_size: float, view_zoom: float) ->
## zoom_compensated_size()'s own behavior at Region's tiny fit zoom.
static func zoom_compensated_stroke_width(screen_space_size: float, view_zoom: float) -> float:
return maxf(zoom_compensated_size(screen_space_size, view_zoom), 1.0)
## T-1170 live round (2026-07-23, coordinator's mouth-ring finding): the
## RADIUS-SMALLER-THAN-STROKE regime — a THIRD sibling to
## zoom_compensated_size()/zoom_compensated_stroke_width(), needed
## specifically for draw_arc() RING markers (mouth rings — the only
## draw_arc() caller in this file whose radius and stroke width are BOTH
## small, zoom-compensated values that can cross each other).
##
## Root-cause evidence (live A/B bracket, temporary instrumentation, since
## reverted — real running client via SR_LIVE=1, real x11/opengl3 driver):
## the ORIGINAL "zero ring pixels" report turned out to be a SEPARATE,
## already-correct-code issue — the terminus point legitimately sits near
## the requesting window's own edge, and the fit-and-center COVER strategy
## crops that edge off the visible viewport (screen_p verified computed as
## (1755, -191) against a 1920x1080 frame — above the top edge, not a
## drawing bug). Panning the view to re-center the SAME point (verified via
## AtlasWindowViewer.set_view()) proves the ring genuinely draws — but as a
## SOLID BLOB, not a hollow ring, at the production radius/stroke pair
## (radius=0.667, stroke=1.0 canvas-local units, Quarter fit zoom 7.5):
## draw_arc()'s stroke, centered ON the radius circle, extends inward past
## the circle's own center once stroke exceeds ~1x the radius, filling the
## hole. Bracket results (stroke fixed at 1.0 canvas-local, radius varied):
## radius=0.51 (~stroke/2) -> still a solid blob; radius=1.0 (=stroke) ->
## solid blob (the production case); radius=1.5 (1.5x stroke) -> hollow ring
## recovers; radius=2.0 (2x stroke) -> hollow ring, cleaner. The blob
## persists past the naive geometric threshold (radius > stroke/2, where an
## infinitely-thin/perfectly-antialiased ring would already have a hole)
## because draw_arc()'s low tessellation (18 points, this file's own call)
## plus antialiasing blur eat into the theoretical hole at these tiny
## absolute magnitudes — an empirical floor, not a derived one, chosen with
## margin over the observed 1.0x-blob/1.5x-hollow transition rather than
## shaving the boundary exactly.
##
## The fix: floor the RADIUS at `stroke * RING_RADIUS_STROKE_MULTIPLIER`
## (2.0, the top-of-file const — verified clean in the bracket above)
## whenever the naive zoom-compensated radius would fall below it — the
## same "floor the OUTPUT, never let a sub-threshold value reach Godot's
## renderer" pattern zoom_compensated_stroke_width() already established,
## applied to the paired radius/stroke relationship a lone-value floor
## can't express (unlike the stroke-width floor, this one's threshold is
## RELATIVE to another draw-time value, not an absolute constant). At every
## zoom where the naive radius already clears the floor on its own
## (Region's dot radii, or any District/Quarter case wide enough), this is
## an exact no-op — identical to calling zoom_compensated_size() directly.
static func zoom_compensated_ring_radius(
screen_space_radius: float, stroke_width_canvas_local: float, view_zoom: float
) -> float:
var naive_radius: float = zoom_compensated_size(screen_space_radius, view_zoom)
return maxf(naive_radius, stroke_width_canvas_local * RING_RADIUS_STROKE_MULTIPLIER)
@@ -362,6 +362,24 @@ func _zs_stroke(screen_space_size: float, ctx: Dictionary) -> float:
return AtlasWindowGeometryNature.zoom_compensated_stroke_width(screen_space_size, ctx["view_zoom"])
## T-1170 live round (2026-07-23, coordinator's mouth-ring blob finding): the
## RING-RADIUS-specific sibling of _zs()/_zs_stroke() — every draw_arc() ring
## marker whose radius and stroke are BOTH small, zoom-compensated values
## (currently: the two _draw_mouth() rings) routes its RADIUS through this
## instead of plain _zs(), so the radius never falls at-or-below its own
## paired stroke width and degenerates from a hollow ring into a solid blob.
## See AtlasWindowGeometryNature.zoom_compensated_ring_radius()'s own doc for
## the full A/B bracket evidence (radius=1x stroke -> blob, 1.5x -> hollow,
## floor set at 2x with margin). `stroke_canvas_local` is the ALREADY
## zoom-compensated stroke value (this function's own caller passes
## _zs_stroke()'s result, not a raw screen-space width) — the floor compares
## against the SAME canvas-local units the naive radius divide produces.
func _zs_ring_radius(screen_space_radius: float, stroke_canvas_local: float, ctx: Dictionary) -> float:
return AtlasWindowGeometryNature.zoom_compensated_ring_radius(
screen_space_radius, stroke_canvas_local, ctx["view_zoom"]
)
## Pixel (row, col) -> fractional district position, wrap-resolved against
## the currently-HELD view's own center (ctx["held_center"]) — the
## representative wrap-image _pos()'s canvas conversion needs. T-1172: this
@@ -616,14 +634,15 @@ func _segment_touches_drawn_water(from_district: Vector2, to_district: Vector2,
## also the marker for REAL course termini (Ruling 5b/3e) — one geometry
## function, both presentation surfaces.
func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
var ring_stroke: float = _zs_stroke(1.5, ctx)
draw_arc(
p,
_zs(AtlasWindowGeometryNature.MOUTH_RING_RADIUS, ctx),
_zs_ring_radius(AtlasWindowGeometryNature.MOUTH_RING_RADIUS, ring_stroke, ctx),
0.0,
TAU,
18,
COLOR_GEN_MOUTH,
_zs_stroke(1.5, ctx)
ring_stroke
)
var halo := Color(
COLOR_GEN_MOUTH.r,
@@ -631,8 +650,15 @@ func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
COLOR_GEN_MOUTH.b,
AtlasWindowGeometryNature.MOUTH_HALO_ALPHA
)
var halo_stroke: float = _zs_stroke(1.0, ctx)
draw_arc(
p, _zs(AtlasWindowGeometryNature.MOUTH_HALO_RADIUS, ctx), 0.0, TAU, 22, halo, _zs_stroke(1.0, ctx)
p,
_zs_ring_radius(AtlasWindowGeometryNature.MOUTH_HALO_RADIUS, halo_stroke, ctx),
0.0,
TAU,
22,
halo,
halo_stroke
)