fix(ui): PR #205 review round — integer px-per-gridunit fit replaces fractional (T-1189, T-1192)
The fractional fit branch is deleted, resolving both review findings at the root: tyre showed its comments cited D-255 for an exception the record does not contain (the language came from the lead's ticket text, not governance), and hoshe showed it returned sub-1x for a canvas exceeding the viewport on one axis. Replacement: fit_scale_ratio() chooses the largest integer pixels-per-gridunit R fitting both legend-reserved axes, floored at 1 (over-viewport draws native and crops like every fixed rung) — the fine-grained integer lattice (GJ1c 1080p -> 9px/gu = 1593x792, ~98% width; 4K -> 20) that makes the fractional hatch unnecessary. center_offset() floors to whole pixels (half-pixel centering would blur the texel grid). Doc comments cite the real sanction (D-255 amendment 2026-07-25, this branch). Legend column constant is now canonical in transport, read directly by the legend (was an independently-typed literal); its test asserts real geometry. New regression test proves _global_body_extent clears on body switch and never caps another body's requests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,12 +71,24 @@ func test_reposition_sets_a_fixed_panel_margin_position() -> void:
|
||||
assert_that(legend.position).is_equal(Vector2(LegendScript.PANEL_MARGIN, 60.0))
|
||||
|
||||
|
||||
## T-1192: StepCanvasTransport.LEGEND_COLUMN_PX (the Global fit-scale
|
||||
## reservation StepCanvasViewer applies) must stay derived from this SAME
|
||||
## panel's own width/margin — a drift here would silently reopen the
|
||||
## "legend overlaps the canvas" defect on one side while the OTHER side
|
||||
## thinks it already reserved enough room.
|
||||
func test_reserved_column_px_matches_the_transport_sides_own_constant() -> void:
|
||||
assert_float(LegendScript.RESERVED_COLUMN_PX).is_equal_approx(
|
||||
StepCanvasTransport.LEGEND_COLUMN_PX, 0.01
|
||||
)
|
||||
## T-1192 review fix: RESERVED_COLUMN_PX is now a direct read of
|
||||
## StepCanvasTransport.LEGEND_COLUMN_PX (not an independently-typed
|
||||
## literal), so a const-vs-const cross-pin test would be structurally
|
||||
## unable to fail — it is the SAME value by construction. What's still
|
||||
## worth proving directly is the geometric guarantee that constant is FOR:
|
||||
## the legend's own ACTUAL laid-out right edge (`reposition()`'s position.x
|
||||
## + the panel's real minimum width) must land at or before the reserved
|
||||
## column, with room to spare — never right up against it, and certainly
|
||||
## never past it into where the canvas is centered from.
|
||||
func test_legend_actual_right_edge_stays_inside_the_reserved_column() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
var legend = LegendScript.new(v)
|
||||
auto_free(legend)
|
||||
legend.reposition()
|
||||
|
||||
var actual_right_edge: float = legend.position.x + legend.custom_minimum_size.x
|
||||
assert_float(actual_right_edge).override_failure_message(
|
||||
"the legend's real laid-out right edge must stay inside the column"
|
||||
+ " StepCanvasViewer reserves for it, with margin to spare"
|
||||
).is_less(StepCanvasTransport.LEGEND_COLUMN_PX)
|
||||
|
||||
@@ -280,72 +280,143 @@ func test_center_offset_goes_negative_when_the_canvas_overflows_the_viewport() -
|
||||
assert_float(offset.y).is_greater(0.0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1192: Global integer-fit scale — D-255 texel-exactness
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_integer_fit_scale_picks_the_largest_multiple_that_fits_both_axes() -> void:
|
||||
# GJ1c reference case: 177x88 texels at the 5x5 shallow display ratio =
|
||||
# 885x440 px footprint, fit against a full 1920x1080 viewport.
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(
|
||||
Vector2(885.0, 440.0), Vector2(1920.0, 1080.0)
|
||||
## D-255 texel-exactness: an integer-px/gridunit canvas (fit_scale_ratio())
|
||||
## can still land on an ODD-vs-viewport remainder that halves to a .5px
|
||||
## boundary — GJ1c's own 1593x792 footprint in a 1628x1080 available area
|
||||
## ((1628-1593)*0.5 = 17.5) is the real case this guards. The offset must be
|
||||
## FLOORED to a whole pixel, never left fractional (a fractional offset
|
||||
## would blur the texel grid this whole mechanism exists to keep crisp).
|
||||
func test_center_offset_floors_a_half_pixel_remainder_to_a_whole_pixel() -> void:
|
||||
var offset: Vector2 = StepCanvasTransport.center_offset(
|
||||
Vector2(1593.0, 792.0), Vector2(1628.0, 1080.0)
|
||||
)
|
||||
assert_int(scale).is_equal(2)
|
||||
assert_float(offset.x).is_equal_approx(17.0, 0.001)
|
||||
assert_float(offset.y).is_equal_approx(144.0, 0.001)
|
||||
|
||||
|
||||
func test_integer_fit_scale_is_bounded_by_the_tighter_axis() -> void:
|
||||
# Wide-but-short viewport: x could fit 4x, y only fits 1x — the smaller
|
||||
# =============================================================================
|
||||
# T-1192: Global integer PIXELS-PER-GRIDUNIT fit — D-255 amendment
|
||||
# 2026-07-25 (rung-0's display ratio is viewport-fitted per body to an
|
||||
# INTEGER px/gridunit ratio; the fractional-fit branch a prior round of this
|
||||
# ticket carried was a mis-citation of D-255 — the record's actual mandate
|
||||
# is unqualified texel-exact — and independently a real bug (Hoshe): a
|
||||
# canvas exceeding the viewport on one axis could fit_scale() down to
|
||||
# sub-1x, violating the "never below native resolution" invariant. Fixed at
|
||||
# the root by fitting the INTEGER ratio, not a fraction of the whole
|
||||
# footprint — this lattice is fine-grained (per gridunit, not per 5px-base
|
||||
# footprint step), so the coverage-threshold escape hatch this section used
|
||||
# to need does not come up: the achievable ratios are close enough together
|
||||
# that the largest one that fits is always a good use of the frame.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_fit_scale_ratio_picks_the_largest_px_per_gridunit_that_fits_both_axes() -> void:
|
||||
# GJ1c reference case (T-1183 eyeball): 177x88 gridunits against a full
|
||||
# 1920x1080 viewport (no legend reservation) — floor(1920/177)=10,
|
||||
# floor(1080/88)=12, the tighter axis (x) wins.
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(177.0, 88.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_int(ratio).is_equal(10)
|
||||
|
||||
|
||||
## The GJ1c reference case AFTER the legend column is reserved (T-1192, the
|
||||
## StepCanvasViewer end-to-end scenario): available area shrinks to
|
||||
## 1628x1080 — floor(1628/177)=9, floor(1080/88)=12 — the lead's own cited
|
||||
## reference number for this exact case.
|
||||
func test_fit_scale_ratio_matches_the_gj1c_legend_reserved_reference_case() -> void:
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(177.0, 88.0), Vector2(1628.0, 1080.0)
|
||||
)
|
||||
assert_int(ratio).is_equal(9)
|
||||
|
||||
|
||||
## GJ1c at 4K, legend column reserved (3840 - 292 = 3548 available) — the
|
||||
## lead's own cited reference number for a large viewport.
|
||||
func test_fit_scale_ratio_matches_the_gj1c_4k_reference_case() -> void:
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(177.0, 88.0), Vector2(3548.0, 2160.0)
|
||||
)
|
||||
assert_int(ratio).is_equal(20)
|
||||
|
||||
|
||||
## Hoshe's repro, now a regression test: a canvas whose BASE footprint
|
||||
## (885x440 px at the old 5x5-multiple framing) exceeds a narrow 348px-wide
|
||||
## available viewport used to make fit_scale() return 0.39x — a sub-1x
|
||||
## downscale violating "never below native resolution". The integer
|
||||
## px/gridunit ratio floors at 1 instead: draws at native (1 px/gridunit)
|
||||
## and crops/pans, exactly like every other fixed rung's own
|
||||
## exceeds-the-viewport precedent.
|
||||
func test_fit_scale_ratio_floors_at_one_when_gridunits_exceed_a_narrow_viewport() -> void:
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(177.0, 88.0), Vector2(348.0, 1080.0)
|
||||
)
|
||||
assert_int(ratio).is_equal(1)
|
||||
|
||||
|
||||
## A small moon's Global canvas (few gridunits) in a large viewport still
|
||||
## only wins as large an integer ratio as fits — no special-casing for a
|
||||
## small canvas, same formula, much larger achievable ratio.
|
||||
func test_fit_scale_ratio_a_tiny_moon_canvas_wins_a_large_integer_ratio() -> void:
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(20.0, 20.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_int(ratio).is_equal(54)
|
||||
|
||||
|
||||
## Exact-fit boundary: the viewport is PRECISELY `extent * 3` on both axes —
|
||||
## the ratio must land exactly on 3, not overshoot to 4 (floor(exact) must
|
||||
## not round up) and not undershoot to 2 (an exact multiple is a legal fit,
|
||||
## not treated as "just barely doesn't fit").
|
||||
func test_fit_scale_ratio_exact_multiple_boundary_lands_on_the_multiple() -> void:
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(100.0, 50.0), Vector2(300.0, 150.0)
|
||||
)
|
||||
assert_int(ratio).is_equal(3)
|
||||
|
||||
|
||||
## One pixel short of the exact multiple must drop to the NEXT integer down
|
||||
## — confirms the boundary isn't fuzzy/off-by-one in the other direction.
|
||||
func test_fit_scale_ratio_one_pixel_short_of_the_multiple_drops_a_step() -> void:
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(100.0, 50.0), Vector2(299.0, 150.0)
|
||||
)
|
||||
assert_int(ratio).is_equal(2)
|
||||
|
||||
|
||||
func test_fit_scale_ratio_is_bounded_by_the_tighter_axis() -> void:
|
||||
# Wide-but-short viewport: x could fit 10x, y only fits 1x — the smaller
|
||||
# wins (never overflow either axis).
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(
|
||||
Vector2(100.0, 100.0), Vector2(1000.0, 150.0)
|
||||
)
|
||||
assert_int(scale).is_equal(1)
|
||||
assert_int(ratio).is_equal(1)
|
||||
|
||||
|
||||
func test_integer_fit_scale_never_drops_below_one() -> void:
|
||||
# A canvas larger than the viewport still gets scale 1 (draw at native
|
||||
# size and let it exceed/crop), never a shrink below native.
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(
|
||||
Vector2(3000.0, 3000.0), Vector2(800.0, 600.0)
|
||||
)
|
||||
assert_int(scale).is_equal(1)
|
||||
func test_fit_scale_ratio_handles_a_zero_extent_axis_without_dividing_by_zero() -> void:
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(Vector2.ZERO, Vector2(800.0, 600.0))
|
||||
assert_int(ratio).is_equal(1)
|
||||
|
||||
|
||||
func test_integer_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> void:
|
||||
var scale: int = StepCanvasTransport.integer_fit_scale(Vector2.ZERO, Vector2(800.0, 600.0))
|
||||
assert_int(scale).is_equal(1)
|
||||
## fit_scale_from_ratio() converts the INTEGER px/gridunit ratio into the
|
||||
## `_canvas.scale` multiplier applied on top of a texture already rendered
|
||||
## at the rung's own base display ratio (5x5 for Global) — this multiplier
|
||||
## itself may be a non-integer float (9/5 = 1.8), and that is CORRECT:
|
||||
## texel-exactness is about the final ratio being a whole number, not the
|
||||
## Node2D scale field.
|
||||
func test_fit_scale_from_ratio_divides_by_the_base_display_ratio() -> void:
|
||||
var scale: float = StepCanvasTransport.fit_scale_from_ratio(9, 5.0)
|
||||
assert_float(scale).is_equal_approx(1.8, 0.001)
|
||||
|
||||
|
||||
func test_fit_scale_matches_the_integer_fit_when_coverage_is_high() -> void:
|
||||
# The GJ1c full-viewport case: integer 2x covers well over the
|
||||
# FIT_MIN_COVERAGE_RATIO bar, so fit_scale() must agree with
|
||||
# integer_fit_scale() exactly (no fractional fallback).
|
||||
var scale: float = StepCanvasTransport.fit_scale(
|
||||
Vector2(885.0, 440.0), Vector2(1920.0, 1080.0)
|
||||
)
|
||||
assert_float(scale).is_equal_approx(2.0, 0.001)
|
||||
func test_fit_scale_from_ratio_at_the_gj1c_4k_reference_case() -> void:
|
||||
var scale: float = StepCanvasTransport.fit_scale_from_ratio(20, 5.0)
|
||||
assert_float(scale).is_equal_approx(4.0, 0.001)
|
||||
|
||||
|
||||
## The GJ1c reference case AFTER the legend column is reserved (T-1192):
|
||||
## available area shrinks to 1628x1080, so the 2x integer candidate (1770 px
|
||||
## wide) no longer fits — integer_fit_scale() drops to 1x, which only covers
|
||||
## ~41% of the tighter available axis, well under FIT_MIN_COVERAGE_RATIO —
|
||||
## fit_scale() must fall back to the non-integer uniform fit that fills the
|
||||
## tighter (x) axis exactly, not settle for the sparse 1x frame.
|
||||
func test_fit_scale_falls_back_to_fractional_fit_on_excessive_letterboxing() -> void:
|
||||
var scale: float = StepCanvasTransport.fit_scale(
|
||||
Vector2(885.0, 440.0), Vector2(1628.0, 1080.0)
|
||||
)
|
||||
assert_float(scale).is_greater(1.0)
|
||||
assert_float(scale).is_less(2.0)
|
||||
# The fractional fit fills the tighter (x) axis exactly.
|
||||
assert_float(885.0 * scale).is_equal_approx(1628.0, 0.01)
|
||||
|
||||
|
||||
func test_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> void:
|
||||
var scale: float = StepCanvasTransport.fit_scale(Vector2.ZERO, Vector2(800.0, 600.0))
|
||||
assert_float(scale).is_equal_approx(1.0, 0.001)
|
||||
func test_fit_scale_from_ratio_handles_a_zero_base_ratio_without_dividing_by_zero() -> void:
|
||||
var scale: float = StepCanvasTransport.fit_scale_from_ratio(9, 0.0)
|
||||
assert_float(scale).is_greater(0.0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -353,9 +424,10 @@ func test_fit_scale_handles_a_zero_canvas_axis_without_dividing_by_zero() -> voi
|
||||
# =============================================================================
|
||||
|
||||
|
||||
func test_legend_column_px_is_positive_and_matches_the_legend_panels_own_sizing() -> void:
|
||||
# Pinned against step_canvas_legend.gd's own RESERVED_COLUMN_PX (260 +
|
||||
# 16*2 = 292) — the two constants must never drift apart, since the
|
||||
# "beside, never over" guarantee depends on both sides agreeing on the
|
||||
# SAME reserved width.
|
||||
## StepCanvasTransport is the CANONICAL source for this width (T-1192
|
||||
## review fix) — step_canvas_legend.gd's own RESERVED_COLUMN_PX is now a
|
||||
## direct read of THIS constant, not an independently-typed literal, so
|
||||
## there is no separate cross-pin test needed here; this just pins the
|
||||
## canonical value itself (260 panel width + 16*2 margin = 292).
|
||||
func test_legend_column_px_is_the_panel_width_plus_margin_on_both_sides() -> void:
|
||||
assert_float(StepCanvasTransport.LEGEND_COLUMN_PX).is_equal_approx(292.0, 0.01)
|
||||
|
||||
@@ -529,6 +529,42 @@ func test_district_request_extent_is_never_capped_by_the_global_extent() -> void
|
||||
assert_that(extent).is_equal(uncapped)
|
||||
|
||||
|
||||
## Hoshe (review round 2): _global_body_extent must NOT leak across a body
|
||||
## switch — land a Global extent for body A, enter() body B, and prove BOTH
|
||||
## that the cap source itself reads back ZERO for the new body AND that a
|
||||
## Region request for body B before ITS OWN Global echo lands goes out
|
||||
## UNCAPPED (never silently capped by body A's leftover grid). The reset
|
||||
## already exists at StepCanvasViewer.enter() ("a new body has its own
|
||||
## region grid") — this proves it, through the real enter()/land/enter()
|
||||
## sequence rather than asserting the field directly only.
|
||||
func test_global_body_extent_resets_on_a_different_body_and_does_not_leak() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
v.size = Vector2(1920.0, 1080.0)
|
||||
|
||||
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
||||
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
||||
assert_that(v._global_body_extent).override_failure_message(
|
||||
"test setup: body A must actually have a landed cap source"
|
||||
).is_equal(GJ1C_GLOBAL_EXTENT)
|
||||
|
||||
v.enter({"body_id": "T1189_extent_cap_body_b", "body_radius_km": 3000.0}, {})
|
||||
|
||||
assert_that(v._global_body_extent).override_failure_message(
|
||||
"entering a DIFFERENT body must reset the cap source to ZERO — body"
|
||||
+ " A's region grid must never leak into body B's requests"
|
||||
).is_equal(Vector2i.ZERO)
|
||||
|
||||
v._scroll_rung(1, Vector2(960.0, 540.0)) # Region, for body B — no Global echo yet
|
||||
var extent: Vector2i = v._request_extent()
|
||||
var uncapped: Vector2i = StepCanvasTransport.viewport_fit_extent(v.size, "Region")
|
||||
assert_that(extent).override_failure_message(
|
||||
"body B's Region request, before body B's own Global echo has"
|
||||
+ " landed, must go out UNCAPPED — never capped by body A's stale"
|
||||
+ " leftover region-grid extent"
|
||||
).is_equal(uncapped)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1189/T-1192: shared letterbox mechanism — centering + Global fit scale.
|
||||
# =============================================================================
|
||||
@@ -551,28 +587,46 @@ func test_global_canvas_arrival_centers_the_view_not_top_left() -> void:
|
||||
).is_not_equal(Vector2.ZERO)
|
||||
|
||||
|
||||
## D-255 texel-exactness (T-1192): at a large (4K-class) viewport, GJ1c's
|
||||
## 885x440 raw footprint (177x88 texels x 5x5 shallow display ratio) clears
|
||||
## the coverage bar even AFTER the legend column is reserved, so the
|
||||
## INTEGER fit wins outright — verified end-to-end through the real viewer
|
||||
## wiring, not just the pure transport function this mirrors. (The T-1183
|
||||
## reference 1920x1080 viewport is deliberately NOT used here — at that
|
||||
## size the legend-column reservation starves the integer candidate below
|
||||
## the coverage bar and the fractional escape hatch fires instead, covered
|
||||
## separately by test_global_canvas_uses_fractional_fit_when_legend_column_
|
||||
## starves_the_integer_fit() below.)
|
||||
func test_global_canvas_scale_is_an_integer_multiple_of_the_raw_footprint() -> void:
|
||||
## D-255 amendment 2026-07-25 texel-exactness: the `_canvas.scale` value
|
||||
## itself is NOT required to be a whole number (9/5 = 1.8 is entirely
|
||||
## legitimate) — what MUST be exact is the resulting on-screen
|
||||
## pixels-per-gridunit ratio. Verified end-to-end through the real viewer
|
||||
## wiring: `_canvas_scale * base_display_ratio` (the rung's own display
|
||||
## ratio, 5.0 for Global) must land on an exact integer, for both the
|
||||
## legend-reserved 1920x1080 case (R=9, scale=1.8) AND the 4K case (R=20,
|
||||
## scale=4.0) — the SAME formula, no separate coverage-threshold branch.
|
||||
func test_global_canvas_scale_yields_an_exact_integer_pixels_per_gridunit_ratio() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
v.size = Vector2(1920.0, 1080.0)
|
||||
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
||||
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
||||
|
||||
assert_that(v._canvas.scale).is_equal(Vector2(v._canvas_scale, v._canvas_scale))
|
||||
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
|
||||
var effective_px_per_gridunit: float = v._canvas_scale * base_ratio
|
||||
assert_float(effective_px_per_gridunit).override_failure_message(
|
||||
"the FINAL on-screen pixels-per-gridunit ratio must be an exact"
|
||||
+ " integer even when the _canvas.scale multiplier itself is not"
|
||||
).is_equal_approx(roundf(effective_px_per_gridunit), 0.001)
|
||||
# The lead's own cited reference number for this exact scenario.
|
||||
assert_float(effective_px_per_gridunit).is_equal_approx(9.0, 0.001)
|
||||
|
||||
|
||||
## Same invariant at a large (4K-class) viewport, where the integer ratio
|
||||
## (20) happens to make the _canvas.scale multiplier itself a whole number
|
||||
## too (20/5 = 4.0) — confirms the 1080p case above isn't a coincidence of
|
||||
## a small viewport, just the same formula at a different achievable ratio.
|
||||
func test_global_canvas_scale_at_4k_also_yields_an_exact_integer_ratio() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
v.size = Vector2(3840.0, 2160.0)
|
||||
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
||||
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
||||
|
||||
assert_that(v._canvas.scale).is_equal(Vector2(v._canvas_scale, v._canvas_scale))
|
||||
assert_float(v._canvas_scale).override_failure_message(
|
||||
"the Global fit scale must be a whole number of texture pixels"
|
||||
+ " when it clears the coverage bar (D-255 texel-exactness)"
|
||||
).is_equal_approx(roundf(v._canvas_scale), 0.001)
|
||||
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
|
||||
var effective_px_per_gridunit: float = v._canvas_scale * base_ratio
|
||||
assert_float(effective_px_per_gridunit).is_equal_approx(20.0, 0.001)
|
||||
|
||||
|
||||
## Fixed rungs must NEVER receive the Global fit multiplier — `_canvas.scale`
|
||||
@@ -632,23 +686,23 @@ func test_resize_recenters_an_already_held_global_canvas() -> void:
|
||||
).is_not_equal(offset_before)
|
||||
|
||||
|
||||
## Small-canvas fallback (D-255's own escape hatch): once the legend column
|
||||
## eats enough of the available width that the integer fit falls under
|
||||
## FIT_MIN_COVERAGE_RATIO, the viewer must fall back to the fractional fit
|
||||
## rather than settling for a sparse integer frame — end-to-end through the
|
||||
## real viewer, mirroring test_fit_scale_falls_back_to_fractional_fit_on_
|
||||
## excessive_letterboxing in test_step_canvas_transport.gd.
|
||||
func test_global_canvas_uses_fractional_fit_when_legend_column_starves_the_integer_fit() -> void:
|
||||
## Hoshe (review round 2): a narrow viewport where GJ1c's canvas exceeds
|
||||
## the available width on the gridunit lattice itself (177 gridunits >
|
||||
## available px after the legend column is reserved) used to make the OLD
|
||||
## fractional-fit branch return a sub-1x scale (0.39x) — a real downscale
|
||||
## below native resolution, violating "never below native". The integer
|
||||
## px/gridunit ratio floors at 1 instead: `_canvas_scale` must never drop
|
||||
## under 1.0, end-to-end through the real viewer wiring, not just the pure
|
||||
## transport function this mirrors.
|
||||
func test_global_canvas_scale_never_drops_below_native_on_a_narrow_viewport() -> void:
|
||||
var v: StepCanvasViewer = auto_free(StepCanvasViewer.new())
|
||||
add_child(v)
|
||||
v.size = Vector2(1920.0, 1080.0)
|
||||
v.size = Vector2(348.0 + StepCanvasTransport.LEGEND_COLUMN_PX, 1080.0)
|
||||
v.enter({"body_id": "T1189_extent_letterbox_test_body", "body_radius_km": 6371.0}, {})
|
||||
TestStepCanvasViewer._land_global_canvas(v, GJ1C_GLOBAL_EXTENT.x, GJ1C_GLOBAL_EXTENT.y)
|
||||
|
||||
# GJ1c at 1920x1080 with the legend column reserved: 2x no longer fits
|
||||
# (1770 > 1628 available), 1x covers only ~41% of the tighter axis — well
|
||||
# under the 75% coverage bar, so a non-integer fit must have been chosen.
|
||||
assert_float(v._canvas_scale).override_failure_message(
|
||||
"the reserved legend column must starve the 2x integer candidate at"
|
||||
+ " this reference viewport, forcing the fractional fit"
|
||||
).is_greater(1.0)
|
||||
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung("Global")
|
||||
assert_float(v._canvas_scale * base_ratio).override_failure_message(
|
||||
"the effective pixels-per-gridunit ratio must floor at 1 (native),"
|
||||
+ " never a sub-1x downscale, even on a viewport this narrow"
|
||||
).is_equal_approx(1.0, 0.001)
|
||||
|
||||
@@ -12,17 +12,19 @@ extends ImplantPanel
|
||||
## No `class_name` on purpose, matching every other viewer-owned helper in
|
||||
## this cluster.
|
||||
|
||||
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
|
||||
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
|
||||
|
||||
const PANEL_MARGIN: float = 16.0
|
||||
const LEGEND_PANEL_WIDTH: float = 260.0
|
||||
|
||||
## T-1192: must stay derivable from PANEL_MARGIN/LEGEND_PANEL_WIDTH above —
|
||||
## StepCanvasTransport.LEGEND_COLUMN_PX mirrors this exact sum so the
|
||||
## Global-rung fit-scale computation reserves precisely this much column,
|
||||
## never more or less than what the legend actually occupies.
|
||||
const RESERVED_COLUMN_PX: float = LEGEND_PANEL_WIDTH + PANEL_MARGIN * 2.0
|
||||
|
||||
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
|
||||
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
|
||||
## T-1192 review fix: StepCanvasTransport.LEGEND_COLUMN_PX is now the ONE
|
||||
## canonical source for this width — this file no longer computes its own
|
||||
## independently-typed literal sum. A read of the transport constant, not a
|
||||
## derivation, so the two sides can never drift apart by construction
|
||||
## (transport is static-only, no scene-tree dependency in this direction —
|
||||
## legend already preloads it above for AtlasOverlayColors-style helpers).
|
||||
const RESERVED_COLUMN_PX: float = StepCanvasTransport.LEGEND_COLUMN_PX
|
||||
|
||||
const MORPHOLOGY_FAMILY_ROWS: Array = [
|
||||
{"label": "water", "zones": [0, 1]},
|
||||
|
||||
@@ -39,8 +39,11 @@ extends Node2D
|
||||
##
|
||||
## **T-1192 outer fit scale:** the owning `_canvas` Node2D
|
||||
## (StepCanvasViewer._recompute_canvas_transform()) may additionally carry
|
||||
## its OWN `.scale` — the Global-rung integer-fit multiplier, always 1.0 for
|
||||
## every fixed rung — applied on top of this node's own texel-exact
|
||||
## its OWN `.scale` — for the Global rung, `StepCanvasTransport.
|
||||
## fit_scale_from_ratio()`'s per-body integer pixels-per-gridunit fit
|
||||
## (D-255 amendment 2026-07-25: rung-0's display ratio is viewport-fitted
|
||||
## per body to an INTEGER px/gridunit ratio, not a fixed 5x5); always 1.0
|
||||
## for every fixed rung — applied on top of this node's own texel-exact
|
||||
## `_footprint_px` draw. That is a SECOND texture-to-viewport resize, same
|
||||
## D-255(e) exemption, kept as a parent-transform multiply rather than a
|
||||
## second internal scale field so this node's own footprint math never has
|
||||
|
||||
@@ -85,27 +85,16 @@ const DISPLAY_RATIO_BY_RUNG: Dictionary = {
|
||||
## server-side, per step_canvas_protocol.gd's own doc).
|
||||
const FIXED_CANVAS_MAX_AXIS: int = 3_840
|
||||
|
||||
## T-1192: the on-screen column width reserved for the Atlas legend panel —
|
||||
## mirrors step_canvas_legend.gd's own LEGEND_PANEL_WIDTH + PANEL_MARGIN*2
|
||||
## (panel width plus a margin on each side). Shared here (not duplicated as
|
||||
## a raw literal in step_canvas_viewer.gd) so the Global integer-fit
|
||||
## computation and the legend's own sizing can never silently drift apart —
|
||||
## "legend beside, never over" only holds if both sides agree on the SAME
|
||||
## reserved width.
|
||||
## T-1192: the on-screen column width reserved for the Atlas legend panel
|
||||
## (panel width plus a margin on each side). THIS is the canonical value —
|
||||
## step_canvas_legend.gd's own RESERVED_COLUMN_PX is a direct read of this
|
||||
## constant (review fix: was previously an independently-typed literal sum
|
||||
## on the legend side, which could silently drift from this one), so the
|
||||
## Global integer-fit computation and the legend's own sizing can never
|
||||
## disagree by construction — "legend beside, never over" only holds if
|
||||
## both sides agree on the SAME reserved width.
|
||||
const LEGEND_COLUMN_PX: float = 260.0 + 16.0 * 2.0
|
||||
|
||||
## Below this fraction of the viewport's SMALLER axis covered, an integer
|
||||
## fit "leaves excessive letterboxing" (D-255's own phrase) and the
|
||||
## non-integer escape hatch fires instead. 0.75: the integer candidate must
|
||||
## already cover at least three-quarters of the tighter axis to win outright
|
||||
## — anything looser and the NEXT integer step down/up is visibly a better
|
||||
## use of the frame. Tuned against the GJ1c reference case at 1920x1080
|
||||
## (integer 1x candidate covers ~41% of the tighter available axis after the
|
||||
## legend-column reservation — well under this bar, so the fractional fit
|
||||
## wins there, matching D-255's own "if that leaves excessive letterboxing
|
||||
## on small canvases" framing). See fit_scale() below for the decision.
|
||||
const FIT_MIN_COVERAGE_RATIO: float = 0.75
|
||||
|
||||
|
||||
## The rung name at ladder index `i`, clamped to the legal [0, 5] range —
|
||||
## the one place RUNG_LADDER is indexed into, so a caller passing an
|
||||
@@ -220,49 +209,46 @@ static func canvas_footprint_px(rung: String, extent_cells: Vector2i) -> Vector2
|
||||
## than the viewport on an axis gets a zero/negative offset on that axis (no
|
||||
## letterbox needed there — it already fills or overflows, matching ordinary
|
||||
## pan-and-crop behavior on that axis rather than shrinking the canvas).
|
||||
## FLOORED to a whole pixel on each axis (D-255 texel-exactness): an
|
||||
## integer-px/gridunit canvas (fit_scale_ratio()) still produces an
|
||||
## odd-vs-even remainder half that can land on a .5px boundary — that would
|
||||
## reintroduce a fractional-pixel blur edge on the very rung this exists to
|
||||
## keep texel-exact, so the offset itself is snapped to the pixel grid.
|
||||
static func center_offset(footprint_px: Vector2, viewport_px: Vector2) -> Vector2:
|
||||
return (viewport_px - footprint_px) * 0.5
|
||||
var raw: Vector2 = (viewport_px - footprint_px) * 0.5
|
||||
return Vector2(floorf(raw.x), floorf(raw.y))
|
||||
|
||||
|
||||
## D-255 texel-exactness for the Global opener (T-1192): the largest INTEGER
|
||||
## scale multiple of `canvas_px` that still fits inside `viewport_px` on
|
||||
## BOTH axes, floored at 1 (never downscale below native size — a canvas
|
||||
## larger than the viewport draws at 1x and simply doesn't fit, matching
|
||||
## every fixed rung's own "canvas can exceed the viewport" precedent rather
|
||||
## than introducing a NEW sub-1x shrink path here). Callers needing the
|
||||
## "non-integer fit acceptable on small canvases" escape hatch (D-255's own
|
||||
## exception, nearest-neighbor only) compute their own fractional scale and
|
||||
## skip this function — it only ever returns integers by design, so it
|
||||
## can't accidentally hand back a blurry non-integer multiple.
|
||||
static func integer_fit_scale(canvas_px: Vector2, viewport_px: Vector2) -> int:
|
||||
if canvas_px.x <= 0.0 or canvas_px.y <= 0.0:
|
||||
## D-255 premise (2), texel-exact: the largest INTEGER pixels-per-gridunit
|
||||
## ratio `R` at which a `extent_cells`-gridunit canvas fits inside
|
||||
## `viewport_px` on BOTH axes, floored at 1 (never below native resolution
|
||||
## — an over-viewport canvas draws at 1 px/gridunit and crops/pans, same as
|
||||
## every fixed rung's own "canvas can exceed the viewport" precedent).
|
||||
## `R` is a PIXELS-PER-GRIDUNIT ratio, not a multiple of the whole footprint
|
||||
## — this is the fine-grained lattice D-255's amendment (2026-07-25)
|
||||
## clarifies rung-0's display ratio to be viewport-fitted-per-body against:
|
||||
## GJ1c's 177x88-gridunit canvas at a 1920x1080 viewport (legend column
|
||||
## reserved) lands on R=9 px/gridunit (1593x792, ~98% width), not a coarse
|
||||
## multiple of the BASE 5px/gridunit footprint (885/1770/2655 — the old,
|
||||
## too-coarse lattice that made a fractional escape hatch look necessary).
|
||||
static func fit_scale_ratio(extent_cells: Vector2, viewport_px: Vector2) -> int:
|
||||
if extent_cells.x <= 0.0 or extent_cells.y <= 0.0:
|
||||
return 1
|
||||
var max_x: int = int(floor(viewport_px.x / canvas_px.x))
|
||||
var max_y: int = int(floor(viewport_px.y / canvas_px.y))
|
||||
var max_x: int = int(floor(viewport_px.x / extent_cells.x))
|
||||
var max_y: int = int(floor(viewport_px.y / extent_cells.y))
|
||||
return maxi(1, mini(max_x, max_y))
|
||||
|
||||
|
||||
## The actual scale to draw the Global canvas at (T-1192): the integer fit
|
||||
## if it covers at least FIT_MIN_COVERAGE_RATIO of the viewport's tighter
|
||||
## axis, otherwise a UNIFORM fractional fit (same scale both axes, so the
|
||||
## canvas is never stretched non-uniformly) that fills the tighter axis
|
||||
## exactly. The fractional branch is legal ONLY under D-255's own
|
||||
## nearest-neighbor condition — StepCanvasTerrainLayer._filter_for_rung()
|
||||
## already forces NEAREST for every orbital rung (Global included)
|
||||
## unconditionally, so this function never has to check or set the filter
|
||||
## itself; it only chooses the number.
|
||||
static func fit_scale(canvas_px: Vector2, viewport_px: Vector2) -> float:
|
||||
if canvas_px.x <= 0.0 or canvas_px.y <= 0.0:
|
||||
return 1.0
|
||||
var int_scale: int = integer_fit_scale(canvas_px, viewport_px)
|
||||
var covered: Vector2 = canvas_px * float(int_scale)
|
||||
var coverage_x: float = covered.x / maxf(viewport_px.x, 0.0001)
|
||||
var coverage_y: float = covered.y / maxf(viewport_px.y, 0.0001)
|
||||
if minf(coverage_x, coverage_y) >= FIT_MIN_COVERAGE_RATIO:
|
||||
return float(int_scale)
|
||||
var frac_x: float = viewport_px.x / canvas_px.x
|
||||
var frac_y: float = viewport_px.y / canvas_px.y
|
||||
return maxf(minf(frac_x, frac_y), 0.0001)
|
||||
## Convert a target pixels-per-gridunit ratio `R` (fit_scale_ratio()'s
|
||||
## return) into the `_canvas.scale` multiplier applied ON TOP OF a texture
|
||||
## already rendered at `base_display_ratio` px/gridunit (canvas_footprint_px()
|
||||
## — the rung's own DISPLAY_RATIO_BY_RUNG entry). The multiplier itself may
|
||||
## be fractional (e.g. 9/5 = 1.8) — texel-exactness is NOT about the scale
|
||||
## factor being a whole number, it is about the FINAL on-screen pixel count
|
||||
## per gridunit (`R`) being an exact integer, so every source texel lands on
|
||||
## a whole number of screen pixels with no fractional-pixel blur boundary.
|
||||
static func fit_scale_from_ratio(ratio: int, base_display_ratio: float) -> float:
|
||||
return float(ratio) / maxf(base_display_ratio, 0.0001)
|
||||
|
||||
|
||||
## Fit a fixed-rung request's extent (in gridunits) to the viewport, capped
|
||||
|
||||
@@ -119,12 +119,15 @@ var _app_has_focus: bool = true
|
||||
## T-1192 Global fit scale — the ONE display-time scale this viewer ever
|
||||
## writes (see the class doc). Always 1.0 for a fixed rung (its canvas is
|
||||
## already texel-exact at its own display ratio; T-1189's letterbox only
|
||||
## adds centered margin, never an extra scale). For Global, an INTEGER
|
||||
## multiple when that covers most of the viewport (D-255 texel-exactness),
|
||||
## else a non-integer fractional fit on small canvases — see
|
||||
## StepCanvasTransport.fit_scale()'s own doc for the coverage rule; NEAREST
|
||||
## filtering (required for the non-integer case) is already unconditional
|
||||
## for every orbital rung via StepCanvasTerrainLayer._filter_for_rung().
|
||||
## adds centered margin, never an extra scale). For Global, the `_canvas.scale`
|
||||
## multiplier that puts the FINAL on-screen pixels-per-gridunit at the
|
||||
## largest INTEGER ratio that fits the (legend-reserved) viewport, floored
|
||||
## at 1 (D-255 amendment 2026-07-25 — rung-0's display ratio is
|
||||
## viewport-fitted per body to an integer, never a non-integer/sub-1x
|
||||
## fraction) — see StepCanvasTransport.fit_scale_ratio()'s own doc. The
|
||||
## multiplier itself (`ratio / base_display_ratio`) may be a non-integer
|
||||
## float — that is expected and correct, since texel-exactness is about the
|
||||
## RATIO being integer, not the Node2D scale field's raw value.
|
||||
var _canvas_scale: float = 1.0
|
||||
|
||||
# ── Overlay visibility ─────────────────────────────────────────────────────
|
||||
@@ -361,20 +364,24 @@ func _recompute_canvas_transform() -> void:
|
||||
_apply_transform()
|
||||
|
||||
|
||||
## The fit scale for a given raw (unscaled) footprint at the CURRENTLY held
|
||||
## rung — Global's fit_scale() reserving the legend column, 1.0 for every
|
||||
## fixed rung. Split out from _recompute_canvas_transform() so
|
||||
## _centered_view_offset() (the drift-check baseline) can share the exact
|
||||
## same scale decision without re-deriving it, keeping the two callers
|
||||
## structurally unable to disagree.
|
||||
## The `_canvas.scale` multiplier for a given raw (unscaled) footprint at
|
||||
## the CURRENTLY held rung — Global's per-body integer pixels-per-gridunit
|
||||
## fit (StepCanvasTransport.fit_scale_ratio()/fit_scale_from_ratio(),
|
||||
## reserving the legend column), 1.0 for every fixed rung. Split out from
|
||||
## _recompute_canvas_transform() so _centered_view_offset() (the drift-check
|
||||
## baseline) can share the exact same scale decision without re-deriving it,
|
||||
## keeping the two callers structurally unable to disagree.
|
||||
func _letterbox_scale_for(raw_footprint: Vector2) -> float:
|
||||
if _held_rung != StepCanvasTransport.RUNG_GLOBAL:
|
||||
return 1.0
|
||||
var base_ratio: float = StepCanvasTransport.display_ratio_for_rung(_held_rung)
|
||||
var extent_cells: Vector2 = raw_footprint / maxf(base_ratio, 0.0001)
|
||||
var viewport: Vector2 = get_rect().size
|
||||
var available: Vector2 = Vector2(
|
||||
maxf(viewport.x - StepCanvasTransport.LEGEND_COLUMN_PX, 1.0), viewport.y
|
||||
)
|
||||
return StepCanvasTransport.fit_scale(raw_footprint, available)
|
||||
var ratio: int = StepCanvasTransport.fit_scale_ratio(extent_cells, available)
|
||||
return StepCanvasTransport.fit_scale_from_ratio(ratio, base_ratio)
|
||||
|
||||
|
||||
func _rebuild_terrain_texture(canvas: Variant = null) -> void:
|
||||
|
||||
Reference in New Issue
Block a user