Bibliothek: 96-px-Kacheln, Kategorie-Gruppen, klappbar + gemerkt, Suche (t_59fc0f21)

This commit is contained in:
Hermes (scopedvr-dev) 2026-10-19 18:49:21 +00:00
parent 91e2b03843
commit 43636c840f
4 changed files with 489 additions and 19 deletions

View file

@ -102,6 +102,26 @@ const GROUP_IDS := {
"pilz_shiitake", "pilz_morchel", "pilz_austernseitling"], "pilz_shiitake", "pilz_morchel", "pilz_austernseitling"],
} }
## Karten-Übersicht (t_59fc0f21): Die feste Gruppenliste oben deckt nur die
## kuratierten Alt-/Natur-Objekte ab — der 347-Objekt-Katalog bringt seine
## eigene Kategorie mit (Manifest-Feld `kategorie`, siehe library/objects.json).
## Diese Zuordnung zeigt Pack-/Pipeline-Objekte in sinnvollen Gruppen statt
## alles in einer Mega-„Sonstiges"-Gruppe abzulegen (das war der eigentliche
## Übersichts-Killer). Reihenfolge = Anzeige-Reihenfolge nach GROUP_ORDER.
const KAT_ORDER := ["mauern", "barrieren", "gebaeude", "requisiten", "fahrzeuge", "natur_uebrig"]
const KAT_GROUPS := {
"walls": {"key": "mauern", "label": "Mauern"},
"barriers": {"key": "barrieren", "label": "Barrieren"},
"buildings": {"key": "gebaeude", "label": "Gebäude"},
"props": {"key": "requisiten", "label": "Requisiten"},
"vehicles": {"key": "fahrzeuge", "label": "Fahrzeuge"},
"nature": {"key": "natur_uebrig", "label": "Natur (übrige)"},
}
## Anzeige-Größe der Vorschaubilder im Browser (Karte t_59fc0f21): vorher 40 px,
## jetzt Kacheln mit 96 px (128-px-PNGs werden auf die Anzeige-Größe skaliert).
const THUMB_DISPLAY_PX := 96
static func object_label(oid: String) -> String: static func object_label(oid: String) -> String:
return NAMES.get(oid, oid.capitalize()) return NAMES.get(oid, oid.capitalize())
@ -115,6 +135,29 @@ static func group_of(oid: String) -> String:
return "sonstiges" return "sonstiges"
## Gruppe eines Katalog-Eintrags (t_59fc0f21): erst die festen Gruppen (oben),
## dann das Manifest-Feld `kategorie` (KAT_GROUPS), sonst "sonstiges".
## entry darf leer sein (z. B. fremde ID ohne Manifest-Eintrag).
static func group_of_entry(oid: String, entry: Dictionary) -> String:
var fixed := group_of(oid)
if fixed != "sonstiges":
return fixed
var kat := str(entry.get("kategorie", ""))
if KAT_GROUPS.has(kat):
return str(KAT_GROUPS[kat]["key"])
return "sonstiges"
## Deutscher Anzeigename: Manifest-Feld `name_de` (Pipeline-Objekte), sonst
## NAMES/fallback.ALLE Pipeline-Objekte zeigen damit ihren echten Namen statt
## des großgeschriebenen IDs (Karte t_59fc0f21).
static func entry_label(oid: String, entry: Dictionary) -> String:
var name_de := str(entry.get("name_de", ""))
if not name_de.is_empty():
return name_de
return object_label(oid)
## Gruppen für den Browser, geprüft gegen das Manifest: nur IDs, die es in ## Gruppen für den Browser, geprüft gegen das Manifest: nur IDs, die es in
## der objects.json WIRKLICH gibt, landen in der Liste (und unbekannte, ## der objects.json WIRKLICH gibt, landen in der Liste (und unbekannte,
## noch nicht gruppierte IDs wandern nach "Sonstiges" — neue Bibliotheks- ## noch nicht gruppierte IDs wandern nach "Sonstiges" — neue Bibliotheks-
@ -123,20 +166,33 @@ static func groups_in_order(manifest: Dictionary) -> Array:
var lib: Dictionary = manifest.get("objects", {}) var lib: Dictionary = manifest.get("objects", {})
var by_group := {} var by_group := {}
for oid in lib.keys(): for oid in lib.keys():
var key := group_of(str(oid)) var entry: Dictionary = lib.get(oid, {}) as Dictionary
var key := group_of_entry(str(oid), entry)
if not by_group.has(key): if not by_group.has(key):
by_group[key] = [] by_group[key] = []
(by_group[key] as Array).append(str(oid)) (by_group[key] as Array).append(str(oid))
var out: Array = [] var out: Array = []
for key in GROUP_ORDER: # Reihenfolge (t_59fc0f21): Kategorie-Gruppen zuerst (347-Objekt-Katalog ist
# überwiegend Pipeline-Objekte — Mauern/Gebäude/Requisiten oben), danach die
# kuratierten Alt-/Natur-Gruppen, "Sonstiges" immer ganz hinten.
for key in KAT_ORDER + GROUP_ORDER:
var ids: Array = (by_group.get(key, []) as Array) var ids: Array = (by_group.get(key, []) as Array)
ids.sort() ids.sort()
if ids.is_empty() and key != "sonstiges": if ids.is_empty() and key != "sonstiges":
continue continue
out.append({"key": key, "label": GROUP_LABELS[key], "ids": ids}) out.append({"key": key, "label": _group_label(key), "ids": ids})
return out return out
static func _group_label(key: String) -> String:
if GROUP_LABELS.has(key):
return str(GROUP_LABELS[key])
for kat in KAT_GROUPS.values():
if str(kat["key"]) == key:
return str(kat["label"])
return key
## Thumbnail-Pfad (128-px-PNG, tools/gen_thumbs.gd erzeugt sie einmalig). ## Thumbnail-Pfad (128-px-PNG, tools/gen_thumbs.gd erzeugt sie einmalig).
static func thumb_path(oid: String) -> String: static func thumb_path(oid: String) -> String:
return "res://library/thumbs/%s.png" % oid return "res://library/thumbs/%s.png" % oid
@ -144,9 +200,11 @@ static func thumb_path(oid: String) -> String:
## Thumbnail als Textur laden — direkt über Image (kein Import-System nötig, ## Thumbnail als Textur laden — direkt über Image (kein Import-System nötig,
## geht damit auch frisch nach git pull ohne Import), auf Anzeige-Größe ## geht damit auch frisch nach git pull ohne Import), auf Anzeige-Größe
## runtergerechnet. null = noch kein Thumbnail → Button fällt aufs Text-Layout ## runtergerechnet. size_px steuert die Anzeige-Größe (Default = THUMB_DISPLAY_PX
## zurück (Fehler sieht man nicht, man sieht einfach nur kein Bild). ## = 96, Karte t_59fc0f21; vorher fest 40). null = noch kein Thumbnail →
static func load_thumb(oid: String, size_px := 40) -> Texture2D: ## Button fällt aufs Text-Layout zurück (Fehler sieht man nicht, man sieht
## einfach nur kein Bild).
static func load_thumb(oid: String, size_px := THUMB_DISPLAY_PX) -> Texture2D:
var path := thumb_path(oid) var path := thumb_path(oid)
if not FileAccess.file_exists(path): if not FileAccess.file_exists(path):
return null return null

View file

@ -48,6 +48,15 @@ var view_builder: Node3D
var camera: Camera3D var camera: Camera3D
var library_box: VBoxContainer var library_box: VBoxContainer
var library_buttons: Dictionary = {} # id → Button var library_buttons: Dictionary = {} # id → Button
## Bibliothek-Gruppen (t_59fc0f21): Klapp-Zustand + Zeilen für Suche/Alle auf/zu.
var library_search: LineEdit
var library_open_all_btn: Button
var library_close_all_btn: Button
var _library_group_open: Dictionary = {} # Gruppen-Key → auf/zu
var _library_group_rows: Array = [] # {key, header, box, ids}
var _library_bold_font: Font = null
const LIBRARY_CFG_SECTION := "library" # Abschnitt in user://scopedvr_editor.cfg
var undo_btn: Button var undo_btn: Button
var redo_btn: Button var redo_btn: Button
var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014) var grid_btn: Button # „⊞ Gitter“ (Karte t_7ac70014)
@ -433,6 +442,33 @@ func _build_left_panel() -> PanelContainer:
vbox.add_child(_header_label("Objekt-Bibliothek")) vbox.add_child(_header_label("Objekt-Bibliothek"))
vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis ESC (Rechte Maustaste = Flug).")) vbox.add_child(_muted_label("Objekt anklicken, dann in die Karte klicken — der Modus bleibt an bis ESC (Rechte Maustaste = Flug)."))
# Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit
# Treffern klappen auf, leere Gruppen verschwinden.
library_search = LineEdit.new()
library_search.placeholder_text = "Suchen (Name oder ID)…"
library_search.clear_button_enabled = true
library_search.custom_minimum_size = Vector2(0, 36)
library_search.text_changed.connect(_apply_library_filter)
library_search.text_submitted.connect(func(_t: String) -> void: library_search.release_focus())
vbox.add_child(library_search)
# „Alle auf" / „Alle zu" (t_59fc0f21): Klapp-Zustand aller Gruppen auf einmal.
var group_btns := HBoxContainer.new()
group_btns.add_theme_constant_override("separation", 6)
library_open_all_btn = Button.new()
library_open_all_btn.text = "Alle auf"
library_open_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
library_open_all_btn.tooltip_text = "Klappt ALLE Gruppen auf"
library_open_all_btn.pressed.connect(_on_library_expand_all)
group_btns.add_child(library_open_all_btn)
library_close_all_btn = Button.new()
library_close_all_btn.text = "Alle zu"
library_close_all_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
library_close_all_btn.tooltip_text = "Klappt ALLE Gruppen zu"
library_close_all_btn.pressed.connect(_on_library_collapse_all)
group_btns.add_child(library_close_all_btn)
vbox.add_child(group_btns)
var scroll := ScrollContainer.new() var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.custom_minimum_size = Vector2(0, 280) scroll.custom_minimum_size = Vector2(0, 280)
@ -478,42 +514,184 @@ func _build_left_panel() -> PanelContainer:
func _fill_library() -> void: func _fill_library() -> void:
library_buttons.clear() library_buttons.clear()
# Gruppen-Browser (Karte t_3a998ae4): Klapp-Überschriften je Gruppe, _library_group_rows.clear()
# darunter die Objekte mit Thumbnail (128-px-Render, runterskaliert). _library_group_open = _library_load_open_states()
# Gruppen-Browser (Karte t_3a998ae4, Überarbeitung t_59fc0f21):
# - Klapp-Überschriften je Gruppe (fett, 18 px, Name + Anzahl),
# - STANDARD EINGEKLAPPT — nur die erste Gruppe ist offen (Übersicht!),
# - Klapp-Zustand je Gruppe wird in user://scopedvr_editor.cfg gemerkt,
# - Objekte als Kacheln mit 96-px-Vorschaubild (2 Spalten je Gruppe).
var first_open: String = ""
for group in LibraryBrowser.groups_in_order(manifest): for group in LibraryBrowser.groups_in_order(manifest):
var g_key: String = group["key"]
var g_label: String = group["label"] var g_label: String = group["label"]
var g_ids: Array = group["ids"] var g_ids: Array = group["ids"]
if g_ids.is_empty(): if g_ids.is_empty():
continue continue
if first_open.is_empty():
first_open = g_key
var is_open: bool = _library_group_open.get(g_key, g_key == first_open)
_library_group_open[g_key] = is_open
var header := Button.new() var header := Button.new()
header.text = "▾ %s (%d)" % [g_label, g_ids.size()] header.text = _library_header_text(is_open, g_label, g_ids.size())
header.toggle_mode = true header.toggle_mode = true
header.button_pressed = true header.button_pressed = is_open
header.alignment = HORIZONTAL_ALIGNMENT_LEFT header.alignment = HORIZONTAL_ALIGNMENT_LEFT
header.custom_minimum_size = Vector2(240, 40) header.custom_minimum_size = Vector2(0, 34)
header.tooltip_text = "Gruppe „%s“ auf-/zuklappen" % g_label
header.add_theme_font_size_override("font_size", 18)
header.add_theme_font_override("font", _library_header_font())
library_box.add_child(header) library_box.add_child(header)
var group_box := VBoxContainer.new() var group_box := VBoxContainer.new()
group_box.add_theme_constant_override("separation", 4) group_box.add_theme_constant_override("separation", 4)
group_box.visible = is_open
var grid := GridContainer.new()
grid.columns = 2
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
grid.add_theme_constant_override("h_separation", 6)
grid.add_theme_constant_override("v_separation", 6)
group_box.add_child(grid)
library_box.add_child(group_box) library_box.add_child(group_box)
header.toggled.connect(func(on: bool) -> void: header.toggled.connect(_on_library_group_toggled.bind(g_key, g_label, g_ids.size(), header, group_box))
group_box.visible = on
header.text = "%s %s (%d)" % ["▾" if on else "▸", g_label, g_ids.size()])
for oid in g_ids: for oid in g_ids:
_add_library_button(str(oid), group_box) _add_library_button(str(oid), grid)
_library_group_rows.append({"key": g_key, "header": header, "box": group_box, "ids": g_ids, "label": g_label, "count": g_ids.size()})
_apply_library_filter(library_search.text)
func _library_header_text(on: bool, label: String, count: int) -> String:
return "%s %s (%d)" % ["▾" if on else "▸", label, count]
func _on_library_group_toggled(on: bool, g_key: String, g_label: String, count: int, header: Button, group_box: VBoxContainer) -> void:
group_box.visible = on
header.text = _library_header_text(on, g_label, count)
_library_group_open[g_key] = on
_library_save_open_state(g_key, on)
func _on_library_expand_all() -> void:
for row in _library_group_rows:
(row["header"] as Button).button_pressed = true
func _on_library_collapse_all() -> void:
for row in _library_group_rows:
(row["header"] as Button).button_pressed = false
func _library_header_font() -> Font:
if _library_bold_font == null:
var sf := SystemFont.new()
sf.font_names = PackedStringArray(["Noto Sans", "DejaVu Sans", "Ubuntu", "sans-serif"])
sf.font_weight = 700
_library_bold_font = sf
return _library_bold_font
func _library_load_open_states() -> Dictionary:
var cfg := ConfigFile.new()
cfg.load("user://scopedvr_editor.cfg")
var out := {}
if cfg.has_section(LIBRARY_CFG_SECTION):
for key in cfg.get_section_keys(LIBRARY_CFG_SECTION):
out[str(key)] = bool(cfg.get_value(LIBRARY_CFG_SECTION, str(key), true))
return out
func _library_save_open_state(g_key: String, on: bool) -> void:
var cfg := ConfigFile.new()
cfg.load("user://scopedvr_editor.cfg")
cfg.set_value(LIBRARY_CFG_SECTION, g_key, on)
cfg.save("user://scopedvr_editor.cfg")
## Live-Suche (t_59fc0f21): leer = alles normal sichtbar (Klapp-Zustand wie
## gemerkt); Text = nur Treffer (Name de/en, ID — case-insensitive), Gruppen
## mit Treffern bleiben offen, leere verschwinden.
func _apply_library_filter(query: String) -> void:
var q := query.strip_edges().to_lower()
var searching := not q.is_empty()
for row in _library_group_rows:
var ids: Array = row["ids"]
var any := false
for oid in ids:
var btn := library_buttons.get(str(oid)) as Button
if btn == null:
continue
var vis := true
if searching:
vis = _library_oid_matches(str(oid), q)
btn.visible = vis
any = any or vis
var header := row["header"] as Button
var box := row["box"] as VBoxContainer
header.visible = any
if searching:
# Treffer-Gruppen bleiben auf — auch wenn sie zugeklappt waren
# (Pfeil zeigt dann offen, kosmetisch konsistent).
box.visible = any
if any:
header.text = _library_header_text(true, str(row["label"]), int(row["count"]))
else:
var is_open := bool(_library_group_open.get(str(row["key"]), false))
box.visible = is_open
header.text = _library_header_text(is_open, str(row["label"]), int(row["count"]))
func _library_oid_matches(oid: String, q: String) -> bool:
if oid.to_lower().contains(q):
return true
var entry: Dictionary = (manifest.get("objects", {}) as Dictionary).get(oid, {})
if LibraryBrowser.entry_label(oid, entry).to_lower().contains(q):
return true
if LibraryBrowser.object_label(oid).to_lower().contains(q):
return true
return false
func _add_library_button(oid: String, parent: Container) -> void: func _add_library_button(oid: String, parent: Container) -> void:
var lib: Dictionary = manifest.get("objects", {}) var lib: Dictionary = manifest.get("objects", {})
var entry: Dictionary = lib.get(oid, {}) var entry: Dictionary = lib.get(oid, {})
var poly := int(entry.get("poly", 0)) var poly := int(entry.get("poly", 0))
var name_txt := LibraryBrowser.entry_label(oid, entry)
var btn := Button.new() var btn := Button.new()
btn.text = "%s · %d Polys" % [LibraryBrowser.object_label(oid), poly]
btn.icon = LibraryBrowser.load_thumb(oid)
btn.custom_minimum_size = Vector2(240, 44)
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
btn.toggle_mode = true btn.toggle_mode = true
btn.button_pressed = false btn.button_pressed = false
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
btn.custom_minimum_size = Vector2(0, 126)
btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid]
btn.pressed.connect(_on_library_pressed.bind(oid)) btn.pressed.connect(_on_library_pressed.bind(oid))
var tex := LibraryBrowser.load_thumb(oid)
if tex == null:
# Kein Thumbnail → Text-Zeile wie früher (Fehlertoleranz, t_3a998ae4).
btn.text = "%s · %d Polys" % [name_txt, poly]
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
btn.custom_minimum_size = Vector2(0, 44)
else:
# Kachel (t_59fc0f21): 96-px-Vorschaubild oben, Name klein darunter.
# Kinder ignorieren die Maus, damit die Klicks am Button ankommen.
var tile := VBoxContainer.new()
tile.mouse_filter = Control.MOUSE_FILTER_IGNORE
tile.set_anchors_preset(Control.PRESET_FULL_RECT)
tile.add_theme_constant_override("separation", 2)
btn.add_child(tile)
var tr := TextureRect.new()
tr.texture = tex
tr.custom_minimum_size = Vector2(LibraryBrowser.THUMB_DISPLAY_PX, LibraryBrowser.THUMB_DISPLAY_PX)
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
tile.add_child(tr)
var lbl := Label.new()
lbl.text = name_txt
lbl.add_theme_font_size_override("font_size", 11)
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.clip_text = true
lbl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
tile.add_child(lbl)
parent.add_child(btn) parent.add_child(btn)
library_buttons[oid] = btn library_buttons[oid] = btn

View file

@ -0,0 +1,233 @@
extends SceneTree
## Editor-Test (headless): Bibliotheks-Browser-UI — Karte t_59fc0f21.
## Ausführen: godot --headless --path . --script res://tests/test_library_browser_ui.gd
## Exit 0 = alles grün.
##
## Instanziert die ECHTE Main-UI (Muster tests/test_gui_smoke.gd) und prüft:
## 1. Kacheln: Thumbnail-TextureRect ≥ 64 px (Vorgabe 96), 2-Spalten-Grid,
## deutscher Name (name_de) unter dem Bild.
## 2. Gruppen: Standard eingeklappt (nur erste Gruppe offen), Header fett/18 px
## mit Name + Anzahl, Manifest-Deckung (alle Objekte als Button).
## 3. Klapp-Zustand wird in user://scopedvr_editor.cfg gemerkt und nach
## Neustart (Neu-Instanz) wiederhergestellt.
## 4. Suche: „pilz" filtert live (nur Pilze-Gruppe sichtbar), Leeren stellt
## den gemerkten Klapp-Zustand wieder her.
## 5. „Alle auf" / „Alle zu" klappen alles um.
var failures := 0
var checks := 0
var main: Control
func _initialize() -> void:
_run_all.call_deferred()
func _run_all() -> void:
_clear_library_cfg_section()
var scene: PackedScene = load("res://scenes/main.tscn")
_check(scene != null, "scenes/main.tscn laedt")
if scene == null:
_finish()
return
main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
_test_built()
_test_default_collapsed()
_test_tiles()
_test_search()
await _test_persistence()
_test_all_buttons()
_finish()
func _finish() -> void:
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)
func _check(cond: bool, label: String) -> void:
checks += 1
if cond:
print(" ok: %s" % label)
else:
failures += 1
print(" FAIL: %s" % label)
func _clear_library_cfg_section() -> void:
# Nur UNSEREN Abschnitt leeren — [test]/[update] der anderen Features bleiben.
var cfg := ConfigFile.new()
cfg.load("user://scopedvr_editor.cfg")
if cfg.has_section("library"):
cfg.erase_section("library")
cfg.save("user://scopedvr_editor.cfg")
func _group_row(g_key: String) -> Dictionary:
for row in main._library_group_rows:
if str(row["key"]) == g_key:
return row
return {}
func _test_built() -> void:
print("Testgruppe: UI-Bau (Suchfeld, Alle auf/zu, Gruppen)")
_check(main.library_search != null, "Suchfeld gebaut")
_check(main.library_open_all_btn != null and main.library_close_all_btn != null,
"Alle auf/Alle zu gebaut")
_check(main._library_group_rows.size() >= 8,
"Gruppen gebaut (%d)" % main._library_group_rows.size())
var lib: Dictionary = main.manifest.get("objects", {})
_check(main.library_buttons.size() == lib.size(),
"Manifest-Deckung: %d/%d Objekte als Button" % [main.library_buttons.size(), lib.size()])
func _test_default_collapsed() -> void:
print("Testgruppe: Standard eingeklappt (nur erste Gruppe offen)")
var open_count := 0
var first_key := ""
for row in main._library_group_rows:
var box := row["box"] as VBoxContainer
if box.visible:
open_count += 1
if first_key.is_empty():
first_key = str(row["key"])
_check(open_count == 1, "genau 1 Gruppe offen (ist %d)" % open_count)
_check(not first_key.is_empty(), "eine Gruppe ist offen (%s)" % first_key)
# Header: 18 px Font, Text enthält Name + Anzahl.
var row0: Dictionary = main._library_group_rows[0]
var header := row0["header"] as Button
_check(header.get_theme_font_size("font_size") >= 18, "Header-Schrift ≥ 18 px")
_check(header.text.contains("(%d)" % int(row0["count"])), "Header zeigt Anzahl (%s)" % header.text)
# Alle Boxen außer der ersten sind zu — trotz 347 Objekten klein.
var hidden_ok := true
for row in main._library_group_rows:
if str(row["key"]) != first_key and not (row["box"] as VBoxContainer).visible:
continue
elif str(row["key"]) == first_key and (row["box"] as VBoxContainer).visible:
continue
hidden_ok = false
_check(hidden_ok, "übrige Gruppen zugeklappt")
func _test_tiles() -> void:
print("Testgruppe: Kacheln (96-px-Vorschaubild + Name)")
var tex := LibraryBrowser.load_thumb("crate_wood")
_check(tex != null and tex.get_width() >= 96, "load_thumb liefert ≥ 96 px (ist %s)"
% (str(tex.get_width()) if tex != null else "null"))
var tile_btn := main.library_buttons.get("watchtower_wood") as Button
_check(tile_btn != null, "Kachel-Button existiert")
if tile_btn != null:
var tr: TextureRect = null
var lbl: Label = null
for child in tile_btn.get_children():
for sub in (child as Control).get_children():
if sub is TextureRect:
tr = sub
elif sub is Label:
lbl = sub
_check(tr != null, "TextureRect in Kachel")
_check(lbl != null, "Name-Label in Kachel")
if tr != null:
_check(tr.custom_minimum_size.x >= 64, "Thumbnail-Zielgröße ≥ 64 px (ist %d)"
% int(tr.custom_minimum_size.x))
_check(tr.size.x >= 64, "Thumbnail rendert ≥ 64 px breit (ist %.0f)" % tr.size.x)
_check(tr.texture != null and tr.texture.get_width() == 96,
"Textur auf 96 px skaliert")
if lbl != null:
_check(lbl.text == "Wachturm", "deutscher Name unter dem Bild (%s)" % lbl.text)
_check(tile_btn.size.y >= 100, "Kachel hoch genug (%.0f px)" % tile_btn.size.y)
func _test_search() -> void:
print("Testgruppe: Suche filtert live")
# „pilz" → nur die Pilze-Gruppe (6 Objekte) sichtbar.
main.library_search.text = "pilz"
main._apply_library_filter("pilz")
var visible_headers := 0
var pilze_row := _group_row("pilze")
for row in main._library_group_rows:
if (row["header"] as Button).visible:
visible_headers += 1
_check(visible_headers == 1, "nur 1 Gruppen-Header sichtbar (ist %d)" % visible_headers)
var visible_tiles := 0
for oid in pilze_row["ids"]:
if (main.library_buttons.get(str(oid)) as Button).visible:
visible_tiles += 1
_check(visible_tiles == 6, "6 Pilz-Kacheln sichtbar (ist %d)" % visible_tiles)
_check((pilze_row["box"] as VBoxContainer).visible, "Treffer-Gruppe offen")
# ID-Suche funktioniert auch: „watchtower" (ID) → Treffer in bauwerke.
main._apply_library_filter("watchtower")
var wt := main.library_buttons.get("watchtower_wood") as Button
_check(wt != null and wt.visible, "ID-Suche „watchtower“ findet Wachturm")
# Leeren → gemerkter Zustand wiederhergestellt (nur erste Gruppe offen).
main.library_search.text = ""
main._apply_library_filter("")
var open_count := 0
for row in main._library_group_rows:
if (row["box"] as VBoxContainer).visible:
open_count += 1
_check(open_count == 1, "nach Leeren wieder 1 Gruppe offen (ist %d)" % open_count)
func _test_persistence() -> void:
print("Testgruppe: Klapp-Zustand überlebt Neustart (cfg)")
# Fahrzeuge-Gruppe aufklappen → cfg speichert.
var row := _group_row("fahrzeuge")
_check(not row.is_empty(), "Gruppe fahrzeuge existiert")
if row.is_empty():
return
var header := row["header"] as Button
header.button_pressed = true
await process_frame
var cfg := ConfigFile.new()
cfg.load("user://scopedvr_editor.cfg")
_check(cfg.get_value("library", "fahrzeuge", null) == true,
"cfg [library] fahrzeuge=true nach Aufklappen")
# „Neustart": Instanz wegwerfen, neu bauen — Zustand muss zurückkommen.
# (queue_free + Frames: free() während laufender Frame-Arbeit ist gesperrt.)
main.queue_free()
await process_frame
await process_frame
main = (load("res://scenes/main.tscn") as PackedScene).instantiate()
root.add_child(main)
await process_frame
await process_frame
var row2 := _group_row("fahrzeuge")
_check(not row2.is_empty() and (row2["box"] as VBoxContainer).visible,
"nach Neu-Instanz ist fahrzeuge wieder offen")
# Und wieder zu machen — Zustand bleibt gemerkt (false).
(_group_row("fahrzeuge")["header"] as Button).button_pressed = false
await process_frame
var cfg2 := ConfigFile.new()
cfg2.load("user://scopedvr_editor.cfg")
_check(cfg2.get_value("library", "fahrzeuge", null) == false,
"cfg [library] fahrzeuge=false nach Zuklappen")
func _test_all_buttons() -> void:
print("Testgruppe: Alle auf / Alle zu")
main._on_library_collapse_all()
var open_count := 0
for row in main._library_group_rows:
if (row["box"] as VBoxContainer).visible:
open_count += 1
_check(open_count == 0, "Alle zu: 0 Gruppen offen (ist %d)" % open_count)
main._on_library_expand_all()
open_count = 0
for row in main._library_group_rows:
if (row["box"] as VBoxContainer).visible:
open_count += 1
_check(open_count == main._library_group_rows.size(),
"Alle auf: alle %d Gruppen offen (ist %d)" % [main._library_group_rows.size(), open_count])
# Aufräumen für die nächsten Läufe: wieder zu.
main._on_library_collapse_all()

View file

@ -0,0 +1 @@
uid://sgkivq27rfuj