fix(client): T-1153/T-1152 round 5 — nearest-wrap-image tile draw placement; canonical reset restores the fit zoom and re-fires
The wrapped tile column (Lendel: -6400 canonicalized to 12739) drew at its canonical column — off-canvas right — leaving the mosaic's left third black. nearest_wrap_image() re-expresses a tile column as the wrap-image closest to held_center for DRAWING only (requests/cache keys stay canonical). The draw-position test asserts overlap FRACTION, not bare intersects() — the buggy placement still clipped ~2px of viewport edge at Lendel scale, so intersects() alone would false-pass. The full-zoom-out reset restored center but not the fit zoom, and its 'already there' guard keyed on a lagging field so it could only ever fire once. The guard now also matches tile mode and compares _view_zoom against the freshly computed fit — restoring the FULL canonical transform (center, offset, fit zoom) and re-firing as a continued gesture keeps zooming out (Jeroen's hard condition, both live shapes). +8 revert-verified tests incl. the continued-gesture reset repro; smoke stub gained get_body_radius_km (crash confirmed real under a real driver before fixing). Suites 286 green; gdlint clean.
This commit is contained in:
@@ -744,6 +744,154 @@ func test_district_to_canvas_local_zero_held_n_does_not_crash() -> void:
|
|||||||
assert_that(result).is_equal(Vector2(5, 5) * CELL_PIXEL_SIZE)
|
assert_that(result).is_equal(Vector2(5, 5) * CELL_PIXEL_SIZE)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Live round 5: nearest_wrap_image() — the tile-mosaic WRAP half of "the
|
||||||
|
# mosaic doesn't fully draw" (the left-third-black repro).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
## Live round 5's OWN repro, pinned exactly: Lendel's wrapped tile
|
||||||
|
## canonicalizes to column 12739 (`-6400 mod 19139`) — the CORRECT
|
||||||
|
## request/cache key — but its nearest wrap-image relative to the canonical
|
||||||
|
## origin (held_center.x = 0) is -6400, the actual visible position
|
||||||
|
## immediately west of center.
|
||||||
|
func test_nearest_wrap_image_matches_the_lendel_repro() -> void:
|
||||||
|
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, 0, 19139)
|
||||||
|
assert_int(result).override_failure_message(
|
||||||
|
"the wrapped tile's nearest wrap-image relative to held_center=0 must be"
|
||||||
|
+ " -6400 (its actual on-screen position), not 12739 (the correct REQUEST"
|
||||||
|
+ " key, but the wrong DRAW position)"
|
||||||
|
).is_equal(-6400)
|
||||||
|
|
||||||
|
|
||||||
|
## The two Lendel tiles that were NEVER wrapped (already close to
|
||||||
|
## held_center) must round-trip unchanged — the fix must not perturb tiles
|
||||||
|
## that were already drawing correctly.
|
||||||
|
func test_nearest_wrap_image_is_a_noop_for_already_nearby_columns() -> void:
|
||||||
|
var cols := 19139
|
||||||
|
for col: int in [0, 6400]:
|
||||||
|
var result: int = AtlasWindowGeometry.nearest_wrap_image(col, 0, cols)
|
||||||
|
assert_int(result).override_failure_message(
|
||||||
|
"column %d is already the nearest wrap-image to held_center=0 — must"
|
||||||
|
+ " be returned unchanged" % col
|
||||||
|
).is_equal(col)
|
||||||
|
|
||||||
|
|
||||||
|
## The result must always be a LEGAL wrap-image of the canonical column —
|
||||||
|
## i.e. `result mod cols == canonical_col mod cols` — regardless of which
|
||||||
|
## image is nearest. This is the correctness invariant the whole function
|
||||||
|
## exists to preserve: re-expressing a column for DRAWING must never change
|
||||||
|
## WHICH district it actually refers to.
|
||||||
|
func test_nearest_wrap_image_preserves_the_canonical_identity() -> void:
|
||||||
|
var cols := 19139
|
||||||
|
for held_col: int in [-50000, -1, 0, 1, 9569, 19138, 50000]:
|
||||||
|
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, held_col, cols)
|
||||||
|
assert_int(posmod(result, cols)).override_failure_message(
|
||||||
|
"nearest_wrap_image(12739, %d, %d) = %d must still canonicalize back"
|
||||||
|
+ " to 12739 — it may only pick a DIFFERENT wrap-image, never a"
|
||||||
|
+ " different district" % [held_col, cols, result]
|
||||||
|
).is_equal(12739)
|
||||||
|
|
||||||
|
|
||||||
|
## The chosen wrap-image must be the CLOSEST one to held_center — never
|
||||||
|
## farther than half the circumference away (otherwise a different
|
||||||
|
## wrap-image would have been nearer).
|
||||||
|
func test_nearest_wrap_image_is_within_half_circumference_of_held_center() -> void:
|
||||||
|
var cols := 19139
|
||||||
|
for canonical_col: int in [0, 1, 9569, 12739, 19138]:
|
||||||
|
for held_col: int in [-30000, -500, 0, 500, 25000]:
|
||||||
|
var result: int = AtlasWindowGeometry.nearest_wrap_image(canonical_col, held_col, cols)
|
||||||
|
var distance: int = absi(result - held_col)
|
||||||
|
assert_int(distance).override_failure_message(
|
||||||
|
(
|
||||||
|
"nearest_wrap_image(%d, %d, %d) = %d is %d districts from"
|
||||||
|
+ " held_center — must never exceed half the circumference"
|
||||||
|
+ " (%d), or a closer wrap-image exists"
|
||||||
|
)
|
||||||
|
% [canonical_col, held_col, cols, result, distance, cols / 2]
|
||||||
|
).is_less_equal(cols / 2)
|
||||||
|
|
||||||
|
|
||||||
|
## `cols <= 0` (no-radius bodies, which never tile per compute_tile_grid()'s
|
||||||
|
## own doc) must be a safe no-op passthrough — no periodicity to resolve.
|
||||||
|
func test_nearest_wrap_image_zero_cols_is_a_passthrough() -> void:
|
||||||
|
var result: int = AtlasWindowGeometry.nearest_wrap_image(12739, 0, 0)
|
||||||
|
assert_int(result).is_equal(12739)
|
||||||
|
|
||||||
|
|
||||||
|
## The coordinator's own draw-position counterpart to
|
||||||
|
## test_compute_tile_grid_tiles_are_all_canonicalized(): the wrapped tile's
|
||||||
|
## DRAW rect (via district_to_canvas_local(), fed through
|
||||||
|
## nearest_wrap_image() the way _draw_tile_mosaic() now does) must land
|
||||||
|
## SUBSTANTIALLY on-canvas when the view covers the whole body — the exact
|
||||||
|
## Lendel shape (whole-body fit at entry, held_center at the canonical
|
||||||
|
## origin). A bare `Rect2.intersects()` check is NOT discriminating enough
|
||||||
|
## here: at Lendel's own whole-body-fit scale, the BUGGY placement (feeding
|
||||||
|
## the canonical column directly) happens to clip the viewport edge by only
|
||||||
|
## a couple of px (confirmed by hand-computation — the tile-grid's own
|
||||||
|
## edge-to-edge tiling means a full-circumference shift lands almost
|
||||||
|
## exactly one screen-width away, so `intersects()` alone would pass on a
|
||||||
|
## near-miss that still reads as "the left third is black" visually).
|
||||||
|
## Asserting a MEANINGFUL overlap FRACTION (at least half the tile's own
|
||||||
|
## area) is what actually distinguishes "correctly drawn" from "barely
|
||||||
|
## clipping the edge."
|
||||||
|
func test_wrapped_tile_draw_rect_lands_substantially_on_canvas_at_whole_body_view() -> void:
|
||||||
|
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
|
||||||
|
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
|
||||||
|
var cols: int = int(extent["cols"])
|
||||||
|
var held_center := Vector2i.ZERO
|
||||||
|
var held_n: int = cols # enter_orbital()'s own whole-body held_n
|
||||||
|
var tile_n: int = AtlasWindowGeometry.TILE_N
|
||||||
|
var half_tile: float = float(tile_n) * 0.5
|
||||||
|
|
||||||
|
# The whole-body fit zoom/viewport (matching enter_orbital()'s own fit).
|
||||||
|
var viewport := Vector2(1600.0, 900.0)
|
||||||
|
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
|
||||||
|
viewport, held_n, CELL_PIXEL_SIZE, 0.0001, 64.0
|
||||||
|
)
|
||||||
|
var view_zoom: float = fit["zoom"]
|
||||||
|
var view_offset: Vector2 = fit["offset"]
|
||||||
|
|
||||||
|
# The wrapped tile's own canonical center — mirrors compute_tile_grid()'s
|
||||||
|
# own dedup/canonicalize step for Lendel's westmost tile.
|
||||||
|
var wrapped_raw_col := -6400
|
||||||
|
var canonical_col: int = posmod(wrapped_raw_col, cols)
|
||||||
|
|
||||||
|
var draw_col: int = AtlasWindowGeometry.nearest_wrap_image(canonical_col, held_center.x, cols)
|
||||||
|
var tile_top_left := Vector2(float(draw_col) - half_tile, 0.0 - half_tile)
|
||||||
|
var local_origin: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
|
||||||
|
tile_top_left, held_center, held_n, CELL_PIXEL_SIZE
|
||||||
|
)
|
||||||
|
var extent_px: float = float(tile_n) * CELL_PIXEL_SIZE
|
||||||
|
|
||||||
|
# Canvas-local -> screen space: _canvas.position = view_offset,
|
||||||
|
# _canvas.scale = view_zoom (AtlasWindowViewer._apply_transform()'s own
|
||||||
|
# transform, mirrored here since this is a pure-geometry test with no
|
||||||
|
# live Control/Node2D tree).
|
||||||
|
var screen_top_left: Vector2 = view_offset + local_origin * view_zoom
|
||||||
|
var screen_extent: Vector2 = Vector2(extent_px, extent_px) * view_zoom
|
||||||
|
var tile_rect := Rect2(screen_top_left, screen_extent)
|
||||||
|
var viewport_rect := Rect2(Vector2.ZERO, viewport)
|
||||||
|
|
||||||
|
var overlap: Rect2 = viewport_rect.intersection(tile_rect)
|
||||||
|
var tile_area: float = screen_extent.x * screen_extent.y
|
||||||
|
var overlap_fraction: float = 0.0
|
||||||
|
if tile_area > 0.0:
|
||||||
|
overlap_fraction = (overlap.size.x * overlap.size.y) / tile_area
|
||||||
|
|
||||||
|
assert_float(overlap_fraction).override_failure_message(
|
||||||
|
(
|
||||||
|
"the wrapped tile's draw rect %s overlaps the viewport %s by only"
|
||||||
|
+ " %.1f%% of its own area — must be at least 50%% when the view"
|
||||||
|
+ " covers the whole body. This is live round 5's 'left third of the"
|
||||||
|
+ " mosaic is black' repro: drawing the CANONICAL column (%d) directly"
|
||||||
|
+ " (without nearest_wrap_image()) places this tile off-canvas RIGHT"
|
||||||
|
+ " instead of its true position on the LEFT"
|
||||||
|
)
|
||||||
|
% [tile_rect, viewport_rect, overlap_fraction * 100.0, canonical_col]
|
||||||
|
).is_greater_equal(0.5)
|
||||||
|
|
||||||
|
|
||||||
## The core contract this function exists for: recomputing `_view_offset` so
|
## The core contract this function exists for: recomputing `_view_offset` so
|
||||||
## a KNOWN screen point continues to map to canvas-local
|
## a KNOWN screen point continues to map to canvas-local
|
||||||
## `new_held_n/2 * cell_px` (the new window's own center) — i.e. feeding the
|
## `new_held_n/2 * cell_px` (the new window's own center) — i.e. feeding the
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ class _TileModeViewerStub:
|
|||||||
var tiles: Array = []
|
var tiles: Array = []
|
||||||
var held_center: Vector2i = Vector2i.ZERO
|
var held_center: Vector2i = Vector2i.ZERO
|
||||||
var held_n: int = 0
|
var held_n: int = 0
|
||||||
|
# Live round 5: nearest_wrap_image()'s cols input — 0 here (a no-radius
|
||||||
|
# passthrough) is fine for these tests, which don't exercise the wrap
|
||||||
|
# seam itself (that's test_atlas_window_geometry.gd's own coverage);
|
||||||
|
# this stub only needs to satisfy _draw_tile_mosaic()'s duck-typed call.
|
||||||
|
var body_radius_km: float = 0.0
|
||||||
|
|
||||||
func get_district_window() -> Variant:
|
func get_district_window() -> Variant:
|
||||||
return null
|
return null
|
||||||
@@ -112,6 +117,9 @@ class _TileModeViewerStub:
|
|||||||
func get_held_n() -> int:
|
func get_held_n() -> int:
|
||||||
return held_n
|
return held_n
|
||||||
|
|
||||||
|
func get_body_radius_km() -> float:
|
||||||
|
return body_radius_km
|
||||||
|
|
||||||
|
|
||||||
class _TileSetStub:
|
class _TileSetStub:
|
||||||
var _tiles: Array = []
|
var _tiles: Array = []
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ extends GdUnitTestSuite
|
|||||||
|
|
||||||
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
||||||
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
||||||
|
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
|
||||||
|
|
||||||
## Dudley's WINDOW_GRANULARITY_REGION_KEY (server/src/atlas/layer_proxy.rs) —
|
## Dudley's WINDOW_GRANULARITY_REGION_KEY (server/src/atlas/layer_proxy.rs) —
|
||||||
## `u32::MAX`, a RESERVED KEY-SPACE TAG the real server ALWAYS puts in the
|
## `u32::MAX`, a RESERVED KEY-SPACE TAG the real server ALWAYS puts in the
|
||||||
@@ -295,6 +296,138 @@ func test_reset_to_canonical_frame_fires_and_re_centers_when_fully_zoomed_out()
|
|||||||
).is_equal("Region")
|
).is_equal("Region")
|
||||||
|
|
||||||
|
|
||||||
|
## Live round 5's OWN repro, end to end: enter a TILING body's canonical
|
||||||
|
## frame, wheel-zoom IN far enough to cross out of tile mode (leaving
|
||||||
|
## `_held_granularity_v2` STALE at "Region" — a real, expected lag per
|
||||||
|
## `_maybe_reselect_rung()`'s own "does NOT touch _held_granularity_v2"
|
||||||
|
## doc, not a bug in that function), then wheel-zoom back OUT past the
|
||||||
|
## fully-zoomed-out threshold. The reset must fire and land EXACTLY on
|
||||||
|
## enter_orbital()'s own fit zoom for this body/viewport — not merely
|
||||||
|
## re-center while leaving `_view_zoom` wherever continued `_zoom_at()`
|
||||||
|
## scaling left it. Before the fix, the stale "Region" granularity
|
||||||
|
## satisfied the guard's OLD (center + granularity only) check forever,
|
||||||
|
## so the reset never fired again and `_view_zoom` kept shrinking via
|
||||||
|
## plain multiplication all the way to MIN_ZOOM.
|
||||||
|
func test_reset_after_crossing_out_and_back_snaps_to_the_canonical_fit_zoom() -> void:
|
||||||
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
||||||
|
add_child(v)
|
||||||
|
v.size = Vector2(1600.0, 900.0)
|
||||||
|
var radius_km := 6238.4 # GJ380c (Lendel) — a tiling body, the live-repro shape
|
||||||
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
|
||||||
|
assert_bool(v.is_tile_mode()).override_failure_message(
|
||||||
|
"sanity: Lendel must enter tile mode — this repro needs a TILING body,"
|
||||||
|
+ " since that's where _held_granularity_v2 can lag is_tile_mode()"
|
||||||
|
).is_true()
|
||||||
|
|
||||||
|
# Zoom IN far enough to cross out of tile mode (matching
|
||||||
|
# test_zoom_crossing_recomputes_view_offset_so_the_new_window_is_on_screen's
|
||||||
|
# own gesture shape).
|
||||||
|
var cursor_pos := Vector2(800.0, 450.0)
|
||||||
|
for _i in range(60):
|
||||||
|
v._zoom_at(cursor_pos, 1.15)
|
||||||
|
if not v.is_tile_mode():
|
||||||
|
break
|
||||||
|
assert_bool(v.is_tile_mode()).override_failure_message(
|
||||||
|
"sanity: this test needs to actually leave tile mode before zooming back out"
|
||||||
|
).is_false()
|
||||||
|
assert_str(v._held_granularity_v2).override_failure_message(
|
||||||
|
"sanity: _held_granularity_v2 must be STALE at Region here (no mock response"
|
||||||
|
+ " ever adopted a new value) — this is the exact lagging-field condition"
|
||||||
|
+ " the guard fix targets, not an artificial setup"
|
||||||
|
).is_equal("Region")
|
||||||
|
|
||||||
|
# Zoom back OUT past the fully-zoomed-out threshold — the reset must fire
|
||||||
|
# (possibly after a few more _zoom_at() ticks, matching a real wheel
|
||||||
|
# gesture rather than asserting it fires on the very first step back).
|
||||||
|
for _i in range(200):
|
||||||
|
v._zoom_at(cursor_pos, 1.0 / 1.05)
|
||||||
|
if v.is_tile_mode():
|
||||||
|
break
|
||||||
|
|
||||||
|
assert_bool(v.is_tile_mode()).override_failure_message(
|
||||||
|
"zooming back out past the threshold must re-fire the reset and land back"
|
||||||
|
+ " in tile mode — the stale-granularity guard bug left this permanently false"
|
||||||
|
).is_true()
|
||||||
|
assert_that(v._held_center).is_equal(Vector2i.ZERO)
|
||||||
|
|
||||||
|
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
|
||||||
|
var n: int = int(extent["cols"])
|
||||||
|
var expected_fit: Dictionary = AtlasWindowGeometry.fit_window_view(
|
||||||
|
v.size, n, AtlasWindowViewer.CELL_PIXEL_SIZE, AtlasWindowViewer.MIN_ZOOM, AtlasWindowViewer.MAX_ZOOM
|
||||||
|
)
|
||||||
|
assert_float(v._view_zoom).override_failure_message(
|
||||||
|
(
|
||||||
|
"post-reset _view_zoom (%.6f) must equal enter_orbital()'s own fit zoom"
|
||||||
|
+ " (%.6f) for this body/viewport — Jeroen's condition is the ORIGINAL"
|
||||||
|
+ " frame (center AND offset AND fit zoom), not merely re-centered at"
|
||||||
|
+ " whatever zoom continued _zoom_at() scaling left behind"
|
||||||
|
)
|
||||||
|
% [v._view_zoom, expected_fit["zoom"]]
|
||||||
|
).is_equal_approx(float(expected_fit["zoom"]), 0.000001)
|
||||||
|
|
||||||
|
|
||||||
|
## Live round 5's OWN live-drive repro, exactly: a REAL wheel gesture does
|
||||||
|
## NOT stop the instant the reset first fires — the coordinator's own
|
||||||
|
## tmp_drive_ladder.gd keeps sending wheel-down ticks toward a fixed target
|
||||||
|
## zoom (0.004, chosen below the fit zoom) regardless of the reset. This
|
||||||
|
## test reproduces that shape directly: continue zooming out PAST the point
|
||||||
|
## where the reset first re-enters tile mode, all the way to a target zoom
|
||||||
|
## BELOW the fit value. Before the second live-round-5 fix, `_view_zoom`
|
||||||
|
## drifted back down from the fit value on every subsequent `_zoom_at()`
|
||||||
|
## tick (ordinary multiplicative scaling doesn't care that a reset just
|
||||||
|
## happened) while the mode/center/granularity guard read "already
|
||||||
|
## canonical" and silently let it drift, landing on whatever the LOOP's
|
||||||
|
## target zoom happened to be instead of the fit value.
|
||||||
|
func test_reset_resnaps_even_after_continued_zoom_out_past_the_first_reset() -> void:
|
||||||
|
var v: AtlasWindowViewer = auto_free(AtlasWindowViewer.new())
|
||||||
|
add_child(v)
|
||||||
|
v.size = Vector2(1600.0, 900.0)
|
||||||
|
var radius_km := 6238.4 # GJ380c (Lendel) — the live-repro body
|
||||||
|
v.enter_orbital({"body_id": "GJ380c", "body_radius_km": radius_km}, {})
|
||||||
|
|
||||||
|
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
|
||||||
|
var n: int = int(extent["cols"])
|
||||||
|
var expected_fit: Dictionary = AtlasWindowGeometry.fit_window_view(
|
||||||
|
v.size, n, AtlasWindowViewer.CELL_PIXEL_SIZE, AtlasWindowViewer.MIN_ZOOM, AtlasWindowViewer.MAX_ZOOM
|
||||||
|
)
|
||||||
|
var fit_zoom: float = float(expected_fit["zoom"])
|
||||||
|
|
||||||
|
# Zoom IN far enough to leave tile mode (same shape as the test above).
|
||||||
|
var cursor_pos := Vector2(1100.0, 300.0) # matches tmp_drive_ladder.gd's own aim point
|
||||||
|
for _i in range(60):
|
||||||
|
v._zoom_at(cursor_pos, 1.15)
|
||||||
|
if not v.is_tile_mode():
|
||||||
|
break
|
||||||
|
assert_bool(v.is_tile_mode()).is_false()
|
||||||
|
|
||||||
|
# Zoom back OUT toward a target BELOW the fit zoom — matching
|
||||||
|
# tmp_drive_ladder.gd's own `_zoom_until(wv, 0.004, false)` exactly
|
||||||
|
# (Lendel's own fit zoom is ~0.00627, comfortably above this target),
|
||||||
|
# WITHOUT stopping early the moment tile mode is first regained. A real
|
||||||
|
# wheel gesture has no way to know when the reset internally fires.
|
||||||
|
var target_zoom := 0.004
|
||||||
|
for _i in range(200):
|
||||||
|
if v._view_zoom <= target_zoom:
|
||||||
|
break
|
||||||
|
v._zoom_at(cursor_pos, 1.0 / 1.05)
|
||||||
|
|
||||||
|
assert_bool(v.is_tile_mode()).override_failure_message(
|
||||||
|
"after continued zoom-out past the reset point, the view must settle back"
|
||||||
|
+ " into tile mode — a genuinely re-snapped canonical frame can't have zoomed"
|
||||||
|
+ " OUT further than the fit value in the first place"
|
||||||
|
).is_true()
|
||||||
|
assert_float(v._view_zoom).override_failure_message(
|
||||||
|
(
|
||||||
|
"post-reset _view_zoom (%.6f) must equal the canonical fit zoom (%.6f) even"
|
||||||
|
+ " though the wheel gesture continued past the point where the reset first"
|
||||||
|
+ " fired (target was %.6f, BELOW the fit zoom) — the guard must re-fire on"
|
||||||
|
+ " every subsequent tick where the zoom has drifted away from the fit value,"
|
||||||
|
+ " not just once"
|
||||||
|
)
|
||||||
|
% [v._view_zoom, fit_zoom, target_zoom]
|
||||||
|
).is_equal_approx(fit_zoom, 0.000001)
|
||||||
|
|
||||||
|
|
||||||
## Not fully zoomed out (a normal District-rung view) must NOT trigger the
|
## Not fully zoomed out (a normal District-rung view) must NOT trigger the
|
||||||
## reset — only reaching the top of the ladder resets, not every zoom step.
|
## reset — only reaching the top of the ladder resets, not every zoom step.
|
||||||
func test_reset_to_canonical_frame_does_not_fire_when_not_fully_zoomed_out() -> void:
|
func test_reset_to_canonical_frame_does_not_fire_when_not_fully_zoomed_out() -> void:
|
||||||
|
|||||||
@@ -406,6 +406,38 @@ static func district_to_canvas_local(
|
|||||||
return Vector2(local_col, local_row)
|
return Vector2(local_col, local_row)
|
||||||
|
|
||||||
|
|
||||||
|
## Live round 5 fix (the tile-mosaic WRAP half of "the mosaic doesn't fully
|
||||||
|
## draw"): `compute_tile_grid()`'s tiles are CANONICAL columns (wrapped into
|
||||||
|
## `[0, cols)` — the correct, single-valued key for REQUESTS and cache
|
||||||
|
## coalescing), but a canonical column has infinitely many EQUIVALENT
|
||||||
|
## on-screen positions (`col`, `col - cols`, `col + cols`, ...), since
|
||||||
|
## longitude is periodic. `district_to_canvas_local()` is a pure LINEAR
|
||||||
|
## function with no wrap concept — fed a canonical column directly, it
|
||||||
|
## places the tile at exactly ONE of those wrap-images, which is only ever
|
||||||
|
## the visually-correct one by coincidence. Lendel's own repro: the tile
|
||||||
|
## whose pre-canonicalization center was -6400 canonicalizes to 12739
|
||||||
|
## (`-6400 mod 19139`) — correct for the request/cache key, but drawing at
|
||||||
|
## column 12739 directly places it canvas-local ~22308 (off-canvas RIGHT),
|
||||||
|
## when the tile's actual visible position (immediately west of the
|
||||||
|
## canonical origin) is at column -6400 (canvas-local ~3169, the LEFT
|
||||||
|
## third of the mosaic).
|
||||||
|
##
|
||||||
|
## The fix: before handing a tile's canonical column to
|
||||||
|
## `district_to_canvas_local()`, re-express it as whichever wrap-image
|
||||||
|
## (`canonical_col + k*cols` for integer `k`) is NEAREST `held_center.x` —
|
||||||
|
## the representative that's actually near the current view, matching how a
|
||||||
|
## real, non-tiling single-window pan already resolves the "which
|
||||||
|
## circumnavigation" question implicitly (screen_center_to_district()'s own
|
||||||
|
## RAW, un-wrapped output). `cols <= 0` (no-radius bodies, which never tile
|
||||||
|
## per compute_tile_grid()'s own doc) is a safe no-op passthrough — there is
|
||||||
|
## no periodicity to resolve.
|
||||||
|
static func nearest_wrap_image(canonical_col: int, held_center_col: int, cols: int) -> int:
|
||||||
|
if cols <= 0:
|
||||||
|
return canonical_col
|
||||||
|
var delta: int = posmod(canonical_col - held_center_col + cols / 2, cols) - cols / 2
|
||||||
|
return held_center_col + delta
|
||||||
|
|
||||||
|
|
||||||
## Live round 4 fix (the SECOND half of the "pitch black" repro, beyond
|
## Live round 4 fix (the SECOND half of the "pitch black" repro, beyond
|
||||||
## district_to_canvas_local()'s tile-mosaic fix above): `_view_offset` is a
|
## district_to_canvas_local()'s tile-mosaic fix above): `_view_offset` is a
|
||||||
## PURE screen<->canvas-local transform, entirely independent of
|
## PURE screen<->canvas-local transform, entirely independent of
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ const REGION_TEMP_NONE_DC: int = AtlasOverlayColors.REGION_TEMP_NONE_DC
|
|||||||
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
||||||
# T-1153, live round 3: TILE_N (the per-tile district extent) for the mosaic draw path.
|
# T-1153, live round 3: TILE_N (the per-tile district extent) for the mosaic draw path.
|
||||||
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
|
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
|
||||||
|
# Live round 5: `cols` (circumference in districts) for the mosaic's wrap-image draw fix.
|
||||||
|
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
||||||
|
|
||||||
## T-1145 item 3: interim presentation toggle — true renders the smoothed
|
## T-1145 item 3: interim presentation toggle — true renders the smoothed
|
||||||
## Image/ImageTexture composite; false keeps the original crisp per-cell
|
## Image/ImageTexture composite; false keeps the original crisp per-cell
|
||||||
@@ -177,6 +179,21 @@ func _draw() -> void:
|
|||||||
## no per-tile placeholder draw, letting COLOR_BG show through as the honest
|
## no per-tile placeholder draw, letting COLOR_BG show through as the honest
|
||||||
## "nothing here yet" read (the viewer's own `_draw()` already documents why
|
## "nothing here yet" read (the viewer's own `_draw()` already documents why
|
||||||
## no separate whole-viewport fade is needed on top of this).
|
## no separate whole-viewport fade is needed on top of this).
|
||||||
|
##
|
||||||
|
## **Live round 5 fix:** `tile["center"]` is CANONICAL (wrapped into
|
||||||
|
## `[0, cols)` by `compute_tile_grid()` — correct for REQUESTS/cache keys,
|
||||||
|
## since longitude is periodic and a canonical column is the single-valued
|
||||||
|
## key both sides of the wire agree on). But `district_to_canvas_local()`
|
||||||
|
## is a pure LINEAR function with no wrap concept — handed a canonical
|
||||||
|
## column directly, it places the tile at exactly ONE of its infinitely
|
||||||
|
## many equivalent on-screen positions (`col + k*cols`), which is only the
|
||||||
|
## visually-correct one by coincidence. Lendel's own repro: the tile whose
|
||||||
|
## true position is immediately WEST of the canonical origin canonicalizes
|
||||||
|
## to column 12739 (`-6400 mod 19139`) — drawn there directly, it lands
|
||||||
|
## off-canvas RIGHT, leaving the mosaic's actual LEFT third black. Fixed by
|
||||||
|
## re-expressing each tile's column via `nearest_wrap_image()` — whichever
|
||||||
|
## wrap-image is closest to `held_center`, i.e. the one actually near the
|
||||||
|
## current view — BEFORE handing it to `district_to_canvas_local()`.
|
||||||
func _draw_tile_mosaic() -> void:
|
func _draw_tile_mosaic() -> void:
|
||||||
var tile_set = viewer.get_tile_set()
|
var tile_set = viewer.get_tile_set()
|
||||||
if tile_set == null:
|
if tile_set == null:
|
||||||
@@ -187,6 +204,9 @@ func _draw_tile_mosaic() -> void:
|
|||||||
var held_n: int = viewer.get_held_n()
|
var held_n: int = viewer.get_held_n()
|
||||||
var half_tile: float = float(AtlasWindowGeometryRef.TILE_N) * 0.5
|
var half_tile: float = float(AtlasWindowGeometryRef.TILE_N) * 0.5
|
||||||
var tiles: Array = tile_set.get_tiles()
|
var tiles: Array = tile_set.get_tiles()
|
||||||
|
var cols: int = int(
|
||||||
|
AtlasDescendGeometryRef.district_extent(viewer.get_body_radius_km()).get("cols", 0)
|
||||||
|
)
|
||||||
|
|
||||||
for i in range(tiles.size()):
|
for i in range(tiles.size()):
|
||||||
var tile: Dictionary = tiles[i]
|
var tile: Dictionary = tiles[i]
|
||||||
@@ -202,8 +222,9 @@ func _draw_tile_mosaic() -> void:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
var center: Vector2i = tile["center"]
|
var center: Vector2i = tile["center"]
|
||||||
|
var draw_col: int = AtlasWindowGeometryRef.nearest_wrap_image(center.x, held_center.x, cols)
|
||||||
var tile_top_left: Vector2 = Vector2(
|
var tile_top_left: Vector2 = Vector2(
|
||||||
float(center.x) - half_tile, float(center.y) - half_tile
|
float(draw_col) - half_tile, float(center.y) - half_tile
|
||||||
)
|
)
|
||||||
var local_origin: Vector2 = AtlasWindowGeometryRef.district_to_canvas_local(
|
var local_origin: Vector2 = AtlasWindowGeometryRef.district_to_canvas_local(
|
||||||
tile_top_left, held_center, held_n, cell_px
|
tile_top_left, held_center, held_n, cell_px
|
||||||
|
|||||||
@@ -63,15 +63,9 @@ const MAX_ZOOM: float = 64.0
|
|||||||
const ZOOM_STEP: float = 1.15
|
const ZOOM_STEP: float = 1.15
|
||||||
|
|
||||||
## T-1145 item 2: WASD/arrow-key continuous pan speed, in CANVAS px/s at
|
## T-1145 item 2: WASD/arrow-key continuous pan speed, in CANVAS px/s at
|
||||||
## zoom=1.0 — the ACTUAL screen-space pan rate is this value times the
|
## zoom=1.0 — actual screen-space rate is this times CURRENT _view_zoom, so
|
||||||
## CURRENT _view_zoom (see _process()'s pan tick), so panning covers the
|
## panning covers the same TERRAIN per second regardless of zoom level.
|
||||||
## same amount of TERRAIN per second regardless of zoom level. A fixed
|
## ~6 districts/s at zoom=1.0 (96/16) — brisk, not a crawl.
|
||||||
## SCREEN-px/s rate (no zoom scaling) would feel painfully slow zoomed in
|
|
||||||
## (each screen pixel is a fraction of a district) and uncontrollably fast
|
|
||||||
## zoomed out — scaling by zoom keeps the "how much world passes per
|
|
||||||
## second" feel constant, matching the ticket's "speed in screen px/s
|
|
||||||
## scaled by zoom" wording. ~6 districts/s at zoom=1.0 (96/16) — brisk
|
|
||||||
## enough to cross a default n=32 window in ~5s, not a crawl.
|
|
||||||
const PAN_SPEED_CANVAS_PX_S: float = 96.0
|
const PAN_SPEED_CANVAS_PX_S: float = 96.0
|
||||||
|
|
||||||
## T-1145 item 2: cursor-to-edge distance (px) that triggers edge-scroll —
|
## T-1145 item 2: cursor-to-edge distance (px) that triggers edge-scroll —
|
||||||
@@ -154,12 +148,10 @@ var _held_n: int = 32
|
|||||||
## T-1153: the granularity_v2 tag ("Quarter"/"District"/"Region") this viewer
|
## T-1153: the granularity_v2 tag ("Quarter"/"District"/"Region") this viewer
|
||||||
## is currently HOLDING (the last-adopted _window's own rung) — distinct from
|
## is currently HOLDING (the last-adopted _window's own rung) — distinct from
|
||||||
## _window_request.get_granularity_v2(), which is what's most recently been
|
## _window_request.get_granularity_v2(), which is what's most recently been
|
||||||
## REQUESTED (may be a finer/coarser rung already in flight while the held
|
## REQUESTED (may be a finer/coarser rung already in flight, per the
|
||||||
## composite is still the previous rung's, per the progressive-refinement
|
## progressive-refinement contract: hold the old composite, swap only when
|
||||||
## contract: hold the old composite, swap only when the new one arrives).
|
## the new one arrives). Defaults to District — the ladder's historical
|
||||||
## Defaults to District — the ladder's historical entry rung, and the correct
|
## entry rung (see enter()'s own doc).
|
||||||
## disposition for AtlasDescendGeometry click-through descent (still District,
|
|
||||||
## see enter()'s own doc).
|
|
||||||
var _held_granularity_v2: String = "District"
|
var _held_granularity_v2: String = "District"
|
||||||
|
|
||||||
# ── Pan/zoom state ─────────────────────────────────────────────────────────
|
# ── Pan/zoom state ─────────────────────────────────────────────────────────
|
||||||
@@ -255,20 +247,14 @@ func _exit_tree() -> void:
|
|||||||
SimBridge.atlas_layers_received.disconnect(_on_atlas_layers_received)
|
SimBridge.atlas_layers_received.disconnect(_on_atlas_layers_received)
|
||||||
|
|
||||||
|
|
||||||
## Enter the window screen centered on `district_center` (a DistrictPos, from
|
## Enter the window screen centered on `district_center` at District
|
||||||
## a click-through's derived position) at District granularity. n defaults to
|
## granularity. n defaults to 32, half the server's hard cap. Thin
|
||||||
## 32, half the server's hard cap. Kept as a thin District-rung wrapper over
|
## District-rung wrapper over _enter_at_rung() (T-1153) — a
|
||||||
## _enter_at_rung() (T-1153) — a click-to-descend-to-point shortcut on top of
|
## click-to-descend-to-point shortcut; survives as a direct-call test entry.
|
||||||
## the continuous ladder (Jeroen's ruling). No screen currently calls this
|
|
||||||
## directly (the retired planetary click-through it served no longer exists
|
|
||||||
## — see atlas_app.gd's own doc); it survives as the landing point a future
|
|
||||||
## map-object click would wire into, and as a direct-call entry for tests.
|
|
||||||
##
|
##
|
||||||
## T-1142: `district_center` is canonicalized (wrap column / clamp row)
|
## T-1142: `district_center` is canonicalized (wrap column / clamp row)
|
||||||
## BEFORE it becomes `_held_center` or reaches the request — matching the
|
## BEFORE it becomes `_held_center` or reaches the request — matching the
|
||||||
## server's normalize_window_center() so the client's echo comparison never
|
## server's normalize_window_center() so the echo comparison never mismatches.
|
||||||
## mismatches. Also fits-and-centers the view instead of resetting to
|
|
||||||
## zoom=1/offset=ZERO.
|
|
||||||
func enter(
|
func enter(
|
||||||
body: Dictionary,
|
body: Dictionary,
|
||||||
system: Dictionary,
|
system: Dictionary,
|
||||||
@@ -283,28 +269,19 @@ func enter(
|
|||||||
|
|
||||||
|
|
||||||
## T-1153: enter the ladder at its TOP REST STATE — the canonical orbital
|
## T-1153: enter the ladder at its TOP REST STATE — the canonical orbital
|
||||||
## frame (Jeroen's HARD condition: "the whole body fitted to the canvas,
|
## frame (Jeroen's HARD condition: whole body fitted to canvas, centered at
|
||||||
## centered at the body's canonical origin"). This is the "regional" nav
|
## the canonical origin). The "regional" nav entry point (T-1152 client
|
||||||
## entry point (T-1152 client half): the player lands on a fully-derived
|
## half): the player lands on a fully-derived Region-rung view of the whole
|
||||||
## Region-rung view of the whole body, then wheel-zoom descends CONTINUOUSLY
|
## body, then wheel-zoom descends CONTINUOUSLY from there.
|
||||||
## from there — no separate planetary screen, no click-through required
|
|
||||||
## (though enter() below stays wired for a click-to-descend shortcut, per
|
|
||||||
## Jeroen's ruling).
|
|
||||||
##
|
##
|
||||||
## Canonical origin = district (0,0) — "district (0,0) sits at lon 0 / the
|
## Canonical origin = district (0,0) — same quantity
|
||||||
## equator" (AtlasDescendGeometry's own doc). Canonical extent = the WHOLE
|
## is_fully_zoomed_out()/_maybe_reset_to_canonical_frame() test against.
|
||||||
## equatorial circumference in districts, the same quantity
|
## No-radius bodies fall back to the District-rung default window.
|
||||||
## is_fully_zoomed_out()/_maybe_reset_to_canonical_frame() test against, so
|
|
||||||
## entry and reset always agree on what "the top" means. No-radius bodies
|
|
||||||
## fall back to the District-rung default window (no circumference concept).
|
|
||||||
##
|
##
|
||||||
## **Live round 3 (design doc §4): the rest state must TILE.** A single
|
## **Live round 3 (design doc §4): the rest state must TILE.** A single
|
||||||
## wire-capped Region window covers at most
|
## wire-capped Region window covers only a fraction of a real body's
|
||||||
## `AtlasWindowGeometry.MAX_COVERAGE_M["Region"]`, a fraction of a real
|
## circumference. Once `compute_tile_grid()` returns MORE than one tile,
|
||||||
## body's circumference. Once `compute_tile_grid()` returns MORE than one
|
## entry goes through `_enter_tile_mode()` instead of `_enter_at_rung()`.
|
||||||
## tile, entry goes through `_enter_tile_mode()` instead of
|
|
||||||
## `_enter_at_rung()`; a body whose circumference fits one Region window's
|
|
||||||
## ceiling still gets exactly one "tile" and stays single-window.
|
|
||||||
func enter_orbital(body: Dictionary, system: Dictionary) -> void:
|
func enter_orbital(body: Dictionary, system: Dictionary) -> void:
|
||||||
var radius_km: float = float(body.get("body_radius_km", 0.0))
|
var radius_km: float = float(body.get("body_radius_km", 0.0))
|
||||||
if radius_km <= 0.0:
|
if radius_km <= 0.0:
|
||||||
@@ -355,13 +332,8 @@ func _enter_tile_mode(body: Dictionary, system: Dictionary, radius_km: float) ->
|
|||||||
##
|
##
|
||||||
## **C1 clamp-mirror, one layer up (live-round finding):** `n` MUST be
|
## **C1 clamp-mirror, one layer up (live-round finding):** `n` MUST be
|
||||||
## clamped via `_clamp_window_n_mirror_v2()` BEFORE it becomes `_held_n` —
|
## clamped via `_clamp_window_n_mirror_v2()` BEFORE it becomes `_held_n` —
|
||||||
## mirroring what AtlasWindowRequest.request_now() already does to ITS OWN
|
## mirroring AtlasWindowRequest.request_now()'s own `_n` clamp (PR #191 Tyre
|
||||||
## `_n` (PR #191 Tyre C1). Storing RAW `n` (e.g. enter_orbital()'s full
|
## C1). Raw `n` left `_held_n` disagreeing with the server's clamped echo.
|
||||||
## district_extent().cols, tens of thousands at Region, vs. the server's
|
|
||||||
## clamped echo of at most 6,400) left `_held_n` permanently disagreeing
|
|
||||||
## with the server's echo — every orbital response silently rejected as
|
|
||||||
## stale forever. `_maybe_reselect_rung()`/`_maybe_refloat_window()` both
|
|
||||||
## read `_held_n` unchanged, so clamping here fixes every downstream caller.
|
|
||||||
func _enter_at_rung(
|
func _enter_at_rung(
|
||||||
body: Dictionary,
|
body: Dictionary,
|
||||||
system: Dictionary,
|
system: Dictionary,
|
||||||
@@ -424,8 +396,7 @@ func leave() -> void:
|
|||||||
|
|
||||||
|
|
||||||
## Named get_district_window(), NOT get_window() — Node already defines
|
## Named get_district_window(), NOT get_window() — Node already defines
|
||||||
## get_window() -> Window (the containing OS window); shadowing it with an
|
## get_window() -> Window; shadowing it with an incompatible type errors.
|
||||||
## incompatible return type is a Godot parse error (confirmed the hard way).
|
|
||||||
func get_district_window() -> Variant:
|
func get_district_window() -> Variant:
|
||||||
return _window
|
return _window
|
||||||
|
|
||||||
@@ -444,9 +415,8 @@ func get_tile_set() -> Variant:
|
|||||||
|
|
||||||
|
|
||||||
## Live round 4: currently-HELD reference frame — AtlasWindowOverlay's
|
## Live round 4: currently-HELD reference frame — AtlasWindowOverlay's
|
||||||
## mosaic draw path converts each tile's absolute district center into
|
## mosaic draw path converts tile centers into canvas-local space via these
|
||||||
## canvas-local space via these (see AtlasWindowGeometry.
|
## (see AtlasWindowGeometry.district_to_canvas_local()'s own doc).
|
||||||
## district_to_canvas_local()'s own doc for the shared convention).
|
|
||||||
func get_held_center() -> Vector2i:
|
func get_held_center() -> Vector2i:
|
||||||
return _held_center
|
return _held_center
|
||||||
|
|
||||||
@@ -455,6 +425,13 @@ func get_held_n() -> int:
|
|||||||
return _held_n
|
return _held_n
|
||||||
|
|
||||||
|
|
||||||
|
## Live round 5: current body's radius — mosaic draw needs `cols` for
|
||||||
|
## nearest_wrap_image()'s wrap resolution. Mirrors the
|
||||||
|
## `_body.get("body_radius_km", 0.0)` pattern used throughout this file.
|
||||||
|
func get_body_radius_km() -> float:
|
||||||
|
return float(_body.get("body_radius_km", 0.0))
|
||||||
|
|
||||||
|
|
||||||
## District-cell pixel size at zoom=1.0 — AtlasWindowOverlay reads this
|
## District-cell pixel size at zoom=1.0 — AtlasWindowOverlay reads this
|
||||||
## rather than hardcoding CELL_PIXEL_SIZE itself, so the viewer stays the
|
## rather than hardcoding CELL_PIXEL_SIZE itself, so the viewer stays the
|
||||||
## single source of geometry truth (same "viewer owns the transform, overlay
|
## single source of geometry truth (same "viewer owns the transform, overlay
|
||||||
@@ -587,19 +564,19 @@ func _current_world_extent_m() -> float:
|
|||||||
## §5 rung-selection rule + progressive refinement (T-1153): after a zoom
|
## §5 rung-selection rule + progressive refinement (T-1153): after a zoom
|
||||||
## step, recompute the legal rung for the NOW-displayed world extent. If it
|
## step, recompute the legal rung for the NOW-displayed world extent. If it
|
||||||
## differs from what's HELD, request the new granularity centered on the
|
## differs from what's HELD, request the new granularity centered on the
|
||||||
## CURRENT screen-center (_screen_center_district(), the same formula
|
## CURRENT screen-center (_screen_center_district(), same formula as
|
||||||
## _maybe_refloat_window() uses).
|
## _maybe_refloat_window()).
|
||||||
##
|
##
|
||||||
## **C1 clamp-mirror, a THIRD layer up (live round 3):** `_held_n` MUST be
|
## **C1 clamp-mirror, a THIRD layer up (live round 3):** `_held_n` MUST be
|
||||||
## re-clamped via `_clamp_window_n_mirror_v2()` for the TARGET rung, not left
|
## re-clamped via `_clamp_window_n_mirror_v2()` for the TARGET rung — a stale
|
||||||
## at the PREVIOUS rung's clamp — crossing rungs changes the clamp ceiling,
|
## large `_held_n` desyncs `_on_window_ready()`'s `w_n != _held_n` check and
|
||||||
## so a stale large `_held_n` fed into a smaller-rung request desyncs
|
## drops the refinement forever.
|
||||||
## `_on_window_ready()`'s `w_n != _held_n` check and drops the refinement
|
|
||||||
## forever. Same bug as _enter_at_rung(), recurring at the CROSSING boundary.
|
|
||||||
##
|
##
|
||||||
## Progressive refinement: does NOT touch `_window`/`_held_granularity_v2` —
|
## Progressive refinement: does NOT touch `_window`/`_held_granularity_v2` —
|
||||||
## the OLD composite keeps drawing until _on_window_ready() adopts the new
|
## the OLD composite keeps drawing until _on_window_ready() adopts the new
|
||||||
## one (§6 "no mode flip": never a blank frame, never clear-then-redraw).
|
## one (§6 "no mode flip"). Live round 5: this lag is exactly what made
|
||||||
|
## `_maybe_reset_to_canonical_frame()`'s OLD guard misfire — see that
|
||||||
|
## function's own doc.
|
||||||
func _maybe_reselect_rung() -> void:
|
func _maybe_reselect_rung() -> void:
|
||||||
if _held_n <= 0:
|
if _held_n <= 0:
|
||||||
return
|
return
|
||||||
@@ -653,10 +630,26 @@ func _screen_center_district() -> Vector2i:
|
|||||||
## Jeroen's HARD condition: "a full zoom-out resets to the original
|
## Jeroen's HARD condition: "a full zoom-out resets to the original
|
||||||
## canonical planetary frame and location" — the ladder's TOP REST STATE,
|
## canonical planetary frame and location" — the ladder's TOP REST STATE,
|
||||||
## never a drifted pan/zoom-out state. Fires when the CURRENTLY DISPLAYED
|
## never a drifted pan/zoom-out state. Fires when the CURRENTLY DISPLAYED
|
||||||
## extent (at _held_granularity_v2, deliberately NOT the in-flight request's
|
## extent covers the whole body AND the player isn't ALREADY at the
|
||||||
## rung) covers the whole body AND the player isn't ALREADY at the canonical
|
## canonical frame (re-snapping every tick would fight a zoom-in-from-the-
|
||||||
## frame (re-snapping every tick would fight a zoom-in-from-the-top
|
## top gesture). Returns true if it fired (caller skips _maybe_reselect_rung()).
|
||||||
## gesture). Returns true if it fired (caller skips _maybe_reselect_rung()).
|
##
|
||||||
|
## **Live round 5 fix:** the "already there" guard checked only
|
||||||
|
## `_held_center`/`_held_granularity_v2` — a LAGGING field (updated only on
|
||||||
|
## response adoption, §6). A TILING body's `_held_granularity_v2` stays
|
||||||
|
## "Region" after zooming IN crosses `_tile_mode -> false` (no response
|
||||||
|
## landed yet); zooming back OUT misread that stale value as "already
|
||||||
|
## canonical," so `_view_zoom` shrank to MIN_ZOOM instead of the fit value.
|
||||||
|
## Fixed by also requiring `is_tile_mode()` to match a fresh entry's value.
|
||||||
|
##
|
||||||
|
## **Live round 5, SECOND fix (same repro, one tick later):** a real wheel
|
||||||
|
## gesture keeps sending zoom-out ticks AFTER the reset fires — `_zoom_at()`
|
||||||
|
## scales `_view_zoom` down every tick regardless, so it drifts below the
|
||||||
|
## fit value again almost immediately (the fit zoom sits right at the
|
||||||
|
## fully-zoomed-out threshold by construction). The mode/center/granularity
|
||||||
|
## guard then reads "already canonical" (true — those never moved) and
|
||||||
|
## skips re-firing, even though the ZOOM drifted away. `_view_zoom` must
|
||||||
|
## also be compared against the CURRENT fit zoom.
|
||||||
func _maybe_reset_to_canonical_frame() -> bool:
|
func _maybe_reset_to_canonical_frame() -> bool:
|
||||||
var radius_km: float = float(_body.get("body_radius_km", 0.0))
|
var radius_km: float = float(_body.get("body_radius_km", 0.0))
|
||||||
if radius_km <= 0.0:
|
if radius_km <= 0.0:
|
||||||
@@ -664,7 +657,18 @@ func _maybe_reset_to_canonical_frame() -> bool:
|
|||||||
var world_extent_m: float = _current_world_extent_m()
|
var world_extent_m: float = _current_world_extent_m()
|
||||||
if not AtlasWindowGeometry.is_fully_zoomed_out(world_extent_m, radius_km):
|
if not AtlasWindowGeometry.is_fully_zoomed_out(world_extent_m, radius_km):
|
||||||
return false
|
return false
|
||||||
if _held_center == Vector2i.ZERO and _held_granularity_v2 == AtlasWindowRequest.GRANULARITY_V2_REGION:
|
var canonical_tile_mode: bool = AtlasWindowGeometry.compute_tile_grid(radius_km).size() > 1
|
||||||
|
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
|
||||||
|
var canonical_n: int = int(extent["cols"])
|
||||||
|
var fit: Dictionary = AtlasWindowGeometry.fit_window_view(
|
||||||
|
get_rect().size, canonical_n, CELL_PIXEL_SIZE, MIN_ZOOM, MAX_ZOOM
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
_held_center == Vector2i.ZERO
|
||||||
|
and _held_granularity_v2 == AtlasWindowRequest.GRANULARITY_V2_REGION
|
||||||
|
and _tile_mode == canonical_tile_mode
|
||||||
|
and is_equal_approx(_view_zoom, float(fit["zoom"]))
|
||||||
|
):
|
||||||
return false # already at the canonical frame — don't fight a zoom-in-from-the-top gesture
|
return false # already at the canonical frame — don't fight a zoom-in-from-the-top gesture
|
||||||
enter_orbital(_body, _system)
|
enter_orbital(_body, _system)
|
||||||
return true
|
return true
|
||||||
@@ -699,13 +703,9 @@ func set_view(zoom: float, offset: Vector2) -> void:
|
|||||||
## — if so, float a NEW window centered on that point via the debounced path.
|
## — if so, float a NEW window centered on that point via the debounced path.
|
||||||
##
|
##
|
||||||
## T-1142 (item 6a): the edge-crossing decision is computed in RAW absolute
|
## T-1142 (item 6a): the edge-crossing decision is computed in RAW absolute
|
||||||
## district space (un-wrapped, un-clamped) — the held window's own local
|
## district space — only the FINAL new_center is canonicalized (wrap column,
|
||||||
## bounds are relative to _held_center as it was BEFORE this pan. Only the
|
## clamp row), matching the server's normalize_window_center() so the echo
|
||||||
## FINAL new_center is canonicalized (wrap column, clamp row), matching the
|
## comparison/cache key stay on the same canonical form.
|
||||||
## server's normalize_window_center() so the echo comparison/cache key stay
|
|
||||||
## on the same canonical form. A pan straddling the antimeridian still
|
|
||||||
## floats correctly: the pre-canonicalization abs_col can go negative or
|
|
||||||
## past cols, and only the resulting new_center gets wrapped before use.
|
|
||||||
func _maybe_refloat_window() -> void:
|
func _maybe_refloat_window() -> void:
|
||||||
if _held_n <= 0:
|
if _held_n <= 0:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user