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(
|
assert_bool(is_finite(result)).override_failure_message(
|
||||||
"a degenerate zero view_zoom must not produce inf/NaN"
|
"a degenerate zero view_zoom must not produce inf/NaN"
|
||||||
).is_true()
|
).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_RADIUS: float = 8.0
|
||||||
const MOUTH_HALO_ALPHA: float = 0.30
|
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
|
## Attractor minimum-strength gate — verbatim from the retired
|
||||||
## atlas_marker_overlay.gd GEN_ATTRACTOR_MIN_STRENGTH (:44). Region-only per
|
## atlas_marker_overlay.gd GEN_ATTRACTOR_MIN_STRENGTH (:44). Region-only per
|
||||||
## the ruling (ATTRACTORS_VISIBLE_BY_RUNG), wave 1 has no attractor rendering
|
## 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.
|
## 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:
|
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)
|
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"])
|
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
|
## Pixel (row, col) -> fractional district position, wrap-resolved against
|
||||||
## the currently-HELD view's own center (ctx["held_center"]) — the
|
## the currently-HELD view's own center (ctx["held_center"]) — the
|
||||||
## representative wrap-image _pos()'s canvas conversion needs. T-1172: this
|
## 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
|
## also the marker for REAL course termini (Ruling 5b/3e) — one geometry
|
||||||
## function, both presentation surfaces.
|
## function, both presentation surfaces.
|
||||||
func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
|
func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
|
||||||
|
var ring_stroke: float = _zs_stroke(1.5, ctx)
|
||||||
draw_arc(
|
draw_arc(
|
||||||
p,
|
p,
|
||||||
_zs(AtlasWindowGeometryNature.MOUTH_RING_RADIUS, ctx),
|
_zs_ring_radius(AtlasWindowGeometryNature.MOUTH_RING_RADIUS, ring_stroke, ctx),
|
||||||
0.0,
|
0.0,
|
||||||
TAU,
|
TAU,
|
||||||
18,
|
18,
|
||||||
COLOR_GEN_MOUTH,
|
COLOR_GEN_MOUTH,
|
||||||
_zs_stroke(1.5, ctx)
|
ring_stroke
|
||||||
)
|
)
|
||||||
var halo := Color(
|
var halo := Color(
|
||||||
COLOR_GEN_MOUTH.r,
|
COLOR_GEN_MOUTH.r,
|
||||||
@@ -631,8 +650,15 @@ func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
|
|||||||
COLOR_GEN_MOUTH.b,
|
COLOR_GEN_MOUTH.b,
|
||||||
AtlasWindowGeometryNature.MOUTH_HALO_ALPHA
|
AtlasWindowGeometryNature.MOUTH_HALO_ALPHA
|
||||||
)
|
)
|
||||||
|
var halo_stroke: float = _zs_stroke(1.0, ctx)
|
||||||
draw_arc(
|
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