feat(ui): T-1170 B1+B2 — visibility-table split; Region skeleton chords from downstream pointers
B1 (Ruling 5c): nature block split out of atlas_window_geometry.gd (954/1000 cap pressure) into atlas_window_geometry_nature.gd; RIVER_ CLASS_VISIBLE_BY_RUNG replaced by SKELETON_CLASS_VISIBLE_BY_RUNG (Region-only now) + COURSE_CLASS_VISIBLE_BY_RUNG (District trunk+ tributary; Quarter all three — the pre-announced Quarter-rivers- return) with width/opacity companion tables as Araminta's single revisit point; deliberately opposite unknown-tag fallbacks per reader (skeleton->full, course->empty), documented. B2 (Ruling 5a): Region dot-scatter upgraded to connected chords via river_downstream — D8 direction decode (0-7 into drainage.rs's (row,col) delta table, antimeridian wrap-aware), sentinel chain ends (MOUTH=8 ring-on-land, EDGE_DRAIN=9 no ring, TERMINAL=10 reserved, decodes like EDGE_DRAIN so the future endorheic server needs no client change). Pure build_skeleton_chords() split from drawing for render-free testability. Chord clip rule (3g pick): segment clips if either endpoint OR midpoint is drawn water — three-point catches both narrow-inlet and long-chord failure modes at one extra lookup; documented. Self-caught during build: first draft misdecoded the pointer as a river_cells INDEX; rewired to direction decode against A1's real convention before leaving the branch. Dual revert-verified (direction sabotage -> 6 named failures incl. the chain-threading pin; midpoint-drop -> exactly the 1 named clip test). Suites: geometry-nature 86/86, geometry 130/130, nature-overlay 24/24; full sweep 3892 with only the 6 known pre-existing garment/gait failures untouched by this batch; gdlint clean. Tickets: T-1170 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,15 @@ extends RefCounted
|
||||
## the SAME wrap/clamp discipline every other piece of this cluster already
|
||||
## depends on, hence the preload below (no circular dependency:
|
||||
## atlas_descend_geometry.gd never references this file).
|
||||
##
|
||||
## T-1170: the T-1156 wave-1 nature-overlay (river/basin/attractor) pixel
|
||||
## mapping and per-rung visibility policy (RIVER_CLASS_*, layer1_pixel_to_*,
|
||||
## *_visible_at_rung, zoom_compensated_size) moved OUT of this file to
|
||||
## atlas_window_geometry_nature.gd (this file was at 954/1000 gdlint
|
||||
## max-file-lines when the move happened) — see that file's own header doc.
|
||||
## cell_index_for_local_offset() (T-1172, near the bottom of this file) stayed
|
||||
## here since it is shared with AtlasWindowOverlay's terrain painter, a
|
||||
## non-nature consumer.
|
||||
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
||||
|
||||
## D-243 rung spacings, metres/cell — the SAME constants
|
||||
@@ -108,93 +117,6 @@ const MAX_COVERAGE_M: Dictionary = {
|
||||
const RUNGS_FINEST_FIRST: Array = ["Quarter", "District", "Region"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1156 wave 1: per-rung nature-overlay visibility/styling policy (Araminta's
|
||||
# presentation ruling, 2026-07-23 — supersedes Tyre's provisional
|
||||
# add-detail-as-you-descend mapping the ticket brief originally carried). The
|
||||
# ladder INVERTS: Region/orbital shows the FULL skeleton (the rung whose data
|
||||
# density actually supports a "river system" read at 76 km/dot spacing);
|
||||
# District and Quarter fade the read DOWN, not up, because a single-heightmap-
|
||||
# pixel river course has nothing finer to reveal as the player descends until
|
||||
# T-1170 invents real sub-heightmap courses. This table is the one place that
|
||||
# posture lives — revisit here, and only here, when T-1170 lands. (Consts only
|
||||
# — the READER functions that consult these tables live further down, grouped
|
||||
# with the other layer1_* pixel-mapping functions per class-definitions-order.)
|
||||
# =============================================================================
|
||||
|
||||
## River class ids — mirrors server/src/atlas/body_world_state.rs
|
||||
## RiverNetwork.river_class's own doc exactly (0=stream, 1=tributary,
|
||||
## 2=trunk). A `river_class` array shorter than `river_cells` (pre-T-1156
|
||||
## payload, or the graceful-fallback empty-array case) has no per-cell class
|
||||
## to read — RIVER_CLASS_FALLBACK is what a missing entry resolves to: TRUNK,
|
||||
## so an old/absent river_class array still shows something at every rung
|
||||
## rather than silently vanishing (Dudley's `#[serde(default)]` empty-array
|
||||
## contract makes "index out of range" the normal case for a pre-T-1156
|
||||
## response, not an edge case to special-case away).
|
||||
const RIVER_CLASS_STREAM: int = 0
|
||||
const RIVER_CLASS_TRIBUTARY: int = 1
|
||||
const RIVER_CLASS_TRUNK: int = 2
|
||||
const RIVER_CLASS_FALLBACK: int = RIVER_CLASS_TRUNK
|
||||
|
||||
## Per-rung river-class visibility — which RIVER_CLASS_* ids draw at all, at
|
||||
## each granularity_v2 tag. Region shows every class (the full skeleton);
|
||||
## District shows trunk only; Quarter shows none (rivers off entirely at that
|
||||
## rung per the ruling).
|
||||
const RIVER_CLASS_VISIBLE_BY_RUNG: Dictionary = {
|
||||
"Region": [RIVER_CLASS_STREAM, RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
|
||||
"District": [RIVER_CLASS_TRUNK],
|
||||
"Quarter": [],
|
||||
}
|
||||
|
||||
## Per-rung feature-group toggles beyond river-cell class filtering — whether
|
||||
## confluences/mouths/basins/attractors draw at all at a given rung (each
|
||||
## still additionally gated by its own overlay-bar toggle, RVR/BAS/ATR, where
|
||||
## applicable — this table is the RUNG gate, the overlay bar is the PLAYER
|
||||
## gate, both must pass). Mouths get the one rung-based exception in the whole
|
||||
## table: District keeps them at full Region styling/opacity (a mouth is
|
||||
## always a landmark, per the ruling) while every other District river feature
|
||||
## is suppressed or de-emphasized.
|
||||
const CONFLUENCES_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
|
||||
const MOUTHS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": true, "Quarter": false}
|
||||
const BASINS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
|
||||
const ATTRACTORS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
|
||||
|
||||
## Per-rung river dot styling (screen-space px, at zoom=1.0 — the same
|
||||
## "canvas-local px" domain every other drawn feature in this cluster already
|
||||
## uses, scaled by the caller's own view zoom like everything else in
|
||||
## `_canvas`). District trunk dots are smaller AND drawn at reduced opacity
|
||||
## (80% — raised from the ruling's initial 60% in Araminta's PR #195 capture
|
||||
## review: at 1.6px/60% the dot was "essentially invisible without knowing
|
||||
## where to look", underselling the 'a major river crosses near here' intent;
|
||||
## 2.0px/80% keeps the fade-down ladder vs Region's 2.2px/100% without
|
||||
## reading as accidentally-erased) — the "fade down" the ruling describes;
|
||||
## Region dots are full-strength
|
||||
## opacity (alpha baked into the reused COLOR_GEN_RIVER/COLOR_GEN_MOUTH
|
||||
## constants themselves, alpha 1.0). Quarter has no entry — rivers don't draw
|
||||
## there at all, so no radius/opacity is ever looked up for that rung.
|
||||
const RIVER_DOT_RADIUS_BY_CLASS_REGION: Dictionary = {
|
||||
RIVER_CLASS_STREAM: 0.9,
|
||||
RIVER_CLASS_TRIBUTARY: 1.4,
|
||||
RIVER_CLASS_TRUNK: 2.2,
|
||||
}
|
||||
const RIVER_CONFLUENCE_RADIUS_REGION: float = 3.5
|
||||
const RIVER_DOT_RADIUS_DISTRICT_TRUNK: float = 2.0
|
||||
const RIVER_DOT_OPACITY_DISTRICT_TRUNK: float = 0.8
|
||||
|
||||
## Mouth double-ring geometry (Region AND District — mouths never de-emphasize,
|
||||
## per the ruling) — verbatim from the retired atlas_marker_overlay.gd
|
||||
## _draw_gen_rivers() (:537-539), reused exactly, not re-tuned.
|
||||
const MOUTH_RING_RADIUS: float = 5.0
|
||||
const MOUTH_HALO_RADIUS: float = 8.0
|
||||
const MOUTH_HALO_ALPHA: float = 0.30
|
||||
|
||||
## Attractor minimum-strength gate — verbatim from the retired
|
||||
## atlas_marker_overlay.gd GEN_ATTRACTOR_MIN_STRENGTH (:44). Region-only per
|
||||
## the ruling (ATTRACTORS_VISIBLE_BY_RUNG), wave 1 has no attractor rendering
|
||||
## at any other rung to gate.
|
||||
const ATTRACTOR_MIN_STRENGTH: float = 0.15
|
||||
|
||||
|
||||
## Fit-and-center: given the viewport size and the window's side length in
|
||||
## districts, compute the zoom/offset that COVERS the viewport (fills it edge
|
||||
## to edge, no side margins) and centers the composite. Mirrors AtlasViewer's
|
||||
@@ -775,155 +697,6 @@ static func centered_label_baseline(viewport_size: Vector2, text_size: Vector2)
|
||||
return center - text_size * 0.5 + Vector2(0.0, text_size.y * 0.5)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1156 wave 1: whole-body Layer-1 (river/basin/attractor) pixel-space ->
|
||||
# canvas-local mapping for the zoom ladder — the nature-overlay counterpart to
|
||||
# the district/canvas machinery above. Layer-1's `river_network`/
|
||||
# `drainage_basins`/`attractors` positions are (row, col) heightmap-pixel
|
||||
# coordinates in a `grid_w`(cols) x `grid_h`(rows) working grid (Rust
|
||||
# `Layer1Output.grid_w/grid_h` = `BodyHeightmap.width/height` = the SAME
|
||||
# `TerrainAnalysis.w/h` river/attractor extraction ran against —
|
||||
# server/src/atlas/layer1.rs, features.rs `TerrainAnalysis::analyze`). This is
|
||||
# NOT the atlas_marker_overlay.gd `_gen_pos()` texture-fraction mapping (that
|
||||
# maps onto a DISPLAYED heightmap texture on the retired planetary screen) —
|
||||
# the ladder has no resident heightmap texture at all, so pixel positions must
|
||||
# go all the way to WORLD METRES -> DISTRICT space -> canvas-local, the same
|
||||
# frame district_to_canvas_local() already establishes for every other drawn
|
||||
# feature on this screen.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Heightmap pixel (row, col) -> absolute world metres (wx east, wy south),
|
||||
## mirroring server/src/atlas/district_profile.rs's `pixel_to_world_m()`
|
||||
## EXACTLY (verified against that function's source, not assumed): longitude
|
||||
## WRAPS and is addressed by the plain column fraction (`col / grid_w`) against
|
||||
## the full circumference — column 0 sits at world/longitude 0, no -0.5
|
||||
## centering unlike latitude. Latitude CLAMPS at the poles and is addressed by
|
||||
## `row / (grid_h - 1) - 0.5`, i.e. row 0 is exactly the pole (lat_frac -0.5 =
|
||||
## north pole = wy negative-most) and row (grid_h - 1) is exactly the opposite
|
||||
## pole (lat_frac +0.5 = south pole = wy positive-most) — the SAME "row
|
||||
## increases southward" convention AtlasDescendGeometry.district_pos_at()
|
||||
## already assumes for its own (inverse-direction) pixel<->district mapping,
|
||||
## confirmed here to be the same convention layer1's grid uses, not a
|
||||
## different one that happens to share variable names.
|
||||
##
|
||||
## No-radius bodies (body_radius_km <= 0, tiny test bodies): 1 heightmap pixel
|
||||
## = 1 district-spacing metre, matching pixel_to_world_m()'s own no-radius
|
||||
## fallback (`px * scale::DISTRICT_M`) and district_pos_at()'s no-radius
|
||||
## branch on the other side of this mapping. Uses this file's own
|
||||
## DISTRICT_SPACING_M (the same 2,048 m/district constant, this file's
|
||||
## existing name for it — NOT a re-derivation).
|
||||
static func layer1_pixel_to_world_m(
|
||||
row: float, col: float, grid_w: float, grid_h: float, body_radius_km: float
|
||||
) -> Vector2:
|
||||
if grid_w <= 0.0 or grid_h <= 0.0:
|
||||
return Vector2.ZERO
|
||||
if body_radius_km <= 0.0:
|
||||
return Vector2(col * DISTRICT_SPACING_M, row * DISTRICT_SPACING_M)
|
||||
var circumference_m: float = TAU * body_radius_km * 1000.0
|
||||
var meridian_m: float = PI * body_radius_km * 1000.0
|
||||
var wx: float = (col / grid_w) * circumference_m
|
||||
var lat_frac: float = (row / (grid_h - 1.0) - 0.5) if grid_h > 1.0 else 0.0
|
||||
var wy: float = lat_frac * meridian_m
|
||||
return Vector2(wx, wy)
|
||||
|
||||
|
||||
## World metres -> fractional DistrictPos (NOT rounded to an integer district
|
||||
## — a river dot's true position is sub-district-precise even though the
|
||||
## window grid itself is district-granular; rounding here would visibly snap
|
||||
## every river pixel onto a district lattice). DISTRICT_SPACING_M is this
|
||||
## file's own existing constant (2,048 m/district, D-243) — one division, no
|
||||
## re-derivation.
|
||||
static func world_m_to_district(world_m: Vector2) -> Vector2:
|
||||
return world_m / DISTRICT_SPACING_M
|
||||
|
||||
|
||||
## The full pixel(row,col) -> canvas-local composition a nature-overlay draw
|
||||
## call needs in one step: heightmap pixel -> world metres -> fractional
|
||||
## district -> canvas-local (via the EXISTING district_to_canvas_local(),
|
||||
## reused verbatim so a river dot lands in exactly the same coordinate frame
|
||||
## every other drawn feature on this screen already agrees on — pan/zoom/rung
|
||||
## crossings all move the SAME transform under everything drawn into
|
||||
## `_canvas`). Wrap resolution (nearest_wrap_image()) is the CALLER's job, same
|
||||
## split the tile mosaic draw path already uses — this function's `district`
|
||||
## output is the RAW (un-wrapped) fractional position; a caller iterating
|
||||
## river cells against a specific held window picks the nearest wrap-image of
|
||||
## the COLUMN only (rows never wrap, matching every other wrap-aware caller in
|
||||
## this cluster).
|
||||
static func layer1_pixel_to_canvas_local(
|
||||
row: float,
|
||||
col: float,
|
||||
grid_w: float,
|
||||
grid_h: float,
|
||||
body_radius_km: float,
|
||||
held_center: Vector2i,
|
||||
held_n: int,
|
||||
cell_pixel_size: float
|
||||
) -> Vector2:
|
||||
var world_m: Vector2 = layer1_pixel_to_world_m(row, col, grid_w, grid_h, body_radius_km)
|
||||
var district: Vector2 = world_m_to_district(world_m)
|
||||
return district_to_canvas_local(district, held_center, held_n, cell_pixel_size)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1156 wave 1: per-rung nature-overlay visibility policy READERS. The policy
|
||||
# TABLES themselves (RIVER_CLASS_VISIBLE_BY_RUNG etc.) live up in the
|
||||
# top-of-file const block per class-definitions-order (Araminta's ruling,
|
||||
# 2026-07-23, is documented there).
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Whether a river cell of `river_class` should draw at `granularity_v2`. An
|
||||
## unrecognized rung tag falls back to Region's (fullest) visibility set —
|
||||
## matching this cluster's existing "unrecognized -> most permissive/safest
|
||||
## already-shipped behavior" posture (see AtlasWindowOverlay._filter_for_
|
||||
## granularity_v2()'s own doc for the same fallback shape, there choosing the
|
||||
## safer LINEAR filter for an unknown tag).
|
||||
static func river_class_visible_at_rung(river_class: int, granularity_v2: String) -> bool:
|
||||
var visible: Array = RIVER_CLASS_VISIBLE_BY_RUNG.get(
|
||||
granularity_v2, RIVER_CLASS_VISIBLE_BY_RUNG["Region"]
|
||||
)
|
||||
return visible.has(river_class)
|
||||
|
||||
|
||||
static func confluences_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(CONFLUENCES_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
static func mouths_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(MOUTHS_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
static func basins_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(BASINS_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
static func attractors_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(ATTRACTORS_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
## Coordinator live-eyeball finding (2026-07-23): Araminta's ruling specifies
|
||||
## nature-overlay marker sizes as SCREEN-SPACE px, constant regardless of
|
||||
## zoom — but every draw call in this cluster (river dots, mouth rings, basin
|
||||
## line widths) executes inside `_canvas`, a Node2D whose `.scale` IS
|
||||
## `_view_zoom` (AtlasWindowViewer._apply_transform()). A raw radius/width
|
||||
## constant handed to draw_circle()/draw_arc()/draw_polyline() therefore gets
|
||||
## multiplied by `_view_zoom` at render time — invisible at the Region
|
||||
## orbital tile mosaic's fit zoom (~0.0063 for Lendel: a 2.2px trunk-river
|
||||
## dot rasterizes at ~0.014 screen px, sub-pixel), even though the SAME
|
||||
## drawing code produces a correctly-sized (visible) mouth ring at District's
|
||||
## much larger fit zoom (~3.75, live capture confirmed this). The fix: every
|
||||
## marker's draw-time radius/width must be pre-divided by `view_zoom` so the
|
||||
## canvas transform's multiply cancels back out to the ruling's literal
|
||||
## screen-space value. `view_zoom` is clamped to a small positive floor
|
||||
## (MIN_ZOOM's own order of magnitude) to avoid a divide-by-zero/near-zero
|
||||
## blowup on a degenerate zero-zoom caller — this floor is far below any
|
||||
## legal `_view_zoom` (AtlasWindowViewer.MIN_ZOOM = 0.0005), so it is inert
|
||||
## for every real caller and only guards a malformed test input.
|
||||
static func zoom_compensated_size(screen_space_size: float, view_zoom: float) -> float:
|
||||
return screen_space_size / maxf(view_zoom, 0.0001)
|
||||
|
||||
|
||||
## T-1172 round 2 (coordinator's "reconsider the split" ask): the SHARED
|
||||
## cell-index formula both AtlasWindowOverlay's terrain painter (which builds
|
||||
## the drawn `grid_side x grid_side` per-cell texture — `i = row * grid_side
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
extends RefCounted
|
||||
|
||||
## Nature-overlay (river/basin/attractor) pure geometry + per-rung policy —
|
||||
## split out of atlas_window_geometry.gd (T-1170, that file was at 954/1000
|
||||
## gdlint max-file-lines when this batch started) exactly the same way
|
||||
## test_atlas_window_geometry_nature.gd was already split from
|
||||
## test_atlas_window_geometry.gd — one file, one concern, room to grow. Every
|
||||
## symbol below moved VERBATIM from atlas_window_geometry.gd; no behavior
|
||||
## change in this split itself. atlas_window_nature_overlay.gd is the only
|
||||
## runtime consumer (verified: grep across client/ before the move) and now
|
||||
## preloads THIS file instead.
|
||||
##
|
||||
## Contains:
|
||||
## - T-1156 wave 1 whole-body Layer-1 pixel-space -> canvas-local mapping
|
||||
## (layer1_pixel_to_world_m/world_m_to_district/layer1_pixel_to_canvas_local)
|
||||
## - T-1156 wave 1 per-rung skeleton visibility/styling policy (RIVER_CLASS_*,
|
||||
## CONFLUENCES/MOUTHS/BASINS/ATTRACTORS_VISIBLE_BY_RUNG, dot/ring/attractor
|
||||
## size consts) — RENAMED this batch (T-1170 Ruling 5c, see below) from
|
||||
## RIVER_CLASS_VISIBLE_BY_RUNG to SKELETON_CLASS_VISIBLE_BY_RUNG.
|
||||
## - zoom_compensated_size() — the screen-space marker-size zoom-compensation
|
||||
## fix (coordinator live-eyeball finding, 2026-07-23).
|
||||
##
|
||||
## atlas_window_geometry.gd retains cell_index_for_local_offset() (T-1172
|
||||
## round 2) rather than moving it here — that function is shared with
|
||||
## AtlasWindowOverlay's terrain painter (a non-nature consumer), so it stays
|
||||
## on the base file both files already depend on, avoiding a nature-file ->
|
||||
## base-file dependency for a symbol the base file's own painter needs too.
|
||||
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
||||
|
||||
## atlas_window_geometry.gd never depends on this file (verified: no preload
|
||||
## of atlas_window_geometry_nature.gd anywhere in that file) — so preloading
|
||||
## it back here is safe, no circular dependency, matching the pattern
|
||||
## AtlasWindowOverlay/AtlasWindowWaterClip already use for
|
||||
## AtlasWindowGeometryRef.
|
||||
const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
|
||||
|
||||
## D-243 district spacing, metres/district — this file's own copy of
|
||||
## AtlasWindowGeometry.DISTRICT_SPACING_M (duplicated, not preloaded-and-read,
|
||||
## matching this cluster's existing "each file owns its own reading of a
|
||||
## small pure constant rather than force a dependency" precedent —
|
||||
## atlas_overlay_colors.gd's header doc states this explicitly; the same
|
||||
## rationale that kept atlas_window_water_clip.gd's cell_grid_side_for_window()
|
||||
## a deliberate duplicate rather than a shared call applies here). MUST stay
|
||||
## numerically identical to the base file's constant — both ultimately trace
|
||||
## to D-243's 2,048 m district spacing, which is locked project vocabulary,
|
||||
## not a value expected to drift.
|
||||
const DISTRICT_SPACING_M: float = 2048.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1156 wave 1 / T-1170: per-rung nature-overlay visibility/styling policy
|
||||
# (Araminta's presentation ruling, 2026-07-23 — supersedes Tyre's provisional
|
||||
# add-detail-as-you-descend mapping the ticket brief originally carried).
|
||||
#
|
||||
# T-1170 Ruling 5c (Tyre, 2026-07-23) — THE REVISIT, split on the carrier
|
||||
# axis: the single RIVER_CLASS_VISIBLE_BY_RUNG table is replaced by TWO
|
||||
# tables, one per presentation surface —
|
||||
# - SKELETON_CLASS_VISIBLE_BY_RUNG: the Region+ whole-body skeleton-chord
|
||||
# path (Ruling 5a) — unchanged posture from wave 1, Region shows every
|
||||
# class.
|
||||
# - COURSE_CLASS_VISIBLE_BY_RUNG: the District/Quarter windowed course-
|
||||
# polyline path (Ruling 5b) — THIS is where "Quarter rivers return"
|
||||
# (the pre-announced wave-1 fade-down revisit executes): District shows
|
||||
# trunk+tributary, Quarter shows all three classes.
|
||||
# Companion per-class width/opacity tables (COURSE_CLASS_WIDTH_PX/
|
||||
# COURSE_CLASS_OPACITY) carry FUNCTIONAL DEFAULTS per the ruling brief
|
||||
# (trunk widest ~2.2px, tributary ~1.4px, stream ~0.9px, screen-space via the
|
||||
# existing zoom-compensation discipline) — Araminta's forthcoming presentation
|
||||
# ruling edits THESE TABLES AND ONLY THESE TABLES, same single-revisit-point
|
||||
# discipline wave 1 established for RIVER_CLASS_VISIBLE_BY_RUNG itself.
|
||||
# =============================================================================
|
||||
|
||||
## River class ids — mirrors server/src/atlas/body_world_state.rs
|
||||
## RiverNetwork.river_class's own doc exactly (0=stream, 1=tributary,
|
||||
## 2=trunk). A `river_class` array shorter than `river_cells` (pre-T-1156
|
||||
## payload, or the graceful-fallback empty-array case) has no per-cell class
|
||||
## to read — RIVER_CLASS_FALLBACK is what a missing entry resolves to: TRUNK,
|
||||
## so an old/absent river_class array still shows something at every rung
|
||||
## rather than silently vanishing (Dudley's `#[serde(default)]` empty-array
|
||||
## contract makes "index out of range" the normal case for a pre-T-1156
|
||||
## response, not an edge case to special-case away).
|
||||
const RIVER_CLASS_STREAM: int = 0
|
||||
const RIVER_CLASS_TRIBUTARY: int = 1
|
||||
const RIVER_CLASS_TRUNK: int = 2
|
||||
const RIVER_CLASS_FALLBACK: int = RIVER_CLASS_TRUNK
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1170 Ruling 2a-2d/5a: river_downstream D8 pointer decode — the wire
|
||||
# convention `RiverNetwork.river_downstream` (Vec<u8>, index-aligned with
|
||||
# river_cells) encodes per river cell: a DIRECTION 0-7 into an adjacent D8
|
||||
# neighbor, or a SENTINEL >= RIVER_DOWNSTREAM_SENTINEL_BASE marking a chain
|
||||
# end (MOUTH/EDGE_DRAIN/reserved-TERMINAL). CONFIRMED against Dudley's A1
|
||||
# (server/src/atlas/drainage.rs:35-44, landed 0fea69feb, relayed by the
|
||||
# coordinator) — these are the REAL shipped values, final until/unless the
|
||||
# server's own encoding changes, in which case this is the one place to
|
||||
# repoint.
|
||||
# =============================================================================
|
||||
|
||||
## Sentinel base — any river_downstream value >= this is a chain-end
|
||||
## sentinel, not a direction. Direction values are 0-7 (8 real D8 neighbors);
|
||||
## sentinels start immediately above at 8.
|
||||
const RIVER_DOWNSTREAM_SENTINEL_BASE: int = 8
|
||||
const RIVER_DOWNSTREAM_MOUTH: int = 8
|
||||
const RIVER_DOWNSTREAM_EDGE_DRAIN: int = 9
|
||||
## TERMINAL is reserved/unused in round 1 (Ruling 2c/7b — future endorheic
|
||||
## basin support) — this client never expects to see it on real data yet, but
|
||||
## decodes it identically to EDGE_DRAIN (chain end, no ring) rather than
|
||||
## treating an unrecognized-but-in-sentinel-range value as an error, so a
|
||||
## future server enabling TERMINAL needs no client change to degrade
|
||||
## gracefully (it would just draw as an unmarked chain end until a future
|
||||
## ticket gives it its own ring treatment, exactly EDGE_DRAIN's own current
|
||||
## disposition).
|
||||
const RIVER_DOWNSTREAM_TERMINAL: int = 10
|
||||
|
||||
## D8 direction index (0-7) -> (row_delta, col_delta), CONFIRMED against
|
||||
## drainage.rs:35-44's own fdir table order (not assumed/guessed — the
|
||||
## coordinator relayed this explicitly from Dudley's A1 source): row
|
||||
## increases SOUTH (matching layer1_pixel_to_world_m()'s own "row 0 = north
|
||||
## pole" convention, confirmed the same convention on both sides of this
|
||||
## mapping), col increases EAST and WRAPS at the antimeridian (handled by the
|
||||
## caller's existing nearest-wrap-image discipline, same as every other
|
||||
## column value flowing through this file — this table itself has no wrap
|
||||
## concept, it is pure grid-adjacency).
|
||||
## 0 = N (-1, 0) 4 = NE (-1, 1)
|
||||
## 1 = S ( 1, 0) 5 = NW (-1, -1)
|
||||
## 2 = E ( 0, 1) 6 = SE ( 1, 1)
|
||||
## 3 = W ( 0, -1) 7 = SW ( 1, -1)
|
||||
const D8_DIRECTION_DELTAS: Array = [
|
||||
Vector2i(-1, 0), # 0 N
|
||||
Vector2i(1, 0), # 1 S
|
||||
Vector2i(0, 1), # 2 E
|
||||
Vector2i(0, -1), # 3 W
|
||||
Vector2i(-1, 1), # 4 NE
|
||||
Vector2i(-1, -1), # 5 NW
|
||||
Vector2i(1, 1), # 6 SE
|
||||
Vector2i(1, -1), # 7 SW
|
||||
]
|
||||
|
||||
## Region+ SKELETON path (Ruling 5a) — the whole-body chord-chain draw, built
|
||||
## from river_downstream. Region shows every class (the full skeleton) — this
|
||||
## table's posture is UNCHANGED from wave 1's original
|
||||
## RIVER_CLASS_VISIBLE_BY_RUNG (renamed, not re-tuned). District/Quarter keys
|
||||
## are retained (both empty) purely so a caller that queries this table by an
|
||||
## unexpected rung tag gets the same documented "nothing visible" answer wave
|
||||
## 1 shipped, rather than a KeyError — the SKELETON path itself is only ever
|
||||
## drawn at Region in practice (District/Quarter draw courses, the OTHER
|
||||
## table, per Ruling 5b).
|
||||
const SKELETON_CLASS_VISIBLE_BY_RUNG: Dictionary = {
|
||||
"Region": [RIVER_CLASS_STREAM, RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
|
||||
"District": [],
|
||||
"Quarter": [],
|
||||
}
|
||||
|
||||
## District/Quarter COURSE path (Ruling 5b/5c) — the windowed polyline draw,
|
||||
## built from DistrictWindowLayer.courses. District: trunk+tributary (streams
|
||||
## stay off at District — the ruling's own example enumeration). Quarter:
|
||||
## ALL THREE classes — "Quarter rivers return", the pre-announced wave-1
|
||||
## fade-down revisit executing here. Region is not a key here at all (Region
|
||||
## never draws courses — it draws the skeleton chord chain, the OTHER table)
|
||||
## — a caller must not query this table at Region; river_class_visible_at_rung()
|
||||
## style readers for this table live on this file too and fall back safely
|
||||
## for an unrecognized tag (see course_class_visible_at_rung()'s own doc).
|
||||
const COURSE_CLASS_VISIBLE_BY_RUNG: Dictionary = {
|
||||
"District": [RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
|
||||
"Quarter": [RIVER_CLASS_STREAM, RIVER_CLASS_TRIBUTARY, RIVER_CLASS_TRUNK],
|
||||
}
|
||||
|
||||
## Per-rung feature-group toggles beyond river-cell class filtering — whether
|
||||
## confluences/mouths/basins/attractors draw at all at a given rung (each
|
||||
## still additionally gated by its own overlay-bar toggle, RVR/BAS/ATR, where
|
||||
## applicable — this table is the RUNG gate, the overlay bar is the PLAYER
|
||||
## gate, both must pass). Mouths get the one rung-based exception in the whole
|
||||
## table: District keeps them at full Region styling/opacity (a mouth is
|
||||
## always a landmark, per the ruling) while every other District river feature
|
||||
## is suppressed or de-emphasized.
|
||||
const CONFLUENCES_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
|
||||
const MOUTHS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": true, "Quarter": false}
|
||||
const BASINS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
|
||||
const ATTRACTORS_VISIBLE_BY_RUNG: Dictionary = {"Region": true, "District": false, "Quarter": false}
|
||||
|
||||
## Per-rung river dot styling (screen-space px, at zoom=1.0 — the same
|
||||
## "canvas-local px" domain every other drawn feature in this cluster already
|
||||
## uses, scaled by the caller's own view zoom like everything else in
|
||||
## `_canvas`). District trunk dots are smaller AND drawn at reduced opacity
|
||||
## (80% — raised from the ruling's initial 60% in Araminta's PR #195 capture
|
||||
## review: at 1.6px/60% the dot was "essentially invisible without knowing
|
||||
## where to look", underselling the 'a major river crosses near here' intent;
|
||||
## 2.0px/80% keeps the fade-down ladder vs Region's 2.2px/100% without
|
||||
## reading as accidentally-erased) — the "fade down" the ruling describes;
|
||||
## Region dots are full-strength opacity (alpha baked into the reused
|
||||
## COLOR_GEN_RIVER/COLOR_GEN_MOUTH constants themselves, alpha 1.0).
|
||||
##
|
||||
## T-1170: these RIVER_DOT_* consts now describe the Region SKELETON path
|
||||
## ONLY (Ruling 5a's chord-chain draw reuses the same per-class radii the old
|
||||
## dot-scatter used — chords are drawn at these widths, not a new table).
|
||||
## RIVER_DOT_RADIUS_DISTRICT_TRUNK/RIVER_DOT_OPACITY_DISTRICT_TRUNK are DEAD
|
||||
## at District now that District draws courses (Ruling 5b/3g retires the
|
||||
## District dot-scatter entirely) — left in place, unread by any T-1170 draw
|
||||
## path, rather than deleted mid-batch: B3 (course polyline drawing) is the
|
||||
## change that stops calling them; deleting here would be a premature edit to
|
||||
## a still-referenced-by-wave-1-code constant ahead of that landing.
|
||||
const RIVER_DOT_RADIUS_BY_CLASS_REGION: Dictionary = {
|
||||
RIVER_CLASS_STREAM: 0.9,
|
||||
RIVER_CLASS_TRIBUTARY: 1.4,
|
||||
RIVER_CLASS_TRUNK: 2.2,
|
||||
}
|
||||
const RIVER_CONFLUENCE_RADIUS_REGION: float = 3.5
|
||||
const RIVER_DOT_RADIUS_DISTRICT_TRUNK: float = 2.0
|
||||
const RIVER_DOT_OPACITY_DISTRICT_TRUNK: float = 0.8
|
||||
|
||||
## T-1170 Ruling 5c: course polyline per-class width/opacity, District/Quarter
|
||||
## COURSE path companion tables to COURSE_CLASS_VISIBLE_BY_RUNG above.
|
||||
## FUNCTIONAL DEFAULTS ONLY (the ruling's own numbers) — Araminta's
|
||||
## forthcoming presentation ruling edits these two tables and only these two
|
||||
## tables, same discipline as every other single-revisit-point table in this
|
||||
## file. Widths are screen-space px at zoom=1.0, routed through
|
||||
## zoom_compensated_size()/the caller's `_zs()` wrapper before reaching
|
||||
## draw_polyline() exactly like every other marker size in this cluster (PR
|
||||
## #195's stroke-width miss is the standing regression class this discipline
|
||||
## exists to prevent — see zoom_compensated_size()'s own doc). Opacities are
|
||||
## plain [0,1] alpha multipliers on COLOR_GEN_RIVER, no zoom involvement.
|
||||
## Trunk widest / stream thinnest, matching the Region skeleton's own
|
||||
## per-class radius ordering (RIVER_DOT_RADIUS_BY_CLASS_REGION) so the visual
|
||||
## "trunk is the biggest river" read is consistent whether the player is
|
||||
## looking at the Region chord chain or a District/Quarter course polyline.
|
||||
const COURSE_CLASS_WIDTH_PX: Dictionary = {
|
||||
RIVER_CLASS_STREAM: 0.9,
|
||||
RIVER_CLASS_TRIBUTARY: 1.4,
|
||||
RIVER_CLASS_TRUNK: 2.2,
|
||||
}
|
||||
const COURSE_CLASS_OPACITY: Dictionary = {
|
||||
RIVER_CLASS_STREAM: 0.8,
|
||||
RIVER_CLASS_TRIBUTARY: 0.9,
|
||||
RIVER_CLASS_TRUNK: 1.0,
|
||||
}
|
||||
|
||||
## Mouth double-ring geometry (Region AND District — mouths never de-emphasize,
|
||||
## per the ruling) — verbatim from the retired atlas_marker_overlay.gd
|
||||
## _draw_gen_rivers() (:537-539), reused exactly, not re-tuned. T-1170: also
|
||||
## the mouth-ring geometry for REAL course termini (Ruling 5b/3e) — one
|
||||
## geometry, both presentation surfaces (skeleton chord ends at Region,
|
||||
## course polyline ends at District/Quarter).
|
||||
const MOUTH_RING_RADIUS: float = 5.0
|
||||
const MOUTH_HALO_RADIUS: float = 8.0
|
||||
const MOUTH_HALO_ALPHA: float = 0.30
|
||||
|
||||
## Attractor minimum-strength gate — verbatim from the retired
|
||||
## atlas_marker_overlay.gd GEN_ATTRACTOR_MIN_STRENGTH (:44). Region-only per
|
||||
## the ruling (ATTRACTORS_VISIBLE_BY_RUNG), wave 1 has no attractor rendering
|
||||
## at any other rung to gate.
|
||||
const ATTRACTOR_MIN_STRENGTH: float = 0.15
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1156 wave 1: whole-body Layer-1 (river/basin/attractor) pixel-space ->
|
||||
# canvas-local mapping for the zoom ladder — the nature-overlay counterpart to
|
||||
# the district/canvas machinery in atlas_window_geometry.gd. Layer-1's
|
||||
# `river_network`/`drainage_basins`/`attractors` positions are (row, col)
|
||||
# heightmap-pixel coordinates in a `grid_w`(cols) x `grid_h`(rows) working
|
||||
# grid (Rust `Layer1Output.grid_w/grid_h` = `BodyHeightmap.width/height` = the
|
||||
# SAME `TerrainAnalysis.w/h` river/attractor extraction ran against —
|
||||
# server/src/atlas/layer1.rs, features.rs `TerrainAnalysis::analyze`). This is
|
||||
# NOT the atlas_marker_overlay.gd `_gen_pos()` texture-fraction mapping (that
|
||||
# maps onto a DISPLAYED heightmap texture on the retired planetary screen) —
|
||||
# the ladder has no resident heightmap texture at all, so pixel positions must
|
||||
# go all the way to WORLD METRES -> DISTRICT space -> canvas-local, the same
|
||||
# frame AtlasWindowGeometry.district_to_canvas_local() already establishes for
|
||||
# every other drawn feature on this screen.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Heightmap pixel (row, col) -> absolute world metres (wx east, wy south),
|
||||
## mirroring server/src/atlas/district_profile.rs's `pixel_to_world_m()`
|
||||
## EXACTLY (verified against that function's source, not assumed): longitude
|
||||
## WRAPS and is addressed by the plain column fraction (`col / grid_w`) against
|
||||
## the full circumference — column 0 sits at world/longitude 0, no -0.5
|
||||
## centering unlike latitude. Latitude CLAMPS at the poles and is addressed by
|
||||
## `row / (grid_h - 1) - 0.5`, i.e. row 0 is exactly the pole (lat_frac -0.5 =
|
||||
## north pole = wy negative-most) and row (grid_h - 1) is exactly the opposite
|
||||
## pole (lat_frac +0.5 = south pole = wy positive-most) — the SAME "row
|
||||
## increases southward" convention AtlasDescendGeometry.district_pos_at()
|
||||
## already assumes for its own (inverse-direction) pixel<->district mapping,
|
||||
## confirmed here to be the same convention layer1's grid uses, not a
|
||||
## different one that happens to share variable names.
|
||||
##
|
||||
## No-radius bodies (body_radius_km <= 0, tiny test bodies): 1 heightmap pixel
|
||||
## = 1 district-spacing metre, matching pixel_to_world_m()'s own no-radius
|
||||
## fallback (`px * scale::DISTRICT_M`) and district_pos_at()'s no-radius
|
||||
## branch on the other side of this mapping. Uses this file's own
|
||||
## DISTRICT_SPACING_M (the same 2,048 m/district constant — see that const's
|
||||
## own doc for why it's a deliberate duplicate, not a preload-and-read).
|
||||
static func layer1_pixel_to_world_m(
|
||||
row: float, col: float, grid_w: float, grid_h: float, body_radius_km: float
|
||||
) -> Vector2:
|
||||
if grid_w <= 0.0 or grid_h <= 0.0:
|
||||
return Vector2.ZERO
|
||||
if body_radius_km <= 0.0:
|
||||
return Vector2(col * DISTRICT_SPACING_M, row * DISTRICT_SPACING_M)
|
||||
var circumference_m: float = TAU * body_radius_km * 1000.0
|
||||
var meridian_m: float = PI * body_radius_km * 1000.0
|
||||
var wx: float = (col / grid_w) * circumference_m
|
||||
var lat_frac: float = (row / (grid_h - 1.0) - 0.5) if grid_h > 1.0 else 0.0
|
||||
var wy: float = lat_frac * meridian_m
|
||||
return Vector2(wx, wy)
|
||||
|
||||
|
||||
## World metres -> fractional DistrictPos (NOT rounded to an integer district
|
||||
## — a river dot's true position is sub-district-precise even though the
|
||||
## window grid itself is district-granular; rounding here would visibly snap
|
||||
## every river pixel onto a district lattice). DISTRICT_SPACING_M is this
|
||||
## file's own existing constant (2,048 m/district, D-243) — one division, no
|
||||
## re-derivation.
|
||||
static func world_m_to_district(world_m: Vector2) -> Vector2:
|
||||
return world_m / DISTRICT_SPACING_M
|
||||
|
||||
|
||||
## The full pixel(row,col) -> canvas-local composition a nature-overlay draw
|
||||
## call needs in one step: heightmap pixel -> world metres -> fractional
|
||||
## district -> canvas-local (via AtlasWindowGeometry.district_to_canvas_local(),
|
||||
## reused verbatim so a river dot lands in exactly the same coordinate frame
|
||||
## every other drawn feature on this screen already agrees on — pan/zoom/rung
|
||||
## crossings all move the SAME transform under everything drawn into
|
||||
## `_canvas`). Wrap resolution (AtlasWindowGeometry.nearest_wrap_image()) is
|
||||
## the CALLER's job, same split the tile mosaic draw path already uses — this
|
||||
## function's `district` output is the RAW (un-wrapped) fractional position; a
|
||||
## caller iterating river cells against a specific held window picks the
|
||||
## nearest wrap-image of the COLUMN only (rows never wrap, matching every
|
||||
## other wrap-aware caller in this cluster).
|
||||
static func layer1_pixel_to_canvas_local(
|
||||
row: float,
|
||||
col: float,
|
||||
grid_w: float,
|
||||
grid_h: float,
|
||||
body_radius_km: float,
|
||||
held_center: Vector2i,
|
||||
held_n: int,
|
||||
cell_pixel_size: float
|
||||
) -> Vector2:
|
||||
var world_m: Vector2 = layer1_pixel_to_world_m(row, col, grid_w, grid_h, body_radius_km)
|
||||
var district: Vector2 = world_m_to_district(world_m)
|
||||
return AtlasWindowGeometryRef.district_to_canvas_local(district, held_center, held_n, cell_pixel_size)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1170 Ruling 2a-2d/5a: river_downstream D8 pointer decode.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Decode one river cell's `river_downstream` wire value into its downstream
|
||||
## neighbor's (row, col) heightmap-pixel position, or `null` if the value is
|
||||
## a chain-end sentinel (MOUTH/EDGE_DRAIN/TERMINAL) or an out-of-range/
|
||||
## malformed direction. `row`/`col` are the UPSTREAM cell's own pixel
|
||||
## position (float, matching this file's own row/col domain everywhere
|
||||
## else); the return value (when non-null) is a Vector2 in that SAME
|
||||
## (row, col) pixel domain — NOT yet converted to world metres/district/
|
||||
## canvas-local, that conversion is the caller's job via the usual
|
||||
## layer1_pixel_to_world_m()/world_m_to_district() pipeline, exactly as if
|
||||
## the target were itself an entry read out of `river_cells`.
|
||||
##
|
||||
## Deliberately returns the RAW grid-adjacent position rather than looking it
|
||||
## up in a `river_cells` array — a D8 downstream pointer always names a real
|
||||
## adjacent grid cell by construction (that is what D8 flow direction means),
|
||||
## whether or not that specific cell independently appears in whatever
|
||||
## (possibly filtered) `river_cells` list the caller is iterating.
|
||||
static func d8_downstream_target(row: float, col: float, downstream_raw: int) -> Variant:
|
||||
if downstream_raw < 0 or downstream_raw >= RIVER_DOWNSTREAM_SENTINEL_BASE:
|
||||
return null # sentinel or malformed — no real direction to decode
|
||||
var delta: Vector2i = D8_DIRECTION_DELTAS[downstream_raw]
|
||||
return Vector2(row + float(delta.x), col + float(delta.y))
|
||||
|
||||
|
||||
## T-1170 Ruling 5a — pure chord-chain CONSTRUCTION (no draw calls, no water
|
||||
## clip, no canvas-local conversion): given `river_cells`/`river_class`/
|
||||
## `river_downstream` (the raw decoded river_network sub-dict arrays) and a
|
||||
## `granularity_v2` rung tag, returns an Array of
|
||||
## `{"from": Vector2, "to": Vector2, "cls": int}` dicts — one per river cell
|
||||
## whose class is visible at this rung AND whose river_downstream pointer
|
||||
## resolves to a real direction (not a sentinel, not out of range, not
|
||||
## missing). `from`/`to` are in the SAME (row, col) heightmap-pixel domain
|
||||
## `river_cells` entries themselves use — the caller converts to world
|
||||
## metres/district/canvas-local and applies the water clip, exactly as if it
|
||||
## had built this list inline (this function exists so that CONSTRUCTION is
|
||||
## unit-testable without a live render pass — draw_line() itself requires
|
||||
## one, per this cluster's own "pure function tests are the gate" draw-smoke
|
||||
## caveat, so the chain-walking logic that actually decides WHICH segments
|
||||
## exist must not be entangled with the draw call that paints them).
|
||||
##
|
||||
## Split out of AtlasWindowNatureOverlay._draw_skeleton_chords() specifically
|
||||
## so a test can assert "this exact set of segments was constructed from
|
||||
## this exact fixture" (including the sentinel-chain-end and malformed-input
|
||||
## cases) without a SubViewport/render context — matching this file's
|
||||
## existing "geometry/construction here, draw calls in the overlay node"
|
||||
## split for every other piece of this cluster.
|
||||
static func build_skeleton_chords(
|
||||
river_cells: Array, river_class: Array, river_downstream: Array, granularity_v2: String
|
||||
) -> Array:
|
||||
var chords: Array = []
|
||||
for idx in range(river_cells.size()):
|
||||
var c: Variant = river_cells[idx]
|
||||
if not (c is Array and c.size() >= 2):
|
||||
continue
|
||||
var cls: int = int(river_class[idx]) if idx < river_class.size() else RIVER_CLASS_FALLBACK
|
||||
if not skeleton_class_visible_at_rung(cls, granularity_v2):
|
||||
continue
|
||||
if idx >= river_downstream.size():
|
||||
continue # no downstream pointer for this cell yet — no segment
|
||||
var downstream_raw: int = int(river_downstream[idx])
|
||||
var row: float = float(c[0])
|
||||
var col: float = float(c[1])
|
||||
var target: Variant = d8_downstream_target(row, col, downstream_raw)
|
||||
if target == null:
|
||||
continue # sentinel (MOUTH/EDGE_DRAIN/TERMINAL) or malformed direction — chain end
|
||||
chords.append({"from": Vector2(row, col), "to": target, "cls": cls})
|
||||
return chords
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# T-1156 wave 1 / T-1170: per-rung nature-overlay visibility policy READERS.
|
||||
# The policy TABLES themselves live up in the top-of-file const block per
|
||||
# class-definitions-order.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## Whether a river cell of `river_class` should draw on the Region+ SKELETON
|
||||
## path (Ruling 5a) at `granularity_v2`. An unrecognized rung tag falls back
|
||||
## to Region's (fullest) visibility set — matching this cluster's existing
|
||||
## "unrecognized -> most permissive/safest already-shipped behavior" posture
|
||||
## (see AtlasWindowOverlay._filter_for_granularity_v2()'s own doc for the same
|
||||
## fallback shape, there choosing the safer LINEAR filter for an unknown tag).
|
||||
static func skeleton_class_visible_at_rung(river_class: int, granularity_v2: String) -> bool:
|
||||
var visible: Array = SKELETON_CLASS_VISIBLE_BY_RUNG.get(
|
||||
granularity_v2, SKELETON_CLASS_VISIBLE_BY_RUNG["Region"]
|
||||
)
|
||||
return visible.has(river_class)
|
||||
|
||||
|
||||
## Whether a river class should draw on the District/Quarter COURSE path
|
||||
## (Ruling 5b) at `granularity_v2`. No Region key exists in
|
||||
## COURSE_CLASS_VISIBLE_BY_RUNG (Region never draws courses) — an unrecognized
|
||||
## OR Region tag both fall back to an EMPTY array (nothing visible), the
|
||||
## inverse fallback posture from skeleton_class_visible_at_rung() above,
|
||||
## deliberately: falling back to "show everything" for a course-path query at
|
||||
## an unexpected rung would risk drawing course polylines at Region, which no
|
||||
## window response ever carries (courses are windowed-only content, Ruling 1)
|
||||
## — failing to EMPTY is the safe direction on this specific table.
|
||||
static func course_class_visible_at_rung(river_class: int, granularity_v2: String) -> bool:
|
||||
var visible: Array = COURSE_CLASS_VISIBLE_BY_RUNG.get(granularity_v2, [])
|
||||
return visible.has(river_class)
|
||||
|
||||
|
||||
static func confluences_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(CONFLUENCES_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
static func mouths_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(MOUTHS_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
static func basins_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(BASINS_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
static func attractors_visible_at_rung(granularity_v2: String) -> bool:
|
||||
return bool(ATTRACTORS_VISIBLE_BY_RUNG.get(granularity_v2, true))
|
||||
|
||||
|
||||
## Per-class course polyline width (screen-space px, zoom=1.0 domain — see the
|
||||
## const's own doc). Falls back to the stream (thinnest) width for an
|
||||
## unrecognized class id, matching RIVER_DOT_RADIUS_BY_CLASS_REGION's own
|
||||
## `.get(cls, 2.2)` call-site fallback shape on the skeleton side (there the
|
||||
## fallback is trunk/widest — the caller passes a literal default; here the
|
||||
## table itself owns a documented fallback since this is a NAMED reader, not
|
||||
## an inline `.get()`).
|
||||
static func course_class_width_px(river_class: int) -> float:
|
||||
return float(COURSE_CLASS_WIDTH_PX.get(river_class, COURSE_CLASS_WIDTH_PX[RIVER_CLASS_STREAM]))
|
||||
|
||||
|
||||
## Per-class course polyline opacity multiplier on COLOR_GEN_RIVER. Same
|
||||
## unrecognized-class fallback posture as course_class_width_px() above.
|
||||
static func course_class_opacity(river_class: int) -> float:
|
||||
return float(COURSE_CLASS_OPACITY.get(river_class, COURSE_CLASS_OPACITY[RIVER_CLASS_STREAM]))
|
||||
|
||||
|
||||
## Coordinator live-eyeball finding (2026-07-23): Araminta's ruling specifies
|
||||
## nature-overlay marker sizes as SCREEN-SPACE px, constant regardless of
|
||||
## zoom — but every draw call in this cluster (river dots, mouth rings, basin
|
||||
## line widths, T-1170 course polylines/chords) executes inside `_canvas`, a
|
||||
## Node2D whose `.scale` IS `_view_zoom` (AtlasWindowViewer._apply_transform()).
|
||||
## A raw radius/width constant handed to draw_circle()/draw_arc()/
|
||||
## draw_polyline() therefore gets multiplied by `_view_zoom` at render time —
|
||||
## invisible at the Region orbital tile mosaic's fit zoom (~0.0063 for Lendel:
|
||||
## a 2.2px trunk-river dot rasterizes at ~0.014 screen px, sub-pixel), even
|
||||
## though the SAME drawing code produces a correctly-sized (visible) mouth
|
||||
## ring at District's much larger fit zoom (~3.75, live capture confirmed
|
||||
## this). The fix: every marker's draw-time radius/width must be pre-divided
|
||||
## by `view_zoom` so the canvas transform's multiply cancels back out to the
|
||||
## ruling's literal screen-space value. `view_zoom` is clamped to a small
|
||||
## positive floor (MIN_ZOOM's own order of magnitude) to avoid a
|
||||
## divide-by-zero/near-zero blowup on a degenerate zero-zoom caller — this
|
||||
## floor is far below any legal `_view_zoom` (AtlasWindowViewer.MIN_ZOOM =
|
||||
## 0.0005), so it is inert for every real caller and only guards a malformed
|
||||
## test input.
|
||||
static func zoom_compensated_size(screen_space_size: float, view_zoom: float) -> float:
|
||||
return screen_space_size / maxf(view_zoom, 0.0001)
|
||||
@@ -6,16 +6,25 @@ extends Node2D
|
||||
## and below UI chrome — same parent, same pan/zoom transform, drawn after so
|
||||
## river dots/basin fills sit on top of the terrain colorizer.
|
||||
##
|
||||
## T-1170 (Ruling 5a): the Region+ river dot-scatter upgraded to CONNECTED
|
||||
## STRAIGHT CHORDS via river_downstream (_draw_skeleton_chords()) — per
|
||||
## Ruling 3b this chord chain IS the rung-truncated course at Region
|
||||
## truncation, not an approximation of it. District/Quarter no longer draw
|
||||
## the (now-retired) dot-scatter at all; they draw windowed course polylines
|
||||
## instead (Ruling 5b, _draw_courses(), landing with B3 once the server's
|
||||
## course inventor — A2/A3 — ships).
|
||||
##
|
||||
## This is a PORT, not a reactivation, of the retired planetary-screen draw
|
||||
## code (atlas_marker_overlay.gd:523-572, _draw_gen_rivers/_draw_gen_basins/
|
||||
## _draw_gen_attractors) — atlas_marker_overlay.gd stays retired/unreachable.
|
||||
## The drawing IDEAS survive (dot-scatter rivers, polygon basins, glyph-free
|
||||
## double-ring mouths, draw order basins-under-rivers-under-attractors); the
|
||||
## The drawing IDEAS survive (polygon basins, glyph-free double-ring mouths,
|
||||
## draw order basins-under-rivers-under-attractors; the dot-scatter idea
|
||||
## itself is superseded at Region by T-1170's chord chain, see above); the
|
||||
## COORDINATE MAPPING does not — the retired code projected onto a resident
|
||||
## displayed heightmap TEXTURE (_gen_pos(), texture-fraction space) that this
|
||||
## ladder screen has no equivalent of. Positions here go all the way through
|
||||
## world metres -> district -> canvas-local
|
||||
## (AtlasWindowGeometry.layer1_pixel_to_canvas_local()), the same frame every
|
||||
## (AtlasWindowGeometryNature.layer1_pixel_to_canvas_local()), the same frame every
|
||||
## other drawn feature on this screen already shares, wrap-resolved exactly
|
||||
## like the tile mosaic resolves terrain tiles.
|
||||
##
|
||||
@@ -59,6 +68,9 @@ extends Node2D
|
||||
## this node never needs a _process() self-heal.
|
||||
|
||||
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
|
||||
# T-1170: the nature-overlay pixel-mapping + per-rung visibility policy split
|
||||
# out of atlas_window_geometry.gd — see that file's own doc.
|
||||
const AtlasWindowGeometryNature := preload("res://ui/implant/apps/atlas/atlas_window_geometry_nature.gd")
|
||||
const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
|
||||
const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd")
|
||||
# T-1172: two-waterline clip — see that file's own header doc.
|
||||
@@ -176,17 +188,17 @@ func _draw() -> void:
|
||||
# invisible at the Region orbital tile mosaic's tiny fit zoom
|
||||
# (~0.006). zs() below pre-divides by view_zoom so the transform's
|
||||
# multiply cancels back to the literal screen-space value. See
|
||||
# AtlasWindowGeometry.zoom_compensated_size()'s own doc.
|
||||
# AtlasWindowGeometryNature.zoom_compensated_size()'s own doc.
|
||||
"view_zoom": viewer.get_view_zoom(),
|
||||
}
|
||||
|
||||
if AtlasWindowGeometry.basins_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
|
||||
if AtlasWindowGeometryNature.basins_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
|
||||
"gen_basins"
|
||||
):
|
||||
_draw_basins(ctx)
|
||||
if viewer.is_overlay_visible("gen_rivers"):
|
||||
_draw_rivers(rn, ctx)
|
||||
if AtlasWindowGeometry.attractors_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
|
||||
if AtlasWindowGeometryNature.attractors_visible_at_rung(granularity_v2) and viewer.is_overlay_visible(
|
||||
"gen_attractors"
|
||||
):
|
||||
_draw_attractors(ctx)
|
||||
@@ -204,12 +216,12 @@ func _cols_for_wrap(radius_km: float) -> int:
|
||||
|
||||
|
||||
## Zoom-compensated screen-space size — thin per-ctx wrapper over
|
||||
## AtlasWindowGeometry.zoom_compensated_size() (see that function's own doc
|
||||
## for the "why divide" rationale). Every draw_circle()/draw_arc()/
|
||||
## AtlasWindowGeometryNature.zoom_compensated_size() (see that function's own
|
||||
## doc for the "why divide" rationale). Every draw_circle()/draw_arc()/
|
||||
## draw_polyline() radius or line-width in this file routes through this so
|
||||
## Araminta's "constant on-screen size" ruling holds at every rung/zoom.
|
||||
func _zs(screen_space_size: float, ctx: Dictionary) -> float:
|
||||
return AtlasWindowGeometry.zoom_compensated_size(screen_space_size, ctx["view_zoom"])
|
||||
return AtlasWindowGeometryNature.zoom_compensated_size(screen_space_size, ctx["view_zoom"])
|
||||
|
||||
|
||||
## Pixel (row, col) -> fractional district position, wrap-resolved against
|
||||
@@ -225,10 +237,10 @@ func _zs(screen_space_size: float, ctx: Dictionary) -> float:
|
||||
## periodic — any wrap-image of the same district resolves to the same
|
||||
## real-world position).
|
||||
func _district(row: float, col: float, ctx: Dictionary) -> Vector2:
|
||||
var world_m: Vector2 = AtlasWindowGeometry.layer1_pixel_to_world_m(
|
||||
var world_m: Vector2 = AtlasWindowGeometryNature.layer1_pixel_to_world_m(
|
||||
row, col, ctx["grid_w"], ctx["grid_h"], ctx["radius_km"]
|
||||
)
|
||||
var district: Vector2 = AtlasWindowGeometry.world_m_to_district(world_m)
|
||||
var district: Vector2 = AtlasWindowGeometryNature.world_m_to_district(world_m)
|
||||
var cols: int = ctx["cols"]
|
||||
if cols > 0:
|
||||
var held_center: Vector2i = ctx["held_center"]
|
||||
@@ -285,50 +297,30 @@ func _is_drawn_water(district: Vector2, ctx: Dictionary) -> bool:
|
||||
return AtlasOverlayColors.is_morphology_water(zone)
|
||||
|
||||
|
||||
## T-1172 clip — retire when T-1170 course invention terminates courses at
|
||||
## the invented coast. River cells, confluences, and mouths are each dropped
|
||||
## (strict, no snap) when their resolved composite cell reads as drawn water
|
||||
## — see AtlasWindowWaterClip's own header doc for the two-waterline
|
||||
## rationale. Basins are explicitly OUT OF SCOPE (Tyre's rule 4) — untouched.
|
||||
## T-1172 clip — RETAINED for this Region-skeleton path only (Ruling 3g: the
|
||||
## clip retires for the District/Quarter COURSE-drawing rungs — see
|
||||
## _draw_courses() below — because courses carry real rung-consistent
|
||||
## termini and the clip's job is done there; the Region skeleton path keeps
|
||||
## drawing against a rung-dependent drawn coast and needs the presentation-
|
||||
## frame reconciliation until Region itself goes windowed, T-1143 ruling 2).
|
||||
## River cells, confluences, and mouths are each dropped (strict, no snap)
|
||||
## when their resolved composite cell reads as drawn water — see
|
||||
## AtlasWindowWaterClip's own header doc for the two-waterline rationale.
|
||||
## Basins are explicitly OUT OF SCOPE (Tyre's rule 4) — untouched.
|
||||
##
|
||||
## T-1170 Ruling 5a: at Region+, river cells draw as CONNECTED STRAIGHT
|
||||
## CHORDS (each river cell to its river_downstream neighbor) instead of a
|
||||
## dot-scatter — see _draw_skeleton_chords() below, called from here.
|
||||
## District/Quarter no longer reach this function's river-cell/confluence
|
||||
## loop at all (SKELETON_CLASS_VISIBLE_BY_RUNG has empty District/Quarter
|
||||
## entries) — they draw via _draw_courses() instead (Ruling 5b), wired from
|
||||
## _draw().
|
||||
func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
|
||||
var granularity_v2: String = ctx["granularity_v2"]
|
||||
var river_cells: Array = rn.get("river_cells", [])
|
||||
var river_class: Array = rn.get("river_class", [])
|
||||
var is_region: bool = granularity_v2 == "Region"
|
||||
|
||||
for idx in range(river_cells.size()):
|
||||
var c: Variant = river_cells[idx]
|
||||
if not (c is Array and c.size() >= 2):
|
||||
continue
|
||||
var cls: int = (
|
||||
int(river_class[idx])
|
||||
if idx < river_class.size()
|
||||
else AtlasWindowGeometry.RIVER_CLASS_FALLBACK
|
||||
)
|
||||
if not AtlasWindowGeometry.river_class_visible_at_rung(cls, granularity_v2):
|
||||
continue
|
||||
var district: Vector2 = _district(float(c[0]), float(c[1]), ctx)
|
||||
# T-1172 clip — retire when T-1170 course invention terminates at the invented coast.
|
||||
if _is_drawn_water(district, ctx):
|
||||
continue
|
||||
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
|
||||
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
|
||||
)
|
||||
if is_region:
|
||||
var radius: float = AtlasWindowGeometry.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2)
|
||||
draw_circle(p, _zs(radius, ctx), COLOR_GEN_RIVER)
|
||||
else:
|
||||
# District: trunk-only (already filtered above), reduced size +
|
||||
# opacity — the ruling's "fade down" treatment.
|
||||
var faded := Color(
|
||||
COLOR_GEN_RIVER.r,
|
||||
COLOR_GEN_RIVER.g,
|
||||
COLOR_GEN_RIVER.b,
|
||||
COLOR_GEN_RIVER.a * AtlasWindowGeometry.RIVER_DOT_OPACITY_DISTRICT_TRUNK
|
||||
)
|
||||
draw_circle(p, _zs(AtlasWindowGeometry.RIVER_DOT_RADIUS_DISTRICT_TRUNK, ctx), faded)
|
||||
_draw_skeleton_chords(rn, ctx)
|
||||
|
||||
if AtlasWindowGeometry.confluences_visible_at_rung(granularity_v2):
|
||||
if AtlasWindowGeometryNature.confluences_visible_at_rung(granularity_v2):
|
||||
for cf: Variant in rn.get("confluences", []):
|
||||
if cf is Array and cf.size() >= 2:
|
||||
var district: Vector2 = _district(float(cf[0]), float(cf[1]), ctx)
|
||||
@@ -337,10 +329,10 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
|
||||
var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
|
||||
district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
|
||||
)
|
||||
var radius: float = _zs(AtlasWindowGeometry.RIVER_CONFLUENCE_RADIUS_REGION, ctx)
|
||||
var radius: float = _zs(AtlasWindowGeometryNature.RIVER_CONFLUENCE_RADIUS_REGION, ctx)
|
||||
draw_circle(p, radius, COLOR_GEN_RIVER)
|
||||
|
||||
if AtlasWindowGeometry.mouths_visible_at_rung(granularity_v2):
|
||||
if AtlasWindowGeometryNature.mouths_visible_at_rung(granularity_v2):
|
||||
for m: Variant in rn.get("mouths", []):
|
||||
if m is Array and m.size() >= 2:
|
||||
var district: Vector2 = _district(float(m[0]), float(m[1]), ctx)
|
||||
@@ -357,18 +349,153 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void:
|
||||
_draw_mouth(p, ctx)
|
||||
|
||||
|
||||
## T-1170 Ruling 5a — the Region+ skeleton-chord draw: each river cell whose
|
||||
## class is visible at this rung draws a STRAIGHT LINE SEGMENT to its
|
||||
## `river_downstream` neighbor. `river_network.river_downstream` is a u8 PER
|
||||
## RIVER CELL (index-aligned with river_cells, the SAME alignment convention
|
||||
## river_class already uses) encoding a **D8 DIRECTION** (0-7, see
|
||||
## AtlasWindowGeometryNature.D8_DIRECTION_DELTAS — NOT a river_cells index;
|
||||
## the target cell's grid position is `c + delta`, decoded via
|
||||
## AtlasWindowGeometryNature.d8_downstream_target()), with SENTINEL values
|
||||
## `>= RIVER_DOWNSTREAM_SENTINEL_BASE` for MOUTH/EDGE_DRAIN/reserved-TERMINAL
|
||||
## (Ruling 2c). Direction-index convention and sentinel values CONFIRMED
|
||||
## against Dudley's A1 (server/src/atlas/drainage.rs:35-44, landed
|
||||
## 0fea69feb; relayed by the coordinator, not guessed) — MOUTH=8,
|
||||
## EDGE_DRAIN=9, TERMINAL=10 (reserved/unused), directions 0-7 = N/S/E/W/NE/
|
||||
## NW/SE/SW. Every place this convention is encoded is a SINGLE named
|
||||
## constant group on AtlasWindowGeometryNature (D8_DIRECTION_DELTAS /
|
||||
## RIVER_DOWNSTREAM_SENTINEL_BASE / RIVER_DOWNSTREAM_MOUTH /
|
||||
## RIVER_DOWNSTREAM_EDGE_DRAIN / RIVER_DOWNSTREAM_TERMINAL) — see that file's
|
||||
## own doc.
|
||||
##
|
||||
## Per Ruling 3b, these chords ARE the rung-truncated course at Region (no
|
||||
## octave warp survives at Region spacing — the invented course degenerates
|
||||
## exactly to this chord), NOT an approximation of it — one function (the
|
||||
## server's course inventor, eventually), every rung, this is simply what it
|
||||
## looks like with zero surviving octaves.
|
||||
##
|
||||
## Sentinel dispositions: MOUTH and EDGE_DRAIN both END the chain — no
|
||||
## downstream segment is drawn for a sentinel-terminated cell (there is no
|
||||
## real neighbor cell to connect to). EDGE_DRAIN gets NO mouth ring (Ruling
|
||||
## 3f — pole-edge drains are grid artifacts, not river-meets-sea events; the
|
||||
## existing mouths array/_draw_mouth() call in _draw_rivers() is already
|
||||
## scoped to real MOUTH sentinels via rn["mouths"], server-side, per Ruling
|
||||
## 3f's "extract_river_network stops classifying grid-edge exits into
|
||||
## mouths" — this function draws NO ring at all, sentinel or otherwise, that
|
||||
## is _draw_rivers()'s mouths-array job).
|
||||
##
|
||||
## `river_downstream` missing or shorter than `river_cells` (pre-T-1170
|
||||
## payload — Dudley's `#[serde(default)]` empty-Vec contract, the exact same
|
||||
## graceful-decode shape river_class already established) means NO chord
|
||||
## segment can be drawn for that index at all (there is no real downstream
|
||||
## direction to connect to, unlike the class-fallback case where TRUNK is a
|
||||
## safe visual default) — those cells draw NOTHING at Region until the field
|
||||
## arrives, a graceful (not crashing) degradation, documented here rather
|
||||
## than silently falling back to the old dot-scatter (which would require
|
||||
## carrying that whole second code path forward past this ticket). The
|
||||
## decoded target cell is ALSO not required to appear in `river_cells` itself
|
||||
## (the chord draws to the raw grid position `c + delta`, not to a looked-up
|
||||
## river-cell entry) — a downstream D8 pointer always names a real adjacent
|
||||
## grid cell by construction, whether or not that cell independently made it
|
||||
## into the (possibly rung/threshold-filtered) `river_cells` list.
|
||||
##
|
||||
## The actual chain-CONSTRUCTION (which segments exist at all, given the
|
||||
## fixture and rung) is delegated to
|
||||
## AtlasWindowGeometryNature.build_skeleton_chords() — a pure function with
|
||||
## no draw calls, unit-tested directly in
|
||||
## test_atlas_window_geometry_nature.gd (the sentinel/malformed/visibility
|
||||
## cases). This function's own job is the remaining per-segment work that DOES
|
||||
## need the overlay's own state: the water clip (_segment_touches_drawn_water(),
|
||||
## needs the composite/tile data only the overlay holds) and the actual
|
||||
## draw_line() call (needs a live render pass).
|
||||
func _draw_skeleton_chords(rn: Dictionary, ctx: Dictionary) -> void:
|
||||
var granularity_v2: String = ctx["granularity_v2"]
|
||||
var river_cells: Array = rn.get("river_cells", [])
|
||||
var river_class: Array = rn.get("river_class", [])
|
||||
var river_downstream: Array = rn.get("river_downstream", [])
|
||||
|
||||
var chords: Array = AtlasWindowGeometryNature.build_skeleton_chords(
|
||||
river_cells, river_class, river_downstream, granularity_v2
|
||||
)
|
||||
for chord: Dictionary in chords:
|
||||
var from_rc: Vector2 = chord["from"]
|
||||
var to_rc: Vector2 = chord["to"]
|
||||
var cls: int = chord["cls"]
|
||||
|
||||
var from_district: Vector2 = _district(from_rc.x, from_rc.y, ctx)
|
||||
var to_district: Vector2 = _district(to_rc.x, to_rc.y, ctx)
|
||||
# T-1172 clip (Region-only, retained per Ruling 3g): a segment is
|
||||
# clipped when EITHER endpoint OR its midpoint resolves to drawn
|
||||
# water — see _segment_touches_drawn_water()'s own doc for why this
|
||||
# three-point rule was chosen over an endpoints-only test.
|
||||
if _segment_touches_drawn_water(from_district, to_district, ctx):
|
||||
continue
|
||||
var from_p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
|
||||
from_district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
|
||||
)
|
||||
var to_p: Vector2 = AtlasWindowGeometry.district_to_canvas_local(
|
||||
to_district, ctx["held_center"], ctx["held_n"], ctx["cell_px"]
|
||||
)
|
||||
var width: float = AtlasWindowGeometryNature.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2)
|
||||
draw_line(from_p, to_p, COLOR_GEN_RIVER, _zs(width, ctx))
|
||||
|
||||
|
||||
## T-1172 clip rule for a CHORD SEGMENT (as opposed to a single point, which
|
||||
## is what the pre-T-1170 dot-scatter clipped): tested at the segment's TWO
|
||||
## ENDPOINTS AND its MIDPOINT, clipping the whole segment if ANY of those
|
||||
## three samples resolves to drawn water. **Decision, documented per the
|
||||
## ruling's ask ("pick the visually cleaner rule, document it, test it"):**
|
||||
## endpoints-only was rejected because a chord that DIPS through a coastal
|
||||
## composite cell without either endpoint landing in it (a river cell just
|
||||
## inland connecting to a river cell just inland on the OTHER side of a
|
||||
## narrow drawn-water inlet/bay) would draw a visible line segment crossing
|
||||
## open water with neither end clipped — worse than the old dot-scatter's
|
||||
## per-point clip, which never had this failure mode since a dot has no
|
||||
## extent to cross anything. Midpoint-only was rejected symmetrically: a
|
||||
## long chord whose midpoint happens to land on drawn land while both real
|
||||
## endpoints sit in drawn water would draw an uncllipped segment starting and
|
||||
## ending in the ocean. Three-point (both ends + midpoint) catches the
|
||||
## common cases of both failure modes at negligible extra cost (one more
|
||||
## _is_drawn_water() lookup per segment) without requiring a full
|
||||
## segment-rasterization walk — chords at Region spacing (~76 km apart) are
|
||||
## short enough relative to Region's own 204.8 km composite cell that a
|
||||
## single midpoint sample is a reasonable proxy for "does this segment pass
|
||||
## through this cell", matching the coarseness the Region rung already draws
|
||||
## at everywhere else in this file (204.8 km cells, not sub-cell precision).
|
||||
func _segment_touches_drawn_water(from_district: Vector2, to_district: Vector2, ctx: Dictionary) -> bool:
|
||||
if _is_drawn_water(from_district, ctx):
|
||||
return true
|
||||
if _is_drawn_water(to_district, ctx):
|
||||
return true
|
||||
var mid_district: Vector2 = (from_district + to_district) * 0.5
|
||||
return _is_drawn_water(mid_district, ctx)
|
||||
|
||||
|
||||
## Double-ring sea-terminus marker — verbatim geometry from the retired
|
||||
## atlas_marker_overlay.gd _draw_gen_rivers() (:536-539). Mouths never fade
|
||||
## (Araminta's ruling: "a mouth is always a landmark") — same styling at
|
||||
## every rung it's visible at (Region, District; never Quarter).
|
||||
## every rung it's visible at (Region, District; never Quarter). T-1170:
|
||||
## also the marker for REAL course termini (Ruling 5b/3e) — one geometry
|
||||
## function, both presentation surfaces.
|
||||
func _draw_mouth(p: Vector2, ctx: Dictionary) -> void:
|
||||
draw_arc(
|
||||
p, _zs(AtlasWindowGeometry.MOUTH_RING_RADIUS, ctx), 0.0, TAU, 18, COLOR_GEN_MOUTH, _zs(1.5, ctx)
|
||||
p,
|
||||
_zs(AtlasWindowGeometryNature.MOUTH_RING_RADIUS, ctx),
|
||||
0.0,
|
||||
TAU,
|
||||
18,
|
||||
COLOR_GEN_MOUTH,
|
||||
_zs(1.5, ctx)
|
||||
)
|
||||
var halo := Color(
|
||||
COLOR_GEN_MOUTH.r, COLOR_GEN_MOUTH.g, COLOR_GEN_MOUTH.b, AtlasWindowGeometry.MOUTH_HALO_ALPHA
|
||||
COLOR_GEN_MOUTH.r,
|
||||
COLOR_GEN_MOUTH.g,
|
||||
COLOR_GEN_MOUTH.b,
|
||||
AtlasWindowGeometryNature.MOUTH_HALO_ALPHA
|
||||
)
|
||||
draw_arc(
|
||||
p, _zs(AtlasWindowGeometryNature.MOUTH_HALO_RADIUS, ctx), 0.0, TAU, 22, halo, _zs(1.0, ctx)
|
||||
)
|
||||
draw_arc(p, _zs(AtlasWindowGeometry.MOUTH_HALO_RADIUS, ctx), 0.0, TAU, 22, halo, _zs(1.0, ctx))
|
||||
|
||||
|
||||
## Basins — Region only, binary (no fade), per the ruling. Polygon fill +
|
||||
@@ -410,7 +537,7 @@ func _draw_attractors(ctx: Dictionary) -> void:
|
||||
if not a is Dictionary:
|
||||
continue
|
||||
var strength: float = float(a.get("strength", 0.0))
|
||||
if strength < AtlasWindowGeometry.ATTRACTOR_MIN_STRENGTH:
|
||||
if strength < AtlasWindowGeometryNature.ATTRACTOR_MIN_STRENGTH:
|
||||
continue
|
||||
var pos_rc: Variant = a.get("position")
|
||||
if not pos_rc is Array or pos_rc.size() < 2:
|
||||
|
||||
Reference in New Issue
Block a user