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.
118 lines
4.5 KiB
GDScript
118 lines
4.5 KiB
GDScript
@tool
|
|
class_name ImplantPanel
|
|
extends PanelContainer
|
|
## Root container for any implant UI overlay (D-169).
|
|
## Add ImplantHeader, ImplantSeparator, ImplantDataRow, ImplantTextBlock
|
|
## as children — they auto-style from the shared theme.
|
|
|
|
@export var theme_resource: ImplantTheme:
|
|
set(value):
|
|
theme_resource = value
|
|
_apply_theme()
|
|
|
|
var _vbox: VBoxContainer
|
|
|
|
|
|
func _ready() -> void:
|
|
# Constrain panel to its custom_minimum_size width
|
|
size_flags_horizontal = Control.SIZE_SHRINK_END
|
|
clip_contents = true
|
|
|
|
_vbox = VBoxContainer.new()
|
|
_vbox.name = "Content"
|
|
add_child(_vbox)
|
|
_apply_theme()
|
|
|
|
|
|
func _apply_theme() -> void:
|
|
if not theme_resource:
|
|
return
|
|
|
|
# Panel background
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = theme_resource.panel_bg
|
|
style.border_color = theme_resource.separator
|
|
style.border_width_top = int(theme_resource.border_width)
|
|
style.border_width_bottom = int(theme_resource.border_width)
|
|
style.border_width_left = int(theme_resource.border_width)
|
|
style.border_width_right = int(theme_resource.border_width)
|
|
style.content_margin_top = theme_resource.padding
|
|
style.content_margin_bottom = theme_resource.padding
|
|
style.content_margin_left = theme_resource.padding
|
|
style.content_margin_right = theme_resource.padding
|
|
add_theme_stylebox_override("panel", style)
|
|
|
|
# VBox spacing
|
|
if _vbox:
|
|
_vbox.add_theme_constant_override("separation", 0)
|
|
|
|
# Propagate theme to existing children
|
|
for child in get_implant_children():
|
|
if child.has_method("apply_implant_theme"):
|
|
child.apply_implant_theme(theme_resource)
|
|
|
|
|
|
## Add an implant component to the panel.
|
|
func add_component(component: Control) -> void:
|
|
if not _vbox:
|
|
_vbox = VBoxContainer.new()
|
|
_vbox.name = "Content"
|
|
add_child(_vbox)
|
|
_vbox.add_child(component)
|
|
if theme_resource and component.has_method("apply_implant_theme"):
|
|
component.apply_implant_theme(theme_resource)
|
|
|
|
|
|
## Get all implant component children.
|
|
func get_implant_children() -> Array:
|
|
if not _vbox:
|
|
return []
|
|
return _vbox.get_children()
|
|
|
|
|
|
## Clear all components. Frees IMMEDIATELY (remove_child() + free()), not
|
|
## via queue_free() — a caller that re-enters refresh()-shaped clear()+
|
|
## add_component() more than once in the SAME frame (confirmed live: the
|
|
## Atlas regional-window legend stacking ~10x around cold-start entry, PR
|
|
## #192 cold-start dossier) would otherwise see get_implant_children() still
|
|
## returning the STALE children (queue_free() only removes them at end of
|
|
## frame), so add_component()'s new ones pile up alongside instead of
|
|
## replacing them. Immediate removal makes clear() genuinely synchronous —
|
|
## the panel can never observe more than one refresh() worth of content, no
|
|
## matter how many times it's called before the next frame.
|
|
func clear() -> void:
|
|
if not _vbox:
|
|
return
|
|
for child in _vbox.get_children():
|
|
_vbox.remove_child(child)
|
|
child.free()
|
|
|
|
|
|
## PR #192 cold-start round 2: call once at the END of a refresh() — AFTER
|
|
## clear() + every add_component() for the new content. A Control's `size`
|
|
## GROWS to fit a rising minimum size automatically (normal Container
|
|
## behavior) but never shrinks back down on its own once a taller stack of
|
|
## content has pushed it up (confirmed live: the same stacking window that
|
|
## caused the duplicate-children bug above ALSO left the panel's rect
|
|
## oversized — 260x2343px, ~10 legends tall — even after the children-count
|
|
## fix landed). This panel is manually positioned (not itself inside a
|
|
## parent Container), so nothing else forces that re-measure.
|
|
##
|
|
## DEFERRED, not a direct reset_size() call — confirmed directly
|
|
## (instrumented and reverted through several shapes before landing here):
|
|
## get_minimum_size() is momentarily WRONG for a frame or more after
|
|
## add_component() runs (RichTextLabel.fit_content's minimum-height
|
|
## computation is width-dependent and the control hasn't been laid out with
|
|
## its real width yet — the classic Godot "fit_content measures huge before
|
|
## its first real layout pass" gap), and — critically — a DIRECT reset_size()
|
|
## call that captures that transitional wrong value does NOT self-correct
|
|
## later even once get_minimum_size() itself settles down on its own:
|
|
## `size` stays stuck at whatever the premature reset_size() captured,
|
|
## because nothing re-triggers a growth pass for an already-`_ready()`'d
|
|
## panel afterward. call_deferred() runs at the END of the current frame's
|
|
## idle processing — after layout has had its chance to update
|
|
## get_minimum_size() — so by the time this actually executes, it reads the
|
|
## real, settled value.
|
|
func reset_to_content_size() -> void:
|
|
call_deferred("reset_size")
|