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