fix(client): T-1153/T-1152 round 4 — mosaic canvas-local frame, offset recompute on rung crossing, tile texture RID lifetime; real-driver draw smoke
Three live-found rendering bugs, all invisible to green unit suites: - _draw_tile_mosaic placed tiles from absolute district (0,0); canvas- local (0,0) is held_center - held_n/2 everywhere else, and tile-mode held_n is the whole-body extent — the entire mosaic drew tens of thousands of px off-canvas. New pure district_to_canvas_local() + viewer accessors route every tile through the shared frame. - _maybe_reselect_rung updated held_n across crossings without recomputing _view_offset — the single-window composite landed off- canvas the moment any crossing happened (why District/Quarter were black too). New pure recompute_offset_for_held_n_change(). - _build_tile_texture created an unstored ImageTexture per _draw, racing the RenderingServer's deferred upload — CPU pixels correct, screen white. Per-tile-index texture cache, same reference-identity discipline as the single-window _cached_texture. Structural close of the twice-bitten 'nothing asserts pixels' gap: test_atlas_window_overlay_draw_smoke.gd renders overlay output into a SubViewport and asserts visible pixels for both modes — runs under a real driver (invocation documented in DEVOPS.md, visual_capture precedent; migration to the T-1157 harness noted on that ticket), skips loud-but-green under the gate's headless run (verified green-with-skips AND genuinely red with detection forced off). +7 geometry/crossing tests, all revert-verified. Targeted suites 270 green; gdlint clean.
This commit is contained in:
@@ -381,6 +381,63 @@ static func screen_center_to_district(
|
||||
return Vector2i(roundi(abs_col), roundi(abs_row))
|
||||
|
||||
|
||||
## Live round 4 fix: the exact INVERSE of screen_center_to_district()'s own
|
||||
## district-space math, in CANVAS-LOCAL space (i.e. _canvas's own child
|
||||
## coordinate system — BEFORE _view_offset/_view_zoom, which is what
|
||||
## AtlasWindowOverlay._draw()/_draw_tile_mosaic() draw into, since the
|
||||
## Node2D's position/scale already carries pan/zoom). Every held-window
|
||||
## convention in this file agrees canvas-local `(0,0)` is absolute district
|
||||
## `(held_center - held_n/2)` — single-window `_draw()`'s own
|
||||
## `Rect2(0,0,extent,extent)` relies on this being true for `held_center` ==
|
||||
## the window's own center. `_draw_tile_mosaic()`'s per-tile placement must
|
||||
## use this SAME formula (with the VIEWER's `held_center`/`held_n`, not a
|
||||
## tile's own center/TILE_N) to land in the same coordinate frame the
|
||||
## fit/pan/zoom machinery already assumes — drawing tiles relative to
|
||||
## absolute district (0,0) directly (the live-round-4 bug) silently
|
||||
## disagreed with fit_window_view()'s own `[0, held_n)`-from-origin
|
||||
## assumption whenever `held_n` (the WHOLE-BODY extent in tile mode) wasn't
|
||||
## itself anchored the same way, pushing the entire mosaic off-canvas.
|
||||
static func district_to_canvas_local(
|
||||
district: Vector2, held_center: Vector2i, held_n: int, cell_pixel_size: float
|
||||
) -> Vector2:
|
||||
var half: float = float(held_n) / 2.0
|
||||
var local_col: float = (district.x - (float(held_center.x) - half)) * cell_pixel_size
|
||||
var local_row: float = (district.y - (float(held_center.y) - half)) * cell_pixel_size
|
||||
return Vector2(local_col, local_row)
|
||||
|
||||
|
||||
## 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
|
||||
## PURE screen<->canvas-local transform, entirely independent of
|
||||
## `held_center`/`held_n` — cursor-anchored zoom (`_zoom_at()`) never
|
||||
## references them. But the single-window `_draw()` path draws the held
|
||||
## composite at canvas-local `Rect2(0,0,extent,extent)`, which is ONLY the
|
||||
## right place on screen if canvas-local (0,0) still equals
|
||||
## `held_center - held_n/2` for the NEW rung. `_maybe_reselect_rung()`
|
||||
## updates `held_center`/`held_n` to the new rung's values (a DIFFERENT
|
||||
## `held_n` — Region's ~thousands vs. District's 64 vs. Quarter's 16) but
|
||||
## never touched `_view_offset` to compensate — so canvas-local (0,0)
|
||||
## silently stopped meaning `held_center - held_n/2` the instant `held_n`
|
||||
## changed, and the composite (still drawn at local (0,0)) landed wherever
|
||||
## the STALE offset happened to put it — off-canvas by tens or hundreds of
|
||||
## thousands of px for a Region-to-District/Quarter crossing (round 4's
|
||||
## repro), same root shape as the tile-mosaic bug, just on the "one held
|
||||
## window" side of the split instead of the "many tiles" side.
|
||||
##
|
||||
## This is the exact INVERSE construction: given the SAME screen point that
|
||||
## used to map to `old_local` must now map to canvas-local
|
||||
## `new_held_n/2 * cell_pixel_size` (i.e. new_held_center's own position
|
||||
## under the NEW window's `[0, new_held_n)` span), solve for the
|
||||
## `view_offset` that makes `screen_point == new_local * view_zoom +
|
||||
## view_offset` true. Pan-edge refetch (`_maybe_refloat_window()`) never
|
||||
## needed this because it never changes `held_n` — only rung crossings do.
|
||||
static func recompute_offset_for_held_n_change(
|
||||
screen_point: Vector2, view_zoom: float, new_held_n: int, cell_pixel_size: float
|
||||
) -> Vector2:
|
||||
var new_local: Vector2 = Vector2.ONE * (float(new_held_n) * 0.5 * cell_pixel_size)
|
||||
return screen_point - new_local * view_zoom
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1145 item 2 (moved here T-1153 for file-length/testability): WASD/
|
||||
# arrow-key held-pan direction + edge-scroll suppression/direction — pure
|
||||
|
||||
@@ -92,6 +92,23 @@ var _cached_texture: ImageTexture = null
|
||||
var _cache_window_ref: Variant = null
|
||||
var _cache_active_toggle: String = ""
|
||||
|
||||
## Live round 4 fix: per-TILE texture cache, keyed by tile index — mirrors
|
||||
## the single-window cache above, but one slot per mosaic tile (a Dictionary
|
||||
## of `{window_ref, active_toggle, texture}`, since the mosaic doesn't have a
|
||||
## single fixed set of tiles the way the single-window path has a single
|
||||
## fixed field). Building a brand-new, UNSTORED `ImageTexture` every
|
||||
## `_draw()` call (the round-3 version) left it referenced only by a local
|
||||
## variable — nothing keeps the RID alive past the function returning, which
|
||||
## raced against the RenderingServer's deferred draw-command flush and
|
||||
## rendered as a blank/white tile (the round-4 "pitch black"/white-mosaic
|
||||
## repro's second half, beyond the coordinate fix above): the CPU-side pixel
|
||||
## data was provably correct (sampled directly), but the GPU-side texture
|
||||
## backing it could be gone by composite time. Caching each tile's texture
|
||||
## as a class-owned Dictionary entry (same reference-identity rebuild-only-
|
||||
## on-change discipline as `_cached_texture`) keeps it alive exactly as long
|
||||
## as the single-window composite's own texture already is.
|
||||
var _tile_texture_cache: Dictionary = {}
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if viewer == null:
|
||||
@@ -137,31 +154,42 @@ func _draw() -> void:
|
||||
_draw_crisp_composite(w, grid_side, n, cell_px, active_toggle)
|
||||
|
||||
|
||||
## T-1153, live round 3 (Jeroen's ruling, design doc §4): the orbital
|
||||
## T-1153, live round 3/4 (Jeroen's ruling, design doc §4): the orbital
|
||||
## rest-state MOSAIC draw path — one call to the EXISTING single-tile
|
||||
## composite-building logic (`_rebuild_texture_if_needed()`/
|
||||
## `_draw_smoothed_composite()`'s own per-tile equivalent below) PER TILE,
|
||||
## each positioned at its own LOCAL offset from the tile-set's reference
|
||||
## origin (district (0,0), matching `_enter_tile_mode()`'s own
|
||||
## `_held_center = Vector2i.ZERO`). A tile centered at absolute district
|
||||
## `tile.center` occupies local canvas space
|
||||
## `[(tile.center - TILE_N/2) * cell_px, (tile.center + TILE_N/2) * cell_px)`
|
||||
## — the SAME "window spans [center - n/2, center + n/2)" convention the
|
||||
## single-window draw path already uses, just evaluated per tile instead of
|
||||
## once for the whole composite. Tiles that haven't arrived yet
|
||||
## (`tile["window"] == null`) are simply SKIPPED — 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 no separate
|
||||
## whole-viewport fade is needed on top of this).
|
||||
## each positioned at its own LOCAL offset in the SAME canvas-local
|
||||
## coordinate frame the single-window path (and fit_window_view()/
|
||||
## screen_center_to_district()) already use.
|
||||
##
|
||||
## **Live round 4 fix:** the round-3 version placed tiles relative to
|
||||
## absolute district (0,0) directly (`(tile.center - TILE_N/2) * cell_px`),
|
||||
## which does NOT match `_fit_and_center()`'s own convention — canvas-local
|
||||
## (0,0) is `held_center - held_n/2` (AtlasWindowGeometry.
|
||||
## district_to_canvas_local()'s own doc), and in tile mode `held_n` is the
|
||||
## WHOLE BODY's extent, not TILE_N. That mismatch pushed the entire mosaic
|
||||
## off-canvas (round 4's "pitch black" repro) — silent, since nothing
|
||||
## errors, it just draws somewhere the viewport never shows. Fixed by
|
||||
## routing every tile's placement through `district_to_canvas_local()` with
|
||||
## the VIEWER's own `held_center`/`held_n`, the exact reference frame every
|
||||
## other canvas-local consumer (fit/pan/reselect) already agrees on. Tiles
|
||||
## that haven't arrived yet (`tile["window"] == null`) are simply SKIPPED —
|
||||
## 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
|
||||
## no separate whole-viewport fade is needed on top of this).
|
||||
func _draw_tile_mosaic() -> void:
|
||||
var tile_set = viewer.get_tile_set()
|
||||
if tile_set == null:
|
||||
return
|
||||
var cell_px: float = viewer.get_cell_pixel_size()
|
||||
var active_toggle: String = _active_toggle_overlay()
|
||||
var held_center: Vector2i = viewer.get_held_center()
|
||||
var held_n: int = viewer.get_held_n()
|
||||
var half_tile: float = float(AtlasWindowGeometryRef.TILE_N) * 0.5
|
||||
var tiles: Array = tile_set.get_tiles()
|
||||
|
||||
for tile: Dictionary in tile_set.get_tiles():
|
||||
for i in range(tiles.size()):
|
||||
var tile: Dictionary = tiles[i]
|
||||
var window: Variant = tile["window"]
|
||||
if not window is Dictionary:
|
||||
continue
|
||||
@@ -174,42 +202,71 @@ func _draw_tile_mosaic() -> void:
|
||||
continue
|
||||
|
||||
var center: Vector2i = tile["center"]
|
||||
var local_origin: Vector2 = Vector2(
|
||||
(float(center.x) - half_tile) * cell_px, (float(center.y) - half_tile) * cell_px
|
||||
var tile_top_left: Vector2 = Vector2(
|
||||
float(center.x) - half_tile, float(center.y) - half_tile
|
||||
)
|
||||
var local_origin: Vector2 = AtlasWindowGeometryRef.district_to_canvas_local(
|
||||
tile_top_left, held_center, held_n, cell_px
|
||||
)
|
||||
var extent: float = float(AtlasWindowGeometryRef.TILE_N) * cell_px
|
||||
_draw_one_tile(w, grid_side, local_origin, extent, active_toggle)
|
||||
_draw_one_tile(i, w, grid_side, local_origin, extent, active_toggle)
|
||||
|
||||
|
||||
## One tile's own composite — the SAME crisp/smoothed per-cell pipeline the
|
||||
## single-window path uses (_cell_color()/_apply_glaciation(), UNCHANGED),
|
||||
## just drawn at `local_origin` instead of always at (0,0). Each tile gets
|
||||
## its OWN texture-rebuild cache slot (keyed by the tile's own window
|
||||
## reference, via `_tile_texture_cache` below) — sharing ONE
|
||||
## `_cached_texture` slot across all tiles (the single-window field) would
|
||||
## thrash on every draw call as different tiles' windows compete for it.
|
||||
## its OWN texture-rebuild cache slot in `_tile_texture_cache`, keyed by
|
||||
## `tile_index` — sharing ONE `_cached_texture` slot across all tiles (the
|
||||
## single-window field) would thrash on every draw call as different tiles'
|
||||
## windows compete for it.
|
||||
func _draw_one_tile(
|
||||
w: Dictionary, grid_side: int, local_origin: Vector2, extent: float, active_toggle: String
|
||||
tile_index: int,
|
||||
w: Dictionary,
|
||||
grid_side: int,
|
||||
local_origin: Vector2,
|
||||
extent: float,
|
||||
active_toggle: String
|
||||
) -> void:
|
||||
if not COMPOSITE_SMOOTH:
|
||||
_draw_crisp_tile(w, grid_side, local_origin, extent, active_toggle)
|
||||
return
|
||||
var tile_texture: ImageTexture = _build_tile_texture(w, grid_side, active_toggle)
|
||||
var tile_texture: ImageTexture = _rebuild_tile_texture_if_needed(
|
||||
tile_index, w, grid_side, active_toggle
|
||||
)
|
||||
if tile_texture == null:
|
||||
return
|
||||
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
draw_texture_rect(tile_texture, Rect2(local_origin, Vector2(extent, extent)), false)
|
||||
|
||||
|
||||
## Builds (uncached — see the class doc's rebuild-cost paragraph for why the
|
||||
## SINGLE-window path caches by reference; a per-tile cache keyed the same
|
||||
## way would need a Dictionary keyed on tile index, a reasonable follow-up
|
||||
## if mosaic redraw cost ever matters in practice, not attempted here since
|
||||
## the mosaic is drawn only at the orbital rest state, never mid-interaction
|
||||
## at a high redraw rate the way single-window zoom/pan is) a tile's own
|
||||
## Image/ImageTexture from its per-cell colors — identical pipeline to
|
||||
## `_rebuild_texture_if_needed()`, just returning the texture directly
|
||||
## instead of writing to the single-window cache fields.
|
||||
## Live round 4 fix: rebuilds (and, critically, KEEPS — see
|
||||
## `_tile_texture_cache`'s own doc for why an unstored local `ImageTexture`
|
||||
## silently rendered blank/white) `_tile_texture_cache[tile_index]`'s texture
|
||||
## ONLY when that tile's window object or the active toggle overlay has
|
||||
## changed since the last build — the SAME reference-identity discipline
|
||||
## `_rebuild_texture_if_needed()` uses for the single-window composite, one
|
||||
## cache entry per tile index instead of one shared field.
|
||||
func _rebuild_tile_texture_if_needed(
|
||||
tile_index: int, w: Dictionary, grid_side: int, active_toggle: String
|
||||
) -> ImageTexture:
|
||||
var entry: Dictionary = _tile_texture_cache.get(tile_index, {})
|
||||
if (
|
||||
is_same(entry.get("window_ref"), w)
|
||||
and entry.get("active_toggle") == active_toggle
|
||||
and entry.get("texture") != null
|
||||
):
|
||||
return entry["texture"]
|
||||
var texture: ImageTexture = _build_tile_texture(w, grid_side, active_toggle)
|
||||
_tile_texture_cache[tile_index] = {
|
||||
"window_ref": w, "active_toggle": active_toggle, "texture": texture
|
||||
}
|
||||
return texture
|
||||
|
||||
|
||||
## Builds a tile's own Image/ImageTexture from its per-cell colors —
|
||||
## identical pipeline to `_rebuild_texture_if_needed()`, just returning the
|
||||
## texture directly instead of writing to the single-window cache fields
|
||||
## (the CALLER, `_rebuild_tile_texture_if_needed()`, owns persisting it).
|
||||
func _build_tile_texture(w: Dictionary, grid_side: int, active_toggle: String) -> ImageTexture:
|
||||
var elev_q: Variant = w.get("elev_q")
|
||||
var glaciation: Variant = w.get("glaciation")
|
||||
|
||||
@@ -19,17 +19,14 @@ extends Control
|
||||
## - _canvas (Node2D) holds AtlasWindowOverlay; pan = _canvas.position, zoom
|
||||
## = _canvas.scale.
|
||||
## - Zoom is client-side on the ALREADY-HELD composite frame-to-frame, but
|
||||
## CONTINUOUS AND UNCLAMPED ACROSS RUNGS (D-013 restored for this seam):
|
||||
## crossing a rung's coverage ceiling (§5, redesigned per live round 3 —
|
||||
## see AtlasWindowGeometry.select_rung()) fires a background request for
|
||||
## the new granularity while the OLD composite keeps drawing —
|
||||
## progressive refinement, no blank frame, no mode flip (§6). A pan past
|
||||
## the held window's edge re-requests the SAME rung at a new center.
|
||||
## CONTINUOUS AND UNCLAMPED ACROSS RUNGS (D-013): crossing a rung's
|
||||
## coverage ceiling (§5, AtlasWindowGeometry.select_rung()) fires a
|
||||
## background request for the new granularity while the OLD composite
|
||||
## keeps drawing — progressive refinement, no blank frame (§6). A pan
|
||||
## past the held window's edge re-requests the SAME rung at a new center.
|
||||
## - Zooming fully out snaps to the CANONICAL planetary frame (Jeroen's HARD
|
||||
## condition) — see _maybe_reset_to_canonical_frame() — which, on a body
|
||||
## needing tiling, re-enters `_tile_mode` (live round 3, design doc §4:
|
||||
## "the top rest state is the WHOLE body, served as progressive
|
||||
## capped-density TILING").
|
||||
## needing tiling, re-enters `_tile_mode` (live round 3, design doc §4).
|
||||
## - _window_request (atlas_window_request.gd) owns the single-window
|
||||
## cache/debounce/retry; _tile_set (atlas_window_tile_set.gd) owns N of
|
||||
## those for the tiled rest state — this Control decides WHICH is active.
|
||||
@@ -207,12 +204,10 @@ var _legend_panel = null
|
||||
var _window_request = null # AtlasWindowRequest
|
||||
var _tile_set = null # AtlasWindowTileSet (T-1153, live round 3)
|
||||
|
||||
## T-1153 (Jeroen's ruling, design doc §4): true while showing the orbital
|
||||
## rest state as a MULTI-WINDOW MOSAIC (AtlasWindowTileSet) instead of the
|
||||
## single held composite (`_window`). Set by `_enter_tile_mode()` when
|
||||
## compute_tile_grid() produces more than one tile; cleared the moment
|
||||
## `_maybe_reselect_rung()` crosses OUT of Region — tiling is purely a
|
||||
## TOP-of-the-ladder concern, never active below Region.
|
||||
## T-1153 (design doc §4): true while showing the orbital rest state as a
|
||||
## MULTI-WINDOW MOSAIC (AtlasWindowTileSet) instead of the single held
|
||||
## composite (`_window`). Set by `_enter_tile_mode()`; cleared the moment
|
||||
## `_maybe_reselect_rung()` crosses OUT of Region.
|
||||
var _tile_mode: bool = false
|
||||
|
||||
|
||||
@@ -303,14 +298,13 @@ func enter(
|
||||
## 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 (Jeroen's ruling, design doc §4): the rest state must
|
||||
## TILE.** A single wire-capped Region window covers at most
|
||||
## `AtlasWindowGeometry.MAX_COVERAGE_M["Region"]` (13,107,200 m) — a THIRD of
|
||||
## Lendel's ~39,197,023 m circumference (shot 01's own header: "13107.2 x
|
||||
## 13107.2 km"). Once `compute_tile_grid()` returns MORE than one 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" (the degenerate case) and stays single-window.
|
||||
## **Live round 3 (design doc §4): the rest state must TILE.** A single
|
||||
## wire-capped Region window covers at most
|
||||
## `AtlasWindowGeometry.MAX_COVERAGE_M["Region"]`, a fraction of a real
|
||||
## body's circumference. Once `compute_tile_grid()` returns MORE than one
|
||||
## 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:
|
||||
var radius_km: float = float(body.get("body_radius_km", 0.0))
|
||||
if radius_km <= 0.0:
|
||||
@@ -330,11 +324,10 @@ func enter_orbital(body: Dictionary, system: Dictionary) -> void:
|
||||
|
||||
|
||||
## T-1153, live round 3: the TILE-MODE entry path — same reset discipline as
|
||||
## `_enter_at_rung()` but populates `_tile_set` instead of `_window_request`,
|
||||
## setting `_tile_mode = true` so drawing/reset/reselect read the mosaic.
|
||||
## `_enter_at_rung()` but populates `_tile_set` instead of `_window_request`.
|
||||
## `_held_n` carries the WHOLE body's extent unclamped (each TILE clamps its
|
||||
## own TILE_N-sized request independently inside AtlasWindowTileSet), so the
|
||||
## extent math elsewhere needs no tile-specific branch.
|
||||
## own TILE_N-sized request independently), so extent math elsewhere needs
|
||||
## no tile-specific branch.
|
||||
func _enter_tile_mode(body: Dictionary, system: Dictionary, radius_km: float) -> void:
|
||||
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
|
||||
var n: int = int(extent["cols"])
|
||||
@@ -398,12 +391,9 @@ func _enter_at_rung(
|
||||
|
||||
|
||||
## T-1142: fit-and-center — applies AtlasWindowGeometry.fit_window_view()'s
|
||||
## zoom/offset, then re-clamps the offset to the pole wall (a freshly-fitted
|
||||
## view can still need the wall on a tiny body whose row span is shorter than
|
||||
## the window itself — see atlas_window_geometry.gd's clamp function doc).
|
||||
## Called from enter(), the FIRST _on_window_ready() after entry, and
|
||||
## NOTIFICATION_RESIZED — never mid-interaction (guarded by _user_adjusted at
|
||||
## each call site, not here, since the three callers gate slightly differently).
|
||||
## zoom/offset, then re-clamps the offset to the pole wall. Called from
|
||||
## enter(), the FIRST _on_window_ready() after entry, and
|
||||
## NOTIFICATION_RESIZED — never mid-interaction (guarded by _user_adjusted).
|
||||
func _fit_and_center() -> void:
|
||||
var viewport: Vector2 = get_rect().size
|
||||
if viewport == Vector2.ZERO:
|
||||
@@ -453,6 +443,18 @@ func get_tile_set() -> Variant:
|
||||
return _tile_set
|
||||
|
||||
|
||||
## Live round 4: currently-HELD reference frame — AtlasWindowOverlay's
|
||||
## mosaic draw path converts each tile's absolute district center into
|
||||
## canvas-local space via these (see AtlasWindowGeometry.
|
||||
## district_to_canvas_local()'s own doc for the shared convention).
|
||||
func get_held_center() -> Vector2i:
|
||||
return _held_center
|
||||
|
||||
|
||||
func get_held_n() -> int:
|
||||
return _held_n
|
||||
|
||||
|
||||
## District-cell pixel size at zoom=1.0 — AtlasWindowOverlay reads this
|
||||
## rather than hardcoding CELL_PIXEL_SIZE itself, so the viewer stays the
|
||||
## single source of geometry truth (same "viewer owns the transform, overlay
|
||||
@@ -558,11 +560,10 @@ func _apply_transform() -> void:
|
||||
_overlay_node.queue_redraw()
|
||||
|
||||
|
||||
## Cursor-anchored zoom (D-013 restored for this seam): the CANVAS POINT
|
||||
## under the cursor stays fixed on screen across the zoom step. Unclamped
|
||||
## ACROSS RUNGS (only the wide MIN_ZOOM/MAX_ZOOM safety clamp applies — see
|
||||
## that constant's own doc); after applying, checks whether the extent now
|
||||
## calls for a different rung or the top rest state.
|
||||
## Cursor-anchored zoom (D-013): the CANVAS POINT under the cursor stays
|
||||
## fixed on screen across the zoom step. Unclamped across rungs (only the
|
||||
## wide MIN_ZOOM/MAX_ZOOM safety clamp applies); after applying, checks
|
||||
## whether the extent now calls for a different rung or the top rest state.
|
||||
func _zoom_at(mouse_pos: Vector2, factor: float) -> void:
|
||||
var new_zoom: float = clampf(_view_zoom * factor, MIN_ZOOM, MAX_ZOOM)
|
||||
if is_equal_approx(new_zoom, _view_zoom):
|
||||
@@ -577,9 +578,8 @@ func _zoom_at(mouse_pos: Vector2, factor: float) -> void:
|
||||
|
||||
|
||||
## The world extent (metres) currently displayed across the LARGER viewport
|
||||
## dimension — the `E` half of the §5 rung-selection rule. A pure function
|
||||
## of `_view_zoom` (see AtlasWindowGeometry.world_extent_m()'s own doc for
|
||||
## why the held rung is NOT an input). Thin wrapper over that pure function.
|
||||
## dimension — the `E` half of the §5 rung-selection rule. Thin wrapper over
|
||||
## AtlasWindowGeometry.world_extent_m() (a pure function of `_view_zoom`).
|
||||
func _current_world_extent_m() -> float:
|
||||
return AtlasWindowGeometry.world_extent_m(CELL_PIXEL_SIZE, _view_zoom, get_rect().size)
|
||||
|
||||
@@ -592,12 +592,10 @@ func _current_world_extent_m() -> float:
|
||||
##
|
||||
## **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
|
||||
## at the PREVIOUS rung's clamp — crossing rungs changes the clamp ceiling
|
||||
## (Region caps at 6,400; District/Quarter at 64), so a stale Region-sized
|
||||
## `_held_n` fed into a Quarter request gets server-clamped small while
|
||||
## `_held_n` stays large — `_on_window_ready()`'s `w_n != _held_n` then
|
||||
## drops every cross-rung refinement forever. Same bug as _enter_at_rung(),
|
||||
## recurring at the CROSSING boundary.
|
||||
## at the PREVIOUS rung's clamp — crossing rungs changes the clamp ceiling,
|
||||
## so a stale large `_held_n` fed into a smaller-rung request desyncs
|
||||
## `_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` —
|
||||
## the OLD composite keeps drawing until _on_window_ready() adopts the new
|
||||
@@ -609,12 +607,9 @@ func _maybe_reselect_rung() -> void:
|
||||
var canvas_px: float = maxf(get_rect().size.x, get_rect().size.y)
|
||||
var target_rung: String = AtlasWindowGeometry.select_rung(world_extent_m, canvas_px)
|
||||
|
||||
# T-1153, live round 3: tile mode is TOP-of-the-ladder only (coordinator's
|
||||
# own scoping — "inside-zoom can stay single-window as now"). Staying at
|
||||
# Region means staying tiled (zooming within a mosaic is a client-side
|
||||
# scale on the SAME held tiles, like single-window zoom on one
|
||||
# composite). Crossing OUT of Region falls through to the single-window
|
||||
# path below, flipping `_tile_mode` off.
|
||||
# T-1153, live round 3: tile mode is TOP-of-the-ladder only. Staying at
|
||||
# Region means staying tiled; crossing OUT of Region falls through to
|
||||
# the single-window path below, flipping `_tile_mode` off.
|
||||
var leaving_tile_mode := false
|
||||
if _tile_mode:
|
||||
if target_rung == AtlasWindowRequest.GRANULARITY_V2_REGION:
|
||||
@@ -623,25 +618,30 @@ func _maybe_reselect_rung() -> void:
|
||||
leaving_tile_mode = true
|
||||
|
||||
# `leaving_tile_mode` FORCES the request through even if
|
||||
# `_window_request`'s own STALE granularity_v2 (never touched while tiled)
|
||||
# happens to already equal `target_rung` by coincidence — without this,
|
||||
# the early-return below would skip the request that's supposed to
|
||||
# POPULATE `_window` for the first time since tile mode replaced it.
|
||||
# `_window_request`'s own STALE granularity_v2 (untouched while tiled)
|
||||
# happens to already equal `target_rung` by coincidence.
|
||||
if not leaving_tile_mode and target_rung == _window_request.get_granularity_v2():
|
||||
return # already requesting (or holding) the rung this extent calls for
|
||||
var new_center: Vector2i = _screen_center_district()
|
||||
var clamped_n: int = AtlasWindowRequest._clamp_window_n_mirror_v2(_held_n, target_rung)
|
||||
_held_center = new_center
|
||||
_held_n = clamped_n
|
||||
# Live round 4: `_view_offset` must be recomputed the instant `held_n`
|
||||
# changes — see recompute_offset_for_held_n_change()'s own doc for why.
|
||||
# Anchors on the SAME screen center just used above, preserving §6 "no
|
||||
# layout jump" across the crossing.
|
||||
_view_offset = AtlasWindowGeometry.recompute_offset_for_held_n_change(
|
||||
size * 0.5, _view_zoom, clamped_n, CELL_PIXEL_SIZE
|
||||
)
|
||||
_apply_transform()
|
||||
_window_request.request_debounced(
|
||||
_dict_str(_body, "body_id", ""), new_center, clamped_n, target_rung
|
||||
)
|
||||
|
||||
|
||||
## The DistrictPos the current screen center maps to, in RAW absolute
|
||||
## district space (the caller canonicalizes the final stored/sent value).
|
||||
## Thin wrapper over AtlasWindowGeometry.screen_center_to_district() so both
|
||||
## the pan-edge refetch and the rung-reselect refetch share ONE formula.
|
||||
## district space. Thin wrapper over AtlasWindowGeometry.
|
||||
## screen_center_to_district() so pan-edge/rung-reselect refetch share one formula.
|
||||
func _screen_center_district() -> Vector2i:
|
||||
var raw: Vector2i = AtlasWindowGeometry.screen_center_to_district(
|
||||
size, _view_offset, _view_zoom, CELL_PIXEL_SIZE, _held_center, _held_n
|
||||
|
||||
Reference in New Issue
Block a user