feat(ui): T-1170 B3 — course polyline drawing at District/Quarter; clip restructured per Ruling 3g

_draw() splits into two independent gates: the Layer-1-gated skeleton
path (Region chords, clip retained) and the NEW DistrictWindowLayer-
gated course path — build_course_render_plan() (pure, render-free-
testable) consumed by draw_polyline with _zs-compensated widths and
opacities from the Araminta revisit tables; mouth double-rings at
Mouth termini only (EdgeDrain/ContinuesBeyondWindow/None: three
meanings, one presentation — draw to last point, stop, documented);
zero water clip on the course path by construction (courses carry
rung-consistent termini). CourseTerminus wire vocabulary kept re-
pointable pending A2's real serde names; synthetic Ruling-3h fixtures
mean the suites need zero changes when the server payload lands. Real
gap found and fixed: window arrival never redrew the nature overlay
after the first fit (one line in _on_window_ready — courses would
miss every window swap post-pan). Water-clip header rewritten to
RESTRUCTURED status (retired on course rungs; permanent at Region
until Region goes windowed, T-1143 ruling 2). Revert-verified
(visibility-gate bypass -> 4 named failures). geometry-nature
112/112, nature-overlay 58/58, viewer 78/78, zero collateral; full
sweep 3928/3928 after the full-import bootstrap; gdlint clean (viewer
1016->1017, pre-existing overage rides T-1158).

Tickets: T-1170

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:05:40 +02:00
co-authored by Claude Fable 5
parent 9d7c01de02
commit c31cc6220e
6 changed files with 485 additions and 6 deletions
@@ -529,6 +529,210 @@ func test_course_class_visible_at_rung_unknown_tag_falls_back_to_empty() -> void
).is_false()
# =============================================================================
# T-1170 Ruling 5b/3h: build_course_render_plan() — pure course-polyline
# CONSTRUCTION (no draw calls), the course-path counterpart to B2's
# build_skeleton_chords(). Synthetic fixtures shaped per Ruling 3h's wire
# shape: {class: u8, points: Vec<(i32,i32)> world-metres, terminus: string}
# — built BEFORE Dudley's A2 (course inventor) lands, per the ticket brief's
# explicit instruction.
# =============================================================================
static func _course_fixture(
cls: int, points: Array, terminus: String = "None"
) -> Dictionary:
return {"edge_id": 1, "class": cls, "points": points, "terminus": terminus}
func test_build_course_render_plan_district_trunk_is_visible_and_constructs_points() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0], [4096, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
assert_int(canvas_pts.size()).is_equal(3)
assert_int(plan["cls"]).is_equal(AtlasWindowGeometryNature.RIVER_CLASS_TRUNK)
assert_str(plan["terminus"]).is_equal("None")
## District does NOT show streams (COURSE_CLASS_VISIBLE_BY_RUNG: District ==
## [TRIBUTARY, TRUNK]) — a stream-class course must construct nothing at
## District, even with perfectly well-formed points.
func test_build_course_render_plan_district_stream_is_not_visible() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, [[0, 0], [2048, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).override_failure_message(
"streams must not draw at District — only tributary+trunk are visible there"
).is_null()
## Quarter rivers return — ALL THREE classes construct at Quarter, including
## streams. This is the wave-1 pre-announced revisit actually landing.
func test_build_course_render_plan_quarter_stream_is_visible() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_STREAM, [[0, 0], [512, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "Quarter", Vector2i.ZERO, 16, CELL_PIXEL_SIZE
)
assert_that(plan).override_failure_message(
"Quarter rivers return — streams must be visible at Quarter"
).is_not_null()
## Region never carries courses — a course-shaped fixture queried at "Region"
## must construct nothing, regardless of class.
func test_build_course_render_plan_region_constructs_nothing() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "Region", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_null()
## Ruling 3h: terminus MOUTH is preserved through to the plan — the caller
## (the overlay's draw function) reads this to decide whether to draw a
## mouth ring at the LAST canvas point.
func test_build_course_render_plan_preserves_mouth_terminus() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0]], "Mouth"
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
assert_str(plan["terminus"]).is_equal(AtlasWindowGeometryNature.COURSE_TERMINUS_MOUTH)
## EdgeDrain, ContinuesBeyondWindow, and the default None terminus are all
## preserved verbatim too — the PLAN doesn't collapse them, the DRAW caller
## decides presentation (no ring for any of these three).
func test_build_course_render_plan_preserves_edge_drain_and_continues_and_none_termini() -> void:
for terminus in ["EdgeDrain", "ContinuesBeyondWindow", "None"]:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], [2048, 0]], terminus
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_str(plan["terminus"]).is_equal(terminus)
## A course with no `terminus` key at all (an old/malformed payload) defaults
## to COURSE_TERMINUS_NONE (the string "None"), never GDScript `null` or an
## empty string — matching the class-fallback graceful-decode posture used
## throughout this cluster.
func test_build_course_render_plan_missing_terminus_defaults_to_none_string() -> void:
var course: Dictionary = {
"edge_id": 1, "class": AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, "points": [[0, 0], [100, 0]]
}
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_str(plan["terminus"]).is_equal(AtlasWindowGeometryNature.COURSE_TERMINUS_NONE)
## A course missing `class` entirely falls back to RIVER_CLASS_FALLBACK
## (TRUNK) — same posture as the skeleton path's river_class fallback.
func test_build_course_render_plan_missing_class_falls_back_to_trunk() -> void:
var course: Dictionary = {"edge_id": 1, "points": [[0, 0], [100, 0]]}
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
assert_int(plan["cls"]).is_equal(AtlasWindowGeometryNature.RIVER_CLASS_FALLBACK)
## Fewer than 2 points (a degenerate single-point or empty course) has no
## line to draw — must construct null, not a 1-point/0-point polyline.
func test_build_course_render_plan_fewer_than_two_points_constructs_nothing() -> void:
var one_point: Dictionary = _course_fixture(AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0]])
var no_points: Dictionary = _course_fixture(AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [])
assert_that(
AtlasWindowGeometryNature.build_course_render_plan(
one_point, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
).is_null()
assert_that(
AtlasWindowGeometryNature.build_course_render_plan(
no_points, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
).is_null()
## Missing `points` key entirely (not just an empty array) must also
## construct nothing, not crash on a null/missing field read.
func test_build_course_render_plan_missing_points_key_constructs_nothing() -> void:
var course: Dictionary = {"edge_id": 1, "class": AtlasWindowGeometryNature.RIVER_CLASS_TRUNK}
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_null()
## A malformed individual point (not an array, or too short) is skipped —
## not fatal to the whole polyline, matching build_skeleton_chords()'s own
## "skip the bad entry, keep going" posture — as long as >= 2 valid points
## remain.
func test_build_course_render_plan_malformed_point_is_skipped_not_fatal() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], "not a point", [2048, 0], [4096, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_not_null()
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
assert_int(canvas_pts.size()).override_failure_message(
"the malformed point must be skipped, leaving exactly the 3 well-formed points"
).is_equal(3)
## Malformed points that leave FEWER than 2 valid entries must still
## construct null (the "too many bad points" case, distinct from "some bad
## points but enough good ones remain" above).
func test_build_course_render_plan_malformed_points_leaving_too_few_constructs_nothing() -> void:
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK, [[0, 0], "bad", "also bad"]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", Vector2i.ZERO, 64, CELL_PIXEL_SIZE
)
assert_that(plan).is_null()
## Points are WORLD METRES (Ruling 3h), not heightmap pixels — cross-checked
## against world_m_to_canvas_local() called manually, proving the plan's
## conversion path matches the documented one-fewer-step-than-skeleton
## pipeline (no layer1_pixel_to_world_m() involved at all).
func test_build_course_render_plan_points_are_world_metres_not_pixels() -> void:
var held_center := Vector2i(5, 5)
var held_n := 64
var world_pt := Vector2(10240.0, -4096.0) # 5 districts east, 2 north of origin
var course: Dictionary = _course_fixture(
AtlasWindowGeometryNature.RIVER_CLASS_TRUNK,
[[int(world_pt.x), int(world_pt.y)], [0, 0]]
)
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, "District", held_center, held_n, CELL_PIXEL_SIZE
)
var expected: Vector2 = AtlasWindowGeometryNature.world_m_to_canvas_local(
world_pt, held_center, held_n, CELL_PIXEL_SIZE
)
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
assert_that(canvas_pts[0]).is_equal_approx(expected, Vector2.ONE * 0.01)
# =============================================================================
# T-1170 Ruling 5c: course_class_width_px() / course_class_opacity() —
# functional-default companion tables to COURSE_CLASS_VISIBLE_BY_RUNG.
@@ -512,3 +512,68 @@ func test_segment_touches_drawn_water_true_when_only_midpoint_is_on_water() -> v
+ " on drawn water must still be clipped — this is the exact failure"
+ " mode an endpoints-only rule would miss (Ruling 3g's ask)"
).is_true()
# =============================================================================
# T-1170 Ruling 5b (B3): _draw_course_path() early-return gating — the SAME
# "call the function directly when its early-return happens BEFORE any
# draw_*() call" precedent test_draw_with_null_viewer_is_a_noop() and
# test_draw_with_zero_grid_dims_returns_before_any_draw_call() already
# establish. Every case below returns before _draw_one_course() is ever
# reached, so calling _draw_course_path() directly (no SubViewport/render
# context) is safe. This is a SEPARATE data source/gate from the Layer-1
# skeleton path above — none of these tests touch _layer1 at all.
# =============================================================================
## The overlay-bar "gen_rivers" toggle gates the course path too — the SAME
## toggle the skeleton path uses (one player-facing "rivers" control covers
## both presentation surfaces, per the ruling).
func test_draw_course_path_returns_before_any_draw_when_gen_rivers_is_off() -> void:
var stub := _ViewerStub.new()
stub.overlay_visibility["gen_rivers"] = false
stub.district_window = {
"n": 64, "granularity_v2": "District",
"courses": [{"class": 2, "points": [[0, 0], [100, 0]], "terminus": "None"}],
}
var o = _make_overlay(stub)
o._draw_course_path() # must return before draw_polyline() — no crash outside a render context
## No district window at all (single-window mode hasn't arrived yet) — the
## course path must return cleanly, not crash on a null window read.
func test_draw_course_path_returns_before_any_draw_when_no_window() -> void:
var stub := _ViewerStub.new()
stub.district_window = null
var o = _make_overlay(stub)
o._draw_course_path()
## Ruling 3h decode tolerance: a window WITHOUT a `courses` key at all (the
## old/pre-A2 payload shape) must draw NOTHING at District/Quarter except
## mouths-on-land from the skeleton (that's the OTHER path's job) — this
## path itself must simply return, not error or fall back to a dot-scatter.
func test_draw_course_path_missing_courses_field_is_tolerated() -> void:
var stub := _ViewerStub.new()
stub.district_window = {"n": 64, "granularity_v2": "District"} # no "courses" key
var o = _make_overlay(stub)
o._draw_course_path()
## An explicitly present but EMPTY courses array must also be tolerated
## cleanly (the loop simply iterates zero times).
func test_draw_course_path_empty_courses_array_is_tolerated() -> void:
var stub := _ViewerStub.new()
stub.district_window = {"n": 64, "granularity_v2": "District", "courses": []}
var o = _make_overlay(stub)
o._draw_course_path()
## A `courses` field that is present but the WRONG TYPE (not an Array — e.g.
## a malformed/corrupted payload) must be tolerated the same way as a
## missing field, not crash attempting to iterate a non-Array.
func test_draw_course_path_non_array_courses_field_is_tolerated() -> void:
var stub := _ViewerStub.new()
stub.district_window = {"n": 64, "granularity_v2": "District", "courses": "not an array"}
var o = _make_overlay(stub)
o._draw_course_path()
@@ -137,6 +137,24 @@ const D8_DIRECTION_DELTAS: Array = [
Vector2i(1, -1), # 7 SW
]
# =============================================================================
# T-1170 Ruling 3h/5b: RiverCourse.terminus wire vocabulary — the
# CourseTerminus enum's variant NAMES as they arrive over msgpack (bare
# strings, the SAME "unit variant -> string tag" convention granularity_v2
# already uses on this same wire — see AtlasMapProtocol's own doc). `None` on
# the Rust side (a mid-window course that neither reaches a real mouth nor
# the window edge — the ordinary "ends because the chord's amplitude taper
# reached zero at a confluence/headwater anchor cell inside this window"
## case) decodes to the bare string "None" per rmp_serde's unit-variant
## convention — NOT GDScript `null`. Callers must compare against the STRING
## constant below, never `== null`.
# =============================================================================
const COURSE_TERMINUS_NONE: String = "None"
const COURSE_TERMINUS_MOUTH: String = "Mouth"
const COURSE_TERMINUS_EDGE_DRAIN: String = "EdgeDrain"
const COURSE_TERMINUS_CONTINUES_BEYOND_WINDOW: String = "ContinuesBeyondWindow"
## 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
@@ -342,6 +360,21 @@ static func layer1_pixel_to_canvas_local(
return AtlasWindowGeometryRef.district_to_canvas_local(district, held_center, held_n, cell_pixel_size)
## T-1170 Ruling 5b/3h: RiverCourse.points are already WORLD METRES on the
## wire (unlike the skeleton path's heightmap-pixel `river_cells` — see
## Ruling 3h's wire shape doc) — one fewer conversion step than
## layer1_pixel_to_canvas_local() above: world metres -> fractional district
## (world_m_to_district(), reused verbatim) -> canvas-local
## (AtlasWindowGeometry.district_to_canvas_local(), same shared transform
## every other drawn feature on this screen uses). No pixel-grid/body-radius
## step at all — courses have no heightmap-pixel domain to convert out of.
static func world_m_to_canvas_local(
world_m: Vector2, held_center: Vector2i, held_n: int, cell_pixel_size: float
) -> Vector2:
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.
# =============================================================================
@@ -415,6 +448,50 @@ static func build_skeleton_chords(
return chords
## T-1170 Ruling 5b/3h — pure course-polyline CONSTRUCTION (no draw calls):
## given one raw `RiverCourse` dict (as decoded off the wire — `class`,
## `points` (world-metres `[x,y]` pairs), `terminus` (a bare string tag)) and
## the window's own `granularity_v2`/`held_center`/`held_n`/`cell_pixel_size`,
## returns `null` if the course should not draw at all at this rung (class
## not visible, missing/degenerate points), or
## `{"canvas_pts": PackedVector2Array, "cls": int, "terminus": String}`
## ready for the caller to draw_polyline() + terminus-marker dispatch.
##
## Class defaults to RIVER_CLASS_FALLBACK (TRUNK) when missing, the same
## graceful-decode posture as the skeleton path's river_class fallback.
## `terminus` defaults to COURSE_TERMINUS_NONE when missing — an ordinary
## interior/no-marker ending, never crashing on an old/malformed payload.
## Malformed individual points are skipped (not fatal to the whole polyline,
## matching build_skeleton_chords()'s own "skip the bad entry, keep going"
## posture) — if fewer than 2 valid points remain after skipping, returns
## `null` (nothing to draw a line between).
static func build_course_render_plan(
course: Dictionary,
granularity_v2: String,
held_center: Vector2i,
held_n: int,
cell_pixel_size: float
) -> Variant:
var cls: int = int(course.get("class", RIVER_CLASS_FALLBACK))
if not course_class_visible_at_rung(cls, granularity_v2):
return null
var points_raw: Variant = course.get("points")
if not points_raw is Array or (points_raw as Array).size() < 2:
return null
var canvas_pts: PackedVector2Array = PackedVector2Array()
for pt: Variant in points_raw:
if not (pt is Array and pt.size() >= 2):
continue # malformed point — skip it, don't fail the whole polyline
var world_m := Vector2(float(pt[0]), float(pt[1]))
canvas_pts.append(world_m_to_canvas_local(world_m, held_center, held_n, cell_pixel_size))
if canvas_pts.size() < 2:
return null # too many malformed points left too few to draw a line
var terminus: String = str(course.get("terminus", COURSE_TERMINUS_NONE))
return {"canvas_pts": canvas_pts, "cls": cls, "terminus": terminus}
# =============================================================================
# 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
@@ -11,8 +11,20 @@ extends Node2D
## 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).
## instead (Ruling 5b, _draw_courses()).
##
## T-1170 Ruling 5b (B3): course polylines ride `DistrictWindowLayer.courses`
## — a SEPARATE data source from `_layer1` above (courses arrive on the
## WINDOWED response, `viewer.get_district_window()`, not the whole-body
## Layer-1 response this node requests via request_layer1()). _draw() is
## therefore two INDEPENDENT gates, not one: the skeleton/basin/attractor
## path gates on `_layer1 != null` (unchanged); the course path gates on
## `viewer.get_district_window()` being a Dictionary with a `courses` key,
## entirely independent of whether Layer-1 has arrived yet — a player who
## descends straight to District without the whole-body fetch completing
## still sees courses the moment the window arrives. NO T-1172 water clip on
## this path (Ruling 3g) — courses carry real rung-consistent termini
## server-side (the whole POINT of windowing course invention, Ruling 1d).
##
## 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/
@@ -154,8 +166,24 @@ func get_layer1() -> Variant:
return _layer1
## T-1170 (B3): TWO INDEPENDENT draw gates, not one — see the class doc's own
## "two independent gates" paragraph. The skeleton/basin/attractor path
## (Layer-1, whole-body) is unchanged from wave 1; the course path (windowed,
## Ruling 5b) is a SEPARATE early-return chain reaching _draw_courses(),
## checked regardless of whether `_layer1` has arrived — a player descending
## straight into District/Quarter must see courses without waiting on the
## whole-body Layer-1 fetch this node happens to also own.
func _draw() -> void:
if viewer == null or _layer1 == null:
if viewer == null:
return
_draw_skeleton_path()
_draw_course_path()
## The pre-T-1170 draw gate, unchanged in shape: whole-body Layer-1
## (rivers/basins/attractors), gated on `_layer1` having arrived.
func _draw_skeleton_path() -> void:
if _layer1 == null:
return
var rn: Variant = _layer1.get("river_network")
if not rn is Dictionary:
@@ -204,6 +232,98 @@ func _draw() -> void:
_draw_attractors(ctx)
## T-1170 Ruling 5b (B3): the District/Quarter windowed COURSE path — an
## entirely separate data source (`viewer.get_district_window()`) and draw
## gate from _draw_skeleton_path() above. Gated on `gen_rivers` (the SAME
## overlay-bar toggle the skeleton path uses — one player-facing "rivers"
## toggle covers both presentation surfaces, matching Araminta's ruling that
## the two paths are one continuous feature from the player's perspective,
## not two separate layers to independently show/hide).
##
## Region NEVER reaches this function's draw calls (course_class_visible_at_
## rung() has no Region key, always false there — Region draws the skeleton
## chord chain, never windowed course content, per Ruling 1). Tile mode
## (the Region orbital mosaic) also never carries `district_window` data at
## all (get_district_window() is single-window-mode-only, per that
## accessor's own doc — courses simply never reach this path in tile mode by
## construction, no separate is_tile_mode() guard needed here).
func _draw_course_path() -> void:
if not viewer.is_overlay_visible("gen_rivers"):
return
var window: Variant = viewer.get_district_window()
if not window is Dictionary:
return
var w: Dictionary = window
var courses: Variant = w.get("courses")
if not courses is Array:
return # missing `courses` field (old/pre-A2 payload) — draw nothing, see class doc
var granularity_v2: String = str(w.get("granularity_v2", "District"))
var ctx := {
"held_center": viewer.get_held_center(),
"held_n": viewer.get_held_n(),
"cell_px": viewer.get_cell_pixel_size(),
"granularity_v2": granularity_v2,
"view_zoom": viewer.get_view_zoom(),
}
for course: Variant in courses:
if not course is Dictionary:
continue
_draw_one_course(course, ctx)
## T-1170 Ruling 5b: one RiverCourse's polyline draw — class-filtered per
## COURSE_CLASS_VISIBLE_BY_RUNG, width/opacity from the COURSE_CLASS_WIDTH_PX/
## COURSE_CLASS_OPACITY companion tables (zoom-compensated via _zs(), the SAME
## discipline every other marker in this cluster follows — PR #195's
## stroke-width miss is the standing regression class this exists to
## prevent). Points arrive as `Vec<(i32,i32)>` WORLD METRES (Ruling 3h, NOT
## heightmap pixels — see world_m_to_canvas_local()'s own doc for why this is
## one conversion step shorter than the skeleton path). NO T-1172 water
## clip on this path (Ruling 3g) — courses carry real rung-consistent
## termini server-side; that is the entire point of windowing course
## invention (Ruling 1d).
##
## Terminus handling (Ruling 3h's CourseTerminus vocabulary):
## - Mouth: double-ring at the LAST point (the real coast anchor — mouths
## return as real geometry here, per Ruling 3g/3e).
## - EdgeDrain: no ring (Ruling 3f — pole-edge drains are grid artifacts,
## not river-meets-sea events; same disposition as the skeleton path's
## EDGE_DRAIN sentinel).
## - ContinuesBeyondWindow: draw to the last point, no marker (the course
## keeps going outside this window's crop — nothing to mark AT this
## window's edge, the polyline simply stops because the data stops).
## - None: an ordinary interior terminus (a headwater/confluence anchor
## inside this window) — no marker, same as ContinuesBeyondWindow's "just
## stop drawing" treatment; the two differ in MEANING (why the points ran
## out) but not in PRESENTATION (neither gets a ring).
## The actual gating/construction (class visibility, point decode, terminus
## lookup) is delegated to AtlasWindowGeometryNature.build_course_render_plan()
## — a pure function with no draw calls, unit-tested directly in
## test_atlas_window_geometry_nature.gd, the SAME split B2's
## build_skeleton_chords() already established. This function's own job is
## just the draw calls the plan feeds.
func _draw_one_course(course: Dictionary, ctx: Dictionary) -> void:
var plan: Variant = AtlasWindowGeometryNature.build_course_render_plan(
course, ctx["granularity_v2"], ctx["held_center"], ctx["held_n"], ctx["cell_px"]
)
if plan == null:
return
var canvas_pts: PackedVector2Array = plan["canvas_pts"]
var cls: int = plan["cls"]
var terminus: String = plan["terminus"]
var width: float = _zs(AtlasWindowGeometryNature.course_class_width_px(cls), ctx)
var opacity: float = AtlasWindowGeometryNature.course_class_opacity(cls)
var color := Color(COLOR_GEN_RIVER.r, COLOR_GEN_RIVER.g, COLOR_GEN_RIVER.b, COLOR_GEN_RIVER.a * opacity)
draw_polyline(canvas_pts, color, width)
if terminus == AtlasWindowGeometryNature.COURSE_TERMINUS_MOUTH:
_draw_mouth(canvas_pts[canvas_pts.size() - 1], ctx)
# EdgeDrain/ContinuesBeyondWindow/None: no marker — draw to the last
# point and stop, per the doc above.
## Circumference in districts, for nearest_wrap_image()'s wrap resolution —
## mirrors AtlasWindowOverlay._draw_tile_mosaic()'s own `cols` computation
## exactly (same source, same reason: only meaningful in tile/orbital mode on
@@ -504,6 +504,7 @@ func _on_window_ready(window: Dictionary) -> void:
_legend_panel.refresh()
queue_redraw()
_overlay_node.queue_redraw()
_nature_overlay.queue_redraw() # T-1170 B3: courses read THIS window too
## T-1153 (design doc §4 "progressive... with visible refinement as tiles
@@ -14,9 +14,21 @@ extends RefCounted
## clip against whichever composite cell is currently ON SCREEN at a given
## river dot's position — strict drop, no snap (a dot that lands on drawn
## water is simply not drawn; Region's 205 km cells may amputate a river's
## final coastal dots, an accepted cost per the ruling). T-1172 clip —
## retire when T-1170 course invention terminates courses at the invented
## coast.
## final coastal dots, an accepted cost per the ruling).
##
## T-1170 Ruling 3g update (RESTRUCTURED, not blanket-retired): the clip is
## RETIRED for the District/Quarter COURSE-drawing rungs
## (AtlasWindowNatureOverlay._draw_course_path()/_draw_one_course()) —
## courses carry real rung-consistent termini invented server-side against
## the SAME rung's drawn coast, so the clip's job is already done there. This
## file's clip machinery is STILL LIVE and used at the Region SKELETON path
## (_draw_skeleton_chords()/_segment_touches_drawn_water()) — Region still
## draws the whole-body skeleton against a rung-dependent drawn coast, which
## is precisely the presentation-frame reconciliation this file exists for.
## The Region clip is PERMANENT-UNTIL-REGION-GOES-WINDOWED (T-1143 ruling 2's
## progressive tiling) — when Region itself becomes a windowed rung, it
## inherits windowed courses too, and this file retires entirely at that
## point, not before.
##
## const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd")