Files
settled-reach/client/tests/test_step_canvas_annotation_layer.gd
T
jpmschweitzerandClaude 474ab90663 feat(ui): rivers as cartographic strokes (D-261, T-1237)
Rivers now appear on the whole-body map for the first time. Five on Ferrath's
Global canvas, drawn as 5 px strokes that stop at the coastline.

Four rules, all client-side over existing server data, computed once on canvas
adoption rather than per draw:

  - fixed 5 px screen-space stroke at every rung
  - contiguous geometry through the river's own cells
  - never drawn over water — ocean and lake end a run
  - culled below 15 px of on-screen length (3x the stroke: below that a line
    is a square, not a river)

TWO THINGS THE MEASUREMENT FOUND THAT THE RECORD DID NOT ANTICIPATE.

First, the cull unit was wrong. A server "course" is an EDGE of the river
network — the stretch between two confluences — not a river. Culling per
course culls per segment, so a long river assembled from many short edges
vanishes entirely. Measured on Ferrath Global: 375 courses, 180 surviving the
water clip, and ZERO surviving a per-course cull. Edges are now chained
end-to-end into rivers before the cull is applied, which also delivers the
other half of D-261's "contiguous": per-course contiguity only makes each edge
unbroken; joining is what makes a river read as one line rather than dashes.
After chaining, 5 rivers survive at Global — the "major systems only from
orbit" behaviour the record predicted, arrived at by a different route.

Second, and worse: uses_orbital_derive() still read `Global | Region` while
the client's mirror had said Global-only since 2026-07-26. The D-255 amendment
claims "Region left the orbital derive set... it now takes the full
courses-aware derive". That was implemented against the MIRROR and never
against the authority, so Region kept running envelope-only and carrying no
courses — the exact thing the amendment said it had stopped doing. Both test
suites stayed green for two days because neither compares itself to the other.
Fixed here, with a note on each side pointing at the other, since the two
cannot be cross-checked automatically.

Also removes the two gates that withheld courses from the orbital rung — the
reason the whole-body map had no rivers at all. Whether a course is worth
drawing is measured in screen pixels, which only the client knows, so the
server now supplies geometry at every rung and the client decides.

The capture harness reports "drawn" alongside "courses", because "375 courses
arrived" and "375 rivers are drawn" are different claims and conflating them
is what made an empty map look like a data problem.

Client suite 1836 / 1810 passed / 26 skipped. Server suite green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 17:51:58 +02:00

502 lines
25 KiB
GDScript

## T-1182 tests: StepCanvasAnnotationLayer — the unscaled screen-space
## sibling's world->screen placement math (course polylines, settlement
## markers) and course visibility/terminus handling. Draw-call correctness
## itself needs a live render pass (this cluster's existing "state-level is
## fine" allowance, per test_atlas_descend_entry.gd's own precedent) — these
## tests pin the FRAME state (_world_to_local, _cell_center_world_m) a draw
## call would read from, without requiring a SubViewport.
class_name TestStepCanvasAnnotationLayer
extends GdUnitTestSuite
const StepCanvasTransport := preload("res://ui/implant/apps/atlas/step_canvas/step_canvas_transport.gd")
func test_set_frame_stores_the_frame_and_triggers_no_crash_on_draw() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var canvas := {
"width": 4,
"height": 4,
"courses": [],
"settlement_id": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
}
layer.set_frame(canvas, Vector2(1000.0, 2000.0), "District", Vector2i(4, 4), 0.0)
# No assertion beyond "did not crash" — set_frame()/queue_redraw() with a
# well-formed empty-feature canvas is the baseline no-op path every
# richer test below builds on.
assert_object(layer).is_not_null()
func test_clear_frame_drops_the_held_canvas() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame({"width": 1, "height": 1, "courses": []}, Vector2.ZERO, "Chunk", Vector2i(1, 1), 0.0)
layer.clear_frame()
assert_that(layer._canvas).is_null()
## _cell_center_world_m() is the inverse of step_canvas.rs's own per-cell
## placement (center_world_m + (col - half_w) * step_m) — a settlement id
## read from cell (col, row) must map back to the world point that cell was
## actually derived at.
## The invariant that survives the D-255 extent inversion, the viewport's
## aspect ratio, AND panning: **half the SHORT axis is half a rung cell.**
##
## Post-inversion the pitch is derived from the canvas rather than fixed per
## rung, so no literal spacing belongs in this test. Two things it must NOT
## assert, both of which an earlier version got wrong:
##
## - *"the corner is half a district away"* holds only on a SQUARE canvas.
## A real widescreen canvas spans one cell on its short axis and ~1.78 on
## its long one, so the corner is half a district on one axis only.
## - The canvas is one district **wide**; it is not sitting **on** a
## district. It is a free-floating window centred wherever the player has
## panned, with no relationship between its edges and any district
## boundary — zoom is stepped, pan is continuous (D-255 premise 3).
## A rung names a scale, the way a paper map says 1:25,000; it does not
## name a cell you are inside.
func test_cell_center_world_m_short_axis_spans_exactly_one_rung_cell() -> void:
var cell_m: float = StepCanvasTransport.RUNG_EXTENT_M["District"]
# Landscape, portrait and square — the short axis is chosen by size, never
# by which axis happens to be the width.
for extent in [Vector2i(8, 4), Vector2i(4, 8), Vector2i(6, 6)]:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame(
{"width": extent.x, "height": extent.y, "courses": []},
Vector2.ZERO,
"District",
extent,
0.0
)
var corner: Vector2 = layer._cell_center_world_m(0, 0)
# On the SHORT axis, the edge sits half a rung cell from centre.
var short_axis_offset: float = absf(corner.y if extent.y <= extent.x else corner.x)
assert_float(short_axis_offset).override_failure_message(
"%s: short-axis edge is %f m from centre, want half a District (%f)"
% [extent, short_axis_offset, cell_m * 0.5]
).is_equal_approx(cell_m * 0.5, 0.001)
# And the centre cell is the world centre, whatever the shape. Note the
# centre is (width/2, height/2) — NOT the same index on both axes once
# the canvas is not square.
assert_that(layer._cell_center_world_m(extent.x / 2, extent.y / 2)).is_equal(Vector2.ZERO)
## Panning does not align the canvas to anything — the frame's world centre is
## wherever the player stopped, and every cell offset is measured from it. A
## centre deliberately off any round boundary guards against a future change
## quietly snapping the canvas to the rung's own lattice, which would make pan
## step instead of slide.
func test_cell_center_world_m_is_independent_of_grid_alignment() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var ragged := Vector2(1_234_567.0, -987_654.0) # deliberately unaligned
layer.set_frame(
{"width": 4, "height": 4, "courses": []}, ragged, "District", Vector2i(4, 4), 0.0
)
assert_that(layer._cell_center_world_m(2, 2)).override_failure_message(
"the centre cell must be the frame's world centre exactly — no snapping"
).is_equal(ragged)
func test_cell_center_world_m_offsets_by_the_frames_world_center() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame(
{"width": 2, "height": 2, "courses": []},
Vector2(10_000.0, 20_000.0),
"Chunk",
Vector2i(2, 2),
0.0
)
# half_w = half_h = 1; spacing = CHUNK_M / short axis = 64/2 = 32 post-
# inversion. Cell (1,1) -> center + (1-1)*32 = center either way — this
# asserts the centre cell lands on the centre, not the pitch itself.
assert_that(layer._cell_center_world_m(1, 1)).is_equal(Vector2(10_000.0, 20_000.0))
## _world_to_local() delegates to StepCanvasTransport.world_m_to_canvas_local
## with the layer's OWN held frame — this pins that the layer actually reads
## its stored _world_center/_rung/_extent_cells, not stale defaults.
func test_world_to_local_uses_the_held_frame() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var world_center := Vector2(5_000.0, -3_000.0)
var extent := Vector2i(32, 32)
layer.set_frame({"width": 32, "height": 32, "courses": []}, world_center, "Quarter", extent, 0.0)
var expected: Vector2 = StepCanvasTransport.world_m_to_canvas_local(
world_center, world_center, "Quarter", extent, 0.0
)
assert_that(layer._world_to_local(world_center)).is_equal_approx(expected, Vector2(0.01, 0.01))
# -----------------------------------------------------------------------
# T-1175 seeded item 2 — source-taper ribbon geometry
# -----------------------------------------------------------------------
## A straight 5-point course (evenly spaced, 10px apart along +X) — the
## simplest case for pinning the arc-length taper ramp: cumulative length
## at vertex i is exactly i*10, total 40, so the taper window
## (TAPER_ARC_FRACTION * 40 = 6px) falls strictly inside the first segment.
func _straight_course_points(spacing_px: float = 10.0) -> PackedVector2Array:
var pts := PackedVector2Array()
for i in range(5):
pts.append(Vector2(float(i) * spacing_px, 0.0))
return pts
## Vertex 0 (the source, cumulative length 0) gets the hairline minimum
## width, never the class's full width — this is the taper's whole point.
## _head_widths_by_arc_length() is handed the HEAD span only (post PR #207
## finding 4's ribbon/polyline split) — this test exercises it directly on
## a short head span (the first two points), which is what
## _split_course_at_arc_length() would hand it for this same course.
func test_head_widths_by_arc_length_starts_at_the_taper_minimum() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var head := PackedVector2Array([Vector2(0.0, 0.0), Vector2(6.0, 0.0)])
var widths: PackedFloat32Array = layer._head_widths_by_arc_length(head, 2.4)
assert_float(widths[0]).is_equal_approx(StepCanvasAnnotationLayer.TAPER_MIN_WIDTH_PX, 0.001)
## The head's own LAST vertex always ramps to exactly full_width — that's
## the butt-joint contract _draw_tapered_course() relies on to hand off to
## the AA polyline tail at identical width.
func test_head_widths_by_arc_length_ends_at_full_width() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var head := PackedVector2Array([Vector2(0.0, 0.0), Vector2(3.0, 0.0), Vector2(6.0, 0.0)])
var widths: PackedFloat32Array = layer._head_widths_by_arc_length(head, 2.4)
assert_float(widths[2]).is_equal_approx(2.4, 0.001)
## The ramp is monotonically non-decreasing from source to the head's last
## vertex — no "wobble" where a later vertex is narrower than an earlier one.
func test_head_widths_by_arc_length_is_monotonic() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var head := _straight_course_points(1.0)
var widths: PackedFloat32Array = layer._head_widths_by_arc_length(head, 2.4)
for i in range(1, widths.size()):
assert_float(widths[i]).is_greater_equal(widths[i - 1])
## A degenerate two-point head where both points coincide (zero-length)
## must not divide by zero — every vertex falls back to full width rather
## than crashing or producing NaN.
func test_head_widths_by_arc_length_handles_a_degenerate_zero_length_span() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var head := PackedVector2Array([Vector2(5.0, 5.0), Vector2(5.0, 5.0)])
var widths: PackedFloat32Array = layer._head_widths_by_arc_length(head, 2.4)
assert_float(widths[0]).is_equal_approx(2.4, 0.001)
assert_float(widths[1]).is_equal_approx(2.4, 0.001)
# -----------------------------------------------------------------------
# PR #207 finding 4 — head/tail split (the AA-hybrid seam)
# -----------------------------------------------------------------------
## The split point lands EXACTLY at TAPER_ARC_FRACTION of the total arc
## length, interpolated within the straddling segment — not snapped to the
## nearest existing vertex (see _split_course_at_arc_length()'s own doc for
## why interpolation, not snapping, is required).
func test_split_course_at_arc_length_interpolates_the_exact_fraction() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
# Total length 40 (4 segments of 10px); taper fraction 0.15 -> split at
# arc-length 6, which is 60% of the way through the FIRST segment
# (0 -> 10), i.e. at x=6.
var pts := _straight_course_points()
var split: Array = layer._split_course_at_arc_length(pts, StepCanvasAnnotationLayer.TAPER_ARC_FRACTION)
var head: PackedVector2Array = split[0]
var tail: PackedVector2Array = split[1]
assert_vector(head[head.size() - 1]).is_equal_approx(Vector2(6.0, 0.0), Vector2(0.001, 0.001))
assert_vector(tail[0]).is_equal_approx(Vector2(6.0, 0.0), Vector2(0.001, 0.001))
## The head and tail share their boundary point EXACTLY (the butt-joint
## contract) — no gap, no overlap.
func test_split_course_at_arc_length_head_and_tail_share_the_boundary_point() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var pts := _straight_course_points()
var split: Array = layer._split_course_at_arc_length(pts, StepCanvasAnnotationLayer.TAPER_ARC_FRACTION)
var head: PackedVector2Array = split[0]
var tail: PackedVector2Array = split[1]
assert_vector(head[head.size() - 1]).is_equal(tail[0])
## `_split_course_at_arc_length()` is a generic arc-length splitter (the
## `t_fraction` parameter is not hardwired to TAPER_ARC_FRACTION) — when the
## requested fraction covers the WHOLE course (t_fraction >= 1.0, "the taper
## window would run past the mouth"), there is no meaningful post-split
## span: the whole course is the head, tail is empty. TAPER_ARC_FRACTION
## itself (0.15) can never trigger this branch for a real course (any
## positive-length course has SOME arc beyond 15% of itself) — this pins
## the branch directly via an out-of-the-ordinary fraction, the same way a
## unit test for a generic clamp function exercises both ends of its range
## regardless of what the one real call site happens to pass.
func test_split_course_at_arc_length_returns_empty_tail_when_fraction_covers_the_whole_course() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var pts := PackedVector2Array([Vector2(0.0, 0.0), Vector2(1.0, 0.0)])
var split: Array = layer._split_course_at_arc_length(pts, 1.0)
var head: PackedVector2Array = split[0]
var tail: PackedVector2Array = split[1]
assert_int(tail.size()).is_equal(0)
assert_int(head.size()).is_equal(pts.size())
## The real call site's fraction (TAPER_ARC_FRACTION, 0.15) DOES still split
## even a very short two-point course — the split point just lands close to
## the source rather than at the mouth, and both head and tail are
## non-empty. This is the behavior _draw_tapered_course() actually relies
## on for a minimal two-point interior-source course.
func test_split_course_at_arc_length_still_splits_a_short_two_point_course() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var pts := PackedVector2Array([Vector2(0.0, 0.0), Vector2(1.0, 0.0)])
var split: Array = layer._split_course_at_arc_length(pts, StepCanvasAnnotationLayer.TAPER_ARC_FRACTION)
var head: PackedVector2Array = split[0]
var tail: PackedVector2Array = split[1]
assert_int(head.size()).is_equal(2)
assert_int(tail.size()).is_equal(2)
assert_vector(head[head.size() - 1]).is_equal_approx(Vector2(0.15, 0.0), Vector2(0.001, 0.001))
## A degenerate (zero-length, coincident-point) course must not divide by
## zero in the split math — falls back to "whole course is the head".
func test_split_course_at_arc_length_handles_a_degenerate_zero_length_course() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var pts := PackedVector2Array([Vector2(5.0, 5.0), Vector2(5.0, 5.0)])
var split: Array = layer._split_course_at_arc_length(pts, StepCanvasAnnotationLayer.TAPER_ARC_FRACTION)
var tail: PackedVector2Array = split[1]
assert_int(tail.size()).is_equal(0)
# -----------------------------------------------------------------------
# PR #207 findings 2/3 — mitred offset (perpendicular width at bends,
# clamped against self-intersection at hairpins)
# -----------------------------------------------------------------------
## A perpendicular offset at any point along a straight horizontal course
## points along +/-Y, never +/-X — the ribbon must widen ACROSS the flow
## direction, not along it. On a straight run theta=0, so the mitred offset
## reduces to the plain half-width (no widening).
func test_mitred_offset_is_perpendicular_on_a_straight_course() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var pts := _straight_course_points()
var offset: Vector2 = layer._mitred_offset(pts, 2, 1.0)
assert_float(offset.x).is_equal_approx(0.0, 0.001)
assert_float(absf(offset.y)).is_equal_approx(1.0, 0.001)
## Finding 3 (Hoshe) — at a 90-degree bend, the mitred offset LENGTH is
## half_w / cos(45deg) = half_w * sqrt(2) ~= 1.414 * half_w, which projects
## back to exactly half_w perpendicular to EACH adjacent segment (the true
## width the old averaged-unit-normal joint under-widened by cos(theta/2),
## a 29% pinch). Course: (0,0) -> (10,0) -> (10,10) — a clean right-angle
## turn at the middle vertex.
func test_mitred_offset_at_a_90_degree_bend_restores_perpendicular_width() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var pts := PackedVector2Array([Vector2(0.0, 0.0), Vector2(10.0, 0.0), Vector2(10.0, 10.0)])
var half_w := 1.0
var offset: Vector2 = layer._mitred_offset(pts, 1, half_w)
# The offset's projection onto EITHER adjacent segment's own unit
# normal must equal half_w (the true perpendicular width on both
# faces of the bend) — not the offset's raw length (which is longer,
# by design, along the bisector).
var incoming_normal := Vector2(0.0, 1.0) # normal to the (0,0)->(10,0) segment
var outgoing_normal := Vector2(1.0, 0.0) # normal to the (10,0)->(10,10) segment
assert_float(absf(offset.dot(incoming_normal))).is_equal_approx(half_w, 0.01)
assert_float(absf(offset.dot(outgoing_normal))).is_equal_approx(half_w, 0.01)
## Finding 2 (Hoshe) — a tight hairpin (turn radius below half-width) must
## not produce a self-intersecting bowtie: the mitre offset is clamped to
## HAIRPIN_SEGMENT_FACTOR of the SHORTER adjacent segment length. Course
## with a very short middle segment (length 1) and a near-180-degree turn
## back on itself — an unclamped mitre would blow the offset length far
## past that short segment.
func test_mitred_offset_clamps_at_a_tight_hairpin() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
# (0,0) -> (1,0) -> (0, 0.01): a near-reversal at vertex 1, short
# adjacent segments (length 1 and ~1).
var pts := PackedVector2Array([Vector2(0.0, 0.0), Vector2(1.0, 0.0), Vector2(0.0, 0.01)])
var half_w := 1.0
var offset: Vector2 = layer._mitred_offset(pts, 1, half_w)
var shortest_segment := minf(pts[1].distance_to(pts[0]), pts[2].distance_to(pts[1]))
assert_float(offset.length()).is_less_equal(
shortest_segment * StepCanvasAnnotationLayer.HAIRPIN_SEGMENT_FACTOR + 0.001
)
## The ribbon polygon for an n-point head span has exactly 2n vertices (n on
## each side) — this pins the "side-A then side-B reversed" construction
## produces a closed strip outline with no dropped or duplicated vertex.
func test_head_widths_output_size_matches_head_point_count() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var head := PackedVector2Array([Vector2(0, 0), Vector2(2, 0), Vector2(4, 0), Vector2(6, 0)])
var widths: PackedFloat32Array = layer._head_widths_by_arc_length(head, 2.4)
assert_int(widths.size()).is_equal(head.size())
# -----------------------------------------------------------------------
# PR #207 finding 1 — crop-edge false-headwater detection gate
# -----------------------------------------------------------------------
## An interior source (well inside the canvas bounds) IS a true source —
## tapering fires.
func test_is_true_source_in_canvas_true_for_an_interior_point() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
# District spacing 2048m, extent 4x4 -> half-extent 4096m on each axis.
layer.set_frame({"width": 4, "height": 4, "courses": []}, Vector2(1000.0, 2000.0), "District", Vector2i(4, 4), 0.0)
assert_bool(layer._is_true_source_in_canvas(Vector2(1000.0, 2000.0))).is_true()
## A point beyond the canvas's own declared bounds is the one-station crop
## overhang (`crop_course_to_window`'s `lo = first_in.saturating_sub(1)`),
## not a true source — no taper.
func test_is_true_source_in_canvas_false_for_a_point_outside_the_bounds() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame({"width": 4, "height": 4, "courses": []}, Vector2(1000.0, 2000.0), "District", Vector2i(4, 4), 0.0)
# Half-extent is 4096m; world center + 5000m on X is well outside.
assert_bool(layer._is_true_source_in_canvas(Vector2(1000.0 + 5000.0, 2000.0))).is_false()
## A source sitting exactly at the boundary (within CROP_EDGE_EPSILON_M)
## behaves conservatively — treated as OUTSIDE (no taper), per the ruling.
func test_is_true_source_in_canvas_is_conservative_at_the_exact_boundary() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame({"width": 4, "height": 4, "courses": []}, Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
# Half-extent is 4096m exactly. A point AT the boundary (x=4096) is
# within epsilon of the edge -> conservatively NOT a true source.
assert_bool(layer._is_true_source_in_canvas(Vector2(4096.0, 0.0))).is_false()
## A null/malformed point (defensive — the caller already guards this via
## screen_pts.size() < 2) is conservatively NOT a true source.
func test_is_true_source_in_canvas_false_for_null() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
layer.set_frame({"width": 4, "height": 4, "courses": []}, Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
assert_bool(layer._is_true_source_in_canvas(null)).is_false()
## Godot only allows draw_*() calls INSIDE an active `_draw()`/NOTIFICATION_
## DRAW context (calling `_draw_tapered_course()` directly, outside that
## context, is a Godot Runtime Error, not a code bug) — so the "does not
## crash" smoke check for the taper=false/true routing goes through the SAME
## public entry every other "no crash" test in this suite already uses:
## `set_frame()` + `queue_redraw()` (matches
## `test_set_frame_stores_the_frame_and_triggers_no_crash_on_draw`'s own
## established pattern). This end-to-end path exercises
## `_draw_one_course()`'s routing decision (`_is_true_source_in_canvas()` ->
## `_draw_tapered_course()`'s `taper` argument) for real, without requiring
## a SubViewport or an explicit live-render await — matching this suite's
## own stated "pin the frame state, not pixels" scope. A crop-passthrough
## course (source point OUTSIDE the canvas bounds) exercises the taper=false
## flat-polyline path.
func test_set_frame_with_a_crop_passthrough_course_does_not_crash() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var canvas := {
"width": 4,
"height": 4,
# world_center (0,0), District extent 4x4 -> half-extent 4096m. A
# source at x=-9000 is well outside the canvas bounds — the crop
# overhang case (finding 1).
"courses": [{"class": 2, "points": [[-9000, 0], [0, 0], [10, 0]], "terminus": ""}],
}
layer.set_frame(canvas, Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
assert_object(layer).is_not_null()
## An interior-source course (source point inside the canvas bounds)
## exercises the taper=true ribbon-head + polyline-tail hybrid path.
func test_set_frame_with_an_interior_source_course_does_not_crash() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var canvas := {
"width": 4,
"height": 4,
"courses": [{"class": 2, "points": [[0, 0], [500, 0], [1000, 0], [1500, 0]], "terminus": "Mouth"}],
}
layer.set_frame(canvas, Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
assert_object(layer).is_not_null()
# -----------------------------------------------------------------------
# D-261 — rivers as cartographic strokes
# -----------------------------------------------------------------------
func _canvas_with_courses(courses: Array) -> Dictionary:
return {"width": 4, "height": 4, "courses": courses}
## A server "course" is an EDGE of the river network — the stretch between two
## confluences — not a river. Culling per course therefore culls per SEGMENT,
## and a long river assembled from many short edges vanishes entirely. Measured
## on Ferrath's Global canvas before chaining: 375 courses, 180 surviving the
## water clip, ZERO surviving the length cull. After chaining: 5 rivers.
func test_edges_chain_into_one_river_before_the_length_cull() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
# Three collinear edges, each meeting the next end-to-end. Sized so each
# is individually UNDER the cull and the chain is comfortably over it.
# At District with a 4x4 canvas the pitch is 512 m/gridunit and 2 px per
# gridunit, i.e. 256 m per screen px — so the 15 px floor is 3,840 m.
# 1,500 m per edge: 5.9 px alone (culled), 17.6 px chained (kept).
var step: float = 1500.0
var edges: Array = []
for i in range(3):
edges.append(
{
"class": 2,
"points": [
[float(i) * step, 0.0],
[float(i + 1) * step, 0.0],
],
}
)
layer.set_frame(_canvas_with_courses(edges), Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
assert_int(layer.get_drawn_course_count()).override_failure_message(
"three end-to-end edges must chain into ONE river, not be culled as three segments"
).is_equal(1)
## The cull itself: a river too short to read as a line is not drawn at all,
## rather than drawn as a speck. 5 px wide by 5 px long is a square.
func test_a_river_shorter_than_the_stroke_reads_is_not_drawn() -> void:
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
add_child(layer)
var tiny: Array = [{"class": 2, "points": [[0.0, 0.0], [0.001, 0.0]]}]
layer.set_frame(_canvas_with_courses(tiny), Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
assert_int(layer.get_drawn_course_count()).override_failure_message(
"a sub-threshold river must be dropped, not drawn as a speck"
).is_equal(0)
## The stroke is a fixed screen-space width at every rung (D-261) — the
## per-class ladder is deliberately flattened until T-1238 restores a
## size-varying width.
func test_stroke_width_is_one_fixed_value() -> void:
assert_float(StepCanvasAnnotationLayer.COURSE_WIDTH_PX).is_equal_approx(5.0, 0.001)
assert_float(StepCanvasAnnotationLayer.MIN_COURSE_LENGTH_PX).override_failure_message(
"the cull must DERIVE from the stroke (3x) so it self-corrects if the width changes"
).is_equal_approx(StepCanvasAnnotationLayer.COURSE_WIDTH_PX * 3.0, 0.001)