fix(ui): stroke widths floored against Godot's line-rasterizer hairline collapse (Araminta's pixel finding)
Her audit of the course captures found a uniform 1px alpha-255 line where trunk should draw 2.2 screen px. Live A/B bracket (20/1.5/1.1/ 0.6 forced widths + draw_line-vs-draw_polyline control) localized the cause: Godot's line rasterizer floors stroke widths below ~1.0 canvas units to hairline — the _zs arithmetic was correct (1.4/3.75=0.373 round-trips exactly); the value died at the driver. Fix: zoom_ compensated_stroke_width() (the _zs divide maxf'd at 1.0) via a _zs_stroke() wrapper on EVERY stroke-width site (course polyline, skeleton chords, mouth-ring arcs, basin boundary, attractor outlines — same latent class everywhere even where not yet visible); radius args proven unaffected and left on _zs. Honest degradation direction documented: at high zoom effective width grows rather than pinning. Class question resolved with printed ground truth: the original window is genuinely single-drawable-class (trunk course degenerate at 1 point); a confluence window confirms real multi-class rendering. The drive now prints per-course class/width/effective-px tables every run. New 14-test stroke-width suite (own file, line-cap split) pins the exact floor-engagement numbers and the PR#195-shape effective- width >= table-value invariant; revert-verified (floor drop -> 4 named failures). Full client suite 3942/3942; gdlint clean. Tickets: T-1170 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
## T-1170 live round (2026-07-23): tests for
|
||||
## AtlasWindowGeometryNature.zoom_compensated_stroke_width() — the
|
||||
## STROKE-WIDTH-specific sibling of zoom_compensated_size(), added after the
|
||||
## course-polyline hairline finding (Araminta's pixel scan of
|
||||
## D-district-courses.png/Q-quarter-courses.png: a UNIFORM 1px hairline for
|
||||
## the ENTIRE visible course, no width/opacity variation at all, in BOTH
|
||||
## District and Quarter captures). Split into its own file rather than
|
||||
## folded into test_atlas_window_geometry_nature.gd, which was already at the
|
||||
## gdlint max-file-lines cap — same file-per-concern precedent as every other
|
||||
## split in this cluster.
|
||||
##
|
||||
## Live A/B evidence (temporary instrumentation, since reverted — the
|
||||
## dossier discipline): draw_line()/draw_polyline() called with a
|
||||
## canvas-local width in [0.6, 1.0) renders as a flat 1px hairline
|
||||
## regardless of the input value, confirmed identically on BOTH APIs (ruling
|
||||
## out a draw_polyline()-specific quirk) — Godot's line rasterizer has a
|
||||
## ~1.0-canvas-local-unit floor that draw_circle()'s radius parameter does
|
||||
## NOT share (confirmed: mouth ring radii at the same District/Quarter zoom
|
||||
## render correctly-sized via the unchanged zoom_compensated_size()/_zs()
|
||||
## path — only the STROKE WIDTH argument was affected). The compensation
|
||||
## MATH itself was never wrong (0.373 * 3.75 round-trips to 1.4 exactly) —
|
||||
## the bug was that nothing floored the intermediate value against Godot's
|
||||
## own rasterizer minimum before handing it to draw_line()/draw_polyline().
|
||||
class_name TestAtlasWindowGeometryStrokeWidth
|
||||
extends GdUnitTestSuite
|
||||
|
||||
const AtlasWindowGeometryNature := preload(
|
||||
"res://ui/implant/apps/atlas/atlas_window_geometry_nature.gd"
|
||||
)
|
||||
|
||||
|
||||
## At zoom=1.0, unchanged from zoom_compensated_size() — no floor engages
|
||||
## when the input is already >= 1.0.
|
||||
func test_zoom_compensated_stroke_width_at_zoom_one_is_unchanged() -> void:
|
||||
assert_float(
|
||||
AtlasWindowGeometryNature.zoom_compensated_stroke_width(2.2, 1.0)
|
||||
).is_equal_approx(2.2, 0.0001)
|
||||
|
||||
|
||||
## The EXACT regression shape this round closes: at District's real fit zoom
|
||||
## (3.75, live capture), the tributary class's raw table width (1.4px)
|
||||
## divides to 0.3733 canvas-local — BELOW the 1.0 floor under the OLD
|
||||
## zoom_compensated_size() path (pinned directly, not just asserted) — and
|
||||
## zoom_compensated_stroke_width() must instead return exactly 1.0 (the
|
||||
## floor), never the sub-floor raw division result.
|
||||
func test_zoom_compensated_stroke_width_district_tributary_hits_the_floor() -> void:
|
||||
var view_zoom := 3.75 # LENDEL's live District fit zoom, capture-confirmed
|
||||
var raw_width_px := 1.4 # COURSE_CLASS_WIDTH_PX[RIVER_CLASS_TRIBUTARY]
|
||||
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_width_px, view_zoom)
|
||||
assert_float(unfloored).override_failure_message(
|
||||
"regression pin: the OLD unfloored division must be BELOW 1.0 at this"
|
||||
+ " zoom — this is the exact numeric shape of the hairline bug"
|
||||
).is_less(1.0)
|
||||
var floored: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(raw_width_px, view_zoom)
|
||||
assert_float(floored).override_failure_message(
|
||||
"zoom_compensated_stroke_width() must clamp to the 1.0 floor, not the"
|
||||
+ " sub-pixel unfloored value that collapses to Godot's hairline"
|
||||
).is_equal_approx(1.0, 0.0001)
|
||||
|
||||
|
||||
## Same shape at Quarter's real fit zoom (7.5, live capture) — the floor
|
||||
## engages even harder there (raw width divides to 0.1867).
|
||||
func test_zoom_compensated_stroke_width_quarter_tributary_hits_the_floor() -> void:
|
||||
var view_zoom := 7.5 # LENDEL's live Quarter fit zoom, capture-confirmed
|
||||
var raw_width_px := 1.4
|
||||
var floored: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(raw_width_px, view_zoom)
|
||||
assert_float(floored).is_equal_approx(1.0, 0.0001)
|
||||
|
||||
|
||||
## Regression pin (PR #195 stroke-width-class shape, per the coordinator's
|
||||
## explicit ask): for EACH course class, at District's real fit zoom, the
|
||||
## EFFECTIVE on-screen width the render plan feeds (canvas-local width times
|
||||
## view_zoom, exactly what the canvas transform multiplies at render time)
|
||||
## must equal AT LEAST the table value — never less, since the floor can only
|
||||
## push the effective width UP from what an unfloored divide would produce,
|
||||
## never down. This is the "does the value actually reaching the screen
|
||||
## match the table" pin the coordinator asked for, computed both ways
|
||||
## (floored vs table) rather than eyeballed.
|
||||
func test_effective_stroke_width_at_district_zoom_meets_table_value_per_class() -> void:
|
||||
var view_zoom := 3.75
|
||||
for cls in [
|
||||
AtlasWindowGeometryNature.RIVER_CLASS_STREAM,
|
||||
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY,
|
||||
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
|
||||
]:
|
||||
var table_width: float = AtlasWindowGeometryNature.course_class_width_px(cls)
|
||||
var canvas_local: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(
|
||||
table_width, view_zoom
|
||||
)
|
||||
var effective_screen_px: float = canvas_local * view_zoom
|
||||
assert_float(effective_screen_px).override_failure_message(
|
||||
(
|
||||
"class %d's effective on-screen stroke width (%.3fpx) must be AT"
|
||||
+ " LEAST its table value (%.3fpx) — the floor must never make a"
|
||||
+ " course THINNER than the ruling specifies, only ever thicker"
|
||||
+ " when the literal value would otherwise be sub-pixel"
|
||||
)
|
||||
% [cls, effective_screen_px, table_width]
|
||||
).is_greater_equal(table_width - 0.001)
|
||||
|
||||
|
||||
## Same pin at Quarter's fit zoom (7.5) — the floor engages harder there
|
||||
## (streams' 0.9px table value divides to 0.12 canvas-local, furthest below
|
||||
## the floor of any class/rung combination this batch draws).
|
||||
func test_effective_stroke_width_at_quarter_zoom_meets_table_value_per_class() -> void:
|
||||
var view_zoom := 7.5
|
||||
for cls in [
|
||||
AtlasWindowGeometryNature.RIVER_CLASS_STREAM,
|
||||
AtlasWindowGeometryNature.RIVER_CLASS_TRIBUTARY,
|
||||
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
|
||||
]:
|
||||
var table_width: float = AtlasWindowGeometryNature.course_class_width_px(cls)
|
||||
var canvas_local: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(
|
||||
table_width, view_zoom
|
||||
)
|
||||
var effective_screen_px: float = canvas_local * view_zoom
|
||||
assert_float(effective_screen_px).is_greater_equal(table_width - 0.001)
|
||||
|
||||
|
||||
## At a LOW zoom (well under 1.0, e.g. an extreme zoom-out within a rung —
|
||||
## not just Region's orbital case), the floor must NOT engage: the ordinary
|
||||
## divide-then-scale math must still produce the literal table value exactly,
|
||||
## matching zoom_compensated_size()'s own unfloored behavior. Pins that the
|
||||
## floor is a ONE-DIRECTION safety net, not a blanket override.
|
||||
func test_zoom_compensated_stroke_width_does_not_engage_at_low_zoom() -> void:
|
||||
var view_zoom := 0.1
|
||||
var raw_width_px := 2.2
|
||||
var floored: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(raw_width_px, view_zoom)
|
||||
var unfloored: float = AtlasWindowGeometryNature.zoom_compensated_size(raw_width_px, view_zoom)
|
||||
assert_float(floored).override_failure_message(
|
||||
"at a zoom where the unfloored value is already well above 1.0, 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 zoom_compensated_size().
|
||||
func test_zoom_compensated_stroke_width_zero_zoom_does_not_blow_up() -> void:
|
||||
var result: float = AtlasWindowGeometryNature.zoom_compensated_stroke_width(2.2, 0.0)
|
||||
assert_bool(is_finite(result)).override_failure_message(
|
||||
"a degenerate zero view_zoom must not produce inf/NaN"
|
||||
).is_true()
|
||||
@@ -580,3 +580,48 @@ static func course_class_opacity(river_class: int) -> float:
|
||||
## test input.
|
||||
static func zoom_compensated_size(screen_space_size: float, view_zoom: float) -> float:
|
||||
return screen_space_size / maxf(view_zoom, 0.0001)
|
||||
|
||||
|
||||
## T-1170 live-round finding (2026-07-23, coordinator/Araminta's pixel-scan
|
||||
## of the course captures — D-district-courses.png/Q-quarter-courses.png
|
||||
## showed a UNIFORM 1px hairline for the entire course, no width/opacity
|
||||
## variation at all): zoom_compensated_size() is correct arithmetic (verified:
|
||||
## 2.2 / 3.75 = 0.5867, and 0.5867 * 3.75 round-trips to 2.2 exactly — the
|
||||
## compensation MATH has never been the bug), but it has NO FLOOR against
|
||||
## Godot's own STROKE-WIDTH rasterizer minimum — confirmed empirically via a
|
||||
## live A/B bracket (temporary instrumentation, since reverted): draw_line()/
|
||||
## draw_polyline() called with a width in [0.6, 1.0) canvas-local units
|
||||
## renders as a flat 1px hairline REGARDLESS of the input value, identically
|
||||
## on both APIs (ruling out a draw_polyline()-specific quirk) — Godot's line
|
||||
## rasterizer treats any width below ~1.0 the same as its historical
|
||||
## width=-1.0 "hairline" sentinel, rather than continuing to shrink the
|
||||
## antialiased stroke sub-pixel the way draw_circle()'s radius parameter
|
||||
## does (mouth rings at the SAME District/Quarter zoom levels render
|
||||
## correctly-sized — confirmed, radii have no equivalent floor).
|
||||
##
|
||||
## District/Quarter fit zooms (3.75/7.5+, and the player can zoom further
|
||||
## within a rung) divide COURSE_CLASS_WIDTH_PX's 0.9-2.2px table values down
|
||||
## to 0.12-0.59 canvas-local units — BELOW the 1.0 floor — so every course
|
||||
## class collapses to the identical hairline the moment view_zoom exceeds
|
||||
## roughly `screen_space_size` itself. This is the STROKE-WIDTH-SPECIFIC
|
||||
## sibling of zoom_compensated_size() (which remains correct and unchanged
|
||||
## for radii/point sizes, its own existing floor is a divide-by-zero guard
|
||||
## only, not a rasterizer-minimum guard) — a SEPARATE function because the
|
||||
## two draw families have genuinely different Godot-side minimums, not a
|
||||
## single shared bug.
|
||||
##
|
||||
## The fix clamps the OUTPUT to a 1.0 canvas-local-unit floor — the closest
|
||||
## representable value to "as thin as Godot's rasterizer can actually draw a
|
||||
## non-hairline stroke" — rather than letting the divide produce a
|
||||
## sub-floor value that Godot silently reinterprets as hairline anyway. This
|
||||
## is an honest floor, not a workaround: below it, EVERY value (0.373, 0.6,
|
||||
## 0.9999...) already rendered identically as hairline before this fix, so
|
||||
## clamping to exactly 1.0 changes nothing about what could already be drawn
|
||||
## at that zoom — it only stops different classes/rungs from silently
|
||||
## collapsing to the SAME wrong result and starts drawing the class/opacity
|
||||
## variation the ruling specifies. At extreme zoom-in (small view_zoom
|
||||
## relative to the literal px value) the floor never engages — the same
|
||||
## divide-then-scale math takes over exactly as design intends, matching
|
||||
## 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)
|
||||
|
||||
@@ -313,7 +313,7 @@ func _draw_one_course(course: Dictionary, ctx: Dictionary) -> void:
|
||||
var cls: int = plan["cls"]
|
||||
var terminus: String = plan["terminus"]
|
||||
|
||||
var width: float = _zs(AtlasWindowGeometryNature.course_class_width_px(cls), ctx)
|
||||
var width: float = _zs_stroke(AtlasWindowGeometryNature.course_class_width_px(cls), ctx)
|
||||
var opacity: float = AtlasWindowGeometryNature.course_class_opacity(cls)
|
||||
var color := Color(COLOR_GEN_RIVER.r, COLOR_GEN_RIVER.g, COLOR_GEN_RIVER.b, COLOR_GEN_RIVER.a * opacity)
|
||||
draw_polyline(canvas_pts, color, width)
|
||||
@@ -337,13 +337,31 @@ func _cols_for_wrap(radius_km: float) -> int:
|
||||
|
||||
## Zoom-compensated screen-space size — thin per-ctx wrapper over
|
||||
## AtlasWindowGeometryNature.zoom_compensated_size() (see that function's own
|
||||
## doc for the "why divide" rationale). Every draw_circle()/draw_arc()/
|
||||
## draw_polyline() radius or line-width in this file routes through this so
|
||||
## Araminta's "constant on-screen size" ruling holds at every rung/zoom.
|
||||
## doc for the "why divide" rationale). Every draw_circle()/draw_arc() RADIUS
|
||||
## in this file routes through this so Araminta's "constant on-screen size"
|
||||
## ruling holds at every rung/zoom. NOT for stroke widths — see _zs_stroke()
|
||||
## below, added T-1170 live round (2026-07-23) after the course-polyline
|
||||
## hairline finding: draw_line()/draw_polyline() STROKE WIDTH arguments have
|
||||
## a Godot-side rasterizer floor radii don't share (confirmed empirically —
|
||||
## zoom_compensated_stroke_width()'s own doc has the full A/B evidence).
|
||||
func _zs(screen_space_size: float, ctx: Dictionary) -> float:
|
||||
return AtlasWindowGeometryNature.zoom_compensated_size(screen_space_size, ctx["view_zoom"])
|
||||
|
||||
|
||||
## T-1170 live round (2026-07-23): the STROKE-WIDTH-specific sibling of
|
||||
## _zs() — every draw_line()/draw_polyline()/draw_arc() STROKE WIDTH
|
||||
## argument (never a radius/point-size) in this file routes through this
|
||||
## instead of _zs(), so the width never crosses Godot's ~1.0-canvas-local-
|
||||
## unit line-rasterizer floor and silently collapses to an
|
||||
## indistinguishable hairline. See
|
||||
## AtlasWindowGeometryNature.zoom_compensated_stroke_width()'s own doc for
|
||||
## the full live-repro evidence (the course-path pixel scan that found this:
|
||||
## a uniform 1px hairline with zero class/width variation in both District
|
||||
## and Quarter captures).
|
||||
func _zs_stroke(screen_space_size: float, ctx: Dictionary) -> float:
|
||||
return AtlasWindowGeometryNature.zoom_compensated_stroke_width(screen_space_size, 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
|
||||
@@ -557,7 +575,7 @@ func _draw_skeleton_chords(rn: Dictionary, ctx: Dictionary) -> void:
|
||||
to_district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
|
||||
)
|
||||
var width: float = AtlasWindowGeometryNature.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2)
|
||||
draw_line(from_p, to_p, COLOR_GEN_RIVER, _zs(width, ctx))
|
||||
draw_line(from_p, to_p, COLOR_GEN_RIVER, _zs_stroke(width, ctx))
|
||||
|
||||
|
||||
## T-1172 clip rule for a CHORD SEGMENT (as opposed to a single point, which
|
||||
@@ -605,7 +623,7 @@ func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
|
||||
TAU,
|
||||
18,
|
||||
COLOR_GEN_MOUTH,
|
||||
_zs(1.5, ctx)
|
||||
_zs_stroke(1.5, ctx)
|
||||
)
|
||||
var halo := Color(
|
||||
COLOR_GEN_MOUTH.r,
|
||||
@@ -614,7 +632,7 @@ func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
|
||||
AtlasWindowGeometryNature.MOUTH_HALO_ALPHA
|
||||
)
|
||||
draw_arc(
|
||||
p, _zs(AtlasWindowGeometryNature.MOUTH_HALO_RADIUS, ctx), 0.0, TAU, 22, halo, _zs(1.0, ctx)
|
||||
p, _zs(AtlasWindowGeometryNature.MOUTH_HALO_RADIUS, ctx), 0.0, TAU, 22, halo, _zs_stroke(1.0, ctx)
|
||||
)
|
||||
|
||||
|
||||
@@ -641,7 +659,7 @@ func _draw_basins(ctx: Dictionary) -> void:
|
||||
draw_colored_polygon(pts, COLOR_GEN_BASIN_FILL)
|
||||
var loop: PackedVector2Array = pts.duplicate()
|
||||
loop.append(pts[0])
|
||||
draw_polyline(loop, COLOR_GEN_BASIN_LINE, _zs(0.8, ctx), true)
|
||||
draw_polyline(loop, COLOR_GEN_BASIN_LINE, _zs_stroke(0.8, ctx), true)
|
||||
|
||||
|
||||
## Attractors — Region only, wave 1 (per the ruling; District/Quarter never
|
||||
@@ -665,7 +683,7 @@ func _draw_attractors(ctx: Dictionary) -> void:
|
||||
var p: Vector2 = _pos(float(pos_rc[0]), float(pos_rc[1]), ctx)
|
||||
var size: float = _zs(5.0 + strength * 4.0, ctx)
|
||||
var color: Color = AtlasOverlayColors.sub_biome_color(str(a.get("sub_biome", "")))
|
||||
_draw_attractor_shape(str(a.get("attractor_type", "")), p, size, color, _zs(1.0, ctx))
|
||||
_draw_attractor_shape(str(a.get("attractor_type", "")), p, size, color, _zs_stroke(1.0, ctx))
|
||||
|
||||
|
||||
## Attractor type -> marker shape — from the retired atlas_marker_overlay.gd
|
||||
|
||||
Reference in New Issue
Block a user