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:
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user