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"],
|
"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
|
||||||
|
|
|
||||||
204
src/ui/main.gd
204
src/ui/main.gd
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
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