Files
settled-reach/client/ui/implant/apps/atlas/atlas_legend_panel.gd
T
jpmschweitzer dd13760d62 fix(client): cold-start round 2 — DERIVING TERRAIN state, legend re-fit, release server for make atlas
Item 1: while zero tiles have arrived, the viewer draws a centered
screen-space 'DERIVING TERRAIN…' label (text_dim role, no new hue),
dropping the instant the first tile lands — a cold wait now reads as
loading, not broken. New has_any_tile_arrived() predicate (distinct
from has_pending_tiles(): both true mid-arrival, tested exactly there).

Item 2: legend re-fit root-caused empirically, two plausible fixes
disproven by trace before the real one: a manually-positioned Control's
size NEVER tracks a shrinking minimum in this parenting shape, and
RichTextLabel.fit_content reports degenerate minimums until laid out at
real width once — so reset must be DEFERRED and run after refill, not
inside clear(). ImplantPanel.reset_to_content_size() (call_deferred),
wired into both legend refresh()es. The load-bearing test compares
size.y to get_minimum_size().y — a size-to-size comparison passed
trivially with both numbers equally stuck (caught on first draft).

Item 3: make atlas now builds the RELEASE server and passes
SR_SERVER_BIN (a cold DEBUG server delivers zero tiles for >10s on a
new body — live-measured — vs 210ms warm; release serves cold in well
under a second). atlas_standalone._server_binary_path() honors the env
override per the SR_PORT two-tier precedent, debug path unchanged
when unset.

All fixes revert-verified; nine suites green collateral-checked;
gdlint clean.
2026-07-22 19:51:12 +02:00

193 lines
8.1 KiB
GDScript

extends ImplantPanel
## Left-side generation-overlay legend (D-226 item 3, T-960) — the shape/color
## key for the generation overlay group (attractor types, sub-biome colors,
## district morphology, road/rail authority + style, settlement size/capital).
##
## This script has no `class_name` on purpose, mirroring atlas_overlay_bar.gd
## (review #8 there): the owner (AtlasViewer) passes the viewer reference to
## _init(), and a `class_name` + required-arg _init() combo is a Godot editor
## footgun. Instance it via
## load("res://ui/implant/apps/atlas/atlas_legend_panel.gd").new(self).
##
## Data-driven (GENERATION_LEGEND below): one spec entry per generation
## overlay id — multiple entries may share an id (e.g. gen_l1_attractors has
## both a shape key and a color key, D-226's shape-vs-color separation taught
## the same way here as it is drawn on the map). Adding a future layer's
## legend is a new table row, never a layout change. refresh() shows only the
## sections whose overlay is currently toggled on, and hides the whole panel
## when none are active (invisible when not needed).
const PANEL_MARGIN: float = 16.0
const LEGEND_PANEL_WIDTH: float = 260.0
# Generation overlay colors (T-960) — kept in sync with the actual render
# values in atlas_marker_overlay.gd; duplicated rather than cross-referenced,
# matching the existing COLOR_ROAD/COLOR_RAIL/COLOR_CITY precedent shared
# between atlas_viewer.gd and atlas_marker_overlay.gd (each file owns its own
# reading of the palette: this one for the legend, the marker overlay for the
# draw calls).
const COLOR_ROAD_ADMINISTRATIVE: Color = Color(0.45, 0.65, 0.90, 0.85)
const COLOR_ROAD_CORPORATE: Color = Color(0.85, 0.65, 0.20, 0.85)
const COLOR_ROAD_COMMUNAL: Color = Color(0.45, 0.75, 0.50, 0.85)
const COLOR_ROAD_TRADE: Color = Color(0.85, 0.60, 0.30, 0.85)
const COLOR_ROAD_ABANDONED: Color = Color(0.45, 0.42, 0.38, 0.65)
const COLOR_SETTLEMENT_GEN: Color = Color(0.94, 0.82, 0.38, 1.0)
const COLOR_SETTLEMENT_CAPITAL_GEN: Color = Color(1.0, 0.92, 0.55, 1.0)
# T-1118 — region climate grid cold/hot ramp endpoints (matches
# atlas_overlay_colors.gd's COLOR_REGION_TEMP_COLD/COLOR_REGION_TEMP_HOT).
const COLOR_REGION_TEMP_COLD_GEN: Color = Color(0.25, 0.45, 0.85, 0.60)
const COLOR_REGION_TEMP_HOT_GEN: Color = Color(0.90, 0.25, 0.20, 0.60)
# T-1119 — quarter-footprint density ramp endpoints, D-226 T-1112 amendment
# SS3's literal legend spec (matches atlas_overlay_colors.gd's
# COLOR_QUARTER_LOW_DENSITY/COLOR_QUARTER_HIGH_DENSITY).
const COLOR_QUARTER_LOW_DENSITY_GEN: Color = Color(0.55, 0.48, 0.30, 0.6)
const COLOR_QUARTER_HIGH_DENSITY_GEN: Color = Color(0.94, 0.82, 0.38, 1.0)
## One entry per generation-overlay id. "color": Color.TRANSPARENT means
## "shape/style carries the meaning here, let the theme's dim text color
## apply" — used for every row where color is NOT the encoded axis.
const GENERATION_LEGEND: Array = [
{
"overlay_id": "gen_l1_rivers",
"title": "RIVERS — L1",
"rows": [
{"glyph": "━", "color": Color(0.353, 0.647, 0.776, 1.0), "label": "channel / confluence"},
{"glyph": "◎", "color": Color(0.353, 0.647, 0.776, 1.0), "label": "sea mouth"},
],
},
{
"overlay_id": "gen_l1_basins",
"title": "DRAINAGE BASINS — L1",
"rows": [
{"glyph": "▭", "color": Color(0.45, 0.65, 0.85, 0.70), "label": "basin fill + boundary"},
],
},
{
"overlay_id": "gen_l1_attractors",
"title": "ATTRACTORS — L1 (shape = type)",
"rows": [
{"glyph": "●", "color": Color.TRANSPARENT, "label": "river mouth"},
{"glyph": "◑", "color": Color.TRANSPARENT, "label": "coastal access"},
{"glyph": "◆", "color": Color.TRANSPARENT, "label": "river crossing"},
{"glyph": "▼", "color": Color.TRANSPARENT, "label": "valley floor"},
{"glyph": "▲", "color": Color.TRANSPARENT, "label": "pass entrance"},
{"glyph": "○", "color": Color.TRANSPARENT, "label": "lake shore"},
{"glyph": "■", "color": Color.TRANSPARENT, "label": "plain center"},
],
},
{
"overlay_id": "gen_l1_attractors",
"title": "SUB-BIOME — L1 (color)",
"rows": [
{"glyph": "●", "color": Color(0.25, 0.72, 0.65, 0.90), "label": "tropical / coastal"},
{"glyph": "●", "color": Color(0.45, 0.68, 0.45, 0.90), "label": "temperate"},
{"glyph": "●", "color": Color(0.78, 0.62, 0.35, 0.90), "label": "arid"},
{"glyph": "●", "color": Color(0.52, 0.58, 0.72, 0.90), "label": "alpine"},
{"glyph": "●", "color": Color(0.40, 0.60, 0.52, 0.90), "label": "wetland"},
{"glyph": "●", "color": Color(0.55, 0.68, 0.82, 0.90), "label": "cold"},
],
},
{
"overlay_id": "gen_region_grid",
"title": "REGION CLIMATE — mean temperature (~205 km cells)",
"rows": [
{"glyph": "▦", "color": COLOR_REGION_TEMP_COLD_GEN, "label": "cold"},
{"glyph": "▦", "color": COLOR_REGION_TEMP_HOT_GEN, "label": "hot"},
{"glyph": "▦", "color": Color.TRANSPARENT, "label": "airless — no reading (skipped)"},
],
},
{
"overlay_id": "gen_district",
"title": "DISTRICT MORPHOLOGY — coarse",
"rows": [
{"glyph": "▦", "color": Color.TRANSPARENT, "label": "terrain-colored fill (17 zones)"},
],
},
{
"overlay_id": "gen_l2_roads",
"title": "ROADS/RAIL — L2 (color = authority)",
"rows": [
{"glyph": "━", "color": COLOR_ROAD_ADMINISTRATIVE, "label": "administrative"},
{"glyph": "━", "color": COLOR_ROAD_CORPORATE, "label": "corporate"},
{"glyph": "━", "color": COLOR_ROAD_COMMUNAL, "label": "communal"},
{"glyph": "━", "color": COLOR_ROAD_TRADE, "label": "trade"},
{"glyph": "━", "color": COLOR_ROAD_ABANDONED, "label": "abandoned"},
],
},
{
"overlay_id": "gen_l2_roads",
"title": "LINE STYLE (road vs rail)",
"rows": [
{"glyph": "━", "color": Color.TRANSPARENT, "label": "road (solid)"},
{"glyph": "┄", "color": Color.TRANSPARENT, "label": "rail (dashed)"},
{"glyph": "◇", "color": Color.TRANSPARENT, "label": "junction (3+ ways)"},
],
},
{
"overlay_id": "gen_l3_settlements",
"title": "SETTLEMENTS — L3 (size = population)",
"rows": [
{"glyph": "●", "color": COLOR_SETTLEMENT_GEN, "label": "settlement"},
{"glyph": "★", "color": COLOR_SETTLEMENT_CAPITAL_GEN, "label": "capital / major hub"},
],
},
{
"overlay_id": "gen_l4_quarters",
"title": "QUARTER FOOTPRINT — L4 (color = density, shape = dominant type)",
"rows": [
{"glyph": "▪", "color": COLOR_QUARTER_LOW_DENSITY_GEN, "label": "low density"},
{"glyph": "▪", "color": COLOR_QUARTER_HIGH_DENSITY_GEN, "label": "high density"},
{"glyph": "◪", "color": Color.TRANSPARENT, "label": "commercial (corner tab, top-right)"},
{"glyph": "◩", "color": Color.TRANSPARENT, "label": "industrial (corner tab, bottom-right)"},
{"glyph": "◈", "color": Color.TRANSPARENT, "label": "administrative (diamond cutout)"},
],
},
]
var _viewer = null # AtlasViewer (untyped to avoid cyclic ref)
func _init(viewer_ref = null) -> void:
_viewer = viewer_ref
custom_minimum_size.x = LEGEND_PANEL_WIDTH
mouse_filter = Control.MOUSE_FILTER_IGNORE
visible = false
func reposition() -> void:
position = Vector2(PANEL_MARGIN, 60.0)
## Rebuilds from GENERATION_LEGEND, showing only the sections whose overlay is
## currently toggled on — invisible when no generation overlay is active,
## updates every time AtlasViewer.set_overlay_visible() runs.
func refresh() -> void:
if _viewer == null:
return
var active_specs: Array = []
for spec: Dictionary in GENERATION_LEGEND:
if _viewer.is_overlay_visible(str(spec.get("overlay_id", ""))):
active_specs.append(spec)
clear()
visible = not active_specs.is_empty()
if active_specs.is_empty():
reset_to_content_size()
return
add_component(
ImplantHeader.new("GENERATION LEGEND", "%d layer(s) active" % active_specs.size())
)
add_component(ImplantSeparator.new())
for i: int in range(active_specs.size()):
var spec: Dictionary = active_specs[i]
add_component(ImplantTextBlock.new(str(spec.get("title", ""))))
for row: Dictionary in spec.get("rows", []):
var text: String = "%s %s" % [str(row.get("glyph", "-")), str(row.get("label", ""))]
add_component(ImplantDataRow.new(text, row.get("color", Color.TRANSPARENT)))
if i < active_specs.size() - 1:
add_component(ImplantSeparator.new())
reposition()
reset_to_content_size()