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:
+1
-1
@@ -7,7 +7,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **The Atlas map now fills the screen properly** (T-1189, T-1192) — the whole-body Global view fit-scales and centers in the viewport instead of sitting small in the top-left corner, and the legend sits beside the map instead of on top of it. The Region zoom step no longer shows the planet repeating side-by-side or smearing into stripes past the poles — the map view is capped at the body's actual size and letterboxed, so what you see is the planet once, correctly framed
|
||||
- **The Atlas map now fills the screen properly** (T-1189, T-1192) — the whole-body Global view now fills the frame at an integer texel scale (never blurry, never a sliver in the corner) and centers in the viewport, with the legend sitting beside the map instead of on top of it. The Region zoom step no longer shows the planet repeating side-by-side or smearing into stripes past the poles — the map view is capped at the body's actual size and letterboxed, so what you see is the planet once, correctly framed
|
||||
|
||||
### Added
|
||||
- **The Atlas remembers across restarts** (D-255, T-1183) — every map view you visit is now kept on disk as well as in memory: relaunch the game, reopen a body, and previously-visited zoom steps draw instantly from the local cache with no server round-trip — only genuinely new ground fetches. Storage stays tidy on its own: fine-grained views of a body you haven't visited in ~two weeks quietly reclaim their space, each body's deep-zoom footprint is capped, and the whole-body overview of every visited body is kept forever. The cache is update-safe by construction — entries from an older game version are silently refetched, never misread
|
||||
|
||||
@@ -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