4e503c356 deleted the client's _chain_runs() when the join moved to the
server, but left the test that pinned it. It asserted that three end-to-end
edges chain into one river, which is now precisely what must NOT happen, so
the gdUnit4 suite went red on main. Caught by the push gate; the parse sweep
I did run cannot see a behavioural assertion.
Replaced with the two properties that actually hold now, rather than dropped:
- A river's length is measured WHOLE. One course of four collinear points,
each 1,500 m segment 5.9 px and under the 15 px floor, total 17.6 px and
over it. That is the invariant the old test was really protecting — long
rivers must not vanish because their pieces are individually small — and it
survives the move to the server.
- Separate courses meeting end-to-end are NOT rejoined. This is the deleted
chaining's headstone. Water splits a course, and D-261 is explicit that a
river crossing a lake is two visible strokes, so a client that helpfully
reconnected them would draw a line across the lake. Three touching
sub-threshold courses must all be culled; a resurrected chaining pass would
report one.
1837 tests, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
533 lines
27 KiB
GDScript
533 lines
27 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}
|
|
|
|
|
|
## The cull measures a WHOLE river, not its segments.
|
|
##
|
|
## A server course used to be one D8 hop, so culling per course culled per hop
|
|
## and a long river assembled from short pieces vanished entirely — measured on
|
|
## Ferrath's Global canvas: 375 courses, 180 surviving the water clip, ZERO
|
|
## surviving the length cull. The client briefly compensated by chaining runs
|
|
## end-to-end; that was the wrong layer and only half-worked (375 hops rejoined
|
|
## into 260 pieces, because each hop was warped independently so shared
|
|
## confluence points no longer coincided). The join moved to the server, which
|
|
## now emits one course per river (`river_course::build_paths`).
|
|
##
|
|
## So the property to hold here is no longer "chain the pieces" but "measure the
|
|
## arriving polyline as a whole": a course whose individual segments are each
|
|
## under the cull must still be kept when its total length clears it.
|
|
func test_a_rivers_length_is_measured_whole_not_per_segment() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
# ONE course of four collinear points. At District with a 4x4 canvas the
|
|
# pitch is 512 m/gridunit at 2 px per gridunit, i.e. 256 m per screen px, so
|
|
# the 15 px floor is 3,840 m. Each 1,500 m segment is 5.9 px on its own —
|
|
# under the floor — while the whole 4,500 m course is 17.6 px and must be
|
|
# kept. A per-segment cull would drop it entirely.
|
|
var step: float = 1500.0
|
|
var points: Array = []
|
|
for i in range(4):
|
|
points.append([float(i) * step, 0.0])
|
|
var river: Array = [{"class": 2, "points": points}]
|
|
layer.set_frame(_canvas_with_courses(river), Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
|
|
assert_int(layer.get_drawn_course_count()).override_failure_message(
|
|
"a river whose segments are each under the cull must survive on its TOTAL length"
|
|
).is_equal(1)
|
|
|
|
|
|
## Separate courses must NOT be rejoined, even when they meet end-to-end.
|
|
##
|
|
## This is the deleted chaining's headstone. Water splits a course, and D-261 is
|
|
## explicit that a river crossing a lake is genuinely two visible strokes — so a
|
|
## client that helpfully reconnected them would erase the lake. Three short
|
|
## collinear courses that touch are three sub-threshold rivers, and all three
|
|
## are culled; a resurrected chaining pass would report 1.
|
|
func test_separate_courses_meeting_end_to_end_are_not_rejoined() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
var step: float = 1500.0
|
|
var courses: Array = []
|
|
for i in range(3):
|
|
courses.append(
|
|
{
|
|
"class": 2,
|
|
"points": [
|
|
[float(i) * step, 0.0],
|
|
[float(i + 1) * step, 0.0],
|
|
],
|
|
}
|
|
)
|
|
layer.set_frame(_canvas_with_courses(courses), Vector2.ZERO, "District", Vector2i(4, 4), 0.0)
|
|
assert_int(layer.get_drawn_course_count()).override_failure_message(
|
|
"three touching courses are three rivers, each under the cull — rejoining them "
|
|
+ "would draw one stroke straight across the water that separates them"
|
|
).is_equal(0)
|
|
|
|
|
|
## 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)
|