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:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user