Bibliothek: 96-px-Kacheln, Kategorie-Gruppen, klappbar + gemerkt, Suche (t_59fc0f21)
This commit is contained in:
parent
91e2b03843
commit
43636c840f
4 changed files with 489 additions and 19 deletions
|
|
@ -102,6 +102,26 @@ const GROUP_IDS := {
|
|||
"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:
|
||||
return NAMES.get(oid, oid.capitalize())
|
||||
|
|
@ -115,6 +135,29 @@ static func group_of(oid: String) -> String:
|
|||
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
|
||||
## der objects.json WIRKLICH gibt, landen in der Liste (und unbekannte,
|
||||
## 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 by_group := {}
|
||||
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):
|
||||
by_group[key] = []
|
||||
(by_group[key] as Array).append(str(oid))
|
||||
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)
|
||||
ids.sort()
|
||||
if ids.is_empty() and key != "sonstiges":
|
||||
continue
|
||||
out.append({"key": key, "label": GROUP_LABELS[key], "ids": ids})
|
||||
out.append({"key": key, "label": _group_label(key), "ids": ids})
|
||||
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).
|
||||
static func thumb_path(oid: String) -> String:
|
||||
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,
|
||||
## geht damit auch frisch nach git pull ohne Import), auf Anzeige-Größe
|
||||
## runtergerechnet. null = noch kein Thumbnail → 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 := 40) -> Texture2D:
|
||||
## runtergerechnet. size_px steuert die Anzeige-Größe (Default = THUMB_DISPLAY_PX
|
||||
## = 96, Karte t_59fc0f21; vorher fest 40). null = noch kein Thumbnail →
|
||||
## 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)
|
||||
if not FileAccess.file_exists(path):
|
||||
return null
|
||||
|
|
|
|||
204
src/ui/main.gd
204
src/ui/main.gd
|
|
@ -48,6 +48,15 @@ var view_builder: Node3D
|
|||
var camera: Camera3D
|
||||
var library_box: VBoxContainer
|
||||
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 redo_btn: Button
|
||||
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(_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()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.custom_minimum_size = Vector2(0, 280)
|
||||
|
|
@ -478,42 +514,184 @@ func _build_left_panel() -> PanelContainer:
|
|||
|
||||
func _fill_library() -> void:
|
||||
library_buttons.clear()
|
||||
# Gruppen-Browser (Karte t_3a998ae4): Klapp-Überschriften je Gruppe,
|
||||
# darunter die Objekte mit Thumbnail (128-px-Render, runterskaliert).
|
||||
_library_group_rows.clear()
|
||||
_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):
|
||||
var g_key: String = group["key"]
|
||||
var g_label: String = group["label"]
|
||||
var g_ids: Array = group["ids"]
|
||||
if g_ids.is_empty():
|
||||
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()
|
||||
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.button_pressed = true
|
||||
header.button_pressed = is_open
|
||||
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)
|
||||
var group_box := VBoxContainer.new()
|
||||
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)
|
||||
header.toggled.connect(func(on: bool) -> void:
|
||||
group_box.visible = on
|
||||
header.text = "%s %s (%d)" % ["▾" if on else "▸", g_label, g_ids.size()])
|
||||
header.toggled.connect(_on_library_group_toggled.bind(g_key, g_label, g_ids.size(), header, group_box))
|
||||
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:
|
||||
var lib: Dictionary = manifest.get("objects", {})
|
||||
var entry: Dictionary = lib.get(oid, {})
|
||||
var poly := int(entry.get("poly", 0))
|
||||
var name_txt := LibraryBrowser.entry_label(oid, entry)
|
||||
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.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))
|
||||
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)
|
||||
library_buttons[oid] = btn
|
||||
|
||||
|
|
|
|||
233
tests/test_library_browser_ui.gd
Normal file
233
tests/test_library_browser_ui.gd
Normal 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()
|
||||
1
tests/test_library_browser_ui.gd.uid
Normal file
1
tests/test_library_browser_ui.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://sgkivq27rfuj
|
||||
Loading…
Reference in a new issue