fix(simulation): derive at the octaves a rung can actually reconstruct (T-1240)
Region rendered as fine uniform stucco while District and Quarter, on identical code, read as terrain. The cause was sampling: `min_wl_m` arrives as an LOD request and defaults to 0, so every invented octave contributed at every rung. MIN_WL_BANDS_M was meant to be the floor but is built from the rung's CELL SIZE (2 x DISTRICT_M), which stopped being the sample spacing at the D-255 extent inversion — a rung fixes EXTENT now and spacing falls out of the canvas size. The bands were off by roughly the cell count, and the served path never consulted them anyway. The cutoff is now derived from the resolved spacing, which is what this ticket asked for. Two things had to be measured rather than reasoned to get it right, and both corrected me. FIRST: the field was the culprit, not the renderer. I attributed the stucco to the client stipple painting noise onto a smooth field. Surfacing the terrain layer's own mean |relief_q gradient| in the capture readout settled it in one shot: Region 18.24 steps per cell — 144 m of relief between NEIGHBOURING cells — against District's 0.30 and Quarter's 0.07. The server was sending noise. That diagnostic ships here for the same reason `plane_variety` did in T-1213: a noisy field and a renderer inventing noise look identical, and one number separates them. SECOND: Nyquist is the wrong threshold. The first version floored at 2 x spacing, the aliasing limit, and Region barely moved (56.16 -> 59.73 lum spread, gradient still 18.24) because 2 samples per cycle is unaliased but renders jagged. The rungs that already worked say what the real bar is: District reconstructs its finest surviving octave at 34 samples per cycle, Quarter at 135. At 8x, Region goes to 1.08 gradient and 70.01 spread, and shows ridges and valleys. THE TRADE, taken deliberately and recorded in the tests: an 8x floor also truncates the coast warp's 2,048 and 1,024 m octaves at Region, the band T-1160 added for "one coastline at every rung". An earlier test here asserted that band must survive; it now asserts the opposite. Same reasoning as the relief: a 1,024 m coastline wiggle at 379.3 m per cell is 2.7 samples per cycle, so drawing it draws noise rather than coastline character — a rung cannot show shape finer than its own cell. The warp is amplitude-capped sub-pixel on the working grid, so what is lost is small. If a future pass wants the warp exempt, the fix is a relief-only floor threaded through derive_at_metres, NOT a lower multiple, which takes the stucco back. Global is exempt: its floor would be ~70 km and would truncate the whole warp band, and it needs none — the orbital derive leaves relief_q flat at 50. District (3.79 m spacing) and Quarter (0.948 m) floor below every octave in play and derive byte-identically, which their own test pins. Cache-safe by construction: the floor is a pure function of (rung, extent, body_radius), all three already in the step-canvas cache key. 0.4.12 is required anyway — this changes derived BYTES at Region, so a 0.4.11 entry holds a field this build would never produce. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ config/name="The Settled Reach"
|
||||
; in the editor and in a shipped build, where res://../project.yaml does not
|
||||
; exist at all (T-1241). Kept honest by `make check-client-version`, which the
|
||||
; pre-push hook runs — do not edit this by hand without moving project.yaml too.
|
||||
config/version="0.4.11"
|
||||
config/version="0.4.12"
|
||||
run/main_scene="res://scenes/main_menu.tscn"
|
||||
config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
@@ -283,8 +283,10 @@ func _settle_atlas_viewer_if_pending(tree_root: Node) -> void:
|
||||
waited += 1
|
||||
if request.is_pending():
|
||||
print(
|
||||
"visual_capture: atlas settle timed out after %d frames — request still pending"
|
||||
% ATLAS_SETTLE_MAX_FRAMES
|
||||
(
|
||||
"visual_capture: atlas settle timed out after %d frames — request still pending"
|
||||
% ATLAS_SETTLE_MAX_FRAMES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -333,27 +335,31 @@ func _log_atlas_view_transform(tree_root: Node, scenario_name: String) -> void:
|
||||
var on_screen_footprint_px: Vector2 = raw_footprint_px * canvas_scale
|
||||
print(
|
||||
(
|
||||
"visual_capture: view-transform[%s] rung=%s world_center=%s held_extent=%s "
|
||||
+ "canvas_position=%s canvas_scale=%s footprint_px=%s canvas_cells=%dx%d "
|
||||
+ "courses=%d runs=%d longest=%.1fpx drawn=%d settlements=%d planes=%s"
|
||||
(
|
||||
"visual_capture: view-transform[%s] rung=%s world_center=%s held_extent=%s "
|
||||
+ "canvas_position=%s canvas_scale=%s footprint_px=%s canvas_cells=%dx%d "
|
||||
+ "courses=%d runs=%d longest=%.1fpx drawn=%d settlements=%d "
|
||||
+ "planes=%s relief_grad=%.2f"
|
||||
)
|
||||
% [
|
||||
scenario_name,
|
||||
rung,
|
||||
str(summary.get("world_center", [])),
|
||||
str(held_extent_raw),
|
||||
str(canvas_pos),
|
||||
str(canvas_scale),
|
||||
str(on_screen_footprint_px),
|
||||
int(summary.get("canvas_width", 0)),
|
||||
int(summary.get("canvas_height", 0)),
|
||||
int(summary.get("course_count", 0)),
|
||||
int(summary.get("runs_built", 0)),
|
||||
float(summary.get("longest_run_px", 0.0)),
|
||||
int(summary.get("drawn_course_count", 0)),
|
||||
int(summary.get("settlement_count", 0)),
|
||||
str(summary.get("plane_variety", {})),
|
||||
float(summary.get("relief_gradient", 0.0)),
|
||||
]
|
||||
)
|
||||
% [
|
||||
scenario_name,
|
||||
rung,
|
||||
str(summary.get("world_center", [])),
|
||||
str(held_extent_raw),
|
||||
str(canvas_pos),
|
||||
str(canvas_scale),
|
||||
str(on_screen_footprint_px),
|
||||
int(summary.get("canvas_width", 0)),
|
||||
int(summary.get("canvas_height", 0)),
|
||||
int(summary.get("course_count", 0)),
|
||||
int(summary.get("runs_built", 0)),
|
||||
float(summary.get("longest_run_px", 0.0)),
|
||||
int(summary.get("drawn_course_count", 0)),
|
||||
int(summary.get("settlement_count", 0)),
|
||||
str(summary.get("plane_variety", {})),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -498,10 +504,7 @@ func _run_atlas_matrix() -> void:
|
||||
# implant_app.gd — not previously documented in the recon blueprint).
|
||||
var first_body_id: String = str(shots[0].get("body_id", ""))
|
||||
var first_body: Dictionary = body_lookup.get(first_body_id, {})
|
||||
app.nav.push(
|
||||
"regional",
|
||||
{"body": first_body, "system": _system_dict_for(first_body)}
|
||||
)
|
||||
app.nav.push("regional", {"body": first_body, "system": _system_dict_for(first_body)})
|
||||
var viewer: Variant = app._regional_screen._viewer
|
||||
if viewer == null:
|
||||
push_error("visual_capture: regional screen has no viewer")
|
||||
@@ -524,9 +527,7 @@ func _run_atlas_matrix() -> void:
|
||||
# then navigates from there.
|
||||
if body_id != current_body_id:
|
||||
current_body_id = body_id
|
||||
app._regional_screen.enter(
|
||||
{"body": body, "system": _system_dict_for(body)}
|
||||
)
|
||||
app._regional_screen.enter({"body": body, "system": _system_dict_for(body)})
|
||||
await _settle_atlas_viewer_if_pending(root)
|
||||
|
||||
# Apply overlay set: clear every gen_dw_* toggle, then enable this
|
||||
@@ -704,7 +705,9 @@ func _print_list() -> void:
|
||||
# tests/atlas_shots.json instead — see _run_atlas_matrix()), so it can't be
|
||||
# discovered by iterating _config above. Listed by hand so --list stays a
|
||||
# complete index of every --scenario value this script accepts.
|
||||
print(" atlas_matrix — D-255 stepped Atlas capture matrix, keyed on (body, rung) (tests/atlas_shots.json, live-mode only)")
|
||||
print(
|
||||
" atlas_matrix — D-255 stepped Atlas capture matrix, keyed on (body, rung) (tests/atlas_shots.json, live-mode only)"
|
||||
)
|
||||
print("flows:")
|
||||
for name in flows:
|
||||
var steps: Array = flows[name].get("steps", [])
|
||||
|
||||
@@ -73,6 +73,9 @@ var _held_rung: String = StepCanvasTransport.RUNG_DISTRICT
|
||||
## values separates them from a single capture.
|
||||
var _plane_variety: Dictionary = {}
|
||||
|
||||
## Mean |relief_q gradient| of the held canvas — see [method get_relief_gradient].
|
||||
var _relief_gradient_scale: float = 0.0
|
||||
|
||||
|
||||
## Rebuild (or reuse) the held texture from a decoded StepCanvasResponse's
|
||||
## `canvas` Dictionary (step_canvas_protocol.gd's shape: width/height +
|
||||
@@ -129,6 +132,7 @@ func _decode_planes(canvas: Dictionary, width: int, height: int) -> StepCanvasCo
|
||||
planes.lake_margin_q = _decode_l8_plane(canvas.get("lake_margin_q"))
|
||||
planes.relief_q = _decode_l8_plane(canvas.get("relief_q"))
|
||||
planes.relief_gradient_scale = _measure_relief_gradient(planes.relief_q)
|
||||
_relief_gradient_scale = planes.relief_gradient_scale
|
||||
planes.glaciation = _decode_l8_plane(canvas.get("glaciation"))
|
||||
planes.temp_dc = canvas.get("temp_dc", [])
|
||||
return planes
|
||||
@@ -181,6 +185,13 @@ func get_plane_variety() -> Dictionary:
|
||||
return _plane_variety
|
||||
|
||||
|
||||
## The held canvas's mean |relief_q gradient| — the number the hillshade
|
||||
## normalizes against, surfaced so a capture can say whether a stucco-looking
|
||||
## rung is a noisy FIELD or a renderer painting noise onto a smooth one (T-1240).
|
||||
func get_relief_gradient() -> float:
|
||||
return _relief_gradient_scale
|
||||
|
||||
|
||||
## Cells stepped between variety samples.
|
||||
##
|
||||
## A full scan is 5 planes x 1290x540 = ~3.5M get_pixel() calls per canvas
|
||||
|
||||
@@ -46,9 +46,15 @@ extends Control
|
||||
|
||||
signal back_pressed
|
||||
|
||||
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
|
||||
const StepCanvasTerrainLayer := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_terrain_layer.gd")
|
||||
const StepCanvasAnnotationLayer := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_annotation_layer.gd")
|
||||
const StepCanvasTransport := preload(
|
||||
"res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd"
|
||||
)
|
||||
const StepCanvasTerrainLayer := preload(
|
||||
"res://ui/implant/apps/atlas/step_canvas/step_canvas_terrain_layer.gd"
|
||||
)
|
||||
const StepCanvasAnnotationLayer := preload(
|
||||
"res://ui/implant/apps/atlas/step_canvas/step_canvas_annotation_layer.gd"
|
||||
)
|
||||
const StepCanvasRequest := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_request.gd")
|
||||
|
||||
const PANEL_MARGIN: float = 16.0
|
||||
@@ -431,16 +437,19 @@ func get_current_canvas_summary() -> Dictionary:
|
||||
"canvas_width": int(d.get("width", 0)),
|
||||
"canvas_height": int(d.get("height", 0)),
|
||||
"course_count": courses.size(),
|
||||
"drawn_course_count": (
|
||||
_annotation_layer.get_drawn_course_count() if _annotation_layer else 0
|
||||
),
|
||||
"drawn_course_count":
|
||||
# T-1239: the two stages between "arrived" and "drawn", so a capture can
|
||||
_annotation_layer.get_drawn_course_count() if _annotation_layer else 0,
|
||||
# say WHICH stage dropped the rivers instead of only that they are gone.
|
||||
"runs_built": _annotation_layer.get_runs_built() if _annotation_layer else 0,
|
||||
"longest_run_px": _annotation_layer.get_longest_run_px() if _annotation_layer else 0.0,
|
||||
# T-1213: distinct values per plane, so "this rung is flat" can name the
|
||||
# plane that went flat rather than leaving it to inference.
|
||||
"plane_variety": _terrain_layer.get_plane_variety() if _terrain_layer else {},
|
||||
# T-1240: the relief field's own mean cell-to-cell gradient. Separates
|
||||
# "the server sent a noisy field" from "the renderer is painting noise
|
||||
# onto a smooth one" — two causes with one appearance (a stucco rung).
|
||||
"relief_gradient": _terrain_layer.get_relief_gradient() if _terrain_layer else 0.0,
|
||||
"course_count_by_class": course_count_by_class,
|
||||
"cliff_count": (d.get("cliffs", []) as Array).size(),
|
||||
"settlement_count": _count_distinct_settlements(d),
|
||||
@@ -605,11 +614,7 @@ func _pan_drift_fraction() -> float:
|
||||
func _refloat_now() -> void:
|
||||
var screen_center: Vector2 = get_rect().size * 0.5
|
||||
_world_center = StepCanvasTransport.canvas_local_to_world_m(
|
||||
screen_center - _view_offset,
|
||||
_world_center,
|
||||
_held_rung,
|
||||
_held_extent,
|
||||
get_body_radius_km()
|
||||
screen_center - _view_offset, _world_center, _held_rung, _held_extent, get_body_radius_km()
|
||||
)
|
||||
_view_offset = Vector2.ZERO
|
||||
_fire_request()
|
||||
@@ -746,11 +751,7 @@ func _scroll_rung(direction: int, cursor_local: Vector2) -> void:
|
||||
_reset_to_global()
|
||||
return
|
||||
var cursor_world: Vector2 = StepCanvasTransport.canvas_local_to_world_m(
|
||||
cursor_local - _view_offset,
|
||||
_world_center,
|
||||
_held_rung,
|
||||
_held_extent,
|
||||
get_body_radius_km()
|
||||
cursor_local - _view_offset, _world_center, _held_rung, _held_extent, get_body_radius_km()
|
||||
)
|
||||
_rung_index = new_index
|
||||
_held_rung = StepCanvasTransport.rung_at_index(_rung_index)
|
||||
@@ -835,7 +836,9 @@ func _centered_view_offset() -> Vector2:
|
||||
var available: Vector2 = Vector2(maxf(viewport.x - reserved_left, 1.0), viewport.y)
|
||||
var scale: float = _letterbox_scale_for(raw_footprint)
|
||||
var scaled_footprint: Vector2 = raw_footprint * scale
|
||||
return StepCanvasTransport.center_offset(scaled_footprint, available) + Vector2(reserved_left, 0.0)
|
||||
return (
|
||||
StepCanvasTransport.center_offset(scaled_footprint, available) + Vector2(reserved_left, 0.0)
|
||||
)
|
||||
|
||||
|
||||
## Hard reset to the Global opener (D-255(a): "a hard full-zoom-out reset to
|
||||
@@ -913,7 +916,6 @@ func _apply_transform() -> void:
|
||||
# Chrome
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## T-1197: the header used to be a bare ImplantHeader added straight to this
|
||||
## Control, drawn directly over the terrain canvas with no backing — every
|
||||
## OTHER implant chrome element (the legend below it) gets its scrim from
|
||||
@@ -978,9 +980,10 @@ func _refresh_screen_header() -> void:
|
||||
if _screen_header == null:
|
||||
return
|
||||
var name_label: String = _dict_str(_body, "proper_name", _dict_str(_body, "body_id", "—"))
|
||||
var spacing_km: float = StepCanvasTransport.spacing_for_rung(
|
||||
_held_rung, _held_extent, get_body_radius_km()
|
||||
) / 1000.0
|
||||
var spacing_km: float = (
|
||||
StepCanvasTransport.spacing_for_rung(_held_rung, _held_extent, get_body_radius_km())
|
||||
/ 1000.0
|
||||
)
|
||||
var title := "ATLAS — %s" % name_label.to_upper()
|
||||
var subtitle := "%s · %.3f km/gridunit" % [_held_rung.to_upper(), spacing_km]
|
||||
_screen_header.set_content(title, subtitle)
|
||||
|
||||
Reference in New Issue
Block a user