The single large polish pass Jeroen requested off the T-1170 captures, benchmarked against RimWorld's world-map fluency. Courses now draw as source-tapered ribbons: per-vertex width ramps from a hairline at the upstream source to full class width over 15% of the course's arc length (arc-length parameterized, not vertex-indexed, so point density doesn't change the read), built as one draw_polygon ribbon with mitred joins. Class width table retuned 0.9/1.4/2.2 -> 0.6/1.2/2.4: a clean ~2x per-class ladder so a tributary-joins-trunk confluence reads as a join, trunk held at its visually-proven weight, stream thinner per the benchmark's thin/consistent/restrained grammar. Opacity untouched (Araminta's T-1170 ruling stands). Coast-gradient item resolved as already-correct (the coastal transition zones + elevation lightness render the shoreline band; capture-verified) — no wire change. Stipple assessment filed as T-1194. Eight new taper-geometry tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
198 lines
9.3 KiB
GDScript
198 lines
9.3 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))
|
|
# 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))
|
|
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.
|
|
func test_cell_center_world_m_matches_the_servers_own_per_cell_placement() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
layer.set_frame({"width": 4, "height": 4, "courses": []}, Vector2(0.0, 0.0), "District", Vector2i(4, 4))
|
|
|
|
# half_w = half_h = 2; spacing = 2048. Cell (0,0) -> (0-2)*2048 = -4096 on
|
|
# both axes; cell (2,2) (the center-ish cell) -> (2-2)*2048 = 0.
|
|
assert_that(layer._cell_center_world_m(0, 0)).is_equal(Vector2(-4096.0, -4096.0))
|
|
assert_that(layer._cell_center_world_m(2, 2)).is_equal(Vector2.ZERO)
|
|
|
|
|
|
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)
|
|
)
|
|
# half_w = half_h = 1; spacing = 64. Cell (1,1) -> center + (1-1)*64 = center.
|
|
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)
|
|
|
|
var expected: Vector2 = StepCanvasTransport.world_m_to_canvas_local(
|
|
world_center, world_center, "Quarter", extent
|
|
)
|
|
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.
|
|
func test_course_widths_by_arc_length_starts_at_the_taper_minimum() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
var pts := _straight_course_points()
|
|
var widths: PackedFloat32Array = layer._course_widths_by_arc_length(pts, 2.4)
|
|
assert_float(widths[0]).is_equal_approx(
|
|
StepCanvasAnnotationLayer.TAPER_MIN_WIDTH_PX, 0.001
|
|
)
|
|
|
|
|
|
## Vertices beyond TAPER_ARC_FRACTION of the total run hold at the class's
|
|
## own full width — the taper does not run the whole length of the course,
|
|
## only its own leading fraction (ticket instruction: "not the whole run").
|
|
func test_course_widths_by_arc_length_holds_full_width_past_the_taper_fraction() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
# Total length 40; taper window = 0.15 * 40 = 6px. Vertex 1 (cumulative
|
|
# 10px) is already past that window.
|
|
var pts := _straight_course_points()
|
|
var widths: PackedFloat32Array = layer._course_widths_by_arc_length(pts, 2.4)
|
|
assert_float(widths[1]).is_equal_approx(2.4, 0.001)
|
|
assert_float(widths[4]).is_equal_approx(2.4, 0.001)
|
|
|
|
|
|
## The ramp is monotonically non-decreasing from source to mouth — no
|
|
## "wobble" where a later vertex is narrower than an earlier one within the
|
|
## taper window.
|
|
func test_course_widths_by_arc_length_is_monotonic_within_the_taper_window() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
# Denser spacing (1px) so several vertices fall inside the 6px taper
|
|
# window (total length 20, taper window 3px).
|
|
var pts := _straight_course_points(1.0)
|
|
var widths: PackedFloat32Array = layer._course_widths_by_arc_length(pts, 2.4)
|
|
for i in range(1, widths.size()):
|
|
assert_float(widths[i]).is_greater_equal(widths[i - 1])
|
|
|
|
|
|
## A degenerate two-point course 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_course_widths_by_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 widths: PackedFloat32Array = layer._course_widths_by_arc_length(pts, 2.4)
|
|
assert_float(widths[0]).is_equal_approx(2.4, 0.001)
|
|
assert_float(widths[1]).is_equal_approx(2.4, 0.001)
|
|
|
|
|
|
## A legal but minimal two-point course (source directly connected to
|
|
## mouth, no interior vertices) still tapers at the source end.
|
|
func test_course_widths_by_arc_length_tapers_a_two_point_course() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
var pts := PackedVector2Array([Vector2(0.0, 0.0), Vector2(100.0, 0.0)])
|
|
var widths: PackedFloat32Array = layer._course_widths_by_arc_length(pts, 2.4)
|
|
assert_float(widths[0]).is_equal_approx(
|
|
StepCanvasAnnotationLayer.TAPER_MIN_WIDTH_PX, 0.001
|
|
)
|
|
# Vertex 1 (the mouth) is at cumulative length 100, far past the
|
|
# TAPER_ARC_FRACTION * 100 = 15px taper window — full width.
|
|
assert_float(widths[1]).is_equal_approx(2.4, 0.001)
|
|
|
|
|
|
## The ribbon polygon for an n-point course 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_draw_tapered_course_ribbon_vertex_count_matches_two_times_point_count() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
layer.set_frame(
|
|
{
|
|
"width": 4,
|
|
"height": 4,
|
|
"courses": [{"class": 2, "points": [[0, 0], [10, 0], [20, 0], [30, 0]], "terminus": ""}],
|
|
},
|
|
Vector2.ZERO,
|
|
"Chunk",
|
|
Vector2i(4, 4)
|
|
)
|
|
# Draw-call correctness needs a live render pass (this suite's own header
|
|
# note); what's pinned here is that _course_widths_by_arc_length()'s
|
|
# output size always matches the input point count, which
|
|
# _draw_tapered_course() relies on 1:1 to build its 2n-vertex ribbon —
|
|
# see the width tests above for the per-vertex ramp itself.
|
|
var pts := PackedVector2Array([Vector2(0, 0), Vector2(10, 0), Vector2(20, 0), Vector2(30, 0)])
|
|
var widths: PackedFloat32Array = layer._course_widths_by_arc_length(pts, 2.4)
|
|
assert_int(widths.size()).is_equal(pts.size())
|
|
|
|
|
|
## 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.
|
|
func test_segment_normal_is_perpendicular_to_a_straight_horizontal_course() -> void:
|
|
var layer: StepCanvasAnnotationLayer = auto_free(StepCanvasAnnotationLayer.new())
|
|
add_child(layer)
|
|
var pts := _straight_course_points()
|
|
var normal: Vector2 = layer._segment_normal(pts, 2)
|
|
assert_float(normal.x).is_equal_approx(0.0, 0.001)
|
|
assert_float(absf(normal.y)).is_equal_approx(1.0, 0.001)
|