From f9ab3088ab288ed63b43c96fdf826173f0cf9a10 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Jul 2026 15:47:33 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20mouth-ring=20radius=20floored=20at?= =?UTF-8?q?=202x=20stroke=20=E2=80=94=20the=20radius-smaller-than-stroke?= =?UTF-8?q?=20draw=5Farc=20regime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...test_atlas_window_geometry_stroke_width.gd | 110 ++++++++++++++++++ .../atlas/atlas_window_geometry_nature.gd | 58 +++++++++ .../apps/atlas/atlas_window_nature_overlay.gd | 32 ++++- 3 files changed, 197 insertions(+), 3 deletions(-) diff --git a/client/tests/test_atlas_window_geometry_stroke_width.gd b/client/tests/test_atlas_window_geometry_stroke_width.gd index cb2c7ed11..9766199dc 100644 --- a/client/tests/test_atlas_window_geometry_stroke_width.gd +++ b/client/tests/test_atlas_window_geometry_stroke_width.gd @@ -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() diff --git a/client/ui/implant/apps/atlas/atlas_window_geometry_nature.gd b/client/ui/implant/apps/atlas/atlas_window_geometry_nature.gd index 7981ca333..9e8f5f8ff 100644 --- a/client/ui/implant/apps/atlas/atlas_window_geometry_nature.gd +++ b/client/ui/implant/apps/atlas/atlas_window_geometry_nature.gd @@ -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) diff --git a/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd b/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd index 40e465081..82998c9f4 100644 --- a/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd +++ b/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd @@ -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 )