feat(client): T-1153 + T-1152 client half — continuous cursor-anchored zoom ladder, Region-rung orbital entry, click-through retired
The atlas 'regional' screen now opens the LADDER at the canonical orbital frame (Region granularity, whole body fitted and centered) and wheel zoom descends continuously — cursor-anchored, unclamped across rungs, with progressive refinement (held composite keeps drawing, finer rung swaps in place on arrival; no blank frame, no mode flip). Full-zoom-out resets to the canonical planetary frame per Jeroen's HARD condition (is_fully_zoomed_out = extent >= body circumference, not a zoom-value heuristic). The district_screen nav hop is deleted — D-013 restored: descent is a zoom gesture, not a nav push. AtlasViewer's heightmap-texture path is unreachable from nav (code intact; overlay surface deferred, see report/tickets). Rung selection: design doc §5's literal formula has NO legal District band at any real viewport (visual-tolerance band and n=64 coverage ceiling never overlap — pinned by executable boundary tests at 1600x900); select_rung() splits it into a coverage ceiling (decides Region) then the 2x visual tolerance (District vs Quarter), documented at the function. In practice the ladder steps Region -> Quarter directly. Wire: window_granularity_v2 encoded (omitted at District for byte-compat), granularity_v2 echoed value keyed + staleness-guarded end to end; Region clamp mirror replicates the server's bounded halving loop (no closed form). MIN/MAX_ZOOM widened to [0.0005, 64] — the old 0.5 floor would have clamped a real body's canonical fit zoom, violating the reset condition. Real pre-existing bug fixed in atlas_window_overlay.gd: the draw path used echoed n as both cell-grid dimension and district extent — only coincidentally correct at District granularity; Quarter/Region would have read wrong array offsets. cell_grid_side_for_window() now mirrors the server's WindowGranularity::cell_grid_side. Tests: +26 pure-function geometry tests, new 30-test zoom-ladder suite, extensions across the window cache/request/overlay/delivery suites. Full suite 3518 green; cold-parse clean.
This commit is contained in:
@@ -2,21 +2,27 @@ extends RefCounted
|
||||
|
||||
## Client-side LRU cache for DistrictWindowLayer responses (T-1138, D-226
|
||||
## T-1124 amendment §4 "Client cache policy"; extended T-1150 for the
|
||||
## granularity/min_wl axes).
|
||||
## granularity/min_wl axes; extended T-1152/T-1153 for the granularity_v2
|
||||
## string-tag axis that makes Region representable at all).
|
||||
##
|
||||
## Keyed on (body_id, center, n, granularity, min_wl_m) — D-227's determinism
|
||||
## guarantee (same seed + body + position + derivation params -> same derived
|
||||
## output, always) means a previously-fetched window is valid FOREVER for
|
||||
## that body+seed. This is an LRU-evict-only cache: no freshness check, no
|
||||
## TTL, no invalidation path at all. The only reason an entry ever leaves is
|
||||
## capacity pressure.
|
||||
## Keyed on (body_id, center, n, granularity, min_wl_m, granularity_v2) —
|
||||
## D-227's determinism guarantee (same seed + body + position + derivation
|
||||
## params -> same derived output, always) means a previously-fetched window is
|
||||
## valid FOREVER for that body+seed. This is an LRU-evict-only cache: no
|
||||
## freshness check, no TTL, no invalidation path at all. The only reason an
|
||||
## entry ever leaves is capacity pressure.
|
||||
##
|
||||
## granularity/min_wl_m default to DISTRICT_GRANULARITY/0 (district spacing,
|
||||
## no octave cutoff) — every pre-T-1150 caller that doesn't pass them keeps
|
||||
## its existing key shape and cache behavior unchanged. This is the client
|
||||
## half of the mandatory aliasing fix (T-1150 design doc §3): a
|
||||
## quarter-granularity request and a district-granularity request at the
|
||||
## identical (body, center, n) MUST NOT collide on the same cache slot.
|
||||
## granularity/min_wl_m/granularity_v2 default to
|
||||
## DISTRICT_GRANULARITY/0/DEFAULT_GRANULARITY_V2 ("District", district
|
||||
## spacing, no octave cutoff) — every pre-T-1150 caller that doesn't pass them
|
||||
## keeps its existing key shape and cache behavior unchanged. This is the
|
||||
## client half of the mandatory aliasing fix (T-1150 design doc §3, extended
|
||||
## T-1152): a quarter-granularity request, a district-granularity request,
|
||||
## and a REGION-granularity request all at the identical (body, center, n)
|
||||
## MUST NOT collide on the same cache slot — the legacy int alone cannot
|
||||
## distinguish Region (it has no legal legacy-int value, see
|
||||
## GRANULARITY_V2_REGION's doc), which is exactly why granularity_v2 is a
|
||||
## SEPARATE key component rather than a replacement for the legacy one.
|
||||
##
|
||||
## Godot's Dictionary preserves insertion order, so "move to the end on
|
||||
## touch, evict from the front on overflow" is the whole LRU implementation —
|
||||
@@ -33,6 +39,24 @@ const DEFAULT_MAX_ENTRIES: int = 24
|
||||
## default granularity every pre-T-1150 caller implicitly requests.
|
||||
const DISTRICT_GRANULARITY: int = 1
|
||||
|
||||
## Mirrors the server's WindowGranularity enum (T-1152, R5 redesign,
|
||||
## layer_proxy.rs) — the string-tag vocabulary rmp_serde encodes a bare
|
||||
## `#[derive(Serialize, Deserialize)]` enum's variant name as, verbatim (same
|
||||
## wire convention `RoadNodeKind` already established on this carrier). This
|
||||
## is the KEY-SPACE axis (T-1153): the legacy int `granularity` param below
|
||||
## stays wired for every existing District/Quarter caller (byte/behavior
|
||||
## compatible), but a cache slot is now ALSO qualified by this string so a
|
||||
## Region-rung window can never alias onto a District/Quarter slot at the
|
||||
## identical (body, center, n, legacy_granularity, min_wl_m) — the exact
|
||||
## aliasing risk the T-1150 design doc §3 flagged, extended to the new axis.
|
||||
const GRANULARITY_V2_QUARTER: String = "Quarter"
|
||||
const GRANULARITY_V2_DISTRICT: String = "District"
|
||||
const GRANULARITY_V2_REGION: String = "Region"
|
||||
## Default v2 tag for every caller that doesn't pass one — matches
|
||||
## DISTRICT_GRANULARITY's own "district is the implicit default" contract, so
|
||||
## an omitted v2 tag and an explicit "District" tag key identically.
|
||||
const DEFAULT_GRANULARITY_V2: String = GRANULARITY_V2_DISTRICT
|
||||
|
||||
var _max_entries: int = DEFAULT_MAX_ENTRIES
|
||||
var _entries: Dictionary = {} # key String -> DistrictWindowLayer Dictionary
|
||||
|
||||
@@ -41,34 +65,42 @@ func _init(max_entries: int = DEFAULT_MAX_ENTRIES) -> void:
|
||||
_max_entries = maxi(1, max_entries)
|
||||
|
||||
|
||||
## Build the cache key from the five fields D-227 + T-1150 make sufficient:
|
||||
## body_id (which world+body), center (a [row, col] pair or Vector2i), n
|
||||
## (window extent in districts), granularity (district=1 / quarter=4), and
|
||||
## min_wl_m (the octave cutoff, 0 = none). String-keyed rather than a nested
|
||||
## Dictionary/Array key — Godot Dictionary keys compare by value for
|
||||
## primitives but a consistent stringification sidesteps any
|
||||
## Vector2i-vs-Array identity mismatch between what a caller happens to hand
|
||||
## in.
|
||||
## Build the cache key from the six fields D-227 + T-1150/T-1152 make
|
||||
## sufficient: body_id (which world+body), center (a [row, col] pair or
|
||||
## Vector2i), n (window extent in districts), granularity (the legacy int:
|
||||
## district=1 / quarter=4), min_wl_m (the octave cutoff, 0 = none), and
|
||||
## granularity_v2 (the T-1152 string tag: "Quarter"/"District"/"Region" — the
|
||||
## axis that actually distinguishes Region from every finer rung, since
|
||||
## Region has no legal legacy-int representation and the legacy slot alone
|
||||
## cannot tell a Region window's cache entry apart from a District one at the
|
||||
## same (center, n)). String-keyed rather than a nested Dictionary/Array key
|
||||
## — Godot Dictionary keys compare by value for primitives but a consistent
|
||||
## stringification sidesteps any Vector2i-vs-Array identity mismatch between
|
||||
## what a caller happens to hand in.
|
||||
static func make_key(
|
||||
body_id: String,
|
||||
center: Vector2i,
|
||||
n: int,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> String:
|
||||
return "%s:%d,%d:%d:%d:%d" % [body_id, center.x, center.y, n, granularity, min_wl_m]
|
||||
return "%s:%d,%d:%d:%d:%d:%s" % [
|
||||
body_id, center.x, center.y, n, granularity, min_wl_m, granularity_v2
|
||||
]
|
||||
|
||||
|
||||
## True if a window is already cached for this exact (body, center, n,
|
||||
## granularity, min_wl_m).
|
||||
## granularity, min_wl_m, granularity_v2).
|
||||
func has(
|
||||
body_id: String,
|
||||
center: Vector2i,
|
||||
n: int,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> bool:
|
||||
return _entries.has(make_key(body_id, center, n, granularity, min_wl_m))
|
||||
return _entries.has(make_key(body_id, center, n, granularity, min_wl_m, granularity_v2))
|
||||
|
||||
|
||||
## Fetch a cached window, touching it (move-to-most-recently-used). Returns
|
||||
@@ -81,9 +113,10 @@ func get_window(
|
||||
center: Vector2i,
|
||||
n: int,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> Variant:
|
||||
var key := make_key(body_id, center, n, granularity, min_wl_m)
|
||||
var key := make_key(body_id, center, n, granularity, min_wl_m, granularity_v2)
|
||||
if not _entries.has(key):
|
||||
return null
|
||||
var value: Variant = _entries[key]
|
||||
@@ -101,9 +134,10 @@ func put(
|
||||
n: int,
|
||||
window: Dictionary,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> void:
|
||||
var key := make_key(body_id, center, n, granularity, min_wl_m)
|
||||
var key := make_key(body_id, center, n, granularity, min_wl_m, granularity_v2)
|
||||
if _entries.has(key):
|
||||
_entries.erase(key)
|
||||
_entries[key] = window
|
||||
|
||||
Reference in New Issue
Block a user